Мира · маркетолог магазина настолок «Кубик»
Каждый понедельник я пишу тебе «пришли цифры за неделю». Ты присылаешь, но то в CSV, то в Excel, и столбцы каждый раз в другом порядке. Можно, чтобы отчёт приходил сам, в одно время и в одном виде?
В разведке «Кубика» мы один раз посмотрели на данные свежим взглядом. Еженедельный отчёт устроен наоборот: одно и то же каждую неделю, и главное в нём не открытия, а то, что он не ломается. В уроке про SQL из Python ты уже сохранил отчёт за период в CSV. Сегодня делаем из него скрипт, который сам проверяет выгрузку, пишет Excel на два листа и запускается по расписанию.
Из блокнота в скрипт
Блокнот хорош для разведки и плох для повторения. Ячейки можно выполнить в любом порядке, переменная из удалённой ячейки живёт в памяти, а дату недели ты каждый раз меняешь руками в третьей ячейке сверху. Через месяц уже никто не вспомнит, в каком порядке их запускать.
Скрипт собирает весь расчёт в одну функцию, а всё, что меняется от запуска к запуску, передаёт ей параметрами:
import argparse
import pandas as pd
def main():
parser = argparse.ArgumentParser(description="Недельный отчёт «Кубика»")
parser.add_argument("--week", required=True, help="понедельник недели, например 2026-06-22")
parser.add_argument("--orders", default="orders.csv", help="выгрузка заказов")
args = parser.parse_args()
orders = pd.read_csv(args.orders)
check_orders(orders, args.week)
build_report(orders, args.week, f"report_{args.week}.xlsx")
if __name__ == "__main__":
main()
Запуск из терминала: python report.py --week 2026-06-22. Модуль argparse из стандартной библиотеки разбирает аргументы командной строки и сам печатает подсказку по python report.py --help. Строка if __name__ == "__main__" значит «выполни main(), только если файл запустили, а не импортировали». Благодаря ей build_report можно импортировать в блокнот или в тест, и отчёт при этом не запустится сам.
Дальше по очереди наполняем три функции: проверку, расчёт и запись.
Неделя по Москве
В выгрузке created_at записан в UTC, как в базе. А неделя у Миры московская: с понедельника 00:00 по Москве до следующего понедельника. Заказ в воскресенье в 22:30 по UTC сделан в понедельник в 01:30 по Москве и относится уже к следующей неделе. Часовые пояса в pandas мы разбирали в уроке про даты:
TZ = "Europe/Moscow"
def week_orders(orders, week_start):
"""Заказы с понедельника week_start по воскресенье включительно, по московскому времени."""
start = pd.Timestamp(week_start, tz=TZ)
local = pd.to_datetime(orders["created_at"]).dt.tz_convert(TZ)
in_week = (local >= start) & (local < start + pd.Timedelta(days=7))
return orders[in_week].assign(created_at=local[in_week])
С апреля по июнь «Кубик» получил 47 заказов после 21:00 по UTC, то есть после полуночи по Москве. Каждый из них по UTC попадает во вчерашний день. За неделю с 22 июня по UTC выходит 91 заказ, по Москве 92.
Проверь вход до расчёта
Однажды выгрузка придёт без столбца, с пустой неделей или с минусом в сумме. Плохой отчёт хуже, чем никакого: Мира увидит «выручка за неделю 0 ₽» и пойдёт разбираться с маркетингом, а сломалась выгрузка. Поэтому скрипт сначала проверяет вход и останавливается с понятным сообщением, а считает только потом.
REQUIRED = ["id", "status", "created_at", "promo_code", "items_rub"]
def check_orders(orders, week_start):
missing = [column for column in REQUIRED if column not in orders.columns]
assert not missing, f"в выгрузке нет столбцов: {', '.join(missing)}"
assert pd.Timestamp(week_start).dayofweek == 0, f"{week_start} не понедельник"
amounts = orders["items_rub"]
assert amounts.notna().all() and (amounts >= 0).all(), "в items_rub пустые или отрицательные суммы"
week = week_orders(orders, week_start)
assert not week.empty, f"за неделю с {week_start} нет ни одного заказа"
return week
assert мы ставили в конец блокнота в уроке про чистку, чтобы сверить итог. Здесь он стоит в начале и сторожит вход. Проверка схемы идёт первой: без created_at упадёт уже week_orders, и вместо «нет столбца created_at» ты увидишь невнятный KeyError.
У assert есть оговорка. Если запустить Python с флагом -O, все assert пропускаются. Для своего скрипта это редкость, но если отчёт будут запускать чужие руки, пиши if missing: raise ValueError(...): такую проверку флаг не отключит.
урок из курса
Урок из курса «Аналитика данных с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.