pandas GroupBy: Ваше руководство по группировке данных в Python

Оглавление

Смотрите сейчас, к этому уроку прилагается соответствующий видеокурс, созданный командой Real Python. Посмотрите его вместе с письменным руководством, чтобы углубить свое понимание: pandas GroupBy: Группировка реальных данных в Python

Метод pandas .groupby() позволяет эффективно анализировать и преобразовывать наборы данных при работе с данными в Python. С помощью df.groupby() вы можете разделить фрейм данных на группы на основе значений столбцов, применить функции к каждой группе и объединить результаты в новый фрейм данных. Этот метод необходим для решения таких задач, как агрегирование, фильтрация и преобразование сгруппированных данных.

К концу этого урока вы поймете, что:

  • Вызов .groupby("column_name") разбивает фрейм данных на группы, применяет функцию к каждой группе и объединяет результаты.
  • Чтобы сгруппировать по нескольким столбцам, вы можете передать список имен столбцов в .groupby().
  • Распространенные методы агрегации в pandas включают .sum(), .mean(), и .count().
  • Вы можете использовать пользовательские функции с помощью pandas .groupby() для выполнения определенных операций с группами.

В этом руководстве предполагается, что у вас есть некоторый опыт работы с самим pandas, включая то, как считывать CSV-файлы в память в виде объектов pandas с помощью read_csv(). Если вам нужно освежить информацию, ознакомьтесь с Чтение CSV-файлов с помощью pandas и pandas: как читать и записывать файлы.

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

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

Предварительные условия

Прежде чем продолжить, убедитесь, что у вас установлена последняя версия pandas, доступная в новой виртуальной среде :

PS> python -m venv venv
PS> venv\Scripts\activate
(venv) PS> python -m pip install pandas



$ python3 -m venv venv
$ source venv/bin/activate
(venv) $ python -m pip install pandas


В этом руководстве вы сосредоточитесь на трех наборах данных:

  1. Набор данных Конгресса США содержит общедоступную информацию о прошлых членах Конгресса и иллюстрирует несколько фундаментальных возможностей .groupby().
  2. Набор данных о качестве воздуха содержит периодические показания газового датчика. Это позволит вам работать с числами с плавающей точкой и данными временных рядов.
  3. Набор данных агрегатора новостей содержит метаданные о нескольких сотнях тысяч новостных статей. Вы будете работать со строками и выполнять редактирование текста с помощью .groupby().

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

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

Как только вы загрузите файл .zip, распакуйте его в папку с именем groupby-data/ в вашем текущем каталоге. Прежде чем читать дальше, убедитесь, что ваше дерево каталогов выглядит следующим образом:

./
│
└── groupby-data/
    │
    ├── legislators-historical.csv
    ├── airqual.csv
    └── news.csv

Установив pandas, активировав вашу виртуальную среду и загрузив наборы данных, вы готовы к работе!

Пример 1: Набор данных Конгресса США

Вы сразу перейдете к делу, проанализировав набор данных об исторических членах Конгресса. Вы можете прочитать CSV-файл в формате pandas DataFrame с помощью read_csv():

pandas_legislators.py
import pandas as pd

dtypes = {
    "first_name": "category",
    "gender": "category",
    "type": "category",
    "state": "category",
    "party": "category",
}
df = pd.read_csv(
    "groupby-data/legislators-historical.csv",
    dtype=dtypes,
    usecols=list(dtypes) + ["birthday", "last_name"],
    parse_dates=["birthday"]
)

Набор данных содержит имена и фамилии членов, дату рождения, пол, тип ("rep" для Палаты представителей или "sen" для Сената), штат США и политическую партию. Вы можете использовать df.tail() для просмотра последних нескольких строк набора данных:

>>> from pandas_legislators import df
>>> df.tail()
      last_name first_name   birthday gender type state       party
11970   Garrett     Thomas 1972-03-27      M  rep    VA  Republican
11971    Handel      Karen 1962-04-18      F  rep    GA  Republican
11972     Jones     Brenda 1959-10-24      F  rep    MI    Democrat
11973    Marino        Tom 1952-08-15      M  rep    PA  Republican
11974     Jones     Walter 1943-02-10      M  rep    NC  Republican


В DataFrame используются категориальные типы для экономии пространства:

>>> df.dtypes
last_name             object
first_name          category
birthday      datetime64[ns]
gender              category
type                category
state               category
party               category
dtype: object


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

Группа Hello, World! из группы панд

Теперь, когда вы ознакомились с набором данных, вы начнете с Hello, World! для операции pandas GroupBy. Каково количество членов Конгресса в разбивке по штатам за всю историю сбора данных? В SQL вы могли бы найти этот ответ с помощью инструкции SELECT:

SELECT state, count(name)
FROM df
GROUP BY state
ORDER BY state;


Вот почти аналогичный показатель в pandas:

>>> n_by_state = df.groupby("state")["last_name"].count()
>>> n_by_state.head(10)
state
AK     16
AL    206
AR    117
AS      2
AZ     48
CA    361
CO     90
CT    240
DC      2
DE     97
Name: last_name, dtype: int64


Вы вызываете .groupby() и передаете имя столбца, по которому вы хотите сгруппировать, а именно "state". Затем вы используете ["last_name"], чтобы указать столбцы, для которых вы хотите выполнить фактическую агрегацию.

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

  • A list из нескольких имен столбцов
  • А dict или панды Series
  • Массив NumPy или pandas Index, или повторяющийся массив, подобный этому

Вот пример совместной группировки по двум столбцам, в которой отображается количество членов Конгресса в разбивке по штатам, а затем по полу:

>>> df.groupby(["state", "gender"])["last_name"].count()
state  gender
AK     F           0
       M          16
AL     F           3
       M         203
AR     F           5
                ...
WI     M         196
WV     F           1
       M         119
WY     F           2
       M          38
Name: last_name, Length: 116, dtype: int64


Аналогичный SQL-запрос будет выглядеть следующим образом:

SELECT state, gender, count(name)
FROM df
GROUP BY state, gender
ORDER BY state, gender;


Как вы увидите далее, .groupby() и сопоставимые операторы SQL являются близкими родственниками, но часто функционально не идентичны.

pandas GroupBy против SQL

Сейчас самое время рассказать об одном существенном различии между операцией pandas GroupBy и SQL-запросом, описанным выше. Результирующий набор SQL-запроса содержит три столбца:

  1. state
  2. gender
  3. count

В версии для pandas сгруппированные столбцы помещаются в MultiIndex из результирующих Series по умолчанию:

>>> n_by_state_gender = df.groupby(["state", "gender"])["last_name"].count()
>>> type(n_by_state_gender)
<class 'pandas.core.series.Series'>
>>> n_by_state_gender.index[:5]
MultiIndex([('AK', 'M'),
            ('AL', 'F'),
            ('AL', 'M'),
            ('AR', 'F'),
            ('AR', 'M')],
           names=['state', 'gender'])


Чтобы более точно имитировать результат SQL и поместить сгруппированные столбцы обратно в столбцы результата, вы можете использовать as_index=False:

>>> df.groupby(["state", "gender"], as_index=False)["last_name"].count()
    state gender  last_name
0      AK      F          0
1      AK      M         16
2      AL      F          3
3      AL      M        203
4      AR      F          5
..    ...    ...        ...
111    WI      M        196
112    WV      F          1
113    WV      M        119
114    WY      F          2
115    WY      M         38

[116 rows x 3 columns]


В результате получается DataFrame с тремя столбцами и символом RangeIndex, вместо Series с символом MultiIndex. Короче говоря, использование as_index=False сделает ваш результат более похожим на вывод SQL по умолчанию для аналогичной операции.

Примечание: В df.groupby(["state", "gender"])["last_name"].count() вы также могли бы использовать .size() вместо .count(), поскольку вы знаете, что там нет фамилий NaN. Использование .count() исключает NaN значений, в то время как .size() включает все, NaN или нет.

Также обратите внимание, что приведенные выше SQL-запросы явно используют ORDER BY, в то время как .groupby() этого не делает. Это потому, что .groupby() делает это по умолчанию с помощью своего параметра sort, который равен True, если вы не укажете иное:

>>> # Don't sort results by the sort keys
>>> df.groupby("state", sort=False)["last_name"].count()
state
DE      97
VA     432
SC     251
MD     305
PA    1053
      ...
AK      16
PI      13
VI       4
GU       4
AS       2
Name: last_name, dtype: int64


Далее вы перейдете к объекту, который на самом деле создает .groupby().

Как работает pandas GroupBy

Прежде чем углубляться в детали, сделайте шаг назад и посмотрите на сам .groupby():

>>> by_state = df.groupby("state")
>>> print(by_state)
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x107293278>


Что такое DataFrameGroupBy? Значение .__str__(), которое отображается в функции печати, не дает вам много информации о том, что это такое на самом деле и как оно работает. Причина, по которой объект DataFrameGroupBy может быть труден для восприятия, заключается в том, что он ленив по своей природе. На самом деле он не выполняет никаких операций для получения полезного результата, пока вы ему не скажете.

Примечание: В этом руководстве общий термин объект pandas GroupBy относится как к DataFrameGroupBy, так и к SeriesGroupBy объекты, у которых много общего.

Один из терминов, который часто используется наряду с .groupby(), - это разделение-применение-объединение. Это относится к цепочке из трех этапов:

  1. Разделите таблицу на группы.
  2. Примените некоторые операции к каждой из этих небольших таблиц.
  3. Объедините результатов.

Проверить df.groupby("state") может быть сложно, потому что он практически ничего из этого не делает, пока вы что-то не сделаете с результирующим объектом. Объект pandas GroupBy задерживает практически каждую часть процесса разделения-применения-объединения до тех пор, пока вы не вызовете для него метод.

Итак, как вы можете мысленно разделить этапы разделения, применения и объединения, если вы не можете представить, что какой-либо из них происходит изолированно? Один из полезных способов проверить объект pandas GroupBy и увидеть разбиение в действии - это выполнить итерацию по нему:

>>> for state, frame in by_state:
...     print(f"First 2 entries for {state!r}")
...     print("------------------------")
...     print(frame.head(2), end="\n\n")
...
First 2 entries for 'AK'
------------------------
     last_name first_name   birthday gender type state        party
6619    Waskey      Frank 1875-04-20      M  rep    AK     Democrat
6647      Cale     Thomas 1848-09-17      M  rep    AK  Independent

First 2 entries for 'AL'
------------------------
    last_name first_name   birthday gender type state       party
912   Crowell       John 1780-09-18      M  rep    AL  Republican
991    Walker       John 1783-08-12      M  sen    AL  Republican


Если вы работаете над сложной задачей агрегирования, то итерация по объекту pandas GroupBy может стать отличным способом визуализации разделения части split-apply-combine.

Существует несколько других методов и свойств, которые позволяют просматривать отдельные группы и их разбиения. Атрибут .groups предоставит вам словарь пар {group name: group label}. Например, by_state.groups - это dict с состояниями в качестве ключей. Вот значение для ключа "PA":

>>> by_state.groups["PA"]
Int64Index([    4,    19,    21,    27,    38,    57,    69,    76,    84,
               88,
            ...
            11842, 11866, 11875, 11877, 11887, 11891, 11932, 11945, 11959,
            11973],
           dtype='int64', length=1053)


Каждое значение представляет собой последовательность расположения индексов для строк, принадлежащих к этой конкретной группе. В приведенных выше выходных данных, 4, 19, и 21 являются первыми индексами в df, при которых состояние равно "PA".

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

>>> by_state.get_group("PA")
      last_name first_name   birthday gender type state                party
4        Clymer     George 1739-03-16      M  rep    PA                  NaN
19       Maclay    William 1737-07-20      M  sen    PA  Anti-Administration
21       Morris     Robert 1734-01-20      M  sen    PA   Pro-Administration
27      Wynkoop      Henry 1737-03-02      M  rep    PA                  NaN
38       Jacobs     Israel 1726-06-09      M  rep    PA                  NaN
...         ...        ...        ...    ...  ...   ...                  ...
11891     Brady     Robert 1945-04-07      M  rep    PA             Democrat
11932   Shuster       Bill 1961-01-10      M  rep    PA           Republican
11945   Rothfus      Keith 1962-04-25      M  rep    PA           Republican
11959  Costello       Ryan 1976-09-07      M  rep    PA           Republican
11973    Marino        Tom 1952-08-15      M  rep    PA           Republican


Это практически эквивалентно использованию .loc[]. Вы могли бы получить тот же результат, используя что-то вроде df.loc[df["state"] == "PA"].

Также стоит упомянуть, что .groupby() выполняет некоторую, но не всю работу по разделению, создавая экземпляр класса Grouping для каждого передаваемого вами ключа. Однако многие методы класса BaseGrouper, который содержит эти группировки, вызываются лениво, а не в .__init__(), и многие из них также используют кэшированный дизайн свойств.

Далее, что насчет части применить? Вы можете представить этот этап процесса как применение одной и той же операции (или вызываемой) к каждой вложенной таблице, созданной на этапе разделения.

Из объекта pandas GroupBy by_state вы можете выбрать начальный штат США и DataFrame с помощью next(). Когда вы выполните итерацию по объекту pandas GroupBy, вы получите пары, которые можно разложить на две переменные:

>>> state, frame = next(iter(by_state))  # First tuple from iterator
>>> state
'AK'
>>> frame.head(3)
     last_name first_name   birthday gender type state        party
6619    Waskey      Frank 1875-04-20      M  rep    AK     Democrat
6647      Cale     Thomas 1848-09-17      M  rep    AK  Independent
7442   Grigsby     George 1874-12-02      M  rep    AK          NaN


Теперь вспомните свою первоначальную полную операцию:

>>> df.groupby("state")["last_name"].count()
state
AK      16
AL     206
AR     117
AS       2
AZ      48
...


Этап применить, когда он применяется к вашему единственному подмножеству DataFrame, будет выглядеть следующим образом:

>>> frame["last_name"].count()  # Count for state == 'AK'
16


Вы можете видеть, что результат, равный 16, соответствует значению для AK в объединенном результате.

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

Читайте дальше, чтобы ознакомиться с другими примерами процесса разделения-применения-объединения.

Пример 2: Набор данных о качестве воздуха

Набор данных о качестве воздуха содержит ежечасные показания газового датчика в Италии. Пропущенные значения в CSV-файле обозначаются символом -200. Вы можете использовать read_csv(), чтобы объединить два столбца в временную метку, используя при этом подмножество других столбцов:

pandas_airqual.py
import pandas as pd

df = pd.read_csv(
    "groupby-data/airqual.csv",
    parse_dates=[["Date", "Time"]],
    na_values=[-200],
    usecols=["Date", "Time", "CO(GT)", "T", "RH", "AH"]
).rename(
    columns={
        "CO(GT)": "co",
        "Date_Time": "tstamp",
        "T": "temp_c",
        "RH": "rel_hum",
        "AH": "abs_hum",
    }
).set_index("tstamp")

В результате получается DataFrame с DatetimeIndex и четырьмя float столбцами:

>>> from pandas_airqual import df
>>> df.head()
                      co  temp_c  rel_hum  abs_hum
tstamp
2004-03-10 18:00:00  2.6    13.6     48.9    0.758
2004-03-10 19:00:00  2.0    13.3     47.7    0.726
2004-03-10 20:00:00  2.2    11.9     54.0    0.750
2004-03-10 21:00:00  2.2    11.0     60.0    0.787
2004-03-10 22:00:00  1.6    11.2     59.6    0.789


Здесь co - это среднее значение окиси углерода за этот час, в то время как temp_c, rel_hum, и abs_hum - это средняя температура по Цельсию, относительная влажность воздуха и абсолютная влажность воздуха за этот час соответственно. Наблюдения проводились с марта 2004 по апрель 2005 года:

>>> df.index.min()
Timestamp('2004-03-10 18:00:00')
>>> df.index.max()
Timestamp('2005-04-04 14:00:00')


До сих пор вы группировали столбцы, задавая их названия как str, например, как df.groupby("state"). Но .groupby() гораздо более гибкий способ, чем этот! Вы увидите, как это делается дальше.

Группировка по производным массивам

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

  • Столбец или список столбцов
  • А dict или панды Series
  • Массив NumPy или pandas Index, или подобный массиву, повторяющийся из них

Вы можете воспользоваться последним вариантом, чтобы сгруппировать данные по дням недели. Используйте значение индекса .day_name() для создания набора строк Index. Вот первые десять наблюдений:

>>> day_names = df.index.day_name()
>>> type(day_names)
<class 'pandas.core.indexes.base.Index'>
>>> day_names[:10]
Index(['Wednesday', 'Wednesday', 'Wednesday', 'Wednesday', 'Wednesday',
       'Wednesday', 'Thursday', 'Thursday', 'Thursday', 'Thursday'],
      dtype='object', name='tstamp')


Затем вы можете взять этот объект и использовать его в качестве ключа .groupby(). В pandas day_names - это , подобный массиву. Это одномерная последовательность надписей.

Примечание: Для доступа к pandas Series, а не к Index, вам понадобится .dt. к таким методам, как .day_name(). Если ser - это ваш Series, то вам понадобится ser.dt.day_name().

Теперь передайте этот объект в .groupby(), чтобы найти среднее значение содержания монооксида углерода (co) по дням недели:

>>> df.groupby(day_names)["co"].mean()
tstamp
Friday       2.543
Monday       2.017
Saturday     1.861
Sunday       1.438
Thursday     2.456
Tuesday      2.382
Wednesday    2.401
Name: co, dtype: float64


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

Что, если бы вы захотели сгруппировать данные не только по дням недели, но и по часам дня? В результате должно получиться 7 * 24 = 168 наблюдений. Для этого вы можете передать список объектов, подобных массиву. В этом случае вы передадите pandas Int64Index объекты:

>>> hr = df.index.hour
>>> df.groupby([day_names, hr])["co"].mean().rename_axis(["dow", "hr"])
dow        hr
Friday     0     1.936
           1     1.609
           2     1.172
           3     0.887
           4     0.823
                 ...
Wednesday  19    4.147
           20    3.845
           21    2.898
           22    2.102
           23    1.938
Name: co, Length: 168, dtype: float64


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

>>> import pandas as pd
>>> bins = pd.cut(df["temp_c"], bins=3, labels=("cool", "warm", "hot"))
>>> df[["rel_hum", "abs_hum"]].groupby(bins).agg(["mean", "median"])
       rel_hum        abs_hum
          mean median    mean median
temp_c
cool    57.651   59.2   0.666  0.658
warm    49.383   49.3   1.183  1.145P
hot     24.994   24.1   1.293  1.274


В данном случае bins на самом деле является Series:

>>> type(bins)
<class 'pandas.core.series.Series'>
>>> bins.head()
tstamp
2004-03-10 18:00:00    cool
2004-03-10 19:00:00    cool
2004-03-10 20:00:00    cool
2004-03-10 21:00:00    cool
2004-03-10 22:00:00    cool
Name: temp_c, dtype: category
Categories (3, object): [cool < warm < hot]


Является ли это Series, массивом NumPy или списком, не имеет значения. Важно то, что bins по-прежнему служит в качестве последовательности меток, состоящей из cool, warm, и hot. Если бы вы действительно захотели, то вы также могли бы использовать массив Categorical или даже обычный старый list:

  • Собственный список Python: df.groupby(bins.tolist())
  • массив панд Categorical: df.groupby(bins.values)

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

Повторная выборка

Вы сгруппировали df по дням недели с помощью df.groupby(day_names)["co"].mean(). Теперь рассмотрим что-то другое. Что, если бы вы захотели сгруппировать данные наблюдения по году и кварталу? Вот один из способов добиться этого:

>>> # See an easier alternative below
>>> df.groupby([df.index.year, df.index.quarter])["co"].agg(
...     ["max", "min"]
... ).rename_axis(["year", "quarter"])
               max  min
year quarter
2004 1         8.1  0.3
     2         7.3  0.1
     3         7.5  0.1
     4        11.9  0.1
2005 1         8.7  0.1
     2         5.0  0.3


Вся эта операция, в качестве альтернативы, может быть выражена с помощью повторной выборки. Одно из применений повторной выборки - это группировка по времени на основе. Все, что вам нужно сделать, это передать частотную строку, например, "Q" для "quarterly", а pandas сделает все остальное:

>>> df.resample("Q")["co"].agg(["max", "min"])
             max  min
tstamp
2004-03-31   8.1  0.3
2004-06-30   7.3  0.1
2004-09-30   7.5  0.1
2004-12-31  11.9  0.1
2005-03-31   8.7  0.1
2005-06-30   5.0  0.3


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

Пример 3: Набор данных новостного агрегатора

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

pandas_news.py
import pandas as pd

def parse_millisecond_timestamp(ts):
    """Convert ms since Unix epoch to UTC datetime instance."""
    return pd.to_datetime(ts, unit="ms")

df = pd.read_csv(
    "groupby-data/news.csv",
    sep="\t",
    header=None,
    index_col=0,
    names=["title", "url", "outlet", "category", "cluster", "host", "tstamp"],
    parse_dates=["tstamp"],
    date_parser=parse_millisecond_timestamp,
    dtype={
        "outlet": "category",
        "category": "category",
        "cluster": "category",
        "host": "category",
    },
)

Чтобы считывать данные в память с правильным dtype, вам нужна вспомогательная функция для анализа столбца временной метки. Это связано с тем, что оно выражается как количество миллисекунд, прошедших с момента эпохи Unix, а не как доли секунды. Если вы хотите узнать больше о работе со временем в Python, ознакомьтесь с Использование Python datetime для работы с датами и временем.

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

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

  • b для бизнеса
  • t для науки и техники
  • e для развлечения
  • m для здоровья

Вот первая строка:

>>> from pandas_news import df
>>> df.iloc[0]
title       Fed official says weak data caused by weather,...
url         http://www.latimes.com/business/money/la-fi-mo...
outlet                                      Los Angeles Times
category                                                    b
cluster                         ddUyU0VZz0BRneMioxUPQVP6sIxvM
host                                          www.latimes.com
tstamp                             2014-03-10 16:52:50.698000
Name: 1, dtype: object


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

Использование лямбда-функций в .groupby()

Этот набор данных наталкивает на гораздо более сложные вопросы. Вот случайный, но значимый вопрос: какие сми больше всего говорят о Федеральной резервной системе? Для простоты предположим, что это влечет за собой поиск упоминаний "Fed" с учетом регистра. Имейте в виду, что это может привести к некоторым ложным срабатываниям с такими терминами, как "Federal government".

Чтобы подсчитать упоминания по аутлетам, вы можете вызвать .groupby() в аутлете, а затем буквально .apply() функцию для каждой группы, используя лямбда-функцию Python:

>>> df.groupby("outlet", sort=False)["title"].apply(
...     lambda ser: ser.str.contains("Fed").sum()
... ).nlargest(10)
outlet
Reuters                         161
NASDAQ                          103
Businessweek                     93
Investing.com                    66
Wall Street Journal \(blog\)     61
MarketWatch                      56
Moneynews                        55
Bloomberg                        53
GlobalPost                       51
Economic Times                   44
Name: title, dtype: int64


Давайте разберем это, поскольку существует несколько последовательных вызовов методов. Как и ранее, вы можете выделить первую группу и соответствующий ей объект pandas, взяв первый tuple из итератора pandas GroupBy:

>>> title, ser = next(iter(df.groupby("outlet", sort=False)["title"]))
>>> title
'Los Angeles Times'
>>> ser.head()
1       Fed official says weak data caused by weather,...
486            Stocks fall on discouraging news from Asia
1124    Clues to Genghis Khan's rise, written in the r...
1146    Elephants distinguish human voices by sex, age...
1237    Honda splits Acura into its own division to re...
Name: title, dtype: object


В данном случае ser - это панда Series, а не DataFrame. Это потому, что вы выполнили вызов .groupby() с помощью ["title"]. Таким образом, в каждой подтаблице выбирается только один столбец.

Далее следует .str.contains("Fed"). Это возвращает логическое значение Series то есть True, когда в заголовке статьи регистрируется совпадение при поиске. Конечно же, первая строка начинается с "Fed official says weak data caused by weather,..." и загорается следующим образом: True:

>>> ser.str.contains("Fed")
1          True
486       False
1124      False
1146      False
1237      False
          ...
421547    False
421584    False
421972    False
422226    False
422905    False
Name: title, Length: 1976, dtype: bool


Следующим шагом будет выполнение .sum() этого Series. Поскольку bool технически является просто специализированным типом int, вы можете суммировать Series из True и False точно так же, как вы бы суммировали последовательность 1 и 0:

>>> ser.str.contains("Fed").sum()
17


Результатом является количество упоминаний "Fed" в Los Angeles Times в наборе данных. Та же процедура применяется к Reuters, NASDAQ, Businessweek и остальным сайтам.

Повышение производительности .groupby()

Теперь вернитесь еще раз к .groupby().apply(), чтобы понять, почему этот шаблон может быть неоптимальным. Чтобы получить некоторую справочную информацию, ознакомьтесь с Как ускорить ваши проекты на pandas. Что может произойти с .apply(), так это то, что он будет эффективно выполнять цикл Python для каждой группы. В то время как шаблон .groupby().apply() может обеспечить некоторую гибкость, он также может помешать pandas иным образом использовать свои оптимизации на основе Cython.

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

  • Одномерный числовой массив
  • Список
  • Панда Series или Index

Также обратите внимание, что .groupby() является допустимым методом экземпляра для Series, а не только для DataFrame, поэтому вы можете по существу, инвертируйте логику разделения. Имея это в виду, вы можете сначала создать Series логических значений, которые указывают, содержит ли заголовок "Fed":

>>> mentions_fed = df["title"].str.contains("Fed")
>>> type(mentions_fed)
<class 'pandas.core.series.Series'>


Теперь .groupby() также является методом Series, поэтому вы можете сгруппировать один Series в другой:

>>> import numpy as np
>>> mentions_fed.groupby(
...     df["outlet"], sort=False
... ).sum().nlargest(10).astype(np.uintc)
outlet
Reuters                         161
NASDAQ                          103
Businessweek                     93
Investing.com                    66
Wall Street Journal \(blog\)     61
MarketWatch                      56
Moneynews                        55
Bloomberg                        53
GlobalPost                       51
Economic Times                   44
Name: title, dtype: uint32


Два Series не обязательно должны быть столбцами одного и того же объекта DataFrame. Они просто должны быть одинаковой формы:

>>> mentions_fed.shape
(422419,)
>>> df["outlet"].shape
(422419,)


Наконец, вы можете преобразовать результат обратно в целое число без знака с помощью np.uintc, если вы хотите получить максимально компактный результат. Вот прямое сравнение двух версий, которое приведет к одинаковому результату:

pandas_news_performance.py
import timeit
import numpy as np

from pandas_news import df

def test_apply():
    """Version 1: using `.apply()`"""
    df.groupby("outlet", sort=False)["title"].apply(
        lambda ser: ser.str.contains("Fed").sum()
    ).nlargest(10)

def test_vectorization():
    """Version 2: using vectorization"""
    mentions_fed = df["title"].str.contains("Fed")
    mentions_fed.groupby(
        df["outlet"], sort=False
    ).sum().nlargest(10).astype(np.uintc)

print(f"Version 1: {timeit.timeit(test_apply, number=3)}")
print(f"Version 2: {timeit.timeit(test_vectorization, number=3)}")

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

Теперь запустите скрипт, чтобы посмотреть, как работают обе версии:

(venv) $ python pandas_news_performance.py
Version 1: 2.5422707499965327
Version 2: 0.3260433749965159


При трехкратном запуске функция test_apply() занимает 2,54 секунды, в то время как test_vectorization() занимает всего 0,33 секунды. Это впечатляющая разница во времени процессора для нескольких сотен тысяч строк. Подумайте, насколько существенной становится разница, когда ваш набор данных увеличивается до нескольких миллионов строк!

Примечание: В этом примере для простоты приведены некоторые детали в данных. А именно, поисковый запрос "Fed" может также содержать упоминания о таких вещах, как "Federal government".

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

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

Группа "панды": Собираем все это воедино

Если вы вызовете dir() для объекта pandas GroupBy, то увидите там достаточно методов, от которых у вас закружится голова! Бывает сложно уследить за всеми функциональными возможностями объекта pandas GroupBy. Один из способов прояснить ситуацию - разделить различные методы на то, что они делают и как себя ведут.

В широком смысле методы объекта pandas GroupBy делятся на несколько категорий:

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

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

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

  4. Мета-методы в меньшей степени связаны с исходным объектом, к которому вы обращались .groupby(), и больше ориентированы на предоставление вам информации высокого уровня, такой как количество групп и индексы этих групп.

  5. Методы построения графиков имитируют API построения графиков для pandas Series или DataFrame, но обычно разбивают выходные данные на несколько подзаголовков.

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

Существует несколько методов группирования объектов pandas, которые не совсем соответствуют указанным выше категориям. Эти методы обычно создают промежуточный объект, который не является DataFrame или Series. Например, df.groupby().rolling() создает RollingGroupby объект, для которого затем можно вызвать методы агрегации, фильтрации или преобразования.

Если вы хотите погрузиться глубже, то документация по API для DataFrame.groupby(), DataFrame.resample() и pandas.Grouper - это ресурсы для изучения методов и объектов.

В pandas docs также есть еще одна отдельная таблица со своей собственной классификационной схемой. Выберите ту, которая вам подходит и кажется наиболее интуитивно понятной!

Заключение

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

Ты научился:

  • Как использовать pandas Групповые операции с реальными данными
  • Как работает цепочка операций разделение-применение-объединение и как вы можете разбить ее на этапы
  • Как методы объекта pandas GroupBy могут быть классифицированы на основе их назначения и результата

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

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

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

Часто задаваемые вопросы

Теперь, когда у вас есть некоторый опыт работы с pandas .groupby() в Python, вы можете использовать вопросы и ответы, приведенные ниже, чтобы проверить свое понимание и резюмировать то, что вы узнали.

Эти часто задаваемые вопросы относятся к наиболее важным понятиям, которые вы рассмотрели в этом руководстве. Нажмите на переключатель Показывать/скрывать рядом с каждым вопросом, чтобы открыть ответ.

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

Для группировки по нескольким столбцам в pandas вы передаете список имен столбцов в .groupby(). Затем вы можете выполнять операции с комбинациями этих столбцов.

Вы обрабатываете пропущенные значения, используя такие методы, как .fillna() или .dropna() до или после применения .groupby(), чтобы обеспечить точные результаты.

Для оптимизации производительности используйте векторизованные операции вместо .apply() и рассмотрите возможность использования категориальных типов данных для сокращения использования памяти.

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

<статус завершения article-slug="pandas-groupby" class="btn-group mb-0" data-api-article-bookmark-url="/api/v1/articles/pandas-groupby/bookmark/" url статуса завершения data-api-article-url="/api/v1/articles/pandas-groupby/завершение_статуса/"> <кнопка поделиться bluesky-text="Интересная статья на #Python от @realpython.com :" email-body="Ознакомьтесь с этой статьей о Python:%0A%0Apandas GroupBy: Ваше руководство по группировке данных в Python" email-subject="Статья о Python для вас" twitter-text="Интересная #Статья о Python от @realpython:" url="https://realpython.com/pandas-groupby /" url-title="pandas GroupBy: ваше руководство по группировке данных в Python">

Смотрите сейчас, к этому уроку прилагается соответствующий видеокурс, созданный командой Real Python. Посмотрите его вместе с письменным руководством, чтобы углубить свое понимание: pandas GroupBy: Группировка реальных данных в Python

Back to Top