Как стать автором
Обновить
590.8
OTUS
Цифровые навыки от ведущих экспертов

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

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров1.7K

Привет, Хабр!

Сегодня рассмотрим, почему 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.

Теги:
Хабы:
+5
Комментарии0

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS