Отфильтруйте каждый столбец data.frame на основе определенного значения

Рассмотрим следующий фрейм данных:

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

Сейчас я делаю:

df %>% filter(X1 >= 2, X2 >= 2, X3 >= 2, X4 >= 2, X5 >= 2)

Что эквивалентно:

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

Примечание. Скажем, я хотел отфильтровать только первые четыре столбца, я бы сделал:

df %>% filter(X1 >= 2, X2 >= 2, X3 >= 2, X4 >= 2) 

или

df %>% filter(!rowSums(.[-5] < 2))

Будет ли более эффективная альтернатива?

Изменить: дополнительный вопрос

Как указать имя столбца и подражать гипотетическому filter_each(funs(. >= 2), -X5)?

Подкатегория Benchmark

Поскольку я должен запускать это на большом наборе данных, я сравнивал эти предложения.

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

enter image description here

Ответы

Ответ 1

Вот еще один вариант с slice, который можно использовать аналогично filter в этом случае. Основное различие заключается в том, что вы отправляете целочисленный вектор в slice, тогда как filter принимает логический вектор.

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

Что мне нравится в этом подходе, так это то, что, поскольку мы используем select внутри rowSums, вы можете использовать все специальные функции, которые 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)), ]

Benchmark:

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.

Ответ 2

Вот идея, которая позволяет довольно просто выбирать имена. Вы можете настроить список вызовов для отправки в аргумент .dots filter_(). Сначала функция, которая создает неоценимый вызов.

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(), например X4 и X5, с

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

Итак, если вы отредактируете names() в аргументе X lapply(), вы должны быть в порядке.

Ответ 3

Как указать имя столбца и имитировать гипотетический 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))