最初に現れた順序を残しながら重複を取り除くなら、文字列一覧ではlist(dict.fromkeys(values))が使える。辞書などのレコード一覧では、レコード全体をそのままキーにできないため、IDなどの重複判定に使うキーを取り出して既出集合で管理する。どの要素を同じとみなし、最初と最後のどちらを残すかを先に決めよう。
集合へ変換するだけでは順序が決まらない
setを使うと要素の重複は取り除けるが、入力順をそのまま残すための道具ではない。処理した順番や利用者が並べた順番を保ちたいなら、挿入順序を保持する辞書や、別の結果リストを使う方法が向いている。
Pythonでは辞書の挿入順序がPython 3.7以降の言語仕様として保証されている。dict.fromkeysは最初に現れたキーをその順に登録するため、キー一覧をlistへ変換すれば、最初の出現順を残した重複除去になる。
文字列とレコードで方法を使い分ける
example.py
names = ["B", "A", "B", "C", "A"]
unique_names = list(dict.fromkeys(names))
print(unique_names)
assert unique_names == ["B", "A", "C"]
def unique_by(records, key):
seen = set()
result = []
for record in records:
marker = key(record)
if marker not in seen:
seen.add(marker)
result.append(record)
return result
records = [
{"id": "A", "value": 1},
{"id": "B", "value": 2},
{"id": "A", "value": 9},
]
result = unique_by(records, key=lambda row: row["id"])
print(result)
assert result == [{"id": "A", "value": 1}, {"id": "B", "value": 2}]
assert result[0] is records[0]
assert len(records) == 3
assert unique_by([], key=lambda row: row["id"]) == []
assert list(dict.fromkeys(["A", "A"])) == ["A"]
実行結果
['B', 'A', 'C']
[{'id': 'A', 'value': 1}, {'id': 'B', 'value': 2}]
unique_byは、まだ現れていないキーだけを結果へ追加する。Aの二つ目のレコードはvalueが違っても、IDを基準に同じとみなしている。そのため結果にはAの最初のvalue=1が残る。何を重複とするかは、key関数で指定できる。
レコード全体と一部のキーを区別する
辞書は通常ハッシュ可能ではないので、そのままsetやdict.fromkeysのキーにはできない。レコードを表す文字列へ無理に変換するより、意味のあるIDや項目の組をキーにする方が分かりやすい。複合キーなら、例えば装置名と試料IDのタプルを使える。
ただしタプルへ入れた要素にも、ハッシュ可能であるという条件が必要である。内側にリストを含むタプルは、そのままキーにできない。キーが値として安定しており、後から変化しないものかを確認して選ぶ。
最後の内容を残す場合は別の仕様になる
同じIDの最後のレコードを残したいなら、最初を残すこの関数とは処理が違う。IDをキーに辞書へ上書きすると値は最後のものになるが、キーの位置は最初に登録された位置に残る。最後の出現順に並ぶわけではない点に注意したい。
後勝ちの内容と最後に現れた順番の両方が必要なら、逆順に走査して最初のものを集め、最後に順序を戻すなど、条件に合わせた設計が必要になる。単に辞書化した結果が希望する順序かどうかを、重複を含む小さな例で確かめよう。
同じとみなすルールを勝手に増やさない
文字列の大文字と小文字、前後の空白を無視したいなら、keyで正規化できる。ただし、この例は入力そのものを比較するので、”A”と”a”は別である。IDによっては大小文字や先頭の0にも意味があるため、一般的な清掃処理として無条件に変換しない方がよい。
また、Pythonで等しいと扱われる数値や真偽値は、同じキーになることがある。型まで区別したいデータなら、その意図を含むキーを作る。欠損値やNaNのように通常の同値比較と違う振る舞いをする値も、重複判定へ入れる前に扱いを決めておきたい。
結果には元のオブジェクトが入る
unique_byは、選んだレコードへの参照を結果リストへ入れるだけで、辞書の中身をコピーしない。結果側でレコードを編集すれば、入力側の同じレコードにも変更が見える。重複除去とデータの独立化は別の処理である。
元を残したまま編集したい場合は、必要な深さのコピーを別に行う。また、IDが欠けたレコードを黙って一つのグループへまとめると、無関係なレコードを落とす危険がある。例のkey関数はIDがなければKeyErrorになるので、入力の不備を見つけやすい。
空入力、すべて異なる場合、すべて同じ場合、同じIDで値が違う場合を確認する。順序を保つ重複除去では、件数だけでなく、どのレコードがどの位置に残ったかまで確かめることが大切である。
動作確認と参考資料
掲載例はLinux・CPython 3.12.14で動作確認した。OS固有のコマンドや環境ごとに変わるパスは、本文中の条件を確認して使ってほしい。
関連するTips
- コピーしたデータまで変わる?浅いコピーと深いコピーを使い分ける
- レコードをIDで素早く探す:辞書の索引と重複IDの検出
- 個数も含めて2つの一覧を比較する:Counterで過不足を調べる
- 2つの名簿の共通・追加・削除を調べる:集合による差分チェック
