Сообщить об ошибке.

Метод .groupby() объектов DataFrame/Series в pandas

Группировка данных DataFrame/Series в pandas

Синтаксис:

groupby_df = DataFrame.groupby(by=None, axis=no_default, level=None, 
                               as_index=True, sort=True, group_keys=True, 
                               observed=no_default, dropna=True)

groupby_s = Series.groupby(by=None, axis=0, level=None, as_index=True, 
                           sort=True, group_keys=True, 
                           observed=.no_default, dropna=True)

Параметры:

  • by=None - используется для определения групп. Если by является функцией, то она вызывается для каждого значения индексной метки объекта. Если передан словарь dict или Series, то ключи будут сопоставляться с индексными метками, а для определения групп будет использоваться ЗНАЧЕНИЯ словаря или Series (сначала выравниваются значения Series). Если передан список или numpy.ndarray длины, равной выбранной оси axis, то для определения групп используются значения как есть. Индексная метка или их список могут быть переданы в группу по столбцам. Обратите внимание, что кортеж интерпретируется как (одиночный) ключ.
  • axis=no_default - устарело с версии 2.1.0. В будущей версии будет вести себя как axis=0. Для axis=1 необходимо использовать DataFrame.T.groupby(...). В Series.groupby() НЕ используется и равна 0.
  • level=None - если ось axis является MultiIndex (иерархической), группируйте ее по определенному уровню или уровням. Не указывайте одновременно аргументы by и level.
  • as_index=True - возвращает объект с метками групп в качестве индекса. Актуально только для входных данных DataFrame. Значение as_index=False - это фактически сгруппированный вывод в стиле SQL. Этот аргумент не влияет на фильтрацию, а также на преобразования.
  • sort=True - сортировка ключей групп. Чтобы повысить производительность, нужно отключать эту функцию. Обратите внимание, что она не влияет на порядок наблюдений внутри каждой группы. Метод .groupby() сохраняет порядок строк в каждой группе. Если задано значение False, то группы будут отображаться в том же порядке, что и в исходном DataFrame. Этот аргумент не влияет на фильтрацию, а также на преобразования.
  • group_keys=True - при вызове метода DataFrame.apply() при переданном аргументе by выдает результат с аналогичным индексом (т. е. преобразование). Чтобы идентифицировать части, нужно добавить групповые ключи в index. По умолчанию, если метки индекса (и столбца) результата совпадают с входными данными, то ключи групп не включаются, и в противном случае включаются.
  • observed=no_default - устарело, начиная с версии 2.1.0. В будущей версии pandas значение по умолчанию изменится на True. (применялся только в том случае, если какие-либо из групповых элементов являлся категориальным)
  • dropna=True - если True и если ключи группы содержат значения NA, то значения NA вместе со строкой/столбцом будут удалены. Если False, то значения NA также будут рассматриваться как ключ в группах.

Возвращаемое значение:

  • объект DataFrameGroupBy, содержащий информацию о группах.

Описание:

Метод DataFrame.groupby() модуля pandas группирует DataFrame с помощью mapper или по последовательности столбцов.

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

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

>>> import pandas as pd
>>> import numpy as np

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),
    }
)

>>> df
#      A      B         C         D
# 0  foo    one -1.468449  0.181378
# 1  bar    one  2.275577  1.118370
# 2  foo    two -1.207740 -0.049727
# 3  bar  three  0.004842 -0.867390
# 4  foo    two  0.306493 -1.486156
# 5  bar    two -0.542448 -0.034376
# 6  foo    one -1.764169 -0.780487
# 7  foo  three  1.869280 -0.337706

>>> df.groupby("A").groups
# {'bar': [1, 3, 5], 'foo': [0, 2, 4, 6, 7]}
>>> df.T.groupby(get_letter_type).groups
# {'consonant': ['B', 'C', 'D'], 'vowel': ['A']}

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

>>> grouped = df.groupby(["A", "B"])
>>> grouped.groups
# {
#     ('bar', 'one'): [1], 
#     ('bar', 'three'): [3], 
#     ('bar', 'two'): [5], 
#     ('foo', 'one'): [0, 6], 
#     ('foo', 'three'): [7], 
#     ('foo', 'two'): [2, 4]
# }
>>> len(grouped)
# 6

Для каждого объекта DataFrameGroupBy создается свой набор атрибутов и методов (в зависимости от анализируемых данных), который можно посмотреть в консоле Python3, поставив после объекта точку и нажав 2 раза <TAB>:

>>> grouped.<TAB><TAB>
grouped.A              grouped.boxplot(       grouped.describe( ...
grouped.B              grouped.corr(          grouped.diff( ...
grouped.C              grouped.corrwith(      grouped.dtypes ...
grouped.D              grouped.count()        grouped.ewm( ...
grouped.agg(           grouped.cov(           grouped.expanding( ...
grouped.aggregate(     grouped.cumcount(      grouped.ffill( ...
grouped.all(           grouped.cummax(        grouped.fillna( ...
grouped.any(           grouped.cummin(        grouped.filter( ...
grouped.apply(         grouped.cumprod(       grouped.first( ...
grouped.bfill(         grouped.cumsum(        grouped.get_group( ...

Все атрибуты и методы объекта DataFrameGroupBy рассматриваться не будут, в виду их большого количества. Их описание можно посмотреть в официальной документации к pandas или функцией help(). Ниже будут рассмотрены основные операции и перечислены агрегирующие методы/функции.

Содержание:


Разделение объекта на группы

Абстрактное определение группировки заключается в обеспечении сопоставления меток с именами групп. Чтобы создать объект DataFrameGroupBy можно сделать следующее:

speeds = 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"),
)

# группируем по столбцу `class`
>>> g = speeds.groupby("class")
>>> g
# <pandas.core.groupby.generic.DataFrameGroupBy object at 0x7fd573ad72e0>
# названия групп - ключи словаря
>>> g.groups
# {'bird': ['falcon', 'parrot'], 'mammal': ['lion', 'monkey', 'leopard']}

>>> g.get_group('bird')
#        class           order  max_speed
# falcon  bird   Falconiformes      389.0
# parrot  bird  Psittaciformes       24.0

# можно группировать по нескольким столбцам
>>> speeds.groupby(["class", "order"])
# <pandas.core.groupby.generic.DataFrameGroupBy object at 0x7fd573ad7df0>

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

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

  • Функция Python, вызываемая для каждой из меток индекса.
  • Список или массив NumPy той же длины, что и индекс.
  • Словарь dict или Series, сопоставляющие индексные метки и название группы.
  • Для объектов DataFrame - строка, указывающая имя столбца или имя уровня индекса, используемое для группировки.
  • Список любой из вышеперечисленных объектов.

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

В совокупности, группирующие объекты называются ключами. Используем созданный ранее DataFrame. Если есть MultiIndex для столбцов A и B, то можно группировать по всем столбцам, кроме того, который указали:

# установим столбцы в качестве `MultiIndex`
>>> df2 = df.set_index(["A", "B"])
>>> df2
#                   C         D
# A   B                        
# foo one   -1.468449  0.181378
# bar one    2.275577  1.118370
# foo two   -1.207740 -0.049727
# bar three  0.004842 -0.867390
# foo two    0.306493 -1.486156
# bar two   -0.542448 -0.034376
# foo one   -1.764169 -0.780487
#     three  1.869280 -0.337706

# index.difference() возвращает новый индекс с элементами, 
# отсутствующими в столбце `B`, т.е. разность множеств двух объектов Index.
>>> grouped = df2.groupby(level=df2.index.names.difference(["B"]))
>>> grouped.sum()
#             C         D
# A                      
# bar  1.737971  0.216603
# foo -2.264585 -2.472699

Приведенный выше GroupBy делит DataFrame по индексным меткам (строкам). Чтобы разбить по столбцам, сначала выполним транспонирование (атрибут df.T):

# определим функцию сопоставления 
# индексных меток
def get_letter_type(letter):
    if letter.lower() in 'aeiou':
        return 'vowel'
    else:
        return 'consonant'

>>> grouped = df.T.groupby(get_letter_type)
>>> grouped.first()
#              0    1    2      3    4    5    6      7
# consonant  one  one  two  three  two  two  one  three
# vowel      foo  bar  foo    bar  foo  bar  foo    foo
>>> grouped.size()
# consonant    3
# vowel        1
# dtype: int64
>>> grouped.groups
# {'consonant': ['B', 'C', 'D'], 'vowel': ['A']}

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

# допустим есть индекс
>>> lst = [1, 2, 3, 1, 2, 3]
# серия с индексом `lst`
>>> s = pd.Series([1, 2, 3, 10, 20, 30], lst)
>>> s
# 1     1
# 2     2
# 3     3
# 1    10
# 2    20
# 3    30
# dtype: int64

# группируем по индексу `level=0`
>>> grouped = s.groupby(level=0)
>>> grouped.first()
# 1    1
# 2    2
# 3    3
# dtype: int64
>>> grouped.sum()
# 1    11
# 2    22
# 3    33
# dtype: int64

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

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

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

По умолчанию ключи групп сортируются во время операции groupby. Тем не менее, можно передать sort=False для потенциального ускорения операции группировки. При sort=False порядок между группами-ключами соответствует порядку появления ключей в исходном DataFrame:

>>> df2 = pd.DataFrame({"X": ["B", "B", "A", "A"], "Y": [1, 2, 3, 4]})
>>> df2.groupby(["X"]).sum()
#    Y
# X   
# A  7
# B  3
>>> df2.groupby(["X"], sort=False).sum()
#    Y
# X   
# B  3
# A  7

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

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

# смотрим на индексные метки
>>> df3.groupby(["X"]).get_group("A")
#    X  Y
# 0  A  1
# 2  A  3
>>> df3.groupby(["X"]).get_group("B")
#    X  Y
# 1  B  4
# 3  B  2

Группировка и пустые значения NA

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

>>> df_list = [[1, 2, 3], [1, None, 4], [2, 1, 3], [1, 2, 2]]
>>> df_dropna = pd.DataFrame(df_list, columns=["a", "b", "c"])
>>> df_dropna
   a    b  c
0  1  2.0  3
1  1  NaN  4
2  2  1.0  3
3  1  2.0  2

По умолчанию dropna=True, что исключает NaN в ключах.

>>> df_dropna.groupby(by=["b"], dropna=True).sum()
#      a  c
# b        
# 1.0  2  3
# 2.0  2  5

Чтобы разрешить использование NaN в ключах, установим для dropna=False:

>>> df_dropna.groupby(by=["b"], dropna=False).sum()
#      a  c
# b        
# 1.0  2  3
# 2.0  2  5
# NaN  1  4

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

Иерархическое/многоуровневое индексирование очень интересно, т.к. оно позволяет производить сложный анализ и манипулирование данными. По сути, многоуровневое индексирование позволяет хранить и манипулировать данными с произвольным количеством измерений в структурах данных меньшей размерности, таких как Series (1d) и DataFrame (2d).

Данные с иерархической индексацией вполне естественно группировать по одному из уровней иерархии. Создадим Series с MultiIndex, удобный для понимания о чем речь.

arrays = [
    ["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
    ["one", "two", "one", "two", "one", "two", "one", "two"],
]

>>> index = pd.MultiIndex.from_arrays(arrays, names=["first", "second"])
>>> s = pd.Series(np.random.randn(8), index=index)
>>> s
# first  second
# bar    one      -0.511733
#        two      -0.357432
# baz    one      -1.258535
#        two      -1.442737
# foo    one      -2.372564
#        two       0.655380
# qux    one       0.941400
#        two      -2.647630
# dtype: float64

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

>>> grouped = s.groupby(level=0)
>>> grouped.sum()
# first
# bar   -0.869164
# baz   -2.701272
# foo   -1.717184
# qux   -1.706230
# dtype: float64

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

>>> s.groupby(level="second").sum()
# second
# one   -3.201431
# two   -3.792419
# dtype: float64

Поддерживается группировка с несколькими уровнями. Создадим Series с MultiIndex, удобный для понимания о чем речь.

arrays = [
    ["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
    ["doo", "doo", "bee", "bee", "bop", "bop", "bop", "bop"],
    ["one", "two", "one", "two", "one", "two", "one", "two"],
]

>>> index = pd.MultiIndex.from_arrays(arrays, names=["first", "second", "third"])
>>> s = pd.Series(np.random.randn(8), index=index)
>>> s
# first  second  third
# bar    doo     one     -0.771680
#                two     -0.660627
# baz    bee     one     -0.022898
#                two     -0.404116
# foo    bop     one      0.780148
#                two     -1.699268
# qux    bop     one      0.742693
#                two     -2.246963
# dtype: float64

>>> s.groupby(level=["first", "second"]).sum()
# first  second
# bar    doo      -1.432306
# baz    bee      -0.427015
# foo    bop      -0.919120
# qux    bop      -1.504270
# dtype: float64

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

>>> s.groupby(["first", "second"]).sum()
# first  second
# bar    doo      -1.432306
# baz    bee      -0.427015
# foo    bop      -0.919120
# qux    bop      -1.504270
# dtype: float64

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

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

Создадим DataFrame с MultiIndex, удобный для понимания темы:

arrays = [
    ["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
    ["one", "two", "one", "two", "one", "two", "one", "two"],
]
>>> index = pd.MultiIndex.from_arrays(arrays, names=["first", "second"])
>>> df = pd.DataFrame({"A": [1, 1, 1, 1, 2, 2, 3, 3], "B": np.arange(8)}, index=index)
>>> df
#               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

Группируем df по второму уровню индекса second и столбцу A.

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

Уровни индекса также могут быть указаны по названию

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

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

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

Операции с объектом DataFrameGroupBy

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

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

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),
    }
)

>>> grouped = df.groupby(["A"])
>>> grouped_C = grouped["C"]
>>> grouped_D = grouped["D"]

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

>>> df["C"].groupby(df["A"])

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

Перебор по группам DataFrameGroupBy

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

>>> grouped = df.groupby('A')

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

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

for name, group in df.groupby(['A', 'B']):
    print(name)
    print(group)

# ('bar', 'one')
#      A    B         C         D
# 1  bar  one -0.650714 -0.992193
# ('bar', 'three')
#      A      B         C         D
# 3  bar  three  0.467266  1.232117
# ('bar', 'two')
#      A    B        C         D
# 5  bar  two  0.02017 -0.593099
# ('foo', 'one')
#      A    B         C         D
# 0  foo  one -1.416678  1.366671
# 6  foo  one  0.440195 -0.141176
# ('foo', 'three')
#      A      B         C         D
# 7  foo  three -0.040652 -1.705956
# ('foo', 'two')
#      A    B         C         D
# 2  foo  two  0.410918  1.245727
# 4  foo  two  0.548223 -0.065070

Выбор группы DataFrameGroupBy

Одну группу можно выбрать с помощью метода DataFrameGroupBy.get_group():

>>> grouped.get_group("bar")
#      A      B         C         D
# 1  bar    one -0.650714 -0.992193
# 3  bar  three  0.467266  1.232117
# 5  bar    two  0.020170 -0.593099

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

>>> df.groupby(["A", "B"]).get_group(("bar", "one"))
#      A    B         C         D
# 1  bar  one -0.650714 -0.992193

Агрегирование в операциях GroupBy

Агрегирование - это операция GroupBy, которая уменьшает размерность объекта группировки. Результатом агрегирования является или, по крайней мере, обрабатывается скалярное значение для каждого столбца в группе. Например, получение суммы каждого столбца в группе значений.

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],
    }
)

>>> animals
#   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

>>> animals.groupby("kind").sum()
#       height  weight
# kind                
# cat     18.6    17.8
# dog     40.0   205.5

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

>>> animals.groupby("kind", as_index=False).sum()
#   kind  height  weight
# 0  cat    18.6    17.8
# 1  dog    40.0   205.5

Встроенные методы агрегирования

Многие распространенные функции агрегирования встроены в объект GroupBy в качестве методов. В перечисленных ниже методах те, у которых есть *, не имеют оптимизированной для Cython реализации.

Метод `GroupBy`Описание
any()являются ли какие-либо значения в группах истинными
all()являются ли истинными все значения в группах
count()количество значений, отличных от NA, в группах
cov() *ковариация групп
first()первое встречающееся значение в каждой группе
idxmax() *индекс максимального значения в каждой группе
idxmin() *индекс минимального значения в каждой группе
last()последнее встречающееся значение в каждой группе
max()максимальное значение в каждой группе
mean()среднее значение для каждой группы
median()медиана каждой группы
min()минимальное значение в каждой группе
nunique()количество уникальных значений в каждой группе
prod()произведение значений в каждой группе
quantile()заданный квантиль значений в каждой группе
sem()стандартная ошибка среднего значения значений в каждой группе
size()количество значений в каждой группе
skew() *перекос значений в каждой группе
std()стандартное отклонение значений в каждой группе
sum()сумма значений в каждой группе
var()разница значений в каждой группе

Несколько примеров:

>>> df.groupby("A")[["C", "D"]].max()
#             C         D
# A                      
# bar  0.467266  1.232117
# foo  0.548223  1.366671

>>> df.groupby(["A", "B"]).mean()
#                   C         D
# A   B                        
# bar one   -0.650714 -0.992193
#     three  0.467266  1.232117
#     two    0.020170 -0.593099
# foo one   -0.488241  0.612748
#     three -0.040652 -1.705956
#     two    0.479571  0.590328

Другим простым примером агрегирования является вычисление размера каждой группы. Он включен в GroupBy в качестве метода .size(). Возвращает ряд, индексом которого являются имена групп, а значениями - размеры каждой группы.

>>> grouped = df.groupby(["A", "B"])
>>> grouped.size()
# A    B    
# bar  one      1
#      three    1
#      two      1
# foo  one      2
#      three    1
#      two      2
# dtype: int64

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

>>> grouped.describe()
#               C                      ...         D                    
#           count      mean       std  ...       50%       75%       max
# A   B                                ...                              
# bar one     1.0 -0.650714       NaN  ... -0.992193 -0.992193 -0.992193
#     three   1.0  0.467266       NaN  ...  1.232117  1.232117  1.232117
#     two     1.0  0.020170       NaN  ... -0.593099 -0.593099 -0.593099
# foo one     2.0 -0.488241  1.313007  ...  0.612748  0.989710  1.366671
#     three   1.0 -0.040652       NaN  ... -1.705956 -1.705956 -1.705956
#     two     2.0  0.479571  0.097090  ...  0.590328  0.918027  1.245727
# 
# [6 rows x 16 columns]

Другим примером агрегирования является вычисление количества уникальных значений каждой группы DataFrameGroupBy.nunique(). Это похоже на метод DataFrame.value_counts(), за исключением того, что она подсчитывает только количество уникальных значений.

>>> ll = [['foo', 1], ['foo', 2], ['foo', 2], ['bar', 1], ['bar', 1]]
>>> df4 = pd.DataFrame(ll, columns=["A", "B"])
>>> df4
#      A  B
# 0  foo  1
# 1  foo  2
# 2  foo  2
# 3  bar  1
# 4  bar  1

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

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

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

Метод DataFrameGroupBy.aggregate()

Метод DataFrameGroupBy.aggregate() может принимать множество различных типов входных данных. Начнем с примеров использования строковых псевдонимов для различных методов GroupBy

Любой агрегирующий метод, реализуемый в pandas, может быть передан в виде строки методу .aggregate() или его псевдониму .agg(). Пользователям рекомендуется использовать сокращенный метод DataFrameGroupBy.agg().

>>> grouped = df.groupby("A")
>>> grouped[["C", "D"]].aggregate("sum")
#             C         D
# A                      
bar -0.163277 -0.353176
foo -0.057993  0.700196
>>> grouped = df.groupby(["A", "B"])
>>> grouped.agg("sum")
#                   C         D
# A   B                        
# bar one   -0.650714 -0.992193
#     three  0.467266  1.232117
#     two    0.020170 -0.593099
# foo one   -0.976483  1.225496
#     three -0.040652 -1.705956
#     two    0.959141  1.180656

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

>>> grouped = df.groupby(["A", "B"], as_index=False)
>>> grouped.agg("sum")
#      A      B         C         D
# 0  bar    one -0.650714 -0.992193
# 1  bar  three  0.467266  1.232117
# 2  bar    two  0.020170 -0.593099
# 3  foo    one -0.976483  1.225496
# 4  foo  three -0.040652 -1.705956
# 5  foo    two  0.959141  1.180656

>>> df.groupby("A", as_index=False)[["C", "D"]].agg("sum")
#      A         C         D
# 0  bar -0.163277 -0.353176
# 1  foo -0.057993  0.700196

Обратите внимание, что для достижения того же результата можно использовать метод DataFrame.reset_index(), т.к. имена столбцов хранятся в результирующем MultiIndex, хотя это приведет к созданию дополнительной копии...

>>> df.groupby(["A", "B"]).agg("sum").reset_index()
#      A      B         C         D
# 0  bar    one -0.650714 -0.992193
# 1  bar  three  0.467266  1.232117
# 2  bar    two  0.020170 -0.593099
# 3  foo    one -0.976483  1.225496
# 4  foo  three -0.040652 -1.705956
# 5  foo    two  0.959141  1.180656

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

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

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

Заметка. Статистическая обработка с помощью определяемой пользователем функции часто менее эффективна, чем использование встроенных методов в GroupBy. Лучше разбить сложную операцию на цепочку операций, использующих встроенные методы.

>>> animals
#   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

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

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

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

Применение нескольких групповых функций.

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

>>> grouped = df.groupby("A")
>>> grouped["C"].agg(["sum", "mean", "std"])
#           sum      mean       std
# A                                
# bar -0.163277 -0.054426  0.562711
# foo -0.057993 -0.011599  0.817179

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

>>> grouped[["C", "D"]].agg(["sum", "mean", "std"])
#             C                             D                    
#           sum      mean       std       sum      mean       std
# A                                                              
# bar -0.163277 -0.054426  0.562711 -0.353176 -0.117725  1.185906
# foo -0.057993 -0.011599  0.817179  0.700196  0.140039  1.250602

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

(
    grouped["C"]
    .agg(["sum", "mean", "std"])
    .rename(columns={"sum": "foo", "mean": "bar", "std": "baz"})
)

#           foo       bar       baz
# A                                
# bar -0.163277 -0.054426  0.562711
# foo -0.057993 -0.011599  0.817179

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

(
    grouped[["C", "D"]].agg(["sum", "mean", "std"]).rename(
        columns={"sum": "foo", "mean": "bar", "std": "baz"}
    )
)
#             C                             D                    
#           foo       bar       baz       foo       bar       baz
# A                                                              
# bar -0.163277 -0.054426  0.562711 -0.353176 -0.117725  1.185906
# foo -0.057993 -0.011599  0.817179  0.700196  0.140039  1.250602

Как правило, имена выходных столбцов должны быть уникальными, но pandas позволит применить одну и ту же функцию (или две функции с одинаковым именем) к одному и тому же столбцу.

>>> grouped["C"].agg(["sum", "sum"])
#           sum       sum
# A                      
# bar -0.163277 -0.163277
# foo -0.057993 -0.057993

Pandas также позволяет предоставлять несколько lambda-выражений. В этом случае pandas будет добавлять _i к каждой последующей lambda-функции.

>>> grouped["C"].agg([lambda x: x.max() - x.min(), lambda x: x.median() - x.mean()])
#      <lambda_0>  <lambda_1>
# A                          
# bar    1.117980    0.074596
# foo    1.964901    0.422516

Именованная агрегация pandas.NamedAgg(column, aggfunc)

Для поддержки агрегирования для конкретных столбцов с контролем над именами выходных столбцов pandas принимает специальный синтаксис в DataFrameGroupBy.agg() и SeriesGroupBy.agg(), известный как "именованная агрегация"

Библиотека предоставляет класс pandas.NamedAgg(column, aggfunc), где

  • column - это имена выходных столбцов
  • aggfunc - функция, применяемая к указанному столбцу. Если строка, то это должна быть встроенная функция pandas.
>>> animals
#   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

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="mean"),
)
#       min_height  max_height  average_weight
# kind                                        
# cat          9.1         9.5            8.90
# dog          6.0        34.0          102.75

Класс pandas.NamedAgg представляет собой именованный кортеж. Простые кортежи также разрешены.

animals.groupby("kind").agg(
    min_height=("height", "min"),
    max_height=("height", "max"),
    average_weight=("weight", "mean"),
)
#       min_height  max_height  average_weight
# kind                                        
# cat          9.1         9.5            8.90
# dog          6.0        34.0          102.75

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

>>> d = {"total weight": pd.NamedAgg(column="weight", aggfunc="sum")}
>>> animals.groupby("kind").agg(**d)
#       total weight
# kind              
# cat           17.8
# dog          205.5

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

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

animals.groupby("kind").height.agg(
    min_height="min",
    max_height="max",
)
#       min_height  max_height
# kind                        
# cat          9.1         9.5
# dog          6.0        34.0

Использование словаря для передачи групповых функций

Передавая словарь dict в метод DataFrameGroupBy.agg(), можно применить различное агрегирование к столбцам DataFrame:

>>> d = {"C": "sum", "D": lambda x: np.std(x, ddof=1)}
>>> grouped.agg(d)
#             C         D
# A                      
# bar -0.163277  1.185906
# foo -0.057993  1.250602

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

>>> d = {"C": "sum", "D": "std"}
>>> grouped.agg(d)
#             C         D
# A                      
# bar -0.163277  1.185906
# foo -0.057993  1.250602

Преобразования в операциях GroupBy

Преобразование - это операция GroupBy, результат которой индексируется так же, как и группируемая. Типичными примерами являются DataFrameGroupBy.cumsum() и DataFrameGroupBy.diff().

>>> speeds
#           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

>>> grouped = speeds.groupby("class")["max_speed"]
>>> grouped.cumsum()
# falcon     389.0
# parrot     413.0
# lion        80.2
# monkey       NaN
# leopard    138.2
# Name: max_speed, dtype: float64

>>> grouped.diff()
# falcon       NaN
# parrot    -365.0
# lion         NaN
# monkey       NaN
# leopard      NaN
# Name: max_speed, dtype: float64

В отличие от агрегаций, группировки, которые используются для разделения исходного объекта, не включаются в результат

Заметка. Так как преобразования не включают группировки, используемые для разделения результата, аргументы as_index и sort в DataFrame.groupby() и Series.groupby() не имеют никакого эффекта.

Обычно преобразование используется для добавления результата обратно в исходный DataFrame.

>>> result = speeds.copy()
>>> result["cumsum"] = grouped.cumsum()
>>> result["diff"] = grouped.diff()
>>> result
#           class           order  max_speed  cumsum   diff
# falcon     bird   Falconiformes      389.0   389.0    NaN
# parrot     bird  Psittaciformes       24.0   413.0 -365.0
# lion     mammal       Carnivora       80.2    80.2    NaN
# monkey   mammal        Primates        NaN     NaN    NaN
# leopard  mammal       Carnivora       58.0   138.2    NaN

Встроенные методы преобразования в GroupBy

Следующие методы GroupBy действуют как преобразования. Из этих методов только DataFrameGroupBy.fillna() не имеет реализации, оптимизированной для Cython.

Метод `GroupBy`Описание
bfill()снова заполнит значения `NA` внутри каждой группы
cumcount()совокупный подсчет внутри каждой группы
cummax()совокупный максимум внутри каждой группы
cummin()совокупный минимум внутри каждой группы
cumprod()совокупный продукт внутри каждой группы
cumsum()совокупная сумма внутри каждой группы
diff()разница между соседними значениями внутри каждой группы
ffill()прямое заполнение значений `NA` внутри каждой группы
fillna()заполняет значения `NA` внутри каждой группы
pct_change()процентное изменение между соседними значениями внутри каждой группы
rank()ранг каждого значения внутри каждой группы
shift()смещение значений вверх или вниз внутри каждой группы

Кроме того, передача любого встроенного метода агрегирования в виде строки в метод DataFrameGroupBy.transform() приведет к трансляции результата по группе, создавая преобразованный результат. Если метод агрегации оптимизирован для Cython, он также будет производительным.

Метод DataFrameGroupBy.transform()

Аналогично методу агрегирования, метод DataFrameGroupBy.transform() может принимать строковые псевдонимы для встроенных методов преобразования. Он также может принимать строковые псевдонимы для встроенных методов агрегирования. Если указан метод агрегирования, то результат будет транслироваться по всей группе.

>>> speeds
#           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

>>> grouped = speeds.groupby("class")[["max_speed"]]
>>> grouped.transform("cumsum")
#          max_speed
# falcon       389.0
# parrot       413.0
# lion          80.2
# monkey         NaN
# leopard      138.2

>>> grouped.transform("sum")
#          max_speed
# falcon       413.0
# parrot       413.0
# lion         138.2
# monkey       138.2
# leopard      138.2

В дополнение к псевдонимам строк метод .transform() также может принимать пользовательские функции. Определяемая пользователем функция должна:

  • Возвращать результат, который либо имеет тот же размер, что и фрагмент группы, либо транслироваться в размер блока группы (например, скаляр, grouped.transform(lambda x: x.iloc[-1])).
  • Обрабатывать фрагмент группы по столбцам. Преобразование применяется к первому фрагменту группы с помощью chunk.apply.
  • Не выполнять операции на месте с фрагментом группы. Фрагменты группы следует рассматривать как неизменяемые, и изменения в фрагменте группы могут привести к неожиданным результатам.
  • (Необязательно) работать сразу со всеми столбцами всего группового блока. Если это поддерживается, то используется быстрый путь, начиная со второго блока.

Заметка. Преобразование путем предоставления определяемой пользователем функции часто менее производительно, чем использование встроенных методов GroupBy. Лучше разбить сложную операцию на цепочку операций, использующих встроенные методы.

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

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

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

>>> index = pd.date_range("10/1/1999", periods=1100)
>>> ts = pd.Series(np.random.normal(0.5, 2, 1100), index)
>>> ts = ts.rolling(window=100, min_periods=100).mean().dropna()
>>> ts.head()
# 2000-01-08    0.115936
# 2000-01-09    0.154858
# 2000-01-10    0.177720
# 2000-01-11    0.133501
# 2000-01-12    0.139585
# Freq: D, dtype: float64

>>> ts.tail()
# 2002-09-30    0.568053
# 2002-10-01    0.530893
# 2002-10-02    0.593561
# 2002-10-03    0.590922
# 2002-10-04    0.581752
# Freq: D, dtype: float64

# НЕ производительный способ
# пример для понимания материала
>>> transformed = ts.groupby(lambda x: x.year).transform(
...     lambda x: (x - x.mean()) / x.std()
... )

# !!! более быстрый аналог 
# grouped = ts.groupby(lambda x: x.year)
# result = (ts - grouped.transform("mean")) / grouped.transform("std")

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

# Исходные данные
>>> grouped = ts.groupby(lambda x: x.year)
>>> grouped.mean()
# 2000    0.466281
# 2001    0.432381
# 2002    0.454201
# dtype: float64

>>> grouped.std()
# 2000    0.128578
# 2001    0.082351
# 2002    0.155647
# dtype: float64

# Преобразованные данные
>>> grouped_trans = transformed.groupby(lambda x: x.year)
>>> grouped_trans.mean()
# 2000   -3.554878e-16
# 2001   -5.770783e-16
# 2002    1.058119e-16
# dtype: float64

>>> grouped_trans.std()
# 2000    1.0
# 2001    1.0
# 2002    1.0
# dtype: float64

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

# НЕ производительный способ 
# пример для понимания материала
>>> ts.groupby(lambda x: x.year).transform(lambda x: x.max() - x.min())
# 2000-01-08    0.665077
# 2000-01-09    0.665077
# 2000-01-10    0.665077
# 2000-01-11    0.665077
# 2000-01-12    0.665077
#                 ...   
# 2002-09-30    0.597013
# 2002-10-01    0.597013
# 2002-10-02    0.597013
# 2002-10-03    0.597013
# 2002-10-04    0.597013
# Freq: D, Length: 1001, dtype: float64


# !!! более быстрый аналог 
# grouped = ts.groupby(lambda x: x.year)
# result = grouped.transform("max") - grouped.transform("min")

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

>>> cols = ["A", "B", "C"]
>>> values = np.random.randn(1000, 3)
>>> values[np.random.randint(0, 1000, 100), 0] = np.nan
>>> values[np.random.randint(0, 1000, 50), 1] = np.nan
>>> values[np.random.randint(0, 1000, 200), 2] = np.nan
>>> data_df = pd.DataFrame(values, columns=cols)
>>> data_df
#             A         B         C
# 0    0.517832  0.204400  1.449715
# 1         NaN -0.044095  0.102307
# 2         NaN  0.768912       NaN
# 3   -0.278080 -1.554905 -0.256375
# 4    0.605055  1.822569  0.579360
# ..        ...       ...       ...
# 995  1.817169  0.504035       NaN
# 996 -0.406149 -2.320358       NaN
# 997  1.213487  0.481328  0.438197
# 998 -2.669296  1.888546 -0.365595
# 999  0.011614 -0.443773  0.138153
# 
# [1000 rows x 3 columns]

>>> countries = np.array(["US", "UK", "GR", "JP"])
>>> key = countries[np.random.randint(0, 4, 1000)]
>>> grouped = data_df.groupby(key)
# Количество не-NA в каждой группе
>>> grouped.count()
#       A    B    C
# GR  219  235  198
# JP  226  233  201
# UK  217  230  205
# US  243  253  212

# НЕ производительный способ 
# пример для понимания материала
>>> transformed = grouped.transform(lambda x: x.fillna(x.mean()))

# !!! более быстрый аналог 
# result = data_df.fillna(grouped.transform("mean"))

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

>>> grouped_trans = transformed.groupby(key)
# исходная группа
>>> grouped.mean()
#            A         B         C
# GR  0.020678 -0.049657  0.110486
# JP  0.104313  0.059318 -0.042290
# UK  0.010247  0.035147 -0.108634
# US -0.060774 -0.000013  0.053476

# трансформация не изменила группу
>>> grouped_trans.mean()
#            A         B         C
# GR  0.020678 -0.049657  0.110486
# JP  0.104313  0.059318 -0.042290
# UK  0.010247  0.035147 -0.108634
# US -0.060774 -0.000013  0.053476

# В оригинале отсутствуют некоторые данные
>>> grouped.count()
#       A    B    C
# GR  219  235  198
# JP  226  233  201
# UK  217  230  205
# US  243  253  212

# подсчитывает после преобразования
>>> grouped_trans.count()
#       A    B    C
# GR  245  245  245
# JP  248  248  248
# UK  242  242  242
# US  265  265  265

# количество значений, отличных от NA, равно размеру группы
>>> grouped_trans.size()
# GR    245
# JP    248
# UK    242
# US    265
# dtype: int64

Оконные операции и повторная выборка в операциях GroupBy

В качестве методов для объекта группировки можно использовать методы .resample(), .expanding() и .rolling().

Использование метода DataFrame.rolling() в GroupBy

В приведенном ниже примере метод DataFrame.rolling() будет применен к выборкам столбца B, основанным на группах столбца A с длиной окна в 4 наблюдения. Для лучшего понимания, что происходит, создадим новый DataFrame:

>>> df_re = pd.DataFrame({"A": [1] * 10 + [5] * 10, "B": np.arange(20)})
>>> df_re
#     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]

>>> df_re.groupby("A").rolling(4).B.mean()
# 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

Использование метода DataFrame.expanding() в операциях GroupBy

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

>>> df_re.groupby("A").expanding().sum()
#           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]

Использование метода DataFrame.resample() в операциях GroupBy

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

df_re = pd.DataFrame(
    {
        "date": pd.date_range(start="2024-01-01", periods=4, freq="W"),
        "group": [1, 1, 2, 2],
        "val": [5, 6, 7, 8],
    }
).set_index("date")

# данные наблюдений с частотой 7 дней
>>> df_re
#             group  val
# date                  
# 2024-01-07      1    5
# 2024-01-14      1    6
# 2024-01-21      2    7
# 2024-01-28      2    8

# получаем частоту наблюдений 1 день и
# восполняем пропущенные значения
>>> df_re.groupby("group").resample("1D").ffill()
#                   group  val
# group date                  
# 1     2024-01-07      1    5
#       2024-01-08      1    5
#       2024-01-09      1    5
#       2024-01-10      1    5
#       2024-01-11      1    5
#       2024-01-12      1    5
#       2024-01-13      1    5
#       2024-01-14      1    6
# 2     2024-01-21      2    7
#       2024-01-22      2    7
#       2024-01-23      2    7
#       2024-01-24      2    7
#       2024-01-25      2    7
#       2024-01-26      2    7
#       2024-01-27      2    7
#       2024-01-28      2    8

Фильтры GroupBy, метод DataFrameGroupBy.filter()

Метод DataFrameGroupBy.filter() может отфильтровывать либо целые группы, либо часть групп, либо и то, и другое. Возвращает отфильтрованную версию вызывающего объекта, включая столбцы группировки, если они указаны.

Метод DataFrameGroupBy.filter() принимает определяемую пользователем функцию, которая при применении ко всей группе возвращает значение True или False. Результатом метода является подмножество групп, для которых определяемая пользователем функция вернула значение True.

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

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

>>> sf = pd.Series([1, 1, 2, 3, 3, 3])
>>> sf.groupby(sf).filter(lambda x: x.sum() > 2)
# 3    3
# 4    3
# 5    3
# dtype: int64

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

>>> dff = pd.DataFrame({"A": np.arange(8), "B": list("aabbbbcc")})
>>> dff.groupby("B").filter(lambda x: len(x) > 2)
#    A  B
# 2  2  b
# 3  3  b
# 4  4  b
# 5  5  b

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

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

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

# добавим столбец `С`
>>> dff["C"] = np.arange(8)
>>> dff.groupby("B").filter(lambda x: len(x["C"]) > 2)
#    A  B  C
# 2  2  b  2
# 3  3  b  3
# 4  4  b  4
# 5  5  b  5

Метод DataFrame.apply() и групповые операции

Некоторые операции с сгруппированными данными могут не вписываться в категории агрегирования, преобразования или фильтрации. Для этого можно использовать метод DataFrame.apply().

Предупреждение. Сгруппированные столбцы могут быть включены в выходные данные или нет в зависимости от того, что будет делать пользовательская функция (агрегировать, трансформировать или фильтровать), переданная в метод DataFrame.apply(). Метод DataFrame.apply() пытается разумно угадать, как себя вести, но иногда он может ошибаться...

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

Для примеров используем ранее созданный DataFrame:

>>> df
#      A      B         C         D
# 0  foo    one -1.416678  1.366671
# 1  bar    one -0.650714 -0.992193
# 2  foo    two  0.410918  1.245727
# 3  bar  three  0.467266  1.232117
# 4  foo    two  0.548223 -0.065070
# 5  bar    two  0.020170 -0.593099
# 6  foo    one  0.440195 -0.141176
# 7  foo  three -0.040652 -1.705956

>>> grouped = df.groupby("A")
# можно было бы просто вызвать `grouped["C"].describe().T`
>>> grouped["C"].apply(lambda x: x.describe())
# A         
# bar  count    3.000000
#      mean    -0.054426
#      std      0.562711
#      min     -0.650714
#      25%     -0.315272
#      50%      0.020170
#      75%      0.243718
#      max      0.467266
# foo  count    5.000000
#      mean    -0.011599
#      std      0.817179
#      min     -1.416678
#      25%     -0.040652
#      50%      0.410918
#      75%      0.440195
#      max      0.548223
# Name: C, dtype: float64

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

>>> grouped = df.groupby('A')['C']
# определим пользовательскую функцию
# возвращающую `DataFrame`
def f(group):
    return pd.DataFrame({'original': group,
                         'demeaned': group - group.mean()})

>>> grouped.apply(f)
#        original  demeaned
# A                        
# bar 1 -0.650714 -0.596288
#     3  0.467266  0.521692
#     5  0.020170  0.074596
# foo 0 -1.416678 -1.405079
#     2  0.410918  0.422516
#     4  0.548223  0.559822
#     6  0.440195  0.451793
#     7 -0.040652 -0.029053

Метод Series.аpply() может работать со значением, возвращаемым из применённой функции, которая сама по себе является серией, и, возможно, приводить результат к DataFrame:

# определим пользовательскую функцию
# возвращающую `Series`
def f(x):
    return pd.Series([x, x ** 2], index=["x", "x^2"])

# создаем `Series` с которым будем работать
>>> s = pd.Series(np.random.rand(5))
>>> s
# 0    0.572127
# 1    0.100233
# 2    0.718959
# 3    0.261216
# 4    0.617886
# dtype: float64

>>> s.apply(f)
#           x       x^2
# 0  0.572127  0.327329
# 1  0.100233  0.010047
# 2  0.718959  0.516902
# 3  0.261216  0.068234
# 4  0.617886  0.381783

Как и в случае с методом DataFrameGroupBy.agg(), результирующий тип dtype будет отражать возвращаемый пользовательской функцией тип. Если результаты из разных групп имеют разные dtypes, то общий dtype будет определен так же, как и построение DataFrame.

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

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

# сравним вывод
>>> df.groupby("A", group_keys=True).apply(lambda x: x)
#          A      B         C         D
# A                                    
# bar 1  bar    one -0.650714 -0.992193
#     3  bar  three  0.467266  1.232117
#     5  bar    two  0.020170 -0.593099
# foo 0  foo    one -1.416678  1.366671
#     2  foo    two  0.410918  1.245727
#     4  foo    two  0.548223 -0.065070
#     6  foo    one  0.440195 -0.141176
#     7  foo  three -0.040652 -1.705956

# с выводом
>>> df.groupby("A", group_keys=False).apply(lambda x: x)
#      A      B         C         D
# 0  foo    one -1.416678  1.366671
# 1  bar    one -0.650714 -0.992193
# 2  foo    two  0.410918  1.245727
# 3  bar  three  0.467266  1.232117
# 4  foo    two  0.548223 -0.065070
# 5  bar    two  0.020170 -0.593099
# 6  foo    one  0.440195 -0.141176
# 7  foo  three -0.040652 -1.705956