Python の集合を使った重複排除パターン
集合は重複を自動的に排除するため、データのユニーク化に最適だ。ここでは様々な重複排除パターンを紹介する。
基本的な重複排除
リストから重複を除去する最もシンプルな方法は、set に変換することだ。
items = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
unique = list(set(items))
print(unique) # [1, 2, 3, 4](順序は不定)
ただし、この方法では元の順序が失われる。順序を保持したい場合は別のアプローチが必要だ。
順序を保持した重複排除
Python 3.7 以降は dict が挿入順序を保持するため、これを利用できる。
items = [3, 1, 2, 1, 3, 2, 4]
unique = list(dict.fromkeys(items))
print(unique) # [3, 1, 2, 4](出現順を保持)
dict.fromkeys() は各要素をキーとする辞書を作る。重複するキーは無視されるため、最初に出現した順序が保たれる。
順序が失われる。最もシンプル。
順序を保持。Python 3.7 以降で確実。
ループで実装する方法
見たことのある要素を set で追跡しながら、新しい要素だけを追加するパターンもある。
def unique_ordered(items):
seen = set()
result = []
for item in items:
if item not in seen:
seen.add(item)
result.append(item)
return result
items = [3, 1, 2, 1, 3, 2, 4]
print(unique_ordered(items)) # [3, 1, 2, 4]
この方法は、途中で処理を挟みたい場合に柔軟性がある。
条件付き重複排除
特定の条件に基づいて重複を判定したい場合がある。例えば、辞書のリストから特定のキーで重複を排除する。
users = [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
{"id": 1, "name": "Alice (duplicate)"},
{"id": 3, "name": "Charlie"},
]
seen_ids = set()
unique_users = []
for user in users:
if user["id"] not in seen_ids:
seen_ids.add(user["id"])
unique_users.append(user)
print(unique_users)
# [{'id': 1, 'name': 'Alice'}, {'id': 2, 'name': 'Bob'}, {'id': 3, 'name': 'Charlie'}]
id が重複する 2 番目の Alice は除外される。
オブジェクトの属性で重複排除
カスタムオブジェクトの場合、特定の属性を基準にできる。
from dataclasses import dataclass
@dataclass
class Product:
sku: str
name: str
price: int
products = [
Product("A001", "Apple", 100),
Product("B002", "Banana", 80),
Product("A001", "Apple (old)", 90),
Product("C003", "Cherry", 150),
]
seen_skus = set()
unique_products = []
for p in products:
if p.sku not in seen_skus:
seen_skus.add(p.sku)
unique_products.append(p)
print([p.name for p in unique_products])
# ['Apple', 'Banana', 'Cherry']
関数化して汎用的に
キー関数を受け取る汎用的な重複排除関数を作ると便利だ。
def unique_by(items, key=None):
seen = set()
result = []
for item in items:
k = key(item) if key else item
if k not in seen:
seen.add(k)
result.append(item)
return result
# 使用例
words = ["Apple", "APPLE", "apple", "Banana", "BANANA"]
unique = unique_by(words, key=str.lower)
print(unique) # ['Apple', 'Banana']
大文字小文字を無視した重複排除ができる。
複数のキーで重複判定
複数の属性を組み合わせて重複を判定する場合は、タプルをキーにする。
records = [
{"date": "2024-01-01", "user": "alice", "action": "login"},
{"date": "2024-01-01", "user": "bob", "action": "login"},
{"date": "2024-01-01", "user": "alice", "action": "login"},
{"date": "2024-01-02", "user": "alice", "action": "login"},
]
seen = set()
unique = []
for r in records:
key = (r["date"], r["user"])
if key not in seen:
seen.add(key)
unique.append(r)
print(len(unique)) # 3
同じ日に同じユーザーが複数回ログインした記録は 1 つにまとめられる。
ジェネレータ版
メモリ効率を重視する場合、ジェネレータで実装できる。
def unique_gen(items, key=None):
seen = set()
for item in items:
k = key(item) if key else item
if k not in seen:
seen.add(k)
yield item
# 大量のデータでもメモリ効率がよい
for item in unique_gen(range(1000000)):
pass # 結果を一度に保持しない
結果を一度にリスト化せず、順次処理する場合に有効だ。
最後に出現した要素を残す
通常は最初に出現した要素を残すが、最後を残したい場合もある。
items = [1, 2, 1, 3, 2, 4, 3]
# 最後に出現した要素を残す
result = list(dict.fromkeys(reversed(items)))[::-1]
print(result) # [1, 4, 2, 3]
リストを反転してから処理し、結果を再度反転する。
pandas での重複排除
pandas を使う場合は drop_duplicates() が便利だ。
import pandas as pd
df = pd.DataFrame({
"id": [1, 2, 1, 3, 2],
"name": ["A", "B", "A", "C", "B"]
})
# id で重複排除(最初を残す)
unique_df = df.drop_duplicates(subset=["id"], keep="first")
print(unique_df)
大規模データの処理では pandas のほうが高速な場合がある。
パフォーマンス比較
各方法のパフォーマンスを比較する。
import time
items = list(range(10000)) * 10 # 10万要素、重複あり
# set 変換
start = time.perf_counter()
result = list(set(items))
print(f"set: {time.perf_counter() - start:.4f}s")
# dict.fromkeys
start = time.perf_counter()
result = list(dict.fromkeys(items))
print(f"dict.fromkeys: {time.perf_counter() - start:.4f}s")
# ループ版
start = time.perf_counter()
seen = set()
result = [x for x in items if not (x in seen or seen.add(x))]
print(f"loop: {time.perf_counter() - start:.4f}s")
set() が最速だが、順序保持が必要なら dict.fromkeys() が良いバランスだ。