Временные ряды: datetime-индекс, resample и rolling
Когда индекс таблицы — это время, pandas раскрывает мощный набор инструментов: агрегацию по периодам, скользящие окна и сдвиги.
DatetimeIndex — индекс из меток времени. С ним работают resample (агрегация по периодам), rolling (скользящие окна) и shift (сдвиг во времени).
Время как индекс
Анализ временных рядов начинается с того, что метки времени становятся индексом. Тогда выборка по датам, ресемплинг и окна работают «из коробки»:
import pandas as pd
df["дата"] = pd.to_datetime(df["дата"])
ts = df.set_index("дата").sort_index()
ts.loc["2024"] # все строки 2024 года
ts.loc["2024-05"] # весь май 2024
ts.loc["2024-05-01":"2024-05-31"] # срез по диапазону дат
Частичный доступ по строке («2024», «2024-05») — это удобство DatetimeIndex: pandas сам понимает, что вы имеете в виду весь год или месяц. Для этого индекс должен быть отсортирован.
Аксессор .dt: компоненты даты
У datetime-столбца (не индекса) есть аксессор .dt — аналог .str для строк, дающий доступ к частям даты:
df["дата"].dt.year # год
df["дата"].dt.month # месяц
df["дата"].dt.dayofweek # день недели (0 = понедельник)
df["дата"].dt.day_name() # название дня
df["дата"].dt.quarter # квартал
Это основа создания признаков: «выходной ли день», «номер недели», «час суток» — всё вытаскивается через .dt и используется в группировках.
resample: groupby по времени
resample — это группировка по временным периодам. «Сумма продаж по неделям», «среднее по часам» — вы задаёте частоту, и pandas бьёт ось времени на интервалы и агрегирует:
ts.resample("D").sum() # по дням
ts.resample("W").mean() # по неделям
ts.resample("ME").sum() # по месяцам (Month End)
ts.resample("h").count() # по часам
Частые коды частот: D (день), W (неделя), ME (конец месяца), QE (квартал), YE (год), h (час), min (минута). Смоделируем downsampling — суммирование по 3-минутным корзинам — на чистом Python:
# (минута от начала, значение)
data = [(0, 5), (1, 3), (2, 4), (3, 10), (4, 2), (5, 6), (6, 8), (7, 1)]
bucket = 3 # размер корзины в минутах
bins = {}
for minute, value in data:
start = (minute // bucket) * bucket # к какой корзине относится точка
bins[start] = bins.get(start, 0) + value
for start in sorted(bins):
print(f"{start:02d}:00 -> {bins[start]}")
Вывод:
00:00 -> 12 03:00 -> 18 06:00 -> 9
Восемь точек схлопнулись в три корзины по 3 минуты с суммой в каждой — ровно то, что делает ts.resample("3min").sum(). resample — это «groupby, где ключ группы вычисляется из времени».
resample работает в обе стороны. Downsampling (понижение частоты: секунды → минуты) агрегирует — много точек в одну, нужен агрегат (sum, mean). Upsampling (повышение частоты: дни → часы) наоборот создаёт пустые промежутки, которые надо заполнить — через ffill (протянуть последнее значение), asfreq (оставить NaN) или интерполяцию. Ещё две тонкости: параметры closed и label управляют тем, какая граница интервала включается и какой меткой подписывается корзина — для большинства частот по умолчанию интервал закрыт слева, а у месячных/недельных — справа. На стыках периодов это влияет, в какую корзину попадёт пограничная точка, поэтому при точных расчётах их стоит указывать явно.
rolling: скользящие окна
rolling считает агрегат в скользящем окне фиксированного размера — классика для сглаживания шума (скользящее среднее) и трендов:
data = [10, 12, 8, 14, 16, 9, 11]
window = 3
# скользящее среднее по окну из 3 точек
for i in range(len(data)):
if i + 1 < window:
print(i, data[i], "-> NaN") # окно ещё не заполнено
else:
chunk = data[i - window + 1 : i + 1]
print(i, data[i], "->", round(sum(chunk) / window, 2))
Вывод:
0 10 -> NaN 1 12 -> NaN 2 8 -> 10.0 3 14 -> 11.33 4 16 -> 12.67 5 9 -> 13.0 6 11 -> 12.0
Первые две точки дают NaN — окно из трёх ещё не набралось. Дальше каждое значение — среднее трёх последних. В pandas это ts.rolling(3).mean(). Так сглаживают дневной шум, оставляя тренд.
shift: сравнение с прошлым
shift сдвигает данные на N позиций — нужен, чтобы сравнить значение с предыдущим (прирост, разность день-к-дню):
ts["вчера"] = ts["продажи"].shift(1) # значение предыдущей строки
ts["прирост"] = ts["продажи"] - ts["продажи"].shift(1)
ts["прирост_%"] = ts["продажи"].pct_change() # относительный прирост
Таймзоны
По умолчанию метки времени «наивны» — без зоны. Для корректной работы с разными регионами их локализуют и конвертируют:
ts = ts.tz_localize("UTC") # пометить как UTC
ts = ts.tz_convert("Europe/Moscow") # перевести в МСК
Смешивать наивные и зональные метки нельзя — pandas выдаст ошибку. Если данные из разных зон, приведите всё к одной (обычно UTC) на входе.
Подводные камни
- Несортированный datetime-индекс. Частичные срезы по дате и resample требуют отсортированного индекса; иначе ошибки или странный результат.
- Строки вместо дат. Если «дата» осталась
object, ни resample, ни.dtне заработают — сперваto_datetime. - NaN в начале rolling и shift. Первые значения окна/сдвига всегда пустые — учитывайте это при последующих расчётах.
- Путаница частот.
Mустарел в пользуME(конец месяца);min— минута, неM. Сверяйтесь с актуальными кодами.
Лучшие практики
- Сразу приводите даты к
datetimeи ставьте их индексом, отсортировав его. - Агрегация по времени —
resample; сглаживание и тренды —rolling; сравнение с прошлым —shift/pct_change. - Извлекайте признаки времени через
.dt(день недели, час, квартал). - Работаете с несколькими зонами — приводите всё к UTC на входе.
Итог
- DatetimeIndex включает частичные срезы по дате и временные операции.
resample— это groupby по временным периодам.rolling— скользящие окна для сглаживания;shift— сравнение с прошлым..dtдаёт компоненты даты; таймзоны приводите к единой.