Group By: split-apply-combine
По «group by» мы подразумеваем процесс, включающий один или несколько из следующих шагов
- Разбиение данных на группы на основе определенных критериев
- Применение функции к каждой группе независимо
- Объединение результатов в структуру данных
Из этих шагов этап разбиения является наиболее простым. На самом деле, во многих ситуациях вы можете разбить набор данных на группы и сами что-то сделать с этими группами. На этапе применения мы можем захотеть выполнить одно из следующих действий:
-
Агрегирование: вычисление сводной статистики (или статистик) по каждой группе. Некоторые примеры:
- Вычислить групповые суммы или средние значения
- Вычислить размеры/количество элементов в группах
-
Преобразование: выполнение некоторых групповых вычислений и возврат данных с теми же индексами. Некоторые примеры:
- Нормализация данных (z-оценка) внутри группы
- Заполнение пропусков (NaN) в группах значением, полученным из каждой группы
-
Фильтрация: отбрасывание некоторых групп на основе группового вычисления, которое вычисляет Истина или Ложь. Некоторые примеры:
- Отбрасывание данных, которые относятся к группам с небольшим количеством членов
- Фильтрация данных на основе групповой суммы или среднего значения
- Некоторое сочетание вышеперечисленного: 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': Int64Index([1, 3, 5], dtype='int64'),
'foo': Int64Index([0, 2, 4, 6, 7], dtype='int64')}
In [20]: df.groupby(get_letter_type, axis=1).groups
Out[20]:
{'consonant': Index([u'B', u'C', u'D'], dtype='object'),
'vowel': Index([u'A'], dtype='object')}
Вызов стандартной Python-функции len для объекта GroupBy просто возвращает длину словаря groups, поэтому это в основном просто удобство:
In [21]: grouped = df.groupby(['A', 'B'])
In [22]: grouped.groups
Out[22]:
{('bar', 'one'): Int64Index([1], dtype='int64'),
('bar', 'three'): Int64Index([3], dtype='int64'),
('bar', 'two'): Int64Index([5], dtype='int64'),
('foo', 'one'): Int64Index([0, 6], dtype='int64'),
('foo', 'three'): Int64Index([7], dtype='int64'),
('foo', 'two'): Int64Index([2, 4], dtype='int64')}
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 0x7ff26f58b810>
Кроме того, этот метод позволяет избежать повторного вычисления внутренней информации о группировке, полученной из переданного ключа.
Итерация по группам
С объектом 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
Обратите внимание, что вы можете использовать функцию reset_index DataFrame, чтобы добиться того же результата, поскольку имена столбцов хранятся в результирующем 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
Примечание
Если вы передаёте словарь в aggregate, порядок выходных столбцов не определён. Если вы хотите быть уверены, что выходные столбцы будут в определённом порядке, вы можете использовать OrderedDict. Сравните вывод следующих двух команд:
In [62]: grouped.agg({'D': 'std', 'C': 'mean'})
Out[62]:
C D
A
bar 0.147823 1.490982
foo 0.505811 0.645875
In [63]: grouped.agg(OrderedDict([('D', 'std'), ('C', 'mean')]))
Out[63]:
D C
A
bar 1.490982 0.147823
foo 0.645875 0.505811
Cython-оптимизированные функции агрегирования
Некоторые распространенные агрегации, в настоящее время только sum, mean, std, и sem, имеют оптимизированные реализации на Cython:
In [64]: df.groupby('A').sum()
Out[64]:
C D
A
bar 0.443469 0.920834
foo 2.529056 -1.724719
In [65]: df.groupby(['A', 'B']).mean()
Out[65]:
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 [66]: index = pd.date_range('10/1/1999', periods=1100)
In [67]: ts = pd.Series(np.random.normal(0.5, 2, 1100), index)
In [68]: ts = ts.rolling(window=100,min_periods=100).mean().dropna()
In [69]: ts.head()
Out[69]:
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 [70]: ts.tail()
Out[70]:
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 [71]: key = lambda x: x.year
In [72]: zscore = lambda x: (x - x.mean()) / x.std()
In [73]: transformed = ts.groupby(key).transform(zscore)
Мы ожидали, что результат теперь будет иметь среднее значение 0 и стандартное отклонение 1 в каждой группе, что мы можем легко проверить:
# Original Data In [74]: grouped = ts.groupby(key) In [75]: grouped.mean() Out[75]: 2000 0.442441 2001 0.526246 2002 0.459365 dtype: float64 In [76]: grouped.std() Out[76]: 2000 0.131752 2001 0.210945 2002 0.128753 dtype: float64 # Transformed Data In [77]: grouped_trans = transformed.groupby(key) In [78]: grouped_trans.mean() Out[78]: 2000 1.168208e-15 2001 1.454544e-15 2002 1.726657e-15 dtype: float64 In [79]: grouped_trans.std() Out[79]: 2000 1.0 2001 1.0 2002 1.0 dtype: float64
Мы также можем визуально сравнить исходные и преобразованные наборы данных.
In [80]: compare = pd.DataFrame({'Original': ts, 'Transformed': transformed})
In [81]: compare.plot()
Out[81]: <matplotlib.axes._subplots.AxesSubplot at 0x7ff26ffe62d0>
Другим распространённым преобразованием данных является замена пропущенных данных средним значением группы.
In [82]: data_df
Out[82]:
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 [83]: countries = np.array(['US', 'UK', 'GR', 'JP'])
In [84]: key = countries[np.random.randint(0, 4, 1000)]
In [85]: grouped = data_df.groupby(key)
# Non-NA count in each group
In [86]: grouped.count()
Out[86]:
A B C
GR 209 217 189
JP 240 255 217
UK 216 231 193
US 239 250 217
In [87]: f = lambda x: x.fillna(x.mean())
In [88]: transformed = grouped.transform(f)
Мы можем проверить, что средние значения групп не изменились в преобразованных данных и что преобразованные данные не содержат значений NaN.
In [89]: grouped_trans = transformed.groupby(key)
In [90]: grouped.mean() # original group means
Out[90]:
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 [91]: grouped_trans.mean() # transformation did not change group means
Out[91]:
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 [92]: grouped.count() # original has some missing data points
Out[92]:
A B C
GR 209 217 189
JP 240 255 217
UK 216 231 193
US 239 250 217
In [93]: grouped_trans.count() # counts after transformation
Out[93]:
A B C
GR 228 228 228
JP 267 267 267
UK 247 247 247
US 258 258 258
In [94]: grouped_trans.size() # Verify non-NA count equals group size
Out[94]:
GR 228
JP 267
UK 247
US 258
dtype: int64
Примечание
Некоторые функции, когда применяются к объекту groupby, автоматически преобразуют входные данные, возвращая объект той же формы, что и исходный. Передача as_index=False не повлияет на эти методы преобразования.
Например: fillna, ffill, bfill, shift.
In [95]: grouped.ffill()
Out[95]:
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]
Новый синтаксис для операций window и resample
Новое в версии 0.18.1.
Работа с операциями resample, expanding или rolling на уровне groupby раньше требовала применения вспомогательных функций. Однако теперь можно использовать resample(), expanding() и rolling() в качестве методов для groupby.
В примере ниже будет применена метод rolling() к образцам столбца B, сгруппированным по столбцу A.
In [96]: df_re = pd.DataFrame({'A': [1] * 10 + [5] * 10,
....: 'B': np.arange(20)})
....:
In [97]: df_re
Out[97]:
A B
0 1 0
1 1 1
2 1 2
3 1 3
4 1 4
5 1 5
6 1 6
.. .. ..
13 5 13
14 5 14
15 5 15
16 5 16
17 5 17
18 5 18
19 5 19
[20 rows x 2 columns]
In [98]: df_re.groupby('A').rolling(4).B.mean()
Out[98]:
A
1 0 NaN
1 NaN
2 NaN
3 1.5
4 2.5
5 3.5
6 4.5
...
5 13 11.5
14 12.5
15 13.5
16 14.5
17 15.5
18 16.5
19 17.5
Name: B, dtype: float64
Метод expanding() будет накапливать заданную операцию (sum() в примере) для всех членов каждой конкретной группы.
In [99]: df_re.groupby('A').expanding().sum()
Out[99]:
A B
A
1 0 1.0 0.0
1 2.0 1.0
2 3.0 3.0
3 4.0 6.0
4 5.0 10.0
5 6.0 15.0
6 7.0 21.0
... ... ...
5 13 20.0 46.0
14 25.0 60.0
15 30.0 75.0
16 35.0 91.0
17 40.0 108.0
18 45.0 126.0
19 50.0 145.0
[20 rows x 2 columns]
Предположим, что вы хотите использовать метод resample() для получения ежедневной частоты в каждой группе вашей таблицы данных и хотите заполнить пропущенные значения методом ffill().
In [100]: df_re = pd.DataFrame({'date': pd.date_range(start='2016-01-01',
.....: periods=4,
.....: freq='W'),
.....: 'group': [1, 1, 2, 2],
.....: 'val': [5, 6, 7, 8]}).set_index('date')
.....:
In [101]: df_re
Out[101]:
group val
date
2016-01-03 1 5
2016-01-10 1 6
2016-01-17 2 7
2016-01-24 2 8
In [102]: df_re.groupby('group').resample('1D').ffill()
Out[102]:
group val
group date
1 2016-01-03 1 5
2016-01-04 1 5
2016-01-05 1 5
2016-01-06 1 5
2016-01-07 1 5
2016-01-08 1 5
2016-01-09 1 5
... ... ...
2 2016-01-18 2 7
2016-01-19 2 7
2016-01-20 2 7
2016-01-21 2 7
2016-01-22 2 7
2016-01-23 2 7
2016-01-24 2 8
[16 rows x 2 columns]
Фильтрация
Новое в версии 0.12.
Метод filter возвращает подмножество исходного объекта. Предположим, мы хотим взять только элементы, принадлежащие группам с суммой группы больше 2.
In [103]: sf = pd.Series([1, 1, 2, 3, 3, 3]) In [104]: sf.groupby(sf).filter(lambda x: x.sum() > 2) Out[104]: 3 3 4 3 5 3 dtype: int64
Аргумент filter должен быть функцией, которая, применённая к группе целиком, возвращает True или False.
Другой полезной операцией является фильтрация элементов, принадлежащих группам с небольшим количеством членов.
In [105]: dff = pd.DataFrame({'A': np.arange(8), 'B': list('aabbbbcc')})
In [106]: dff.groupby('B').filter(lambda x: len(x) > 2)
Out[106]:
A B
2 2 b
3 3 b
4 4 b
5 5 b
Вместо удаления нарушающих групп, можно вернуть объекты с аналогичной индексацией, где группы, не прошедшие фильтрацию, заполнены значениями NaN.
In [107]: dff.groupby('B').filter(lambda x: len(x) > 2, dropna=False)
Out[107]:
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 [108]: dff['C'] = np.arange(8)
In [109]: dff.groupby('B').filter(lambda x: len(x['C']) > 2)
Out[109]:
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 [110]: dff.groupby('B').head(2)
Out[110]:
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 [111]: grouped = df.groupby('A')
In [112]: grouped.agg(lambda x: x.std())
Out[112]:
C D
A
bar 0.301765 1.490982
foo 0.966450 0.645875
Но это довольно громоздко и может быть неудобно, если вам нужно передать дополнительные аргументы. Используя немного хитрости метапрограммирования, GroupBy теперь имеет возможность «диспетчеризировать» вызовы методов к группам:
In [113]: grouped.std()
Out[113]:
C D
A
bar 0.301765 1.490982
foo 0.966450 0.645875
На самом деле здесь генерируется функция-обертка. При вызове она принимает все переданные аргументы и вызывает функцию со всеми аргументами в каждой группе (в приведенном выше примере функция std). Результаты затем объединяются в стиле agg и transform (на самом деле она использует apply для определения склеивания, документация которого приведена далее). Это позволяет выполнять некоторые операции довольно лаконично:
In [114]: tsdf = pd.DataFrame(np.random.randn(1000, 3),
.....: index=pd.date_range('1/1/2000', periods=1000),
.....: columns=['A', 'B', 'C'])
.....:
In [115]: tsdf.ix[::2] = np.nan
In [116]: grouped = tsdf.groupby(lambda x: x.year)
In [117]: grouped.fillna(method='pad')
Out[117]:
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 [118]: s = pd.Series([9, 8, 7, 5, 19, 1, 4.2, 3.3])
In [119]: g = pd.Series(list('abababab'))
In [120]: gb = s.groupby(g)
In [121]: gb.nlargest(3)
Out[121]:
a 4 19.0
0 9.0
2 7.0
b 1 8.0
3 5.0
7 3.3
dtype: float64
In [122]: gb.nsmallest(3)
Out[122]:
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 [123]: df
Out[123]:
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 [124]: grouped = df.groupby('A')
# could also just call .describe()
In [125]: grouped['C'].apply(lambda x: x.describe())
Out[125]:
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 [126]: grouped = df.groupby('A')['C']
In [127]: def f(group):
.....: return pd.DataFrame({'original' : group,
.....: 'demeaned' : group - group.mean()})
.....:
In [128]: grouped.apply(f)
Out[128]:
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 может работать с возвращаемым значением из применяемой функции, которое само по себе является серией, и возможно повысить результат до DataFrames.
In [129]: def f(x):
.....: return pd.Series([ x, x**2 ], index = ['x', 'x^2'])
.....:
In [130]: s
Out[130]:
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 [131]: s.apply(f)
Out[131]:
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 дважды вызывает func для первой группы, чтобы определить, может ли он использовать быстрый или медленный путь кода. Это может привести к неожиданному поведению, если func имеет побочные эффекты, так как они будут выполняться дважды для первой группы.
In [132]: d = pd.DataFrame({"a":["x", "y"], "b":[1,2]})
In [133]: def identity(df):
.....: print df
.....: return df
.....:
In [134]: d.groupby("a").apply(identity)
a b
0 x 1
a b
0 x 1
a b
1 y 2
Out[134]:
a b
0 x 1
1 y 2
Другие полезные функции
Автоматическое исключение «мешающих» столбцов
Рассмотрим снова пример DataFrame, который мы рассматривали:
In [135]: df
Out[135]:
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 [136]: df.groupby('A').std()
Out[136]:
C D
A
bar 0.301765 1.490982
foo 0.966450 0.645875
Обработка групп NaN и NaT
Если в ключе группировки есть какие-либо значения NaN или NaT, они будут автоматически исключены. Таким образом, никогда не будет группы «NaN» или «NaT». В старых версиях pandas это было не так, но пользователи обычно отбрасывали группу NaN (и поддержка этой группы была проблемой реализации).
Группировка с упорядоченными факторами
Категориальные переменные, представленные экземплярами класса Categorical pandas, могут использоваться в качестве ключей группировки. В этом случае порядок уровней будет сохранён:
In [137]: data = pd.Series(np.random.randn(100)) In [138]: factor = pd.qcut(data, [0, .25, .5, .75, 1.]) In [139]: data.groupby(factor).mean() Out[139]: [-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 [140]: import datetime
In [141]: 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 [142]: df
Out[142]:
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 [143]: df.groupby([pd.Grouper(freq='1M',key='Date'),'Buyer']).sum()
Out[143]:
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 [144]: df = df.set_index('Date')
In [145]: df['Date'] = df.index + pd.offsets.MonthEnd(2)
In [146]: df.groupby([pd.Grouper(freq='6M',key='Date'),'Buyer']).sum()
Out[146]:
Quantity
Date Buyer
2013-02-28 Carl 1
Mark 3
2014-02-28 Carl 9
Joe 18
In [147]: df.groupby([pd.Grouper(freq='6M',level='Date'),'Buyer']).sum()
Out[147]:
Quantity
Date Buyer
2013-01-31 Carl 1
Mark 3
2014-01-31 Carl 9
Joe 18
Взятие первых строк каждой группы
Так же, как для DataFrame или Series, вы можете вызвать head и tail для groupby:
In [148]: df = pd.DataFrame([[1, 2], [1, 4], [5, 6]], columns=['A', 'B'])
In [149]: df
Out[149]:
A B
0 1 2
1 1 4
2 5 6
In [150]: g = df.groupby('A')
In [151]: g.head(1)
Out[151]:
A B
0 1 2
2 5 6
In [152]: g.tail(1)
Out[152]:
A B
1 1 4
2 5 6
Это показывает первые или последние n строк из каждой группы.
Предупреждение
До версии 0.14.0 это реализовывалось с помощью 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-ой строки каждой группы
Для выбора из DataFrame или Series n-го элемента используйте метод nth. Это метод сокращения, и он вернёт одну строку (или не вернёт строку) на группу, если вы передадите целое число для n:
In [153]: df = pd.DataFrame([[1, np.nan], [1, 4], [5, 6]], columns=['A', 'B'])
In [154]: g = df.groupby('A')
In [155]: g.nth(0)
Out[155]:
B
A
1 NaN
5 6.0
In [156]: g.nth(-1)
Out[156]:
B
A
1 4.0
5 6.0
In [157]: g.nth(1)
Out[157]:
B
A
1 4.0
Если вы хотите выбрать n-ый непустой элемент, используйте параметр dropna. Для DataFrame это должно быть либо 'any' или 'all', как вы бы передали для dropna, для Series это просто должно быть истинным значением.
# nth(0) is the same as g.first()
In [158]: g.nth(0, dropna='any')
Out[158]:
B
A
1 4.0
5 6.0
In [159]: g.first()
Out[159]:
B
A
1 4.0
5 6.0
# nth(-1) is the same as g.last()
In [160]: g.nth(-1, dropna='any') # NaNs denote group exhausted when using dropna
Out[160]:
B
A
1 4.0
5 6.0
In [161]: g.last()
Out[161]:
B
A
1 4.0
5 6.0
In [162]: g.B.nth(0, dropna=True)
Out[162]:
A
1 4.0
5 6.0
Name: B, dtype: float64
Как и в других методах, передача as_index=False, приведет к фильтрации, которая возвращает сгруппированную строку.
In [163]: df = pd.DataFrame([[1, np.nan], [1, 4], [5, 6]], columns=['A', 'B'])
In [164]: g = df.groupby('A',as_index=False)
In [165]: g.nth(0)
Out[165]:
A B
0 1 NaN
2 5 6.0
In [166]: g.nth(-1)
Out[166]:
A B
1 1 4.0
2 5 6.0
Вы также можете выбрать несколько строк из каждой группы, указав несколько значений nth в виде списка целых чисел.
In [167]: business_dates = pd.date_range(start='4/1/2014', end='6/30/2014', freq='B')
In [168]: df = pd.DataFrame(1, index=business_dates, columns=['a', 'b'])
# get the first, 4th, and last date index for each month
In [169]: df.groupby((df.index.year, df.index.month)).nth([0, 3, -1])
Out[169]:
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 [170]: df = pd.DataFrame(list('aaabba'), columns=['A'])
In [171]: df
Out[171]:
A
0 a
1 a
2 a
3 b
4 b
5 a
In [172]: df.groupby('A').cumcount()
Out[172]:
0 0
1 1
2 2
3 0
4 1
5 3
dtype: int64
In [173]: df.groupby('A').cumcount(ascending=False) # kwarg only
Out[173]:
0 3
1 2
2 1
3 1
4 0
5 0
dtype: int64
Использование с графиками
GroupBy также работает с некоторыми методами построения графиков. Например, предположим, что мы подозреваем, что некоторые признаки в DataFrame могут отличаться по группам, в этом случае значения в столбце 1, где группа «B», в среднем на 3 больше.
In [174]: np.random.seed(1234) In [175]: df = pd.DataFrame(np.random.randn(50, 2)) In [176]: df['g'] = np.random.choice(['A', 'B'], size=50) In [177]: df.loc[df['g'] == 'B', 1] += 3
Мы можем легко визуализировать это с помощью boxplot:
In [178]: df.groupby('g').boxplot()
Out[178]:
A Axes(0.1,0.15;0.363636x0.75)
B Axes(0.536364,0.15;0.363636x0.75)
dtype: object
Результат вызова boxplot — словарь, ключами которого являются значения нашего столбца группировки g («A» и «B»). Значения результирующего словаря можно контролировать с помощью ключевого слова return_type метода boxplot. См. документацию по визуализации для получения дополнительной информации.
Предупреждение
По историческим причинам, df.groupby("g").boxplot() не эквивалентно df.boxplot(by="g"). См. здесь для объяснения.
Примеры
Группировка по фактору
Группировка столбцов DataFrame по их сумме и суммирование агрегированных значений.
In [179]: df = pd.DataFrame({'a':[1,0,0], 'b':[0,1,0], 'c':[1,0,0], 'd':[2,3,4]})
In [180]: df
Out[180]:
a b c d
0 1 0 1 2
1 0 1 0 3
2 0 0 0 4
In [181]: df.groupby(df.sum(), axis=1).sum()
Out[181]:
1 9
0 2 2
1 1 3
2 0 4
Группировка по индексатору для «перевыборки» данных
Перевыборка генерирует новые гипотетические выборки (перевыборки) из уже существующих наблюдаемых данных или из модели, которая генерирует данные. Эти новые выборки похожи на предварительно существующие выборки.
Для перевыборки по индексам, которые не являются datetimelike, можно использовать следующий метод.
В следующих примерах df.index // 5 возвращает двоичный массив, который используется для определения, что выбирается для операции groupby.
Примечание
В примере ниже показано, как можно уменьшить выборку путем объединения выборок в меньшее количество выборок. Используя df.index // 5, мы агрегируем выборки в интервалах. Применяя функцию std(), мы агрегируем информацию, содержащуюся во многих выборках, в небольшой подмножество значений, которые представляют собой их стандартное отклонение, тем самым уменьшая количество выборок.
In [182]: df = pd.DataFrame(np.random.randn(10,2))
In [183]: df
Out[183]:
0 1
0 -0.832423 0.114059
1 1.218203 -0.890593
2 0.165445 -1.127470
3 -1.192185 0.818644
4 0.237185 -0.336384
5 0.694727 0.750161
6 0.247055 0.645433
7 -1.366120 0.313160
8 0.205207 0.089987
9 0.186062 1.314182
In [184]: df.index // 5
Out[184]: Int64Index([0, 0, 0, 0, 0, 1, 1, 1, 1, 1], dtype='int64')
In [185]: df.groupby(df.index // 5).std()
Out[185]:
0 1
0 0.955154 0.783648
1 0.788428 0.467576
Возвращение Series для распространения имен
Группировка столбцов DataFrame, вычисление набора метрик и возвращение именованного Series. Имя Series используется в качестве имени для индекса столбца. Это особенно полезно в сочетании с операциями изменения формы, такими как stacking, в котором имя индекса столбца будет использоваться в качестве имени вставленного столбца:
In [186]: 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 [187]: def compute_metrics(x):
.....: result = {'b_sum': x['b'].sum(), 'c_mean': x['c'].mean()}
.....: return pd.Series(result, name='metrics')
.....:
In [188]: result = df.groupby('a').apply(compute_metrics)
In [189]: result
Out[189]:
metrics b_sum c_mean
a
0 2.0 0.5
1 2.0 0.5
2 2.0 0.5
In [190]: result.stack()
Out[190]:
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.19.2/groupby.html