Урок 4.3: Ранжирование данных

Урок 4.3: Ранжирование данных

Модуль 4: Фильтрация, сортировка и ранжирование

Введение: Зачем нужно ранжирование в аналитике

После изучения фильтрации и сортировки мы переходим к ранжированию — мощному инструменту для определения позиции элементов в упорядоченном наборе. Ранжирование отвечает на вопросы: "Какое место занимает этот продукт по продажам?", "Входит ли клиент в топ-10?", "Какой процентиль занимает регион по прибыли?".

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

Теоретические основы ранжирования

Функция RANK и её синтаксис

Основная функция для ранжирования в MDX — это RANK. Её синтаксис:

RANK(элемент, набор [, выражение_сортировки])

Где:

  • элемент — член, для которого определяется ранг
  • набор — упорядоченный набор для ранжирования
  • выражение_сортировки — необязательный параметр для определения критерия

Важная особенность: если третий параметр не указан, RANK использует текущий порядок элементов в наборе. Если указан — сначала выполняется сортировка, затем определяется позиция.

Типы ранжирования

В аналитике существует несколько подходов к ранжированию:

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

MDX по умолчанию использует уникальное ранжирование на основе позиции в наборе.

Взаимосвязь ранжирования с сортировкой

Ранжирование тесно связано с сортировкой. Обычный подход:

  1. Создаем отсортированный набор с помощью ORDER
  2. Применяем RANK к этому набору
  3. Используем ранг для анализа или фильтрации

Базовое ранжирование

Простейший пример ранжирования

Начнем с определения рангов продуктов по продажам:

WITH 
-- Создаем отсортированный набор продуктов
SET [SortedProducts] AS
    ORDER(
        [Product].[Product].[Product].Members,
        [Measures].[Internet Sales Amount],
        DESC  -- Сортируем по убыванию продаж
    )

-- Создаем меру для отображения ранга
MEMBER [Measures].[Sales Rank] AS
    RANK(
        [Product].[Product].CurrentMember,  -- Текущий продукт
        [SortedProducts]                    -- В отсортированном наборе
    )

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Sales Rank]} ON COLUMNS,
    NON EMPTY [SortedProducts] ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

Разберем этот код подробно:

  • SET [SortedProducts] создает именованный набор отсортированных продуктов
  • MEMBER [Measures].[Sales Rank] создает расчетную меру для ранга
  • CurrentMember указывает на текущий элемент в контексте вычисления
  • RANK возвращает позицию элемента в отсортированном наборе

Ранжирование с фильтрацией

Часто нужно ранжировать только релевантные элементы:

WITH 
-- Фильтруем продукты с существенными продажами
SET [SignificantProducts] AS
    FILTER(
        [Product].[Product].[Product].Members,
        [Measures].[Internet Sales Amount] > 10000
    )

-- Сортируем отфильтрованный набор
SET [RankedProducts] AS
    ORDER(
        [SignificantProducts],
        [Measures].[Internet Sales Amount],
        DESC
    )

-- Вычисляем ранг
MEMBER [Measures].[Filtered Rank] AS
    RANK(
        [Product].[Product].CurrentMember,
        [RankedProducts]
    )

-- Добавляем индикатор топ-10
MEMBER [Measures].[Is Top 10] AS
    IIF(
        [Measures].[Filtered Rank] <= 10 AND [Measures].[Filtered Rank] > 0,
        "Yes",
        "No"
    )

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Filtered Rank],
     [Measures].[Is Top 10]} ON COLUMNS,
    HEAD([RankedProducts], 20) ON ROWS  -- Показываем топ-20
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

Ранжирование в группах

Локальное ранжирование внутри категорий

Определим ранг продукта внутри его категории:

WITH 
-- Для каждого продукта определяем ранг в его категории
MEMBER [Measures].[Category Rank] AS
    RANK(
        [Product].[Product].CurrentMember,
        ORDER(
            -- Берем все продукты из той же категории
            FILTER(
                [Product].[Product].[Product].Members,
                [Product].[Product].CurrentMember.Parent.Parent = 
                [Product].[Category].CurrentMember
            ),
            [Measures].[Internet Sales Amount],
            DESC
        )
    )

-- Определяем категорию для контекста
MEMBER [Measures].[Product Category] AS
    [Product].[Product].CurrentMember.Parent.Parent.Name

-- Маркер лидера категории
MEMBER [Measures].[Category Leader] AS
    IIF([Measures].[Category Rank] = 1, "★ LEADER", "")

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Product Category],
     [Measures].[Category Rank],
     [Measures].[Category Leader]} ON COLUMNS,
    NON EMPTY 
        FILTER(
            [Product].[Product].[Product].Members,
            [Measures].[Category Rank] <= 5  -- Топ-5 в каждой категории
        ) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

Многоуровневое ранжирование

Создадим систему с глобальным и локальным рангами:

WITH 
-- Глобальный отсортированный набор
SET [AllSubcategories] AS
    ORDER(
        [Product].[Subcategory].[Subcategory].Members,
        [Measures].[Internet Sales Amount],
        DESC
    )

-- Глобальный ранг
MEMBER [Measures].[Global Rank] AS
    RANK(
        [Product].[Subcategory].CurrentMember,
        [AllSubcategories]
    )

-- Локальный ранг внутри категории
MEMBER [Measures].[Local Rank] AS
    RANK(
        [Product].[Subcategory].CurrentMember,
        ORDER(
            FILTER(
                [Product].[Subcategory].[Subcategory].Members,
                [Product].[Subcategory].CurrentMember.Parent = 
                [Product].[Category].CurrentMember
            ),
            [Measures].[Internet Sales Amount],
            DESC
        )
    )

-- Комбинированный показатель
MEMBER [Measures].[Rank Display] AS
    "G:" + CStr([Measures].[Global Rank]) + 
    " / L:" + CStr([Measures].[Local Rank])

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Global Rank],
     [Measures].[Local Rank],
     [Measures].[Rank Display]} ON COLUMNS,
    CrossJoin(
        [Product].[Category].[Category].Members,
        [Product].[Subcategory].[Subcategory].Members
    ) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

Процентильное ранжирование

Вычисление процентилей

WITH 
-- Отсортированный набор клиентов
SET [SortedCustomers] AS
    ORDER(
        FILTER(
            [Customer].[Customer].[Customer].Members,
            NOT ISEMPTY([Measures].[Internet Sales Amount])
        ),
        [Measures].[Internet Sales Amount],
        DESC
    )

-- Общее количество клиентов
MEMBER [Measures].[Total Customers] AS
    COUNT([SortedCustomers])

-- Ранг клиента
MEMBER [Measures].[Customer Rank] AS
    RANK(
        [Customer].[Customer].CurrentMember,
        [SortedCustomers]
    )

-- Процентиль (какой процент клиентов ниже)
MEMBER [Measures].[Percentile] AS
    IIF(
        [Measures].[Total Customers] = 0,
        NULL,
        ((1 - ([Measures].[Customer Rank] - 1) / [Measures].[Total Customers]) * 100)
    ),
    FORMAT_STRING = "#,##0.00"

-- Квартиль
MEMBER [Measures].[Quartile] AS
    CASE
        WHEN [Measures].[Percentile] >= 75 THEN "Q1 (Top 25%)"
        WHEN [Measures].[Percentile] >= 50 THEN "Q2 (25-50%)"
        WHEN [Measures].[Percentile] >= 25 THEN "Q3 (50-75%)"
        ELSE "Q4 (Bottom 25%)"
    END

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Customer Rank],
     [Measures].[Percentile],
     [Measures].[Quartile]} ON COLUMNS,
    HEAD([SortedCustomers], 100) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

Децильный анализ

WITH 
-- Отсортированные регионы
SET [SortedRegions] AS
    ORDER(
        FILTER(
            [Customer].[State-Province].[State-Province].Members,
            NOT ISEMPTY([Measures].[Internet Sales Amount])
        ),
        [Measures].[Internet Sales Amount],
        DESC
    )

-- Общее количество
MEMBER [Measures].[Total Count] AS
    COUNT([SortedRegions])

-- Ранг
MEMBER [Measures].[Region Rank] AS
    RANK(
        [Customer].[State-Province].CurrentMember,
        [SortedRegions]
    )

-- Дециль (группа из 10)
MEMBER [Measures].[Decile] AS
    IIF(
        [Measures].[Total Count] = 0,
        NULL,
        CEILING(([Measures].[Region Rank] * 10.0) / [Measures].[Total Count])
    )

-- Метка дециля
MEMBER [Measures].[Decile Label] AS
    "D" + CStr([Measures].[Decile])

Использование функций TopCount, BottomCount, TopPercent

TopCount для выбора лидеров

TopCount возвращает заданное количество лидирующих элементов:

WITH 
-- Топ-10 продуктов по продажам
SET [Top10Products] AS
    TOPCOUNT(
        [Product].[Product].[Product].Members,
        10,  -- Количество элементов
        [Measures].[Internet Sales Amount]  -- Критерий
    )

-- Ранг среди топ-10
MEMBER [Measures].[Top10 Rank] AS
    RANK(
        [Product].[Product].CurrentMember,
        [Top10Products]
    )

-- Доля от суммы топ-10
MEMBER [Measures].[Share of Top10] AS
    [Measures].[Internet Sales Amount] / 
    SUM([Top10Products], [Measures].[Internet Sales Amount]),
    FORMAT_STRING = "Percent"

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Top10 Rank],
     [Measures].[Share of Top10]} ON COLUMNS,
    [Top10Products] ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

BottomCount для анализа аутсайдеров

WITH 
-- Худшие 10 субкатегорий по марже
MEMBER [Measures].[Profit Margin] AS
    IIF(
        [Measures].[Internet Sales Amount] = 0,
        NULL,
        ([Measures].[Internet Sales Amount] - [Measures].[Internet Total Product Cost]) /
        [Measures].[Internet Sales Amount]
    ),
    FORMAT_STRING = "Percent"

SET [Bottom10Subcategories] AS
    BOTTOMCOUNT(
        NON EMPTY [Product].[Subcategory].[Subcategory].Members,
        10,
        [Measures].[Profit Margin]
    )

-- Обратный ранг (от худшего)
MEMBER [Measures].[Bottom Rank] AS
    RANK(
        [Product].[Subcategory].CurrentMember,
        ORDER([Bottom10Subcategories], [Measures].[Profit Margin], ASC)
    )

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Profit Margin],
     [Measures].[Bottom Rank]} ON COLUMNS,
    [Bottom10Subcategories] ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

TopPercent для процентного отбора

WITH 
-- Продукты, составляющие 80% продаж (принцип Парето)
SET [Pareto80Products] AS
    TOPPERCENT(
        [Product].[Product].[Product].Members,
        80,  -- 80% от общей суммы
        [Measures].[Internet Sales Amount]
    )

-- Количество продуктов в 80%
MEMBER [Measures].[Pareto Count] AS
    COUNT([Pareto80Products])

-- Процент от общего количества продуктов
MEMBER [Measures].[Pareto Product %] AS
    [Measures].[Pareto Count] / 
    COUNT([Product].[Product].[Product].Members),
    FORMAT_STRING = "Percent"

-- Ранг в Парето-группе
MEMBER [Measures].[Pareto Rank] AS
    IIF(
        [Product].[Product].CurrentMember IN [Pareto80Products],
        RANK(
            [Product].[Product].CurrentMember,
            ORDER([Pareto80Products], [Measures].[Internet Sales Amount], DESC)
        ),
        NULL
    )

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Pareto Rank],
     [Measures].[Pareto Count],
     [Measures].[Pareto Product %]} ON COLUMNS,
    HEAD(
        ORDER([Pareto80Products], [Measures].[Internet Sales Amount], DESC),
        20
    ) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

Динамическое ранжирование

Ранжирование с изменяющимся контекстом

WITH 
-- Ранг продукта среди всех продуктов его категории
MEMBER [Measures].[Dynamic Category Rank] AS
    RANK(
        [Product].[Product].CurrentMember,
        ORDER(
            EXISTING [Product].[Product].[Product].Members,  -- EXISTING учитывает текущий контекст
            [Measures].[Internet Sales Amount],
            DESC
        )
    )

-- Общее количество продуктов в контексте
MEMBER [Measures].[Context Product Count] AS
    COUNT(EXISTING [Product].[Product].[Product].Members)

-- Относительная позиция
MEMBER [Measures].[Relative Position] AS
    [Measures].[Dynamic Category Rank] + " из " + 
    CStr([Measures].[Context Product Count])

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Dynamic Category Rank],
     [Measures].[Relative Position]} ON COLUMNS,
    NON EMPTY [Product].[Product].[Product].Members ON ROWS
FROM [Adventure Works]
WHERE (
    [Date].[Calendar Year].&[2013],
    [Product].[Category].[Bikes]  -- Фильтр по категории влияет на ранжирование
)

Практические упражнения

Упражнение 1: Комплексная система ранжирования

-- Задача: создать многомерную систему оценки клиентов

WITH 
-- Ранжирование по разным метрикам
SET [ActiveCustomers] AS
    FILTER(
        [Customer].[Customer].[Customer].Members,
        [Measures].[Internet Sales Amount] > 0
    )

-- Ранг по объему продаж
MEMBER [Measures].[Sales Rank] AS
    RANK(
        [Customer].[Customer].CurrentMember,
        ORDER([ActiveCustomers], [Measures].[Internet Sales Amount], DESC)
    )

-- Ранг по количеству заказов
MEMBER [Measures].[Order Rank] AS
    RANK(
        [Customer].[Customer].CurrentMember,
        ORDER([ActiveCustomers], [Measures].[Internet Order Count], DESC)
    )

-- Средний чек
MEMBER [Measures].[Avg Order Value] AS
    IIF(
        [Measures].[Internet Order Count] = 0,
        NULL,
        [Measures].[Internet Sales Amount] / [Measures].[Internet Order Count]
    ),
    FORMAT_STRING = "Currency"

-- Ранг по среднему чеку
MEMBER [Measures].[AOV Rank] AS
    RANK(
        [Customer].[Customer].CurrentMember,
        ORDER([ActiveCustomers], [Measures].[Avg Order Value], DESC)
    )

-- Комплексная оценка (чем меньше, тем лучше)
MEMBER [Measures].[Combined Score] AS
    ([Measures].[Sales Rank] * 0.5 + 
     [Measures].[Order Rank] * 0.3 + 
     [Measures].[AOV Rank] * 0.2) / 1,
    FORMAT_STRING = "#,##0.00"

-- Финальный ранг по комплексной оценке
MEMBER [Measures].[Final Rank] AS
    RANK(
        [Customer].[Customer].CurrentMember,
        ORDER([ActiveCustomers], [Measures].[Combined Score], ASC)
    )

-- Категория клиента
MEMBER [Measures].[Customer Tier] AS
    CASE
        WHEN [Measures].[Final Rank] <= 10 THEN "Platinum"
        WHEN [Measures].[Final Rank] <= 50 THEN "Gold"
        WHEN [Measures].[Final Rank] <= 200 THEN "Silver"
        ELSE "Bronze"
    END

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Internet Order Count],
     [Measures].[Avg Order Value],
     [Measures].[Sales Rank],
     [Measures].[Order Rank],
     [Measures].[AOV Rank],
     [Measures].[Combined Score],
     [Measures].[Final Rank],
     [Measures].[Customer Tier]} ON COLUMNS,
    HEAD(
        ORDER([ActiveCustomers], [Measures].[Final Rank], ASC),
        25
    ) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

Упражнение 2: Анализ изменения рангов

-- Задача: отследить изменение позиций продуктов между периодами

WITH 
-- Ранг в текущем году
MEMBER [Measures].[Current Year Rank] AS
    RANK(
        [Product].[Subcategory].CurrentMember,
        ORDER(
            [Product].[Subcategory].[Subcategory].Members,
            ([Measures].[Internet Sales Amount], [Date].[Calendar Year].&[2013]),
            DESC
        )
    )

-- Ранг в предыдущем году
MEMBER [Measures].[Previous Year Rank] AS
    RANK(
        [Product].[Subcategory].CurrentMember,
        ORDER(
            [Product].[Subcategory].[Subcategory].Members,
            ([Measures].[Internet Sales Amount], [Date].[Calendar Year].&[2012]),
            DESC
        )
    )

-- Изменение позиции
MEMBER [Measures].[Rank Change] AS
    [Measures].[Previous Year Rank] - [Measures].[Current Year Rank]

-- Визуализация изменения
MEMBER [Measures].[Trend] AS
    CASE
        WHEN [Measures].[Rank Change] > 2 THEN "↑↑ Strong Growth"
        WHEN [Measures].[Rank Change] > 0 THEN "↑ Growth"
        WHEN [Measures].[Rank Change] = 0 THEN "→ Stable"
        WHEN [Measures].[Rank Change] > -2 THEN "↓ Decline"
        ELSE "↓↓ Strong Decline"
    END

-- Продажи текущего года
MEMBER [Measures].[Sales 2013] AS
    ([Measures].[Internet Sales Amount], [Date].[Calendar Year].&[2013]),
    FORMAT_STRING = "Currency"

-- Продажи предыдущего года
MEMBER [Measures].[Sales 2012] AS
    ([Measures].[Internet Sales Amount], [Date].[Calendar Year].&[2012]),
    FORMAT_STRING = "Currency"

SELECT 
    {[Measures].[Sales 2013],
     [Measures].[Sales 2012],
     [Measures].[Current Year Rank],
     [Measures].[Previous Year Rank],
     [Measures].[Rank Change],
     [Measures].[Trend]} ON COLUMNS,
    NON EMPTY 
        ORDER(
            [Product].[Subcategory].[Subcategory].Members,
            ABS([Measures].[Rank Change]),  -- Сортируем по величине изменения
            DESC
        ) ON ROWS
FROM [Adventure Works]

Оптимизация ранжирования

Кэширование для производительности

WITH 
-- Кэшируем отсортированный набор один раз
SET [CachedSortedSet] AS
    ORDER(
        FILTER(
            [Product].[Product].[Product].Members,
            NOT ISEMPTY([Measures].[Internet Sales Amount])
        ),
        [Measures].[Internet Sales Amount],
        DESC
    )

-- Используем кэшированный набор для всех вычислений
MEMBER [Measures].[Cached Rank] AS
    RANK([Product].[Product].CurrentMember, [CachedSortedSet])

MEMBER [Measures].[Is Top 20] AS
    IIF([Measures].[Cached Rank] <= 20 AND [Measures].[Cached Rank] > 0, "Yes", "No")

MEMBER [Measures].[Rank Group] AS
    CASE
        WHEN [Measures].[Cached Rank] <= 10 THEN "Top 10"
        WHEN [Measures].[Cached Rank] <= 50 THEN "Top 50"
        WHEN [Measures].[Cached Rank] <= 100 THEN "Top 100"
        ELSE "Other"
    END

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Cached Rank],
     [Measures].[Is Top 20],
     [Measures].[Rank Group]} ON COLUMNS,
    HEAD([CachedSortedSet], 30) ON ROWS
FROM [Adventure Works]
WHERE [Date].[Calendar Year].&[2013]

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

Ошибка 1: Ранжирование пустого набора

-- НЕПРАВИЛЬНО: может вызвать ошибку
MEMBER [Measures].[Bad Rank] AS
    RANK(
        [Product].[Product].CurrentMember,
        {}  -- Пустой набор
    )

-- ПРАВИЛЬНО: проверка на пустоту
MEMBER [Measures].[Safe Rank] AS
    IIF(
        COUNT([MySet]) = 0,
        NULL,
        RANK([Product].[Product].CurrentMember, [MySet])
    )

Ошибка 2: Ранжирование без учета NULL

-- НЕПРАВИЛЬНО: NULL значения могут исказить ранги
SET [AllProducts] AS
    ORDER(
        [Product].[Product].[Product].Members,
        [Measures].[Some Measure],  -- Может содержать NULL
        DESC
    )

-- ПРАВИЛЬНО: фильтрация NULL перед ранжированием
SET [ValidProducts] AS
    ORDER(
        FILTER(
            [Product].[Product].[Product].Members,
            NOT IsEmpty([Measures].[Some Measure])
        ),
        [Measures].[Some Measure],
        DESC
    )

Заключение

В этом уроке мы детально изучили ранжирование данных в MDX. Мы научились:

  • Использовать функцию RANK для определения позиций элементов
  • Создавать локальное и глобальное ранжирование
  • Вычислять процентили и квартили
  • Применять функции TopCount, BottomCount и TopPercent
  • Реализовывать динамическое ранжирование с учетом контекста
  • Оптимизировать производительность через кэширование

Ранжирование — это мощный инструмент для создания рейтингов, выявления лидеров и аутсайдеров, а также для сегментации данных. В сочетании с фильтрацией и сортировкой, изученными в предыдущих уроках, ранжирование позволяет создавать сложные аналитические отчеты.

Домашнее задание

Задание 1: Многокритериальное ранжирование

Создайте систему ранжирования продуктов, учитывающую продажи, маржу и оборачиваемость. Реализуйте взвешенную оценку.

Задание 2: Динамическое изменение рангов

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

Задание 3: Процентильный анализ

Реализуйте полный процентильный анализ регионов с автоматическим определением квинтилей и расчетом статистики по каждой группе.

Контрольные вопросы

  1. В чем разница между RANK и простой нумерацией строк?
  2. Как влияет сортировка набора на результат функции RANK?
  3. Можно ли определить ранг элемента, не входящего в набор?
  4. Как реализовать плотное ранжирование в MDX?
  5. В чем преимущество TopCount перед комбинацией ORDER и HEAD?
  6. Как правильно обрабатывать одинаковые значения при ранжировании?
  7. Какие стратегии оптимизации применимы к ранжированию больших наборов?

Модуль 4: Фильтрация, сортировка и ранжирование • Урок 4.3

Следующий урок!
Прошлый урок