pandas: типы столбцов и очистка по правилу

Пропуски в таблице: обнаружение, dropna, fillna и nullable dtype

Содержание курса

Заполнение пропусков: константа через fillna и распространение через ffill/bfill

Там, где удалять строки нельзя — данных и без того мало, или строка ценна частично, — пропуск нужно заполнить. Два главных инструмента для этого: fillna для подстановки фиксированного значения и ffill/bfill для распространения соседних значений.

fillna(value) — константное заполнение

series.fillna(value) заменяет каждый пропуск в Series скалярным значением value. df.fillna(value) делает то же самое для всей таблицы сразу. Исходный объект при этом не меняется — метод возвращает новый объект:

import pandas as pd

df = pd.DataFrame({
    'age':    pd.array([25, None, 32], dtype='Int64'),
    'salary': [50000.0, float('nan'), 75000.0],
    'dept':   ['IT', None, 'HR']
})

# Заполняем только salary
filled_salary = df['salary'].fillna(0.0)
print(filled_salary)
0    50000.0
1        0.0
2    75000.0
Name: salary, dtype: float64

Для разных столбцов можно передать словарь — тогда каждый столбец получит своё значение:

df_filled = df.fillna({'salary': 0.0, 'dept': 'Unknown'})
print(df_filled)
    age   salary     dept
0    25  50000.0       IT
1  <NA>      0.0  Unknown
2    32  75000.0       HR

Заметьте: age всё ещё <NA>, потому что в словаре мы его не указали.

Подбирать значение для заполнения нужно осмысленно: 0 в salary говорит «зарплата равна нулю», а это может исказить агрегаты. Иногда правильнее заполнить медианой или средним — но это уже задача анализа, а не синтаксиса fillna.

ffill и bfill — распространение соседних значений

Когда данные упорядочены по времени или по какой-то логике и пропуск — это просто «пропуск в записи, а не в реальности», удобнее взять ближайшее известное значение.

ffill() (forward fill) распространяет последнее известное значение вниз — каждый пропуск заменяется тем, что стоит выше него:

s = pd.Series([10.0, float('nan'), float('nan'), 40.0, float('nan')])
print(s.ffill())
0    10.0
1    10.0
2    10.0
3    40.0
4    40.0
dtype: float64

bfill() работает в обратном направлении — заполняет пропуск ближайшим значением снизу:

print(s.bfill())
0    10.0
1    40.0
2    40.0
3    40.0
4     NaN
dtype: float64

Оба метода имеют граничный случай, который легко упустить: если пропуск стоит в самом начале Series, ffill не может его заполнить — выше нет данных. Аналогично bfill не заполнит пропуск в самом конце — ниже ничего нет. В обоих случаях пропуск просто остаётся NaN.

В примере выше последний элемент после bfill так и остался NaN — именно по этой причине.

Оба метода применимы и к DataFrame — тогда заполнение происходит вдоль каждого столбца независимо:

print(df['salary'].ffill())
0    50000.0
1    50000.0
2    75000.0
Name: salary, dtype: float64

Здесь пропуск в строке 1 заполнился значением из строки 0 — 50000.0.

Выбор между fillna, ffill и bfill диктует природа данных: для временного ряда ffill часто уместен, для категориальной переменной — скорее константа или мода. Что именно происходит с dtype после заполнения — тема следующей секции.