Типовая задача: в каталоге 3 000 товаров. Менеджер открывает дашборд, видит bar-chart «Выручка по товарам» — и ничего не может прочитать. Столбиков слишком много, большинство нулевые, график нечитаем. Нужно показать топ-10, а остальное свернуть в одну строку «Прочее». Звучит просто, но в Power BI без боли это не сделать.
Проблема в том, что стандартный TOP-N фильтр визуала не даёт строку «Прочее». Он просто скрывает остальные, из-за чего total на графике не совпадает с total на KPI-карточке. И это главная причина, почему люди делают корявые решения в Power Query («вычислю топ в ETL и материализую») — которые разваливаются при любом фильтре визуала.
Решение — на DAX. Ниже пять паттернов от базового к продвинутому, с готовым кодом и разбором типичных ошибок.
Цель визуала: что мы строим
Простой пример — bar-chart, где видно топ-5 товаров и последнюю строку «Прочее» с суммой всех остальных. Total совпадает с общим итогом.
Всё, что нужно: сумма верхних 5 + одна серая строка «Прочее» внизу, которая даёт в итоге 100%. Вот как это сделать.
Модель и базовые меры
Модель простая: fact_sales (date, product_id, выручка, quantity), dim_product (product_id, product_name, category), dim_date. Базовая мера:
Revenue =
SUM(fact_sales[revenue])
Всё остальное — надстройки над этим.
Пять паттернов 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 млн?». Решается следующим паттерном.
Топ-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.
Товары, дающие 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())
Логика:
- Для каждого товара считаем «накопительную выручку товаров с выручкой не меньше, чем у меня» (это даёт нам место в сортированном топе)
- Делим на общую выручку — получаем «накопительную долю»
- Если ≤ 80%, товар попадает в top
По тому же принципу делается вариант с «Прочим» — применяя схему из паттерна 02, но заменяя RANKX-условие на cumulative share.
Параметризация порога. Вместо жёстких 80% сделайте What-If параметр «Порог ABC» с опциями 70/80/90/95. Пользователь двигает срез — видит разные Парето-кривые.
Пользователь выбирает 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 & " в Прочем)"
Топ-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» для детального анализа.
Типичные ошибки
- Считать «Прочее» через ALL(dim_product). Если на странице есть срез по категории или региону, ALL снимет и эти фильтры тоже. «Прочее» будет показывать мусор. Используйте ALLSELECTED или KEEPFILTERS по нужным измерениям.
- Не обрабатывать tie-breakers. Если два товара с одинаковой выручкой, RANKX может выдать им один ранг — в топ попадает 6 вместо 5. Для predictable результата добавьте вторичный ключ:
[Revenue] + DIVIDE(1, RANKX_by_product_id)— обеспечит уникальность. - Забыть sort-колонку для «Прочее». Без custom sort «Прочее» может оказаться в алфавитном порядке посреди товаров. Используйте sort_key с префиксом zzz_.
- Слишком большое N на big model. TOP-50 на каталоге 100k товаров — тяжёлая операция, визуал тормозит. Либо ограничьте N до разумных 20, либо материализуйте топ в ETL (если обновляется раз в сутки — достаточно).
- «Прочее» с NEGATIVE значением. Если есть возвраты, и топ-N состоит из товаров с большим выручка, а в хвосте много возвратов — «Прочее» может стать отрицательным. Это математически правильно, но визуально странно. Добавьте IF с MAX(0, ...) если хотите прятать отрицательные.
- Использовать TOPN с «равными» значениями как фильтр. TOPN(5, ..., DESC) может вернуть 5, 6 или больше строк при tie. Всегда проверяйте на модели с tie-scenarios.
- Неправильная ось для Top + Others. Если вы просто кладёте
dim_product[product_name]в ось визуала, «Прочее» не появится — для него нет строки в dim_product. Обязательно используйте disconnected axis из UNION. - Меру с 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».
Шаги внедрения
- Определите тип: статический N (паттерн 01-02), процентный (паттерн 03), динамический (паттерн 04), multi-level (паттерн 05).
- Решите, нужно ли «Прочее». Если total должен совпадать с общим — да. Если нет — можно обойтись паттерном 01.
- Создайте disconnected-таблицу для оси визуала через UNION. Добавьте sort_key + is_other.
- Напишите меру по одному из паттернов. Начните с простого, усложняйте постепенно.
- Тестируйте на tie-scenarios: товары с одинаковой выручкой, нулевые значения, отрицательные (возвраты).
- Проверьте производительность: откройте визуал, замерьте time через Performance Analyzer. Если > 2 сек — оптимизируйте.
- Добавьте условное форматирование: серый цвет для «Прочее», bold для top-1, иконки для top-3 и т.д.
- Параметризуйте N через срез, если нужна гибкость пользователей.
Что делать дальше
- Если dashboard с TOP-N тормозит — смотрите P13 про рефакторинг Power BI, раздел про RANKX и aggregation tables.
- Для ABC-анализа (расширение паттерна 03) — отдельная статья готовится. Присылайте запрос на ранний доступ через форму консультации.
- Нужен полный дашборд с TOP-N под ваши данные — 30-минутный звонок. Обычно внедряется за 1-2 недели.
Связанные материалы:
- DAX для управленцев: 15 формул — формула 11 даёт базовую версию TOP N + Others
- Рефакторинг Power BI в 10 раз — про оптимизацию RANKX и aggregation tables
- RFM-сегментация на DAX — ещё один кейс с RANKX и квинтилями
- когорта удержание на DAX — паттерн TREATAS для disconnected axis