論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
表の結合
Table Join / Merge
データ前処理 pandas SQL

🔖 キーワード索引

💡 30秒で分かる 📍 文脈 🎨 直感 📐 数式 🔬 記号 🧮 実値計算 🐍 Python ⚠️ 落とし穴 📰 事例 ❓ FAQ 🌐 関連手法 🔗 関連用語 📚 グループ教材 🗺 概念マップ

💡 30秒で分かる結論

表の結合 (JOIN / merge) = 2 つ以上の表を「共通のキー」で繋ぎ、列を横方向に増やす操作。データ分析の 80% は結合と前処理に時間を使う。

📍 あなたが今見ているもの

用語集 → データ前処理 / 関係データ操作 分野 → 表の結合 (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 と他データの結合

SSDSE-B-2026 (47都道府県×年度) に、独自に作った「県庁所在地」表 (47行) と「観光客数」表 (一部欠損) を結合します。

結合方法SSDSE-B (1410行)県庁所在地 (47行)観光客数 (40県のみ)結合後の行数
INNER (3 表)1410474040×30 = 1200
LEFT (B 基準)141047401410 (7県は NaN)
OUTER14104740≥1410

→ どの結合を選ぶかで「欠損 7 県をどう扱うか」が変わる。LEFT で残し、後で fillna で粗化することが多い。

🐍 Python 実装 — SSDSE-B と他データの merge

① 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())

⚠️ よくある落とし穴

❌ キーの型が違う
「年」が片方で int、片方で str だと無音で一致 0 件になる。df.dtypes を必ず確認、または validate オプションを使う。
❌ 県名のゆらぎ
「東京」「東京都」「トウキョウ」が共存。str.strip()、Unicode 正規化 (NFKC)、辞書による正規化が必要。
❌ many-to-many で爆発
想定外の重複キーで行数が 100 倍に。validate='one_to_one'drop_duplicates() を事前確認。
❌ INNER で落ちた行に気づかない
「47都道府県結合したら45県になった」を発見できない。indicator=True で left_only/right_only を可視化。
❌ NULL を含むキーでの結合
NULL = NULL は false (SQL 規約)。結合キーに欠損があると消える。事前に dropna するか、専用フラグ列を作る。

📰 結合のケーススタディ

分析テーマ主テーブル結合先キー結合種別
都道府県の生産性と教育SSDSE-B 県民所得SSDSE-C 進学率(Prefecture, Year)INNER
高齢化率と医療費SSDSE-B国民健康保険データ市町村コードLEFT
気候と農業気象庁 47 県月別作物統計(Pref, Year, Month)INNER
観光と為替訪日外国人 (国別)為替レート (日次)Date (日付丸めが必要)ASOF JOIN

❓ FAQ

Q1. concatmerge の違いは?
A. concat は単純な縦/横の連結 (キー一致を見ない)、merge はキーに基づく結合。同じ列構造の年度別ファイルを縦に重ねるのは concat

Q2. joinmerge は?
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 / 文字ゆらぎ