使用多个度量列将数据从长格式转换为宽格式

50

我在尝试将数据从长格式转换为宽格式时遇到了困难,特别是当我有多个要导入的度量变量时,最优雅和灵活的方法是什么,我还不确定。

例如,下面是一个简单的长格式数据框。其中ID表示主题,TIME是时间变量,XY是对IDTIME上进行的测量:

> my.df <- data.frame(ID=rep(c("A","B","C"), 5), TIME=rep(1:5, each=3), X=1:15, Y=16:30)
> my.df

   ID TIME  X  Y
1   A    1  1 16
2   B    1  2 17
3   C    1  3 18
4   A    2  4 19
5   B    2  5 20
6   C    2  6 21
7   A    3  7 22
8   B    3  8 23
9   C    3  9 24
10  A    4 10 25
11  B    4 11 26
12  C    4 12 27
13  A    5 13 28
14  B    5 14 29
15  C    5 15 30

如果我只想将TIME的值转换为包含X的列标题,我知道可以使用来自reshape包(或reshape2中的dcast())的cast()

> cast(my.df, ID ~ TIME, value="X")
  ID 1 2 3  4  5
1  A 1 4 7 10 13
2  B 2 5 8 11 14
3  C 3 6 9 12 15

但是我真正想做的是将Y作为另一个度量变量带来,并使列名反映出度量变量名称和时间值:

  ID X_1 X_2 X_3  X_4 X_5 Y_1 Y_2 Y_3 Y_4 Y_5
1  A   1   4   7   10  13  16  19  22  25  28
2  B   2   5   8   11  14  17  20  23  26  29
3  C   3   6   9   12  15  18  21  24  27  30

顺便一提,我并不在乎所有的X是先出现还是后跟着Y,或者它们是交替出现的,比如X_1Y_1X_2Y_2等。

通过两次将长数据转换(cast)并合并结果,我可以接近实现这一点,尽管需要调整列名,并且如果需要添加第三个或第四个变量,除了XY之外,我需要进行一些微调:

merge(
cast(my.df, ID ~ TIME, value="X"),
cast(my.df, ID ~ TIME, value="Y"),
by="ID", suffixes=c("_X","_Y")
)

看起来像是使用reshape2和/或plyr中的一些函数可以更优雅地完成这个任务,并且更清晰地处理多个测量变量。类似cast(my.df, ID ~ TIME, value=c("X","Y"))这样的操作,但它不是有效的。但我还没有找到解决方法。

6个回答

26

为了处理像您想要的这样的多个变量,您需要在转换之前 “melt”数据。

library("reshape2")

dcast(melt(my.df, id.vars=c("ID", "TIME")), ID~variable+TIME)

提供了

  ID X_1 X_2 X_3 X_4 X_5 Y_1 Y_2 Y_3 Y_4 Y_5
1  A   1   4   7  10  13  16  19  22  25  28
2  B   2   5   8  11  14  17  20  23  26  29
3  C   3   6   9  12  15  18  21  24  27  30

根据评论进行编辑:

数据框架

num.id = 10 
num.time=10 
my.df <- data.frame(ID=rep(LETTERS[1:num.id], num.time), 
                    TIME=rep(1:num.time, each=num.id), 
                    X=1:(num.id*num.time), 
                    Y=(num.id*num.time)+1:(2*length(1:(num.id*num.time))))

由于ID / TIME组合未指示唯一行,因此会产生不同的结果(所有条目都为2)。实际上,每个ID / TIME组合有两行。 reshape2假设每个变量可能的组合仅有一个值,并将应用摘要函数以创建单个变量,如果存在多个条目,则为其创建单个变量。这就是为什么会出现警告的原因。

Aggregation function missing: defaulting to length

如果你添加一个打破了冗余的变量,那么你就可以得到能够工作的东西。

my.df$cycle <- rep(1:2, each=num.id*num.time)
dcast(melt(my.df, id.vars=c("cycle", "ID", "TIME")), cycle+ID~variable+TIME)

这是因为cycle/ID/time现在唯一地定义了my.df中的一行。


1
我试图评估哪个解决方案最快,但是发现如果数据框架如下:num.id = 10,num.time=10,则您的代码存在问题。my.df <- data.frame(ID=rep(LETTERS[1:num.id], num.time), TIME=rep(1:num.time, each=num.id), X=1:(num.idnum.time), Y=(num.idnum.time)+1:(2length(1:(num.idnum.time)))) - Manoel Galdino
太好了,谢谢Brian。由于转换似乎有效,我没有意识到融化仍然是必要的。 - colonel.triq

23

这个回答比较老了,也许可以找一些最新的信息。

   reshape(my.df,
           idvar = "ID",
           timevar = "TIME",
           direction = "wide")

提供

  ID X.1 Y.1 X.2 Y.2 X.3 Y.3 X.4 Y.4 X.5 Y.5
1  A   1  16   4  19   7  22  10  25  13  28
2  B   2  17   5  20   8  23  11  26  14  29
3  C   3  18   6  21   9  24  12  27  15  30

17

使用 data.table_1.9.5,可以不使用 melt 完成此操作,因为它可以处理多个 value.var 列。您可以从这里安装它。

 library(data.table)
 dcast(setDT(my.df), ID~TIME, value.var=c('X', 'Y'))
 #   ID 1_X 2_X 3_X 4_X 5_X 1_Y 2_Y 3_Y 4_Y 5_Y
 #1:  A   1   4   7  10  13  16  19  22  25  28
 #2:  B   2   5   8  11  14  17  20  23  26  29
 #3:  C   3   6   9  12  15  18  21  24  27  30

谢谢您提供的解决方案。当我运行这段代码时,我收到了以下警告信息:请问有什么建议吗?聚合函数缺失,默认为'length'@akrun 这与我在帖子中的一个问题有关: https://stackoverflow.com/questions/67830282/complex-data-reshaping-into-wide-format-where-input-is-a-mix-of-long-and-wide-da - Sandy
1
@Sandy,这是因为您有重复的元素,所以 dcast 默认使用 length。也许您想要 dcast(setDT(my.df), ID + rowid(ID) ~ TIME, value.var = c("X", "Y")) - akrun

7

pivot_wider()函数是tidyr的第二代方法(在tidyr 1.0.0中发布)。

library(magrittr); requireNamespace("tidyr");

my.df %>%
  tidyr::pivot_wider(
    names_from  = c(TIME), # Can accommodate more variables, if needed.
    values_from = c(X, Y)
  )

结果:

# A tibble: 3 x 11
  ID      X_1   X_2   X_3   X_4   X_5   Y_1   Y_2   Y_3   Y_4   Y_5
  <fct> <int> <int> <int> <int> <int> <int> <int> <int> <int> <int>
1 A         1     4     7    10    13    16    19    22    25    28
2 B         2     5     8    11    14    17    20    23    26    29
3 C         3     6     9    12    15    18    21    24    27    30

这可能比之前的tidyr方法更可取(它使用gather()spread()的组合)。 pivoting vignette中描述了更多功能。这个例子特别简洁,因为您所需的规格与id_colsnames_sep的默认值匹配。

2
很惊讶这个没有更多的赞。如果你正在处理一个非常大的数据框和/或很多列,这是唯一能够工作的方法。所有其他解决方案都会耗尽内存。不太确定阈值是什么,但我正在处理一个有6M记录和24列的数据框。 - Chris Townsend

7
注意-2019年9月:在tidyr中,gather()+spread()方法(本答案中描述)已被pivot_wider()方法所取代(在这个更新的tidyr答案中有描述)。有关转换的当前信息,请参见旋转vignette

以下是使用tidyr软件包的解决方案,它已经基本取代了reshapereshape2。与这两个软件包一样,策略是首先使数据集变得更长,然后再变得更宽。

library(magrittr); requireNamespace("tidyr"); requireNamespace("dplyr")
my.df %>%
  tidyr::gather(key=variable, value=value, c(X, Y)) %>%   # Make it even longer.
  dplyr::mutate(                                          # Create the spread key.
    time_by_variable   = paste0(variable, "_", TIME)
  ) %>%
  dplyr::select(ID, time_by_variable, value) %>%          # Retain these three.
  tidyr::spread(key=time_by_variable, value=value)        # Spread/widen.

在调用 tidyr::gather() 函数后,中间数据集为:
ID TIME variable value
1   A    1        X     1
2   B    1        X     2
3   C    1        X     3
...
28  A    5        Y    28
29  B    5        Y    29
30  C    5        Y    30

最终的结果是:
  ID X_1 X_2 X_3 X_4 X_5 Y_1 Y_2 Y_3 Y_4 Y_5
1  A   1   4   7  10  13  16  19  22  25  28
2  B   2   5   8  11  14  17  20  23  26  29
3  C   3   6   9  12  15  18  21  24  27  30

tidyr::unite() 是一个由 @JWilliman 推荐的替代方案。当 remove 参数为 true 时(默认情况),其与上述dplyr::mutate()dplyr::select() 组合在功能上等效。

如果您不习惯此类操作,tidyr::unite() 可能会是一个小障碍,因为它是您必须学习和记住的另一个函数。然而,它的好处包括:(a)更简明的代码(即用一行代码取代了四行);(b)减少重复变量名的地方(即您不必在 dplyr::select() 子句中重复/修改变量名)。

my.df %>%
  tidyr::gather(key=variable, value=value, c(X, Y)) %>%           # Make it even longer.
  tidyr::unite("time_by_variable", variable, TIME, remove=T) %>%  # Create the spread key `time_by_variable` while simultaneously dropping `variable` and `TIME`.
  tidyr::spread(key=time_by_variable, value=value)                # Spread/widen.

1
可以用 tidyr::unite("time_by_variable", variable, TIME) 替换掉 dplyr::mutate(time_by_variable = paste0(variable, "_", TIME)) 这一行。 - JWilliman
我同意,@JWilliman。我认为明确的mutate()paste0()调用使意图更清晰,并避免引入新函数。但是我发现在过去的一年中使用了更多的tidyr::unite()。我会追加回复以反映您的建议。 - wibeasley

0

有点晚了,但以下解决方案也可能有效。

# Input
my.df <- data.frame(ID=rep(c("A","B","C"), 5), TIME=rep(1:5, each=3), X=1:15, Y=16:30)

# Conversion into wide format
my.df %>%
  pivot_wider(ID, c(TIME), 
              values_from = c(X, Y),
              names_glue = '{.value}_{TIME}')

1- 通过这个解决方案,您可以通过在names_glue选项中设置列名来进行调整,例如:

names_glue = '{.value}_{TIME}' or
names_glue = '{TIME}_{.value}'

2- 同样地,使用这个解决方案,你可以连接两个或者更多的字段,例如c(TIME, DAY, MONTH),然后相应地更新names_glue选项:

names_glue = '{TIME}_{DAY}_{MONTH}_{.value}'

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接