加快在熊猫分组中的滚动总和计算

时间:2019-07-04 09:36:52

标签: python pandas performance pandas-groupby rolling-computation

我想针对大量组逐个计算滚动总和,但是我很难快速地完成它。

Pandas具有用于滚动和扩展计算的内置方法

这是一个例子:

import pandas as pd
import numpy as np
obs_per_g = 20
g = 10000
obs = g * obs_per_g
k = 20
df = pd.DataFrame(
    data=np.random.normal(size=obs * k).reshape(obs, k),
    index=pd.MultiIndex.from_product(iterables=[range(g), range(obs_per_g)]),
)

我可以使用要滚动和扩展的总和

df.groupby(level=0).expanding().sum()
df.groupby(level=0).rolling(window=5).sum()

但是对于大量的团体来说,这需要很长时间。要扩展总和,相反,使用pandas方法的cumsum快60倍(上例中为16s vs 280ms),并将小时变成分钟。

df.groupby(level=0).cumsum()

在大熊猫中是否可以快速实现滚动总和,例如cumsum用于扩展总和?如果没有,我可以使用numpy完成此操作吗?

2 个答案:

答案 0 :(得分:1)

我对.rolling()很满意,但只有小型数据集,或者如果您要应用的函数是非标准的,我就拥有相同的经验,对于sum(),我建议使用cumsum()并减去cumsum().shift(5)

df.groupby(level=0).cumsum() - df.groupby(level=0).cumsum().shift(5)

答案 1 :(得分:1)

提供这方面的最新信息,如果升级pandas,groupby 滚动的性能得到了显着提高。与 0.24 或 1.0.0 相比,这在 1.1.0 中快了大约 4-5 倍,在 >1.2.0 中快了 12 倍。

我相信最大的性能改进来自这个 PR,这意味着它可以在 cython 中做更多的事情(在它像 groupby.apply(lambda x: x.rolling()) 一样实现之前)。

我使用以下代码进行基准测试:

import pandas
import numpy

print(pandas.__version__)
print(numpy.__version__)


def stack_overflow_df():
    obs_per_g = 20
    g = 10000
    obs = g * obs_per_g
    k = 2
    df = pandas.DataFrame(
        data=numpy.random.normal(size=obs * k).reshape(obs, k),
        index=pandas.MultiIndex.from_product(iterables=[range(g), range(obs_per_g)]),
    )
    return df


df = stack_overflow_df()

# N.B. droplevel important to make indices match
rolling_result = (
    df.groupby(level=0)[[0, 1]].rolling(10, min_periods=1).sum().droplevel(level=0)
)
df[["value_0_rolling_sum", "value_1_rolling_sum"]] = rolling_result
%%timeit
# results:
# numpy version always 1.19.4
# pandas 0.24 = 12.3 seconds
# pandas 1.0.5 = 12.9 seconds
# pandas 1.1.0 = broken with groupby rolling bug
# pandas 1.1.1 = 2.9 seconds
# pandas 1.1.5 = 2.5 seconds
# pandas 1.2.0 = 1.06 seconds
# pandas 1.2.2 = 1.06 seconds

我认为如果尝试使用 numpy.cumsum 来提高性能(无论 Pandas 版本如何),必须小心。例如,使用以下内容:

# Gives different output
df.groupby(level=0)[[0, 1]].cumsum() - df.groupby(level=0)[[0, 1]].cumsum().shift(10)

虽然这要快得多,但输出不正确。这种转变在所有行上执行,并混合不同组的累积。即下一组的第一个结果被移回上一组。

要获得与上述相同的行为,您需要使用 apply:

df.groupby(level=0)[[0, 1]].cumsum() - df.groupby(level=0)[[0, 1]].apply(
    lambda x: x.cumsum().shift(10).fillna(0)
)

在最新版本 (1.2.2) 中,这比直接使用滚动要慢。 因此,对于 groupby 滚动总和,我不认为 numpy.cumsum 是 pandas>=1.1.1

的最佳解决方案

为了完整起见,如果您的组是列而不是索引,您应该使用如下语法:

# N.B. reset_index important to make indices match
rolling_result = (
    df.groupby(["category_0", "category_1"])[["value_0", "value_1"]]
    .rolling(10, min_periods=1)
    .sum()
    .reset_index(drop=True)
)
df[["value_0_rolling_sum", "value_1_rolling_sum"]] = rolling_result