You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Pandas从0.25.1升级至2.1.4后,千万级数据按用户分组按月FFill操作性能骤降的优化方案咨询

优化Pandas 2.x中大规模数据缺失月份填充的性能方案

我完全理解这种升级后性能暴跌的挫败感——1000万行数据从20分钟涨到8小时,确实让人头大。结合你的场景和Pandas版本变化的背景,咱们一步步拆解优化方案,把速度拉回升级前的水平。

先分析原代码的性能瓶颈

你的核心慢点在**分组后用apply(lambda x: x.asfreq('MS', method='ffill'))**这一步。Pandas 2.x对groupby.apply的内部实现做了不少调整(比如更严格的类型校验、分组逻辑优化),但对于大规模分组(比如customer+user_id的组合可能多达几十万甚至上百万),逐组的lambda操作会产生巨大的开销,这是导致时间暴增的主要原因。

另外,原代码中循环用_append合并DataFrame的方式,在Pandas新版本里效率也很低,这部分可以先优化。


优化方案一:批量读取+pd.concat替代循环追加

首先优化数据读取和合并环节,减少不必要的内存开销和循环 overhead:

import pandas as pd
import os

path = "your_data_directory"
file_list = ['Data_202601.csv', 'Data_202602.csv', 'Data_202603.csv']

# 预定义数据类型,避免Pandas自动推断的性能损耗,同时减少内存占用
dtypes = {
    'customer': 'int32',  # 如果数值范围允许,用更小的整数类型
    'user_id': 'int32',
    'user_name': 'category',  # 字符串列重复率高,转成category大幅节省内存
    'status': 'int8',
    'user_type': 'category',
    'login_counter': 'int32',
    'month': 'str'  # 先读为字符串,后续统一转换为datetime
}

# 批量读取并预处理数据
df_list = []
for file in file_list:
    df = pd.read_csv(
        os.path.join(path, file),
        sep=';',
        dtype=dtypes,
        parse_dates=['last_login_timestamp'],
        date_parser=lambda x: pd.to_datetime(x, format='%d.%m.%Y %H:%M', exact=True)
    )
    # 转换month列
    df['month'] = pd.to_datetime(df['month'], format='%Y%m')
    df_list.append(df)

# 用pd.concat一次性合并,比循环_append效率高10倍以上
df_all = pd.concat(df_list, ignore_index=True)

# 排序保持原逻辑
df_all = df_all.sort_values(
    ['customer', 'user_id', 'month'],
    ascending=True
).reset_index(drop=True)

优化方案二:用笛卡尔积+左连接替代groupby.apply

这是解决性能问题的核心——把逐组的lambda操作替换为向量化的合并和填充,彻底避免分组循环的开销:

步骤说明:

  1. 生成所有存在的月度序列;
  2. 生成所有customer+user_id的唯一组合;
  3. 用笛卡尔积生成所有用户+月份的完整索引;
  4. 左连接原数据,再按用户组向前填充缺失值。
# 1. 获取所有涉及的月份,生成完整的月度序列
all_months = pd.date_range(
    start=df_all['month'].min(),
    end=df_all['month'].max(),
    freq='MS'
)

# 2. 获取所有唯一的用户组(customer+user_id),并保留用户的基础静态信息
user_base_info = df_all[['customer', 'user_id', 'user_name', 'status', 'user_type']].drop_duplicates()

# 3. 生成用户组和月份的笛卡尔积(所有可能的组合)
full_combination = user_base_info.assign(key=1).merge(
    pd.DataFrame({'month': all_months, 'key': 1}),
    on='key'
).drop('key', axis=1)

# 4. 左连接原数据,得到包含缺失行的完整数据集
df_full = pd.merge(
    full_combination,
    df_all[['customer', 'user_id', 'month', 'last_login_timestamp', 'login_counter']],
    on=['customer', 'user_id', 'month'],
    how='left'
)

# 5. 按用户组向前填充缺失值(向量化操作,速度极快)
df_filled = df_full.sort_values(['customer', 'user_id', 'month'])\
                   .groupby(['customer', 'user_id'])\
                   .ffill()\
                   .reset_index(drop=True)

这个方法的优势在于:所有核心操作都是Pandas的向量化函数,没有逐组的lambda循环,在1000万行数据的场景下,速度能提升几十倍甚至上百倍。


额外优化:内存压缩

处理大规模数据时,内存占用直接影响运行速度。除了上面代码中已经用到的category类型和小整数类型,还可以:

  • 检查last_login_timestamp是否可以用datetime64[ns]之外的类型(比如datetime64[us],但一般没必要);
  • 如果某些列的缺失值较多,可以用Nullable类型(比如Int32代替int32),但这对性能影响不大;
  • 处理完数据后及时释放中间变量:del df_list, full_combination, df_full; import gc; gc.collect()

验证小范围数据的正确性

在全量数据运行前,建议用你提供的3个月样本数据测试上述代码,确保填充后的结果和原代码一致,再推广到全量数据。

内容的提问来源于stack exchange,提问作者cph_sto

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 10:52:30