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

Различные ситуации c группировками в pandas, примеры со StackOverflow

Стоить отметить, что в случае с групповыми операциями, да и вообще в pandas, часто быстрее (а также проще для понимания) писать более подробный код, а не сокращать.

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

Содержание:


Базовая группировка с помощью GroupBy.apply():

В отличие от DataFrame.agg(), вызываемому объекту метода GroupBy.apply() передается суб-DataFrame, который дает доступ ко всем исходным столбцам.

>>> import pandas as pd
df = pd.DataFrame(
    {
        "animal": "cat dog cat fish dog cat cat".split(),
        "size": list("SSMMMLL"),
        "weight": [8, 10, 11, 1, 20, 12, 12],
        "adult": [False] * 5 + [True] * 2,
    }
)
>>> df
#   animal size  weight  adult
# 0    cat    S       8  False
# 1    dog    S      10  False
# 2    cat    M      11  False
# 3   fish    M       1  False
# 4    dog    M      20  False
# 5    cat    L      12   True
# 6    cat    L      12   True

# размер животных с наибольшим весом.
>>> df.groupby("animal").apply(lambda df: df["size"][df["weight"].idxmax()], include_groups=False)
# animal
# cat     L
# dog     M
# fish    M
# dtype: object

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

# исходные данные смотрим выше
# здесь получаем объект группировки
>>> grby = df.groupby("animal")

# теперь из объекта группировки
# просто выбираем нужную группу
>>> grby.get_group("cat")
#   animal size  weight  adult
# 0    cat    S       8  False
# 2    cat    M      11  False
# 5    cat    L      12   True
# 6    cat    L      12   True

Применение GroupBy.apply() к различным элементам в группе

# исходные данные смотрим выше, объект 
# группировки `grby` получили в предыдущем примере

# определим функцию
def grow_up(x):
    avg_weight = sum(x[x["size"] == "S"].weight * 1.5)
    avg_weight += sum(x[x["size"] == "M"].weight * 1.25)
    avg_weight += sum(x[x["size"] == "L"].weight)
    avg_weight /= len(x)
    return pd.Series(["L", avg_weight, True], index=["size", "weight", "adult"])

# применяем функцию `grow_up`
>>> expected_df = grby.apply(grow_up, include_groups=False)
>>> expected_df
#        size   weight  adult
# animal                     
# cat       L  12.4375   True
# dog       L  20.0000   True
# fish      L   1.2500   True

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

>>> df = pd.DataFrame({"A": [1, 1, 2, 2], "B": [1, -1, 1, 2]})
>>> df
#    A  B
# 0  1  1
# 1  1 -1
# 2  2  1
# 3  2  2

def replace(g):
    "Подсчет `mean` в группе"
    mask = g < 0
    # оператор `~` - логическое отрицание отри
    return g.where(~mask, g[~mask].mean())

# получаем объект группировки по 
# столбцу `A` (он ушел в индекс)
>>> grby = df.groupby("A")

# применяем функцию `replace` 
# к остальной части группы 
>>> grby.transform(replace)
#    B
# 0  1
# 1  1
# 2  1
# 3  2

Сортировка групп по агрегированным данным

df = pd.DataFrame(
    {
        "code": ["foo", "bar", "baz"] * 2,
        "data": [0.16, -0.21, 0.33, 0.45, -0.59, 0.62],
        "flag": [False, True] * 3,
    }
)
>>> df
#   code  data   flag
# 0  foo  0.16  False
# 1  bar -0.21   True
# 2  baz  0.33  False
# 3  foo  0.45   True
# 4  bar -0.59  False
# 5  baz  0.62   True

# получаем объект группировки 
>>> code_groups = df.groupby("code")
# сортируем столбец `data` по `sum()` в группах столбца `code`
>>> agg_n_sort_order = code_groups[["data"]].transform("sum").sort_values(by="data")
>>> agg_n_sort_order
#    data
# 1 -0.80
# 4 -0.80
# 0  0.61
# 3  0.61
# 2  0.95
# 5  0.95

# выравниваем данные по индексу сортировки 
>>> sorted_df = df.loc[agg_n_sort_order.index]
>>> sorted_df
#   code  data   flag
# 1  bar -0.21   True
# 4  bar -0.59  False
# 0  foo  0.16  False
# 3  foo  0.45   True
# 2  baz  0.33  False
# 5  baz  0.62   True

Создание несколько столбцов с агрегированными данными

# создадим индекс `rng`
>>> rng = pd.date_range(start="2014-10-07", periods=10, freq="2min")
# создадим серию с индексом `rng`
>>> ts = pd.Series(data=list(range(10)), index=rng)
>>> ts
# 2014-10-07 00:00:00    0
# 2014-10-07 00:02:00    1
# 2014-10-07 00:04:00    2
# 2014-10-07 00:06:00    3
# 2014-10-07 00:08:00    4
# 2014-10-07 00:10:00    5
# 2014-10-07 00:12:00    6
# 2014-10-07 00:14:00    7
# 2014-10-07 00:16:00    8
# 2014-10-07 00:18:00    9
# Freq: 2min, dtype: int64

def mycust(x):
    if len(x) > 2:
        return x.iloc[1] * 1.234
    return pd.NaT

# словарь с именами итоговых столбцов 
# и именем применяемой функции
>>> mhc = {"Mean": "mean", "Max": "max", "Custom": mycust}
# делаем повторную выборку с применением функций
>>> ts.resample("5min").apply(mhc)
#                      Mean  Max Custom
# 2014-10-07 00:00:00   1.0    2  1.234
# 2014-10-07 00:05:00   3.5    4    NaT
# 2014-10-07 00:10:00   6.0    7  7.404
# 2014-10-07 00:15:00   8.5    9    NaT

Создание дополнительного столбца с количеством групп в DataFrame.

df = pd.DataFrame(
    {"Color": "Red Red Red Blue".split(), "Value": [100, 150, 50, 50]}
)
>>> df
#   Color  Value
# 0   Red    100
# 1   Red    150
# 2   Red     50
# 3  Blue     50

# Здесь все просто, `DataFrame` 
# присваивается новый столбец
>>> df["Counts"] = df.groupby(["Color"]).transform(len)
>>> df
#   Color  Value  Counts
# 0   Red    100       3
# 1   Red    150       3
# 2   Red     50       3
# 3  Blue     50       1

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

df = pd.DataFrame(
    {"line_race": [10, 10, 8, 10, 10, 8], "beyer": [99, 102, 103, 103, 88, 100]},
    index=[
        "Last Gunfighter",
        "Last Gunfighter",
        "Last Gunfighter",
        "Paynter",
        "Paynter",
        "Paynter",
    ],
)

>>> df
#                  line_race  beyer
# Last Gunfighter         10     99
# Last Gunfighter         10    102
# Last Gunfighter          8    103
# Paynter                 10    103
# Paynter                 10     88
# Paynter                  8    100

# создаем столбец со смещением значения из столбца `beyer`
# на одно значение вниз ДЛЯ КАЖДОЙ ГРУППЫ
>>> df["beyer_shifted"] = df.groupby(level=0)["beyer"].shift(1)
>>> df
#                  line_race  beyer  beyer_shifted
# Last Gunfighter         10     99            NaN
# Last Gunfighter         10    102           99.0
# Last Gunfighter          8    103          102.0
# Paynter                 10    103            NaN
# Paynter                 10     88          103.0
# Paynter                  8    100           88.0

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

df = pd.DataFrame(
    {
        "host": ["other", "other", "that", "this", "this"],
        "service": ["mail", "web", "mail", "mail", "web"],
        "no": [1, 2, 1, 3, 2],
    }
).set_index(["host", "service"])

>>> df
#                no
# host  service    
# other mail      1
#       web       2
# that  mail      1
# this  mail      3
#       web       2

# группируем и ищем индекс с максимальным значением
>>> mask = df.groupby(level=0).agg("idxmax")
>>> mask
#                  no
# host               
# other  (other, web)
# that   (that, mail)
# this   (this, mail)

# выравниваем данные по `mask`
>>> df_count = df.loc[mask["no"]].reset_index()
>>> df_count
#     host service  no
# 0  other     web   2
# 1   that    mail   1
# 2   this    mail   3

Группировка, подобная itertools.groupby() в Python

>>> df = pd.DataFrame([0, 1, 0, 1, 1, 1, 0, 1, 1], columns=["A"])
>>> df
#    A
# 0  0
# 1  1
# 2  0
# 3  1
# 4  1
# 5  1
# 6  0
# 7  1
# 8  1

>>> df["A"].groupby((df["A"] != df["A"].shift()).cumsum()).groups
# {1: [0], 2: [1], 3: [2], 4: [3, 4, 5], 5: [6], 6: [7, 8]}

>>> df["A"].groupby((df["A"] != df["A"].shift()).cumsum()).cumsum()
# 0    0
# 1    1
# 2    0
# 3    1
# 4    2
# 5    3
# 6    0
# 7    1
# 8    2
# Name: A, dtype: int64

Собираем промежуточные значения во время reduce()

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

>>> s = pd.Series([i / 100.0 for i in range(1, 11)])
>>> s
# 0    0.01
# 1    0.02
# 2    0.03
# 3    0.04
# 4    0.05
# 5    0.06
# 6    0.07
# 7    0.08
# 8    0.09
# 9    0.10
# dtype: float64

# функцию для сбора 
# промежуточных значений
def cum_ret(x, y):
    return x * (1 + y)

# функция с `reduce`
def red(x):
    return functools.reduce(cum_ret, x, 1.0)

# применим ее при расчет расширяющегося окна 
>>> s.expanding().apply(red, raw=True)
# 0    1.010000
# 1    1.030200
# 2    1.061106
# 3    1.103550
# 4    1.158728
# 5    1.228251
# 6    1.314229
# 7    1.419367
# 8    1.547110
# 9    1.701821
# dtype: float64

Сохраняем другие столбцы при использовании min() с GroupBy

df = pd.DataFrame(
    {"AAA": [1, 1, 1, 2, 2, 2, 3, 3], "BBB": [2, 1, 3, 4, 5, 1, 2, 3]}
)

>>> df
#    AAA  BBB
# 0    1    2
# 1    1    1
# 2    1    3
# 3    2    4
# 4    2    5
# 5    2    1
# 6    3    2
# 7    3    3

Вариант 1. GroupBy.idxmin() для получения индекса минимумов

>>> df.loc[df.groupby("AAA")["BBB"].idxmin()]
#    AAA  BBB
# 1    1    1
# 5    2    1
# 6    3    2

Вариант 2. Сортируем, а потом берем первое значение из каждой группы

>>> df.sort_values(by="BBB").groupby("AAA", as_index=False).first()
#    AAA  BBB
# 0    1    1
# 1    2    1
# 2    3    2