Pandas Dataframe groupby описывает в 8 раз медленнее, чем вычисление отдельно

Следующий код суммирует числовые данные, используя два разных подхода.

Первый подход использует Dataframe(). Description () и передает некоторые дополнительные процентили.

Второй подход отдельно вычисляет итоговую статистику (среднее значение, стандартное отклонение, N), суммирует ее, вычисляет те же квантили, затем добавляет их и сортирует по индексу, так что результат по существу такой же, как и в первом подходе.

Есть некоторые незначительные различия в именах, которые мы можем устранить после слов, и поскольку суммарные данные невелики, это очень быстро.

Оказывается, что использование функции description в этом примере было примерно в 8 раз медленнее.

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

import pandas as pd
import numpy as np
from datetime import datetime

def make_data (n):

    ts = datetime.now().timestamp() + abs(np.random.normal(60, 30, n)).cumsum()

    df = pd.DataFrame({
        'c1': np.random.choice(list('ABCDEFGH'), n),
        'c2': np.random.choice(list('ABCDEFGH'), n),
        'c3': np.random.choice(list('ABCDEFGH'), n),
        't1': np.random.randint(1, 20, n),
        't2': pd.to_datetime(ts, unit='s'),
        'x1': np.random.randn(n),
        'x2': np.random.randn(n),
        'x3': np.random.randn(n)
        })

    return df

def summarize_numeric_1 (df, mask, groups, values, quantiles): 

    dfg = df[mask].groupby(groups)[values]

    return dfg.describe(percentiles = quantiles).stack()

def summarize_numeric_2 (df, filt, groups, values, quantiles): 

    dfg = df[mask].groupby(groups)[values]

    dfg_stats = dfg.agg([np.mean, np.std, len]).stack()
    dfg_quantiles = dfg.quantile(all_quantiles)

    return dfg_stats.append(dfg_quantiles).sort_index()

%time df = make_data(1000000)

groups = ['c1', 'c2', 't1']
mask = df['c3'].eq('H') & df['c1'].eq('A')
values = ['x1', 'x3']
base_quantiles = [0, .5, 1] 
extd_quantiles = [0.25, 0.75, 0.9]
all_quantiles = base_quantiles + extd_quantiles

%timeit summarize_numeric_1(df, mask, groups, values, extd_quantiles)
%timeit summarize_numeric_2(df, mask, groups, values, all_quantiles)

Время на моем ПК для этого:

Использование описания: 873 мс ± 8,9 мс на цикл (среднее ± стандартное отклонение из 7 циклов, по 1 циклу каждый)

Используя двухэтапный метод: 105 мс ± 490 мкс на цикл (среднее ± стандартное отклонение из 7 циклов, по 10 циклов в каждом)

Все отзывы приветствуются!

0 ответов

Образованное предположение

Я отправлю это в качестве ответа, возможно, для удаления позже, поскольку это скорее обоснованное предположение, чем фактический ответ. Кроме того, это слишком долго для комментария.

Итак, первое, что я сделал после прочтения вашего ответа, - это повторно запустил ваши тайминги в профилировщике, чтобы поближе взглянуть на проблему. Поскольку время самого вычисления было довольно коротким, генерация данных его немного затмила. Однако в целом время было похоже на то, что вы описали. Кроме того, разница стала еще более
заметной: 1094 мс для первого подхода против 63 мс для второго. Это составляет разницу в 17 раз.

Поскольку меньшее время было довольно маленьким, я решил, что он слишком мал, чтобы доверять ему, и повторно запустил тест с размером сгенерированной выборки данных *10. Он увеличил шаг генерации данных до одной минуты, и цифры стали странными: 1173 мс для первого подхода против 506 мс для второго. Фактор лишь немногим хуже, чем два.

Я начал что-то подозревать. Чтобы подтвердить свое подозрение, я провожу последний тест, еще раз увеличивая размер данных в 10 раз. Результат может вас удивить:
12258 мс для первого подхода против 3646 мс для второго. Таблицы полностью изменились, коэффициент равен ~0,3.

Я предполагаю, что в этом случае вычисление панд на самом деле имеет лучшую оптимизацию / алгоритм. Однако, поскольку это панды, у него довольно много дополнительного багажа - цена, которую платят за удобство и надежность. Это означает, что существует слой "ненужного" (с точки зрения вычислений) багажа, который необходимо носить с собой, независимо от размера набора данных.

Так что, если вы хотите быть быстрее панд даже на наборах данных вашего размера, возьмите их операции и напишите их самостоятельно - самым простым из возможных способов. Это сохранит их оптимизацию и для удобства откажется от оплаченного багажа.

Примечание: это ответ для панд версии 1.0.5 . Для других версий все может быть иначе.

tl; dr

панды describe() всегда будет медленнее, чем ваша версия, потому что под капотом он использует почти ту же логику, а также некоторые другие вещи, такие как проверка правильности размеров данных, упорядочение результатов и проверка NaN и правильных типов данных.


Более длинный ответ

Взглянув на исходный код describeметода, мы можем увидеть несколько вещей:

  • pandas использует ту же логику, что и ваш код, для вычисления статистики. Смотрите эту строку внутри describe()метод для примера того, как он использует ту же логику. Это означает, что панды describeбудет всегда медленнее.
  • pandas считает значения, отличные от NaN, используя s.count(), но ваш код считает все значения. Давайте попробуем изменить ваш код, чтобы использовать тот же метод вместо len():
def summarize_numeric_3(df, filt, groups, values, quantiles): 
    dfg = df[mask].groupby(groups)[values]
    dfg_stats = dfg.agg([np.mean, np.std, pd.Series.count]).stack()
    dfg_quantiles = dfg.quantile(all_quantiles)
    return dfg_stats.append(dfg_quantiles).sort_index()

%timeit -n 10 summarize_numeric_3(df, mask, groups, values, all_quantiles)

# outputs
# 48.9 ms ± 1.11 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

это занимает ~49 мс, против 42 мс для вашей версии на моей машине. Только на эту относительно небольшую модификацию ушло на 7 мс больше!

  • pandas делает гораздо больше, чем ваш код, чтобы гарантировать, что данные имеют правильный тип и форму, и представить их в красивом формате. Я извлек панд describeкод метода в "самодостаточную"* версию, которую вы можете профилировать и использовать здесь (слишком долго, чтобы быть опубликованной в этом ответе). Анализируя это, я вижу, что очень большая часть времени уходит на "установку удобного порядка для строк". Удаление этого порядка улучшило describe время на ~8%, с 530 мс до 489 мс.
Другие вопросы по тегам