덴드로그램

Dendrogram

항목들이 어떤 순서로, 얼마나 다를 때 짝지어 묶이는지를 나무 모양 연결선의 높이로 보여주는 차트.

다른 이름: Cluster treeHierarchical clustering tree
···
html
<div class="viz">
  <svg id="svg"></svg>
</div>
css
body{display:block}
.viz{position:relative;width:100%;height:100%}
svg{display:block;width:100%;height:100%;overflow:visible}
js
var svg = document.getElementById('svg');
var NS = 'http://www.w3.org/2000/svg';
function se(t){ return document.createElementNS(NS,t); }
function sa(e,o){ for (var k in o) e.setAttribute(k,o[k]); }
function R(a,b){ return a + Math.random()*(b-a); }

var LEAVES = ['A','B','C','D','E','F','G','H'];
var cur = { h1:[30,30,30,30], h2:[55,55], h3:75 };
var W=300, H=200;

function measure(){
  var r = svg.getBoundingClientRect();
  W = Math.max(r.width,10); H = Math.max(r.height,10);
  sa(svg,{ viewBox: '0 0 ' + W + ' ' + H });
}

function draw(){
  svg.innerHTML = '';
  var padL=14,padR=14,padT=14,padB=20;
  var pw=W-padL-padR, ph=H-padT-padB;
  var n=LEAVES.length;
  var leafX = LEAVES.map(function(_,i){ return padL + pw*(i+0.5)/n; });
  var baseline = padT+ph;
  function yFor(hpct){ return baseline - (hpct/100)*ph; }

  LEAVES.forEach(function(name,i){
    var t = se('text');
    sa(t,{ x:leafX[i], y:baseline+13, 'text-anchor':'middle', fill:'var(--muted)', 'font-size':Math.max(7,Math.min(10,W/50)), 'font-weight':600 });
    t.textContent = name;
    svg.appendChild(t);
  });

  function link(xA,yA,xB,yB,yP){
    var d = 'M'+xA.toFixed(1)+','+yA.toFixed(1)+'V'+yP.toFixed(1)+'H'+xB.toFixed(1)+'V'+yB.toFixed(1);
    var p = se('path');
    sa(p,{ d:d, fill:'none', stroke:'var(--accent)', 'stroke-width':1.6, 'stroke-linecap':'round' });
    svg.appendChild(p);
  }

  var level1X=[];
  for (var k=0;k<4;k++){
    var a=2*k, b=2*k+1;
    var yP = yFor(cur.h1[k]);
    link(leafX[a], baseline, leafX[b], baseline, yP);
    level1X.push((leafX[a]+leafX[b])/2);
  }
  var level2X=[];
  for (var k2=0;k2<2;k2++){
    var a2=2*k2, b2=2*k2+1;
    var yA = yFor(cur.h1[a2]), yB = yFor(cur.h1[b2]);
    var yP2 = yFor(cur.h2[k2]);
    link(level1X[a2], yA, level1X[b2], yB, yP2);
    level2X.push((level1X[a2]+level1X[b2])/2);
  }
  var yA3 = yFor(cur.h2[0]), yB3 = yFor(cur.h2[1]), yP3 = yFor(cur.h3);
  link(level2X[0], yA3, level2X[1], yB3, yP3);
}

function animate(target){
  var fromH1 = cur.h1.slice(), fromH2 = cur.h2.slice(), fromH3 = cur.h3;
  var t0 = performance.now();
  function step(t){
    var p = Math.min(1,(t-t0)/750);
    var e = 1-Math.pow(1-p,3);
    cur = {
      h1: fromH1.map(function(v,i){ return v+(target.h1[i]-v)*e; }),
      h2: fromH2.map(function(v,i){ return v+(target.h2[i]-v)*e; }),
      h3: fromH3+(target.h3-fromH3)*e
    };
    draw();
    if (p<1) requestAnimationFrame(step);
  }
  requestAnimationFrame(step);
}

function nextTarget(){
  return { h1: [R(18,38),R(18,38),R(18,38),R(18,38)], h2: [R(45,62),R(45,62)], h3: R(70,90) };
}

new ResizeObserver(function(){ measure(); draw(); }).observe(svg);
measure();
draw();
animate(nextTarget());
setInterval(function(){ animate(nextTarget()); }, 3800);

계층적 군집화(hierarchical clustering)의 결과를 그대로 그린 그림입니다. 잎(leaf)은 원본 항목이고, 두 가지가 합쳐지는 지점(높이)은 "그 둘을 하나로 묶기로 한 시점의 비유사도"를 나타냅니다 — 낮은 높이에서 일찍 합쳐질수록 서로 비슷하고, 높은 높이에서야 합쳐지면 그만큼 다르다는 뜻입니다.

가장 자주 오해하는 지점은 "가로축 순서"입니다. 잎의 좌우 나열 순서는 나무를 그리기 위해 임의로 고른 하나의 배치일 뿐, 옆에 붙어 있다고 더 비슷한 게 아닙니다 — 실제 유사도는 오직 두 항목이 만나는 병합 높이로만 판단해야 합니다. 같은 나무라도 가지를 좌우로 뒤집어 그리면 잎의 순서가 완전히 달라질 수 있는데, 그래도 나무가 담은 정보(어느 높이에서 무엇이 합쳐지는가)는 그대로입니다.

군집 개수를 정하려고 나무를 특정 높이에서 수평으로 자를 때, 그 높이를 아무 데서나 고르면 안 됩니다 — 병합 높이들 사이에 유난히 큰 간격(gap)이 있는 지점을 찾아 그 틈 사이에서 잘라야 "우연이 아니라 실제로 갈라지는 지점"을 자른 것이 됩니다.

언제 쓰나

항목들을 계층적으로 군집화한 결과나 그 유사도 관계를 보여주고 싶을 때. 항목이 매우 많으면 잎이 겹쳐 읽기 어려워지므로 일부만 추리거나 다른 군집 표현을 쓰세요.