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 мс.