Skip to content
トピック
Pandas
Pandas Drop Duplicates: Pythonで重複行を削除する方法

Pandas Drop Duplicates: Pythonで重複行を削除する方法

Updated on

重複行は、実世界のデータセットで最も一般的なデータ品質問題の1つです。繰り返しのAPIコール、重複するCSVエクスポート、故障したETLパイプライン、または手動データ入力時の単純なコピー&ペーストエラーによって忍び込みます。放置すると、重複は行数を膨らませ、平均値や合計値を歪め、機械学習モデルにバイアスを導入します。10,000件の顧客レコードがあるように見えるデータセットが、実際には8,200人のユニークな顧客と、その上に構築されたすべての計算を静かに破壊する1,800件のファントムエントリしか含んでいないかもしれません。

pandasのdrop_duplicates()メソッドは、これらの冗長な行を検出して削除する標準的な方法です。このガイドではすべてのパラメータを解説し、実世界での重複排除の一般的なパターンを示し、大規模データセットでのパフォーマンスに関する考慮事項を扱います。すべてのコード例はコピー可能で、期待される出力を含んでいます。

📚

なぜ重複が重要なのか

コードに入る前に、重複行が正確に何を壊すのかを理解する価値があります:

問題何が起こるか
膨張したカウントlen(df)value_counts()が過大にカウントする顧客が3回表示され、「総顧客数」が3倍高くなる
誤った平均値mean()が重複行をより重く重み付けする高額注文が2回カウントされ、平均注文額が上方に歪む
壊れたジョイン重複のあるキーでのマージが行の爆発を引き起こす多対多のマージが1:1マッピングではなくデカルト積を生成する
不良なMLモデル繰り返しサンプルを含む訓練データがモデルにバイアスを与えるモデルが重複した例を記憶し、過学習する
ストレージの無駄冗長な行がディスクとメモリを消費する2GBのデータセットが重複排除後に1.4GBになる可能性がある

修正は簡単です:重複を見つけ、どのコピーを保持するか(もしあれば)を決定し、残りを削除します。

基本構文: df.drop_duplicates()

最も単純な呼び出しは、すべての列値が同一の行を削除します:

import pandas as pd
 
df = pd.DataFrame({
    'name': ['Alice', 'Bob', 'Alice', 'Charlie', 'Bob'],
    'age': [30, 25, 30, 35, 25],
    'city': ['NYC', 'LA', 'NYC', 'Chicago', 'LA']
})
 
print("Before:")
print(df)
 
df_clean = df.drop_duplicates()
 
print("\nAfter:")
print(df_clean)

出力:

Before:
      name  age     city
0    Alice   30      NYC
1      Bob   25       LA
2    Alice   30      NYC
3  Charlie   35  Chicago
4      Bob   25       LA

After:
      name  age     city
0    Alice   30      NYC
1      Bob   25       LA
3  Charlie   35  Chicago

行2と行4は行0と行1の完全なコピーだったため、削除されました。元のインデックス値(0, 1, 3)が保持されていることに注意してください。連続したインデックスが必要な場合は、.reset_index(drop=True)をチェーンしてください。

完全なメソッドシグネチャ

DataFrame.drop_duplicates(subset=None, keep='first', inplace=False, ignore_index=False)
パラメータデフォルト説明
subset列ラベルまたはリストNone(全列)重複を特定する際にこれらの列のみを考慮する
keep'first', 'last', False'first'どの重複を保持するか;Falseはすべてのコピーを削除する
inplaceboolFalseTrueの場合、DataFrameをインプレースで変更しNoneを返す
ignore_indexboolFalseTrueの場合、結果のインデックスを0からn-1にリセットする

まずdf.duplicated()で重複を見つける

重複を削除する前に、それらを検査したいことがよくあります。duplicated()メソッドは重複行をマークするブーリアンSeriesを返します:

df = pd.DataFrame({
    'order_id': [101, 102, 103, 101, 104, 102],
    'product': ['Widget', 'Gadget', 'Widget', 'Widget', 'Gizmo', 'Gadget'],
    'amount': [29.99, 49.99, 29.99, 29.99, 19.99, 49.99]
})
 
# Show which rows are duplicates
print(df.duplicated())

出力:

0    False
1    False
2    False
3     True
4    False
5     True
dtype: bool

実際の重複行を表示するには:

duplicates = df[df.duplicated(keep=False)]
print(duplicates)

出力:

   order_id product  amount
0       101  Widget   29.99
1       102  Gadget   49.99
3       101  Widget   29.99
5       102  Gadget   49.99

keep=Falseを使用すると、すべてのコピーを重複としてマークします(2番目の出現だけでなく)。これにより、重複に関与するすべての行を確認できます。

重複のカウント

重複行がいくつ存在するかを素早く数えるには:

num_duplicates = df.duplicated().sum()
print(f"Number of duplicate rows: {num_duplicates}")
 
total_rows = len(df)
unique_rows = df.drop_duplicates().shape[0]
print(f"Total: {total_rows}, Unique: {unique_rows}, Duplicates: {total_rows - unique_rows}")

出力:

Number of duplicate rows: 2
Total: 6, Unique: 4, Duplicates: 2

これはクリーニングの前後で有用なサニティチェックです。

subsetパラメータ: 特定の列のみをチェック

すべての列が一致することを要求するのではなく、キー列に基づいて重複排除したいことがよくあります。subsetパラメータを使用すると、一意性を決定する列を指定できます:

df = pd.DataFrame({
    'email': ['alice@mail.com', 'bob@mail.com', 'alice@mail.com', 'charlie@mail.com'],
    'name': ['Alice', 'Bob', 'Alice Smith', 'Charlie'],
    'signup_date': ['2025-01-01', '2025-01-02', '2025-01-15', '2025-01-03']
})
 
print("Original:")
print(df)
 
# Deduplicate by email only
df_deduped = df.drop_duplicates(subset=['email'])
print("\nDeduplicated by email:")
print(df_deduped)

出力:

Original:
              email       name signup_date
0    alice@mail.com      Alice  2025-01-01
1      bob@mail.com        Bob  2025-01-02
2    alice@mail.com  Alice Smith  2025-01-15
3  charlie@mail.com    Charlie  2025-01-03

Deduplicated by email:
              email     name signup_date
0    alice@mail.com    Alice  2025-01-01
1      bob@mail.com      Bob  2025-01-02
3  charlie@mail.com  Charlie  2025-01-03

namesignup_dateの値が異なっていても、alice@mail.comが既に行0に存在していたため、行2が削除されました。複数の列を渡すこともできます:subset=['email', 'name']は両方の列が一致する場合にのみ行を重複とみなします。

keepパラメータ: first、last、またはFalse

keepパラメータは、どの出現を残すかを制御します:

df = pd.DataFrame({
    'sensor_id': ['S1', 'S2', 'S1', 'S2', 'S1'],
    'reading': [22.5, 18.3, 23.1, 18.3, 24.0],
    'timestamp': ['08:00', '08:00', '09:00', '09:00', '10:00']
})
 
print("keep='first' (default):")
print(df.drop_duplicates(subset=['sensor_id'], keep='first'))
 
print("\nkeep='last':")
print(df.drop_duplicates(subset=['sensor_id'], keep='last'))
 
print("\nkeep=False (drop all duplicates):")
print(df.drop_duplicates(subset=['sensor_id'], keep=False))

出力:

keep='first' (default):
  sensor_id  reading timestamp
0        S1     22.5     08:00
1        S2     18.3     08:00

keep='last':
  sensor_id  reading timestamp
3        S2     18.3     09:00
4        S1     24.0     10:00

keep=False (drop all duplicates):
Empty DataFrame
Columns: [sensor_id, reading, timestamp]
Index: []
keepの値動作ユースケース
'first'最初の出現を保持し、後のものを削除する最も古いレコードを保持する
'last'最後の出現を保持し、前のものを削除する最新のレコードを保持する
False重複がある行をすべて削除する本当にユニークな行を見つける(コピーがまったくない)

上の例では、両方のセンサーIDが複数回出現するため、keep=Falseは空のDataFrameを返します。

インプレース vs 新しいDataFrameを返す

デフォルトでは、drop_duplicates()は新しいDataFrameを返し、元のDataFrameは変更されません。inplace=Trueを設定すると、元のDataFrameを直接変更します:

df = pd.DataFrame({
    'id': [1, 2, 2, 3],
    'value': ['a', 'b', 'b', 'c']
})
 
# Returns new DataFrame (original unchanged)
df_new = df.drop_duplicates()
print(f"Original length: {len(df)}, New length: {len(df_new)}")
 
# Modifies in place (returns None)
df.drop_duplicates(inplace=True)
print(f"After inplace: {len(df)}")

出力:

Original length: 4, New length: 3
After inplace: 3

モダンなpandasスタイルでは、inplace=Trueを避け、代わりに代入を使用することを推奨しています(df = df.drop_duplicates())。これにより、特にチェーン操作で、コードが読みやすくデバッグしやすくなります。

大文字小文字を区別しない重複検出

デフォルトでは、pandasは"Alice""alice"を異なる値として扱います。大文字小文字を区別しない重複排除を行うには、まず列を正規化します:

df = pd.DataFrame({
    'name': ['Alice', 'alice', 'ALICE', 'Bob', 'bob'],
    'score': [90, 85, 92, 78, 80]
})
 
# Create a normalized column for comparison
df['name_lower'] = df['name'].str.lower()
 
# Deduplicate on the normalized column, keep the first original-case entry
df_deduped = df.drop_duplicates(subset=['name_lower']).drop(columns=['name_lower'])
print(df_deduped)

出力:

    name  score
0  Alice     90
3    Bob     78

このパターンは、大文字小文字を区別しない重複を正しく識別しながら、元の大文字小文字を保持します。

実世界の例: 顧客データベースのクリーニング

現実的なシナリオをご紹介します。CRMから顧客エクスポートを受け取りましたが、同じ顧客が異なる営業担当者によって複数回入力されていました:

import pandas as pd
 
customers = pd.DataFrame({
    'customer_id': [1001, 1002, 1001, 1003, 1002, 1004],
    'name': ['Acme Corp', 'Beta Inc', 'Acme Corp', 'Gamma LLC', 'Beta Inc.', 'Delta Co'],
    'email': ['acme@mail.com', 'beta@mail.com', 'acme@mail.com', 'gamma@mail.com', 'beta@mail.com', 'delta@mail.com'],
    'revenue': [50000, 30000, 52000, 45000, 30000, 20000],
    'last_contact': ['2025-12-01', '2025-11-15', '2026-01-10', '2025-10-20', '2025-11-15', '2026-01-05']
})
 
print(f"Rows before cleaning: {len(customers)}")
print(f"Duplicate customer_ids: {customers.duplicated(subset=['customer_id']).sum()}")
 
# Sort by last_contact descending so the most recent entry is first
customers['last_contact'] = pd.to_datetime(customers['last_contact'])
customers = customers.sort_values('last_contact', ascending=False)
 
# Keep the most recent record for each customer_id
customers_clean = customers.drop_duplicates(subset=['customer_id'], keep='first')
customers_clean = customers_clean.sort_values('customer_id').reset_index(drop=True)
 
print(f"\nRows after cleaning: {len(customers_clean)}")
print(customers_clean)

出力:

Rows before cleaning: 6
Duplicate customer_ids: 2

Rows after cleaning: 4
   customer_id       name           email  revenue last_contact
0         1001  Acme Corp   acme@mail.com    52000   2026-01-10
1         1002   Beta Inc  beta@mail.com    30000   2025-11-15
2         1003  Gamma LLC  gamma@mail.com    45000   2025-10-20
3         1004   Delta Co  delta@mail.com    20000   2026-01-05

ここでのキーパターンはまずソートし、次にkeep='first'で重複排除することです。重複排除の前にlast_contactを降順でソートすることで、各顧客の最新レコードが残ります。

実世界の例: Webスクレイピング結果の重複排除

Webスクレイパーは、ページが複数回クロールされたり、ページネーションが重複したりすると、よく重複を生成します:

import pandas as pd
 
scraped = pd.DataFrame({
    'url': [
        'https://shop.com/item/101',
        'https://shop.com/item/102',
        'https://shop.com/item/101',
        'https://shop.com/item/103',
        'https://shop.com/item/102',
        'https://shop.com/item/104',
    ],
    'title': ['Blue Widget', 'Red Gadget', 'Blue Widget', 'Green Gizmo', 'Red Gadget', 'Yellow Thing'],
    'price': [19.99, 29.99, 19.99, 39.99, 31.99, 14.99],
    'scraped_at': ['2026-02-01', '2026-02-01', '2026-02-02', '2026-02-02', '2026-02-02', '2026-02-02']
})
 
print(f"Total scraped rows: {len(scraped)}")
print(f"Unique URLs: {scraped['url'].nunique()}")
 
# Keep the latest scrape for each URL (prices may have changed)
scraped['scraped_at'] = pd.to_datetime(scraped['scraped_at'])
scraped = scraped.sort_values('scraped_at', ascending=False)
products = scraped.drop_duplicates(subset=['url'], keep='first').reset_index(drop=True)
 
print(f"\nCleaned rows: {len(products)}")
print(products)

出力:

Total scraped rows: 6
Unique URLs: 4

Cleaned rows: 4
                          url        title  price scraped_at
0  https://shop.com/item/101  Blue Widget  19.99 2026-02-02
1  https://shop.com/item/102   Red Gadget  31.99 2026-02-02
2  https://shop.com/item/103  Green Gizmo  39.99 2026-02-02
3  https://shop.com/item/104  Yellow Thing  14.99 2026-02-02

Red Gadgetの価格がスクレイピング間で29.99から31.99に更新されたことに注意してください。最新のエントリを保持することで、最新の価格を取得しています。

drop_duplicates() vs groupby().first(): いつどちらを使うか

どちらのアプローチもデータを重複排除できますが、動作が異なります:

import pandas as pd
 
df = pd.DataFrame({
    'user_id': [1, 1, 2, 2, 3],
    'action': ['login', 'purchase', 'login', 'login', 'purchase'],
    'timestamp': ['2026-01-01', '2026-01-02', '2026-01-01', '2026-01-03', '2026-01-01']
})
 
# Method 1: drop_duplicates
result1 = df.drop_duplicates(subset=['user_id'], keep='first')
print("drop_duplicates:")
print(result1)
 
# Method 2: groupby().first()
result2 = df.groupby('user_id').first().reset_index()
print("\ngroupby().first():")
print(result2)

出力:

drop_duplicates:
   user_id    action   timestamp
0        1     login  2026-01-01
2        2     login  2026-01-01
4        3  purchase  2026-01-01

groupby().first():
   user_id    action   timestamp
0        1     login  2026-01-01
1        2     login  2026-01-01
2        3  purchase  2026-01-01

結果は似ていますが、重要な違いがあります:

特徴drop_duplicates()groupby().first()
速度単純な重複排除ではより高速グループ化のオーバーヘッドにより低速
NaN処理NaN値をそのまま保持するfirst()はデフォルトでNaNをスキップする
インデックス元のインデックスを保持するグループキーにリセットされる
集約他の列を集約できないagg()と組み合わせて複数列のサマリーが可能
メモリメモリ使用量が少ない中間のGroupByオブジェクトを作成する
ユースケース完全または部分的な重複を削除する集約を計算しながら重複排除する

経験則: 単純に重複行を削除したい場合はdrop_duplicates()を使用してください。重複行の値も集約する必要がある場合はgroupby().first()(またはgroupby().agg())を使用してください -- 例えば、最初の名前を保持しながら重複顧客レコード間で売上を合計する場合などです。

大規模DataFrameのパフォーマンスのヒント

数百万行を扱う場合、重複排除がボトルネックになることがあります。以下は高速化するための実践的な方法です:

1. subset列を指定する

すべての列をチェックするよりも、キー列のみをチェックする方が高速です:

# Slower: checks every column
df.drop_duplicates()
 
# Faster: checks only the key column
df.drop_duplicates(subset=['user_id'])

2. 適切なデータ型を使用する

カーディナリティが低い場合、重複排除の前に文字列列をcategory型に変換します:

df['status'] = df['status'].astype('category')
df['country'] = df['country'].astype('category')
df_clean = df.drop_duplicates(subset=['status', 'country'])

3. 必要な場合にのみ重複排除前にソートする

keep='first'がどの行を選択するかを制御するためだけに大規模DataFrameをソートすると、かなりの時間がかかります。どの重複が残るかを気にしない場合は、ソートをスキップしてください。

4. 非常に大きなファイルにはチャンク処理を検討する

メモリに収まらないファイルの場合、チャンクで処理します:

chunks = pd.read_csv('large_file.csv', chunksize=100_000)
seen = set()
clean_chunks = []
 
for chunk in chunks:
    chunk = chunk.drop_duplicates(subset=['id'])
    new_rows = chunk[~chunk['id'].isin(seen)]
    seen.update(new_rows['id'].tolist())
    clean_chunks.append(new_rows)
 
df_clean = pd.concat(clean_chunks, ignore_index=True)

5. ベンチマーク: 典型的なパフォーマンス

行数チェックする列時間(約)
100Kすべて(10列)~15 ms
1Mすべて(10列)~150 ms
1M1列~50 ms
10M1列~500 ms

これらの数値はハードウェアやデータ型によって異なりますが、重要なポイントはdrop_duplicates()が線形にスケールし、ほとんどのデータセットを1秒未満で処理することです。

PyGWalkerでクリーニングしたデータを探索する

重複を削除した後、次のステップは通常、クリーニングしたデータセットを探索することです -- 分布を確認し、外れ値を見つけ、重複排除が期待通りに機能したことを検証します。複数のmatplotlibやseaborn呼び出しを書く代わりに、PyGWalker (opens in a new tab)を使用できます。これは、pandas DataFrameをJupyter Notebook上で直接インタラクティブなTableau風のビジュアライゼーションインターフェースに変換するオープンソースのPythonライブラリです。

import pandas as pd
import pygwalker as pyg
 
# Your cleaned customer data
customers_clean = pd.DataFrame({
    'customer_id': [1001, 1002, 1003, 1004],
    'name': ['Acme Corp', 'Beta Inc', 'Gamma LLC', 'Delta Co'],
    'revenue': [52000, 30000, 45000, 20000],
    'region': ['East', 'West', 'East', 'South']
})
 
# Launch interactive visualization
walker = pyg.walk(customers_clean)

PyGWalkerを使えば、regionをx軸に、revenueをy軸にドラッグして、地域別の売上分布を即座に確認できます。フィールドをドラッグ&ドロップするだけで、棒グラフ、散布図、ヒストグラム、ヒートマップを作成できます -- チャートコードは不要です。重複排除後にクリーニングロジックが妥当な結果を生み出したかを確認したい場合に特に便利です。

pip install pygwalkerでPyGWalkerをインストールするか、Google Colab (opens in a new tab)で試してみてください。

FAQ

drop_duplicates()は元のDataFrameを変更しますか?

いいえ、デフォルトではdrop_duplicates()は新しいDataFrameを返し、元のDataFrameは変更されません。インプレースで変更するにはinplace=Trueを渡しますが、推奨されるアプローチは代入を使用することです:df = df.drop_duplicates()

1つの列のみに基づいて重複を削除するにはどうすればよいですか?

列名をsubsetパラメータに渡します:df.drop_duplicates(subset=['email'])。これは各ユニークなメールアドレスの最初の行を保持し、他の列の違いに関係なく、同じメールアドレスを持つ後続の行を削除します。

duplicated()とdrop_duplicates()の違いは何ですか?

duplicated()はどの行が重複かをマークするブーリアンSeriesを返します(検査とカウントに便利)。drop_duplicates()は重複行が削除されたDataFrameを返します。データを理解するためにまずduplicated()を使用し、次にクリーニングするためにdrop_duplicates()を使用します。

条件に基づいて重複を削除できますか?

drop_duplicates()で直接はできません。代わりに、まず条件列でDataFrameをソートし、次にkeep='first'drop_duplicates()を呼び出します。例えば、各顧客の最高売上の行を保持するには:df.sort_values('revenue', ascending=False).drop_duplicates(subset=['customer_id'], keep='first')

大文字小文字を区別しない重複をどう処理しますか?

一時的な小文字列を作成し、それで重複排除を行い、ヘルパー列を削除します:df['key'] = df['name'].str.lower()の後にdf.drop_duplicates(subset=['key']).drop(columns=['key'])。これにより、元の大文字小文字を保持しながら、重複を正しく識別できます。

まとめ

重複行の削除は、あらゆるデータクリーニングワークフローにおける基本的なステップです。pandasのdrop_duplicates()メソッドは、わずかなパラメータで重複排除タスクの大部分を処理します:

  • **subset**を使用して、すべての列ではなく特定の列で重複排除します。
  • **keep='first'またはkeep='last'を使用して、どの出現が残るかを制御します;keep=False**ですべてのコピーを削除します。
  • **duplicated()**を使用して、削除する前に重複を検査しカウントします。
  • 特定の行を保持する必要がある場合(例:最新のレコード)は、重複排除の前にソートします。
  • 大文字小文字を区別しない重複排除には、まず一時列で小文字に正規化します。
  • 大規模データセットでは、subset列を制限し、適切なデータ型を使用してパフォーマンスを維持します。

データがクリーニングされたら、PyGWalker (opens in a new tab)のようなツールを使えば、チャートコードを書かずに結果を視覚的に探索でき、重複排除が正しく機能したことを確認してすぐに分析に移ることができます。

📚