Группировка: split-apply-combine
Под «группировкой» мы подразумеваем процесс, включающий один или несколько следующих шагов:
Разделение данных на группы на основе определенных критериев.
Применение функции к каждой группе независимо.
Объединение результатов в структуру данных.
Из этих шагов шаг разделения является самым простым. На самом деле, во многих ситуациях мы можем захотеть разделить набор данных на группы и сделать что-то с этими группами. На шаге применения мы можем захотеть сделать одно из следующего:
-
Агрегация: вычислить сводные статистические данные (или статистику) для каждой группы. Некоторые примеры:
Вычислить суммы или средние значения групп.
Вычислить размер/количество элементов в группах.
-
Трансформация: выполнить некоторые вычисления, специфичные для группы, и вернуть объект с аналогичными индексами. Некоторые примеры:
Нормализация данных (z-score) внутри группы.
Заполнение пропущенных значений (NaN) в группах значением, полученным из каждой группы.
-
Фильтрация: отбросить некоторые группы в соответствии с вычислением по группам, которое вычисляет True или False. Некоторые примеры:
Отбросить данные, относящиеся к группам с небольшим количеством членов.
Фильтровать данные на основе суммы или среднего значения группы.
Некоторое сочетание вышеперечисленного: GroupBy будет проверять результаты шага применения и попытается вернуть разумный объединенный результат, если он не попадает ни в одну из вышеперечисленных категорий.
Поскольку набор методов экземпляров объектов в pandas-структурах данных обычно богатый и выразительный, мы часто просто хотим вызвать, скажем, функцию DataFrame для каждой группы. Имя GroupBy должно быть знакомо тем, кто работал с SQL-инструментами (или itertools), где можно написать код, например:
SELECT Column1, Column2, mean(Column3), sum(Column4)
FROM SomeTable
GROUP BY Column1, Column2
Мы стремимся сделать такие операции естественными и лёгкими для выражения с помощью pandas. Мы рассмотрим каждую область функциональности GroupBy, а затем приведём несколько нетривиальных примеров/случаев использования.
См. справочник по рецептам для некоторых продвинутых стратегий.
Разделение объекта на группы
Объекты pandas могут быть разделены по любым из своих осей. Абстрактное определение группировки — это предоставление отображения меток на имена групп. Для создания объекта GroupBy (подробнее об объекте GroupBy позже), вы можете сделать следующее:
In [1]: df = pd.DataFrame(
...: [
...: ("bird", "Falconiformes", 389.0),
...: ("bird", "Psittaciformes", 24.0),
...: ("mammal", "Carnivora", 80.2),
...: ("mammal", "Primates", np.nan),
...: ("mammal", "Carnivora", 58),
...: ],
...: index=["falcon", "parrot", "lion", "monkey", "leopard"],
...: columns=("class", "order", "max_speed"),
...: )
...:
In [2]: df
Out[2]:
class order max_speed
falcon bird Falconiformes 389.0
parrot bird Psittaciformes 24.0
lion mammal Carnivora 80.2
monkey mammal Primates NaN
leopard mammal Carnivora 58.0
# default is axis=0
In [3]: grouped = df.groupby("class")
In [4]: grouped = df.groupby("order", axis="columns")
In [5]: grouped = df.groupby(["class", "order"])
Отображение может быть задано различными способами:
Функция Python, которая должна вызываться для каждой метки оси.
Список или массив NumPy той же длины, что и выбранная ось.
Словарь или
Series, предоставляющий отображениеlabel -> group name.Для объектов
DataFrame, строка, указывающая имя столбца или имя уровня индекса для группировки.df.groupby('A')— это просто синтаксический сахар дляdf.groupby(df['A']).Список из любого из вышеперечисленного.
Совокупно объекты группировки называются **ключами**. Например, рассмотрим следующий DataFrame.
Примечание
Строка, переданная в groupby, может ссылаться на столбец или уровень индекса. Если строка соответствует и имени столбца, и имени уровня индекса, будет поднято исключение ValueError.
In [6]: df = pd.DataFrame(
...: {
...: "A": ["foo", "bar", "foo", "bar", "foo", "bar", "foo", "foo"],
...: "B": ["one", "one", "two", "three", "two", "two", "one", "three"],
...: "C": np.random.randn(8),
...: "D": np.random.randn(8),
...: }
...: )
...:
In [7]: df
Out[7]:
A B C D
0 foo one 0.469112 -0.861849
1 bar one -0.282863 -2.104569
2 foo two -1.509059 -0.494929
3 bar three -1.135632 1.071804
4 foo two 1.212112 0.721555
5 bar two -0.173215 -0.706771
6 foo one 0.119209 -1.039575
7 foo three -1.044236 0.271860
Для DataFrame мы получаем объект GroupBy, вызвав groupby(). Мы могли бы естественным образом сгруппировать данные по столбцам A или B, или по обоим:
In [8]: grouped = df.groupby("A")
In [9]: grouped = df.groupby(["A", "B"])
Если у нас также есть MultiIndex по столбцам A и B, мы можем сгруппировать по всем столбцам, кроме указанных.
In [10]: df2 = df.set_index(["A", "B"])
In [11]: grouped = df2.groupby(level=df2.index.names.difference(["B"]))
In [12]: grouped.sum()
Out[12]:
C D
A
bar -1.591710 -1.739537
foo -0.752861 -1.402938
Эти действия разделят DataFrame по его индексу (строкам). Мы также можем разделить по столбцам:
In [13]: def get_letter_type(letter):
....: if letter.lower() in 'aeiou':
....: return 'vowel'
....: else:
....: return 'consonant'
....:
In [14]: grouped = df.groupby(get_letter_type, axis=1)
Объекты pandas Index поддерживают дублируемые значения. Если в операции groupby в качестве ключа группировки используется не уникальный индекс, все значения с одинаковым значением индекса будут рассматриваться как принадлежащие к одной группе, и, следовательно, результат функций агрегации будет содержать только уникальные значения индекса:
In [15]: lst = [1, 2, 3, 1, 2, 3]
In [16]: s = pd.Series([1, 2, 3, 10, 20, 30], lst)
In [17]: grouped = s.groupby(level=0)
In [18]: grouped.first()
Out[18]:
1 1
2 2
3 3
dtype: int64
In [19]: grouped.last()
Out[19]:
1 10
2 20
3 30
dtype: int64
In [20]: grouped.sum()
Out[20]:
1 11
2 22
3 33
dtype: int64
Обратите внимание, что **разделение не происходит**, пока это не потребуется. Создание объекта GroupBy только проверяет, что вы передали допустимое отображение.
Примечание
Многие виды сложных манипуляций с данными могут быть выражены в терминах операций GroupBy (хотя не гарантируется, что это будет наиболее эффективным). Вы можете проявить творческий подход к функциям отображения меток.
Группировка сортировка
По умолчанию ключи групп сортируются во время операции groupby. Однако вы можете передать sort=False для потенциального ускорения:
In [21]: df2 = pd.DataFrame({"X": ["B", "B", "A", "A"], "Y": [1, 2, 3, 4]})
In [22]: df2.groupby(["X"]).sum()
Out[22]:
Y
X
A 7
B 3
In [23]: df2.groupby(["X"], sort=False).sum()
Out[23]:
Y
X
B 3
A 7
Обратите внимание, что groupby сохранит порядок, в котором отсортированы *наблюдения* *внутри* каждой группы. Например, группы, созданные groupby() ниже, расположены в том порядке, в котором они появились в исходном DataFrame:
In [24]: df3 = pd.DataFrame({"X": ["A", "B", "A", "B"], "Y": [1, 4, 3, 2]})
In [25]: df3.groupby(["X"]).get_group("A")
Out[25]:
X Y
0 A 1
2 A 3
In [26]: df3.groupby(["X"]).get_group("B")
Out[26]:
X Y
1 B 4
3 B 2
Введено в версии 1.1.0.
GroupBy dropna
По умолчанию значения NA исключаются из ключей групп во время операции groupby. Однако, если вы хотите включить значения NA в ключи групп, вы можете передать dropna=False для достижения этого.
In [27]: df_list = [[1, 2, 3], [1, None, 4], [2, 1, 3], [1, 2, 2]]
In [28]: df_dropna = pd.DataFrame(df_list, columns=["a", "b", "c"])
In [29]: df_dropna
Out[29]:
a b c
0 1 2.0 3
1 1 NaN 4
2 2 1.0 3
3 1 2.0 2
# Default ``dropna`` is set to True, which will exclude NaNs in keys
In [30]: df_dropna.groupby(by=["b"], dropna=True).sum()
Out[30]:
a c
b
1.0 2 3
2.0 2 5
# In order to allow NaN in keys, set ``dropna`` to False
In [31]: df_dropna.groupby(by=["b"], dropna=False).sum()
Out[31]:
a c
b
1.0 2 3
2.0 2 5
NaN 1 4
Значение аргумента dropna по умолчанию равно True, что означает, что значения NA не включаются в ключи групп.
Атрибуты объекта GroupBy
Атрибут groups — это словарь, ключами которого являются вычисленные уникальные группы, а соответствующие значения — метки осей, принадлежащие каждой группе. В приведенном выше примере у нас есть:
In [32]: df.groupby("A").groups
Out[32]: {'bar': [1, 3, 5], 'foo': [0, 2, 4, 6, 7]}
In [33]: df.groupby(get_letter_type, axis=1).groups
Out[33]: {'consonant': ['B', 'C', 'D'], 'vowel': ['A']}
Вызов стандартной Python-функции len для объекта GroupBy просто возвращает длину словаря groups, поэтому это в основном просто удобство:
In [34]: grouped = df.groupby(["A", "B"])
In [35]: grouped.groups
Out[35]: {('bar', 'one'): [1], ('bar', 'three'): [3], ('bar', 'two'): [5], ('foo', 'one'): [0, 6], ('foo', 'three'): [7], ('foo', 'two'): [2, 4]}
In [36]: len(grouped)
Out[36]: 6
GroupBy — это автодополнение имён столбцов (и других атрибутов):
In [37]: df
Out[37]:
height weight gender
2000-01-01 42.849980 157.500553 male
2000-01-02 49.607315 177.340407 male
2000-01-03 56.293531 171.524640 male
2000-01-04 48.421077 144.251986 female
2000-01-05 46.556882 152.526206 male
2000-01-06 68.448851 168.272968 female
2000-01-07 70.757698 136.431469 male
2000-01-08 58.909500 176.499753 female
2000-01-09 76.435631 174.094104 female
2000-01-10 45.306120 177.540920 male
In [38]: gb = df.groupby("gender")
In [39]: gb.<TAB> # noqa: E225, E999
gb.agg gb.boxplot gb.cummin gb.describe gb.filter gb.get_group gb.height gb.last gb.median gb.ngroups gb.plot gb.rank gb.std gb.transform
gb.aggregate gb.count gb.cumprod gb.dtype gb.first gb.groups gb.hist gb.max gb.min gb.nth gb.prod gb.resample gb.sum gb.var
gb.apply gb.cummax gb.cumsum gb.fillna gb.gender gb.head gb.indices gb.mean gb.name gb.ohlc gb.quantile gb.size gb.tail gb.weight
Группировка с MultiIndex
С иерархически индексированными данными вполне естественно сгруппировать данные по одному из уровней иерархии.
Создадим Series с двухъярусным MultiIndex.
In [40]: arrays = [
....: ["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
....: ["one", "two", "one", "two", "one", "two", "one", "two"],
....: ]
....:
In [41]: index = pd.MultiIndex.from_arrays(arrays, names=["first", "second"])
In [42]: s = pd.Series(np.random.randn(8), index=index)
In [43]: s
Out[43]:
first second
bar one -0.919854
two -0.042379
baz one 1.247642
two -0.009920
foo one 0.290213
two 0.495767
qux one 0.362949
two 1.548106
dtype: float64
Затем мы можем сгруппировать по одному из уровней в s.
In [44]: grouped = s.groupby(level=0)
In [45]: grouped.sum()
Out[45]:
first
bar -0.962232
baz 1.237723
foo 0.785980
qux 1.911055
dtype: float64
Если у MultiIndex указаны имена, вместо номера уровня можно передать их:
In [46]: s.groupby(level="second").sum()
Out[46]:
second
one 0.980950
two 1.991575
dtype: float64
Поддерживается группировка по нескольким уровням.
In [47]: s
Out[47]:
first second third
bar doo one -1.131345
two -0.089329
baz bee one 0.337863
two -0.945867
foo bop one -0.932132
two 1.956030
qux bop one 0.017587
two -0.016692
dtype: float64
In [48]: s.groupby(level=["first", "second"]).sum()
Out[48]:
first second
bar doo -1.220674
baz bee -0.608004
foo bop 1.023898
qux bop 0.000895
dtype: float64
Имена уровней индекса могут быть переданы в качестве ключей.
In [49]: s.groupby(["first", "second"]).sum()
Out[49]:
first second
bar doo -1.220674
baz bee -0.608004
foo bop 1.023898
qux bop 0.000895
dtype: float64
Подробнее о функции sum и агрегации позже.
Группировка DataFrame с уровнями индексов и столбцами
DataFrame может быть сгруппирован по сочетанию столбцов и уровней индексов, указав имена столбцов как строки, а уровни индексов как объекты pd.Grouper.
In [50]: arrays = [
....: ["bar", "bar", "baz", "baz", "foo", "foo", "qux", "qux"],
....: ["one", "two", "one", "two", "one", "two", "one", "two"],
....: ]
....:
In [51]: index = pd.MultiIndex.from_arrays(arrays, names=["first", "second"])
In [52]: df = pd.DataFrame({"A": [1, 1, 1, 1, 2, 2, 3, 3], "B": np.arange(8)}, index=index)
In [53]: df
Out[53]:
A B
first second
bar one 1 0
two 1 1
baz one 1 2
two 1 3
foo one 2 4
two 2 5
qux one 3 6
two 3 7
В следующем примере DataFrame группируется по уровню индекса second и столбцу A.
In [54]: df.groupby([pd.Grouper(level=1), "A"]).sum()
Out[54]:
B
second A
one 1 2
2 4
3 6
two 1 4
2 5
3 7
Уровни индексов также можно указать по имени.
In [55]: df.groupby([pd.Grouper(level="second"), "A"]).sum()
Out[55]:
B
second A
one 1 2
2 4
3 6
two 1 4
2 5
3 7
Имена уровней индексов также можно указать непосредственно в качестве ключей для groupby.
In [56]: df.groupby(["second", "A"]).sum()
Out[56]:
B
second A
one 1 2
2 4
3 6
two 1 4
2 5
3 7
Выбор столбцов DataFrame в GroupBy
После создания объекта GroupBy из DataFrame вы можете захотеть выполнить разные действия для каждого столбца. Таким образом, используя [], аналогично получению столбца из DataFrame, вы можете сделать:
In [57]: df = pd.DataFrame(
....: {
....: "A": ["foo", "bar", "foo", "bar", "foo", "bar", "foo", "foo"],
....: "B": ["one", "one", "two", "three", "two", "two", "one", "three"],
....: "C": np.random.randn(8),
....: "D": np.random.randn(8),
....: }
....: )
....:
In [58]: df
Out[58]:
A B C D
0 foo one -0.575247 1.346061
1 bar one 0.254161 1.511763
2 foo two -1.143704 1.627081
3 bar three 0.215897 -0.990582
4 foo two 1.193555 -0.441652
5 bar two -0.077118 1.211526
6 foo one -0.408530 0.268520
7 foo three -0.862495 0.024580
In [59]: grouped = df.groupby(["A"])
In [60]: grouped_C = grouped["C"]
In [61]: grouped_D = grouped["D"]
Это в основном синтаксический сахар для альтернативного и гораздо более громоздкого способа:
In [62]: df["C"].groupby(df["A"])
Out[62]: <pandas.core.groupby.generic.SeriesGroupBy object at 0x7f2808f6ef40>
Кроме того, этот метод позволяет избежать повторного вычисления внутренней информации о группировке, полученной из переданного ключа.
Проход по группам
Используя объект GroupBy, проход по сгруппированным данным очень естественен и работает аналогично itertools.groupby():
In [63]: grouped = df.groupby('A')
In [64]: for name, group in grouped:
....: print(name)
....: print(group)
....:
bar
A B C D
1 bar one 0.254161 1.511763
3 bar three 0.215897 -0.990582
5 bar two -0.077118 1.211526
foo
A B C D
0 foo one -0.575247 1.346061
2 foo two -1.143704 1.627081
4 foo two 1.193555 -0.441652
6 foo one -0.408530 0.268520
7 foo three -0.862495 0.024580
В случае группировки по нескольким ключам имя группы будет кортежем:
In [65]: for name, group in df.groupby(['A', 'B']):
....: print(name)
....: print(group)
....:
('bar', 'one')
A B C D
1 bar one 0.254161 1.511763
('bar', 'three')
A B C D
3 bar three 0.215897 -0.990582
('bar', 'two')
A B C D
5 bar two -0.077118 1.211526
('foo', 'one')
A B C D
0 foo one -0.575247 1.346061
6 foo one -0.408530 0.268520
('foo', 'three')
A B C D
7 foo three -0.862495 0.02458
('foo', 'two')
A B C D
2 foo two -1.143704 1.627081
4 foo two 1.193555 -0.441652
См. Проход по группам.
Выбор группы
Отдельную группу можно выбрать, используя get_group():
In [66]: grouped.get_group("bar")
Out[66]:
A B C D
1 bar one 0.254161 1.511763
3 bar three 0.215897 -0.990582
5 bar two -0.077118 1.211526
Или для объекта, сгруппированного по нескольким столбцам:
In [67]: df.groupby(["A", "B"]).get_group(("bar", "one"))
Out[67]:
A B C D
1 bar one 0.254161 1.511763
Агрегирование
После создания объекта GroupBy доступны несколько методов для выполнения вычислений на сгруппированных данных. Эти операции аналогичны API агрегирования API агрегирования, API окон API окон и API ресемплирования временных рядов API ресемплирования.
Очевидным является агрегирование с помощью метода aggregate() или, что эквивалентно, agg() метода:
In [68]: grouped = df.groupby("A")
In [69]: grouped[["C", "D"]].aggregate(np.sum)
Out[69]:
C D
A
bar 0.392940 1.732707
foo -1.796421 2.824590
In [70]: grouped = df.groupby(["A", "B"])
In [71]: grouped.aggregate(np.sum)
Out[71]:
C D
A B
bar one 0.254161 1.511763
three 0.215897 -0.990582
two -0.077118 1.211526
foo one -0.983776 1.614581
three -0.862495 0.024580
two 0.049851 1.185429
Как видите, результат агрегирования будет иметь имена групп в качестве нового индекса вдоль сгруппированной оси. В случае нескольких ключей результатом по умолчанию является MultiIndex, хотя это можно изменить с помощью опции as_index.
In [72]: grouped = df.groupby(["A", "B"], as_index=False)
In [73]: grouped.aggregate(np.sum)
Out[73]:
A B C D
0 bar one 0.254161 1.511763
1 bar three 0.215897 -0.990582
2 bar two -0.077118 1.211526
3 foo one -0.983776 1.614581
4 foo three -0.862495 0.024580
5 foo two 0.049851 1.185429
In [74]: df.groupby("A", as_index=False)[["C", "D"]].sum()
Out[74]:
A C D
0 bar 0.392940 1.732707
1 foo -1.796421 2.824590
Обратите внимание, что вы можете использовать функцию DataFrame reset_index для достижения того же результата, так как имена столбцов хранятся в результирующем MultiIndex:
In [75]: df.groupby(["A", "B"]).sum().reset_index()
Out[75]:
A B C D
0 bar one 0.254161 1.511763
1 bar three 0.215897 -0.990582
2 bar two -0.077118 1.211526
3 foo one -0.983776 1.614581
4 foo three -0.862495 0.024580
5 foo two 0.049851 1.185429
Еще один простой пример агрегирования — вычисление размера каждой группы. Это включено в GroupBy как метод size. Он возвращает Series, индекс которого — имена групп, а значения — размеры каждой группы.
In [76]: grouped.size()
Out[76]:
A B size
0 bar one 1
1 bar three 1
2 bar two 1
3 foo one 2
4 foo three 1
5 foo two 2
In [77]: grouped.describe()
Out[77]:
C ... D
count mean std min ... 25% 50% 75% max
0 1.0 0.254161 NaN 0.254161 ... 1.511763 1.511763 1.511763 1.511763
1 1.0 0.215897 NaN 0.215897 ... -0.990582 -0.990582 -0.990582 -0.990582
2 1.0 -0.077118 NaN -0.077118 ... 1.211526 1.211526 1.211526 1.211526
3 2.0 -0.491888 0.117887 -0.575247 ... 0.537905 0.807291 1.076676 1.346061
4 1.0 -0.862495 NaN -0.862495 ... 0.024580 0.024580 0.024580 0.024580
5 2.0 0.024925 1.652692 -1.143704 ... 0.075531 0.592714 1.109898 1.627081
[6 rows x 16 columns]
Еще один пример агрегирования — вычисление количества уникальных значений каждой группы. Это аналогично функции value_counts, за исключением того, что она подсчитывает только уникальные значения.
In [78]: ll = [['foo', 1], ['foo', 2], ['foo', 2], ['bar', 1], ['bar', 1]]
In [79]: df4 = pd.DataFrame(ll, columns=["A", "B"])
In [80]: df4
Out[80]:
A B
0 foo 1
1 foo 2
2 foo 2
3 bar 1
4 bar 1
In [81]: df4.groupby("A")["B"].nunique()
Out[81]:
A
bar 1
foo 2
Name: B, dtype: int64
Примечание
Функции агрегирования не будут возвращать группы, по которым вы агрегируете, если они являются именованными столбцами, если as_index=True, по умолчанию. Сгруппированные столбцы будут индексами возвращаемого объекта.
Передача as_index=False будет возвращать группы, по которым вы агрегируете, если они являются именованными столбцами.
Функции агрегирования — это те, которые уменьшают размер возвращаемых объектов. Некоторые распространенные функции агрегирования приведены ниже:
Функция | Описание |
|---|---|
| Вычисление среднего арифметического групп |
| Вычисление суммы значений группы |
| Вычисление размеров групп |
| Подсчет количества в группе |
| Среднее квадратическое отклонение групп |
| Вычисление дисперсии групп |
| Стандартная ошибка среднего арифметического групп |
| Генерирует описательные статистические данные |
| Вычисление первого значения группы |
| Вычисление последнего значения группы |
| Получение n-го значения или подмножества, если n — список |
| Вычисление минимума значений группы |
| Вычисление максимума значений группы |
Перечисленные выше функции агрегирования исключают значения NaN. Любая функция, которая сводит Series к скалярному значению, является функцией агрегирования и будет работать, тривиальным примером является df.groupby('A').agg(lambda ser: 1). Обратите внимание, что nth() может выполнять роль уменьшающего или фильтрующего элемента, см. здесь.
Применение нескольких функций одновременно
Сгруппированным Series также можно передать список или словарь функций для агрегирования, выдав DataFrame:
In [82]: grouped = df.groupby("A")
In [83]: grouped["C"].agg([np.sum, np.mean, np.std])
Out[83]:
sum mean std
A
bar 0.392940 0.130980 0.181231
foo -1.796421 -0.359284 0.912265
К сгруппированному DataFrame можно передать список функций для применения к каждому столбцу, что даёт результат агрегирования с иерархическим индексом:
In [84]: grouped[["C", "D"]].agg([np.sum, np.mean, np.std])
Out[84]:
C D
sum mean std sum mean std
A
bar 0.392940 0.130980 0.181231 1.732707 0.577569 1.366330
foo -1.796421 -0.359284 0.912265 2.824590 0.564918 0.884785
Полученные агрегации именованные самими функциями. Если нужно переименовать, можно добавить цепочку операций для Series так:
In [85]: (
....: grouped["C"]
....: .agg([np.sum, np.mean, np.std])
....: .rename(columns={"sum": "foo", "mean": "bar", "std": "baz"})
....: )
....:
Out[85]:
foo bar baz
A
bar 0.392940 0.130980 0.181231
foo -1.796421 -0.359284 0.912265
Для сгруппированного DataFrame можно переименовать аналогичным образом:
In [86]: (
....: grouped[["C", "D"]].agg([np.sum, np.mean, np.std]).rename(
....: columns={"sum": "foo", "mean": "bar", "std": "baz"}
....: )
....: )
....:
Out[86]:
C D
foo bar baz foo bar baz
A
bar 0.392940 0.130980 0.181231 1.732707 0.577569 1.366330
foo -1.796421 -0.359284 0.912265 2.824590 0.564918 0.884785
Примечание
В общем случае имена выходных столбцов должны быть уникальными. Нельзя применять одну и ту же функцию (или две функции с одинаковым именем) к одному и тому же столбцу.
In [87]: grouped["C"].agg(["sum", "sum"])
Out[87]:
sum sum
A
bar 0.392940 0.392940
foo -1.796421 -1.796421
pandas разрешает передавать несколько лямбда-функций. В этом случае pandas будет изменять имя (безымянных) лямбда-функций, добавляя _<i> к каждой последующей лямбда-функции.
In [88]: grouped["C"].agg([lambda x: x.max() - x.min(), lambda x: x.median() - x.mean()])
Out[88]:
<lambda_0> <lambda_1>
A
bar 0.331279 0.084917
foo 2.337259 -0.215962
Именованное агрегирование
Новое в версии 0.25.0.
Для поддержки агрегирования по столбцам с контролем имён выходных столбцов pandas поддерживает специальный синтаксис в GroupBy.agg(), известный как «именованное агрегирование», где:
Ключевые слова являются именами выходных столбцов
Значения — кортежи, первый элемент которых — столбец для выбора, а второй — функция агрегирования, применяемая к этому столбцу. pandas предоставляет именованную кортежную функцию
pandas.NamedAggс полями['column', 'aggfunc']для большей ясности аргументов. Как обычно, агрегирование может быть вызываемым объектом или строковым псевдонимом.
In [89]: animals = pd.DataFrame(
....: {
....: "kind": ["cat", "dog", "cat", "dog"],
....: "height": [9.1, 6.0, 9.5, 34.0],
....: "weight": [7.9, 7.5, 9.9, 198.0],
....: }
....: )
....:
In [90]: animals
Out[90]:
kind height weight
0 cat 9.1 7.9
1 dog 6.0 7.5
2 cat 9.5 9.9
3 dog 34.0 198.0
In [91]: animals.groupby("kind").agg(
....: min_height=pd.NamedAgg(column="height", aggfunc="min"),
....: max_height=pd.NamedAgg(column="height", aggfunc="max"),
....: average_weight=pd.NamedAgg(column="weight", aggfunc=np.mean),
....: )
....:
Out[91]:
min_height max_height average_weight
kind
cat 9.1 9.5 8.90
dog 6.0 34.0 102.75
pandas.NamedAgg — это просто namedtuple. Также разрешены обычные кортежи.
In [92]: animals.groupby("kind").agg(
....: min_height=("height", "min"),
....: max_height=("height", "max"),
....: average_weight=("weight", np.mean),
....: )
....:
Out[92]:
min_height max_height average_weight
kind
cat 9.1 9.5 8.90
dog 6.0 34.0 102.75
Если желаемые имена выходных столбцов не являются допустимыми ключевыми словами Python, создайте словарь и распакуйте ключевые аргументы:
In [93]: animals.groupby("kind").agg(
....: **{
....: "total weight": pd.NamedAgg(column="weight", aggfunc=sum)
....: }
....: )
....:
Out[93]:
total weight
kind
cat 17.8
dog 205.5
Дополнительные ключевые аргументы не передаются функциям агрегирования. Только пары (column, aggfunc) должны передаваться как **kwargs. Если функции агрегирования требуют дополнительные аргументы, частично примените их с помощью functools.partial().
Примечание
Для Python 3.5 и более ранних версий порядок **kwargs в функциях не сохранялся. Это означало, что порядок выходных столбцов не был бы постоянным. Для обеспечения постоянного порядка ключи (и, следовательно, выходные столбцы) всегда будут отсортированы для Python 3.5.
Именованное агрегирование также допустимо для агрегирования сгруппированных Series. В этом случае выбора столбцов нет, поэтому значения — это просто функции.
In [94]: animals.groupby("kind").height.agg(
....: min_height="min",
....: max_height="max",
....: )
....:
Out[94]:
min_height max_height
kind
cat 9.1 9.5
dog 6.0 34.0
Применение различных функций к столбцам DataFrame
Передав словарь в aggregate, вы можете применить разное агрегирование к столбцам DataFrame:
In [95]: grouped.agg({"C": np.sum, "D": lambda x: np.std(x, ddof=1)})
Out[95]:
C D
A
bar 0.392940 1.366330
foo -1.796421 0.884785
Имена функций также могут быть строками. Для того, чтобы строка была валидной, она должна быть реализована в GroupBy или доступна через диспетчеризацию:
In [96]: grouped.agg({"C": "sum", "D": "std"})
Out[96]:
C D
A
bar 0.392940 1.366330
foo -1.796421 0.884785
Функции агрегирования с оптимизацией на Cython
Некоторые распространённые агрегации, в настоящее время только sum, mean, std, и sem, имеют оптимизированные реализации на Cython:
In [97]: df.groupby("A")[["C", "D"]].sum()
Out[97]:
C D
A
bar 0.392940 1.732707
foo -1.796421 2.824590
In [98]: df.groupby(["A", "B"]).mean()
Out[98]:
C D
A B
bar one 0.254161 1.511763
three 0.215897 -0.990582
two -0.077118 1.211526
foo one -0.491888 0.807291
three -0.862495 0.024580
two 0.024925 0.592714
Конечно, sum и mean реализованы в объектах pandas, поэтому вышеуказанный код будет работать даже без специальных версий через диспетчеризацию (см. ниже).
Агрегирование с пользовательскими функциями
Пользователи также могут предоставлять свои собственные функции для пользовательского агрегирования. При агрегировании с пользовательской функцией (UDF) UDF не должна изменять предоставленный Series, см. Изменение с пользовательскими функциями (UDF) для получения дополнительной информации.
In [99]: animals.groupby("kind")[["height"]].agg(lambda x: set(x))
Out[99]:
height
kind
cat {9.1, 9.5}
dog {34.0, 6.0}
Результирующий тип данных будет отражать тип данных функции агрегирования. Если результаты разных групп имеют разные типы данных, то общий тип данных будет определяться так же, как и при построении DataFrame.
In [100]: animals.groupby("kind")[["height"]].agg(lambda x: x.astype(int).sum())
Out[100]:
height
kind
cat 18
dog 40
Преобразование
Метод transform возвращает объект, индексированный так же, как и группируемый. Функция преобразования должна:
Возвращать результат, имеющий размер, совпадающий с размером фрагмента группы, или допускающий трансляцию к размеру фрагмента группы (например, скаляр,
grouped.transform(lambda x: x.iloc[-1])).Осуществлять операции по столбцам в фрагменте группы. Преобразование применяется к первому фрагменту группы с использованием chunk.apply.
Не выполнять операции на месте с фрагментом группы. Фрагменты группы должны рассматриваться как неизменяемые, а изменения в фрагменте группы могут привести к непредсказуемым результатам. Например, при использовании
fillna,inplaceдолжно бытьFalse(grouped.transform(lambda x: x.fillna(inplace=False))).(Необязательно) выполнять операции над всем фрагментом группы. Если это поддерживается, используется быстрый путь, начиная со второго фрагмента.
Устарело начиная с версии 1.5.0: При использовании .transform на сгруппированной таблице DataFrame, и если функция преобразования возвращает DataFrame, в настоящее время pandas не выравнивает индекс результата с индексом входных данных. Это поведение устарело, и выравнивание будет выполнено в будущей версии pandas. Вы можете применить .to_numpy() к результату функции преобразования, чтобы избежать выравнивания.
Аналогично Агрегации с пользовательскими функциями, тип данных результата будет отражать тип данных функции преобразования. Если результаты разных групп имеют разные типы данных, то общий тип данных будет определён таким же способом, как и при построении DataFrame.
Предположим, мы хотели стандартизировать данные в каждой группе:
In [101]: index = pd.date_range("10/1/1999", periods=1100)
In [102]: ts = pd.Series(np.random.normal(0.5, 2, 1100), index)
In [103]: ts = ts.rolling(window=100, min_periods=100).mean().dropna()
In [104]: ts.head()
Out[104]:
2000-01-08 0.779333
2000-01-09 0.778852
2000-01-10 0.786476
2000-01-11 0.782797
2000-01-12 0.798110
Freq: D, dtype: float64
In [105]: ts.tail()
Out[105]:
2002-09-30 0.660294
2002-10-01 0.631095
2002-10-02 0.673601
2002-10-03 0.709213
2002-10-04 0.719369
Freq: D, dtype: float64
In [106]: transformed = ts.groupby(lambda x: x.year).transform(
.....: lambda x: (x - x.mean()) / x.std()
.....: )
.....:
Мы ожидали, что результат будет иметь среднее значение 0 и стандартное отклонение 1 внутри каждой группы, что мы можем легко проверить:
# Original Data
In [107]: grouped = ts.groupby(lambda x: x.year)
In [108]: grouped.mean()
Out[108]:
2000 0.442441
2001 0.526246
2002 0.459365
dtype: float64
In [109]: grouped.std()
Out[109]:
2000 0.131752
2001 0.210945
2002 0.128753
dtype: float64
# Transformed Data
In [110]: grouped_trans = transformed.groupby(lambda x: x.year)
In [111]: grouped_trans.mean()
Out[111]:
2000 -4.870756e-16
2001 -1.545187e-16
2002 4.136282e-16
dtype: float64
In [112]: grouped_trans.std()
Out[112]:
2000 1.0
2001 1.0
2002 1.0
dtype: float64
Мы также можем визуально сравнить исходные и преобразованные наборы данных.
In [113]: compare = pd.DataFrame({"Original": ts, "Transformed": transformed})
In [114]: compare.plot()
Out[114]: <AxesSubplot: >
Функции преобразования, имеющие выходные данные меньшей размерности, транслируются для соответствия форме входного массива.
In [115]: ts.groupby(lambda x: x.year).transform(lambda x: x.max() - x.min())
Out[115]:
2000-01-08 0.623893
2000-01-09 0.623893
2000-01-10 0.623893
2000-01-11 0.623893
2000-01-12 0.623893
...
2002-09-30 0.558275
2002-10-01 0.558275
2002-10-02 0.558275
2002-10-03 0.558275
2002-10-04 0.558275
Freq: D, Length: 1001, dtype: float64
В качестве альтернативы, встроенные методы могут быть использованы для получения тех же результатов.
In [116]: max_ts = ts.groupby(lambda x: x.year).transform("max")
In [117]: min_ts = ts.groupby(lambda x: x.year).transform("min")
In [118]: max_ts - min_ts
Out[118]:
2000-01-08 0.623893
2000-01-09 0.623893
2000-01-10 0.623893
2000-01-11 0.623893
2000-01-12 0.623893
...
2002-09-30 0.558275
2002-10-01 0.558275
2002-10-02 0.558275
2002-10-03 0.558275
2002-10-04 0.558275
Freq: D, Length: 1001, dtype: float64
Ещё одним распространённым преобразованием данных является замена пропущенных данных средним значением группы.
In [119]: data_df
Out[119]:
A B C
0 1.539708 -1.166480 0.533026
1 1.302092 -0.505754 NaN
2 -0.371983 1.104803 -0.651520
3 -1.309622 1.118697 -1.161657
4 -1.924296 0.396437 0.812436
.. ... ... ...
995 -0.093110 0.683847 -0.774753
996 -0.185043 1.438572 NaN
997 -0.394469 -0.642343 0.011374
998 -1.174126 1.857148 NaN
999 0.234564 0.517098 0.393534
[1000 rows x 3 columns]
In [120]: countries = np.array(["US", "UK", "GR", "JP"])
In [121]: key = countries[np.random.randint(0, 4, 1000)]
In [122]: grouped = data_df.groupby(key)
# Non-NA count in each group
In [123]: grouped.count()
Out[123]:
A B C
GR 209 217 189
JP 240 255 217
UK 216 231 193
US 239 250 217
In [124]: transformed = grouped.transform(lambda x: x.fillna(x.mean()))
Мы можем проверить, что средние значения групп не изменились в преобразованных данных и что преобразованные данные не содержат значений NaN.
In [125]: grouped_trans = transformed.groupby(key)
In [126]: grouped.mean() # original group means
Out[126]:
A B C
GR -0.098371 -0.015420 0.068053
JP 0.069025 0.023100 -0.077324
UK 0.034069 -0.052580 -0.116525
US 0.058664 -0.020399 0.028603
In [127]: grouped_trans.mean() # transformation did not change group means
Out[127]:
A B C
GR -0.098371 -0.015420 0.068053
JP 0.069025 0.023100 -0.077324
UK 0.034069 -0.052580 -0.116525
US 0.058664 -0.020399 0.028603
In [128]: grouped.count() # original has some missing data points
Out[128]:
A B C
GR 209 217 189
JP 240 255 217
UK 216 231 193
US 239 250 217
In [129]: grouped_trans.count() # counts after transformation
Out[129]:
A B C
GR 228 228 228
JP 267 267 267
UK 247 247 247
US 258 258 258
In [130]: grouped_trans.size() # Verify non-NA count equals group size
Out[130]:
GR 228
JP 267
UK 247
US 258
dtype: int64
Примечание
Некоторые функции автоматически преобразуют входные данные при применении к объекту GroupBy, но возвращают объект той же формы, что и исходный. Передача as_index=False не повлияет на эти методы преобразования.
Например: fillna, ffill, bfill, shift..
In [131]: grouped.ffill()
Out[131]:
A B C
0 1.539708 -1.166480 0.533026
1 1.302092 -0.505754 0.533026
2 -0.371983 1.104803 -0.651520
3 -1.309622 1.118697 -1.161657
4 -1.924296 0.396437 0.812436
.. ... ... ...
995 -0.093110 0.683847 -0.774753
996 -0.185043 1.438572 -0.774753
997 -0.394469 -0.642343 0.011374
998 -1.174126 1.857148 -0.774753
999 0.234564 0.517098 0.393534
[1000 rows x 3 columns]
Операции окон и ресемплирования
Можно использовать resample(), expanding() и rolling() как методы для groupby.
В примере ниже метод rolling() будет применён к образцам столбца B, в зависимости от групп столбца A.
In [132]: df_re = pd.DataFrame({"A": [1] * 10 + [5] * 10, "B": np.arange(20)})
In [133]: df_re
Out[133]:
A B
0 1 0
1 1 1
2 1 2
3 1 3
4 1 4
.. .. ..
15 5 15
16 5 16
17 5 17
18 5 18
19 5 19
[20 rows x 2 columns]
In [134]: df_re.groupby("A").rolling(4).B.mean()
Out[134]:
A
1 0 NaN
1 NaN
2 NaN
3 1.5
4 2.5
...
5 15 13.5
16 14.5
17 15.5
18 16.5
19 17.5
Name: B, Length: 20, dtype: float64
Метод expanding() будет накапливать заданную операцию (sum() в примере) для всех членов каждой конкретной группы.
In [135]: df_re.groupby("A").expanding().sum()
Out[135]:
B
A
1 0 0.0
1 1.0
2 3.0
3 6.0
4 10.0
... ...
5 15 75.0
16 91.0
17 108.0
18 126.0
19 145.0
[20 rows x 1 columns]
Предположим, вы хотите использовать метод resample() для получения ежедневной частоты в каждой группе вашей таблицы DataFrame и хотите заполнить пропущенные значения методом ffill().
In [136]: df_re = pd.DataFrame(
.....: {
.....: "date": pd.date_range(start="2016-01-01", periods=4, freq="W"),
.....: "group": [1, 1, 2, 2],
.....: "val": [5, 6, 7, 8],
.....: }
.....: ).set_index("date")
.....:
In [137]: df_re
Out[137]:
group val
date
2016-01-03 1 5
2016-01-10 1 6
2016-01-17 2 7
2016-01-24 2 8
In [138]: df_re.groupby("group").resample("1D").ffill()
Out[138]:
group val
group date
1 2016-01-03 1 5
2016-01-04 1 5
2016-01-05 1 5
2016-01-06 1 5
2016-01-07 1 5
... ... ...
2 2016-01-20 2 7
2016-01-21 2 7
2016-01-22 2 7
2016-01-23 2 7
2016-01-24 2 8
[16 rows x 2 columns]
Фильтрация
Метод filter возвращает подмножество исходного объекта. Предположим, мы хотим взять только элементы, которые принадлежат группам с суммой группы, большей 2.
In [139]: sf = pd.Series([1, 1, 2, 3, 3, 3])
In [140]: sf.groupby(sf).filter(lambda x: x.sum() > 2)
Out[140]:
3 3
4 3
5 3
dtype: int64
Аргумент filter должен быть функцией, которая, применённая ко всей группе, возвращает True или False.
Ещё одной полезной операцией является фильтрация элементов, которые принадлежат группам, содержащим лишь несколько членов.
In [141]: dff = pd.DataFrame({"A": np.arange(8), "B": list("aabbbbcc")})
In [142]: dff.groupby("B").filter(lambda x: len(x) > 2)
Out[142]:
A B
2 2 b
3 3 b
4 4 b
5 5 b
В качестве альтернативы, вместо удаления проблемных групп, мы можем вернуть подобный индексированный объект, где группы, которые не прошли фильтр, заполнены значениями NaN.
In [143]: dff.groupby("B").filter(lambda x: len(x) > 2, dropna=False)
Out[143]:
A B
0 NaN NaN
1 NaN NaN
2 2.0 b
3 3.0 b
4 4.0 b
5 5.0 b
6 NaN NaN
7 NaN NaN
Для таблиц DataFrame с несколькими столбцами фильтры должны явно указывать столбец в качестве критерия фильтрации.
In [144]: dff["C"] = np.arange(8)
In [145]: dff.groupby("B").filter(lambda x: len(x["C"]) > 2)
Out[145]:
A B C
2 2 b 2
3 3 b 3
4 4 b 4
5 5 b 5
Примечание
Некоторые функции при применении к объекту groupby будут действовать как фильтр на входе, возвращая уменьшенную форму оригинала (и потенциально удаляя группы), но с неизменённым индексом. Передача as_index=False не повлияет на эти методы преобразования.
Например: head, tail.
In [146]: dff.groupby("B").head(2)
Out[146]:
A B C
0 0 a 0
1 1 a 1
2 2 b 2
3 3 b 3
6 6 c 6
7 7 c 7
Вызов методов экземпляров
При выполнении агрегирования или преобразования вы можете просто вызвать метод экземпляра для каждой группы данных. Это довольно просто сделать, передав лямбда-функции:
In [147]: grouped = df.groupby("A")
In [148]: grouped.agg(lambda x: x.std())
Out[148]:
C D
A
bar 0.181231 1.366330
foo 0.912265 0.884785
Но это довольно громоздко и может быть неряшливым, если вам нужно передать дополнительные аргументы. Используя небольшую хитрость метапрограммирования, GroupBy теперь имеет возможность «вызывать» вызовы методов к группам:
In [149]: grouped.std()
Out[149]:
C D
A
bar 0.181231 1.366330
foo 0.912265 0.884785
На самом деле здесь генерируется обертка функции. При вызове она принимает любые переданные аргументы и вызывает функцию с любыми аргументами для каждой группы (в приведённом выше примере функция std). Результаты затем объединяются в стиле agg и transform (на самом деле используется apply для определения склеивания, документация приведена далее). Это позволяет выполнять некоторые операции довольно лаконично:
In [150]: tsdf = pd.DataFrame(
.....: np.random.randn(1000, 3),
.....: index=pd.date_range("1/1/2000", periods=1000),
.....: columns=["A", "B", "C"],
.....: )
.....:
In [151]: tsdf.iloc[::2] = np.nan
In [152]: grouped = tsdf.groupby(lambda x: x.year)
In [153]: grouped.fillna(method="pad")
Out[153]:
A B C
2000-01-01 NaN NaN NaN
2000-01-02 -0.353501 -0.080957 -0.876864
2000-01-03 -0.353501 -0.080957 -0.876864
2000-01-04 0.050976 0.044273 -0.559849
2000-01-05 0.050976 0.044273 -0.559849
... ... ... ...
2002-09-22 0.005011 0.053897 -1.026922
2002-09-23 0.005011 0.053897 -1.026922
2002-09-24 -0.456542 -1.849051 1.559856
2002-09-25 -0.456542 -1.849051 1.559856
2002-09-26 1.123162 0.354660 1.128135
[1000 rows x 3 columns]
В этом примере мы разделили набор временных рядов на годовые фрагменты, а затем независимо вызывали fillna для групп.
Методы nlargest и nsmallest работают с группами в стиле Series:
In [154]: s = pd.Series([9, 8, 7, 5, 19, 1, 4.2, 3.3])
In [155]: g = pd.Series(list("abababab"))
In [156]: gb = s.groupby(g)
In [157]: gb.nlargest(3)
Out[157]:
a 4 19.0
0 9.0
2 7.0
b 1 8.0
3 5.0
7 3.3
dtype: float64
In [158]: gb.nsmallest(3)
Out[158]:
a 6 4.2
2 7.0
0 9.0
b 5 1.0
7 3.3
3 5.0
dtype: float64
Гибкое apply
Некоторые операции над сгруппированными данными могут не вписываться в категории агрегирования или преобразования. Или вы просто хотите, чтобы GroupBy определял, как объединять результаты. Для этого используйте функцию apply, которая может быть заменена как aggregate, так и transform во многих стандартных случаях использования. Однако apply может обрабатывать некоторые исключительные случаи.
Примечание
apply может действовать как функция сокращения, преобразования или фильтрации, в зависимости от того, что именно передано ей. Это может зависеть от переданной функции и от того, что именно вы группируете. Таким образом, сгруппированные столбцы могут быть включены в выходные данные, а также устанавливать индексы.
In [159]: df
Out[159]:
A B C D
0 foo one -0.575247 1.346061
1 bar one 0.254161 1.511763
2 foo two -1.143704 1.627081
3 bar three 0.215897 -0.990582
4 foo two 1.193555 -0.441652
5 bar two -0.077118 1.211526
6 foo one -0.408530 0.268520
7 foo three -0.862495 0.024580
In [160]: grouped = df.groupby("A")
# could also just call .describe()
In [161]: grouped["C"].apply(lambda x: x.describe())
Out[161]:
A
bar count 3.000000
mean 0.130980
std 0.181231
min -0.077118
25% 0.069390
...
foo min -1.143704
25% -0.862495
50% -0.575247
75% -0.408530
max 1.193555
Name: C, Length: 16, dtype: float64
Размер возвращаемого результата также может изменяться:
In [162]: grouped = df.groupby('A')['C']
In [163]: def f(group):
.....: return pd.DataFrame({'original': group,
.....: 'demeaned': group - group.mean()})
.....:
apply на Series может работать со значениями, возвращаемыми функцией, которая сама является Series, и, возможно, увеличит результат до DataFrame:
In [164]: def f(x):
.....: return pd.Series([x, x ** 2], index=["x", "x^2"])
.....:
In [165]: s = pd.Series(np.random.rand(5))
In [166]: s
Out[166]:
0 0.321438
1 0.493496
2 0.139505
3 0.910103
4 0.194158
dtype: float64
In [167]: s.apply(f)
Out[167]:
x x^2
0 0.321438 0.103323
1 0.493496 0.243538
2 0.139505 0.019462
3 0.910103 0.828287
4 0.194158 0.037697
Управление размещением сгруппированных столбцов с помощью group_keys
Примечание
Если group_keys=True указан при вызове groupby(), функции, переданные в apply, которые возвращают объекты с аналогичными индексами, будут иметь ключи групп, добавленные к индексу результата. Предыдущие версии pandas добавляли ключи групп только тогда, когда результат от применённой функции имел другой индекс, чем вход. Если group_keys не указан, ключи групп не будут добавлены для выходных данных с одинаковыми индексами. В будущем это поведение изменится, чтобы всегда учитывать group_keys, по умолчанию True.
Изменено в версии 1.5.0.
Чтобы контролировать включение сгруппированных столбцов в индексы, можно использовать аргумент group_keys. Сравните
In [168]: df.groupby("A", group_keys=True).apply(lambda x: x)
Out[168]:
A B C D
A
bar 1 bar one 0.254161 1.511763
3 bar three 0.215897 -0.990582
5 bar two -0.077118 1.211526
foo 0 foo one -0.575247 1.346061
2 foo two -1.143704 1.627081
4 foo two 1.193555 -0.441652
6 foo one -0.408530 0.268520
7 foo three -0.862495 0.024580
с
In [169]: df.groupby("A", group_keys=False).apply(lambda x: x)
Out[169]:
A B C D
0 foo one -0.575247 1.346061
1 bar one 0.254161 1.511763
2 foo two -1.143704 1.627081
3 bar three 0.215897 -0.990582
4 foo two 1.193555 -0.441652
5 bar two -0.077118 1.211526
6 foo one -0.408530 0.268520
7 foo three -0.862495 0.024580
Аналогично Агрегации с пользовательскими функциями, тип данных результата будет отражать тип данных функции apply. Если результаты разных групп имеют разные типы данных, то общий тип данных будет определен таким же образом, как при построении DataFrame.
Ускоренные вычисления Numba
В версии 1.1.
Если Numba установлена как необязательная зависимость, методы transform и aggregate поддерживают аргументы engine='numba' и engine_kwargs. См. улучшение производительности с помощью Numba для общего использования аргументов и рекомендаций по производительности.
Подпись функции должна начинаться с values, index точно, так как данные каждой группы будут переданы в values, а индекс группы — в index.
Предупреждение
При использовании engine='numba' внутренней «обратной связи» не будет. Данные группы и индекс группы будут переданы в качестве массивов NumPy в определенную пользователем функцию JIT, и никакие альтернативные попытки выполнения не будут предприниматься.
Другие полезные функции
Автоматическое исключение «вредных» столбцов
Рассмотрим пример DataFrame, который мы рассматривали:
In [170]: df
Out[170]:
A B C D
0 foo one -0.575247 1.346061
1 bar one 0.254161 1.511763
2 foo two -1.143704 1.627081
3 bar three 0.215897 -0.990582
4 foo two 1.193555 -0.441652
5 bar two -0.077118 1.211526
6 foo one -0.408530 0.268520
7 foo three -0.862495 0.024580
Предположим, мы хотим вычислить стандартное отклонение, сгруппированное по столбцу A. Существует небольшая проблема, а именно, что нас не интересуют данные в столбце B. Мы называем это «вредным» столбцом. Вы можете избежать вредных столбцов, указав numeric_only=True:
In [171]: df.groupby("A").std(numeric_only=True)
Out[171]:
C D
A
bar 0.181231 1.366330
foo 0.912265 0.884785
Обратите внимание, что df.groupby('A').colname.std(). более эффективен, чем df.groupby('A').std().colname, поэтому если результат функции агрегирования интересен только для одного столбца (здесь colname ), он может быть отфильтрован до применения функции агрегирования.
Примечание
Любой столбец типа object, даже если он содержит числовые значения, такие как объекты Decimal , считается «вредным» столбцом. Они автоматически исключаются из функций агрегирования в groupby.
Если вы хотите включить столбцы с десятичными знаками или объектами в агрегирование с другими типами данных, не являющимися вредными, вы должны сделать это явно.
Предупреждение
Автоматическое удаление вредных столбцов устарело и будет удалено в будущей версии pandas. Если будут включены столбцы, которые нельзя обработать, pandas вместо этого выдаст ошибку. Чтобы этого избежать, выберите столбцы, которые вы хотите обработать, или укажите numeric_only=True.
In [172]: from decimal import Decimal
In [173]: df_dec = pd.DataFrame(
.....: {
.....: "id": [1, 2, 1, 2],
.....: "int_column": [1, 2, 3, 4],
.....: "dec_column": [
.....: Decimal("0.50"),
.....: Decimal("0.15"),
.....: Decimal("0.25"),
.....: Decimal("0.40"),
.....: ],
.....: }
.....: )
.....:
# Decimal columns can be sum'd explicitly by themselves...
In [174]: df_dec.groupby(["id"])[["dec_column"]].sum()
Out[174]:
dec_column
id
1 0.75
2 0.55
# ...but cannot be combined with standard data types or they will be excluded
In [175]: df_dec.groupby(["id"])[["int_column", "dec_column"]].sum()
Out[175]:
int_column
id
1 4
2 6
# Use .agg function to aggregate over standard and "nuisance" data types
# at the same time
In [176]: df_dec.groupby(["id"]).agg({"int_column": "sum", "dec_column": "sum"})
Out[176]:
int_column dec_column
id
1 4 0.75
2 6 0.55
Обработка (не)наблюдаемых категориальных значений
При использовании группировщика Categorical (в качестве единственного группировщика или в составе нескольких группировщиков) ключевое слово observed управляет тем, следует ли возвращать декартово произведение всех возможных значений группировщиков (observed=False) или только тех, которые наблюдаются (observed=True).
Показать все значения:
In [177]: pd.Series([1, 1, 1]).groupby(
.....: pd.Categorical(["a", "a", "a"], categories=["a", "b"]), observed=False
.....: ).count()
.....:
Out[177]:
a 3
b 0
dtype: int64
Показать только наблюдаемые значения:
In [178]: pd.Series([1, 1, 1]).groupby(
.....: pd.Categorical(["a", "a", "a"], categories=["a", "b"]), observed=True
.....: ).count()
.....:
Out[178]:
a 3
dtype: int64
Возвращаемый тип данных сгруппированных данных всегда включает все категории, которые были сгруппированы.
In [179]: s = (
.....: pd.Series([1, 1, 1])
.....: .groupby(pd.Categorical(["a", "a", "a"], categories=["a", "b"]), observed=False)
.....: .count()
.....: )
.....:
In [180]: s.index.dtype
Out[180]: CategoricalDtype(categories=['a', 'b'], ordered=False)
Обработка групп NA и NaT
Если в ключе группировки есть значения NaN или NaT, они будут автоматически исключены. Другими словами, никогда не будет группы «NA» или «NaT». В более старых версиях pandas это было не так, но пользователи обычно игнорировали группу NA (и её поддержка представляла трудности реализации).
Группировка с упорядоченными факторами
Категориальные переменные, представленные экземплярами класса pandas Categorical , могут использоваться в качестве ключей группировки. В этом случае порядок уровней сохранится:
In [181]: data = pd.Series(np.random.randn(100))
In [182]: factor = pd.qcut(data, [0, 0.25, 0.5, 0.75, 1.0])
In [183]: data.groupby(factor).mean()
Out[183]:
(-2.645, -0.523] -1.362896
(-0.523, 0.0296] -0.260266
(0.0296, 0.654] 0.361802
(0.654, 2.21] 1.073801
dtype: float64
Группировка с указанием группировщика
Возможно, вам потребуется указать немного больше данных для правильной группировки. Вы можете использовать pd.Grouper для этого локального управления.
In [184]: import datetime
In [185]: df = pd.DataFrame(
.....: {
.....: "Branch": "A A A A A A A B".split(),
.....: "Buyer": "Carl Mark Carl Carl Joe Joe Joe Carl".split(),
.....: "Quantity": [1, 3, 5, 1, 8, 1, 9, 3],
.....: "Date": [
.....: datetime.datetime(2013, 1, 1, 13, 0),
.....: datetime.datetime(2013, 1, 1, 13, 5),
.....: datetime.datetime(2013, 10, 1, 20, 0),
.....: datetime.datetime(2013, 10, 2, 10, 0),
.....: datetime.datetime(2013, 10, 1, 20, 0),
.....: datetime.datetime(2013, 10, 2, 10, 0),
.....: datetime.datetime(2013, 12, 2, 12, 0),
.....: datetime.datetime(2013, 12, 2, 14, 0),
.....: ],
.....: }
.....: )
.....:
In [186]: df
Out[186]:
Branch Buyer Quantity Date
0 A Carl 1 2013-01-01 13:00:00
1 A Mark 3 2013-01-01 13:05:00
2 A Carl 5 2013-10-01 20:00:00
3 A Carl 1 2013-10-02 10:00:00
4 A Joe 8 2013-10-01 20:00:00
5 A Joe 1 2013-10-02 10:00:00
6 A Joe 9 2013-12-02 12:00:00
7 B Carl 3 2013-12-02 14:00:00
Группировка по конкретному столбцу с желаемой частотой. Это похоже на пересэмплирование.
In [187]: df.groupby([pd.Grouper(freq="1M", key="Date"), "Buyer"])[["Quantity"]].sum()
Out[187]:
Quantity
Date Buyer
2013-01-31 Carl 1
Mark 3
2013-10-31 Carl 6
Joe 9
2013-12-31 Carl 3
Joe 9
У вас есть неоднозначное указание, так как у вас есть именованный индекс и столбец, которые могут быть потенциальными группировщиками.
In [188]: df = df.set_index("Date")
In [189]: df["Date"] = df.index + pd.offsets.MonthEnd(2)
In [190]: df.groupby([pd.Grouper(freq="6M", key="Date"), "Buyer"])[["Quantity"]].sum()
Out[190]:
Quantity
Date Buyer
2013-02-28 Carl 1
Mark 3
2014-02-28 Carl 9
Joe 18
In [191]: df.groupby([pd.Grouper(freq="6M", level="Date"), "Buyer"])[["Quantity"]].sum()
Out[191]:
Quantity
Date Buyer
2013-01-31 Carl 1
Mark 3
2014-01-31 Carl 9
Joe 18
Получение первых строк каждой группы
Как и для DataFrame или Series, вы можете вызвать head и tail для groupby:
In [192]: df = pd.DataFrame([[1, 2], [1, 4], [5, 6]], columns=["A", "B"])
In [193]: df
Out[193]:
A B
0 1 2
1 1 4
2 5 6
In [194]: g = df.groupby("A")
In [195]: g.head(1)
Out[195]:
A B
0 1 2
2 5 6
In [196]: g.tail(1)
Out[196]:
A B
1 1 4
2 5 6
Это показывает первые или последние n строк каждой группы.
Получение n-й строки каждой группы
Чтобы выбрать из DataFrame или Series n-й элемент, используйте nth(). Это метод сокращения, и он вернёт одну строку (или не вернёт строки) на группу, если вы передадите целое число для n:
In [197]: df = pd.DataFrame([[1, np.nan], [1, 4], [5, 6]], columns=["A", "B"])
In [198]: g = df.groupby("A")
In [199]: g.nth(0)
Out[199]:
B
A
1 NaN
5 6.0
In [200]: g.nth(-1)
Out[200]:
B
A
1 4.0
5 6.0
In [201]: g.nth(1)
Out[201]:
B
A
1 4.0
Если вы хотите выбрать n-й ненулевой элемент, используйте ключевое слово dropna. Для DataFrame это должно быть либо 'any', либо 'all', так же, как вы бы передавали значения для dropna:
# nth(0) is the same as g.first()
In [202]: g.nth(0, dropna="any")
Out[202]:
B
A
1 4.0
5 6.0
In [203]: g.first()
Out[203]:
B
A
1 4.0
5 6.0
# nth(-1) is the same as g.last()
In [204]: g.nth(-1, dropna="any") # NaNs denote group exhausted when using dropna
Out[204]:
B
A
1 4.0
5 6.0
In [205]: g.last()
Out[205]:
B
A
1 4.0
5 6.0
In [206]: g.B.nth(0, dropna="all")
Out[206]:
A
1 4.0
5 6.0
Name: B, dtype: float64
Как и в других методах, передача as_index=False, приведет к фильтрации, которая вернёт сгруппированную строку.
In [207]: df = pd.DataFrame([[1, np.nan], [1, 4], [5, 6]], columns=["A", "B"])
In [208]: g = df.groupby("A", as_index=False)
In [209]: g.nth(0)
Out[209]:
A B
0 1 NaN
2 5 6.0
In [210]: g.nth(-1)
Out[210]:
A B
1 1 4.0
2 5 6.0
Вы также можете выбрать несколько строк из каждой группы, указав несколько значений nth как список целых чисел.
In [211]: business_dates = pd.date_range(start="4/1/2014", end="6/30/2014", freq="B")
In [212]: df = pd.DataFrame(1, index=business_dates, columns=["a", "b"])
# get the first, 4th, and last date index for each month
In [213]: df.groupby([df.index.year, df.index.month]).nth([0, 3, -1])
Out[213]:
a b
2014 4 1 1
4 1 1
4 1 1
5 1 1
5 1 1
5 1 1
6 1 1
6 1 1
6 1 1
Перечисление элементов группы
Чтобы увидеть порядок, в котором каждая строка появляется в своей группе, используйте метод cumcount:
In [214]: dfg = pd.DataFrame(list("aaabba"), columns=["A"])
In [215]: dfg
Out[215]:
A
0 a
1 a
2 a
3 b
4 b
5 a
In [216]: dfg.groupby("A").cumcount()
Out[216]:
0 0
1 1
2 2
3 0
4 1
5 3
dtype: int64
In [217]: dfg.groupby("A").cumcount(ascending=False)
Out[217]:
0 3
1 2
2 1
3 1
4 0
5 0
dtype: int64
Перечисление групп
Чтобы увидеть порядок групп (в отличие от порядка строк внутри группы, заданного cumcount), вы можете использовать ngroup().
Обратите внимание, что числа, присваиваемые группам, соответствуют порядку, в котором группы будут видны при итерации по объекту groupby, а не порядку, в котором они впервые наблюдаются.
In [218]: dfg = pd.DataFrame(list("aaabba"), columns=["A"])
In [219]: dfg
Out[219]:
A
0 a
1 a
2 a
3 b
4 b
5 a
In [220]: dfg.groupby("A").ngroup()
Out[220]:
0 0
1 0
2 0
3 1
4 1
5 0
dtype: int64
In [221]: dfg.groupby("A").ngroup(ascending=False)
Out[221]:
0 1
1 1
2 1
3 0
4 0
5 1
dtype: int64
Построение графиков
GroupBy также работает с некоторыми методами построения графиков. Например, предположим, что мы подозреваем, что некоторые функции в DataFrame могут отличаться по группам, в этом случае значения в столбце 1, где группа «B», в среднем на 3 больше.
In [222]: np.random.seed(1234)
In [223]: df = pd.DataFrame(np.random.randn(50, 2))
In [224]: df["g"] = np.random.choice(["A", "B"], size=50)
In [225]: df.loc[df["g"] == "B", 1] += 3
Мы можем легко визуализировать это с помощью boxplot:
In [226]: df.groupby("g").boxplot()
Out[226]:
A AxesSubplot(0.1,0.15;0.363636x0.75)
B AxesSubplot(0.536364,0.15;0.363636x0.75)
dtype: object
Результат вызова boxplot — словарь, ключами которого являются значения нашего столбца группировки g («A» и «B»). Значения результирующего словаря можно контролировать с помощью ключевого слова return_type метода boxplot . Подробнее см. в документации по визуализации.
Предупреждение
По историческим причинам, df.groupby("g").boxplot() не эквивалентно df.boxplot(by="g") . См. здесь для объяснения.
Цепочки вызовов функций
Аналогично функциональности, предоставляемой DataFrame и Series , функции, которые принимают объекты GroupBy , могут быть объединены с помощью метода pipe , что позволит создать более чистый и читаемый синтаксис. Более подробно о .pipe см. здесь.
Сочетание .groupby и .pipe часто бывает полезно, когда вам нужно повторно использовать объекты GroupBy.
Например, представьте DataFrame со столбцами для магазинов, продуктов, дохода и количества проданных товаров. Мы хотим выполнить групповое вычисление цен (т. е. доход/количество) на магазин и на продукт. Мы могли бы сделать это в многошаговой операции, но выражение в виде конвейера может сделать код более читаемым. Сначала зададим данные:
In [227]: n = 1000
In [228]: df = pd.DataFrame(
.....: {
.....: "Store": np.random.choice(["Store_1", "Store_2"], n),
.....: "Product": np.random.choice(["Product_1", "Product_2"], n),
.....: "Revenue": (np.random.random(n) * 50 + 10).round(2),
.....: "Quantity": np.random.randint(1, 10, size=n),
.....: }
.....: )
.....:
In [229]: df.head(2)
Out[229]:
Store Product Revenue Quantity
0 Store_2 Product_1 26.12 1
1 Store_2 Product_1 28.86 1
Теперь, чтобы найти цены на магазин/продукт, мы можем просто сделать:
In [230]: (
.....: df.groupby(["Store", "Product"])
.....: .pipe(lambda grp: grp.Revenue.sum() / grp.Quantity.sum())
.....: .unstack()
.....: .round(2)
.....: )
.....:
Out[230]:
Product Product_1 Product_2
Store
Store_1 6.82 7.05
Store_2 6.30 6.64
Конвейер также может быть выразителен, когда вы хотите передать сгруппированный объект в произвольную функцию, например:
In [231]: def mean(groupby):
.....: return groupby.mean()
.....:
In [232]: df.groupby(["Store", "Product"]).pipe(mean)
Out[232]:
Revenue Quantity
Store Product
Store_1 Product_1 34.622727 5.075758
Product_2 35.482815 5.029630
Store_2 Product_1 32.972837 5.237589
Product_2 34.684360 5.224000
где mean принимает объект GroupBy и находит среднее значение столбцов Доход и Количество соответственно для каждой комбинации Магазин-Продукт. Функция mean может быть любой функцией, принимающей объект GroupBy; .pipe передаст объект GroupBy в качестве параметра в указанную вами функцию.
Примеры
Перегруппировка по фактору
Перегруппируйте столбцы DataFrame по их сумме и просуммируйте агрегированные значения.
In [233]: df = pd.DataFrame({"a": [1, 0, 0], "b": [0, 1, 0], "c": [1, 0, 0], "d": [2, 3, 4]})
In [234]: df
Out[234]:
a b c d
0 1 0 1 2
1 0 1 0 3
2 0 0 0 4
In [235]: df.groupby(df.sum(), axis=1).sum()
Out[235]:
1 9
0 2 2
1 1 3
2 0 4
Факторизация нескольких столбцов
Используя ngroup(), мы можем извлечь информацию о группах аналогичным способом, как и в factorize() (как описано далее в API переструктурирования), но это применяется естественным образом к нескольким столбцам смешанного типа и различным источникам. Это может быть полезно как промежуточный категориальный шаг в обработке, когда взаимоотношения между строками групп важнее их содержимого, или как входной параметр алгоритма, который принимает только целочисленное кодирование. (Дополнительную информацию о поддержке pandas для полных категориальных данных см. в Введении в категориальные данные и документации API.)
In [236]: dfg = pd.DataFrame({"A": [1, 1, 2, 3, 2], "B": list("aaaba")})
In [237]: dfg
Out[237]:
A B
0 1 a
1 1 a
2 2 a
3 3 b
4 2 a
In [238]: dfg.groupby(["A", "B"]).ngroup()
Out[238]:
0 0
1 0
2 1
3 2
4 1
dtype: int64
In [239]: dfg.groupby(["A", [0, 0, 0, 1, 1]]).ngroup()
Out[239]:
0 0
1 0
2 1
3 3
4 2
dtype: int64
Группировка по индексатору для «перевыборки» данных
Перевыборка создает новые гипотетические выборки (перевыборки) из уже существующих наблюдаемых данных или из модели, которая генерирует данные. Эти новые выборки похожи на предварительно существующие выборки.
Для того, чтобы перевыборка работала с индексами, которые не являются datetimelike, можно использовать следующий метод.
В следующих примерах df.index // 5 возвращает двоичный массив, который используется для определения того, что выбирается для операции groupby.
Примечание
В примере ниже показано, как можно выполнить выборочное усреднение путем консолидации выборок в меньшее количество выборок. Здесь, используя df.index // 5, мы агрегируем выборки в ячейки. Применяя функцию std(), мы агрегируем информацию, содержащуюся во многих выборках, в небольшой подмножество значений, которые представляют собой их стандартное отклонение, тем самым уменьшая количество выборок.
In [240]: df = pd.DataFrame(np.random.randn(10, 2))
In [241]: df
Out[241]:
0 1
0 -0.793893 0.321153
1 0.342250 1.618906
2 -0.975807 1.918201
3 -0.810847 -1.405919
4 -1.977759 0.461659
5 0.730057 -1.316938
6 -0.751328 0.528290
7 -0.257759 -1.081009
8 0.505895 -1.701948
9 -1.006349 0.020208
In [242]: df.index // 5
Out[242]: Int64Index([0, 0, 0, 0, 0, 1, 1, 1, 1, 1], dtype='int64')
In [243]: df.groupby(df.index // 5).std()
Out[243]:
0 1
0 0.823647 1.312912
1 0.760109 0.942941
Возвращение Series для распространения имен
Группировка столбцов DataFrame, вычисление набора метрик и возвращение именованной Series. Имя Series используется в качестве имени для индекса столбца. Это особенно полезно в сочетании с операциями переструктурирования, такими как стэкинг, в котором имя индекса столбца будет использоваться в качестве имени вставленного столбца:
In [244]: df = pd.DataFrame(
.....: {
.....: "a": [0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2],
.....: "b": [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1],
.....: "c": [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0],
.....: "d": [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1],
.....: }
.....: )
.....:
In [245]: def compute_metrics(x):
.....: result = {"b_sum": x["b"].sum(), "c_mean": x["c"].mean()}
.....: return pd.Series(result, name="metrics")
.....:
In [246]: result = df.groupby("a").apply(compute_metrics)
In [247]: result
Out[247]:
metrics b_sum c_mean
a
0 2.0 0.5
1 2.0 0.5
2 2.0 0.5
In [248]: result.stack()
Out[248]:
a metrics
0 b_sum 2.0
c_mean 0.5
1 b_sum 2.0
c_mean 0.5
2 b_sum 2.0
c_mean 0.5
dtype: float64
© 2008–2022, AQR Capital Management, LLC, Lambda Foundry, Inc. and PyData Development Team
Licensed under the 3-clause BSD License.
https://pandas.pydata.org/pandas-docs/version/1.5.0/user_guide/groupby.html