一覧へ

ベクトル化 — forループを捨てるべき理由

NumPy/pandasのベクトル化演算がforループよりも数十〜数百倍高速である理由を、コードとベンチマークを用いて説明します。

中級
|
10
|
検証済み (2026-07)
ベクトル化vectorizationnumpy パフォーマンスforループの削除配列演算
進捗0/17 (0%)

ベクトル化 — ループを捨てるべき理由

このトピックを終えると

ベクトル化がなぜ高速なのかを構造的に説明できるようになり、forループの代わりにNumPy/pandasのベクトル演算でコードを書き換える習慣が身につきます。


直感 — 同じ結果、100倍の差

100万個の数字を2乗する2つの方法:

python
import numpy as np
import 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 ** 2
print(f"ベクトル化: {time.time() - start:.3f}秒")
text
forループ: 0.142秒
ベクトル化:   0.002秒    ← 約70倍高速

結果は同じです。コードはより短いです。速度は数十倍速くなります。


なぜこれほど差が出るのか

Pythonのforループが遅いのは、各反復処理でPythonインタプリタが関与するためです:

text
forループ (Python):
  反復1: 型チェック → オブジェクト生成 → 演算 → オブジェクト保存
  反復2: 型チェック → オブジェクト生成 → 演算 → オブジェクト保存
  ... (100万回反復)

ベクトル演算 (NumPy):
  1回: Cコードがメモリ連続データをまとめて処理 → 結果配列を返す

NumPyは内部的にC/Fortranコードでループを実行します。Pythonインタプリタのオーバーヘッドがなく、データがメモリに連続して保存されるため、CPUキャッシュを効率的に使用します。


パターン1 — 算術演算

python
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 — 条件フィルタリング

python
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でも同じ

python
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 — 条件付き値の割り当て

python
# ❌ forループ
for i, row in df.iterrows():
if row["profit_vec"] > 500:
df.at[i, "grade"] = "A"
else:
df.at[i, "grade"] = "B"
# ✅ np.where
df["grade"] = np.where(df["profit_vec"] > 500, "A", "B")

np.where(条件, 真の場合, 偽の場合) — if/elseを配列全体に一度に適用します。

より複雑な複数の条件:

python
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")

ベクトル化できない場合

すべての演算がベクトル化可能というわけではありません:

python
# 前の行の値に依存する累積計算
# (各行が前の行の結果を参照する必要があるため、並列化不可)
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()を使用します:

python
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アクセサ

python
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アクセサは、文字列メソッドをベクトル化します。splitreplacecontainsextractなどのほとんどの文字列演算をforループなしで実行できます。


リファクタリングチェックリスト

既存のコードでforループを発見した場合、この順序で確認します:

  1. 算術/比較演算ですか? → 演算子を直接使用 (arr + 1, arr > 0)
  2. 条件分岐ですか?np.where()またはnp.select()
  3. 文字列処理ですか?.strアクセサ
  4. 集計ですか?.sum().mean().groupby()
  5. 複雑なロジックですか?apply()
  6. 前の行に依存しますか?cumsum()shift()、専用関数を探索
  7. 上記すべてに該当しない → forループ (最小限の範囲)

groupby — グループごとのベクトル演算

python
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にも影響を受けます:

python
# 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ループを書く前に、ベクトル化された方法がないか最初に確認する」という習慣が、データ処理のパフォーマンスを大きく向上させます。

💬 質問・コメント

0件のコメント

ログインせずに投稿できます。ゲスト投稿は投稿者自身で編集・削除できません。

0/2000

読み込み中...