Skip to content

Repository files navigation

gmc-forecaster

Двухшаговая модель «доля рынка → спрос» для прогнозирования спроса в бизнес-симуляции Global Management Challenge (pandas, scikit-learn).

Установка

Проект использует uv. Весь код запускается только внутри виртуального окружения через uv run — окружение и сама утилита ставятся автоматически при первом запуске:

uv sync

Запуск

Утилита вызывается командой gmc-forecaster и имеет три подкоманды:

  • forecast — прогноз спроса на следующий квартал под решениями игрока;
  • backtest — оценка качества модели на исторических данных;
  • cost — полная себестоимость и contribution margin по 9 ячейкам.
uv run gmc-forecaster --help
uv run gmc-forecaster forecast --help
uv run gmc-forecaster backtest --help
uv run gmc-forecaster cost --help

forecast — «крутить цены/рекламу»

Основной рабочий интерфейс: считает спрос на следующий квартал по всем 9 ячейкам (3 продукта × 3 канала) под решениями игрока. Решения читаются прямо с первого листа 'Your decisions' файла --current — правишь свои what-if цены/рекламу/дистрибьюторов в этом листе Excel и запускаешь прогноз (json-файл сценария больше не нужен).

uv run gmc-forecaster forecast \
    --current  W115264.xls \
    --train    W1152*.xls W1312*.xls \
    --history  Hst*.xlsx \
    --out      forecast.csv
  • --current — текущий отчёт (обязательно); решения берутся с его первого листа 'Your decisions';
  • --train — конкурентные отчёты для обучения логит-модели доли (обязательно);
  • --history — файлы группы 0 для оценки сезонности (опционально);
  • --dist-k-n, --dist-k-comm — сила эффекта числа/комиссии дистрибьюторов (опционально, дефолт 0.15 / 0.10);
  • --out — сохранить прогноз в CSV (опционально); коэффициенты при этом дублируются в sibling-файл <out>.coef.csv;
  • --coeffs {key,full,none} — объём блока коэффициентов модели доли (дефолт key; подробнее ниже).

С листа 'Your decisions' считываются (по позиционной, языконезависимой схеме):

  • цены по 9 ячейкам (абсолютные, ерз);
  • реклама — имидж по 3 каналам + товарная по 9 ячейкам, сворачивается в множитель рекламного бюджета уровня компании;
  • число и комиссия (%) дистрибьюторовпер-канальные (EAEU/ASEAN/INT), действуют на все 3 продукта канала. Наблюдаемы только у своей компании → дают множитель собственной привлекательности в логите доли (конкуренты не затронуты). У канала INT дистрибьюторов нет (в данных всегда 0).

База сравнения (спрос_база, Δ_рычаг) — исходные решения квартала с листа W; правишь форму решений → колонки _сцен и Δ_рычаг показывают эффект. Незатронутые ячейки листа наследуют текущие значения → Δ_рычаг=0. Квартал прогноза берётся из --current (следующий за кварталом отчёта).

Колонка Δ_рычаг_% в прогнозе изолирует эффект решения: сезонность есть и в базе, и в сценарии, поэтому в отношении она сокращается.

Если у отчёта есть производственные данные (компания 1–8), таблица прогноза дополняется колонками себестоимости: себест_полн_ед (полная себестоимость единицы), CM_ед (contribution margin) и прибыль_ячейка (= (цена_сцен − себест_полн_ед) × спрос_сцен) — прогноз спроса сразу превращается в прибыль на ячейку (см. подкоманду cost ниже). В history-режиме (без своих производственных данных) эти колонки пропускаются с предупреждением.

Модель доли (стадия 1) учитывает гетерогенность эластичности по группам (свой наклон цены усаживается к глобальному при нехватке данных — partial pooling) и исключает неактивные компании (цена 0 — не предлагают продукт, иначе забрали бы всю долю). Обоснование, бэктест и разбор — в docs/FINDINGS.md.

После таблицы прогноза печатается блок коэффициентов модели доли с их значимостью (коэффициент, ст. ошибка, t, p, звёзды) и качеством подгонки (R²/n/edf/λ), плюс строка смысла β_price (эластичность доли по цене):

Коэффициенты модели доли (стадия 1) | R²=0.909 n=504 edf=27.0 | ridge λ=1e+03 ...
  признак                     коэф   ст.ош      t       p  знач
  log(цена)                 -3.549   0.237 -15.00   0.000  ***
  log(реклама+1)             0.024   0.058   0.41   0.679
  рейтинг (звёзды)          -0.166   0.122  -1.37   0.171
  наклон Δ группы 11        -0.002   0.001  -2.96   0.003  ***
  Смысл: β_price=-3.55 → эластичность доли по цене ≈ β_price·(1−s); при s=8% ≈ -3.25.
  • --coeffs key (дефолт) — константа, ключевые признаки (цена/реклама/ рейтинг) и наклоны цены по группам u_g;
  • --coeffs full — до-выводит все фиксированные эффекты (ячейка/группа/фирма);
  • --coeffs none — выключает блок.

Ст. ошибки — из сэндвич-ковариации штрафованного МНК (для непенализуемых признаков совпадает с обычной OLS); p-value — нормальное приближение. Для штрафуемых наклонов u_g значимость приблизительная.

Первая итерация half-final (только история, без долей)

В начале полуфинала/финала на руках лишь history-кварталы своей компании (группа 0, компания 0, лист 'Your decisions' помечен This is a history quarter) — реального отчёта с долями рынка ещё нет: доли помечены Not requested, а 8 «компаний» листа W в них идентичные клоны. Конкурентную модель доли (стадию 1) по таким данным построить нельзя.

Если --current — такой history-квартал, forecast автоматически переключается в упрощённый режим:

  • бейзлайн спроса = персистенция наблюдаемого спроса по ячейке (последний квартал × сезонность, если задана);
  • рычаг решений = заимствованная собственная эластичность из модели, обученной на регулярных отчётах с долями (--train): Δln(спрос) ≈ β_price·Δln(цена) + β_adspend·Δln(реклама+1) + ln(dist_mult), демпфируется --lever-k.
uv run gmc-forecaster forecast \
    --current  half-final/1.xlsx \
    --train    W1152*.xls W131*.xls W318*.xls W496*.xls

--train (регулярные отчёты с долями) здесь обязателен — он даёт коэффициенты эластичности. Правишь цены/рекламу/дистрибьюторов на листе 'Your decisions' файла --current — колонка Δ_рычаг_% показывает эффект своих решений; колонка доля_сцен_% не заполняется (долей нет). Это грубый перенос эластичностей из чужого контекста → низкая уверенность, о чём forecast печатает предупреждение. Как только появится реальный отчёт своей компании (компания 1–8 с долями), включится штатный конкурентный режим без изменений в команде.

backtest — оценка качества модели

Прогоняет модель по всем смежным парам кварталов (Q_t → Q_{t+1}) в --reports и сравнивает прогноз с фактом из отчёта Q_{t+1}. Сценарий не нужен: фактические решения следующего квартала уже лежат в его отчёте, а модель всё равно реагирует лишь на цену (по ячейкам) и рекламу (одно число на компанию) — их берём из файла напрямую, без потерь. Прочие решения (НИОКР, производство, per-cell/имидж-реклама) в модель не входят → их влияние на спрос и есть измеряемая ошибка.

uv run gmc-forecaster backtest \
    --reports W1152*.xls W1312*.xls \
    --history Hst*.xlsx \
    --out     backtest.csv
  • --reports — отчёты для оценки; пары смежных кварталов ищутся внутри каждой серии (одинаковые группа+компания), обязательно;
  • --train — файлы для обучения модели доли (опционально, дефолт = --reports; для честного OOS передай сюда только чужие группы/кварталы);
  • --history — файлы группы 0 для сезонности (опционально);
  • --out — сохранить детализацию по ячейкам в CSV (опционально).

Ошибка раскладывается на две стадии (ломаются по-разному) и меряется в двух режимах подачи входов:

  • стадия 1 (доля) — предсказанная доля vs фактическая (MAE, п.п.); чистый тест логита, считается и по всем 8 компаниям;
  • стадия 2 (объём)sold_t/share_t × сезонность vs sold_{t+1}/share_{t+1} (MAPE);
  • сквозной спрос = доля × объём (MAPE), против бейзлайнов — персистенция (спрос_{t+1}=спрос_t) и сезонный наив;
  • режим realistic — конкуренты заморожены на Q_t, меняются только свои цена+реклама (ровно то, что делает forecast); режим oracle — подаётся фактическое рыночное состояние Q_{t+1} целиком (изолирует функцию доли). Разрыв между режимами = цена незнания ходов конкурентов.

Если модель не бьёт сезонный наив — это сигнал, что рычаги пока не окупают себя (см. docs/FINDINGS.md §5).

Полный прогон backtest по всем наборам данных (2017–2026) с разбивкой по группам и оговорками — в docs/BACKTEST.md.

cost — себестоимость решения по 9 ячейкам

Считает полную себестоимость (absorption) и contribution margin на единицу по каждой из 9 ячеек (продукт × канал) для РЕШЕНИЯ, которое ты подаёшь: объём, цены, реклама, комиссии, смены и парк станков читаются с первого листа 'Your decisions' файла --current (parse_decisions), а не из фактов продаж прошлого квартала. Себестоимость строится снизу вверх из нормативов справочника GMC (сырьё 1/2/3 шт, изготовление 60/75/120 мин, машино-час 8 ерз, амортизация 2.5 % и т.д.) и рыночных величин отчёта (спот-цена сырья, зарплаты). Время сборки берётся с листа решений (для этой игры — не константа мануала; сверено с фактически отработанными сборщико-часами).

uv run gmc-forecaster cost \
    --current W115264.xls \
    --train   W11526[1-3].xls \
    --out     cost.csv
# опц.: продажи для базы «прогноз» из результата forecast
uv run gmc-forecaster cost --current W115264.xls --train W11526[1-3].xls \
    --forecast forecast.csv
  • --current — текущий отчёт компании 1–8 (обязательно); объём/цены/реклама/ комиссии/смены/станки — с листа 'Your decisions'; производственные и стоимостные данные — с листов 'Resources and products'/'Financial statements';
  • --train, --history — кварталы своей фирмы для калибровки yield-факторов (брак, болезни, эффективность станков — EWMA) и спот-цены сырья (опционально);
  • --forecast <csv> — результат команды forecast: добавляет колонки выручки/ прибыли на базе «прогноз» (продано = спрос_сцен, клип ≤ план). Без флага — только база «план» (продано = план поставок);
  • --yield-halflife — полураспад EWMA yield-факторов, кв. (дефолт 2);
  • --out — сохранить смету по 9 ячейкам в CSV.

Оборотная база — план поставок (строки 23–25 листа решений), а не проданные единицы: материал/конверсия считаются на произведённом, что убирает искажение «продано со склада». Затраты разносятся на ячейки в два этапа: материал → по единицам плана; конверсия (труд, эксплуатация станков, амортизация, ОТК) — по сборщико-минутам на фактическом времени сборки; канал-затраты → к каналам; реклама (из решений) и прочие накладные → по плановой выручке. Себест/ед и CM/ед считаются на плановом прогоне и не зависят от базы продаж. Три yield-поправки повышают себестоимость (брак/болезни/эффективность станков). Смены и парк станков берутся из решений (труд-пул — фикс из отчёта, v1). Contribution margin = цена − материал − полуфабрикат − комиссия (конверсия/накладные исключены).

Сверка двухрежимна. Предзаполненный лист (решения = исполненным) → смета сходится с COGS + накладные отчёта в пределах inventory-шума (COGS считает проданное, смета — произведённое). Отредактированный лист (what-if) → форвард- оценка, сверка помечается неприменимой. Для history-рампа (компания 0) данных нет → команда завершается с ошибкой.

Разработка

Форматтер, линтер, тайп-чекер и тесты запускаются одной командой:

make validate     # ruff format + ruff check + mypy --strict + pytest
make test         # только pytest

About

Two-stage market-share model for demand forecasting in a business simulation (pandas, scikit-learn).

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages