从宽变换到长而不排序列

时间:2016-12-15 11:50:57

标签: r dataframe data.table transformation

我想将数据帧从宽格式转换为长格式。

这是一个玩具示例:

mydata <- data.frame(ID=1:5, ZA_1=1:5, 
            ZA_2=5:1,BB_1=rep(3,5),BB_2=rep(6,5),CC_7=6:2)

ID ZA_1 ZA_2 BB_1 BB_2 CC_7
1    1    5    3    6    6
2    2    4    3    6    5
3    3    3    3    6    4
4    4    2    3    6    3
5    5    1    3    6    2

有些变量将保持原样(此处仅为ID),有些变量将转换为长格式(此处所有其他变量均以_1,_2或_7结尾)

为了将其转换为长格式,我使用data.table melt和dcast,这是一种能够自动检测变量的通用方法。其他解决方案也是受欢迎的。

library(data.table)
setDT(mydata)
idvars =  grep("_[1-7]$",names(mydata) , invert = TRUE)
temp <- melt(mydata, id.vars = idvars)  
nuevo <- dcast(
  temp[, `:=`(var = sub("_[1-7]$", '', variable),
  measure = sub('.*_', '', variable), variable = NULL)],  
  ... ~ var, value.var='value') 



ID measure BB  CC  ZA
 1      1   3  NA   1
 1      2   6  NA   5
 1      7  NA   6  NA
 2      1   3  NA   2
 2      2   6  NA   4
 2      7  NA   5  NA
 3      1   3  NA   3
 3      2   6  NA   3
 3      7  NA   4  NA
 4      1   3  NA   4
 4      2   6  NA   2
 4      7  NA   3  NA
 5      1   3  NA   5
 5      2   6  NA   1
 5      7  NA   2  NA

正如您所看到的那样,列按字母顺序重新排序,但我希望尽可能保留原始顺序,例如考虑变量首次出现的顺序。

  

ID ZA_1 ZA_2 BB_1 BB_2 CC_7

应该是

ID ZA BB CC

我不介意idvars列是否在开始时全部完成,或者它们是否也保持在原始位置。

  

ID ZA_1 ZA_2 TEMP BB_1 BB_2 CC_2 CC_1

将是

ID ZA TEMP BB CC

ID TEMP ZA BB CC

我更喜欢最后一个选项。

另一个问题是一切都变成了角色。

5 个答案:

答案 0 :(得分:2)

如果将列名列表传递给参数measure =,则可以同时熔化多个列。以可扩展的方式执行此操作的一种方法是:

  1. 提取列名和相应的前两个字母:

    measurevars <- names(mydata)[grepl("_[1-9]$",names(mydata))]
    groups <- gsub("_[1-9]$","",measurevars)
    
  2. groups转换为系数对象,并确保不按字母顺序排序关卡。我们将在下一步中使用它来创建具有正确结构的列表对象。

    split_on <- factor(groups, levels = unique(groups))
    
  3. 使用带有measurevars的{​​{1}}创建列表,并为split()中的value.name =参数创建向量。

    melt()
  4. 将所有这些结合在一起:

    measure_list <- split(measurevars, split_on)
    measurenames <- unique(groups)
    

答案 1 :(得分:1)

以下是使用基本R函数split.defaultdo.call的方法。

# split the non-ID variables into groups based on their name suffix
myList <- split.default(mydata[-1], gsub(".*_(\\d)$", "\\1", names(mydata[-1])))

# append variables by row after setting the regularizing variable names, cbind ID
cbind(mydata[1],
      do.call(rbind, lapply(myList, function(x) setNames(x, gsub("_\\d$", "", names(x))))))
    ID ZA BB
1.1  1  1  3
1.2  2  2  3
1.3  3  3  3
1.4  4  4  3
1.5  5  5  3
2.1  1  5  6
2.2  2  4  6
2.3  3  3  6
2.4  4  2  6
2.5  5  1  6

第一行将data.frame变量(减去ID)拆分为同意其变量名称的最终字符的列表。该标准使用gsub确定。第二行使用do.call在此变量列表上调用rbind,使用setNames进行修改,以便从名称中删除最后的数字和下划线。最后,cbind将ID附加到生成的data.frame。

请注意,数据必须定期构建,不会丢失变量等。

答案 2 :(得分:1)

当一半列为melt()时,OP已经更新了他自己的问题的答案,抱怨中间id.vars步骤的内存消耗。他要求 data.table需要一种直接的方法,而不需要创建巨大的中间步骤

嗯,data.table已经具备了这种能力,它被称为 join

给定来自Q的样本数据,通过仅使用一个id.var进行重新整形,然后将重新整形的结果与原始data.table连接,可以以更少的内存消耗方式实现整个操作。表:

setDT(mydata)

# add unique row number to join on later 
# (leave `ID` col as placeholder for all other id.vars)
mydata[, rn := seq_len(.N)]

# define columns to be reshaped
measure_cols <- stringr::str_subset(names(mydata), "_\\d$")

# melt with only one id.vars column
molten <- melt(mydata, id.vars = "rn", measure.vars = measure_cols)

# split column names of measure.vars
# Note that "variable" is reused to save memory 
molten[, c("variable", "measure") := tstrsplit(variable, "_")]

# coerce names to factors in the same order as the columns appeared in mydata
molten[, variable := forcats::fct_inorder(variable)]

# remove columns no longer needed in mydata _before_ joining to save memory
mydata[, (measure_cols) := NULL]

# final dcast and right join
result <- mydata[dcast(molten, ... ~ variable), on = "rn"]
result
#    ID rn measure ZA BB CC
# 1:  1  1       1  1  3 NA
# 2:  1  1       2  5  6 NA
# 3:  1  1       7 NA NA  6
# 4:  2  2       1  2  3 NA
# 5:  2  2       2  4  6 NA
# 6:  2  2       7 NA NA  5
# 7:  3  3       1  3  3 NA
# 8:  3  3       2  3  6 NA
# 9:  3  3       7 NA NA  4
#10:  4  4       1  4  3 NA
#11:  4  4       2  2  6 NA
#12:  4  4       7 NA NA  3
#13:  5  5       1  5  3 NA
#14:  5  5       2  1  6 NA
#15:  5  5       7 NA NA  2

最后,如果result[, rn := NULL]不再需要,您可以删除行号。

此外,您可以按molten删除中间rm(molten)

我们已经开始使用data.table组成的1个id列,5个度量列和5个行。重新整形的结果有1个id列,3个度量列和15个行。因此,存储在id列中的数据量有效地增加了三倍。但是,中间步骤只需要一个id.var rn

编辑如果内存消耗是至关重要的,可能值得考虑将id.vars和measure.vars保存在两个单独的data.tables中并加入只需要带有measure.vars的必要id.var列。

请注意,measure.vars的{​​{1}}参数允许使用特殊功能melt()。有了这个,patterns()的调用可以写成

melt()

答案 3 :(得分:1)

使用data.table的替代方法:

melt(mydata, id = 'ID')[, c("variable", "measure") := tstrsplit(variable, '_')
                        ][, variable := factor(variable, levels = unique(variable))
                          ][, dcast(.SD, ID + measure ~ variable, value.var = 'value')]

给出:

    ID measure ZA BB CC
 1:  1       1  1  3 NA
 2:  1       2  5  6 NA
 3:  1       7 NA NA  6
 4:  2       1  2  3 NA
 5:  2       2  4  6 NA
 6:  2       7 NA NA  5
 7:  3       1  3  3 NA
 8:  3       2  3  6 NA
 9:  3       7 NA NA  4
10:  4       1  4  3 NA
11:  4       2  2  6 NA
12:  4       7 NA NA  3
13:  5       1  5  3 NA
14:  5       2  1  6 NA
15:  5       7 NA NA  2

答案 4 :(得分:0)

最后,我找到了方法,修改了我的初始解决方案

mydata <- data.table(ID=1:5, ZA_2001=1:5, ZA_2002=5:1,
BB_2001=rep(3,5),BB_2002=rep(6,5),CC_2007=6:2)

idvars =  grep("_20[0-9][0-9]$",names(mydata) , invert = TRUE)
temp <- melt(mydata, id.vars = idvars)  
temp[, `:=`(var = sub("_20[0-9][0-9]$", '', variable), 
measure = sub('.*_', '', variable), variable = NULL)]  
temp[,var:=factor(var, levels=unique(var))]
dcast( temp,   ... ~ var, value.var='value' )

它为您提供了适当的测量值。 无论如何,这个解决方案需要大量的内存。

诀窍是将var变量转换为因子指定我想要的水平顺序,正如mtoto所做的那样。 mtoto解决方案很好,因为它不需要转换和融化,只能融化,但在我更新的示例中不起作用,只有在每个单词的数字变化数量相同时才有效。

PD: 我正在解析每一步,发现在使用大型数据表时,熔化步骤可能是一个大问题。如果你有一个只有100000行x 1000列的data.table并且使用一半列作为id.vars,那么输出大约是50000000 x 500,只是太多而不能继续下一步。 data.table需要一种直接的方法,而不需要创建巨大的中间步骤。