🍰 まずはやさしく
バラバラの表をつなげるパズルのような操作です。
情報を一つにまとめて分析しやすくするために使います。
出席簿とテスト結果の表を、出席番号でつなげるイメージです。
結合の種類と注意点を短時間で確認しましょう。
複数の表をキーで結合する操作
データ結合 を 30 秒で把握する重要ポイント:
🍰 まずはやさしく
複数の表を共通の項目でつなぐ操作のことです。
バラバラに保存されたデータを1枚の表にするために使います。
スマホの連絡先と、個別のメモ帳を名前でつなげるようなものです。
現場でよく使われる5つの結合パターンを学びましょう。
あなたは今、 データ結合 (Data Join) という、 複数の表 (テーブル) を共通キーで接続して 1 枚にまとめる操作の用語ページを見ています。 SQL の JOIN、 pandas の merge / join / concat に相当する上位概念です。 内部結合、 外部結合 はその下位概念。 SSDSE-B-2026 のような分割された公的統計を統合するときの基本動作です。
日々の分析作業で頻出する結合パターンを 5 つ紹介する。 すべて SSDSE-B-2026 の都道府県データで動かせる。
SSDSE の Code (R13000 等) を都道府県コード一覧と突合。 マスタに無い県は NaN で残し、 後で確認。
「対象 10 県のリスト」と SSDSE 47 県を INNER JOIN すれば、 10 行だけが残る。 SQL の WHERE IN と同等。
SSDSE-A-2025 と SSDSE-B-2026 は対象年度が異なる。 県をキーに OUTER で結合すると「片方にしかない年」を含めて全体像を見られる。
SSDSE は年次データだが、 日次イベントと突合したい場合は merge_asof で「直近の年データ」を引っ張れる。 年度切替の前後の挙動に注意。
merge(..., how='outer', indicator=True) で _merge 列を見て、 'left_only' だけ抽出すれば「A にあって B に無い」差集合が取れる。 SSDSE 47 県と外部 46 県を比較すれば「沖縄だけが浮く」と一目で分かる。
🍰 まずはやさしく
表の横に新しい列を付け足す操作です。
異なる種類のデータを組み合わせて、詳しく調べるために使います。
県の人口の表と、支出の表を県名でつなげるイメージです。
図を使って結合の種類や、よくある失敗例を理解しましょう。
データ結合は「複数の表を共通キー (たとえば都道府県コード) でつなぎ、 横に列を増やす操作」。 SSDSE-B-2026 の人口表 (A1101) と県別の消費支出表 (L3221) を都道府県コードで結合すれば、 1 行 47 都道府県に両方の列が並ぶ。 SQL の JOIN・pandas の merge がこれにあたり、 INNER / LEFT / RIGHT / OUTER のどれを選ぶかで「沖縄が欠落していたらどう扱うか」が決まる。
以下では、 SSDSE-B-2026 の人口表 (47 県) と県別の消費支出表を都道府県コードでつなぐ実例から始め、 INNER / LEFT / RIGHT / OUTER の 4 種を集合論的に整理します。 pandas merge と SQL JOIN の対応、 「キー型不一致 (str vs int) で結合行数 0」「1 対多結合で行数が爆発」「複合キー設定漏れによる重複行」といった頻発トラブルの診断手順まで順に辿ります。
結合の 4 種類は「Venn 図」のどの部分を取るかで表せる。 SSDSE-B-2026 と外部表 (沖縄欠落) を例に、 図と表で対応を示す。
| 結合 | Venn 図イメージ | 日常の比喩 |
|---|---|---|
| INNER | A ∩ B (中央の重なり) | 「両方の名簿に載ってる人だけ呼ぶ」 |
| LEFT | A 全体 (左の円ぜんぶ) | 「A の名簿全員。 B にないなら空欄でいい」 |
| RIGHT | B 全体 (右の円ぜんぶ) | 「B の名簿全員。 A にないなら空欄でいい」 |
| OUTER | A ∪ B (両方の和) | 「どっちかに載ってれば全員呼ぶ」 |
SSDSE-B-2026 (47 県) と「沖縄欠落の 46 県表」では、 INNER = 46 (重なり)、 LEFT = 47 (47 県全部)、 RIGHT = 46 (46 県全部)、 OUTER = 47 (和集合だが両方とも 47 県以下なので 47 が上限)。 これは Venn 図上の各領域の要素数として直感的に納得できる。
🍰 まずはやさしく
共通のキー(目印)を使って表を合体させることです。
別々の場所にあるデータを正しく結びつけるために使います。
学生IDを使って、名簿と成績表をセットにするようなものです。
正しい結合の方法と、ミスを防ぐための確認手順を学びましょう。
複数の表をキーで結合する操作
英語名 Data Join。 同義・関連語:join, merge。
merge、 Spark の join として実装され、 結合タイプ (inner / left / right / outer / cross) を場面で使い分けます。この用語を理解・使用するときは、 次のような前提を意識してください:
このコードでやること: 「結合後に行数が爆発した」事故を防ぐため、 結合前にキーの一意性 × 多重度を組み合わせて期待行数を計算する。 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 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=1, encoding='cp932') left = df[df['年度']==2022][['都道府県','総人口']].copy() right = pd.DataFrame({ '都道府県': ['東京','東京','大阪','愛知','北海道'], '産業': ['IT','金融','卸売','製造','観光'], '従業者比率': [0.32,0.18,0.25,0.40,0.22] }) print(f'left: {len(left)} 行 (キー一意: {left["都道府県"].is_unique})') print(f'right: {len(right)} 行 (キー一意: {right["都道府県"].is_unique})') print(f'right の重複: {right["都道府県"].value_counts().to_dict()}') # 期待行数: left の一致行数 × right の重複数 match_count = left[left['都道府県'].isin(right['都道府県'])].shape[0] print(f'\n期待行数 (inner):') print(f' - 一致 left 行数: {match_count}') print(f' - right の最大重複: {right["都道府県"].value_counts().max()}') merged = left.merge(right, on='都道府県', how='inner') print(f'\n実測行数: {len(merged)} 行') print(merged) |
📤 実行例:
💬 結果の読み方: 期待では「東京が 2 行に展開されて 5 行」のはずが、 実測は 1 行しか出ない。 原因は表記揺れ: right の '東京'・'大阪'・'愛知' は略称で、 SSDSE-B-2026 側は正式名称 '東京都'・'大阪府'・'愛知県'。 文字列が完全一致しないため inner join で全て脱落し、 唯一表記が一致する '北海道' だけが結合された。 match_count も 4 ではなく 1。 これはこのページが繰り返し警告する「キーの表記揺れ」の落とし穴そのもの。 結合前に set(left['都道府県']) & set(right['都道府県']) で共通キーを数え、 略称・正式名称・全角半角を正規化してから結合する癖をつけたい。 pandas の validate='one_to_one' は重複は検知できても表記揺れによる脱落は検知できない点に注意。
'東京都'・'大阪府'・'愛知県' と正式名称に直せば、 当初意図した「left 一意 × right 重複 → 東京が 2 行に展開して計 5 行」の挙動になる。 表記を揃えることが結合成立の前提。2 つのリレーション $R(K, A)$、 $S(K, B)$ に対し、 結合演算 $\bowtie$ は以下のように定義されます。 結合方式 $\theta$ に応じて出力集合が変化します:
$$R \bowtie^\theta_K S = \begin{cases} \{(k,a,b) \mid (k,a)\in R \land (k,b)\in S\} & (\theta=\text{inner}) \\ R \bowtie^{\text{inner}} S \;\cup\; \{(k,a,\text{NA}) \mid (k,a)\in R, k\notin K_S\} & (\theta=\text{left}) \\ R \bowtie^{\text{left}} S \;\cup\; \{(k,\text{NA},b) \mid (k,b)\in S, k\notin K_R\} & (\theta=\text{outer}) \end{cases}$$
| 記号 | 意味 | SSDSE での具体例 |
|---|---|---|
$R$ | 左の表 (主データ) | 人口表 (Code, A1101) |
$S$ | 右の表 (追加データ) | 経済表 (Code, F3101) |
$K$ | 共通キー | Code |
NA | 片側に対応行が無い場合の NaN 補填 | 沖縄が右にしかないと A1101=NaN |
$\theta$ | 結合タイプ | inner / left / right / outer / cross |
$n_R$ と $n_S$ をそれぞれの行数、 $|K_R \cap K_S|$ を共通キー数とすると、 おおまかな行数は次のようになります (両側ユニーク前提):
$$n_{\text{inner}} = |K_R \cap K_S|,\quad n_{\text{left}} = n_R,\quad n_{\text{outer}} = |K_R \cup K_S|,\quad n_{\text{cross}} = n_R \cdot n_S$$
SSDSE-B-2026 47 都道府県を 2 つに分けて結合する典型ケースでは、 共通キー数 = 47 → inner も outer も 47 行。 もし右から 1 件落とせば inner=46、 outer=47。
| 操作 | 方向 | pandas | SQL | 用途 |
|---|---|---|---|---|
| merge | 横 (列方向) | pd.merge | JOIN ON | キーで列追加 |
| join | 横 (列方向) | df.join | JOIN USING | インデックス結合の簡易版 |
| concat | 縦 / 横 | pd.concat | UNION ALL | 同構造の積み上げ |
| append | 縦 | df.append (非推奨) | INSERT | 廃止予定 |
| combine_first | 横 + 上書き | df.combine_first | COALESCE | 欠損補完結合 |
| merge_asof | 横 (近傍) | pd.merge_asof | window join | 時系列キー近似結合 |
astype(str) で揃え、 dtypes 確認。columns を事前に揃える。validate、 事前 dedupe。_x, _y が付くままレポート出力。 対策: suffixes で意味のある名前に。JOIN ... ON による宣言的結合。merge / join / concat / merge_asof。この節では「データ結合 (data join)」を、 単なる pandas メソッド呼び出しではなく 関係代数 (relational algebra) として理解する。 SSDSE-B-2026 都道府県データ (47 都道府県、 12 年間、 564 行 × 112 列) を別の外部表と結合する具体例を通じて、 INNER / LEFT / RIGHT / OUTER の 4 種類が「どのキー集合の和や交を取るか」で完全に区別できることを示す。
表 A のキー集合を $K_A$、 表 B のキー集合を $K_B$ とする。 4 種類の結合は以下の集合演算と一対一に対応する。
関係代数では結合演算子を「⋈ (bowtie)」で表す。 等結合 (equijoin) の形式定義は次の通り。
$$ R \mathbin{\bowtie_{R.k = S.k}} S = \sigma_{R.k = S.k}(R \times S) $$
これは「直積 $R \times S$ を作り、 そのうち $R.k = S.k$ を満たす行のみを選択 $\sigma$ する」という意味。 つまり結合は 直積 + 選択 の合成にすぎない。 ただし直積は $|R| \times |S|$ 行を生むため、 ナイーブに計算すると爆発する。 そこで実装では「ハッシュ結合」「ソートマージ結合」「ネステッドループ結合」などのアルゴリズムが使われる。
| 記号 | 読み方 | 意味 |
|---|---|---|
| $R, S$ | 関係 (リレーション) | 表 1 つを表す。 行集合と属性集合を持つ。 |
| $R.k$ | アール ドット ケー | 表 $R$ の属性 $k$ (例: Prefecture 列) |
| $\bowtie$ | ボウタイ / ジョイン | 結合演算子。 添字に結合条件を書く。 |
| $\sigma_{cond}$ | シグマ | 選択演算子。 条件 $cond$ を満たす行のみ残す。 |
| $\times$ | クロス / 直積 | 全行同士の組合せ。 $|R| \times |S|$ 行に膨張。 |
| $|R|$ | 基数 (カーディナリティ) | 表 $R$ の行数 |
日本語にすると「2 表の全組合せを並べ、 共通キーの値が一致する行だけ残す」となる。 これが INNER JOIN の本質である。 LEFT JOIN は「さらに左表の残り全部に NULL 行を付け足す」という拡張に過ぎない。
pandas.merge の内部は、 データサイズや結合タイプに応じて以下のアルゴリズムを自動選択する。 これを理解しておくと、 大規模データで結合が遅いときに何を変えればよいか判断できる。
| アルゴリズム | 計算量 | 適する状況 | SSDSE-B での実例 |
|---|---|---|---|
| ネステッドループ | $O(|R| \cdot |S|)$ | 小規模 + 非等価条件 | 47 × 4 = 188 比較 |
| ハッシュ結合 | $O(|R| + |S|)$ | 等価結合 + メモリ十分 | 47 + 46 = 93 操作 |
| ソートマージ | $O(|R|\log|R| + |S|\log|S|)$ | 既ソート or 範囲結合 | merge_asof で使用 |
ハッシュ結合では、 小さい方の表を「ハッシュテーブル」に格納し、 大きい方を 1 行ずつスキャンしてマッチを探す。
$$ \text{HashJoin}(R, S) = \bigcup_{s \in S} \{ (r, s) \mid r \in H[s.k] \} $$
ここで $H$ は表 $R$ のキー値をハッシュ関数で索引付けした辞書、 $H[s.k]$ は表 $S$ のキー値 $s.k$ にマッチする $R$ の行集合。 ハッシュ参照は $O(1)$ 平均なので、 全体の計算量は $O(|R|+|S|)$ になる。
「表 $S$ の全行 $s$ について、 ハッシュテーブル $H$ から $s.k$ をキーに引いた結果 $H[s.k]$ にある行 $r$ とを全部組合せてくっつけて、 それを足し合わせたもの」。 直積を全探索する $O(|R| \cdot |S|)$ 比較に対し、 ハッシュ参照 $O(1)$ + 全行スキャン $O(|S|)$ で $O(|R|+|S|)$ に削減できる。 これが pandas が 100 万行同士でも数秒で結合できる理由。
SSDSE-B-2026 (47 都道府県) から、 人口表と経済表を別 CSV と仮定して 5 種類の結合を比較します。
| 結合タイプ | 出力行数 (右に沖縄なし) | A1101 が NaN の件数 | F3101 が NaN の件数 |
|---|---|---|---|
| inner | 46 | 0 | 0 |
| left | 47 | 0 | 1 (沖縄) |
| right | 46 | 0 | 0 |
| outer | 47 | 0 | 1 |
| cross | 47 × 46 = 2,162 | 0 | 0 |
手計算で確認: 右に沖縄が無い場合、 共通キー数 = 46。 inner=46 (両方にある分のみ)、 left は左 47 を全部残すから 47 (沖縄行は F3101=NaN)。 cross は単純積 47×46。
ここでは SSDSE-B-2026 から「都道府県人口 (A1101)」と「合計特殊出生率 (A4103)」を取り出した表 A と、 別途用意した「仮想 IT 投資指数」表 B を結合する。 表 B は意図的に 46 行のみ (沖縄県を欠落) にしてあり、 INNER / LEFT / OUTER で行数がどう変わるかを観察する。
| 結合タイプ | 期待行数 | 理由 | 沖縄の状態 |
|---|---|---|---|
| INNER | 46 | 両表に存在するキー = 46 県 | 消える |
| LEFT (A 基準) | 47 | A の全 47 県保持 | IT_invest が NaN |
| RIGHT (B 基準) | 46 | B の全 46 県保持 | 消える |
| OUTER | 47 | A ∪ B = 47 県 | IT_invest が NaN |
この「事前予測」を必ずやる癖をつけると、 結合ミスに気付きやすい。 行数が予測と異なれば、 キーの重複や型不一致を疑う。
本番コードでは「結合前後の検査」を必ず assert で挟む。 これにより、 上流データが変わったときも即座に異常を検出できる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) A = df[df['SSDSE-B-2026'] == 2023][['Prefecture', 'A1101']].rename(columns={'A1101':'pop'}) B = pd.DataFrame({ 'Prefecture': ['東京都', '大阪府', '北海道', '沖縄県'], 'capital_pop_million': [9.7, 2.7, 1.97, 0.32] }) # [1] 結合前検査 assert A['Prefecture'].is_unique, '表 A のキーが重複している' assert B['Prefecture'].is_unique, '表 B のキーが重複している' assert A['Prefecture'].dtype == B['Prefecture'].dtype, 'キーの型が不一致' print(f'結合前: A={len(A)} 行, B={len(B)} 行') # [2] 結合実行 merged = pd.merge(A, B, on='Prefecture', how='left', validate='one_to_one') # [3] 結合後検査 assert len(merged) == len(A), f'LEFT 結合なのに行数変化: {len(A)} → {len(merged)}' n_matched = merged['capital_pop_million'].notna().sum() print(f'結合後: {len(merged)} 行 (うちマッチ {n_matched} 行)') print(merged[merged['capital_pop_million'].notna()].to_string(index=False)) |
📤 実行結果:
💬 結果の読み方: 4 つの assert により「結合前に問題があれば即停止」する保護網を作っている。 LEFT JOIN は左表の行数を保つはずなので、 結合後に len(merged) != len(A) なら異常。 これらの assert が無い場合、 異常データが下流に流れて分析結果を歪める。
以下の問題は SSDSE-B-2026 を使って実際に解ける。 答えは段階的なヒント付きで掲載。
📥 入力: SSDSE-B-2026 から 2 年分を抽出。 期待出力: 47 行、 列は Prefecture, pop_2010, pop_2023, ratio。
ヒント: SSDSE-B-2026 は 2012-2023 を含む。 2010 年は無いので 2012 年で代用するか、 利用可能な最古年を使う。 year=2012 と year=2023 をそれぞれ抽出し、 Prefecture をキーに INNER JOIN。
📥 入力: 問題 1 の結果 (47 行) + SSDSE-B-2026 の A4103 (合計特殊出生率)。 期待: 減少率と出生率の関係を 47 県で plot。
ヒント: 自己結合で 2023 年 vs 2022 年の人口変化率を出し、 同じ年の出生率を merge で添えて scatter plot。
📥 入力: 上記のコード例。 ヒント: shift(1) は単純に 1 行ずらすだけで「県」境界を超えてしまう。 groupby('Prefecture').shift(1) なら県内で完結。 これは「同じ結果に見えて、 仕組みが違う」典型例。
合成データで Inner/Left/Outer JOIN の結果行数を計算する。
| テーブル | 行数 | キー一致数 |
|---|---|---|
| A (顧客) | 100 | — |
| B (注文) | 80 | — |
| マッチ | — | 70 |
1 2 3 4 5 | A, B, match = 100, 80, 70 print(f"INNER: {match}") print(f"LEFT: {A}") print(f"RIGHT: {B}") print(f"FULL OUTER: {A + B - match}") |
💬 手計算 (Step 2) と Python 出力が完全一致。
SSDSE-B-2026 のような公的統計データを Python で扱う際の基本パターン:
1 2 3 4 5 6 7 8 9 10 11 12 | import pandas as pd import numpy as np # データ読み込み df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1) print(df.shape) print(df.dtypes) print(df.describe()) # 「データ結合」の文脈で扱う場合の例: # 分野: データ処理 # 関連手法は同カテゴリの他用語を参照してください。 |
具体的なコードは データエンジニアリング を参照してください。
分析結果を報告するときに含めるべき情報:
🎯 このコードでやること: SSDSE-B-2026 を 2 つの仮想 CSV に分割し、 inner/left/right/outer/cross の 5 通りの結合を順に実行して行数を比較する。
📥 入力データ (SSDSE-B-2026 抜粋):
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]) df = df[df['SSDSE-B-2026'] == 2023].copy() # 年度を 2023 に固定(この行が無いと全 12 年分が結合され行数が爆発する) df_pop = df[['Code', 'Prefecture', 'A1101']].copy() df_house = df[df['Code'] != 'R47000'][['Code', 'F3101']].copy() for how in ['inner', 'left', 'right', 'outer']: m = pd.merge(df_pop, df_house, on='Code', how=how) print(f'{how:6s}: {m.shape}, F3101 NaN = {m["F3101"].isna().sum()}') cross = pd.merge(df_pop, df_house, how='cross') print('cross :', cross.shape) |
📤 実行結果:
💬 結果の読み方: inner は両方にあるキーのみ → 46 行。 left は左を全部残す → 47 行 (沖縄の F3101 が NaN)。 cross は 47×46=2,162 行に爆発。 用途に応じて使い分ける。
🎯 このコードでやること: SSDSE-B-2026 の SSDSE-B-2026 (年度) 列と Code を 2 段キーにして、 仮想的な「2023 年データ + 2022 年データ」のマルチイヤー結合を模擬する。
📥 入力: 年度列を含む 2 表
1 2 3 4 5 | df_a = df[['SSDSE-B-2026', 'Code', 'A1101']].copy() df_b = df[['SSDSE-B-2026', 'Code', 'F3101']].copy() merged = pd.merge(df_a, df_b, on=['SSDSE-B-2026', 'Code'], how='inner') print('多キー結合 shape:', merged.shape) print(merged.head(2)) |
📤 実行結果:
💬 結果の読み方: 年度 (2023 のみ) と Code (47 個) の組合せは 47 通り。 多キー結合は年度横断分析の基本パターン。
🎯 このコードでやること: 同じ列構造の 2 表 (たとえば 2022 年と 2023 年データ) を縦方向に積み上げ、 時系列パネルを作る。
📥 入力: 同列の上下 47 行ずつ (合計 94 行)
1 2 3 4 5 6 7 8 | df_2023 = df[['Code', 'Prefecture', 'A1101']].copy() df_2022 = df_2023.copy() # ここでは同じ値を使い疑似 2022 年として扱う df_2022['Year'] = 2022 df_2023['Year'] = 2023 panel = pd.concat([df_2022, df_2023], axis=0, ignore_index=True) print('縦連結 shape:', panel.shape) print(panel['Year'].value_counts()) |
📤 実行結果:
💬 結果の読み方: concat は「列方向の merge」ではなく「行方向の積み上げ」。 同じ構造のパネルを長期化するときに使用。
🎯 このコードでやること: indicator=True で「両方 / 左のみ / 右のみ」の内訳を可視化し、 データ不整合を検知する。
📥 入力: 左 47 行、 右 46 行 (沖縄欠落)
1 2 3 4 | diag = pd.merge(df_pop, df_house, on='Code', how='outer', indicator=True) print(diag['_merge'].value_counts()) miss = diag[diag['_merge'] == 'left_only'] print('左にしかない:', miss['Prefecture'].tolist()) |
📤 実行結果:
💬 結果の読み方: 沖縄が左にしか存在しない → inner なら消える。 結合前に必ず indicator で診断するのが鉄則。
customer_id で結合し、 1 顧客あたり 1 行のフルプロファイルを構築。Code で結合し、 47 都道府県の総合カルテを作る。order_id でチェーン結合し、 リードタイムを算出。🎯 このコードでやること: SSDSE-B-2026 から都道府県の 2023 年データを取り出し、 仮想 IT 投資指数表 (46 県、 沖縄欠) と 4 種類で結合し、 行数と欠損値の出現パターンを比較する。
📥 入力データ: 上記の表 A (47 行) と表 B (46 行)。 結合キーは Prefecture 列。
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 | import pandas as pd # 表 A: SSDSE-B-2026 から 2023 年の 47 都道府県を抽出 df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) A = df[df['SSDSE-B-2026'] == 2023][['Prefecture', 'A1101', 'A4103']].copy() A.columns = ['Prefecture', 'population', 'fertility_rate'] print('表 A 行数:', len(A)) # 表 B: 仮想 IT 投資指数 (沖縄欠落の 46 県) prefs = A[A['Prefecture'] != '沖縄県']['Prefecture'].tolist() B = pd.DataFrame({ 'Prefecture': prefs, 'IT_invest_index': [45.2, 32.1, 33.5, 58.7, 36.9, 38.2, 42.5, 52.8, 41.1, 46.3, 65.4, 71.2, 98.4, 88.1, 39.8, 35.2, 44.6, 36.5, 38.9, 48.7, 51.3, 67.8, 84.5, 56.2, 49.1, 76.3, 92.7, 81.4, 53.6, 37.4, 29.8, 31.2, 47.3, 58.9, 42.1, 33.7, 39.5, 41.8, 36.2, 64.5, 38.1, 35.6, 44.2, 40.7, 32.5, 35.1] }) print('表 B 行数:', len(B)) # 4 種類の結合 inner = pd.merge(A, B, on='Prefecture', how='inner', validate='one_to_one') left = pd.merge(A, B, on='Prefecture', how='left', validate='one_to_one') right = pd.merge(A, B, on='Prefecture', how='right', validate='one_to_one') outer = pd.merge(A, B, on='Prefecture', how='outer', validate='one_to_one') for name, df_j in [('INNER', inner), ('LEFT', left), ('RIGHT', right), ('OUTER', outer)]: n_rows = len(df_j) n_nan_it = df_j['IT_invest_index'].isna().sum() n_nan_pop = df_j['population'].isna().sum() print(f'{name:6s}: 行数={n_rows}, IT_NaN={n_nan_it}, 人口_NaN={n_nan_pop}') |
📤 実行結果:
💬 結果の読み方: 予測通り 4 種類の結合は 46, 47, 46, 47 行になる。 LEFT と OUTER は 47 行で、 沖縄県の IT_invest_index が NaN として表れる (IT_NaN=1)。 INNER と RIGHT は沖縄が消えるため 46 行で NaN は出ない。 validate='one_to_one' を付けたことで もしキーが重複していれば即エラーになる。 これは「想定外の多対多結合による行数爆発」を防ぐ最重要オプション。
🎯 このコードでやること: 結合キーの「データ型」が片方は文字列、 もう片方は数値だと、 値が同じに見えても結合できないことを SSDSE-B-2026 で再現する。
📥 入力データ: SSDSE-B-2026 の Code 列 (例: R13000 = 東京都) を意図的に文字列と整数の双方で持つ 2 表を用意する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) A = df[df['SSDSE-B-2026'] == 2023][['Code', 'Prefecture', 'A1101']].copy() A['Code_str'] = A['Code'].astype(str) A['Code_num'] = A['Code'].str.replace('R', '').str.replace('000', '').astype(int) # 表 B: Code を整数で持つ B = pd.DataFrame({ 'Code_num': [1, 13, 27, 47], 'capital_name': ['札幌市', '新宿区', '大阪市', '那覇市'] }) # 失敗例: 文字列キーで結合しようとすると 0 行に A_str = A[['Code_str', 'Prefecture']].copy() B_str = B.copy() B_str['Code_str'] = B_str['Code_num'].astype(str) # '1','13','27','47' fail = pd.merge(A_str, B_str[['Code_str','capital_name']], on='Code_str', how='inner') print(f'失敗例 (型は str 同士だが値が違う): {len(fail)} 行') # 成功例: 数値キーに揃える success = pd.merge(A[['Code_num', 'Prefecture']], B, on='Code_num', how='inner') print(f'成功例 (整数キーで揃えた): {len(success)} 行') print(success.to_string(index=False)) |
📤 実行結果:
💬 結果の読み方: 表 A の Code 列は R01000, R13000, R27000, R47000 形式の文字列なのに、 表 B の Code_str は '1', '13', '27', '47'。 「文字列同士」なので Python の型エラーは出ないが、 値が違うため 0 行しかマッチしない。 これが現場で最も多い失敗。 対策は (1) 結合前に必ず df.dtypes を確認、 (2) サンプル値を print(A['key'].head().tolist()) で目視。
🎯 このコードでやること: SSDSE-B-2026 の 12 年間×47 都道府県 = 564 行に対し、 「2018-2020 のサブセット」と「2021-2023 のサブセット」を別々に作り、 縦結合 (concat) と横結合 (merge) の両方を試して違いを示す。
📥 入力データ: SSDSE-B-2026 のフル CSV。 564 行 × 112 列。
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 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) # サブセット 1: 2018-2020 の人口 sub1 = df[df['SSDSE-B-2026'].between(2018, 2020)][ ['SSDSE-B-2026', 'Prefecture', 'A1101'] ].rename(columns={'SSDSE-B-2026': 'year', 'A1101': 'pop'}) # サブセット 2: 2021-2023 の人口 sub2 = df[df['SSDSE-B-2026'].between(2021, 2023)][ ['SSDSE-B-2026', 'Prefecture', 'A1101'] ].rename(columns={'SSDSE-B-2026': 'year', 'A1101': 'pop'}) print('サブセット 1 行数:', len(sub1)) # 3 年 x 47 県 = 141 print('サブセット 2 行数:', len(sub2)) # 3 年 x 47 県 = 141 # 縦結合: concat (年が異なる行を縦に積む) vertical = pd.concat([sub1, sub2], ignore_index=True) print('縦結合行数:', len(vertical)) # 282 # 横結合: merge (年も県も同じ場合のみマッチ → 共通行ゼロ) horizontal_keys = pd.merge( sub1, sub2, on=['year', 'Prefecture'], how='inner' ) print('横結合 (年・県キー) 行数:', len(horizontal_keys)) # 0 (年が重ならない) # 県だけで横結合 → 行数爆発 horizontal_pref = pd.merge( sub1, sub2, on='Prefecture', how='inner', suffixes=('_18-20', '_21-23') ) print('県のみで横結合 行数:', len(horizontal_pref)) # 3 x 3 x 47 = 423 # 期待される正しい操作: 縦結合 + ピボット wide = vertical.pivot(index='Prefecture', columns='year', values='pop') print('ピボット後 形状:', wide.shape) # (47, 6) print(wide.head(3)) |
📤 実行結果:
💬 結果の読み方: 「縦結合 (concat)」は年を区別したまま積み上げるので 141 + 141 = 282 行になる。 「年と県の複合キーで横結合」は両サブセットに共通する (year, Prefecture) ペアが存在しないため 0 行。 「県のみで結合」は 3 × 3 × 47 = 423 行に爆発する。 時系列を扱うときの定石は「縦結合 → ピボット (pivot)」で、 行が県、 列が年の「ワイド形式」に変形する流れ。 47 県 × 6 年 = (47, 6) の表が得られる。
🎯 このコードでやること: SSDSE-B-2026 の都道府県人口を「同じ表の 1 年ずれ」で自己結合し、 各県の年次変化率を求める。 これは「SELF JOIN」と呼ばれる結合の応用例。
📥 入力データ: SSDSE-B-2026 全 564 行から Prefecture, Year, 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 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) sub = df[['SSDSE-B-2026', 'Prefecture', 'A1101']].rename( columns={'SSDSE-B-2026': 'year', 'A1101': 'pop'} ) # 1 年ずらした表を作成 (year+1 をキーにする = 翌年と結合) shifted = sub.copy() shifted['year'] = shifted['year'] + 1 shifted = shifted.rename(columns={'pop': 'pop_prev'}) # 自己結合 (現在年と前年を横にくっつける) merged = pd.merge(sub, shifted, on=['year', 'Prefecture'], how='inner') # 変化率計算 merged['change_pct'] = (merged['pop'] - merged['pop_prev']) / merged['pop_prev'] * 100 # 直近 2023 年の上位と下位 y2023 = merged[merged['year'] == 2023].sort_values('change_pct') print('2023 年 人口減少 TOP5:') print(y2023.head(5)[['Prefecture','pop_prev','pop','change_pct']].to_string(index=False)) print('\n2023 年 人口増加 TOP5:') print(y2023.tail(5)[['Prefecture','pop_prev','pop','change_pct']].to_string(index=False)) |
📤 実行結果:
💬 結果の読み方: 自己結合により「現在年と前年」が 1 行に並び、 引き算で変化率が得られる。 2023 年で人口が増えたのは東京都 (+0.34%) のみ、 他はすべて減少。 秋田・青森が -1.6% で最大減少。 これは SELF JOIN の典型応用で、 「同じ表を時間方向にずらしてくっつける」という考え方は時系列分析の基本テクニック。
🎯 このコードでやること: SSDSE-B-2026 の 47 県と外部 46 県の差集合 (沖縄のみ) を見つける。 「アンチ・ジョイン」と呼ばれる、 SQL の NOT IN や NOT EXISTS に相当する操作。
📥 入力データ: SSDSE-B-2026 から抽出した 2023 年 47 県と、 沖縄を除外した 46 県表。
1 2 3 4 5 6 7 8 9 10 11 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) A = df[df['SSDSE-B-2026'] == 2023][['Prefecture']].copy() B = A[A['Prefecture'] != '沖縄県'].copy() # OUTER + indicator でアンチ・ジョイン merged = pd.merge(A, B, on='Prefecture', how='outer', indicator=True) left_only = merged[merged['_merge'] == 'left_only'] print(f'A にあって B に無い県: {len(left_only)} 件') print(left_only) |
📤 実行結果:
💬 結果の読み方: 47 県 - 46 県 = 1 県の差。 indicator=True で生成される _merge 列が 'left_only' なら左のみ、 'right_only' なら右のみ、 'both' なら両方に存在することを示す。 これは「マスタとトランザクションの突合で発生する未登録エンティティ検出」など、 現場で頻出する。
pandas の merge は内部的に hash join を使うが、 大規模データ (Spark / Dask / DuckDB) では「片方が極端に小さい / 両方ソート済み / 両方とも巨大」など状況に応じて broadcast join、 hash join、 sort-merge join が使い分けられる。 ここでは三戦略の計算量と適用場面を比較し、 SSDSE-B-2026 を題材に pandas merge の挙動を確認する。
| 戦略 | 時間計算量 | 空間計算量 | 適用場面 |
|---|---|---|---|
| Broadcast Join | $O(N + M)$ | $O(M)$ 全 worker に複製 | 片方が極小 (~10MB 以下)。 マスタテーブル付与 |
| Hash Join | $O(N + M)$ 期待値 | $O(\min(N,M))$ | 中規模、 等値結合 (=)。 pandas merge のデフォルト |
| Sort-Merge Join | $O(N \log N + M \log M)$ | $O(1)$ (in-place) | 両方巨大かつソート済み。 範囲結合・時系列 asof |
意味: $N, M$ は左右テーブルの行数。 Broadcast は小テーブルを全 worker にコピーするためメモリ消費は worker 数倍に膨らむが、 シャッフルが発生しない。 Hash join は片方からハッシュテーブルを作りもう片方を順次プローブするため、 メモリは小さい方の表サイズに比例する。 Sort-Merge join は両方をソートしてから線形マージするため、 範囲条件・最近傍マッチ(merge_asof)に強い。
🎯 このコードでやること: SSDSE-B-2026 を年度別の二テーブルに分割し、 (1) 通常の等値結合と (2) merge_asof による最近傍時系列結合を比較する。 後者は sort-merge join の典型例。
📥 入力例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=1, encoding='cp932') A = df[df['年度']==2023][['都道府県','総人口']].copy() A['Year'] = 2024 B = df[df['年度']==2023][['都道府県','消費支出(二人以上の世帯)']].copy() B['Year'] = 2026 # (1) 通常 merge (hash join) inner = A.merge(B, on='都道府県', how='inner') print(f'hash join 結果: {len(inner)} 行') # (2) merge_asof (sort-merge join, 時系列最近傍) A2 = A.sort_values('Year') B2 = B.sort_values('Year') asof = pd.merge_asof(A2, B2, on='Year', by='都道府県', direction='nearest') print(f'merge_asof 結果: {len(asof)} 行') print(asof.head(3)) |
📤 実行結果:
💬 結果の読み方: merge は厳密一致のみだが、 merge_asof は direction='nearest' で最も近い時点を当てに行く。 株価データやセンサーログのように「サンプリング間隔が不規則」な時系列結合で必須の機能。 Dask / Spark では同じロジックが broadcast_hint や sort_merge_join として明示できる。
.memory_usage() で確認。direction='backward'(前方の最後の値)と 'forward' を取り違えると、 ルックアヘッド(未来情報リーク)が起きる。 学習データ作成では必ず backward。on を指定し忘れて how='cross' 相当の挙動になると行数が積になる。 47 × 47 = 2,209 程度なら無害だが、 100 万 × 100 万 = 1012 で即死。関連: ハッシュテーブル / 時系列 / Dask / Apache Spark / DuckDB



データ結合 (JOIN) は SQL でも pandas でも 1 行で書ける単純なように見えるが、 ETL パイプラインを止める障害の上位 3 位に必ず入る。 結合キーが揺れる、 重複が起こる、 行数が予期せず増減する、 NULL 処理が不統一、 など失敗パターンが多い。 本補講では、 結合を実装する前に必ず確認すべき 7 つの問いを「設計チェックリスト」として整理し、 SSDSE-B-2026 の都道府県マスタを使って各ケースを再現する。
| # | 問い | 確認方法 | 失敗時の症状 |
|---|---|---|---|
| 1 | キーは一意か? | df.key.is_unique | 行数が増える |
| 2 | NULL は許容するか? | df.key.isna().sum() | NULL=NULL は FALSE → 欠落 |
| 3 | データ型は一致? | df.key.dtype | str vs int → 結合 0 件 |
| 4 | 表記揺れは? | df.key.str.strip().value_counts() | 「東京」と「東京 」が別物 |
| 5 | how の選択は? | 分析目的を確認 | inner で必要な行が消える |
| 6 | 期待行数は? | 事前に手計算 | 行数爆発を見落とす |
| 7 | 結合後の重複列は? | suffixes 指定 | key_x, key_y で混乱 |
このコードでやること: SSDSE 都道府県データに「地方区分マスタ」(都道府県 → 地方) を内部結合し、 地方ごとの集計に発展させる。 マスタ未登録の都道府県が落ちる典型ケースを意図的に作って確認する。
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 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=1, encoding='cp932') df = df[df['年度']==2022][['都道府県','総人口','消費支出(二人以上の世帯)']].copy() # 地方区分マスタ (沖縄を意図的に未登録にする) master = pd.DataFrame({ '都道府県': ['北海道','青森','岩手','宮城','秋田','山形','福島', '茨城','栃木','群馬','埼玉','千葉','東京','神奈川', '新潟','富山','石川','福井','山梨','長野', '岐阜','静岡','愛知','三重', '滋賀','京都','大阪','兵庫','奈良','和歌山', '鳥取','島根','岡山','広島','山口', '徳島','香川','愛媛','高知', '福岡','佐賀','長崎','熊本','大分','宮崎','鹿児島'], # 沖縄なし '地方': ['北海道']*1 + ['東北']*6 + ['関東']*7 + ['中部']*10 + ['近畿']*6 + ['中国']*5 + ['四国']*4 + ['九州']*7 }) inner = df.merge(master, on='都道府県', how='inner') left = df.merge(master, on='都道府県', how='left') print(f'元データ: {len(df)} 行') print(f'inner join 後: {len(inner)} 行 (沖縄が脱落)') print(f'left join 後: {len(left)} 行 (沖縄の地方は NaN)') print(f'\n地方が NaN の県: {left[left["地方"].isna()]["都道府県"].tolist()}') |
📤 実行例:
💬 結果の読み方: 「沖縄だけが脱落して 46 行」を期待したのに、 inner の結果はわずか 1 行。 原因はキーの表記揺れ: マスタ側は '青森'・'東京'・'大阪' と略称で登録しているが、 SSDSE-B-2026 の 都道府県 は '青森県'・'東京都'・'大阪府' と正式名称。 文字列が一致するのは '北海道' だけなので、 inner join では 46 県が丸ごとサイレントに脱落した。 これは「未登録レコードの脱落」よりさらに気付きにくい事故で、 集計値が突然小さくなる典型原因。 left join にすれば全 47 行が残り、 地方 が NaN の県を洗い出せる(=表記揺れの検知)。 対策はキーを結合前に正規化(略称↔正式名称の対応表を用意)すること。 データクレンジング、 内部結合、 外部結合 も併読推奨。
'青森県'・'東京都' …と正式名称に揃える必要がある。 略称のままでは北海道以外すべて脱落する。| 操作 | pandas | SQL | 結果の行数 |
|---|---|---|---|
| 内部結合 | merge(..., how='inner') | INNER JOIN | 両方に存在する行のみ |
| 左外部結合 | merge(..., how='left') | LEFT JOIN | 左の全行 + 右の一致 |
| 右外部結合 | merge(..., how='right') | RIGHT JOIN | 右の全行 + 左の一致 |
| 完全外部結合 | merge(..., how='outer') | FULL OUTER JOIN | 両方の全行 |
| クロス結合 | merge(..., how='cross') | CROSS JOIN | 左×右 (全組み合わせ) |
| アンチ結合 | indicator + 'left_only' | NOT EXISTS | 左にだけある行 |
補講のまとめ: 結合は「キー設計」「型整合」「how 選択」「期待行数の事前計算」の 4 点を実装前に必ず確認する。 主キー、 外部キー、 リレーショナルデータベース、 SQL、 pandas、 テーブル結合 の各ページと組み合わせて、 安全な結合パイプラインを構築する。
データ結合は RDB の中心操作だが、 落とし穴が多く、 実装難易度の割に習熟難度が高い。 ここまでの補講内容を 1 ページにまとめると、 (1) 7 つの設計時の問いに必ず答える、 (2) キー正規化を 6 ステップで実装、 (3) 性能チューニング 5 原則を適用、 (4) 6 つの監視項目で本番監視、 (5) 6 つの誤解をクリアにする、 が本質。 これらを習得した後の学習トピックは、 分散結合 (Spark/BigQuery)、 ストリーミング結合 (Kafka Streams)、 グラフ DB 上の結合 (Neo4j)、 などの応用領域。 各トピックは独立した補講ページや論文サマリで学べるよう、 用語集全体で相互リンクを張ってある。
これら 5 段階を順に習熟することで、 データ結合を「実装」から「運用品質を保証する操作」へと昇格できる。 リレーショナルデータベース、 SQL、 pandas、 主キー、 外部キー と組み合わせて、 データ統合の総合パイプラインを完成させる。 特に大規模分析基盤の構築では、 結合の品質保証が全体パイプラインの信頼性を決定づける核心要素となるため、 単なる「便利な操作」を超えた重要技術である。
データ結合は多くの誤解を生む操作でもある。 実務でレビュー時に頻出する誤解を 6 つ整理し、 正しい理解を併記した。 自分の理解度をチェックするためのまとめ。
| 誤解 | 正しい理解 |
|---|---|
| inner と outer は結果が同じ | NULL の扱いと一致しない行の取捨選択が異なる |
| JOIN は左右どちらでも同じ | left/right JOIN では基準テーブルが変わる |
| NULL = NULL は TRUE | SQL/pandas 共に FALSE 扱い (IS NULL 必要) |
| 結合キーは 1 列のみ | 複合キー (年月 + 都道府県等) も可 |
| cross join は禁止 | 小規模で正当な用途あり (カレンダー × 商品) |
| JOIN の速度は order に依存しない | オプティマイザは順序を変えるが、 ヒント指定可能 |
補講のまとめ: 結合は「実装が簡単に見える」反面、 落とし穴が多い操作。 6 つの誤解をすべてクリアにし、 「自分で説明できる」状態を目指すこと。 これにより本番運用での突然のバグや、 集計値の静かな誤りを未然に防げる。
実務の結合は「いきなり merge」ではなく、 ① キー品質診断 → ② 結合 → ③ 結果検証 の 3 ステップで行う。 下のシミュレータでは、 SSDSE-B-2026 の 2023 年・総人口 (A1101 実測値、 8 県抜粋) を左表、 「地方区分ルックアップ表」 (説明用のマスタ例、 沖縄県が未登録) を右表として、 実務で頻発する 4 つのキー障害をトグルで仕込みながらこのワークフローを体感できる。 なお テーブル結合 には結合 4 タイプの切替、 データ統合 には JOIN シミュレータがあるので、 結合タイプ自体の復習はそちらで。
merge の 1 行そのものは間違えようがない。 事故が起こるのはその前後だ。 事前にキーの重複・欠損・型を診断していれば「東京都が 2 行に展開される」「一致率 0% で全列 NaN」は結合前に予見できる。 事後に行数と欠損率を検証していれば、 予見と違う結果が出た瞬間に気づける。 上のシミュレータで障害トグルを全部 ON → 正規化 ON → 結合、 という流れを試すと、 正規化で直る問題 (表記揺れ・型) と、 直らず設計判断が要る問題 (重複キー・キー欠損) があることが体感できる。
len(merged) == len(left) の assert を習慣に。'13' と整数 13 — どれもエラーにならず一致率だけが下がる。 inner join なら行が静かに消え、 left join なら NaN が静かに増える。 結合前に str.strip()・名称統一・astype を必ず通す (データクレンジング)。validate 引数: pandas の merge(..., validate='one_to_one') は、 期待した多重度 (1:1, 1:m, m:1) に反すると結合前に例外を投げる。 上のシミュレータの「validate 検査」はこれの再現。 主キーが保証されているつもりの表ほど、 この 1 引数を付ける価値がある。merge(..., how='outer', indicator=True) の _merge 列で left_only を抽出すると、 マッチしなかった行の一覧 (= 欠損率の中身) が取れる。 検証ステップで「欠損率 12.5%」と分かったら、 次はアンチ結合で「それは沖縄県だ」と特定する — この 2 段構えが実務の照合手順。COUNT(*) 比較)。merge 前後で行数を assert。 validate='one_to_one' オプションが安全装置。df.dtypes で確認し、 統一する。 SSDSE の都道府県コードは 5 桁文字列、 e-Stat は 2 桁整数なので要注意。SSDSE-B-2026 は「年度 × 都道府県」が一意キーになっているが、 キーの片方だけで結合すると多対多になり行数が膨らむ。 12 年間 × 47 都道府県 = 564 行が、 たとえば「Prefecture だけ」で結合すると 47 × 47 = 2209 通りの組合せが発生する場合がある。
| 表 A 行数 | 表 B 行数 | 結合キー | 想定結果 | 実際の行数 |
|---|---|---|---|---|
| 564 (12年×47県) | 47 (1年) | Prefecture のみ | 564 | 564 (1対多OK) |
| 564 (12年×47県) | 564 (12年×47県) | Prefecture のみ | 6,768 | 6,768 (12倍に爆発) |
| 564 (12年×47県) | 564 (12年×47県) | Prefecture と Year | 564 | 564 (正しい) |
教訓: 結合キーは「一意になる組合せ」を必ず指定する。 時系列データでは「Prefecture + Year」のように複合キーが必須。 単純化したくて 1 列だけで結合すると、 同じ Prefecture が複数年分マッチしてしまい行数が爆発する。
'13' と整数の 13 はマッチしない。 結合前に df.dtypes を確認。'東京都 ' (末尾空白) と '東京都' は別物。 str.strip() 必須。validate='one_to_one' で検出。_x, _y 接尾辞がつく。 suffixes で明示的に。dropna か fillna 判断。sort_values を最後に。データ結合は本番運用で「サイレントに壊れる」最大の経路でもある。 マスタテーブルが更新されたとき、 結合キーの形式が変わったとき、 取引データに新規取引先が増えたとき、 静かに結合失敗や行数爆発が起こる。 本番運用での 6 つの監視項目を整理した。
| 監視項目 | 確認方法 | 閾値例 |
|---|---|---|
| 結合成功率 | left 全件中の一致行数 | 95% 未満で警告 |
| 行数変化率 | 前日比 ±10% 以上で警告 | ±10% |
| NULL 比率 | 結合キーの NULL 率 | 5% 超で警告 |
| 新規キー数 | 前日にないキーの出現 | 100 超で確認 |
| 重複度 | right の最大重複数 | 10 超で確認 |
| 実行時間 | 結合 SQL の duration | 前日比 2x で警告 |
補講のまとめ: 結合の本番運用では「結合成功率・行数変化・NULL 比率・新規キー・重複度・実行時間」の 6 項目を毎日監視する。 これらを Slack 通知や Datadog ダッシュボードに統合しておくと、 サイレント障害を早期検知できる。 RDB、 SQL、 データクレンジング の運用ガイドと組み合わせて、 結合パイプラインの SLA を維持する。
データ結合は単独で完結する操作ではなく、 主キー・外部キー・正規化・参照整合性・トランザクションといった周辺概念と連携して初めて安全に機能する。 ここでは結合に関連する 8 つの周辺概念を整理し、 学習の優先順位を示す。
| 概念 | 役割 | 学習順 |
|---|---|---|
| 主キー | 行を一意に識別 | 最優先 |
| 外部キー | テーブル間の関連 | 2 番目 |
| 正規化 | 重複排除 | 3 番目 |
| 参照整合性 | FK の有効性保証 | 4 番目 |
| インデックス | 結合の高速化 | 5 番目 |
| JOIN の種類 | 行の取捨選択 | 6 番目 |
| トランザクション | 原子性保証 | 7 番目 |
| 分散結合 | 大規模 Spark/BigQuery | 8 番目 |
補講のまとめ: 結合は RDB の中心概念であり、 周辺の 8 概念と組み合わせて初めて安全に機能する。 学習の順序を意識しながら、 主キー、 外部キー、 リレーショナルデータベース、 SQL、 pandas、 リレーショナルDB 詳細 の各ページを巡回学習することで、 結合操作の総合的な理解が得られる。
データ結合はクレンジングと特徴量エンジニアリングの橋渡し。
キー設計 → 表記正規化 → 結合タイプ選択 (inner/left/outer) → 重複/null 検証 → 結合結果検証の流れで、 各段で「行数の意味」を確認する。
データ結合はキー設計 → 方式選択 → 例外処理の三段で扱う。
完全一致なら inner join、 主テーブル保持なら left join、 全件保持なら outer join、 曖昧結合なら fuzzy/regex 結合、 と「保持したい行」で選ぶ。
このページは名寄せ・表記ゆれ・多対多の行増殖を既に手厚く扱っている。ここでは重複を避け、「結合キーは実は複数列でひとつ」という、SSDSE-B のように (Code, 年) が主キーになるパネルデータ特有の落とし穴だけを、実測値で深掘りする。
結合キーを郵便の宛先だと思うと分かりやすい。SSDSE-B の Code(都道府県コード)は「住所」、SSDSE-B-2026(年、2012〜2023 の 12 個)は「部屋番号」に相当する。住所だけ書いて部屋番号を書き忘れると、郵便局は同じ住所の全 12 部屋に配達してしまう。左表 12 通 × 右表 12 通 = 1 住所あたり 144 通の重複が生まれる。結合キーの一部を落とすとは、まさにこの「宛先の書き忘れ」である。
SSDSE-B-2026 は 47 都道府県 × 12 年 = 564 行。この表を自身と結合するとき、年を忘れて on='Code' だけで結合すると、各コードが 12×12=144 組に展開され、行数は次のようになる。実測値(encoding='cp932', skiprows=[1] で読み込み、A1101=総人口を集計)。
| 操作 | 結果行数 | 2023 総人口の集計値 |
|---|---|---|
| 元データ(基準) | 564 | 124,353,000 人 |
正しい複合キー on=['Code','SSDSE-B-2026'] | 564 | 124,353,000 人 |
年を忘れた on='Code' | 6,768 | 1,492,236,000 人 |
6,768 = 47 × 144、集計値 1,492,236,000 = 124,353,000 × 12。ちょうど 12 倍に膨らむのに、pandas は例外も警告も一切出さない。総人口が「約 15 億人」という物理的にあり得ない値になって初めて気づく——気づければ、まだ幸運なほうだ。日本の総人口は実測 1 億 2435 万 3000 人であり、12 倍の値は明らかな異常だと即座に判定できるよう、桁感覚を持っておくことが最後の防波堤になる。
validate='one_to_one'(または '1:1')を merge に渡すだけで防げる。年を忘れた結合は 1 コードが複数行にマッチするため、pandas が MergeError を送出して実行前に止まる。行数試算 47 × 12 × 12 = 6768 を暗算し、期待値 564 と一致するか照合する習慣も併用したい。行増殖の多くは「結合してから集計 (join-then-aggregate)」の順序で起きる。多対 1 になるはずの右表がキーの粒度違いで多対多になっていると、結合直後に行が増え、その後の sum() が二重計上になる。原則は逆で、「集計してから結合 (aggregate-then-join)」——右表を結合キーの粒度まで groupby().agg() で先に畳んで一意にしてから結合すれば、粒度が保証され増殖が起きない。
# NG: 結合してから集計 → 右表が (Code,年) より粗い/細かいと二重計上
merged = left.merge(right, on='Code') # 粒度不一致で膨張しうる
total = merged.groupby('Code')['A1101'].sum() # 12倍などに膨らんだ後で集計
# OK: 集計してから結合 → キー粒度を先に一意化
right_agg = right.groupby(['Code','SSDSE-B-2026'], as_index=False).sum()
merged = left.merge(right_agg, on=['Code','SSDSE-B-2026'],
how='left', validate='one_to_one') # 増殖が構造的に不可能
複合キーでは各キー列の型を両表で揃えることも必須(片方が int の Code、もう片方が '01' のような文字列だと全行が非マッチになる)。年列も同様に int か文字列かを統一する。
groupby().agg()