Ускорение расчета скользящей суммы в pandas groupby

Я хочу вычислять скользящие суммы по группам для большого количества групп, и у меня возникают проблемы с тем, чтобы сделать это достаточно быстро.

В 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 раз быстрее (16 с против 280 мс в приведенном выше примере) и превращает часы в минуты.

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

Есть ли быстрая реализация скользящей суммы в пандах, например cumsum для увеличения сумм? Если нет, могу ли я использовать numpy для этого?


person CloseToC    schedule 04.07.2019    source источник


Ответы (2)


У меня был такой же опыт .rolling(), это приятно, но только с небольшими наборами данных или если функция, которую вы применяете, нестандартная, с sum() я бы предложил использовать cumsum() и вычесть cumsum().shift(5)

df.groupby(level=0).cumsum() - df.groupby(level=0).cumsum().shift(5)
person Mark    schedule 04.07.2019
comment
Я только что проверил, на удивление .rolling() немного быстрее %timeit на 242 мкс, а мой метод - %timeit 371 мкс, мой опыт был другим с моим набором данных, он был примерно в 10 раз быстрее, интересно, почему. - person Mark; 04.07.2019
comment
Хорошее решение, надо было подумать об этом! Cumsum не быстрее, чем expand (). Sum () (или Rolling ()) для одной группы или небольшого количества групп. Но для огромного количества групп это становится существенно быстрее. Должна быть оптимизация cumsum, которая связана с тем, как выполняется groupby - person CloseToC; 04.07.2019
comment
Я не уверен, что этот ответ работает должным образом. Разве df.groupby(level=0).cumsum().shift(5) не перемещается по всем строкам и не смешивает совокупность разных групп? т.е. первый результат следующей группы переносится обратно в предыдущую группу? Я думаю, вам нужно включить смену в заявку. Примерно так: df.groupby(level=0).cumsum() - df.groupby(level=0).apply(lambda x: x.cumsum().shift(10).fillna(0)) Мои тесты показывают, что это примерно в 2 раза быстрее, чем катятся панды. (Довольно медленно по сравнению со временем для приведенного выше ответа, который не дает такого же результата). - person user2175850; 21.02.2021

Чтобы предоставить самую свежую информацию об этом, если вы обновите pandas, производительность групповой прокрутки будет значительно улучшена. Это примерно в 4-5 раз быстрее в 1.1.0 и в 12 раз быстрее в ›1.2.0 по сравнению с 0.24 или 1.0.0.

Я считаю, что наибольшее улучшение производительности связано с этим 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) работает медленнее, чем при использовании прямой прокрутки. Следовательно, для групповых скользящих сумм я не думаю, что numpy.cumsum - лучшее решение для панд ›= 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
person user2175850    schedule 21.02.2021