Streamlitを活用して、Leafmapで将来推計人口を表示し推計年を切り替えてみる

## はじめに
将来推計人口メッシュデータのGeoJSONファイルを読み込み、5年毎に推計されている人口を切り替えて [Leafmap](https://leafmap.org/) のOpenStreetMap上で可視化してみます。

内容は前回記事の拡張版とも言えますので、合わせて読んでいただけると理解が深まるかと思います。

https://qiita.com/y-mto/items/321c113eb5a161cd46dc

### Streamlitとは？
今回はStreamlitのメリットをより活かすように考えました。

StreamlitとはPythonベースのフレームワークの一つで、データサイエンス分野でのWebアプリケーション開発を効率化するために設計されています。本格的なWebアプリケーション作成には向かないかもしれませんが、主に以下のようなメリットがあると思います。
1. フロントエンドの経験が無くても、Pythonの比較的少ないコードでWebアプリケーションを開発できる
1. Web UIに特化しており、インタラクティブなUIを簡単に作成できる
1. Streamlit Community Cloudを利用すれば、迅速なデプロイも可能
 
https://streamlit.io/

https://streamlit.io/cloud

## 将来推計人口データ
国立社会保障・人口問題研究所(社人研)が「コーホート要因法」で推計した「日本の地域別将来推計人口」をベースにして、マップマーケティングで作成しているデータ（未来人口データ2020）を利用しています。

https://www.mapmarketing.co.jp/data/tokei/future_population.html?grid=qiita&utm_source=qiita.com&utm_medium=referral&utm_campaign=qiita_select_future_populations_on_leafmap_2590ecb542924efd6fe4_01

### GeoJSONファイル（抜粋）
2020年人口総数、2025～2050年の将来推計人口を属性に持つGeoJSONファイルについては、[TerraMap API](https://www.mapmarketing.co.jp/terramap-api?grid=qiita&utm_source=qiita.com&utm_medium=referral&utm_campaign=qiita_select_future_populations_on_leafmap_2590ecb542924efd6fe4_02)から取得できるものを利用しました。ポリゴンに関しては人口密集地域がはっきり分かるように「2分の1地域メッシュ(約500mメッシュ)」を選択しました。
```future-populations.geojson
{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "area": {
          "area": 0.2618852293753624,
          "ratio_area": [
            0.003195323713839054
          ],
          "ratio": [
            0.012201236860362097
          ]
        },
        "data": [
          {
            "is_authorized": true,
            "ratio_value": [
              "60.810964512044691448"
            ],
            "stat_item_id": 23098,
            "stat_id": "029002300",
            "value": "4984"
          },
          {
            "is_authorized": true,
            "ratio_value": [
              "60.896373170067226127"
            ],
            "stat_item_id": 23099,
            "stat_id": "029002300",
            "value": "4991"
          },

          // .... 将来推計人口データを省略しています
        ],
        "point_coordinates": [
          139.646875,
          35.58125
        ],
        "geocode": "533925914",
        "points": []
      },
      "geometry": {
        "type": "MultiPolygon",
        "coordinates": [
          [
            [
              [
                139.65,
                35.579166667
              ],
              [
                139.64375,
                35.579166667
              ],
              [
                139.64375,
                35.583333333
              ],
              [
                139.65,
                35.583333333
              ],
              [
                139.65,
                35.579166667
              ]
            ]
          ]
        ]
      }
    },

    // .... featureを省略しています
```

## 環境準備
前回記事と同じですが、Pythonやpipを使えるようにして、まずはleafmap、geopandas、streamlitをインストールします。
```:パッケージのインストール
pip install leafmap geopandas streamlit
```

## サイドバーにセレクトボックスを設置
Streamlitアプリのサイドバーに人口データを選択するセレクトボックスを追加するには、以下のような比較的短いコードで実現することができます。

```py
import streamlit as st
```
```py
# 各人口データにはIDがあり、今回のGeoJSONファイルにもIDが含まれています
populations_dict = {
    "(基準)2020年人口総数": 23098,
    "2025年人口総数": 23099,
    "2030年人口総数": 23100,
    "2035年人口総数": 23101,
    "2040年人口総数": 23102,
    "2045年人口総数": 23103,
    "2050年人口総数": 23104,
}

with st.sidebar:
    st.header("将来推計人口の切り替え表示")
    selected_population = st.selectbox("年指定", options=list(populations_dict.keys()))
    selected_population_id = populations_dict[selected_population]
```

## 完成したPythonコード
前回記事のコードに対して、主に以下のアレンジを加えて完成させてみました。
- 参照するGeoJSONファイルを変更（前述）
- Streamlitのサイドバーとセレクトボックスを追加（前述）
- `population_styles()` で選択した人口データに対応
- 人口データに対する配色を変更

```app.py
import leafmap.foliumap as leafmap
import json
import streamlit as st

def get_color(d):
    """
    人口総数に応じて色を決めます

    :param d: 評価する人口総数
    :type d: int
    :return str: カラーコード
    """
    if d > 7000:
        return "#8B0000"
    elif d > 6000:
        return "#D7352B"
    elif d > 5000:
        return "#FFA746"
    elif d > 4000:
        return "#FFD976"
    elif d > 3000:
        return "#C9D0BB"
    elif d > 2000:
        return "#92C7FF"
    elif d > 1000:
        return "#5B97EE"
    else:
        return "#0855C4"

def population_styles(feature):
    """
    フィーチャの人口総数に応じてポリゴンスタイルを決めます

    :param feature: GeoJSONのフィーチャ
    :return style: ポリゴンスタイル
    """
    # 選択した人口データのIDに一致したdata要素を取得
    data = json.loads(feature["properties"]["data"])
    data_item = next(
        (item for item in data if item["stat_item_id"] == selected_population_id),
        None
    )
    # data要素が見つからなければ無色
    if data_item is None:
        return {
            "color": "black",
            "weight": 1,
            "fillColor": "#000000",
            "fillOpacity": 0.0,
        }
  
    value = float(data_item["value"])
    return {
        "color": "black",
        "weight": 1,
        "fillColor": get_color(value),
        "fillOpacity": 0.5,
    }

# 各人口データにはIDがあり、今回のGeoJSONファイルにもIDが含まれています
populations_dict = {
    "(基準)2020年人口総数": 23098,
    "2025年人口総数": 23099,
    "2030年人口総数": 23100,
    "2035年人口総数": 23101,
    "2040年人口総数": 23102,
    "2045年人口総数": 23103,
    "2050年人口総数": 23104,
}

with st.sidebar:
    st.header("将来推計人口の切り替え表示")
    selected_population = st.selectbox("年指定", options=list(populations_dict.keys()))
    selected_population_id = populations_dict[selected_population]


def main():
    # Mapオブジェクト作成
    m = leafmap.Map(
        # コントロールは最小限に
        draw_control=False,
        measure_control=False,
        # center=(35.71967, 139.65313), zoom=13,   # 地図の初期範囲を決めた場合
    )

    # 地図にGeoJSONのレイヤーを追加
    m.add_geojson(
        "future-populations.geojson",              # GeoJSONのパス
        layer_name="GeoJSON Layer",
        info_mode="on_click",
        style_callback=population_styles,          # 人口総数によるスタイル設定
        # zoom_to_layer=False,                     # GeoJSONレイヤーでズームさせない
    )

    # 凡例のDictionary
    population_color_dict = {
        "0-1000": "0855C4",
        "1000-2000": "5B97EE",
        "2000-3000": "92C7FF",
        "3000-4000": "C9D0BB",
        "4000-5000": "FFD976",
        "5000-6000": "FFA746",
        "6000-7000": "D7352B",
        "7000+": "8B0000",
    }
    # 凡例の追加
    m.add_legend(
        title="Population", 
        legend_dict=population_color_dict, 
        opacity=0.5
    )

    # 地図をStreamlitアプリ内に描画
    m.to_streamlit(height=500)

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

## 地図の表示
```:アプリ起動
streamlit run app.py
```

アプリを起動すると、以下のような地図表示になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2893989/ba0103c9-d306-194c-3036-49b69f4a481d.png)

:::note warn
**注意**　以上で人口データを切り替えて表示できるようになりましたが、Streamlitアプリはセレクトボックスなどのウィジェットの値が変更されるとアプリ全体のスクリプトが再実行されるので、例えば地図の場所を移動させた場合は元の場所に戻ってしまいます。
:::

## 将来推計人口の切り替え
地図の初期範囲を変更して、将来推計人口を切り替えた結果が以下になります。

```diff_python:地図の初期範囲を変更
- # center=(35.71967, 139.65313), zoom=13,
+ center=(35.71967, 139.65313), zoom=13,

- # zoom_to_layer=False,
+ zoom_to_layer=False,
```

![select-future-populations-on-leafmap.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2893989/fcb4140f-3856-e60c-7f4b-6055b7fd953e.gif)

年指定を切り替えた際に地図が1度白くなってしまうので、拡大しても変化は分かりづらいところがあります。また今回のコードでは、切り替えた際の地図範囲（中心位置やズームレベル）の保持と再利用に対しては課題が残りました。

とはいえ、Pythonコード120行ぐらいでここまで出来て、StreamlitとLeafmapの組み合わせは面白いと思いました。