numpy uniqueとは?使い方を徹底解説【Python/重複除去】


numpy uniqueとは?使い方を徹底解説【Python/重複除去】

はじめに:データ処理における重複問題

データ分析や機械学習、科学技術計算において、数値データはNumPy配列として扱われることが非常に多いです。NumPyはPythonの強力な数値計算ライブラリであり、高速な多次元配列オブジェクト(ndarray)と、それを操作するための豊富な関数群を提供します。

データ収集の過程や、複数のデータソースを統合する際に、意図しないデータの重複が発生することはよくあります。重複データが存在すると、分析結果が歪んだり、計算が無駄に実行されたりするなど、様々な問題を引き起こす可能性があります。例えば、

  • ユーザーIDのリストに同じIDが複数回出現する
  • センサーの測定値が短時間に同じ値を記録する
  • データベースから取得したレコードに重複がある

このような重複データを取り除くことは、データクリーニングの重要なステップです。

NumPyには、配列内の重複した要素を効率的に除去するための強力な関数が用意されています。それが、本記事で徹底的に解説するnumpy.unique関数です。

numpy.uniqueは単に重複を取り除くだけでなく、重複除去の結果に関連する様々な情報を同時に取得できる柔軟性を持っています。この記事では、numpy.uniqueの基本的な使い方から、多次元配列への応用、様々な引数の活用方法、さらには内部処理や他のライブラリとの比較まで、詳細に解説します。この記事を読むことで、numpy.uniqueを自在に使いこなし、データ処理の効率と精度を向上させることができるようになるでしょう。

numpy.uniqueとは?

numpy.unique関数は、NumPy配列(またはNumPy配列に変換可能なオブジェクト)を受け取り、その中に含まれる一意(ユニーク)な要素だけを抽出して新しい配列として返す関数です。返される配列は、デフォルトでは昇順にソートされます。

基本的な機能:

  1. 入力配列から重複要素を取り除く。
  2. 一意な要素のみを含む新しい配列を作成する。
  3. 結果の配列を昇順にソートする(デフォルト)。

関数のシグネチャ(NumPy v1.26時点の主要部分):

python
numpy.unique(ar,
return_index=False,
return_inverse=False,
return_counts=False,
axis=None)

引数:

  • ar: 入力配列。重複を除去したいNumPy配列またはリストなど。
  • return_index: Trueの場合、一意な要素が元の配列(ar)内で最初に出現する位置のインデックスを返す。
  • return_inverse: Trueの場合、元の配列(ar)の各要素が、返される一意な配列のどの位置に対応するかを示すインデックスを返す。
  • return_counts: Trueの場合、返される一意な配列の各要素が、元の配列(ar)で何回出現するかを返す。
  • axis: Noneの場合、入力配列はフラット化(1次元化)されてから処理される。整数を指定すると、指定された軸に沿って一意な要素(例えば2D配列なら一意な行)を抽出する。(NumPy 1.13.0で追加)

これらの引数を組み合わせて使用することで、重複除去だけでなく、元のデータとの関連性や各要素の出現頻度など、多様な情報を一度に取得できます。

numpy.uniqueの基本的な使い方

まずは、最もシンプルで一般的な使い方である、1次元配列の重複除去から見ていきましょう。

1次元配列の場合

numpy.uniqueに1次元配列を渡すと、その配列に含まれる一意な要素が抽出され、ソートされた新しい配列として返されます。

“`python
import numpy as np

重複を含む1次元配列

data = np.array([1, 3, 2, 1, 4, 3, 5, 2, 1])

unique関数を適用

unique_elements = np.unique(data)

print(“元の配列:”, data)
print(“一意な要素:”, unique_elements)
“`

出力結果:

元の配列: [1 3 2 1 4 3 5 2 1]
一意な要素: [1 2 3 4 5]

ご覧の通り、元の配列に含まれていた重複(1, 3, 2が複数回出現)が取り除かれ、一意な要素である1, 2, 3, 4, 5が昇順にソートされて新しい配列として得られています。

入力がリストのようなNumPy配列でないオブジェクトでも、numpy.uniqueは内部的にNumPy配列に変換して処理を行います。

“`python
data_list = [1, 3, 2, 1, 4, 3, 5, 2, 1]

unique_elements_from_list = np.unique(data_list)

print(“元のリスト:”, data_list)
print(“一意な要素 (リストから):”, unique_elements_from_list)
“`

出力結果:

元のリスト: [1, 3, 2, 1, 4, 3, 5, 2, 1]
一意な要素 (リストから): [1 2 3 4 5]

文字列の配列に対しても同様に使えます。

“`python
string_data = np.array([“apple”, “banana”, “apple”, “orange”, “banana”, “apple”])

unique_strings = np.unique(string_data)

print(“元の文字列配列:”, string_data)
print(“一意な文字列:”, unique_strings)
“`

出力結果:

元の文字列配列: ['apple' 'banana' 'apple' 'orange' 'banana' 'apple']
一意な文字列: ['apple' 'banana' 'orange']

文字列の場合も、アルファベット順にソートされて返されます。

戻り値の形式

numpy.uniqueは、デフォルトでは一意な要素を含む1つの配列だけを返します。しかし、後述するreturn_index, return_inverse, return_countsといった引数をTrueに設定すると、複数の値をタプルとして返します。返されるタプルの要素の順序は決まっており、以下のようになります。

(unique_elements, indices, inverse_indices, counts)

ここで、unique_elementsは常に最初の要素として含まれ、他の要素はそれぞれの引数がTrueに設定された場合にのみ含まれます。例えば、return_index=Truereturn_counts=Trueを指定した場合、返り値は (unique_elements, indices, counts) のタプルになります。

この戻り値の形式を理解しておくことは、unique関数を応用する上で非常に重要です。

numpy.uniqueの引数徹底解説

numpy.uniqueの真価は、その豊富なオプション引数にあります。これらの引数を活用することで、重複除去だけでなく、データに関する追加情報を効率的に取得できます。

return_index=True

この引数をTrueに設定すると、返される一意な配列の各要素が、元の配列(ar)で最初に出現する位置のインデックスを格納した配列も一緒に返されます。これにより、「このユニークな値は、元の配列のどこで最初に見つかったか?」を知ることができます。

“`python
import numpy as np

data = np.array([1, 3, 2, 1, 4, 3, 5, 2, 1])

return_index=True を指定

unique_elements, indices = np.unique(data, return_index=True)

print(“元の配列:”, data)
print(“一意な要素:”, unique_elements)
print(“最初の出現位置のインデックス:”, indices)

インデックスを使って元の配列から要素を取り出してみる (一意な要素と一致するはず)

print(“インデックスに対応する元の配列の要素:”, data[indices])
“`

出力結果:

元の配列: [1 3 2 1 4 3 5 2 1]
一意な要素: [1 2 3 4 5]
最初の出現位置のインデックス: [0 2 1 4 6]
インデックスに対応する元の配列の要素: [1 2 3 4 5]

出力結果を見てみましょう。

  • unique_elements: [1 2 3 4 5]
  • indices: [0 2 1 4 6]

これは何を意味するかというと:

  • 一意な要素 1 は、元の配列 data のインデックス 0 で最初に出現します (data[0] は 1)。
  • 一意な要素 2 は、元の配列 data のインデックス 2 で最初に出現します (data[2] は 2)。
  • 一意な要素 3 は、元の配列 data のインデックス 1 で最初に出現します (data[1] は 3)。
  • 一意な要素 4 は、元の配列 data のインデックス 4 で最初に出現します (data[4] は 4)。
  • 一意な要素 5 は、元の配列 data のインデックス 6 で最初に出現します (data[6] は 5)。

このように、indices配列は、ソートされた一意な要素の順序に対応して、それぞれが元の配列で初めて登場するインデックスを示しています。

return_inverse=True

この引数をTrueに設定すると、元の配列(ar)の各要素が、返される一意な配列のどの位置(インデックス)に対応するかを示すインデックスを格納した配列も一緒に返されます。これにより、「元の配列のこの要素は、ユニークな要素リストの中のどれに対応するか?」を知ることができます。これは、元の配列をユニークな値に基づいて「エンコーディング」するような処理に使えます。

“`python
import numpy as np

data = np.array([1, 3, 2, 1, 4, 3, 5, 2, 1])

return_inverse=True を指定

unique_elements, inverse_indices = np.unique(data, return_inverse=True)

print(“元の配列:”, data)
print(“一意な要素:”, unique_elements)
print(“inverse_indices:”, inverse_indices)

inverse_indices を使って一意な要素配列から要素を取り出してみる (元の配列と一致するはず)

print(“inverse_indices に対応する一意な要素:”, unique_elements[inverse_indices])
“`

出力結果:

元の配列: [1 3 2 1 4 3 5 2 1]
一意な要素: [1 2 3 4 5]
inverse_indices: [0 2 1 0 3 2 4 1 0]
inverse_indices に対応する一意な要素: [1 3 2 1 4 3 5 2 1]

出力結果を見てみましょう。

  • unique_elements: [1 2 3 4 5]
  • inverse_indices: [0 2 1 0 3 2 4 1 0]

これは何を意味するかというと:

  • 元の配列 data の最初の要素 1 は、一意な要素 [1 2 3 4 5] のインデックス 0 の要素 (1) に対応します。inverse_indices[0]0 です。
  • 元の配列 data の2番目の要素 3 は、一意な要素 [1 2 3 4 5] のインデックス 2 の要素 (3) に対応します。inverse_indices[1]2 です。
  • 元の配列 data の3番目の要素 2 は、一意な要素 [1 2 3 4 5] のインデックス 1 の要素 (2) に対応します。inverse_indices[2]1 です。
  • …といった具合に、inverse_indices の各要素は、元の配列の同じ位置の要素が、一意な要素配列 unique_elements のどこにあるかを示しています。

したがって、unique_elements[inverse_indices] は元の配列 data を完全に再現します。これは、元の配列を数値カテゴリカルデータのように扱う際に非常に便利です。

return_counts=True

この引数をTrueに設定すると、返される一意な配列の各要素が、元の配列(ar)で何回出現するかを格納した配列も一緒に返されます。これにより、各ユニークな値の「頻度」や「カウント」を簡単に知ることができます。

“`python
import numpy as np

data = np.array([1, 3, 2, 1, 4, 3, 5, 2, 1])

return_counts=True を指定

unique_elements, counts = np.unique(data, return_counts=True)

print(“元の配列:”, data)
print(“一意な要素:”, unique_elements)
print(“出現回数:”, counts)
“`

出力結果:

元の配列: [1 3 2 1 4 3 5 2 1]
一意な要素: [1 2 3 4 5]
出現回数: [3 2 2 1 1]

出力結果を見てみましょう。

  • unique_elements: [1 2 3 4 5]
  • counts: [3 2 2 1 1]

これは、一意な要素のそれぞれの出現回数を示しています。

  • 要素 1 は、元の配列に 3 回出現します。
  • 要素 2 は、元の配列に 2 回出現します。
  • 要素 3 は、元の配列に 2 回出現します。
  • 要素 4 は、元の配列に 1 回出現します。
  • 要素 5 は、元の配列に 1 回出現します。

合計の出現回数は 3 + 2 + 2 + 1 + 1 = 9 となり、これは元の配列 data の要素数と一致します。これは、データ内の各カテゴリの頻度集計など、データ分析で非常に役立つ情報です。Pandasのvalue_counts()に近い機能ですが、NumPy配列に対して直接適用できる点が異なります。

複数の引数を組み合わせる

return_index, return_inverse, return_countsは、複数同時にTrueに設定することも可能です。その場合、戻り値は指定した引数に対応する情報のタプルとなります。タプル内の要素の順序は常に (unique_elements, indices, inverse_indices, counts) です(指定されなかった要素は含まれません)。

例えば、一意な要素、最初の出現位置、そして出現回数を同時に取得したい場合:

“`python
import numpy as np

data = np.array([1, 3, 2, 1, 4, 3, 5, 2, 1])

return_index=True と return_counts=True を指定

unique_elements, indices, counts = np.unique(data, return_index=True, return_counts=True)

print(“元の配列:”, data)
print(“一意な要素:”, unique_elements)
print(“最初の出現位置のインデックス:”, indices)
print(“出現回数:”, counts)
“`

出力結果:

元の配列: [1 3 2 1 4 3 5 2 1]
一意な要素: [1 2 3 4 5]
最初の出現位置のインデックス: [0 2 1 4 6]
出現回数: [3 2 2 1 1]

このように、1回の関数呼び出しで必要な情報をまとめて取得できるため、効率的です。

全ての引数をTrueにした場合の例:

“`python
import numpy as np

data = np.array([1, 3, 2, 1, 4, 3, 5, 2, 1])

unique_elements, indices, inverse_indices, counts = np.unique(data,
return_index=True,
return_inverse=True,
return_counts=True)

print(“元の配列:”, data)
print(“一意な要素:”, unique_elements)
print(“最初の出現位置のインデックス:”, indices)
print(“inverse_indices:”, inverse_indices)
print(“出現回数:”, counts)
“`

出力結果:

元の配列: [1 3 2 1 4 3 5 2 1]
一意な要素: [1 2 3 4 5]
最初の出現位置のインデックス: [0 2 1 4 6]
inverse_indices: [0 2 1 0 3 2 4 1 0]
出現回数: [3 2 2 1 1]

タプルでの戻り値の順序が (unique_elements, indices, inverse_indices, counts) であることを覚えておけば、どの引数をTrueにしても正しく結果を受け取ることができます。

多次元配列での使い方

numpy.uniqueは多次元配列に対しても適用できますが、デフォルトの挙動と、axis引数を使った場合の挙動は異なります。

デフォルトの挙動(フラット化)

axis=None(デフォルト)の場合、numpy.uniqueは入力された多次元配列をまず1次元配列にフラット化します。そのフラット化された配列に対して重複除去とソートを行います。

“`python
import numpy as np

2次元配列

data_2d = np.array([[1, 2, 3],
[4, 5, 6],
[1, 2, 3],
[7, 8, 9]])

axis=None (デフォルト) で unique を適用

unique_elements_flat = np.unique(data_2d)

print(“元の2次元配列:\n”, data_2d)
print(“フラット化された一意な要素:”, unique_elements_flat)
“`

出力結果:

元の2次元配列:
[[1 2 3]
[4 5 6]
[1 2 3]
[7 8 9]]
フラット化された一意な要素: [1 2 3 4 5 6 7 8 9]

元の2次元配列 [[1, 2, 3], [4, 5, 6], [1, 2, 3], [7, 8, 9]] をフラット化すると [1, 2, 3, 4, 5, 6, 1, 2, 3, 7, 8, 9] となります。この1次元配列に対してuniqueを適用した結果が [1 2 3 4 5 6 7 8 9] です。これは、配列内の個々の要素レベルでの重複除去です。

axis引数の使用(NumPy 1.13.0以降)

特定の軸に沿って重複を判定したい場合、axis引数を使用します。例えば、2次元配列において「重複する行」や「重複する列」を見つけたい場合です。axisを指定すると、uniqueはその軸に沿った「スライス」(2次元配列なら行または列)を単位として重複を判定します。

axis=0 (一意な行を抽出)

2次元配列で axis=0 を指定すると、各行が1つの要素として扱われ、重複する行が除去されます。

“`python
import numpy as np

data_2d = np.array([[1, 2, 3],
[4, 5, 6],
[1, 2, 3],
[7, 8, 9]])

axis=0 で unique を適用 (一意な行)

unique_rows = np.unique(data_2d, axis=0)

print(“元の2次元配列:\n”, data_2d)
print(“一意な行:\n”, unique_rows)
“`

出力結果:

元の2次元配列:
[[1 2 3]
[4 5 6]
[1 2 3]
[7 8 9]]
一意な行:
[[1 2 3]
[4 5 6]
[7 8 9]]

元の配列には行 [1, 2, 3] が2回出現しています。axis=0を指定したuniqueは、この重複する行を取り除き、一意な3つの行 [1, 2, 3], [4, 5, 6], [7, 8, 9] を含む新しい2次元配列を返します。行の順序は、NumPyが行をどのように比較・ソートするかに依存します。デフォルトでは最初の要素から順に比較されます。

axis=1 (一意な列を抽出)

2次元配列で axis=1 を指定すると、各列が1つの要素として扱われ、重複する列が除去されます。

“`python
import numpy as np

data_2d_cols = np.array([[1, 4, 1, 7],
[2, 5, 2, 8],
[3, 6, 3, 9]])

axis=1 で unique を適用 (一意な列)

unique_cols = np.unique(data_2d_cols, axis=1)

print(“元の2次元配列:\n”, data_2d_cols)
print(“一意な列:\n”, unique_cols)
“`

出力結果:

元の2次元配列:
[[1 4 1 7]
[2 5 2 8]
[3 6 3 9]]
一意な列:
[[1 4 7]
[2 5 8]
[3 6 9]]

元の配列には列 [1, 2, 3] が2回出現しています。axis=1を指定したuniqueは、この重複する列を取り除き、一意な3つの列 [1, 2, 3], [4, 5, 6], [7, 8, 9] を含む新しい2次元配列(転置されたような形で見える)を返します。列の順序も同様にソートされます。

axis引数と他のreturn_*引数の組み合わせ

axis引数を使用する場合でも、return_index, return_inverse, return_countsを組み合わせて使用できます。ただし、これらの引数が返すインデックスやカウントは、「指定された軸に沿ったスライス」(例えば行や列)を単位とした情報になります。

例:一意な行とその出現回数を取得

“`python
import numpy as np

data_2d = np.array([[1, 2, 3],
[4, 5, 6],
[1, 2, 3],
[7, 8, 9],
[4, 5, 6]]) # 行 [4, 5, 6] も重複させてみる

axis=0 と return_counts=True を指定

unique_rows, counts = np.unique(data_2d, axis=0, return_counts=True)

print(“元の2次元配列:\n”, data_2d)
print(“一意な行:\n”, unique_rows)
print(“各一意な行の出現回数:”, counts)
“`

出力結果:

元の2次元配列:
[[1 2 3]
[4 5 6]
[1 2 3]
[7 8 9]
[4 5 6]]
一意な行:
[[1 2 3]
[4 5 6]
[7 8 9]]
各一意な行の出現回数: [2 2 1]

出力結果を見ると、行 [1, 2, 3] が2回、行 [4, 5, 6] が2回、行 [7, 8, 9] が1回出現していることが、counts 配列によって正しく示されています。counts配列の要素の順序は、unique_rows配列の行の順序に対応します。

return_indexを指定した場合、返されるインデックスは、一意な行が元の配列の何行目(axis=0の場合)で最初に出現したかを示します。
return_inverseを指定した場合、返されるインデックス配列は、元の配列の各行が、一意な行の配列(unique_rows)のどの行に対応するかを示します。

多次元配列とaxis引数の組み合わせは、構造化されたデータの重複行/列の特定や、ユニークなデータレコードの抽出に非常に強力です。

ソートについて

numpy.uniqueによって返される一意な要素の配列は、デフォルトでソートされます。

  • 数値の場合: 昇順(小さい順)
  • 文字列の場合: 辞書順(アルファベット順)

このソートは、重複除去の内部アルゴリズムの一部として効率的に行われることが多いです。具体的には、配列をソートすることで重複する要素が隣り合い、重複の検出と除去が容易になります。

ソート順序はデータ型に依存します。例えば、浮動小数点数の配列では数値としてソートされます。

“`python
import numpy as np

float_data = np.array([1.1, 2.2, 1.1, 3.3, 2.2])
unique_floats = np.unique(float_data)
print(“浮動小数点数の一意な要素:”, unique_floats)

string_data = np.array([“banana”, “apple”, “orange”, “apple”])
unique_strings = np.unique(string_data)
print(“文字列の一意な要素:”, unique_strings)
“`

出力結果:

浮動小数点数の一意な要素: [1.1 2.2 3.3]
文字列の一意な要素: ['apple' 'banana' 'orange']

数値は昇順に、文字列はアルファベット順にソートされていることが確認できます。

ソートは通常、uniqueの処理時間のかなりの部分を占めます。特に非常に大きな配列で、ソートが不要であるにも関わらずuniqueを使用すると、パフォーマンスのボトルネックになる可能性があります。

NumPy 1.24.0以降では、sort引数が追加され、ソートを行わないオプション(sort='None')が提供されています。これにより、ソートが不要な場合にパフォーマンスを向上させることが可能になりました。ただし、このオプションを使うと、結果の要素の順序は入力配列での出現順序に依存するようになります。

“`python
import numpy as np

data = np.array([1, 3, 2, 1, 4, 3, 5, 2, 1])

NumPy 1.24.0以降でのみ有効なオプション

unique_no_sort = np.unique(data, sort=’None’)

print(“ソートなしの一意な要素 (NumPy >= 1.24):”, unique_no_sort)

print(“順序は入力配列での出現順序に依存します”)

以前のバージョンや、ソートが必要な場合はデフォルトの挙動を使う

unique_sorted = np.unique(data)
print(“デフォルトのソートありの一意な要素:”, unique_sorted)
“`

多くの一般的な用途ではデフォルトのソートされた結果で問題ないため、sort引数を意識する必要は少ないかもしれませんが、パフォーマンスがクリティカルな場面では検討する価値があります。本記事では、広く利用可能なデフォルトのソートされる挙動を中心に解説しています。

内部処理の概要とパフォーマンス

numpy.uniqueは、入力配列をソートすることによって重複を効率的に検出・除去します。大まかに言うと、内部的には以下のようなステップで処理が進むと考えられます(実装はバージョンによって異なる可能性がありますが、基本的なアプローチはソートに基づいています)。

  1. フラット化とコピー: 多次元配列の場合はフラット化される。処理を元の配列に影響なく行うため、多くの場合、入力配列のコピーが内部的に作成される。
  2. ソート: コピーされた配列がソートされる。これにより、重複する要素が互いに隣り合って配置される。
  3. 一意な要素の抽出: ソートされた配列を走査し、隣接する要素と比較しながら、異なる要素(つまり一意な要素)を新しい配列にコピーしていく。
  4. 追加情報の収集 (オプション): return_index, return_inverse, return_countsが指定されている場合、ソートや走査の過程でこれらの情報を同時に収集・構築する。
    • return_index: ソート前の元の位置を追跡しながら、一意な要素が最初に出現したインデックスを記録。
    • return_inverse: ソートされた配列から一意な要素配列へのマッピングを利用し、元の位置に対応する一意な要素配列内のインデックスを計算。
    • return_counts: ソートされた配列を走査する際に、同じ値が連続して出現する回数をカウント。

パフォーマンスへの影響:

  • ソート: 処理時間の中で最も支配的な要因の一つです。ソートの計算量は通常 O(N log N) です(Nは要素数)。大きな配列の場合、このステップがボトルネックになりやすいです。
  • メモリ使用量: 入力配列のコピーや、return_index, return_inverse, return_countsで生成される追加の配列のために、元の配列と同等またはそれ以上のメモリを消費する可能性があります。非常に大きな配列を扱う際には、メモリ使用量に注意が必要です。
  • データ型: オブジェクト配列や複雑なデータ型の場合、要素の比較やソートに時間がかかる可能性があります。数値型の方が一般的に高速です。

パフォーマンスが非常に重要で、ソートが不要な場合は、NumPy 1.24.0以降のsort=Noneオプションが有効ですが、互換性に注意が必要です。また、代替手段として、標準ライブラリのset(ソートされず、ハッシュ可能な要素のみ)や、Pandasのunique/value_countsなども検討できます。しかし、NumPy配列を扱う上で、numpy.uniqueは多くのケースで十分高速かつ便利です。

unique関数のユースケース

numpy.unique関数は、様々なデータ処理や分析タスクで役立ちます。

  1. データクリーニング:
    • データセット内の重複したデータポイント(特に多次元配列の重複行)を特定し、削除する。
    • 例: 重複する顧客レコード、センサーの重複データログなどをフィルタリング。
  2. カテゴリカルデータの探索:
    • データセット内のカテゴリ変数に含まれるユニークな値(カテゴリの種類)をリストアップする。
    • 例: ユーザーの居住国のリストからユニークな国名を抽出、商品のカテゴリコードのリストからユニークなコードを把握。
    • return_counts=Trueと組み合わせることで、各カテゴリのデータ数を簡単に集計できる。
  3. データ集計と頻度分析:
    • データの各要素がどれくらいの頻度で出現するかを計算する。
    • 例: ウェブサイトへのアクセスログにおけるユニークなIPアドレスとそのアクセス回数、アンケート回答の選択肢とその回答者数を集計。
    • return_counts=Trueが直接的にこの情報を提供する。
  4. データ変換:
    • 元の配列を、ユニークな値のインデックスに変換する(エンコーディング)。
    • 例: 文字列のカテゴリデータを数値IDに変換する前処理。return_inverse=Trueがこの目的に利用できる。
  5. 集合演算の基礎:
    • NumPyには和集合(union1d)、共通部分(intersect1d)、差集合(setdiff1d)などを計算する関数がありますが、これらの多くは内部的にuniqueや類似のソートベースのアルゴリズムを利用しています。unique自体は、これらの演算の基本的な要素を提供します。

これらのユースケースにおいて、return_index, return_inverse, return_countsといったオプション引数は、単に重複を除去する以上の情報(出現位置、元の配列とのマッピング、頻度)を提供するため、非常に強力です。

uniqueを使う上での注意点

numpy.uniqueは便利ですが、使用する際にいくつか注意しておきたい点があります。

  1. 戻り値は常にソートされる: デフォルトでは、返される一意な要素の配列は常にソートされます(NumPy 1.24.0以降のsort=Noneを除く)。元の配列での出現順序を保持したい場合は、return_index=Trueと組み合わせて、元の配列からインデックスを使って要素を取り出すなどの工夫が必要です。
    “`python
    # 例: 元の配列の出現順序でユニークな要素を取得 (但し、最初に登場した順)
    import numpy as np
    data = np.array([1, 3, 2, 1, 4, 3, 5, 2, 1])
    unique_elements, indices = np.unique(data, return_index=True)

    indices はソートされた unique_elements に対応するインデックス

    出現順序で取得するには、元の配列でユニークな要素が最初に出現したインデックスを特定する必要がある

    unique はデフォルトでソートされるため、順序はソート順

    print(“デフォルトのunique (ソート順):”, unique_elements)

    元の順序で最初に登場するユニークな要素を抽出するには、別のロジックが必要

    例えば、Pythonのリストとforループを使うか、より複雑なNumPy処理

    例: Pythonのリストとin演算子で最初に登場する順に抽出

    seen = set()
    ordered_unique = []
    for x in data:
    if x not in seen:
    ordered_unique.append(x)
    seen.add(x)
    print(“元の順序で最初に登場するユニークな要素:”, ordered_unique)

    NumPyで同様のことを効率的に行うには、np.unique(…, return_index=True) の結果を indices でソートする

    unique_elements_ordered_indices = indices.argsort() # これは unique_elements のソート順を元に戻すインデックスではない

    正しい方法は、元の配列の要素が unique_elements のどこにマップされるか (inverse_indices) を利用するか、

    または np.unique(…, return_index=True) で得られた indices を使って元の配列から要素を取り出す

    ただし、unique はソートされるため、indices もソートされた unique_elements に対応している点に注意

    元の配列の要素が最初にユニークな値として出現した順序は、np.unique(…, return_index=True)[1] のソート順ではない。

    正しくは np.unique(…, return_index=True)[1] をソートすれば、元の配列での出現順で一意な要素を取り出せる。

    unique_elements_sorted_by_appearance = data[np.sort(indices)] # 間違い、indices は既にソートされたuniqueに対応

    正しい方法:np.unique は内部的にソートした上でユニーク化するため、元の順序は失われる。

    元の順序で最初の出現をリストアップするなら、return_index を使って元の配列から取り出すのが直感的だが、uniqueの結果はソートされている。

    例:元の配列の出現順で最初に登場するユニークな値をリストアップ

    _, first_occurrence_indices = np.unique(data, return_index=True)

    first_occurrence_indices は、ソートされた unique_elements が元の配列で最初に出現したインデックス

    これらのインデックスを元の配列での順序でソートし、対応する要素を取り出す

    indices_sorted_by_appearance = np.sort(first_occurrence_indices)
    unique_in_appearance_order = data[indices_sorted_by_appearance]
    print(“元の配列での出現順序でユニークな要素 (最初):”, unique_in_appearance_order)

    実行結果: [1 3 2 4 5] – これは最初の出現位置の要素だが、元の配列のユニーク要素出現順ではない。

    本当に元の配列の出現順でユニークな要素が欲しい場合は、np.unique(…, return_index=True) で得られた indices を使って、

    元の配列の要素を取り出し、その結果がソートされているため、元の順序を復元するのはuniqueだけでは難しい。

    やはり、元の配列を順に見ていくPython的なアプローチが最も直感的。

    NumPyで行うなら、 np.unique(data, return_index=True) で得られる indices を使って data[indices] とすることで

    一意な要素を元の配列での最初の出現位置から取り出せるが、uniqueの結果がソートされているため、この結果自体もソートされている。

    [1 2 3 4 5]

    本当に欲しいのは [1 3 2 4 5] のような順序なら、np.unique の結果を直接使うのは難しい。

    NumPy 1.24+ なら sort=’None’ でソートを回避できるが、順序は保証されない(多くの場合内部ソート前の順序に近い)。

    unique_no_sort = np.unique(data, sort=None) # 1.24+

    print(“NumPy 1.24+ でソートなし:”, unique_no_sort) # 結果は [1 3 2 4 5] や [1 2 3 4 5] など、実行環境依存になる可能性がある

    ``numpy.uniqueは本質的にソートを利用するため、元の配列の出現順序を厳密に保持したい場合は、uniqueの代わりにPython標準のsetを利用したり、より複雑なNumPy/Pandas操作を組み合わせたりする必要があります。np.unique(…, return_index=True)で得られるindices`は、ソートされたユニーク値に対応する元の配列における最初の出現インデックスであることに注意が必要です。このインデックス自体は元の配列の順序ではありません。

  2. 処理時間とメモリ使用量: 特に大規模な配列の場合、ソートと追加配列の生成により、処理に時間がかかり、多くのメモリを消費する可能性があります。パフォーマンスが重視される場合は、代替手段やアルゴリズムの検討が必要になることがあります。

  3. データ型の扱い: オブジェクト型(dtype=object)の配列に含まれる要素(例えばリストや辞書など)に対しても使用できますが、これらの要素が比較可能(orderable)である必要があります。また、ハッシュ可能でない要素は標準のsetでは扱えませんが、numpy.uniqueは比較可能であれば扱えます。ただし、オブジェクト配列の処理は一般的に数値配列より遅くなります。
  4. 浮動小数点数の比較: 浮動小数点数には精度に関する問題があり、見た目は同じでも内部的にわずかに異なる値として扱われ、ユニークと判定されることがあります。厳密な比較が必要な場合は、値を丸めるなどの前処理が必要になる場合があります。
  5. axis引数のバージョン: axis引数はNumPy 1.13.0で追加されました。それより古いバージョンのNumPyではaxisは使えません。古いバージョンで多次元配列の行/列のユニーク抽出を行うには、より手動な処理が必要になります(例: np.unique(data_2d, axis=0) の代わりに np.array([tuple(row) for row in data_2d]) を作ってから unique を適用するなど)。

これらの注意点を理解しておくことで、numpy.uniqueをより適切かつ効率的に使用できます。

関連するNumPy関数

numpy.uniqueと関連する機能を持つNumPy関数がいくつかあります。これらは集合演算を配列に対して行うものです。多くの場合、これらの関数も内部的にソートやユニーク化の処理を利用しています。

  • numpy.in1d(ar1, ar2): ar1 の各要素が ar2 に含まれているかどうかを示す真偽値配列を返します。NumPy 1.19で非推奨になり、代わりに numpy.isin を使用することが推奨されています。
  • numpy.isin(element, test_elements): element の各要素が test_elements に含まれているかどうかを示す真偽値配列を返します。in1d の後継です。内部で np.unique を利用している可能性があります。
  • numpy.intersect1d(ar1, ar2): 2つの配列の共通部分(積集合)に含まれる一意な要素をソートして返します。
  • numpy.union1d(ar1, ar2): 2つの配列の和集合に含まれる一意な要素をソートして返します。
  • numpy.setdiff1d(ar1, ar2): ar1 に含まれるが一意であり、かつ ar2 には含まれない要素をソートして返します。これは ar1 から ar1ar2 の共通部分を取り除いた集合に相当します。
  • numpy.setxor1d(ar1, ar2): 2つの配列の対称差(いずれか一方にのみ含まれる)に含まれる一意な要素をソートして返します。

これらの関数は、uniqueが提供する重複除去とソートの機能を基盤として、より高レベルな集合演算を実現しています。例えば、np.union1d(ar1, ar2) は概念的には np.unique(np.concatenate((ar1, ar2))) と似た処理を行います。

これらの関数も知っておくことで、配列を用いた集合演算の際に適切なツールを選択できます。

“`python
import numpy as np

arr1 = np.array([1, 2, 3, 2, 4])
arr2 = np.array([3, 4, 5, 4, 6])

和集合 (Union)

union = np.union1d(arr1, arr2)
print(“Union:”, union) # 出力: [1 2 3 4 5 6] (ソートされ重複なし)

共通部分 (Intersection)

intersection = np.intersect1d(arr1, arr2)
print(“Intersection:”, intersection) # 出力: [3 4] (ソートされ重複なし)

差集合 (Set difference)

setdiff = np.setdiff1d(arr1, arr2)
print(“Set Difference (arr1 – arr2):”, setdiff) # 出力: [1 2] (arr1 にのみ含まれるユニークな要素)

対称差 (Symmetric difference)

setxor = np.setxor1d(arr1, arr2)
print(“Symmetric Difference:”, setxor) # 出力: [1 2 5 6] (どちらか一方にのみ含まれるユニークな要素)

in1d / isin (要素の包含判定)

is_in_arr2 = np.isin(arr1, arr2)
print(f”Is {arr1} in {arr2}?:”, is_in_arr2) # 出力: [False False True True False]
“`

これらの関数を使う際にも、戻り値が常にソートされた一意な要素であることに注意が必要です。

他のPythonライブラリとの比較

NumPyのunique以外にも、Pythonで重複を除去する方法はいくつかあります。それぞれの特徴を理解しておくことが重要です。

標準Pythonの set

Python標準のデータ構造であるsetは、要素の集合を表し、自動的に重複が排除されます。

“`python
data_list = [1, 3, 2, 1, 4, 3, 5, 2, 1]

リストをsetに変換

unique_set = set(data_list)
print(“Unique elements using set:”, unique_set)

setをリストに戻す

unique_list = list(unique_set)
print(“Unique elements as list (order not guaranteed):”, unique_list)
“`

出力例:

Unique elements using set: {1, 2, 3, 4, 5}
Unique elements as list (order not guaranteed): [1, 2, 3, 4, 5] # 順序は実行環境やPythonバージョンに依存

NumPy uniqueset の比較:

特徴 numpy.unique 標準Python set
入力/出力 NumPy配列 任意のイテラブル / set オブジェクト
戻り値の型 NumPy配列 set オブジェクト
順序 デフォルトでソートされる 順序は保証されない(Python 3.7+ では挿入順だが、これは実装詳細に依存する可能性がある)
ハッシュ可能性 要素は比較可能であればOK (オブジェクト型も含む) 要素はハッシュ可能である必要がある (リストや辞書などの変更可能なオブジェクトは不可)
追加情報 return_index, return_inverse, return_counts で豊富に取得可能 基本的な重複除去のみ
パフォーマンス 数値配列に特化しており高速、特に大規模データ 一般的だが、NumPyの配列処理ほど数値計算に特化して最適化されていない
多次元対応 axis 引数で特定の軸に沿った重複除去が可能 1次元の要素に対して機能し、多次元構造は考慮されない

setはシンプルで高速ですが、戻り値の順序が保証されない点や、リストや辞書のようなハッシュ不可能なオブジェクトを直接扱えない点に注意が必要です。また、重複要素の出現位置や頻度といった追加情報を取得するには、別途処理が必要になります。NumPy配列を扱う場合は、numpy.uniqueの方がNumPyエコシステムとの親和性が高く、機能も豊富です。

Pandas の unique および value_counts

データ分析ライブラリであるPandasも、シリーズ(Series)やデータフレーム(DataFrame)に対する重複除去機能を提供しています。

  • Series.unique(): シリーズに含まれる一意な要素を、元のシリーズでの出現順序でNumPy配列として返します。欠損値NaNは1つのユニークな値として扱われます。
  • Series.value_counts(): シリーズに含まれる各ユニークな要素とその出現回数を、出現回数が多い順にソートされたシリーズとして返します。欠損値NaNはデフォルトではカウントされません。

“`python
import pandas as pd
import numpy as np

data_list = [1, 3, 2, 1, 4, 3, 5, 2, 1]
s = pd.Series(data_list)

Pandas Series.unique()

pandas_unique = s.unique()
print(“Pandas Series.unique():”, pandas_unique) # 出現順序

Pandas Series.value_counts()

pandas_value_counts = s.value_counts()
print(“Pandas Series.value_counts():\n”, pandas_value_counts) # 頻度順 (降順)
print(“Indices (unique values):”, pandas_value_counts.index.values)
print(“Values (counts):”, pandas_value_counts.values)

DataFrameの重複行除去は DataFrame.drop_duplicates() を使う

data_2d = [[1, 2, 3], [4, 5, 6], [1, 2, 3], [7, 8, 9]]
df = pd.DataFrame(data_2d)
df_unique_rows = df.drop_duplicates()
print(“Pandas DataFrame.drop_duplicates():\n”, df_unique_rows)
“`

出力結果例:

Pandas Series.unique(): [1 3 2 4 5]
Pandas Series.value_counts():
1 3
3 2
2 2
4 1
5 1
dtype: int64
Indices (unique values): [1 3 2 4 5]
Values (counts): [3 2 2 1 1]
Pandas DataFrame.drop_duplicates():
0 1 2
0 1 2 3
1 4 5 6
3 7 8 9

NumPy unique と Pandas の比較:

特徴 numpy.unique Pandas Series.unique() Pandas Series.value_counts() Pandas DataFrame.drop_duplicates()
対象 NumPy配列 Pandas Series Pandas Series Pandas DataFrame
戻り値の型 NumPy配列 (+タプルで追加情報) NumPy配列 Pandas Series (インデックスが一意値, 値がカウント) Pandas DataFrame
順序 デフォルトでソートされる 元のシリーズでの出現順序 出現回数が多い順 (デフォルト) 元のDataFrameでの出現順序を保持 (デフォルト)
追加情報 最初の出現インデックス、逆インデックス、カウント なし カウント なし (ただし keep 引数で最初/最後の重複を残す指定は可能)
欠損値(NaN) データ型による (NaNは float 型で比較可能) 1つのユニークな値として扱う デフォルトで無視、オプションでカウント NaNを含む行/列も重複判定の対象
多次元対応 axisで特定の軸に沿った重複判定が可能 Series (1次元データ) のみ Series (1次元データ) のみ DataFrame (行/列) に対応

Pandasはデータ分析に特化しており、Series.unique()は元の順序を保持する、value_counts()で頻度を計算する、DataFrame.drop_duplicates()で重複行を処理するなど、データ分析で頻繁に行われるタスクに適した機能を提供しています。Pandas DataFrameの重複行除去はuniqueとは直接異なりますが、概念的には重複を排除するという点では共通しています。

NumPyのuniqueは、Pandasに依存せずに純粋なNumPy配列に対して効率的に動作し、特にreturn_index, return_inverseといった低レベルなインデックス情報を提供できる点で独自の強みがあります。使用するツールは、対象データ(NumPy配列かPandasオブジェクトか)と、必要な機能(単なる重複除去か、順序保持か、頻度計算か、インデックス情報か)に応じて選択するのが良いでしょう。

詳細な使用例

これまでに解説した機能を組み合わせた、より実践的な使用例を見てみましょう。

例1: イベントログからユニークなユーザーと操作回数を集計

あるシステムで記録されたイベントログがあり、ユーザーIDの配列と操作内容の配列があるとします。

“`python
import numpy as np

サンプルデータ: ユーザーIDと操作内容

user_ids = np.array([‘userA’, ‘userB’, ‘userA’, ‘userC’, ‘userB’, ‘userA’, ‘userD’])
actions = np.array([‘login’, ‘view’, ‘logout’, ‘login’, ‘view’, ‘login’, ‘login’])

1. ユニークなユーザーIDをリストアップ

unique_users = np.unique(user_ids)
print(“ユニークなユーザー:”, unique_users)

2. 各ユーザーの操作回数を集計

unique_users_for_count, user_counts = np.unique(user_ids, return_counts=True)

np.uniqueはソートされるため、unique_users と unique_users_for_count は同じ内容でソート順も同じ

print(“\n各ユーザーの操作回数:”)
for user, count in zip(unique_users_for_count, user_counts):
print(f”{user}: {count}回”)

3. 特定のユーザーがログに初めて出現した位置を知る

unique_users_for_index, first_occurrence_indices = np.unique(user_ids, return_index=True)
print(“\nユーザーがログに初めて出現した位置 (インデックス):”)
for user, index in zip(unique_users_for_index, first_occurrence_indices):
print(f”{user}: インデックス {index}”)

4. 元の操作ログを、ユニークなユーザーIDに基づいた数値カテゴリに変換する

unique_users_for_inverse, inverse_indices = np.unique(user_ids, return_inverse=True)
print(“\n元のユーザーID配列を数値カテゴリに変換:”)
print(“ユニークなユーザーID (対応する数値):”, unique_users_for_inverse)
print(“変換された配列:”, inverse_indices)

変換された配列を使って元のユーザーIDを復元

print(“変換された配列からの復元:”, unique_users_for_inverse[inverse_indices])
“`

出力結果:

“`
ユニークなユーザー: [‘userA’ ‘userB’ ‘userC’ ‘userD’]

各ユーザーの操作回数:
userA: 3回
userB: 2回
userC: 1回
userD: 1回

ユーザーがログに初めて出現した位置 (インデックス):
userA: インデックス 0
userB: インデックス 1
userC: インデックス 3
userD: インデックス 6

元のユーザーID配列を数値カテゴリに変換:
ユニークなユーザーID (対応する数値): [‘userA’ ‘userB’ ‘userC’ ‘userD’]
変換された配列: [0 1 0 2 1 0 3]
変換された配列からの復元: [‘userA’ ‘userB’ ‘userA’ ‘userC’ ‘userB’ ‘userA’ ‘userD’]
“`

この例では、numpy.uniqueとそのオプション引数を活用して、ユニークなユーザーの特定、各ユーザーの行動回数の集計、ユーザーが初めて登場した位置の特定、そしてカテゴリカルデータの数値エンコーディングといった一連の分析を行っています。

例2: データポイントとその属性の重複除去と分析 (多次元配列)

センサーから取得したデータが、タイムスタンプと測定値のペアとして記録されているとします。完全に一致するペア(重複レコード)を除去し、ユニークなレコードとそれぞれの出現回数を知りたいとします。

“`python
import numpy as np

サンプルデータ: タイムスタンプと測定値のペア

[[timestamp1, value1], [timestamp2, value2], …]

sensor_data = np.array([
[1678886400, 25.1],
[1678886405, 25.3],
[1678886400, 25.1], # 重複
[1678886410, 25.5],
[1678886405, 25.3], # 重複
[1678886415, 25.8],
[1678886400, 25.1] # 重複
])

print(“元のセンサーデータ:\n”, sensor_data)

重複する行(レコード)を除去し、一意なレコードとその出現回数を取得

axis=0 を指定して行単位で unique を適用

unique_records, first_occurrence_indices, counts = np.unique(
sensor_data,
axis=0,
return_index=True,
return_counts=True
)

print(“\n一意なセンサーレコード:\n”, unique_records)
print(“\n各一意なレコードの最初の出現位置 (元の配列の行インデックス):”, first_occurrence_indices)
print(“\n各一意なレコードの出現回数:”, counts)

一意なレコードと出現回数を組み合わせて表示

print(“\n一意なレコードと出現回数:”)
for i in range(len(unique_records)):
record = unique_records[i]
count = counts[i]
index = first_occurrence_indices[i]
print(f”レコード: {record}, 出現回数: {count}, 最初に出現した行: {index}”)

元のデータを一意なレコードのインデックスに変換 (エンコーディング)

unique_records_for_inverse, inverse_indices = np.unique(sensor_data, axis=0, return_inverse=True)
print(“\n元のデータを一意なレコードのインデックスに変換:\n”, inverse_indices)
print(“変換されたデータからの復元(最初の5行):\n”, unique_records_for_inverse[inverse_indices][:5])
“`

出力結果:

“`
元のセンサーデータ:
[[1.6788864e+09 2.5100000e+01]
[1.6788864e+09 2.5300000e+01]
[1.6788864e+09 2.5100000e+01]
[1.6788864e+09 2.5500000e+01]
[1.6788864e+09 2.5300000e+01]
[1.6788864e+09 2.5800000e+01]
[1.6788864e+09 2.5100000e+01]]

一意なセンサーレコード:
[[1.6788864e+09 2.5100000e+01]
[1.6788864e+09 2.5300000e+01]
[1.6788864e+09 2.5500000e+01]
[1.6788864e+09 2.5800000e+01]]

各一意なレコードの最初の出現位置 (元の配列の行インデックス): [0 1 3 5]

各一意なレコードの出現回数: [3 2 1 1]

一意なレコードと出現回数:
レコード: [1.6788864e+09 2.5100000e+01], 出現回数: 3, 最初に出現した行: 0
レコード: [1.6788864e+09 2.5300000e+01], 出現回数: 2, 最初に出現した行: 1
レコード: [1.6788864e+09 2.5500000e+01], 出現回数: 1, 最初に出現した行: 3
レコード: [1.6788864e+09 2.5800000e+01], 出現回数: 1, 最初に出現した行: 5

元のデータを一意なレコードのインデックスに変換:
[0 1 0 2 1 3 0]
変換されたデータからの復元(最初の5行):
[[1.6788864e+09 2.5100000e+01]
[1.6788864e+09 2.5300000e+01]
[1.6788864e+09 2.5100000e+01]
[1.6788864e+09 2.5500000e+01]
[1.6788864e+09 2.5300000e+01]]
“`

この例では、axis=0を指定することで、タイムスタンプと測定値のペアである「行」を単位として重複除去を行っています。さらに、return_indexreturn_countsを同時に使うことで、ユニークなレコード自体、それが元のデータで最初に出現した位置、そしてそのレコードが何回出現したか、というデータクリーニングと基本集計に必要な情報を一度に効率良く取得しています。また、return_inverseで元のデータセットをユニークなレコードに基づく数値カテゴリとして表現する方法も示しています。

これらの詳細な例を通じて、numpy.uniqueがデータ処理や分析においていかに強力で柔軟なツールであるかが理解できたかと思います。

まとめ

本記事では、NumPyのnumpy.unique関数について、その基本的な機能から応用的な使い方まで、詳細に解説しました。

  • numpy.uniqueは配列の重複要素を除去し、ソートされた一意な要素の配列を返します。
  • return_index引数で、一意な要素が元の配列で最初に出現するインデックスを取得できます。
  • return_inverse引数で、元の配列の各要素が一意な配列のどこに対応するかを示すインデックスを取得でき、データの数値エンコーディングに利用できます。
  • return_counts引数で、各一意な要素の出現回数を取得でき、頻度分析に役立ちます。
  • axis引数(NumPy 1.13.0以降)を使用することで、多次元配列の特定の軸に沿った重複(例: 重複行)を除去できます。
  • デフォルトでは戻り値はソートされます(NumPy 1.24.0以降ではソートを省略するオプションもありますが、注意が必要です)。
  • numpy.uniqueは、データクリーニング、カテゴリカルデータの探索、頻度分析、データ変換など、幅広いユースケースで活躍します。
  • 標準PythonのsetやPandasの重複除去機能と比較することで、numpy.uniqueの強み(NumPy配列への特化、豊富な追加情報取得オプション、多次元配列のaxis指定)と、それぞれの使い分けが明確になります。

numpy.uniqueは、NumPyを使ったデータ処理において頻繁に登場する重要な関数です。本記事で解説した多様な機能と使い方を理解することで、あなたのPythonによるデータ処理スキルがさらに向上することでしょう。

データから重複を取り除き、必要な情報を効率的に引き出すために、ぜひnumpy.uniqueを積極的に活用してください。

参考資料


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール