把Pandas从 0.25.1升级到2.1.4之后,按用户分组并按月进行前向填充(ffill)的速度变慢了很多
我手头有以 .csv 格式呈现的月度数据,总行数约为一千万。 我正在从中创建一个所需的数据框,发现创建该数据框的运行时间在将pandas版本从0.25.1升级到2.1.4并重新安装Anaconda之后,从大约20分钟增长到了8 小时。这是否与升级有关还未知,但问题确实是在升级后才出现。
为便于说明,设想我有以下3 个Jan-26、Feb-26、Mar-26的 .csv文件。
Jan-26:
customer;user_id;user_name;status;user_type;last_login_timestamp;login_counter;month
1111;1;A;1;Normal;11.01.2026 11:38;100;202601
1111;2;B;1;Normal;04.01.2026 11:11;50;202601
2222;4;P;1;Normal;04.01.2026 21:16;40;202601
2222;5;Q;1;Admin;12.01.2026 04:54;55;202601
2222;6;R;1;Normal;23.01.2026 21:06;20;202601
3333;9;X;2;Normal;04.01.2026 06:56;70;202601
Feb-26:
customer;user_id;user_name;status;user_type;last_login_timestamp;login_counter;month
1111;1;A;1;Normal;21.02.2026 06:38;120;202602
2222;4;P;1;Normal;13.02.2026 18:11;50;202602
2222;5;Q;1;Admin;19.02.2026 14:51;60;202602
2222;6;R;1;Normal;11.02.2026 16:41;25;202602
Mar-26:
customer;user_id;user_name;status;user_type;last_login_timestamp;login_counter;month
1111;1;A;1;Normal;05.03.2026 03:30;150;202603
1111;2;B;1;Normal;04.03.2026 21:51;60;202603
2222;4;P;1;Normal;12.03.2026 15:16;80;202603
2222;5;Q;1;Admin;24.03.2026 04:54;90;202603
2222;6;R;1;Normal;03.03.2026 21:06;45;202603
3333;9;X;2;Normal;21.03.2026 13:03;95;202603
每一行都对应某个客户的一个特定用户。 例如,客户 1111 有两个用户 1 和 2。如果某个用户在某个月没有登录系统,该月就不会有该用户的记录。 比如,客户 3333 的用户 9 在Feb-26未登录,因此数据中缺少该条记录。
数据中有一列 login_counter,它统计用户迄今为止登录系统的次数。 现在,如果某个用户在某个月没有登录,login_counter 将保持不变。因此,我想为该用户再添加一行,使用前一个月(或最近一个有登录记录的月)相同的 login_counter 和 last_login_timestamp。
首先,将所有数据文件追加到一起:
import pandas as pd
df_all = pd.DataFrame()
for i in ['Data_202601.csv', 'Data_202602.csv', 'Data_202603.csv']:
df = pd.read_csv(os.path.join(path, i), sep=';')
df['last_login_timestamp'] = pd.to_datetime(
df['last_login_timestamp'],
format='%d.%m.%Y %H:%M')
df['month'] = pd.to_datetime(df['month'], format='%Y%m')
df_all = df_all._append(df)
df_all = df_all.sort_values(
['customer', 'user_id', 'month'],
ascending=(True, True, True)
).reset_index(drop=True)
print(df_all)
数据看起来像这样:

下面的代码用上一个有数据的月的数据来补齐缺失的月份:
df_all = (df_all.set_index('month')
.groupby(['customer', 'user_id'])
.apply(lambda x: x.asfreq('MS', method='ffill'))
.drop(['customer', 'user_id'], axis=1)).reset_index()
print(df_all)

现在,这段代码在升级后变得非常慢——从20分钟慢到8 小时
如何让执行速度回到之前的水平?
更新:Brenden在他的回答中写道:
作为额外的好处,我相信这也修复了一个bug:最近一个月的数据没有进行前向填充。现在已经有了前向填充!
为了填充数据,必须同时具备起始月和结束月,否则不会进行填充。 例如,假设我们有一个 customer/user_id(4444/8),它只出现在Jan-26的数据中。最终数据只会在Jan-26出现 (4444/8),因为它既是起始月也是结束月。采用Brenden的方法,这并不是问题,因为他是从前一个月开始填充,且对所有文件(按月排序)都在执行填充,因此不会有遗漏。按照他的方法 customer/user_id(4444/8)将在Feb-26和 Mar-26出现,且last_login_timestamp和login_counter的值与Jan-26相同。
解决方案
我猜测你是在内存不足的情况下运行。以我的笔记本16 GB RAM为例,使用以下参数对一个人工数据集进行测试:
- 1000个客户
- 每个客户1000个用户
- 每个用户每月有80% 的登录概率
- 12个月的数据
总行数与你那约1000万行的数据集相仿。快速处理这些数据会把16 GB RAM全部用光,Windows被迫开始大量分页。这导致整个操作的耗时远远超过用小数据集得到的运行时间所能预期。
在用pandas 0.25.1测试时,并未看到在Pandas 2.1.4上出现的那种大幅减速。它大概以一种更节省内存的方式完成了你的操作。 但内存占用仍然很高。我毫不怀疑如果数据集再大一些,无论哪个版本的Pandas都会变得非常慢。
一次性处理整个数据集会在内存中创建整个数据框的中间副本,这在如此规模下是个问题。若你有多于几个月的数据,另一种做法是一次处理一个月。用第一月的数据来填充第二月的缺口,再用第二月的数据填充第三月的缺口,依此类推。这样每次只处理两个月的数据。下面是一个示例:
import pandas as pd
import os
import time
path = "generated_data"
files = [
'Jan-26.csv', 'Feb-26.csv', 'Mar-26.csv', 'Apr-26.csv',
'May-26.csv', 'Jun-26.csv', 'Jul-26.csv', 'Aug-26.csv',
'Sep-26.csv', 'Oct-26.csv', 'Nov-26.csv', 'Dec-26.csv'
]
start_time = time.perf_counter()
previous_month_df = None
output_frames = []
for file in files: # This assumes you have the files listed in chronological order
loop_start_time = time.perf_counter()
current_month_df = pd.read_csv(os.path.join(path, file), sep=';')
current_month_df['last_login_timestamp'] = pd.to_datetime(
current_month_df['last_login_timestamp'], format='%d.%m.%Y %H:%M'
)
current_month_df['month'] = pd.to_datetime(current_month_df['month'], format='%Y%m')
# The first month doesn't need to be processed any further
if previous_month_df is None:
previous_month_df = current_month_df.set_index(['customer', 'user_id'])
output_frames.append(current_month_df)
continue
current_month_df = current_month_df.set_index(['customer', 'user_id'])
current_month = current_month_df['month'].iloc[0]
combined_index = previous_month_df.index.union(current_month_df.index)
previous_month_df = previous_month_df.reindex(combined_index)
current_month_df = current_month_df.reindex(combined_index)
current_month_df = current_month_df.combine_first(previous_month_df)
current_month_df['month'] = current_month
output_frames.append(current_month_df.reset_index())
previous_month_df = current_month_df
print(f"Completed {file} in {time.perf_counter() - loop_start_time:.2f}s")
df_all = pd.concat(output_frames, ignore_index=True)
df_all = df_all.sort_values(
['customer', 'user_id', 'month'],
ascending=(True, True, True)
).reset_index(drop=True)
print(f"Completed in {time.perf_counter() - start_time:.2f}s")
在我的笔记本上,使用Pandas 2.1.4针对同一个大型人工数据集运行这段脚本,约在45秒内完成。作为额外的好处,我相信这也修复了一个问题:最近一个月的数据没有进行前向填充。现在已经完成前向填充了!