pandas-performance について
pandas-performance は、pandas の速度とメモリ使用量を改善するスキルで、大規模な DataFrame、遅い読み込み、処理のボトルネックに取り組むデータ担当者向けです。dtype の最適化、ベクトル化、Arrow バックエンド、チャンク処理、効率的な入出力を扱います。
主な機能
- 01ビット幅の小さい dtype でメモリ削減
- 02行単位の処理をベクトル化で置き換え
- 03PyArrow バックエンドでデータを読み込み
- 04Parquet の列と行を絞って読み込み
- 05大容量ファイルをチャンク処理
- 06検索、結合、重複排除を最適化
活用シーン
- 01大規模 DataFrame のメモリ負荷を軽減
- 02大きな CSV の読み込みを高速化
- 03数百万行の計算や検索を最適化
- 04機械学習向けデータを効率よく準備
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ tondevrel/ scientific-agent-skills --skill pandas-performanceインストール後、Claude Code で/pandas-performanceを入力して読み込まれたか確認します。
詳細
pandas - パフォーマンスとメモリ管理
標準的な pandas コードは、メモリを大量に消費し、処理も遅くなりがちです。このサブスキルでは、内部アーキテクチャ(BlockManager と Arrow バックエンド)を理解することで、pandas を 10 倍高速化し、RAM 使用量を 5 分の 1 に抑える手法を紹介します。
使用する場面
- DataFrame が 1GB を超え、RAM に負荷がかかっている場合。
pd.read_csvによるデータの読み込みに時間がかかりすぎる場合。- 行単位の処理(
apply、iterrows)がボトルネックになっている場合。 - 数百万行に対して複雑な結合や検索を実行する必要がある場合。
- 高性能な ML モデル向けにデータを準備する場合。
参考ドキュメント
- 公式パフォーマンスガイド: https://pandas.pydata.org/docs/user_guide/enhancingperf.html
- 大規模データへのスケーリング: https://pandas.pydata.org/docs/user_guide/scale.html
- 検索パターン:
df.memory_usage、pd.to_numeric(downcast=...)、pd.Categorical、DataFrame.eval()
基本原則
RAM がボトルネックです
pandas は通常、処理中にデータのコピーを作成します。大規模データを扱うには、コピーを最小限に抑え、データ型に最も適したビット幅を使う必要があります。
ベクトル化とループ
- レベル 1(最良):NumPy/pandas の組み込みベクトル化関数。
- レベル 2(良好):複雑な計算には
df.eval()またはdf.query()。 - レベル 3(平均的):
np.vectorizeまたはdf.apply()(ロジックが複雑な場合のみ)。 - レベル 4(最低):
iterrows()またはitertuples()。
メモリ最適化パターン
1. 「ダウンキャスト」ワークフロー
標準の整数列と浮動小数点数列は、デフォルトで 64 ビットを使用します。科学データの大半は 16 ビットまたは 32 ビットで十分です。
import pandas as pd
import numpy as np
def optimize_memory(df):
start_mem = df.memory_usage().sum() / 1024**2
for col in df.columns:
col_type = df[col].dtype
if col_type != object:
c_min = df[col].min()
c_max = df[col].max()
if str(col_type)[:3] == 'int':
if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
df[col] = df[col].astype(np.int8)
elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
df[col] = df[col].astype(np.int16)
else:
if c_min > np.finfo(np.float32).min and c_max < np.finfo(np.float32).max:
df[col] = df[col].astype(np.float32)
else:
# Convert low-cardinality strings to Categorical
num_unique = df[col].nunique()
if num_unique / len(df) < 0.5:
df[col] = df[col].astype('category')
end_mem = df.memory_usage().sum() / 1024**2
print(f'Memory reduced by {100 * (start_mem - end_mem) / start_mem:.1f}%')
return df
2. 最新の PyArrow バックエンド(Pandas 2.0 以降)
文字列操作を大幅に高速化し、読み込みも速くするには、Arrow バックエンドを使用します。
# Load with Arrow engine for 2-3x speedup
df = pd.read_csv("data.csv", engine="pyarrow", dtype_backend="pyarrow")
速度最適化パターン
1. 複雑な「if-else」のベクトル化(.apply の代わり)
各行に対して Python 関数を呼び出す代わりに、次のようにします。
# ❌ SLOW:
# df['status'] = df.apply(lambda x: 'High' if x['val'] > 100 else 'Low', axis=1)
# ✅ FAST:
df['status'] = np.where(df['val'] > 100, 'High', 'Low')
# ✅ FAST (Multiple conditions):
conditions = [
(df['val'] > 100),
(df['val'] > 50) & (df['val'] <= 100),
(df['val'] <= 50)
]
choices = ['High', 'Medium', 'Low']
df['status'] = np.select(conditions, choices, default='Unknown')
2. 高速な検索
辞書や別のテーブルから値を数百万回にわたって対応付ける必要がある場合:
# ❌ SLOW: df.merge() or df['id'].map(large_dict)
# ✅ FAST: Use a Series as a lookup table with index
lookup_table = pd.Series(data=values, index=keys)
result = lookup_table.reindex(df['target_ids']).values
効率的な I/O
1. フィルタリング付き Parquet(述語プッシュダウン)
大規模データの保存に CSV は使わないでください。Parquet を使用します。
# Save as partitioned parquet
df.to_parquet('data_dir', partition_cols=['year', 'month'])
# Load only specific columns and rows (Fast)
df_subset = pd.read_parquet('data_dir', columns=['price', 'id'],
filters=[('year', '==', 2023)])
2. メモリが限られたシステム向けのチャンク処理
ファイルが 50GB で、RAM が 16GB の場合:
# Process in chunks of 100k rows
chunk_size = 100_000
for chunk in pd.read_csv("massive.csv", chunksize=chunk_size):
# Perform aggregation
summary = chunk.groupby('id')['value'].sum()
# Save or update a running total
パフォーマンスに関する重要なルール
✅ 推奨事項
- インプレース操作は控えめに使う - 一般に信じられていることとは異なり、
inplace=Trueでも内部的にコピーが作られることがよくあります。代わりにデータ型に注目してください。 - スライスする前にインデックスをソートする - 大きな DataFrame をインデックスでスライスする場合は、次のようにインデックスをソートしてください:
df.sort_index(inplace=True)。これにより、O(N) の処理が O(log N) になります。 - format を指定して pd.to_datetime を使う - 書式(
%Y-%m-%d)を指定すると、自動解析よりも大幅に高速になります。 - .eval() を活用する -
(A + B) / (C * D)のような複雑な算術処理では、numexpr を使用するdf.eval()のほうが高速でメモリ効率も優れています。
❌ 非推奨事項
- iterrows() で反復処理しない - 各行を Series オブジェクトに変換するため、非常に遅くなります。
- object 型は避ける - object 型の列(通常は文字列)は Python オブジェクトへのポインターであり、多くのメモリを消費します。
categoryまたはstring[pyarrow]を使用してください。 - ループ内で append() を使わない - 呼び出すたびに DataFrame 全体のコピーが作成されます。データをリストに収集してから
pd.concat()を使用してください。
アンチパターン(絶対に避ける)
# ❌ BAD: Growing a DataFrame row by row
df = pd.DataFrame()
for data in large_source:
df = pd.concat([df, pd.DataFrame([data])]) # ❌ Disaster for performance!
# ✅ GOOD: List of dicts to DataFrame
data_list = []
for data in large_source:
data_list.append(data)
df = pd.DataFrame(data_list)
# ❌ BAD: Manual string formatting
# df['name'].apply(lambda x: f"USER_{x}")
# ✅ GOOD: Vectorized string accessor
df['name'] = "USER_" + df['name'].astype(str)
実践的なワークフロー
1. メモリを大量に消費する列を特定する
# Get detailed memory breakdown (including object overhead)
print(df.memory_usage(deep=True))
# Identify columns with too many unique strings (bad for 'category')
for col in df.select_dtypes(include=['object']):
print(f"{col}: {df[col].nunique() / len(df):.2%}")
2. 1,000 万行以上を高速に重複排除する
# Using sorting + shift is often faster than drop_duplicates
df = df.sort_values(['id', 'timestamp'])
mask = (df['id'] != df['id'].shift())
df_unique = df[mask]
3. マルチインデックスで結合する
# If you join on multiple columns, setting them as an index
# and using join() can be 5x faster than merge()
df1.set_index(['key1', 'key2'], inplace=True)
df2.set_index(['key1', 'key2'], inplace=True)
result = df1.join(df2, how='inner')
このサブスキルにより、pandas はプロトタイピング用ツールから、産業規模の科学データを処理できる高性能エンジンへと変わります。