重複行と表記ゆれを見つける:pandasで名寄せ前に点検する手順
AIを使いこなすClaudeで執筆し、GPTで自動校閲・修正しています。

結論から言うと、名寄せ前の点検は「①何をもって同じとみなすかのキーを決める」「②キー用の列を正規化して作る」「③元の表記を残したまま、同じキーに複数の書き方がぶら下がっていないかを一覧にする」の3段階で足ります。重複行は duplicated で、表記ゆれは正規化キーごとに元の表記を数える groupby で洗い出せます。ここでは行を消さず、確認用の一覧を作るところまでを扱います。 集計結果が割れる典型は、同じ顧客が「株式会社ひまわり商事」と「(株)ひまわり商事」に分かれて2行として数えられる場合です。目視では同じに見えても、全角スペースや末尾の空白、大文字小文字の違いで、pandasは別の値として扱います。
重複判定のキーを選ぶ
DataFrame.duplicated は、重複している行を示す真偽値のSeriesを返します。subset で判定に使う列を絞れ、省略すると全列が対象です。keep は、'first'(最初を重複扱いにしない)、'last'(最後を重複扱いにしない)、False(重複するすべての行をTrueにする)から選びます(pandas公式:DataFrame.duplicated)。
点検では keep=False を使うと、重複グループの全行が見えて比較しやすくなります。'first' では2行目以降しか拾えず、どの行と重なっているか分かりません。
キーの選び方は、データの性質で変わります。下の表は編集上の目安です。
- 発行済みの一意なIDがある、キーの候補:ID列、注意点:IDが違っても同一人物の可能性は残るため、IDの重複とは別に確認します
- IDがなく、メールアドレスがある、キーの候補:正規化したメール、注意点:家族や部署で共用する場合は別人でも一致します
- 名称と住所・電話しかない、キーの候補:正規化した名称 + 電話または住所、注意点:名称だけだと同名の別人・別法人を同一視します
- 取引明細などの行データ、キーの候補:日付 + 取引先 + 金額 + 伝票番号、注意点:伝票番号なしだと、同日に同額を2回買った正当な2行を重複と誤認します
「全列が同じ行」だけでは、登録日時や連番が異なる候補を見つけられません。同一性の判断に不要な管理用の列を外して、subset を指定します。一意な顧客IDなど、点検の目的に合う列はキーとして使います。
説明用のデータで点検してみる
以下は説明用の架空の例です。氏名やメールは実在しないダミーで、実際のデータの結果ではありません。AIにコードを相談するときも、本物の顧客データは貼らず、このように置き換えたものを渡してください。
以下のコードは、Python 3.14.3 / pandas 2.3.3で、説明用ダミーデータの出力と元の6行の保持を実行確認済みです(2026年10月30日)。この例の name と email は欠損のない文字列です。実データの名寄せ精度を確認したものではないため、手元のpandasでもダミーデータから実行して結果を確かめてください。
import pandas as pd
# 説明用の例(架空のデータ)
df = pd.DataFrame({
"customer_id": ["C001", "C002", "C003", "C004", "C005", "C006"],
"name": ["株式会社ひまわり商事", "(株)ひまわり商事", "ひまわり商事 株式会社 ",
"山田 太郎", "山田\u3000太郎", "佐藤 花子"],
"email": ["info@himawari.example", "INFO@himawari.example",
"info@himawari.example ", "taro@example.test",
"taro@example.test", "hanako@example.test"],
"amount": [1000, 1000, 500, 300, 300, 700],
})
df.loc[1, "name"] = "\uff08株\uff09ひまわり商事" # 全角かっこの「(株)」
# 1) 元の値のまま判定:表記ゆれがあると見つからない
raw = ["name", "email", "amount"]
print(df.duplicated(subset=raw, keep=False).sum()) # 想定: 0
# 2) 正規化キーを別列で作る(元の列は変更しない)
def norm(s):
return s.str.normalize("NFKC").str.lower().str.strip()
df["email_key"] = norm(df["email"])
df["name_key"] = norm(df["name"]).str.replace(
r"株式会社|\(株\)|\s+", "", regex=True)
# 3) 正規化キーで重複グループを抜き出す
key = ["name_key", "email_key"]
df["is_dup"] = df.duplicated(subset=key, keep=False)
review = df[df["is_dup"]].sort_values(key)
print(review[["customer_id", "name", "email", "amount"] + key])
# 4) 表記ゆれ一覧:同じキーに複数の元表記がある組
def variants(df, col, keycol):
g = df.groupby(keycol)[col]
out = g.agg(rows="size", n_variants="nunique",
variants=lambda s: " | ".join(repr(v) for v in sorted(s.unique())))
return out[out["n_variants"] > 1].reset_index()
print(variants(df, "name", "name_key"))
print(variants(df, "email", "email_key"))
# 5) 同じ名称キーに別メールがないか(同名別人の確認)
n_mail = df.groupby("name_key")["email_key"].nunique()
print(n_mail[n_mail > 1]) # 想定: 空この環境で確認した出力は次のとおりです。
- 手順1は、氏名の書き方やメールの空白が違うため、重複0件でした。「重複なし」と見えてしまう落とし穴です。
- 手順3は、C001〜C003(ひまわり商事)とC004・C005(山田太郎)が拾われ、C006は確認用の一覧に含まれません。元の df から行は削除していません。
- 手順4の名称一覧は、ひまわり商事 に3種類、山田太郎 に2種類の元表記が並びました。
手順3は顧客の同一性を確認する候補の抽出です。金額が異なるC003も含まれるため、この一覧をそのまま重複明細として削除してはいけません。
正規化キーの作り方と、やりすぎの境界
Series.str.normalize は、'NFC'、'NFKC'、'NFD'、'NFKD' のUnicode正規形を指定して文字列を変換します(pandas公式:Series.str.normalize)。この例では全角かっこや全角スペースの違いをそろえるために 'NFKC' を使っています。NFKCは互換分解後に正準合成する方式で、ローマ数字の「Ⅰ」と英字の「I」のような元の区別も失われます(Python公式:unicodedata.normalize)。正規化キーの一致は、人や企業の同一性を保証しません。キーにだけ使い、元の列は残してください。
どの変換が誤判定(別物の同一視)につながるかは、データの識別ルールによって変わります。次の表で適用前に確認してください。
- strip、NFKC、lower、適用前の確認事項:前後の空白や互換文字、大文字小文字を識別に使っていないか。メール全体の小文字化も一律には採用しません
- 空白の全削除、記号(ハイフンなど)の統一、適用前の確認事項:単語の区切りや記号が消えて、別の名称と一致しないか
- 「株式会社」など法人格の削除、表記辞書による置換、適用前の確認事項:正式名称の違いまで消さないか。複数の法人格を削除するルールでは「株式会社A」と「有限会社A」も同じキーになります
上のコードは「株式会社」と「(株)」まで削る設定で、「有限会社」は削除しません。法人格を削るのは、一覧を広めに拾う点検用だからです。採用するかは次の節で人が判断します。点検の初回は必要な処理を一つずつ追加して、グループ数と一致した行がどう変わるかを見ると、どの処理が効いているか分かります。
正規化後に空文字になった行にも注意してください。空文字同士の一致は、同一人物・同一法人の根拠にはなりません。実データでは、使うキーの全列について欠損と空文字の両方を除外した点検用データを作り、そのデータで手順3〜5を行います。key = ["name_key", "email_key"] の場合、df.loc[df[key].notna().all(axis=1) & df[key].ne("").all(axis=1)] で両方のキーが非欠損かつ空文字でない行を取り出せます。除外した行も別途確認し、件数を記録してください。欠損値(NaN)は、groupby の既定ではグループから除外されます。dropna=False を指定すると、NaNも一つのグループとして扱えますが、それも同一性を意味しません(pandas公式:DataFrame.groupby)。
表記ゆれ一覧を読んで判断する
variants の出力で、repr を使っているのは、末尾の空白や全角スペースを見分けるためです。通常の表示では、'ひまわり商事 株式会社 ' の末尾の空白は見えません。
一覧は、行を消す前の確認資料です。次の表で、グループごとに人が判断します。
- 空白・大文字小文字・全角半角だけの違い、判断:同一かは未確定、次の操作:IDや住所などで同一性を確認し、代表表記と変換表を決める
- 法人格の有無・位置の違い(株式会社/(株))、判断:同一かは未確定、次の操作:住所や電話など、別の列で裏取りする
- 同じ名称キーでメールが違う、判断:同名別人、または複数の連絡先、次の操作:自動で統合しない
- 金額・日付も一致する完全な重複明細、判断:二重登録の可能性、次の操作:元システムの登録履歴を確認する
確認が済んだグループは、review をCSVに書き出して、判断の列(統合する/しない/保留)を追記する運用が扱いやすいです。変換表(元表記→代表表記)を別ファイルにしておけば、同じデータを再取り込みしたときも、同じ基準で名寄せできます。
集計前の最終チェック
重複の削除や統合は、点検が終わってから行います。そのときの確認事項は次の3点です。
- 件数を記録する:削除前後の行数と、重複グループ数。
- 合計値で突き合わせる:金額列の合計が、意図しない形で変わっていないか。明細を削除するなら、削除前後の合計の差が、削除対象の金額の合計と一致するか確認します。ゼロや負の金額を含む場合、合計が減るとは限りません。
- 元データを残す:正規化は別列に作ったので、元の列はそのままです。統合後のファイルとは別に、元ファイルも保存しておきます。
AIに作業を手伝わせる場合、任せられるのはコードの下書き、正規化ルールの案出し、一覧の見落としの指摘までです。「このグループは同一人物か」という最終判断は、業務の事情を知る人が行います。依頼文の例は次のとおりです。
次の列を持つpandasのDataFrameで、名寄せ前の点検コードを書いてください。列は customer_id、name、email、amount です。データは別途渡す説明用のダミー(5〜10行)で、本物の個人情報ではありません。・重複判定は name と email を正規化したキーで行い、元の列は変更しないでください。・重複グループ全行の一覧と、同じキーに複数の元表記がある一覧を出してください。・行の削除はしないでください。・正規化ルールごとに、誤判定が起きうる例を挙げてください。
返ってきたコードは、必ず自分の環境で、ダミーデータから実行してください。想定と異なる結果が出たら、その出力を添えて質問し直すと原因を絞れます。本物のデータに適用するのは、そのあとです。

