Сравните строки фрейма данных с «главной» строкой/с

Сегодня, разрабатывая ответ на чей-то вопрос, я понял, что некоторое время полагался на циклы for , что, как правило, является грехом, учитывая, что R известен тем, насколько хорошо он может использовать векторизованные функции.

Скажем, у нас есть фрейм данных, в котором мы храним наши «условия», это строки данных, которые мы хотим использовать для сравнения с нашими данными. Если одна из наших «главных» строк полностью соответствует строке фрейма данных, мы хотим напечатать «совпадение».

# Data to be checked
DF<-data.frame(A=c(1,4,5,6),B=c(1,5,4,2),C=c(5,6,3,2),D=c(1,2,3,4),
               E=c(4,2,3,4))

# Our condition/master data
Compare<-data.frame(A=4,B=5,C=6,D=2,E=2)

Используя цикл for, это простая задача, но она утомительна, когда мы создаем все большие и большие наборы данных:

#This Works, but ew
for(i in 1:length(DF)){
    ifelse(DF[i,]==Compare, print("match"),print("no match"))
}
[1] "no match"
[1] "match"
[1] "no match"
[1] "no match"

Мой вопрос в том, как бы кто-то сделал это без использования цикла for в базе R. Я понимаю, что такие пакеты, как compare и sqldf, могут сделать это с легкостью, но мне интересно, может ли кто-нибудь сделать это в базе R. Я думаю, что это очевидно, но я не могу понять это.

Обновлено: Как @Frank ответил в комментариях rowSums(DF == Compare[rep(1, nrow(DF)), ]) == ncol(DF) отлично работает, если все данные числовые. Давайте усложним ситуацию и скажем, что в нашем мастер-списке также есть строковые данные, которые нужно сопоставить.

# Sample Data
DF<-data.frame(
A=c("N","J","K","L"),
B=c(1,3,4,2),
C=c(5,4,3,2),
D=c(1,5,3,4),
E=c(4,2,3,4),stringsAsFactors=F)

Compare<-data.frame(A = "J",B=3,C=4,D=5,E=2)

#This Works
for(i in 1:length(DF)){
    ifelse(DF[i,]==Compare, print("match"),print("no match"))
}

Исследование:

Я вижу, что мы могли бы использовать «слияние» для сравнения, но это не позволило бы мне узнать, где мои совпадения были в исходных данных, я буду видеть только возвращенные совпадения:

Получить все строки во фрейме данных, соответствующие строкам другого фрейма данных

Что-то ниже вроде как делает это, но невозможно масштабировать.

which(DF$A == Compare$A & DF$B==Compare$B & DF$C == Compare$C) #etc.
[1] 2

Вернуть строку фрейма данных на основе значения в столбце - R

rowSums(DF == Compare[rep(1, nrow(DF)), ]) == ncol(DF)? Если вы имеете в виду примеры с несколькими строками master/compare, возможно, отредактируйте, чтобы проиллюстрировать.
Frank 12.03.2019 20:40

@Frank Это работает, если все строки представляют собой числовые данные (а они есть), я не думал просто сравнивать суммы. Предположим, что некоторые из строк содержат строки для сопоставления, смешанные с числовыми данными. Я отредактирую свой вопрос.

Chabo 12.03.2019 20:44

Тот факт, что R умеет обрабатывать данные векторизованным способом, не умаляет циклов. У них есть свое место в любом R-коде.

Roman Luštrik 12.03.2019 22:25
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
3
93
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

apply(DF,1,paste,collapse = " ") %in% apply(Compare,1,paste,collapse = " ")
[1] FALSE  TRUE FALSE FALSE

Это своего рода мошенничество, потому что применение само по себе является циклом, но это все же намного быстрее, чем ваше решение:

Unit: microseconds
 expr      min       lq       mean    median         uq       max neval cld
  iod  375.289   488.41   627.6017   570.742   705.7295  2050.474   100  a 
   op 9070.273 10491.32 14143.7312 11770.471 15281.4865 96645.749   100   b

Спасибо, это тоже сработало (как числовое, так и символьное), результат @Gregor был ближе к ожидаемому.

Chabo 12.03.2019 21:01

Потому что написано "совпадение"/"не совпадение"? Или потому что он был добавлен в сам DF? Потому что либо это чрезвычайно просто с моим решением (или Wimpel, который, откровенно говоря, лучше и менее запутан).

iod 12.03.2019 21:03

имейте в виду, что paste0(1,0,0) дает тот же результат, что и paste0(10,0) ! Вероятно, должно быть хорошо, но, возможно, лучше быть в безопасности и использовать разделитель (в этом конкретном случае использования)....

Wimpel 12.03.2019 21:04

@iod Потому что он изменил существующий df, что в конечном итоге просто сохраняет шаг.

Chabo 12.03.2019 21:13

Удивительно, но я не могу заставить этот метод работать с нецелочисленными числовыми данными. Смотрите комментарий к моему ответу для более подробной информации. Я не копал достаточно, чтобы понять, почему, но это очень неожиданно...

Gregor Thomas 12.03.2019 22:35

Это будет работать в baseR

DF[ do.call("paste", DF) %in% do.call("paste", Compare), ]

#   A B C D E
# 2 4 5 6 2 2

или просто:

do.call("paste", DF) %in% do.call("paste", Compare)
#[1] FALSE  TRUE FALSE FALSE
Ответ принят как подходящий

Просто используйте merge:

Compare$in_compare = "match"
merge(DF, Compare, all.x = TRUE)
#   A B C D E in_compare
# 1 1 1 5 1 4       <NA>
# 2 4 5 6 2 2      match
# 3 5 4 3 3 3       <NA>
# 4 6 2 2 4 4       <NA>

Если вы предпочитаете не изменять Compare, мы можем использовать transform для модификации на месте в однострочнике:

merge(DF, transform(Compare, in_compare = "match"), all.x = TRUE)

Сроки

set.seed(47)
data(diamonds, package = "ggplot2")
diam = unique(diamonds)
DF = diam[rep(1:nrow(diam), times = 10), ]
# 538k rows, 10 columns

compare_rows = sample(nrow(diam), size = 10000)
compare_df = diam[compare_rows, ]

# test:
merge_result = merge(DF, transform(compare_df, result = "match"), all.x = TRUE)
apply_paste_result = transform(DF, result = apply(DF, 1, paste, collapse = " ") %in% apply(compare_df, 1, paste, collapse = " "))
do_call_paste_result = transform(DF, result = do.call("paste", DF) %in% do.call("paste", compare_df))

sum(merge_result$result == "match", na.rm = TRUE) #10000
sum(apply_paste_result$result) # 0!!!
sum(do_call_paste_result$result) #10000

Немного покопался, похоже, что метод apply_paste плохо работает с десятичными знаками. Я не копал достаточно глубоко, чтобы понять, почему или попытаться отладить, но здесь это явно не работает. Тот же код отлично работает на игрушечном примере. При внимательном рассмотрении do.call("paste", head(DF)) удивительно дает другой результат, чем apply(head(DF), 1, paste, collapse = " "), в 4-й строке метод apply имеет "4.2", где метод do.call имеет "4.20"... возможно, это связано. Я предполагаю, что что-то происходит, когда apply преобразует фрейм данных в матрицу, но, как я уже сказал, это кажется удивительным.

Итак, исключая этот метод из теста, похоже, что метод вставки do.call самый быстрый! Лично я думаю, что я бы все же выбрал merge, так как он кажется подходящим инструментом для работы... странная ошибка в методе apply_paste иллюстрирует риск умных хаков.

Если производительность составляет В самом деле, то data.table будет значительно быстрее, чем любой из этих методов:

library(data.table)
DT = as.data.table(DF)
compare_dt = as.data.table(compare_df)
dt_result = merge(DT, compare_dt[, result := "match"], all.x = TRUE)
sum(dt_result$result == "match", na.rm = TRUE) #10000


library(microbenchmark)
microbenchmark(
  merge = merge(DF, transform(Compare, in_compare = "match"), all.x = TRUE),
  do_call_paste = transform(DF, result = do.call("paste", DF) %in% do.call("paste", compare_df)),
  dt_result = merge(DT, compare_dt[, result := "match"], all.x = TRUE)
  times = 20
)
# Unit: milliseconds
#           expr       min        lq      mean    median       uq       max neval
#          merge 5905.6842 6171.2211 6473.5351 6253.0117 6946.098 7246.2727    10
#  do_call_paste 4269.8849 4635.2662 4850.4076 4824.5035 5196.911 5289.6226    10
#      dt_result  460.4646  494.7083  555.8484  538.2203  614.905  683.1518    10

Я подумал, что будет творческий способ сделать это с помощью слияния. Это работает и с символьными данными.

Chabo 12.03.2019 20:59

Однако не очень быстрое решение: 'gregor 1829.139 1896.0745 1971.4576 1946.2760 2028.607 2552.494 100 c'

iod 12.03.2019 21:02

@iod в этом масштабе эталон бессмысленен. Смоделируйте DF с 500 000 строк и Compare с 10 000 строк и посмотрите, что получится.

Gregor Thomas 12.03.2019 21:06

@iod обновлен с разумными ориентирами. Это очень близко. merge в целом быстрее на крошечный.

Gregor Thomas 12.03.2019 21:17

@Грегор, не могли бы вы включить ответ do.call в эти тесты?

Wimpel 12.03.2019 21:53

@Вимпел, конечно! Вы выиграли базовую гонку R!

Gregor Thomas 12.03.2019 22:34

Рассмотрим еще раз merge, так как вы можете указать all.x (т. е. LEFT JOIN), чтобы сохранить исходные данные. Ниже используются within и transform в качестве менеджеров контекста для добавления/обновления столбца результат и возврата обновленного фрейма данных:

final_df <- within(merge(DF, transform(Compare, result = "match"), all.x=TRUE),
                   result <- ifelse(is.na(result), "no match", "match"))    

final_df
#   A B C D E   result
# 1 1 1 5 1 4 no match
# 2 4 5 6 2 2    match
# 3 5 4 3 3 3 no match
# 4 6 2 2 4 4 no match

Другие вопросы по теме