R как ускорить сопоставление шаблонов с помощью векторов

#r #string #dataframe

#r #строка #фрейм данных

Вопрос:

У меня есть столбец в одном фрейме данных с названиями городов и штатов в нем:

ac lt;- c("san francisco ca", "pittsburgh pa", "philadelphia pa", "washington dc", "new york ny", "aliquippa pa", "gainesville fl", "manhattan ks")

ac lt;- as.data.frame(ac)

Я хотел бы найти значения ac$ac в другом столбце фрейма данных d$description и вернуть значение столбца, id если есть совпадение.

 dput(df) structure(list(month = c(202110L, 201910L, 202005L, 201703L,  201208L, 201502L), id = c(100559687L, 100558763L, 100558934L,  100558946L, 100543422L, 100547618L), description = c("residential local telephone service local with more san francisco ca flat rate with eas package plan includes voicemail call forwarding call waiting caller id call restriction three way calling id block speed dialing call return call screening modem rental voip transmission telephone access line 34 95 modem rental 7 00 total 41 95",  "digital video programming service multilatino ultra bensalem pa service includes digital economy multilatino digital preferred tier and certain additonal digital channels coaxial cable transmission",  "residential all distance telephone service unlimited voice only harrisburg pa flat rate with eas only features call waiting caller id caller id with call waiting call screening call forwarding call forwarding selective call return 69 3 way calling anonymous call rejection repeat dialing speed dial caller id blocking coaxial cable transmission",  "residential all distance telephone service unlimited voice only pittsburgh pa flat rate with eas only features call waiting caller id caller id with call waiting call screening call forwarding call forwarding selective call return 69 3 way calling anonymous call rejection repeat dialing speed dial caller id blocking",  "local spot advertising 30 second advertisement austin tx weekday 6 am 6 pm other audience demographic w18 49 number of rating points for daypart 0 29 average cpp 125",  "residential public switched toll interstate manhattan ks ks plan area residence switched toll base period average revenue per minute 0 18 minute online" )), row.names = c(1L, 1245L, 3800L, 10538L, 20362L, 50000L), class = "data.frame")  

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

  1. which(ac$ac %in% df$description) —это возвращается integer(0) .
  2. grep(ac$ac, df$description, value = FALSE) —это возвращает первый индекс, 1. Но он не векторизован.
  3. str_detect(string = ac$ac, pattern = df$description) — но это возвращает все FALSE , что неверно.

Мой вопрос: как мне выполнить поиск ac$ac в df$description и вернуть соответствующее значение df$id в случае совпадения? Обратите внимание, что векторы не имеют одинаковой длины. Я ищу ВСЕ совпадения, а не только первое. Я бы предпочел что-то простое и быстрое, потому что фактические наборы данных, которые я буду использовать, содержат более 100 тысяч строк в каждом, но любые предложения или идеи приветствуются. Спасибо.

Редактировать. В связи с приведенным ниже первоначальным ответом Андре название вопроса было изменено с учетом изменения объема вопроса.

Правка (12/7): щедрость добавлена для создания дополнительного интереса и быстрого, эффективного масштабируемого решения.

Редактировать (12/8): Уточнение-я хотел бы иметь возможность добавить id переменную из df ac в фрейм данных, как в ac$id .

Комментарии:

1. Вопрос изменяется после того, как дан ответ. Имя переменной было заменено. Если вы измените значительную часть своего вопроса, вам лучше добавить свой вопрос новым блоком, иначе люди, добровольно отвечающие на ваши вопросы, будут, по-видимому, терять свое время, поскольку их ответы станут бессмысленными.

2. @asd-tm справедливая точка зрения. Я должен был уточнить свой вопрос. Теперь я отредактировал. Надеюсь, этого будет достаточно.

3. моя заметка была посвящена моему ответу относительно названия переменных

4. Я спрашиваю, потому что в противном случае можно было бы захватить/собрать результат в вектор вместо списка.

5. @javlenti Я обновил свой ответ. Надеюсь, это то, чего вы ожидали сейчас.

Ответ №1:

Самые простые решения, как правило, самые быстрые! Вот мое предложение:

 str = paste0(ac, collapse="|") df$id[grep(str, df$description)]  

Но вы также можете сделать это

 df$id[as.logical(rowSums(!is.na(sapply(ac, function(x) stringr::str_match(df$description, x)))))]  

Или вот так

 df$id[grepl(str, df$description, perl=T)]  

Однако это необходимо сравнить. Кстати, я добавил предложения от @Andre Wildberg и @Martina C. Arnolda. Ниже приведен контрольный показатель.

 str = paste0(ac, collapse="|") fFiolka1 = function() df$id[grep(str, df$description)] fFiolka2 = function() df$id[as.logical(rowSums(!is.na(sapply(ac, function(x) stringr::str_match(df$description, x)))))] fFiolka3 = function() df$id[grepl(str, df$description, perl=T)]  fWildberg1 = function() df$id[unlist(sapply(ac, function(x) grep(x, df$description)))] fWildberg2 = function() df$id[as.logical(rowSums(sapply(ac, function(x) stri_detect_regex(df$description, x))))]  fArnolda1 = function() df[grep(str, df$description), ]["id"] fArnolda2 = function() df[stringi::stri_detect_regex(df$description, str), ]["id"] fArnolda3 = function() df %gt;% filter(description %gt;% str_detect(str)) %gt;% select(id)  library(microbenchmark) ggplot2::autoplot(microbenchmark(  fFiolka1(), fFiolka2(), fFiolka3(),  fWildberg1(), fWildberg2(),  fArnolda1(), fArnolda2(), fArnolda3(),  times=100))  

введите описание изображения здесь

Note, for the sake of simplicity I left ac as a vector !.

 ac lt;- c("san francisco ca", "pittsburgh pa", "philadelphia pa", "washington dc", "new york ny", "aliquippa pa", "gainesville fl", "manhattan ks")  

Special update for @jvalenti

OKAY. Now I understand better what you want to achieve. However, in order to fully show the best solution, I have slightly modified your data. Here they are

 library(tidyverse)  ac lt;- c("san francisco ca", "pittsburgh pa", "philadelphia pa", "washington dc", "new york ny", "aliquippa pa", "gainesville fl", "manhattan ks") ac = tibble(ac = ac)  df = structure(list(  month = c(202110L, 201910L, 202005L, 201703L, 201208L, 201502L),   id = c(100559687L, 100558763L, 100558934L, 100558946L, 100543422L, 100547618L),   description = c(  "residential local telephone pittsburgh pa local with more san francisco ca flat rate with eas philadelphia pa plan includes voicemail call forwarding call waiting caller id call restriction three way calling id block speed dialing call return call screening modem rental voip transmission telephone access line 34 95 modem rental 7 00 total 41 95",  "digital video san francisco ca pittsburgh pa multilatino ultra bensalem pa service includes digital economy multilatino digital preferred tier and certain additonal digital channels coaxial cable transmission",  "residential all distance telephone pittsburgh pa unlimited voice only harrisburg pa flat rate with eas only features call waiting caller id caller id with call waiting call screening call forwarding call forwarding selective call return 69 3 way calling anonymous call rejection repeat dialing speed dial caller id blocking coaxial cable transmission",  "residential all distance telephone pittsburgh pa unlimited voice philadelphia pa san francisco ca pa flat rate with eas only features call waiting caller id caller id with call waiting call screening call forwarding call forwarding selective call return 69 3 way calling anonymous call rejection repeat dialing speed dial caller id blocking",  "local spot advertising 30 second advertisement austin tx weekday 6 am 6 pm other audience demographic w18 49 number of rating points for daypart 0 29 average cpp 125",  "residential public switched toll pittsburgh pa manhattan ks ks plan area residence switched toll base san philadelphia pa ca average revenue per minute 0 18 minute online"  )), row.names = c(1L, 1245L, 3800L, 10538L, 20362L, 50000L), class = "data.frame")   

Below you will find four different solutions. One based on the for loop, two solutions based on the functions from the dplyr package, and yet a function from the collapse package.

 fSolition1 = function(){  id = vector("list", nrow(ac))  for(i in seq_along(ac$ac)){  id[[i]] = df$id[grep(ac$ac[i], df$description)]  }  ac %gt;% mutate(id = id) %gt;% unnest(id) } fSolition1()  fSolition2 = function(){  ac %gt;% group_by(ac) %gt;%   mutate(id = list(df$id[grep(ac, df$description)])) %gt;%   unnest(id) } fSolition2()  fSolition3 = function(){  ac %gt;% rowwise(ac) %gt;%   mutate(id = list(df$id[grep(ac, df$description)])) %gt;%   unnest(id) } fSolition3()  fSolition4 = function(){ ac %gt;%   collapse::ftransform(id = lapply(ac, function(x) df$id[grep(x, df$description)])) %gt;%   unnest(id) } fSolition4()  

Обратите внимание, что для приведенных данных все функции, возвращающие в результате следующую таблицу

 # A tibble: 12 x 2  ac id  lt;chrgt; lt;intgt;  1 san francisco ca 100559687  2 san francisco ca 100558763  3 san francisco ca 100558946  4 pittsburgh pa 100559687  5 pittsburgh pa 100558763  6 pittsburgh pa 100558934  7 pittsburgh pa 100558946  8 pittsburgh pa 100547618  9 philadelphia pa 100559687 10 philadelphia pa 100558946 11 philadelphia pa 100547618 12 manhattan ks 100547618  

Пришло время для ориентира

  library(microbenchmark) ggplot2::autoplot(microbenchmark(  fSolition1(), fSolition2(), fSolition3(), fSolition4(), times=100))  

введите описание изображения здесь

Возможно, никого не удивит, что collapse решение на основе является самым быстрым. Однако второе место может стать большим сюрпризом. Старое доброе решение, основанное на for функции, находится на втором месте!! Кто-нибудь еще хочет сказать, что for это медленно?

Специальное обновление для @Gwang-Jin Kim

Действия по векторам не сильно изменились. Посмотрите ниже.

 df_ac = ac$ac df_decription = df$description df_id = df$id fSolition5 = function(){  id = vector("list", length = length(df_ac))  for(i in seq_along(df_ac)){  id[[i]] = df_id[grep(df_ac[i], df_decription)]  }  ac %gt;% mutate(id = id) %gt;% unnest(id) } fSolition5()  library(microbenchmark) ggplot2::autoplot(microbenchmark(  fSolition1(), fSolition2(), fSolition3(), fSolition4(), fSolition5(), times=100))  

введите описание изображения здесь

Но сочетание for и ftransform может быть удивительным !!!

 fSolition6 = function(){  id = vector("list", nrow(ac))  for(i in seq_along(ac$ac)){  id[[i]] = df$id[grep(ac$ac[i], df$description)]  }  ac %gt;% collapse::ftransform(id = id) %gt;% unnest(id) } fSolition6()  library(microbenchmark) ggplot2::autoplot(microbenchmark(  fSolition1(), fSolition2(), fSolition3(), fSolition4(), fSolition5(), fSolition6(), times=100))  

введите описание изображения здесь

Last update for @jvalenti

Dear jvaleniti, in your question you wrote I have a column in one dataframe with city and state names and then I will be using have over 100k rows. My conclusion is that it is very likely that a given city will appear several times in your variable description .

However, in the comment you wrote I don’t want to change the number of rows in ac So what kind of results do you expect? Let’s see what can be done with it.

Solution 1 — we return all id as a list of vectors

 ac %gt;% collapse::ftransform(id = map(ac, ~df$id[grep(.x, df$description)]))  # # A tibble: 8 x 2 # ac id  # * lt;chrgt; lt;listgt;  # 1 san francisco ca lt;int [3]gt; # 2 pittsburgh pa lt;int [5]gt; # 3 philadelphia pa lt;int [3]gt; # 4 washington dc lt;int [0]gt; # 5 new york ny lt;int [0]gt; # 6 aliquippa pa lt;int [0]gt; # 7 gainesville fl lt;int [0]gt; # 8 manhattan ks lt;int [1]gt;  

Solution 2 — we only return the first id

 ac %gt;% collapse::ftransform(id = map_int(ac, ~df$id[grep(.x, df$description)][1]))  # # A tibble: 8 x 2 # ac id # * lt;chrgt; lt;intgt; # 1 san francisco ca 100559687 # 2 pittsburgh pa 100559687 # 3 philadelphia pa 100559687 # 4 washington dc NA # 5 new york ny NA # 6 aliquippa pa NA # 7 gainesville fl NA # 8 manhattan ks 100547618  

Solution 3 — we only return the last id

 ac %gt;%  collapse::ftransform(id = map_int(ac, function(x) {  idx = grep(x, df$description)  ifelse(length(idx)gt;0, df$id[idx[length(idx)]], NA)}))  # # A tibble: 8 x 2 # ac id # * lt;chrgt; lt;intgt; # 1 san francisco ca 100558946 # 2 pittsburgh pa 100547618 # 3 philadelphia pa 100547618 # 4 washington dc NA # 5 new york ny NA # 6 aliquippa pa NA # 7 gainesville fl NA # 8 manhattan ks 100547618  

Решение 4 — или, может быть, вы хотели бы выбрать любое id из всех возможных

 ac %gt;%  collapse::ftransform(id = map_int(ac, function(x) {  idx = grep(x, df$description)  ifelse(length(idx)==0, NA, ifelse(length(idx)==1, df$id[idx], df$id[sample(idx, 1)]))}))  # # A tibble: 8 x 2 # ac id # * lt;chrgt; lt;intgt; # 1 san francisco ca 100558763 # 2 pittsburgh pa 100559687 # 3 philadelphia pa 100547618 # 4 washington dc NA # 5 new york ny NA # 6 aliquippa pa NA # 7 gainesville fl NA # 8 manhattan ks 100547618  

Решение 5 — если вы случайно захотели увидеть все идентификаторы и хотели сохранить количество ac строк одновременно

 ac %gt;%  collapse::ftransform(id = map(ac, function(x) {  idx = grep(x, df$description)  if(length(idx)==0) tibble(id = NA, idn = "id1") else tibble(  id = df$id[idx],  idn = paste0("id",1:length(id)))})) %gt;%   unnest(id) %gt;%   pivot_wider(ac, names_from = idn, values_from = id) # # A tibble: 8 x 6 # ac id1 id2 id3 id4 id5 # lt;chrgt; lt;intgt; lt;intgt; lt;intgt; lt;intgt; lt;intgt; # 1 san francisco ca 100559687 100558763 100558946 NA NA # 2 pittsburgh pa 100559687 100558763 100558934 100558946 100547618 # 3 philadelphia pa 100559687 100558946 100547618 NA NA # 4 washington dc NA NA NA NA NA # 5 new york ny NA NA NA NA NA # 6 aliquippa pa NA NA NA NA NA # 7 gainesville fl NA NA NA NA NA # 8 manhattan ks 100547618 NA NA NA NA   

К сожалению, в предоставленном вами описании не указано, какое из вышеперечисленных пяти решений является приемлемым для вас. Вам придется решать самому.

Комментарии:

1. Мне нужно добавить id столбец в мой ac исходный фрейм данных. Поскольку они имеют разную длину, как это будет работать?

2. что, если uniqe(ac$ac) бы их использовали?

3. если оставить его в виде вектора или работать с кадрами данных, это определенно изменит скорость.

4. это здорово, но он не возвращает исходный кадр данных, только совпадения. можно ли вернуть исходный кадр данных ac с исходным количеством строк и id var, добавленным пробелами или NA в строках без совпадений? Я не хочу изменять количество строк ac . Извините за путаницу.

5. Большое тебе спасибо за твою помощь в этом, Марек

Ответ №2:

Возможно, это вариант?

 ac$id lt;- sapply(ac$ac, function(x) d$id[grep(x, d$description)]) # ac id # 1 san francisco ca 100559687 # 2 pittsburgh pa 100558946 # 3 philadelphia pa  # 4 washington dc  # 5 new york ny  # 6 aliquippa pa  # 7 gainesville fl  # 8 manhattan ks 100547618  

Комментарии:

1. это было бы немного быстрее при подаче заявления fixed = TRUE

Ответ №3:

Попробуйте это sapply с grep помощью .

 df$id[ unlist( sapply( ac$ac, function(x) grep(x, df$description ) ) ) ] [1] 100559687 100558946 100547618  

ОТРЕДАКТИРУЙТЕ, попробуйте stri_detect_regex с stringi . Должно быть в 2-5 раз быстрее.

 library(stringi)  df$id[ as.logical( rowSums( sapply( ac$ac, function(x)   stri_detect_regex( df$description, x ) ) ) ) ] [1] 100559687 100558946 100547618  

Микробенчмарка на расширенном наборе данных с 1,728 Млн строк:
Память не должна быть проблемой, если вы не используете систему с общим объемом оперативной памяти менее 4 ГБ.

 nrow(df) [1] 1728000  library(microbenchmark)  microbenchmark(   "grep1" = { res lt;- sapply(ac$ac, function(x) df$id[grep(x, df$description)]) },  "grep2" = { res lt;- df$id[ unlist( sapply( ac$ac, function(x) grep(x, df$description ) ) ) ] },  "stringi" = { res lt;- df$id[ as.logical( rowSums( sapply( ac$ac, function(x) stri_detect_regex( df$description, x ) ) ) ) ] }, times=10 )  Unit: seconds  expr min lq mean median uq max neval cld  grep1 96.90757 97.98706 100.13299 99.05837 101.99050 107.04312 10 b  grep2 97.51382 97.66425 100.00610 99.20753 101.17921 106.86661 10 b stringi 46.15548 46.65894 48.68073 47.29635 50.15713 53.50351 10 a  

Объем памяти во время микропозиции:
Путь: /Библиотека/Фреймворки/R. фреймворк/Версии/4.0/Ресурсы/bin/exec/R
Физический след: 638,3 М
Физический след (пик): 1,8 Г

Комментарии:

1. это, кажется, работает, но очень медленно

2. @asd-tm Спасибо за записку! Я был в процессе редактирования, а затем увидел изменения. Так что все обновлено в ответе.

3. @Andre извините, что я по ошибке разместил комментарий к вашему ответу вместо того, чтобы поместить его под вопросом!

4. @asd-tm Не беспокойтесь, это полезно для недавних ответов, чтобы узнать, работает ли их код по-прежнему. И по формулировке я понял, что вы имели в виду операцию 🙂

5. Мне нравится это решение, потому что оно простое и читаемое, но, похоже, оно не работает для масштаба. Когда я попытался, я получил ошибку от R: cannot allocate vector of size 2 GB

Ответ №4:

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

 gt; library(fuzzyjoin)  gt; regex_inner_join(df, ac, by = c(description = "ac"))  month id 1 202110 100559687 2 201703 100558946 3 201502 100547618   description 1 residential local telephone service local with more san francisco ca flat rate with eas package plan includes voicemail call forwarding call waiting caller id call restriction three way calling id block speed dialing call return call screening modem rental voip transmission telephone access line 34 95 modem rental 7 00 total 41 95 2 residential all distance telephone service unlimited voice only pittsburgh pa flat rate with eas only features call waiting caller id caller id with call waiting call screening call forwarding call forwarding selective call return 69 3 way calling anonymous call rejection repeat dialing speed dial caller id blocking 3 residential public switched toll interstate manhattan ks ks plan area residence switched toll base period average revenue per minute 0 18 minute online  ac 1 san francisco ca 2 pittsburgh pa 3 manhattan ks  

Ответ №5:

Проверка с использованием регулярного выражения и недорогих функций должна быть быстрой:

Во-первых, мы генерируем шаблон для проверки: ac_regex lt;- paste(ac$ac, collapse = "|") .

Существует несколько способов обнаружения совпадений в description и подмножестве. Вот три:

 # 1 grep() df[grep(ac_regex, df$description), ]["id"], # 2 stringi::stri_detect_*() df[stri_detect_regex(df$description, ac_regex), ]["id"], # 3 stringr::str_detect()   tidy subsetting df %gt;% filter(description %gt;% str_detect(ac_regex)) %gt;% select(id),  

Все три возвращают желаемое подмножество df :

 id 1 100559687 2 100558946 3 100547618  

(Вам нужны пакеты tidyverse и stringi для вариантов 2 и 3.)

Давайте проведем тест (используя пакет bench ):

 bench::mark(  base_grep = df[grep(ac_regex, df$description), ]["id"],  base_stringi = df[stringi::stri_detect_regex(df$description, ac_regex), ]["id"],  tidy = df %gt;% filter(description %gt;% str_detect(ac_regex)) %gt;% select(id),  check = F )  
 expression median   lt;bch:exprgt; lt;bch:tmgt;  1 base_grep 146.61µs  2 base_stringi 119.6µs  3 tidy 1.99ms   

Я бы пошел с stringi тобой !

Комментарии:

1. по какой-то причине это приводит invalid regular expression к ошибке при использовании всего фрейма данных. Кроме того, есть предупреждение: In grep(ac_regex, df$description): TRE pattern compilation error 'Out of memory' . Я не понимаю, как у меня не хватает памяти, когда у меня много оперативной памяти.

2. Это потому paste0() , что должен ac быть вектор. Я забыл включить это в свой ответ. Исправленный

Ответ №6:

Во — первых c$c , в предоставленном коде нет присвоения. Все данные присваиваются переменной с именем c . У этой переменной нет никаких c членов ( c$c ), с которыми вы пытаетесь работать.

Во-вторых, очень плохая практика-присваивать какие-либо данные переменным, называемым базовыми функциями R c lt;- c(...) .