Uploaded by qlna6

Шаблоны DAX

advertisement
Альберто Феррари
Марко Руссо
Шаблоны DAX
Наиболее полное собрание готовых
к использованию решений на языке DAX
для Power BI, Analysis Services и Power Pivot
DAX Patterns
Seсond edition
The most comprehensive collection
of ready-to-use solutions in DAX for Power BI,
Analysis Services, and Power Pivot
Alberto Ferrari
Marco Russo
Шаблоны DAX
Наиболее полное собрание готовых
к использованию решений на языке DAX
для Power BI, Analysis Services и Power Pivot
Альберто Феррари
Марко Руссо
Москва, 2021
УДК 004.42DAX
ББК 32.97
Ф43
Ф43 Альберто Феррари, Марко Руссо
Шаблоны DAX. Наиболее полное собрание готовых к использованию решений на языке DAX для Power BI, Analysis Services и Power Pivot / пер.
с англ. А. Ю. Гинько. – М.: ДМК Пресс, 2021. – 408 с.: ил.
ISBN 978-5-97060-909-5
Данная книга предназначена для разработчиков, уже знакомых с
языком DAX и желающих повысить свою квалификацию, используя
представленные здесь шаблоны. В большинстве из них применяются
продвинутые техники DAX, которые читателю следует освоить и использовать в своих рабочих сценариях. Авторы уделяют пристальное
внимание сценариям с использованием функций логики операций со
временем, а также рассматривают большое количество действительно
полезных шаблонов, наиболее часто встречающихся на практике.
Для каждого шаблона предоставляются демонстрационные файлы в
формате Power BI и Power Pivot для Excel. Иногда версии кода незначительно отличаются. Предпочтение отдается программному продукту
Power BI, в котором на момент написания книги реализованы все актуальные новинки DAX. Рассматриваемые шаблоны проверены в июньской версии Power BI 2020 года, Excel 2019 и Excel для Microsoft 365 версии 2006.
Авторы книги – одни из ведущих специалистов в области бизнесаналитики, основавшие сайт SQLBI.com, на котором регулярно публикуются статьи по DAX и другим инструментам Microsoft, и выпустившие
«Подробное руководство по DAX» (издание на русском языке – М.: ДМК
Пресс, 2021) для начинающих пользователей.
Publisher / Editorial Production: SQLBI Corp., Las Vegas, NV, Unites States. Copyright © 2020
by Alberto Ferrari and Marco Russo. All rights reserved. © 2021 by DMK Press. All rights reserved.
Все права защищены. Любая часть этой книги не может быть воспроизведена в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения
владельцев авторских прав.
Материал, изложенный в данной книге, многократно проверен. Но, поскольку вероятность технических ошибок все равно существует, издательство не может гарантировать
абсолютную точность и правильность приводимых сведений. В связи с этим издательство
не несет ответственности за возможные ошибки, связанные с использованием книги.
ISBN 978-1-7353652-0-6 (англ.)
ISBN 978-5-97060-909-5 (рус.)
© Alberto Ferrari, Marco Russo, 2020
© Оформление, перевод на русский язык,
издание, ДМК Пресс, 2021
Оглавление
https://t.me/it_boooks
Предисловие от издательства .......................................................... 11
Введение ............................................................................................ 12
Глава 1. Вычисления, связанные со временем ............................... 18
Глава 2. Стандартные вычисления на основе времени ................. 23
Введение в вычисления на основе времени ...................................................24
Стандартные функции логики операций со временем ....................................... 25
Отключение автоматической даты и времени .................................................... 27
Ограничения функций логики операций со временем ....................................... 27
Создание таблицы дат............................................................................................ 28
Управление визуализациями для будущих дат.................................................... 30
Соглашение об именованиях ................................................................................ 30
Вычисление нарастающих итогов ...................................................................32
Нарастающие итоги с начала года ........................................................................ 33
Нарастающие итоги с начала квартала ................................................................ 35
Нарастающие итоги с начала месяца ................................................................... 36
Сравнение периодов ........................................................................................38
Годовое сравнение ................................................................................................. 38
Квартальное сравнение ......................................................................................... 40
Месячное сравнение .............................................................................................. 42
Сравнение периодов .............................................................................................. 43
Сравнение нарастающих итогов .....................................................................45
Годовое сравнение нарастающих итогов ............................................................. 45
Квартальное сравнение нарастающих итогов ..................................................... 47
Месячное сравнение нарастающих итогов .......................................................... 49
Сравнение нарастающих итогов с полным предыдущим периодом ...........50
Сравнение нарастающих итогов за год с полным прошлым годом ................... 51
Сравнение нарастающих итогов за квартал с полным прошлым кварталом.... 54
Сравнение нарастающих итогов за месяц с полным прошлым месяцем.......... 56
Вычисление скользящей годовой суммы .......................................................58
Скользящая годовая сумма.................................................................................... 58
Сравнение скользящих годовых сумм .................................................................. 59
Скользящее среднее .........................................................................................61
Скользящее среднее за 30 дней ............................................................................. 62
Скользящее среднее за три месяца ....................................................................... 63
Скользящее среднее за год .................................................................................... 64
Фильтрация других атрибутов даты ...............................................................65
Глава 3. Вычисления на основе месяца ........................................... 69
Введение в вычисления на основе месяца .....................................................70
Создание таблицы дат............................................................................................ 70
Соглашение об именованиях ................................................................................ 75
6
 Оглавление
Вычисление нарастающих итогов ...................................................................77
Нарастающие итоги с начала года ........................................................................ 77
Нарастающие итоги с начала квартала ................................................................ 78
Сравнение периодов ........................................................................................79
Годовое сравнение ................................................................................................. 80
Квартальное сравнение ......................................................................................... 81
Месячное сравнение .............................................................................................. 83
Сравнение периодов .............................................................................................. 84
Сравнение нарастающих итогов .....................................................................86
Годовое сравнение нарастающих итогов ............................................................. 86
Квартальное сравнение нарастающих итогов ..................................................... 88
Сравнение нарастающих итогов с полным предыдущим периодом ...........89
Сравнение нарастающих итогов за год с полным прошлым годом ................... 90
Сравнение нарастающих итогов за квартал с полным прошлым кварталом.... 91
Вычисление скользящей годовой суммы .......................................................93
Скользящая годовая сумма.................................................................................... 93
Сравнение скользящих годовых сумм .................................................................. 94
Скользящее среднее .........................................................................................96
Скользящее среднее за три месяца ....................................................................... 96
Скользящее среднее за год .................................................................................... 97
Работа с годами, содержащими больше 12 месяцев ......................................97
Глава 4. Вычисления на основе недели .......................................... 100
Введение в вычисления на основе недели....................................................101
Создание таблицы дат.......................................................................................... 101
Фильтрозащищенные столбцы ........................................................................... 104
Управление визуализациями для будущих дат.................................................. 105
Соглашение об именованиях .............................................................................. 106
Вычисление нарастающих итогов .................................................................108
Нарастающие итоги с начала года ...................................................................... 109
Нарастающие итоги с начала квартала .............................................................. 111
Нарастающие итоги с начала месяца ................................................................. 112
Нарастающие итоги с начала недели.................................................................. 114
Сравнение периодов ......................................................................................115
Годовое сравнение ............................................................................................... 115
Квартальное сравнение ....................................................................................... 117
Недельное сравнение ........................................................................................... 119
Сравнение периодов ............................................................................................ 121
Сравнение нарастающих итогов ...................................................................123
Годовое сравнение нарастающих итогов ........................................................... 123
Квартальное сравнение нарастающих итогов ................................................... 125
Недельное сравнение нарастающих итогов ....................................................... 127
Сравнение нарастающих итогов с полным предыдущим периодом .........130
Сравнение нарастающих итогов за год с полным прошлым годом ................. 130
Сравнение нарастающих итогов за квартал с полным прошлым кварталом .... 131
Сравнение нарастающих итогов за неделю с полной прошлой неделей......... 133
Оглавление  7
Вычисление скользящей годовой суммы .....................................................135
Скользящая годовая сумма.................................................................................. 135
Сравнение скользящих годовых сумм ................................................................ 136
Скользящее среднее .......................................................................................138
Скользящее среднее за четыре недели ............................................................... 138
Скользящее среднее за квартал .......................................................................... 140
Скользящее среднее за год .................................................................................. 141
Глава 5. Пользовательские вычисления, связанные
со временем ...................................................................................... 143
Введение в пользовательские вычисления, связанные со временем .........144
Создание таблицы дат.......................................................................................... 145
Фильтрозащищенные столбцы ........................................................................... 148
Управление визуализациями для будущих дат.................................................. 150
Соглашение об именованиях .............................................................................. 151
Вычисление нарастающих итогов .................................................................153
Нарастающие итоги с начала года ...................................................................... 153
Нарастающие итоги с начала квартала .............................................................. 156
Нарастающие итоги с начала месяца ................................................................. 157
Сравнение периодов ......................................................................................158
Годовое сравнение ............................................................................................... 158
Квартальное сравнение ....................................................................................... 161
Месячное сравнение ............................................................................................ 163
Сравнение периодов ............................................................................................ 164
Сравнение нарастающих итогов ...................................................................166
Годовое сравнение нарастающих итогов ........................................................... 166
Квартальное сравнение нарастающих итогов ................................................... 168
Месячное сравнение нарастающих итогов ........................................................ 171
Сравнение нарастающих итогов с полным предыдущим периодом .........174
Сравнение нарастающих итогов за год с полным прошлым годом ................. 174
Сравнение нарастающих итогов за квартал с полным прошлым кварталом .. 176
Сравнение нарастающих итогов за месяц с полным прошлым месяцем........ 178
Вычисление скользящей годовой суммы .....................................................179
Скользящая годовая сумма.................................................................................. 180
Сравнение скользящих годовых сумм ................................................................ 181
Скользящее среднее .......................................................................................184
Скользящее среднее за 30 дней ........................................................................... 184
Скользящее среднее за три месяца ..................................................................... 186
Скользящее среднее за год .................................................................................. 187
Глава 6. Сравнение разных временных интервалов .................... 189
Описание шаблона .........................................................................................189
Глава 7. Полуаддитивные вычисления........................................... 193
Введение..........................................................................................................194
Первая и последняя даты ...............................................................................195
Первая и последняя даты с данными ............................................................196
8
 Оглавление
Первая и последняя даты по клиенту ...........................................................198
Остатки на начало и конец периода .............................................................200
Рост за период .................................................................................................204
Глава 8. Нарастающие итоги ........................................................... 208
Базовый сценарий ..........................................................................................208
Нарастающие итоги по столбцам с сортировкой .........................................211
Глава 9. Таблица параметров .......................................................... 213
Изменение единиц измерения меры ............................................................213
Множество независимых параметров ..........................................................216
Множество зависимых параметров ..............................................................217
Динамический выбор N лидирующих товаров ............................................219
Глава 10. Статическая сегментация................................................ 222
Базовый шаблон .............................................................................................222
Диапазоны цен по категориям ......................................................................226
Диапазоны цен в объемных таблицах ..........................................................228
Глава 11. Динамическая сегментация ............................................ 230
Базовый шаблон .............................................................................................230
Кластеризация по изменению продаж .........................................................234
Кластеризация по лучшему статусу ..............................................................236
Глава 12. ABC-анализ........................................................................ 239
Статический ABC-анализ ...............................................................................240
Снимок ABC-анализа......................................................................................242
Динамический ABC-анализ ...........................................................................247
Поиск категории ABC .....................................................................................249
Глава 13. Новые и постоянные покупатели ................................... 252
Введение..........................................................................................................253
Описание шаблона .........................................................................................257
Внутренние меры ................................................................................................. 260
Внешние меры ...................................................................................................... 261
Как использовать меры из шаблона ................................................................... 261
Динамический относительный шаблон........................................................263
Внутренние меры ................................................................................................. 263
Новые покупатели ................................................................................................ 265
Ушедшие покупатели ........................................................................................... 266
Временно ушедшие покупатели ......................................................................... 267
Вернувшиеся покупатели .................................................................................... 269
Постоянные покупатели ...................................................................................... 270
Динамический абсолютный шаблон .............................................................271
Внутренние меры ................................................................................................. 272
Новые покупатели ................................................................................................ 273
Ушедшие покупатели ........................................................................................... 274
Оглавление  9
Временно ушедшие покупатели ......................................................................... 275
Вернувшиеся покупатели .................................................................................... 276
Постоянные покупатели ...................................................................................... 277
Обобщенный динамический шаблон (по категории) ..................................278
Внутренние меры ................................................................................................. 280
Новые покупатели ................................................................................................ 281
Ушедшие покупатели ........................................................................................... 284
Временно ушедшие покупатели ......................................................................... 285
Вернувшиеся покупатели .................................................................................... 287
Постоянные покупатели ...................................................................................... 290
Использование снимков ................................................................................292
Создание производных снимков в DAX.............................................................. 296
Глава 14. Количество уникальных связанных элементов ............ 301
Описание шаблона .........................................................................................301
Глава 15. Незавершенные события................................................. 306
Определение незавершенных событий ........................................................306
Открытые заказы............................................................................................309
Открытые заказы с использованием снимков .............................................313
Глава 16. Ранжирование .................................................................. 318
Статическое ранжирование ...........................................................................318
Динамическое ранжирование .......................................................................320
Три лидирующих товара в категории ...........................................................322
Глава 17. Иерархии ........................................................................... 325
Определение текущего уровня иерархии .....................................................325
Доля от родительского уровня .......................................................................327
Глава 18. Иерархии типа родитель–потомок ................................ 329
Введение..........................................................................................................329
Базовый шаблон иерархии типа родитель–потомок ...................................331
Иерархия ведомости.......................................................................................336
Шаблон безопасности для иерархии родитель–потомок ............................343
Глава 19. Сравнение сопоставимых показателей ......................... 345
Введение..........................................................................................................345
Сопоставимые продажи по магазинам с помощью снимков......................347
Сопоставимые продажи по магазинам без помощи снимков ....................350
Глава 20. Матрица переходов .......................................................... 352
Введение..........................................................................................................352
Статическая матрица переходов ...................................................................355
Динамическая матрица переходов ...............................................................358
Глава 21. Опросник ........................................................................... 362
Описание шаблона .........................................................................................362
10
 Оглавление
Глава 22. Анализ покупательской корзины ................................... 368
Определение метрик ассоциативных правил ..............................................369
Простые отчеты ..............................................................................................371
Базовый пример шаблона ..............................................................................373
Оптимизированный пример шаблона ..........................................................379
Глава 23. Пересчет курсов валют .................................................... 382
Несколько валют источника, одна валюта отчета ........................................383
Одна валюта источника, несколько валют отчета ........................................386
Несколько валют источника, несколько валют отчета .................................389
Глава 24. Прогнозирование ............................................................. 392
Введение..........................................................................................................392
Модель данных ...............................................................................................393
Бизнес-требования .........................................................................................396
Прогноз на основании предыдущего года ......................................................... 396
Вышедшие из оборота товары не учитываются в прогнозе ............................. 396
У новых товаров есть свой прогноз .................................................................... 397
Товары добавляются и удаляются на ежегодной основе ................................... 397
Прогнозирование ...........................................................................................398
Фактические данные и прогноз на одной диаграмме .................................401
Предметный указатель .................................................................... 406
Предисловие от издательства
Отзывы и пожелания
Мы всегда рады отзывам наших читателей. Расскажите нам, что вы
думаете об этой книге, – что понравилось или, может быть, не понравилось. Отзывы важны для нас, чтобы выпускать книги, которые будут для
вас максимально полезны.
Вы можете написать отзыв на нашем сайте www.dmkpress.com, зайдя на
страницу книги и оставив комментарий в разделе «Отзывы и рецензии». Также можно послать письмо главному редактору по адресу
dmkpress@gmail.com; при этом укажите название книги в теме письма.
Если вы являетесь экспертом в какой-либо области и заинтересованы
в написании новой книги, заполните форму на нашем сайте по адресу
http://dmkpress.com/authors/publish_book/ или напишите в издательство по адресу
dmkpress@gmail.com.
Список опечаток
Хотя мы приняли все возможные меры для того, чтобы обеспечить
высокое качество наших текстов, ошибки все равно случаются. Если вы
найдете ошибку в одной из наших книг – возможно, ошибку в основном тексте или программном коде, – мы будем очень благодарны, если
вы сообщите нам о ней. Сделав это, вы избавите других читателей от
недопонимания и поможете нам улучшить последующие издания этой
книги.
Если вы найдете какие-либо ошибки в коде, пожалуйста, сообщите о
них главному редактору по адресу dmkpress@gmail.com, и мы исправим это
в следующих тиражах.
Нарушение авторских прав
Пиратство в интернете по-прежнему остается насущной проблемой.
Издательства «ДМК Пресс» и SQLBI Corp. очень серьезно относятся к
вопросам защиты авторских прав и лицензирования. Если вы столкнетесь в интернете с незаконной публикацией какой-либо из наших книг,
пожалуйста, пришлите нам ссылку на интернет-ресурс, чтобы мы могли применить санкции.
Ссылку на подозрительные материалы можно прислать по адресу
электронной почты dmkpress@gmail.com.
Мы высоко ценим любую помощь по защите наших авторов, благодаря которой мы можем предоставлять вам качественные материалы.
Введение
В SQLBI у нас прекрасная работа – мы обучаем и консультируем людей
по всему миру. Ежегодно мы встречаемся с тысячами людей из разных
стран – такими разными, но такими одинаковыми в своей страсти к
бизнес-аналитике и DAX. Вместе со студентами и заказчиками мы решаем сценарии различной степени сложности.
Представьте, что к вам обратился студент с просьбой помочь ему
рассчитать количество новых покупателей для своего отчета. Вы, разумеется, помогаете ему. Затем решаете подобную задачу для кого-то
еще. И еще. В какой-то момент вам, естественно, захочется выработать
готовое решение или шаблон. И именно это сподвигло нас в 2013 году
запустить сайт daxpatterns.com. Мы постепенно начали накапливать шаблоны повторяющихся задач и собирать коллекцию готовых формул на
языке DAX, призванных решать наиболее часто встречающиеся сценарии. В то время мы не собирались писать новую книгу, а лишь хотели
собрать некое подобие базы данных для своих будущих решений. При
этом сайт предназначался преимущественно для нас самих.
Однако, как это часто бывает, жизнь распорядилась по-своему.
К счастью, на этот раз в положительном смысле. Наш сайт показал невероятные цифры по посещаемости. Читатели загружали шаблоны,
преследуя сразу две цели: во-первых, они получали готовое решение
своего сценария, а во-вторых, улучшали свои навыки в области DAX,
разбирая наши формулы. Из-за разницы форматов мы включали примеры для Excel 2010 и Excel 2013 – кстати, последние прекрасно работают и в более поздних версиях. В конце концов мы объединили все
написанные нами рецепты в одну книгу – так на свет появилось первое
издание «Шаблонов DAX» (DAX Patterns). Это был конец 2015 года. В то
время мы еще даже не опубликовали первое издание книги «Подробное
руководство по DAX» (The Definitive Guide to DAX), так что решили включить в «Шаблоны DAX» небольшое введение в язык.
Много воды утекло с тех пор. Язык DAX обогатился несколькими
новыми функциями. Но, что более важно, рынок пополнился новым
продуктом под названием Power BI, и рост количества потенциальных
разработчиков DAX приобрел экспоненциальный характер. Сегодня эти
люди в основной своей массе работают именно с Power BI, а когда мы
опубликовали первое издание книги, которую вы держите в руках, этот
программный продукт еще даже не был анонсирован.
В течение пяти последних лет мы не прекращали разрабатывать новые шаблоны. Мы знакомились с новыми студентами и заказчиками,
решали новые сценарии, улучшали свои собственные навыки владения языком DAX. К тому же у нас появились тысячи читателей, которые
Введение  13
обеспечивали нам качественную обратную связь по созданным ранее
шаблонам. Внимательное изучение их отзывов позволило нам лучше
понять, чего они ждут от наших шаблонов. За это время мы также успели выпустить два издания книги «Подробное руководство по DAX» (The
Definitive Guide to DAX), что лишило смысла снабжение нового варианта
книги о шаблонах описанием языка по примеру первого издания.
В общем, в какой-то момент пришло время обновить как наш сайт,
посвященный шаблонам DAX, так и книгу. Тогда мы закатали рукава и
принялись за работу, плоды которой вы сейчас держите в руках.
Важно отметить, что при написании второго издания мы не использовали примеры из первой редакции. Мы хотели все начать с нуля –
таков был наш план. В результате весь код был переписан заново с
использованием последних новинок в DAX и Power BI и при необходимости адаптирован под Excel 2019.
При разработке второго издания книги мы уделили повышенное
внимание следующим аспектам:
• существенно увеличили долю сценариев с использованием
функций логики операций со временем. Это одна из наиболее
распространенных и проблемных тем, так что мы решили посвятить ей гораздо больше времени;
• большой популярностью в первом издании книги пользовались
шаблоны, посвященные вычислению количества новых покупателей и постоянных клиентов. Этому аспекту бизнес-аналитики
во второй редакции мы также уделили повышенное внимание,
расширив количество формул и моделей данных при расчете
соответствующих показателей;
• увеличили количество действительно полезных шаблонов в книге – в частности это касается примеров, которые, по нашему опыту, наиболее часто встречаются на практике;
• одновременно мы уменьшили число шаблонов, которые со временем утратили свою актуальность. Например, в 2015 году большой популярностью пользовались специфические статистические расчеты по причине недостатка статистических функций в
прежних версиях языка DAX. С тех пор в DAX появилось множество полезных функций, призванных облегчить написание формул для соответствующих вычислений. В связи с этим необходимость в подобном контенте просто отпала;
• мы исключили из книги незаполненные фрагменты кода. В первом издании книги большинство листингов было представлено с
полями для подстановки, которые читатели могли менять на свои
названия столбцов. Сейчас же мы отказались от такой практики
14
 Введение
в пользу показа полностью рабочего кода, поскольку вам и так
в большинстве случаев придется адаптировать модель данных и
формулы для полноценного использования наших примеров. Мы
посчитали, что такой подход позволит сделать листинги более
простыми для восприятия и встраивания в конкретную модель;
• мы пересмотрели все без исключения формулы. В результате весь
код, представленный в книге, был максимально оптимизирован с
точки зрения производительности. При этом мы не заявляем, что
выстроенные нами шаблоны безусловно лучшие – они лучшие из
того, что мы смогли придумать. Если вы знаете, как сделать наш
код быстрее и лучше, расскажите. Раздел с комментариями на нашем сайте – отличное место для подобных предложений;
• каждый сценарий представлен в сопроводительных файлах
в двух вариантах: для Power BI и Excel, тогда как иллюстрации в
книге приведены исключительно на примере Power BI;
• и наконец, мы улучшили электронную версию книги. Это означает, что форматирование листингов теперь должно оставаться
неизменным вне зависимости от размера вашего мобильного
устройства.
Для чего мы издали эту книгу
Если вас интересует разница между содержимым книги, которую вы
держите в руках, и наполнением сайта daxpatterns.com, спешим заверить
вас, что ее нет. Стоит ли покупать эту книгу, чтобы получить дополнительный контент? Нет. Доступ к нашему сайту бесплатный, и вы можете
изучить все его содержимое1, а также скачать все шаблоны.
Приобретать книгу стоит, если вы хотите всегда иметь под рукой разработанные нами шаблоны на бумажном носителе или пополнить нашей книгой свою коллекцию. Это также поможет нам поддержать свой
бизнес. Честно говоря, мы были удивлены тем, сколько людей купили
первое издание этой книги. Это мотивировало нас на продолжение работы – мы всерьез занялись актуализацией и обновлением сайта и следующего издания книги. Надеемся, что эта работа будет продолжена!
На сайте daxpatterns.com все представленные в книге шаблоны также
снабжены видеофрагментами. В них подробно объясняется, как использовать тот или иной шаблон и как работают формулы. Эти видео – платные. При этом вы можете купить их все или по одному – для конкретной
темы, которую хотите изучить более подробно. Это дополнительная услуга, и она пользуется популярностью. Некоторым достаточно книги,
другие хотят смотреть видео, а есть и такие, которым нужно все и сразу.
1
Все содержимое сайта на английском языке. – Прим. перев.
Введение  15
Как использовать эту книгу
Что вы найдете в данной книге? В каждой главе мы будем разбирать
один конкретный шаблон, и читать ее можно без предварительного
ознакомления с другими главами книги. Например, вы можете разбирать шаблон, касающийся курсов валют, без рассмотрения примеров
с анализом покупательской корзины или вычислениями, связанными
с логикой операций со временем.
Каждая глава начинается с краткого описания соответствующего
бизнес-сценария, следом за которым идет подробный разбор примененного решения, включая код на языке DAX, использованный в примере. При этом описание самого листинга приводится довольно краткое, а меры при необходимости документируются в коде при помощи
комментариев.
Для каждого шаблона вы можете загрузить соответствующий ему сопроводительный контент, короткая ссылка на который размещается в
начале каждой главы. Как мы уже говорили, скачиваемые файлы представлены в двух форматах: для Power BI и Excel.
Книга может быть использована в качестве справочного пособия.
Когда вам нужно реализовать тот или иной шаблон, вы не хотите читать
длинные описания – вам нужен лишь код и краткое сопровождение к
нему. В связи с этим мы решили не утомлять вас пространными размышлениями, поставив во главу угла код на языке DAX.
И все же, перед тем как реализовывать на практике тот или иной шаблон, мы настоятельно советуем вам прочитать соответствующую главу
целиком. Причина в том, что мы зачастую даем несколько вариантов
решения сценария, и вам предстоит самому выбрать наиболее подходящий для вашего случая. Для каждого шаблона мы также предоставляем демонстрационные файлы в формате Power BI и Power Pivot для
Excel. Иногда версии кода будут незначительно отличаться. В качестве
решения предпочтение всегда будет отдаваться программному продукту Power BI, в котором на момент написания книги реализованы все актуальные новинки DAX. Некоторые из этих особенностей пока не были
реализованы в Power Pivot, и именно это послужило поводом для нашего выбора в пользу Power BI.
Есть в данной книге одно исключение, и касается оно темы вычислений, связанных со временем. Как мы уже сказали, этому аспекту бизнес-аналитики мы уделили в книге особое внимание, представив четыре различных шаблона для подобных расчетов. Каждый из четырех
шаблонов весьма объемен, а вместе они занимают более 40 % книги.
Поэтому мы решили выделить отдельную вводную главу, посвященную
вычислениям, связанным со временем, цель которой – помочь вам сделать правильный выбор шаблона для вашего сценария. Таким образом,
16
 Введение
если вам необходимо реализовать на практике расчеты, связанные с логикой операций со временем, для начала прочитайте введение, после
чего приступайте к освоению главы, в которой представлено описание
выбранного вами шаблона.
Требования
Мы хотим сразу оговориться о том, что данная книга не является полноценным учебником по языку DAX2.
Мы предполагаем, что вы уже знакомы с этим языком, иначе вы не
сможете в полной мере использовать представленные в данной книге
шаблоны. В большинстве шаблонов применяются продвинутые техники DAX, которые вам следует освоить и использовать в своих рабочих
сценариях. Читая эту книгу, вы не научитесь писать запросы на DAX с
нуля. Скорее, она позволит вам стать более квалифицированным разработчиком.
Мы надеемся, что вы будете использовать предложенные в книге шаблоны с последними версиями Power BI или Excel, поскольку язык DAX
не стоит на месте, а постоянно развивается. Мы проверяли шаблоны в
июньской версии Power BI 2020 года, Excel 2019 и Excel для Microsoft 365
версии 2006. Большинство примеров будет прекрасно работать и с более ранними версиями Power BI и Excel, но мы не можем гарантировать
этого, поскольку не проводили тесты с использованием всех прежних
версий продуктов.
Благодарности
Последним по порядку, но не по значению, во введении мы приведем
раздел с благодарностями.
Кого мы больше всех хотим поблагодарить, так это вас. Работа, которую мы проделали над книгой, стала возможна только благодаря
продолжительным дискуссиям с нашими читателями, пользователями,
заказчиками и студентами – такими, как вы. Так что вы, даже не осознавая этого, можете считать себя в некотором смысле соавтором этой
книги. А если разместите свои комментарии на наших общедоступных
форумах, то фактически продолжите вносить вклад и в наши будущие
работы.
Но есть и люди, которых мы хотим отметить отдельно за участие в
создании данной книги. В первую очередь это Даниил Маслюк (Daniil
Maslyuk), тщательнейшим образом проверивший все предложенные
нами шаблоны на практике, нашедший все возможные ошибки и обеспечивший нас неоценимой обратной связью. Также хотелось бы сер2
Феррари А., Руссо М. Подробное руководство по DAX. – М.: ДМК Пресс, 2020. – 776 с.: ил. ISBN:9785-97060-859-3.
Введение  17
дечно поблагодарить Клэр Косту (Claire Costa) за вклад в редакцию
наших текстов на английском и улучшение их восприятия. Точностью
выражений и правильностью формулировок мы обязаны именно ей.
Серджио Мурру (Sergio Murru) приложил руку к созданию версий сопроводительных файлов для Excel, что позволило пользователям Power
Pivot для Excel также применять в работе наши шаблоны. Кроме того,
хочется отметить Даниэле Перилли (Daniele Perilli), без которого книга
и сайт не были бы столь привлекательными. За наполнение книги и все
возможные ошибки в ней ответственность несем только мы, но, если
эта книга сможет помочь вам рассчитать нужные вам показатели в Excel
или Power BI и хорошо разобраться в интересующих вас темах, в этом
заслуга тех, кого мы перечислили.
Наслаждайтесь DAX!
Глава
1
Вычисления, связанные
со временем
https://t.me/it_boooks
В данной главе мы представим вашему вниманию четыре шаблона вычислений, связанных со временем (time-related calculation), о которых
подробнее будем говорить далее в книге. Цель этой главы – помочь вам
выбрать наиболее подходящий шаблон в зависимости от поставленной
задачи. Дело в том, что, когда речь идет о вычислениях, связанных со
временем, бывает довольно трудно выбрать лучший вариант для конкретного сценария.
Что же подразумевается под вычислениями, связанными со временем? Как ясно из определения, сюда включаются все возможные расчеты, в которых так или иначе фигурирует временная составляющая.
В примерах вы встретите разные варианты вычислений, включая нарастающие итоги с начала года, квартала и месяца. В этих вычислениях
значения учитываются с начала указанного временного периода (года,
квартала или месяца соответственно) до отчетной даты, и возвращается нужная агрегация. При этом определение временного периода меняется в зависимости от того, с каким календарем вы работаете: с григорианским или финансовым. На рис. 1.1 приведен пример вычисления
нарастающих итогов, где YTD (year-to-date) означает итог с начала года,
в QTD (quarter-to-date) – с начала квартала.
Также в шаблоны работы со временем включаются вычисления показателей сравнения одного временного периода с другим. Например,
вы можете сравнить продажи за текущий месяц с аналогичным показателем за этот же месяц годом ранее. Еще одним примером шаблона, связанного со временем, является вычисление скользящего среднего
(moving average) за определенный период. Допустим, вам может быть
интересно узнать скользящее среднее по продажам за последние 12 месяцев. Этот метод позволяет сгладить линии на графике и исключить
из вычислений влияние сезонности. В четырех шаблонах для работы со
временем реализован соответствующий набор вычислений.
Вычисления, связанные со временем  19
Рис. 1.1. Примеры вычисления нарастающих итогов
Что отличает один шаблон от другого, так это определение используемого календаря. Вы, наверное, заметили, что на рис. 1.1 для нарастающего итога с начала года выделены сразу два столбца. В зависимости от
того, какой календарь вы используете – григорианский или финансовый, – показатели будут разные. Из-за разницы в определениях календарей вычисления, связанные со временем, могут значительно усложняться.
Например, в вашей компании может быть принят недельный календарь на основе стандарта ISO или даже свой собственный. В календаре, основанном на неделях, каждый месяц и год начинаются с одного
и того же дня недели. Таким образом, год в соответствии с недельным
календарем может начаться в предыдущем календарном году, а закончиться – в следующем. Усложняется ситуация и тем, что в некоторых календарях год может разбиваться на 13 периодов вместо привычных 12
для нужд учета. Именно требования к календарю должны лежать в основе выбора шаблона в той или иной ситуации.
Четыре представленных шаблона для работы со временем в порядке
увеличения сложности:
1)
2)
3)
4)
стандартные вычисления на основе времени;
вычисления, базирующиеся на месяцах;
вычисления, базирующиеся на неделях;
пользовательские вычисления, связанные со временем.
Шаблон стандартных вычислений для работы со временем реализован при помощи встроенных функций логики операций со временем
языка DAX. В этом случае предполагается, что вы используете для расчетов стандартный григорианский календарь, а ваш финансовый год
20
 Глава 1. Вычисления, связанные со временем
начинается одновременно с началом квартала по григорианскому календарю. Например, функции логики операций со временем прекрасно
работают, если ваш финансовый год начинается 1 июля (начало третьего квартала по григорианскому календарю). В то же время эти функции
могут выдавать неожиданные результаты при начале финансового года
1 марта. И причины здесь две: во-первых, эта дата не совпадает с началом квартала по григорианскому календарю, во-вторых, есть застарелая ошибка в обработке високосных годов при работе с финансовыми
календарями. Несмотря на указанные ограничения, данный шаблон достаточно легко использовать и реализовывать, поскольку он опирается
на стандартные функции DAX и работает с обычной таблицей дат.
В следующих трех шаблонах стандартные функции логики операций
со временем не используются. Вместо этого они основаны на обычных
функциях DAX, что добавляет гибкости в отношении определения кварталов, месяцев и недель. При реализации этих шаблонов вам необходимо создать собственную таблицу дат со столбцами, которые будут
использоваться в мерах DAX для определения составных временных частей года. Например, вам понадобится один столбец для года, один для
квартала, месяца, а также дополнительные столбцы, которые позволят
упростить вычисления.
Более того, при определении и фильтрации периодов должны быть
учтены многие детали. Некоторые вычисления, которые могут представляться простыми с точки зрения человека, могут оказаться довольно сложными для компьютера. Сравнивая текущий квартал с предыдущим, вы будете оперировать разным количеством дней в этих
периодах, поскольку первый квартал года (с января по март) включает
в себя меньше дней по сравнению со вторым (с апреля по июнь). То же
самое касается и месяцев: январь всегда длиннее февраля, и, если вы захотите сравнивать показатели по месяцам, вам придется оперировать
разным количеством дней.
Если же стандартные функции логики операций со временем не подходят для вашего сценария, вам необходимо обратиться к одному из
следующих трех шаблонов, каждый из которых предполагает создание
собственной таблицы дат.
Шаблон, основанный на месяцах, является наиболее простым из них.
Вычисления в нем реализованы с учетом того, что вас не будет интересовать детализация по дням. К примеру, такой шаблон подойдет в случае, если вы собираетесь сравнивать друг с другом исключительно месяцы. Если вы предполагаете в процессе анализа опускаться на уровень
ниже, этот шаблон не годится. Допустим, если вам понадобится сравнить показатели за три дня в одном квартале с теми же тремя днями в
предыдущем квартале, потенциала выбранного шаблона вам просто не
хватит. Помимо недостатков данного шаблона в виде ограничений, за-
Вычисления, связанные со временем  21
ключающихся в невозможности проводить детализированный анализ,
есть у него и свои плюсы: работает он достаточно быстро, и реализовать
его относительно просто. Более того, он прекрасно справляется со сценариями, предполагающими наличие более 12 месяцев во временном
интервале. Данный шаблон обладает гибкостью, характерной для пользовательских шаблонов, в сочетании с легкостью реализации – большей
в сравнении со стандартным шаблоном. Если вас не пугает ограничение
в виде месячной гранулярности, этот шаблон может стать вашим выбором.
Шаблоны, базирующиеся на неделях, рассматривают в качестве календарной основы, как ясно из названия, недели. ISO 8601 представляет
собой один из стандартов определения недельной системы дат, хотя во
многих странах приняты другие национальные стандарты для идентификации годов, кварталов и недель. В году 52 или 53 недели, а квартал состоит из 13 недель, при этом каждый квартал по составу недель в
месяцах может относиться к одной из следующих категорий: 5 + 4 + 4,
4 + 5 + 4 или 4 + 4 + 5. Если в году 53 недели, в одном из кварталов будет присутствовать 14 недель. Поскольку неделя совсем не обязательно
должна полностью входить в месяц, группу недель, входящих в квартал,
стоит называть периодом, даже если зачастую к ней обращаются как к
месяцу. Именно поэтому в следующем описании мы будем называть
месяцы периодами.
В связи с тем, что базовой сущностью недельного календаря является именно неделя, не может быть никакого соответствия между годом
в григорианском календаре и недельном. Недельный календарь всегда
начинается с одного и того же дня недели, например с понедельника
или воскресенья. И этот день недели далеко не всегда будет выпадать
на 1 января. Для недельного календаря вполне нормальной будет ситуация, при которой год может начаться, к примеру, 29 декабря предыдущего года или 3 января текущего. Несмотря на всю свою необычность,
у недельных календарей есть и свои достоинства, в числе которых то,
что каждый «месяц» в квартале насчитывает одинаковое количество
дней недели. Таким образом, при сравнении двух кварталов будет сопоставляться одинаковое количество дней при одинаковом распределении дней недели.
Недельные календари требуют наличия в модели данных отдельной
таблицы дат со столбцами, облегчающими вычисления в DAX. Стоит
отметить, что в языке DAX нет готовых функций для осуществления
вычислений в подобных нестандартных календарях. В связи с этим все
вычисления в этом случае выполняются при помощи особого кода на
DAX. Что касается сложности шаблонов, то для недельных календарей
она выше по сравнению с месячными, поскольку здесь вы можете опускаться в фильтрах до детализации по дням. Если в вашем сценарии
22
 Глава 1. Вычисления, связанные со временем
предусмотрена недельная аналитика, этот шаблон подойдет вам лучше
остальных.
Шаблон, предполагающий пользовательские вычисления, связанные
со временем, является одновременно самым гибким, но и наиболее
сложным в реализации. Расчеты в этом шаблоне схожи со стандартным
шаблоном для работы со временем, за тем лишь исключением, что вычисления производятся при помощи базовых функций языка DAX – без
использования специальных функций логики операций со временем.
В связи с этим шаблон получается невероятно гибким, и вы можете свободно влиять на поведение всех сопутствующих вычислений. Но с большой гибкостью приходит и большая сложность.
Код на языке DAX при реализации таких шаблонов зачастую оказывается слишком сложным. К тому же разработчику необходимо уделять
большое внимание мелким деталям. Используйте этот шаблон только в
том случае, если остальные не отвечают требованиям вашей бизнес-логики и вам просто необходимо воспользоваться максимально возможной гибкостью системы дат.
Так какой же шаблон для работы со временем вам использовать?
• Если ваши требования к работе с датами и временем укладываются в понятия, принятые в григорианском календаре, вашим
выбором может быть стандартный шаблон.
• Если для ваших отчетов вполне достаточно уровня гранулярности
по месяцам (а это зачастую так, даже если кажется, что иначе), вам
подойдет месячный шаблон – он достаточно быстрый и простой.
• Если ваша бизнес-логика завязана на недельный календарь, вам
предстоит присмотреться к соответствующему шаблону.
• В случае, если всего перечисленного выше вам недостаточно и
вам действительно нужна максимальная гибкость, что ж, готовьтесь к трудному и увлекательному путешествию в мир контекстов
фильтра с погружением в пользовательские вычисления, связанные со временем.
Помните, что в проектах, связанных с бизнес-аналитикой, зачастую
проще – значит, лучше. Выберите наиболее простой шаблон, удовлетворяющий вашим требованиям. Но если вы хотите узнать обо всех возможных шаблонах перед тем, как сделать свой выбор, вы можете прочитать
все четыре главы, посвященные шаблонам для работы со временем, и
только после этого остановиться на одном из предложенных вариантов.
Глава
2
Стандартные вычисления
на основе времени
https://t.me/it_boooks
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
В данном шаблоне мы покажем, как осуществлять вычисления на основе
времени, такие как нарастающий итог с начала года, аналогичный период прошлого года и процентный прирост, с использованием стандартного календаря. Главным достоинством работы со стандартным календарем является то, что вы можете в своих расчетах всецело полагаться
на специальные функции логики операций со временем, присутствующие в языке DAX. Эти встроенные функции разработаны так, чтобы в
большинстве сценариев давать ожидаемые результаты.
Если ваши требования к системе не могут быть удовлетворены при
помощи встроенных функций DAX или вы располагаете нестандартным календарем, то вам придется реализовывать необходимую вам
логику без использования специальных функций для работы со временем. В этом случае вы сможете менять логику, реализованную в коде, по
своему усмотрению. В то же время вам может понадобиться добавить в
таблицу дат специфические столбцы, которые понадобятся формулам
DAX для правильного распространения фильтров. Эти пользовательские шаблоны будут описаны позже в данной книге.
При использовании стандартного григорианского календаря наиболее простым и эффективным способом осуществить вычисления на
основе времени будет применение встроенных функций логики операций со временем. Помните, что эти функции будут корректно работать только со стандартным григорианским календарем, в котором год
делится на 12 месяцев, в каждом из которых заранее известное количество дней, кварталы состоят из трех месяцев и т. д.
24
 Глава 2. Стандартные вычисления на основе времени
Введение в вычисления на основе времени
Первое, что вам понадобится для выполнения вычислений, зависящих
от времени, – это таблица дат, построенная по всем канонам. Эта таблица Date должна отвечать следующим требованиям:
• в ней должны присутствовать все без исключения даты отчетного года. Иными словами, таблица дат всегда должна начинаться с
1 января и заканчиваться 31 декабря, включая все дни между этими двумя датами. Если в отчете используется только финансовый
календарь, таблица дат должна включать все даты от начала до
конца финансового года. Например, если финансовый 2008 год
начинается 1 июля 2007 года и заканчивается 30 июня 2008-го, то
в таблице дат должны присутствовать все даты в этом интервале
включительно;
• в таблице дат обязательно должен присутствовать столбец с типом данных DateTime или Date, содержащий уникальные значения. Обычно этот столбец называется Date. В большинстве случаев именно этот столбец используется для связи таблицы дат с
другими таблицами в модели данных, но это требование не является обязательным. Так или иначе, в этом столбце должны присутствовать только уникальные значения, а сама таблица должна быть выделена определенным образом при помощи пункта
«Отметить как таблицу дат» (Mark as Date Table). Если в столбце
вместе с датой присутствует время, оно никак не должно использоваться – например, все времена должны быть выставлены на
12:00 пополудни;
• таблица дат должна быть соответствующим образом помечена в
модели данных, если связь между ней и другой таблицей (в нашем случае это таблица Sales) основывается не на столбце Date.
Создать таблицу дат можно сразу несколькими способами. От способа создания таблицы Date никак не зависит методика использования
стандартных функций для работы со временем, пока соблюдаются перечисленные выше требования к таблице. Если у вас уже есть таблица
дат, прекрасно работающая с вашими отчетами, просто импортируйте ее и пометьте соответствующим образом в модели, предварительно
убедившись, что отвечает всем заявленным требованиям. Если нет, вы
можете легко создать таблицу дат с использованием вычисляемой таблицы в DAX, как будет показано далее.
Соответствующая пометка таблицы дат, использующейся для вычислений, связанных со временем, в модели данных является хорошей
практикой. Эта пометка позволяет автоматически добавлять модификатор REMOVEFILTERS каждый раз, когда к столбцу Date применяется
Введение в вычисления на основе времени  25
фильтр. А применение фильтра происходит всегда, когда внутри функции CALCULATE используются функции логики операций со временем.
DAX реализует такое же поведении при объединении таблиц Sales и Date
с использованием столбца Date. Так или иначе, лучше всегда помечать
таблицу дат в модели данных соответствующим образом. В случае, если
в вашей модели присутствует несколько календарей, вы можете все их
пометить как таблицы дат.
Если не пометить таблицу дат и не использовать столбец Date для
связей с другими таблицами, всякий раз при использовании функций
логики операций со временем внутри CALCULATE вам придется явно
указывать модификатор REMOVEFILTERS. Такое поведение подробно
описано в статье по адресу https://sql.bi/28211.
Стандартные функции логики операций со временем
Встроенные в язык DAX функции логики операций со временем
представляют собой обычные табличные функции, возвращающие список дат для использования в качестве фильтров в функции
CALCULATE. Тот же результат всегда можно получить и без помощи
этих специальных функций, но выражение при этом будет намного
более многословным и сложным. Например, функция DATESYTD возвращает список дат начиная с первого числа года и до последней даты,
видимой в текущем контексте фильтра. Таким образом, следующее
короткое выражение:
DATESYTD ( 'Date'[Date] )
соответствует представленному ниже фильтрующему выражению:
VAR LastDateAvailable = MAX ( 'Date'[Date] )
VAR FirstJanuaryOfLastDate = DATE ( YEAR ( LastDateAvailable ); 1; 1 )
RETURN
FILTER (
ALL ( 'Date'[Date] );
AND (
'Date'[Date] >= FirstJanuaryOfLastDate;
'Date'[Date] <= LastDateAvailable
)
)
Существует множество функций логики операций со временем, и
большинство из них будет представлено в данном шаблоне. Заметим,
что эти функции следует применять в качестве аргументов фильтра в
26
 Глава 2. Стандартные вычисления на основе времени
функции CALCULATE, и иногда для этого вы будете прибегать к помощи переменных. При этом использовать функции логики операций со
временем внутри итераторов (iterator) бывает опасно по причине возникновения неявного преобразования контекста (context transition),
выполняемого с целью извлечения дат, присутствующих в контексте
фильтра. Подробнее о таком поведении можно почитать в инструкции
по DAX на странице https://dax.guide/datesytd.
Ниже приведены основные правила использования функций логики
операций со временем:
• применяйте функции логики операций со временем, подобные
DATESYTD, исключительно в качестве аргументов фильтра в
CALCULATE/CALCULATETABLE напрямую или через присвоение
переменным;
• используйте скалярные функции вроде EDATE и EOMONTH в
формулах DAX, возвращающих значения, также называемых скалярными выражениями (scalar expression). Эти функции не управляют логикой операций со временем и могут быть использованы
в выражениях, выполняемых в контексте строки;
• используйте функцию CONVERT для преобразования дат в числа
и наоборот;
• полный обновленный список функций логики операций со временем представлен по адресу https://dax.guide.
Новички в DAX, бывает, путают функции логики операций со временем с обычными скалярными функциями для работы со временем. Эта
путаница часто ведет к ошибкам, которых можно легко избежать, если
придерживаться следующих правил:
• НЕ используйте функцию DATEADD для получения предыдущего и следующего дня. Вы можете применять для этого простые
математические операторы;
• НЕ используйте функцию PREVIOUSDAY для вычисления предшествующего дня в скалярных выражениях. Для этого можно
просто вычесть единицу из даты;
• НЕ используйте функцию EOMONTH в качестве фильтра – вместо нее применяйте функцию ENDOFMONTH.
EOMONTH
представляет
собой
скалярное
выражение, а
ENDOFMONTH – это полноценная функция логики операций со временем. Всегда обращайте внимание на тип возвращаемого функцией значения: только табличные функции могут управлять логикой операций со
временем, и они не должны использоваться в скалярных выражениях.
Введение в вычисления на основе времени  27
Отключение автоматической даты и времени
В Power BI у вас есть возможность автоматически добавить в модель
данных таблицу дат. Но мы настоятельно рекомендуем отключить
эту опцию в Power BI и импортировать или создавать таблицу дат
вручную в явном виде. Более подробную информацию об этой настройке можно найти по адресу https://sql.bi/137706.
Присутствие автоматически созданной таблицы дат предполагает
использование своеобразной синтаксической нотации на основе столбцов, в которой следом за столбцом даты ставится точка, после чего указывается столбец автоматически созданной таблицы дат:
Sales[Order Date].[Date]
Быстрые меры в Power BI активно применяют такую нотацию при
использовании с автоматически созданными таблицами дат. Мы же не
полагаемся на такие таблицы в Power BI, поскольку хотим иметь полный контроль над нашими моделями данных. Показанный синтаксис
не используется для таблиц дат, являющихся частью модели, т. е. не созданных автоматически.
Ограничения функций логики операций со временем
Как мы уже говорили ранее, функции логики операций со временем
работают исключительно со стандартным григорианским календарем.
При этом у них есть некоторые ограничения, которые мы перечислим в
данном разделе. Если ваши требования к проекту несовместимы с этими ограничениями, вам придется обратиться к другим шаблонам (см.
главы, посвященные пользовательским и недельным шаблонам).
1. Год должен начинаться 1 января. В то же время есть ограниченная поддержка финансовых календарей, которые могут брать
свое начало с других дат. При этом все годы должны начинаться
с одной и той же даты – любой, за исключением 1 марта – из-за
исторической ошибки, связанной с високосными годами.
2. Квартал должен начинаться с первого числа января, апреля, июля
и октября. Диапазон дат в кварталах не может быть изменен.
3. Месяц всегда должен совпадать с календарным месяцем.
4. Фильтры по дополнительным столбцам, таким как день недели
или рабочий день, не поддерживаются корректно в стандартных
функциях логики операций со временем. Обходные пути будут
подробно описаны позже в этой книге.
28
 Глава 2. Стандартные вычисления на основе времени
Таким образом, многие продвинутые вычисления вроде недельного
анализа могут быть недоступны при использовании стандартных функций логики операций со временем. Они могут потребовать наличия
особых календарей.
Создание таблицы дат
Функции логики операций со временем DAX работают с любой стандартной таблицей григорианского календаря. Если у вас уже есть таблица дат, вы можете импортировать и использовать ее. В противном
случае можно легко создать таблицу дат с нуля с использованием вычисляемых таблиц (calculated table). Следующая формула вполне сгодится
для создания календаря в модели данных, которым мы будем пользоваться в этой главе.
Date =
VAR FirstFiscalMonth = 7 -- Первый месяц финансового года
VAR FirstDayOfWeek = 0
-- 0 = воскресенье, 1 = понедельник, ...
VAR FirstYear =
-- Первый год
YEAR ( MIN ( Sales[Order Date] ))
RETURN
GENERATE (
FILTER (
CALENDARAUTO ();
YEAR ( [Date] ) >= FirstYear
);
VAR Yr = YEAR ( [Date] )
-- Номер года
VAR Mn = MONTH ( [Date] )
-- Номер месяца (1-12)
VAR Qr = QUARTER ( [Date] )
-- Номер квартала (1-4)
VAR MnQ = Mn - 3 * (Qr - 1)
-- Номер месяца внутри
-- квартала (1-3)
VAR Wd = WEEKDAY ( [Date]; 1 ) - 1
-- Номер дня недели
-- (0 = воскресенье,
-- 1 = понедельник, ...)
VAR Fyr =
-- Номер финансового года
Yr + 1 * ( FirstFiscalMonth > 1 && Mn >= FirstFiscalMonth )
VAR Fqr =
-- Финансовый квартал (строка)
FORMAT ( EOMONTH ( [Date]; 1 - FirstFiscalMonth ); "\QQ" )
RETURN ROW (
"Year"; DATE ( Yr; 12; 31 );
"Year Quarter"; FORMAT ( [Date]; "\QQ-YYYY" );
"Year Quarter Date"; EOMONTH ( [Date]; 3 - MnQ );
"Quarter"; FORMAT ( [Date]; "\QQ" );
"Year Month"; EOMONTH ( [Date]; 0 );
"Month"; DATE ( 1900; MONTH ( [Date] ); 1 );
"Day of Week"; DATE ( 1900; 1; 7 + Wd + (7 * (Wd < FirstDayOfWeek)) );
"Fiscal Year"; DATE ( Fyr + (FirstFiscalMonth = 1);
FirstFiscalMonth; 1 ) - 1;
"Fiscal Year Quarter"; "F" & Fqr & "-" & Fyr;
Введение в вычисления на основе времени  29
"Fiscal Year Quarter Date"; EOMONTH ( [Date]; 3 - MnQ );
"Fiscal Quarter"; "F" & Fqr
)
)
Вы можете настроить под себя первые три переменные в этой формуле для соответствия построенной таблицы дат вашим бизнес-требованиям. Чтобы достичь желаемых результатов, столбцы в модели данных
должны быть сконфигурированы, как показано ниже. Если столбец не
текстовый, у него будет тип данных Date со стандартным или пользовательским форматом:
• Date: m/dd/yyyy (8/14/2007), используется в качестве столбца для
пометки таблицы дат;
• Year: yyyy (2007);
• Year Quarter: Text (Q3-2008);
• Year Quarter Date: скрытый (9/30/2008);
• Quarter: Text (Q1);
• Year Month: mmm yyyy (Aug 2007);
• Month: mmm (Aug);
• Day of Week: ddd (Tue);
• Fiscal Year: \F\Y yyyy (FY 2008);
• Fiscal Year Quarter: Text (FQ1-2008);
• Fiscal Year Quarter Date: скрытый (9/30/2008);
• Fiscal Quarter: Text (FQ1).
В данном шаблоне таблица дат обладает двумя иерархиями: календарной и финансовой:
• Calendar: Year (Year), Quarter (Year Quarter), Month (Year Month);
• Fiscal: Year (Fiscal Year), Quarter (Fiscal Year Quarter), Month (Year
Month).
Вне зависимости от источника таблица Date также должна содержать скрытый вычисляемый столбец DateWithSales для использования
в формулах.
Вычисляемый столбец в таблице дат:
DateWithSales =
'Date'[Date] <= MAX ( Sales[Order Date] )
Значение столбца Date[DateWithSales] будет равно TRUE для дат, располагающихся раньше последнего дня с транзакциями, включая этот
день. В противном случае значение будет FALSE. Иными словами,
Powered by TCPDF (www.tcpdf.org)
30
 Глава 2. Стандартные вычисления на основе времени
DateWithSales будет TRUE для «прошлых» дат и FALSE – для «будущих»,
где прошлое и будущее рассчитывается относительно последнего дня с
транзакциями в таблице Sales.
Управление визуализациями для будущих дат
В большинстве случаев вычисления, связанные с логикой операций
со временем, не должны показываться для будущих периодов. Например, расчет нарастающего итога с начала года может теоретически
производиться и для будущих дат, но нам бы хотелось скрыть эти значения. Набор данных, используемый в данном примере, ограничивается
15 августа 2009 года. Таким образом, мы рассматриваем август 2009-го,
третий квартал того года (Q3-2009) и сам 2009 год в качестве последних
периодов с данными. Любая дата позднее 15 августа 2009 года должна
рассматриваться как будущая, и значения по ней выводиться не должны.
Чтобы избежать отображения значений в будущих периодах, воспользуемся мерой ShowValueForDates, которая будет возвращать TRUE,
если выбранный временной период располагается не позже последнего
периода с данными.
Мера (скрытая) в таблице дат:
ShowValueForDates :=
VAR LastDateWithData =
CALCULATE (
MAX ( 'Sales'[Order Date] );
REMOVEFILTERS ()
)
VAR FirstDateVisible =
MIN ( 'Date'[Date] )
VAR Result =
FirstDateVisible <= LastDateWithData
RETURN
Result
Мера ShowValueForDates является скрытой. Это чисто техническая
мера, созданная с целью использования одной и той же логики в различных вычислениях, связанных с логикой операций со временем.
Пользователь не должен иметь возможности использовать эту меру в
отчетах напрямую.
Соглашение об именованиях
В табл. 2.1 приведены наименования, принятые для обозначения
вычислений, основанных на логике операций со временем. В столбцах
отображена информация о том:
Введение в вычисления на основе времени  31
• сдвигается ли вычисление во времени, например для определения того же периода в прошлом году;
• выполняется ли агрегация при вычислении, например как в случае с нарастающим итогом с начала года;
• производится ли сравнение двух временных периодов: допустим,
текущего года с прошлым.
Таблица 2.1. Принятые обозначения вычислений, основанных на логике операций
со временем
Сокращение
Описание
Сдвиг Агрегация
Сравнение
YTD
Year-to-date
(нарастающий итог с начала года)
X
QTD
Quarter-to-date
(нарастающий итог с начала квартала)
X
MTD
Month-to-date
(нарастающий итог с начала месяца)
X
MAT
Moving annual total
(скользящая годовая сумма)
X
PY
Previous year
(предыдущий год)
X
PQ
Previous quarter
(предыдущий квартал)
X
PM
Previous month
(предыдущий месяц)
X
PYC
Previous year complete
(предыдущий год полный)
X
PQC
Previous quarter complete
(предыдущий квартал полный)
X
PMC
Previous month complete
(предыдущий месяц полный)
X
PP
Previous period
(прошлый период) (автоматически
выбираются год, квартал или месяц)
X
PYMAT
Previous year moving annual total
(скользящая годовая сумма за прошлый год)
X
YOY
Year-over-year
(годовое сравнение)
X
QOQ
Quarter-over-quarter
(квартальное сравнение)
X
X
32
 Глава 2. Стандартные вычисления на основе времени
Сокращение
Описание
Сдвиг Агрегация
MOM
Month-over-month
(месячное сравнение)
MATG
Moving annual total growth
(скользящая годовая сумма, прирост)
POP
Period-over-period
(сравнение периодов) (автоматически
выбираются год, квартал или месяц)
PYTD
Previous year-to-date
(прошлогодний нарастающий итог с начала
года)
X
X
Previous quarter-to-date
(прошлогодний нарастающий итог с начала
квартала)
X
X
Previous month-to-date
(прошлогодний нарастающий итог с начала
месяца)
X
X
YOYTD
Year-over-year-to-date
(годовое сравнение нарастающим итогом)
X
X
QOQTD
Quarter-over-quarter-to-date (квартальное
сравнение нарастающим итогом)
X
X
MOMTD
Month-over-month-to-date
(месячное сравнение нарастающим итогом)
X
X
YTDOPY
Year- to-date-over-previous-year
(нарастающий итог с начала года
по сравнению с предыдущим годом)
X
X
Quarter- to-date-over-previous-quarter
(нарастающий итог с начала квартала
по сравнению с предыдущим кварталом)
X
X
Month- to-date-over-previous-month
(нарастающий итог с начала месяца
по сравнению с предыдущим месяцем)
X
X
PQTD
PMTD
QTDOPQ
MTDOPM
Сравнение
X
X
X
X
X
X
X
X
X
X
X
Вычисление нарастающих итогов
Вычисление нарастающего итога с начала года, квартала или месяца
модифицирует контекст фильтра таблицы Date, применяя диапазон дат
в качестве фильтра, заменяющего фильтр для текущего периода.
Все эти вычисления могут быть реализованы с использованием стандартной функции CALCULATE с функциями логики операций со временем в качестве аргументов фильтра, либо же с применением функций из
Вычисление нарастающих итогов  33
группы TOTAL, например TOTALYTD. Эти функции представляют собой
просто синтаксический сахар (syntactic sugar) для версии с CALCULATE.
Мы покажем, как их использовать, но сами предпочитаем применять
версию с функцией CALCULATE, поскольку в этом случае логика вычисления становится более очевидной, да и в гибкости функции группы
TOTAL уступают. Формулы, в которых будут использоваться эти функции, будут помечены в следующих примерах при помощи цифры (2).
Мы их здесь приводим лишь для того, чтобы показать, что они возвращают те же результаты, что и составная функция CALCULATE.
Нарастающие итоги с начала года
Вычисление нарастающего итога с начала года агрегирует значения,
начиная с 1 января текущего года, как показано на рис. 2.1.
Рис. 2.1. В столбце Sales YTD (simple) выведены значения для всех периодов,
тогда как в Sales YTD и Sales YTD (2) скрыты цифры за будущие даты
В мере для расчета нарастающего итога с начала года можно использовать функцию DATESYTD следующим образом.
Мера в таблице Sales:
Sales YTD (simple) :=
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date] )
)
34
 Глава 2. Стандартные вычисления на основе времени
Функция DATESYTD возвращает набор дат начиная с первого дня текущего года и заканчивая последней датой, видимой в текущем контексте
фильтра. Так что в мере Sales YTD (simple) будут показаны все результаты, включая будущие периоды. В мере Sales YTD мы исключим этот
эффект, поскольку будем учитывать данные только в случае, если мера
ShowValueForDates возвращает TRUE.
Мера в таблице Sales:
Sales YTD :=
IF (
[ShowValueForDates];
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date] )
)
)
Если отчет базируется на финансовом году, не соответствующем календарному, в функцию DATESYTD потребуется передать еще один аргумент, представляющий дату окончания финансового года. Взгляните
на рис. 2.2.
Рис. 2.2. В столбцах Sales Fiscal YTD и Sales Fiscal YTD (2) показан
нарастающий итог с начала года на базе финансового периода
Вычисление нарастающих итогов  35
В мере Sales Fiscal YTD вторым аргументом в функцию DATESYTD
передается строка «6-30», что символизирует окончание финансового
года 30 июня. Это должна быть константа, также называемая литералом, которая накладывает ограничения на таблицу дат, и она не может
быть вычислена динамически.
Мера в таблице Sales:
Sales Fiscal YTD :=
IF (
[ShowValueForDates];
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date]; "6-30" )
)
)
Альтернативная мера с использованием функции TOTALYTD вместо
DATESYTD.
Мера в таблице Sales:
Sales YTD (2) :=
IF (
[ShowValueForDates];
TOTALYTD (
[Sales Amount];
'Date'[Date]
)
)
Мера в таблице Sales:
Sales Fiscal YTD (2) :=
IF (
[ShowValueForDates];
TOTALYTD (
[Sales Amount];
'Date'[Date];
"6-30"
)
)
Нарастающие итоги с начала квартала
Вычисление нарастающего итога с начала квартала агрегирует значения начиная с первого дня текущего квартала, что видно по рис. 2.3.
36
 Глава 2. Стандартные вычисления на основе времени
Рис. 2.3. В столбце Sales QTD показан нарастающий итог с начала квартала,
который на уровне 2009 года не заполнен, поскольку для Q4-2009 нет данных
Нарастающий итог с начала квартала вычисляется при помощи функции DATESQTD, как показано ниже.
Мера в таблице Sales:
Sales QTD :=
IF (
[ShowValueForDates];
CALCULATE (
[Sales Amount];
DATESQTD ( 'Date'[Date] )
)
)
Альтернативой функции DATESQTD является функция TOTALQTD.
Мера в таблице Sales:
Sales QTD (2) :=
IF (
[ShowValueForDates];
TOTALQTD (
[Sales Amount];
'Date'[Date]
)
)
Нарастающие итоги с начала месяца
Вычисление нарастающего итога с начала месяца агрегирует значения начиная с первого дня текущего месяца, как показано на рис. 2.4.
Вычисление нарастающих итогов  37
Рис. 2.4. В столбце Sales MTD показан нарастающий итог
с начала месяца, который на уровнях CY 2009 и Q3-2009 пуст,
поскольку после 15 августа 2009 года нет данных
Нарастающий итог с начала месяца вычисляется при помощи функции DATESMTD.
Мера в таблице Sales:
Sales MTD :=
IF (
[ShowValueForDates];
CALCULATE (
[Sales Amount];
DATESMTD ( 'Date'[Date] )
)
)
Альтернативой функции DATESMTD является функция TOTALMTD.
38
 Глава 2. Стандартные вычисления на основе времени
Мера в таблице Sales:
Sales MTD (2) :=
IF (
[ShowValueForDates];
TOTALMTD (
[Sales Amount];
'Date'[Date]
)
)
Сравнение периодов
Зачастую требуется сравнить показатели за определенный период времени с аналогичным периодом в прошлом году, квартале или месяце.
При этом текущий год может быть не окончен, так что с целью сохранения сопоставимости результатов необходимо принимать во внимание
эквивалентные периоды. Для этого в примерах, показанных в данном
разделе, используется вычисляемый столбец Date[DateWithSales], о чем
подробно можно почитать по адресу https://sql.bi/78171.
Годовое сравнение
Вычисление годового сравнения позволяет сопоставить эквивалентные периоды в текущем и прошлом годах. В нашем примере данные
по продажам содержатся по 15 августа 2009 года. Именно поэтому в
мере Sales PY производится сравнение с 2008 годом и только по транзакциям до 15 августа. На рис. 2.5 видно, что мера Sales Amount за август
2008 года показала 721 560,95, тогда как мера Sales PY за август 2009-го
вернула лишь 296 529,51. Причина этого в том, что в вычислении участвуют только продажи до 15 августа включительно.
В мере Sales PY используется функция DATEADD с одновременной фильтрацией по полю Date[DateWithSales] для гарантии справедливого сравнения двух периодов. При этом сравнение выполняется как
в абсолютном выражении (мера Sales YOY), так и в процентном (мера
Sales YOY %). В обеих мерах производятся расчеты Sales PY только до
15 августа.
Мера в таблице Sales:
Sales PY :=
IF (
[ShowValueForDates];
CALCULATE (
[Sales Amount];
CALCULATETABLE (
Сравнение периодов  39
DATEADD ( 'Date'[Date]; -1; YEAR );
'Date'[DateWithSales] = TRUE
)
)
)
Рис. 2.5. В августе 2009-го мера Sales PY отразила продажи с 1 по 15 августа
2008 года, поскольку после этой даты в 2009-м данные отсутствуют
Мера в таблице Sales:
Sales YOY :=
VAR ValueCurrentPeriod = [Sales Amount]
VAR ValuePreviousPeriod = [Sales PY]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales YOY % :=
DIVIDE (
40
 Глава 2. Стандартные вычисления на основе времени
[Sales YOY];
[Sales PY]
)
Формула меры Sales PY могла быть также записана при помощи функции SAMEPERIODLASTYEAR. Эта функция более простая для восприятия, но не дает никакого преимущества в плане скорости. Причина в
том, что внутренне SAMEPERIODLASTYEAR все равно преобразуется в
функцию DATEADD, которую мы использовали в предыдущих формулах.
Мера в таблице Sales:
Sales PY (2) :=
IF (
[ShowValueForDates];
CALCULATE (
[Sales Amount];
CALCULATETABLE (
SAMEPERIODLASTYEAR ( 'Date'[Date] );
'Date'[DateWithSales] = TRUE
)
)
)
Квартальное сравнение
При квартальном сравнении вычисляется соответствие между текущим периодом и аналогичным в предыдущем квартале. Мы по-прежнему используем набор данных, заполненный до 15 августа 2009 года, что
эквивалентно середине третьего квартала 2009 года. Таким образом, в
мере Sales PQ за август 2009-го (второй месяц третьего квартала) показаны продажи вплоть до 15 мая этого года, что совпадает с первой половиной второго месяца предыдущего квартала. На рис. 2.6 видно, что
мера Sales Amount за май 2009-го показывает значение 1 067 165,23, тогда как мера Sales PQ за август того же года возвращает только 435 306,10,
поскольку в зачет пошли лишь продажи до 15 мая этого года включительно.
Мера Sales PQ использует функцию DATEADD и фильтр по полю
Date[DateWithSales], чтобы обеспечить справедливое сравнение двух
временных диапазонов. Так же как и в годовом сравнении, здесь мы
получаем как абсолютные цифры (мера Sales QOQ), так и относительные (мера Sales QOQ %). Обе эти меры использует мера Sales PQ для
гарантии сравнения сопоставимых периодов.
Сравнение периодов  41
Рис. 2.6. Для августа 2009-го мера Sales PQ показывает продажи с 1 по 15 мая
того же года, поскольку после 15 августа данных нет
Мера в таблице Sales:
Sales PQ :=
IF (
[ShowValueForDates];
CALCULATE (
[Sales Amount];
CALCULATETABLE (
DATEADD ( 'Date'[Date]; -1; QUARTER );
'Date'[DateWithSales] = TRUE
)
)
)
Мера в таблице Sales:
Sales QOQ :=
VAR ValueCurrentPeriod = [Sales Amount]
VAR ValuePreviousPeriod = [Sales PQ]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
42
 Глава 2. Стандартные вычисления на основе времени
Мера в таблице Sales:
Sales QOQ % :=
DIVIDE (
[Sales QOQ];
[Sales PQ]
)
Месячное сравнение
При месячном сравнении вычисляется соответствие между текущим периодом и аналогичным периодом в предыдущем месяце. Данные у нас есть по 15 августа 2009 года. По этой причине в мере Sales
PM для августа 2009-го учитываются только продажи с 1 по 15 июля
того же года. Таким образом, мы получим только часть продаж предшествующего месяца. На рис. 2.7 видно, что мера Sales Amount для июля
2009 года показывает значение 1 068 396,58, тогда как Sales PM для августа возвращает 584 212,78, поскольку продажи здесь учитываются только
до 15 июля 2009-го.
Рис. 2.7. Для августа 2009 года мера Sales PM показывает сумму продаж
с 1 по 15 июля того же года, поскольку после 15 августа у нас информация
не заведена
В мере Sales PM используется функция DATEADD с фильтром по
Date[DateWithSales] для гарантии правильного сравнения соответствующих периодов. Как и раньше, сравнение приводится как в абсолютных
цифрах (мера Sales MOM), так и в процентах (мера Sales MOM %). Обе
меры используют при расчете меру Sales PM.
Сравнение периодов  43
Мера в таблице Sales:
Sales PM :=
IF (
[ShowValueForDates];
CALCULATE (
[Sales Amount];
CALCULATETABLE (
DATEADD ( 'Date'[Date]; -1; MONTH );
'Date'[DateWithSales] = TRUE
)
)
)
Мера в таблице Sales:
Sales MOM :=
VAR ValueCurrentPeriod = [Sales Amount]
VAR ValuePreviousPeriod = [Sales PM]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales MOM % :=
DIVIDE (
[Sales MOM];
[Sales PM]
)
Сравнение периодов
При сравнении периодов происходит автоматический выбор мер,
описанных ранее в данном разделе, на основании текущего выбора в
визуализации. Например, если в визуализации отображаются данные
на уровне месяца, будет произведено месячное сравнение, а если по годам – годовое. Ожидаемый результат показан на рис. 2.8.
44
 Глава 2. Стандартные вычисления на основе времени
Рис. 2.8. В столбце Sales PP показано значение за прошлый месяц
на уровне месяца, за прошлый квартал – на уровне квартала
и за прошлый год – на уровне года
В мерах Sales PP, Sales POP и Sales POP % происходит отсылка к соответствующему месяцу, кварталу или году в зависимости от текущего
выбора в отчете. Определение выбранного уровня производится при
помощи функции ISINSCOPE. Аргументы, передаваемые в эту функцию, – это атрибуты, представленные в строках матрицы, показанной
на рис. 2.8. Формулы приведенных мер следующие.
Мера в таблице Sales:
Sales POP % :=
SWITCH (
TRUE;
ISINSCOPE ( 'Date'[Year Month] ); [Sales MOM %];
ISINSCOPE ( 'Date'[Year Quarter] ); [Sales QOQ %];
ISINSCOPE ( 'Date'[Year] ); [Sales YOY %]
)
Сравнение нарастающих итогов  45
Мера в таблице Sales:
Sales POP :=
SWITCH (
TRUE;
ISINSCOPE ( 'Date'[Year Month] ); [Sales MOM];
ISINSCOPE ( 'Date'[Year Quarter] ); [Sales QOQ];
ISINSCOPE ( 'Date'[Year] ); [Sales YOY]
)
Мера в таблице Sales:
Sales PP :=
SWITCH (
TRUE;
ISINSCOPE ( 'Date'[Year Month] ); [Sales PM];
ISINSCOPE ( 'Date'[Year Quarter] ); [Sales PQ];
ISINSCOPE ( 'Date'[Year] ); [Sales PY]
)
Сравнение нарастающих итогов
Под сравнением нарастающих итогов мы понимаем сопоставление
значения конкретной меры с нарастающим итогом с той же мерой, вычисленной с определенным временным сдвигом. Например, вы можете
сравнить агрегированную меру с начала года с прошлогодним нарастающим итогом.
Все меры из этого раздела принимают в расчет частичные временные интервалы. Поскольку данные в нашей модели доступны только по
15 августа 2009 года, за прошлый год вычисления также будут производиться по эту дату.
Годовое сравнение нарастающих итогов
При годовом сравнении нарастающих итогов выполняется сопоставление значения меры, вычисленной на конкретную дату, с аналогичным показателем за предыдущий год. На рис. 2.9 видно, что при расчете
меры Sales PYTD для 2009 года учитываются лишь продажи до 15 августа предыдущего года включительно. Именно поэтому значение меры
Sales YTD за Q3-2008, равное 7 129 971,53, превратилось в 5 741 502,86 из
меры Sales PYTD за Q3-2009.
В мере Sales PYTD используется функция DATEADD с фильтром по
полю Date[DateWithSales] для обеспечения корректного сравнения. При
46
 Глава 2. Стандартные вычисления на основе времени
этом меры Sales YOYTD и Sales YOYTD % полагаются на ранее описанную меру Sales PYTD также для выполнения правильного сравнения.
Рис. 2.9. Для Q3-2009 мера Sales PYTD показывает продажи с 1 января
по 15 августа 2008 года, поскольку данных после 15 августа 2009-го
в нашем распоряжении нет
Мера в таблице Sales:
Sales PYTD :=
IF (
[ShowValueForDates];
CALCULATE (
[Sales YTD];
CALCULATETABLE (
DATEADD ( 'Date'[Date]; -1; YEAR );
'Date'[DateWithSales] = TRUE
)
)
)
Мера в таблице Sales:
Sales YOYTD :=
VAR ValueCurrentPeriod = [Sales YTD]
VAR ValuePreviousPeriod = [Sales PYTD]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
Сравнение нарастающих итогов  47
)
RETURN
Result
Мера в таблице Sales:
Sales YOYTD % :=
DIVIDE (
[Sales YOYTD];
[Sales PYTD]
)
В мере Sales PYTD фильтр по датам смещается на год назад с помощью функции DATEADD. Также эта функция может быть использована
для смещения на другое количество периодов. Меру Sales PYTD можно
написать и с применением функции SAMEPERIODLASTYEAR, как показано в следующем примере, в котором функция DATEADD вызывается
в неявной форме.
Мера в таблице Sales:
Sales PYTD (2) :=
IF (
[ShowValueForDates];
CALCULATE (
[Sales YTD];
CALCULATETABLE (
SAMEPERIODLASTYEAR ( 'Date'[Date] );
'Date'[DateWithSales] = TRUE
)
)
)
Квартальное сравнение нарастающих итогов
Квартальное сравнение нарастающих итогов позволяет сопоставить
значение нарастающего итога с начала квартала до указанной даты с
тем же показателем по эквивалентной дате в предыдущем квартале. На
рис. 2.10 мы видим, что мера Sales PQ за август 2009 года учитывает
только транзакции вплоть до 15 мая 2008-го, таким образом извлекая
продажи только из первой половины квартала. Как раз поэтому мера
Sales QTD по маю 2009 года показывает цифру 1 746 058,45, тогда как в
Sales PQTD по августу 2009-го мы видим значение меньше – 1 114 199,32.
48
 Глава 2. Стандартные вычисления на основе времени
Рис. 2.10. В мере Sales PQTD за август 2009 года учтены продажи за период
с 1 апреля по 15 мая 2009-го, поскольку данных позже 15 августа 2009-го у нас нет
Мера Sales PQTD использует функцию DATEADD и фильтр по полю
Date[DateWithSales], чтобы сравнение проходило правильно. Меры Sales
QOQTD и Sales QOQTD % в свою очередь полагаются на меру Sales PQTD.
Мера в таблице Sales:
Sales PQTD :=
IF (
[ShowValueForDates];
CALCULATE (
[Sales QTD];
CALCULATETABLE (
DATEADD ( 'Date'[Date]; -1; QUARTER );
'Date'[DateWithSales] = TRUE
)
)
)
Мера в таблице Sales:
Sales QOQTD :=
VAR ValueCurrentPeriod = [Sales QTD]
VAR ValuePreviousPeriod = [Sales PQTD]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Сравнение нарастающих итогов  49
Мера в таблице Sales:
Sales QOQTD % :=
DIVIDE (
[Sales QOQTD];
[Sales PQTD]
)
Месячное сравнение нарастающих итогов
Месячное сравнение нарастающих итогов подразумевает сопоставление значения нарастающего итога с начала месяца до конкретной
даты с тем же показателем по предыдущему месяцу. На рис. 2.11 видно, что мера Sales PMTD за август 2009 года учитывает продажи только по 15 июля 2009-го, таким образом захватывая лишь часть продаж
за предыдущий месяц. Поэтому накопительные месячные продажи за
июль 2009-го (мера Sales MTD), составляющие 1 068 396,58, почти вдвое
превышают значение меры Sales PMTD за август того же года, которая
показывает 584 212,78.
Рис. 2.11. За август 2009 года мера Sales PQTD учитывает данные с 1 по 15 июля
того же года, поскольку нам недоступны данные позже 15 августа 2009-го
50
 Глава 2. Стандартные вычисления на основе времени
Как и раньше, мера Sales PMTD использует функцию DATEADD и
фильтр по полю Date[DateWithSales] для обеспечения сравнимых диапазонов. Кроме того, меры Sales MOMTD и Sales MOMTD % полагаются при
расчетах на меру Sales PMTD.
Мера в таблице Sales:
Sales PMTD :=
IF (
[ShowValueForDates];
CALCULATE (
[Sales MTD];
CALCULATETABLE (
DATEADD ( 'Date'[Date]; -1; MONTH );
'Date'[DateWithSales] = TRUE
)
)
)
Мера в таблице Sales:
Sales MOMTD :=
VAR ValueCurrentPeriod = [Sales MTD]
VAR ValuePreviousPeriod = [Sales PMTD]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales MOMTD % :=
DIVIDE (
[Sales MOMTD];
[Sales PMTD]
)
Сравнение нарастающих итогов с полным
предыдущим периодом
Сравнение нарастающих итогов с полным предыдущим периодом может пригодиться, если вы рассматриваете прошлый период в качестве
Сравнение нарастающих итогов с полным предыдущим периодом  51
ориентира. Когда нарастающий итог за текущий год достигнет отметки
в 100 %, по сравнению с годом предыдущим, можно будет говорить, что
вы вышли на прошлогодние показатели – благо, если за меньшее количество дней.
Сравнение нарастающих итогов за год с полным
прошлым годом
В данном разделе мы поговорим про сравнение нарастающих итогов
с начала года с полным предшествующим годом. На рис. 2.12 видно,
что в ноябре 2008 года мера Sales YTD почти достигла значения меры
Sales Amount за весь предыдущий год. В процентном отношении разницу можно отслеживать посредством меры Sales YTDOPY % – она начнет
демонстрировать положительные значения, когда накопительный показатель за текущий год превысит итоговый прошлогодний. В нашем
случае, как видно на рисунке, это произошло 1 декабря 2008 года.
Рис. 2.12. Мера Sales YTDOPY % начала показывать положительные цифры
с 1 декабря 2008 года, когда значение меры Sales YTD превысило ориентир
в виде Sales Amount за 2007 год
Сравнение нарастающих итогов с полным предыдущим периодом
производится в мерах Sales YTDOPY и Sales YTDOPY %, которые в свою
очередь строятся на основании мер Sales YTD для расчета нарастающего итога с начала года и Sales PYC – для получения итоговых продаж за
весь предшествующий год.
Мера в таблице Sales:
Sales PYC :=
IF (
[ShowValueForDates];
52
 Глава 2. Стандартные вычисления на основе времени
CALCULATE (
[Sales Amount];
PARALLELPERIOD ( 'Date'[Date]; -1; YEAR )
)
)
Мера в таблице Sales:
Sales YTDOPY :=
VAR ValueCurrentPeriod = [Sales YTD]
VAR ValuePreviousPeriod = [Sales PYC]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales YTDOPY % :=
DIVIDE (
[Sales YTDOPY];
[Sales PYC]
)
Мера Sales PYC также может быть написана при помощи функции
PREVIOUSYEAR, поведение которой похоже на поведение функции
PARALLELPERIOD (разница между ними не столь весома для этого примера).
Мера в таблице Sales:
Sales PYC (2) :=
IF (
[ShowValueForDates];
CALCULATE (
[Sales Amount];
PREVIOUSYEAR ( 'Date'[Date] )
)
)
Сравнение нарастающих итогов с полным предыдущим периодом  53
Функцию PREVIOUSYEAR придется использовать в обязательном
порядке, если сравнение выполняется в рамках финансового года, поскольку этой функции можно передать второй аргумент в виде окончания финансового года. На рис. 2.13 показаны меры, оперирующие
финансовыми периодами.
Рис. 2.13. В мере Sales Fiscal YTDOPY % производится сравнение меры Sales YTD
с Sales Amount с предыдущего финансового года
Определения мер, представленных в отчете, приведены ниже. Обратите внимание на второй аргумент при вызове функции PREVIOUSYEAR
в мере Sales Fiscal PYC.
Мера в таблице Sales:
Sales Fiscal PYC :=
IF (
[ShowValueForDates];
CALCULATE (
[Sales Amount];
PREVIOUSYEAR ( 'Date'[Date]; "06-30" )
)
)
Мера в таблице Sales:
Sales Fiscal YTDOPY :=
VAR ValueCurrentPeriod = [Sales Fiscal YTD]
VAR ValuePreviousPeriod = [Sales Fiscal PYC]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
54
 Глава 2. Стандартные вычисления на основе времени
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales Fiscal YTDOPY % :=
DIVIDE (
[Sales Fiscal YTDOPY];
[Sales Fiscal PYC]
)
Сравнение нарастающих итогов за квартал с полным
прошлым кварталом
В этом разделе обратимся к сравнению нарастающих итогов с начала
квартала с полным предшествующим кварталом. На рис. 2.14 показано,
что в мае 2008 года мера Sales QTD превысила общие продажи (мера
Sales Amount) за Q1-2008. Мера Sales QTDOPQ % обеспечивает сравнение
нарастающего итога с начала квартала с общими продажами за предыдущий квартал. При положительных значениях фиксируется рост показателя, и в нашем случае он был отмечен начиная с мая 2008 года.
Рис. 2.14. Мера Sales QTDOPQ % начала показывать положительные проценты
начиная с мая 2008 года, когда значение меры Sales QTD перевалило за отметку
меры Sales Amount по Q1-2008 (первому кварталу 2008 года)
Сравнение нарастающих итогов с начала квартала с полным предшествующим кварталом выполняется в мерах Sales QTDOPQ и Sales
QTDOPQ %, при этом сами они опираются на меры Sales QTD для расче-
Сравнение нарастающих итогов с полным предыдущим периодом  55
та нарастающего итога с начала квартала и Sales PQC – для получения
итоговых продаж за предыдущий квартал.
Мера в таблице Sales:
Sales PQC :=
IF (
[ShowValueForDates] && HASONEVALUE ( 'Date'[Year Quarter] );
CALCULATE (
[Sales Amount];
PARALLELPERIOD ( 'Date'[Date]; -1; QUARTER )
)
)
Мера в таблице Sales:
Sales QTDOPQ :=
VAR ValueCurrentPeriod = [Sales QTD]
VAR ValuePreviousPeriod = [Sales PQC]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales QTDOPQ % :=
DIVIDE (
[Sales QTDOPQ];
[Sales PQC]
)
Мера Sales PQC может быть написана и с использованием функции
PREVIOUSQUARTER, если она не используется на уровне годов для более
чем одного квартала.
Мера в таблице Sales:
Sales PQC (2) :=
IF (
[ShowValueForDates] && HASONEVALUE ( 'Date'[Year Quarter] );
CALCULATE (
56
 Глава 2. Стандартные вычисления на основе времени
[Sales Amount];
PREVIOUSQUARTER ( 'Date'[Date] )
)
)
Сравнение нарастающих итогов за месяц с полным
прошлым месяцем
Здесь мы рассмотрим вариант сравнения нарастающих итогов с начала месяца с полным предшествующим месяцем. На рис. 2.15 видно,
что в апреле 2008 года мера Sales MTD превысила итоговую сумму Sales
Amount за март 2008-го. Мера Sales MTDOPM % сравнивает нарастающий итог с начала месяца с общими продажами за предыдущий месяц.
Когда процент становится положительным, мы фиксируем преодоление ориентира. В нашем случае это случилось 19 апреля 2008 года.
Рис. 2.15. Мера Sales MTDOPM % демонстрирует положительные значения
начиная с 19 апреля 2008 года, когда значение Sales MTD впервые превысило
Sales Amount за март 2008-го
Сравнение нарастающих итогов с полным предыдущим периодом  57
Сравнение нарастающего итога с начала месяца с полным предшествующим месяцем происходит в мерах Sales MTDOPM % и Sales
MTDOPM, которые зависят от мер Sales MTD и Sales PMC.
Мера в таблице Sales:
Sales PMC :=
IF (
[ShowValueForDates] && HASONEVALUE ( 'Date'[Year Month] );
CALCULATE (
[Sales Amount];
PARALLELPERIOD ( 'Date'[Date]; -1; MONTH )
)
)
Мера в таблице Sales:
Sales MTDOPM :=
VAR ValueCurrentPeriod = [Sales MTD]
VAR ValuePreviousPeriod = [Sales PMC]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales MTDOPM % :=
DIVIDE (
[Sales MTDOPM];
[Sales PMC]
)
Мера Sales PMC может быть написана с использованием функции
PREVIOUSMONTH, если она не используется на уровне годов или кварталов для более чем одного месяца.
Sales PMC (2) :=
IF (
[ShowValueForDates] && HASONEVALUE ( 'Date'[Year Month] );
CALCULATE (
[Sales Amount];
58
 Глава 2. Стандартные вычисления на основе времени
PREVIOUSMONTH ( 'Date'[Date] )
)
)
Вычисление скользящей годовой суммы
Распространенным способом агрегировать данные за несколько месяцев является расчет скользящей годовой суммы (moving annual total)
вместо нарастающих итогов с начала года. Скользящая годовая сумма
включает в себя данные за последние 12 месяцев. К примеру, если мы
рассчитываем такую сумму для марта 2008 года, то должны учитывать
данные с апреля 2007-го по март 2008-го.
Скользящая годовая сумма
На рис. 2.16 показана мера Sales MAT, вычисляющая скользящую годовую сумму.
Рис. 2.16. В мере Sales MAT за март 2008 года агрегируются
значения Sales Amount с апреля 2007-го по март 2008-го
Вычисление скользящей годовой суммы  59
Мера, рассчитывающая скользящую годовую сумму, использует функцию DATESINPERIOD для выбора предшествующего года.
Мера в таблице Sales:
Sales MAT :=
IF (
[ShowValueForDates];
CALCULATE (
[Sales Amount];
DATESINPERIOD (
'Date'[Date];
MAX ( 'Date'[Date] );
-1;
YEAR
)
)
)
Функция DATESINPERIOD возвращает набор дат, включающий дату,
переданную вторым аргументом, с временным сдвигом, определенным
в третьем и четвертом аргументах. В случае с Sales MAT мера возвращает весь предыдущий год относительно последней даты, доступной в
контексте фильтра. Того же результата можно было добиться, передав
функции в качестве третьего и четвертого аргументов –12 и MONTH
соответственно.
Сравнение скользящих годовых сумм
Сравнение скользящих годовых сумм реализовано при помощи мер
Sales PYMAT, Sales MATG и Sales MATG %, которые в свою очередь основаны на мере Sales MAT. При этом мера Sales MAT показывает корректное значение только через год после первой продажи (т. е. после
того, как наберется годовой массив данных), а если текущий год неполный, цифры могут быть неожиданными. Например, для 2009 года мера
Sales PYMAT на рис. 2.17 показывает значение 9 927 582,99, что соответствует агрегации меры Sales Amount за 2008 год. Если сравнивать полученный результат с продажами за 2009 год, мы получим сопоставление
за период меньше восьми месяцев, поскольку за 2009 год у нас есть данные только по 15 августа, а за 2008-й – по всему году. Также обратите
внимание, что значения в столбце Sales MATG % начинаются с очень
больших величин и стабилизируются только спустя год. Это происходит
из-за отсутствия данных о продажах за предыдущий год. Такое поведение характерно для подобного вида вычисления: скользящая годовая
Powered by TCPDF (www.tcpdf.org)
60
 Глава 2. Стандартные вычисления на основе времени
сумма обычно рассчитывается на уровне гранулярности по месяцам
или дням, чтобы показать тренды на диаграмме.
Рис. 2.17. Мера Sales MATG % показывает отношение между Sales MAT
и Sales PYMAT в процентах
Определения представленных мер показаны ниже.
Мера в таблице Sales:
Sales PYMAT :=
IF (
[ShowValueForDates];
CALCULATE (
[Sales MAT];
DATEADD ( 'Date'[Date]; -1; YEAR )
)
)
Скользящее среднее  61
Мера в таблице Sales:
Sales MATG :=
VAR ValueCurrentPeriod = [Sales MAT]
VAR ValuePreviousPeriod = [Sales PYMAT]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales MATG % :=
DIVIDE (
[Sales MATG];
[Sales PYMAT]
)
Мера Sales PYMAT может быть реализована при помощи функции
SAMEPERIODLASTYEAR, как в представленном ниже примере, где функция DATEADD вызывается в неявной форме.
Мера в таблице Sales:
Sales PYMAT (2) :=
IF (
[ShowValueForDates];
CALCULATE (
[Sales MAT];
SAMEPERIODLASTYEAR ( 'Date'[Date] )
)
)
Скользящее среднее
Скользящее среднее (moving average) традиционно используется для
отображения тенденций на диаграммах. На рис. 2.18 демонстрируется показатель скользящего среднего для меры Sales Amount за последние 30 дней (Sales AVG 30D), три месяца (Sales AVG 3M) и один год (Sales
AVG 1Y).
62
 Глава 2. Стандартные вычисления на основе времени
Рис. 2.18. Меры Sales AVG 30D, Sales AVG 3M и Sales AVG 1Y демонстрируют
скользящее среднее за 30 дней, три месяца и год соответственно
Скользящее среднее за 30 дней
В мере Sales AVG 30D рассчитывается скользящее среднее за 30 дней
путем прохода по последним 30 дням, полученным при помощи функции DATESINPERIOD.
Мера в таблице Sales:
Sales AVG 30D :=
VAR Period30D =
CALCULATETABLE (
DATESINPERIOD (
'Date'[Date];
MAX ( 'Date'[Date] );
-30;
DAY
);
'Date'[DateWithSales] = TRUE
)
VAR FirstDayWithData =
CALCULATE (
MIN ( Sales[Order Date] );
REMOVEFILTERS ()
)
VAR FirstDayInPeriod =
MINX ( Period 30D; 'Date'[Date] )
VAR Result =
IF (
FirstDayWithData <= FirstDayInPeriod;
AVERAGEX (
Скользящее среднее  63
Period 30D;
[Sales Amount]
)
)
RETURN
Result
Это очень гибкий шаблон. Но для обычного аддитивного вычисления
переменная Result может быть реализована посредством другой, более
быстрой формулы:
VAR Result =
IF (
FirstDayWithData <= FirstDayInPeriod;
CALCULATE (
DIVIDE (
[Sales Amount];
DISTINCTCOUNT ( Sales[Order Date] )
);
Period30D
)
)
Скользящее среднее за три месяца
В мере Sales AVG 3M рассчитывается скользящее среднее за три месяца при помощи итераций по последним трем месяцам, полученным
посредством функции DATESINPERIOD.
Мера в таблице Sales:
Sales AVG 3M :=
VAR Period3M =
CALCULATETABLE (
DATESINPERIOD (
'Date'[Date];
MAX ( 'Date'[Date] );
-3;
MONTH
);
'Date'[DateWithSales] = TRUE
)
VAR FirstDayWithData =
CALCULATE (
MIN ( Sales[Order Date] );
REMOVEFILTERS ()
)
64
 Глава 2. Стандартные вычисления на основе времени
VAR FirstDayInPeriod =
MINX ( Period3M; 'Date'[Date] )
VAR Result =
IF (
FirstDayWithData <= FirstDayInPeriod;
AVERAGEX (
Period3M;
[Sales Amount]
)
)
RETURN
Result
Для создания простой аддитивной меры можно использовать шаблон, базирующийся на функции DIVIDE, прототип которого показан в
разделе расчета скользящего среднего за 30 дней.
Скользящее среднее за год
Мера Sales AVG 1Y демонстрирует скользящее среднее за последний
год путем перебора соответствующих дат, полученных при помощи
функции DATESINPERIOD.
Мера в таблице Sales:
Sales AVG 1Y :=
VAR Period1Y =
CALCULATETABLE (
DATESINPERIOD (
'Date'[Date];
MAX ( 'Date'[Date] );
-1;
YEAR
);
'Date'[DateWithSales] = TRUE
)
VAR FirstDayWithData =
CALCULATE (
MIN ( Sales[Order Date] );
REMOVEFILTERS ()
)
VAR FirstDayInPeriod =
MINX ( Period1Y; 'Date'[Date] )
VAR Result =
IF (
FirstDayWithData <= FirstDayInPeriod;
AVERAGEX (
Period1Y;
Фильтрация других атрибутов даты  65
[Sales Amount]
)
)
RETURN
Result
Так же как и раньше, переменная Result может быть рассчитана при
помощи функции DIVIDE.
Фильтрация других атрибутов даты
Помечая таблицу Date как таблицу дат в модели данных, вы тем самым
указываете DAX автоматически удалять любые фильтры с этой таблицы каждый раз, когда функция CALCULATE фильтрует столбец с датой.
Такое поведение заложено в движке по умолчанию, и его цель – облегчить написание формул с использованием функций логики операций
со временем. Если бы DAX не удалял эти фильтры, вы вынуждены были
бы добавлять функцию REMOVEFILTERS каждый раз, когда используете
стандартные функции логики операций со временем, что было бы совсем неудобно в плане разработки.
Но автоматическое удаление фильтров из таблицы Date может обернуться проблемами в определенных отчетах. Например, если в отчете
рассчитывается нарастающий итог с начала года со срезом по дням недели, результат, который вы получите путем применения только функции DATESYTD, будет неверным. На рис. 2.19 представлен результат
вычисления меры Sales YTD по дням недели, и мы видим, что по каждому из них цифра либо равна, либо чуть меньше итогового значения
по строке, в которой агрегированы все дни недели.
Рис. 2.19. Срез меры Sales YTD по дням недели приводит к некорректным
результатам расчета
66
 Глава 2. Стандартные вычисления на основе времени
Причина получения неправильных расчетов состоит в том, что функция DATESYTD применяет фильтр к столбцу Date[Date]. Поскольку таблица Date помечена как таблица дат, DAX автоматически применяет
модификатор REMOVEFILTERS('Date') к функции CALCULATE, в аргументах фильтра которой присутствует функция DATESYTD, тем самым
удаляя фильтр по дням недели. В результате мы получаем нарастающий
итог с начала года без учета фильтра по дням недели. Этот фильтр влияет только на последний день периода, указанного в строке, будь то год
или квартал. Правильный результат построения отчета, показанный на
рис. 2.20, требует другого подхода.
Рис. 2.20. Срез меры Sales YTD по дням недели с корректными результатами
Существует два варианта получения правильных результатов: либо
восстанавливать фильтр по дням недели в функции CALCULATE, либо
обновлять модель данных.
Первый способ требует добавления в функцию CALCULATE аргумента
фильтра VALUES (Date[Day of Week]) только по отфильтрованным столбцам, как показано ниже.
Мера в таблице Sales:
Sales YTD (day of week) :=
IF (
[ShowValueForDates];
IF (
ISFILTERED ( 'Date'[Day of Week] );
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date] );
VALUES ( 'Date'[Day of Week] )
);
Фильтрация других атрибутов даты  67
CALCULATE (
[Sales Amount];
DATESYTD ( 'Date'[Date] )
)
)
)
Первое решение работает неплохо, но у него есть один существенный
недостаток, состоящий в том, что здесь производится два разных вычисления в зависимости от того, отфильтрован или нет столбец 'Date'[Day of
Week]. В больших моделях данных это может вести к ощутимым проблемам с производительностью.
Существует еще одно решение данного сценария, предполагающее
изменение структуры модели данных. Вместо использования таблицы
Date для выбора дней недели можно создать отдельную таблицу с таким
полем, которая будет фильтровать таблицу Sales, не будучи связанной
с таблицей Date. Таким образом, автоматическое удаление фильтров в
таблице Date не затронет существующий фильтр по дням недели. Такая
таблица с именем Day of Week, например, может быть создана как вычисляемая при помощи следующего кода.
Вычисляемая таблица:
Day of Week =
SELECTCOLUMNS (
'Date';
"Date"; 'Date'[Date];
"Day of Week"; 'Date'[Day of Week]
)
Таблица Day of week должна быть связана с таблицей Sales по полям
Date и [Order Date] соответственно. Пример получившейся модели данных представлен на рис. 2.21.
Заметьте, что мы создали таблицу Day of Week с использованием
всех без исключения дат из таблицы Date, чтобы затем объединить
ее с существующим столбцом Sales[Order Date] в таблице продаж.
Можно создать таблицу и всего с семью значениями (с понедельника
по воскресенье), но этот способ потребует создания дополнительного
столбца в таблице Sales, а значит, возрастет и объем памяти, требуемый
для хранения модели данных.
Осуществление срезов по дням недели в созданной нами таблице Day
of Week допускает выполнение любых вычислений, связанных с логикой
операций со временем, и любую фильтрацию по таблице Day of Week,
68
 Глава 2. Стандартные вычисления на основе времени
поскольку разные фильтры (по дате и дню недели) происходят из двух
разных таблиц.
Рис. 2.21. Новая таблица Day of Week связана с тем же полем Order Date
таблицы Sales, с которым объединена таблица Date
Дополнительная таблица может объединить в себе любой набор атрибутов, удовлетворяющих требованиям вашей конкретной бизнес-логики. Мы показали пример с днями недели, но вы можете использовать и
другие атрибуты, такие как рабочие дни, праздники и сезоны, при условии, что они зависят от поля Order Date.
Глава
3
Вычисления
на основе месяца
https://t.me/it_boooks
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Данный шаблон описывает рассмотренные нами выше вычисления,
такие как нарастающий итог с начала года, сравнение с аналогичным
периодом в предыдущем году и другие с использованием месячной
гранулярности (granularity). В этих шаблонах не будут использоваться
встроенные в DAX функции логики операций со временем.
Вы можете использовать предложенные здесь шаблоны на основе месяца, если в вашей компании анализ продаж производится по месяцам,
кварталам или годам, без более подробной детализации. Иными словами, показанные здесь формулы просто перестанут работать, если вы
попробуете опуститься в своем анализе до уровня дней. А поскольку в
представленном шаблоне не используются стандартные календарные
даты для связи с продажами, вы спокойно можете применять собственные финансовые календари, состоящие из 13 месяцев, и любые другие,
не соответствующие принятым стандартам, при условии, что максимальным уровнем детализации отчетов будет месяц, а не неделя или
день. Кроме того, отчеты недопустимо будет фильтровать или группировать по неделям, дням недели или рабочим дням. Несмотря на то что
в стандартной таблице дат гранулярность может находиться на уровне
дня, эти столбцы не должны присутствовать в таблице, поскольку они
несовместимы с формулами в рассматриваемом шаблоне.
Шаблон вычислений на основе месяца может оказаться полезным
при создании простых и быстрых формул в случаях, когда детализация
до дня не требуется. Кроме того, это единственный шаблон, который
70
 Глава 3. Вычисления на основе месяца
допустимо использовать в ситуации с созданием дополнительных месяцев – например, 13-го виртуального месяца в финансовом году, в котором традиционно производятся бухгалтерские корректировки в конце
года. Если вы в своей системе применяете вычисления на основе месяца, но вам понадобилась гранулярность до дня, рассмотрите вариант
использования пользовательского шаблона, который будет рассмотрен
далее в этой книге. Если же вам необходимо выполнять анализ данных
исключительно по неделям, вы можете воспользоваться соответствующим шаблоном, о котором мы также поговорим далее.
Введение в вычисления на основе месяца
Вычисления, связанные с логикой операций со временем, представленные в данном разделе, обеспечивают получение результата путем
изменения контекста фильтра в таблице Date. При этом соответствующие формулы являются оптимизированными именно для применения
фильтров на уровне гранулярности по месяцам, что позволяет повысить
их производительность вне зависимости от кратности таблицы Date.
К примеру, во многих вычислениях контекст фильтра меняется именно
на уровне месяца, а не на уровне дня. Эта техника позволяет существенно снизить затраты на вычисление нового фильтра и его применение к
контексту фильтра. Подобная оптимизация особенно полезна, когда используется тип подключения DirectQuery, но и с загруженной в память
моделью данных будет ощущаться прирост быстродействия.
Шаблон вычислений на основе месяца не задействует стандартные
функции логики операций со временем, поэтому на таблицу Date не накладываются столь серьезные ограничения, как в случае с использованием этих функций. При этом формулы получаются идентичными, будь
у вас одна строка для каждого месяца или для каждого дня.
Если в таблице Date присутствует столбец Date, пометить ее как таблицу дат можно, но в этом нет особой необходимости. Формулы в рассматриваемом шаблоне не полагаются на автоматическое применение
функции REMOVEFILTERS к таблице Date, когда столбец Date отфильтрован. Вместо этого во всех формулах функция REMOVEFILTERS указана явно, чтобы избавиться от существующих фильтров, заменив их
на минимально необходимое количество фильтров для обеспечения
желаемого результата.
Создание таблицы дат
Таблицу дат для использования в шаблоне на основе месяца можно
создать множеством способов. Требования шаблона таковы, что нам
необходимо иметь отдельные столбцы, связанные с месяцами и любы-
Введение в вычисления на основе месяца  71
ми агрегациями по месяцам, такими как кварталы и годы. При этом
месяцы могут отличаться от тех, которые присутствуют в стандартном
григорианском календаре, как в случае с дополнительным 13-м месяцем. Сопроводительные файлы для этого шаблона включают в себя четыре различных сценария относительно таблицы Date.
1. По одной строке на каждую дату, основываясь на григорианском
календаре, со столбцом Date в качестве первичного ключа. В данном случае все очень похоже на стандартные вычисления, за исключением того, что формулы должны работать быстрее.
2. По одной строке на каждый месяц, основываясь на григорианском календаре с полем Year Month Number в качестве первичного
ключа. Этот сценарий лучше предыдущего из-за меньшего размера таблицы дат.
3. По одной строке на каждый месяц из бухгалтерского календаря –
с дополнительным 13-м месяцем между последним месяцем одного финансового года и первым – другого. Быстродействие этого
сценария близко ко второму.
4. По одной строке на каждый месяц из бухгалтерского календаря –
с 13-м месяцем, размещенным на месте последнего финансового
месяца григорианского календаря. Быстродействие и поведение
этого сценария очень близко к третьему варианту.
Если вы решили остановиться на сценарии, предполагающем наличие одной строки для каждого месяца, вы не должны выбирать дату в
качестве поля для связки таблиц Sales и Date, если не используете особые даты для идентификации каждого месяца. Например, 1 декабря –
для декабря, а 31 декабря – для 13-го месяца.
Для данного шаблона необходимо, чтобы таблица Date включала в
себя все без исключения месяцы из диапазона между первой и последней записями в таблице Sales. Таким образом, если последняя продажа была произведена в августе 2009 года, то именно этот месяц должен
быть последним в таблице Date. Это требование отличается от требования к шаблону со стандартными функциями логики операций со временем, где все месяцы года должны быть представлены в таблице Date для
обеспечения корректного поведения механизма.
Если у вас уже есть готовая таблица Date, вы можете просто импортировать ее и отображать только нужные столбцы, а колонки с гранулярностью по дням и неделям скрыть. Если же у вас таблицы дат еще
нет, вы легко можете создать ее в виде вычисляемой таблицы в DAX.
В качестве примера представим выражение DAX, описывающее таблицу Date, используемую в трех из четырех перечисленных сценариев.
72
 Глава 3. Вычисления на основе месяца
Вычисляемая таблица:
Date =
VAR FirstFiscalMonth = 3
VAR MonthsInYear = 12
-- Первый месяц финансового года
-- Должна быть равна 12 для GranularityByDate
-- Может быть другой для GranularityByMonth
VAR CalendarFirstDate = MIN ( Sales[Order Date] )
VAR CalendarLastDate = MAX ( Sales[Order Date] )
VAR CalendarFirstYear = YEAR ( CalendarFirstDate )
VAR CalendarFirstMonth = MONTH ( CalendarFirstDate )
VAR CalendarLastYear = YEAR ( CalendarLastDate )
VAR CalendarLastMonth = MONTH ( CalendarLastDate )
-------------------------- Внутренние вычисления
------------------------VAR GranularityByDate =
ADDCOLUMNS (
CALENDAR (
DATE ( CalendarFirstYear; CalendarFirstMonth; 1 );
EOMONTH (
DATE ( CalendarLastYear; CalendarLastMonth; 1 );
0
)
);
"Year Month Number"; YEAR ( [Date] ) * MonthsInYear
+ MONTH ( [Date] ) - 1
)
VAR GranularityByMonth =
SELECTCOLUMNS (
GENERATESERIES (
CalendarFirstYear * MonthsInYear + CalendarFirstMonth - 1
- (MonthsInYear - 12) * (CalendarFirstMonth < FirstFiscalMonth);
CalendarLastYear * MonthsInYear + CalendarLastMonth - 1
- (MonthsInYear - 12) * (CalendarLastMonth < FirstFiscalMonth);
1
);
"Year Month Number"; [Value]
)
RETURN GENERATE (
GranularityByDate;
-- Используем GranularityByMonth для строк по месяцам
VAR YearMonthNumber = [Year Month Number]
VAR FiscalMonthNumber =
MOD (
YearMonthNumber + 1
* (FirstFiscalMonth > 1)
* (MonthsInYear + 1 - FirstFiscalMonth);
MonthsInYear
) + 1
VAR FiscalYearNumber =
QUOTIENT (
YearMonthNumber + 1
Введение в вычисления на основе месяца  73
* (FirstFiscalMonth > 1)
* (MonthsInYear + 1 - FirstFiscalMonth);
MonthsInYear
)
VAR OffsetFiscalMonthNumber = MonthsInYear + 1 - (MonthsInYear - 12)
VAR MonthNumber =
IF (
FiscalMonthNumber <= 12 && FirstFiscalMonth > 1;
FiscalMonthNumber + FirstFiscalMonth
- IF (
FiscalMonthNumber > (OffsetFiscalMonthNumber - FirstFiscalMonth);
OffsetFiscalMonthNumber;
1
);
FiscalMonthNumber
)
VAR YearNumber = FiscalYearNumber - 1 * (MonthNumber > FiscalMonthNumber)
VAR YearMonthKey = YearNumber * 100 + MonthNumber
VAR
VAR
VAR
VAR
MonthDate = DATE ( YearNumber; MonthNumber; 1 )
FiscalQuarterNumber = MIN ( ROUNDUP ( FiscalMonthNumber / 3; 0 ); 4 )
FiscalYearQuarterNumber = FiscalYearNumber * 4 + FiscalQuarterNumber - 1
FiscalMonthInQuarterNumber =
MOD ( FiscalMonthNumber - 1; 3 ) + 1 + 3 * (MonthNumber > 12)
VAR MonthInQuarterNumber = MOD ( MonthNumber - 1; 3 ) + 1 + 3 * (MonthNumber > 12)
VAR QuarterNumber = MIN ( ROUNDUP ( MonthNumber / 3; 0 ); 4 )
VAR YearQuarterNumber = YearNumber * 4 + QuarterNumber - 1
RETURN ROW (
"Year Month Key"; YearMonthKey;
"Year"; YearNumber;
"Year Quarter"; FORMAT ( QuarterNumber; "\Q0" )
& "-" & FORMAT ( YearNumber; "0000" );
"Year Quarter Number"; YearQuarterNumber;
"Quarter"; FORMAT ( QuarterNumber; "\Q0" );
"Year Month"; IF (
MonthNumber > 12;
FORMAT ( MonthNumber; "\M00" ) & FORMAT ( YearNumber; " 0000" );
FORMAT ( MonthDate; "mmm yyyy" )
);
"Month"; IF (
MonthNumber > 12;
FORMAT ( MonthNumber; "\M00" );
FORMAT ( MonthDate; "mmm" )
);
"Month Number"; MonthNumber;
"Month In Quarter Number"; MonthInQuarterNumber;
"Fiscal Year"; FORMAT ( FiscalYearNumber; "\F\Y 0000" );
"Fiscal Year Number"; FiscalYearNumber;
"Fiscal Year Quarter"; FORMAT ( FiscalQuarterNumber; "\F\Q0" ) & "-"
& FORMAT ( FiscalYearNumber; "0000" );
"Fiscal Year Quarter Number"; FiscalYearQuarterNumber;
74
 Глава 3. Вычисления на основе месяца
"Fiscal Quarter"; FORMAT ( FiscalQuarterNumber; "\F\Q0" );
"Fiscal Month"; IF (
MonthNumber > 12;
FORMAT ( MonthNumber; "\M00" );
FORMAT ( MonthDate; "mmm" )
);
"Fiscal Month Number"; FiscalMonthNumber;
"Fiscal Month In Quarter Number"; FiscalMonthInQuarterNumber
)
)
Вы можете настроить первые две переменные, чтобы таблица Date
отвечала вашим требованиям. Переменная FirstFiscalMonth отвечает за
первый финансовый месяц года, а MonthsInYear – за количество месяцев в финансовом году. Также настройке поддается первый аргумент
функции GENERATE. Он может быть:
• GranularityByMonth – в этом случае в таблице дат будет по одной
строке на каждый месяц;
• GranularityByDate – в таблице дат будет по одной строке на каждый день.
Аргумент GranularityByDate используется в первом сценарии (со строкой на каждый день), а GranularityByMonth – в трех оставшихся (с гранулярностью по месяцам). В столбце Year Month содержится одно значение для каждого месяца, при этом описание месяца одинаковое как
для финансового, так и для григорианского календаря. В четвертом сценарии у нас есть несколько дополнительных столбцов для обеспечения
различий между Month и Fiscal Month. Это требуется для альтернативного подхода к 13-му месяцу в зависимости от иерархии.
Чтобы получить корректную визуализацию, календарные столбцы
должны быть сконфигурированы в модели данных так, как показано
ниже. Для каждого столбца мы демонстрирует тип данных и пример
значения с допуском о том, что финансовый год начинается с марта и
насчитывает 12 месяцев.
• Date: Date, Hidden (8/14/2007), используется только для первого
сценария;
• Year Month Key: Whole Number, Hidden (200708), используется для
определения связей;
• Year Month: Text (Aug 2007);
• Year Quarter: Text (Q3-2007);
• Year Quarter Number: Whole Number, Hidden (8030);
• Quarter: Text (Q3);
• Year Month Number: Whole Number, Hidden (24091);
Введение в вычисления на основе месяца  75
•
•
•
•
•
•
•
•
•
•
•
Month: Text (Aug);
Month Number: Whole Number, Hidden (8);
Month In Quarter Number: Whole Number, Hidden (2);
Fiscal Month: Text (Aug);
Fiscal Month Number: Whole Number, Hidden (6);
Fiscal Month in Quarter Number: Whole Number, Hidden (3);
Fiscal Year: Text (FY 2008);
Fiscal Year Number: Whole Number, Hidden (2008);
Fiscal Year Quarter: Text (FQ2-2008);
Fiscal Year Quarter Number: Whole Number, Hidden (8033);
Fiscal Quarter: Text (FQ2).
Таблица Date в этом шаблоне обладает следующими четырьмя иерархиями:
• Fiscal Year-Quarter: Year (Fiscal Year), Quarter (Fiscal Year Quarter),
Month (Year Month);
• Fiscal Year-Month: Year (Fiscal Year), Month (Year Month);
• Year-Quarter: Year (Year), Quarter (Year Quarter), Month (Year Month);
• Year-Month: Year (Year), Month (Year Month).
Некоторые столбцы служат единственной цели упрощения формул,
используемых в пользовательских вычислениях логики операций со
временем. Столбец Year Month Key используется только для связи с таблицей Sales в целочисленном формате YYYYMM. Числовой формат для
месяца – распространенная идея хранения информации в случае гранулярности по месяцам.
В таблице Date содержатся только месяцы, присутствующие в фактических данных. В нашем примере таблица дат будет заполнена месяцами с марта 2007 года по август 2009-го. В данном шаблоне нет необходимости хранить в таблице дат все месяцы одного года. Поэтому не
обязательно иметь дополнительный столбец вроде DateWithSales, который мы использовали в шаблоне со стандартными функциями логики
операций со временем.
Соглашение об именованиях
В табл. 3.1 приведены наименования, принятые для обозначения
вычислений, основанных на логике операций со временем. В столбцах
отображена информация о том:
• сдвигается ли вычисление во времени, например для определения того же периода в прошлом году;
• выполняется ли агрегация при вычислении, например как в случае с нарастающим итогом с начала года;
 Глава 3. Вычисления на основе месяца
76
• производится ли сравнение двух временных периодов: допустим,
текущего года с прошлым.
Таблица 3.1. Принятые обозначения вычислений, основанных на логике операций
со временем
Сокращение
Описание
Сдвиг Агрегация
Сравнение
YTD
Year-to-date
(нарастающий итог с начала года)
X
QTD
Quarter-to-date
(нарастающий итог с начала квартала)
X
MAT
Moving annual total
(скользящая годовая сумма)
X
PY
Previous year
(предыдущий год)
X
PQ
Previous quarter
(предыдущий квартал)
X
PM
Previous month
(предыдущий месяц)
X
PYC
Previous year complete
(предыдущий год полный)
X
PQC
Previous quarter complete
(предыдущий квартал полный)
X
PMC
Previous month complete
(предыдущий месяц полный)
X
PP
Previous period
(прошлый период) (автоматически
выбираются год, квартал или месяц)
X
PYMAT
Previous year moving annual total
(скользящая годовая сумма за прошлый год)
X
YOY
Year-over-year
(годовое сравнение)
X
QOQ
Quarter-over-quarter
(квартальное сравнение)
X
MOM
Month-over-month
(месячное сравнение)
X
MATG
Moving annual total growth
(скользящая годовая сумма, прирост)
POP
Period-over-period
(сравнение периодов) (автоматически
выбираются год, квартал или месяц)
PYTD
Previous year-to-date
(прошлогодний нарастающий итог с начала
года)
X
X
X
X
X
X
X
Вычисление нарастающих итогов  77
Сокращение
PQTD
Описание
Сдвиг Агрегация
Сравнение
Previous quarter-to-date
(прошлогодний нарастающий итог с начала
квартала)
X
X
YOYTD
Year-over-year-to-date
(годовое сравнение нарастающим итогом)
X
X
X
QOQTD
Quarter-over-quarter-to-date
(квартальное сравнение нарастающим
итогом)
X
X
X
Year- to-date-over-previous-year
(нарастающий итог с начала года
по сравнению с предыдущим годом)
X
X
X
Quarter- to-date-over-previous-quarter
(нарастающий итог с начала квартала
по сравнению с предыдущим кварталом)
X
X
X
YTDOPY
QTDOPQ
Вычисление нарастающих итогов
Вычисление нарастающего итога с начала года, квартала или месяца
изменяет контекст фильтра таблицы Date, чтобы в ней присутствовали
даты с начала периода до текущего выбранного месяца.
Нарастающие итоги с начала года
Вычисление нарастающего итога с начала года агрегирует значения
начиная с первого дня года, как показано на рис. 3.1.
Агрегация нарастающего итога с начала года фильтрует все месяцы,
принадлежащие году из последней даты, доступной в контексте фильтра, при условии, что номера выбранных месяцев меньше или равны
номеру месяца этой последней даты.
Мера в таблице Sales:
Sales YTD :=
VAR LastMonthAvailable = MAX ( 'Date'[Year Month Number] )
VAR LastYearAvailable = MAX ( 'Date'[Year] )
VAR Result =
CALCULATE (
[Sales Amount];
REMOVEFILTERS ( 'Date' );
'Date'[Year Month Number] <= LastMonthAvailable;
'Date'[Year] = LastYearAvailable
)
RETURN
Result
78
 Глава 3. Вычисления на основе месяца
Рис. 3.1. В столбце Sales YTD показаны агрегированные значения с начала года,
а в столбце Sales Fiscal YTD – с начала финансового года
Если в отчете используется иерархия, основанная на финансовом году,
то мера должна фильтровать соответствующие столбцы с приставкой
«Fiscal» перед сокращением, определяющим вычисление. Например,
мера Sales Fiscal YTD использует поле Fiscal Year Number вместо Year, при
этом в ней не меняется фильтр по столбцу Year Month Number, поскольку
он идентичен для финансовой и календарной иерархии.
Мера в таблице Sales:
Sales Fiscal YTD :=
VAR LastMonthAvailable = MAX ( 'Date'[Year Month Number] )
VAR LastFiscalYearAvailable = MAX ( 'Date'[Fiscal Year Number] )
VAR Result =
CALCULATE (
[Sales Amount];
REMOVEFILTERS ( 'Date' );
'Date'[Year Month Number] <= LastMonthAvailable;
'Date'[Fiscal Year Number] = LastFiscalYearAvailable
)
RETURN
Result
Нарастающие итоги с начала квартала
Вычисление нарастающего итога с начала квартала агрегирует значения начиная с первого месяца финансового квартала, что показано
на рис. 3.2.
Сравнение периодов  79
Рис. 3.2. В столбце Sales QTD рассчитывается нарастающий итог
с начала квартала, который на уровне года равен итогу
по последнему доступному кварталу в нем
Для расчета нарастающего итога с начала квартала используется та
же техника, что и для итога с начала года. Единственное отличие состоит в том, что теперь фильтруется поле Year Quarter Number, а не Year.
Мера в таблице Sales:
Sales QTD :=
VAR LastMonthAvailable = MAX ( 'Date'[Year Month Number] )
VAR LastYearQuarterAvailable = MAX ( 'Date'[Year Quarter Number] )
VAR Result =
CALCULATE (
[Sales Amount];
REMOVEFILTERS ( 'Date' );
'Date'[Year Month Number] <= LastMonthAvailable;
'Date'[Year Quarter Number] = LastYearQuarterAvailable
)
RETURN
Result
Сравнение периодов
Зачастую требуется сравнить показатели за определенный период времени с аналогичным периодом в прошлом году, квартале или месяце.
И чтобы сравнение было корректным, мера должна учитывать сопоставимое количество месяцев в прошлом году или квартале.
80
 Глава 3. Вычисления на основе месяца
Годовое сравнение
Вычисление годового сравнения позволяет сопоставить эквивалентные периоды в текущем и прошлом годах. В нашем примере данные по
продажам содержатся по август 2009 года. Именно поэтому в мере Sales
PY производится сравнение с 2008 годом, и только по транзакциям до
августа. На рис. 3.3 показано, что мера Sales Amount за 2008 год показала
9 927 582,99, тогда как мера Sales PY за 2009-й вернула лишь 6 166 534,30.
Причина этого в том, что в вычислении учитываются только продажи
до августа 2008 года.
Рис. 3.3. Для 2009 года мера Sales PY отразила продажи с января
по август 2008-го, поскольку после августа в 2009-м данные отсутствуют
В мере Sales PY удаляются все фильтры с таблицы Date. Происходит
фильтрация столбца Year с использованием значения прошлого года,
и с помощью функции VALUES по Month Number восстанавливаются
месяцы, видимые в текущем контексте фильтра. При этом, как мы уже
говорили, в таблице Date должны содержаться только месяцы с продажами, а не все месяцы в году, как в случае использования шаблона со
стандартными функциями логики операций со временем. Таким образом, любой выбор месяца – явный или неявный – будет применен к
прошедшему году.
Мера в таблице Sales:
Sales PY :=
VAR CurrentYearNumber = SELECTEDVALUE ( 'Date'[Year] )
Сравнение периодов  81
VAR PreviousYearNumber = CurrentYearNumber - 1
VAR Result =
CALCULATE (
[Sales Amount];
REMOVEFILTERS ( 'Date' );
'Date'[Year] = PreviousYearNumber;
VALUES ( 'Date'[Month Number] )
)
RETURN
Result
Сравнение периодов производится в абсолютном и относительном
выражении в мерах Sales YOY и Sales YOY %. При этом обе меры берут за
основу меру Sales PY, чтобы учитывать даты только до августа 2009 года.
Мера в таблице Sales:
Sales YOY :=
VAR ValueCurrentPeriod = [Sales Amount]
VAR ValuePreviousPeriod = [Sales PY]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales YOY % :=
DIVIDE (
[Sales YOY];
[Sales PY]
)
Квартальное сравнение
При квартальном сравнении вычисляется соответствие между текущим периодом и аналогичным в предыдущем квартале. Мы используем
набор данных, заполненный до августа 2009 года. Поэтому в мере Sales
PQ для Q3-2009 (третий квартал 2009 года) показаны продажи только
до второго месяца Q2-2009 (второй квартал 2009 года). На рис. 3.4 видно, что мера Sales Amount за Q2-2009 показывает значение 2 618 644,64,
82
 Глава 3. Вычисления на основе месяца
тогда как мера Sales PQ за Q3-2009 возвращает только 1 746 058,45, поскольку в зачет пошли только продажи по первым двум месяцам второго квартала.
Рис. 3.4. Для Q3-2009 мера Sales PQ показывает продажи с апреля по май 2009-го,
поскольку в Q3-2009 есть только два месяца для сравнения с Q2-2009
В мере Sales PQ удаляются все фильтры с таблицы Date. Выполняется фильтрация столбца Year Quarter Number с использованием значения предыдущего квартала, и с помощью функции VALUES по Month In
Quarter Number восстанавливаются месяцы, видимые в текущем контексте фильтра. Таким образом, явный или неявный выбор месяца будет применен к прошедшему кварталу.
Мера в таблице Sales:
Sales PQ :=
VAR CurrentYearQuarterNumber = SELECTEDVALUE ( 'Date'[Year Quarter Number] )
VAR PreviousYearQuarterNumber = CurrentYearQuarterNumber - 1
VAR Result =
CALCULATE (
[Sales Amount];
REMOVEFILTERS ( 'Date' );
'Date'[Year Quarter Number] = PreviousYearQuarterNumber;
VALUES ( 'Date'[Month In Quarter Number] )
)
RETURN
Result
Сравнение периодов производится в абсолютном и относительном
выражении в мерах Sales QOQ и Sales QOQ %. Обе меры при этом основываются на мере Sales PQ для обеспечения корректного сравнения.
Сравнение периодов  83
Мера в таблице Sales:
Sales QOQ :=
VAR ValueCurrentPeriod = [Sales Amount]
VAR ValuePreviousPeriod = [Sales PQ]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales QOQ % :=
DIVIDE (
[Sales QOQ];
[Sales PQ]
)
Месячное сравнение
При месячном сравнении вычисляется соответствие между текущим
периодом и аналогичным периодом в предыдущем месяце. На рис. 3.5
видно, что мера Sales PM всегда точно соответствует значению меры
Sales Amount по предыдущему месяцу и не присутствует на уровнях
кварталов и годов (на рис. 3.5 показаны только последние).
Рис. 3.5. Значение меры Sales PM всегда соответствует Sales Amount
по предыдущему месяцу
84
 Глава 3. Вычисления на основе месяца
В мере Sales PM удаляются все фильтры с таблицы Date, а фильтрация
выполняется только по столбцу Year Month Number с использованием
значения предыдущего месяца.
Мера в таблице Sales:
Sales PM :=
VAR CurrentYearMonthNumber = SELECTEDVALUE ( 'Date'[Year Month Number] )
VAR PreviousYearMonthNumber = CurrentYearMonthNumber - 1
VAR Result =
CALCULATE (
[Sales Amount];
REMOVEFILTERS ( 'Date' );
'Date'[Year Month Number] = PreviousYearMonthNumber
)
RETURN
Result
Сравнение месяцев здесь выполняется в абсолютном и относительном выражении в мерах Sales MOM и Sales MOM %.
Мера в таблице Sales:
Sales MOM :=
VAR ValueCurrentPeriod = [Sales Amount]
VAR ValuePreviousPeriod = [Sales PM]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales MOM % :=
DIVIDE (
[Sales MOM];
[Sales PM]
)
Сравнение периодов
При сравнении периодов происходит автоматический выбор мер,
описанных ранее в данном разделе, на основании текущего выбора в
визуализации. Например, если в визуализации отображаются данные
Сравнение периодов  85
на уровне месяца, будет произведено месячное сравнение, а если по годам – годовое. Ожидаемый результат показан на рис. 3.6.
Рис. 3.6. В столбце Sales PP отображается значение за предыдущий месяц
на уровне месяца, за прошлый квартал – на уровне квартала
и за прошлый год – на уровне года
Три меры Sales PP, Sales POP и Sales POP % перенаправляют вычисление соответствующим мерам по году, кварталу или месяцу в зависимости от выбранного уровня в отчете. За определение этого уровня отвечает функция ISINSCOPE. Аргументами, передаваемыми в эту функцию,
являются атрибуты, используемые в строках матричной визуализации,
показанной на рис. 3.6. Определения мер при этом будут следующие.
Мера в таблице Sales:
Sales POP % :=
SWITCH (
TRUE;
ISINSCOPE ( 'Date'[Year Month] ); [Sales MOM %];
ISINSCOPE ( 'Date'[Year Quarter] ); [Sales QOQ %];
ISINSCOPE ( 'Date'[Year] ); [Sales YOY %]
)
86
 Глава 3. Вычисления на основе месяца
Мера в таблице Sales:
Sales POP :=
SWITCH (
TRUE;
ISINSCOPE ( 'Date'[Year Month] ); [Sales MOM];
ISINSCOPE ( 'Date'[Year Quarter] ); [Sales QOQ];
ISINSCOPE ( 'Date'[Year] ); [Sales YOY]
)
Мера в таблице Sales:
Sales PP :=
SWITCH (
TRUE;
ISINSCOPE ( 'Date'[Year Month] ); [Sales PM];
ISINSCOPE ( 'Date'[Year Quarter] ); [Sales PQ];
ISINSCOPE ( 'Date'[Year] ); [Sales PY]
)
Сравнение нарастающих итогов
Под сравнением нарастающих итогов мы понимаем сопоставление
значения конкретной меры с нарастающим итогом с той же мерой, вычисленной с определенным временным сдвигом. Например, вы можете
сравнить агрегированную меру с начала года с прошлогодним нарастающим итогом.
Все меры из этого раздела учитывают частичные временные интервалы. Поскольку данные в нашей модели доступны только по август
2009 года, за прошлый год вычисления также будут производиться
только по этот месяц.
Годовое сравнение нарастающих итогов
При годовом сравнении нарастающих итогов выполняется сопоставление значения меры, вычисленной на конкретную дату, с нарастающим итогом за тот же месяц годом ранее. На рис. 3.7 видно, что при
расчете меры Sales PYTD для 2009 года учитываются только продажи
до августа предыдущего года включительно. Именно поэтому значение меры Sales YTD за Q3-2008 (третий квартал 2008 года), равное
7 129 971,53, превратилось в 6 166 534,30 из меры Sales PYTD за Q3-2009.
В мере Sales PYTD фильтруется прошлогоднее значение по полю
Year и берутся в расчет все месяцы до последнего видимого месяца в
контексте фильтра включительно.
Сравнение нарастающих итогов  87
Рис. 3.7. Для Q3-2009 мера Sales PYTD показывает продажи с июля по август
2008 года, поскольку данных после августа 2009-го в нашей модели нет
Мера в таблице Sales:
Sales PYTD :=
VAR LastMonthInYearAvailable = MAX ( 'Date'[Month Number] )
VAR LastYearAvailable = SELECTEDVALUE ( 'Date'[Year] )
VAR PreviousYearAvailable = LastYearAvailable - 1
VAR Result =
CALCULATE (
[Sales Amount];
REMOVEFILTERS ( 'Date' );
'Date'[Month Number] <= LastMonthInYearAvailable;
'Date'[Year] = PreviousYearAvailable
)
RETURN
Result
Меры Sales YOYTD и Sales YOYTD % полагаются на Sales PYTD для выполнения корректного сравнения.
Мера в таблице Sales:
Sales YOYTD :=
VAR ValueCurrentPeriod = [Sales YTD]
VAR ValuePreviousPeriod = [Sales PYTD]
VAR Result =
IF (
88
 Глава 3. Вычисления на основе месяца
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales YOYTD % :=
DIVIDE (
[Sales YOYTD];
[Sales PYTD]
)
Квартальное сравнение нарастающих итогов
Квартальное сравнение нарастающих итогов позволяет сопоставить
значение нарастающего итога с начала квартала до указанной даты с
нарастающим итогом по эквивалентному месяцу в предыдущем квартале. На рис. 3.8 мы видим, что мера Sales PQTD за 2009 год принимает в
расчет только транзакции вплоть до мая, являющегося вторым месяцем
в квартале. Как раз поэтому мера Sales QTD по Q2-2009 показывает цифру 2 61888644,64, тогда как в Sales PQTD за Q3-2009 мы видим значение
меньше: 1 746 058,45.
Рис. 3.8. В мере Sales PQTD за Q3-2009 учтены продажи за период с апреля по май
2009-го, поскольку данных позже августа 2009-го у нас нет
В мере Sales PQTD фильтруется предыдущее значение по полю Year
Quarter Number и берутся в расчет все месяцы в квартале до последнего
относительного месяца в квартале, видимого в контексте фильтра.
Сравнение нарастающих итогов с полным предыдущим периодом  89
Мера в таблице Sales:
Sales PQTD :=
VAR LastMonthInQuarterAvailable = MAX ( 'Date'[Month In Quarter Number] )
VAR LastYearQuarterAvailable = MAX ( 'Date'[Year Quarter Number] )
VAR PreviousYearQuarterAvailable = LastYearQuarterAvailable - 1
VAR Result =
CALCULATE (
[Sales Amount];
REMOVEFILTERS ( 'Date' );
'Date'[Month In Quarter Number] <= LastMonthInQuarterAvailable;
'Date'[Year Quarter Number] = PreviousYearQuarterAvailable
)
RETURN
Result
Меры Sales QOQTD и Sales QOQTD % полагаются на Sales PQTD для
выполнения корректного сравнения.
Мера в таблице Sales:
Sales QOQTD :=
VAR ValueCurrentPeriod = [Sales QTD]
VAR ValuePreviousPeriod = [Sales PQTD]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales QOQTD % :=
DIVIDE (
[Sales QOQTD];
[Sales PQTD]
)
Сравнение нарастающих итогов с полным
предыдущим периодом
Сравнение нарастающих итогов с полным предыдущим периодом может пригодиться, если вы рассматриваете прошлый период в качестве
Powered by TCPDF (www.tcpdf.org)
90
 Глава 3. Вычисления на основе месяца
ориентира. Когда нарастающий итог за текущий год достигнет отметки
в 100 %, по сравнению с годом предыдущим, можно будет говорить, что
вы вышли на прошлогодние показатели.
Сравнение нарастающих итогов за год с полным
прошлым годом
В данном разделе мы остановимся на сравнении нарастающих итогов с начала года с полным предшествующим годом. На рис. 3.9 показано, что в ноябре 2008 года (что близко к окончанию года) мера Sales
YTD почти достигла значения меры Sales Amount за весь 2007 год. Мера
Sales YTDOPY % обеспечивает сравнение нарастающего итога с начала года и прошлогоднего показателя. Если значение положительное,
это означает, что был зафиксирован рост, как в примере с декабрем
2008 года.
Рис. 3.9. В мере Sales YTDOPY % отображаются отрицательные значения,
пока мы не достигнем уровня прошлогодних продаж
Сравнение нарастающих итогов с полным предыдущим периодом
производится в мерах Sales YTDOPY и Sales YTDOPY %. Эти меры базируются на мере Sales YTD для расчета нарастающего итога с начала
года и Sales PYC – для вычисления общих продаж за весь предшествующий год.
Мера в таблице Sales:
Sales PYC :=
VAR CurrentYearNumber = SELECTEDVALUE ( 'Date'[Year] )
VAR PreviousYearNumber = CurrentYearNumber - 1
VAR Result =
CALCULATE (
[Sales Amount];
Сравнение нарастающих итогов с полным предыдущим периодом  91
REMOVEFILTERS ( 'Date' );
'Date'[Year] = PreviousYearNumber
)
RETURN
Result
Мера в таблице Sales:
Sales YTDOPY :=
VAR ValueCurrentPeriod = [Sales YTD]
VAR ValuePreviousPeriod = [Sales PYC]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales YTDOPY % :=
DIVIDE (
[Sales YTDOPY];
[Sales PYC]
)
Сравнение нарастающих итогов за квартал с полным
прошлым кварталом
Здесь мы обратимся к сравнению нарастающих итогов с начала квартала с полным предшествующим кварталом. На рис. 3.10 видно, что в
мае 2009 года мера Sales QTD превысила значение меры Sales Amount за
весь предыдущий квартал (Q1-2009). Мера Sales QTDOPQ % обеспечивает сравнение нарастающего итога с начала квартала с общими продажами за предыдущий квартал. При положительных значениях фиксируется рост показателя, и в нашем случае он был отмечен в мае и июне
2009 года.
Сравнение нарастающих итогов с начала квартала с полным предшествующим кварталом выполняется в мерах Sales QTDOPQ и Sales
QTDOPQ %, а сами они используют в расчетах меру Sales QTD для вы-
92
 Глава 3. Вычисления на основе месяца
числения нарастающего итога с начала квартала и Sales PQC – для получения итоговых продаж за предыдущий квартал.
Рис. 3.10. Мера Sales QTDOPQ % показывала положительные проценты в мае
и июне 2009 года, когда значение меры Sales QTD перевалило за отметку общих
продаж Sales Amount по Q1-2009 (первому кварталу 2009 года)
Мера в таблице Sales:
Sales PQC :=
VAR CurrentYearQuarterNumber = SELECTEDVALUE ( 'Date'[Year Quarter Number] )
VAR PreviousYearQuarterNumber = CurrentYearQuarterNumber - 1
VAR Result =
CALCULATE (
[Sales Amount];
REMOVEFILTERS ( 'Date' );
'Date'[Year Quarter Number] = PreviousYearQuarterNumber
)
RETURN
Result
Мера в таблице Sales:
Sales QTDOPQ :=
VAR ValueCurrentPeriod = [Sales QTD]
VAR ValuePreviousPeriod = [Sales PQC]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
Вычисление скользящей годовой суммы  93
)
RETURN
Result
Мера в таблице Sales:
Sales QTDOPQ % :=
DIVIDE (
[Sales QTDOPQ];
[Sales PQC]
)
Вычисление скользящей годовой суммы
Удобным методом агрегации данных за несколько месяцев является
расчет скользящей годовой суммы вместо нарастающих итогов с начала года. Скользящая годовая сумма включает в себя данные за последние 12 месяцев. К примеру, если мы вычисляем такую сумму для марта
2009 года, то должны принимать в расчет данные с апреля 2008-го по
март 2009-го.
Скользящая годовая сумма
На рис. 3.11 показана мера Sales MAT, вычисляющая скользящую годовую сумму.
В мере Sales MAT определен временной диапазон по столбцу Year
Month Number, включающий целый год от последнего месяца, видимого
в контексте фильтра.
Мера в таблице Sales:
Sales MAT :=
VAR MonthsInRange = 12
VAR LastMonthRange = MAX ( 'Date'[Year Month Number] )
VAR FirstMonthRange = LastMonthRange - MonthsInRange + 1
VAR Result =
CALCULATE (
[Sales Amount];
REMOVEFILTERS ( 'Date' );
'Date'[Year Month Number] >= FirstMonthRange
&& 'Date'[Year Month Number] <= LastMonthRange
)
RETURN
Result
94
 Глава 3. Вычисления на основе месяца
Рис. 3.11. В мере Sales MAT за март 2009 года агрегируются значения меры
Sales Amount с апреля 2008-го по март 2009-го
Сравнение скользящих годовых сумм
Операция сравнения скользящих годовых сумм реализуется при помощи мер Sales PYMAT, Sales MATG и Sales MATG %, которые строят свои
расчеты на основе меры Sales MAT. При этом мера Sales MAT начинает
показывать корректные значения только через год после первой продажи (т. е. только когда наберется информация за целый год), а если
текущий год не полный, цифры могут оказаться неожиданными.
Например, для 2009 года мера Sales PYMAT на рис. 3.11 (см. предыдущий раздел) показывает значение 9 927 582,99, что соответствует
агрегации меры Sales Amount за 2008 год. Если сравнивать полученный
результат с продажами за 2009 год, мы получим сопоставление за период в восемь месяцев, поскольку за 2009 год у нас есть данные только
по август, а за 2008-й – по всему году. Также обратите внимание, что
значения в столбце Sales MATG % начинаются в марте 2008 года с очень
больших величин и стабилизируются только спустя год. Это происходит
Вычисление скользящей годовой суммы  95
из-за отсутствия данных о продажах за предыдущий год. Такое поведение характерно для этого вида вычисления: скользящая годовая сумма обычно рассчитывается на уровне гранулярности по месяцам для
демонстрации трендов на диаграммах.
Определения представленных мер показаны ниже.
Мера в таблице Sales:
Sales PYMAT :=
VAR MonthsInRange = 12
VAR LastMonthRange =
MAX ( 'Date'[Year Month Number] ) - MonthsInRange
VAR FirstMonthRange =
LastMonthRange - MonthsInRange + 1
VAR Result =
CALCULATE (
[Sales Amount];
REMOVEFILTERS ( 'Date' );
'Date'[Year Month Number] >= FirstMonthRange
&& 'Date'[Year Month Number] <= LastMonthRange
)
RETURN
Result
Мера в таблице Sales:
Sales MATG :=
VAR ValueCurrentPeriod = [Sales MAT]
VAR ValuePreviousPeriod = [Sales PYMAT]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales MATG % :=
DIVIDE (
[Sales MATG];
[Sales PYMAT]
)
96
 Глава 3. Вычисления на основе месяца
Скользящее среднее
Скользящее среднее обычно используется для демонстрации трендов
на диаграммах. На рис. 3.12 отображен показатель скользящего среднего для меры Sales Amount за последние три месяца (Sales AVG 3M) и один
год (Sales AVG 1Y).
Рис. 3.12. Меры Sales AVG 3M и Sales AVG 1Y демонстрируют
скользящее среднее за три месяца и год соответственно
Скользящее среднее за три месяца
В мере Sales AVG 3M рассчитывается скользящее среднее за три месяца при помощи итераций по последним трем месяцам, собранным в
переменной Period3M.
Мера в таблице Sales:
Sales AVG 3M :=
VAR MonthsInRange = 3
VAR LastMonthRange =
MAX ( 'Date'[Year Month Number] )
VAR FirstMonthRange =
LastMonthRange - MonthsInRange + 1
VAR Period3M =
FILTER (
ALL ( 'Date'[Year Month Number] );
'Date'[Year Month Number] >= FirstMonthRange
&& 'Date'[Year Month Number] <= LastMonthRange
)
VAR Result =
IF (
COUNTROWS ( Period3M ) >= MonthsInRange;
Работа с годами, содержащими больше 12 месяцев
 97
CALCULATE (
AVERAGEX ( Period3M; [Sales Amount] );
REMOVEFILTERS ( 'Date' )
)
)
RETURN
Result
Скользящее среднее за год
Мера Sales AVG 1Y вычисляет скользящее среднее за год путем перебора последних 12 месяцев, сохраненных в переменной Period1Y.
Мера в таблице Sales:
Sales AVG 1Y :=
VAR MonthsInRange = 12
VAR LastMonthRange =
MAX ( 'Date'[Year Month Number] )
VAR FirstMonthRange =
LastMonthRange - MonthsInRange + 1
VAR Period1Y =
FILTER (
ALL ( 'Date'[Year Month Number] );
'Date'[Year Month Number] >= FirstMonthRange
&& 'Date'[Year Month Number] <= LastMonthRange
)
VAR Result =
IF (
COUNTROWS ( Period1Y ) >= MonthsInRange;
CALCULATE (
AVERAGEX ( Period1Y; [Sales Amount] );
REMOVEFILTERS ( 'Date' )
)
)
RETURN
Result
Работа с годами, содержащими
больше 12 месяцев
Как мы отметили во введении, данный шаблон работает даже в сценариях, в которых год насчитывает более 12 месяцев. Например, в бухгалтерском учете зачастую присутствует 13-й месяц, в котором производятся все необходимые корректировки в конце года. В таких сценариях
очень важно правильно установить значения в таблице Date. В столбце
98
 Глава 3. Вычисления на основе месяца
Year Month Number должны храниться порядковые номера для каждого
месяца года – таким образом, к текущему месяцу в прошлом году можно будет обратиться путем вычитания числа 13 из значения месяца при
условии, что в году предполагается наличие 13 месяцев.
Кроме того, необходимо уделять повышенное внимание содержимому столбцов Month и Year Month. В этих столбцах должно содержаться
текстовое наименование 13-го месяца, и здесь все зависит от того, как
вы хотите отображать этот месяц в финансовом и григорианском календаре.
В примере, показанном на рис. 3.13, 13-й месяц отображается как
«M13» и занимает место следом за июнем, поскольку в этом месяце заканчивается финансовый год. При этом данный месяц видим как в финансовом, так и в григорианском календаре.
Рис. 3.13. 13 финансовых месяцев и 13 календарных месяцев
На рис. 3.14 показан альтернативный вариант, когда 13-й месяц оказывается виден только в финансовом календаре. Когда отчет формируется по григорианскому календарю, значение из 13-го месяца автоматически прибавляется к июньскому показателю. Таким образом,
в отчете остается традиционное количество месяцев для григорианского календаря, а именно 12.
Если вы хотите объединить значения июня и 13-го месяца, как показано слева на рис. 3.14, то должны правильно организовать содержимое
столбцов в таблице Date и больше не использовать одни и те же столбцы
для финансового и григорианского календарей. Таким образом, колонки
для финансового календаря будут содержать разные значения для 12-го
и 13-го месяцев года, тогда как для григорианского название месяца
Работа с годами, содержащими больше 12 месяцев
 99
и его номер будут совпадать. В результате таблица Date по-прежнему
будет содержать 13 месяцев, но в случае с григорианским календарем
два из них будут содержать одинаковые значения. При формировании
отчета эти месяцы будут успешно объединены, и пользователь получит
на выходе ожидаемый результат.
Наборы значений для представленных в данной главе рисунков вы
сможете найти в сопроводительных файлах Month-related calculations –
13 Fiscal and 13 Calendar Months.pbix и Month-related calculations – 13 Fiscal
and 12 Calendar Months.pbix соответственно, в которых столбцы Year
Month, Year Month Number, Month и Month Number для григорианского календаря соответствуют Fiscal Year Month, Fiscal Year Month Number, Fiscal
Month и Fiscal Month Number для финансового.
Рис. 3.14. 13 финансовых месяцев и 12 календарных месяцев
Глава
4
Вычисления
на основе недели
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Данный шаблон описывает рассмотренные выше вычисления, такие
как нарастающий итог с начала года, сравнение с аналогичным периодом в предыдущем году и другие с использованием недельной гранулярности. В этих шаблонах не будут использоваться встроенные в DAX
функции логики операций со временем. При этом все меры реализованы применительно к финансовому календарю, поскольку сама концепция недельных вычислений несовместима с определением месяца
в обычном григорианском календаре. Вы можете использовать шаблон
со стандартными функциями логики операций со временем для вычислений, основанных на стандартном григорианском календаре.
В любом случае, если ваша аналитика в финансовом календаре базируется на неделях, необходимо использовать именно этот шаблон, а не
месячный. Существуют самые разнообразные стандарты для определения недельных календарей. В данном шаблоне мы сделаем следующие
вводные допущения:
• каждый год должен состоять из конечного количества недель;
• каждый период внутри года (квартал, месяц) должен состоять из
конечного количества недель;
• финансовый год всегда начинается с одного и того же дня недели,
так что он совсем не обязательно должен брать старт с 1 января;
• финансовый месяц и финансовый квартал всегда начинаются с
одного и того же дня недели, так что они совсем не обязательно
должны стартовать в первый день месяца.
Введение в вычисления на основе недели  101
Введение в вычисления на основе недели
Вычисления, связанные с логикой операций со временем, представленные в данном разделе, модифицируют контекст фильтра в таблице
Date для получения желаемого результата. Формулы в данном шаблоне
реализованы с применением фильтров на уровне гранулярности, соответствующем требованиям вычисления, без необходимости удалять
фильтры, примененные к таким атрибутам, как рабочий день и день
недели. Это сделано для того, чтобы гранулярность отчета не была ограничена особенностями реализации шаблона.
Шаблон вычислений на основе недели не задействует стандартные
функции логики операций со временем, поэтому на таблицу Date не накладываются столь серьезные ограничения, как в случае с использованием этих функций. При этом формулы получаются идентичными, будь
у вас одна строка для каждой недели или для каждого дня. В примерах у
нас будет одна строка для каждого дня, чтобы можно было создать связь
с таблицей Sales по полю Sales[Order Date].
Если в таблице Date присутствует столбец Date, пометить ее как таблицу дат можно, но это совсем не обязательно. Формулы в рассматриваемом шаблоне не полагаются на автоматическое применение функции REMOVEFILTERS к таблице Date, когда столбец Date отфильтрован.
Вместо этого во всех формулах функция REMOVEFILTERS указана явно,
чтобы избавиться от существующих фильтров и заменить их на минимально необходимое количество фильтров для обеспечения желаемого
результата.
Создание таблицы дат
Таблица Date, используемая в шаблоне по неделям, должна содержать
правильное определение для всех необходимых финансовых периодов,
а именно для квартала, месяца и недели. В данном шаблоне особую
важность приобретают столбцы, относящиеся к неделям, и агрегации,
выполняемые на основе недель, такие как кварталы и годы. Месяцы при
этом могут сильно отличаться от представленных в обычном григорианском календаре, как в нашем примере, в котором мы используем календарь типа 4-4-5.
Если у вас уже есть готовая таблица Date, вы можете просто импортировать ее, убедившись при этом, что у вас есть все необходимые для
этого шаблона столбцы. Если же у вас таблицы дат еще нет, вы легко можете создать ее в виде вычисляемой таблицы в DAX. В качестве примера
представим выражение DAX, описывающее таблицу Date с календарем
типа 4-4-5 и описанием недель, соответствующим стандарту ISO 8601.
В первых строках формулы, описывающей создание вычисляемой
таблицы Date, содержатся переменные, в которых определяется тип не-
102
 Глава 4. Вычисления на основе недели
дельной таблицы. Например, следующие строки определяют календарь
типа 4-4-5, каждый год которого начинается с января, но при этом первый день финансового года может приходиться на предшествующий
декабрь.
Вычисляемая таблица:
Date =
VAR FirstFiscalMonth = 1
-- Первый месяц финансового года
VAR FirstDayOfWeek = 0
-- 0 = воскресенье, 1 = понедельник, ...
VAR FirstSalesDate = MIN ( Sales[Order Date] )
VAR LastSalesDate = MAX ( Sales[Order Date] )
VAR TypeStartFiscalYear = 1
-- Финансовый год как календарный год от :
-- 0 - Первого дня финансового года
-- 1 - Последнего дня финансового года
VAR QuarterWeekType = "445"
-- Поддерживает значения "445", "454" и "544"
VAR WeeklyType = "Last"
-- Используются: "Nearest" или "Last"
-- Оставшийся код вычисляемой таблицы приведен в сопроводительном файле
Мы настоятельно советуем вам прочитать комментарии в листинге
создания вычисляемой таблицы Date, чтобы понимать, удовлетворяет
ли она вашим требованиям. Но если у вас уже присутствует таблица дат
в модели данных, вам достаточно лишь добавить к ней несколько столбцов, о которых мы напишем в следующих абзацах.
Для обеспечения корректной визуализации календарные столбцы
в модели данных должны быть настроены так, как показано ниже. Для
каждого столбца мы обозначили его тип данных и пример заполнения:
• Date: Date, m/dd/yyyy (8/14/2007), используется в качестве столбца
для пометки Date как таблицы дат, что не обязательно;
• Sequential Day Number: Whole Number, Hidden (40040), то же, что и
Date, но в целочисленном формате;
• Fiscal Year: Text (FY 2007);
• Fiscal Year Number: Whole Number, Hidden (2007);
• Fiscal Quarter: Text (FQ3);
• Fiscal Quarter Number: Whole Number, Hidden (3);
• Fiscal Year Quarter: Text (FQ3-2007);
• Fiscal Year Quarter Number: Whole Number, Hidden (8030);
• Fiscal Week: Text (FW33);
• Fiscal Week Number: Whole Number, Hidden (33);
• Fiscal Year Week: Text (FW33-2007);
• Fiscal Year Week Number: Whole Number, Hidden (5564);
• Fiscal Month: Text (FM Aug);
• Fiscal Month Number: Whole Number, Hidden (8);
Введение в вычисления на основе недели  103
•
•
•
•
•
Fiscal Year Month: Text (FM Aug 2007);
Fiscal Year Month Number: Whole Number, Hidden (24091);
Day of Fiscal Month Number: Whole Number, Hidden (17);
Day of Fiscal Quarter Number: Whole Number, Hidden (45);
Day of Fiscal Year Number: Whole Number, Hidden (227).
Также мы хотим познакомить вас с концепцией фильтрозащищенных
столбцов (filter-safe columns). В таблицах могут быть столбцы, фильтры
по которым должны всегда сохраняться. И фильтры по фильтрозащищенным столбцам как раз не изменяются в результате вычислений,
связанных с логикой операций со временем. При этом они будут влиять
на вычисления, представленные в данном шаблоне. В нашей таблице
дат фильтрозащищенными столбцами являются следующие:
• Day of Week: ddd (Tue);
• Day of Week Number: Whole Number, Hidden (6);
• Working Day: Text (Working Day).
В следующем разделе мы дадим более подробное описание фильтрозащищенных столбцов.
Таблица Date в нашем примере обладает несколькими иерархиями:
• Year-Month-Week: Year (Fiscal Year), Month (Fiscal Year Month), Week
(Fiscal Year Week);
• Year-Quarter-Month-Week: Year (Fiscal Year), Quarter (Fiscal Year
Quarter), Month (Fiscal Year Month), Week (Fiscal Year Week);
• Year-Quarter-Week: Year (Fiscal Year), Quarter (Fiscal Year Quarter),
Week (Fiscal Year Week);
• Year- Week: Year (Fiscal Year), Week (Fiscal Year Week).
Некоторые столбцы созданы с единственной целью упростить процесс написания формул. Например, столбец Day of Fiscal Year Number
содержит количество дней, прошедших с начала финансового года, и
облегчает процесс поиска соответствующего диапазона дат в предшествующем году.
Таблица Date также должна включать скрытый вычисляемый столбец
DateWithSales, который будет использован в нескольких формулах данного шаблона.
Вычисляемый столбец в таблице Date:
DateWithSales =
'Date'[Date] <= MAX ( Sales[Order Date] )
Значение в столбце Date[DateWithSales] будет истинным (TRUE), если
дата входит в период, когда были продажи, и ложным (FALSE) в про-
104
 Глава 4. Вычисления на основе недели
тивном случае. Иными словами, DateWithSales будет равна TRUE для
«прошлых» дат и FALSE – для «будущих», где прошлое и будущее рассчитывается относительно последнего дня с транзакциями в таблице Sales.
Если вы импортируете таблицу Date, позаботьтесь о создании вспомогательных столбцов, которые мы описываем в данном шаблоне.
Фильтрозащищенные столбцы
Таблица Date содержит два типа столбцов: обычные и фильтрозащищенные. С обычными столбцами взаимодействуют меры, показанные в
данном шаблоне. В то же время фильтры, наложенные на фильтрозащищенные колонки, всегда сохраняют свое значение и не изменяются
посредством мер. Пример позволит вам понять, о чем речь.
Столбец Year Quarter Number является обычным, и формулы в этом
шаблоне способны изменять его значения в процессе вычисления.
Чтобы получить предыдущий квартал, формула изменяет контекст
фильтра путем вычитания единицы из значения столбца Year Quarter
Number. Столбец Day of Week, напротив, является фильтрозащищенным.
Если пользователь установит фильтр с понедельника по пятницу, формулы не смогут изменить его в момент вычисления. Таким образом,
мера, вычисляющая предыдущий квартал, сохранит значение фильтра
по дням недели и изменит только фильтр по календарным столбцам,
таким как год, месяц и день.
Для реализации данного шаблона вам необходимо определиться с
тем, какие именно столбцы должны быть фильтрозащищенными, поскольку к ним требуется иной подход. Ниже приведена классификация
столбцов в таблице Date в представленном здесь шаблоне:
• календарные столбцы: Date, Fiscal Year, Fiscal Year Number, Fiscal
Quarter, Fiscal Quarter Number, Fiscal Year Quarter, Fiscal Year Quarter
Number, Fiscal Week, Fiscal Week Number, Fiscal Year Week, Fiscal Year
Week Number, Fiscal Month, Fiscal Month Number, Fiscal Year Month,
Fiscal Year Month Number, Day of Fiscal Month Number, Day of Fiscal
Quarter Number, Day of Fiscal Year Number;
• фильтрозащищенные столбцы: Day of Week, Day of Week Number, Working Day.
Особый подход к фильтрозащищенным столбцам, о котором мы говорили ранее, связан с контекстом фильтра. Все меры в этом шаблоне
влияют на контекст фильтра, заменяя фильтр по календарным столбцам, но при этом не затрагивают фильтрозащищенные столбцы. Иными словами, каждая мера следует двум правилам:
• удалять фильтры с календарных столбцов;
• не изменять фильтры по фильтрозащищенным столбцам.
Введение в вычисления на основе недели  105
Помочь реализовать эту концепцию способна функция ALLEXCEPT,
если первым аргументом передать таблицу Date, а следующими –
фильтрозащищенные столбцы:
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
... // Фильтры по одному или больше календарным столбцам
)
Если таблица Date не содержит фильтрозащищенных столбцов,
фильтры могут быть удалены при помощи функции REMOVEFILTERS
по таблице Date вместо применения функции ALLEXCEPT:
CALCULATE (
[Sales Amount];
REMOVEFILTERS ( 'Date' );
... // Фильтры по одному или больше календарным столбцам
)
Если в вашей таблице Date отсутствуют фильтрозащищенные столбцы,
вы можете использовать функцию REMOVEFILTERS вместо ALLEXCEPT
во всех мерах шаблона. Мы предлагаем полноценный сценарий, включающий фильтрозащищенные столбцы. Если понадобится, вы сможете
легко его упростить.
Хотя в функцию ALLEXCEPT должны быть включены все без исключения фильтрозащищенные столбцы, мы не стали указывать скрытые
столбцы, используемые исключительно для сортировки других колонок. Например, мы не включили в список скрытый столбец Day of Week
Number, служащий для сортировки столбца Day of Week. Мы исходим из
предположения о том, что пользователь никогда не применяет фильтры к скрытым столбцам. Если это не так, то скрытые фильтрозащищенные столбцы также должны быть включены в число аргументов функции ALLEXCEPT. Вы можете найти примеры использования функций
REMOVEFILTERS и ALLEXCEPT в разделе, посвященном нарастающим
итогам с начала года далее в этом шаблоне.
Управление визуализациями для будущих дат
В большинстве случаев вычисления, связанные с логикой операций
со временем, не должны отображать значения для будущих периодов.
Например, расчет нарастающего итога с начала года может в теории
106
 Глава 4. Вычисления на основе недели
производиться и для дат будущих периодов, но нам бы не хотелось показывать эти значения. Набор данных, используемый в данном примере, как и раньше, ограничивается 15 августа 2009 года. Таким образом, мы рассматриваем финансовый месяц август 2009-го (FM August
2009), третий финансовый квартал того года (FQ3-2009) и сам 2009 год
(FY 2009) в качестве последних периодов с данными. Любая дата позднее 15 августа 2009 года будет рассматриваться как будущая, и значения
по ней выводиться не будут.
Чтобы избежать показа значений в будущих периодах, воспользуемся мерой ShowValueForDates, которая будет возвращать TRUE, если выбранный временной период располагается раньше последнего периода
с данными.
Мера (скрытая) в таблице дат:
ShowValueForDates :=
VAR LastDateWithData =
CALCULATE (
MAX ( 'Sales'[Order Date] );
REMOVEFILTERS ()
)
VAR FirstDateVisible =
MIN ( 'Date'[Date] )
VAR Result =
FirstDateVisible <= LastDateWithData
RETURN
Result
Мера ShowValueForDates является скрытой. Это исключительно техническая мера, созданная с целью использования одной и той же логики в различных вычислениях, связанных с операциями со временем.
Пользователь не должен иметь возможности использовать эту меру в
отчетах напрямую. Модификатор REMOVEFILTERS применяется здесь
для удаления фильтров со всех таблиц в модели данных, поскольку нашей целью является извлечение последней даты, присутствующей в
таблице Sales, вне зависимости от фильтров.
Соглашение об именованиях
В табл. 4.1 приведены наименования, принятые для обозначения
вычислений, основанных на логике операций со временем. В столбцах
отображена информация о том:
• сдвигается ли вычисление во времени, например для определения того же периода в прошлом году;
Введение в вычисления на основе недели  107
• выполняется ли агрегация при вычислении, например как в случае с нарастающим итогом с начала года;
• производится ли сравнение двух временных периодов: допустим,
текущего года с прошлым.
Таблица 4.1. Принятые обозначения вычислений, основанных на логике операций
со временем
Сокращение
Описание
Сдвиг Агрегация Сравнение
YTD
Year-to-date
(нарастающий итог с начала года)
X
QTD
Quarter-to-date
(нарастающий итог с начала квартала)
X
MTD
Month-to-date
(нарастающий итог с начала месяца)
X
WTD
Week-to-date
(нарастающий итог с начала недели)
X
MAT
Moving annual total
(скользящая годовая сумма)
X
PY
Previous year
(предыдущий год)
X
PQ
Previous quarter
(предыдущий квартал)
X
PW
Previous week
(предыдущая неделя)
X
PYC
Previous year complete
(предыдущий год полный)
X
PQC
Previous quarter complete
(предыдущий квартал полный)
X
PWC
Previous week complete
(предыдущая неделя полная)
X
PP
Previous period
(прошлый период) (автоматически
выбираются год, квартал или месяц)
X
PYMAT
Previous year moving annual total
(скользящая годовая сумма за прошлый год)
X
YOY
Year-over-year
(годовое сравнение)
X
QOQ
Quarter-over-quarter
(квартальное сравнение)
X
X
108
 Глава 4. Вычисления на основе недели
Сокращение
Описание
WOW
Week-over-week
(недельное сравнение)
MATG
Moving annual total growth
(скользящая годовая сумма, прирост)
POP
Period-over-period
(сравнение периодов) (автоматически
выбираются год, квартал или месяц)
PYTD
Сдвиг Агрегация Сравнение
X
X
X
X
X
Previous year-to-date
(прошлогодний нарастающий итог с начала
года)
X
X
Previous quarter-to-date
(прошлогодний нарастающий итог с начала
квартала)
X
X
Previous week-to-date
(прошлогодний нарастающий итог с начала
недели)
X
X
YOYTD
Year-over-year-to-date
(годовое сравнение нарастающим итогом)
X
X
X
QOQTD
Quarter-over-quarter-to-date
(квартальное сравнение нарастающим
итогом)
X
X
X
WOWTD
Week-over-week-to-date
(недельное сравнение нарастающим итогом)
X
X
X
YTDOPY
Year- to-date-over-previous-year
(нарастающий итог с начала года
по сравнению с предыдущим годом)
X
X
X
Quarter- to-date-over-previous-quarter
(нарастающий итог с начала квартала
по сравнению с предыдущим кварталом)
X
X
X
Week- to-date-over-previous-week
(нарастающий итог с начала недели
по сравнению с предыдущей неделей)
X
X
X
PQTD
PWTD
QTDOPQ
WTDOPW
Вычисление нарастающих итогов
Вычисление нарастающего итога с начала года, квартала, месяца или
недели модифицирует контекст фильтра таблицы дат, оставляя значения с начала периода до последней даты, доступной в контексте фильтра.
Вычисление нарастающих итогов  109
Нарастающие итоги с начала года
Вычисление нарастающего итога с начала года агрегирует значения
начиная с первой даты финансового года, как показано на рис. 4.1.
Рис. 4.1. В столбце Sales YTD (simple) показаны значения
для всех периодов, тогда как в Sales YTD скрыты цифры за будущие даты
В мере остаются все даты, предшествующие последней дате, видимой
в последнем финансовом году, включая ее саму. Также выбирается последний видимый финансовый год путем наложения фильтра на поле
Fiscal Year Number.
Мера в таблице Sales:
Sales YTD (simple) :=
VAR LastDayAvailable = MAX ( 'Date'[Day of Fiscal Year Number] )
VAR LastFiscalYearAvailable = MAX ( 'Date'[Fiscal Year Number] )
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Day of Fiscal Year Number] <= LastDayAvailable;
'Date'[Fiscal Year Number] = LastFiscalYearAvailable
110
 Глава 4. Вычисления на основе недели
)
RETURN
Result
Поскольку LastDayAvailable содержит последнюю видимую дату в контексте фильтра, в мере Sales YTD (simple) отображаются даты даже для
будущих периодов в году. В мере Sales YTD мы исключим такое поведение – просто будем возвращать только те данные, для которых значение
поля ShowValueForDates будет равно TRUE.
Мера в таблице Sales:
Sales YTD :=
IF (
[ShowValueForDates];
VAR LastDayAvailable = MAX ( 'Date'[Day of Fiscal Year Number] )
VAR LastFiscalYearAvailable = MAX ( 'Date'[Fiscal Year Number] )
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Day of Fiscal Year Number] <= LastDayAvailable;
'Date'[Fiscal Year Number] = LastFiscalYearAvailable
)
RETURN
Result
)
Функция ALLEXCEPT необходима здесь, чтобы сохранить фильтрозащищенные столбцы Working Day или Day of Week в случае, если они будут использованы в отчете. Чтобы продемонстрировать это, создадим
заведомо ошибочную меру Sales YTD (wrong), в которой будут удаляться
фильтры с таблицы Date при помощи функции REMOVEFILTERS вместо
ALLEXCEPT. Но при этом формула потеряла фильтр по полю Working Day,
использующийся в столбцах матрицы, что привело к некорректным результатам.
Мера в таблице Sales:
Sales YTD (wrong) :=
IF (
[ShowValueForDates];
VAR LastDayAvailable = MAX ( 'Date'[Day of Fiscal Year Number] )
VAR LastFiscalYearAvailable = MAX ( 'Date'[Fiscal Year Number] )
RETURN
CALCULATE (
Вычисление нарастающих итогов  111
[Sales Amount];
REMOVEFILTERS ( 'Date' );
'Date'[Day of Fiscal Year Number] <= LastDayAvailable;
'Date'[Fiscal Year Number] = LastFiscalYearAvailable
)
)
На рис. 4.2 показаны отличия в выводе правильной и неправильной
меры.
Рис. 4.2. В мере Sales YTD (wrong) показан расчет меры Sales YTD
с игнорированием фильтра по полю Working Day
Мера Sales YTD (wrong) работала бы правильно, если бы в таблице Date
не было фильтрозащищенных столбцов. Их присутствие требует использования функции ALLEXCEPT вместо REMOVEFILTERS. Мы использовали меру Sales YTD просто в качестве примера, но те же концепции
могут быть распространены на все меры в этом шаблоне.
Нарастающие итоги с начала квартала
Вычисление нарастающего итога с начала квартала агрегирует значения начиная с первого дня финансового квартала, как показано на рис. 4.3.
Нарастающий итог с начала квартала рассчитывается при помощи той
же техники, что и с начала года. Разница только в том, что в фильтрах
указаны поля Fiscal Year Quarter Number вместо Fiscal Year Number и Day
of Fiscal Quarter Number вместо Day of Fiscal Year Number.
112
 Глава 4. Вычисления на основе недели
Рис. 4.3. В столбце Sales QTD показан нарастающий итог
с начала квартала, который на уровне 2009 года не заполнен,
поскольку для FQ4-2009 нет данных
Мера в таблице Sales:
Sales QTD :=
IF (
[ShowValueForDates];
VAR LastDayAvailable = MAX ( 'Date'[Day of Fiscal Quarter Number] )
VAR LastFiscalYearQuarterAvailable =
MAX ( 'Date'[Fiscal Year Quarter Number] )
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Day of Fiscal Quarter Number] <= LastDayAvailable;
'Date'[Fiscal Year Quarter Number] =
LastFiscalYearQuarterAvailable
)
RETURN
Result
)
Нарастающие итоги с начала месяца
Вычисление нарастающего итога с начала месяца собирает значения начиная с первого дня финансового месяца, как показано на
рис. 4.4.
Нарастающий итог с начала месяца рассчитывается при помощи той
же техники, что и с начала года или квартала, – с фильтрацией всех дат
Вычисление нарастающих итогов  113
раньше последней даты в последнем финансовом месяце, включая ее
саму. Фильтры накладываются также на поля Day of Fiscal Month Number
и Fiscal Year Month Number.
Рис. 4.4. В столбце Sales MTD показан нарастающий итог
с начала месяца, который на уровнях FY 2009 и FQ3-2009 пуст,
поскольку после 15 августа 2009 года нет данных
Мера в таблице Sales:
IF (
[ShowValueForDates];
VAR LastDayAvailable = MAX ( 'Date'[Day of Fiscal Month Number] )
VAR LastFiscalYearMonthAvailable = MAX ( 'Date'[Fiscal Year Month Number] )
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Day of Fiscal Month Number] <= LastDayAvailable;
'Date'[Fiscal Year Month Number] = LastFiscalYearMonthAvailable
)
RETURN
Result
)
В этой мере фильтруется столбец Day of Fiscal Month Number вместо
Day of Fiscal Year Number. Смысл в том, чтобы фильтровать столбец с
меньшим количеством уникальных значений, что положительно скажется на производительности запроса.
114
 Глава 4. Вычисления на основе недели
Нарастающие итоги с начала недели
Вычисление нарастающего итога с начала месяца агрегирует значения начиная с первого дня недели, что видно по рис. 4.5.
Рис. 4.5. В столбце Sales WTD показан нарастающий итог с начала недели,
который на уровнях FY 2009, FQ3-2009 и FM Aug 2009 пуст,
поскольку после 15 августа 2009 года информации в базе нет
Техника вычислений здесь та же, которая использовалась для расчета
нарастающих итогов с начала года и квартала, – с фильтрацией всех дат
раньше последнего дня недели, видимого в последней финансовой неделе, включая его самого. Фильтры накладываются также на поля Day of
Week Number и Fiscal Year Week Number.
Мера в таблице Sales:
Sales WTD :=
IF (
[ShowValueForDates];
VAR LastDayOfWeekAvailable = MAX ( 'Date'[Day of Week Number] )
VAR LastFiscalYearWeekAvailable = MAX ( 'Date'[Fiscal Year Week Number] )
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
Сравнение периодов  115
'Date'[Day of Week Number] <= LastDayOfWeekAvailable;
'Date'[Fiscal Year Week Number] = LastFiscalYearWeekAvailable
)
RETURN
Result
)
В этой мере фильтруется столбец Day of Week Number вместо Day of
Fiscal Year Number. Это сделано для того, чтобы фильтровать столбец с
меньшим количеством уникальных значений, что положительно скажется на производительности запроса.
Сравнение периодов
Часто требуется сравнить значения показателей за определенный период времени с аналогичным периодом в прошлом году, квартале или
неделе. Мы не говорим о сравнении двух месяцев, поскольку в недельном календаре типа 4-4-5 в месяцах может оказаться разное количество
недель. А для корректного сравнения необходимо, чтобы периоды были
сопоставимы. Кроме того, необходимо помнить о возможности наличия незаконченного последнего периода: недели, квартала или года.
Поэтому в вычислениях, показанных в данном разделе, используется
вычисляемый столбец Date[DateWithSales], о чем подробно можно почитать по адресу https://sql.bi/78171.
Годовое сравнение
Вычисление годового сравнения позволяет сопоставить эквивалентные периоды в текущем и прошлом годах. В нашем примере данные по
продажам содержатся по 15 августа 2009 года. Именно поэтому в мере
Sales PY производится сравнение с 2008 годом и только по транзакциям до 15 августа. На рис. 4.6 видно, что мера Sales Amount за период
FQ3-2008 показала 2 573 182,08, тогда как в мере Sales PY по периоду
FQ3-2009 стоит сумма 1 270 748,28. Причина именно в том, что в вычислении участвуют только продажи до 15 августа включительно.
В мере Sales PY осуществляется проход по всем активным годам в текущем контексте фильтра. Для каждого года извлекаются выбранные
дни, но при этом игнорируются фильтрозащищенные столбцы (в нашем случае это Working Day, Day of Week и Day of Week Number). Дни
определяются путем расчета относительного номера дня в году. Эти
дни применяются в качестве фильтра к предыдущему году. Фильтры
по фильтрозащищенным столбцам удерживаются в контексте фильтра
при помощи функции ALLEXCEPT.
116
 Глава 4. Вычисления на основе недели
Рис. 4.6. За период FQ3-2009 мера Sales PY отразила продажи
в периоде FQ3-2008 до 15 августа 2008 года,
поскольку после этой даты в 2009-м данные отсутствуют
Мера в таблице Sales:
Sales PY :=
IF (
[ShowValueForDates];
SUMX (
VALUES ( 'Date'[Fiscal Year Number] );
VAR CurrentFiscalYearNumber = 'Date'[Fiscal Year Number]
VAR DaysSelected =
CALCULATETABLE (
VALUES ( 'Date'[Day of Fiscal Year Number] );
REMOVEFILTERS (
'Date'[Working Day];
'Date'[Day of Week];
'Date'[Day of Week Number]
);
'Date'[DateWithSales] = TRUE
)
RETURN
CALCULATE (
[Sales Amount];
'Date'[Fiscal Year Number] = CurrentFiscalYearNumber - 1;
DaysSelected;
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] )
)
)
)
Годовое сравнение в абсолютном выражении вычисляется в мере
Sales YOY, а в процентном – в мере Sales YOY %. При этом обе меры
Сравнение периодов  117
используют в качестве основы меру Sales PY для учета дат только по
15 августа.
Мера в таблице Sales:
Sales YOY :=
VAR ValueCurrentPeriod = [Sales Amount]
VAR ValuePreviousPeriod = [Sales PY]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales YOY % :=
DIVIDE (
[Sales YOY];
[Sales PY]
)
Квартальное сравнение
При квартальном сравнении вычисляется соответствие между текущим периодом и аналогичным в предыдущем квартале. Мы по-прежнему используем набор данных, заполненный до 15 августа 2009 года,
что эквивалентно середине третьего квартала 2009-го финансового
года – 49-й день квартала. Таким образом, в мере Sales PQ за август
2009-го (второй месяц третьего квартала) показаны продажи вплоть до
16 мая этого года, т. е. 49-го дня предыдущего квартала (FQ2-2009). На
рис. 4.7 видно, что мера Sales Amount за период FQ2-2009 показывает
сумму 2 531 034,28, тогда как мера Sales PQ за период FQ3-2009 возвращает 1 140117186,77, поскольку в зачет пошли только продажи до 16 мая
этого года включительно.
Мера Sales PQ демонстрирует ту же технику, что и Sales PY. Различия
заключаются в том, что вместо Fiscal Year Number итерации осуществляются по Fiscal Year Quarter Number, а фильтр применяется к полю Day of
Fiscal Quarter Number, а не к Day of Fiscal Year Number.
118
 Глава 4. Вычисления на основе недели
Рис. 4.7. Для периода FQ3-2009 мера Sales PQ показывает продажи
с начала квартала FQ2-2009 до 16 мая 2009 года – после 15 августа
(также 49-го дня в квартале) у нас данных нет
Мера в таблице Sales:
Sales PQ :=
IF (
[ShowValueForDates];
SUMX (
VALUES ( 'Date'[Fiscal Year Quarter Number] );
VAR CurrentFiscalYearQuarterNumber = 'Date'[Fiscal Year Quarter Number]
VAR DaysSelected =
CALCULATETABLE (
VALUES ( 'Date'[Day of Fiscal Quarter Number] );
REMOVEFILTERS (
'Date'[Working Day];
'Date'[Day of Week];
'Date'[Day of Week Number]
Сравнение периодов  119
);
'Date'[DateWithSales] = TRUE
)
RETURN
CALCULATE (
[Sales Amount];
'Date'[Fiscal Year Quarter Number] =
CurrentFiscalYearQuarterNumber - 1;
DaysSelected;
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] )
)
)
)
Квартальное сравнение в абсолютном выражении вычисляется в мере
Sales QOQ, а в процентном – в мере Sales QOQ %. При этом обе меры используют в качестве основы меру Sales PQ для гарантии корректного
сравнения.
Мера в таблице Sales:
Sales QOQ :=
VAR ValueCurrentPeriod = [Sales Amount]
VAR ValuePreviousPeriod = [Sales PQ]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales QOQ % :=
DIVIDE (
[Sales QOQ];
[Sales PQ]
)
Недельное сравнение
В случае недельного сравнения производится сопоставления временного периода с аналогичным на предыдущей неделе. Вычисления
Powered by TCPDF (www.tcpdf.org)
120
 Глава 4. Вычисления на основе недели
аналогичны тем, что мы видели при расчете годового и квартального
сравнения, за исключением того, что в данном примере у нас нет частично заполненной недели, соответствующей последнему доступному
дню (15 августа 2009 года). В мере Sales PW суммируются показатели
всех недель за период со сдвигом на неделю, если в отчете агрегируется
больше одной недели, как на уровнях года и квартала. На рис. 4.8 показан пример результата.
Рис. 4.8. В мере Sales PW показывается значение меры Sales Amount
за предыдущую неделю
При расчете мера Sales PW демонстрирует ту же технику, что и Sales
PY. Различия заключаются в том, что вместо Fiscal Year Number итерации осуществляются по Fiscal Year Week Number, а фильтр применяется
к полю Day of Week Number, а не к Day of Fiscal Year Number.
Мера в таблице Sales:
Sales PW :=
IF (
[ShowValueForDates];
SUMX (
VALUES ( 'Date'[Fiscal Year Week Number] );
VAR CurrentFiscalYearWeekNumber = 'Date'[Fiscal Year Week Number]
VAR DaysSelected =
CALCULATETABLE (
VALUES ( 'Date'[Day of Week Number] );
REMOVEFILTERS (
'Date'[Working Day];
'Date'[Day of Week];
'Date'[Day of Week Number]
Сравнение периодов  121
);
'Date'[DateWithSales] = TRUE
)
RETURN
CALCULATE (
[Sales Amount];
'Date'[Fiscal Year Week Number] =
CurrentFiscalYearWeekNumber - 1;
KEEPFILTERS ( DaysSelected );
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] )
)
)
)
Недельное сравнение в абсолютном выражении вычисляется в мере
Sales WOW, а в процентном – в мере Sales WOW %. При этом обе меры
используют в качестве основы меру Sales PW для гарантии корректного
сравнения.
Мера в таблице Sales:
Sales WOW :=
VAR ValueCurrentPeriod = [Sales Amount]
VAR ValuePreviousPeriod = [Sales PW]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales WOW % :=
DIVIDE (
[Sales WOW];
[Sales PW]
)
Сравнение периодов
При сравнении периодов происходит автоматический выбор мер,
описанных ранее в данном разделе, на основании текущего выбора в
122
 Глава 4. Вычисления на основе недели
визуализации. Например, если в визуализации отображаются данные
на уровне недели, будет произведено недельное сравнение, а если по
кварталам – квартальное. В календаре типа 4-4-5 месячное сравнение
не поддерживается. Ожидаемый результат показан на рис. 4.9.
Рис. 4.9. В столбце Sales PP показано значение за прошлую неделю
на уровне недели, за прошлый квартал – на уровне квартала
и за прошлый год – на уровне года
В мерах Sales PP, Sales POP и Sales POP % происходит отсылка к соответствующей неделе, кварталу или году в зависимости от текущего
выбора в отчете. Определение выбранного уровня производится при
помощи функции ISINSCOPE. Аргументы, передаваемые в эту функцию, – это атрибуты, представленные в строках матрицы, показанной
на рис. 4.9. Формулы приведенных мер следующие.
Мера в таблице Sales:
Sales POP % :=
SWITCH (
TRUE;
ISINSCOPE (
-- Месячное
ISINSCOPE (
ISINSCOPE (
)
'Date'[Fiscal Year Week] ); [Sales WOW %];
сравнение в календаре типа 4-4-5 не реализуется
'Date'[Fiscal Year Quarter] ); [Sales QOQ %];
'Date'[Fiscal Year] ); [Sales YOY %]
Сравнение нарастающих итогов  123
Мера в таблице Sales:
Sales POP :=
SWITCH (
TRUE;
ISINSCOPE (
-- Месячное
ISINSCOPE (
ISINSCOPE (
)
'Date'[Fiscal Year Week] ); [Sales WOW];
сравнение в календаре типа 4-4-5 не реализуется
'Date'[Fiscal Year Quarter] ); [Sales QOQ];
'Date'[Fiscal Year] ); [Sales YOY]
Мера в таблице Sales:
Sales PP :=
SWITCH (
TRUE;
ISINSCOPE (
-- Месячное
ISINSCOPE (
ISINSCOPE (
)
'Date'[Fiscal Year Week] ); [Sales PW];
сравнение в календаре типа 4-4-5 не реализуется
'Date'[Fiscal Year Quarter] ); [Sales PQ];
'Date'[Fiscal Year] ); [Sales PY]
Сравнение нарастающих итогов
Сравнение нарастающих итогов помогает сопоставить меру с вычислением нарастающего итога с той же мерой, рассчитанной за аналогичный период времени с определенным сдвигом. Например, вы можете
сравнить нарастающий итог с таким же показателем за предыдущий
финансовый год, т. е. со временным сдвигом ровно в один финансовый
год.
При этом все меры будут вычисляться с учетом частичных временных
периодов. Поскольку в нашем случае данные в модели имеются только
по 15 августа 2009 года, в мерах со сдвигом не будут учитываться продажи, которые совершались позже этого числа в 2008 году.
Годовое сравнение нарастающих итогов
Вычисление годового сравнения позволяет сопоставить эквивалентные периоды в текущем и прошлом годах. На рис. 4.10 показано, что
в мере Sales PYTD за период FY 2009 учитываются только продажи по
16 августа 2008 года, поскольку это та же относительная дата в периоде
FY 2008, что и 15 августа – в периоде FY 2009. Именно поэтому мера Sales
YTD за период FQ3-2008 показывает значение 7 124 371,27, тогда как в
мере Sales PYTD за FQ3-2009 сумма получается меньше: 5 821 937,47.
124
 Глава 4. Вычисления на основе недели
Рис. 4.10. За период FQ3-2009 мера Sales PYTD включает продажи
с начала периода FQ3-2008 и до 16 августа 2008 года,
поскольку после 15 августа 2009-го данных нет
Мера Sales PYTD рассчитывается примерно так же, как Sales YTD: в
ней происходит фильтрация поля Fiscal Year Number по предыдущему
году вместо последнего года, видимого в контексте фильтра. Главное
же отличие состоит в расчете переменной LastDayOfFiscalYearAvailable,
в которой должны учитываться только даты с продажами и игнорироваться фильтры по фильтрозащищенным столбцам, которые присутствуют при расчете меры Sales Amount.
Мера в таблице Sales:
Sales PYTD :=
IF (
[ShowValueForDates];
VAR PreviousFiscalYear = MAX ( 'Date'[Fiscal Year Number] ) - 1
VAR LastDayOfFiscalYearAvailable =
CALCULATE (
MAX ( 'Date'[Day of Fiscal Year Number] );
REMOVEFILTERS (
-- Удаляем фильтры
'Date'[Working Day];
-- c фильтрозащищенных столбцов,
'Date'[Day of Week];
-- чтобы получить последнюю дату
'Date'[Day of Week Number] -- с продажами, выбранную в отчете
);
'Date'[DateWithSales] = TRUE
)
VAR Result =
Сравнение нарастающих итогов  125
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Fiscal Year Number] = PreviousFiscalYear;
'Date'[Day of Fiscal Year Number] <= LastDayOfFiscalYearAvailable;
'Date'[DateWithSales] = TRUE
)
RETURN
Result
)
Меры Sales YOYTD и Sales YOYTD % базируются на вычислении меры
Sales PYTD.
Мера в таблице Sales:
Sales YOYTD :=
VAR ValueCurrentPeriod = [Sales YTD]
VAR ValuePreviousPeriod = [Sales PYTD]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales YOYTD % :=
DIVIDE (
[Sales YOYTD];
[Sales PYTD]
)
Квартальное сравнение нарастающих итогов
Квартальное сравнение нарастающих итогов позволяет сопоставить
значение нарастающего итога с начала квартала до указанной даты с
тем же показателем по эквивалентной дате в предыдущем квартале. На
рис. 4.11 показано, что мера Sales PQTD за август 2009 года учитывает
только транзакции вплоть до 16 мая 2008-го, получая тем самым соответствующую долю предыдущего квартала. Как раз поэтому мера Sales
126
 Глава 4. Вычисления на основе недели
QTD по маю 2009 года демонстрирует цифру 1 411 541,99, тогда как в
Sales PQTD по августу 2009-го мы видим значение меньше: 1 140 186,77.
Рис. 4.11. В мере Sales PQTD за август 2009 года учтены продажи за период
с 29 марта по 16 мая 2009-го, поскольку данных позже 15 августа 2009-го у нас
нет, и сравнение производится только за первые 49 дней квартала
Мера Sales PQTD рассчитывается примерно так же, как Sales QTD: в
ней происходит фильтрация поля Fiscal Year Quarter Number по предыдущему значению вместо последнего квартала, видимого в контексте фильтра. Основное отличие заключается в расчете переменной
LastDayOfFiscalYearQuarterAvailable. Она должна учитывать только даты
с продажами и игнорировать фильтры по фильтрозащищенным столбцам, которые присутствуют при расчете меры Sales Amount.
Мера в таблице Sales:
Sales PQTD :=
IF (
[ShowValueForDates];
VAR PreviousFiscalYearQuarter = MAX ( 'Date'[Fiscal Year Quarter Number] ) - 1
VAR LastDayOfFiscalYearQuarterAvailable =
CALCULATE (
MAX ( 'Date'[Day of Fiscal Quarter Number] );
REMOVEFILTERS (
-- Удаляем фильтры с
'Date'[Working Day];
-- фильтрозащищенных столбцов, чтобы
'Date'[Day of Week];
-- получить последнюю дату с продажами,
'Date'[Day of Week Number] -- выбранную в отчете
);
'Date'[DateWithSales] = TRUE
)
VAR Result =
CALCULATE (
[Sales Amount];
Сравнение нарастающих итогов  127
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Fiscal Year Quarter Number] = PreviousFiscalYearQuarter;
'Date'[Day of Fiscal Quarter Number] <=
LastDayOfFiscalYearQuarterAvailable;
'Date'[DateWithSales] = TRUE
)
RETURN
Result
)
Меры Sales QOQTD и Sales QOQTD % базируются на вычислении меры
Sales PQTD.
Мера в таблице Sales:
Sales QOQTD :=
VAR ValueCurrentPeriod = [Sales QTD]
VAR ValuePreviousPeriod = [Sales PQTD]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales QOQTD % :=
DIVIDE (
[Sales QOQTD];
[Sales PQTD]
)
Недельное сравнение нарастающих итогов
Недельное сравнение нарастающих итогов позволяет сопоставить
значение нарастающего итога с начала недели до указанной даты с тем
же показателем по эквивалентной дате на предыдущей неделе. Вычисление в этом случае будет походить на аналогичный расчет по годам и
кварталам, правда, в нашем случае мы не сможем получить значения
за частичную последнюю неделю периода, в который входит 15 августа
2009 года. На рис. 4.12 показан итоговый отчет.
128
 Глава 4. Вычисления на основе недели
Рис. 4.12. В мере Sales PWTD результат в точности соответствует значению меры
Sales WTD за предыдущую неделю
Мера Sales PWTD рассчитывается похожим образом с Sales WTD: здесь
происходит фильтрация поля Fiscal Year Week Number по предыдущему значению вместо последней недели, видимой в контексте фильтра.
Главное отличие, как и раньше, заключается в расчете переменной
LastDayOfFiscalYearWeekAvailable. Она должна учитывать только даты с
продажами и игнорировать фильтры по фильтрозащищенным столбцам, которые присутствуют при расчете меры Sales Amount.
Мера в таблице Sales:
Sales PWTD :=
IF (
[ShowValueForDates];
Сравнение нарастающих итогов  129
VAR PreviousFiscalYearWeek = MAX ( 'Date'[Fiscal Year Week Number] ) - 1
VAR LastDayOfWeekAvailable =
CALCULATE (
MAX ( 'Date'[Day of Week Number] );
REMOVEFILTERS (
-- Удаляем фильтры
'Date'[Working Day];
-- с фильтрозащищенных столбцов,
'Date'[Day of Week];
-- чтобы получить последнюю дату
'Date'[Day of Week Number] -- с продажами, выбранную в отчете
);
'Date'[DateWithSales] = TRUE
)
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Fiscal Year Week Number] = PreviousFiscalYearWeek;
'Date'[Day of Week Number] <= LastDayOfWeekAvailable;
'Date'[DateWithSales] = TRUE
)
RETURN
Result
)
Меры Sales WOWTD и Sales WOWTD % базируются на вычислении
меры Sales PWTD.
Мера в таблице Sales:
Sales WOWTD :=
VAR ValueCurrentPeriod = [Sales WTD]
VAR ValuePreviousPeriod = [Sales PWTD]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales WOWTD % :=
DIVIDE (
[Sales WOWTD];
[Sales PWTD]
)
130
 Глава 4. Вычисления на основе недели
Сравнение нарастающих итогов с полным
предыдущим периодом
Эта разновидность анализа может пригодиться, если вы рассматриваете
прошлый период в качестве ориентира. При достижении нарастающим
итогом за текущий год отметки в 100 %, по сравнению с годом предыдущим, можно будет говорить, что вы вышли на прошлогодние показатели.
Сравнение нарастающих итогов за год с полным
прошлым годом
В данном разделе мы поговорим про сравнение нарастающих итогов
с начала года с полным предшествующим годом. На рис. 4.13 видно, что
за период FW48-2008 мера Sales YTD превзошла значение меры Sales
Amount за весь предыдущий финансовый год. В процентном отношении разницу можно отслеживать посредством меры Sales YTDOPY % –
она начнет демонстрировать положительные значения, когда накопительный показатель за текущий год превысит итоговое значение за
предыдущий финансовый год.
Рис. 4.13. Мера Sales YTDOPY % показывает положительные цифры, когда значение
меры Sales YTD превышает Sales Amount за предыдущий финансовый год
Сравнение нарастающих итогов с полным предыдущим периодом
производится в мерах Sales YTDOPY и Sales YTDOPY %, которые в свою
очередь строятся на основании мер Sales YTD для расчета нарастающего итога с начала года и Sales PYC – для получения итоговых продаж за
весь предшествующий год.
Сравнение нарастающих итогов с полным предыдущим периодом  131
Мера в таблице Sales:
Sales PYC :=
IF (
[ShowValueForDates] && HASONEVALUE ( 'Date'[Fiscal Year Number] );
VAR PreviousFiscalYear = MAX ( 'Date'[Fiscal Year Number] ) - 1
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Fiscal Year Number] = PreviousFiscalYear
)
RETURN
Result
)
Мера в таблице Sales:
Sales YTDOPY :=
VAR ValueCurrentPeriod = [Sales YTD]
VAR ValuePreviousPeriod = [Sales PYC]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales YTDOPY % :=
DIVIDE (
[Sales YTDOPY];
[Sales PYC]
)
Сравнение нарастающих итогов за квартал с полным
прошлым кварталом
В данном разделе речь пойдет о сравнении нарастающих итогов с начала квартала с полным предшествующим финансовым кварталом. На
рис. 4.14 показано, что за неделю FW23-2009 мера Sales QTD превысила
общие продажи (мера Sales Amount) за период FQ1-2009 (первый квар-
132
 Глава 4. Вычисления на основе недели
тал 2009 финансового года). Мера Sales QTDOPQ % обеспечивает сравнение нарастающего итога с начала квартала с общими продажами за
предыдущий квартал. При положительных значениях фиксируется рост
показателя.
Рис. 4.14. Мера Sales QTDOPQ % начала показывать положительные проценты
начиная с периода FW23-2009, когда значение меры Sales QTD перевалило за
отметку меры Sales Amount по FQ1-2009 (первому кварталу 2009 финансового года)
Сравнение нарастающих итогов с начала квартала с полным предшествующим кварталом выполняется в мерах Sales QTDOPQ и Sales
QTDOPQ %, при этом сами они опираются на меры Sales QTD для расчета нарастающего итога с начала квартала и Sales PQC – для получения
итоговых продаж за предыдущий квартал.
Мера в таблице Sales:
Sales PQC :=
IF (
[ShowValueForDates] && HASONEVALUE ( 'Date'[Fiscal Year Quarter Number] );
VAR PreviousFiscalYearQuarter = MAX ( 'Date'[Fiscal Year Quarter Number] ) - 1
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Fiscal Year Quarter Number] = PreviousFiscalYearQuarter
)
RETURN
Result
)
Сравнение нарастающих итогов с полным предыдущим периодом  133
Мера в таблице Sales:
Sales QTDOPQ :=
VAR ValueCurrentPeriod = [Sales QTD]
VAR ValuePreviousPeriod = [Sales PQC]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales QTDOPQ % :=
DIVIDE (
[Sales QTDOPQ];
[Sales PQC]
)
Сравнение нарастающих итогов за неделю с полной
прошлой неделей
Данный вид анализа обращается к сравнению нарастающих итогов с
начала недели с полной предшествующей неделей. На рис. 4.15 видно,
что на неделе FW33-2009 мера Sales WTD превысила итоговую сумму
Sales Amount за предыдущую неделю FW32-2009. Мера Sales WTDOPW %
отвечает за сравнение нарастающего итога с начала недели с общими
продажами за предыдущую неделю. Когда процент становится положительным, мы фиксируем преодоление ориентира. В нашем случае это
произошло 11 августа 2009 года.
Сравнение нарастающего итога с начала недели с полным предшествующим месяцем происходит в мерах Sales WTDOPW % и Sales
WTDOPW, которые зависят от мер Sales WTD и Sales PWC.
Мера в таблице Sales:
Sales PWC :=
IF (
[ShowValueForDates] && HASONEVALUE ( 'Date'[Fiscal Year Week Number] );
VAR PreviousFiscalYearWeek = MAX ( 'Date'[Fiscal Year Week Number] ) - 1
VAR Result =
CALCULATE (
134
 Глава 4. Вычисления на основе недели
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Fiscal Year Week Number] = PreviousFiscalYearWeek
)
RETURN
Result
)
Рис. 4.15. Мера Sales WTDOPW % демонстрирует положительные цифры
начиная с 11 августа 2009 года, когда значение Sales WTD впервые
превысило Sales Amount за период FW32-2009
Мера в таблице Sales:
Sales WTDOPW :=
VAR ValueCurrentPeriod = [Sales WTD]
VAR ValuePreviousPeriod = [Sales PWC]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Вычисление скользящей годовой суммы  135
Мера в таблице Sales:
Sales WTDOPW % :=
DIVIDE (
[Sales WTDOPW];
[Sales PWC]
)
Вычисление скользящей годовой суммы
Распространенным способом агрегировать данные за несколько месяцев
является расчет скользящей годовой суммы (moving annual total) вместо
нарастающих итогов с начала года. В недельном календаре скользящая
годовая сумма включает последние 52 недели (364 дня) данных.
Скользящая годовая сумма
На рис. 4.16 показана мера Sales MAT (364), вычисляющая скользящую
годовую сумму.
Рис. 4.16. В мере Sales MAT (364) за период FQ3-2009 агрегируются
значения Sales Amount с FQ4-2008 по FQ3-2009
Мера Sales MAT (364), рассчитывающая скользящую годовую сумму,
определяет в столбце Date[Date] временной диапазон, состоящий из всех
дней полного года, включающего последний день в контексте фильтра.
136
 Глава 4. Вычисления на основе недели
Мера в таблице Sales:
Sales MAT (364) :=
IF (
[ShowValueForDates];
VAR LastDayMAT = MAX ( 'Date'[Sequential Day Number] )
VAR FirstDayMAT = LastDayMAT - 363
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Sequential Day Number] >= FirstDayMAT
&& 'Date'[Sequential Day Number] <= LastDayMAT
)
RETURN
Result
)
Мера Sales MAT (364) может не соответствовать полному году в случае,
если в году будет больше 52 недель, что случается каждые 5–6 недель в
календаре типа 4-4-5. Но она представляет собой вполне подходящий
метод расчета трендов, поскольку всегда включает в себя одинаковое
количество дней и недель.
Сравнение скользящих годовых сумм
Сравнение скользящих годовых сумм реализовано при помощи мер
Sales PYMAT (364), Sales MATG и Sales MATG %, которые в свою очередь
основаны на мере Sales MAT (364). При этом мера Sales MAT (364) начинает показывать корректные значения лишь через год после первой
продажи (т. е. после того, как наберется годовой массив данных), а если
текущий год не полный, цифры могут быть неожиданными. Например,
для периода FY 2009 мера Sales PYMAT (364) на рис. 4.17 показывает
значение 9 788 101,45, что соответствует агрегации меры Sales Amount
за финансовый год FY 2008. Если сравнивать полученный результат с
продажами за 2009-й финансовый год, мы получим сопоставление за
период меньше шести месяцев, поскольку за 2009 год у нас есть данные
только по 15 августа. Также обратите внимание, что значения в столбце
Sales MATG % начинаются в периоде FY 2008 с очень больших величин
и стабилизируются только спустя год. Это происходит из-за отсутствия
данных о продажах за предыдущий год. Такое поведение характерно
для этого вида вычисления, поскольку скользящая годовая сумма обычно рассчитывается на уровне гранулярности по месяцам или дням, чтобы показать тренды на диаграмме.
Вычисление скользящей годовой суммы  137
Рис. 4.17. Мера Sales MATG % показывает отношение между Sales MAT (364)
и Sales PYMAT (364) в процентах
Определения представленных мер показаны ниже.
Мера в таблице Sales:
Sales PYMAT (364) :=
IF (
[ShowValueForDates];
VAR LastDayAvailable = MAX ( 'Date'[Sequential Day Number] )
VAR LastDayMAT = LastDayAvailable - 364 -- возвращаемся на 52 недели
VAR FirstDayMAT = LastDayMAT - 363
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Sequential Day Number] >= FirstDayMAT
&& 'Date'[Sequential Day Number] <= LastDayMAT
)
RETURN
Result
)
Мера в таблице Sales:
Sales MATG :=
VAR ValueCurrentPeriod = [Sales MAT (364)]
VAR ValuePreviousPeriod = [Sales PYMAT (364)]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
138
 Глава 4. Вычисления на основе недели
)
RETURN
Result
Мера в таблице Sales:
Sales MATG % :=
DIVIDE (
[Sales MATG];
[Sales PYMAT (364)]
)
Скользящее среднее
Скользящее среднее обычно используется для отображения линий
трендов на диаграммах. На рис. 4.18 демонстрируется показатель скользящего среднего для меры Sales Amount за последние четыре недели
(Sales AVG 4W), квартал (Sales AVG 1Q) и финансовый год (Sales AVG 1Y).
Рис. 4.18. Меры Sales AVG 4W, Sales AVG 1Q и Sales AVG 1Y демонстрируют
скользящее среднее за четыре недели, квартал и год соответственно
Скользящее среднее за четыре недели
В мере Sales AVG 4W рассчитывается скользящее среднее за четыре
недели путем прохода по последним 28 дням, собранным в переменной
Period4W. При этом в переменной Period4W реализованы два исключения при извлечении дней: во-первых, игнорируются дни без продаж,
Скользящее среднее  139
во-вторых, применяются существующие фильтры в фильтрозащищенных столбцах в таблице Date.
Мера в таблице Sales:
Sales AVG 4W :=
IF (
[ShowValueForDates];
VAR LastDayMAT =
MAX ( 'Date'[Sequential Day Number] )
VAR FirstDayMAT = LastDayMAT - 27
VAR Period4W =
CALCULATETABLE (
VALUES ( 'Date'[Sequential Day Number] );
ALLEXCEPT (
'Date';
'Date'[Working Day];
'Date'[Day of Week]
);
'Date'[Sequential Day Number] >= FirstDayMAT
&& 'Date'[Sequential Day Number] <= LastDayMAT;
'Date'[DateWithSales] = TRUE
)
VAR FirstDayWithData =
CALCULATE (
MIN ( Sales[Order Date] );
REMOVEFILTERS ()
)
VAR FirstDayInPeriod =
MINX (
Period4W;
'Date'[Sequential Day Number]
)
VAR Result =
IF (
FirstDayWithData <= FirstDayInPeriod;
CALCULATE (
AVERAGEX ( Period4W; [Sales Amount] );
REMOVEFILTERS ( 'Date' )
)
)
RETURN
Result
)
Это довольно гибкий шаблон, поскольку прекрасно работает и с неаддитивными мерами. В то же время для традиционных аддитивных мер
переменная Result может быть реализована с использованием следующей более быстрой формулы:
140
 Глава 4. Вычисления на основе недели
VAR Result =
IF (
FirstDayWithData <= FirstDayInPeriod;
CALCULATE (
DIVIDE (
[Sales Amount];
DISTINCTCOUNT ( Sales[Order Date] )
);
REMOVEFILTERS ( 'Date' );
Period4W
)
)
Скользящее среднее за квартал
В мере Sales AVG 1Q рассчитывается скользящее среднее за последние 13 недель при помощи итераций по дням из последнего квартала, собранным в переменной Period1Q. При этом извлечение данных за
последние 13 недель (91 день) происходит с двумя оговорками: игнорируются дни без продаж и применяются существующие фильтры в
фильтрозащищенных столбцах в таблице Date.
Мера в таблице Sales:
Sales AVG 1Q :=
IF (
[ShowValueForDates];
VAR LastDayMAT =
MAX ( 'Date'[Sequential Day Number] )
VAR FirstDayMAT = LastDayMAT - 13 * 7 + 1
VAR Period1Q =
CALCULATETABLE (
VALUES ( 'Date'[Sequential Day Number] );
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Sequential Day Number] >= FirstDayMAT
&& 'Date'[Sequential Day Number] <= LastDayMAT;
'Date'[DateWithSales] = TRUE
)
VAR FirstDayWithData =
CALCULATE (
MIN ( Sales[Order Date] );
REMOVEFILTERS ()
)
VAR FirstDayInPeriod =
MINX (
Period1Q;
'Date'[Sequential Day Number]
Скользящее среднее  141
)
VAR Result =
IF (
FirstDayWithData <= FirstDayInPeriod;
CALCULATE (
AVERAGEX ( Period1Q; [Sales Amount] );
REMOVEFILTERS ( 'Date' )
)
)
RETURN
Result
)
Для простых аддитивных мер может быть также использован шаблон
на основе функции DIVIDE из предыдущего раздела.
Скользящее среднее за год
В мере Sales AVG 1Y рассчитывается скользящее среднее за последний
год путем осуществления итераций по 364 последним дням, собранным
в переменной Period1Y. При этом извлекаются только даты, входящие в
последний финансовый год (52 недели), а также игнорируются дни без
продаж и применяются существующие фильтры в фильтрозащищенных столбцах в таблице Date.
Мера в таблице Sales:
Sales AVG 1Y :=
IF (
[ShowValueForDates];
VAR LastDayMAT =
MAX ( 'Date'[Sequential Day Number] )
VAR FirstDayMAT = LastDayMAT - 363
VAR Period1Y =
CALCULATETABLE (
VALUES ( 'Date'[Sequential Day Number] );
ALLEXCEPT (
'Date';
'Date'[Working Day];
'Date'[Day of Week]
);
'Date'[Sequential Day Number] >= FirstDayMAT
&& 'Date'[Sequential Day Number] <= LastDayMAT;
'Date'[DateWithSales] = TRUE
)
VAR FirstDayWithData =
CALCULATE (
MIN ( Sales[Order Date] );
142
 Глава 4. Вычисления на основе недели
REMOVEFILTERS ()
)
VAR FirstDayInPeriod =
MINX (
Period1Y;
'Date'[Sequential Day Number]
)
VAR Result =
IF (
FirstDayWithData <= FirstDayInPeriod;
CALCULATE (
AVERAGEX ( Period1Y; [Sales Amount] );
REMOVEFILTERS ( 'Date' )
)
)
RETURN
Result
)
Для простых аддитивных мер здесь также может быть использован
шаблон на основе функции DIVIDE из предыдущего раздела, но с расчетом на 364 дня.
Глава
5
Пользовательские вычисления,
связанные со временем
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Данный шаблон создан для выполнения вычислений, связанных с логикой операций со временем, таких как нарастающий итог с начала года,
сравнение аналогичных периодов в разных годах и прочих, при наличии пользовательского календаря. В этом случае, как вы понимаете, мы
не сможем полагаться на набор стандартных функций логики операций
со временем, присутствующих в DAX. Все меры здесь будут вычисляться
для финансового календаря, поскольку для григорианского вы всегда
можете воспользоваться шаблоном стандартных вычислений в области
времени.
Существует множество сценариев, в которых встроенные функции
DAX для работы с логикой времени просто не подойдут. К примеру, если
ваш финансовый год начинается не с января, апреля, июля или октября, вы не сможете воспользоваться стандартными функциям DAX для
квартальных вычислений. В этих случаях вам придется самим проектировать логику операций со временем с использованием обычных функций, входящих в состав языка DAX, вроде FILTER и CALCULATE. Кроме
того, вам необходимо будет создать таблицу Date, содержащую дополнительные столбцы для расчета временных периодов, таких как предыдущий квартал или целый год. Стандартные функции логики операций
со временем извлекают эту информацию из столбца Date таблицы дат.
В пользовательском шаблоне это реализовать невозможно, в связи с
чем вам и потребуется создавать дополнительные колонки.
144
 Глава 5. Пользовательские вычисления, связанные со временем
Меры из данного шаблона будут работать с григорианским календарем при выполнении следующих условий:
• годы и кварталы должны начинаться с первого дня месяца;
• месяц всегда представляет собой календарный месяц.
Иными словами, этот шаблон работает, если финансовый год начинается с первого дня месяца, а квартал состоит из трех календарных месяцев. Например, если первым днем финансового года является 3 марта
или все финансовые кварталы содержат по 90 дней, формулы, приведенные в данном шаблоне, работать не будут.
Примером календаря, не удовлетворяющего требованиям такого шаблона, является недельный календарь. Если вам необходимо провести
вычисления на базе периодов, основанных на неделях, вы должны воспользоваться описанным ранее шаблоном для недель.
Введение в пользовательские вычисления,
связанные со временем
Пользовательские вычисления, реализованные в представленном
здесь шаблоне, модифицируют контекст фильтра в таблице Date для
получения требуемого результата. Формулы разработаны так, чтобы
фильтры применялись к максимально низкому уровню гранулярности, при котором запросы будут выполняться наиболее эффективно.
Например, вычисление на основе месяцев работает путем изменения
контекста фильтра на уровне месяцев, а не отдельных дней. Такая техника позволяет повысить эффективность вычисления нового фильтра
и применения его к текущему контексту. Подобная оптимизация наиболее полезна при использовании режима DirectQuery, но и с загруженной в память моделью данных будет ощущаться прирост быстродействия.
Шаблон вычислений на основе месяца не задействует стандартные
функции логики операций со временем, поэтому на таблицу Date не
накладываются столь серьезные ограничения, как в случае с использованием этих функций.
Например, можно пометить таблицу Date как таблицу дат, но в этом
нет особой необходимости. Формулы в рассматриваемом шаблоне не
полагаются на автоматическое применение функции REMOVEFILTERS
к таблице Date, когда столбец Date отфильтрован. Вместо этого в таблице располагаются дополнительные столбцы, требуемые для расчета
мер. Так что даже если в вашей модели данных уже присутствует таблица дат, вам все равно необходимо прочитать следующих раздел, чтобы
убедиться, что в ней присутствуют все нужные столбцы.
Введение в пользовательские вычисления, связанные со временем  145
Создание таблицы дат
Таблица Date для использования в пользовательских вычислениях,
связанных со временем, базируется на месяцах стандартного григорианского календаря.
Если у вас уже есть готовая таблица Date, вы можете просто импортировать ее, попутно расширив за счет столбцов с информацией, необходимой для формул DAX. Мы опишем эти столбцы позже в этой главе.
Если же у вас таблицы дат еще нет, вы легко можете создать ее в виде
вычисляемой таблицы в DAX. В качестве примера представим выражение DAX, описывающее таблицу Date, используемую в данном шаблоне, – с финансовым годом, начинающимся 1 марта.
Вычисляемая таблица:
Date =
VAR FirstFiscalMonth = 3
-- Первый месяц финансового года
VAR FirstDayOfWeek = 0
-- 0 = воскресенье, 1 = понедельник, ...
VAR FirstSalesDate = MIN ( Sales[Order Date] )
VAR LastSalesDate = MAX ( Sales[Order Date] )
VAR FirstFiscalYear =
-- Настройка первого финансового года
YEAR ( FirstSalesDate )
-- для использования
+ 1 * ( MONTH ( FirstSalesDate ) >= FirstFiscalMonth
&& FirstFiscalMonth > 1)
VAR LastFiscalYear =
-- Настройка последнего финансового года
YEAR ( LastSalesDate )
-- для использования
+ 1 * ( MONTH ( LastSalesDate ) >= FirstFiscalMonth
&& FirstFiscalMonth > 1)
RETURN
GENERATE (
VAR FirstDay =
DATE (
FirstFiscalYear - 1 * (FirstFiscalMonth > 1);
FirstFiscalMonth;
1
)
VAR LastDay =
DATE (
LastFiscalYear + 1 * (FirstFiscalMonth = 1);
FirstFiscalMonth; 1
) - 1
RETURN
CALENDAR ( FirstDay; LastDay );
VAR
VAR
VAR
VAR
VAR
CurrentDate = [Date]
Yr = YEAR ( CurrentDate )
Mn = MONTH ( CurrentDate )
Mdn = DAY ( CurrentDate )
DateKey = Yr*10000+Mn*100+Mdn
-- Номер года
-- Номер месяца (1-12)
-- День месяца
146
 Глава 5. Пользовательские вычисления, связанные со временем
VAR Wd =
-- Номер дня недели (0 = воскресенье, 1 = понедельник, ...)
WEEKDAY ( CurrentDate + 7 - FirstDayOfWeek; 1 )
VAR WorkingDay =
-- Рабочий день (1 = рабочий, 0 = нерабочий)
( WEEKDAY ( CurrentDate; 1 ) IN { 2; 3; 4; 5; 6 } )
VAR Fyr =
-- Номер финансового года
Yr + 1 * ( FirstFiscalMonth > 1 && Mn >= FirstFiscalMonth )
VAR Fmn =
-- Номер финансового месяца (1-12)
Mn - FirstFiscalMonth + 1 + 12 * (Mn < FirstFiscalMonth)
VAR Fqrn =
-- Финансовый квартал
ROUNDUP ( Fmn / 3; 0 )
VAR Fmqn =
MOD ( FMn - 1; 3 ) + 1
VAR Fqr =
-- Финансовый квартал (строка)
FORMAT ( Fqrn; "\Q0" )
VAR FirstDayOfYear =
DATE ( Fyr - 1 * (FirstFiscalMonth > 1); FirstFiscalMonth; 1 )
VAR Fydn =
SUMX (
CALENDAR ( FirstDayOfYear; CurrentDate );
1 * ( MONTH ( [Date] ) <> 2 || DAY ( [Date] ) <> 29 )
)
RETURN ROW (
"DateKey"; INT ( DateKey );
"Sequential Day Number"; INT ( [Date] );
"Year Month"; FORMAT ( CurrentDate; "mmm yyyy" );
"Year Month Number"; Yr * 12 + Mn - 1;
"Fiscal Year"; "FY " & Fyr;
"Fiscal Year Number"; Fyr;
"Fiscal Year Quarter"; "F" & Fqr & "-" & Fyr;
"Fiscal Year Quarter Number"; CONVERT ( Fyr * 4 + FQrn - 1; INTEGER );
"Fiscal Quarter"; "F" & Fqr;
"Month"; FORMAT ( CurrentDate; "mmm" );
"Fiscal Month Number"; Fmn;
"Fiscal Month in Quarter Number"; Fmqn;
"Day of Week"; FORMAT ( CurrentDate; "ddd" );
"Day of Week Number"; Wd;
"Day of Month Number"; Mdn;
"Day of Fiscal Year Number"; Fydn;
"Working Day"; IF ( WorkingDay; "Working Day"; "Non-Working Day" )
)
)
Первые две переменные используются для настройки начала финансового года и недели. В следующих переменных определяется диапазон
требуемых в расчетах финансовых лет на основе транзакций из таблицы
Sales. Вы должны настроить переменные FirstSalesDate и LastSalesDate
таким образом, чтобы они извлекали из вашей конкретной модели
данных первую и последнюю дату транзакции. Или вы можете вруч-
Введение в пользовательские вычисления, связанные со временем  147
ную настроить диапазон финансовых лет в переменных FirstFiscalYear
и LastFiscalYear.
Кварталы рассчитываются начиная с первого месяца финансового
года. Таблица Date содержит скрытые столбцы для поддержки правильной сортировки лет, кварталов и месяцев. В этих столбцах содержатся
последовательности чисел, облегчающие применение фильтров для
извлечения предыдущих и последующих лет, кварталов и месяцев без
произведения громоздких вычислений во время выполнения запросов.
Среди прочих стоит особо отметить столбец с названием Year Month
Number. В нем содержится номер года, умноженный на 12, с добавлением номера месяца. Итоговое значение мало о чем говорит, но оно
позволяет облегчить проведение математических операций с месяцами. Например, чтобы перенестись в расчетах ровно на год назад, достаточно из числа, содержащегося в этой колонке, отнять 12. Во многих
формулах мы будем использовать подобную нехитрую математику для
осуществления временных сдвигов.
Чтобы получить корректную визуализацию данных, календарные
столбцы должны быть настроены в модели данных так, как показано
ниже. Для каждого столбца мы привели ее тип данных, строку форматирования и пример заполнения:
• Date: Date, m/dd/yyyy (8/14/2007), используется в качестве столбца
для пометки Date как таблицы дат, что необязательно;
• DateKey: Whole Number, (20070814), используется в качестве альтернативного ключа для связей;
• Sequential Day Number: Whole Number, Hidden (40040), то же, что и
Date, но в целочисленном формате;
• Year Month: Text (Aug 2007);
• Year Month Number: Whole Number, Hidden (24091);
• Month: Text (Aug);
• Fiscal Month Number: Whole Number, Hidden (6);
• Fiscal Month in Quarter Number: Whole Number, Hidden (3);
• Fiscal Year: Text (FY 2008);
• Fiscal Year Number: Whole Number, Hidden (2008);
• Fiscal Year Quarter: Text (FQ2-2008);
• Fiscal Year Quarter Number: Whole Number, Hidden (8033);
• Fiscal Quarter: Text (FQ2);
• Day of Fiscal Year Number: Whole Number, Hidden (167);
• Day of Month Number: Whole Number, Hidden (14).
Также в таблице должно присутствовать несколько фильтрозащищенных столбцов. Это столбцы, фильтры по которым должны всегда
сохраняться. Фильтры по ним не изменяются в результате вычислений,
148
 Глава 5. Пользовательские вычисления, связанные со временем
связанных с логикой операций со временем. При этом они будут влиять
на вычисления, представленные в данном шаблоне. В нашей таблице
дат фильтрозащищенными столбцами являются следующие:
• Day of Week: ddd (Tue);
• Day of Week Number: Whole Number, Hidden (6);
• Working Day: Text (Working Day).
В следующем разделе мы опишем работу фильтрозащищенных
столбцов.
Таблица Date в нашем примере обладает одной иерархией: Fiscal:
Year (Fiscal Year), Quarter (Fiscal Year Quarter), Month (Year Month).
Многие столбцы созданы с единственной целью упростить процесс
написания формул. Например, столбец Day of Fiscal Year Number содержит количество дней, прошедших с начала финансового года, игнорируя 29 февраля в високосных годах, и облегчает процесс поиска соответствующего диапазона дат в предшествующем году.
Таблица Date также должна включать скрытый вычисляемый столбец
DateWithSales, который будет использован в формулах этого шаблона.
Вычисляемый столбец в таблице Date:
DateWithSales =
'Date'[Date] <= MAX ( Sales[Order Date] )
Значение в столбце Date[DateWithSales] будет истинным (TRUE), если
дата попадает в период, когда осуществлялись продажи, и ложным
(FALSE) в противном случае. Иными словами, поле DateWithSales будет
содержать TRUE для «прошлых» дат и FALSE – для «будущих», где прошлое и будущее рассчитывается относительно последнего дня, в который
были транзакции в таблице Sales.
Если вы импортируете таблицу дат, позаботьтесь о ее расширении за
счет вспомогательных столбцов, которые мы описываем в данном шаблоне.
Фильтрозащищенные столбцы
Таблица Date содержит два типа столбцов: обычные и фильтрозащищенные. К обычным столбцам обращаются меры, реализованные
в данном шаблоне. В то же время фильтры, наложенные на фильтрозащищенные столбцы, сохраняют свое значение и не модифицируются
при помощи мер. Следующий пример поможет вам понять, о чем речь.
Столбец Year Month Number является обычным, и формулы в этом шаблоне способны изменять его значения в процессе вычисления. Допус-
Введение в пользовательские вычисления, связанные со временем  149
тим, чтобы получить предыдущий месяц, формула изменяет контекст
фильтра путем вычитания единицы из значения столбца Year Month
Number. Столбец Day of Week, напротив, является фильтрозащищенным.
Если установить фильтр с понедельника по пятницу, формулы не смогут изменить его в момент вычисления. Таким образом, мера, вычисляющая предыдущий год, сохранит значение фильтра по дням недели и
изменит только фильтр по календарным столбцам, таким как год, месяц и день.
Для реализации данного шаблона вам необходимо понять, какие
именно столбцы должны стать фильтрозащищенными, поскольку они
требуют иного подхода. Ниже приведена классификация столбцов в
таблице Date в представленном здесь шаблоне:
• календарные столбцы: Date, DateKey, Sequential Day Number, Year
Month, Year Month Number, Month, Fiscal Month Number, Fiscal Month
in Quarter Number, Fiscal Year, Fiscal Year Number, Fiscal Year Quarter,
Fiscal Year Quarter Number, Fiscal Quarter, Day of Fiscal Year Number,
Day of Month Number;
• фильтрозащищенные столбцы: Day of Week, Day of Week Number, Working Day.
Особый подход к фильтрозащищенным столбцам, о котором мы упоминали ранее, напрямую связан с контекстом фильтра. Все меры в этом
шаблоне влияют на контекст фильтра, модифицируя фильтр по календарным столбцам, но при этом не затрагивают фильтрозащищенные
столбцы. Иными словами, каждая мера следует двум правилам:
• удалять фильтры с календарных столбцов;
• сохранять фильтры по фильтрозащищенным столбцам.
Помочь реализовать эту концепцию призвана функция ALLEXCEPT,
если ей первым аргументом передать таблицу дат, а следующими –
фильтрозащищенные столбцы:
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
... // Фильтры по одному или больше календарным столбцам
)
Если таблица дат не имеет в своем составе фильтрозащищенных столбцов, фильтры могут быть удалены при помощи функции REMOVEFILTERS по таблице Date вместо применения функции
ALLEXCEPT:
Powered by TCPDF (www.tcpdf.org)
150
 Глава 5. Пользовательские вычисления, связанные со временем
CALCULATE (
[Sales Amount];
REMOVEFILTERS ( 'Date' );
... // Фильтры по одному или больше календарным столбцам
)
Если в вашей таблице Date отсутствуют фильтрозащищенные столбцы,
вы можете использовать функцию REMOVEFILTERS вместо ALLEXCEPT
во всех мерах шаблона. Мы предлагаем полноценный сценарий, с фильтрозащищенными столбцами. Но вы всегда можете его упростить.
Хотя в функцию ALLEXCEPT должны быть включены все без исключения фильтрозащищенные столбцы, мы не стали добавлять скрытые
столбцы, используемые только для сортировки других колонок. Например, мы не включили в список скрытый столбец Day of Week Number,
служащий для сортировки столбца Day of Week. Мы предполагаем,
что пользователь никогда не применяет фильтры к скрытым столбцам. В противном случае скрытые фильтрозащищенные столбцы также должны быть включены в число аргументов функции ALLEXCEPT.
Вы можете найти примеры использования функций REMOVEFILTERS
и ALLEXCEPT в разделе, посвященном нарастающим итогам с начала
года далее в этом шаблоне.
Управление визуализациями для будущих дат
В большинстве случаев вычисления, связанные с логикой операций
со временем, не должны демонстрировать значения показателей для
будущих периодов. Допустим, расчет нарастающего итога с начала года
в принципе может производиться и для дат будущих периодов, но эти
значения нам бы показывать не хотелось. Набор данных, используемый
в наших примерах, ограничивается 15 августа 2009 года. Таким образом, мы рассматриваем август 2009-го, третий квартал того года и сам
2009 год в качестве последних периодов с данными. Любая дата позднее 15 августа 2009 года будет рассматриваться как будущая, и значения
по ней выводиться не будут.
Чтобы избежать показа значений в будущих периодах, мы будем
пользоваться мерой ShowValueForDates, которая будет возвращать TRUE,
если выбранный временной период располагается раньше последнего
периода с данными.
Мера (скрытая) в таблице дат:
ShowValueForDates :=
VAR LastDateWithData =
Введение в пользовательские вычисления, связанные со временем  151
CALCULATE (
MAX ( 'Sales'[Order Date] );
REMOVEFILTERS ()
)
VAR FirstDateVisible =
MIN ( 'Date'[Date] )
VAR Result =
FirstDateVisible <= LastDateWithData
RETURN
Result
Мера ShowValueForDates – скрытая. Это техническая мера, созданная
для использования одной и той же логики в различных вычислениях,
связанных с операциями со временем. Пользователь не должен использовать эту меру в отчетах напрямую. Модификатор REMOVEFILTERS
применяется здесь для удаления фильтров со всех таблиц в модели данных, поскольку нам нужно извлечь последнюю дату, присутствующую в
таблице Sales, вне зависимости от фильтров.
Соглашение об именованиях
В табл. 5.1 перечислены наименования, принятые для обозначения
вычислений, базирующихся на логике операций со временем. В столбцах отображена информация о том:
• сдвигается ли вычисление во времени, например для определения аналогичного периода в прошлом году;
• выполняется ли агрегация при вычислении, например как в случае с нарастающим итогом с начала года;
• производится ли сравнение двух временных периодов: допустим,
текущего года с прошлым.
Таблица 5.1. Принятые обозначения вычислений, основанных на логике операций
со временем
Сокращение
Описание
Сдвиг Агрегация Сравнение
YTD
Year-to-date
(нарастающий итог с начала года)
X
QTD
Quarter-to-date
(нарастающий итог с начала квартала)
X
MTD
Month-to-date
(нарастающий итог с начала месяца)
X
MAT
Moving annual total
(скользящая годовая сумма)
X
152
 Глава 5. Пользовательские вычисления, связанные со временем
Сокращение
Описание
Сдвиг Агрегация Сравнение
PY
Previous year
(предыдущий год)
X
PQ
Previous quarter
(предыдущий квартал)
X
PM
Previous month
(предыдущий месяц)
X
PYC
Previous year complete
(предыдущий год полный)
X
PQC
Previous quarter complete (
предыдущий квартал полный)
X
PMC
Previous month complete
(предыдущий месяц полный)
X
PP
Previous period
(прошлый период) (автоматически
выбираются год, квартал или месяц)
X
PYMAT
Previous year moving annual total
(скользящая годовая сумма за прошлый год)
X
YOY
Year-over-year
(годовое сравнение)
X
QOQ
Quarter-over-quarter
(квартальное сравнение)
X
MOM
Month-over-month
(месячное сравнение)
X
MATG
Moving annual total growth
(скользящая годовая сумма, прирост)
POP
Period-over-period
(сравнение периодов) (автоматически
выбираются год, квартал или месяц)
X
X
X
X
X
PYTD
Previous year-to-date
(прошлогодний нарастающий итог с начала года)
X
X
PQTD
Previous quarter-to-date (прошлогодний
нарастающий итог с начала квартала)
X
X
PMTD
Previous month-to-date (прошлогодний
нарастающий итог с начала месяца)
X
X
YOYTD
Year-over-year-to-date (годовое сравнение
нарастающим итогом)
X
X
X
QOQTD
Quarter-over-quarter-to-date
(квартальное сравнение нарастающим итогом)
X
X
X
Вычисление нарастающих итогов  153
Сокращение
Описание
Сдвиг Агрегация Сравнение
MOMTD
Month-over-month-to-date
(месячное сравнение нарастающим итогом)
X
X
X
YTDOPY
Year-to-date-over-previous-year
(нарастающий итог с начала года
по сравнению с предыдущим годом)
X
X
X
Quarter-to-date-over-previous-quarter
(нарастающий итог с начала квартала
по сравнению с предыдущим кварталом)
X
X
X
Month-to-date-over-previous-month
(нарастающий итог с начала месяца
по сравнению с предыдущим месяцем)
X
X
X
QTDOPQ
MTDOPM
Вычисление нарастающих итогов
Вычисление нарастающего итога с начала года, квартала и месяца изменяет контекст фильтра таблицы дат, оставляя значения с начала периода до последней даты, доступной в контексте фильтра.
Нарастающие итоги с начала года
Вычисление нарастающего итога с начала года агрегирует значения
начиная с первой даты финансового года, как показано на рис. 5.1.
Рис. 5.1. В столбце Sales YTD (simple) демонстрируются значения для всех
периодов без исключения, тогда как в Sales YTD скрыты цифры за будущие даты
154
 Глава 5. Пользовательские вычисления, связанные со временем
В мере выделяются все даты, предшествующие последней дате, видимой в последнем финансовом году, включая ее саму. Также выбирается последний видимый финансовый год путем наложения фильтра на
столбец Fiscal Year Number.
Мера в таблице Sales:
Sales YTD (simple) :=
VAR LastDayAvailable = MAX ( 'Date'[Day of Fiscal Year Number] )
VAR LastFiscalYearAvailable = MAX ( 'Date'[Fiscal Year Number] )
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Day of Fiscal Year Number] <= LastDayAvailable;
'Date'[Fiscal Year Number] = LastFiscalYearAvailable
)
RETURN
Result
Поскольку LastDayAvailable содержит последнюю видимую дату в
контексте фильтра, в мере Sales YTD (simple) демонстрируются даты
даже для будущих периодов в году. В мере Sales YTD мы исключим такое
поведение – просто будем возвращать только те данные, для которых
значение поля ShowValueForDates будет равно TRUE.
Мера в таблице Sales:
Sales YTD :=
IF (
[ShowValueForDates];
VAR LastDayAvailable = MAX ( 'Date'[Day of Fiscal Year Number] )
VAR LastFiscalYearAvailable = MAX ( 'Date'[Fiscal Year Number] )
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Day of Fiscal Year Number] <= LastDayAvailable;
'Date'[Fiscal Year Number] = LastFiscalYearAvailable
)
RETURN
Result
)
Функция ALLEXCEPT здесь нужна, чтобы сохранить значения фильтрозащищенных столбцов Working Day или Day of Week в случае, если
Вычисление нарастающих итогов  155
понадобится выводить их в отчете. Для демонстрации этого создадим
заведомо некорректную меру Sales YTD (wrong), в которой будут удаляться фильтры с таблицы Date посредством функции REMOVEFILTERS
вместо ALLEXCEPT. При этом формула потеряла фильтр по полю Working
Day, использующийся в столбцах матрицы, что привело к ошибочным
результатам.
Мера в таблице Sales:
Sales YTD (wrong) :=
IF (
[ShowValueForDates];
VAR LastDayAvailable = MAX ( 'Date'[Day of Fiscal Year Number] )
VAR LastFiscalYearAvailable = MAX ( 'Date'[Fiscal Year Number] )
VAR Result =
CALCULATE (
[Sales Amount];
REMOVEFILTERS ( 'Date' );
'Date'[Day of Fiscal Year Number] <= LastDayAvailable;
'Date'[Fiscal Year Number] = LastFiscalYearAvailable
)
RETURN
Result
)
На рис. 5.2 показаны отличия в выводе правильной и ошибочной
меры.
Рис. 5.2. В мере Sales YTD (wrong) показан расчет меры Sales YTD
с игнорированием фильтра по полю Working Day
Мера Sales YTD (wrong) показывала бы корректные результаты, если
бы в таблице Date не присутствовали фильтрозащищенные столбцы. Их наличие требует применения функции ALLEXCEPT вместо
REMOVEFILTERS. Мы использовали меру Sales YTD просто в качестве
156
 Глава 5. Пользовательские вычисления, связанные со временем
примера, но те же концепции могут быть распространены на остальные
меры в этом шаблоне.
Нарастающие итоги с начала квартала
Вычисление нарастающего итога с начала квартала суммирует значения
начиная с первого дня финансового квартала, как показано на рис. 5.3.
Рис. 5.3. В столбце Sales QTD показан нарастающий итог
с начала квартала, который на уровне FY 2010 не заполнен,
поскольку для четвертого квартала (FQ4-2010) нет данных
Нарастающий итог с начала квартала рассчитывается с помощью той
же техники, что и с начала года. Разница только в том, что в фильтрах
указано поле Fiscal Year Quarter Number вместо Fiscal Year Number.
Мера в таблице Sales:
Sales QTD :=
IF (
[ShowValueForDates];
VAR LastDayAvailable = MAX ( 'Date'[Day of Fiscal Year Number] )
VAR LastFiscalYearQuarterAvailable = MAX ( 'Date'[Fiscal Year Quarter Number] )
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Day of Fiscal Year Number] <= LastDayAvailable;
'Date'[Fiscal Year Quarter Number] = LastFiscalYearQuarterAvailable
)
RETURN
Result
)
Вычисление нарастающих итогов  157
Нарастающие итоги с начала месяца
Вычисление нарастающего итога с начала месяца агрегирует значения
начиная с первого дня финансового месяца, как показано на рис. 5.4.
Рис. 5.4. В мере Sales MTD показан нарастающий итог с начала месяца,
который на уровне FY 2010 пуст, поскольку после 15 августа 2009 года нет данных
Нарастающий итог с начала месяца рассчитывается с помощью той же
техники, что и с начала года или квартала – с фильтрацией всех дат раньше последней даты в последнем месяце, включая ее саму. Фильтры накладываются также на столбцы Day of Month Number и Year Month Number.
Мера в таблице Sales:
Sales MTD :=
IF (
[ShowValueForDates];
VAR LastDayAvailable = MAX ( 'Date'[Day of Month Number] )
VAR LastFiscalYearMonthAvailable = MAX ( 'Date'[Year Month Number] )
VAR Result =
158
 Глава 5. Пользовательские вычисления, связанные со временем
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Day of Month Number] <= LastDayAvailable;
'Date'[Year Month Number] = LastFiscalYearMonthAvailable
)
RETURN
Result
)
В этой мере фильтруется столбец Day of Month Number вместо Day of
Fiscal Year Number. Суть в том, чтобы осуществлять фильтрацию столбца
с меньшим количеством уникальных значений, что всегда положительно сказывается на производительности запросов. В вычислении нарастающих итогов с начала квартала мы не делали подобную оптимизацию, поскольку в том случае прирост производительности запросов
был бы невелик.
Сравнение периодов
Зачастую нам необходимо сравнивать значения показателей за определенный период времени с аналогичным периодом в прошлом году,
квартале или месяце. При этом последний год/квартал/месяц может
быть незавершенным. Для корректного сравнения необходимо, чтобы
сравниваемые периоды были сопоставимы. Поэтому в вычислениях,
представленных в данном разделе, используется вычисляемый столбец Date[DateWithSales], о котором подробно можно почитать по адресу
https://sql.bi/78171.
Годовое сравнение
Вычисление годового сравнения позволяет соотнести аналогичные
периоды в текущем и прошлом годах. В нашем примере данные по продажам содержатся по 15 августа 2009 года. Именно поэтому в мере Sales
PY, относящейся к периоду FY 2010, учитываются транзакции только
до 15 августа 2008 года. На рис. 5.5 видно, что мера Sales Amount за август 2008 года показала 721 560,95, тогда как в мере Sales PY по августу
2009-го стоит сумма 296 529,51. Причина в том, что в вычислении участвуют только продажи до 15 августа 2008 года включительно.
При реализации меры Sales PY была использована стандартная техника временного сдвига, смещающая выборку на определенное количество месяцев, заданное в переменной MonthsOffset. В данном случае
эта переменная равна 12, чтобы осуществить сдвиг назад ровно на год.
В коде, используемом для мер Sales PQ и Sales PM, значение переменной
MonthsOffset будет уже другим.
Сравнение периодов  159
Рис. 5.5. За август 2009 года мера Sales PY отражает продажи
с 1 по 15 августа 2008 года, поскольку после этой даты
в 2009-м информации нет
В мере Sales PY выполняется проход по всем активным месяцам в текущем контексте фильтра. Для каждого месяца выполняется проверка,
соответствуют ли дни, выбранные в месяце, всем дням в месяце, учитывая фильтрозащищенные столбцы (в нашем примере это Working Day
и Day of Week). Если это так, значит, текущий контекст фильтра содержит полный месяц, и фильтр сдвигается на предыдущий полный месяц.
Если же были выбраны не все дни, это может означать, что пользователь
установил один или несколько фильтров на календарных столбцах, тем
самым выбрав неполный месяц. В этом случае выбранные дни сдвигаются назад в соответствующем месяце предыдущего года. Фильтр по
столбцу Date[DateWithSales] обеспечивает сопоставимое сравнение в
плане наличия продаж.
Мера в таблице Sales:
Sales PY :=
VAR MonthsOffset = 12
RETURN IF (
[ShowValueForDates];
SUMX (
SUMMARIZE ( 'Date'; 'Date'[Year Month Number] );
VAR CurrentYearMonthNumber = 'Date'[Year Month Number]
VAR PreviousYearMonthNumber = CurrentYearMonthNumber - MonthsOffset
VAR DaysOnMonth =
160
 Глава 5. Пользовательские вычисления, связанные со временем
CALCULATE (
COUNTROWS ( 'Date' );
ALLEXCEPT (
'Date';
'Date'[Year Month Number]; -- Гранулярность Year Month
'Date'[Working Day];
-- Фильтрозащищенный столбец из Date
'Date'[Day of week]
-- Фильтрозащищенный столбец из Date
)
)
VAR DaysSelected =
CALCULATE (
COUNTROWS ( 'Date' );
'Date'[DateWithSales] = TRUE
)
RETURN IF (
DaysOnMonth = DaysSelected;
-- Выбор всех дней в месяце
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Year Month Number] = PreviousYearMonthNumber
);
-- Выбор не всех дней в месяце
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Year Month Number] = PreviousYearMonthNumber;
CALCULATETABLE (
VALUES ( 'Date'[Day of Month Number] );
ALLEXCEPT (
-- Удаляем фильтры со всех
'Date';
-- столбцов, не имеющих уровень
'Date'[Day of Month Number]; -- гранулярности день, сохраняя
'Date'[Date]
-- только столбцы Date
);
-- и Day of Month Number
'Date'[Year Month Number] = CurrentYearMonthNumber;
'Date'[DateWithSales] = TRUE
)
)
)
)
)
Годовое сравнение в абсолютном выражении вычисляется в мере
Sales YOY, а в процентном – в мере Sales YOY %. При этом обе меры
в качестве основы используют меру Sales PY для учета дат только до
15 августа.
Сравнение периодов  161
Мера в таблице Sales:
Sales YOY :=
VAR ValueCurrentPeriod = [Sales Amount]
VAR ValuePreviousPeriod = [Sales PY]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales YOY % :=
DIVIDE (
[Sales YOY];
[Sales PY]
)
Квартальное сравнение
При квартальном сравнении определяется соответствие между текущим периодом и аналогичным в предыдущем квартале. В нашем наборе данных информация есть по 15 августа 2009 года, т. е. по 15-й день
третьего месяца по втором квартале финансового года FY 2010. Таким
образом, в мере Sales PQ за август 2009-го показаны продажи вплоть до
15 мая 2009 года, что эквивалентно 15-му дню третьего месяца предшествующего квартала. На рис. 5.6 видно, что мера Sales Amount за
май 2009 года показывает сумму 1 067 165,23, тогда как мера Sales PQ за
август 2009-го возвращает 435 306,10, принимая в расчет только продажи до 15 мая этого года включительно.
Мера Sales PQ использует ту же технику, что и Sales PY. Единственное
отличие состоит в том, что переменная MonthsOffset установлена в значение 3, а не 12.
Мера в таблице Sales:
Sales PQ :=
VAR MonthsOffset = 3
... // Оставшееся определение такое же, как для Sales PY
162
 Глава 5. Пользовательские вычисления, связанные со временем
Рис. 5.6. Для августа 2009 года мера Sales PQ показывает продажи
с 1 по 15 мая 2009 года – после 15 августа у нас данных нет
Квартальное сравнение в абсолютном выражении вычисляется в
мере Sales QOQ, а в относительном – в мере Sales QOQ %. При этом обе
меры используют в качестве основы меру Sales PQ для обеспечения корректного сравнения.
Мера в таблице Sales:
Sales QOQ :=
VAR ValueCurrentPeriod = [Sales Amount]
VAR ValuePreviousPeriod = [Sales PQ]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales QOQ % :=
DIVIDE (
[Sales QOQ];
[Sales PQ]
)
Сравнение периодов  163
Месячное сравнение
При месячном сравнении временные периоды сопоставляются с аналогичными периодами в предыдущем месяце. В нашем примере данные имеются только по 15 августа 2009 года. Поэтому в мере Sales PM
за август 2009 года учитываются продажи только с 1 по 15 июля того же
года. Таким образом, возвращается не весь предыдущий месяц, а лишь
его часть. На рис. 5.7 видно, что в мере Sales Amount за июль 2009 года
стоит сумма 1 068 396,58, тогда как мера Sales PM за август 2019-го
показывает 584 212,78, поскольку данные в июле учитываются лишь по
15 число.
Рис. 5.7. Для августа 2009 года мера Sales PM показывает продажи
с 1 по 15 июля того же года
В мере Sales PM применяется та же техника, что и при определении
меры Sales PY. Единственное отличие заключается в том, что значение
переменной MonthsOffset изменилось с 12 на 1.
Мера в таблице Sales:
Sales PM :=
VAR MonthsOffset = 1
... // Оставшееся определение такое же, как для Sales PY
Месячное сравнение в абсолютном выражении вычисляется в мере
Sales MOM, а в относительном – в мере Sales MOM %. При этом обе меры
используют в качестве основы меру Sales PM для обеспечения корректного сравнения.
164
 Глава 5. Пользовательские вычисления, связанные со временем
Мера в таблице Sales:
Sales MOM :=
VAR ValueCurrentPeriod = [Sales Amount]
VAR ValuePreviousPeriod = [Sales PM]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales MOM % :=
DIVIDE (
[Sales MOM];
[Sales PM]
)
Сравнение периодов
При сравнении периодов происходит автоматический выбор мер,
определенных ранее в этом разделе, на основании текущего выбора в
визуализации. Например, если в визуализации показываются данные
на уровне недели, будет произведено недельное сравнение, а если по
годам – годовое. Ожидаемый результат показан на рис. 5.8.
В мерах Sales PP, Sales POP и Sales POP % происходит отсылка к соответствующей неделе, кварталу или году в зависимости от текущего
выбора в отчете. Определение выбранного уровня выполняется посредством функции ISINSCOPE. Аргументы, передаваемые в эту функцию, – это атрибуты, представленные в строках матрицы, показанной
на рис. 5.8. Формулы приведенных мер следующие.
Мера в таблице Sales:
Sales POP % :=
SWITCH (
TRUE;
ISINSCOPE ( 'Date'[Year Month] ); [Sales MOM %];
ISINSCOPE ( 'Date'[Fiscal Year Quarter] ); [Sales QOQ %];
ISINSCOPE ( 'Date'[Fiscal Year] ); [Sales YOY %]
)
Сравнение периодов  165
Рис. 5.8. В столбце Sales PP показано значение за прошлый месяц
на уровне месяца, за прошлый квартал – на уровне квартала
и за прошлый год – на уровне года
Мера в таблице Sales:
Sales POP :=
SWITCH (
TRUE;
ISINSCOPE ( 'Date'[Year Month] ); [Sales MOM];
ISINSCOPE ( 'Date'[Fiscal Year Quarter] ); [Sales QOQ];
ISINSCOPE ( 'Date'[Fiscal Year] ); [Sales YOY]
)
Мера в таблице Sales:
Sales PP :=
SWITCH (
TRUE;
ISINSCOPE ( 'Date'[Year Month] ); [Sales PM];
ISINSCOPE ( 'Date'[Fiscal Year Quarter] ); [Sales PQ];
ISINSCOPE ( 'Date'[Fiscal Year] ); [Sales PY]
)
166
 Глава 5. Пользовательские вычисления, связанные со временем
Сравнение нарастающих итогов
Сравнение нарастающих итогов позволяет сопоставить меру с вычислением нарастающего итога с самой собой, рассчитанной за эквивалентный период времени с определенным сдвигом. Например, вы можете
сравнить нарастающий итог на конец года с таким же показателем за
предыдущий год, т. е. со временным сдвигом в один год.
При этом все вычисляемые меры будут рассчитываться с учетом неполных временных периодов. В нашем случае информация в модели
присутствует только по 15 августа 2009 года, а значит, в мерах со сдвигом не будут учитываться продажи, совершенные позже этого числа в
2008 году.
Годовое сравнение нарастающих итогов
Годовое сравнение позволяет сопоставить аналогичные нарастающие итоги в текущем и прошлом годах. На рис. 5.9 видно, что в мере
Sales PYTD за период FY 2010 учитываются только продажи по 15 августа 2008 года. Поэтому мера Sales YTD за период FQ2-2009 показывает
значение 4 909 687,61, тогда как в мере Sales PYTD за FQ2-2010 сумма
получается меньше: 4 484 656,17.
Рис. 5.9. За период FQ2-2010 мера Sales PYTD показывает продажи
с 1 марта по 15 августа 2008 года, поскольку после 15 августа 2009-го данных нет
Сравнение нарастающих итогов  167
Мера Sales PYTD рассчитывается по той же схеме, что и Sales YTD:
в ней происходит фильтрация поля Fiscal Year Number по предыдущему
значению вместо последнего года, видимого в контексте фильтра. Основное отличие состоит в расчете переменной LastDayOfFiscalYearAvailable,
в которой должны учитываться только даты с продажами и игнорироваться фильтры по фильтрозащищенным столбцам, которые присутствуют при расчете меры Sales Amount.
Мера в таблице Sales:
Sales PYTD :=
IF (
[ShowValueForDates];
VAR PreviousFiscalYear = MAX ( 'Date'[Fiscal Year Number] ) - 1
VAR LastDayOfFiscalYearAvailable =
CALCULATE (
MAX ( 'Date'[Day of Fiscal Year Number] );
REMOVEFILTERS (
-- Удаляем фильтры
'Date'[Working Day];
-- с фильтрозащищенных столбцов
'Date'[Day of Week];
-- для получения последнего дня
'Date'[Day of Week Number] -- с данными в отчете
);
'Date'[DateWithSales] = TRUE
)
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Fiscal Year Number] = PreviousFiscalYear;
'Date'[Day of Fiscal Year Number] <= LastDayOfFiscalYearAvailable;
'Date'[DateWithSales] = TRUE
)
RETURN
Result
)
Меры Sales YOYTD и Sales YOYTD % базируются на вычислении меры
Sales PYTD для обеспечения корректного сравнения.
Мера в таблице Sales:
Sales YOYTD :=
VAR ValueCurrentPeriod = [Sales YTD]
VAR ValuePreviousPeriod = [Sales PYTD]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
168
 Глава 5. Пользовательские вычисления, связанные со временем
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales YOYTD % :=
DIVIDE (
[Sales YOYTD];
[Sales PYTD]
)
Квартальное сравнение нарастающих итогов
Квартальное сравнение нарастающих итогов помогает сопоставить
сумму нарастающего итога с начала квартала до указанной даты с
тем же показателем по аналогичной дате в предыдущем квартале. На
рис. 5.10 показано, что мера Sales PQTD за август 2009 года учитывает
транзакции только вплоть до 15 мая 2008-го, чтобы покрыть соответствующую долю предыдущего квартала. Как раз поэтому мера Sales QTD
за май 2009 года показывает цифру 2 242 196,31, тогда как в Sales PQTD
по августу 2009-го мы видим меньшее значение: 1 610 337,18.
Рис. 5.10. В мере Sales PQTD за август 2009 года учтены продажи за период
с 1 по 15 мая 2009-го, поскольку данные после 15 августа 2009-го отсутствует
В мере Sales PQTD присутствуют шаги, не все из которых очевидны.
Первые две переменные довольно простые: LastMonthSelected содержит
последний месяц, видимый в контексте фильтра, а в DaysOnLastMonth
хранится количество дней в LastMonthSelected.
Сравнение нарастающих итогов  169
Важно отметить, что, если значения в переменных DaysOnLastMonth
и DaysLastMonthSelected равны, это означает, что текущий контекст
фильтра включает конец месяца, а значит, соответствующая выборка в
предыдущем квартале должна включать полный относительный месяц.
Если переменные DaysOnLastMonth и DaysLastMonthSelected не равны,
значит, количество видимых дней в контексте фильтра ограничено. Следовательно, мы вычисляем последний день месяца с данными и ограничиваем результат таким образом, чтобы проход осуществлялся только
до того же номера дня в соответствующем месяце в предыдущем квартале. Эти расчеты происходят в переменной LastDayOfMonthWithSales,
которая содержит последний день месяца с продажами безотносительно фильтрозащищенных столбцов.
Если выборка предыдущего месяца включает в себя целый месяц, в переменной LastDayOfMonthWithSales будет находиться значение 31 – число,
большее или равное любому другому порядковому дню месяца. Похожие
вычисления производятся и в переменной LastMonthInQuarterWithSales,
но с номерами месяцев. Эти две переменные используются на последнем
шаге для расчета переменной FilterQTD. Данная переменная содержит
все пары значений (FiscalMonthInQuarter, FiscalDayInMonth), меньшие или
равные парам (LastMonthInQuarterWithSales, LastDayOfMonthWithSales).
Использование функции ISONORAFTER ( ..., DESC ) позволяет добиться
того же эффекта, что и NOT ISONORAFTER со значением сортировки по
умолчанию – ASC.
Мера в таблице Sales:
Sales PQTD :=
IF (
[ShowValueForDates];
VAR LastMonthSelected =
MAX ( 'Date'[Year Month Number] )
VAR DaysOnLastMonth =
CALCULATE (
COUNTROWS ( 'Date' );
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of week] );
'Date'[Year Month Number] = LastMonthSelected
)
VAR DaysLastMonthSelected =
CALCULATE (
COUNTROWS ( 'Date' );
'Date'[DateWithSales] = TRUE;
'Date'[Year Month Number] = LastMonthSelected
)
VAR LastDayOfMonthWithSales =
MAX (
170
 Глава 5. Пользовательские вычисления, связанные со временем
-- Конец месяца
31 * (DaysOnLastMonth = DaysLastMonthSelected);
-- или последний выбранный день с данными
CALCULATE (
MAX ( 'Date'[Day of Month Number] );
REMOVEFILTERS (
-- Удаляем фильтры со всех
'Date'[Working Day];
-- фильтрозащищенных столбцов
'Date'[Day of Week];
-- для получения последнего
'Date'[Day of Week Number] -- дня с данными в отчете
);
'Date'[DateWithSales] = TRUE
)
)
VAR LastMonthInQuarterWithSales =
CALCULATE (
MAX ( 'Date'[Fiscal Month In Quarter Number] );
REMOVEFILTERS (
-- Удаляем фильтры со всех
'Date'[Working Day];
-- фильтрозащищенных столбцов
'Date'[Day of Week];
-- для получения последнего дня
'Date'[Day of Week Number] -- с данными в отчете
);
'Date'[DateWithSales] = TRUE
)
VAR PreviousFiscalYearQuarter =
MAX ( 'Date'[Fiscal Year Quarter Number] ) - 1
VAR FilterQTD =
FILTER (
ALL ( 'Date'[Fiscal Month In Quarter Number];
'Date'[Day of Month Number] );
ISONORAFTER (
'Date'[Fiscal Month In Quarter Number];
LastMonthInQuarterWithSales; DESC;
'Date'[Day of Month Number]; LastDayOfMonthWithSales; DESC
)
)
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Fiscal Year Quarter Number] = PreviousFiscalYearQuarter;
FilterQTD
)
RETURN
Result
)
Меры Sales QOQTD и Sales QOQTD % базируются на вычислении меры
Sales PQTD.
Сравнение нарастающих итогов  171
Мера в таблице Sales:
Sales QOQTD :=
VAR ValueCurrentPeriod = [Sales QTD]
VAR ValuePreviousPeriod = [Sales PQTD]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales QOQTD % :=
DIVIDE (
[Sales QOQTD];
[Sales PQTD]
)
Месячное сравнение нарастающих итогов
Месячное сравнение нарастающих итогов помогает сопоставить
значения нарастающего итога с начала месяца до конкретной даты с
аналогичным показателем по предыдущему месяцу. На рис. 5.11 видно, что мера Sales PMTD за август 2009 года учитывает продажи только
по 15 июля 2009-го, таким образом захватывая лишь соответствующую
часть продаж за предыдущий месяц. Именно поэтому накопительные
месячные продажи за июль 2009-го (мера Sales MTD), составляющие
1 068 396,58, практически вдвое превышают значение меры Sales PMTD
за август того же года, которая показывает 584 212,78.
В определении меры Sales PMTD присутствуют довольно сложные
шаги. Первые две переменные не так сложны: в LastMonthSelected находится последний месяц, видимый в текущем контексте фильтра, а в
DaysOnLastMonth – количество дней в LastMonthSelected.
Стоит заметить, что, если переменные DaysOnLastMonth и
DaysLastMonthSelected равны, это значит, что текущий контекст фильтра включает конец месяца, следовательно, соответствующая выборка в
предыдущем квартале должна включать полный относительный месяц.
И напротив, если переменные DaysOnLastMonth и DaysLastMonthSelected
не равны, это означает, что количество видимых дней в контексте фильт-
172
 Глава 5. Пользовательские вычисления, связанные со временем
ра ограничено. Таким образом, мы вычисляем последний день месяца с
данными и ограничиваем результат таким образом, чтобы проход осуществлялся только до того же номера дня в соответствующем месяце
в предыдущем квартале. Эти вычисления производятся в переменной
LastDayOfMonthWithSales, которая содержит последний день месяца с
продажами безотносительно фильтрозащищенных столбцов.
Если выборка предыдущего месяца состоит из целого месяца, в переменной LastDayOfMonthWithSales окажется значение 31 – число, большее или равное любому другому порядковому дню месяца. Затем переменная LastDayOfMonthWithSales используется для фильтрации дней
в предыдущем месяце, что достигается путем вычитания единицы из
LastMonthSelected.
Рис. 5.11. За август 2009 года мера Sales PMTD агрегирует данные
с 1 по 15 июля того же года, поскольку нам недоступны данные
позже 15 августа 2009-го
Сравнение нарастающих итогов  173
Мера в таблице Sales:
Sales PMTD :=
IF (
[ShowValueForDates];
VAR LastMonthSelected =
MAX ( 'Date'[Year Month Number] )
VAR DaysOnLastMonth =
CALCULATE (
COUNTROWS ( 'Date' );
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of week] );
'Date'[Year Month Number] = LastMonthSelected
)
VAR DaysLastMonthSelected =
CALCULATE (
COUNTROWS ( 'Date' );
'Date'[DateWithSales] = TRUE;
'Date'[Year Month Number] = LastMonthSelected
)
VAR LastDayOfMonthWithSales =
MAX (
-- Конец месяца
31 * (DaysOnLastMonth = DaysLastMonthSelected);
-- или последний выбранный день с данными
CALCULATE (
MAX ( 'Date'[Day of Month Number] );
REMOVEFILTERS (
-- Удаляем фильтры со всех
-- фильтрозащищенных столбцов
'Date'[Working Day];
'Date'[Day of Week];
-- для получения последнего
'Date'[Day of Week Number] -- дня с данными в отчете
);
'Date'[DateWithSales] = TRUE
)
)
VAR PreviousYearMonth =
LastMonthSelected - 1
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Year Month Number] = PreviousYearMonth;
'Date'[Day of Month Number] <= LastDayOfMonthWithSales
)
RETURN
Result
)
В основе мер Sales MOMTD и Sales MOMTD % лежит мера Sales
PMTD.
174
 Глава 5. Пользовательские вычисления, связанные со временем
Мера в таблице Sales:
Sales MOMTD :=
VAR ValueCurrentPeriod = [Sales MTD]
VAR ValuePreviousPeriod = [Sales PMTD]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales MOMTD % :=
DIVIDE (
[Sales MOMTD];
[Sales PMTD]
)
Сравнение нарастающих итогов с полным
предыдущим периодом
Такой вид анализа может пригодиться, если вы рассматриваете прошлый период в качестве ориентира. Когда нарастающий итог за текущий год достигает отметки в 100 %, по сравнению с предыдущим годом,
можно говорить, что вы вышли на прошлогодние показатели.
Сравнение нарастающих итогов за год с полным
прошлым годом
В данном шаблоне речь пойдет о сравнении нарастающих итогов с
начала года с полным предшествующим годом. На рис. 5.12 показано,
что за январь 2009 года (период, близкий к окончанию 2009-го финансового года) мера Sales YTD оказалась на 10 % ниже значения меры Sales
Amount за весь предыдущий финансовый год. В процентном отношении
разницу можно отслеживать посредством меры Sales YTDOPY % – она
начнет демонстрировать положительные значения, когда накопительный показатель за текущий год превысит итоговое значение за предыдущий финансовый год, чего в данном примере так и не произошло.
Сравнение нарастающих итогов с полным предыдущим периодом  175
Рис. 5.12. Мера Sales YTDOPY % показывает отрицательные цифры,
поскольку значение меры Sales YTD так и не превысило Sales Amount
за предыдущий финансовый год
Сравнение нарастающих итогов с полным предыдущим периодом
производится в мерах Sales YTDOPY и Sales YTDOPY %, которые в свою
очередь строятся на основании мер Sales YTD для расчета нарастающего итога с начала года и Sales PYC – для получения итоговых продаж за
весь предшествующий год.
Мера в таблице Sales:
Sales PYC :=
IF (
[ShowValueForDates] && HASONEVALUE ( 'Date'[Fiscal Year Number] );
VAR PreviousFiscalYear = MAX ( 'Date'[Fiscal Year Number] ) - 1
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Fiscal Year Number] = PreviousFiscalYear
)
RETURN
Result
)
Мера в таблице Sales:
Sales YTDOPY :=
VAR ValueCurrentPeriod = [Sales YTD]
VAR ValuePreviousPeriod = [Sales PYC]
VAR Result =
176
 Глава 5. Пользовательские вычисления, связанные со временем
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales YTDOPY % :=
DIVIDE (
[Sales YTDOPY];
[Sales PYC]
)
Сравнение нарастающих итогов за квартал с полным
прошлым кварталом
В данном разделе будет представлен шаблон для сравнения нарастающих итогов с начала квартала с полным предшествующим финансовым
кварталом. По рис. 5.13 видно, что мера Sales QTD преодолела отметку
общих продаж (мера Sales Amount) за период FQ1-2008 (первый квартал
2008 финансового года) лишь в августе 2008-го. Мера Sales QTDOPQ %
обеспечивает сравнение нарастающего итога с начала квартала с общими продажами за предыдущий квартал. При положительных значениях
фиксируется рост показателя.
Рис. 5.13. Мера Sales QTDOPQ % начала показывать положительные проценты
начиная с августа 2008 года, когда значение меры Sales QTD перевалило
за отметку меры Sales Amount за период FQ1-2008
Сравнение нарастающих итогов с полным предыдущим периодом  177
Сравнение нарастающих итогов с начала квартала с полным предшествующим кварталом выполняется в мерах Sales QTDOPQ и Sales
QTDOPQ %, при этом сами они опираются на меры Sales QTD для расчета нарастающего итога с начала квартала и Sales PQC – для получения
итоговых продаж за предыдущий квартал.
Мера в таблице Sales:
Sales PQC :=
IF (
[ShowValueForDates] && HASONEVALUE ( 'Date'[Fiscal Year Quarter Number] );
VAR PreviousFiscalYearQuarter = MAX ( 'Date'[Fiscal Year Quarter Number] ) - 1
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Fiscal Year Quarter Number] = PreviousFiscalYearQuarter
)
RETURN
Result
)
Мера в таблице Sales:
Sales QTDOPQ :=
VAR ValueCurrentPeriod = [Sales QTD]
VAR ValuePreviousPeriod = [Sales PQC]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales QTDOPQ % :=
DIVIDE (
[Sales QTDOPQ];
[Sales PQC]
)
178
 Глава 5. Пользовательские вычисления, связанные со временем
Сравнение нарастающих итогов за месяц с полным
прошлым месяцем
Здесь мы рассмотрим шаблон для сравнения нарастающих итогов
с начала месяца с предшествующим месяцем. На рис. 5.14 показано,
что мера Sales MTD в апреле 2008 года преодолела отметку меры Sales
Amount за март того же года. Мера Sales MTDOPM % служит для сравнения нарастающего итога с начала месяца с суммой продаж за предыдущий месяц. При положительных значениях можно говорить о росте
показателя, что в нашем случае произошло 19 апреля 2008 года.
Рис. 5.14. Мера Sales MTDOPM % начала показывать положительные цифры
с 19 апреля 2008 года, когда значение меры Sales MTD превысило
отметку меры Sales Amount за март того же года
Сравнение нарастающих итогов с начала месяца с полным предшествующим месяцем осуществляется в абсолютном и относительном выражении в мерах Sales MTDOPM и Sales MTDOPM % соответственно, при
этом сами они базируются на мерах Sales MTD для расчета нарастаю-
Вычисление скользящей годовой суммы  179
щего итога с начала месяца и Sales PMC – для получения суммы продаж
за предыдущий месяц.
Мера в таблице Sales:
Sales PMC :=
IF (
[ShowValueForDates] && HASONEVALUE ( 'Date'[Year Month Number] );
VAR PreviousFiscalYearMonth = MAX ( 'Date'[Year Month Number] ) - 1
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Year Month Number] = PreviousFiscalYearMonth
)
RETURN
Result
)
Мера в таблице Sales:
Sales MTDOPM :=
VAR ValueCurrentPeriod = [Sales MTD]
VAR ValuePreviousPeriod = [Sales PMC]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales MTDOPM % :=
DIVIDE (
[Sales MTDOPM];
[Sales PMC]
)
Вычисление скользящей годовой суммы
Общепринятым способом анализировать данные за несколько месяцев является расчет скользящей годовой суммы вместо нарастающих
итогов с начала года. Скользящая годовая сумма традиционно вклю-
Powered by TCPDF (www.tcpdf.org)
180
 Глава 5. Пользовательские вычисления, связанные со временем
чает данные за последние 12 месяцев. Например, показатель за март
2008 года будет учитывать продажи с апреля 2007 года по март 2008-го.
Скользящая годовая сумма
На рис. 5.15 продемонстрирована мера Sales MAT, вычисляющая
скользящую годовую сумму. Также в показанном отчете присутствует
мера Sales MAT (364), отличительная черта которой состоит в том, что
она использует для агрегации последние 364 дня (что соответствует
52 неделям) вместо полного года.
Рис. 5.15. В мере Sales MAT за март 2008 года агрегируются
значения Sales Amount с апреля 2007 года по март 2008-го
Мера Sales MAT определяет в столбце Date[Date] временной диапазон,
состоящий из всех дней полного года начиная с последнего дня в контексте фильтра.
Мера в таблице Sales:
Sales MAT :=
IF (
Вычисление скользящей годовой суммы  181
[ShowValueForDates];
VAR LastDayMAT = MAX ( 'Date'[Sequential Day Number] )
VAR FirstDayMAT = INT ( EDATE ( LastDayMAT + 1; -12 ) )
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Sequential Day Number] >= FirstDayMAT
&& 'Date'[Sequential Day Number] <= LastDayMAT
)
RETURN
Result
)
Показатель в мере Sales MAT (364) может не соответствовать полной
годовой сумме. Но это вполне уместный метод расчета трендов, поскольку всегда включает в себя одинаковое количество дней и недель.
Соответственно, и дни недели распределены в итоговом результате
строго равномерно. Данная мера определяет в столбце Date[Date] временной диапазон, включающий 364 дня начиная с последнего дня в
контексте фильтра.
Мера в таблице Sales:
Sales MAT (364) :=
IF (
[ShowValueForDates];
VAR LastDayMAT = MAX ( 'Date'[Sequential Day Number] )
VAR FirstDayMAT = LastDayMAT - 363
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Sequential Day Number] >= FirstDayMAT
&& 'Date'[Sequential Day Number] <= LastDayMAT
)
RETURN
Result
)
Сравнение скользящих годовых сумм
Сравнение скользящих годовых сумм реализуется при помощи мер
Sales PYMAT, Sales MATG и Sales MATG %, которые в свою очередь основаны на мере Sales MAT. Стоит учитывать, что мера Sales MAT начинает
показывать корректные значения лишь через год после первой продажи
182
 Глава 5. Пользовательские вычисления, связанные со временем
(когда наберется массив данных за год), а если текущий год не полный,
цифры могут оказаться неожиданными. Например, для финансового
2010 года мера Sales PYMAT показывает значение 9 874 218,49, что соответствует агрегации меры Sales Amount за период FY 2009 с рис. 5.16.
Если сравнивать полученный результат с продажами за 2010-й финансовый год, мы получим сопоставление за период меньше шести месяцев, поскольку за 2009 год у нас есть данные только по 15 августа. Также
обратите внимание, что значения в столбце Sales MATG % начинаются
в периоде FY 2009 с очень больших величин и стабилизируются только
спустя год. Это происходит из-за отсутствия данных о продажах за предыдущий год. Такое поведение характерно для этого вида вычисления,
ведь скользящая годовая сумма обычно рассчитывается на уровне гранулярности по месяцам или дням, чтобы показать тренды на диаграмме.
Рис. 5.16. Мера Sales MATG % демонстрирует отношение
между Sales MAT и Sales PYMAT в процентах
Определения представленных мер показаны ниже.
Вычисление скользящей годовой суммы  183
Мера в таблице Sales:
Sales PYMAT :=
IF (
[ShowValueForDates];
VAR LastDayAvailable = MAX ( 'Date'[Sequential Day Number] )
VAR LastDayMAT = INT ( EDATE ( LastDayAvailable; -12 ) )
VAR FirstDayMAT = INT ( EDATE ( LastDayAvailable + 1; -24 ) )
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Sequential Day Number] >= FirstDayMAT
&& 'Date'[Sequential Day Number] <= LastDayMAT
)
RETURN
Result
)
Мера в таблице Sales:
Sales MATG :=
VAR ValueCurrentPeriod = [Sales MAT]
VAR ValuePreviousPeriod = [Sales PYMAT]
VAR Result =
IF (
NOT ISBLANK ( ValueCurrentPeriod )
&& NOT ISBLANK ( ValuePreviousPeriod );
ValueCurrentPeriod - ValuePreviousPeriod
)
RETURN
Result
Мера в таблице Sales:
Sales MATG % :=
DIVIDE (
[Sales MATG];
[Sales PYMAT]
)
Меру Sales PYMAT можно также реализовать с использованием последних 364 дней, подобно мере Sales MAT (364). Разница между мерами
Sales PYMAT и Sales PYMAT (364) заключается в вычислении переменных FirstDayMAT и LastDayMAT.
184
 Глава 5. Пользовательские вычисления, связанные со временем
Мера в таблице Sales:
Sales PYMAT (364) :=
IF (
[ShowValueForDates];
VAR LastDayAvailable = MAX ( 'Date'[Sequential Day Number] )
VAR LastDayMAT = LastDayAvailable - 364
VAR FirstDayMAT = LastDayMAT - 363
VAR Result =
CALCULATE (
[Sales Amount];
ALLEXCEPT ( 'Date'; 'Date'[Working Day]; 'Date'[Day of Week] );
'Date'[Sequential Day Number] >= FirstDayMAT
&& 'Date'[Sequential Day Number] <= LastDayMAT
)
RETURN
Result
)
Скользящее среднее
Скользящее среднее обычно используется для демонстрации линий
трендов на диаграммах. На рис. 5.17 отображается показатель скользящего среднего для меры Sales Amount за последние 30 дней (Sales AVG
30D), три месяца (Sales AVG 3M) и год (Sales AVG 1Y).
Рис. 5.17. Меры Sales AVG 30D, Sales AVG 3M и Sales AVG 1Y демонстрируют
скользящее среднее за 30 дней, три месяца и год соответственно
Скользящее среднее за 30 дней
В мере Sales AVG 30D рассчитывается скользящее среднее за месяц путем прохода по последним 30 дням, собранным в переменной
Скользящее среднее  185
Period30D. Итерации осуществляются по видимым дням за исключением дней, когда не было продаж, и с учетом фильтров в таблице Date по
фильтрозащищенным столбцам.
Мера в таблице Sales:
Sales AVG 30D :=
IF (
[ShowValueForDates];
VAR LastDayMAT =
MAX ( 'Date'[Sequential Day Number] )
VAR FirstDayMAT = LastDayMAT - 29
VAR Period30D =
CALCULATETABLE (
VALUES ( 'Date'[Sequential Day Number] );
ALLEXCEPT (
'Date';
'Date'[Working Day];
'Date'[Day of Week]
);
'Date'[Sequential Day Number] >= FirstDayMAT
&& 'Date'[Sequential Day Number] <= LastDayMAT;
'Date'[DateWithSales] = TRUE
)
VAR FirstDayWithData =
CALCULATE (
INT ( MIN ( Sales[Order Date] ) );
REMOVEFILTERS ()
)
VAR FirstDayInPeriod =
MINX (
Period30D;
'Date'[Sequential Day Number]
)
VAR Result =
IF (
FirstDayWithData <= FirstDayInPeriod;
CALCULATE (
AVERAGEX ( Period30D; [Sales Amount] );
REMOVEFILTERS ( 'Date' )
)
)
RETURN
Result
)
Это достаточно гибкий шаблон, поскольку он также будет работать
и с неаддитивными мерами. При этом для обычных мер реализация
переменной Result может быть и более эффективной:
186
 Глава 5. Пользовательские вычисления, связанные со временем
VAR Result =
IF (
FirstDayWithData <= FirstDayInPeriod;
CALCULATE (
DIVIDE (
[Sales Amount];
DISTINCTCOUNT ( Sales[Order Date] )
);
Period30D;
REMOVEFILTERS ( 'Date' )
)
)
Скользящее среднее за три месяца
В мере Sales AVG 3M рассчитывается скользящее среднее за три месяца при помощи итераций по последним трем месяцам, хранящимся
в переменной Period3M. При этом проход осуществляется по видимым
дням за исключением дней, когда не было продаж, и с учетом фильтров
в таблице Date по фильтрозащищенным столбцам.
Мера в таблице Sales:
Sales AVG 3M :=
IF (
[ShowValueForDates];
VAR LastDayMAT =
MAX ( 'Date'[Sequential Day Number] )
VAR FirstDayMAT =
INT ( EDATE ( LastDayMAT + 1; -3 ) )
VAR Period3M =
CALCULATETABLE (
VALUES ( 'Date'[Sequential Day Number] );
ALLEXCEPT (
'Date';
'Date'[Working Day];
'Date'[Day of Week]
);
'Date'[Sequential Day Number] >= FirstDayMAT
&& 'Date'[Sequential Day Number] <= LastDayMAT;
'Date'[DateWithSales] = TRUE
)
VAR FirstDayWithData =
CALCULATE (
INT ( MIN ( Sales[Order Date] ) );
REMOVEFILTERS ()
)
Скользящее среднее  187
VAR FirstDayInPeriod =
MINX (
Period3M;
'Date'[Sequential Day Number]
)
VAR Result =
IF (
FirstDayWithData <= FirstDayInPeriod;
CALCULATE (
AVERAGEX ( Period3M; [Sales Amount] );
REMOVEFILTERS ( 'Date' )
)
)
RETURN
Result
)
Для простых аддитивных мер здесь также может быть применен шаблон с функцией DIVIDE, показанный в расчетах для скользящего среднего за 30 дней.
Скользящее среднее за год
Мера Sales AVG 1Y демонстрирует скользящее среднее за последний
год путем перебора соответствующих дат, собранных в переменной
Period1Y. При этом итерации также осуществляются по видимым дням
за исключением дней без продаж и с учетом фильтров в таблице Date по
фильтрозащищенным столбцам.
Мера в таблице Sales:
Sales AVG 1Y :=
IF (
[ShowValueForDates];
VAR LastDayMAT =
MAX ( 'Date'[Sequential Day Number] )
VAR FirstDayMAT =
INT ( EDATE ( LastDayMAT + 1; 12 ) )
VAR Period1Y =
CALCULATETABLE (
VALUES ( 'Date'[Sequential Day Number] );
ALLEXCEPT (
'Date';
'Date'[Working Day];
'Date'[Day of Week]
);
'Date'[Sequential Day Number] >= FirstDayMAT
&& 'Date'[Sequential Day Number] <= LastDayMAT;
188
 Глава 5. Пользовательские вычисления, связанные со временем
'Date'[DateWithSales] = TRUE
)
VAR FirstDayWithData =
CALCULATE (
INT ( MIN ( Sales[Order Date] ) );
REMOVEFILTERS ()
)
VAR FirstDayInPeriod =
MINX (
Period1Y;
'Date'[Sequential Day Number]
)
VAR Result =
IF (
FirstDayWithData <= FirstDayInPeriod;
CALCULATE (
AVERAGEX ( Period1Y; [Sales Amount] );
REMOVEFILTERS ( 'Date' )
)
)
RETURN
Result
)
Для аддитивных мер в данном шаблоне также может быть применен
вариант с функцией DIVIDE, показанный в расчетах для скользящего
среднего за 30 дней.
Глава
6
Сравнение разных временных
интервалов
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Представленный в данной главе шаблон идеально подойдет для сравнения значений показателей за разные по длительности периоды времени. Например, мы можем сравнить продажи товаров за последний месяц с аналогичным показателем за выбранный пользователем период.
При этом сравниваемые интервалы могут характеризоваться разным
количеством дней – допустим, месячные показатели можно сопоставлять с годовыми. В этом случае необходимо подогнать значения таким
образом, чтобы операция сравнения выполнялась корректно.
Описание шаблона
Пользователь выбирает два различных временных интервала (текущий
и для сравнения) путем настройки срезов. В отчете, показанном на
рис. 6.1, отображены продажи за текущий период и интервал для сравнения. При этом нам нужно скорректировать интервал для сравнения с
использованием количества дней в обоих периодах в качестве соотносительного коэффициента (allocation factor).
Чтобы пользователь имел возможность выбрать два временных интервала, в модели данных должны присутствовать две таблицы дат:
одна для выбора текущего периода, вторая – для периода сравнения.
Как показано на рис. 6.2, вспомогательная таблица Comparison Date объединена с базовой таблицей Date посредством неактивной связи. Это
упростит поддержку связей с другими таблицами фактов.
190
 Глава 6. Сравнение разных временных интервалов
Рис. 6.1. В отчете показаны продажи товаров в разные временные интервалы
вместе с откорректированными значениями
Рис. 6.2. Таблицы Comparison Date и Date объединены
при помощи неактивной связи
Когда при вычислении меры используется выражение с фильтром по
таблице Comparison Date, в модели данных активируется связь между
таблицами Comparison Date и Date. Одновременно с этим применяется
модификатор REMOVEFILTERS к таблице Date, чтобы можно было в таблице Sales использовать фильтр из таблицы Comparison Date. В подобной модели данных любая мера может быть вычислена как в текущем
периоде, так и в интервале для сравнения при помощи простого изменения активных связей. Ниже приведено определение меры Comparison
Sales Amount.
Описание шаблона  191
Мера в таблице Sales:
Comparison Sales Amount :=
VAR ComparisonPeriod =
CALCULATETABLE (
VALUES ( 'Date'[Date] );
REMOVEFILTERS ( 'Date' );
USERELATIONSHIP ( 'Date'[Date]; 'Comparison Date'[Comparison Date] )
)
VAR Result =
CALCULATE (
[Sales Amount];
ComparisonPeriod
)
RETURN
Result
Чтобы вычислить скорректированное значение для меры Comparison
Sales Amount, необходимо применить метод соотношений. В данном
примере мы использовали количество дней в обоих периодах в качестве
соотносительного коэффициента. При этом особенности бизнес-логики
могут диктовать свои условия: к примеру, в качестве соотносительного
коэффициента могут использоваться только рабочие дни. Здесь все
зависит от конкретных условий и требований бизнеса.
В нашем случае, если в интервале для сравнения будет больше дней,
чем в текущем периоде, мы будем уменьшать значение меры Comparison
Sales Amount на коэффициент, рассчитывающийся как отношение между количеством дней в двух периодах.
Мера в таблице Sales:
Adjusted Comp. Sales Amount :=
VAR CurrentPeriod =
VALUES ( 'Date'[Date] )
VAR ComparisonPeriod =
CALCULATETABLE (
VALUES ( 'Date'[Date] );
REMOVEFILTERS ( 'Date' );
USERELATIONSHIP ( 'Date'[Date]; 'Comparison Date'[Comparison Date] )
)
VAR ComparisonSales =
CALCULATE ( [Sales Amount]; ComparisonPeriod )
VAR DaysInCurrentPeriod =
COUNTROWS ( CurrentPeriod )
VAR DaysInComparisonPeriod =
COUNTROWS ( ComparisonPeriod )
VAR DailyComparisonSales =
192
 Глава 6. Сравнение разных временных интервалов
DIVIDE (
ComparisonSales;
DaysInComparisonPeriod
)
VAR Result =
DaysInCurrentPeriod * DailyComparisonSales
RETURN
Result
Глава
7
Полуаддитивные
вычисления
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Вычисления для расчета значений на начало или конец временного периода – традиционная головная боль для специалистов по бизнес-аналитике, и разработчики DAX не исключение. Сами эти меры рассчитать
несложно. Другое дело, что понять досконально их поведение бывает
довольно затруднительно. В подобных вычислениях значения не агрегируются за определенный период времени, как обычно происходит с
продажами. Вместо этого расчеты должны возвращать значения на начало или конец выбранного временного интервала. Подобные вычисления называются полуаддитивными (semi-additive calculations). Название
обусловлено тем, что значения по таким мерам могут суммироваться
по одним атрибутам (например, по покупателям) и не суммироваться
по другим (допустим, по дате).
В качестве примера используем модель данных, содержащую текущий баланс банковского счета. По клиентам данная мера должна быть
аддитивной, поскольку общий баланс для всех покупателей равен сумме остатков на их банковских счетах. В то же время, агрегируя данные
по дате, использовать функцию SUM будет некорректно. Иными словами, итоговый баланс на конец квартала не высчитывается при помощи
суммирования балансов всех входящих в этот квартал месяцев. Вместо
этого логично будет отождествить сумму на конец квартала с остатком
денежных средств на конец последнего из входящих в него месяцев.
При определении мер для расчета сумм на начало и конец временных
периодов мы сталкиваемся с большим количеством нюансов. Именно
194
 Глава 7. Полуаддитивные вычисления
поэтому в данном шаблоне мы рассмотрим множество разных примеров. Советуем все их внимательно изучить, чтобы досконально понять
тонкости и различия между ними и выбрать подходящий для своего
конкретного сценария.
Введение
В нашей модели данных представлена информация о состоянии банковских счетов нескольких клиентов. Цифры на каждую дату отражают
остаток денег на счете. На рис. 7.1 показан отчет о балансах некоторых
клиентов.
Рис. 7.1. В исходной таблице содержится информация
о состоянии банковских счетов клиентов
По своей природе представленные данные таковы, что просто недопустимо использовать функцию суммирования по датам. Вместо этого
необходимо агрегировать данные на уровне месяца, квартала или года
с использованием первого или последнего значения за период. Перед
тем как обратиться к коду меры, необходимо уяснить некоторые детали, а для этого надо ответить на следующие простые вопросы.
1. Каким должен быть итоговый баланс на банковском счете Кэти
Джордан (Katie Jordan) на конец 2020 года? Последний доступный
баланс для нее соответствует 30 сентября 2020 года. Должны ли
мы использовать это значение в качестве баланса на конец года?
Первая и последняя даты  195
То же самое касается и счета Луиса Бонифаца (Luis Bonifaz) – каков будет его баланс на конец 2020 года: ноль или 1813,00?
2. Каким будет итоговый баланс всех трех клиентов на конец
2020 года? Актуальные данные на этом уровне есть только по
Маурицио Маканьо (Maurizio Macagno). Брать ли их за итоговую
сумму баланса по всем клиентам или суммировать все последние
доступные цифры по каждому из них?
3. С каким начальным балансом на счете встретил новый 2020 год
Луис Бонифац? Брать ли в расчет баланс на 1 января? Или на
последние даты декабря?
Как видите, ответить на эти вопросы можно по-разному, и ни один
ответ не стоит считать единственно правильным. Вместо этого в зависимости от ваших требований необходимо подобрать наиболее подходящий шаблон для конкретного сценария. По сути, все представленные
здесь вычисления рассчитывают меры на начало или конец временного
интервала. Единственное отличие состоит в том, как трактуется понятие конца интервала.
Первая и последняя даты
Начнем с простейшего шаблона расчета меры на первую и последнюю
даты периода. Использовать его мы сможем в ограниченном количестве сценариев, предполагающих наличие данных на начало и конец
всех без исключения периодов. Представленная ниже формула возвращает баланс на первую или последнюю дату в таблице Date в рамках
текущего контекста фильтра вне зависимости от того, представлена ли
информация за эту дату. В случае отсутствия информации будет возвращено пустое значение.
Мера в таблице Balances:
Balance LastDate :=
CALCULATE (
SUM ( Balances[Balance] );
LASTDATE ( 'Date'[Date] ) -- Для первой даты используйте функцию FIRSTDATE
)
На рис. 7.2 показан результат вычисления этой меры.
Как видите, по месяцам, на последние даты которых данные о балансе не заведены, показаны пустые значения. Это самый быстрый шаблон
из всех, представленных в данной главе, но еще раз повторимся, что
корректно он будет работать только в случае наличия остатков по всем
196
 Глава 7. Полуаддитивные вычисления
клиентам и на каждую без исключений дату в модели данных или по
крайней мере на конец каждого периода. К примеру, подобные шаблоны могут применяться в финансовых приложениях, в которых данные
содержатся помесячно.
Рис. 7.2. В отчете показаны остатки средств на последнюю дату из таблицы Date
Первая и последняя даты с данными
В представленном в этом разделе шаблоне мы будем искать баланс на
последнюю доступную дату в текущем контексте фильтра. Таким образом, вместо того чтобы извлекать последнюю дату из таблицы Date, мы
будем брать ее из таблицы Balances. Результат вычисления меры показан на рис. 7.3.
Сначала в формуле осуществляется поиск подходящей даты путем
извлечения последней даты из модели с заполненными данными, после чего она используется в фильтре.
Мера в таблице Balances:
Balance LastDateWithData :=
VAR MaxBalanceDate =
Первая и последняя даты с данными  197
CALCULATE (
MAX ( Balances[Date] );
-- Используйте MIN для получения первой даты
ALLEXCEPT (
Balances;
-- Удаляем фильтры с расширенной таблицы
'Date'
-- Balances, за исключением Date
)
)
VAR Result =
CALCULATE (
SUM ( Balances[Balance] );
'Date'[Date] = MaxBalanceDate
)
RETURN
Result
Рис. 7.3. В отчете показаны балансы на последнюю дату,
на которую в модели присутствует информация
Стоит особо отметить присутствие модификатора ALLEXCEPT в формуле переменной MaxBalanceDate. Этот модификатор необходим во
избежание извлечения последней даты из текущего контекста фильтра, что привело бы к использованию разных дат для каждого клиента
198
 Глава 7. Полуаддитивные вычисления
и на уровне итогов. Применение функции ALLEXCEPT гарантирует извлечение одной и той же даты для всех клиентов. В своем сценарии вы
можете изменить этот фильтр, чтобы расчеты соответствовали вашим
требованиям.
Если вы не хотите использовать одинаковые даты для всех клиентов,
а вместо этого собираетесь использовать разные и собирать по ним
итоги, такой шаблон вам не подойдет. В этом случае вам нужно будет
взглянуть на шаблон Первая и последняя даты по клиенту, который
мы рассмотрим далее.
Альтернативная версия расчета с использованием функции
LASTNONBLANK будет менее эффективной. Применять ее стоит только в том случае, если факт включения даты в отчет определяется более
сложными методами, чем проверка существования соответствующей
строки в таблице Balances. Например, следующая формула произведет
тот же результат, что и предыдущая, но с большими затратами времени
и памяти.
Мера в таблице Balances:
Slower Balance LastDateWithData :=
CALCULATE (
SUM ( Balances[Balance] );
LASTNONBLANK (
'Date'[Date];
CALCULATE ( SUM ( Balances[Balance] ) )
)
)
Первая и последняя даты по клиенту
Если набор данных содержит разные даты для каждого клиента – а в
общем случае для каждой сущности, – используемый шаблон придется
изменить таким образом, чтобы вычислялась последняя дата для каждого клиента с получением подытогов путем суммирования частичных
результатов по другому атрибуту, не представляющему собой дату. Результат вычисления подобной меры показан на рис. 7.4.
Следующая формула для меры Balance LastDateByCustomer обеспечит
нам нужный результат.
Мера в таблице Balances:
Balance LastDateByCustomer :=
VAR MaxBalanceDates =
ADDCOLUMNS (
Первая и последняя даты по клиенту  199
SUMMARIZE (
-- Извлекаем клиентов
Balances;
-- из таблицы Balances
Customers[Name]
);
"@MaxBalanceDate"; CALCULATE (
-- Вычисляем для каждого
MAX ( Balances[Date] )
-- клиента свою последнюю дату
)
)
VAR MaxBalanceDatesWithLineage =
TREATAS (
-- Меняем привязку в табличной переменной
MaxBalanceDates;
-- MaxBalanceDates, чтобы она могла
Customers[Name];
-- фильтровать имя клиента
'Date'[Date]
-- и дату
)
VAR Result =
CALCULATE (
SUM ( Balances[Balance] );
MaxBalanceDatesWithLineage
)
RETURN
Result
Рис. 7.4. В отчете показаны балансы на последнюю дату по клиентам
с выводом итоговой колонки
200
 Глава 7. Полуаддитивные вычисления
В эту формулу вы можете захотеть внести еще одно изменение. Вы наверняка заметили, что по Кэти Джордан за четвертый квартал 2020 года
информация отсутствует. Причина в том, что последняя дата в этом случае находилась за пределами текущего контекста фильтра по кварталу.
Если вы хотите, чтобы в таких случаях до конца года (и на следующие
годы, если они есть) протягивалось значение сентября, вам придется
модифицировать меру следующим образом.
Мера в таблице Balances:
Balance LastDateByCustomerEver :=
VAR MaxDate =
MAX ( 'Date'[Date] )
VAR MaxBalanceDates =
CALCULATETABLE (
ADDCOLUMNS (
SUMMARIZE ( Balances, Customers[Name] );
"@MaxBalanceDate"; CALCULATE ( MAX ( Balances[Date] ) )
);
'Date'[Date] <= MaxDate
)
VAR MaxBalanceDatesWithLineage =
TREATAS ( MaxBalanceDates; Customers[Name]; 'Date'[Date] )
VAR Result =
CALCULATE ( SUM ( Balances[Balance] ); MaxBalanceDatesWithLineage )
RETURN
Result
Результат вычисления меры Balance LastDateByCustomerEver показан
на рис. 7.5.
Остатки на начало и конец периода
Предыдущие вычисления мер для последней даты временного интервала могут быть использованы для расчета остатка на конец периода –
в зависимости от требований вы можете выбрать подходящую технику. В то же время приемы для извлечения первой даты не могут быть
применены в качестве альтернативы для расчета остатка на начало
периода, который обычно эквивалентен остатку на конец предыдущего периода.
В мере Opening мы производим фильтрацию по дню, предшествующему первому дню временного интервала, тогда как в мере Closing для
получения последнего дня периода достаточно использования функции LASTDATE.
Остатки на начало и конец периода  201
Рис. 7.5. Последний доступный баланс по клиентам протягивается
до конца года
Мера в таблице Balances:
Opening :=
VAR PreviousClosingDate =
DATEADD ( FIRSTDATE ( 'Date'[Date] ); -1; DAY )
VAR Result =
CALCULATE ( SUM ( Balances[Balance] ); PreviousClosingDate )
RETURN
Result
Мера в таблице Balances:
Closing :=
CALCULATE (
SUM ( Balances[Balance] );
LASTDATE ( 'Date'[Date] )
)
202
 Глава 7. Полуаддитивные вычисления
В отчете, показанном на рис. 7.6, видно, что у Кэти Джордан пустой
начальный остаток из-за отсутствия данных на 31 декабря 2019 года.
Таким образом, этот шаблон для начального и конечного остатков перекликается с нашим первым шаблоном с первой и последней датами, –
он также работает только в случае наличия баланса на конец месяца для
всех клиентов.
Рис. 7.6. Начальный и конечный остатки с использованием
стандартных функций DAX
Помимо стандартных в языке DAX присутствуют специальные функции логики операций со временем, предназначенные для конкретных
периодов: месяца, квартала или года. Но эти функции являются более
сложными и требуют написания дополнительного кода в мерах. Таким
образом, использовать их следует только в том случае, если начальный
и конечный остатки в вашем отчете вычисляются с фиксированой гранулярностью вне зависимости от выбора. Например, если мера призвана возвращать начальный и конечный остатки в соответствующем году,
можно сколько угодно выбирать уровни месяца или квартала, возвращаемым значением этой меры все равно будет годовой остаток.
В нашем простом отчете функция CLOSINGBALANCEMONTH
может быть использована вместо CLOSINGBALANCEQUARTER и
Остатки на начало и конец периода  203
CLOSINGBALANCEYEAR, поскольку для последнего месяца периода они возвращают такой же результат. То же самое можно сказать и в отношении трио функций OPENINGBALANCEMONTH,
OPENINGBALANCEQUARTER и OPENINGBALANCEYEAR, но применительно к первому месяцу периода.
Определения мер Opening Dax и Closing Dax будут выглядеть следующим образом.
Мера в таблице Balances:
Opening Dax :=
OPENINGBALANCEMONTH (
SUM ( Balances[Balance] );
'Date'[Date]
)
Мера в таблице Balances:
Closing Dax :=
CLOSINGBALANCEMONTH (
SUM ( Balances[Balance] );
'Date'[Date]
)
Если вы хотите добиться от этих мер поведения, сравнимого с шаблоном Первая и последняя даты по клиенту, который мы описывали
ранее, для меры Closing Ever вам понадобится написанная нами мера
Balance LastDateByCustomerEver, и небольших доработок будет достаточно, чтобы получилась мера Opening Ever.
Мера в таблице Balances:
Opening Ever :=
VAR MinDate =
MIN ( 'Date'[Date] )
VAR MaxBalanceDates =
CALCULATETABLE (
ADDCOLUMNS (
SUMMARIZE ( Balances; Customers[Name] );
"@MaxBalanceDate"; CALCULATE ( MAX ( Balances[Date] ) )
);
'Date'[Date] < MinDate
)
204
 Глава 7. Полуаддитивные вычисления
VAR MaxBalanceDatesWithLineage =
TREATAS ( MaxBalanceDates; Customers[Name]; 'Date'[Date] )
VAR Result =
CALCULATE ( SUM ( Balances[Balance] ); MaxBalanceDatesWithLineage )
RETURN
Result
Мера в таблице Balances:
Closing Ever :=
VAR MaxDate =
MAX ( 'Date'[Date] )
VAR MaxBalanceDates =
CALCULATETABLE (
ADDCOLUMNS (
SUMMARIZE ( Balances; Customers[Name] );
"@MaxBalanceDate"; CALCULATE ( MAX ( Balances[Date] ) )
);
'Date'[Date] <= MaxDate
)
VAR MaxBalanceDatesWithLineage =
TREATAS ( MaxBalanceDates; Customers[Name]; 'Date'[Date] )
VAR Result =
CALCULATE ( SUM ( Balances[Balance] ); MaxBalanceDatesWithLineage )
RETURN
Result
По рис. 7.7 видно, что входящий остаток Кэти Джордан по январю и
первому кварталу 2020 года совпадает с последним исходящим остатком, доступным в 2019 году.
Рост за период
Этот шаблон может быть применен для вычисления величины изменения значения меры за выбранный временной интервал. На примере,
показанном на рис. 7.8, вычислим разницу между начальным и конечным остатком за выбранный период.
В мере Growth мы используем ранее представленные меры Opening и
Closing, базирующиеся на шаблоне Первая и последняя даты.
Мера в таблице Balances:
Growth :=
VAR Opening = [Opening]
-- Используйте меру Opening Ever, если нужно
Рост за период  205
VAR Closing = [Closing]
-- Используйте меру Closing Ever, если нужно
VAR Delta =
IF (
NOT ISBLANK ( Opening ) && NOT ISBLANK ( Closing );
Closing - Opening
)
VAR Result =
IF ( Delta <> 0; Delta )
RETURN
Result
Как ясно из комментариев, сопровождающих меру Growth, допустимо использовать альтернативную логику для получения начальных и
конечных остатков. Для этого достаточно переопределить переменные
Opening и Closing.
Так, например, может выглядеть мера Growth Ever, основывающаяся
на мерах Opening Ever и Closing Ever, описанных ранее в шаблоне Остатки на начало и конец периода.
Рис. 7.7. Начальный и конечный остатки с использованием пользовательских
вычислений
206
 Глава 7. Полуаддитивные вычисления
Рис. 7.8. Разница между начальным и конечным остатком
Мера в таблице Balances:
Growth Ever :=
VAR Opening = [Opening Ever]
VAR Closing = [Closing Ever]
VAR Delta =
IF (
NOT ISBLANK ( Opening ) && NOT ISBLANK ( Closing );
Closing - Opening
)
VAR Result =
IF ( Delta <> 0; Delta )
RETURN
Result
Рост за период  207
Результат вычисления меры Growth Ever показан на рис. 7.9.
Рис. 7.9. Разница между начальным и конечным остатком (в версии Ever)
Глава
8
Нарастающие итоги
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Шаблон нарастающих итогов позволит вам производить вычисления,
подобные расчету суммы с накоплением. Его можно применять для отслеживания складских запасов или формирования балансовых отчетов
с использованием исходных транзакций, а не снимков данных по периодам.
Например, чтобы сформировать таблицу с остатками всех товаров на
складах на каждый месяц, вы можете воспользоваться этим шаблоном,
имея в наличии лишь таблицу движения товаров – без предварительной обработки и консолидации данных.
Наиболее частым примером вычисления суммы с накоплением является агрегирование всех транзакций до определенной даты. Эти же
расчеты могут использоваться в любом сценарии, в котором вы накапливаете значения по сортируемому столбцу, что будет показано в одном из примеров данного шаблона.
Базовый сценарий
Нам необходимо создать меру, суммирующую все значения до определенной даты. Результат должен выглядеть так, как показано на
рис. 8.1.
Формула должна вычислять значение меры Sales Amount по всем датам до последней видимой даты в текущем контексте фильтра включительно. В коде также должна выполняться проверка того, что мы не увидим значения для будущих дат, т. е. когда минимальная видимая дата
больше, чем последняя дата с продажами.
Базовый сценарий  209
Рис. 8.1. Сумма с накоплением аккумулирует
значения до определенной даты
Мера в таблице Sales:
Sales Amount RT :=
VAR LastVisibleDate =
MAX ( 'Date'[Date] )
VAR FirstVisibleDate =
MIN ( 'Date'[Date] )
VAR LastDateWithSales =
CALCULATE (
MAX ( 'Sales'[Order Date] );
REMOVEFILTERS ()
-- Используйте ALL ( Sales ), если
-- REMOVEFILTERS () и ALL () недоступны
)
VAR Result =
IF (
FirstVisibleDate <= LastDateWithSales;
CALCULATE (
[Sales Amount];
'Date'[Date] <= LastVisibleDate
)
)
RETURN
Result
Чтобы формула работала корректно, необходимо убедиться, что таблица Date помечена в модели данных как таблица дат. В противном слу-
Powered by TCPDF (www.tcpdf.org)
210
 Глава 8. Нарастающие итоги
чае нужно добавить еще один модификатор REMOVEFILTERS на таблицу Date в функцию CALCULATE при вычислении переменной Result:
VAR Result =
IF (
FirstVisibleDate <= LastDateWithSales;
CALCULATE (
[Sales Amount];
'Date'[Date] <= LastVisibleDate;
REMOVEFILTERS ( 'Date' )
)
)
В формуле меры Sales Amount RT применяется фильтр к таблице Date,
удаляющий все ранее наложенные на нее фильтры. Если вам необходимо сохранить какие-либо из них, придется устанавливать их заново. Например, чтобы вычислить сумму с накоплением и сохранить при
этом фильтр по дню недели, нужно модифицировать код следующим
образом.
Мера в таблице Sales:
RT Weekdays :=
VAR LastVisibleDate =
MAX ( 'Date'[Date] )
VAR FirstVisibleDate =
MIN ( 'Date'[Date] )
VAR LastDateWithSales =
CALCULATE (
MAX ( 'Sales'[Order Date] );
REMOVEFILTERS ()
)
VAR Result =
IF (
FirstVisibleDate <= LastDateWithSales;
CALCULATE (
[Sales Amount];
'Date'[Date] <= LastVisibleDate;
VALUES ( 'Date'[Day of Week] )
)
)
RETURN
Result
На рис. 8.2 показана разница в поведении мер RT Weekdays и Sales
Amount RT в зависимости от установки фильтра по дню недели.
Нарастающие итоги по столбцам с сортировкой  211
Рис. 8.2. В мере RT Weekdays накопление значений идет только по выбранным
дням недели, тогда как мера Sales Amount RT игнорирует этот фильтр
Нарастающие итоги по столбцам
с сортировкой
Наиболее часто шаблон с нарастающими итогами базируется на датах.
И все же он может быть адаптирован к любым столбцам, к которым может быть применена операция сортировки. Возможность сортировать
столбец очень важна, поскольку в код для его правильной работы включено условие «меньше или равно».
В качестве примера давайте классифицируем покупателей на основании объемов продаж согласно таблице, изображенной на рис. 8.3.
Рис. 8.3. Конфигурационная таблица для классификации покупателей
Нам необходимо создать отчет, в котором показывались бы объемы продаж для каждого класса покупателей с нарастающей суммой по
классам, как показано на рис. 8.4.
Рис. 8.4. Сумма с накоплением рассчитывает сумму продаж с учетом
предшествующих классов покупателей
В формуле мы должны уделить особое внимание возможности сортировать столбец. В самом деле, поскольку в отчет мы выводим стол-
212
 Глава 8. Нарастающие итоги
бец Customer[Customer Class], а сортировка выполняется по столбцу
Customer[Customer Class Number], формула должна переопределять
фильтры по обоим столбцам, даже если все вычисление базируется на
порядковом номере класса.
Мера в таблице Sales:
Sales Amount RT Class :=
VAR LastVisibleClass =
MAX ( Customer[Customer Class Number] )
VAR ClassesToSum =
FILTER (
ALLSELECTED (
Customer[Customer Class];
Customer[Customer Class Number]
);
Customer[Customer Class Number] <= LastVisibleClass
)
VAR Result =
CALCULATE (
[Sales Amount];
ClassesToSum
)
RETURN
Result
Функция ALLSELECTED используется для того, чтобы при вычислении переменной ClassesToSum учитывались только классы, видимые в
элементе визуализации для расчета нарастающих итогов. Если сортировка по столбцу не задействуется, функция ALLSELECTED может содержать единственный столбец для фильтрации.
Глава
9
Таблица параметров
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Шаблон с использованием таблицы параметров (parameter table) может применяться для создания интерактивных отчетов, в которых
пользователь имеет возможность самостоятельно настраивать срезы и
динамически менять поведение отчета. Например, в отчете могут выводиться N лидирующих товаров по категориям, и пользователь может
сам решить посредством настройки среза, сколько лучших товаров он
хочет видеть: 3, 5, 10 или сколько-то еще. Значения для параметров
должны находиться в одной или нескольких отдельных таблицах, не
объединенных связями с другими таблицами в модели данных. В этой
главе мы рассмотрим несколько примеров использования таблиц параметров, но их применение гораздо шире и не ограничивается описанными здесь задачами.
В данном шаблоне мы будем создавать таблицу параметров при помощи языка DAX. Соответствующий инструмент в Power BI Desktop использует похожую технику. По сути, он создает срез, привязанный к вычисляемой таблице, рассчитанной при помощи функции GENERATESERIES.
Также создается мера, возвращающая выбранное значение параметра.
Именно такого подхода мы будем придерживаться в этом шаблоне.
Главным преимуществом создания вычисляемой таблицы вручную в
DAX является большая гибкость при использовании параметров.
Изменение единиц измерения меры
Пользователю может понадобиться менять по своему усмотрению единицы измерения меры Sales Amount с долларов на тысячи или милли-
214
 Глава 9. Таблица параметров
оны долларов. Это можно реализовать при помощи среза в отчете, как
показано на рис. 9.1. Хотя общий объем продаж перевалил за отметку в
20 млн долл., в таблице цифры отображаются в тысячах, согласно выбору в таблице параметров слева.
Рис. 9.1. Пользователь может выбрать единицу измерения
меры Sales Amount посредством среза
Размещение среза на дашборде требует наличия таблицы Scale со
списком возможных единиц измерения. Эта таблица должна содержать
два столбца: наименование, как оно будет показываться в срезе (Units,
Thousands, Millions), и числовой делитель для преобразования значений меры (1, 1000, 1 000 000). Вычисляемая таблица Scale может быть
создана при помощи функции DATATABLE.
Вычисляемая таблица:
Scale =
DATATABLE (
"Scale"; STRING;
"Denominator"; INTEGER;
{
{ "Units"; 1 };
{ "Thousands"; 1000 };
{ "Millions"; 1000000 }
}
)
После этого лучше всего будет упорядочить таблицу Scale по
Denominator при помощи соответствующего инструмента.
Далее нужно скорректировать значение меры Sales Amount в соответствии с выбранным значением Scale[Denominator].
Изменение единиц измерения меры  215
Мера в таблице Sales:
Sales Amount :=
VAR RealValue =
SUMX ( Sales; Sales[Quantity] * Sales[Net Price] )
VAR Denominator =
SELECTEDVALUE ( Scale[Denominator]; 1 )
VAR Result =
DIVIDE ( RealValue; Denominator )
RETURN
Result
Стоит заметить, что, хоть срез в визуализации и основан на столбце Scale[Scale], он через перекрестную фильтрацию связан со столбцом
Scale[Denominator], что позволяет напрямую обращаться к нему в функции SELECTEDVALUE.
Если на основании одного среза необходимо корректировать значения нескольких мер, целесообразно будет определить меру, возвращающую значение делителя вместо повторения одного и того же фрагмента кода в каждой мере, которая должна быть адаптирована.
Мера (скрытая) в таблице Scale:
Scale Denominator :=
SELECTEDVALUE ( Scale[Denominator]; 1 )
Мера в таблице Sales:
Gross Sales :=
DIVIDE (
SUMX ( Sales; Sales[Quantity] * Sales[Unit Price] );
[Scale Denominator]
)
Мера в таблице Sales:
Total Cost :=
DIVIDE (
SUMX ( Sales; Sales[Quantity] * Sales[Unit Cost] );
[Scale Denominator]
)
216
 Глава 9. Таблица параметров
Множество независимых параметров
Если вычисление зависит сразу от нескольких параметров, в модели
данных необходимо создать таблицы для всех из них – по одной для
каждого независимого параметра.
Представьте себе такую систему скидок для заказов: когда общее количество товаров в заказе превышает заданное число (параметр Min
Quantity), в мере Discounted Amount применяется скидка (параметр
Discount) к транзакции. Пользователь при этом может делать нужные
ему прикидки применительно к историческим данным посредством
управления срезами, как показано на рис. 9.2.
Рис. 9.2. В мере Discounted Amount применена скидка в размере 15 %
к заказам с количеством товаров, большим шести
В реализации меры Discounted Amount мы сначала готовим таблицу
заказов в табличной переменной Orders, включающей количество и
сумму заказов. Результат получается путем прохода по таблице в переменной Orders и применения скидки к каждому из заказов, общее количество товаров в которых больше или равно минимальному количеству
для скидки.
Мера в таблице Sales:
# Quantity :=
SUM ( Sales[Quantity] )
Мера в таблице Sales:
Discounted Amount :=
VAR MinQty =
SELECTEDVALUE ( 'Min Quantity'[Min Quantity]; 1 )
Множество зависимых параметров  217
VAR Disc =
SELECTEDVALUE ( Discount[Discount]; 0 )
VAR Orders =
ADDCOLUMNS (
SUMMARIZE ( Sales; Sales[Order Number] );
"@Qty"; [# Quantity];
"@Amt"; [Sales Amount]
)
VAR Result =
SUMX (
Orders;
IF (
[@Qty] >= MinQty;
( 1 - Disc ) * [@Amt];
[@Amt]
)
)
RETURN
Result
При использовании нескольких таблиц параметров сами параметры
остаются независимыми друг от друга. Иначе говоря, пользователь может выбрать любые значения этих параметров, и установка одних параметров никак не повлияет на другие. Если необходимо ограничить возможное количество комбинаций параметров в разных срезах, советуем
реализовать шаблон с зависимыми параметрами.
Множество зависимых параметров
Если вычисление должно зависеть от нескольких параметров с ограниченным списком возможных вариантов, можно хранить все параметры в отдельных столбцах одной таблицы, каждая строка которой будет
представлять доступную комбинацию сочетания параметров.
Возьмем для примера сценарий из предыдущего раздела с двумя
параметрами: Min Quantity и Discount. Единственным дополнительным
условием будет то, что процент скидки не может более чем в десять
раз превышать значение минимального количества для обеспечения
скидки. Иными словами, если пользователь выберет в первом параметре цифру 3, доступными останутся лишь скидки, не превышающие 30 %.
На практике это реализовано так: при выборе в соответствующем срезе значения параметра Min Quantity в соседнем разделе Discount останутся только варианты, удовлетворяющие выбранному минимальному
количеству. На рис. 9.3 представлен внешний вид такого сценария.
218
 Глава 9. Таблица параметров
Рис. 9.3. Поскольку в минимальном количестве выбрана тройка,
максимально предлагаемой скидкой оказалась скидка в 30 %
Мера Discounted Amount в этом шаблоне будет такой же, как и в предыдущем примере, – она подготавливает табличную переменную Orders,
включающую количество товаров и общую сумму по каждому заказу, а
также обеспечивает итерации по этой переменной.
Мера в таблице Sales:
# Quantity :=
SUM ( Sales[Quantity] )
Мера в таблице Sales:
Discounted Amount :=
VAR MinQty =
SELECTEDVALUE ( Discount[Min Quantity]; 1 )
VAR Disc =
SELECTEDVALUE ( Discount[Discount]; 0 )
VAR Orders =
ADDCOLUMNS (
SUMMARIZE ( Sales; Sales[Order Number] );
"@Qty"; [# Quantity];
"@Amt"; [Sales Amount]
)
VAR Result =
SUMX (
Orders;
IF (
[@Qty] >= MinQty;
( 1 - Disc ) * [@Amt];
[@Amt]
)
Динамический выбор N лидирующих товаров  219
)
RETURN
Result
Таблица Discount содержит оба параметра в столбцах Discount[Min
Quantity] и Discount[Discount]. При этом она должна включать только
строки с допустимыми комбинациями минимального количества в заказе и скидки. Следующая формула поможет сгенерировать вычисляемую таблицу Discount, в которой размер скидки не будет превышать
десятикратного минимального количества товаров.
Вычисляемая таблица:
Discount =
VAR Discounts =
SELECTCOLUMNS ( GENERATESERIES ( 0; 19; 1 ); "Discount"; [Value] / 20 )
VAR Quantities =
SELECTCOLUMNS ( GENERATESERIES ( 1; 10; 1 ); "Min Quantity"; [Value] )
RETURN
GENERATE (
Quantities;
FILTER (
Discounts;
[Discount] <= [Min Quantity] / 10
)
)
В таблице Discount не будет строк, например, с минимальным количеством 3 и скидкой 50 %. Таким образом, когда пользователь в левом
срезе выбирает значение 3 для минимального количества, правый срез
автоматически заполняется значениями скидок до 30 % включительно.
Связь между двумя и более параметрами поддерживается неявно за
счет нахождения этих столбцов в одной таблице и оказывает влияние
на срезы визуализации посредством перекрестной фильтрации.
Динамический выбор N лидирующих товаров
Представьте, что вам необходимо получить отчет, подобный тому, что
показан на рис. 9.4. Каждый столбец здесь фильтрует свое количество
товаров с наивысшими значениями меры Sales Amount. Иными словами,
в каждой колонке показан объем продаж по N лидирующим товарам,
где число N присутствует в названии столбца. В данном случае каждое
видимое название колонки преобразовано с использованием значения
параметра в мере Top Sales.
220
 Глава 9. Таблица параметров
Рис. 9.4. Параметр TopN Products, соответствующий столбцу, определяет
количество товаров, по которым должна вычисляться мера Sales Amount
В Power BI такой отчет получить сложно, поскольку в одном элементе визуализации может находиться только один фильтр уровня
визуализации Top N. В этом случае для каждого столбца будет передаваться свой аргумент в функцию TOPN, используемую в мере Top
Sales.
В таблице параметров должны присутствовать два столбца: один
для наименования (TopN Products), содержащий описание параметра, а второй (TopN), представляющий номер, соответствующий результату выбора параметра и порядку сортировки значений в TopN
Products.
Таблица TopN Filter может быть определена при помощи следующего
кода.
Вычисляемая таблица:
TopN Filter =
ADDCOLUMNS (
SELECTCOLUMNS (
{ 0; 1; 5; 10; 20; 50 };
"TopN"; [Value]
);
"TopN Products"; IF (
[TopN] = 0;
"All";
"Top " & [TopN]
)
)
В мере Top Sales применяется фильтрация количества лидирующих
товаров путем вычисления меры Sales Amount.
Динамический выбор N лидирующих товаров  221
Мера в таблице Sales:
Top Sales :=
VAR TopNvalue =
SELECTEDVALUE ( 'TopN Filter'[TopN]; 0 )
VAR TopProducts =
TOPN (
TopNvalue;
'Product';
[Sales Amount]
)
VAR AllSales = [Sales Amount]
VAR TopSales =
CALCULATE (
[Sales Amount];
KEEPFILTERS ( TopProducts )
)
VAR Result =
IF (
TopNvalue = 0;
AllSales;
TopSales
)
RETURN
Result
Глава
10
Статическая сегментация
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Шаблон статической сегментации (static segmentation) позволяет классифицировать числовые значения по диапазонам. Типичный пример – анализ продаж по диапазону цен. Вы не хотите осуществлять срез
данных по продажам по какой-то одной цене товара. Вместо этого вам
хочется упростить анализ, группируя цены в диапазоны. Сами диапазоны при этом хранятся в конфигурационной таблице, и шаблон предполагает, что модель будет полностью управляться данными. Это значит,
что при обновлении конфигурационной таблицы модель также будет
обновляться без необходимости вносить правки в код на DAX.
В зависимости от объема модели данных этот шаблон предполагает
разные варианты реализации. Если данных не так много (до нескольких миллионов строк), лучше всего будет использовать вычисляемые
столбцы и/или вычисляемые связи. На более объемных моделях (с сотнями миллионов записей) вычисляемые столбцы могут увеличить время обработки модели. В этом случае лучше будет создать вычисляемую
таблицу для цен, тем самым снизив до минимума количество вычисляемых столбцов в больших таблицах.
Базовый шаблон
Задача – проанализировать продажи по диапазонам цен. Для ее решения мы создадим конфигурационную таблицу, хранящую ценовые
диапазоны: цена должна быть больше или равна нижней границе (Min
Price) и меньше верхней (Max Price), как показано на рис. 10.1.
Базовый шаблон  223
Рис. 10.1. В конфигурационной таблице определены
ценовые диапазоны
Затем необходимо провести анализ продаж по диапазонам цен, получив отчет, показанный на рис. 10.2.
Рис. 10.2. В отчете показаны продажи
с разбивкой по диапазонам цен
Уровень VERY LOW (очень низкий) соответствует продажам товаров
с ценой до 100 долл.
Для получения желаемого результата необходимо настроить связь
между конфигурационной таблицей (Price Ranges) и таблицей продаж
(Sales). В нашем примере мы используем для определения цены товара
поле Sales[Net Price], а не Sales[Unit Price], чтобы учесть возможные скидки. Требуемое объединение должно использовать условие «between»
(между), что не поддерживается в движке Tabular по умолчанию. Но мы
можем создать в таблице Sales вычисляемый столбец, в котором будут
храниться ключи ценовых диапазонов для каждой строки. Для этого
понадобится следующая формула.
Вычисляемый столбец в таблице Sales:
PriceRangeKey =
VAR CurrentPrice = Sales[Net Price]
VAR FilterSegment =
FILTER (
'Price Ranges';
AND (
'Price Ranges'[Min Price] < CurrentPrice;
'Price Ranges'[Max Price] >= CurrentPrice
224
 Глава 10. Статическая сегментация
)
)
VAR Result =
CALCULATE (
DISTINCT ( 'Price Ranges'[PriceRangeKey] );
FilterSegment
)
RETURN
Result
При создании вычисляемого столбца не стоит использовать функции,
которые могут ссылаться на пустые строки, такие как ALL и VALUES.
Именно поэтому мы применили функцию DISTINCT вместо VALUES для
извлечения ключа ценового диапазона.
После этого необходимо объединить таблицы Sales и Price Ranges на
основании нового вычисляемого столбца, как показано на рис. 10.3.
Рис. 10.3. Связь, основывающаяся на вычисляемом столбце
Создав связь между таблицами, можно осуществлять срез продаж по
полю 'Price Ranges'[Price Range].
При этом необходимо убедиться, что конфигурационная таблица заполнена правильно – чтобы одна цена могла принадлежать одному и
только одному ценовому диапазону. Присутствие в конфигурационной
таблице перекрывающихся сегментов может привести к ошибке при
вычислении столбца PriceRangeKey. Если вы хотите удостовериться в
том, что в вашей таблице все заполнено правильно, без перекрывающихся диапазонов, вы можете заполнять содержимое колонки Max Price
для следующего сегмента с использованием вычисляемого столбца,
извлекающего значения из Min Price для предыдущего. Это показано в
следующем примере.
Базовый шаблон  225
Вычисляемый столбец в таблице Price Ranges:
Max Price Calculated =
VAR CurrentMinPrice = 'Price Ranges'[Min Price]
VAR NextMinPrice =
CALCULATE (
MIN ( 'Price Ranges'[Min Price] );
REMOVEFILTERS ( 'Price Ranges' );
'Price Ranges'[Min Price] > CurrentMinPrice
)
VAR MaxPrice =
IF ( ISBLANK ( NextMinPrice ); 999999999; NextMinPrice )
RETURN
MaxPrice
Можно также написать более безопасную версию формулы, которая
будет записывать пустые значения или генерировать ошибку, если одной цене будут соответствовать несколько диапазонов, как показано
ниже.
Вычисляемый столбец в таблице Sales:
PriceRangeKey =
VAR CurrentPrice = Sales[Net Price]
VAR FilterSegment =
FILTER (
'Price Ranges';
AND (
'Price Ranges'[Min Price] < CurrentPrice;
'Price Ranges'[Max Price] >= CurrentPrice
)
)
VAR FilteredPriceRangeKey =
CALCULATETABLE (
DISTINCT ( 'Price Ranges'[PriceRangeKey] );
FilterSegment
)
VAR Result =
IF (
COUNTROWS ( FilteredPriceRangeKey ) = 1;
FilteredPriceRangeKey;
-- В следующей строке генерируется ошибка в вычисляемом столбце.
-- Вы можете заменить ERROR на BLANK(), чтобы просто игнорировать
-- цены, соответствующие нескольким сегментам, но помните, что
-- это может привести к сокрытию ошибок в будущих отчетах.
ERROR ( "Пересекающиеся диапазоны цен в таблице Price Ranges" )
)
226
 Глава 10. Статическая сегментация
RETURN
Result
Код, показанный в этом шаблоне, должен удовлетворять требованиям к вычисляемым столбцам, используемым в связях, чтобы избежать
возникновения циклических зависимостей. Подробнее об этом можно
почитать по адресу https://sql.bi/59891.
Диапазоны цен по категориям
Одним из вариантов статической сегментации является случай с более
сложным разделением значений на диапазоны, чем простое условие
вхождения. Например, вам может понадобиться вести свои диапазоны
для разных категорий товаров: сегмент LOW (низкий) для игр и игрушек должен отличаться от аналогичного сегмента для бытовой техники.
В данном сценарии конфигурационная таблица будет содержать дополнительный столбец, показывающий, для какой категории товаров
мы определяем ценовой диапазон. Для разных категорий у нас будут
разные сегменты, что видно по рис. 10.4.
Рис. 10.4. Конфигурационная таблица, содержащая категории товаров
Этот шаблон в значительной степени похож на предыдущий, единственным отличием является условие для нахождения ключа корректного диапазона цен. А именно, нам необходимо ограничить выбор строк
из таблицы Price Ranges по нужной категории и вхождению цены в диапазон.
Вычисляемый столбец в таблице Sales:
PriceRangeKey =
VAR CurrentPrice = Sales[Net Price]
VAR CurrentCategory = RELATED ( 'Product'[Category] )
Диапазоны цен по категориям  227
VAR FilterSegment =
FILTER (
'Price Ranges';
'Price Ranges'[Category] = CurrentCategory
&& 'Price Ranges'[Min Price] < CurrentPrice
&& 'Price Ranges'[Max Price] >= CurrentPrice
)
VAR FilteredPriceRangeKey =
CALCULATETABLE (
DISTINCT ( 'Price Ranges'[PriceRangeKey] );
FilterSegment
)
VAR Result =
IF (
COUNTROWS ( FilteredPriceRangeKey ) = 1;
FilteredPriceRangeKey;
ERROR ( "Пересекающиеся диапазоны цен в таблице Price Ranges" )
)
RETURN
Result
Похожим образом вы можете использовать любое другое условие, если
оно обеспечит гарантию того, что в конфигурационной таблице останется видна одна строка. Чтобы удостовериться, что в таблице не содержится пересечения диапазонов цен, вы можете, как и в предыдущем шаблоне, заполнять столбец Max Price на основании вычисляемого столбца. Но
в данном случае необходимо будет использовать функцию ALLEXCEPT
вместо REMOVEFILTERS, чтобы фильтр по столбцу 'Price Ranges'[Category],
возникающий вследствие действия преобразования контекста (context
transition), сохранялся в итоговом контексте фильтра.
Вычисляемый столбец в таблице Price Ranges:
Max Price Calculated =
VAR CurrentMinPrice = 'Price Ranges'[Min Price]
VAR NextMinPrice =
CALCULATE (
MIN ( 'Price Ranges'[Min Price] );
-- Функция ALLEXCEPT используется, чтобы в фильтр попадали только
-- сегменты нужной категории товаров
ALLEXCEPT ( 'Price Ranges'; 'Price Ranges'[Category] );
'Price Ranges'[Min Price] > CurrentMinPrice
)
VAR MaxPrice =
IF ( ISBLANK ( NextMinPrice ); 999999999; NextMinPrice )
RETURN
MaxPrice
228
 Глава 10. Статическая сегментация
Диапазоны цен в объемных таблицах
Шаблон статической сегментации требует создания вычисляемого
столбца в таблице Sales. Сам столбец обычно много места не занимает, поскольку содержит не так много уникальных значений. Однако с
увеличением объема исходной таблицы размер столбца может начать
расти, что может привести к возникновению проблемы, связанной с
необходимостью полного пересчета столбца при каждом обновлении
данных. На таблицах объемом в несколько миллиардов записей, которые с большой вероятностью будут разбиты на секции, столбец будет
обновляться для всей таблицы при обновлении всего одной секции. Это
приведет к замедлению работы при каждой операции обновления.
В таких случаях можно воспользоваться специальной версией шаблона статического сегментирования, не подразумевающей создания
вычисляемого столбца в таблице Sales. Вместо создания связи по новому вычисляемому столбцу можно использовать колонку Sales[Net Price]
в качестве ключа для связи с новой вычисляемой таблицей. На самом
деле мы не можем создать связь между таблицами Sales и Price Ranges,
поскольку в Price Ranges отсутствует подходящий столбец. Но этот столбец может быть создан путем увеличения количества строк в конфигурационной таблице.
Таблица, которую мы хотим получить, должна содержать по одной
строке для каждого значения из колонки Sales[Net Price] с указанием
соответствующего диапазона цен, как показано на рис. 10.5.
Рис. 10.5. Расширенная конфигурационная таблица
с одной строкой для каждого значения Net Price
Мы переименовали исходную конфигурационную таблицу в Price
Ranges Configuration. Таблица Price Ranges может быть создана как вычисляемая по следующей формуле.
Вычисляемая таблица:
Price Ranges =
GENERATE (
Диапазоны цен в объемных таблицах  229
'Price Ranges Configuration';
FILTER (
ALLNOBLANKROW ( Sales[Net Price] );
AND (
Sales[Net Price] > 'Price Ranges Configuration'[Min Price];
Sales[Net Price] <= 'Price Ranges Configuration'[Max Price]
)
)
)
В новой таблице будет содержаться по одной строке для каждого уникального значения столбца Sales[Net Price]. Так что мы сможем объединить таблицы Sales и Price Ranges на основании столбца Net Price, как
показано на рис. 10.6.
Рис. 10.6. Связь между таблицами по полю Net Price
С учетом проведенной оптимизации более нет необходимости создавать новый столбец в таблице Sales, поскольку для установки связи
в модели данных используется существующий столбец Sales[Net Price].
А значит, при обновлении таблицы Sales никакие вычисляемые столбцы
пересчитываться не будут. Исходная таблица Price Ranges Configuration
должна быть скрыта в модели, чтобы не вводить в заблуждение конечных пользователей.
В небольших моделях данных создание вычисляемого столбца в таблице не представляет большой проблемы. Поэтому базовый шаблон, не
предусматривающий создания новых таблиц, все же является предпочтительным. Однако на объемных моделях лучше с точки зрения эффективности может себя показать последняя версия шаблона.
Глава
11
Динамическая сегментация
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Шаблон динамической сегментации (dynamic segmentation) может пригодиться для выполнения классификации сущностей, основываясь на мерах. Типичный пример – разделение клиентов на кластеры по объемам
затрат. Кластеризация выполняется динамически, так что разбиение на
категории учитывает активный фильтр в отчете. На самом деле один и
тот же клиент может относиться к разным кластерам в разные даты.
Базовый шаблон
Нам необходимо разбить наших покупателей на кластеры в зависимости от суммы покупок. Используя конфигурационную таблицу, показанную на рис. 11.1, определим кластеры.
Рис. 11.1. В конфигурационной таблице определены границы сегментов
Каждый сегмент представляет собой классификацию для покупателей, основываясь на значении меры Sales Amount по ним за год. С помощью этой таблицы вам необходимо проанализировать, сколько покупателей принадлежат каждому сегменту и как это соотношение меняется
с течением лет. При этом один и тот же покупатель может иметь статус
Silver в одном году и Platinum – в другом.
Базовый шаблон  231
Рис. 11.2. В отчете показано распределение покупателей
по сегментам в разрезе лет
По рис. 11.2 мы видим, например, что в 2007 году статус Silver был
у 2142 покупателей. Добавив к этому отчету срез Category, мы получаем возможность сегментировать наших покупателей в зависимости от
приобретения товаров из одной конкретной категории, как показано
на рис. 11.3.
Рис. 11.3. Распределение покупателей по сегментам с учетом приобретения
товаров из выбранной категории
Будучи динамическим, данный шаблон реализован посредством
меры. В мере осуществляется поиск покупателей, принадлежащих выбранному кластеру. Затем полученная таблица используется в качестве
фильтра в функции CALCULATE для ограничения вычисления выбранными покупателями. Модификатор KEEPFILTERS необходим для выполнения пересечения между списком покупателей и найденными на
предыдущем шаге покупателями.
Мера в таблице Sales:
# Seg. Customers :=
IF (
HASONEVALUE ( 'Date'[Calendar Year] ); -- Сегментация только по одному
-- выбранному году
VAR CustomersInSegment =
-- Получаем покупателей из текущего
-- сегмента
FILTER (
ALLSELECTED ( Customer );
VAR SalesOfCustomer = [Sales Amount] -- Берем Sales Amount
232
 Глава 11. Динамическая сегментация
-- для одного клиента
VAR SegmentForCustomer =
-- Извлекаем сегмент, которому
FILTER (
-- он принадлежит
'Customer segments';
NOT ISBLANK ( SalesOfCustomer )
&& 'Customer segments'[Min Sales] < SalesOfCustomer
&& 'Customer segments'[Max Sales] >= SalesOfCustomer
)
VAR IsCustomerInSegments = NOT ISEMPTY ( SegmentForCustomer )
RETURN IsCustomerInSegments
)
VAR Result =
CALCULATE (
COUNTROWS ( Customer );
-- Выражение для подсчета
KEEPFILTERS ( CustomersInSegment ) -- Применяем фильтр по клиентам
)
-- из сегмента
RETURN Result
)
Мера должна проходить по всем сегментам для каждого покупателя,
чтобы гарантировать, что подсчет будет корректным для произвольного выбора сегментов, как показано на рис. 11.4.
Рис. 11.4. В отчете показаны итоги по всем годам
с учетом только выбранных сегментов
По своей природе представленное здесь вычисление является неаддитивным. Показанная реализация будет работать только на уровне
лет, что вполне приемлемо в случае подсчета покупателей по сегментам. В этом случае один покупатель никогда не будет посчитан дважды.
Однако для других мер может быть необходимо, чтобы сегментация
обладала аддитивными свойствами. Например, представьте себе меру,
показывающую значение Sales Amount по покупателю в рамках сегмента с возможностью агрегировать итоги за несколько лет. Следующая
реализация меры носит аддитивный характер по годам.
Мера в таблице Sales:
Sales Seg. Customers :=
SUMX (
VALUES ( 'Date'[Calendar Year] ); -- Повторяем сегментацию по выбранным
-- годам
Базовый шаблон  233
VAR CustomersInSegment =
-- Получаем покупателей из текущего
-- сегмента
FILTER (
ALLSELECTED ( Customer );
VAR SalesOfCustomer = [Sales Amount] -- Берем Sales Amount для
-- одного клиента
VAR SegmentForCustomer =
-- Извлекаем сегмент, которому
FILTER (
-- он принадлежит
'Customer segments';
NOT ISBLANK ( SalesOfCustomer )
&& 'Customer segments'[Min Sales] < SalesOfCustomer
&& 'Customer segments'[Max Sales] >= SalesOfCustomer
)
VAR IsCustomerInSegments = NOT ISEMPTY ( SegmentForCustomer )
RETURN IsCustomerInSegments
)
VAR Result =
CALCULATE (
[Sales Amount];
-- Выражение для подсчета
KEEPFILTERS ( CustomersInSegment ) -- Применяем фильтр по клиентам
)
-- из сегмента
RETURN Result
)
Результат этого вычисления показан на рис. 11.5. Как видите, справа
выводятся итоги по строкам, в которых суммируются значения меры
по каждому году.
Рис. 11.5. Мера Sales Seg. Customers аддитивна по годам
Вам необходимо убедиться, что конфигурационная таблица заполнена корректно – чтобы любое возможное значение меры Sales Amount
относилось только к одному сегменту. Наличие пересекающихся границ сегментов в конфигурационном файле может привести к ошибкам
при вычислении переменной CustomersInSegment. Если вы хотите удостовериться, что в конфигурационной таблице нет ошибок, подобных
пересечению диапазонов, то можете сгенерировать содержимое колонки Max Sales с использованием вычисляемого столбца, автоматически
извлекающего значение столбца Min Sales для следующего сегмента.
Это показано в следующем коде.
234
 Глава 11. Динамическая сегментация
Вычисляемый столбец в таблице Customer Segments:
Max
VAR
VAR
VAR
Sales Calculated =
CurrentMinSales = 'Customer Segments'[Min Sales]
MaxEverSales = CALCULATE ( [Sales Amount]; REMOVEFILTERS ( ) )
NextMinSales =
CALCULATE (
MIN ( 'Customer Segments'[Min Sales] );
REMOVEFILTERS ( 'Customer Segments' );
'Customer Segments'[Min Sales] > CurrentMinSales
)
VAR MaxSales =
IF ( ISBLANK ( NextMinSales ); MaxEverSales; NextMinSales )
RETURN
MaxSales
Кластеризация по изменению продаж
Шаблон динамической сегментации очень гибок, поскольку позволяет
категоризировать сущности на основании динамических вычислений.
Более того, одна сущность может принадлежать сразу нескольким кластерам. Прекрасным примером подобной гибкости является кластеризация товаров на основании изменения их годовых продаж.
В нашей упрощенной модели данных мы будем считать, что если
изменение в объемах продаж товара за год укладывается в диапазон
+/– 20 %, то спрос на него стабильный. Соответственно, если зафиксирован спад продаж более чем на 20 % – спрос будет угасающий, а если
подъем больше чем на 20 % – растущий. При этом один и тот же товар
может в 2008 году попасть в категорию угасающего спроса, а в 2009-м –
в категорию стабильного, как показано на рис. 11.6.
Рис. 11.6. Один и тот же товар принадлежит разным кластерам в разные годы
Кластеризация по изменению продаж  235
Начнем с построения конфигурационной таблицы, показанной на
рис. 11.7.
Рис. 11.7. Конфигурационная таблица определяет границы сегментов
После создания таблицы с конфигурацией в модели данных можно
использовать слегка измененную вариацию базового шаблона. В этот
раз, однако, вместо отнесения покупателей к сегментам в зависимости
от объема покупок мы будем классифицировать товары по изменяемости спроса на них. Единственным отличием итоговой меры будет обращение к мере Growth %.
Мера в таблице Sales:
Growth % :=
VAR SalesCY = [Sales Amount]
VAR SalesPY =
CALCULATE (
[Sales Amount];
SAMEPERIODLASTYEAR ( 'Date'[Date] )
)
VAR Result =
IF (
NOT ISBLANK ( SalesCY ) && NOT ISBLANK ( SalesPY );
DIVIDE ( SalesCY - SalesPY; SalesPY )
)
RETURN
Result
Мера в таблице Sales:
# Seg. Products :=
IF (
HASONEVALUE ( 'Date'[Calendar Year]
VAR ProductsInSegment =
FILTER (
ALLSELECTED ( 'Product' );
VAR GrowthPerc = [Growth %]
VAR SegmentForProduct =
);
-- Получаем товары из текущего сегмента
-- Вычисляем Growth% для одного товара
-- Извлекаем сегмент, которому принад-- лежит товар
FILTER (
'Growth segments';
NOT ISBLANK ( GrowthPerc )
236
 Глава 11. Динамическая сегментация
&& 'Growth segments'[Min Growth] < GrowthPerc
&& 'Growth segments'[Max Growth] >= GrowthPerc
)
VAR IsProductInSegments = NOT ISEMPTY ( SegmentForProduct )
RETURN IsProductInSegments
)
VAR Result =
CALCULATE (
COUNTROWS ( 'Product' );
KEEPFILTERS ( ProductsInSegment )
)
RETURN Result
-- Выражение для подсчета
-- Применяем фильтр по товарам
-- из сегмента
)
Кластеризация по лучшему статусу
Шаблон динамической сегментации также может оказаться полезным
при разделении покупателей на кластеры таким образом, чтобы каждый из них попадал только в один единственный кластер в зависимости
от лучшего показателя годовых продаж по нему за весь период.
Если покупатели распределяются по кластерам статически, лучше будет воспользоваться шаблоном статической сегментации, который был
описан ранее. В случае динамического распределения, но без возможности в разные годы присваивать одному и тому же покупателю разный
статус, вам пригодится динамический шаблон.
Начнем с конфигурационной таблицы, показанной на рис. 11.8. Мы
будем присваивать покупателям статус в зависимости от наибольших
годовых продаж. Таким образом, покупатель будет обладать статусом
Platinum, если он за год тратил больше 500 долл. И если мы определили,
что покупатель обладает таким статусом, все его продажи по всем годам
будут попадать в этот сегмент.
Рис. 11.8. Конфигурационная таблица определяет границы сегментов
В отчете, показанном на рис. 11.9, продажи из сегмента Platinum захватывают транзакции всех покупателей, достигших этого статуса хотя
бы в одном из выбранных годов. И продажи в одном сегменте не могут
появиться в другом.
Кластеризация по лучшему статусу  237
Рис. 11.9. Продажи со срезом по наивысшим статусам покупателей
Мера, используемая в данном отчете, реализована при помощи одного из вариантов шаблона динамической сегментации. На этот раз нет
необходимости проходить по годам. В переменной CustomersInSegment
вычисляются максимальные продажи за каждый год в отчете с использованием меры Max Yearly Sales, в которой игнорируются все остальные
фильтры по таблице Date. Результат применяется в качестве фильтра
при вычислении меры Sales Amount.
Мера в таблице Sales:
Max Yearly Sales :=
MAXX (
ALLSELECTED ( 'Date'[Calendar Year] ); -- Итерации по выбранным годам
CALCULATE (
[Sales Amount];
-- Вычисляем Sales Amount за год,
ALLEXCEPT (
-- игнорируя остальные фильтры по таблице
'Date';
-- Date, за исключением пришедшего,
'Date'[Calendar Year] -- благодаря преобразованию контекста
)
)
)
Мера в таблице Sales:
Sales Seg. Customers :=
VAR CustomersInSegment =
-- Получаем клиентов из текущего сегмента
FILTER (
ALLSELECTED ( Customer );
VAR SalesOfCustomer = [Max Yearly Sales] -- Берем Sales Amount для
-- одного клиента
VAR SegmentForCustomer =
-- Извлекаем сегмент,
FILTER (
-- которому он принадлежит
'Customer segments';
NOT ISBLANK ( SalesOfCustomer )
&& 'Customer segments'[Min Sales] < SalesOfCustomer
&& 'Customer segments'[Max Sales] >= SalesOfCustomer
)
VAR IsCustomerInSegments = NOT ISEMPTY ( SegmentForCustomer )
RETURN IsCustomerInSegments
238
 Глава 11. Динамическая сегментация
)
VAR Result =
CALCULATE (
[Sales Amount];
KEEPFILTERS ( CustomersInSegment )
)
RETURN
Result
-- Выражение для подсчета
-- Применяем фильтр по клиентам
-- из сегмента
Глава
12
ABC-анализ
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Шаблон, реализующий на практике ABC-анализ, позволяет классифицировать сущности путем объединения их в группы, основываясь на
проценте в общем вкладе. Типичный пример проведения ABC-анализа заключается в разбиении всех товаров (сущности) на сегменты на
основе продаж (значения). Товары, пользующиеся большим спросом
(доля которых в общих продажах суммарно достигает 70 %), относятся
к категории A. Товары, составляющие следующие 20 % спроса, именуются категорией B, а оставшимся товарам присваивается низшая категория C. Именно по этим трем категориям, собственно, и назван рассматриваемый в данной главе вид анализа.
Этот шаблон может быть использован для идентификации ядра бизнеса компании – обычно на примере самых продаваемых товаров или
активных покупателей. Больше информации о проведении ABC-анализа содержится по адресу http://en.wikipedia.org/wiki/ABC_analysis.
ABC-анализ может быть как статическим, так и динамическим. В первом случае, как ясно из названия, категории присваиваются сущностям
статически – таким образом, что они не меняются в зависимости от
примененных в отчете фильтров. В то же время динамический ABCанализ подразумевает автоматический перерасчет категорий сущностей в зависимости установленных фильтров. Поэтому динамический
анализ необходимо реализовывать при помощи мер, что снижает эффективность алгоритма, зато повышает его гибкость.
Есть еще один тип кластеризации сущностей, лежащий в области между статическим и динамическим анализом. Это так называемый снимок
(snapshot) ABC. Например, если вам необходимо обновлять категорию
продукции на ежегодной основе, можно сделать это путем создания
Powered by TCPDF (www.tcpdf.org)
240
 Глава 12. ABC-анализ
таблицы снимка (snapshot table), содержащей классификацию товаров
для каждого года.
Статический ABC-анализ
В данном разделе мы будем разбивать товары на кластеры, основываясь на продажах. Каждому товару статически присваивается категория, которая в дальнейшем может использоваться как в строках, так и
в столбцах отчетов. Вывод, показанный на рис. 12.1, демонстрирует, что
товары категории A в количестве 493 наименований принесли компании более 21 млн долл., тогда как 1455 наименований товаров из категории C – лишь чуть более 3 млн долл.
Рис. 12.1. Категория товаров может использоваться в качестве фильтра
Статический ABC-анализ базируется на создании вычисляемых
столбцов в таблицах. Вам понадобятся новые колонки для анализа, показанные на рис. 12.2.
Рис. 12.2. Шаблон статического ABC-анализа требует
создания вычисляемых столбцов
Здесь вы видите четыре вычисляемых столбца, а именно:
• Product Sales: суммарные продажи по товару (на данный момент);
• Cumulated Sales: накопительная сумма продаж из столбца Product
Sales, упорядоченного по убыванию;
• Cumulated Pct: отношение столбца Cumulated Sales в общей сумме продаж;
• ABC Class: категория товара: A, B или C.
Статический ABC-анализ  241
Формулы для перечисленных вычисляемых столбцов будут следующие.
Вычисляемый столбец в таблице Product:
Product Sales =
[Sales Amount]
Вычисляемый столбец в таблице Product:
Cumulated Sales =
VAR CurrentProductSales = 'Product'[Product Sales]
VAR BetterProducts =
FILTER (
'Product';
'Product'[Product Sales] >= CurrentProductSales
)
VAR Result =
SUMX (
BetterProducts;
'Product'[Product Sales]
)
RETURN
Result
Вычисляемый столбец в таблице Product:
Cumulated Pct =
DIVIDE (
'Product'[Cumulated Sales];
SUM ( 'Product'[Product Sales] )
)
Вычисляемый столбец в таблице Product:
ABC Class =
SWITCH (
TRUE;
'Product'[Cumulated Pct] <= 0.7; "A";
'Product'[Cumulated Pct] <= 0.9; "B";
"C"
)
242
 Глава 12. ABC-анализ
Категория товара определяется исходя из значения столбца Cumulated
Pct. Как видно по рис. 12.3, для товаров с долей до 70 % определена категория A, а превышение этой границы переводит товары в категорию B.
Рис. 12.3. Товары, составляющие долю более 70 % от общих продаж,
определяются в категорию B
Представленные выше четыре колонки можно заменить одним вычисляемым столбцом, объединив логику при помощи нескольких переменных.
Вычисляемый столбец в таблице Product:
ABC Class Optimized =
VAR SalesByProduct = ADDCOLUMNS ( 'Product'; "@ProdSales"; [Sales Amount] )
VAR CurrentSales = [Sales Amount]
VAR BetterProducts = FILTER ( SalesByProduct; [@ProdSales] >= CurrentSales )
VAR CumulatedSales = SUMX ( BetterProducts; [@ProdSales] )
VAR AllSales = CALCULATE ( [Sales Amount]; ALL ( 'Product' ) )
VAR CumulatedPct = DIVIDE ( CumulatedSales; AllSales )
VAR AbcClass =
SWITCH (
TRUE;
CumulatedPct <= 0.7; "A";
CumulatedPct <= 0.9; "B";
"C"
)
RETURN
AbcClass
Использование этой версии кода позволяет уменьшить размер модели данных за счет создания одного столбца вместо четырех. В то же время при работе с большими объемами данных вычисление этой колонки
может потребовать значительных затрат памяти.
Снимок ABC-анализа
Вам может понадобиться классифицировать товары в модели данных
на ежегодной основе, чтобы один и тот же товар мог принадлежать раз-
Снимок ABC-анализа  243
ным категориям в разные годы. В этом случае реализация может потребовать создания дополнительной таблицы снимка, содержащей корректное указание категории для каждого товара в сочетании с годом.
Конечная цель – построить отчет, показанный на рис. 12.4, в котором
распределение товаров по категориям продемонстрировано по годам.
Рис. 12.4. ABC-классификация по годам
В модели данных нам потребуется дополнительная таблица, в которой будет храниться соответствие категории, товара и года по всей продукции. Таблица ABC by Year не будет связана с другими таблицами в
модели и будет содержать ключ товара, год и ассоциированную с этим
сочетанием категорию, как показано на рис. 12.5.
Рис. 12.5. Вычисляемая таблица, хранящая историю ABC-анализа по годам
Формула, которая поможет создать подобную таблицу, приведена
ниже.
Вычисляемая таблица:
ABC by Year =
VAR ProductsByYear =
SUMMARIZE (
Sales;
'Product'[ProductKey];
'Date'[Calendar Year]
)
VAR SaleByYearProduct =
ADDCOLUMNS (
ProductsByYear;
"@ProdSales"; [Sales Amount];
"@YearlySales"; CALCULATE (
[Sales Amount];
244
 Глава 12. ABC-анализ
ALL ( 'Product' )
)
)
VAR CumulatedSalesByYearProduct =
ADDCOLUMNS (
SaleByYearProduct;
"@CumulatedSales";
VAR CurrentSales = [@ProdSales]
VAR CurrentYear = 'Date'[Calendar Year]
VAR CumulatedSalesWithinYear =
FILTER (
SaleByYearProduct;
AND (
'Date'[Calendar Year] = CurrentYear;
[@ProdSales] >= CurrentSales
)
)
RETURN
SUMX (
CumulatedSalesWithinYear;
[@ProdSales]
)
)
VAR CumulatedPctByYearProduct =
ADDCOLUMNS (
CumulatedSalesByYearProduct;
"@CumulatedPct"; DIVIDE (
[@CumulatedSales];
[@YearlySales]
)
)
VAR ClassByYearProduct =
ADDCOLUMNS (
CumulatedPctByYearProduct;
"@AbcClass"; SWITCH (
TRUE;
[@CumulatedPct] <= 0.7; "A";
[@CumulatedPct] <= 0.9; "B";
"C"
)
)
VAR Result =
SELECTCOLUMNS (
ClassByYearProduct;
"ProductKey"; 'Product'[ProductKey];
"Calendar Year"; 'Date'[Calendar Year];
"ABC Class"; [@AbcClass]
)
RETURN
Result
Снимок ABC-анализа  245
Окончательный результат запуска этого кода показан на рис. 12.5, а с
промежуточными колонками, которые создаются в процессе выполнения формулы, его можно посмотреть на рис. 12.6.
Рис. 12.6. Промежуточные вычисления, содержащиеся
в переменной ClassByYearProduct
После создания в модели данных таблица ABC by Year может использоваться как фильтр, осуществляющий привязку данных между столбцами ProductKey и Calendar Year и соответствующими им столбцами в
таблицах Product и Date. Например, в отчете, показанном в начале данного раздела, используются следующие две меры.
Мера в таблице Sales:
# Products :=
VAR RemapFilterAbc =
TREATAS (
'ABC by Year';
-- Привязываем столбцы из таблицы
'Product'[ProductKey];
-- ABC by Year, чтобы только конкретные
'Date'[Calendar Year];
-- комбинации товара и года включались
'ABC by Year'[ABC Class] -- в контекст фильтра
)
VAR Result =
CALCULATE (
DISTINCTCOUNT ( Sales[ProductKey] );
KEEPFILTERS ( RemapFilterAbc )
)
RETURN
Result
Мера в таблице Sales:
ABC Sales Amount :=
VAR RemapFilterAbc =
246
 Глава 12. ABC-анализ
TREATAS (
'ABC by Year';
-'Product'[ProductKey];
-'Date'[Calendar Year];
-'ABC by Year'[ABC Class] -)
VAR Result =
CALCULATE (
[Sales Amount];
KEEPFILTERS ( RemapFilterAbc
)
RETURN
Result
Привязываем столбцы из таблицы
ABC by Year, чтобы только конкретные
комбинации товара и года включались
в контекст фильтра
)
В результате использования функции TREATAS обе меры распространяют фильтры со снимка на таблицы Product и Date, что позволяет добиться нужного результата. Очень важно применять ProductKey и
Calendar Year в одном и том же фильтре, в противном случае в меру могут быть включены комбинации товаров и годов, не присутствующие в
выбранных категориях ABC-анализа.
Существует и альтернативное решение, которое может сработать более эффективно в моделях данных с большим количеством товаров, –
оно включает в себя расширенные таблицы (expanded table). Как видно
по рис. 12.7, этот способ требует создания в модели данных промежуточной таблицы Years, объединенной с таблицей Date связью с двунаправленной фильтрацией (bidirectional filter), так что в Excel Power
Pivot такой вариант не сработает.
Таблица Years легко создается при помощи функции DISTINCT:
Years = DISTINCT ( 'Date'[Calendar Year] )
Меры в этом случае будут проще (но сложнее для понимания), поскольку они будут опираться на расширенные таблицы.
Мера в таблице Sales:
# Products Opt :=
CALCULATE (
DISTINCTCOUNT ( Sales[ProductKey] );
'ABC by Year'
)
Динамический ABC-анализ  247
Рис. 12.7. Вычисляемая таблица Years позволяет распространить
фильтр с таблицы Date на ABC by Year
Мера в таблице Sales:
ABC Sales Amount Opt :=
CALCULATE (
[Sales Amount];
'ABC by Year'
)
Версия ABC-анализа со снимками обладает большей динамикой по
сравнению со статическим сценарием. Разумеется, вычисляемая таблица требует определенных ресурсов. Но она создается в момент обновления данных, а при выполнении запросов отрабатывает довольно
быстро. Таким образом, вариант со снимками можно рассматривать в
качестве компромисса между скоростью и гибкостью. Если приоритетом является именно гибкость, то лучше подойдет более медленный
шаблон динамического ABC-анализа.
Динамический ABC-анализ
Шаблон, реализующий проведение динамического ABC-анализа, является наиболее гибким из трех перечисленных вариантов, но вместе с тем
весьма медленным и дорогостоящим с точки зрения расходования па-
248
 Глава 12. ABC-анализ
мяти. Цель состоит в динамическом расчете количества товаров, суммы
продажи или иной меры, определяющей набор товаров, принадлежащих выбранной категории в контексте отчета. Например, на рис. 12.8
показан отчет, в котором ABC-категории товаров определяются только
в рамках подкатегории Cell phones, а если пользователь выберет другую
группу товаров, отчет пересчитается с учетом новых фильтров.
Рис. 12.8. ABC-анализ строится динамически на основании выбранных фильтров
Вся логика шаблона реализована в одной мере, извлекающей список товаров определенной категории и использующей его в качестве
фильтра для требуемого вычисления. Более того, в модели данных нужно будет создать дополнительную таблицу ABC Classes, содержащую три
категории товаров с соответствующими им границами. Эта таблица показана на рис. 12.9.
Рис. 12.9. В таблице ABC Classes содержатся границы для всех категорий
Код меры ABC Sales Amount представлен ниже.
Мера в таблице Sales:
ABC Sales Amount :=
VAR SalesByProduct =
ADDCOLUMNS (
ALLSELECTED ( 'Product' );
"@ProdSales"; [Sales Amount]
)
VAR AllSales =
CALCULATE (
[Sales Amount];
ALLSELECTED ( 'Product' )
)
VAR CumulatedPctByProduct =
ADDCOLUMNS (
SalesByProduct;
Поиск категории ABC  249
"@CumulatedPct";
VAR CurrentSalesAmt = [@ProdSales]
VAR CumulatedSales =
FILTER (
SalesByProduct;
[@ProdSales] >= CurrentSalesAmt
)
VAR CumulatedSalesAmount =
SUMX (
CumulatedSales;
[@ProdSales]
)
RETURN
DIVIDE (
CumulatedSalesAmount;
AllSales
)
)
VAR ProductsInClass =
FILTER (
CROSSJOIN (
CumulatedPctByProduct;
'ABC Classes'
);
AND (
[@CumulatedPct] > 'ABC Classes'[Lower Boundary];
[@CumulatedPct] <= 'ABC Classes'[Upper Boundary]
)
)
VAR Result =
CALCULATE (
-- Шаблон будет одинаковым для всех мер
[Sales Amount];
-- Достаточно просто изменить название меры
KEEPFILTERS ( ProductsInClass )
)
RETURN
Result
Сложность этой формулы главным образом зависит от количества
товаров – чем больше товаров в модели, тем более медленной и требовательной к ресурсам компьютера будет формула. Десять тысяч товаров – примерно та граница, когда код может стать слишком медленным
для обеспечения полной интерактивности отчета. Этот фактор снижает
общую ценность данного шаблона.
Поиск категории ABC
В данном шаблоне описывается способ динамического поиска категории
ABC для товара, причем результат будет получен при помощи меры,
250
 Глава 12. ABC-анализ
а не вычисляемого столбца. Остальные шаблоны нацелены на разделение товаров по различным категориям и вычисление какого-то значения вроде суммы продаж или количества товаров. Этот же шаблон может
пригодиться при необходимости отобразить категорию товара динамически для отчета, подобного тому, что показан на рис. 12.10. Здесь
мы видим классификацию товаров из группы Computers за 2008 год.
Рис. 12.10. Динамическое сегментирование товаров,
основываясь на текущем выборе
Мера, определяющая категорию товара, основана на динамическом
шаблоне проведения ABC-анализа. Но на этот раз нам нет необходимости определять категорию для всех товаров – достаточно найти категорию выбранного элемента. Таким образом, определив список товаров с
суммами продажи, мера использует полученную информацию для вычисления значения только для текущего товара.
Мера в таблице Sales:
ABC Class :=
IF (
HASONEVALUE ( 'Product'[ProductKey] );
VAR SalesByProduct =
ADDCOLUMNS (
ALLSELECTED ( 'Product' );
"@ProdSales"; [Sales Amount]
)
VAR AllSales =
CALCULATE (
[Sales Amount];
ALLSELECTED ( 'Product' )
)
VAR CurrentSalesAmt = [Sales Amount]
VAR CumulatedSales =
Поиск категории ABC  251
FILTER (
SalesByProduct;
[@ProdSales] >= CurrentSalesAmt
)
VAR CumulatedSalesAmount =
SUMX (
CumulatedSales;
[@ProdSales]
)
VAR CurrentCumulatedPct =
DIVIDE (
CumulatedSalesAmount;
AllSales
)
VAR Result =
SWITCH (
TRUE;
ISBLANK ( CurrentCumulatedPct ); BLANK ();
CurrentCumulatedPct <= 0.7; "A";
CurrentCumulatedPct <= 0.9; "B";
"C"
)
RETURN
Result
)
Глава
13
Новые и постоянные
покупатели
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Шаблон учета новых и постоянных покупателей может пригодиться
для довольно распространенной задачи определения, какое количество
клиентов за заданный период можно считать новыми, а какое – постоянными, ушедшими или вернувшимися. Существует несколько вариантов этого шаблона, отличающихся в плане эффективности и итоговых результатов в зависимости от исходных требований. Кроме того,
рассматриваемый шаблон является достаточно гибким и позволяет как
идентифицировать новых и постоянных покупателей за период, так и
вычислять их объемы покупок.
Перед определением самого шаблона необходимо четко формализовать термины, обозначающие новых покупателей, постоянных,
ушедших и вернувшихся. В зависимости от принятой терминологии
вы сможете подбирать для этих вычислений свои формулы, отличающиеся по написанию и, что более важно, по эффективности. И хотя вы
можете использовать наиболее гибкий вариант шаблона для решения
всех сценариев в данной области, все же мы рекомендуем вам потратить немного времени на эксперименты, чтобы найти наиболее подходящую реализацию, удовлетворяющую всем вашим требованиям. При
этом наиболее гибкая формула ожидаемо является и наиболее дорогой
с точки зрения сопутствующих вычислений, что обусловливает низкую скорость выполнения расчетов даже применительно к небольшим
наборам данных.
Введение  253
Введение
Для заданного временного интервала вы можете рассчитать следующие
показатели:
• покупатели: общее количество людей, сделавших покупки в указанное время;
• новые покупатели: количество клиентов, сделавших свою первую покупку в этом интервале;
• постоянные покупатели: число людей, приобретавших у нас
что-то в прошлые периоды и вернувшихся за покупками вновь;
• ушедшие покупатели: количество клиентов, совершивших последнюю на данный момент покупку более двух месяцев назад
относительно начала указанного периода;
• вернувшиеся покупатели: количество людей, которые в предыдущем периоде были классифицированы как ушедшие, а затем
совершили покупку в заданном интервале.
Внешний вид желаемого отчета показан на рис. 13.1.
Рис. 13.1. В отчете показаны основные показатели рассматриваемого шаблона
Как видите, в январе 2007 года все покупатели попали в категорию
новых. В феврале 116 клиентов были классифицированы как постоянные, а 1037 – как новые. А месяцем позже сразу 603 покупателя мы посчитали ушедшими.
И если меры, рассчитывающие общее количество покупателей и
количество новых клиентов, довольно просты для понимания, с вычис-
254
 Глава 13. Новые и постоянные покупатели
лением ушедших могут возникнуть трудности. Давайте в нашем отчете взглянем на значение 603 в последней колонке по марту 2007 года.
Согласно нашей терминологии, все эти покупатели должны были приобретать у нас товары в последний раз в январе. Иными словами, из
1375 клиентов, пришедших к нам в первом месяце года, 603 покупателя не приобретали товары в феврале, марте и последующих месяцах –
именно поэтому мы посчитали их ушедшими на конец марта.
В вашей конкретной специфике определение ушедшего клиента может серьезно отличаться от приведенного здесь. Например, вы можете
считать его ушедшим, если последнюю покупку он совершил два месяца
назад, даже если знаете, что в следующем месяце он вновь что-нибудь
приобретет. Представьте, что клиент сделал покупки в январе и апреле.
Можно ли его считать ушедшим на конец марта? Ответ на этот вопрос
может вылиться в разные формулы для выполнения одного и того же
вычисления. Мы будем считать этого клиента временно ушедшим на
конец марта, поскольку знаем, что позже он попадет в категорию вернувшихся. Отчет, в котором отдельно учитываются временно ушедшие
покупатели (т. е. те, которые не делали покупок два месяца, а затем чтото приобрели), показан на рис. 13.2.
Рис. 13.2. В отчете выведены временно ушедшие клиенты и вернувшиеся
Заметьте, что количество временно ушедших покупателей превышает число ушедших из предыдущего отчета. Причина в том, что многие
из учтенных здесь клиентов приобрели что-то в будущих месяцах. В то
же время они попадут в категорию вернувшихся в том месяце, когда
вновь сделают покупку.
Введение  255
Еще одним важным аспектом при выборе подходящего шаблона для
сценария является то, как вы хотите, чтобы пользователь работал с
фильтрами. Если он выберет категорию товаров, как это должно сказаться на вычислениях? Скажем, он остановит выбор на категории Cell
Phones (Мобильные телефоны). Стоит ли в этом случае причислять клиента к новым при первой покупке мобильного телефона? Если да, то
один и тот же покупатель будет несколько раз причислен к категории
новых в зависимости от выбранного фильтра. Если же каждого клиента считать новым только один раз, необходимо будет игнорировать
фильтры при расчете количества новых покупателей. Так же точно и
другие меры должны быть подвержены или не подвержены фильтрации.
Позвольте пояснить эту концепцию на следующем примере. На
рис. 13.3 показаны сырые необработанные данные уменьшенной версии базы Contoso всего с тремя покупателями.
Рис. 13.3. В отчете показаны три клиента с их историями покупок
Воспользовавшись представленным отчетом, можете ли вы сказать,
в каком месяце Дейл Лал (Dale Lal) будет считаться новым покупателем, если пользователь добавит фильтр по Games and Toys? Первый раз
он приобрел товар из этой категории в апреле, хотя ранее уже покупал
что-то из группы Cameras and camcorders. Теперь взгляните на клиента
с именем Тамми Мета (Tammy Metha). Будет ли она считаться ушедшей
спустя два месяца после первой покупки в категории Games and Toys?
Она в дальнейшем не приобрела ни одного товара из этой группы, хотя
делала покупки в других категориях. Ответить на подобные вопросы
очень важно для выбора правильного шаблона, который будет наилучшим образом отвечать вашим требованиям.
Подсчитывать количество покупателей очень важно и полезно, но
иногда бывает необходимо также получить сумму продаж в разрезе
новых, постоянных и вернувшихся клиентов. Кроме того, любопытно
было бы попробовать оценить сумму потерь из-за ушедших клиентов,
как показано на рис. 13.4. В данном отчете мы используем средние объемы продаж наших ушедших покупателей за 12 месяцев, чтобы приблизительно рассчитать потери.
256
 Глава 13. Новые и постоянные покупатели
Рис. 13.4. В отчете показаны суммы продаж по новым, постоянным,
вернувшимся и ушедшим покупателям
Также стоит обратить внимание на то, как в формулах подсчитываются различные статусы покупателей внутри каждого временного интервала. К примеру, если взять период один год, то есть вероятность,
что один и тот же покупатель успеет за это время сменить статус с нового на временно ушедшего, далее на вернувшегося и на окончательно
ушедшего, и все за один год. В конкретный день статус покупателя четко
определен, но на более длинной дистанции он может меняться. Наши
формулы разработаны таким образом, чтобы учитывать разные статусы каждого клиента. На рис. 13.5 показан простой отчет, фильтрующий
и выводящий информацию только по одному клиенту: Дейлу Лалу.
Рис. 13.5. Один и тот же покупатель был новым и ушедшим в отчетном году
Здесь один клиент находился в двух статусах на протяжении года.
Дейл Лал числился в качестве постоянного покупателя на протяжении
нескольких месяцев, но на уровне года такой отметки у него нет, по-
Описание шаблона  257
скольку в начале года он был новым. На рис. 13.6 этот же отчет выведен
без учета января, так что наш покупатель трижды был обозначен как
постоянный клиент и ни разу – как новый.
Рис. 13.6. После исключения января, в котором клиент был новым,
на уровне года стал показываться статус постоянного покупателя
Если бы мы собирались описать все возможные комбинации мер в
данном шаблоне, нам бы и целой книги не хватило. Вместо этого мы покажем несколько наиболее распространенных шаблонов, оставив вам
возможности выбора конкретных формул, если ваш сценарий отличается от рассматриваемого.
Наконец, шаблоны расчета новых и постоянных покупателей требуют проведения довольно серьезных вычислений. Поэтому мы решили
показать две разновидности этих формул: динамическую и при помощи снимка.
Описание шаблона
Данный шаблон базируется на двух типах формул:
• внутренних формулах, цель которых состоит в вычислении относительных дат для конкретного покупателя;
• внешних формулах – это формулы, применяемые в отчетах. Они
в свою очередь используют внутренние формулы для вычисления
количества клиентов, суммы продаж или иных мер.
Например, чтобы рассчитать количество новых покупателей, для
каждого покупателя при помощи внутренней формулы будет вычислена дата первой покупки. После этого внешняя формула рассчитывает
количество покупателей, чья первая покупка входит в отфильтрованный временной диапазон.
258
 Глава 13. Новые и постоянные покупатели
Рассмотрим пример, который позволит лучше понять эту технику.
Взгляните на рис. 13.7, на котором изображен ограниченный набор
данных, – мы используем его для описания поведения различных
формул.
Рис. 13.7. В отчете показано несколько клиентов с их историей покупок
Используя эти данные в качестве примера, подумайте, как можно
подсчитать количество новых клиентов в марте. Внешняя мера должна
проверять, сколько покупателей сделали свое первое приобретение в
марте. Для этого она должна обращаться к внутренней формуле по каждому клиенту, проверяя дату первой покупки для всех них. Внутренняя
формула возвращает дату 14 марта для первого покупателя, коим является Джеральд Сури (Gerald Suri), тогда как другие клиенты совершили
свои первые покупки раньше марта. Таким образом, внешняя формула возвращает единицу в ответ на вопрос, сколько в марте было новых
покупателей.
Остальные меры ведут себя похожим образом, но со своими важными
нюансами, стоящими того, чтобы сказать о них отдельно.
Сначала давайте взглянем на внутреннюю формулу, вычисляющую
дату, на которую покупатель должен считаться новым. Заметьте, что
для каждого примера будет своя формула, и мы подробно рассмотрим
каждую из них в соответствующих разделах главы. Первый пример на
языке DAX мы приводим лишь в качестве введения.
Мера (скрытая) в таблице Sales:
Date New Customer :=
CALCULATE (
MIN ( Sales[Order Date] );
ALLEXCEPT (
Sales;
Sales[CustomerKey];
Customer
)
)
Описание шаблона  259
Эта внутренняя формула в дальнейшем используется во внешней формуле, вычисляющей количество новых покупателей в заданный период.
Мера в таблице Sales:
# New Customers :=
VAR CustomersWithNewDate =
CALCULATETABLE (
-- Готовим таблицу, в которой для
ADDCOLUMNS (
-- каждого клиента будет присутстVALUES ( Sales[CustomerKey] ); -- вовать дата его первой покупки
"@NewCustomerDate"; [Date New Customer]
);
ALLSELECTED ( Customer );
-- Без учета фильтров по покупателям
ALLSELECTED ( 'Date' )
-- и по датам
)
VAR CustomersWithLineage =
-- Меняем привязку данных
TREATAS (
-- в переменной CustomersWithNewDate,
CustomersWithNewDate;
-- чтобы она могла фильтровать
Customer[CustomerKey];
-- таблицы Customer и Date
'Date'[Date]
)
VAR Result =
CALCULATE (
DISTINCTCOUNT ( Sales[CustomerKey] ); -- Считаем покупателей, входящих
KEEPFILTERS ( CustomersWithLineage ) -- в переменную @NewCustomerDate
)
RETURN
Result
С использованием этого подхода шаблон становится весьма гибким.
И правда, если вам необходимо будет изменить логику, определяющую,
является ли покупатель новым, ушедшим или временно ушедшим,
достаточно будет обновить внутреннюю формулу, оставив внешнюю
без изменений. Но здесь стоит предупредить наших читателей: формулы, представленные в этой главе, очень чувствительно относятся к
контексту фильтра, и вам может понадобиться изменить их, чтобы они
удовлетворяли вашим требованиям. Делайте это только в случае, если
досконально понимаете принципы их работы. В этом шаблоне каждая
строка кода на языке DAX является плодом многочасовых раздумий и
тестов с целью обеспечить максимальную работоспособность итоговых
мер. В общем, ведите себя очень аккуратно с этим шаблоном. Как мы.
Итак, мы разбили наши шаблоны на две группы: динамические и
с использованием снимков. В первом случае меры рассчитываются
динамически с учетом всех действующих в отчете фильтров. Версия со
снимками предварительно рассчитывает значения внутренних мер при
помощи вычисляемых таблиц, чтобы затем внешние меры считались
260
 Глава 13. Новые и постоянные покупатели
быстрее. Таким образом, вариант с использованием снимков является
менее гибким, зато более быстрым.
Мы также представим вам три разные реализации в зависимости от
того, как меры должны взаимодействовать с активными фильтрами в
отчете:
• относительный шаблон: покупатель считается новым из расчета
даты, когда он впервые приобрел один из товаров, выбранных в
отчете;
• абсолютный шаблон: клиент считается новым в тот момент,
когда он впервые купил любой товар вне зависимости от
присутствующих в отчете фильтров;
• шаблон по категории: покупатель считается новым в момент
первого приобретения любого товара из категорий, выбранных
в отчете. При этом, если было куплено два товара одной категории, новым он будет считаться лишь раз, а если приобретенные
товары относились к разным категориям, новым клиент будет
считаться дважды.
Вы сможете найти более подробное описание вычислений в соответствующих разделах каждого шаблона. Мы советуем прочитать эту главу от
начала до конца, прежде чем пытаться реализовать данный шаблон в
своей модели данных. Необходимо хорошо понимать все ваши требования к отчету заранее – еще до реализации шаблона. Иначе можно в
последний момент понять, что вы выбрали не ту разновидность.
Также стоит отметить, что демонстрационные файлы к этой главе
включают в себя две версии: полную, которая работает со всей базой
данных в целом, и базовую – с тремя покупателями. Работая с базовой
версией, можно быстро понять особенности шаблона, поскольку строк
с данными в этом варианте не так много. Полная версия может пригодиться при оценке эффективности разных вычислений.
Внутренние меры
У нас есть три внутренние меры:
• Date New Customer: возвращает дату, когда покупатель считался
новым;
• Date Lost Customer: возвращает дату, в которую клиент считался
временно ушедшим, с проверкой того, что у него нет покупок в
следующих периодах;
• Date Temporary Lost Customer: возвращает дату, когда покупатель
мог бы считаться ушедшим, без проверки того, вернется ли он в
следующем периоде.
Описание шаблона  261
Эти меры не будут напрямую использоваться в отчетах – они должны использоваться только в реализациях внешних мер. Код внутренних
мер будет отличаться для каждого шаблона.
Внешние меры
В каждом шаблоне будет определено несколько мер для подсчета количества клиентов и оценки сумм продаж для разных статусов покупателей:
• # New Customers: подсчитывает количество новых покупателей;
• # Returning Customers: подсчитывает количество покупателей,
считавшихся новыми в предыдущем периоде и сделавших новую
покупку в отчетном интервале;
• # Lost Customers: определяет, сколько клиентов считается ушедшими;
• # Temporarily Lost Customers: подсчитывает количество покупателей, считающихся ушедшими в текущем отчетном периоде, с
возможностью возвращения в будущих временных интервалах;
• # Recovered Customers: сюда относятся клиенты, считавшиеся временно ушедшими, но сделавшие новую покупку в отчетном периоде;
• Sales New Customers: возвращает значение меры Sales Amount с
фильтром по новым покупателям;
• Sales Returning Customers: возвращает значение меры Sales Amount
с фильтром по клиентам, считавшимся новыми в предыдущий
временной интервал, но сделавшим новую покупку в отчетном
периоде;
• Sales Lost Customers (12M): возвращает Sales Amount за последние
12 месяцев до начала выбранного периода с фильтрацией покупателей, считающихся временно ушедшими в отчетный период;
• Sales Recovered Customers: возвращает значение меры Sales Amount
с фильтром по покупателям, ранее считавшимся временно ушедшими, но сделавшим новую покупку в отчетном периоде.
Код для внешних мер будет очень похожим во всех шаблонах. Все отличающиеся нюансы мы отдельно рассмотрим при описании соответствующих шаблонов.
Как использовать меры из шаблона
Формулы, представленные в шаблоне, могут быть разбиты на две категории. Меры, начинающиеся с символа # (решетка), призваны под-
262
 Глава 13. Новые и постоянные покупатели
считывать количество уникальных клиентов с учетом конкретного
фильтра. Обычно эти меры используются как есть и являются оптимизированными именно для такого применения. К примеру, следующая
мера возвращает количество новых покупателей.
Мера в таблице Sales:
# New Customers :=
VAR CustomersWithNewDate =
CALCULATETABLE (
-- Готовим таблицу, в которой для
ADDCOLUMNS (
-- каждого клиента будет присутстVALUES ( Sales[CustomerKey] ); -- вовать дата его первой покупки
"@NewCustomerDate"; [Date New Customer]
);
ALLSELECTED ( Customer );
-- Без учета фильтров по
ALLSELECTED ( 'Date' )
-- покупателям и по датам
)
VAR CustomersWithLineage =
-- Меняем привязку данных в
TREATAS (
-- переменной CustomersWithNewDate,
CustomersWithNewDate;
-- чтобы она могла фильтровать
Customer[CustomerKey];
-- таблицы Customer и
'Date'[Date]
-- Date
)
VAR Result =
CALCULATE (
DISTINCTCOUNT ( Sales[CustomerKey] ); -- Считаем покупателей, входящих
KEEPFILTERS ( CustomersWithLineage ) -- в переменную @NewCustomerDate
)
RETURN
Result
В мерах, не начинающихся с символа решетки, создается фильтр по
покупателям, который позже применяется к другой мере. Например, в
мерах с префиксом Sales фильтр по покупателям применяется к мере
Sales Amount. Следующая мера может быть повторно использована для
расчета других мер путем замены меры Sales Amount в последней функции CALCULATE.
Мера в таблице Sales:
Sales New Customers :=
VAR CustomersWithFirstSale =
CALCULATETABLE (
-- Готовим таблицу, в которой для
ADDCOLUMNS (
-- каждого клиента будет присутстVALUES ( Sales[CustomerKey] ); -- вовать дата его первой покупки
"@NewCustomerDate"; [Date New Customer]
),
Динамический относительный шаблон  263
ALLSELECTED ( Customer );
ALLSELECTED ( 'Date' )
)
VAR NewCustomers =
FILTER (
CustomersWithFirstSale;
[@NewCustomerDate]
IN VALUES ( 'Date'[Date] )
)
VAR Result =
CALCULATE (
[Sales Amount];
KEEPFILTERS ( NewCustomers )
)
RETURN
Result
-- Без учета фильтров по покупателям
-- и по датам
-- Фильтруем покупателей, у которых
-- NewCustomerDate находится в
-- текущем периоде
-- Рассчитываем меру Sales Amount
-- путем применения фильтра
-- по NewCustomers
В каждом шаблоне мы показываем по две меры (с префиксами #
и Sales), если присутствуют различия в их структуре, хотя бы в плане оптимизации. Если две меры отличаются только выражением в
последней функции CALCULATE, мы включаем только версию меры с
префиксом #.
Динамический относительный шаблон
Динамический относительный шаблон принимает во внимание все
фильтры, установленные в отчете, при выполнении вычислений. Таким
образом, если в отчете выбрана только одна категория товаров (например, Audio), покупатель будет считаться новым только в момент первого
приобретения товара из этой категории. То же самое касается и процедуры определения того, что клиент считается ушедшим, – здесь также
будут считаться дни с момента совершения последней покупки в категории Audio. На рис. 13.8 показан отчет, который поможет вам лучше
понять поведение описываемого шаблона.
В данном отчете область видимости ограничена единственным покупателем. Он числится как новый в январе, когда впервые приобрел
товар из группы Cameras and camcorders, и в апреле, когда в первый
раз купил что-то из категории Games and Toys. Все остальные меры
ведут себя похожим образом – с учетом фильтра, в котором выполняются.
Внутренние меры
Внутренние меры здесь будут следующие.
264
 Глава 13. Новые и постоянные покупатели
Рис. 13.8. Единственный видимый клиент считается новым
несколько раз – для разных категорий товаров
Мера (скрытая) в таблице Sales:
Date New Customer :=
CALCULATE (
MIN ( Sales[Order Date] ); -- Дата первой покупки – это MIN по Order Date
REMOVEFILTERS ( 'Date' )
-- в любое время в прошлом
)
Мера (скрытая) в таблице Sales:
Date Lost Customer :=
CALCULATE (
-- Последняя покупка совершена
EOMONTH ( MAX ( Sales[Order Date] ); 2 ); -- на 2 месяца позже последней
REMOVEFILTERS ( 'Date' )
-- транзакции (конец месяца)
)
-- за весь период
Мера (скрытая) в таблице Sales:
Date Temporary Lost Customer :=
VAR MaxDate =
-- Дата последней продажи – это MAX по Order Date
MAX ( Sales[Order Date] ) -- в текущем периоде (установлен вызывающей мерой)
VAR Result =
IF (
NOT ISBLANK ( MaxDate );
EOMONTH ( MaxDate; 2 ) -- на два месяца позже (конец месяца)
)
Динамический относительный шаблон  265
RETURN
Result
Новые покупатели
Формула меры для определения количества новых покупателей будет
следующей.
Мера в таблице Sales:
# New Customers :=
VAR CustomersWithNewDate =
CALCULATETABLE (
-- Готовим таблицу, в которой для
ADDCOLUMNS (
-- каждого клиента будет присутстVALUES ( Sales[CustomerKey] ); -- вовать дата его первой покупки
"@NewCustomerDate"; [Date New Customer]
);
ALLSELECTED ( Customer );
-- Без учета фильтров по покупателям
ALLSELECTED ( 'Date' )
-- и по датам
)
VAR CustomersWithLineage =
-- Меняем привязку данных в
TREATAS (
-- переменной CustomersWithNewDate,
CustomersWithNewDate;
-- чтобы она могла фильтровать
Sales[CustomerKey];
-- таблицы Customer и
'Date'[Date]
-- Date
)
VAR Result =
CALCULATE (
DISTINCTCOUNT ( Sales[CustomerKey] ); -- Считаем покупателей, входящих
KEEPFILTERS ( CustomersWithLineage ) -- в переменную @NewCustomerDate
)
RETURN
Result
В коде вычисляются даты, когда каждый покупатель считался новым.
Функция ALLSELECTED хорошо подходит для целей оптимизации:
она позволяет движку повторно использовать значение переменной
CustomersWithNewDate при многократном вычислении одного и того же
выражения.
Затем в переменной CustomersWithLineage формула обновляет привязку данных в табличной переменной CustomersWithNewDate, чтобы
она могла фильтровать поля Sales[CustomerKey] и Date[Date]. Будучи использованной в качестве фильтра, переменная CustomersWithLineage позволяет сделать покупателей видимыми только с датами, когда они считались новыми. В последней функции CALCULATE применяется фильтр
CustomersWithLineage с использованием модификатора KEEPFILTERS
266
 Глава 13. Новые и постоянные покупатели
для осуществления пересечения с текущим контекстом фильтра. Таким
образом, в новом контексте фильтра будут проигнорированы клиенты,
не считавшиеся новыми в отчетный период времени.
Для применения новых покупателей в качестве фильтра к мерам вроде Sales Amount придется немного изменить подход, как показано в реализации следующей меры Sales New Customers.
Мера в таблице Sales:
Sales New Customers :=
VAR CustomersWithFirstSale =
CALCULATETABLE (
-- Готовим таблицу, в которой для
ADDCOLUMNS (
-- каждого клиента будет присутстVALUES ( Sales[CustomerKey] ); -- вовать дата его первой покупки
"@NewCustomerDate"; [Date New Customer]
),
ALLSELECTED ( Customer );
-- Без учета фильтров по покупателям
ALLSELECTED ( 'Date' )
-- и по датам
)
VAR NewCustomers =
FILTER (
CustomersWithFirstSale;
-- Фильтруем покупателей, у которых
[@NewCustomerDate]
-- NewCustomerDate находится в
IN VALUES ( 'Date'[Date] )
-- текущем периоде
)
VAR Result =
CALCULATE (
[Sales Amount];
-- Рассчитываем меру Sales Amount путем
KEEPFILTERS ( NewCustomers ) -- применения фильтра по NewCustomers
)
RETURN
Result
В переменной NewCustomers содержится список значений
Sales[CustomerKey], соответствующих новым покупателям, полученный
путем проверки нахождения поля @NewCustomerDate в рамках текущего
контекста фильтра. В дальнейшем переменная NewCustomers, полученная
таким способом, используется в качестве фильтра при расчете меры Sales
Amount. Хотя в переменной содержится два столбца (Sales[CustomerKey] и
@NewCustomerDate), только один из них – Sales[CustomerKey] – активно
фильтрует модель данных, поскольку добавленная колонка не привязана
ни к одному другому столбцу в модели данных.
Ушедшие покупатели
В мере, подсчитывающей количество ушедших покупателей, необходимо учесть клиентов, не входящих в текущий контекст фильтра. На-
Динамический относительный шаблон  267
пример, в марте мы могли потерять клиента, совершавшего покупки
еще в январе. Таким образом, при фильтрации отчета по марту этот покупатель виден не будет. В формуле необходимо обратиться к январю,
чтобы найти этого покупателя. Именно поэтому структура кода в данном случае будет отличаться от меры New Customers.
Мера в таблице Sales:
# Lost Customers :=
VAR LastDateLost =
CALCULATE (
MAX ( 'Date'[Date] );
ALLSELECTED ( 'Date' )
)
VAR CustomersWithLostDate =
CALCULATETABLE (
ADDCOLUMNS (
VALUES ( Sales[CustomerKey]
"@LostCustomerDate"; [Date
);
ALLSELECTED ( Customer );
'Date'[Date] <= LastDateLost
)
VAR LostCustomers =
FILTER (
CustomersWithLostDate;
[@LostCustomerDate]
IN VALUES ( 'Date'[Date] )
)
VAR Result =
COUNTROWS ( LostCustomers )
-- Готовим таблицу, в которой для
-- каждого клиента будет присутство); -- вать дата,когда он считался ушедшим
Lost Customer]
-- Без учета фильтров по покупателям
-- и по датам
-- Фильтруем покупателей, у которых
-- @LostCustomerDate находится в
-- текущем периоде
-- При подсчете ушедших клиентов мы не
-- используем таблицу Sales (продаж за
-- период нет)
RETURN
Result
В переменной CustomersWithLostDate мы рассчитываем даты ухода
всех клиентов. Переменная LostCustomers исключает из списка покупателей, чьи даты ухода не входят в текущий период. В конечном счете мера
вычисляет количество оставшихся в переменной LostCustomers клиентов
путем подсчета числа строк, что соответствует списку покупателей, чьи
даты ухода попадают в период, видимый в текущем контексте фильтра.
Временно ушедшие покупатели
Мера, вычисляющая количество временно ушедших покупателей, в
значительной степени отличается от меры для просто ушедших покупателей. Здесь мы должны проверить, что в текущем контексте фильтра
268
 Глава 13. Новые и постоянные покупатели
потенциально ушедший покупатель не делал покупок до даты, когда
мог бы считаться ушедшим. Следующий код реализует эту логику.
Мера в таблице Sales:
# Temporarily Lost Customers :=
VAR MinDate = MIN ( 'Date'[Date] )
VAR CustomersWithLostDateComplete =
CALCULATETABLE (
-- Готовим таблицу, в которой для
ADDCOLUMNS (
-- каждого клиента будет присутстVALUES ( Sales[CustomerKey] ); -- вовать дата временного ухода
"@TemporarilyLostCustomerDate"; CALCULATE (
[Date Temporary Lost Customer];
'Date'[Date] < MinDate
)
);
ALLSELECTED ( Customer );
-- Без учета фильтров по покупателям
ALLSELECTED ( 'Date' )
-- и по датам
)
VAR CustomersWithLostDate =
FILTER (
-- Исключаем покупателей без даты
CustomersWithLostDateComplete;
-- временного ухода
NOT ISBLANK ( [@TemporarilyLostCustomerDate] )
)
VAR PotentialTemporarilyLostCustomers =
FILTER (
CustomersWithLostDate;
-- Фильтруем покупателей, у которых
[@TemporarilyLostCustomerDate]
-- @TemporarilyLostCustomerDate
IN VALUES ( 'Date'[Date] )
-- находится в текущем периоде
)
VAR ActiveCustomers =
ADDCOLUMNS (
-- Получаем первую дату покупки по
VALUES ( Sales[CustomerKey] );
-- клиентам в текущей выборке
"@MinOrderDate"; CALCULATE ( MIN ( Sales[Order Date] ) )
)
VAR TemporarilyLostCustomers =
FILTER (
-- Фильтруем временно ушедших
NATURALLEFTOUTERJOIN (
-- клиентов путем комбинирования
PotentialTemporarilyLostCustomers; -- потенциально ушедших покупаActiveCustomers
-- телей и активных покупателей
);
-- и сравнения дат
OR (
ISBLANK ( [@MinOrderDate] );
[@MinOrderDate] > [@TemporarilyLostCustomerDate]
)
)
VAR Result =
COUNTROWS ( TemporarilyLostCustomers )
RETURN
Result
Динамический относительный шаблон  269
Сначала в мере рассчитываются потенциальные даты ухода каждого покупателя. При этом также накладывается фильтр по дате, так что
учитываются только транзакции, произведенные до начала текущего
периода. Затем выполняется проверка того, у каких покупателей дата
ухода входит в диапазон текущего периода.
Итоговая таблица PotentialTemporarilyLostCustomers содержит всех покупателей, которые могут считаться временно ушедшими в текущем
периоде. Перед возвращением результата требуется провести завершающую проверку, заключающуюся в том, что эти покупатели не должны
были ничего приобрести в текущем периоде до даты, когда они были
признаны ушедшими. Эта проверка осуществляется в процессе создания таблицы TemporarilyLostCustomers – здесь производятся все необходимые для этого действия.
Вернувшиеся покупатели
Под вернувшимися покупателями мы подразумеваем тех, кто ранее
считался временно ушедшим, но сделал покупку в текущий период времени. Количество вернувшихся покупателей можно подсчитать по следующей формуле.
Мера в таблице Sales:
# Recovered Customers :=
VAR MinDate =
MIN ( 'Date'[Date] )
VAR CustomersWithLostDateComplete =
CALCULATETABLE (
-- Готовим таблицу, в которой для
ADDCOLUMNS (
-- каждого клиента будет присутстVALUES ( Sales[CustomerKey] ); -- вовать дата временного ухода
"@TemporarilyLostCustomerDate"; CALCULATE (
[Date Temporary Lost Customer];
'Date'[Date] < MinDate
)
);
ALLSELECTED ( Customer );
-- Без учета фильтров по покупателям
ALLSELECTED ( 'Date' )
-- и по датам
)
VAR CustomersWithLostDate =
FILTER (
-- Исключаем покупателей без даты
CustomersWithLostDateComplete;
-- временного ухода
NOT ISBLANK ( [@TemporarilyLostCustomerDate] )
)
VAR ActiveCustomers =
ADDCOLUMNS (
-- Получаем первую дату покупки по
VALUES ( Sales[CustomerKey] );
-- клиентам в текущей выборке
"@MinOrderDate"; CALCULATE ( MIN ( Sales[Order Date] ) )
)
Powered by TCPDF (www.tcpdf.org)
270
 Глава 13. Новые и постоянные покупатели
VAR RecoveredCustomers =
FILTER (
NATURALINNERJOIN (
-- Фильтруем вернувшихся клиентов
ActiveCustomers;
-- путем комбинирования активных
CustomersWithLostDate
-- клиентов и временно ушедших
);
-- и сравнения дат
[@MinOrderDate] > [@TemporarilyLostCustomerDate]
)
VAR Result =
COUNTROWS ( RecoveredCustomers )
RETURN
Result
В
переменной
CustomersWithLostDateComplete
рассчитываются даты временного ухода клиентов. При создании переменной
CustomersWithLostDate из созданного ранее списка исключаются клиенты без даты временного ухода. В переменной ActiveCustomers извлекаются первые даты покупок для клиентов в текущей выборке.
В результате в переменной RecoveredCustomers движок оставляет покупателей, которые присутствуют в обоих списках (ActiveCustomers и
CustomersWithLostDate) и покупали что-то позднее даты, когда были
признаны временно ушедшими. Наконец, в переменной Result подсчитывается количество таких клиентов.
Постоянные покупатели
Последней мерой в этом наборе будет мера # Returning Customers.
Мера в таблице Sales:
# Returning Customers :=
VAR MinDate = MIN ( 'Date'[Date] )
VAR CustomersWithNewDate =
CALCULATETABLE (
-- Готовим таблицу, в которой для
ADDCOLUMNS (
-- каждого клиента будет присутстVALUES ( Sales[CustomerKey] ); -- вовать дата его первой покупки
"@NewCustomerDate"; [Date New Customer]
);
ALLSELECTED ( Customer );
-- Без учета фильтров по покупателям
ALLSELECTED ( 'Date' )
-- и по датам
)
VAR ExistingCustomers =
-- Для получения действующих клиентов
FILTER (
-- фильтруем всех покупателей
CustomersWithNewDate;
-- и проверяем, что их первая покупка
[@NewCustomerDate] < MinDate
-- произошла до начала текущего периода
)
Динамический абсолютный шаблон  271
VAR ReturningCustomers =
-- Получаем список постоянных клиентов
INTERSECT (
-- как пересечение между
VALUES ( Sales[CustomerKey] ); -- активными покупателями в выборке
SELECTCOLUMNS (
-- и существующими покупателями
ExistingCustomers;
"CustomerKey"; Sales[CustomerKey]
)
)
VAR Result =
COUNTROWS ( ReturningCustomers )
RETURN
Result
Сначала в мере подготавливается таблица CustomersWithNewDate
с первой датой покупки для каждого клиента. В переменной
ExistingCustomers исключаем покупателей, у которых дата покупки находится не раньше начала текущего периода. В результате в переменной
ExistingCustomers остаются клиенты, совершившие покупки до начала
отчетного интервала времени. Таким образом, если они также приобретали наши товары и в текущем периоде, их можно смело называть
постоянными покупателями. Последнее условие выполняется путем
комбинирования списка из переменной ExistingCustomers с перечнем
активных покупателей в отчетном периоде. Результат, сохраненный в
переменной ReturningCustomers, может быть использован для подсчета
количества постоянных покупателей – как в данной мере – или в качестве фильтра в других вычислениях.
Динамический абсолютный шаблон
Динамический абсолютный шаблон не принимает во внимание фильтры, установленные в отчете, при вычислении дат для покупателей. Его
реализация является разновидностью базового динамического относительного шаблона, а изменения ограничиваются другим набором модификаторов в функции CALCULATE для явного игнорирования фильтров.
В результате покупателям присваиваются статусы вне зависимости
от установленных в отчете фильтров, как показано на рис. 13.9: покупатель Дейл Лал считается новым покупателем в январе в категории
товаров Games and Toys, хотя по факту он приобретал товары из группы
Cameras and camcorders.
Единственная мера, которая меняется в зависимости от выбранной
категории, – это # Customers – в ней показано, когда именно клиент совершал покупки. Все остальные меры игнорируют фильтры по товарам:
например, клиент считается новым только в момент своей первой покупки вне зависимости от фильтра отчета.
272
 Глава 13. Новые и постоянные покупатели
Рис. 13.9. Дейл Лал обладает статусами «новый», «вернувшийся»
и «ушедший» без учета выбранных в отчете фильтров
Внутренние меры
Внутренние меры у нас будут следующие.
Мера (скрытая) в таблице Sales:
Date New Customer :=
CALCULATE (
MIN ( Sales[Order Date] );
ALLEXCEPT (
Sales;
Sales[CustomerKey];
Customer
)
)
-- Дата первой покупки – это MIN
-- по Order Date
-- Игнорируем все фильтры,
-- кроме покупателей
Мера (скрытая) в таблице Sales:
Date Lost Customer :=
VAR MaxDate =
CALCULATE (
MAX ( Sales[Order Date] );
ALLEXCEPT (
Sales;
Sales[CustomerKey];
Customer
-- Дата последней продажи – это MAX
-- по Order Date в текущем периоде
-- (установлен вызывающей мерой)
-- Игнорируем все фильтры,
-- кроме покупателей
Динамический абсолютный шаблон  273
)
)
VAR Result =
IF (
NOT ISBLANK ( MaxDate );
EOMONTH ( MaxDate; 2 )
)
RETURN
Result
-- на два месяца позже (конец месяца)
Мера (скрытая) в таблице Sales:
Date Temporary Lost Customer :=
VAR MaxDate =
CALCULATE (
MAX ( Sales[Order Date] );
ALLEXCEPT (
Sales;
'Date';
Sales[CustomerKey];
Customer
)
)
VAR Result =
IF (
NOT ISBLANK ( MaxDate );
EOMONTH ( MaxDate; 2 )
)
RETURN
Result
-- Дата последней продажи – это MAX
-- по Order Date в текущем периоде
-- (установлен вызывающей мерой)
-- Игнорируем все фильтры,
-- кроме даты
-- и покупателей
-- на два месяца позже (конец месяца)
Как видно из предыдущих фрагментов кода, внутренние меры действительно игнорируют все фильтры, кроме покупателя. Исключение составляет мера Date Temporary Lost Customer, в которой нам также необходимо учитывать фильтр по дате.
Заметьте, что внутренние меры будут корректно работать при вызове из внешних мер. В этом причина того, что функция ALLEXCEPT
в мерах явным образом сохраняет фильтр по полю Sales[CustomerKey],
что весьма необычно. При вызове в рамках итерации, включающей этот
столбец, во внутренней мере сохранится фильтр по нему, и тем самым
выполнятся требования внешней меры.
Новые покупатели
Формула меры, рассчитывающей количество новых клиентов, будет
следующей.
274
 Глава 13. Новые и постоянные покупатели
Мера в таблице Sales:
# New Customers :=
VAR CustomersWithNewDate =
CALCULATETABLE (
-- Готовим таблицу, в которой для
ADDCOLUMNS (
-- каждого клиента будет присутстVALUES ( Sales[CustomerKey] ); -- вовать дата его первой покупки
"@NewCustomerDate"; [Date New Customer]
);
ALLEXCEPT ( Sales; Customer )
)
VAR NewCustomers =
FILTER (
CustomersWithNewDate;
-- Фильтруем покупателей, у которых
[@NewCustomerDate]
-- @NewCustomerDate находится в
IN VALUES ( 'Date'[Date] )
-- текущем периоде
)
VAR Result =
-- Расчет количества новых клиентов
COUNTROWS ( NewCustomers )
-- не требует обращения к таблице Sales
RETURN
Result
Применительно к данной мере стоит сделать два важных замечания.
Во-первых, при вычислении переменной CustomersWithNewDate используется модификатор ALLEXCEPT для игнорирования всех фильтров, кроме фильтра по клиентам. Во-вторых, чтобы определить, что
клиент является новым, в мере выполняется фильтрация содержимого переменной CustomersWithNewDate. Затем мы просто подсчитываем
количество строк в переменной NewCustomers, вместо того чтобы применять функцию TREATAS, как это было в случае с динамическим относительным шаблоном. Эта техника может оказаться несколько более
медленной по сравнению с динамическим относительным шаблоном,
но она необходима, поскольку нам нужно подсчитать даже тех клиентов, которые не видны в рамках текущего контекста фильтра.
Ушедшие покупатели
Формула меры, рассчитывающей количество ушедших клиентов, будет следующей.
Мера в таблице Sales:
# Lost Customers :=
VAR LastDateLost =
CALCULATE (
MAX ( 'Date'[Date] );
Динамический абсолютный шаблон  275
ALLSELECTED ( 'Date' )
)
VAR CustomersWithLostDate =
CALCULATETABLE (
-- Готовим таблицу, в которой для
ADDCOLUMNS (
-- каждого клиента будет присутствовать
VALUES ( Sales[CustomerKey] ); -- дата,когда он считался ушедшим
"@LostCustomerDate"; [Date Lost Customer]
);
ALLEXCEPT ( Sales; Customer );
'Date'[Date] <= LastDateLost
)
VAR LostCustomers =
FILTER (
CustomersWithLostDate;
-- Фильтруем покупателей, у которых
[@LostCustomerDate]
-- @LostCustomerDate находится в
IN VALUES ( 'Date'[Date] ) -- текущем периоде
)
VAR Result =
COUNTROWS ( LostCustomers )
-- При подсчете ушедших клиентов мы не
-- используем таблицу Sales
-- (продаж за период нет)
RETURN
Result
Структурно эта мера очень похожа на меру New Customers, а основное
отличие заключается в расчете переменной CustomersWithLostDate.
Временно ушедшие покупатели
Мера, вычисляющая количество временно ушедших покупателей, является разновидностью меры для просто ушедших покупателей.
Мера в таблице Sales:
# Temporarily Lost Customers :=
VAR MinDate = MIN ( 'Date'[Date] )
VAR CustomersWithLostDateComplete =
CALCULATETABLE (
-- Готовим таблицу, в которой для
ADDCOLUMNS (
-- каждого клиента будет присутстVALUES ( Sales[CustomerKey] );
-- вовать дата временного ухода
"@TemporarilyLostCustomerDate"; CALCULATE (
[Date Temporary Lost Customer];
'Date'[Date] < MinDate
)
);
-- Игнорируем все фильтры,
ALLEXCEPT ( Sales; Customer )
-- за исключением фильтра по клиентам
)
VAR CustomersWithLostDate =
FILTER (
-- Исключаем покупателей без даты
276
 Глава 13. Новые и постоянные покупатели
CustomersWithLostDateComplete;
-- временного ухода
NOT ISBLANK ( [@TemporarilyLostCustomerDate] )
)
VAR PotentialTemporarilyLostCustomers =
FILTER (
CustomersWithLostDate;
-- Фильтруем покупателей, у которых
[@TemporarilyLostCustomerDate]
-- @TemporarilyLostCustomerDate
IN VALUES ( 'Date'[Date] )
-- находится в текущем периоде
)
VAR ActiveCustomers =
CALCULATETABLE (
ADDCOLUMNS (
-- Получаем первую дату покупки
VALUES ( Sales[CustomerKey] );
-- по клиентам в текущей выборке
"@MinOrderDate"; CALCULATE ( MIN ( Sales[Order Date] ) )
);
ALLEXCEPT ( Sales; Customer; 'Date' )
)
VAR TemporarilyLostCustomers =
FILTER (
-- Фильтруем временно ушедших
NATURALLEFTOUTERJOIN (
-- клиентов путем комбинирования
PotentialTemporarilyLostCustomers; -- потенциально ушедших покупаActiveCustomers
-- телей и активных покупателей
);
-- и сравнения дат
OR (
ISBLANK ( [@MinOrderDate] );
[@MinOrderDate] > [@TemporarilyLostCustomerDate]
)
)
VAR Result =
COUNTROWS ( TemporarilyLostCustomers )
RETURN
Result
Поведение этой меры очень похоже на поведение аналогичной меры
из динамического относительного шаблона. Главным отличием является использование функции ALLEXCEPT при вычислении переменных CustomersWithLostDateComplete и ActiveCustomers. В переменной
CustomersWithLostDateComplete все фильтры, за исключением фильтра
по клиентам, удалены, тогда как в переменной ActiveCustomers были
оставлены фильтры по датам и клиентам.
Вернувшиеся покупатели
Под вернувшимися покупателями мы подразумеваем тех, кто ранее
считался временно ушедшим, но сделал покупку в текущий период времени. Количество вернувшихся покупателей можно подсчитать по следующей формуле.
Динамический абсолютный шаблон  277
Мера в таблице Sales:
# Recovered Customers :=
VAR MinDate = MIN ( 'Date'[Date] )
VAR CustomersWithLostDateComplete =
CALCULATETABLE (
-- Готовим таблицу, в которой для
ADDCOLUMNS (
-- каждого клиента будет присутстVALUES ( Sales[CustomerKey] ); -- вовать дата временного ухода
"@TemporarilyLostCustomerDate"; CALCULATE (
[Date Temporary Lost Customer];
'Date'[Date] < MinDate
)
);
-- Игнорируем все фильтры,за исклюALLEXCEPT ( Sales; Customer )
-- чением фильтра по клиентам
)
VAR CustomersWithLostDate =
FILTER (
-- Исключаем покупателей без
CustomersWithLostDateComplete;
-- даты временного ухода
NOT ISBLANK ( [@TemporarilyLostCustomerDate] )
)
VAR ActiveCustomers =
CALCULATETABLE (
ADDCOLUMNS (
-- Получаем первую дату покупки
VALUES ( Sales[CustomerKey] );
-- по клиентам в текущей выборке
"@MinOrderDate"; CALCULATE ( MIN ( Sales[Order Date] ) )
);
ALLEXCEPT ( Sales; Customer, 'Date' )
)
VAR RecoveredCustomers =
FILTER (
NATURALINNERJOIN (
-- Фильтруем вернувшихся клиентов
ActiveCustomers;
-- путем комбинирования активных
CustomersWithLostDate
-- клиентов и временно ушедших
);
-- и сравнения дат
[@MinOrderDate] > [@TemporarilyLostCustomerDate]
)
VAR Result =
COUNTROWS ( RecoveredCustomers )
RETURN
Result
Поведение этой меры также очень похоже на поведение аналогичной
меры из динамического относительного шаблона. Главным отличием
является использование функции ALLEXCEPT при вычислении переменных CustomersWithLostDateComplete и ActiveCustomers.
Постоянные покупатели
Последней мерой в этом наборе будет мера # Returning Customers.
278
 Глава 13. Новые и постоянные покупатели
Мера в таблице Sales:
# Returning Customers :=
VAR MinDate = MIN ( 'Date'[Date] )
VAR CustomersWithNewDate =
CALCULATETABLE (
-- Готовим таблицу, в которой для
ADDCOLUMNS (
-- каждого клиента будет присутстVALUES ( Sales[CustomerKey] ); -- вовать дата его первой покупки
"@NewCustomerDate"; [Date New Customer]
);
-- Игнорируем все фильтры, за исклюALLEXCEPT ( Sales; Customer )
-- чением фильтра по клиентам
)
VAR ExistingCustomers =
-- Для получения действующих клиентов
FILTER (
-- фильтруем всех покупателей и проверяем,
CustomersWithNewDate;
-- что их первая покупка произошла
[@NewCustomerDate] < MinDate -- до начала текущего периода
)
VAR ActiveCustomers =
CALCULATETABLE (
VALUES ( Sales[CustomerKey] );
-- Получаем активных покупателей
ALLEXCEPT ( Sales; Customer; 'Date' )
)
VAR ReturningCustomers =
-- Получаем список постоянных клиентов
INTERSECT (
-- как пересечение между
ActiveCustomers;
-- активными покупателями в выборке
SELECTCOLUMNS (
-- и существующими покупателями
ExistingCustomers;
"CustomerKey"; Sales[CustomerKey]
)
)
VAR Result =
COUNTROWS ( ReturningCustomers )
RETURN
Result
Опять же, поведение этой меры очень похоже на поведение аналогичной меры из динамического относительного шаблона. Главным отличием является использование функции ALLEXCEPT при вычислении
переменных CustomersWithNewDate и ActiveCustomers для точной установки требуемых фильтров.
Обобщенный динамический шаблон
(по категории)
Обобщенный динамический шаблон можно рассматривать в качестве
промежуточного звена между абсолютным и динамическим шаблоном.
В данном шаблоне игнорируются все фильтры в отчете, за исключени-
Обобщенный динамический шаблон (по категории)
 279
ем атрибутов, определяемых особенностями бизнес-логики. В примерах, представленных в этой главе, все меры будут строиться по категориям товаров. Результат будет динамическим для категории товара
и абсолютным для всех остальных атрибутов в модели данных. Например, один и тот же клиент может считаться новым для одной категории
товаров и постоянным – для другой в рамках одного месяца. При этом
один покупатель может быть классифицирован как новый несколько
раз, если он совершал покупки товаров из разных категорий. Иначе
можно сказать, что анализ новых и постоянных клиентов в данном случае производится в разрезе категорий. Вы можете настроить шаблон
под себя, заменив категорию товара одним или несколькими другими
атрибутами, чтобы он полностью соответствовал требованиям вашей
бизнес-логики.
Мы намеренно избегали чрезмерной оптимизации при написании
кода для этого шаблона. Главной целью мы ставили себе простоту изменения представленных вычислений. Так что, если вы хотите адаптировать данный шаблон под собственные нужды, показанный набор мер
может рассматриваться в качестве отправной точки.
Правила рассматриваемого здесь шаблона следующие:
• один и тот же покупатель может классифицироваться как новый
множество раз – по разу на каждую комбинацию динамических
атрибутов (в нашем примере это категория товаров);
• покупатели считаются постоянными, если ранее они уже делали приобретение в той же комбинации динамических атрибутов
(у нас это категория товаров), в которой совершают покупку в выбранном периоде;
• покупатели считаются временно ушедшими, если они не совершали покупку в определенной комбинации динамических атрибутов (категории товаров) в течение двух месяцев, даже если в
других комбинациях атрибутов приобретения быть могли;
• клиенты классифицируются как вернувшиеся, если совершают
покупку в комбинации динамических атрибутов (категории товаров), для которой считались временно ушедшими.
Важно отметить, что в данном шаблоне определяются только покупатели, а не комбинация из динамических атрибутов и покупателей (в нашем случае категории товаров и покупателей). Таким образом, меры,
начинающиеся с префикса #, будут возвращать количество уникальных
покупателей, а меры с префиксом Sales – вычислять меру Sales Amount
вне зависимости от комбинации динамических атрибутов (категории
товаров), для которой определяется статус клиента: новый, ушедший
или вернувшийся. Разница видна при выполнении фильтрации двух или
280
 Глава 13. Новые и постоянные покупатели
более комбинаций динамических атрибутов. Например, при фильтрации двух категорий товаров меры Sales для новых и постоянных покупателей могут в сумме давать большее значение, чем Sales Amount.
Фактически та же сумма может быть получена с учетом одного и того
же покупателя в качестве нового и постоянного клиента, поскольку он
может обладать разными статусами для разных категорий.
Ваши требования могут отличаться от принятых в данном примере.
В этом случае, как мы уже говорили ранее, прежде чем вносить какие-то
изменения, необходимо убедиться в том, что вы досконально понимаете, как организована фильтрация во всех мерах, поскольку они весьма
сложные и нарушить их работу можно довольно легко даже при небольших корректировках.
Внутренние меры
Внутренние меры в нашем случае будут следующие.
Мера (скрытая) в таблице Sales:
Date New Customer :=
CALCULATE (
MIN ( Sales[Order Date] ); -- Дата первой покупки – это MIN по Order Date
ALLEXCEPT (
Sales;
-- Игнорируем все фильтры,
Sales[CustomerKey];
-- за исключением Customer
Customer;
'Product'[Category]
-- и Product Category
)
)
Мера (скрытая) в таблице Sales:
Date Lost Customer :=
VAR MaxDate =
CALCULATE (
MAX ( Sales[Order Date] );
ALLEXCEPT (
Sales;
Sales[CustomerKey];
Customer;
'Product'[Category]
)
)
VAR Result =
IF (
NOT ISBLANK ( MaxDate );
EOMONTH ( MaxDate; 2 )
------
Дата последней продажи – это MAX
по Order Date в текущем периоде
(установлен вызывающей мерой)
Игнорируем все фильтры,
за исключением Customer
-- и Product Category
-- на два месяца позже (конец месяца)
Обобщенный динамический шаблон (по категории)
 281
)
RETURN
Result
Мера (скрытая) в таблице Sales:
Date Temporary Lost Customer :=
VAR MaxDate =
CALCULATE (
MAX ( Sales[Order Date] );
ALLEXCEPT (
Sales;
'Date';
Sales[CustomerKey];
Customer;
'Product'[Category]
)
)
VAR Result =
IF (
NOT ISBLANK ( MaxDate );
EOMONTH ( MaxDate; 2 )
)
RETURN
Result
-------
Дата последней продажи – это MAX
по Order Date в текущем периоде
(установлен вызывающей мерой)
Игнорируем все фильтры,
за исключением Date,
Customer
-- и Product Category
-- на два месяца позже (конец месяца)
Как видите, внутренние меры спроектированы таким образом, чтобы
игнорировать все фильтры, кроме Customer и Product[Category].
Новые покупатели
Мера, вычисляющая количество новых покупателей, будет следующей.
Мера в таблице Sales:
# New Customers :=
VAR FilterCategories =
CALCULATETABLE (
VALUES ( 'Product'[Category] );
ALLSELECTED ( 'Product' )
)
VAR CustomersWithNewDate =
CALCULATETABLE (
-- Готовим таблицу, в которой для каждого
ADDCOLUMNS (
-- клиента и категории будет присутствовать
SUMMARIZE (
-- дата его первой покупки
Sales;
Sales[CustomerKey];
282
 Глава 13. Новые и постоянные покупатели
'Product'[Category]
);
"@NewCustomerDate"; [Date New Customer]
);
ALLSELECTED ( Customer );
FilterCategories;
ALLEXCEPT (
Sales;
Sales[CustomerKey];
Customer
)
------
Фильтруем Product Category из ALLSELECTED
Удаляем все фильтры, за исключением
Customer, полученного при помощи
ALLSELECTED, чтобы результат оставался
неизменным в разных ячейках отчета
)
VAR CustomersCategoryNewDate =
TREATAS (
CustomersWithNewDate -- Меняем привязку данных, чтобы
Sales[CustomerKey];
-- NewCustomerDate соответствовала Date[Date]
'Product'[Category]; -- и могла использоваться для связи и фильтрации
'Date'[Date]
-- с подобными столбцами в модели данных
)
VAR ActiveCustomersCategories =
CALCULATETABLE (
SUMMARIZE (
-- Извлекаем комбинации
Sales;
-- Customer, Category и Date,
Sales[CustomerKey];
-- активные в текущей выборке
'Product'[Category];
'Date'[Date]
);
ALLEXCEPT (
-- Удаляем все фильтры, за исключением,
Sales;
-- Customer полученного при помощи
'Date';
-- ALLSELECTED, чтобы результат оставался
Sales[CustomerKey];
-- неизменным в разных ячейках отчета
Customer
);
VALUES ( 'Product'[Category] ) -- Восстанавливаем связанный фильтр
)
-- по Category
VAR ActiveNewCustomers =
NATURALINNERJOIN (
-- Фильтруем покупателей
CustomersCategoryNewDate;
-- в рамках текущей выборки,
ActiveCustomersCategories
-- связывая Date и Category
)
VAR NewCustomers =
DISTINCT (
-- Получаем список уникальных
SELECTCOLUMNS (
-- новых покупателей
ActiveNewCustomers;
"CustomerKey"; Sales[CustomerKey]
)
)
VAR Result =
COUNTROWS ( NewCustomers )
RETURN
Result
Обобщенный динамический шаблон (по категории)
 283
В этой версии меры переменная CustomersWithNewDate может вычислять разные даты для каждой категории товара. Функция SUMMARIZE
использует столбец Product[Category] в качестве элемента группировки.
Таким образом, функция TREATAS определяет привязку для трех столбцов в переменной CustomersWithNewDate таким образом, чтобы можно
было использовать поле @NewCustomerDate для фильтрации или объединения со столбцом Date[Date].
Для
повышения
производительности
переменные
CustomersWithNewDate и CustomersCategoryNewDate не зависят от контекста фильтра в ячейках отчета, так что они вычисляются лишь раз для
одной визуализации. Чтобы получить актуальный список, необходимо
отфильтровать комбинации, невидимые в контексте фильтра, в котором
вычисляется мера # New Customers. Этого мы добьемся путем применения в переменной ActiveNewCustomers функции NATURALINNERJOIN,
которая объединяет комбинации клиентов, дат и категорий товаров,
видимых в контексте фильтра (ActiveCustomersCategories), с комбинациями в CustomersCategoryNewDate.
В переменной NewCustomers происходит удаление дубликатов покупателей, которые могут быть новыми для разных категорий товаров в
один и тот же период. Теперь переменную NewCustomers можно использовать в качестве фильтра в будущих вычислениях, а можно и подсчитать количество строк в ней, что мы и делаем в нашей мере.
Мера Sales New Customers похожа на # New Customers, единственное отличие содержится в формуле переменной Result, которая использует переменную NewCustomersCat в качестве фильтра в функции CALCULATE
вместо подсчета строк в NewCustomers. Так что здесь мы приведем лишь
заключительную измененную часть меры, а начало у нее такое же.
Мера в таблице Sales:
Sales New Customers :=
...
VAR NewCustomersCat =
SELECTCOLUMNS (
-- Новый клиент/категория, убираем дату
ActiveNewCustomers;
"CustomerKey"; Sales[CustomerKey];
"Category"; 'Product'[Category]
)
VAR Result =
CALCULATE (
[Sales Amount];
-- Подсчитаем новых клиентов,
KEEPFILTERS ( NewCustomersCat )
-- применив фильтр по новым клиентам
)
RETURN
Result
284
 Глава 13. Новые и постоянные покупатели
Ушедшие покупатели
Формула меры для подсчета количества ушедших покупателей будет
следующая.
Мера в таблице Sales:
# Lost Customers :=
VAR LastDateLost =
CALCULATE (
MAX ( 'Date'[Date] );
ALLSELECTED ( 'Date' )
)
VAR CustomersWithLostDate =
CALCULATETABLE (
-- Готовим таблицу, в которой для каждого
ADDCOLUMNS (
-- клиента и категории будет присутствовать
SUMMARIZE (
-- дата, когда он считался ушедшим
Sales;
Sales[CustomerKey];
'Product'[Category]
);
"@LostCustomerDate"; [Date Lost Customer]
);
'Date'[Date] <= LastDateLost;
ALLSELECTED ( Customer );
VALUES ( 'Product'[Category] );
ALLEXCEPT (
-- Удаляем все фильтры, за исключением
Sales;
-- Customer, полученного из ALLSELECTED,
Sales[CustomerKey];
-- чтобы результат не менялся в разных
Customer
-- ячейках отчета
)
)
VAR LostCustomersCategories =
FILTER (
CustomersWithLostDate;
-- Фильтруем покупателей, у которых
[@LostCustomerDate]
-- @LostCustomerDate находится
IN VALUES ( 'Date'[Date] )
-- в текущем периоде
)
VAR LostCustomers =
DISTINCT (
-- Получаем список уникальных
SELECTCOLUMNS (
-- ушедших покупателей
LostCustomersCategories;
"CustomerKey"; Sales[CustomerKey]
)
)
VAR Result =
COUNTROWS ( LostCustomers )
RETURN
Result
Обобщенный динамический шаблон (по категории)
 285
В этой версии меры переменная CustomersWithLostDate может вычислять разные даты для разных категорий товаров. Причина в том, что
функция SUMMARIZE использует столбец Product[Category] в качестве
условия группировки и у клиентов могут быть разные даты ухода – по
одной для каждой категории.
В переменной LostCustomersCategories выполняется фильтрация комбинаций клиентов и категорий товаров с датой ухода, входящей в выбранный временной интервал. Как и в мере New Customers, здесь происходит удаление дубликатов покупателей, так что итоговая переменная
может быть использована как в качестве фильтра, так и в качестве основы для подсчета количества клиентов.
Временно ушедшие покупатели
Мера, вычисляющая количество временно ушедших покупателей,
реализуется как разновидность вычисления количества ушедших клиентов.
Мера в таблице Sales:
# Temporarily Lost Customers :=
VAR LastDateLost =
CALCULATE (
MAX ( 'Date'[Date] );
ALLSELECTED ( 'Date' )
)
VAR MinDate = MIN ( 'Date'[Date] )
VAR FilterCategories =
CALCULATETABLE (
VALUES ( 'Product'[Category] );
ALLSELECTED ( 'Product' )
)
VAR CustomersWithLostDateComplete =
CALCULATETABLE (
-- Готовим таблицу, в которой для каждого
ADDCOLUMNS (
-- клиента и категории будет присутствовать
SUMMARIZE (
-- дата, когда он считался ушедшим
Sales;
Sales[CustomerKey];
'Product'[Category]
);
"@TemporarilyLostCustomerDate"; CALCULATE (
[Date Temporary Lost Customer];
'Date'[Date] < MinDate
)
);
ALLSELECTED ( Customer );
FilterCategories;
-- Фильтруем Product Category из ALLSELECTED
ALLEXCEPT (
-- Удаляем все фильтры, за исключением
286
 Глава 13. Новые и постоянные покупатели
Sales;
Sales[CustomerKey];
Customer
-- Customer, полученного из ALLSELECTED,
-- чтобы результат не менялся
-- в разных ячейках отчета
)
)
VAR CustomersWithLostDate =
FILTER (
-- Удаляем покупателей без
CustomersWithLostDateComplete;
-- даты временного ухода
NOT ISBLANK ( [@TemporarilyLostCustomerDate] )
)
VAR PotentialTemporarilyLostCustomers =
FILTER (
CustomersWithLostDate;
-- Фильтруем покупателей, у которых
[@TemporarilyLostCustomerDate]
-- @TemporarilyLostCustomerDate
IN VALUES ( 'Date'[Date] )
-- находится в текущем периоде
)
VAR ActiveCustomersCategories =
CALCULATETABLE (
ADDCOLUMNS (
SUMMARIZE (
-- Получаем первую дату покупки
Sales;
-- по всем комбинациям
Sales[CustomerKey];
-- клиента и категории товара
'Product'[Category]
-- в текущей выборке
);
"@MinOrderDate"; CALCULATE ( MIN ( Sales[Order Date] ) )
);
ALLEXCEPT (
-- Удаляем все фильтры, за исключением
Sales;
-- покупателя и даты
Sales[CustomerKey];
Customer;
'Date'
);
VALUES ( 'Product'[Category] ) -- Восстанавливаем связанный фильтр Category
)
VAR TemporarilyLostCustomersCategories =
FILTER (
-- Фильтруем временно ушедших клиентов
NATURALLEFTOUTERJOIN (
-- путем комбинирования потенциально
PotentialTemporarilyLostCustomers; -- ушедших покупателей и активных
ActiveCustomersCategories
-- покупателей и сравнения дат
);
OR (
ISBLANK ( [@MinOrderDate] );
[@MinOrderDate] > [@TemporarilyLostCustomerDate]
)
)
VAR TemporarilyLostCustomers =
DISTINCT (
-- Получаем список уникальных
SELECTCOLUMNS (
-- временно ушедших покупателей
TemporarilyLostCustomersCategories;
"CustomerKey"; Sales[CustomerKey]
)
)
Обобщенный динамический шаблон (по категории)
 287
VAR Result =
COUNTROWS ( TemporarilyLostCustomers )
RETURN
Result
В переменной CustomersWithLostDateComplete нам необходимо было
восстановить фильтр по полю Product[Category] при помощи функции
VALUES.
В переменной ActiveCustomersCategories создается таблица комбинаций значений полей Sales[CustomerKey] и Product[Category] вместе с первой датой продажи для каждой комбинации. Данная таблица впоследствии объединяется с переменной PotentialTemporarilyLostCustomers,
в которой находится содержимое переменной CustomersWithLostDate,
видимое в текущей выборке. Результат объединения фильтруется по
дате @TemporarilyLostCustomerDate и возвращается в виде переменной
TemporarilyLostCustomersCategories.
Наконец, для исключения ситуации с повторным анализом одного и того же покупателя мы извлекаем из табличной переменной
TemporarilyLostCustomersCategories поле с ключом клиента, а затем подсчитываем количество строк в полученной переменной
TemporarilyLostCustomers.
Вернувшиеся покупатели
Вернувшимися покупателями мы называем тех, кто ранее считался
временно ушедшим, но сделал покупку в текущий период времени. Количество вернувшихся покупателей можно подсчитать по следующей
формуле.
Мера в таблице Sales:
# Recovered Customers :=
VAR LastDateLost =
CALCULATE (
MAX ( 'Date'[Date] );
ALLSELECTED ( 'Date' )
)
VAR MinDate = MIN ( 'Date'[Date] )
VAR FilterCategories =
CALCULATETABLE (
VALUES ( 'Product'[Category] );
ALLSELECTED ( 'Product' )
)
VAR CustomersWithLostDateComplete =
CALCULATETABLE (
-- Готовим таблицу, в которой для каждого
ADDCOLUMNS (
-- клиента и категории будет присутствовать
288
 Глава 13. Новые и постоянные покупатели
SUMMARIZE (
-- дата, когда он считался ушедшим
Sales;
Sales[CustomerKey];
'Product'[Category]
);
"@TemporarilyLostCustomerDate"; CALCULATE (
[Date Temporary Lost Customer];
'Date'[Date] < MinDate
)
);
ALLSELECTED ( Customer );
FilterCategories;
ALLEXCEPT (
Sales;
Sales[CustomerKey];
Customer
)
------
Фильтруем Product Category из ALLSELECTED
Удаляем все фильтры, за исключением
Customer, полученного из ALLSELECTED,
чтобы результат не менялся
в разных ячейках отчета
)
VAR CustomersWithLostDate =
FILTER (
-- Удаляем покупателей без
CustomersWithLostDateComplete;
-- даты временного ухода
NOT ISBLANK ( [@TemporarilyLostCustomerDate] )
)
VAR ActiveCustomersCategories =
CALCULATETABLE (
ADDCOLUMNS (
SUMMARIZE (
-- Получаем первую дату покупки
Sales;
-- по всем комбинациям
Sales[CustomerKey];
-- клиента и категории товара
'Product'[Category]
-- в текущей выборке
);
"@MinOrderDate"; CALCULATE ( MIN ( Sales[Order Date] ) )
);
ALLEXCEPT (
-- Удаляем все фильтры, за исключением
Sales;
-- покупателя и даты
Sales[CustomerKey];
Customer;
'Date'
);
VALUES ( 'Product'[Category] )
-- Восстанавливаем связанный
)
-- фильтр Category
VAR RecoveredCustomersCategories =
FILTER (
-- Фильтруем вернувшихся клиентов
NATURALINNERJOIN (
-- путем комбинирования активных
ActiveCustomersCategories; -- покупателей и временно ушедших
CustomersWithLostDate
-- покупателей и сравнения дат
);
[@MinOrderDate] > [@TemporarilyLostCustomerDate]
)
VAR RecoveredCustomers =
DISTINCT (
-- Получаем список уникальных
SELECTCOLUMNS (
-- вернувшихся покупателей
Обобщенный динамический шаблон (по категории)
 289
RecoveredCustomersCategories;
"CustomerKey"; Sales[CustomerKey]
)
)
VAR Result =
COUNTROWS ( RecoveredCustomers )
RETURN
Result
Сначала в мере определяется список покупателей, которые числились временно ушедшими до текущей даты, с группировкой
по полю Product[Category]. Поскольку столбцы Sales[CustomerKey] и
Product[Category] являются частью таблиц, хранящихся в переменных
CustomersWithLostDateComplete и ActiveCustomers, объединение, выполненное в переменной RecoveredCustomersCategories, возвращает таблицу
с обоими этими столбцами. Это гарантирует, что покупатель, который
считался временно ушедшим в рамках какой-либо категории товаров,
будет считаться вернувшимся, только если приобретет товар из этой
же категории. При этом один клиент может появляться в этой таблице
больше одного раза, в связи с чем мы избавляемся от дубликатов в переменной RecoveredCustomersCategories, чтобы можно было подсчитать
количество или отфильтровать по уникальным вернувшимся покупателям.
Мера Sales Recovered Customers похожа на # Recovered Customers,
единственное отличие содержится в формуле переменной Result,
которая использует переменную RecoveredCustomersCat в качестве
фильтра в функции CALCULATE вместо подсчета строк в переменной
RecoveredCustomersCategories. Так что здесь мы приведем лишь заключительную измененную часть меры, а начало будет таким же.
Мера в таблице Sales:
Sales Recovered Customers :=
...
VAR RecoveredCustomersCat =
DISTINCT (
-- Получаем список уникальных вернувшихся
SELECTCOLUMNS (
-- клиентов и категорий товаров
RecoveredCustomersCategories;
"CustomerKey"; Sales[CustomerKey];
"Category"; 'Product'[Category]
)
)
VAR Result =
CALCULATE (
[Sales Amount];
290
 Глава 13. Новые и постоянные покупатели
KEEPFILTERS ( RecoveredCustomersCat )
)
RETURN
Result
Постоянные покупатели
Последней мерой в этом списке рассмотрим меру # Returning Customers.
Мера в таблице Sales:
# Returning Customers :=
VAR MinDate = MIN ( 'Date'[Date] )
VAR FilterCategories =
CALCULATETABLE (
VALUES ( 'Product'[Category] );
ALLSELECTED ( 'Product' )
)
VAR CustomersWithNewDate =
CALCULATETABLE (
-- Готовим таблицу, в которой для
ADDCOLUMNS (
-- каждого клиента и категории будет
SUMMARIZE (
-- присутствовать дата первой покупки
Sales;
Sales[CustomerKey];
'Product'[Category]
);
"@NewCustomerDate"; [Date New Customer]
); ALLSELECTED ( Customer );
FilterCategories;
-- Фильтруем Product Category из ALLSELECTED
ALLEXCEPT (
-- Удаляем все фильтры, за исключением
Sales;
-- Customer, полученного из ALLSELECTED,
Sales[CustomerKey]; -- чтобы результат не менялся
Customer
-- в разных ячейках отчета
)
)
VAR ExistingCustomers =
-- Чтобы получить существующих клиентов,
FILTER (
-- фильтруем всех покупателей
CustomersWithNewDate;
-- и проверяем, что их первая покупка
[@NewCustomerDate] < MinDate -- была сделана до текущего периода
)
VAR ActiveCustomersCategories =
CALCULATETABLE (
SUMMARIZE (
-- Извлекаем комбинации
Sales;
-- Customer, Category и Date,
Sales[CustomerKey];
-- активные в текущей выборке
'Product'[Category];
'Date'[Date]
);
ALLEXCEPT (
-- Удаляем все фильтры, кроме Date и
Sales;
-- Customer, полученного из ALLSELECTED,
Обобщенный динамический шаблон (по категории)
'Date';
Sales[CustomerKey];
Customer
 291
-- чтобы результат не менялся
-- в разных ячейках отчета
);
VALUES ( 'Product'[Category] )
-)
-VAR ReturningCustomersCategories =
NATURALINNERJOIN (
ActiveCustomersCategories;
-ExistingCustomers
-)
VAR ReturningCustomers =
DISTINCT (
-SELECTCOLUMNS (
-ReturningCustomersCategories;
"CustomerKey"; Sales[CustomerKey]
)
)
VAR Result =
COUNTROWS ( ReturningCustomers )
RETURN
Result
Восстанавливаем связанный
фильтр по Category
Комбинируем активных
покупателей и существующих
Получаем список уникальных
покупателей
В данной мере создается переменная CustomersWithNewDate, в которой собираются первые даты покупок для каждой комбинации покупателя и категории товара. Полученный результат объединяется с
комбинацией клиентов и категорий, присутствующих в текущем контексте фильтра по таблице Sales. Результатом будет набор постоянных
покупателей, который мы сохраняем в переменной ReturningCustomers,
в которой впоследствии подсчитываем количество строк. В мере Sales
Returning Customers в качестве фильтра используется переменная
ReturningCustomersCat, а не ReturningCustomers. Здесь мы представим
лишь заключительные строки кода, все предыдущие полностью совпадают с описанной выше мерой.
Мера в таблице Sales:
Sales Returning Customers :=
...
VAR ReturningCustomersCat =
SELECTCOLUMNS (
-- Покупатели и категории товаров, без дат
ReturningCustomersCategories;
"CustomerKey"; Sales[CustomerKey];
"Category"; 'Product'[Category]
)
VAR Result =
CALCULATE (
292
 Глава 13. Новые и постоянные покупатели
[Sales Amount];
KEEPFILTERS ( ReturningCustomersCat )
)
RETURN
Result
-- Подсчитываем количество
-- клиентов, присутствующих
-- в ReturningCustomersCat
Использование снимков
Динамическое вычисление количества новых и постоянных покупателей – довольно затратная операция. Поэтому данные шаблоны зачастую реализуются при помощи предварительно вычисленных таблиц
(снимков), в которых хранится наиболее актуальная информация о датах на желаемом уровне гранулярности.
Использование снимков позволяет значительно увеличить скорость
вычислений, пусть и ценой некоторой гибкости. В абсолютном шаблоне
со снимками статус нового или постоянного покупателя не зависит от
фильтров, примененных в отчете. Результаты, полученные посредством
этого шаблона, совпадают с отчетом, сформированным при помощи
динамического абсолютного шаблона.
В шаблоне используется снимок со статусами покупателей (New = новый, Lost = ушедший, Temporarily lost = временно ушедший и Recovered =
вернувшийся), показанный на рис. 13.10.
Рис. 13.10. Снимок, содержащий полную историю
статусов по всем покупателям
Статусы New и Lost уникальны для каждого покупателя, тогда как
Temporarily lost и Recovered могут появляться для одного и того же клиента несколько раз.
Результирующая таблица объединена с таблицами Customer и Date
посредством обычных связей в модели данных, которая продемонстрирована на рис. 13.11.
Использование снимков  293
Рис. 13.11. Таблица CustomerEvents находится в связи
с таблицами Customer и Date
Проектирование таблицы CustomerEvents является очень важным шагом. Создание ее в виде производного снимка (derived snapshot) с использованием вычисляемой таблицы в DAX может быть достаточно эффективным для статусов New и Lost, тогда как для статусов Temporarily lost и
Recovered это может оказаться весьма дорогостоящей операцией. Также
стоит помнить, что статусы временно ушедших покупателей могут понадобиться для определения статуса вернувшихся. В моделях данных с
сотнями тысяч клиентов или сотнями миллионов транзакций необходимо рассмотреть возможность подготовки такой таблицы вне модели
данных и загрузки ее в виде обычной таблицы.
После того как модель данных будет полностью готова, останется
написать несколько несложных и эффективных мер на DAX. На самом
деле для представленной модели нет необходимости писать отдельно
внутренние и внешние меры – внешние и так будут достаточно просты,
а логика определения статусов покупателей заложена в самой таблице.
В этом и состоит причина того, что результирующий код на DAX будет
довольно несложным.
Единственное вычисление, требующее отдельного пояснения, кроется в мере # Returning Customers, поскольку здесь количество покупателей рассчитывается динамически, с игнорированием всех фильтров,
кроме Date и Customer. Затем из полученного значения вычитается
294
 Глава 13. Новые и постоянные покупатели
количество новых покупателей, извлеченное в результате запроса к
таблице снимка.
Мера в таблице Sales:
# New Customers :=
CALCULATE (
COUNTROWS ( CustomerEvents );
KEEPFILTERS ( CustomerEvents[Event] = "New" )
)
Мера в таблице Sales:
# Lost Customers :=
CALCULATE (
COUNTROWS ( CustomerEvents );
KEEPFILTERS ( CustomerEvents[Event] = "Lost" )
)
Мера в таблице Sales:
# Temporarily Lost Customers :=
CALCULATE (
DISTINCTCOUNT ( CustomerEvents[CustomerKey] );
KEEPFILTERS ( CustomerEvents[Event] = "Temporarily lost" )
)
Мера в таблице Sales:
# Recovered Customers :=
CALCULATE (
DISTINCTCOUNT ( CustomerEvents[CustomerKey] );
KEEPFILTERS ( CustomerEvents[Event] = "Recovered" )
)
Мера в таблице Sales:
# Returning Customers :=
VAR NewCustomers = [# New Customers]
VAR NumberOfCustomers =
CALCULATE (
[# Customers];
ALLEXCEPT ( Sales; 'Date'; Customer )
Использование снимков  295
)
VAR ReturningCustomers =
NumberOfCustomers - NewCustomers
VAR Result =
IF ( ReturningCustomers <> 0; ReturningCustomers )
RETURN
Result
В мерах, вычисляющих суммы продаж для новых и постоянных
покупателей, мы воспользуемся преимуществом наличия физической
связи между снимком CustomerEvents и таблицей Customer, тем самым
снизив объем кода на DAX и повысив эффективность вычислений.
Мера в таблице Sales:
Sales New Customers :=
CALCULATE (
[Sales Amount];
KEEPFILTERS ( CustomerEvents[Event] = "New" );
CROSSFILTER (
CustomerEvents[CustomerKey];
Customer[CustomerKey];
BOTH
)
)
Мера в таблице Sales:
Sales Recovered Customers :=
CALCULATE (
[Sales Amount];
KEEPFILTERS ( CustomerEvents[Event] = "Recovered" );
CROSSFILTER (
CustomerEvents[CustomerKey];
Customer[CustomerKey];
BOTH
)
)
Мера в таблице Sales:
Sales Returning Customers :=
VAR SalesAmount = [Sales Amount]
VAR SalesNewCustomers = [Sales New Customers]
VAR SalesReturningCustomers = SalesAmount - [Sales New Customers]
VAR Result =
296
 Глава 13. Новые и постоянные покупатели
IF (
SalesReturningCustomers <> 0;
SalesReturningCustomers
)
RETURN
Result
Создание производных снимков в DAX
Ранее мы предположили, что было бы эффективно создать таблицу CustomerEvents отдельно от модели данных и затем загрузить ее в
качестве простой таблицы. В самом деле, создание подобной таблицы
в DAX может обернуться достаточно дорогостоящим процессом с точки
зрения ресурсов компьютера. Реализация создания снимка при помощи DAX, описанная в этом разделе, будет приемлемо работать с моделями, содержащими до нескольких тысяч покупателей и до нескольких
миллионов транзакций. Если вы имеете дело с более объемной моделью, то можете реализовать подобную бизнес-логику с использованием других инструментов и языков – более оптимизированных с точки
зрения подготовки данных.
Сложная часть вычисления заключается в определении дат, когда
покупатель перешел в категорию временно ушедших с возможностью
возврата в будущем. Такие статусы могут быть множественными для
каждого отдельного клиента. Именно поэтому для каждой транзакции
мы фиксируем по две даты в двух вычисляемых столбцах в таблице
Sales:
• TemporarilyLostDate: это дата, которую мы вычисляем при помощи
меры Date Temporary Lost Customer, когда не существует транзакций в интервале между текущей строкой в таблице Sales и датой.
Если покупатель совершил несколько приобретений в один день,
для всех этих покупок будет прописано одно значение в столбце
TemporarilyLostDate;
• RecoveredDate: это дата первой покупки, сделанной покупателем
после даты из поля TemporarilyLostDate. Если транзакций после
этой даты обнаружено не было, в столбец записывается пустое
значение.
Код упомянутых вычисляемых столбцов будет следующим.
Вычисляемый столбец в таблице Sales:
TemporarilyLostDates =
VAR TemporarilyLostDate =
Использование снимков  297
CALCULATE (
[Date Temporary Lost Customer];
ALLEXCEPT ( Sales; Sales[Order Date]; Sales[CustomerKey] )
)
VAR CurrentCustomerKey = Sales[CustomerKey]
VAR CurrentDate = Sales[Order Date]
VAR CheckTemporarilyLost =
ISEMPTY (
CALCULATETABLE (
Sales;
REMOVEFILTERS ( Sales );
Sales[CustomerKey] = CurrentCustomerKey;
Sales[Order Date] > CurrentDate
&& Sales[Order Date] <= TemporarilyLostDate
)
)
VAR Result =
IF ( CheckTemporarilyLost; TemporarilyLostDate )
RETURN
Result
Вычисляемый столбец в таблице Sales:
RecoveredDates =
VAR TemporarilyLostDate = Sales[TemporarilyLostDate]
VAR Result =
IF (
NOT ISBLANK ( TemporarilyLostDate );
CALCULATE (
MIN ( Sales[Order Date] );
ALLEXCEPT ( Sales; Sales[CustomerKey] );
DATESBETWEEN ( 'Date'[Date]; TemporarilyLostDate+1; BLANK() )
)
)
RETURN
Result
Впоследствии мы используем эти вычисляемые столбцы для получения вычисляемой таблицы, которая станет промежуточным шагом
к снимку CustomerEvents. Если вы хотите воспользоваться сторонним
инструментом для расчета статусов Temporarily Lost и Recovered, вам необходимо рассмотреть вариант загрузки этих двух таблиц из источника
данных и преобразования содержимого при помощи соответствующего
инструмента. Фрагменты данных из таблиц представлены на рис. 13.12
и 13.13.
298
 Глава 13. Новые и постоянные покупатели
Рис. 13.12. Таблица TempLostDates содержит только статусы Temporarily Lost
Рис. 13.13. Таблица RecoveredDates содержит только статусы Recovered
Итоговый снимок данных CustomerEvents может быть получен посредством полного объединения четырех таблиц с использованием
функции UNION.
Вычисляемая таблица:
CustomerEvents =
VAR CustomerGranularity =
ALLNOBLANKROW ( Sales[CustomerKey] )
VAR NewDates =
ADDCOLUMNS (
CustomerGranularity;
"Date"; [Date New Customer];
"Event"; "New"
)
VAR LostDates =
ADDCOLUMNS (
CustomerGranularity;
"Date"; [Date Lost Customer];
"Event"; "Lost"
)
VAR Result =
UNION (
NewDates;
LostDates;
TempLostDates;
RecoveredDates
)
RETURN
Result
Разбивать объемные операции на более мелкие составляющие бывает очень полезно для обучения и позволяет лучше понять весь процесс
и попробовать реализовать его в собственной модели данных. Однако
Использование снимков  299
если вы решите выполнить поставленную задачу исключительно силами DAX, то можете пропустить этап создания промежуточных вычисляемых таблиц TempLostDates и RecoveredDates. В этом случае вам
необходимо будет уделить особое внимание функциям CALCULATE,
чтобы избежать создания циклических зависимостей, путем реализации явных фильтров, полученных в результате выполнения итераций по выводу функции ALLNOBLANKROW. Это приведет к более
многословной формуле для снимка, и здесь мы назвали такую таблицу
CustomerEventsSingleTable.
Вычисляемая таблица:
CustomerEventsSingleTable =
VAR CustomerGranularity =
ALLNOBLANKROW ( Sales[CustomerKey] )
VAR LostDates =
ADDCOLUMNS (
CustomerGranularity;
"Date"; [Date New Customer];
"Event"; "New"
)
VAR NewDates =
ADDCOLUMNS (
CustomerGranularity;
"Date"; [Date Lost Customer];
"Event"; "Lost"
)
VAR _TempLostDates =
CALCULATETABLE (
SUMMARIZE (
Sales;
Sales[CustomerKey];
Sales[TemporarilyLostDate];
"Event"; "Temporarily lost"
);
FILTER (
ALLNOBLANKROW ( Sales[TemporarilyLostDate] );
NOT ISBLANK ( Sales[TemporarilyLostDate] )
)
)
VAR _RecoveredDates =
CALCULATETABLE (
SUMMARIZE (
Sales;
Sales[CustomerKey];
Sales[RecoveredDate];
"Event"; "Recovered"
);
FILTER (
Powered by TCPDF (www.tcpdf.org)
300
 Глава 13. Новые и постоянные покупатели
ALLNOBLANKROW ( Sales[RecoveredDate] );
NOT ISBLANK ( Sales[RecoveredDate] )
)
)
VAR Result =
UNION (
NewDates;
LostDates;
_TempLostDates;
_RecoveredDates
)
RETURN
Result
Хотя в файлах примеров содержится определение снимка
CustomerEventsSingleTable, меры в отчете его не используют. Если вы хотите использовать именно этот подход, можете заменить определение
CustomerEvents на выражение из формулы для CustomerEventsSingleTable,
удалив предыдущее выражение из модели. Также вам нужно будет удалить вычисляемые таблицы TempLostDates и RecoveredDates, которые
больше не будут использоваться.
Глава
14
Количество уникальных
связанных элементов
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Шаблон подсчета количества уникальных связанных элементов (related
distinct count) применим в случае, если у вас есть одна или несколько
таблиц фактов, связанных с измерениями и вам необходимо извлечь
количество уникальных значений столбца в измерении, но только по
элементам, для которых существуют транзакции в таблице фактов. Для
примера мы будем пробовать вычислять количество уникальных наименований товаров в модели данных с двумя таблицами фактов Sales и
Receipts.
Поскольку в нашем примере наименование товара не является уникальным атрибутом по причине исключения из него указания на цвет,
простое получение количества уникальных значений ключа товара в
таблицах Sales или Receipts не решит нашу проблему. Наконец, мы также
покажем, как определить количество уникальных наименований товаров, присутствующих в обеих таблицах фактов и как минимум в одной
из них.
Описание шаблона
Столбец Product[Product Name] в таблице Product не является уникальным, а в наши планы входит получение количества уникальных наименований товаров, по которым были продажи. В модели данных, представленной на рис. 14.1, содержится две таблицы фактов, связанные с
измерением товаров: Sales и Receipts.
 Глава 14. Количество уникальных связанных элементов
302
Рис. 14.1. Модель данных содержит две таблицы фактов: Sales и Receipts
Основываясь на этой модели данных, нам необходимо вычислить
количество уникальных наименований товаров, движения по которым
присутствуют в:
•
•
•
•
таблице Sales;
таблице Receipts;
обеих таблицах Sales и Receipts;
по крайней мере в одной из этих таблиц.
Желаемый вид отчета показан на рис. 14.2.
Код для первых двух мер будет таким.
Мера в таблице Sales:
# Prods from Sales :=
VAR ProdsFromSales =
SUMMARIZE ( Sales; 'Product'[Product Name] )
VAR Result =
-- Оптимизированная версия
SUMX ( ProdsFromSales; 1 )
-- COUNTROWS ( ProdsFromSales )
RETURN
Result
Описание шаблона  303
Рис. 14.2. В отчете показаны четыре меры из данного шаблона
Мера в таблице Receipts:
# Prods from Receipts :=
VAR ProdsFromReceipts =
SUMMARIZE ( Receipts; 'Product'[Product Name] )
VAR Result =
-- Оптимизированная версия
SUMX ( ProdsFromReceipts; 1 )
-- COUNTROWS ( ProdsFromReceipts )
RETURN
Result
С использованием функции SUMMARIZE меры # Prods from Sales и
# Prods from Receipts извлекают количество уникальных наименований
товаров, присутствующих в соответствующей таблице фактов. Функция
SUMX просто подсчитывает полученные значения и используется вместо функций COUNTROWS или DISTINCTCOUNT с целью оптимизации.
Больше информации об этом можно получить по адресу https://sql.bi/432941.
Несмотря на более длинный код в сравнении с использованием
функции DISTINCTCOUNT совместно с двунаправленной перекрестной
фильтрацией (bidirectional cross-filtering), представленная здесь версия
меры будет выполняться быстрее в подавляющем большинстве случаев – когда количество транзакций в таблице фактов значительно превышает число элементов в справочнике.
Примечание. Наиболее часто в мерах, подобных # Prods from Sales и # Prods
from Receipts, для вычисления переменной Result будет использоваться функ-
304
 Глава 14. Количество уникальных связанных элементов
ция COUNTROWS. Версия с SUMX предложена здесь только в целях оптимизации подсчета, что работает применительно к простым мерам. В следующих
мерах, рассматриваемых в данной главе, мы будем использовать более привычную функцию COUNTROWS, поскольку в более сложных случаях использование функции SUMX не дает прироста производительности.
Формулы, использующие функции SUMMARIZE и COUNTROWS, могут быть легко адаптированы для применения в следующих формулах,
выполняющих пересечение (# Prods from Both) и объединение (# Prods
from Any) товаров.
Мера в таблице Receipts:
# Prods from Both :=
VAR ProdsFromSales =
SUMMARIZE ( Sales; 'Product'[Product Name] )
VAR ProdsFromReceipts =
SUMMARIZE ( Receipts; 'Product'[Product Name] )
VAR ProdsFromBoth =
INTERSECT ( ProdsFromSales; ProdsFromReceipts )
VAR Result =
COUNTROWS ( ProdsFromBoth )
RETURN
Result
Мера в таблице Receipts:
# Prods from Any :=
VAR ProdsFromSales =
SUMMARIZE ( Sales; 'Product'[Product Name] )
VAR ProdsFromReceipts =
SUMMARIZE ( Receipts; 'Product'[Product Name] )
VAR ProdsFromOne =
DISTINCT ( UNION ( ProdsFromSales; ProdsFromReceipts ) )
VAR Result =
COUNTROWS ( ProdsFromOne )
RETURN
Result
Здесь мы показали, как работают функции INTERSECT и UNION. Но
данный шаблон может быть легко адаптирован и для более сложных вычислений. В следующем примере в мере # Prods in Sales and not in Receipts
будет подсчитано количество наименований товаров, которые присутствуют в таблице Sales, но отсутствуют в таблице Receipts. Для выполнения задачи мы воспользовались функцией EXCEPT вместо INTERSECT и
UNION, примененных в предыдущих примерах.
Описание шаблона  305
Мера в таблице Sales:
# Prods in Sales and not in Receipts :=
VAR ProdsFromSales =
SUMMARIZE ( Sales; 'Product'[Product Name] )
VAR ProdsFromReceipts =
SUMMARIZE ( Receipts; 'Product'[Product Name] )
VAR ProdsFromSalesAndNotReceipts =
EXCEPT ( ProdsFromSales; ProdsFromReceipts )
VAR Result =
COUNTROWS ( ProdsFromSalesAndNotReceipts )
RETURN
Result
Результат вычисления меры # Prods in Sales and not in Receipts показан
на рис. 14.3.
Рис. 14.3. В мере # Prods in Sales and not in Receipts подсчитывается
количество уникальных наименований товаров, присутствующих
в таблице Sales, но отсутствующих в Receipts
Данный шаблон может быть расширен для вычисления количества
уникальных значений по любому столбцу в таблице, которая может
быть достигнута из таблицы фактов посредством цепочки связей типа
«многие к одному». Это представляется возможным благодаря способности функции SUMMARIZE осуществлять группировку по любым
столбцам из этой таблицы.
Глава
15
Незавершенные события
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Шаблон для работы с незавершенными событиями (events in progress)
может быть применен в очень широком спектре сценариев. Его уместно
задействовать, когда речь идет о различного рода событиях, обладающих определенной длительностью, то есть датой начала и датой окончания. В таком случае событие считается незавершенным в любой день
в интервале между этими двумя датами. Для примера мы будем разбирать сценарий с заказами из демонстрационной базы данных Contoso.
У каждого заказа есть дата оформления (order date) и дата поставки
(delivery date). В терминах событий дата, когда заказ был размещен в
системе, является датой начала события, а дата получения заказа адресатом – датой его окончания. При этом заказ считается открытым, если
он был оформлен, но еще не был доставлен. Нас интересует, сколько
заказов на определенную дату являются открытыми и на какую общую
сумму.
Как и многие другие шаблоны, описанные в данной книге, шаблон
для работы с незавершенными событиями может быть реализован как
динамически при помощи мер, так и статически – с использованием
снимков.
Определение незавершенных событий
Нам необходимо выяснить, сколько заказов обладают открытым статусом на определенную дату применительно к базе Contoso. На рис. 15.1
показан результат вычисления на уровне дней: количество открытых
заказов на конкретную дату составляется из количества открытых заказов на предыдущий день плюс количество полученных заказов и минус
Определение незавершенных событий  307
количество доставленных заказов в отчетный день. Сокращение EOP
здесь означает End Of Period (конец периода).
Рис. 15.1. В отчете показаны полученные заказы, доставленные
и открытые с детализацией по дням
Однако, если формировать отчет за более длительный период, например за несколько дней, месяц или год, итоги могут оказаться неоднозначными. Чтобы избежать этой двойственности в вычислениях,
необходимо четко определиться с желаемым результатом. Когда мы
смотрим отчет по дням, картина нам представляется совершенно ясной, как на рис. 15.2.
Рис. 15.2. На 15 октября 2019 года у нас есть два открытых заказа
Вполне очевидно, что в сценарии, представленном на рис. 15.2,
только заказы с номерами 2 и 5 можно считать открытыми на момент
формирования отчета (15 октября 2019 года). И действительно, заказ с
308
 Глава 15. Незавершенные события
номером 1 на этот момент уже доставлен, а заказы 3 и 4 еще не были
сформированы. Таким образом, здесь вычисление определено очень
четко. Увы, но, если взять период из нескольких дней, месяца или года,
картина станет менее очевидной и прозрачной. Взгляните на рис. 15.3,
на котором отчетный период составляет целый месяц.
Рис. 15.3. Сколько заказов было открыто в октябре: один, два или три?
В сценарии, показанном на рис. 15.3, заказ под номером 1 был закрыт
еще до наступления отчетного периода. Что касается заказа 2, он был
создан до начала октября, а доставлен в течение этого месяца. Заказ номер 3 был открыт в течение октября, но до окончания месяца доставлен
не был. Пятый заказ был сформирован и доставлен в течение октября, а
четвертый не попал в рассматриваемый нами временной интервал. Как
видите, определение, которое казалось предельно ясным и четким на
уровне дней, нуждается в дополнительной расшифровке на более высоких уровнях агрегации.
Но нам бы не хотелось описывать все возможные случаи для наших
сценариев. Таким образом, мы ограничимся тремя определениями для
заказов в случае, когда отчетный период превышает интервал одного
дня. Каждая из рассматриваемых мер будет иметь определенное окончание в своем названии:
• ALL: возвращает количество заказов, которые были открыты на
протяжении хотя бы одного дня из заданного диапазона. В сценарии, представленном на рис. 15.3, таких заказов три: с номерами 2, 3 и 5, при этом последний считается открытым по причине
того, что в какой-то момент в течение октября он действительно
был сформирован и не доставлен;
• EOP: оценивает статус заказов на конец отчетного периода. В рассмотренном сценарии под такое определение подпадает только
один заказ под номером 3 – он был сформирован в течение октября и на момент окончания месяца не был доставлен;
Открытые заказы  309
• AVG: вычисляет среднесуточное количество открытых заказов за
период. Для этого рассчитывается количество открытых заказов
по дням и усредняется применительно к более длительному периоду.
Могут быть и другие определения открытых заказов, которые в той
или иной степени будут базироваться на трех перечисленных выше.
Открытые заказы
Если в таблице Orders данные хранятся на правильном уровне гранулярности – по одной строке для каждого заказа, с указанием даты формирования заказа и даты его поставки, – то соответствующую модель
можно представить так, как показано на рис. 15.4.
Рис. 15.4. Модель данных с таблицей Orders
Код на языке DAX для меры, вычисляющей количество открытых заказов в подобной модели данных, будет довольно простым.
Мера в таблице Orders:
# Open Orders ALL :=
VAR MinDate = MIN ( 'Date'[Date] )
VAR MaxDate = MAX ( 'Date'[Date] )
VAR Result =
CALCULATE (
COUNTROWS ( Orders );
Orders[Order Date] <= MaxDate;
Orders[Deliver Date] > MinDate;
REMOVEFILTERS ( 'Date' )
)
RETURN
Result
310
 Глава 15. Незавершенные события
Стоит отметить, что модификатор REMOVEFILTERS используется в
данном случае для того, чтобы исключить все фильтры в отчете, которые могут оказывать влияние на таблицу Date.
На основании этой меры можно сформулировать и две ее разновидности (на конец периода и по среднему) с использованием следующих
формул.
Мера в таблице Orders:
# Open Orders EOP :=
CALCULATE (
[# Open Orders ALL];
LASTDATE ( 'Date'[Date] )
)
Мера в таблице Orders:
# Open Orders AVG :=
AVERAGEX (
'Date';
[# Open Orders ALL]
)
Результат вычисления этих мер можно видеть на рис. 15.5.
Рис. 15.5. В отчете показаны три меры для вычисления
количества открытых заказов
В таблице Orders могут присутствовать и несколько строк для одного
заказа. В случае, если в таблице Orders хранится по одной записи для
каждой строки из табличной части заказа, а не для всего документа,
Открытые заказы  311
необходимо использовать функцию DISTINCTCOUNT применительно
к столбцу, хранящему уникальные идентификаторы заказов, вместо
функции COUNTROWS по таблице Orders. Рассматриваемая здесь
модель данных не подпадает под этот сценарий, но, если бы это было
так, формула меры # Open Orders ALL отличалась бы всего одной строкой, как показано ниже.
Мера в таблице Orders:
# Open Orders ALL :=
VAR MinDate = MIN ( 'Date'[Date] )
VAR MaxDate = MAX ( 'Date'[Date] )
VAR Result =
CALCULATE (
-- Если для заказа хранятся
DISTINCTCOUNT ( Orders[Order Number] ); -- несколько строк, сработает
Orders[Order Date] <= MaxDate;
-- DISTINCTCOUNT, а не COUNTROWS
Orders[Deliver Date] > MinDate;
REMOVEFILTERS ( 'Date' )
)
RETURN
Result
Если вам необходимо рассчитать денежный эквивалент открытых заказов в долларах, используйте обращение к мере Sales Amount вместо
вызова функций COUNTROWS или DISTINCTCOUNT. Например, определение меры Open Amount ALL в данном случае будет таким.
Мера в таблице Orders:
Open Amount ALL :=
VAR MinDate = MIN ( 'Date'[Date] )
VAR MaxDate = MAX ( 'Date'[Date] )
VAR Result =
CALCULATE (
[Sales Amount];
-- Мера Sales Amount вместо
Orders[Order Date] <= MaxDate; -- функций DISTINCTCOUNT/COUNTROWS
Orders[Deliver Date] > MinDate;
REMOVEFILTERS ( 'Date' )
)
RETURN
Result
В оставшихся двух мерах необходимо будет просто сменить обращение к мере # Open Orders ALL на Open Amount ALL.
312
 Глава 15. Незавершенные события
Мера в таблице Orders:
Open Amount EOP :=
CALCULATE (
[Open Amount ALL];
LASTDATE ( 'Date'[Date] )
)
Мера в таблице Orders:
Open Amount AVG :=
AVERAGEX (
'Date';
[Open Amount ALL]
)
На рис. 15.6 показаны результаты вычисления описанных выше
мер.
Рис. 15.6. Три меры, показывающие денежный эквивалент
открытых заказов
Формулы, описанные в данном разделе, хорошо работают применительно к небольшим наборам данных, но при этом предъявляют
немалые требования к движку формул. Это приводит к снижению эффективности расчетов, когда количество заказов в модели данных исчисляется сотнями тысяч. Если вам необходимо более действенное и
быстрое решение, воспользуйтесь для своего сценария шаблоном со
снимками.
Открытые заказы с использованием снимков  313
Открытые заказы с использованием снимков
Создание снимков помогает упростить вычисления и повысить их эффективность. Ежедневные снимки, к примеру, могут содержать по одной строке для даты и номера заказа, который был открыт в этот день.
Таким образом, для одного заказа, который был открыт в течение десяти дней, в таком снимке потребуется ровно десять строк.
На рис. 15.7 показан фрагмент подобной ежедневной таблицы-снимка.
Рис. 15.7. Каждому заказу отведено в таблице ровно столько строк,
сколько дней документ был открыт
В объемных моделях данных, в которых подобные снимки могут
насчитывать десятки миллионов строк, рекомендуется использовать
специализированные инструменты ETL или запросы на языке SQL для
получения результатов таких снимков. В менее крупномасштабных моделях можно обойтись силами Power Query или DAX. Например, снимок
для нашей модели данных может быть создан при помощи следующего
кода на DAX.
Вычисляемая таблица:
Open Orders =
SELECTCOLUMNS (
GENERATE (
Orders;
DATESBETWEEN (
'Date'[Date];
Orders[Order Date];
Orders[Deliver Date] - 1
)
);
"StoreKey"; Orders[StoreKey];
314
 Глава 15. Незавершенные события
"CustomerKey"; Orders[CustomerKey];
"Order Number"; Orders[Order Number];
"Date"; 'Date'[Date]
)
На рис. 15.8 показана модель данных со снимком Open Orders, объединенным с измерениями обычными связями типа «один ко многим».
Рис. 15.8. Снимок связан с остальными таблицами
для возможности использовать фильтрацию
С использованием таблицы-снимка формулы, вычисляющие количество открытых заказов, значительно упростились и ускорились.
Мера в таблице Orders:
# Open Orders ALL :=
DISTINCTCOUNT ( 'Open Orders'[Order Number] )
Мера (скрытая) в таблице Orders:
# Rows Open Orders :=
COUNTROWS ( 'Open Orders' )
Мера в таблице Orders:
# Open Orders EOP :=
CALCULATE (
Открытые заказы с использованием снимков  315
[# Rows Open Orders];
LASTDATE ( 'Date'[Date] )
)
Мера в таблице Orders:
# Open Orders AVG :=
AVERAGEX (
'Date';
[# Rows Open Orders]
)
Среди перечисленных мер лишь мера # Open Orders ALL потребовала использования функции DISTINCTCOUNT. Другие две меры –
# Open Orders EOP и # Open Orders AVG – вычисляют количество открытых заказов по одному дню за раз, для чего больше подходит
более быстрая функция COUNTROWS, вызванная применительно к
таблице-снимку.
В мере Open Amount ALL список открытых заказов будет использоваться в качестве фильтра, примененного к таблице Orders. Этого можно добиться при помощи функции TREATAS.
Мера в таблице Orders:
Open Amount ALL :=
VAR OpenOrders =
DISTINCT ( 'Open Orders'[Order Number] )
VAR FilterOpenOrders =
TREATAS (
OpenOrders;
Orders[Order Number]
)
VAR Result =
CALCULATE (
[Sales Amount];
FilterOpenOrders;
REMOVEFILTERS ( Orders )
)
RETURN Result
Меры Open Amount EOP и Open Amount AVG просто ссылаются на меру
Open Amount ALL вместо # Open Orders ALL.
316
 Глава 15. Незавершенные события
Мера в таблице Orders:
Open Amount EOP :=
CALCULATE (
[Open Amount ALL];
LASTDATE ( 'Date'[Date] )
)
Мера в таблице Orders:
Open Amount AVG :=
AVERAGEX (
'Date';
[Open Amount ALL]
)
Размер снимка в данном случае зависит от количества заказов и
средней длительности их выполнения. Если заказ в среднем остается
незакрытым в течение нескольких дней, вполне приемлемо будет
использовать гранулярность по дням. Если же заказы в системе
могут оставаться открытыми гораздо более длительные промежутки
времени (допустим, годы), логично будет задуматься о смене уровня
гранулярности снимка до месяцев – по одной строке для каждого заказа
и месяца, когда он был открыт.
В качестве одного из возможных вариантов оптимизации можно использовать неактивную связь между таблицами Orders и Open Orders.
Это реализуемо только в случае, если в таблице Orders хранится по одной строке для каждого заказа, т. е. столбец Orders[Order Number] является уникальным, а связь между таблицами может иметь тип «один
ко многим». Неактивная связь может быть установлена примерно так,
как показано на рис. 15.9. Не используйте технику создания связи типа
«многие ко многим», поскольку подход с использованием DAX и функции TREATAS будет не менее эффективным и более простым для понимания и поддержки.
Также вам необходимо будет заменить меру Open Amount ALL на Open
Amount ALL optimized, формула которой показана ниже.
Мера в таблице Orders:
Open Amount ALL optimized :=
CALCULATE (
[Sales Amount];
USERELATIONSHIP ( 'Open Orders'[Order Number]; Orders[Order Number] );
Открытые заказы с использованием снимков  317
CROSSFILTER ( Orders[Order Date]; 'Date'[Date]; NONE )
)
Использование неактивной связи для распространения фильтра
призвано снизить нагрузку на движок формул DAX и повысить эффективность всех мер, основанных на мере Open Amount ALL. Но необходимо помнить о побочных эффектах применения функции
USERELATIONSHIP и аккуратно использовать функцию CROSSFILTER для
исключения возможных неоднозначностей в модели данных. Обычно
будет достаточно деактивировать связь между таблицами Orders и
Date, как в нашем примере, но не лишним будет сравнить результаты
оптимизированной меры с мерой, основанной на функции TREATAS.
Рис. 15.9. Неактивная связь типа «один ко многим» позволяет оптимизировать
скорость выполнения запросов
Глава
16
Ранжирование
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Возможность выполнять операцию ранжирования применительно к самым разнообразным сущностям – весьма распространенное требование. Поиск лучших покупателей, товаров с самым высоким спросом или
стран с максимальными объемами продаж – одни из наиболее частых
задач для менеджмента компании.
Ранжирование может быть как статическим, так и динамическим.
Статическое ранжирование присваивает, например, товарам определенные ранги без учета установленных фильтров, тогда как при выполнении динамического ранжирования позиции элементов в списке
вычисляются заново каждый раз, когда пользователь взаимодействует
с отчетом. Допустим, если будет изменен отчетный год, ранги тут же
пересчитаются.
Все базовые вычисления в области ранжирования основываются на
функции RANKX, тогда как более продвинутые техники – к примеру,
выбор первой десятки товаров – требуют использования функции TOPN
совместно со сложными табличными вычислениями.
Статическое ранжирование
Статические ранги присваиваются элементам в таблице при помощи
вычисляемых столбцов, которые, как вы знаете, пересчитываются при
обновлении данных. Таким образом, статические ранги не зависят от
выбранных фильтров в отчетах. Взгляните на рис. 16.1. Первый товар
в списке обладает и первым рангом, поскольку речь идет о ведущем
продавце по всем категориям. В то же время второй в списке товар
Статическое ранжирование  319
(SV 16xDVD M360 Black) обладает только четвертым рангом, а не вторым, как можно было ожидать. Причина в том, что есть два других товара с рангами 2 и 3, не входящих в категорию TV and Video, выбранную в
области фильтра слева. Как видите, статическое ранжирование не зависит от текущего выбор в фильтрах, а показывает ранги по всей модели
данных для товаров, видимых в данный момент в отчете.
Рис. 16.1. В отчете показаны сквозные ранги товаров по всей модели,
несмотря на то что не все товары видимы
Если снять выбор по категории товаров в фильтре слева, все товары
будут выведены в отчет, а вместе с этим восстановится и привычный
глазу порядок ранжирования.
Рис. 16.2. Без выбора в фильтре ранжирование выполняется
по возрастанию без пропусков
Для выполнения статического ранжирования товаров на основе
меры Sales Amount необходимо добавить в таблицу Product вычисляемый столбец со следующей простой формулой.
Вычисляемый столбец в таблице Product:
Product Rank =
RANKX (
ALL ( 'Product' );
[Sales Amount]
)
В данном случае в функции ALL нет особой необходимости. Однако
с ней лучше понятно намерение выполнить ранжирование всех без ис-
320
 Глава 16. Ранжирование
ключения товаров – это единственная причина, по которой мы добавили ее в код вычисляемого столбца.
Похожая формула может быть использована при выполнении ранжирования в рамках ограниченного набора данных внутри таблицы.
Например, в следующем примере мы проведем ранжирование товаров
внутри одной категории.
Вычисляемый столбец в таблице Product:
Rank in Category =
RANKX (
ALLEXCEPT ( 'Product'; 'Product'[Category] );
[Sales Amount]
)
Как видно по рис. 16.3, товар в четвертой строке отчета (SV 16xDVD
M360 Black) обладает четвертым сквозным рангом (поле Product
Rank), а в рамках категории (Rank in Category) TV and Video его ранг
составляет 2.
Рис. 16.3. В столбце Rank in Category показаны ранги товаров
в своих категориях
Динамическое ранжирование
Шаблон динамического ранжирования предназначен для определения
рангов, которые будут меняться в зависимости от выбранных фильтров
в отчете. Соответственно, его реализация основана не на вычисляемых
столбцах, а на мерах.
Рис. 16.4. Динамическое ранжирование товаров в рамках
выбранного в отчете фильтра
Динамическое ранжирование  321
Код меры Product Rank будет следующим.
Мера в таблице Product:
Product Rank :=
IF (
ISINSCOPE ( 'Product'[Product Name] );
VAR SalesAmountCurrentProduct = [Sales Amount]
VAR ProductRank =
RANKX (
ALLSELECTED ( 'Product' );
[Sales Amount]
)
VAR Result =
IF (
NOT ISBLANK ( SalesAmountCurrentProduct );
ProductRank
)
RETURN
Result
)
Получение разных рангов требует обновления таблицы, по которой
осуществляются итерации при помощи функции RANKX. Например,
на рис. 16.5 показана мера Rank in Category, вычисляющая ранг товара
внутри своей категории с учетом всех существующих в отчете фильтров.
Рис. 16.5. В колонке Rank in Category проводится ранжирование товаров
внутри категории
322
 Глава 16. Ранжирование
Определение меры Rank in Category будет следующим.
Мера в таблице Product:
Rank in Category :=
VAR SalesAmountCurrentProduct = [Sales Amount]
VAR ProductsInCategory =
CALCULATETABLE (
'Product';
REMOVEFILTERS ( 'Product'[Product Name] );
ALLSELECTED ( 'Product' );
VALUES ( 'Product'[Category] )
)
VAR ProductRank =
IF (
ISINSCOPE ( 'Product'[Product Name] ),
RANKX (
ProductsInCategory,
[Sales Amount]
)
)
VAR Result =
IF (
NOT ISBLANK ( SalesAmountCurrentProduct ),
ProductRank
)
RETURN
Result
Три лидирующих товара в категории
Ранжирование часто используется для получения отчетов, фильтрующих товары на основании их рангов внутри заданной группы. Например, в отчете на рис. 16.6 показано, как производить аналитику по трем
лидирующим товарам внутри каждой категории. В подобных сценариях существует два возможных решения в зависимости от того, является
ли наименование товара частью отчета.
Если в отчете показываются наименования товаров, можно использовать меру Rank in Category из динамического шаблона ранжирования и вынести ее в фильтры визуализации в Power BI, как показано на
рис. 16.6.
И хотя данную технику нельзя назвать наиболее мощной, мы решили
показать ее как достаточно эффективный способ фильтрации лидирующих товаров по категориям. К тому же она позволяет удовлетворить
наиболее распространенное требование показывать лидирующие товары в категориях с использованием наименований.
Три лидирующих товара в категории  323
Рис. 16.6. В отчете показано по три лидирующих товара из каждой категории
путем выполнения фильтрации по мере Rank in Category
Если же наименования товаров не присутствуют в визуализации,
описанный подход использовать не получится по причине несовместимости гранулярности визуального элемента и меры. На рис. 16.7 показан пример с удаленными из отчета наименованиями товаров.
Рис. 16.7. В мере Sales Amount показаны общие цифры по всем товарам из
категории, без учета установленного фильтра в мере Rank in Category
Причина того, почему был проигнорирован фильтр, заключается в
том, что он применяется только к уровню визуального элемента с наибольшей гранулярностью. Таким образом, фильтр в визуализации может не затронуть товары. Для гарантии применения фильтра на уровне
наименований товаров мера, включающая Sales Amount, должна обеспечить вычисление ранжирования на корректном уровне гранулярности.
Это подразумевает определение списка лидирующих товаров, которые
должны войти в калькуляцию, и использование его в качестве фильтра
при выполнении вычисления. В мере Sales Top 3 Products показано применение такой техники.
324
 Глава 16. Ранжирование
Мера в таблице Sales:
Sales Top 3 Products :=
VAR TopThreeProducts =
GENERATE (
ALLSELECTED ( 'Product'[Category] );
-- Для каждой категории
TOPN (
-- извлекаем наименования
3;
-- трех лидирующих товаров,
ALLSELECTED ( 'Product'[Product Name] ); -- основываясь на вычисле[Sales Amount]
-- нии меры Sales Amount
)
)
VAR Result =
CALCULATE (
[Sales Amount];
-- Вычисляем меру Sales Amount
KEEPFILTERS ( TopThreeProducts ) -- с использованием TopThreeProducts
)
-- в качестве фильтра
RETURN
Result
На рис. 16.8 показано вычисление мер Sales Top 3 Products и Sales
Amount в одном отчете. Несмотря на то что наименования товаров не
демонстрируются в отчете, в мере Sales Top 3 Products учитываются
только продажи по трем ведущим товарам в каждой категории, игнорируя продажи по остальным товарам. Эти же условия сохраняются и
при вычислении итогов.
Рис. 16.8. В колонке Sales Top 3 Products показаны продажи только по трем
лидирующим товарам в каждой категории
С точки зрения производительности формула меры Sales Top 3 Products
немного уступает варианту с выводом меры в фильтр уровня визуализации. Так что всегда, когда это возможно, лучше пользоваться первым
из предложенных шаблонов, а данный вариант можно использовать
в случае необходимости или, к примеру, если инструмент клиента не
поддерживает фильтрацию на уровне визуального элемента.
Глава
17
Иерархии
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Иерархии (hierarchies) зачастую создаются в моделях данных с целью
упрощения навигации пользователей по элементам и атрибутам. Определение иерархии отвечает требованиям конкретной модели. Например, в иерархии по датам вполне естественно может присутствовать
иерархия с уровнями год, квартал, месяц и день. Что касается справочника товаров, то здесь уместно будет создать уровни иерархии по категории, подкатегории и, собственно, товару.
Иерархии предполагают добавление в отчет сразу несколько связанных столбцов, но они также полезны при выполнении вычислений. Допустим, в мере могут отображаться продажи товара в виде процента на
том или ином уровне иерархии. Все остальные вычисления также могут
использовать этот подход путем настройки расчета для каждого уровня
иерархии.
Определение текущего уровня иерархии
Любые вычисления, в которые вовлечены иерархии, должны опираться
на код DAX для идентификации текущего уровня иерархии. Так что вам
необходимо четко уяснить, как в той или иной ситуации легко и просто определить, на каком уровне производится вычисление. На рис. 17.1
показана мера Product Level, единственной целью которой является
определение отображаемого уровня иерархии. Обычно подобные меры
скрыты в модели данных, поскольку зачастую они используются в других мерах для произведения вычислений применительно к конкретному уровню.
326
 Глава 17. Иерархии
Рис. 17.1. В отчете показан текущий уровень иерархии для каждого элемента
Код меры Product Level будет следующим.
Мера (скрытая) в таблице Product:
Product Level :=
VAR IsProductInScope = ISINSCOPE ( 'Product'[Product Name] )
VAR IsSubcatInScope = ISINSCOPE ( 'Product'[Subcategory] )
VAR IsCatInScope = ISINSCOPE ( 'Product'[Category] )
VAR Result =
SWITCH (
TRUE ();
IsProductInScope; "Product";
IsSubcatInScope; "Subcategory";
IsCatInScope; "Category";
"No filter"
)
RETURN
Result
При помощи функции ISINSCOPE в переменных IsProductInScope,
IsSubcatInScope и IsCatInScope выполняется проверка на то, осуществляется ли группировка по данному уровню иерархии. В этом случае
в соответствующем столбце будет находиться единственное значение,
видимое в контексте фильтра.
В функции SWITCH выполняется идентификация текущего уровня
иерархии путем последовательной проверки на истинность введенных
выше переменных начиная с уровня с наибольшей гранулярностью.
Порядок следования проверок в функции SWITCH имеет значение. По
сути, если уровень товара находится в области видимости, автоматически в ней будут присутствовать и категория с подкатегорией. Таким
Доля от родительского уровня  327
образом, сначала в мере должны проверяться уровни с наибольшими
ограничениями, а определение активного уровня должно начинаться
с максимально низкого уровня иерархии с постепенным увеличением.
Мера Product Level сама по себе не представляет никакой особенной
ценности. Техника, примененная в данной мере, зачастую используется
для реализации вычисления, напрямую зависящего от текущего уровня
иерархии. Мы создали эту меру с целью задействования ее в других мерах для идентификации актуального уровня иерархии.
Примечание. Если функция ISINSCOPE недоступна, можно в качестве альтернативы использовать функцию ISFILTERED, например в версиях Excel до
2019. Однако при применении функции ISFILTERED в выражениях DAX,
использующих иерархии, стоит помнить о том, что уровни, находящиеся за
границами верхнего уровня иерархии, отображаемого в визуализации, не
должны фильтроваться вне элемента визуализации, т. е. не могут присутствовать в фильтрах и срезах, а также не могут быть выбраны в других элементах
визуализации. Чтобы предотвратить возникновение такой ситуации, в отсутствие функции ISINSCOPE лучше всего создавать иерархии с использованием
лишь скрытых столбцов. Это означает дублирование столбцов, использующихся на уровнях иерархии, таким образом, чтобы они были также доступны в
качестве отдельных фильтров и срезов без оказания влияния на вычисления
в DAX в рамках иерархии.
Доля от родительского уровня
Распространенным вычислением применительно к иерархическим
данным является расчет показателя в процентном отношении к родительскому элементу, как показано на рис. 17.2.
В мере % Parent определяется уровень иерархии для рассчитываемой
ячейки и при вычислении процента в качестве знаменателя используется значение рассматриваемого показателя непосредственного родителя текущего элемента.
Мера в таблице Sales:
% Parent :=
VAR AllSelProds =
ALLSELECTED ( 'Product' )
VAR ProdsInCat =
CALCULATETABLE (
'Product';
AllSelProds;
VALUES ( 'Product'[Category] )
)
VAR ProdsInSub =
CALCULATETABLE (
328
 Глава 17. Иерархии
'Product';
ProdsInCat;
VALUES ( 'Product'[SubCategory] )
)
VAR Numerator = [Sales Amount]
VAR Denominator =
SWITCH (
[Product Level];
"Category"; CALCULATE ( [Sales Amount]; AllSelProds );
"Subcategory"; CALCULATE ( [Sales Amount]; ProdsInCat );
"Product"; CALCULATE ( [Sales Amount]; ProdsInSub )
)
VAR Result =
DIVIDE (
Numerator;
Denominator
)
RETURN
Result
Рис. 17.2. Процент рассчитывается относительно родительского
элемента в иерархии
Глава
18
Иерархии типа
родитель–потомок
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Иерархии типа родитель–потомок (parent-child hierarchies) зачастую
используются для отображения различного рода ведомостей, справочников магазинов или сотрудников и многого другого. Структура
хранения данных при реализации иерархии типа родитель–потомок
довольно своеобразная с той точки зрения, что подобные иерархии
обладают переменной глубиной вложенности. В данном шаблоне мы
рассмотрим примеры использования иерархий типа родитель–потомок при планировании бюджета и прогнозировании, а работать при
этом будем со статьями доходов и расходов и географической иерархией.
Введение
В шаблоне родитель–потомок иерархия определяется не присутствием определенных столбцов в таблицах источника данных. Вместо этого отношение между элементами базируется на структуре подчинения посредством хранения уникального идентификатора родителя.
Например, на рис. 18.1 показаны первые несколько строк иерархии
типа родитель–потомок с определением географической структуры
продаж.
Powered by TCPDF (www.tcpdf.org)
330
 Глава 18. Иерархии типа родитель–потомок
Рис. 18.1. В таблице Entity хранится ключ родителя
для каждого элемента сущности
Основываясь на представленной структуре данных, нам необходимо
отобразить иерархию с соблюдением принципов древовидности, т. е.
чтобы, например, элемент Contoso United States располагался внутри
Contoso North America, как показано на рис. 18.2.
Рис. 18.2. Иерархия родитель–потомок на основании ключей
Фактически иерархия типа родитель–потомок базируется на связи
таблицы, содержащей сущности, самой с собой, что не поддерживается
в движке Tabular по умолчанию. Кроме того, по своей природе иерархии такого типа могут обладать переменной глубиной, т. е. количество
уровней, необходимых для преодоления при проходе от вышестоящих
элементов к нижестоящим, может варьироваться в зависимости от выбранного маршрута. В связи с этим структуру иерархии типа родитель–
потомок лучше всего реализовывать при помощи техники, описанной
в данной главе.
Иерархии типа родитель–потомок часто используются для ведения
ведомостей с учетом доходов и расходов. В этом случае узлы также
определяют арифметический знак, используемый для агрегирования
значений. На рис. 18.3 показана ведомость, в которой суммы из категории расходов (expenses) вычитаются из итогов, а из категории доходов
Базовый шаблон иерархии типа родитель–потомок
 331
(incomes) прибавляются, несмотря на внешнее отсутствие арифметических знаков.
Рис. 18.3. Иерархия типа родитель–потомок может определять знаки
для агрегирования значений
В выражениях на DAX при агрегировании значений в подобных
иерархиях арифметический знак должен учитываться на всех нижележащих уровнях.
Базовый шаблон иерархии типа
родитель–потомок
В движке Tabular напрямую не поддерживаются ни иерархии с переменной глубиной, ни соединения таблиц самих с собой. Первым делом
при работе с любой иерархией типа родитель–потомок необходимо
выровнять структуру таким образом, чтобы для каждого возможного
уровня иерархии было по одному столбцу. Таким образом, мы должны
прийти от структуры, изображенной на рис. 18.4, в которой у нас есть
332
 Глава 18. Иерархии типа родитель–потомок
минимальное количество столбцов для определения иерархии, к структуре с рис. 18.5.
Рис. 18.4. В таблице представлено по одной строке
для каждого элемента с указанием наименования сущности
вне зависимости от количества уровней иерархии
Полностью раскрытая иерархия в данном случае включает в себя четыре уровня. На рис. 18.5 показана выровненная структура, в которой
каждому уровню иерархии соответствует столбец с названием от Level1
до Level4. Актуальное количество столбцов будет зависеть от конкретных данных, так что можно добавить дополнительные столбцы на случай расширения глубины иерархии в будущем.
Рис. 18.5. Выровненная иерархия, в которой каждому уровню
соответствует свой столбец
Для начала создадим технический столбец с названием EntityPath, в
котором воспользуемся функцией PATH.
Вычисляемый столбец в таблице Entity:
EntityPath =
PATH ( Entity[EntityKey]; Entity[ParentEntityKey] )
Базовый шаблон иерархии типа родитель–потомок
 333
В столбце EntityPath содержатся полные пути для достижения текущего узла от корневого элемента, как показано на рис. 18.6. Этот технический столбец может пригодиться для определения столбцов Level.
Рис. 18.6. В технической колонке EntityPath прописаны
полные пути для достижения текущего элемента
Код для всех столбцов с префиксов в наименовании Level будет похожим. Единственным отличием будет значение, присвоенное переменной LevelNumber. Например, такой будет формула для столбца Level1.
Вычисляемый столбец в таблице Entity:
Level1 =
VAR LevelNumber = 1
VAR LevelKey = PATHITEM ( Entity[EntityPath]; LevelNumber; INTEGER )
VAR LevelName = LOOKUPVALUE ( Entity[EntityName]; Entity[EntityKey]; LevelKey )
VAR Result = LevelName
RETURN
Result
У остальных столбцов из этой группы будут свои цифры в названиях и
свои значения переменной LevelNumber, соответствующие относительной позиции их уровня в иерархии. После определения всех столбцов
Level мы их скрываем и создаем обычную иерархию в таблице, включающую все наши столбцы Level. Очень важно, чтобы пользователь видел
эти поля только в составе иерархии – лишь так можно гарантировать,
что он будет осуществлять правильную навигацию по ним.
При использовании этих столбцов напрямую в отчете иерархия не
будет работать оптимально. Все уровни будут показываться, даже если
содержат пустые значения. В отчете, показанном на рис. 18.7, под элементом Contoso Asia Online Store виднеется еще одна строка, несмотря
334
 Глава 18. Иерархии типа родитель–потомок
на то что в столбце Level4 для этого узла стоит пустое значение, а это
означает, что данный узел может быть развернут на три уровня, а не на
четыре.
Рис. 18.7. Строки с пустыми наименованиями должны быть скрыты,
но этого не происходит по умолчанию
Чтобы скрыть нежелательные строки, для каждой строки необходимо выполнить проверку того, доступен ли текущий уровень из данного узла. Это можно сделать путем контроля глубины каждого узла. Нам
понадобится вычисляемый столбец в таблице иерархии, содержащий
глубину узла, определенного в каждой строке.
Вычисляемый столбец в таблице Entity:
Depth =
PATHLENGTH ( Entity[EntityPath] )
Теперь нам необходимо создать две меры: EntityRowDepth возвращает максимальную глубину текущего узла, а EntityBrowseDepth – текущую
глубину матрицы при помощи функции ISINSCOPE.
Мера в таблице Entity:
EntityRowDepth :=
MAX ( Entity[Depth] )
Мера в таблице Entity:
EntityBrowseDepth :=
ISINSCOPE ( Entity[Level1] )
Базовый шаблон иерархии типа родитель–потомок
 335
+ ISINSCOPE ( Entity[Level2] )
+ ISINSCOPE ( Entity[Level3] )
+ ISINSCOPE ( Entity[Level4] )
Наконец, используем две эти меры для скрытия результата, если значение переменной EntityRowDepth больше, чем EntityBrowseDepth.
Мера в таблице StrategyPlan:
Sum Amount :=
SUM ( StrategyPlan[Amount] )
Мера в таблице StrategyPlan:
Total Base :=
VAR Val = [Sum Amount]
VAR EntityShowRow =
[EntityBrowseDepth] <= [EntityRowDepth]
VAR Result =
IF ( EntityShowRow; Val )
RETURN
Result
Отчет, полученный с использованием меры Total Base и не содержащий пустых строк, как раньше, показан на рис. 18.8.
Рис. 18.8. Строки с пустыми наименованиями исчезли,
поскольку мера Total Base в этом случае также возвращает
пустое значение
Такой же шаблон должен быть применен к любой мере, которая будет
выводиться в отчете с использованием иерархии типа родитель–потомок.
336
 Глава 18. Иерархии типа родитель–потомок
Иерархия ведомости
Шаблон иерархии ведомости является разновидностью базового шаблона иерархии типа родитель–потомок, где иерархия также используется для управления вычислениями. Каждая строка в иерархии помечена одним из трех значений: Income (доход), Expense (расход) или Taxation
(налог). Доходы необходимо суммировать, а расходы и налоги должны
вычитаться из итоговых сумм. На рис. 18.9 показано содержимое таблицы с элементами иерархии.
Рис. 18.9. В каждой строке значение столбца AccountType
влияет на вычисления
Реализация здесь будет похожей на шаблон с иерархией родитель–
потомок – группируем вычисление по полю AccountType и применяем
соответствующий математический оператор в зависимости от его значения.
Иерархия ведомости  337
Мера в таблице StrategyPlan:
Total :=
VAR Val =
SUMX (
SUMMARIZE ( StrategyPlan; Account[AccountType] );
VAR SignToUse =
IF ( Account[AccountType] = "Income"; +1; -1 )
VAR Amount = [Sum Amount]
RETURN
Amount * SignToUse
)
VAR AccountShowRow = [AccountBrowseDepth] <= [AccountRowDepth]
VAR EntityShowRow = [EntityBrowseDepth] <= [EntityRowDepth]
VAR Result =
IF ( AccountShowRow && EntityShowRow; Val )
RETURN
Result
Мера Total может использовать обе иерархии типа родитель–потомок: иерархию, определенную в таблице Entity и показанную в предыдущем примере, и иерархию, определенную в таблице Account, которая
является предметом изучения в этом разделе.
Формула меры Total возвращает правильный результат для любого
узла в иерархии. Однако в подобных отчетах обычно существует требование, чтобы цифры показывались как положительные, даже если
они представляют расходы. Это требование может быть удовлетворено
путем смены знака результата на уровне отчета. В следующей реализации меры Total No Signs сначала определяется знак для использования
в отчете, после чего изменяется знак результата, чтобы расходы показывались как положительные числа, несмотря на свою отрицательную
природу.
Мера в таблице StrategyPlan:
Total No Signs :=
VAR BrowseLevel = [AccountBrowseDepth]
VAR AccountName =
SWITCH (
BrowseLevel;
1; SELECTEDVALUE ( Account[Level1]
2; SELECTEDVALUE ( Account[Level2]
3; SELECTEDVALUE ( Account[Level3]
4; SELECTEDVALUE ( Account[Level4]
5; SELECTEDVALUE ( Account[Level5]
6; SELECTEDVALUE ( Account[Level6]
);
);
);
);
);
);
338
 Глава 18. Иерархии типа родитель–потомок
7; SELECTEDVALUE ( Account[Level7] )
)
VAR AccountType =
LOOKUPVALUE ( Account[AccountType]; Account[AccountName]; AccountName )
VAR ValueToShow = [Total]
VAR Result =
IF ( AccountType = "Income"; +1; -1 ) * ValueToShow
RETURN
Result
Отчет, построенный при помощи меры Total No Signs, продемонстрирован на рис. 18.10.
Рис. 18.10. Результат вывода иерархии типа родитель–потомок
с использованием меры Total No Signs
Шаблон, представленный выше, прекрасно работает, если модель ведомости содержит поле AccountType, разделяющее все транзакции на
доходы и расходы. Но иногда в подобных моделях данных определение
Иерархия ведомости  339
арифметического знака операций определяется совсем иначе. Например, в таблице может присутствовать столбец, однозначно определяющий знак для целей агрегирования. Фрагмент такой таблицы с полем
Operator показан на рис. 18.11.
Рис. 18.11. В столбце Operator хранится математический знак для агрегирования
В этом случае код вычисляемых столбцов и мер будет гораздо более
сложным. Нам понадобится по одному столбцу для каждого уровня иерархии с указанием того, как как данный конкретный счет должен быть
виден при агрегировании на любом уровне иерархии. Иными словами,
один и тот же счет на одном уровне может агрегироваться со знаком
плюс, а на другом – со знаком минус.
Эти столбцы должны строиться от нижнего уровня иерархии. В нашем
примере понадобится семь столбцов – по максимальному числу уровней. Каждый столбец содержит знак, который должен быть применен в
агрегации к конкретному значению на том или ином уровне иерархии.
На рис. 18.12 показан фрагмент такой таблицы с семью дополнительными столбцами.
340
 Глава 18. Иерархии типа родитель–потомок
Рис. 18.12. В колонках с наименованиями от S L1 до S L7 проставлены знаки,
применяемые на соответствующем уровне иерархии
Давайте для примера посмотрим на строки со значениями 4 и 5 в
столбце AccountKey. Значения по счету 4 (Sale Revenue) должны суммироваться на уровнях 1, 2, 3 и 4, тогда как на других уровнях должны оставаться невидимыми. В свою очередь значения по счету 5 (Cost of Goods
Sold) должны суммироваться на четвертом уровне иерархии и вычитаться на уровнях 1, 2 и 3.
Формулы на DAX для каждого уровня иерархии будем писать, начиная с уровня с наибольшей гранулярностью – в нашем случае с седьмого. Для наиболее гранулярного уровня расчет знака сводится к простому конвертированию значения из поля Operator, как показано ниже.
Вычисляемый столбец в таблице Account:
SignToLevel7 =
VAR LevelNumber = 7
VAR Depth = Account[Depth]
RETURN
IF ( LevelNumber = Depth; IF ( Account[Operator] = "-"; -1; +1 ) )
Все остальные столбцы (для уровней от 1 до 6) будут следовать тому
же шаблону, но для каждого уровня выражение на DAX должно учитывать знак на смежном уровне с большей гранулярностью, хранящийся
Иерархия ведомости  341
в переменной PrevSign, и инвертировать результат при необходимости,
как показано ниже на примере для шестого уровня.
Вычисляемый столбец в таблице Account:
SignToLevel6 =
VAR LevelNumber = 6
VAR PrevSign = Account[SignToLevel7]
VAR Depth = Account[Depth]
VAR LevelKey =
PATHITEM ( Account[AccountPath]; LevelNumber; INTEGER )
VAR LevelSign =
LOOKUPVALUE ( Account[Operator]; Account[AccountKey]; LevelKey )
RETURN
IF (
LevelNumber = Depth;
IF ( Account[Operator] = "-"; -1; +1 );
IF ( LevelNumber < Depth; IF ( LevelSign = "-"; -1; +1 ) * PrevSign )
)
После определения всех столбцов для уровней необходимо создать
меру Signed Total для подсчета итогов с учетом знаков.
Мера в таблице StrategyPlan:
Signed Total :=
VAR BrowseDepth =
MAX ( [AccountBrowseDepth]; 1 )
VAR AccountShowRow = [AccountBrowseDepth] <= [AccountRowDepth]
VAR EntityShowRow = [EntityBrowseDepth] <= [EntityRowDepth]
VAR Result =
IF (
AccountShowRow && EntityShowRow;
SWITCH (
BrowseDepth;
1; SUMX (
VALUES ( Account[SignToLevel1] );
[Sum Amount] * Account[SignToLevel1]
);
2; SUMX (
VALUES ( Account[SignToLevel2] );
[Sum Amount] * Account[SignToLevel2]
);
3; SUMX (
VALUES ( Account[SignToLevel3] );
[Sum Amount] * Account[SignToLevel3]
);
4; SUMX (
VALUES ( Account[SignToLevel4] );
342
 Глава 18. Иерархии типа родитель–потомок
[Sum Amount] * Account[SignToLevel4]
);
5; SUMX (
VALUES ( Account[SignToLevel5] );
[Sum Amount] * Account[SignToLevel5]
);
6; SUMX (
VALUES ( Account[SignToLevel6] );
[Sum Amount] * Account[SignToLevel6]
);
7; SUMX (
VALUES ( Account[SignToLevel7] );
[Sum Amount] * Account[SignToLevel7]
)
)
)
RETURN
Result
Можно сравнить значения этой меры Signed Total с мерой Total в отчете, представленном на рис. 18.13.
Рис. 18.13. Две формулы возвращают разные знаки
для одних и тех же узлов в иерархии
Шаблон безопасности для иерархии родитель–потомок  343
В мере Total сумма по счету Internet стоит со знаком минус, поскольку
речь идет о расходе. В то же время в мере Signed Total сумма за интернет
отражена со знаком плюс, а отрицательное значение, например, присутствует в счете Expense, который агрегируется в родительском элементе со знаком минус.
Шаблон безопасности для иерархии
родитель–потомок
Распространенным требованием безопасности к иерархиям типа родитель–потомок является необходимость ограничения видимости элементов до конкретного узла (или набора узлов), включая все нижележащие элементы. В реализации этой функциональности нам поможет
функция PATHCONTAINS.
Применяя следующее выражение к роли безопасности в таблице
Account, мы ограничим видимость иерархии узлом, представленным
во втором аргументе функции PATHCONTAINS. Таким образом, все потомки данного узла станут видимыми пользователю, поскольку запрашиваемый узел (2, т. е. Income) также является частью значения поля
AccountPath для всех нижележащих элементов:
PATHCONTAINS (
Account[AccountPath];
2
)
-- Ключ элемента Income
Если бы мы использовали столбец AccountKey для ограничения видимости, мы бы в конце концов ограничили ее лишь одной строкой,
и пользователь не увидел бы вложенных элементов. Выбрав для этой
цели столбец AccountPath, мы исключили этот недостаток, и в области
видимости окажутся все элементы, входящие в путь к выбранному узлу.
Таким образом, пользователь сможет видеть только узлы (и соответствующие им значения), входящие в дерево, начинающееся с узла
Income, как показано на рис. 18.14.
Рис. 18.14. Иерархия ограничена узлом, видимым для активной роли безопасности
344
 Глава 18. Иерархии типа родитель–потомок
Узлы выше элемента Income (Level3) теперь не учитывают суммы для
других потомков в мере Total. Если это может вводить пользователей в
заблуждение, рассмотрите вариант исключения из отчета вышележащих элементов (в нашем примере это Level1 и Level2) или придания им
других заголовков, чтобы они лучше отражали результат.
Стоит отметить, что роль безопасности с используемой в ней функцией PATHCONTAINS может негативно сказаться на быстродействии
отображения иерархии, состоящей из тысяч узлов. Выражение, прописанное в роли, должно выполняться для каждого узла в иерархии, когда пользователь открывает соединение, и многократный вызов функции PATHCONTAINS может оказаться весьма дорогостоящей затеей в
случае работы с объемной иерархией.
Глава
19
Сравнение сопоставимых
показателей
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Под сравнением сопоставимых продаж (like-for-like sales) мы понимаем
скорректированную метрику, сопоставляющую два периода времени
по товарам или магазинам, имеющим схожие характеристики. В данной главе мы будем выполнять сравнение сопоставимых продаж в магазинах Contoso, которые работали на протяжении всех без исключения
рассматриваемых временных периодов. При этом список магазинов
постоянно обновляется: добавляются новые, удаляются и обновляются
существующие. Но при сравнении сопоставимых продаж мы будет учитывать только те магазины, которые функционировали на протяжении
всего рассматриваемого периода времени. Таким образом, в итоговый
отчет не попадут продажи по магазинам, которые по причине временных простоев не смогли показать запланированных результатов.
Как и многие другие шаблоны, сравнение сопоставимых показателей
можно проводить как статически, так и динамически. Какой вариант
расчета использовать, зависит от требований к производительности и
условий конкретной задачи. Мы рассмотрим все способы сравнения сопоставимых показателей на примере меры Same Store Sales.
Введение
Если вы анализируете продажи без учета того, были ли случаи закрытия магазинов в течение отчетного периода, вы можете посмотреть на
346
 Глава 19. Сравнение сопоставимых показателей
отчет, показанный на рис. 19.1, и подумать, что в 2009 году у компании
были серьезные проблемы из-за существенного падения объемов продаж.
Рис. 19.1. В 2009 году продажи значительно снизились
В действительности в 2009 году многие магазины компании были закрыты. Именно этим можно объяснить такой серьезный спад динамики
продаж. На рис. 19.2 показан отчет, в котором можно отследить, когда
какие магазины работали. Пустая ячейка символизирует закрытый магазин в этот период.
Рис. 19.2. Не все магазины бывают открыты круглогодично
В мере Same Store Sales мы постараемся рассчитать продажи только
по тем магазинам, которые были открыты в течение всего отчетного
периода (с 2007 по 2009 год), т. е. всего по трем из указанного списка,
что видно по рис. 19.3.
Сопоставимые продажи по магазинам с помощью снимков  347
Рис. 19.3. Всего три магазина работали на протяжении
все трех отчетных лет
Мера должна показывать правильные цифры даже при осуществлении среза по различным атрибутам, как показано на рис. 19.4.
Рис. 19.4. В мере вычисляются корректные цифры даже
при выполнении различных срезов
Сопоставимые продажи по магазинам
с помощью снимков
Лучший способ решения сценария, связанного со сравнением сопоставимых продаж по магазинам, связан с использованием таблицы-снимка, в которой хранятся статусы магазинов. Позже в данном шаблоне мы
покажем, как вычислить ту же меру динамически, без использования
снимков. Но именно метод со снимками является наиболее эффективным в плане производительности и масштабируемости.
Таблица-снимок должна содержать все магазины компании со всеми годами и дополнительным столбцом со статусом, как показано на
рис. 19.5.
Создать таблицу-снимок StoreStatus можно посредством следующего
кода.
348
 Глава 19. Сравнение сопоставимых показателей
Вычисляемая таблица:
StoreStatus =
VAR AllStores =
CROSSJOIN (
FILTER (
ALLNOBLANKROW ( 'Date'[Calendar Year Number] );
'Date'[Calendar Year Number] IN { 2007; 2008; 2009 }
);
ALLNOBLANKROW ( Store[StoreKey] )
)
VAR OpenStores =
SUMMARIZE (
Receipts;
'Date'[Calendar Year Number];
Receipts[StoreKey]
)
VAR Result =
UNION (
ADDCOLUMNS ( OpenStores; "Status"; "Open" );
ADDCOLUMNS ( EXCEPT ( AllStores; OpenStores ); "Status"; "Closed" )
)
RETURN
Result
Рис. 19.5. В таблице StoreStatus отображаются статусы
магазинов по всем годам
Получившаяся таблица StoreStatus будет обладать гранулярностью на
уровне магазина и года. Таким образом, она будет объединена при помощи обычной сильной связи (strong relationship) с таблицей магазинов
Store и слабой связью (weak relationship) типа «многие ко многим» (MMR –
Many-Many-Relationship) с таблицей Date. Если слабые связи недоступны в используемом вами инструменте (как, например, в Power Pivot), вы
можете распространить фильтр с таблицы Date на таблицу Store в DAX
при помощи функций TREATAS или INTERSECT.
Сопоставимые продажи по магазинам с помощью снимков  349
Рис. 19.6. Модель данных требует создания слабой связи
между таблицами Date и StoreStatus
Мера Same Store Sales проверяет статус магазинов за весь отчетный
период. В случае вхождения статуса Closed в любой момент времени
функция SELECTEDVALUE вернет строку «Closed» или пустое значение,
что позволит исключить этот магазин из итоговой выборки.
Мера в таблице Receipts:
Same Store Sales :=
VAR OpenStores =
CALCULATETABLE (
FILTER (
ALLSELECTED ( StoreStatus[StoreKey] );
CALCULATE (
SELECTEDVALUE ( StoreStatus[Status] )
) = "Open"
);
ALLSELECTED ( 'Date' )
)
VAR FilterOpenStores =
TREATAS (
-- Используем таблицу
------
Оставляем только
магазины со
статусом Open
по всем выбранным
годам
OpenStores для
350
 Глава 19. Сравнение сопоставимых показателей
OpenStores;
Store[StoreKey]
-- фильтрации Store[StoreKey]
-- путем изменения привязки данных
)
VAR Result =
CALCULATE (
[Sales Amount];
KEEPFILTERS ( FilterOpenStores )
)
RETURN
Result
Формула требует, чтобы в таблице-снимке присутствовала информация по всем магазинам и годам. Если в таблице хранятся данные только
по тем годам, когда магазин был открыт, код работать не будет.
Сопоставимые продажи по магазинам
без помощи снимков
Если у вас нет возможности построить снимок для хранения статусов
всех магазинов по годам, вы можете воспользоваться кодом на DAX для
выполнения динамического сравнения сопоставимых продаж.
Для этого необходимо «на лету» подсчитать количество лет в отчетном периоде, после чего отфильтровать все магазины, в которых были
продажи по этим годам. Иными словами, если в отчете выбрано три
года, фильтр должны пройти только те магазины, в которых были продажи на протяжении всех трех лет. Если как минимум в одном из трех
лет продаж не было, магазин автоматически исключается из списка сопоставимых.
Мера в таблице Receipts:
Same Store Sales Dynamic :=
VAR NumberOfYears =
CALCULATE (
DISTINCTCOUNT ( 'Date'[Calendar Year] );
CROSSFILTER ( Receipts[Sale Date]; 'Date'[Date]; BOTH );
ALLSELECTED ( )
)
VAR StoresAndYears =
CALCULATETABLE (
SUMMARIZE (
-- Группируем таблицу Receipts
Receipts;
-- по магазину и году,
Store[StoreKey];
-- чтобы посчитать, сколько лет
'Date'[Calendar Year]
-- магазин существует
);
Сопоставимые продажи по магазинам без помощи снимков  351
ALLSELECTED ( )
-- По всем выбранным годам и магазинам
)
VAR StoresAndYearCount =
GROUPBY (
StoresAndYears;
Store[StoreKey];
"@Years"; SUMX ( CURRENTGROUP (); 1 )
)
VAR OpenStores =
FILTER (
StoresAndYearCount;
[@Years] = NumberOfYears
)
VAR Result =
CALCULATE (
[Sales Amount];
KEEPFILTERS ( OpenStores )
-- Фильтруем Store[StoreKey]
)
RETURN
Result
В плане вычисления эта формула будет куда более дорогостоящей, по
сравнению с методом, основанным на создании снимка. К тому же здесь
логика определения статусов магазинов заложена непосредственно в
код формулы. А исходя из опыта, лучше все же держать бизнес-логику
отдельно от DAX – возможно, хранить ее в источнике данных. Так что,
если у вас подобной информации в источнике данных нет, мы советовали бы вам обратить внимание на реализацию с использованием снимка, даже для небольших наборов данных.
Мера Same Store Sales Dynamic показывает продажи по трем магазинам в Канаде, которые были открыты на протяжении всех трех лет
(с 2007-го по 2009-й).
Рис. 19.7. Лишь три магазина в Канаде были открыты
на протяжении всего отчетного периода
Глава
20
Матрица переходов
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Шаблон матрицы переходов (transition matrix) помогает анализировать
изменения атрибута, назначенного сущности, через равные промежутки времени. Например, покупателям могут присваиваться оценки
каждый месяц, или у товаров могут еженедельно меняться определенные рейтинги. Оценка изменений показателей между двумя точками
во времени может потребовать определения того, сколько элементов
сменили один статус на другой в отчетном периоде. Матрица переходов позволяет конечному пользователю производить подобный анализ
без написания дополнительных запросов – просто воспользовавшись
фильтрами в отчете.
Введение
Допустим, что каждому товару ежемесячно присваивается рейтинг
на основании отношения объема продаж в текущем месяце к предыдущему. Соответствующая конфигурационная таблица показана на
рис. 20.1.
Рис. 20.1. Конфигурационная таблица рейтингов
на основании роста продаж
Введение  353
Простая реализация динамической сегментации позволит вам узнать, сколько товаров входило в ту или иную категорию каждый месяц,
как показано на рис. 20.2.
Рис. 20.2. Подсчет количества товаров по рейтингам
при помощи динамической сегментации
Как вы понимаете, один и тот же товар может иметь разный рейтинг в
разные моменты времени. Та же матрица, но в разрезе одного товара A.
Datum SLR Camera, показана на рис. 20.3.
Рис. 20.3. Изменение рейтинга товара с течением времени
В целом было бы интересно провести следующий анализ: взять все
товары с определенным рейтингом в начальном месяце и проследить
изменение рейтинга со временем. Повышался или понижался рейтинг выбранных товаров? Результат исследования можно видеть на
рис. 20.4.
354
 Глава 20. Матрица переходов
Рис. 20.4. Изменение рейтингов 36 товаров,
которые в марте 2007 года имели рейтинг Stable
В отчете, изображенном на рис. 20.4, показана сводная информация
о рейтингах 36 товаров, которые в марте 2007 года обладали рейтингом
Stable. Рейтинги товаров из этой группы менялись с течением времени,
при этом присутствуют они только в тех месяцах, когда были продажи.
Таким образом, от месяца к месяцу общее количество отслеживаемых
товаров может меняться. Например, в апреле у 8 из 36 товаров был низкий рейтинг, 10 наименований сохранили свой статус, а 13 повысили
рейтинг. В сумме 31. Получается, что 5 наименований в апреле 2007 года
не продавались вовсе. Необходимо понимать, что в апрельской оценке участвуют только те товары, у которых в марте был рейтинг Stable
(см. группу фильтров слева от диаграммы). Эти же принципы сохраняются и при анализе последующих месяцев – в исследовании участвуют
те же 36 наименований товаров.
Существует множество способов создания матрицы переходов. В данной главе мы подробно остановимся на двух из них. Первый подход
базируется на создании таблицы-снимка, что ведет к созданию очень
быстрой статической матрицы переходов. Во втором подходе мы будем
полагаться исключительно на DAX, что обусловливает создание чуть
более медленной, но гораздо более гибкой динамической матрицы переходов.
Оба способа создания матрицы переходов разделяют некоторые общие требования к модели данных. Начнем мы с более простого статического подхода, после чего углубимся в детали при изучении концепции
динамической матрицы переходов. При этом базовые требования мы
повторять не будем. Так что, если вам необходимо построить динамическую матрицу переходов, для начала мы советуем ознакомиться с
Статическая матрица переходов  355
разделом создания статической, чтобы не пропустить важные нюансы
настройки вашей модели данных.
Статическая матрица переходов
При создании статической матрицы переходов (static transition matrix)
мы воспользуемся таблицей-снимком, содержащей ежемесячные рейтинги всех товаров. В представленном примере мы сформировали
данную таблицу при помощи DAX. В вашем случае вся необходимая
информация уже может быть представлена в источнике данных. Важно отметить, что в таблице должен быть указан месяц, товар и присвоенный рейтинг. На рис. 20.5 показан фрагмент снимка с названием
Monthly Ratings.
Рис. 20.5. В снимке содержится информация о месяце, товаре
и соответствующих им рейтингах
Но одной лишь таблицы-снимка недостаточно для решения сценария. Нам понадобятся две дополнительные таблицы, которые позволят
конечному пользователю выбрать начальный месяц и рейтинг для анализа. Интерфейс пользователя показан на рис. 20.6.
Рис. 20.6. Для создания этого отчета необходимо наличие четырех
независимых полей в модели данных
356
 Глава 20. Матрица переходов
Срез для начального месяца (Starting Month) (1) не может основываться на календарном поле Date[Calendar Year Month]. По сути, таблица Date уже представлена на строках матрицы (3), так что она не может
быть отфильтрована при помощи внешнего среза для отображения,
скажем, сентября 2007 года, даже если в качестве начального месяца указан март 2007-го. Также и срез с начальным рейтингом (Starting
Rating) (2) не может использовать тот же атрибут рейтинга из снимка, использующийся в столбцах матрицы (4). Иначе говоря, столбцы
матрицы и срез должны использовать в качестве источника данных
разные таблицы.
Итак, нам понадобятся две вычисляемые таблицы для наполнения
срезов, которые мы назовем Starting Month и Starting Rating.
Вычисляемая таблица:
Starting Month =
SELECTCOLUMNS (
SUMMARIZE ( Sales; 'Date'[Calendar Year Month];
'Date'[Calendar Year Month Number] );
"Starting Month"; 'Date'[Calendar Year Month];
"Starting Month Sort"; 'Date'[Calendar Year Month Number]
)
Вычисляемая таблица:
Starting Rating =
SELECTCOLUMNS (
SUMMARIZE ( Rating; Rating[Rating]; Rating[RatingKey] );
"Starting Rating"; Rating[Rating];
"Starting Rating Sort"; Rating[RatingKey]
)
Эти таблицы срезов не связаны с другими таблицами в модели данных. Таким образом, считывать выбор в срезах и использовать его для
вычисления мер мы будем исключительно посредством DAX.
В то же время таблицы снимков должны быть объединены связями
с другими таблицами в модели данных. В нашем случае мы прибегнем
к помощи слабой связи типа «многие ко многим» с таблицей Date на
основании столбца Calendar Year Month Number, а с таблицей Product
свяжемся традиционным способом по полю ProductKey. Фрагмент получившейся модели данных представлен на рис. 20.7.
Статическая матрица переходов  357
Рис. 20.7. Снимок должен быть связан с другими таблицами в модели данных
После создания модели данных необходимо при помощи формул на
DAX считать текущий выбор пользователя из срезов и использовать эту
информацию для определения списка товаров, статус которых соответствует выбранному в указанном месяце. Далее этот список будет
использоваться в качестве фильтра к таблице-снимку с целью ограничения расчетов выбранными товарами.
Мера в таблице Sales:
# Products Matrix :=
VAR SelectedStartingMonths =
TREATAS (
VALUES ( 'Starting Month'[Starting Month] );
'Date'[Calendar Year Month]
)
VAR SelectedStartingRatings =
TREATAS (
VALUES ( 'Starting Rating'[Starting Rating] );
'Monthly Ratings'[Product Rating]
)
VAR StartingProducts =
CALCULATETABLE (
VALUES ( 'Monthly Ratings'[ProductKey] );
SelectedStartingRatings;
SelectedStartingMonths;
REMOVEFILTERS ( 'Monthly Ratings'[Product Rating Sort] );
REMOVEFILTERS ( 'Date' )
)
VAR Result =
CALCULATE (
DISTINCTCOUNT ( 'Monthly Ratings'[ProductKey] );
KEEPFILTERS ( StartingProducts )
358
 Глава 20. Матрица переходов
)
RETURN
Result
Поскольку статическая матрица переходов основывается на вычисляемой таблице, ее содержимое не может считаться динамическим. Это
означает, что, если пользователь отфильтрует покупателей по конкретной стране, цифры в матрице переходов не изменятся. В то же время на
результат будет влиять выбор элементов в таблицах, непосредственно
связанных со снимком. В нашем случае это таблицы Date и Product.
Если вам необходима полностью динамическая матрица переходов с
пересчетом результатов на основе текущего выбора полей во всей модели данных, советуем взглянуть на более мощный, хотя и более медленный шаблон создания динамической матрицы переходов.
Динамическая матрица переходов
Динамическая матрица переходов служит тем же целям, что и статическая с тем лишь различием, что не требует для своего функционирования создания таблицы-снимка. Вместо этого расчет показателей
происходит каждый раз при вычислении меры, что обеспечивает необходимую динамику.
Модель данных для динамической матрицы переходов будет такой
же, как для статической, за исключением таблицы-снимка. Результат
показан на рис. 20.8. Здесь мы добавили срез по континенту – в случае
со статической матрицей переходов этот фильтр не сработал бы.
Рис. 20.8. Динамическая матрица переходов чувствительна
ко всем фильтрам в отчете
Динамическая матрица переходов  359
Поскольку шаблон требует многократного вычисления рейтинга по
товарам, в данном случае мы создадим вспомогательную меру, возвращающую рейтинг товара за указанный месяц.
Мера в таблице Sales:
Status :=
CALCULATE (
DISTINCT ( Rating[Rating] );
FILTER (
ALLNOBLANKROW ( Rating );
VAR CurrentValue = [% Of Previous Month]
VAR LowerBoundary = Rating[Min Growth]
VAR UpperBoundary = Rating[Max Growth]
RETURN
AND ( CurrentValue >= LowerBoundary;
CurrentValue < UpperBoundary )
)
)
Основная мера будет довольно запутанной и сложной. Логически она
состоит из двух шагов.
1. Вычисляем список товаров, входящих в выбранные срезы. Для
этого, поскольку изначально рейтинги товаров неизвестны, рассчитываем рейтинги для всех товаров, а затем исключаем те, которые не были выбраны.
2. Определяем рейтинги товаров, вычисленных на предыдущем
шаге, на этот раз в текущем контексте фильтра. Этот шаг очень
похож на предыдущий – единственная разница заключается в
фильтрации дат и товаров, что лучше объяснено в комментариях
к следующей формуле.
Мера в таблице Sales:
# Products Matrix Dynamic :=
VAR SelectedStartingMonths =
-- Сначала сохраним
TREATAS (
-- выбранный начальный
VALUES ( 'Starting Month'[Starting Month] ); -- месяц как столбец
'Date'[Calendar Year Month]
-- из таблицы Date, чтобы
)
-- фильтровать таблицу Date
VAR SelectedStartingRatings =
-- Сохраним выбранный рейтинг
VALUES ( 'Starting Rating'[Starting Rating] ) -- как начальную точку
VAR CurrentRatings =
-- Сохраним рейтинги текущего контекста
VALUES ( Rating[Rating] )
-- фильтра (не начальные, а текущие)
Powered by TCPDF (www.tcpdf.org)
360
 Глава 20. Матрица переходов
VAR StartingProdsAndMonths =
-- Сохраним товары и месяцы
CALCULATETABLE (
-- начального периода в переменной
SUMMARIZE (
'Sales';
'Product'[ProductKey];
'Date'[Calendar Year Month]
-- Чтобы отчет не фильтровался по
);
-- другим периодам, необходимо
SelectedStartingMonths;
-- удалить внешний фильтр по датам
REMOVEFILTERS ( 'Date' )
-- и заменить его на начальный месяц
)
VAR StartingProdAndStatus =
-- Рассчитываем рейтинги товаров
CALCULATETABLE (
-- за начальный месяц,
FILTER (
-- только по товарам, соответствующим
StartingProdsAndMonths;
-- выбранному стартовому рейтингу
[Status] IN SelectedStartingRatings
);
REMOVEFILTERS ( 'Date' )
-- Избавляемся от исходного фильтра
)
VAR StartingProductsInStatus =
-- Наконец, нам нужны только ключи
DISTINCT (
-- товаров, так что мы удаляем остальные
SELECTCOLUMNS (
-- столбцы из предыдущей таблицы
StartingProdAndStatus;
-- Эта таблица будет фильтровать
"ProductKey"; 'Product'[ProductKey] -- только Product
)
)
--- На данный момент мы определились со списком товаров, которые обладали указанным
-- рейтингом в начальный месяц. Следующим шагом используем текущий контекст
-- фильтра, созданный матрицей, чтобы узнать, где оказались товары в отчетном
-- периоде
--- Код сильно похож на предыдущий, на этот раз с использованием
-- StartingProductsInStatus в качестве фильтра по таблице Sales
-- с целью ограничить анализ
-VAR CurrentProdsAndMonths =
-- Определяем товары и месяцы
CALCULATETABLE (
-- в текущем периоде
SUMMARIZE (
'Sales';
'Product'[ProductKey];
'Date'[Calendar Year Month]
);
-- Ограничиваем видимость товаров теми,
StartingProductsInStatus
-- что были определены на предыдущем шаге
)
VAR CurrentProdAndStatus =
CALCULATETABLE (
-- Рассчитываем рейтинг каждого товара
FILTER (
-- для каждого месяца в текущем периоде
CurrentProdsAndMonths;
[Status] IN CurrentRatings
);
REMOVEFILTERS ( 'Date' )
Динамическая матрица переходов  361
)
VAR CurrentProducts =
-- Нам надо посчитать товары, так что мы удаляем
DISTINCT (
-- все столбцы, оставляя лишь уникальные
SELECTCOLUMNS (
-- значения в столбце ProductKey
CurrentProdAndStatus;
"ProductKey"; 'Product'[ProductKey]
)
)
VAR Result =
COUNTROWS ( CurrentProducts )
RETURN
Result
Как видите, код получился отнюдь не тривиальным. И его изменение
под ваши собственные нужды потребует глубокого понимания принципов его работы.
Динамическая матрица переходов, несмотря на всю свою мощь и гибкость, невероятно требовательна к центральному процессору компьютера и оперативной памяти. Скорость работы этого шаблона напрямую
зависит от количества товаров. В моделях данных с сотнями тысяч
товаров этот шаблон вряд ли будет жизнеспособным. Но в меньших по
масштабу моделях он будет прекрасно справляться со своей задачей,
хотя со статической матрицей переходов по эффективности, конечно,
не сравнится.
Глава
21
Опросник
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
В шаблоне опросника (survey) модель данных используется для анализа
зависимостей между различными событиями в рамках единой сущности, например для проведения анкетирования покупателей. В организациях, связанных со здравоохранением, данный шаблон может применяться для анализа информации о состоянии пациентов, диагнозах и
предписанных медикаментах.
Описание шаблона
У вас есть модель данных, в которой хранятся ответы на вопросы. Предположим, все вопросы с возможными вариантами ответов располагаются в таблице Questions, как показано на рис. 21.1.
Рис. 21.1. Каждый вопрос имеет
несколько вариантов ответа
Описание шаблона  363
Ответы на вопросы, как и ожидается, хранятся в таблице Answers – по
одной строке для каждого ответа покупателя на вопрос. При этом один
покупатель может дать несколько ответов на каждый вопрос – в этом
случае строки в таблице будут отличаться только значением в поле
Answer, тогда как поля Customer и Question будут содержать одинаковые
значения. В реальной модели данных информация хранилась бы вместе с целочисленными ключами. Но здесь для лучшего понимания концепции мы решили обойтись строковыми данными. Таблица ответов
на вопросы показана на рис. 21.2.
Рис. 21.2. Каждая строка в таблице Answers содержит
один ответ покупателя на вопрос
Используя формулы DAX, мы можем, к примеру, ответить на следующий вопрос: «Скольким покупателям нравятся комиксы (cartoons) с
разбивкой по полу и профессии?» Взгляните на рис. 21.3 в качестве примера. Здесь итоговые цифры считаются неверно. Но об этом дальше.
Рис. 21.3. В каждой ячейке отражено количество покупателей,
предпочитающих тот или иной жанр кино, с группировкой
по полу и роду деятельности
364
 Глава 21. Опросник
В отчет включены два среза для выбора пересечений в представлении данных, а именно: в столбцах матрицы располагаются ответы на
вопросы, выбранные в срезе Question 1, тогда как в строках выводятся
ответы на вопросы, выбранные в срезе Question 2. Например, из всех
голосовавших женщин (Female) девять отметили в своих предпочтениях жанр комиксов (Cartoons).
Для реализации данного шаблона вам необходимо дважды загрузить
таблицу Questions. Так вы сможете задействовать два среза для вывода
вопросов. К тому же связи с копиями таблиц с вопросами должны присутствовать в модели данных как неактивные. Поскольку мы используем таблицы в качестве фильтров, назовем их соответствующе: Filter1 и
Filter2. Итоговая модель данных показана на рис. 21.4.
Рис. 21.4. Модель данных включает в себя неактивные связи
между таблицами Filter и Answers
Чтобы подсчитать количество респондентов, ответивших на первый (отфильтрованный при помощи Filter1) и второй (отфильтрованный при помощи Filter2) вопрос, можно использовать следующую
формулу.
Мера в таблице Customers:
CustomersQ1andQ2 :=
VAR CustomersQ1 =
CALCULATETABLE (
VALUES ( Answers[CustomerKey] );
USERELATIONSHIP ( Answers[AnswerKey]; Filter1[AnswerKey] )
)
VAR CustomersQ2 =
CALCULATETABLE (
VALUES ( Answers[CustomerKey] );
USERELATIONSHIP ( Answers[AnswerKey]; Filter2[AnswerKey] )
)
RETURN
CALCULATE (
Описание шаблона  365
COUNTROWS ( Customers );
CROSSFILTER ( Answers[CustomerKey]; Customers[CustomerKey]; BOTH );
CustomersQ1;
CustomersQ2
)
Формула активирует соответствующую связь между таблицами
при вычислении переменных CustomersQ1 и CustomersQ2. В дальнейшем эти переменные используются в качестве фильтров для таблицы Answers при фильтрации покупателей посредством модификатора
CROSSFILTER.
Вы можете производить абсолютно любые вычисления при помощи этой формулы при условии, что модификатор CROSSFILTER позволит таблице Answers фильтровать таблицу, на которой базируется ваш
код. Таким образом, вы можете заменить выражение COUNTROWS
(Customer) на любое другое, включающее таблицу Customers. Например, в мере RevenueQ1andQ2 вычисляется доход от каждого покупателя,
включенного в выборку. Единственным отличием этой меры от предыдущей является то, что она в своих расчетах ссылается на другую меру
Revenue Amount, а не на выражение COUNTROWS (Customer).
Мера в таблице Customers:
RevenueQ1andQ2 :=
VAR CustomersQ1 =
CALCULATETABLE (
VALUES ( Answers[CustomerKey] );
USERELATIONSHIP ( Answers[AnswerKey]; Filter1[AnswerKey] )
)
VAR CustomersQ2 =
CALCULATETABLE (
VALUES ( Answers[CustomerKey] );
USERELATIONSHIP ( Answers[AnswerKey]; Filter2[AnswerKey] )
)
RETURN
CALCULATE (
[Revenue Amount];
CROSSFILTER ( Answers[CustomerKey]; Customers[CustomerKey]; BOTH );
CustomersQ1;
CustomersQ2
)
Результат вычисления меры RevenueQ1andQ2 приведен на рис. 21.5.
366
 Глава 21. Опросник
Рис. 21.5. В каждой ячейке отчета показывается доход от покупателей
на пересечении их ответов на заданные вопросы
Если вас интересует лишь подсчет количества покупателей, вы можете упростить предыдущий код и ускорить его выполнение, применив
следующую его вариацию.
Мера в таблице Customers:
CustomersQ1andQ2optimized :=
VAR CustomersQ1 =
CALCULATETABLE (
VALUES ( Answers[CustomerKey] );
USERELATIONSHIP ( Answers[AnswerKey]; Filter1[AnswerKey] )
)
VAR CustomersQ2 =
CALCULATETABLE (
VALUES ( Answers[CustomerKey] );
USERELATIONSHIP ( Answers[AnswerKey]; Filter2[AnswerKey] )
)
RETURN
CALCULATE (
DISTINCTCOUNT ( Answers[CustomerKey] );
CustomersQ1;
CustomersQ2
)
При этом очень важно понимать, какая информация отображается в
каждой ячейке отчета. На рис. 21.6 мы поясним семантику отчета подробнее, используя для этого латинские буквы от A до E.
Описание шаблона  367
Рис. 21.6. В каждой ячейке вычисляются свои значения,
описание которых показано ниже
Таблица 21.1. Описание вычислений в каждой ячейке
A
Женщины (Female), предпочитающие комиксы (Cartoons)
B
Женщины (Female) или мужчины (Male), предпочитающие комиксы (Cartoons)
C
Женщины (Female) или мужчины (Male), или консультанты (Consultant), или айтишники (IT Pro), или преподаватели (Teacher), предпочитающие комиксы (Cartoons)
D
Женщины (Female) или мужчины (Male), предпочитающие комиксы (Cartoons),
комедии (Comedy) или ужасы (Horror)
E
Женщины (Female) или мужчины (Male), или консультанты (Consultant), или айтишники (IT Pro), или преподаватели (Teacher), предпочитающие комиксы (Cartoons),
комедии (Comedy) или ужасы (Horror)
В формуле используется условие И (AND) для выполнения пересечения между вопросами, выбранными в фильтрах Question 1 и Question 2,
тогда как условие ИЛИ (OR) применяется для ответа на один и тот же
вопрос. Помните, что условие ИЛИ означает «любую комбинацию» (не
путайте с исключающим ИЛИ) и ведет к образованию неаддитивных
вычислений в мере.
Глава
22
Анализ покупательской
корзины
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Шаблон анализа покупательской корзины базируется на частном случае
шаблона опросника, который мы рассматривали ранее. Целью подобного анализа является исследование связей между событиями. Один из
типичных примеров проведения такого анализа заключается в определении товаров, которые покупатель часто приобретает вместе. Это означает, что такие товары находятся в одной корзине, что и обусловило
выбор названия для шаблона.
Два товара считаются связанными, если они находятся в одной корзине. Иными словами, гранулярность события определяется конкретной
покупкой товара. Корзина может представлять собой как интуитивно понятные вещи вроде заказа на покупку, так и не столь очевидные – например, покупателя. В этом случае товары будут считаться связанными, если
они были приобретены одним покупателем, пусть и в разных заказах.
Поскольку данный шаблон напрямую связан с идентификацией наличия связей между товарами, в модели данных будет присутствовать
две копии одной и той же таблицы товаров. Мы назовем их Product и And
Product. Пользователь будет выбирать набор товаров в таблице Product,
а мера будет вычислять, с какой вероятностью товары из таблицы And
Product будут соотноситься с исходным выбором.
Мы также включили в рассматриваемый шаблон дополнительные
метрики ассоциативных правил (association rules), такие как поддержка (support), доверие (confidence) и лифт (lift). Эти меры помогут лучше
понять полученные результаты и сделать важные выводы из анализа.
Определение метрик ассоциативных правил  369
Определение метрик ассоциативных правил
Данный шаблон содержит несколько мер, которые мы подробно рассмотрим в этом разделе. Чтобы выработать определения, мы рассмотрим
заказы, содержащие как минимум один товар из группы Cameras and
camcorders и один – из категории Computers, как показано на рис. 22.1.
Рис. 22.1. Анализ заказов по товарам группы Cameras and camcorders,
содержащих также товары категории Computers
В отчете, показанном на рис. 22.1, используется два среза: срез Category
показывает выбор по столбцу Product[Category], а срез And Category – по
столбцу 'And Product'[And Category]. В мере # Orders отражено количество
заказов, содержащих как минимум один товар из категории Cameras
and camcorders, тогда как мера # Orders And отражает количество заказов с присутствием как минимум по одному товару из обеих категорий:
Cameras and camcorders и Computers. Позже мы поясним значение остальных мер. Здесь нужно сделать важное замечание о том, что, если поменять местами категории Cameras and camcorders и Computers, результаты
частично поменяются. Некоторые меры, такие как # Orders Both, % Orders
Support и Orders Lift, сохранят свои прежние значения, тогда как, например, метрика доверия (% Orders Confidence) изменится, поскольку напрямую зависит от порядка следования выборов. На рис. 22.2 вы можете
увидеть, как изменятся значения мер при обратном выборе категорий.
Рис. 22.2. Анализ заказов по товарам группы Computers,
содержащим также товары категории Cameras and camcorders
 Глава 22. Анализ покупательской корзины
370
Далее мы приведем описание всех мер, используемых в шаблоне. Все
эти меры могут быть представлены в двух вариантах: когда под корзиной мы понимаем заказ и покупателя. Например, описание меры # And
применимо как к # Orders And, так и к # Customers And.
#
Меры # Orders и # Customers возвращают количество уникальных
корзин в текущем контексте фильтра. На рис. 22.1 цифрой 2361
обозначено количество заказов, содержащих как минимум один
товар из категории Cameras and camcorders, тогда как на рис. 22.2
показано, что в 2933 заказах упоминаются товары из группы
Computers.
# And
Меры # Orders And и # Customers And возвращают количество уникальных корзин, содержащих как минимум одно упоминание товаров из категории, выбранной в списке And Product, в текущем
контексте фильтра. В данных мерах игнорируется выбор в списке
Product. По рис. 22.1 мы видим, что в 2933 заказах присутствует
как минимум один товар из категории Computers.
# Total
Меры # Orders Total и # Customers Total возвращают общее количество корзин с игнорированием всех фильтров по Product и And
Product. На обоих показанных рисунках общее количество корзин равно 21 601. Обратите внимание, что фильтр по And Product
игнорируется по умолчанию, поскольку связь не активна; единственный фильтр, который явным образом игнорируется в этой
мере, это фильтр по Product. Если бы у нас был еще фильтр по
таблице Date, в мере учитывались бы только корзины из текущего
временного интервала, но фильтр по Product продолжил бы игнорироваться.
# Both
Меры # Orders Both и # Customer Both возвращают количество
уникальных корзин, содержащих товары из обеих категорий,
выбранных в срезах. На рис. 22.1 показано, что ровно в 400 заказах одновременно присутствуют товары из групп Cameras and
camcorders и Computers.
% Support
Меры % Orders Support и % Customers Support возвращают показатель поддержки (support) ассоциативного правила. Этот показатель рассчитывается как отношение между мерами # Both и
# Total. На рис. 22.1 видно, что в 1,85 % всех заказов одновременно
присутствуют товары из групп Cameras and camcorders и Computers.
Простые отчеты  371
% Confidence
Меры % Orders Confidence и % Customers Confidence демонстрируют показатель доверия (confidence) ассоциативного правила. Доверие определяется как отношение между мерами # Both и #. На
рис. 22.1 видно, что из всех заказов, в которых присутствуют товары категории Cameras and camcorders, в 16,94 % также содержатся
и товары из группы Computers.
Lift
Меры Orders Lift и Customers Lift именуются лифтом (lift) и вычисляются как отношение показателя доверия к вероятности
присутствия товаров из выборки And Product в общем количестве
заказов:
.
Если показатель лифта превышает единицу, это означает, что ассоциативное правило достаточно хорошо подходит, чтобы на его основании
предсказывать события. И чем выше лифт, тем прочнее ассоциация. На
примере с рис. 22.1 видно, что этот показатель для ассоциативного правила между категориями Cameras and camcorders и Computers составляет 1,25 – эта цифра была получена путем деления % Confidence (16,94 %)
на частоту события # Orders And в общей массе заказов # Orders Total:
(2933/21601 = 13,58 %).
Простые отчеты
В данном разделе мы опишем несколько простых отчетов, созданных
применительно к нашей элементарной модели данных. Представленные здесь отчеты помогут вам лучше понять обсуждаемый шаблон.
В отчете на рис. 22.3 показан список товаров, которые с определенной вероятностью окажутся в корзине, если покупатель приобретет оптическую мышь Contoso Optical USB Mouse M45 White.
Как видите, вероятность того, что покупатель приобретет вместе
с указанной мышкой клавиатуру SV Keyboard E90 White, составляет 99,45 % (доверие). Показатель поддержки (% Orders Support) 3,33 %
здесь говорит о том, что заказы с комбинациями представленных здесь
товаров составляют лишь 3,33 % от общего количества заказов, равного 21 601, как мы выяснили из рис. 22.1. Высокий показатель лифта
(29,67) говорит о том, что ассоциативное правило, связывающее эти
два товара, действительно достаточно прочное.
372
 Глава 22. Анализ покупательской корзины
Рис. 22.3. Анализ покупательской корзины по товарам с группировкой
по категориям
В отчете, представленном на рис. 22.4, показаны пары товаров, которые с наибольшей вероятностью окажутся в одной корзине, с сортировкой по показателю доверия.
Рис. 22.4. Анализ покупательской корзины по товарам
В нашем наборе данных значения показателя доверия при обмене товаров позициями получились довольно близкими. Но так бывает не всегда. Взгляните на подсвеченные строки. Когда товар Contoso
USB Cable M250 White располагается в левой колонке, а SV 40GB USB2.0
Portable hard Disk e400 Silver в правой, показатель доверия оказывается
чуть ниже, чем если бы они размещались в обратном порядке. В реальных наборах данных эта разница зачастую оказывается больше. В то же
время показатели поддержки и лифта остаются неизменными, меняется только уровень доверия.
Базовый пример шаблона  373
В данном шаблоне в качестве корзины можно рассматривать целого
покупателя, а не отдельный заказ. В этом случае количество товаров в
корзинах может серьезно возрасти, и может появиться смысл производить анализ по целым категориям товаров вместо отдельных позиций.
На рис. 22.5 показаны ассоциации между категориями товаров в покупательской истории клиентов.
Рис. 22.5. Анализ покупательской корзины по категориям товаров
Люди, покупающие товары из категории Cell phones, с большой долей вероятности приобретут и что-то из группы Computers (показатель
доверия равен 48,19 %). В то же время лишь 12,74 % покупателей, остановивших свой выбор на товаре из категории Computers, также сделают
приобретение в отделе мобильных телефонов (Cell phones).
Базовый пример шаблона
Для начала необходимо создать копию таблицы Product, чтобы в отчете
пользователь мог выбирать товары из списка And Product. Это можно
сделать при помощи вычисляемой таблицы по следующей формуле.
Вычисляемая таблица:
And Product =
SELECTCOLUMNS (
'Product';
"And Category"; 'Product'[Category];
"And Subcategory"; 'Product'[Subcategory];
"And Product"; 'Product'[Product Name];
"And ProductKey"; 'Product'[ProductKey]
)
Между таблицами And Product и Sales должна быть установлена неактивная связь по полю Sales[ProductKey], которое также используется
для объединения таблиц Product и Sales. Связь таблиц And Product и Sales
должна быть неактивной, поскольку она будет использоваться только в
374
 Глава 22. Анализ покупательской корзины
мерах из этого шаблона и не должна оказывать влияния на остальные
меры в модели данных. На рис. 22.6 показана модель данных, состоящая из трех таблиц: Product, And Product и Sales.
Рис. 22.6. Связи между таблицами Product, And Product и Sales
Мы используем два типа корзины: по заказам и по покупателям. Заказы идентифицируются по полю Sales[Order Number], а покупатели –
по полю Sales[CustomerKey]. Здесь мы покажем лишь меры для заказов,
поскольку обновить их для покупателей очень легко – достаточно заменить Sales[Order Number] на Sales[CustomerKey]. Если вам интересно,
с мерами для покупателей можно ознакомиться в демонстрационных
файлах.
В первой мере мы подсчитаем количество уникальных заказов в текущем контексте фильтра.
Мера в таблице Sales:
# Orders :=
SUMX ( SUMMARIZE ( Sales; Sales[Order Number] ); 1 )
Перед тем как представить остальные меры в данном шаблоне, необходимо сделать небольшое отступление. Мера # Orders, по сути, может быть получена путем вызова функции DISTINCTCOUNT по полю
Sales[Order Number]. Мы же использовали альтернативную реализацию
из соображений гибкости и эффективности. Позвольте нам рассказать
об этом подробнее.
Как мы уже сказали, мера # Orders может быть записана с использованием функции DISTINCTCOUNT следующим образом:
Базовый пример шаблона  375
DISTINCTCOUNT ( Sales[Order Number] )
В языке DAX эта функция представляет собой сокращенный вариант
использования двух функций COUNTROWS и DISTINCT:
COUNTROWS ( DISTINCT ( Sales[Order Number] ) )
При этом функция DISTINCT может быть заменена на SUMMARIZE
следующим образом:
COUNTROWS ( SUMMARIZE ( Sales; Sales[Order Number] ) )
Последние три варианта написания формулы возвращают один и тот
же результат в отношении эффективности и итогового плана выполнения запроса. Использование функции SUMX вместо COUNTROWS даст
аналогичный результат:
SUMX ( SUMMARIZE ( Sales; Sales[Order Number] ); 1 )
Обычно замена COUNTROWS на SUMX ведет к снижению эффективности плана выполнения запроса. Но именно в специфике шаблона
анализа покупательской корзины эта альтернатива приводит к ускорению вычислений. Подробнее об этом можно почитать по адресу https://
sql.bi/432941.
Преимущество использования функции SUMMARIZE в том, что мы
можем изменить второй аргумент на столбец, представляющий логику
формирования покупательской корзины, даже если он находится в
другой таблице, объединенной посредством связи с таблицей Sales.
Например, мера, вычисляющая количество уникальных городов
покупателей, может быть написана так:
SUMX ( SUMMARIZE ( Sales; Customer[City] ); 1 )
В первом аргументе функции SUMMARIZE должна быть указана таблица, в которой хранятся транзакции, – например, Sales. Если во втором
376
 Глава 22. Анализ покупательской корзины
аргументе вы хотите обратиться к столбцу в таблице Customer, у вас нет
вариантов – вы должны использовать именно такое именование. Например, если вам необходимо построить отчет по городам покупателей,
вы должны указать столбец Customer[City]. В случае, если вы используете
столбец, по которому определяется связь между таблицами – к примеру, поле CustomerKey в таблице Customer, – то вы можете выбирать между
обращением к ней как к Sales[CustomerKey] или Customer[CustomerKey].
Всегда, когда это возможно, мы советуем использовать нотацию столбца
через таблицу Sales во избежание задействования связи. Именно по этой
причине вместо обращения к покупателю при формировании корзины
как к Customer[CustomerKey] мы используем поле Sales[CustomerKey]:
SUMX ( SUMMARIZE ( Sales; Sales[CustomerKey] ); 1 )
Теперь, когда мы пояснили, почему используем функцию
SUMMARIZE вместо DISTINCT для идентификации атрибута покупательской корзины, можно двигаться дальше и представить остальные
меры в шаблоне.
В мере # Orders And вычисляется количество заказов с использованием выборки And Product. В данной формуле активируется связь между
таблицами Sales и And Product.
Мера в таблице Sales:
# Orders And :=
CALCULATE (
[# Orders];
REMOVEFILTERS ( 'Product' );
USERELATIONSHIP ( Sales[ProductKey]; 'And Product'[And ProductKey] )
)
Мера # Orders Total возвращает количество заказов, игнорируя все
фильтры по таблице Product.
Мера в таблице Sales:
# Orders Total :=
CALCULATE (
[# Orders];
REMOVEFILTERS ( 'Product' )
)
Базовый пример шаблона  377
Скрытая мера # Orders Both (внутренняя) служит для подсчета количества заказов, включающих как минимум один элемент из таблицы
Product и один из таблицы And Product.
Мера в таблице Sales:
# Orders Both (Internal) :=
VAR OrdersWithAndProducts =
CALCULATETABLE (
SUMMARIZE ( Sales; Sales[Order Number] );
REMOVEFILTERS ( 'Product' );
REMOVEFILTERS ( Sales[ProductKey] );
USERELATIONSHIP ( Sales[ProductKey]; 'And Product'[And ProductKey] )
)
VAR Result =
CALCULATE (
[# Orders];
KEEPFILTERS ( OrdersWithAndProducts )
)
RETURN
Result
Скрытая мера пригодится нам для вычисления меры # Orders Both,
а также мер в разделе с оптимизированной версией шаблона. В мере
# Orders Both добавлена проверка с возвращением пустого значения в
случае, если пересечение выборок Product и And Product содержит как
минимум одно пересечение по товарам. Это необходимо для того, чтобы в отчете не проводились ассоциации товара с самим собой.
Мера в таблице Sales:
# Orders Both :=
IF (
ISEMPTY (
INTERSECT (
DISTINCT ( 'Product'[ProductKey] );
DISTINCT ( 'And Product'[And ProductKey] )
)
);
[# Orders Both (Internal)]
)
Мера % Orders Support вычисляется как отношение # Orders Both к
# Orders Total.
378
 Глава 22. Анализ покупательской корзины
Мера в таблице Sales:
% Orders Support :=
DIVIDE ( [# Orders Both]; [# Orders Total] )
Мера % Orders Confidence получается путем деления # Orders Both на
# Orders.
Мера в таблице Sales:
% Orders Confidence :=
DIVIDE ( [# Orders Both]; [# Orders] )
Значение меры Orders Lift является результатом деления меры
% Orders Confidence на отношение # Orders And к # Orders Total, как было
показано в формуле, которую мы приводили ранее:
.
Мера в таблице Sales:
Orders Lift :=
DIVIDE (
[% Orders Confidence];
DIVIDE (
[# Orders And];
[# Orders Total]
)
)
Формулы, показанные в данном разделе, прекрасно работают в моделях данных с количеством товаров, не превышающим нескольких
тысяч. В более объемных моделях возможно возникновение проблем
с быстродействием. В следующем разделе мы рассмотрим оптимизированный подход к решению нашей задачи, который, однако, требует
в обмен на скорость создания дополнительных вычисляемых таблиц и
связей.
Оптимизированный пример шаблона  379
Оптимизированный пример шаблона
В улучшенной версии нашего шаблона мы снизим нагрузку на движок, связанную с динамическим поиском лучших комбинаций товаров. Повышения эффективности запроса мы добьемся путем создания
вычисляемых таблиц с предварительно рассчитанными сочетаниями
товаров в доступных корзинах. А поскольку мы можем рассматривать
в качестве корзин как заказы, так и покупателей, создадим две вычисляемые таблицы, связанные с таблицей Product, как показано на
рис. 22.7.
Рис. 22.7. Отношения между таблицами Product, RawProductsCustomers,
RawProductsOrders, And Product и Sales
Таблицы RawProductsOrders и RawProductsCustomers содержат в каждой строке комбинацию из двух ключей товаров вместе с количеством
корзин, содержащих оба товара. Строки с одинаковыми товарами при
этом исключаются.
380
 Глава 22. Анализ покупательской корзины
Вычисляемая таблица:
RawProductsOrders =
FILTER (
SUMMARIZECOLUMNS (
'Sales'[ProductKey];
'And Product'[And ProductKey];
"Orders"; [# Orders Both (Internal)]
);
NOT ISBLANK ( [Orders] ) && 'And Product'[And ProductKey]
<> 'Sales'[ProductKey]
)
Вычисляемая таблица:
RawProductsCustomers =
FILTER (
SUMMARIZECOLUMNS (
'Sales'[ProductKey];
'And Product'[And ProductKey];
"Customers"; [# Customers Both (Internal)]
);
NOT ISBLANK ( [Customers] ) && 'And Product'[And ProductKey]
<> 'Sales'[ProductKey]
)
Фильтр из таблицы Product автоматически распространяется на обе
таблицы RawProducts. При этом в мере # Orders Both только фильтр из
таблицы And Product должен быть распространен посредством выражения DAX. Фактически в этой вариации шаблона только мера # Orders
Both отличается от своих аналогов в базовом шаблоне.
Мера в таблице Sales:
# Orders Both :=
VAR ExistingAndProductKey =
CALCULATETABLE (
DISTINCT ( RawProductsOrders[And ProductKey] );
TREATAS (
DISTINCT ( 'And Product'[And ProductKey] );
RawProductsOrders[And ProductKey]
)
)
VAR FilterAndProducts =
TREATAS (
EXCEPT (
Оптимизированный пример шаблона  381
ExistingAndProductKey;
DISTINCT ( 'Product'[ProductKey] )
);
Sales[ProductKey]
)
VAR OrdersWithAndProducts =
CALCULATETABLE (
SUMMARIZE ( Sales; Sales[Order Number] );
REMOVEFILTERS ( 'Product' );
FilterAndProducts
)
VAR Result =
CALCULATE (
[# Orders];
KEEPFILTERS ( OrdersWithAndProducts )
)
RETURN
Result
В мере # Orders Both нельзя использовать реализацию внутренней
меры # Orders Both (Internal) по причине того, как в ней применяются
фильтры. С таблицы And Product фильтр распространяется на таблицу
RawProductsOrders, а затем на Sales для извлечения заказов, включающих любой из товаров в таблице Any Product. Это довольно сложная техника, но она позволяет снизить нагрузку на движок формул и увеличить
скорость выполнения запроса.
Глава
23
Пересчет курсов валют
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
Необходимость пересчета курсов валют в сценариях практически всегда приводит к сложностям, включая структуру модели данных и сам код
на DAX. Существует два типа валют применительно к отчетной системе:
валюта, в которой заполняются документы, и валюта, в которой формируется отчетность. Вы можете заводить заказы в систему в разных
валютах, а в отчетах видеть все цифры в одной валюте, чтобы иметь
возможность сопоставлять разные показатели. Также вы имеете право
формировать заказы в единой валюте, а отчеты строить в разных. Наконец, вы можете как вводить документы, так и формировать отчеты в
разных валютах.
В данном шаблоне мы рассмотрим три различных сценария, в которых для простоты будем оперировать лишь двумя валютами: евро (EUR)
и долларом (USD):
• много источников, одна цель: заказы вводятся в систему в двух
валютах (EUR и USD), а отчеты формируются только в USD;
• один источник, много целей: заказы вводятся в систему только
в USD, а отчеты формируются в двух валютах (EUR и USD);
• много источников, много целей: заказы вводятся в систему в
двух валютах (EUR и USD), и отчеты также могут формироваться в
двух валютах (EUR и USD).
Формулы в данном шаблоне зависят от конкретной реализации таблицы с курсами валют. Часто требуется хранить курсы валют на каждый
день. А иногда бывает вполне допустимо выполнять пересчет курсов
Несколько валют источника, одна валюта отчета  383
валют на другом уровне гранулярности, например за месяц. Различия в
способах обращения с такими сценариями незначительны, и мы обратим ваше внимание на них, когда будем обсуждать код DAX.
В качестве демонстрации мы создали модели данных с ежедневными
и ежемесячными пересчетами курсов валют. При этом формулы и модели располагаются в одном демонстрационном файле, и вам просто
нужно использовать один из двух уровней гранулярности курсов валют
для конкретной реализации.
Мы создали ежедневные таблицы пересчета курсов валют на основе
данных, имеющихся в модели Contoso. Так что все курсы валют у нас
будут вымышленными. Единственная наша цель – показать вам принципы и технику работы с валютами.
Несколько валют источника, одна валюта
отчета
В данном сценарии в источнике данных содержатся заказы, оформленные в разных валютах, тогда как при формировании отчета все цифры
пересчитываются в валюту отчета. Например, ваши заказы могут быть
заведены в евро или долларах, а в отчете вы всегда будете видеть цифры
исключительно в долларах.
Первое, что нам предстоит проанализировать, это модель данных,
представленную на рис. 23.1.
Рис. 23.1. В модели данных видно, что таблицы Sales и Daily Exchange Rates
объединены посредством таблиц Date и Currency
В таблице Sales хранятся транзакции в местной валюте – именно в ней
занесены все данные в денежных полях, таких как Net Price, Unit Price и
Unit Discount. Таблица Sales объединена связью с таблицей Currency, в
которой хранятся валюты транзакций.
384
 Глава 23. Пересчет курсов валют
Простая мера, вычисляющая сумму продажи, будет корректно работать только со срезом по валюте источника. Фактически мы не имеем
возможности агрегировать значения в разных валютах, не выполнив
предварительно пересчет курсов. Именно поэтому мы назвали меру,
выполняющую эти вычисления, Sales (Internal), т. е. «внутренняя», и
вдобавок скрыли ее от пользователя:
Sales (Internal) := SUMX ( Sales; Sales[Quantity] * Sales[Net Price] )
Как видно в отчете, представленном на рис. 23.2, агрегирование меры
Sales (Internal) ведет к полной бессмыслице, поскольку исходные значения хранятся в разных валютах. В то же время меры Sales USD (Monthly)
и Sales USD (Daily) производят весьма осмысленный результат, поскольку предварительно конвертируют значение меры Sales (Internal) в доллары. Разница в значениях между столбцами Sales USD (Monthly) и Sales
USD (Daily) обусловлена колебаниями курсов валют в течение месяца.
Рис. 23.2. Агрегирование по полю Sales (Internal) бессмысленно
Чтобы корректно провести пересчет курсов валют, мы агрегируем
поле Sales (Internal) на уровне гранулярности курса для каждой валюты,
после чего выполняем конверсию. Например, в мере Sales USD (Daily)
реализовано вычисление на уровне гранулярности по дням путем
итераций функцией SUMX по таблице, в которой каждая строка представляет дату и валюту.
Мера в таблице Sales:
Sales USD (Daily) :=
VAR AggregatedSalesInCurrency =
ADDCOLUMNS (
SUMMARIZE (
Sales;
Несколько валют источника, одна валюта отчета  385
'Date'[Date];
'Currency'[Currency]
-- Гранулярность по дням
);
"@SalesAmountInCurrency"; [Sales (Internal)];
"@Rate"; CALCULATE (
SELECTEDVALUE ( 'Daily Exchange Rates'[Rate] )
)
)
VAR Result =
SUMX (
AggregatedSalesInCurrency;
[@SalesAmountInCurrency] / [@Rate]
)
RETURN
Result
С целью повышения эффективности выполнения расчетов крайне
важно снизить количество итераций для извлечения курса валюты.
Осуществлять поиск курса для каждой транзакции было бы довольно
накладно и излишне, поскольку для всех транзакций, произведенных
в один день, курс будет одинаковым. Вызов функции SUMMARIZE по
таблице Sales позволяет существенно снизить гранулярность всей формулы. Если курсы валют хранятся помесячно, уровень гранулярности
должен быть снижен до месяца, как в случае с мерой Sales USD (Monthly).
Мера в таблице Sales:
Sales USD (Monthly) :=
VAR AggregatedSalesInCurrency =
ADDCOLUMNS (
SUMMARIZE (
Sales;
'Date'[Calendar Year Month]; -- Гранулярность по месяцам
'Currency'[Currency]
);
"@SalesAmountInCurrency"; [Sales (Internal)];
"@Rate"; CALCULATE (
SELECTEDVALUE ( 'Monthly Exchange Rates'[Rate] )
)
)
VAR Result =
SUMX (
AggregatedSalesInCurrency;
[@SalesAmountInCurrency] / [@Rate]
)
RETURN
Result
386
 Глава 23. Пересчет курсов валют
Меры, представленные в этом разделе, не выполняют проверку на
присутствие актуального курса валюты в таблице, поскольку мы производим операцию деления, которая вернет ошибку деления на ноль
при отсутствии курса. Другой способ заключается в использовании условного выражения и выводе ошибки в случае отсутствия курса валюты – его мы опробуем в следующих примерах. Вы должны использовать
один из двух вариантов с генерированием ошибки, в противном случае отчет будет показывать неправильные цифры, даже не известив об
этом пользователя.
Одна валюта источника, несколько валют
отчета
В данном сценарии в источнике данных будут храниться документы
исключительно в одной валюте (в нашем случае в долларах), а при формировании отчета у пользователя будет возможность выбрать любую
валюту, в которой он хочет видеть сводные цифры. Суммы продаж из
заказов при этом будут конвертироваться с учетом курса выбранной
валюты на дату документа.
В модели данных, представленной на рис. 23.3, нет прямой связи
между таблицами Sales и Currency. Все транзакции содержатся в базе в
единой валюте (USD), а таблица Currency служит для того, чтобы дать
пользователю возможность выбрать требуемую валюту отчета.
Рис. 23.3. Между таблицами Sales и Currency нет прямой связи
Одна валюта источника, несколько валют отчета  387
Пользователь может выбрать валюту формирования отчета посредством среза, а может задействовать столбец Currency[Currency] в матрице,
как показано на рис. 23.4. В этом случае пересчет курсов происходит на
месячной основе.
Рис. 23.4. Продажи по товарам в разных валютах
Структурно формула для получения желаемого результата будет
очень похожа на ту, что мы использовали в предыдущем примере, хотя
ее реализация существенно отличается из-за различий в моделях данных. В мере Sales (Daily) применяются разные курсы валют на ежедневной основе.
Мера в таблице Sales:
Sales (Daily) :=
IF (
HASONEVALUE ( 'Currency'[Currency] );
VAR AggregatedSalesInUSD =
ADDCOLUMNS (
SUMMARIZE (
Sales;
'Date'[Date]
-- Гранулярность на уровне дней
);
"@Rate"; CALCULATE ( SELECTEDVALUE (
'Daily Exchange Rates'[Rate] ) );
"@USDSalesAmount"; [Sales (internal)]
)
VAR Result =
SUMX (
AggregatedSalesInUSD;
IF (
NOT ( ISBLANK ( [@Rate] ) );
[@USDSalesAmount] * [@Rate];
ERROR ( "Missing conversion rate" )
388
 Глава 23. Пересчет курсов валют
)
)
RETURN
Result
)
Проверка при помощи функции HASONEVALUE позволяет гарантировать, что в текущем контексте фильтра видима лишь одна валюта.
В переменной AggregatedSalesInUSD хранится таблица с суммами продаж в долларах и соответствующими курсами валют с гранулярностью
по дням. В столбце @Rate извлекается актуальный курс благодаря наличию фильтра по столбцу Currency[Currency] и преобразованию контекста из Date[Date], агрегированной при помощи функции SUMMARIZE.
В переменной Result сводится итоговый результат путем суммирования произведений полей @Rate и @USDSalesAmount и выводом ошибки, если значение @Rate недоступно. В этом случае формирование отчета прерывается ошибкой с вашим собственным описанием: «Missing
conversion rate» (Отсутствует курс пересчета валюты).
Если курсы валют доступны только на уровне месяцев, можно воспользоваться мерой Sales (Monthly), отличающейся от Sales (Daily) лишь
аргументом функции SUMMARIZE.
Мера в таблице Sales:
Sales (Monthly) :=
IF (
HASONEVALUE ( 'Currency'[Currency] );
VAR AggregatedSalesInUSD =
ADDCOLUMNS (
SUMMARIZE (
Sales;
-- Гранулярность на
'Date'[Calendar Year Month Number]
-- уровне месяцев
);
"@Rate"; CALCULATE ( SELECTEDVALUE (
'Monthly Exchange Rates'[Rate] ) );
"@USDSalesAmount"; [Sales (internal)]
)
VAR Result =
SUMX (
AggregatedSalesInUSD;
IF (
NOT ( ISBLANK ( [@Rate] ) );
[@USDSalesAmount] * [@Rate];
ERROR ( "Missing conversion rate" )
)
)
Несколько валют источника, несколько валют отчета  389
RETURN
Result
)
Несколько валют источника, несколько валют
отчета
Этот сценарий является комбинацией двух предыдущих. В источнике
данных хранятся документы, заведенные в разных валютах, и при формировании отчета пользователь посредством среза также может выбрать любую валюту. При этом пересчет курсов валют выполняется на
дату документа.
В модели данных у нас будет присутствовать две таблицы валют:
Source Currency и Target Currency. Первая из них напрямую связана с таблицей Sales и служит для хранения валют транзакций. Таблица Target
Currency позволяет пользователю выбрать желаемую валюту при формировании отчета. Модель данных показана на рис. 23.5.
Рис. 23.5. В модели данных есть две таблицы валют: валюты источника
и валюты отчета
Такая модель позволяет пользователю выполнить пересчет любой
валюты транзакции в любую валюту источника. На рис. 23.6 показан
отчет в нескольких валютах по разным странам с учетом курсов валют
390
 Глава 23. Пересчет курсов валют
по месяцам. В отчете исходные суммы транзакций пересчитываются в
валюты, расположенные в столбцах матрицы.
Рис. 23.6. Любая валюта источника конвертируется в любую валюту отчета
Формула меры, как и модель данных, представляет собой комбинацию двух предыдущих формул. Функция HASONEVALUE проверяет, что выбрана единственная целевая валюта. В переменной
AggregatedSalesInCurrency хранится таблица с суммами продаж, агрегированными на требуемом уровне гранулярности курсов валют, включая
также валюту источника. В столбце @Rate извлекается нужный нам курс
валюты благодаря наличию фильтра по столбцу 'Target Currency'[Currency]
и преобразованию контекста из Date[Date] и 'Source Currency'[Currency],
агрегированных функцией SUMMARIZE. В переменной Result сводится
итоговый результат путем суммирования произведений полей @Rate и
@SalesAmount и выводом ошибки, если значение @Rate недоступно.
Мера в таблице Sales:
Sales (Daily) :=
IF (
HASONEVALUE ( 'Target Currency'[Currency] );
VAR AggregatedSalesInCurrency =
ADDCOLUMNS (
SUMMARIZE (
Sales;
'Date'[Date];
-- Гранулярность на уровне дней
'Source Currency'[Currency]
);
"@SalesAmount"; [Sales (Internal)];
"@Rate"; CALCULATE ( SELECTEDVALUE (
'Daily Exchange Rates'[Rate] ) )
Несколько валют источника, несколько валют отчета  391
)
VAR Result =
SUMX (
AggregatedSalesInCurrency;
IF (
NOT ( ISBLANK ( [@Rate] ) );
[@SalesAmount] * [@Rate];
ERROR ( "Missing conversion rate" )
)
)
RETURN
Result
)
Как и в предыдущем примере, важно использовать правильный уровень гранулярности при вычислении мер. Если курсы валют доступны
только по месяцам, следует воспользоваться мерой Sales (Monthly), отличающейся от Sales (Daily) лишь аргументом функции SUMMARIZE.
Мера в таблице Sales:
Sales (Monthly) :=
IF (
HASONEVALUE ( 'Target Currency'[Currency] );
VAR AggregatedSalesInCurrency =
ADDCOLUMNS (
SUMMARIZE (
Sales;
'Date'[Calendar Year Month];
-- Гранулярность на уровне
'Source Currency'[Currency]
-- месяцев
);
"@SalesAmount"; [Sales (Internal)];
"@Rate"; CALCULATE ( SELECTEDVALUE (
'Monthly Exchange Rates'[Rate] ) )
)
VAR Result =
SUMX (
AggregatedSalesInCurrency;
IF (
NOT ( ISBLANK ( [@Rate] ) );
[@SalesAmount] * [@Rate];
ERROR ( "Missing conversion rate" )
)
)
RETURN
Result
)
Глава
24
Прогнозирование
Загрузите примеры на сайте издательства по ссылке:
https://dl.dmkpress.com/978-5-97060-909-5.zip.
В данном шаблоне мы представим вашему вниманию несколько техник, которые могут быть использованы в сфере финансового планирования. Но этой областью применение показанного шаблона отнюдь не
ограничивается, поскольку сценарий прогнозирования мы используем
для демонстрации примера размещения мер на разных уровнях гранулярности и комбинирования на одной диаграмме мер из разных таблиц
с разными уровнями гранулярности.
Стоит помнить, что у каждой компании может быть свой подход к
формированию прогноза. Так что здесь мы представим лишь базовые
принципы того, как можно подходить к этому вопросу. Разумеется, вам
придется самим адаптировать меры и использованные в данной главе
техники под собственные нужды.
Введение
В исходной таблице, используемой для формирования прогноза, располагаются суммы по продажам с определенным уровнем гранулярности.
В нашем случае данные хранятся по суммам продажам в разрезе стран
магазинов, категорий товаров и годов. При этом у нас есть три типа сценария прогнозирования: Low (низкий), Medium (средний) и High (высокий). На рис. 24.1 показан весь набор данных.
На основании имеющегося набора данных выдвинем следующие требования:
• создание прогноза на разных уровнях гранулярности. Например,
мы можем построить месячный прогноз на основании прошлогодних продаж;
Модель данных  393
• сочетание актуальных и прогнозных данных в одном отчете с
использованием фактических значений по предшествующим
месяцам и прогнозных – по следующим месяцам в текущем году;
• корректировка прогнозов на будущие месяцы с учетом того, насколько далеки от фактических значений они оказались в предшествующих месяцах.
В то же время необходимо помнить о новых товарах, которые могут
появиться в ассортименте в течение лет, и вышедших из обращения,
для которых строить прогнозы без надобности. Для этих целей мы будем использовать таблицу Override, в которой будет содержаться информация о введении товара в эксплуатацию с прогнозом по продажам на
первый год и выводе его из обращения. Распределение новых товаров
по странам магазинов должно базироваться на продажах других товаров за предыдущие периоды.
Рис. 24.1. Прогнозный набор данных содержит информацию по странам,
категориям товаров и годам
Модель данных
Перед тем как углубляться в детали вычислений, важно сделать несколько уточнений по поводу модели данных.
В рассматриваемом сценарии используется нисходящий тип прогнозирования. Таким образом, в нашем источнике данных содержится прогноз по продажам для различных сценариев с низкой гранулярностью.
 Глава 24. Прогнозирование
394
Это означает, что информация представлена на низком уровне детализации: по годам, странам и категориям товаров. Данными по конкретным товарам, месяцам или магазинам мы не располагаем. В связи с
этим таблица Forecast в нашей модели данных будет объединена с соответствующими измерениями при помощи слабых связей типа «многие
ко многим» (Many-Many-Relationships – MMR), а с таблицей Scenario –
посредством связи «один ко многим» (Single-Many-Relationship – SMR),
что показано на рис. 24.2.
Рис. 24.2. Связи между таблицей Forecast и измерениями,
за исключением Scenario, – слабые
Объединение таблиц выполнено по следующим полям:
•
•
•
•
с таблицей Store по полю CountryRegion («многие ко многим»);
с таблицей Product по полю Category («многие ко многим»);
с таблицей Date по полю Year («многие ко многим»);
с таблицей Scenario по полю Scenario («один ко многим»).
Все связи типа «многие ко многим» являются слабыми. Они осуществляют фильтрацию лишь на уровне гранулярности связи. На уровне с
Модель данных  395
большей детализацией они просто повторяют итоги с поддерживаемого уровня.
Примечание. Мы используем термины MMR и SMR, чтобы избежать путаницы
с другими определениями связей типа «многие ко многим». Полное определение связей MMR и SMR в Power BI можно найти по адресу https://sql.bi/649118.
В данном случае реализуется наиболее эффективная концепция максимально возможного задействования измерений для осуществления
фильтрации и срезов вместо использования для этой цели столбцов из
таблицы фактов (в нашем случае из Forecast).
В качестве источника прогнозной информации мы будем использовать файл Excel. Этот же файл включает в себя и таблицу Override с информацией о новых и вышедших из оборота товарах. Для этих целей в
таблице Override присутствуют следующие столбцы:
• Year New: год введения в обращение нового товара;
• Year Del: год, когда товар был (или будет) снят из обращения;
• Amount: прогнозные продажи товара по всем странам за первый
год.
Поскольку таблица Override обладает такой же гранулярностью, как
Product, мы использовали инструмент Power Query для объединения
этих трех столбцов прямо в таблице Product. На рис. 24.3 показано содержимое этих трех столбцов, импортированное в таблицу Product из
таблицы Override.
Рис. 24.3. Для каждого нового или исключенного из оборота товара
соответствующая информация сохраняется прямо в таблице Product
Наша модель данных может быть не самой оптимальной. Мы разработали ее для того, чтобы продемонстрировать вам специфический
код DAX. Для других требований может лучше подойти другая модель.
В этом случае вам придется обновить все вычисления, которые мы покажем в данной главе.
396
 Глава 24. Прогнозирование
Бизнес-требования
Как и с любой моделью данных, нам необходимо определиться с бизнес-аспектами для написания корректного кода на DAX. В следующих
разделах мы обсудим некоторые бизнес-правила, реализованные в
данном шаблоне.
Прогноз на основании предыдущего года
Когда нам необходимо построить прогноз, мы рассматриваем прошлогодние цифры продаж в качестве соотносительного коэффициента.
Иными словами, чтобы подсчитать прогноз по конкретной подкатегории товаров, мы разбиваем цифры прогноза, полученные для соответствующей ей группы товаров, в процентном отношении в соответствии с
прошлогодними продажами.
Этот принцип показан на рис. 24.4.
Рис. 24.4. Прогноз по подкатегориям товаров строится
на основании предыдущего года
Таким образом, если по товару не было продаж за предыдущий год,
в отчете по нему будут нули.
Вышедшие из оборота товары не учитываются в прогнозе
Если товар был выведен из обращения, в прогнозе на следующий год
по нему будут стоять нули. Соответственно, и в распределении процен-
Бизнес-требования  397
тов он участвовать не должен. Если не учесть этот принцип, цифры в
прогнозе могут вас немало удивить.
Представьте, что было бы, если бы в подкатегории Cell Phones
Accessories все товары были исключены из обращения. В США их доля в
соответствующей категории составляет 26 %, как видно по рис. 24.4. Поскольку этих товаров больше нет, в следующем году прогноз не будет их
включать. Но в формуле распределения процентов должен быть учтен
этот факт, чтобы по другим подкатегориям из этой группы проценты
стали выше. Если этого не сделать, в сумме новый прогноз будет составлять 74, а не 100 %, так что наши 26 % будут потеряны.
Таким образом, если товары подкатегории не участвуют в обращении
в заданный год, их процент по продажам за предшествующий год не
должен влиять на распределение прогнозов остальных подгрупп.
У новых товаров есть свой прогноз
Это скорее не бизнес-требование, а осознанный выбор для упрощения модели данных. Если товар представлен как новый, значит, продажи по нему в предыдущем году будут нулевые. А как мы заявляли
раньше, в этом случае и прогноз на следующий год по этому товару
останется пустым.
Поэтому каждому новому товару мы приписываем определенный
прогноз на тот год, когда он был введен. Это значение, полученное в зависимости от распределения продаж по другим товарам в тех же странах.
Как и с другими требованиями, это не обязательно должен быть лучший выбор. Но мы вынуждены идти на компромиссы, чтобы писать рабочий код. В других случаях мы могли бы создать таблицу, содержащую
более детализированные прогнозы, а быть может, нам вовсе не пришлось бы с этим ничего делать. Так что рассматривайте этот пункт всего лишь как рекомендацию, а не правило.
Товары добавляются и удаляются на ежегодной основе
Для упрощения модели данных мы ввели одно искусственное ограничение, заключающееся в том, что новый товар может быть введен
в обращение только с начала года, т. е. в продажу он поступает в январе, а вывод товаров из обращения производится в конце года, а значит,
в следующем году продаж по нему быть не должно.
Если пренебречь этим правилом и позволить вводить и выводить
товары из обращения в любой момент года, сценарий существенно
усложнится. Мы решили не идти на такие сложности, чтобы больше
внимания уделить самому алгоритму. При наличии особых бизнес-требований вам, разумеется, придется вносить в формулы коррективы.
398
 Глава 24. Прогнозирование
Прогнозирование
Процесс прогнозирования (forecast) заключается в анализе продаж за
предыдущий год, выявлении соотношений и установке аналогичных
процентов распределения продаж в выбранном периоде. Формула состоит из двух частей: распределения прогнозных величин и вычисления значений для новых товаров.
На рис. 24.5 представлены две меры: Forecast Amount и % Sales PY (BG).
Мера % Sales PY (BG) представляет собой процент распределения на
уровне гранулярности прогноза, который внутренне рассчитывается в
мере Forecast Amount. В сопутствующем файле также присутствует отдельное определение меры % Sales PY (BG) для демонстрации промежуточных вычислений, не относящихся к шаблону.
Рис. 24.5. Цифры распределены в соответствии с процентами по продажам
В отчете выбраны две категории товаров: Cell phones и Clothes. При
этом Clothes – это новая категория, которая не была представлена в модели в прошлом году. Полный прогноз для каждой страны (на рис. 24.5
видны только Китай и Германия) включает в себя распределенный
прогноз по выбранной стране плюс сумму, назначенную новым товарам посредством столбца Forecast New, импортированного из столбца
Amount в таблице Override нашего файла Excel.
Наша модель данных спроектирована таким образом, что для каждого товара хранится одно прогнозное значение суммы продаж за год.
Это значение должно быть распределено между всеми странами, основываясь на продажах за предыдущий год по этим странам, как показано
Прогнозирование  399
в мере % Sales PY by Store на рис. 24.6. На этот раз распределение было
произведено только по странам, другие столбцы не участвуют.
Рис. 24.6. Цифры показывают относительное распределение
прогноза по странам
Представляем формулу для меры Forecast Amount.
Мера в таблице Forecast:
Forecast Amount :=
IF (
HASONEVALUE( 'Date'[Calendar Year Number] );
VAR SelectedScenario = SELECTEDVALUE ( Scenario[Scenario]; "Medium" )
VAR Categories = VALUES ( 'Product'[Category] )
VAR Countries = VALUES ( Store[CountryRegion] )
VAR CurrentYear = VALUES ( 'Date'[Calendar Year Number] )
-----VAR
Здесь мы рассчитываем суммы продаж за прошлый год на уровне
гранулярности прогноза, то есть Category, CountryRegion и Year.
Мы делаем это, удаляя все фильтры по таблицам гранулярности
и затем восстанавливая фильтры, соответствующие гранулярности
прогноза
PYSalesAmountAtGrain =
CALCULATE (
[Sales PY];
REMOVEFILTERS ( 'Product' );
REMOVEFILTERS ( 'Store' );
REMOVEFILTERS ( 'Date' );
Categories;
Countries;
CurrentYear;
KEEPFILTERS ( 'Product'[Year Del] < CurrentYear )
)
-- Никаких сложностей с вычислением прогноза, ведь он уже на
-- гранулярности Category, CountryRegion и Year, несмотря на все фильтры
VAR CurrentForecastedAmount =
CALCULATE (
SUM ( Forecast[Forecast] );
Scenario[Scenario] = SelectedScenario
)
VAR CategoriesCountries =
Powered by TCPDF (www.tcpdf.org)
400
 Глава 24. Прогнозирование
CROSSJOIN ( Categories; Countries )
Чтобы вычислить прогноз, проходим по категориям товаров и странам
(один год всегда выбран, так что по нему проходить не надо),
вычисляем сумму продаж на уровне Category/Country.
Затем делим результат на сумму продаж на уровне гранулярности
прогноза, чтобы получить процент распределения прогноза.
Фильтр по Product[Year Del] гарантирует, что товары, выведенные
до текущего года, не будут учтены, поскольку у них не будет
прогнозной суммы
ForecastValue =
CALCULATE (
SUMX (
KEEPFILTERS ( CategoriesCountries );
VAR PYSalesAmount = [Sales PY]
VAR AllocationFactor = DIVIDE ( PYSalesAmount;
PYSalesAmountAtGrain )
RETURN AllocationFactor * CurrentForecastedAmount
);
KEEPFILTERS ( 'Product'[Year Del] < CurrentYear )
)
-- Теперь определяем сумму по новым товарам, которая должна быть
-- распределена по странам с учетом продаж любых товаров за тот же
-- период в прошлом году
VAR NewProductsAmount =
CALCULATE (
SUM ( 'Product'[Forecast New] );
KEEPFILTERS ( 'Product'[Year New] = CurrentYear )
)
--------VAR
----VAR
Рассчитываем прошлогодние продажи на уровне гранулярности прогноза
вне зависимости от товара и страны, поскольку прогноз для нового
товара указывается как число, представляющее сумму продаж по всем
странам
PYSalesAmountAtGrainAnyProduct =
CALCULATE (
[Sales PY];
REMOVEFILTERS ( 'Product' );
REMOVEFILTERS ( 'Store' );
REMOVEFILTERS ( 'Date' );
CurrentYear
)
---VAR
Подобно тому, как мы делали с прогнозом, рассчитаем и это значение.
На этот раз проходим только по Countries, поскольку один год у нас
есть, а категории товаров не должны быть частью итераций
NewValue =
SUMX (
KEEPFILTERS ( Countries );
VAR PYSalesAmountAnyProduct =
CALCULATE (
[Sales PY];
Фактические данные и прогноз на одной диаграмме  401
REMOVEFILTERS ( 'Product' )
)
VAR AllocationFactor =
DIVIDE ( PYSalesAmountAnyProduct;
PYSalesAmountAtGrainAnyProduct
RETURN
AllocationFactor * NewProductsAmount
)
)
--- Результат – это сумма прогноза и значения для новых товаров,
-- распределенных согласно различным бизнес-требованиям
-VAR Result =
ForecastValue + NewValue
RETURN
Result
)
Формула работает с одним выбранным годом, но также корректно
функционирует и с уменьшенным набором дат в рамках одного года.
Кроме того, применительно к мере Forecast Amount можно выполнять
вычисления, связанные с логикой операций со временем, например
определить нарастающий итог с начала года следующим образом:
Мера в таблице Forecast:
YTD Forecast :=
CALCULATE (
[Forecast Amount];
DATESYTD ( 'Date'[Date] )
)
Фактические данные и прогноз на одной
диаграмме
Часто необходимо вывести на одной диаграмме одновременно прогнозные данные и фактические. Подобное требование может быть реализовано, например, так, как показано на рис. 24.7, что не очень удобно.
Мера YTD Sales (not filtered) отображает нарастающий итог с начала года
по мере Sales Amount, а, поскольку в таблице Sales данные содержатся
только по 14 августа 2010 года, с этого месяца и до декабря мера нарастающего итога превращается в горизонтальную линию.
402
 Глава 24. Прогнозирование
Рис. 24.7. Вывод прогноза и факта на одной диаграмме
Лучше было бы выводить фактические продажи до последнего дня,
доступного в модели данных, а для будущих периодов использовать
прогноз. На рис. 24.8 показан такой тип отображения графика.
Рис. 24.8. Вывод прогноза и факта на одной диаграмме при помощи проекции
Последним полным месяцем с данными по мере YTD Sales Amount является июль 2010 года. Последующие месяцы на диаграмму не выводятся по причине проверки на дату, реализованной в мере.
Фактические данные и прогноз на одной диаграмме  403
Мера в таблице Sales:
YTD Sales Amount :=
VAR LastAvailableDate =
CALCULATE (
MAX ( Sales[Order Date] );
REMOVEFILTERS ( 'Date' )
)
VAR LastVisibleDate =
MAX ( 'Date'[Date] )
VAR Result =
IF (
LastVisibleDate <= LastAvailableDate;
CALCULATE ( [Sales Amount]; DATESYTD ( 'Date'[Date] ) )
)
RETURN
Result
Мера YTD Forecast используется для отображения прогноза на графике. Все, что она делает, это вычисляет нарастающий итог с начала года
по мере Forecast Amount, определенной в предыдущем разделе.
Мера в таблице Forecast:
YTD Forecast :=
CALCULATE (
[Forecast Amount];
DATESYTD ( 'Date'[Date] )
)
В мере YTD Projection вычисляется нарастающий итог с начала года
по мере Projection Amount, в которой в свою очередь используется мера
Sales Amount для существующих продаж (у нас для 14 августа 2010 года)
и Forecast Amount – для последующих дат (позже 14 августа).
Мера в таблице Forecast:
YTD Projection :=
CALCULATE (
[Projection Amount];
DATESYTD ( 'Date'[Date] )
)
404
 Глава 24. Прогнозирование
Мера в таблице Forecast:
Projection Amount :=
VAR LastAvailableDate =
CALCULATE (
MAX ( Sales[Order Date] );
REMOVEFILTERS ( 'Date' )
)
VAR ActualSalesAmount = [Sales Amount]
VAR ForecastedAmount =
CALCULATE (
[Forecast Amount];
KEEPFILTERS ( 'Date'[Date] > LastAvailableDate )
)
VAR Result =
ActualSalesAmount + ForecastedAmount
RETURN
Result
Мера YTD Adjusted Projection подобна мере YTD Projection, но с учетом корректирующего коэффициента, примененного к мере Forecast
Amount на основании сравнения доступных транзакций и соответствующего прогноза.
Мера в таблице Forecast:
YTD Adjusted Projection :=
CALCULATE (
[Adjusted Projection Amount];
DATESYTD ( 'Date'[Date] )
)
Мера в таблице Forecast:
Adjusted Projection Amount :=
VAR LastAvailableDate =
CALCULATE (
MAX ( Sales[Order Date] );
REMOVEFILTERS ( 'Date' )
)
VAR ActualSalesAmount = [Sales Amount]
VAR AdjustmentFactor = [% Adjustment]
VAR ForecastAmount =
CALCULATE (
[Forecast Amount] * AdjustmentFactor;
KEEPFILTERS ( 'Date'[Date] > LastAvailableDate )
Фактические данные и прогноз на одной диаграмме  405
)
VAR Result =
ActualSalesAmount + ForecastAmount
RETURN
Result
Корректирующий коэффициент, вычисленный в мере % Adjustment,
может иметь самые разные реализации в зависимости от конкретных
требований. В данном случае мы использовали отношение между мерами Sales Amount и Forecast Amount для интервала дат, доступного в
таблице Sales.
Мера в таблице Forecast:
% Adjustment :=
VAR LastAvailableDate =
CALCULATE (
MAX ( Sales[Order Date] );
REMOVEFILTERS ( 'Date' )
)
VAR CurrentYear =
SELECTEDVALUE ( 'Date'[Calendar Year] )
VAR Result =
CALCULATE (
DIVIDE (
[Sales Amount];
[Forecast Amount]
);
'Date'[Date] <= LastAvailableDate;
'Date'[Calendar Year] = CurrentYear
)
RETURN
Result
Предметный указатель
A
ABC-анализ 239
динамический 247
динамический поиск категории 249
снимок 243
статический 240
ALLEXCEPT 105, 197
ALLNOBLANKROW 299
C
CLOSINGBALANCEMONTH 202
CLOSINGBALANCEQUARTER 202
CLOSINGBALANCEYEAR 203
CONVERT 26
CROSSFILTER 317, 365
D
DATATABLE 214
DATEADD 26, 38
DATESINPERIOD 59
DATESMTD 37
DATESQTD 36
DATESYTD 25, 34
DISTINCT 224
DISTINCTCOUNT 311
E
KEEPFILTERS 231
L
LASTDATE 200
N
NATURALINNERJOIN 283
O
OPENINGBALANCEMONTH 203
OPENINGBALANCEQUARTER 203
OPENINGBALANCEYEAR 203
P
PARALLELPERIOD 52
PATHCONTAINS 343
PREVIOUSDAY 26
PREVIOUSMONTH 57
PREVIOUSQUARTER 55
PREVIOUSYEAR 52
R
RANKX 318
REMOVEFILTERS 24, 310
S
G
SAMEPERIODLASTYEAR 40, 47, 61
SELECTEDVALUE 215, 349
SUMMARIZE 283
SUMX 303
SWITCH 326
GENERATESERIES 213
T
ENDOFMONTH 26
EOMONTH 26
EXCEPT 304
H
HASONEVALUE 388
I
INTERSECT 304
ISFILTERED 327
ISINSCOPE 44, 85, 326
ISONORAFTER 169
K
TOPN 220, 318
TOTALMTD 37
TOTALQTD 36
TOTALYTD 33, 35
TREATAS 283, 315
U
UNION 298, 304
USERELATIONSHIP 317
V
Предметный указатель  407
VALUES 287
статическое 318
С
А
Анализ покупательской корзины 368
Ассоциативные правила 368
В
Вычисления на основе времени 23
Вычисления, связанные со временем 18
Сегментация
динамическая 230
статическая 222
Скользящая годовая сумма 58
Скользящее среднее 61
Соотносительный коэффициент 189
Сопоставимые продажи 345
Т
Д
Таблица параметров 213
Доверие 371
Ф
И
Фильтро-защищенные столбцы 104
Иерархии 325
родитель-потомок 329
К
Количество уникальных связанных
элементов 301
Л
Лифт 371
М
Матрица переходов 352
динамическая 358
статическая 355
Н
Незавершенные события 306
О
Опросник 362
П
Поддержка 370
Полуаддитивные вычисления 193
Прогнозирование 398
Производный снимок 293
Р
Ранжирование 318
динамическое 320
Книги издательства «ДМК ПРЕСС» можно купить оптом и в розницу
в книготорговой компании «Галактика»
(представляет интересы издательств
«ДМК ПРЕСС», «СОЛОН ПРЕСС», «КТК Галактика»).
Адрес: г. Москва, пр. Андропова, 38;
Тел.: +7(499) 782-38-89. Электронная почта: books@alians-kniga.ru.
При оформлении заказа следует указать
адрес (полностью), по которому должны быть высланы книги;
фамилию, имя и отчество получателя.
Желательно также указать свой телефон и электронный адрес.
Эти книги вы можете заказать и в интернет-магазине: www.a-planeta.ru.
Альберто Феррари
Марко Руссо
Шаблоны DAX
Наиболее полное собрание готовых к использованию решений
на языке DAX для Power BI, Analysis Services и Power Pivot
Главный редактор
Мовчан Д. А.
dmkpress@gmail.com
Перевод с английского
Корректор
Верстка
Дизайн обложки
Гинько А. Ю.
Абросимова Л. А.
Паранская Н. В.
Мовчан А. Г.
Формат 70×1001/16. Печать цифровая.
Усл. печ. л. 33.15. Тираж 200 экз.
Веб-сайт издательства: www.dmkpress.com
Powered by TCPDF (www.tcpdf.org)
Download