Календари в Power BI: тихий ужас аналитика
Вы строите отчет по недельным продажам. Все выглядит отлично, пока не замечаете, что сумма за последнюю неделю января не сходится с данными из ERP. Вы проверяете формулы, пересчитываете в Excel - все верно. А в Power BI - нет. Знакомо?
Проблема не в ваших навыках DAX. Проблема в календарях. Точнее, в их конфликте. Power BI любит грегорианский календарь. Ваш бизнес - нет. Производственные недели, 4-4-5 периоды, фискальные годы - все это ломает стандартную временную аналитику.
И когда вы создаете пользовательскую таблицу дат, начинается интерференция. Две таблицы дат борются за отношения с фактами. Меры DAX начинают возвращать не то, путаются в контекстах. Отчеты врут.
Я расскажу, как этого избежать. Не поверхностно, а с деталями, которые понимают только те, кто уже наступил на эти грабли.
Грегорианский календарь против бизнеса: кто кого?
Power BI по умолчанию использует стандартный календарь. Год начинается 1 января, недели с понедельника или воскресенья (зависит от локали). Для бизнес-аналитики это часто бесполезно.
Потому что бизнес живет по своим циклам:
- Производственные недели: с понедельника по воскресенье? Или с воскресенья по субботу? А может, среда-вторник? Да, бывает и так.
- Финансовые периоды: 4-4-5 (два месяца по 4 недели, один - 5). Или 5-4-4. Или 13 периодов по 28 дней. Стандартные месяцы не подходят.
- ISO недели: неделя начинается с понедельника, первая неделя года - та, где есть четверг. Это международный стандарт, но Power BI не использует его по умолчанию.
Вы пытаетесь добавить столбец ISOWeekNum в таблицу дат. И все равно получаете не те числа. Почему? Потому что функции DAX типа WEEKNUM используют разные системы нумерации. А если у вас свой календарь - вообще беда.
Таблица дат: 5 ошибок, которые делают все
Создаете таблицу дат через CALENDAR или CALENDARAUTO? Уже ошибка. Нет, эти функции хороши для базового календаря. Но для пользовательского - нет.
Вот как НЕ надо делать:
DateTable =
ADDCOLUMNS (
CALENDAR ( DATE ( 2020, 1, 1 ), DATE ( 2025, 12, 31 ) ),
"Year", YEAR ( [Date] ),
"Month", MONTH ( [Date] ),
"WeekNum", WEEKNUM ( [Date], 2 ) -- ISO? Нет, это система 2, но не совсем ISO.
)
Эта таблица даст вам недели, но они не совпадут с ISO. Система 2 в WEEKNUM - это недели, начинающиеся с понедельника, но первая неделя года - та, где 1 января. В ISO первая неделя - где есть четверг. Разница может быть в несколько дней.
Другие ошибки:
- Несколько таблиц дат: одна для стандартного календаря, другая для производственного. Они конфликтуют за отношения с фактами. Power BI может использовать не ту, что нужно.
- Отсутствие ключей для связи: вы связываете таблицу фактов с датой, но у вас есть еще периоды. Нужен составной ключ? Иногда да, но чаще - отдельное отношение.
- Использование автоматических временных интеллект-функций: они работают только с одной таблицей дат, которую вы пометили как date table. Если у вас две календарных системы - они сломаются.
- Неправильная сортировка: текстовые поля типа "Январь 2026" не отсортируются по хронологии без числового столбца сортировки.
- Пропуск дат: в производственном календаре есть только рабочие дни. Но если в фактах есть продажи на выходные? Они не попадут в отчет.
Одна таблица дат на все календари: правильная архитектура
Не создавайте несколько таблиц дат. Создайте одну, но с полями для всех календарных систем. Это сложнее, но работает.
1 Определите диапазон дат
Возьмите минимальную и максимальную дату из всех фактовых таблиц. Добавьте запас в пару лет вперед и назад. Не используйте CALENDARAUTO - он берет даты из текущего контекста, а если данных еще нет, он ошибется.
StartDate = DATE ( 2020, 1, 1 )
EndDate = DATE ( 2030, 12, 31 )
Dates = CALENDAR ( StartDate, EndDate )
2 Добавьте столбцы для каждого календаря
Создайте отдельные столбцы для грегорианского календаря, ISO недель, производственного календаря. Используйте DAX или лучше - заранее подготовленную таблицу в источнике данных. DAX может тормозить на больших объемах.
Пример для ISO недель:
DatesWithISO =
ADDCOLUMNS (
Dates,
"ISO Year", YEAR ( [Date] ) - IF ( WEEKDAY ( [Date], 2 ) < 4, 0, 1 ) + IF ( WEEKNUM ( [Date], 21 ) = 1, 1, 0 ), -- Упрощенная логика, лучше использовать ISOWEEKNUM
"ISO Week Number", ISOWEEKNUM ( [Date] ) -- В DAX есть ISOWEEKNUM с 2023 года? Проверим. На 2026 год - да, функция есть.
)
Для производственного календаря вам понадобится таблица с рабочими днями и периодами. Ее лучше загрузить из внешнего источника (Excel, базы данных). Затем связать с таблицей дат по полной дате.
3 Настройте отношения
Свяжите таблицу фактов с таблицей дат по полной дате (date). Это основное отношение. Для производственного календаря создайте отдельную таблицу периодов (Periods) с полями PeriodKey, StartDate, EndDate, PeriodName. Затем свяжите ее с фактами через вспомогательную таблицу связи.
Не создавайте прямое отношение фактов с Periods по диапазону дат - DAX не поддерживает отношения по диапазону. Вместо этого используйте технику с bridge table или меры с FILTER.
Подробнее о сложных отношениях я писал в статье про финансовые модели и Star Schema.
DAX-меры: как считать в разных календарях
Создали таблицу дат. Теперь нужно написать меры, которые будут правильно работать в контексте каждого календаря.
Стандартная мера суммы продаж:
Total Sales = SUM ( Sales[Amount] )
Чтобы считать продажи по ISO неделям, просто используйте столбец ISO Week Number в визуалах. Но если вам нужна мера, которая возвращает сумму за текущую ISO неделю (в контексте фильтра), то это уже сложнее.
Не пишите так:
Sales ISO Week =
CALCULATE (
[Total Sales],
FILTER (
Dates,
Dates[ISO Week Number] = SELECTEDVALUE ( Dates[ISO Week Number] )
)
)
Это вызовет циклические зависимости или будет работать медленно. Вместо этого используйте функцию VALUES и отношение по дате.
Правильно:
Sales ISO Week =
CALCULATE (
[Total Sales],
KEEPFILTERS ( Dates[ISO Week Number] )
)
KEEPFILTERS гарантирует, что фильтр по ISO неделе применяется правильно, не удаляя другие контексты.
Для производственного календаря, где нет прямого отношения, используйте меры с пересечением дат:
Sales Production Period =
CALCULATE (
[Total Sales],
FILTER (
Sales,
Sales[Date] >= MIN ( ProductionPeriods[StartDate] )
&& Sales[Date] <= MAX ( ProductionPeriods[EndDate] )
)
)
Эта мера будет работать, но может быть медленной на больших данных. Если возможно, создайте bridge table, которая связывает каждую дату с периодом, и установите отношение через нее.
Интерференция календарей: когда два календаря дерутся
Интерференция возникает, когда у вас несколько календарных систем и они конфликтуют в контексте фильтрации. Например, вы фильтруете по ISO неделе 1 2026 года и одновременно по производственному периоду P1. Эти периоды могут пересекаться только частично.
Power BI применяет оба фильтра к таблице дат. Если у вас одна таблица дат, то фильтры применяются к разным столбцам одной таблицы - это нормально. Но если у вас две отдельные таблицы дат, связанные с фактами, то Power BI попытается использовать оба отношения, и результат будет непредсказуемым.
Решение: используйте одну таблицу дат с разными атрибутами. Если все же нужны отдельные таблицы (например, для гибкости), то управляйте отношениями с помощью USERELATIONSHIP в мерах.
Sales Gregorian =
CALCULATE (
[Total Sales],
USERELATIONSHIP ( Sales[Date], GregorianDates[Date] )
)
Sales Production =
CALCULATE (
[Total Sales],
USERELATIONSHIP ( Sales[Date], ProductionDates[Date] )
)
Но тогда в одном визуале нельзя будет использовать оба календаря одновременно - только по отдельности. Это ограничение.
Интерференция также ломает временные интеллект-функции. SAMEPERIODLASTYEAR, DATEADD работают только с одной таблицей дат, отмеченной как date table. Если вам нужны аналогичные функции для другого календаря, пишите свои меры.
Например, продажи за предыдущий производственный период:
Sales Prev Production Period =
VAR CurrentPeriod = SELECTEDVALUE ( ProductionPeriods[PeriodKey] )
VAR PrevPeriod = LOOKUPVALUE (
ProductionPeriods[PeriodKey],
ProductionPeriods[PeriodKey], CurrentPeriod - 1
)
RETURN
CALCULATE (
[Total Sales],
ProductionPeriods[PeriodKey] = PrevPeriod
)
Это работает, если периоды идут по порядку. Если нет - нужна более сложная логика.
5 ошибок, которые вы совершите (и как их исправить)
| Ошибка | Почему происходит | Как исправить |
|---|---|---|
| Пустые значения в неделях | Даты, которые не попадают ни в одну неделю по вашему определению (например, 29 февраля в календаре 4-4-5) | Убедитесь, что каждая дата имеет назначенный период. Добавьте в таблицу дат столбец с периодом по умолчанию. |
| Неправильные итоги | Меры DAX считают в контексте стандартного календаря, даже когда вы фильтруете по производственному | Используйте KEEPFILTERS или перепишите меры с явным указанием таблицы дат через USERELATIONSHIP. |
| Медленная работа отчетов | Сложные меры с FILTER по таблице дат на лету | Вынесите логику в расчетные столбцы в таблице дат или в источник данных. Или используйте агрегации. |
| Конфликт имен столбцов | В двух календарях есть столбец "Week", но с разными значениями | Используйте префиксы: "ISO Week", "Prod Week". Или поместите каждый календарь в отдельную таблицу. |
| Сломанная сортировка | Текстовые поля типа "Week 1" сортируются как текст: Week 1, Week 10, Week 2 | Создайте числовой столбец сортировки и установите его в свойствах столбца. |
FAQ: частые вопросы о календарях в Power BI
Можно ли использовать автоматические временные интеллект-функции с пользовательским календарем?
Да, но только если вы пометите таблицу дат как date table (правый клик на таблице -> Mark as date table). Однако эти функции будут работать только с полем даты, которое вы указали как уникальное. Они не будут учитывать ваши пользовательские периоды. Для кастомных периодов пишите свои меры.
Как обрабатывать финансовый год, который начинается не с января?
Добавьте столбец Financial Year в таблицу дат. Логика: если месяц даты больше или равен начальному месяцу финансового года, то финансовый год = год даты, иначе год даты - 1. Например, финансовый год начинается в апреле: Financial Year = YEAR([Date]) + IF(MONTH([Date])>=4,0,-1).
Что делать, если в бизнесе используется несколько календарей одновременно?
Создайте одну таблицу дат со столбцами для каждого календаря. Если периоды разных календарей не совпадают (например, производственные недели и финансовые периоды), то создайте отдельные таблицы периодов и свяжите их с фактами через bridge table или меры с FILTER.
Почему мои недельные отчеты показывают неполные данные на границе годов?
Скорее всего, вы используете WEEKNUM без учета года. Неделя 1 2026 года и неделя 1 2025 года - разные. Используйте составной ключ, например, "2026-W01" или отдельные столбцы Year и Week Number. В визуалах группируйте по обоим.
Как автоматизировать создание таблицы дат с пользовательскими периодами?
Лучше всего создать таблицу в источнике данных (SQL, Excel) и обновлять ее скриптом. Если хотите автоматизацию в Power BI, посмотрите мою статью про автоматизацию Power BI с помощью ИИ. Там есть примеры генерации DAX и схем.
Что в итоге?
Пользовательские календари - это боль. Но если сделать правильно, они превращаются в мощный инструмент. Запомните:
- Одна таблица дат - много атрибутов.
- Отношения только по дате, для периодов - bridge table или меры.
- Пишите меры с учетом контекста фильтрации.
- Тестируйте на граничных случаях: конец года, високосные годы, пропущенные даты.
И последний совет: если ваш бизнес использует экзотический календарь (скажем, лунный), подумайте, действительно ли он нужен в Power BI. Может, проще преобразовать даты в стандартные периоды на этапе ETL? Иногда проще изменить бизнес-процесс, чем заставить Power BI работать с календарем майя. (Шучу. Но лишь отчасти.)