Python: от данных до своей сводки Урок 32 из 56
Время в таблице: ресемплинг, скользящее среднее и честность
Тридцать первый урок курса по Python. Индекс из дат умеет то, чего не умеет список: `asfreq` показывает дни, которых нет, `resample` складывает дни в недели, `rolling` сглаживает. И измеренная цена сглаживания: размах падает вдвое, а день настоящего пика исчезает из ряда.
Зачем это нужно
Ряды по времени приходят почти в любой работе: курс по дням, продажи по часам, показания счётчика по минутам. С ними делают три вещи — укрупняют, сглаживают и сравнивают с прошлым периодом, — и все три в pandas есть готовыми.
Есть и четвёртая вещь, которой нет ни в одной библиотеке: не соврать. Сглаженный ряд выглядит убедительнее настоящего, и именно поэтому его так легко показать вместо настоящего. В конце урока это измерено в тенге.
Сразу целиком
Файл vremya.py. Сорок календарных дней курса, из них тридцать рабочих, и один день с новостями.
"""Урок 31: время в таблице.
Сорок дней курса доллара: будни есть, выходных нет. Смотрим, что с этим делает
ресемплинг, что — скользящее среднее, и где оба начинают врать.
"""
import pandas as pd
# Ряд построен по счётчику: у всех одинаковый. Выходных в нём нет — как в
# настоящей выписке с биржи.
days, rates = [], []
start = pd.Timestamp("2026-01-05")
for i in range(40):
day = start + pd.Timedelta(days=i)
if day.weekday() >= 5:
continue
value = 512.0 + i * 0.35 + ((i * 7) % 11 - 5) * 0.6
if i == 21:
value += 14.0 # день новостей: один скачок посреди ряда
days.append(day)
rates.append(value)
rate = pd.Series(rates, index=pd.DatetimeIndex(days, name="day"), name="rate").round(2)
print(rate.head(3))
print("точек:", len(rate), "| с", rate.index.min().date(), "по", rate.index.max().date())
print()
print("== индекс из дат умеет то, чего не умеет список")
print("январь:", len(rate.loc["2026-01"]), "точек | одна дата:", rate.loc["2026-01-19"])
print("дни недели:", sorted(set(rate.index.day_name())))
print()
print("== asfreq('D') показывает дыры")
daily = rate.asfreq("D")
print("стало точек:", len(daily), "| из них пустых:", int(daily.isna().sum()))
print(daily.loc["2026-01-09":"2026-01-12"])
print()
print("== resample: неделя вместо дня")
weekly = rate.resample("W").agg(["mean", "min", "max", "count"]).round(2)
print(weekly.head(3))
print()
print("== rolling: скользящее среднее по пяти дням")
smooth = rate.rolling(5).mean().round(2)
print("первые четыре пусты:", int(smooth.head(4).isna().sum()))
print(smooth.head(6))
print()
print("== чем сглаживание платит")
print("настоящий размах:", round(rate.max() - rate.min(), 2))
print("размах сглаженного:", round(smooth.max() - smooth.min(), 2))
print("день максимума: настоящий", rate.idxmax().date(), "| сглаженный", smooth.idxmax().date())
print("скачок 26 января: в ряду", rate.loc["2026-01-26"], "| в сглаженном", smooth.loc["2026-01-26"])
print()
print("== изменение к предыдущему дню")
print((rate.diff().round(2)).head(3))
print("дней роста:", int((rate.diff() > 0).sum()), "| падения:", int((rate.diff() < 0).sum()))
Выводит:
day
2026-01-05 509.00
2026-01-06 513.55
2026-01-07 511.50
Name: rate, dtype: float64
точек: 30 | с 2026-01-05 по 2026-02-13
== индекс из дат умеет то, чего не умеет список
январь: 20 точек | одна дата: 519.9
дни недели: ['Friday', 'Monday', 'Thursday', 'Tuesday', 'Wednesday']
== asfreq('D') показывает дыры
стало точек: 40 | из них пустых: 10
day
2026-01-09 514.00
2026-01-10 NaN
2026-01-11 NaN
2026-01-12 514.45
Freq: D, Name: rate, dtype: float64
== resample: неделя вместо дня
mean min max count
day
2026-01-11 512.82 509.0 516.05 5
2026-01-18 514.31 512.4 516.95 5
2026-01-25 518.44 515.8 520.35 5
== rolling: скользящее среднее по пяти дням
первые четыре пусты: 4
day
2026-01-05 NaN
2026-01-06 NaN
2026-01-07 NaN
2026-01-08 NaN
2026-01-09 512.82
2026-01-12 513.91
Name: rate, dtype: float64
== чем сглаживание платит
настоящий размах: 23.75
размах сглаженного: 12.73
день максимума: настоящий 2026-01-26 | сглаженный 2026-02-13
скачок 26 января: в ряду 532.75 | в сглаженном 521.01
== изменение к предыдущему дню
day
2026-01-05 NaN
2026-01-06 4.55
2026-01-07 -2.05
Name: rate, dtype: float64
дней роста: 14 | падения: 15
Разбор
Индекс из дат — это не подписи-строки
pd.DatetimeIndex — тот же индекс из двадцать пятого урока, но он понимает, что подписи — время. Отсюда всё остальное:
rate.loc["2026-01"]— весь январь, хотя такой подписи в индексе нет: pandas читает это как отрезок;rate.index.day_name(),.month,.quarter— у каждой подписи есть свои части;- ряд можно спросить о частоте, сдвинуть на неделю, разложить по кварталам.
Со строками "2026-01-05" ничего этого не работает: для resample они просто текст, и он отвечает TypeError. Первое, что делают с чужим файлом, — pd.to_datetime для столбца с датой (двадцать шестой урок, аргумент parse_dates), и только потом set_index.
asfreq: дни, которых нет
В ряду тридцать точек, а календарных дней сорок. Десять дней — выходные, наблюдений в эти дни нет, и в списке это никак не видно: список не знает, что между пятницей и понедельником есть пропуск.
rate.asfreq("D") перестраивает ряд на календарную сетку: дни без наблюдения появляются, и в них NaN. Это первое, что стоит сделать с любым рядом по времени, — не чтобы заполнить дыры, а чтобы узнать, что они есть. Дальше уже решают: выходные — это нормально, а пропавший вторник посреди недели — повод спросить у источника.
resample: неделя вместо дня
resample("W") режет ряд по неделям и считает в каждой то, что попросили. Это groupby из двадцать восьмого урока, только ключ — календарный отрезок, и его не надо строить самому.
Частоты пишутся коротко: D — день, W — неделя, ME — конец месяца, QE — квартал, YE — год, h — час. Подпись у недели по умолчанию — её последний день (в выводе 2026-01-11 — это неделя с 5 по 11 января). Это стоит проверять каждый раз: недельный отчёт, подписанный последним днём, легко прочитать как «на 11 января», хотя это «за неделю до 11 января».
count в агрегате — не украшение. Он показывает, сколько точек попало в неделю: если в одной пять, а в другой две, средние этих недель несравнимы.
rolling: скользящее среднее
rate.rolling(5).mean() — среднее по последним пяти наблюдениям, шагающее по ряду. В этом примере это пять рабочих дней, а не пять календарных дней. Первые четыре значения пусты, и это правильно: пяти точек ещё не набралось, а считать по трём и называть это средним по пяти — уже неправда. Если пустое начало мешает, есть min_periods=3, но тогда первые значения посчитаны по другому числу точек, и об этом надо помнить.
Окно по умолчанию смотрит назад: значение на 9 января — это среднее за 5–9 января. Поэтому сглаженный ряд отстаёт от настоящего. center=True ставит окно симметрично, и тогда ряд не отстаёт, но в начале и в конце пустых значений становится вдвое больше.
Чем сглаживание платит
Вот ради чего урок. В ряду есть один день с новостями — 26 января, курс подскочил на 14 тенге. Что с ним стало:
| настоящий ряд | сглаженный | |
|---|---|---|
| размах | 23.75 | 12.73 |
| день пика | 26 января | 13 февраля |
| значение 26 января | 532.75 | 521.01 |
Сглаживание сделало ровно то, о чём его просили: убрало выбросы. Но выброс здесь — не шум, а событие. На сглаженном графике 26 января ничем не выделяется, размах вдвое меньше настоящего, а пик уехал на две с половиной недели вперёд — туда, где ряд просто выше в среднем.
Отсюда три правила, которые стоит принять как есть:
- Сглаженный ряд — вывод, а не данные. Рядом с ним всегда должен быть настоящий: хотя бы бледной линией на том же графике.
- Размах, максимум и минимум считают по настоящему ряду, а не по сглаженному. Иначе в отчёте окажется, что курс не превышал 526, хотя он был 533.
- У сглаживания есть параметр — ширина окна, — и его выбирают до того, как посмотрели на результат. Иначе окно подбирается под нужную форму кривой, и это уже не анализ.
Сравнение с прошлым периодом
diff() — разница с предыдущей точкой, pct_change() — она же в долях, shift(1) — сам предыдущий ряд, сдвинутый на шаг. Все три считают по соседям в том порядке, в каком лежит ряд, поэтому перед ними его сортируют по индексу; несортированный ряд даст числа, которые выглядят правдоподобно и ничего не значат.
Для таблицы с несколькими рядами — как в проекте, где стран три, — diff берут внутри группы: table.groupby("country")["value"].diff(). Без группировки первая строка каждой следующей страны вычтет последний год предыдущей.
Карта урока
Скажите своими словами
Не подглядывая, ответьте вслух или на бумаге. Ответы — в конце урока.
- Зачем делать
asfreq("D"), если заполнять пропуски вы не собираетесь? - Почему первые четыре значения скользящего среднего по пяти точкам пусты?
- Что теряет отчёт, в котором показан только сглаженный ряд?
Разминка
Три коротких шага перед заданием: предсказать, дописать, починить. Ответы — в конце урока, но сначала ответьте сами.
1. Предскажите. Что напечатает эта программа?
import pandas as pd
days = pd.to_datetime(["2026-01-05", "2026-01-06", "2026-01-07", "2026-01-12", "2026-01-13"])
rate = pd.Series([512.0, 514.0, 513.0, 520.0, 522.0], index=days)
print(rate.resample("W").mean().round(2))
print("недель:", len(rate.resample("W").mean()))
2. Заполните пропуск. Вместо ... поставьте ширину окна так, чтобы пустых значений в начале было ровно два.
# сколько точек в окне — столько минус один пустых значений в начале
import pandas as pd
days = pd.date_range("2026-01-05", periods=6, freq="D")
rate = pd.Series([512.0, 514.0, 513.0, 520.0, 522.0, 519.0], index=days)
smooth = rate.rolling(...).mean().round(2)
print(smooth.tolist())
print("пустых в начале:", int(smooth.isna().sum()))
3. Почините. Программа падает: Only valid with DatetimeIndex, TimedeltaIndex or PeriodIndex. Даты в индексе есть, но это строки.
# индекс выглядит датами, а pandas видит текст
import pandas as pd
rate = pd.Series([512.0, 514.0, 520.0], index=["2026-01-05", "2026-01-06", "2026-01-12"])
print(rate.resample("W").mean().round(2).tolist())
Задание
Обязательное. Возьмите тот же ряд из урока — сорок календарных дней, тридцать рабочих, скачок 26 января. Постройте недельный отчёт (count, mean, min, max) и скользящее среднее по пяти дням. Напечатайте: сколько календарных дней осталось без наблюдения; недельную таблицу; размах настоящего ряда и сглаженного; день и величину пика в обоих; самый большой дневной скачок и то, во что он превратился в сглаженном ряду.
Ожидаемый вывод:
дней в ряду: 30 | календарных дней: 40 | без наблюдения: 10
по неделям:
count mean min max
day
2026-01-11 5 512.82 509.00 516.05
2026-01-18 5 514.31 512.40 516.95
2026-01-25 5 518.44 515.80 520.35
2026-02-01 5 522.73 516.70 532.75
2026-02-08 5 522.74 520.10 524.65
2026-02-15 5 525.55 523.05 528.05
скользящее среднее по пяти дням:
пустых в начале: 4
размах ряда: 23.75 | размах сглаженного: 12.73
пик ряда: 2026-01-26 532.75 | пик сглаженного: 2026-02-13 525.55
самый большой скачок за день: 2026-01-26 + 14.45
в сглаженном ряду этот день: 521.01 — меньше настоящего на 11.74
Готово, когда: вывод совпадает построчно; пропущенные дни найдены через asfreq, а не вычитанием длин; недели получены resample, а не группировкой по номеру недели из строки; размах и пик настоящего ряда считаются по настоящему ряду; разница в день скачка посчитана, а не оценена на глаз.
На своих данных. Возьмите любой свой ряд по времени — расходы по дням, шаги, вес, что угодно. Сделайте asfreq и посмотрите, сколько дней в нём на самом деле пропущено. Потом постройте скользящее среднее двух разных окон и честно скажите себе, какое из них вы выбрали бы для отчёта и почему.
По желанию.
- Сравните
rolling(5).mean()иrolling(5, center=True).mean(): где ряд отстаёт, а где обрезан с двух сторон. - Сделайте
resample("ME")и посмотрите, какой подписью pandas называет месяц. - Посчитайте
pct_change()и найдите день с наибольшим ростом в процентах — совпадёт ли он с днём наибольшего роста в тенге.
Куда это встанет в проекте
Двенадцатый шаг, и в нём меньше, чем хотелось бы, — по честной причине.
Год в сводке перестаёт быть числом и становится датой: pd.to_datetime(year, format="%Y"). Число сортируется и вычитается по случайности, дата — по смыслу, и в тот день, когда сводка прочитает месячный ряд, ничего выше этой строки менять не придётся.
Отчёт получает вопрос, на который годовой ряд действительно может ответить: насколько последний год отличается от предыдущего. Это diff() внутри группировки — одна строка и никакого сдвига на единицу, который так любит цикл по отсортированным годам.
А resample и rolling в проект не попали, и это записано в шаге: пять годовых точек — не ряд, который сглаживают. Скользящее среднее по пяти точкам было бы картинкой, за которой ничего нет. Они появятся, когда сводка начнёт читать что-то плотнее одной цифры в год.
Долги. Индекс у сводки теперь дата, но частота у ряда не объявлена: годовой он или месячный, программа не знает — узнаёт по тому, что пришло. Это нормально, пока источник один.
Ответы
Показать ответы
На вопросы
- Чтобы увидеть, чего нет. Пропущенный день в списке никак не проявляется — ряд просто короче, чем календарь.
asfreq("D")ставит эти дни на место сNaN, и дальше уже решают: выходные — это ожидаемо, а пропавший вторник — повод спросить у источника, что случилось. - Потому что пяти точек ещё не набралось. Посчитать по трём и назвать это средним по пяти — неправда;
NaNздесь означает «ответа пока нет», и это честный ответ. - Он теряет события. Сглаживание убирает выбросы, а выброс — это часто самое важное, что было: день новостей, сбой, аврал. В примере размах падает с 23.75 до 12.73, а день пика уезжает на две с половиной недели. Поэтому сглаженный ряд показывают рядом с настоящим, а не вместо него.
К разминке
resample("W")считает по календарным неделям и подписывает каждую её последним днём — воскресеньем. Две недели: 5–11 января и 12–18 января.
2026-01-11 513.0
2026-01-18 521.0
Freq: W-SUN, dtype: float64
недель: 2
rolling(3). Пустых значений в начале всегда на одно меньше, чем точек в окне: окно из трёх даёт два пустых.
import pandas as pd
days = pd.date_range("2026-01-05", periods=6, freq="D")
rate = pd.Series([512.0, 514.0, 513.0, 520.0, 522.0, 519.0], index=days)
smooth = rate.rolling(3).mean().round(2)
print(smooth.tolist())
print("пустых в начале:", int(smooth.isna().sum()))
[nan, nan, 513.0, 515.67, 518.33, 520.33]
пустых в начале: 2
rate.index = pd.to_datetime(rate.index). Строки, похожие на даты, для pandas остаются строками, пока их не превратили в даты;resampleработает только с временным индексом.
import pandas as pd
rate = pd.Series([512.0, 514.0, 520.0], index=["2026-01-05", "2026-01-06", "2026-01-12"])
rate.index = pd.to_datetime(rate.index)
print(rate.resample("W").mean().round(2).tolist())
[513.0, 520.0]
К заданию
Пропущенные дни считают через asfreq("D"), а не вычитанием 40 − 30: длина календаря известна только потому, что мы сами её построили, а у настоящего ряда её пришлось бы откуда-то взять.
Размах и пик настоящего ряда считаются до сглаживания и по настоящему ряду. Это главное, что проверяется в задании: как только в отчёте появляются max и min сглаженного ряда, отчёт начинает утверждать то, чего не было.
Источники
- Работа со временем в pandas — индекс из дат, частоты,
resampleи сдвиги. - Скользящие окна —
rolling,expanding,min_periodsи центрирование окна. - Обозначения частот —
D,W,ME,QE,YEи остальные.
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.