Группировка и агрегация данных
Группировка данных и применение функций к каждой группе — важный этап в анализе данных. После того как данные загружены, объединены и подготовлены, обычно выполняются расчёты статистических показателей для групп или создаются сводные таблицы.
Pandas предоставляет гибкий и эффективный инструмент groupby, который позволяет легко разрезать, нарезать и суммировать данные. Реляционные базы данных и SQL (Structured Query Language) популярны отчасти благодаря их способности удобно объединять, фильтровать, преобразовывать и группировать данные. Однако SQL-запросы могут выполнять ограниченный набор групповых операций. В этой главе вы увидите, что благодаря мощным возможностям выражения Python и pandas мы можем выполнять гораздо более сложные групповые операции, используя любую функцию, которая принимает pandas-объект или массив NumPy. В этой главе вы узнаете:
Примечание: Группировка временных рядов данных (одна из специальных функций groupby) также называется повторной выборкой (resampling), и она будет рассмотрена отдельно в главе 11.
10.1 Механизм GroupBy
Hadley Wickham (автор многих популярных пакетов R) придумал термин «split-apply-combine» для обозначения групповых операций. На первом этапе объекты pandas (Series, DataFrame или другие) разбиваются на несколько групп в соответствии с предоставленными ключами. Разделение выполняется по определённой оси объекта. Например, DataFrame можно разделить по строкам (axis=0) или столбцам (axis=1). Затем функция применяется к каждой группе, создавая новое значение. Наконец, результаты выполнения функции объединяются в конечный результат. Форма результата обычно зависит от выполняемой операции над данными. Рисунок 10-1 показывает простой процесс группировки и агрегирования.
Рисунок 10–1: Иллюстрация простого процесса группировки и агрегирования
Ключи для разделения могут иметь различные формы и типы:
Обратите внимание, что последние три варианта — это просто удобные способы создания списка значений для разделения объектов. Если эти концепции кажутся абстрактными, не беспокойтесь, я приведу множество примеров в этой главе. Давайте сначала рассмотрим простой табличный набор данных в формате DataFrame:
In [10]: df = pd.DataFrame({'key1' : ['a', 'a', 'b', 'b', 'a'],
....: 'key2' : ['one', 'two', 'one', 'two', 'one'],
....: 'data1' : np.random.randn(5),
....: 'data2' : np.random.randn(5)})
In [11]: df
Out[11]:
data1 data2 key1 key2
0 -0.204708 1.393406 a one
1 0.478943 0.092908 a two
2 -0.519439 0.281746 b one
3 -0.555730 0.769023 b two
4 1.965781 1.246435 a one
Предположим, вы хотите сгруппировать данные по key1 и вычислить среднее значение data1. Существует несколько способов сделать это, но мы будем использовать: доступ к data1 и вызов groupby с ключом df['key1']:
In [12]: grouped = df['data1'].groupby(df['key1'])
In [13]: grouped
Out[13]: <pandas.core.groupby.SeriesGroupBy object at 0x7faa31537390>
Переменная grouped является объектом GroupBy. Фактически, он ещё не выполнил никаких вычислений, а просто содержит промежуточную информацию о ключах разделения df['key1']. Другими словами, этот объект уже имеет всё необходимое для выполнения операций над каждой группой. Например, мы можем вызвать метод mean объекта GroupBy для вычисления среднего значения по группам:
In [14]: grouped.mean()
Out[14]:
key1
a 0.746672
b -0.537585
Name: data1, dtype: float64
Позже я подробно объясню процесс вызова .mean(). Здесь важно то, что данные (Series) были сгруппированы по ключу и создали новый Series с индексом, состоящим из уникальных значений key1. Имя индекса «key1» происходит от исходного столбца DataFrame с таким же именем.
Если мы передадим список массивов, мы получим разные результаты:
In [15]: means = df['data1'].groupby([df['key1'], df['key2']]).mean()
In [16]: means
Out[16]:
key1 key2
a one 0.880536
two 0.478943
b one -0.519439
two -0.555730
Name: data1, dtype: float64
Здесь мы сгруппировали данные по двум ключам, получив Series с многоуровневым индексом (состоящим из уникальных пар ключ-значение):
In [17]: means.unstack()
Out[17]:
key2 one two
key1
a 0.880536 0.478943
b -0.519439 -0.555730
В этом примере ключи группировки являются Series. На самом деле, ключи могут быть любыми подходящими массивами:
In [18]: states = np.array(['Ohio', 'California', 'California', 'Ohio', 'Ohio'])
In [19]: years = np.array([2005, 2005, 2006, 2005, 2006])
In [20]: df['data1'].groupby([states, years]).mean()
Out[20]:
California 2005 0.478943
2006 -0.519439
Ohio 2005 -0.380219
2006 1.965781
Name: data1, dtype: float64
Обычно информация о группировке находится в том же DataFrame, который нужно обработать. Здесь вы также можете использовать имена столбцов (строки, числа или другие объекты Python) в качестве ключей группировки:
In [21]: df.groupby('key1').mean()
Out[21]:
data1 data2
key1
a 0.746672 0.910916
b -0.537585 0.525384
In [22]: df.groupby(['key1', 'key2']).mean()
Out[22]:
data1 data2
key1 key2
a one 0.880536 1.319920
two 0.478943 0.092908
b one -0.519439 0.281746
two -0.555730 0.769023
Возможно, вы заметили, что в первом примере при выполнении df.groupby('key1').mean(), результат не содержит столбец key2. Это потому, что df['key2'] не является числовыми данными («мешающий столбец»), поэтому он исключается из результата. По умолчанию все числовые столбцы объединяются, хотя иногда они могут быть отфильтрованы до подмножества, как мы увидим позже.
Независимо от того, что вы планируете делать с groupby, полезно знать метод size, который возвращает Series с количеством элементов в каждой группе:
In [23]: df.groupby(['key1', 'key2']).size()
Out[23]:
key1 key2
a one 2
two 1
b one 1
two 1
dtype: int64
Обратите внимание, что любые пропущенные значения в ключевом столбце будут исключены из результата. Стив 2 3 Уэс 1 2 Джим 2 3 Тревис 2 3
## Группировка с использованием функций
По сравнению с использованием словаря или серии, использование функции Python является более естественным способом определения группового сопоставления. Любая функция, используемая в качестве ключа группировки, будет вызываться один раз для каждого значения индекса, а возвращаемое значение будет использоваться в качестве имени группы.
Например, если взять индекс DataFrame из предыдущего раздела, то в качестве примера можно использовать имена людей. Можно вычислить массив длины строки, но проще передать функцию len:
```python
In [44]: people.groupby(len).sum()
Out[44]:
a b c d e
3 0.591569 -0.993608 0.798764 -0.791374 2.119639
5 0.886429 -2.001637 -0.371843 1.669025 -0.438570
6 -0.713544 -0.831154 -2.370232 -1.860761 -0.860757
Смешивание функций с массивами, списками, словарями и сериями также не является проблемой, поскольку всё внутри преобразуется в массив:
In [45]: key_list = ['one', 'one', 'one', 'two', 'two']
In [46]: people.groupby([len, key_list]).min()
Out[46]:
a b c d e
3 one -0.539741 -1.296221 0.274992 -1.021228 -0.577087
two 0.124121 0.302614 0.523772 0.000940 1.343810
5 one 0.886429 -2.001637 -0.371843 1.669025 -0.438570
6 two -0.713544 -0.831154 -2.370232 -1.860761 -0.860757
Наиболее удобным местом для иерархически индексированных наборов данных является их способность группировать по оси на одном уровне индекса:
In [47]: columns = pd.MultiIndex.from_arrays([['US', 'US', 'US', 'JP', 'JP'],
....: [1, 3, 5, 1, 3]],
....: names=['cty', 'tenor'])
In [48]: hier_df = pd.DataFrame(np.random.randn(4, 5), columns=columns)
In [49]: hier_df
Out[49]:
cty US JP
tenor 1 3 5 1 3
0 0.560145 -1.265934 0.119827 -1.063512 0.332883
1 -2.359419 -0.199543 -1.541996 -0.970736 -1.307030
2 0.286350 0.377984 -0.753887 0.331286 1.349742
3 0.069877 0.246674 -0.011862 1.004812 1.327195
Для группировки по уровню используйте ключевое слово level для передачи номера или имени уровня:
In [50]: hier_df.groupby(level='cty', axis=1).count()
Out[50]:
cty JP US
0 2 3
1 2 3
2 2 3
3 2 3
``` **Перевод текста на русский язык:**
Вы уже видели, что агрегатные операции над столбцами Series или DataFrame фактически представляют собой использование aggregate (с использованием пользовательской функции) или вызов таких методов, как mean и std. Однако вы можете захотеть применить разные агрегатные функции к разным столбцам или использовать несколько функций одновременно. Это тоже легко сделать, и я объясню это на нескольких примерах. Сначала я группирую данные tips по дням и статусу курильщика:
```python
In [60]: grouped = tips.groupby(['day', 'smoker'])
Обратите внимание, что для описательной статистики в таблице 10-1 можно передать имена функций в виде строк:
In [61]: grouped_pct = grouped['tip_pct']
In [62]: grouped_pct.agg('mean')
Out[62]:
day smoker
Fri No 0.151650
Yes 0.174783
Sat No 0.158048
Yes 0.147906
Sun No 0.160113
Yes 0.187250
Thur No 0.160298
Yes 0.163863
Name: tip_pct, dtype: float64
Если вы передадите набор функций или имён функций, то столбцы результирующего DataFrame будут названы соответствующими функциями:
In [63]: grouped_pct.agg(['mean', 'std', peak_to_peak])
Out[63]:
mean std peak_to_peak
day smoker
Fri No 0.151650 0.028123 0.067349
Yes 0.174783 0.051293 0.159925
Sat No 0.158048 0.039767 0.235193
Yes 0.147906 0.061375 0.290095
Sun No 0.160113 0.042347 0.193226
Yes 0.187250 0.154134 0.644685
Thur No 0.160298 0.038774 0.193350
Yes 0.163863 0.039389 0.151240
Здесь мы передали набор агрегатных функций для выполнения агрегации, независимо оценивая данные в каждой группе.
Вам не обязательно принимать имена столбцов, автоматически предоставляемые GroupBy, особенно для лямбда-функций, их имя — '', что делает их идентификацию очень низкой (вы можете увидеть это, посмотрев на атрибут name функции). Поэтому, если вы передаёте список кортежей (имя, функция), то первый элемент каждого кортежа будет использоваться в качестве имени столбца DataFrame (этот список бинарных кортежей можно рассматривать как упорядоченное отображение):
In [64]: grouped_pct.agg([('foo', 'mean'), ('bar', np.std)])
Out[64]:
foo bar
day smoker
Fri No 0.151650 0.028123
Yes 0.174783 0.051293
Sat No 0.158048 0.039767
Yes 0.147906 0.061375
Sun No 0.160113 0.042347
Yes 0.187250 0.154134
Thur No 0.160298 0.038774
Yes 0.163863 0.039389
Для DataFrame у вас есть больше возможностей выбора, вы можете определить набор функций, применяемых ко всем столбцам, или разные функции для разных столбцов. Предположим, мы хотим вычислить три статистических показателя для столбцов tip_pct и total_bill:
In [65]: functions = ['count', 'mean', 'max']
In [66]: result = grouped['tip_pct', 'total_bill'].agg(functions)
In [67]: result
Out[67]:
tip_pct total_bill
count mean max count mean max
day smoker
Fri No 4 0.151650 0.187735 4 18.420000 22.75
Yes 15 0.174783 0.263480 15 16.813333 40.17
Sat No 45 0.158048 0.291990 45 19.661778 48.33
Yes 42 0.147906 0.325733 42 21.276667 50.81
Sun No 57 0.160113 0.252672 57 20.506667 48.17
Yes 19 0.187250 0.710345 19 24.120000 45.35
Thur No 45 0.160298 0.266312 45 17.113111 41.19
Yes 17 0.163863 0.241255 17 19.190588 43.11
Как видите, результирующий DataFrame имеет иерархические столбцы, которые эквивалентны отдельным агрегациям для каждого столбца, а затем использованию concat для объединения результатов вместе с использованием имён столбцов в качестве ключей:
In [68]: result['tip_pct']
Out[68]:
count mean max
day smoker
Fri No 4 0.151650 0.187735
Yes 15 0.174783 0.263480
Sat No 45 0.158048 0.291990
Yes 42 0.147906 0.325733
Sun No 57 0.160113 0.252672
Yes 19 0.187250 0.710345
Thur No 45 0.160298 0.266312
Yes 17 0.163863 0.241255
Аналогично, здесь также можно передать набор кортежей с настраиваемыми именами:
In [69]: ftuples = [('Durchschnitt', 'mean'),('Abweichung', np.var)]
In [70]: grouped['tip_pct', 'total_bill'].agg(ftuples)
Out[70]:
tip_pct total_bill
Durchschnitt Abweichung
``` **Теперь предположим, что вы хотите применить к одному или нескольким столбцам различные функции.**
Конкретный способ заключается в передаче словаря, который отображает имена столбцов на функции, в agg:
```python
In [71]: grouped.agg({'tip' : np.max, 'size' : 'sum'})
Out[71]:
tip size
day smoker
Fri No 3.50 9
Yes 4.73 31
Sat No 9.00 115
Yes 10.00 104
Sun No 6.00 167
Yes 6.50 49
Thur No 6.70 112
Yes 5.00 40
Только когда несколько функций применяются хотя бы к одному столбцу, DataFrame будет иметь иерархические столбцы.
До сих пор все примеры агрегированных данных имели уникальный ключ группировки (возможно, иерархический). Поскольку это не всегда необходимо, вы можете передать as_index=False в groupby, чтобы отключить эту функцию:
In [73]: tips.groupby(['day', 'smoker'], as_index=False).mean()
Out[73]:
day smoker total_bill tip size tip_pct
0 Fri No 18.420000 2.812500 2.250000 0.151650
1 Fri Yes 16.813333 2.714000 2.066667 0.174783
2 Sat No 19.661778 3.102889 2.555556 0.158048
3 Sat Yes 21.276667 2.875476 2.476190 0.147906
4 Sun No 20.506667 3.167895 2.929825 0.160113
5 Sun Yes 24.120000 3.516842 2.578947 0.187250
6 Thur No 17.113111 2.673778 2.488889 0.160298
7 Thur Yes 19.190588 3.030000 2.352941 0.163863
Конечно, вызов reset_index для результата также может дать такой результат. Использование as_index = False позволяет избежать некоторых ненужных вычислений.
Наиболее общий метод GroupBy — это apply, и в оставшейся части этого раздела основное внимание уделяется его объяснению. Как показано на рисунке 10-2, apply разбивает обрабатываемый объект на несколько фрагментов, затем вызывает переданную функцию для каждого фрагмента и, наконец, пытается объединить фрагменты вместе.
Вернувшись к предыдущему примеру с данными о чаевых, предположим, что вы хотите выбрать пять самых высоких значений tip_pct для каждой группы. Сначала напишите функцию, которая выбирает строку с максимальным значением в указанном столбце:
In [74]: def top(df, n=5, column='tip_pct'):
....: return df.sort_values(by=column)[-n:]
In [75]: top(tips, n=6)
Out[75]:
total_bill tip smoker day time size tip_pct
109 14.31 4.00 Yes Sat Dinner 2 0.279525
183 23.17 6.50 Yes Sun Dinner 4 0.280535
232 11.61 3.39 No Sat Dinner 2 0.291990
67 3.07 1.00 Yes Sat Dinner 1 0.325733
178 9.60 4.00 Yes Sun Dinner 2 0.416667
172 7.25 5.15 Yes Sun Dinner 2 0.710345
Теперь, если вы примените эту функцию к группам, сгруппированным по курильщикам, используя apply, вы получите:
In [76]: tips.groupby('smoker').apply(top)
Out[76]:
total_bill tip smoker day time size tip_pct
smoker
No 88 24.71 5.85 No Thur Lunch 2 0.236746
185 20.69 5.00 No Sun Dinner 5 0.241663
51 10.29 2.60 No Sun Dinner 2 0.252672
149 7.51 2.00 No Thur Lunch 2 0.266312
232 11.61 3.39 No Sat Dinner 2 0.291990
Yes 109 14.31 4.00 Yes Sat Dinner 2 0.279525
183 23.17 6.50 Yes Sun Dinner 4 0.280535
67 3.07 1.00 Yes Sat Dinner 1 0.325733
178 9.60 4.00 Yes
``` **Перевод текста на русский язык:**
**Пример: выборка из колоды карт**
deck[:13]
AH 1 2H 2 3H 3 4H 4 5H 5 6H 6 7H 7 8H 8 9H 9 10H 10 JH 10 KH 10 QH 10 dtype: int64
Теперь, согласно тому, что я сказал выше, выберем 5 карт из полной колоды. Код следующий:
```python
In [109]: def draw(deck, n=5):
.....: return deck.sample(n)
In [110]: draw(deck)
AD 1
8C 8
5H 5
KC 10
2C 2
dtype: int64
Предположим, вы хотите выбрать по две карты каждой масти из полной колоды. Поскольку масть — это последний символ в названии карты, мы можем использовать это для группировки и применить:
In [111]: get_suit = lambda card: card[-1] # last letter is suit
In [112]: deck.groupby(get_suit).apply(draw, n=2)
C 2C 2
3C 3
D KD 10
8D 8
H KH 10
3H 3
S 2S 2
4S 4
dtype: int64
Или можно написать так:
In [113]: deck.groupby(get_suit, group_keys=False).apply(draw, n=2)
KC 10
JC 10
AD 1
5D 5
5H 5
6H 6
7S 7
KS 10
dtype: int64
Согласно шаблону «разделить-применить-объединить» метода groupby, можно выполнять операции между столбцами DataFrame или двумя Series (например, групповое взвешенное среднее). Рассмотрим следующий набор данных, который содержит ключ группы, значение и некоторые весовые значения:
In [114]: df = pd.DataFrame({'category': ['a', 'a', 'a', 'a',
.....: 'b', 'b', 'b', 'b'],
.....: 'data': np.random.randn(8),
.....: 'weights': np.random.rand(8)})
In [115]: df
Затем можно использовать категорию для вычисления группового взвешенного среднего:
In [116]: grouped = df.groupby('category')
In [117]: get_wavg = lambda g: np.average(g['data'], weights=g['weights'])
In [118]: grouped.apply(get_wavg)
Другой пример: рассмотрим набор данных с несколькими акциями и индексом Standard & Poor's 500 (символ SPX):
In [119]: close_px = pd.read_csv('examples/stock_px_2.csv', parse_dates=True,
.....: index_col=0)
In [120]: close_px.info()
Далее создадим функцию, которая будет вычислять парный коэффициент корреляции каждого столбца с SPX:
In [122]: spx_corr = lambda x: x.corrwith(x['SPX'])
Наконец, мы используем pct_change для расчёта процентного изменения close_px:
In [123]: rets = close_px.pct_change().dropna()
И затем используем год для группировки процентных изменений, чтобы получить DataFrame с коэффициентами корреляции за каждый год:
In [126]: by_year = rets.groupby(get_year)
In [127]: by_year.apply(spx_corr)
``` **Перевод текста на русский язык:**
0.207893 0.000000
3 Нет 0.000000 0.154661 0.152663 0.000000
Да 0.000000 0.144995 0.152660 0.000000
4 Нет 0.000000 0.150096 0.148143 0.000000
Да 0.117750 0.124515 0.193370 0.000000
5 Нет 0.000000 0.000000 0.206928 0.000000
Да 0.000000 0.106572 0.065660 0.000000
... ... ... ... ...
Обед 1 Нет 0.000000 0.000000 0.000000 0.181728
Да 0.223776 0.000000 0.000000 0.000000
2 Нет 0.000000 0.000000 0.000000 0.166005
Да 0.181969 0.000000 0.000000 0.158843
3 Нет 0.187735 0.000000 0.000000 0.084246
Да 0.000000 0.000000 0.000000 0.204952
4 Нет 0.000000 0.000000 0.000000 0.138919
Да 0.000000 0.000000 0.000000 0.155410
5 Нет 0.000000 0.000000 0.000000 0.121389
6 Нет 0.000000 0.000000 0.000000 0.173706
[21 строка x 4 столбца]
**Параметры pivot_table см. в таблице 10-2.**
## Перекрёстная таблица: crosstab
Перекрёстная таблица (cross-tabulation, сокращённо — crosstab) — это особый вид сводной таблицы, который используется для расчёта частоты групп. Рассмотрим следующий пример:
```python
In [138]: data
Out[138]:
Sample Nationality Handedness
0 1 USA Right-handed
1 2 Japan Left-handed
2 3 USA Right-handed
3 4 Japan Right-handed
4 5 Japan Left-handed
5 6 Japan Right-handed
6 7 USA Right-handed
7 8 USA Left-handed
8 9 Japan Right-handed
9 10 USA Right-handed
В рамках исследовательского анализа мы можем захотеть провести статистический анализ этих данных с учётом национальности и привычки использования рук. Хотя можно использовать pivot_table для реализации этой функции, функция pandas.crosstab будет более удобной:
In [139]: pd.crosstab(data.Nationality, data.Handedness, margins=True)
Out[139]:
Handedness Left-handed Right-handed All
Nationality
Japan 2 3 5
USA 1 4 5
All 3 7 10
Два первых параметра crosstab могут быть массивом или Series, или списком массивов. Например, как в случае с данными о чаевых:
In [140]: pd.crosstab([tips.time, tips.day], tips.smoker, margins=True)
Out[140]:
smoker No Yes All
time day
Dinner Fri 3 9 12
Sat 45 42 87
Sun 57 19 76
Thur 1 0 1
Lunch Fri 1 6 7
Thur 44 17 61
All 151 93 244
Владение инструментами группировки данных в pandas помогает не только в очистке данных, но и в моделировании или статистическом анализе. В главе 14 мы рассмотрим несколько примеров использования groupby на реальных данных.
В следующей главе мы сосредоточимся на данных временных рядов.
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )