【Python】住所データを名寄せする際に実施した8のこと

# はじめに
住所データは使用するシステムによって入力方法、データ保持の方法が違っていたり、番地の表記が違ったりと名寄せを行う際に問題となる点が多数存在します。本記事では多数存在する問題の内、8個を解決し、2種のデータを結合するPythonプログラムを紹介します。

# 8つの問題とその解決策
今回解決する8つの問題と解決策は以下の通りです。

| NO | 問題 | 解決策  |
| ---- | ---- | ---- |
| 1 | 結合するデータ2種の住所情報の持ち方が違う | それぞれのデータを加工し、以下の6カラムに統一する<br>都道府県、市区町村、町名、番地番号1、番地番号2、番地番号3 |
| 2 | 住所情報にスペースが含んでいる | 半角・全角スペースを削除する |
| 3 | 丁、番、号の表記が違う（漢字 or ハイフン） | 丁、番、号の数字だけ抽出し、番地番号1、番地番号2、番地番号3のカラムに情報を入れる |
| 4 | 市区町村以降の住所を入れるカラムに都道府県も入っている | 都道府県の情報を削除する |
| 5 | 市区町村以降の住所を入れるカラムに建物名が入っている場合と入っていない場合がある | 建物名は結合に使わない（NO.1のカラムを結合に使う） |
| 6 | 丁の数字の表記が違う（算用数字 or 漢数字） | 漢数字を算用数字に変換する |
| 7 | 番地の数字が全角数字になっている | 半角数字に変換する |
| 8 | 旧漢字が入っている | 旧漢字を新漢字に変換する |

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

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

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

| 都道府県 | 市区町村 | 町名・番地 | 氏名 |
| ---- | ---- | ---- | ---- |
| 北海道 | 釧路市 | 光陽町2-649-13 | 山田 |
| 栃木県 | 宇都宮市 | 花房本町732-8 | 加藤 |
| 愛知県 | 知多市 | 東大僧4-368-2 | 阿部 |
| 宮城県 | 仙台市若林区 | 古城4-736-16 | 佐藤 |
| 沖縄県 | 沖縄市 | 越来2-360-1 | 伊藤 |
| 青森県 | むつ市 | 柳町2-496-14 | 太田 |
| 宮城県 | 塩竈市 | 中の島414-14 | 和田 |
| 千葉県 | 千葉市美浜区 | ひび野2-136-16 | 後藤 |

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

| 都道府県 | 市区町村以下住所 | 満足度 | 市区町村以下住所の不備 |
| ---- | ---- | ---- | ---- |
| 北海道 | 釧路市光陽町2丁649番13号 | 3 | 丁が漢字になっている |
| 栃木県 | 栃木県宇都宮市花房本町732-8 | 2 | 都道府県名が入っている |
| 愛知県 | 知多市 東大僧 4-368-2 | 4 | 半角スペースが入っている |
| 宮城県 | 仙台市若林区　古城　4-736-16 | 1 | 全角スペースが入っている |
| 沖縄県 | 沖縄市越来2-360-1 越来ビル3F | 5 | ビル情報が入っている |
| 青森県 | むつ市柳町二丁496-14 | 2 | 丁の数字が漢数字になっている |
| 宮城県 | 塩竈市中の島４１４番１４号 | 3 | 番地の数字が全角数字になっている |
| 千葉県 | 千葉市美濱区ひび野2-136-16 | 3 | 旧漢字が入っている |

# データ結合後（完成系）
住所データ1と住所データ2を加工後に結合した状態は以下の通りです。

| 都道府県 | 市区町村 | 町名 | 番地番号1 | 番地番号2 | 番地番号3 | 氏名 | 満足度 |
| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- |
| 北海道 | 釧路市 | 光陽町 | 2 | 649 | 13 | 山田 | 3 |
| 栃木県 | 宇都宮市 | 花房本町 | 732 | 8 |  | 加藤 | 2 |
| 愛知県 | 知多市 | 東大僧 | 4 | 368 | 2 | 阿部 | 4 |
| 宮城県 | 仙台市若林区 | 古城 | 4 | 736 | 16 | 佐藤 | 1 |
| 沖縄県 | 沖縄市 | 越来 | 2 | 360 | 1 | 伊藤 | 5 |
| 青森県 | むつ市 | 柳町 | 2 | 496 | 14 | 太田 | 2 |
| 宮城県 | 塩竈市 | 中の島 | 414 | 14 |  | 和田 | 3 |
| 千葉県 | 千葉市美浜区 | ひび野 | 2 | 136 | 16 | 後藤 | 3 |

# 実装環境
python 3.10.4
Windows 11 Home

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

```python
import pandas as pd
import jaconv
import re

def main():
    #住所データ1を加工する
    address_1_df = pd.read_excel('住所データ1.xlsx')
    address_1_df[['町名', '番地番号1', '番地番号2', '番地番号3']] = address_1_df['町名・番地'].apply(split_town_and_number)

    #住所データ2を加工する
    address_2_df = pd.read_excel('住所データ2.xlsx', dtype='object')
    address_2_df = translate_below_municipality(address_2_df)
    address_2_df[['市区町村', '町名・番地']] = address_2_df['市区町村以下住所'].apply(split_address)
    address_2_df[['町名', '番地番号1', '番地番号2', '番地番号3']] = address_2_df['町名・番地'].apply(split_town_and_number)

    #住所データ1と住所データ2を結合する
    join_df = pd.merge(address_1_df, address_2_df, on=['都道府県', '市区町村', '町名', '番地番号1', '番地番号2', '番地番号3'], how='left')
    join_df = join_df[['都道府県', '市区町村', '町名', '番地番号1', '番地番号2', '番地番号3', '氏名', '満足度']]
    join_df.to_csv('結合後データ.csv', index=False)

#NO.1 町名・番地を町名、番地番号1、番地番号2、番地番号3に分割する
def split_town_and_number(address):
    pattern = '''\d{1,4}'''
    #NO.3 1~4桁の数字を配列で取得する（丁、番、号の表記が違う場合も対応できるようにする）
    matches = re.findall(pattern, address)
    matches_len = len(matches)
    town_name = address
    house_number_1 = ''
    house_number_2 = ''
    house_number_3 = ''

    if matches_len == 1:
        #〇〇町〇等の場合

        #最初に見つかった4桁以下の数字の前までの文字列を町名に入れる
        town_name = re.split(pattern, address)[0]

        #最初に見つかった4桁以下の数字を番地番号1に入れる
        house_number_1 = matches[0]
    elif matches_len == 2:
        #〇〇町〇-〇等の場合
        town_name = re.split(pattern, address)[0]
        house_number_1 = matches[0]
        house_number_2 = matches[1]
    elif matches_len >= 3:
        #〇〇町〇-〇-〇等の場合
        town_name = re.split(pattern, address)[0]
        house_number_1 = matches[0]
        house_number_2 = matches[1]
        house_number_3 = matches[2]

    #町名, 番地番号1, 番地番号2, 番地番号3の順で返却する
    return pd.Series([town_name, house_number_1, house_number_2, house_number_3])

#市区町村以下住所を変換する
def translate_below_municipality(address_df):
    #NO.2 スペースを削除する
    address_df['市区町村以下住所'] = address_df['市区町村以下住所'].str.replace(' ','')
    address_df['市区町村以下住所'] = address_df['市区町村以下住所'].str.replace('　','')

    #NO.6 丁の漢数字を算用数字に変換する
    address_df['市区町村以下住所'] = address_df['市区町村以下住所'].apply(translate_street_num)

    #NO.7 数字を半角に変換する
    address_df['市区町村以下住所'] = address_df['市区町村以下住所'].apply(lambda x:jaconv.z2h(x,kana=False, digit=True, ascii=False))

    #No.8 旧漢字を新漢字に変換する
    trans_table = str.maketrans('亞惡壓圍爲醫壹稻飮隱羽營榮衞益驛悅圓艷鹽奧應橫歐毆穩假價畫會囘懷繪擴殼覺學嶽樂勸卷寬歡罐觀閒關陷館巖顏歸氣龜僞戲犧卻糺舊據擧峽挾敎狹鄕堯曉區驅勳薰羣徑惠攜溪經繼莖螢輕鷄藝缺儉劍圈檢權獻縣險顯驗嚴效廣恆鑛號國黑濟碎齋劑冱櫻册雜參慘棧蠶贊殘絲飼齒兒辭濕實舍寫釋壽收從澁獸縱肅處緖諸敍奬將牀燒祥稱證乘剩壤孃條淨疊穰讓釀囑觸寢愼晉眞神刄盡圖粹醉隨髓數樞瀨晴淸精靑聲靜齊蹟攝竊專戰淺潛纖踐錢禪曾雙壯搜插爭窗總聰莊裝騷增臟藏屬續墮體對帶滯臺瀧擇澤單擔膽團彈斷癡遲晝蟲鑄猪廳聽敕鎭塚遞鐵轉點傳都黨盜燈當鬪德獨讀屆繩貳姙黏惱腦霸廢拜賣麥發髮拔飯蠻祕濱甁福拂佛竝變邊邉辨辯瓣舖穗寶萠襃豐沒飜槇萬滿默餠彌藥譯藪豫餘與譽搖樣謠遙瑤慾來賴亂覽畧隆龍兩獵綠鄰凛壘勵禮隸靈齡戀爐勞朗樓郞祿亙灣',
                                '亜悪圧囲為医壱稲飲隠羽営栄衛益駅悦円艶塩奥応横欧殴穏仮価画会回懐絵拡殻覚学岳楽勧巻寛歓缶観間関陥館巌顔帰気亀偽戯犠却糾旧拠挙峡挟教狭郷尭暁区駆勲薫群径恵携渓経継茎蛍軽鶏芸欠倹剣圏検権献県険顕験厳効広恒鉱号国黒済砕斎剤冴桜冊雑参惨桟蚕賛残糸飼歯児辞湿実舎写釈寿収従渋獣縦粛処緒諸叙奨将床焼祥称証乗剰壌嬢条浄畳穣譲醸嘱触寝慎晋真神刃尽図粋酔随髄数枢瀬晴清精青声静斉跡摂窃専戦浅潜繊践銭禅曽双壮捜挿争窓総聡荘装騒増臓蔵属続堕体対帯滞台滝択沢単担胆団弾断痴遅昼虫鋳猪庁聴勅鎮塚逓鉄転点伝都党盗灯当闘徳独読届縄弐妊粘悩脳覇廃拝売麦発髪抜飯蛮秘浜瓶福払仏並変辺辺弁弁弁舗穂宝萌褒豊没翻槙万満黙餅弥薬訳薮予余与誉揺様謡遥瑶欲来頼乱覧略隆竜両猟緑隣凜塁励礼隷霊齢恋炉労朗楼郎禄亘湾')
    address_df['市区町村以下住所'] = address_df['市区町村以下住所'].map(lambda x:x.translate(trans_table))

    return address_df

#NO.6 丁の漢数字を算用数字に変換する（一丁目から四十二丁目まで）
def translate_street_num(bef_municipality):
    aft_municipality = bef_municipality

    chinese_numerals = ['四十二','四十一','四十','三十九','三十八','三十七','三十六','三十五','三十四','三十三','三十二','三十一',
                        '三十','二十九','二十八','二十七','二十六','二十五','二十四','二十三','二十二','二十一',
                        '二十','十九','十八','十七','十六','十五','十四','十三','十二','十一',
                        '十','九','八','七','六','五','四','三','二','一'
                        ]
    
    arabics_numerals = ['42','41','40','39','38','37','36','35','34','33','32','31',
                        '30','29','28','27','26','25','24','23','22','21',
                        '20','19','18','17','16','15','14','13','12','11',
                        '10','9','8','7','6','5','4','3','2','1'
                        ]

    for num_i in range(42):
        chinese_num_street = chinese_numerals[num_i] + '丁'
        arabics_num_street = arabics_numerals[num_i] + '丁'
        aft_municipality = aft_municipality.replace(chinese_num_street, arabics_num_street)

    return aft_municipality

#市区町村以下住所を市区町村、町名・番地以下に分割する
def split_address(address):
    below_municipality = delete_prefecture(address)

    #正規表現を用いて市区町村と町名・番地（番地以降の情報も入っている場合はそれも含める）に分割する
    pattern = '''((?:札幌|仙台|さいたま|千葉|横浜|川崎|相模原|新潟|静岡|浜松|名古屋|京都|大阪|堺|神戸|岡山|広島|北九州|福岡|熊本)市.+?区|(?:蒲郡|大和郡山)市|.+?郡(?:玉村|大町|.+?)[町村]|[^市]+?[区]|(?:野々市|四日市|廿日市|.+?)[市]|.+?[町村])(.+)'''
    matches = re.match(pattern, below_municipality)

    #市区町村、町名・番地の順で返却する
    return pd.Series([matches[1], matches[2]])

#No.4 市区町村以下住所に都道府県が入っている場合、削除する
def delete_prefecture(address):
    pattern = '''(...??[都道府県])(.+)'''
    matches = re.match(pattern, address)
    if matches is None:
        #都道府県が含まれていない場合、文字列をそのまま返す
        below_municipality = address
    else:
        #都道府県が含まれている場合、都道府県を削除した文字列を返す
        below_municipality = matches[2]

    return below_municipality

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

# 終わりに
今回は住所データを名寄せする際に実施した8のことを記載しました。
本記事の処理である程度は名寄せできますが、完璧とは言い難く、誤字脱字がある場合には結合ができません。
誤字脱字があっても対応する方法の1つとして、ジャロウィンクラー距離を利用する方法があるそうです。
~~そのため、次回はジャロウィンクラー距離を利用した名寄せについて調査し、記載する予定です。~~
[ジャロウィンクラー距離を利用した名寄せ](https://qiita.com/yuya2220/items/83371476582c9729ec5b)について記載いたしました。

以上、ご参考になれば幸いです。

# 参考
この記事は以下の記事を参考にして執筆しました。
・[標準化でデータのばらつきを解消する](https://xtech.nikkei.com/it/article/COLUMN/20070615/274913/)
・[【保存版】Pythonの文字変換（半角,全角,小文字,大文字）](https://qiita.com/shakechi/items/d12641d6cad01479785f)
・[新漢字・旧漢字対照表](http://www2.japanriver.or.jp/search_kasen/search_help/refer_kanji.htm)
・[なるべく短い正規表現で住所を「都道府県/市区町村/それ以降」に分けるエクストリームスポーツ（Pythonで挑戦）](https://note.com/soysoysoyb/n/nc39eedb20a4b)
・[pandasのapplyで複数の値を返す](https://qiita.com/kromiii/items/ab4ff4cfd378dca0f099)
・[文字の変換にはstr.translate()が便利](https://qiita.com/tag1216/items/df6c93bdb823dd48af6c)