Null Talisman
Streamlit-приложение для объяснимого расчёта заказов поставщикам: очистка разовых заказов, восстановление упущенного спроса, сезонность/тренд…
О проекте
Команда: Null talisman
Кейс HackAlem.ai: Логистика — Электрокомплект · ekt.kz
Стадия: Рабочий MVP
Задача кейса: Автоматический расчёт заказов поставщикам для пополнения склада, с утверждением менеджером
Суть
Streamlit-приложение для объяснимого расчёта заказов поставщикам: очистка разовых заказов, восстановление упущенного спроса, сезонность/тренд, страховой запас, ручная правка и утверждение с экспортом XLSX/CSV.
Как устроено
CSV/XLSX (синтетика seed 42 или выгрузки партнёра) проходят engine.io → demand → forecast → replenish/pipeline. Разовые строки выявляются по robust z-score (MAD) >6 при существенности >30% и нерегулярности клиента и обрезаются до медианы; спрос в месяцы дефицита масштабируется по доле доступности или заменяется медианой. Сезонность через CMA-12 с подмешиванием категории, тренд Theil–Sen с проверкой знака в обеих половинах окна, страховой запас z·σ·√t (σ только по месяцам с наличием), backtest на 3 отложенных месяцах с WAPE против наивного прогноза задаёт confidence. LLM не используется.
Ключевые решения
- robust z-score MAD
- восстановление спроса при stockout
- CMA-12 seasonality
- Theil–Sen trend
- safety stock z·σ·√t
- backtest WAPE
- BOM-развёртка
Метрики
DEMO-SEASON: WAPE модели 35.5% vs наивное среднее 77.0% на 3 отложенных месяцах; пик/минимум 3.13×
Особенности
Нашли и закрыли утечку будущего в backtest (сезонность пересчитывается только по train) и эффект раздувания σ провалами без разметки дефицита — оба зафиксированы отдельными тестами. Крупный регулярный клиент не обрезается как разовый.
Стек
Python · Streamlit · pandas · NumPy · pytest
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-dc7e6965-null-talisman.
README
README.mdNull Talisman — заказы поставщикам
Проект HackAlem AI 2026 для кейса «Электрокомплект»: объяснимый расчёт пополнения склада. Разовые крупные продажи завышают закупку, а отсутствие товара скрывает спрос. Приложение учитывает эти эффекты, сезонность, рост, остатки, поставки в пути и ограничения поставщиков.
Менеджер выбирает склад и категорию, получает рекомендации по поставщикам, проверяет график и обоснование, правит количество, утверждает заказ и скачивает XLSX/CSV. Автоматической отправки поставщикам нет. Экспорт предназначен для дальнейшего импорта; прямая интеграция с конкретной конфигурацией 1С не заявляется.
План работ на хакатон: docs/PLAN.md, фаза 2 (15:00–18:00): docs/PLAN-2.md
Запуск
Python 3.11+; команды из корня репозитория в активном виртуальном окружении:
python -m pip install -r requirements.txt
python -m streamlit run app/streamlit_app.py
Открыть локальный адрес, который напечатает Streamlit. При отсутствии
data/demo/sales.csv интерфейс сам запускает python -m data.generate.
Генератор создаёт восемь CSV с синтетическими данными (seed 42), 153 товара,
два склада и шесть поставщиков. Существующие демофайлы UI не перезаписывает.
Можно загрузить собственные CSV/XLSX по контракту.
Архитектура
Один процесс Python без БД и отдельного API:
Streamlit → engine.io → demand → forecast → replenish/pipeline → UI → утверждение JSON → engine.export.
engine.io: проверка схем, приведение типов, хеширование клиентов. Дата расчёта ограничивает историю продаж.engine.demand: разовые крупные заказы и восстановление спроса в дни без товара (must-have 3–4).engine.forecast: сезонность, тренд, прирост, backtest и confidence (must-have 2).engine.replenish/engine.pipeline: страховой запас, горизонт, срочность, обоснование, контрактrecommend().app/orders.py: автор, время и неизменяемый снимок утверждённого заказа.engine.export: XLSX/CSV этого снимка.
BOM (материальная ведомость 1С) разворачивается заново при каждом вызове recommend(), а не один раз при загрузке файлов — чтобы правки продаж комплекта сразу отражались в спросе на компоненты, без повторной загрузки данных.
Методология расчёта
Все шаги ниже реализованы в engine/demand.py и engine/forecast.py — детерминированный код, без модели машинного обучения и без LLM. Каждое число в интерфейсе можно проследить до конкретной формулы; это осознанный выбор в пользу объяснимости, которую явно требует трек.
1. Очистка разовых крупных заказов (must-have 4)
Строка заказа — сумма количества по (артикул, склад, дата, клиент). Строка признаётся разовой, только если выполнены все три условия одновременно:
- Выброс по объёму. Robust z-оценка
(qty − медиана) / (1.4826 × MAD)больше 6. Если MAD строки равен нулю (совсем ровный ряд), используется среднее отклонение как запасной масштаб. - Существенность. Сама строка больше 30% медианного месячного спроса по этому артикулу — иначе у редких товаров обычный разброс продаж постоянно выглядел бы как «разовый заказ».
- Нерегулярность клиента. Клиент покупал этот артикул не более чем в 2 разных месяцах. Постоянный оптовик с тем же объёмом не попадает под правило, даже если его строка — статистический выброс.
Найденная строка не удаляется, а обрезается до медианы по артикулу: клиент остаётся обычным покупателем, но сверхобъём выше медианы исключается из расчёта регулярной потребности и записывается в oneoff_excluded — это число и уходит в обоснование и на график красной точкой.
Тест tests/test_demand.py::test_regular_bulk_client_is_not_trimmed отдельно проверяет обратный случай: крупный, но регулярный клиент не обрезается.
2. Восстановление упущенного спроса (must-have 3)
Для каждого месяца считается доля доступности f = (дней в месяце − дней stockout) / дней в месяце:
f ≥ 0.5— фактические продажи делятся наf(месяц был частично доступен, спрос масштабируется вверх пропорционально).f < 0.5— товара почти не было весь месяц, делить на почти ноль опасно, поэтому месяц заменяется медианой всего ряда по этому артикулу.- В обоих случаях восстановленное значение не может превысить 3× медиану ряда — защита от выброса на артикулах с единичной аномалией.
Текущий (незавершённый) календарный месяц исключается из ряда целиком: 2–3 дня месяца дают слишком шумную оценку.
3. Сезонность и тренд (must-have 2)
- Сезонность — отношение факта к центрированному 12-месячному скользящему среднему (CMA-12), усреднённое по месяцам года, нормировано на среднее = 1, клип
[0.3, 3.0].- Меньше 24 месяцев истории — используется сезонность категории (пул всех артикулов категории), артикул получает флаг
short_history. - 24+ месяцев — смесь
w·сезонность_артикула + (1−w)·сезонность_категории, гдеw = min(1, лет_истории / 3): чем длиннее собственная история, тем меньше вес категории.
- Меньше 24 месяцев истории — используется сезонность категории (пул всех артикулов категории), артикул получает флаг
- Тренд — медиана попарных наклонов (Theil–Sen) на десезонализированном ряде за последние 12 месяцев, в %/мес. Тренд принимается, только если знак наклона одинаков в обеих половинах окна (иначе это не устойчивый рост, а один всплеск или разворот) и превышает 1%/мес по модулю. Клип
[−5%, +10%]/мес. - Прирост менеджера — отдельный множитель поверх тренда (контракты, промо), по умолчанию 0%, не подменяет и не удваивает уже найденный тренд.
- Прерывистый спрос (>50% месяцев без продаж): сезонность отключается (индекс = 1 везде), уровень — среднее по ненулевым месяцам, домноженное на их долю.
- Новый артикул (<3 месяцев истории): среднее по имеющимся месяцам,
confidence = low, backtest не считается.
4. Страховой запас и защита от ложной волатильности
Страховой запас = z(уровень_сервиса) × σ × √(горизонт_дней / 30). Уровень сервиса (90/95/98%) задаёт z через квантиль нормального распределения.
Важная деталь: σ считается по месяцам с нормальной доступностью товара (availability_frac ≥ 0.5), а не по всему ряду. Если stockout не размечен в данных, нескомпенсированный провал продаж выглядит как экстремально низкий месяц и искусственно раздувает дисперсию — а значит и страховой запас — сильнее, чем реальная волатильность спроса. Мы нашли это на синтетическом контрольном сценарии: без этой поправки восстановление упущенного спроса могло снижать итоговую потребность, хотя обязано было её увеличивать. Тест tests/test_acceptance.py::test_lost_demand_compensated проверяет это на need_raw, а не только на прогнозе спроса.
5. Confidence и backtest — честная проверка без утечки будущего
Для рядов длиннее 9 месяцев последние 3 месяца прячутся, модель обучается заново только на оставшейся истории (включая пересчёт сезонности — не только уровня и тренда) и сравнивается с фактом через WAPE (Σ|факт − прогноз| / Σ|факт|). Результат сравнивается с наивным прогнозом (среднее за последние 6 месяцев train): если модель хуже наивного больше чем на 10%, confidence = low, иначе high (при 24+ месяцах истории) или medium.
Пересчёт сезонности именно по train-части — не деталь реализации: если сезонность для backtest брать из полного ряда (включая отложенные месяцы), проверка «подглядывает» в будущее и WAPE модели становится обманчиво оптимистичным. tests/test_forecast.py::test_backtest_train_season_index_ignores_held_out_months фиксирует это явно.
Фактические цифры на эталонном артикуле DEMO-SEASON (зимний товар, 36 месяцев истории, демоданные seed=42):
| Показатель | Значение |
|---|---|
| Отношение пикового месяца к минимальному | 3.13× |
| WAPE модели на 3 отложенных месяцах | 35.5% |
| WAPE наивного среднего на тех же месяцах | 77.0% |
| Итог | модель почти в 2 раза точнее наивного среднего |
| Confidence | high |
Это backtest ядра с одним разбиением train/holdout. Страница «Проверки» отдельно использует rolling-проверку с переобучением перед каждым из трёх месяцев и наивным средним всей доступной истории: её WAPE нельзя напрямую сравнивать с таблицей выше. Фактические показатели UI зафиксированы в сценарии демо.
6. Итоговая потребность и срочность
need_raw = прогноз_на_горизонт + страховой_запас − остаток − подтверждённое_в_пути. Горизонт = срок поставки поставщика + цикл заказа. Округление вверх до кратности упаковки, не ниже минимальной партии (MOQ). Срочность: critical, если остатка не хватит до прихода новой поставки (дней хватит < срока поставки); high, если остатка и уже подтверждённого в пути не хватит на весь горизонт; иначе normal.
Ограничения
- Все данные в демо — синтетика (seed=42), сгенерированная
data/generate.py. Абсолютные значения WAPE/confidence на реальных данных партнёра не гарантированы и требуют повторной проверки на их выгрузке. - В контракте нет закупочных цен, поэтому денежная сумма заказа не считается (сумма в тенге сознательно не выводится, а не забыта).
- Модель не использует LLM и внешние API — только
pandas/numpy. Это осознанное решение ради объяснимости каждого числа, а не ограничение по времени.
Конфигурация и хранение
Обязательных секретов нет. Переменные перечислены в .env.example:
CLIENT_HASH_SALT— необязательная соль хеширования клиентов; без неё используется демонстрационная константа. Задаётся переменной окружения процесса;.envавтоматически не загружается.AI_API_KEY— зарезервирована для опционального LLM; в текущем приложении не используется.
Нужны права записи в data/demo/ и data/orders/. CSV демо, JSON утверждений,
виртуальные окружения и секреты исключены из Git. Сохранённые JSON переживают
перезапуск; восстановление состояния редактора из журнала пока не реализовано.
Проверка и демо
python -m pytest -q
Тесты создают данные в отдельных временных директориях. Проверяются контракты, приёмочные сценарии, UI с реальным ядром, ручная правка, утверждение и содержимое XLSX/CSV. Подробности интерфейса, демо и питч.
Ручной сценарий: выбрать демоданные и 23.09.2026 → «Рассчитать» → открыть «Показать обоснование и график» → изменить количество → указать имя → утвердить → скачать XLSX. После следующей правки экспорт блокируется до повторного утверждения. На странице «Проверки» нажать «Прогнать все».
Обновление: после исправления расчёта σ для страхового запаса (см. раздел
«Методология», п. 4 — sigma считается только по месяцам без stockout)
восстановление упущенного спроса увеличивает и прогноз спроса
(forecast_horizon), и итоговую потребность (need_raw) — обе метрики
проверяются в tests/test_acceptance.py::test_lost_demand_compensated и в
сценарии «Восстановление упущенного спроса» на странице «Проверки».
Stockout-проверка требует роста прогноза спроса (forecast_horizon) и потребности
до округления (need_raw). Страница показывает время сценариев и сохраняет
результат для тех же входных данных с отметкой времени. Есть принудительный
повтор без кэша. Быстрый доступ к DEMO-позициям доступен на главной странице.
Исходные XLSX ИЭК / Systeme Electric принимаются без переименования: «Загрузить файлы» → выбрать комплект → проверить настройки импорта → «Подготовить данные» → «Рассчитать». Поддерживаемые отчёты и ограничения — docs/IMPORT.md. В Excel нет клиентов, BOM и точных периодов отсутствия; эти данные не выдумываются. Расчёт сводный, доверие помечается низким. Месячный срез остатка не выдаётся за восстановленный остаток на сегодня. Закупочных цен в исходном контракте нет, поэтому сумма в тенге не рассчитывается; показатели качества на синтетике не доказывают качество на данных партнёра.
Зависимости и сторонние компоненты
Общие ограничения версий находятся в requirements.txt и app/requirements.txt.
Используются pandas и numpy (таблицы и расчёт), Streamlit (UI и AppTest),
Plotly (графики), openpyxl (XLSX), pytest и pytest-cov (тесты).
Демоданные синтетические, генерируются data/generate.py. Дополнительно поддерживаются
предоставленные пользователем отчёты ИЭК / Systeme Electric; исходные файлы и
их записи в Git не включены. Внешние модели и LLM API не используются. При разработке
использованы AI coding tools.
Проверенный запуск
Интерфейс использует настоящее ядро расчёта; инструкции и ручной сценарий описаны в app/README.md. Команды Windows PowerShell из корня репозитория:
py -3 -m venv .venv
$env:PYTHONUTF8 = '1'
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
.\.venv\Scripts\python.exe -m streamlit run app/streamlit_app.py
.\.venv\Scripts\python.exe -m pytest -q
macOS:
python3 -m venv .venv
./.venv/bin/python -m pip install -r requirements.txt
./.venv/bin/python -m streamlit run app/streamlit_app.py
./.venv/bin/python -m pytest -q
На Windows с Python 3.12 проверены в свежем clone установка зависимостей,
генерация демоданных, полный набор из 91 теста, запуск Streamlit, расчёт,
утверждение и открытие XLSX. Пять живых сценариев проходят; на текущем ноутбуке
их холодный прогон занимает около 40 секунд, поэтому перед трёхминутным показом
стоит выполнить их заранее и показать сохранённый результат.
Приёмочные тесты создают свежие данные во временном каталоге, поэтому локальные
data/demo/ и исходные материалы партнёра не изменяются.
Результаты проверок: первая контрольная точка и ускорение расчёта.
Авторы коммитов
История коммитов
- 2026-09-23 17:43fix(ui): finalize release presentationIkaNit
- 2026-09-23 17:27Merge remote-tracking branch 'origin/main'IkaNit
- 2026-09-23 17:20feat(ui): selectable "corporate" theme via impeccable (Operate mode)bagetvann2-cyber
- 2026-09-23 17:19docs: record partner import safeguardsIkaNit
- 2026-09-23 17:15fix: harden partner XLSX importsIkaNit
- 2026-09-23 17:12test: cover aggregate XLSX safety budgetsIkaNit
- 2026-09-23 17:08test: bound partner workbook importsIkaNit
- 2026-09-23 17:02fix: treat missing MOQ markers as defaultsIkaNit
Все коммиты (45)
- 2026-09-23 17:00test: reproduce missing MOQ markerIkaNit
- 2026-09-23 17:00fix: accept unambiguous duplicate MOQ rowsIkaNit
- 2026-09-23 16:58perf(demand,forecast): fully vectorize one-off detection and CMA seasonalitybagetvann2-cyber
- 2026-09-23 16:57test: reproduce duplicate MOQ rowsIkaNit
- 2026-09-23 16:57fix: deduplicate exact transit rowsIkaNit
- 2026-09-23 16:55test: reproduce duplicate transit rowsIkaNit
- 2026-09-23 16:43chore: clean release artifactsIkaNit
- 2026-09-23 16:13docs: record performance verificationIkaNit
- 2026-09-23 16:01fix: balance generated stock coverageIkaNit
- 2026-09-23 16:01perf: scope and preaggregate pipeline contextIkaNit
- 2026-09-23 15:59test: require scoped pipeline contextIkaNit
- 2026-09-23 15:59test: require actionable critical shareIkaNit
- 2026-09-23 15:53perf(pipeline): pre-group monthly/raw/oneoff/stockouts/in_transit by sku+warehousebagetvann2-cyber
- 2026-09-23 15:38fix: keep uncached checks available after the first runAmir Ahmetov
- 2026-09-23 15:33docs: record clean macOS validation and separate units in analyticsAmir Ahmetov
- 2026-09-23 15:29feat: import partner Excel reports and complete demo UI checksAmir Ahmetov
- 2026-09-23 15:20docs: methodology section with real DEMO-SEASON numbers + stale caveat fixbagetvann2-cyber
- 2026-09-23 15:17fix(forecast): eliminate future-data leak in backtest season index + perfbagetvann2-cyber
- 2026-09-23 15:00docs: phase-2 plan (15:00-18:00) with per-person tasks and perf targetsbagetvann2-cyber
- 2026-09-23 14:53docs: add hackathon resources section ($50 OpenAI + $50 NVIDIA credits)bagetvann2-cyber
- 2026-09-23 14:47Merge remote-tracking branch 'origin/main'bagetvann2-cyber
- 2026-09-23 14:45fix: complete UI integration with forecast engine and shared exportsAmir Ahmetov
- 2026-09-23 14:41Merge remote-tracking branch 'origin/main'bagetvann2-cyber
- 2026-09-23 14:39fix: robustness + performance in demand/forecast/pipelinebagetvann2-cyber
- 2026-09-23 14:19fix: integrate supplier UI with real engine and isolated testsIkaNit
- 2026-09-23 14:00docs: record first checkpoint tdd evidenceIkaNit
- 2026-09-23 13:59fix: make reference demo scenarios reproducibleIkaNit
- 2026-09-23 13:59test: cover growth and short-history fixturesIkaNit
- 2026-09-23 13:59feat: add pipeline contract and demo generatorIkaNit
- 2026-09-23 13:59test: cover reference demo scenariosIkaNit
- 2026-09-23 13:59test: define pipeline and demo data contractsIkaNit
- 2026-09-23 13:57feat: replenishment engine (demand, forecast, replenish, pipeline) + acceptance testsbagetvann2-cyber
- 2026-09-23 13:54feat: add supplier order UI, approvals, checks and demo pitchAmir Ahmetov
- 2026-09-23 13:28docs: add hackathon plan (stack, roles, algorithm, timeline)bagetvann2-cyber
- 2026-09-23 12:43docs: add hackathon agent instructionsIkaNit
- 2026-09-23 12:03Add verification commit from ikanit1 to test push accessIkaNit
- 2026-09-23 11:46Add verification commit to test push accessBaget
Структура репозитория (48 файлов)
.env.example .gitignore AGENTS.md DESIGN.md PRODUCT.md README.md app/.gitignore app/README.md app/__init__.py app/backend.py app/charts.py app/checks.py app/orders.py app/pages/2_Проверки.py app/pages/3_Аналитика.py app/partner_import.py app/partner_ui.py app/requirements.txt app/streamlit_app.py app/tests/test_partner_import.py app/tests/test_ui.py app/theme.py data/__init__.py data/demo/.gitkeep data/generate.py data/orders/.gitkeep docs/DEMO.md docs/IMPORT.md docs/PLAN-2.md docs/PLAN.md docs/tdd/nikita-first-checkpoint.tdd.md docs/tdd/nikita-performance-p0.tdd.md docs/tdd/partner-import-real-files.tdd.md engine/__init__.py engine/demand.py engine/export.py engine/forecast.py engine/io.py engine/pipeline.py engine/replenish.py requirements.txt tests/__init__.py tests/test_acceptance.py tests/test_demand.py tests/test_forecast.py tests/test_generate.py tests/test_pipeline_contract.py tests/test_pipeline_performance.py
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.