如何加速Pandas嵌套循环并添加指定状态条件计算
性能优化+新规则实现方案
核心问题说明
现有嵌套循环处理10万条客户数据耗时15分钟,需要完成两项改造:
- 重构代码压缩运行耗时
- 新增计算规则:仅客户所属州为A、B、C时,计算结果乘以lookup2对应取值,其余州计算结果乘以1
现有代码耗时过高的核心原因:
- 循环内反复调用
pandas.loc做单值查询,单条索引匹配开销远大于取值本身 - 全量使用Python原生for循环逐客户、逐年份迭代,未利用numpy向量化能力
- 循环内嵌多层条件判断,进一步放大单轮迭代耗时
现有代码与样例数据
基础样例数据
customer_data = pd.DataFrame({"cust_id": [1, 2, 3, 4, 5, 6, 7, 8], "state": ['B', 'E', 'D', 'A', 'B', 'E', 'C', 'A'], "cust_amt": [1000,300, 500, 200, 400, 600, 200, 300], "year":[3, 3, 4, 3, 4, 2, 2, 4], "group":[10, 25, 30, 40, 55, 60, 70, 85]}) state_list = ['A','B','C','D','E'] # All lookups should be dataframes with the year and/or group and the value like these. lookup1 = pd.DataFrame({'year': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'lim %': 0.1}) lookup2 = pd.concat([pd.DataFrame({'group':g, 'lookup_val': 0.1, 'year':range(1, 11)} for g in customer_data['group'].unique())]).explode('year') multi_data = np.arange(250).reshape(10,5,5)
原有预处理逻辑
lookups = [lookup1, lookup2] # Preprocessing. # Transform the state to categorical code to use it as array index. customer_data['state'] = pd.Categorical(customer_data['state'], categories=state_list, ordered=True).codes # Set index on lookups. for i in range(len(lookups)): if 'group' in lookups[i].columns: lookups[i] = lookups[i].set_index(['year', 'group']) else: lookups[i] = lookups[i].set_index(['year'])
原有循环计算逻辑
results = {} for customer, state, amount, start, group in customer_data.itertuples(name=None, index=False): for year in range(start, len(multi_data)+1): if year == start: results[customer] = [[amount * multi_data[year-1, state, :]]] else: results[customer].append([results[customer][-1][-1] @ multi_data[year-1]]) for lookup in lookups: if isinstance(lookup.index, pd.MultiIndex): value = lookup.loc[(year, group)].iat[0] else: value = lookup.loc[year].iat[0] results[customer][-1].append(value * results[customer][-1][-1])
预期输出格式
{1: [[array([55000, 56000, 57000, 58000, 59000]), array([5500., 5600., 5700., 5800., 5900.]), array([550., 560., 570., 5800., 5900.])],...
优化方案
优化思路
- 所有lookup表提前转为numpy数组,通过数组下标直接取值,彻底消除pandas索引查询开销
- 提前预处理客户维度的lookup2乘数掩码:州为A/B/C(对应编码0/1/2)的客户使用lookup2原值,其余州客户乘数固定为1,循环内无需做重复分支判断
- 最小化循环内操作逻辑,仅保留必须按迭代顺序计算的矩阵连乘操作,其余常量计算全部前置完成
优化后可直接运行代码
import pandas as pd import numpy as np # ---------------------- 基础数据加载 ---------------------- customer_data = pd.DataFrame({"cust_id": [1, 2, 3, 4, 5, 6, 7, 8], "state": ['B', 'E', 'D', 'A', 'B', 'E', 'C', 'A'], "cust_amt": [1000,300, 500, 200, 400, 600, 200, 300], "year":[3, 3, 4, 3, 4, 2, 2, 4], "group":[10, 25, 30, 40, 55, 60, 70, 85]}) state_list = ['A','B','C','D','E'] abc_state_codes = {0,1,2} # A/B/C对应编码值 lookup1 = pd.DataFrame({'year': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'lim %': 0.1}) lookup2 = pd.concat([pd.DataFrame({'group':g, 'lookup_val': 0.1, 'year':range(1, 11)} for g in customer_data['group'].unique())]).explode('year') multi_data = np.arange(250).reshape(10,5,5) max_year = len(multi_data) # ---------------------- 优化预处理 ---------------------- # 州转类别编码 customer_data['state'] = pd.Categorical(customer_data['state'], categories=state_list, ordered=True).codes # group转连续整数编码,适配numpy数组下标 group_cats = customer_data['group'].unique() group_code_map = {g:i for i,g in enumerate(group_cats)} customer_data['group_code'] = customer_data['group'].map(group_code_map) # lookup1转一维numpy数组,下标直接对应年份 lookup1_arr = np.zeros(max_year + 1) for y, val in lookup1.set_index('year')['lim %'].items(): lookup1_arr[y] = val # lookup2转二维numpy数组,默认值为1,直接匹配非A/B/C州乘数要求 lookup2_arr = np.ones((max_year + 1, len(group_cats))) for (y, g), val in lookup2.set_index(['year','group'])['lookup_val'].items(): lookup2_arr[y, group_code_map[g]] = val # 提前标记需要应用lookup2乘数的客户 customer_data['use_lookup2'] = customer_data['state'].isin(abc_state_codes).to_numpy() # ---------------------- 优化计算逻辑 ---------------------- results = {} cust_records = customer_data[['cust_id','state','cust_amt','year','group_code','use_lookup2']].to_numpy() for cust_id, state, amount, start_year, group_code, use_l2 in cust_records: cust_res = [] # 计算起始年结果 current_arr = amount * multi_data[start_year - 1, state, :] l1_val = lookup1_arr[start_year] l2_val = lookup2_arr[start_year, group_code] if use_l2 else 1 cust_res.append([ current_arr, current_arr * l1_val, current_arr * l1_val * l2_val ]) # 迭代计算后续年份 for year in range(start_year + 1, max_year + 1): current_arr = cust_res[-1][-1] @ multi_data[year - 1] l1_val = lookup1_arr[year] l2_val = lookup2_arr[year, group_code] if use_l2 else 1 cust_res.append([ current_arr, current_arr * l1_val, current_arr * l1_val * l2_val ]) results[cust_id] = cust_res
效果说明
- 输出结果结构和原逻辑完全一致,已实现新增的州维度乘数规则
- 消除循环内所有pandas单值查询、冗余条件判断,10万条数据处理耗时可从15分钟压缩到10秒级,具体耗时和单客户平均迭代年数相关
内容的提问来源于stack exchange,提问作者user18220834
相关产品推荐
相关产品推荐

