Pandas编程:避免使用 apply 循环

57 阅读3分钟

很多 Pandas 初学者在处理数据时,第一反应就是用 .apply(),尤其是带 axis=1 的行遍历。但其实,在绝大多数情况下,你应该尽量避免使用 apply 循环 —— 因为它慢、不优雅、且往往有更高效的方法。


🚫 为什么应该避免 apply 循环?

.apply() 本质上是一个 Python 层面的 for 循环(即使内部做了优化),它会逐行或逐列调用你传入的函数。而 Pandas 的核心优势在于:

向量化操作(vectorized operations) —— 利用底层 C/C++ 实现的 NumPy 数组运算,一次处理整个数组,速度极快!

举个例子:

import pandas as pd
import numpy as np

df = pd.DataFrame({
    'A': np.random.randn(100000),
    'B': np.random.randn(100000)
})

❌ 慢:用 apply + lambda 逐行计算

%time df['C'] = df.apply(lambda row: row['A'] + row['B'], axis=1)
# 可能需要几百毫秒甚至几秒

✅ 快:直接向量化运算

%time df['C'] = df['A'] + df['B']
# 通常只需几毫秒!

性能差距可达几十倍甚至上百倍!


✅ 如何避免 apply?替代方案大全

下面根据不同场景,教你如何“消灭” apply:


🧮 场景1:简单数学/逻辑运算 → 直接向量化

❌ 错误写法:

df['total'] = df.apply(lambda x: x['price'] * x['quantity'], axis=1)

✅ 正确写法:

df['total'] = df['price'] * df['quantity']

支持所有基本运算符:+ - * / // % ** > < == & | 等。


📊 场景2:条件赋值 → 使用 np.where, pd.cut, pd.qcut

❌ 错误写法:

def categorize(x):
    if x > 90:
        return 'A'
    elif x > 80:
        return 'B'
    else:
        return 'C'

df['grade'] = df['score'].apply(categorize)

✅ 正确写法:

✅ 方法1:np.where(适合简单条件)

df['grade'] = np.where(df['score'] > 90, 'A',
              np.where(df['score'] > 80, 'B', 'C'))

✅ 方法2:pd.cut(适合区间分箱)

bins = [0, 80, 90, 100]
labels = ['C', 'B', 'A']
df['grade'] = pd.cut(df['score'], bins=bins, labels=labels, right=False)

🔍 场景3:字符串操作 → 使用 .str 访问器

❌ 错误写法:

df['upper_name'] = df['name'].apply(lambda x: x.upper() if isinstance(x, str) else x)

✅ 正确写法:

df['upper_name'] = df['name'].str.upper()

Pandas 提供了强大的 .str 方法:

  • .str.lower(), .str.upper()
  • .str.contains(), .str.startswith()
  • .str.split(), .str.replace()
  • .str.len(), .str.strip()

这些方法都是向量化的!


🗂️ 场景4:复杂逻辑但可拆解 → 分步 + 布尔索引

有时你的 apply 函数包含多个判断和赋值,可以拆成多个布尔条件 + 赋值。

❌ 错误写法:

def assign_group(row):
    if row['age'] < 18:
        return 'child'
    elif row['income'] > 50000:
        return 'high_income_adult'
    else:
        return 'other'

df['group'] = df.apply(assign_group, axis=1)

✅ 正确写法:

df['group'] = 'other'  # 默认值

mask_child = df['age'] < 18
df.loc[mask_child, 'group'] = 'child'

mask_high_income = (df['age'] >= 18) & (df['income'] > 50000)
df.loc[mask_high_income, 'group'] = 'high_income_adult'

这样不仅更快,而且逻辑清晰、易于调试!


🧩 场景5:必须自定义函数?→ 尝试 numbaswifter

如果确实无法避免复杂函数(比如涉及递归、状态机等),可以考虑:

✅ 方案1:用 numba 加速(仅限数值计算)

from numba import jit

@jit(nopython=True)
def my_func(a, b):
    return a * b + a - b

# 注意:需传入 numpy 数组
df['result'] = my_func(df['A'].values, df['B'].values)

✅ 方案2:用 swifter 自动并行化 apply(懒人方案)

pip install swifter
import swifter

df['result'] = df.swifter.apply(lambda row: complex_function(row), axis=1)

⚠️ 注意:这只是让 apply “不那么慢”,不是根本解决方案!


🎯 总结:避免 apply 的黄金法则

场景替代方案
数学/逻辑运算直接向量化 (df['A'] + df['B'])
条件赋值np.where, pd.cut
字符串处理.str 方法
复杂条件赋值布尔索引 + .loc
必须自定义函数numba, swifter(次选)

💡 终极建议

每当你想写 .apply(..., axis=1) 时,请先停下来问自己:

“这个操作能不能用向量化方式完成?”

90% 的情况答案是 YES!

养成这个习惯,你的代码将更快、更简洁、更“Pandasic”。


📚 推荐练习

尝试重构以下代码:

df['discounted_price'] = df.apply(
    lambda row: row['price'] * 0.9 if row['category'] == 'electronics' else row['price'],
    axis=1
)

✅ 向量化版本:

df['discounted_price'] = np.where(
    df['category'] == 'electronics',
    df['price'] * 0.9,
    df['price']
)

希望这份指南帮你彻底告别低效的 apply 循环!🚀