在不对列进行排序的情况下从宽转换为长

Transform from Wide to Long without sorting columns

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

这是一个玩具示例:

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

我更喜欢最后一个选项。

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

如果将列名列表传递给参数 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. 使用 measurevarssplit() 创建列表,并为 melt() 中的 value.name = 参数创建向量。

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

综合考虑:

melt(setDT(mydata), 
     measure = measure_list, 
     value.name = measurenames,
     variable.name = "measure")
#    ID measure ZA BB
# 1:  1       1  1  3
# 2:  2       1  2  3
# 3:  3       1  3  3
# 4:  4       1  4  3
# 5:  5       1  5  3
# 6:  1       2  5  6
# 7:  2       2  4  6
# 8:  3       2  3  6
# 9:  4       2  2  6
#10:  5       2  1  6

这是一个使用基本 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.

请注意,数据必须定期结构化,没有遗漏变量等

终于找到方法了,修改我的初始方案

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 需要一种直接的方法来完成它,而无需创建巨大的中间步骤。

OP 更新了他对自己问题的回答,抱怨当一半列为 id.vars 时中间 melt() 步骤的内存消耗。他要求 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] 不再需要,您可以删除行号。

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

我们从 data.table 开始,其中包含 1 个 ID 列、5 个度量列和 5 行。重塑后的结果有 1 个 ID 列、3 个度量列和 15 行。因此,存储在 id 列中的数据量实际上增加了两倍。然而,中间步骤只需要一个 id.var rn.

编辑 如果内存消耗很重要,可能值得考虑保留 id.vars 和 measure.vars 在两个单独的 data.table 中,并根据需要仅将必要的 id.var 列与 measure.vars 连接。

请注意 melt()measure.vars 参数允许使用特殊函数 patterns()。有了这个,对 melt() 的调用就可以写成

molten <- melt(mydata, id.vars = "rn", measure.vars = patterns("_\d$"))

另一种方法 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