ベネッセと河合塾の大学コードを対応させる② PythonでN-gram
ベネッセと河合塾の大学コード番号を寄せ、突き合わせたいというところから出発し、Excelでのbi-gramでは十分な精度が得られなかったため、pythonを使ってuni- bi- tri-gramを行い、最も一致率の高いものとマッチングする。
大学コード番号の処理に特化せず、ちょっと汎用性を持たせ、一般的に1列目がindex番号(重複のない番号ならOK)、2列目が文字列、3列目以降がデータとなっている2つのタブ区切りファイル(.tsv)をそれぞれの2列目の文字列の類似度でマッチングし、1つのファイル(match.csv)を作成します。つまり、日本語の揺らぎを許容するリレーショナルツールです。
【概要】
make_bene_kawai_tsv.xlsmを使って、(使わなくても良いのだが)読込ファイルを2つ作って…
サンプル(こんな感じのものが出力されます)
benesse_sample.tsv
kawai_sample.tsv
これをマッチングプログラムに読ませまると、1つのファイル(match_(日付).csv)として出力されます。
match201901111622.csv
【マッチングプログラムのpythonコード】
(Anacondaを導入して、Spyderを使うなどして実行してください。)
import sys from os.path import abspath, dirname from csv import QUOTE_ALL, QUOTE_NONE from sklearn.feature_extraction.text import TfidfVectorizer from pandas import DataFrame, read_table, merge import unicodedata from tkinter import Tk, StringVar, LEFT, ttk, filedialog, messagebox from datetime import datetime ####参照ボタンのイベント # button1クリック時の処理 def button_master_clicked(): fTyp = [("","*")] iDir = abspath(dirname(__file__)) filepath_master = filedialog.askopenfilename(filetypes = fTyp,initialdir = iDir) file_master.set(filepath_master) def button_slave_clicked(): fTyp = [("","*")] iDir = abspath(dirname(__file__)) filepath_slave = filedialog.askopenfilename(filetypes = fTyp,initialdir = iDir) file_slave.set(filepath_slave) ####マッチングの処理 # button4クリック時の処理 def button4_clicked(): path_files = { "master": file_master.get(), "slave":file_slave.get() } df_master = read_table(path_files["master"], header=0, dtype=str, encoding='Shift_JISx0213', engine="python") df_slave = read_table(path_files["slave"], header=0, dtype=str, encoding='Shift_JISx0213', engine="python") df_master = df_master.rename(columns={df_master.columns[0]:"INDEX_master"}) df_slave = df_slave.rename(columns={df_slave.columns[0]:"INDEX_slave"}) # drop empty rows nan_to_none = lambda v: None if (v is None or v == "nan") else v df_master["INDEX_master"] = df_master["INDEX_master"].map(nan_to_none) df_slave["INDEX_slave"] = df_slave["INDEX_slave"].map(nan_to_none) df_master.dropna(axis=0, subset=["INDEX_master"], inplace=True) df_slave.dropna(axis=0, subset=["INDEX_slave"], inplace=True) # convert index into integer df_master["INDEX_master"] = df_master["INDEX_master"].astype(int) df_slave["INDEX_slave"] = df_slave["INDEX_slave"].astype(int) print(file_master.get()) print(file_slave.get()) print('読み込み完了') # NFKC-normalization, lowercase and fill empty with special token normalizer = lambda s: unicodedata.normalize("NFKC", s).lower() for df in [df_master, df_slave]: for column in df.columns: df[column] = df[column].map(lambda v: normalizer(v) if isinstance(v, str) else v) print('# NFKC-normalization完了') #tup n_master = df_master.shape[0] n_slave = df_slave.shape[0] print("master: %d 個, slave: %d 個" % (n_master, n_slave)) tup_suffix = ("_master","_slave") #character-ngram function def ngrams_single(string, n, pad): if pad and (n > 1): s_pad = "$"*(n-1) string = s_pad+string+s_pad ngrams = zip(*[string[i:] for i in range(n)]) return [''.join(ngram) for ngram in ngrams] def ngrams(string, lst_n, pad=True): ret = [] for n in lst_n: ret.extend(ngrams_single(string, n, pad)) return ret ### instanciate feature extractor: character {uni,bi,tri}-gram vectorizer = TfidfVectorizer(min_df=1, analyzer=lambda s: ngrams(s, lst_n=[1,2,3]), norm="l2", use_idf=False) vec_master = df_master.iloc[:,1] vec_slave = df_slave.iloc[:,1] #文字unigram/bigram/trigramを適用して,辞書を生成する vectorizer.fit(vec_master) vectorizer.fit(vec_slave) mat_index = vectorizer.transform(vec_master) mat_query = vectorizer.transform(vec_slave) #文字列を term-frequency vector に変換する #文字列を unigram/bigram/trigram に分解 #定義した辞書を用いて,整数列に変換 #整数ごとに集計 #二乗和が1になるように正規化 #find most similar entry #calculate similarity matrix mat_sim = mat_query.dot(mat_index.T) #matching function #find the best alignment under non-duplicate matching def match_without_replacement_single(mat_sim, idx_row, idx_col): # row-major matching idx_match = mat_sim[idx_row][:,idx_col].argmax(axis=1).A1 T_row = set([(i,idx_col[m]) for i,m in zip(idx_row, idx_match)]) if len(set(idx_match)) == len(idx_row): return T_row # column-major matching idx_match = mat_sim[idx_row][:,idx_col].argmax(axis=0).A1 T_col = set([(idx_row[m],j) for m,j in zip(idx_match, idx_col)]) # merge with two result T_match = T_row & T_col return T_match def match_without_replacement(mat_sim, score=True): T_match = set() idx_row = list(range(mat_sim.shape[0])) idx_col = list(range(mat_sim.shape[1])) while True: T_match_t = match_without_replacement_single(mat_sim, idx_row, idx_col) T_match = T_match | T_match_t idx_row = [row for row in idx_row if row not in [i for i,j in T_match_t]] idx_col = [col for col in idx_col if col not in [j for i,j in T_match_t]] if len(idx_row) == 0 or len(idx_col) == 0: break if score: T_match = [(i,j,mat_sim[i,j]) for i,j in T_match] return T_match #execute matching T_match = match_without_replacement(mat_sim, score=True) df_match = DataFrame(list(T_match), columns=["idx_slave","idx_master","matching_score"]).sort_values(by="idx_slave").reset_index(drop=True) assert df_match.shape[0] == df_slave.shape[0] assert set(df_match["idx_slave"]) == set(range(df_slave.shape[0])) print(str(df_slave.shape[0]) + ' 個マッチング完了') #store result df_slave["INDEX_master"] = df_master.iloc[:,0].values[df_match["idx_master"]] df_slave["matching_score"] = df_match["matching_score"] df_result = merge(df_master, df_slave, on="INDEX_master", how="left", suffixes=tup_suffix) basename = datetime.now().strftime("%Y%m%d%H%M") # output as csv file(compatible with excel) df_result.to_csv("./match"+basename+".csv", encoding="Shift_JISx0213", header=True, index=False) messagebox.showinfo('マッチングツール ver.1.0 by kinkin','match'+basename+'.csvを出力しました。') ########GUIに関する部分 if __name__ == '__main__': # rootの作成 root = Tk() root.title('マッチングツール ver.1.0 by kinkin') root.resizable(False, False) # Frame1の作成 frame1 = ttk.Frame(root, padding=10) frame1.grid(row=1) # ラベルの作成 # 「親ファイル」ラベルの作成 s_master = StringVar() s_master.set('親ファイル>>') label1 = ttk.Label(frame1, textvariable=s_master) label1.grid(row=1, column=1) # 参照ファイルパス表示ラベル1の作成 file_master = StringVar() file_master_entry = ttk.Entry(frame1, textvariable=file_master, width=55) file_master_entry.grid(row=1, column=2) # 参照ボタン1の作成 button_master = ttk.Button(root, text=u'参照1', command=button_master_clicked) button_master.grid(row=1, column=3) # Frame2の作成 frame2 = ttk.Frame(root, padding=10) frame2.grid(row=2) # ラベルの作成 # 「子ファイル」ラベルの作成 s_slave = StringVar() s_slave.set('子ファイル>>') label3 = ttk.Label(frame2, textvariable=s_slave) label3.grid(row=1, column=1) # 参照ファイルパス表示ラベル2の作成 file_slave = StringVar() file_slave_entry = ttk.Entry(frame2, textvariable=file_slave, width=55) file_slave_entry.grid(row=1, column=2) # 参照ボタン2の作成 button_slave = ttk.Button(root, text=u'参照2', command=button_slave_clicked) button_slave.grid(row=2, column=3) # Frame3の作成 frame3 = ttk.Frame(root, padding=(10,5)) frame3.grid(row=3) # ラベルの作成 # 「ファイル」ラベルの作成 s4 = StringVar() s4.set('2列目を比較して親と子をマッチングし,match.exeフォルダに結果を出力します。') label4 = ttk.Label(frame3, textvariable=s4) label4.grid(row=1) # Frame4の作成 frame4 = ttk.Frame(root, padding=(0,5)) frame4.grid(row=4) # 処理開始ボタンの作成 button4 = ttk.Button(frame4, text='マッチング処理', command=button4_clicked) button4.pack(side=LEFT) # Cancelボタンの作成 button3 = ttk.Button(frame4, text='終了する', command=sys.exit) button3.pack(side=LEFT) root.mainloop()
そのうち、codeの解説と、exeファイルを置きたいと思っています。そのうち…。