Сегодня, разрабатывая ответ на чей-то вопрос, я понял, что некоторое время полагался на циклы 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
@Frank Это работает, если все строки представляют собой числовые данные (а они есть), я не думал просто сравнивать суммы. Предположим, что некоторые из строк содержат строки для сопоставления, смешанные с числовыми данными. Я отредактирую свой вопрос.
Тот факт, что R умеет обрабатывать данные векторизованным способом, не умаляет циклов. У них есть свое место в любом R-коде.





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 был ближе к ожидаемому.
Потому что написано "совпадение"/"не совпадение"? Или потому что он был добавлен в сам DF? Потому что либо это чрезвычайно просто с моим решением (или Wimpel, который, откровенно говоря, лучше и менее запутан).
имейте в виду, что paste0(1,0,0) дает тот же результат, что и paste0(10,0) ! Вероятно, должно быть хорошо, но, возможно, лучше быть в безопасности и использовать разделитель (в этом конкретном случае использования)....
@iod Потому что он изменил существующий df, что в конечном итоге просто сохраняет шаг.
Удивительно, но я не могу заставить этот метод работать с нецелочисленными числовыми данными. Смотрите комментарий к моему ответу для более подробной информации. Я не копал достаточно, чтобы понять, почему, но это очень неожиданно...
Это будет работать в 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
Я подумал, что будет творческий способ сделать это с помощью слияния. Это работает и с символьными данными.
Однако не очень быстрое решение: 'gregor 1829.139 1896.0745 1971.4576 1946.2760 2028.607 2552.494 100 c'
@iod в этом масштабе эталон бессмысленен. Смоделируйте DF с 500 000 строк и Compare с 10 000 строк и посмотрите, что получится.
@iod обновлен с разумными ориентирами. Это очень близко. merge в целом быстрее на крошечный.
@Грегор, не могли бы вы включить ответ do.call в эти тесты?
@Вимпел, конечно! Вы выиграли базовую гонку R!
Рассмотрим еще раз 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
rowSums(DF == Compare[rep(1, nrow(DF)), ]) == ncol(DF)? Если вы имеете в виду примеры с несколькими строками master/compare, возможно, отредактируйте, чтобы проиллюстрировать.