都道府県ごとの人口推移をアニメーション化する

この記事は ESM Advent Calendar 2024 の24日目の記事です。

https://adventar.org/calendars/10806

## はじめに

ちょっと間が空いてしまいましたが、[前回](https://qiita.com/a-nagaoka/items/a7a0a47820ae4881b9e3)の記事の続きです。
今回は、都道府県ごとの各年の人口をオープンデータから取得し、日本地図上に描画する方法についてまとめていきます。

## APIで人口データを取得する

### 1. 使用したAPI

人口データは、日本の政府統計をオープンデータとして公開している[e-Stat](https://www.e-stat.go.jp/)から、APIを使用して取得しました。今回使用したAPIのURLです。
```
https://api.e-stat.go.jp/rest/3.0/app/json/getStatsData?statsDataId=0000010101&cdCat01=A1101&appId={アプリケーションID}
```
```https://api.e-stat.go.jp/rest/3.0/app/json/getStatsData```が統計データをJSON形式で取得するためのエンドポイントで、それ以降に取得したい統計データのパラメータを指定します。今回使用したパラメータは以下の通りです。
- **statsDataId**: 対象の統計表を指定するためのID。「人口・世帯」の統計表IDは```0000010101```
- **cdCat01**: 分類事項のコード。「総人口」の場合は```A1101```
- **appId**: [e-Stat](https://www.e-stat.go.jp/)のページで発行したアプリケーションID。発行にはユーザ登録が必要です。詳細はe-Statの[利用ガイド](https://www.e-stat.go.jp/api/api-info/api-guide)を参照。

他にも時間や地域など、色々なパラメータがあります。詳細な情報は、以下の公式ドキュメントをご参照ください。
- [API仕様](https://www.e-stat.go.jp/api/api-info/e-stat-manual3-0)
- [APIの使い方](https://www.e-stat.go.jp/api/api-dev/how_to_use)

### 2. データ取得に必要なパラメータの探し方

[e-Stat](https://www.e-stat.go.jp/)では様々な統計データがAPIで公開されています。以下は、[「統計データを探す」のページ](https://www.e-stat.go.jp/stat-search)から目的のデータを得るパラメータを見つける方法です。

1. データ種別で「データベース」を選択し、API対応のデータのみに絞り込みます。
1. 絞り込み条件（キーワードや検索オプション）を入力し、検索ボタンをクリックします。
1. 検索結果一覧で、「API」ボタンをクリックするとAPIのURLが表示されます。このURLで、対象データを取得するためのパラメータを確認できます。また「DB」ボタンをクリックすると、対象データの内容を表形式で確認できます。

![2024-12-04_11h44_22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919085/2fb00e51-cb25-1d32-11ba-172d461062b5.png)

## アニメーションの作成

この章では、[前回](https://qiita.com/a-nagaoka/items/a7a0a47820ae4881b9e3)作成した地図データと[e-Stat](https://www.e-stat.go.jp/)のAPIで取得した人口データを組み合わせて、都道府県ごとの人口増減を地図上で視覚化し、GIFアニメーションとして保存する方法を解説します。

### 1. 作成の手順
1. Google Colabのファイル直下に[前回記事](https://qiita.com/a-nagaoka/items/a7a0a47820ae4881b9e3)で作成した地図データのファイル```japan_p_simply5.geojson```を配置する
1. コードセルに下記の全体コードの内容を張り付け、```APP_ID =```の右辺を[e-Stat](https://www.e-stat.go.jp/)で発行した独自のアプリケーションIDに置き換える
1. コードセルを実行する。実行が終了すると、ファイル直下にGIFアニメーションファイル```人口推移_base_1975.gif```が出力される

- 全体コード
  <details><summary>展開してください</summary>

    ```python
    # 都道府県ごとの人口の推移を見える化するアニメーション
    import geopandas as gpd
    import matplotlib.pyplot as plt
    import numpy as np
    import pandas as pd
    import requests
    from matplotlib.animation import FuncAnimation, PillowWriter
    from matplotlib.colors import TwoSlopeNorm
    import matplotlib.font_manager as fm
    
    # 定数の設定
    APP_ID = "発行したアプリケーションID"
    GEO_FILE = "japan_p_simply5.geojson"
    CD_CAT = "A1101"
    LABEL = "人口"
    
    def fetch_population_data(first_year = 0):
        """
        指定の年度以降の都道府県ごとの総人口データを取得し、Pandas DataFrameとして返す関数。
    
        e-Stat API を使用して日本の都道府県ごとの総人口データを取得し、指定された
        年度以降のデータをフィルタリングして整理されたDataFrameを返します。
    
        パラメータ:
            first_year (int, optional): フィルタリングの基準となる開始年度。
                                        デフォルト値は 0 (全てのデータを取得)。
    
        戻り値:
            pd.DataFrame: 都道府県ごとの人口データ。
                            以下の列を持つ:
                            - "prefecture": 都道府県名
                            - "population": 人口（整数値）
                            - "year": 年度（整数値）
    
        注意:
            - APIへのリクエストには、事前に適切なAPP_IDが設定されている必要があります。
        """
        # e-Stat APIを使用して日本の都道府県ごとの総人口データをjson形式で取得
        url = f"https://api.e-stat.go.jp/rest/3.0/app/json/getStatsData?statsDataId=0000010101&cdCat01={CD_CAT}&appId={APP_ID}"
        response = requests.get(url)
        data = response.json()
    
        # 都道府県の情報から、キーがコード、値が都道県名の辞書を作成
        # code:"00000"のデータは「全国」だが、今回は不要のため除外する
        area_classes = data['GET_STATS_DATA']['STATISTICAL_DATA']['CLASS_INF']['CLASS_OBJ'][2]['CLASS']
        code_to_prefecture = {entry['@code']: entry['@name'] for entry in area_classes if entry['@code'] != '00000'}
    
        # 年度、都道府県名、総人口数の情報を格納したDataFrameを構築
        population_data = [
            {"prefecture": code_to_prefecture.get(value['@area'], None), "population": int(float(value['$'])), "year": int(value["@time"][:4])}
            for value in data['GET_STATS_DATA']['STATISTICAL_DATA']['DATA_INF']['VALUE']
            if code_to_prefecture.get(value['@area'], None) is not None
        ]
        df_population = pd.DataFrame(population_data)
    
        # 指定年度以降になるようyearでフィルタリング
        df_population = df_population[df_population["year"] >= first_year]
        # yearでソート
        df_population = df_population.sort_values(by="year").reset_index(drop=True)
        return df_population
    
    def add_population_change(year_df_population, base_population_data):
        """
        指定した年度の人口増減率を計算し、結果を含むDataFrameを返す関数。
    
        パラメータ:
            year_df_population (pd.DataFrame): 指定年度の人口データ。
                                                以下の列を持つ:
                                                - "prefecture": 都道府県名
                                                - "population": 人口
                                                - "year": 年度
            base_population_data (pd.DataFrame): 基準年度の人口データ。
                                                  以下の列を持つ:
                                                  - "prefecture": 都道府県名
                                                  - "population": 基準年度の人口
    
        戻り値:
            pd.DataFrame: 増減率を含む人口データ。
                          以下の列を持つ:
                          - "prefecture": 都道府県名
                          - "population": 指定年度の人口
                          - "year": 年度
                          - "population_change_rate": 人口増減率（%）
        """
        if not year_df_population.empty:
            # 基準年度からの増減率を計算
            merged_df = pd.merge(year_df_population, base_population_data,
                                 on="prefecture", suffixes=('', '_base'))
            merged_df["population_change_rate"] = (merged_df["population"] - merged_df["population_base"]) / merged_df["population_base"] * 100.0
            # 必要な列のみを取得して返す
            return merged_df[["prefecture", "population", "year", "population_change_rate"]]
    
    def get_population_data(first_year = 0):
        """
        指定の年度以降の総人口データを取得し、最初の年度からの増減率を計算してDataFrameとして返す関数。
    
        e-Stat APIを使用して日本の都道府県ごとの総人口データを取得し、基準年度からの増減率を計算して
        結果をDataFrameとして整形します。
    
        パラメータ:
            first_year (int, optional): データを取得する範囲の開始年度。
                                        デフォルト値は0（全てのデータを取得）。
    
        戻り値:
            tuple:
                - pd.DataFrame: 各都道府県の人口データと増減率を含むDataFrame。
                                以下の列を持つ:
                                - "prefecture": 都道府県名
                                - "population": 人口
                                - "year": 年度
                                - "population_change_rate": 基準年度からの人口増減率（%）
                - list: 取得した年度のリスト。
        """
        df = fetch_population_data(first_year)
        years = df["year"].unique().tolist()
    
        # 基準年のデータを抽出
        base_population_data = df[df["year"] == years[0]]
    
        # 基準年以降のデータについて、増減率の列を追加
        all_population_data = [
            add_population_change(df[df["year"] == year], base_population_data)
            if year != years[0] else df[df["year"] == year]
            for year in years
        ]
    
        # 全データを結合してインデックスをリセット
        df_population_all = pd.concat(all_population_data, ignore_index=True)
        return df_population_all, years
    
    def setup_animation_plot(df_population_all, figsize=(13, 11)):
        """
        アニメーション用のプロットとカラーマップを設定する関数。
    
        この関数は、人口増減率を表示するためのプロット領域（Figure と Axes）を作成し、
        カラーマップ（TwoSlopeNorm）を設定して返します。増減率の最大値と最小値に基づいて
        カラーマップの範囲を動的に調整します。
    
        パラメータ:
            df_population_all (pd.DataFrame): 人口増減率を含むデータフレーム。
                                              必須列:
                                              - "population_change_rate": 増減率（%）
            figsize (tuple, optional): プロットのサイズ（幅, 高さ）。デフォルトは (13, 11)。
    
        戻り値:
            tuple: 以下の3つのオブジェクトを返します。
                   - fig: MatplotlibのFigureオブジェクト（プロット領域）。
                   - ax: MatplotlibのAxesオブジェクト（プロット軸）。
                   - norm: TwoSlopeNormオブジェクト（カラーマップの正規化）。
    
        注意:
            - `df_population_all` に "population_change_rate" 列が含まれていない場合、この関数は動作しません。
            - カラーマップの範囲は、増減率の値に基づいて動的に設定されます。
        """
        # プロットの設定
        fig, ax = plt.subplots(1, 1, figsize=figsize)
        plt.subplots_adjust(left=0.0, right=1.0, top=1.0, bottom=0.0)
    
        # カラーマップの設定
        max_value = df_population_all['population_change_rate'].max()
        min_value = df_population_all['population_change_rate'].min()
        rounded_up_value = np.ceil(max_value / 10) * 10
        rounded_down_value = np.floor(min_value / 10) * 10
        if (rounded_up_value <= 0):
          rounded_up_value = 0.01
        if (rounded_down_value >= 0):
          rounded_down_value = -0.01
        norm = TwoSlopeNorm(vmin=rounded_down_value, vcenter=0, vmax=rounded_up_value)
    
        return fig, ax, norm
    
    def create_animation(fig, ax, norm, df_population_all, years, filename='anime.gif'):
        """
        人口増減率の推移を示す地図アニメーションを作成し、保存する関数。
    
        日本地図上に都道府県ごとの人口増減率をプロットし、指定された年度の範囲でアニメーションを生成します。
        アニメーションはGIFファイルとして保存されます。
    
        パラメータ:
            fig (matplotlib.figure.Figure): アニメーション用のFigureオブジェクト。
            ax (matplotlib.axes.Axes): アニメーション用のAxesオブジェクト。
            norm (TwoSlopeNorm): カラーマップの正規化オブジェクト。
                                 人口増減率を視覚化する際の色範囲を定義。
            df_population_all (pd.DataFrame): 各年度の人口データと増減率を含むDataFrame。
                                              必須列:
                                              - "prefecture": 都道府県名
                                              - "year": 年度
                                              - "population_change_rate": 増減率（%）
            years (list): 表示する年度のリスト。最初の年が基準年。
            filename (str, optional): 作成したアニメーションの保存先ファイル名。デフォルトは 'anime.gif'。
    
        注意:
            - 使用するGeoJSONファイルのパスは、定数 `GEO_FILE` として事前に定義されている必要があります。
            - 日本語フォントが必要なため、フォントインストールコマンド（`apt-get`）が含まれています。
            - アニメーション保存には `PillowWriter` を使用します。インストールされていない場合は事前にインストールしてください。
    
        戻り値:
            なし: 結果のアニメーションは指定された `filename` に保存されます。
        """
        # 地図データの読み込み
        japan_map = gpd.read_file(GEO_FILE)
        # 日本語が使用できるフォントをインストール
        !sudo apt-get install fonts-noto-cjk
        # フォントの読み込み
        font_path = '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc'
        font_prop = fm.FontProperties(fname=font_path)
    
        # 地図のサイズを広げる調整
        plt.subplots_adjust(left=0.05, right=0.95, top=1.0, bottom=0.0)
    
        # 地図データと人口データを都道府県名で結合
        merged_data = japan_map.merge(df_population_all,
                                      left_on="N03_001", right_on="prefecture", how="left")
        # 最初の年
        first_year = years[0]
    
        def init():
            ax.clear()
            ax.set_title(f"{first_year}年からの{LABEL}推移", fontproperties=font_prop)
            ax.axis('off')
    
        def update(frame):
            ax.clear()
            year = years[frame + 1]
            df_population_year = df_population_all[df_population_all["year"] == year]
            japan_map_year = merged_data[merged_data["year"] == year]
            japan_map_year.plot(column="population_change_rate", cmap="seismic_r",
                                linewidth=0.8, ax=ax, edgecolor="0.8", norm=norm)
    
            if frame == 0:
                cbar = fig.colorbar(plt.cm.ScalarMappable(norm=norm, cmap="seismic_r"),
                                    ax=ax, shrink=0.5, orientation='vertical', fraction=0.02, pad=0.02)
                cbar.ax.tick_params(labelsize=8)
                cbar.set_label(f"{LABEL}増減率 (%)", fontsize=10, fontproperties=font_prop)
    
            ax.set_title(f"{first_year}年からの{LABEL}推移 [{year}年]",
                         fontproperties=font_prop, fontsize=16)
            ax.axis('off')
    
        # アニメーションを作成
        frame_count = len(years) - 1
        # すぐにGIFアニメーションを繰り返すと見づらいため、最後のフレームを5秒間分追加        
        fps = 5 # 1秒間のフレーム数
        last_frame_count = fps * 5  # fps * 秒数
        frames_with_pause = list(range(frame_count)) + [frame_count - 1] * last_frame_count
        # FuncAnimationを一度だけ作成
        ani = FuncAnimation(fig, update, frames=frames_with_pause, init_func=init)
    
        ani.save(filename, writer=PillowWriter(fps=fps))
    
    
    # 人口データの収集と増減率計算
    df_population_all, years = get_population_data()
    
    # アニメーションを設定
    fig, ax, norm = setup_animation_plot(df_population_all)
    
    # アニメーションを作成して保存
    create_animation(fig, ax, norm, df_population_all, years, f"{LABEL}推移_base_{years[0]}.gif")
    ```
    </details>　

- **注意点**
  - 地図データのファイル```japan_p_simply5.geojson```が存在しない場合、アニメーションの生成時にエラーが発生します。ファイルが正しい場所にあるか確認してください。
  - APIのリクエストには有効なアプリケーションIDが必要です。取得方法はe-Statの[利用ガイド](https://www.e-stat.go.jp/api/api-info/api-guide)を参照してください。


### 2. GIFアニメーション
出力されたGIFアニメーションは以下です。アニメーションでは、1975年のデータをベースとして、人口増加を青色、人口減少を赤色で示しています。色の濃淡で変化の度合いを表現し、都道府県ごとの傾向が一目でわかるようになっています。例えば、都市部では人口が増加している一方、地方では人口減少が進んでいることがわかります。

![人口推移_base_1975.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919085/94069b90-de4e-2bc6-2a2a-974d8b86919a.gif)

### 3. データ項目を変えてアニメーション作成

[e-Stat](https://www.e-stat.go.jp/)の統計表IDが```0000010101```のデータでは、総人口以外にも世代別人口や男女別人口、未婚人口など多くの項目が公開されています。全体コードの定数を下記のように変更し、出生数推移のアニメーションを作成してみました。

```python
# 定数の設定
...
CD_CAT = "A4101"
LABEL = "出生数"
```
出力されたアニメーションは以下です。出生数は、総人口以上に全国で減少していることがわかります。日本がだんだん赤黒い色に染まっていく様子を見ると、少子化の深刻さを実感します。

![出生数推移_base_1980 (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3919085/3bb96941-06c3-7de4-bc0f-0c8b53ea86f4.gif)

他にも、以下のようなアニメーションを作成できます。色々な切り口で見える化してみると、今まで気づかなかった傾向が見えてくるかもしれません。
- 高齢化率の推移：高齢者人口（65歳以上）を総人口で割った値
- 男女別人口推移：男女別人口を比較し、都市部の性別比率の変化を分析

## 終わりに

今回は、地図データと人口データを組み合わせてアニメーションを作る方法についてまとめました。ただ、このアニメーション、海(白い部分)の面積が大きく目立っています。これは小さな島々を含めて忠実に日本全体を描画するとこのようになるからです。今回のアニメーションでは都道府県ごとの傾向を見ることが目的のため、陸地の多い部分を拡大すると、より直感的に傾向をとらえやすくなると感じました。

次回は、小さな島々の描画を省略し、見やすく伝わりやすい「天気予報風」の地図でアニメーションを作る方法をご紹介します。