Пропуски в таблице: обнаружение, 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 после заполнения — тема следующей секции.
