</>
Тема 7 из 7

Пропуски и даты

Реальные данные никогда не бывают идеальными: где-то не заполнили поле, где-то датчик не передал значение. А почти любые данные о событиях привязаны ко времени. В этой теме — как работать с пропусками и датами.

Пропуски: 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 — полный ряд периодов.