pandasで売上データを整理していると、同じ注文IDが複数行に現れることがあります。しかし、値が重複しているからといって、すぐ削除してよいとは限りません。完全な二重登録だけでなく、金額を訂正した更新後の記録が混ざっている可能性もあるためです。
drop_duplicates()は、重複の正誤を自動判定するメソッドではありません。先にduplicated(keep=False)で候補を確認し、subsetで同一レコードの基準、keepで残す記録を決めてから削除するのが基本です。
この記事では、注文データを使って、重複候補の確認から削除、削除後の検算までをGoogle Colabで順番に実行します。最後には、自分のデータで「なぜこの行を残したか」を説明できる状態を目指します。
この記事でわかること
duplicated()とdrop_duplicates()の役割の違いsubsetで重複を判定する列を選ぶ方法keep='first'・keep='last'・keep=Falseの選び方- 更新日時を確認して最新の記録を残す方法
- 削除前後の行数・残った値・重複件数を検算する方法
- 重複が消えない、消えすぎる、元のDataFrameへ反映されない場合の確認方法
drop_duplicates()は、データ分析の「読み込み→状態確認→前処理→集計」のうち、集計前の前処理で使います。前処理全体の順序を先に確認したい場合は、Pandasデータ前処理の基本を参照してください。
先に結論:判定列と残す行を決めてから削除する
重複削除では、次の2点を先に決めます。
- どの列が同じなら同一レコードとみなすか:
subsetで指定する - 重複した記録のどれを残すか:
keepで指定する
| 判断したいこと | 選ぶ設定 | 適する場面 | 注意点 |
|---|---|---|---|
| 全列が同じ行だけを削除したい | subsetを指定しない |
完全なコピー行を除く | 一部の値が違う更新記録は残る |
| 注文IDなど特定列で判定したい | subset='注文ID' |
1注文1行に整理する | 注文明細が複数行なら消しすぎる |
| 最初の行を残したい | keep='first' |
先着記録を正とする | デフォルトだが、最初が正しいとは限らない |
| 最後の行を残したい | keep='last' |
最新記録を残す | 先に更新日時で並べる必要がある |
| 重複したグループを全行除外したい | keep=False |
重複群を保留対象にする | 1行も残らない |
正しい記録を判断できない場合は、削除せず候補を別のDataFrameへ取り出して確認します。
注文データを用意して処理前の状態を確認する
実行目的
完全に同じ行と、注文IDは同じでも売上・更新日時が異なる行を含むDataFrameを作ります。以降は同じデータを使うため、コード、表、出力の違いを追いやすくなります。
import pandas as pd
orders = pd.DataFrame({
"注文ID": ["A100", "A101", "A100", "A102", "A101", "A103", "A104", "A104"],
"商品": ["ペン", "ノート", "ペン", "ファイル", "ノート", "消しゴム", "ペン", "ペン"],
"売上": [1200, 800, 1200, 500, 900, 300, 600, 600],
"更新日時": [
"2026-09-01 09:00", "2026-09-01 10:00", "2026-09-01 09:00",
"2026-09-02 11:00", "2026-09-03 15:00", "2026-09-03 16:00",
"2026-09-04 10:00", "2026-09-04 10:00"
]
})
orders["更新日時"] = pd.to_datetime(orders["更新日時"])
display(orders)
print("処理前の行数:", len(orders))
print("更新日時のdtype:", orders["更新日時"].dtype)
| 注文ID | 商品 | 売上 | 更新日時 | |
|---|---|---|---|---|
| 0 | A100 | ペン | 1200 | 2026-09-01 09:00:00 |
| 1 | A101 | ノート | 800 | 2026-09-01 10:00:00 |
| 2 | A100 | ペン | 1200 | 2026-09-01 09:00:00 |
| 3 | A102 | ファイル | 500 | 2026-09-02 11:00:00 |
| 4 | A101 | ノート | 900 | 2026-09-03 15:00:00 |
| 5 | A103 | 消しゴム | 300 | 2026-09-03 16:00:00 |
| 6 | A104 | ペン | 600 | 2026-09-04 10:00:00 |
| 7 | A104 | ペン | 600 | 2026-09-04 10:00:00 |
処理前の行数: 8 更新日時のdtype: datetime64[ns]
ordersは8行です。注文IDA100とA104は全列が同じ完全重複です。A101は2行ありますが、売上が800から900へ変わり、更新日時も異なります。
ここで見るべきなのは、注文IDだけではありません。A101の2行は更新前後の記録と考えられるため、どちらを残すか判断するには更新日時も必要です。
削除前にduplicated()で重複候補をすべて確認する
実行目的
drop_duplicates()で消す前に、注文IDが重複する行をすべて表示します。keep=Falseは、重複グループの先頭行も含めてすべてTrueにする指定です。
duplicate_mask = orders.duplicated(subset="注文ID", keep=False)
duplicate_candidates = orders.loc[duplicate_mask].sort_values(
["注文ID", "更新日時"]
)
display(duplicate_candidates)
print("重複候補の行数:", duplicate_mask.sum())
print("重複している注文ID:", duplicate_candidates["注文ID"].unique().tolist())
| 注文ID | 商品 | 売上 | 更新日時 | |
|---|---|---|---|---|
| 0 | A100 | ペン | 1200 | 2026-09-01 09:00:00 |
| 2 | A100 | ペン | 1200 | 2026-09-01 09:00:00 |
| 1 | A101 | ノート | 800 | 2026-09-01 10:00:00 |
| 4 | A101 | ノート | 900 | 2026-09-03 15:00:00 |
| 6 | A104 | ペン | 600 | 2026-09-04 10:00:00 |
| 7 | A104 | ペン | 600 | 2026-09-04 10:00:00 |
重複候補の行数: 6 重複している注文ID: ['A100', 'A101', 'A104']
重複候補は6行で、注文IDはA100・A101・A104です。A100とA104は完全重複ですが、A101は売上と更新日時が違います。
duplicated()は候補をTrue/Falseで確認し、drop_duplicates()は条件に合う行を実際に削除します。詳しい真偽値の読み方は、pandas duplicated()の使い方で確認できます。
引数なしでは全列が同じ行だけを削除する
実行目的
まず、すべての列が一致する完全重複だけを削除します。元データを残すため、結果をexact_cleanという別のDataFrameへ代入します。
exact_clean = orders.drop_duplicates()
display(exact_clean)
print("処理前の行数:", len(orders))
print("処理後の行数:", len(exact_clean))
print("削除した行数:", len(orders) - len(exact_clean))
| 注文ID | 商品 | 売上 | 更新日時 | |
|---|---|---|---|---|
| 0 | A100 | ペン | 1200 | 2026-09-01 09:00:00 |
| 1 | A101 | ノート | 800 | 2026-09-01 10:00:00 |
| 3 | A102 | ファイル | 500 | 2026-09-02 11:00:00 |
| 4 | A101 | ノート | 900 | 2026-09-03 15:00:00 |
| 5 | A103 | 消しゴム | 300 | 2026-09-03 16:00:00 |
| 6 | A104 | ペン | 600 | 2026-09-04 10:00:00 |
処理前の行数: 8 処理後の行数: 6 削除した行数: 2
8行から6行になり、完全に同じA100とA104の2行目が削除されます。一方、A101は売上と更新日時が異なるため2行とも残ります。
「注文IDが同じなら1行にしたい」場合、引数なしでは目的を達成できません。 次にsubsetで判定列を指定します。
subsetで重複を判定する列を選ぶ
subsetには列名を1つ、または列名のリストを指定できます。
| 指定 | 重複とみなす条件 | このデータでの意味 |
|---|---|---|
| 指定なし | 全列が一致 | 完全なコピー行だけを削除 |
subset='注文ID' |
注文IDが一致 | 1注文を1行にする |
subset=['注文ID', '商品'] |
注文IDと商品が両方一致 | 注文と商品単位で1行にする |
実データでは、注文IDが「注文全体」を表すのか「注文明細」を表すのかを確認してください。1つの注文IDに複数商品が正しく含まれるデータでsubset='注文ID'を使うと、必要な明細まで消してしまいます。
実行目的
注文IDを同一レコードの基準として、各注文を1行にします。まずはデフォルトのkeep='first'で先頭行を残します。
first_clean = orders.drop_duplicates(subset="注文ID", keep="first")
display(first_clean)
| 注文ID | 商品 | 売上 | 更新日時 | |
|---|---|---|---|---|
| 0 | A100 | ペン | 1200 | 2026-09-01 09:00:00 |
| 1 | A101 | ノート | 800 | 2026-09-01 10:00:00 |
| 3 | A102 | ファイル | 500 | 2026-09-02 11:00:00 |
| 5 | A103 | 消しゴム | 300 | 2026-09-03 16:00:00 |
| 6 | A104 | ペン | 600 | 2026-09-04 10:00:00 |
A101は先に現れた売上800の行が残ります。コードは正常に動いていますが、更新後の売上900を残したい場合には誤った結果です。firstは「正しい行」ではなく、現在の並び順で最初の行を意味します。
複数列の組み合わせで重複を判定する
注文IDだけで判定すると、1つの注文に含まれる別の商品まで削除することがあります。注文明細を商品単位で残す場合は、subsetへ複数の列をリストで指定します。
detail_orders = pd.DataFrame({
"注文ID": ["A300", "A300", "A300", "A301"],
"商品": ["ペン", "ノート", "ペン", "ファイル"],
"売上": [200, 500, 200, 800]
})
order_only = detail_orders.drop_duplicates(subset="注文ID")
order_and_product = detail_orders.drop_duplicates(
subset=["注文ID", "商品"],
keep="first",
ignore_index=True
)
print("処理前")
display(detail_orders)
print("注文IDだけで判定した結果")
display(order_only)
print("注文IDと商品で判定した結果")
display(order_and_product)
処理前
| 注文ID | 商品 | 売上 | |
|---|---|---|---|
| 0 | A300 | ペン | 200 |
| 1 | A300 | ノート | 500 |
| 2 | A300 | ペン | 200 |
| 3 | A301 | ファイル | 800 |
注文IDだけで判定した結果
| 注文ID | 商品 | 売上 | |
|---|---|---|---|
| 0 | A300 | ペン | 200 |
| 3 | A301 | ファイル | 800 |
注文IDと商品で判定した結果
| 注文ID | 商品 | 売上 | |
|---|---|---|---|
| 0 | A300 | ペン | 200 |
| 1 | A300 | ノート | 500 |
| 2 | A301 | ファイル | 800 |
subset="注文ID"では、注文A300の最初の「ペン」だけが残り、正しい明細である「ノート」まで消えます。subset=["注文ID", "商品"]では、注文IDと商品の組み合わせが同じ「ペン」の重複だけを削除し、「ノート」は残ります。
この結果から、subsetは列数を増やせば安全になるのではなく、データの1行が何を表すかに合わせて選ぶ必要があると分かります。
keepで残す行を選ぶ
同じ入力データでkeepだけを変え、残る行を比較します。
実行目的
first・last・Falseの結果から、A101と重複グループ全体がどう変わるかを確認します。
keep_first = orders.drop_duplicates(subset="注文ID", keep="first")
keep_last = orders.drop_duplicates(subset="注文ID", keep="last")
keep_none = orders.drop_duplicates(subset="注文ID", keep=False)
comparison = pd.DataFrame({
"設定": ["first", "last", "False"],
"残る注文ID": [
keep_first["注文ID"].tolist(),
keep_last["注文ID"].tolist(),
keep_none["注文ID"].tolist()
],
"A101で残る売上": [
keep_first.loc[keep_first["注文ID"].eq("A101"), "売上"].tolist(),
keep_last.loc[keep_last["注文ID"].eq("A101"), "売上"].tolist(),
keep_none.loc[keep_none["注文ID"].eq("A101"), "売上"].tolist()
]
})
display(comparison)
| 設定 | 残る注文ID | A101で残る売上 | |
|---|---|---|---|
| 0 | first | [A100, A101, A102, A103, A104] | [800] |
| 1 | last | [A100, A102, A101, A103, A104] | [900] |
| 2 | False | [A102, A103] | [] |
firstではA101の800、lastでは900が残ります。Falseでは重複したA100・A101・A104が全行消え、重複しないA102・A103だけが残ります。
keep=Falseは「2行目以降を削除」ではありません。重複グループを1行も残さない指定です。重複データを保留用に分ける場合には使えますが、各注文を1行残したい処理では選びません。
最新の行を残すなら更新日時で並べてからkeep=’last’を使う
keep='last'のlastは、時間的に最新という意味ではなく、現在の行順で最後という意味です。更新日時が文字列のまま、またはデータが順不同のままでは、古い記録を残す可能性があります。
実行目的
更新日時を日時型にしたことを確認し、注文IDと更新日時で並べた後、注文ごとの最新行を残します。
print("更新日時のdtype:", orders["更新日時"].dtype)
print("更新日時の欠損数:", orders["更新日時"].isna().sum())
orders_sorted = orders.sort_values(["注文ID", "更新日時"])
latest_orders = orders_sorted.drop_duplicates(
subset="注文ID",
keep="last",
ignore_index=True
)
display(latest_orders)
更新日時のdtype: datetime64[ns] 更新日時の欠損数: 0
| 注文ID | 商品 | 売上 | 更新日時 | |
|---|---|---|---|---|
| 0 | A100 | ペン | 1200 | 2026-09-01 09:00:00 |
| 1 | A101 | ノート | 900 | 2026-09-03 15:00:00 |
| 2 | A102 | ファイル | 500 | 2026-09-02 11:00:00 |
| 3 | A103 | 消しゴム | 300 | 2026-09-03 16:00:00 |
| 4 | A104 | ペン | 600 | 2026-09-04 10:00:00 |
A101では更新日時が新しい売上900の行が残ります。ignore_index=Trueにより、削除後のインデックスは0から4へ振り直されます。これは重複判定を変える指定ではなく、結果のインデックスを整える指定です。
更新日時に欠損がある場合や、どの記録が正しいか判断できない場合は、自動削除しないでください。日付文字列の変換方法は、pandas to_datetime()の使い方で確認できます。
削除後は行数・残った値・重複件数を検算する
実行目的
「エラーが出なかった」だけで終わらせず、削除行数、残ったA101の売上、残存する注文ID重複を確認します。
before_rows = len(orders)
after_rows = len(latest_orders)
removed_rows = before_rows - after_rows
remaining_duplicates = latest_orders.duplicated(
subset="注文ID", keep=False
).sum()
a101_sales = latest_orders.loc[
latest_orders["注文ID"].eq("A101"), "売上"
].iloc[0]
validation = pd.DataFrame({
"確認項目": ["処理前の行数", "削除した行数", "処理後の行数", "残存する重複行数", "A101で残った売上"],
"結果": [before_rows, removed_rows, after_rows, remaining_duplicates, a101_sales]
})
display(validation)
print("行数の式が一致:", before_rows - removed_rows == after_rows)
| 確認項目 | 結果 | |
|---|---|---|
| 0 | 処理前の行数 | 8 |
| 1 | 削除した行数 | 3 |
| 2 | 処理後の行数 | 5 |
| 3 | 残存する重複行数 | 0 |
| 4 | A101で残った売上 | 900 |
行数の式が一致: True
処理前8行から3行を削除し、処理後は5行です。注文IDの残存重複行数は0、A101で残った売上は900です。行数の式もTrueになっています。
ただし、重複数が0でも判定列が正しいとは限りません。最終的には「注文IDだけで1レコードを表せるか」をデータの意味から確認してください。
よくある失敗を現象から確認・修正する
重複が消えない
- 現象:見た目は同じ値なのに、
drop_duplicates()後も複数行が残る - 主な原因:前後空白、大文字・小文字、数値と文字列の型違い、
subsetの指定違い - 確認方法:
df.dtypes、df.columns.tolist()、repr()相当の表示、duplicated(subset=..., keep=False)で候補を確認する - 修正方法:必要な列だけ空白・表記・型をそろえ、同じ
subsetで再確認してから削除する - 修正後の確認:削除後に同じ
subsetでduplicated(keep=False).sum()が想定件数になったか確認する
必要な行まで消えた
- 現象:正しい注文明細や異なる観測まで削除された
- 主な原因:
subsetが粗すぎる、またはkeep=Falseを使った - 確認方法:削除前データから重複候補を全件表示し、判定列以外の値を比較する
- 修正方法:商品・明細番号など必要な列を
subsetへ追加する。各グループを1行残すならfirstまたはlastを選ぶ - 修正後の確認:削除した行と残った行を注文IDごとに照合する
最新ではない行が残った
- 現象:
keep='last'を使ったのに古い値が残る - 主な原因:
lastを「最新」と誤解し、更新日時で並べていない - 確認方法:更新日時のdtype、欠損数、並び順を表示する
- 修正方法:
to_datetime()で日時型へ変換し、更新日時で並べてからkeep='last'を実行する - 修正後の確認:重複していたIDの更新日時と残った値を個別に確認する
元のDataFrameへ反映されない
- 現象:メソッドを実行した後も元の行数が変わらない
- 主な原因:
drop_duplicates()は既定で新しいDataFrameを返し、元データを変更しない - 確認方法:戻り値を変数へ代入したか確認する
- 修正方法:
clean_df = df.drop_duplicates(...)のように別変数へ代入する - 修正後の確認:
len(df)とlen(clean_df)を比較する
inplace=Trueでも変更できますが、初心者は元データと処理後データを比較できるよう、まず別変数への代入をおすすめします。
NaNを含む判定列は削除前に意味を確認する
欠損した注文IDが複数あると、それらが重複候補として扱われる場合があります。しかし「注文ID不明」が同じ注文を意味するとは限りません。実際の挙動を確認します。
missing_id_sample = pd.DataFrame({
"注文ID": [None, None, "A200"],
"商品": ["ペン", "ノート", "ファイル"]
})
missing_id_sample["重複候補"] = missing_id_sample.duplicated(
subset="注文ID", keep=False
)
display(missing_id_sample)
| 注文ID | 商品 | 重複候補 | |
|---|---|---|---|
| 0 | None | ペン | True |
| 1 | None | ノート | True |
| 2 | A200 | ファイル | False |
2つの欠損した注文IDは重複候補になります。ただし、商品が異なるため、同一注文と断定して削除すると情報を失うおそれがあります。判定列に欠損がある場合は、欠損行を別に確認し、補完・保持・除外の方針を決めてください。欠損行の削除は重複行削除とは別の処理です。詳しくはPandas dropna()の使い方を参照してください。
自分のDataFrameへ置き換える前のチェックリスト
subsetに指定する列は、業務上の同一レコードを表しているか- 1つのIDに複数明細が存在してよいデータではないか
- 残す行を決める更新日時・状態・優先順位の列があるか
- 更新日時は日時型か、欠損していないか、正しく並んでいるか
- 判定列に前後空白、表記ゆれ、型違い、NaNがないか
- 元DataFrameを残し、処理後を別変数へ保存したか
- 削除前後の行数、残った値、残存重複数を確認したか
- 集計値が想定外に減っていないか
判断根拠がない重複候補は、削除せず別DataFrameへ取り出して確認してください。
重複を削除したデータを集計へつなげる
実行目的
重複除去後の注文データを商品別に集計し、二重計上のない件数と売上合計を確認します。
product_summary = (
latest_orders.groupby("商品", as_index=False)
.agg(注文件数=("注文ID", "count"), 売上合計=("売上", "sum"))
.sort_values("売上合計", ascending=False, ignore_index=True)
)
display(product_summary)
print("集計した注文件数:", product_summary["注文件数"].sum())
print("重複除去後の売上合計:", product_summary["売上合計"].sum())
| 商品 | 注文件数 | 売上合計 | |
|---|---|---|---|
| 0 | ペン | 2 | 1800 |
| 1 | ノート | 1 | 900 |
| 2 | ファイル | 1 | 500 |
| 3 | 消しゴム | 1 | 300 |
集計した注文件数: 5 重複除去後の売上合計: 3500
集計した注文件数は5件、売上合計は3,500です。latest_ordersの5行と件数が一致します。重複削除後は、件数だけでなく合計値も確認すると、削除しすぎや二重計上に気づきやすくなります。
複数列・複数集計へ進む場合は、pandas groupby・aggの使い方を参照してください。
まとめ
drop_duplicates()の前にduplicated(keep=False)で重複候補を確認するsubsetは「何を同一レコードとみなすか」で選ぶkeepは「どの記録を正として残すか」で選ぶ- 最新行を残す場合は、更新日時を日時型へ変換し、並べてから
keep='last'を使う keep=Falseは重複グループを全行削除するため、目的を確認して使う- 削除後は行数、残った値、残存重複数、集計値を検算する
- 判断できない候補は自動削除せず、元データを残して確認する
重複削除の目的は、単に行数を減らすことではありません。分析に使う正しい観測単位を決め、集計の二重計上や必要データの消失を防ぐことです。
関連記事
drop_duplicates()とduplicated()の違いは何ですか?
duplicated()は重複候補をTrue/Falseで確認するメソッド、drop_duplicates()は重複行を除いたDataFrameを返すメソッドです。安全に処理するには、先にduplicated(keep=False)で全候補を表示してから削除します。
複数列が同じ行だけを削除するにはどうしますか?
subset=['注文ID', '商品']のように列名をリストで指定します。指定した列がすべて同じ行が重複と判定されます。列を増やす前に、それらが業務上の同一レコードを表すか確認してください。
最新の行だけを残すにはどうしますか?
更新日時を日時型へ変換し、古い順に並べてからkeep='last'を使います。lastは現在の行順で最後という意味なので、並べ替えずに使っても最新行が残るとは限りません。
実行しても元のDataFrameが変わらないのはなぜですか?
drop_duplicates()は既定で新しいDataFrameを返すためです。clean_df = df.drop_duplicates(...)のように戻り値を代入してください。元データを残せるため、処理前後の比較もしやすくなります。
NaNを含む行はどう判定されますか?
判定列で欠損値が重なる行は重複候補になり得ます。ただし、欠損値同士が同じ実体を示すとは限りません。削除前に欠損行を表示し、同一レコードとして扱ってよいかデータの意味から判断してください。
コメント