AAYMA
RU
Войти

Null Talisman

Streamlit-приложение для объяснимого расчёта заказов поставщикам: очистка разовых заказов, восстановление упущенного спроса, сезонность/тренд…

Рабочий MVP
Команда «Null talisman» 7💬 0 комментариевс 23 сентября 2026 г.

О проекте

Команда: 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.md

Null 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)

Строка заказа — сумма количества по (артикул, склад, дата, клиент). Строка признаётся разовой, только если выполнены все три условия одновременно:

  1. Выброс по объёму. Robust z-оценка (qty − медиана) / (1.4826 × MAD) больше 6. Если MAD строки равен нулю (совсем ровный ряд), используется среднее отклонение как запасной масштаб.
  2. Существенность. Сама строка больше 30% медианного месячного спроса по этому артикулу — иначе у редких товаров обычный разброс продаж постоянно выглядел бы как «разовый заказ».
  3. Нерегулярность клиента. Клиент покупал этот артикул не более чем в 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): чем длиннее собственная история, тем меньше вес категории.
  • Тренд — медиана попарных наклонов (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 раза точнее наивного среднего
Confidencehigh

Это 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/ и исходные материалы партнёра не изменяются.

Результаты проверок: первая контрольная точка и ускорение расчёта.

Звёзды
⭐ 1
Форки
0
Язык
Python
Последний push
в прошлом месяце
Коммиты
45
Авторы
3
Файлы
48
Ветки
3

Авторы коммитов

IkaNit 27bagetvann2-cyber 13Amir Ahmetov 5

История коммитов

  • 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
Ветки (3)

Обсуждение 0 комментариев

Войдите, чтобы комментировать

Пока нет комментариев — будьте первым.