Автоматический выбор масштабов (линейных, степенных, логарифмических) для условных обозначений

#algorithm #math #d3.js #graph #data-science

#алгоритм #математика #d3.js #График #наука о данных

Вопрос:

Это скорее вопрос науки о данных, чем d3.js но я думаю, что другие люди, должно быть, тоже думали об этом.

У меня есть набор данных с ежедневными значениями обновления. Набор также содержит исторические данные за все или несколько дней. В основном так:

 {data: [
    "ItemA" : {
        "24.10.2020" : 123,
        "25.10.2020" : 134,
        "26.10.2020" : 145,
        "27.10.2020" : 156,
        "28.10.2020" : 167      
    },
    "ItemB" : {
        "24.10.2020" : 123,
        "25.10.2020" : 234,
        "26.10.2020" : 456,
        "27.10.2020" : 567,
        "28.10.2020" : 678      
    },
    "ItemC" : {
        "24.10.2020" : 123,
        "25.10.2020" : 136,
        "26.10.2020" : 149,
        "27.10.2020" : 152,
        "26.10.2020" : 165,
        "28.10.2020" : 178      
    },
]}
  

Как вы видите, itemB — это выброс, значения которого растут намного быстрее, чем у других элементов.

Настройка масштаба для условных обозначений для отображения роста с течением времени была простой, если значения росли почти с одинаковой скоростью. A d3.scaleLinear().domain([0, upperBoundValues]) был в порядке. В то время как значения росли, пользователь все еще мог различать меньшие и более высокие значения.

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

Вручную я бы переключился на шкалу мощности или логарифмическую шкалу. Особенно потому, что мне пришлось бы ежедневно проверять, что происходит.

Я бы предпочел иметь функцию, которая проверяет такие события и автоматически переключает масштаб, если значения. Еще лучше было бы неплохо выбрать подходящий масштаб (в основном выбирая наиболее подходящий показатель степенной шкалы и / или базы для логарифмических шкал). Мне не нужна логарифмическая шкала base10, если мои значения никогда не вырастут до сверхбольших цифр.

Есть ли какая-либо функция / алгоритм аппроксимации, которую я могу реализовать, которая упрощает выбор или возвращает значение, при котором я мог бы выбрать масштаб (например: 0 … 1 -> Линейный // 1 … n -> Log)

Комментарии:

1. Обычно я бы сказал, что вы хотите видеть значения относительно друг друга. И если это означает, что одна строка на линейной диаграмме больше, то, по крайней мере, перспектива та же. Но звучит так, как будто вы хотите знать тенденцию , чтобы вы могли видеть, какая линия растет быстрее, чем другие. В таком случае, почему бы не избежать этой проблемы и не использовать индексацию: для каждой строки первое значение равно 100%, а все последующие точки относятся к этому значению. Вы все равно можете видеть тенденцию, и вам не нужно менять масштаб, потому что он сбрасывается каждый день до 100%

2. Тенденция играет в этом роль, но имеет значение только с точки зрения алгоритма. Фактический вариант использования — это карта с несколькими областями (ItemA …), Которые должны быть окрашены в соответствии с имеющимся у них значением. [На карте есть ввод временного слайдера для временных рядов] Если у меня есть один выброс, который просто отличается от других областей, наступит момент, когда у меня будет много черных областей и одна красная (чтобы продолжить мой турбо-пример). Я бы хотел по-прежнему отличать другие области друг от друга. Также может быть интересно сопоставить это с пороговым масштабом с пороговыми значениями, которые имеют смысл…

3. Когда вы начали о карте, я подумал то же самое. Я бы устанавливал пороговые значения не регулярно, а с использованием 1-9 децилей. Таким образом, ваш пример turbo будет красным, но остальные все равно будут дифференцируемыми

4. Я использовал нечто подобное на карте Covid, которую я сделал с заболеваемостью. Мне приходилось отображать разные допустимые пороговые значения (0,20, 35, 50, верхний предел). Когда-то несколько округов прошли отметку 50, но стали больше. Я противопоставил этому новые пороговые значения для 100, 250 и верхней границы. Но заболеваемость covid будет расти и падать математически более предсказуемым образом.

5. С этим другим масштабом я не могу делать никаких прогнозов. Единственное, что я могу знать, это то, что будет несколько, которые могут иметь высокие значения, но большинство значений похожи друг на друга. d3.scaleQuantile() с 1-9-м децилями выглядит многообещающе, но, вероятно, потерпит неудачу, если один из выбросов выберет логарифмический, я думаю. Я попробую квантировать и вернусь к этому. — Есть ли способ получить «хорошие» шаги для них? (Например, «100» предпочтительнее «99» в качестве порогового шага)

Ответ №1:

В качестве продолжения моего комментария рассмотрим следующее, в котором используются scaleThreshold децильные значения. Я нарисовал 5 кругов, и значение первого увеличивается намного быстрее, чем у остальных. Но вы все равно увидите достаточную разницу между ними из-за порогового масштаба.

 const data = d3.range(5).map(i => {
  let values = [1];
  d3.range(50).forEach(() => {
    // Either a multiplier [0.9, 1.2], or (if it's the first one, [1.2, 1.5]
    const multiplier = (i === 0 ? 1.2 : 0.9)   (Math.random() * 0.3);
    values.push(values[values.length - 1] * multiplier);
  });

  return {
    x: 50   i * 100,
    y: 50,
    r: 40,
    values: values,
  };
});

const allValues = data.map(d => d.values).flat().sort((a, b) => a - b);
const colours = d3.scaleThreshold()
  .domain([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9].map(i => d3.quantile(allValues, i)))
  .range(d3.schemeSpectral[10]);

const svg = d3.select("svg")
  .attr("width", 500);

const colourbar = svg.append("g");

colourbar
  .selectAll("rect")
  .data(colours.range())
  .enter()
  .append("rect")
  .attr("x", (d, i) => i * 50)
  .attr("y", 100)
  .attr("height", 20)
  .attr("width", 50)
  .attr("fill", d => d);

colourbar
  .selectAll("text")
  .data(colours.domain())
  .enter()
  .append("text")
  .attr("x", (d, i) => (i   1) * 50)
  .attr("y", 135)
  .text(d => d.toFixed(1));

const circles = svg.append("g")
  .selectAll("circle")
  .data(data)
  .enter()
  .append("circle")
  .attr("cx", d => d.x)
  .attr("cy", d => d.y)
  .attr("r", d => d.r);

const labels = svg
  .append("g")
  .selectAll("text")
  .data(data)
  .enter()
  .append("text")
  .style("fill", "white")
  .attr("dy", 5)
  .attr("x", d => d.x)
  .attr("y", d => d.y);

let counter = -1;

function colour() {
  counter = (counter   1) % 50;
  labels.text(d => d.values[counter].toFixed(1));
  circles
    .transition()
    .duration(1000)
    .ease(d3.easeLinear)
    .attr("fill", d => colours(d.values[counter]))
    .filter((d, i) => i === 0)
    .on("end", colour);
}

colour();  
 text {
  text-anchor: middle;
}  
 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>  

Комментарии:

1. Вау, очень аккуратно. Я попробую это и дам вам знать, насколько хорошо это работает для моего варианта использования. Даже если это не так, я думаю, это может оказаться очень полезным.

2. Я добавил цветную полосу к примеру, я думаю, это может помочь лучше проиллюстрировать его