404 Not Found

404 Not Found


nginx

【Pandas パフォーマンス最適化】ベクトル化・eval/query・category・downcast・Copy-on-Write 完全ガイド

ボブのapplyスクリプトは10分かかりましたが、アリスはベクトル化に切り替えて30秒で完了しました。20倍の高速化です。これは特殊な例ではありません。Pandasのパフォーマンス問題の90%は「ベクトル化の代わりにループを使うこと」から生じます。本章では、コーディング習慣からエンジンレベルの最適化まで、パフォーマンスツールキットを網羅し、高速かつメモリ効率の良いPandasコードを書く方法を解説します。

⚠️ 注意: 以下のコードを実行するには、ローカルのPython環境が必要です。

1. 学べること



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) ▶ パフォーマンス最適化チェックリスト

100%
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 よりずっと少ないメモリで済みます。

📖 まとめ


📝 練習問題

  1. 基礎(難易度 ⭐):1万行のデータフレームを作成し、apply とベクトル化の両方で新しい列を計算して、memory_usage を比較してください。
  2. 応用(難易度 ⭐⭐):object 列を持つ10万行のデータフレームを作成し、category + downcast でメモリを最適化して、削減率を計算してください。
  3. 挑戦(難易度 ⭐⭐⭐):100万行のデータフレーム(category/float/int 列を含む)を作成し、以下を完了してください:ベクトル化による計算 → eval による計算 → 結果の比較 → memory_usage を通して追跡 → 完全な最適化レポートの作成。

← 前へ:可視化 · 次へ:スタイル付き出力 →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%