关于r:从宽到长的转换,无需对列进行排序

Transform from Wide to Long without sorting columns

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

这是一个玩具示例:

1
2
3
4
5
6
7
8
9
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.tablemelt和dcast,这是一种能够自动检测变量的通用方法。也欢迎其他解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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

应该是

1
ID ZA BB CC

我不介意idvars列开头是否全部在一起,或者它们是否也保持其原始位置。

ID ZA_1 ZA_2 TEMP BB_1 BB_2 CC_2 CC_1

将是

1
ID ZA TEMP BB CC

1
ID TEMP ZA BB CC

我更喜欢最后一个选择。

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


如果将列名列表传递给参数measure =,则可以同时融化几列。一种以可伸缩方式进行此操作的方法是:

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

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

    1
    split_on <- factor(groups, levels = unique(groups))
  • 使用measurevarssplit()创建列表,并在melt()中为value.name =参数创建向量。

    1
    2
    measure_list <- split(measurevars, split_on)
    measurenames <- unique(groups)
  • 将所有内容放在一起:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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


    使用data.table

    的替代方法

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

    给出:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
        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

    OP更新了他对自己的问题的回答,抱怨一半列为id.vars时中间melt()步骤的内存消耗。他要求data.table需要一种直接的方法来做到这一点,而又不会产生巨大的中间步骤。

    好吧,data.table已经具备了这种能力,称为联接。

    考虑到来自Q的样本数据,可以通过仅使用一个id.var进行整形,然后将整形后的结果与原始数据结合起来,从而以较少的内存消耗方式实现整个操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    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个度量cols和5行组成。调整后的结果具有1个id列,3个度量cols和15行。因此,存储在id列中的数据量实际上增加了两倍。但是,中间步骤仅需要一个id.var rn

    EDIT如果内存消耗至关重要,那么可能有必要考虑将id.vars和measure.vars保留在两个单独的data.tables中,并根据需要仅将必要的id.var列与measure.vars连接起来。

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

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


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # 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。

    请注意,数据必须定期组织,没有丢失的变量等。


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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需要一种直接的方法来执行此操作,而无需创建巨大的中间步骤。