Python の NumPy 配列 (ndarray) の基本

NumPy 配列 (ndarray) とは

Python の NumPy ライブラリ (numpy) は、多次元配列を高速かつ便利に扱うための ndarray クラスを提供しています(n-dimensional array の略)。 ndarray は Python 標準のリストと比べて次のような違いがあります。

  • 内部実装に C 言語などの低レベル言語が利用されており、処理が高速でメモリ効率がよい
  • C 言語の配列と同様、単一のデータタイプ固定サイズ の配列である(サイズ変更は新しい ndarray の生成になる)
  • 全要素に対するブロードキャスト演算や、行列(ベクトル)演算など、便利な演算方法 が提供されている

NumPy はこのような特徴を持つため、次のように様々なライブラリで利用されています。

  • データ解析ライブラリ: Pandas
  • 画像処理ライブラリ: OpenCV
  • 科学計算ライブラリ: SciPy
  • 機械学習ライブラリ: TensorFlow、scikit-learn
  • プロットライブラリ: Matplotlib

つまり、ほとんどの数学的、科学的な処理を行うライブラリは NumPy を利用していると考えられます。

NumPy 配列を生成する(numpy.array 関数)

ndarray インスタンスは、numpy.array() 関数で生成することができます。 実際には、numpy ライブラリは np という別名でインポートするのが慣例となっているので、コード中では numpy.array() ではなく np.array() と記述されることが多いです。

1 次元の ndarray

次の例では、1 次元の ndarray インスタンスを生成しています。

1 次元の ndarray を生成
>>> import numpy as np
>>> a = np.array([1, 2, 3])
>>> a
array([1, 2, 3])
>>> type(a)
<class 'numpy.ndarray'>

多次元の ndarray

多次元の ndarray インスタンスも同様に生成できます。

2 次元(2 行 3 列)ndarray を生成
>>> a = np.array([[1, 2, 3], [4, 5, 6]])
>>> a
array([[1, 2, 3],
       [4, 5, 6]])

NumPy 配列の形状(各次元のサイズ)は、shape 属性を参照することで調べることができます。

ndarray の形状を調べる
>>> a.shape
(2, 3)  # 2 行 3 列を表すタプルが返される

行列として扱うには、各行のサイズ(列数)は揃えておく必要があります。 次のように、各行のサイズが異なるとエラーになります。

a = np.array([[1, 2, 3], [4, 5]])  # ValueError!

NumPy 配列は 1 つのデータタイプ (dtype) の要素のみを持つ

効率化のため、NumPy 配列に格納される要素の型(データタイプ)は統一されます。 データタイプは ndarray インスタンスの初期化時の要素の値によって自動的に設定されます。 ndarray インスタンスが保持している要素のデータタイプを調べるには、dtype 属性を参照します。

ndarray のデータタイプを調べる
>>> a = np.array([[1, 2, 3], [4, 5, 6]])  # 整数値のみで初期化
>>> a.dtype
dtype('int64')

>>> a = np.array([[1, 2, 3], [1.5, 2.5, 3.5]])  # 浮動小数点数を含む値で初期化
>>> a.dtype
dtype('float64')

データタイプは、NumPy 配列の生成時に dtype オプションで明示的に指定することができます。 次の例では、整数値のみで NumPy 配列を初期化していますが、浮動小数点数の要素として扱うように指定しています。

ndarray のデータタイプを指定する
>>> a = np.array([[1, 2, 3], [4, 5, 6]], dtype='float64')
>>> a.dtype
dtype('float64')
>>> a
array([[ 1.,  2.,  3.],
       [ 4.,  5.,  6.]])

NumPy 配列のインデックスアクセスとスライス

NumPy 配列 (ndarray) の各要素は、リストと同様にインデックスでアクセスできます。

a = np.array([[1, 2, 3], [4, 5, 6]])
print(a[0][2])  # 3

a[0][2] = 100   # 値の変更
print(a[0])     # [1 100 3]

print(len(a))     # 2
print(len(a[0]))  # 3

ndarray はリストよりも柔軟なスライスを行うことができます。

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

# 2 行目だけを取得
print(a[1])     # [4, 5, 6]
print(a[1, :])  # 同上

# 2 行目以降を取得
print(a[1:])     # [[4, 5, 6], [7, 8, 9]]
print(a[1:, :])  # 同上

# すべての行の 2 列目までを取得
print(a[:, :2])   # [[1, 2], [4, 5], [7, 8]]
print(a[:, :-1])  # 同上

# すべての行の列を 1 つ飛びで取得
print(a[:, ::2])  # [[1, 3], [4, 6], [7, 9]]

ndarray のスライスは、元の ndarray のデータの参照であることに注意してください。 スライス経由での値の変更は、スライス元の ndarray の変更を意味します。 一方で、Python 標準のリストのスライスは、Shallow Copy(一階層目だけコピー)です。

# NumPy 配列のスライスは元のデータを参照する
np_arr = np.array([1, 2, 3])
np_slice = np_arr[:]
np_slice[0] = 100
print(np_arr)  # [100, 2, 3]

# リストのスライスは Shallow Copy(一階層目はコピーによって作られた新しいデータ)
py_arr = [1, 2, 3]
py_slice = py_arr[:]
py_slice[0] = 100
print(py_arr)  # [1, 2, 3]

NumPy 配列に対する演算(ブロードキャスト)

NumPy 配列 (ndarray) やそのスライスに対してスカラー値(単一の値)の四則演算や代入操作を行うと、全ての要素に対してその演算 が実行されます(この仕組みを ブロードキャスト と呼びます)。 これは非常に強力な仕組みで、この仕組みをうまく活用することで、多くの計算をループを記述せずに実装することができます。

ndarray とスカラー値のブロードキャスト演算
a = np.array([[1, 2, 3], [4, 5, 6]])
a[0] = 10    # 1 行目の要素をすべて 10 にする
print(a)     # [[10, 10, 10], [4, 5, 6]]

a -= 1       # すべての要素をマイナス 1 する
print(a)     # [[9, 9, 9], [3, 4, 5]]

a[1] **= 2   # 2 行目の要素をすべて 2 乗する
print(a)     # [[9, 9, 9], [9, 16, 25]]

a[:, 1] = 0  # 全ての行の 2 列目の要素を 0 にする
print(a)     # [[9, 0, 9], [9, 0, 25]]

NumPy 配列 (ndarray) 同士の四則演算も、それぞれ対応する要素に対して演算が行われます。 ただし、形状の異なる NumPy 配列同士で演算しようとすると ValueError が発生します。

行列同士の四則演算
a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([[5, 5, 5], [2, 2, 2]])

c = a + b   # [[6, 7, 8], [6, 7, 8]]
c = a - b   # [[-4, -3, -2], [2, 3, 4]]
c = a * b   # [[5, 10, 15], [8, 10, 12]]
c = a / b   # [[0.2, 0.4, 0.6], [2.0, 2.5, 3.0]]
c = a // b  # [[0, 0, 0], [2, 2, 3]]
c = a % b   # [[1, 2, 3], [0, 1, 0]]
c = a ** b  # [[1, 32, 243], [16, 25, 36]]

特に、除算 (a / b) の結果は、float64 型のデータタイプになることに注意してください。

多次元の NumPy 配列と、1 次元の NumPy 配列の演算でもブロードキャストの仕組みが働きます。 列のサイズは等しくないといけません。

行列とベクトルの四則演算
a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
b = np.array([1, 2, 3])

c = a + b   # [[2, 4, 6], [5, 7, 9], [8, 10, 12]]
c = a - b   # [[0, 0, 0], [3, 3, 3], [6, 6, 6]]
c = a * b   # [[1, 4, 9], [4, 10, 18], [7, 16, 27]]
c = a / b   # [[1.0, 1.0, 1.0], [4.0, 2.5, 2.0], [7.0, 4.0, 3.0]]
c = a // b  # [[1, 1, 1], [4, 2, 2], [7, 4, 3]]
c = a % b   # [[0, 0, 0], [0, 1, 0], [0, 0, 0]]
c = a ** b  # [[1, 4, 27], [4, 25, 216], [7, 64, 729]]

行列演算(内積、外積、転置)

NumPy 配列 (ndarray) には、行列の内積、外積、転置などの演算を行うためのメソッドが用意されています。

内積 (dot)
a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([1, 2, 3])

result = a.dot(b)  # array([14, 32])
外積 (cross)
a = np.array([1, 2, 3])
b = np.array([10, 10, 10])

result = np.cross(a, b)  # array([-10,  20, -10])
転置 (transpose)
a = np.array([[1, 2, 3], [4, 5, 6]])

result = a.transpose()  # array([[1, 4], [2, 5], [3, 6]])

ちなみに、各メソッドの引数としては、ndarray インスタンスではなく、リストやタプルも渡せるようになっていますが、戻り値は結局 ndarray になります。

result = np.cross([1, 2, 3], (10, 10, 10))  # array([-10, 20, -10])