構造化データを作る作業は「取得 → パース → 抽出 → 正規化 → 検証」の順に進む。 下のチップは、 その各段で出てくる用語である。 後半の 5 つ(充填率・スキーマ準拠率・名寄せ・列定義書・丸めの規則)は、 このページで SSDSE-B-2026 を使って実際に数値を出している。
🍰 まずはやさしく
バラバラな情報を表にまとめることです。
データを分析しやすくするために使います。
SNSの書き込みをリストにするような作業です。
この章では、変換の手順について読みます。
最も忙しい読者のために、 まず結論だけまとめます。 詳細は以下のセクションへ:
🍰 まずはやさしく
データの整理整頓のような作業です。
そのままでは計算できないデータを直します。
Webサイトの表をコピーして集計する時です。
この章では、どんな場面で使うかを読みます。
「Web ページの表から都道府県データを取りたい」 「PDF レポートの数値を集計したい」 — そのままでは分析できないデータを、 表形式 に直す作業。 SSDSE のような既に整った CSV は 誰かが先にこの作業をしてくれている。
このページの読み方:まず 30秒結論 と 直感 を読み、 必要に応じて 数式 や 計算例、 落とし穴 に進んでください。
🍰 まずはやさしく
料理の下準備のようなものです。
分析という調理を始めるために行います。
PDFの数字を書き出して表にする作業です。
この章では、データの整え方を読みます。
料理に喩えるなら 下処理。 魚を捌き、 野菜を切り、 計量する — その後の調理(分析)が始められる状態にすること。
典型的なソース:
非構造化データを「行×列のテーブル」に整える工程は分析の前提作業。 ここでは「データ構造のスペクトル」「PDF→CSV 変換フロー」「Tidy Data の 3 原則」を視覚化する。
🍰 まずはやさしく
データを表にするためのルールです。
正確な分析結果を出すために使います。
表記のゆれを一つにまとめる作業です。
この章では、具体的な5つの工程を読みます。
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.05 | 0.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 つを示す。
気象庁の HTML 表から都道府県の気温データを取得する例の構造:
requests.get(url) で HTML 取得BeautifulSoup で <table> を見つける<tr> を列に分解23.5 (float)に変換to_csv で保存合成ログ (生テキスト 100 行) から構造化 CSV のレコード数を計算する。
| 列 | 型 | 例 |
|---|---|---|
| timestamp | datetime | 2026-06-03 10:00 |
| user_id | int | 1001 |
| action | str | login |
| status | int | 200 |
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]}") |
💬 手計算 (Step 3) 400 と Python 出力が完全一致。
🎯 このコードでやること:SSDSE-B-2026 の 2023 年度から総人口上位 10 都道府県を取り、 県名の「都・府・県」を落とし、 数値を桁区切り付きの文字列にし、 1 セルを「-」、 1 セルを全角数字にした「崩れた表」を作る。 それを整形して F と S を測り、 県名で SSDSE-B-2026 に結合できるかを確かめる
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)) |
💬 整形前は 20 セル中 19 セルに値があるので F = 0.95 だが、 桁区切りと全角数字のせいで数値として読めるセルが 1 つも無く S = 0.00。 カンマを外し全角を半角にすると S = 1.00 になる一方、 「-」だった 1 セルは欠損のまま残り F は 0.95 のまま。 県名をそのまま結合キーにすると、 「都・府・県」を含まない北海道の 1 行しか SSDSE-B-2026 と一致しない。 「東京→東京都」の対応表で名寄せしてから結合すると 10 行すべてがつながり、 整形後の総人口は 10 行とも元の A1101 と一致した。 取得→整形→名寄せ→照合の各段で件数を数えておくと、 どこで行が落ちたかがすぐ分かる。
最小再現コード。 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'), 'バイト') |
💬 HTML から取り出した直後は 3 列とも dtype が O(文字列)で、「14,086,000」のままでは足し算も並べ替えもできない。カンマを外して int64 に変えると東京都 14086000・出生数 86348 のような数値列になり、5 行 3 列の表として 157 バイトの CSV に保存できた。Web の表を扱うときは、取り出した直後に dtypes を見て文字列のまま残っている数値列がないかを確かめる。
各コードブロックは 🎯 目的 / 📥 入力 / 🐍 コード / 📤 出力 / 💬 解説 の 5 要素セット。 SSDSE-B-2026 の 47 都道府県データ(2023 年)に実値計算して結論まで導く。
🎯 このコードでやること:SSDSE-B-2026.csv(Shift_JIS、 2 行ヘッダー)を Python で読み込み、 2023 年・47 都道府県だけ抜き出して構造化済み DataFrame を作る
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)) |
💬 結果の読み方:47 行に揃った時点で構造化は成功。 もし「行数: 46」なら ある県のデータが落ちている。 もし 564 のままなら年度フィルタが効いていない。
🎯 このコードでやること:Wikipedia の表をスクレイピングし、 列名を SSDSE の Prefecture にそろえ、 面積を数値の列にして、 SSDSE と県名で結合できる形に整える流れ(取得→パース→正規化→検証)の最小例。 結合そのものはこのコードではまだしない
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)) |
💬 結果の読み方:例の表示のように、北海道 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 年データで計算する
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.000 は欠損ゼロ、 S=0.98 は 112 列中 110 列が数値型。 残り 2 列は Code と Prefecture(文字列)。 整備済み公的データの理想形。
手を動かさないと身につかない。 5 問とも SSDSE-B-2026 を必ず触る前提で設計。
🎯 このコードでやること:SSDSE-B-2026 の 1 行目(列コード)と 2 行目(日本語の項目名)を、 データ本体とは別の「列定義書」として取り出し、 列の分野ごとの数を数える。 あわせて、 2 行目を飛ばさずに読んだ場合に型がどう崩れるかを確かめる
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())) |
💬 列定義書は 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 を過不足なく覆うか、 コードと県名の対応がどの年度でも変わらないかを、 正規表現と件数で検査する
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 行すべてが書式に合い、 都道府県番号は 1〜47 の 47 種類、 (Code, 県名) の組は 47 通りで、 12 年度のどれを見ても同じ対応になっている。 年度ごとの行数もすべて 47。 自分で表を作るときも、 キーの列にはこうした書式の検査を最初に入れておく。 たとえば県庁所在市の表(SSDSE-C)の地域コードは R01100(札幌市)のように末尾 3 桁が 000 でないので、 同じ正規表現で検査すれば「県レベルの表ではない」ことが結合の前に分かる。
🎯 このコードでやること:2022 年度と 2023 年度の断面(各 47 行)を縦に積むとき、 年度の列を付けない場合と付けた場合で主キーの検査結果を比べ、 年度を列に持つ横持ちの表に組み替えて前年からの増減を出す
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 個重複し、 東京都の 2 行がどちらの年度かを表から判別できなくなる。 年度の列を付けてから積めば (年度, Code) の重複は 0 で、 これが主キーになる。 その表を pivot で年度を列にした横持ちに組み替えると、 2022→2023 年度に総人口が増えたのは東京都だけで、 減少が最も大きいのは北海道の −48,000 人と読める。 縦持ち(1 行 = 1 県 1 年度)で持っておけば、 横持ちへの組み替えはいつでも pivot 1 回でできる。
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 列(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()) |
📤 実行例:
💬 欠損ゼロ、 最小 53.7 万人(鳥取), 最大 1408 万人(東京)。 妥当な範囲なのでデータは「クリーン」と判定。
このコードでやること: 複数指標列を 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)) |
📤 実行例:
💬 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: スキーマ検証通過") |
📤 実行例(実測):
💬 pandera で型と値域を宣言しておけば、 上流の SSDSE-B-2026.csv が更新されて列構造が変わった瞬間に例外で気付ける。 これが「構造化データを作る」の最重要ポイント。
構造化データを作る を実務で扱うとき、 多くの分析者が同じところでつまずきます。 代表的な失敗パターンを先回りで押さえておくと、 後工程のトラブルを大幅に減らせます。
chardet や明示指定で対処。🎯 このコードでやること:総人口を桁区切り付きの文字列のまま持った場合と、 整数に直した場合で、 最大値・上位 5 県・合計がどう変わるかを比べる(2023 年度 47 都道府県)
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」の香川県になり、 実際に最も多い東京都(14,086,000)ではない。 文字列の比較は先頭の文字から 1 字ずつ行うので、 「9」で始まる 926,000(香川県)・914,000(秋田県)・9,229,000(神奈川県)が「1」で始まる東京都より上に来る。 上位 5 県も香川・秋田・神奈川・和歌山・大阪と、 数値で並べた東京・神奈川・大阪・愛知・埼玉とまるで違う。 合計を取ると数値の足し算ではなく文字列の連結になる。 エラーが出ないまま結果だけが壊れるので、 表を作った直後に dtypes を確かめ、 数値の列が object のまま残っていないかを見る。
🎯 このコードでやること:SSDSE-B-2026 の総人口が千人単位に丸められているかを年度別に調べ、 「男 + 女 = 総人口」を厳密な等号と ±1,000 人の許容で検査した結果を比べる(564 行)
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()), '行') |
💬 国勢調査の年である 2015・2020 年度は総人口が 1 人単位の値で、 それ以外の 10 年度は千人単位に丸めた推計値になっている。 丸めは男・女・総数それぞれで行われるので、 男 + 女 − 総人口 は 445 行で 0、 119 行で ±1,000 人ずれる。 == で検査すると 119 行が「不整合」と判定されるが、 ずれは全部 1,000 人ちょうどで、 1 人単位の 2015・2020 年度には 1 行も無い。 構造化データを作るときは「単位は人、 推計年は千人単位で丸め」のような丸めの規則もメタデータとして残し、 検査はその規則に合わせた許容幅で書く。
🎯 このコードでやること:2023 年度の出生数(A4101)を、 SSDSE-B-2026 の総人口(人)で割った場合と、 別の資料から「総人口(千人)」として写した値で割った場合で、 人口千人あたり出生数を比べ、 取り込み時の範囲検査(0〜20)で見分けられるかを確かめる
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)) |
💬 正しく人で割ると北海道 4.80、 47 県の範囲は 3.95〜8.55 で、 範囲検査に引っかかる県は無い。 千人単位の列で割ると北海道 4797.72 のように 1,000 倍になり、 47 県すべてが範囲外になる。 一方で県の順位は完全に一致し、 2 つの列の相関係数は 1.0 なので、 並べ替えや相関を見ただけでは取り違えに気づけない。 別の資料から列を足して表を作るときは、 列定義書に単位を書き、 取り込みの段で値の範囲を検査する。
🎯 このコードでやること:崩れた表で「-」(未掲載)だった愛知県の出生数を、 欠損(NaN)として扱った場合と 0 に置き換えた場合で、 総人口上位 10 都道府県の出生数の平均と合計がどう変わるかを比べる
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()):,})') |
💬 本当の平均は 43,175.5 人。 「-」を欠損にすると残り 9 県の平均 42,594.8 人で、 愛知県が抜けたぶん少しずれるだけで済む。 0 に置き換えると「愛知県の出生数は 0 人」と記録したことになり、 平均は 38,335.3 人、 合計は本当の 431,755 人より 48,402 人少ない 383,353 人まで下がる。 「値が無い」と「値が 0」は別物なので、 構造化の段階で「-」「…」「x」のような記号を 0 ではなく欠損に変換し、 どの記号を欠損とみなしたかを列定義書に書いておく。
解答
非構造化データ (テキスト / 画像 / PDF / Web) を入力にして、 OCR (文字認識) / 自然言語処理 / Webスクレイピング を経由し、 表 (CSV / Parquet) に落とし込むパイプラインを中心に配置する。 SSDSE-B-2026 のような既に整った表データに到達するまでの典型工程を 1 枚で俯瞰できる。
構造化データ作成 (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 形式に揃えるための変換手法選択フロー。
pdfplumber / Camelot。 画像 PDF → Tesseract OCR + 表抽出。 HTML → BeautifulSoup / pandas.read_html。 Excel → pd.read_excel。pd.melt / pd.pivot で相互変換。groupby.sum) するか、 そのまま市区町村テーブルを別管理するか決める。構造化済データ (SSDSE-B-2026 など) を起点にすると分析がすぐ始められる一方、 PDF / 画像 / 自由記述から構造化する場合は ①②③ の各ステップで品質確認 (サンプル目視・型チェック・主キー一意性) が必須。
構造化とは単なる「表への写し替え」ではなく、 変数設計(どの情報を列として残すか・どの粒度で記録するか)という 設計判断 です。 ここでは 架空のカフェ営業日誌 14 日分(以下のデータはすべて架空です)を素材に、 列の選び方と粒度の選び方で「できる分析」がどう変わるかをリアルタイムに体感できます。
表を作った瞬間に、 その表で答えられる質問の集合が決まります。 「天気」列を捨てれば天気と売上の関係は 二度と 分析できず、 日別を週別に丸めれば曜日効果は復元できません。 逆に「曜日」は日付から後で導出できる — つまり列には 原情報(捨てたら戻らない) と 派生情報(後で計算できる) の 2 種類があり、 構造化で優先して守るべきは原情報です。 SSDSE-B-2026 が (年, 地域コード) × 指標という形で公開されているのも、 「どの県・どの年でも比較できる」ように変数設計された結果です。
列の名前・型・制約(欠損可否・値域・主キー)を先に文書化する作業は スキーマ設計 と呼ばれ、 データベースだけでなく CSV 1 枚の分析でも有効です。 整形の理論的支柱は tidy data(①1 観測 = 1 行、 ②1 変数 = 1 列、 ③1 観測単位 = 1 表)。 また、 自由文・画像のような 非構造データ に人手でラベルを付与して構造化する工程は アノテーション と呼ばれ、 機械学習の教師データ作成そのものです。 本ページの体験で言えば「メモ欄から『田中さん来店』を 0/1 列に起こす」作業がまさにアノテーションに相当します。
ここまでのウィジェットで「何を列にするか」の設計判断を体感しました。 この節では、 その一歩先である 整然データ化(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)等はファイルが分かれる |
df.duplicated(subset=['SSDSE-B-2026','Prefecture']).sum() は 0。 つまり(年, 都道府県)が主キーとして一意に効いており、 原則②が厳密に守られた整然データの好例です。生データを表に写す途中で、 分析者は同じ罠に繰り返し落ちます。 「表にはなったが tidy ではない」代表的な 8 パターンを、 実データと架空の最小例で示します。
6/1(月)晴 と詰め込むと、 日付・曜日・天気のどれでも集計できません。 str.split や正規表現で date / weekday / weather の 3 列に分解します(原則①違反)。pivot(index='Prefecture', columns='年', values='A1101') すると下表のように 年が列名に化けます。 melt で縦持ちに戻すのが定石(後述の実例参照)。str.replace(',','').astype(int) で正規化。 SSDSE は数値列がすべて整数で入っているため、 112 列中 110 列が数値型(Code/Prefecture のみ文字列)。-・NA・0・9999 が混在すると「本当の 0」と「未観測」を区別できません。 読み込み時に na_values=['-','―','…','NA'] を明示し、 欠測は必ず NaN に統一します。df.duplicated(subset=キー列).sum() を必ず確認します。 SSDSE-B は(年, 都道府県)で重複 0 件を実測。 スクレイピングでページを二度取得すると簡単に重複するので、 取り込み直後に検査します。A1101)、 2 行目に日本語見出し(総人口)という 2 段ヘッダーで、 2 行目はメタデータであって観測値ではありません。 だから skiprows=[1] でメタ行を外します。 これを外し忘れると全列が文字列化します。 見出し・単位・出所は値の表と分け、 列辞書(メタデータ)として別管理するのが定石。R13000)を主キーに正規化し、 名称は対応表で 1 箇所に集約します。※ 上記のうち 1・5 は架空の最小例、 2・3・4・6・7 は SSDSE-B-2026 の実データで確認できる論点です。
落とし穴 2 の「変数が列見出しに散らばる」を、 実データで往復してみます。 まず 3 県 × 3 年の総人口 A1101 を 年を列に広げた wide 表(非整然)にすると次の通り(実測値):
| Prefecture | 2021 | 2022 | 2023 |
|---|---|---|---|
| 大阪府 | 8,806,000 | 8,782,000 | 8,763,000 |
| 東京都 | 14,010,000 | 14,038,000 | 14,086,000 |
| 鳥取県 | 549,000 | 544,000 | 537,000 |
この形は「年」という変数が列名に化けており、 groupby や折れ線描画がしづらい。 melt で 縦持ち(long, 整然)へ戻すと、 3×3=9 行の (Prefecture, year, value) 表になります。 逆に pivot で wide へ戻せる、 これが long ⇄ wide 変換です。
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) |
💬 東京都と鳥取県の 2 県 × 3 指標(総人口・出生数・転入者数)の wide 表 (2, 4) を melt すると、1 行に 1 県 1 指標の値を持つ long 表 (6, 3) になる。行数は 2 県 × 3 指標 = 6 で、県名の列はそのまま残り、元の 3 列の列名が indicator 列の値に移った。東京都と鳥取県で総人口は約 26 倍、出生数も約 26 倍だが、転入者数は約 54 倍と差が大きく、long 形式にしておくと指標ごとの比較や groupby がしやすい。
整然データ化は単発の整形ではなく、 再現可能なパイプラインの一部として設計すると効きます。 学びを広げる方向を、 本用語集の実在ページへのリンクとともに示します。
duplicated().sum()==0(キー一意)・isna().mean()(欠損率)・dtypes(型)の 3 点を取り込み直後に検査する習慣をつけると、 後工程の事故が激減します。 SSDSE-B-2026 はこの 3 点をすべて満たす「整然データのお手本」です。chardet や明示指定で対処。