Время от времени полезно сделать шаг назад и посмотреть на новые способы решения старых задач. Недавно, работая над проблемой, я заметил, что в pandas есть функция Grouper
, которую я никогда раньше не вызывал. Я изучил, как ее можно использовать, и оказалось, что она полезна для того типа сводного анализа, который я обычно выполняю.
Оригинал статьи Криса по ссылке
В дополнение к ранним функциям pandas с каждым выпуском продолжает предоставлять новые и улучшенные возможности. Например, обновленная функция agg
- еще один очень полезный и интуитивно понятный инструмент для обобщения данных.
В этой статье рассказывается, как вы можете использовать функции Grouper
и agg
для собственных данных. Попутно я буду включать некоторые советы и приемы, как их использовать наиболее эффективно.
Pandas берет свое начало в финансовой индустрии, поэтому неудивительно, что у него есть надежные средства для обработки данных временных рядов. Просто посмотрите обширную документацию по временным рядам, чтобы почувствовать все возможности.
Рассмотрим пример данных о продажах и некоторые простые операции для получения общих продаж по месяцам, дням, годам и т.д.
import pandas as pd
df = pd.read_excel("https://github.com/chris1610/pbpython/blob/master/data/sample-salesv3.xlsx?raw=True")
df.head()
account number | name | sku | quantity | unit price | ext price | date | |
---|---|---|---|---|---|---|---|
0 | 740150 | Barton LLC | B1-20000 | 39 | 86.69 | 3380.91 | 2014-01-01 07:21:51 |
1 | 714466 | Trantow-Barrows | S2-77896 | -1 | 63.16 | -63.16 | 2014-01-01 10:00:47 |
2 | 218895 | Kulas Inc | B1-69924 | 23 | 90.70 | 2086.10 | 2014-01-01 13:24:58 |
3 | 307599 | Kassulke, Ondricka and Metz | S1-65481 | 41 | 21.05 | 863.05 | 2014-01-01 15:05:22 |
4 | 412290 | Jerde-Hilpert | S2-34077 | 6 | 83.21 | 499.26 | 2014-01-01 23:26:55 |
Обратим внимание на типы данных:
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 1500 entries, 0 to 1499 Data columns (total 7 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 account number 1500 non-null int64 1 name 1500 non-null object 2 sku 1500 non-null object 3 quantity 1500 non-null int64 4 unit price 1500 non-null float64 5 ext price 1500 non-null float64 6 date 1500 non-null object dtypes: float64(2), int64(2), object(3) memory usage: 82.2+ KB
Столбец date
приведем к типу datetime
:
df["date"] = pd.to_datetime(df['date'])
df.dtypes
account number int64 name object sku object quantity int64 unit price float64 ext price float64 date datetime64[ns] dtype: object
Прежде чем я продвинусь дальше, полезно познакомиться с псевдонимами смещения (Offset Aliases
). Эти строки используются для представления различных временных частот, таких как дни, недели и годы.
Например, если вы хотите суммировать все продажи по месяцам, то можете использовать функцию resample
. Особенность использования resample
заключается в том, что она работает только с индексом. В этом наборе данные не индексируются по столбцу date
, поэтому resample
не будет работать без реструктуризации (restructuring).
Используйте set_index
, чтобы сделать столбец date
индексом, а затем выполните resample
:
df.set_index('date').resample('M')["ext price"].sum()
date 2014-01-31 185361.66 2014-02-28 146211.62 2014-03-31 203921.38 2014-04-30 174574.11 2014-05-31 165418.55 2014-06-30 174089.33 2014-07-31 191662.11 2014-08-31 153778.59 2014-09-30 168443.17 2014-10-31 171495.32 2014-11-30 119961.22 2014-12-31 163867.26 Freq: M, Name: ext price, dtype: float64
Это довольно простой способ суммирования данных, но он усложняется, если вы хотите дополнительно провести группировку.
Можно посмотреть ежемесячные результаты для каждого клиента:
df.set_index('date').groupby('name')["ext price"].resample("M").sum()
name date Barton LLC 2014-01-31 6177.57 2014-02-28 12218.03 2014-03-31 3513.53 2014-04-30 11474.20 2014-05-31 10220.17 ... Will LLC 2014-08-31 1439.82 2014-09-30 4345.99 2014-10-31 7085.33 2014-11-30 3210.44 2014-12-31 12561.21 Name: ext price, Length: 240, dtype: float64
Это работает, но выглядит немного неуклюжим...
К счастью, Grouper
упрощает данную процедуру!
Вместо того, чтобы играть с переиндексированием, мы можем использовать обычный синтаксис groupby
, но предоставить немного больше информации о том, как сгруппировать данные в столбце date
:
df.groupby(['name', pd.Grouper(key='date', freq='M')])['ext price'].sum()
name date Barton LLC 2014-01-31 6177.57 2014-02-28 12218.03 2014-03-31 3513.53 2014-04-30 11474.20 2014-05-31 10220.17 ... Will LLC 2014-08-31 1439.82 2014-09-30 4345.99 2014-10-31 7085.33 2014-11-30 3210.44 2014-12-31 12561.21 Name: ext price, Length: 240, dtype: float64
Поскольку groupby
- одна из моих любимых функций, этот подход кажется мне более простым и, скорее всего, останется в моей памяти.
Приятным дополнением является то, что для обобщенния в другом временном интервале, достаточно измените параметр freq
на один из допустимых псевдонимов смещения.
Например, годовая сводка, использующая декабрь в качестве последнего месяца, будет выглядеть так:
df.groupby(['name', pd.Grouper(key='date', freq='A-DEC')])['ext price'].sum()
name date Barton LLC 2014-12-31 109438.50 Cronin, Oberbrunner and Spencer 2014-12-31 89734.55 Frami, Hills and Schmidt 2014-12-31 103569.59 Fritsch, Russel and Anderson 2014-12-31 112214.71 Halvorson, Crona and Champlin 2014-12-31 70004.36 Herman LLC 2014-12-31 82865.00 Jerde-Hilpert 2014-12-31 112591.43 Kassulke, Ondricka and Metz 2014-12-31 86451.07 Keeling LLC 2014-12-31 100934.30 Kiehn-Spinka 2014-12-31 99608.77 Koepp Ltd 2014-12-31 103660.54 Kuhn-Gusikowski 2014-12-31 91094.28 Kulas Inc 2014-12-31 137351.96 Pollich LLC 2014-12-31 87347.18 Purdy-Kunde 2014-12-31 77898.21 Sanford and Sons 2014-12-31 98822.98 Stokes LLC 2014-12-31 91535.92 Trantow-Barrows 2014-12-31 123381.38 White-Trantow 2014-12-31 135841.99 Will LLC 2014-12-31 104437.60 Name: ext price, dtype: float64
Если ваши годовые продажи были не календарными, то данные можно легко изменить, передав параметр freq
.
Призываю вас поиграть с разными смещениями, чтобы понять, как это работает. При суммировании данных временных рядов это невероятно удобно!
Попробуйте реализовать это в Excel
, что, безусловно, возможно (с использованием сводных таблиц и настраиваемой группировки), но я не думаю, что это так же интуитивно понятно, как в pandas.
В pandas 0.20.0 была добавлена новая функция agg
, которая значительно упрощает суммирование данных аналогично groupby.
Чтобы проиллюстрировать ее функциональность, предположим, что нам нужно получить сумму в столбцах ext price
и quantity
(количество), а также среднее значение unit price
(цены за единицу).
Процесс не очень удобный:
df[["ext price", "quantity"]].sum()
ext price 2018784.32 quantity 36463.00 dtype: float64
df["unit price"].mean()
55.00752666666659
Это работает, но немного беспорядочно...
Новый agg
упрощает процесс:
df[["ext price", "quantity", "unit price"]].agg(['sum', 'mean'])
ext price | quantity | unit price | |
---|---|---|---|
sum | 2.018784e+06 | 36463.000000 | 82511.290000 |
mean | 1.345856e+03 | 24.308667 | 55.007527 |
Хорошие результаты, но включение суммы unit price
не очень полезно.
К счастью, мы можем передать словарь в agg
и указать, какие операции применять к каждому столбцу.
df.agg({'ext price': ['sum', 'mean'], 'quantity': ['sum', 'mean'], 'unit price': ['mean']})
ext price | quantity | unit price | |
---|---|---|---|
mean | 1.345856e+03 | 24.308667 | 55.007527 |
sum | 2.018784e+06 | 36463.000000 | NaN |
Я считаю этот подход действительно удобным, когда хочу суммировать несколько столбцов. Раньше я выполнял отдельные вычисления и создавал результирующий DateFrame
по строке за раз - было утомительно.
В качестве дополнительного бонуса вы можете определять свои собственные функции. Например, мне часто нужно агрегировать данные и использовать функцию mode
, которая бы работала с текстом.
Для своих задач я нашел лямбда-функцию, которая использует value_counts
:
get_max = lambda x: x.value_counts(dropna=False).index[0]
Затем, если я хочу включить наиболее часто используемые sku
(артикулы) в сводную таблицу:
df.agg({'ext price': ['sum', 'mean'], 'quantity': ['sum', 'mean'], 'unit price': ['mean'], 'sku': [get_max]})
ext price | quantity | unit price | sku | |
---|---|---|---|---|
<lambda> | NaN | NaN | NaN | S2-77896 |
mean | 1.345856e+03 | 24.308667 | 55.007527 | NaN |
sum | 2.018784e+06 | 36463.000000 | NaN | NaN |
Это довольно круто, но есть одна вещь, которая меня всегда беспокоила в этом подходе: в столбце написано <lambda>
.
В идеале я хочу указать most frequent
(наиболее часто). Раньше я прыгал через несколько обручей, чтобы произвести переименование, но, работая над этой статьей, я наткнулся на другой подход - явное определение имени лямбда-функции:
get_max.__name__ = "most frequent"
Теперь, когда я выполняю агрегирование:
df.agg({'ext price': ['sum', 'mean'], 'quantity': ['sum', 'mean'], 'unit price': ['mean'], 'sku': [get_max]})
ext price | quantity | unit price | sku | |
---|---|---|---|---|
mean | 1.345856e+03 | 24.308667 | 55.007527 | NaN |
most frequent | NaN | NaN | NaN | S2-77896 |
sum | 2.018784e+06 | 36463.000000 | NaN | NaN |
Получили гораздо более приятные названия столбцов! Конечно, это мелочь, но я несомненно рад, что понял ее.
В качестве завершающего финального бонуса вот еще один трюк.
Агрегатная (aggregate) функция, использующая словарь, полезна, но проблема заключается в том, что она не сохраняет порядок.
Если вы хотите убедиться, что ваши столбцы расположены в определенном порядке, вы можете использовать OrderedDict
:
import collections
f = collections.OrderedDict([('ext price', ['sum', 'mean']), ('quantity', ['sum', 'mean']), ('sku', [get_max])])
df.agg(f)
ext price | quantity | sku | |
---|---|---|---|
mean | 1.345856e+03 | 24.308667 | NaN |
most frequent | NaN | NaN | S2-77896 |
sum | 2.018784e+06 | 36463.000000 | NaN |
Библиотека pandas
продолжает расти и развиваться с течением времени. Иногда бывает полезно убедиться, что не появилось более простых решений. Функция Grouper
и обновленная функция agg
действительно полезны при агрегировании и обобщении данных.