← Назад к вопросам

Что делает функция transform при использовании с groupby в Pandas?

2.0 Middle🔥 222 комментариев
#Pandas и обработка данных

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Функция transform при groupby в Pandas

Это одна из самых важных и часто неправильно используемых функций в Pandas. Расскажу что она делает и чем отличается от других методов.

Основное определение

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

Пример: сравнение groupby методов

import pandas as pd
import numpy as np

# Исходные данные
df = pd.DataFrame({
    'department': ['Sales', 'IT', 'Sales', 'IT', 'HR', 'Sales'],
    'salary': [50000, 80000, 55000, 85000, 60000, 52000],
    'bonus': [5000, 8000, 5500, 8500, 6000, 5200]
})

print(df)
#   department  salary  bonus
# 0     Sales   50000   5000
# 1        IT   80000   8000
# 2     Sales   55000   5500
# 3        IT   85000   8500
# 4        HR   60000   6000
# 5     Sales   52000   5200

# ============================================
# 1. AGGREGATE (agg) — уменьшает размер
# ============================================
print("\n=== AGG ===")
df.groupby('department')['salary'].agg('mean')
#            salary
# department        
# HR         60000.0
# IT         82500.0
# Sales      52333.3
# Результат: 3 строки (по числу групп)

# ============================================
# 2. TRANSFORM — сохраняет размер!
# ============================================
print("\n=== TRANSFORM ===")
mean_salary = df.groupby('department')['salary'].transform('mean')
print(mean_salary)
# 0    52333.3  <- Это значение для Sales группы
# 1    82500.0  <- Это значение для IT группы
# 2    52333.3  <- Это значение для Sales группы
# 3    82500.0  <- Это значение для IT группы
# 4    60000.0  <- Это значение для HR группы
# 5    52333.3  <- Это значение для Sales группы
# Name: salary, dtype: float64
# Результат: 6 строк (исходный размер!)

# ============================================
# 3. APPLY — гибкий, но сложный
# ============================================
print("\n=== APPLY ===")
df.groupby('department')['salary'].apply(np.mean)
#            salary
# department        
# HR         60000.0
# IT         82500.0
# Sales      52333.3
# Результат: 3 строки (как agg)

Ключевое отличие

transform сохраняет исходный индекс:

# AGG: теряется исходный индекс
df.groupby('department')['salary'].agg('mean')
# Index: ['HR', 'IT', 'Sales']  ← новый индекс (по группам)

# TRANSFORM: сохраняется исходный индекс
df.groupby('department')['salary'].transform('mean')
# Index: [0, 1, 2, 3, 4, 5]  ← исходный индекс!

Практический пример: нормализация по группам

# Нормализовать зарплату в пределах каждого отдела
# (вычесть среднюю зарплату отдела)

df['salary_normalized'] = (
    df['salary'] - 
    df.groupby('department')['salary'].transform('mean')
)

print(df[['department', 'salary', 'salary_normalized']])
#   department  salary  salary_normalized
# 0     Sales   50000         -2333.333333
# 1        IT   80000        -2500.000000
# 2     Sales   55000         2666.666667
# 3        IT   85000         2500.000000
# 4        HR   60000             0.000000
# 5     Sales   52000        -333.333333

# ✅ Важно: transform вернул Series с исходным индексом
# поэтому можно просто вычесть из df['salary']

Типы функций в transform

1. Встроенные функции

# transform('mean', 'std', 'min', 'max', 'count', ...)
df.groupby('department')['salary'].transform('mean')
df.groupby('department')['salary'].transform('std')
df.groupby('department')['salary'].transform('min')

2. Пользовательские функции

# Функция должна:
# - Принимать Series (группа)
# - Возвращать Series такого же размера

def zscore(x):
    """Стандартизация (z-score): (x - mean) / std"""
    return (x - x.mean()) / x.std()

df['salary_zscore'] = df.groupby('department')['salary'].transform(zscore)

print(df[['department', 'salary', 'salary_zscore']])
#   department  salary  salary_zscore
# 0     Sales   50000       -0.521405
# 1        IT   80000       -0.707107
# 2     Sales   55000        0.751150
# 3        IT   85000        0.707107
# 4        HR   60000             NaN  <- Нельзя вычислить на 1 значении!
# 5     Sales   52000       -0.229745

3. Lambda функции

# Процентиль каждого значения в группе
df['salary_percentile'] = df.groupby('department')['salary'].transform(
    lambda x: x.rank(pct=True) * 100
)

print(df[['department', 'salary', 'salary_percentile']])
#   department  salary  salary_percentile
# 0     Sales   50000               33.333333
# 1        IT   80000               50.000000
# 2     Sales   55000               66.666667
# 3        IT   85000              100.000000
# 4        HR   60000              100.000000
# 5     Sales   52000               33.333333  <- Wait, это неправильно
# Нужно пересчитать: 2/3*100 = 66.7, но для Sales это (50k, 52k, 55k)

Важное: transform должен возвращать правильный размер

# ✅ ПРАВИЛЬНО: функция возвращает Series размер как входная группа
def correct_func(group):
    return group * 2  # Series размер len(group)

df.groupby('department').apply(correct_func)  # Это работает

# ❌ НЕПРАВИЛЬНО: функция возвращает скаляр
def wrong_func(group):
    return group.sum()  # Возвращает скаляр!

df.groupby('department')['salary'].transform(wrong_func)
# ValueError: Inferred output shape ... is different

Случай использования 1: Z-score нормализация

from sklearn.preprocessing import StandardScaler

# Нормализовать признак в пределах каждой группы
def normalize_group(x):
    return (x - x.mean()) / x.std()

df['salary_norm'] = df.groupby('department')['salary'].transform(normalize_group)

Случай использования 2: Обнаружение выбросов

# Определить значения > 2 std от среднего в группе
def is_outlier(x):
    mean = x.mean()
    std = x.std()
    return np.abs(x - mean) > 2 * std

df['is_salary_outlier'] = df.groupby('department')['salary'].transform(is_outlier)

print(df[['department', 'salary', 'is_salary_outlier']])
#   department  salary  is_salary_outlier
# 0     Sales   50000              False
# 1        IT   80000              False
# 2     Sales   55000              False
# 3        IT   85000              False
# 4        HR   60000              False
# 5     Sales   52000              False

Случай использования 3: Импutation

# Заполнить пропуски средним значением группы
df_with_nans = df.copy()
df_with_nans.loc[1, 'salary'] = np.nan
df_with_nans.loc[4, 'salary'] = np.nan

df_with_nans['salary'] = df_with_nans.groupby('department')['salary'].transform(
    lambda x: x.fillna(x.mean())
)

print(df_with_nans[['department', 'salary']])
#   department  salary
# 0     Sales 50000.0
# 1        IT 82500.0  <- Заполнено средним IT
# 2     Sales 55000.0
# 3        IT 85000.0
# 4        HR 60000.0  <- Было NaN, осталось NaN (одна группа)
# 5     Sales 52000.0

Сравнение производительности

import time

df_large = pd.DataFrame({
    'group': np.random.choice(['A', 'B', 'C'], size=1_000_000),
    'value': np.random.randn(1_000_000)
})

# AGG — быстрый
start = time.time()
df_large.groupby('group')['value'].agg('mean')
print(f"AGG: {time.time() - start:.4f}s")

# TRANSFORM — медленнее (нужно восстанавливать индекс)
start = time.time()
df_large.groupby('group')['value'].transform('mean')
print(f"TRANSFORM: {time.time() - start:.4f}s")

# Результат:
# AGG: 0.0012s
# TRANSFORM: 0.0045s  <- 4x медленнее

Когда использовать transform

# ✅ Используй transform когда:
# 1. Нужно сохранить исходный размер датасета
df['group_mean'] = df.groupby('department')['salary'].transform('mean')

# 2. Нужно добавить результат в новый столбец
df['salary_rank'] = df.groupby('department')['salary'].transform('rank')

# 3. Нужно применить функцию без агрегации
df['salary_diff'] = df.groupby('department')['salary'].transform(
    lambda x: x - x.mean()
)

# ❌ Используй agg когда:
# 1. Нужно получить одно значение на группу
summary = df.groupby('department')['salary'].agg('mean')

# 2. Нужна многостолбцовая агрегация
summary = df.groupby('department')['salary'].agg(['mean', 'std', 'count'])

Итоговый совет

Помни золотое правило:

  • transform: input shape = output shape (размер не меняется)
  • agg: output shape < input shape (размер уменьшается)
  • apply: гибкий (любой output shape)

Если ты в DataFrame добавляешь новый столбец на основе groupby → используй transform!