論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
データ結合
Data Join
データ処理
別称: join / merge

🔖 キーワード索引

直感で掴む 定義 数式を言葉で読み解く SSDSE-B-2026 実値計算 Python での扱い 触って理解する よくある落とし穴 関連手法・派生 関連用語・派生 関連グループ教材 用語マップ 隣接手法への橋渡し 手法選択フロー

💡 30秒で分かる結論

🍰 まずはやさしく

バラバラの表をつなげるパズルのような操作です。

情報を一つにまとめて分析しやすくするために使います。

出席簿とテスト結果の表を、出席番号でつなげるイメージです。

結合の種類と注意点を短時間で確認しましょう。

複数の表をキーで結合する操作

データ結合 を 30 秒で把握する重要ポイント:

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

🍰 まずはやさしく

複数の表を共通の項目でつなぐ操作のことです。

バラバラに保存されたデータを1枚の表にするために使います。

スマホの連絡先と、個別のメモ帳を名前でつなげるようなものです。

現場でよく使われる5つの結合パターンを学びましょう。

あなたは今、 データ結合 (Data Join) という、 複数の表 (テーブル) を共通キーで接続して 1 枚にまとめる操作の用語ページを見ています。 SQL の JOIN、 pandas の merge / join / concat に相当する上位概念です。 内部結合外部結合 はその下位概念。 SSDSE-B-2026 のような分割された公的統計を統合するときの基本動作です。

📍 現場で使える結合パターン集 (SSDSE-B-2026 を例に)

日々の分析作業で頻出する結合パターンを 5 つ紹介する。 すべて SSDSE-B-2026 の都道府県データで動かせる。

パターン 1: マスタ表をルックアップとして使う (LEFT JOIN)

SSDSE の Code (R13000 等) を都道府県コード一覧と突合。 マスタに無い県は NaN で残し、 後で確認。

パターン 2: 抽出表で母集団を絞る (INNER JOIN)

「対象 10 県のリスト」と SSDSE 47 県を INNER JOIN すれば、 10 行だけが残る。 SQL の WHERE IN と同等。

パターン 3: 異質な期間を統合 (OUTER JOIN)

SSDSE-A-2025 と SSDSE-B-2026 は対象年度が異なる。 県をキーに OUTER で結合すると「片方にしかない年」を含めて全体像を見られる。

パターン 4: 時刻最近接結合 (merge_asof)

SSDSE は年次データだが、 日次イベントと突合したい場合は merge_asof で「直近の年データ」を引っ張れる。 年度切替の前後の挙動に注意。

パターン 5: アンチ・ジョイン (NOT IN 相当)

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 対多結合で行数が爆発」「複合キー設定漏れによる重複行」といった頻発トラブルの診断手順まで順に辿ります。

🎨 直感で掴む: 結合の図解 (Venn 図と表のイメージ)

結合の 4 種類は「Venn 図」のどの部分を取るかで表せる。 SSDSE-B-2026 と外部表 (沖縄欠落) を例に、 図と表で対応を示す。

結合Venn 図イメージ日常の比喩
INNERA ∩ B (中央の重なり)「両方の名簿に載ってる人だけ呼ぶ」
LEFTA 全体 (左の円ぜんぶ)「A の名簿全員。 B にないなら空欄でいい」
RIGHTB 全体 (右の円ぜんぶ)「B の名簿全員。 A にないなら空欄でいい」
OUTERA ∪ B (両方の和)「どっちかに載ってれば全員呼ぶ」

SSDSE-B-2026 (47 県) と「沖縄欠落の 46 県表」では、 INNER = 46 (重なり)、 LEFT = 47 (47 県全部)、 RIGHT = 46 (46 県全部)、 OUTER = 47 (和集合だが両方とも 47 県以下なので 47 が上限)。 これは Venn 図上の各領域の要素数として直感的に納得できる。

📐 定義

🍰 まずはやさしく

共通のキー(目印)を使って表を合体させることです。

別々の場所にあるデータを正しく結びつけるために使います。

学生IDを使って、名簿と成績表をセットにするようなものです。

正しい結合の方法と、ミスを防ぐための確認手順を学びましょう。

複数の表をキーで結合する操作

英語名 Data Join。 同義・関連語:join, merge。

🎯 いつ・どこで使うか

📋 前提条件・適用範囲

この用語を理解・使用するときは、 次のような前提を意識してください:

📐 結合後の行数を「事前検算」する習慣

このコードでやること: 「結合後に行数が爆発した」事故を防ぐため、 結合前にキーの一意性 × 多重度を組み合わせて期待行数を計算する。 SSDSE-B-2026 のような小規模データでも、 同じ習慣を「縮小版」で確認すれば、 大規模化したときに同じ計算で守れる。

📥 入力例(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
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)

📤 実行例:

left: 47 行 (キー一意: True) right: 5 行 (キー一意: False) right の重複: {'東京': 2, '大阪': 1, '愛知': 1, '北海道': 1} 期待行数 (inner): - 一致 left 行数: 1 - right の最大重複: 2 実測行数: 1 行 都道府県 総人口 産業 従業者比率 0 北海道 5140000 観光 0.22

💬 結果の読み方: 期待では「東京が 2 行に展開されて 5 行」のはずが、 実測は 1 行しか出ない。 原因は表記揺れ: right の '東京'・'大阪'・'愛知' は略称で、 SSDSE-B-2026 側は正式名称 '東京都'・'大阪府'・'愛知県'。 文字列が完全一致しないため inner join で全て脱落し、 唯一表記が一致する '北海道' だけが結合された。 match_count も 4 ではなく 1。 これはこのページが繰り返し警告する「キーの表記揺れ」の落とし穴そのもの。 結合前に set(left['都道府県']) & set(right['都道府県']) で共通キーを数え、 略称・正式名称・全角半角を正規化してから結合する癖をつけたい。 pandasvalidate='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。

🔬 関連手法比較表 (結合の手段)

操作方向pandasSQL用途
merge横 (列方向)pd.mergeJOIN ONキーで列追加
join横 (列方向)df.joinJOIN USINGインデックス結合の簡易版
concat縦 / 横pd.concatUNION ALL同構造の積み上げ
appenddf.append (非推奨)INSERT廃止予定
combine_first横 + 上書きdf.combine_firstCOALESCE欠損補完結合
merge_asof横 (近傍)pd.merge_asofwindow join時系列キー近似結合

💔 データ結合の失敗例 5 連発

  1. キー型不一致: 文字列 vs 整数の Code が一致せず、 inner join で 0 行に。 対策: astype(str) で揃え、 dtypes 確認。
  2. concat の列ずれ: 2022 年と 2023 年で列名が微妙に違うと、 concat 後に NaN だらけになる。 対策: columns を事前に揃える。
  3. 多対多の爆発: 想定 1,000 行が 100 万行に。 対策: validate、 事前 dedupe。
  4. サフィックス汚染: 同名列に _x, _y が付くままレポート出力。 対策: suffixes で意味のある名前に。
  5. キーの空白・全角半角: 'R13000' vs 'R13000' (全角) で結合失敗。 対策: 正規化関数を結合前に必ず通す。

📝 演習問題 5 問

  1. Q1: SSDSE-B-2026 を 47 行 × 3 列の df_pop と 47 行 × 2 列の df_house に分け、 inner join した結果の shape は? (答: (47, 4))
  2. Q2: 同じ表を outer join した結果の shape と F3101 の NaN 件数は? (答: (47, 4) と 0)
  3. Q3: 上下に縦連結 (concat) すると行数はいくつ? (答: 94)
  4. Q4: cross join の行数は? (答: 47 × 47 = 2,209)
  5. Q5: merge と join の主な違いは? (答: merge は列名キー指定、 join はインデックスキー指定が基本)

📖 関連用語辞典 10 語

内部結合
共通キーがある行のみ残す。 もっとも厳格な結合。
外部結合
片側にしかなくても NaN で残す。 損失回避型。
主キー
結合の正確性を担保するユニークキー。
外部キー
他テーブルの主キーを参照する列。
SQL
JOIN ... ON による宣言的結合。
pandas
merge / join / concat / merge_asof
データベース
結合本来の住処。 索引で高速化。
テーブル
結合の最小単位。 行 × 列。
Tidy data
結合しやすい 1 観測 1 行形式。
ピボットテーブル
結合後の集計に直結する操作。

🔬 Round 91 追補: データ結合の深掘り (関係代数・型・整合性検査)

この節では「データ結合 (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 は速いのか

pandas.merge の内部は、 データサイズや結合タイプに応じて以下のアルゴリズムを自動選択する。 これを理解しておくと、 大規模データで結合が遅いときに何を変えればよいか判断できる。

3 つの主要結合アルゴリズム

アルゴリズム計算量適する状況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 実値で計算

SSDSE-B-2026 (47 都道府県) から、 人口表と経済表を別 CSV と仮定して 5 種類の結合を比較します。

結合タイプ出力行数 (右に沖縄なし)A1101 が NaN の件数F3101 が NaN の件数
inner4600
left4701 (沖縄)
right4600
outer4701
cross47 × 46 = 2,16200

手計算で確認: 右に沖縄が無い場合、 共通キー数 = 46。 inner=46 (両方にある分のみ)、 left は左 47 を全部残すから 47 (沖縄行は F3101=NaN)。 cross は単純積 47×46。

🧮 SSDSE-B-2026 で実値結合: 都道府県 × 仮想 IT 投資データ

ここでは SSDSE-B-2026 から「都道府県人口 (A1101)」と「合計特殊出生率 (A4103)」を取り出した表 A と、 別途用意した「仮想 IT 投資指数」表 B を結合する。 表 B は意図的に 46 行のみ (沖縄県を欠落) にしてあり、 INNER / LEFT / OUTER で行数がどう変わるかを観察する。

📥 入力 1: 表 A (SSDSE-B-2026 から作成、 47 行)

SSDSE-B-2026 Prefecture A1101 A4103 2023 北海道 5092000 1.06 2023 青森県 1184000 1.23 2023 岩手県 1163000 1.16 2023 宮城県 2264000 1.07 ... 2023 東京都 14086000 0.99 ... 2023 沖縄県 1468000 1.60 [全 47 都道府県、 2023 年データ]

📥 入力 2: 表 B (仮想 IT 投資指数、 46 行、 沖縄欠落)

Prefecture IT_invest_index 北海道 45.2 青森県 32.1 岩手県 33.5 宮城県 58.7 ... 東京都 98.4 ... 鹿児島県 35.1 [沖縄県は欠落、 全 46 行]

🎯 結合操作の予測 (実行前に手計算で確認)

結合タイプ期待行数理由沖縄の状態
INNER46両表に存在するキー = 46 県消える
LEFT (A 基準)47A の全 47 県保持IT_invest が NaN
RIGHT (B 基準)46B の全 46 県保持消える
OUTER47A ∪ 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))

📤 実行結果:

結合前: A=47 行, B=4 行 結合後: 47 行 (うちマッチ 4 行) Prefecture pop capital_pop_million 北海道 5092000 1.97 東京都 14086000 9.70 大阪府 8763000 2.70 沖縄県 1468000 0.32

💬 結果の読み方: 4 つの assert により「結合前に問題があれば即停止」する保護網を作っている。 LEFT JOIN は左表の行数を保つはずなので、 結合後に len(merged) != len(A) なら異常。 これらの assert が無い場合、 異常データが下流に流れて分析結果を歪める。

🧮 実習問題: SSDSE-B-2026 で結合の手を動かす

以下の問題は SSDSE-B-2026 を使って実際に解ける。 答えは段階的なヒント付きで掲載。

問題 1: 2010 年と 2023 年の人口を横並びにせよ

📥 入力: SSDSE-B-2026 から 2 年分を抽出。 期待出力: 47 行、 列は Prefecture, pop_2010, pop_2023, ratio

ヒント: SSDSE-B-2026 は 2012-2023 を含む。 2010 年は無いので 2012 年で代用するか、 利用可能な最古年を使う。 year=2012year=2023 をそれぞれ抽出し、 Prefecture をキーに INNER JOIN。

問題 2: 人口減少率 TOP 5 県の合計特殊出生率と相関を見よ

📥 入力: 問題 1 の結果 (47 行) + SSDSE-B-2026 の A4103 (合計特殊出生率)。 期待: 減少率と出生率の関係を 47 県で plot。

ヒント: 自己結合で 2023 年 vs 2022 年の人口変化率を出し、 同じ年の出生率を merge で添えて scatter plot。

問題 3: 自己結合と pandas.shift の違いを説明せよ

📥 入力: 上記のコード例。 ヒント: shift(1) は単純に 1 行ずらすだけで「県」境界を超えてしまう。 groupby('Prefecture').shift(1) なら県内で完結。 これは「同じ結果に見えて、 仕組みが違う」典型例。

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

合成データで Inner/Left/Outer JOIN の結果行数を計算する。

Step 1: 元データ

テーブル行数キー一致数
A (顧客)100
B (注文)80
マッチ70

Step 2: 各 JOIN 結果

INNER: 70 行 (両方にあるキーのみ) LEFT: 100 行 (A 全件 + B 未一致は NULL) RIGHT: 80 行 (B 全件 + A 未一致は NULL) FULL OUTER: 100 + 80 - 70 = 110 行

🐍 Python で再現

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}")

📤 実行結果

INNER: 70 LEFT: 100 RIGHT: 80 FULL OUTER: 110

💬 手計算 (Step 2) と 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)
print(df.shape)
print(df.dtypes)
print(df.describe())

# 「データ結合」の文脈で扱う場合の例:
# 分野: データ処理
# 関連手法は同カテゴリの他用語を参照してください。
📤 実行例(実測) (564, 112) 年度 int64 地域コード object 都道府県 object 総人口 int64 総人口(男) int64 ... 保健医療費(二人以上の世帯) int64 交通・通信費(二人以上の世帯) int64 教育費(二人以上の世帯) int64 教養娯楽費(二人以上の世帯) int64 その他の消費支出(二人以上の世帯) int64 Length: 112, dtype: object 年度 総人口 ... 教養娯楽費(二人以上の世帯) その他の消費支出(二人以上の世帯) count 564.000000 5.640000e+02 ... 564.000000 564.000000 mean 2017.500000 2.690688e+06 ... 26931.026596 59784.718085 std 3.455117 2.730951e+06 ... 4219.487086 8813.812956 min 2012.000000 5.370000e+05 ... 14661.000000 35 …(以下略)

具体的なコードは データエンジニアリング を参照してください。

📝 レポートでの報告

分析結果を報告するときに含めるべき情報:

✅ チェックリスト

🐍 Python 実装 (4 要素遵守)

コード A: 5 種類の結合を順に実行

🎯 このコードでやること: SSDSE-B-2026 を 2 つの仮想 CSV に分割し、 inner/left/right/outer/cross の 5 通りの結合を順に実行して行数を比較する。

📥 入力データ (SSDSE-B-2026 抜粋):

df_pop : 47 行 (Code, Prefecture, A1101) df_house: 46 行 (Code, F3101) ※沖縄を意図的に除外
 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, 4), F3101 NaN = 0 left : (47, 4), F3101 NaN = 1 right : (46, 4), F3101 NaN = 0 outer : (47, 4), F3101 NaN = 1 cross : (2162, 5)

💬 結果の読み方: inner は両方にあるキーのみ → 46 行。 left は左を全部残す → 47 行 (沖縄の F3101 が NaN)。 cross は 47×46=2,162 行に爆発。 用途に応じて使い分ける。

コード B: 多キーで結合 (年度 + コード)

🎯 このコードでやること: 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))

📤 実行結果:

多キー結合 shape: (47, 4) SSDSE-B-2026 Code A1101 F3101 0 2023 R01000 5092000 156458 1 2023 R02000 1184000 43713

💬 結果の読み方: 年度 (2023 のみ) と Code (47 個) の組合せは 47 通り。 多キー結合は年度横断分析の基本パターン。

コード C: concat による縦方向の結合

🎯 このコードでやること: 同じ列構造の 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())

📤 実行結果:

縦連結 shape: (94, 4) 2022 47 2023 47 Name: Year, dtype: int64

💬 結果の読み方: concat は「列方向の merge」ではなく「行方向の積み上げ」。 同じ構造のパネルを長期化するときに使用。

コード D: indicator で結合品質を診断

🎯 このコードでやること: 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())

📤 実行結果:

both 46 left_only 1 right_only 0 Name: _merge, dtype: int64 左にしかない: ['沖縄県']

💬 結果の読み方: 沖縄が左にしか存在しない → inner なら消える。 結合前に必ず indicator で診断するのが鉄則。

🏭 産業界での活用事例 6 件

  1. 顧客 360°ビュー: CRM × 購買履歴 × Web ログ × アンケートを customer_id で結合し、 1 顧客あたり 1 行のフルプロファイルを構築。
  2. 商品マスタ拡充: 商品 ID をキーに製造情報・在庫情報・価格履歴・レビューサマリを結合し、 BI 用の統合テーブルを夜間バッチで生成。
  3. 都道府県統計ダッシュボード: SSDSE-B (経済) と SSDSE-C (家計) と SSDSE-E (教育) を Code で結合し、 47 都道府県の総合カルテを作る。
  4. ログ × マスタ照合: アクセスログ (数百万行) と IP 範囲マスタ (数千行) を range-join で結合し、 地域別アクセス分析。
  5. 金融取引照合: 入金明細と出金明細を取引 ID で結合し、 不整合を検知 (照合監査)。
  6. サプライチェーン可視化: 発注 × 受注 × 出荷 × 到着 のテーブルを order_id でチェーン結合し、 リードタイムを算出。

🐍 Python 実装 (1): pandas.merge で 4 種類の結合を比較

🎯 このコードでやること: 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}')

📤 実行結果:

表 A 行数: 47 表 B 行数: 46 INNER : 行数=46, IT_NaN=0, 人口_NaN=0 LEFT : 行数=47, IT_NaN=1, 人口_NaN=0 RIGHT : 行数=46, IT_NaN=0, 人口_NaN=0 OUTER : 行数=47, IT_NaN=1, 人口_NaN=0

💬 結果の読み方: 予測通り 4 種類の結合は 46, 47, 46, 47 行になる。 LEFT と OUTER は 47 行で、 沖縄県の IT_invest_index が NaN として表れる (IT_NaN=1)。 INNER と RIGHT は沖縄が消えるため 46 行で NaN は出ない。 validate='one_to_one' を付けたことで もしキーが重複していれば即エラーになる。 これは「想定外の多対多結合による行数爆発」を防ぐ最重要オプション。

🐍 Python 実装 (2): 型不一致による「結合できない」失敗の再現

🎯 このコードでやること: 結合キーの「データ型」が片方は文字列、 もう片方は数値だと、 値が同じに見えても結合できないことを 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))

📤 実行結果:

失敗例 (型は str 同士だが値が違う): 0 行 成功例 (整数キーで揃えた): 4 行 Code_num Prefecture capital_name 1 北海道 札幌市 13 東京都 新宿区 27 大阪府 大阪市 47 沖縄県 那覇市

💬 結果の読み方: 表 A の Code 列は R01000, R13000, R27000, R47000 形式の文字列なのに、 表 B の Code_str は '1', '13', '27', '47'。 「文字列同士」なので Python の型エラーは出ないが、 値が違うため 0 行しかマッチしない。 これが現場で最も多い失敗。 対策は (1) 結合前に必ず df.dtypes を確認、 (2) サンプル値を print(A['key'].head().tolist()) で目視。

🐍 Python 実装 (3): SSDSE-B-2026 の時系列を「年-県」複合キーで縦結合する

🎯 このコードでやること: 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))

📤 実行結果:

サブセット 1 行数: 141 サブセット 2 行数: 141 縦結合行数: 282 横結合 (年・県キー) 行数: 0 県のみで横結合 行数: 423 ピボット後 形状: (47, 6) year 2018 2019 2020 2021 2022 2023 Prefecture 三重県 1782000 1771000 1770254 1756000 1742000 1727000 京都府 2591000 2583000 2578087 2561000 2550000 2540000 佐賀県 819000 815000 811442 806000 801000 795000

💬 結果の読み方: 「縦結合 (concat)」は年を区別したまま積み上げるので 141 + 141 = 282 行になる。 「年と県の複合キーで横結合」は両サブセットに共通する (year, Prefecture) ペアが存在しないため 0 行。 「県のみで結合」は 3 × 3 × 47 = 423 行に爆発する。 時系列を扱うときの定石は「縦結合 → ピボット (pivot)」で、 行が県、 列が年の「ワイド形式」に変形する流れ。 47 県 × 6 年 = (47, 6) の表が得られる。

🐍 Python 実装 (4): SSDSE-B-2026 で「自己結合」して年次変化率を計算

🎯 このコードでやること: 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))

📤 実行結果:

2023 年 人口減少 TOP5: Prefecture pop_prev pop change_pct 秋田県 930000 914000 -1.720 青森県 1204000 1184000 -1.661 岩手県 1181000 1163000 -1.524 高知県 676000 666000 -1.479 山形県 1041000 1026000 -1.441 2023 年 人口増加 TOP5: Prefecture pop_prev pop change_pct 滋賀県 1409000 1407000 -0.142 埼玉県 7337000 7331000 -0.082 神奈川県 9232000 9229000 -0.032 沖縄県 1468000 1468000 0.000 東京都 14038000 14086000 +0.342

💬 結果の読み方: 自己結合により「現在年と前年」が 1 行に並び、 引き算で変化率が得られる。 2023 年で人口が増えたのは東京都 (+0.34%) のみ、 他はすべて減少。 秋田・青森が -1.6% で最大減少。 これは SELF JOIN の典型応用で、 「同じ表を時間方向にずらしてくっつける」という考え方は時系列分析の基本テクニック。

🐍 アンチ・ジョインの実装例 (差集合の検出)

🎯 このコードでやること: SSDSE-B-2026 の 47 県と外部 46 県の差集合 (沖縄のみ) を見つける。 「アンチ・ジョイン」と呼ばれる、 SQL の NOT INNOT 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)

📤 実行結果:

A にあって B に無い県: 1 件 Prefecture _merge 28 沖縄県 left_only

💬 結果の読み方: 47 県 - 46 県 = 1 県の差。 indicator=True で生成される _merge 列が 'left_only' なら左のみ、 'right_only' なら右のみ、 'both' なら両方に存在することを示す。 これは「マスタとトランザクションの突合で発生する未登録エンティティ検出」など、 現場で頻出する。

⚙️ 補遺: 結合戦略の比較 — broadcast / hash / sort-merge join

pandas の merge は内部的に hash join を使うが、 大規模データ (Spark / Dask / DuckDB) では「片方が極端に小さい / 両方ソート済み / 両方とも巨大」など状況に応じて broadcast joinhash joinsort-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)に強い。

🐍 Python 実装: pandas merge と merge_asof の使い分け

🎯 このコードでやること: SSDSE-B-2026 を年度別の二テーブルに分割し、 (1) 通常の等値結合と (2) merge_asof による最近傍時系列結合を比較する。 後者は sort-merge join の典型例。

📥 入力例:

A: Prefecture, Year(2024), 人口 B: Prefecture, Year(2026), 消費支出
 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))

📤 実行結果:

hash join 結果: 47 行 merge_asof 結果: 47 行 都道府県 総人口 Year 消費支出(二人以上の世帯) 0 北海道 5092000 2024 296888 1 京都府 2535000 2024 314636 2 大阪府 8763000 2024 271246

💬 結果の読み方: merge は厳密一致のみだが、 merge_asofdirection='nearest' で最も近い時点を当てに行く。 株価データやセンサーログのように「サンプリング間隔が不規則」な時系列結合で必須の機能。 Dask / Spark では同じロジックが broadcast_hintsort_merge_join として明示できる。

⚠️ 結合戦略を誤ると壊滅する例

関連: ハッシュテーブル時系列DaskApache SparkDuckDB

🔗 補講 R280: データ結合の「設計時に決めるべき 7 つの問い」

データ結合前後の値変化の概念図

結合キーの一致確認に使う散布図の例

複数テーブル間の関連を可視化した例

データ結合 (JOIN) は SQL でも pandas でも 1 行で書ける単純なように見えるが、 ETL パイプラインを止める障害の上位 3 位に必ず入る。 結合キーが揺れる、 重複が起こる、 行数が予期せず増減する、 NULL 処理が不統一、 など失敗パターンが多い。 本補講では、 結合を実装する前に必ず確認すべき 7 つの問いを「設計チェックリスト」として整理し、 SSDSE-B-2026 の都道府県マスタを使って各ケースを再現する。

▶ JOIN 実装前のチェックリスト 7 問

#問い確認方法失敗時の症状
1キーは一意か?df.key.is_unique行数が増える
2NULL は許容するか?df.key.isna().sum()NULL=NULL は FALSE → 欠落
3データ型は一致?df.key.dtypestr vs int → 結合 0 件
4表記揺れは?df.key.str.strip().value_counts()「東京」と「東京 」が別物
5how の選択は?分析目的を確認inner で必要な行が消える
6期待行数は?事前に手計算行数爆発を見落とす
7結合後の重複列は?suffixes 指定key_x, key_y で混乱

▶ SSDSE-B-2026 と地方区分マスタの inner join 実例

このコードでやること: SSDSE 都道府県データに「地方区分マスタ」(都道府県 → 地方) を内部結合し、 地方ごとの集計に発展させる。 マスタ未登録の都道府県が落ちる典型ケースを意図的に作って確認する。

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 SSDSE-B-2026(年度) Prefecture(都道府県) A1101(総人口) L3221(消費支出(二人以上の世帯)) 北海道 2,023 北海道 5,092,000 296,888 東京都 2,023 東京都 14,086,000 341,320 沖縄県 2,023 沖縄県 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
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()}')

📤 実行例:

元データ: 47 行 inner join 後: 1 行 (北海道のみ一致) left join 後: 47 行 (46 県の地方は NaN) 地方が NaN の県: ['青森県', '岩手県', '宮城県', '秋田県', '山形県', '福島県', '茨城県', '栃木県', '群馬県', '埼玉県', '千葉県', '東京都', '神奈川県', '新潟県', '富山県', '石川県', '福井県', '山梨県', '長野県', '岐阜県', '静岡県', '愛知県', '三重県', '滋賀県', '京都府', '大阪府', '兵庫県', '奈良県', '和歌山県', '鳥取県', '島根県', '岡山県', '広島県', '山口県', '徳島県', '香川県', '愛媛県', '高知県', '福岡県', '佐賀県', '長崎県', '熊本県', '大分県', '宮崎県', '鹿児島県', '沖縄県']

💬 結果の読み方: 「沖縄だけが脱落して 46 行」を期待したのに、 inner の結果はわずか 1 行。 原因はキーの表記揺れ: マスタ側は '青森'・'東京'・'大阪' と略称で登録しているが、 SSDSE-B-2026 の 都道府県'青森県'・'東京都'・'大阪府' と正式名称。 文字列が一致するのは '北海道' だけなので、 inner join では 46 県が丸ごとサイレントに脱落した。 これは「未登録レコードの脱落」よりさらに気付きにくい事故で、 集計値が突然小さくなる典型原因。 left join にすれば全 47 行が残り、 地方 が NaN の県を洗い出せる(=表記揺れの検知)。 対策はキーを結合前に正規化(略称↔正式名称の対応表を用意)すること。 データクレンジング内部結合外部結合 も併読推奨。

📝 より正確な分析:本来意図した「沖縄のみ脱落 → inner 46 行」を得るには、 マスタの都道府県名を '青森県'・'東京都' …と正式名称に揃える必要がある。 略称のままでは北海道以外すべて脱落する。

▶ pandas merge と SQL JOIN の対応表

操作pandasSQL結果の行数
内部結合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 点を実装前に必ず確認する。 主キー外部キーリレーショナルデータベースSQLpandasテーブル結合 の各ページと組み合わせて、 安全な結合パイプラインを構築する。

🎓 補講 R280-9: データ結合の総括と学習ロードマップ - 次のトピックへ

データ結合は RDB の中心操作だが、 落とし穴が多く、 実装難易度の割に習熟難度が高い。 ここまでの補講内容を 1 ページにまとめると、 (1) 7 つの設計時の問いに必ず答える、 (2) キー正規化を 6 ステップで実装、 (3) 性能チューニング 5 原則を適用、 (4) 6 つの監視項目で本番監視、 (5) 6 つの誤解をクリアにする、 が本質。 これらを習得した後の学習トピックは、 分散結合 (Spark/BigQuery)、 ストリーミング結合 (Kafka Streams)、 グラフ DB 上の結合 (Neo4j)、 などの応用領域。 各トピックは独立した補講ページや論文サマリで学べるよう、 用語集全体で相互リンクを張ってある。

▶ 学習ロードマップの整理

  1. 基礎: inner/left/right/outer/cross の挙動を 47 県データで確認できる
  2. 実装: pandas merge と SQL JOIN を等価に書ける
  3. 設計: 7 つの問いに答えてから結合を実装できる
  4. 運用: 6 つの監視項目を本番に組み込める
  5. 性能: Hash/Sort-Merge/Nested Loop の使い分けを EXPLAIN で確認できる

これら 5 段階を順に習熟することで、 データ結合を「実装」から「運用品質を保証する操作」へと昇格できる。 リレーショナルデータベースSQLpandas主キー外部キー と組み合わせて、 データ統合の総合パイプラインを完成させる。 特に大規模分析基盤の構築では、 結合の品質保証が全体パイプラインの信頼性を決定づける核心要素となるため、 単なる「便利な操作」を超えた重要技術である。

📖 補講 R280-8: 結合に関する誤解と正しい理解 まとめ

データ結合は多くの誤解を生む操作でもある。 実務でレビュー時に頻出する誤解を 6 つ整理し、 正しい理解を併記した。 自分の理解度をチェックするためのまとめ。

▶ 6 つの誤解と正しい理解

誤解正しい理解
inner と outer は結果が同じNULL の扱いと一致しない行の取捨選択が異なる
JOIN は左右どちらでも同じleft/right JOIN では基準テーブルが変わる
NULL = NULL は TRUESQL/pandas 共に FALSE 扱い (IS NULL 必要)
結合キーは 1 列のみ複合キー (年月 + 都道府県等) も可
cross join は禁止小規模で正当な用途あり (カレンダー × 商品)
JOIN の速度は order に依存しないオプティマイザは順序を変えるが、 ヒント指定可能

補講のまとめ: 結合は「実装が簡単に見える」反面、 落とし穴が多い操作。 6 つの誤解をすべてクリアにし、 「自分で説明できる」状態を目指すこと。 これにより本番運用での突然のバグや、 集計値の静かな誤りを未然に防げる。

🎮 触って理解する

実務の結合は「いきなり merge」ではなく、 ① キー品質診断 → ② 結合 → ③ 結果検証 の 3 ステップで行う。 下のシミュレータでは、 SSDSE-B-2026 の 2023 年・総人口 (A1101 実測値、 8 県抜粋) を左表、 「地方区分ルックアップ表」 (説明用のマスタ例、 沖縄県が未登録) を右表として、 実務で頻発する 4 つのキー障害をトグルで仕込みながらこのワークフローを体感できる。 なお テーブル結合 には結合 4 タイプの切替、 データ統合 には JOIN シミュレータがあるので、 結合タイプ自体の復習はそちらで。

使い方: (1) 障害トグルを ON にして「① 診断」のカードが赤くなるのを確認 → (2) 「キー正規化」で直せるものと直せないものを見分ける → (3) 「結合実行」 → (4) 「③ 検証」で行数と欠損率が期待どおりか自動チェック。 図のキーの点はタップ / ドラッグでなぞると対応関係が見られる。
① キー品質診断 ② 結合 ③ 検証
右表 (ルックアップ表) に障害を仕込む:

① キー品質診断ダッシュボード

キーの点をタップ / ドラッグすると対応関係を表示

💡 直感: 結合は「事前診断と事後検証」で安全になる

merge の 1 行そのものは間違えようがない。 事故が起こるのはその前後だ。 事前にキーの重複・欠損・型を診断していれば「東京都が 2 行に展開される」「一致率 0% で全列 NaN」は結合前に予見できる。 事後に行数と欠損率を検証していれば、 予見と違う結果が出た瞬間に気づける。 上のシミュレータで障害トグルを全部 ON → 正規化 ON → 結合、 という流れを試すと、 正規化で直る問題 (表記揺れ・型) と、 直らず設計判断が要る問題 (重複キー・キー欠損) があることが体感できる。

⚠️ よくある落とし穴 (ワークフロー視点)

🚀 発展: validate 引数とアンチ結合での照合

⚠️ よくある落とし穴

❌ 多対多結合での行数爆発
「Prefecture だけ」でキーを取ると同名が複数行に対応 (12 年 × 47 県 = 564 行が 564×47=26,508 行に膨張)。 必ず複合キー (Prefecture + Year) を指定し、 merge 前後で行数を assert。 validate='one_to_one' オプションが安全装置。
❌ join 種別の使い分け誤り
INNER は両方にあるキーのみ、 LEFT は左側全保持、 OUTER は和集合。 「人口が全 47 県揃っているはず」と思って INNER したら 46 県になっていた、 等の脱落を見落とす。 LEFT JOIN + 欠損列カウントで漏れを検知。
❌ キー型の不一致 (string vs int)
片方が「01」「02」(zero-padded string)、 もう片方が 1, 2 (int) だと一切マッチしない静かな失敗。 結合前に 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 のみ564564 (1対多OK)
564 (12年×47県)564 (12年×47県)Prefecture のみ6,7686,768 (12倍に爆発)
564 (12年×47県)564 (12年×47県)Prefecture と Year564564 (正しい)

教訓: 結合キーは「一意になる組合せ」を必ず指定する。 時系列データでは「Prefecture + Year」のように複合キーが必須。 単純化したくて 1 列だけで結合すると、 同じ Prefecture が複数年分マッチしてしまい行数が爆発する。

⚠️ 落とし穴: 結合で陥りやすい 8 大失敗

  1. キー型の不一致: 文字列の '13' と整数の 13 はマッチしない。 結合前に df.dtypes を確認。
  2. キーの空白・大文字小文字: '東京都 ' (末尾空白) と '東京都' は別物。 str.strip() 必須。
  3. 多対多の暗黙発生: 想定外の重複が原因で行数爆発。 validate='one_to_one' で検出。
  4. 結合方向の誤解: LEFT と RIGHT を混同し、 想定外に行が消える/増える。 必ず行数を事前予測。
  5. カラム名の衝突: 同じ列名が両表にあると _x, _y 接尾辞がつく。 suffixes で明示的に。
  6. NULL の伝播: 結合後の片方が NaN になった行を集計に使うと平均が歪む。 必ず dropnafillna 判断。
  7. 順序の喪失: 結合後は行順が保証されない。 必要なら sort_values を最後に。
  8. 巨大表の総積: 100 万行 × 100 万行のクロス結合は 10^12 行。 メモリ爆発するので注意。

⚠️ 補講 R280-7: 結合の本番運用で押さえる 6 つの監視項目

データ結合は本番運用で「サイレントに壊れる」最大の経路でもある。 マスタテーブルが更新されたとき、 結合キーの形式が変わったとき、 取引データに新規取引先が増えたとき、 静かに結合失敗や行数爆発が起こる。 本番運用での 6 つの監視項目を整理した。

▶ 本番運用 6 つの監視項目

監視項目確認方法閾値例
結合成功率left 全件中の一致行数95% 未満で警告
行数変化率前日比 ±10% 以上で警告±10%
NULL 比率結合キーの NULL 率5% 超で警告
新規キー数前日にないキーの出現100 超で確認
重複度right の最大重複数10 超で確認
実行時間結合 SQL の duration前日比 2x で警告

補講のまとめ: 結合の本番運用では「結合成功率・行数変化・NULL 比率・新規キー・重複度・実行時間」の 6 項目を毎日監視する。 これらを Slack 通知や Datadog ダッシュボードに統合しておくと、 サイレント障害を早期検知できる。 RDBSQLデータクレンジング の運用ガイドと組み合わせて、 結合パイプラインの SLA を維持する。

🗺 補講 R280-6: 結合関連の用語マップと周辺概念

データ結合は単独で完結する操作ではなく、 主キー・外部キー・正規化・参照整合性・トランザクションといった周辺概念と連携して初めて安全に機能する。 ここでは結合に関連する 8 つの周辺概念を整理し、 学習の優先順位を示す。

▶ 結合関連の周辺概念 8 つ

概念役割学習順
主キー行を一意に識別最優先
外部キーテーブル間の関連2 番目
正規化重複排除3 番目
参照整合性FK の有効性保証4 番目
インデックス結合の高速化5 番目
JOIN の種類行の取捨選択6 番目
トランザクション原子性保証7 番目
分散結合大規模 Spark/BigQuery8 番目

補講のまとめ: 結合は RDB の中心概念であり、 周辺の 8 概念と組み合わせて初めて安全に機能する。 学習の順序を意識しながら、 主キー外部キーリレーショナルデータベースSQLpandasリレーショナルDB 詳細 の各ページを巡回学習することで、 結合操作の総合的な理解が得られる。

データ結合 INNER JOIN LEFT / OUTER JOIN 主キー・外部キー pandas merge cardinality (1:N) スキーマ・キー整合

🔗 隣接手法への橋渡し

データ結合はクレンジングと特徴量エンジニアリングの橋渡し。

キー設計 → 表記正規化 → 結合タイプ選択 (inner/left/outer) → 重複/null 検証 → 結合結果検証の流れで、 各段で「行数の意味」を確認する。

🌳 手法選択フロー

データ結合はキー設計 → 方式選択 → 例外処理の三段で扱う。

  1. 共通キーはあるか? Yes → 主キー、 No → 外部キー を先に確認
  2. 結合方式は? Yes → 内部結合、 No → 外部結合 を先に確認
  3. 時系列・近傍結合か? Yes → AS OF 結合、 No → あいまい結合 を先に確認

完全一致なら inner join、 主テーブル保持なら left join、 全件保持なら outer join、 曖昧結合なら fuzzy/regex 結合、 と「保持したい行」で選ぶ。

🧭 解説深化: 複合キーの「一部を忘れる」罠 (SSDSE-B 実測)

このページは名寄せ・表記ゆれ・多対多の行増殖を既に手厚く扱っている。ここでは重複を避け、「結合キーは実は複数列でひとつ」という、SSDSE-B のように (Code, 年) が主キーになるパネルデータ特有の落とし穴だけを、実測値で深掘りする。

💡 直感

結合キーを郵便の宛先だと思うと分かりやすい。SSDSE-B の Code(都道府県コード)は「住所」、SSDSE-B-2026(年、2012〜2023 の 12 個)は「部屋番号」に相当する。住所だけ書いて部屋番号を書き忘れると、郵便局は同じ住所の全 12 部屋に配達してしまう。左表 12 通 × 右表 12 通 = 1 住所あたり 144 通の重複が生まれる。結合キーの一部を落とすとは、まさにこの「宛先の書き忘れ」である。

⚠️ 落とし穴 (重要): キーから「年」を落とすと SUM が静かに 12 倍になる

SSDSE-B-2026 は 47 都道府県 × 12 年 = 564 行。この表を自身と結合するとき、年を忘れて on='Code' だけで結合すると、各コードが 12×12=144 組に展開され、行数は次のようになる。実測値(encoding='cp932', skiprows=[1] で読み込み、A1101=総人口を集計)。

操作 結果行数 2023 総人口の集計値
元データ(基準)564124,353,000 人
正しい複合キー on=['Code','SSDSE-B-2026']564124,353,000 人
年を忘れた on='Code'6,7681,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')  # 増殖が構造的に不可能

複合キーでは各キー列の型を両表で揃えることも必須(片方が intCode、もう片方が '01' のような文字列だと全行が非マッチになる)。年列も同様に int か文字列かを統一する。

🔗 関連ページ