【问题标题】:Perform row-wise operations on a data.table for a vector-valued column对向量值列的 data.table 执行逐行操作
【发布时间】:2014-09-12 16:49:06
【问题描述】:

编辑:

(对于我的示例过于简单,我深表歉意,我将尝试解决这个问题,并将我更相关的示例格式化为更方便的格式,以便直接复制到 R 中。特别是,有多个值列,以及前面的一些列以及不需要解析的其他信息。)

我对 R 和 data.table 还很陌生,所以我希望能就我发现的问题提供意见。我正在使用一个数据表,其中一列是以冒号分隔的格式字符串,用作其他以冒号分隔的列中值的图例。为了解析它,我必须首先将它拆分为它的组件,然后搜索我需要稍后索引值字符串的组件的索引。这是我可能遇到的那种情况的简化示例

DT <- data.table(number=c(1:5),
                 format=c("name:age","age:name","age:name:height","height:age:name","weight:name:age"),
                 person1=c("john:30","40:bill","20:steve:100","300:70:george","140:fred:20"),
                 person2=c("jane:31","42:ivan","21:agnes:120","320:72:vivian","143:rose:22"))

当评估时,我们得到

> DT
   number          format       person1       person2
1:      1        name:age       john:30       jane:31
2:      2        age:name       40:bill       42:ivan
3:      3 age:name:height  20:steve:100  21:agnes:120
4:      4 height:age:name 300:70:george 320:72:vivian
5:      5 weight:name:age   140:fred:20   143:rose:22

假设对于每个人,我只需要知道他们的姓名和年龄,而不需要他们的身高或体重;在这个例子中,在我的实际数据中,每个格式字符串都有姓名和年龄字段,但可能在不同的位置(我实际查找的字段通常固定在某些列中,但我不愿意硬编码任何索引,因为我不完全熟悉我正在使用的数据文件的生成)。我会首先拆分格式字符串,然后使用 match() 搜索我想要的字段名称。

DT[, format.split := strsplit(format, ":")]

此时,我用来执行匹配的唯一方法是 vapply:

DT[, index.name := vapply(format.split, function (x) match('name', x), 0L)]
DT[, index.age := vapply(format.split, function (x) match('age', x), 0L)]

因为我不知道有任何其他方法可以让 R 知道它应该单独查看列中的行,而不是作为向量聚集在一起,并对向量值 format.split 执行匹配每一行的列,而不是试图匹配整列的行。即使这样,一旦我找到每一行的索引,我必须执行另一个 strsplit,然后执行一个 mapply 来解析每个人的值字符串中的名称值和年龄值:

DT[, person1.split := strsplit(person1, ':')]
DT[, person1.name := mapply(function (x,y) x[y], person1.split, index.name]
DT[, person1.age := mapply(function (x,y) x[y], person1.split, index.age]
DT[, person2.split := strsplit(person2, ':')]
DT[, person2.name := mapply(function (x,y) x[y], person2.split, index.name]
DT[, person2.age := mapply(function (x,y) x[y], person2.split, index.age]

(当然,我也会为年龄做同样的事情)

我正在处理相当大的数据集,因此我希望我的代码尽可能高效。有没有人对我可以加快或优化我的代码的方法提出建议?

(注意:我真的在寻找正确的方法,而不是使用正确的 *apply 或 *ply 或 Map 函数。如果 *(ap)ply 或 Map 真的是正确的方法,我会很感激知道哪个是最有效或最适合我的情况的方法,但如果有更好的方法来测试行内重复,我更愿意对此提出建议而不是功能建议。不过,欢迎提出建议。

编辑 2:

事实证明,我的示例比它需要的更通用。我只需要两个字段,它们始终是格式字符串中的前两个字段,没有变化。第一个字段只是一个文字字符串。但是,第二个字段至少包含 2 个数字,用逗号分隔(最终,我会过滤掉第二个字段中任何超过 2 个数字的行,因此只有在解析之后发生过滤时,更多的可能性才相关) .对于 (3) 个值字符串中的每一个,我只需要创建三列:第一个字段的字符列和两个数字列,一个用于第二个字段中逗号分隔对的每个成员。任何其他字段都无关紧要。我目前的方法可能效率很低,它是使用 sub() 对所需字段和具有反向引用的子字段进行模式匹配。

> DT <- data.table(id=1:5,
format=c(rep("A:B:C:D:E", 5)),
person1=paste(paste0("foo",LETTERS[1:5]), paste(1:5, 10:6, sep=','), "blah", "bleh", "bluh", sep=':'),
person2=paste(paste0("bar",LETTERS[1:5]), paste(16:20, 5:1, sep=','), "blah", "bleh", "bluh", sep=':'),
person3=paste(paste0("baz",LETTERS[1:5]), paste(0:4, 12:8, sep=','), "blah", "bleh", "bluh", sep=':'))

> DT
   id    format                  person1                  person2                  person3
1:  1 A:B:C:D:E fooA:1,10:blah:bleh:bluh barA:16,5:blah:bleh:bluh bazA:0,12:blah:bleh:bluh
2:  2 A:B:C:D:E  fooB:2,9:blah:bleh:bluh barB:17,4:blah:bleh:bluh bazB:1,11:blah:bleh:bluh
3:  3 A:B:C:D:E  fooC:3,8:blah:bleh:bluh barC:18,3:blah:bleh:bluh bazC:2,10:blah:bleh:bluh
4:  4 A:B:C:D:E  fooD:4,7:blah:bleh:bluh barD:19,2:blah:bleh:bluh  bazD:3,9:blah:bleh:bluh
5:  5 A:B:C:D:E  fooE:5,6:blah:bleh:bluh barE:20,1:blah:bleh:bluh  bazE:4,8:blah:bleh:bluh

然后我的代码会这样做:

DT[, `:=`(person1.A=sub("^([^:]*):.*$","\\1", person1),
          person2.A=sub("^([^:]*):.*$","\\1", person2),
          person3.A=sub("^([^:]*):.*$","\\1", person3),
          person1.B.first=sub("^[^:]*:([^:,]*),.*$","\\1", person1),
          person1.B.second=sub("^[^:]*:[^:,]*,([^:,]*)(,[^:,]*)*:.*$","\\1", person1),
          person2.B.first=sub("^[^:]*:([^:,]*),.*$","\\1", person2),
          person2.B.second=sub("^[^:]*:[^:,]*,([^:,]*)(,[^:,]*)*:.*$","\\1", person2),
          person3.B.first=sub("^[^:]*:([^:,]*),.*$","\\1", person3),
          person3.B.second=sub("^[^:]*:[^:,]*,([^:,]*)(,[^:,]*)*:.*$","\\1", person3))]

用于拆分,并过滤

DT <- DT[grepl("^[^:]*:[^:,]*,[^:,]*:.*$", person1) &
         grepl("^[^:]*:[^:,]*,[^:,]*:.*$", person2) &
         grepl("^[^:]*:[^:,]*,[^:,]*:.*$", person3) ]

我知道这种方法可能效率很低,但这是我对重复应用 strsplit 的旧方法提出的第一个改进。考虑到新的条件,有没有比melt、csplit、dcast更好的做事方式?

编辑 3:

由于我只需要前两个字段,因此我最终修剪了所有值字符串,删除了超过两个逗号(即超过 3 个第二字段数字)的字符串,将逗号更改为冒号,替换了格式字符串每行都有(现在是 3 个)字段的名称,并按照@AnandaMahto 的建议执行 dcast(csplit(melt)) 。它似乎运作良好。

【问题讨论】:

  • 它是否总是由冒号分隔的两个值?
  • @AnandaMahto 这只是一个例子。可能存在不同数量的字段和不同的字段顺序。
  • 好的。但通常最好用可能实际代表您的问题的术语来描述您的问题:-)
  • @AnandaMahto 我改变了问题,因为我的问题超出了过滤范围。只是想给你一个提示。我还让这个例子更符合我将要使用的标准。

标签: r data.table


【解决方案1】:

@bskaggs 有一个正确的想法,将数据放入长格式甚至是结构化的宽格式可能更有意义。

我将向您展示两个选项,但首先,以其他人可以实际使用的方式共享您的数据总是更好:

DT <- data.table(
  format = c("name:age", "name:age:height", "age:height:name",
             "height:weight:name:age", "name:age:weight:height",
             "name:age:height:weight"),
  values = c("john:30", "rene:33:183", "100:10:speck",
             "100:400:sumo:11", "james:43:120:120", 
             "plink:2:300:400"))

我还建议你使用my cSplit function

以下是您可以轻松将此数据集转换为长格式的方法:

cSplit(DT, c("format", "values"), ":", "long")
#     format values
#  1:   name   john
#  2:    age     30
#  3:   name   rene
#  4:    age     33
#  5: height    183
#  6:    age    100
#  7: height     10
#  8:   name  speck
#  9: height    100
# 10: weight    400
# 11:   name   sumo
# 12:    age     11
# 13:   name  james
# 14:    age     43
# 15: weight    120
# 16: height    120
# 17:   name  plink
# 18:    age      2
# 19: height    300
# 20: weight    400

一旦数据为“长”形式,您可以使用dcast.data.table 轻松将其转换为“宽”形式,如下所示。 (我还使用setcolorder 对列进行了重新排序,这样您无需复制即可重新排列数据。)

X <- dcast.data.table(
  cSplit(cbind(id = 1:nrow(DT), DT), 
         c("format", "values"), ":", "long"), 
  id ~ format, value.var = "values")
setcolorder(X, c("id", "name", "age", "height", "weight"))
X
#    id  name age height weight
# 1:  1  john  30     NA     NA
# 2:  2  rene  33    183     NA
# 3:  3 speck 100     10     NA
# 4:  4  sumo  11    100    400
# 5:  5 james  43    120    120
# 6:  6 plink   2    300    400

这在速度方面如何?

首先,一个非常温和的数据集:

DT <- rbindlist(replicate(2000, DT, FALSE))
dim(DT)
# [1] 12000     2

## @bskaggs's suggestion    
system.time(colonMelt(DT))
#    user  system elapsed 
#    0.27    0.00    0.27

## cSplit. It would be even faster if you already had
##   an id column and didn't need to cbind one in
system.time(cSplit(cbind(id = 1:nrow(DT), DT),
                   c("format", "values"), ":", "long"))
#    user  system elapsed 
#    0.02    0.00    0.01 

## cSplit + dcast.data.table
system.time(dcast.data.table(
  cSplit(cbind(id = 1:nrow(DT), DT), 
         c("format", "values"), ":", "long"), 
  id ~ format, value.var = "values"))
#    user  system elapsed 
#    0.08    0.00    0.08 

更新

对于您更新的问题,您可以先melt“data.table”,然后类似地继续:

library(reshape2)

## Melting, but no reshaping -- a nice long format
cSplit(melt(DT, id.vars = c("number", "format")), 
       c("format", "value"), ":", "long")

## Try other combinations for the LHS and RHS of the 
##   formula. This seems to be what you might be after
dcast.data.table(
  cSplit(melt(DT, id.vars = c("number", "format")), 
         c("format", "value"), ":", "long"),
  number ~ variable + format, value.var = "value")

【讨论】:

  • 很抱歉将球门柱移到这个位置。我编辑了我的问题以包含修改示例的代码,这更准确地反映了我的情况。
  • 如果我只想要某些值,例如我的示例中的姓名和年龄,并且最终会在出现的任何地方丢弃身高和体重,有没有办法将它们作为 dcast 的副作用丢弃( csplit(melt)),或者我应该只在转换后对列进行空分配?此外,对于我正在使用的数据,dcast 调用将聚合函数默认为长度,而对于示例数据,它不会。为什么要这样做?
  • @archaephyrryx,在使用 cSplit 之前添加另一列以帮助创建唯一 ID(例如,将其设为 1:nrow(DT))。这应该解决length 聚合问题。我也只会在投射后删除列,除非您期望最终得到 lots 列。
  • 很抱歉再次这样做,但我发现了有关我正在使用的数据的新信息,所以我做了一些更改。
  • @archaephyrryx,我很困惑。编辑 3 表明事情已经解决。还有问题没有解决吗?
【解决方案2】:

我认为使用高大整洁的格式可能会更好地为您服务:

colonMelt <- function(DT) {
  formats <- strsplit(DT$format, ":")
  rows <- rep(row.names(DT), sapply(formats, length))
  data.frame(row = rows,
             key = unlist(formats), 
             value = unlist(strsplit(DT$values, ":"))
  )
}

newDT <- colonMelt(DT)

结果是一种更易于搜索和过滤的格式,无需一直拆分字符串:

   row    key value
1    1   name  john
2    1    age    30
3    2   name  rene
4    2    age    33
5    2 height   183
6    3    age   100
7    3 height    10
8    3   name speck

【讨论】:

  • +1。我认为这至少应该让 OP 到达他们需要的地方。 FWIW,我已经编写了一个名为 cSplit 的函数 I demonstrate in my answer 这将更有效且更适用于其他条件。
  • colonMelt - 听起来很痛苦
猜你喜欢
  • 1970-01-01
  • 2020-07-18
  • 2014-08-12
  • 1970-01-01
  • 2011-12-14
  • 1970-01-01
  • 2014-02-12
  • 1970-01-01
  • 2019-09-13
相关资源
最近更新 更多