株式分析では「価格データ」「銘柄マスタ」「財務データ」「指標データ」など、別々のテーブルに分かれて配信されるデータを キーで結合 する場面が頻繁にあります。pandas には merge / concat / join の 3 種類の結合手段があり、用途で使い分けます。

本記事では、3 つの結合関数の違いと、株価データを題材にした実用的な結合パターンを示します。

目次

  1. インストール
  2. 3 つの結合関数の違い
  3. サンプルデータの準備
  4. merge の基本(内部結合)
  5. 4 種類の結合方式
  6. キー名が違う場合
  7. merge_asof — 最近傍の日付で結合
  8. concat — 縦方向の連結
  9. join — インデックスベースの結合
  10. ワイド ↔ ロングの変換
  11. 結合でよくあるトラブル

インストール

Terminal window
pip install pandas

検証バージョン: Python 3.12.5 / pandas 2.2.3

3 つの結合関数の違い

まず全体像を表で押さえます。

関数主な用途結合キー
pd.merge(a, b, on=...)SQL の JOIN 相当。列名で結合列(on / left_on / right_on)
pd.concat([a, b])縦方向 / 横方向の単純な連結キーなし(位置で連結)
df.join(other)インデックスをキーにした結合インデックス

実務では merge が中心です。concat は「銘柄ごとの DataFrame を縦に積む」「期間ごとの DataFrame を時間軸で連結する」場面で使います。join は両者がインデックスを揃えて持っているときに簡潔に書けます。

サンプルデータの準備

価格テーブルと銘柄マスタを別々に用意します。

import pandas as pd
prices = pd.DataFrame({
"Date": pd.to_datetime([
"2026-04-01", "2026-04-01", "2026-04-01",
"2026-04-02", "2026-04-02", "2026-04-02",
]),
"Code": [7203, 9984, 8306, 7203, 9984, 8306],
"C": [2900, 9800, 1450, 2925, 9750, 1448],
})
masters = pd.DataFrame({
"Code": [7203, 9984, 8306, 6758],
"CoName": ["トヨタ", "ソフトバンクG", "三菱UFJ", "ソニーG"],
"S33Nm": ["輸送用機器", "情報・通信業", "銀行業", "電気機器"],
})

merge の基本(内部結合)

両方のテーブルにキーが存在する行だけを残すのが 内部結合(inner join) です。merge のデフォルトはこれです。

joined = pd.merge(prices, masters, on="Code")
print(joined.head())
Date Code C CoName S33Nm
0 2026-04-01 7203 2900 トヨタ 輸送用機器
1 2026-04-01 9984 9800 ソフトバンクG 情報・通信業
2 2026-04-01 8306 1450 三菱UFJ 銀行業
3 2026-04-02 7203 2925 トヨタ 輸送用機器
...

prices には 6758(ソニーG)が無いため、masters 側にあっても結果には現れません。

4 種類の結合方式

how で結合方式を切り替えます。

how残る行
"inner"両方にキーが存在する行のみ(既定)
"left"左側の全行(右側に無いキーは NaN)
"right"右側の全行(左側に無いキーは NaN)
"outer"片方にだけ存在する行も含む
left = pd.merge(prices, masters, on="Code", how="left")
outer = pd.merge(prices, masters, on="Code", how="outer")
print(left)
print(outer)

how="left" は「価格データを軸に、対応するマスタ情報を引っ張ってくる」用途で多用します。マスタ側に該当銘柄が無ければ NaN になり、後段の処理で気付けます。

キー名が違う場合

両側でキーの列名が異なるときは left_on / right_on を使います。たとえば自分で用意した銘柄リストの列名が ticker、株価データ側が Code のように食い違う場合です。

watchlist = pd.DataFrame({"ticker": [7203, 6758], "memo": ["主力", "監視"]})
joined = pd.merge(prices, watchlist, left_on="Code", right_on="ticker")

結合後は Codeticker の両方が残ります。不要なら .drop(columns="ticker") で落とします。

merge_asof — 最近傍の日付で結合

「価格データの日付に対して、その日以前で最も近い財務発表日のデータを引く」という時系列特有の結合は merge_asof で書けます。

ratings = pd.DataFrame({
"Date": pd.to_datetime(["2026-03-15", "2026-04-01"]),
"Code": [7203, 7203],
"rating": ["BUY", "HOLD"],
})
prices_sorted = prices.sort_values(["Code", "Date"])
ratings_sorted = ratings.sort_values(["Code", "Date"])
merged = pd.merge_asof(
prices_sorted,
ratings_sorted,
on="Date",
by="Code",
direction="backward",
)
print(merged)

direction="backward" は「価格日付以前で最も近いキー」を選びます。先読みバイアスを防ぐ用途で重要です。両入力とも結合キーで ソート済み が必須です。

concat — 縦方向の連結

複数銘柄の DataFrame を縦に積むには concat が使えます。

df_7203 = prices[prices["Code"] == 7203]
df_9984 = prices[prices["Code"] == 9984]
stacked = pd.concat([df_7203, df_9984], ignore_index=True)
print(stacked)

ignore_index=True を付けないと、元の行番号がそのまま残るため、結合後の行番号が重複します。

横方向の連結は axis=1 です。インデックスが一致していることが前提になります。

horizontal = pd.concat([df_7203.reset_index(drop=True), df_9984.reset_index(drop=True)], axis=1)

join — インデックスベースの結合

両方の DataFrame が同じ意味のインデックスを持っているとき、join が簡潔です。

prices_indexed = prices.set_index("Code")
masters_indexed = masters.set_index("Code")
joined = prices_indexed.join(masters_indexed, how="left")
print(joined.head())

価格データを (Code, Date) のマルチインデックスにしておけば、銘柄マスタとの突合が join 一発で書けます。

ワイド ↔ ロングの変換

結合とセットで覚えたいのが、データの形を変える meltpivot です。

ワイド(列に銘柄が並ぶ)→ ロング(銘柄列を持つ縦長)への変換。

wide = pd.DataFrame({
"Date": pd.to_datetime(["2026-04-01", "2026-04-02"]),
"7203": [2900, 2925],
"9984": [9800, 9750],
})
long = wide.melt(id_vars="Date", var_name="Code", value_name="C")
print(long)

ロング → ワイドの変換は pivot または pivot_table(#4-3「pandas で集計・グループ化」)で行います。

結合でよくあるトラブル

実務でハマりやすい点を 3 つ挙げます。

  • キーの型不一致: 片方が int64、もう片方が object(文字列)だと結合されません。astype で揃えます。
  • 重複キーによる行数の爆発: 両側に同じキーが複数行ある場合、デカルト積で行数が掛け算されます。merge(..., validate="one_to_many") で検査できます。
  • 欠損行の見落とし: how="left" で結合した結果、右側列に NaN がどれだけあるかを result.isna().sum() で必ず確認します。
# 検査オプションの例
pd.merge(prices, masters, on="Code", validate="many_to_one")

生成AI へのプロンプト例

結合は意図を間違えると黙って壊れるため、要件を明確に書きます。

pandas で次の結合を行うコードを書いてください。
入力:
- prices(列: Date, Code, C)
- statements(列: DiscDate, Code, EPS, bps)
要件:
- 各価格行に対して、DiscDate <= Date を満たす最新の財務行を結合する
(発表前のデータを使わないこと)
- statements の銘柄コードは Code、prices は Code(結合時に対応づける)
- 価格 / EPS から PER、価格 / BPS から PBR を計算する
- Date は datetime64、銘柄コードは同じ型に揃える
- 検証として、結合後に NaN が発生した行の件数を集計して print する
- pandas 2.2 系で merge_asof を使う

「先読み禁止」のような制約を明示するのが、時系列結合で間違いを減らすコツです。

まとめ

  • merge は SQL の JOIN 相当、concat は単純連結、join はインデックス結合
  • 結合方式(how)は inner / left / right / outer の 4 種
  • merge_asof は最近傍日付の結合に使い、先読み防止に有用
  • 結合後は NaN の発生件数と行数 を必ず確認する
  • ワイド ↔ ロング変換は melt / pivot