Группировка: split-apply-combine
Под «группировкой» мы подразумеваем процесс, включающий один или несколько из следующих шагов:
- Разделение данных на группы на основе определенных критериев
- Применение функции к каждой группе независимо
- Объединение результатов в структуру данных
Из них шаг разделения является наиболее простым. Фактически, во многих ситуациях вы можете разделить набор данных на группы и сами что-то сделать с этими группами. На шаге применения мы можем захотеть сделать следующее:
-
Агрегирование: вычисление сводной статистики (или статистик) по каждой группе. Некоторые примеры:
- Вычисление сумм или средних значений групп
- Вычисление размеров / подсчета групп
-
Преобразование: выполнение некоторых групповых вычислений и возвращение индексированного результата. Некоторые примеры:
- Нормализация данных (z-score) внутри группы
- Заполнение значений NaN внутри групп значением, полученным из каждой группы
-
Фильтрация: отбрасывание некоторых групп на основе группового вычисления, возвращающего True или False. Некоторые примеры:
- Отбрасывание данных, принадлежащих группам с небольшим количеством членов
- Фильтрация данных на основе групповой суммы или среднего значения
- Некоторое сочетание вышеперечисленного: GroupBy изучит результаты шага применения и попытается вернуть осмысленный объединенный результат, если он не соответствует ни одной из двух вышеупомянутых категорий
Поскольку набор методов экземпляров объектов в структурах данных pandas, как правило, богат и выразителен, мы часто просто хотим вызвать, скажем, функцию DataFrame для каждой группы. Название GroupBy должно быть знакомо тем, кто использовал инструмент на основе SQL (или itertools), в котором вы можете писать код, например:
SELECT Column1, Column2, mean(Column3), sum(Column4) FROM SomeTable GROUP BY Column1, Column2
Мы стремимся сделать такие операции естественными и простыми в выражении с помощью pandas. Мы рассмотрим каждую область функциональности GroupBy, а затем предоставим несколько нетривиальных примеров / вариантов использования.
См. справочник для некоторых продвинутых стратегий
Разделение объекта на группы
Объекты pandas могут быть разделены на любые их оси. Абстрактное определение группировки заключается в предоставлении отображения меток на имена групп. Для создания объекта GroupBy (подробнее о том, что такое объект GroupBy, позже) необходимо выполнить следующие действия:
>>> grouped = obj.groupby(key) >>> grouped = obj.groupby(key, axis=1) >>> grouped = obj.groupby([key1, key2])
Отображение может быть задано множеством различных способов:
- Функция Python, которая должна вызываться для каждой метки оси
- Список или массив NumPy той же длины, что и выбранная ось
- Словарь или Series, предоставляющие отображение
label -> group name - Для объектов DataFrame строка, указывающая столбец, используемый для группировки. Конечно,
df.groupby('A')- это всего лишь синтаксический сахар дляdf.groupby(df['A']), но это упрощает жизнь - Список любых из вышеперечисленных элементов
В совокупности мы называем объекты группировки **ключами**. Например, рассмотрим следующую таблицу DataFrame:
In [1]: df = pd.DataFrame({'A' : ['foo', 'bar', 'foo', 'bar',
...: 'foo', 'bar', 'foo', 'foo'],
...: 'B' : ['one', 'one', 'two', 'three',
...: 'two', 'two', 'one', 'three'],
...: 'C' : np.random.randn(8),
...: 'D' : np.random.randn(8)})
...:
In [2]: df
Out[2]:
A B C D
0 foo one 0.469112 -0.861849
1 bar one -0.282863 -2.104569
2 foo two -1.509059 -0.494929
3 bar three -1.135632 1.071804
4 foo two 1.212112 0.721555
5 bar two -0.173215 -0.706771
6 foo one 0.119209 -1.039575
7 foo three -1.044236 0.271860
Мы можем естественным образом сгруппировать по столбцам A или B или по обоим:
In [3]: grouped = df.groupby('A')
In [4]: grouped = df.groupby(['A', 'B'])
Это разделит DataFrame по его индексу (строкам). Мы также можем разделить по столбцам:
In [5]: def get_letter_type(letter): ...: if letter.lower() in 'aeiou': ...: return 'vowel' ...: else: ...: return 'consonant' ...: In [6]: grouped = df.groupby(get_letter_type, axis=1)
Начиная с версии 0.8, объекты Index pandas теперь поддерживают дублирующиеся значения. Если не уникальный индекс используется в качестве ключа группы в операции groupby, все значения для одного значения индекса будут считаться одной группой, и поэтому результат функций агрегирования будет содержать только уникальные значения индекса:
In [7]: lst = [1, 2, 3, 1, 2, 3] In [8]: s = pd.Series([1, 2, 3, 10, 20, 30], lst) In [9]: grouped = s.groupby(level=0) In [10]: grouped.first() Out[10]: 1 1 2 2 3 3 dtype: int64 In [11]: grouped.last() Out[11]: 1 10 2 20 3 30 dtype: int64 In [12]: grouped.sum() Out[12]: 1 11 2 22 3 33 dtype: int64
Обратите внимание, что **разделение не происходит**, пока это не потребуется. Создание объекта GroupBy только проверяет, что вы передали допустимое отображение.
Примечание
Многие виды сложных манипуляций с данными могут быть выражены с помощью операций GroupBy (хотя не гарантируется, что это будет наиболее эффективный способ). Вы можете проявить творчество с функциями отображения меток.
Сортировка GroupBy
По умолчанию ключи групп сортируются во время операции groupby. Однако вы можете передать sort=False для потенциального ускорения:
In [13]: df2 = pd.DataFrame({'X' : ['B', 'B', 'A', 'A'], 'Y' : [1, 2, 3, 4]})
In [14]: df2.groupby(['X']).sum()
Out[14]:
Y
X
A 7
B 3
In [15]: df2.groupby(['X'], sort=False).sum()
Out[15]:
Y
X
B 3
A 7
Обратите внимание, что groupby сохранит порядок, в котором сортируются наблюдения внутри каждой группы. Например, группы, созданные с помощью groupby() ниже, расположены в порядке, в котором они появились в исходном DataFrame.
In [16]: df3 = pd.DataFrame({'X' : ['A', 'B', 'A', 'B'], 'Y' : [1, 4, 3, 2]})
In [17]: df3.groupby(['X']).get_group('A')
Out[17]:
X Y
0 A 1
2 A 3
In [18]: df3.groupby(['X']).get_group('B')
Out[18]:
X Y
1 B 4
3 B 2
Атрибуты объекта GroupBy
Атрибут groups представляет собой словарь, ключами которого являются вычисленные уникальные группы, а соответствующие значениями - метки оси, принадлежащие каждой группе. В приведенном выше примере у нас есть:
In [19]: df.groupby('A').groups
Out[19]: {'bar': [1, 3, 5], 'foo': [0, 2, 4, 6, 7]}
In [20]: df.groupby(get_letter_type, axis=1).groups
Out[20]: {'consonant': ['B', 'C', 'D'], 'vowel': ['A']}
Вызов стандартной Python-функции len для объекта GroupBy просто возвращает длину словаря groups, поэтому он в основном просто удобство:
In [21]: grouped = df.groupby(['A', 'B'])
In [22]: grouped.groups
Out[22]:
{('bar', 'one'): [1],
('bar', 'three'): [3],
('bar', 'two'): [5],
('foo', 'one'): [0, 6],
('foo', 'three'): [7],
('foo', 'two'): [2, 4]}
In [23]: len(grouped)
Out[23]: 6
GroupBy позволит автоматически завершать ввод имен столбцов (и других атрибутов)
In [24]: df
Out[24]:
gender height weight
2000-01-01 male 42.849980 157.500553
2000-01-02 male 49.607315 177.340407
2000-01-03 male 56.293531 171.524640
2000-01-04 female 48.421077 144.251986
2000-01-05 male 46.556882 152.526206
2000-01-06 female 68.448851 168.272968
2000-01-07 male 70.757698 136.431469
2000-01-08 female 58.909500 176.499753
2000-01-09 female 76.435631 174.094104
2000-01-10 male 45.306120 177.540920
In [25]: gb = df.groupby('gender')
In [26]: gb.<TAB> gb.agg gb.boxplot gb.cummin gb.describe gb.filter gb.get_group gb.height gb.last gb.median gb.ngroups gb.plot gb.rank gb.std gb.transform gb.aggregate gb.count gb.cumprod gb.dtype gb.first gb.groups gb.hist gb.max gb.min gb.nth gb.prod gb.resample gb.sum gb.var gb.apply gb.cummax gb.cumsum gb.fillna gb.gender gb.head gb.indices gb.mean gb.name gb.ohlc gb.quantile gb.size gb.tail gb.weight
GroupBy с MultiIndex
С многоуровневыми индексированными данными очень естественно сгруппировать данные по одному из уровней иерархии.
Давайте создадим Series с двухуровневым MultiIndex.
In [27]: arrays = [['bar', 'bar', 'baz', 'baz', 'foo', 'foo', 'qux', 'qux'],
....: ['one', 'two', 'one', 'two', 'one', 'two', 'one', 'two']]
....:
In [28]: index = pd.MultiIndex.from_arrays(arrays, names=['first', 'second'])
In [29]: s = pd.Series(np.random.randn(8), index=index)
In [30]: s
Out[30]:
first second
bar one -0.575247
two 0.254161
baz one -1.143704
two 0.215897
foo one 1.193555
two -0.077118
qux one -0.408530
two -0.862495
dtype: float64
Затем мы можем сгруппировать по одному из уровней в s.
In [31]: grouped = s.groupby(level=0) In [32]: grouped.sum() Out[32]: first bar -0.321085 baz -0.927807 foo 1.116437 qux -1.271025 dtype: float64
Если для MultiIndex указаны имена, их можно передать вместо номера уровня:
In [33]: s.groupby(level='second').sum() Out[33]: second one -0.933926 two -0.469555 dtype: float64
Функции агрегирования, такие как sum, будут принимать параметр уровня напрямую. Кроме того, имя результирующего индекса будет определено по выбранному уровню:
In [34]: s.sum(level='second') Out[34]: second one -0.933926 two -0.469555 dtype: float64
Также начиная с версии 0.6, поддерживается группировка по нескольким уровням.
In [35]: s
Out[35]:
first second third
bar doo one 1.346061
two 1.511763
baz bee one 1.627081
two -0.990582
foo bop one -0.441652
two 1.211526
qux bop one 0.268520
two 0.024580
dtype: float64
In [36]: s.groupby(level=['first', 'second']).sum()
Out[36]:
first second
bar doo 2.857824
baz bee 0.636499
foo bop 0.769873
qux bop 0.293100
dtype: float64
Подробнее о функции sum и агрегировании позже.
Выбор столбца DataFrame в GroupBy
После создания объекта GroupBy из DataFrame, например, вы можете сделать что-то другое для каждого из столбцов. Таким образом, используя [] аналогично получению столбца из DataFrame, вы можете сделать:
In [37]: grouped = df.groupby(['A']) In [38]: grouped_C = grouped['C'] In [39]: grouped_D = grouped['D']
Это в основном синтаксический сахар для альтернативы и гораздо более подробного:
In [40]: df['C'].groupby(df['A']) Out[40]: <pandas.core.groupby.SeriesGroupBy object at 0x129fce310>
Кроме того, этот метод позволяет избежать повторного вычисления внутренней информации о группировке, полученной из переданного ключа.
Итерация по группам
С объектом GroupBy в руках итерация по сгруппированным данным очень естественна и работает аналогично itertools.groupby.
In [41]: grouped = df.groupby('A')
In [42]: for name, group in grouped:
....: print(name)
....: print(group)
....:
bar
A B C D
1 bar one -0.042379 -0.089329
3 bar three -0.009920 -0.945867
5 bar two 0.495767 1.956030
foo
A B C D
0 foo one -0.919854 -1.131345
2 foo two 1.247642 0.337863
4 foo two 0.290213 -0.932132
6 foo one 0.362949 0.017587
7 foo three 1.548106 -0.016692
В случае группировки по нескольким ключам имя группы будет кортежем:
In [43]: for name, group in df.groupby(['A', 'B']):
....: print(name)
....: print(group)
....:
('bar', 'one')
A B C D
1 bar one -0.042379 -0.089329
('bar', 'three')
A B C D
3 bar three -0.00992 -0.945867
('bar', 'two')
A B C D
5 bar two 0.495767 1.95603
('foo', 'one')
A B C D
0 foo one -0.919854 -1.131345
6 foo one 0.362949 0.017587
('foo', 'three')
A B C D
7 foo three 1.548106 -0.016692
('foo', 'two')
A B C D
2 foo two 1.247642 0.337863
4 foo two 0.290213 -0.932132
Это стандартный Python-прием, но помните, что вы можете распаковать кортеж в операторе цикла for, если хотите: for (k1, k2), group in grouped:.
Выбор группы
Одну группу можно выбрать с помощью GroupBy.get_group().
In [44]: grouped.get_group('bar')
Out[44]:
A B C D
1 bar one -0.042379 -0.089329
3 bar three -0.009920 -0.945867
5 bar two 0.495767 1.956030
Или для объекта, сгруппированного по нескольким столбцам:
In [45]: df.groupby(['A', 'B']).get_group(('bar', 'one'))
Out[45]:
A B C D
1 bar one -0.042379 -0.089329
Агрегирование
После создания объекта GroupBy доступны несколько методов для выполнения вычислений на сгруппированных данных.
Очевидный способ - агрегирование с помощью метода aggregate или, что эквивалентно, метода agg.
In [46]: grouped = df.groupby('A')
In [47]: grouped.aggregate(np.sum)
Out[47]:
C D
A
bar 0.443469 0.920834
foo 2.529056 -1.724719
In [48]: grouped = df.groupby(['A', 'B'])
In [49]: grouped.aggregate(np.sum)
Out[49]:
C D
A B
bar one -0.042379 -0.089329
three -0.009920 -0.945867
two 0.495767 1.956030
foo one -0.556905 -1.113758
three 1.548106 -0.016692
two 1.537855 -0.594269
Как вы можете видеть, результат агрегирования будет иметь имена групп в качестве нового индекса по оси группировки. В случае нескольких ключей результат по умолчанию представляет собой MultiIndex, хотя это можно изменить с помощью опции as_index:
In [50]: grouped = df.groupby(['A', 'B'], as_index=False)
In [51]: grouped.aggregate(np.sum)
Out[51]:
A B C D
0 bar one -0.042379 -0.089329
1 bar three -0.009920 -0.945867
2 bar two 0.495767 1.956030
3 foo one -0.556905 -1.113758
4 foo three 1.548106 -0.016692
5 foo two 1.537855 -0.594269
In [52]: df.groupby('A', as_index=False).sum()
Out[52]:
A C D
0 bar 0.443469 0.920834
1 foo 2.529056 -1.724719
Обратите внимание, что вы можете использовать функцию DataFrame reset_index для достижения того же результата, поскольку имена столбцов хранятся в результирующем MultiIndex:
In [53]: df.groupby(['A', 'B']).sum().reset_index()
Out[53]:
A B C D
0 bar one -0.042379 -0.089329
1 bar three -0.009920 -0.945867
2 bar two 0.495767 1.956030
3 foo one -0.556905 -1.113758
4 foo three 1.548106 -0.016692
5 foo two 1.537855 -0.594269
Еще один простой пример агрегирования - вычисление размера каждой группы. Это включено в GroupBy в качестве метода size. Он возвращает Series, индекс которого - имена групп, а значения - размеры каждой группы.
In [54]: grouped.size()
Out[54]:
A B
bar one 1
three 1
two 1
foo one 2
three 1
two 2
dtype: int64
In [55]: grouped.describe()
Out[55]:
C D
0 count 1.000000 1.000000
mean -0.042379 -0.089329
std NaN NaN
min -0.042379 -0.089329
25% -0.042379 -0.089329
50% -0.042379 -0.089329
75% -0.042379 -0.089329
... ... ...
5 mean 0.768928 -0.297134
std 0.677005 0.898022
min 0.290213 -0.932132
25% 0.529570 -0.614633
50% 0.768928 -0.297134
75% 1.008285 0.020364
max 1.247642 0.337863
[48 rows x 2 columns]
Примечание
Функции агрегирования **не будут** возвращать группы, по которым вы агрегируете, если они являются именованными столбцами, когда as_index=True, по умолчанию. Сгруппированные столбцы будут **индексами** возвращаемого объекта.
Передача as_index=False **будет** возвращать группы, по которым вы агрегируете, если они являются именованными столбцами.
Функции агрегирования - это функции, которые уменьшают размер возвращаемых объектов, например: mean, sum, size, count, std, var, sem, describe, first, last, nth, min, max. Это происходит, когда вы делаете, например, DataFrame.sum() и получаете Series.
nth может выступать в качестве редуктора или фильтра, см. здесь
Применение нескольких функций одновременно
Для сгруппированных Series вы также можете передать список или словарь функций для агрегирования, выдав DataFrame:
In [56]: grouped = df.groupby('A')
In [57]: grouped['C'].agg([np.sum, np.mean, np.std])
Out[57]:
sum mean std
A
bar 0.443469 0.147823 0.301765
foo 2.529056 0.505811 0.966450
Если передается словарь, ключи будут использоваться для именования столбцов. В противном случае используется имя функции (сохраненное в объекте функции).
In [58]: grouped['D'].agg({'result1' : np.sum,
....: 'result2' : np.mean})
....:
Out[58]:
result2 result1
A
bar 0.306945 0.920834
foo -0.344944 -1.724719
Для сгруппированного DataFrame вы можете передать список функций для применения к каждому столбцу, что приводит к агрегированному результату с многоуровневым индексом:
In [59]: grouped.agg([np.sum, np.mean, np.std])
Out[59]:
C D
sum mean std sum mean std
A
bar 0.443469 0.147823 0.301765 0.920834 0.306945 1.490982
foo 2.529056 0.505811 0.966450 -1.724719 -0.344944 0.645875
Передача словаря функций по умолчанию ведет себя иначе, см. следующий раздел.
Применение разных функций к столбцам DataFrame
Передав словарь в aggregate, вы можете применить различное агрегирование к столбцам DataFrame:
In [60]: grouped.agg({'C' : np.sum,
....: 'D' : lambda x: np.std(x, ddof=1)})
....:
Out[60]:
C D
A
bar 0.443469 1.490982
foo 2.529056 0.645875
Имена функций также могут быть строками. Для того, чтобы строка была допустимой, она должна быть реализована в GroupBy или доступна через диспетчеризацию:
In [61]: grouped.agg({'C' : 'sum', 'D' : 'std'})
Out[61]:
C D
A
bar 0.443469 1.490982
foo 2.529056 0.645875
Оптимизированные функциями агрегирования Cython
Некоторые общие агрегации, в настоящее время только sum, mean, std, и sem, имеют оптимизированные реализации Cython:
In [62]: df.groupby('A').sum()
Out[62]:
C D
A
bar 0.443469 0.920834
foo 2.529056 -1.724719
In [63]: df.groupby(['A', 'B']).mean()
Out[63]:
C D
A B
bar one -0.042379 -0.089329
three -0.009920 -0.945867
two 0.495767 1.956030
foo one -0.278452 -0.556879
three 1.548106 -0.016692
two 0.768928 -0.297134
Конечно, sum и mean реализованы в объектах pandas, поэтому приведенный выше код будет работать даже без специальных версий через диспетчеризацию (см. ниже).
Преобразование
Метод transform возвращает объект, индексированный так же (того же размера), как и группируемый объект. Таким образом, переданная функция преобразования должна возвращать результат такого же размера, как фрагмент группы. Например, предположим, что мы хотим стандартизировать данные внутри каждой группы:
In [64]: index = pd.date_range('10/1/1999', periods=1100)
In [65]: ts = pd.Series(np.random.normal(0.5, 2, 1100), index)
In [66]: ts = ts.rolling(window=100,min_periods=100).mean().dropna()
In [67]: ts.head()
Out[67]:
2000-01-08 0.779333
2000-01-09 0.778852
2000-01-10 0.786476
2000-01-11 0.782797
2000-01-12 0.798110
Freq: D, dtype: float64
In [68]: ts.tail()
Out[68]:
2002-09-30 0.660294
2002-10-01 0.631095
2002-10-02 0.673601
2002-10-03 0.709213
2002-10-04 0.719369
Freq: D, dtype: float64
In [69]: key = lambda x: x.year
In [70]: zscore = lambda x: (x - x.mean()) / x.std()
In [71]: transformed = ts.groupby(key).transform(zscore)
Мы ожидаем, что результат будет иметь среднее значение 0 и стандартное отклонение 1 в каждой группе, что мы легко можем проверить:
# Original Data In [72]: grouped = ts.groupby(key) In [73]: grouped.mean() Out[73]: 2000 0.442441 2001 0.526246 2002 0.459365 dtype: float64 In [74]: grouped.std() Out[74]: 2000 0.131752 2001 0.210945 2002 0.128753 dtype: float64 # Transformed Data In [75]: grouped_trans = transformed.groupby(key) In [76]: grouped_trans.mean() Out[76]: 2000 -1.668427e-16 2001 2.609785e-16 2002 2.853714e-16 dtype: float64 In [77]: grouped_trans.std() Out[77]: 2000 1.0 2001 1.0 2002 1.0 dtype: float64
Мы также можем визуально сравнить исходные и преобразованные наборы данных.
In [78]: compare = pd.DataFrame({'Original': ts, 'Transformed': transformed})
In [79]: compare.plot()
Out[79]: <matplotlib.axes._subplots.AxesSubplot at 0x129f9d150>
Другим распространённым преобразованием данных является замена пропущенных данных средним значением группы.
In [80]: data_df
Out[80]:
A B C
0 1.539708 -1.166480 0.533026
1 1.302092 -0.505754 NaN
2 -0.371983 1.104803 -0.651520
3 -1.309622 1.118697 -1.161657
4 -1.924296 0.396437 0.812436
5 0.815643 0.367816 -0.469478
6 -0.030651 1.376106 -0.645129
.. ... ... ...
993 0.012359 0.554602 -1.976159
994 0.042312 -1.628835 1.013822
995 -0.093110 0.683847 -0.774753
996 -0.185043 1.438572 NaN
997 -0.394469 -0.642343 0.011374
998 -1.174126 1.857148 NaN
999 0.234564 0.517098 0.393534
[1000 rows x 3 columns]
In [81]: countries = np.array(['US', 'UK', 'GR', 'JP'])
In [82]: key = countries[np.random.randint(0, 4, 1000)]
In [83]: grouped = data_df.groupby(key)
# Non-NA count in each group
In [84]: grouped.count()
Out[84]:
A B C
GR 209 217 189
JP 240 255 217
UK 216 231 193
US 239 250 217
In [85]: f = lambda x: x.fillna(x.mean())
In [86]: transformed = grouped.transform(f)
Мы можем проверить, что средние значения групп не изменились в преобразованных данных и что преобразованные данные не содержат значений NA.
In [87]: grouped_trans = transformed.groupby(key)
In [88]: grouped.mean() # original group means
Out[88]:
A B C
GR -0.098371 -0.015420 0.068053
JP 0.069025 0.023100 -0.077324
UK 0.034069 -0.052580 -0.116525
US 0.058664 -0.020399 0.028603
In [89]: grouped_trans.mean() # transformation did not change group means
Out[89]:
A B C
GR -0.098371 -0.015420 0.068053
JP 0.069025 0.023100 -0.077324
UK 0.034069 -0.052580 -0.116525
US 0.058664 -0.020399 0.028603
In [90]: grouped.count() # original has some missing data points
Out[90]:
A B C
GR 209 217 189
JP 240 255 217
UK 216 231 193
US 239 250 217
In [91]: grouped_trans.count() # counts after transformation
Out[91]:
A B C
GR 228 228 228
JP 267 267 267
UK 247 247 247
US 258 258 258
In [92]: grouped_trans.size() # Verify non-NA count equals group size
Out[92]:
GR 228
JP 267
UK 247
US 258
dtype: int64
Примечание
Некоторые функции, когда применяются к объекту groupby, автоматически преобразуют входные данные, возвращая объект той же формы, что и исходный. Передача as_index=False не повлияет на эти методы преобразования.
Например: fillna, ffill, bfill, shift.
In [93]: grouped.ffill()
Out[93]:
A B C
0 1.539708 -1.166480 0.533026
1 1.302092 -0.505754 0.533026
2 -0.371983 1.104803 -0.651520
3 -1.309622 1.118697 -1.161657
4 -1.924296 0.396437 0.812436
5 0.815643 0.367816 -0.469478
6 -0.030651 1.376106 -0.645129
.. ... ... ...
993 0.012359 0.554602 -1.976159
994 0.042312 -1.628835 1.013822
995 -0.093110 0.683847 -0.774753
996 -0.185043 1.438572 -0.774753
997 -0.394469 -0.642343 0.011374
998 -1.174126 1.857148 -0.774753
999 0.234564 0.517098 0.393534
[1000 rows x 3 columns]
Фильтрация
Введено в версии 0.12.
Метод filter возвращает подмножество исходного объекта. Предположим, что мы хотим взять только элементы, принадлежащие группам с суммой группы больше 2.
In [94]: sf = pd.Series([1, 1, 2, 3, 3, 3]) In [95]: sf.groupby(sf).filter(lambda x: x.sum() > 2) Out[95]: 3 3 4 3 5 3 dtype: int64
Аргумент filter должен быть функцией, которая, применённая к группе в целом, возвращает True или False.
Ещё одной полезной операцией является фильтрация элементов, принадлежащих группам с небольшим количеством членов.
In [96]: dff = pd.DataFrame({'A': np.arange(8), 'B': list('aabbbbcc')})
In [97]: dff.groupby('B').filter(lambda x: len(x) > 2)
Out[97]:
A B
2 2 b
3 3 b
4 4 b
5 5 b
В качестве альтернативы, вместо удаления проблемных групп, мы можем вернуть объекты с аналогичными индексами, где группы, которые не прошли фильтрацию, заполнены значениями NaN.
In [98]: dff.groupby('B').filter(lambda x: len(x) > 2, dropna=False)
Out[98]:
A B
0 NaN NaN
1 NaN NaN
2 2.0 b
3 3.0 b
4 4.0 b
5 5.0 b
6 NaN NaN
7 NaN NaN
Для фреймов данных с несколькими столбцами фильтры должны явно указывать столбец в качестве критерия фильтрации.
In [99]: dff['C'] = np.arange(8)
In [100]: dff.groupby('B').filter(lambda x: len(x['C']) > 2)
Out[100]:
A B C
2 2 b 2
3 3 b 3
4 4 b 4
5 5 b 5
Примечание
Некоторые функции, когда применяются к объекту groupby, действуют как фильтр на входе, возвращая уменьшенную форму исходного (и потенциально удаляя группы), но с неизменённым индексом. Передача as_index=False не повлияет на эти методы преобразования.
Например: head, tail.
In [101]: dff.groupby('B').head(2)
Out[101]:
A B C
0 0 a 0
1 1 a 1
2 2 b 2
3 3 b 3
6 6 c 6
7 7 c 7
Передача в методы экземпляров
При выполнении агрегации или преобразования вы можете просто вызвать метод экземпляра для каждой группы данных. Это довольно легко сделать, передав лямбда-функции:
In [102]: grouped = df.groupby('A')
In [103]: grouped.agg(lambda x: x.std())
Out[103]:
C D
A
bar 0.301765 1.490982
foo 0.966450 0.645875
Но это довольно громоздко и может быть неудобно, если вам нужно передать дополнительные аргументы. Используя немного метапрограммного мастерства, GroupBy теперь имеет возможность «передавать» вызовы методов группам:
In [104]: grouped.std()
Out[104]:
C D
A
bar 0.301765 1.490982
foo 0.966450 0.645875
На самом деле здесь генерируется функция-обёртка. При вызове она принимает все переданные аргументы и вызывает функцию с любыми аргументами для каждой группы (в приведённом выше примере функция std). Результаты затем объединяются примерно в стиле agg и transform (на самом деле используется apply для вывода склеивания, документированно ниже). Это позволяет выполнять некоторые операции довольно лаконично:
In [105]: tsdf = pd.DataFrame(np.random.randn(1000, 3),
.....: index=pd.date_range('1/1/2000', periods=1000),
.....: columns=['A', 'B', 'C'])
.....:
In [106]: tsdf.ix[::2] = np.nan
In [107]: grouped = tsdf.groupby(lambda x: x.year)
In [108]: grouped.fillna(method='pad')
Out[108]:
A B C
2000-01-01 NaN NaN NaN
2000-01-02 -0.353501 -0.080957 -0.876864
2000-01-03 -0.353501 -0.080957 -0.876864
2000-01-04 0.050976 0.044273 -0.559849
2000-01-05 0.050976 0.044273 -0.559849
2000-01-06 0.030091 0.186460 -0.680149
2000-01-07 0.030091 0.186460 -0.680149
... ... ... ...
2002-09-20 2.310215 0.157482 -0.064476
2002-09-21 2.310215 0.157482 -0.064476
2002-09-22 0.005011 0.053897 -1.026922
2002-09-23 0.005011 0.053897 -1.026922
2002-09-24 -0.456542 -1.849051 1.559856
2002-09-25 -0.456542 -1.849051 1.559856
2002-09-26 1.123162 0.354660 1.128135
[1000 rows x 3 columns]
В этом примере мы разбили коллекцию временных рядов на годовые фрагменты, а затем независимо вызвали fillna для групп.
Введено в версии 0.14.1.
Методы nlargest и nsmallest работают с группами в стиле Series:
In [109]: s = pd.Series([9, 8, 7, 5, 19, 1, 4.2, 3.3])
In [110]: g = pd.Series(list('abababab'))
In [111]: gb = s.groupby(g)
In [112]: gb.nlargest(3)
Out[112]:
a 4 19.0
0 9.0
2 7.0
b 1 8.0
3 5.0
7 3.3
dtype: float64
In [113]: gb.nsmallest(3)
Out[113]:
a 6 4.2
2 7.0
0 9.0
b 5 1.0
7 3.3
3 5.0
dtype: float64
Гибкая apply
Некоторые операции над сгруппированными данными могут не вписываться ни в категорию агрегации, ни в категорию преобразования. Или вы просто хотите, чтобы GroupBy определил, как объединить результаты. Для этого используйте функцию apply , которую можно заменить как aggregate , так и transform во многих стандартных случаях использования. Однако apply может обрабатывать некоторые исключительные случаи, например:
In [114]: df
Out[114]:
A B C D
0 foo one -0.919854 -1.131345
1 bar one -0.042379 -0.089329
2 foo two 1.247642 0.337863
3 bar three -0.009920 -0.945867
4 foo two 0.290213 -0.932132
5 bar two 0.495767 1.956030
6 foo one 0.362949 0.017587
7 foo three 1.548106 -0.016692
In [115]: grouped = df.groupby('A')
# could also just call .describe()
In [116]: grouped['C'].apply(lambda x: x.describe())
Out[116]:
A
bar count 3.000000
mean 0.147823
std 0.301765
min -0.042379
25% -0.026149
50% -0.009920
75% 0.242924
...
foo mean 0.505811
std 0.966450
min -0.919854
25% 0.290213
50% 0.362949
75% 1.247642
max 1.548106
Name: C, dtype: float64
Размер возвращаемого результата также может измениться:
In [117]: grouped = df.groupby('A')['C']
In [118]: def f(group):
.....: return pd.DataFrame({'original' : group,
.....: 'demeaned' : group - group.mean()})
.....:
In [119]: grouped.apply(f)
Out[119]:
demeaned original
0 -1.425665 -0.919854
1 -0.190202 -0.042379
2 0.741831 1.247642
3 -0.157743 -0.009920
4 -0.215598 0.290213
5 0.347944 0.495767
6 -0.142862 0.362949
7 1.042295 1.548106
apply для Series может работать со значением, возвращённым применённой функцией, которое само по себе является Series, и, возможно, повысит результат до DataFrame
In [120]: def f(x):
.....: return pd.Series([ x, x**2 ], index = ['x', 'x^2'])
.....:
In [121]: s
Out[121]:
0 9.0
1 8.0
2 7.0
3 5.0
4 19.0
5 1.0
6 4.2
7 3.3
dtype: float64
In [122]: s.apply(f)
Out[122]:
x x^2
0 9.0 81.00
1 8.0 64.00
2 7.0 49.00
3 5.0 25.00
4 19.0 361.00
5 1.0 1.00
6 4.2 17.64
7 3.3 10.89
Примечание
apply может выполнять функцию агрегатора, трансформатора или фильтра, в зависимости от того, что именно передаётся в apply. Таким образом, в зависимости от выбранного пути и точного значения группировки, сгруппированные столбцы (столбец/столбцы) могут быть включены в выходные данные, а также задать индексы.
Предупреждение
В текущей реализации apply дважды вызывает func для первой группы, чтобы определить, может ли он использовать быстрый или медленный путь кода. Это может привести к непредсказуемому поведению, если func имеет побочные эффекты, так как они вступят в силу дважды для первой группы.
In [123]: d = pd.DataFrame({"a":["x", "y"], "b":[1,2]})
In [124]: def identity(df):
.....: print df
.....: return df
.....:
In [125]: d.groupby("a").apply(identity)
a b
0 x 1
a b
0 x 1
a b
1 y 2
Out[125]:
a b
0 x 1
1 y 2
Другие полезные функции
Автоматическое исключение «вредных» столбцов
Рассмотрим пример DataFrame, который мы рассматривали:
In [126]: df
Out[126]:
A B C D
0 foo one -0.919854 -1.131345
1 bar one -0.042379 -0.089329
2 foo two 1.247642 0.337863
3 bar three -0.009920 -0.945867
4 foo two 0.290213 -0.932132
5 bar two 0.495767 1.956030
6 foo one 0.362949 0.017587
7 foo three 1.548106 -0.016692
Предположим, мы хотели вычислить стандартное отклонение, сгруппированное по столбцу A. Существует небольшая проблема, а именно, что нам не нужны данные в столбце B. Мы называем это «вредным» столбцом. Если переданная функция агрегации не может быть применена к некоторым столбцам, проблемные столбцы будут (молча) удалены. Таким образом, это не создаёт никаких проблем:
In [127]: df.groupby('A').std()
Out[127]:
C D
A
bar 0.301765 1.490982
foo 0.966450 0.645875
Обработка групп NA и NaT
Если в ключе группировки есть значения NaN или NaT, они будут автоматически исключены. Таким образом, никогда не будет «группы NA» или «группы NaT». Это не было так в старых версиях pandas, но пользователи обычно всё равно отбрасывали группу NA (и её поддержка была проблемой в реализации).
Группирование с упорядоченными факторами
Категориальные переменные, представленные экземплярами класса pandas Categorical , могут использоваться в качестве ключей группировки. В таком случае порядок уровней будет сохранён:
In [128]: data = pd.Series(np.random.randn(100)) In [129]: factor = pd.qcut(data, [0, .25, .5, .75, 1.]) In [130]: data.groupby(factor).mean() Out[130]: [-2.617, -0.684] -1.331461 (-0.684, -0.0232] -0.272816 (-0.0232, 0.541] 0.263607 (0.541, 2.369] 1.166038 dtype: float64
Группирование с помощью спецификации Grouper
Возможно, вам нужно будет указать больше данных для правильной группировки. Вы можете использовать pd.Grouper для обеспечения этого локального управления.
In [131]: import datetime
In [132]: df = pd.DataFrame({
.....: 'Branch' : 'A A A A A A A B'.split(),
.....: 'Buyer': 'Carl Mark Carl Carl Joe Joe Joe Carl'.split(),
.....: 'Quantity': [1,3,5,1,8,1,9,3],
.....: 'Date' : [
.....: datetime.datetime(2013,1,1,13,0),
.....: datetime.datetime(2013,1,1,13,5),
.....: datetime.datetime(2013,10,1,20,0),
.....: datetime.datetime(2013,10,2,10,0),
.....: datetime.datetime(2013,10,1,20,0),
.....: datetime.datetime(2013,10,2,10,0),
.....: datetime.datetime(2013,12,2,12,0),
.....: datetime.datetime(2013,12,2,14,0),
.....: ]
.....: })
.....:
In [133]: df
Out[133]:
Branch Buyer Date Quantity
0 A Carl 2013-01-01 13:00:00 1
1 A Mark 2013-01-01 13:05:00 3
2 A Carl 2013-10-01 20:00:00 5
3 A Carl 2013-10-02 10:00:00 1
4 A Joe 2013-10-01 20:00:00 8
5 A Joe 2013-10-02 10:00:00 1
6 A Joe 2013-12-02 12:00:00 9
7 B Carl 2013-12-02 14:00:00 3
Группировка по определённому столбцу с желаемой частотой. Это похоже на ресемплирование.
In [134]: df.groupby([pd.Grouper(freq='1M',key='Date'),'Buyer']).sum()
Out[134]:
Quantity
Date Buyer
2013-01-31 Carl 1
Mark 3
2013-10-31 Carl 6
Joe 9
2013-12-31 Carl 3
Joe 9
У вас есть неоднозначная спецификация, так как у вас есть именованный индекс и столбец, которые могут быть потенциальными группировщиками.
In [135]: df = df.set_index('Date')
In [136]: df['Date'] = df.index + pd.offsets.MonthEnd(2)
In [137]: df.groupby([pd.Grouper(freq='6M',key='Date'),'Buyer']).sum()
Out[137]:
Quantity
Date Buyer
2013-02-28 Carl 1
Mark 3
2014-02-28 Carl 9
Joe 18
In [138]: df.groupby([pd.Grouper(freq='6M',level='Date'),'Buyer']).sum()
Out[138]:
Quantity
Date Buyer
2013-01-31 Carl 1
Mark 3
2014-01-31 Carl 9
Joe 18
Получение первых строк каждой группы
Так же, как для DataFrame или Series, вы можете вызвать head и tail для groupby:
In [139]: df = pd.DataFrame([[1, 2], [1, 4], [5, 6]], columns=['A', 'B'])
In [140]: df
Out[140]:
A B
0 1 2
1 1 4
2 5 6
In [141]: g = df.groupby('A')
In [142]: g.head(1)
Out[142]:
A B
0 1 2
2 5 6
In [143]: g.tail(1)
Out[143]:
A B
1 1 4
2 5 6
Это отображает первые или последние n строк каждой группы.
Предупреждение
До версии 0.14.0 это реализовывалось с помощью fall-through apply, поэтому результат неправильно учитывал флаг as_index:
>>> g.head(1): # was equivalent to g.apply(lambda x: x.head(1))
A B
A
1 0 1 2
5 2 5 6
Получение n-й строки каждой группы
Чтобы выбрать элемент n в DataFrame или Series, используйте метод nth. Это метод сокращения, и он вернёт одну строку (или без строки) на группу, если вы передадите int для n:
In [144]: df = pd.DataFrame([[1, np.nan], [1, 4], [5, 6]], columns=['A', 'B'])
In [145]: g = df.groupby('A')
In [146]: g.nth(0)
Out[146]:
B
A
1 NaN
5 6.0
In [147]: g.nth(-1)
Out[147]:
B
A
1 4.0
5 6.0
In [148]: g.nth(1)
Out[148]:
B
A
1 4.0
Если вы хотите выбрать n-й ненулевой элемент, используйте аргумент dropna. Для DataFrame это должно быть либо 'any' или 'all' точно так же, как вы передаёте dropna, для Series это просто должно быть истинным значением.
# nth(0) is the same as g.first()
In [149]: g.nth(0, dropna='any')
Out[149]:
B
A
1 4.0
5 6.0
In [150]: g.first()
Out[150]:
B
A
1 4.0
5 6.0
# nth(-1) is the same as g.last()
In [151]: g.nth(-1, dropna='any') # NaNs denote group exhausted when using dropna
Out[151]:
B
A
1 4.0
5 6.0
In [152]: g.last()
Out[152]:
B
A
1 4.0
5 6.0
In [153]: g.B.nth(0, dropna=True)
Out[153]:
A
1 4.0
5 6.0
Name: B, dtype: float64
Как и в других методах, передача as_index=False, обеспечит фильтрацию, которая вернёт сгруппированную строку.
In [154]: df = pd.DataFrame([[1, np.nan], [1, 4], [5, 6]], columns=['A', 'B'])
In [155]: g = df.groupby('A',as_index=False)
In [156]: g.nth(0)
Out[156]:
A B
0 1 NaN
2 5 6.0
In [157]: g.nth(-1)
Out[157]:
A B
1 1 4.0
2 5 6.0
Вы также можете выбрать несколько строк из каждой группы, указав несколько значений n как список целых чисел.
In [158]: business_dates = pd.date_range(start='4/1/2014', end='6/30/2014', freq='B')
In [159]: df = pd.DataFrame(1, index=business_dates, columns=['a', 'b'])
# get the first, 4th, and last date index for each month
In [160]: df.groupby((df.index.year, df.index.month)).nth([0, 3, -1])
Out[160]:
a b
2014 4 1 1
4 1 1
4 1 1
5 1 1
5 1 1
5 1 1
6 1 1
6 1 1
6 1 1
Перечисление элементов группы
Введено в версии 0.13.0.
Чтобы увидеть порядок, в котором каждая строка появляется в своей группе, используйте метод cumcount:
In [161]: df = pd.DataFrame(list('aaabba'), columns=['A'])
In [162]: df
Out[162]:
A
0 a
1 a
2 a
3 b
4 b
5 a
In [163]: df.groupby('A').cumcount()
Out[163]:
0 0
1 1
2 2
3 0
4 1
5 3
dtype: int64
In [164]: df.groupby('A').cumcount(ascending=False) # kwarg only
Out[164]:
0 3
1 2
2 1
3 1
4 0
5 0
dtype: int64
Использование с графиками
GroupBy также работает с некоторыми методами построения графиков. Например, предположим, что мы подозреваем, что некоторые характеристики в DataFrame могут отличаться по группам, в этом случае значения в столбце 1, где группа «B», в среднем на 3 больше.
In [165]: np.random.seed(1234) In [166]: df = pd.DataFrame(np.random.randn(50, 2)) In [167]: df['g'] = np.random.choice(['A', 'B'], size=50) In [168]: df.loc[df['g'] == 'B', 1] += 3
Мы можем легко визуализировать это с помощью boxplot:
In [169]: df.groupby('g').boxplot()
Out[169]:
OrderedDict([('A',
{'boxes': [<matplotlib.lines.Line2D at 0x129abe210>,
<matplotlib.lines.Line2D at 0x10ef39090>],
'caps': [<matplotlib.lines.Line2D at 0x129727350>,
<matplotlib.lines.Line2D at 0x1214858d0>,
<matplotlib.lines.Line2D at 0x11291c810>,
<matplotlib.lines.Line2D at 0x129a7e290>],
'fliers': [<matplotlib.lines.Line2D at 0x129795150>,
<matplotlib.lines.Line2D at 0x113a77c10>],
'means': [],
'medians': [<matplotlib.lines.Line2D at 0x1131b7610>,
<matplotlib.lines.Line2D at 0x1213be590>],
'whiskers': [<matplotlib.lines.Line2D at 0x129abe150>,
<matplotlib.lines.Line2D at 0x129924750>,
<matplotlib.lines.Line2D at 0x12134ff90>,
<matplotlib.lines.Line2D at 0x120cd4f10>]}),
('B',
{'boxes': [<matplotlib.lines.Line2D at 0x129abe9d0>,
<matplotlib.lines.Line2D at 0x129af1d90>],
'caps': [<matplotlib.lines.Line2D at 0x129c98110>,
<matplotlib.lines.Line2D at 0x129c988d0>,
<matplotlib.lines.Line2D at 0x1299ec510>,
<matplotlib.lines.Line2D at 0x1299ecf50>],
'fliers': [<matplotlib.lines.Line2D at 0x1128ee850>,
<matplotlib.lines.Line2D at 0x129d6ca90>],
'means': [],
'medians': [<matplotlib.lines.Line2D at 0x12138a790>,
<matplotlib.lines.Line2D at 0x129961250>],
'whiskers': [<matplotlib.lines.Line2D at 0x129f56450>,
<matplotlib.lines.Line2D at 0x129767750>,
<matplotlib.lines.Line2D at 0x129a68410>,
<matplotlib.lines.Line2D at 0x129a68e90>]})])
Результат вызова boxplot — это словарь, ключами которого являются значения нашего группировочного столбца g («A» и «B»). Значения результирующего словаря можно контролировать с помощью ключевого слова return_type метода boxplot. См. документацию по визуализации здесь для получения дополнительной информации.
Предупреждение
По историческим причинам df.groupby("g").boxplot() не эквивалентно df.boxplot(by="g") См. здесь для объяснения.
Примеры
Перегруппировка по фактору
Перегруппируйте столбцы DataFrame в соответствии с их суммой и просуммируйте агрегированные.
In [170]: df = pd.DataFrame({'a':[1,0,0], 'b':[0,1,0], 'c':[1,0,0], 'd':[2,3,4]})
In [171]: df
Out[171]:
a b c d
0 1 0 1 2
1 0 1 0 3
2 0 0 0 4
In [172]: df.groupby(df.sum(), axis=1).sum()
Out[172]:
1 9
0 2 2
1 1 3
2 0 4
Возврат Series для распространения имён
Сгруппируйте столбцы DataFrame, вычислите набор метрик и верните именованный Series. Имя Series используется в качестве имени для индекса столбца. Это особенно полезно при совместном использовании с операциями изменения формы, такими как stacking, в которых имя индекса столбца будет использоваться в качестве имени вставляемого столбца:
In [173]: df = pd.DataFrame({
.....: 'a': [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2],
.....: 'b': [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1],
.....: 'c': [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0],
.....: 'd': [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
.....: })
.....:
In [174]: def compute_metrics(x):
.....: result = {'b_sum': x['b'].sum(), 'c_mean': x['c'].mean()}
.....: return pd.Series(result, name='metrics')
.....:
In [175]: result = df.groupby('a').apply(compute_metrics)
In [176]: result
Out[176]:
metrics b_sum c_mean
a
0 2.0 0.5
1 2.0 0.5
2 2.0 0.5
In [177]: result.stack()
Out[177]:
a metrics
0 b_sum 2.0
c_mean 0.5
1 b_sum 2.0
c_mean 0.5
2 b_sum 2.0
c_mean 0.5
dtype: float64
© 2008–2012, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team
Licensed under the 3-clause BSD License.
https://pandas.pydata.org/pandas-docs/version/0.18.1/groupby.html