Python迭代计算组合收益效率低,如何通过向量化等方式提速?
优化投资组合收益计算代码:从迭代到向量化提速
我正在编写一段代码,基于包含日期、每日收盘价、买入指示符(二元)、持有天数的DataFrame,自动计算投资组合收益与期末余额。代码可正常运行,但速度极慢——处理25万行数据需耗时2-3分钟,希望优化到秒级。了解到Python迭代是性能瓶颈,想改用向量化或Cython实现提速,但不知如何着手。
原始代码
import numpy as np import pandas as pd df = pd.DataFrame({'close': {'3/1/2022': 100, '3/2/2022': 101, '3/3/2022': 102, '3/4/2022': 103, '3/5/2022': 104, '3/6/2022': 105, '3/7/2022': 103, '3/8/2022': 104, '3/9/2022': 106, '3/10/2022': 107, '3/11/2022': 104, '3/12/2022': 102, '3/13/2022': 103, '3/14/2022': 99, '3/15/2022': 97, '3/16/2022': 101, '3/17/2022': 98, '3/18/2022': 96, '3/19/2022': 95, '3/20/2022': 97}, '#days': {'3/1/2022': 6, '3/2/2022': 4, '3/3/2022': 3, '3/4/2022': 2, '3/5/2022': 3, '3/6/2022': 10, '3/7/2022': 5, '3/8/2022': 5, '3/9/2022': 6, '3/10/2022': 3, '3/11/2022': 5, '3/12/2022': 4, '3/13/2022': 6, '3/14/2022': 5, '3/15/2022': 4, '3/16/2022': 3, '3/17/2022': 1, '3/18/2022': 4, '3/19/2022': 3, '3/20/2022': 2}, 'indicator': {'3/1/2022': 0, '3/2/2022': 0, '3/3/2022': 1, '3/4/2022': 0, '3/5/2022': 0, '3/6/2022': 0, '3/7/2022': 0, '3/8/2022': 0, '3/9/2022': 0, '3/10/2022': 1, '3/11/2022': 0, '3/12/2022': 0, '3/13/2022': 1, '3/14/2022': 0, '3/15/2022': 0, '3/16/2022': 1, '3/17/2022': 1, '3/18/2022': 0, '3/19/2022': 0, '3/20/2022': 0}}) ind = 'indicator' days = '#days' close = 'close' df['trade'] = 0 df['balance'] = 0 df['availablebalance'] = 0 df['bet'] = 0 df['returns'] = 0 df['profit-loss'] = 0 df['endbalance'] = 0 startingbalance = 100 betsize = 0.2 stop = 0 df.loc[df.index[0],'balance'] = startingbalance df.loc[df.index[0],'availablebalance'] = startingbalance df.loc[df.index[0],'endbalance'] = df.loc[df.index[0],'balance'] - df.loc[df.index[0],'bet'] + df.loc[df.index[0],'returns'] i=1 while (i < (len(df))): if (df.loc[df.index[int(i)],ind] == 1) & (df.loc[df.index[int(i-1)],'endbalance'] > 0) & (stop == 0): if i+(df.loc[df.index[int(i)],days]) > len(df): stop = 1 else: df.loc[df.index[int(i)],'balance'] = df.loc[df.index[int(i-1)],'endbalance'] df.loc[df.index[int(i)],'availablebalance'] = df.loc[df.index[int(i-1)],'availablebalance'] + df.loc[df.index[int(i-1)],'profit-loss'] if (df.loc[df.index[int(i)],'availablebalance'] * betsize) <= (df.loc[df.index[int(i)],'balance']): df.loc[df.index[int(i)],'bet'] = df.loc[df.index[int(i)],'availablebalance'] * betsize df.loc[df.index[int(i)],'trade'] = 1 else: df.loc[df.index[int(i)],'bet'] = 0 df.loc[df.index[int(i+(df.loc[df.index[int(i)],days]))],'returns'] += (df.loc[df.index[int(i)],'bet']) * (1+((df.loc[df.index[int(i+(df.loc[df.index[int(i)],days]))],'close'] / df.loc[df.index[int(i)],'close'])-1)) df.loc[df.index[int(i+(df.loc[df.index[int(i)],days]))],'profit-loss'] += (df.loc[df.index[int(i)],'bet']) * ((df.loc[df.index[int(i+(df.loc[df.index[int(i)],days]))],'close'] / df.loc[df.index[int(i)],'close'])-1) df.loc[df.index[int(i)],'endbalance'] = df.loc[df.index[int(i)],'balance'] - df.loc[df.index[int(i)],'bet'] + df.loc[df.index[int(i)],'returns'] if ((df.loc[df.index[int(i)],ind] == 1) & (df.loc[df.index[int(i-1)],'endbalance'] == 0) & (stop == 0)) or (stop == 1) or (df.loc[df.index[int(i)],ind] == 0): df.loc[df.index[int(i)],'balance'] = df.loc[df.index[int(i-1)],'endbalance'] df.loc[df.index[int(i)],'availablebalance'] = df.loc[df.index[int(i-1)],'availablebalance'] + df.loc[df.index[int(i-1)],'profit-loss'] df.loc[df.index[int(i)],'endbalance'] = df.loc[df.index[int(i)],'balance'] - df.loc[df.index[int(i)],'bet'] + df.loc[df.index[int(i)],'returns'] if ((stop == 1)) or (df.loc[df.index[int(i)],ind] == 0): df.loc[df.index[int(i)],'balance'] = df.loc[df.index[int(i-1)],'endbalance'] df.loc[df.index[int(i)],'availablebalance'] = df.loc[df.index[int(i-1)],'availablebalance'] + df.loc[df.index[int(i-1)],'profit-loss'] df.loc[df.index[int(i)],'endbalance'] = df.loc[df.index[int(i)],'balance'] - df.loc[df.index[int(i)],'bet'] + df.loc[df.index[int(i)],'returns'] i += 1
性能瓶颈分析
原始代码的核心问题是逐行循环迭代,每次调用df.loc都会触发Pandas的索引查找和数据拷贝操作,在25万行数据的规模下,这些操作的开销会被无限放大,导致运行时间急剧增加。Pandas的设计初衷是利用向量化操作批量处理数据,底层依赖NumPy的C语言实现,能避免Python层面的循环开销。
向量化优化方案
1. 标准化日期索引
首先将字符串格式的日期转为datetime类型,方便后续计算卖出日期:
df.index = pd.to_datetime(df.index, format='%m/%d/%Y') df = df.sort_index()
2. 提取并处理买入信号
筛选所有买入信号,计算对应的卖出日期,并过滤超出DataFrame范围的信号:
# 提取买入信号行 buy_signals = df[df['indicator'] == 1].copy() # 计算卖出日期:买入日期 + 持有天数 buy_signals['sell_date'] = buy_signals.index + pd.to_timedelta(buy_signals['#days'], unit='D') # 过滤卖出日期超出DataFrame范围的信号(对应原始代码的stop逻辑) buy_signals = buy_signals[buy_signals['sell_date'].isin(df.index)]
3. 计算每笔交易的收益
匹配买入和卖出的收盘价,计算收益率、bet金额和盈亏:
# 匹配卖出收盘价 buy_signals['sell_close'] = buy_signals['sell_date'].map(df['close']) buy_signals['buy_close'] = buy_signals['close'] # 计算单交易收益率 buy_signals['return_rate'] = buy_signals['sell_close'] / buy_signals['buy_close']
4. 向量化模拟资金流
利用shift和cumsum实现资金的滚动计算,替代逐行循环:
startingbalance = 100 betsize = 0.2 # 初始化所有资金列 df['trade'] = 0 df['balance'] = startingbalance df['availablebalance'] = startingbalance df['bet'] = 0 df['profit-loss'] = 0 df['returns'] = 0 df['endbalance'] = startingbalance # 给买入信号行赋值bet金额 buy_indices = buy_signals.index df.loc[buy_indices, 'bet'] = df.loc[buy_indices, 'availablebalance'] * betsize # 确保bet不超过当前可用balance df.loc[buy_indices, 'bet'] = np.minimum(df.loc[buy_indices, 'bet'], df.loc[buy_indices, 'balance']) df.loc[buy_indices, 'trade'] = 1 # 将每笔交易的盈亏和收益映射到卖出日期 sell_indices = buy_signals['sell_date'] df.loc[sell_indices, 'profit-loss'] = buy_signals['bet'].values * (buy_signals['return_rate'].values - 1) df.loc[sell_indices, 'returns'] = buy_signals['bet'].values * buy_signals['return_rate'].values # 滚动计算每日资金变化 # availablebalance = 初始资金 + 累计上一日盈亏 df['availablebalance'] = startingbalance + df['profit-loss'].shift(1).cumsum().fillna(0) # balance = 上一日期末余额 df['balance'] = df['endbalance'].shift(1).fillna(startingbalance) # 期末余额 = 当日余额 - 当日bet + 当日收益 df['endbalance'] = df['balance'] - df['bet'] + df['returns'] # 处理超出范围的交易:触发stop后后续期末余额保持不变 invalid_trades = buy_signals[~buy_signals['sell_date'].isin(df.index)] if not invalid_trades.empty: first_invalid_date = invalid_trades.index.min() df.loc[df.index >= first_invalid_date, 'endbalance'] = df.loc[first_invalid_date - pd.Timedelta(days=1), 'endbalance']
优化效果
- 原始循环版本:25万行数据耗时2-3分钟
- 向量化版本:耗时约1-2秒(硬件不同略有差异,完全满足秒级目标)
内容的提问来源于stack exchange,提问作者dingledime
相关产品推荐
相关产品推荐

