Фильтрация строк в наборе данных по столбцам

У меня есть следующая таблица:

FN LN LN1 LN2 LN3 LN4 LN5
a   b   b   x   x   x   x
a   c   b   d   e   NA  NA
a   d   c   a   b   x   x
a   e   b   c   d   x   e

Я фильтрую записи, для которых LN присутствует в LN1-LN5.

Код, который я использовал:

testFilter = filter(test, LN %in% c(LN1, LN2, LN3, LN4, LN5)) 

Результат - не то, что я ожидаю:

ï..FN LN LN1 LN2 LN3  LN4  LN5
1     a  b   b   x   x    x    x
2     a  c   b   d   e <NA> <NA>
3     a  d   c   a   b    x    x
4     a  e   b   c   d    x    e

Я понимаю, что c(LN1, LN2, LN3, LN4, LN5) дает: "b" "b" "c" "b" "x" "d" "a" "c" "x" "e" "b" "d" "x" NA "x" "x" "x" NA "x" "e" и знать, что это ошибка.

В идеале я хочу вернуть только 1-й и 4-й записи.

FN LN LN1 LN2 LN3 LN4 LN5
a   b   b   x   x   x   x
a   e   b   c   d   x   e

Я хочу отфильтровать их только с использованием имен столбцов. Это всего лишь подмножество записей 5.4M.

Ответ 1

Использование:

# data
df1 <- read.table(text = "
FN LN LN1 LN2 LN3 LN4 LN5
a   b   b   x   x   x   x
a   c   b   d   e   NA  NA
a   d   c   a   b   x   x
a   e   b   c   d   x   e", header = TRUE, stringsAsFactors = FALSE)


df1[ apply(df1, 1, function(i) i[2] %in% i[3:7]), ]
#   FN LN LN1 LN2 LN3 LN4 LN5
# 1  a  b   b   x   x   x   x
# 4  a  e   b   c   d   x   e

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

Ответ 2

Существует альтернативный подход с использованием data.table и Reduce():

library(data.table)
cols <- paste0("LN", 1:5)
setDT(test)[test[, .I[Reduce('|', lapply(.SD, function(x) !is.na(x) & LN == x))], 
                 .SDcols = cols]]
   FN LN LN1 LN2 LN3 LN4 LN5
1:  a  b   b   x   x   x   x
2:  a  e   b   c   d   x   e

Данные

library(data.table)
test <- fread(
"FN LN LN1 LN2 LN3 LN4 LN5
  a   b   b   x   x   x   x
  a   c   b   d   e   NA  NA
  a   d   c   a   b   x   x
  a   e   b   c   d   x   e")

эталонный тест

library(data.table)
library(dplyr)
n_row <- 1e6L
set.seed(123L)
DT <- data.table(
  FN = "a",
  LN = sample(letters, n_row, TRUE))
cols <- paste0("LN", 1:5)
DT[, (cols) := lapply(1:5, function(x) sample(c(letters, NA), n_row, TRUE))]
DT
df1 <- as.data.frame(DT)

bm <- microbenchmark::microbenchmark(
  zx8754 = {
    df1[ apply(df1, 1, function(i) i[2] %in% i[3:7]), ]
  },
  eric = {
    df1[ which(df1$LN == df1$LN1 |
                 df1$LN == df1$LN2 |
                 df1$LN == df1$LN3 |
                 df1$LN == df1$LN4 |
                 df1$LN == df1$LN5), ]
  },
  uwe = {
    DT[DT[, .I[Reduce('|', lapply(.SD, function(x) !is.na(x) & LN == x))], 
          .SDcols = cols]]
  },
  axe = { 
    filter_at(df1, vars(num_range("LN", 1:5)), any_vars(. == LN))
  },
  jaap = {df1[!!rowSums(df1$LN == df1[, 3:7], na.rm = TRUE),]},
  times = 50L
)
print(bm, "ms")
Unit: milliseconds
   expr        min         lq       mean     median         uq       max neval cld
 zx8754 3120.68925 3330.12289 3508.03001 3460.83459 3589.10255 4552.9070    50   c
   eric   69.74435   79.11995  101.80188   83.78996   98.24054  309.3864    50 a  
    uwe   93.26621  115.30266  130.91483  121.64281  131.75704  292.8094    50 a  
    axe   69.82137   79.54149   96.70102   81.98631   95.77107  315.3111    50 a  
   jaap  362.39318  489.86989  543.39510  544.13079  570.10874 1110.1317    50  b

Для 1 M строк жесткое кодированное подмножество является самым быстрым, за которым следуют data.table/Reduce() и dplyr/filter_at. Использование apply() в 60 раз медленнее.

ggplot(bm, aes(expr, time)) + geom_violin() + scale_y_log10() + stat_summary(fun.data = mean_cl_boot)

enter image description here

Ответ 3

не самый простой код, но

df1[ which(df1$LN == df1$LN1 |
           df1$LN == df1$LN2 |
           df1$LN == df1$LN3 |
           df1$LN == df1$LN4 |
           df1$LN == df1$LN5), ]
#>   FN LN LN1 LN2 LN3 LN4 LN5
#> 1  a  b   b   x   x   x   x
#> 4  a  e   b   c   d   x   e

Ответ 4

Быстрое и очень простое решение dplyr:

filter_at(df1, vars(num_range("LN", 1:5)), any_vars(. == LN))

Это очень похоже на производительность, как на жестком закодированном ответе @EricFail, потому что это просто внутренне расширяет вызов:

filter(df1, (LN1 == LN) | (LN2 == LN) | (LN3 == LN) | (LN4 == LN) | (LN5 == LN))

Вместо num_range любые другие помощники select могут использоваться в пределах vars чтобы легко выбирать многие переменные на основе их имен. Или можно напрямую указать позиции столбцов.

Ответ 5

Вы также можете использовать rowSums:

df1[!!rowSums(df1$LN == df1[, 3:7], na.rm = TRUE),]

который дает:

  FN LN LN1 LN2 LN3 LN4 LN5
1  a  b   b   x   x   x   x
4  a  e   b   c   d   x   e

Для теста см. Ответ @Uwe.