Box plot и violin plot

D3.js

Box plot (ящик с усами) представляет собой компактное отображение распределения данных через квартильную структуру. Основная цель — визуализация медианы, разброса и потенциальных выбросов без построения полной гистограммы.

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

  • минимальное значение (без учёта выбросов)
  • первый квартиль Q1 (25%)
  • медиана Q2 (50%)
  • третий квартиль Q3 (75%)
  • максимальное значение (без учёта выбросов)

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

IQR = Q3 − Q1

На основе IQR определяются границы выбросов:

  • нижняя граница: Q1 − 1.5 × IQR
  • верхняя граница: Q3 + 1.5 × IQR

Все значения за пределами этих границ отображаются как отдельные точки.

Визуальная структура box plot включает:

  • прямоугольник между Q1 и Q3
  • линию медианы внутри прямоугольника
  • «усы», тянущиеся до крайних значений
  • точки выбросов

Построение box plot в D3.js

Реализация начинается с расчёта статистических характеристик массива данных.

const data = [12, 15, 13, 20, 22, 18, 19, 24, 30, 28];

data.sort(d3.ascending);

const q1 = d3.quantile(data, 0.25);
const median = d3.quantile(data, 0.5);
const q3 = d3.quantile(data, 0.75);

const iqr = q3 - q1;

const min = d3.min(data);
const max = d3.max(data);

const lowerBound = q1 - 1.5 * iqr;
const upperBound = q3 + 1.5 * iqr;

const outliers = data.filter(d => d < lowerBound || d > upperBound);

Масштабирование выполняется через линейную шкалу:

const y = d3.scaleLinear()
  .domain([min, max])
  .range([400, 0]);

Геометрия box plot формируется из SVG-примитивов:

const svg = d3.select("svg");

svg.append("rect")
  .attr("x", 100)
  .attr("y", y(q3))
  .attr("width", 50)
  .attr("height", y(q1) - y(q3))
  .attr("fill", "#cce5ff")
  .attr("stroke", "#333");

svg.append("line")
  .attr("x1", 100)
  .attr("x2", 150)
  .attr("y1", y(median))
  .attr("y2", y(median))
  .attr("stroke", "black");

Усы строятся как вертикальные линии:

svg.append("line")
  .attr("x1", 125)
  .attr("x2", 125)
  .attr("y1", y(lowerBound))
  .attr("y2", y(q1))
  .attr("stroke", "black");

svg.append("line")
  .attr("x1", 125)
  .attr("x2", 125)
  .attr("y1", y(q3))
  .attr("y2", y(upperBound))
  .attr("stroke", "black");

Выбросы отображаются точками:

svg.selectAll("circle")
  .data(outliers)
  .enter()
  .append("circle")
  .attr("cx", 125)
  .attr("cy", d => y(d))
  .attr("r", 3)
  .attr("fill", "red");

Violin plot: плотность распределения и непрерывная форма

Violin plot расширяет box plot, заменяя дискретные квартильные характеристики непрерывной оценкой плотности распределения. Основой служит kernel density estimation (KDE).

Функция плотности описывает вероятность значений в каждой точке диапазона:

  • высокая плотность — «широкая» часть виолончели
  • низкая плотность — «узкая» часть

Форма симметрична относительно центральной оси.


Оценка плотности (KDE) в D3.js

Для построения violin plot используется d3.contourDensity или ручная реализация KDE.

const kde = kernelDensityEstimator(kernelEpanechnikov(7), y.ticks(40));

const density = kde(data);

Ядровая функция:

function kernelEpanechnikov(k) {
  return function(v) {
    return Math.abs(v /= k) <= 1 ? 0.75 * (1 - v * v) / k : 0;
  };
}

Оценка плотности:

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

Построение violin plot через area path

Основная геометрия строится как зеркальная area-диаграмма.

const x = d3.scaleLinear()
  .domain([0, d3.max(density, d => d[1])])
  .range([0, 50]);

const area = d3.area()
  .x0(d => -x(d[1]))
  .x1(d => x(d[1]))
  .y(d => y(d[0]))
  .curve(d3.curveCatmullRom);

Отрисовка:

svg.append("path")
  .datum(density)
  .attr("d", area)
  .attr("fill", "#a6cee3")
  .attr("stroke", "#333");

Ось симметрии:

svg.append("line")
  .attr("x1", 0)
  .attr("x2", 0)
  .attr("y1", y(d3.min(data)))
  .attr("y2", y(d3.max(data)))
  .attr("stroke", "black");

Сравнение box plot и violin plot через геометрию данных

Box plot опирается на дискретное статистическое резюмирование:

  • устойчив к шуму
  • компактен
  • теряет форму распределения

Violin plot опирается на непрерывную плотность:

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

В терминах D3.js различие выражается в уровне абстракции:

  • box plot использует quantile, min, max
  • violin plot использует kernelDensityEstimator и d3.area

Интеграция box plot и violin plot в единую визуализацию

Комбинированный подход часто реализуется наложением элементов:

// violin shape
svg.append("path")
  .datum(density)
  .attr("d", area)
  .attr("fill", "#e0f3f8");

// box plot поверх
svg.append("rect")
  .attr("x", -10)
  .attr("y", y(q3))
  .attr("width", 20)
  .attr("height", y(q1) - y(q3))
  .attr("fill", "#fc8d62");

Медиана накладывается поверх плотности:

svg.append("line")
  .attr("x1", -10)
  .attr("x2", 10)
  .attr("y1", y(median))
  .attr("y2", y(median))
  .attr("stroke", "black");

Масштабирование и нормализация данных

Корректная визуализация требует согласованного масштабирования:

  • линейная шкала применяется к значениям данных
  • плотность нормализуется до диапазона ширины графика
  • симметрия violin plot достигается зеркальным отображением значений плотности

Типичная ошибка заключается в несогласованности шкал y и x, что приводит к искажению формы распределения.


Особенности интерактивной реализации в D3.js

В интерактивных версиях применяются:

  • динамическое обновление KDE при фильтрации данных
  • переходы (transition) при изменении выборки
  • tooltips для отображения квартилей и плотности
  • связывание нескольких violin plots через общую шкалу

Пример обновления:

svg.selectAll("path")
  .data([density])
  .join("path")
  .transition()
  .duration(600)
  .attr("d", area);

Интерпретационные модели распределений

Box plot формализует распределение через устойчивые статистики, минимизируя влияние крайних значений. Violin plot добавляет оценку формы распределения, позволяя различать:

  • нормальные распределения (симметричная форма)
  • бимодальные распределения (две «выпуклости»)
  • скошенные распределения (асимметрия плотности)

В D3.js эти различия выражаются исключительно через трансформации данных, без изменения базовой SVG-структуры, что подчёркивает декларативный характер библиотеки.