作者:小小明
文章目錄
- 使用編輯距離算法進行模糊比對
- 使用fuzzywuzzy進行批量模糊比對
- fuzz子產品
- process子產品
- 整體代碼
- 使用Gensim進行批量模糊比對
- Gensim簡介
- 使用詞袋模型直接進行批量相似度比對
- 使用TF-IDF主題向量變換後進行批量相似度比對
- 同時擷取最大的3個結果
- 完整代碼
- 總結
有時有些資料存在一定的對應關系,但是缺少連接配接字段,需要人工找出能夠比對的資料建立關系。這裡,我展示幾種模糊比對的思路,應對不同量級的資料。
當然,基于排序的模糊比對(類似于Excel的VLOOKUP函數的模糊比對模式)也屬于模糊比對的範疇,但那種過于簡單,不是本文讨論的範疇。
本文主要讨論的是以公司名稱或位址為主的字元串的模糊比對。
使用編輯距離算法進行模糊比對
進行模糊比對的基本思路就是,計算每個字元串與目标字元串的相似度,取相似度最高的字元串作為與目标字元串的模糊比對結果。
對于計算字元串之間的相似度,最常見的思路便是使用編輯距離算法。
下面我們有28條名稱需要從資料庫(390條資料)中找出最相似的名稱:
import pandas as pd
excel = 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] = i
for 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] + 1
down = D[i][j - 1] + 1
left_down = D[i - 1][j - 1]
if word1[i - 1] != word2[j - 1]:
left_down += 1
D[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"] =
測試後發現部分位址的效果不佳。
我們任取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
簡單比對(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
extract提取多條資料:
users = data.user.to_list()
a = process.extract('河南美銳信陽潢川四中分店', users, limit=10)
a = pd.DataFrame(a, columns=["名稱", "相似度"])
從結果看,process子產品似乎同時綜合了fuzz子產品簡單比對(Ratio)和非完全比對(Partial Ratio)的結果。
當我們隻需要傳回一條資料時,使用extractOne會更加友善:
users = data.user.to_list()
find["result"] = find.name.apply(lambda x: process.extractOne(x, users)[0])
可以看到準确率相對前面自寫的編輯距離算法有了大幅度提升,但個别名稱比對結果依然不佳。
檢視這兩個比對不準确的位址:
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)
雖然可能有個别正确結果這5個都不是,但整體來說為人工篩查節省了大量時間。
整體代碼
from fuzzywuzzy import process
import pandas as pd
excel = 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)
使用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 pd
data = 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 jieba
data_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 corpora
dictionary = 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 similarities
index = 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 models
tfidf = 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 models
tfidf = 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 pd
data = 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)