Урок курса
groupby и transform: групповые признаки на уровне строк и фильтрация групп
NumPy и pandas: практический тренажёрВ прошлом уроке groupby давал сводную таблицу: одна строка на группу, агрегат в колонке. Это удобно для отчётов, но неудобно, когда нужно добавить групповой показатель обратно к каждой строке исходной таблицы. Именно здесь agg заканчивается и начинается transform.
transform: групповой агрегат на уровне строк
Когда вызываешь groupby(col).agg('mean'), pandas собирает группы и возвращает сжатую таблицу — одна строка на уникальное значение ключа. Индекс результата состоит из значений группировочного столбца, и длина результата равна числу уникальных групп, а не числу строк исходного DataFrame.
transform работает иначе. Он тоже разбивает данные на группы и вычисляет агрегат внутри каждой из них, но затем «разворачивает» результат обратно по исходному индексу. Каждая строка получает значение агрегата своей группы. Длина возвращаемой Series всегда равна len(df), индекс совпадает с исходным.
Представь таблицу с тремя сотрудниками из отдела HR и двумя из IT. После groupby('dept')['salary'].agg('mean') получаешь две строки — по одной на отдел. После groupby('dept')['salary'].transform('mean') — пять строк: каждый сотрудник несёт среднюю зарплату своего отдела рядом со своей строкой.
Это принципиальная разница: результат transform выровнен по исходному индексу и немедленно готов к присваиванию в новый столбец без каких-либо слияний или ручного сопоставления. Именно поэтому transform — стандартный инструмент, когда нужен групповой признак на уровне строки, а не сводная статистика.
Добавление группового показателя как нового столбца
Процедура укладывается в два шага: вызвать transform и присвоить результат новому столбцу.
import pandas as pd
df = pd.DataFrame({
'dept': ['HR', 'IT', 'HR', 'IT', 'HR'],
'salary': [60000, 120000, 70000, 110000, 80000]
})
df['dept_mean'] = df.groupby('dept')['salary'].transform('mean')
df['dept_sum'] = df.groupby('dept')['salary'].transform('sum')
print(df)
Вывод:
dept salary dept_mean dept_sum 0 HR 60000 70000.0 210000 1 IT 120000 115000.0 230000 2 HR 70000 70000.0 210000 3 IT 110000 115000.0 230000 4 HR 80000 70000.0 210000
Что здесь происходит. Pandas разбивает строки по значению dept, считает среднее и сумму внутри каждой группы, а затем возвращает значение обратно в каждую строку по её исходной позиции. HR получает среднее (60000 + 70000 + 80000) / 3 = 70000, IT — (120000 + 110000) / 2 = 115000. Длина результата — ровно 5, как у исходного df.
В строку присваивания df['dept_mean'] = ... результат transform ложится без каких-либо дополнительных операций — индексы совпадают, pandas выравнивает значения автоматически.
Аргумент func в transform — строковый псевдоним агрегата: 'mean', 'sum', 'max', 'min', 'std' и другие стандартные агрегатные имена, которые принимает pandas. Передавать туда NumPy-функцию или лямбду тоже можно, но строковые имена работают быстрее, потому что pandas использует оптимизированные пути вычисления.
Самостоятельная практика. Попробуйте самостоятельно добавить групповой показатель и собрать отчёт: в подборке есть задачи на groupby, transform и merge. Перейти к практике группировок и соединений. Полный маршрут от массивов до обработки таблиц доступен в бесплатном тренажёре NumPy и pandas с проверкой решений. Продолжить обучение pandas бесплатно.
Фильтрация строк по условию на групповом агрегате
До этого момента transform использовался для добавления нового столбца с групповым показателем. Но та же идея — результат той же длины, что исходный DataFrame — открывает второй сценарий: фильтрацию строк по условию на уровне группы, а не на уровне строки.
Представь задачу: есть таблица продаж по магазинам, и нужно оставить только строки из магазинов, чья суммарная выручка превышает пороговое значение. Наивное решение — посчитать groupby('store')['revenue'].agg('sum'), получить сжатую таблицу, отфильтровать там нужные магазины, а потом вернуться к исходному DataFrame и выкинуть строки через merge или isin. Это работает, но требует нескольких шагов.
С transform паттерн короче:
import pandas as pd
df = pd.DataFrame({
'store': ['A', 'B', 'A', 'C', 'B', 'C', 'A'],
'revenue': [100, 50, 200, 30, 80, 20, 150]
})
group_sum = df.groupby('store')['revenue'].transform('sum')
mask = group_sum > 200
df_filtered = df.loc[mask]
print(df_filtered)
Вывод:
store revenue 0 A 100 2 A 200 6 A 150
Что происходит по шагам. transform('sum') для каждой строки возвращает суммарную выручку её магазина: строки магазина A получают 450, строки B — 130, строки C — 50. Результат — Series длиной 7 с теми же индексами, что у df. Сравнение > 200 превращает её в булевую маску той же длины. df.loc[mask] отбирает только строки, для которых маска истинна, — то есть все строки магазина A.
Ключевое здесь: маска живёт в пространстве строк, а решение принято на уровне группы. Магазины B и C не проходят порог целиком — ни одна их строка не попадает в результат, даже если отдельные транзакции большие. Это именно групповая фильтрация, а не строчная.
Паттерн одинаков для любого агрегата. Хочешь оставить группы, где среднее выше порога — меняешь 'sum' на 'mean' и подбираешь нужный threshold. Хочешь отбросить группы с единственной строкой — transform('count') > 1. Маска в любом случае строится за одно выражение и напрямую передаётся в df.loc без промежуточных таблиц.
Типичная ошибка: agg вместо transform при создании группового признака
Когда нужно добавить в DataFrame столбец с групповым средним, иногда пишут через agg — по аналогии с тем, как только что считали сводные показатели. Код выглядит похоже, но результат оказывается полностью сломан.
import pandas as pd
df = pd.DataFrame({
'dept': ['HR', 'IT', 'HR', 'IT', 'HR'],
'salary': [60000, 120000, 70000, 110000, 80000]
})
# Неверный вариант: agg вместо transform
df['dept_mean_wrong'] = df.groupby('dept')['salary'].agg('mean')
print(df)
Вывод:
dept salary dept_mean_wrong 0 HR 60000 NaN 1 IT 120000 NaN 2 HR 70000 NaN 3 IT 110000 NaN 4 HR 80000 NaN
Почему все значения — NaN. agg('mean') возвращает Series с двумя элементами и индексом ['HR', 'IT']: это метки групп, а не позиции строк. Исходный df имеет числовой индекс [0, 1, 2, 3, 4]. Когда pandas присваивает Series в столбец, он выравнивает значения по меткам индекса. Метка 'HR' не совпадает ни с одной из меток 0..4, метка 'IT' — тоже. Совпадений нет вообще, поэтому все пять строк получают NaN. Pandas не сообщает об ошибке — он молча выполняет присваивание, и баг легко пропустить без проверки результата.
Исправление прямое: заменить agg на transform.
# Верный вариант
df['dept_mean'] = df.groupby('dept')['salary'].transform('mean')
print(df[['dept', 'salary', 'dept_mean']])
Вывод:
dept salary dept_mean 0 HR 60000 70000.0 1 IT 120000 115000.0 2 HR 70000 70000.0 3 IT 110000 115000.0 4 HR 80000 70000.0
transform возвращает Series длиной 5 с числовым индексом [0, 1, 2, 3, 4] — точно как у df. Выравнивание при присваивании проходит корректно, каждая строка получает среднее своей группы.
Правило простое: если нужен групповой показатель как столбец в исходной таблице — transform. Если нужна сводная статистика отдельной таблицей — agg.
Попробуйте сами. Отберите строки магазинов, суммарная выручка которых превышает порог, сохранив исходную детализацию. Решить задачу на transform — в бесплатном курсе, с проверкой решения.
Если используете группировки в отчётах, продолжите открытым уроком о взвешенном среднем: почему среднее по группам может дать неверный общий результат. Урок входит в платную практическую аналитику. Посмотреть продолжение.
Задача 1
DataFrame df содержит столбцы 'team' и 'score'. Выполнен код:
result = df.groupby('team')['score'].transform('mean')
Что верно описывает result?
Задача 2
DataFrame df содержит столбцы 'dept' и 'salary'. Нужно оставить только строки сотрудников из отделов, где суммарная зарплата превышает 200000. Какой подход использует transform('sum'), возвращает Series с тем же индексом и длиной, что у df, и позволяет сразу передать булеву маску в df.loc?
Задача 3
Фильтрация магазинов по суммарной выручке через transform + булевую маску
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.
Что нужно сделать
Напишите программу, которая:
- Читает из
stdinцелое числоn— количество строк таблицы. - Читает следующие
nстрок; каждая содержит три токена через пробел:store(строка без пробелов),product(строка без пробелов),revenue(целое число). Пример строки:Alpha Laptop 300. - Создаёт
pd.DataFrameс именемdfиз словаря списков с ключами'store','product','revenue'. - Добавляет в
dfстолбец'store_total'— суммарную выручку каждого магазина для каждой строки — черезdf.groupby('store')['revenue'].transform('sum'). - Читает из
stdinцелое числоthreshold. - Строит булевую маску
mask = df['store_total'] > thresholdи создаётdf_big = df.loc[mask]. - Выводит в
stdoutровно три блока:
- Блок 1. Строку
store_total values:, затем значения столбца'store_total'через пробел (в порядке исходных строк). - Блок 2. Строку
filtered rows: <k>, гдеk— число строк вdf_big. - Блок 3. Для каждой строки
df_big(в порядке возрастания исходного индекса) одну строку вида<store> <product> <revenue>.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Формат ввода
n store1 product1 revenue1 ... storeN productN revenueN threshold
Формат вывода
store_total values:
<v1> <v2> ... <vN>
filtered rows: <k>
<store> <product> <revenue>
...
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 4
Флаг «выше среднего по региону»: transform('mean') и векторное сравнение
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.
Что нужно сделать
Напишите программу, которая:
- Читает из
stdinцелое числоn— количество строк таблицы. - Читает следующие
nстрок; каждая содержит три токена через пробел:region(строка без пробелов),manager(строка без пробелов),sales(целое число). Пример строки:Север Аня 200. - Создаёт
pd.DataFrameс именемdfиз словаря списков с ключами'region','manager','sales'. - Добавляет столбец
'region_avg'— среднееsalesпо каждомуregionдля каждой строки — черезdf.groupby('region')['sales'].transform('mean'). - Добавляет булев столбец
'above_avg'—True, еслиsalesданной строки строго большеregion_avg. Используйте векторное сравнение без циклов. - Выводит в
stdoutровноn + 2строк:
- Строки 1…n: для каждой строки (в исходном порядке) одну строку вида
<manager> <region_avg_rounded_to_1> <above_avg>, гдеregion_avgокруглён до одного знака после запятой,above_avg—TrueилиFalse. - Строка n+1:
above_count: <k>— число строк, гдеabove_avgравенTrue(черезdf['above_avg'].sum()). - Строка n+2:
shape: <shape>— кортежdf.shape.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Ограничения: n >= 1.
Формат ввода
n region1 manager1 sales1 ... regionN managerN salesN
Формат вывода
manager1 region_avg1 above_avg1
...
managerN region_avgN above_avgN
above_count: <k>
shape: (<n>, <cols>)
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 5
Найди и исправь: agg вместо transform и неверный порог фильтрации
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет.
В редакторе уже есть сломанный код. В нём ровно три ошибки в трёх независимых строках:
- Столбец
'cat_total'вычисляется черезdf.groupby('category')['value'].agg('sum')— ноaggвозвращает Series с индексом из меток групп, поэтому при присваивании вdf['cat_total']все значения станутNaN. Нужно заменитьaggнаtransform. - Маска
maskстроится какdf['cat_total'] >= threshold, хотя по условию нужно отбирать строки, где суммарное значение категории строго больше порога. Нужно заменить>=на>. - Вывод числа отфильтрованных строк написан как
print(len(df_filtered)), хотя по условию нужно вывести строку видаfiltered: <k>(например,filtered: 3). Нужно исправить наprint(f'filtered: {len(df_filtered)}').
Каждая ошибка находится в отдельной строке кода. Исправьте все три.
Что нужно сделать
Исправьте три ошибки так, чтобы программа:
- Читала из
stdinцелое числоn— количество строк таблицы. - Читала следующие
nстрок; каждая содержит три токена через пробел:category(строка без пробелов),item(строка без пробелов),value(целое число). - Создавала
pd.DataFrameс именемdfиз словаря списков с ключами'category','item','value'. - Добавляла столбец
'cat_total'— суммарное значениеvalueпо каждойcategoryдля каждой строки — черезtransform('sum'). - Читала целое число
threshold. - Строила маску
mask = df['cat_total'] > thresholdи создавалаdf_filtered = df.loc[mask]. - Выводила ровно три блока:
- Строку
cat_total: <v1> <v2> ... <vN>— значения столбца'cat_total'в порядке исходных строк через пробел. - Строку
filtered: <k>— число строк вdf_filtered. - Строку
categories: <уникальные категории через пробел>— отсортированный список уникальных значенийcategoryвdf_filtered.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Ограничения: n >= 1.
Формат ввода
n category1 item1 value1 ... categoryN itemN valueN threshold
Формат вывода
cat_total: <v1> <v2> ... <vN>
filtered: <k>
categories: <cat1> <cat2> ...
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задача 6
Фильтрация отделов по числу сотрудников через transform('count')
Что уже дано
Проверяющая система запускает вашу программу и подаёт данные через stdin. Никаких заранее созданных переменных нет — вы читаете всё сами.
Что нужно сделать
Напишите программу, которая:
- Читает из
stdinцелое числоn— количество строк таблицы. - Читает следующие
nстрок; каждая содержит три токена через пробел:dept(строка без пробелов),employee(строка без пробелов),hours(целое число). Пример строки:Dev Alpha 40. - Создаёт
pd.DataFrameс именемdfиз словаря списков с ключами'dept','employee','hours'. - Добавляет столбец
'dept_hours_sum'— суммарные часы по каждомуdeptдля каждой строки — черезdf.groupby('dept')['hours'].transform('sum'). - Добавляет столбец
'dept_count'— число сотрудников в каждомdeptдля каждой строки — черезdf.groupby('dept')['hours'].transform('count'). - Читает из
stdinцелое числоmin_count— минимальный размер отдела. - Строит булевую маску
mask = df['dept_count'] >= min_countи создаётdf_big = df.loc[mask]. - Выводит в
stdoutровно четыре блока:
- Блок 1. Строку
dept_hours_sum:, затем значения столбца'dept_hours_sum'через пробел (в порядке исходных строк). - Блок 2. Строку
dept_count:, затем значения столбца'dept_count'через пробел. - Блок 3. Строку
big_dept rows: <k>, гдеk— число строк вdf_big. - Блок 4. Для каждой строки
df_big(в порядке исходного индекса) одну строку вида<dept> <employee> <hours>.
Ввод и вывод
Программа читает stdin и пишет в stdout.
Ограничения: n >= 1.
Формат ввода
n dept1 employee1 hours1 ... deptN employeeN hoursN min_count
Формат вывода
dept_hours_sum:
<v1> <v2> ... <vN>
dept_count:
<c1> <c2> ... <cN>
big_dept rows: <k>
<dept> <employee> <hours>
...
Без регистрации. Гостевая проверка не сохраняет прогресс. Для сохранения перейдите к уроку.
Отправьте решение, чтобы увидеть результаты тестов.
Задачи к уроку
- groupby и transform: групповые признаки на уровне строк и фильтрация групп — задача по NumPy и pandas
Практическая задача по теме «groupby и transform: групповые признаки на уровне строк и фильтрация групп» с проверкой ответа онлайн.
- pandas groupby transform: среднее группы для каждой строки
Чем groupby transform отличается от agg? Пример среднего по группе без потери строк и задача pandas с проверкой кода.
- pandas transform count: фильтрация групп по числу строк
Посчитайте размер каждой группы через groupby.transform('count') и отберите отделы с нужным числом сотрудников.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
