公的統計のポータルサイト e-Stat から表を取ってきて、分析に使える形(縦持ち)に直すまでの手順をまとめます。これまで100本以上の分析記事で e-Stat のデータを使ってきましたが、時間がかかったのは分析よりも「表の読み方」でした。
この記事で扱うこと:
- 「ファイル」画面の Excel を statInfId で取る方法
- API(アプリケーションID が必要)で取る方法の概要
- 横持ちの表を縦持ちにする小さな関数
- 「-」「…」「x」などの記号、単位、桁ずれを点検する関数
1. Excel ファイルを statInfId で取る
e-Stat の「統計データを探す → ファイル」の画面では、表ごとに Excel や CSV が置かれています。ダウンロードボタンのリンク先は、次の形です。
https://www.e-stat.go.jp/stat-search/file-download?statInfId=<13桁の番号>&fileKind=0
statInfId は表ごとの番号で、fileKind=0 が Excel です(CSV が用意されている表は fileKind=1)。番号さえ控えておけば、同じ表をいつでも取り直せます。分析のフォルダには、この番号と取得日を必ず書いておくようにしています。
import requests
FILE_URL = "https://www.e-stat.go.jp/stat-search/file-download?statInfId={sid}&fileKind=0"
def download(stat_inf_id: str, path: str) -> str:
"""e-Stat の「ファイル」画面にある Excel を statInfId で取得する"""
r = requests.get(FILE_URL.format(sid=stat_inf_id), timeout=60)
r.raise_for_status()
with open(path, "wb") as f:
f.write(r.content)
return path
私の作業環境では、サーバーから政府のサイトに直接つなげない場面があったため、ブラウザの中で同じ URL を fetch し、JavaScript の Excel ライブラリ(SheetJS)で読んでから保存したこともあります。どちらの方法でも、取ってくるファイルは同じです。
2. API で取る(アプリケーションID が必要)
e-Stat には API もあります。ユーザー登録をしてアプリケーションID(appId)を発行すると、統計表ID(statsDataId)を指定して JSON で値を取れます。
import requests
API = "https://api.e-stat.go.jp/rest/3.0/app/json/getStatsData"
params = {"appId": "<あなたのappId>", "statsDataId": "<統計表ID>", "limit": 100000}
js = requests.get(API, params=params, timeout=60).json()
values = js["GET_STATS_DATA"]["STATISTICAL_DATA"]["DATA_INF"]["VALUE"]
# values は {"@tab": ..., "@cat01": ..., "@area": ..., "@time": ..., "$": "1234"} のリスト
API の値はもともと縦持ちなので、整形は楽です。その代わり、項目のコード(@cat01 など)を名前に直す対応表(CLASS_INF)を自分でつなぐ必要があります。また、API で出ていない表(古い年次の Excel だけの表など)もあります。私は「API があれば API、なければ Excel」という順で使い分けています。
3. 横持ちの表を縦持ちにする
Excel の表は、たいてい「行が地域、列が項目や年」の横持ちです。分析では「地域・項目・値」の縦持ちのほうが扱いやすいので、最初に変換します。ここで大事なのは、数値に直せなかった記号を捨てずに、別の列に残すことです。
import numpy as np
import pandas as pd
# 記号の意味は表ごとに違う。ここでは「消さずに記録する」だけにして、扱いは呼び出し側で決める
SYMBOLS = {"-", "-", "…", "...", "x", "X", "***"}
def to_number(s: pd.Series, dash_as: str = "na"):
"""数値に直す。戻り値は(数値, 記号)"""
raw = s.astype(str).str.strip().str.replace(" ", "", regex=False)
sym = raw.where(raw.isin(SYMBOLS))
num = pd.to_numeric(raw.str.replace(",", "", regex=False), errors="coerce")
if dash_as == "zero":
num = num.mask(raw.isin(["-", "-"]), 0.0)
return num, sym
def tidy(df: pd.DataFrame, id_cols: list[str], dash_as: str = "na") -> pd.DataFrame:
"""横持ちを縦持ちにする。value と symbol の2列を持たせる"""
long = df.melt(id_vars=id_cols, var_name="item", value_name="raw")
long["value"], long["symbol"] = to_number(long["raw"], dash_as)
return long
"1,234" のようなカンマ入りの文字列は、カンマを外さないと NaN になります。PDF の表を機械で読んだときも、カンマや小数点の扱いを間違えると「4,479」が「4」と「479」に分かれるなど、静かに値が壊れました(エラーは出ません)。
4. 記号の意味を、合計で確かめる
「-」が 0 なのか、欠測なのか、秘匿なのかは、表によって違います。私が見た5つの表では、次のようになっていました。
| 表 | 「-」の意味(確かめた方法) |
|---|---|
| 感染症の定点報告 | 0(「-」を0とみなすと、都道府県の合計=全国が全週で一致) |
| 国勢調査の基本集計 | 0(恒等式と「男+女=総数」が全行で一致) |
| 社会・人口統計体系(市区町村) | 0ではない(同じ列に数字の0と「-」が両方ある。区域がない年・調査の対象外などが混ざる) |
| 家計調査の増減率 | 「計算できない」(比べる前の年がない期間に集まる) |
表の注記を読むのが基本ですが、内訳の合計=総数で確かめると確実です。そのための関数です。
def check_sum(long, key, total_label, parts, col="item", tol=0.5):
"""内訳の合計=総数 を確かめる。内訳に欠けがあれば「確認できない」"""
w = long.pivot_table(index=key, columns=col, values="value", aggfunc="first", dropna=False)
diff = w[parts].sum(axis=1, min_count=len(parts)) - w[total_label]
w = w.assign(diff=diff)
w["status"] = np.select([diff.isna(), diff.abs() <= tol], ["確認できない", "一致"], "不一致")
return w
小さな例で動かすと、「-」の扱いで結果が変わるのが分かります。
df = pd.DataFrame({"地域": ["A市", "B町", "C村"],
"総数": ["1,234", "30", "…"], "男": ["600", "30", "…"], "女": ["634", "-", "…"]})
print(check_sum(tidy(df, ["地域"]), ["地域"], "総数", ["男", "女"]))
print(check_sum(tidy(df, ["地域"], dash_as="zero"), ["地域"], "総数", ["男", "女"]))
item 女 男 総数 diff status
地域
A市 634.0 600.0 1234.0 0.0 一致
B町 NaN 30.0 30.0 NaN 確認できない
C村 NaN NaN NaN NaN 確認できない
item 女 男 総数 diff status
地域
A市 634.0 600.0 1234.0 0.0 一致
B町 0.0 30.0 30.0 0.0 一致
C村 NaN NaN NaN NaN 確認できない
「-」を0とみなすと B町 の合計が合うので、この表の「-」は0と読むのが妥当、と判断できます。C村の「…」はどちらでも確認できないので、欠測として扱います。
5. 単位と桁ずれ
単位:表の見出しに【千円】【千人】と書かれていることがよくあります。課税対象所得【千円】を「円」と読み違えると、1人あたり所得の中央値は約282万円ではなく約2,800円になります。計算したあとに「ありうる範囲に入っているか」を必ず見ます。
桁ずれ:前の時点からの比が極端なセルを探します。
def check_jump(long, key, order, ratio=3.0):
"""前の時点から ratio 倍以上・1/ratio 以下に動いたセル(桁ずれの候補)"""
d = long.sort_values(key + [order]).copy()
d["prev"] = d.groupby(key)["value"].shift()
r = d["value"] / d["prev"]
return d[(r >= ratio) | (r <= 1 / ratio)]
引っかかったセルが、すべて入力の誤りとは限りません。市町村の総人口で3倍以上の変化を探したときは、見つかったのはすべて原発事故の避難指示区域で、実際の変化でした。検出は機械に任せ、判断は中身を見て人がする、という分担にしています。
まとめ
- Excel は statInfId で取り直せる形にしておく。API があれば API が楽
- 数値に直すときは、記号を捨てずに別の列に残す
- 記号の意味は表ごとに違う。注記を読み、合計で確かめる
- 単位と桁ずれは、計算したあとに「ありうる範囲か」で点検する
ここに載せた関数は、どの表にも使えるように小さくしてあります。実際の分析では、表ごとに SYMBOLS の扱いと点検の閾値を決めて、README に書いています。
関連リンク
- 分析記事(note):https://note.com/takayukimatsuura
- コードとデータ(GitHub):https://github.com/takayuki-0127/japan-open-data-notes
- ポートフォリオ:https://takayuki-onelinehisyo.netlify.app/portfolio