はじめに
総務省統計局は日本の各種統計データを作成しています
統計局の Web サイトでは様々なデータを公開しており、利用規約に従ってこれらのデータを利用することができます
というわけで、今回は Livebook から統計局の家計調査データを取得し、加工、分析してみたいと思います
実装したノートブックはこちら
出典
総務省統計局ホームページ
家計調査(家計収支編) 時系列データ(二人以上の世帯)
https://www.stat.go.jp/data/kakei/longtime/index.html#time
- 月 全品目(2015年改定)
- 月 全品目(2020年改定)
(2023年2月20日に利用)
セットアップ
必要なモジュールをインストールします
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: データ分析
- Codepagex: 文字コード変換
- Kino: 強力な UI/UX
- Kino VegaLite: グラフ描画
- Req: HTTP通信(データダウンロード)
今回使うデータは文字コードが Shift_JIS になっているため、 Codepagex を使って UTF-8 に変換します
config
で Codepagex で Shift_JIS を使うための設定をしています
準備
Explorer を使うための準備をします
alias Explorer.DataFrame
alias Explorer.Series
require Explorer.DataFrame
alias
によって Explorer.
を省略できるようになります
require Explorer.DataFrame
によって、データ解析時にクエリが使えるようになります
詳細は以下の記事を参照してください
Shift_JIS を短く指定できるようにしておきます
shift_jis = "VENDORS/MICSFT/WINDOWS/CP932"
データ取得
統計局の家計収支データ(二人以上の世帯)をダウンロードし、文字列として読み込みます
まずは2020年改訂の品目別月次をダウンロードしてみます
このデータは世帯平均でいつ、どういう品目にいくら使ったか、を見ることができます
例えば2022年4月、各家庭でお米をどれくらい買ったかの平均が分かります
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 に変換しています
これをしないとバイナリのままです
結果は以下のようになります
"二人以上の世帯_支出金額[円],,,,,,,,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,..."
これを見ても何の事だと思いますが、CSVデータが文字列として取得できています
整形
データフレームへの読込
文字列を改行 \n
で分割し、先頭2行は不要なのでスキップします
各行をカンマ ,
で分割することで各列の値にします
結果を [header_row | data_rows]
で受けることにより、ヘッダー行(列名の行)と、データ行に分割します
[header_row | data_rows] =
string_value
|> String.split("\r\n")
|> Enum.slice(3..-1)
|> Enum.map(&String.split(&1, ","))
結果は以下のようになります
[
["表側連番", "階層", "大分類", "中分類", "小分類", "中間計", "符号",
"品目分類", "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"],
...
]
最初の7列分は良いのですが、それより後ろは各年(2020年から2022年)の月が繰り返されています
列名が重複しているのは悪いので、ヘッダー行を修正します
まず先頭7列は別に分けておきます
fixed_cols = Enum.slice(header_row, 0..7)
各月の列を YYYY-MM-DD に変換します
month_cols =
header_row
# 8列目以降
|> 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 で各パイプの中身を見るとこんな感じです
加工した列を元の場所に戻します
header_row = fixed_cols ++ month_cols
結果は以下のようになります
["表側連番", "階層", "大分類", "中分類", "小分類", "中間計", "符号",
"品目分類", "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!
で読み込みます
df_2020 =
[header_row | data_rows]
|> Enum.map(&Enum.join(&1, ","))
|> Enum.join("\n")
|> DataFrame.load_csv!()
Kino.DataTable.new(df_2020)
データは以下のようになり、品目分類毎に各年月で何円消費したかを表します
データの結合
2015年改訂分のデータも同様に読み込みます
こちらは2015年から2019年までのデータです
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年改訂分では同じ品目コードでも違う値になっています
「表側連番」を結合のキーにしてはいけないため、それ以外の項目で結合します
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にする)
pivot_df =
DataFrame.pivot_longer(
df_all,
&String.starts_with?(&1, "20"),
names_to: "年月",
values_to: "支出金額"
)
Kino.DataTable.new(pivot_df)
ピボットされた結果は以下のようになります
これで、ある品目分類にいつどれくらい支出したのか分かりやすくなりました
年列、月列の追加
年月の列では年や月で集計したいときに不便なので、年だけ、月だけの列も作っておきます
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)
結果は以下のようになります
加工したデータのダウンロード
Kino.Download.new
でデータをローカルにダウンロードできます
Kino.Download.new(
fn -> DataFrame.dump_csv!(household_df) end,
filename: "家計支出統計_品目年月別.csv"
)
実行すると、以下のようなダウンロードボタンが表示され、そこからファイルとしてダウンロードできます
加工結果はリポジトリーにも保存しています(次回使います)
マスター情報
大分類などのマスター情報を確認します
集約項目一覧
大分類にも含まれない、品目分類ではない項目です
household_df
|> DataFrame.filter(大分類 == "-")
|> DataFrame.distinct(["品目分類"])
|> Kino.DataTable.new()
結果の一部は以下のようなものです
- 世帯数分布(抽出率調整)
- 集計世帯数
- 世帯人員(人)
- 18歳未満人員(人)
- 65歳以上人員(人)
- うち無職者人員(人)
- 有業人員(人)
- 世帯主の年齢(歳)
- 持家率(%)
品目別の支出額と関連付けて考察することができそうです
品目分類
大分類は以下のようにして取得できます
household_df
|> DataFrame.filter(
大分類 != "-" and
中分類 == "-" and
小分類 == "-" and
中間計 == "-" and
符号 == "-"
)
|> DataFrame.distinct(["大分類", "品目分類"])
|> Kino.DataTable.new()
大分類は以下の10分類です
大分類 | 名称 |
---|---|
1 | 食料 |
2 | 住居 |
3 | 光熱・水道 |
4 | 家具・家事用品 |
5 | 被服及び履物 |
6 | 保健医療 |
7 | 交通・通信 |
8 | 教育 |
9 | 教養娯楽 |
10 | その他の消費支出 |
中分類、小分類も同様です
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 | 外食 |
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 | 加工肉 |
小分類よりも細かい中間計もあります
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 | 他の野菜 |
最小単位は符号で、具体的な品目名になっています
household_df
|> DataFrame.filter(符号 != "-")
|> DataFrame.distinct(["符号", "品目分類"])
|> Kino.DataTable.new()
符号の一部抜粋
符号 | 名称 |
---|---|
340 | ようかん |
341 | まんじゅう |
342 | 他の和生菓子 |
343 | カステラ |
344 | ケーキ |
347 | ゼリー |
348 | プリン |
345 | 他の洋生菓子 |
ケーキの支出推移
データをグラフにしてみましょう
前述した符号の一覧で、「ケーキ」の符号が「344」と言うことが分かるので、ケーキのデータだけを抽出します
cake_df = DataFrame.filter(household_df, 符号 == "344")
Kino.DataTable.new(cake_df)
結果は以下のようになります
では支出金額推移をグラフ化してみましょう(勘のいい人はおおよその形が予想できると思います)
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: "支出金額")
心電図のように一定周期で脈打っています
2022年だけに絞ってみましょう
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: "支出金額")
12月のクリスマス特需が突き抜けていることが分かります
どれくらい突出しているのか数値化してみましょう
latest_cake_df
|> DataFrame.group_by("品目分類")
|> DataFrame.mutate(
最小: min(支出金額),
最大: max(支出金額),
最小最大比: max(支出金額) / min(支出金額)
)
|> DataFrame.select(["最小", "最大", "最小最大比"])
|> DataFrame.to_rows()
|> List.first()
結果は以下のようになります
%{
"品目分類" => "ケーキ",
"最大" => 1411.0,
"最小" => 482.0,
"最小最大比" => 2.9273858921161824
}
一番低い7月と比べて、一番高い12月は約3倍ケーキにお金を掛けています
まとめ
総務省統計局のデータを使えばいろいろ調査できそうですね