Python基礎

NumPy超入門:配列計算を直感で理解|“ベクトル化×ブロードキャスト×軸”で速く美しく

for文を書き連ねて処理が遅い…配列の形が合わなくてエラー…

axisが分からない…そんな悩み、丸ごと解消します。

本記事では、NumPyの3本柱「ベクトル化」「ブロードキャスト」「軸(axis)」を核に、現場でそのまま使える最小セットを配布します。
見通しのよいコードに置き換え、速度・保守性・再現性を一気に底上げしましょう。

この記事で身に付く力

  • ndarrayの最小作法(生成・dtype・形変換)
  • ベクトル化(算術・比較・where・ufunc)でforを減らす
  • ブロードキャスト(5パターン)を安全に使い分ける
  • インデクシング(スライス/ブール/ファンシー)とコピーの罠回避
  • 軸(axis)思考で集計と接続(pandas/Sklearn)を一気通貫に

NumPyが「速く・短く・間違いにくい」理由

配列思考のコアは3つ。ベクトル化で式にまとめ、ブロードキャストで形を合わせ、axisは「どちらに潰すか」で考える。筆者(ふみと)は現場でこの3原則に徹底的に寄せることで、10〜100倍の速度改善とバグ削減を繰り返してきました。以下、“コピペで確認できる”最短ルートで進めます。

1) ndarrayの最小作法(生成・shape・dtype)

まずは配列の「型」と「形」を揃える作法から。ここを丁寧にすると後工程が一気に安定します。

import numpy as np

# 生成(Pythonリスト→ndarray)

a = np.array(\[1, 2, 3], dtype=np.int32)

# 等差/ゼロ/同形ones

r = np.arange(0, 10, 2)         # 0,2,4,6,8
z = np.zeros((2, 3), dtype=float)
ones = np.ones\_like(z)          # 形とdtypeを継承

# 形と次元

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

# 形変換

b\_T = b.T                 # 転置(ビュー)
b2  = b.reshape(3, 2)     # 要素数は不変
b3  = b.reshape(-1)       # 1次元へ(長さは自動)

# dtypeの変換(不要なコピーを避ける)

x = np.array(\[1, 2, 3], dtype=np.float32)
x64 = x.astype(np.float64, copy=False)

要点

  • reshape要素数不変が大原則。
  • astype(copy=False)不要なコピー回避

2) ベクトル化:forを書かず“式にする”

配列演算は基本「要素ごと」。np.wherenp.clipは、高速で読みやすい条件分岐に最適です。

x = np.array([1,2,3,4])

# 1) 算術

x2 = x \* 2 + 1

# 2) 比較とマスク

mask = (x % 2 == 0)   # 偶数だけTrue

# 3) where(ifの代替)

y = np.where(mask, x\*10, -x)

# 4) ufunc(普遍関数)

z = np.sqrt(x)
w = np.clip(x, 2, 3)

3) ブロードキャスト:形が違っても足せる理由

右から次元を突き合わせ、長さが同じ or どちらか1なら整合します。合わなければエラー。使い分けの5パターンを覚えれば怖くありません。

# (3,) と スカラー → (3,)
np.array([1,2,3]) + 10  # [11,12,13]

# (3, 1) と (1, 4) → (3, 4)

A = np.arange(3).reshape(3,1)
B = np.arange(4).reshape(1,4)
C = A + B  # 3×4 の足し合わせ表

# 画像(H×W×3)と (1×1×3)

img = np.random.rand(480, 640, 3)
shift = np.array(\[0.1, -0.05, 0.0]).reshape(1,1,3)
adj = np.clip(img + shift, 0, 1)

ブロードキャストの5パターン

  1. スカラー × 配列
  2. (n, 1) × (1, m)
  3. (n, m) × (1, m)(行方向)
  4. (n, m) × (n, 1)(列方向)
  5. (b, h, w, c) × (1, 1, 1, c)(画像/チャネル)

4) インデクシング:スライス/ブール/ファンシー

「どれがビュー(参照共有)で、どれがコピーか」を理解しておくと、思わぬ破壊を防げます。

A = np.arange(12).reshape(3,4)

# スライス:ビュー(元を共有)

A\[:2, 1:3] = 0

# ブール

mask = A % 2 == 0
A\[mask] = -A\[mask]

# ファンシー(整数配列で抽出)

rows = np.array(\[0,2])
cols = np.array(\[1,3])
sub = A\[rows\[:,None], cols]  # 交差(2×2)

注意:コピーとビュー

  • スライスはビュー、ファンシーはコピーが返りやすい。
  • 代入の可否・速度・メモリに影響します。

5) 軸(axis)と集計:「どちらに潰すか」

axis=0 は縦に潰す(列ごと)、axis=1 は横に潰す(行ごと)。言い換えると「残したい方向の逆がaxis」です。

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

print(X.sum())          # 21(全体)
print(X.sum(axis=0))    # \[5,7,9]   列和(2行を潰す)
print(X.mean(axis=1))   # \[2.,5.]   行平均(3列を潰す)
print(np.nanmean(X))    # NaN無視の平均

6) 乱数と線形代数(最小セット)

rng = np.random.default_rng(42)

# サンプル

u = rng.uniform(0, 1, size=5)
n = rng.normal(loc=0, scale=1, size=(2,3))
idx = rng.choice(10, size=5, replace=False)

# 線形代数

A = rng.normal(size=(3,3))
b = rng.normal(size=3)
sol = np.linalg.solve(A, b)
vals, vecs = np.linalg.eig(A)
C = A @ A.T

7) メモリ/コピーの罠(ravel/flatten/copy)

X = np.arange(6).reshape(2,3)
Y = X[:, :2]  # ビュー(共有)
Y[0,0] = 999
print(X[0,0])  # 999(元も変わる)

# 1次元化

v1 = X.ravel()    # 可能ならビュー
v2 = X.flatten()  # 常にコピー

Y2 = Y.copy()

8) スタッキング/結合/反復の基本

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

print(np.concatenate(\[A,B], axis=0))  # 上下(4×2)
print(np.concatenate(\[A,B], axis=1))  # 左右(2×4])
print(np.stack(\[A,B], axis=0).shape)  # (2,2,2) 新しい軸

print(np.tile(\[1,2,3], reps=2))               # \[1,2,3,1,2,3]
print(np.repeat(\[1,2,3], repeats=\[1,2,3]))    # \[1,2,2,3,3,3]

9) NumPy×pandas×Scikit-learnの接続

pandas→NumPyは df.to_numpy() を推奨。MLでは X.shape=(n_samples, n_features)y.shape=(n_samples,) が基本形です。
→ [内部リンク:pandas基礎]/[内部リンク:scikit-learn基礎]

10) ベンチ“型”:forをやめる(timeit)

import numpy as np, timeit
x = np.arange(10_000, dtype=np.float64)

# ループ

loop = timeit.timeit('s=0\nfor v in x: s+=v\*v+1', number=100, globals=globals())

# ベクトル化

vec  = timeit.timeit('y = x\*x + 1', number=100, globals=globals())

print(loop/vec)  # 通常 10x〜100x 以上速い

業務テンプレ3本(コピペ)

A) 標準化(平均0・分散1)

X = np.array([[1,2,3],[4,5,6]], dtype=float)
mu = X.mean(axis=0, keepdims=True)
sd = X.std(axis=0, keepdims=True)
Z  = (X - mu) / sd

B) 距離行列(各行のユークリッド距離)

P = np.array([[0,0],[1,1],[2,0]], dtype=float)
# 3×2 と 1×2 のブロードキャストで差分→2乗→和→平方根
D = np.sqrt(((P[:,None,:] - P[None,:,:])**2).sum(axis=2))  # 3×3

C) one-hot(カテゴリ→ベクトル)

labels = np.array([0,2,1])
K = labels.max()+1
onehot = (np.arange(K) == labels[:,None]).astype(np.int8)  # 3×K

用途別の最初の一手(現場チートシート)

  • 可視化前処理:clip/whereで外れ値を抑えてから → [内部リンク:可視化入門]
  • 学習データ作成:stack/concatenateで特徴を結合 → [内部リンク:scikit-leーン基礎]
  • 大量CSV整形:ブロードキャストで条件置換 → [内部リンク:pandas実践]

今日やること(45分)

  1. 既存のfor配列式(ベクトル化)1箇所に置換。
  2. 自分のデータでブロードキャスト5パターンのいずれかを再現。
  3. axisを変えた集計(sum/mean)をkeepdims=Trueあり/なしで比較。
  4. ravel/flatten/copyの挙動の違いを確認。

よくある罠と対処(早見表)

症状原因対処
結果が変わるビューを意図せず書き換えcopy()で境界/ arr.baseで共有確認
ブロードキャストエラー形の不一致.shapeをprint、None/np.newaxisで次元追加
NaNが伝播演算にNaN混入np.nan*系(nanmeanなど)を使う
遅いPythonループ/ファンシー乱用ベクトル化/スライス(ビュー)を優先
dtypeが勝手に変わる混合型/整数÷整数初期からdtype=float///の違いに注意

まとめ:NumPyは“式で考える”

forが遅い、axisが曖昧、ビューで壊す——この3つの壁はベクトル化ブロードキャストaxis思考で一気に越えられます。テンプレと罠回避表を手元に、今日から配列思考へ置き換えていきましょう。

最近のコメント

    • この記事を書いた人
    • 最新記事

    ふみと

    このブログでは、データサイエンティストとして市場価値を上げる方法を独自にまとめて発信しています。

    【プロフィール】
    ・大手企業データサイエンティスト/マーケティングサイエンティスト(10年、年収900万円台)/案件100件以上
    ・資格:JDLA E資格(日本ディープラーニング協会主催)/JDLA Community(CDLE会員)/Advanced Marketer/ビジネス統計スペシャリスト/統計検定2級/TOEIC 805
    ・スキル:Python/Tableau/SQL/機械学習/Deep Learning/RPA

    -Python基礎