ベクトル化 — ループを捨てるべき理由
このトピックを終えると
ベクトル化がなぜ高速なのかを構造的に説明できるようになり、forループの代わりにNumPy/pandasのベクトル演算でコードを書き換える習慣が身につきます。
直感 — 同じ結果、100倍の差
100万個の数字を2乗する2つの方法:
import numpy as npimport time
data = list(range(1_000_000))arr = np.array(data)
# 方法1: forループstart = time.time()result_loop = [x ** 2 for x in data]print(f"forループ: {time.time() - start:.3f}秒")
# 方法2: NumPyベクトル演算start = time.time()result_vec = arr ** 2print(f"ベクトル化: {time.time() - start:.3f}秒")forループ: 0.142秒
ベクトル化: 0.002秒 ← 約70倍高速結果は同じです。コードはより短いです。速度は数十倍速くなります。
なぜこれほど差が出るのか
Pythonのforループが遅いのは、各反復処理でPythonインタプリタが関与するためです:
forループ (Python):
反復1: 型チェック → オブジェクト生成 → 演算 → オブジェクト保存
反復2: 型チェック → オブジェクト生成 → 演算 → オブジェクト保存
... (100万回反復)
ベクトル演算 (NumPy):
1回: Cコードがメモリ連続データをまとめて処理 → 結果配列を返すNumPyは内部的にC/Fortranコードでループを実行します。Pythonインタプリタのオーバーヘッドがなく、データがメモリに連続して保存されるため、CPUキャッシュを効率的に使用します。
パターン1 — 算術演算
import numpy as np
prices = np.array([1000, 2500, 3000, 1500, 4000])tax_rate = 0.1
# ❌ forループtotal_loop = []for p in prices: total_loop.append(p * (1 + tax_rate))
# ✅ ベクトル化total_vec = prices * (1 + tax_rate)print(total_vec) # [1100. 2750. 3300. 1650. 4400.]prices * (1 + tax_rate) — 配列全体に一度に演算を行います。
パターン2 — 条件フィルタリング
scores = np.array([85, 42, 91, 67, 55, 78, 93, 38])
# ❌ forループpassed = []for s in scores: if s >= 60: passed.append(s)
# ✅ ブールインデックスpassed = scores[scores >= 60]print(passed) # [85 91 67 78 93]scores >= 60 は [True, False, True, True, False, True, True, False] ブール配列を作成し、これをインデックスとして使用すると、Trueの要素のみが抽出されます。
パターン3 — pandasでも同じ
import pandas as pd
df = pd.DataFrame({ "name": ["A", "B", "C", "D", "E"], "revenue": [500, 800, 300, 1200, 900], "cost": [200, 600, 150, 700, 400]})
# ❌ forループprofits = []for _, row in df.iterrows(): profits.append(row["revenue"] - row["cost"])df["profit_loop"] = profits
# ✅ ベクトル化df["profit_vec"] = df["revenue"] - df["cost"]df.iterrows()で行を1つずつ走査することは、pandasではほとんどの場合、間違ったアプローチです。列単位の演算がベクトル化されます。
パターン4 — 条件付き値の割り当て
# ❌ forループfor i, row in df.iterrows(): if row["profit_vec"] > 500: df.at[i, "grade"] = "A" else: df.at[i, "grade"] = "B"
# ✅ np.wheredf["grade"] = np.where(df["profit_vec"] > 500, "A", "B")np.where(条件, 真の場合, 偽の場合) — if/elseを配列全体に一度に適用します。
より複雑な複数の条件:
conditions = [ df["profit_vec"] > 500, df["profit_vec"] > 200, df["profit_vec"] > 0]choices = ["A", "B", "C"]df["grade"] = np.select(conditions, choices, default="D")ベクトル化できない場合
すべての演算がベクトル化可能というわけではありません:
# 前の行の値に依存する累積計算# (各行が前の行の結果を参照する必要があるため、並列化不可)result = [data[0]]for i in range(1, len(data)): result.append(result[-1] + data[i])このような場合は、np.cumsum()のような専用関数を探し、見つからない場合はforループを使用しますが、できるだけ範囲を狭めてください。
ベンチマークルール
| データサイズ | forループ | ベクトル化 | 差 |
|---|---|---|---|
| 1,000 | ~0.2ms | ~0.01ms | ~20x |
| 100,000 | ~20ms | ~0.1ms | ~200x |
| 10,000,000 | ~2s | ~20ms | ~100x |
データが大きいほど、ベクトル化のメリットが大きくなります。
重要なまとめ
| 状況 | 使用する |
|---|---|
| 算術演算 | arr * 2, arr + arr2 |
| 条件フィルタ | arr[arr > 0] |
| 条件割り当て | np.where(), np.select() |
| 集計 | arr.sum(), arr.mean() |
| 行ごとの走査 | ❌ iterrows()の代わりに列演算 |
apply() — ベクトル化とforループの中間
ベクトル演算で表現するのが難しい複雑なロジックには、apply()を使用します:
def categorize(row): if row["revenue"] > 1000 and row["profit_vec"] > 300: return "優秀" elif row["profit_vec"] > 0: return "普通" else: return "赤字"
df["category"] = df.apply(categorize, axis=1)apply()はiterrows()よりも高速ですが、純粋なベクトル演算ほど高速ではありません。複雑なロジックにはapply()、単純な演算にはベクトル化が原則です。
文字列ベクトル化 — .strアクセサ
names = pd.Series(["Alice Smith", "Bob Jones", "Charlie Brown"])
# ❌ forループupper_names = []for name in names: upper_names.append(name.upper())
# ✅ ベクトル化upper_names = names.str.upper()first_names = names.str.split(" ").str[0]has_e = names.str.contains("e", case=False)
print(first_names) # ["Alice", "Bob", "Charlie"]pandasの.strアクセサは、文字列メソッドをベクトル化します。split、replace、contains、extractなどのほとんどの文字列演算をforループなしで実行できます。
リファクタリングチェックリスト
既存のコードでforループを発見した場合、この順序で確認します:
- 算術/比較演算ですか? → 演算子を直接使用 (
arr + 1,arr > 0) - 条件分岐ですか? →
np.where()またはnp.select() - 文字列処理ですか? →
.strアクセサ - 集計ですか? →
.sum()、.mean()、.groupby() - 複雑なロジックですか? →
apply() - 前の行に依存しますか? →
cumsum()、shift()、専用関数を探索 - 上記すべてに該当しない → forループ (最小限の範囲)
groupby — グループごとのベクトル演算
df = pd.DataFrame({ "department": ["営業", "開発", "営業", "開発", "開発"], "salary": [5000, 6000, 4500, 7000, 5500]})
# ❌ forループで部署ごとの平均departments = df["department"].unique()for dept in departments: mask = df["department"] == dept print(f"{dept}: {df.loc[mask, 'salary'].mean()}")
# ✅ groupbyベクトル演算print(df.groupby("department")["salary"].mean())groupby()は、グループごとにベクトル演算を適用します。内部的にCで最適化されているため、forループよりも高速で、コードも簡潔です。
dtypeの注意点
ベクトル演算の速度は、dtypeにも影響を受けます:
# object dtype (文字列が混在する列) → ベクトル化不可mixed = pd.Series([1, "two", 3]) # dtype: object → 遅いclean = pd.Series([1, 2, 3]) # dtype: int64 → 高速
# dtypeの確認print(df.dtypes)# 数値列がobjectとして認識されている場合は変換が必要df["price"] = pd.to_numeric(df["price"], errors="coerce")CSVを読み込む際、数値列に空の文字列が1つでも含まれていると、全体がobject dtypeとなり、ベクトル演算ができなくなります。pd.to_numeric()で変換してから使用します。
「forループを書く前に、ベクトル化された方法がないか最初に確認する」という習慣が、データ処理のパフォーマンスを大きく向上させます。