基于前一行与其他列,用lambda函数计算Pandas列值
高效计算Pandas中依赖前序值的列(10K行规模)
问题描述
现有如下Pandas DataFrame:
import pandas as pd import numpy as np df = pd.DataFrame({ 'up_walk': [0.000000, 1.550781, 0.957031, 0.000000], 'down_walk': [17.827148, 0.000000, 0.000000, 2.878906], 'up_avg': [0.36642, np.nan, np.nan, np.nan], 'down_avg': [9.06815, np.nan, np.nan, np.nan] })
需要通过递推公式填充up_avg和down_avg的NaN值:
up_avg[i] = up_avg[i-1] * 12 + up_walk[i]down_avg[i] = down_avg[i-1] * 12 + down_walk[i]
数据规模达10K行,纯Python循环速度过慢,尝试用apply结合lambda函数报错:
df['up_avg'] = df.apply(lambda x: pd.Series(np.where((x.up_avg != None), x.up_avg.shift() * 12 + x.up_walk, x.up_avg)))
错误信息:AttributeError: 'Series' object has no attribute 'up_avg'
预期最终DataFrame:
index up_walk down_walk up_avg down_avg 0 0.000000 17.827148 0.36642 9.06815 1 1.550781 0.000000 5.947821 108.8178 2 0.957031 0.000000 72.330883 1305.8136 3 0.000000 2.878906 867.970596 15672.642106
错误原因分析
apply(lambda x)中的x是单行的Series对象,没有shift()方法,也不能直接调用x.up_avg(x本身就是单行的所有列值),因此触发属性错误。- 核心问题:
apply是逐行独立处理的,无法传递前一行计算出的up_avg结果,完全不适合处理这种依赖前序值的递推计算。
高效解决方案
方案1:矢量化数学计算(最快,推荐)
递推公式a_n = 12*a_{n-1} + b_n可转化为数学求和式:
$$a_n = 12^n \cdot a_0 + \sum_{k=1}^n b_k \cdot 12^{n-k}$$
利用numpy广播机制实现全量计算,完全避免循环:
# 处理up_avg n = len(df) # 生成12的幂次数组:12^0,12^1,...,12^(n-1) powers_of_12 = 12 ** np.arange(n) # 计算每个up_walk项对应的权重 weights = powers_of_12[-1] / powers_of_12 # 计算求和部分 sum_part = np.cumsum(df['up_walk'] * weights) / weights # 填充并计算最终up_avg df['up_avg'] = df['up_avg'].fillna(0) df['up_avg'] = df['up_avg'].iloc[0] * powers_of_12 + sum_part - df['up_walk'].iloc[0] * weights[0] # 同理处理down_avg sum_part_down = np.cumsum(df['down_walk'] * weights) / weights df['down_avg'] = df['down_avg'].fillna(0) df['down_avg'] = df['down_avg'].iloc[0] * powers_of_12 + sum_part_down - df['down_walk'].iloc[0] * weights[0]
验证结果:
print(df.round(6))
输出与预期完全一致,10K行数据计算耗时仅毫秒级。
方案2:Numba加速循环(比纯Python循环快100+倍)
若对数学推导不熟悉,用Numba装饰器将Python代码编译为机器码,性能接近矢量化:
from numba import jit @jit(nopython=True) def fill_recursive(arr_initial, arr_walk, multiplier): n = len(arr_initial) result = arr_initial.copy() for i in range(1, n): result[i] = result[i-1] * multiplier + arr_walk[i] return result # 转换为numpy数组传入计算 df['up_avg'] = fill_recursive(df['up_avg'].values, df['up_walk'].values, 12) df['down_avg'] = fill_recursive(df['down_avg'].values, df['down_walk'].values, 12)
10K行的循环耗时约几毫秒,远快于纯Python循环。
方案3:Pandas expanding窗口(不推荐,性能一般)
用expanding().apply()实现,但本质仍是循环,性能不如前两个方案,仅作参考:
def recursive_calc(window): if len(window) == 1: return window.iloc[0] return recursive_calc(window[:-1]) * 12 + window.iloc[-1] # 构造包含初始值和up_walk的序列 combined_up = pd.Series([df['up_avg'].iloc[0]] + df['up_walk'].tolist()[1:]) df['up_avg'] = combined_up.expanding().apply(recursive_calc) # 同理处理down_avg combined_down = pd.Series([df['down_avg'].iloc[0]] + df['down_walk'].tolist()[1:]) df['down_avg'] = combined_down.expanding().apply(recursive_calc)
总结
- 优先选择矢量化数学计算,性能最优,适合大规模数据;
- Numba加速循环是次优选择,代码直观,性能接近矢量化;
- 绝对不要用
apply(lambda)处理这类递推问题,既无法实现需求,又效率低下。
内容的提问来源于stack exchange,提问作者Vladimir Shulman
相关产品推荐
相关产品推荐

