Пропуски и даты
Реальные данные никогда не бывают идеальными: где-то не заполнили поле, где-то датчик не передал значение. А почти любые данные о событиях привязаны ко времени. В этой теме — как работать с пропусками и датами.
Пропуски: NaN
В pandas отсутствующее значение обозначается NaN (Not a Number) или None. При чтении CSV пустые ячейки автоматически становятся NaN:
name,age,city
Аня,20,Казань
Боря,,Пермь
import pandas as pd
import io
df = pd.read_csv(io.StringIO("name,age,city\nАня,20,Казань\nБоря,,Пермь\n"))
print(df["age"])
0 20.0
1 NaN
Name: age, dtype: float64
Обрати внимание: из-за пропуска целый столбец стал дробным (float64) — у целых чисел numpy нет «пустого» значения.
Найти пропуски
print(df.isna()) # таблица True/False: где пропуск
print(df.isna().sum()) # число пропусков в каждом столбце
print(df["age"].notna()) # где значение есть
Агрегаты пропускают NaN
sum, mean, min, max, count по умолчанию игнорируют пропуски:
s = pd.Series([4, None, 8])
print(s.mean()) # 6.0 — по двум известным значениям
print(s.count()) # 2 — количество непропущенных
print(len(s)) # 3 — всего элементов
Убрать или заполнить
df.dropna() # убрать строки, где есть хоть один пропуск
df.dropna(subset=["age"]) # только где пропущен возраст
df["age"].fillna(0) # заменить пропуски числом
df["age"].fillna(df["age"].mean()) # или средним
Для рядов во времени часто подходит заполнение соседним значением:
temps = pd.Series([None, 12, None, None, 15])
print(temps.ffill().tolist()) # [nan, 12, 12, 12, 15] — предыдущим значением
print(temps.bfill().tolist()) # [12, 12, 15, 15, 15] — следующим значением
ffill (forward fill) тянет последнее известное значение вперёд, bfill (backward fill) — следующее известное назад. Их можно комбинировать.
Выбор способа зависит от смысла данных: пропущенная сумма покупки — скорее 0, пропущенная температура — скорее как в предыдущий час, пропущенный возраст — лучше вообще не учитывать.
Даты
Строки с датами превращают в настоящие даты функцией pd.to_datetime:
dates = pd.to_datetime(pd.Series(["2026-03-15", "2026-12-01"]))
print(dates.dt.year) # 2026, 2026
print(dates.dt.month) # 3, 12
print(dates.dt.day) # 15, 1
print(dates.dt.dayofweek) # 6, 1 — 0 это понедельник
После этого с датами можно сравнивать, сортировать и вычитать:
print(dates.max() - dates.min()) # 261 days
print(dates[dates >= "2026-06-01"]) # даты после 1 июня
Группировка по периодам
Чтобы посчитать что-то «по месяцам», дату удобно превратить в период — месяц как единое значение:
months = dates.dt.to_period("M")
print(months) # 2026-03, 2026-12
Дальше — обычный groupby по этим значениям. Аналогично to_period("Y") — по годам, "D" — по дням.
Последовательность всех периодов подряд даёт pd.period_range:
print(pd.period_range("2026-01", "2026-04", freq="M"))
# PeriodIndex(['2026-01', '2026-02', '2026-03', '2026-04'], ...)
А reindex подгоняет Series под новый список подписей — отсутствующие получат NaN или значение из fill_value:
s = pd.Series({"a": 1, "c": 3})
print(s.reindex(["a", "b", "c"], fill_value=0).tolist()) # [1, 0, 3]
Итог
- пустые значения —
NaN;isna(),notna(),isna().sum()— найти и посчитать; - агрегаты пропуски игнорируют;
count— число известных значений; dropna— убрать,fillna— заменить,ffill/bfill— соседним значением;pd.to_datetime— строки в даты,.dt.year/.month/.day/.dayofweek— части даты;.dt.to_period("M")— месяц,period_rangeиreindex— полный ряд периодов.