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() は各要素をキーとする辞書を作る。重複するキーは無視されるため、最初に出現した順序が保たれる。

set を使用

順序が失われる。最もシンプル。

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() が良いバランスだ。