在R中快速子集取值

7

我有一个大小为30000 x 50的数据框,另外还有一个单独的列表,其中包含对此数据框行分组的指针,例如:

rows <- list(c("34", "36", "39"), c("45", "46"))

这段代码指出具有行名称(不是数值索引,而是字符型的rownames(dat))“34”、“36”、“39”的dataframe行构成一个分组,而“45”、“46”则构成另一个分组。

现在我想将这些分组从dataframe中提取到一个并行列表中,但我的代码(如下所示)非常慢。我该如何加速它?

> system.time(lapply(rows, function(r) {dat[r, ]}))
   user  system elapsed 
 246.09    0.01  247.23 

这是在一台非常快的计算机上运行的,R 2.14.1 x64 版本。


你能大概说明一下rows里有多少元素,rows[[i]]中大概有多少元素吗?另外,你的rownames都是唯一的对吗?(我构造了一个随机的dat,30000x50,但是我似乎得到了很快的rows时间,可能是我的数据不够大?) - mathematical.coffee
rows 大约有 15000 个元素;length(rows[[i]]) 的取值范围为 1 到 50。 - Jack Tanner
5个回答

18

主要问题之一是行名称的匹配 - 在 [.data.frame 中默认情况下是部分匹配行名称,你可能不希望这样,所以最好使用 match。如果你想进一步加快速度,可以使用 fastmatch 中的 fmatch。这是一个小修改,可以提高一些速度:

# naive
> system.time(res1 <- lapply(rows,function(r) dat[r,]))
   user  system elapsed 
 69.207   5.545  74.787 

# match
> rn <- rownames(dat)
> system.time(res1 <- lapply(rows,function(r) dat[match(r,rn),]))
   user  system elapsed 
 36.810  10.003  47.082 

# fastmatch
> rn <- rownames(dat)
> system.time(res1 <- lapply(rows,function(r) dat[fmatch(r,rn),]))
   user  system elapsed 
 19.145   3.012  22.226 

如果你的rows不重叠并且覆盖所有行(因此可以将每一行映射到rows中的一个条目),则可以通过不使用[(它对数据框来说速度很慢)而拆分数据帧(使用split)来进一步加速。

根据你实际的数据情况,你可能会更喜欢使用矩阵,因为它们具有远比数据框更快的子集操作符,因为它们是本地的。


fmatch真的很神奇。现在我看到这些时间(与问题中的时间相比):用户 系统 经过时间 11.48 0.02 11.64 - Jack Tanner
...但是fmatch不能处理对table参数的更新:s<-'a';fmatch('a',s);s[1]<-'b';fmatch('a',s)第二次,fmatch不应该找到匹配,但它确实找到了...一般来说,改变已命名对象似乎是危险的,并且依赖于它们永远不会再次被修改... - Tommy
是的,文档中也警告了这一点。这就是你为获得速度所付出的代价 ;) 不幸的是,在 R 中没有办法在更新时得到通知。而问题根本不在于 NAMED,而在于 R 会将属性愉快地复制到新对象上。 - Simon Urbanek
实际上,我找到了一种方法来识别您所举的案例(通过在哈希中存储父对象),因此下一个版本的 fastmatch 应该能够检测到已复制但不同步的属性。感谢您提供的示例 :)。 - Simon Urbanek
很高兴你能解决那个问题。而且,所谓的NAMED问题是指通过给一个已命名对象赋予新属性来“违反规则”。也许在你的情况下,好处大于风险。遵守规则的解决方案会更加繁琐:m <- fmakeMap(table); fmatch(x, m) - Tommy

5

更新

我的原始帖子有一个错误的表述:

通过 rownamescolnames 进行索引的问题在于,你需要针对每个元素运行一次向量/线性扫描,例如,你需要查找每一行来查看哪一行被命名为“36”,然后重新开始查找以寻找“34”。

Simon 在这里的评论中指出,R 显然使用哈希表进行索引。对于我的错误,我感到非常抱歉。

原始答案

请注意,本答案中的建议假定您具有数据的非重叠子集。

如果您想保留列表查找策略,我建议存储实际的行索引而不是字符串名称。

另一种方法是将您的“组”信息存储为 data.frame 的另一列,然后根据其组拆分 data.frame,例如,假设您重新编码的 data.frame 如下:

dat <- data.frame(a=sample(100, 10),
                  b=rnorm(10),
                  group=sample(c('a', 'b', 'c'), 10, replace=TRUE))

你可以这样做:

然后你可以执行:

split(dat, dat$group)
$a
   a           b group
2 66 -0.08721261     a
9 62 -1.34114792     a

$b
    a          b group
1  32  0.9719442     b
5  79 -1.0204179     b
6  83 -1.7645829     b
7  73  0.4261097     b
10 44 -0.1160913     b

$c
   a          b group
3 77  0.2313654     c
4 74 -0.8637770     c
8 29  1.0046095     c

或者,根据你想要用“splits”做什么,你可以将你的data.frame转换为一个data.table,并将其键设置为新的group列:

library(data.table)
dat <- data.table(dat, key="group")

现在进行列表操作,这将会给你和上面使用的split函数相同的结果。
 x <- lapply(unique(dat$group), function(g) dat[J(g),])

但是你可能想要“改进你的调试”,可以使用行内方式实现,例如:
ans <- dat[, {
  ## do some code over the data in each split
  ## and return a list of results, eg:
  list(nrow=length(a), mean.a=mean(a), mean.b=mean(b))
}, by="group"]

ans
     group nrow mean.a     mean.b
[1,]     a    2   64.0 -0.7141803
[2,]     b    5   62.2 -0.3006076
[3,]     c    3   60.0  0.1240660

您可以使用plyr以类似的方式完成最后一步,例如:

library(plyr)
ddply(dat, "group", summarize, nrow=length(a), mean.a=mean(a),
      mean.b=mean(b))
  group nrow mean.a     mean.b
1     a    2   64.0 -0.7141803
2     b    5   62.2 -0.3006076
3     c    3   60.0  0.1240660

但是既然你提到你的数据集相当大,我认为你会喜欢data.table提供的速度提升。


5
使用行名和列名进行索引的问题在于,对于每个元素,您都需要运行向量/线性扫描 - 这是完全错误的,R不会那么愚蠢- 它使用哈希表进行索引。但是,由于默认情况下进行部分匹配,因此最好使用match来避免这种情况(或者最好使用fastmatch,因为您想重用哈希表)- 我已添加了几个示例响应。 - Simon Urbanek
@SteveLianoglou,虽然线性扫描注释存在错误,但感谢你演示了split的使用。 - Jack Tanner

4
这里有一个加速的尝试 - 它依赖于查找行索引比查找行名称更快的事实,因此尝试在dat中创建行名到行号的映射。
首先创建与您的数据大小相同的一些数据,并分配一些数字行名称:
> dat <- data.frame(matrix(runif(30000*50),ncol=50))
> rownames(dat) <- as.character(sample.int(nrow(dat)))
> rownames(dat)[1:5]
[1] "21889" "3050"  "22570" "28140" "9576" 

现在生成一个随机的rows,包含15000个元素,每个元素由50个1到30000之间的随机数字组成(在此情况下为row*names*)。
# 15000 groups of up to 50 rows each
> rows <- sapply(1:15000, function(i) as.character(sample.int(30000,size=sample.int(50,size=1))))

为了计时,可以尝试你提出的方法 (哎呀!):

# method 1
> system.time((res1 <- lapply(rows,function(r) dat[r,])))
   user  system elapsed 
182.306   0.877 188.362 

现在,尝试将行名称映射到行号。 map [i] 应该给出名称为i 的行的行号。
首先,如果您的行名称是1:nrow(dat)的排列,那么您很幸运!您只需要对行名称进行排序,并返回索引:
> map <- sort(as.numeric(rownames(dat)), index.return=T)$ix
# NOTE: map[ as.numeric(rowname) ] -> rownumber into dat for that rowname.

现在查找行的索引而不是行名:
> system.time((res2 <- lapply(rows,function(r) dat[map[as.numeric(r)],])))
   user  system elapsed
 32.424   0.060  33.050

检查我们没有搞砸任何东西(请注意,仅匹配行名称已足够,因为在 R 中行名称是唯一的):

> all(rownames(res1)==rownames(res2))
[1] TRUE

因此,速度提升了约6倍。尽管如此,仍然不是很惊人...

第二点 如果你的行名称与nrow(dat)没有任何关系,你可以尝试这个方法,但前提是max(as.numeric(rownames(dat)))不要比nrow(dat)大太多。它基本上使用map[rowname]来给出行号,但由于行名称不再连续,所以在map中可能会有大量的间隙,这会浪费一些内存:

map <- rep(-1,max(as.numeric(rownames(dat))))
obj <- sort(as.numeric(rownames(dat)), index.return=T)
map[obj$x] <- obj$ix

然后像以前一样使用mapdat[map[as.numeric(r),]])。


2
您可以尝试这个修改:
system.time(lapply(rows, function(r) {dat[ rownames(dat) %in% r, ]}))

这个编程问题真的让我的电脑崩溃了——我认为 dat[rownames(dat) %in% r,]dat[r,] 更慢。 - mathematical.coffee
是的,这个几乎要加倍所需的时间。但感谢您的建议。 - Jack Tanner

1

我同意数学咖啡的观点,我也能够快速地完成这个任务。

不知道是否可能,但是通过将向量取消列表并转换为数字,您可以获得速度提升。

dat <- data.frame(matrix(rnorm(30000*50), 30000, 50 ))
rows <- as.numeric(unlist(list(c("34", "36", "39"), c("45", "46"))))
system.time(lapply(rows, function(r) {dat[r, ]}))

编辑:

dat$observ <- rownames(dat)
rownames(dat) <- 1:nrow(dat)

然而,这些行是行名称而不是行索引,因此 as.numeric 将导致提取错误的行。 - mathematical.coffee
行名是否可以转换为数字或它们是实际的字符向量?如果它们是字符向量,我建议将其作为另一个变量/列,并具有数字行名。这给您数字行名并保留您的信息。我将演示。 - Tyler Rinker
是的,我现在明白了字符行名的问题。这里可能可以使用哈希表,但我想先看看DWin和mathematical coffee的解决方案如何。 - Tyler Rinker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接