0Pricing
Pandas & NumPy Academy · レッスン

iterrowsとPythonループの回避

行ごとのループをベクトル化された列操作、np.where、pd.cutに置き換え、処理を10~100倍高速化します。

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

行単位の反復処理にかかるコスト

PandasはNumPyを基盤としており、コンパイル済みのCコードを使って配列全体を一度に処理します。for _, row in df.iterrows()で行ごとに反復すると、この仕組みを使えず、純粋なPythonに戻ってしまいます。各行はSeriesオブジェクトとして取り出され、ループはPythonインタープリター内で実行されるため、同等のベクトル化処理と比べておよそ100~1000倍のコストがかかります。100万行のDataFrameでは、ミリ秒ではなく数分かかることもあります。

import pandas as pd
import numpy as np
import timeit

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

# Slow: iterrows loop
def loop_version(df):
    result = []
    for _, row in df.iterrows():
        result.append(row['a'] + row['b'])
    return pd.Series(result)

# Fast: vectorised
t_loop = timeit.timeit(lambda: loop_version(df), number=5)
t_vec = timeit.timeit(lambda: df['a'] + df['b'], number=500)

print(f'Loop (5 runs):       {t_loop:.3f}s total')
print(f'Vectorised (500 runs): {t_vec:.3f}s total')
print(f'Speedup: ~{(t_loop/5)/(t_vec/500):.0f}x')

条件付きループをnp.whereに置き換える

np.where(condition, value_if_true, value_if_false)は、要素ごとのif/elseに相当するベクトル化処理です。行を反復してif文を書く代わりに、条件と両方の結果値を配列として渡します。np.whereは列全体を一度にCで計算するため、大きなDataFrameでは同等のPythonループより50~200倍高速です。

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({'price': np.random.uniform(10, 100, 100000)})

# Slow: iterrows
def label_loop(df):
    labels = []
    for _, row in df.iterrows():
        if row['price'] > 50:
            labels.append('expensive')
        else:
            labels.append('cheap')
    return labels

# Fast: np.where
def label_vectorised(df):
    return np.where(df['price'] > 50, 'expensive', 'cheap')

# Verify equivalence on a small subset
assert list(label_loop(df.head(100))) == list(label_vectorised(df.head(100)))
print('Results match!')
print('Fast version result sample:', label_vectorised(df)[:5])

np.selectで複数の条件を扱う

3つ以上の条件がある場合は、入れ子にしたnp.whereではなくnp.select(condlist, choicelist, default=)を使用します。ブール配列のリストと、それに対応する出力値のリストを渡します。最初に一致した条件によって出力が決まり、どの条件にも一致しない行にはdefaultの値が設定されます。これにより、ループ内の複雑なif/elif/elseの連鎖を、簡潔なベクトル化式に置き換えられます。

import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({'score': np.random.randint(0, 101, 20)})

conditions = [
    df['score'] >= 90,
    df['score'] >= 75,
    df['score'] >= 60
]
choices = ['A', 'B', 'C']

df['grade'] = np.select(conditions, choices, default='F')
print(df.sort_values('score', ascending=False).head(10))

.strによるベクトル化された文字列操作

文字列操作は、遅いループの原因になりやすい処理です。Pandasの.strアクセサーには、Pythonの文字列メソッドに対応するベクトル化された機能が用意されています。.str.lower()、.str.strip()、.str.replace()、.str.contains()などがあり、ほかにも多数存在します。.strメソッドを使うと、行を反復してPythonの文字列メソッドを手動で呼び出すより10~50倍高速です。

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
names = ['Alice Smith', 'BOB JONES', '  carol  ', 'Dave Brown'] * 25000
df = pd.DataFrame({'name': names})

# Slow: loop
def clean_loop(df):
    return [n.strip().title() for n in df['name']]

# Fast: .str accessor
def clean_vectorised(df):
    return df['name'].str.strip().str.title()

t1 = timeit.timeit(lambda: clean_loop(df), number=10)
t2 = timeit.timeit(lambda: clean_vectorised(df), number=50)

print(f'Loop (10 runs): {t1:.3f}s')
print(f'.str (50 runs): {t2:.3f}s')
print(f'Speedup: {(t1/10)/(t2/50):.0f}x')
print('Sample:', clean_vectorised(df).head(4).tolist())

ループの代わりにpd.cutとpd.qcutを使う

pd.cut()とpd.qcut()を使うと、複数のif/elif条件を使ったループとして書かれがちなビニング処理をベクトル化できます。行ループ内で'if value < 18: age_group = child elif value < 65: age_group = adult'のように書いているなら、列全体をCの速度で一度に処理するpd.cut()の呼び出し1つに置き換えてください。

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({'age': np.random.randint(0, 90, 100000)})

# Slow: loop with conditionals
def categorise_loop(df):
    result = []
    for age in df['age']:
        if age < 18:
            result.append('child')
        elif age < 65:
            result.append('adult')
        else:
            result.append('senior')
    return result

# Fast: pd.cut
def categorise_cut(df):
    return pd.cut(df['age'], bins=[0, 18, 65, 100],
                  labels=['child', 'adult', 'senior'],
                  right=False)

assert list(categorise_loop(df.head(5))) == list(categorise_cut(df.head(5)).astype(str))
print('Fast version sample:', categorise_cut(df).head(5).tolist())

apply()が常に答えとは限らない

多くのチュートリアルでは、ループを.apply(func)に置き換えることを推奨しています。しかし、applyも内部ではPythonレベルのループであり、iterrowsよりわずかに速い程度で、真のベクトル化処理よりはるかに遅くなります。ベクトル化された代替手段が存在しない場合(複数列にまたがる複雑なロジックなど)に限ってapplyを使用してください。PandasまたはNumPyの組み込み関数で処理を表現できる場合は、常にapplyよりそちらを優先してください。

import pandas as pd
import numpy as np
import timeit

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

# These all do the same thing — compare performance
t1 = timeit.timeit(lambda: df['x'].apply(lambda v: v**2), number=20)
t2 = timeit.timeit(lambda: df['x'] ** 2, number=200)
t3 = timeit.timeit(lambda: np.square(df['x']), number=200)

print(f'apply(v**2): {t1/20*1000:.2f} ms per run')
print(f'** operator: {t2/200*1000:.2f} ms per run')
print(f'np.square(): {t3/200*1000:.2f} ms per run')

groupbyループをaggとtransformに置き換える

よくあるパターンとして、グループを手動で反復する方法があります。for name, group in df.groupby('cat'): do_something(group)のようなコードです。これは、iterrowsが遅いのと同じ理由で低速です。集計統計を作成する場合はgroupby().agg()に、グループ単位の統計値を元の行位置に展開する場合はgroupby().transform()に置き換えてください。

import pandas as pd
import numpy as np
import timeit

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

# Slow: manual loop to add group mean
def slow_group_mean(df):
    means = {}
    for name, group in df.groupby('cat'):
        means[name] = group['val'].mean()
    return df['cat'].map(means)

# Fast: transform
def fast_group_mean(df):
    return df.groupby('cat')['val'].transform('mean')

t1 = timeit.timeit(lambda: slow_group_mean(df), number=10)
t2 = timeit.timeit(lambda: fast_group_mean(df), number=100)
print(f'Loop:       {t1/10*1000:.1f} ms')
print(f'transform:  {t2/100*1000:.1f} ms')
print(f'Speedup: {(t1/10)/(t2/100):.0f}x')

iterrowsを使ってもよい場合

低速ではあるものの、iterrowsとitertuplesが適している場合もあります。たとえば、行の情報を表示またはログ出力する場合(性能は重要ではありません)、各行をもとにHTTP呼び出しを行うAPIペイロードを作成する場合(ネットワーク遅延が支配的になります)、複雑な条件を持つ特定の行をデバッグする場合です。1,000行未満で、処理を1回だけ実行するのであれば、ループとベクトル化の差はミリ秒程度です。コードを複雑にする価値はありません。

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'order_id': [101, 102, 103],
    'product': ['Widget', 'Gadget', 'Doohickey'],
    'amount': [29.99, 49.99, 9.99]
})

# Acceptable use: building a structured log (small data, one-time)
for _, row in df.iterrows():
    print(f'Order {row["order_id"]}: {row["product"]} — ${row["amount"]:.2f}')

itertuplesはiterrowsより高速

ベクトル化された同等の処理がなく、Pythonで行を反復しなければならない場合は、iterrows()ではなくitertuples()を使用してください。itertuples()は各行をPandas Seriesではなく名前付きタプルとして返すため、Seriesの構築にかかるオーバーヘッドを避けられます。通常、iterrowsより5~10倍高速になります。値には属性記法row.column_nameでアクセスします。列名にスペースが含まれる場合は、有効な属性名ではないため使用しないでください。

import pandas as pd
import numpy as np
import timeit

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

t1 = timeit.timeit(
    lambda: [row.a + row.b for row in df.itertuples()], number=10)
t2 = timeit.timeit(
    lambda: [row['a'] + row['b'] for _, row in df.iterrows()], number=10)

print(f'itertuples: {t1/10*1000:.1f} ms')
print(f'iterrows:   {t2/10*1000:.1f} ms')
print(f'itertuples speedup: {t2/t1:.1f}x')

ベクトル化パターンのチェックリスト

ループを書く前に、次の質問を確認してください。

  • 1列に対する要素単位の処理ですか? → 列の算術式またはNumPy ufuncを使用します。
  • 条件分岐を含みますか? → 2条件ならnp.where、3条件以上ならnp.selectを使用します。
  • 値を範囲に分けますか? → pd.cutまたはpd.qcutを使用します。
  • 文字列操作を行いますか? → .strアクセサーを使用します。
  • グループ内で集計しますか? → groupby().agg()またはgroupby().transform()を使用します。
上記のどれにも当てはまらない場合に限り、apply()またはitertuples()を使用してください。

実際の高速化例: 価格計算

ここでは、よくある業務計算である注文数量に応じた段階的な割引適用について、リファクタリング前後の完全な例を示します。ループ版は読みやすいものの、大きなDataFrameでは許容できないほど低速です。np.selectを使ったベクトル化版なら、同じ結果を10分の1の時間で得られます。

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({
    'price': np.random.uniform(10, 200, 100000),
    'qty': np.random.randint(1, 500, 100000)
})

# BEFORE: loop with conditionals
def slow_discount(df):
    result = []
    for _, row in df.iterrows():
        if row['qty'] >= 100:
            disc = 0.2
        elif row['qty'] >= 50:
            disc = 0.1
        elif row['qty'] >= 10:
            disc = 0.05
        else:
            disc = 0.0
        result.append(row['price'] * (1 - disc))
    return pd.Series(result)

# AFTER: np.select
def fast_discount(df):
    conditions = [df['qty'] >= 100, df['qty'] >= 50, df['qty'] >= 10]
    discounts = [0.20, 0.10, 0.05]
    disc = np.select(conditions, discounts, default=0.0)
    return df['price'] * (1 - disc)

assert slow_discount(df.head(200)).round(4).equals(fast_discount(df.head(200)).reset_index(drop=True).round(4))
print('Both versions agree!')

クイックチェック

このレッスンで学んだベクトル化の理解度を確認しましょう。

レッスンのまとめ

このレッスンでは、iterrowsはベクトル化された処理より100~1000倍遅いこと、np.whereとnp.selectで条件付きループを置き換えられること、.strアクセサーで文字列操作をベクトル化できること、そしてgroupby().transform()で手動のグループ反復を置き換えられることを学びました。反復が必要な場合は、iterrowsよりitertuplesを使うと5~10倍高速になります。次は、効率的なデータ型について学びます。数値をダウンキャストし、Categoricalを使ってメモリ使用量を最大70%削減します。

よくある質問

「iterrowsとPythonループの回避」レッスンは無料ですか?

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

「iterrowsとPythonループの回避」で何を学びますか?

行ごとのループをベクトル化された列操作、np.where、pd.cutに置き換え、処理を10~100倍高速化します。 ブラウザで直接実行するハンズオンコードでPandas & NumPy Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

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

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

「iterrowsとPythonループの回避」レッスンにはどのくらい時間がかかりますか?

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

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

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

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

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