使用dplyr在多列上过滤多个条件

12

我在SO上搜索尝试找到解决方案,但没有成功。这是我的数据框中有许多列,其中一些是数值型的,并且应该是非负的。由于这些数值列中的某些值为负,因此我想清理数据。现在我可以使用正则表达式提取这些列的列名,但我不确定如何基于这些列实现行过滤。

举个例子,假设:

library(dplyr)
df <- read.table(text = 
  "id   sth1    tg1_num   sth2    tg2_num    others   
  1     dave    2         ca      35         new
  2     tom     5         tn      -3         old
  3     jane    -3        al       0         new
  4     leroy   0         az      25         old
  5     jerry   4         mi      55        old", header=TRUE)
pattern <- "_num$"
ind <- grep(pattern, colnames(df))
target_columns <- colnames(df)[ind]
df <- df %>% filter(target_columns >= 0) # it's is wrong, but it's what I want to do

我希望通过这种过滤获得以下内容:

id   sth1 tg1_num   sth2 tg2_num others
1    dave       2     ca      35    new
4   leroy       0     az      25    old
5   jerry       4     mi      55    old

由于tg1_num和tg2_num列中至少有一列包含负数,所以第2行和第3行被过滤掉。


1
df %>% select(matches("_num$")) - Vlo
1
你想要什么样的输出?是要整个数据集还是只有符合模式的部分?你希望两列都大于或等于零,还是只需要其中一列满足条件即可?请展示给我们最终的产品。 - David Arenburg
@Vlo 这是选择目标列的一种方式。但并不能解决我的问题。 - breezymri
7个回答

6

这里有一个可能的向量化解决方案。

ind <- grep("_num$", colnames(df))
df[!rowSums(df[ind] < 0),]
#   id  sth1 tg1_num sth2 tg2_num others
# 1  1  dave       2   ca      35    new
# 4  4 leroy       0   az      25    old
# 5  5 jerry       4   mi      55    old

这里的想法是使用<函数创建一个逻辑矩阵(它是一个通用函数,具有data.frame方法 - 这意味着它返回一个类似数据框的结构)。然后,我们使用rowSums查找是否有任何匹配条件(>0 - 匹配,0- 不匹配)。然后,我们使用!函数将其转换为逻辑向量:>0变成了TRUE,而0变成了FALSE。最后,我们根据该向量进行子集划分。

谢谢。这是一个好的和直观的解决方案。我接受了@user295691的答案,因为我认为他的答案很全面。使用rowMins可能比使用rowSums更快。 - breezymri

4
这是对 dplyr 的非常拙劣的使用,但可能符合其精神。
> df %>% mutate(m = do.call(pmin, select(df, ends_with("_num"))))
  id  sth1 tg1_num sth2 tg2_num others  m
1  1  dave       2   ca      35    new  2
2  2   tom       5   tn      -3    old -3
3  3  jane      -3   al       0    new -3
4  4 leroy       0   az      25    old  0
5  5 jerry       4   mi      55    old  4

从那里,您可以添加一个 filter(m >= 0) 来获得您想要的答案。如果存在类似于 rowMeansrowMins,那么这将显着简化此过程。

> rowMins <- function(df) { do.call(pmin, df) }
> df %>% mutate(m = rowMins(select(df, ends_with("_num"))))
  id  sth1 tg1_num sth2 tg2_num others  m
1  1  dave       2   ca      35    new  2
2  2   tom       5   tn      -3    old -3
3  3  jane      -3   al       0    new -3
4  4 leroy       0   az      25    old  0
5  5 jerry       4   mi      55    old  4

我不知道这是否高效。嵌套select看起来真的很丑。

编辑3:借鉴了其他解决方案/评论的想法(感谢@Vlo),我可以大大加快我的速度(不幸的是,类似的优化会使@Vlo的解决方案速度更快(编辑4:哎呀,读错图表了,我是最快的,好的,就不再讨论了))

df %>% select(ends_with("_num")) %>% rowMins %>% {df[. >= 0,]}

编辑:出于好奇,我对一些解决方案进行了微基准测试(编辑2:添加了更多的解决方案)

microbenchmark(rowmins(df), rowmins2(df), reducer(df), sapplyer(df), grepapply(df), tchotchke(df), withrowsums(df), reducer2(df))

Unit: microseconds
            expr       min         lq      mean    median        uq       max
     rowmins(df)  1373.452  1431.9700  1732.188  1576.043  1729.410  5147.847
    rowmins2(df)   836.885   875.9900  1015.364   913.285  1038.729  2510.339
     reducer(df)   990.096  1058.6645  1217.264  1201.159  1297.997  3103.809
    sapplyer(df) 14119.236 14939.8755 16820.701 15952.057 16612.709 66023.721
   grepapply(df) 12907.657 13686.2325 14517.140 14485.520 15146.294 17291.779
   tchotchke(df)  2770.818  2939.6425  3114.233  3036.926  3172.325  4098.161
 withrowsums(df)  1526.227  1627.8185  1819.220  1722.430  1876.360  3025.095
    reducer2(df)   900.524   943.1265  1087.025  1003.820  1109.188  3869.993

以下是我使用的定义:

rowmins <- function(df) {
  df %>%
    mutate(m = rowMins(select(df, ends_with("_num")))) %>%
    filter(m >= 0) %>%
    select(-m)
}

rowmins2 <- function(df) {
  df %>% select(ends_with("_num")) %>% rowMins %>% {df[. >= 0,]}
}

reducer <- function(df) {
  df %>%
    select(matches("_num$")) %>%
    lapply(">=", 0) %>%
    Reduce(f = "&", .) %>%
    which %>%
    slice(.data = df)
}

reducer2 <- function(df) {
  df %>%
    select(matches("_num$")) %>%
    lapply(">=", 0) %>%
    Reduce(f = "&", .) %>%
    {df[.,]}
}

sapplyer <- function(df) {
  nums <- sapply(df, is.numeric)
  df[apply(df[, nums], MARGIN=1, function(x) all(x >= 0)), ]
}

grepapply <- function(df) {
  cond <- df[, grepl("_num$", colnames(df))] >= 0
    df[apply(cond, 1, function(x) {prod(x) == 1}), ]
}

tchotchke <- function(df) {
  pattern <- "_num$"
  ind <- grep(pattern, colnames(df))
  target_columns <- colnames(df)[ind]
  desired_rows <- sapply(target_columns, function(x) which(df[,x]<0), simplify=TRUE)
  as.vector(unique(unlist(desired_rows)))
}

withrowsums <- function(df) {
  df %>% mutate(m=rowSums(select(df, ends_with("_num"))>0)) %>% filter(m==2) %>% select(-m)
}


df <- data.frame(id=1:10000, sth1=sample(LETTERS, 10000, replace=T), tg1_num=runif(10000,-1,1), tg2_num=runif(10000,-1, 1))

看这个。 df 必须包含像 OP 的样本数据集一样的负整数和正整数(没有小数位)。superreducer <- function(df) { df %>% select(matches("_num$")) %>% Reduce(bitwOr, .) %>% {.>=0L} %>% which %>% slice(.data = df) }reducer() 快20%。 - Vlo
花括号 {df[. >= 0,]} 的意思是什么? - dpprdan
1
@dapperdan:这是magrittr的一个小问题;如果您将管道运行到匿名块(即括在大括号中),则上一个命令的结果将存储在“.”中--因此3%>% {。+1}会产生4。因此,这意味着过滤所有前一个元素大于零的行。 - user295691

4
我想通过使用dplyr的标准评估函数filter_来实现这一点。结果发现可以借助于lazyeval中的interp,参考本页面上的示例代码即可实现。实质上,您需要创建一个interp条件列表,然后将其传递给filter_.dots参数。
library(lazyeval)

dots <- lapply(target_columns, function(cols){
    interp(~y >= 0, .values = list(y = as.name(cols)))
})

filter_(df, .dots = dots)   

  id  sth1 tg1_num sth2 tg2_num others
1  1  dave       2   ca      35    new
2  4 leroy       0   az      25    old
3  5 jerry       4   mi      55    old

更新

dplyr_0.7 开始,可以直接使用 filter_atall_vars 进行操作(不需要 lazyeval)。

df %>%
     filter_at(vars(target_columns), all_vars(. >= 0) )

  id  sth1 tg1_num sth2 tg2_num others
1  1  dave       2   ca      35    new
2  4 leroy       0   az      25    old
3  5 jerry       4   mi      55    old

2

这是我的丑陋解决方案。欢迎提出建议/批评。

df %>% 
  # Select the columns we want
  select(matches("_num$")) %>%
  # Convert every column to logical if >= 0
  lapply(">=", 0) %>%
  # Reduce all the sublist with AND 
  Reduce(f = "&", .) %>%
  # Convert the one vector of logical into numeric
  # index since slice can't deal with logical. 
  # Can simply write `{df[.,]}` here instead,
  # which is probably faster than which + slice
  # Edit: This is not true. which + slice is faster than `[` in this case
  which %>%
  slice(.data = df)

  id  sth1 tg1_num sth2 tg2_num others
1  1  dave       2   ca      35    new
2  4 leroy       0   az      25    old
3  5 jerry       4   mi      55    old

到目前为止,这似乎是最快的提议;我的回答中有一些基准测试。 - user295691
@user295691 不用了,看起来使用 which + slice 比基本的 R subset 快得多。 - Vlo

1
使用基本的 R 语言来获取你的结果。
cond <- df[, grepl("_num$", colnames(df))] >= 0
df[apply(cond, 1, function(x) {prod(x) == 1}), ]

  id  sth1 tg1_num sth2 tg2_num others
1  1  dave       2   ca      35    new
4  4 leroy       0   az      25    old
5  5 jerry       4   mi      55    old

编辑:这假定您有多个带“_num”的列。如果您只有一个_num列,则无法正常工作。

1

首先,我们创建一个所有数值列的索引。然后我们对所有大于或等于零的列进行子集筛选。因此,无需检查列名,且列ID始终为正数。

nums <- sapply(df, is.numeric)
df[apply(df[, nums], MARGIN = 1, function(x) all(x >= 0)), ]

输出:

  id  sth1 tg1_num sth2 tg2_num others
1  1  dave       2   ca      35    new
4  4 leroy       0   az      25    old
5  5 jerry       4   mi      55    old

0

这将为您提供一个向量,其中包含小于0的行:

desired_rows <- sapply(target_columns, function(x) which(df[,x]<0), simplify=TRUE)
desired_rows <- as.vector(unique(unlist(desired_rows)))

然后获取您所需行的数据框:

setdiff(df, df[desired_rows,])
  id  sth1 tg1_num sth2 tg2_num others
1  1  dave       2   ca      35    new
2  4 leroy       0   az      25    old
3  5 jerry       4   mi      55    old

这看起来会起作用。但是,我想避免使用循环。我的数据相当大,可能会运行得非常慢。 - breezymri
@Tchotchke 只是出于兴趣,您认为在您的第一行代码中也可以使用 filter(...) 吗? - maj

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