3  NumPy で配列を扱う

NumPy は,数値計算を効率的に行うためのライブラリであり,多次元配列を扱うことができる.本書では,ベクトルや行列の計算,データの集計などに用いる.

通常は,次のようにインポートし,np という別名を付けて使う.

import numpy as np

以降のコードでは,このインポートがすでに行われているものとする.

3.1 配列の作成

3.1.1 np.array() 関数

配列は,np.array() 関数を使って作成する.次のコードは,1次元配列を作成する例である.1次元配列は,線形代数におけるベクトルを表すために使う.

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

次のコードは,2次元配列を作成する例である.2次元配列は,線形代数における行列を表すために使う.

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

NumPy 配列で要素が並ぶ各方向を軸(axis)という.ndim は軸の数,すなわち配列の次元数を表す.ここでいう次元数は,ベクトルの成分数とは異なる.例えば,上の a は5次元ベクトルを表せるが,配列としては1次元である.

配列の基本的な属性には,ndim のほかに,shape,size,dtype がある.shape は各軸の長さを並べた組,size は配列全体の要素数,dtype は要素のデータ型を表す.NumPy 配列では,すべての要素が共通のデータ型で格納される.

print(b.ndim)   # 2
print(b.shape)
print(b.size)   # 6
print(b.dtype)
2
(2, 3)
6
int64

b は2次元配列であり,2行3列であるため,shape は (2, 3) となる.

ノート0次元配列

一つの数値を np.array() に渡すと,スカラーを格納した0次元配列になる.軸を持たないため,ndim は 0 となる.

scalar = np.array(5)
print(scalar)
print(scalar.ndim)  # 0
5
0

練習 3.1 np.array() を使って,次の行列に対応する2次元配列を作成し,表示せよ.

\begin{bmatrix} 1 & 2 \\ 3 & 4 \end{bmatrix}

3.1.2 np.arange() 関数

規則的に並んだ数の配列は,一つずつ書き並べなくても作成できる.

a と b が整数で a < b のとき,np.arange(a, b) は,a から b-1 までの整数を並べた配列を返す.終点の b を含まない点は,range() と同じである.np.arange(a, b, step) と書くと,値の増分を step で指定できる.

print(np.arange(1, 7))
print(np.arange(1, 7, 2))
[1 2 3 4 5 6]
[1 3 5]

3.1.3 np.linspace() 関数

np.linspace(a, b, n) は,a から b までの範囲に等間隔に並ぶ n 個の値を返す.n \geq 2 のとき,デフォルトでは両端の a と b を含む.np.arange() では値の増分を指定するのに対し,np.linspace() では値の個数を指定する.

print(np.linspace(0, 1, 5))
[0.   0.25 0.5  0.75 1.  ]

np.linspace() は,グラフを描くときに横軸の値を作るためによく使う.

練習 3.2 np.linspace() を使って,-1 と 1 を両端として等間隔に並ぶ 5 個の値を生成し,表示せよ.

3.2 要素の取り出し

配列の要素は,[] の中に位置を表すインデックスを指定して取り出す.インデックスは 0 から始まるため,先頭の要素は a[0] である.

a = np.array([10, 20, 30, 40, 50])

print(a[0])
print(a[2])
10
30

インデックスに負の値を指定すると,末尾から数える.a[-1] は最後の要素である.

print(a[-1])
50

a[i:j] と書くと,インデックス i の位置から j の直前までの要素をまとめて取り出せる.これをスライスという.終点の j に対応する要素は含まれない.

print(a[1:3])
[20 30]

2次元配列では,[行インデックス, 列インデックス] の順に指定する.次のコードは,行インデックス 0,列インデックス 2 の要素,すなわち1行3列目の要素を取り出す.

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

print(A[0, 2])
3

スライスの開始位置と終了位置を省略して : だけを書くと,その軸のすべての要素を指定できる.A[:, 0] は,すべての行から列インデックス 0,すなわち1列目を取り出す.データから特定の列だけを取り出すときによく使う.

print(A[:, 0])
[1 4]

練習 3.3 上の2次元配列 A から,行インデックス 1,すなわち2行目の [4, 5, 6] を取り出して表示せよ.

3.3 条件による要素の取り出し

配列と数値を比較すると,各要素について条件を満たすかどうかを表すブール配列が得られる.条件を満たす位置には True,満たさない位置には False が入る.

scores = np.array([65, 80, 72, 91, 58])
condition = scores >= 80

print(condition)
[False  True False  True False]

このブール配列をインデックスとして指定すると,True に対応する要素だけを取り出せる.この方法を,ブール配列によるインデックス指定(boolean indexing)という.

print(scores[condition])
[80 91]

条件を直接書いて,scores[scores >= 80] とすることもできる.

3.4 配列の形状の変更

reshape() メソッドを使うと,要素数を保ったまま形状を変えた配列が得られる.次のコードでは,要素数が 6 の1次元配列 a から,2行3列の2次元配列 A を作成する.a 自体の形状は変わらない.

a = np.arange(1, 7)
A = a.reshape(2, 3)
print(A)
[[1 2 3]
 [4 5 6]]

指定する各軸の長さの積は,元の配列の要素数と一致しなければならない.例えば,要素数が 6 の配列を2行4列に変形することはできない.

一つの軸の長さに -1 を指定すると,その長さは全体の要素数とほかの軸の長さから自動的に決まる.a.reshape(-1, 1) は,1次元配列 a を1列に並べた2次元配列を返す.この例では,形状が (6,) から (6, 1) になる.

column = a.reshape(-1, 1)
print(column)
print(a.shape)       # (6,)
print(column.shape)  # (6, 1)
[[1]
 [2]
 [3]
 [4]
 [5]
 [6]]
(6,)
(6, 1)

3.5 配列の演算

Python のリストに + を使うと,二つのリストが連結される.一方,NumPy 配列に + を使うと,要素ごとの加算が行われる.

list_a = [1, 2, 3]
list_b = [4, 5, 6]
print(list_a + list_b)

array_a = np.array([1, 2, 3])
array_b = np.array([4, 5, 6])
print(array_a + array_b)
[1, 2, 3, 4, 5, 6]
[5 7 9]

NumPy 配列では,+,-,*,/,** によって要素ごとの演算を行う.同じ形状の配列同士では,同じ位置にある要素を組み合わせて計算する.例えば,二つの2次元配列の和は,次のように計算できる.

A = np.array([[1, 2, 3], [4, 5, 6]])
B = np.array([[7, 8, 9], [10, 11, 12]])
C = A + B

print(C)
[[ 8 10 12]
 [14 16 18]]

この演算を数式で表すと,次のようになる.

\begin{bmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{bmatrix}+ \begin{bmatrix} 7 & 8 & 9 \\ 10 & 11 & 12 \end{bmatrix}= \begin{bmatrix} 8 & 10 & 12 \\ 14 & 16 & 18 \end{bmatrix}

なお,* は要素ごとの乗算を表し,行列の積ではない.

異なる形状の配列同士でも,一定の条件を満たせば,ブロードキャストという仕組みによって演算できる.線形代数の章では,配列とスカラーの演算を例に,この仕組みを紹介する.

練習 3.4 要素がそれぞれ [5, 7, 9] と [1, 2, 3] である NumPy の1次元配列 a と b を作成し,その差 a - b を求めよ.

3.6 配列の集計

NumPy の集計関数を使うと,配列の要素の合計や平均値などを計算できる.np.sum() は合計,np.mean() は平均値を返す.以下のように配列だけを引数に指定すると,配列全体が集計の対象となる.

array = np.array([1, 2, 3])
total = np.sum(array)
print(total)
print(np.mean(array))
6
2.0

同じように,np.max() は最大値を,np.min() は最小値を返す.

print(np.max(array))
print(np.min(array))
3
1

3.7 乱数

乱数は,手法を試すための架空のデータを作るときなどに用いる.NumPy では,np.random.default_rng() を使って乱数生成器(Generator オブジェクト)を作成し,そこから乱数を生成する方法が推奨されている.

乱数生成器を初期化するための値をシードという.引数を指定せずに np.random.default_rng() を実行すると,通常は作成するたびに異なる乱数列が得られる.同じ実行環境で,同じ非負整数をシードに指定して生成器を作り直し,同じ順序・引数で乱数を生成すれば,同じ乱数列を再現できる.次のコードでは,シードを 0 として乱数生成器 rng を作成する.

rng.random(n) は,0 以上 1 未満の一様分布に従う乱数を n 個返す.

rng = np.random.default_rng(0)
print(rng.random(3))
[0.63696169 0.26978671 0.04097352]

同じ rng で続けて乱数を生成すると,乱数列の続きが得られる.以下の例では,それぞれを単独でも再現できるように,例ごとに rng を初期化する.

rng.normal(mu, sigma, n) は,平均 mu,標準偏差 sigma の正規分布に従う乱数を n 個返す.正規分布については,確率統計の章で扱う.

rng = np.random.default_rng(0)
print(rng.normal(50, 10, 3))
[51.25730221 48.67895137 56.4042265 ]

ここで指定する平均と標準偏差は,乱数の生成元となる分布の値である.生成した有限個の値の平均や標準偏差が,指定した値と一致するとは限らない.

ノート標準正規分布の乱数

rng.standard_normal(n) は,平均 0,標準偏差 1 の標準正規分布に従う乱数を n 個返す.分布の指定としては,rng.normal(0, 1, n) と同じである.

rng = np.random.default_rng(0)
print(rng.standard_normal(3))
[ 0.12573022 -0.13210486  0.64042265]

練習 3.5 平均 170,標準偏差 6 の正規分布に従う乱数を 5 個生成し,表示せよ.ただし,np.random.default_rng(0) を使って Generator オブジェクトを作成すること.

3.8 総合練習

練習 3.6 次の1次元配列 data から,2行3列に変形した配列 matrix を作成し,表示せよ.次に,元の1次元配列 data から 10 以上の要素だけを取り出し,それらの要素と平均値を表示せよ.

data = np.array([12, 5, 8, 20, 16, 3])

3.9 まとめ

  • NumPy 配列の次元数,形状,要素数,データ型は,ndim,shape,size,dtype で確認できる.
  • インデックス,スライス,ブール配列によるインデックス指定を使うと,必要な要素を取り出せる.
  • reshape() を使うと,要素数を変えずに配列の形状を変更できる.
  • NumPy 配列では,要素ごとの演算や,合計,平均値,最大値,最小値などの集計ができる.
  • 同じ実行環境で,同じシードと手順を使うと,Generator が生成する乱数列を再現できる.