Урок 5.5: Временная декомпозиция и сезонный анализ

Урок 5.5: Временная декомпозиция и сезонный анализ

Модуль 5: Анализ временных рядов

Введение

Представьте владельца магазина игрушек, который в декабре 2023 года продал товаров на 40% больше, чем в ноябре. Успех? Не спешите с выводами. Рождественские продажи традиционно составляют до 40% годовой выручки в этом бизнесе. Возможно, рост в 40% — это провал, если обычно декабрь показывает +60%. Как отделить реальный рост бизнеса от сезонного пика? Как понять, действительно ли декабрь был успешным? Ответ — в декомпозиции временного ряда, разложении данных на составляющие компоненты.

Сезонность окружает нас повсюду. Туристические компании видят пики летом и провалы зимой. Энергетики фиксируют всплески потребления электричества летом из-за кондиционеров и зимой из-за отопления. Ритейлеры живут от "черной пятницы" до новогодних распродаж. Даже рестораны быстрого питания знают, что пятница вечером принесет в три раза больше заказов, чем понедельник утром. Без понимания этих паттернов невозможно отличить естественные колебания от реальных проблем или успехов.

Декомпозиция временного ряда — это как снятие нескольких слоев с луковицы. Мы отделяем долгосрочный тренд (растет ли бизнес в целом?) от сезонности (ожидаемые колебания) и случайных факторов (разовые акции, погодные аномалии). Это позволяет принимать правильные решения: не паниковать при сезонном январском спаде после новогодних праздников и не переоценивать декабрьский подъем. В этом финальном уроке модуля мы объединим все изученные временные функции MDX для профессионального анализа сезонности.

Теоретическая часть

A. Компоненты временного ряда

Любой временной ряд можно представить как комбинацию четырех компонентов:

  • Тренд (Trend) — долгосрочное направление развития, очищенное от всех колебаний. Это ответ на вопрос "Куда движется бизнес?"
  • Сезонность (Seasonal) — регулярные, предсказуемые колебания с фиксированным периодом (год, квартал, месяц, неделя).
  • Циклическая компонента (Cyclical) — долгосрочные волны без фиксированного периода, связанные с бизнес-циклами экономики.
  • Случайная компонента (Irregular) — непредсказуемые флуктуации: разовые акции, погодные аномалии, форс-мажоры.

Модели комбинирования:

  • Аддитивная: Y = Trend + Seasonal + Irregular (когда амплитуда сезонности постоянна)
  • Мультипликативная: Y = Trend × Seasonal × Irregular (когда амплитуда растет с уровнем)

???? Pro Tip: Выбор между аддитивной и мультипликативной моделью

  • Если сезонные колебания постоянны по амплитуде → аддитивная
  • Если амплитуда растет с ростом уровня → мультипликативная Простой тест: постройте график, если "конверт" расширяется — используйте мультипликативную модель.

B. Методы выявления сезонности в MDX

MDX предоставляет богатый инструментарий для анализа сезонности:

  • ParallelPeriod — сравнение с аналогичным периодом прошлого года устраняет сезонность
  • Расчет сезонных индексов — отношение среднего за конкретный месяц к общему среднему
  • LastPeriods + скользящие средние — выделение тренда через сглаживание
  • Year-over-Year анализ — фокус на изменениях, а не абсолютных значениях

C. Расчет сезонных коэффициентов

Классический метод:

  1. Рассчитать 12-месячную скользящую среднюю (устраняет сезонность)
  2. Найти отношение фактических значений к скользящей средней
  3. Усреднить отношения для каждого месяца за несколько лет
  4. Нормализовать, чтобы сумма индексов = 12

D. Десезонализация данных

Десезонализированные данные показывают, какими были бы продажи без сезонного эффекта. Это критически важно для:

  • Оценки реального роста бизнеса
  • Сравнения разносезонных периодов
  • Выявления трендов и точек перелома
  • Обнаружения аномалий

E. Прогнозирование с учетом сезонности

Качественный прогноз = Экстраполяция тренда × Сезонный коэффициент × Поправка на особые факторы

F. Интеграция с предыдущими уроками

Каждая изученная функция играет свою роль в декомпозиции:

  • Lag/Lead (5.1) — расчет приростов и темпов изменения
  • ParallelPeriod (5.2) — year-over-year анализ без сезонности
  • PeriodsToDate (5.3) — накопительные итоги для оценки годовых трендов
  • LastPeriods (5.4) — скользящие средние для выделения тренда

Визуализация декомпозиции:

Original: ╱╲╱╲╱╲╱╲  (тренд + сезонность + шум)
Trend:    ╱╱╱╱╱╱╱╱   (выделенный тренд)
Seasonal: ∩∪∩∪∩∪∩∪   (сезонный паттерн)
Random:   -~-~-~-~   (остаток)

Практическая часть

Пример 1: Расчет сезонных индексов

WITH 
-- Среднее значение для каждого месяца за 3 года (2011-2013)
MEMBER [Measures].[Jan Avg] AS
    AVG({
        ([Date].[Calendar].[Calendar Year].&[2011].FirstChild.FirstChild, [Measures].[Internet Sales Amount]),
        ([Date].[Calendar].[Calendar Year].&[2012].FirstChild.FirstChild, [Measures].[Internet Sales Amount]),
        ([Date].[Calendar].[Calendar Year].&[2013].FirstChild.FirstChild, [Measures].[Internet Sales Amount])
    })

-- Аналогично для других месяцев (показываем несколько для примера)
MEMBER [Measures].[Jun Avg] AS
    AVG({
        ([Date].[Calendar].[Calendar Year].&[2011].Children.Item(1).LastChild, [Measures].[Internet Sales Amount]),
        ([Date].[Calendar].[Calendar Year].&[2012].Children.Item(1).LastChild, [Measures].[Internet Sales Amount]),
        ([Date].[Calendar].[Calendar Year].&[2013].Children.Item(1).LastChild, [Measures].[Internet Sales Amount])
    })

MEMBER [Measures].[Dec Avg] AS
    AVG({
        ([Date].[Calendar].[Calendar Year].&[2011].LastChild.LastChild, [Measures].[Internet Sales Amount]),
        ([Date].[Calendar].[Calendar Year].&[2012].LastChild.LastChild, [Measures].[Internet Sales Amount]),
        ([Date].[Calendar].[Calendar Year].&[2013].LastChild.LastChild, [Measures].[Internet Sales Amount])
    })

-- Общее среднее за все месяцы
MEMBER [Measures].[Overall Monthly Avg] AS
    AVG(
        DESCENDANTS(
            {[Date].[Calendar].[Calendar Year].&[2011],
             [Date].[Calendar].[Calendar Year].&[2012],
             [Date].[Calendar].[Calendar Year].&[2013]},
            [Date].[Calendar].[Month],
            SELF
        ),
        [Measures].[Internet Sales Amount]
    )

-- Сезонный индекс для текущего месяца
MEMBER [Measures].[Seasonal Index] AS
    CASE [Date].[Calendar].CurrentMember.Name
        WHEN "January" THEN [Measures].[Jan Avg] / [Measures].[Overall Monthly Avg]
        WHEN "June" THEN [Measures].[Jun Avg] / [Measures].[Overall Monthly Avg]
        WHEN "December" THEN [Measures].[Dec Avg] / [Measures].[Overall Monthly Avg]
        -- Добавить остальные месяцы
        ELSE 1.0
    END,
    FORMAT_STRING = "#,##0.00"

-- Интерпретация индекса
MEMBER [Measures].[Index Interpretation] AS
    CASE
        WHEN [Measures].[Seasonal Index] > 1.2 THEN "High Season (+" + 
            STR(ROUND(([Measures].[Seasonal Index] - 1) * 100, 0)) + "%)"
        WHEN [Measures].[Seasonal Index] < 0.8 THEN "Low Season (" + 
            STR(ROUND(([Measures].[Seasonal Index] - 1) * 100, 0)) + "%)"
        ELSE "Normal Season"
    END

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Seasonal Index],
     [Measures].[Index Interpretation]} ON COLUMNS,
    DESCENDANTS(
        [Date].[Calendar].[Calendar Year].&[2013],
        [Date].[Calendar].[Month],
        SELF
    ) ON ROWS
FROM [Adventure Works]

Пример 2: Декомпозиция временного ряда

WITH 
-- Тренд: 12-месячная центрированная скользящая средняя
MEMBER [Measures].[Trend Component] AS
    IIF(
        COUNT(LastPeriods(12, [Date].[Calendar].CurrentMember), EXCLUDEEMPTY) < 12,
        NULL,
        AVG(
            LastPeriods(12, [Date].[Calendar].CurrentMember),
            [Measures].[Internet Sales Amount]
        )
    ),
    FORMAT_STRING = "Currency"

-- Детрендированные данные (факт / тренд для мультипликативной модели)
MEMBER [Measures].[Detrended] AS
    IIF(
        ISEMPTY([Measures].[Trend Component]) OR [Measures].[Trend Component] = 0,
        NULL,
        [Measures].[Internet Sales Amount] / [Measures].[Trend Component]
    ),
    FORMAT_STRING = "#,##0.00"

-- Сезонная компонента (упрощенно - как среднее отклонение от тренда)
MEMBER [Measures].[Seasonal Component] AS
    -- Для полной реализации нужно усреднить Detrended по месяцам за несколько лет
    [Measures].[Detrended],
    FORMAT_STRING = "#,##0.00"

-- Случайная компонента (остаток после удаления тренда и сезонности)
MEMBER [Measures].[Irregular Component] AS
    IIF(
        ISEMPTY([Measures].[Trend Component]),
        NULL,
        [Measures].[Internet Sales Amount] - 
        ([Measures].[Trend Component] * [Measures].[Seasonal Component])
    ),
    FORMAT_STRING = "Currency"

-- Качество декомпозиции (доля объясненной вариации)
MEMBER [Measures].[R-Squared] AS
    1 - (
        [Measures].[Irregular Component] * [Measures].[Irregular Component] /
        (([Measures].[Internet Sales Amount] - [Measures].[Trend Component]) * 
         ([Measures].[Internet Sales Amount] - [Measures].[Trend Component]))
    ),
    FORMAT_STRING = "Percent"

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Trend Component],
     [Measures].[Seasonal Component],
     [Measures].[Irregular Component],
     [Measures].[R-Squared]} ON COLUMNS,
    DESCENDANTS(
        [Date].[Calendar].[Calendar Year].&[2013],
        [Date].[Calendar].[Month],
        SELF
    ) ON ROWS
FROM [Adventure Works]

Пример 3: Десезонализация и анализ реального роста

WITH 
-- Базовый сезонный индекс (упрощенный расчет для демонстрации)
MEMBER [Measures].[Monthly Seasonal Index] AS
    CASE MONTH([Date].[Calendar].CurrentMember.Name)
        WHEN 1 THEN 0.85  -- Январь обычно -15%
        WHEN 6 THEN 1.05  -- Июнь обычно +5%
        WHEN 11 THEN 1.25 -- Ноябрь обычно +25%
        WHEN 12 THEN 1.40 -- Декабрь обычно +40%
        ELSE 1.0
    END,
    FORMAT_STRING = "#,##0.00"

-- Десезонализированные продажи
MEMBER [Measures].[Deseasonalized Sales] AS
    [Measures].[Internet Sales Amount] / [Measures].[Monthly Seasonal Index],
    FORMAT_STRING = "Currency"

-- Реальный рост (месяц к месяцу, десезонализированный)
MEMBER [Measures].[Real MoM Growth] AS
    IIF(
        ISEMPTY(([Date].[Calendar].CurrentMember.Lag(1), [Measures].[Deseasonalized Sales])),
        NULL,
        ([Measures].[Deseasonalized Sales] - 
         ([Date].[Calendar].CurrentMember.Lag(1), [Measures].[Deseasonalized Sales])) /
        ([Date].[Calendar].CurrentMember.Lag(1), [Measures].[Deseasonalized Sales])
    ),
    FORMAT_STRING = "Percent"

-- Номинальный рост для сравнения
MEMBER [Measures].[Nominal MoM Growth] AS
    IIF(
        ISEMPTY(([Date].[Calendar].CurrentMember.Lag(1), [Measures].[Internet Sales Amount])),
        NULL,
        ([Measures].[Internet Sales Amount] - 
         ([Date].[Calendar].CurrentMember.Lag(1), [Measures].[Internet Sales Amount])) /
        ([Date].[Calendar].CurrentMember.Lag(1), [Measures].[Internet Sales Amount])
    ),
    FORMAT_STRING = "Percent"

-- Детекция аномалий (отклонение от ожидаемого сезонного уровня)
MEMBER [Measures].[Anomaly Score] AS
    ABS([Measures].[Internet Sales Amount] - 
        ([Measures].[Deseasonalized Sales] * [Measures].[Monthly Seasonal Index])) /
    [Measures].[Internet Sales Amount],
    FORMAT_STRING = "Percent"

MEMBER [Measures].[Anomaly Status] AS
    CASE
        WHEN [Measures].[Anomaly Score] > 0.2 THEN "⚠️ Major Anomaly"
        WHEN [Measures].[Anomaly Score] > 0.1 THEN "Minor Deviation"
        ELSE "Normal"
    END

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Monthly Seasonal Index],
     [Measures].[Deseasonalized Sales],
     [Measures].[Nominal MoM Growth],
     [Measures].[Real MoM Growth],
     [Measures].[Anomaly Score],
     [Measures].[Anomaly Status]} ON COLUMNS,
    DESCENDANTS(
        [Date].[Calendar].[Calendar Year].&[2013],
        [Date].[Calendar].[Month],
        SELF
    ) ON ROWS
FROM [Adventure Works]

Пример 4: Комплексный прогноз с учетом тренда и сезонности

WITH 
-- Линейный тренд (упрощенный - прирост за последние 3 месяца)
MEMBER [Measures].[Trend Growth Rate] AS
    AVG(
        LastPeriods(3, [Date].[Calendar].CurrentMember),
        ([Measures].[Internet Sales Amount] - 
         ([Date].[Calendar].CurrentMember.Lag(1), [Measures].[Internet Sales Amount]))
    ),
    VISIBLE = 0

-- Прогноз тренда на следующий месяц
MEMBER [Measures].[Trend Forecast] AS
    [Measures].[Internet Sales Amount] + [Measures].[Trend Growth Rate],
    FORMAT_STRING = "Currency"

-- Сезонный множитель следующего месяца
MEMBER [Measures].[Next Month Season] AS
    CASE MONTH([Date].[Calendar].CurrentMember.Lead(1).Name)
        WHEN 1 THEN 0.85
        WHEN 12 THEN 1.40
        ELSE 1.0
    END,
    FORMAT_STRING = "#,##0.00"

-- Финальный прогноз = тренд × сезонность
MEMBER [Measures].[Final Forecast] AS
    [Measures].[Trend Forecast] * [Measures].[Next Month Season],
    FORMAT_STRING = "Currency"

-- Доверительный интервал (±15% для примера)
MEMBER [Measures].[Forecast Lower Bound] AS
    [Measures].[Final Forecast] * 0.85,
    FORMAT_STRING = "Currency"

MEMBER [Measures].[Forecast Upper Bound] AS
    [Measures].[Final Forecast] * 1.15,
    FORMAT_STRING = "Currency"

-- Оценка точности (если есть фактические данные)
MEMBER [Measures].[Forecast Accuracy] AS
    IIF(
        ISEMPTY(([Date].[Calendar].CurrentMember.Lead(1), [Measures].[Internet Sales Amount])),
        "No Actual Data",
        STR(ROUND(
            (1 - ABS(
                [Measures].[Final Forecast] - 
                ([Date].[Calendar].CurrentMember.Lead(1), [Measures].[Internet Sales Amount])
            ) / ([Date].[Calendar].CurrentMember.Lead(1), [Measures].[Internet Sales Amount])
        ) * 100, 1)) + "% Accurate"
    )

SELECT 
    {[Measures].[Internet Sales Amount],
     [Measures].[Trend Forecast],
     [Measures].[Next Month Season],
     [Measures].[Final Forecast],
     [Measures].[Forecast Lower Bound],
     [Measures].[Forecast Upper Bound],
     [Measures].[Forecast Accuracy]} ON COLUMNS,
    HEAD(
        DESCENDANTS(
            [Date].[Calendar].[Calendar Year].&[2013],
            [Date].[Calendar].[Month],
            SELF
        ),
        6
    ) ON ROWS
FROM [Adventure Works]

⚠️ Важно: Сезонность может меняться со временем! Пандемия COVID-19 полностью изменила сезонные паттерны во многих индустриях. Всегда проверяйте актуальность исторических сезонных коэффициентов.

Таблица сезонных индексов (пример):

Месяц
Индекс
Интерпретация
Январь
0.85
-15% от среднего
Февраль
0.90
-10% от среднего
Июнь
1.05
+5% от среднего
Ноябрь
1.25
+25% от среднего
Декабрь
1.40
+40% от среднего

График сезонности:

Sales Index by Month:
1.4 |                      *Dec
1.2 |                  *Nov
1.0 |---*Jun---*Oct-----------
0.8 | *Jan  *Feb

Заключение и завершение курса

Поздравляем с завершением курса по MDX! Этот урок, посвященный временной декомпозиции, является кульминацией нашего путешествия. Мы прошли долгий путь: от основ OLAP и синтаксиса MDX до сложнейших техник фильтрации, ранжирования и, наконец, глубокого анализа временных рядов. Вы научились видеть за цифрами настоящие бизнес-процессы, отделять устойчивый рост от сезонных колебаний и случайных аномалий.

Теперь в вашем арсенале есть мощные инструменты: ParallelPeriod для year-over-year сравнений, PeriodsToDate для анализа накопительных итогов и LastPeriods для выявления трендов. Вы готовы решать реальные аналитические задачи, строить сложные отчеты и давать бизнесу ценные инсайты, основанные на данных.

Это не конец, а начало вашего пути в мире многомерного анализа. Продолжайте практиковаться, экспериментировать с новыми подходами и никогда не переставайте задавать данным правильные вопросы. Удачи в ваших будущих аналитических проектах!

Модуль 5: Анализ временных рядов • Урок 5.5

Прошлый урок