論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
構造化データを作る
Creating Structured Data
データ処理

🔖 キーワード索引

構造化データを作る作業は「取得 → パース → 抽出 → 正規化 → 検証」の順に進む。 下のチップは、 その各段で出てくる用語である。 後半の 5 つ(充填率・スキーマ準拠率・名寄せ・列定義書・丸めの規則)は、 このページで SSDSE-B-2026 を使って実際に数値を出している。

構造化データETLスクレイピング正規化パースJSONデータ整備スキーマセル充填率 Fスキーマ準拠率 S名寄せ列定義書(コードブック)丸めの規則
非構造化→構造化変換レコード化列定義データクレンジングテキスト抽出正規表現OCRXML/HTML パースDWH 取込

💡 30秒で分かる結論 — 構造化データを作る

🍰 まずはやさしく

バラバラな情報を表にまとめることです。

データを分析しやすくするために使います。

SNSの書き込みをリストにするような作業です。

この章では、変換の手順について読みます。

最も忙しい読者のために、 まず結論だけまとめます。 詳細は以下のセクションへ:

📍 文脈 — どこで出会うか

🍰 まずはやさしく

データの整理整頓のような作業です。

そのままでは計算できないデータを直します。

Webサイトの表をコピーして集計する時です。

この章では、どんな場面で使うかを読みます。

「Web ページの表から都道府県データを取りたい」 「PDF レポートの数値を集計したい」 — そのままでは分析できないデータを、 表形式 に直す作業。 SSDSE のような既に整った CSV は 誰かが先にこの作業をしてくれている。

このページの読み方:まず 30秒結論 と 直感 を読み、 必要に応じて 数式 や 計算例、 落とし穴 に進んでください。

🎨 直感で掴む

🍰 まずはやさしく

料理の下準備のようなものです。

分析という調理を始めるために行います。

PDFの数字を書き出して表にする作業です。

この章では、データの整え方を読みます。

料理に喩えるなら 下処理。 魚を捌き、 野菜を切り、 計量する — その後の調理(分析)が始められる状態にすること。

典型的なソース:

🎨 概念図で押さえる

非構造化データを「行×列のテーブル」に整える工程は分析の前提作業。 ここでは「データ構造のスペクトル」「PDF→CSV 変換フロー」「Tidy Data の 3 原則」を視覚化する。

データ構造のスペクトルデータ構造のスペクトル (構造化 ⇔ 非構造化)構造化 (高)非構造化 (高)CSV / RDBSSDSE-B-2026行×列が確定直接分析可JSON / XML階層ありAPI レスポンス展開必要PDF 表 / HTMLスクレイピング白書・報告書抽出が必要テキスト / 画像自由文・写真NLP/OCR大きな処理音声 / 動画時系列波形特徴抽出最も難→ 右に行くほど「テーブル化のコスト」が高い → 抽出・正規化が必要'>
図 A. データは構造化 (CSV/RDB) から非構造化 (画像/音声) まで連続的に広がる。 SSDSE-B-2026 は最も構造化された状態 (直接分析可)。 統計白書 PDF は中央 (抽出が必要)。 SNS テキストは右側 (NLP 処理が必要)。
PDF表からCSVへの変換フローPDF 表 → 構造化 CSV への変換 5 ステップ① 入手統計白書PDF 取得② 抽出camelottabula で抜く③ 整形列名統一セル結合解除④ 検証行数・合計PDF と照合⑤ CSV 保存分析直前形式to_csv注意: PDF の表は「セル結合」「ヘッダ複数行」が頻発 → ④の検証が一番大事SSDSE-B-2026 のように最初から CSV 化されたデータを使うとこの工程を省略できる'>
図 B. PDF 表から CSV への 5 段階。 camelot / tabula-py で抽出 → 列名統一 → 検証 → CSV 保存。 「セル結合」「複数行ヘッダ」「ページ跨ぎ」が主な落とし穴。 SSDSE-B-2026 のような前処理済データを優先する根拠もここにある。
Tidy Data 3原則Tidy Data の 3 原則 (Wickham 2014)❌ Untidy (人間が読みやすい)県 | 2020 人口 | 2021 人口 | 2022 人口東京 | 1396 | 1404 | 1410大阪 | 884 | 881 | 879列に値が混入 → 分析しづらい○ Tidy (機械が処理しやすい)県 | 年 | 人口東京 | 2020 | 1396東京 | 2021 | 1404縦長 (long format) → groupby が直感的3 原則:① 1 つの観測値 = 1 行 (年×県を 1 行ずつ)② 1 つの変数 = 1 列 (「人口」 列に値だけ入れる)③ 1 つの観測単位 = 1 表 (人口表と面積表は分ける)→ pandas.melt() で untidy → tidy 変換可能'>
図 C. Tidy Data の 3 原則は「分析しやすいテーブル形」の基準。 列に年 (2020/2021/2022) を入れるのは人間に読みやすいが、 機械的処理 (groupby, plot) では「県・年・人口」の 3 列に縦長化したほうが扱いやすい。 pandas.melt() で変換可能。

📐 定義と要素の読み解き

🍰 まずはやさしく

データを表にするためのルールです。

正確な分析結果を出すために使います。

表記のゆれを一つにまとめる作業です。

この章では、具体的な5つの工程を読みます。

取得 (Extract)
HTTP リクエスト、 ファイル読込、 DB クエリ等。 認証・レート制限・robots.txt に注意。
パース (Parse)
HTML/JSON/PDF 等の構造を理解してデータを取り出す。 ライブラリ任せがほぼ。
抽出 (Extract fields)
必要な情報を選び出す。 XPath, CSS セレクタ, 正規表現。
正規化 (Normalize)
「東京」「東京都」「Tokyo」を 1 つに統一。 日付形式統一、 単位統一。
検証 (Validate)
欠損、 範囲外、 型エラーをチェック。 pandera, Great Expectations が便利。

📐 工程ごとに「何を数えれば検査になるか」

5 つの工程は、 それぞれ数えるものを決めておくと検査に変えられる。 このページで SSDSE-B-2026 を使って実際に出した数を、 工程ごとに並べると次のとおり。

工程数えるものこのページでの実測
取得読み込んだ行数・列数2 行目(項目名)を飛ばすと 564 行 × 112 列、 飛ばさないと 565 行で 112 列すべて object 型
パース型として読めた値の割合 S桁区切り・全角数字のままでは S = 0.00、 整形後 S = 1.00
抽出値が入っているセルの割合 F と欠損の数F = 0.95(「-」の 1 セルは欠損のまま)
正規化結合キーで一致した行数県名そのままでは 10 行中 1 行、 名寄せ後は 10 行
検証主キーの重複・値の範囲・合計の整合(年度, Code) の重複 0、 千人単位の取り違えで 47 県すべて範囲外、 男 + 女 − 総人口 は ±1,000 人の許容で不一致 0

どの工程も「数えた結果が期待どおりか」を assert で書いておけば、 表が崩れたときに最初に壊れた工程で止まる。 取得から検証まで同じ数え方を使い回せるのが、 工程を分けて考える利点である。

🔬 数式を言葉で読み解く

「構造化度合い」を測る指標として、 セル充填率 $F = \\dfrac{\\text{値が入ったセル数}}{\\text{総セル数}}$、 スキーマ準拠率 $S = \\dfrac{\\text{型が一致した値の数}}{\\text{総値数}}$、 欠損率 $M = 1 - F$ を定義する。 これら 3 つで「データの作りやすさ」を定量化する。

3 つの記号を、 このページの 🧮 章で作る「崩れた表」(2023 年度の総人口上位 10 都道府県を、 Web の表でよく見る形に崩したもの。 数値の列は総人口と出生数の 2 列で 20 セル)で読むと次のとおり。

記号何を数えるか崩れた表整形後
$F$(セル充填率)「-」や空欄でない値が入っているセルの割合。 分母は数値列の全セル19 / 20 = 0.95(出生数に「-」が 1 つ)0.95(整形しても欠けた値は戻らない)
$S$(スキーマ準拠率)値が入っているセルのうち、 列の型(ここでは整数)として読めるものの割合0 / 19 = 0.00(「14,086,000」のような桁区切りと全角数字は数値として読めない)19 / 19 = 1.00
$M$(欠損率)$1 - F$。 整形で減らせない「元から無い値」の割合0.050.05

$S$ は整形(カンマ除去・全角→半角)で 0 から 1 に上がるが、 $F$ は上がらない。 構造化の作業で直せるのは「値の書き方」までで、 取れていない値を作ることはできない。 だから $F$ と $S$ を分けて記録しておくと、 「整形が足りないのか、 元の資料に値が無いのか」を区別できる。

表を作る目的が他の表と結合することなら、 もう 1 つ、 結合キーの一致率 $J = \dfrac{\text{相手の表と一致した行数}}{\text{自分の表の行数}}$ も記録しておくとよい。 同じ崩れた表で、 県名をそのまま使うと $J = 1/10 = 0.1$(北海道だけ)、 「東京→東京都」の対応表で名寄せすると $J = 10/10 = 1.0$ になる。 $F$ と $S$ がともに 1 に近くても $J$ が低ければ、 その表は単独では整っていても他のデータとつながらない。 $J$ が 1 未満のときは、 一致しなかった行の県名を必ず目で確かめる(この例では「東京」「大阪」のように接尾辞の無い 9 県)。

📊 関連手法 比較表

構造化データを作る と近接する手法群の位置づけを並べる。 隣接領域を押さえると、 自分のタスクで何を使うべきかが立体的に見えてくる。

手法位置づけ代表ツール
ETL(Extract-Transform-Load)DWH 向け一括変換Talend / Airbyte
ELT(最近主流)クラウド DWH で変換dbt / Fivetran
スクレイピングWeb からの抽出に特化BeautifulSoup / Scrapy
OCR + NLP画像・PDF 文書からTesseract / pdfplumber
LLM 抽出プロンプトで構造化GPT-4 / Claude
人手アノテーション正解データ作成Label Studio

💥 現場の失敗例 5 件

「うまくいくケース」より「失敗ケース」のほうが学びが多い。 実プロジェクトで頻発する 5 つを示す。

失敗例 1:URL を 1 秒間に 100 回叩いてサイトから IP BAN → 失敗例。 必ず `time.sleep(2)` と `robots.txt` 確認
失敗例 2:PDF の数値「1,234,567」を文字列のまま join 演算 → 計算結果が壊滅。 必ず `.str.replace(',', '').astype(int)`
失敗例 3:HTML パースで `<table>` を全部読むと広告枠も含まれる → 列名で固定指定するか `id=` でターゲット
失敗例 4:Excel の結合セルをそのまま `read_excel` → 半分が NaN。 `ffill()` で上の値を下へ埋めて補完
失敗例 5:文字コード判定をミスして日本語が文字化けする → `chardet` で先頭 4KB を判定し `encoding=` 明示

🧮 実値で計算してみる

気象庁の HTML 表から都道府県の気温データを取得する例の構造:

  1. requests.get(url) で HTML 取得
  2. BeautifulSoup で <table> を見つける
  3. 各 <tr> を列に分解
  4. 「23.5℃」のような文字列を 23.5 (float)に変換
  5. 欠損「-」や「--」を NaN に統一
  6. DataFrame に変換して to_csv で保存

🧮 数式に値を入れて手で計算する: 構造化データのレコード数

合成ログ (生テキスト 100 行) から構造化 CSV のレコード数を計算する。

Step 1: 生ログ

JSON ログ: 100 行 1 行 = 1 リクエスト = {timestamp, user_id, action, status} すべて parse 可能 → 100 レコード

Step 2: 構造化

列型例
timestampdatetime2026-06-03 10:00
user_idint1001
actionstrlogin
statusint200

Step 3: サイズ

レコード = 100、 列 = 4 → 400 セル

🐍 Python で再現

1
2
3
4
5
import pandas as pd
records = [{"timestamp":f"2026-06-03 10:{i:02d}", "user_id":1000+i, "action":"login", "status":200} for i in range(100)]
df = pd.DataFrame(records)
print(f"形状: {df.shape}")
print(f"セル数: {df.shape[0]*df.shape[1]}")

📤 実行結果

形状: (100, 4) セル数: 400

💬 手計算 (Step 3) 400 と Python 出力が完全一致。

🧮 崩れた Web 表を整えて F・S・結合できた行数を測る

🎯 このコードでやること:SSDSE-B-2026 の 2023 年度から総人口上位 10 都道府県を取り、 県名の「都・府・県」を落とし、 数値を桁区切り付きの文字列にし、 1 セルを「-」、 1 セルを全角数字にした「崩れた表」を作る。 それを整形して F と S を測り、 県名で SSDSE-B-2026 に結合できるかを確かめる

📥 入力例 崩れた表(先頭 4 行) 県名 総人口 出生数 東京 14,086,000 86,348 神奈川 9,229,000 53,991 大阪 8,763,000 55,292 愛知 7,477,000 - 照合用: SSDSE-B-2026(2023 年度 47 行)の Prefecture・A1101
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import unicodedata
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
d23 = df[df['SSDSE-B-2026'] == 2023]
top = d23.nlargest(10, 'A1101').reset_index(drop=True)

# Web の表を写したような「崩れた表」を作る(値は SSDSE-B-2026 の 2023 年度)
raw = pd.DataFrame({
    '県名': top['Prefecture'].str.replace(r'[都府県]$', '', regex=True),  # 「東京」「大阪」…
    '総人口': [f'{v:,}' for v in top['A1101']],                          # 桁区切り付き文字列
    '出生数': [f'{v:,}' for v in top['A4101']],
})
raw.loc[3, '出生数'] = '-'                                              # 未掲載
raw.loc[6, '総人口'] = unicodedata.normalize('NFKC', raw.loc[6, '総人口']).translate(
    str.maketrans('0123456789,', '0123456789,'))            # 全角数字

def metrics(t):
    vals = t[['総人口', '出生数']]
    filled = vals.apply(lambda c: c.notna() & (c.astype(str) != '-'))
    num = vals.apply(lambda c: pd.to_numeric(c, errors='coerce').notna())
    return filled.values.mean(), num.values.sum() / filled.values.sum()

F0, S0 = metrics(raw)
print(f'整形前: F = {F0:.2f}, S = {S0:.2f}')

clean = raw.copy()
for c in ['総人口', '出生数']:
    s = clean[c].map(lambda x: unicodedata.normalize('NFKC', x))   # 全角 → 半角
    s = s.str.replace(',', '', regex=False).replace('-', pd.NA)
    clean[c] = pd.to_numeric(s).astype('Int64')
F1, S1 = metrics(clean)
print(f'整形後: F = {F1:.2f}, S = {S1:.2f}  (欠損 {clean.isna().sum().sum()} セルは残る)')

# 県名で SSDSE-B-2026 に結合できるか
key = d23[['Prefecture', 'A1101']]
m0 = clean.merge(key, left_on='県名', right_on='Prefecture', how='inner')
print('県名そのままで結合できた行:', len(m0), m0['県名'].tolist())
alias = dict(zip(key['Prefecture'].str.replace(r'[都府県]$', '', regex=True), key['Prefecture']))
clean['Prefecture'] = clean['県名'].map(alias)
m1 = clean.merge(key, on='Prefecture', how='inner', suffixes=('', '_ssdse'))
print('名寄せ後に結合できた行:', len(m1))
print('総人口が元の値と一致:', int((m1['総人口'] == m1['A1101']).sum()), '/', len(m1))
📤 実行例(実測) 整形前: F = 0.95, S = 0.00 整形後: F = 0.95, S = 1.00 (欠損 1 セルは残る) 県名そのままで結合できた行: 1 ['北海道'] 名寄せ後に結合できた行: 10 総人口が元の値と一致: 10 / 10

💬 整形前は 20 セル中 19 セルに値があるので F = 0.95 だが、 桁区切りと全角数字のせいで数値として読めるセルが 1 つも無く S = 0.00。 カンマを外し全角を半角にすると S = 1.00 になる一方、 「-」だった 1 セルは欠損のまま残り F は 0.95 のまま。 県名をそのまま結合キーにすると、 「都・府・県」を含まない北海道の 1 行しか SSDSE-B-2026 と一致しない。 「東京→東京都」の対応表で名寄せしてから結合すると 10 行すべてがつながり、 整形後の総人口は 10 行とも元の A1101 と一致した。 取得→整形→名寄せ→照合の各段で件数を数えておくと、 どこで行が落ちたかがすぐ分かる。

🐍 Python での扱い

最小再現コード。 Web ページの HTML の表を BeautifulSoup で行と列に分解し、 数値列を整えて CSV に保存する。 ネットワークには接続せず、 手元の SSDSE-B-2026(2023 年・総人口上位 5 都府県)から桁区切り付きの HTML 表を作って入力にする:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import os
import pandas as pd
from bs4 import BeautifulSoup

# Web ページの代わりに、手元の SSDSE-B-2026 から「Web でよく見る形の表」を作る
# (桁区切りのカンマ付き文字列。ネットワークには接続しない)
src = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
top = src[src['SSDSE-B-2026'] == 2023].nlargest(5, 'A1101')
html = '<table><tr><th>都道府県</th><th>総人口</th><th>出生数</th></tr>' + ''.join(
    f'<tr><td>{p}</td><td>{a:,}</td><td>{b:,}</td></tr>'
    for p, a, b in zip(top['Prefecture'], top['A1101'], top['A4101'])) + '</table>'

# ここからが「HTML の表 → 構造化データ」の本処理
soup = BeautifulSoup(html, 'html.parser')
rows = [[c.text.strip() for c in tr.find_all(['th', 'td'])] for tr in soup.find_all('tr')]
df = pd.DataFrame(rows[1:], columns=rows[0])
print(df.dtypes.to_dict())                        # この時点ではすべて文字列
for c in ['総人口', '出生数']:
    df[c] = df[c].str.replace(',', '').astype(int)  # カンマを外して整数にする
print(df)
print(df.dtypes.to_dict())

os.makedirs('data/processed', exist_ok=True)   # 書き出し先のフォルダを先に作る
df.to_csv('data/processed/extracted.csv', index=False)
print('保存:', os.path.getsize('data/processed/extracted.csv'), 'バイト')
📤 実行例(実測) {'都道府県': dtype('O'), '総人口': dtype('O'), '出生数': dtype('O')} 都道府県 総人口 出生数 0 東京都 14086000 86348 1 神奈川県 9229000 53991 2 大阪府 8763000 55292 3 愛知県 7477000 48402 4 埼玉県 7331000 42108 {'都道府県': dtype('O'), '総人口': dtype('int64'), '出生数': dtype('int64')} 保存: 157 バイト

💬 HTML から取り出した直後は 3 列とも dtype が O(文字列)で、「14,086,000」のままでは足し算も並べ替えもできない。カンマを外して int64 に変えると東京都 14086000・出生数 86348 のような数値列になり、5 行 3 列の表として 157 バイトの CSV に保存できた。Web の表を扱うときは、取り出した直後に dtypes を見て文字列のまま残っている数値列がないかを確かめる。

🐍 Python 実装 — 4 段構え narration 付き

各コードブロックは 🎯 目的 / 📥 入力 / 🐍 コード / 📤 出力 / 💬 解説 の 5 要素セット。 SSDSE-B-2026 の 47 都道府県データ(2023 年)に実値計算して結論まで導く。

🎯 このコードでやること:SSDSE-B-2026.csv(Shift_JIS、 2 行ヘッダー)を Python で読み込み、 2023 年・47 都道府県だけ抜き出して構造化済み DataFrame を作る

📥 入力例: 入力データ(先頭 3 行プレビュー) SSDSE-B-2026,Code,Prefecture,A1101,A4101,A5101,... 年度,地域コード,都道府県,総人口,出生数,転入者数,... 2023,R01000,北海道,5092000,24430,47388,...
1
2
3
4
5
6
import pandas as pd
# SSDSE は Shift_JIS、 2 行目に日本語見出しが入る
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='shift_jis', skiprows=[1])
d2023 = df[df['SSDSE-B-2026'] == 2023].reset_index(drop=True)
print(d2023[['Code', 'Prefecture', 'A1101']].head())
print('行数:', len(d2023))
📤 実行例(実測) Code Prefecture A1101 0 R01000 北海道 5092000 1 R02000 青森県 1184000 2 R03000 岩手県 1163000 3 R04000 宮城県 2264000 4 R05000 秋田県 914000 行数: 47

💬 結果の読み方:47 行に揃った時点で構造化は成功。 もし「行数: 46」なら ある県のデータが落ちている。 もし 564 のままなら年度フィルタが効いていない。

🎯 このコードでやること:Wikipedia の表をスクレイピングし、 列名を SSDSE の Prefecture にそろえ、 面積を数値の列にして、 SSDSE と県名で結合できる形に整える流れ(取得→パース→正規化→検証)の最小例。 結合そのものはこのコードではまだしない

📥 入力例: 想定する HTML(実際は requests + BeautifulSoup で取得) <table> <tr><th>県</th><th>面積(km²)</th></tr> <tr><td>北海道</td><td>83,424</td></tr> <tr><td>青森県</td><td>9,646</td></tr> </table>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pandas as pd
from urllib.parse import quote
# pd.read_html は HTML の <table> を直接 DataFrame に変換できる
# URL の日本語はパーセントエンコードし、User-Agent を付けないと Wikipedia は 403 を返す
url = 'https://ja.wikipedia.org/wiki/' + quote('日本の都道府県')
tables = pd.read_html(url, match='面積', storage_options={'User-Agent': 'Mozilla/5.0'})
# 表の番号は記事の編集で変わるので、列名で 47 都道府県の一覧表を探す
t = next(t for t in tables if '都道府県' in t.columns and len(t) == 47)
area_col = next(c for c in t.columns if str(c).startswith('面積'))
wiki = t[['都道府県', area_col]].copy()
wiki.columns = ['Prefecture', 'area_km2']
# カンマ除去・float 化(正規化工程。すでに数値で読めていれば何も変わらない)
wiki['area_km2'] = wiki['area_km2'].astype(str).str.replace(',', '').astype(float)
print(wiki.head())
print(wiki['area_km2'].dtype, len(wiki))
📤 実行例(イメージ・未実測:Wikipedia から取得するコードのため、ここでは再実行して確かめていない。値は記事の記載しだいで変わる) Prefecture area_km2 0 北海道 83424.44 1 青森県 9645.64 2 岩手県 15275.01 3 宮城県 7282.29 4 秋田県 11637.52 float64 47

💬 結果の読み方:例の表示のように、北海道 83424.44、青森県 9645.64 と面積が float64 で 47 行そろえば、Web の表が分析に使える列になった。pd.read_html は「83,424」のような桁区切りも数値として読むので、ここでの str.replace は保険にすぎない。表の番号(tables[1] など)で決め打ちすると記事の編集で別の表を拾うので、列名と行数(47)で目的の表を探している。値は取得時点の Wikipedia の記載で、記事が更新されると小数点以下が変わる。

🎯 このコードでやること:構造化品質の 3 指標(充填率 F、 欠損率 M、 スキーマ準拠率 S)を SSDSE-B-2026 の 2023 年データで計算する

📥 入力例: d2023(47 行 × 112 列) Code Prefecture A1101 A4101 ...(112 列)
1
2
3
4
5
6
7
8
9
10
import pandas as pd
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='shift_jis', skiprows=[1])
d = df[df['SSDSE-B-2026'] == 2023]
F = 1 - d.isna().sum().sum() / (d.shape[0] * d.shape[1])
M = 1 - F
# 数値列の比率を「スキーマ準拠率」と見立てる
S = (d.dtypes.apply(lambda t: t.kind in 'if').sum()) / d.shape[1]
print(f'充填率 F = {F:.4f}')
print(f'欠損率 M = {M:.4f}')
print(f'スキーマ準拠率 S = {S:.4f}')
📤 実行例(実測) 充填率 F = 1.0000 欠損率 M = 0.0000 スキーマ準拠率 S = 0.9821

💬 結果の読み方:F=1.000 は欠損ゼロ、 S=0.98 は 112 列中 110 列が数値型。 残り 2 列は Code と Prefecture(文字列)。 整備済み公的データの理想形。

📝 演習問題 5 問

手を動かさないと身につかない。 5 問とも SSDSE-B-2026 を必ず触る前提で設計。

  1. SSDSE-B-2026.csv の総人口 A1101 を 2023 年だけに絞り、 47 行 × 3 列(Code, Prefecture, A1101)の DataFrame を作れ
  2. 上記から人口降順に並び替え、 上位 10 県を Markdown 表で出力するスクリプトを書け
  3. 総人口 A1101 と出生数 A4101 の相関を 2023 年データで計算し、 散布図を保存せよ(PNG)
  4. 全 12 年分(2012-2023)を long 形式に変換し、 `pd.melt` で `(year, prefecture, indicator, value)` に整形せよ
  5. Wikipedia の「日本の都道府県」表をスクレイピングし、 県名を SSDSE の Prefecture にそろえてから結合し、 人口密度を算出せよ

🐍 列定義書(メタデータ)も一緒に作る

🎯 このコードでやること:SSDSE-B-2026 の 1 行目(列コード)と 2 行目(日本語の項目名)を、 データ本体とは別の「列定義書」として取り出し、 列の分野ごとの数を数える。 あわせて、 2 行目を飛ばさずに読んだ場合に型がどう崩れるかを確かめる

📥 入力例 data/raw/SSDSE-B-2026.csv の先頭 2 行 SSDSE-B-2026,Code,Prefecture,A1101,A110101,… 年度,地域コード,都道府県,総人口,総人口(男),…
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pandas as pd

# 1 行目 = 列コード、 2 行目 = 日本語の項目名。 データとは別に「列定義書」として取り出す
head = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=None, nrows=2)
book = pd.DataFrame({'列コード': head.iloc[0], '項目名': head.iloc[1]})
book['分野'] = book['列コード'].str[0].where(book['列コード'].str.match(r'^[A-L]\d'), '識別')
print(book.head(6).to_string(index=False))
print('列数:', len(book), '/ 列コードの重複:', int(book['列コード'].duplicated().sum()))
print(book['分野'].value_counts().sort_index().to_string())
print('「(二人以上の世帯)」を含む列:', int(book['項目名'].str.contains('二人以上の世帯').sum()))

# 2 行目を飛ばさずに読むと、 項目名の行がデータに混ざって型が崩れる
bad = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932')
good = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
print('2 行目を飛ばさない: 行数', len(bad), '/ object 型の列', int((bad.dtypes == 'object').sum()))
print('2 行目を飛ばす    : 行数', len(good), '/ object 型の列', int((good.dtypes == 'object').sum()))
📤 実行例(実測) 列コード 項目名 分野 SSDSE-B-2026 年度 識別 Code 地域コード 識別 Prefecture 都道府県 識別 A1101 総人口 A A110101 総人口(男) A A110102 総人口(女) A 列数: 112 / 列コードの重複: 0 分野 A 30 B 5 C 6 E 30 F 5 G 3 H 11 I 3 J 5 L 11 識別 3 「(二人以上の世帯)」を含む列: 11 2 行目を飛ばさない: 行数 565 / object 型の列 112 2 行目を飛ばす : 行数 564 / object 型の列 2

💬 列定義書は 112 行で、 列コードの重複は 0。 年度・地域コード・都道府県の 3 列を除く 109 列は、 列コードの頭文字で A(人口・世帯)30 列、 E(教育)30 列、 H(住宅・ごみ)11 列、 L(家計)11 列などに分かれる。 L の 11 列はすべて「(二人以上の世帯)」の家計の値で、 県庁所在市の調査値である。 こうした列の意味は数値の行には書かれていないので、 表を作るときに列定義書として残しておかないと失われる。 2 行目を飛ばさずに読むと項目名の行が 1 行目のデータとして混ざり、 565 行・112 列すべてが object 型になる。 skiprows=[1] で飛ばせば 564 行・文字列は 2 列だけになる。

🐍 キーの列は書式と対応を検査してから使う

🎯 このコードでやること:SSDSE-B-2026 の地域コード(Code)が「R + 都道府県番号 2 桁 + 000」の書式にそろっているか、 番号が 1〜47 を過不足なく覆うか、 コードと県名の対応がどの年度でも変わらないかを、 正規表現と件数で検査する

📥 入力例 SSDSE-B-2026 全 564 行 SSDSE-B-2026 Code Prefecture 2023 R01000 北海道 2023 R13000 東京都 2012 R47000 沖縄県
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])

# 地域コードの書式: 「R」+ 都道府県番号 2 桁 +「000」
ok = df['Code'].str.fullmatch(r'R(\d{2})000')
print('書式に合う行:', int(ok.sum()), '/', len(df))
num = df['Code'].str[1:3].astype(int)
print('都道府県番号の範囲:', num.min(), '〜', num.max(), '/ 種類:', num.nunique())

# コードと県名は 1 対 1 か(どの年度でも同じ対応か)
pairs = df[['Code', 'Prefecture']].drop_duplicates()
print('(Code, 県名) の組の数:', len(pairs))
print('年度ごとの行数:', df.groupby('SSDSE-B-2026').size().unique().tolist())
print(pairs.head(3).to_string(index=False))
📤 実行例(実測) 書式に合う行: 564 / 564 都道府県番号の範囲: 1 〜 47 / 種類: 47 (Code, 県名) の組の数: 47 年度ごとの行数: [47] Code Prefecture R01000 北海道 R02000 青森県 R03000 岩手県

💬 564 行すべてが書式に合い、 都道府県番号は 1〜47 の 47 種類、 (Code, 県名) の組は 47 通りで、 12 年度のどれを見ても同じ対応になっている。 年度ごとの行数もすべて 47。 自分で表を作るときも、 キーの列にはこうした書式の検査を最初に入れておく。 たとえば県庁所在市の表(SSDSE-C)の地域コードは R01100(札幌市)のように末尾 3 桁が 000 でないので、 同じ正規表現で検査すれば「県レベルの表ではない」ことが結合の前に分かる。

🐍 年度をキーに入れてから縦に積む

🎯 このコードでやること:2022 年度と 2023 年度の断面(各 47 行)を縦に積むとき、 年度の列を付けない場合と付けた場合で主キーの検査結果を比べ、 年度を列に持つ横持ちの表に組み替えて前年からの増減を出す

📥 入力例 SSDSE-B-2026 の 2022・2023 年度(各 47 行) Code Prefecture A1101 R13000 東京都 14038000 ← 2022 年度 R13000 東京都 14086000 ← 2023 年度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
cols = ['Code', 'Prefecture', 'A1101']
d22 = df[df['SSDSE-B-2026'] == 2022][cols]
d23 = df[df['SSDSE-B-2026'] == 2023][cols]

# 年度の列を付けずに縦に積む
naive = pd.concat([d22, d23], ignore_index=True)
print('年度なし: 行数', len(naive), '/ Code の重複', int(naive['Code'].duplicated().sum()))
print(naive[naive['Code'] == 'R13000'].to_string(index=False))

# 年度の列を付けてから積み、 (年度, Code) を主キーとして検査する
tidy = pd.concat([d22.assign(年度=2022), d23.assign(年度=2023)], ignore_index=True)
print('年度あり: 行数', len(tidy), '/ (年度, Code) の重複',
      int(tidy.duplicated(['年度', 'Code']).sum()))
wide = tidy.pivot(index='Prefecture', columns='年度', values='A1101')
wide['増減'] = wide[2023] - wide[2022]
print('2022→2023 で総人口が増えた県:', wide.index[wide['増減'] > 0].tolist())
print('減少が最も大きい県:', wide['増減'].idxmin(), f"{wide['増減'].min():,}")
📤 実行例(実測) 年度なし: 行数 94 / Code の重複 47 Code Prefecture A1101 R13000 東京都 14038000 R13000 東京都 14086000 年度あり: 行数 94 / (年度, Code) の重複 0 2022→2023 で総人口が増えた県: ['東京都'] 減少が最も大きい県: 北海道 -48,000

💬 年度の列を付けずに積むと 94 行のうち Code が 47 個重複し、 東京都の 2 行がどちらの年度かを表から判別できなくなる。 年度の列を付けてから積めば (年度, Code) の重複は 0 で、 これが主キーになる。 その表を pivot で年度を列にした横持ちに組み替えると、 2022→2023 年度に総人口が増えたのは東京都だけで、 減少が最も大きいのは北海道の −48,000 人と読める。 縦持ち(1 行 = 1 県 1 年度)で持っておけば、 横持ちへの組み替えはいつでも pivot 1 回でできる。

🐍 追加 Python — 構造化データ作成ハンズオン

SSDSE-B-2026 を題材に、 「読み込み → 検証 → 正規化 → tidy 変換」までを 4 段で体験する。 すべて公的実データのみ使用。

① 読み込みとスキーマ確認

このコードでやること: SSDSE-B-2026.csv を読み込み、 列名と dtype を出力。 構造化データ作成の最初のステップ「スキーマ把握」。

📥 入力データ: data/raw/SSDSE-B-2026.csv(Shift_JIS)

1
2
3
4
import pandas as pd
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='shift_jis', skiprows=[1])
print(df.shape)
print(df.dtypes.head(5))

📤 実行例:

(564, 112) SSDSE-B-2026 int64 Code object Prefecture object A1101 int64 A110101 int64 dtype: object

💬 564 行 × 112 列(47 都道府県 × 12 年度)。 skiprows=[1] で 2 行目の日本語見出しを飛ばしたので、 列名は 1 行目のコードになり、 年度列は 'SSDSE-B-2026'、 県名は 'Prefecture' と読める。 A1101(総人口)などの数値列が int64 で入っているので、 文字列混じりの列は無いと分かる。 「dtype が想定通りか」をまず確認するのが構造化データ作成の鉄則。

② 値の検証(欠損・範囲外チェック)

このコードでやること: 主要列 A1101(総人口)の欠損数と最小値・最大値を確認し、 異常値を検知する。

📥 入力データ: ①の df

1
2
3
print("欠損数:", df['A1101'].isna().sum())
print("最小値:", df['A1101'].min())
print("最大値:", df['A1101'].max())

📤 実行例:

欠損数: 0 最小値: 537000 最大値: 14086000

💬 欠損ゼロ、 最小 53.7 万人(鳥取), 最大 1408 万人(東京)。 妥当な範囲なのでデータは「クリーン」と判定。

③ wide → long への正規化(tidy 化)

このコードでやること: 複数指標列を pandas.melt で縦持ちに変換。 集計やプロットがやりやすい tidy 形式に整える。

📥 入力データ: ①の df

1
2
3
4
5
# ①は skiprows=[1] で読んだので、年の列名は 'SSDSE-B-2026'、県名は 'Prefecture'
subset = (df[df['SSDSE-B-2026'] == 2023][['Prefecture', 'A1101', 'A4101']]
          .rename(columns={'Prefecture': '都道府県'}))
tidy = subset.melt(id_vars='都道府県', var_name='指標', value_name='値')
print(tidy.head(4))

📤 実行例:

都道府県 指標 値 0 北海道 A1101 5092000 1 青森県 A1101 1184000 2 岩手県 A1101 1163000 3 宮城県 A1101 2264000

💬 47 × 2 = 94 行の縦持ちに整形。 これで「指標」をキーにしたフィルタや groupby が直感的に書ける。

④ スキーマ検証付き保存

このコードでやること: pandera で型と値域を宣言し、 違反があれば例外。 構造化データの「契約」を実装。

📥 入力データ: ③の tidy

1
2
3
4
5
6
7
8
9
10
11
import os
os.makedirs('data/processed', exist_ok=True)   # 書き出し先を先に作る
import pandera as pa
schema = pa.DataFrameSchema({
    '都道府県': pa.Column(str),
    '指標':     pa.Column(str, pa.Check.isin(['A1101','A4101'])),
    '値':       pa.Column(int, pa.Check.gt(0)),
})
schema.validate(tidy)
tidy.to_csv('data/processed/tidy_47pref.csv', index=False)
print("OK: スキーマ検証通過")

📤 実行例(実測):

OK: スキーマ検証通過

💬 pandera で型と値域を宣言しておけば、 上流の SSDSE-B-2026.csv が更新されて列構造が変わった瞬間に例外で気付ける。 これが「構造化データを作る」の最重要ポイント。

⚠️ よくある落とし穴

構造化データを作る を実務で扱うとき、 多くの分析者が同じところでつまずきます。 代表的な失敗パターンを先回りで押さえておくと、 後工程のトラブルを大幅に減らせます。

❌ 文字コード問題
Shift_JIS / UTF-8 を取り違えると文字化け。 chardet や明示指定で対処。
❌ HTML 構造変更
Web スクレイピングは「サイト改修で即死」が宿命。 定期検査と例外処理を。
❌ 法的・倫理的問題
robots.txt 遵守、 利用規約確認、 過剰なアクセス禁止(DoS とみなされる)。
❌ 型変換の落とし穴
「1,234」「1234」「千二百三十四」が同じ意味なのに別物扱いされる。 正規化処理を徹底。
❌ OCR の誤読
「1」と「l」、 「0」と「O」を混同しがち。 信頼度を保存し、 重要箇所は人間検査。

⚠️ 数値を文字列のまま残すと、 最大値も順位も静かに壊れる

🎯 このコードでやること:総人口を桁区切り付きの文字列のまま持った場合と、 整数に直した場合で、 最大値・上位 5 県・合計がどう変わるかを比べる(2023 年度 47 都道府県)

📥 入力例 SSDSE-B-2026 の 2023 年度 47 行 Prefecture A1101 総人口_文字列 北海道 5092000 5,092,000 東京都 14086000 14,086,000 香川県 926000 926,000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
d = df[df['SSDSE-B-2026'] == 2023][['Prefecture', 'A1101']].copy()
d['総人口_文字列'] = d['A1101'].map('{:,}'.format)   # Web の表から取ったままの形

print('文字列のまま max  :', d['総人口_文字列'].max(),
      d.loc[d['総人口_文字列'].idxmax(), 'Prefecture'])
print('数値にして max    :', f"{d['A1101'].max():,}",
      d.loc[d['A1101'].idxmax(), 'Prefecture'])
print('文字列で並べた上位 5:',
      d.sort_values('総人口_文字列', ascending=False)['Prefecture'].head(5).tolist())
print('数値で並べた上位 5 :',
      d.sort_values('A1101', ascending=False)['Prefecture'].head(5).tolist())
print('文字列の「合計」の先頭:', d['総人口_文字列'].sum()[:30], '...')
📤 実行例(実測) 文字列のまま max : 926,000 香川県 数値にして max : 14,086,000 東京都 文字列で並べた上位 5: ['香川県', '秋田県', '神奈川県', '和歌山県', '大阪府'] 数値で並べた上位 5 : ['東京都', '神奈川県', '大阪府', '愛知県', '埼玉県'] 文字列の「合計」の先頭: 5,092,0001,184,0001,163,0002,2 ...

💬 文字列のままだと max は「926,000」の香川県になり、 実際に最も多い東京都(14,086,000)ではない。 文字列の比較は先頭の文字から 1 字ずつ行うので、 「9」で始まる 926,000(香川県)・914,000(秋田県)・9,229,000(神奈川県)が「1」で始まる東京都より上に来る。 上位 5 県も香川・秋田・神奈川・和歌山・大阪と、 数値で並べた東京・神奈川・大阪・愛知・埼玉とまるで違う。 合計を取ると数値の足し算ではなく文字列の連結になる。 エラーが出ないまま結果だけが壊れるので、 表を作った直後に dtypes を確かめ、 数値の列が object のまま残っていないかを見る。

⚠️ 丸めの規則を記録しないと、 正しいデータが「不整合」に見える

🎯 このコードでやること:SSDSE-B-2026 の総人口が千人単位に丸められているかを年度別に調べ、 「男 + 女 = 総人口」を厳密な等号と ±1,000 人の許容で検査した結果を比べる(564 行)

📥 入力例 SSDSE-B-2026 全 564 行 年度 Prefecture A1101 A110101(男) A110102(女) 2023 北海道 5092000 2405000 2688000 2020 北海道 5224614 2465088 2759526
1
2
3
4
5
6
7
8
9
10
11
12
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
r = (df['A1101'] % 1000 == 0).groupby(df['SSDSE-B-2026']).mean()
print('総人口が 1,000 の倍数になっている行の割合(年度別):')
print(r[r < 1].to_string(), ' ← この年度以外はすべて 1.0')
diff = df['A110101'] + df['A110102'] - df['A1101']        # 男 + 女 − 総人口
print('男+女−総人口 の分布:')
print(diff.value_counts().sort_index().to_string())
print('厳密に == で検査すると不一致:', int((diff != 0).sum()), '行',
      '(うち 2015・2020 年度:', int(((diff != 0) & df['SSDSE-B-2026'].isin([2015, 2020])).sum()), '行)')
print('±1,000 人の許容で検査すると不一致:', int((diff.abs() > 1000).sum()), '行')
📤 実行例(実測) 総人口が 1,000 の倍数になっている行の割合(年度別): SSDSE-B-2026 2015 0.0 2020 0.0 ← この年度以外はすべて 1.0 男+女−総人口 の分布: -1000 62 0 445 1000 57 厳密に == で検査すると不一致: 119 行 (うち 2015・2020 年度: 0 行) ±1,000 人の許容で検査すると不一致: 0 行

💬 国勢調査の年である 2015・2020 年度は総人口が 1 人単位の値で、 それ以外の 10 年度は千人単位に丸めた推計値になっている。 丸めは男・女・総数それぞれで行われるので、 男 + 女 − 総人口 は 445 行で 0、 119 行で ±1,000 人ずれる。 == で検査すると 119 行が「不整合」と判定されるが、 ずれは全部 1,000 人ちょうどで、 1 人単位の 2015・2020 年度には 1 行も無い。 構造化データを作るときは「単位は人、 推計年は千人単位で丸め」のような丸めの規則もメタデータとして残し、 検査はその規則に合わせた許容幅で書く。

⚠️ 単位の取り違えは、 順位や相関では見つからない

🎯 このコードでやること:2023 年度の出生数(A4101)を、 SSDSE-B-2026 の総人口(人)で割った場合と、 別の資料から「総人口(千人)」として写した値で割った場合で、 人口千人あたり出生数を比べ、 取り込み時の範囲検査(0〜20)で見分けられるかを確かめる

📥 入力例 SSDSE-B-2026 の 2023 年度 47 行 Prefecture A4101(出生数) A1101(総人口, 人) 北海道 24430 5092000 青森県 5696 1184000 別資料の想定: 総人口(千人) 北海道 5092、 青森県 1184 …
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
d = df[df['SSDSE-B-2026'] == 2023][['Prefecture', 'A1101', 'A4101']].copy()

# 別の資料(PDF の表など)から「総人口(千人)」として写した列を想定
d['総人口_千人'] = d['A1101'] // 1000

d['出生率_正'] = d['A4101'] / d['A1101'] * 1000          # 人口千人あたり
d['出生率_誤'] = d['A4101'] / d['総人口_千人'] * 1000     # 単位を取り違えた計算

print(d[['Prefecture', 'A4101', 'A1101', '総人口_千人', '出生率_正', '出生率_誤']]
      .head(3).round(2).to_string(index=False))

# 取り込み時の範囲検査(人口千人あたり出生数は 0〜20 に収まるはず)
for c in ['出生率_正', '出生率_誤']:
    bad = ~d[c].between(0, 20)
    print(f'{c}: 範囲外 {int(bad.sum())} / 47 県, 最小 {d[c].min():.2f}, 最大 {d[c].max():.2f}')

# 並び順や相関だけを見ても、 単位の取り違えは見つからない
print('県の順位がすべて一致:', bool((d['出生率_正'].rank() == d['出生率_誤'].rank()).all()))
print('2 つの列の相関係数:', round(d['出生率_正'].corr(d['出生率_誤']), 6))
📤 実行例(実測) Prefecture A4101 A1101 総人口_千人 出生率_正 出生率_誤 北海道 24430 5092000 5092 4.80 4797.72 青森県 5696 1184000 1184 4.81 4810.81 岩手県 5432 1163000 1163 4.67 4670.68 出生率_正: 範囲外 0 / 47 県, 最小 3.95, 最大 8.55 出生率_誤: 範囲外 47 / 47 県, 最小 3950.77, 最大 8548.37 県の順位がすべて一致: True 2 つの列の相関係数: 1.0

💬 正しく人で割ると北海道 4.80、 47 県の範囲は 3.95〜8.55 で、 範囲検査に引っかかる県は無い。 千人単位の列で割ると北海道 4797.72 のように 1,000 倍になり、 47 県すべてが範囲外になる。 一方で県の順位は完全に一致し、 2 つの列の相関係数は 1.0 なので、 並べ替えや相関を見ただけでは取り違えに気づけない。 別の資料から列を足して表を作るときは、 列定義書に単位を書き、 取り込みの段で値の範囲を検査する。

⚠️ 「-」を 0 に置き換えると、 平均も合計も静かに下がる

🎯 このコードでやること:崩れた表で「-」(未掲載)だった愛知県の出生数を、 欠損(NaN)として扱った場合と 0 に置き換えた場合で、 総人口上位 10 都道府県の出生数の平均と合計がどう変わるかを比べる

📥 入力例 SSDSE-B-2026 の 2023 年度・総人口上位 10 都道府県の出生数 A4101 東京都 86348、 神奈川県 53991、 大阪府 55292、 愛知県「-」、 …
1
2
3
4
5
6
7
8
9
10
11
12
13
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
top = df[df['SSDSE-B-2026'] == 2023].nlargest(10, 'A1101')[['Prefecture', 'A4101']]
true_mean = top['A4101'].mean()

s = top['A4101'].astype('Int64').copy()
s[top['Prefecture'] == '愛知県'] = pd.NA          # 資料では「-」(未掲載)だったセル

print(f'10 都道府県の本当の平均        : {true_mean:,.1f}')
print(f'「-」を欠損 NaN にして平均     : {s.mean():,.1f}  (9 県で平均)')
print(f'「-」を 0 に置き換えて平均      : {s.fillna(0).mean():,.1f}  (10 県で平均)')
print(f'「-」を 0 にした合計            : {int(s.fillna(0).sum()):,}  (本当の合計 {int(top["A4101"].sum()):,})')
📤 実行例(実測) 10 都道府県の本当の平均 : 43,175.5 「-」を欠損 NaN にして平均 : 42,594.8 (9 県で平均) 「-」を 0 に置き換えて平均 : 38,335.3 (10 県で平均) 「-」を 0 にした合計 : 383,353 (本当の合計 431,755)

💬 本当の平均は 43,175.5 人。 「-」を欠損にすると残り 9 県の平均 42,594.8 人で、 愛知県が抜けたぶん少しずれるだけで済む。 0 に置き換えると「愛知県の出生数は 0 人」と記録したことになり、 平均は 38,335.3 人、 合計は本当の 431,755 人より 48,402 人少ない 383,353 人まで下がる。 「値が無い」と「値が 0」は別物なので、 構造化の段階で「-」「…」「x」のような記号を 0 ではなく欠損に変換し、 どの記号を欠損とみなしたかを列定義書に書いておく。

🧠 理解度チェック

  1. Q1. 上の「崩れた表」の数値 2 列(20 セル)で、 出生数の 1 セルが「-」、 残り 19 セルはすべて桁区切り付きの文字列だった。 整形前の F と S はいくつか。
  2. Q2. 整形後に F が 0.95 のまま上がらないのはなぜか。
  3. Q3. 県名「東京」「大阪」「北海道」…をそのまま結合キーにして SSDSE-B-2026 の Prefecture と結合すると、 上位 10 県のうち何行がつながるか。
  4. Q4. 2023 年度の総人口を「14,086,000」のような文字列のまま max にかけると、 どの県が返るか。 またその理由は。
  5. Q5. SSDSE-B-2026 の 564 行で「男 + 女 = 総人口」を == で検査すると何行が不一致になるか。 その不一致は誤りとみなすべきか。
  6. Q6. 2023 年度の出生数を「総人口(千人)」の列で割って人口千人あたり出生数を作ってしまった。 47 県の順位と相関係数で誤りに気づけるか。 気づくにはどうするか。
  7. Q7. 総人口上位 10 都道府県の出生数のうち愛知県だけが「-」だった。 「-」を 0 に置き換えると、 10 県の出生数の平均は本当の 43,175.5 人からいくつになるか。

解答

  1. F = 19 / 20 = 0.95、 S = 0 / 19 = 0.00(桁区切りのせいで数値として読めるセルが無い)。
  2. F は「値が入っているか」を数える。 整形で直せるのは書き方(カンマ・全角)だけで、 元の表に無かった値は作れないから。
  3. 1 行(北海道だけ)。 名寄せの対応表を通すと 10 行すべてつながる。
  4. 香川県(926,000)。 文字列は先頭の文字から比べるので、 「9」で始まる値が「1」で始まる 14,086,000 より大きいと判定される。
  5. 119 行。 推計年の値が男・女・総数それぞれ千人単位に丸められているためのずれ(すべて ±1,000 人ちょうど)で、 誤りではない。 ±1,000 人の許容で検査すれば 0 行。
  6. 気づけない。 値は 1,000 倍になるが、 47 県の順位は完全に一致し相関係数も 1.0 のまま。 「0〜20 に収まるはず」という範囲検査を入れると 47 県すべてが範囲外(3950.77〜8548.37)になって気づける。
  7. 38,335.3 人(合計が 48,402 人少ない 383,353 人になる)。 欠損として扱えば 9 県の平均 42,594.8 人。

🗺 概念マップ

非構造化データ (テキスト / 画像 / PDF / Web) を入力にして、 OCR (文字認識) / 自然言語処理 / Webスクレイピング を経由し、 表 (CSV / Parquet) に落とし込むパイプラインを中心に配置する。 SSDSE-B-2026 のような既に整った表データに到達するまでの典型工程を 1 枚で俯瞰できる。

structured data creation ETL ツール 正規表現 Tabula LLM でのデータ抽出 Schema-on-Read OCR / Camelot

構造化データ作成 (structured data creation) の概念マップは「非構造化 (PDF・画像・自由記述) → 半構造化 (JSON・XML) → 構造化 (CSV / RDB / Parquet)」の変換パイプラインを中心に置く。 SSDSE-B-2026 は公開時点で既に CSV 構造化されているが、 自治体のオープンデータ PDF を OCR + 表抽出 (Camelot / Tabula) で SSDSE 互換テーブルに整える作業が現実の構造化の典型例。

図の 6 つの周辺ノードは、 どれも中心の「構造化データを作る」の工程のどこかを受け持つ道具である。 ETL ツールは取得から格納までの流れ全体、 正規表現はパースと抽出(このページでは地域コードの書式検査にも使った)、 Tabula と OCR / Camelot は PDF や画像の表を行と列に起こす段、 LLM でのデータ抽出は自由文から項目を切り出す段、 Schema-on-Read は「型を決めずにいったん溜め、 読むときに型を当てる」やり方で、 このページの S(スキーマ準拠率)を後回しにする選択にあたる。

🔗 隣接手法への橋渡し

構造化データ作成は ETL パイプラインの「T (Transform)」中核に位置する。 SSDSE-B-2026 のような既存 CSV を起点に分析するだけでなく、 自治体オープンデータ (PDF / HTML テーブル) を pandas DataFrame に正規化し、 SSDSE と同じ「都道府県コード × 指標 × 年」の long format に整える操作が分析の前提になる。

構造化の品質 = 分析の品質。 SSDSE のような「クリーンな状態を起点にできるオープンデータ」は世界的にも稀で、 実務では構造化に分析時間の 6〜8 割を費やす。

🌳 手法選択フロー

非構造化データを SSDSE 形式に揃えるための変換手法選択フロー。

  1. ① 入力形式は何か? PDF (テキスト層あり) → pdfplumber / Camelot。 画像 PDF → Tesseract OCR + 表抽出。 HTML → BeautifulSoup / pandas.read_html。 Excel → pd.read_excel。
  2. ② 出力スキーマは決まっているか? SSDSE-B-2026 互換 (都道府県コード × 指標) なら横持ち wide format。 BI 用に集計したいなら long format。 pd.melt / pd.pivot で相互変換。
  3. ③ 主キーは何か? SSDSE は (年, 地域コード) が主キー。 取り込み元データの粒度 (市区町村別) を都道府県別に集約 (groupby.sum) するか、 そのまま市区町村テーブルを別管理するか決める。

構造化済データ (SSDSE-B-2026 など) を起点にすると分析がすぐ始められる一方、 PDF / 画像 / 自由記述から構造化する場合は ①②③ の各ステップで品質確認 (サンプル目視・型チェック・主キー一意性) が必須。

🎮 触って理解する — 「何を列にするか」が分析の可能性を決める

構造化とは単なる「表への写し替え」ではなく、 変数設計(どの情報を列として残すか・どの粒度で記録するか)という 設計判断 です。 ここでは 架空のカフェ営業日誌 14 日分(以下のデータはすべて架空です)を素材に、 列の選び方と粒度の選び方で「できる分析」がどう変わるかをリアルタイムに体感できます。

📓 生の記録(架空のカフェ営業日誌 — 非構造の自由文)
① どの情報を「列」として取り出す?
② どの粒度で記録する?
📈 売上チャート(バーに触れる/なぞると値を表示)
 
🔍 この設計で「できる分析 / できない分析」
🗑 この設計で失われる情報

🎨 直感 — 列の選択は「未来の質問」への投資

表を作った瞬間に、 その表で答えられる質問の集合が決まります。 「天気」列を捨てれば天気と売上の関係は 二度と 分析できず、 日別を週別に丸めれば曜日効果は復元できません。 逆に「曜日」は日付から後で導出できる — つまり列には 原情報(捨てたら戻らない) と 派生情報(後で計算できる) の 2 種類があり、 構造化で優先して守るべきは原情報です。 SSDSE-B-2026 が (年, 地域コード) × 指標という形で公開されているのも、 「どの県・どの年でも比較できる」ように変数設計された結果です。

⚠️ よくある落とし穴 — 設計段階の失敗は後工程で取り返せない

🚀 発展 — スキーマ設計・tidy data・アノテーション

列の名前・型・制約(欠損可否・値域・主キー)を先に文書化する作業は スキーマ設計 と呼ばれ、 データベースだけでなく CSV 1 枚の分析でも有効です。 整形の理論的支柱は tidy data(①1 観測 = 1 行、 ②1 変数 = 1 列、 ③1 観測単位 = 1 表)。 また、 自由文・画像のような 非構造データ に人手でラベルを付与して構造化する工程は アノテーション と呼ばれ、 機械学習の教師データ作成そのものです。 本ページの体験で言えば「メモ欄から『田中さん来店』を 0/1 列に起こす」作業がまさにアノテーションに相当します。

🧩 深掘り — 整然データ化(tidy data)を実データで体得する

ここまでのウィジェットで「何を列にするか」の設計判断を体感しました。 この節では、 その一歩先である 整然データ化(tidy data 化) を、 実在の SSDSE-B-2026 を素材に追記解説します。 「表にした」だけでは不十分で、 機械が素直に処理できる形 に整えて初めて構造化は完成します。 以下の数値は data/raw/SSDSE-B-2026.csv(cp932, skiprows=[1])の実測値です。

🎨 直感 — 「表形式」と「整然形式」は別物

構造化データを作るとは、 非構造・半構造の生データを 行=観測・列=変数 のテーブルに整える工程です。 ただし人間が読みやすい表(年を横に並べたクロス表など)と、 機械が処理しやすい整然データ(tidy data)は同じではありません。 Wickham(2014) の 整然データ 3 原則を、 SSDSE の実際の並びに当てはめると次のようになります。

原則意味SSDSE-B-2026 での対応
① 1 変数 = 1 列各列はちょうど 1 種類の変数を持つA1101=総人口、 A4101=出生数… と 1 指標 1 列
② 1 観測 = 1 行1 行はちょうど 1 つの観測単位1 行 =(年 × 都道府県)の組。 47×12=564 行
③ 1 観測単位 = 1 表種類の違う観測は別表に分ける人口動態(B)と家計(A/C)等はファイルが分かれる
実測の裏づけ:SSDSE-B-2026 は 564 行 × 112 列で、 df.duplicated(subset=['SSDSE-B-2026','Prefecture']).sum() は 0。 つまり(年, 都道府県)が主キーとして一意に効いており、 原則②が厳密に守られた整然データの好例です。

⚠️ よくある落とし穴(重要)— 8 つの非整然パターン

生データを表に写す途中で、 分析者は同じ罠に繰り返し落ちます。 「表にはなったが tidy ではない」代表的な 8 パターンを、 実データと架空の最小例で示します。

❌ 1. 1 セルに複数の値
架空例:日誌の 1 セルに 6/1(月)晴 と詰め込むと、 日付・曜日・天気のどれでも集計できません。 str.split や正規表現で date / weekday / weather の 3 列に分解します(原則①違反)。
❌ 2. 変数が列見出しに散らばる(最重要)
「年」を列見出しに広げたクロス表は人間には読みやすいが非整然です。 SSDSE を pivot(index='Prefecture', columns='年', values='A1101') すると下表のように 年が列名に化けます。 melt で縦持ちに戻すのが定石(後述の実例参照)。
❌ 3. 観測単位の混在
「都道府県の行」と「全国計の行」を 1 表に混ぜると、 合計が二重計上されます。 SSDSE-B は都道府県のみ(47×12 行)で全国行を含めず、 観測単位を統一しています。 自作表では粒度の違う行を必ず別表 or フラグ列で区別します(原則③違反)。
❌ 4. 型の不統一
「1,234」「1234」「―」が同じ列に混ざると文字列型に落ち、 数値演算が壊れます。 str.replace(',','').astype(int) で正規化。 SSDSE は数値列がすべて整数で入っているため、 112 列中 110 列が数値型(Code/Prefecture のみ文字列)。
❌ 5. 欠損の表現がばらばら
空欄・-・NA・0・9999 が混在すると「本当の 0」と「未観測」を区別できません。 読み込み時に na_values=['-','―','…','NA'] を明示し、 欠測は必ず NaN に統一します。
❌ 6. キー(主キー)の重複
結合や集計の前提が崩れる致命傷。 df.duplicated(subset=キー列).sum() を必ず確認します。 SSDSE-B は(年, 都道府県)で重複 0 件を実測。 スクレイピングでページを二度取得すると簡単に重複するので、 取り込み直後に検査します。
❌ 7. メタデータと値の混在
SSDSE 生ファイルは 1 行目に列コード(A1101)、 2 行目に日本語見出し(総人口)という 2 段ヘッダーで、 2 行目はメタデータであって観測値ではありません。 だから skiprows=[1] でメタ行を外します。 これを外し忘れると全列が文字列化します。 見出し・単位・出所は値の表と分け、 列辞書(メタデータ)として別管理するのが定石。
❌ 8. 正規化不足(1 表に詰め込みすぎ)
「東京」「東京都」「Tokyo」の表記ゆれや、 都道府県名と地域コードを別々に持たない設計は、 結合時に破綻します。 コード(R13000)を主キーに正規化し、 名称は対応表で 1 箇所に集約します。

※ 上記のうち 1・5 は架空の最小例、 2・3・4・6・7 は SSDSE-B-2026 の実データで確認できる論点です。

🧮 実例 — SSDSE を wide ⇄ long 変換する(実測値)

落とし穴 2 の「変数が列見出しに散らばる」を、 実データで往復してみます。 まず 3 県 × 3 年の総人口 A1101 を 年を列に広げた wide 表(非整然)にすると次の通り(実測値):

Prefecture202120222023
大阪府8,806,0008,782,0008,763,000
東京都14,010,00014,038,00014,086,000
鳥取県549,000544,000537,000

この形は「年」という変数が列名に化けており、 groupby や折れ線描画がしづらい。 melt で 縦持ち(long, 整然)へ戻すと、 3×3=9 行の (Prefecture, year, value) 表になります。 逆に pivot で wide へ戻せる、 これが long ⇄ wide 変換です。

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 A1101(総人口) A4101(出生数) A5101(転入者数(日本人移動者)) 北海道 5,092,000 24,430 47,388 東京都 14,086,000 86,348 406,749 沖縄県 1,468,000 12,549 26,410 …(全 47 行)
1
2
3
4
5
6
7
8
import pandas as pd
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
# 2 県(東京都・鳥取県)× 2023 年 の 3 指標を melt で縦持ち(wide → long)
d = df[(df['SSDSE-B-2026'] == 2023) & df['Prefecture'].isin(['東京都', '鳥取県'])]
wide = d[['Prefecture', 'A1101', 'A4101', 'A5101']]
long = wide.melt(id_vars='Prefecture', var_name='indicator', value_name='value')
print(long.to_string(index=False))
print('wide', wide.shape, '-> long', long.shape)
📤 実行例(実測) Prefecture indicator value 東京都 A1101 14086000 鳥取県 A1101 537000 東京都 A4101 86348 鳥取県 A4101 3263 東京都 A5101 406749 鳥取県 A5101 7578 wide (2, 4) -> long (6, 3)

💬 東京都と鳥取県の 2 県 × 3 指標(総人口・出生数・転入者数)の wide 表 (2, 4) を melt すると、1 行に 1 県 1 指標の値を持つ long 表 (6, 3) になる。行数は 2 県 × 3 指標 = 6 で、県名の列はそのまま残り、元の 3 列の列名が indicator 列の値に移った。東京都と鳥取県で総人口は約 26 倍、出生数も約 26 倍だが、転入者数は約 54 倍と差が大きく、long 形式にしておくと指標ごとの比較や groupby がしやすい。

Prefecture indicator value 東京都 A1101 14086000 鳥取県 A1101 537000 東京都 A4101 86348 鳥取県 A4101 3263 東京都 A5101 406749 鳥取県 A5101 7578 wide (2, 4) -> long (6, 3)
読み方:wide の 2 行 × 4 列(うちキー 1 列+指標 3 列)が、 long では 6 行 × 3 列へ。 行数 = 観測数 × 変数数 になるのが melt の本質です。 可視化や機械学習では long、 人が眺める報告表では wide、 と目的で持ち方を切り替えるのが実務の勘所。

🚀 発展 — tidy から ETL・スキーマ・検証へ

整然データ化は単発の整形ではなく、 再現可能なパイプラインの一部として設計すると効きます。 学びを広げる方向を、 本用語集の実在ページへのリンクとともに示します。

💡 実務の一言:「表にできた」で止めず、 duplicated().sum()==0(キー一意)・isna().mean()(欠損率)・dtypes(型)の 3 点を取り込み直後に検査する習慣をつけると、 後工程の事故が激減します。 SSDSE-B-2026 はこの 3 点をすべて満たす「整然データのお手本」です。
❌ 文字コード問題
Shift_JIS / UTF-8 を取り違えると文字化け。 chardet や明示指定で対処。
❌ HTML 構造変更
Web スクレイピングは「サイト改修で即死」が宿命。 定期検査と例外処理を。
❌ 法的・倫理的問題
robots.txt 遵守、 利用規約確認、 過剰なアクセス禁止(DoS とみなされる)。
❌ 型変換の落とし穴
「1,234」「1234」「千二百三十四」が同じ意味なのに別物扱いされる。 正規化処理を徹底。
❌ OCR の誤読
「1」と「l」、 「0」と「O」を混同しがち。 信頼度を保存し、 重要箇所は人間検査。