【问题标题】:Joining two dataframes on a condition (grepl)在一个条件下连接两个数据框(grepl)
【发布时间】:2021-12-03 01:08:30
【问题描述】:

我希望根据一个条件加入两个数据帧,在这种情况下,一个字符串在另一个字符串中。假设我有两个数据框,

df1 <- data.frame(fullnames=c("Jane Doe", "Mr. John Smith", "Nate Cox, Esq.", "Bill Lee III", "Ms. Kate Smith"), 
                  ages = c(30, 51, 45, 38, 20))

       fullnames ages
1       Jane Doe   30
2 Mr. John Smith   51
3 Nate Cox, Esq.   45
4   Bill Lee III   38
5 Ms. Kate Smith   20

df2 <- data.frame(lastnames=c("Doe", "Cox", "Smith", "Jung", "Smith", "Lee"), 
                  ages=c(30, 45, 20, 28, 51, 38), 
                  homestate=c("NJ", "CT", "MA", "RI", "MA", "NY"))
  lastnames ages homestate
1       Doe   30        NJ
2       Cox   45        CT
3     Smith   20        MA
4      Jung   28        RI
5     Smith   51        MA
6       Lee   38        NY

我想对这两个数据框的年龄和df2$lastnames 包含在df1$fullnames 中的行进行左连接。我认为fuzzy_join 可能会这样做,但我认为它不喜欢我的grepl:

joined_dfs <- fuzzy_join(df1, df2, by = c("ages", "fullnames"="lastnames"), 
+                          match_fun = c("=", "grepl()"),
+                          mode="left")
Error in which(m) : argument to 'which' is not logical

期望的结果:与第一个相同但附加了“homestate”列的数据框。有什么想法吗?

【问题讨论】:

  • 我找到了解决方案!给我一点时间输入并解释你哪里出错了
  • 发布解决方案here。这是一个快速修复!

标签: r left-join grepl fuzzyjoin


【解决方案1】:

TLDR

你只需要修复match_fun:

# ...
match_fun = list(`==`, stringr::str_detect),
# ...

背景

您的想法是正确的,但是您对fuzzyjoin::fuzzy_join() 中的match_fun 参数的解释出错了。根据documentation,match_fun 应该是一个

给定两列的向量化函数,返回 TRUE 或 FALSE 以判断它们是否匹配。可以是by 中指定的每对列的函数列表(如果是命名列表,则使用 x 中的名称)。如果只给出一个函数,则它会用于所有列对。

解决方案

通过dplyr 进一步格式化,一个简单的修正就可以解决问题。为了概念清晰,我将by 列与用于匹配它们的functions 排版对齐:

library(dplyr)

# ...
# Existing code
# ...

joined_dfs <- fuzzy_join(
  df1, df2,

  by        =       c("ages", "fullnames" = "lastnames"),
  #                   |----|  |-----------------------|
  match_fun =    list(`==`  , stringr::str_detect      ),
  #                   |--|    |-----------------|
  #   Match by equality ^      ^ Match by detection of `lastnames` in `fullnames`    

  mode = "left"
) %>%
  # Format resulting dataset as you requested.
  select(fullnames, ages = ages.x, homestate)

结果

鉴于您在此处复制的示例数据

df1 <- data.frame(
  fullnames = c("Jane Doe", "Mr. John Smith", "Nate Cox, Esq.", "Bill Lee III", "Ms. Kate Smith"),
  ages = c(30, 51, 45, 38, 20)
)

df2 <- data.frame(
  lastnames = c("Doe", "Cox", "Smith", "Jung", "Smith", "Lee"),
  ages = c(30, 45, 20, 28, 51, 38),
  homestate = c("NJ", "CT", "MA", "RI", "MA", "NY")
)

此解决方案应为joined_dfs 生成以下data.frame,并按要求格式化:

        fullnames ages homestate
1       Jane Doe   30        NJ
2 Mr. John Smith   51        MA
3 Nate Cox, Esq.   45        CT
4   Bill Lee III   38        NY
5 Ms. Kate Smith   20        MA

注意

因为每一个ages都是一个唯一的key,所以下面只加入*names

fuzzy_join(
  df1, df2,
  by = c("fullnames" = "lastnames"),
  match_fun = stringr::str_detect,
  mode = "left"
)

将更好地说明匹配子字符串的行为:

       fullnames ages.x lastnames ages.y homestate
1       Jane Doe     30       Doe     30        NJ
2 Mr. John Smith     51     Smith     20        MA
3 Mr. John Smith     51     Smith     51        MA
4 Nate Cox, Esq.     45       Cox     45        CT
5   Bill Lee III     38       Lee     38        NY
6 Ms. Kate Smith     20     Smith     20        MA
7 Ms. Kate Smith     20     Smith     51        MA

你哪里出错了

类型错误

传递给match_fun 的值应该是(symbol for)function

fuzzyjoin::fuzzy_join(
  # ...
  match_fun = grepl
  # ...
)

或list (symbols for)functions:

fuzzyjoin::fuzzy_join(
  # ...
  match_fun = list(`=`, grepl)
  # ...
)

而不是提供list 的symbols

match_fun = list(=, grepl)

您错误地提供了 vector 的 character 字符串:

match_fun = c("=", "grepl()")

语法错误

用户应该命名functions

`=`
grepl

但你错误地尝试呼叫他们:

=
grepl()

按预期命名它们会将functions 它们自己传递给match_fun,而调用它们将传递它们的返回值*。在 R 中,像 = 这样的运算符使用反引号命名:`=`。

* 假设调用没有因错误而失败。在这里,他们会失败。

不当功能

要比较两个值是否相等,这里是character 向量df1$fullnames 和df2$lastnames,您应该使用关系运算符==;但是您错误地提供了 assignment 运算符 =。

此外,grepl() 并没有像 match_fun 所希望的那样矢量化。而它的第二个argument (x) 确实是一个向量

寻找匹配的字符向量,或者可以被 as.character 强制转换为字符向量的对象。支持长向量。

它的第一个 argument (pattern) 是(被视为)单个 character 字符串:

字符串,包含要在给定字符向量中匹配的正则表达式(或fixed = TRUE 的字符串)。如果可能,由as.character 强制转换为字符串。 如果提供长度为 2 或更大的字符向量,则使用第一个元素并发出警告。除了regexpr、gregexpr 和regexec 之外,允许缺少值。

因此,grepl() 不是

给定两个列的向量化函数...

而是给定一个字符串(标量)和一列(向量)字符串的function。

你的祈祷的答案不是grepl(),而是类似stringr::str_detect(),即

在string 和pattern 上矢量化。相当于grepl(pattern, x)。

其中包含stringi::stri_detect()。

注意

由于您只是想检测df1$fullnames 中的literal 字符串是否包含df2$lastnames 中的literal 字符串,因此您不想意外处理df2$lastnames 中的字符串为 regular expression patterns。现在您的df2$lastnames 列在统计上不太可能包含带有特殊正则表达式字符的名称;除了- 之外的唯一例外,它在[] 之外进行字面解释,不太可能在名称中找到。

如果您仍然担心意外的正则表达式,您可能需要考虑 alternative search methods 和 stringi::stri_detect_fixed() 或 stringi::stri_detect_coll()。它们分别通过byte 或"canonical equivalence" 执行文字匹配;后者根据自然语言处理调整区域设置和特殊字符。

【讨论】:

  • 这太好了,谢谢!我找不到很多关于fuzzy_join 的例子,而且在match_fun 上也完全不清楚。
  • @JessCT 很高兴听到!如果这解决了您的问题,请点赞并将其标记为“已接受”。 :)
  • 感谢您提供这么长、详细、翔实的回复!
  • 它告诉我我没有足够的声誉来投票,所以我不确定如何将其标记为已接受?如果还有一个人支持我的问题,我应该有足够的“声誉”。
  • 谢谢杰西,我的荣幸!
【解决方案2】:

考虑到您的两个数据框,这似乎可行:

已根据@Greg 的评论编辑:

代码适用于发布的数据;如果在您的实际数据中,特别是姓氏有更多变体,例如不仅有III,还有IV,请随意调整代码:

library(dplyr)
df1 %>%
  mutate(
    # create new column that gets rid of strings after last name:
    lastnames = sub("\\sI{1,3}$|,.+$", "", fullnames),
    # grab last names:
    lastnames = sub(".*?(\\w+)$", "\\1", lastnames)) %>%
  # join the two dataframes:
  left_join(., df2, by = c("lastnames", "ages"))
       fullnames ages lastnames homestate
1       Jane Doe   30       Doe        NJ
2 Mr. John Smith   51     Smith        MA
3 Nate Cox, Esq.   45       Cox        CT
4   Bill Lee III   38       Lee        NY
5 Ms. Kate Smith   20     Smith        MA

如果你想要 lastnamesremoved 只需在 %&gt;% 后面附加这个:

select(-lastnames) 

编辑#2:

如果您不相信上述解决方案,因为姓氏的实际标注方式存在巨大差异,那么当然fuzzy_join 也是一种选择。 但是,目前的fuzzy_join 解决方案还不够;它需要通过一个关键的数据转换进行修改。这是因为str_detect 检测一个字符串是否包含 在另一个字符串中。也就是说,如果将Smith 与Smithsonian 或Hammer-Smith 进行比较,它将返回TRUE——每次字符串Smith 确实包含在较长的名称中。如果在大型数据集中可能会出现这种情况,Smith 和 Smithsonian 恰好具有相同的 ages,那么不匹配将是完美的:fuzzy_join 将错误地加入两者。当你有相同年龄的Smith 和Smith-Klein 时,也会出现同样的问题:fuzzy_join 也会加入他们。

第一组有问题的情况可以通过在df2 中包含单词边界锚\\b 来解决。这些断言,例如,Smith 必须以任意一侧的单词边界为界,Smithsonian 的情况并非如此,Smithsonian 的左侧确实有一个不可见的边界,但右侧的锚是在最后一封信n 之后。第二组有问题的情况可以通过在\\b 之后包含一个否定前瞻来解决,即\\b(?!-),它断言在单词边界之后不能有连字符。

使用mutate 和paste0 可以轻松实现该解决方案,如下所示:

fuzzy_join(
  df1, df2 %>%
    mutate(lastnames = paste0("\\b", lastnames, "\\b(?!-)")),
  by  = c("ages", "fullnames" = "lastnames"),
  match_fun = list(`==`, str_detect),
  mode = "left"
) %>%
  select(fullnames, ages = ages.x, homestate) 

【讨论】:

  • OP 使用这两个标准进行匹配:(1) ages 相同; (2) lastname 是 fullname 的子字符串。实际上,ages 和 lastnames 充当“键”来唯一标识其他不明确的行,例如 "Smith"。我怀疑 OP 故意选择 "Mr. John Smith" 和 "Ms. Kate Smith" 来说明这一点。但是,这个答案 *_joins 仅在名称上,因此,它包括 "Smith"s 的额外、不准确的行。 “所需结果:与第一个 [df1] 相同的数据框,但附加了一个“homestate”列。”
  • 不要批评,但 OP 指出 df1 和 df2 只是更丰富数据集的说明:“说我有两个数据框”我>。然而,这个答案对姓氏做出了不可扩展的假设,尤其是没有一个姓氏是double-barreled。请注意在这种情况下您的最后一个 mutate() 将如何失败:sub(".*?(\\w+)$", "\\1", c("Sacha Baron Cohen", "Catherine Zeta-Jones")) 给了我们c("Cohen", "Jones")。它实际上只考虑了几个可能的后缀:"I"、"II"、"III",或者任何以逗号开头的东西;所以"IV" 会被忽略
  • 至于我的第一条评论,我认为它仍然存在:结果输出不是 OP 要求的,因为缺少 *_join 标准。但是,这可以通过left_joining by = c("lastnames", "ages") 轻松解决。 :)
  • 我已经编辑了我的解决方案。感谢您的提示!
  • 很高兴为您提供帮助,非常感谢您的支持! :)
猜你喜欢
  • 2019-12-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-18
  • 1970-01-01
  • 2020-09-19
  • 2016-11-18
  • 1970-01-01
相关资源
最近更新 更多