ベイジアン・フィルターを使って患者氏名情報だけで日本人か外国人かの判別をする

外国人患者数の集計を行う必要

があるのですが、電子カルテの患者プロファイルに国籍情報を入力していないため、手がかりになる情報が患者氏名だけ、という状況です。DWHから患者マスタを抽出し、Excelで氏名ソートをかけ、アルファベットとカタカナで表記されたものは外国人患者とする、という手作業が一番手軽な方法ですが、日本人でも常用漢字でない場合にカタカナで入力されていたり、結婚などで外国人名と日本人名が混合していたり、漢字名でも外国人(東アジア系)である場合等は全て抜け落ちてしまうので最後は目視確認ということになります。 こういう状況こそ機械学習の出番であることはすぐに分かったのですが、さてではどのような手法を取ればいいのか長らく不明でした。機械学習の教科書を見ていると、学習器にかけるためにはデータをベクトルにしないといけない、と前提条件みたく説明されていたので一時期はテキストをベクトル化するにはどうすればいいか本気で悩んでおりました。前回の医療情報学会(福井開催)でもMicrosoftパナソニックの人が機械学習をテーマにパネルディスカッションをしていたので思い切ってマイクの前に立って質問をぶつけて見ましたが具体的な回答は得られませんでした。 そうした中で手元にあった本にヒントを見つけました。

スパムメールを判別するために広く利用されているベイジアン・フィルター(ナイーブ・ベイズ分類器)であればテキスト情報をそのまま学習器にかけることができる!(正確には実装の中で単語出現回数とカテゴリ出現回数のベクトル化がなされます。)これならお手軽です。この本に紹介されていたベイジアン・フィルターの実装をそのまま拝借させていただきました。

import math, sys
from janome.tokenizer import Tokenizer # 形態素解析用

class BayesianFilter:
    """ ベイジアンフィルタ """
    def __init__(self):
        self.words = set() # 出現した単語を全て記録
        self.word_dict = {} # カテゴリごとの単語出現回数を記録
        self.category_dict = {} # カテゴリの出現回数を記録

    # 形態素解析を行う
    def split(self, text):
        result = []
        t = Tokenizer()
        malist = t.tokenize(text)
        for w in malist:
            sf = w.surface   # 区切られた単語そのまま 
            bf = w.base_form # 単語の基本形
            if bf == '' or bf == "*": bf = sf
            result.append(bf)
        return result

    # 単語とカテゴリを数える処理 
    def inc_word(self, word, category):
        # 単語をカテゴリに追加
        if not category in self.word_dict:
            self.word_dict[category] = {}
        if not word in self.word_dict[category]:
            self.word_dict[category][word] = 0
        self.word_dict[category][word] += 1
        self.words.add(word)
    def inc_category(self, category):
        # カテゴリを加算する
        if not category in self.category_dict:
            self.category_dict[category] = 0
        self.category_dict[category] += 1

    # テキストを学習する 
    def fit(self, text, category):
        """ テキストの学習 """
        word_list = self.split(text)
        for word in word_list:
            self.inc_word(word, category)
        self.inc_category(category)

    # カテゴリにおける単語リストのスコアを計算する 
    def score(self, words, category):
        score = math.log(self.category_prob(category))
        for word in words:
            score += math.log(self.word_prob(word, category))
        return score

    # テキストのカテゴリ分けを行う
    def predict(self, text):
        best_category = None
        max_score = -sys.maxsize 
        words = self.split(text)
        score_list = []
        for category in self.category_dict.keys():
            score = self.score(words, category)
            score_list.append((category, score))
            if score > max_score:
                max_score = score
                best_category = category
        return best_category, score_list

    # カテゴリ内の単語出現数を得る
    def get_word_count(self, word, category):
        if word in self.word_dict[category]:
            return self.word_dict[category][word]
        else:
            return 0

    # カテゴリ/総カテゴリを計算
    def category_prob(self, category):
        sum_categories = sum(self.category_dict.values())
        category_v = self.category_dict[category]
        return category_v / sum_categories
        
    # カテゴリ内の単語の出現率を計算 
    def word_prob(self, word, category):
        n = self.get_word_count(word, category) + 1
        d = sum(self.word_dict[category].values()) + len(self.words)
        return n / d

以上をbayes.pyとし、

from bayes import BayesienFilter

で呼び出します。bayes.pyはカレントディレクトリに置くことをお忘れなく。以下jupyter notebookをそのまま貼り付けます。

gist989cadcd1b7c8c3873b8ee67d4ec505d

まるで外国人患者をスパムのように扱う

という倫理観のかけらもない行い(笑)ですが楽をするためにはいたしかたありません。カテゴリは
- 外国人
- カタカナ外国人
- 漢字外国人
- 日本人
の4つとしました。カテゴリは幾つでも追加できます。初めは外国人と日本人の2択にしましたが、こうすると外国人カテゴリにアルファベット名外国人とカタカナ名外国人、漢字名外国人の3タイプが分類されることとなり、機械が混乱するのではないかと考え細分化することとしました。結果は見ての通りです。学習量が圧倒的に足りていないため精度はひどいものです。実際には患者マスタを使って学習させる計画です。患者マスタをアルファベット名、カタカナ名、漢字東アジア系、日本人系の4グループに分割し学習させます。学習量が多くなると結構な時間がかかりますがjsonファイルに学習結果を格納し、次回からはjsonファイル読み込みだけですぐに"強くてニューゲーム"が可能ですので助かります。
ところで

漢字東アジア系を目視確認以外でどうやって抽出するか

という問題がここで浮上します。WebスクレイピングWikipedia

漢姓 - Wikipedia

朝鮮人の姓の一覧 - Wikipedia

から漢姓(中国人姓)と朝鮮姓データを抜いてくることにしました。

gist58f7a2414250bfd924ff1fe69d4f96a0

朝鮮姓には一部日本人姓と共通のもの(例:岡田(カンジョン))がありましたのでそれらは除外とさせていただきました。桂、東、西、星、林もあります、こいつらどうするかな・・・。まあ、目視確認の手間は100%取り除くことはできない、ということでしょう。