🧭 核心思路:为什么要减少 apply?
apply 的问题在于它绕过了 pandas 的向量化引擎,退化为逐行 Python 循环。替代策略的核心优先级如下:
| 优先级 | 方法 | 适用场景 |
|---|---|---|
| ⭐⭐⭐⭐⭐ | merge / join | 多表关联查询 |
| ⭐⭐⭐⭐⭐ | 向量化运算(+, *, np.where) | 列间计算 |
| ⭐⭐⭐⭐ | map / isin | 单列映射/过滤 |
| ⭐⭐⭐⭐ | query() | 条件筛选 |
| ⭐⭐⭐ | groupby + 聚合函数 | 分组统计 |
| ⭐⭐ | np.vectorize | 复杂逻辑兜底 |
| ⭐ | apply | 最后手段 |
🔍 实战案例与示例代码
案例 1:多表关联后的条件计算(merge 替代 apply)
场景:订单表关联用户表、商品表,计算折后价格。
import pandas as pd
import numpy as np
# 三张源表
orders = pd.DataFrame({
'order_id': [1, 2, 3, 4],
'user_id': [101, 102, 101, 103],
'product_id': [10, 20, 30, 10],
'quantity': [2, 1, 3, 1]
})
users = pd.DataFrame({
'user_id': [101, 102, 103],
'level': ['vip', 'normal', 'vip']
})
products = pd.DataFrame({
'product_id': [10, 20, 30],
'price': [100.0, 200.0, 150.0]
})
# ❌ 慢方法:apply 逐行查找
def calc_price_slow(row):
price = products.loc[products['product_id'] == row['product_id'], 'price'].values[0]
level = users.loc[users['user_id'] == row['user_id'], 'level'].values[0]
discount = 0.8 if level == 'vip' else 1.0
return price * row['quantity'] * discount
orders['total_slow'] = orders.apply(calc_price_slow, axis=1)
# ✅ 快方法:merge 向量化
df = orders \
.merge(users, on='user_id') \
.merge(products, on='product_id')
df['discount'] = np.where(df['level'] == 'vip', 0.8, 1.0)
df['total_fast'] = df['price'] * df['quantity'] * df['discount']
print(df[['order_id', 'total_slow', 'total_fast']])
性能对比(10万行数据):apply ≈ 8s,merge+np.where ≈ 0.05s [1]
案例 2:多表 query() 链式过滤(替代 apply 条件判断)
场景:从销售记录中筛选"华东区 + 金额 > 500 + 产品在白名单"的订单。
sales = pd.DataFrame({
'region': ['华东', '华北', '华东', '华南', '华东'],
'amount': [600, 300, 800, 450, 520],
'product': ['A', 'B', 'A', 'C', 'B'],
'rep_id': [1, 2, 1, 3, 2]
})
whitelist = pd.DataFrame({
'product': ['A', 'B'],
'category': ['电子', '服装']
})
# ❌ 慢方法:apply 判断
def filter_slow(row):
return (row['region'] == '华东' and
row['amount'] > 500 and
row['product'] in ['A', 'B'])
mask_slow = sales.apply(filter_slow, axis=1)
# ✅ 快方法:query + isin + merge
valid_products = whitelist['product'].tolist()
result = (sales
.query("region == '华东' and amount > 500") # 向量化过滤
.loc[sales['product'].isin(valid_products)] # isin 替代 in 判断
.merge(whitelist, on='product') # 关联白名单表
)
print(result)
案例 3:groupby + transform 替代分组内 apply
场景:多表 join 后,计算每个用户的订单金额占该用户总金额的比例。
orders_ext = pd.DataFrame({
'user_id': [101, 101, 102, 103, 103, 103],
'order_id': [1, 2, 3, 4, 5, 6],
'amount': [200, 300, 150, 400, 100, 250]
})
users_info = pd.DataFrame({
'user_id': [101, 102, 103],
'city': ['上海', '北京', '广州']
})
# merge 联表
df = orders_ext.merge(users_info, on='user_id')
# ❌ 慢方法:apply 分组计算
df['ratio_slow'] = df.groupby('user_id')['amount'].apply(
lambda x: x / x.sum()
).reset_index(level=0, drop=True)
# ✅ 快方法:transform 向量化(无需 reset_index)
df['user_total'] = df.groupby('user_id')['amount'].transform('sum')
df['ratio_fast'] = df['amount'] / df['user_total']
print(df[['user_id', 'city', 'amount', 'ratio_slow', 'ratio_fast']])
transform 保持索引对齐,比 apply 快约 3–5 倍,且代码更简洁。[4]
案例 4:map 字典映射替代 apply 单列转换
场景:多表关联后,用字典映射替代逐行 apply 做编码转换。
df_main = pd.DataFrame({
'product_id': [10, 20, 30, 10, 20],
'sales': [5, 3, 8, 2, 6]
})
category_map = {10: '电子', 20: '服装', 30: '食品'}
# ❌ 慢方法
df_main['category_slow'] = df_main['product_id'].apply(lambda x: category_map.get(x, '未知'))
# ✅ 快方法:map 直接接受字典
df_main['category_fast'] = df_main['product_id'].map(category_map).fillna('未知')
print(df_main)
💡 总结:替代 apply 的决策树
需要多表关联?
└─ 是 → merge / join
需要条件筛选?
└─ 是 → query() + isin()
需要列间计算?
└─ 是 → 向量化运算 + np.where
需要分组统计?
└─ 是 → groupby + transform / agg
需要单列映射?
└─ 是 → map(dict)
以上都不满足?
└─ np.vectorize → 实在不行再用 apply
核心原则:让数据操作尽量留在 pandas/NumPy 的 C 层,而不是回到 Python 解释器层。这一原则在大数据量场景下,性能差距会从"秒级"拉开到"分钟级"。