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

如何加速百万行数据的自定义无损groupby聚合?

优化aggregate_nondestructive函数的大数据集运行速度(非Numba方案)

我编写了名为aggregate_nondestructive的自定义聚合函数,可在不删除任何值的前提下,将同一时间点的测量数据合并为尽可能少的数据点。函数代码如下:

import pandas as pd
from pandas import Series, DataFrame

def aggregate_nondestructive(df: Series | DataFrame) -> Series | DataFrame:
    """Aggregate multiple simultaneous measurements in a non-destructive way.
    
    Given a DataFrame of size m×k, 
    this will construct a new DataFrame of size m'×k, where m' = max(df.notna().sum()) is the 
    maximal number of measured not-null values in a column.
    
    Example
    -------

                              Acetate  Base   DOT  Fluo_GFP   Glucose  OD600  Probe_Volume    pH
        measurement_time                                                                        
        2020-12-09 09:48:38      <NA>  <NA>  <NA>      <NA>  4.578233   <NA>          <NA>  <NA>
        2020-12-09 09:48:38      <NA>  <NA>  <NA>      <NA>      <NA>  0.445          <NA>  <NA>
        2020-12-09 09:48:38  0.116585  <NA>  <NA>      <NA>      <NA>   <NA>          <NA>  <NA>
        2020-12-09 09:48:38  0.114842  <NA>  <NA>      <NA>      <NA>   <NA>          <NA>  <NA>
        2020-12-09 09:48:38      <NA>  <NA>  <NA>      <NA>      <NA>  0.485          <NA>  <NA>
        2020-12-09 09:48:38      <NA>  <NA>  <NA>      <NA>      <NA>   <NA>           200  <NA>
        2020-12-09 09:48:38      <NA>  <NA>  <NA>    1112.5      <NA>   <NA>          <NA>  <NA>
        2020-12-09 09:48:38      <NA>  <NA>  <NA>     912.5      <NA>   <NA>          <NA>  <NA>
        2020-12-09 09:48:38      <NA>  <NA>  <NA>      <NA>  4.554859   <NA>          <NA>  <NA>
        
    Returns
    
                              Acetate  Base   DOT  Fluo_GFP   Glucose  OD600  Probe_Volume    pH
        measurement_time                                                                        
        2020-12-09 09:48:38  0.116585  <NA>  <NA>    1112.5  4.578233  0.445           200  <NA>
        2020-12-09 09:48:38  0.114842  <NA>  <NA>     912.5  4.554859  0.485          <NA>  <NA>    
    """
    if isinstance(df, Series):
        return df

    mask = df.notna()
    nitems = mask.sum()
    nrows = nitems.max()
    result = DataFrame(index=df.index[:nrows], columns=df.columns).astype(df.dtypes)
    
    for col in result:
        result[col].iloc[:nitems[col]] = df.loc[mask[col], col]
    return result

现在需要将该函数用于约100万行的表格,通过groupby(["measurement_time"]).apply(aggregate_nondestructive)聚合同时测量的数据,但操作速度极慢。以下是除Numba重写之外的简单提速方法:

1. 抛弃逐组apply,用分组行号映射重构数据

apply在处理大量分组时会产生巨大的调用开销,直接通过构造分组内的行号来重组数据是更高效的方案:

import pandas as pd
import numpy as np

# 重置索引,方便后续处理
df_process = df.reset_index().copy()

# 为每个分组内的非空值分配组内行号
for col in df_process.columns.drop("measurement_time"):
    non_null_mask = df_process[col].notna()
    # 仅对非空行生成序号
    df_process.loc[non_null_mask, "row_idx"] = df_process[non_null_mask].groupby("measurement_time").cumcount()

# 按measurement_time和row_idx分组,取每组内的第一个非空值(自动对齐列)
result = df_process.groupby(["measurement_time", "row_idx"]).first().reset_index(level=1, drop=True)

这个方法完全利用pandas的分组内置逻辑,避免了自定义函数的逐组调用,速度提升显著。

2. 优化自定义函数的向量化逻辑

原函数中的for循环和iloc链式索引是效率瓶颈,修改为直接操作数组的向量化版本:

def aggregate_nondestructive_fast(df: Series | DataFrame) -> Series | DataFrame:
    if isinstance(df, Series):
        return df

    # 一次性提取所有列的非空值数组
    col_non_null = {col: df[col].dropna().values for col in df.columns}
    max_rows = max(len(vals) for vals in col_non_null.values())
    
    # 预分配结果DataFrame
    result = pd.DataFrame(
        index=df.index[:max_rows],
        columns=df.columns,
        dtype=df.dtypes
    )
    
    # 直接赋值数组,避免索引开销
    for col, vals in col_non_null.items():
        result.loc[:len(vals)-1, col] = vals
    
    return result

这里用dropna().values直接获取非空值的numpy数组,赋值时使用loc而非iloc,减少pandas的索引计算开销。

3. 提前过滤全空行,减少分组处理量

如果原数据中存在大量全空的行(即所有列都是NaN),先过滤这些行能大幅减少每个分组的处理规模:

# 过滤掉所有列均为NaN的行
df_clean = df.dropna(how="all")
# 再进行分组聚合
result = df_clean.groupby("measurement_time").apply(aggregate_nondestructive_fast)

4. 使用agg对列单独聚合后合并

利用groupby.agg对每个列单独处理非空值,再将结果组合成目标格式:

def agg_single_col(series):
    # 获取列的非空值,不足最大行数的补NaN
    vals = series.dropna().values
    group_max_len = series.notna().sum()
    return np.pad(vals, (0, group_max_len - len(vals)), constant_values=np.nan)

# 对每个列应用聚合函数,转置后得到结果
agg_result = df.groupby("measurement_time").agg(agg_single_col).T
# 重置索引层级,恢复原格式
result = agg_result.reset_index(level=0, drop=True)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 09:45:42