Урок 07 · 10 мин чтения

Like-for-Like: сопоставимые продажи

Честное сравнение периодов — только по точкам, работавшим во всех периодах. Паттерн same-store sales на DAX.

«Выручка выросла на 18% год к году» — звучит отлично, пока не выяснится, что за год открылось двадцать новых магазинов. Рост дали новые точки, а не бизнес как таковой. Чтобы измерить органический рост, в рознице считают like-for-like (LFL), он же same-store sales — сравнивают только те точки, что работали во всех сравниваемых периодах. Разберём паттерн на DAX по статье SQLBI.

На каком примере

В нашем учебном наборе один год, поэтому LFL на нём не посчитать — нужен мультипериод. Поэтому примеры даю на классической рознице: магазины (Магазины) и продажи за 2024–2026. Логика универсальна — так же работает для товаров, клиентов, филиалов.

Зачем это нужно

Наивное сравнение год-к-году смешивает два эффекта:

  • органический рост (та же точка стала продавать больше);
  • рост от расширения (открыли новые точки).

Для управленческих решений их надо разделять. LFL отвечает на вопрос «как выросли продажи на сопоставимой базе» — только по точкам, открытым во всех годах анализа. Новые и закрытые точки из сравнения исключаются.

Шаг 1. Статус точки по периодам

Сначала строим расчётную таблицу: какая точка в каком году работала. «Работала» = были продажи.

StoreStatus =
VAR AllStores =
    CROSSJOIN (
        SUMMARIZE ( Продажи, Календарь[Год] ),
        ALLNOBLANKROW ( Магазины[КодМагазина] )
    )
VAR OpenStores =
    SUMMARIZE ( Продажи, Календарь[Год], Продажи[КодМагазина] )
RETURN
    UNION (
        ADDCOLUMNS ( OpenStores, "Статус", "Активен" ),
        ADDCOLUMNS ( EXCEPT ( AllStores, OpenStores ), "Статус", "Неактивен" )
    )

Логика:

  • AllStores — все пары «год × магазин» (полная матрица);
  • OpenStores — пары, где были продажи;
  • EXCEPT — пары без продаж (точка в этот год не работала);
  • результат — таблица, где каждая пара «год-магазин» помечена «Активен» / «Неактивен».

Шаг 2. Мера сопоставимых продаж

Теперь считаем выручку только по точкам, активным во всех выбранных годах:

Сопоставимые продажи =
VAR _OpenStores =
    CALCULATETABLE (
        FILTER (
            ALLSELECTED ( StoreStatus[КодМагазина] ),
            CALCULATE ( SELECTEDVALUE ( StoreStatus[Статус] ) ) = "Активен"
        ),
        ALLSELECTED ( Календарь )
    )
VAR _Filter = TREATAS ( _OpenStores, Магазины[КодМагазина] )
RETURN
    CALCULATE ( [Выручка], KEEPFILTERS ( _Filter ) )

Что происходит:

  1. отбираем магазины со статусом «Активен» по всем выбранным годам (ALLSELECTED уважает срез пользователя);
  2. TREATAS переносит этот список как фильтр на Магазины[КодМагазина] — та самая виртуальная связь из прошлого урока;
  3. KEEPFILTERS пересекает его с текущим контекстом, а не затирает;
  4. [Выручка] считается только по сопоставимым точкам.
Сердце паттерна

LFL = «посчитать меру только по сущностям, присутствующим во всех периодах». Список таких сущностей собирается из таблицы статусов и накладывается через TREATAS + KEEPFILTERS. Дальше LFL рост % = DIVIDE ( [Сопоставимые продажи] - [Сопоставимые продажи ПГ], [Сопоставимые продажи ПГ] ).

Шаг 3. Упаковать в функцию (по желанию)

В новых версиях DAX появились пользовательские функции — паттерн можно вынести в переиспользуемую функцию, отделив универсальную логику от схемы конкретной модели:

// универсальная (не зависит от модели)
DaxPatterns.LikeForLike.ForSameEntity =
    ( statusKey: ANYREF, statusCol: ANYREF, entityKey: ANYREF,
      dateTable: ANYREF, formula: EXPR ) =>
    VAR _Open =
        CALCULATETABLE (
            FILTER ( ALLSELECTED ( statusKey ),
                CALCULATE ( SELECTEDVALUE ( statusCol ) ) = "Активен" ),
            ALLSELECTED ( dateTable )
        )
    RETURN CALCULATE ( formula, KEEPFILTERS ( TREATAS ( _Open, entityKey ) ) )

Дальше тонкая обёртка под вашу модель — и вызов в одну строку: Сопоставимые продажи = Local.ForSameStore ( [Выручка] ). Это убирает копипаст, когда LFL нужен на нескольких уровнях (магазины, товары, клиенты).

Функции — свежая возможность

Пользовательские DAX-функции — относительно новый механизм (Tabular/Fabric, развивается). Если их в вашей среде ещё нет — паттерн прекрасно работает обычными мерами из шага 2. Функции — про переиспользование, а не про саму логику.

Подводные камни

  • Что считать «работал». Мы взяли «были продажи». Иногда правильнее «магазин числился открытым» — тогда статус берут из справочника дат открытия/закрытия, а не из факта продаж.
  • Грануляция периода. LFL по годам и по месяцам — разные меры; решите, на каком уровне «сопоставимость».
  • Стоимость. Таблица статусов и TREATAS считаются на лету; на больших каталогах точек следите за производительностью.

Что дальше

LFL — типовой розничный паттерн, и он показывает, как связываются темы курса: time intelligence, виртуальные связи (TREATAS) и работа с контекстом. Дальше — field parameters, what-if и другие DAX-паттерны (ABC, retention, остатки).

Что показывает like-for-like (сопоставимые продажи)?
LFL убирает эффект расширения сети: сравниваются только точки, присутствующие во всех периодах. Это показывает органический рост, а не рост за счёт новых открытий.
Прогресс сохраняется в вашем браузере.
§ Power BI под ключ

Нужно внедрить
это в компании?

Соберём DWH, модель и дашборды под ваши данные. Бесплатная консультация — 30 минут.

Телефон+7 918 042 34 43