В Evotrex мы создаем IoT-решения для интеллектуальных складов и используем Go для поддержки нескольких наших ключевых приложений и микросервисов. И хотя мы наслаждались преимуществами того, что большинство наших сервисов на одном языке, была одна область, где мы все еще оглядывались на определенные инструменты и плакали. И это была визуализация данных.

Создание объемных выходных данных (таких как трехмерные векторные поля со многими важными статистическими свойствами) означает, что у нас должны быть инструменты для их проверки. Имея в команде опытных программистов на Python, часто бывает непросто решить, хотим ли мы оставаться на плаву для конкретного инструмента анализа данных.

Вот тогда-то и появился гонум/сюжет. Gonum уже позволил нам воспроизвести некоторые из наших числовых расчетов из NumPy и специализированных утилит C++ в Go, однако нам отчаянно нужен был правильный способ визуализации наших наборов данных. Поначалу не очевидно, что gonum/plot поддерживает отображение 2D-данных, кроме плоттера Bubbles, который демонстрируется в вики: в коде есть еще два плоттера: Heatmap и Field. В этом первом посте я приведу пример того, как использовать первый.

Структура данных

Наша цель — визуализировать двумерный массив, который в данном случае будет иметь одно значение float64, связанное с каждой ячейкой. В следующем примере я буду использовать var dataset [][]float64 в качестве источника наших данных, которые мы хотим построить.

Как и во всех плоттерах gonum/plot, нам нужно хранить наши данные в структуре, которая удовлетворяет ожидаемому интерфейсу, чтобы плоттер мог их визуализировать. Базовой структурой данных для плоттера/тепловой карты является интерфейс GridXYZ, который имеет следующие методы:

  1. Dims() int: возвращает количество столбцов и строк нашего двумерного массива.
  2. Z(c, r int) float64: возвращает фактическое значение, хранящееся в позициях индексов c и r, например. dataset[c][r]
  3. X(c int) float64: возвращает значение, которое мы хотим использовать для оси X в столбце c.
  4. Y(r int) float64: возвращает значение, которое мы хотим использовать для оси Y в строке r.

Хотя Z(c, r int) float64, вероятно, очевидно, стоит потратить минуту, чтобы понять, что делают функции X и Y. В типичном сценарии вы рисуете данные, где координаты X и Y представляют количество, например. это данные ширины и высоты. Это зависит от вас, кто определяет этот набор данных, чтобы придать смысл целочисленным данным строки и столбца, что делается с помощью этих методов.

В дополнение к набору данных для построения необходимо определить цветовой диапазон, чтобы визуализировать диапазон значений. Пакет gonum.org/v1/plot/palette обеспечивает это очень хорошо, и вы можете найти полный набор палитр на выбор здесь: https://godoc.org/gonum.org/v1/plot/palette/brewer и здесь: https:/ /godoc.org/gonum.org/v1/plot/palette/moreland.

Хранение реальных данных

Давайте построим тепловую карту для сетки, которая представляет собой скалярное значение на физической сетке 0,5 м x 0,5 м, как карта высот. Сначала мы определяем нашу структуру хранения данных, которая будет соответствовать интерфейсу GridXYZ.

type plottable struct {
  grid [][]float64
  N int
  M int
  resolution float64
  minX float64
  minY float64
}

Как видите, из-за физической интерпретации базовой сетки мы должны включить эту структуру с соответствующими параметрами:

  1. Разрешение нашей сетки, в данном случае 0,5 (в этом примере мы предполагаем одинаковое разрешение в обоих направлениях X и Y)
  2. minX и minY представляют, какие физические координаты должна принимать ячейка 0, 0
  3. и N и M для хранения информации о ширине и высоте, перемещенных сюда в структуру в основном для удобства.

Реализация интерфейса

Наша структура plottable должна реализовывать интерфейс GridXYZ, чтобы мы могли в конечном итоге передать его в конструктор NewHeatMap():

func (p plottable) Dims() (c, r int) {
  return p.N, p.M
}
func (p plottable) X(c int) float64 {
  return p.minX + float64(c)*p.resolution
}
func (p plottable) Y(r int) float64 {
  return p.minY + float64(r)*p.resolution
}
func (p plottable) Z(c, r int) float64 {
  return p.grid[c][r]
}

Таким образом, индексы X и Y теперь связаны с правильными значимыми значениями на нашем графике.

Заговор

Окончательный сюжет теперь прост. Создайте экземпляр структуры plottable, заполните поля grid, N и M (или используйте len()) и вызовите плоттер как обычно. Если наш исходный набор данных был var dataset [][]float64, то следующий код создаст хорошую тепловую карту, которая начинается с (-0.5; 42.0) и имеет разрешение 0,5:

plotData := plottable{
  grid: dataset,
  N: cols,
  M: rows,
  minX: -0.5,
  minY: 42.0,
  resolution: 0.5,
}
pal := moreland.SmoothBlueRed().Palette(255)
hm := plotter.NewHeatMap(plotData, pal)

Наслаждаться!