- 概要(配列計算の高速化は「Pythonのループを捨て、NumPyのベクトル化へ乗り換える」)
- ベクトル化とブロードキャスト(ループをなくして配列全体で計算する)
- 集約・条件・インデックス操作(「まとめて算出・抽出」で速くする)
- メモリとdtypeの設計(「同一型・連続メモリ・事前確保」で無駄を消す)
- 線形代数とufunc(BLASとユニバーサル関数を活用する)
- pandasでの高速化(「ベクトル化」を徹底、applyを最小限に)
- 実践例(典型タスクを「高速な型」で書き直す)
- つまずき対策(形状不一致・不要なコピー・append連発・型混在)
- まとめ(「ベクトル化・ブロードキャスト・集約・BLAS」。型とメモリを整えれば速さは出る)
概要(配列計算の高速化は「Pythonのループを捨て、NumPyのベクトル化へ乗り換える」)
配列計算の高速化の要は、NumPy配列(ndarray)に処理を寄せることです。Pythonのforループや要素ごとの処理をやめ、配列同士の演算・ブロードキャスト・集約関数・線形代数APIを使うだけで、桁違いに速く、短いコードになります。さらにdtypeの統一、メモリ連続性(C/F順)、事前確保(pre-allocation)、in-place演算を押さえると、初心者でも「遅い」をほぼ解消できます。
ベクトル化とブロードキャスト(ループをなくして配列全体で計算する)
Pythonループを配列演算へ置き換える最小例
import numpy as np
# NG: Pythonループ(遅い)
data = list(range(10_000))
out1 = [x * 2 + 1 for x in data]
# OK: NumPyベクトル化(速い)
arr = np.arange(10_000)
out2 = arr * 2 + 1
Python配列演算は内部でC実装が使われるため、Pythonループより圧倒的に速く、読みやすさも向上します。
ブロードキャストで「形が違う配列」同士を自然に計算
A = np.arange(12).reshape(3, 4) # 3×4
b = np.array([1, 2, 3, 4]) # 1×4(行ベクトル)
out = A + b # 各行に b を加算(自動で列方向に拡張)
Python大きさの合う次元を自動的に拡張するのがブロードキャスト。ループ無しで「行/列ごとの足し込み」が書けます。
形状を揃える工夫(新しい軸の追加)
x = np.arange(3)[:, None] # 3×1 にする(列ベクトル化)
y = np.arange(4)[None, :] # 1×4 にする(行ベクトル化)
grid = x + y # 3×4 の和のグリッド
PythonNone(またはnp.newaxis)で次元を増やすと、狙い通りのブロードキャストができます。
集約・条件・インデックス操作(「まとめて算出・抽出」で速くする)
集約関数を使う(sum/mean/std/min/max)
arr = np.random.default_rng(0).normal(size=(1_000, 500))
total = arr.sum(axis=0) # 列方向合計
avg = arr.mean(axis=1) # 行方向平均
Python軸を指定して「どちら向きに集約するか」を明示します。Pythonでループを回すより桁違いに速いです。
条件抽出と条件代入(ブールマスクとwhere)
x = np.array([1, 5, -2, 0, 8])
mask = x >= 0
pos = x[mask] # 非負だけを抽出
clamped = np.where(x < 0, 0, x) # 負を0へ置換(条件代入)
Pythonブールインデックスとnp.whereで、ループ無しの条件処理が書けます。
ユニーク・出現回数・集合演算
vals = np.array([1,2,2,3,3,3])
u, counts = np.unique(vals, return_counts=True)
# u: [1,2,3], counts: [1,2,3]
Python集合・頻度はnp.uniqueやnp.isinなどの組み合わせで高速に処理できます。
メモリとdtypeの設計(「同一型・連続メモリ・事前確保」で無駄を消す)
dtypeを統一して無駄をなくす
x = np.array([1, 2, 3], dtype=np.float64) # 計算はfloatへ寄せる
y = np.array([4, 5, 6], dtype=np.float64)
z = x * y
Python型が混在すると暗黙の型変換が生じて遅くなることがあります。数値計算はfloat64(または目的の精度)に揃えます。
事前確保(pre-allocation)で連結コストをゼロに近づける
n = 1_000_000
out = np.empty(n, dtype=np.float64)
base = np.arange(n, dtype=np.float64)
out[:] = base * 0.5 + 1.0 # 一度に代入(appendしない)
Pythonappendやconcatenateの連続使用はコスト増。最終サイズが分かっているなら空配列を確保し、一括代入します。
スライスは「ビュー」なので速い(コピーを避ける)
A = np.arange(10)
B = A[2:8] # ビュー(同じメモリを参照)
B[:] = 0
# Aも反映される → コピーが不要ならビュー活用で高速化
Python不要なコピーを避けるほど速く、メモリも節約できます。コピーが必要ならA[2:8].copy()を使います。
連続メモリ(C/F順)を意識する
M = np.ones((1000, 1000), order="C") # 行優先
N = np.ones((1000, 1000), order="F") # 列優先
Python走査方向が連続な方がキャッシュに乗りやすく速いことがあります。行方向の演算が多いならC順が相性良いです。
線形代数とufunc(BLASとユニバーサル関数を活用する)
行列積・ベクトル積はBLASに乗る(@ と dot)
A = np.random.default_rng(0).normal(size=(1000, 512))
B = np.random.default_rng(1).normal(size=(512, 256))
C = A @ B # 高速な行列積(BLAS/LAPACKルーチンが使われる)
Python@演算子(dot)で、手書きループに比べて桁違いの速度が出ます。線形代数は必ずAPIに載せます。
einsumで「多次元の畳み込み・縮約」を簡潔に
X = np.random.default_rng(2).normal(size=(100, 20))
w = np.random.default_rng(3).normal(size=(20,))
y = np.einsum("ij,j->i", X, w) # 各行の内積(X @ w と同等)
Pythoneinsumは複雑な軸操作や縮約を読みやすく書け、ブロードキャスト・コピー削減にも役立ちます。
ufuncのout/in-placeでメモリ節約
x = np.random.default_rng(4).normal(size=1_000_000)
y = np.empty_like(x)
np.add(x, 1.0, out=y) # y = x + 1 を直接書き込み(追加の中間配列を作らない)
Pythonout引数やin-place代入(x += 1)は中間配列の作成を避け、速度とメモリ効率が上がります。
pandasでの高速化(「ベクトル化」を徹底、applyを最小限に)
Series/DataFrame同士の演算に寄せる
import pandas as pd
s = pd.Series(np.arange(10_000))
out = s * 2 + 1 # ベクトル化(速い)
# NG例: s.apply(lambda x: x*2+1) は遅い(Python関数が都度呼ばれる)
Pythonpandasの演算は内部でNumPyが使われます。apply/mapではなく、演算子や組み込み関数(where, clip, abs, sum, mean)を使います。
値抽出は「マスク」やwhereで
df = pd.DataFrame({"x": np.arange(10), "y": np.arange(10)[::-1]})
sel = df["x"] > 5
df["z"] = df["y"].where(sel, 0) # 条件代入の高速パターン
Pythonブールマスク・whereは配列処理のまま実行され、Pythonループを避けられます。
大規模結合前にdtypeを揃える
df["key"] = df["key"].astype("int64")
# join/mergeは型が一致している方が速く、予期せぬ型変換も起きない
Python結合やグループ化の前に型を統一しておくと、内部アルゴリズムが最適に働きます。
実践例(典型タスクを「高速な型」で書き直す)
画像の明るさ調整(配列全体を一括処理)
img = np.random.default_rng(0).integers(0, 256, size=(1080, 1920), dtype=np.int32)
# ループ無しでコントラスト・明るさを調整してクリップ
out = np.clip(img * 1.2 + 10, 0, 255).astype(np.uint8)
Python画素ループをやめ、配列演算とclip/astypeで一発処理します。
zスコア標準化(列ごとの平均・分散を使う)
X = np.random.default_rng(1).normal(size=(1000, 50))
mu = X.mean(axis=0)
sd = X.std(axis=0)
Z = (X - mu) / sd
Python学習前の前処理は「集約→ベクトル引き算→割り算」でループ不要。軸指定を間違えないようにします。
全組み合わせの距離を一括計算(ブロードキャスト)
P = np.random.default_rng(2).normal(size=(1000, 2)) # 1000点の座標
Q = np.random.default_rng(3).normal(size=(800, 2)) # 800点の座標
# 形を揃えて差分→二乗和→平方根
diff = P[:, None, :] - Q[None, :, :]
dist = np.sqrt((diff ** 2).sum(axis=2)) # 1000×800 の距離行列
Python二重ループを使わず、ブロードキャストで全ペア距離を計算します。
つまずき対策(形状不一致・不要なコピー・append連発・型混在)
形状不一致は「軸と次元」を確認する
reshapeやNoneで次元を合わせ、axis引数を明示します。ブロードキャストの規則(末尾次元から見て、同一か1か)を意識すれば迷いません。
不要なコピーは避けてビューで処理する
スライスはビューです。中間配列が重なる計算は、out=やin-place代入で抑えます。コピーが必要かどうかを意識して選択します。
append/concatenateの連続使用は高コスト
サイズが分かるならempty/zerosで事前確保し、一括代入。ログ蓄積はリストに溜めて最後にnp.arrayへ変換するのも有効です。
dtypeの混在で暗黙変換が発生する
計算対象は同じdtypeに統一します。整数と浮動小数が混じると意図せぬ型昇格や精度差、速度低下に繋がります。
まとめ(「ベクトル化・ブロードキャスト・集約・BLAS」。型とメモリを整えれば速さは出る)
配列計算の高速化は、NumPyに任せる設計に尽きます。Pythonループを捨て、配列演算とブロードキャストで書き、sum/mean/whereなどの集約・条件を使う。線形代数は@/dot/einsumに載せ、dtype統一・事前確保・ビュー活用・in-placeでメモリを最適化する。これらを型として体に入れれば、初心者でも“速くて正確で短い”配列コードが安定して書けます。
