【问题标题】:Filter each column of a data.frame based on a specific value根据特定值过滤 data.frame 的每一列
【发布时间】:2015-03-26 20:17:18
【问题描述】:

考虑以下数据框:

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(. &gt;= 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(. &gt;= 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

【问题讨论】:

  • 一定要用dplyr吗?
  • 史蒂文,我想你用df %&gt;% filter(!rowSums(. &lt; 2))搞定了
  • @MaratTalipov 我猜是的。但如果能够仅指定您不想过滤的列 name 会很方便。类似于假设的filter_each(funs(. &gt;= 2), -X5)
  • 史蒂文,它没有你的代码那么优雅,但你可以试试df %&gt;% filter(!rowSums(.[,!colnames(.)%in%'X5',drop=F] &lt; 2))
  • 我同意 Marat 和 Richard 的观点。 rowSums() 我觉得不错!

标签: r dplyr


【解决方案1】:

这是一个让选择名称变得相当简单的想法。您可以设置要发送到filter_().dots 参数的调用列表。首先是一个创建未评估调用的函数。

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

现在我们使用filter_(),使用lapply() 将调用列表传递给.dots 参数,选择您想要的任何名称和值。

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(),应该没问题。

【讨论】:

    【解决方案2】:

    如何指定列名并模仿假设的 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))
    

    【讨论】:

    • 使用names 可能比colnames 更快,因为names 是原始的
    【解决方案3】:

    这是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
    

    编辑说明:更新了更可靠的基准,重复 50 次(次数 = 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
    

    这两种基本 R 方法实际上并没有更好或可比的性能。

    编辑注释#2:添加了带有基本 R 选项的基准。

    【讨论】:

    • 这感觉更自然,更dplyr-esque,实际上更高效。将更新 OP 中的基准。
    • 这很狡猾。不错的答案
    • @ColonelBeauvel,真的吗?我用base R做了基准测试,它比较慢。有兴趣了解您的意思
    • @ 实际上我用我唯一的解决方案运行了微基准测试,所以这导致了 1!你是对的!
    【解决方案4】:

    如果您只想过滤前四列,如:

    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) )
    

    【讨论】:

      猜你喜欢
      • 2018-02-16
      • 2021-06-10
      • 1970-01-01
      • 2016-01-06
      • 1970-01-01
      • 2020-04-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多