基于特定值筛选数据框中的每一列

15

考虑以下数据框:

df <- data.frame(replicate(5,sample(1:10,10,rep=TRUE)))

#   X1 X2 X3 X4 X5
#1   7  9  8  4 10
#2   2  4  9  4  9
#3   2  7  8  8  6
#4   8  9  6  6  4
#5   5  2  1  4  6
#6   8  2  2  1  7
#7   3  8  6  1  6
#8   3  8  5  9  8
#9   6  2  3 10  7
#10  2  7  4  2  9

使用dplyr,如何在不显式命名列的情况下对每一列进行过滤,并筛选出大于2的所有值。

我需要类似于假设的filter_each(funs(. >= 2))的东西。

目前我正在执行:


```R filter_all(my_data, all_vars(.>=2)) ```
df %>% filter(X1 >= 2, X2 >= 2, X3 >= 2, X4 >= 2, X5 >= 2)

这相当于:

df %>% filter(!rowSums(. < 2))

注意:假设我只想筛选前4列,我会执行以下操作:

df %>% filter(X1 >= 2, X2 >= 2, X3 >= 2, X4 >= 2) 
或者
df %>% filter(!rowSums(.[-5] < 2))

是否有更高效的替代方案?

编辑:子问题

如何指定列名并模拟假设的filter_each(funs(. >= 2), -X5)

基准测试子问题

由于我必须在大型数据集上运行此操作,因此我对建议进行了基准测试。

df <- data.frame(replicate(5,sample(1:10,10e6,rep=TRUE)))

mbm <- microbenchmark(
Marat = df %>% filter(!rowSums(.[,!colnames(.) %in% "X5", drop = FALSE] < 2)),
Richard = filter_(df, .dots = lapply(names(df)[names(df) != "X5"], function(x, y) { call(">=", as.name(x), y) }, 2)),
Docendo = df %>% slice(which(!rowSums(select(., -matches("X5")) < 2L))),
times = 50
)

这里是结果:

#Unit: milliseconds
#    expr       min        lq      mean    median       uq      max neval
#   Marat 1209.1235 1320.3233 1358.7994 1362.0590 1390.342 1448.458    50
# Richard 1151.7691 1196.3060 1222.9900 1216.3936 1256.191 1266.669    50
# Docendo  874.0247  933.1399  983.5435  985.3697 1026.901 1053.407    50

在此输入图片描述


2
它必须使用dplyr吗? - shecode
1
Steven,我认为你用 df %>% filter(!rowSums(. < 2)) 做得很好。 - Marat Talipov
1
Steven,这段代码不如你的优雅,但你可以尝试df%>% filter(!rowSums(.[,!colnames(.)%in%'X5',drop = F] <2)) - Marat Talipov
1
我同意Marat和Richard的观点。 rowSums()在我看来很好! - Gabriel L'Heureux
@StevenBeaupré, 当然可以。 - Marat Talipov
显示剩余4条评论
4个回答

6

这里有一个简单的想法可以帮助您选择名称。您可以设置一个调用列表,发送到filter_().dots参数中。首先,需要创建一个未求值调用的函数。

Call <- function(x, value, fun = ">=") call(fun, as.name(x), value)

现在我们使用filter_(),将调用列表传递到.dots参数中,使用lapply(),选择任何您想要的名称和值。
nm <- names(df) != "X5"
filter_(df, .dots = lapply(names(df)[nm], Call, 2L))
#   X1 X2 X3 X4 X5
# 1  6  5  7  3  1
# 2  8 10  3  6  5
# 3  5  7 10  2  5
# 4  3  4  2  9  9
# 5  8  3  5  6  2
# 6  9  3  4 10  9
# 7  2  9  7  9  8

你可以查看由 Call() 创建的未计算调用,例如 X4X5,使用以下方法:

lapply(names(df)[4:5], Call, 2L)
# [[1]]
# X4 >= 2L
#
# [[2]]
# X5 >= 2L

因此,如果您在lapply()函数的X参数中调整了names(),那么就可以了。


5
如何指定列名并模拟一个假设的filter_each(funs(. >= 2), -X5)?这可能不是最优雅的解决方案,但它可以完成工作。
df %>% filter(!rowSums(.[,!colnames(.)%in%'X5',drop=F] < 2))

如果有多个需要排除的列(例如X3、X5),可以使用以下方法:

df %>% filter(!rowSums(.[,!colnames(.)%in%c('X3','X5'),drop=F] < 2))

2
дҪҝз”Ё names еҸҜиғҪжҜ” colnames жӣҙеҝ«пјҢеӣ дёә names жҳҜеҺҹиҜӯгҖӮ - Rich Scriven

4

这里有另一种选项,可以使用 slice,在这种情况下可以类似于使用 filter。主要区别是您向 slice 提供整数向量,而 filter 接受逻辑向量。

df %>% slice(which(!rowSums(select(., -matches("X5")) < 2L)))

我喜欢这种方法的原因是因为我们在rowSums函数中使用select,你可以利用select提供的所有特殊函数,例如matches


让我们看看它与其他答案的比较:

df <- data.frame(replicate(5,sample(1:10,10e6,rep=TRUE)))

mbm <- microbenchmark(
    Marat = df %>% filter(!rowSums(.[,!colnames(.) %in% "X5", drop = FALSE] < 2)),
    Richard = filter_(df, .dots = lapply(names(df)[names(df) != "X5"], function(x, y) { call(">=", as.name(x), y) }, 2)),
    dd_slice = df %>% slice(which(!rowSums(select(., -matches("X5")) < 2L))),
    times = 50L,
    unit = "relative"
)

#Unit: relative
#     expr      min       lq   median       uq      max neval
#    Marat 1.304216 1.290695 1.290127 1.288473 1.290609    50
#  Richard 1.139796 1.146942 1.124295 1.159715 1.160689    50
# dd_slice 1.000000 1.000000 1.000000 1.000000 1.000000    50

pic

编辑说明:更新了50次重复的更可靠的基准测试数据(times = 50L)。


针对一条评论,声称使用基础R语言会与slice方法具有相同的速度(未明确说明是哪种基础R语言方法),我决定使用几乎与我的答案相同的方法来比较基础R语言。 对于基础R,我使用了:

base = df[!rowSums(df[-5L] < 2L), ],
base_which = df[which(!rowSums(df[-5L] < 2L)), ]

基准测试:

df <- data.frame(replicate(5,sample(1:10,10e6,rep=TRUE)))

mbm <- microbenchmark(
  Marat = df %>% filter(!rowSums(.[,!colnames(.) %in% "X5", drop = FALSE] < 2)),
  Richard = filter_(df, .dots = lapply(names(df)[names(df) != "X5"], function(x, y) { call(">=", as.name(x), y) }, 2)),
  dd_slice = df %>% slice(which(!rowSums(select(., -matches("X5")) < 2L))),
  base = df[!rowSums(df[-5L] < 2L), ],
  base_which = df[which(!rowSums(df[-5L] < 2L)), ],
  times = 50L,
  unit = "relative"
)

#Unit: relative
#       expr      min       lq   median       uq      max neval
#      Marat 1.265692 1.279057 1.298513 1.279167 1.203794    50
#    Richard 1.124045 1.160075 1.163240 1.169573 1.076267    50
#   dd_slice 1.000000 1.000000 1.000000 1.000000 1.000000    50
#       base 2.784058 2.769062 2.710305 2.669699 2.576825    50
# base_which 1.458339 1.477679 1.451617 1.419686 1.412090    50

pic2

这两种基于R语言的方法性能并不是特别好,也没有可比性。

编辑注释 #2:添加了使用基本R选项的基准测试。


1
这种方式更自然,更符合dplyr的风格,并且实际上更高效。将在原始帖子中更新基准测试结果。 - Steven Beaupré
这非常巧妙。很好的答案。 - Rich Scriven
@ColonelBeauvel,真的吗?我用基本 R 进行了基准测试,速度比较慢。很想看看您的意思是什么。 - talat
实际上,我使用了我的唯一解决方案来运行微基准测试,因此这导致了1!你是正确的! - Colonel Beauvel

2
如果您只想在前四列进行过滤,可以这样实现:
df %>% filter(X1 >= 2, X2 >= 2, X3 >= 2, X4 >= 2) 

尝试这个:

df %>% 
  filter_at(vars(X1:X4), #<Select columns to filter
  all_vars(.>=2) )       #<Scope with all_vars (or any_vars)

有一种替代方法是排除你想要过滤的列,如下:

df %>% 
  filter_at(vars(-X5)), #<Exclude column X5
  all_vars(.>=2) )

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