スポンサーリンク
Python

pandas の apply の正体:for ループのラッパーだから遅い。その限界と代替手法

Python
この記事は約4分で読めます。

データ分析や ETL の現場で pandas を使っていると、つい多用してしまうのが apply。 「とりあえず apply で書けば動く」ため便利ですが、規模が大きくなると途端に処理が重くなり、システム全体のボトルネックになります。

この記事では、apply がなぜ遅いのか、その正体と限界、そして代替手法や Polars という次の選択肢までを体系的に解説します。

1. apply の正体:for ループのラッパー

pandas の apply は、「行または列に対して Python の関数を適用する仕組み」です。

df["y"] = df["x"].apply(lambda v: v * 2)

このコードは一見 pandas らしく見えますが、内部ではこう動いています。

result = []
for v in df["x"]:
    result.append(v * 2)
df["y"] = result

つまり apply の本質は Python の for ループ + 関数呼び出し。 pandas の高速性を支える NumPy のベクトル演算とは無関係の世界に戻ってしまうのです。

2. apply が遅い理由

2-1. Python 関数呼び出しが行ごとに発生する

10 万行あれば 10 万回関数呼び出し。 これは C レベルで処理する NumPy と比べて圧倒的に遅い。ここが一番のネックになる部分だと思います。

2-2. pandas の行は軽量ではない

pandas の Series や行は Python オブジェクトの集合。 連続メモリではないためキャッシュ効率が悪く、ループ処理に向かない。

2-3. GIL により並列化されない

apply は基本的にシングルスレッド。 マルチコア CPU を活かせない。

2-4. ベクトル化できる処理まで apply に押し込んでしまう

初心者ほど「とりあえず apply」で書いてしまい、本来 1 行で終わる処理が行ループに変換されてしまう。

3. apply の限界

apply は便利ですが、以下のような構造的な限界があります。

  • Python の for ループである以上、根本的に高速化できない
  • 並列化されない
  • pandas の内部構造(Python オブジェクトの集合)と相性が悪い
  • データ量が増えるほど指数的に遅くなる

つまり、apply は「最後の手段」であり、デフォルトで使うものではありません。

4. apply の代替手法(高速化の基本パターン)

4-1. ベクトル化(最優先)

NumPy/pandas の演算を使う。

df["y"] = df["x"] * 2

4-2. map(値変換)

辞書や関数で値を変換する場合に最適。

mapping = {1: "A", 2: "B", 3: "C"}
df["label"] = df["code"].map(mapping)

4-3. np.where(条件分岐)

if 文を apply で書くのは非効率。

df["flag"] = np.where(df["score"] > 80, 1, 0)

4-5. assign + ベクトル化

複数列をまとめて処理する場合に読みやすい。

df = df.assign(
    score2=df["score"] * 2,
    flag=np.where(df["score"] > 80, 1, 0)
)

5. apply が必要なケース(例外)

上記内容だとapplyを使ってはダメな感じですが、必要な場合もあります。

以下のようなケースでは apply が有効です。

  • 行全体を使う複雑なロジック
  • 外部 API 呼び出し
  • pandas/NumPy に代替がない特殊処理

ただし、apply をデフォルトで使うのはアンチパターンです。

6. Polars という選択肢

apply の遅さは pandas の構造的な問題でもあります。

  • Python オブジェクトの集合
  • GIL による並列化の制限
  • NumPy ベースのメモリ構造

これらは根本的に改善が難しいため、近年注目されているのが Polars です。

7. Polars は apply をほぼ使わなくてよい

Polars は Rust で実装された高速データフレームライブラリで、 内部は Apache Arrow のカラムナフォーマット。

特徴は以下の通り。

  • Rust による高速処理
  • 自動並列化
  • 連続メモリでキャッシュ効率が高い
  • lazy(遅延評価)によるクエリ最適化
  • apply をほぼ使わず、式(expression)で書くだけで最適化される

pandas(apply)

df["score2"] = df["score"].apply(lambda x: x * 2)

Polars(apply 不要)

df = df.with_columns(
    (pl.col("score") * 2).alias("score2")
)

8. まとめ:apply は便利だが、デフォルトで使うものではない

  • apply の正体は Python の for ループ
  • そのため遅く、並列化されず、pandas の構造とも相性が悪い
  • ベクトル化・map・where などの代替手法を優先すべき
  • 大規模データや高速化が必要な場合は Polars が有力な選択肢

apply のパフォーマンス問題を抱えている方はぜひ参考にしていただければと思います。

Polarsのパフォーマンスについては、以下の記事も書いてます。

コメント

タイトルとURLをコピーしました