使用dplyr从数据框中抽取子组的示例行

36

如果我想从不同的组中随机选择一些样本,我会使用plyr包和下面的代码

require(plyr)
sampleGroup<-function(df,size) {
  df[sample(nrow(df),size=size),]
}

iris.sample<-ddply(iris,.(Species),function(df) sampleGroup(df,10))

每个物种都选取了10个样本。

我的一些数据框非常大,我的问题是我能否使用dplyr包中的同一sampleGroup函数?或者在dplyr中有另一种方法可以做到相同的效果?

编辑

dplyr包的0.2版本引入了两个新函数sample_n和sample_frac,用于从表格中选择随机行。


这是一个 dplyr 入门的链接。http://rpubs.com/hadley/dplyr-intro - marbel
谢谢,但我认为这个问题的解决方案还没有在文档中。不过用 data.table 的解决方案很好! - Robert
1
为什么不直接使用 iris %.% group_by(Species) %.% sampleGroup(size = 10) 呢? - dickoa
2
我认为没有一个自然纯粹的dplyr解决方案,但抽样似乎非常重要,应该作为一个顶级函数进行处理:https://github.com/hadley/dplyr/issues/202 - hadley
@Robert,我不确定为什么我在你的问题中错过了那个部分;它很明显地被说明了。我会删除我的评论。 - Brian Diggs
很棒,@hadley想要将一个样例函数添加到dplyr包中。 我发现只使用dplyr函数的解决方案非常慢: system.time(rbind_all(do(testdata %.% group_by(group),function(x) sampleGroup(x,10)))) @Troy的dplyr解决方案更快。 - Robert
4个回答

70

是的,你可以使用dplyr:

mtcars %>% 
    group_by(cyl) %>%
    slice_sample(n = 2))

结果就像这样

Source: local data frame [6 x 11]
Groups: cyl

   mpg cyl  disp  hp drat    wt  qsec vs am gear carb
1 24.4   4 146.7  62 3.69 3.190 20.00  1  0    4    2
2 26.0   4 120.3  91 4.43 2.140 16.70  0  1    5    2
3 21.0   6 160.0 110 3.90 2.875 17.02  0  1    4    4
4 17.8   6 167.6 123 3.92 3.440 18.90  1  0    4    4
5 14.3   8 360.0 245 3.21 3.570 15.84  0  0    3    4
6 15.0   8 301.0 335 3.54 3.570 14.60  0  1    5    8

历史记录:slice_sample() 在 dplyr 1.0.0(2020年5月)中取代了sample_n()。早期版本的dplyr需要do(sample_n(., 2))


@Arun,是的,但你应该将dplyr更新到最新版本0.1.3.0.99。 - PhilChang
有没有不使用 do 的方法来实现这个? - Brani
3
你能否将你的代码与上面提到的 data.table 解决方案进行比较?我尽可能地使用 dplyr,因为它的语法更简单(或者至少我还没有学习过 data.table)。每当在 SO 上询问 dplyr 问题时,都会得到关于 data.table 的答案,这让我有些烦恼,因此我想看看这段新代码是否接近解决问题。 - gregmacfarlane
@gregmacfarlane 刚刚看了上面的评论,就会明白了。当时使用 dplyr 没有可接受的方法来完成这个任务。在阅读了当时的文档后,OP 回答道:“谢谢,但我认为这个问题的解决方案还没有在文档中。不过使用 data.table 的解决方案确实很好!- Robert”。同时请阅读提问时其他回答,它们似乎不是很好的解决方案... - marbel
@PhilChang 当我运行以下代码时,出现了以下错误消息:clickers%>% group_by(ListName)%>% sample_n(200) 错误:size必须小于或等于29(数据大小),设置replace= TRUE以使用替换抽样。 - user3614783
显示剩余2条评论

10
使用data.table很容易实现这一点,并且对于大型表格非常有用。
注意:正如Troy在评论中提到的那样,使用data.table可以更有效地完成这项工作,但我想尊重OP示例函数和答案格式。
require(data.table)
DT <- data.table(x = rnorm(10e6, 100, 50), y = letters)

sampleGroup<-function(df,size) {
  df[sample(nrow(df),size=size),]
}

result <- DT[, sampleGroup(.SD, 10), by=y]
print(result)

# y         x y
# 1: a  30.11659 m
# 2: a  57.99974 h
# 3: a  58.13634 o
# 4: a  87.28466 x
# 5: a  85.54986 j
# ---              
# 256: z 149.85817 d
# 257: z 160.24293 e
# 258: z  26.63071 j
# 259: z  17.00083 t
# 260: z 130.27796 f

system.time(DT[, sampleGroup(.SD, 10), by=y])
# user  system elapsed 
# 0.66    0.02    0.69 

Using the iris dataset:
iris <- data.table(iris)
iris[,sampleGroup(.SD, 10), by=Species]

2
使用data.table的+1。使用.I可以将性能速度提高一倍:iris[iris[,list(idx=sample(.I,10)),by="Species"]$idx] - Troy
1
我认为你想要使用 sampleGroup(.SD, 10)(注意是 .SD 而不是 DT)。 - eddi

7

那是个好问题!使用文档记录的 dplyr 语法似乎没有简单的方法来实现它,但是有没有类似于以下的解决方法?

sampleGroup<-function(df,x=1){

  df[
    unlist(lapply(attr((df),"indices"),function(r)sample(r,min(length(r),x))))
    ,]

}

sampleGroup(iris %.% group_by(Species),3)

#Source: local data frame [9 x 5]
#Groups: Species
#
#    Sepal.Length Sepal.Width Petal.Length Petal.Width    Species
#39           4.4         3.0          1.3         0.2     setosa
#16           5.7         4.4          1.5         0.4     setosa
#25           4.8         3.4          1.9         0.2     setosa
#51           7.0         3.2          4.7         1.4 versicolor
#62           5.9         3.0          4.2         1.5 versicolor
#59           6.6         2.9          4.6         1.3 versicolor
#148          6.5         3.0          5.2         2.0  virginica
#103          7.1         3.0          5.9         2.1  virginica
#120          6.0         2.2          5.0         1.5  virginica

编辑 - 性能比较

下面是一项测试,针对100万行、26个组,使用data.table(包括本地和按照示例进行函数调用的方式)。

本地data.table的速度约为dplyr解决方法的两倍,也比带有调用输出的data.table调用要快。因此,dplyr/data.table的性能大致相同。

希望dplyr团队能够尽快为我们提供一些本地语法来进行抽样!(或者更好的是,也许已经有了)

sampleGroup.dt<-function(df,size) {
  df[sample(nrow(df),size=size),]
}

testdata<-data.frame(group=sample(letters,10e5,T),runif(10e5))

dti<-data.table(testdata)

# using the dplyr workaround with external function call
system.time(sampleGroup(testdata %.% group_by(group),10))
#user  system elapsed 
#0.07    0.00    0.06 

#using native data.table
system.time(dti[dti[,list(val=sample(.I,10)),by="group"]$val])
#user  system elapsed 
#0.04    0.00    0.03 

#using data.table with external function call
system.time(dti[, sampleGroup.dt(dti, 10), by=group])
#user  system elapsed 
#0.06    0.02    0.08 

Troy的回答使用data.table的方式正确,值得点赞。我的回答可能会更慢,因为它复制了两次表格。 - marbel
1
+1非常好的比较。但是我不理解你最后一个基准测试的原因?对于每个组,您都会对所有数据进行10个元素的采样。而在dplyr案例中,您正在对attributes执行某些操作。为什么不使用类似于DT第三种情况的函数来对dplyr进行相同的基准测试呢? - Arun
3
此外,基准测试的一个重要方面是看它的 可扩展性 如只有26个组进行聚合,那么很难检测到实际差异。请将代码行更改为:testdata<-data.frame(group=sample(paste("id", 1:1e5, sep=""),10e5,T),runif(10e5)) 并重新运行基准测试。 - Arun
请注意,dplyr 的内部结构(例如 indices 属性)可能会发生变化。不要依赖它们的结构。 - Romain Francois

3
Dplyr 1.0.2现在可以使用各种动词进行子集操作,包括随机抽取slice_sample:https://dplyr.tidyverse.org/reference/slice.html
mtcars %>% 
  slice_sample(n = 10)

并添加一个按类别分组的group by进行抽样:

mtcars %>% 
  group_by(cyl) %>% 
  slice_sample(n = 2)

嗨 @zoë-turner,不知道您是否知道如何为slice_sample设置种子?请参阅我的问题:https://dev59.com/bNq7pIgBRmDukGFEhFcj - WenliL

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