【Pandas マージ】merge()とjoin()の結合方法|inner・left・right・outer完全ガイド
現実世界のデータが1つのテーブルにまとまっていることはまずありません。ユーザー情報はあるテーブルに、注文履歴は別のテーブルに、商品詳細はさらに別のテーブルに、という具合です。分析ではこれらを互いに結びつける必要があり、それを実現するのがまさにmergeです。Pandasのmergeは、SQLのJOINタイプをすべて実装しています。本节では、Mermaid図を使って4種類すべての結合タイプの効果を図解し、「左テーブルから何が残り、右テーブルから何が残るのか」を明確に理解できるようにします。
⚠️ 注意: 以下のコードはローカルのPython環境で実行する必要があります。
1. 学習内容
- ❶ mergeの4つの結合タイプ
- ❷ on / left_on / right_on
- ❸ 複数キー結合
- ❹ suffixesとindicator
- ❺ validateとクロス結合
2. ボブのユーザー・注文関連付け
(1) 問題点:2つの別々のテーブルでは分析できない
ボブにはusersテーブルとordersテーブルがあります。彼は「各ユーザーが何を買ったか」を確認したいと考えています。
PYTHON
import pandas as pd
users = pd.DataFrame({
'user_id': [1, 2, 3, 4],
'name': ['アリス', 'ボブ', 'チャーリー', 'キャロル']
})
orders = pd.DataFrame({
'order_id': ['O001', 'O002', 'O003'],
'user_id': [1, 2, 2],
'amount': [120, 85, 200]
})
# どうやって組み合わせる?user_idで関連付ける必要がある
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
(2) 解決策:1行のmerge
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
: 基本的なマージ(難易度 ⭐)
PYTHON
import pandas as pd
users = pd.DataFrame({
'user_id': [1, 2, 3, 4],
'name': ['アリス', 'ボブ', 'チャーリー', 'キャロル']
})
orders = pd.DataFrame({
'order_id': ['O001', 'O002', 'O003'],
'user_id': [1, 2, 2],
'amount': [120, 85, 200]
})
# 内部結合(デフォルト)— 一致するuser_idのみ
result = pd.merge(users, orders, on='user_id')
print(result)
# user_id name order_id amount
# 0 1 アリス O001 120
# 1 2 ボブ O002 85
# 2 2 ボブ O003 200
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
3. 4つの結合タイプ
(1) Mermaid図
graph TB
subgraph Inner["inner — 一致する行のみ残す"]
I1["user_id 1: アリス+O001"]
I2["user_id 2: ボブ+O002"]
I3["user_id 2: ボブ+O003"]
end
subgraph Left["left — 左テーブルの全行を残す"]
L1["user_id 1: アリス+O001"]
L2["user_id 2: ボブ+O002"]
L3["user_id 2: ボブ+O003"]
L4["user_id 3: チャーリー+NaN"]
L5["user_id 4: キャロル+NaN"]
end
subgraph Right["right — 右テーブルの全行を残す"]
R1["user_id 1: アリス+O001"]
R2["user_id 2: ボブ+O002"]
R3["user_id 2: ボブ+O003"]
end
subgraph Outer["outer — 両側の全行を残す"]
O1["user_id 1: アリス+O001"]
O2["user_id 2: ボブ+O002"]
O3["user_id 2: ボブ+O003"]
O4["user_id 3: チャーリー+NaN"]
O5["user_id 4: キャロル+NaN"]
end
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
(2) 4つの結合タイプの比較
| タイプ | how | 左テーブルで不一致 | 右テーブルで不一致 | 行数 |
|---|---|---|---|---|
| inner | 'inner' | 削除 | 削除 | ≤min(left, right) |
| left | 'left' | 保持(NaNで埋める) | 削除 | = 左行数 × 一致数 |
| right | 'right' | 削除 | 保持(NaNで埋める) | = 右行数 × 一致数 |
| outer | 'outer' | 保持(NaNで埋める) | 保持(NaNで埋める) | ≥max(left, right) |
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
: 4つの結合タイプの比較(難易度 ⭐⭐)
PYTHON
import pandas as pd
left = pd.DataFrame({
'id': [1, 2, 3],
'name': ['アリス', 'ボブ', 'チャーリー']
})
right = pd.DataFrame({
'id': [2, 3, 4],
'score': [85, 92, 78]
})
# 内部結合 — id 2、3のみ(両テーブルに存在)
print("内部結合(INNER):")
print(pd.merge(left, right, on='id', how='inner'))
# id name score
# 0 2 ボブ 85
# 1 3 チャーリー 92
# 左結合 — 左テーブルの全行、右は不一致のところをNaNで埋める
print("\n左結合(LEFT):")
print(pd.merge(left, right, on='id', how='left'))
# id name score
# 0 1 アリス NaN ← 右テーブルに一致なし
# 1 2 ボブ 85.0
# 2 3 チャーリー 92.0
# 右結合 — 右テーブルの全行
print("\n右結合(RIGHT):")
print(pd.merge(left, right, on='id', how='right'))
# id name score
# 0 2 ボブ 85
# 1 3 チャーリー 92
# 2 4 NaN 78 ← 左テーブルに一致なし
# 外部結合 — 両側の全行
print("\n外部結合(OUTER):")
print(pd.merge(left, right, on='id', how='outer'))
# id name score
# 0 1 アリス NaN
# 1 2 ボブ 85.0
# 2 3 チャーリー 92.0
# 3 4 NaN 78.0
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
4. 複数キー結合と異なる列名
(1) 複数キー結合
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
: 複数キー結合(難易度 ⭐⭐)
PYTHON
import pandas as pd
sales = pd.DataFrame({
'region': ['北', '北', '南', '南'],
'category': ['家電', '衣料品', '家電', '衣料品'],
'revenue': [5000, 800, 2000, 600]
})
targets = pd.DataFrame({
'region': ['北', '北', '南', '南'],
'category': ['家電', '衣料品', '家電', '衣料品'],
'target': [4500, 1000, 2500, 500]
})
# 複数のキーで結合
result = pd.merge(sales, targets, on=['region', 'category'])
print(result)
# region category revenue target
# 0 北 家電 5000 4500
# 1 北 衣料品 800 1000
# 2 南 家電 2000 2500
# 3 南 衣料品 600 500
# 達成率を計算
result['achievement'] = (result['revenue'] / result['target'] * 100).round(1)
print(result[['region', 'category', 'achievement']])
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
(2) 異なる列名での結合
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
: left_on/right_on(難易度 ⭐⭐)
PYTHON
import pandas as pd
employees = pd.DataFrame({
'emp_id': [1, 2, 3],
'name': ['アリス', 'ボブ', 'チャーリー']
})
salaries = pd.DataFrame({
'employee_id': [1, 2, 3],
'salary': [75000, 92000, 68000]
})
# 列名が異なる:emp_id と employee_id
result = pd.merge(
employees, salaries,
left_on='emp_id', right_on='employee_id',
how='inner'
)
print(result)
# emp_id name employee_id salary
# 0 1 アリス 1 75000
# 1 2 ボブ 2 92000
# 2 3 チャーリー 3 68000
# 冗長な列を削除
result = result.drop(columns='employee_id')
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
5. suffixesとindicator
(1) 列名の競合を処理する
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
: 重複する列名に対するsuffixes(難易度 ⭐)
PYTHON
import pandas as pd
df1 = pd.DataFrame({
'id': [1, 2],
'value': [100, 200]
})
df2 = pd.DataFrame({
'id': [1, 2],
'value': [300, 400]
})
# デフォルトのsuffixes:_x と _y
result = pd.merge(df1, df2, on='id')
print(result)
# id value_x value_y
# 0 1 100 300
# 1 2 200 400
# カスタムsuffixes
result2 = pd.merge(df1, df2, on='id', suffixes=('_left', '_right'))
print(result2)
# id value_left value_right
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
(2) マージ元を診断するindicator
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
: 行の出所を追跡するindicator(難易度 ⭐⭐)
PYTHON
import pandas as pd
left = pd.DataFrame({'id': [1, 2, 3], 'name': ['アリス', 'ボブ', 'チャーリー']})
right = pd.DataFrame({'id': [2, 3, 4], 'score': [85, 92, 78]})
# _merge列を追加して各行の出所を確認する
result = pd.merge(left, right, on='id', how='outer', indicator=True)
print(result)
# id name score _merge
# 0 1 アリス NaN left_only
# 1 2 ボブ 85.0 both
# 2 3 チャーリー 92.0 both
# 3 4 NaN 78.0 right_only
# マージ元でフィルタリング
only_left = result[result['_merge'] == 'left_only']
print(f"左テーブルのみに存在:{len(only_left)} 行") # 1(アリス)
# データの整合性を検証 — 一致しないレコードを見つける
unmatched = result[result['_merge'] != 'both']
print(f"一致しないレコード:{len(unmatched)}") # 2
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
6. validateとクロス結合
(1) 結合関係を検証するvalidate
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
: validateによる検証(難易度 ⭐⭐)
PYTHON
import pandas as pd
users = pd.DataFrame({'user_id': [1, 2, 3], 'name': ['アリス', 'ボブ', 'チャーリー']})
orders = pd.DataFrame({'order_id': ['O1', 'O2', 'O3'], 'user_id': [1, 2, 2]})
# 検証:各ユーザーの注文は最大1件であるべき(1:1)
# ユーザー2は2件の注文があるため、これは失敗します
try:
result = pd.merge(users, orders, on='user_id', validate='1:1')
except Exception as e:
print(f"検証に失敗しました:{e}")
# 検証:各ユーザーは複数の注文を持てる(1:m)— 成功
result = pd.merge(users, orders, on='user_id', validate='1:m')
print(result) # OK
# 検証タイプ:'1:1'、'1:m'、'm:1'、'm:m'
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
(2) クロス結合
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
: クロス結合(難易度 ⭐)
PYTHON
import pandas as pd
colors = pd.DataFrame({'color': ['赤', '青']})
sizes = pd.DataFrame({'size': ['S', 'M', 'L']})
# すべての組み合わせ(直積)
result = pd.merge(colors, sizes, how='cross')
print(result)
# color size
# 0 赤 S
# 1 赤 M
# 2 赤 L
# 3 青 S
# 4 青 M
# 5 青 L
# 6行 = 2色 × 3サイズ
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
7. mergeとjoinの違い
| 機能 | merge | join |
|---|---|---|
| 呼び出し形式 | pd.merge(df1, df2) |
df1.join(df2) |
| デフォルトの結合キー | onで指定した列 | インデックス |
| 複数列の結合 | ✅ on=['a','b'] | ❌ インデックスのみ |
| 結合タイプ | inner/left/right/outer | left(デフォルト) |
| 柔軟性 | 高い | 低い |
| 最適な用途 | 汎用的な結合 | インデックスの位置合わせ |
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
: joinのショートカット(難易度 ⭐)
PYTHON
import pandas as pd
df1 = pd.DataFrame({'A': [1, 2, 3]}, index=['x', 'y', 'z'])
df2 = pd.DataFrame({'B': [4, 5]}, index=['x', 'y'])
# joinはデフォルトでインデックスを使用
result = df1.join(df2) # how='left'がデフォルト
print(result)
# A B
# x 1 4.0
# y 2 5.0
# z 3 NaN ← 左結合は左テーブルの全行を保持
# howパラメータを指定したjoin
result2 = df1.join(df2, how='inner')
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
8. 総合例:3テーブル結合分析
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
: 3テーブルマージのワークフロー(難易度 ⭐⭐⭐)
PYTHON
import pandas as pd
# ============================================
# 総合例:3テーブルのマージ
# users + orders + products → 完全な分析
# ============================================
# 1. ユーザーテーブル
users = pd.DataFrame({
'user_id': [1, 2, 3, 4, 5],
'name': ['アリス', 'ボブ', 'チャーリー', 'キャロル', 'デビッド'],
'region': ['北', '南', '東', '北', '西']
})
# 2. 注文テーブル
orders = pd.DataFrame({
'order_id': ['O001', 'O002', 'O003', 'O004', 'O005', 'O006'],
'user_id': [1, 2, 2, 3, 1, 5],
'product_id': ['P01', 'P02', 'P03', 'P01', 'P04', 'P02'],
'quantity': [2, 1, 3, 1, 5, 2]
})
# 3. 商品テーブル
products = pd.DataFrame({
'product_id': ['P01', 'P02', 'P03', 'P04', 'P05'],
'product_name': ['ノートPC', 'スマホ', 'タブレット', 'マウス', 'キーボード'],
'price': [999, 699, 349, 29, 79]
})
# ステップ1:orders + products → 価格付きの注文明細
order_details = pd.merge(orders, products, on='product_id', how='left')
order_details['total'] = order_details['quantity'] * order_details['price']
# ステップ2:order_details + users → 全体像
full = pd.merge(order_details, users, on='user_id', how='left')
# ステップ3:地域別に分析
region_sales = full.groupby('region')['total'].sum().sort_values(ascending=False)
print("=== 地域別売上 ===")
print(region_sales)
# ステップ4:上位顧客
top_customers = full.groupby('name')['total'].sum().sort_values(ascending=False)
print("\n=== 上位顧客 ===")
print(top_customers)
# ステップ5:一致しない商品(productsにはあるが一度も注文されていない)
unmatched = pd.merge(
products, orders[['product_id']].drop_duplicates(),
on='product_id', how='left', indicator=True
)
never_ordered = unmatched[unmatched['_merge'] == 'left_only']
print(f"\n=== 一度も注文されていない商品 ===")
print(never_ordered[['product_id', 'product_name']])
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して、一緒に進めてください。実際の値はpandasのバージョンにより若干異なる場合があります。
❓ よくある質問
Q innerとleftはどちらを使うべきですか?
A デフォルトはinner(一致する行のみを保持し、クリーンなデータになる)にします。左テーブルの全行が必要な場合(例:「注文の有無にかかわらず全ユーザー」)はleftを使います。rightはほとんど使いません(テーブルを入れ替えてleftを使えば済みます)。outerは差分を見つける場合に使います(「注文のないユーザー+ユーザーのない注文」)。日常的な場面では80%の場合、innerが正しい選択です。
Q テーブル間でキーの列名が異なる場合は?
A left_onとright_onを使って、各テーブルのキー列を個別に指定します。例えば
pd.merge(a, b, left_on='emp_id', right_on='employee_id') のようにします。マージ後は両方の列が残るので、冗長な方はdropで削除します。片方のキーがインデックスの場合は、left_index=True / right_index=Trueを使います。Q suffixesの競合はどう処理しますか?
A 両テーブルに同名の列(キー以外)がある場合、mergeは自動的に_xと_yのsuffixを付与します。suffixes=('_left','_right')でカスタマイズできます。ベストプラクティスは、マージ前に列名を変更して曖昧さを避けることです:
df.rename(columns={'value': 'value_a'})。Q mergeとjoinの違いは何ですか?
A mergeはより多用途で、列ベースの結合、複数キー結合、4種類すべての結合タイプをサポートします。joinはより簡潔で、デフォルトでインデックスに基づいて結合し、デフォルトは左結合です。複数列の結合が必要ならmergeを、単純なインデックスの位置合わせならjoinを使います。一般的には、完全性のためmergeを優先し、joinはショートカットとして扱います。
Q マージ結果が正しいことをどう検証しますか?
A 3段階でチェックします。①マージ前後の行数を比較(innerは≤min、leftは左行数×一致数になるはず)。②indicator=Trueで各行の出所を確認(both/left_only/right_only)。③validate='1:1'や'1:m'で期待する関係を表明し、違反すればエラーが発生します。
Q mergeで行数が爆発的に増えたら?
A 行数の爆発はm:m(多対多)マージを示しています。右テーブルの各キーがN行に一致すると、結果は左行数×Nになります。修正方法:マージ前に右テーブルを重複排除または集計(groupby + agg)して、一致する行を減らします。validate='m:1'を使えば早期に検出できます。
Q クロス結合はいつ使うべきですか?
A クロス結合は直積(全行×全行)を生成し、行数=左行数×右行数になります。組み合わせの生成(例:色×サイズ=全SKUバリエーション)にのみ使います。大きなテーブルにクロス結合は絶対に使わないでください。1000×1000=100万行になります!
📖 まとめ
- mergeには4つの結合タイプがあります:inner(一致のみ)/left(左テーブルの全行を保持)/right(右テーブルの全行を保持)/outer(両側のすべてを保持)
- 複数キー結合はon=['key1','key2']、異なる列名にはleft_on/right_onを使います
- suffixesは重複する列名を処理し、indicatorは行の出所を追跡します
- validateは結合関係(1:1/1:m/m:1)を検証し、予期せぬ結果を防ぎます
- クロス結合は直積を生成します。小さなテーブルにのみ使います
- joinはmergeのショートカットです(デフォルトはインデックス+左結合)
- マージ後は必ず行数とindicatorを確認し、正しさを検証しましょう
📝 練習問題
- 基礎(難易度 ⭐):studentsテーブルとgradesテーブルを作成し(student_idを共有)、inner/left/outerマージを実行して、行数がどう変化するか観察してください。
- 応用(難易度 ⭐⭐):departmentsテーブル(dept_id/dept_name)とemployeesテーブル(emp_id/dept_id/salary)を作成して結合し、部門ごとの平均給与を計算し、indicatorを使って従業員がいない部門を見つけてください。
- 挑戦(難易度 ⭐⭐⭐):3つのテーブル(users/orders/products)をシミュレートし、以下を完成させてください:innerマージで結合 → ユーザーごとの総支出を計算 → leftマージで支出ゼロのユーザーを抽出 → validateで関係を検証 → indicatorで出所を診断。



