【譯】如何合併無共同標識符的數據集

做者: 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…

搜索技術問答的公衆號:老齊教室

爲了方便你們閱讀、查詢本微信公衆號的資源,回覆:老齊,便可顯示本公衆號的服務目錄。

相關文章
相關標籤/搜索