はじめに
pandasを使い始めてしばらく経ったころ、「pandasだけでいいか」と思っていたNumPyを理解していないせいで詰まる場面が出てきた。
pandasの内部はNumPyで動いている。DataFrameの数値計算はNumPyの配列操作がベースになっているので、エラーメッセージにndarrayと出てきたり、NumPyの関数を直接使う場面が普通にある。「下層の理解がないと詰まる」というのは本当だった。
PHPにはNumPyに相当するものがない。近いのは数値計算ライブラリだが、ほぼ使ったことがなかったので概念から整理した。
インストールと基本
pip install numpy
import numpy as np # npというエイリアスが慣習
ndarray — NumPyの基本データ構造
NumPyの中心にあるのがndarray(N次元配列)。Pythonのlistとは別物。
# Pythonのlist
py_list = [1, 2, 3, 4, 5]
# NumPyのndarray
np_array = np.array([1, 2, 3, 4, 5])
print(type(py_list)) # <class 'list'>
print(type(np_array)) # <class 'numpy.ndarray'>
listとndarrayの主な違い
# listは異なる型を混在できる
py_list = [1, "hello", True, 3.14]
# ndarrayは全要素が同じ型
np_array = np.array([1, 2, 3, 4, 5])
print(np_array.dtype) # int64
# 型が混在するとキャストされる
mixed = np.array([1, 2.5, 3])
print(mixed.dtype) # float64(全部floatに揃えられる)
# 演算の速度が全然違う
py_list = list(range(1_000_000))
np_array = np.array(range(1_000_000))
# list → 全要素に2倍(ループが必要)
result = [x * 2 for x in py_list]
# ndarray → ベクトル演算(ループ不要)
result = np_array * 2 # 全要素に一括で適用される
NumPyがlistより速い理由は、内部でC言語で実装された連続したメモリ領域に型が揃った数値を格納しているから。pandasのDataFrameで大量データを高速に処理できるのもNumPyがベースにあるおかげ。
配列の作成
# リストから作成
a = np.array([1, 2, 3, 4, 5])
# 2次元配列
matrix = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
])
# ゼロ埋め
zeros = np.zeros((3, 4)) # 3行4列のゼロ配列
ones = np.ones((2, 3)) # 1で埋めた配列
full = np.full((3, 3), 7) # 7で埋めた配列
# 連番
arange = np.arange(0, 10, 2) # [0, 2, 4, 6, 8]
linspace = np.linspace(0, 1, 5) # [0. , 0.25, 0.5 , 0.75, 1. ]
# 乱数
np.random.seed(42)
rand_int = np.random.randint(0, 100, size=(3, 4)) # 整数乱数
rand_float = np.random.rand(3, 4) # 0〜1の一様分布
rand_norm = np.random.randn(3, 4) # 標準正規分布
np.zeros()やnp.ones()はDataFrameの初期化でも使う。np.linspace()はグラフのx軸を均等に作るときに使った。
配列の形状操作
a = np.array([1, 2, 3, 4, 5, 6])
# 形状確認
print(a.shape) # (6,)
print(a.ndim) # 1(次元数)
print(a.size) # 6(要素数)
print(a.dtype) # int64
# 形状変換
b = a.reshape(2, 3)
print(b)
# [[1 2 3]
# [4 5 6]]
# 1次元に戻す
c = b.flatten() # コピーを返す
d = b.ravel() # ビューを返す(メモリ効率が良い)
print(c) # [1 2 3 4 5 6]
# 転置
print(b.T)
# [[1 4]
# [2 5]
# [3 6]]
reshape()のとき、要素数が合わない場合はエラーになる。片方の次元に-1を使うと自動計算される。
a = np.arange(12)
b = a.reshape(3, -1) # (3, 4)に自動計算
c = a.reshape(-1, 4) # (3, 4)に自動計算
インデックスとスライス
a = np.array([10, 20, 30, 40, 50])
# インデックスアクセス
print(a[0]) # 10
print(a[-1]) # 50
# スライス(Pythonのlistと同じ)
print(a[1:4]) # [20 30 40]
print(a[::2]) # [10 30 50]
# 2次元配列
matrix = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
])
# [行, 列]でアクセス
print(matrix[1, 2]) # 6(1行目、2列目)
print(matrix[0, :]) # [1 2 3](0行目全列)
print(matrix[:, 1]) # [2 5 8](全行の1列目)
print(matrix[0:2, 1:]) # [[2 3] [5 6]]
ファンシーインデックス
a = np.array([10, 20, 30, 40, 50])
# インデックスのリストで複数要素を取得
print(a[[0, 2, 4]]) # [10 30 50]
# 条件でフィルタリング(ブールインデックス)
print(a[a > 25]) # [30 40 50]
print(a[a % 20 == 0]) # [20 40]
pandasのフィルタリング(df[df["age"] > 30])は内部でこのブールインデックスを使っている。
ブロードキャスト — これが最初に混乱した
ブロードキャストは形状が異なる配列同士の演算を自動で拡張して行う仕組み。
まず形状が同じ場合の基本演算。
a = np.array([1, 2, 3])
b = np.array([10, 20, 30])
print(a + b) # [11 22 33](要素ごとの加算)
print(a * b) # [10 40 90](要素ごとの乗算)
PHPのforeachで全要素に同じ処理をするのが、NumPyだとループ不要で書ける。
次にスカラー値との演算。
a = np.array([1, 2, 3, 4, 5])
print(a + 10) # [11 12 13 14 15](全要素に10を加算)
print(a * 2) # [2 4 6 8 10](全要素を2倍)
print(a ** 2) # [1 4 9 16 25](全要素を2乗)
これがブロードキャストの一番シンプルな例。スカラー値10が配列の形状に合わせて自動的に拡張されて演算される。
2次元配列とのブロードキャスト
matrix = np.array([
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
])
row_vector = np.array([10, 20, 30])
# 各行にrow_vectorを加算
print(matrix + row_vector)
# [[11 22 33]
# [14 25 36]
# [17 28 39]]
row_vectorの形状は(3,)、matrixの形状は(3, 3)。ブロードキャストによりrow_vectorが3行分に拡張されて演算される。
col_vector = np.array([[10], [20], [30]]) # (3, 1)
# 各列にcol_vectorを加算
print(matrix + col_vector)
# [[11 12 13]
# [24 25 26]
# [37 38 39]]
ブロードキャストのルール
形状が合わない場合にどう拡張されるかのルール。
ルール1: 次元数が異なる場合、少ない側の左に1を補う
(3,) → (1, 3)
ルール2: サイズが1の次元は、相手の次元に合わせて拡張される
(1, 3) + (3, 3) → (3, 3) + (3, 3)
ルール3: サイズが違い、どちらも1でない次元があるとエラー
(2, 3) + (3, 3) → ValueError
a = np.ones((3, 4))
b = np.ones((4,))
c = a + b # (3, 4) + (4,) → OK(bが(1,4)→(3,4)に拡張)
a = np.ones((3, 4))
b = np.ones((3,))
c = a + b # (3, 4) + (3,) → エラー(次元が合わない)
# ValueError: operands could not be broadcast together with shapes (3,4) (3,)
このエラーはpandasを使っていてもたまに出てきたので、ブロードキャストのルールを理解してから原因がわかるようになった。
数学・統計関数
a = np.array([3, 1, 4, 1, 5, 9, 2, 6, 5, 3])
print(np.sum(a)) # 39
print(np.mean(a)) # 3.9
print(np.median(a)) # 3.5
print(np.std(a)) # 2.3...(標準偏差)
print(np.var(a)) # 5.49(分散)
print(np.max(a)) # 9
print(np.min(a)) # 1
print(np.argmax(a)) # 5(最大値のインデックス)
print(np.argmin(a)) # 1(最小値のインデックス)
print(np.sort(a)) # [1 1 2 3 3 4 5 5 6 9]
# 2次元配列の軸指定
matrix = np.array([[1, 2, 3], [4, 5, 6]])
print(np.sum(matrix)) # 21(全要素の合計)
print(np.sum(matrix, axis=0)) # [5 7 9](列方向の合計)
print(np.sum(matrix, axis=1)) # [6 15](行方向の合計)
axis=0が列方向(縦)、axis=1が行方向(横)。pandasの.sum(axis=0)も同じ意味。最初はどちらが縦か横か混乱したが、axis=0が「0次元目(行)を潰す方向」と覚えると整理できた。
行列演算
a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6], [7, 8]])
# 要素ごとの積(ブロードキャスト演算)
print(a * b)
# [[ 5 12]
# [21 32]]
# 行列積(ドット積)
print(np.dot(a, b))
# [[19 22]
# [43 50]]
# @ 演算子でも書ける(Python 3.5以降)
print(a @ b)
# [[19 22]
# [43 50]]
機械学習の計算では行列積が頻出する。*が要素ごとの積で@が行列積、という区別は最初に確認しておく。
pandasとの連携
import pandas as pd
import numpy as np
df = pd.DataFrame({
"score": [85, 92, 78, 88, 95],
})
# pandasのSeriesからndarrayを取り出す
arr = df["score"].values # ndarray
arr = df["score"].to_numpy() # 推奨される書き方
# NumPy関数をDataFrameに適用
df["z_score"] = (df["score"] - np.mean(df["score"])) / np.std(df["score"])
# ndarrayからDataFrameを作成
arr = np.random.randint(0, 100, size=(5, 3))
new_df = pd.DataFrame(arr, columns=["A", "B", "C"])
# np.whereはSQLのCASE WHENに相当
df["grade"] = np.where(df["score"] >= 90, "A",
np.where(df["score"] >= 80, "B", "C"))
print(df)
# score z_score grade
# 0 85 -0.42 B
# 1 92 0.86 A
# 2 78 -1.15 C
# 3 88 0.13 B
# 4 95 1.41 A
np.where()はpandasと組み合わせてよく使う。複数条件の分岐をネストして書けるので、SQL的な条件付き列の追加に使える。
よく使う関数まとめ
# 配列作成
np.array() # リストからndarray作成
np.zeros() # ゼロ配列
np.ones() # 1配列
np.arange() # 連番配列
np.linspace() # 均等間隔配列
np.random.rand() # 乱数配列
# 形状操作
.shape # 形状確認
.reshape() # 形状変換
.flatten() # 1次元化(コピー)
.T # 転置
# 集計
np.sum() # 合計
np.mean() # 平均
np.std() # 標準偏差
np.max() # 最大値
np.min() # 最小値
np.argmax() # 最大値のインデックス
np.sort() # ソート
# 便利な関数
np.where() # 条件分岐(CASE WHEN相当)
np.unique() # 重複なし配列
np.concatenate() # 配列の結合
np.clip() # 値の上限・下限を設定
np.abs() # 絶対値
まとめ
- ndarrayはPythonのlistと違い全要素が同じ型で高速
- ブロードキャストはループなしで配列全体に演算できる仕組み
-
axis=0が列方向(行を潰す)、axis=1が行方向(列を潰す) -
np.where()はpandasと組み合わせてCASE WHENの代わりになる - pandasの内部はNumPyなのでエラー時に理解が助けになる
「pandasだけでいい」と思っていたが、NumPyを知ってからエラーメッセージの読み方とブロードキャストエラーの原因がわかるようになった。機械学習ライブラリを使うようになるとNumPy操作が直接出てくるので、早めに触っておいてよかった。