【译】如何合并无共同标识符的数据集

做者: Chris Moffitthtml

翻译:老齐python

与本文相关的图书推荐:《数据准备和特征工程》算法


引言

合并数据集,是数据科学中常见的操做。对于有共同标识符的两个数据集,可使用Pandas中提供的常规方法合并,可是,若是两个数据集没有共同的惟一标识符,怎么合并?这就是本文所要阐述的问题。对此,有两个术语会常常用到:记录链接和模糊匹配,例如,尝试把基于人名把不一样数据文件链接在一块儿,或合并只有组织名称和地址的数据等,都是利用“记录连接”和“模糊匹配”完成的。sql

合并无共同特征的数据,是比较常见且具备挑战性的业务,很难系统地解决,特别是当数据集很大时。若是用人工的方式,使用Excel和查询语句等简单方法可以实现,但这无疑要有很大的工做量。如何解决?Python此时必须登场。Python中有两个库,它们能垂手可得地解决这种问题,而且能够用相对简单的API支持复杂的匹配算法。编程

第一个库叫作fuzzymatcher,它用一个简单的接口就能根据两个DataFrame中记录的几率把它们链接起来,第二个库叫作RecordLinkage 工具包,它提供了一组强大的工具,可以实现自动链接记录和消除重复的数据。浏览器

在本文中,咱们将学习如何使用这两个工具(或者两个库)来匹配两个不一样的数据集,也就是基于名称和地址信息的数据集。此外,咱们还将简要学习如何把这些匹配技术用于删除重复的数据。bash

问题

只要试图将不一样的数据集合并在一块儿,任何人均可能遇到相似的挑战。在下面的简单示例中,系统中有一个客户记录,咱们须要肯定数据匹配,而又不使用公共标识符。(下图中箭头标识的两个记录,就是要匹配的对象,它们没有公共标识符。)微信

根据一个小样本的数据集和咱们的直觉,记录号为18763和记录号为A1278两条记录看起来是同样的。咱们知道Brothers 和 Bro以及Lane和LN是等价的,因此这个过程对人来讲相对容易。然而,尝试在编程中利用逻辑来处理这个问题就是一个挑战。markdown

以个人经验,大多数人会想到使用Excel,查看地址的各个组成部分,并根据州、街道号或邮政编码找到最佳匹配。在某些状况下,这是可行的。可是,咱们可能但愿使用更精细的方法来比较字符串,为此,几年前我曾写过一个叫作fuzzywuzzy的包。app

挑战在于,这些算法(例如Levenshtein、Damerau-Levenshtein、Jaro-Winkler、q-gram、cosine)是计算密集型的,在大型数据集上进行大量匹配是没法调节比例的。

若是你有兴趣了解这些概念上的更多数学细节,能够查看维基百科中的有关内容,本文也包含了一些详解。最后,本文将更详细地讨论字符串匹配的方法。

幸运的是,有一些Python工具能够帮助咱们实现这些方法,并解决其中的一些具备挑战性的问题。

数据

在本文中,咱们将使用美国医院的数据。之因此选这个数据集,是由于医院的数据具备一些独特性,使其难以匹配:

  • 许多医院在不一样的城市都有类似的名字(圣卢克斯、圣玛丽、社区医院,这很相似我国不少城市都有“协和医院”同样)
  • 在某个城市内,医院能够占用几个街区,所以地址可能不明确
  • 医院附近每每有许多诊所和其余相关设施
  • 医院也会被收购,名字的变动也很常见,从而使得数据处理过程更加困难
  • 最后,美国有成千上万的医疗机构,因此这个问题很难按比例处理

在这些例子中,我有两个数据集。第一个是内部数据集,包含基本的医院账号、名称和全部权信息。

第二个数据集包含医院信息(含有Provider的特征),以及特定心衰手术的出院人数和医疗保险费用。

以上数据集来自Medicare.gov 和 CMS.gov,并通过简单的数据清洗。

本文项目已经发布到在线实验平台,请关注微信公众号《老齐教室》后,回复:#姓名+手机号+案例#。注意,#符号不要丢掉,不然没法查找到回复信息。

咱们的业务场景:如今有医院报销数据和内部账户数据,要讲二者进行匹配,以便从更多层面来分析每一个医院的患者。在本例中,咱们有5339个医院账户和2697家医院的报销信息。可是,这两类数据集没有通用的ID,因此咱们将看看是否可使用前面提到的工具,根据医院的名称和地址信息将两个数据集合并。

方法1:fuzzymather包

在第一种方法中,咱们将尝试使用fuzzymatcher,这个包利用sqlite的全文搜索功能来尝试匹配两个不一样DataFrame中的记录。

安装fuzzymatcher很简单,若是使用conda安装,依赖项会自动检测安装,也可使用pip安装fuzzymatcher。考虑到这些算法的计算负担,你会但愿尽量多地使用编译后的c组件,能够用conda实现。

在全部设置完成后,咱们导入数据并将其放入DataFrames:

import pandas as pd
from pathlib import Path
import fuzzymatcher
hospital_accounts = pd.read_csv('hospital_account_info.csv')
hospital_reimbursement = pd.read_csv('hospital_reimbursement.csv')
复制代码

如下是医院帐户信息:

Here is the reimbursement information:

这是报销信息:

因为这些列有不一样的名称,咱们须要定义哪些列与左右两边的DataFrame相匹配,医院账户信息是左边的DataFrame,报销信息是右边的DataFrame。

left_on = ["Facility Name", "Address", "City", "State"]

right_on = [
    "Provider Name", "Provider Street Address", "Provider City",
    "Provider State"
]
复制代码

如今用fuzzymatcher中的fuzzy_left_join函数找出匹配项:

matched_results = fuzzymatcher.fuzzy_left_join(hospital_accounts,
                                            hospital_reimbursement,
                                            left_on,
                                            right_on,
                                            left_id_col='Account_Num',
                                            right_id_col='Provider_Num')
复制代码

在幕后,fuzzymatcher为每一个组合肯定最佳匹配。对于这个数据集,咱们分析了超过1400万个组合。在个人笔记本电脑上,这个过程花费了2分11秒。

变量matched_results所引用的DataFrame对象包含链接在一块儿的全部数据以及best_match_score——这个特征的数据用于评估该匹配链接的优劣。

下面是这些列的一个子集,前5个最佳匹配项通过从新排列加强了可读性:

cols = [
    "best_match_score", "Facility Name", "Provider Name", "Address", "Provider Street Address",
    "Provider City", "City", "Provider State", "State"
]

matched_results[cols].sort_values(by=['best_match_score'], ascending=False).head(5)
复制代码

第一个项目的匹配得分是3.09分,看起来确定是良好的匹配。你能够看到,对位于Red Wing的Mayo诊所,特征Facility NameProvider Name的值基本同样,观察结果也证明这条匹配是很合适的。

咱们也能够查看哪些地方的匹配效果很差:

matched_results[cols].sort_values(by=['best_match_score'], ascending=True).head(5)
复制代码

这里显示了一些糟糕的分数以及明显的不匹配状况:

这个例子凸显了一部分问题,即一个数据集包括来自Puerto Rico的数据,而另外一个数据集中没有,这种差别明确显示,在尝试匹配以前,你须要确保对数据的真正了解,以及尽量对数据进行清理和筛选。

咱们已经看到了一些极端的状况。如今看一看,分数小于0.8的一些匹配,它们可能会更具挑战性:

matched_results[cols].query("best_match_score <= .80").sort_values(
    by=['best_match_score'], ascending=False).head(5)
复制代码

上述示例展现了一些匹配如何变得更加模糊,例如,ADVENTIST HEALTH UKIAH VALLEY)是否与UKIAH VALLEY MEDICAL CENTER 相同?根据你的数据集和需求,你须要找到自动和手动匹配检查的正确平衡点。

总的来讲,fuzzymatcher是一个对中型数据集有用的工具。若是样本量超过10000行时,将须要较长时间进行计算,对此,要有良好的规划。然而,fuzzymatcher的确很好用,特别是与Pandas结合,使它成为一个很好的工具。

方法2:RecordLinkage工具包

RecordLinkage工具包提供了另外一组强有力的工具,用于链接数据集中的记录和识别数据中的重复记录。

其主要功能以下:

  • 可以根据列的数据类型,为每一个列定义匹配的类型
  • 使用“块”限制潜在的匹配项的池
  • 使用评分算法提供匹配项的排名
  • 衡量字符串类似度的多种算法
  • 有监督和无监督的学习方法
  • 多种数据清理方法

权衡之下,若是仅仅是为了进一步验证而管理这些数据结果,这些操做就有点太复杂了。然而,这些步骤都会用标准的Panda指令实现,因此不要惧怕。

依然可使用pip来安装库。咱们将使用前面的数据集,但会在读取数据的时候设置某列为索引,这使得后续的数据链接更容易解释。

import pandas as pd
import recordlinkage

hospital_accounts = pd.read_csv('hospital_account_info.csv', index_col='Account_Num')
hospital_reimbursement = pd.read_csv('hospital_reimbursement.csv', index_col='Provider_Num')
复制代码

由于RecordLinkage有更多的配置选项,因此咱们须要几个步骤来定义链接规则。第一步是建立indexer对象:

indexer = recordlinkage.Index()
indexer.full()
复制代码
# 输出

WARNING:recordlinkage:indexing - performance warning - A full index can result in large number of record pairs.

复制代码

这个警告指出了记录链接库和模糊匹配器之间的区别。经过记录链接,咱们能够灵活地影响评估的记录对的数量。调用索引对象的full方法,能够计算出全部可能的记录对(咱们知道这些记录对的数量超过了14M)。我过一下子再谈其余的选择,下面继续探讨完整的索引,看看它是如何运行的。

下一步是创建全部须要检查的潜在的候选记录:

candidates = indexer.index(hospital_accounts, hospital_reimbursement)
print(len(candidates))
复制代码
# 输出

14399283
复制代码

这个快速检查刚好确认了比较的记录总数。

既然咱们已经定义了左、右数据集和全部候选数据集,就可使用Compare()进行比较。

compare = recordlinkage.Compare()
compare.exact('City', 'Provider City', label='City')
compare.string('Facility Name',
            'Provider Name',
            threshold=0.85,
            label='Hosp_Name')
compare.string('Address',
            'Provider Street Address',
            method='jarowinkler',
            threshold=0.85,
            label='Hosp_Address')
features = compare.compute(candidates, hospital_accounts,
                        hospital_reimbursement)
复制代码

以上选定几个特征,用它们肯定一个城市的精确匹配,此外在执行string方法中还设置了阈值。除了这些选参数以外,你还能够定义其余一些参数,好比数字、日期和地理坐标。了解更多示例,请参阅文档。

最后一步是使用compute方法对全部特征进行比较。在本例中,咱们使用完整索引,用时3分钟41秒。

下面是一个优化方案,这里有一个重要概念,就是块,使用块能够减小比较的记录数量。例如,若是只想比较处于同一个州的医院,咱们能够依据State列建立块:

indexer = recordlinkage.Index()
indexer.block(left_on='State', right_on='Provider State')
candidates = indexer.index(hospital_accounts, hospital_reimbursement)
print(len(candidates))
复制代码
# 输出

475830
复制代码

依据State分块,候选项将被筛选为只包含州值相同的那些,筛选后只剩下475,830条记录。若是咱们运行相同的比较代码,只须要7秒。一个很好的加速方法!

在这个数据集中,State的数据是干净的,可是若是有点混乱的话,还可使用另外一种分块算法,好比SortedNeighborhood,减小一些小的拼写错误带来的影响。

例如,若是州名包含“Tenessee”和“Tennessee”怎么办?前面的分块就无效了,但可使用sortedneighbourhood方法处理此问题。

indexer = recordlinkage.Index()
indexer.sortedneighbourhood(left_on='State', right_on='Provider State')
candidates = indexer.index(hospital_accounts, hospital_reimbursement)
print(len(candidates))
复制代码
# 输出

998860
复制代码

上述示例,sortedneighbourhood处理了998,860个记录,花费了15.9秒,这一操做彷佛很合理的。

无论你使用哪一个方法,结果都入下所示,是一个DataFrame。

这个DataFrame显示全部比较的结果,在账户和报销DataFrames中,每行有一个比较结果。这些项目对应着咱们所定义的比较,1表明匹配,0表明不匹配。

因为大量记录没有匹配项,难以看出咱们可能有多少匹配项,为此能够把单个的得分加起来查看匹配的效果。

features.sum(axis=1).value_counts().sort_index(ascending=False)
复制代码
# 输出

3.0      2285
2.0       451
1.0      7937
0.0    988187
dtype: int6
复制代码

如今咱们知道有988187行没有任何匹配值,7937行至少有一个匹配项,451行有2个匹配项,2285行有3个匹配项。

为了使剩下的分析更简单,让咱们用2或3个匹配项获取全部记录,并添加总分:

potential_matches = features[features.sum(axis=1) > 1].reset_index()
potential_matches['Score'] = potential_matches.loc[:, 'City':'Hosp_Address'].sum(axis=1)
复制代码

下面是对所得结果进行解释:索引为1的行,Account_Num值为26270、Provider_Num值为868740,该行显示,在城市、医院名称和医院地址方面相匹配。

再详细查看这两个记录的内容:

hospital_accounts.loc[26270,:]
复制代码
Facility Name         SCOTTSDALE OSBORN MEDICAL CENTER
Address                          7400 EAST OSBORN ROAD
City                                        SCOTTSDALE
State                                               AZ
ZIP Code                                         85251
County Name                                   MARICOPA
Phone Number                            (480) 882-4004
Hospital Type                     Acute Care Hospitals
Hospital Ownership                         Proprietary
Name: 26270, dtype: object
复制代码
hospital_reimbursement.loc[868740,:]
复制代码
Provider Name                SCOTTSDALE OSBORN MEDICAL CENTER
Provider Street Address                 7400 EAST OSBORN ROAD
Provider City                                      SCOTTSDALE
Provider State                                             AZ
Provider Zip Code                                       85251
Total Discharges                                           62
Average Covered Charges                               39572.2
Average Total Payments                                6551.47
Average Medicare Payments                             5451.89
Name: 868740, dtype: object
复制代码

是的。它们看起来很匹配。

如今咱们知道了匹配项,还须要对数据进行调整,以便更容易地对全部数据进行检查。我将为每个数据集建立一个用于链接的名称和地址查询。

hospital_accounts['Acct_Name_Lookup'] = hospital_accounts[[
    'Facility Name', 'Address', 'City', 'State'
]].apply(lambda x: '_'.join(x), axis=1)

hospital_reimbursement['Reimbursement_Name_Lookup'] = hospital_reimbursement[[
    'Provider Name', 'Provider Street Address', 'Provider City',
    'Provider State'
]].apply(lambda x: '_'.join(x), axis=1)

account_lookup = hospital_accounts[['Acct_Name_Lookup']].reset_index()
reimbursement_lookup = hospital_reimbursement[['Reimbursement_Name_Lookup']].reset_index()
复制代码

如今与账户信息数据合并:

account_merge = potential_matches.merge(account_lookup, how='left')
复制代码

最后,与报销数据合并:

final_merge = account_merge.merge(reimbursement_lookup, how='left')
复制代码

看看最终的数据:

cols = ['Account_Num', 'Provider_Num', 'Score',
        'Acct_Name_Lookup', 'Reimbursement_Name_Lookup']
final_merge[cols].sort_values(by=['Account_Num', 'Score'], ascending=False)
复制代码

此处演示的方法和fuzzymatcher有所不一样,fuzzymatcher每每包含多个匹配结果,例如,账号32725能够匹配两个对应项:

final_merge[final_merge['Account_Num']==32725][cols]
复制代码

在这种状况下,须要有人找出哪个匹配是最好的。幸运的是,很容易将全部数据保存到Excel中并进行进一步分析:

final_merge.sort_values(by=['Account_Num', 'Score'],
                    ascending=False).to_excel('merge_list.xlsx',
                                              index=False)
复制代码

从这个例子中能够看到,RecordLinkage工具包比fuzzymatcher更加灵活,便于自定义。RecordLinkage也并不是完美,例如对我的而言,RecordLinkage须要执行更多操做步骤才能完成数据的比较。

删除重复数据

RecordLinkage的另外一个用途是查找数据集里的重复记录,这个过程与匹配很是类似,只不过是你传递的是一个针对自身的DataFrame。

咱们来看一个使用相似数据集的例子:

hospital_dupes = pd.read_csv('hospital_account_dupes.csv', index_col='Account_Num')
复制代码

而后建立索引对象,并基于State执行sortedneighbourhood

dupe_indexer = recordlinkage.Index()
dupe_indexer.sortedneighbourhood(left_on='State')
dupe_candidate_links = dupe_indexer.index(hospital_dupes)
复制代码

根据城市、名称和地址检查是否有重复记录:

compare_dupes = recordlinkage.Compare()
compare_dupes.string('City', 'City', threshold=0.85, label='City')
compare_dupes.string('Phone Number',
                    'Phone Number',
                    threshold=0.85,
                    label='Phone_Num')
compare_dupes.string('Facility Name',
                    'Facility Name',
                    threshold=0.80,
                    label='Hosp_Name')
compare_dupes.string('Address',
                    'Address',
                    threshold=0.85,
                    label='Hosp_Address')
dupe_features = compare_dupes.compute(dupe_candidate_links, hospital_dupes)
复制代码

由于只与单个DataFrame进行比较,所以获得的DataFrame带有Account_Num_1Account_Num_2:

下面是咱们的评分方法:

dupe_features.sum(axis=1).value_counts().sort_index(ascending=False)
复制代码
3.0         7
2.0       206
1.0      7859
0.0    973205
dtype: int64
复制代码

添加分数列:

potential_dupes = dupe_features[dupe_features.sum(axis=1) > 1].reset_index()
potential_dupes['Score'] = potential_dupes.loc[:, 'City':'Hosp_Address'].sum(axis=1)
复制代码

下面是一个例子:

这些记录颇有多是重复的,咱们来查看其中一组,看看他们是否是相同的记录:

hospital_dupes.loc[51567, :]
复制代码
Facility Name                SAINT VINCENT HOSPITAL
Address                      835 SOUTH VAN BUREN ST
City                                      GREEN BAY
State                                            WI
ZIP Code                                      54301
County Name                                   BROWN
Phone Number                         (920) 433-0112
Hospital Type                  Acute Care Hospitals
Hospital Ownership    Voluntary non-profit - Church
Name: 51567, dtype: object
复制代码
hospital_dupes.loc[41166, :]
复制代码
Facility Name                   ST VINCENT HOSPITAL
Address                          835 S VAN BUREN ST
City                                      GREEN BAY
State                                            WI
ZIP Code                                      54301
County Name                                   BROWN
Phone Number                         (920) 433-0111
Hospital Type                  Acute Care Hospitals
Hospital Ownership    Voluntary non-profit - Church
Name: 41166, dtype: object
复制代码

没错,观察结果说明它们有多是重复记录,姓名和地址类似,电话号码只少了一位数字。

如你所见,这种是一个强大且相对容易的工具,用于检查数据和重复的记录。

高级用法

除了这里展现的匹配方法以外,RecordLinkage还包含了用于匹配记录的几种机器学习方法。我鼓励感兴趣的读者阅读文档中的示例。

其中一个很是方便的功能是:有一个基于浏览器的工具,它能够用来为机器学习算法生成记录对。

本文所介绍的两个包,都包含一些预处理数据的功能,以便使匹配更加可靠。

总结

在数据处理上,常常会遇到诸如“名称”和“地址”等文本字段链接不一样的记录的问题,这是颇有挑战性的。Python生态系统包含两个有用的库,它们可使用多种算法将多个数据集的记录进行匹配。

fuzzymatcher对全文搜索,经过几率实现记录链接,将两个DataFrames简单地匹配在一块儿。若是你有更大的数据集或须要使用更复杂的匹配逻辑,那么RecordLinkage是一组很是强大的工具,用于链接数据和删除重复项。

原文连接:pbpython.com/record-link…

搜索技术问答的公众号:老齐教室

为了方便你们阅读、查询本微信公众号的资源,回复:老齐,便可显示本公众号的服务目录。

相关文章
相关标签/搜索