Log odds ratio

로그오즈비

Comparing how strongly a term favors one group over another.

···
html
<canvas aria-label="단계별 개념 시연"></canvas><div class="controls"><button id="play">일시정지</button><button id="next">한 단계</button><button id="reset">다시</button><label><span id="param-label">속도</span> <input id="range" type="range" min="2" max="8" value="4"></label><span id="value">4</span></div>
css
body{display:block;margin:0;overflow:hidden;background:var(--bg);color:var(--fg);font:12px/1.35 system-ui,sans-serif}
canvas{position:absolute;inset:0;width:100%;height:100%}
.controls{position:absolute;right:12px;bottom:10px;display:flex;align-items:center;gap:8px;padding:6px 9px;border:1px solid var(--line);border-radius:10px;background:var(--surface);color:var(--fg)}
button{border:1px solid var(--line);border-radius:6px;background:var(--bg);color:var(--fg);padding:5px 8px;font:inherit;cursor:pointer}
input{width:90px;accent-color:var(--accent)}
@media(max-width:500px){.controls{display:none}}
js
const MODE = "log-odds-ratio";

const c=document.querySelector('canvas'),g=c.getContext('2d');
const play=document.querySelector('#play'),next=document.querySelector('#next'),reset=document.querySelector('#reset'),range=document.querySelector('#range'),value=document.querySelector('#value'),paramLabel=document.querySelector('#param-label');
const cs=getComputedStyle(document.documentElement);
const color=(key,fallback)=>cs.getPropertyValue(key).trim()||fallback;
const C={bg:color('--bg','#fff'),fg:color('--fg','#182031'),muted:color('--muted','#667085'),line:color('--line','#cbd5e1'),accent:color('--accent','#5b5bf7'),a2:color('--accent-2','#db638d'),a3:color('--accent-3','#26a899'),surface:color('--surface','#f4f5f8')};
const labels={
 'text-analysis-pipeline':'글 모음 분석: 단계별 변환','tokenization':'문장을 토큰으로 나누기','morphological-analysis':'형태소 분석: 어근과 조사','bag-of-words':'단어 가방: 문서별 빈도','n-gram':'N-그램: 인접 단어 묶기','tf-idf':'TF-IDF: 흔한 단어 낮추기','log-odds-ratio':'로그오즈비: 매체별 차이','sentence-embedding':'문장 임베딩: 의미 좌표','cosine-similarity':'코사인 유사도: 방향 비교','centroid':'중심 벡터: 평균 위치','vector-space':'벡터 공간: 특징 축','dimensionality-reduction':'차원 축소: 큰 지도를 작게','pca':'PCA: 퍼짐이 큰 방향','umap':'UMAP 단순화한 시연','t-sne':'t-SNE 단순화한 시연','k-means':'K-means: 중심 재배치','dbscan':'DBSCAN: 밀집 연결','hdbscan':'HDBSCAN 단순화한 시연','cluster-keywords':'군집 대표어: TF-IDF','median-and-iqr':'중앙값과 사분위 범위','long-tail-distribution':'긴 꼬리 분포','z-score':'Z 점수: 평균에서 거리'};
const stages={
 'text-analysis-pipeline':['원문','형태소·명사','빈도·가중치','벡터·군집'],
 'tokenization':['원문','경계 찾기','토큰 목록'], 'morphological-analysis':['문장','형태소 분해','품사 선택'],
 'bag-of-words':['문서','단어 세기','빈도 행렬'],'n-gram':['토큰','인접 창','묶음 세기'],
 'tf-idf':['원빈도','문서빈도','가중치'],'log-odds-ratio':['두 집단','오즈 비교','표준화'],
 'sentence-embedding':['문장','벡터 변환','가까운 뜻'],'cosine-similarity':['두 벡터','각도 비교','유사도'],
 'centroid':['점들','좌표 평균','중심 표시'],'vector-space':['특징 추출','축에 배치','거리 비교'],
 'dimensionality-reduction':['여러 특징','관계 압축','2D 지도'],'pca':['중심 맞추기','공분산·주축','주축 투영'],
 'umap':['이웃 그래프','가까운 점 당기기','먼 점 밀기'],'t-sne':['고차원 이웃','2D 이웃 맞추기','지도 읽기'],
 'k-means':['초기 중심','가까운 중심 배정','중심 다시 계산'],'dbscan':['반경 이웃','핵심점 연결','노이즈 분리'],
 'hdbscan':['거리 문턱 변화','밀도 계층','안정 군집 선택'],'cluster-keywords':['군집 묶기','단어 가중치','대표어 순위'],
 'median-and-iqr':['값 정렬','중앙값','Q1–Q3'],'long-tail-distribution':['빈도 정렬','상위 소수','긴 꼬리'],
 'z-score':['평균 구하기','표준편차','표준 점수']};
let w=0,h=0,dpr=1,phase=4,running=true,parameter=4,last=0;
const paramNames={'n-gram':'N','k-means':'K','dbscan':'ε','hdbscan':'최소 군집','umap':'n_neighbors','t-sne':'perplexity'};
paramLabel.textContent=paramNames[MODE]||'속도';
if(MODE==='n-gram'||MODE==='k-means'){range.max='3';range.value='2';parameter=2;value.textContent='2'}
if(MODE==='t-sne'){range.min='5';range.max='25';range.step='5';range.value='10';parameter=10;value.textContent='10'}
const small=()=>w<500;
function resize(){w=innerWidth;h=innerHeight;dpr=Math.min(devicePixelRatio||1,2);c.width=Math.round(w*dpr);c.height=Math.round(h*dpr);g.setTransform(dpr,0,0,dpr,0,0);draw()}
function rng(seed){let a=seed;return ()=>{a+=0x6D2B79F5;let t=a;t=Math.imul(t^(t>>>15),t|1);t^=t+Math.imul(t^(t>>>7),t|61);return((t^(t>>>14))>>>0)/4294967296}}
const random=rng(305),points=[];
for(let j=0;j<3;j++)for(let i=0;i<9;i++){const a=i*2.4;points.push({x:0.22+j*0.28+(random()-.5)*.19,y:.23+(i%3)*.22+(random()-.5)*.11,group:j})}
function txt(s,x,y,size=12,shade=C.fg,align='left',bold=false){g.fillStyle=shade;g.font=(bold?'700 ':'500 ')+size+'px system-ui,sans-serif';g.textAlign=align;g.fillText(s,x,y)}
function line(x1,y1,x2,y2,shade=C.line,width=1){g.strokeStyle=shade;g.lineWidth=width;g.beginPath();g.moveTo(x1,y1);g.lineTo(x2,y2);g.stroke()}
function rect(x,y,ww,hh,fill=C.surface,stroke=C.line,r=5){g.fillStyle=fill;g.strokeStyle=stroke;g.lineWidth=1;g.beginPath();g.roundRect(x,y,ww,hh,r);g.fill();g.stroke()}
function dot(x,y,r,fill){g.fillStyle=fill;g.beginPath();g.arc(x,y,r,0,Math.PI*2);g.fill()}
function plot(){const x=small()?24:80,y=50,ww=w-(small()?48:160),hh=h-(small()?78:125);return{x,y,ww,hh}}
function caption(s){txt(s,small()?12:25,h-(small()?13:23),small()?10:12,C.muted)}
function drawPoints(opts={}){const p=plot(),colors=[C.accent,C.a2,C.a3];rect(p.x,p.y,p.ww,p.hh,C.surface,C.line,8);points.forEach((v,i)=>{let x=p.x+v.x*p.ww,y=p.y+v.y*p.hh;if(opts.pull){const t=Math.min(1,phase/7);x=p.x+(v.x+(v.group-1)*.08*t)*p.ww;y=p.y+(v.y+(v.group-1)*.03*t)*p.hh}dot(x,y,small()?3.5:5,opts.neutral?C.muted:colors[v.group]);if(opts.focus===i){g.strokeStyle=C.fg;g.lineWidth=2;g.beginPath();g.arc(x,y,small()?7:10,0,7);g.stroke()}});return p}
function head(){g.fillStyle=C.bg;g.fillRect(0,0,w,h);txt(labels[MODE],small()?12:25,small()?21:27,small()?12:17,C.fg,'left',true);const ss=stages[MODE],step=Math.floor(phase/3)%ss.length;txt((step+1)+' / '+ss.length+'   '+ss[step],small()?12:25,small()?39:47,small()?10:12,C.muted)}
function words(){const p=plot(),morph=MODE==='morphological-analysis',terms=morph?['개발자','들','이','분석','했다']:['토스','기술','블로그','데이터','분석'];const count=MODE==='n-gram'?Math.max(2,Math.min(3,parameter)):1;const selected=phase%Math.max(1,terms.length-count+1);terms.forEach((t,i)=>{const x=p.x+i*p.ww/5+3,ww=p.ww/5-7,active=morph?(i===0||i===3):MODE==='n-gram'?i>=selected&&i<selected+count:i===phase%terms.length;rect(x,p.y+p.hh*.33,ww,Math.min(42,p.hh*.35),active?C.accent:C.surface,active?C.accent:C.line);txt(t,x+ww/2,p.y+p.hh*.33+Math.min(26,p.hh*.24),small()?9:13,active?C.bg:C.fg,'center');if(morph)txt(i===0||i===3?'명사':i===4?'동사':'조사',x+ww/2,p.y+p.hh*.33+Math.min(60,p.hh*.5),small()?8:11,C.muted,'center')});caption(morph?'형태소와 품사를 나눠 명사만 선택':MODE==='n-gram'?count+'개씩 묶어 순서를 보존':'경계를 옮기며 토큰을 확인')}
function matrix(){const p=plot(),rows=['매체 A','매체 B','매체 C'],cols=['개발','서비스','기술','분석'];const vals=[[8,1,5,0],[2,7,4,0],[1,2,3,8]],df=cols.map((_,i)=>vals.filter(row=>row[i]>0).length);const weight=(v,i)=>MODE==='bag-of-words'?v:v*(Math.log(4/(1+df[i]))+1);const cell=Math.min(p.ww/5,p.hh/4,small()?30:52),ox=p.x+(p.ww-4*cell)/2,oy=p.y+(p.hh-3*cell)/2;cols.forEach((t,i)=>txt(t,ox+i*cell+cell/2,oy-8,small()?9:12,C.muted,'center'));rows.forEach((r,j)=>{txt(r,ox-8,oy+(j+.65)*cell,small()?8:11,C.muted,'right');cols.forEach((_,i)=>{const score=weight(vals[j][i],i),top=Math.max(...vals[j].map((v,k)=>weight(v,k)));const active=MODE==='cluster-keywords'?score===top:(i+j*4)%8<=phase;rect(ox+i*cell,oy+j*cell,cell-3,cell-3,active?C.accent:C.surface,C.line,3);txt(String(Math.round(score*10)/10),ox+(i+.5)*cell-2,oy+(j+.62)*cell,small()?9:12,active?C.bg:C.fg,'center')})});caption(MODE==='bag-of-words'?'행=문서 · 열=단어 · 값=등장 횟수':MODE==='tf-idf'?'문서빈도가 낮은 단어에 IDF를 더함':'각 행에서 가중치가 가장 큰 단어를 대표어로')}
function tfidf(){
  const p=plot(),counts=[[8,1,5,0],[2,7,0,0],[1,0,0,8]],df=[3,2,1,1];
  const weighted=counts.map(row=>row.map((v,i)=>v*(Math.log(4/(1+df[i]))+1)));
  const cell=small()?22:48,gap=small()?26:90,total=8*cell+gap,ox=p.x+(p.ww-total)/2,oy=p.y+(p.hh-3*cell)/2+7;
  txt('원 빈도',ox+2*cell,oy-9,small()?9:12,C.muted,'center',true);
  txt('TF × IDF',ox+6*cell+gap,oy-9,small()?9:12,C.muted,'center',true);
  line(ox+4*cell+4,oy+cell*1.5,ox+4*cell+gap-4,oy+cell*1.5,C.a2,small()?2:3);
  [counts,weighted].forEach((table,panel)=>table.forEach((row,j)=>row.forEach((score,i)=>{
    const x=ox+panel*(4*cell+gap)+i*cell,y=oy+j*cell,max=panel?14:8;
    g.fillStyle=panel?C.accent:C.muted;g.globalAlpha=score?0.16+0.8*score/max:0.04;g.fillRect(x,y,cell-2,cell-2);g.globalAlpha=1;
    g.strokeStyle=i===phase%4?C.a2:C.line;g.lineWidth=i===phase%4?1.5:0.7;g.strokeRect(x,y,cell-2,cell-2);
    txt(String(Math.round(score*10)/10),x+(cell-2)/2,y+cell*.64,small()?8:11,C.fg,'center');
  })));
  caption('같은 빈도라도 문서빈도에 따라 모든 칸의 진하기가 바뀜');
}
function sentenceEmbedding(){
  const p=plot(),examples=[['캐시 최적화',[.9,.2,.1,.7],C.accent],['DB 성능',[.8,.3,.2,.8],C.a2],['브랜드 캠페인',[.1,.9,.8,.2],C.a3]];
  rect(p.x,p.y,p.ww,p.hh,C.surface,C.line,8);
  const cols=small()?[.02,.47,.85]:[.04,.43,.83],rowH=(p.hh-22)/3;
  ['문장','숫자 벡터','의미 점'].forEach((s,i)=>txt(s,p.x+p.ww*cols[i],p.y+15,small()?9:12,C.muted));
  examples.forEach(([sentence,vec,col],j)=>{
    const y=p.y+24+(j+.55)*rowH,barX=p.x+p.ww*cols[1],barW=small()?8:18;
    txt(sentence,p.x+p.ww*cols[0],y+3,small()?8:13,C.fg);
    line(p.x+p.ww*.33,y,p.x+p.ww*.41,y,C.line,1);
    vec.forEach((v,i)=>{rect(barX+i*(barW+2),y-10,barW,Math.max(3,v*16),col,C.line,2)});
    line(barX+4*(barW+2)+3,y,p.x+p.ww*.79,y,C.line,1);
    dot(p.x+p.ww*(cols[2]+(j-1)*.017),y,small()?5:7,col);
  });
  caption('문장 → 고정 길이 벡터 → 가까운 의미 좌표 (개념도)');
}
function dimensionalityReduction(){
  const p=plot(),mid=p.x+p.ww*.5,cy=p.y+p.hh*.55,scale=Math.min(p.ww*.38,p.hh*.74),angle=.3+phase*.16;
  rect(p.x,p.y,p.ww,p.hh,C.surface,C.line,8);
  txt('3D 특징',p.x+p.ww*.24,p.y+16,small()?9:12,C.muted,'center');
  txt('2D 투영',p.x+p.ww*.76,p.y+16,small()?9:12,C.muted,'center');
  const lx=p.x+p.ww*.24,rx=p.x+p.ww*.76;
  line(lx-scale*.44,cy,lx+scale*.44,cy,C.line);line(lx,cy+scale*.39,lx,cy-scale*.39,C.line);line(lx-scale*.22,cy+scale*.2,lx+scale*.22,cy-scale*.2,C.a2);
  line(rx-scale*.44,cy,rx+scale*.44,cy,C.line);line(rx,cy+scale*.39,rx,cy-scale*.39,C.line);
  points.filter((_,i)=>i%2===0).forEach((v,i)=>{
    const x=(v.x-.5)*1.3,y=(v.y-.45)*1.3,z=((i%5)-2)*.17;
    const turn=x*Math.cos(angle)-z*Math.sin(angle),depth=x*Math.sin(angle)+z*Math.cos(angle);
    const shade=[C.accent,C.a2,C.a3][v.group];
    dot(lx+(turn+depth*.28)*scale,cy+(y-depth*.26)*scale,small()?2.6:4,shade);
    dot(rx+x*scale,cy+y*scale,small()?2.6:4,shade);
  });
  txt('→',mid,cy+4,small()?15:22,C.a2,'center',true);
  caption('회전하는 3D 점구름의 깊이 축을 접어 2D로');
}
function umap(){
  const p=plot(),sample=points.filter((_,i)=>i%2===0),k=Math.min(parameter,sample.length-1),half=p.ww/2;
  rect(p.x,p.y,p.ww,p.hh,C.surface,C.line,8);
  txt('kNN 그래프  k='+k,p.x+half*.5,p.y+16,small()?9:12,C.muted,'center');
  txt('2D 배치',p.x+half*1.5,p.y+16,small()?9:12,C.muted,'center');
  line(p.x+half,p.y+6,p.x+half,p.y+p.hh-6,C.line);
  const pos=(v,right)=>({x:p.x+(right?half:0)+half*(.1+v.x*.8+(right?(v.group-1)*.05:0)),y:p.y+19+(p.hh-25)*v.y});
  sample.forEach((v,i)=>{
    const neighbors=sample.map((q,j)=>({j,d:Math.hypot(v.x-q.x,v.y-q.y)})).filter(q=>q.j!==i).sort((a,b)=>a.d-b.d).slice(0,k);
    const from=pos(v,false);neighbors.forEach(({j})=>{const to=pos(sample[j],false);line(from.x,from.y,to.x,to.y,C.a2,small()?.7:1)});
  });
  sample.forEach(v=>{const col=[C.accent,C.a2,C.a3][v.group];for(const right of [false,true]){const q=pos(v,right);dot(q.x,q.y,small()?2.7:4.5,col)}});
  caption('n_neighbors='+k+' · 이웃선을 만든 뒤 당겨 놓는 단순화한 시연');
}
function tsne(){
  const p=plot(),sample=points,half=p.ww/2;
  rect(p.x,p.y,p.ww,p.hh,C.surface,C.line,8);
  txt('perplexity 5',p.x+half*.5,p.y+16,small()?9:12,C.muted,'center');
  txt('perplexity '+parameter,p.x+half*1.5,p.y+16,small()?9:12,C.muted,'center');
  line(p.x+half,p.y+6,p.x+half,p.y+p.hh-6,C.line);
  const centers=[[[.2,.47],[.49,.49],[.79,.47]],[[.21,.27],[.77,.34],[.49,.72]]];
  sample.forEach(v=>{for(let panel=0;panel<2;panel++){
    const center=centers[panel][v.group],spread=panel===0?.68:.52+(parameter-5)*.006;
    const localX=(v.x-(.22+v.group*.28))*spread,localY=(v.y-.45)*spread;
    const x=p.x+panel*half+half*(center[0]+localX),y=p.y+20+(p.hh-26)*(center[1]+localY);
    dot(x,y,small()?2.8:4.5,[C.accent,C.a2,C.a3][v.group]);
  }});
  caption('같은 데이터, 다른 섬 배치 · 군집 간 거리 해석 금지');
}
function clusterKeywords(){
  const p=plot(),terms=['캐시','인증','검색','서비스'],counts=[[7,0,1,6],[0,7,0,5],[1,0,8,4]],colors=[C.accent,C.a2,C.a3];
  const df=terms.map((_,i)=>counts.filter(row=>row[i]>0).length);
  const weights=counts.map(row=>row.map((count,i)=>count*(Math.log(4/(1+df[i]))+1)));
  const names=weights.map(row=>terms[row.indexOf(Math.max(...row))]);
  rect(p.x,p.y,p.ww,p.hh,C.surface,C.line,8);
  points.forEach(v=>dot(p.x+v.x*p.ww,p.y+v.y*p.hh,small()?3:5,colors[v.group]));
  names.forEach((name,j)=>{
    const ps=points.filter(v=>v.group===j),mx=ps.reduce((s,v)=>s+v.x,0)/ps.length;
    const x=p.x+mx*p.ww,bw=small()?47:85,bh=small()?19:27,y=p.y+6;
    rect(x-bw/2,y,bw,bh,C.surface,colors[j],6);
    txt(name,x,y+bh*.69,small()?10:14,colors[j],'center',true);
    line(x,y+bh,x,p.y+p.hh*.38,colors[j],1);
  });
  caption('군집 점 위에 TF-IDF 대표어 후보를 붙이고 원문 확인');
}
function bars(){const p=plot(),v=MODE==='long-tail-distribution'?[26,16,10,6,4,3,2,1,1,1,1,1]:MODE==='log-odds-ratio'?[-2,-1.5,-.6,.2,.7,1.3,2.2]:[3,6,9,12,15,18,22];const max=Math.max(...v.map(Math.abs));line(p.x,p.y+p.hh*.55,p.x+p.ww,p.y+p.hh*.55,C.line,2);v.forEach((n,i)=>{const bw=p.ww/v.length*.7,x=p.x+(i+.15)*p.ww/v.length,hh=Math.abs(n)/max*p.hh*.35;const y=n>=0?p.y+p.hh*.55-hh:p.y+p.hh*.55;rect(x,y,bw,Math.max(2,hh),i<=phase?C.accent:C.a2,C.line,2)});caption(MODE==='log-odds-ratio'?'왼쪽=매체 B · 오른쪽=매체 A':'소수 단어는 매우 잦고 대부분은 드묾')}
function vectors(){const p=plot(),cx=p.x+p.ww*.5,cy=p.y+p.hh*.55,scale=Math.min(p.ww,p.hh)*.35;line(p.x+8,cy,p.x+p.ww-8,cy);line(cx,p.y+8,cx,p.y+p.hh-8);const a=[.9,-.5],b=MODE==='cosine-similarity'?[.75,-.62]:[.45,-.8];[[a,C.accent,'A'],[b,C.a2,'B']].forEach(([v,col,name])=>{line(cx,cy,cx+v[0]*scale,cy+v[1]*scale,col,4);dot(cx+v[0]*scale,cy+v[1]*scale,5,col);txt(name,cx+v[0]*scale+7,cy+v[1]*scale-5,11,col)});caption(MODE==='cosine-similarity'?'각도가 작을수록 높음 · 절대값보다 순위':'축마다 하나의 특징값을 놓은 좌표')}
function centroid(){const p=drawPoints(),colors=[C.accent,C.a2,C.a3];for(let j=0;j<3;j++){const ps=points.filter(v=>v.group===j),mx=ps.reduce((s,v)=>s+v.x,0)/ps.length,my=ps.reduce((s,v)=>s+v.y,0)/ps.length;dot(p.x+mx*p.ww,p.y+my*p.hh,small()?7:10,colors[j]);txt('×',p.x+mx*p.ww,p.y+my*p.hh+4,13,C.bg,'center',true)}caption('각 집단 좌표의 산술평균을 중심으로')}
function pca(){const p=drawPoints({neutral:true}),mx=points.reduce((s,v)=>s+v.x,0)/points.length,my=points.reduce((s,v)=>s+v.y,0)/points.length;let xx=0,xy=0,yy=0;for(const q of points){const x=q.x-mx,y=q.y-my;xx+=x*x;xy+=x*y;yy+=y*y}let vx=1,vy=1;for(let i=0;i<12;i++){const nx=xx*vx+xy*vy,ny=xy*vx+yy*vy,n=Math.hypot(nx,ny)||1;vx=nx/n;vy=ny/n}const cx=p.x+mx*p.ww,cy=p.y+my*p.hh,len=Math.min(p.ww,p.hh)*.43;line(cx-vx*len,cy-vy*len,cx+vx*len,cy+vy*len,C.accent,3);dot(cx,cy,5,C.a2);const variance=(xx*vx*vx+2*xy*vx*vy+yy*vy*vy)/(xx+yy);caption('제1주성분 · 2D → 1D · 분산 '+Math.round(variance*100)+'%')}
function clusters(){const p=drawPoints({pull:MODE==='umap'||MODE==='t-sne',neutral:MODE==='hdbscan'});if(MODE==='k-means'){const colors=[C.accent,C.a2,C.a3],k=Math.max(2,Math.min(3,parameter));let centers=[[.2,.25],[.5,.75],[.8,.24]].slice(0,k);for(let z=0;z<Math.max(1,phase);z++){const bins=centers.map(()=>[]);for(const v of points){let idx=0,best=Infinity;centers.forEach((q,j)=>{const d=(v.x-q[0])**2+(v.y-q[1])**2;if(d<best){best=d;idx=j}});bins[idx].push(v)}centers=centers.map((q,j)=>bins[j].length?[bins[j].reduce((s,v)=>s+v.x,0)/bins[j].length,bins[j].reduce((s,v)=>s+v.y,0)/bins[j].length]:q)}centers.forEach((q,j)=>{dot(p.x+q[0]*p.ww,p.y+q[1]*p.hh,9,colors[j]);txt('×',p.x+q[0]*p.ww,p.y+q[1]*p.hh+4,13,C.bg,'center',true)})}
else if(MODE==='hdbscan'){
  // A thresholded neighbor graph illustrates the hierarchy; this does not compute HDBSCAN stability.
  const threshold=.075+phase*.018,parent=points.map((_,i)=>i),edges=[];
  function root(i){while(parent[i]!==i)i=parent[i]=parent[parent[i]];return i}
  points.forEach((a,i)=>points.forEach((b,j)=>{if(j<=i)return;if(Math.hypot(a.x-b.x,a.y-b.y)<threshold){parent[root(j)]=root(i);edges.push([i,j])}}));
  const sizes=new Map();points.forEach((_,i)=>sizes.set(root(i),(sizes.get(root(i))||0)+1));
  edges.forEach(([i,j])=>{if(sizes.get(root(i))>=parameter)line(p.x+points[i].x*p.ww,p.y+points[i].y*p.hh,p.x+points[j].x*p.ww,p.y+points[j].y*p.hh,C.accent,1.5)});
  points.forEach((v,i)=>{if(sizes.get(root(i))>=parameter)dot(p.x+v.x*p.ww,p.y+v.y*p.hh,small()?4:6,C.accent)});
}
else if(MODE==='dbscan'){const eps=.065+parameter*.009,minPts=3;points.forEach((v,i)=>{const near=points.filter(q=>Math.hypot(v.x-q.x,v.y-q.y)<=eps);if(i<=phase*4&&near.length>=minPts){g.strokeStyle=C.accent;g.lineWidth=1;g.beginPath();g.arc(p.x+v.x*p.ww,p.y+v.y*p.hh,eps*p.ww,0,7);g.stroke()}})}
caption(MODE==='k-means'?'가까운 중심에 배정 → 중심을 다시 계산':MODE==='dbscan'?'ε 반경의 이웃 수로 핵심점 판정':MODE==='hdbscan'?'회색 점=노이즈(−1) · 안정도 생략한 시연':MODE==='umap'?'근접 그래프의 당김·밀어냄을 단순화한 시연':'이웃 확률을 맞추는 움직임을 단순화한 시연')}
function distribution(){const p=plot(),vals=[2,3,3,4,4,5,6,7,8,9,10,11,12,14,18,29],min=2,max=29,px=v=>p.x+12+(v-min)/(max-min)*(p.ww-24),cy=p.y+p.hh*.54;line(px(min),cy,px(max),cy,C.line,2);vals.forEach((v,i)=>dot(px(v),cy-((i%3)-1)*10,small()?3:4,i<=phase*2?C.accent:C.muted));if(MODE==='median-and-iqr'){const q1=4,q2=7.5,q3=11.5;rect(px(q1),cy-28,px(q3)-px(q1),56,C.surface,C.accent,2);line(px(q2),cy-28,px(q2),cy+28,C.a2,3);txt('Q1',px(q1),cy-36,11,C.fg,'center');txt('중앙값',px(q2),cy-36,11,C.fg,'center');txt('Q3',px(q3),cy-36,11,C.fg,'center')}else{const mean=vals.reduce((a,b)=>a+b,0)/vals.length,sd=Math.sqrt(vals.reduce((a,b)=>a+(b-mean)**2,0)/vals.length);line(px(mean),cy-42,px(mean),cy+42,C.a2,2);txt('평균',px(mean),cy-48,11,C.a2,'center');txt('z = '+((vals[Math.min(vals.length-1,phase+5)]-mean)/sd).toFixed(2),p.x+10,p.y+22,small()?10:13,C.fg)}caption(MODE==='median-and-iqr'?'가운데 50%는 Q1–Q3 상자 안에':'z = (값 − 평균) / 표준편차')}
function pipeline(){const p=plot(),names=['글','명사','빈도','벡터','군집'],n=names.length,bw=Math.min(100,(p.ww-24)/n*.78),cy=p.y+p.hh*.55;names.forEach((t,i)=>{const x=p.x+(i+.5)*p.ww/n;rect(x-bw/2,cy-25,bw,50,i<=phase%n?C.accent:C.surface,C.line,6);txt(t,x,cy+5,small()?10:14,i<=phase%n?C.bg:C.fg,'center',true);if(i<n-1)line(x+bw/2+3,cy,p.x+(i+1.5)*p.ww/n-bw/2-3,cy,C.a2,2)});caption('각 단계의 출력이 다음 단계의 입력')}
function draw(){
  if(!w)return;
  head();
  if(['tokenization','morphological-analysis','n-gram'].includes(MODE))words();
  else if(MODE==='bag-of-words')matrix();
  else if(MODE==='tf-idf')tfidf();
  else if(MODE==='cluster-keywords')clusterKeywords();
  else if(MODE==='sentence-embedding')sentenceEmbedding();
  else if(MODE==='dimensionality-reduction')dimensionalityReduction();
  else if(MODE==='umap')umap();
  else if(MODE==='t-sne')tsne();
  else if(['log-odds-ratio','long-tail-distribution'].includes(MODE))bars();
  else if(['cosine-similarity','vector-space'].includes(MODE))vectors();
  else if(MODE==='centroid')centroid();
  else if(MODE==='pca')pca();
  else if(['k-means','dbscan','hdbscan'].includes(MODE))clusters();
  else if(['median-and-iqr','z-score'].includes(MODE))distribution();
  else if(MODE==='text-analysis-pipeline')pipeline();
}
function tick(t){const interval=paramNames[MODE]?600:Math.max(300,900-parameter*75);if(running&&t-last>interval){phase=(phase+1)%9;last=t;draw()}requestAnimationFrame(tick)}
play.onclick=()=>{running=!running;play.textContent=running?'일시정지':'재생'};
next.onclick=()=>{running=false;play.textContent='재생';phase=(phase+1)%9;draw()};
reset.onclick=()=>{phase=0;draw()};
range.oninput=()=>{parameter=Number(range.value);value.textContent=String(parameter);phase=0;draw()};
addEventListener('resize',resize);resize();requestAnimationFrame(tick);

Imagine terms placed on a balance between two publications. Positive values lean toward A; negative values toward B. The article analysis compared noun document frequency for each publication against the rest, a document-level adaptation of the original token-count method.

Monroe, Colaresi, and Quinn’s “Fightin’ Words” adds an informative Dirichlet prior α_w proportional to background-corpus term counts, then compares the groups’ token log odds. Approximate variance is 1/(y_A+α_w)+1/(y_B+α_w); z divides the contrast by its standard error to rank terms. A document-frequency adaptation changes the observation unit and model, so its numbers are not directly interchangeable with the original formula.

Thousands of terms are compared, and documents may be overdispersed, so do not read this z as a ready-made significance probability. Use it to rank candidate distinctive terms, then inspect group sizes and source articles.

When to use

Use it to compare candidate distinctive terms between two document groups.

Open as page ↗