Python
scipy
numpy
配列
数学

NumPyの配列ndarrayまとめ

Pythonで数値計算を行うための標準的なモジュールNumPyでは、ndarray(N-dimensional array、N次元配列)という、型付き高次元配列を表すオブジェクトが中心的な役割を果たしています。単にarray、配列とも言います。

この記事では、そんなNumPyのndarrayについてまとめてみました。

目次

参考文献


NumPyについて
以下では、NumPyをimport numpy as npでインポートしたものと考えてほしい。np.も適宜省略することにする。

  • SciPyはNumPyを含んでいるので、SciPyだけ入れてSciPyからNumPyに含まれるものを呼び出しても大丈夫

NumPyには別名関数(エイリアス)が色々あったり、同じことをするのにやたらと複数のやり方があったりして、設計がPythonの思想からちょっとずれてる感じがする。

  • 配列aのどのメソッドa.f(x)に対しても、同じ働きをする同名の関数np.f(a, x)がたいていある
  • 整数のタプルを引数とする関数は、たいていの場合は、可変個引数関数としてその整数たちをそのまま入れても使える。例えば配列aに対して、a.reshape((3, 4, 5))a.reshape(3, 4, 5)とも書ける

NumPyは色々と複雑な挙動、奇妙な挙動をする場合があるらしい。

高次元配列としてのndarray

ndarrayとは、型付き高次元配列だ。

  • 要素が高次元直方体の形に並んでる(多重ネストされたシーケンスで表されて、ネストの各深さにおいて、要素となっているシーケンスの長さが等しい)
  • 要素は全て同じ型

それぞれについて説明する。

高次元配列とは

ndarrayは、要素を高次元直方体上に並べた配列だ。どういうことか。

0次元配列は要素が0次元上に点の形に並んでいる配列、つまりただの数(スカラー)$a$ だ。

1次元配列は要素が1次元的に線分の形に並んでいる配列、つまり数列(ベクトル)

(a[0], a[1], \dots, a[n_0-1])

だ。これは

(a[i_0])_{\,0 \le i_0 < n_0}

とも書ける。

2次元配列は要素が2次元的に長方形の形に並んでいる配列、つまり行列

\left(
\begin{matrix}
a[0][0] & a[0][1] & \dots & a[0][n_1-1] \\
a[1][0] & a[1][1] & \dots & a[1][n_1-1] \\
\vdots & \vdots & \ddots & \vdots \\
a[n_0-1][0] & a[n_0-1][1] & \dots & a[n_0-1][n_1-1]
\end{matrix}
\right)

だ。これは

(a[i_0][i_1])_{\,0 \le i_0 < n_0,\, 0 \le i_1 < n_1}

とも書ける。つまり2重にネストされたシーケンスで、深さ1にある$n_0$本のシーケンスの長さは全て$n_1$。この長方形は $n_0\times n_1$ の形をしている。

3次元配列は要素が3次元的に直方体の形に並んでいる配列で、画面が2次元なので残念ながらここには表示できないけれど、

(a[i_0][i_1][i_2])_{\,0 \le i_0 < n_0,\, 0 \le i_1 < n_1, \, 0 \le i_2 < n_2}

とは書くことができる。つまり3重にネストされたシーケンスで、ネストの各深さにおいて、要素となっているシーケンスの長さが等しい(深さ1にあるシーケンスの長さは全て$n_1$、深さ2にあるシーケンスの長さは全て$n_2$)。この直方体は $n_0\times n_1 \times n_2$ の形をしている。

より一般に、$d$ 次元配列は要素が $d$ 次元的に高次元直方体(超直方体)の形に並んでいる配列で、

(a[i_0] \dots [i_{d-1}])_{\,0 \le i_0 < n_0, \, \dots, \, 0 \le i_{d-1} < n_{d-1}}

と書くことができる。つまり $d$ 次元配列とは、$d$ 重にネストされたシーケンスで、ネストの各深さにおいて、要素となっているシーケンスの長さが等しい(深さ $s$ にあるシーケンスの長さは全て$n_s$)ような配列のことである。この高次元直方体は $n_0\times n_1 \times\cdots\times n_{d-1}$ の形をしている。

$d$ 次元では空間軸は第 $0$ 軸から第 $d-1$ 軸まで $d$ 本あり、第 $i$ 軸目が $i$ 重目のネストによって表されることになる。つまり、軸の番号が若いほうが外側のネストで、一番内側のネストが最後の軸を表す。

配列を画面に表示した場合、一番内側のネスト、つまり最後の第 $d-1$ 軸方向が横に並ぶことになる。二番目に内側のネスト、つまり第 $d-2$ 軸方向が縦に上から下に並ぶ。残りの軸方向は、上から下に空白を挟みながら並べることで表わされる。

例:$3 \times 2 \times 4$の直方体上に並んだ3次元配列の表示

print(np.arange(3 * 2 * 4).reshape(3, 2, 4))

[[[ 0  1  2  3]
  [ 4  5  6  7]]

 [[ 8  9 10 11]
  [12 13 14 15]]

 [[16 17 18 19]
  [20 21 22 23]]]

まず、一番内側のネストである長さ4の第2軸目が横に並び、次に長さ2の第1軸目が縦に並び、余った長さ3の第0軸目は空白を挟みながら並べることで表現されている。

numpy内部にはC言語と互換性があるように定義された型がいろいろあって、int, float, complex, strといったPythonの組み込み型で入力しても、勝手にそれらに翻訳される。自分で型を指定することもできる。同じ整数でもint8int16など、データサイズの違う型が用意されている。

  • int{n}: 整数型。nは8, 16, 32, 64のいずれか
  • uint{n}: 符号なし整数型。nは8, 16, 32, 64のいずれか
  • float{n}: 浮動小数点型。nは16, 32, 64, 128のいずれか
  • complex{n}: 複素数型。nは64, 128, 256のいずれか

nはデータサイズをビット数を表す数字。Pythonの組み込み型は、デフォルトではint64, float64, complex128に翻訳される。文字列strは、適切な固定長の固定長文字列型に変換される。

  • a.astype(dtype): 配列adtype型に型変換した配列を返す

データ属性

  • a.ndim:  配列aの次元。上で言うところの $d$
  • a.shape: a.ndim個の要素からなるタプルで、各軸方向の長さ。高次元直方体の形を表す。上で言うところの $(n_0, \dots, n_{d-1})$
  • a.size: 全要素数。いわば高次元直方体の体積。a.shapeの要素の積 $n_0\cdots n_{d-1}$
  • a.dtype: 要素の型

配列の変形

※ビューとは、自身に要素を保持せずに元のオブジェクトへの参照を保っているだけの、イテラブルなオブジェクト。

  • a.reshape(shape): 配列aをshapeの形に整形しなおした配列のビューを返す。サイズ(shapeの要素の積)は元と同じである必要がある。可変引数関数としても使える

    • ビューを返すのではなくaのshapeを直接変更するには、a.shape = 3, 4, 5などとデータ属性に直接代入する
  • a.transpose(): 軸の順番をひっくり返した配列のビューを返す。例えば、shape (3, 4, 5, 6)の配列aをとると、a.transopse()のshapeは (6, 5, 4, 3) である。a.Tとも書ける。行列(2次元配列)に関しては、普通の意味での行列の転置を与える

    • より一般に、d次の置換(0からd-1までの数字を好きな順に並べたもの)をintのタプルsとして引数に入れることで、軸をどう置換するかを指定できる。a.transpose(s)は、i軸目が元の配列のs[i]軸目になっている配列のビューを返す。可変引数関数としても使える
  • a.swapaxes(ax1, ax2): 軸ax1と軸ax2を取り替えた配列のビューを返す

  • a.ravel(): 多重ネストを解いて1次元配列へ潰したビューを返す。コピーを返すにはa.flatten()を使う

配列の結合

  • concatenate((a, b, ...), axis=0): 配列たちa, b, ...axis軸方向に結合する。axis軸方向以外の長さは揃っている必要がある

実際は高々2次元の配列(行列)しか扱わないことも多いので、0軸方向(行)と1軸方向(列)に対しては、別の名前の関数が特別に用意されている

  • vstack(a, b, ...), r_[a, b, ...]: 配列たちを0軸(行)方向に結合する
  • hstack(a, b, ...), c_[a, b, ...]: 配列たちを1軸(列)方向に結合する
  • r_c_は名前ちょっとひどすぎやしませんか、

配列の分割

  • split(a, n, axis=0): 配列aaxis軸方向にn等分する
  • split(a, [x, y, ...], axis=0): 配列aを、axis軸方向のx番目、y番目、...の手前でそれぞれ分割する
  • 0軸(列)方向にはvsplit、1軸(列)方向にはhsplitという別名関数も用意されている

結果は配列のタプルで返ってくる。

配列の生成方法

たいていの場合、キーワード引数dtype=で型を指定できる。

  • array(s): シーケンスsから配列を作る。キーワード引数dtypeで要素の型も指定できるけど、省略しても型を勝手に推測してくれる。

sはリストやタプルや配列(混ざっていてもよい)の多重ネストで、ネストの各深さにおいて、要素となっているシーケンスの長さが等しいもの(つまり、要素を高次元直方体の形に並べられるもの)を使う。

あるshapeの配列を作る

  • zeros(shape): 全要素0の配列を作る
  • ones(shape): 全要素1の配列を作る
  • empty(shape): 値をとくに初期化せずに配列を作る。でたらめな値(典型的にはものすごく小さい値)が入っている

shapeと型が配列aと同じであるような配列を作る

  • zeros_like(a): 全要素0の配列を作る
  • ones_like(a): 全要素1の配列を作る
  • empty_like(a): 値をとくに初期化せずに配列を作る。でたらめな値(典型的にはものすごく小さい値)が入っている

等差数列を作る

  • arange(stop), arange(start, stop[, stride]): rangeと同じように使う。rangeとは違って、stridefloatでもよい
  • linspace(start, stop, num=50): 区間 [start, stop] から、両端を含むように等間隔にnum個取ってくる
  • logspace(start, stop, num=50, base=10.0): linspaceの等比級数版。区間 [base ** start, base ** stop] から、両端を含むようにlogの意味で等間隔にnum個取ってくる

行列(2次元配列)を作る

  • diag(v, k=0): 1次元配列vの要素が対角項に入った対角行列を作る。kで対角線の位置を上下にずらせる
  • eye(N, M=None, k=0): $N\times M$の単位行列を作る。$M$を省略すると$N$次正方行列を返す。kで対角線の位置を上下にずらせる

例: Jordan細胞行列を作る

print(np.eye(4, 4, 1) + np.diag((3, 4, 5, 6)))

[[ 3.  1.  0.  0.]
 [ 0.  4.  1.  0.]
 [ 0.  0.  5.  1.]
 [ 0.  0.  0.  6.]]

乱数配列

大規模データを扱う場合、Python標準のrandomモジュールよりもnumpy.randomモジュールのほうが効率的。作る配列のshapeを指定できるけれど、単一の値が欲しいときはshapeを省略する

  • np.random.random(shape): 半開区間 $[0, 1)$ 上の連続一様分布に従う乱数を要素とする配列を作る。可変引数関数でもある
  • np.random.randn(shape): 標準正規分布 $N(0, 1)$ に従う乱数を要素とする配列を作る。可変引数関数でもある
  • np.random.randint(low, high, shape): 区間 [low, high) の範囲の離散一様分布に従う整数値の乱数を返す。Python標準のrandom.randint(low, high)では終端highも範囲に含むが、numpyでは含まない

配列同士の演算とブロードキャスト

基本的には同じshapeの配列同士での演算が可能だけど、ブロードキャストと呼ばれる機能により、違うshapeの配列同士でも演算が可能になっている。

  • 配列に対しては論理演算and, or, notを使うことができない。代わりに &, |, ~を使う

同じshapeの配列同士の演算

shapeが同じ配列同士でスカラー演算を行うと、スカラー演算がベクトル化されて、各要素での演算結果からできた配列ができる。例えば、配列

a = (a[i_0] \dots [i_{d-1}])_{\,0 \le i_s < n_s}

と配列

b = (b[i_0] \dots [i_{d-1}])_{\,0 \le i_s < n_s}

との和 $a + b$ は、通常のベクトルや行列の和のように、各要素の和からなる配列

(\, a[i_0] \dots [i_{d-1}] + b[i_0] \dots [i_{d-1}] \, )_{\,0 \le i_s < n_s}

となる。

線形代数における通常の積との違い

同じように、配列の積 $a * b$ は要素同士の積からなる配列になる。行列(2次元配列)では、これはいわゆるHadamard積になっていて、通常の行列積とは違うことに注意。とくに、配列の積は要素ごとの積なので可換 $a * b = b * a$ だ。

通常の行列積 $ab$ を得るには、以下のどれかを使う:

  • a @ b (Python 3.5以降で可能。他より若干速い)
  • np.dot(a, b)
  • a.dot(b)

ベクトル(1次元配列)は左引数に入れると横ベクトル、右引数に入れると縦ベクトルと解釈される。

  • ベクトル $v$ と行列 $a$ に対し、v @ aは横ベクトルと行列の積 $v^T a$、a @ vは行列と縦ベクトルの積 $a v$ を与える
  • ベクトル $v$, $w$ に対し、v @ wは内積 $v\cdot w = v^\mathrm{T} w$ を与える

shapeが違う配列同士の演算

shapeが違う配列同士の演算を可能にする、ブロードキャスト機能について。どのような場合にブロードキャスト可能か理解するために、以下の2つの例を見てみよう。

まず、配列はただの数(スカラー)と演算ができる。例えば、配列

a = (a[i_0] \dots [i_{d-1}])_{\,0 \le i_s < n_s}

とスカラー $c$ との積 $c * a$ は、各要素のスカラー倍からなる配列

(c * a[i_0] \dots [i_{d-1}])_{\,0 \le i_s < n_s}

になる。これは、スカラー $c$ が、全要素が $c$ の配列

(c)_{\,0 \le i_s < n_s}

に水増しされたあとに、同じshapeの配列同士の演算が行われた結果と思える。

次の例。2次元配列(行列) $a$ に対して、各列に対して違う操作をしたいときがある。例えば、$1$列目は $v[1]$ 倍して、$2$列目は $v[2]$ 倍して、・・・のように、$j$ 列目を $v[j]$ 倍したいときがある。こういうときに次の演算が便利だ。shapeが $(n_0, n_1)$ の配列($n_0 \times n_1$行列)

a = (a[i_0][i_1])_{\,0 \le i_0 < n_0,\, 0 \le i_1 < n_1}

と、shapeが $(n_1)$ の配列(長さ $n_1$ のベクトル)

v = (v[i_1])_{\,0 \le i_0 < n_1}

との積 $a * v$ は、配列 $a$ の各 $j$ 列を $v[j]$ 倍した配列

\left(
\begin{matrix}
a[0][0]v[0] & a[0][1]v[1] & \dots & a[0][n_1-1]v[n_1-1] \\
a[1][0]v[0] & a[1][1]v[1] & \dots & a[1][n_1-1]v[n_1-1] \\
\vdots & \vdots & \ddots & \vdots \\
a[n_0-1][0]v[0] & a[n_0-1][1]v[1] & \dots & a[n_0-1][n_1-1]v[n_1-1]
\end{matrix}
\right)

つまり

(a[i_0][i_1] * v[i_1])_{\,0 \le i_0 < n_0,\, 0 \le i_1 < n_1}

になる。これは、ベクトル $v$ が、各行が同じ配列

\left(
\begin{matrix}
v[0] & v[1] & \dots & v[n_1-1] \\
\vdots & \vdots & \vdots & \vdots \\
v[0] & v[1] & \dots & v[n_1-1]
\end{matrix}
\right)

つまり

(v[i_1])_{\,0 \le i_0 < n_0,\, 0 \le i_1 < n_1}

に水増しされたあとに、同じshapeの配列同士の演算が行われた結果と思える。この計算では、$a$ の列数と $v$ の長さが一致している必要がある。

  • この配列の積 $a * v$ は通常の意味での行列とベクトルの積ではない。(行列とベクトルの積であったら、行列ではなくベクトルが返ってくるはずである)。とくに、配列の積は可換 $a * v = v * a$ である

ブロードキャスト機能はこういった演算を一般化したもので、各軸方向(ネストの各深さ)での配列の長さを比較して、一方の長さが1ならもう一方の長さにまで水増しすることで、配列のshapeをそろえてから計算する機能のことだ。

shape $(n_0,n_1)$ の2次元配列 $a$ とshape $(n_1)$ の1次元配列 $v$ との積の例で見たように、軸は一番最後の軸方向(一番内側のネスト)から比較していき、配列の次元が合っていない場合は、足りていない軸方向の配列の長さは全て自然に1と解釈される。この例では、shape $(n_1)$ の1次元配列が、shape $(1, n_1)$ の2次元配列(いわゆる列ベクトル)と解釈されている。

  • 例。shape $(2,3,1,5,1)$ の5次元配列aと、shape $(4,5,6)$ の3次元配列bは、ブロードキャストにより演算可能である。まず次元を合わせるために、bのshapeが $(1,1,4,5,6)$ と解釈される。そして各軸方向の長さを比較して、長さ1の場合はもう一方の長さにまで水増しすることで、abは同じshape $(2,3,4,5,6)$ にそろう

  • 一番最初の軸方向(一番浅いネスト)から比較して、shapeの後ろに1を入れる仕様のほうが自然で分かりやすかったんじゃないかな、と思う。例えば、shape $(2,3,4,5)$ とshape $(2,3)$ の積では後者を $(2,3,1,1)$ と解釈する、といったような。なぜか一番最後の軸方向(一番深いネスト)から比較して、shapeの前に1を入れる仕様になっている

配列同士の演算におけるブロードキャストの規則まとめ:

  1. shape の先頭に 1 を適宜加えることで次元 ndim を揃える
  2. 各軸方向で、長さが一致しているか一方の長さが1、であればブロードキャスト可能。長さが1の方向には同じ要素を詰めて、もう一方の長さにまで水増しすることでshapeを揃える
  3. shapeが同じ配列同士の演算結果(要素ごとの演算結果からなる配列)を返す

ベクトル演算

最初に例で挙げたように、スカラーは自然に0次元の配列と解釈されるので、ブロードキャスト機能によって配列とスカラーの2項演算は常にできる。配列の各要素に対してスカラーと2項演算した結果からなる配列を返す。例:

  • 1 / a: 全要素が逆数になった配列
  • a ** 2: 全要素を2乗した配列
  • a > x: 各要素がxより大きいかどうかのboolが入った配列

2項演算の結果はまた配列なので、2項演算はネストできる。例:

  • a % 3 == 0: 各要素が3で割り切れるかどうかのboolが入った配列

要素・部分配列へのアクセス

インデックス参照・スライシング

インデックス参照やスライスの書き方a[i], a[i:j], a[i:j:k](負値も可能、i,j省略可)は、リストやタプルといったPythonの組み込みシーケンスと同じ。

  • Pythonの基本的な組み込み型については、著者別記事のPythonの基本的な組み込み型とその一般論まとめを是非ご覧下さい(宣伝)

  • Pythonの組み込みシーケンスとは違って、スライスのネストa[:][-5][1:8:2]をカンマ区切りでa[:, -5, 1:8:2]とも書くことができ、こちらの方が高速なので普通こちらを使う。Pythonのsliceオブジェクトとタプルを使って、a[(slice(None), -5, slice(1, 8, 2))]と書くこともできる

  • もちろん全ての軸方向について範囲を指定する必要はない。4次元配列においてa[:5, 3:]a[:5, 3:, :, :]と同じ

  • Python のEllipsisオブジェクト...を使って:,の連続を表せる謎機能がある。例えば、6次元配列のx[1, :, :, :, 2:3]x[1, ..., 2:3, :]と書ける

Pythonの組み込みシーケンスとは違って、配列のスライスは新たにシーケンスを生成せず、もとのシーケンスへのビューを生成する。

  • ビューではなくコピーが作りたいときは、copyメソッドを使ってa[:5, 3:].copy()などとする

ビューは元の配列を参照しているだけなので、ビューの要素に代入すると元の配列が変更される。例:

a = np.arange(10)
a_slice = a[3:6]
a_slice[0] = 100
a

Out[1]:
array([  0,   1,   2, 100,   4,   5,   6,   7,   8,   9])

ビューオブジェクトa_sliceへの代入によって、配列aが変更されている。

リストとは違って、スライスにスカラーを代入できる(リストでできたのは、該当部分を別のイテラブルへ置き換えることだった)。該当する全要素へのそのスカラーを代入する。例:

a_slice[:] = -1
a

Out[2]:
array([ 0,  1,  2, -1, -1, -1,  6,  7,  8,  9])
a = np.arange(12).reshape(3, 2, 2)
a[1] = 0
print(a)

[[[ 0  1]
  [ 2  3]]

 [[ 0  0]
  [ 0  0]]

 [[ 8  9]
  [10 11]]]

ブールインデックス参照(マスキング)

真偽値の配列によって、配列のどの要素・部分配列にアクセスするかを指定することもできる。Trueの要素のみアクセスされる。この機能を使った場合、ビューではなく新しい配列が生成される。

例えば、shapeが(2, 3, 4, 5, 6)の配列aに対し、shapeが(3, 4)の真偽値の配列bを用いてa[1:, b, 3]といった範囲指定ができる。0軸目は1:、1, 2軸目がb, 3軸目が3、4軸目が全範囲、という範囲指定になる。例:

  • a[a < 0] = 0: 負の要素だけ全て0にする。(a < 0boolの配列になっている)
  • a[(a < 0) & (a % 3 == 0)]: boolの配列への論理演算には&, |, ~を使う

ファンシーインデックス参照

整数の配列やリストを使って各軸方向を指定することで、各要素が元の配列 $a$ の要素のどれかであるような、新しい配列 $b$ を何であれ作ることができる。$b$ の次元やshapeは好きに選べる。

  • ただし実際には、後で実例として示すような、行列から行や列を部分的に取ってくる操作にしかほとんど使わないかもしれない

この機能の一般的なルールはなかなか分かりづらいので、実例を見た方が速いと思うけれど、まず一般的なルールについて書いてみる。

配列 $b$ の各要素 $b[j_0,\dots,j_{d'-1}]$ が配列 $a$ の要素 $a[i_0,\dots,i_{d-1}]$ のどれになるかを指定するためには、各 $d'$ 次元位置ベクトル $(j_0,\dots,j_{d'-1})$ に対して $d$ 次元位置ベクトル $(i_0,\dots,i_{d-1})$ のどれかを対応づければよい。つまり、この $d$ 次元空間の各 $s$ 軸方向において、各 $d'$ 次元位置ベクトル $(j_0,\dots,j_{d'-1})$ に対してスカラー $i_s$ を与えればよい。これは、 $b$ と同じshapeの整数配列 $f_s$ と見なせる。ファンシーインデックス機能では、配列 $a$ の各 $s$ 軸方向にこの配列 $f_s$ を入れて

  a[f_0,\dots,f_{d-1}]

と書くことで、望みの配列 $b$ を生成できる。

  • $f = [f_0,\dots,f_{d-1}]$ は、$d'$ 次元位置ベクトルを入れると $d$ 次元位置ベクトルを返す写像 $f(j_0,\dots,j_{d'-1}) = (i_0,\dots,i_{d-1})$ と思える。この機能はつまり、そのような写像 $f$ を指定することで、 $b[j_0,\dots,j_{d'-1}] = a[f(j_0,\dots,j_{d'-1})]$ となる配列 $b$ を作る機能である

  • aに入れた配列たち $f_s$ のshapeが合っていない場合、ブロードキャストしてshapeをそろえようとする

  • 全ての軸方向に配列を入れる必要はない。配列を入れなかった方向については、元の配列aと同じになる

実例を見てみよう。

In [1]:
a = np.arange(12).reshape(3, 4)
a

Out[1]:
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])

この配列を使って、いくつか例を挙げる。

In [2]:
a[[0, 0, 1, 0, 2], [3, 2, 2, 3, 0]]

Out[2]:
array([3, 2, 6, 3, 8])

引数に[0, 0, 1, 0, 2], [3, 2, 2, 3, 0]を入れることで、配列 [a[0,3], a[0,2], a[1,2], a[0,3], a[2,0]] が返ってきている。

In [3]:
a[[[0, 0], [1, 0]], [[3, 2], [2, 3]]]

Out[3]:
array([[3, 2],
       [6, 3]])

引数に[[0, 0], [1, 0]], [[3, 2], [2, 3]]を入れることで、配列 [[a[0,3], a[0,2]], [a[1,3], a[0,3]]] が返ってきている。

In [4]:
a[:, [3, 2, 2, 3, 0]]

Out[4]:
array([[ 3,  2,  2,  3,  0],
       [ 7,  6,  6,  7,  4],
       [11, 10, 10, 11,  8]])

0軸方向(行)に配列を入れなかったので、行方向は元の配列aと同じになっている。1軸方向(列)からは、3列目、2列目、2列目、3列目、0列目の順に取ってきている。これは便利だ。

このように行列から行や列を部分的に取ってくる操作にしか、このファンシーインデックス機能はほとんど使わないかもしれない。

複数の軸方向についてこのように取捨選択したい場合は、単にネストすればよい。

In [5]:
a[[1, 0]] [:, [3, 2, 2]]

Out[5]:
array([[7, 6, 6],
       [3, 2, 2]])

0軸方向(行)からは1行目、0行目の順に取り、1軸方向(列)からは3行目、2行目、2行目の順に取ってきている。

別の例: one-of-K表現を作る(参考: NumPyのeyeまたはidentityでone-hot表現に変換

In [1]:
np.eye(5)[[1, 4, 2]]

Out[1]:
array([[ 0.,  1.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  1.],
       [ 0.,  0.,  1.,  0.,  0.]])

K = 5において、1, 4, 2に対応するone-of-K表現ベクトルを作っている

配列に対するさまざまな操作

軸の追加

np.newaxisを使うことで、既にある軸と軸の間に新しい軸を追加した配列のビューを作ることができる(その軸方向の長さは1である)。例えば、shape (3, 4, 5)の配列aに対して、a[:, :, np.newaxis]はshape (3, 4, 1, 5)の配列のビューを返す。

ブロードキャスト機能をうまく使うために必要なときがある。

  • 例。shape $(n_0, n_1)$ の配列aはshape $(n_1)$ の配列vとそのままブロードキャスト演算可能であった。ここで、vはshape $(1, n_1)$ の配列(いわゆる列ベクトル)と解釈される。一方、aはshape $(n_0)$ の配列vとはそのままではブロードキャスト演算できない。v[:, np.newaxis]として第1軸を追加し、vをshape $(n_0, 1)$ の配列(いわゆる行ベクトル)にする必要がある

  • np.newaxisの中身は、実はただのNonenp.newaxis is None)。Noneでも同じことができるけど、可読性のためだけにnp.newaxisと書いている

ユニバーサル関数 (ufunc)

配列を入れると各要素に対して関数適用した結果の配列を返すような、ベクトル化された関数がnumpyに用意されている。例:

  • abs, sign
  • rint(偶数丸め), ceil, floor
  • sqrt, square
  • exp, exp2, log, log2, log10
  • sin, cosh, arctanh などの三角関数、双曲線関数、およびその逆関数
  • isnan, isfinite, isinf

配列aに対して、np.sin(a)などとして使う。各要素のsinの値が入った配列が返ってくる。

自分で定義した関数fをユニバーサル関数にしたいときは、np.vectorize(f)とする。

複数の引数を取るufuncもあるけれど、それらはshapeが同じ配列同士の2項演算として既に紹介したものでだいたい実現できる。例えば、np.add(a, b, c)は単にa + b + cと同じだ。2項演算子で表せないものは

  • maximum(a, b, ...), minimum(a, b, ...): 要素ごとに最大値、最小値を取ってくる
    • shapeが合わない場合はブロードキャストされる。例えばmaximum(a, 0)と書くとスカラー0がブロードキャストされて、配列aの各要素xに対し、各要素が $\max(x,0)$ からなる配列を返す
    • もしnanがあったらnanを取る。nanを無視するには、代わりにfmax(a, b, ...), fmin(a, b, ...)を使う

条件を満たすインデックスの一覧

  • a.nonzero(): ゼロでない要素のインデックスたちを配列で返す
  • a.where(): boolの配列aに対し、Trueである要素のインデックスたちを配列で返す
    • 各要素に対する3項演算子としても使えて、例えばa.where(a > 0, a, 0)でヒンジ関数みたいに使える

ソート

  • a.sort(axis=-1): axis軸方向にソートする。a.shape[axis]個の要素をソートする操作が、a.size / a.shape[axis]回行われる
    • a自身をソートするのではなく、ソートされたコピーを返したい場合はnp.sort(a, axis=-1)を使う

集約関数 (aggregate function)

キーワード引数axis=で集約する軸方向を指定できる。a.shape[axis]個の要素を集約する操作がa.size / a.shape[axis]回行われ、指定された軸方向は潰れる。例えば、shape (2,3,4,5)の配列の1軸目を集約すると、shape(2,4,5)の配列が返ってくる。

軸方向を特に指定しない場合、全要素が集約されてスカラーが返ってくる。

  • a.max(), min(): 要素の最大値、最小値
    • もしnanがあったらnanを取る。nanを無視するには、代わりにa.nanmax(), a.nanmin()を使う
    • 多項ユニバーサル関数のmaximum,minimumと名前が紛らわしい。maxはある配列内の要素の比較、maximumは複数の配列間の要素ごとの比較
  • a.argmax(), a.argmin(): 最大、最小の要素のインデックス
  • a.sum(), a.prod(): 要素の和、積
  • a.any(), a.all(): bool値の全和、全積
  • a.cumsum(), a.cumprod(): 要素の累積和、累積積
    • この場合は軸方向を潰さない
  • a.mean(), a.median(): 平均値、中央値
  • a.var(), a.std() : 標本分散、標本標準偏差
    • これは $\frac{1}{N}\sum_{i=1}^N(x_i-\bar{x})^2$ とその平方根のこと。真の平均からのずれではなく標本平均 $\bar{x}=\frac{1}{N}\sum_{i=1}^N x_i$ からのずれを見ているせいで、真の分散や標準偏差の推定量としては系統的に小さく見積もるバイアスがある。不偏推定量である、不偏分散 $\frac{1}{N-1}\sum_{i=1}^N(x_i-\bar{x})^2$ とその平方根である不偏標準偏差を得るには、a.var(ddof=1), a.std(ddof=1)とする

以上です。お読み頂いてありがとうございます。
どこか間違っているところ、怪しいところ、用語の使い方が変なところなどあるかもしれません。指摘、コメント、質問、感想など、何でもお待ちしております。
もし良いなと感じたら、いいねを押して頂けるととても嬉しいです。他の記事も書いてみようというモチベーションになります。