トップページ -> Pythonを使って郵便番号上2桁で地図を色分けする

Pythonを使って郵便番号上2桁で地図を色分けする

今回はPythonで郵便番号で地図を色分けすることを考えます. geopandasを使った色分け地図の描画とfoliumを使ったインタラクティブな色分け地図の作成をします.
今回は郵便番号のデータと市町村の境界データを組み合わせて1から作りますが,出来上がっているものがほしい場合は郵便番号境界データ - 地図地理Sandbox都道府県別郵便番号地図 - 地図地理Sandboxが便利です.
【関連(外部サイト)】地図地理Sandbox
地理関連の面白いコンテンツがたくさん扱われています.

  1. 郵便番号のデータをダウンロードする
  2. 境界データをダウンロードする
  3. 郵便番号データの下処理をする
  4. 郵便番号と境界データを組み合わせて地図を色分けする
  5. foliumでインタラクティブな地図を作る
  6. 参考サイト
【完成例】インタラクティブな色分け地図
クリックすることで郵便番号上2桁とその郵便番号を持つ地名を確認できます.本ページの最後で東京都だけ作ります.
インタラクティブな地図のスクリーンショット
インタラクティブな地図

1. 郵便番号のデータをダウンロードする

郵便局のホームページで公開されている郵便番号のデータをダウンロードします. 郵便番号データダウンロードから【読み仮名データの促音・拗音を小書きで表記するもの】をクリックしてKEN_ALL.CSVというファイルをダウンロードします.
01101,"064 ","0640941","ホッカイドウ","サッポロシチュウオウク","アサヒガオカ","北海道","札幌市中央区","旭ケ丘",0,0,1,0,0,0
中には上のような郵便番号と地名のデータが入っています.

2. 境界データをダウンロードする

e-Stat 統計地理情報システムデータダウンロードから市町村の境界データをダウンロードします. 今回は全国地図を色分けするので以下のコードで一括ダウンロードしましたが,処理を軽くしたい場合は1つの都の中に郵便番号上2桁にバリエーションのある東京だけでも試すことができます.


import os
import requests
import zipfile
from io import BytesIO
import time

# 保存フォルダ
output_dir = "境界データ"
os.makedirs(output_dir, exist_ok=True)

# 都道府県コード(01〜47)
pref_codes = [f"{i:02d}" for i in range(1, 48)]

# ダウンロードに使うURL 
url_template = "https://www.e-stat.go.jp/gis/statmap-search/data?dlserveyId=A002005212015&code={code}&coordSys=1&format=shape&downloadType=5"

for code in pref_codes:
    url = url_template.format(code=code)
    zip_path = os.path.join(output_dir, f"h27ka{code}.zip")

    try:
        print(f"Downloading: {url}")
        response = requests.get(url, stream=True)
        response.raise_for_status()

        # ZIPファイルを保存
        with open(zip_path, "wb") as f:
            f.write(response.content)

        # ZIPを解凍
        with zipfile.ZipFile(zip_path, "r") as zip_ref:
            zip_ref.extractall(os.path.join(output_dir, f"h27ka{code}"))

        print(f"Downloaded and extracted: {zip_path}")

        # サーバー負荷を抑えるために1リクエストにつき5秒待機を入れる
        time.sleep(5)

    except requests.exceptions.RequestException as e:
        print(f"Failed to download {url}: {e}")
以下のコードでダウンロードした境界データを表示することができます. shpファイルをgeopandasを使って表示します. まずは北海道だけ表示してみます. geopandasはgeometryの列にあるPOLYGONデータ(多角形の頂点集合)を描画して地図を表示してくれます.
北海道の境界データ
北海道の境界データ

import geopandas as gpd

# 北海道の境界データを表示
gdf = gpd.read_file("境界データ/h27ka01/h27ka01.shp")

# 内容を表示する
print(gdf.head())  # 最初の数行を表示する

# 地図をプロット
gdf.plot(figsize=(10, 10)) # 拡大したい場合はサイズを大きくしてください
plt.show()
境界データを結合して表示したもの
境界データを結合して表示したもの

import geopandas as gpd
import os
import matplotlib.pyplot as plt

# 境界データのフォルダ
base_dir = "境界データ"

# 各フォルダ内の .shp ファイルパスをリスト化
file_list = [os.path.join(base_dir, f, f"{f}.shp") 
             for f in os.listdir(base_dir) 
             if os.path.isdir(os.path.join(base_dir, f))]

gdfs = [] # gdfのリスト

for f in file_list:
    gdf = gpd.read_file(f)
    gdfs.append(gdf)

# すべてのデータを結合
gdf = gpd.pd.concat(gdfs, ignore_index=True)

# 地図をプロット
gdf.plot(figsize=(10, 10)) # 拡大したい場合はサイズを大きくしてください
plt.show()

3. 郵便番号データの下処理をする

組み合わせて表示することもできたので郵便番号のデータと境界データを組み合わせて地図を色分けをしたいのですが,郵便番号の地名と境界データのCITY_NAMEが微妙に違います. 以下のコードでpost_code_dict[都道府県名:地名] = "XY"を作りました. まとめて示されるとコードの必然性がわかりにくいですが,実際にはpost_code_dictのキーとset(gdf["CITY_NAME"])の包含関係を調べながら少しずつ書きました. ※ キーを都道府県:地名としている理由は全国に北区(などの同じ地名)がたくさん(北区は11地区)あり判別できなくなるためなのですが,厳密には大阪に大阪市北区,堺市北区の2つの北区が存在するためこれでも不十分です.(他にもあるかもしれません) 本来ならgdfのCITYを使って処理すべきなのですが,簡略化のため割愛します.


# ----------------- 郵便番号データの手直し -------------------

# CSVファイルを開く(Shift-JIS でエンコードされている)
with open("KEN_ALL.CSV", "r", encoding="shift_jis") as f:
    lines = f.readlines()

from collections import defaultdict

# post_code_dict[都道府県名:地名] = {"XX": cnt_x, "YY": cnt_y}のようにして後で多い方を採用する
post_code_dict = defaultdict(lambda: defaultdict(int))  

for line in lines:
    data = line.replace('"',"").split(",") # 行をリスト化する
    post_code = data[2][:2] # 郵便番号の上2桁
    city_name = data[7] # 地名
    # 郡の処理
    if not any(city in data[7] for city in ['小郡市', '大和郡山市', '蒲郡市', '郡上市', '郡山市']):
        city_name = data[7].split("郡")[-1]
        # 区の処理
        if city_name[-1] == "区":
            city_name = city_name.split("市")[-1]
            
    # 島の処理
    if data[7] == '八丈島八丈町':
        city_name = '八丈町'
    if data[7] == '三宅島三宅村':
        city_name = '三宅村'
    if data[7] == '赤穂郡上郡町':
        city_name = '上郡町'
    # '郡上市' に巻き込まれるので特殊処理
    if data[7] == '中新川郡上市町':
        city_name = data[7].split("郡")[-1]

    post_code_dict[data[6]+":"+city_name][post_code] += 1
    
# その郵便番号の地域の数が多いものをその地区の郵便番号とする
for city in post_code_dict:
    post_code_dict[city] = max(post_code_dict[city], key=post_code_dict[city].get)
    
# 2015年以降に再編された地名の追加
post_code_dict["兵庫県:宝塚市"] = "66"
post_code_dict["千葉県:袖ヶ浦市"] = "29"
post_code_dict["兵庫県:篠山市"] = "66"
post_code_dict["福岡県:須恵町"] = "81"
post_code_dict["宮城県:富谷町"] = "98"
post_code_dict["福岡県:那珂川町"] = "32"

post_code_dict["静岡県:西区"] = "43"
post_code_dict["静岡県:中区"] = "43"
post_code_dict["静岡県:東区"] = "43"
post_code_dict["静岡県:南区"] = "43"
post_code_dict["静岡県:北区"] = "43"

4. 郵便番号と境界データを組み合わせて地図を色分けする

post_code_dictとgdfを組み合わせて色分けをして表示します. 完全に正確ではありませんが,郵便番号の上2桁で地図を色分けすることができました.

郵便番号上2桁で色分けしたもの
郵便番号上2桁で色分けしたもの

# 100色のカラーマップを作成
color_list = [
    "#E41A1C", "#377EB8", "#4DAF4A", "#984EA3", "#FF7F00", "#FFFF33", "#A65628", "#F781BF", "#999999",
    "#66C2A5", "#FC8D62", "#8DA0CB", "#E78AC3", "#A6D854", "#FFD92F", "#E5C494", "#B3B3B3", "#1B9E77",
    "#D95F02", "#7570B3", "#E7298A", "#66A61E", "#E6AB02", "#A6761D", "#666666", "#7FC97F", "#BEAED4",
    "#FDC086", "#FFFF99", "#386CB0", "#F0027F", "#BF5B17", "#666699", "#CC66CC", "#9999FF", "#99CC99",
    "#CC9966", "#CC9999", "#FF6666", "#669999", "#FF99CC", "#6699FF", "#FFCC99", "#CCCC66", "#CCCCFF",
    "#99CCFF", "#FFCC66", "#9999CC", "#66FF66", "#FF6699", "#33CC33", "#FF9966", "#66CCCC", "#CCFF66",
    "#9933CC", "#FF3399", "#33FF99", "#996633", "#3399FF", "#CCFF99", "#993399", "#669933", "#FF9933",
    "#99FFCC", "#FF3300", "#99FF99", "#6633FF", "#CC3333", "#33FFFF", "#FF3366", "#99CC33", "#3333FF",
    "#99FFFF", "#FF6600", "#66FF99", "#FFCC00", "#6699CC", "#CCCC99", "#336699", "#FF0066", "#669966",
    "#CC6633", "#FF33CC", "#33CCFF", "#33CC66", "#9966CC", "#33FF33", "#CCFF33", "#6600CC", "#FF6633",
    "#33FFCC", "#663366", "#3366CC", "#99CC66", "#66CC33", "#66FFCC", "#9900CC", "#FFCCCC", "#66CC66",
    "#FF99FF", "#339966", "#CC99FF", "#333366"
]


# 各都道府県+市区町村に対応する郵便番号の上2桁を取得し、色を決定する関数(applyで一括適用するため)
def get_zip_prefix(pref, city):
    """ 都道府県名 + 市区町村名 をキーにして郵便番号の上2桁を取得 """
    return post_code_dict.get(pref + ":" + city, "00")

# applyで郵便番号の上2桁を取得 
gdf["zip_prefix"] = gdf.apply(lambda row: get_zip_prefix(row["PREF_NAME"], row["CITY_NAME"]), axis=1)

# 上2桁ごとに色をマッピング
unique_zip_prefixes = sorted(gdf["zip_prefix"].unique())  # ソートして色の順番を固定
color_dict = {prefix: color_list[i] for i, prefix in enumerate(unique_zip_prefixes)}
gdf["color"] = gdf["zip_prefix"].map(color_dict)

# 地図をプロット(郵便番号の上2桁ごとに色分け)
fig, ax = plt.subplots(figsize=(10, 10))
gdf.plot(color=gdf["color"], edgecolor=None, linewidth=0, ax=ax)
plt.show()

5. foliumでインタラクティブな地図を作る

色分けはできましたが,イマイチどうなっているのかよく分からないのでクリックして地名を確かめられるようにします. 以下のコードは東京だけ読み込んでインタラクティブな地図を作るコードです.処理が終わると"interactive_map_tokyo.html"ができ,ブラウザで開くと画像のようなページが開きます.

インタラクティブな地図のスクリーンショット
インタラクティブな地図

import folium
# 東京だけ読み込む
gdf = gpd.read_file('境界データ\\h27ka13')

# 郵便番号の上2桁を取得
def get_zip_prefix(pref, city):
    return post_code_dict.get(pref + ":" + city, "00")

gdf["zip_prefix"] = gdf.apply(lambda row: get_zip_prefix(row["PREF_NAME"], row["CITY_NAME"]), axis=1)

# 郵便番号の上2桁を取得(加工済みデータには `zip_prefix` がある前提)
unique_zip_prefixes = sorted(gdf["zip_prefix"].unique())  # ソートして色の順番を固定
color_dict = {prefix: color_list[i % len(color_list)] for i, prefix in enumerate(unique_zip_prefixes)}
gdf["color"] = gdf["zip_prefix"].map(color_dict)

# マップの中心を計算(全体の中心)
gdf = gdf.to_crs(epsg=4326)  # WGS84(緯度経度)
center = gdf.geometry.centroid.unary_union.centroid
m = folium.Map(location=[center.y+0.2, center.x], zoom_start=10)

# 地図に境界データを追加
for _, row in gdf.iterrows():
    geo_json = row.geometry.__geo_interface__  # GeoJSON形式に変換
    folium.GeoJson(
        geo_json,
        style_function=lambda feature, color=row["color"]: {
            "fillColor": color,
            "color": "black",
            "weight": 0.5,
            "fillOpacity": 0.6
        },
        tooltip=f'{row["zip_prefix"]}
{row["PREF_NAME"]} {row["CITY_NAME"]} {row["S_NAME"] if row["S_NAME"] != None else ""}' ).add_to(m) # 地図を保存 m.save("interactive_map_tokyo.html")

6. 参考サイト

地図地理Sandbox
郵便番号データダウンロード
e-Stat 統計地理情報システムデータダウンロード
郵便番号の割り振りに規則性ってあるの? 7ケタでどこまでわかるの?