Привет, Хабр!
Сегодня рассмотрим, почему 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
) — она считает пропущенными:
None
np.nan
pd.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()
может быть неявным и зависеть от содержимого конкретных колонок:
В колонке
value
NaN — будет проигнорированВ колонке
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.