Дневная выручка прыгает: сегодня 9 тысяч, завтра 18, послезавтра 4. За этими скачками трудно разглядеть,
растут продажи или падают. resample из прошлого урока укрупняет данные — но вместе с шумом теряет и
подробности: из 365 точек остаётся 12. Есть способ сгладить ряд, сохранив точку на каждый день.
Скользящее окно
Идея: для каждого дня взять не одно его значение, а итог по нескольким последним дням. Метод rolling(n)
«прикладывает» к ряду окно из n значений и двигает его шаг за шагом.
import pandas as pd
steps = pd.Series([1, 2, 3, 4, 5, 6])
print(steps.rolling(3).sum().tolist())
print(steps.rolling(3).mean().tolist())[nan, nan, 6.0, 9.0, 12.0, 15.0] [nan, nan, 2.0, 3.0, 4.0, 5.0]
Третье значение суммы — 1 + 2 + 3 = 6, четвёртое — 2 + 3 + 4 = 9: окно сдвинулось на одно значение. Первые
два результата — nan: окно из трёх значений там ещё не набралось.
Теперь выручка магазина по дням — ряд с полным календарём из прошлых уроков:
orders = pd.read_csv("data/shop_orders.csv", parse_dates=["date"])
orders["revenue"] = orders["price"] * orders["quantity"]
sales = orders.groupby("date")["revenue"].sum().reindex(pd.date_range("2025-01-01", "2025-12-31"), fill_value=0)
sales.head(8).tolist()[9060, 10450, 18620, 4090, 8610, 14550, 5400, 13070]
sales.rolling(3).mean().head(8).round(1).tolist()[nan, nan, 12710.0, 11053.3, 10440.0, 9083.3, 9520.0, 11006.7]
Каждое число — средняя дневная выручка за три дня, заканчивая этим днём. Это и есть скользящее среднее. Для дневных продаж чаще берут окно в семь дней: в него всегда попадает ровно по одному понедельнику, вторнику и так далее, и недельный ритм (в выходные покупают больше) не мешает.
Что выведет print(pd.Series([10, 20, 30, 40]).rolling(2).mean().tolist()[-1])?
Сохраните в smooth скользящее среднее ряда sales с окном 7 дней. В n_empty — сколько в нём пропусков.
В peak_day — день, на который приходится наибольшее значение smooth, в peak_value — само это значение.
sales.rolling(7).mean().smooth.idxmax() и smooth.max() — пропуски они не учитывают.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
smooth = ...
n_empty = ...
peak_day = ...
peak_value = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
smooth = sales.rolling(7).mean()
n_empty = smooth.isna().sum()
peak_day = smooth.idxmax()
peak_value = smooth.max()Лучшие семь дней подряд закончились 16 апреля: в среднем 14.3 тысячи в день. Это не календарная неделя из прошлого урока, а любое окно из семи дней — поэтому и дата другая.
Пропуски в начале можно убрать параметром min_periods — «считать, если в окне есть хотя бы столько
значений»:
sales.rolling(7, min_periods=1).mean().head(3).round(1).tolist()[9060.0, 9755.0, 12710.0]
Первое значение — один день, второе — среднее двух, третье — трёх. Это удобно для графика, но помните: первые точки посчитаны по меньшему числу дней и менее надёжны.
Сдвиг
Метод shift(n) сдвигает значения на n шагов вперёд, оставляя метки на месте. На каждой дате оказывается
значение, которое было n дней назад:
print(sales.head(4).tolist())
print(sales.shift(1).head(4).tolist())
print(sales.shift(-1).head(4).tolist())[9060, 10450, 18620, 4090] [nan, 9060.0, 10450.0, 18620.0] [10450.0, 18620.0, 4090.0, 8610.0]
shift(1) — «вчера»: у 2 января стоит выручка 1 января. shift(-1) сдвигает в обратную сторону — «завтра».
В начале (или в конце) появляются пропуски: сдвигать туда нечего.
Сдвиг нужен, чтобы сравнивать ряд с самим собой в прошлом. Разность с прошлым значением встречается так
часто, что для неё есть метод diff:
print(sales.diff().head(4).tolist())
print((sales - sales.shift(1)).head(4).tolist())[nan, 1390.0, 8170.0, -14530.0] [nan, 1390.0, 8170.0, -14530.0]
Сравнивать вторник с понедельником нечестно — у дней недели разный уровень продаж. Честнее сравнить с тем же
днём недели неделей раньше. Сохраните в week_ago ряд sales, сдвинутый на 7 дней, в change — разность
sales и week_ago. В better — число дней, когда выручка выше, чем неделей раньше. В better_share — долю
таких дней среди дней, для которых сравнение возможно.
sales.shift(7).change.notna().sum().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
week_ago = ...
change = ...
better = ...
better_share = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
week_ago = sales.shift(7)
change = sales - week_ago
better = (change > 0).sum()
better_share = better / change.notna().sum()Рост в процентах
Разность в рублях мало говорит без масштаба: плюс 50 тысяч — это много для июля и немного для декабря.
Метод pct_change считает относительное изменение: текущее значение, делённое на предыдущее, минус
один.
print(pd.Series([100, 120, 90]).pct_change().round(2).tolist())
monthly = sales.resample("ME").sum()
print(monthly.head(3).tolist())[nan, 0.2, -0.25] [318940, 250430, 297780]
0.2 значит «на 20 % больше предыдущего значения» (120 против 100), −0.25 — «на 25 % меньше» (90 против 120).
У первого значения сравнивать не с чем — пропуск. Во второй строке — начало ряда monthly: выручка
по месяцам.
Сохраните в growth рост месячной выручки monthly к прошлому месяцу, округлённый до трёх знаков.
В best_growth — номер месяца с наибольшим ростом, в falls — сколько раз за год выручка упала по сравнению
с прошлым месяцем.
monthly.pct_change().round(3).growth.idxmax().month; падения — сумма маски growth < 0.Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
growth = ...
best_growth = ...
falls = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
growth = monthly.pct_change().round(3)
best_growth = growth.idxmax().month
falls = (growth < 0).sum()Окна побольше
Размер окна подбирают под задачу. Неделя убирает недельный ритм, месяц — показывает сезон:
weather = pd.read_csv("data/weather.csv", parse_dates=["date"])
temp = weather[weather["city"] == "Москва"].set_index("date")["temp_max"]
temp.rolling(7).mean().round(2).iloc[6:9]date 2025-01-07 -5.76 2025-01-08 -5.90 2025-01-09 -5.84 Name: temp_max, dtype: float64
В temp — дневной максимум температуры в Москве. Сохраните в month_avg скользящее среднее с окном 30 дней,
в warmest_end — дату, которой заканчиваются самые тёплые 30 дней года. В jump сохраните изменение
температуры за сутки, в biggest_drop — день самого резкого похолодания.
temp.rolling(30).mean() и idxmax().temp.diff(); самое сильное похолодание — наименьшее (самое отрицательное) значение: idxmin().Заготовка упражнения обновилась с тех пор, как вы сохранили свой код.
Вернуть заготовку? Ваш код в этом упражнении будет удалён.
month_avg = ...
warmest_end = ...
jump = ...
biggest_drop = ...Упражнение пока не решено, поэтому для ячеек ниже выполнено эталонное решение — ваш код не изменён.
month_avg = temp.rolling(30).mean()
warmest_end = month_avg.idxmax()
jump = temp.diff()
biggest_drop = jump.idxmin()Итоги
rolling(n).mean()— скользящее среднее: итог по последнимnзначениям для каждой точки.- В начале ряда окно не набрано —
NaN;min_periodsразрешает неполное окно. shift(n)— значениеnшагов назад;diff()— разность с ним;pct_change()— относительный рост.- Сравнивайте с тем же днём недели:
shift(7). - Окно считает строки — сначала восстановите полный календарь.