Siluetta Ozon: быстрый план-факт заказов и CPO через корзины
После получения JSON и до выводов «как дела» проверить
журнал изменений и тестов: активные тесты и изменения, пересекающие
период/базу сравнения. Отразить релевантные изменения рекламы и фото, сезонные решения
и зрелость результата. Если новая цель владельца отличается от config/plan.yaml,
назвать расхождение; не выдавать прежний норматив за новую цель и не менять план молча.
Для этой проверки не нужны дополнительные API или полный рекламный разбор.
Проверено 12.09.2026. Маршрут для «как дела по план-факту», «вчера и 7 дней по позициям», «заказы против плана», «плановое CPO на основе стоимости корзин». Поддерживается Siluetta Ozon, shop_id=4. WB и другие магазины не включать.
Один запуск
Из корня Bot_TG:
.\report.cmd plan-fact
.\report.cmd plan-fact --format text
.\report.cmd plan-fact --date yesterday
.\report.cmd plan-fact --period "за неделю"
.\report.cmd plan-fact --since 2026-09-05 --to 2026-09-11
Из корня Big-System-MP:
.venv\Scripts\python.exe scripts\report_cli.py plan-fact
Без периода — вчера, последние 7 закрытых дней и предыдущие 7 дней в одном JSON.
Все относительные даты по Москве. Текущий день запрещён. Магазин по умолчанию —
siluetta-ozon; можно указать явно. --summary-only убирает строки по позициям.
Фильтры остатков и --sku здесь не поддерживаются.
Windows-команда сама вызывает Hetzner через общий маршрут scripts/report_cli.py.
На Hetzner из /home/mi/Bot_TG/mp-system:
.venv/bin/python scripts/report_cli.py plan-fact --source-label production
Команда читает production-БД через read-only соединение в одной транзакции.
API, запись данных, смена цен/кампаний и отправка сообщений в неё не входят.
Локальный mp.db, старые JSON и разовый скрипт от 12.09 не использовать как fallback.
Штатному запросу не нужны SSH-SQL, исследование кода, соседний репозиторий и полный
финансовый/рекламный анализ. После получения JSON использовать его; дополнительные
плейбуки открывать только для недостающей диагностики.
Источники и границы плана
| Что | Источник |
|---|---|
| План | config/plan.yaml: need_per_day, ads, buyout_planned, match |
| Все заказы и корзины | ozon_sku_traffic_daily.ordered_units, total_hits_to_cart |
| Полнота трафика | ozon_sku_traffic_runs: success, rows_count; реальные строки и корзины |
| Весь расход | ozon_ad_metrics_daily.money, включая впоследствии отключённые РК |
| Выкуп для норматива | ozon_postings + ozon_posting_items.quantity |
| Коэффициенты | существующий app/web/plan_fact.py::compute |
| Покрытие и JSON | app/reports/plan_fact.py, вход scripts/report_cli.py |
Заказы — заказанные единицы с отменами, не количество ID и не выкупы. Факт
всегда из аналитики карточек, в том числе при остановке постингов. Не менять источник
между периодами. Страница /plan-fact использует постинги и включает WB: её общий
итог может отличаться; формулы коэффициентов остаются общими.
Артикулы объединять по явному match и существующему удалению размерного суффикса.
Не использовать sku_alias: skirt-atlas-max-2 не равен skirt-milk.
Двойной match вызывает ошибку. Несопоставленные offer_id — в unmatched_offers;
для контроля всего магазина — total.shop_orders, total.shop_ad_spend.
Позиции без привязки к магазину — excluded, не нулевые продажи. on_shelf — запись
плана, не проверка сегодняшних остатков. Норматив задан на общий пул, включая будущие
партии, и не распределён между магазинами. Итог называть «выполнение общего норматива
выбранных товаров силами Siluetta Ozon». После deadline сравнение справочное.
При обсуждении цен читать ../Strategy-Operations/docs/Цены — плановые по артикулам.md:
решения владельца сильнее арифметического пола. Для обычного получения чисел это не нужно.
Формулы
A = ads из плана, рубли на выкуп (CPS), не дневной бюджет и не ставка РК.
b = выкуп; q = корзина→заказ. Коэффициенты — доли, не проценты.
Заказы план/день = need_per_day / b
Заказы план за период = план/день × все календарные дни запрошенного периода
CPO план = A × b
CPL план = A × b × q
CPL факт = весь расход / все корзины
CPO через корзины (оценка) = CPL факт / q
CPS через корзины (оценка) = CPL факт / (q × b)
CPO факт = весь расход / все заказанные единицы
Рекламные atbs и атрибутированные orders + orders_model здесь не используются.
Не заменять этот отчёт report.cmd ads: у него другой знаменатель.
q — отношение сумм заказов и корзин скользящего окна buyout_window_days (сейчас 60)
до вчера, только по полным дням с ненулевыми общими корзинами магазина. Своя конверсия:
≥50 корзин и 0 < q <= 1; иначе медиана по правилу существующего план-факта, с пометкой.
Если нет медианы — неизвестно. Не закреплять HZ020 на медиане навсегда: проверять
отношение каждый раз. Число пригодных дней — methodology.conversion_covered_days.
Это отношение потоков периода, не отслеживание конкретной корзины.
b = delivered / (delivered + cancelled/canceled) за скользящее окно. При <20
разрешившихся единицах используется buyout_planned, buyout_source=plan. Считать
SUM(quantity), не COUNT строк. Для b сохраняется календарная конвенция постингов
действующего план-факта; московские даты спроса берутся из трафика карточек.
Это не зрелая когорта с отступом 15 дней; возвраты после вручения не вычтены.
Коэффициенты общие на дату расчёта для всех периодов. Исторический запрос —
ретроспектива с нынешним планом/коэффициентами, не реконструкция старого плана.
Корзинный прогноз не доказывает финансовую прибыль и не заменяет финансовый CPS.
ads=0 — действительный нулевой бюджет. Расход при нём обязательно показать.
Нулевой расход при корзинах даёт CPL/CPO=0: это отсутствие расходов за окно.
При нуле заказов фактический CPO неизвестен. <30 корзин — малая выборка; день не тренд.
Полнота: сначала флаги
JSON: source, shop, periods, freshness, methodology, warnings, excluded,
elapsed_ms. В периодах: абсолютные даты, rows, total, coverage, unmatched_offers.
fact_complete: есть дневной трафик для заказов и строки каждой позиции;funnel_complete: дополнительно корзины и реклама на всех тех же днях;complete: всё выше, свежие постинги, определённая конверсия, действующий срок плана и отсутствие несопоставленных позиций.
complete=false при старом выкупе не отменяет свежие заказы: назвать конкретную причину.
Постинги свежие, если fetched_at не раньше начала сегодняшнего дня МСК. Это проверка
возраста, не независимый аудит полноты API. Реклама требует строк на каждый день и
fetched_at после окончания этого дня МСК; отсутствие строк магазина не доказывает ноль.
Контроль не заменяет сверку Performance с finance при полном рекламном разборе.
Если день пропущен/неполон, соответствующие полные периодные метрики = null;
план сохраняет исходную длительность. observed_aligned — отдельный частичный срез:
расход и корзины строго на coverage.aligned_dates. Не выдавать его за полную неделю
и не уменьшать план до числа доступных дней. Нет строки артикула — неизвестно, не ноль.
Добор при необходимости
- Назвать недостающие даты/источники из JSON. Проверить, что сборщик того же источника не работает. Аналитический кэш обновлять штатно от mi, из корня production: WAL от root ломает последующие сборы.
- Корзины добирать только за отсутствующие даты, последовательно:
cd /home/mi/Bot_TG/mp-system
sudo -u mi .venv/bin/python scripts/backfill_ozon_sku_traffic.py YYYY-MM-DD YYYY-MM-DD 4
collect_ozon_sku_traffic.py собирает только вчера. Success с нулевыми корзинами
при живых заказах может означать отсутствие платной аналитики. Не повторять backfill
бесконечно; назвать проблему подписки/сессии. Покупка подписки требует команды владельца.
- Для старого выкупа при необходимости один штатный прогон:
sudo -u mi .venv/bin/python scripts/ingest_ozon.py --shop-id 4 postings --days 60
После встроенных повторов и OzonRateLimitError/429 остановиться, не запускать снова
и не менять лимиты. Дать свежие заказы из трафика с оговоркой о старом/плановом выкупе.
Добор рекламы — по data/notes/ozon/ads-playbook.md, без параллельных Performance-выгрузок.
4. Повторить report.cmd plan-fact. Он читает production напрямую: не ждать и не
форсировать синхронизацию часового snapshot ради этого отчёта.
Ответ и чек-лист
Начать с состояния и покрытия. За неделю и вчера — отдельные таблицы:
Артикул | Заказы факт / план | Выполнение % | Все корзины | Весь расход
| CPL факт / план | CPO через корзины / план
Итог обязателен. Общий CPL/CPO факт — отношение сумм; проценты/стоимости строк не усреднять. Общий плановый CPO и смесь прогнозов по разным коэффициентам не изобретать. Выделить отставание по темпу, превышение CPO, расход при нулевом бюджете и выполнение обоих условий. Предыдущую неделю сравнивать только при полном покрытии. Дешёвая корзина не доказывает качество трафика, причинный эффект рекламы и прибыль.
При необходимости сохранять срез с временем и JSON в data/ads/YYYY-MM-DD/, не
перезаписывая предыдущий. Архивы не входят в синхронизацию плейбуков. Настройки
кабинета по итогам анализа автоматически не менять.
- Магазин 4, закрытые московские дни, один источник заказов?
- Названы пропуски, возраст постингов и fallback коэффициентов?
- CPO заказа и CPS выкупа различены; прогноз подписан?
- Все корзины и весь расход на одинаковых датах?
- План на исходное число дней, будущие партии и общий товарный пул названы?
- Нулевой бюджет, unmatched/excluded и малые выборки показаны?
- Нет выводов о прибыли/причинности только из корзинной оценки?
Контроль 12.09 после добора: 05–11.09 — 348 единиц, 1266 корзин, 40 717,33 ₽;
11.09 — 52, 165, 5924 ₽. Источники корректируются задним числом: это контрольный
срез, не числа для подстановки. Проверки:
.venv\Scripts\python.exe -m unittest tests.test_plan_fact_report tests.test_report_cli.