5-3. NumPyライブラリ

NumPyについて説明します。

参考

NumPyとは、多次元配列を効率的に扱うライブラリです。 Pythonの標準ライブラリではありませんが、科学技術計算や機械学習など、ベクトルや行列の演算が多用される分野では、事実上の標準ライブラリとしての地位を確立しています。

NumPyを用いるには、まず、numpy モジュールをインポートする必要があります。 慣習として、np と別名をつけて利用されます。

[1]:
import numpy as np

NumPyでは、Python標準の数値やリストの代わりに、特別な数値や配列を用いることで、格段に効率的な配列演算を実現します。 以下では、配列の基本的な操作や機能を説明します。

配列の構築

配列とは、特定の型の値の並びです。 numpy.array() 関数で構築できます。 このとき、配列の要素はPython標準のリストやタプルで指定します。 どちらを用いて作成しても全く同じ配列を作成できます。

[2]:
a = np.array([1,2,3]) # リストから配列作成
print(a)
b = np.array((1,2,3)) # タプルからの配列作成
print(b)
[1 2 3]
[1 2 3]

print の結果はリストと似ていますが、要素が , ではなく空白で区切られているに注意してください。 print ではなく、式の評価結果の場合、より違いが明示されます。

[3]:
a
[3]:
array([1, 2, 3])

配列は numpy.ndarray というデータ型によって実現されています。 組み込み関数 type() を使うと、データ型を調べられます。

[4]:
type(np.array([1,2,3,4,5])) # 配列の型
[4]:
numpy.ndarray
[5]:
type([1,2,3,4,5])
[5]:
list

array() が、リストではなく ndarray を返していることがわかります。

要素型

配列の要素を構成する値には幾つかの型がありますが、次の4つの型を知っていればとりあえずは十分です。

型名

説明

numpy.int32

整数(32-bit)を表す型

numpy.float64

実数(64-bit)を表す型

numpy.complex128

複素数(64-bit実数の組)を表す型

numpy.bool_

真理値を表す型

配列は、リストと異なり、型の異なる要素を混在させることはできません。

array()dtype 引数に、要素型を表すオブジェクトや文字列値を与えることで、指定された要素型の配列を構築できます。

[6]:
print(np.array([-1,0,1], dtype=np.int32)) # np.int32の代わりに'int32'でも同じ
[-1  0  1]

実数には、小数点が付与されて印字されます。

[7]:
print(np.array([-1,0,1], dtype=np.float64)) # np.float64の代わりに'float64'でも同じ
[-1.  0.  1.]

複素数は実部と虚部を表す実数の組であり、虚部には j が付与されて印字されます。

[8]:
print(np.array([-1,0,1], dtype=np.complex128)) # np.complex128の代わりに'complex128'でも同じ
[-1.+0.j  0.+0.j  1.+0.j]

数値から真理値への変換では、0False で、0 以外が True になります。

[9]:
print(np.array([-1,0,1], dtype=np.bool_)) # np.bool_の代わりに'bool'でも同じ
[ True False  True]

多次元配列

多次元配列は、配列の中に配列がある入れ子の配列です。 入れ子のリストやタプルを numpy.array() に渡すことで構築できます。

[10]:
print(np.array([[1,2],[3,4]])) # 2次元配列の構築
[[1 2]
 [3 4]]
[11]:
print(np.array([[[1,2],[3,4]],[[5,6],[7,8]]])) # 3次元配列の構築
[[[1 2]
  [3 4]]

 [[5 6]
  [7 8]]]

上の例からわかるように、2次元配列は行列のように、3次元配列は行列の配列のように印字されます。

多次元配列は、要素となる配列の長さが等しいことが想定されます。 つまり、2次元配列は、行列のように各行の長さが等しくなければなりません。

[12]:
print(np.array([[1,2],[3]])) # 行の長さが異なる場合
[list([1, 2]) list([3])]

このように行の長さが異なる場合は、多次元配列とは見做されません。

多次元配列の各次元の長さの組を、多次元配列の (shape) と呼びます。 特に2次元配列の場合、行列と同様に、行数(内側にある配列の数)と列数(内側にある配列の要素数)の組を使って、行数×列数で形を表記します。

1次元配列に対して reshape() メソッドを使うと、引数で指定された形の多次元配列に変換することができます。

[13]:
a1 = np.array([0, 1, 2, 3, 4, 5]) # 1次元配列
a2 = a1.reshape(2,3)              # 2×3の2次元配列
a2
[13]:
array([[0, 1, 2],
       [3, 4, 5]])

ここで、reshape() を適用する前後の配列(ここでは a1a2)は、内部的にデータを共有していることに注意してください。 つまり、a1 の要素を更新すると、a2 にも影響を及ぼします。

[14]:
a1[1] = 6
print(a1)
print(a2)
[0 6 2 3 4 5]
[[0 6 2]
 [3 4 5]]

ravel() メソッドを使うと、多次元配列を1次元配列に戻すことができます。

[15]:
a = np.array([0, 1, 2, 3, 4, 5]).reshape(2,3)
print(a)
print(a.ravel())
[[0 1 2]
 [3 4 5]]
[0 1 2 3 4 5]

ravel() の結果も、reshape() と同様に、元の配列と要素を共有します。

[16]:
elems = np.array([0, 1, 2, 3, 4, 5])
a = elems.reshape(2,3).ravel() # ravel()は要素をelemsと共有
elems[1] = 6
print(a)
[0 6 2 3 4 5]

なお、要素をコピーして変換する flatten() メソッドもありますが、コピーしない ravel() の方が効率的です。

配列のデータ属性

配列はオブジェクトであり、その配列に関する様々な情報を属性として保持します。 (オブジェクトの属性については6-3に簡単な説明があります。) 配列が持つ代表的なデータ属性(メソッド以外の属性)を次の表にまとめます。

属性

意味

a.dtype

配列 a の要素型

a.shape

配列 a の形(各次元の長さのタプル)

a.ndim

配列 a の次元数(len(a.shape) と等しい)

a.size

配列 a の要素数(a.shape の総乗と等しい)

a.flat

配列 a の1次元表現(a.ravel() と等しい)

a.T

配列 a を転置した配列(a と要素を共有)

配列要素を生成する構築関数

要素を生成して配列を構築する代表的な関数を紹介します。 特に断りが無い場合、ここで紹介する関数は、array() と同様に dtype 引数で要素型を指定可能です。

arange

numpy.arange() は、組み込み関数 range() の配列版です(arange は array range の略)。 開始値・終了値・刻み幅を引数にとります。 デフォルトの開始値は 0、刻み幅は 1 です。 range() と違って、引数の値は整数に限定されません。

[17]:
print(np.arange(3)) # range(3)に対応する配列
print(np.arange(0, 1, 0.2)) # 0を開始値として0.2刻みで1未満の要素を生成
[0 1 2]
[0.  0.2 0.4 0.6 0.8]

linspace

numpy.linspace() 関数は、範囲を等分割した値からなる配列を生成します。 第1引数と第2引数には、それぞれ範囲の開始値と終了値、第3引数には分割数を指定します。

[18]:
print(np.linspace(0, 1, 4)) # 0から1の値を4分割した値を要素に持つ配列
[0.         0.33333333 0.66666667 1.        ]

zerosones

numpy.zeros() 関数は、0 からなる配列を生成します。 同様に、numpy.ones() 関数は、1 からなる配列を生成します。 どちらも、生成される形を第1引数に取ります。 デフォルトの要素型は、実数です。

[19]:
print(np.zeros(4))     # 長さ4の1次元配列
print(np.zeros((2,3))) # 2×3の2次元配列を生成
print(np.ones(4))     # 長さ4の1次元配列
print(np.ones((2,3))) # 2×3の2次元配列を生成
[0. 0. 0. 0.]
[[0. 0. 0.]
 [0. 0. 0.]]
[1. 1. 1. 1.]
[[1. 1. 1.]
 [1. 1. 1.]]

random.rand

numpy.random.rand() 関数は、0 以上 1 未満の乱数からなる配列を生成します。 引数には生成される配列の形を指定します。 要素型は実数に限定されます。

[20]:
print(np.random.rand(4))   # 長さ4の1次元配列
print(np.random.rand(2,3)) # 2×3の2次元配列を生成
[0.78999303 0.90482231 0.67346348 0.16217231]
[[0.61202115 0.34825497 0.25168207]
 [0.02817442 0.5133705  0.24220049]]

この他にも、numpy.random.randn()numpy.random.binomial()numpy.random.poisson() は、それぞれ、正規分布・二項分布・ポアソン分布の乱数からなる配列を生成します。

練習

引数に整数 \(n\) を取り、\(i\) から始まる連番の整数からなる配列を\(i\)番目 (\(i\ge 0\)) の行として持つ \(n\times n\) の2次元配列を返す関数 range_square_matrix() を、arange() を用いて定義してください。

たとえば、range_square_matrix(3) は、

[[0 1 2]
 [1 2 3]
 [2 3 4]]

と印字されるような2次元配列を返します。

[21]:
def arange_square_matrix(n):
    ...

以下のセルを実行して、True が表示されることを確認してください。

[22]:
print(all(map(all,(arange_square_matrix(3) == np.array([[0,1,2],[1,2,3],[2,3,4]])))))
False

配列要素の操作

インデックスアクセス

配列の要素には、リストの場合と同様に、0 から始まるインデックスを使って参照できます。 リストと同じく、配列の先頭要素のインデックスは 0、最後の要素のインデックスは -1 となります。

[23]:
a = np.arange(3)
print(a)
[0 1 2]
[24]:
a[0]
[24]:
0
[25]:
a[-1]
[25]:
2
[26]:
a[-1] = 3 # 要素への代入もできる
print(a)
[0 1 3]

多次元配列では、高次元(入れ子の外側)から順にインデックスを指定します。 特に2次元配列、すなわち行列の場合は、行インデックスと列インデックスを順に指定します。

[27]:
a = np.arange(6).reshape(2,3)
print(a)
[[0 1 2]
 [3 4 5]]
[28]:
a[1,2] # 行と列のインデックスをまとめて指定
[28]:
5
[29]:
a[1,2] = 6 # 要素への代入もできる
print(a)
[[0 1 2]
 [3 4 6]]

スライス

リストと同様に、配列のスライスを構築できます。

[30]:
a = np.arange(5)
print(a)
print(a[1:4])
print(a[1:])
print(a[:-2])
print(a[::2])
print(a[::-1])
[0 1 2 3 4]
[1 2 3]
[1 2 3 4]
[0 1 2]
[0 2 4]
[4 3 2 1 0]

配列のスライスに対して代入すると、右辺の値がコピーされて、スライス元の配列にまとめて代入されます。

[31]:
a = np.arange(5)
print(a)
a[1:4] = 6
print(a)
a = np.arange(5)
a[::2] = 6
print(a)
[0 1 2 3 4]
[0 6 6 6 4]
[6 1 6 3 6]

一方、リストに対しては、以下はエラーになります。

[32]:
a = [0,1,2,3,4]
print(a)
a[1:4] = 6
[0, 1, 2, 3, 4]
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Input In [32], in <module>
      1 a = [0,1,2,3,4]
      2 print(a)
----> 3 a[1:4] = 6

TypeError: can only assign an iterable
[33]:
a = [0,1,2,3,4]
print(a)
a[1:4] = [6]
print(a)
[0, 1, 2, 3, 4]
[0, 6, 4]

このように、配列のスライスに対する代入の振舞いは、リストの場合と異なることに注意してください。

多次元配列に対しては、インデックスの参照と同様に、高い次元のスライスから順に並べて指定します。

[34]:
a = np.arange(9).reshape(3,3)
print(a)
print(a[:2,:2])
print(a[1:,1:])
[[0 1 2]
 [3 4 5]
 [6 7 8]]
[[0 1]
 [3 4]]
[[4 5]
 [7 8]]

多次元配列に対するスライスは、入れ子リストに対するスライスとは意味が異なることに注意してください。

for文

リストと同様に、for文を用いて、配列要素への反復処理を記述できます。

[35]:
for v in np.arange(3):
    print(v)
0
1
2

多次元配列の場合は、最外の配列に対して反復します。 つまり、2次元配列の場合、行の配列に対する反復処理となります。

[36]:
for row in np.arange(6).reshape(2,3):
    print(row)
[0 1 2]
[3 4 5]

for文と併用される enumerate() の多次元配列版として、numpy.ndenumerate() 関数が提供されています。 numpy.ndenumerate() は、(多次元)インデックスと要素の組を列挙します。

[37]:
for idx, e in np.ndenumerate(np.arange(6).reshape(2,3)):
    print(idx, e)
(0, 0) 0
(0, 1) 1
(0, 2) 2
(1, 0) 3
(1, 1) 4
(1, 2) 5
[38]:
for idx, e in np.ndenumerate(np.arange(3)):
    print(idx, e)
(0,) 0
(1,) 1
(2,) 2

要素毎の演算

配列に対する要素毎の演算は、簡潔に記述できます。 しかも、for文で記述するより、効率がよいです。 要素毎の演算を上手く使えるかどうかが、NumPyプログラミングの肝と言っても過言ではないでしょう。

配列のスカラ演算

配列とスカラとの算術演算を記述すると、要素毎のスカラ演算となります。 演算結果として、新しい配列が返ります。

[39]:
a = np.arange(4)
print(a)

print(a + 1) # 各要素に1を加算
print(a - 1) # 各要素に1を減算
print(a * 2) # 各要素に2を乗算
print(a / 2) # 各要素を2で除算
print(a // 2) # 各要素を2で整数除算
print(a % 2) # 各要素に2の剰余演算
print(a ** 2) # 各要素を2乗

print(1 + a) # 左側がスカラでもよい
print(1 - a) # 左側がスカラでもよい
print(2 * a) # 左側がスカラでもよい
b = a + 1
print(1 / b) # 左側がスカラでもよい
print(9 // b) # 左側がスカラでもよい
[0 1 2 3]
[1 2 3 4]
[-1  0  1  2]
[0 2 4 6]
[0.  0.5 1.  1.5]
[0 0 1 1]
[0 1 0 1]
[0 1 4 9]
[1 2 3 4]
[ 1  0 -1 -2]
[0 2 4 6]
[1.         0.5        0.33333333 0.25      ]
[9 4 3 2]

配列同士の演算

形が同じ配列同士の算術演算は、同じ位置の要素同士の演算となります。 演算結果として、新しい配列が返ります。

[40]:
a = np.arange(4).reshape(2,2)
b = np.arange(1,5).reshape(2,2)
print(a)
print(b)
print(a + b)
print(a - b)
print(a * b)
print(a / b)
c = 3 * a
print(c // b)
print(a % b)
print(a ** b)
[[0 1]
 [2 3]]
[[1 2]
 [3 4]]
[[1 3]
 [5 7]]
[[-1 -1]
 [-1 -1]]
[[ 0  2]
 [ 6 12]]
[[0.         0.5       ]
 [0.66666667 0.75      ]]
[[0 1]
 [2 2]]
[[0 1]
 [2 3]]
[[ 0  1]
 [ 8 81]]

実は、形が同じでない配列同士の算術演算も可能ですが、振舞いが複雑なので間違いやすいです。 配列同士の算術演算は、形が同じ配列に限定する方が賢明です。

ユニバーサル関数

NumPyにはユニバーサル関数と呼ばれる、任意の形の配列を取り、各要素に所定の演算を与えた結果を返す関数があります。 その代表例は、numpy.sqrt() 関数です。

[41]:
a = np.zeros(3) + 2
print(a)
print(np.sqrt(a)) # 各要素はsqrt(2)
b = np.zeros((2,2)) + 2
print(np.sqrt(b)) # 各要素はsqrt(2)
print(np.sqrt(2)) # スカラ(0次元配列)も扱える
[2. 2. 2.]
[1.41421356 1.41421356 1.41421356]
[[1.41421356 1.41421356]
 [1.41421356 1.41421356]]
1.4142135623730951

この他にも、多数のユニバーサル関数が提供されています。 詳しくは、ユニバーサル関数の一覧を参照してください。

よく使われる配列操作

dot

numpy.dot() は、2つの配列を引数に取り、そのドット積を返します。 両者が1次元配列のときは、ベクトル内積と等しいです。

[42]:
np.dot(np.arange(4), np.arange(1,5)) # 0*1 + 1*2 + 2*3 + 3*4
[42]:
20

2次元配列同士だと、行列乗算と等しいです。

[43]:
# [[0 1]     [[1 2]
#  [2 3]] と  [3 4]] の行列積
print(np.dot(np.arange(4).reshape(2,2), np.arange(1,5).reshape(2,2)))
[[ 3  4]
 [11 16]]

sort

numpy.sort() 関数は、昇順でソートされた新しい配列を返します。 これは、組み込み関数 sorted() の配列版です。

[44]:
a = np.array([3, 4, -1, 0, 2])
print(a)
print(np.sort(a))
[ 3  4 -1  0  2]
[-1  0  2  3  4]

一方、配列の sort() メソッドは、配列を破壊的に(インプレースで)ソートします。 これは、リストの sort() メソッドの配列版です。

[45]:
a = np.array([3, 4, -1, 0, 2])
print(a)
a.sort()
print(a)
[ 3  4 -1  0  2]
[-1  0  2  3  4]

sum, max, min, mean

配列のメソッド sum()max()min()mean() は、それぞれ総和・最大値・最小値・算術平均を返します。 これらのメソッドは、引数が与えられない場合、全要素を集計した結果を返します。 多次元配列の場合、集計する次元を指定できます。 具体的には、2次元配列の場合、0 を指定すると各列に、1 を指定すると各行に、対応するメソッドを適用した結果が返されます。

[46]:
a = np.arange(6).reshape(2,3)
print(a)
print(a.sum())
print(a.sum(0))
print(a.sum(1))
[[0 1 2]
 [3 4 5]]
15
[3 5 7]
[ 3 12]

この他にも、多数の数学・統計関連のメソッドや関数が提供されています。 詳しくは、数学関数統計関数を参照してください。

配列の保存と復元

配列は、ファイルに保存したり、ファイルから読み出したりすることが、簡単にできます。

numpy.savetxt() 関数は、与えられた配列を指定されたファイル名をつけてテキスト形式で保存します。

[47]:
np.savetxt('arange3.txt', np.arange(3))

この arange3.txt は、次のような内容になっているはずです。

0.000000000000000000e+00
1.000000000000000000e+00
2.000000000000000000e+00

2次元配列は、列が空白区切りで保存されます

[48]:
np.savetxt('arange2x3.txt', np.arange(6).reshape(2,3))

この arange2x3.txt は、次のような内容になっているはずです。

0.000000000000000000e+00 1.000000000000000000e+00 2.000000000000000000e+00
3.000000000000000000e+00 4.000000000000000000e+00 5.000000000000000000e+00

一方、numpy.loadtxt() 関数は、与えられた名前のファイルに保存された配列を復元します。

[49]:
a = np.loadtxt('arange2x3.txt')
print(a)
[[0. 1. 2.]
 [3. 4. 5.]]

保存するときに、列の区切り文字をデフォルトの ' ' 以外にしたい場合、savetxt()delimiter 引数に区切り文字(列)を指定します。これを復元するときには、loadtxt()delimiter 引数に同じ値を指定する必要があります。 ただし、区切り文字列はASCII(正確にはLatin-1)で解釈可能でなければなりません。

大規模な配列をテキスト形式で保存すると、ファイルサイズがとても大きくなります。 そういう場合、圧縮保存が有用です。

保存するファイル名の拡張子を .gz とすることで、savetxt() は自動的にGZip形式で圧縮して保存します。 復元するファイル名の拡張子が .gz であれば、loadtxt() はGZip形式だと判断して、自動的に解凍して復元します。

真理値配列によるインデックスアクセス

配列に対して、比較演算を適用すると、算術演算と同様に要素毎に演算されて、真理値の配列が返ります。

[50]:
a = np.arange(6)
print(a)
print(a < 3)
[0 1 2 3 4 5]
[ True  True  True False False False]

このように作られた真理値配列は、インデックスとして利用することができます。 これによって、条件を満たす範囲を取り出すような記述が可能になります。 次の具体例を見てみましょう。

[51]:
a = np.array([0,1,2,-3,-4,5,-6,-7])
print(a)
print(a[a < 0]) # 負の要素を取り出し
print(a[(a < 0) & (a % 2 == 0)]) # 負で偶数の要素を取り出し
a[a < 0] = 8 # 負の要素を8に上書き
print(a)
[ 0  1  2 -3 -4  5 -6 -7]
[-3 -4 -6 -7]
[-4 -6]
[0 1 2 8 8 5 8 8]

一見すると単なる条件式のように見えますが、インデックスとなるのは真理値ではなく真理値の配列です。 したがって、真理値を返す andornot の代わりに、要素毎の演算を行う &|~ を用いる必要があります。

同様の記法は、7-1で扱うpandasライブラリでも利用されます。

▲線形代数の演算

numpy.dot() は、2次元配列を与えたときには、行列積となりました。 それだけでなく、行列積専用の numpy.matmul() も提供されています。

また、単位行列は numpy.identity() 関数で作成することができます。 引数に行列のサイズを指定します。

[52]:
I = np.identity(3)
print(I)
a = np.arange(9).reshape(3,3)
print(a)
print(np.matmul(a, I))
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
[[0 1 2]
 [3 4 5]
 [6 7 8]]
[[0. 1. 2.]
 [3. 4. 5.]
 [6. 7. 8.]]

numpy.linalg.norm() 関数は、与えられたベクトル(1次元配列)もしくは行列(2次元配列)のノルムを返します。

[53]:
np.linalg.norm(np.ones(3)) # ユークリッドノルムを計算するのでsqrt(3)と等しい
[53]:
1.7320508075688772

NumPyでは、行列の分解、転置、行列式などの計算を含む線形代数の演算は、numpy.linalg モジュールで提供されています。 詳しくは、線形代数関連関数を参照してください。

練習の解答

[54]:
def arange_square_matrix(n):
    return np.array([np.arange(i, n+i) for i in range(n)])