Elixir Livebook で地域経済分析システム RESAS から人口構成を取得して表、グラフで可視化する

## はじめに

RESAS は日本の地域経済データを取得できるサービスです

> 地域経済分析システム（RESAS：リーサス）は、地方創生の様々な取り組みを情報面から支援するために、経済産業省と内閣官房デジタル田園都市国家構想実現会議事務局が提供しています。

https://resas.go.jp/#/44/44201

RESAS-API に利用登録すると、 REST API でデータを取得することもできます

https://opendata.resas-portal.go.jp/

今回は Elixir Livebook 上で Explorer を使って、 RESAS のデータを分析、可視化してみたいと思います

https://livebook.dev/

https://github.com/elixir-nx/explorer

今回実装したノートブックの全量はこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/resas/population.livemd

## 出典

この記事はRESAS（地域経済分析システム）のデータを加工して作成しています

## 実行環境

このリポジトリーの Docker コンテナ上で実行しました

https://github.com/RyoWakabayashi/elixir-learning

## RESAS-API の使い方

公式サイトから「利用登録」に進んで、必要項目を入力して仮登録します

https://opendata.resas-portal.go.jp/

本登録用のメールに記載されているリンクにアクセスすれば登録完了です

![スクリーンショット 2022-11-15 22.30.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/3ea42dd7-209d-6d9e-660a-0fb4819e5e08.png)

ここに表示されるAPIキーを利用します

利用登録後は、 RESAS-API マイページの最下部にもAPIキーが表示されます

![スクリーンショット 2022-11-15 22.29.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9a0135bf-f64c-76fe-e32b-42ec297a304c.png)

## セットアップ

Livebook を起動してノートブックを開きます

以下のコードをノートブック上で実行してください

```elixir
Mix.install([
  {:httpoison, "~> 1.8"},
  {:json, "~> 1.4"},
  {:explorer, "~> 0.3"},
  {:kino, "~> 0.7"},
  {:kino_vega_lite, "~> 0.1.4"}
])
```

必要なライブラリをインストールします

- httpoison: REST API を呼び出す
- json: JSON をエンコード、デコードする
- explorer: データ分析
- kino: 実行結果を可視化する
- kino_vega_lite: データをグラフ化する

以下のコードを実行するとテキストエリアが表示されるので、 Tellus で作っておいたトークンを入力します

```elixir
# RESAS のAPIキーを入力する
api_key_input = Kino.Input.password("API_KEY")
```

ベースAPIを設定します

```elixir
base_url = "https://opendata.resas-portal.go.jp"
```

エイリアスを指定しておきます

```elixir
alias Explorer.DataFrame
alias Explorer.Series
```

## RESAS の認証

RESAS の認証は API キーをヘッダーに入れるだけです

認証ヘッダーを設定しておきます

```elixir
auth_header = {"X-API-KEY", Kino.Input.read(api_key_input)}
```

## 都道府県一覧の取得

公式リファレンスを参照すれば、どういう API をどう呼び出せばどういう内容が返ってくるのかが分かります

https://opendata.resas-portal.go.jp/docs/api/v1/prefectures.html

都道府県一覧は以下のようにして呼び出します

```elixir
prefectures_url = "#{base_url}/api/v1/prefectures"

prefectures =
  prefectures_url
  |> HTTPoison.get!([auth_header])
  |> then(&JSON.decode!(&1.body))
```

![スクリーンショット 2022-11-15 23.05.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/e9c9fc7c-de7a-3d2e-0ceb-9454582281bb.png)

レスポンスの `result` に都道府県マップのリストが入っていますね

`DataFrame.new()` にマップのリストを渡すだけでデータフレームが出来上がります

```elixir
prefectures_df = DataFrame.new(prefectures["result"])
```

![スクリーンショット 2022-11-15 23.14.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/4778dc39-dc09-7c32-f704-2c7c78735cb0.png)

データフレームのままだと見にくいので、表形式にしてみます

`Kino.DataTable.new()` にデータフレームをそのまま渡すだけです

```elixir
Kino.DataTable.new(prefectures_df, sorting_enabled: true)
```

ページングやソートができる表として表示されました

![table.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/300f2249-daa6-1c55-af83-ead4e9aef6d2.gif)

続いて大分県の都道府県コードをデータフレームから取得してみましょう

```elixir
oita_pref_code =
  prefectures_df
  # "prefName" が "大分県" の行を抽出
  |> DataFrame.filter_with(&Series.equal(&1["prefName"], "大分県"))
  # "prefCode" の列を選択
  |> DataFrame.pull("prefCode")
  # 先頭のデータを取得
  |> Series.first()
  |> dbg()
```

![スクリーンショット 2022-11-15 23.28.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/bc12d952-1b97-0a23-07e7-142b93b61322.png)

最後に `dbg` を入れたことにより、各パイプラインの途中経過を見ることができます

## 市区町村一覧の取得

都道府県コードを指定して市区町村一覧を取得します

空文字 `""` が渡された場合は全都道府県の市区町村になります

```elixir
get_cities = fn pref_code ->
  query =
    case pref_code do
      "" ->
        ""
      pref_code ->
        "?prefCode=#{pref_code}"
    end

  cities_url = "#{base_url}/api/v1/cities#{query}"

  cities_url
  |> HTTPoison.get!([auth_header])
  |> then(&JSON.decode!(&1.body))
  |> then(&(&1["result"]))
end
```

まず全国の市区町村を取得してみましょう

```elixir
all_cities_df =
  ""
  |> get_cities.()
  |> DataFrame.new()

all_cities_df
|> Kino.DataTable.new(sorting_enabled: true)
```

![スクリーンショット 2022-11-15 23.34.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/4dc954a7-556b-d053-efad-aae134ad8745.png)

全国で 1922 の市区町村が取得できました

bigCityFlag は「特別区・行政区フラグ」で、以下の値を持ちます

- 0:一般の市区町村
- 1:政令指定都市の区
- 2:政令指定都市の市
- 3:東京都23区

大分県内の市町村を取得してみましょう

```elixir
oita_cities_df =
  oita_pref_code
  |> get_cities.()
  |> DataFrame.new()

oita_cities_df
|> Kino.DataTable.new(sorting_enabled: true)
```

![スクリーンショット 2022-11-15 23.36.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/71c72b40-485e-69f8-019b-5c0c348c5b99.png)

18 市町村が取得できました

後で使うために大分市の市区町村コードを取得しておきます

```elixir
oita_city_code =
  oita_cities_df
  |> DataFrame.filter_with(&Series.equal(&1["cityName"], "大分市"))
  |> DataFrame.pull("cityCode")
  |> Series.first()
  |> dbg()
```

![スクリーンショット 2022-11-15 23.42.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/c318cfa2-9c3e-2eeb-c91f-f775bdbd3c07.png)

## 特別区・行政区のある都道府県

大分県に特別区・行政区があるか見てみましょう

```elixir
oita_cities_df
|> DataFrame.filter_with(&Series.not_equal(&1["bigCityFlag"], "0"))
|> Kino.DataTable.new(sorting_enabled: true)
```

![スクリーンショット 2022-11-15 23.43.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/741626c7-a301-2ce6-206c-28aa9891ed1a.png)

大分県に特別区・行政区はないですね

全国に特別区・行政区がある都道府県はどれくらいあるのでしょう

```elixir
all_cities_df
# bigCityFlag が 0 でない行を抽出する
|> DataFrame.filter_with(&Series.not_equal(&1["bigCityFlag"], "0"))
# 都道府県コードで重複排除する
|> DataFrame.distinct(["prefCode"])
# 都道府県一覧データフレームと結合する
|> DataFrame.join(prefectures_df)
|> Kino.DataTable.new(sorting_enabled: true)
|> dbg()
```

![スクリーンショット 2022-11-15 23.46.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/3ccf7d3c-c5bb-e66c-428b-c871d036f92e.png)

はい、上画像の 16 都道府県は特別区・行政区があります

## 人口構成の取得

いよいよ人口構成（年代別の人口）を取得します

人口構成 API は都道府県コードが必須、市区町コードは指定しない場合 `-` を渡します

```elixir
get_population_composition = fn pref_code, city_code ->
  query =
    "?prefCode=#{pref_code}&cityCode="
    <>
    case city_code do
      "" ->
        "-"
      city_code ->
        city_code
    end

  url = "#{base_url}/api/v1/population/composition/perYear#{query}"

  url
  |> HTTPoison.get!([auth_header])
  |> then(&JSON.decode!(&1.body))
  |> then(&(&1["result"]["data"]))
end
```

大分県全体の人口構成を取得します

```elixir
population_composition = get_population_composition.(oita_pref_code, "")
```

![スクリーンショット 2022-11-15 23.54.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/79d5fd26-9e96-62a1-f6a1-c9109dd6fdaa.png)

レスポンスは以下のような、ちょっと特殊な形になっています

```elixir[
  %{
    "data" => [
      %{"value" => 1239655, "year" => 1960},
      %{"value" => 1187480, "year" => 1965},
      ...
      %{"value" => 946917, "year" => 2040},
      %{"value" => 896653, "year" => 2045}
    ],
    "label" => "総人口"
  },
  %{
    "data" => [
      %{"rate" => 32.6, "value" => 405103, "year" => 1960},
      %{"rate" => 27.7, "value" => 329717, "year" => 1965},
      ...
      %{"rate" => 11, "value" => 105073, "year" => 2040},
      %{"rate" => 11, "value" => 99257, "year" => 2045}
    ],
    "label" => "年少人口"
  },
  %{
    "data" => [
      %{"rate" => 60.1, "value" => 746271, "year" => 1960},
      %{"rate" => 63.9, "value" => 759450, "year" => 1965},
      ...
      %{"rate" => 50.8, "value" => 481160, "year" => 2040},
      %{"rate" => 49.5, "value" => 444584, "year" => 2045}
    ],
    "label" => "生産年齢人口"
  },
  %{
    "data" => [
      %{"rate" => 7.1, "value" => 88281, "year" => 1960},
      %{"rate" => 8.2, "value" => 98313, "year" => 1965},
      ...
      %{"rate" => 38, "value" => 360684, "year" => 2040},
      %{"rate" => 39.3, "value" => 352812, "year" => 2045}
    ],
    "label" => "老年人口"
  }
]
```

このままだとデータフレームにできないので、ちょっと加工してからデータフレームにします

```elixir
population_composition_df =
  population_composition
  |> Enum.flat_map(fn data ->
    data["data"]
    |> Enum.map(fn datum ->
      Map.merge(datum, %{"label" => data["label"]})
    end)
  end)
  |> DataFrame.new()

population_composition_df
|> Kino.DataTable.new(sorting_enabled: true)
```

![スクリーンショット 2022-11-16 0.00.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ded7fa00-5a1e-81d5-98be-ae7c1757811a.png)

ではグラフにしてみましょう

楽をしたいので Smart Cells を使います

セル追加するときに `+Smart` から `Chart` を選びます

![スクリーンショット 2022-11-16 0.01.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/c662bfcb-d385-5e87-c901-a50890d571af.png)

すると以下のようなセルが追加されます

![スクリーンショット 2022-11-16 0.04.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/4932467d-61d9-0ef6-3adf-66bc73e0a56b.png)

以下のように入力してセルを実行しましょう

- CHARTING: 大分県人口構成推移
- WIDTH: 700
- HEIGHT: 400
- Data: population_composition_df
- Chart: line
- x-axis: year
- y-axis: value
- Color: label

すると、以下のようにグラフが表示できます

![スクリーンショット 2022-11-16 0.07.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/fc992a2d-a211-fea5-7773-2810f092bf19.png)

ちなみに2015年までは実績で、それより後は予測値です

人口減少と高齢化が見えて悲しいですね

大分市で見てみましょう

```elixir
oita_city_population_composition_df =
  oita_pref_code
  |> get_population_composition.(oita_city_code)
  |> Enum.flat_map(fn data ->
    data["data"]
    |> Enum.map(fn datum ->
      Map.merge(datum, %{"label" => data["label"]})
    end)
  end)
  |> DataFrame.new()

oita_city_population_composition_df
|> Kino.DataTable.new(sorting_enabled: true)
```

![スクリーンショット 2022-11-16 0.10.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/383a01f6-37fd-3b8f-d4db-b12a92815e47.png)

大分県全体と比べるとマシですが、やはり人口減少と高齢化が進むように予測されています

## まとめ

RESAS-API と Explore を使えばもっと色々な分析、可視化ができそうですね