§ P19 — DAX-кейс · Визуализация

Forecast + actual на одном визуале без разрыва линии

DAX · BLANK-трюк · Conditional formatting · Доверительный интервал

Типичная задача: показать на одном line-графике факт (сплошной линией) и прогноз (пунктиром), чтобы они плавно переходили друг в друга без визуального разрыва. Решение — не в визуале, а в модели данных и DAX-мерах.

Руководитель открывает дашборд, видит линию выручки за 12 месяцев. Хочет видеть, куда тренд идёт на 3-6 месяцев вперёд. Желание понятное — но в Power BI не так просто сделать красиво. Типичные неудачи:

  • Две раздельные линии, которые не соединяются в точке «сегодня». Выглядит разорванно.
  • Одна линия, но без визуального отличия факта и прогноза. Непонятно, где заканчиваются реальные данные.
  • Встроенный Forecast в стандартном line-визуале Power BI — работает только для простых случаев, плохо настраивается, не поддерживает бизнес-логику.

Правильное решение: две меры (Actual и Forecast), одна точка перекрытия, conditional formatting на линии. Результат выглядит примерно так:

§ Выручка, млн ₽
0 25 50 75 100 СЕГОДНЯ Июл Окт Янв Апр Июл Окт
Factual (сплошной) Forecast (пунктир) 95% доверительный интервал

Линия проходит через точку «сегодня» без разрыва. Фактическая часть — сплошной синий. Прогноз — пунктирный фиолетовый. Доверительный интервал — полупрозрачная область вокруг прогноза. Сделаем это пошагово.

Модель данных: готовим источники

Минимум нужны две таблицы:

  1. fact_sales — реальные данные: date, выручка. История, скажем, 24 месяца назад до «сегодня».
  2. fact_forecast — прогнозные значения: date, forecast_выручка, forecast_lower (нижняя граница интервала), forecast_upper (верхняя). От «сегодня» на 6-12 месяцев вперёд. Откуда берётся прогноз — отдельная тема (ниже обсудим варианты).

Обе таблицы связаны с общей dim_date. Важно: диапазон dim_date должен покрывать и факт, и прогноз. Если прогноз на июль 2027, а dim_date заканчивается в апреле 2026, прогнозные точки не отобразятся.

Базовые меры Actual и Forecast

Actual Revenue =
SUM(fact_sales[revenue])

Forecast Revenue =
SUM(fact_forecast[forecast_revenue])

Если положить обе в line-chart с осью dim_date[date], получите две линии: Actual заканчивается «сегодня», Forecast начинается «сегодня». Но визуал отрисует разрыв — в точке today одна линия заканчивается, другая стартует с нуля в предыдущей точке (NaN показывается как 0 или gap).

Трюк 1: общая точка «сегодня»

Чтобы линии соединились, в точке «сегодня» оба значения должны существовать. Делается это в мере Forecast: добавляем туда фактическое значение последнего дня.

Forecast Revenue (Seamless) =
VAR _today = TODAY()
VAR _current_date = SELECTEDVALUE(dim_date[date])
VAR _is_today_or_future = _current_date >= _today
VAR _forecast_value = SUM(fact_forecast[forecast_revenue])
VAR _actual_today =
    CALCULATE(
        SUM(fact_sales[revenue]),
        dim_date[date] = _today
    )
RETURN
    SWITCH(
        TRUE(),
        _current_date = _today, _actual_today,  // точка «сегодня» = actual
        _is_today_or_future, _forecast_value,   // будущее = forecast
        BLANK()                                  // прошлое = blank (не отрисовываем)
    )

Симметрично в Actual — чтобы не было «хвоста» от фактической линии в будущее:

Actual Revenue (Seamless) =
VAR _today = TODAY()
VAR _current_date = SELECTEDVALUE(dim_date[date])
RETURN
    IF(
        _current_date <= _today,
        SUM(fact_sales[revenue]),
        BLANK()
    )

Теперь в line-chart две линии проходят через общую точку «сегодня», Actual — слева, Forecast — справа. BLANK ячейки Power BI просто не рисует. Получается непрерывная кривая.

Трюк 2: разный стиль линий

По умолчанию обе линии визуал рисует одинаково. Чтобы forecast был пунктирным, в line-chart настраиваем:

  • Выбираем серию «Forecast выручка (Seamless)»
  • Format → Shapes → Stroke style: Dashed
  • Stroke width: 2-3px
  • Line type: Dashed (в зависимости от версии Power BI)
  • Можно поменять цвет на более «гипотетический» (фиолетовый, серый)

Для Actual: Solid, более насыщенный цвет (обычно основной бренд-цвет).

Трюк 3: доверительный интервал

Прогноз без интервала — просто точка, которая может сильно ошибаться. Нормальный визуал показывает диапазон уверенности: «скорее всего будет от X до Y». Делается через тип графика «Line and stacked column chart» или через наложение shaded area.

Создаём две дополнительные меры:

Forecast Lower (Seamless) =
VAR _today = TODAY()
VAR _current_date = SELECTEDVALUE(dim_date[date])
RETURN
    IF(
        _current_date > _today,
        SUM(fact_forecast[forecast_lower]),
        BLANK()
    )

Forecast Upper (Seamless) =
VAR _today = TODAY()
VAR _current_date = SELECTEDVALUE(dim_date[date])
RETURN
    IF(
        _current_date > _today,
        SUM(fact_forecast[forecast_upper]),
        BLANK()
    )

// Вспомогательная: ширина интервала (для stacked area)
Forecast Band Width =
[Forecast Upper (Seamless)] - [Forecast Lower (Seamless)]

Вариант 1 (простой): в отдельном line-chart поверх основного рисуем область между Lower и Upper как полупрозрачный fill. Нужен custom visual «Advanced Scorecard Deneb» или аналогичный.

Вариант 2 (через нативный визуал): делаем Area Chart с двумя слоями — Lower (невидимый, только базис) и Band Width (полупрозрачный fill). Визуально получается «облако вокруг forecast».

Вариант 3 (Deneb / Vega-Lite): полная свобода. Задаёте JSON-спецификацию с AreaMark для интервала + LineMark для точек. Рекомендуется для production-дашбордов, где внешний вид критичен.

Откуда брать прогноз

Главный вопрос, который не связан с DAX: как посчитать forecast_выручка. Пять распространённых вариантов:

МетодГде считаетсяКогда применять
Простая экстраполяция (линейный тренд)DAX (TRENDLINE) или ETLБыстрый старт, стабильный тренд без сезонности
Скользящее среднее × YoY-коэффициентETL (SQL) или DAXЕсть сезонность, но нужна простая модель
Holt-Winters (экспоненциальное сглаживание)Python/R в ETLСезонность + тренд, стандарт ритейла
ARIMA / SARIMAPython (statsmodels)Сложная временна́я динамика, большие ряды
Prophet / NeuralProphetPythonМножественная сезонность, праздники, outliers
Бизнес-план (ручной прогноз)Excel/CRM → fact_forecastКогда есть S&OP-процесс и утверждённые цифры

На практике чаще всего: ETL-скрипт на Python раз в сутки считает Prophet или Holt-Winters, складывает результат с доверительными интервалами в таблицу fact_forecast. Power BI читает оттуда через тот же DWH. Пересчёт автоматический, визуал обновляется при очередном refresh.

Практика: для среднего ритейла (50-500 млн ₽ месячной выручки) Holt-Winters с недельной сезонностью и годовой ковариацией обычно даёт MAPE 8-15% на горизонте 3 месяца. Prophet улучшает это до 5-10% при наличии явных праздников (Новый год, 8 марта, чёрная пятница).

Простейший forecast прямо в DAX

Если ML-инфраструктуры нет, а прогноз нужен уже сегодня — можно собрать наивный forecast на DAX. Формула: «среднее за последние 3 месяца × YoY-коэффициент». Не production-качество, но даёт приемлемый результат в стабильном бизнесе.

Forecast Naive =
VAR _current = SELECTEDVALUE(dim_date[date])
VAR _today = TODAY()
VAR _is_future = _current > _today
VAR _last_3m_avg =
    CALCULATE(
        AVERAGEX(
            VALUES(dim_date[year_month]),
            [Actual Revenue]
        ),
        DATESINPERIOD(dim_date[date], _today, -3, MONTH)
    )
VAR _yoy_coef =
    DIVIDE(
        CALCULATE(
            [Actual Revenue],
            DATESINPERIOD(dim_date[date], _today, -12, MONTH)
        ),
        CALCULATE(
            [Actual Revenue],
            DATESINPERIOD(dim_date[date], _today, -24, MONTH),
            DATESINPERIOD(dim_date[date], EOMONTH(_today,-12), -12, MONTH)
        )
    )
RETURN
    IF(_is_future, _last_3m_avg * _yoy_coef, BLANK())

Грубо, но работоспособно. Для серьёзных задач обязательно выносите прогноз в отдельный конвейер (Python/R), а в DAX только отображаете результат.

Фишки, которые делают визуал production-ready

Вертикальная линия «сегодня»

Добавляет читаемости. В Power BI через Analytics pane → Constant Line → установите значение как TODAY() (через DAX-меру).

Today Marker =
CALCULATE(MAX(dim_date[date]), fact_sales[revenue] > 0)
// возвращает последнюю дату факта

Подпись точки перехода

Label «Сегодня: X млн ₽» прямо на точке перехода. Делается через Data Labels только для точки today (conditional formatting): IF(SELECTEDVALUE(dim_date[date]) = TODAY(), Actual Revenue, BLANK()).

Tooltip с метаинформацией

Для точек прогноза в tooltip показывайте не только значение, но и интервал + тип («прогноз Prophet, обновлён 17 апреля 2026»). Это убирает иллюзию точности, которую прямая линия создаёт.

Accuracy back-check

Отдельный визуал «MAPE прогноза за последние 30 дней» — показывает, насколько модель точна. Считается как AVERAGEX(recent_days, ABS(actual - forecast) / actual). Если MAPE > 20%, модель врёт — нужно пересматривать.

Переключатель горизонта

срез «Прогноз на»: 1, 3, 6, 12 месяцев. В мере фильтруем Forecast по выбранному горизонту:

Forecast Horizon =
VAR _h = SELECTEDVALUE('Forecast Horizon'[Months], 6)
VAR _today = TODAY()
VAR _limit = EDATE(_today, _h)
VAR _current = SELECTEDVALUE(dim_date[date])
RETURN
    IF(
        _current > _today && _current <= _limit,
        SUM(fact_forecast[forecast_revenue]),
        BLANK()
    )

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

  1. Общая точка «сегодня» не добавлена. Две линии с разрывом в 1 точку. Визуально разорвано, руководитель сразу спрашивает «почему?».
  2. Разные dim_date для fact и forecast. Получаются две независимых оси, визуал не работает. Всегда одна dim_date на всю модель.
  3. Forecast выходит за dim_date. Прогноз на июль 2027, а календарь заканчивается апрелем 2026. Расширьте dim_date минимум на 2 года в будущее.
  4. Забыть BLANK в «сыне». Если в Actual не вернуть BLANK для будущих дат, а просто 0 — линия Actual уходит в ноль в будущем, выглядит как «мы обанкротились».
  5. Не тестировать на границе. «Сегодня» перескочит через 00:00 — TODAY() вернёт новое значение. Визуал может внезапно поменяться, если FACT обновляется только раз в сутки. Обсудите с бизнесом, что считается «сегодняшней датой» для отчёта.
  6. Показывать forecast без интервала. Создаёт иллюзию точности. Минимум — tooltip с интервалом.
  7. Одинаковые цвета для Actual и Forecast. Пользователь не понимает, где факт, где прогноз. Обязательно визуально отличать: пунктир + другой оттенок.

Вариации для разных задач

План-факт-прогноз на одном графике

Помимо факта и прогноза, показать ещё и план. Три меры: Plan, Actual, Forecast. Plan часто идёт тонкой серой линией на весь год, Actual — жирный синий до today, Forecast — пунктир после today. Полезно для CFO: одним взглядом видно «куда идём vs куда планировали».

Два forecast-сценария

Оптимистичный и пессимистичный (или base-case и best-case). Две пунктирные линии разного цвета, расходящиеся от today. Полезно при планировании: «в базовом сценарии закроем 500 млн, в оптимистичном — 600».

Forecast vs forecast (версионирование)

Текущий прогноз vs прогноз месячной давности. Показывает, как менялся взгляд на будущее. Если оба forecast сильно расходятся — значит, в бизнесе произошли изменения, которые стоит обсудить.

Шаги внедрения

  1. Решите, откуда брать forecast. От простого (наивная экстраполяция) до сложного (ML-модель). Лучше начать с простого и улучшать.
  2. Создайте таблицу fact_forecast в DWH. Схема: date, metric_name, forecast_value, forecast_lower, forecast_upper, forecast_run_date.
  3. Свяжите с общей dim_date. Расширьте календарь в будущее минимум на 1-2 года.
  4. Напишите пару Seamless-мер (Actual + Forecast) с логикой BLANK.
  5. Постройте line-chart с двумя сериями, настройте стиль: Actual sola, Forecast dashed.
  6. Добавьте доверительный интервал — через Deneb для production, через Area Chart для MVP.
  7. Протестируйте на граничных датах: первое число, конец месяца, смена года.
  8. Добавьте accuracy-виджет: MAPE последних 30 дней, чтобы бизнес видел степень доверия прогнозу.

Что делать дальше

§ Проект · 4-6 недель

Forecast-дашборд
под ключ

ML-модель прогноза + seamless-визуал в Power BI + мониторинг точности. Для ритейла, e-commerce, HoReCa, SaaS.

Телефон+7 918 042 34 43