17
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ElixirAdvent Calendar 2022

Day 6

Elixir Livebook で地域経済分析システム RESAS から産業構造を取得して分析する

Last updated at Posted at 2022-11-22

はじめに

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

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

前回は人口構成を可視化してみましたが、今回は産業構造を分析してみたいと思います

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

出典

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

実行環境

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

RESAS-API の使い方

前回の記事を参照してください

セットアップ

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

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

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 で作っておいたトークンを入力します

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

ベースAPIを設定します

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

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

alias Explorer.DataFrame
alias Explorer.Series
alias VegaLite, as: Vl

RESAS の認証

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

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

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

都道府県一覧の取得

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

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

prefectures_df =
  prefectures_url
  |> HTTPoison.get!([auth_header])
  |> then(&JSON.decode!(&1.body))
  |> then(&DataFrame.new(&1["result"]))

prefectures_df
|> Kino.DataTable.new(sorting_enabled: true)

スクリーンショット 2022-11-22 10.33.39.png

大分県の都道府県コードを取得します

oita_pref_code =
  prefectures_df
  |> DataFrame.filter_with(&Series.equal(&1["prefName"], "大分県"))
  |> DataFrame.pull("prefCode")
  |> Series.first()

スクリーンショット 2022-11-22 10.35.32.png

産業一覧の取得

産業は大分類、中分類、小分類でそれぞれ分類されています

# 大分類
industries_broad_df =
  "#{base_url}/api/v1/industries/broad"
  |> HTTPoison.get!([auth_header])
  |> then(&JSON.decode!(&1.body))
  |> then(&DataFrame.new(&1["result"]))

industries_broad_df
|> Kino.DataTable.new(sorting_enabled: true)

スクリーンショット 2022-11-22 10.36.43.png

中分類は大分類コードでループして取得します

# 中分類
get_industries_middle = fn sic_code ->
  "#{base_url}/api/v1/industries/middle?sicCode=#{sic_code}"
  |> HTTPoison.get!([auth_header])
  |> then(&JSON.decode!(&1.body))
  |> then(&DataFrame.new(&1["result"]))
end

industries_middle_df =
  industries_broad_df
  |> DataFrame.pull("sicCode")
  |> Series.to_list()
  |> Enum.map(&get_industries_middle.(&1))
  |> Enum.filter(&(DataFrame.n_rows(&1) > 0))
  |> DataFrame.concat_rows()

industries_middle_df
|> Kino.DataTable.new(sorting_enabled: true)

スクリーンショット 2022-11-22 10.37.58.png

小分類は中分類コードでループして取得します

# 小分類
get_industries_narrow = fn simc_code ->
  "#{base_url}/api/v1/industries/narrow?simcCode=#{simc_code}"
  |> HTTPoison.get!([auth_header])
  |> then(&JSON.decode!(&1.body))
  |> then(&DataFrame.new(&1["result"]))
end

industries_narrow_df =
  industries_middle_df
  |> DataFrame.pull("simcCode")
  |> Series.to_list()
  |> Enum.map(&get_industries_narrow.(&1))
  |> Enum.filter(&(DataFrame.n_rows(&1) > 0))
  |> DataFrame.concat_rows()

industries_narrow_df
|> Kino.DataTable.new(sorting_enabled: true)

スクリーンショット 2022-11-22 10.38.57.png

地域別特化係数の取得

産業構造を数値化するものとして、特化係数を取得します

特化係数は、ある地域のある産業が、全国と比べてどれだけ特化しているか、を示す値です

1.0 を超えていれば、その地域はその産業に特化した地域である、と言えます

今回は付加価値額を基準に算出した特化係数を使用します

$$
特化係数(付加価値額) = \frac{域内における当該産業の付加価値額÷域内における全産業の付加価値額}{全国の当該産業の付加価値額÷全国の全産業の付加価値額}
$$

地域別特化係数は以下のようにして取得します

get_industry_power = fn year, pref_code, area_type, disp_type, sic_code, simc_code ->
  query =
    "?year=#{year}" <>
      "&prefCode=#{pref_code}" <>
      "&areaType=#{area_type}" <>
      "&dispType=#{disp_type}" <>
      "&sicCode=#{sic_code}" <>
      "&simcCode=#{simc_code}"

  url = "#{base_url}/api/v1/industry/power/forArea#{query}"

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

大分県の 2016 年時点、農業の地域別特化係数(付加価値額)を取得する場合は以下のように指定します

industry_power = get_industry_power.("2016", oita_pref_code, "1", "1", "A", "01")

スクリーンショット 2022-11-22 10.47.51.png

このとき、大分県内の各市町村の特化係数と同時に、都道府県別の特化係数も取得できます

今回はこちらの都道府県別データを使っていきます

スクリーンショット 2022-11-22 10.48.41.png

都道府県別データをデータフレーム化します

prefectures_industry_power_df =
  industry_power["prefectures"]
  |> DataFrame.new()

prefectures_industry_power_df
|> Kino.DataTable.new(sorting_enabled: true)

スクリーンショット 2022-11-22 10.49.39.png

都道府県毎にどれだけ農業に特化しているか、のデータが一覧化できました

グラフ化するときに使う関数を定義しておきます

get_values = fn df, col ->
  df
  |> DataFrame.pull(col)
  |> Series.to_list()
end

棒グラフを表示する関数です

# 都道府県別棒グラフ
prefectures_bar = fn df, col, value_label ->
  x = get_values.(df, "prefName")
  y = get_values.(df, col)

  Vl.new(width: 800, height: 400, title: "都道府県別#{value_label}")
  |> Vl.data_from_values(x: x, y: y)
  |> Vl.mark(:bar)
  |> Vl.encode_field(
    :x,
    "x",
    type: :nominal,
    title: "都道府県"
  )
  |> Vl.encode_field(
    :y,
    "y",
    type: :quantitative,
    title: value_label
  )
end

鹿児島、青森、宮崎、北海道が農業に特化しています

スクリーンショット 2022-11-22 11.04.53.png

中分類単位で全産業の特化係数を取得しましょう

prefectures_industry_power_df =
  # 中分類コードリストを取得
  industries_middle_df
  |> DataFrame.select(["sicCode", "simcCode"])
  |> DataFrame.to_rows()
  # 中分類毎に特化係数を取得
  |> Enum.map(fn industry ->
    get_industry_power.(
      "2016",
      oita_pref_code,
      "1",
      "1",
      industry["sicCode"],
      industry["simcCode"]
    )
    |> then(& &1["prefectures"])
    # 該当データがないものは除去する
    |> Enum.filter(&(&1 != nil))
    # データを整形する
    |> Enum.map(fn datum ->
      Map.merge(datum, %{"sicCode" => industry["sicCode"], "simcCode" => industry["simcCode"]})
    end)
  end)
  # 中分類毎にデータフレーム化する
  |> Enum.map(&DataFrame.new(&1))
  # 該当データがないものは除去する
  |> Enum.filter(&(DataFrame.n_rows(&1) > 0))
  # 全中分類のデータフレームを結合する
  |> DataFrame.concat_rows()
  # 大分類、中分類名を表示するために結合する
  |> DataFrame.join(industries_broad_df)
  |> DataFrame.join(industries_middle_df)

prefectures_industry_power_df
|> Kino.DataTable.new(sorting_enabled: true)

グラフタイトルに産業中分類を表示するための関数を用意します

# 中分類名取得
get_simc_name = fn simc_code ->
  industries_middle_df
  |> DataFrame.filter_with(&Series.equal(&1["simcCode"], simc_code))
  |> DataFrame.pull("simcName")
  |> Series.first()
end
``

```elixir
# 都道府県別産業構造中分類棒グラフ
prefectures_industry_bar = fn simc_code ->
  label = get_simc_name.(simc_code)

  prefectures_industry_power_df
  |> DataFrame.filter_with(&Series.equal(&1["simcCode"], simc_code))
  |> prefectures_bar.("value", "#{label}特化係数")
end

中分類コード 81 の学校教育特化係数を表示してみます

prefectures_industry_bar.("81")

京都は教育関連の産業に特化しているようです

スクリーンショット 2022-11-22 11.06.21.png

中分類コード 50 の各種商品卸売業特化係数を表示してみます

prefectures_industry_bar.("50")

卸売は東京に一極集中しています

北海道はマイナスなので、卸売では2016年時点で赤字のようです

スクリーンショット 2022-11-22 11.07.58.png

ピボット

特化係数と他の値の相関を見るため、データを形を変えます

今1列になっている中分類コードを、中分類コード別の列に変換します

このような操作をピボットと言います

変換前

  • prefCode
  • prefName
  • simcCode
  • value

変換後

  • prefCode
  • prefName
  • 01
  • 02
  • 03
    ...

DataFrame.pivot_wider で、行数を減らして列数を増やします

pivot_df =
  prefectures_industry_power_df
  |> DataFrame.select(["prefCode", "prefName", "simcCode", "value"])
  |> DataFrame.pivot_wider("simcCode", "value")

pivot_df
|> Kino.DataTable.new(sorting_enabled: true)

今まで都道府県数 * 中分類数(一部欠落) になっていた行数が、都道府県数(47)になりました

スクリーンショット 2022-11-22 11.12.36.png

ただし、 03 の列を見てみると、栃木県、群馬県の値が nil になっています

おそらくデータが取得できなかったものと思われます

このような値が存在しない部分を欠損値と言います

今後の分析のため欠損値を 0 で補完します

# 補完対象の列
cols =
  prefectures_industry_power_df
  |> DataFrame.distinct(["simcCode"])
  |> DataFrame.pull("simcCode")
  |> Series.to_list()

スクリーンショット 2022-11-22 11.16.21.png

Series.fill_missing を各列に適用することで補完できます

# 欠損値の補完
pivot_df =
  cols
  |> Enum.reduce(pivot_df, fn col, df ->
    DataFrame.mutate_with(df, &%{col => Series.fill_missing(&1[col], 0.0)})
  end)

pivot_df
|> Kino.DataTable.new()

スクリーンショット 2022-11-22 11.16.48.png

散布図の表示

中分類毎の特化係数がそれくらい相関しているか、散布図で見てみます

ついでに回帰直線も表示しましょう

scatter = fn df, x_col, y_col, size ->
  x = get_values.(df, x_col)
  y = get_values.(df, y_col)

  Vl.new(width: size, height: size)
  |> Vl.data_from_values(x: x, y: y)
  |> Vl.encode_field(:x, "x",
    type: :quantitative,
    scale: [domain: [Enum.min(x), Enum.max(x)]],
    title: x_col
  )
  |> Vl.encode_field(:y, "y",
    type: :quantitative,
    scale: [domain: [Enum.min(y), Enum.max(y)]],
    title: y_col
  )
  |> Vl.layers([
    Vl.new()
    |> Vl.mark(:point),
    Vl.new()
    |> Vl.mark(:line)
    |> Vl.transform(regression: "x", on: "y"),
  ])
end

農業と林業の散布図は以下のようになります

scatter.(pivot_df, "01", "02", 300)

 点が回帰直線の周辺にあり、そこそこ相関がありそうなことが分かります

つまり、農業に特化した地域は林業にも特化している傾向がある、ということです

スクリーンショット 2022-11-22 11.19.48.png

農業と娯楽業だとこのようになります

scatter.(pivot_df, "01", "80", 300)

あまり関係性はないか、負の相関、つまり農業に特化していれば娯楽業には特化していない、という傾向がありそうです

スクリーンショット 2022-11-22 11.22.40.png

一人当たり地方税の取得

都道府県の豊かさをどう示すか難しいですが、税収を比べてみましょう

地方税は地方公共団体が課税する税金で、以下のようなものが含まれます

  • 住民税
  • 事業税
  • 固定資産税
  • 地方消費税
  • 自動車税

一人当たりの地方税を見れば、どれだけたくさん稼いだり資産を持っていたりするのか分かりそうです

一人当たり地方税を以下のようにして取得します

get_local_tax = fn pref_code ->
  query =
    "?prefCode=#{pref_code}&cityCode=-"

  url = "#{base_url}/api/v1/municipality/taxes/perYear#{query}"

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

全都道府県について取得します

local_tax_df =
  prefectures_df
  |> DataFrame.pull("prefCode")
  |> Series.to_list()
  |> Enum.flat_map(fn pref_code ->
    pref_code
    |> get_local_tax.()
    |> Enum.map(&Map.merge(&1, %{"prefCode" => pref_code}))
  end)
  |> DataFrame.new()
  |> DataFrame.join(prefectures_df)

local_tax_df
|> Kino.DataTable.new(sorting_enabled: true)

スクリーンショット 2022-11-22 11.33.13.png

大分県の一人当たり地方税推移を見てみましょう

target_df = DataFrame.filter_with(local_tax_df, &Series.equal(&1["prefCode"], oita_pref_code))

Smart Cells の Chart を使います

2009 年から 2014 年に落ち込み、 2015 年以降は上昇しています

スクリーンショット 2022-11-22 11.38.23.png

2019 年の全国の一人当たり地方税を見てみましょう

local_tax_df
|> DataFrame.filter_with(&Series.equal(&1["year"], 2019))
|> prefectures_bar.("value", "一人当たり地方税")

東京が突出して高いのが分かります

スクリーンショット 2022-11-22 11.40.27.png

人口構成の取得

産業構造と人口構成の関係も見てみましょう

get_population_composition = fn pref_code ->
  query = "?prefCode=#{pref_code}&cityCode=-"

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

  url
  |> HTTPoison.get!([auth_header])
  |> then(&JSON.decode!(&1.body))
  |> then(& &1["result"]["data"])
end
population_composition_df =
  prefectures_df
  |> DataFrame.pull("prefCode")
  |> Series.to_list()
  |> Enum.flat_map(fn pref_code ->
    pref_code
    |> get_population_composition.()
    |> Enum.flat_map(fn data ->
      data["data"]
      |> Enum.map(fn datum ->
        Map.merge(datum, %{"label" => data["label"], "prefCode" => pref_code})
      end)
    end)
  end)
  |> DataFrame.new()
  |> DataFrame.join(prefectures_df)

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

スクリーンショット 2022-11-22 11.43.36.png

前回と同じではありますが、大分県の人口構成推移です

target_df =
  population_composition_df
  |> DataFrame.filter_with(&Series.equal(&1["prefCode"], oita_pref_code))

スクリーンショット 2022-11-22 11.44.44.png

全都道府県の 2015 年時点の人口構成を見てみましょう

target_df =
  population_composition_df
  |> DataFrame.filter_with(&Series.equal(&1["year"], 2015))
  |> DataFrame.filter_with(&Series.not_equal(&1["label"], "総人口"))

東京、神奈川、大阪の人口が多いですね

スクリーンショット 2022-11-22 11.45.55.png

人口構成データを産業構造データと同じ都道府県毎のデータにするため 2015 年に絞ってピボットします

また、人口構成を数値化するため、老年率を計算します

population_df =
  population_composition_df
  |> DataFrame.filter_with(&Series.equal(&1["year"], 2015))
  |> DataFrame.mutate(value: &Series.cast(&1["value"], :float))
  |> DataFrame.select(["prefCode", "prefName", "label", "value"])
  |> DataFrame.pivot_wider("label", "value")
  |> DataFrame.mutate_with(fn df ->
    [
      老年率: Series.divide(df["老年人口"], df["総人口"])
    ]
  end)

population_df
|> Kino.DataTable.new(sorting_enabled: true)

スクリーンショット 2022-11-22 11.49.34.png

都道府県別の老年率を見てみましょう

population_df
|> prefectures_bar.("老年率", "老年率")

秋田が高く、沖縄が低いです

スクリーンショット 2022-11-22 11.50.48.png

相関係数の取得

明確に相関を表すため、相関係数を算出します

産業構造、地方税、人口構成のデータを結合します

target_df =
  local_tax_df
  |> DataFrame.filter_with(&Series.equal(&1["year"], 2015))
  |> DataFrame.mutate(value: &Series.cast(&1["value"], :float))
  |> DataFrame.select(["prefCode", "value"])
  |> DataFrame.rename(["prefCode", "localTax"])
  |> DataFrame.join(population_df)
  |> DataFrame.join(pivot_df)

target_df
|> Kino.DataTable.new(sorting_enabled: true)

スクリーンショット 2022-11-22 11.53.06.png

いくつか散布図を見てみましょう

農業と地方税

scatter.(target_df, "localTax", "01", 300)

右下の東京が地方税で突出しているため、全体が見えにくいです

スクリーンショット 2022-11-22 11.55.10.png

東京を外れ値として除外して表示しましょう

target_df
|> DataFrame.filter_with(&Series.not_equal(&1["prefCode"], 13))
|> scatter.("localTax", "01", 300)

見やすくなりましたが、点がバラバラなので相関はなさそうです

スクリーンショット 2022-11-22 11.56.38.png

地方税と総人口で見てみます

scatter.(target_df, "localTax", "総人口", 300)

これまた右上に東京が突出しています

スクリーンショット 2022-11-22 11.57.55.png

東京以外で見てみましょう

target_df
|> DataFrame.filter_with(&Series.not_equal(&1["prefCode"], 13))
|> scatter.("localTax", "総人口", 300)

これもそんなに相関がなさそうです

スクリーンショット 2022-11-22 11.59.06.png

以前やったのと同じように相関行列を計算します

相関行列に含ませる列を指定します

cols =
  prefectures_industry_power_df
  |> DataFrame.distinct(["simcCode"])
  |> DataFrame.pull("simcCode")
  |> Series.to_list()

cols = ["localTax", "総人口", "年少人口", "生産年齢人口", "老年人口", "老年率"] ++ cols

データを標準化します

standardize = fn df, column ->
  mean =
    df
    |> DataFrame.pull(column)
    |> Series.mean()

  std =
    df
    |> DataFrame.pull(column)
    |> Series.std()

  df
  |> DataFrame.mutate_with(fn in_df ->
    %{column => Series.subtract(in_df[column], mean)}
  end)
  |> DataFrame.mutate_with(fn in_df ->
    %{column => Series.divide(in_df[column], std)}
  end)
end
standardized_df =
  cols
  |> Enum.reduce(target_df, fn col, df ->
    standardize.(df, col)
  end)

standardized_df
|> Kino.DataTable.new()

スクリーンショット 2022-11-22 12.02.12.png

行列化して転置したものと内積を取ります

df_to_tensor = fn df ->
  df
  |> DataFrame.names()
  |> Enum.map(fn col ->
    standardized_df
    |> DataFrame.pull(col)
    |> Series.to_tensor()
  end)
  |> Nx.concatenate()
  |> Nx.reshape({DataFrame.n_columns(df), DataFrame.n_rows(df)})
end

standardized_tensor =
  standardized_df
  |> DataFrame.select(cols)
  |> df_to_tensor.()
  |> Nx.transpose()
covariance_tensor =
  standardized_tensor
  |> Nx.transpose()
  |> Nx.dot(standardized_tensor)
  |> Nx.divide(DataFrame.n_rows(standardized_df))

スクリーンショット 2022-11-22 12.04.27.png

相関行列をデータフレームに変換します

add_cols_label = fn list, cols_ ->
  [{"x", cols_} | list]
end

covariance_df =
  cols
  |> Stream.with_index()
  |> Enum.map(fn {col, index} ->
    {col, Nx.to_flat_list(covariance_tensor[index])}
  end)
  |> add_cols_label.(cols)
  |> DataFrame.new()

covariance_df
|> Kino.DataTable.new(keys: ["x" | cols])

スクリーンショット 2022-11-22 12.05.18.png

地方税との相関係数をヒートマップ化します

local_tax_heatmap =
  cols
  |> Stream.with_index()
  |> Enum.map(fn {col_1, index_1} ->
    covariance =
      covariance_tensor[0][index_1]
      |> Nx.to_number()

    %{
      x: "localTax",
      y: col_1,
      covariance: covariance
    }
  end)
  |> List.flatten()
Vl.new(width: 100, height: 1600)
|> Vl.data_from_values(local_tax_heatmap)
|> Vl.mark(:rect)
|> Vl.encode_field(:x, "x", type: :nominal)
|> Vl.encode_field(:y, "y", type: :nominal)
|> Vl.encode_field(
  :fill,
  "covariance",
  type: :quantitative,
  scale: [
    domain: [-1, 1],
    scheme: :blueorange
  ]
)

赤いところは正の相関が強く、青いところは負の相関が強いです

visualization.png

中分類コード 37 通信業の相関は強そうです

散布図を見てみましょう

# 通信業
target_df
|> scatter.("localTax", "37", 300)

通信業は東京一強なので、相関が強くなっているようです

スクリーンショット 2022-11-22 12.10.29.png

prefectures_industry_bar.("37")

スクリーンショット 2022-11-22 12.12.26.png

逆に社会保険・社会福祉・介護事業は真っ青です

# 社会保険・社会福祉・介護事業
target_df
|> scatter.("localTax", "85", 300)

スクリーンショット 2022-11-22 12.14.58.png

prefectures_industry_bar.("85")

スクリーンショット 2022-11-22 12.15.42.png

お金を稼いでいる東京ですが、医療や福祉では全く稼げていないことが分かります

続いて老齢率の相関ヒートマップも見てみましょう

elderly_heatmap =
  cols
  |> Stream.with_index()
  |> Enum.map(fn {col_1, index_1} ->
    covariance =
      covariance_tensor[5][index_1]
      |> Nx.to_number()

    %{
      x: "老年率",
      y: col_1,
      covariance: covariance
    }
  end)
  |> List.flatten()
Vl.new(width: 100, height: 1600)
|> Vl.data_from_values(elderly_heatmap)
|> Vl.mark(:rect)
|> Vl.encode_field(:x, "x", type: :nominal)
|> Vl.encode_field(:y, "y", type: :nominal)
|> Vl.encode_field(
  :fill,
  "covariance",
  type: :quantitative,
  scale: [
    domain: [-1, 1],
    scheme: :blueorange
  ]
)

visualization (1).png

そんなに相関が強いものはなく、不動産取引で負の相関があるくらいでしょうか

# 不動産取引業
target_df
|> scatter.("老年率", "68", 300)

スクリーンショット 2022-11-22 12.19.01.png

prefectures_industry_bar.("68")

スクリーンショット 2022-11-22 12.20.04.png

お年寄りが多いと、あまり引っ越しをしない、ということでしょうか

まとめ

RESAS には他にもいろいろな API があるので、色々なデータの組み合わせを見ると面白そうです

17
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?