Гистограмма и плотностная кривая

Гистограмма в D3.js строится на основе преобразования непрерывных данных в дискретные интервалы (бины). Основной инструмент — d3.bin, который принимает массив чисел и возвращает набор корзин, каждая из которых содержит элементы, попавшие в соответствующий диапазон.

Ключевая особенность гистограммы в D3 заключается в том, что библиотека не строит визуализацию автоматически — она формирует структуру данных, которую затем можно отрисовать через SVG.

Основные этапы:

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

Подготовка данных и разбиение на интервалы

Функция d3.bin() создаёт генератор разбиения:

const binGenerator = d3.bin()
  .domain([0, 100])
  .thresholds(10);

Параметры:

  • domain — диапазон значений;
  • thresholds — количество интервалов или массив границ.

Применение:

const bins = binGenerator(data);

Результат представляет собой массив объектов:

[
  { x0: 0, x1: 10, length: 5, ... },
  { x0: 10, x1: 20, length: 12, ... }
]

Каждый элемент содержит:

  • x0 — левая граница;
  • x1 — правая граница;
  • массив значений внутри интервала.

Построение шкал и осей

Для корректного отображения требуется линейная шкала:

const xScale = d3.scaleLinear()
  .domain([0, 100])
  .range([0, width]);

Шкала Y зависит от максимальной частоты:

const yScale = d3.scaleLinear()
  .domain([0, d3.max(bins, d => d.length)])
  .range([height, 0]);

Оси:

const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);

Отрисовка гистограммы через SVG

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

svg.selectAll("rect")
  .data(bins)
  .enter()
  .append("rect")
  .attr("x", d => xScale(d.x0))
  .attr("y", d => yScale(d.length))
  .attr("width", d => xScale(d.x1) - xScale(d.x0))
  .attr("height", d => height - yScale(d.length))
  .attr("fill", "steelblue");

Смысл отображения:

  • ширина соответствует интервалу;
  • высота — частоте элементов.

Нормализация гистограммы

Для сопоставления с плотностными функциями используется нормализация:

[ = ]

Это позволяет сравнивать гистограмму с кривыми плотности.

В D3:

const total = data.length;

bins.forEach(bin => {
  bin.density = bin.length / total;
});

Плотностная кривая: концепция и математическая основа

Плотностная кривая (Kernel Density Estimation, KDE) представляет непрерывную аппроксимацию распределения данных.

Общая формула KDE:

f(x)=_{i=1}^{n}K()

Где:

  • ( n ) — количество наблюдений;
  • ( h ) — bandwidth (ширина сглаживания);
  • ( K ) — ядро (kernel function).

На практике часто используется гауссово ядро:

K(u)=e^{-}


Реализация KDE в D3.js

D3 не включает KDE как готовую функцию, поэтому используется пользовательская реализация.

Гауссово ядро

function kernelGaussian(k) {
  return function(v) {
    return k(v) / Math.sqrt(2 * Math.PI);
  };
}

Функция оценки плотности

function kernelDensityEstimator(kernel, X) {
  return function(V) {
    return X.map(x => [
      x,
      d3.mean(V, v => kernel(x - v))
    ]);
  };
}

Подготовка точек для кривой

Создаётся сетка значений:

const xDensity = d3.range(0, 100, 1);

Выбирается bandwidth:

const bandwidth = 7;
const kernel = kernelGaussian(bandwidth);

Вычисление плотности:

const density = kernelDensityEstimator(kernel, xDensity)(data);

Результат:

[
  [x0, y0],
  [x1, y1],
  ...
]

Построение линии плотности

Используется d3.line():

const line = d3.line()
  .x(d => xScale(d[0]))
  .y(d => yScale(d[1]))
  .curve(d3.curveBasis);

Отрисовка:

svg.append("path")
  .datum(density)
  .attr("fill", "none")
  .attr("stroke", "red")
  .attr("stroke-width", 2)
  .attr("d", line);

Совмещение гистограммы и плотностной кривой

Для корректного наложения требуется единая шкала Y. Обычно применяют нормированную гистограмму.

Изменённая шкала:

yScale.domain([0, d3.max(density, d => d[1])]);

Гистограмма масштабируется к плотности:

svg.selectAll("rect")
  .attr("height", d => height - yScale(d.density))
  .attr("y", d => yScale(d.density));

Управление bandwidth и его влияние

Bandwidth определяет степень сглаживания:

  • малый bandwidth → резкие пики, шум;
  • большой bandwidth → чрезмерное сглаживание.

Формально влияние видно в ядре:

Увеличение ( h ) уменьшает вклад локальных отклонений, расширяя вклад соседних точек.


Синхронизация масштабов и осей

При совмещении графиков критично обеспечить:

  • одинаковый domain по X;
  • совместимую шкалу Y;
  • единый origin SVG координат.
const xScale = d3.scaleLinear()
  .domain(d3.extent(data))
  .range([0, width]);

Оптимизация вычислений KDE

При больших наборах данных вычисления становятся дорогими:

  • сложность O(n·m), где m — число точек сетки;
  • оптимизация через уменьшение resolution;
  • предварительное сглаживание данных;
  • использование typed arrays.

Ускорение достигается также за счёт уменьшения количества точек:

const xDensity = d3.range(min, max, 2);

Альтернативный подход: нормализованная гистограмма как приближение плотности

Гистограмма может служить приближением KDE при:

  • увеличении числа бинов;
  • нормализации по площади.

Формула нормализации:

_{i} f_i x = 1

Где:

  • ( f_i ) — плотность в бине;
  • ( x ) — ширина интервала.

Практическая структура визуализации

Типовая архитектура SVG-графика:

const svg = d3.select("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform", `translate(${margin.left},${margin.top})`);

Слои:

  • оси;
  • гистограмма;
  • KDE-кривая;
  • вспомогательная сетка.

Согласование визуальной интерпретации

Гистограмма показывает дискретную частотность, KDE — непрерывную плотность. При наложении важно:

  • избегать визуального доминирования одного слоя;
  • использовать прозрачность для прямоугольников;
  • сохранять читаемость кривой.
.attr("fill-opacity", 0.5);

Геометрическая интерпретация сглаживания

KDE можно интерпретировать как свёртку:

f(x)=(K_h * )(x)

Где:

  • ( ) — эмпирическое распределение;
  • ( K_h ) — масштабированное ядро.

Связь с вероятностными моделями

Плотностная кривая приближается к истинной функции распределения при:

  • росте выборки;
  • оптимальном выборе bandwidth.

Формально:

_{n} f_n(x)=f(x)