Spec-Zone.ru › pandas 0.18

Группировка: 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>
_images/groupby_transform_plot.png

Другим распространённым преобразованием данных является замена пропущенных данных средним значением группы.

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>]})])
_images/groupby_boxplot.png

Результат вызова 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API