Пропуски в данных — как обработать NaN в Pandas
Что такое NaN и None
В pandas пропущенные значения представлены двумя объектами: NaN (Not a Number) из NumPy и None из Python. На практике разница минимальна — pandas автоматически приводит None к NaN в числовых столбцах. Главное — оба означают «здесь нет данных».
Пропуски появляются по разным причинам: пользователь не заполнил поле в форме, LEFT JOIN не нашёл пару, данные потерялись при экспорте, сенсор не сработал. Задача аналитика — понять природу пропусков и выбрать стратегию: удалить, заполнить или оставить как есть.
На собеседованиях аналитиков pandas-блок часто начинается с вопроса: «Что вы делаете с пропусками в данных?» Ожидают не просто dropna(), а обоснование выбора стратегии.
Обнаружение пропусков: isna, isnull, notna
Первый шаг — понять, где и сколько пропусков в датасете.
import pandas as pd
df = pd.DataFrame({
'user_id': [1, 2, 3, 4, 5],
'age': [25, None, 32, 28, None],
'city': ['Москва', 'Питер', None, 'Москва', 'Казань'],
'revenue': [1500, 3200, None, 800, 2100]
})
# Булева маска: True где пропуск
df.isna()
# isnull — алиас isna, делает то же самое
df.isnull()
# Количество пропусков по столбцам
df.isna().sum()
# age 2
# city 1
# revenue 1
# Процент пропусков
df.isna().mean() * 100
# age 40.0
# city 20.0
# revenue 20.0
# Обратная маска: True где значение есть
df.notna()isna() и isnull() — одно и то же. Используйте любой, но будьте последовательны в проекте. notna() — инверсия, удобна для фильтрации строк с заполненными значениями.
Удаление строк и столбцов: dropna
Самый простой, но часто не лучший способ — удалить строки с пропусками.
# Удалить строки, где хотя бы одно значение NaN
df.dropna() # how='any' по умолчанию
# Удалить только если все значения в строке NaN
df.dropna(how='all')
# Удалить только если NaN в конкретных столбцах
df.dropna(subset=['age', 'revenue'])
# Оставить строки, где заполнено >= 3 столбцов
df.dropna(thresh=3)
# Удалить столбцы (а не строки) с пропусками
df.dropna(axis=1)Когда dropna оправдан:
- Пропусков мало (до 5% строк) и они случайны
- Датасет большой, потеря строк не критична
- Пропуски в ключевых столбцах, без которых строка бесполезна
Когда dropna опасен:
- Пропуски не случайны (например, богатые клиенты не указывают возраст — удаление исказит выборку)
- Пропусков много — потеряете значительную часть данных
Заполнение значений: fillna
fillna заменяет NaN на указанное значение. Есть несколько стратегий.
# Заполнить скаляром
df['revenue'].fillna(0)
# Разные значения для разных столбцов
df.fillna({'age': df['age'].median(), 'city': 'Неизвестно', 'revenue': 0})
# Forward fill — заполнить предыдущим значением
df['revenue'].ffill() # pandas >= 2.0
df['revenue'].fillna(method='ffill') # старый синтаксис
# Backward fill — заполнить следующим значением
df['revenue'].bfill()ffill и bfill полезны для временных рядов, где значение вчера — разумная оценка для сегодня. Для обычных таблиц обычно заполняют средним, медианой или модой.
Заполнение средним, медианой, модой
# Среднее — чувствительно к выбросам
df['age'].fillna(df['age'].mean())
# Медиана — устойчива к выбросам, обычно лучший выбор
df['age'].fillna(df['age'].median())
# Мода — для категориальных данных
df['city'].fillna(df['city'].mode()[0])Заполнение средним по группе
Самый продвинутый вариант — заполнить пропуск средним или медианой внутри группы. Например, пропуск в зарплате заменить медианой по должности:
df['salary'] = df.groupby('position')['salary'].transform(
lambda x: x.fillna(x.median())
)transform сохраняет индекс исходного DataFrame, поэтому результат можно записать обратно в столбец.
interpolate: интерполяция
Для временных рядов и упорядоченных данных interpolate подбирает промежуточное значение по соседям.
s = pd.Series([10, None, None, 40, 50])
s.interpolate()
# 0 10.0
# 1 20.0
# 2 30.0
# 3 40.0
# 4 50.0
# Интерполяция по времени
df = df.set_index('date')
df['metric'].interpolate(method='time')Линейная интерполяция (method='linear', по умолчанию) проводит прямую между соседними известными точками. Для временных рядов с неравными интервалами используйте method='time'.
Практические стратегии
Универсального рецепта нет, но есть рабочий чек-лист:
- Посчитайте % пропусков по каждому столбцу. Если в столбце 80% пропусков — скорее всего, его стоит удалить целиком.
- Поймите причину пропусков. Случайные пропуски (MCAR) можно удалять или заполнять. Систематические (MNAR) — нельзя просто удалять, это исказит выборку.
- Категориальные столбцы — заполните значением «Неизвестно» или модой.
- Числовые столбцы — медиана обычно лучше среднего (устойчива к выбросам). Заполнение средним по группе — ещё точнее.
- Временные ряды —
ffill,bfillилиinterpolate.
Типичные ошибки
Заполнение до анализа. Если заполнить пропуски средним, а потом считать среднее — вы искусственно занизите дисперсию. Сначала проанализируйте данные с пропусками, потом решайте, как заполнять.
Не проверяете долю пропусков. df.dropna() на датасете, где 30% строк с пропусками, убьёт треть данных. Всегда смотрите df.isna().mean() до принятия решения.
fillna(0) без раздумий. Ноль — это значение, а не пропуск. Заполнить revenue нулём — значит сказать «пользователь заплатил 0», а не «мы не знаем, сколько он заплатил». Это может сломать расчёт метрик.
Вопросы с собеседований
Чем отличается
isna()отisnull()? Ничем — это алиасы. Оба возвращают булеву маску пропусков.isnaпоявился позже как более читаемый вариант.Как бы вы обработали 30% пропусков в столбце «доход»? Удалять нельзя — потеряем много данных. Заполнить медианой по группе (например, по должности или региону). Если группировочной переменной нет — общей медианой. Важно: не средним, потому что доход обычно имеет правую асимметрию.
В чём опасность
fillna(0)для столбца revenue? Ноль — это значение «клиент заплатил 0», а не «данных нет». Это занизит средний чек, исказит ARPU и другие метрики. Лучше заполнить медианой или удалить строки.Когда нельзя использовать
dropna()? Когда пропуски систематические: например, богатые клиенты не заполняют анкету — удаление исказит выборку в сторону менее состоятельных. Или когда пропусков много и потеря строк критична для размера выборки.Как заполнить пропуски средним по группе? Через
groupby+transform:df.groupby('group')['col'].transform(lambda x: x.fillna(x.median())).transformсохраняет индекс, поэтому результат можно записать обратно.
Что дальше
Потренируйтесь решать задачи по Python и pandas в Карьернике — тренажёре для подготовки к собеседованиям аналитиков. А если хотите порешать на сайте — загляните в Python-тренажёр или примеры вопросов.
FAQ
Чем отличается NaN от None в pandas?
NaN (Not a Number) — специальное значение из NumPy для обозначения пропуска в числовых данных. None — объект Python, используется в столбцах с типом object. В числовых столбцах pandas автоматически конвертирует None в NaN. Для обнаружения обоих используйте isna().
Как быстро узнать количество пропусков в DataFrame?
df.isna().sum() покажет количество пропусков по каждому столбцу. df.isna().mean() * 100 — процент пропусков. Эти две команды — первое, что стоит запустить после загрузки данных. Подробнее о базовом осмотре данных — в шпаргалке по pandas.
Когда лучше удалять пропуски, а когда заполнять?
Удалять — когда пропусков мало (до 5%), они случайны и датасет достаточно большой. Заполнять — когда пропусков много, удаление исказит выборку или потеряет важные данные. Для числовых столбцов заполняйте медианой (устойчива к выбросам), для категориальных — модой или значением «Неизвестно».
Что такое forward fill и backward fill?
ffill (forward fill) заполняет пропуск предыдущим известным значением, bfill (backward fill) — следующим. Оба метода полезны для временных рядов, где вчерашнее значение — разумная оценка для сегодня. Для обычных таблиц без порядка эти методы обычно не подходят.