Перейти к содержимому

XYZ-анализ закупок по вариативности

ABC отвечает, сколько денег уходит на категорию. XYZ отвечает, насколько ровно эти деньги уходят: одинаковая сумма каждый месяц или один пик и десять месяцев тишины. Для закупщика разница практическая. Ровный спрос позволяет держать рамочный договор и торговаться объёмом. Рваный — требует запаса, срочных закупок и запаса по цене.

Меру разброса берут относительную: среднеквадратичное отклонение делят на среднее. Это коэффициент вариации, CV. Он безразмерный, поэтому категории с разными суммами можно сравнивать между собой.

Та же purchases, но счёт идёт не по категориям, а по парам «категория — месяц». Сначала сворачиваем заказы в месячные суммы, потом считаем разброс этих сумм. Такой двухшаговый расчёт — обычная схема: сначала агрегат нужного уровня, затем статистика по нему.

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

WITH m AS (
SELECT category, strftime(order_date, '%Y-%m') AS mon, sum(amount) AS spend
FROM purchases
GROUP BY category, mon
),
stat AS (
SELECT category, count(*) AS months, avg(spend) AS avg_spend,
stddev_samp(spend) AS sd
FROM m GROUP BY category
)
SELECT category AS "категория",
months AS "месяцев",
round(avg_spend / 1000, 1) AS "средний, тыс. ₽",
round(100 * sd / avg_spend, 1) AS "CV, %",
CASE WHEN sd / avg_spend < 0.10 THEN 'X'
WHEN sd / avg_spend <= 0.25 THEN 'Y'
ELSE 'Z' END AS "класс"
FROM stat
ORDER BY sd / avg_spend;
┌─────────────────┬─────────┬─────────────────┬───────┬───────┐
│ категория │ месяцев │ средний, тыс. ₽ │ CV, % │ класс │
├─────────────────┼─────────┼─────────────────┼───────┼───────┤
│ Металлопрокат │ 24 │ 5416.6 │ 5.5 │ X │
│ Крепёж │ 24 │ 480.9 │ 5.6 │ X │
│ Химия и ГСМ │ 24 │ 380.4 │ 6.2 │ X │
│ Упаковка │ 24 │ 361.4 │ 8.9 │ X │
│ Электрика │ 24 │ 1287.8 │ 18.1 │ Y │
│ ИТ-оборудование │ 24 │ 2186.1 │ 21.4 │ Y │
│ Логистика │ 24 │ 840.9 │ 39.7 │ Z │
│ Спецодежда │ 24 │ 531.5 │ 60.0 │ Z │
│ Канцелярия │ 24 │ 94.2 │ 65.0 │ Z │
└─────────────────┴─────────┴─────────────────┴───────┴───────┘

Четыре категории попали в X: Металлопрокат, Крепёж, Химия и ГСМ, Упаковка. Их месячные затраты отклоняются от среднего на 5,5–8,9% — это ровный поток, для которого работают годовые договоры и плановые цены.

Две категории в Y: Электрика (18,1%) и ИТ-оборудование (21,4%). Здесь уже видна сезонность: месячные затраты уходят вверх к концу года и остаются высокими в январе.

Три категории в Z. Канцелярия — 65,0%: пик в июле-августе, к новому учебному году, и провал в феврале. Спецодежда — 60,0%: закупки к отопительному сезону. Логистика — 39,7%: декабрьский пик отправок. Для таких категорий бессмысленно считать «средний месячный расход»: он не описывает ни один реальный месяц.

Обратите внимание на пару Металлопрокат — Канцелярия: 130,0 млн ₽ против 2,3 млн ₽ за два года, но по стабильности это разные полюса. ABC поставил бы их в разные классы по деньгам, XYZ — по ритму, и решения нужны оба.

Цифра CV говорит, что разброс есть, но не говорит, какой он формы. Форму видно на месячном ряду. Для Канцелярии за 2024 год:

SELECT strftime(order_date, '%Y-%m') AS "месяц",
round(sum(amount) / 1000) AS "канцелярия, тыс. ₽"
FROM purchases
WHERE category = 'Канцелярия' AND order_date < DATE '2025-01-01'
GROUP BY 1 ORDER BY 1;
┌─────────┬────────────────────┐
│ месяц │ канцелярия, тыс. ₽ │
├─────────┼────────────────────┤
│ 2024-01 │ 21.0 │
│ 2024-02 │ 9.0 │
│ 2024-03 │ 22.0 │
│ 2024-04 │ 53.0 │
│ 2024-05 │ 94.0 │
│ 2024-06 │ 137.0 │
│ 2024-07 │ 177.0 │
│ 2024-08 │ 173.0 │
│ 2024-09 │ 165.0 │
│ 2024-10 │ 135.0 │
│ 2024-11 │ 90.0 │
│ 2024-12 │ 51.0 │
└─────────┴────────────────────┘

Максимум — июль и август (177 и 173 тыс. ₽), минимум — февраль (9 тыс. ₽). Разница почти в двадцать раз. Это не случайный разброс, а календарь: если планировать закупку канцелярии на февраль по «среднему», денег не хватит в августе.

Границы 10% и 25% — рабочая договорённость, а не закон. Сдвиньте их, и те же данные дадут другие классы: часть категорий переедет из X в Y или из Y в Z. Поэтому правило простое: границы фиксируют до расчёта и пишут в описании отчёта. Иначе спор о том, «правильно ли категория попала в Z», превращается в спор о порогах.

Второе: CV считается по выборке (stddev_samp), а не по генеральной совокупности. Для 24 месяцев разница между ними около 2% — не критично, но в отчёте должно быть указано, что именно считали.

Ошибка Что происходит Как правильно
Считать CV по суммам заказов, а не по месяцам Разброс отражает размер заказов, а не регулярность спроса Сначала свернуть в месячные суммы, потом считать разброс
Выбрасывать месяцы без заказов Разброс падает, сезонная категория выглядит стабильной Пустой месяц — это ноль, он входит в ряд
Сравнивать CV в рублях между категориями У крупной категории абсолютный разброс всегда больше Сравнивать относительную величину — сам коэффициент
Считать «средний месячный расход» для категории Z Число не описывает ни один месяц Для Z планируют по пику и по календарю
Менять границы после расчёта Категория «переезжает» в удобный класс Границы фиксируются до расчёта