💡 30秒で分かる結論
🍰 まずはやさしく
共通点があるデータだけを集める方法です。
必要な情報だけをまとめて分析するために使います。
部活の名簿と出席簿を合わせて確認するような操作です。
この手法のポイントと注意点を短時間で学びます。
両方のテーブルに存在するキーのみ残す結合
分野 :データ処理 — 📚 データエンジニアリング
用途 :分析・前処理・モデル構築・解釈支援などの場面で使われます
注意 :適用条件と限界を理解してから使うのが鉄則
inner join を 30 秒で把握する重要ポイント:
何ができるか : inner join は統計・データ分析で特定の目的のために使う概念・手法。 詳細は後続の各章を参照。
いつ使うか : 適切な前提条件下で、 他の手法より優位な場面で使う。 適用範囲と限界を理解することが重要。
注意点 : 前提確認 / 過大解釈の回避 / 他手法との比較検証 が不可欠。
関連 : 上位概念・並列手法・派生形をネットワークで理解すると応用の幅が広がる。
📍 あなたが今見ているもの
🍰 まずはやさしく
2つの表を横につなげる操作のことです。
バラバラの表から共通のデータを探すために使います。
スマホの連絡先とSNSのアカウントを紐付ける例です。
内部結合という操作の内容について詳しく読みます。
あなたは今、 内部結合 (Inner Join) という、 2 つの表 (DataFrame) を共通キーで横方向に連結する操作の用語ページを見ています。 SQL の INNER JOIN、 pandas の pd.merge(..., how='inner') に相当し、 「両方の表に存在するキーだけ残す」点が 外部結合 との決定的な違いです。 SSDSE-B-2026 のような公的統計を複数年・複数指標で組み合わせるときの基本動作です。
🎨 直感で掴む
🍰 まずはやさしく
データを使いやすく整えるパズルのようなものです。
分析しやすい形に情報をまとめるために使います。
買い物リストと店の在庫表を照らし合わせる例です。
使い方の流れや注意点を順番に見ていきましょう。
データを「使える形に整える」工程。 結合・集約・フィルタリングが基本操作。
本ページでは 内部結合 を、 定義・前提条件・使い方・落とし穴の順に整理して解説します。 厳密な定義より、 まず何を、 いつ、 どう使うか を理解することを優先してください。
inner join を直感的に理解するには、 「具体的な日常の場面に当てはめる」のが効果的。 抽象的な定義から入るより、 「これは何の役に立つのか」「どんな場面で使うか」「他の手段とどう違うのか」の 3 点を先に押さえることで、 数式や手続きの理解が定着しやすい。
本ページでは inner join を「データから意思決定までの一連のプロセス」に位置付け、 (1) 入力、 (2) 処理、 (3) 出力、 (4) 解釈 の 4 段階で順に解説する。 この枠組みで他の手法と比較すれば、 inner join の独自性と共通性が見えてくる。
具体例として、 SSDSE-B-2026 のデータを使い、 inner join を実際に動かすイメージを次節以降で示す。 数式 → 値代入 → 手計算 → Python 実装 の流れで、 抽象と具体を行き来しながら理解を深める。
🔬 数式を言葉で読み解く (詳細版)
内部結合は集合論で書くと次のとおりです。 2 つのリレーション $R(K, A)$ と $S(K, B)$ があり、 $K$ が共通キー、 $A$, $B$ がそれ以外の属性のとき:
$$R \bowtie_K S = \{ (k, a, b) \mid (k, a) \in R \land (k, b) \in S \}$$
記号の読み方
記号 意味 SSDSE-B-2026 での具体例
$R$左の表 人口表 (Code, Prefecture, A1101)
$S$右の表 経済表 (Code, C3301) ※着工建築物数
$K$共通キー Code (R01000 〜 R47000)
$\bowtie$結合演算 (ジョイン) pd.merge(df1, df2, on='Code')
$\land$論理 AND (両方真) 「両方の表に Code が存在する」
「両方に存在する」を 1 行ずつ追う
行 $r=(k_1, a_1)$ が $R$ にあるとします。 内部結合の出力に $r$ が現れる条件は「$S$ に $k_1$ を持つ行が存在する」こと。 もし $S$ に $k_1$ が複数あれば、 $r$ はそれぞれと組まれて出力行が増えます (デカルト積)。 $S$ に $k_1$ がなければ、 $r$ は出力に現れません (情報損失)。 したがって出力行数 $n_{\bowtie}$ は次で表されます:
$$n_{\bowtie} = \sum_{k \in K_R \cap K_S} n_R(k) \cdot n_S(k)$$
$K_R$ は $R$ のキー集合、 $K_R \cap K_S$ がいわゆる「両方に存在するキー集合」です。 SSDSE-B-2026 の都道府県コードはすべてユニーク (重複なし) なので、 結合後も 47 行で済みます。
🔬 関連手法比較表 (結合の種類)
結合 残る行 pandas SSDSE 例での出力行数 用途
内部 inner 両方にあるキーのみ how='inner'46 (沖縄欠ければ 46) 完全データ抽出
左 left 左側を全部残す how='left'47 (沖縄は NaN) マスタ拡充
右 right 右側を全部残す how='right'46 使用頻度は低い
外 outer どちらかにあれば残す how='outer'47 (沖縄含む、 NaN 付き) 差分検出
交差 cross 全組合せ how='cross'47 × 47 = 2,209 全ペア比較
縦 concat 行方向に追加 pd.concat([a,b])94 行 縦に伸ばす別操作
💔 内部結合の失敗例 5 連発
キー型不一致 : 左 Code が文字列 'R13000'、 右が整数 13000。 全行 both ゼロで気づかず空集合に。 対策 : df.dtypes で確認、 astype(str) で揃える。
余分な空白 : 'R13000' と 'R13000 ' (末尾空白)。 内部結合で消える。 対策 : df['Code'] = df['Code'].str.strip()。
多対多結合 : 顧客 ID が両方で重複していて、 結合後行数が想定 1,000 → 実際 27,000 に膨張。 対策 : validate='one_to_one' または事前 drop_duplicates。
NaN を残した結合 : 左の Code に NaN があると pandas は inner で除外。 「47 行のはずが 46」を見落とす。 対策 : 事前 dropna(subset=['Code'])。
サイレントな大量削除 : 結合前 100 万行 → inner 結合で 3,000 行に。 ログ出さず気づかない。 対策 : 必ず print(len(before), len(after)) を併記、 削減率 90% 超なら警告。
📝 演習問題 5 問
Q1 : SSDSE-B-2026 から「人口 A1101」と「着工建築物数 C3301」の 2 表を作り、 inner join した結果の shape を答えよ。 (答: (47, 4))
Q2 : 右表から東京 (R13000) を抜くと、 inner join の結果は何行になるか。 (答: 46)
Q3 : 左に都道府県 47 件、 右に「東京都の市区町村」3 件 (Code: R13000) があるとき、 on='Code' の内部結合は何行になるか。 (答: 3 行、 1×3)
Q4 : 左にキー 'R01000' が 2 行、 右に 1 行あるときの inner join 行数は? (答: 2 × 1 = 2 行)
Q5 : validate='one_to_many' を渡したとき、 左右どちらの重複が許されるか。 (答: 右側のみ重複可。 左はユニーク)
📖 関連用語辞典 10 語
データ結合 テーブルを連結する全般。 inner は最も厳格な方式。
外部結合 片側に無いキーも NaN 付きで残す方式。 損失回避用途。
主キー テーブル内で行を一意に識別するキー。 結合の信頼度を担保。
外部キー 他テーブルの主キーを参照するキー。 結合の根拠。
SQL 結合を宣言的に書く言語。 INNER JOIN ... ON。
pandas Python の表操作ライブラリ。 merge, join, concat を提供。
データベース 結合演算の本来の住処。 索引で高速化。
テーブル 行と列の構造。 結合の最小単位。
Tidy data 結合しやすい正規化形式。 1 観測 1 行。
欠損値 結合で行が消えたとき / NaN が増えたときに発生。 診断必須。
🔬 Inner Join のキー設計 — 結合精度を支える 7 つの実務判断
Inner Join は「両側の表に同じキー値が存在する行だけを残す」最も基本の結合だが、 実務では キーの設計次第で「正しいはずの結合が消える」「データが重複爆発する」 といった事故が日常的に起きる。 ここでは SSDSE-B-2026(都道府県 × 年度)を中心に、 Inner Join を堅牢に使うための 7 つの判断軸を整理する。 これらは単なる SQL の syntax 知識ではなく、 「結合前にどんな前処理が必要か」「結合後にどんな検証が必要か」を含む、 データエンジニアリングの実践知である。
判断軸
具体的なチェック項目
失敗時の症状
SSDSE-B-2026 で発生する例
1. キー型の一致
左右表で dtype が一致しているか(int64 vs object など)
マッチ件数 0 件で警告も出ない
都道府県コード R01000 が片方は文字列・片方は整数化されている
2. 前後空白・改行
"北海道 " と "北海道" は別物
9 割マッチ・1 割は脱落で気づきにくい
Excel 経由でコピペした都道府県名に末尾空白
3. 全角半角
数字や英字が全角半角混在していないか
同じ意味のキーが別 ID として扱われる
年度 2026 と 2026 が混在
4. NULL の扱い
NULL 同士は結合されない(SQL 仕様)
必要な行が消える
調査未実施年度の都道府県が脱落
5. カーディナリティ
1:1 / 1:N / N:N のどれか事前に確認
N:N で行数が爆発し OOM
「都道府県名」だけで結合し全市区町村×全年度の直積が発生
6. 複合キー
単独キーで一意でないなら on=[...] でリスト指定
同じキーで複数行マッチし重複
「県名+年度」が必要なのに県名だけで結合
7. 順序の保存
Inner Join 後は行順が保証されない
時系列分析で順序がバラバラ
年度順に並んでいたデータが結合後にシャッフル
この 7 軸は 「結合の前後で必ず検証」 がポイントである。 とくに 1(型)と 2(空白)は Inner Join 失敗の 8 割以上の原因 を占めると言われる。 Pandas であれば df['key'].dtype と df['key'].str.strip() を機械的に挟む癖をつけたい。
さらに、 結合後の行数を 「期待行数の範囲」 で assertion する習慣も重要である。 SSDSE-B-2026 で「都道府県×年度」をキーに結合するなら、 結合後の行数は最大 47×年度数のはずだ。 これを超えていれば N:N 結合の重複が疑われる。 単純な assert len(merged) <= 47 * n_years という 1 行が、 後段の集計エラーを未然に防ぐ防波堤になる。
🧮 結合前後の検証パターン
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口) L3221(消費支出(二人以上の世帯))
北海道 5,092,000 296,888
東京都 14,086,000 341,320
沖縄県 1,468,000 251,222
…(全 47 行)
📋 コピー 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 import pandas as pd
# SSDSE-B-2026 から人口テーブルと消費支出テーブルを別々に取得した想定
pop = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , skiprows = [ 1 ], encoding = 'cp932' )[[ 'Code' , 'Prefecture' , 'SSDSE-B-2026' , 'A1101' ]]
inc = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , skiprows = [ 1 ], encoding = 'cp932' )[[ 'Code' , 'Prefecture' , 'SSDSE-B-2026' , 'L3221' ]]
# 結合前のサニティチェック
assert pop [ 'Code' ] . dtype == inc [ 'Code' ] . dtype , 'キー型不一致'
pop [ 'Prefecture' ] = pop [ 'Prefecture' ] . str . strip ()
inc [ 'Prefecture' ] = inc [ 'Prefecture' ] . str . strip ()
# Inner Join
merged = pd . merge ( pop , inc , on = [ 'Code' , 'SSDSE-B-2026' ], how = 'inner' , suffixes = ( '_pop' , '_inc' ))
# 結合後のサニティチェック (47 都道府県 × 年度数 が上限)
n_years = pop [ 'SSDSE-B-2026' ] . nunique ()
assert len ( merged ) <= 47 * n_years , f '重複疑い: { len ( merged ) } 行'
print ( f '結合行数: { len ( merged ) } / 上限 { 47 * n_years } ' )
📤 実行例(実測)
結合行数: 564 / 上限 564
📊 Inner Join と他結合の振る舞いを SSDSE-B-2026 で完全比較
Inner Join を正しく理解するには、 同じデータに Left / Right / Outer を適用したときに何件残るか を体感するのが最短ルートである。 ここでは SSDSE-B-2026 から「全 47 都道府県の人口テーブル」と「一部 9 県のみの延べ宿泊者数(観光)テーブル」を作り、 4 種類の結合の差分を一覧化する。 「件数の差」がそのまま「どの結合を選ぶべきか」の判断材料になる。
結合種別
残る行数
NaN の発生箇所
向いている分析
注意点
Inner
9
なし
「両方の値が揃った県だけで相関を見たい」
情報損失が大きい場合あり
Left
47
右表(延べ宿泊者数)に 38 県分の NaN
「人口は全県で集計、 観光は任意」
NaN の集計方法を決めておく必要
Right
9
なし(左の人口は全県あるため)
「観光がある県を基準に分析」
Inner と結果が同じになりやすい
Outer
47
右表に 38 県分の NaN
「どの県が観光データを持つか俯瞰」
分析というより俯瞰用途
🐍 4 結合を一気に試す Python
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口) G7101(延べ宿泊者数)
北海道 5,092,000 32,783,470
東京都 14,086,000 80,273,650
沖縄県 1,468,000 20,038,190
…(全 47 行)
📋 コピー import pandas as pd
pop = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , skiprows = [ 1 ], encoding = 'cp932' ) . query ( '`SSDSE-B-2026` == 2023' )[[ 'Code' , 'Prefecture' , 'A1101' ]]
# 延べ宿泊者数テーブルは一部県のみと想定 (北海道・東京・京都・大阪・沖縄など 9 県)
focus = [ 'R01000' , 'R13000' , 'R26000' , 'R27000' , 'R47000' , 'R14000' , 'R23000' , 'R40000' , 'R34000' ]
tour = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , skiprows = [ 1 ], encoding = 'cp932' ) . query ( '`SSDSE-B-2026` == 2023' ) . query ( 'Code in @focus' )[[ 'Code' , 'G7101' ]] . rename ( columns = { 'G7101' : '延べ宿泊者数' })
for how in [ 'inner' , 'left' , 'right' , 'outer' ]:
m = pd . merge ( pop , tour , on = 'Code' , how = how )
print ( f ' { how : 5s } 行数: { len ( m ) : 3d } NaN 件数: { m . isna () . sum () . sum () } ' )
📤 実行例(実測)
inner 行数: 9 NaN 件数: 0
left 行数: 47 NaN 件数: 38
right 行数: 9 NaN 件数: 0
outer 行数: 47 NaN 件数: 38
この比較から導かれる実務指針は 「分析の前提(全県か一部県か)に合致する結合を選ぶ」 である。 たとえば回帰分析で「全 47 県の人口で観光需要をモデル化」したいなら、 Left で 38 県の NaN を含めて欠損補完するか、 Inner で 9 県だけで限定モデルを作るか、 という判断が必要になる。 「とりあえず Inner」と思考停止すると、 重要な県が分析対象から落ちてしまう。
✅ 理解度チェック — Inner Join 8 問
Inner Join を「使える」レベルに引き上げるための 8 問。 単なる用語暗記ではなく、 結合の前後でどんな前処理・検証を組み合わせるかを問う実務寄りの問題群である。
#
設問
解答
Q1
Inner Join の最も基本的な性質は?
両表に「同じキー値」が存在する行だけを残す
Q2
Inner Join で結合件数が想定より少ない時、 最初に疑うべきことは?
キーの型 (dtype) と前後空白
Q3
SQL で NULL 同士は Inner Join されるか?
されない(仕様)
Q4
結合キーが N:N の時、 結合後の行数はどうなる傾向か?
直積に近づき爆発的に増加する
Q5
複合キーで結合するときの pandas 構文は?
pd.merge(a, b, on=['key1','key2'])
Q6
Inner Join 後に行順は保証されるか?
保証されない (必要なら sort_values で再ソート)
Q7
SSDSE-B-2026 で「県名」だけ Inner Join するのは安全か?
単年なら可、 複数年なら「県名+年度」が必要
Q8
Inner Join と Right Join が同じ件数になる条件は?
右表のキーがすべて左表に存在する場合
8 問中 6 問以上正答なら Inner Join を業務に投入できるレベル。 4 問以下なら、 まず「キーの型と空白を結合前に必ず揃える」「結合後に件数 assertion を入れる」の 2 点を習慣化することから始めるとよい。
関連トピックとして 外部結合 (Outer Join) 、 左外部結合 (Left Join) 、 外部キー (Foreign Key) 、 複合キー (Composite Key) 、 正規化 (Normalization) も参照すると、 結合戦略全体の見通しが立ちやすい。
🧩 Inner Join のパフォーマンス特性と最適化
Inner Join は単純な操作に見えるが、 内部的には 3 つの代表的なアルゴリズム が使い分けられる。 すなわち Nested Loop Join、 Hash Join、 Sort-Merge Join である。 SSDSE-B-2026 のように行数が数万件程度なら気にする必要はないが、 数百万行〜数億行のテーブルでは選択を誤ると 処理時間が桁違いに変わる 。 RDB の Optimizer は通常自動で選択するが、 Pandas や Spark など API 層では明示的に選ぶことも多い。
アルゴリズム
計算量
メモリ要件
向く場面
SSDSE-B-2026 規模での体感
Nested Loop
O(N×M)
最小
片方が極小 (数十行)
47×47 程度なら一瞬
Hash Join
O(N+M)
大 (ハッシュ表)
中規模〜大規模で等価結合
Pandas のデフォルトに近い
Sort-Merge
O((N+M)log)
中
既ソート or 範囲結合
時系列の連続区間結合で有利
最適化の実務観点では 「キーにインデックスを張る」「キー列の dtype を category に変える」「不要な列は結合前に drop する」 の 3 つが最も効果的である。 とくに Pandas で巨大データを扱う場合、 結合前に df = df[['key','val']] と必要列だけに絞ると、 メモリ使用量が劇的に減り、 結果として結合自体も高速化する。
🔧 Pandas で結合を高速化する 3 つのテクニック
キー列を index 化 : df.set_index('key') 後に join を使うと、 Hash Join 相当の高速化が得られる。
category 型へ変換 : 都道府県名のようなカテゴリは astype('category') でメモリ削減とハッシュ高速化の両方が効く。
列の絞り込み : 結合に不要な列を drop しておくと、 結合後のメモリピークが下がる。
これらは数万行のデータでは差を感じにくいが、 数百万行を超えるあたりから 体感で 2〜10 倍の差 が出る。 EDA のうちは気にしなくてよいが、 本番パイプラインに乗せる段階では押さえておきたい。
🧮 SSDSE-B-2026 実値で計算
SSDSE-B-2026 (47 都道府県) を 2 つに分割し、 Code をキーにして内部結合します。 想定シナリオ: 「人口指標 (A1101)」と「着工建築物数 (C3301)」を別 CSV から取り込み、 1 つの分析用テーブルに統合する。
手順 操作 結果
① 左表 df_pop: Code, Prefecture, A1101 47 行 × 3 列
② 右表 df_house: Code, C3301 47 行 × 2 列
③ merge(how='inner', on='Code') 47 行 × 4 列
④ 仮に右表から沖縄(R47000)を除外 46 行 (内部結合で沖縄が脱落)
手計算で確認 : 共通キー集合の大きさ $|K_R \cap K_S|$ を数える。 北海道 R01000 が両方にあれば +1、 青森 R02000 が両方にあれば +1、 ... 沖縄 R47000 が片方にしかなければ +0。 SSDSE-B-2026 オリジナルでは 47 個すべて共通なので 47 行。
🧮 数式に値を入れて手で計算する: INNER JOIN 結果
合成データで A (顧客) と B (注文) の INNER JOIN 結果を計算する。
Step 1: テーブル
A: customer_id ∈ {1, 2, 3, 4, 5}
B: customer_id = [1, 2, 5, 6, 2, 7]
B 行数 = 6
Step 2: INNER JOIN 結果
マッチング B の customer_id ∈ {1, 2, 2, 5} → 4 行
{6, 7} は A にないので除外
INNER JOIN 結果 = 4 行
🐍 Python で再現
📋 コピー import pandas as pd
A = pd . DataFrame ({ 'customer_id' :[ 1 , 2 , 3 , 4 , 5 ], 'name' :[ 'a' , 'b' , 'c' , 'd' , 'e' ]})
B = pd . DataFrame ({ 'customer_id' :[ 1 , 2 , 5 , 6 , 2 , 7 ], 'amount' :[ 100 , 200 , 300 , 400 , 500 , 600 ]})
joined = pd . merge ( A , B , on = 'customer_id' , how = 'inner' )
print ( f "結果行数: { len ( joined ) } " )
print ( joined )
📤 実行結果
結果行数: 4
customer_id name amount
0 1 a 100
1 2 b 200
2 2 b 500
3 5 e 300
💬 手計算 (Step 2) 4 行と Python 出力が完全一致。
🐍 Python での扱い
SSDSE-B-2026 のような公的統計データを Python で扱う際の基本パターン:
📥 入力例(SSDSE-B-2026 全体:564 行 × 112 列 = 47 都道府県 × 2012〜2023 年)
年度 地域コード 都道府県 A1101(総人口) A1303(65歳以上人口) A4101(出生数) …
2023 R01000 北海道 5,092,000 1,681,000 24,430 …
2023 R13000 東京都 14,086,000 3,205,000 86,348 …
2023 R47000 沖縄県 1,468,000 350,000 12,549 …
…(残り 112 列は住宅・家計・教育・医療など)
📋 コピー 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 ] ) ; df = df [ df [ 'SSDSE-B-2026' ] == 2023 ]
print ( df . shape )
print ( df . dtypes )
print ( df . describe ())
# 「内部結合」の文脈で扱う場合の例:
# 分野: データ処理
# 関連手法は同カテゴリの他用語を参照してください。
📤 実行例(実測)
(47, 112)
SSDSE-B-2026 int64
Code object
Prefecture object
A1101 int64
A110101 int64
...
L322106 int64
L322107 int64
L322108 int64
L322109 int64
L322110 int64
Length: 112, dtype: object
SSDSE-B-2026 A1101 ... L322109 L322110
count 47.0 4.700000e+01 ... 47.000000 47.000000
mean 2023.0 2.645809e+06 ... 27409.148936 55356.936170
std 0.0 2.797551e+06 ... 4495.381291 6610.419905
min 2023.0 5.370000e+05 ... 18374.000000 35708.000000
25% 2023.0 1.034000e+06 ... 25662.500000 51114.500000
50%
…(以下略)
具体的なコードは データエンジニアリング を参照してください。
📝 レポートでの報告
分析結果を報告するときに含めるべき情報:
使ったデータ :出典・期間・サンプル数
適用条件の確認 :前提が満たされているか
計算結果 :数値だけでなく不確実性(CI・SE)も
解釈 :何を意味するか、 何を意味しないか
限界 :適用範囲外への拡張は避ける
✅ チェックリスト
□ 「内部結合」を使う場面か再確認したか
□ データの尺度・分布・サンプル数を確認したか
□ 前提条件を満たしているか
□ 計算した値だけでなく不確実性も把握したか
□ 解釈と限界を区別したか
□ 関連グループ教材で全体像を確認したか
🐍 Python 実装 (4 要素遵守)
コード A: 2 表を作って inner join
🎯 このコードでやること : SSDSE-B-2026 を読み込み、 人口側 (A1101) と着工建築物側 (C3301) に分割し、 pd.merge で内部結合する。 結合後の行数とキー一致を検証する。
📥 入力データ (SSDSE-B-2026 抜粋) :
Code Prefecture A1101 C3301
0 R01000 北海道 5092000 15872
1 R02000 青森県 1184000 5043
2 R13000 東京都 14086000 41817
3 R47000 沖縄県 1468000 5217
📋 コピー 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 ]
df_pop = df [[ 'Code' , 'Prefecture' , 'A1101' ]] . copy ()
df_house = df [[ 'Code' , 'C3301' ]] . copy ()
joined = pd . merge ( df_pop , df_house , on = 'Code' , how = 'inner' )
print ( '左 :' , df_pop . shape )
print ( '右 :' , df_house . shape )
print ( '結合:' , joined . shape )
print ( joined . head ( 3 ))
📤 実行結果 :
左 : (47, 3)
右 : (47, 2)
結合: (47, 4)
Code Prefecture A1101 C3301
0 R01000 北海道 5092000 15872
1 R02000 青森県 1184000 5043
2 R03000 岩手県 1163000 5731
💬 結果の読み方 : 左 47 行 × 右 47 行で結合後も 47 行。 共通キー Code がすべて一致しているため情報損失なし。 列数は 3 + 2 − 1 (キー重複) = 4 列。
コード B: 片側からキーを欠落させて損失を観察
🎯 このコードでやること : 右表から沖縄 (R47000) を意図的に除外して内部結合し、 行数が 46 に減ることを確認する。
📥 入力 : df_house から沖縄を除外
📋 コピー df_house_partial = df_house [ df_house [ 'Code' ] != 'R47000' ]
joined2 = pd . merge ( df_pop , df_house_partial , on = 'Code' , how = 'inner' )
print ( '結合後行数 :' , len ( joined2 ))
print ( '沖縄は残ってる?' , ( joined2 [ 'Code' ] == 'R47000' ) . any ())
📤 実行結果 :
結合後行数 : 46
沖縄は残ってる? False
💬 結果の読み方 : 右表に沖縄がないため、 内部結合では沖縄行が消える。 これが「情報損失」の典型例。 残したい場合は 外部結合 (left/right/outer) を使う。
コード C: indicator で結合結果を診断
🎯 このコードでやること : indicator=True オプションを付けて、 各行が「both / left_only / right_only」のどれだったかを記録する。 まず outer で実行し、 inner との差分を可視化。
📥 入力 : df_pop 47 行と df_house_partial 46 行
📋 コピー diag = pd . merge ( df_pop , df_house_partial , on = 'Code' , how = 'outer' , indicator = True )
print ( diag [ '_merge' ] . value_counts ())
print ( diag [ diag [ '_merge' ] != 'both' ][[ 'Code' , 'Prefecture' ]])
📤 実行結果 :
both 46
left_only 1
right_only 0
Name: _merge, dtype: int64
Code Prefecture
46 R47000 沖縄県
💬 結果の読み方 : left_only が 1 件 → 沖縄が左にしかない。 inner join なら除外、 left join なら NaN 付きで残る。 結合前に必ず indicator で診断すると不整合の発見が早い。
コード D: validate で 1:1 結合を強制
🎯 このコードでやること : validate='one_to_one' オプションで、 キーが両側でユニークであることを宣言。 重複があれば即エラーで気づける。
📥 入力 : df_pop, df_house (47 都道府県、 ユニーク Code)
📋 コピー joined_v = pd . merge ( df_pop , df_house , on = 'Code' , how = 'inner' , validate = 'one_to_one' )
print ( 'OK 1:1 結合成立, 行数:' , len ( joined_v ))
📤 実行結果 :
OK 1:1 結合成立, 行数: 47
💬 結果の読み方 : 47 行と表示され、 例外もない → キーは両側ユニーク。 もし片方に重複があれば MergeError で停止し、 デカルト積による行数膨張を未然に防げる。
🏭 産業界での活用事例 6 件
EC サイトの売上分析 : 顧客マスタ × 注文履歴を customer_id で inner join し、 「実在する顧客の注文」だけ抽出。 退会顧客やテスト顧客が混じらない。
金融与信スコアリング : 申込書テーブル × 信用情報テーブルを内部結合し、 信用情報のある申込者のみを学習データに採用。
製造業 IoT : センサーログ × 設備マスタを device_id で結合し、 マスタに登録された正規設備からのログだけ採用 (野良デバイスを除外)。
地方創生 / 都道府県分析 : SSDSE-B-2026 (経済) と SSDSE-C (家計) を Prefecture コード で内部結合し、 両方そろっている都道府県だけ比較分析。
医療レセプト分析 : 患者マスタ × レセプト明細を patient_id で結合し、 同意取得済の患者の医療行為だけを集計。
広告効果測定 : クリックログ × CV (コンバージョン) ログを session_id で内部結合し、 クリック後 CV が発生したセッションだけのファネル分析。
⚠️ よくある落とし穴
❌ 結合での行数膨張
merge 後の行数を必ず確認。
❌ 欠損の扱い
NaN を 0 にすると「測定値 0」と区別できなくなる。
❌ 日付型
datetime64 に変換しないと正しい比較ができない。
inner join を実務で使う際の典型的失敗パターン。 SSDSE-B-2026 など複数テーブルを跨ぐ結合は、 事前のキー検証なしに走らせると静かにデータが落ちる事故が多い。
キー値の表記揺れで行が一致しない : 「東京都」と「東京」、 「北海道」と「北 海道」(全角スペース)、 「神奈川県」と「神奈川」など、 結合キーの正規化が甘いと inner で空に近い結果になる。 対策: df['pref'].str.strip().str.replace(' ','') や正規辞書で事前統一。
暗黙の行落ちに気付かない : 左 47 行 × 右 47 行を merge した結果が 32 行でも警告は出ない。 対策: indicator=True で _merge 列を確認し、 both 以外の比率を必ずログ出力。
キーが多対多になり行数爆発 : 都道府県コード(A1101)を主キーと思い込んでも、 年度違いで重複していると 47 × 47 = 2,209 行に膨張する。 対策: 結合前に validate='one_to_one' または 'many_to_one' を指定して contract 検査。
dtype の不一致で結合不成立 : 左の都道府県コードが int64、 右が str (cp932 読込で文字列化) だと inner で 0 行になる。 対策: 結合直前に df['code'] = df['code'].astype(str).str.zfill(2) で型を揃える。
欠損キーの行が黙って消える : SSDSE-B-2026 で一部年の値が NaN のとき、 NaN キー同士は inner では一致せず除外される (SQL の NULL ≠ NULL と同じ挙動)。 対策: 結合前に dropna(subset=['key']) か、 欠損保持が必要なら left/outer 切替を検討。
列名衝突で suffix の解釈ミス : 両テーブルに population 列があると pandas は population_x / population_y を自動付与。 どちらが「結合前」「結合後」かを誤読すると分析全体が逆転する。 対策: suffixes=('_left','_right') を明示。
🌐 関連手法・派生
🧭 拡張 Q&A シナリオ集 (15 ケース)
内部結合 を実務で使うときに起こりがちな状況を 15 ケース取り上げ、 それぞれに「症状 → 原因 → 対処」を整理します。 SSDSE-B-2026 (47 都道府県) を題材とします。
S1: データが想定より少ない
症状 : 47 件あるはずなのに 38 件しか出てこない。 原因 : フィルタ条件で意図せず除外、 結合の inner で脱落、 型不一致による無音脱落。 対処 : フィルタ前後の shape をログ。 indicator=True で結合不整合を確認。
S2: 全国平均と都道府県値が合わない
症状 : 47 都道府県の単純平均と「全国」値が一致しない。 原因 : 全国は人口加重平均なので算術平均と異なる。 対処 : (df['値']*df['A1101']).sum()/df['A1101'].sum() で加重平均を算出。
S3: 沖縄が外れ値で結論が変わる
症状 : 沖縄を含む / 含まないで分析結果が反転。 原因 : 沖縄は気候・人口構成が他県と大きく異なる。 対処 : 必ず「沖縄を含む / 除外」の双方で結果を報告。
S4: コード列が文字列か数値か
症状 : Code 列が int64 と object で結合できない。 原因 : read_csv のデフォルト推論差。 対処 : dtype={'Code': str} を明示。
S5: 列名に全角スペースが混入
症状 : df['Prefecture'] で KeyError。 原因 : ヘッダに全角スペース・改行コードが含まれる。 対処 : df.columns = df.columns.str.strip() で正規化。
S6: 結合後に重複行が増える
症状 : 47 + 47 → 100 行を超える。 原因 : 多対多結合。 対処 : validate='one_to_one' で MergeError として早期検出。
S7: 数値が文字列のまま処理される
症状 : df['A1101'].mean() が TypeError。 原因 : CSV で人口列が「'5,092,000'」のようなカンマ区切り。 対処 : str.replace(',','').astype(int) で数値化。
S8: 結果が小数になる
症状 : 「人口あたり病院数」が 0.000038 と読みにくい。 原因 : 単位の不整合。 対処 : 人口 10 万人あたり、 1 万人あたり等にスケール。
S9: 標準化忘れで比較が無意味
症状 : 「東京の着工建築物数が多い → 政策成功」と結論。 原因 : 人口正規化忘れ。 対処 : 必ず比率にしてから比較。
S10: バージョン差で挙動が変わる
症状 : 同じコードが pandas 1.x と 2.x で結果違う。 原因 : API 変更 (append 廃止、 dtype 推論変更)。 対処 : pd.__version__ をログ、 requirements.txt をピン留め。
S11: メモリ不足で kill される
症状 : 数百万行で OOM。 原因 : object 列の文字列が大量メモリ。 対処 : category 化、 chunksize 分割、 必要列のみ読み込み (usecols=)。
S12: 並列処理で順序保証なし
症状 : 並列実行で結果順がブレる。 原因 : マージ後に並びが保証されない。 対処 : 必ず sort_values で再ソート。
S13: 結果が再現できない
症状 : 同じデータで結果が異なる。 原因 : ランダム要素、 浮動小数誤差、 dict 順序。 対処 : random_state 固定、 ソート明示。
S14: 可視化で凡例が崩れる
症状 : matplotlib で日本語が豆腐 (□)。 原因 : 日本語フォント未設定。 対処 : plt.rcParams['font.family'] = 'IPAexGothic' 等。
S15: レポートに誤った因果記述
症状 : 「A が増えると B が増える」と書いてしまう。 原因 : 相関と因果の混同。 対処 : 「関連がある」「同時に動く」と書く、 介入実験や DiD 等の証拠を別途。
📑 内部結合 ベストプラクティス 30 箇条
処理前に df.shape, df.dtypes, df.head() を必ず確認
列名は ASCII の snake_case に正規化
キー列は文字列型に統一
欠損値の意味を明記
処理の各ステップで shape を print
計算結果は単位を明記
表とグラフは独立して読めるようキャプション付与
外れ値の有無で結果が変わるか必ず両方提示
random_state 等の乱数シードを固定
結合の cardinality を validate で宣言
結果は人口で正規化してから比較
全国平均は加重平均で計算
沖縄など外れ自治体は明示的に扱う
仮説と結果の分離を文章で明示
p 値だけでなく効果量も報告
サンプル数を必ず併記
可視化は誤解を生まないスケール選択
3 次元プロットは原則避ける (2 次元 + 色)
色覚多様性に配慮 (viridis 推奨)
テーブルは縞模様で行を判別しやすく
コードは関数化して再利用
関数には docstring を付ける
テスト関数を 1 つ書く
Git でバージョン管理
環境を requirements.txt で固定
レポートにはデータ取得日を明記
引用は具体的な URL とアクセス日
結果の限界を最後に列挙
他者がコードを再実行できるか検証
レビューを受けてから公開
📈 グラフの読み方 (5 種類)
棒グラフ : 47 都道府県を横棒にすると比較が一目瞭然。 上位 / 下位 5 件を強調色に。
散布図 : 2 変数の関連を確認。 sequential color で人口規模を載せると 3 次元情報。
箱ひげ図 : 47 件の分布全体を 1 枚で把握。 外れ値が点で表示される。
ヒートマップ : 47×n の指標を一気に比較。 z-score で標準化してから配色。
地理マップ (choropleth) : 都道府県地図に色塗り。 ただし面積バイアスに注意 (北海道が目立ちすぎる)。
📜 内部結合 の発展史と現在
内部結合 (Inner Join) はデータサイエンスの基本概念であり、 統計学 / 計算機科学 / データベース理論の交差点に位置します。 以下に発展の節目を時系列で示します。
1900 年代前半 : 統計学が記述統計から推測統計へと拡張、 R.A. Fisher らによる基礎理論整備。
1950–60 年代 : 計算機の登場で大規模データ処理が現実に。 IBM の SQL 前身 SEQUEL 提案。
1970 年 : Codd によるリレーショナルモデル提案。 集合・関係演算が形式化。
1980 年代 : 探索的データ解析 (Tukey)、 統計可視化の確立。
1990 年代 : SQL 標準化と OLAP の台頭。 大規模統計データ流通。
2000 年代 : NumPy / pandas / R が一般化。 都道府県データ分析が個人 PC で可能に。
2010 年代 : Tidy data 原則、 Big data / Hadoop / Spark、 機械学習との統合。
2020 年代 : SSDSE のような教育用統計の整備、 Polars / DuckDB の高速処理、 LLM との連携。
🔍 他言語・他ツールでの実装
ツール 内部結合 相当の表現 特徴
Python pandas pd.merge / .groupby / .apply教育・研究の標準
R dplyr inner_join / left_join / summarisetidyverse 生態系
SQL SELECT ... JOIN ... ON ...宣言的・DB 標準
Polars .join() + 遅延評価高速・並列
DuckDB SQL on parquet / csv 単独実行可能
Excel VLOOKUP / XLOOKUP / Power Query 業務現場
Tableau データ関係 (Relationship) GUI 操作
Spark df.join(other, on)分散処理
🧱 知識マップ: 周辺概念 30
内部結合 を学ぶときに併せて押さえておくとよい概念を 30 個並べました。 リンク先がない場合は今後拡充される予定です。
🎓 内部結合 学習ロードマップ (4 週間プラン)
第 1 週 : 基本概念の理解。 SSDSE-B-2026 を読み込み、 head/info/describe で全体像を掴む。 用語の定義を自分の言葉で説明できるまで反芻。
第 2 週 : 実装演習。 各種 API を 47 都道府県データで 30 回以上手を動かす。 結果の shape と内容を毎回 print して確認。
第 3 週 : 失敗パターンの体験。 意図的に型を壊す / 欠損を入れる / 重複を作るなどして、 エラーを観察。 「期待通りに失敗する」を経験する。
第 4 週 : 応用と関連分野。 結果を別表と結合 / 集計 / 可視化し、 1 ページのレポートにまとめる。 同期の人にレビューしてもらう。
🏁 まとめ: 内部結合 の本質
内部結合 (Inner Join) は、 単なる「テクニック」ではなく、 データの構造を理解し、 集合論的に扱うための思考道具です。 SSDSE-B-2026 のような小さなデータ (47 行) で操作の意味を完全に理解できれば、 数百万行のビジネスデータでも同じ原則で扱えます。 ポイントは「事前検証 → 実行 → 事後検証」のループを必ず回すこと。 失敗は知識ではなく検証不足から生まれます。
🎯 概念は単純: 構造の理解と集合の演算
🔬 実装は pandas / SQL で確立済
⚠️ 失敗の 9 割は型不一致と検証不足
📊 必ず実値で確かめる (SSDSE-B-2026 で 47 行を回す)
🧪 ETL の現場では assert + ログで保護
🔢 集合論で見る Inner Join — 関係代数の自然結合
Inner Join は「キー」で結ばれた 2 つの表の関係代数的な共通部分演算 です。 集合論的には「両方の表に存在するキーの直積部分集合」と定義できます。
📐 関係代数による定義
$$R \bowtie_\theta S = \{ r \cup s \mid r \in R, s \in S, \theta(r, s) \}$$
$$R \bowtie S = \pi_{\text{attrs}(R) \cup \text{attrs}(S)}(\sigma_{R.k = S.k}(R \times S))$$
🔬 数式を言葉で読み解く
$R, S$ : 2 つのリレーション (テーブル)
$\bowtie_\theta$ : θ-結合 (条件 θ を満たすペアを抽出)
$\bowtie$ : 自然結合 (キー列で自動マッチ)
$\pi$ : 射影 (列選択)、 $\sigma$ : 選択 (行フィルタ)、 $\times$ : 直積
分解 : 自然結合 = 直積 → 選択 → 射影 という 3 段階に分解できる
計算量 : 素朴に直積を取ると $O(|R| \cdot |S|)$、 ハッシュ結合なら $O(|R| + |S|)$
🧮 SSDSE 47 県と「宿泊者数カテゴリ表」の Inner Join
このコードでやること : SSDSE-B-2026 47 県の人口データ (A1101) と、 別途用意した「県コード → 延べ宿泊者数 (G7101) カテゴリ (大/中/小)」分類表を inner join し、 結果の行数と列構成を確認する。
📥 入力例 (SSDSE 抜粋 + カテゴリ表):
[A] SSDSE-B-2026 (47 行)
Code Prefecture A1101 G7101
R01000 北海道 5092000 32783470
R13000 東京都 14086000 80273650
...
[B] 宿泊者数カテゴリ表 (45 行、 沖縄・東京のレコードなし)
Code カテゴリ
R01000 大
R02000 小
...
📋 コピー 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 # SSDSE 47 県と宿泊者数カテゴリ表の Inner Join
import pandas as pd
df = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , encoding = 'cp932' , skiprows = [ 1 ])
latest = df . sort_values ( 'SSDSE-B-2026' ) . groupby ( 'Code' ) . tail ( 1 ) . sort_values ( 'Code' )
print ( f 'A (SSDSE): { len ( latest ) } 行' )
# 別途カテゴリ表を作成 (沖縄 R47000、東京 R13000 を意図的に除外 → 45 行)
def categorize ( nights ):
return '大' if nights > 10000000 else ( '中' if nights > 5000000 else '小' )
cat_df = latest [ ~ latest [ 'Code' ] . isin ([ 'R47000' , 'R13000' ])][[ 'Code' , 'G7101' ]] . copy ()
cat_df [ 'カテゴリ' ] = cat_df [ 'G7101' ] . apply ( categorize )
cat_df = cat_df . drop ( columns = 'G7101' )
print ( f 'B (カテゴリ): { len ( cat_df ) } 行' )
# Inner Join
joined = pd . merge ( latest , cat_df , on = 'Code' , how = 'inner' )
print ( f 'A ⋈ B: { len ( joined ) } 行 (沖縄・東京は inner で消える)' )
print ( joined [[ 'Code' , 'Prefecture' , 'A1101' , 'カテゴリ' ]] . head ())
📤 実行例:
A (SSDSE): 47 行
B (カテゴリ): 45 行
A ⋈ B: 45 行 (沖縄・東京は inner で消える)
Code Prefecture A1101 カテゴリ
0 R01000 北海道 5092000 大
1 R02000 青森県 1184000 小
2 R03000 岩手県 1163000 小
3 R04000 宮城県 2264000 中
4 R05000 秋田県 914000 小
💬 Inner Join の結果は 45 行 = min(47, 45) ではなく キー一致のみ 。 沖縄・東京は B に存在しないため除外される。 集合論的には $|A| = 47, |B| = 45$、 共通キー = 45 個 → 結果 = 45 行。 「行数が想定と違う」とき、 まず両表のキー集合の Venn 図を set(A).symmetric_difference(set(B)) で確認するのが鉄則。
🌀 重複キーと多対多結合 — Inner Join が「爆発」する仕組み
「Join 後の行数が予想の何倍にもなってメモリ不足」は実務での最頻発の事故。 原因は結合キーの重複 です。 Inner Join は「キー一致行のデカルト積」を取るため、 重複キーが両側に N 個・M 個あれば結果は N×M 行に膨張します。
📐 結合カーディナリティの計算
$$|R \bowtie S| = \sum_{k \in K_R \cap K_S} n_R(k) \cdot n_S(k)$$
ここで $K_R, K_S$ は表 $R, S$ のキー集合、 $n_R(k)$ は表 $R$ におけるキー $k$ の出現回数。
🔬 数式を言葉で読み解く
一対一 (1:1) : 両側でキー一意 → 結果行数 = キー一致数。 SSDSE 県コードはこのケース
一対多 (1:N) : 片側だけ一意 → 結果行数 = 多側のキー一致数。 安全
多対多 (N:M) : 両側に重複 → 結果行数 = $\sum n_R \cdot n_S$。 危険、 爆発する
事前検証コマンド : R.groupby(key).size().max() と S.groupby(key).size().max() を実行して両方 1 であることを確認
🧮 SSDSE × 年度違い結合の爆発を実演
このコードでやること : SSDSE 47 県 × 年度 のデータと、 47 県 × 別の年度集計表を「地域コード」だけで inner join し、 行数が想定の 年数倍 に膨張する様子を観察する。
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口) G7101(延べ宿泊者数)
北海道 5,092,000 32,783,470
東京都 14,086,000 80,273,650
沖縄県 1,468,000 20,038,190
…(全 47 行)
📋 コピー 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' , encoding = 'cp932' , skiprows = [ 1 ])
print ( f '元データ: { len ( df ) } 行 (47 県 × 年度)' )
A = df [[ 'Code' , 'SSDSE-B-2026' , 'A1101' ]] . copy ()
B = df [[ 'Code' , 'SSDSE-B-2026' , 'G7101' ]] . copy ()
print ( f 'A: { len ( A ) } , B: { len ( B ) } ' )
# 危険: 地域コードだけで結合 (年度を忘れる)
bad = pd . merge ( A , B , on = 'Code' , how = 'inner' )
print ( f '地域コードのみ: { len ( bad ) } 行 (爆発: { len ( bad ) / len ( A ) : .1f } 倍)' )
# 正解: 複合キー (地域コード + 年度)
good = pd . merge ( A , B , on = [ 'Code' , 'SSDSE-B-2026' ], how = 'inner' )
print ( f '複合キー: { len ( good ) } 行 (元と同じ)' )
# validate オプションで事前検証
try :
pd . merge ( A , B , on = 'Code' , validate = 'one_to_one' )
except pd . errors . MergeError as e :
print ( f 'validate エラー検出: { e } ' )
📤 実行例:
元データ: 564 行 (47 県 × 年度)
A: 564, B: 564
地域コードのみ: 6768 行 (爆発: 12.0 倍)
複合キー: 564 行 (元と同じ)
validate エラー検出: Merge keys are not unique in either left or right dataset; not a one-to-one merge
💬 564 行 × 564 行を「地域コードだけ」で結合すると 6768 行 = 564 × 12 (年数) に爆発。 メモリ消費 12 倍。 複合キー (地域コード + 年度) で結合すれば正しく 564 行に。 validate='one_to_one' または 'one_to_many' オプションを使えば pandas が事前にエラーを返してくれるので 本番コードでは必須 。
🔗 SQL ⇔ pandas Inner Join 対応表 — シンタックス完全対応
同じ Inner Join でも SQL と pandas で書き方が異なります。 両方を行き来できると、 BigQuery / SQL Server / pandas の現場で迷いません。
🔧 シンタックス対応表
操作 SQL pandas
単一キー FROM A INNER JOIN B ON A.k = B.kpd.merge(A, B, on='k', how='inner')
複合キー ON A.k1=B.k1 AND A.k2=B.k2on=['k1', 'k2']
名前違いキー ON A.id = B.user_idleft_on='id', right_on='user_id'
インデックス結合 (該当なし)A.join(B, how='inner') または left_index=True
3 表結合 A INNER JOIN B ON ... INNER JOIN C ON ...A.merge(B, on='k').merge(C, on='k')
不等号結合 ON A.dt BETWEEN B.s AND B.epd.merge_asof() または pyjanitor.conditional_join
事前検証 (該当なし、EXPLAIN で確認)validate='one_to_one'
🧮 SSDSE × sqlite3 で SQL Inner Join を直接実行
このコードでやること : SSDSE-B-2026 を sqlite3 メモリ DB に投入し、 SQL の INNER JOIN を直接書いて pandas の結果と一致することを確認。
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口) G7101(延べ宿泊者数)
北海道 5,092,000 32,783,470
東京都 14,086,000 80,273,650
沖縄県 1,468,000 20,038,190
…(全 47 行)
📋 コピー 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 # SSDSE を sqlite3 に投入して SQL INNER JOIN
import pandas as pd , sqlite3
df = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , encoding = 'cp932' , skiprows = [ 1 ])
latest = df . sort_values ( 'SSDSE-B-2026' ) . groupby ( 'Code' ) . tail ( 1 )
con = sqlite3 . connect ( ':memory:' )
latest [[ 'Code' , 'Prefecture' , 'A1101' ]] . to_sql ( 'population' , con , index = False )
latest [[ 'Code' , 'G7101' ]] . to_sql ( 'nights' , con , index = False )
sql = """
SELECT p.Code, p.Prefecture, p.A1101, n.G7101,
ROUND(n.G7101 * 1000.0 / p.A1101, 1) AS nights_per_1k
FROM population p
INNER JOIN nights n ON p.Code = n.Code
ORDER BY nights_per_1k DESC
LIMIT 5;
"""
print ( pd . read_sql ( sql , con ))
con . close ()
📤 実行例:
Code Prefecture A1101 G7101 nights_per_1k
0 R47000 沖縄県 1468000 20038190 13650.0
1 R26000 京都府 2535000 26464250 10439.5
2 R19000 山梨県 796000 6010700 7551.1
3 R17000 石川県 1109000 7970020 7186.7
4 R20000 長野県 2004000 13414630 6693.9
💬 SQL INNER JOIN で 47 県の「人口千人あたり延べ宿泊者数 (G7101)」トップ 5 を抽出。 沖縄・京都が観光需要で突出。 pandas でも pd.merge(pop_df, nights_df, on='Code').assign(r=lambda d: d['G7101']*1000/d['A1101']).nlargest(5, 'r') と同等の処理が可能。 大規模データ (1 億行超) なら SQL の方が高速だが、 小規模 (~100 万行) なら pandas が便利。
🛠️ ETL 現場での Inner Join 5 つの落とし穴
1. キー型の不一致 (str vs int)。 「地域コード」が片方では "R01000" (文字列)、 もう片方では 1100 (整数) の場合、 inner join は無音で 0 行 を返します (エラーではなくマッチなし)。 必ず df.dtypes で型を確認し、 必要なら df['key'].astype(str) で揃える。 「結合結果が 0 行」のときはまず型をチェック。
2. 文字列の前後空白・大文字小文字。 "R01000 " (末尾空白) と "R01000" は inner join では別物。 ETL 取込時に str.strip() と str.lower() または str.upper() で正規化するのが鉄則。 Excel から取り込んだデータでは特に頻発。
3. NaN キーは絶対にマッチしない (SQL の NULL と同じ)。 pandas は SQL 同様 NaN == NaN を False と扱うため、 両方の表で NaN になっているキーは結合されません。 「NaN 同士もマッチさせたい」場合は fillna('__MISSING__') でセンチネル値に置換してから結合。 ただし業務的な妥当性は別途検討。
4. インデックスのリセット忘れ。 groupby().tail(1) や filter の結果は元のインデックスを保持するため、 続けて merge → 別の groupby を行うと意図しない並び順や重複行が発生。 「次に merge する前に必ず reset_index(drop=True)」を癖にする。
5. メモリ爆発の事前検出を怠る。 100 万行 × 100 万行の inner join は理論的に最大 10^12 行 = 1 TB のメモリ消費。 まず R['k'].nunique() と R.groupby('k').size().describe() でキーの分布を確認し、 ピーク重複が許容範囲か検証。 BigQuery では EXPLAIN、 pandas では validate を使う。
📚 安全な Inner Join 実装テンプレート (再利用可)
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口) G7101(延べ宿泊者数)
北海道 5,092,000 32,783,470
東京都 14,086,000 80,273,650
沖縄県 1,468,000 20,038,190
…(全 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 # 実務 ETL で推奨される Inner Join テンプレート
import pandas as pd
def safe_inner_join ( left , right , on , expected_card = 'one_to_one' ):
"""
Inner Join の安全な実装。
- 型の検証
- キー重複の検証
- 結合前後の行数ログ
"""
# 1. 型確認
for k in ([ on ] if isinstance ( on , str ) else on ):
assert left [ k ] . dtype == right [ k ] . dtype , f '型不一致 on= { k } '
# 2. NaN キー確認
for k in ([ on ] if isinstance ( on , str ) else on ):
l_nan = left [ k ] . isna () . sum ()
r_nan = right [ k ] . isna () . sum ()
if l_nan + r_nan > 0 :
print ( f '警告: NaN キー存在 left= { l_nan } , right= { r_nan } ' )
# 3. カーディナリティ検証 + 結合
result = pd . merge ( left , right , on = on , how = 'inner' , validate = expected_card )
print ( f 'A= { len ( left ) } , B= { len ( right ) } -> A⋈B= { len ( result ) } ' )
return result
# 使用例
df = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , encoding = 'cp932' , skiprows = [ 1 ])
latest = df . sort_values ( 'SSDSE-B-2026' ) . groupby ( 'Code' ) . tail ( 1 ) . reset_index ( drop = True )
pop_df = latest [[ 'Code' , 'A1101' ]]
area_df = latest [[ 'Code' , 'G7101' ]]
out = safe_inner_join ( pop_df , area_df , on = 'Code' )
📤 実行例:
A=47, B=47 -> A⋈B=47
💬 上記テンプレートで「型・NaN・カーディナリティ・行数」の 4 項目を 1 関数で検証できる。 本番 ETL では logging モジュールに置き換え、 警告は Slack / Sentry に転送する設計が標準。
⚡ Inner Join のパフォーマンス — アルゴリズムの選択
SQL エンジンや pandas が内部で使う Inner Join アルゴリズムは 3 種類。 データサイズと分布で自動選択されるが、 何が動いているかを知ると最適化できます。
🔧 主要アルゴリズム
アルゴリズム 計算量 適用場面
Nested Loop $O(|R| \cdot |S|)$ 小規模、 範囲条件
Hash Join $O(|R| + |S|)$ 中規模、 等価結合、 メモリ十分
Sort-Merge Join $O(|R|\log|R| + |S|\log|S|)$ 大規模、 既にソート済、 ディスク IO
🔬 数式を言葉で読み解く
Nested Loop : 全行ペアを直接比較。 $|R|=1000, |S|=1000$ なら 100 万比較
Hash Join : 小さい表でハッシュテーブル構築 → 大きい表で lookup。 $|R|+|S|$ の線形時間
Sort-Merge : 両表をキーでソート → ポインタ進めながらマージ。 ソート済なら最速
pandas の merge : デフォルトは hash 系 (内部で Algorithms.merge)。 sort=True で sort-merge に切替
🧮 SSDSE データのベンチマーク
このコードでやること : SSDSE 47 県データを 1 万倍に複製し、 pandas merge の処理時間を計測する。
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口)
北海道 5,092,000
東京都 14,086,000
沖縄県 1,468,000
…(全 47 行)
📋 コピー 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 # SSDSE データを膨らませてベンチマーク
import pandas as pd , time
df = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , encoding = 'cp932' , skiprows = [ 1 ])
latest = df . sort_values ( 'SSDSE-B-2026' ) . groupby ( 'Code' ) . tail ( 1 )
for scale in [ 1 , 100 , 10000 ]:
A = pd . concat ([ latest [[ 'Code' , 'A1101' ]]] * scale ) . reset_index ( drop = True )
A [ 'Code' ] = A [ 'Code' ] + '_' + A . index . astype ( str )
B = A [[ 'Code' ]] . copy ()
B [ '価値' ] = range ( len ( B ))
t0 = time . time ()
out = pd . merge ( A , B , on = 'Code' , how = 'inner' )
elapsed = time . time () - t0
print ( f 'N= { len ( A ) : 7d } : { elapsed * 1000 : 6.1f } ms ( { len ( A ) / elapsed / 1000 : 7.0f } K rows/sec)' )
📤 実行例 (Mac M2、 pandas 2.x):
N= 47: 0.7 ms ( 67 K rows/sec)
N= 4700: 2.1 ms ( 2238 K rows/sec)
N= 470000: 142.5 ms ( 3298 K rows/sec)
💬 N=47 では関数呼び出しオーバーヘッドが支配的。 N が 1 万を超えると pandas のハッシュ結合実装が効いて 300 万行/秒以上の処理速度。 BigQuery のような分散 SQL なら数億行でもサブ秒で完了するが、 pandas でも 100 万行クラスなら十分実用的。
🔄 Inner Join vs Left/Right/Outer/Cross — Venn 図で完全比較
「inner / left / right / outer / cross」の使い分けは現場の最頻出問題。 Venn 図と SSDSE 実データで完全に整理します。
📐 5 種類の Join 型の集合論的定義
Join 型 集合論 結果行数 SSDSE 例 (A=47, B=45)
Inner $A \cap B$ マッチ数 45 行
Left Outer $A$ $|A|$ 47 行 (B 側 NaN 含む)
Right Outer $B$ $|B|$ 45 行 (A 側 NaN 含む)
Full Outer $A \cup B$ $|A \cup B|$ 47 行
Cross $A \times B$ $|A| \cdot |B|$ 2115 行
🔬 数式を言葉で読み解く
Inner : 両側にキーがあるレコードのみ。 NaN を作らない
Left Outer : 左の表を残す。 右にない行は NaN 埋め。 「左の master table を保持」したいときに使う
Right Outer : pandas では how='right'。 SQL の RIGHT JOIN。 実務では left に書き換える方が読みやすい
Full Outer : 両側の和集合。 不一致をすべて NaN で可視化。 整合性確認に最適
Cross : 直積。 全組み合わせを生成。 「全県 × 全月」のスケジュール表生成などに使う
🧮 SSDSE で 5 種類の Join を全て実行
このコードでやること : SSDSE 47 県データから A (47 行)・B (沖縄/東京を除いた 45 行) を作り、 5 種類の Join 結果の行数を比較する。
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口) G7101(延べ宿泊者数)
北海道 5,092,000 32,783,470
東京都 14,086,000 80,273,650
沖縄県 1,468,000 20,038,190
…(全 47 行)
📋 コピー 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 # SSDSE で 5 種類の Join を全部実行して行数を比較
import pandas as pd
df = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , encoding = 'cp932' , skiprows = [ 1 ])
latest = df . sort_values ( 'SSDSE-B-2026' ) . groupby ( 'Code' ) . tail ( 1 ) . reset_index ( drop = True )
A = latest [[ 'Code' , 'Prefecture' , 'A1101' ]] # 47 行
B = latest [ ~ latest [ 'Code' ] . isin ([ 'R47000' , 'R13000' ])][[ 'Code' , 'G7101' ]] # 45 行
print ( f 'A: { len ( A ) } 行, B: { len ( B ) } 行' )
for how in [ 'inner' , 'left' , 'right' , 'outer' ]:
out = pd . merge ( A , B , on = 'Code' , how = how )
n_nan = out [ 'G7101' ] . isna () . sum ()
print ( f ' { how : 6s } : { len ( out ) : 3d } 行 (G7101 NaN= { n_nan } )' )
# Cross Join
cross = A . merge ( B , how = 'cross' )
print ( f 'cross : { len ( cross ) : 3d } 行 = { len ( A ) } × { len ( B ) } ' )
📤 実行例:
A: 47 行, B: 45 行
inner : 45 行 (G7101 NaN=0)
left : 47 行 (G7101 NaN=2)
right : 45 行 (G7101 NaN=0)
outer : 47 行 (G7101 NaN=2)
cross : 2115 行 = 47 × 45
💬 NaN を許すかどうかが選択基準。 「データ欠損も把握したい (沖縄・東京の宿泊者数が無いことを知りたい)」なら left/outer 、 「完全に揃ったデータだけ欲しい」なら inner 。 cross は 2115 行 = 47×45 に爆発。 sklearn の交差検証グリッドのように全組み合わせを作る 用途で重宝。
📚 使い分けフローチャート
「完全な行のみ欲しい?」 → YES なら Inner
「左表を必ず残したい?」 → YES なら Left Outer
「両方の不一致を可視化したい?」 → YES なら Full Outer (整合性確認)
「全組み合わせを作りたい?」 → YES なら Cross (グリッド生成)
「行数が予想と違う」 → まず両表のキー集合の Venn 図を set(A).symmetric_difference(set(B)) で確認
🎓 数式を言葉で読み解く — Inner Join の本質を 3 視点でまとめる
同じ Inner Join を、 集合論・関係代数・コードの 3 視点で同時に 解釈できると、 SQL も pandas も Spark も同じ概念として扱えます。
📐 視点 1: 集合論
$$A \bowtie B = \{(a, b) \mid a \in A, b \in B, \text{key}(a) = \text{key}(b)\}$$
「2 つの集合の要素について、 キーが一致するペアを全列挙する」。 中学校で習う「共通部分 $A \cap B$」を、 タプル (組) に拡張した概念。
📐 視点 2: 関係代数
$$A \bowtie B = \pi_{\text{schema}(A) \cup \text{schema}(B)} \big( \sigma_{A.k = B.k} (A \times B) \big)$$
「直積 → キー等価で行選択 → スキーマ和で列射影」の 3 段階。 SQL の JOIN ... ON は内部でこの 3 段階を実行。 オプティマイザがハッシュ結合や sort-merge に置き換えるだけ。
📐 視点 3: pandas / SQL のコード
pandas : pd.merge(A, B, on='key', how='inner')
SQL : SELECT * FROM A INNER JOIN B ON A.key = B.key
Spark : A.join(B, on='key', how='inner')
R (dplyr) : inner_join(A, B, by='key')
🔬 3 視点を統合した実例
このコードでやること : SSDSE で「集合論的に何件マッチしているか」と「pandas merge の結果」が完全一致することを実装で確認する。
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口) G7101(延べ宿泊者数)
北海道 5,092,000 32,783,470
東京都 14,086,000 80,273,650
沖縄県 1,468,000 20,038,190
…(全 47 行)
📋 コピー 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 # 集合論的予測と pandas inner join の整合性確認
import pandas as pd
df = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , encoding = 'cp932' , skiprows = [ 1 ])
latest = df . sort_values ( 'SSDSE-B-2026' ) . groupby ( 'Code' ) . tail ( 1 )
A = latest [[ 'Code' , 'A1101' ]]
B = latest [ latest [ 'A1101' ] > 2000000 ][[ 'Code' , 'G7101' ]] # 人口 200 万超の県のみ
# 集合論的予測
keys_A = set ( A [ 'Code' ])
keys_B = set ( B [ 'Code' ])
predicted = len ( keys_A & keys_B )
# pandas 実行
actual = len ( pd . merge ( A , B , on = 'Code' , how = 'inner' ))
print ( f '|A| = { len ( keys_A ) } , |B| = { len ( keys_B ) } ' )
print ( f '集合論予測 |A ∩ B| = { predicted } ' )
print ( f 'pandas 実測値 = { actual } ' )
print ( f '一致: { predicted == actual } ' )
📤 実行例:
|A| = 47, |B| = 16
集合論予測 |A ∩ B| = 16
pandas 実測値 = 16
一致: True
💬 集合論的に予測した「16 件マッチ」と pandas merge の結果が完全に一致。 これが Inner Join の本質。 「数式 (集合論) ↔ 実装 (pandas)」を常に対応付けて考える癖をつけると、 100 万行のデータでも自信を持って Join できる。
🔀 Inner Join vs 他の JOIN — 結果件数の比較
同じ 2 表でも JOIN 種別で結果件数が大きく変わる。 SSDSE-B-2026 都道府県表 (47 件) と仮想的な「地方別集計表」(地方コード 8 件、 ただし沖縄+1 件で 9 件、 47 件中 46 件にマッチ) を例に比較する。
JOIN 種別 pandas 書き方 結果件数
Inner Join how='inner'46 (両方に存在する key のみ)
Left Join how='left'47 (左表全件、 右表は NaN 含む)
Right Join how='right'9 (右表全件)
Outer Join how='outer'48 (両表の和集合、 NaN 多発)
💡 「データ件数を維持したい」場合は Left Join、 「整合した行のみで分析したい」場合は Inner Join。 用途で使い分けるのが鉄則。
🌐 SQL と Pandas の対応関係 — Inner Join 実装の二面性
Inner Join は SQL と Pandas の両方に存在するが、 細部の挙動が微妙に異なる 。 この差を把握しないと、 SQL で検証したクエリを Pandas に移植したときに「件数が合わない」「カラム名がぶつかる」「順序がズレる」といったトラブルが起きる。 ここでは両者を行単位で対応づけ、 移植時のチェックポイントを示す。
機能
SQL の書き方
Pandas の書き方
差異・注意点
基本の Inner Join
SELECT * FROM a INNER JOIN b ON a.k=b.k
pd.merge(a, b, on='k', how='inner')
Pandas は同名列に suffix が自動付与
複合キー
ON a.k1=b.k1 AND a.k2=b.k2
on=['k1','k2']
順序は影響しない
異なるキー名
ON a.kA=b.kB
left_on='kA', right_on='kB'
両方の列が結果に残る
NULL の挙動
NULL=NULL は False
NaN 同士もマッチしない (デフォルト)
仕様は一致するが NaN 比較の罠あり
大文字小文字
DB 設定依存 (COLLATE)
完全一致 (大文字小文字を区別)
Pandas は str.lower() で揃える
出力順
未保証
概ね左表の順序
明示的に sort_values 推奨
特に 「異なるキー名で結合した時、 SQL は結合に使った列が 1 つ残るが、 Pandas は両方残る」 点は移植時の盲点になりやすい。 Pandas でも drop(columns='kB') を後段で挟むか、 結合前にリネームしておくと SQL と挙動を揃えられる。
🐍 SQL → Pandas 移植の典型パターン
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口) L3221(消費支出(二人以上の世帯))
北海道 5,092,000 296,888
東京都 14,086,000 341,320
沖縄県 1,468,000 251,222
…(全 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 import pandas as pd
import sqlite3
df = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , skiprows = [ 1 ], encoding = 'cp932' ) ; df = df [ df [ 'SSDSE-B-2026' ] == 2023 ]
# SQL で Inner Join
conn = sqlite3 . connect ( ':memory:' )
df [[ 'Code' , 'Prefecture' , 'A1101' ]] . to_sql ( 'pop' , conn , index = False )
df [[ 'Code' , 'L3221' ]] . to_sql ( 'spend' , conn , index = False )
sql_result = pd . read_sql_query ( """
SELECT p.Prefecture, p.A1101, g.L3221
FROM pop p INNER JOIN spend g ON p.Code = g.Code
""" , conn )
# Pandas で同じ Inner Join
pd_result = pd . merge (
df [[ 'Code' , 'Prefecture' , 'A1101' ]],
df [[ 'Code' , 'L3221' ]],
on = 'Code' , how = 'inner'
)[[ 'Prefecture' , 'A1101' , 'L3221' ]]
print ( 'SQL 行数:' , len ( sql_result ))
print ( 'Pd 行数:' , len ( pd_result ))
print ( '一致:' , sql_result . shape == pd_result . shape )
📤 実行例(実測)
SQL 行数: 47
Pd 行数: 47
一致: True
この「2 つの実装で同じ答えが出るか」を検証する習慣は、 移植バグの早期発見・回帰テストの基盤 として極めて有効である。 特に分析が長期化するプロジェクトでは、 SQL でプロトタイプ → Pandas で本番という流れが多く、 その分かれ目で見落としが起きやすい。
🛠 Inner Join 失敗時のデバッグフローチャート
「Inner Join したら 0 行になった」「件数が想定より少ない」「逆に多すぎる」 — これらは現場で頻発する。 そして対処法はパターン化できる。 ここでは Inner Join トラブルを系統的に切り分ける 6 ステップ を、 SSDSE-B-2026 を題材に紹介する。 順番通りに辿れば、 ほぼすべての結合トラブルが特定できる。
ステップ
確認内容
使うコード
怪しい兆候
1. 件数の確認
結合前後の行数
len(a), len(b), len(merged)
merged=0 または極端に少ない
2. キー型
両表のキーの dtype
a['k'].dtype, b['k'].dtype
片方 int、 片方 object
3. ユニーク値の重複
両表のキーの共通集合
set(a['k']) & set(b['k'])
共通集合が空
4. 空白・大小
サンプル 5 行を repr で見る
a['k'].head().apply(repr)
"'北海道 '" のように空白
5. NULL 率
キー列の NaN 件数
a['k'].isna().sum()
NaN が想定外に多い
6. カーディナリティ
キーの重複度
a['k'].value_counts().head()
特定キーが極端に多い (重複爆発予兆)
🐍 デバッグ用ユーティリティ関数
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口) L3221(消費支出(二人以上の世帯))
北海道 5,092,000 296,888
東京都 14,086,000 341,320
沖縄県 1,468,000 251,222
…(全 47 行)
📋 コピー 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 import pandas as pd
def diagnose_join ( a , b , key ):
print ( f '--- Inner Join 診断: key= { key } ---' )
print ( f '1. 行数: a= { len ( a ) } , b= { len ( b ) } ' )
print ( f '2. dtype: a= { a [ key ] . dtype } , b= { b [ key ] . dtype } ' )
common = set ( a [ key ]) & set ( b [ key ])
print ( f '3. 共通キー数: { len ( common ) } ' )
sample_a = a [ key ] . dropna () . head ( 3 ) . apply ( repr ) . tolist ()
sample_b = b [ key ] . dropna () . head ( 3 ) . apply ( repr ) . tolist ()
print ( f '4. サンプル a: { sample_a } ' )
print ( f ' サンプル b: { sample_b } ' )
print ( f '5. NaN: a= { a [ key ] . isna () . sum () } , b= { b [ key ] . isna () . sum () } ' )
print ( f '6. 最大重複 a: { a [ key ] . value_counts () . iloc [ 0 ] if len ( a ) else 0 } ' )
df = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , skiprows = [ 1 ], encoding = 'cp932' ) ; df = df [ df [ 'SSDSE-B-2026' ] == 2023 ]
diagnose_join ( df [[ 'Code' , 'A1101' ]], df [[ 'Code' , 'L3221' ]], 'Code' )
📤 実行例(実測)
--- Inner Join 診断: key=Code ---
1. 行数: a=47, b=47
2. dtype: a=object, b=object
3. 共通キー数: 47
4. サンプル a: ["'R01000'", "'R02000'", "'R03000'"]
サンプル b: ["'R01000'", "'R02000'", "'R03000'"]
5. NaN: a=0, b=0
6. 最大重複 a: 1
このような診断関数を 1 つ持っておくと、 「とりあえず動かして件数が違ったら呼び出す」運用が定着し、 結合トラブルの解決時間が劇的に短縮する。 とくに新人やデータ受領後の最初のチェックには有効である。
📚 関連グループ教材
この用語の全体像を学ぶには、 まず横断的な教材で文脈を掴むのが効率的です:
🔗 同カテゴリの他用語
📚 参考文献
Codd, E.F. (1970). "A Relational Model of Data for Large Shared Data Banks." Communications of the ACM , 13(6), 377–387. — 関係代数とジョインの原典。
Wickham, H. (2014). "Tidy Data." Journal of Statistical Software , 59(10). — 結合しやすいデータ形状の議論。
McKinney, W. (2022). Python for Data Analysis (3rd ed.). O'Reilly. — pandas merge / join の決定版解説。
Garcia-Molina, H., Ullman, J.D., Widom, J. (2008). Database Systems: The Complete Book (2nd ed.). Pearson. — SQL の inner join 計算量。
SSDSE-B-2026 (独立行政法人 統計センター, 2026). 地域・都道府県データ . https://www.nstac.go.jp/use/literacy/ssdse/
Date, C.J. (2019). SQL and Relational Theory (3rd ed.). O'Reilly.
📘 拡張ハンドブック: 結合の手順書
準備 : 左右の shape, dtypes, head() を必ず確認。 キー候補の nunique() を取り、 重複を発見しておく。
型統一 : キーの型を揃える (astype(str) 推奨)。 大文字小文字、 前後空白を正規化。
欠損処理 : キー列の NaN を許容するか決定。 通常は dropna(subset=['key']) でキー欠損行を捨てる。
indicator で診断 : 最初の試行は how='outer', indicator=True で実行し、 both / left_only / right_only の内訳を必ず print。
validate で契約宣言 : 'one_to_one', 'one_to_many', 'many_to_one' のうち期待する形を渡す。 食い違えば即例外。
本番結合 : 設計通りの how (多くは 'inner' または 'left') で結合。
事後確認 : 結合後の行数・列数・NaN 件数をログ。 削減率 90% 超や倍増は要警戒。
サフィックス管理 : 同名列が両側にあると _x, _y が付く。 suffixes=('_pop','_house') で意味のある名前に。
多キー結合 : on=['Code', 'Year'] のようにキー複数指定。 SSDSE 系で年度を含めるとき必須。
左右で列名が異なるとき : left_on='Code', right_on='都道府県コード'。 結合後は片方を drop。
🎯 50 連発: 内部結合の頻出パターン
単純結合: pd.merge(a, b, on='key')
明示 how: how='inner'
キー名違い: left_on, right_on
複数キー: on=['Code','Year']
サフィックス: suffixes=('_a','_b')
1:1 検証: validate='one_to_one'
1:N 検証: 'one_to_many'
N:1 検証: 'many_to_one'
インジケータ: indicator=True
大文字小文字統一: str.upper()
前後空白除去: str.strip()
型統一: astype(str)
NaN キー除去: dropna(subset=['key'])
重複キー除去: drop_duplicates('key')
インデックス結合: left_index=True
結合後リネーム: .rename(columns=...)
不要列削除: .drop(columns=...)
結合後並び替え: .sort_values('Code')
結合数確認: print(len(merged))
削減率: 1 - len(after)/len(before)
外部キー → 主キー: many_to_one
主キー → 外部キー: one_to_many
キーの正規化: str.zfill(6)
3 表結合: (a.merge(b).merge(c))
4 表結合: chain merges
キー一致率: (both/total)*100
欠落都道府県の一覧: left_only filter
左にしかない: indicator=='left_only'
右にしかない: indicator=='right_only'
両側にある: indicator=='both'
SQL での同等: INNER JOIN ON
USING 句: USING (Code)
自己結合: 同一表を別名でジョイン
キー結合不要時: how='cross'
結合前 sample: .sample(5) で目視
結合後 head: .head(3) で確認
結合後 info: .info() で型確認
結合後 describe: .describe()
NaN 件数: .isna().sum()
巨大表は chunksize: 分割 merge
カテゴリ型: category 化でメモリ削減
キー文字列の正規表現: str.contains
キーのスペル統一辞書: map(dict)
結合キーの存在確認: 'key' in df.columns
キーの空集合検知: set(a)&set(b) == set()
キー一致率を可視化: matplotlib bar
差集合の保存: only_a.to_csv(...)
結合履歴ログ: logging.info(len)
テストで shape 固定: assert df.shape == (47, 4)
結合の単体テスト: pytest + parametrize
❓ FAQ 20 問
Q1. inner と left の違いは? inner は両側にあるキーのみ、 left は左の全行を残す。
Q2. inner で行が減ったらバグ? 仕様通りだが、 想定外なら不整合の兆候。 indicator で確認。
Q3. キー列名が違ったら? left_on='Code', right_on='都道府県コード' を使う。
Q4. 複数キーで結合できる? できる。 on=['Code', 'Year'] でリスト指定。
Q5. 行数が想定外に増えた 多対多になっている。 validate='one_to_one' で検出を。
Q6. NaN は結合される? pandas は NaN 同士をマッチさせない。 デフォルトで除外。
Q7. SQL と pandas の inner join は同じ? 意味論はほぼ同じ。 NULL 処理など細部のみ差。
Q8. 文字列キーと数値キーは結合できる? 原則できない。 型を揃える。
Q9. インデックス結合と列結合どっち? 列結合 (on=) のほうが明示的でデバッグ容易。
Q10. 大規模データで遅い キー列を category 型に変換、 または DuckDB / Polars を検討。
Q11. 結合後の列順は? 左の列 → 共通キー以外の右の列 → 共通列順を踏襲。
Q12. メモリ不足 結合前に不要列 drop、 chunksize で分割、 集計してから結合。
Q13. 並列処理は? Dask / Polars で分散結合可能。
Q14. inner join の計算量は? ハッシュ結合で O(n + m)、 ソート結合で O(n log n + m log m)。
Q15. 自分自身と結合 (self join) は? 同じ表を df.merge(df, ...) でできる。 サフィックス必須。
Q16. 結合キーがなくなった 事前に set(a.key) & set(b.key) でサイズ確認。
Q17. 結合前後のテスト assert merged.shape[0] == expected。
Q18. 日付キーで結合 pd.to_datetime で揃える。 文字列のままだとミスマッチが残る。
Q19. キーの大文字小文字 事前に str.upper() / str.lower()。
Q20. inner join の代わりに filter で十分? キーが片方のみなら filter で済むが、 列の追加もしたいなら merge。
🔥 結合戦略の選び方フローチャート
どの結合タイプを選ぶか、 用途別の判断フローを整理します。
[Step 1] 結合の目的は?
├─ A: 列を増やしたい (横方向) → Step 2 へ
└─ B: 行を増やしたい (縦方向) → pd.concat 一択
[Step 2] 主データはどっち?
├─ A: 左 (例: 全顧客マスタ) → how='left' を第一候補
├─ B: 右 (例: 全注文履歴) → how='right' (使用稀)
├─ C: 両方とも完全データ → how='inner'
└─ D: 差分検出が目的 → how='outer' + indicator=True
[Step 3] キーの cardinality は?
├─ 1:1 → validate='one_to_one'
├─ 1:N → validate='one_to_many' (左マスタ × 右トランザクション)
├─ N:1 → validate='many_to_one' (左トランザクション × 右マスタ)
└─ N:N → 設計を疑え。 中間テーブルが必要かも
[Step 4] 結合後の検証
├─ assert merged.shape == (期待行数, 期待列数)
├─ assert merged['key'].is_unique (1:1 のとき)
└─ NaN 件数を必ず print
📊 計算量と性能
内部結合の代表的アルゴリズムは 3 つ。 データサイズと統計に応じて選ばれます。
アルゴリズム 計算量 向いている状況 SSDSE 47 行での選択
ネストループ $O(n \cdot m)$ 小さい表 (数十行) 47×47 = 2,209 比較で十分軽量
ハッシュ結合 $O(n + m)$ 中規模・キーがハッシュ可能 pandas merge のデフォルト相当
ソートマージ結合 $O(n \log n + m \log m)$ 大規模・キーが順序づけ可能 事前ソート済なら最速
SSDSE-B-2026 (47 行) では結合コストはほぼ無視できます。 数千万行になると category 型化や DuckDB / Polars の採用を検討します。
🛠 デバッグ用 1 行スニペット集
キー型確認: print(df_pop['Code'].dtype, df_house['Code'].dtype)
キーユニーク確認: print(df_pop['Code'].is_unique, df_house['Code'].is_unique)
共通キー数: len(set(df_pop['Code']) & set(df_house['Code']))
左にしかないキー: set(df_pop['Code']) - set(df_house['Code'])
右にしかないキー: set(df_house['Code']) - set(df_pop['Code'])
結合前後の行数: print(len(df_pop), len(df_house), '→', len(merged))
NaN 件数: print(merged.isna().sum())
削減率: 1 - len(merged) / len(df_pop)
サフィックス警告: any('_x' in c or '_y' in c for c in merged.columns)
結合の検証 assert: assert len(merged) == 47
📋 SSDSE-B-2026 で内部結合の集合演算 10 連
内部結合の出力は集合 $K_R \cap K_S$ を取る操作と等価。 SSDSE-B-2026 を 47 行で固定し、 様々な抽出条件と組合せた結果を表にします。
パターン 左 (df_pop) 右 (df_house) inner 行数
全件 47 47 47
右から沖縄除外 47 46 46
右から関東 1 都 6 県除外 47 40 40
左を関東 1 都 6 県のみ 7 47 7
左と右で別地方 7 (関東) 6 (関西) 0
左を北海道のみ 1 47 1
右を東京のみ 47 1 1
両方とも 47 47 47 47
左を 10 件、 右を 5 件 (重ならない) 10 5 0
左 10 件、 右 47 件 10 47 10
🧪 詳細ケーススタディ (SSDSE-B-2026 ベース)
ケース 1: 人口 × 経済の都道府県カルテ作成
背景: 政策研究で「人口規模 × 経済活力」のクロス表を作る必要がある。 元データは A1101 (人口) と C3301 (着工建築物数) の 2 列。
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口) C3301(着工建築物数)
北海道 5,092,000 15,872
東京都 14,086,000 41,817
沖縄県 1,468,000 5,217
…(全 47 行)
📋 コピー df = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , encoding = 'cp932' , skiprows = [ 1 ] ) ; df = df [ df [ 'SSDSE-B-2026' ] == 2023 ]
pop = df [[ 'Code' , 'Prefecture' , 'A1101' ]]
house = df [[ 'Code' , 'C3301' ]]
karte = pd . merge ( pop , house , on = 'Code' , how = 'inner' , validate = 'one_to_one' )
karte [ '人口あたり着工建築物' ] = karte [ 'C3301' ] / karte [ 'A1101' ] * 1000
print ( karte . sort_values ( '人口あたり着工建築物' , ascending = False ) . head ( 5 ))
📤 実行例(実測)
Code Prefecture A1101 C3301 人口あたり着工建築物
15 R16000 富山県 1007000 7041 6.992056
19 R20000 長野県 2004000 10958 5.468064
9 R10000 群馬県 1902000 10063 5.290747
14 R15000 新潟県 2126000 11109 5.225306
18 R19000 山梨県 796000 4111 5.164573
💬 結合直後に派生列を計算。 1:1 結合の信頼性を validate で担保。
ケース 2: 多年度パネル (concat → merge の組合せ)
背景: 仮想的に「2022 年データ」と「2023 年データ」の縦パネル化、 そこに地方区分マスタを結合。
📋 コピー 1
2
3
4
5
6
7
8
9
10
11
12
13 pop23 = df [[ 'Code' , 'Prefecture' , 'A1101' ]] . assign ( Year = 2023 )
pop22 = pop23 . assign ( Year = 2022 , A1101 = pop23 [ 'A1101' ] * 0.998 ) # 仮の前年値
panel = pd . concat ([ pop22 , pop23 ], ignore_index = True )
region = pd . DataFrame ({ 'Code' : df [ 'Code' ] . tolist (),
'Region' : [ '北海道' , '東北' , '東北' , '東北' , '東北' , '東北' , '東北' ,
'関東' , '関東' , '関東' , '関東' , '関東' , '関東' , '関東' ,
'中部' , '中部' , '中部' , '中部' , '中部' , '中部' , '中部' , '中部' , '中部' ,
'近畿' , '近畿' , '近畿' , '近畿' , '近畿' , '近畿' , '近畿' ,
'中国' , '中国' , '中国' , '中国' , '中国' ,
'四国' , '四国' , '四国' , '四国' ,
'九州' , '九州' , '九州' , '九州' , '九州' , '九州' , '九州' , '九州' ]})
panel2 = pd . merge ( panel , region , on = 'Code' , how = 'inner' )
print ( panel2 . groupby ([ 'Year' , 'Region' ])[ 'A1101' ] . sum () . unstack ())
📤 実行例(実測)
Region 中国 中部 九州 ... 東北 近畿 関東
Year ...
2022 7055860.0 20707502.0 14000942.0 ... 8301364.0 21946020.0 43439946.0
2023 7070000.0 20749000.0 14029000.0 ... 8318000.0 21990000.0 43527000.0
[2 rows x 8 columns]
💬 inner join + groupby で「年×地方」のクロス表が完成。
ケース 3: 3 表チェーン結合 (人口 × 着工建築物 × 病院)
背景: 「人口規模 × 着工建築物数 × 一般病院数」の 3 指標カルテ。
📋 コピー pop = df [[ 'Code' , 'Prefecture' , 'A1101' ]]
house = df [[ 'Code' , 'C3301' ]]
hospital = df [[ 'Code' , 'I510120' ]] # 一般病院数
merged = ( pop
. merge ( house , on = 'Code' , how = 'inner' , validate = 'one_to_one' )
. merge ( hospital , on = 'Code' , how = 'inner' , validate = 'one_to_one' ))
print ( merged . shape )
print ( merged . head ( 3 ))
📤 実行例(実測)
(47, 5)
Code Prefecture A1101 C3301 I510120
0 R01000 北海道 5092000 15872 464
1 R02000 青森県 1184000 5043 72
2 R03000 岩手県 1163000 5731 76
💬 メソッドチェーンで読みやすく。 各段階で validate を付け、 中間結果の shape をチェック。
ケース 4: indicator で欠落自治体を特定
背景: 仮想的に「都市部だけ集めた指標 df_urban」を用意し、 全 47 から欠落しているものを列挙。
📋 コピー urban_codes = [ 'R01000' , 'R04000' , 'R11000' , 'R12000' , 'R13000' , 'R14000' ,
'R22000' , 'R23000' , 'R26000' , 'R27000' , 'R28000' , 'R40000' ]
df_urban = df [ df [ 'Code' ] . isin ( urban_codes )][[ 'Code' , 'A1101' ]]
diag = pd . merge ( df [[ 'Code' , 'Prefecture' ]], df_urban , on = 'Code' , how = 'outer' , indicator = True )
missing = diag [ diag [ '_merge' ] == 'left_only' ]
print ( '都市部に含まれない自治体数:' , len ( missing ))
print ( missing [ 'Prefecture' ] . tolist ()[: 10 ])
📤 実行例(実測)
都市部に含まれない自治体数: 35
['青森県', '岩手県', '秋田県', '山形県', '福島県', '茨城県', '栃木県', '群馬県', '新潟県', '富山県']
💬 outer + indicator で「左にしかない」自治体を即抽出。
ケース 5: 集計表との結合 (groupby + merge)
背景: 各都道府県の人口に対し、 「全国平均との差」を結合して可視化したい。
📋 コピー pop = df [[ 'Code' , 'Prefecture' , 'A1101' ]] . copy ()
mean_table = pd . DataFrame ({ '全国平均' : [ pop [ 'A1101' ] . mean ()] * 47 , 'Code' : pop [ 'Code' ]})
result = pd . merge ( pop , mean_table , on = 'Code' , how = 'inner' )
result [ '差分' ] = result [ 'A1101' ] - result [ '全国平均' ]
print ( result . sort_values ( '差分' , ascending = False ) . head ( 3 ))
📤 実行例(実測)
Code Prefecture A1101 全国平均 差分
12 R13000 東京都 14086000 2.645809e+06 1.144019e+07
13 R14000 神奈川県 9229000 2.645809e+06 6.583191e+06
26 R27000 大阪府 8763000 2.645809e+06 6.117191e+06
💬 全国平均を 47 行に展開して結合する典型パターン。
ケース 6: 結合の単体テスト (pytest 風)
背景: ETL パイプラインで結合の不整合を未然に検知する。
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口) C3301(着工建築物数)
北海道 5,092,000 15,872
東京都 14,086,000 41,817
沖縄県 1,468,000 5,217
…(全 47 行)
📋 コピー def test_inner_join_47 ():
df = pd . read_csv ( 'data/raw/SSDSE-B-2026.csv' , encoding = 'cp932' , skiprows = [ 1 ] ) ; df = df [ df [ 'SSDSE-B-2026' ] == 2023 ]
pop = df [[ 'Code' , 'Prefecture' , 'A1101' ]]
house = df [[ 'Code' , 'C3301' ]]
merged = pd . merge ( pop , house , on = 'Code' , how = 'inner' , validate = 'one_to_one' )
assert merged . shape == ( 47 , 4 ), f 'Expected 47 rows, got { len ( merged ) } '
assert merged [ 'Code' ] . is_unique , 'Codes should be unique'
assert merged . isna () . sum () . sum () == 0 , 'No NaN expected'
print ( 'OK: SSDSE 47 行 inner join 正常' )
test_inner_join_47 ()
📤 実行例(実測)
OK: SSDSE 47 行 inner join 正常
💬 結合の契約 (shape, uniqueness, no NaN) を明文化して assert。 CI に組み込めばリグレッションを防げる。
📚 Inner Join をマスターするための学習ロードマップ
Inner Join は「SQL の入口」であると同時に、 リレーショナル設計・データ整合性・計算量・分析戦略までを横断する概念である。 ここでは学習者が 「動く」→「正しく動かす」→「速く動かす」→「設計できる」 の 4 段階で達成すべき到達目標を整理する。
段階
到達目標
使う技術
SSDSE-B-2026 での到達例
L1: 動く
2 テーブルを 1 キーで結合できる
SQL INNER JOIN / pd.merge
人口 (A1101) と消費支出 (L3221) を県名で結合
L2: 正しく動かす
型・空白・NULL を事前に揃え、 件数を検証できる
assert / sanity check
「47 行を超えていないか」を assert で確認
L3: 速く動かす
インデックス・型最適化・列絞り込みでパフォーマンス向上
index / category / drop
複数年×47 県を 1 秒以内で結合
L4: 設計できる
複合キー・カーディナリティ・正規化を見通した結合戦略
ER 図 / 正規化 / 結合計画
年度×県×部門の 3 テーブル結合を設計
この 4 段階のうち L1〜L2 は数時間で到達できるが、 L3 以降は 「実データを触りながらしか身につかない」 ことが多い。 SSDSE-B-2026 のような実データを使い、 「同じ結合でも書き方を変えると速度がどう変わるか」を計測する経験が最良の教材になる。
📖 推奨学習リソース
これらを横断して触ると、 Inner Join が「単なる結合」ではなく 「データ設計の積み上げの上に成立する操作」 であることが見えてくる。 そこまで到達すれば、 結合トラブルの 9 割は事前に予防できる。
🎯 ケーススタディ集 — SSDSE-B-2026 で Inner Join を使い倒す
Inner Join の理解を深める最良の方法は、 同じデータで 「問いを変えながら何度も結合してみる」 ことである。 ここでは SSDSE-B-2026 を題材に、 実務でよく出会う 6 つの分析シナリオを取り上げ、 それぞれで Inner Join がどう活躍するかを示す。 各ケースは独立しており、 興味のあるものから順に試してほしい。
ケース 1: 人口と消費支出の相関を「データが揃った県」だけで見る
分析の出発点は「人口が多い県ほど消費支出も大きいのか」という素朴な問い。 SSDSE-B-2026 は全 47 県を網羅しているため Inner Join しても全件残るが、 仮に一部の県で消費支出データが欠けていたとすると、 Inner Join はその県を自動で除外する。 これは 「揃ったデータだけで相関を見たい」 という典型的なニーズに合致する。
ケース 2: 消費支出 (L3221) と延べ宿泊者数 (G7101) を結合して地域プロファイリング
消費支出と延べ宿泊者数はどちらも地域の経済活動の側面を表すが、 両方を結合してランキングを作ると、 「消費は大きいが観光需要は小さい」 県、 逆に 「消費は中程度だが観光需要が高い」 県が浮き彫りになる。 Inner Join はこのような多変量プロファイリングの基盤となる。
ケース 3: 人口と高齢化率を結合して「シルバー市場規模」を算出
「高齢者人口 = 人口 × 高齢化率」のような複合指標を作る際、 別ソースの 2 列を Inner Join で揃えるのが定石。 結合後に df['silver'] = df['pop'] * df['rate'] と書けば即座に新指標が得られる。 この単純な掛け算が、 マーケティングの市場規模推計に直結する。
ケース 4: 県別の医療資源を Inner Join で網羅性チェック
医師数・病床数・看護師数など複数のテーブルを Inner Join したとき、 行数が 47 を下回ったら「どこかのテーブルに欠損県がある」というシグナル。 これを使えば 網羅性検査 をワンライナーで実施できる。
ケース 5: 年度×県の縦長テーブル同士の結合
SSDSE-B-2026 は年度×県の縦長フォーマットなので、 別年度シリーズと結合する場合は on=['SSDSE-B-2026','Code'] の複合キーが必須。 ここを単独キーで結合すると N:N の直積 が発生し、 行数が 47×年度数の二乗になってメモリを食い潰す。
ケース 6: 結合前後の差分テーブルで「脱落した県」を可視化
Inner Join 前後で set(before) - set(after) を取れば、 結合で消えた県の一覧が得られる。 これは 「なぜこの県が落ちたのか」を追跡する第一歩 として極めて重要。 単なる結果の集計ではなく、 結合のメタ分析として実務でよく使う。
🐍 ケース 5 の実装例
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口) L3221(消費支出(二人以上の世帯))
北海道 5,092,000 296,888
東京都 14,086,000 341,320
沖縄県 1,468,000 251,222
…(全 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' , skiprows = [ 1 ], encoding = 'cp932' )
# 全年度データ (縦長) をそのまま使い、 年度列 SSDSE-B-2026 を複合キーに含める
n_years = df [ 'SSDSE-B-2026' ] . nunique ()
a = df [[ 'Code' , 'SSDSE-B-2026' , 'A1101' ]]
b = df [[ 'Code' , 'SSDSE-B-2026' , 'L3221' ]]
# 単独キーで結合 (危険)
solo = pd . merge ( a , b , on = 'Code' )
# 複合キーで結合 (安全)
combo = pd . merge ( a , b , on = [ 'Code' , 'SSDSE-B-2026' ])
print ( f '単独キー結合: { len ( solo ) : 4d } 行' )
print ( f '複合キー結合: { len ( combo ) : 4d } 行' )
📤 実行例(実測)
単独キー結合: 6768 行
複合キー結合: 564 行
これらケーススタディに共通するメッセージは、 「Inner Join は問いに合わせて使い分ける道具」 である。 「とりあえず Inner」と思考停止せず、 「自分が何を残したいのか」を毎回意識すれば、 結合は強力な分析エンジンに変わる。 このマインドセットこそが、 データサイエンティストの基礎体力である。
また、 これらのシナリオは BI ツール(Tableau / Power BI / Looker) でも全く同じ考え方で適用できる。 GUI のドラッグ&ドロップで結合する場合でも、 内部では Inner Join が走っており、 件数の検証・キー型の確認・複合キー指定の重要性は SQL/Pandas と変わらない。 GUI を使うほど「内部で何が起きているか」を意識する必要が高まるとも言える。
🔄 Inner Join の連鎖 — 3 表以上を結ぶときの戦略
実務では「3 つ以上のテーブルを順に結合する」場面が頻発する。 たとえば SSDSE-B-2026 を起点に、 人口 (A1101)・消費支出 (L3221)・延べ宿泊者数 (G7101) を 3 段重ねで結合するようなケースである。 このとき結合の 順序・中間結果のサイズ・キー一貫性 という 3 つの観点を意識しないと、 簡単に行数爆発やメモリ不足を招く。 ここでは安全に多段結合するための実践的ガイドラインを示す。
観点
推奨
悪い例
理由
順序
小さい表 → 大きい表の順
大きい表同士から始める
中間結果が小さい方がメモリ効率良い
キー一貫性
全表で同じキー名・型に正規化
表ごとに異なるキー名
left_on/right_on の混在は読みにくい
中間検証
各段階で行数を assert
最後の結果だけ見る
エラー箇所の特定が早い
失敗時のリカバリ
中間結果をキャッシュ
全部再実行
大規模データは再実行コスト高
🐍 3 段 Inner Join の安全パターン
📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行)
都道府県 A1101(総人口) G7101(延べ宿泊者数) L3221(消費支出(二人以上の世帯))
北海道 5,092,000 32,783,470 296,888
東京都 14,086,000 80,273,650 341,320
沖縄県 1,468,000 20,038,190 251,222
…(全 47 行)
📋 コピー 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' )
df = df [ df [ 'SSDSE-B-2026' ] == 2023 ]
pop = df [[ 'Code' , 'Prefecture' , 'A1101' ]]
inc = df [[ 'Code' , 'L3221' ]]
edu = df [[ 'Code' , 'G7101' ]]
# 段階 1: 人口 ∩ 消費支出
m1 = pd . merge ( pop , inc , on = 'Code' , how = 'inner' )
assert len ( m1 ) == 47 , f '段階 1 異常: { len ( m1 ) } 行'
# 段階 2: + 延べ宿泊者数
m2 = pd . merge ( m1 , edu , on = 'Code' , how = 'inner' )
assert len ( m2 ) == 47 , f '段階 2 異常: { len ( m2 ) } 行'
print ( f '最終 { len ( m2 ) } 行 / { len ( m2 . columns ) } 列' )
print ( m2 . head ( 3 ) . to_string ( index = False ))
📤 実行例(実測)
最終 47 行 / 5 列
Code Prefecture A1101 L3221 G7101
R01000 北海道 5092000 296888 32783470
R02000 青森県 1184000 263371 3880720
R03000 岩手県 1163000 298536 4980740
多段結合はパフォーマンスの落とし穴になりやすい。 とくに 「中間結果が爆発する」 パターンに注意したい。 たとえば 47 行同士の結合は問題ないが、 中間結果が N:N 結合により 47×47=2,209 行になり、 次の結合で更に乗算されると一気に数百万行に達することがある。 段階ごとの assertion がこれを未然に防ぐ。
📝 Inner Join に関する誤解 Top 10 を一掃する
Inner Join は「単純なはず」というイメージから、 細部で誤解が生まれやすい。 ここでは現場でよく聞く 10 の誤解と、 その正しい理解を整理する。 「自分はちゃんとわかっている」と思っている人ほど 2-3 個は引っかかる、 意外性のあるラインアップである。
#
誤解
正しい理解
1
Inner Join は左表が基準
基準なし。 両表の共通キー値のみ残る
2
行数は必ず減る
N:N 結合では増えることもある
3
NaN 同士もマッチする
マッチしない (SQL 仕様)
4
結合後の順序は保証される
保証されない (実装依存)
5
複合キーは順番が重要
順番は影響しない
6
同名カラムは自動マージされる
Pandas では suffix で別カラム化
7
大小文字は無視される
区別する (Pandas デフォルト)
8
空白は自動 trim される
されない (明示的に strip 必要)
9
Inner と Left は速度同じ
通常 Inner の方が速い (結果が小さい)
10
Inner Join はインデックス不要
大規模データでは必須レベル
この 10 個のうち、 とくに 3 (NaN)、 6 (suffix)、 9 (速度) は実務で頻繁にハマる三大誤解。 たとえば 6 は、 SQL で USING を使うと同名カラムが 1 つに集約されるが、 Pandas merge では _x, _y サフィックスがついて 2 列残る。 後段で集計するときにこの差を意識しておかないと、 想定外のカラムを参照してしまう。
これら誤解は「最初に教科書で学んだ知識をアップデートしていない」ことに起因することが多い。 Inner Join は基本概念だからこそ、 定期的に 「自分の理解は今でも正しいか」を見直す ことが、 結合バグの予防につながる。 そして実データで何度も検証する経験こそが、 誤解を最も効率的に解消する手段である。
最後にもう 1 点だけ強調したい。 Inner Join は 「データの 結婚 (marriage) ではなく 待ち合わせ (rendezvous)」 という比喩が分かりやすい。 結婚は両者の状態を永続的に統合するが、 待ち合わせは「両者がその場に同時に居る瞬間だけ」が成立する。 Inner Join はまさに後者で、 「両表に同時に存在するキーだけが残り、 そうでなければ脱落する」という、 厳格で潔いルールに支えられている。 この理解があれば、 Inner Join を多用しても結果に驚かされることは減るはずである。 そして、 この厳格さゆえに、 Inner Join はデータ整合性を担保する強力な道具になる。
🖼 視覚的理解 (3 図)
Inner Join を SSDSE-B-2026 都道府県データで可視化する。 3 つの異なる切り口 (散布図・分布・群間比較) で結合結果を多面的に理解する。
図 1: 散布図による 2 変数関係の可視化。 SSDSE-B-2026 と人口統計テーブルを inner join した結果について、 人口と消費支出の関係を直感的に把握する。
図 2: ヒストグラムで 1 次元分布を可視化。 inner join 後のレコード数 (= 両テーブル共通キーの件数) の分布を確認し、 結合損失の中心傾向・ばらつきを読み取る。
図 3: 複数群の箱ひげ図比較。 地方区分など複数カテゴリ間で inner join 後の指標分布がどう異なるかを比較し、 群間差・群内ばらつきを同時に把握する。
🗺 概念マップ
関連概念を視覚的に整理した概念マップ。
inner join
結合キー (Code)
left / outer join
cross / semi join
pd.merge / SQL
validate オプション
関係代数 (∩)
内部結合 (inner join) は SQL ・ pandas ・ Spark など多様なツールで提供される基本演算。 上図中心からは、 左右テーブル ・ 結合キー ・ 結合条件 (等価 / 範囲 / 複合キー) ・ 関連手法 (left/right/outer/cross join) の軸が放射する。 SSDSE-B のように都道府県コードを結合キーとした統計データ統合では最頻出の演算。
🔗 隣接手法への橋渡し
inner join は、 データ準備フェーズで複数テーブルを 1 つの分析ユニットにまとめる中核演算。 前後の処理と密接に連携する。
上流 : CSV ・ 集計 ・ 欠損値処理 — 結合前に各テーブルを正規化・キー列のユニーク化が必要
並列 : 左結合 (left join) ・ 右結合 (right join) ・ 外部結合 (outer join) — どちらかにしか存在しない行を含めるか否かで使い分け
下流 : 回帰分析 ・ BI ツール ・ 箱ひげ図 — 結合後の分析・可視化
inner join の落とし穴は「キーが一意でないと積集合 (cartesian product) が発生して行数が爆発する」点。 結合前に df.groupby(key).size() でキーの重複を確認する習慣をつける。
🌳 手法選択フロー
テーブル結合の種類を選ぶとき、 「どちらにしかない行を残すか」と「キー対応関係」で判定する。
両テーブルに存在する行のみが欲しいか? Yes → inner join (例: 観測 ID と購入 ID が両方ある行のみ抽出)、 No → 次へ
左 (基準) テーブルの全行を保持したいか? Yes → left join (例: 全顧客リストに購入履歴を付与、 購入が無い顧客は NaN)、 両方の全行なら full outer join
結合キーは一意か? 一意 (1:1) なら通常の inner join、 重複 (1:N / N:M) なら結果行数を予想して groupby 集計を先に行うか確認
結合は順序が結果に影響しないが、 行数増減と NaN 発生は join 種別で大きく異なる。 結合後は必ず df.shape と df.isna().sum() で確認する。