Spec-Zone.ru › pandas 1

Группировка: 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 позже), вы можете сделать следующее:

In [1]: df = pd.DataFrame(
   ...:     [
   ...:         ("bird", "Falconiformes", 389.0),
   ...:         ("bird", "Psittaciformes", 24.0),
   ...:         ("mammal", "Carnivora", 80.2),
   ...:         ("mammal", "Primates", np.nan),
   ...:         ("mammal", "Carnivora", 58),
   ...:     ],
   ...:     index=["falcon", "parrot", "lion", "monkey", "leopard"],
   ...:     columns=("class", "order", "max_speed"),
   ...: )
   ...: 

In [2]: df
Out[2]: 
          class           order  max_speed
falcon     bird   Falconiformes      389.0
parrot     bird  Psittaciformes       24.0
lion     mammal       Carnivora       80.2
monkey   mammal        Primates        NaN
leopard  mammal       Carnivora       58.0

# default is axis=0
In [3]: grouped = df.groupby("class")

In [4]: grouped = df.groupby("order", axis="columns")

In [5]: grouped = df.groupby(["class", "order"])

Отображение может быть задано различными способами:

  • Функция Python, которая должна вызываться для каждой метки оси.

  • Список или массив NumPy той же длины, что и выбранная ось.

  • Словарь или Series, предоставляющий отображение label -> group name.

  • Для объектов DataFrame, строка, указывающая имя столбца или имя уровня индекса для группировки.

  • df.groupby('A') — это просто синтаксический сахар для df.groupby(df['A']).

  • Список из любого из вышеперечисленного.

Совокупно объекты группировки называются **ключами**. Например, рассмотрим следующий DataFrame.

Примечание

Строка, переданная в groupby, может ссылаться на столбец или уровень индекса. Если строка соответствует и имени столбца, и имени уровня индекса, будет поднято исключение ValueError.

In [6]: 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 [7]: df
Out[7]: 
     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

Для DataFrame мы получаем объект GroupBy, вызвав groupby(). Мы могли бы естественным образом сгруппировать данные по столбцам A или B, или по обоим:

In [8]: grouped = df.groupby("A")

In [9]: grouped = df.groupby(["A", "B"])

Если у нас также есть MultiIndex по столбцам A и B, мы можем сгруппировать по всем столбцам, кроме указанных.

In [10]: df2 = df.set_index(["A", "B"])

In [11]: grouped = df2.groupby(level=df2.index.names.difference(["B"]))

In [12]: grouped.sum()
Out[12]: 
            C         D
A                      
bar -1.591710 -1.739537
foo -0.752861 -1.402938

Эти действия разделят DataFrame по его индексу (строкам). Мы также можем разделить по столбцам:

In [13]: def get_letter_type(letter):
   ....:     if letter.lower() in 'aeiou':
   ....:         return 'vowel'
   ....:     else:
   ....:         return 'consonant'
   ....: 

In [14]: grouped = df.groupby(get_letter_type, axis=1)

Объекты pandas Index поддерживают дублируемые значения. Если в операции groupby в качестве ключа группировки используется не уникальный индекс, все значения с одинаковым значением индекса будут рассматриваться как принадлежащие к одной группе, и, следовательно, результат функций агрегации будет содержать только уникальные значения индекса:

In [15]: lst = [1, 2, 3, 1, 2, 3]

In [16]: s = pd.Series([1, 2, 3, 10, 20, 30], lst)

In [17]: grouped = s.groupby(level=0)

In [18]: grouped.first()
Out[18]: 
1    1
2    2
3    3
dtype: int64

In [19]: grouped.last()
Out[19]: 
1    10
2    20
3    30
dtype: int64

In [20]: grouped.sum()
Out[20]: 
1    11
2    22
3    33
dtype: int64

Обратите внимание, что **разделение не происходит**, пока это не потребуется. Создание объекта GroupBy только проверяет, что вы передали допустимое отображение.

Примечание

Многие виды сложных манипуляций с данными могут быть выражены в терминах операций GroupBy (хотя не гарантируется, что это будет наиболее эффективным). Вы можете проявить творческий подход к функциям отображения меток.

Группировка сортировка

По умолчанию ключи групп сортируются во время операции groupby. Однако вы можете передать sort=False для потенциального ускорения:

In [21]: df2 = pd.DataFrame({"X": ["B", "B", "A", "A"], "Y": [1, 2, 3, 4]})

In [22]: df2.groupby(["X"]).sum()
Out[22]: 
   Y
X   
A  7
B  3

In [23]: df2.groupby(["X"], sort=False).sum()
Out[23]: 
   Y
X   
B  3
A  7

Обратите внимание, что groupby сохранит порядок, в котором отсортированы *наблюдения* *внутри* каждой группы. Например, группы, созданные groupby() ниже, расположены в том порядке, в котором они появились в исходном DataFrame:

In [24]: df3 = pd.DataFrame({"X": ["A", "B", "A", "B"], "Y": [1, 4, 3, 2]})

In [25]: df3.groupby(["X"]).get_group("A")
Out[25]: 
   X  Y
0  A  1
2  A  3

In [26]: df3.groupby(["X"]).get_group("B")
Out[26]: 
   X  Y
1  B  4
3  B  2

Введено в версии 1.1.0.

GroupBy dropna

По умолчанию значения NA исключаются из ключей групп во время операции groupby. Однако, если вы хотите включить значения NA в ключи групп, вы можете передать dropna=False для достижения этого.

In [27]: df_list = [[1, 2, 3], [1, None, 4], [2, 1, 3], [1, 2, 2]]

In [28]: df_dropna = pd.DataFrame(df_list, columns=["a", "b", "c"])

In [29]: df_dropna
Out[29]: 
   a    b  c
0  1  2.0  3
1  1  NaN  4
2  2  1.0  3
3  1  2.0  2
# Default ``dropna`` is set to True, which will exclude NaNs in keys
In [30]: df_dropna.groupby(by=["b"], dropna=True).sum()
Out[30]: 
     a  c
b        
1.0  2  3
2.0  2  5

# In order to allow NaN in keys, set ``dropna`` to False
In [31]: df_dropna.groupby(by=["b"], dropna=False).sum()
Out[31]: 
     a  c
b        
1.0  2  3
2.0  2  5
NaN  1  4

Значение аргумента dropna по умолчанию равно True, что означает, что значения NA не включаются в ключи групп.

Атрибуты объекта GroupBy

Атрибут groups — это словарь, ключами которого являются вычисленные уникальные группы, а соответствующие значения — метки осей, принадлежащие каждой группе. В приведенном выше примере у нас есть:

In [32]: df.groupby("A").groups
Out[32]: {'bar': [1, 3, 5], 'foo': [0, 2, 4, 6, 7]}

In [33]: df.groupby(get_letter_type, axis=1).groups
Out[33]: {'consonant': ['B', 'C', 'D'], 'vowel': ['A']}

Вызов стандартной Python-функции len для объекта GroupBy просто возвращает длину словаря groups, поэтому это в основном просто удобство:

In [34]: grouped = df.groupby(["A", "B"])

In [35]: grouped.groups
Out[35]: {('bar', 'one'): [1], ('bar', 'three'): [3], ('bar', 'two'): [5], ('foo', 'one'): [0, 6], ('foo', 'three'): [7], ('foo', 'two'): [2, 4]}

In [36]: len(grouped)
Out[36]: 6

GroupBy — это автодополнение имён столбцов (и других атрибутов):

In [37]: df
Out[37]: 
               height      weight  gender
2000-01-01  42.849980  157.500553    male
2000-01-02  49.607315  177.340407    male
2000-01-03  56.293531  171.524640    male
2000-01-04  48.421077  144.251986  female
2000-01-05  46.556882  152.526206    male
2000-01-06  68.448851  168.272968  female
2000-01-07  70.757698  136.431469    male
2000-01-08  58.909500  176.499753  female
2000-01-09  76.435631  174.094104  female
2000-01-10  45.306120  177.540920    male

In [38]: gb = df.groupby("gender")
In [39]: gb.<TAB>  # noqa: E225, E999
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

Группировка с MultiIndex

С иерархически индексированными данными вполне естественно сгруппировать данные по одному из уровней иерархии.

Создадим Series с двухъярусным MultiIndex.

In [40]: arrays = [
   ....:     ["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
   ....:     ["one", "two", "one", "two", "one", "two", "one", "two"],
   ....: ]
   ....: 

In [41]: index = pd.MultiIndex.from_arrays(arrays, names=["first", "second"])

In [42]: s = pd.Series(np.random.randn(8), index=index)

In [43]: s
Out[43]: 
first  second
bar    one      -0.919854
       two      -0.042379
baz    one       1.247642
       two      -0.009920
foo    one       0.290213
       two       0.495767
qux    one       0.362949
       two       1.548106
dtype: float64

Затем мы можем сгруппировать по одному из уровней в s.

In [44]: grouped = s.groupby(level=0)

In [45]: grouped.sum()
Out[45]: 
first
bar   -0.962232
baz    1.237723
foo    0.785980
qux    1.911055
dtype: float64

Если у MultiIndex указаны имена, вместо номера уровня можно передать их:

In [46]: s.groupby(level="second").sum()
Out[46]: 
second
one    0.980950
two    1.991575
dtype: float64

Поддерживается группировка по нескольким уровням.

In [47]: s
Out[47]: 
first  second  third
bar    doo     one     -1.131345
               two     -0.089329
baz    bee     one      0.337863
               two     -0.945867
foo    bop     one     -0.932132
               two      1.956030
qux    bop     one      0.017587
               two     -0.016692
dtype: float64

In [48]: s.groupby(level=["first", "second"]).sum()
Out[48]: 
first  second
bar    doo      -1.220674
baz    bee      -0.608004
foo    bop       1.023898
qux    bop       0.000895
dtype: float64

Имена уровней индекса могут быть переданы в качестве ключей.

In [49]: s.groupby(["first", "second"]).sum()
Out[49]: 
first  second
bar    doo      -1.220674
baz    bee      -0.608004
foo    bop       1.023898
qux    bop       0.000895
dtype: float64

Подробнее о функции sum и агрегации позже.

Группировка DataFrame с уровнями индексов и столбцами

DataFrame может быть сгруппирован по сочетанию столбцов и уровней индексов, указав имена столбцов как строки, а уровни индексов как объекты pd.Grouper.

In [50]: arrays = [
   ....:     ["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
   ....:     ["one", "two", "one", "two", "one", "two", "one", "two"],
   ....: ]
   ....: 

In [51]: index = pd.MultiIndex.from_arrays(arrays, names=["first", "second"])

In [52]: df = pd.DataFrame({"A": [1, 1, 1, 1, 2, 2, 3, 3], "B": np.arange(8)}, index=index)

In [53]: df
Out[53]: 
              A  B
first second      
bar   one     1  0
      two     1  1
baz   one     1  2
      two     1  3
foo   one     2  4
      two     2  5
qux   one     3  6
      two     3  7

В следующем примере DataFrame группируется по уровню индекса second и столбцу A.

In [54]: df.groupby([pd.Grouper(level=1), "A"]).sum()
Out[54]: 
          B
second A   
one    1  2
       2  4
       3  6
two    1  4
       2  5
       3  7

Уровни индексов также можно указать по имени.

In [55]: df.groupby([pd.Grouper(level="second"), "A"]).sum()
Out[55]: 
          B
second A   
one    1  2
       2  4
       3  6
two    1  4
       2  5
       3  7

Имена уровней индексов также можно указать непосредственно в качестве ключей для groupby.

In [56]: df.groupby(["second", "A"]).sum()
Out[56]: 
          B
second A   
one    1  2
       2  4
       3  6
two    1  4
       2  5
       3  7

Выбор столбцов DataFrame в GroupBy

После создания объекта GroupBy из DataFrame вы можете захотеть выполнить разные действия для каждого столбца. Таким образом, используя [], аналогично получению столбца из DataFrame, вы можете сделать:

In [57]: 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 [58]: df
Out[58]: 
     A      B         C         D
0  foo    one -0.575247  1.346061
1  bar    one  0.254161  1.511763
2  foo    two -1.143704  1.627081
3  bar  three  0.215897 -0.990582
4  foo    two  1.193555 -0.441652
5  bar    two -0.077118  1.211526
6  foo    one -0.408530  0.268520
7  foo  three -0.862495  0.024580

In [59]: grouped = df.groupby(["A"])

In [60]: grouped_C = grouped["C"]

In [61]: grouped_D = grouped["D"]

Это в основном синтаксический сахар для альтернативного и гораздо более громоздкого способа:

In [62]: df["C"].groupby(df["A"])
Out[62]: <pandas.core.groupby.generic.SeriesGroupBy object at 0x7f2808f6ef40>

Кроме того, этот метод позволяет избежать повторного вычисления внутренней информации о группировке, полученной из переданного ключа.

Проход по группам

Используя объект GroupBy, проход по сгруппированным данным очень естественен и работает аналогично itertools.groupby():

In [63]: grouped = df.groupby('A')

In [64]: for name, group in grouped:
   ....:     print(name)
   ....:     print(group)
   ....: 
bar
     A      B         C         D
1  bar    one  0.254161  1.511763
3  bar  three  0.215897 -0.990582
5  bar    two -0.077118  1.211526
foo
     A      B         C         D
0  foo    one -0.575247  1.346061
2  foo    two -1.143704  1.627081
4  foo    two  1.193555 -0.441652
6  foo    one -0.408530  0.268520
7  foo  three -0.862495  0.024580

В случае группировки по нескольким ключам имя группы будет кортежем:

In [65]: for name, group in df.groupby(['A', 'B']):
   ....:     print(name)
   ....:     print(group)
   ....: 
('bar', 'one')
     A    B         C         D
1  bar  one  0.254161  1.511763
('bar', 'three')
     A      B         C         D
3  bar  three  0.215897 -0.990582
('bar', 'two')
     A    B         C         D
5  bar  two -0.077118  1.211526
('foo', 'one')
     A    B         C         D
0  foo  one -0.575247  1.346061
6  foo  one -0.408530  0.268520
('foo', 'three')
     A      B         C        D
7  foo  three -0.862495  0.02458
('foo', 'two')
     A    B         C         D
2  foo  two -1.143704  1.627081
4  foo  two  1.193555 -0.441652

См. Проход по группам.

Выбор группы

Отдельную группу можно выбрать, используя get_group():

In [66]: grouped.get_group("bar")
Out[66]: 
     A      B         C         D
1  bar    one  0.254161  1.511763
3  bar  three  0.215897 -0.990582
5  bar    two -0.077118  1.211526

Или для объекта, сгруппированного по нескольким столбцам:

In [67]: df.groupby(["A", "B"]).get_group(("bar", "one"))
Out[67]: 
     A    B         C         D
1  bar  one  0.254161  1.511763

Агрегирование

После создания объекта GroupBy доступны несколько методов для выполнения вычислений на сгруппированных данных. Эти операции аналогичны API агрегирования API агрегирования, API окон API окон и API ресемплирования временных рядов API ресемплирования.

Очевидным является агрегирование с помощью метода aggregate() или, что эквивалентно, agg() метода:

In [68]: grouped = df.groupby("A")

In [69]: grouped[["C", "D"]].aggregate(np.sum)
Out[69]: 
            C         D
A                      
bar  0.392940  1.732707
foo -1.796421  2.824590

In [70]: grouped = df.groupby(["A", "B"])

In [71]: grouped.aggregate(np.sum)
Out[71]: 
                  C         D
A   B                        
bar one    0.254161  1.511763
    three  0.215897 -0.990582
    two   -0.077118  1.211526
foo one   -0.983776  1.614581
    three -0.862495  0.024580
    two    0.049851  1.185429

Как видите, результат агрегирования будет иметь имена групп в качестве нового индекса вдоль сгруппированной оси. В случае нескольких ключей результатом по умолчанию является MultiIndex, хотя это можно изменить с помощью опции as_index.

In [72]: grouped = df.groupby(["A", "B"], as_index=False)

In [73]: grouped.aggregate(np.sum)
Out[73]: 
     A      B         C         D
0  bar    one  0.254161  1.511763
1  bar  three  0.215897 -0.990582
2  bar    two -0.077118  1.211526
3  foo    one -0.983776  1.614581
4  foo  three -0.862495  0.024580
5  foo    two  0.049851  1.185429

In [74]: df.groupby("A", as_index=False)[["C", "D"]].sum()
Out[74]: 
     A         C         D
0  bar  0.392940  1.732707
1  foo -1.796421  2.824590

Обратите внимание, что вы можете использовать функцию DataFrame reset_index для достижения того же результата, так как имена столбцов хранятся в результирующем MultiIndex:

In [75]: df.groupby(["A", "B"]).sum().reset_index()
Out[75]: 
     A      B         C         D
0  bar    one  0.254161  1.511763
1  bar  three  0.215897 -0.990582
2  bar    two -0.077118  1.211526
3  foo    one -0.983776  1.614581
4  foo  three -0.862495  0.024580
5  foo    two  0.049851  1.185429

Еще один простой пример агрегирования — вычисление размера каждой группы. Это включено в GroupBy как метод size. Он возвращает Series, индекс которого — имена групп, а значения — размеры каждой группы.

In [76]: grouped.size()
Out[76]: 
     A      B  size
0  bar    one     1
1  bar  three     1
2  bar    two     1
3  foo    one     2
4  foo  three     1
5  foo    two     2
In [77]: grouped.describe()
Out[77]: 
      C                                ...         D                              
  count      mean       std       min  ...       25%       50%       75%       max
0   1.0  0.254161       NaN  0.254161  ...  1.511763  1.511763  1.511763  1.511763
1   1.0  0.215897       NaN  0.215897  ... -0.990582 -0.990582 -0.990582 -0.990582
2   1.0 -0.077118       NaN -0.077118  ...  1.211526  1.211526  1.211526  1.211526
3   2.0 -0.491888  0.117887 -0.575247  ...  0.537905  0.807291  1.076676  1.346061
4   1.0 -0.862495       NaN -0.862495  ...  0.024580  0.024580  0.024580  0.024580
5   2.0  0.024925  1.652692 -1.143704  ...  0.075531  0.592714  1.109898  1.627081

[6 rows x 16 columns]

Еще один пример агрегирования — вычисление количества уникальных значений каждой группы. Это аналогично функции value_counts, за исключением того, что она подсчитывает только уникальные значения.

In [78]: ll = [['foo', 1], ['foo', 2], ['foo', 2], ['bar', 1], ['bar', 1]]

In [79]: df4 = pd.DataFrame(ll, columns=["A", "B"])

In [80]: df4
Out[80]: 
     A  B
0  foo  1
1  foo  2
2  foo  2
3  bar  1
4  bar  1

In [81]: df4.groupby("A")["B"].nunique()
Out[81]: 
A
bar    1
foo    2
Name: B, dtype: int64

Примечание

Функции агрегирования не будут возвращать группы, по которым вы агрегируете, если они являются именованными столбцами, если as_index=True, по умолчанию. Сгруппированные столбцы будут индексами возвращаемого объекта.

Передача as_index=False будет возвращать группы, по которым вы агрегируете, если они являются именованными столбцами.

Функции агрегирования — это те, которые уменьшают размер возвращаемых объектов. Некоторые распространенные функции агрегирования приведены ниже:

Функция

Описание

mean()

Вычисление среднего арифметического групп

sum()

Вычисление суммы значений группы

size()

Вычисление размеров групп

count()

Подсчет количества в группе

std()

Среднее квадратическое отклонение групп

var()

Вычисление дисперсии групп

sem()

Стандартная ошибка среднего арифметического групп

describe()

Генерирует описательные статистические данные

first()

Вычисление первого значения группы

last()

Вычисление последнего значения группы

nth()

Получение n-го значения или подмножества, если n — список

min()

Вычисление минимума значений группы

max()

Вычисление максимума значений группы

Перечисленные выше функции агрегирования исключают значения NaN. Любая функция, которая сводит Series к скалярному значению, является функцией агрегирования и будет работать, тривиальным примером является df.groupby('A').agg(lambda ser: 1). Обратите внимание, что nth() может выполнять роль уменьшающего или фильтрующего элемента, см. здесь.

Применение нескольких функций одновременно

Сгруппированным Series также можно передать список или словарь функций для агрегирования, выдав DataFrame:

In [82]: grouped = df.groupby("A")

In [83]: grouped["C"].agg([np.sum, np.mean, np.std])
Out[83]: 
          sum      mean       std
A                                
bar  0.392940  0.130980  0.181231
foo -1.796421 -0.359284  0.912265

К сгруппированному DataFrame можно передать список функций для применения к каждому столбцу, что даёт результат агрегирования с иерархическим индексом:

In [84]: grouped[["C", "D"]].agg([np.sum, np.mean, np.std])
Out[84]: 
            C                             D                    
          sum      mean       std       sum      mean       std
A                                                              
bar  0.392940  0.130980  0.181231  1.732707  0.577569  1.366330
foo -1.796421 -0.359284  0.912265  2.824590  0.564918  0.884785

Полученные агрегации именованные самими функциями. Если нужно переименовать, можно добавить цепочку операций для Series так:

In [85]: (
   ....:     grouped["C"]
   ....:     .agg([np.sum, np.mean, np.std])
   ....:     .rename(columns={"sum": "foo", "mean": "bar", "std": "baz"})
   ....: )
   ....: 
Out[85]: 
          foo       bar       baz
A                                
bar  0.392940  0.130980  0.181231
foo -1.796421 -0.359284  0.912265

Для сгруппированного DataFrame можно переименовать аналогичным образом:

In [86]: (
   ....:     grouped[["C", "D"]].agg([np.sum, np.mean, np.std]).rename(
   ....:         columns={"sum": "foo", "mean": "bar", "std": "baz"}
   ....:     )
   ....: )
   ....: 
Out[86]: 
            C                             D                    
          foo       bar       baz       foo       bar       baz
A                                                              
bar  0.392940  0.130980  0.181231  1.732707  0.577569  1.366330
foo -1.796421 -0.359284  0.912265  2.824590  0.564918  0.884785

Примечание

В общем случае имена выходных столбцов должны быть уникальными. Нельзя применять одну и ту же функцию (или две функции с одинаковым именем) к одному и тому же столбцу.

In [87]: grouped["C"].agg(["sum", "sum"])
Out[87]: 
          sum       sum
A                      
bar  0.392940  0.392940
foo -1.796421 -1.796421

pandas разрешает передавать несколько лямбда-функций. В этом случае pandas будет изменять имя (безымянных) лямбда-функций, добавляя _<i> к каждой последующей лямбда-функции.

In [88]: grouped["C"].agg([lambda x: x.max() - x.min(), lambda x: x.median() - x.mean()])
Out[88]: 
     <lambda_0>  <lambda_1>
A                          
bar    0.331279    0.084917
foo    2.337259   -0.215962

Именованное агрегирование

Новое в версии 0.25.0.

Для поддержки агрегирования по столбцам с контролем имён выходных столбцов pandas поддерживает специальный синтаксис в GroupBy.agg(), известный как «именованное агрегирование», где:

  • Ключевые слова являются именами выходных столбцов

  • Значения — кортежи, первый элемент которых — столбец для выбора, а второй — функция агрегирования, применяемая к этому столбцу. pandas предоставляет именованную кортежную функцию pandas.NamedAgg с полями ['column', 'aggfunc'] для большей ясности аргументов. Как обычно, агрегирование может быть вызываемым объектом или строковым псевдонимом.

In [89]: animals = pd.DataFrame(
   ....:     {
   ....:         "kind": ["cat", "dog", "cat", "dog"],
   ....:         "height": [9.1, 6.0, 9.5, 34.0],
   ....:         "weight": [7.9, 7.5, 9.9, 198.0],
   ....:     }
   ....: )
   ....: 

In [90]: animals
Out[90]: 
  kind  height  weight
0  cat     9.1     7.9
1  dog     6.0     7.5
2  cat     9.5     9.9
3  dog    34.0   198.0

In [91]: animals.groupby("kind").agg(
   ....:     min_height=pd.NamedAgg(column="height", aggfunc="min"),
   ....:     max_height=pd.NamedAgg(column="height", aggfunc="max"),
   ....:     average_weight=pd.NamedAgg(column="weight", aggfunc=np.mean),
   ....: )
   ....: 
Out[91]: 
      min_height  max_height  average_weight
kind                                        
cat          9.1         9.5            8.90
dog          6.0        34.0          102.75

pandas.NamedAgg — это просто namedtuple. Также разрешены обычные кортежи.

In [92]: animals.groupby("kind").agg(
   ....:     min_height=("height", "min"),
   ....:     max_height=("height", "max"),
   ....:     average_weight=("weight", np.mean),
   ....: )
   ....: 
Out[92]: 
      min_height  max_height  average_weight
kind                                        
cat          9.1         9.5            8.90
dog          6.0        34.0          102.75

Если желаемые имена выходных столбцов не являются допустимыми ключевыми словами Python, создайте словарь и распакуйте ключевые аргументы:

In [93]: animals.groupby("kind").agg(
   ....:     **{
   ....:         "total weight": pd.NamedAgg(column="weight", aggfunc=sum)
   ....:     }
   ....: )
   ....: 
Out[93]: 
      total weight
kind              
cat           17.8
dog          205.5

Дополнительные ключевые аргументы не передаются функциям агрегирования. Только пары (column, aggfunc) должны передаваться как **kwargs. Если функции агрегирования требуют дополнительные аргументы, частично примените их с помощью functools.partial().

Примечание

Для Python 3.5 и более ранних версий порядок **kwargs в функциях не сохранялся. Это означало, что порядок выходных столбцов не был бы постоянным. Для обеспечения постоянного порядка ключи (и, следовательно, выходные столбцы) всегда будут отсортированы для Python 3.5.

Именованное агрегирование также допустимо для агрегирования сгруппированных Series. В этом случае выбора столбцов нет, поэтому значения — это просто функции.

In [94]: animals.groupby("kind").height.agg(
   ....:     min_height="min",
   ....:     max_height="max",
   ....: )
   ....: 
Out[94]: 
      min_height  max_height
kind                        
cat          9.1         9.5
dog          6.0        34.0

Применение различных функций к столбцам DataFrame

Передав словарь в aggregate, вы можете применить разное агрегирование к столбцам DataFrame:

In [95]: grouped.agg({"C": np.sum, "D": lambda x: np.std(x, ddof=1)})
Out[95]: 
            C         D
A                      
bar  0.392940  1.366330
foo -1.796421  0.884785

Имена функций также могут быть строками. Для того, чтобы строка была валидной, она должна быть реализована в GroupBy или доступна через диспетчеризацию:

In [96]: grouped.agg({"C": "sum", "D": "std"})
Out[96]: 
            C         D
A                      
bar  0.392940  1.366330
foo -1.796421  0.884785

Функции агрегирования с оптимизацией на Cython

Некоторые распространённые агрегации, в настоящее время только sum, mean, std, и sem, имеют оптимизированные реализации на Cython:

In [97]: df.groupby("A")[["C", "D"]].sum()
Out[97]: 
            C         D
A                      
bar  0.392940  1.732707
foo -1.796421  2.824590

In [98]: df.groupby(["A", "B"]).mean()
Out[98]: 
                  C         D
A   B                        
bar one    0.254161  1.511763
    three  0.215897 -0.990582
    two   -0.077118  1.211526
foo one   -0.491888  0.807291
    three -0.862495  0.024580
    two    0.024925  0.592714

Конечно, sum и mean реализованы в объектах pandas, поэтому вышеуказанный код будет работать даже без специальных версий через диспетчеризацию (см. ниже).

Агрегирование с пользовательскими функциями

Пользователи также могут предоставлять свои собственные функции для пользовательского агрегирования. При агрегировании с пользовательской функцией (UDF) UDF не должна изменять предоставленный Series, см. Изменение с пользовательскими функциями (UDF) для получения дополнительной информации.

In [99]: animals.groupby("kind")[["height"]].agg(lambda x: set(x))
Out[99]: 
           height
kind             
cat    {9.1, 9.5}
dog   {34.0, 6.0}

Результирующий тип данных будет отражать тип данных функции агрегирования. Если результаты разных групп имеют разные типы данных, то общий тип данных будет определяться так же, как и при построении DataFrame.

In [100]: animals.groupby("kind")[["height"]].agg(lambda x: x.astype(int).sum())
Out[100]: 
      height
kind        
cat       18
dog       40

Преобразование

Метод transform возвращает объект, индексированный так же, как и группируемый. Функция преобразования должна:

  • Возвращать результат, имеющий размер, совпадающий с размером фрагмента группы, или допускающий трансляцию к размеру фрагмента группы (например, скаляр, grouped.transform(lambda x: x.iloc[-1])).

  • Осуществлять операции по столбцам в фрагменте группы. Преобразование применяется к первому фрагменту группы с использованием chunk.apply.

  • Не выполнять операции на месте с фрагментом группы. Фрагменты группы должны рассматриваться как неизменяемые, а изменения в фрагменте группы могут привести к непредсказуемым результатам. Например, при использовании fillna, inplace должно быть False (grouped.transform(lambda x: x.fillna(inplace=False))).

  • (Необязательно) выполнять операции над всем фрагментом группы. Если это поддерживается, используется быстрый путь, начиная со второго фрагмента.

Устарело начиная с версии 1.5.0: При использовании .transform на сгруппированной таблице DataFrame, и если функция преобразования возвращает DataFrame, в настоящее время pandas не выравнивает индекс результата с индексом входных данных. Это поведение устарело, и выравнивание будет выполнено в будущей версии pandas. Вы можете применить .to_numpy() к результату функции преобразования, чтобы избежать выравнивания.

Аналогично Агрегации с пользовательскими функциями, тип данных результата будет отражать тип данных функции преобразования. Если результаты разных групп имеют разные типы данных, то общий тип данных будет определён таким же способом, как и при построении DataFrame.

Предположим, мы хотели стандартизировать данные в каждой группе:

In [101]: index = pd.date_range("10/1/1999", periods=1100)

In [102]: ts = pd.Series(np.random.normal(0.5, 2, 1100), index)

In [103]: ts = ts.rolling(window=100, min_periods=100).mean().dropna()

In [104]: ts.head()
Out[104]: 
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 [105]: ts.tail()
Out[105]: 
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 [106]: transformed = ts.groupby(lambda x: x.year).transform(
   .....:     lambda x: (x - x.mean()) / x.std()
   .....: )
   .....: 

Мы ожидали, что результат будет иметь среднее значение 0 и стандартное отклонение 1 внутри каждой группы, что мы можем легко проверить:

# Original Data
In [107]: grouped = ts.groupby(lambda x: x.year)

In [108]: grouped.mean()
Out[108]: 
2000    0.442441
2001    0.526246
2002    0.459365
dtype: float64

In [109]: grouped.std()
Out[109]: 
2000    0.131752
2001    0.210945
2002    0.128753
dtype: float64

# Transformed Data
In [110]: grouped_trans = transformed.groupby(lambda x: x.year)

In [111]: grouped_trans.mean()
Out[111]: 
2000   -4.870756e-16
2001   -1.545187e-16
2002    4.136282e-16
dtype: float64

In [112]: grouped_trans.std()
Out[112]: 
2000    1.0
2001    1.0
2002    1.0
dtype: float64

Мы также можем визуально сравнить исходные и преобразованные наборы данных.

In [113]: compare = pd.DataFrame({"Original": ts, "Transformed": transformed})

In [114]: compare.plot()
Out[114]: <AxesSubplot: >
../_images/groupby_transform_plot.png

Функции преобразования, имеющие выходные данные меньшей размерности, транслируются для соответствия форме входного массива.

In [115]: ts.groupby(lambda x: x.year).transform(lambda x: x.max() - x.min())
Out[115]: 
2000-01-08    0.623893
2000-01-09    0.623893
2000-01-10    0.623893
2000-01-11    0.623893
2000-01-12    0.623893
                ...   
2002-09-30    0.558275
2002-10-01    0.558275
2002-10-02    0.558275
2002-10-03    0.558275
2002-10-04    0.558275
Freq: D, Length: 1001, dtype: float64

В качестве альтернативы, встроенные методы могут быть использованы для получения тех же результатов.

In [116]: max_ts = ts.groupby(lambda x: x.year).transform("max")

In [117]: min_ts = ts.groupby(lambda x: x.year).transform("min")

In [118]: max_ts - min_ts
Out[118]: 
2000-01-08    0.623893
2000-01-09    0.623893
2000-01-10    0.623893
2000-01-11    0.623893
2000-01-12    0.623893
                ...   
2002-09-30    0.558275
2002-10-01    0.558275
2002-10-02    0.558275
2002-10-03    0.558275
2002-10-04    0.558275
Freq: D, Length: 1001, dtype: float64

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

In [119]: data_df
Out[119]: 
            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
..        ...       ...       ...
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 [120]: countries = np.array(["US", "UK", "GR", "JP"])

In [121]: key = countries[np.random.randint(0, 4, 1000)]

In [122]: grouped = data_df.groupby(key)

# Non-NA count in each group
In [123]: grouped.count()
Out[123]: 
      A    B    C
GR  209  217  189
JP  240  255  217
UK  216  231  193
US  239  250  217

In [124]: transformed = grouped.transform(lambda x: x.fillna(x.mean()))

Мы можем проверить, что средние значения групп не изменились в преобразованных данных и что преобразованные данные не содержат значений NaN.

In [125]: grouped_trans = transformed.groupby(key)

In [126]: grouped.mean()  # original group means
Out[126]: 
           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 [127]: grouped_trans.mean()  # transformation did not change group means
Out[127]: 
           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 [128]: grouped.count()  # original has some missing data points
Out[128]: 
      A    B    C
GR  209  217  189
JP  240  255  217
UK  216  231  193
US  239  250  217

In [129]: grouped_trans.count()  # counts after transformation
Out[129]: 
      A    B    C
GR  228  228  228
JP  267  267  267
UK  247  247  247
US  258  258  258

In [130]: grouped_trans.size()  # Verify non-NA count equals group size
Out[130]: 
GR    228
JP    267
UK    247
US    258
dtype: int64

Примечание

Некоторые функции автоматически преобразуют входные данные при применении к объекту GroupBy, но возвращают объект той же формы, что и исходный. Передача as_index=False не повлияет на эти методы преобразования.

Например: fillna, ffill, bfill, shift..

In [131]: grouped.ffill()
Out[131]: 
            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
..        ...       ...       ...
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]

Операции окон и ресемплирования

Можно использовать resample(), expanding() и rolling() как методы для groupby.

В примере ниже метод rolling() будет применён к образцам столбца B, в зависимости от групп столбца A.

In [132]: df_re = pd.DataFrame({"A": [1] * 10 + [5] * 10, "B": np.arange(20)})

In [133]: df_re
Out[133]: 
    A   B
0   1   0
1   1   1
2   1   2
3   1   3
4   1   4
.. ..  ..
15  5  15
16  5  16
17  5  17
18  5  18
19  5  19

[20 rows x 2 columns]

In [134]: df_re.groupby("A").rolling(4).B.mean()
Out[134]: 
A    
1  0      NaN
   1      NaN
   2      NaN
   3      1.5
   4      2.5
         ... 
5  15    13.5
   16    14.5
   17    15.5
   18    16.5
   19    17.5
Name: B, Length: 20, dtype: float64

Метод expanding() будет накапливать заданную операцию (sum() в примере) для всех членов каждой конкретной группы.

In [135]: df_re.groupby("A").expanding().sum()
Out[135]: 
          B
A          
1 0     0.0
  1     1.0
  2     3.0
  3     6.0
  4    10.0
...     ...
5 15   75.0
  16   91.0
  17  108.0
  18  126.0
  19  145.0

[20 rows x 1 columns]

Предположим, вы хотите использовать метод resample() для получения ежедневной частоты в каждой группе вашей таблицы DataFrame и хотите заполнить пропущенные значения методом ffill().

In [136]: 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 [137]: df_re
Out[137]: 
            group  val
date                  
2016-01-03      1    5
2016-01-10      1    6
2016-01-17      2    7
2016-01-24      2    8

In [138]: df_re.groupby("group").resample("1D").ffill()
Out[138]: 
                  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
...                 ...  ...
2     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]

Фильтрация

Метод filter возвращает подмножество исходного объекта. Предположим, мы хотим взять только элементы, которые принадлежат группам с суммой группы, большей 2.

In [139]: sf = pd.Series([1, 1, 2, 3, 3, 3])

In [140]: sf.groupby(sf).filter(lambda x: x.sum() > 2)
Out[140]: 
3    3
4    3
5    3
dtype: int64

Аргумент filter должен быть функцией, которая, применённая ко всей группе, возвращает True или False.

Ещё одной полезной операцией является фильтрация элементов, которые принадлежат группам, содержащим лишь несколько членов.

In [141]: dff = pd.DataFrame({"A": np.arange(8), "B": list("aabbbbcc")})

In [142]: dff.groupby("B").filter(lambda x: len(x) > 2)
Out[142]: 
   A  B
2  2  b
3  3  b
4  4  b
5  5  b

В качестве альтернативы, вместо удаления проблемных групп, мы можем вернуть подобный индексированный объект, где группы, которые не прошли фильтр, заполнены значениями NaN.

In [143]: dff.groupby("B").filter(lambda x: len(x) > 2, dropna=False)
Out[143]: 
     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

Для таблиц DataFrame с несколькими столбцами фильтры должны явно указывать столбец в качестве критерия фильтрации.

In [144]: dff["C"] = np.arange(8)

In [145]: dff.groupby("B").filter(lambda x: len(x["C"]) > 2)
Out[145]: 
   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 [146]: dff.groupby("B").head(2)
Out[146]: 
   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 [147]: grouped = df.groupby("A")

In [148]: grouped.agg(lambda x: x.std())
Out[148]: 
            C         D
A                      
bar  0.181231  1.366330
foo  0.912265  0.884785

Но это довольно громоздко и может быть неряшливым, если вам нужно передать дополнительные аргументы. Используя небольшую хитрость метапрограммирования, GroupBy теперь имеет возможность «вызывать» вызовы методов к группам:

In [149]: grouped.std()
Out[149]: 
            C         D
A                      
bar  0.181231  1.366330
foo  0.912265  0.884785

На самом деле здесь генерируется обертка функции. При вызове она принимает любые переданные аргументы и вызывает функцию с любыми аргументами для каждой группы (в приведённом выше примере функция std). Результаты затем объединяются в стиле agg и transform (на самом деле используется apply для определения склеивания, документация приведена далее). Это позволяет выполнять некоторые операции довольно лаконично:

In [150]: tsdf = pd.DataFrame(
   .....:     np.random.randn(1000, 3),
   .....:     index=pd.date_range("1/1/2000", periods=1000),
   .....:     columns=["A", "B", "C"],
   .....: )
   .....: 

In [151]: tsdf.iloc[::2] = np.nan

In [152]: grouped = tsdf.groupby(lambda x: x.year)

In [153]: grouped.fillna(method="pad")
Out[153]: 
                   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
...              ...       ...       ...
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 для групп.

Методы nlargest и nsmallest работают с группами в стиле Series:

In [154]: s = pd.Series([9, 8, 7, 5, 19, 1, 4.2, 3.3])

In [155]: g = pd.Series(list("abababab"))

In [156]: gb = s.groupby(g)

In [157]: gb.nlargest(3)
Out[157]: 
a  4    19.0
   0     9.0
   2     7.0
b  1     8.0
   3     5.0
   7     3.3
dtype: float64

In [158]: gb.nsmallest(3)
Out[158]: 
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 может обрабатывать некоторые исключительные случаи.

Примечание

apply может действовать как функция сокращения, преобразования или фильтрации, в зависимости от того, что именно передано ей. Это может зависеть от переданной функции и от того, что именно вы группируете. Таким образом, сгруппированные столбцы могут быть включены в выходные данные, а также устанавливать индексы.

In [159]: df
Out[159]: 
     A      B         C         D
0  foo    one -0.575247  1.346061
1  bar    one  0.254161  1.511763
2  foo    two -1.143704  1.627081
3  bar  three  0.215897 -0.990582
4  foo    two  1.193555 -0.441652
5  bar    two -0.077118  1.211526
6  foo    one -0.408530  0.268520
7  foo  three -0.862495  0.024580

In [160]: grouped = df.groupby("A")

# could also just call .describe()
In [161]: grouped["C"].apply(lambda x: x.describe())
Out[161]: 
A         
bar  count    3.000000
     mean     0.130980
     std      0.181231
     min     -0.077118
     25%      0.069390
                ...   
foo  min     -1.143704
     25%     -0.862495
     50%     -0.575247
     75%     -0.408530
     max      1.193555
Name: C, Length: 16, dtype: float64

Размер возвращаемого результата также может изменяться:

In [162]: grouped = df.groupby('A')['C']

In [163]: def f(group):
   .....:     return pd.DataFrame({'original': group,
   .....:                          'demeaned': group - group.mean()})
   .....: 

apply на Series может работать со значениями, возвращаемыми функцией, которая сама является Series, и, возможно, увеличит результат до DataFrame:

In [164]: def f(x):
   .....:     return pd.Series([x, x ** 2], index=["x", "x^2"])
   .....: 

In [165]: s = pd.Series(np.random.rand(5))

In [166]: s
Out[166]: 
0    0.321438
1    0.493496
2    0.139505
3    0.910103
4    0.194158
dtype: float64

In [167]: s.apply(f)
Out[167]: 
          x       x^2
0  0.321438  0.103323
1  0.493496  0.243538
2  0.139505  0.019462
3  0.910103  0.828287
4  0.194158  0.037697

Управление размещением сгруппированных столбцов с помощью group_keys

Примечание

Если group_keys=True указан при вызове groupby(), функции, переданные в apply, которые возвращают объекты с аналогичными индексами, будут иметь ключи групп, добавленные к индексу результата. Предыдущие версии pandas добавляли ключи групп только тогда, когда результат от применённой функции имел другой индекс, чем вход. Если group_keys не указан, ключи групп не будут добавлены для выходных данных с одинаковыми индексами. В будущем это поведение изменится, чтобы всегда учитывать group_keys, по умолчанию True.

Изменено в версии 1.5.0.

Чтобы контролировать включение сгруппированных столбцов в индексы, можно использовать аргумент group_keys. Сравните

In [168]: df.groupby("A", group_keys=True).apply(lambda x: x)
Out[168]: 
         A      B         C         D
A                                    
bar 1  bar    one  0.254161  1.511763
    3  bar  three  0.215897 -0.990582
    5  bar    two -0.077118  1.211526
foo 0  foo    one -0.575247  1.346061
    2  foo    two -1.143704  1.627081
    4  foo    two  1.193555 -0.441652
    6  foo    one -0.408530  0.268520
    7  foo  three -0.862495  0.024580

с

In [169]: df.groupby("A", group_keys=False).apply(lambda x: x)
Out[169]: 
     A      B         C         D
0  foo    one -0.575247  1.346061
1  bar    one  0.254161  1.511763
2  foo    two -1.143704  1.627081
3  bar  three  0.215897 -0.990582
4  foo    two  1.193555 -0.441652
5  bar    two -0.077118  1.211526
6  foo    one -0.408530  0.268520
7  foo  three -0.862495  0.024580

Аналогично Агрегации с пользовательскими функциями, тип данных результата будет отражать тип данных функции apply. Если результаты разных групп имеют разные типы данных, то общий тип данных будет определен таким же образом, как при построении DataFrame.

Ускоренные вычисления Numba

В версии 1.1.

Если Numba установлена как необязательная зависимость, методы transform и aggregate поддерживают аргументы engine='numba' и engine_kwargs. См. улучшение производительности с помощью Numba для общего использования аргументов и рекомендаций по производительности.

Подпись функции должна начинаться с values, index точно, так как данные каждой группы будут переданы в values, а индекс группы — в index.

Предупреждение

При использовании engine='numba' внутренней «обратной связи» не будет. Данные группы и индекс группы будут переданы в качестве массивов NumPy в определенную пользователем функцию JIT, и никакие альтернативные попытки выполнения не будут предприниматься.

Другие полезные функции

Автоматическое исключение «вредных» столбцов

Рассмотрим пример DataFrame, который мы рассматривали:

In [170]: df
Out[170]: 
     A      B         C         D
0  foo    one -0.575247  1.346061
1  bar    one  0.254161  1.511763
2  foo    two -1.143704  1.627081
3  bar  three  0.215897 -0.990582
4  foo    two  1.193555 -0.441652
5  bar    two -0.077118  1.211526
6  foo    one -0.408530  0.268520
7  foo  three -0.862495  0.024580

Предположим, мы хотим вычислить стандартное отклонение, сгруппированное по столбцу A. Существует небольшая проблема, а именно, что нас не интересуют данные в столбце B. Мы называем это «вредным» столбцом. Вы можете избежать вредных столбцов, указав numeric_only=True:

In [171]: df.groupby("A").std(numeric_only=True)
Out[171]: 
            C         D
A                      
bar  0.181231  1.366330
foo  0.912265  0.884785

Обратите внимание, что df.groupby('A').colname.std(). более эффективен, чем df.groupby('A').std().colname, поэтому если результат функции агрегирования интересен только для одного столбца (здесь colname ), он может быть отфильтрован до применения функции агрегирования.

Примечание

Любой столбец типа object, даже если он содержит числовые значения, такие как объекты Decimal , считается «вредным» столбцом. Они автоматически исключаются из функций агрегирования в groupby.

Если вы хотите включить столбцы с десятичными знаками или объектами в агрегирование с другими типами данных, не являющимися вредными, вы должны сделать это явно.

Предупреждение

Автоматическое удаление вредных столбцов устарело и будет удалено в будущей версии pandas. Если будут включены столбцы, которые нельзя обработать, pandas вместо этого выдаст ошибку. Чтобы этого избежать, выберите столбцы, которые вы хотите обработать, или укажите numeric_only=True.

In [172]: from decimal import Decimal

In [173]: df_dec = pd.DataFrame(
   .....:     {
   .....:         "id": [1, 2, 1, 2],
   .....:         "int_column": [1, 2, 3, 4],
   .....:         "dec_column": [
   .....:             Decimal("0.50"),
   .....:             Decimal("0.15"),
   .....:             Decimal("0.25"),
   .....:             Decimal("0.40"),
   .....:         ],
   .....:     }
   .....: )
   .....: 

# Decimal columns can be sum'd explicitly by themselves...
In [174]: df_dec.groupby(["id"])[["dec_column"]].sum()
Out[174]: 
   dec_column
id           
1        0.75
2        0.55

# ...but cannot be combined with standard data types or they will be excluded
In [175]: df_dec.groupby(["id"])[["int_column", "dec_column"]].sum()
Out[175]: 
    int_column
id            
1            4
2            6

# Use .agg function to aggregate over standard and "nuisance" data types
# at the same time
In [176]: df_dec.groupby(["id"]).agg({"int_column": "sum", "dec_column": "sum"})
Out[176]: 
    int_column dec_column
id                       
1            4       0.75
2            6       0.55

Обработка (не)наблюдаемых категориальных значений

При использовании группировщика Categorical (в качестве единственного группировщика или в составе нескольких группировщиков) ключевое слово observed управляет тем, следует ли возвращать декартово произведение всех возможных значений группировщиков (observed=False) или только тех, которые наблюдаются (observed=True).

Показать все значения:

In [177]: pd.Series([1, 1, 1]).groupby(
   .....:     pd.Categorical(["a", "a", "a"], categories=["a", "b"]), observed=False
   .....: ).count()
   .....: 
Out[177]: 
a    3
b    0
dtype: int64

Показать только наблюдаемые значения:

In [178]: pd.Series([1, 1, 1]).groupby(
   .....:     pd.Categorical(["a", "a", "a"], categories=["a", "b"]), observed=True
   .....: ).count()
   .....: 
Out[178]: 
a    3
dtype: int64

Возвращаемый тип данных сгруппированных данных всегда включает все категории, которые были сгруппированы.

In [179]: s = (
   .....:     pd.Series([1, 1, 1])
   .....:     .groupby(pd.Categorical(["a", "a", "a"], categories=["a", "b"]), observed=False)
   .....:     .count()
   .....: )
   .....: 

In [180]: s.index.dtype
Out[180]: CategoricalDtype(categories=['a', 'b'], ordered=False)

Обработка групп NA и NaT

Если в ключе группировки есть значения NaN или NaT, они будут автоматически исключены. Другими словами, никогда не будет группы «NA» или «NaT». В более старых версиях pandas это было не так, но пользователи обычно игнорировали группу NA (и её поддержка представляла трудности реализации).

Группировка с упорядоченными факторами

Категориальные переменные, представленные экземплярами класса pandas Categorical , могут использоваться в качестве ключей группировки. В этом случае порядок уровней сохранится:

In [181]: data = pd.Series(np.random.randn(100))

In [182]: factor = pd.qcut(data, [0, 0.25, 0.5, 0.75, 1.0])

In [183]: data.groupby(factor).mean()
Out[183]: 
(-2.645, -0.523]   -1.362896
(-0.523, 0.0296]   -0.260266
(0.0296, 0.654]     0.361802
(0.654, 2.21]       1.073801
dtype: float64

Группировка с указанием группировщика

Возможно, вам потребуется указать немного больше данных для правильной группировки. Вы можете использовать pd.Grouper для этого локального управления.

In [184]: import datetime

In [185]: 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 [186]: df
Out[186]: 
  Branch Buyer  Quantity                Date
0      A  Carl         1 2013-01-01 13:00:00
1      A  Mark         3 2013-01-01 13:05:00
2      A  Carl         5 2013-10-01 20:00:00
3      A  Carl         1 2013-10-02 10:00:00
4      A   Joe         8 2013-10-01 20:00:00
5      A   Joe         1 2013-10-02 10:00:00
6      A   Joe         9 2013-12-02 12:00:00
7      B  Carl         3 2013-12-02 14:00:00

Группировка по конкретному столбцу с желаемой частотой. Это похоже на пересэмплирование.

In [187]: df.groupby([pd.Grouper(freq="1M", key="Date"), "Buyer"])[["Quantity"]].sum()
Out[187]: 
                  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 [188]: df = df.set_index("Date")

In [189]: df["Date"] = df.index + pd.offsets.MonthEnd(2)

In [190]: df.groupby([pd.Grouper(freq="6M", key="Date"), "Buyer"])[["Quantity"]].sum()
Out[190]: 
                  Quantity
Date       Buyer          
2013-02-28 Carl          1
           Mark          3
2014-02-28 Carl          9
           Joe          18

In [191]: df.groupby([pd.Grouper(freq="6M", level="Date"), "Buyer"])[["Quantity"]].sum()
Out[191]: 
                  Quantity
Date       Buyer          
2013-01-31 Carl          1
           Mark          3
2014-01-31 Carl          9
           Joe          18

Получение первых строк каждой группы

Как и для DataFrame или Series, вы можете вызвать head и tail для groupby:

In [192]: df = pd.DataFrame([[1, 2], [1, 4], [5, 6]], columns=["A", "B"])

In [193]: df
Out[193]: 
   A  B
0  1  2
1  1  4
2  5  6

In [194]: g = df.groupby("A")

In [195]: g.head(1)
Out[195]: 
   A  B
0  1  2
2  5  6

In [196]: g.tail(1)
Out[196]: 
   A  B
1  1  4
2  5  6

Это показывает первые или последние n строк каждой группы.

Получение n-й строки каждой группы

Чтобы выбрать из DataFrame или Series n-й элемент, используйте nth(). Это метод сокращения, и он вернёт одну строку (или не вернёт строки) на группу, если вы передадите целое число для n:

In [197]: df = pd.DataFrame([[1, np.nan], [1, 4], [5, 6]], columns=["A", "B"])

In [198]: g = df.groupby("A")

In [199]: g.nth(0)
Out[199]: 
     B
A     
1  NaN
5  6.0

In [200]: g.nth(-1)
Out[200]: 
     B
A     
1  4.0
5  6.0

In [201]: g.nth(1)
Out[201]: 
     B
A     
1  4.0

Если вы хотите выбрать n-й ненулевой элемент, используйте ключевое слово dropna. Для DataFrame это должно быть либо 'any', либо 'all', так же, как вы бы передавали значения для dropna:

# nth(0) is the same as g.first()
In [202]: g.nth(0, dropna="any")
Out[202]: 
     B
A     
1  4.0
5  6.0

In [203]: g.first()
Out[203]: 
     B
A     
1  4.0
5  6.0

# nth(-1) is the same as g.last()
In [204]: g.nth(-1, dropna="any")  # NaNs denote group exhausted when using dropna
Out[204]: 
     B
A     
1  4.0
5  6.0

In [205]: g.last()
Out[205]: 
     B
A     
1  4.0
5  6.0

In [206]: g.B.nth(0, dropna="all")
Out[206]: 
A
1    4.0
5    6.0
Name: B, dtype: float64

Как и в других методах, передача as_index=False, приведет к фильтрации, которая вернёт сгруппированную строку.

In [207]: df = pd.DataFrame([[1, np.nan], [1, 4], [5, 6]], columns=["A", "B"])

In [208]: g = df.groupby("A", as_index=False)

In [209]: g.nth(0)
Out[209]: 
   A    B
0  1  NaN
2  5  6.0

In [210]: g.nth(-1)
Out[210]: 
   A    B
1  1  4.0
2  5  6.0

Вы также можете выбрать несколько строк из каждой группы, указав несколько значений nth как список целых чисел.

In [211]: business_dates = pd.date_range(start="4/1/2014", end="6/30/2014", freq="B")

In [212]: df = pd.DataFrame(1, index=business_dates, columns=["a", "b"])

# get the first, 4th, and last date index for each month
In [213]: df.groupby([df.index.year, df.index.month]).nth([0, 3, -1])
Out[213]: 
        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

Перечисление элементов группы

Чтобы увидеть порядок, в котором каждая строка появляется в своей группе, используйте метод cumcount:

In [214]: dfg = pd.DataFrame(list("aaabba"), columns=["A"])

In [215]: dfg
Out[215]: 
   A
0  a
1  a
2  a
3  b
4  b
5  a

In [216]: dfg.groupby("A").cumcount()
Out[216]: 
0    0
1    1
2    2
3    0
4    1
5    3
dtype: int64

In [217]: dfg.groupby("A").cumcount(ascending=False)
Out[217]: 
0    3
1    2
2    1
3    1
4    0
5    0
dtype: int64

Перечисление групп

Чтобы увидеть порядок групп (в отличие от порядка строк внутри группы, заданного cumcount), вы можете использовать ngroup().

Обратите внимание, что числа, присваиваемые группам, соответствуют порядку, в котором группы будут видны при итерации по объекту groupby, а не порядку, в котором они впервые наблюдаются.

In [218]: dfg = pd.DataFrame(list("aaabba"), columns=["A"])

In [219]: dfg
Out[219]: 
   A
0  a
1  a
2  a
3  b
4  b
5  a

In [220]: dfg.groupby("A").ngroup()
Out[220]: 
0    0
1    0
2    0
3    1
4    1
5    0
dtype: int64

In [221]: dfg.groupby("A").ngroup(ascending=False)
Out[221]: 
0    1
1    1
2    1
3    0
4    0
5    1
dtype: int64

Построение графиков

GroupBy также работает с некоторыми методами построения графиков. Например, предположим, что мы подозреваем, что некоторые функции в DataFrame могут отличаться по группам, в этом случае значения в столбце 1, где группа «B», в среднем на 3 больше.

In [222]: np.random.seed(1234)

In [223]: df = pd.DataFrame(np.random.randn(50, 2))

In [224]: df["g"] = np.random.choice(["A", "B"], size=50)

In [225]: df.loc[df["g"] == "B", 1] += 3

Мы можем легко визуализировать это с помощью boxplot:

In [226]: df.groupby("g").boxplot()
Out[226]: 
A         AxesSubplot(0.1,0.15;0.363636x0.75)
B    AxesSubplot(0.536364,0.15;0.363636x0.75)
dtype: object
../_images/groupby_boxplot.png

Результат вызова boxplot — словарь, ключами которого являются значения нашего столбца группировки g («A» и «B»). Значения результирующего словаря можно контролировать с помощью ключевого слова return_type метода boxplot . Подробнее см. в документации по визуализации.

Предупреждение

По историческим причинам, df.groupby("g").boxplot() не эквивалентно df.boxplot(by="g") . См. здесь для объяснения.

Цепочки вызовов функций

Аналогично функциональности, предоставляемой DataFrame и Series , функции, которые принимают объекты GroupBy , могут быть объединены с помощью метода pipe , что позволит создать более чистый и читаемый синтаксис. Более подробно о .pipe см. здесь.

Сочетание .groupby и .pipe часто бывает полезно, когда вам нужно повторно использовать объекты GroupBy.

Например, представьте DataFrame со столбцами для магазинов, продуктов, дохода и количества проданных товаров. Мы хотим выполнить групповое вычисление цен (т. е. доход/количество) на магазин и на продукт. Мы могли бы сделать это в многошаговой операции, но выражение в виде конвейера может сделать код более читаемым. Сначала зададим данные:

In [227]: n = 1000

In [228]: df = pd.DataFrame(
   .....:     {
   .....:         "Store": np.random.choice(["Store_1", "Store_2"], n),
   .....:         "Product": np.random.choice(["Product_1", "Product_2"], n),
   .....:         "Revenue": (np.random.random(n) * 50 + 10).round(2),
   .....:         "Quantity": np.random.randint(1, 10, size=n),
   .....:     }
   .....: )
   .....: 

In [229]: df.head(2)
Out[229]: 
     Store    Product  Revenue  Quantity
0  Store_2  Product_1    26.12         1
1  Store_2  Product_1    28.86         1

Теперь, чтобы найти цены на магазин/продукт, мы можем просто сделать:

In [230]: (
   .....:     df.groupby(["Store", "Product"])
   .....:     .pipe(lambda grp: grp.Revenue.sum() / grp.Quantity.sum())
   .....:     .unstack()
   .....:     .round(2)
   .....: )
   .....: 
Out[230]: 
Product  Product_1  Product_2
Store                        
Store_1       6.82       7.05
Store_2       6.30       6.64

Конвейер также может быть выразителен, когда вы хотите передать сгруппированный объект в произвольную функцию, например:

In [231]: def mean(groupby):
   .....:     return groupby.mean()
   .....: 

In [232]: df.groupby(["Store", "Product"]).pipe(mean)
Out[232]: 
                     Revenue  Quantity
Store   Product                       
Store_1 Product_1  34.622727  5.075758
        Product_2  35.482815  5.029630
Store_2 Product_1  32.972837  5.237589
        Product_2  34.684360  5.224000

где mean принимает объект GroupBy и находит среднее значение столбцов Доход и Количество соответственно для каждой комбинации Магазин-Продукт. Функция mean может быть любой функцией, принимающей объект GroupBy; .pipe передаст объект GroupBy в качестве параметра в указанную вами функцию.

Примеры

Перегруппировка по фактору

Перегруппируйте столбцы DataFrame по их сумме и просуммируйте агрегированные значения.

In [233]: df = pd.DataFrame({"a": [1, 0, 0], "b": [0, 1, 0], "c": [1, 0, 0], "d": [2, 3, 4]})

In [234]: df
Out[234]: 
   a  b  c  d
0  1  0  1  2
1  0  1  0  3
2  0  0  0  4

In [235]: df.groupby(df.sum(), axis=1).sum()
Out[235]: 
   1  9
0  2  2
1  1  3
2  0  4

Факторизация нескольких столбцов

Используя ngroup(), мы можем извлечь информацию о группах аналогичным способом, как и в factorize() (как описано далее в API переструктурирования), но это применяется естественным образом к нескольким столбцам смешанного типа и различным источникам. Это может быть полезно как промежуточный категориальный шаг в обработке, когда взаимоотношения между строками групп важнее их содержимого, или как входной параметр алгоритма, который принимает только целочисленное кодирование. (Дополнительную информацию о поддержке pandas для полных категориальных данных см. в Введении в категориальные данные и документации API.)

In [236]: dfg = pd.DataFrame({"A": [1, 1, 2, 3, 2], "B": list("aaaba")})

In [237]: dfg
Out[237]: 
   A  B
0  1  a
1  1  a
2  2  a
3  3  b
4  2  a

In [238]: dfg.groupby(["A", "B"]).ngroup()
Out[238]: 
0    0
1    0
2    1
3    2
4    1
dtype: int64

In [239]: dfg.groupby(["A", [0, 0, 0, 1, 1]]).ngroup()
Out[239]: 
0    0
1    0
2    1
3    3
4    2
dtype: int64

Группировка по индексатору для «перевыборки» данных

Перевыборка создает новые гипотетические выборки (перевыборки) из уже существующих наблюдаемых данных или из модели, которая генерирует данные. Эти новые выборки похожи на предварительно существующие выборки.

Для того, чтобы перевыборка работала с индексами, которые не являются datetimelike, можно использовать следующий метод.

В следующих примерах df.index // 5 возвращает двоичный массив, который используется для определения того, что выбирается для операции groupby.

Примечание

В примере ниже показано, как можно выполнить выборочное усреднение путем консолидации выборок в меньшее количество выборок. Здесь, используя df.index // 5, мы агрегируем выборки в ячейки. Применяя функцию std(), мы агрегируем информацию, содержащуюся во многих выборках, в небольшой подмножество значений, которые представляют собой их стандартное отклонение, тем самым уменьшая количество выборок.

In [240]: df = pd.DataFrame(np.random.randn(10, 2))

In [241]: df
Out[241]: 
          0         1
0 -0.793893  0.321153
1  0.342250  1.618906
2 -0.975807  1.918201
3 -0.810847 -1.405919
4 -1.977759  0.461659
5  0.730057 -1.316938
6 -0.751328  0.528290
7 -0.257759 -1.081009
8  0.505895 -1.701948
9 -1.006349  0.020208

In [242]: df.index // 5
Out[242]: Int64Index([0, 0, 0, 0, 0, 1, 1, 1, 1, 1], dtype='int64')

In [243]: df.groupby(df.index // 5).std()
Out[243]: 
          0         1
0  0.823647  1.312912
1  0.760109  0.942941

Возвращение Series для распространения имен

Группировка столбцов DataFrame, вычисление набора метрик и возвращение именованной Series. Имя Series используется в качестве имени для индекса столбца. Это особенно полезно в сочетании с операциями переструктурирования, такими как стэкинг, в котором имя индекса столбца будет использоваться в качестве имени вставленного столбца:

In [244]: 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 [245]: def compute_metrics(x):
   .....:     result = {"b_sum": x["b"].sum(), "c_mean": x["c"].mean()}
   .....:     return pd.Series(result, name="metrics")
   .....: 

In [246]: result = df.groupby("a").apply(compute_metrics)

In [247]: result
Out[247]: 
metrics  b_sum  c_mean
a                     
0          2.0     0.5
1          2.0     0.5
2          2.0     0.5

In [248]: result.stack()
Out[248]: 
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–2022, 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/1.5.0/user_guide/groupby.html

Spec-Zone.ru

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