← Назад к вопросам
Что делает функция 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!