window: скользящие и ранговые вычисления

Трансформация window предназначена для вычислений, зависящих от соседних строк, порядка данных или накопительного контекста. В отличие от обычных агрегатов (aggregate), работающих над всей группой сразу, window выполняет вычисления построчно, сохраняя каждую запись в результирующем наборе.

Через window реализуются:

  • скользящие средние;
  • накопительные суммы;
  • ранжирование;
  • вычисление процентов;
  • доступ к предыдущим и следующим строкам;
  • вычисление отклонений относительно соседних значений;
  • статистика внутри окон;
  • dense rank, percent rank, row number и другие аналитические функции.

Структурно window напоминает SQL-конструкции OVER (PARTITION BY ... ORDER BY ...).


Базовая структура window

{
  "transform": [
    {
      "window": [
        {
          "op": "sum",
          "field": "sales",
          "as": "running_total"
        }
      ]
    }
  ]
}

Основные элементы:

Поле Назначение
window список вычислений
op операция
field поле для обработки
as имя результирующего поля
sort порядок строк
groupby разбиение на группы
frame границы окна

Простое накопительное вычисление

Накопительная сумма

const spec = {
  data: {
    values: [
      {day: 1, sales: 10},
      {day: 2, sales: 15},
      {day: 3, sales: 7},
      {day: 4, sales: 20}
    ]
  },

  transform: [
    {
      window: [
        {
          op: "sum",
          field: "sales",
          as: "running_total"
        }
      ],
      sort: [
        {field: "day"}
      ],
      frame: [null, 0]
    }
  ],

  mark: "line",

  encoding: {
    x: {field: "day", type: "quantitative"},
    y: {field: "running_total", type: "quantitative"}
  }
};

Как работает frame

frame: [null, 0]

Окно задаётся двумя границами:

Значение Смысл
null начало или конец всей группы
0 текущая строка
-1 предыдущая строка
1 следующая строка

Интерпретация

[null, 0]

означает:

от первой строки до текущей.

Именно поэтому получается накопительный итог.


Скользящее среднее

Среднее по трём точкам

const spec = {
  data: {
    values: [
      {day: 1, value: 5},
      {day: 2, value: 8},
      {day: 3, value: 12},
      {day: 4, value: 7},
      {day: 5, value: 10}
    ]
  },

  transform: [
    {
      window: [
        {
          op: "mean",
          field: "value",
          as: "moving_avg"
        }
      ],

      sort: [
        {field: "day"}
      ],

      frame: [-1, 1]
    }
  ],

  layer: [
    {
      mark: "line",
      encoding: {
        x: {field: "day", type: "quantitative"},
        y: {field: "value", type: "quantitative"}
      }
    },

    {
      mark: {
        type: "line",
        color: "red"
      },

      encoding: {
        x: {field: "day", type: "quantitative"},
        y: {field: "moving_avg", type: "quantitative"}
      }
    }
  ]
};

Что означает [-1, 1]

frame: [-1, 1]

означает:

Позиция Значение
-1 предыдущая строка
0 текущая
1 следующая

Таким образом окно состоит из трёх элементов.


Центрированное и одностороннее окно

Центрированное окно

frame: [-2, 2]

Включает:

  • две предыдущие строки;
  • текущую;
  • две последующие.

Используется для сглаживания временных рядов.


Одностороннее окно

frame: [-4, 0]

Включает:

  • четыре предыдущих значения;
  • текущую строку.

Это типичное «ретроспективное» окно.


Ранговые функции

rank

Функция rank вычисляет позицию строки в отсортированном наборе.

{
  transform: [
    {
      window: [
        {
          op: "rank",
          as: "rank"
        }
      ],

      sort: [
        {
          field: "sales",
          order: "descending"
        }
      ]
    }
  ]
}

Особенности rank

При одинаковых значениях появляются пропуски.

Пример:

sales rank
100 1
90 2
90 2
80 4

Ранг 3 отсутствует.


dense_rank

Плотное ранжирование

{
  transform: [
    {
      window: [
        {
          op: "dense_rank",
          as: "dense_rank"
        }
      ],

      sort: [
        {
          field: "sales",
          order: "descending"
        }
      ]
    }
  ]
}

Отличие от обычного rank

sales rank dense_rank
100 1 1
90 2 2
90 2 2
80 4 3

dense_rank не создаёт пропусков.


row_number

Нумерация строк

{
  transform: [
    {
      window: [
        {
          op: "row_number",
          as: "row"
        }
      ]
    }
  ]
}

Результат:

row
1
2
3
4

Процентные ранги

percent_rank

{
  transform: [
    {
      window: [
        {
          op: "percent_rank",
          as: "percent"
        }
      ],

      sort: [
        {
          field: "score"
        }
      ]
    }
  ]
}

Интерпретация

Результат находится в диапазоне:

0 → 1

Используется для:

  • квантилей;
  • анализа распределения;
  • вычисления процентилей;
  • heatmap-аналитики.

Кумулятивные вычисления

Накопительное среднее

{
  transform: [
    {
      window: [
        {
          op: "mean",
          field: "sales",
          as: "running_avg"
        }
      ],

      sort: [
        {field: "date"}
      ],

      frame: [null, 0]
    }
  ]
}

Накопительный максимум

{
  transform: [
    {
      window: [
        {
          op: "max",
          field: "temperature",
          as: "max_so_far"
        }
      ],

      sort: [
        {field: "time"}
      ],

      frame: [null, 0]
    }
  ]
}

Доступ к соседним строкам

lag

Функция lag возвращает значение из предыдущей строки.

{
  transform: [
    {
      window: [
        {
          op: "lag",
          field: "price",
          as: "prev_price"
        }
      ],

      sort: [
        {field: "date"}
      ]
    }
  ]
}

Вычисление изменения между строками

{
  transform: [
    {
      window: [
        {
          op: "lag",
          field: "price",
          as: "prev_price"
        }
      ],

      sort: [
        {field: "date"}
      ]
    },

    {
      calculate: "datum.price - datum.prev_price",
      as: "delta"
    }
  ]
}

lead

Доступ к следующей строке

{
  transform: [
    {
      window: [
        {
          op: "lead",
          field: "price",
          as: "next_price"
        }
      ],

      sort: [
        {field: "date"}
      ]
    }
  ]
}

Использование groupby

Независимые окна по категориям

{
  transform: [
    {
      window: [
        {
          op: "sum",
          field: "sales",
          as: "category_total"
        }
      ],

      groupby: ["category"],

      sort: [
        {field: "date"}
      ],

      frame: [null, 0]
    }
  ]
}

Что делает groupby

Без groupby окно строится по всему набору данных.

С groupby:

  • каждая категория получает собственное окно;
  • вычисления не пересекаются между группами.

Аналог SQL:

PARTITION BY category

Сортировка внутри окна

Ключ sort

sort: [
  {
    field: "date",
    order: "ascending"
  }
]

Без сортировки:

  • ранги становятся непредсказуемыми;
  • накопительные вычисления теряют смысл;
  • lag и lead работают неправильно.

Несколько вычислений одновременно

Комбинированное окно

{
  transform: [
    {
      window: [
        {
          op: "sum",
          field: "sales",
          as: "running_sum"
        },

        {
          op: "mean",
          field: "sales",
          as: "running_avg"
        },

        {
          op: "rank",
          as: "sales_rank"
        }
      ],

      sort: [
        {
          field: "sales",
          order: "descending"
        }
      ],

      frame: [null, 0]
    }
  ]
}

Rolling statistics

Скользящая медиана

{
  transform: [
    {
      window: [
        {
          op: "median",
          field: "value",
          as: "rolling_median"
        }
      ],

      sort: [
        {field: "date"}
      ],

      frame: [-3, 0]
    }
  ]
}

Скользящее стандартное отклонение

{
  transform: [
    {
      window: [
        {
          op: "stdev",
          field: "value",
          as: "rolling_std"
        }
      ],

      sort: [
        {field: "date"}
      ],

      frame: [-5, 0]
    }
  ]
}

Нормализация через window

Доля строки от общего объёма

{
  transform: [
    {
      window: [
        {
          op: "sum",
          field: "sales",
          as: "total_sales"
        }
      ],

      frame: [null, null]
    },

    {
      calculate: "datum.sales / datum.total_sales",
      as: "percent"
    }
  ]
}

Полное окно

frame: [null, null]

Означает:

использовать все строки набора.


Вычисление процентов накопления

Running percentage

{
  transform: [
    {
      window: [
        {
          op: "sum",
          field: "sales",
          as: "running_sum"
        },

        {
          op: "sum",
          field: "sales",
          as: "total_sum"
        }
      ],

      sort: [
        {field: "date"}
      ],

      frame: [null, 0]
    },

    {
      joinaggregate: [
        {
          op: "sum",
          field: "sales",
          as: "grand_total"
        }
      ]
    },

    {
      calculate: "datum.running_sum / datum.grand_total",
      as: "running_percent"
    }
  ]
}

Разница между window и joinaggregate

joinaggregate

{
  joinaggregate: [
    {
      op: "mean",
      field: "value",
      as: "avg"
    }
  ]
}

Вычисляет агрегат по всей группе и копирует его в каждую строку.


window

{
  window: [
    {
      op: "mean",
      field: "value",
      as: "rolling_avg"
    }
  ],

  frame: [-2, 0]
}

Вычисляет агрегат относительно текущей позиции.


Практический пример: анализ временного ряда

Скользящее среднее + отклонение

const spec = {
  data: {
    url: "data/stocks.csv"
  },

  transform: [
    {
      window: [
        {
          op: "mean",
          field: "price",
          as: "avg_price"
        },

        {
          op: "stdev",
          field: "price",
          as: "std_price"
        }
      ],

      sort: [
        {field: "date"}
      ],

      frame: [-10, 0]
    },

    {
      calculate: "datum.price - datum.avg_price",
      as: "deviation"
    }
  ],

  layer: [
    {
      mark: "line",

      encoding: {
        x: {field: "date", type: "temporal"},
        y: {field: "price", type: "quantitative"}
      }
    },

    {
      mark: {
        type: "line",
        color: "red"
      },

      encoding: {
        x: {field: "date", type: "temporal"},
        y: {field: "avg_price", type: "quantitative"}
      }
    }
  ]
};

Поддерживаемые операции window

Агрегаты

Операция Назначение
sum сумма
mean среднее
median медиана
min минимум
max максимум
count количество
variance дисперсия
stdev стандартное отклонение

Ранговые функции

Операция Назначение
rank обычный ранг
dense_rank плотный ранг
row_number номер строки
percent_rank процентный ранг
cume_dist накопительное распределение

Навигационные функции

Операция Назначение
lag предыдущая строка
lead следующая строка
first_value первое значение окна
last_value последнее значение окна
nth_value N-е значение

Особенности производительности

window — одна из наиболее тяжёлых трансформаций Vega-Lite.

Причины:

  • требуется сортировка;
  • вычисления выполняются для каждой строки;
  • большие окна требуют повторных проходов;
  • группировки увеличивают объём промежуточных данных.

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

Отсутствие sort

window: [...]

без:

sort: [...]

часто приводит к неправильным результатам.


Неверный frame

frame: [0, 0]

Такое окно содержит только текущую строку.

Скользящего вычисления не будет.


Использование строк вместо чисел

Ошибка:

frame: ["-1", "1"]

Правильно:

frame: [-1, 1]

Визуальные сценарии применения

Где особенно полезен window

Финансовые графики

  • moving average;
  • cumulative return;
  • rolling volatility.

BI-аналитика

  • ranking;
  • top-N;
  • percent contribution.

Мониторинг

  • скользящие метрики;
  • аномалии;
  • сглаживание шумов.

Data science

  • feature engineering;
  • rolling statistics;
  • временные признаки.

Комбинирование с другими transform

filter

{
  filter: "datum.rank <= 10"
}

Позволяет реализовать Top-N после ранжирования.


calculate

{
  calculate: "datum.value / datum.avg",
  as: "normalized"
}

Используется после оконных вычислений.


fold

Часто применяется перед window для подготовки широких таблиц к временной аналитике.


Полный пример Top-N по категориям

const spec = {
  data: {
    values: [
      {category: "A", product: "P1", sales: 100},
      {category: "A", product: "P2", sales: 80},
      {category: "A", product: "P3", sales: 60},

      {category: "B", product: "P4", sales: 120},
      {category: "B", product: "P5", sales: 110},
      {category: "B", product: "P6", sales: 50}
    ]
  },

  transform: [
    {
      window: [
        {
          op: "rank",
          as: "rank"
        }
      ],

      groupby: ["category"],

      sort: [
        {
          field: "sales",
          order: "descending"
        }
      ]
    },

    {
      filter: "datum.rank <= 2"
    }
  ],

  mark: "bar",

  encoding: {
    x: {field: "product"},
    y: {field: "sales"},
    color: {field: "category"}
  }
};

Семантика окон в Vega-Lite

window работает поверх уже подготовленного набора данных.

Порядок выполнения:

  1. filter
  2. calculate
  3. aggregate
  4. window
  5. кодирование (encoding)
  6. рендеринг

Из-за этого результаты оконных вычислений напрямую зависят от всех предыдущих трансформаций.