Spec-Zone.ru › pandas 2

Группировка: split-apply-combine

Под «группировкой» мы подразумеваем процесс, включающий один или несколько из следующих шагов:

  • Разделение данных на группы на основе определённых критериев.

  • Применение функции к каждой группе независимо.

  • Объединение результатов в структуру данных.

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

  • Агрегирование: вычисление сводной статистики (или статистик) для каждой группы. Некоторые примеры:

    • Вычисление сумм или средних значений по группам.

    • Вычисление размеров/количества элементов в группах.

  • Преобразование: выполнение вычислений, специфичных для группы, и возвращение объекта с аналогичной индексацией. Некоторые примеры:

    • Нормализация данных (z-score) внутри группы.

    • Заполнение пропущенных значений (NaN) в группах значениями, полученными из каждой группы.

  • Фильтрация: отбрасывание некоторых групп на основе группового вычисления, которое возвращает значение True или False. Некоторые примеры:

    • Отбрасывание данных, принадлежащих группам с небольшим числом элементов.

    • Фильтрация данных на основе групповой суммы или среднего значения.

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

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

Примечание

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

Название GroupBy должно быть знакомо тем, кто работал с инструментом на основе SQL (или itertools), в котором можно написать код, например:

SELECT Column1, Column2, mean(Column3), sum(Column4)
FROM SomeTable
GROUP BY Column1, Column2

Мы стремимся сделать такие операции естественными и простыми для выражения с помощью pandas. Мы рассмотрим каждый аспект функциональности GroupBy, а затем предоставим несколько нетривиальных примеров/случаев использования.

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

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

Абстрактное определение группировки — это предоставление отображения меток к именам групп. Чтобы создать объект GroupBy (подробнее об объекте GroupBy позже), можно сделать следующее:

In [1]: 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"),
   ...: )
   ...: 

In [2]: speeds
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

In [3]: grouped = speeds.groupby("class")

In [4]: grouped = speeds.groupby(["class", "order"])

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

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

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

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

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

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

Мы обобщаем объекты группировки, как ключи. Например, рассмотрим следующий DataFrame:

Примечание

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

In [5]: 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 [6]: df
Out[6]: 
     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(). Этот метод возвращает экземпляр pandas.api.typing.DataFrameGroupBy. Естественно, мы можем сгруппировать по столбцам A или B, или по обоим:

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

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

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

Примечание

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

Если у нас также есть 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

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

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

In [14]: grouped = df.T.groupby(get_letter_type)

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

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

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

In [17]: s
Out[17]: 
1     1
2     2
3     3
1    10
2    20
3    30
dtype: int64

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

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

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

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

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

Примечание

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

Сортировка GroupBy

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

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

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

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

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

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

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

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

GroupBy dropna

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

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

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

In [30]: df_dropna
Out[30]: 
   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 [31]: df_dropna.groupby(by=["b"], dropna=True).sum()
Out[31]: 
     a  c
b        
1.0  2  3
2.0  2  5

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

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

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

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

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

In [34]: df.T.groupby(get_letter_type).groups
Out[34]: {'consonant': ['B', 'C', 'D'], 'vowel': ['A']}

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

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

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

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

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

In [38]: n = 10

In [39]: weight = np.random.normal(166, 20, size=n)

In [40]: height = np.random.normal(60, 10, size=n)

In [41]: time = pd.date_range("1/1/2000", periods=n)

In [42]: gender = np.random.choice(["male", "female"], size=n)

In [43]: df = pd.DataFrame(
   ....:     {"height": height, "weight": weight, "gender": gender}, index=time
   ....: )
   ....: 

In [44]: df
Out[44]: 
               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 [45]: gb = df.groupby("gender")
In [46]: 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

GroupBy с MultiIndex

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

Давайте создадим Series с двух-уровневым MultiIndex.

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

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

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

In [50]: s
Out[50]: 
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 [51]: grouped = s.groupby(level=0)

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

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

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

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

In [54]: 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"],
   ....: ]
   ....: 

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

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

In [57]: s
Out[57]: 
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 [58]: s.groupby(level=["first", "second"]).sum()
Out[58]: 
first  second
bar    doo      -1.220674
baz    bee      -0.608004
foo    bop       1.023898
qux    bop       0.000895
dtype: float64

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

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

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

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

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

Давайте сначала создадим DataFrame с MultiIndex:

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

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

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

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

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

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

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

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

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

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

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

In [67]: 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 [68]: df
Out[68]: 
     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 [69]: grouped = df.groupby(["A"])

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

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

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

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

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

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

In [73]: grouped[["A", "B"]].sum()
Out[73]: 
                   A                  B
A                                      
bar        barbarbar        onethreetwo
foo  foofoofoofoofoo  onetwotwoonethree

Итерация по группам

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

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

In [75]: 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 [76]: 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

См. Итерация по группам.

Выбор группы

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

In [77]: grouped.get_group("bar")
Out[77]: 
     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 [78]: df.groupby(["A", "B"]).get_group(("bar", "one"))
Out[78]: 
     A    B         C         D
1  bar  one  0.254161  1.511763

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

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

In [79]: 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 [80]: animals
Out[80]: 
  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 [81]: animals.groupby("kind").sum()
Out[81]: 
      height  weight
kind                
cat     18.6    17.8
dog     40.0   205.5

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

In [82]: animals.groupby("kind", as_index=False).sum()
Out[82]: 
  kind  height  weight
0  cat    18.6    17.8
1  dog    40.0   205.5

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

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

Метод

Описание

any()

Вычисление того, истинно ли хоть одно из значений в группах

all()

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

count()

Вычисление количества значений, отличных от NA, в группах

cov() *

Вычисление ковариации групп

first()

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

idxmax()

Вычисление индекса максимального значения в каждой группе

idxmin()

Вычисление индекса минимального значения в каждой группе

last()

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

max()

Вычисление максимального значения в каждой группе

mean()

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

median()

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

min()

Вычисление минимального значения в каждой группе

nunique()

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

prod()

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

quantile()

Вычисление заданного квантиля значений в каждой группе

sem()

Вычисление стандартной ошибки среднего значения значений в каждой группе

size()

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

skew() *

Вычисление асимметрии значений в каждой группе

std()

Вычисление стандартного отклонения значений в каждой группе

sum()

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

var()

Вычисление дисперсии значений в каждой группе

Примеры:

In [83]: df.groupby("A")[["C", "D"]].max()
Out[83]: 
            C         D
A                      
bar  0.254161  1.511763
foo  1.193555  1.627081

In [84]: df.groupby(["A", "B"]).mean()
Out[84]: 
                  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

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

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

In [86]: grouped.size()
Out[86]: 
A    B    
bar  one      1
     three    1
     two      1
foo  one      2
     three    1
     two      2
dtype: int64

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

In [87]: grouped.describe()
Out[87]: 
              C                      ...         D                    
          count      mean       std  ...       50%       75%       max
A   B                                ...                              
bar one     1.0  0.254161       NaN  ...  1.511763  1.511763  1.511763
    three   1.0  0.215897       NaN  ... -0.990582 -0.990582 -0.990582
    two     1.0 -0.077118       NaN  ...  1.211526  1.211526  1.211526
foo one     2.0 -0.491888  0.117887  ...  0.807291  1.076676  1.346061
    three   1.0 -0.862495       NaN  ...  0.024580  0.024580  0.024580
    two     2.0  0.024925  1.652692  ...  0.592714  1.109898  1.627081

[6 rows x 16 columns]

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

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

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

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

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

Примечание

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

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

Метод aggregate()

Примечание

Метод aggregate() может принимать множество различных типов ввода. В этом разделе подробно описывается использование строковых псевдонимов для различных методов GroupBy; другие вводы подробно описаны в следующих разделах.

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

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

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

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

In [95]: grouped.agg("sum")
Out[95]: 
                  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

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

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

In [97]: grouped.agg("sum")
Out[97]: 
     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 [98]: df.groupby("A", as_index=False)[["C", "D"]].agg("sum")
Out[98]: 
     A         C         D
0  bar  0.392940  1.732707
1  foo -1.796421  2.824590

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

In [99]: df.groupby(["A", "B"]).agg("sum").reset_index()
Out[99]: 
     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

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

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

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

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

Примечание

Агрегирование с помощью UDF часто менее производительно, чем использование встроенных методов pandas для GroupBy. Подумайте о том, чтобы разбить сложную операцию на цепочку операций, использующих встроенные методы.

In [100]: animals
Out[100]: 
  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 [101]: animals.groupby("kind")[["height"]].agg(lambda x: set(x))
Out[101]: 
           height
kind             
cat    {9.1, 9.5}
dog   {34.0, 6.0}

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

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

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

На сгруппированном Series, можно передать список или словарь функций методу SeriesGroupBy.agg(), получив на выходе DataFrame:

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

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

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

In [105]: grouped[["C", "D"]].agg(["sum", "mean", "std"])
Out[105]: 
            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 [106]: (
   .....:     grouped["C"]
   .....:     .agg(["sum", "mean", "std"])
   .....:     .rename(columns={"sum": "foo", "mean": "bar", "std": "baz"})
   .....: )
   .....: 
Out[106]: 
          foo       bar       baz
A                                
bar  0.392940  0.130980  0.181231
foo -1.796421 -0.359284  0.912265

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

In [107]: (
   .....:     grouped[["C", "D"]].agg(["sum", "mean", "std"]).rename(
   .....:         columns={"sum": "foo", "mean": "bar", "std": "baz"}
   .....:     )
   .....: )
   .....: 
Out[107]: 
            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

Примечание

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

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

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

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

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

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

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

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

In [110]: animals
Out[110]: 
  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 [111]: 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"),
   .....: )
   .....: 
Out[111]: 
      min_height  max_height  average_weight
kind                                        
cat          9.1         9.5            8.90
dog          6.0        34.0          102.75

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

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

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

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

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

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

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

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

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

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

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

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

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

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

In [117]: speeds
Out[117]: 
          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

In [118]: grouped = speeds.groupby("class")["max_speed"]

In [119]: grouped.cumsum()
Out[119]: 
falcon     389.0
parrot     413.0
lion        80.2
monkey       NaN
leopard    138.2
Name: max_speed, dtype: float64

In [120]: grouped.diff()
Out[120]: 
falcon       NaN
parrot    -365.0
lion         NaN
monkey       NaN
leopard      NaN
Name: max_speed, dtype: float64

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

Примечание

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

Частое применение преобразования — добавление результата обратно в исходную DataFrame.

In [121]: result = speeds.copy()

In [122]: result["cumsum"] = grouped.cumsum()

In [123]: result["diff"] = grouped.diff()

In [124]: result
Out[124]: 
          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 действуют как преобразования.

Метод

Описание

bfill()

Заполнить пропущенные значения (NA) в каждой группе с конца

cumcount()

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

cummax()

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

cummin()

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

cumprod()

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

cumsum()

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

diff()

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

ffill()

Заполнить пропущенные значения (NA) в каждой группе с начала

pct_change()

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

rank()

Вычислить ранг каждого значения в каждой группе

shift()

Сдвинуть значения вверх или вниз в каждой группе

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

Метод transform()

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

In [125]: speeds
Out[125]: 
          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

In [126]: grouped = speeds.groupby("class")[["max_speed"]]

In [127]: grouped.transform("cumsum")
Out[127]: 
         max_speed
falcon       389.0
parrot       413.0
lion          80.2
monkey         NaN
leopard      138.2

In [128]: grouped.transform("sum")
Out[128]: 
         max_speed
falcon       413.0
parrot       413.0
lion         138.2
monkey       138.2
leopard      138.2

Помимо строковых алиасов, метод transform() также может принимать пользовательские функции (UDFs). Пользовательская функция должна:

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

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

  • Не выполнять операции на месте на фрагменте группы. Фрагменты группы должны рассматриваться как неизменяемые, а изменения в фрагменте группы могут привести к непредвиденным результатам. См. Изменение с помощью пользовательских функций (UDF) для получения дополнительной информации.

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

Примечание

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

Все примеры в этом разделе можно сделать более производительными, вызвав встроенные методы вместо использования UDF. См. ниже для примеров.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

In [143]: ts.groupby(lambda x: x.year).transform(lambda x: x.max() - x.min())
Out[143]: 
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 [144]: cols = ["A", "B", "C"]

In [145]: values = np.random.randn(1000, 3)

In [146]: values[np.random.randint(0, 1000, 100), 0] = np.nan

In [147]: values[np.random.randint(0, 1000, 50), 1] = np.nan

In [148]: values[np.random.randint(0, 1000, 200), 2] = np.nan

In [149]: data_df = pd.DataFrame(values, columns=cols)

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

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

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

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

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

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

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

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

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

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

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

# result = ts.groupby(lambda x: x.year).transform(
#     lambda x: (x - x.mean()) / x.std()
# )
In [162]: grouped = ts.groupby(lambda x: x.year)

In [163]: result = (ts - grouped.transform("mean")) / grouped.transform("std")

# result = ts.groupby(lambda x: x.year).transform(lambda x: x.max() - x.min())
In [164]: grouped = ts.groupby(lambda x: x.year)

In [165]: result = grouped.transform("max") - grouped.transform("min")

# grouped = data_df.groupby(key)
# result = grouped.transform(lambda x: x.fillna(x.mean()))
In [166]: grouped = data_df.groupby(key)

In [167]: result = data_df.fillna(grouped.transform("mean"))

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

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

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

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

In [169]: df_re
Out[169]: 
    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 [170]: df_re.groupby("A").rolling(4).B.mean()
Out[170]: 
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 [171]: df_re.groupby("A").expanding().sum()
Out[171]: 
          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 [172]: 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 [173]: df_re
Out[173]: 
            group  val
date                  
2016-01-03      1    5
2016-01-10      1    6
2016-01-17      2    7
2016-01-24      2    8

In [174]: df_re.groupby("group").resample("1D", include_groups=False).ffill()
Out[174]: 
                  val
group date           
1     2016-01-03    5
      2016-01-04    5
      2016-01-05    5
      2016-01-06    5
      2016-01-07    5
...               ...
2     2016-01-20    7
      2016-01-21    7
      2016-01-22    7
      2016-01-23    7
      2016-01-24    8

[16 rows x 1 columns]

Фильтрация

Фильтрация — это операция GroupBy, которая подмножит исходный объект группирования. Она может фильтровать целые группы, части групп или оба варианта. Фильтрации возвращают отфильтрованную версию вызываемого объекта, включая столбцы группировки, если они указаны. В следующем примере class включён в результат.

In [175]: speeds
Out[175]: 
          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

In [176]: speeds.groupby("class").nth(1)
Out[176]: 
         class           order  max_speed
parrot    bird  Psittaciformes       24.0
monkey  mammal        Primates        NaN

Примечание

В отличие от агрегаций, фильтрации не добавляют ключи групп в индекс результата. Из-за этого передача as_index=False или sort=True не повлияет на эти методы.

Фильтрации будут учитывать подмножество столбцов объекта GroupBy.

In [177]: speeds.groupby("class")[["order", "max_speed"]].nth(1)
Out[177]: 
                 order  max_speed
parrot  Psittaciformes       24.0
monkey        Primates        NaN

Встроенные фильтрации

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

Метод

Описание

head()

Выбор первой строки(ы) каждой группы

nth()

Выбор n-ой строки(ы) каждой группы

tail()

Выбор последней строки(ы) каждой группы

Пользователи также могут использовать преобразования вместе с булевыми индексами для создания сложных фильтраций в пределах групп. Например, предположим, что нам даны группы продуктов и их объёмы, и мы хотим подмножить данные только до самых больших продуктов, которые занимают не более 90% от общего объёма в каждой группе.

In [178]: product_volumes = pd.DataFrame(
   .....:     {
   .....:         "group": list("xxxxyyy"),
   .....:         "product": list("abcdefg"),
   .....:         "volume": [10, 30, 20, 15, 40, 10, 20],
   .....:     }
   .....: )
   .....: 

In [179]: product_volumes
Out[179]: 
  group product  volume
0     x       a      10
1     x       b      30
2     x       c      20
3     x       d      15
4     y       e      40
5     y       f      10
6     y       g      20

# Sort by volume to select the largest products first
In [180]: product_volumes = product_volumes.sort_values("volume", ascending=False)

In [181]: grouped = product_volumes.groupby("group")["volume"]

In [182]: cumpct = grouped.cumsum() / grouped.transform("sum")

In [183]: cumpct
Out[183]: 
4    0.571429
1    0.400000
2    0.666667
6    0.857143
3    0.866667
0    1.000000
5    1.000000
Name: volume, dtype: float64

In [184]: significant_products = product_volumes[cumpct <= 0.9]

In [185]: significant_products.sort_values(["group", "product"])
Out[185]: 
  group product  volume
1     x       b      30
2     x       c      20
3     x       d      15
4     y       e      40
6     y       g      20

Метод filter

Примечание

Фильтрация путём предоставления filter с пользовательской функцией (UDF) часто менее производительна, чем использование встроенных методов GroupBy. Рассмотрите возможность разбивки сложной операции на цепочку операций, использующих встроенные методы.

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

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

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

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

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

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

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

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

In [190]: dff.groupby("B").filter(lambda x: len(x) > 2, dropna=False)
Out[190]: 
     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 с несколькими столбцами фильтры должны явно указать столбец в качестве критерия фильтрации.

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

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

Гибкое применение apply

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

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

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

Примечание

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

In [193]: df
Out[193]: 
     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 [194]: grouped = df.groupby("A")

# could also just call .describe()
In [195]: grouped["C"].apply(lambda x: x.describe())
Out[195]: 
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 [196]: grouped = df.groupby('A')['C']

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

In [198]: grouped.apply(f)
Out[198]: 
       original  demeaned
A                        
bar 1  0.254161  0.123181
    3  0.215897  0.084917
    5 -0.077118 -0.208098
foo 0 -0.575247 -0.215962
    2 -1.143704 -0.784420
    4  1.193555  1.552839
    6 -0.408530 -0.049245
    7 -0.862495 -0.503211

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

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

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

In [201]: s
Out[201]: 
0    0.582898
1    0.098352
2    0.001438
3    0.009420
4    0.815826
dtype: float64

In [202]: s.apply(f)
Out[202]: 
          x       x^2
0  0.582898  0.339770
1  0.098352  0.009673
2  0.001438  0.000002
3  0.009420  0.000089
4  0.815826  0.665572

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

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

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

Сравните

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

с

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

Ускоренные Numba процедуры

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

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

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

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

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

Дополнительные полезные функции

Исключение нечисловых столбцов

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

In [205]: df
Out[205]: 
     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 [206]: df.groupby("A").std(numeric_only=True)
Out[206]: 
            C         D
A                      
bar  0.181231  1.366330
foo  0.912265  0.884785

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

In [207]: from decimal import Decimal

In [208]: 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"),
   .....:         ],
   .....:     }
   .....: )
   .....: 

In [209]: df_dec.groupby(["id"])[["dec_column"]].sum()
Out[209]: 
   dec_column
id           
1        0.75
2        0.55

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

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

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

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

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

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

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

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

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

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

Под NA, мы подразумеваем любые NA значения, включая NA, NaN, NaT, и None. Если в ключе группировки присутствуют NA значения, по умолчанию они будут исключены. Другими словами, любая «NA группа» будет отброшена. Вы можете включить группы NA, указав dropna=False.

In [214]: df = pd.DataFrame({"key": [1.0, 1.0, np.nan, 2.0, np.nan], "A": [1, 2, 3, 4, 5]})

In [215]: df
Out[215]: 
   key  A
0  1.0  1
1  1.0  2
2  NaN  3
3  2.0  4
4  NaN  5

In [216]: df.groupby("key", dropna=True).sum()
Out[216]: 
     A
key   
1.0  3
2.0  4

In [217]: df.groupby("key", dropna=False).sum()
Out[217]: 
     A
key   
1.0  3
2.0  4
NaN  8

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

Категориальные переменные, представленные как экземпляры класса Categorical pandas, могут использоваться в качестве ключей группировки. В этом случае порядок уровней будет сохранён. При observed=False и sort=False, любые не наблюдаемые категории будут в конце результата в порядке.

In [218]: days = pd.Categorical(
   .....:     values=["Wed", "Mon", "Thu", "Mon", "Wed", "Sat"],
   .....:     categories=["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
   .....: )
   .....: 

In [219]: data = pd.DataFrame(
   .....:    {
   .....:        "day": days,
   .....:        "workers": [3, 4, 1, 4, 2, 2],
   .....:    }
   .....: )
   .....: 

In [220]: data
Out[220]: 
   day  workers
0  Wed        3
1  Mon        4
2  Thu        1
3  Mon        4
4  Wed        2
5  Sat        2

In [221]: data.groupby("day", observed=False, sort=True).sum()
Out[221]: 
     workers
day         
Mon        8
Tue        0
Wed        5
Thu        1
Fri        0
Sat        2
Sun        0

In [222]: data.groupby("day", observed=False, sort=False).sum()
Out[222]: 
     workers
day         
Wed        5
Mon        8
Thu        1
Sat        2
Tue        0
Fri        0
Sun        0

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

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

In [223]: import datetime

In [224]: 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 [225]: df
Out[225]: 
  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 [226]: df.groupby([pd.Grouper(freq="1ME", key="Date"), "Buyer"])[["Quantity"]].sum()
Out[226]: 
                  Quantity
Date       Buyer          
2013-01-31 Carl          1
           Mark          3
2013-10-31 Carl          6
           Joe           9
2013-12-31 Carl          3
           Joe           9

Когда freq указано, объект, возвращаемый pd.Grouper, будет экземпляром pandas.api.typing.TimeGrouper. Если столбец и индекс имеют одинаковое имя, вы можете использовать key для группировки по столбцу и level для группировки по индексу.

In [227]: df = df.set_index("Date")

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

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

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

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

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

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

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

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

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

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

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

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

Чтобы выбрать n-й элемент из каждой группы, используйте DataFrameGroupBy.nth() или SeriesGroupBy.nth(). Передаваемые аргументы могут быть любыми целыми числами, списками целых чисел, срезами или списками срезов; примеры см. ниже. Когда n-й элемент группы не существует, ошибка не генерируется; вместо этого не возвращаются соответствующие строки.

В общем, эта операция действует как фильтрация. В некоторых случаях она также вернёт одну строку на группу, что также является сокращением. Однако поскольку, в общем случае, она может вернуть ноль или несколько строк на группу, pandas обрабатывает её как фильтрацию во всех случаях.

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

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

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

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

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

Если n-й элемент группы не существует, то соответствующая строка не включается в результат. В частности, если указанное n значение больше, чем любая группа, результат будет пустым DataFrame.

In [241]: g.nth(5)
Out[241]: 
Empty DataFrame
Columns: [A, B]
Index: []

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

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

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

# nth(-1) is the same as g.last()
In [244]: g.nth(-1, dropna="any")
Out[244]: 
   A    B
1  1  4.0
2  5  6.0

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

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

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

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

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

# get the first, 4th, and last date index for each month
In [249]: df.groupby([df.index.year, df.index.month]).nth([0, 3, -1])
Out[249]: 
            a  b
2014-04-01  1  1
2014-04-04  1  1
2014-04-30  1  1
2014-05-01  1  1
2014-05-06  1  1
2014-05-30  1  1
2014-06-02  1  1
2014-06-05  1  1
2014-06-30  1  1

Вы также можете использовать срезы или списки срезов.

In [250]: df.groupby([df.index.year, df.index.month]).nth[1:]
Out[250]: 
            a  b
2014-04-02  1  1
2014-04-03  1  1
2014-04-04  1  1
2014-04-07  1  1
2014-04-08  1  1
...        .. ..
2014-06-24  1  1
2014-06-25  1  1
2014-06-26  1  1
2014-06-27  1  1
2014-06-30  1  1

[62 rows x 2 columns]

In [251]: df.groupby([df.index.year, df.index.month]).nth[1:, :-1]
Out[251]: 
            a  b
2014-04-01  1  1
2014-04-02  1  1
2014-04-03  1  1
2014-04-04  1  1
2014-04-07  1  1
...        .. ..
2014-06-24  1  1
2014-06-25  1  1
2014-06-26  1  1
2014-06-27  1  1
2014-06-30  1  1

[65 rows x 2 columns]

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

In [264]: df.groupby("g").boxplot()
Out[264]: 
A         Axes(0.1,0.15;0.363636x0.75)
B    Axes(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 [265]: n = 1000

In [266]: 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 [267]: df.head(2)
Out[267]: 
     Store    Product  Revenue  Quantity
0  Store_2  Product_1    26.12         1
1  Store_2  Product_1    28.86         1

Теперь мы находим цены на магазин/продукт.

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

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

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

In [270]: df.groupby(["Store", "Product"]).pipe(mean)
Out[270]: 
                     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 и находит среднее значение столбцов Revenue и Quantity соответственно для каждой комбинации Магазин-Продукт. Функция mean может быть любой функцией, которая принимает объект GroupBy; функция .pipe передаст объект GroupBy в качестве параметра в указанную вами функцию.

Примеры

Мультиколонная факторизация

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

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

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

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

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

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

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

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

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

Примечание

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

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

In [276]: df
Out[276]: 
          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 [277]: df.index // 5
Out[277]: Index([0, 0, 0, 0, 0, 1, 1, 1, 1, 1], dtype='int64')

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

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

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

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

In [281]: result = df.groupby("a").apply(compute_metrics, include_groups=False)

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

In [283]: result.stack(future_stack=True)
Out[283]: 
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/2.2.2/user_guide/groupby.html

Spec-Zone.ru

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