Планы и факты: работаем с денормализованной таблицей
Привет, Хабр! В этой статье я хотел бы поговорить про особенности план-факт анализа, а также о работе с денормализованной таблицей, которая «была, есть и будет использоваться», потому что оказывается удобной для некоторых приемов работы с BI. Под катом вы найдете 7 примеров решения типовых задач план-факт анализа, включая расчет долей, отображение данных с учетом иерархии, разбивку по регионам и так далее. Всех, кому интересны эти практические аспекты, жду под катом :)
Типичной задачей является сравнение плана и факта, и сегодня я разберу несколько примеров, как сделать это на базе денормализованной таблицы фактов — то есть когда в одной таблице хранятся и фактические значения с детализацией по дням, и фактические значения за месяц. Это очень интересная сущность, ведь в реальной жизни расхождения между суммами фактических данных по дням и фактическими данными за месяц могут быть ощутимыми…а еще там же могут лежать плановые значения за месяц. Но на базе BI-платформы можно красиво свести все эти данные воедино для дальнейшего анализа.
Обычно в таком случае (то есть в такой денормализованной таблице фактов) создаются отдельные колонки (план, факт по месяцу, факт по заказу). Работать с ними немного сложнее, чем с «нормальными» таблицами, но, к счастью, уже есть наработанные практики успешного разгребания подобного бардака.
В качестве примера рассмотрим данные о продажах. На схеме приведена модель данных из Visiology.
Данные плана и факта хранятся в денормализованной таблице sales
следующего вида:
1. Отображение данных фактических и плановых продаж по месяцам в одной таблице
Если мы хотим провести план-факт анализ, для совмещения плановых и фактических значений в одной UI-таблице нужно подготовить денормализованную таблицу фактов, а именно — заполнить в ней столбец, по которому будет строиться UI-таблица. В нашем случае, это месяц.
За счет того, что план и факт по месяцам хранятся в разных столбцах денормализованной таблицы фактов, для проведения анализа будет достаточно суммирования — даже не потребуется создавать новую меру.
При этом платформа Visiology генерирует следующий DAX:
EVALUATE
TOPN (
3000,
SUMMARIZECOLUMNS (
'sales'[month],
"fact", SUM ( 'sales'[fact] ),
"plan", SUM ( 'sales'[plan] )
),
'sales'[month], ASC,
[fact], ASC,
[plan], ASC
)
ORDER BY
'sales'[month] ASC,
[fact] ASC,
[plan] ASC
В результате объединенная таблица выглядит так:
2. Отображение вместе плана, факта по месяцам и факта по дням (заказам)
Часто суммы факта по дням и за месяц расходятся, и если всё хранится в одной денормализованной таблице, можно очень просто отобразить данные вместе. Для этого достаточно добавить orderamount
из отдельного столбца и провести суммирование по нему. Нужно только проследить, чтобы в денормализованной таблице при заполнении всех трех столбцов также был заполнен столбец month, чтобы вывести все результаты в одной таблице.
Платформа Visiology генерирует следующий DAX:
EVALUATE
TOPN (
3000,
SUMMARIZECOLUMNS (
'sales'[month],
"fact", SUM ( 'sales'[fact] ),
"orderamount", SUM ( 'sales'[orderamount] ),
"plan", SUM ( 'sales'[plan] )
),
'sales'[month], ASC,
[fact], ASC,
[orderamount], ASC,
[plan], ASC
)
ORDER BY
'sales'[month] ASC,
[fact] ASC,
[orderamount] ASC,
[plan] ASC
3. Отображение плана и факта с учетом иерархии компании
Часто от BI требуется анализ в рамках иерархии компании. Чтобы решить эту задачу, мы просто меняем тип таблицы на сводную, добавляем в нее regionname и cityname из справочников для отображения данных по регионам и городам.
Для сводной таблицы по мере детализации генерируется несколько DAX, например:
EVALUATE
TOPN (
3000,
SUMMARIZECOLUMNS (
'city'[cityname],
FILTER ( 'region', 'region'[regionname] IN { "Центр" } ),
"fact", SUM ( 'sales'[fact] ),
"orderamount", SUM ( 'sales'[orderamount] ),
"plan", SUM ( 'sales'[plan] )
),
'city'[cityname], ASC,
[fact], ASC,
[orderamount], ASC,
[plan], ASC
)
ORDER BY
'city'[cityname] ASC,
[fact] ASC,
[orderamount] ASC,
[plan] ASC
Итог выглядит следующим образом:
4. Расчет долей месяца в году
От аналитиков нередко требуют показать долю текущего месяца в продажах за выбранный период — то есть обычно за год. Для того чтобы проще решить эту задачу добавим фильтр с month
из sales
(не из calendar
) и выберем в нем 12 месяцев за нужный год. Таким образом, за счет фильтра по sales[month]
мы работаем с данными за актуальный период — за год:
Используем следующую меру:
Доля месяца в продажах за период =
SUM ( sales[orderamount] )
/ CALCULATE ( SUM ( sales[orderamount] ), REMOVEFILTERS ( calendar ) )
Добавим меру на дашборд, и в результате получим:
Visiology генерирует следующий DAX:
EVALUATE
TOPN (
3000,
SUMMARIZECOLUMNS (
'calendar'[month],
FILTER (
'sales',
'sales'[month]
IN {
"202401",
"202402",
"202403",
"202404",
"202405",
"202406",
"202407",
"202408",
"202409",
"202410",
"202411",
"202412"
}
),
"orderamount", SUM ( 'sales'[orderamount] ),
"Доля месяца в продажах за период", 'sales'[Доля месяца в продажах за период]
),
'calendar'[month], ASC,
[orderamount], ASC,
[Доля месяца в продажах за период], ASC
)
ORDER BY
'calendar'[month] ASC,
[orderamount] ASC,
[Доля месяца в продажах за период] ASC
Стоит отметить, что в качестве примера использовано форматирование с округлением до трех знаков после запятой:
!(typeof @value === 'string') && @value ?
@value.toFixed(3) : @value
При необходимости можно использовать собственную логику форматирования, что описано в документации Visiology.
5. Отображение долей по регионам и городам
По аналогии с долей месяца можно рассчитать долю города и региона в продажах, это уже в некотором смысле аналог коэффициента проникновения по регионам или городам.
Для решения задачи используем подход, аналогичный примененному для календаря и месяцев. Чтобы нам было удобнее, учитываем, что в одном CALCULATE
можно использовать несколько REMOVEFILTERS
.
Создадим аналогичную меру:
Доля региона в продажах за период =
SUM ( sales[orderamount] )
/ CALCULATE (
SUM ( sales[orderamount] ),
REMOVEFILTERS ( region ),
REMOVEFILTERS ( city )
)
Новые фильтры по регионам и городам из sales можно и не создавать, в итоге получим следующий дашборд:
Платформа генерирует следующий DAX:
EVALUATE
TOPN (
3000,
SUMMARIZECOLUMNS (
'region'[regionname],
"orderamount", SUM ( 'sales'[orderamount] ),
"Доля города в продажах за период", 'sales'[Доля города в продажах за период]
),
'region'[regionname], ASC,
[orderamount], ASC,
[Доля города в продажах за период], ASC
)
ORDER BY
'region'[regionname] ASC,
[orderamount] ASC,
[Доля города в продажах за период] ASC
Возможность дважды использовать REMOVEFILTERS
позволяет добиться необходимой детализации по городам. Также можно и использовать больше REMOVEFILTERS
, например, три REMOVEFILTERS
для расчета долей по городам, регионам и месяцам. Возможности здесь практически безграничны. :)
6. Отображение долей по регионам, городам и месяцам
Можно объединить расчет долей по иерархии компании (регионам и городам) и месяцам, чтобы продемонстрировать возможности нескольких REMOVEFILTERS
для нескольких таблиц.
Чтобы оценить это на примере, добавим фильтр для 2024 года (точнее, для 12 месяцев из sales[month]
), и создадим меру:
Доля города в продажах за период по месяцам =
SUM ( sales[orderamount] )
/ CALCULATE (
SUM ( sales[orderamount] ),
REMOVEFILTERS ( region ),
REMOVEFILTERS ( city ),
REMOVEFILTERS ( calendar )
)
Получим следующий дашборд.
Для этого дашборда сводная таблица генерирует несколько DAX запросов в зависимости от уровня детализации, например:
EVALUATE
TOPN (
3000,
SUMMARIZECOLUMNS (
'city'[cityname],
FILTER ( 'region', 'region'[regionname] IN { "Восток" } ),
FILTER (
'sales',
'sales'[month]
IN {
"202401",
"202402",
"202403",
"202404",
"202405",
"202406",
"202407",
"202408",
"202409",
"202410",
"202411",
"202412"
}
),
"orderamount", SUM ( 'sales'[orderamount] ),
"Доля города в продажах за период по месяцам", 'sales'[Доля города в продажах за период по месяцам]
),
'city'[cityname], ASC,
[orderamount], ASC,
[Доля города в продажах за период по месяцам], ASC
)
ORDER BY
'city'[cityname] ASC,
[orderamount] ASC,
[Доля города в продажах за период по месяцам] ASC
7. Все в одном! Анализируем план, факт по заказам, суммарный факт, долю в продажах — по иерархии компании и месяцам
Наконец, рассмотрим результаты на одном дашборде. Отобразим вместе план, факт по заказам, суммарный факт и для примера созданную ранее меру для доли города в продажах по месяцам sales[Доля города в продажах за период по месяцам]
, в итоге получим дашборд следующего вида со сводной таблицей:
Как обычно, сводная таблица генерирует несколько DAX запросов в зависимости от уровня детализации, для примера один из таких DAX запросов:
EVALUATE
TOPN (
3000,
SUMMARIZE (
SUMMARIZECOLUMNS (
'city'[cityname],
'sales'[month],
FILTER ( 'region', 'region'[regionname] IN { "Центр" } ),
"orderamount", SUM ( 'sales'[orderamount] ),
"plan", SUM ( 'sales'[plan] ),
"fact", SUM ( 'sales'[fact] ),
"Доля города в продажах за период по месяцам", 'sales'[Доля города в продажах за период по месяцам]
),
'city'[cityname]
),
'city'[cityname], ASC
)
Заключение
В этой статье мы убедились, что на денормализованной таблице можно сравнить план и факт, рассчитать доли и решить множество других задач.
При необходимости расчета доли по столбцу (например, month
) удобно создать справочник с этим столбцом (например, calendar
) и использовать REMOVEFILTERS
для нового справочника (например, REMOVEFILTERS(calendar)
) для расчета суммы по всем значениям справочника (т.е. по году).
Для фильтрации на дашборде (например, по месяцу) можно добавить на дашборд фильтр не по справочнику calendar[month]
, а по таблице фактов sales[month]
, тогда этот фильтр не будет отменен в REMOVEFILTERS
через REMOVEFILTERS(calendar)
.
Успехов в построении дашбордов! :)