【Pandas パフォーマンス最適化】ベクトル化・eval/query・category・downcast・Copy-on-Write 完全ガイド
ボブのapplyスクリプトは10分かかりましたが、アリスはベクトル化に切り替えて30秒で完了しました。20倍の高速化です。これは特殊な例ではありません。Pandasのパフォーマンス問題の90%は「ベクトル化の代わりにループを使うこと」から生じます。本章では、コーディング習慣からエンジンレベルの最適化まで、パフォーマンスツールキットを網羅し、高速かつメモリ効率の良いPandasコードを書く方法を解説します。
⚠️ 注意: 以下のコードを実行するには、ローカルのPython環境が必要です。
1. 学べること
- ❶ ベクトル化 vs apply vs ループ
- ❷ eval / query 式エンジン
- ❸ category によるメモリ最適化
- ❹ downcast による数値圧縮
- ❺ Copy-on-Write とビッグデータ戦略
2. ボブの10分かかるapply
(1) 問題点:行ごとのループは遅すぎる
ボブはiterrowsで10万行を処理し、10分かかりました:
PYTHON
import pandas as pd
import numpy as np
# これはやらないこと — 非常に遅い!
# for i, row in df.iterrows():
# df.loc[i, 'new_col'] = row['a'] * row['b'] + row['c']
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して進めてください。実際の値はpandasのバージョンにより多少異なる場合があります。
(2) 解決策:30秒でベクトル化
▶ サンプル:ベクトル化 vs ループ(難易度 ⭐)
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'a': np.random.randn(100000),
'b': np.random.randn(100000),
'c': np.random.randn(100000)
})
# ❌ 遅い:iterrows(10万行で約300秒)
# for i, row in df.iterrows():
# df.loc[i, 'result'] = row['a'] * row['b'] + row['c']
# ✅ 高速:ベクトル化(約0.03秒)
df['result'] = df['a'] * df['b'] + df['c']
print(f"結果の平均: {df['result'].mean():.4f}")
# 約30秒 vs 約0.03秒 — 1000倍高速!
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して進めてください。実際の値はpandasのバージョンにより多少異なる場合があります。
3. パフォーマンスランキング:ループ → apply → ベクトル化
(1) 4つの手法の比較
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して進めてください。実際の値はpandasのバージョンにより多少異なる場合があります。
:4つの手法の速度比較(難易度 ⭐⭐)
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'a': np.random.randn(50000),
'b': np.random.randn(50000)
})
# 手法1:iterrows(最も遅い — 計算には絶対に使わない)
# 5万行で約60秒
# 手法2:apply(遅い)
result2 = df.apply(lambda row: row['a'] + row['b'], axis=1)
# 手法3:ベクトル化(高速)
result3 = df['a'] + df['b']
# 手法4:numpy(最速)
result4 = (df['a'].values + df['b'].values)
# 同じ結果になることを検証
print(np.allclose(result2.values, result3.values)) # True
print(np.allclose(result3.values, result4)) # True
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して進めてください。実際の値はpandasのバージョンにより多少異なる場合があります。
(2) パフォーマンスランキング表
| 順位 | 手法 | 相対速度 | 最適な用途 |
|---|---|---|---|
| 1 | NumPy 演算 | 最速(1倍) | 純粋な数値計算 |
| 2 | Pandas ベクトル化 | 高速(1〜2倍) | ラベル + 数値 |
| 3 | map(辞書) | 中程度(5〜10倍) | 1対1のマッピング |
| 4 | apply | 遅い(50〜100倍) | 複雑なロジック |
| 5 | itertuples | 非常に遅い(200倍) | 行の反復処理が必須の場合 |
| 6 | iterrows | 最遅(500倍) | 絶対に使わない |
🔥 黄金律:可能な限りベクトル化を使うこと。できない場合はapplyを使うこと。ループは最後の手段です。iterrowsは常に最後の選択肢(あるいは選択肢にしない)にすべきです。
4. eval / query 式エンジン
(1) eval は複雑な演算を高速化する
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して進めてください。実際の値はpandasのバージョンにより多少異なる場合があります。
:eval 式エンジン(難易度 ⭐⭐)
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'a': np.random.randn(100000),
'b': np.random.randn(100000),
'c': np.random.randn(100000),
'd': np.random.randn(100000)
})
# 複雑な式 — eval は中間データフレームを回避する
result1 = df['a'] * df['b'] + df['c'] / df['d'] - df['a'] ** 2
result2 = df.eval('a * b + c / d - a ** 2')
print(np.allclose(result1, result2)) # True
# eval が高速になる場合:
# 1. 式に含まれる列が多い(4列超)
# 2. データフレームが大きい(1万行超)
# 3. メモリが限られている(中間結果を回避)
# query によるフィルタリング
filtered = df.query('a > 0 and b < 0')
# 同等の処理:df[(df['a'] > 0) & (df['b'] < 0)]
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して進めてください。実際の値はpandasのバージョンにより多少異なる場合があります。
(2) eval vs 通常の演算
| シナリオ | eval の利点 | eval の欠点 |
|---|---|---|
| 複数列の複雑な式 | 中間変数が少ない | 構文が限定的 |
| 大きなデータフレーム | メモリを節約 | 小さなデータでは効果なし |
| 単純な演算 | 利点なし | 可読性が低い |
| 条件フィルタリング(query) | 可読性が高い | 柔軟性が限定的 |
5. category によるメモリ最適化
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して進めてください。実際の値はpandasのバージョンにより多少異なる場合があります。
:category 型による圧縮(難易度 ⭐⭐)
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
# シミュレーション:10万行、ユニークな都市は10件
df = pd.DataFrame({
'city': np.random.choice(['NYC', 'LA', 'Chicago', 'Houston', 'Phoenix',
'Philly', 'San Antonio', 'San Diego', 'Dallas', 'Austin'], 100000),
'status': np.random.choice(['Active', 'Inactive', 'Pending', 'Churned'], 100000)
})
# 変換前:object 型
mem_before = df.memory_usage(deep=True).sum()
print(f"変換前(object): {mem_before / 1024:.1f} KB")
# 変換後:category 型
df['city'] = df['city'].astype('category')
df['status'] = df['status'].astype('category')
mem_after = df.memory_usage(deep=True).sum()
print(f"変換後(category): {mem_after / 1024:.1f} KB")
print(f"削減率: {(1 - mem_after / mem_before) * 100:.1f}%")
# 低カーディナリティの文字列では通常80〜95%のメモリ削減!
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して進めてください。実際の値はpandasのバージョンにより多少異なる場合があります。
(1) category を使うべき場面
| 条件 | 向いている | 向いていない |
|---|---|---|
| ユニーク値が総行数の50%未満 | ✅ | ❌ |
| 文字列の列 | ✅ | — |
| 順序に意味がある | ✅ | 順序のないランダムな文字列 |
| 文字列メソッドが必要 | ❌(.str は限定的) | ✅(object を使う) |
| groupby を頻繁に行う | ✅(より高速) | — |
6. downcast による数値圧縮
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して進めてください。実際の値はpandasのバージョンにより多少異なる場合があります。
:downcast による数値型の変換(難易度 ⭐⭐)
PYTHON
import pandas as pd
import numpy as np
np.random.seed(42)
df = pd.DataFrame({
'id': np.arange(100000),
'age': np.random.randint(0, 120, 100000),
'score': np.random.randint(0, 100, 100000),
'price': np.round(np.random.uniform(0, 1000, 100000), 2),
'flag': np.random.choice([0, 1], 100000)
})
# 現在の型とメモリを確認
print("変換前:")
print(df.dtypes)
print(f"メモリ: {df.memory_usage(deep=True).sum() / 1024:.1f} KB")
# 整数のダウンキャスト
df['id'] = pd.to_numeric(df['id'], downcast='unsigned') # uint32 → uint32
df['age'] = pd.to_numeric(df['age'], downcast='unsigned') # int64 → uint8(0〜255)
df['score'] = pd.to_numeric(df['score'], downcast='unsigned')
df['flag'] = pd.to_numeric(df['flag'], downcast='unsigned') # int64 → uint8
# 浮動小数点数のダウンキャスト
df['price'] = pd.to_numeric(df['price'], downcast='float') # float64 → float32
print("\n変換後:")
print(df.dtypes)
print(f"メモリ: {df.memory_usage(deep=True).sum() / 1024:.1f} KB")
# 典型的な削減率:50〜75%
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して進めてください。実際の値はpandasのバージョンにより多少異なる場合があります。
7. Copy-on-Write とビッグデータ戦略
(1) Copy-on-Write(CoW)
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して進めてください。実際の値はpandasのバージョンにより多少異なる場合があります。
:CoW の挙動(難易度 ⭐⭐)
PYTHON
import pandas as pd
import numpy as np
# Pandas 3.x:CoW がデフォルト
pd.options.mode.copy_on_write = True
df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
# CoW あり:スライスはビューを返し、変更時にコピーが作成される
subset = df[['a']] # 変更されるまでコピーなし
df.loc[0, 'a'] = 99 # df を変更、subset は不変
print(subset) # 依然として [1, 2, 3] — CoW が保護
# CoW 下での安全なパターン:
df['c'] = df['a'] + df['b'] # ✅ 新しい列を代入
df = df.drop(columns='c') # ✅ 再代入
# 避けるべきこと:
# subset['a'] = 10 # ❌ SettingWithCopyWarning(CoW 以前の問題、CoW により解消)
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して進めてください。実際の値はpandasのバージョンにより多少異なる場合があります。
(2) ビッグデータ戦略
| データサイズ | 戦略 |
|---|---|
| 1GB 未満 | 標準の Pandas |
| 1〜5GB | downcast + category + eval |
| 5〜50GB | chunksize による分割処理 |
| 50GB 超 | Dask / Polars / PySpark を検討 |
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して進めてください。実際の値はpandasのバージョンにより多少異なる場合があります。
:chunksize による分割処理(難易度 ⭐)
PYTHON
import pandas as pd
from io import StringIO
# 大きな CSV をシミュレーション
csv_data = "id,value\n" + "\n".join([f"{i},{i*10}" for i in range(1000)])
# チャンク単位で処理
total = 0
for chunk in pd.read_csv(StringIO(csv_data), chunksize=200):
total += chunk['value'].sum()
print(f"合計: {total}")
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して進めてください。実際の値はpandasのバージョンにより多少異なる場合があります。
8. 総合例:100万行のエンドツーエンド最適化
(6) ▶ パフォーマンス最適化チェックリスト
graph TB
A[パフォーマンス最適化] --> B[1. ループよりベクトル化]
A --> C[2. eval / query による高速化]
A --> D[3. category による文字列圧縮]
A --> E[4. downcast による数値圧縮]
A --> F[5. chunksize による分割処理]
A --> G[6. Copy-on-Write]
B --> H[100〜500倍の高速化]
C --> I[メモリ節約、中間結果を回避]
D --> J[80〜95%のメモリ削減]
E --> K[50〜75%のメモリ削減]
F --> L[メモリを超えるデータの処理]
G --> M[ゼロコピーのスライス]
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して進めてください。実際の値はpandasのバージョンにより多少異なる場合があります。
▶ サンプル
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して進めてください。実際の値はpandasのバージョンにより多少異なる場合があります。
:遅いコードから高速なコードへ、完全なワークフロー(難易度 ⭐⭐⭐)
PYTHON
import pandas as pd
import numpy as np
# ============================================
# 総合例:100万行の最適化
# iterrows → apply → ベクトル化 → eval → category
# ============================================
np.random.seed(42)
df = pd.DataFrame({
'city': np.random.choice([f'City_{i}' for i in range(20)], 1000000),
'category': np.random.choice(['Electronics', 'Clothing', 'Home', 'Sports'], 1000000),
'price': np.round(np.random.uniform(10, 500, 1000000), 2),
'quantity': np.random.randint(1, 100, 1000000),
'discount': np.random.choice([0, 0.1, 0.2, 0.3], 1000000)
})
# ステップ1:ベースラインのメモリ
mem1 = df.memory_usage(deep=True).sum() / (1024**2)
print(f"ステップ1 — 生のメモリ: {mem1:.1f} MB")
# ステップ2:数値のダウンキャスト
df['quantity'] = pd.to_numeric(df['quantity'], downcast='unsigned')
df['price'] = pd.to_numeric(df['price'], downcast='float')
df['discount'] = pd.to_numeric(df['discount'], downcast='float')
mem2 = df.memory_usage(deep=True).sum() / (1024**2)
print(f"ステップ2 — downcast 後: {mem2:.1f} MB({(1-mem2/mem1)*100:.0f}% 削減)")
# ステップ3:文字列に category を適用
df['city'] = df['city'].astype('category')
df['category'] = df['category'].astype('category')
mem3 = df.memory_usage(deep=True).sum() / (1024**2)
print(f"ステップ3 — category 後: {mem3:.1f} MB(累計 {(1-mem3/mem1)*100:.0f}% 削減)")
# ステップ4:ベクトル化による計算(高速)
df['revenue'] = df['price'] * df['quantity'] * (1 - df['discount'])
print(f"ステップ4 — 売上を計算(ベクトル化、約0.01秒)")
# ステップ5:eval による代替
df['revenue2'] = df.eval('price * quantity * (1 - discount)')
print(f"ステップ5 — eval による売上計算")
# 検証
print(f"結果の一致: {np.allclose(df['revenue'], df['revenue2'])}")
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して進めてください。実際の値はpandasのバージョンにより多少異なる場合があります。
❓ よくある質問
Q iterrows と itertuples の違いは何ですか?
A iterrows は(インデックス, Series)のペアを返し、毎回新しい Series を作成するため非常に遅い(ベクトル化より約500倍遅い)です。itertuples は(インデックス, namedtuple)のペアを返し、Series を作成しないため、iterrows より5〜10倍高速ですが、それでもベクトル化より50倍遅いです。行ごとに反復処理が必要な場合は itertuples を使いますが、まずベクトル化が可能かどうかを検討してください。
Q eval はどのようなときに高速になりますか?
A eval は大きなデータ(1万行超)と複数列の複雑な式で高速になります。中間データフレームの作成を回避し、numexpr エンジンを使って高速化するからです。小さなデータや単純な式では、eval はむしろ遅くなります(エンジンの起動オーバーヘッド)。経験則として、eval は大きなデータセットで4列以上の複合演算を行う場合にのみ有効です。
Q inplace は本当にメモリを節約しますか?
A Pandas 2.x では、inplace は通常メモリを節約しません。内部的には代入前に一時コピーを作成する可能性があるためです。Pandas 3.x の Copy-on-Write モードでは、inplace と非 inplace 操作のメモリオーバーヘッドは本質的に同じです。推奨:inplace は避け、より明確なセマンティクスのために
df = df.method() という代入パターンを使ってください。Q Copy-on-Write とは何ですか?
A CoW は Pandas 3.x のデフォルトの挙動で、データは内部メモリを共有し、変更が発生したときのみコピーが作成されます。利点:スライスがコピーしない(メモリ節約)、連鎖代入が安全(SettingWithCopyWarning なし)。影響:一部の inplace 操作の挙動が異なり、再代入が必要になります。
Q 極めて大きなファイルの場合はどうすればよいですか?
A 3段階の戦略があります。① chunksize による分割読み込み + 集計(分割可能な統計に向く)、② downcast/category による行ごとのメモリ削減(メモリにより多くの行を収める)、③ ツールの乗り換え — Dask(分散 Pandas API)、Polars(Rust で書かれた超高速データフレーム)、PySpark(クラスタレベル)。単一マシンのメモリ上限はおおよそデータサイズの3〜5倍です。
Q apply は常にベクトル化より遅いのですか?
A ほとんどの場合、そうです。apply は Python 関数を行ごとに呼び出しますが、ベクトル化は C レベルで実行されるためです。まれな例外:ベクトル化で複数の巨大な中間配列を作成する必要がある場合、apply の方がメモリを節約できることがあります(ただし時間は節約できません)。戦略:まずベクトル化を優先し、パフォーマンスが不十分なら eval を試し、apply は最後の手段として使ってください。
Q memory_usage はどう使いますか?
A
df.memory_usage(deep=True) は各列のバイト数を返します。deep=True は object 列の実際の文字列メモリを計算します(そうでない場合はポインタサイズのみをカウント)。合計は df.memory_usage(deep=True).sum() です。この関数で最適化の前後を比較してください。category 列は整数コード + 辞書のみを格納するため、object よりずっと少ないメモリで済みます。📖 まとめ
- 黄金律:ベクトル化 > map > apply > itertuples > iterrows
- eval/query は numexpr エンジンで複雑な式を高速化 — 列の多い大きなデータで有効
- category 型は低カーディナリティの文字列列で80〜95%のメモリを節約
- downcast は int64 → uint8/uint16/uint32、float64 → float32 に変換し、50〜75%を節約
- Copy-on-Write はゼロコピーのスライスと安全な代入を実現
- ビッグデータ戦略:downcast + category → chunksize → Dask/Polars
📝 練習問題
- 基礎(難易度 ⭐):1万行のデータフレームを作成し、apply とベクトル化の両方で新しい列を計算して、memory_usage を比較してください。
- 応用(難易度 ⭐⭐):object 列を持つ10万行のデータフレームを作成し、category + downcast でメモリを最適化して、削減率を計算してください。
- 挑戦(難易度 ⭐⭐⭐):100万行のデータフレーム(category/float/int 列を含む)を作成し、以下を完了してください:ベクトル化による計算 → eval による計算 → 結果の比較 → memory_usage を通して追跡 → 完全な最適化レポートの作成。



