히스토그램

Histogram

연속형 값을 구간(bin)으로 나눠 각 구간에 몇 개가 속하는지 세는 차트. "값이 어떻게 분포하는가"에 답합니다.

다른 이름: Frequency distribution
···
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}
.axis{stroke:var(--line);stroke-width:1}
.tick{fill:var(--muted);font-weight:600}
.minor{display:none}
@media (min-width:460px){.minor{display:inline}}
.bar{fill:var(--accent)}
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 BINS = 12;
var cur = new Array(BINS).fill(0);
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 bellTarget(){
  var peak = R(3,BINS-4);
  var out = [];
  for (var i=0;i<BINS;i++){
    var d = (i-peak)/2.1;
    out.push(Math.max(4, 96*Math.exp(-d*d) + R(-6,6)));
  }
  return out;
}

function draw(){
  svg.innerHTML = '';
  var padL=24, padR=8, padT=14, padB=20;
  var pw=W-padL-padR, ph=H-padT-padB;
  var bw = pw/BINS;
  var fs = Math.max(8, Math.min(11, W/85));
  var base = se('line');
  sa(base,{x1:padL,y1:H-padB,x2:W-padR,y2:H-padB,class:'axis'});
  svg.appendChild(base);
  cur.forEach(function(v,i){
    var h = (v/100)*ph;
    var x = padL+i*bw;
    var y = H-padB-h;
    var rect = se('rect');
    sa(rect,{x:x,y:y,width:Math.max(bw-1,0),height:Math.max(h,0.5),class:'bar'});
    svg.appendChild(rect);
    if (i===0 || i===BINS-1 || i===Math.floor(BINS/2)){
      var t = se('text');
      sa(t,{x:x+bw/2,y:H-padB+13,'text-anchor':'middle',class:'tick minor','font-size':fs-1});
      t.textContent = (i*8);
      svg.appendChild(t);
    }
  });
}

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

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

막대 차트와 생김새는 비슷하지만 x축의 성격이 다릅니다. 막대 차트의 x축은 서로 무관한 범주(사과, 바나나)라 막대 사이 간격이 있는 게 맞고, 히스토그램의 x축은 하나의 연속된 값(구간)이라 막대가 서로 맞닿아야 합니다 — 간격을 주면 "이 구간과 저 구간은 무관하다"는 잘못된 인상을 줍니다.

구간(bin)의 개수가 이야기를 바꿉니다. 구간이 너무 적으면 봉우리가 뭉개져 단봉인지 쌍봉인지 못 보고, 너무 많으면 노이즈까지 패턴처럼 보입니다. 같은 데이터라도 구간 수를 바꾸면 완전히 다른 분포처럼 보일 수 있으니, 여러 구간 수로 먼저 확인해보는 게 안전합니다.

y축은 개수(도수) 또는 비율(밀도)일 수 있는데, 서로 다른 크기의 두 그룹을 비교한다면 반드시 비율로 정규화해야 공정한 비교가 됩니다.

언제 쓰나

한 연속형 변수의 분포 모양(치우침, 봉우리 개수, 퍼짐)을 파악하고 싶을 때.