表の結合 (JOIN / merge) = 2 つ以上の表を「共通のキー」で繋ぎ、列を横方向に増やす操作。データ分析の 80% は結合と前処理に時間を使う。
Prefecture, Year で結合し、相関分析やモデル特徴量を作る。broadcast() ヒント、pandas では set_index 後 join が高速。用語集 → データ前処理 / 関係データ操作 分野 → 表の結合 (Table Join)。SQL の JOIN、pandas の merge() / join() / concat()、R の dplyr::*_join() がすべて同じ概念です。データクレンジング・分散処理 と隣接します。
コンペでは、SSDSE-B-2026 を主、SSDSE-C や他の指標 (RESAS, e-Stat) を副として、共通キー (Prefecture, Year) で結合するシーンが頻出します。本ページでは SSDSE-B と SSDSE-C, 自作データを結合する完全な手順を扱います。
「同じ社員番号で勤怠表と人事表を横並びにする」のがイメージです。社員番号 (= キー) を頼りに、各社員の行に追加情報をくっつけます。
4 種類の結合をベン図で覚える:
| 種類 | 残るキー | 片方しかないキーの扱い | 用途 |
|---|---|---|---|
| INNER | 両方に存在するキーのみ | 捨てる | 確実に両方に値がある分析 |
| LEFT | 左テーブル全部 | 右は NaN で埋める | マスタに付加情報を追加 |
| RIGHT | 右テーブル全部 | 左は NaN で埋める | 右がマスタの場合 |
| OUTER (FULL) | 両方の合計 | 両方とも NaN 埋め | 欠損を含む状態を保持 |
カーディナリティ (関係の多重度):
SSDSE-B-2026 のキー (Prefecture, Year) は 47 × 約 30 ≈ 1,400 行で完全に 1:1。ここに SSDSE-C 等の同粒度データを結合するなら安心です。
自然結合 (natural join):
$$ R \bowtie S = \{ (r, s) \mid r \in R, s \in S, \pi_K(r) = \pi_K(s) \} $$
$\pi_K$ は共通キー $K$ への射影。両表でキーが一致する組のみを残す (INNER JOIN)。
左外部結合:
$$ R \mathbin{\unicode{x27D5}} S = R \bowtie S \cup \{ (r, \mathrm{NULL}) \mid r \in R, \not\exists s \in S: \pi_K(r) = \pi_K(s) \} $$
結合後の行数 (一意キー時):
$$ |R \bowtie S| = \sum_{k \in K_R \cap K_S} 1 = |K_R \cap K_S| $$
キーが重複しているときは積になります。$|R| \cdot |S| / |K|$ の上限。
結合計算量:
| アルゴリズム | 計算量 | 使い所 |
|---|---|---|
| Nested-loop | $O(|R| \cdot |S|)$ | 片方が極小 |
| Sort-merge | $O((|R| + |S|) \log)$ | 事前ソート済み |
| Hash join | $O(|R| + |S|)$ | 片方がメモリ収まる |
| Broadcast join | $O(|R|)$ | 片方が極小 (分散) |
| 記号 | 読み方 | 意味 |
|---|---|---|
| $R, S$ | relation | 表 (リレーション) |
| $\bowtie$ | join | 自然結合 (INNER) |
| $\unicode{x27D5}, \unicode{x27D6}$ | left/right outer join | 外部結合 |
| $\pi_K$ | projection | キー列だけ取り出す射影 |
| $K_R \cap K_S$ | key intersection | 両表で共通するキー集合 |
SSDSE-B-2026 (47都道府県×年度) に、独自に作った「県庁所在地」表 (47行) と「観光客数」表 (一部欠損) を結合します。
| 結合方法 | SSDSE-B (1410行) | 県庁所在地 (47行) | 観光客数 (40県のみ) | 結合後の行数 |
|---|---|---|---|---|
| INNER (3 表) | 1410 | 47 | 40 | 40×30 = 1200 |
| LEFT (B 基準) | 1410 | 47 | 40 | 1410 (7県は NaN) |
| OUTER | 1410 | 47 | 40 | ≥1410 |
→ どの結合を選ぶかで「欠損 7 県をどう扱うか」が変わる。LEFT で残し、後で fillna で粗化することが多い。
① SSDSE-B を読み込み、キーを整える
import pandas as pd
ssdse_b = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='utf-8', skiprows=1)
ssdse_b['Prefecture'] = ssdse_b['Prefecture'].str.strip() # 前後空白除去
ssdse_b['Year'] = ssdse_b['Year'].astype(int)
print(ssdse_b.shape, ssdse_b.columns.tolist()[:6])
② 県庁所在地マスタを LEFT JOIN
capitals = pd.DataFrame({
'Prefecture': ['北海道', '青森県', '岩手県', '宮城県', '東京都', '大阪府', '沖縄県'],
'Capital' : ['札幌', '青森', '盛岡', '仙台', '新宿', '大阪', '那覇'],
})
merged = ssdse_b.merge(capitals, on='Prefecture', how='left')
print(merged[['Prefecture','Year','Capital']].head(8))
print('欠損行:', merged['Capital'].isna().sum()) # マスタにない県の数
③ 観光客数 (一部のみ) を結合し indicator で確認
tour = pd.DataFrame({
'Prefecture': ['北海道', '東京都', '京都府', '沖縄県'] * 3,
'Year' : [2020,2020,2020,2020, 2021,2021,2021,2021, 2022,2022,2022,2022],
'visitors' : [2.1e7, 5.2e8, 4.8e7, 9.4e6,
1.4e7, 3.0e8, 3.2e7, 5.1e6,
2.8e7, 4.5e8, 5.0e7, 8.7e6],
})
joined = merged.merge(tour, on=['Prefecture','Year'], how='left', indicator=True)
print(joined['_merge'].value_counts()) # left_only / both
④ 結合キーの型違いをデバッグ
left = pd.DataFrame({'Year': [2020, 2021, 2022], 'a': [1,2,3]})
right = pd.DataFrame({'Year': ['2020','2021','2022'], 'b': [10,20,30]})
# 型が違うと一切マッチしない!
bad = left.merge(right, on='Year', how='inner')
print('型違い:', bad.shape) # (0, 3)
right['Year'] = right['Year'].astype(int)
good = left.merge(right, on='Year', how='inner')
print('修正後:', good.shape) # (3, 3)
⑤ many-to-many を意図する場合は validate
try:
ssdse_b.merge(capitals, on='Prefecture', how='left',
validate='many_to_one') # OK: 県マスタは 1 行ずつ
print('1:N 結合は安全')
except Exception as e:
print(e)
⑥ index 利用で高速 join
a = ssdse_b.set_index(['Prefecture','Year']) b = tour.set_index(['Prefecture','Year']) %timeit a.join(b, how='left') # 高速 (B+ tree) %timeit ssdse_b.merge(tour, on=['Prefecture','Year'], how='left') # やや遅
⑦ SQL でやる場合 (DuckDB)
import duckdb q = """ SELECT b.Prefecture, b.Year, b.Total_population, c.Capital FROM ssdse_b b LEFT JOIN capitals c USING (Prefecture) WHERE b.Year >= 2015 ORDER BY b.Prefecture, b.Year """ result = duckdb.query(q).to_df() print(result.head())
df.dtypes を必ず確認、または validate オプションを使う。str.strip()、Unicode 正規化 (NFKC)、辞書による正規化が必要。validate='one_to_one' や drop_duplicates() を事前確認。indicator=True で left_only/right_only を可視化。| 分析テーマ | 主テーブル | 結合先 | キー | 結合種別 |
|---|---|---|---|---|
| 都道府県の生産性と教育 | SSDSE-B 県民所得 | SSDSE-C 進学率 | (Prefecture, Year) | INNER |
| 高齢化率と医療費 | SSDSE-B | 国民健康保険データ | 市町村コード | LEFT |
| 気候と農業 | 気象庁 47 県月別 | 作物統計 | (Pref, Year, Month) | INNER |
| 観光と為替 | 訪日外国人 (国別) | 為替レート (日次) | Date (日付丸めが必要) | ASOF JOIN |
Q1. concat と merge の違いは?
A. concat は単純な縦/横の連結 (キー一致を見ない)、merge はキーに基づく結合。同じ列構造の年度別ファイルを縦に重ねるのは concat。
Q2. join と merge は?
A. pandas の join は index ベース、merge は列ベース。事前に set_index しているなら join が速い。
Q3. 時系列で「最も近い時刻」で結ぶには?
A. pd.merge_asof または DuckDB の ASOF JOIN を使う。SSDSE のような年単位なら通常の merge で済む。
Q4. 結合後にメモリが足りない
A. 不要列を事前に usecols で落とす、category 型化、Dask/DuckDB に切り替え、chunk 処理。
Q5. 結合の検証方法
A. (1) 結合前後の行数、(2) indicator で left_only/right_only、(3) キー列の nunique 比較、(4) スポットチェックを毎回。
表の操作
├─ 縦方向 (concat / UNION)
└─ 横方向 (JOIN) ★
├─ INNER : 共通だけ
├─ LEFT : 左を残す
├─ RIGHT : 右を残す
├─ OUTER : 両方
├─ CROSS : デカルト積
├─ SEMI : 左の絞り込み
├─ ANTI : 左の不在
└─ ASOF : 時系列近傍
表の結合 ★
├─ アルゴリズム: nested loop / hash / sort-merge / broadcast
├─ キー設計: 単一 / 複合 / 代理キー / 自然キー
└─ 落とし穴: 型不一致 / 重複 / NULL / 文字ゆらぎ