AAYMA
RU
Войти

ProcureAI

Детерминированный расчёт рекомендаций по пополнению склада с группировкой по поставщикам и обоснованием для менеджера, который утверждает заказ сам.

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

О проекте

Команда: Solocore
Кейс HackAlem.ai: Логистика — Электрокомплект · ekt.kz
Стадия: Рабочий MVP

Задача кейса: Автоматический расчёт заказов поставщикам для пополнения склада, с утверждением менеджером

Суть

Детерминированный расчёт рекомендаций по пополнению склада с группировкой по поставщикам и обоснованием для менеджера, который утверждает заказ сам. Интерфейс на Streamlit.

Как устроено

Синтетические продажи, остатки и stockout-флаги (5 демонстрационных SKU со сценариями stable, seasonal, growth, stockout, outlier) проходят пайплайн: валидация → исключение разовых крупных заказов → маскирование stockout и восстановление спроса → устойчивый тренд (медиана YoY, минимум 5 положительных сравнений, ограничение 30%) → сезонные факторы (0.5–2.0) → потребность на горизонт lead_time+14 → вычет остатка и товаров в пути → округление до упаковки → срочность по days_of_cover. Для сравнения есть наивный baseline. Модуль procure/ai собирает объяснение из проверенного контракта фактов (шаблон и валидация).

Ключевые решения

  • детекция разовых выбросов
  • восстановление спроса при stockout
  • YoY-медиана тренда
  • сезонное разложение
  • формула заказа с pack_size
  • days_of_cover urgency
  • naive baseline

Особенности

Обоснован порядок шагов: выбросы удаляются до оценки тренда, stockout маскируется до расчёта сезонности. Метод 3σ отвергнут, потому что на 24 точках выброс сам раздувает σ. Демоданные генерируются с фиксированным seed и совпадают байт в байт. README местами отстаёт от кода: части помечены «в разработке», хотя коммиты их уже реализуют.

Стек

Python · Streamlit · pandas · pytest


Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-ef26dce6-solocore.

README

README.md

ProcureAI — автоматический расчёт заказов поставщикам

ProcureAI готовит проверяемую рекомендацию по пополнению склада для менеджера отдела закупа.

Проблема

Менеджер отдела закупа считает потребность вручную в Excel. Расчёт трудоёмкий, поэтому выполняется редко. Две системные ошибки искажают результат:

  • разовая крупная продажа одному клиенту завышает регулярный спрос и ведёт к избыточному запасу;
  • период отсутствия товара занижает наблюдаемые продажи, и система повторно заказывает мало — дефицит воспроизводится.

Решение

Предусмотрен следующий процесс:

  1. загрузить историю продаж, остатки и признаки отсутствия товара;
  2. отделить регулярный спрос от разовых крупных заказов;
  3. восстановить спрос за подтверждённые периоды отсутствия;
  4. оценить тренд и сезонность;
  5. рассчитать потребность на горизонт пополнения;
  6. вычесть остаток и подходящие товары в пути;
  7. сгруппировать рекомендации по поставщикам;
  8. показать факты и обоснование менеджеру;
  9. передать решение об утверждении человеку.

Репозиторий содержит воспроизводимый синтетический набор, детерминированный расчёт рекомендации и интерфейс для проверки результата менеджером. Система рекомендует количество, но не утверждает заказ вместо менеджера.

Быстрый старт

Требуется Python 3.12.

Команды, которые работают в текущем состоянии репозитория:

pip install -r requirements.txt
python -m procure.run              # расчёт по поставщикам
python -m streamlit run app.py     # интерфейс
python -m pytest                   # тесты

После установки зависимостей расчёт и тесты не требуют интернета. Загрузка пакетов командой pip install может требовать интернет или локальный кэш пакетов.

Данные

Все текущие входные данные синтетические. Клиенты обезличены идентификаторами вида CUST-XXX; персональных данных в наборе нет. Использование синтетики прямо разрешено техническим заданием для демонстрации метода.

ФайлКолонкиТипСмысл
data/sales.csvdate, sku, qty, price, customer_id, warehouseCSV, месячные строки по клиентамИстория продаж пяти SKU за 24 месяца
data/stock.csvsku, name, supplier, lead_time_days, stock, in_transit, pack_sizeCSV, одна строка на SKUТекущий товарный справочник, остаток и параметры поставки
data/stockout.csvsku, month, is_stockoutCSV, одна строка на SKU и месяцЯвный признак отсутствия товара в конкретном месяце

Период истории: с октября 2024 года по сентябрь 2026 года включительно. Склад один: WH-01. Генератор использует фиксированный seed 20260920; два запуска создают байт-в-байт одинаковые файлы.

Демонстрационные SKU:

SKUИмяПоставщикЧто заложено в данные
A-100STABLESUP-01Стабильные продажи от 90 до 110 единиц в месяц
A-200SEASONALSUP-0150–70 единиц обычно и 130–160 в ноябре–январе; профиль повторяется по годам
A-300GROWTHSUP-02Плавный рост 40→65 в первый год и 65→100 во второй, без одиночного скачка
A-400STOCKOUTSUP-02Около 90 единиц обычно; 10–20 в апреле и мае 2026 года, оба месяца отмечены как stockout
A-500OUTLIERSUP-03Регулярные 40–55 единиц и одна дополнительная продажа 500 единиц клиенту CUST-017 в марте 2026 года

В каждом месяце регулярный объём распределён между несколькими клиентами, поэтому выброс A-500 виден как событие одного клиента, а не как общий рост спроса.

Методология расчёта

Порядок вычислений

Целевой порядок вычислений:

  1. исходные данные;
  2. валидация;
  3. исключение выбросов;
  4. маскирование stockout;
  5. тренд;
  6. сезонность;
  7. восстановление спроса;
  8. потребность на горизонт;
  9. вычет остатка и товаров в пути;
  10. округление до упаковки;
  11. срочность;
  12. обоснование.

Этот основной pipeline в разработке. Сейчас код выполняет только генерацию и отдельный наивный baseline без очистки.

Порядок важен по двум причинам: выбросы должны удаляться до оценки тренда, иначе одиночная продажа создаст ложный рост; stockout должен маскироваться до расчёта сезонности, иначе низкие продажи занизят коэффициент соответствующего месяца.

1. Базовый спрос

Основной устойчивый показатель базового спроса в разработке. Окно, минимальное число наблюдений и выбор между средним и медианой ещё не реализованы; до появления кода README не фиксирует эти параметры.

Реализован только наивный показатель для контрольного сравнения. Код сначала суммирует все клиентские строки по SKU и месяцу, затем берёт среднее месячных сумм по всем 24 месяцам:

naive_monthly_demand = mean(sum(qty) по каждому месяцу за 24 месяца)

Наивный показатель намеренно не очищает выбросы и не исправляет stockout. Он моделирует простой ручной расчёт и служит точкой сравнения, а не итоговой рекомендацией.

2. Исключение разовых крупных заказов

Алгоритм исключения разовых крупных заказов в разработке. Числовые пороги в коде отсутствуют и здесь не задаются. Критерий, окно сравнения и минимальное число наблюдений ещё должны быть реализованы и проверены тестами.

Планируемые признаки для проверки:

  • размер клиентской продажи относительно обычного уровня SKU;
  • единичность события для конкретного клиента;
  • повторяемость сопоставимых заказов того же клиента;
  • наличие аналогичного сезонного пика в другом году;
  • согласованность роста между несколькими последовательными периодами.

Метод 3σ не принят и не реализован. На истории из 24 месячных точек одна крупная величина одновременно повышает среднее и стандартное отклонение, поэтому порог может стать практически недостижимым. Перед выбором другого критерия нужны измеримые тесты на синтетике и затем калибровка на разрешённых реальных данных.

Полное или частичное исключение объёма в разработке. Код пока не удаляет и не корректирует ни одну продажу. Исходное событие должно сохраняться для аудита независимо от будущего правила очистки.

Начало устойчивого роста нельзя определять по одной крупной продаже. Оно должно подтверждаться несколькими последовательными периодами и несколькими клиентами. Конкретное окно и доля подтверждающих периодов в разработке.

Главный риск ложноположительного решения — принять реальный новый регулярный заказ за разовый. Ограничители этого риска в разработке. До их реализации результат по A-500 существует только в наивном baseline и остаётся завышенным.

3. Восстановление спроса за периоды отсутствия

Восстановление спроса в разработке. Сейчас период отсутствия определяется только входным флагом is_stockout в data/stockout.csv. Для A-400 отмечены ровно два месяца: 2026-04 и 2026-05.

Замена наблюдаемого значения пока не реализована. Она не должна подставлять ноль, потому что низкая продажа при отсутствии товара не доказывает низкий спрос. Будущая оценка должна учитывать обычный уровень SKU и сезон месяца. Точная формула и минимальная история в разработке.

Восстановленный спрос всегда будет оценкой, а не фактом состоявшейся продажи. Этот статус должен сохраняться в фактах и объяснении результата.

4. Сезонность

Сезонность рассчитывается на очищенном и восстановленном помесячном спросе. Требуются минимум 24 месяца истории; при более короткой истории для всех месяцев используется season_factor = 1.0.

residual_m = restored_demand_m / trend_factor^(index_m / 12)
season_factor(month) = mean(residual_m for calendar month) / mean(month factors)

Из расчёта исключаются явно отмеченные stockout-месяцы. Для календарного месяца с менее чем двумя пригодными наблюдениями используется фактор 1.0; остальные факторы ограничиваются диапазоном 0.5–2.0.

ARIMA и Prophet не используются. В текущем проекте нет ML-библиотек. Приоритет отдан прозрачному разложению, потому что каждое изменение заказа должно иметь проверяемое объяснение.

5. Устойчивый рост

Устойчивый рост измеряется по шести последним сопоставлениям год к году и требует минимум 18 месяцев истории.

g_i = demand_i / demand_(i-12) - 1
support = count(g_i > 0)
trend_pct = 100 × clamp(median(g_i), 0, 0.30), if support >= 5; otherwise 0
trend_factor = 1 + trend_pct / 100

Один скачок не считается устойчивым ростом: без как минимум пяти положительных сравнений trend_pct = 0. Верхняя граница — 30%, а отрицательная медиана не уменьшает спрос этим этапом.

6. Формула заказа

Рекомендация рассчитывается из очищенного, восстановленного спроса, сезонного коэффициента месяца ожидаемой поставки и устойчивого тренда.

planning_horizon_days = lead_time_days + 14
expected_daily = restored_demand × season_factor × trend_factor / 30
horizon_need = expected_daily × planning_horizon_days
raw = max(0, horizon_need - stock - in_transit)
recommended_qty = ceil(raw), then rounded up to pack_size

Страховой запас на P0 равен нулю. Остаток и товары в пути вычитаются из потребности; физический остаток не включает товары в пути. Для сравнения с очищенным расчётом naive_qty использует ту же формулу, но raw demand до исключения разовой аномалии.

Реализованный наивный baseline использует более узкую формулу:

naive_qty = max(
    0,
    naive_monthly_demand × lead_time_days / 30 - stock - in_transit
)

Он не добавляет страховой запас, период пересмотра и округление до упаковки.

7. Срочность

Срочность использует только физический остаток и ожидаемый суточный спрос:

days_of_cover = stock / expected_daily

При нулевом или ненадёжном спросе статус — unknown. Иначе: high при покрытии меньше срока поставки; medium от срока поставки включительно до lead_time_days + 14; low при покрытии не меньше lead_time_days + 14.

Что на выходе

Текущий --naive печатает таблицу:

КолонкаТипСмысл
skuстрокаИдентификатор товара
naive_monthly_demandчислоСреднее месячных сумм без очистки
naive_qtyчислоКоличество по наивной формуле

Итоговая таблица рекомендаций в разработке. Контракт ItemFacts уже фиксирует поля спроса, сезонности, тренда, горизонта, остатка, товаров в пути, покрытия, выброса, количества, срочности и флагов.

Группировка строк по поставщикам, CSV-экспорт и XLSX-экспорт в разработке.

Целевой формат короткого детерминированного обоснования:

На горизонт нужно 200; в наличии 60; в пути к сроку 40; рекомендуем 100.

Это пример формата, а не результат текущего набора. Фактическое обоснование должно строиться из фактов конкретной строки; реализация в разработке.

Роль AI

Все числа рекомендации вычисляет детерминированный Python-код. Рабочий режим объяснений — детерминированный русский шаблон: он подставляет только рассчитанные факты и не меняет количество, поставщика или срочность.

Языковая модель не требуется для работы продукта и остаётся опциональным будущим дополнением. Сетевые вызовы для объяснений сейчас отсутствуют; шаблон работает без API-ключа и доступа к интернету.

Как проверить

pip install -r requirements.txt
python -m procure.run              # расчёт по поставщикам
python -m streamlit run app.py     # интерфейс
python -m pytest                   # 52 теста

На Windows тесты запускаются именно через python -m pytest, чтобы корень проекта оставался доступен для импорта пакета procure.

СценарийЧто проверяетРезультат
Разовая продажаисключение выбросаA-500: 124 → 49 шт (−60.5%)
Дефицитвосстановление спросаA-400: 84.5 → 90.3 шт/мес
Сезонкоэффициент месяца приходаA-200: пик 1.74–1.86
Ростустойчивый трендA-300: коэффициент 1.30
Стабильныйочистка не меняет результатA-100: 89 = 89
Изменение входапересчёт без правки кодаВ пути 0→20: 49 → 29 шт
Без моделичисла идентичныAI_MODE=off, 12 SKU

Ограничения подхода

  • На реальной базе из 145 товаров минимальный остаток задан только у 10 (7%). Расчёт от порога покрывает малую часть ассортимента — это обоснование подхода от прогноза спроса.
  • 70% товаров не имеют продаж за последний месяц. На таких позициях система возвращает «Нет данных», а не ноль.
  • Сезонность требует 24 месяцев истории. На коротких рядах коэффициент равен 1.0: выдуманная сезонность хуже её отсутствия.
  • Порог выброса median + 4×MAD откалиброван на синтетических данных и требует проверки на реальном ряде продаж.
  • Открытые заказы засчитываются без проверки ожидаемой даты. Зависший заказ бессрочно уменьшает потребность. Будущее правило: засчитывать только заказы с ожидаемой датой не позже следующего пересмотра.
  • Кратность упаковки применяется только при подтверждённой связи товар—поставщик.
  • Цены в демонстрационных данных синтетические.
  • Восстановленный спрос является оценкой, а не фактом; текущий набор содержит один склад и не моделирует межскладские перемещения.
  • Наивный baseline нельзя использовать как итоговую закупочную рекомендацию; совместимость с 1С не проверялась.

Масштабирование

Текущие CSV и обработка pandas достаточны для маленького демонстрационного набора. При сотнях тысяч SKU и многолетней истории месячная агрегация и расчёт факторов должны выполняться инкрементально и кэшироваться по каждому SKU; сами правила расчёта при этом не меняются.

Для такого объёма следует инкрементально считать месячные агрегаты, пересчитывать только SKU с новыми данными или изменённой политикой и хранить версию входа вместе с параметрами метода. Можно кэшировать проверенные месячные агрегаты, признаки доступности, сезонные профили и результаты по неизменившимся SKU.

Разбиение по SKU и складу допускает параллельную обработку независимых групп. Экспорт и объяснения можно строить после числового расчёта отдельным этапом. Переход от CSV к колоночному формату или базе данных потребуется только после измерения объёма и времени.

Смысл текущего подхода сохраняется: сначала формируется проверяемый регулярный спрос, затем прогноз на явный горизонт, после чего из потребности вычитаются доступные запасы и подходящие поставки. Меняется способ хранения и выполнения, а не причинная структура расчёта.

Реализация масштабирования не входит в текущий этап.

Структура проекта

procure/
├── __init__.py          пакет ProcureAI
├── contracts.py         замороженные dataclass-контракты строк и объяснений
├── audit.py             аудит расчёта — в разработке
├── export.py            экспорт результата — в разработке
├── run.py               CLI генерации, baseline и заглушки pipeline
├── data/
│   ├── __init__.py      пакет работы с данными
│   ├── generate.py      детерминированный генератор трёх CSV
│   └── load.py          загрузка и валидация входа — в разработке
├── calc/
│   ├── __init__.py      пакет расчётов
│   ├── naive.py         реализованный наивный baseline
│   ├── outliers.py      исключение выбросов — в разработке
│   ├── stockout.py      маскирование stockout — в разработке
│   ├── trend.py         устойчивый тренд — в разработке
│   ├── season.py        сезонные коэффициенты — в разработке
│   ├── restore.py       восстановление спроса — в разработке
│   └── order.py         итоговая рекомендация — в разработке
└── ai/
    ├── __init__.py      пакет объяснений
    ├── template.py      шаблонный fallback — в разработке
    ├── prompt.py        безопасный промпт — в разработке
    ├── render.py        подстановка фактов — в разработке
    ├── validate.py      валидация ответа — в разработке
    └── explain.py       оркестрация объяснения — в разработке

data/
├── sales.csv            синтетическая история продаж
├── stock.csv            остатки и параметры пяти SKU
└── stockout.csv         месячные признаки отсутствия

tests/
├── test_contracts.py    неизменяемость контрактов
└── test_data.py         воспроизводимость набора и свойства fixture

requirements.txt         pandas, numpy, openpyxl и pytest
README.md                состояние, методология и ограничения проекта
Звёзды
⭐ 0
Форки
0
Язык
Python
Последний push
в прошлом месяце
Коммиты
35
Авторы
1
Файлы
47
Ветки
2

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

Yergaly 35

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

  • 2026-09-23 17:51UI-08: add system explanation panelYergaly
  • 2026-09-23 17:41T006l: lock AI system facts contractYergaly
  • 2026-09-23 17:35UI-07: persist transit state across SKU selectionYergaly
  • 2026-09-23 17:08T006k: protect transit demo invariantsYergaly
  • 2026-09-23 17:07UI-06: implement truthful Procure AI demo interfaceYergaly
  • 2026-09-23 16:58WORK-03: harden demo reproducibility and repo hygieneYergaly
  • 2026-09-23 16:47T006j: lock demo calculation contractYergaly
  • 2026-09-23 16:27UI-04: demo polishYergaly
Все коммиты (35)
  • 2026-09-23 16:24T006g: fix pytest.ini encoding, confirm demo numbersYergaly
  • 2026-09-23 16:17chore: pytest temp dirYergaly
  • 2026-09-23 16:16T006e: realistic urgency spreadYergaly
  • 2026-09-23 16:12docs: verification sectionYergaly
  • 2026-09-23 16:10docs: verification section and updated exportYergaly
  • 2026-09-23 16:08chore: commit demo dataset and sample exportYergaly
  • 2026-09-23 16:05UI-03: seasonal reason in explanationYergaly
  • 2026-09-23 16:05T006d: restore naive baseline semanticsYergaly
  • 2026-09-23 16:03REG-01: end-to-end scenario testsYergaly
  • 2026-09-23 15:57docs: evidence-based limitationsYergaly
  • 2026-09-23 15:56T002b: realistic electrical catalogueYergaly
  • 2026-09-23 15:55T007: deterministic explanationYergaly
  • 2026-09-23 15:53T006b: align naive baseline and fix urgency gapYergaly
  • 2026-09-23 15:52UI-02: connect real pipelineYergaly
  • 2026-09-23 15:48T006a: stronger outlier contrast in demo dataYergaly
  • 2026-09-23 15:45T006: order quantity, urgency, supplier groupingYergaly
  • 2026-09-23 15:42UI-01: screen on fixturesYergaly
  • 2026-09-23 15:39T005: trend and seasonalityYergaly
  • 2026-09-23 15:21test: cover stockout timeline edge casesYergaly
  • 2026-09-23 15:07feat: restore demand during stockoutsYergaly
  • 2026-09-23 14:48feat: detect one-off demand anomaliesYergaly
  • 2026-09-23 14:37docs: freeze execution contractYergaly
  • 2026-09-23 14:25Merge remote mainYergaly
  • 2026-09-23 14:22Add project filesYergaly
  • 2026-09-23 14:18docs: detailed README with methodologyYergaly
  • 2026-09-23 14:08T002: synthetic dataset and naive baselineYergaly
  • 2026-09-23 13:58T001: project skeleton and frozen contractsYergaly
Структура репозитория (47 файлов)
.gitignore
.streamlit/config.toml
AGENTS.md
README.md
TASK_PROMPT.md
app.py
calculation.md
data/sales.csv
data/stock.csv
data/stockout.csv
out/orders.csv
procure/__init__.py
procure/ai/__init__.py
procure/ai/explain.py
procure/ai/prompt.py
procure/ai/render.py
procure/ai/template.py
procure/ai/validate.py
procure/audit.py
procure/calc/__init__.py
procure/calc/naive.py
procure/calc/order.py
procure/calc/outliers.py
procure/calc/restore.py
procure/calc/season.py
procure/calc/stockout.py
procure/calc/trend.py
procure/contracts.py
procure/data/__init__.py
procure/data/generate.py
procure/data/load.py
procure/export.py
procure/run.py
pytest.ini
requirements.txt
tests/fixtures/ui_items.py
tests/test_ai_facts_contract.py
tests/test_contracts.py
tests/test_data.py
tests/test_demo_contract.py
tests/test_order.py
tests/test_outliers.py
tests/test_scenarios.py
tests/test_season.py
tests/test_stockout.py
tests/test_template.py
tests/test_trend.py
Ветки (2)

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

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

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