総務省統計局の家計調査データを Livebook で取得、加工、分析する

## はじめに

総務省統計局は日本の各種統計データを作成しています

https://www.stat.go.jp/index.html

統計局の Web サイトでは様々なデータを公開しており、利用規約に従ってこれらのデータを利用することができます

https://www.stat.go.jp/data/guide/1.html

https://www.stat.go.jp/info/riyou.html

というわけで、今回は Livebook から統計局の家計調査データを取得し、加工、分析してみたいと思います

https://livebook.dev/

実装したノートブックはこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/explorer/household_expense_statistics.livemd

## 出典

総務省統計局ホームページ

家計調査（家計収支編）　時系列データ（二人以上の世帯）

<https://www.stat.go.jp/data/kakei/longtime/index.html#time>

- 月 全品目（2015年改定）
- 月 全品目（2020年改定）

（2023年2月20日に利用）

## セットアップ

必要なモジュールをインストールします

```elixir
Mix.install(
  [
    {:explorer, "~> 0.5"},
    {:codepagex, "~> 0.1"},
    {:kino, "~> 0.8"},
    {:kino_vega_lite, "~> 0.1"},
    {:req, "~> 0.3"}
  ],
  config: [
    codepagex: [
      encodings: ["VENDORS/MICSFT/WINDOWS/CP932"]
    ]
  ]
)
```

- [Explorer](https://github.com/elixir-nx/explorer): データ分析
- [Codepagex](https://github.com/tallakt/codepagex): 文字コード変換
- [Kino](https://github.com/livebook-dev/kino): 強力な UI/UX
- [Kino VegaLite](https://github.com/livebook-dev/kino_vega_lite): グラフ描画
- [Req](https://github.com/wojtekmach/req): HTTP通信(データダウンロード)

今回使うデータは文字コードが Shift_JIS になっているため、 Codepagex を使って UTF-8 に変換します

`config` で Codepagex で Shift_JIS を使うための設定をしています

## 準備

Explorer を使うための準備をします

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

`alias` によって `Explorer.` を省略できるようになります

`require Explorer.DataFrame` によって、データ解析時にクエリが使えるようになります

詳細は以下の記事を参照してください

https://qiita.com/RyoWakabayashi/items/053296b36d8744aca9ca

Shift_JIS を短く指定できるようにしておきます

```
shift_jis = "VENDORS/MICSFT/WINDOWS/CP932"
```

## データ取得

統計局の家計収支データ（二人以上の世帯）をダウンロードし、文字列として読み込みます

まずは2020年改訂の品目別月次をダウンロードしてみます

このデータは世帯平均でいつ、どういう品目にいくら使ったか、を見ることができます

例えば2022年4月、各家庭でお米をどれくらい買ったかの平均が分かります

```elixir
string_value =
  "https://www.stat.go.jp/data/kakei/longtime/csv/h-mon-2020.csv"
  |> Req.get!()
  |> then(&Codepagex.to_string!(&1.body, shift_jis))
```

`Codepagex.to_string` で文字コードを UTF-8 に変換しています

これをしないとバイナリのままです

結果は以下のようになります

```elixir
"二人以上の世帯_支出金額[円],,,,,,,,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,..."
```

これを見ても何の事だと思いますが、CSVデータが文字列として取得できています

## 整形

### データフレームへの読込

文字列を改行 `\n` で分割し、先頭2行は不要なのでスキップします

各行をカンマ `,` で分割することで各列の値にします

結果を `[header_row | data_rows]` で受けることにより、ヘッダー行（列名の行）と、データ行に分割します

```elixir
[header_row | data_rows] =
  string_value
  |> String.split("\r\n")
  |> Enum.slice(3..-1)
  |> Enum.map(&String.split(&1, ","))
```

結果は以下のようになります

```elixir
[
  ["表側連番", "階層", "大分類", "中分類", "小分類", "中間計", "符号",
   "品目分類", "1月", " 2月", " 3月", " 4月", " 5月", " 6月", " 7月", " 8月", " 9月",
   " 10月", " 11月", " 12月", " 1月", " 2月", " 3月", " 4月", " 5月", " 6月", " 7月",
   " 8月", " 9月", " 10月", " 11月", " 12月", " 1月", " 2月", " 3月", " 4月", " 5月",
   " 6月", " 7月", " 8月", " 9月", " 10月", " 11月", " 12月"],
  ["1", "-", "-", "-", "-", "-", "-", "世帯数分布(抽出率調整)", "10000", "10000", "10000",
   "10000", "10000", "10000", "10000", "10000", "10000", "10000", "10000", "10000", "10000",
   "10000", "10000", "10000", "10000", "10000", "10000", "10000", "10000", "10000", "10000",
   "10000", "10000", "10000", "10000", "10000", "10000", "10000", "10000", "10000", "10000",
   "10000", "10000", "10000"],
  ...
]
```

最初の７列分は良いのですが、それより後ろは各年（2020年から2022年）の月が繰り返されています

列名が重複しているのは悪いので、ヘッダー行を修正します

まず先頭７列は別に分けておきます

```elixir
fixed_cols = Enum.slice(header_row, 0..7)
```

各月の列を YYYY-MM-DD に変換します

```elixir
month_cols =
  header_row
  # ８列目以降
  |> Enum.slice(8..-1)
  # M月をMMに変換
  |> Enum.map(fn month ->
    month
    |> String.trim()
    |> String.replace("月", "")
    |> then(&String.slice("0" <> &1, -2..-1))
  end)
  # 年毎に分ける
  |> Enum.chunk_every(12)
  # 対象年
  |> Enum.zip([2020, 2021, 2022])
  " MMをYYYY-MM-DDに変換する
  |> Enum.flat_map(fn {month_list, year} ->
    Enum.map(month_list, fn month -> "#{year}-#{month}-01" end)
  end)
  |> dbg()
```

dbg で各パイプの中身を見るとこんな感じです

![dbg.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/a27fc255-8d63-0b60-47df-cc6b4c730094.gif)

加工した列を元の場所に戻します

```elixir
header_row = fixed_cols ++ month_cols
```

結果は以下のようになります

```elixir
["表側連番", "階層", "大分類", "中分類", "小分類", "中間計", "符号",
 "品目分類", "2020-01-01", "2020-02-01", "2020-03-01", "2020-04-01", "2020-05-01", "2020-06-01",
 "2020-07-01", "2020-08-01", "2020-09-01", "2020-10-01", "2020-11-01", "2020-12-01", "2021-01-01",
 "2021-02-01", "2021-03-01", "2021-04-01", "2021-05-01", "2021-06-01", "2021-07-01", "2021-08-01",
 "2021-09-01", "2021-10-01", "2021-11-01", "2021-12-01", "2022-01-01", "2022-02-01", "2022-03-01",
 "2022-04-01", "2022-05-01", "2022-06-01", "2022-07-01", "2022-08-01", "2022-09-01", "2022-10-01",
 "2022-11-01", "2022-12-01"]
```

加工したヘッダー行とデータ行を改めてくっつけて、 `DataFrame.load_csv!` で読み込みます

```elixir
df_2020 =
  [header_row | data_rows]
  |> Enum.map(&Enum.join(&1, ","))
  |> Enum.join("\n")
  |> DataFrame.load_csv!()

Kino.DataTable.new(df_2020)
```

データは以下のようになり、品目分類毎に各年月で何円消費したかを表します

![table.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/f038ecc2-ee68-b735-2660-c5624e142792.png)

### データの結合

2015年改訂分のデータも同様に読み込みます

こちらは2015年から2019年までのデータです

```elixir
df_2015 =
  "https://www.stat.go.jp/data/kakei/longtime/csv/h-mon-2015.csv"
  |> Req.get!()
  |> then(&Codepagex.to_string!(&1.body, shift_jis))
  |> String.split("\r\n")
  |> Enum.slice(3..-1)
  |> Enum.map(&String.split(&1, ","))
  |> then(fn [header_row | data_rows] ->
    fixed_cols = Enum.slice(header_row, 0..7)

    month_cols =
      header_row
      |> Enum.slice(8..-1)
      |> Enum.map(fn month ->
        month
        |> String.trim()
        |> String.replace("月", "")
        |> then(&String.slice("0" <> &1, -2..-1))
      end)
      |> Enum.chunk_every(12)
      |> Enum.zip(2015..2019)
      |> Enum.flat_map(fn {month_list, year} ->
        Enum.map(month_list, fn month -> "#{year}-#{month}-01" end)
      end)

    header_row = fixed_cols ++ month_cols

    [header_row | data_rows]
    |> Enum.map(&Enum.join(&1, ","))
    |> Enum.join("\n")
  end)
  |> DataFrame.load_csv!()

Kino.DataTable.new(df_2015)
```

2020年改訂分と2015年改訂分を結合して8年分のデータにします

`DataFrame.join` でデータフレーム同士で同じ名前の列をキーとして結合します

2020年改訂分と2015年改訂分では品目が変わっているため、 `how: :outer` を指定し、外部結合します

また、「表側連番」の項目は名前の通り連番なので、2020年改訂分と2015年改訂分では同じ品目コードでも違う値になっています

「表側連番」を結合のキーにしてはいけないため、それ以外の項目で結合します

```elixir
df_all =
  DataFrame.join(
    DataFrame.select(df_2015, &(&1 != "表側連番")),
    DataFrame.select(df_2020, &(&1 != "表側連番")),
    how: :outer
  )

Kino.DataTable.new(df_all)
```

これによって8年分のデータが作成できました

### ピボット

　各年月が列になっているのは時系列で分析したいときに不便です

年月を一つの列に変換してしまいましょう

`DataFrame.pivot_longer` で横方向に伸びていた（wideだった）データを縦方向に伸ばします（longにする）

```elixir
pivot_df =
  DataFrame.pivot_longer(
    df_all,
    &String.starts_with?(&1, "20"),
    names_to: "年月",
    values_to: "支出金額"
  )

Kino.DataTable.new(pivot_df)
```

ピボットされた結果は以下のようになります

![pivot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/74df27a7-c437-0d59-0735-34849683d323.png)

これで、ある品目分類にいつどれくらい支出したのか分かりやすくなりました

### 年列、月列の追加

年月の列では年や月で集計したいときに不便なので、年だけ、月だけの列も作っておきます

```elixir
date_list = Series.to_list(pivot_df["年月"])

month_series =
  date_list
  |> Enum.map(&(&1 |> String.split("-") |> Enum.at(1) |> String.to_integer()))
  |> Series.from_list()

year_series =
  date_list
  |> Enum.map(&(&1 |> String.split("-") |> Enum.at(0) |> String.to_integer()))
  |> Series.from_list()

household_df =
  pivot_df
  |> DataFrame.put("月", month_series)
  |> DataFrame.put("年", year_series)

Kino.DataTable.new(household_df)
```

結果は以下のようになります

![year_month.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/669f5bcd-4007-ede0-bf76-ea299f8fd38a.png)

### 加工したデータのダウンロード

`Kino.Download.new` でデータをローカルにダウンロードできます

```elixir
Kino.Download.new(
  fn -> DataFrame.dump_csv!(household_df) end,
  filename: "家計支出統計_品目年月別.csv"
)
```

実行すると、以下のようなダウンロードボタンが表示され、そこからファイルとしてダウンロードできます

![download_btn.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/6d5f19b2-7df5-32c5-d1d6-11777b06d1ab.png)

加工結果はリポジトリーにも保存しています（次回使います）

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/explorer/%E5%AE%B6%E8%A8%88%E6%94%AF%E5%87%BA%E7%B5%B1%E8%A8%88_%E5%93%81%E7%9B%AE%E5%B9%B4%E6%9C%88%E5%88%A5.csv

## マスター情報

大分類などのマスター情報を確認します

### 集約項目一覧

大分類にも含まれない、品目分類ではない項目です

```elixir
household_df
|> DataFrame.filter(大分類 == "-")
|> DataFrame.distinct(["品目分類"])
|> Kino.DataTable.new()
```

結果の一部は以下のようなものです

- 世帯数分布(抽出率調整)
- 集計世帯数
- 世帯人員(人)
- 18歳未満人員(人)
- 65歳以上人員(人)
- うち無職者人員(人)
- 有業人員(人)
- 世帯主の年齢(歳)
- 持家率(％)

品目別の支出額と関連付けて考察することができそうです

### 品目分類

大分類は以下のようにして取得できます

```elixir
household_df
|> DataFrame.filter(
  大分類 != "-" and
    中分類 == "-" and
    小分類 == "-" and
    中間計 == "-" and
    符号 == "-"
)
|> DataFrame.distinct(["大分類", "品目分類"])
|> Kino.DataTable.new()
```

大分類は以下の１０分類です

|大分類|名称        |
|----:|:----------|
|   1|食料         |
|   2|住居         |
|   3|光熱・水道     |
|   4|家具・家事用品  |
|   5|被服及び履物   |
|   6|保健医療      |
|   7|交通・通信     |
|   8|教育         |
|   9|教養娯楽      |
|  10|その他の消費支出|

中分類、小分類も同様です

```elixir
household_df
|> DataFrame.filter(
  大分類 != "-" and
    中分類 != "-" and
    小分類 == "-" and
    中間計 == "-" and
    符号 == "-"
)
|> DataFrame.distinct(["大分類", "中分類", "品目分類"])
|> Kino.DataTable.new()
```

中分類一部抜粋

|大分類|中分類|名称     |
|----:|----:|:-------|
|    1|    1|穀類     |
|    1|    2|魚介類   |
|    1|    3|肉類     |
|    1|    4|乳卵類   |
|    1|    5|野菜・海藻 |
|    1|    6|果物     |
|    1|    7|油脂・調味料|
|    1|    8|菓子類    |
|    1|    9|調理食品  |
|    1|   10|飲料     |
|    1|   11|酒類     |
|    1|   12|外食     |

```elixir
household_df
|> DataFrame.filter(
  大分類 != "-" and
    中分類 != "-" and
    小分類 != "-" and
    中間計 == "-" and
    符号 == "-"
)
|> DataFrame.distinct(["大分類", "中分類", "小分類", "品目分類"])
|> Kino.DataTable.new()
```

小分類一部抜粋

|大分類|中分類|小分類|名称       |
|----:|----:|----:|:---------|
|    1|    1|    2|パン       |
|    1|    1|    3|麺類       |
|    1|    1|    4|他の穀類    |
|    1|    2|    1|生鮮魚介    |
|    1|    2|    2|塩干魚介    |
|    1|    2|    3|魚肉練製品   |
|    1|    2|    4|他の魚介加工品|
|    1|    3|    1|生鮮肉      |
|    1|    3|    2|加工肉      |

小分類よりも細かい中間計もあります

```elixir
household_df
|> DataFrame.filter(
  大分類 != "-" and
    中分類 != "-" and
    小分類 != "-" and
    中間計 != "-" and
    符号 == "-"
)
|> DataFrame.distinct(["中間計", "品目分類"])
|> Kino.DataTable.new()
```

中間計の一部抜粋

|中間計          |名称   |
|--------------:|:-----|
|170-189        |鮮魚   |
|190-194        |貝類   |
|240-249        |葉茎菜 |
|250-259,25X    |根菜   |
|260-269,26B,26X|他の野菜|

最小単位は符号で、具体的な品目名になっています

```elixir
household_df
|> DataFrame.filter(符号 != "-")
|> DataFrame.distinct(["符号", "品目分類"])
|> Kino.DataTable.new()
```

符号の一部抜粋

|符号|名称      |
|--:|:---------|
|340|ようかん    |
|341|まんじゅう   |
|342|他の和生菓子|
|343|カステラ    |
|344|ケーキ     |
|347|ゼリー      |
|348|プリン      |
|345|他の洋生菓子|

## ケーキの支出推移

データをグラフにしてみましょう

前述した符号の一覧で、「ケーキ」の符号が「344」と言うことが分かるので、ケーキのデータだけを抽出します

```elixir
cake_df = DataFrame.filter(household_df, 符号 == "344")

Kino.DataTable.new(cake_df)
```

結果は以下のようになります

![cake_table.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9a6b1d09-91ff-9c91-4a28-f1987eeee701.png)

では支出金額推移をグラフ化してみましょう（勘のいい人はおおよその形が予想できると思います）

```elixir
month = Series.to_list(cake_df["年月"])
expenses = Series.to_list(cake_df["支出金額"])

VegaLite.new(width: 700, title: "ケーキ支出金額推移")
|> VegaLite.data_from_values(x: month, y: expenses)
|> VegaLite.mark(:line, tooltip: true)
|> VegaLite.encode_field(:x, "x", type: :temporal, title: "年月")
|> VegaLite.encode_field(:y, "y", type: :quantitative, title: "支出金額")
```

![cake_graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/3262f690-49ee-1fe6-4447-bf4d6e482297.png)

心電図のように一定周期で脈打っています

2022年だけに絞ってみましょう

```elixir
latest_cake_df = DataFrame.filter(cake_df, 年 == 2022)

month = Series.to_list(latest_cake_df["月"])
expenses = Series.to_list(latest_cake_df["支出金額"])

VegaLite.new(width: 700, title: "ケーキ支出金額推移")
|> VegaLite.data_from_values(x: month, y: expenses)
|> VegaLite.mark(:line, tooltip: true)
|> VegaLite.encode_field(:x, "x", type: :ordinal, title: "年月")
|> VegaLite.encode_field(:y, "y", type: :quantitative, title: "支出金額")
```

![cake_graph_2022.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ae840dbe-7654-c46d-030d-8c47990b9bc3.png)

12月のクリスマス特需が突き抜けていることが分かります

どれくらい突出しているのか数値化してみましょう

```elixir
latest_cake_df
|> DataFrame.group_by("品目分類")
|> DataFrame.mutate(
  最小: min(支出金額),
  最大: max(支出金額),
  最小最大比: max(支出金額) / min(支出金額)
)
|> DataFrame.select(["最小", "最大", "最小最大比"])
|> DataFrame.to_rows()
|> List.first()
```

結果は以下のようになります

```elixir
%{
  "品目分類" => "ケーキ",
  "最大" => 1411.0,
  "最小" => 482.0,
  "最小最大比" => 2.9273858921161824
}
```

一番低い7月と比べて、一番高い12月は約3倍ケーキにお金を掛けています

## まとめ

総務省統計局のデータを使えばいろいろ調査できそうですね