別棟備忘録4

*

ベネッセと河合塾の大学コードを対応させる② PythonでN-gram

   

ベネッセと河合塾の大学コード番号を寄せ、突き合わせたいというところから出発し、Excelでのbi-gramでは十分な精度が得られなかったため、pythonを使ってuni- bi- tri-gramを行い、最も一致率の高いものとマッチングする。
大学コード番号の処理に特化せず、ちょっと汎用性を持たせ、一般的に1列目がindex番号(重複のない番号ならOK)、2列目が文字列、3列目以降がデータとなっている2つのタブ区切りファイル(.tsv)をそれぞれの2列目の文字列の類似度でマッチングし、1つのファイル(match.csv)を作成します。つまり、日本語の揺らぎを許容するリレーショナルツールです。

match

【概要】
make_bene_kawai_tsv.xlsmを使って、(使わなくても良いのだが)読込ファイルを2つ作って…
サンプル(こんな感じのものが出力されます)
benesse_sample.tsv
kawai_sample.tsv
これをマッチングプログラムに読ませまると、1つのファイル(match_(日付).csv)として出力されます。
match201901111622.csv
match_result

【マッチングプログラムのpythonコード】
Anacondaを導入して、Spyderを使うなどして実行してください。)

match.py

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ファイルを置きたいと思っています。そのうち…。

 - stringDistance