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

Экономия против базовой цены

Экономия — самая спорная метрика в закупках, потому что её считают по-разному. Сравнение с прошлогодней ценой даёт один эффект, с бюджетом — другой, с рынком — третий. В практикуме база одна, поэтому договоримся о формуле сразу.

Экономия = базовая цена минус факт, в рублях на количество. Базовая цена — это baseline_unit_price в позиции заказа: референс, от которого отсчитывают эффект. Считаем по позициям, а не по заказам: базовая цена задана на единицу товара, и складывать проценты по заказам с разным составом нельзя — получится среднее из средних.

Нужна purchase_lines: количество, цена факта и базовая цена. Одна строка — позиция заказа. Сумма позиции — qty * unit_price, базовая сумма — qty * baseline_unit_price.

Заказы в расчёт идут все: и закрытые, и те, что ещё в работе. Обязательство уже создано, цена зафиксирована. Если считать только по закрытым, из периода выпадут ноябрь и декабрь 2025 года.

WITH l AS (
SELECT sum(qty * unit_price) AS actual,
sum(qty * baseline_unit_price) AS baseline
FROM purchase_lines
)
SELECT round(baseline / 1000000, 1) AS "базовая, млн ₽",
round(actual / 1000000, 1) AS "факт, млн ₽",
round((baseline - actual) / 1000000, 1) AS "экономия, млн ₽",
round(100 * (baseline - actual) / baseline, 1) AS "экономия, %"
FROM l;
┌────────────────┬─────────────┬─────────────────┬─────────────┐
│ базовая, млн ₽ │ факт, млн ₽ │ экономия, млн ₽ │ экономия, % │
├────────────────┼─────────────┼─────────────────┼─────────────┤
│ 289.3 │ 277.9 │ 11.4 │ 3.9 │
└────────────────┴─────────────┴─────────────────┴─────────────┘

Экономия 11,4 млн ₽ на базе 289,3 млн ₽ — это 3,9%. Число выглядит скромно, и это честно: в нём нет ни «эффекта от переговоров», ни «эффекта от объёма», только разница между референсом и фактической ценой по каждой позиции.

Ещё важнее, что 3,9% — это средневзвешенная величина, за которой скрываются два разных потока: где-то цена ниже базы, где-то выше. Следующий запрос показывает, где именно.

SELECT p.category AS "категория",
round(sum(l.qty * l.baseline_unit_price) / 1000000, 1) AS "базовая, млн ₽",
round(sum(l.qty * l.unit_price) / 1000000, 1) AS "факт, млн ₽",
round(100 * (sum(l.qty * l.baseline_unit_price) - sum(l.qty * l.unit_price))
/ sum(l.qty * l.baseline_unit_price), 1) AS "экономия, %",
count(*) FILTER (WHERE l.unit_price > l.baseline_unit_price) AS "позиций дороже"
FROM purchase_lines l
JOIN purchases p ON p.id = l.order_id
GROUP BY p.category
ORDER BY 4 DESC;
┌─────────────────┬────────────────┬─────────────┬─────────────┬────────────────┐
│ категория │ базовая, млн ₽ │ факт, млн ₽ │ экономия, % │ позиций дороже │
├─────────────────┼────────────────┼─────────────┼─────────────┼────────────────┤
│ Электрика │ 32.5 │ 30.9 │ 4.8 │ 10 │
│ Упаковка │ 9.1 │ 8.7 │ 4.6 │ 12 │
│ ИТ-оборудование │ 54.8 │ 52.5 │ 4.3 │ 9 │
│ Крепёж │ 12.0 │ 11.5 │ 4.0 │ 17 │
│ Спецодежда │ 13.3 │ 12.8 │ 3.9 │ 10 │
│ Металлопрокат │ 135.0 │ 130.0 │ 3.7 │ 21 │
│ Канцелярия │ 2.3 │ 2.3 │ 3.5 │ 5 │
│ Логистика │ 20.9 │ 20.2 │ 3.4 │ 13 │
│ Химия и ГСМ │ 9.4 │ 9.1 │ 3.0 │ 12 │
└─────────────────┴────────────────┴─────────────┴─────────────┴────────────────┘

Разброс экономии узкий: от 3,0% (Химия и ГСМ) до 4,8% (Электрика). Но на базе 289,3 млн ₽ один процентный пункт — это 2,9 млн ₽, поэтому даже такая разница между категориями стоит денег.

Позиции дороже базовой цены есть в каждой категории: от 5 в Канцелярии до 21 в Металлопрокате. Всего их 109 из 923. Это не брак данных, а нормальная картина: часть закупок идёт по срочной потребности, часть — у единственного поставщика, часть просто подорожала с момента установки базовой цены.

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

Средняя экономия прячет переплату. Разделим два потока:

SELECT round(sum(CASE WHEN unit_price > baseline_unit_price
THEN qty * (unit_price - baseline_unit_price) END) / 1000000, 2) AS "переплата, млн ₽",
round(sum(CASE WHEN unit_price <= baseline_unit_price
THEN qty * (baseline_unit_price - unit_price) END) / 1000000, 2) AS "выигрыш, млн ₽",
count(*) FILTER (WHERE unit_price > baseline_unit_price) AS "позиций дороже",
count(*) AS "позиций всего"
FROM purchase_lines;
┌──────────────────┬────────────────┬────────────────┬───────────────┐
│ переплата, млн ₽ │ выигрыш, млн ₽ │ позиций дороже │ позиций всего │
├──────────────────┼────────────────┼────────────────┼───────────────┤
│ 2.39 │ 13.78 │ 109 │ 923 │
└──────────────────┴────────────────┴────────────────┴───────────────┘

13,78 млн ₽ выигрыша и 2,39 млн ₽ переплаты дают те же 11,4 млн ₽ чистыми. Но управленчески это две разные задачи: 2,39 млн ₽ — это то, что можно вернуть, разобравшись с причинами роста цен на конкретные позиции, а 13,78 млн ₽ — уже полученный результат, который надо не потерять при следующем продлении.

Если показать руководителю только 3,9%, он не увидит ни того, ни другого. Поэтому в отчёте об экономии принято давать три числа: база, факт и разложение на выигрыш и переплату.

Ошибка Что происходит Как правильно
Считать экономию как среднее процентов по позициям Мелкая позиция с экономией 40% весит столько же, сколько крупная с 2% Считать в рублях, процент — от суммы
Сравнивать факт с бюджетом и называть это экономией Бюджет часто завышен, «экономия» появляется из воздуха Базовая цена и бюджет — разные базы, их не смешивают
Считать по заказам, а не по позициям Базовая цена задана на единицу, а не на заказ Считать по purchase_lines
Не смотреть на отрицательную экономию 2,39 млн ₽ переплаты исчезают в среднем Показывать выигрыш и переплату отдельно
Менять методику базовой цены между периодами Экономия растёт или падает без изменения цен Методика фиксируется, период сравнения — один