merge後に行数が増える場合は、結合キーの重複を疑う。左の同じキーがm行、右がn行なら、そのキーだけでm×n行になり得る。台帳側は1キー1行という前提ならvalidate=”many_to_one”で検査し、indicatorで未対応を調べる。欠測キー同士が一致するpandasの挙動にも注意したい。
最小例で確かめる
以下は説明用に作った小さなデータである。実測データや実行速度の測定結果ではない。コード全体をexample.pyとして保存すれば、入力ファイルを別途用意せずに実行できる。assertは、この例で成り立つべき形や値を確認するために入れてある。
import pandas as pd
left = pd.DataFrame({"id": ["A", "A", "C", None],
"value": [1, 2, 3, 4]})
bad_master = pd.DataFrame({"id": ["A", "A", "B", None],
"label": ["alpha", "alternate", "beta", "unknown"]})
bad = left.merge(bad_master, on="id", how="left")
print("unchecked rows:", len(bad))
print("null matched:", bad.loc[bad["id"].isna(), "label"].tolist())
try:
left.merge(bad_master, on="id", how="left", validate="many_to_one")
except pd.errors.MergeError:
print("expected: MergeError")
else:
raise AssertionError("duplicate master key was accepted")
# 例では内容を確認済みの台帳を別に作る。機械的な重複削除ではない。
master = pd.DataFrame({"id": ["A", "B"], "label": ["alpha", "beta"]})
missing_key = left[left["id"].isna()].copy()
joined = left[left["id"].notna()].merge(
master, on="id", how="left", validate="many_to_one", indicator=True)
print(joined.to_string(index=False))
print("missing key rows:", len(missing_key))
print("unmatched IDs:", joined.loc[joined["_merge"] == "left_only", "id"].tolist())
assert len(bad) == 6 and len(joined) == 3
assert len(joined) + len(missing_key) == len(left)
assert joined.loc[joined["id"] == "A", "label"].tolist() == ["alpha", "alpha"]
assert joined.loc[joined["id"] == "C", "label"].isna().all()
実行結果
unchecked rows: 6
null matched: ['unknown']
expected: MergeError
id value label _merge
A 1 alpha both
A 2 alpha both
C 3 NaN left_only
missing key rows: 1
unmatched IDs: ['C']
行数の増加は組合せで考える
leftにはAが2行、bad_masterにもAが2行ある。そのためAについて4行が作られ、Cと欠測キーの各1行を加えて結果は6行になる。pandasが同じレコードを勝手に複製したというより、結合条件に合うすべての組合せが存在しているのである。
測定表に同じ試料の繰り返しがあること自体は正常でも、試料台帳の説明が1件であるべきなら台帳側の重複は問題になる。validate=”many_to_one”は右側キーの一意性を検査する。両側1行ずつを想定するならone_to_one、逆向きならone_to_manyというように、表の粒度から契約を選ぶ。
validateとindicatorの役割を分ける
validateはキーの重複条件を調べるもので、すべてのキーに対応先があることまでは保証しない。左結合でindicator=Trueを指定すれば、_merge列にbothやleft_onlyなどの区分が入る。例ではCがleft_onlyとなり、台帳に存在しないことを確認できる。
inner結合にすると未対応のCが結果から消えるので、行数減少を見落としやすい。元の測定を残して点検したい場面では、まずleft結合とindicatorで対応を確認するとよい。右側だけに存在する台帳の項目も調べたいなら、outer結合など別の監査方法を使う。
欠測キーは事前に分ける
pandasのmergeでは、両側のキーが欠測の行同士も対応する。例のuncheckedな結合では、ID不明の測定にunknownという台帳情報が付いた。これは一般的なSQLのNULLの結合とは異なるため、SQLに慣れていると特に見落としやすい。
試料IDが不明なら対応付けてはいけない、という方針に従い、修正版ではmissing_keyとして別に保存した。残りを結合し、結合後の行数と欠測キー行数の合計が元の件数になることを検査している。欠測行を無言で削除せず、なぜ対応付けられなかったかを区別して報告できる。
台帳を直す前に意味を確認する
例では正式な台帳を新しく小さな表として定義した。実務で同じ問題が起きても、drop_duplicatesで先頭を残せば解決するとは限らない。台帳に有効期間や版があるなら、試料IDだけでなく日時条件を含む対応が必要かもしれない。まず1行が何を意味するか確認する。
結合前にはキーのdtype、前後の空白、先頭ゼロ、大文字小文字などの規則もそろえる。結合後は行数だけでなく、キーごとの件数と代表レコードの内容を確認したい。件数が偶然同じでも、誤った台帳情報が付くことはあり得る。自動的な型変換や正規化に頼らず、対応の根拠を残そう。
動作確認環境と参考資料
Linux・CPython 3.12.14、NumPy 2.3.5、pandas 2.2.3、SciPy 1.17.0、Matplotlib 3.10.8の環境で掲載コードを実行した。使用するライブラリはコード冒頭のimportを参照してほしい。公式資料の最新版と、この実行確認版は区別している。数値の末尾や表の表示幅は環境によって変わることがある。
