批量模糊匹配的三种方法
作者:小小明
文章目錄
- 使用編輯距離算法進行模糊匹配
- 使用fuzzywuzzy進行批量模糊匹配
- fuzz模塊
- process模塊
- 整體代碼
- 使用Gensim進行批量模糊匹配
- Gensim簡介
- 使用詞袋模型直接進行批量相似度匹配
- 使用TF-IDF主題向量變換后進行批量相似度匹配
- 同時獲取最大的3個結果
- 完整代碼
- 總結
有時有些數據存在一定的對應關系,但是缺少連接字段,需要人工找出能夠匹配的數據建立關系。這里,我展示幾種模糊匹配的思路,應對不同量級的數據。
當然,基于排序的模糊匹配(類似于Excel的VLOOKUP函數的模糊匹配模式)也屬于模糊匹配的范疇,但那種過于簡單,不是本文討論的范疇。
本文主要討論的是以公司名稱或地址為主的字符串的模糊匹配。
使用編輯距離算法進行模糊匹配
進行模糊匹配的基本思路就是,計算每個字符串與目標字符串的相似度,取相似度最高的字符串作為與目標字符串的模糊匹配結果。
對于計算字符串之間的相似度,最常見的思路便是使用編輯距離算法。
下面我們有28條名稱需要從數據庫(390條數據)中找出最相似的名稱:
import pandas as pdexcel = pd.ExcelFile("所有客戶.xlsx") data = excel.parse(0) find = excel.parse(1) display(data.head()) print(data.shape) display(find.head()) print(find.shape)編輯距離算法,是指兩個字符串之間,由一個轉成另一個所需的最少編輯操作次數。允許的編輯操作包括將一個字符替換成另一個字符,插入一個字符,刪除一個字符。
一般來說,編輯距離越小,表示操作次數越少,兩個字符串的相似度越大。
創建計算編輯距離的函數:
def minDistance(word1: str, word2: str):'編輯距離的計算函數'n = len(word1)m = len(word2)# 有一個字符串為空串if n * m == 0:return n + m# DP 數組D = [[0] * (m + 1) for _ in range(n + 1)]# 邊界狀態初始化for i in range(n + 1):D[i][0] = ifor j in range(m + 1):D[0][j] = j# 計算所有 DP 值for i in range(1, n + 1):for j in range(1, m + 1):left = D[i - 1][j] + 1down = D[i][j - 1] + 1left_down = D[i - 1][j - 1]if word1[i - 1] != word2[j - 1]:left_down += 1D[i][j] = min(left, down, left_down)return D[n][m]關于上述代碼的解析可參考力扣題解:https://leetcode-cn.com/problems/edit-distance/solution/bian-ji-ju-chi-by-leetcode-solution/
遍歷每個被查找的名稱,計算它與數據庫所有客戶名稱的編輯距離,并取編輯距離最小的客戶名稱:
result = [] for name in find.name.values:a = data.user.apply(lambda user: minDistance(user, name))user = data.user[a.argmin()]result.append(user) find["result"] = result find測試后發現部分地址的效果不佳。
我們任取2個結果為信陽息縣淮河路店地址看看編輯距離最小的前10個地址和編輯距離:
a = data.user.apply(lambda user: minDistance(user, '河南美銳信陽息縣淮河路分店')) a = a.nsmallest(10).reset_index() a.columns = ["名稱", "編輯距離"] a.名稱 = data.user[a.名稱].values a a = data.user.apply(lambda user: minDistance(user, '河南美銳信陽潢川四中分店')) a = a.nsmallest(10).reset_index() a.columns = ["名稱", "編輯距離"] a.名稱 = data.user[a.名稱].values a可以看到,在前十個編輯距離最小的名稱中還是存在我們想要的結果。
使用fuzzywuzzy進行批量模糊匹配
通過上面的代碼,我們已經基本了解了通過編輯距離算法進行批量模糊匹配的基本原理。不過自己編寫編輯距離算法的代碼較為復雜,轉換為相似度進行分析也比較麻煩,如果已經有現成的輪子就不用自己寫了。
而fuzzywuzzy庫就是基于編輯距離算法開發的庫,而且將數值量化為相似度評分,會比我們寫的沒有針對性優化的算法效果要好很多,可以通過pip install FuzzyWuzzy來安裝。
對于fuzzywuzzy庫,主要包含fuzz模塊和process模塊,fuzz模塊用于計算兩個字符串之間的相似度,相當于對上面的代碼的封裝和優化。而process模塊則可以直接提取需要的結果。
fuzz模塊
from fuzzywuzzy import fuzz簡單匹配(Ratio):
a = data.user.apply(lambda user: fuzz.ratio(user, '河南美銳信陽潢川四中分店')) a = a.nlargest(10).reset_index() a.columns = ["名稱", "相似度"] a.名稱 = data.user[a.名稱].values a非完全匹配(Partial Ratio):
a = data.user.apply(lambda user: fuzz.partial_ratio(user, '河南美銳信陽潢川四中分店')) a = a.nlargest(10).reset_index() a.columns = ["名稱", "相似度"] a.名稱 = data.user[a.名稱].values a顯然fuzzywuzzy庫的 ratio()函數比前面自己寫的編輯距離算法,準確度高了很多。
process模塊
process模塊則是進一步的封裝,可以直接獲取相似度最高的值和相似度:
from fuzzywuzzy import processextract提取多條數據:
users = data.user.to_list() a = process.extract('河南美銳信陽潢川四中分店', users, limit=10) a = pd.DataFrame(a, columns=["名稱", "相似度"]) a從結果看,process模塊似乎同時綜合了fuzz模塊簡單匹配(Ratio)和非完全匹配(Partial Ratio)的結果。
當我們只需要返回一條數據時,使用extractOne會更加方便:
users = data.user.to_list() find["result"] = find.name.apply(lambda x: process.extractOne(x, users)[0]) find可以看到準確率相對前面自寫的編輯距離算法有了大幅度提升,但個別名稱匹配結果依然不佳。
查看這兩個匹配不準確的地址:
process.extract('許灣鄉許灣村焦艷芳衛生室', users) [('小寨溝村衛生室', 51),('周口城鄉一體化焦艷芳一體化衛生室', 50),('西華縣皮營鄉樓陳村衛生室', 42),('葉縣鄧李鄉杜楊村第二衛生室', 40),('湯陰縣瓦崗鄉龍虎村東衛生室', 40)] process.extract('河南美銳信陽息縣淮河路分店', users) [('信陽息縣淮河路店', 79),('河南美銳大藥房連鎖有限公司息縣淮河路分店', 67),('河南美銳大藥房連鎖有限公司息縣大河文錦分店', 53),('河南美銳大藥房連鎖有限公司息縣千佛庵東路分店', 51),('河南美銳大藥房連鎖有限公司息縣包信分店', 50)]對于這樣的問題,個人并沒有一個很完美的解決方案,個人建議是將相似度最高的n個名稱都加入結果列表中,后期再人工篩選:
result = find.name.apply(lambda x: next(zip(*process.extract(x, users, limit=3)))).apply(pd.Series) result.rename(columns=lambda i: f"匹配{i+1}", inplace=True) result = pd.concat([find.drop(columns="result"), result], axis=1) result雖然可能有個別正確結果這5個都不是,但整體來說為人工篩查節省了大量時間。
整體代碼
from fuzzywuzzy import process import pandas as pdexcel = pd.ExcelFile("所有客戶.xlsx") data = excel.parse(0) find = excel.parse(1) users = data.user.to_list() result = find.name.apply(lambda x: next(zip(*process.extract(x, users, limit=3)))).apply(pd.Series) result.rename(columns=lambda i: f"匹配{i+1}", inplace=True) result = pd.concat([find, result], axis=1) result使用Gensim進行批量模糊匹配
Gensim簡介
Gensim支持包括TF-IDF,LSA,LDA,和word2vec在內的多種主題模型算法,支持流式訓練,并提供了諸如相似度計算,信息檢索等一些常用任務的API接口。
基本概念:
- 語料(Corpus):一組原始文本的集合,用于無監督地訓練文本主題的隱層結構。語料中不需要人工標注的附加信息。在Gensim中,Corpus通常是一個可迭代的對象(比如列表)。每一次迭代返回一個可用于表達文本對象的稀疏向量。
- 向量(Vector):由一組文本特征構成的列表。是一段文本在Gensim中的內部表達。
- 稀疏向量(SparseVector):可以略去向量中多余的0元素。此時,向量中的每一個元素是一個(key, value)的元組
- 模型(Model):是一個抽象的術語。定義了兩個向量空間的變換(即從文本的一種向量表達變換為另一種向量表達)。
安裝:pip install gensim
官網:https://radimrehurek.com/gensim/
什么情況下需要使用NLP來進行批量模糊匹配呢?那就是數據庫數據過于龐大時,例如達到幾萬級別:
import pandas as pddata = pd.read_csv("所有客戶.csv", encoding="gbk") find = pd.read_csv("被查找的客戶.csv", encoding="gbk") display(data.head()) print(data.shape) display(find.head()) print(find.shape)此時如果依然用編輯距離或fuzzywuzzy暴力遍歷計算,預計1小時也無法計算出結果,但使用NLP神器Gensim僅需幾秒鐘,即可計算出結果。
使用詞袋模型直接進行批量相似度匹配
首先,我們需要先對原始的文本進行分詞,得到每一篇名稱的特征列表:
import jiebadata_split_word = data.user.apply(jieba.lcut) data_split_word.head(10) 0 [珠海, 廣藥, 康鳴, 醫藥, 有限公司] 1 [深圳市, 寶安區, 中心醫院] 2 [中山, 火炬, 開發區, 伴康, 藥店] 3 [中山市, 同方, 醫藥, 有限公司] 4 [廣州市, 天河區, 元崗金, 健民, 醫藥, 店] 5 [廣州市, 天河區, 元崗居, 健堂, 藥房] 6 [廣州市, 天河區, 元崗潤佰, 藥店] 7 [廣州市, 天河區, 元崗, 協心, 藥房] 8 [廣州市, 天河區, 元崗, 心怡, 藥店] 9 [廣州市, 天河區, 元崗永亨堂, 藥店] Name: user, dtype: object接下來,建立語料特征的索引字典,并將文本特征的原始表達轉化成詞袋模型對應的稀疏向量的表達:
from gensim import corporadictionary = corpora.Dictionary(data_split_word.values) data_corpus = data_split_word.apply(dictionary.doc2bow) data_corpus.head() 0 [(0, 1), (1, 1), (2, 1), (3, 1), (4, 1)] 1 [(5, 1), (6, 1), (7, 1)] 2 [(8, 1), (9, 1), (10, 1), (11, 1), (12, 1)] 3 [(0, 1), (3, 1), (13, 1), (14, 1)] 4 [(0, 1), (15, 1), (16, 1), (17, 1), (18, 1), (... Name: user, dtype: object這樣得到了每一個名稱對應的稀疏向量(這里是bow向量),向量的每一個元素代表了一個詞在這個名稱中出現的次數。
至此我們就可以構建相似度矩陣:
from gensim import similaritiesindex = similarities.SparseMatrixSimilarity(data_corpus.values, num_features=len(dictionary))再對被查找的名稱作相同的處理,即可進行相似度批量匹配:
find_corpus = find.name.apply(jieba.lcut).apply(dictionary.doc2bow) sim = index[find_corpus] find["result"] = data.user[sim.argmax(axis=1)].values find.head(30)可以看到該模型計算速度非常快,準確率似乎整體上比fuzzywuzzy更高,但fuzzywuzzy對河南美銳大藥房連鎖有限公司308廠分店的匹配結果是正確的。
使用TF-IDF主題向量變換后進行批量相似度匹配
之前我們使用的Corpus都是詞頻向量的稀疏矩陣,現在將其轉換為TF-IDF模型后再構建相似度矩陣:
from gensim import modelstfidf = models.TfidfModel(data_corpus.to_list()) index = similarities.SparseMatrixSimilarity(tfidf[data_corpus], num_features=len(dictionary))被查找的名稱也作相同的處理:
sim = index[tfidf[find_corpus]] find["result"] = data.user[sim.argmax(axis=1)].values find.head(30)可以看到許灣鄉許灣村焦艷芳衛生室匹配正確了,但河南美銳信陽息縣淮河路分店又匹配錯誤了,這是因為在TF-IDF模型中,由于美銳在很多條數據中都出現被降權。
假如只對數據庫做TF-IDF轉換,被查找的名稱只使用詞頻向量,匹配效果又如何呢?
from gensim import modelstfidf = models.TfidfModel(data_corpus.to_list()) index = similarities.SparseMatrixSimilarity(tfidf[data_corpus], num_features=len(dictionary)) sim = index[find_corpus] find["result"] = data.user[sim.argmax(axis=1)].values find.head(30)可以看到除了數據庫本來不包含正確名稱的愛聯寶之林大藥房外還剩下河南美銳大藥房連鎖有限公司308廠分店匹配不正確。這是因為不能識別出308的語義等于三零八。如果這類數據較多,我們可以先將被查找的數據統一由小寫數字轉換為大寫數字(保持與數據庫一致)后,再分詞處理:
trantab = str.maketrans("0123456789", "零一二三四五六七八九") find_corpus = find.name.apply(lambda x: dictionary.doc2bow(jieba.lcut(x.translate(trantab))))sim = index[find_corpus] find["result"] = data.user[sim.argmax(axis=1)].values find.head(30)經過這樣處理后,308廠分店也被正確匹配上了,其他類似的問題都可以使用該思路進行轉換。
雖然經過上面的處理,匹配準確率幾乎達到100%,但不代表其他類型的數據也會有如此高的準確率,還需根據數據的情況具體去分析轉換。并沒有一個很完美的批量模糊匹配的處理辦法,對于這類問題,我們不能完全信任程序匹配的結果,都需要人工的二次檢查,除非能夠接受一定的錯誤率。
為了我們人工篩選的方便,我們可以將前N個相似度最高的數據都保存到結果中,這里我們以三個為例:
同時獲取最大的3個結果
下面我們將相似度最高的3個值都添加到結果中:
result = [] for corpus in find_corpus.values:sim = pd.Series(index[corpus])result.append(data.user[sim.nlargest(3).index].values) result = pd.DataFrame(result) result.rename(columns=lambda i: f"匹配{i+1}", inplace=True) result = pd.concat([find.drop(columns="result"), result], axis=1) result.head(30)完整代碼
from gensim import corpora, similarities, models import jieba import pandas as pddata = pd.read_csv("所有客戶.csv", encoding="gbk") find = pd.read_csv("被查找的客戶.csv", encoding="gbk")data_split_word = data.user.apply(jieba.lcut) dictionary = corpora.Dictionary(data_split_word.values) data_corpus = data_split_word.apply(dictionary.doc2bow) trantab = str.maketrans("0123456789", "零一二三四五六七八九") find_corpus = find.name.apply(lambda x: dictionary.doc2bow(jieba.lcut(x.translate(trantab))))tfidf = models.TfidfModel(data_corpus.to_list()) index = similarities.SparseMatrixSimilarity(tfidf[data_corpus], num_features=len(dictionary))result = [] for corpus in find_corpus.values:sim = pd.Series(index[corpus])result.append(data.user[sim.nlargest(3).index].values) result = pd.DataFrame(result) result.rename(columns=lambda i: f"匹配{i+1}", inplace=True) result = pd.concat([find, result], axis=1) result.head(30)總結
本文首先分享了編輯距離的概念,以及如何使用編輯距離進行相似度模糊匹配。然后介紹了基于該算法的輪子fuzzwuzzy,封裝的較好,使用起來也很方便,但是當數據庫量級達到萬條以上時,效率極度下降,特別是數據量達到10萬級別以上時,跑一整天也出不了結果。于是通過Gensim計算分詞后對應的tf-idf向量來計算相似度,計算時間由幾小時降低到幾秒,而且準確率也有了較大提升,能應對大部分批量相似度模糊匹配問題。
我是小小明,歡迎在下方留言評論發表你的看法。
總結
以上是生活随笔為你收集整理的批量模糊匹配的三种方法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 联想Y9000X Opencore引导黑
- 下一篇: LSTM神经网络和GRU