在不对列进行排序的情况下从宽转换为长
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 =
,则可以同时熔化多个列。以可扩展的方式执行此操作的一种方法是:
提取列名和对应的前两个字母:
measurevars <- names(mydata)[grepl("_[1-9]$",names(mydata))]
groups <- gsub("_[1-9]$","",measurevars)
将 groups
转换为因子对象,并确保级别不按字母顺序排列。我们将在下一步中使用它来创建具有正确结构的列表对象。
split_on <- factor(groups, levels = unique(groups))
使用 measurevars
和 split()
创建列表,并为 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.default
和 do.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
我想将数据帧从宽格式转换为长格式。
这是一个玩具示例:
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 =
,则可以同时熔化多个列。以可扩展的方式执行此操作的一种方法是:
提取列名和对应的前两个字母:
measurevars <- names(mydata)[grepl("_[1-9]$",names(mydata))] groups <- gsub("_[1-9]$","",measurevars)
将
groups
转换为因子对象,并确保级别不按字母顺序排列。我们将在下一步中使用它来创建具有正确结构的列表对象。split_on <- factor(groups, levels = unique(groups))
使用
measurevars
和split()
创建列表,并为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.default
和 do.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