0Pricing
Pandas & NumPy Academy · レッスン

timeitとmemory_profilerによるプロファイリング

%timeitで実行時間を、memory_profilerでピークメモリ使用量を測定し、パイプラインの最も遅い部分を特定します。

「timeitとmemory_profilerによるプロファイリング」はCoddyKit上の無料Pandas & NumPy Academyレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPandas & NumPy Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Pandas & NumPy Academyコースには全4レッスンが含まれています。

最適化の前にプロファイリングする理由

よくある間違いは、コードを直感的に最適化することです。つまり、実際にボトルネックかどうかを測定せずに、遅そうに見える関数を書き換えてしまうことです。データパイプラインでは、実行時間の90%がコードの10%で発生することがよくあります。プロファイリングを行うと、時間とメモリが正確にどこで消費されているかが分かるため、重要な箇所に絞って最適化できます。基本原則は、まず測定し、その後で最適化することです。timeitなどのツールは時間を測定し、memory_profilerはRAM使用量を測定します。

import pandas as pd
import numpy as np
import timeit

# Naive approach vs vectorised approach — which is faster?
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(100000), 'b': np.random.randn(100000)})

t1 = timeit.timeit(lambda: df['a'] + df['b'], number=100)
t2 = timeit.timeit(lambda: [a + b for a, b in zip(df['a'], df['b'])], number=10)

print(f'Vectorised (100 runs): {t1:.3f}s')
print(f'List comprehension (10 runs): {t2:.3f}s')
print(f'Per-run speedup: ~{(t2/10)/(t1/100):.0f}x')

timeit:Python組み込みのタイマー

timeitモジュールは、文を何度も実行して経過時間の合計を返すことで、実行時間を測定します。timeit.timeit(stmt, number=n)はstmtをn回実行し、合計秒数を返します。numberで割ると、1回あたりの実行時間が得られます。正確に測定するには、合計実行時間が少なくとも0.1秒になるようにnumberを選んでください。高速な処理を1回だけ実行しても、OSのスケジューリングによるノイズのため、代表的な結果にはなりません。

import timeit
import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(10000)})

# Compare three ways to square a column
time_a = timeit.timeit(lambda: df['x'] ** 2, number=1000)
time_b = timeit.timeit(lambda: df['x'].apply(lambda v: v**2), number=100)
time_c = timeit.timeit(lambda: np.square(df['x']), number=1000)

print(f'Operator **2  per run: {time_a/1000*1000:.3f} ms')
print(f'.apply(v**2) per run: {time_b/100*1000:.3f} ms')
print(f'np.square()  per run: {time_c/1000*1000:.3f} ms')

Jupyterの%timeitマジック

Jupyter Notebookでは、%timeitマジックが反復回数と実行回数を自動的に選択し、統計的に安定した推定値を提示します。その後、標準偏差とともに最良時間と平均時間を報告します。1行の処理をすばやくベンチマークする最も便利な方法です。セル全体の時間を測定するには、%%timeit(パーセント記号を2つ)を使用します。出力形式は、X ns/μs/ms ± Y ns per loop (mean ± std. dev. of 7 runs, N loops each)です。

# In Jupyter notebook:
# %timeit df['x'] ** 2
# Output: 47.3 us +- 1.2 us per loop (mean +- std. dev. of 7 runs, 10000 loops each)

# Multi-line cell timing:
# %%timeit
# result = df['x'] ** 2
# total = result.sum()

# In a regular Python script, use timeit.timeit() instead:
import timeit
import pandas as pd, numpy as np
df = pd.DataFrame({'x': np.random.randn(10000)})
result = timeit.repeat(lambda: df['x']**2, number=1000, repeat=7)
print(f'Best run: {min(result)/1000*1000:.3f} ms per call')

関数単位のプロファイリングに使うcProfile

timeitが単一の式を測定するのに対し、cProfileは呼び出しスタック全体をプロファイリングします。実行中に呼び出された各関数で、どれだけの時間が費やされたかを確認できます。複雑なパイプラインのボトルネックを特定するうえで不可欠です。コマンドラインからpython -m cProfile -s cumulative script.pyを実行するか、スクリプト内でcProfile.run('function()')を使用してください。Jupyterでは%prun function()を使用します。

import cProfile
import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(50000), 'b': np.random.randn(50000)})

def slow_pipeline(df):
    result = []
    for _, row in df.iterrows():
        result.append(row['a'] * row['b'])
    return pd.Series(result)

# Profile the function
print('Profile output (top 5 functions by cumulative time):')
cProfile.run('slow_pipeline(df.head(1000))', sort='cumulative')

メモリ使用量の測定

df.memory_usage(deep=True)を使うと、各列が消費するRAMの容量を確認できます。deep=True引数は、object型の列(文字列)が実際に使用しているメモリを測定します。これを指定しない場合は、ポインタのサイズしか報告されません。.sum()を使うと、DataFrame全体のメモリ使用量をバイト単位で取得できます。MBに変換するには1e6で、GBに変換するには1e9で割ります。これはメモリ最適化の出発点です。測定していないものを減らすことはできません。

import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({
    'id': range(100000),
    'name': ['user_' + str(i) for i in range(100000)],
    'score': np.random.randn(100000),
    'category': np.random.choice(['A', 'B', 'C', 'D'], 100000)
})

mem = df.memory_usage(deep=True)
print('Memory per column (bytes):')
print(mem)
print(f'\nTotal: {mem.sum() / 1e6:.2f} MB')

行単位のメモリ測定に使うmemory_profiler

memory_profilerパッケージ(pip install memory-profiler)は、関数内のRAM使用量を行単位で測定します。関数に@profileデコレーターを付け、python -m memory_profiler script.pyで実行してください。Jupyterでは、%memitマジック(ipython拡張機能から提供)を使って、単一の式によるピークメモリを測定できます。これにより、どのデータ構造がメモリ使用量の急増を引き起こしているかを特定できます。多くの場合、ループ内で連結を繰り返すことが原因であり、データを収集してから一度に連結するほうが効率的です。

# Install: pip install memory-profiler
# Usage as a decorator:

# from memory_profiler import profile
# @profile
# def process_data():
#     import pandas as pd, numpy as np
#     df = pd.DataFrame({'x': np.random.randn(1000000)})
#     result = df['x'].rolling(100).mean()    # <-- sees if this spikes RAM
#     return result

# Run: python -m memory_profiler script.py

# In Jupyter (after %load_ext memory_profiler):
# %memit pd.DataFrame({'x': np.random.randn(1000000)}).rolling(100).mean()

print('memory_profiler decorates functions for line-by-line RAM measurement.')
print('Typical output: Line # Mem usage Increment Line Contents')

オブジェクトにおけるsys.getsizeofとmemory_usageの比較

sys.getsizeof(obj)はPythonオブジェクトのメモリサイズを返しますが、pandasのDataFrameに対してはコンテナのオーバーヘッド(数百バイト程度)しか報告せず、実際のデータは含まれません。DataFrameには必ずdf.memory_usage(deep=True).sum()を使用してください。リストや辞書など個々のPythonオブジェクトではsys.getsizeofは正確ですが、ネストされたオブジェクトまでは再帰的に調べません。オブジェクト全体のサイズを調べるには、pymplerライブラリのasizeofを使用してください。

import sys
import pandas as pd
import numpy as np

df = pd.DataFrame({'x': np.random.randn(100000)})

# sys.getsizeof reports only the container metadata — misleading!
print('sys.getsizeof(df):', sys.getsizeof(df), 'bytes (misleading!)')

# memory_usage gives the true memory
true_mem = df.memory_usage(deep=True).sum()
print('df.memory_usage(deep=True).sum():', true_mem, 'bytes')
print(f'True memory: {true_mem/1e6:.2f} MB')

タイミング用コンテキストマネージャーパターン

timeitが適さない長時間のパイプライン処理(データベースクエリやファイルI/Oなど)には、time.perf_counter()を使ったシンプルなコンテキストマネージャーを使用してください。各パイプライン処理をタイマーコンテキストで囲み、所要時間をログに記録します。これは、timeitよりも本番コードに適しています。I/Oの待機時間を含む実際の経過時間を記録でき、timeitでも測定自体はできますが、ロギングフレームワークとの統合が難しいためです。

import time
import contextlib
import pandas as pd
import numpy as np

@contextlib.contextmanager
def timer(name):
    start = time.perf_counter()
    yield
    elapsed = time.perf_counter() - start
    print(f'[{name}] {elapsed*1000:.1f} ms')

np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(500000), 'y': np.random.randn(500000)})

with timer('rolling mean'):
    result1 = df['x'].rolling(30).mean()

with timer('groupby mean'):
    df['group'] = df['x'].round(0)
    result2 = df.groupby('group')['y'].mean()

Pandas操作を体系的にプロファイリングする

体系的なプロファイリングの手順: 1) データの代表的なサンプルから始めます(時間比較には10,000行で十分です)。2) タイマーコンテキストまたはcProfileを使って、最も遅い処理を特定します。3) 最も遅い処理を、より高速な方法(ベクトル化、Categorical dtype、C拡張)で書き直します。4) assert文で正しさを確認します。5) 速度を再計測して、向上を確認します。6) パイプラインがSLAを満たすまで、次のボトルネックについて繰り返します。

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({
    'value': np.random.randn(100000),
    'category': np.random.choice(['A', 'B', 'C', 'D', 'E'], 100000)
})

# Benchmark two groupby implementations
t1 = timeit.timeit(lambda: df.groupby('category')['value'].mean(), number=100)
df2 = df.copy()
df2['category'] = df2['category'].astype('category')
t2 = timeit.timeit(lambda: df2.groupby('category')['value'].mean(), number=100)

print(f'Object dtype groupby: {t1/100*1000:.2f} ms per call')
print(f'Categorical dtype:    {t2/100*1000:.2f} ms per call')
print(f'Speedup: {t1/t2:.1f}x')

プロファイリングを行うタイミング: 実用的な目安

すべてのスクリプトにプロファイリングが必要なわけではありません。次のような場合は、プロファイリングに取り組む価値があります。パイプラインの実行に30秒以上かかり、頻繁(毎日以上)に実行される場合、1つの関数が100,000行を超えるデータを処理する場合、またはメモリ使用量が利用可能なRAMの50%を超える場合です。1回だけ実行するスクリプトや数秒で終わるスクリプトでは、マイクロ秒単位の最適化よりも読みやすさが重要です。高度に最適化された分かりにくいコードは、最適化による短縮時間を上回る保守コストがかかることがよくあります。

スナップショット: プロファイリングツールの概要

クイックリファレンス:

  • timeit.timeit — 1つの文(スクリプト)の実行時間を測定
  • %timeit / %%timeit — Jupyterで実行時間を測定(自動的に繰り返し実行)
  • %prun / cProfile — 関数レベルのコールスタックプロファイリング
  • df.memory_usage(deep=True) — 列ごとのRAM使用量を測定
  • memory_profiler @profile — 行ごとのRAM使用量を測定
  • time.perf_counter() — パイプラインの各処理を手動で計測するタイマー
本番環境に移行する前に、必ず代表的なデータサンプルでプロファイリングを行い、assertによるチェックで最適化の効果を確認してください。

クイックチェック

このレッスンで学んだプロファイリングツールの理解度を確認しましょう。

レッスンのまとめ

このレッスンでは、timeit.timeitと%timeitで特定の処理の実行時間を測定する方法、df.memory_usage(deep=True)で列ごとのRAM使用量を正確に測定する方法、そしてcProfile / memory_profilerで複雑なパイプラインをより詳しくプロファイリングする方法を学びました。最適化する前には必ずプロファイリングを行い、変更するたびに正しさを確認してください。次は、遅いiterrowsループを避け、ベクトル化された処理に置き換える方法を学びます。

よくある質問

「timeitとmemory_profilerによるプロファイリング」レッスンは無料ですか?

はい。「timeitとmemory_profilerによるプロファイリング」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Pandas & NumPy Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Pandas & NumPy Academyコースには全4レッスンが含まれています。

「timeitとmemory_profilerによるプロファイリング」で何を学びますか?

%timeitで実行時間を、memory_profilerでピークメモリ使用量を測定し、パイプラインの最も遅い部分を特定します。 ブラウザで直接実行するハンズオンコードでPandas & NumPy Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Pandas & NumPy Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのPandas & NumPy Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。

「timeitとmemory_profilerによるプロファイリング」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このPandas & NumPy Academyレッスンでコードを書いて実行できますか?

はい。すべてのPandas & NumPy Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. timeitとmemory_profilerによるプロファイリング
  2. iterrowsとPythonループの回避
  3. メモリ削減のための効率的なデータ型
  4. 大容量ファイルのチャンク読み込み
← Pandas & NumPy Academyに戻る