ProcureAI
Детерминированный расчёт рекомендаций по пополнению склада с группировкой по поставщикам и обоснованием для менеджера, который утверждает заказ сам.
О проекте
Команда: 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.mdProcureAI — автоматический расчёт заказов поставщикам
ProcureAI готовит проверяемую рекомендацию по пополнению склада для менеджера отдела закупа.
Проблема
Менеджер отдела закупа считает потребность вручную в Excel. Расчёт трудоёмкий, поэтому выполняется редко. Две системные ошибки искажают результат:
- разовая крупная продажа одному клиенту завышает регулярный спрос и ведёт к избыточному запасу;
- период отсутствия товара занижает наблюдаемые продажи, и система повторно заказывает мало — дефицит воспроизводится.
Решение
Предусмотрен следующий процесс:
- загрузить историю продаж, остатки и признаки отсутствия товара;
- отделить регулярный спрос от разовых крупных заказов;
- восстановить спрос за подтверждённые периоды отсутствия;
- оценить тренд и сезонность;
- рассчитать потребность на горизонт пополнения;
- вычесть остаток и подходящие товары в пути;
- сгруппировать рекомендации по поставщикам;
- показать факты и обоснование менеджеру;
- передать решение об утверждении человеку.
Репозиторий содержит воспроизводимый синтетический набор, детерминированный расчёт рекомендации и интерфейс для проверки результата менеджером. Система рекомендует количество, но не утверждает заказ вместо менеджера.
Быстрый старт
Требуется 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.csv | date, sku, qty, price, customer_id, warehouse | CSV, месячные строки по клиентам | История продаж пяти SKU за 24 месяца |
data/stock.csv | sku, name, supplier, lead_time_days, stock, in_transit, pack_size | CSV, одна строка на SKU | Текущий товарный справочник, остаток и параметры поставки |
data/stockout.csv | sku, month, is_stockout | CSV, одна строка на SKU и месяц | Явный признак отсутствия товара в конкретном месяце |
Период истории: с октября 2024 года по сентябрь 2026 года включительно. Склад один: WH-01. Генератор использует фиксированный seed 20260920; два запуска создают байт-в-байт одинаковые файлы.
Демонстрационные SKU:
| SKU | Имя | Поставщик | Что заложено в данные |
|---|---|---|---|
A-100 | STABLE | SUP-01 | Стабильные продажи от 90 до 110 единиц в месяц |
A-200 | SEASONAL | SUP-01 | 50–70 единиц обычно и 130–160 в ноябре–январе; профиль повторяется по годам |
A-300 | GROWTH | SUP-02 | Плавный рост 40→65 в первый год и 65→100 во второй, без одиночного скачка |
A-400 | STOCKOUT | SUP-02 | Около 90 единиц обычно; 10–20 в апреле и мае 2026 года, оба месяца отмечены как stockout |
A-500 | OUTLIER | SUP-03 | Регулярные 40–55 единиц и одна дополнительная продажа 500 единиц клиенту CUST-017 в марте 2026 года |
В каждом месяце регулярный объём распределён между несколькими клиентами, поэтому выброс A-500 виден как событие одного клиента, а не как общий рост спроса.
Методология расчёта
Порядок вычислений
Целевой порядок вычислений:
- исходные данные;
- валидация;
- исключение выбросов;
- маскирование stockout;
- тренд;
- сезонность;
- восстановление спроса;
- потребность на горизонт;
- вычет остатка и товаров в пути;
- округление до упаковки;
- срочность;
- обоснование.
Этот основной 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 состояние, методология и ограничения проекта
Авторы коммитов
История коммитов
- 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 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.