Трансформация heatmap

Heatmap (тепловая карта) — способ визуализации двумерных данных, при котором интенсивность цвета отражает числовое значение в определённой точке сетки. Такой тип графика особенно эффективен при анализе:

  • корреляций;
  • распределений;
  • активности по времени;
  • плотности данных;
  • статистических матриц;
  • логов и телеметрии;
  • матриц переходов;
  • производительности систем.

В Vega-Lite heatmap чаще всего строится с помощью mark rect, где:

  • ось x определяет колонку;
  • ось y определяет строку;
  • канал color определяет интенсивность.

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

{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",

  "data": {
    "values": [
      {"day": "Mon", "hour": 9, "value": 12},
      {"day": "Mon", "hour": 10, "value": 18},
      {"day": "Tue", "hour": 9, "value": 7}
    ]
  },

  "mark": "rect",

  "encoding": {
    "x": {"field": "hour", "type": "ordinal"},
    "y": {"field": "day", "type": "ordinal"},
    "color": {"field": "value", "type": "quantitative"}
  }
}

Архитектура heatmap

Heatmap в Vega-Lite состоит из нескольких компонентов:

Компонент Назначение
mark: rect Рисование ячеек
x Горизонтальная координата
y Вертикальная координата
color Интенсивность
scale Настройка цветовой шкалы
transform Подготовка и агрегация данных
tooltip Интерактивные подсказки

Трансформация данных для heatmap

На практике данные редко приходят в готовом виде. Vega-Lite предоставляет набор трансформаций:

  • aggregate
  • bin
  • calculate
  • filter
  • fold
  • pivot
  • joinaggregate
  • window
  • impute

Именно трансформации превращают сырые данные в структуру, пригодную для heatmap.


Aggregate-трансформация

Heatmap часто отображает агрегированные значения.

Например:

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

Пример агрегации

Исходные данные:

[
  {"hour": 9, "day": "Mon", "sales": 100},
  {"hour": 9, "day": "Mon", "sales": 200},
  {"hour": 10, "day": "Mon", "sales": 150}
]

Трансформация:

{
  "transform": [
    {
      "aggregate": [
        {
          "op": "sum",
          "field": "sales",
          "as": "totalSales"
        }
      ],
      "groupby": ["hour", "day"]
    }
  ]
}

Результат:

[
  {"hour": 9, "day": "Mon", "totalSales": 300},
  {"hour": 10, "day": "Mon", "totalSales": 150}
]

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

"color": {
  "field": "totalSales",
  "type": "quantitative"
}

Heatmap плотности

Heatmap часто используется как визуализация плотности распределения.

Для этого применяются:

  • bin
  • aggregate

Бинирование данных

{
  "transform": [
    {
      "bin": true,
      "field": "x",
      "as": "x_bin"
    },
    {
      "bin": true,
      "field": "y",
      "as": "y_bin"
    }
  ]
}

После бинирования значения группируются по диапазонам.

Например:

Исходное значение Bin
12 10–20
17 10–20
29 20–30

Построение density heatmap

{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",

  "data": {
    "url": "data/points.json"
  },

  "transform": [
    {
      "bin": {"maxbins": 20},
      "field": "x",
      "as": "xbin"
    },
    {
      "bin": {"maxbins": 20},
      "field": "y",
      "as": "ybin"
    },
    {
      "aggregate": [
        {
          "op": "count",
          "as": "count"
        }
      ],
      "groupby": ["xbin", "ybin"]
    }
  ],

  "mark": "rect",

  "encoding": {
    "x": {
      "field": "xbin",
      "type": "ordinal"
    },

    "y": {
      "field": "ybin",
      "type": "ordinal"
    },

    "color": {
      "field": "count",
      "type": "quantitative"
    }
  }
}

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

Трансформация calculate создаёт вычисляемые поля.

Это особенно важно при:

  • преобразовании времени;
  • категоризации;
  • вычислении индексов;
  • нормализации данных.

Пример вычисления периода суток

{
  "transform": [
    {
      "calculate": "datum.hour < 12 ? 'Morning' : 'Evening'",
      "as": "period"
    }
  ]
}

Теперь можно строить heatmap:

"encoding": {
  "x": {"field": "period"},
  "y": {"field": "day"},
  "color": {"aggregate": "count"}
}

Pivot-трансформация

Heatmap визуально напоминает таблицу.

Иногда данные хранятся в “длинном” формате:

[
  {"month": "Jan", "metric": "Sales", "value": 100},
  {"month": "Jan", "metric": "Profit", "value": 40}
]

Для матричного представления используется pivot.

Pivot-пример

{
  "transform": [
    {
      "pivot": "metric",
      "value": "value",
      "groupby": ["month"]
    }
  ]
}

Результат:

[
  {
    "month": "Jan",
    "Sales": 100,
    "Profit": 40
  }
]

Fold-трансформация

Обратная операция — fold.

Она превращает широкую таблицу в длинную.

Исходная структура:

[
  {
    "month": "Jan",
    "Sales": 100,
    "Profit": 40
  }
]

Fold:

{
  "transform": [
    {
      "fold": ["Sales", "Profit"]
    }
  ]
}

Результат:

[
  {
    "month": "Jan",
    "key": "Sales",
    "value": 100
  },
  {
    "month": "Jan",
    "key": "Profit",
    "value": 40
  }
]

Это удобно для построения универсальных heatmap.


Impute и заполнение пропусков

Heatmap должен отображать полную сетку.

Если некоторые комбинации отсутствуют, возникают “дыры”.

Трансформация impute создаёт недостающие значения.

Пример

{
  "transform": [
    {
      "impute": "value",
      "key": "hour",
      "groupby": ["day"],
      "value": 0
    }
  ]
}

Теперь отсутствующие часы получат значение 0.


Цветовые шкалы heatmap

Цвет — центральный элемент heatmap.

Настройка scale

"color": {
  "field": "value",
  "type": "quantitative",
  "scale": {
    "scheme": "viridis"
  }
}

Популярные color schemes

Scheme Особенности
viridis Универсальная
magma Тёмная палитра
inferno Контрастная
plasma Яркая
blues Монохром
reds Тепловая
greenblue Аналитическая

Дивергентные heatmap

Если данные содержат отрицательные и положительные значения:

"scale": {
  "scheme": "redblue",
  "domainMid": 0
}

Такая heatmap показывает:

  • отрицательные значения — синие;
  • положительные — красные;
  • ноль — нейтральный.

Нормализация heatmap

Часто значения нужно привести к диапазону.

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

{
  "transform": [
    {
      "joinaggregate": [
        {
          "op": "max",
          "field": "value",
          "as": "maxValue"
        }
      ]
    },
    {
      "calculate": "datum.value / datum.maxValue",
      "as": "normalized"
    }
  ]
}

Temporal heatmap

Один из наиболее распространённых вариантов — временная heatmap.

Например:

  • активность пользователей;
  • нагрузка CPU;
  • события по дням;
  • посещаемость.

Извлечение времени

{
  "transform": [
    {
      "timeUnit": "hours",
      "field": "timestamp",
      "as": "hour"
    },
    {
      "timeUnit": "day",
      "field": "timestamp",
      "as": "day"
    }
  ]
}

Heatmap активности

{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",

  "data": {
    "url": "logs.json"
  },

  "transform": [
    {
      "timeUnit": "hours",
      "field": "timestamp",
      "as": "hour"
    },
    {
      "timeUnit": "day",
      "field": "timestamp",
      "as": "day"
    },
    {
      "aggregate": [
        {
          "op": "count",
          "as": "events"
        }
      ],
      "groupby": ["hour", "day"]
    }
  ],

  "mark": "rect",

  "encoding": {
    "x": {
      "field": "hour",
      "type": "ordinal"
    },

    "y": {
      "field": "day",
      "type": "ordinal"
    },

    "color": {
      "field": "events",
      "type": "quantitative"
    }
  }
}

Layered heatmap

Heatmap можно комбинировать со слоями.

Например:

  • текст поверх ячеек;
  • контуры;
  • аннотации.

Heatmap + text

{
  "layer": [
    {
      "mark": "rect"
    },
    {
      "mark": {
        "type": "text",
        "baseline": "middle"
      },

      "encoding": {
        "text": {
          "field": "value"
        }
      }
    }
  ]
}

Условное форматирование

Heatmap поддерживает condition.

Пример

"color": {
  "condition": {
    "test": "datum.value > 80",
    "value": "red"
  },
  "value": "lightgray"
}

Window-трансформации

Heatmap может отображать ранги и скользящие показатели.

Ранжирование

{
  "transform": [
    {
      "window": [
        {
          "op": "rank",
          "as": "rank"
        }
      ],
      "sort": [
        {
          "field": "value",
          "order": "descending"
        }
      ]
    }
  ]
}

Correlation heatmap

Один из классических вариантов — матрица корреляций.

Данные:

A B C
A 1 0.4 -0.2
B 0.4 1 0.7
C -0.2 0.7 1

Преобразование в long format:

[
  {"x": "A", "y": "A", "corr": 1},
  {"x": "A", "y": "B", "corr": 0.4},
  {"x": "A", "y": "C", "corr": -0.2}
]

Heatmap:

{
  "mark": "rect",

  "encoding": {
    "x": {"field": "x"},
    "y": {"field": "y"},

    "color": {
      "field": "corr",
      "type": "quantitative",

      "scale": {
        "scheme": "redblue",
        "domain": [-1, 1]
      }
    }
  }
}

Оптимизация heatmap

При больших объёмах данных возникают проблемы:

  • перегрузка DOM;
  • медленный рендеринг;
  • переполнение памяти.

Практики оптимизации

1. Предварительная агрегация

Лучше агрегировать данные заранее.

Плохо:

aggregate count over millions rows

Хорошо:

pre-aggregated bins

2. Ограничение количества bins

{
  "bin": {
    "maxbins": 50
  }
}

3. Canvas renderer

vegaEmbed("#view", spec, {
  renderer: "canvas"
});

Canvas быстрее SVG при большом количестве ячеек.


Интерактивные heatmap

Vega-Lite поддерживает selections и parameters.


Hover-эффекты

{
  "params": [
    {
      "name": "hover",
      "select": {
        "type": "point",
        "on": "mouseover"
      }
    }
  ]
}

Подсветка ячеек

"opacity": {
  "condition": {
    "param": "hover",
    "value": 1
  },
  "value": 0.5
}

Tooltip

"tooltip": [
  {"field": "hour"},
  {"field": "day"},
  {"field": "value"}
]

Heatmap в Vega

Vega предоставляет более низкоуровневый контроль.

В отличие от Vega-Lite:

  • требуется ручное описание scales;
  • axes задаются отдельно;
  • signals управляют интерактивностью;
  • marks описываются явно.

Heatmap в чистом Vega

{
  "$schema": "https://vega.github.io/schema/vega/v5.json",

  "data": [
    {
      "name": "table",
      "values": [
        {"x": 1, "y": 1, "value": 10},
        {"x": 2, "y": 1, "value": 20}
      ]
    }
  ],

  "scales": [
    {
      "name": "xscale",
      "type": "band",
      "domain": {"data": "table", "field": "x"},
      "range": "width"
    },

    {
      "name": "yscale",
      "type": "band",
      "domain": {"data": "table", "field": "y"},
      "range": "height"
    },

    {
      "name": "color",
      "type": "linear",
      "domain": {"data": "table", "field": "value"},
      "range": {"scheme": "viridis"}
    }
  ],

  "marks": [
    {
      "type": "rect",

      "from": {
        "data": "table"
      },

      "encode": {
        "enter": {
          "x": {"scale": "xscale", "field": "x"},
          "y": {"scale": "yscale", "field": "y"},
          "width": {"scale": "xscale", "band": 1},
          "height": {"scale": "yscale", "band": 1},

          "fill": {
            "scale": "color",
            "field": "value"
          }
        }
      }
    }
  ]
}

Отличия Vega и Vega-Lite

Возможность Vega-Lite Vega
Простота Высокая Средняя
Автоматизация Есть Нет
Контроль Ограниченный Полный
Signals Частично Полностью
Сложная интерактивность Ограничена Максимальная

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

Неверный тип поля

Плохо:

"type": "nominal"

для числового color.

Правильно:

"type": "quantitative"

Слишком много bins

maxbins: 500

приводит к перегрузке.


Отсутствие impute

Heatmap с пропущенными значениями визуально ломается.


Некорректная шкала

Например:

"scheme": "reds"

для отрицательных значений.

Лучше:

"scheme": "redblue"

Комплексный пример heatmap

{
  "$schema": "https://vega.github.io/schema/vega-lite/v5.json",

  "data": {
    "url": "activity.json"
  },

  "transform": [
    {
      "timeUnit": "hours",
      "field": "timestamp",
      "as": "hour"
    },

    {
      "timeUnit": "day",
      "field": "timestamp",
      "as": "day"
    },

    {
      "aggregate": [
        {
          "op": "count",
          "as": "events"
        }
      ],

      "groupby": [
        "hour",
        "day"
      ]
    },

    {
      "impute": "events",
      "key": "hour",
      "groupby": ["day"],
      "value": 0
    }
  ],

  "mark": "rect",

  "encoding": {
    "x": {
      "field": "hour",
      "type": "ordinal"
    },

    "y": {
      "field": "day",
      "type": "ordinal"
    },

    "color": {
      "field": "events",
      "type": "quantitative",

      "scale": {
        "scheme": "viridis"
      }
    },

    "tooltip": [
      {"field": "day"},
      {"field": "hour"},
      {"field": "events"}
    ]
  },

  "config": {
    "view": {
      "stroke": null
    }
  }
}