大型DataFrame向量化处理或多进程加速方案咨询
优化Pandas宽表转长表的性能:从循环到向量化/多进程
问题场景
你的DataFrame是宽表结构:每行对应一个玩家,包含Day1_Date、Day1_YearWeek、Day1_Deposit……Day30_Date、Day30_YearWeek、Day30_Deposit共90列(30天×3个字段)。目标是转成长表:每行对应一个玩家的单日数据,包含玩家ID、日期、年周、理论存款额。
示例输入数据
先构造一个简化版的宽表(2个玩家、3天数据),方便演示:
import pandas as pd import numpy as np data = { '玩家ID': ['P001', 'P002'], 'Day1_Date': ['2024-01-01', '2024-01-01'], 'Day1_YearWeek': ['2024W01', '2024W01'], 'Day1_Deposit': [100.0, 150.0], 'Day2_Date': ['2024-01-02', '2024-01-02'], 'Day2_YearWeek': ['2024W01', '2024W01'], 'Day2_Deposit': [200.0, np.nan], 'Day3_Date': ['2024-01-03', '2024-01-03'], 'Day3_YearWeek': ['2024W01', '2024W01'], 'Day3_Deposit': [300.0, 250.0] } df_wide = pd.DataFrame(data)
原循环代码(性能瓶颈)
你现在用的循环逻辑大概是这样——逐行遍历玩家,再逐天拼接数据,这种方式反复调用pd.concat会频繁分配内存,大数据下直接拉胯:
df_long = pd.DataFrame() for idx, row in df_wide.iterrows(): player_id = row['玩家ID'] for day in range(1, 4): # 实际替换为30 temp_df = pd.DataFrame({ '玩家ID': [player_id], '日期': [row[f'Day{day}_Date']], '年周': [row[f'Day{day}_YearWeek']], '理论存款额': [row[f'Day{day}_Deposit']] }) df_long = pd.concat([df_long, temp_df], ignore_index=True)
方案1:向量化转换(最优选择)
用Pandas内置的向量化方法,速度比循环快几个数量级,优先选这个。
方法A:用wide_to_long(最简洁)
wide_to_long专门处理带编号的宽表,完美匹配你的场景:
# 先调整列名格式,让后缀统一为数字(Day1_Date → Date_1) df_renamed = df_wide.rename(columns=lambda x: x.replace('Day', '') if 'Day' in x else x) df_renamed = df_renamed.rename(columns=lambda x: '_'.join(reversed(x.split('_'))) if '_' in x else x) # 一键转长表 df_long = pd.wide_to_long( df_renamed, stubnames=['Date', 'YearWeek', 'Deposit'], # 前缀字段 i='玩家ID', # 分组键 j='Day', # 编号列(可后续删除) sep='_' ).reset_index() # 重命名为期望的列名 df_long = df_long.rename(columns={ 'Date': '日期', 'YearWeek': '年周', 'Deposit': '理论存款额' }).drop(columns='Day')
方法B:用melt分字段转换再合并
如果觉得列名调整麻烦,可以分别对日期、年周、存款做melt,再合并结果:
# 处理日期列 date_melt = df_wide.melt( id_vars='玩家ID', value_vars=[col for col in df_wide.columns if 'Date' in col], var_name='Day', value_name='日期' ) date_melt['Day'] = date_melt['Day'].str.extract('(\d+)').astype(int) # 处理年周列 yw_melt = df_wide.melt( id_vars='玩家ID', value_vars=[col for col in df_wide.columns if 'YearWeek' in col], var_name='Day', value_name='年周' ) yw_melt['Day'] = yw_melt['Day'].str.extract('(\d+)').astype(int) # 处理存款列 deposit_melt = df_wide.melt( id_vars='玩家ID', value_vars=[col for col in df_wide.columns if 'Deposit' in col], var_name='Day', value_name='理论存款额' ) deposit_melt['Day'] = deposit_melt['Day'].str.extract('(\d+)').astype(int) # 合并三个结果 df_long = pd.merge(date_melt, yw_melt, on=['玩家ID', 'Day']) df_long = pd.merge(df_long, deposit_melt, on=['玩家ID', 'Day']).drop(columns='Day')
方案2:多进程加速(超大规模数据用)
如果数据量到千万级以上,用多进程并行处理进一步提速,推荐两种方式:
方法A:用Dask DataFrame
Dask语法和Pandas几乎一致,自动帮你做并行:
import dask.dataframe as dd # 转成Dask DataFrame,分区数设为CPU核心数 ddf_wide = dd.from_pandas(df_wide, npartitions=4) # 定义转换函数(和向量化逻辑相同) def convert_chunk(df): df_renamed = df.rename(columns=lambda x: x.replace('Day', '') if 'Day' in x else x) df_renamed = df_renamed.rename(columns=lambda x: '_'.join(reversed(x.split('_'))) if '_' in x else x) df_long = pd.wide_to_long( df_renamed, stubnames=['Date', 'YearWeek', 'Deposit'], i='玩家ID', j='Day', sep='_' ).reset_index() return df_long.rename(columns={ 'Date': '日期', 'YearWeek': '年周', 'Deposit': '理论存款额' }).drop(columns='Day') # 并行处理并计算结果 ddf_long = ddf_wide.map_partitions(convert_chunk) df_long = ddf_long.compute()
方法B:手动用multiprocessing拆分处理
不想用Dask的话,手动拆分数据块,用多进程跑:
from multiprocessing import Pool, cpu_count import numpy as np # 拆分DataFrame为多个子块 def split_df(df, n_splits): return np.array_split(df, n_splits) # 单个数据块的转换函数 def process_chunk(chunk): df_renamed = chunk.rename(columns=lambda x: x.replace('Day', '') if 'Day' in x else x) df_renamed = df_renamed.rename(columns=lambda x: '_'.join(reversed(x.split('_'))) if '_' in x else x) df_long = pd.wide_to_long( df_renamed, stubnames=['Date', 'YearWeek', 'Deposit'], i='玩家ID', j='Day', sep='_' ).reset_index() return df_long.rename(columns={ 'Date': '日期', 'YearWeek': '年周', 'Deposit': '理论存款额' }).drop(columns='Day') # 多进程执行 if __name__ == '__main__': n_cores = cpu_count() chunks = split_df(df_wide, n_cores) with Pool(n_cores) as pool: results = pool.map(process_chunk, chunks) df_long = pd.concat(results, ignore_index=True)
期望输出
转换后的长表结构如下:
玩家ID 日期 年周 理论存款额 0 P001 2024-01-01 2024W01 100.0 1 P001 2024-01-02 2024W01 200.0 2 P001 2024-01-03 2024W01 300.0 3 P002 2024-01-01 2024W01 150.0 4 P002 2024-01-02 2024W01 NaN 5 P002 2024-01-03 2024W01 250.0
性能参考
- 原循环:10万玩家×30天,耗时60分钟+
- 向量化方法:同数据量,耗时10-30秒
- 多进程方法:100万玩家×30天,耗时1-5分钟(依CPU核心数而定)
内容的提问来源于stack exchange,提问作者bbbb
相关产品推荐
相关产品推荐

