【Python】住所データをジャロ・ウィンクラー距離を用いて名寄せする

# はじめに
住所データは使用するシステムによって入力方法、データ保持の方法が違っていたり、番地の表記（漢字 or ハイフン）が違ったりと名寄せを行う際に問題となる点が多数存在します。
前回の記事「[【Python】住所データを名寄せする際に実施した8のこと][past_document]」では正規表現や文字列置換で住所を変換後、完全一致で突合させていましたが、誤字・脱字がある場合には突合できないという問題がありました。
そのため、本記事では住所データに誤字脱字があっても名寄せを行う方法の1つである、ジャロ・ウィンクラー距離を利用した手法を紹介いたします。

[past_document]:https://qiita.com/yuya2220/items/1106a7acdd70208f4fd4

# ジャロ・ウィンクラー距離とは
2つの文字列がどのくらい類似しているかを0から1の数値で示したものです。
数値が0の時は全く異なる文字列で、数値が1の時は完全に一致している文字列になります。

# 使用するデータ
使用するデータは以下の2つです。（住所データは[こちら][random]のランダム住所作成サイトで作成しています）

[random]:https://nextvitz.com/jp/post-address-random-1.php

- 住所データ1
住所が3つのカラムで入っている

    | 都道府県 | 市区町村 | 町名・番地 |
    | ---- | ---- | ---- |
    | 北海道 | 釧路市 | 光陽町2-649-13 |
    | 宮城県 | 仙台市若林区 | 古城4-736-16 |
    | 宮城県 | 仙台市青葉区 | 子平町4-546-1 |

<br>

- 住所データ2
住所が2つのカラムで入っている
住所データ1と異なり「市区町村以下住所」に不備がある

    | 都道府県 | 市区町村以下住所 | 市区町村以下住所の不備 |
    | ---- | ---- | ---- |
    | 北海道 | 釧路市光陽町2丁649番13号 | 番地の表記が漢字になっている |
    | 宮城県 | 仙台市若林区湖上4-736 | 誤字脱字がある |
    | 宮城県 | 仙台市青葉区子平町4-546-1 東ビル3F | ビル情報が入っている |

# 処理の流れ

1. 住所データ1と住所データ2の住所を1つのカラムにまとめる

    - 住所データ1
    
    | 住所データ1の住所 |
    | ---- | 
    | 北海道釧路市光陽町2-649-13 |
    | 宮城県仙台市若林区古城4-736-16 |
    | 宮城県仙台市青葉区子平町4-546-1 |

    - 住所データ2
    
    | 住所データ2の住所 |
    | ---- |
    | 北海道釧路市光陽町2丁649番13号 |
    | 宮城県仙台市若林区湖上4-736 |
    | 宮城県仙台市青葉区子平町4-546-1 東ビル3F |

<br>
2. クロス結合を行い、ジャロ・ウィンクラー距離を求める

| 住所データ1の住所 | 住所データ2の住所 | ジャロ・ウィンクラー距離 |
| ---- | ---- | ---- |
| 北海道釧路市光陽町2-649-13 | 北海道釧路市光陽町2丁649番13号 | 0.94 |
| 北海道釧路市光陽町2-649-13 | 宮城県仙台市若林区湖上4-736 | 0.4 |
| 北海道釧路市光陽町2-649-13 | 宮城県仙台市青葉区子平町4-546-1 東ビル3F | 0.56 |
| 宮城県仙台市若林区古城4-736-16 | 北海道釧路市光陽町2丁649番13号 | 0.38 |
| 宮城県仙台市若林区古城4-736-16 | 宮城県仙台市若林区湖上4-736 | 0.92 |
| 宮城県仙台市若林区古城4-736-16 | 宮城県仙台市青葉区子平町4-546-1 東ビル3F | 0.68 |
| 宮城県仙台市青葉区子平町4-546-1 | 北海道釧路市光陽町2-649-13 | 0.45 |
| 宮城県仙台市青葉区子平町4-546-1 | 宮城県仙台市若林区湖上4-736 | 0.83 |
| 宮城県仙台市青葉区子平町4-546-1 | 宮城県仙台市青葉区子平町4-546-1 東ビル3F | 0.95 |

<br>
3. 「住所データ1の住所」でグループ化し、ジャロ・ウィンクラー距離の値が最も高いレコードだけ残す

| 住所データ1の住所 | 住所データ2の住所 | ジャロ・ウィンクラー距離 |
| ---- | ---- | ---- |
| 北海道釧路市光陽町2-649-13 | 北海道釧路市光陽町2丁649番13号 | 0.94 |
| 宮城県仙台市若林区古城4-736-16 | 宮城県仙台市若林区湖上4-736 | 0.92 |
| 宮城県仙台市青葉区子平町4-546-1 | 宮城県仙台市青葉区子平町4-546-1 東ビル3F | 0.95 |

<br>
4. ジャロ・ウィンクラー距離の閾値を設定し、値が小さいデータは除外する（プログラムでは0.8に設定しているが、今回の例では0.8未満のデータはないため変化せず）

| 住所データ1の住所 | 住所データ2の住所 | ジャロ・ウィンクラー距離（0.8以上） |
| ---- | ---- | ---- |
| 北海道釧路市光陽町2-649-13 | 北海道釧路市光陽町2丁649番13号 | 0.94 |
| 宮城県仙台市若林区古城4-736-16 | 宮城県仙台市若林区湖上4-736 | 0.92 |
| 宮城県仙台市青葉区子平町4-546-1 | 宮城県仙台市青葉区子平町4-546-1 東ビル3F | 0.95 |

# ジャロ・ウィンクラー距離を用いて名寄せする際のメリット・デメリット
ジャロ・ウィンクラー距離を用いて名寄せする際のメリット・デメリットについて記載します。

## メリット
1. 誤字脱字があっても名寄せできる
2. 番地の表記（漢字 or ハイフン）が違くても名寄せできる
3. 数字の表記（算用数字 or 漢数字）が違くても名寄せできる

## デメリット
1. 似ている別の住所と突合してしまう可能性がある
2. 件数が多いと処理に時間がかかる

1のデメリットは閾値の値を大きくすることで減らすことが可能ですが、突合できるデータ数は減少します。
2のデメリットは完全一致で名寄せ出来なかったもののみ処理を行う等、ジャロ・ウィンクラー距離を計算するデータ数を事前に減らしておくことで、ある程度の時間短縮が可能です。

# 実装環境
python 3.10.4
Windows 11 Home

# 作成コード
作成したコードは以下になります。

```python
import pandas as pd
import Levenshtein

def main():
    address_1_df = pd.read_excel('住所データ1.xlsx', dtype='object')
    address_1_df['住所データ1_住所'] = address_1_df['都道府県'] + address_1_df['市区町村'] + address_1_df['町名・番地']
    address_1_df = address_1_df.reset_index()
    address_1_df = address_1_df.rename(columns={'index': '住所データ1_index'})

    address_2_df = pd.read_excel('住所データ2.xlsx', dtype='object')
    address_2_df['住所データ2_住所'] = address_2_df['都道府県'] + address_2_df['市区町村以下住所']
    address_2_df = address_2_df.reset_index()
    address_2_df = address_2_df.rename(columns={'index': '住所データ2_index'})

    jaro_winkler_df = make_jaro_winkler_df(address_1_df, address_2_df)
    jaro_winkler_df = filter_jaro_winkler_df(jaro_winkler_df)

    join_df = pd.merge(address_1_df, jaro_winkler_df, on=['住所データ1_index'], how='left')
    join_df = pd.merge(join_df, address_2_df, on=['住所データ2_index'], how='left')
    join_df = join_df[['住所データ1_住所', '住所データ2_住所', 'ジャロ・ウィンクラー距離']]

    join_df.to_csv('結合後データ.csv', index=False)

#ジャロ・ウィンクラー距離の計算を実行したdataframeを取得する
def make_jaro_winkler_df(address_1_df, address_2_df):
    conversion_tables = []
    values_1 = address_1_df['住所データ1_住所'].values
    values_2 = address_2_df['住所データ2_住所'].values

    #クロス結合を行い、全てのパターンでジャロ・ウィンクラー距離の計算を行う
    for adr_1_i in range(address_1_df.shape[0]):
        address_1 = values_1[adr_1_i]
        for adr_2_i in range(address_2_df.shape[0]):
            address_2 = values_2[adr_2_i]
            dist_jaro = Levenshtein.jaro_winkler(address_1, address_2)
            dist_jaro_round = round(dist_jaro,2)
            conversion_tables.append([adr_1_i, adr_2_i, dist_jaro_round])

    jaro_winkler_df = pd.DataFrame(conversion_tables, columns=['住所データ1_index','住所データ2_index','ジャロ・ウィンクラー距離'])

    return jaro_winkler_df

#クロス結合を行ったデータを絞り込む
def filter_jaro_winkler_df(jaro_winkler_df):
    #住所データ1_indexでグループ化後、ジャロ・ウィンクラー距離の最大値を取得する
    jaro_group_df = jaro_winkler_df.groupby(['住所データ1_index'])
    jaro_winkler_df = jaro_winkler_df.loc[jaro_group_df['ジャロ・ウィンクラー距離'].idxmax(), :]

    #ジャロ・ウィンクラー距離が0.8以上のレコードだけ取得する（閾値の設定を行う）
    jaro_winkler_df = jaro_winkler_df[jaro_winkler_df['ジャロ・ウィンクラー距離'] > 0.8]
 
    return jaro_winkler_df

if __name__ == '__main__':
    main()
```

# 終わりに
今回は住所データをジャロ・ウィンクラー距離を用いて名寄せする方法を記載しました。
本記事で紹介した手法は、似ている別の住所と突合してしまう可能性がある以上、絶対に正しい住所同士で突合しなければならない場合には向きません。
ですが、多少間違っていても多くのデータを突合させたいといった場合には、誤字脱字にも対応できることから向いていると言えそうです。
以上、ご参考になれば幸いです。

# 参考
この記事は以下の記事を参考にして執筆しました。
・[【Python】レーベンシュタイン距離とジャロ・ウィンクラー距離の計算方法](https://qiita.com/tetsuro731/items/1bb1700b687d391c3c7b)