§ P22 — DAX-кейс · Визуализация

TOP N и TOP N% с «Прочими» на DAX

Статический · Процентный · Динамический · Multi-level

Как показать топ-5 товаров, топ-10 клиентов или «товары, дающие 80% выручки», а остальное свернуть в одну строку «Прочее». Разбираем пять паттернов от базового статического до multi-level top внутри категорий, с граблями и оптимизацией для Power BI.

Типовая задача: в каталоге 3 000 товаров. Менеджер открывает дашборд, видит bar-chart «Выручка по товарам» — и ничего не может прочитать. Столбиков слишком много, большинство нулевые, график нечитаем. Нужно показать топ-10, а остальное свернуть в одну строку «Прочее». Звучит просто, но в Power BI без боли это не сделать.

Проблема в том, что стандартный TOP-N фильтр визуала не даёт строку «Прочее». Он просто скрывает остальные, из-за чего total на графике не совпадает с total на KPI-карточке. И это главная причина, почему люди делают корявые решения в Power Query («вычислю топ в ETL и материализую») — которые разваливаются при любом фильтре визуала.

Решение — на DAX. Ниже пять паттернов от базового к продвинутому, с готовым кодом и разбором типичных ошибок.

Цель визуала: что мы строим

Простой пример — bar-chart, где видно топ-5 товаров и последнюю строку «Прочее» с суммой всех остальных. Total совпадает с общим итогом.

§ Выручка по товарам, млн ₽
Смартфон A144.2
Ноутбук S153.6
Планшет M112.9
Наушники X22.2
Часы W51.8
Прочее (2 995 товаров)3.0

Всё, что нужно: сумма верхних 5 + одна серая строка «Прочее» внизу, которая даёт в итоге 100%. Вот как это сделать.

Модель и базовые меры

Модель простая: fact_sales (date, product_id, выручка, quantity), dim_product (product_id, product_name, category), dim_date. Базовая мера:

Revenue =
SUM(fact_sales[revenue])

Всё остальное — надстройки над этим.

Пять паттернов TOP N

Паттерн 01Static TOP N

Статический топ-5 без «Прочего»

«Покажите топ-5 товаров, остальные не показывайте»

Самый простой сценарий. Используем RANKX для каждой строки визуала и возвращаем значение только для первых 5.

Top 5 Revenue =
VAR _rank =
    RANKX(
        ALL(dim_product[product_name]),
        [Revenue],
        , DESC, DENSE
    )
RETURN
    IF(_rank <= 5, [Revenue], BLANK())

В bar-chart с осью dim_product[product_name] и значением [Top 5 Revenue] показываются только топ-5. BLANK в DAX не отрисовывается, что даёт нужный эффект.

Проблема: нет «Прочего». Остальные товары просто исчезли, но визуальный total ≠ реальный total. Пользователь видит «5 товаров сделали 14.7 млн», а в KPI-карточке выручка 17.7 млн — возникает вопрос «а куда делись 3 млн?». Решается следующим паттерном.

Паттерн 02TOP N + Others

Топ-5 + одна строка «Прочее»

«Топ-5 отдельно, остальные — одной строкой «Прочее»

Нужна отдельная таблица-ось, которая содержит и реальные product_name, и искусственное значение «Прочее». В модели создаётся calculated table:

dim_product_top_axis =
UNION(
    SELECTCOLUMNS(
        dim_product,
        "label", dim_product[product_name],
        "sort_key", dim_product[product_name],
        "is_other", FALSE
    ),
    ROW(
        "label", "Прочее",
        "sort_key", "zzz_Прочее",
        "is_other", TRUE
    )
)

Связь с основной dim_product не делаем — эта таблица работает как disconnected axis, наполняется через меры. Теперь мера:

Top 5 with Others =
VAR _top_n = 5
VAR _current_label = SELECTEDVALUE(dim_product_top_axis[label])
VAR _is_other = SELECTEDVALUE(dim_product_top_axis[is_other])
RETURN
    SWITCH(
        TRUE(),
        _is_other = TRUE,
            // Для строки "Прочее": всё минус топ-N
            VAR _total_all = CALCULATE([Revenue], ALL(dim_product))
            VAR _top_n_sum =
                SUMX(
                    TOPN(
                        _top_n,
                        ALL(dim_product[product_name]),
                        [Revenue], DESC
                    ),
                    [Revenue]
                )
            RETURN _total_all - _top_n_sum,

        // Для реального товара: показать revenue, если он в топ-N
        VAR _this_rev =
            CALCULATE(
                [Revenue],
                dim_product[product_name] = _current_label
            )
        VAR _rank =
            RANKX(
                ALL(dim_product[product_name]),
                [Revenue], , DESC, DENSE
            )
        RETURN IF(_rank <= _top_n && _this_rev > 0, _this_rev, BLANK())
    )

В bar-chart:

  • Ось — dim_product_top_axis[label], сортировка по sort_key
  • Значение — [Top 5 with Others]
  • Условное форматирование: для строки «Прочее» цвет серый (через is_other)

Получаем 5 столбиков реальных товаров + одна серая «Прочее» снизу. Total совпадает с общей выручкой — математика сходится.

Частая грабля: sort_key = "zzz_Прочее" нужен, чтобы «Прочее» всегда оказывалось в конце при сортировке. Иначе при alphabetic sort оно может оказаться посередине и ломает визуал. В Power BI в Model View пометьте колонку label как Sort by Column → sort_key.
Паттерн 03TOP N%

Товары, дающие 80% выручки (Парето)

«Покажите все товары, накопительно дающие первые 80% выручки. Остальное — в «Прочее»

Задача ABC-анализа: не «сколько штук», а «сколько даёт ≤ X% выручки». Количество строк зависит от распределения: в ритейле обычно топ-20 товаров дают 80%, в длинном хвосте — 200.

Top 80pct Revenue =
VAR _threshold = 0.8
VAR _current_product = SELECTEDVALUE(dim_product[product_name])
VAR _total = CALCULATE([Revenue], ALL(dim_product))
VAR _cum_revenue_rank =
    CALCULATE(
        [Revenue],
        FILTER(
            ALL(dim_product),
            [Revenue] >= CALCULATE([Revenue], dim_product[product_name] = _current_product)
        )
    )
VAR _cum_share = DIVIDE(_cum_revenue_rank, _total)
RETURN
    IF(_cum_share <= _threshold, [Revenue], BLANK())

Логика:

  1. Для каждого товара считаем «накопительную выручку товаров с выручкой не меньше, чем у меня» (это даёт нам место в сортированном топе)
  2. Делим на общую выручку — получаем «накопительную долю»
  3. Если ≤ 80%, товар попадает в top

По тому же принципу делается вариант с «Прочим» — применяя схему из паттерна 02, но заменяя RANKX-условие на cumulative share.

Параметризация порога. Вместо жёстких 80% сделайте What-If параметр «Порог ABC» с опциями 70/80/90/95. Пользователь двигает срез — видит разные Парето-кривые.

Паттерн 04Dynamic TOP N

Пользователь выбирает N через срез

«Дайте срез 5/10/15/20, пусть пользователь сам выбирает, сколько показывать»

What-If параметр Power BI. Modeling → New Parameter → Whole Number → значения от 5 до 50 с шагом 5. Power BI создаёт вспомогательную таблицу 'Top N' с колонкой Top N Value и срез.

Top N with Others (Dynamic) =
VAR _top_n = SELECTEDVALUE('Top N'[Top N Value], 5)
VAR _current_label = SELECTEDVALUE(dim_product_top_axis[label])
VAR _is_other = SELECTEDVALUE(dim_product_top_axis[is_other])
RETURN
    SWITCH(
        TRUE(),
        _is_other = TRUE,
            VAR _total = CALCULATE([Revenue], ALL(dim_product))
            VAR _top_n_sum =
                SUMX(
                    TOPN(_top_n, ALL(dim_product[product_name]), [Revenue], DESC),
                    [Revenue]
                )
            RETURN _total - _top_n_sum,

        VAR _this_rev =
            CALCULATE([Revenue], dim_product[product_name] = _current_label)
        VAR _rank =
            RANKX(ALL(dim_product[product_name]), [Revenue], , DESC, DENSE)
        RETURN IF(_rank <= _top_n && _this_rev > 0, _this_rev, BLANK())
    )

Ровно то же, что паттерн 02, но _top_n берётся из срез. Если срез не выбран, дефолт — 5. Удобно для self-service: руководителю проще крутить ползунок, чем просить разработчика поменять число.

Расширение — dynamic title. Текст заголовка визуала тоже может быть динамическим: «Топ-N товаров (ещё M товаров в Прочем)». Это делается через conditional formatting заголовка визуала по мере:

Chart Title =
VAR _n = SELECTEDVALUE('Top N'[Top N Value], 5)
VAR _total_products =
    CALCULATE(DISTINCTCOUNT(dim_product[product_name]), ALL(dim_product))
VAR _others = _total_products - _n
RETURN
    "Топ-" & _n & " товаров (ещё " & _others & " в Прочем)"
Паттерн 05Multi-Level TOP N

Топ-3 товара в каждой категории

«В каждой категории покажите свои топ-3, остальное внутри категории — в «Прочее в Электронике», «Прочее в Одежде» и т.д.»

Кейс посложнее. Нужен ранг товаров внутри их категории, а не глобально. Ключевое слово — ALLEXCEPT: снимаем фильтр по товарам, но сохраняем фильтр по категории.

Top 3 per Category =
VAR _top_n = 3
VAR _this_product = SELECTEDVALUE(dim_product[product_name])
VAR _this_revenue =
    CALCULATE([Revenue], dim_product[product_name] = _this_product)
VAR _rank =
    RANKX(
        ALLEXCEPT(dim_product, dim_product[category]),
        [Revenue], , DESC, DENSE
    )
RETURN
    IF(_rank <= _top_n, _this_revenue, BLANK())

Для версии «Топ-3 + Прочее по категории» нужна более хитрая disconnected таблица: dim_product_top_axis_hier с колонками category + label (label может быть реальным product_name или «Прочее в Электронике»). Логика по аналогии с паттерном 02, но с привязкой к категории.

Сокращённая версия (без «Прочего», только топ внутри категории):

// В матрице: строки category + product_name
// В качестве значения используем Top 3 per Category
// Фильтр визуала: Top 3 per Category is not blank
// Результат: матрица показывает в каждой категории только топ-3

Такая схема покрывает 80% реальных задач «покажи top-N внутри группы». Для «Прочего в каждой группе» дополнительно строится disconnected-таблица по схеме паттерна 02.

Вариации визуализации

Bar chart

Самый частый вариант. Top N + «Прочее» сверху вниз, «Прочее» серым цветом для визуального отделения. Условное форматирование цвета: по значению is_other из disconnected-таблицы.

Pie chart / Donut

Работает плохо с большим числом сегментов. В pie-chart «Прочее» особенно полезно — показывайте максимум 5-7 кусков, остальное в одной серой секции. Без этого круг становится мозаикой из микросекторов.

Treemap

Хорошо работает даже с 20+ товарами, но если каталог 3 000 — читается плохо. Группировка «Прочее» помогает, но для treemap часто интереснее сначала показать категории, а внутри уже — drill-down до товаров.

Stacked bar / 100% stacked

Для динамики по месяцам отлично работает. Ось X — месяцы, стек — топ-5 товаров + серый блок «Прочее». Видно, как меняется структура продаж.

Matrix

Как таблица: строки — товары из топа + «Прочее», колонки — месяцы/регионы/каналы. Стандартный «cross-tab» для детального анализа.

Типичные ошибки

  1. Считать «Прочее» через ALL(dim_product). Если на странице есть срез по категории или региону, ALL снимет и эти фильтры тоже. «Прочее» будет показывать мусор. Используйте ALLSELECTED или KEEPFILTERS по нужным измерениям.
  2. Не обрабатывать tie-breakers. Если два товара с одинаковой выручкой, RANKX может выдать им один ранг — в топ попадает 6 вместо 5. Для predictable результата добавьте вторичный ключ: [Revenue] + DIVIDE(1, RANKX_by_product_id) — обеспечит уникальность.
  3. Забыть sort-колонку для «Прочее». Без custom sort «Прочее» может оказаться в алфавитном порядке посреди товаров. Используйте sort_key с префиксом zzz_.
  4. Слишком большое N на big model. TOP-50 на каталоге 100k товаров — тяжёлая операция, визуал тормозит. Либо ограничьте N до разумных 20, либо материализуйте топ в ETL (если обновляется раз в сутки — достаточно).
  5. «Прочее» с NEGATIVE значением. Если есть возвраты, и топ-N состоит из товаров с большим выручка, а в хвосте много возвратов — «Прочее» может стать отрицательным. Это математически правильно, но визуально странно. Добавьте IF с MAX(0, ...) если хотите прятать отрицательные.
  6. Использовать TOPN с «равными» значениями как фильтр. TOPN(5, ..., DESC) может вернуть 5, 6 или больше строк при tie. Всегда проверяйте на модели с tie-scenarios.
  7. Неправильная ось для Top + Others. Если вы просто кладёте dim_product[product_name] в ось визуала, «Прочее» не появится — для него нет строки в dim_product. Обязательно используйте disconnected axis из UNION.
  8. Меру с TOP-N кладут в срез. срез ожидает single-column table, а мера возвращает число. Это не работает — для срез нужна реальная таблица с параметром (see паттерн 04).

Производительность

TOP-N на DAX с разрезом по многим измерениям — одна из самых тяжёлых операций в Power BI. Каждая строка визуала делает RANKX по всей таблице. Оптимизации:

ПроблемаРешение
RANKX на большом каталоге (> 10k)Материализовать rank в ETL или в calculated column
Медленно на разных фильтрахИспользовать KEEPFILTERS + ALLSELECTED вместо ALL
Пересчёт «Прочего» при каждом визуалеВынести в отдельную меру, кешировать через VAR
Cumulative sum (для TOP N%) тормозитПредрассчитать накопительный ранг в ETL
Много визуалов с TOP-N на одной страницеAggregation tables + Aggregation mappings

Практическое правило: если TOP-N считается динамически на базе > 50k строк в измерении, материализуйте rank в ETL. На SQL это тривиально:

-- Пересчёт топа раз в сутки, кладём в dim_product
SELECT
    product_id,
    product_name,
    category,
    last_90d_revenue,
    RANK() OVER (ORDER BY last_90d_revenue DESC) AS global_rank,
    RANK() OVER (
        PARTITION BY category
        ORDER BY last_90d_revenue DESC
    ) AS category_rank
FROM ...

После этого в DAX просто фильтр: FILTER(dim_product, dim_product[global_rank] <= _top_n). Работает мгновенно, но теряется гибкость на произвольных фильтрах визуала.

Расширенные варианты

TOP N + BOTTOM N

«Топ-5 лидеров + топ-5 худших + серединные скрыть». Полезно для выявления аномалий. Добавляется вторая проверка RANKX ASC и ещё одна disconnected-строка «Середина».

Dynamic threshold (TOP N%)

срез не с вариантами «5/10/15», а численный What-If «процент от выручки». Пользователь крутит ползунок от 50% до 99% и видит, как меняется число товаров в топе.

Threshold-based (по абсолютному значению)

Вместо «топ-N» или «N%» — показывать все товары с выручка > 500k. Самый простой вариант, решается прямым фильтром визуала (или мерой с IF).

«Прочее» как drill-down

Клик по «Прочее» раскрывает отдельный экран с детализацией по товарам хвоста. Делается через Bookmarks + Buttons. Главное меню показывает только топ, «что в Прочем» — отдельная кнопка/страница.

Tooltip на «Прочее»

Tooltip при наведении на серую строку «Прочее» может показывать дополнительные метрики: «2 995 товаров, средняя выручка 1 050 ₽, минимальная — 85 ₽, максимальная — 48 000 ₽». Создаётся через отдельную tooltip-страницу с фильтром «не в топ-N».

Шаги внедрения

  1. Определите тип: статический N (паттерн 01-02), процентный (паттерн 03), динамический (паттерн 04), multi-level (паттерн 05).
  2. Решите, нужно ли «Прочее». Если total должен совпадать с общим — да. Если нет — можно обойтись паттерном 01.
  3. Создайте disconnected-таблицу для оси визуала через UNION. Добавьте sort_key + is_other.
  4. Напишите меру по одному из паттернов. Начните с простого, усложняйте постепенно.
  5. Тестируйте на tie-scenarios: товары с одинаковой выручкой, нулевые значения, отрицательные (возвраты).
  6. Проверьте производительность: откройте визуал, замерьте time через Performance Analyzer. Если > 2 сек — оптимизируйте.
  7. Добавьте условное форматирование: серый цвет для «Прочее», bold для top-1, иконки для top-3 и т.д.
  8. Параметризуйте N через срез, если нужна гибкость пользователей.

Что делать дальше

  • Если dashboard с TOP-N тормозит — смотрите P13 про рефакторинг Power BI, раздел про RANKX и aggregation tables.
  • Для ABC-анализа (расширение паттерна 03) — отдельная статья готовится. Присылайте запрос на ранний доступ через форму консультации.
  • Нужен полный дашборд с TOP-N под ваши данные — 30-минутный звонок. Обычно внедряется за 1-2 недели.

Связанные материалы:

§ Внедрение · 1-2 недели

Читаемый
дашборд ассортимента

TOP-N с «Прочим», multi-level, динамические slicers, drill-down в хвост — под ваши данные за 1-2 недели.

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