【问题标题】:Comparing Columns of Names Between Two Dataframes Before Joining with Dplyr在加入 Dplyr 之前比较两个数据帧之间的名称列
【发布时间】:2016-06-09 21:15:17
【问题描述】:

我想知道在 dplyr 中进行联接之前是否有一种简单的方法来比较列。下面是两个简单的数据框。我想根据名字和姓氏进行完全连接,但是有一些拼写错误或不同的格式,例如“Elizabeth Ray”与“Elizabeth”。

我想在加入之前比较这些列。我希望有一种方法可以生成带有索引的所有差异的列表或向量,以便我可以在加入之前更正它们。

如果有更简单的方法,我也愿意,但我希望有最简单的方法。我想要一个基于 dplyr、tidyr 和 stringr 的解决方案。

FirstNames<-c("Chris","Doug","Shintaro","Bubbles","Elsa")
LastNames<-c("MacDougall","Shapiro","Yamazaki","Murphy","Elizabeth Ray")
Pets<-c("Cat","Dog","Cat","Dog","Cat")
Names1<-data.frame(FirstNames,LastNames,Pets)

FirstNames2<-c("Chris","Doug","Shintaro","Bubbles","Elsa")
LastNames2<-c("MacDougal","Shapiro","Yamazaku","Murphy","Elizabeth")
Dwelling<-c("House","House","Apartment","Condo","House")
Names2<-data.frame(FirstNames2,LastNames2,Dwelling)

【问题讨论】:

标签: r dplyr tidyr stringr


【解决方案1】:

为了比较您的记录之间的相似性,我认为您可能正在寻找一种方法来将模糊逻辑匹配的度量应用于您的名称比较任务。又名:应用String Distance Function 在执行您的Record Linkage 任务时。 (如果您已经知道这一切,请原谅我 - 但这些关键字在一开始对我有很大帮助。)

有一个很棒的包stringdist 非常适合这些应用程序,但recordlinkage 可能会帮助您以最快的速度对齐数据框。

如果您希望查看最相似的名字和姓氏的值,直到最不同的值,您可以使用如下代码:

library(RecordLinkage)
library(dplyr)

id <- c(1:5) # added in to allow joining of data tables & comparison results
firstName <- c("Chris","Doug","Shintaro","Bubbles","Elsa")
lastName <- c("MacDougall","Shapiro","Yamazaki","Murphy","Elizabeth Ray")
pet <- c("Cat","Dog","Cat","Dog","Cat")
Names1 <- data.frame(id, firstName, lastName, pet)

id <- c(1:5) # added in to allow joining of data tables & comparison results
firstName2 <- c("Chris","Doug","Shintaro","Bubbles","Elsa")
lastName2 <- c("MacDougal","Shapiro","Yamazaku","Murphy","Elizabeth")
dwelling <- c("House","House","Apartment","Condo","House")
Names2 <- data.frame(id, firstName2, lastName2, dwelling)

# RecordLinkage function that calculates string distance b/w records in two data frames
Results <- compare.linkage(Names1, Names2, blockfld = 1, strcmp = T, exclude = 4)
Results
#  $data1
#    firstName      lastName  pet
# 1      Chris    MacDougall  Cat
# 2       Doug       Shapiro  Dog
# 3   Shintaro      Yamazaki  Cat
# 4    Bubbles        Murphy  Dog
# 5       Elsa Elizabeth Ray  Cat

# $data2
#    firstName2  lastName2  dwelling
# 1       Chris  MacDougal     House
# 2        Doug    Shapiro     House
# 3    Shintaro   Yamazaku Apartment
# 4     Bubbles     Murphy     Condo
# 5        Elsa  Elizabeth     House

# $pairs
# id1 id2 id firstName  lastName is_match
# 1   1   1  1         1 0.9800000       NA
# 2   2   2  1         1 1.0000000       NA
# 3   3   3  1         1 0.9500000       NA
# 4   4   4  1         1 1.0000000       NA
# 5   5   5  1         1 0.9384615       NA

# $frequencies
# id firstName  lastName 
# 0.200     0.200     0.125 
# $type
# [1] "linkage"

# attr(,"class")
# [1] "RecLinkData"

# Trim $pairs dataframe (seen above) to contain just id's & similarity measures
PairsSelect <- 
    Results$pairs %>% 
    select(id = id1, firstNameSim = firstName, lastNameSim = lastName)

# Join original data & string comparison results together
# reorganize data to facilitate review
JoinedResults <-
    left_join(Names1, Names2) %>% 
    left_join(PairsSelect) %>% 
    select(id, firstNameSim, firstName, firstName2, lastNameSim, lastName, lastName2) %>% 
    arrange(desc(lastNameSim), desc(firstNameSim), id)
JoinedResults
# id firstNameSim firstName firstName2 lastNameSim      lastName lastName2
# 1  2            1      Doug       Doug   1.0000000       Shapiro   Shapiro
# 2  4            1   Bubbles    Bubbles   1.0000000        Murphy    Murphy
# 3  1            1     Chris      Chris   0.9800000    MacDougall MacDougal
# 4  3            1  Shintaro   Shintaro   0.9500000      Yamazaki  Yamazaku
# 5  5            1      Elsa       Elsa   0.9384615 Elizabeth Ray Elizabeth

# If you want to collect just the perfect matches
PerfectMatches <- 
    JoinedResults %>% 
    filter(firstNameSim == 1 & lastNameSim == 1) %>% 
    select(id, firstName, lastName)
PerfectMatches
#   id firstName lastName
# 1  2      Doug  Shapiro
# 2  4   Bubbles   Murphy

# To collect the matches that are going to need alignment
ImperfectMatches <- 
    JoinedResults %>% 
    filter(firstNameSim < 1 | lastNameSim < 1) %>% 
    mutate(flgFrstNm = 0, flgLstNm = 0)
ImperfectMatches
#   id firstNameSim firstName firstName2 lastNameSim      lastName lastName2 flgFrstNm flgLstNm
# 1  1            1     Chris      Chris   0.9800000    MacDougall MacDougal         0        0
# 2  3            1  Shintaro   Shintaro   0.9500000      Yamazaki  Yamazaku         0        0
# 3  5            1      Elsa       Elsa   0.9384615 Elizabeth Ray Elizabeth         0        0
# 

# If you want to enter your column preference in a flag column to facilitate faster rectification...
write.csv(ImperfectMatches, "ImperfectMatches.csv", na = "", row.names = F)
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
# Flag data externally - save file to new name with '_reviewed' appended to filename
## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ##
#reload results
FlaggedMatches <- read.csv("ImperfectMatches_reviewed.csv", stringsAsFactors = F)
FlaggedMatches
## Where a 1 is the 1st data set preferred and 0 (or 2 if that is easier for the 'data processor') means the 2nd data set is preferred.
#   id firstNameSim firstName firstName2 lastNameSim      lastName lastName2 flgFrstNm flgLstNm
# 1  1            1     Chris      Chris   0.9800000    MacDougall MacDougal         1        0
# 2  3            1  Shintaro   Shintaro   0.9500000      Yamazaki  Yamazaku         1        1
# 3  5            1      Elsa       Elsa   0.9384615 Elizabeth Ray Elizabeth         1        0

## Executing Assembly of preferred/rectified firstName and lastName columns
ResolvedMatches <- 
    FlaggedMatches %>% 
    mutate(rectifiedFirstName = ifelse(flgFrstNm == 1,firstName, firstName2),
           rectifiedLastName = ifelse(flgLstNm == 1, lastName, lastName2)) %>% 
    select(id, starts_with("rectified"))

ResolvedMatches
# id rectifiedFirstName rectifiedLastName
# 1  1              Chris         MacDougal
# 2  3           Shintaro          Yamazaki
# 3  5               Elsa         Elizabeth

dplyr 非常直观,但compare.linkage() 函数可以使用一些解释。

前两个参数很明显:您要比较的两个数据帧(dataframe1 和 dataframe2)。 [如果您只想将 onedataframe 中的记录与它们自己进行比较(以对记录集进行重复数据删除),那么您可以改用 compare.dedup(),并且只引用一个 dataframe。

在这种情况下,将blockfld 设置为 1 或 2 将指定名字或姓氏的匹配必须分别为 100%。相反,您可能希望在数据集中包含主键/外键并在 blckfld 参数中引用该列。或者,如果您的记录实际上并非如此等价地构造,您可以完全忽略此参数(默认为 FALSE),然后将比较所有可能的组合 [数据帧的叉积]。

strcmpTRUE 为您提供一个字符串距离函数,该函数应用于您正在比较的数据列;如果您将其保留为 false,那么它只会测试精确的 1:1 字符串对应关系。

exclude 也是一种避免构建中间数据帧并仅选择您希望相互比较的列的好方法:排除3 只是允许我们从结果中删除 Pets 和 Dwelling 比较。

上面代码中的 4 列,keyed,dataframes(不是原始问题的 3 column dataframes)产生的结果如下:

#  $data1
#    firstName      lastName  pet
# 1      Chris    MacDougall  Cat
# 2       Doug       Shapiro  Dog
# 3   Shintaro      Yamazaki  Cat
# 4    Bubbles        Murphy  Dog
# 5       Elsa Elizabeth Ray  Cat

# $data2
#    firstName2  lastName2  dwelling
# 1       Chris  MacDougal     House
# 2        Doug    Shapiro     House
# 3    Shintaro   Yamazaku Apartment
# 4     Bubbles     Murphy     Condo
# 5        Elsa  Elizabeth     House

# $pairs
# id1 id2 id firstName  lastName is_match
# 1   1   1  1         1 0.9800000       NA
# 2   2   2  1         1 1.0000000       NA
# 3   3   3  1         1 0.9500000       NA
# 4   4   4  1         1 1.0000000       NA
# 5   5   5  1         1 0.9384615       NA

# $frequencies
# id firstName  lastName 
# 0.200     0.200     0.125 
# $type
# [1] "linkage"

# attr(,"class")
# [1] "RecLinkData"

上面的每个部分(例如 $pairs)都是它自己的数据框。 添加一个键,您可以将它们全部连接在一起,然后引用并使用成对的值 df 作为切换级别门,然后甚至将 data1 值复制到 data2 框架中,例如,当配对评级中的值 > 0.95 时. (注意:is_match 看起来很重要,但它是用于训练匹配工具的,与我们这里的任务无关。)

无论如何,我希望您发现这些库的突然增强功能将使您能够像我第一次遇到它们时一样兴奋地投入工作。

顺便说一句:我还发现 Comparison of String Distance Algorithms 是对当前可用的字符串距离指标的一个很好的调查。

【讨论】:

  • 感谢您的出色回答,我一定会检查包裹。但我想一定有更简单的东西。也许我的解释不够清楚。这不一定是拼写错误,而是匹配。是否有一个匹配函数来查找数据框的一列中与另一列不匹配的单词?这样拼写错误的名字会被标记为不匹配?
  • 是的!这正是您在上面的代码中得到的结果。您会得到一个介于 1 和 0 之间的值来表示字符串中有多少相似度。因此,如果您仅过滤 >=0.95 值,您将获得字符串之间只有几个字母差异的行。您也可以在该列上arrange(desc()),您会得到完全相同的值,但底部的值完全不同;如果加入 $data1、$data2 和 $pairs 数据帧结果,那么您可以将这些简单的东西一起推开(甚至 `mutate(ifelse(pairsLastNames > 0.95, data2LastNames, data1LastName))。
  • 如果你愿意,很高兴将整个过程编码出来。以上是一个很好的解决方案,还是您有其他想要更改值的方法。 (即您是否更喜欢,当有一个紧密匹配覆盖第一个数据帧的值超过第二个或第二个超过第一个时,或者您可能只想在 Oll Korrect 值旁边放置一个标志,也许是Mostly Complete 值旁边有不同的标志,以便您以后可以将它们整理出来进行手动重新编码?)
  • 感谢您抽出宝贵时间提供帮助!如果你能把整个过程编码出来,那就太好了。根据涉及的编码量,很高兴看到如何将第二个数据帧的值覆盖在第一个数据帧上,以及如何查看标志用于手动重新编码。我也喜欢在上面的评论中使用“mutate”。我期待看到它是如何工作的,我很高兴学习如何使用这个包。我很难准确地表达我想要什么,因为我不知道这是可能的。再次感谢您的帮助!
  • @alistaire 建议使用agrep / adist 也是一个非常有效的解决方案。参见例如:r-bloggers.com/…(这些原生 R 方法使用广义的 Levenshtein(编辑)距离,在这种情况下,这已经绰绰有余,而且实际上可能与您的处理方法最相关。)
【解决方案2】:

我在回答问题,因为我无法访问评论

df = Names1[!(Names1$LastNames %in% Names2$LastNames2), ]

试试 about 代码。

【讨论】:

  • 感谢您的出色回答,我一定会检查包裹。但我想一定有更简单的东西。也许我的解释不够清楚。这不一定是拼写错误,而是匹配。是否有一个匹配函数可以在数据框的一列中查找在另一列中不匹配的单词?这样拼写错误的名字会被标记为不匹配?
【解决方案3】:

使用@alistaire 建议的标准adist() 函数提供了一种非常有效的方法(并且很可能是教师希望看到的使用方法。)adist 的字符串度量仅限于广义 Levenshtein(编辑)距离,但这看起来正是您正在寻找的。​​p>

代码如下: (因为这看起来像是专门针对数据处理的 R 编码类的介绍,所以我在一些最佳实践中对可重现/提出的问题进行了润色。)

library(dplyr)

id <- c(1:5)
firstName <- c("Chris","Doug","Shintaro","Bubbles","Elsa")
lastName <- c("MacDougall","Shapiro","Yamazaki","Murphy","Elizabeth Ray")
pet <- c("Cat","Dog","Cat","Dog","Cat")
Names1 <- data.frame(id, firstName, lastName, pet)

id <- c(1:5)
firstName2 <- c("Chris","Doug","Shintaro","Bubbles","Elsa")
lastName2 <- c("MacDougal","Shapiro","Yamazaku","Murphy","Elizabeth")
dwelling <- c("House","House","Apartment","Condo","House")
Names2 <- data.frame(id, firstName2, lastName2, dwelling)
# NB: technically you could merge these data frames later with `bind_cols()` but best 
# datahandling practices dictate joining/comparing data based on keys (instead of 
# binding columns together based upon the order in which tables are initially arranged.)
#[also preference is for column headers to be singular and lower case, and tables/dataframes to be uppercase and plural - from (or extension from principles in): https://google.github.io/styleguide/Rguide.xml]

## adist() calculates string distance b/w records in two data frames
# Matrix between all columns is great way to ascertain similarity of data
# on overall column to column basis.
# 0 is closest resemblance, higher numbers are lowest resemblance
ResultsInterColumnComparison <-
    adist(Names1, Names2, partial = T)
ResultsInterColumnComparison

# firstName to firstName2 & lastName to LastName2 are similar columns.
#           id firstName2 lastName2 dwelling
# id         0          2         2        2
# firstName 15          0         3        4
# lastName  15          3         0        5
# pet       15          5         4        3

# adist column to column DifferenceCount (using dplyr)
dltFrstN <- diag(adist(Names1$firstName, Names2$firstName2, partial = T))
dltLstN <- diag(adist(Names1$lastName, Names2$lastName2, partial = T))

# Join all info together
DFcompilation <- 
    data.frame(id, dltFrstN, firstName, firstName2, dltLstN, lastName, lastName2) %>% 
    arrange(desc(dltLstN), desc(dltLstN))
DFcompilation
#   id dltFrstN firstName firstName2 dltLstN      lastName lastName2
# 1  5        0      Elsa       Elsa       4 Elizabeth Ray Elizabeth
# 2  1        0     Chris      Chris       1    MacDougall MacDougal
# 3  3        0  Shintaro   Shintaro       1      Yamazaki  Yamazaku
# 4  2        0      Doug       Doug       0       Shapiro   Shapiro
# 5  4        0   Bubbles    Bubbles       0        Murphy    Murphy

这种方法更简单,所需的编码也更简洁。我希望这对您的目的也更有帮助。

【讨论】:

  • 谢谢! adist 选项似乎更简单。因为实际上我将使用从 csv 文件上传的数据集,我想我将不得不使用 dplyr "select" 来提取 "firstName"、"firstName2"、"lastName" 和 "lastName2" 列,以便我可以在上面示例的最后一步中创建新的数据框吗?另外,为了让事情变得更复杂,一个数据帧如何被另一个数据帧覆盖?例如,如果我想用 Names1 数据框中的名称替换 Names2 数据框中 dltLstN 大于 0 的所有名称?
  • 我在上面示例的最后一步也遇到了一些麻烦 - 将所有内容连接在一起。我使用 Dplyr "select" 为 "firstName","firstName2","lastName" 和 "lastName2" 以及 ID 创建了单独的对象,但是当我尝试将它们放在一起时,出现一个错误,提示存在“行数的差异”。我对 R 还比较陌生,所以我可能缺少一些明显的东西。也太晚了,所以我的大脑可能无法正常工作。您可以提出任何建议,我们将不胜感激!
  • 我希望我没有把事情弄得太复杂。基本上,我的最终目标只是找到所有不同拼写的集合,然后用另一列覆盖一列,这样我就可以加入数据框。这是主要目标。
  • 覆盖或制作“首选列”都可以得到你想要的。唯一的区别是前一个选项有一个mutate(),它以组成列之一的名称开头和结尾,而第二个方法的 mutate 使列具有全新的名称。你仍然需要弄清楚选择哪一个。选择列的方式可以是使用 RecordLinkage 示例中的标志,或者如果您更喜欢自动选择使用最短的标志 - 您的 ifelse() 可以通过 nrow() 比较来平衡。
  • 主要目标应该通过在RecordLinkage解决方案中执行完整的解决方案来完全解决:-D
猜你喜欢
  • 2018-01-26
  • 1970-01-01
  • 1970-01-01
  • 2023-03-27
  • 2019-01-02
  • 1970-01-01
  • 2021-06-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多