論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
外部結合
Outer Join
データ処理

🔖 キーワード索引

#LEFT JOIN#FULL JOIN#欠損#merge#結合#NULL

「outer join」は統計データ分析の文脈で扱う重要概念のひとつ。 本ページでは「outer join」を取り巻く中核キーワードを以下にチップで一覧化する。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になる。

outer join統計分析SSDSE-B-2026前提条件適用範囲落とし穴関連手法Python 実装検証方法

これらのキーワードは「outer join の理解 → 適用 → 検証」のプロセスを構成する。 各章で詳しく解説する。

💡 30秒で分かる結論

🍰 まずはやさしく

足りない部分を埋めてつなげる方法です。

データの抜けをなくすために使います。

名簿とアンケート結果を合わせる時に便利です。

外部結合の結論をさっと確認しましょう。

外部結合:片側または両側のキーをすべて残す結合

📍 文脈ボックス

🍰 まずはやさしく

データ処理の重要なテクニックです。

分析でデータを正しく扱うために使います。

スマホのアプリなどでデータをまとめる時に役立ちます。

この用語を学ぶための全体の流れを説明します。

この用語は データ処理 カテゴリに属します。 関連する別称・略号:(なし)。

論文・実務レポートで 外部結合 が登場したら、 まず本ページの「30秒で分かる結論」と「直感で掴む」を読めば、 その文脈で何を言っているか把握できます。

本ページでは「outer join」を扱う。 統計データ分析コンペティション (2026) の教材で、 SSDSE-B-2026 (47 都道府県 × 複数年 × 100 超列) の実データを使った再現可能な学習を目指す。

「outer join」は統計・データサイエンスの体系における重要概念のひとつ。 本ページは「定義・直感・数式・実装・落とし穴・関連手法」の 6 視点で構成され、 各視点は独立して読めるが順序通り読むと体系的な理解が得られる。

🎨 直感で掴む

🍰 まずはやさしく

パズルのピースを無理やり合わせるイメージです。

どちらか一方にしかない情報を残すために使います。

部活の名簿と出席簿を比べる時に便利です。

図や例を使って直感的に仕組みを理解しましょう。

SSDSE-B-2026 (47 都道府県 × 経済社会指標) と、 別ファイルの SSDSE-D-2023 (一部都道府県のみの観光統計) を 都道府県 列で結合したいとする。 B には 47 件全て、 D には 30 件しかない。 結合の仕方でテーブルの形が劇的に変わる:
・内部結合 (inner join):両方に存在する 30 件のみ残す。
・左外部結合 (left outer join):B の 47 件全部残し、 D の値は無ければ NaN。
・右外部結合 (right outer join):D の 30 件全部残し、 B の値は無ければ NaN。
・完全外部結合 (full outer join):B にも D にも在る全ての都道府県 (この例なら 47 件) を残し、 片側欠損は NaN。

つまり 外部結合 (outer join)とは「片方にしか無いキーも保持する」結合のこと。 内部結合と違って 結合キーの不一致による情報損失が無いのが最大の特徴で、 欠損行を後で「分析対象から除く/追加調査する」と判断できるのが実務的価値。 SQL では LEFT/RIGHT/FULL OUTER JOIN、 pandas では df.merge(other, how='left'|'right'|'outer')。

初学者がよくミスするのは「外部結合したのに結果が内部結合と同じ件数になる」現象。 これは マージキーの型不一致(str vs int) や、 表記揺れ (北海道 vs 北海道 [全角・半角]) で照合が失敗しているサイン。 後述の落とし穴で詳説する。

📐 定義・数式

🍰 まずはやさしく

数学的なルールで決まったつなぎ方です。

正確にデータを結合させるために使います。

買い物リストと在庫表を厳密に合わせる時に役立ちます。

数式を使って外部結合の定義を詳しく学びましょう。

【外部結合の論理】
$$ A \,\bowtie_{\text{LEFT}}\, B = (A \,\bowtie\, B) \cup \{(a, \text{NULL}) \mid a \in A,\, \nexists b \in B\} $$

LEFT OUTER JOIN は、 INNER JOIN の結果に「左にしか存在しない行を NULL 埋めで追加」したもの。 RIGHT は逆、 FULL は両方追加。

🔬 数式を言葉で読み解く

数式に出てくる記号の意味を 1 つずつ確認しましょう。

INNER JOIN
両方に存在するキーのみ。 マッチしないと消える。
LEFT OUTER
左テーブルを全部残し、 右の不足は NULL。
RIGHT OUTER
右テーブルを全部残す。
FULL OUTER
両方とも全部残す。

🔬 数式を言葉で読み解く(深掘り)

先の「記号を読み解く」を、 外部結合 の固有事情に即して 500 字以上で詳説する。 ここを読めば、 数式 1 行の裏にある設計判断と歴史的経緯が見えるはずだ。

LEFT (左): 左表 $L$ の全行を残し、 右表 $R$ にマッチが無ければ右側列を NULL/NaN。 件数は $|L|$ 以上 (1 対多なら膨張)。 RIGHT (右): 鏡像。 右表 $R$ の全行を残す。 SQL 標準だが、 慣習的に LEFT JOIN を使うほうが読みやすく推奨される。 FULL (完全外部): 両側の全行を残し、 片側欠損は NULL/NaN。 件数は $|L \cup R|$。 INNER: マッチした行のみ。 件数は $|L \cap R|$ で、 outer の特殊形 (NaN 行を捨てる)。 JOIN キーは 1 列でも複数列でもよく、 pandas では on=['都道府県', '年度'] のように複合キーを指定できる。 $L \bowtie_K R$ という関係代数の表記は、 集合論的には「直積 + 選択 + 射影」の合成で、 外部結合はこれに「片側に対応する NULL 行を補う」操作を追加したもの。 つまり外部結合 = 内部結合 + マッチしなかった側を NULL で補完。 この拡張により情報損失なく集合演算を行える設計で、 リレーショナル代数を「現実のデータ統合」に橋渡しする中核演算。

📌 数式は 暗記対象ではなく検算ツール。 「結果が変だ」と感じたとき、 「この記号は本来こういう意味だから、 ここの値はおかしい」と 逆引きできる状態が、 中級者と上級者の差を生む。

🧪 SSDSE-B-2026 ハンズオン

本サイトの標準データ SSDSE-B-2026(47 都道府県 × 約 100 列、 独立行政法人統計センター提供) を使い、 外部結合 の概念を 実コードで体感する。 取得経路は data/raw/SSDSE-B-2026.csv(リポジトリ同梱)。

SSDSE-B-2026 (経済社会指標) と SSDSE-D-2023 (社会生活基本調査) を都道府県名で完全外部結合する。 D には鳥取・島根が含まれない仮想シナリオで、 結果テーブルの「鳥取・島根」行で D 由来の列が NaN になることを確認する。 4 ステップ:(1) 両 CSV 読込 → (2) キー前処理 → (3) how='outer' でマージ → (4) 欠損確認・indicator=True で由来追跡。

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 Prefecture(都道府県) 北海道 北海道 東京都 東京都 沖縄県 沖縄県 …(全 47 行)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# SSDSE-B (経済社会) と SSDSE-D (社会生活) を都道府県名で full outer join する
import pandas as pd
b = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=1, encoding='cp932')
d = pd.read_csv('data/raw/SSDSE-D-2023.csv', skiprows=1, encoding='cp932')
# 1 都道府県 1 行にそろえる(B は 12 年度分、D は 総数・男・女 の 3 行ずつあり、
# そのまま結合すると 1 県あたり 12 × 3 = 36 行に膨らむ)
b = b[b['年度'] == 2023][['都道府県', '総人口']]
d = d[d['男女の別'] == '0_総数'][['都道府県', '推定人口(10歳以上)']]
# 仮想シナリオ:D に鳥取県・島根県の行が無いとする
d = d[~d['都道府県'].isin(['鳥取県', '島根県'])]
# キー正規化(前後空白)
b['都道府県'] = b['都道府県'].str.strip()
d['都道府県'] = d['都道府県'].str.strip()
# full outer join + 由来追跡
merged = b.merge(d, on='都道府県', how='outer', indicator=True)
print('shape =', merged.shape)
print(merged['_merge'].value_counts())  # left_only / both / right_only
# どの都道府県が片側だけだったか
print('B にのみ:', merged.loc[merged['_merge']=='left_only', '都道府県'].tolist())
print('D にのみ:', merged.loc[merged['_merge']=='right_only', '都道府県'].tolist())
# 完全マッチ件数と NaN を抱える件数の比較
print('NaN を含む行 =', merged.isna().any(axis=1).sum())
print(merged[merged['_merge'] != 'both'])
📤 実行例(実測) shape = (48, 4) _merge both 45 left_only 2 right_only 1 Name: count, dtype: int64 B にのみ: ['島根県', '鳥取県'] D にのみ: ['全国'] NaN を含む行 = 3 都道府県 総人口 推定人口(10歳以上) _merge 3 全国 NaN 112462.0 right_only 21 島根県 650000.0 NaN left_only 46 鳥取県 537000.0 NaN left_only

💬 結果は 48 行で、both 45・left_only 2・right_only 1。仮想的に D から外した島根県・鳥取県は B 側にだけ残り、推定人口(10 歳以上)が NaN になる。D にだけある「全国」(推定人口 112,462 千人)は集計行なので、県の分析では結合前に落とすべき行だと indicator で気づける。年度と男女の別で 1 県 1 行に絞らずに結合すると、1 県あたり 12 × 3 = 36 行に膨らみ、このコードの条件(D から 2 県を外した状態)では 1,647 行になってしまうので、キーが一意かどうかは結合の前に必ず確かめる。

📌 動かないときは: (1) data/raw/SSDSE-B-2026.csv がリポジトリに存在するか、 (2) skiprows=1 でヘッダーが正しく読めているか、 (3) 列名が SSDSE 公式の最新版と一致しているかを df.columns.tolist() で確認。

🔬 外部結合と関連トピックの統合理解

外部結合を「単独の操作」 として理解するのではなく、 データ分析パイプラインの中での位置づけを意識すると、 その価値が見えてくる。 典型的な分析フローは「データ取得 → 前処理 → 結合 → 集計 → 可視化 → 解釈」 で、 結合はパイプラインの中間に位置する。 結合の前にはキーの正規化・型変換・欠損処理が必要、 結合の後には集計や可視化が続く。 結合だけ最適化しても、 前後の処理が雑だと結果は信頼できない。 SSDSE-B-2026 のような教育用データセットを使って、 結合前後の処理を含めた一連のフローを体得することが学習の本質。

📊 分析パイプラインでの外部結合の位置づけ

段階処理外部結合との関連SSDSE-B-2026 での例
1. データ取得CSV・API・DB からロード取得元が複数なら結合が必要SSDSE と e-Stat を別々にダウンロード
2. 前処理正規化・型変換・欠損処理結合キーの統一が必須都道府県コードの文字列化、 ゼロパディング
3. 結合LEFT/RIGHT/FULL OUTER本トピック中心マスタ + 集計テーブルを結合
4. 集計GROUP BY、 pivot_table結合後の NULL に注意地方別集計、 年度別集計
5. 可視化matplotlib, seaborn結合結果の品質確認都道府県別の棒グラフ・地図
6. 解釈統計的検定・モデル化結合の欠損が結果に影響地域差の有意性検定
7. 報告レポート・ダッシュボード結合手法の明示が必要方法論セクションに結合戦略を記述

→ 結合はパイプラインの中間に位置するが、 前後の処理に影響を与える critical な操作。 雑な結合は、 雑な集計・可視化・解釈を生む。 「結合のためだけに前処理を念入りにする」 という意識が、 データ品質を支える。 SSDSE-B-2026 のような小データでも、 こうしたパイプライン意識を持って取り組むことが、 実務での大規模データへの拡張を可能にする。

📏 結合品質の定量指標

指標定義計算式SSDSE-B-2026 での目安
マッチ率左テーブルの行のうち右テーブルとマッチした割合matched / left_rows100% が理想 (47/47)
NULL 率結合後の値列で NULL の割合nulls / total_rows0% が理想
重複率1:1 結合のはずが N:M で増えた割合(joined - left) / left0% が理想 (爆発検知)
カバレッジ右テーブルの行が結合に使われた割合used_right / right_rows100% が理想
欠損キー数結合キーが NULL の行数count where key IS NULL0 が理想
型不一致数左右で結合キーの型が異なる行—0 が理想
前後比較結合前後の集計値の整合性SUM_before == SUM_afterNULL 補正後の総計が一致

これらの品質指標を結合の都度確認することで、 「気づかないうちにデータが消失」「気づかないうちに行数爆発」 等の事故を防げる。 マッチ率 95% を下回ったら結合キーを疑う、 NULL 率が想定外に高ければデータ欠損を調査する、 重複率が 0% でなければ結合キーの一意性を再確認する、 という判断基準を持つことが、 データエンジニアの基礎力。 SSDSE-B-2026 のような小データで原理を学び、 業務で大規模データに適用する際に、 これらの指標を必ずチェックする習慣が、 品質保証の根幹となる。

🧪 SSDSE-B-2026 を題材にした外部結合のステップバイステップ演習

SSDSE-B-2026 (47 都道府県データ) を題材に、 外部結合の演習を順に進める。 ステップ 1: SSDSE-B-2026 から「都道府県コード, 都道府県名, 総人口」 の 3 列を抽出して左テーブルとする。 ステップ 2: 別途、 e-Stat 等から「都道府県コード, 65 歳以上人口」 を取得して右テーブルとする (一部県のみのデータと仮定)。 ステップ 3: pd.merge(left, right, on='code', how='left', indicator=True) で LEFT OUTER 結合し、 indicator 列で結合状況を記録。 ステップ 4: df['_merge'].value_counts() で「左のみ」「両方」 の行数を確認。 ステップ 5: df['rate_65_plus'] = df['65歳以上人口'] / df['総人口'] で高齢化率を計算 (NULL は無視される)。 ステップ 6: df.dropna(subset=['rate_65_plus']) で欠損行を除いて分析、 または df['rate_65_plus'].fillna(df['rate_65_plus'].mean()) で平均値補完。 ステップ 7: 結果を都道府県別の棒グラフで可視化。 こうした 7 ステップの演習が、 外部結合と pandas の総合理解を体感的に身につけさせる。

この演習の発展として、 (1) FULL OUTER で双方の欠損を確認、 (2) INNER JOIN との結果比較、 (3) 結合品質を indicator 列の集計で定量化、 (4) NULL の集計時の影響 (SUM/AVG が NULL を無視する性質) を体感、 (5) COALESCE 相当の fillna で NULL を 0 に置換した場合の結果との比較、 等を試す。 各ステップで len(df)、 df.isnull().sum()、 df.head() を確認する習慣を身につけることで、 結合のバグを早期発見する基礎力が育つ。 SSDSE のような信頼できる小データだからこそ、 こうした地道な確認作業の重要性を体感できる。 業務で大規模・複雑なデータを扱う際にも、 この基礎が品質保証の根幹となる。 この演習を通じて、 学生諸氏は「結合の本質」 を頭で理解するだけでなく、 「自分の手で動かして体感する」 ことができる。 こうした実践的経験が、 将来の業務・研究で出会う多様なデータ結合の場面で生かされる、 一生ものの基礎技能となる。 SSDSE-B-2026 という公的データを使った地道な演習の積み重ねが、 データサイエンティストとしての成長の核心であり、 統計データ分析コンペティションへの参加準備の最良の方法でもある。 学生諸氏には、 本ページの内容を起点に、 実際にコードを書き、 結果を観察し、 仮説を検証する反復を通じて、 外部結合の本質を体得してほしい。 そうした地道な学びこそが、 将来のあらゆるデータ分析の場面で生きる力となる。 SSDSE のような公的データを使った演習と、 業務での大規模データへの応用を往復することで、 外部結合の理論と実践が一体化し、 真に「使える技能」 として定着していく。

🧮 実値で計算してみる

都道府県マスターと観光統計表を結合する例。

STEP 1 マスター読み込み
47 都道府県のテーブル A。
STEP 2 明細読み込み
30 県分の観光客数テーブル B。
STEP 3 LEFT JOIN
pd.merge(A, B, how='left', on='都道府県')
STEP 4 NaN 確認
結合後、 17 県の観光客数列が NaN になる。

🧮 数式に値を入れて手で計算する: OUTER JOIN 結果行数

合成 2 テーブルで FULL OUTER JOIN の結果行数を計算する。

Step 1: テーブル

A: {1, 2, 3, 4} (4 行) B: {3, 4, 5, 6, 7} (5 行) INTERSECT: {3, 4} (2 行)

Step 2: 各種 JOIN 行数

INNER: 2 (一致) LEFT: 4 (A 全件) RIGHT: 5 (B 全件) FULL OUTER: 4 + 5 - 2 = 7

🐍 Python で再現

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
A = {1, 2, 3, 4}
B = {3, 4, 5, 6, 7}
inner = len(A & B)
left = len(A)
right = len(B)
full = len(A | B)
print(f"INNER: {inner}")
print(f"LEFT: {left}")
print(f"RIGHT: {right}")
print(f"FULL OUTER: {full}")

📤 実行結果

INNER: 2 LEFT: 4 RIGHT: 5 FULL OUTER: 7

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

🧮 実データで行数を手計算する — 高齢化率 33% 以上の県 × 出生数 1 万人以上の県

外部結合の行数は、キーの集合の大きさだけで決まる。キーが両表で重複しない(1 県 1 行)なら、INNER は共通部分 |A∩B|、LEFT は |A|、RIGHT は |B|、FULL OUTER は和集合 |A∪B| = |A| + |B| − |A∩B| になる。2023 年度の SSDSE-B-2026 から 2 つの県の表を作って確かめる。

Step計算値
1表 A = 高齢化率が 33% 以上の県、表 B = 出生数が 10,000 人以上の県を数える|A| = 19、|B| = 20
2両方に入る県(高齢化率が高いのに出生数も多い県)を探す北海道・新潟県の 2 県 → |A∩B| = 2
3INNER = |A∩B|、LEFT = |A|、RIGHT = |B|2 行・19 行・20 行
4FULL OUTER = |A| + |B| − |A∩B|19 + 20 − 2 = 37 行
5FULL OUTER で NaN が入る行 = 片側だけの行A だけ 17 行(出生数が NaN)+ B だけ 18 行(高齢化率が NaN)= 35 行

🎯 このコードでやること:Step 1〜5 と同じ 2 つの表を pandas で作り、how を inner・left・right・outer に変えて行数と _merge の内訳を数える。

📥 入力例 SSDSE-B-2026(2023 年度・47 都道府県) Code Prefecture A1101(総人口) A1303(65歳以上) A4101(出生数) R01000 北海道 5,092,000 1,681,000 24,430 R13000 東京都 14,086,000 3,205,000 86,348 R15000 新潟県 2,126,000 720,000 10,916 (高齢化率 = A1303 / A1101 × 100)
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', encoding='cp932', skiprows=[1])
d = df[df['SSDSE-B-2026'] == 2023].copy()
d['高齢化率'] = d['A1303'] / d['A1101'] * 100

# A: 高齢化率が 33% 以上の県 / B: 出生数が 1 万人以上の県(どちらも 2023 年度)
A = d.loc[d['高齢化率'] >= 33, ['Prefecture', '高齢化率']]
B = d.loc[d['A4101'] >= 10000, ['Prefecture', 'A4101']]
print('|A| =', len(A), ' |B| =', len(B))

for how in ['inner', 'left', 'right', 'outer']:
    m = A.merge(B, on='Prefecture', how=how, indicator=True)
    print(f'{how:5s}: {len(m):2d} 行', m['_merge'].value_counts().to_dict())

m = A.merge(B, on='Prefecture', how='outer', indicator=True)
print('両方に入る県:', m.loc[m['_merge'] == 'both', 'Prefecture'].tolist())
print('|A∪B| = |A| + |B| - |A∩B| =', len(A), '+', len(B), '-', (m['_merge'] == 'both').sum(), '=', len(m))
📤 実行例(実測) |A| = 19 |B| = 20 inner: 2 行 {'both': 2, 'left_only': 0, 'right_only': 0} left : 19 行 {'left_only': 17, 'both': 2, 'right_only': 0} right: 20 行 {'right_only': 18, 'both': 2, 'left_only': 0} outer: 37 行 {'right_only': 18, 'left_only': 17, 'both': 2} 両方に入る県: ['北海道', '新潟県'] |A∪B| = |A| + |B| - |A∩B| = 19 + 20 - 2 = 37

💬 手計算どおり、inner 2 行・left 19 行・right 20 行・outer 37 行になり、outer の内訳は left_only 17・right_only 18・both 2 で Step 5 の 35 行(片側だけの行)とも一致する。両方に入るのは北海道と新潟県だけで、高齢化率の高い県の多くは出生数が 1 万人に届かないことが、right_only と left_only の多さに表れている。どの方式でも、キーが重複しない限り行数は集合の計算だけで事前に予想できるので、結合後の len() がこの予想と違えば重複か表記ゆれを疑う。

🐍 Python 実装

最小実装の例。 SSDSE のような実データに対して、 まずはコピペで動かしてみるのが理解の早道です。

1
2
3
4
5
import pandas as pd
master = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=1, encoding='cp932')
detail = pd.read_csv('data/raw/tourism.csv')
merged = pd.merge(master, detail, how='left', on='都道府県')
print(merged['観光客数'].isna().sum(), 'rows missing')

🐍 Python 実装(応用編)

先の Python 実装は最小例だった。 ここでは 外部結合 の本格的な実務シナリオに即した、 もう一段難易度を上げたコードを示す。 そのままコピペで動くよう、 SSDSE 系の実データパスを直書きで残してある。

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 SSDSE-B-2026(年度) Prefecture(都道府県) A1101(総人口) 北海道 2,023 北海道 5,092,000 東京都 2,023 東京都 14,086,000 沖縄県 2,023 沖縄県 1,468,000 …(全 47 行)
 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', header=0)
df = df[df['Code'].astype(str).str.match(r'^R\d{5}$', na=False)].copy()
for _c in df.columns[3:]:
    df[_c] = pd.to_numeric(df[_c], errors='coerce')
df = df[df['SSDSE-B-2026'] == df['SSDSE-B-2026'].max()]

# ── この抜粋で使う 3 つの表を用意します ──
import pandas as pd

b = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=1, encoding='cp932')
b = b[b['年度'] == 2023][['都道府県', '総人口']]
d = pd.read_csv('data/raw/SSDSE-D-2023.csv', skiprows=1, encoding='cp932')
d = d[d['男女の別'] == '0_総数'][['都道府県', '推定人口(10歳以上)']]
e = pd.read_csv('data/raw/SSDSE-E-2026.csv', skiprows=2, encoding='cp932')
e = e[['都道府県', '一般世帯数']]
for _t in (b, d, e):
    _t['都道府県'] = _t['都道府県'].astype(str).str.strip()

# SSDSE-B, SSDSE-D, SSDSE-E の 3 つを都道府県名で full outer join し
# 「どの表に居ない都道府県があるか」を indicator で追跡する例
import pandas as pd

# それぞれ独立に full outer join
step1 = b.merge(d, on='都道府県', how='outer', indicator='in_BD')
step2 = step1.merge(e, on='都道府県', how='outer', indicator='in_BDE')
print('shape =', step2.shape)
print(step2['in_BD'].value_counts())
print(step2['in_BDE'].value_counts())

# 全表にあるのは? 1 表だけのは?
in_all = step2.query('in_BD == "both" and in_BDE == "both"')['都道府県'].tolist()
print('全表に存在:', len(in_all), '件:', in_all[:5])
only_b = step2.query('in_BD == "left_only"')['都道府県'].tolist()
print('B のみ:', only_b[:5])
📤 実行例(実測) shape = (48, 6) in_BD both 47 right_only 1 left_only 0 Name: count, dtype: int64 in_BDE both 48 left_only 0 right_only 0 Name: count, dtype: int64 全表に存在: 47 件: ['三重県', '京都府', '佐賀県', '兵庫県', '北海道'] B のみ: []

💬 3 表の外部結合で 48 行 × 6 列。in_BD の right_only 1 件は D にだけある「全国」で、E にも全国行があるため 2 段目では 48 行すべて both になり、全国行が「全表に存在」と紛れかける。条件を両方 both にした in_all は 47 件で都道府県だけが残る。indicator は結合のたびに列名を変えておかないと、2 段目で 1 段目の由来が上書きされて追跡できなくなる。

📌 このコードのポイント: (1) 引数化せず実データパスを直書きで読みやすさ優先、 (2) 集約・結合・型最適化など 外部結合 関連の典型処理を一気通貫で示す、 (3) print() で各ステップの結果を確認できる。

🐍 県名の表記ゆれを indicator で見つけて直す

SSDSE-B-2026 の県名は「東京都」「大阪府」だが、別の資料では「東京」「大阪」と書かれていることが多い。結合キーの表記がずれると、外部結合は「一致しなかった行」を黙って両側に並べる。indicator=True の _merge 列を数えれば、ずれにすぐ気づける。

🎯 このコードでやること:2023 年度の SSDSE-B-2026 から、県名がそのままの表(総人口)と、県名の末尾の「都・府・県」を落とした表(出生数)を作り、県名で外部結合して一致した行数を数える。そのあと両側のキーを同じ規則でそろえて結合し直す。

📥 入力例 SSDSE-B-2026(2023 年度・47 都道府県) Code Prefecture A1101(総人口) A1303(65歳以上) A4101(出生数) R01000 北海道 5,092,000 1,681,000 24,430 R13000 東京都 14,086,000 3,205,000 86,348 R15000 新潟県 2,126,000 720,000 10,916 (高齢化率 = A1303 / A1101 × 100) 別の表の県名の例: 北海道 / 東京 / 大阪 / 京都 / 神奈川 …
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])
d = df[df['SSDSE-B-2026'] == 2023]

master = d[['Code', 'Prefecture', 'A1101']]                     # 県名は「東京都」「大阪府」…
other = d[['Prefecture', 'A4101']].copy()
other['Prefecture'] = other['Prefecture'].str.replace(r'[都府県]$', '', regex=True)  # 別の表では「東京」「大阪」…

m = master.merge(other, on='Prefecture', how='outer', indicator=True)
print('県名のまま結合:', len(m), '行', m['_merge'].value_counts().to_dict())
print('一致したのは:', m.loc[m['_merge'] == 'both', 'Prefecture'].tolist())
print('A4101 が NaN の行:', m['A4101'].isna().sum(), ' A1101 が NaN の行:', m['A1101'].isna().sum())

# 直し方: 両側の県名から末尾の「都・府・県」を落としてそろえる(北海道はそのまま)
key = lambda s: s.str.replace(r'[都府県]$', '', regex=True)
m2 = (master.assign(key=key(master['Prefecture']))
      .merge(other.rename(columns={'Prefecture': 'key'}), on='key', how='outer', indicator=True))
print('キーをそろえて結合:', len(m2), '行', m2['_merge'].value_counts().to_dict())
print('京都府 と 東京都 のキー:', key(pd.Series(['京都府', '東京都'])).tolist())
📤 実行例(実測) 県名のまま結合: 93 行 {'left_only': 46, 'right_only': 46, 'both': 1} 一致したのは: ['北海道'] A4101 が NaN の行: 46 A1101 が NaN の行: 46 キーをそろえて結合: 47 行 {'both': 47, 'left_only': 0, 'right_only': 0} 京都府 と 東京都 のキー: ['京都', '東京']

💬 県名のまま外部結合すると 47 行ではなく 93 行になり、一致したのは末尾に「都・府・県」の付かない北海道の 1 行だけ。残る 46 県は left_only と right_only に 1 行ずつ分かれ、出生数(A4101)も総人口(A1101)も 46 行ずつ NaN になる。このまま出生率を計算すると 46 県が欠損になるが、エラーは出ない。両側のキーを同じ規則(末尾の都・府・県を落とす。京都府 → 京都、東京都 → 東京)でそろえると 47 行すべてが both になる。県名より、表記のぶれない都道府県コード(Code 列)をキーにできるならそのほうが確実。

🐍 年度を入れ忘れた結合を validate で止める

SSDSE-B-2026 は 47 県 × 12 年度の 564 行なので、県コードだけでは行が決まらない。キーに年度を入れ忘れると、外部結合は同じ県の行どうしをすべて掛け合わせる。validate 引数を付けると、キーが一意でない時点で止められる。

🎯 このコードでやること:564 行の総人口の表と 564 行の出生数の表を、①Code だけで外部結合、②Code だけで validate='one_to_one' を付けて結合、③(Code, 年度) で結合、の 3 通りで比べる。

📥 入力例 SSDSE-B-2026(564 行 = 47 県 × 12 年度) Code 年度 A1101(総人口) A4101(出生数) R01000 2023 5,092,000 24,430 R01000 2022 5,140,000 … …(同じ Code が 12 行ずつ)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df.rename(columns={'SSDSE-B-2026': '年度'})
pop = df[['Code', '年度', 'A1101']]            # 564 行(47 県 × 12 年度)
birth = df[['Code', '年度', 'A4101']]          # 564 行

print('Code の重複: 左', pop['Code'].duplicated().sum(), '/ 右', birth['Code'].duplicated().sum())

m_bad = pop.merge(birth, on='Code', how='outer')                 # 年度をキーに入れ忘れた
print('Code だけで結合:', len(m_bad), '行(1 県あたり', len(m_bad) // 47, '行)')

try:
    pop.merge(birth, on='Code', how='outer', validate='one_to_one')
except pd.errors.MergeError as e:
    print('validate で停止:', e)

m_ok = pop.merge(birth, on=['Code', '年度'], how='outer', validate='one_to_one', indicator=True)
print('(Code, 年度) で結合:', len(m_ok), '行', m_ok['_merge'].value_counts().to_dict())
📤 実行例(実測) Code の重複: 左 517 / 右 517 Code だけで結合: 6768 行(1 県あたり 144 行) validate で停止: Merge keys are not unique in either left or right dataset; not a one-to-one merge (Code, 年度) で結合: 564 行 {'both': 564, 'left_only': 0, 'right_only': 0}

💬 左右とも Code は 564 行中 517 行が重複しているので、Code だけで結合すると 1 県あたり 12 × 12 = 144 行、全体で 6,768 行に膨らむ。エラーは出ず、合計や平均を取ると 12 倍に数えた値が静かに出てしまう。validate='one_to_one' を付けると「キーが一意でない」という MergeError で止まり、(Code, 年度) をキーにすれば 564 行すべてが both で 1 対 1 に対応する。結合の前に「1 行を決めるキーは何か」を書き出し、validate で機械的に確かめる。

🐍 同じ列名の 2 表を結合するときは suffixes で年度を名前に入れる

同じ CSV から取り出した 2 つの年度の表は列名がすべて同じなので、結合すると pandas が既定の _x・_y を付ける。どちらがどの年度かを列名から読めるように、suffixes に年度を入れておく。

🎯 このコードでやること:SSDSE-B-2026 の 2012 年度と 2023 年度の表(Code・県名・総人口・高齢化率)を Code で外部結合し、既定の列名と suffixes=('_2012', '_2023') を付けた列名を比べる。そのうえで 11 年間の高齢化率の上昇幅と人口増減率を計算し、上昇幅の大きい県・小さい県を並べる。

📥 入力例 SSDSE-B-2026 表 1: 2012 年度の 47 県(Code, Prefecture, A1101, 高齢化率) 表 2: 2023 年度の 47 県(同じ列)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df['高齢化率'] = df['A1303'] / df['A1101'] * 100
cols = ['Code', 'Prefecture', 'A1101', '高齢化率']
y12 = df.loc[df['SSDSE-B-2026'] == 2012, cols]
y23 = df.loc[df['SSDSE-B-2026'] == 2023, cols]

m_default = y12.merge(y23, on='Code', how='outer')
print('suffixes 既定の列名:', list(m_default.columns))

m = y12.merge(y23.drop(columns='Prefecture'), on='Code', how='outer',
              suffixes=('_2012', '_2023'), indicator=True, validate='one_to_one')
print('行数:', len(m), m['_merge'].value_counts().to_dict())
m['上昇幅'] = m['高齢化率_2023'] - m['高齢化率_2012']
m['人口増減率'] = (m['A1101_2023'] / m['A1101_2012'] - 1) * 100
top = m.sort_values('上昇幅', ascending=False)[['Prefecture', '高齢化率_2012', '高齢化率_2023', '上昇幅', '人口増減率']]
print(top.head(3).round(2).to_string(index=False))
print(top.tail(2).round(2).to_string(index=False, header=False))
📤 実行例(実測) suffixes 既定の列名: ['Code', 'Prefecture_x', 'A1101_x', '高齢化率_x', 'Prefecture_y', 'A1101_y', '高齢化率_y'] 行数: 47 {'both': 47, 'left_only': 0, 'right_only': 0} Prefecture 高齢化率_2012 高齢化率_2023 上昇幅 人口増減率 秋田県 30.67 39.06 8.39 -14.02 青森県 26.96 35.22 8.26 -12.30 徳島県 28.00 35.40 7.40 -10.32 大阪府 23.69 27.66 3.97 -1.11 東京都 21.25 22.75 1.50 6.44

💬 既定では Prefecture_x・A1101_y のような列名になり、x と y のどちらが 2012 年度かは結合した順序を覚えていないと分からない。suffixes に年度を入れ、片方の県名を落としておけば、列名だけで意味が読める。47 行すべてが both(validate も通過)なので、比較は 47 県そろっている。高齢化率の上昇幅は秋田県 8.39 ポイント(30.67% → 39.06%)、青森県 8.26 ポイント、徳島県 7.40 ポイントが大きく、いずれも人口が 10〜14% 減った県。小さいのは大阪府 3.97 ポイントと東京都 1.50 ポイントで、東京都は人口が 6.44% 増えている。

⚠️ よくある落とし穴

外部結合は「片方にしか存在しない行」を残す結合で、 INNER JOIN との取り違えと NaN 処理ミスが事故の二大原因。 SSDSE で「都道府県名」と「県名」を結合する際、 表記揺れ (東京 / 東京都) で NaN 量産 → 集計結果が崩壊するパターンが典型。 結合前に必ず両側の unique key を可視化したい。

❌ 結合キーの表記揺れ
SSDSE-B-2026 では「東京都」、 観光統計では「東京」、 英文資料では「Tokyo」と表記が違う。 outer join で何も前処理しないと NaN が量産され、 実際にマッチする行はわずか。 結合前に set(df1.key) - set(df2.key) で差分を確認し、 名寄せ辞書(マッピング表)で統一する。
❌ 行数増殖
結合キーがユニークでない場合(同じ都道府県コードが複数行ある等)、 outer join 結果は左 m 行 × 右 n 行に増殖する。 47 県のはずが数百行になることも。 結合前に df.duplicated(subset='key') で重複を確認し、 必要なら集約(groupby().agg())してから結合する。
❌ NaN を 0 で埋める誤り
outer join で発生した NaN を fillna(0) で埋めると、 「観光客 0 人だった県」と「データ取得失敗の県」が同じ 0 として扱われ、 平均値・分散・回帰係数が歪む。 NaN の発生源(左のみ・右のみ)を indicator=True で残し、 「データ欠損」と「真の 0」を区別する。
❌ FULL OUTER の濫用
FULL OUTER は「両側にしかないキー」を全て残すため、 共通キーが少ない 2 表(東京 23 区データ × 全国都道府県データ)では片側 NaN が大量発生する。 分析目的に応じて inner / left / right を選び、 「両側存在を残す必要性」を本当に検討してから FULL OUTER を選択する。

⚠️ 年度の範囲が違う 2 表の外部結合と fillna(0)

外部結合で生じた NaN は「その表にその年度のデータが無い」という意味で、「値が 0」ではない。年度の範囲が違う 2 つの表を外部結合し、NaN を 0 で埋めてから集計すると、存在しない年度が「出生数 0」に化ける。

🎯 このコードでやること:高齢化率の表 A(2012〜2020 年度)と出生数の表 B(2015〜2023 年度)を (Code, 年度) で外部結合し、全国の出生数を年度別に合計する。NaN のまま sum(min_count=1) で合計した場合と、fillna(0) してから合計した場合、および 1 県 1 年あたりの平均を比べる。

📥 入力例 SSDSE-B-2026(564 行)から 表 A: 2012〜2020 年度 × 47 県 = 423 行(Code, 年度, 高齢化率) 表 B: 2015〜2023 年度 × 47 県 = 423 行(Code, 年度, A4101 出生数)
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])
df = df.rename(columns={'SSDSE-B-2026': '年度'})
df['高齢化率'] = df['A1303'] / df['A1101'] * 100

A = df.loc[df['年度'] <= 2020, ['Code', '年度', '高齢化率']]      # 2012〜2020 年度
B = df.loc[df['年度'] >= 2015, ['Code', '年度', 'A4101']]         # 2015〜2023 年度(出生数)
print('A:', len(A), '行  B:', len(B), '行')

m = A.merge(B, on=['Code', '年度'], how='outer', indicator=True)
print('外部結合:', len(m), '行', m['_merge'].value_counts().to_dict())

# 全国の出生数を年度ごとに合計する
nat = m.groupby('年度')['A4101'].sum(min_count=1)                  # 全県 NaN の年度は NaN のまま
nat0 = m.fillna({'A4101': 0}).groupby('年度')['A4101'].sum()       # NaN を 0 で埋めてから合計
print(pd.DataFrame({'NaN のまま': nat, 'fillna(0)': nat0}).loc[[2013, 2014, 2015, 2023]])

print('1 県 1 年あたりの出生数の平均  NaN を除く:', round(m['A4101'].mean()),
      ' 0 で埋める:', round(m['A4101'].fillna(0).mean()))
📤 実行例(実測) A: 423 行 B: 423 行 外部結合: 564 行 {'both': 282, 'left_only': 141, 'right_only': 141} NaN のまま fillna(0) 年度 2013 NaN 0.0 2014 NaN 0.0 2015 1005668.0 1005668.0 2023 727269.0 727269.0 1 県 1 年あたりの出生数の平均 NaN を除く: 18589 0 で埋める: 13941

💬 外部結合は 564 行で、両方に年度がある 2015〜2020 年度が both 282 行、A だけの 2012〜2014 年度が 141 行、B だけの 2021〜2023 年度が 141 行。NaN のままなら 2013・2014 年度の全国出生数は NaN(データ無し)と正しく出るが、fillna(0) すると 0 人になり、2015 年度の 1,005,668 人から見て「出生数が消えた年」があるかのような系列になる。1 県 1 年あたりの平均も、NaN を除けば 18,589 人なのに 0 で埋めると 13,941 人と 4 分の 3 に下がる。外部結合の NaN は _merge で由来を確かめ、集計では除くか、別のデータで埋める。

🧠 理解度チェック(SSDSE-B-2026 の実データで)

Q1. 2023 年度で「高齢化率 33% 以上の 19 県」と「出生数 1 万人以上の 20 県」を県名で FULL OUTER JOIN すると何行になる? 両方に入る県は?

A. 19 + 20 − 2 = 37 行。両方に入るのは北海道と新潟県の 2 県で、INNER なら 2 行、LEFT なら 19 行、RIGHT なら 20 行。

Q2. 県名が「東京都」の表と「東京」の表(どちらも 47 県)を県名で外部結合したら 93 行になった。何が起きている?

A. 末尾の「都・府・県」の有無で一致しないため、北海道の 1 行だけが both、残る 46 県が left_only と right_only に 1 行ずつ分かれた(1 + 46 + 46 = 93)。キーを同じ規則でそろえるか、県コードをキーにすれば 47 行になる。

Q3. 564 行どうしを Code だけで結合したら 6,768 行になった。1 県あたり何行で、どう防ぐ?

A. 1 県あたり 12 × 12 = 144 行。キーに年度を加えて (Code, 年度) にし、validate='one_to_one' を付けておけば、キーが一意でないときに MergeError で止まる。

Q4. 2012〜2020 年度の表と 2015〜2023 年度の表を外部結合し、fillna(0) してから出生数の平均を取ると、NaN を除いた平均 18,589 人に対していくつになる? なぜ?

A. 13,941 人。片側にしかない 2012〜2014 年度(141 行)の出生数が「0 人」として分母に入るため。外部結合の NaN は「データが無い」の意味で、0 ではない。

⚠️ NaN を 0 と読んでよい場合 — 「載っていない = 0」の表を LEFT JOIN する

上の節とは逆に、元の表の作り方によっては外部結合の NaN を 0 と読むのが正しいこともある。公表資料では「該当が 1 件以上ある県だけを載せる」表がよくあり、その場合は載っていない県が 0 件という意味になる。どちらなのかはデータではなく、表の作り方(注記)で決まる。

🎯 このコードでやること:2023 年度の保育所等利用待機児童数(J250502)が 1 人以上の県だけを載せた表を作り、47 県のマスタに INNER と LEFT で結合する。LEFT で生じた NaN を 0 にした平均と、INNER の平均と、元の 47 県の列の平均を比べる。

📥 入力例 SSDSE-B-2026(2023 年度) マスタ: 47 県(Code, Prefecture) 右の表: 待機児童数(J250502)が 1 人以上の県だけ(Code, 待機児童数)
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', encoding='cp932', skiprows=[1])
d = df[df['SSDSE-B-2026'] == 2023]
master = d[['Code', 'Prefecture']]                              # 47 県のマスタ
# 公表資料によくある形: 待機児童が 1 人以上いる県だけを載せた表
wait = d.loc[d['J250502'] > 0, ['Code', 'J250502']].rename(columns={'J250502': '待機児童数'})
print('待機児童のいる県だけの表:', len(wait), '行')

inner = master.merge(wait, on='Code', how='inner')
left = master.merge(wait, on='Code', how='left', indicator=True)
print('INNER:', len(inner), '行  LEFT:', len(left), '行', left['_merge'].value_counts().to_dict())

left['待機児童数_0'] = left['待機児童数'].fillna(0)             # この表では「載っていない = 0 人」
print('1 県あたり平均  INNER(載っている県だけ):', round(inner['待機児童数'].mean(), 1),
      ' LEFT + fillna(0)(47 県):', round(left['待機児童数_0'].mean(), 1),
      ' 元の 47 県の列:', round(d['J250502'].mean(), 1))
print('待機児童 0 人の県(先頭 5):', left.loc[left['_merge'] == 'left_only', 'Prefecture'].head().tolist())
📤 実行例(実測) 待機児童のいる県だけの表: 32 行 INNER: 32 行 LEFT: 47 行 {'both': 32, 'left_only': 15, 'right_only': 0} 1 県あたり平均 INNER(載っている県だけ): 83.8 LEFT + fillna(0)(47 県): 57.0 元の 47 県の列: 57.0 待機児童 0 人の県(先頭 5): ['青森県', '山形県', '栃木県', '群馬県', '新潟県']

💬 待機児童が 1 人以上の県は 32 県で、INNER だと 32 行、LEFT だと 47 行(left_only 15 県は青森県・山形県・栃木県・群馬県・新潟県など)。INNER の平均 83.8 人は「待機児童がいる県の平均」で、全国の県の平均ではない。この表は「載っていない = 0 人」なので、LEFT で残した 15 県の NaN を 0 にした平均 57.0 人が、元の 47 県の列の平均 57.0 人と一致する。前の節(年度の範囲が違う表)では 0 で埋めると誤りになった。NaN を 0 にしてよいかは、右の表が「0 件の行を省いた表」か「その期間のデータが無い表」かで決まる。

🗺 概念マップ

本サイト全体の 概念マップ の中で、 外部結合 がどの位置にあるかをテキストツリーで表現する。 視覚的なグラフは 概念マップページ を参照。

[ データエンジニアリング ]
  ├─ 外部結合 (本ページ)
  ├─ 関連手法・派生 → 「🌐 関連手法・派生」セクション
  ├─ 前提概念 → 「🔬 数式を言葉で読み解く」セクション
  └─ 派生・応用 → Anti / Semi / ASOF Join、時系列の merge_asof、分散結合
  

📌 全用語の俯瞰には 概念マップ を、 全用語一覧には 用語集トップ を参照してください。

🔢 数値例による手計算ウォークスルー

外部結合 を「コード任せ」にせず、 一度は手計算してみることで理解が定着する。 ここでは最小サイズのデータで、 値の動きを 1 ステップずつ追う。

数値例:47 vs 30 件の都道府県表を full outer join

SSDSE-B-2026 (47 都道府県) と仮想 D 表 (30 都道府県のみ、 鳥取・島根・福井・山梨・佐賀・宮崎・大分・愛媛・徳島・高知・福島・岩手・秋田・新潟・富山・石川・岐阜 の 17 県を欠く) を都道府県名で結合した結果を 4 方式で比較。

結合方式結果行数NaN 行数用途
INNER JOIN300両方在る対象だけ分析
LEFT OUTER JOIN (B 左)4717 (D 由来列が NaN)B 全件を残したい
RIGHT OUTER JOIN (D 右)300 (D が部分集合のとき)D 全件を残したい (この例では INNER と同等)
FULL OUTER JOIN4717両側を全部残したい

D が完全に B の部分集合の場合、 RIGHT JOIN と INNER JOIN は同じ結果になる。 D に B にないキー (たとえば D に「東北地方」のような集約名が混じる) があると RIGHT JOIN は 30 行を超え、 FULL は 47 を超える。 つまり 結合方式の選び方は「B と D のどちらが部分集合か、 あるいは交差状態か」で決まる。 まず set(B.key) - set(D.key) と set(D.key) - set(B.key) の両方を確認するのが、 結合事故ゼロへの第一歩。

❓ よくある質問(用語固有 10 連発)

外部結合 でよく出る質問 10 件を Q&A 形式でまとめた。 自分の状況に近いものから読んでほしい。

Q1. LEFT と RIGHT の違いは?
A. テーブルの位置だけ。 L LEFT JOIN R ≡ R RIGHT JOIN L。 慣習的に LEFT を使い、 「マスタは左に置く」スタイルが読みやすい。
Q2. FULL OUTER と UNION の違いは?
A. FULL OUTER JOINはキーでマッチした行を統合、 不一致は NULL 列。 UNIONは 2 テーブルを 縦に連結 (列構造が同じ必要)。 全く別の操作。
Q3. OUTER JOIN の結果が遅い
A. (i) 結合キーにインデックス、 (ii) 小さい側を右に置く (Hash Join 戦略)、 (iii) 事前フィルタでテーブルを縮小、 (iv) パーティション結合 (Spark)。
Q4. JOIN 後に NaN が大量に出る
A. 片側にしかないキーが多い証拠。 (i) キー前処理 (strip, lower, 型統一) で照合率を上げる、 (ii) indicator=True でどの側にあるか追跡、 (iii) 必要なら INNER に切替。
Q5. merge と join の違いは pandas で?
A. df.merge() は SQL 風の柔軟な結合 (任意の列をキーに)、 df.join() はインデックスベース。 通常は merge を使う。
Q6. 複合キーでの outer join は?
A. on=['都道府県', '年度'] のようにリスト指定。 SQL なら ON L.pref = R.pref AND L.year = R.year。
Q7. 3 テーブル以上の outer join は?
A. a.merge(b, ...).merge(c, ...) と段階結合。 indicator も段階的に追加。 SQL なら FROM a FULL OUTER JOIN b USING(k) FULL OUTER JOIN c USING(k)。
Q8. OUTER JOIN は INNER より遅い?
A. 原理的にはほぼ同じコスト (走査回数は同等)。 ただし結果行数が増えるので、 後段の処理で差が出る。 plan を必ず EXPLAIN で確認。
Q9. ANTI JOIN って何?
A. 「左にあって右に無い」行のみ返す結合。 LEFT OUTER + WHERE right.k IS NULL で実装。 SQL なら NOT EXISTS、 pandas なら merge + indicator=='left_only'。
Q10. FULL OUTER は商用 RDB なら全部使える?
A. MySQL 8 系は標準で対応していない (UNIONでエミュレート)、 PostgreSQL/Oracle/SQL Server/SQLite は対応。 BigQuery/Snowflake 等の DWH は標準対応。

🏋️ 演習問題(5 問)

学習の定着には、 自分の手を動かすのが一番。 SSDSE-B-2026 や実ログを題材に、 外部結合 を実践する 5 問を用意した。 答えは Python 実装セクションと数値例セクションを参考に組み合わせれば導ける。

  1. 演習 1:SSDSE-B と SSDSE-D を都道府県名で full outer join し、 NaN 行数を indicator で集計せよ。
  2. 演習 2:結合前に str.strip().str.normalize('NFKC') でキー正規化、 表記揺れがマッチ率に与える影響を測れ。
  3. 演習 3:LEFT JOIN と RIGHT JOIN を入れ替えて結果が鏡像になることを確認せよ。
  4. 演習 4:3 つの SSDSE 表 (B, D, E) を full outer join し、 全表にあるキー・1 表のみのキー数を求めよ。
  5. 演習 5:suffixes を意味のあるラベルにして、 結合後に重複列名を分かりやすく区別せよ。

📌 演習を解いて疑問が残ったら、 「よくある質問」セクションに戻るか、 リポジトリの「論文一覧」から類似研究を探して、 実コード (本サイトには 159 本の再現論文) を読むのが最速の理解への道。

🍳 コード・クックブック(外部結合 編)

外部結合 を実務で使う際の頻出パターンを、 動くコードのレシピ集としてまとめた。 必要な料理 (タスク) だけを取り出して使ってほしい。

left / right / outer の 4 方式比較

1
2
3
4
5
6
import pandas as pd
a = pd.DataFrame({'k': ['x','y','z'], 'v_a':[1,2,3]})
b = pd.DataFrame({'k': ['y','z','w'], 'v_b':[20,30,40]})
for how in ('inner','left','right','outer'):
    m = a.merge(b, on='k', how=how, indicator=True)
    print(how, '\n', m, '\n')
📤 実行例(実測) inner k v_a v_b _merge 0 y 2 20 both 1 z 3 30 both left k v_a v_b _merge 0 x 1 NaN left_only 1 y 2 20.0 both 2 z 3 30.0 both right k v_a v_b _merge 0 y 2.0 20 both 1 z 3.0 30 both 2 w NaN 40 right_only outer k v_a v_b _merge 0 w NaN 40.0 right_only 1 x 1.0 NaN left_only 2 y 2.0 20.0 both 3 z 3.0 30.0 both

💬 同じ a(x, y, z)と b(y, z, w)で how だけを変えると、行数は inner 2・left 3・right 3・outer 4 になる。outer は left_only の x と right_only の w を両方残すので、inner の 2 行に 2 行足した 4 行。整数だった v_a・v_b が NaN の入る側だけ 1.0 や 20.0 と小数に変わっているのは、pandas の int 列が NaN を持てず float に格上げされるためで、結合後に ID 列が小数になる事故の原因になる。

複合キーで結合 + 型統一

1
2
3
4
5
6
7
import pandas as pd
a = pd.DataFrame({'pref':['東京','大阪'], 'year':[2023, 2023], 'v_a':[1,2]})
b = pd.DataFrame({'pref':['東京', '大阪', '京都'], 'year':[2023.0, 2024.0, 2023.0], 'v_b':[10,20,30]})
# year が int と float で型不一致 → 結合失敗の可能性
b['year'] = b['year'].astype(int)
m = a.merge(b, on=['pref','year'], how='outer', indicator=True)
print(m)
📤 実行例(実測) pref year v_a v_b _merge 0 京都 2023 NaN 30.0 right_only 1 大阪 2023 2.0 NaN left_only 2 大阪 2024 NaN 20.0 right_only 3 東京 2023 1.0 10.0 both

💬 year を int に揃えたうえで複合キー (pref, year) で結合すると、キーが完全一致したのは東京 2023 の 1 行だけ。大阪は a が 2023 年、b が 2024 年なので別の行(left_only と right_only)に分かれ、京都は b にしか無い。型を揃えても値そのものがずれていれば一致しないので、「大阪 2 行」を見たら年度の取り違えを疑う。

巨大表を broadcast hint で結合 (Spark)

1
2
3
4
5
6
7
8
# from pyspark.sql import SparkSession
# from pyspark.sql.functions import broadcast
# spark = SparkSession.builder.appName('outer-join').getOrCreate()
# big = spark.read.parquet('data/processed/big_transactions.parquet')
# small = spark.read.parquet('data/processed/master.parquet')
# joined = big.join(broadcast(small), on='id', how='left_outer')
# print(joined.count())
# 上記疑似コード(Spark 環境で実行)。 broadcast hint で小表を全ノードに配布、 巨大表との外部結合を高速化。
📤 実行例(実測) このブロックは標準出力を出さない(図を描く・変数を定義するだけ)。

📌 すべてのレシピは「実データ前提・引数なし直書き」スタイル。 そのままコピペで動くよう設計したので、 まずは動かしてから読むのを推奨する。

🙋 著者・教材設計について

本教材は、 広島工業大学 統計・データ解析コンペティション参加学生向けに、 松本伸平 (s.matsumoto.gk@cc.it-hiroshima.ac.jp) が監修・執筆した。 「論文を読む過程で出てきた専門用語を、 その場で 5 分で補完して論文に戻る」というジャストインタイム型の学習体験を目的に、 514 用語ページを統一フォーマットで整備している。

本ページ「外部結合」は データエンジニアリング カテゴリに属し、 同カテゴリ内の他用語と相互リンクされている。 用語間の関係性は 概念マップ でも俯瞰できる。

本サイトは SSDSE (教育用標準データセット, 独立行政法人統計センター) を主データとして使い、 「合成データ ではなく実公的データを使う」 を方針に据えている。 ハンズオン教材の質を最大化するための判断である。

outer join 外部結合 日本語 英語 中国語 ドイツ語 フランス語

🔗 隣接手法への橋渡し

「外部結合」は単独で完結せず、 前後の手法と組み合わさって価値が発揮される。 入力データの準備 (上流)・同目的の代替手法との比較 (並列)・結果の活用 (下流) という 3 軸で隣接領域を整理する。

この上流・並列・下流の対応を地図化することで、 「外部結合」を中核に据えた分析パイプライン (データ準備 → 手法選択 → 結果の検証と展開) の全体像が見えてくる。

🌳 手法選択フロー

「外部結合」を実際に使うとき、 何をどう選ぶかを順に判断する。 上から順に答えていくと、 使うべき手法と評価の仕方が決まる。

  1. 片方にしか無い行をどう扱うか
    残すなら外部結合、 落とすなら内部結合。 「落としたつもりが無い」まま内部結合すると、 行が静かに減る。 結合前後で行数を必ず数える。
  2. どちら側を残すか
    左を全部残すなら LEFT、 両方なら FULL OUTER。 「マスタに無いコードが来ていないか」を調べたいなら、 あえて外部結合して欠測を見る。
  3. キーは一意か
    結合キーが重複していると、 行数が掛け算で増える。 SSDSE-B-2026 を地域コードだけで結合すると、 12 年分が掛かって 12 倍になる。 結合前に duplicated() で確認する。
  4. 欠測をどう埋めるか
    外部結合で生じた NaN は「値が無い」であって 0 ではない。 0 で埋めてよいかは、 その列が件数なのか金額なのか比率なのかで変わる。

外部結合は「合わなかった行」を可視化する道具でもある。 結合してから欠測を数えると、 データの不整合が見つかることが多い。