【问题标题】:Pandas groupby resample poor performancePandas groupby 重新采样性能不佳
【发布时间】:2019-08-19 09:35:35
【问题描述】:

我的问题

我在重采样功能与 groupby 结合使用时遇到问题。我正在做的操作目前对 5000 行的数据样本需要 8 秒以上的时间,这完全不符合我的要求。

样本数据(500 行)

将数据作为字典的粘贴箱:https://pastebin.com/RPNdhXsy


逻辑

我有一个季度间隔日期的数据,我想按列分组,然后每月对组内的日期重新采样。

Input:
     isin  report_date   val
    SE001   2018-12-31     1
    SE001   2018-09-30     2
    SE001   2018-06-31     3
    US001   2018-10-31     4
    US001   2018-07-31     5

Output:
    isin   report_date      val        
    SE001   2018-12-31        1
            2018-11-30      NaN
            2018-10-31      NaN
            2018-09-30        2
            2018-08-31      NaN
            2018-07-31      NaN
            2018-06-30        3
    US001   2018-10-30        4    
            2018-09-31      NaN
            2018-08-31      NaN
            2018-07-31        5

我曾经有过这样的操作:

df.groupby('isin').resample('M', on="report_date").first()[::-1]

由于asfreq() 的性能似乎比在resample 中使用on= 稍好,所以我目前改为执行以下操作。不过还是很慢。 我反转了,因为resample 似乎非可选地对日期进行降序排序。

df.set_index('report_date').groupby('isin').resample('M').asfreq()[::-1]

如上所述,有 5000 行和大约 16 列,这需要 15 秒才能运行,因为我需要在两个单独的数据帧上执行此操作。 使用 pastebin 中的示例数据(500 行),操作需要 0.7 秒,这对我来说太长了,因为我的最终数据将有 800k 行。

编辑:不同操作的时间

当前方式

setindex --- 0.001055002212524414 seconds ---
groupby --- 0.00033092498779296875 seconds ---
resample --- 0.004662036895751953 seconds ---
asfreq --- 0.8990700244903564 seconds ---
[::-1] --- 0.0013098716735839844 seconds ---
= 0.9056s

老办法

groupby --- 0.0005779266357421875 seconds ---
resample --- 0.0044629573822021484 seconds ---
first --- 1.6829369068145752 seconds ---
[::-1] --- 0.001600027084350586 seconds ---
= 1.6894s

由此判断,从pandas.core.resample.DatetimeIndexResamplerGroupby 转换为 df 似乎需要很长时间。现在呢?

EDIT2:使用重新索引

df.set_index('report_date').groupby('isin').apply(lambda x: x.reindex(pd.date_range(x.index.min(), x.index.max(), freq='M'), fill_value=0))[::-1]

这需要 0.28 秒,这是一个巨大的改进。不过还是不太好。


如何加快速度?有没有其他方法可以做同样的事情?

【问题讨论】:

  • 这听起来有点慢。运行 groupby 并在单独的行上重新采样需要多长时间?通过分解它,您可以确定瓶颈是在 groupby 还是 resample 调用中?
  • @tnknepp 查看我的编辑
  • 我认为你是对的,数据框的创建是瓶颈。我唯一的建议是也许不要重新采样到 1M 分辨率。您从季度数据开始,因此具有更高分辨率的所有内容都只有 NaN……尽管您可能有这样做的理由。
  • @tnknepp 我稍后执行操作,用正确的数据填充 NaN。我需要每个月成为它的单独行,因为在完成所有数据转换和计算后它们会进入数据库。
  • 您的示例数据集已过期。

标签: python pandas pandas-groupby


【解决方案1】:

我将 25k 行测试数据集的执行时间从 850 毫秒缩短到 320 毫秒。我将重新索引逻辑包装在一个函数中,以使计时更容易:

def orig_pipeline(df):
    return (df
            .set_index('report_date')
            .groupby('isin')
            .apply(lambda x: x.reindex(pd.date_range(x.index.min(), 
                                                     x.index.max(), 
                                                     freq='M'), 
                                       fill_value=0))
            [::-1])

然后,我创建了新函数来加快日期运算和重新索引:

def create_params(df):
    return (df.groupby('isin')['report_date']
            .agg(['min', 'max']).sort_index().reset_index())

def create_multiindex(df, params):
    all_dates = pd.date_range(start='1999-12-31', end='2020-12-31', freq='M')
    midx = (
        (row.isin, d)
        for row in params.itertuples()
        for d in all_dates[(row.min <= all_dates) & (all_dates <= row.max)])
    return pd.MultiIndex.from_tuples(midx, names=['isin', 'report_date'])

def apply_mulitindex(df, midx):
    return df.set_index(['isin', 'report_date']).reindex(midx)

def new_pipeline(df):
    params = create_params(df)
    midx = create_multiindex(df, params)
    return apply_mulitindex(df, midx)

新旧管道给出相同的结果(可能排序顺序除外):

v1 = orig_pipeline(df).drop(columns='isin').sort_index()
v2 = new_pipeline(df).sort_index().fillna(0)
assert(v1 == v2).all().all()

计时结果:

%%timeit
v1 = orig_pipeline(df_big)
854 ms ± 2.72 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

%%timeit
v2 = new_pipeline(df_big)
322 ms ± 5.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

【讨论】:

  • 感谢您的回答,这是在我的特定数据集上产生最佳性能的回答。赏金已颁发。我将在下面发布我的实验作为另一个答案。您是否知道为什么在 python 中编码重采样逻辑与 pandas 实现相比会显示性能改进?
  • 关于性能改进:pd.date_range() 很慢。实用函数create_multiindex() 调用 date_range 一次,以创建月末日期。然后,它使用布尔索引来提取数据集的日期。
【解决方案2】:

我想说明我所做的实验,试图确定哪种解决方案产生最高性能,它表明 @jsmart 的解决方案是最好的。

我的数据集如下(抱歉截图我无法粘贴漂亮的表格):

我的目标是让每个(orgacom、客户)的指标按工作日重新采样。

解决方案一:groupby / apply asfreq

%%time
sol1 = (
to_process.groupby(['orgacom', 'client'], observed=True, )
          .apply(lambda x: x.asfreq('B', fill_value=np.nan))
)

CPU 时间:用户 4 分 6 秒,系统:2.91 秒,总计:4 分 9 秒 挂墙时间:4 分 9 秒

解决方案 2:groupby / apply reindex(截至@jokab EDIT2)

%%time
sol2 = (
to_process.groupby(['orgacom', 'client'], observed=True, )
    .apply(lambda x: x.reindex(pd.date_range(x.index.min(), x.index.max(), freq='B'), fill_value=np.nan))
)

CPU 时间:用户 4 分 13 秒,系统:2.16 秒,总计:4 分 15 秒 挂墙时间:4 分 15 秒

解决方案 3:重新编码重新采样(截至 @jsmart 答案)

def create_params(df):
    return (df.reset_index().groupby(['orgacom', 'client'], observed=True, )['date']
            .agg(['min', 'max']).sort_index().reset_index())

def create_multiindex(df, params):
    all_dates = pd.date_range(start='2016-12-31', end='2020-12-31', freq='B')
    midx = (
        (row.orgacom, row.client, d)
        for row in params.itertuples()
        for d in all_dates[(row.min <= all_dates) & (all_dates <= row.max)])
    return pd.MultiIndex.from_tuples(midx, names=['orgacom', 'client', 'date'])

def apply_mulitindex(df, midx):
    return df.set_index(['orgacom', 'client', 'date']).reindex(midx)

def new_pipeline(df):
    params = create_params(df)
    midx = create_multiindex(df, params)
    return apply_mulitindex(df, midx)

%%time
sol3 = new_pipeline(to_process.reset_index())

CPU 时间:用户 1 分钟 46 秒,系统:4.93 秒,总计:1 分钟 51 秒 挂墙时间:1 分 51 秒

解决方案 4:groupby / resample asfreq(从@jokab 第一个解决方案开始)

%%time
sol4 = to_process.groupby(['orgacom', 'client']).resample('B').asfreq()

CPU 时间:用户 4 分 22 秒,系统:8.01 秒,总计:4 分 30 秒 挂墙时间:4分30秒

【讨论】:

    【解决方案3】:

    我还注意到在 groupby 上重新采样可能会很慢。就我而言,我使用数据重塑来加快速度,

    df.set_index(['isin', 'report_date'])['val'].unstack(0).resample('M')
    

    【讨论】:

    • 我需要在重新采样和重塑数据后进行更多操作,这将迫使我重写所有以下代码。所以不幸的是,这种方法目前并不是一个真正的选择
    【解决方案4】:

    还有另一种方法可以做到这一点。使用 itertools.groupby() 和列表理解

    import time
    from itertools import groupby
    print(time.time())
    data = (
        ('SE001', '2018-12-31', 1),
        ('SE001', '2018-09-30', 2),
        ('SE001', '2018-06-31', 3),
        ('US001', '2018-10-31', 4),
        ('US001', '2018-07-31', 5),
    )
    
    aggr = [(key, sum([g[2] for g in grp])) for key, grp in groupby(sorted(data), key=lambda x: x[0])]
    print(aggr)
    print(time.time())
    
    
    # 100,000 records
    # 2.5 seconds
    

    【讨论】:

    • 我可能弄错了,但我没有看到这个答案在哪里重新采样了第二列中的日期?
    猜你喜欢
    • 2015-11-07
    • 1970-01-01
    • 2016-12-16
    • 1970-01-01
    • 2018-03-18
    • 1970-01-01
    • 1970-01-01
    • 2016-12-23
    • 1970-01-01
    相关资源
    最近更新 更多