做者: 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,因此咱們將看看是否可使用前面提到的工具,根據醫院的名稱和地址信息將兩個數據集合並。
在第一種方法中,咱們將嘗試使用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 Name
和Provider 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結合,使它成爲一個很好的工具。
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_1
和Account_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…
搜索技術問答的公衆號:老齊教室
爲了方便你們閱讀、查詢本微信公衆號的資源,回覆:老齊,便可顯示本公衆號的服務目錄。