В ритейле каждый сантиметр полки – это деньги (буквально) и от того, насколько эффективно используется торговое пространство, зависит:
– Выручка – товары на видных местах продаются лучше;
– Прибыль – маржинальные позиции должны занимать лучшие места;
– Удовлетворенность клиентов – если товар не на своем месте или отсутствует, покупатель уходит;
– Отношения с поставщиками – они платят за определенное количество "лиц" товара.

В этой статье я разберу примеры задач, которые решает аналитик в ритейле, и покажу, как их решать на SQL. Каждая задача сложнее предыдущей для каждой есть код и готовые синтетические данные, поэтому все результаты можно получить самостоятельно, повторив код.

Задача 1. Эффективность использования полочного пространства (ROSP)

Представьте, что вы управляете супермаркетом и у вас есть 100 метров полок, и на них нужно разместить тысячи товаров.
Вопрос: как понять, какие товары достойны лучшего места, а какие стоит ужать или убрать совсем?

Метрика ROSP (Return on Shelf Space) отвечает на этот вопрос, она показывает, сколько выручки приносит каждый метр полки, это аналог более известной метрик ROI, но для торгового пространства.

Для чего это нужно:
- Перераспределить место в пользу эффективных товаров;
- Обосновать перед руководством, почему тот или иной товар стоит расширить;
- Выявить товары, которые занимают место, но не приносят выручки.

Математика метрики

ROSP=Выручка за период/Занимаемое место на полке (в метрах)
Занимаемое место считается как: Ширина товара (см) * Количество единиц товара /100

Пример кода на SQL:

WITH product_shelf_metrics AS (
    SELECT 
        p.product_id,
        p.product_name,
        p.category,
        (p.width * p.facing_count / 100.0) AS shelf_meters,
        SUM(s.amount) AS monthly_revenue
    FROM products p
    JOIN sales s ON p.product_id = s.product_id
    WHERE s.sale_date BETWEEN '2026-02-01' AND '2026-02-28'
    GROUP BY p.product_id, p.product_name, p.category, p.width, p.facing_count
)
SELECT 
    product_name,
    category,
    shelf_meters,
    monthly_revenue,
    ROUND(monthly_revenue / shelf_meters, 2) AS revenue_per_meter,
    ROUND(AVG(monthly_revenue / shelf_meters) OVER(PARTITION BY category), 2) AS avg_category_rosp,
    ROUND(100.0 * (monthly_revenue / shelf_meters) / AVG(monthly_revenue / shelf_meters) OVER(PARTITION BY category) - 100, 2) AS vs_category_avg_percent
FROM product_shelf_metrics
ORDER BY revenue_per_meter DESC;

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

product_name

category

shelf_meters

monthly_revenue

revenue_per_meter

avg_category_rosp

vs_category_avg

Шоколад Аленка

Кондитерка

0.20

810.00

4050.00

4050.00

0.00

Сок Добрый 1л

Напитки

0.21

1680.00

8000.00

8000.00

0.00

Молоко Простоквашино 1л

Молочка

0.20

2700.00

13500.00

13500.00

0.00

Пельмени Цезарь 500г

Заморозка

0.60

1620.00

2700.00

2700.00

0.00

Майонез Слобода 400г

Соусы

0.16

770.00

4812.50

4812.50

0.00

Хлеб Бородинский

Хлеб

0.15

810.00

5400.00

5400.00

0.00

Пример вывода по полученным данным: Молоко показывает самую высокую выручку на метр полки 13 500 руб, пельмени при всей популярности занимают много места (0,6 м) и дают только 2700 руб/м. Молоку можно выделить больше места на полке (если спрос позволяет), пельмени – требуют дополнительного анализа: может, дело в цене? В сезонности? Или действительно стоит ужать? Вообще, в идеале для пельменей нужно считать не только выручку, но и прибыль (возможно, маржа выше).

Задача 2. Анализ доступности товаров на полке (OSA)

Одна из главных проблем ритейла – это out-of-shelf (OOS), когда товар есть на складе, числится в системе, но на полке его нет и клиент не находит товар и уходит к конкуренту, возникают потери.

Метрика OSA (On-Shelf Availability) показывает, насколько товар доступен покупателю и в идеальном мире она должна быть 100%, но в реальности часто проседает, важно знать насколько.

Почему товар может отсутствовать на полке:
– Не успели выложить из подсобки
– Поставили не на то место
– Сбой в планограмме
– Кража (да, такое тоже бывает)

Как диагностировать OSA по данным

У нас нет прямых данных "есть товар на полке / нет товара". Но мы можем косвенно определить проблемы через такие маркеры, как, например:
– Резкое падение продаж при стабильном среднем
– Нулевые продажи несколько дней подряд при том, что раньше продажи были
– Аномалии в динамике

Пример кода на SQL:

WITH daily_sales_pattern AS (
    -- Собираем ежедневную статистику по продажам
    SELECT 
        store_id,
        product_id,
        sale_date,
        SUM(quantity) AS daily_qty,
        CASE WHEN SUM(quantity) > 0 THEN 1 ELSE 0 END AS was_sold
    FROM sales
    GROUP BY store_id, product_id, sale_date
),
stockout_candidates AS (
    -- Считаем скользящие метрики для поиска аномалий
    SELECT 
        sp.store_id,
        sp.product_id,
        sp.sale_date,
        sp.daily_qty,
        -- Были ли продажи в последние 7 дней (исключая сегодня)
        SUM(sp.was_sold) OVER(PARTITION BY sp.store_id, sp.product_id 
                               ORDER BY sp.sale_date 
                               ROWS BETWEEN 7 PRECEDING AND 1 PRECEDING) AS sales_last_7_days,
        -- Среднее количество за последние 30 дней (для сравнения)
        AVG(sp.daily_qty) OVER(PARTITION BY sp.store_id, sp.product_id 
                                ORDER BY sp.sale_date 
                                ROWS BETWEEN 30 PRECEDING AND 1 PRECEDING) AS avg_qty_last_30_days
    FROM daily_sales_pattern sp
)
SELECT 
    s.store_name,
    p.product_name,
    sc.sale_date,
    sc.daily_qty,
    sc.sales_last_7_days,
    ROUND(sc.avg_qty_last_30_days, 2) AS avg_daily_qty,
    CASE 
        WHEN sc.sales_last_7_days = 0 AND sc.avg_qty_last_30_days > 0 THEN '? Вероятный out-of-shelf'
        WHEN sc.sales_last_7_days = 0 AND sc.avg_qty_last_30_days = 0 THEN '⚫ Мертвый товар'
        WHEN sc.daily_qty < 0.3 * sc.avg_qty_last_30_days AND sc.avg_qty_last_30_days > 2 THEN '? Аномальное падение'
        ELSE '? Норма'
    END AS shelf_status
FROM stockout_candidates sc
JOIN stores s ON sc.store_id = s.store_id
JOIN products p ON sc.product_id = p.product_id
WHERE sc.sale_date = '2026-02-03'  -- проверяем конкретный день
  AND sc.shelf_status IN ('? Вероятный out-of-shelf', '? Аномальное падение')
ORDER BY s.store_name, p.category;

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

store_name

product_name

sale_date

daily_qty

sales_last_7

avg_qty

shelf_status

Магнит Столичный

Майонез Слобода

2026-02-03

5

2

5.50

? Аномальное падение

Пример вывода по полученным данным:

Показатель

Значение

Что означает

Среднее за 30 дней

5,5 шт/день

Обычно продается 5-6 пачек майонеза в день

Продажи 3 февраля

5 шт

Вроде нормально, но...

Продажи за 7 дней до 3 февраля

2 дня из 7

Это аномалия, значит, 5 февраля продажи восстановились после провала

Что произошло на самом деле:
Последние 7 дней майонез продавался только 2 дня, вероятно, его не выкладывали, а 3 февраля выложили и продажи сразу вернулись к норме. Это пример классического out-of-shelf, который мы поймали по косвенным признакам.

Что нам делать дальше?
– Направить запрос в магазин: Что случилось с майонезом в конце января?
– Проверить, не было ли проблем с поставкой;
– Если товар был на складе — разобраться с выкладкой.

Задача 3. Анализ планограмм и соблюдения выкладки

В ритейле есть понятие фейс товара (facing) – это одна единица товара, стоящая лицом к покупателю и производители платят ритейлерам за определенное количество фейсов, шаблон выкладки "лицом" называется планограмма – она же схема выкладки товаров на полке.

Проблема: фактические фейсы часто не соответствуют запланированным:
– Меньше, чем в договоре и мы теряем деньги поставщика (и бонусы);
– Больше, чем в договоре и мы неэффективно используем место (можно было поставить что-то более маржинальное).

Зачем это контролировать

Штрафы/бонусы поставщикам – если мы не выполняем планограмму, поставщик может требовать компенсацию;
Эффективность – лишние фейсы на слабых товарах съедают место сильных;
Контроль исполнения – в больших сетях без автоматизации сложно уследить.

Пример кода на SQL:

WITH planogram_compliance AS (
    SELECT 
        sp.store_id,
        sp.product_id,
        sp.facing_actual,
        p.facing_count AS facing_planned,
        p.category,
        -- Считаем отклонение
        (sp.facing_actual - p.facing_count) AS facing_diff,
        -- Средняя цена товара (для оценки потерь)
        (SELECT AVG(amount/quantity) FROM sales s2 
         WHERE s2.product_id = sp.product_id AND s2.sale_date >= '2026-02-01') AS avg_price
    FROM shelf_placement sp
    JOIN products p ON sp.product_id = p.product_id
    WHERE sp.is_active = TRUE
)
SELECT 
    s.store_name,
    p.product_name,
    p.category,
    pc.facing_planned,
    pc.facing_actual,
    pc.facing_diff,
    ROUND(pc.avg_price, 2) AS avg_price,
    -- Оценка потерь: если фейсов меньше, чем надо, считаем упущенную выручку
    ROUND(ABS(pc.facing_diff) * pc.avg_price * 30, 2) AS estimated_monthly_loss
FROM planogram_compliance pc
JOIN stores s ON pc.store_id = s.store_id
JOIN products p ON pc.product_id = p.product_id
WHERE pc.facing_diff != 0
ORDER BY estimated_monthly_loss DESC;

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

store_name

product_name

planned

actual

diff

avg_price

monthly_loss

Магнит Столичный

Пельмени Цезарь 500г

3

4

+1

180.00

0

Магнит Центральный

Шоколад Аленка

4

3

-1

30.00

900.00

Пример вывода по полученным данным:

Кейс 1. Шоколад (Магазин Центральный)
По плану: 4 фейса
Факт: 3 фейса
Потеря: 1 фейс × 30 руб. (цена) × 30 дней = 900 руб/мес

Поставщик платил за 4 фейса, а получил 3, это либо возврат части оплаты, либо недовольство партнера.

Кейс 2. Пельмени (Магазин Столичный)
По плану: 3 фейса
Факт: 4 фейса
Потеря: не денежная, а пространственная

Мы поставили лишний фейс пельменей (которые и так плохо продаются) и вместо них можно было поставить, например, еще молока.

Итог:
По шоколаду: либо вернуть 4-е фейс, либо объяснить поставщику, почему так (и, возможно, снизить оплату)
По пельменям: убрать лишний фейс, освободить место под более эффективный товар

Данные для всех задач

Если есть необходимость повторить решение из примеров, ниже код по созданию таблиц и код по заполнению данными.

-- Таблица магазинов
CREATE TABLE stores (
    store_id INT PRIMARY KEY,
    store_name VARCHAR(100),
    city VARCHAR(50),
    total_shelf_area DECIMAL(10,2),
    shelf_length DECIMAL(10,2)
);

-- Таблица товаров
CREATE TABLE products (
    product_id INT PRIMARY KEY,
    product_name VARCHAR(200),
    category VARCHAR(100),
    subcategory VARCHAR(100),
    width DECIMAL(10,2),
    depth DECIMAL(10,2),
    height DECIMAL(10,2),
    facing_count INT DEFAULT 1
);

-- Таблица размещения товаров
CREATE TABLE shelf_placement (
    placement_id INT PRIMARY KEY,
    store_id INT,
    product_id INT,
    shelf_number INT,
    facing_actual INT,
    start_date DATE,
    end_date DATE,
    is_active BOOLEAN DEFAULT TRUE
);

-- Таблица продаж
CREATE TABLE sales (
    sale_id INT PRIMARY KEY,
    store_id INT,
    product_id INT,
    sale_date DATE,
    quantity INT,
    amount DECIMAL(10,2),
    profit DECIMAL(10,2)
);

Заполнение созданных ранее таблиц данными

-- Магазины
INSERT INTO stores VALUES
(1, 'Магнит Столичный', 'Москва', 2500.0, 450.0),
(2, 'Магнит Центральный', 'Москва', 1800.0, 320.0),
(3, 'Магнит Северный', 'СПб', 2200.0, 400.0),
(4, 'Магнит Южный', 'Краснодар', 1500.0, 280.0),
(5, 'Магнит Западный', 'Казань', 2000.0, 360.0);

-- Товары
INSERT INTO products VALUES
(101, 'Молоко Простоквашино 1л', 'Молочка', 'Молоко', 10.0, 10.0, 20.0, 2),
(102, 'Хлеб Бородинский', 'Хлеб', 'Хлеб', 15.0, 25.0, 10.0, 1),
(103, 'Пельмени Цезарь 500г', 'Заморозка', 'Пельмени', 20.0, 15.0, 8.0, 3),
(104, 'Майонез Слобода 400г', 'Соусы', 'Майонез', 8.0, 8.0, 15.0, 2),
(105, 'Колбаса Докторская', 'Мясная гастрономия', 'Колбаса', 12.0, 12.0, 12.0, 2),
(106, 'Сок Добрый 1л', 'Напитки', 'Сок', 7.0, 7.0, 20.0, 3),
(107, 'Шоколад Аленка', 'Кондитерка', 'Шоколад', 5.0, 10.0, 1.0, 4),
(108, 'Чай Greenfield 25пак', 'Бакалея', 'Чай', 8.0, 8.0, 15.0, 2),
(109, 'Кофе Jardin', 'Бакалея', 'Кофе', 10.0, 10.0, 20.0, 2),
(110, 'Сахар песок 1кг', 'Бакалея', 'Сахар', 12.0, 8.0, 18.0, 2);

-- Размещение товаров
INSERT INTO shelf_placement VALUES
(1, 1, 101, 2, 2, '2026-01-01', NULL, TRUE),
(2, 1, 102, 1, 1, '2026-01-01', NULL, TRUE),
(3, 1, 103, 3, 4, '2026-01-01', NULL, TRUE),
(4, 1, 104, 3, 2, '2026-01-01', NULL, TRUE),
(5, 1, 105, 1, 1, '2026-01-01', NULL, TRUE),
(6, 2, 101, 2, 2, '2026-01-01', NULL, TRUE),
(7, 2, 103, 3, 3, '2026-01-01', NULL, TRUE),
(8, 2, 107, 4, 3, '2026-01-01', NULL, TRUE),
(9, 3, 101, 2, 2, '2026-01-01', NULL, TRUE),
(10, 3, 106, 4, 3, '2026-01-01', NULL, TRUE);

-- Продажи (февраль 2026)
INSERT INTO sales VALUES
(1, 1, 101, '2026-02-01', 5, 500.00, 100.00),
(2, 1, 101, '2026-02-02', 3, 300.00, 60.00),
(3, 1, 101, '2026-02-03', 4, 400.00, 80.00),
(4, 1, 102, '2026-02-01', 10, 450.00, 90.00),
(5, 1, 102, '2026-02-02', 8, 360.00, 72.00),
(6, 1, 103, '2026-02-01', 3, 540.00, 108.00),
(7, 1, 103, '2026-02-02', 4, 720.00, 144.00),
(8, 1, 104, '2026-02-01', 6, 420.00, 84.00),
(9, 1, 104, '2026-02-03', 5, 350.00, 70.00),
(10, 2, 101, '2026-02-01', 4, 400.00, 80.00),
(11, 2, 101, '2026-02-02', 6, 600.00, 120.00),
(12, 2, 103, '2026-02-01', 2, 360.00, 72.00),
(13, 2, 107, '2026-02-01', 15, 450.00, 90.00),
(14, 2, 107, '2026-02-02', 12, 360.00, 72.00),
(15, 3, 101, '2026-02-01', 7, 700.00, 140.00),
(16, 3, 106, '2026-02-01', 8, 560.00, 112.00),
(17, 3, 106, '2026-02-02', 10, 700.00, 140.00),
(18, 3, 106, '2026-02-03', 6, 420.00, 84.00),
(19, 4, 101, '2026-02-01', 3, 300.00, 60.00),
(20, 4, 105, '2026-02-01', 4, 720.00, 144.00),
(21, 5, 108, '2026-02-01', 5, 400.00, 80.00),
(22, 5, 109, '2026-02-01', 3, 600.00, 120.00),
(23, 5, 110, '2026-02-02', 8, 320.00, 64.00);

Предыдущие статьи-разборы задач на аналитику ассортимента:
https://habr.com/ru/articles/1005262/
https://habr.com/ru/articles/1005284/

? Больше про будни и задачи аналитика данных в моем тг ?Таня и Данные?

Комментарии (10)


  1. Akina
    07.03.2026 18:02

    Ну очень обидно - вроде хорошее дело делаете, но опять, как говорят некоторые товарищи, стремясь остаться в цензурном пространстве, через "универсальный интерфейс". Сформулировали задачу - кратенько так, без подробностей, зато с красочными дополнительными деталями,- и сразу выдали подходящий SQL-код. Формулы? нету. Подробные объяснения, почему итоговый SQL получился именно таким? и не пахнет. Последовательность решения, отдельные этапы решения, соответствующие трансформация и усложнение SQL? даже в мыслях не было. А по итогу - ну совершенно пустая и ни к чему не применимая статья.

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


    1. TanyaVSdannye Автор
      07.03.2026 18:02

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


      1. Akina
        07.03.2026 18:02

        Так вы ж не первую и не вторую статью публикуете. Пора бы уже. Пусть в статье одна, да хотя бы и самая простая, задача разбирается подробно и полностью, а остальные тезисно - уже совсем другой уровень получится. Впрочем, вам, наверное, виднее..


    1. TanyaVSdannye Автор
      07.03.2026 18:02

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


      1. Akina
        07.03.2026 18:02

        Потребителю это всё пофиг, он ж за пределами. Он поёт про то, что видит.

        А я в реальном гипермаркете вижу ежемесячные перемещения - иногда минорные, но раз в пару лет настолько глобальные, что потом месяц привыкаешь искать те же товары в новых местах. Торговая сеть достаточно немелкая, уж аналитики-то у них точно в наличии, и не все джуны. Ан поди ж ты..


        1. TanyaVSdannye Автор
          07.03.2026 18:02

          Ну вообще не всегда аналитика ассортимент двигает, часто сам персонал магазина делает так, как по его мнению лучше


          1. hystrix
            07.03.2026 18:02

            Сам вряд ли, разные полки стоят разные деньги, хотя бы по высоте.

            Была раньше гипотеза: идея переставить всё идёт от мерчандайзеров, чтобы потребитель, который бежит по привычному flow, тормознулся и пошел искать, нашел что-то новенькое.

            Потом рассказали, что в магазинах одной сети наоборот стараются делать одинаковый порядок.


        1. rozhnev
          07.03.2026 18:02

          Я думаю что такие перемещения позволяют магазину познакомить вас с новыми товарами. Если не менять вы приходите берете нужный товар и уходите. Послп перестсновки - вы обойдёте весь торговый зал в поисках вашего товара и возможно купите ещё что-то что не планировали.


          1. Akina
            07.03.2026 18:02

            Ну тогда я какой-то неправильный покупатель... не найдя нужный мне товар на привычном месте, я не стану его искать - у меня под боком ещё 4 разных магазина, и минимум в половине из них такой же товар есть. И из тех 4 "резервных" магазинов я посещу минимум пару и доберу что не попалось и что "хочется вотпрямсчас".

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

            Хотя покупатель, приходящий с магазин со списком - это по определению "покупатель неправильный".


  1. IVA48
    07.03.2026 18:02

    Кратко:

    1. Точнее формулировать как задача эфф.использования пространства полки, а НЕ площади для расположения товара.

    2. Товары (продукты) надо подразделять по категориям спроса:

      ежедневного - 3-ех дневного, недельного, месячного и от этого планировать их расположение (ближе, дальше).

    3. Сохранять по возможности локацию товара (продуктов) на полках, чтобы покупателю было привычнее их находить и экономить время. В больших супермаркетах на мониторе-покупателя по названию товара показывать на плане магазина его расположение на полке и маршрут в эту точку от места монитора.

    4. По недоступности товара на полке (кончился, кончается), но при наличии на складе магазина, в информационной системе управления работой магазина иметь доступ к базе данных информационной системы продажи и через нее контролировать кол-во оставшегося товара на полке.

    5. Кол-во единиц "face-товара" напрямую зависит от площади полки и удобства упаковки для ее расположения "лицом" к покупателю.

      Как видно из всего выше сказанного, SQL-запросы напрямую относятся только к реализации пункта 4.