Не доверяйте groupby().first()

Привет, Хабр!
Сегодня рассмотрим, почему groupby().first() в pandas — не такая уж безопасная и очевидная штука, как может показаться. Особенно когда нужно достать первую строку группы в точности, как она была в датафрейме — с NaN, с порядком, без сюрпризов.
Но для начала рассмотрим отличия first от других подобных методов.
Отличие first () от nth (0), head (1) и idxmin ()
first()
По факту это alias для агрегатора, который берёт первый ненулевой (ненулевой в смысле не‑NaN) элемент по каждой колонке в каждой группе. Если в первой строке группы значения NaN,first()пропустит её и отдаст следующую строку, где хотя бы одна колонка заполнена.nth(0)
Читает ровно нулевой элемент в каждой группе, еслиdropna=True(по умолчанию) — пропускает строки, где все значения NaN. Еслиdropna=False, берёт точно нулевую строку, независимо от NaN.head(1)
Просто берёт первую строку каждой группы, сохраняя порядок строк (правда, вместе с мультииндексом и group_keys).idxmin()
Находит индекс минимального значения по колонке‑ориентиру (например, по дате или сорту). Если хочется первую строку по хронологии, можно сделать что‑то вроде:idx = df.groupby('key')['timestamp'].idxmin() df.loc[idx]
Пример, чтобы почувствовать разницу:
import pandas as pd
df = pd.DataFrame({
'key': ['A','A','B','B'],
'val': [None, 10, None, 20],
'timestamp': [pd.NaT, '2025-01-01','2025-02-01','2025-03-01']
})
df['timestamp'] = pd.to_datetime(df['timestamp'])
print(df.groupby('key').first(), '\n')
print(df.groupby('key').nth(0), '\n')
print(df.groupby('key').head(1), '\n')
idx = df.groupby('key')['timestamp'].idxmin()
print(df.loc[idx])Вывод:
val timestamp
key
A 10 2025-01-01
B 20 2025-02-01
val timestamp
key
A None NaT
B None 2025-02-01
key val timestamp
0 A NaN NaT
2 B NaN 2025-02-01
key val timestamp
1 A 10.0 2025-01-01
2 B NaN 2025-02-01Итак, first() — не head(1), не nth(0) и уж точно не idxmin(), если вы оперируете с NaN или хронологией.
Когда first () пропускает NaN, а когда — нет
На первый взгляд groupby().first() в pandas кажется честным способом получить первую строку группы. Но это не совсем так. first() работает по колонкам, а не по строкам — и в этом кроется главная ловушка.
Когда вы вызываете df.groupby('key').first(), pandas не возвращает первую строку группы, а проходит по каждой колонке отдельно и ищет первое непропущенное (not‑NaN) значение. То есть итоговая строка, которую вы получите, может быть склеена из разных строк.
Простой пример:
import pandas as pd
grp = pd.DataFrame({
'key': ['X', 'X', 'X'],
'a': [None, 1, 2],
'b': [0, None, 3]
})
res = grp.groupby('key').first()
print(res)Вывод:
a b
key
X 1.0 0.0Колонка 'a': первая строка группы — None, пропущенное значение. Значит, first() идёт дальше — и берёт 1.
Колонка 'b': первая строка группы — 0. Это не NaN, значит first() берёт его.
И вот результат: колонка 'a' взята из второй строки, а 'b' — из первой. Это уже не та строка, что была в DataFrame.
Что считается «NaN» для first ()?
pandas использует функцию isna() (или pd.isna) — она считает пропущенными:
Nonenp.nanpd.NaTВ случае
object‑колонок — даже пустые списки и словари не считаются NaN
Всё остальное — считается валидным значением. Например, 0, пустая строка '', False — всё это не будет пропущено.
import numpy as np
pd.isna([None, np.nan, pd.NaT, 0, False, '', [], {}])
# [True, True, True, False, False, False, False, False]Т.е first() не пропускает «некрасивые» значения, он пропускает только технически NaN.
first () может быть опасен
Проблема в том, что поведение first() может быть неявным и зависеть от содержимого конкретных колонок:
В колонке
valueNaN — будет проигнорированВ колонке
flag, гдеFalse— не будет пропущен, хотя логически вы бы могли ожидать иногоЕсли хотя бы одна колонка в первой строке валидная —
first()её возьмёт, но остальные — продолжит искать дальше
Пример на многоколонных группах
df = pd.DataFrame({
'user_id': ['u1', 'u1', 'u1'],
'event': ['start', 'middle', 'end'],
'score': [None, 10, 20],
'result': [None, None, 'win']
})
res = df.groupby('user_id').first()
print(res)Вывод:
event score result
user_id
u1 start 10.0 win'event': строка "start" взята из первой строки 'score': None — пропущено, берётся 10 'result': два None, берётся 'win' из третьей строки
Т.е на выходе строка, составленная из трёх разных строк оригинального DataFrame.
Какое поведение можно считать «безопасным»?
Если вы хотите быть уверены, что получаете одну и ту же строку целиком — first() не подойдёт.
Особенно в этих кейсах:
Вы работаете с метаданными или анкетами, где порядок строк важен
Вы хотите сохранить семантику «первого события»
У вас есть временная колонка (
timestamp), и вы надеетесь наfirst()для упорядочивания
Реальная строка может быть выброшена из результата first(), если в ней NaN в какой‑то колонке.
Что делать?
Если нужна первая реальная строка, неважно — есть там NaN или нет, используйте:
df.groupby('user_id').nth(0, dropna=False)Если нужна строка с минимальным временем (или другим полем) — вот так:
idx = df.groupby('user_id')['timestamp'].idxmin()
df.loc[idx]Если просто хотите первую строку по текущему порядку — даже если он изменился после sort_values():
df.sort_values('timestamp', inplace=True)
df.groupby('user_id').head(1)Подытожим
first()работает постолбцово, а не построчно.Он пропускает только NaN, не «некрасивые» или «ложные» значения.
В результате можно получить строку, составленную из нескольких строк.
Это может приводить к багам, особенно если вы опираетесь на временные поля, уникальные идентификаторы или логически цельные строки.
Если хотите получить первую строку как есть — лучше использовать
nth(0, dropna=False)илиdrop_duplicates, либоidxmin, если опираетесь на конкретную колонку.
groupby().first() — не зло, но только если вы понимаете, что именно он делает.
Если вы работаете с данными и используете pandas не только для «почистить и склеить», но и для принятия решений, обратите внимание на несколько открытых вебинаров — они помогут точнее понимать поведение библиотек, избегать подводных камней при агрегации и увереннее работать с аналитикой. Записаться можно по ссылкам ниже:
28 апреля, 20:00 — Подготовка данных в Pandas
Разбор техник и инструментов, которые помогут избежать типичных ловушек в трансформации и группировке данных.30 апреля, 20:00 — Важные навыки аналитика
О системном подходе к анализу: как не потерять данные, сохранить структуру и не дать багам пройти в прод.13 мая, 20:00 — Абстрактные классы и протоколы в Python
Для тех, кто хочет понимать поведение Python‑библиотек глубже: от базовых протоколов до принципов работы pandas.
Habrahabr.ru прочитано 6500 раз
