Python Folium Relearning(2) from May 2025(国土数値情報と総務省e-Statを活用した都道府県別Choropleth Mapの描画)

Foliumの再学習Relaerningの2回目はChoroplethMapからStartいたします。

まずはChoroplethMapについてGemini先生に聞いてみました。

**ChoroplethMap(コロプレスマップ)**とは、地理的な領域(国、都道府県、市町村などの行政区画や、特定の地域)を、その領域に関連する統計データの値に応じて色やパターンで塗り分けた地図のことです。

簡単に言うと、**「データによって色が変わる地図」**です。

主な特徴と目的:

  • 視覚的なデータの分布表示: 特定のデータ(人口密度、平均所得、感染者数、犯罪率など)が、地理的にどのように分布しているかを一目で視覚的に把握できます。
  • 地域間の比較: 各領域の色の濃淡や種類を見ることで、異なる地域間でデータの値がどのように異なるかを比較できます。
  • 傾向やパターンの特定: 地図上で色が変化する様子から、データの空間的な傾向やパターン(例:都市部に集中している、特定の沿岸地域で値が高いなど)を特定しやすくなります。
  • 階級区分: 統計データは通常、いくつかの階級(範囲)に分けられ、それぞれの階級に異なる色や濃淡が割り当てられます。
  • 相対値の表現に適している: 絶対量よりも、人口密度や一人当たりの収入など、面積や人口で正規化された**相対値(比率や平均)**を表現するのに特に適しています。これは、面積の大きい地域が絶対量で大きく見えてしまい、実態を誤解させる可能性があるためです。

例:

  • 都道府県別の人口密度を、人口密度が高いほど濃い青色で、低いほど薄い青色で塗り分けた地図。
  • 市町村別の世帯平均年収を、年収が高いほど緑色で、低いほど黄色で塗り分けた地図。

コロプレスマップは、政府機関の統計データや研究結果など、さまざまな分野で広く利用されている、非常に効果的なデータ可視化ツールです。

だそうです。ということで、日本統計局が提供しているe-StatのAPI機能を活用して、日本の都道府県別の人口の全人口に占める割合をDownLoadしてChoroplethMap表示にChallengeしようおと思います。

e-StatURL:https://www.e-stat.go.jp/

以前、e-StatのAPI機能を使って都道府県別の人口の全人口に占める割合についてMatPlotLibで表示する学習をしていたので、それを活用します。

ShikuuBlogURL:https://shikuuk.blogspot.com/2023/07/e-stat-with-python1-python.html

e-StatAPIから当該データをDownLoadするコードは以下のようになります。
import requests
import pandas as pd

APP_ID = '**********' #自分のe-Stat API IDを*********に入力
API_URL = "http://api.e-stat.go.jp/rest/3.0/app/json/getStatsData"

params = {
    "appId": APP_ID,
    "statsDataId": "0003448233", #人口推計-総務省-都道府県別人口の割合(総人口)のstatDataID
    "lang": "J",  # 日本語を指定
    "cdTime": "2021000000" #対象とする時期(年)を2021年指定
}

#人口統計
response = requests.get(API_URL, params=params)
# Process the response
data = response.json()
data

ここで「あれ?、APP_IDって何だっけ?」となりました。At firstにLoginIDを入力してみましたが、エラーが発生。Gemini先生に聞いてみたら以下のような手順で取得するとのことでした。
まず、e-StatにLoginして、マイページに移動します。

そして、API機能(アプリケーションIDの取得)をクリックして、APP_IDの発行操作をするとAPP_IDをGetできます。

前述のコードを実行すると以下のようなOutputがShowします。
{'GET_STATS_DATA': {'RESULT': {'STATUS': 0,
   'ERROR_MSG': '正常に終了しました。',
   'DATE': '2025-05-21T19:55:23.837+09:00'},
  'PARAMETER': {'LANG': 'J',
   'STATS_DATA_ID': '0003448233',
   'NARROWING_COND': {'CODE_TIME_SELECT': 2021000000},
   'DATA_FORMAT': 'J',
   'START_POSITION': 1,
   'METAGET_FLG': 'Y'},
  'STATISTICAL_DATA': {'RESULT_INF': {'TOTAL_NUMBER': 48,
    'FROM_NUMBER': 1,
    'TO_NUMBER': 48},
   'TABLE_INF': {'@id': '0003448233',
    'STAT_NAME': {'@code': '00200524', '$': '人口推計'},
    'GOV_ORG': {'@code': '00200', '$': '総務省'},
    'STATISTICS_NAME': '人口推計 各年10月1日現在人口 令和2年国勢調査基準 統計表',
    'TITLE': {'@no': '006', '$': '都道府県別人口の割合-総人口'},
    'CYCLE': '年次',
    'SURVEY_DATE': 0,
    'OPEN_DATE': '2025-04-14',
    'SMALL_AREA': 0,
    'COLLECT_AREA': '該当なし',
    'MAIN_CATEGORY': {'@code': '02', '$': '人口・世帯'},
    'SUB_CATEGORY': {'@code': '01', '$': '人口'},
    'OVERALL_TOTAL_NUMBER': 336,
    'UPDATED_DATE': '2025-04-14',
    'STATISTICS_NAME_SPEC': {'TABULATION_CATEGORY': '人口推計',
     'TABULATION_SUB_CATEGORY1': '各年10月1日現在人口',
     'TABULATION_SUB_CATEGORY2': '令和2年国勢調査基準',
     'TABULATION_SUB_CATEGORY3': '統計表'},
    'DESCRIPTION': '',
    'TITLE_SPEC': {'TABLE_NAME': '都道府県別人口の割合-総人口'}},
   'CLASS_INF': {'CLASS_OBJ': [{'@id': 'tab',
      '@name': '表章項目',
      'CLASS': {'@code': '003', '@name': '人口割合', '@level': '', '@unit': '%'}},
     {'@id': 'area',
      '@name': '全国・都道府県',
      'CLASS': [{'@code': '00000', '@name': '全国', '@level': '1'},
       {'@code': '01000', '@name': '北海道', '@level': '2'},
       {'@code': '02000', '@name': '青森県', '@level': '2'},
       {'@code': '03000', '@name': '岩手県', '@level': '2'},
       {'@code': '04000', '@name': '宮城県', '@level': '2'},
       {'@code': '05000', '@name': '秋田県', '@level': '2'},
       {'@code': '06000', '@name': '山形県', '@level': '2'},
       {'@code': '07000', '@name': '福島県', '@level': '2'},
       {'@code': '08000', '@name': '茨城県', '@level': '2'},
       {'@code': '09000', '@name': '栃木県', '@level': '2'},
       {'@code': '10000', '@name': '群馬県', '@level': '2'},
       {'@code': '11000', '@name': '埼玉県', '@level': '2'},
       {'@code': '12000', '@name': '千葉県', '@level': '2'},
       {'@code': '13000', '@name': '東京都', '@level': '2'},
       {'@code': '14000', '@name': '神奈川県', '@level': '2'},
       {'@code': '15000', '@name': '新潟県', '@level': '2'},
       {'@code': '16000', '@name': '富山県', '@level': '2'},
       {'@code': '17000', '@name': '石川県', '@level': '2'},
       {'@code': '18000', '@name': '福井県', '@level': '2'},
       {'@code': '19000', '@name': '山梨県', '@level': '2'},
       {'@code': '20000', '@name': '長野県', '@level': '2'},
       {'@code': '21000', '@name': '岐阜県', '@level': '2'},
       {'@code': '22000', '@name': '静岡県', '@level': '2'},
       {'@code': '23000', '@name': '愛知県', '@level': '2'},
       {'@code': '24000', '@name': '三重県', '@level': '2'},
       {'@code': '25000', '@name': '滋賀県', '@level': '2'},
       {'@code': '26000', '@name': '京都府', '@level': '2'},
       {'@code': '27000', '@name': '大阪府', '@level': '2'},
       {'@code': '28000', '@name': '兵庫県', '@level': '2'},
       {'@code': '29000', '@name': '奈良県', '@level': '2'},
       {'@code': '30000', '@name': '和歌山県', '@level': '2'},
       {'@code': '31000', '@name': '鳥取県', '@level': '2'},
       {'@code': '32000', '@name': '島根県', '@level': '2'},
       {'@code': '33000', '@name': '岡山県', '@level': '2'},
       {'@code': '34000', '@name': '広島県', '@level': '2'},
       {'@code': '35000', '@name': '山口県', '@level': '2'},
       {'@code': '36000', '@name': '徳島県', '@level': '2'},
       {'@code': '37000', '@name': '香川県', '@level': '2'},
       {'@code': '38000', '@name': '愛媛県', '@level': '2'},
       {'@code': '39000', '@name': '高知県', '@level': '2'},
       {'@code': '40000', '@name': '福岡県', '@level': '2'},
       {'@code': '41000', '@name': '佐賀県', '@level': '2'},
       {'@code': '42000', '@name': '長崎県', '@level': '2'},
       {'@code': '43000', '@name': '熊本県', '@level': '2'},
       {'@code': '44000', '@name': '大分県', '@level': '2'},
       {'@code': '45000', '@name': '宮崎県', '@level': '2'},
       {'@code': '46000', '@name': '鹿児島県', '@level': '2'},
       {'@code': '47000', '@name': '沖縄県', '@level': '2'}]},
     {'@id': 'time',
      '@name': '時間軸(年)',
      'CLASS': {'@code': '2021000000', '@name': '2021年', '@level': '1'}}]},
   'DATA_INF': {'NOTE': [{'@char': '***', '$': '該当数値がないもの'},
     {'@char': '-', '$': '該当数値がないもの\u3000'}],
    'VALUE': [{'@tab': '003',
      '@area': '00000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '100.00'},
     {'@tab': '003',
      '@area': '01000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '4.13'},
     {'@tab': '003',
      '@area': '02000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '0.97'},
     {'@tab': '003',
      '@area': '03000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '0.95'},
     {'@tab': '003',
      '@area': '04000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '1.82'},
     {'@tab': '003',
      '@area': '05000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '0.75'},
     {'@tab': '003',
      '@area': '06000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '0.84'},
     {'@tab': '003',
      '@area': '07000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '1.44'},
     {'@tab': '003',
      '@area': '08000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '2.27'},
     {'@tab': '003',
      '@area': '09000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '1.53'},
     {'@tab': '003',
      '@area': '10000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '1.54'},
     {'@tab': '003',
      '@area': '11000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '5.85'},
     {'@tab': '003',
      '@area': '12000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '5.00'},
     {'@tab': '003',
      '@area': '13000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '11.16'},
     {'@tab': '003',
      '@area': '14000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '7.36'},
     {'@tab': '003',
      '@area': '15000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '1.73'},
     {'@tab': '003',
      '@area': '16000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '0.82'},
     {'@tab': '003',
      '@area': '17000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '0.90'},
     {'@tab': '003',
      '@area': '18000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '0.61'},
     {'@tab': '003',
      '@area': '19000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '0.64'},
     {'@tab': '003',
      '@area': '20000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '1.62'},
     {'@tab': '003',
      '@area': '21000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '1.56'},
     {'@tab': '003',
      '@area': '22000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '2.87'},
     {'@tab': '003',
      '@area': '23000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '5.99'},
     {'@tab': '003',
      '@area': '24000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '1.40'},
     {'@tab': '003',
      '@area': '25000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '1.12'},
     {'@tab': '003',
      '@area': '26000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '2.04'},
     {'@tab': '003',
      '@area': '27000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '7.02'},
     {'@tab': '003',
      '@area': '28000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '4.33'},
     {'@tab': '003',
      '@area': '29000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '1.05'},
     {'@tab': '003',
      '@area': '30000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '0.73'},
     {'@tab': '003',
      '@area': '31000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '0.44'},
     {'@tab': '003',
      '@area': '32000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '0.53'},
     {'@tab': '003',
      '@area': '33000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '1.50'},
     {'@tab': '003',
      '@area': '34000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '2.21'},
     {'@tab': '003',
      '@area': '35000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '1.06'},
     {'@tab': '003',
      '@area': '36000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '0.57'},
     {'@tab': '003',
      '@area': '37000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '0.75'},
     {'@tab': '003',
      '@area': '38000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '1.05'},
     {'@tab': '003',
      '@area': '39000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '0.55'},
     {'@tab': '003',
      '@area': '40000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '4.08'},
     {'@tab': '003',
      '@area': '41000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '0.64'},
     {'@tab': '003',
      '@area': '42000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '1.03'},
     {'@tab': '003',
      '@area': '43000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '1.38'},
     {'@tab': '003',
      '@area': '44000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '0.89'},
     {'@tab': '003',
      '@area': '45000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '0.85'},
     {'@tab': '003',
      '@area': '46000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '1.26'},
     {'@tab': '003',
      '@area': '47000',
      '@time': '2021000000',
      '@unit': '%',
      '$': '1.17'}]}}}}
何やらよくわからないのでGemini先生に整理の方法を聞きました。e-StatAPIのDataはJSONTypeというDataなので、これをPadasのDataFrameに変換してあげるそうです。
#JSONデータをPandasのDataFrameに変換
df = pd.DataFrame(data['GET_STATS_DATA']['STATISTICAL_DATA']['DATA_INF']['VALUE'])

# Pandas dataの加工
# Column名の変更
column_rename = {
    '@tab': 'タブ',
    '@area': '都道府県コード',
    '@time': '調査年',
    '@unit': '単位',
    '$': '人口比(対全国人口)'
}

df.rename(columns=column_rename, inplace=True)

df.head()
OutputはBelowです。
タブ都道府県コード調査年単位人口比(対全国人口)
0003000002021000000100.00
10030100020210000004.13
20030200020210000000.97
30030300020210000000.95
40030400020210000001.82

それらしい表になりました。おそらく都道府県コードというのが都道府県を表しているので、このコードを使ってPandasDataFrameに都道府県の名称を追加します。これもGemini先生に聞きましたが、都道府県コードと都道府県名の対応辞書を自動で作成してくれるなど、本当に便利です。
# 都道府県コードと都道府県名の対応辞書
pref_code_to_name = {
    '01000': '北海道', '02000': '青森県', '03000': '岩手県', '04000': '宮城県', '05000': '秋田県',
    '06000': '山形県', '07000': '福島県', '08000': '茨城県', '09000': '栃木県', '10000': '群馬県',
    '11000': '埼玉県', '12000': '千葉県', '13000': '東京都', '14000': '神奈川県', '15000': '新潟県',
    '16000': '富山県', '17000': '石川県', '18000': '福井県', '19000': '山梨県', '20000': '長野県',
    '21000': '岐阜県', '22000': '静岡県', '23000': '愛知県', '24000': '三重県', '25000': '滋賀県',
    '26000': '京都府', '27000': '大阪府', '28000': '兵庫県', '29000': '奈良県', '30000': '和歌山県',
    '31000': '鳥取県', '32000': '島根県', '33000': '岡山県', '34000': '広島県', '35000': '山口県',
    '36000': '徳島県', '37000': '香川県', '38000': '愛媛県', '39000': '高知県', '40000': '福岡県',
    '41000': '佐賀県', '42000': '長崎県', '43000': '熊本県', '44000': '大分県', '45000': '宮崎県',
    '46000': '鹿児島県', '47000': '沖縄県', '00000': '全国'
}

# 都道府県コードに基づいて都道府県名を追加する新しい列を作成
df['N03_001'] = df['都道府県コード'].map(pref_code_to_name)

df.head()
OutPutはBelowです。
タブ都道府県コード調査年単位人口比(対全国人口)N03_001
0003000002021000000100.00全国
10030100020210000004.13北海道
20030200020210000000.97青森県
30030300020210000000.95岩手県
40030400020210000001.82宮城県
次に「PythonのFoliumで、GeoPandasDataFrameに格納された都道府県名称と都道府県の輪郭に関するMultiPolygonと、PandasDataFrameに格納された都道府県名称と日本の全人口に対する各都道府県の人口の比率(%)を組み合わせて、コロプレス地図を作成するコードを教えて」と、Gemini先生に教えてもらいました。
# --- 2. GeoDataFrameとDataFrameの結合 ---
# GeoDataFrameとDataFrameを都道府県名で結合します。
# これにより、各都道府県のポリゴンデータに人口比率の情報を追加できます。
prefecture_gdf_merged = prefecture_gdf.merge(df, on='N03_001', how='left')

prefecture_gdf_merged.head()
OutputはBelowです。
N03_001geometrysimplified_geometryタブ都道府県コード調査年単位人口比(対全国人口)
0三重県MULTIPOLYGON (((136.02156 33.7343, 136.02154 3...MULTIPOLYGON (((136.02158 33.73431, 136.02153 ...0032400020210000001.40
1京都府MULTIPOLYGON (((134.89358 35.64858, 134.89358 ...MULTIPOLYGON (((134.89369 35.64859, 134.89358 ...0032600020210000002.04
2佐賀県MULTIPOLYGON (((129.78926 33.44452, 129.7893 3...MULTIPOLYGON (((129.78922 33.4445, 129.7893 33...0034100020210000000.64
3兵庫県MULTIPOLYGON (((134.38287 34.71978, 134.38288 ...MULTIPOLYGON (((134.38286 34.71979, 134.38287 ...0032800020210000004.33
4北海道MULTIPOLYGON (((139.34092 41.49493, 139.34094 ...MULTIPOLYGON (((139.34074 41.49497, 139.34089 ...0030100020210000004.13

GeoPandasのGeoDataFrameに人口比(対全国人口)の列が追加されました。
ここでいくつかエラーが出たのですが、エラー対応の途中過程を残すことを失念していたので、結論だけを残しておきます。この「人口比(対全国人口)」の値のTypeを確認すると以下のようになります。
print(prefecture_gdf_merged.info())
Outputです。
<class 'geopandas.geodataframe.GeoDataFrame'> RangeIndex: 47 entries, 0 to 46 Data columns (total 8 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 N03_001 47 non-null object 1 geometry 47 non-null geometry 2 simplified_geometry 47 non-null geometry 3 タブ 47 non-null object 4 都道府県コード 47 non-null object 5 調査年 47 non-null object 6 単位 47 non-null object 7 人口比(対全国人口) 47 non-null object dtypes: geometry(2), object(6) memory usage: 3.1+ KB None
「7   人口比(対全国人口)           47 non-null     object 」この表示が曲者で、この値がintTypeやFloatTypeなどの数値型ではないということを表現しています。ここが数値型でないと、コードがうまく動きません。なので、この値を数値型に変換します。
# '人口比(対全国人口)' カラムを数値型に変換
# errors='coerce' を指定することで、もし変換できない値があってもエラーにならずNaNになる
prefecture_gdf_merged['人口比(対全国人口)'] = pd.to_numeric(
    prefecture_gdf_merged['人口比(対全国人口)'], errors='coerce'
)

# 念のため、変換後のデータ型を確認
print(prefecture_gdf_merged['人口比(対全国人口)'].dtype)
# ここで float64 などになっていれば成功です。

Outputは「float64」となり、数値型に変換できました。

都道府県のGISデータについては前回の再学習(第1回)をそのまま利用します。

Dataが整ったので、図化作業に入りましょう。FoliumのChoroplethMap作図コードです。試行錯誤はありますが、こんな感じのコードです。
<<これはError Codeです>>
import folium
import geopandas as gpd # geopandasがインポートされていることを想定

# --- (prefecture_gdf_merged をロードして結合する以前のコード) ---
# デモンストレーションのため、prefecture_gdf_merged が既にロードされていると仮定します。
# もしprefecture_gdf_mergedが定義されていない場合は、まずロードする必要があります。
# 例:
# prefecture_gdf = gpd.read_file("path/to/your/N03-230101_GML.gml")
# population_data = pd.read_csv("path/to/your/population_data.csv") # 人口データがあると仮定
# prefecture_gdf_merged = prefecture_gdf.merge(population_data, on='N03_001', how='left')
# prefecture_gdf_merged['人口比(対全国人口)'] = (prefecture_gdf_merged['人口'] / prefecture_gdf_merged['全国人口'].iloc[0]) * 100


# --- CRSの設定と再投影 ---
# 現在のCRSを確認(オプション、デバッグ用)
print(f"Original CRS: {prefecture_gdf_merged.crs}")

# CRSが設定されていないか、正しくない場合に設定します。
# 一般的なシナリオ:元のGMLでCRSが指定されていないか、「ナイーブ」な場合。
# 元のデータがJGD2011 (EPSG:6668) であると確信している場合:
if prefecture_gdf_merged.crs is None:
    prefecture_gdf_merged.set_crs(epsg=6668, inplace=True) # JGD2011に設定

# WGS84 (EPSG:4326) にまだない場合は、再投影します。
# Foliumの地図は通常WGS84です。
if prefecture_gdf_merged.crs != 'EPSG:4326':
    prefecture_gdf_merged = prefecture_gdf_merged.to_crs(epsg=4326)
    print(f"Reprojected CRS: {prefecture_gdf_merged.crs}")



# --- 3. Folium地図の作成 ---
# 地図の中心座標を日本の中心付近に設定
m = folium.Map(location=[36.2048, 138.2529], zoom_start=6)

# コロプレスレイヤーの追加
folium.Choropleth(
    geo_data=prefecture_gdf_merged, # アクティブなジオメトリが切り替わったGeoDataFrameを使用
    name='人口比(対全国人口)',
    data=prefecture_gdf_merged, # 結合済みのGeoDataFrameを使用
    columns=['N03_001', '人口比(対全国人口)'],
    key_on='feature.properties.N03_001',
    fill_color='YlGnBu',
    fill_opacity=0.7,
    line_opacity=0.2,
    legend_name='日本の全人口に対する都道府県人口比率 (%)',
    highlight=True
).add_to(m)

# ツールチップの追加
style_function = lambda x: {'fillColor': '#ffffff', 'color':'#000000', 'fillOpacity': 0.0, 'weight': 0.0}
highlight_function = lambda x: {'fillColor': '#000000', 'color':'#000000', 'fillOpacity': 0.50, 'weight': 0.1}
NIL = folium.features.GeoJson(
    prefecture_gdf_merged, # アクティブなジオメトリが切り替わったGeoDataFrameを使用
    style_function=style_function,
    control=False,
    highlight_function=highlight_function,
    tooltip=folium.features.GeoJsonTooltip(
        fields=['N03_001', '人口比(対全国人口)'],
        aliases=['N03_001:', '人口比率 (%):'],
        localize=True,
        sticky=False,
        labels=True,
        style="""
            background-color: #F0F0F0;
            color: #333333;
            font-family: arial;
            font-size: 12px;
            padding: 10px;
        """
    )
)
m.add_child(NIL)
m.keep_in_front(NIL)

# レイヤーコントロールの追加
folium.LayerControl().add_to(m)

m
しかし、Gemini先生でも解決できないこのエラーメッセージに長時間悩まされました。

Original CRS: None
Reprojected CRS: EPSG:4326
Gemini先生とも相談しましたが決定的な解決策がないままErrorMassegeとにらめっこしながら悩み続けた結果、もしかしてMultiPolygonをJSON型に変換できていないのではないかという疑いを持ちました。そこでGISデータをイチから作り直すことにしました。

まずは、国土数値情報から行政区域の情報をDownloadしてGeopandasDataFrameに整理します。(第1回のコードと同じ)
import urllib.request
import zipfile
import geopandas as gpd

def download_map_data(url, zip_file_name, shp_file_name):
    zip_file_path = 'unzipped_data/' + zip_file_name

# URLからデータをダウンロード
    urllib.request.urlretrieve(url, zip_file_name)

# ZIPファイルを解凍
    with zipfile.ZipFile(zip_file_name, 'r') as zip_ref:
        zip_ref.extractall('unzipped_data')  # 解凍先のディレクトリを指定

# Shapefileを読み込む
#shapefile_path = 'unzipped_data/W05-06_36-g_Stream.shp'
    shp_file_path = 'unzipped_data/' + shp_file_name
    gdf = gpd.read_file(shp_file_path)

    return gdf

# データフレームを格納するリスト
gdf_shikoku_river = gpd.GeoDataFrame()

# ダウンロードとデータ読み込みのループ
url = 'http://nlftp.mlit.go.jp/ksj/gml/data/N03/N03-2024/N03-20240101_GML.zip'
zip_file_name = 'N03-20240101_GML.zip'
shp_file_name = 'N03-20240101.shp'

# データをダウンロードして読み込む
gdf = download_map_data(url, zip_file_name, shp_file_name)

# データを表示
gdf
以下、Outputです。
N03_001N03_002N03_003N03_004N03_005N03_007geometry
0北海道石狩振興局None札幌市中央区01101POLYGON ((141.25694 42.99782, 141.2563 42.9978...
1北海道石狩振興局None札幌市北区01102POLYGON ((141.33325 43.07505, 141.3326 43.0757...
2北海道石狩振興局None札幌市東区01103POLYGON ((141.37341 43.0684, 141.37333 43.0684...
3北海道石狩振興局None札幌市白石区01104POLYGON ((141.38202 43.04832, 141.38193 43.048...
4北海道石狩振興局None札幌市豊平区01105POLYGON ((141.36371 42.94154, 141.36378 42.941...
........................
124128沖縄県None八重山郡与那国町None47382POLYGON ((122.93299 24.4517, 122.93299 24.4517...
124129沖縄県None八重山郡与那国町None47382POLYGON ((122.93396 24.4482, 122.93396 24.4482...
124130沖縄県None八重山郡与那国町None47382POLYGON ((122.94213 24.44395, 122.94214 24.443...
124131沖縄県None八重山郡与那国町None47382POLYGON ((122.99876 24.43985, 122.99875 24.439...
124132沖縄県None八重山郡与那国町None47382POLYGON ((122.93308 24.45176, 122.93308 24.451...

124133 rows × 7 columns


一度、このGDFをFoliumで図化してみます。
import pandas as pd
import geopandas as gpd
from shapely.geometry import Polygon
import folium

m = folium.Map(location=[35.6895, 139.6917], zoom_start=5, tiles="OpenStreetMap") # 東京の座標を例に

# GeoDataFrameのジオメトリをFoliumマップに追加
# GeoJsonはGeoPandasのGeoDataFrameを直接受け入れます
folium.GeoJson(
    gdf,
    name='自治体境界', # レイヤーの名前
    # スタイル関数でポリゴンの色、線の太さ、透過度などを設定
    style_function=lambda x: {
        'fillColor': '#4daf4a', # 緑色
        'color': 'black',
        'weight': 0.5,
        'fillOpacity': 0.7
    },
    # ツールチップでDataFrameの情報を表示
    # ユーザーがカーソルを合わせたときに表示したい列を指定
    tooltip=folium.features.GeoJsonTooltip(
        fields=['N03_001', 'N03_002', 'N03_003', 'N03_005'],
        aliases=['都道府県名', '支庁・振興局名', '市区町村名', '市区町村コード'],
        localize=True
    )
).add_to(m)

# レイヤーコントロールを追加(複数のレイヤーがある場合に表示/非表示を切り替えるのに便利)
folium.LayerControl().add_to(m)

# マップを表示
m
このコードではエラーは出ませんが、Mapが表示されません。おそらくデータが多すぎてメモリが不足しているものと想像します。

▼△▼△▼△▼△▼△▼△▼△▼△▼△▼△▼△▼△▼△▼△▼△▼△▼△▼△
ということで、ここまでのコードを一度白紙に戻して、コードを作り直してみました。
国土数値情報(https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N03-2024.html)から行政区域に関するInformationをGetします。
import urllib.request
import zipfile
import geopandas as gpd

def download_map_data(url, zip_file_name, shp_file_name):
    zip_file_path = 'unzipped_data/' + zip_file_name

# URLからデータをダウンロード
    urllib.request.urlretrieve(url, zip_file_name)

# ZIPファイルを解凍
    with zipfile.ZipFile(zip_file_name, 'r') as zip_ref:
        zip_ref.extractall('unzipped_data')  # 解凍先のディレクトリを指定

# Shapefileを読み込む
#shapefile_path = 'unzipped_data/W05-06_36-g_Stream.shp'
    shp_file_path = 'unzipped_data/' + shp_file_name
    gdf = gpd.read_file(shp_file_path)

    return gdf

# ダウンロードとデータ読み込みのループ
url = 'http://nlftp.mlit.go.jp/ksj/gml/data/N03/N03-2024/N03-20240101_GML.zip'
zip_file_name = 'N03-20240101_GML.zip'
shp_file_name = 'N03-20240101.shp'

# データをダウンロードして読み込む
gdf = download_map_data(url, zip_file_name, shp_file_name)

# データを表示
gdf
OutputはBelowです。
N03_001N03_002N03_003N03_004N03_005N03_007geometry
0北海道石狩振興局None札幌市中央区01101POLYGON ((141.25694 42.99782, 141.2563 42.9978...
1北海道石狩振興局None札幌市北区01102POLYGON ((141.33325 43.07505, 141.3326 43.0757...
2北海道石狩振興局None札幌市東区01103POLYGON ((141.37341 43.0684, 141.37333 43.0684...
3北海道石狩振興局None札幌市白石区01104POLYGON ((141.38202 43.04832, 141.38193 43.048...
4北海道石狩振興局None札幌市豊平区01105POLYGON ((141.36371 42.94154, 141.36378 42.941...
........................
124128沖縄県None八重山郡与那国町None47382POLYGON ((122.93299 24.4517, 122.93299 24.4517...
124129沖縄県None八重山郡与那国町None47382POLYGON ((122.93396 24.4482, 122.93396 24.4482...
124130沖縄県None八重山郡与那国町None47382POLYGON ((122.94213 24.44395, 122.94214 24.443...
124131沖縄県None八重山郡与那国町None47382POLYGON ((122.99876 24.43985, 122.99875 24.439...
124132沖縄県None八重山郡与那国町None47382POLYGON ((122.93308 24.45176, 122.93308 24.451...

124133 rows × 7 columns

問題なしです。念のため座標系を確認します。
print(gdf.crs)
OutputはBelowです。
EPSG:6668

次に、ChoroplethMapwp作成する前に、都道府県ごとに色分けして日本地図を塗りつぶしたいと思い、Gemini先生に次のような質問をしました。

「16進数のカラーコードを使って47種類の色を出すにはどうしたらいいですか?」

47種類もの「視覚的に識別しやすい」色を生成するのは、一筋縄ではいきません。単にランダムに生成するだけでは、似たような色が多くなってしまい、区別がつきにくくなる可能性が高いです。

しかし、Pythonにはこれを助けるためのライブラリや手法がいくつか存在します。

1. distinctipy ライブラリを使う (推奨)

distinctipy は、その名の通り「視覚的に区別しやすい色」を生成するために設計されたPythonライブラリです。指定した数だけ、最も異なる色を生成しようと試みます。

まず、インストールします。

Bash
pip install distinctipy

次に、Pythonコードで色を生成します。

Python
import distinctipy

# 生成したい色の数
num_colors = 47

# 視覚的に区別しやすい色をN個生成
# return_hex=True を指定すると、直接16進数カラーコードのリストが返されます
colors_hex = distinctipy.get_colors(num_colors, return_hex=True)

print(f"{num_colors}個の識別しやすいカラーコード:")
for i, hex_color in enumerate(colors_hex):
    print(f"Color {i+1}: {hex_color}")

# (オプション) 生成された色を視覚的に確認する
# matplotlib と pandas が必要です (pip install distinctipy[extras] でインストールできます)
# import matplotlib.pyplot as plt
# import pandas as pd
# distinctipy.color_swatch(colors_hex)
# plt.show()

このライブラリは、色空間(HSL、Labなど)を考慮して、色相、彩度、明度を調整しながら、既存の色から最も遠い色を見つけ出すアルゴリズムを使用しています。

2. HSL/HSV色空間で色相を均等に分割する方法

RGB色空間でランダムに色を生成すると似た色が出やすいですが、HSL(Hue, Saturation, Lightness)やHSV(Hue, Saturation, Value)色空間は、人間の色の知覚に近い形で色を表現するため、視覚的に区別しやすい色を生成するのに役立ちます。

特に「色相 (Hue)」を均等に分割することで、多様な色を効率的に生成できます。

Python
import colorsys

def get_distinct_colors_hsl(n_colors, saturation=0.7, lightness=0.5):
    """
    HSL色空間を使ってN個の識別しやすい色を生成し、16進数カラーコードで返す。
    """
    hex_colors = []
    for i in range(n_colors):
        # 色相 (hue) を均等に分割
        hue = i / n_colors
        
        # HSLをRGBに変換 (0.0-1.0の範囲)
        r, g, b = colorsys.hls_to_rgb(hue, lightness, saturation)
        
        # RGBを0-255の整数に変換
        r_int = int(r * 255)
        g_int = int(g * 255)
        b_int = int(b * 255)
        
        # 16進数カラーコードに変換
        hex_color = f"#{r_int:02x}{g_int:02x}{b_int:02x}"
        hex_colors.append(hex_color)
    return hex_colors

# 47種類の色を生成
num_colors = 47
colors_hsl = get_distinct_colors_hsl(num_colors)

print(f"{num_colors}個のHSLベースのカラーコード:")
for i, hex_color in enumerate(colors_hsl):
    print(f"Color {i+1}: {hex_color}")

# saturation(彩度)とlightness(明度)を調整して、色のバリエーションを増やすこともできます。
# 例えば、よりパステル調の色にするには lightness を上げたり、saturation を下げたりします。
# colors_hsl_pastel = get_distinct_colors_hsl(num_colors, saturation=0.5, lightness=0.8)

この方法の利点はシンプルさですが、色の数が多くなると、隣接する色相の色が似て見えたり、明度や彩度が固定されるためバリエーションが限られる可能性があります。

3. Matplotlibのカラーマップを利用する

matplotlibはデータ可視化ライブラリですが、多くの優れたカラーマップ(色の連続的なグラデーション)を提供しています。これらのカラーマップから指定した数の色を抽出し、16進数に変換することができます。

Python
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import numpy as np

def get_matplotlib_colors(n_colors, colormap_name='viridis'):
    """
    MatplotlibのカラーマップからN個の色を抽出し、16進数カラーコードで返す。
    """
    hex_colors = []
    # カラーマップを取得
    colormap = cm.get_cmap(colormap_name)
    
    for i in range(n_colors):
        # 0.0から1.0の範囲で均等に値をサンプリング
        rgba_color = colormap(i / (n_colors - 1)) # n_colors-1 で最後の色まで含める
        
        # RGBA (0.0-1.0) を RGB (0-255) に変換し、16進数に
        r_int = int(rgba_color[0] * 255)
        g_int = int(rgba_color[1] * 255)
        b_int = int(rgba_color[2] * 255)
        
        hex_color = f"#{r_int:02x}{g_int:02x}{b_int:02x}"
        hex_colors.append(hex_color)
    return hex_colors

# 47種類の色を生成('viridis'カラーマップを使用)
num_colors = 47
colors_mpl = get_matplotlib_colors(num_colors, colormap_name='viridis')

print(f"{num_colors}個のMatplotlibベースのカラーコード ('viridis'より):")
for i, hex_color in enumerate(colors_mpl):
    print(f"Color {i+1}: {hex_color}")

# 他のカラーマップも試してみる (例: 'plasma', 'cividis', 'tab20', 'Dark2'など)
# 'tab20' は20色しかないので、47色には不向きです。連続的なカラーマップが適しています。
# colors_mpl_plasma = get_matplotlib_colors(num_colors, colormap_name='plasma')

Matplotlibのカラーマップは、科学的なデータ可視化のために色の識別性や色覚異常への配慮がされているものも多く、有効な選択肢です。

どの方法を選ぶべきか?

  • distinctipy: 最も推奨されます。視覚的な識別性を最適化するよう設計されているため、色の数が多くても比較的区別しやすい色が得られます。
  • HSL/HSVベース: 実装が非常にシンプルで、基本的な要件を満たすなら十分です。ただし、色の数が多くなると、隣接する色が似て見えることがあります。
  • Matplotlibカラーマップ: 既存の高品質なカラーマップを利用できるため、特定のグラデーションやテーマに沿った色が必要な場合に便利です。

47種類もの色を完璧に「識別しやすい」ものにするのは、人間にとって困難なタスクですが、これらの方法を使えば、ある程度の識別性を確保したカラーコードのリストを生成できます。


「1. distinctipy ライブラリを使う (推奨)」を選択しましたが、エラーが発生。単純な内容で、以下を追加したら、無事実行できました。
pip install --upgrade distinctipy
pip install --upgrade distinctipy

import distinctipy

# 生成したい色の数
num_colors = 47

# 視覚的に区別しやすい色をN個生成 (RGB値のタプルで返される)
# return_hex 引数を削除
colors_rgb_float = distinctipy.get_colors(num_colors)

# RGB (0.0-1.0) を16進数カラーコードに変換する関数
def rgb_to_hex(rgb_float_tuple):
    r_int = int(rgb_float_tuple[0] * 255)
    g_int = int(rgb_float_tuple[1] * 255)
    b_int = int(rgb_float_tuple[2] * 255)
    return f"#{r_int:02x}{g_int:02x}{b_int:02x}"

colors_hex = [rgb_to_hex(c) for c in colors_rgb_float]

print(f"{num_colors}個の識別しやすいカラーコード:")
for i, hex_color in enumerate(colors_hex):
    print(f"Color {i+1}: {hex_color}")

# (オプション) 生成された色を視覚的に確認する
distinctipy.color_swatch(colors_hex)
import matplotlib.pyplot as plt
plt.show()
OutputはBelowで、これはとっても参考になりました。今後も使えるような気がします。
47個の識別しやすいカラーコード:
Color 1: #00ff00
Color 2: #ff00ff
Color 3: #007fff
Color 4: #ff7f00
Color 5: #7fbf7f
Color 6: #5e0ba4
Color 7: #cb0119
Color 8: #398002
Color 9: #cf7efa
Color 10: #00ffff
Color 11: #ffff00
Color 12: #00ff7f
Color 13: #007f7f
Color 14: #b45074
Color 15: #fcb687
Color 16: #0000ff
Color 17: #8dea08
Color 18: #7bd6fd
Color 19: #1a3153
Color 20: #6368be
Color 21: #ff007f
Color 22: #8c2cfe
Color 23: #c8fc7a
Color 24: #78340d
Color 25: #a09d1f
Color 26: #10bdc0
Color 27: #0039bf
Color 28: #2ecb41
Color 29: #60674f
Color 30: #f54bb8
Color 31: #4efec0
Color 32: #850453
Color 33: #f0412e
Color 34: #b70ab0
Color 35: #62fd65
Color 36: #ddcb34
Color 37: #bbbfc0
Color 38: #00007f
Color 39: #3948fc
Color 40: #f57573
Color 41: #6594fe
Color 42: #a18a9e
Color 43: #fdb0f0
Color 44: #02520e
Color 45: #00ab0e
Color 46: #b76602
Color 47: #5204f7
この色彩を47都道府県に割り付けます。このコードもGemini先生に教えてもらいました。
import distinctipy

# 1. 47都道府県のリスト (一般的な順序)
prefectures = [
    "北海道", "青森県", "岩手県", "宮城県", "秋田県", "山形県", "福島県",
    "茨城県", "栃木県", "群馬県", "埼玉県", "千葉県", "東京都", "神奈川県",
    "新潟県", "富山県", "石川県", "福井県", "山梨県", "長野県", "岐阜県",
    "静岡県", "愛知県", "三重県", "滋賀県", "京都府", "大阪府", "兵庫県",
    "奈良県", "和歌山県", "鳥取県", "島根県", "岡山県", "広島県", "山口県",
    "徳島県", "香川県", "愛媛県", "高知県", "福岡県", "佐賀県", "長崎県",
    "熊本県", "大分県", "宮崎県", "鹿児島県", "沖縄県"
]

# 都道府県の数が47であることを確認
if len(prefectures) != 47:
    print(f"エラー: 都道府県の数が47ではありません。現在の数: {len(prefectures)}")
    exit()

# 2. distinctipy を使って47種類の識別しやすい色を生成
num_colors = 47

# distinctipy.get_colors() の戻り値を手動で16進数に変換する
# (TypeError: get_colors() got an unexpected keyword argument 'return_hex' 対策)
colors_rgb_float = distinctipy.get_colors(num_colors)

def rgb_to_hex(rgb_float_tuple):
    r_int = int(rgb_float_tuple[0] * 255)
    g_int = int(rgb_float_tuple[1] * 255)
    b_int = int(rgb_float_tuple[2] * 255)
    return f"#{r_int:02x}{g_int:02x}{b_int:02x}"

colors_hex = [rgb_to_hex(c) for c in colors_rgb_float]

# 3. 都道府県名と色を割り振る辞書を作成
prefecture_colors = {}
for i in range(num_colors):
    prefecture_colors[prefectures[i]] = colors_hex[i]

# 4. 結果を表示 (一部のみ表示)
print("生成された都道府県と色の割り当て辞書:")
count = 0
for pref, color in prefecture_colors.items():
    print(f"'{pref}': '{color}'")
    count += 1
    if count >= 10: # 最初と最後の数件のみ表示
        print("...")
        break

print("\n(辞書の全体は 'prefecture_colors' 変数に格納されています)")
print(f"辞書の要素数: {len(prefecture_colors)}")

# 辞書の全体を表示したい場合は、以下のコメントを外してください
# import pprint
# pprint.pprint(prefecture_colors)
OutputはBelowでございます。
生成された都道府県と色の割り当て辞書: '北海道': '#00ff00' '青森県': '#fa00df' '岩手県': '#007fff' '宮城県': '#f99308' '秋田県': '#43feac' '山形県': '#7a4975' '福島県': '#ff0000' '茨城県': '#0000ff' '栃木県': '#a68ef7' '群馬県': '#007f00' ... (辞書の全体は 'prefecture_colors' 変数に格納されています) 辞書の要素数: 47

これで地図側、GIS側の準備は完了したような気がします。ということで、ちょっとTestのため一部の都道府県のみ塗りつぶししてみます。
import folium

# 地図の中心となる緯度経度を設定
center_lat = 35.6895
center_lon = 139.6917

# Foliumの地図オブジェクトを作成
m = folium.Map(location=[center_lat, center_lon], zoom_start=4, tiles="cartodbpositron")
gdf.to_crs(epsg=4326) # 座標系をEPGS=6668からEPGS=4326に変更

def func_creat_map(row, color, name):
    geom = gpd.GeoSeries(row['geometry']).simplify(tolerance=0.01)
    geojson_geom = geom.to_json()

    folium.GeoJson(
        geojson_geom,
        style_function=lambda feature: {
            'fillColor': color,
            'color': color,
            'weight': 0, # 線の太さをゼロに設定
            'fillOpacity': 0.6,
        },
        tooltip = name
    ).add_to(m)

# 簡略化した形状をGeoJSON形式で追加
for index, row in gdf.iterrows():
    if row['N03_001'] == '千葉県': #例えば、千葉県
        color = prefecture_colors[row['N03_001']]
        func_creat_map(row, color, row['N03_001'])

    if row['N03_001'] == '青森県': #例えば、青森県
        color = prefecture_colors[row['N03_001']]
        func_creat_map(row, color, row['N03_001'])

    if row['N03_001'] == '熊本県': #例えば、熊本県
        color = prefecture_colors[row['N03_001']]
        func_creat_map(row, color, row['N03_001'])

# 地図を表示
m
OutputはBelowです。


Make this Notebook Trusted to load map: File -> Trust Notebook


「simplify(tolerance=0.01)」だと、塗りつぶしがちょっと粗目にはなっていますが、そこは根本的なことではないので、目をつぶってください。次に全国バージョンの都道府県別の色分け地図を作ろうと考えました。そのためのコードはBelowです。
import folium

# 地図の中心となる緯度経度を設定
center_lat = 35.6895
center_lon = 139.6917

# Foliumの地図オブジェクトを作成
m = folium.Map(location=[center_lat, center_lon], zoom_start=4, tiles="cartodbpositron")
gdf.to_crs(epsg=4326) # 座標系をEPGS=6668からEPGS=4326に変更

def func_creat_map(row, color, name):
    geom = gpd.GeoSeries(row['geometry']).simplify(tolerance=0.01)
    geojson_geom = geom.to_json()

    folium.GeoJson(
        geojson_geom,
        style_function=lambda feature: {
            'fillColor': color,
            'color': color,
            'weight': 0, # 線の太さをゼロに設定
            'fillOpacity': 0.6,
        },
        tooltip = name
    ).add_to(m)

# 簡略化した形状をGeoJSON形式で追加
for index, row in gdf.iterrows():
    color = prefecture_colors[row['N03_001']]
    func_creat_map(row, color, row['N03_001'])

# 地図を表示
m
長時間の処理後に以下のメッセージが出て、GISデータについてsimplify(tolerance=0.01)で単純化を図っても、NomalなGoogleColabではBufferがいっぱいになって描画できませんでした。
Buffered data was truncated after reaching the output size limit.








コメント