NumPy配列ndarrayのユニークな要素の値・個数・位置を取得

Modified: | Tags: Python, NumPy

NumPy配列ndarrayのユニーク(一意)な要素の値を抽出したり、個数・頻度(出現回数)をカウントしたり、位置(インデックス、座標)を取得したりするには、np.unique()を使う。

ここでは以下の内容について説明する。

  • np.unique()の基本的な使い方
  • ユニークな要素の個数・頻度をカウント: 引数return_counts
  • ユニークな要素の位置(インデックス)を取得: 引数return_index, return_inverse
  • 個数と位置の同時取得
  • 二次元配列(多次元配列)の場合: 引数axis
  • 多次元配列におけるユニークな要素の位置(座標)

pandasにおけるユニークな要素についての処理は以下の記事を参照。二次元のテーブルデータを列ごとに処理する場合などはpandasのほうが楽。

np.unique()の基本的な使い方

np.unique()の第一引数に配列ndarrayを指定すると、そのndarrayのユニークな値を要素とする新たなndarrayが生成される。元のndarrayの順番ではなくソートされる。

import numpy as np

a = np.array([0, 0, 30, 10, 10, 20])
print(a)
# [ 0  0 30 10 10 20]

print(np.unique(a))
# [ 0 10 20 30]

print(type(np.unique(a)))
# <class 'numpy.ndarray'>

ndarrayだけでなくリストなどのいわゆるarray_likeオブジェクトを引数に指定可能。元のオブジェクトの型によらず返り値はndarray

l = [0, 0, 30, 10, 10, 20]
print(l)
# [0, 0, 30, 10, 10, 20]

print(np.unique(l))
# [ 0 10 20 30]

print(type(np.unique(l)))
# <class 'numpy.ndarray'>

ユニークな要素の個数・頻度をカウント: 引数return_counts

ユニークな要素の種類の数をカウントするにはnp.unique()の返り値の要素数をカウントすればよい。size属性またはlen()を使う。

print(np.unique(a).size)
# 4

print(len(np.unique(a)))
# 4

ユニークな要素ごとの頻度(出現回数)を取得するにはnp.unique()の引数return_countsTrueとする。

ユニークな値のndarrayと、それぞれの値の頻度(出現回数)を示すndarrayが返される。

u, counts = np.unique(a, return_counts=True)
print(u)
# [ 0 10 20 30]

print(counts)
# [2 2 1 1]

ndarrayの要素が対応しており、この例の場合は010が2個、2030が1個という意味。

重複していない値(出現回数が1回)や重複している値(出現回数が1回でない)は以下のように抽出可能。

print(u[counts == 1])
# [20 30]

print(u[counts != 1])
# [ 0 10]

なお、return_counts=Trueとした場合のnp.unique()の返り値はndarrayを要素とするタプル。

print(np.unique(a, return_counts=True))
# (array([ 0, 10, 20, 30]), array([2, 2, 1, 1]))

print(type(np.unique(a, return_counts=True)))
# <class 'tuple'>

上の例ではアンパックでそれぞれの変数に代入している。次に説明する 引数return_index, return_inverseの例でも同様。

ユニークな要素の位置(インデックス)を取得: 引数return_index, return_inverse

引数return_indexTrueとすると、ユニークな要素が最初に現れる位置(インデックス)を示すndarrayが同時に返される。

u, indices = np.unique(a, return_index=True)
print(u)
# [ 0 10 20 30]

print(indices)
# [0 3 5 2]

この例におけるindicesの値は、元のndarrayで最初に現れる位置(インデックス)を表している。0は元の配列の0番目、10は3番目、20は5番目、30は2番目に最初に現れることを示している。

元のndarrayと組み合わせると、ユニークな要素の位置(インデックス)が示されていることが確認できる。

print(a)
# [ 0  0 30 10 10 20]

print(a[indices])
# [ 0 10 20 30]

引数return_inverseTrueとすると、反対に、元のndarrayのどの位置にユニークな要素があるかを示すndarrayが同時に返される。

u, inverse = np.unique(a, return_inverse=True)
print(u)
# [ 0 10 20 30]

print(inverse)
# [0 0 3 1 1 2]

この例におけるinverseの値は、対応するユニークな要素のndarray(例ではu)での位置(インデックス)を表している。

print(a)
# [ 0  0 30 10 10 20]

print(u[inverse])
# [ 0  0 30 10 10 20]

個数と位置の同時取得

引数return_index, return_inverse, return_countsは同時にTrueとすることが可能。

u, indices, inverse, counts = np.unique(a, return_index=True, return_inverse=True, return_counts=True)
print(u)
# [ 0 10 20 30]

print(indices)
# [0 3 5 2]

print(inverse)
# [0 0 3 1 1 2]

print(counts)
# [2 2 1 1]

キーワード引数での指定の順番は関係なく、常にreturn_index, return_inverse, return_countsの順番となる。

print(np.unique(a, return_counts=True, return_index=True, return_inverse=True))
# (array([ 0, 10, 20, 30]), array([0, 3, 5, 2]), array([0, 0, 3, 1, 1, 2]), array([2, 2, 1, 1]))

二次元配列(多次元配列)の場合: 引数axis

多次元配列の場合、デフォルトでは平坦化(一次元化)された上で全要素に対して処理される。

a_2d = np.array([[20, 20, 10, 10], [0, 0, 10, 30], [20, 20, 10, 10]])
print(a_2d)
# [[20 20 10 10]
#  [ 0  0 10 30]
#  [20 20 10 10]]

print(np.unique(a_2d))
# [ 0 10 20 30]

引数axisを指定すると、各軸に沿って処理される。二次元配列の場合、axis=0だとユニークな行の抽出、axis=1だとユニークな列の抽出になる。行単位または列単位でユニークかどうかが判定される。

上述の一次元配列と同様に、結果はソートされる。行や列の場合、先頭の要素から順に比較される。

print(np.unique(a_2d, axis=0))
# [[ 0  0 10 30]
#  [20 20 10 10]]

print(np.unique(a_2d, axis=1))
# [[10 10 20]
#  [10 30  0]
#  [10 10 20]]

行ごと、列ごとにユニークな値を抽出するにはそれぞれの行・列を選択してnp.unique()を適用する。

print(a_2d[0])
# [20 20 10 10]

print(np.unique(a_2d[0]))
# [10 20]

print(a_2d[:, 2])
# [10 10 10]

print(np.unique(a_2d[:, 2]))
# [10]

リスト内包表記を利用すると各行に対して処理可能。ndarraytolist()でリストに変換できる。size属性などで各行のユニークな要素の種類の数を取得することも可能。

print([np.unique(row) for row in a_2d])
# [array([10, 20]), array([ 0, 10, 30]), array([10, 20])]

print([np.unique(row).tolist() for row in a_2d])
# [[10, 20], [0, 10, 30], [10, 20]]

print([np.unique(row).size for row in a_2d])
# [2, 3, 2]

各列に対して処理したい場合は転置する方法がある。

print(a_2d.T)
# [[20  0 20]
#  [20  0 20]
#  [10 10 10]
#  [10 30 10]]

print([np.unique(row) for row in a_2d.T])
# [array([ 0, 20]), array([ 0, 20]), array([10]), array([10, 30])]

列を順に選択してもよい。

print(a_2d.shape)
# (3, 4)

print([np.unique(a_2d[:, i]) for i in range(a_2d.shape[1])])
# [array([ 0, 20]), array([ 0, 20]), array([10]), array([10, 30])]

上述のように、このように列ごとに処理するような場合はpandasのほうが楽。

return_indexreturn_inverse, return_countsなどの引数をTrueとする場合も、デフォルトでは平坦化(一次元化)したndarrayに対して処理される。

u, indices, inverse, counts = np.unique(a_2d, return_index=True, return_inverse=True, return_counts=True)
print(u)
# [ 0 10 20 30]

print(indices)
# [4 2 0 7]

print(a_2d.flatten())
# [20 20 10 10  0  0 10 30 20 20 10 10]

print(a_2d.flatten()[indices])
# [ 0 10 20 30]

print(inverse)
# [2 2 1 1 0 0 1 3 2 2 1 1]

print(u[inverse])
# [20 20 10 10  0  0 10 30 20 20 10 10]

print(u[inverse].reshape(a_2d.shape))
# [[20 20 10 10]
#  [ 0  0 10 30]
#  [20 20 10 10]]

print(counts)
# [2 5 4 1]

引数axisを指定すると行や列に対して処理される。

u, indices, inverse, counts = np.unique(a_2d, axis=0, return_index=True, return_inverse=True, return_counts=True)
print(u)
# [[ 0  0 10 30]
#  [20 20 10 10]]

print(indices)
# [1 0]

print(a_2d[indices])
# [[ 0  0 10 30]
#  [20 20 10 10]]

print(inverse)
# [1 0 1]

print(u[inverse])
# [[20 20 10 10]
#  [ 0  0 10 30]
#  [20 20 10 10]]

print(counts)
# [1 2]

多次元配列におけるユニークな要素の位置(座標)

上述のように、多次元配列においてreturn_index=Trueとした場合、デフォルトでは平坦化したndarrayにおいて最初に出現する位置(インデックス)が返される。

print(a_2d)
# [[20 20 10 10]
#  [ 0  0 10 30]
#  [20 20 10 10]]

u, indices = np.unique(a_2d, return_index=True)
print(u)
# [ 0 10 20 30]

print(a_2d.flatten())
# [20 20 10 10  0  0 10 30 20 20 10 10]

print(indices)
# [4 2 0 7]

元のndarrayの形状における位置(インデックス、座標)を取得したい場合、np.where()を利用する。例えば、値が0である要素の位置のリストは以下のように取得できる。

print(list(zip(*np.where(a_2d == 0))))
# [(1, 0), (1, 1)]

これをnp.unique()とあわせて利用すると、キーkeyがユニークな値、要素valueがその位置のリストである辞書を生成したりできる。辞書内包表記を使っている。

d = {u: list(zip(*np.where(a_2d == u))) for u in np.unique(a_2d)}
print(d)
# {0: [(1, 0), (1, 1)], 10: [(0, 2), (0, 3), (1, 2), (2, 2), (2, 3)], 20: [(0, 0), (0, 1), (2, 0), (2, 1)], 30: [(1, 3)]}

print(d[0])
# [(1, 0), (1, 1)]

print(d[10])
# [(0, 2), (0, 3), (1, 2), (2, 2), (2, 3)]

print(d[20])
# [(0, 0), (0, 1), (2, 0), (2, 1)]

print(d[30])
# [(1, 3)]

出現回数に応じて条件判定することも可能。出現回数が1回やn回以下といった条件で絞り込んで値とその位置を取得できる。

d = {u: list(zip(*np.where(a_2d == u)))
     for u, c in zip(*np.unique(a_2d, return_counts=True)) if c == 1}
print(d)
# {30: [(1, 3)]}

d = {u: list(zip(*np.where(a_2d == u)))
     for u, c in zip(*np.unique(a_2d, return_counts=True)) if c <= 2}
print(d)
# {0: [(1, 0), (1, 1)], 30: [(1, 3)]}

関連カテゴリー

関連記事