Pythonでリスト(配列)から重複した要素を削除・抽出
Pythonで、リスト(配列)から、
- 重複した要素を削除(一意な要素・ユニークな要素のみを抽出)
- 重複した要素を抽出
して、新たなリストを生成する方法について説明する。
なお、リストではなくタプルの場合も同様の考え方で実現可能。
リストやタプルが重複した要素を持っているかどうかを判定したい場合、一つのリストではなく複数のリスト間で共通する要素や共通しない要素を抽出したい場合は以下の記事を参照。
なお、リストは異なる型のデータを格納可能で、厳密には配列とは異なる。配列を扱いたい場合はarray(標準ライブラリ)やNumPyを使う。
numpy.ndarray
に対する一意な要素の抽出については以下の記事を参照。
重複した要素を削除し、新たなリストを生成
元のリストの順序を保持しない: set()
元のリストの順序を保持する必要がない場合は、集合型set
を生成するset()
を使う。
set
型は重複した要素をもたないデータ型で、set()
にリストなどを渡すと、重複する値は無視されて一意な値のみが要素となるset
型のオブジェクトを返す。
これをlist()
で再びリストにする。タプルにしたい場合はtuple()
を使えばよい。もちろんset
のままでもよい。
l = [3, 3, 2, 1, 5, 1, 4, 2, 3]
print(set(l))
# {1, 2, 3, 4, 5}
print(list(set(l)))
# [1, 2, 3, 4, 5]
元のリストの順序を保持する: dict.fromkeys(), sorted()
元のリストの順番を保持したい場合は、辞書型dict
のクラスメソッドfromkeys()
、または組み込み関数sorted()
を使う。
dict.fromkeys()
は引数に指定したリストやタプルなどをキーとした新たな辞書オブジェクトを生成する。第二引数を省略した場合、値はNone
。
辞書のキーは重複した要素をもたないので、set()
と同じく重複する値は無視される。さらに辞書オブジェクトをlist()
の引数に渡すと辞書のキーを要素とするリストが得られるので、これを利用する。
l = [3, 3, 2, 1, 5, 1, 4, 2, 3]
print(dict.fromkeys(l))
# {3: None, 2: None, 1: None, 5: None, 4: None}
print(list(dict.fromkeys(l)))
# [3, 2, 1, 5, 4]
dict.fromkeys()
で引数のシーケンスの順序が保持されることが保証されているのはPython3.7(CPythonは3.6)から。それより前のバージョンでは以下のように組み込み関数sorted()
を使う。
要素をソートしたリストを返すsorted
の引数key
にリスト・タプルのメソッドindex()
を指定する。
index()
は値のインデックス(リスト中の何番目の要素か)を返すメソッドで、sorted()
のkey
に指定することで、元のリストの順番を基準に並べ替えられる。
print(sorted(set(l), key=l.index))
# [3, 2, 1, 5, 4]
二次元配列(リストのリスト)の場合
二次元配列(リストのリスト)の場合、set()
やdict.fromkeys()
を使う方法はエラーTypeError
になる。
l_2d = [[1, 1], [0, 1], [0, 1], [0, 0], [1, 0], [1, 1], [1, 1]]
# l_2d_unique = list(set(l_2d))
# TypeError: unhashable type: 'list'
# l_2d_unique_order = dict.fromkeys(l_2d)
# TypeError: unhashable type: 'list'
これは、リストなどのハッシュ不可能なオブジェクトはset
型の要素やdict
型のキーにできないから。
以下のような関数を定義する。元のリストの順番は保持され、一次元のリストやタプルに対しても動作する。
def get_unique_list(seq):
seen = []
return [x for x in seq if x not in seen and not seen.append(x)]
print(get_unique_list(l_2d))
# [[1, 1], [0, 1], [0, 0], [1, 0]]
print(get_unique_list(l))
# [3, 2, 1, 5, 4]
リスト内包表記を使っている。
- 関連記事: Pythonリスト内包表記の使い方
ここでは、以下の性質を利用している。
and
演算子のショートサーキット(短絡評価)でX and Y
のX
がFalse
であればY
は評価されない(実行されない)ことappend()
メソッドがNone
を返すこと
元のリストseq
の要素がseen
に存在しない場合(x not in seen
がTrue
)、and
以降のnot seen.append(x)
が評価される。seen.append(x)
が実行され、seen
にその要素が追加される。append()
メソッドがNone
を返し、None
はFalse
であるため、not seen.append(x)
はTrue
と評価される。リスト内包表記の条件式がTrue
となり、最終的に生成されるリストの要素として追加される。
元のリストseq
の要素がseen
に存在する場合は、x not in seen
がFalse
となり、リスト内包表記の条件式がFalse
となるため、最終的に生成されるリストの要素として追加されない。
そのほか、結果はソートされるが、NumPyの関数np.unique()
で引数axis
を設定する方法もある。
重複した要素を抽出し、新たなリストを生成
元のリストの順序を保持しない
元のリストから重複している要素のみを抽出する場合は、collections.Counter()
を使う。要素をキー、その個数を値とするcollections.Counter
(辞書のサブクラス)を返す。
import collections
l = [3, 3, 2, 1, 5, 1, 4, 2, 3]
print(collections.Counter(l))
# Counter({3: 3, 2: 2, 1: 2, 5: 1, 4: 1})
辞書のサブクラスなのでitems()
でキーと値を取り出せる。個数が2個以上のキーを抽出すればよい。
print([k for k, v in collections.Counter(l).items() if v > 1])
# [3, 2, 1]
元のリストの順序を保持する
上の例のように、Python 3.7以降はcollections.Counter
のキーは元のリストなどの順序を保持する。
それより前のバージョンでは、重複した要素を削除する場合と同様にsorted()
でソートすればよい。
l = [3, 3, 2, 1, 5, 1, 4, 2, 3]
print(sorted([k for k, v in collections.Counter(l).items() if v > 1], key=l.index))
# [3, 2, 1]
重複した状態のまま抽出したい場合は、単純に元のリストから個数が2個以上の要素を残せばよい。順序も保持される。
cc = collections.Counter(l)
print([x for x in l if cc[x] > 1])
# [3, 3, 2, 1, 1, 2, 3]
二次元配列(リストのリスト)の場合
二次元配列(リストのリスト)に対しては、元のリストの順番を保持しない場合と保持する場合でそれぞれ以下のような関数が考えられる。一次元のリストやタプルに対しても動作する。
l_2d = [[1, 1], [0, 1], [0, 1], [0, 0], [1, 0], [1, 1], [1, 1]]
def get_duplicate_list(seq):
seen = []
return [x for x in seq if not seen.append(x) and seen.count(x) == 2]
def get_duplicate_list_order(seq):
seen = []
return [x for x in seq if seq.count(x) > 1 and not seen.append(x) and seen.count(x) == 1]
print(get_duplicate_list(l_2d))
# [[0, 1], [1, 1]]
print(get_duplicate_list_order(l_2d))
# [[1, 1], [0, 1]]
print(get_duplicate_list(l))
# [3, 1, 2]
print(get_duplicate_list_order(l))
# [3, 2, 1]
重複した状態のまま抽出したいのであれば、元のリストから個数が2個以上の要素を残す。
print([x for x in l_2d if l_2d.count(x) > 1])
# [[1, 1], [0, 1], [0, 1], [1, 1], [1, 1]]
なお、count()
の計算量はO(n)
であるため、上で示したcount()
を繰り返し実行する関数は非常に効率が悪い。もっとスマートな方法があるかも知れない。
collections.Counter
は辞書のサブクラスなので、collections.Counter()
にリストなどのハッシュ不可能なオブジェクトを要素とするリストやタプルを渡すとエラーになり使えない。
# print(collections.Counter(l_2d))
# TypeError: unhashable type: 'list'