Пропуски в данных — как обработать 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'.

Практические стратегии

Универсального рецепта нет, но есть рабочий чек-лист:

  1. Посчитайте % пропусков по каждому столбцу. Если в столбце 80% пропусков — скорее всего, его стоит удалить целиком.
  2. Поймите причину пропусков. Случайные пропуски (MCAR) можно удалять или заполнять. Систематические (MNAR) — нельзя просто удалять, это исказит выборку.
  3. Категориальные столбцы — заполните значением «Неизвестно» или модой.
  4. Числовые столбцы — медиана обычно лучше среднего (устойчива к выбросам). Заполнение средним по группе — ещё точнее.
  5. Временные рядыffill, bfill или interpolate.

Типичные ошибки

Заполнение до анализа. Если заполнить пропуски средним, а потом считать среднее — вы искусственно занизите дисперсию. Сначала проанализируйте данные с пропусками, потом решайте, как заполнять.

Не проверяете долю пропусков. df.dropna() на датасете, где 30% строк с пропусками, убьёт треть данных. Всегда смотрите df.isna().mean() до принятия решения.

fillna(0) без раздумий. Ноль — это значение, а не пропуск. Заполнить revenue нулём — значит сказать «пользователь заплатил 0», а не «мы не знаем, сколько он заплатил». Это может сломать расчёт метрик.

Вопросы с собеседований

  1. Чем отличается isna() от isnull()? Ничем — это алиасы. Оба возвращают булеву маску пропусков. isna появился позже как более читаемый вариант.

  2. Как бы вы обработали 30% пропусков в столбце «доход»? Удалять нельзя — потеряем много данных. Заполнить медианой по группе (например, по должности или региону). Если группировочной переменной нет — общей медианой. Важно: не средним, потому что доход обычно имеет правую асимметрию.

  3. В чём опасность fillna(0) для столбца revenue? Ноль — это значение «клиент заплатил 0», а не «данных нет». Это занизит средний чек, исказит ARPU и другие метрики. Лучше заполнить медианой или удалить строки.

  4. Когда нельзя использовать dropna()? Когда пропуски систематические: например, богатые клиенты не заполняют анкету — удаление исказит выборку в сторону менее состоятельных. Или когда пропусков много и потеря строк критична для размера выборки.

  5. Как заполнить пропуски средним по группе? Через 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) — следующим. Оба метода полезны для временных рядов, где вчерашнее значение — разумная оценка для сегодня. Для обычных таблиц без порядка эти методы обычно не подходят.