2 Phân phối xác suất
2.1 Định nghĩa
Phân phối xác suất (probability distribution) là một hàm số (function) thể hiện xác suất của mọi biến cố (tập hợp con) nằm trong không gian mẫu \(\Omega\).
Phân phối xác suất thỏa 3 điều kiện sau:
\(\mathbb{P}(\Omega) = 1\)
\(0 \leq \mathbb{P}(A) \leq 1\) với mọi biến cố \(A\)
Nếu các biến cố \(A_1\), \(A_2\), …, \(A_n\) xung khắc, thì:
\[\mathbb{P}(A_1 \cup A_2 \cup \cdots \cup A_n) = \mathbb{P}(A_1) + \mathbb{P}(A_2) + \cdots + \mathbb{P}(A_n)\]
Trong thực tế ứng dụng, để tổng quát hóa, phân phối xác suất thường được hiểu là hàm số để tính xác suất của biến ngẫu nhiên. Mỗi biến ngẫu nhiên có một phân phối xác suất. Do có 2 loại biến rời rạc và liên tục, nên phân phối xác suất cũng được phân loại thành phân phối rời rạc và liên tục tương ứng.
2.2 Các hàm phân phối xác suất
viewof binomViz = {
// ═══════════════════ PALETTE ═══════════════════
const C = {
txt:'#16213E', sub:'#8294AA', light:'#E4E9F0',
card:'#FFFFFF', grid:'#F0F2F5',
pmf:'#1565C0', pmfDark:'#0D47A1',
cdf:'#00796B', cdfDark:'#004D40',
quant:'#AD1457', quantLight:'#F8BBD0',
highlight:'#FF6F00', highlightLight:'#FFF3E0',
accent:'#283593',
};
const mono = "'SF Mono',SFMono-Regular,Menlo,monospace";
function kCol(k,n){ return d3.interpolateBlues(0.22+0.58*k/Math.max(n,1)); }
function kColSoft(k,n){ return d3.interpolateBlues(0.06+0.22*k/Math.max(n,1)); }
// ═══════════════════ MATH ═══════════════════
// Pre-compute log-factorials for fast binomial
const maxN = 20;
const logFact = new Float64Array(maxN + 1);
logFact[0] = 0;
for (let i = 1; i <= maxN; i++) logFact[i] = logFact[i-1] + Math.log(i);
function pmf(k,n,p){
if(k<0||k>n) return 0;
if(p===0) return k===0?1:0;
if(p===1) return k===n?1:0;
return Math.exp(logFact[n]-logFact[k]-logFact[n-k]+k*Math.log(p)+(n-k)*Math.log(1-p));
}
function cdfVal(k,n,p){let s=0;for(let i=0;i<=k;i++)s+=pmf(i,n,p);return Math.min(1,s);}
function qbinom(alpha,n,p){for(let k=0;k<=n;k++){if(cdfVal(k,n,p)>=alpha)return k;}return n;}
// ═══════════════════ STATE ═══════════════════
let hoveredK = -1;
let mode = 'pmf';
// ═══════════════════ OUTER ═══════════════════
const outer = document.createElement("div");
outer.style.cssText = `display:flex;flex-direction:column;gap:14px;
font-family:'Inter',-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
width:100%;max-width:960px;margin:0 auto;`;
if(!document.querySelector('link[href*="Inter"]')){
const lk=document.createElement('link');lk.rel='stylesheet';
lk.href='https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap';
document.head.appendChild(lk);
}
outer.appendChild(injectStyle());
// ═══════════════════ SLIDERS ═══════════════════
const SL = {};
SL.n = createSlider("Số phép thử (n)",1,20,1,8,C.pmf,"blue");
SL.p = createSlider("Xác suất thành công mỗi lần (p)",0.01,0.99,0.01,0.40,'#E53935',"red");
SL.alpha = createSlider("Phân vị (\u03B1)",0.01,0.99,0.01,0.50,C.quant,"pink");
const slRow1 = document.createElement("div");
slRow1.style.cssText = "display:flex;gap:14px;width:100%;";
slRow1.append(SL.n.el, SL.p.el);
outer.appendChild(slRow1);
const slRowAlpha = document.createElement("div");
slRowAlpha.style.cssText = "display:flex;gap:14px;width:100%;";
slRowAlpha.append(SL.alpha.el);
outer.appendChild(slRowAlpha);
// ═══════════════════ MODE TOGGLES ═══════════════════
const toggleRow = document.createElement("div");
toggleRow.style.cssText = "display:flex;gap:6px;justify-content:center;";
const modeConfig = {
pmf: {label:'PMF P(X = k)', color:C.pmf},
cdf: {label:'CDF P(X \u2264 k)', color:C.cdf},
quantile:{label:'Quantile Q(\u03B1)', color:C.quant},
};
const modeBtns = {};
Object.entries(modeConfig).forEach(([key,cfg])=>{
const b = document.createElement("button");
b.style.cssText = `padding:8px 22px;border-radius:10px;border:2px solid ${C.light};
font-size:13px;font-weight:700;cursor:pointer;font-family:inherit;
transition:all 0.15s;background:${C.card};color:${C.txt};`;
b.textContent = cfg.label;
b.addEventListener('click',()=>{mode=key;hoveredK=-1;renderAll();});
toggleRow.appendChild(b);
modeBtns[key] = b;
});
outer.appendChild(toggleRow);
function styleToggles(){
Object.entries(modeBtns).forEach(([key,b])=>{
const active = key===mode;
const col = modeConfig[key].color;
b.style.background = active?col:C.card;
b.style.color = active?'#fff':C.txt;
b.style.borderColor = active?col:C.light;
});
slRowAlpha.style.display = mode==='quantile'?'flex':'none';
}
// ═══════════════════ PANELS ═══════════════════
const panelRow = document.createElement("div");
panelRow.style.cssText = "display:grid;grid-template-columns:1fr 1fr;gap:14px;width:100%;";
outer.appendChild(panelRow);
function mkPanel(titleText){
const card = document.createElement("div");
card.style.cssText = `background:${C.card};border-radius:16px;border:1px solid ${C.light};
padding:14px;box-shadow:0 2px 16px rgba(0,0,0,0.04);`;
const title = document.createElement("div");
title.style.cssText = `font-size:13px;font-weight:800;color:${C.txt};margin-bottom:8px;text-align:center;`;
title.innerHTML = titleText;
card.appendChild(title);
return {card, title};
}
const leftPanel = mkPanel('\u03A9 Sample Space');
const rightPanel = mkPanel('P(X = k)');
const LW=440, LH=400, RW=440, RH=400;
const leftSvg = d3.create("svg").attr("viewBox",[0,0,LW,LH])
.style("width","100%").style("border-radius","12px")
.style("background","#FAFBFD").style("border",`1px solid ${C.light}`);
const rightSvg = d3.create("svg").attr("viewBox",[0,0,RW,RH])
.style("width","100%").style("border-radius","12px")
.style("background","#FAFBFD").style("border",`1px solid ${C.light}`);
leftPanel.card.appendChild(leftSvg.node());
rightPanel.card.appendChild(rightSvg.node());
panelRow.append(leftPanel.card, rightPanel.card);
// ═══════════════════ SQUARIFIED TREEMAP ═══════════════════
function squarify(probs, x, y, w, h){
const rects = [];
const total = probs.reduce((a,b)=>a+b, 0);
if(total === 0) return rects;
const items = probs.map((p,i)=>({i, area:(p/total)*w*h})).filter(d=>d.area>0.5);
function lay(items, x, y, w, h){
if(!items.length) return;
if(items.length===1){rects.push({i:items[0].i, x, y, w, h}); return;}
const ta = items.reduce((s,it)=>s+it.area, 0);
const wide = w >= h;
let bestA=Infinity, bestS=1, cum=0;
for(let s=1;s<items.length;s++){
cum += items[s-1].area;
let worst = 0;
for(let j=0;j<s;j++){
const frac = items[j].area/cum;
const rw = wide?(cum/ta)*w:w*frac;
const rh = wide?h*frac:(cum/ta)*h;
worst = Math.max(worst, Math.max(rw/rh, rh/rw));
}
if(worst<bestA){bestA=worst; bestS=s;}
}
const strip=items.slice(0,bestS), rest=items.slice(bestS);
const sArea = strip.reduce((s,it)=>s+it.area, 0);
const ratio = sArea/ta;
if(wide){
const sw=w*ratio; let cy=y;
strip.forEach(it=>{const sh=h*(it.area/sArea);rects.push({i:it.i,x,y:cy,w:sw,h:sh});cy+=sh;});
lay(rest, x+sw, y, w-sw, h);
} else {
const sh=h*ratio; let cx=x;
strip.forEach(it=>{const sw=w*(it.area/sArea);rects.push({i:it.i,x:cx,y,w:sw,h:sh});cx+=sw;});
lay(rest, x, y+sh, w, h-sh);
}
}
lay(items, x, y, w, h);
return rects;
}
// ═══════════════════ RENDER ═══════════════════
// Use a rAF guard so we never queue multiple renders per frame
let rafId = 0;
function scheduleRender(){
if(rafId) return;
rafId = requestAnimationFrame(()=>{ rafId=0; renderAll(); });
}
function renderAll(){
SL.n.sync(); SL.p.sync(); SL.alpha.sync();
const n=SL.n.val(), p=SL.p.val(), alpha=SL.alpha.val();
const isCDF = mode==='cdf', isQ = mode==='quantile';
styleToggles();
const probs = Array.from({length:n+1},(_,k)=>pmf(k,n,p));
const cdfs = Array.from({length:n+1},(_,k)=>cdfVal(k,n,p));
const qK = qbinom(alpha, n, p);
const modeColor = isQ?C.quant:isCDF?C.cdf:C.pmf;
const effectiveHover = isQ ? qK : hoveredK;
const showCumRange = isCDF || isQ;
// Titles
leftPanel.title.innerHTML = `Không gian mẫu \u03A9`;
rightPanel.title.innerHTML = isQ
? `Q(\u03B1) = min{k : F(k) \u2265 \u03B1}`
: isCDF ? `CDF`
: `PMF`;
leftSvg.selectAll("*").remove();
rightSvg.selectAll("*").remove();
// ═══════════════════ LEFT: MOSAIC (no dots) ═══════════════════
const pad = 12;
const rects = squarify(probs, pad, pad, LW-2*pad, LH-2*pad);
// Frame
leftSvg.append("rect").attr("x",pad-1).attr("y",pad-1)
.attr("width",LW-2*pad+2).attr("height",LH-2*pad+2)
.attr("rx",12).attr("fill","none").attr("stroke",C.light).attr("stroke-width",1);
// Build all tile elements in a document fragment approach via d3
const tileG = leftSvg.append("g");
rects.forEach(r=>{
const k=r.i, gap=2;
const isHK = effectiveHover===k;
const inRange = showCumRange && effectiveHover>=0 && k<=effectiveHover;
const active = isHK || inRange;
const anyHover = effectiveHover >= 0;
const dimmed = anyHover && !active;
const col = kCol(k,n), colS = kColSoft(k,n);
// Tile background
tileG.append("rect")
.attr("x",r.x+gap/2).attr("y",r.y+gap/2)
.attr("width",Math.max(0,r.w-gap)).attr("height",Math.max(0,r.h-gap))
.attr("rx",6)
.attr("fill", dimmed ? '#F0F0F0' : active ? d3.interpolateBlues(0.12+0.35*k/Math.max(n,1)) : colS)
.attr("stroke", isHK ? C.highlight : (inRange ? modeColor : 'rgba(255,255,255,0.5)'))
.attr("stroke-width", isHK ? 3 : (inRange ? 2 : 0.5));
// Inner subtle gradient overlay for depth (no dots)
if(!dimmed && r.w > 10 && r.h > 10){
tileG.append("rect")
.attr("x",r.x+gap/2).attr("y",r.y+gap/2)
.attr("width",Math.max(0,r.w-gap)).attr("height",Math.max(0,(r.h-gap)*0.4))
.attr("rx",6)
.attr("fill","rgba(255,255,255,0.15)");
}
// Label
if(r.w > 26 && r.h > 22){
const cx=r.x+r.w/2, cy=r.y+r.h/2;
const fs = Math.min(20, Math.min(r.w, r.h)*0.3);
const showPct = r.h > 46;
if(active && r.h > 32){
const pw=Math.min(r.w-6, fs*3.5), ph=showPct ? fs+20 : fs+8;
tileG.append("rect").attr("x",cx-pw/2).attr("y",cy-ph/2)
.attr("width",pw).attr("height",ph)
.attr("rx",7).attr("fill","rgba(255,255,255,0.92)");
}
tileG.append("text").attr("x",cx).attr("y",cy-(showPct?4:0))
.attr("text-anchor","middle").attr("dominant-baseline","middle")
.attr("font-size",active?fs+2:fs).attr("font-weight",900)
.attr("fill",dimmed?'#ccc':col).attr("font-family",mono)
.text(`k=${k}`);
if(showPct){
tileG.append("text").attr("x",cx).attr("y",cy+fs*0.5+5)
.attr("text-anchor","middle").attr("font-size",active?11:10).attr("font-weight",700)
.attr("fill",dimmed?'#ddd':col).attr("opacity",dimmed?0.3:1)
.attr("font-family",mono).text((probs[k]*100).toFixed(1)+'%');
}
}
// Hit area
if(!isQ){
tileG.append("rect").attr("x",r.x).attr("y",r.y).attr("width",r.w).attr("height",r.h)
.attr("fill","transparent").attr("cursor","pointer")
.on("mouseenter",()=>{hoveredK=k;renderAll();})
.on("mouseleave",()=>{hoveredK=-1;renderAll();});
}
});
// Omega watermark
leftSvg.append("text").attr("x",LW-16).attr("y",20)
.attr("font-size",15).attr("font-weight",900).attr("fill",C.sub).attr("opacity",0.2)
.attr("text-anchor","end").text("\u03A9");
// Cumulative area label
if(showCumRange && effectiveHover>=0){
const cumP = cdfs[effectiveHover];
leftSvg.append("text").attr("x",pad+6).attr("y",LH-pad-4)
.attr("font-size",12).attr("font-weight",800).attr("fill",modeColor)
.attr("font-family",mono).text(`\u2211 = ${(cumP*100).toFixed(1)}%`);
}
// ═══════════════════ RIGHT PANEL ═══════════════════
const mg = {l:54, r:16, t:20, b:42};
const iw = RW-mg.l-mg.r, ih = RH-mg.t-mg.b;
const gR = rightSvg.append("g").attr("transform",`translate(${mg.l},${mg.t})`);
const xS = d3.scaleBand().domain(d3.range(n+1)).range([0,iw]).padding(0.12);
if(mode==='pmf'){
// ── PMF BARS ──
const yMax = d3.max(probs)*1.2;
const yS = d3.scaleLinear().domain([0,yMax]).range([ih,0]);
yS.ticks(5).forEach(v=>{
gR.append("line").attr("x2",iw).attr("y1",yS(v)).attr("y2",yS(v)).attr("stroke",C.grid);
gR.append("text").attr("x",-8).attr("y",yS(v)+4).attr("text-anchor","end")
.attr("font-size",10).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(2));
});
probs.forEach((pk,k)=>{
const isH = hoveredK===k, dim = hoveredK>=0 && !isH;
const col = kCol(k,n), bx = xS(k), by = yS(pk), bh = ih-by;
if(isH) gR.append("rect").attr("x",bx+2).attr("y",by+3).attr("width",xS.bandwidth()).attr("height",bh)
.attr("rx",5).attr("fill","rgba(0,0,0,0.05)");
gR.append("rect").attr("x",bx).attr("y",by).attr("width",xS.bandwidth()).attr("height",bh)
.attr("rx",5).attr("fill",col)
.attr("opacity",dim?0.12:(isH?1:0.72))
.attr("stroke",isH?C.highlight:"none").attr("stroke-width",isH?3:0);
if(isH || hoveredK<0)
gR.append("text").attr("x",bx+xS.bandwidth()/2).attr("y",by-8)
.attr("text-anchor","middle").attr("font-size",isH?12:9).attr("font-weight",800)
.attr("fill",isH?C.highlight:col).attr("font-family",mono).attr("opacity",dim?0.12:0.85)
.text(pk.toFixed(3));
});
// Y label
rightSvg.append("text").attr("x",14).attr("y",mg.t+ih/2)
.attr("text-anchor","middle").attr("font-size",11).attr("font-weight",700).attr("fill",C.sub)
.attr("transform",`rotate(-90,14,${mg.t+ih/2})`).text("P(X = k)");
} else {
// ── CDF / QUANTILE ──
const yS = d3.scaleLinear().domain([0,1.05]).range([ih,0]);
const ticks = [0,0.25,0.5,0.75,1.0];
ticks.forEach(v=>{
gR.append("line").attr("x2",iw).attr("y1",yS(v)).attr("y2",yS(v)).attr("stroke",C.grid);
gR.append("text").attr("x",-8).attr("y",yS(v)+4).attr("text-anchor","end")
.attr("font-size",10).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(2));
});
// ── FIX: CDF shaded bars — each bar k reaches to its OWN cdf value ──
if(effectiveHover >= 0){
for(let k=0; k<=effectiveHover; k++){
const barTop = yS(cdfs[k]);
gR.append("rect")
.attr("x", xS(k))
.attr("y", barTop)
.attr("width", xS.bandwidth())
.attr("height", ih - barTop)
.attr("fill", modeColor).attr("opacity", 0.08);
}
}
// Step function
for(let k=0; k<=n; k++){
const cx = xS(k)+xS.bandwidth()/2, cy = yS(cdfs[k]);
const isHK = effectiveHover===k;
const inR = showCumRange && effectiveHover>=0 && k<=effectiveHover;
const dim = effectiveHover>=0 && !inR && !isHK;
// Horizontal segment to next
if(k<n){
const nx = xS(k+1)+xS.bandwidth()/2;
gR.append("line").attr("x1",cx).attr("y1",cy).attr("x2",nx).attr("y2",cy)
.attr("stroke",modeColor).attr("stroke-width",2).attr("opacity",dim?0.1:0.5);
}
// Vertical riser
if(k>0){
gR.append("line").attr("x1",cx).attr("y1",yS(cdfs[k-1])).attr("x2",cx).attr("y2",cy)
.attr("stroke",modeColor).attr("stroke-width",1.5).attr("stroke-dasharray","3,2")
.attr("opacity",dim?0.1:0.4);
}
// Dot
gR.append("circle").attr("cx",cx).attr("cy",cy).attr("r",isHK?7:4.5)
.attr("fill",isHK?C.highlight:modeColor).attr("stroke","#fff").attr("stroke-width",isHK?2.5:1.5)
.attr("opacity",dim?0.1:1);
// Value
if(isHK || (effectiveHover<0 && k%Math.max(1,Math.ceil((n+1)/8))===0))
gR.append("text").attr("x",cx).attr("y",cy-12)
.attr("text-anchor","middle").attr("font-size",isHK?12:9).attr("font-weight",800)
.attr("fill",isHK?C.highlight:modeColor).attr("font-family",mono)
.attr("opacity",dim?0.1:0.8).text(cdfs[k].toFixed(3));
}
// ── QUANTILE LOOKUP LINES ──
if(isQ){
const aY = yS(alpha);
const qX = xS(qK)+xS.bandwidth()/2;
const qY = yS(cdfs[qK]);
gR.append("line").attr("x1",0).attr("y1",aY).attr("x2",qX).attr("y2",aY)
.attr("stroke",C.quant).attr("stroke-width",2).attr("stroke-dasharray","6,4");
gR.append("line").attr("x1",qX).attr("y1",aY).attr("x2",qX).attr("y2",ih)
.attr("stroke",C.quant).attr("stroke-width",2).attr("stroke-dasharray","6,4");
gR.append("rect").attr("x",-48).attr("y",aY-10).attr("width",40).attr("height",20)
.attr("rx",5).attr("fill",C.quant);
gR.append("text").attr("x",-28).attr("y",aY+4)
.attr("text-anchor","middle").attr("font-size",11).attr("font-weight",800)
.attr("fill","#fff").attr("font-family",mono).text(`${alpha.toFixed(2)}`);
gR.append("circle").attr("cx",qX).attr("cy",aY).attr("r",5)
.attr("fill",C.quant).attr("stroke","#fff").attr("stroke-width",2);
gR.append("rect").attr("x",qX-20).attr("y",ih+2).attr("width",40).attr("height",20)
.attr("rx",5).attr("fill",C.quant);
gR.append("text").attr("x",qX).attr("y",ih+15)
.attr("text-anchor","middle").attr("font-size",12).attr("font-weight",900)
.attr("fill","#fff").attr("font-family",mono).text(`k=${qK}`);
if(qY < aY-4){
gR.append("line").attr("x1",qX+12).attr("y1",aY-2).attr("x2",qX+12).attr("y2",qY+2)
.attr("stroke",C.quant).attr("stroke-width",1.5).attr("opacity",0.6);
}
}
// Y label
rightSvg.append("text").attr("x",14).attr("y",mg.t+ih/2)
.attr("text-anchor","middle").attr("font-size",11).attr("font-weight",700).attr("fill",C.sub)
.attr("transform",`rotate(-90,14,${mg.t+ih/2})`)
.text("F(k) = P(X \u2264 k)");
}
// ── X axis labels (shared) ──
gR.append("line").attr("x2",iw).attr("y1",ih).attr("y2",ih).attr("stroke",C.sub).attr("opacity",0.25);
for(let k=0; k<=n; k++){
const isHK = effectiveHover===k;
if(isQ && k===qK) continue;
gR.append("text").attr("x",xS(k)+xS.bandwidth()/2).attr("y",ih+18)
.attr("text-anchor","middle").attr("font-size",isHK?13:11)
.attr("font-weight",isHK?800:500).attr("fill",isHK?C.highlight:C.sub)
.attr("font-family",mono).text(k);
}
rightSvg.append("text").attr("x",mg.l+iw/2).attr("y",RH-4)
.attr("text-anchor","middle").attr("font-size",11).attr("font-weight",700).attr("fill",C.sub).text("k");
// ── Hit areas on right panel ──
if(!isQ){
for(let k=0; k<=n; k++){
rightSvg.append("rect")
.attr("x",mg.l+xS(k)-xS.step()*0.1).attr("y",mg.t)
.attr("width",xS.step()).attr("height",ih)
.attr("fill","transparent").attr("cursor","pointer")
.on("mouseenter",()=>{hoveredK=k;renderAll();})
.on("mouseleave",()=>{hoveredK=-1;renderAll();});
}
}
}
// ═══════════════════ EVENTS ═══════════════════
SL.n.input.addEventListener("input",()=>{hoveredK=-1;scheduleRender();});
SL.p.input.addEventListener("input",()=>{hoveredK=-1;scheduleRender();});
SL.alpha.input.addEventListener("input", scheduleRender);
renderAll();
outer.value = {};
return outer;
}2.2.1 Hàm khối/mật độ xác suất
Là một hàm số để tính xác suất cho từng giá trị của biến ngẫu nhiên. Hàm này có hai thành phần:
\[f(\underbrace{X = x}_{\text{Giá trị biến ngẫu nhiên}} \mid \underbrace{\theta}_{\text{Tham số}})\]
Giá trị của biến ngẫu nhiên: nằm trên trục hoành của đồ thị
Tham số: là một hoặc nhiều yếu tố quy định xác suất của các giá trị của biến ngẫu nhiên
Với biến rời rạc thì gọi là hàm khối xác suất (probability mass function, pmf). Với biến liên tục thì gọi là hàm mật độ xác suất (probability density function, pdf).
Ví dụ: Hàm khối xác suất của phân phối nhị thức được viết như sau
\[f(\underbrace{X = k}_{\text{Giá trị biến ngẫu nhiên}} \mid \underbrace{n, p}_{\text{Tham số}}) = \binom{n}{k} p^k (1-p)^{n-k}\]
Phân phối nhị thức dùng để đếm số lần thành công trong một số lượng cố định các phép thử Bernoulli độc lập.
Nếu phép thử là tung một đồng xu 10 lần, chúng ta quan tâm có bao nhiêu lần ra mặt ngửa. Hai thành phần của hàm pmf này là:
Giá trị của biến ngẫu nhiên (\(k\)): là số lần ra mặt ngửa mà ta muốn tính xác suất. \(k\) có thể có các giá trị từ 0 đến 10.
Tham số: có 2 yếu tố quy định xác suất của các giá trị \(k\)
- Tổng số lần tung đồng xu (\(n = 10\)): vì tổng xác suất luôn bằng 1, càng tung đồng xu nhiều lần thì xác suất để xảy ra mỗi trường hợp nhất định sẽ nhỏ đi
- Xác suất ra mặt ngửa của mỗi lần tung đồng xu \(p = 0.5\): vì khả năng mỗi lần ra mặt ngửa càng lớn thì tổng số mặt ngửa \(k\) ra càng nhiều và ngược lại
Chúng ta gắn các tham số \(n\) và \(p\) mà mình muốn vào hàm pmf. Sau đó lần lượt gắn \(k = 0, 1, 2...\) vào để tính ra xác suất của từng giá trị của biến ngẫu nhiên.
viewof binomial_dist = (() => {
// ══════════════════════════════════════════════════════
// 1. MATH
// ══════════════════════════════════════════════════════
const lcCache = new Map();
function lc(n,k){
if(k<0||k>n)return -Infinity;
if(k>n-k)k=n-k;
const key=(n<<12)|k;
if(lcCache.has(key))return lcCache.get(key);
let r=0;for(let i=0;i<k;i++)r+=Math.log(n-i)-Math.log(i+1);
lcCache.set(key,r);return r;
}
function pmf(x,n,p){
if(x<0||x>n)return 0;
if(p<=0)return x===0?1:0;
if(p>=1)return x===n?1:0;
return Math.exp(lc(n,x)+x*Math.log(p)+(n-x)*Math.log(1-p));
}
// ══════════════════════════════════════════════════════
// 2. WRAPPER + STYLE
// ══════════════════════════════════════════════════════
const wrapper = document.createElement("div");
wrapper.style.cssText = `
display:flex;flex-direction:column;align-items:center;
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
width:100%;max-width:900px;margin:0 auto;
`;
const style = document.createElement("style");
style.textContent = `
.bd-slider{position:relative;height:8px;border-radius:4px;background:#e2e8f0;}
.bd-slider-fill{position:absolute;left:0;top:0;height:100%;border-radius:4px;transition:width 0.04s;}
.bd-slider input[type=range]{
position:absolute;top:0;left:0;width:100%;height:100%;
-webkit-appearance:none;appearance:none;background:transparent;
cursor:pointer;margin:0;padding:0;
}
.bd-slider input[type=range]::-webkit-slider-thumb{
-webkit-appearance:none;appearance:none;
width:20px;height:20px;border-radius:50%;
border:3px solid #fff;box-shadow:0 1px 5px rgba(0,0,0,0.28);
cursor:pointer;margin-top:-6px;background:#475569;
}
.bd-slider input[type=range]::-moz-range-thumb{
width:14px;height:14px;border-radius:50%;
border:3px solid #fff;box-shadow:0 1px 5px rgba(0,0,0,0.28);
cursor:pointer;background:#475569;
}
.bd-slider input[type=range]::-webkit-slider-runnable-track{height:8px;background:transparent;}
.bd-slider input[type=range]::-moz-range-track{height:8px;background:transparent;}
.bd-slider-dark input[type=range]::-webkit-slider-thumb{background:#1e293b;}
.bd-slider-dark input[type=range]::-moz-range-thumb{background:#1e293b;}
.bd-slider-blue input[type=range]::-webkit-slider-thumb{background:#3b82f6;}
.bd-slider-blue input[type=range]::-moz-range-thumb{background:#3b82f6;}
`;
wrapper.appendChild(style);
// ══════════════════════════════════════════════════════
// 3. SLIDERS
// ══════════════════════════════════════════════════════
function createSlider(label, min, max, step, val, color, cls) {
const row = document.createElement("div");
row.style.cssText = "display:flex;flex-direction:column;flex:1;min-width:140px;";
const head = document.createElement("div");
head.style.cssText = "display:flex;justify-content:space-between;align-items:baseline;margin-bottom:5px;";
const lbl = document.createElement("span");
lbl.style.cssText = "font-size:12px;font-weight:600;color:#64748b;letter-spacing:0.3px;text-transform:uppercase;";
lbl.textContent = label;
const valSpan = document.createElement("span");
valSpan.style.cssText = `font-size:18px;font-weight:800;color:${color};font-variant-numeric:tabular-nums;font-family:"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;`;
valSpan.textContent = step < 1 ? Number(val).toFixed(2) : val;
head.appendChild(lbl); head.appendChild(valSpan);
const track = document.createElement("div");
track.className = "bd-slider bd-slider-" + cls;
const fill = document.createElement("div");
fill.className = "bd-slider-fill";
fill.style.background = color;
fill.style.width = ((val - min) / (max - min)) * 100 + "%";
track.appendChild(fill);
const input = document.createElement("input");
input.type = "range"; input.min = min; input.max = max;
input.step = step; input.value = val;
track.appendChild(input);
row.appendChild(head); row.appendChild(track);
return { el: row, input, valSpan, fill,
val() { return +input.value; },
sync() {
const v = +input.value;
valSpan.textContent = step < 1 ? v.toFixed(2) : String(v);
fill.style.width = ((v - min) / (max - min)) * 100 + "%";
}
};
}
const SL = {};
// Changed default 'n' from 20 to 10
SL.n = createSlider("Số phép thử (n)", 1, 100, 1, 10, "#1e293b", "dark");
SL.p = createSlider("Xác suất thành công mỗi lần (p)", 0.01, 0.99, 0.01, 0.50, "#3b82f6", "blue");
const ctrlRow = document.createElement("div");
ctrlRow.style.cssText = "display:flex;gap:28px;width:100%;margin-bottom:14px;padding-bottom:14px;border-bottom:1px solid #e2e8f0;";
ctrlRow.appendChild(SL.n.el); ctrlRow.appendChild(SL.p.el);
wrapper.appendChild(ctrlRow);
// ══════════════════════════════════════════════════════
// 4. SVG CHART (pre-allocated pools)
// ══════════════════════════════════════════════════════
const NS = "http://www.w3.org/2000/svg";
const W = 760, H = 380;
// Increased margins to accommodate larger text sizes
const mg = {t:20, r:20, b:56, l:64};
const cw = W-mg.l-mg.r, ch = H-mg.t-mg.b;
const svg = document.createElementNS(NS,"svg");
svg.setAttribute("viewBox",`0 0 ${W} ${H}`);
svg.style.cssText = `width:100%;max-width:${W}px;background:#fafbfc;border-radius:10px;border:1px solid #e2e8f0;`;
function el(tag,a){const e=document.createElementNS(NS,tag);if(a)for(const[k,v]of Object.entries(a))e.setAttribute(k,v);return e;}
// Grid
const gridLines=[];
for(let i=0;i<=4;i++){const l=el("line",{stroke:"#e2e8f0","stroke-width":"0.7"});svg.appendChild(l);gridLines.push(l);}
// Y labels - Increased font-size to 14
const yLabels=[];
for(let i=0;i<=4;i++){const t=el("text",{"text-anchor":"end",fill:"#94a3b8","font-size":"14","font-family":"'SF Mono',monospace"});svg.appendChild(t);yLabels.push(t);}
// Bar pool
const POOL=101;
const bars=[];
for(let i=0;i<POOL;i++){
const r=el("rect",{rx:"2",fill:"#3b82f6",stroke:"#2563eb","stroke-width":"0.5"});
r.style.display="none";svg.appendChild(r);bars.push(r);
}
// Hover bar
const hoverBar=el("rect",{rx:"2",fill:"rgba(59,130,246,0.35)",stroke:"#1d4ed8","stroke-width":"1"});
hoverBar.style.display="none";svg.appendChild(hoverBar);
// X axis
const xAxisLine=el("line",{stroke:"#94a3b8"});svg.appendChild(xAxisLine);
const xTicks=[];
for(let i=0;i<30;i++){
const ln=el("line",{stroke:"#94a3b8",y2:"5"});ln.style.display="none";svg.appendChild(ln);
// Increased font-size to 14 and dy to 20
const t=el("text",{"text-anchor":"middle",fill:"#64748b","font-size":"14","font-family":"'SF Mono',monospace",dy:"20"});t.style.display="none";svg.appendChild(t);
xTicks.push({ln,t});
}
// Increased font-size to 16
const xLabel=el("text",{"text-anchor":"middle",fill:"#64748b","font-size":"16",y:String(H-6)});
xLabel.textContent="Số lần thành công (k)";svg.appendChild(xLabel);
// Y axis
svg.appendChild(el("line",{x1:String(mg.l),x2:String(mg.l),y1:String(mg.t),y2:String(mg.t+ch),stroke:"#94a3b8"}));
// Increased font-size to 16
const yLabel=el("text",{"text-anchor":"middle",fill:"#64748b","font-size":"16",x:"18",y:String(mg.t+ch/2),transform:`rotate(-90,18,${mg.t+ch/2})`});
yLabel.textContent="P(X = k)";svg.appendChild(yLabel);
// Tooltip
const tipG=el("g");tipG.style.display="none";
const tipRect=el("rect",{rx:"6",fill:"#1e293b",opacity:"0.92"});
// Increased font-sizes to 14
const tipText1=el("text",{fill:"#fff","font-size":"14","font-weight":"700","font-family":"'SF Mono',monospace"});
const tipText2=el("text",{fill:"#94a3b8","font-size":"14","font-family":"'SF Mono',monospace"});
tipG.appendChild(tipRect);tipG.appendChild(tipText1);tipG.appendChild(tipText2);
svg.appendChild(tipG);
wrapper.appendChild(svg);
// ══════════════════════════════════════════════════════
// 5. RENDER
// ══════════════════════════════════════════════════════
let curData=[], curMaxY=0, hoverIdx=-1;
let sx, sy, barW, baseline;
function render(){
const nV=SL.n.val(), pV=SL.p.val();
const mean=nV*pV, sd=Math.sqrt(nV*pV*(1-pV));
let loX=Math.max(0,Math.floor(mean-4*sd-1));
let hiX=Math.min(nV,Math.ceil(mean+4*sd+1));
if(nV<=30){loX=0;hiX=nV;}
curData=[];curMaxY=0;
for(let x=loX;x<=hiX;x++){
const p=pmf(x,nV,pV);
curData.push({x,prob:p});
if(p>curMaxY)curMaxY=p;
}
curMaxY*=1.15;
if(curMaxY<1e-9)curMaxY=0.01;
const cnt=hiX-loX+1;
sx=x=>mg.l+((x-loX)/(hiX-loX))*cw;
sy=y=>mg.t+ch-(y/curMaxY)*ch;
barW=Math.max(2,Math.min(28,cw/cnt*0.75));
baseline=sy(0);
// Grid + Y labels
for(let i=0;i<=4;i++){
const v=(curMaxY/4)*i,yy=sy(v);
gridLines[i].setAttribute("x1",mg.l);gridLines[i].setAttribute("x2",W-mg.r);
gridLines[i].setAttribute("y1",yy);gridLines[i].setAttribute("y2",yy);
yLabels[i].setAttribute("x",mg.l-8);yLabels[i].setAttribute("y",yy+4);
yLabels[i].textContent=v.toFixed(v<0.01?4:3);
}
// Bars
for(let i=0;i<POOL;i++){
if(i<curData.length){
const d=curData[i];
const bx=sx(d.x)-barW/2,by=sy(d.prob);
bars[i].setAttribute("x",bx);bars[i].setAttribute("y",by);
bars[i].setAttribute("width",barW);bars[i].setAttribute("height",Math.max(0,baseline-by));
bars[i].style.display="";
}else{bars[i].style.display="none";}
}
// X axis
xAxisLine.setAttribute("x1",mg.l);xAxisLine.setAttribute("x2",W-mg.r);
xAxisLine.setAttribute("y1",baseline);xAxisLine.setAttribute("y2",baseline);
xLabel.setAttribute("x",mg.l+cw/2);
const span=hiX-loX;
const ts=span>80?10:span>40?5:span>20?2:1;
let ti=0;
for(let x=Math.ceil(loX/ts)*ts;x<=hiX&&ti<30;x+=ts){
const xx=sx(x);
xTicks[ti].ln.setAttribute("x1",xx);xTicks[ti].ln.setAttribute("x2",xx);
xTicks[ti].ln.setAttribute("y1",baseline);xTicks[ti].ln.setAttribute("y2",baseline+5);
xTicks[ti].ln.style.display="";
xTicks[ti].t.setAttribute("x",xx);xTicks[ti].t.setAttribute("y",baseline+5);
xTicks[ti].t.textContent=x;xTicks[ti].t.style.display="";
ti++;
}
for(;ti<30;ti++){xTicks[ti].ln.style.display="none";xTicks[ti].t.style.display="none";}
updateHover();
wrapper.value={n:nV,p:pV};
wrapper.dispatchEvent(new Event("input",{bubbles:true}));
}
// ══════════════════════════════════════════════════════
// 6. HOVER
// ══════════════════════════════════════════════════════
function updateHover(){
if(hoverIdx<0||hoverIdx>=curData.length){
hoverBar.style.display="none";tipG.style.display="none";return;
}
const d=curData[hoverIdx];
const bx=sx(d.x)-barW/2,by=sy(d.prob);
hoverBar.setAttribute("x",bx-1);hoverBar.setAttribute("y",by-1);
hoverBar.setAttribute("width",barW+2);hoverBar.setAttribute("height",Math.max(0,baseline-by)+2);
hoverBar.style.display="";
const t1="k = "+d.x;
const t2="P = "+d.prob.toFixed(5);
const tw=Math.max(t1.length,t2.length)*8.5+16; // Adjusted for wider text
const th=50; // Increased height
let tx=sx(d.x)+barW/2+8,ty=by-10;
if(tx+tw>W-mg.r)tx=sx(d.x)-barW/2-tw-8;
if(ty<mg.t)ty=mg.t+4;
tipRect.setAttribute("x",tx);tipRect.setAttribute("y",ty);
tipRect.setAttribute("width",tw);tipRect.setAttribute("height",th);
tipText1.setAttribute("x",tx+8);tipText1.setAttribute("y",ty+20);tipText1.textContent=t1;
tipText2.setAttribute("x",tx+8);tipText2.setAttribute("y",ty+40);tipText2.textContent=t2;
tipG.style.display="";
}
function onMove(e){
const rect=svg.getBoundingClientRect();
const mouseX=(e.clientX-rect.left)*(W/rect.width);
if(mouseX<mg.l||mouseX>W-mg.r||!curData.length){hoverIdx=-1;updateHover();return;}
let best=-1,bestD=Infinity;
for(let i=0;i<curData.length;i++){
const dist=Math.abs(sx(curData[i].x)-mouseX);
if(dist<bestD){bestD=dist;best=i;}
}
if(bestD>barW*1.2)best=-1;
if(best!==hoverIdx){hoverIdx=best;updateHover();}
}
function onLeave(){hoverIdx=-1;updateHover();}
svg.addEventListener("mousemove",onMove);
svg.addEventListener("mouseleave",onLeave);
// ══════════════════════════════════════════════════════
// 7. EVENTS
// ══════════════════════════════════════════════════════
let rafId=0;
function schedule(){cancelAnimationFrame(rafId);rafId=requestAnimationFrame(render);}
function onN(){SL.n.sync();schedule();}
function onP(){SL.p.sync();schedule();}
SL.n.input.addEventListener("input",onN);
SL.p.input.addEventListener("input",onP);
render();
// ══════════════════════════════════════════════════════
// 8. CLEANUP
// ══════════════════════════════════════════════════════
invalidation.then(()=>{
cancelAnimationFrame(rafId);
SL.n.input.removeEventListener("input",onN);
SL.p.input.removeEventListener("input",onP);
svg.removeEventListener("mousemove",onMove);
svg.removeEventListener("mouseleave",onLeave);
lcCache.clear();curData=[];
});
wrapper.value={};
return wrapper;
})()2.2.2 Hàm phân phối tích lũy
Là một hàm số để tính tổng xác suất cộng dồn từ giá trị nhỏ nhất có thể có cho đến một giá trị giới hạn của biến ngẫu nhiên. Hàm phân phối tích lũy (cumulative distribution function, cdf, kí hiệu là \(F(x)\)) cũng có hai thành phần giống với hàm pmf/pdf:
\[F(\underbrace{X \le x}_{\text{Giá trị biến ngẫu nhiên}} \mid \underbrace{\theta}_{\text{Tham số}})\]
Hàm cdf đơn giản là phép cộng dồn tất cả các xác suất của pmf/pdf, từ đầu đến giá trị giới hạn \(x\).
Với biến rời rạc, cdf được tính bằng tổng (\(\sum\)) của pmf:
\[F(x) = \mathbb{P}(X \le x) = \sum_{x_i \le x} f(x_i)\]
Với biến liên tục, cdf được tính bằng tích phân (\(\int\)) của pdf:
\[F(x) = \mathbb{P}(X \le x) = \int_{-\infty}^{x} f(t) \, dt\]
Khi lấy đạo hàm bậc nhất của cdf thì chúng ta có pdf:
\[F'(x) = f(x)\]
viewof cdfAccum = {
const C = {
txt:'#16213E', sub:'#8294AA', light:'#E4E9F0',
card:'#fff', grid:'#F0F2F5',
pdf:'#0d9488', pdfFill:'#ccfbf1',
cdf:'#6366f1', cdfFill:'#dbeafe',
cursor:'#e11d48', cursorSoft:'#ffe4e6',
slope:'#d97706',
dimmed:'#e2e8f0',
};
const mono = "'SF Mono',SFMono-Regular,Menlo,monospace";
// ═══════════════════ MATH ═══════════════════
const _lf=[0];
function lf(n){while(_lf.length<=n)_lf.push(_lf[_lf.length-1]+Math.log(_lf.length));return _lf[n];}
function lnC(n,k){return(k<0||k>n)?-Infinity:lf(n)-lf(k)-lf(n-k);}
function binPmf(k,n,p){
if(k<0||k>n)return 0;if(p<=0)return k===0?1:0;if(p>=1)return k===n?1:0;
return Math.exp(lnC(n,k)+k*Math.log(p)+(n-k)*Math.log(1-p));
}
function binCdf(k,n,p){let s=0;for(let i=0;i<=Math.floor(k);i++)s+=binPmf(i,n,p);return Math.min(1,s);}
function lnGamma(z){
if(z<0.5)return Math.log(Math.PI/Math.sin(Math.PI*z))-lnGamma(1-z);
z-=1;const c=[0.99999999999980993,676.5203681218851,-1259.1392167224028,771.32342877765313,
-176.61502916214059,12.507343278686905,-0.13857109526572012,9.9843695780195716e-6,1.5056327351493116e-7];
let x=c[0];for(let i=1;i<9;i++)x+=c[i]/(z+i);const t=z+7.5;
return 0.5*Math.log(2*Math.PI)+(z+0.5)*Math.log(t)-t+Math.log(x);
}
function gammaPdf(x,a,b){
if(x<=0)return a<1?Infinity:a===1?b:0;if(x<1e-300)return 0;
return Math.exp(a*Math.log(b)-lnGamma(a)+(a-1)*Math.log(x)-b*x);
}
function gammainc(a,x){
if(x<=0)return 0;if(x>a+40||x>200)return 1-gammainc_ucf(a,x);
let sum=1/a,term=1/a;
for(let n=1;n<300;n++){term*=x/(a+n);sum+=term;if(Math.abs(term)<Math.abs(sum)*1e-14)break;}
return Math.exp(-x+a*Math.log(x)-lnGamma(a))*sum;
}
function gammainc_ucf(a,x){
const lnp=-x+a*Math.log(x)-lnGamma(a);
let d=x+1-a;if(Math.abs(d)<1e-30)d=1e-30;d=1/d;let f=d,cc=1e-30;
for(let i=1;i<200;i++){const an=i*(a-i),bn=x+2*i+1-a;
d=bn+an*d;if(Math.abs(d)<1e-30)d=1e-30;d=1/d;
cc=bn+an/cc;if(Math.abs(cc)<1e-30)cc=1e-30;
const delta=cc*d;f*=delta;if(Math.abs(delta-1)<1e-14)break;}
return Math.exp(lnp)*f;
}
function gammaCdf(x,a,b){return x<=0?0:gammainc(a,b*x);}
function curvePts(fn,lo,hi,n){
const p=[],dx=(hi-lo)/(n-1);
for(let i=0;i<n;i++){const x=lo+i*dx,y=fn(x);if(isFinite(y))p.push({x,y});}
return p;
}
// ═══════════════════ DOM ═══════════════════
const outer=document.createElement("div");
outer.style.cssText=`display:flex;flex-direction:column;gap:14px;
font-family:'Inter',-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
width:100%;max-width:960px;margin:0 auto;`;
if(!document.querySelector('link[href*="Inter"]')){
const lk=document.createElement('link');lk.rel='stylesheet';
lk.href='https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap';
document.head.appendChild(lk);
}
outer.appendChild(injectStyle());
// ═══════════════════ MODE TOGGLE ═══════════════════
let mode='discrete'; // 'discrete' | 'continuous'
const toggleRow=document.createElement("div");
toggleRow.style.cssText="display:flex;gap:6px;justify-content:center;";
const modeConfig={
discrete:{label:'Rời rạc',color:C.pdf},
continuous:{label:'Liên tục',color:C.cdf},
};
const modeBtns={};
Object.entries(modeConfig).forEach(([k,cfg])=>{
const b=document.createElement("button");
b.style.cssText=`padding:10px 28px;border-radius:10px;border:2px solid ${C.light};
font-size:14px;font-weight:700;cursor:pointer;font-family:inherit;transition:all 0.15s;`;
b.textContent=cfg.label;
b.addEventListener('click',()=>{mode=k;styleMode();renderAll();});
toggleRow.appendChild(b);modeBtns[k]=b;
});
outer.appendChild(toggleRow);
function styleMode(){
Object.entries(modeBtns).forEach(([k,b])=>{
const on=k===mode,col=modeConfig[k].color;
b.style.background=on?col:C.card;b.style.color=on?'#fff':C.txt;b.style.borderColor=on?col:C.light;
});
// Show/hide relevant sliders
slDiscrete.style.display=mode==='discrete'?'flex':'none';
slContinuous.style.display=mode==='continuous'?'flex':'none';
}
// ═══════════════════ SLIDERS ═══════════════════
const SL={};
// Discrete params
SL.n=createSlider("n",2,30,1,12,C.pdf,"teal");
SL.p=createSlider("p",0.01,0.99,0.01,0.35,C.pdf,"teal");
const slDiscrete=document.createElement("div");
slDiscrete.style.cssText="display:flex;gap:14px;width:100%;";
slDiscrete.append(SL.n.el,SL.p.el);
outer.appendChild(slDiscrete);
// Continuous params
SL.a=createSlider("shape",0.5,10,0.5,3,C.cdf,"purple");
SL.b=createSlider("rate",0.2,4,0.2,1,C.cdf,"purple");
const slContinuous=document.createElement("div");
slContinuous.style.cssText="display:flex;gap:14px;width:100%;";
slContinuous.append(SL.a.el,SL.b.el);
outer.appendChild(slContinuous);
// ═══════════════════ INFO LINE ═══════════════════
const infoEl=document.createElement("div");
infoEl.style.cssText=`font-size:13px;color:${C.txt};text-align:center;line-height:1.7;font-weight:600;
background:${C.card};border-radius:12px;border:1px solid ${C.light};padding:10px 14px;font-family:${mono};`;
outer.appendChild(infoEl);
// ═══════════════════ PANELS ═══════════════════
const panelRow=document.createElement("div");
panelRow.style.cssText="display:grid;grid-template-columns:1fr 1fr;gap:14px;width:100%;";
outer.appendChild(panelRow);
const VW=460,VH=380;
function mkPanel(titleHtml,color){
const card=document.createElement("div");
card.style.cssText=`background:${C.card};border-radius:16px;border:1px solid ${C.light};
padding:14px;box-shadow:0 2px 16px rgba(0,0,0,.04);`;
const title=document.createElement("div");
title.style.cssText=`font-size:14px;font-weight:800;color:${color};margin-bottom:8px;text-align:center;`;
title.innerHTML=titleHtml;card.appendChild(title);
const svg=d3.create("svg").attr("viewBox",[0,0,VW,VH])
.style("width","100%").style("display","block").style("border-radius","12px")
.style("background","#FAFBFD").style("border",`1px solid ${C.light}`);
card.appendChild(svg.node());panelRow.appendChild(card);
return{svg,title,VW,VH};
}
const pdfPanel=mkPanel('PDF / PMF',C.pdf);
const cdfPanel=mkPanel('CDF — Accumulating',C.cdf);
// ═══════════════════ STATE ═══════════════════
let cursorX=null; // the x-value where the scanline sits (null = show everything)
let dragging=false;
// ═══════════════════ RENDER ═══════════════════
function renderAll(){
Object.values(SL).forEach(s=>s.sync());
const isD=mode==='discrete';
if(isD){
const n=SL.n.val(), p=SL.p.val();
renderDiscrete(n,p);
} else {
const a=SL.a.val(), b=SL.b.val();
renderContinuous(a,b);
}
}
// ═══════════════════ DISCRETE RENDER ═══════════════════
function renderDiscrete(n,p){
const mg={l:52,r:14,t:16,b:44};
const iw=VW-mg.l-mg.r,ih=VH-mg.t-mg.b;
// Domain
const domain=d3.range(0,n+1);
const pmfVals=domain.map(k=>binPmf(k,n,p));
const cdfVals=domain.map(k=>binCdf(k,n,p));
const yMaxPmf=d3.max(pmfVals)*1.18;
const xS=d3.scaleBand().domain(domain).range([0,iw]).padding(0.2);
const yPmf=d3.scaleLinear().domain([0,yMaxPmf]).range([ih,0]);
const yCdf=d3.scaleLinear().domain([0,1.05]).range([ih,0]);
// xS continuous for cursor mapping
const xLinear=d3.scaleLinear().domain([-0.5,n+0.5]).range([0,iw]);
const cursorK=cursorX!==null?Math.round(Math.max(-0.5,Math.min(n+0.5,cursorX))):n;
const cursorXPx=xLinear(cursorX!==null?cursorX:n+0.5);
// Current CDF value at cursor
const cdfAtCursor=cursorK>=0?binCdf(cursorK,n,p):0;
const pmfAtCursor=cursorK>=0&&cursorK<=n?binPmf(cursorK,n,p):0;
pdfPanel.title.innerHTML=`PMF — Bin(${n}, ${p.toFixed(2)})`;
cdfPanel.title.innerHTML=`CDF — Accumulating the PMF`;
// ── PMF panel ──
{
const{svg}=pdfPanel; svg.selectAll("*").remove();
const g=svg.append("g").attr("transform",`translate(${mg.l},${mg.t})`);
// Grid
yPmf.ticks(5).forEach(v=>{
g.append("line").attr("x2",iw).attr("y1",yPmf(v)).attr("y2",yPmf(v)).attr("stroke",C.grid);
g.append("text").attr("x",-6).attr("y",yPmf(v)+4).attr("text-anchor","end")
.attr("font-size",11).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(2));
});
// Bars
domain.forEach(k=>{
const pk=pmfVals[k],bx=xS(k),by=yPmf(pk),bh=ih-by;
const active=k<=cursorK;
const isCurrent=k===cursorK&&cursorK>=0&&cursorK<=n;
g.append("rect").attr("x",bx).attr("y",by).attr("width",xS.bandwidth()).attr("height",bh)
.attr("rx",4).attr("fill",active?C.pdf:C.dimmed)
.attr("opacity",active?(isCurrent?1:0.6):0.3);
if(isCurrent){
// Highlight the bar that's being "added" right now
g.append("rect").attr("x",bx-2).attr("y",by-2).attr("width",xS.bandwidth()+4).attr("height",bh+4)
.attr("rx",6).attr("fill","none").attr("stroke",C.cursor).attr("stroke-width",2.5);
// Label
g.append("text").attr("x",bx+xS.bandwidth()/2).attr("y",by-8)
.attr("text-anchor","middle").attr("font-size",11).attr("font-weight",800)
.attr("fill",C.cursor).attr("font-family",mono).text(`+${pk.toFixed(3)}`);
}
});
// X labels
g.append("line").attr("x2",iw).attr("y1",ih).attr("y2",ih).attr("stroke",C.sub).attr("opacity",0.25);
const step=n>20?2:1;
domain.forEach(k=>{
if(k%step!==0&&k!==n)return;
g.append("text").attr("x",xS(k)+xS.bandwidth()/2).attr("y",ih+18)
.attr("text-anchor","middle").attr("font-size",11)
.attr("font-weight",k===cursorK?800:500)
.attr("fill",k===cursorK?C.cursor:C.sub).attr("font-family",mono).text(k);
});
svg.append("text").attr("x",mg.l+iw/2).attr("y",VH-2)
.attr("text-anchor","middle").attr("font-size",12).attr("font-weight",700).attr("fill",C.sub).text("k");
svg.append("text").attr("x",12).attr("y",mg.t+ih/2)
.attr("text-anchor","middle").attr("font-size",12).attr("font-weight",700).attr("fill",C.sub)
.attr("transform",`rotate(-90,12,${mg.t+ih/2})`).text("P(X = k)");
// Hit area for dragging
g.append("rect").attr("width",iw).attr("height",ih).attr("fill","transparent").attr("cursor","col-resize")
.on("mousedown",()=>{dragging=true;})
.on("mousemove",(e)=>{if(!dragging)return;const[mx]=d3.pointer(e);cursorX=xLinear.invert(mx);renderAll();})
.on("mouseup",()=>{dragging=false;})
.on("mouseleave",()=>{dragging=false;})
.on("click",(e)=>{const[mx]=d3.pointer(e);cursorX=xLinear.invert(mx);renderAll();});
}
// ── CDF panel ──
{
const{svg}=cdfPanel; svg.selectAll("*").remove();
const g=svg.append("g").attr("transform",`translate(${mg.l},${mg.t})`);
[0,0.25,0.5,0.75,1].forEach(v=>{
g.append("line").attr("x2",iw).attr("y1",yCdf(v)).attr("y2",yCdf(v)).attr("stroke",C.grid);
g.append("text").attr("x",-6).attr("y",yCdf(v)+4).attr("text-anchor","end")
.attr("font-size",11).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(2));
});
// Draw CDF as step function up to cursorK
// Full CDF in dimmed
for(let k=0;k<=n;k++){
const cx=xS(k)+xS.bandwidth()/2;
if(k<n){
const nx=xS(k+1)+xS.bandwidth()/2;
g.append("line").attr("x1",cx).attr("x2",nx).attr("y1",yCdf(cdfVals[k])).attr("y2",yCdf(cdfVals[k]))
.attr("stroke",C.cdf).attr("stroke-width",1).attr("opacity",0.15);
}
}
// Active CDF steps (up to cursor)
for(let k=0;k<=Math.min(cursorK,n);k++){
const cx=xS(k)+xS.bandwidth()/2;
const prevCdf=k>0?cdfVals[k-1]:0;
// Vertical riser (the "jump")
if(k<=cursorK){
g.append("line").attr("x1",cx).attr("x2",cx)
.attr("y1",yCdf(prevCdf)).attr("y2",yCdf(cdfVals[k]))
.attr("stroke",k===cursorK?C.cursor:C.cdf)
.attr("stroke-width",k===cursorK?3:2)
.attr("opacity",k===cursorK?1:0.7);
}
// Horizontal segment
if(k<Math.min(cursorK,n)){
const nx=xS(k+1)+xS.bandwidth()/2;
g.append("line").attr("x1",cx).attr("x2",nx)
.attr("y1",yCdf(cdfVals[k])).attr("y2",yCdf(cdfVals[k]))
.attr("stroke",C.cdf).attr("stroke-width",2).attr("opacity",0.7);
} else if(k===cursorK&&k<=n){
// Extend to cursor position or rightward
const nx=k<n?xS(k+1)+xS.bandwidth()/2:iw;
g.append("line").attr("x1",cx).attr("x2",Math.min(nx,cursorXPx+mg.l>mg.l?cursorXPx:nx))
.attr("y1",yCdf(cdfVals[k])).attr("y2",yCdf(cdfVals[k]))
.attr("stroke",C.cdf).attr("stroke-width",2);
}
// Dot at each step
g.append("circle").attr("cx",cx).attr("cy",yCdf(cdfVals[k]))
.attr("r",k===cursorK?5:3)
.attr("fill",k===cursorK?C.cursor:C.cdf)
.attr("stroke","#fff").attr("stroke-width",k===cursorK?2:1);
}
// Jump annotation: show the height of the current jump
if(cursorK>=0&&cursorK<=n){
const cx=xS(cursorK)+xS.bandwidth()/2;
const prevCdf_=cursorK>0?cdfVals[cursorK-1]:0;
const jumpH=pmfAtCursor;
// Bracket showing jump height
const bx=cx+14;
g.append("line").attr("x1",bx).attr("x2",bx)
.attr("y1",yCdf(prevCdf_)).attr("y2",yCdf(cdfVals[cursorK]))
.attr("stroke",C.slope).attr("stroke-width",2.5);
g.append("line").attr("x1",bx-4).attr("x2",bx+4)
.attr("y1",yCdf(prevCdf_)).attr("y2",yCdf(prevCdf_))
.attr("stroke",C.slope).attr("stroke-width",2);
g.append("line").attr("x1",bx-4).attr("x2",bx+4)
.attr("y1",yCdf(cdfVals[cursorK])).attr("y2",yCdf(cdfVals[cursorK]))
.attr("stroke",C.slope).attr("stroke-width",2);
g.append("text").attr("x",bx+8).attr("y",(yCdf(prevCdf_)+yCdf(cdfVals[cursorK]))/2+4)
.attr("font-size",11).attr("font-weight",800).attr("fill",C.slope)
.attr("font-family",mono).text(`+${jumpH.toFixed(3)}`);
}
// CDF value label
g.append("rect").attr("x",-50).attr("y",yCdf(cdfAtCursor)-11).attr("width",44).attr("height",22)
.attr("rx",5).attr("fill",C.cdf);
g.append("text").attr("x",-28).attr("y",yCdf(cdfAtCursor)+5)
.attr("text-anchor","middle").attr("font-size",11).attr("font-weight",800)
.attr("fill","#fff").attr("font-family",mono).text(cdfAtCursor.toFixed(3));
g.append("line").attr("x2",iw).attr("y1",ih).attr("y2",ih).attr("stroke",C.sub).attr("opacity",0.25);
const step=n>20?2:1;
domain.forEach(k=>{
if(k%step!==0&&k!==n)return;
g.append("text").attr("x",xS(k)+xS.bandwidth()/2).attr("y",ih+18)
.attr("text-anchor","middle").attr("font-size",11)
.attr("font-weight",k===cursorK?800:500)
.attr("fill",k===cursorK?C.cursor:C.sub).attr("font-family",mono).text(k);
});
svg.append("text").attr("x",mg.l+iw/2).attr("y",VH-2)
.attr("text-anchor","middle").attr("font-size",12).attr("font-weight",700).attr("fill",C.sub).text("k");
svg.append("text").attr("x",12).attr("y",mg.t+ih/2)
.attr("text-anchor","middle").attr("font-size",12).attr("font-weight",700).attr("fill",C.sub)
.attr("transform",`rotate(-90,12,${mg.t+ih/2})`).text("F(k)");
// Hit area
g.append("rect").attr("width",iw).attr("height",ih).attr("fill","transparent").attr("cursor","col-resize")
.on("mousedown",()=>{dragging=true;})
.on("mousemove",(e)=>{if(!dragging)return;const[mx]=d3.pointer(e);cursorX=xLinear.invert(mx);renderAll();})
.on("mouseup",()=>{dragging=false;})
.on("mouseleave",()=>{dragging=false;})
.on("click",(e)=>{const[mx]=d3.pointer(e);cursorX=xLinear.invert(mx);renderAll();});
}
// Info
infoEl.innerHTML=`<span style="color:${C.cursor};">k = ${cursorK}</span> \u2502 `
+`PMF: P(X=${cursorK}) = <span style="color:${C.pdf};">${pmfAtCursor.toFixed(4)}</span> \u2502 `
+`CDF: F(${cursorK}) = <span style="color:${C.cdf};">${cdfAtCursor.toFixed(4)}</span>`;
}
// ═══════════════════ CONTINUOUS RENDER ═══════════════════
function renderContinuous(a,b){
const mg={l:52,r:14,t:16,b:44};
const iw=VW-mg.l-mg.r,ih=VH-mg.t-mg.b;
const mean=a/b, sd=Math.sqrt(a/(b*b));
const xLo=0, xHi=Math.max(mean+4.5*sd, 1);
const nPts=300;
const xS=d3.scaleLinear().domain([xLo,xHi]).range([0,iw]);
const cx=cursorX!==null?Math.max(xLo,Math.min(xHi,cursorX)):xHi;
const cxPx=xS(cx);
const pdfVal=gammaPdf(cx,a,b);
const cdfVal=gammaCdf(cx,a,b);
const peakPdf=a>=1?gammaPdf((a-1)/b,a,b):gammaPdf(0.01,a,b);
const capY=a<1?Math.min(peakPdf,b*15):peakPdf;
const yMaxPdf=capY*1.15;
const yPdf=d3.scaleLinear().domain([0,yMaxPdf]).range([ih,0]);
const yCdf=d3.scaleLinear().domain([0,1.05]).range([ih,0]);
pdfPanel.title.innerHTML=`PDF — Gamma(\u03B1=${a.toFixed(1)}, \u03B2=${b.toFixed(1)})`;
cdfPanel.title.innerHTML=`CDF — Accumulating the PDF`;
const pts=curvePts(x=>Math.min(gammaPdf(x,a,b),capY),Math.max(1e-6,xLo),xHi,nPts);
// ── PDF panel ──
{
const{svg}=pdfPanel;svg.selectAll("*").remove();
const g=svg.append("g").attr("transform",`translate(${mg.l},${mg.t})`);
yPdf.ticks(5).forEach(v=>{
g.append("line").attr("x2",iw).attr("y1",yPdf(v)).attr("y2",yPdf(v)).attr("stroke",C.grid);
g.append("text").attr("x",-6).attr("y",yPdf(v)+4).attr("text-anchor","end")
.attr("font-size",11).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(2));
});
xS.ticks(6).forEach(v=>{
g.append("text").attr("x",xS(v)).attr("y",ih+18)
.attr("text-anchor","middle").attr("font-size",11).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(1));
});
// Full curve dimmed
g.append("path")
.attr("d",d3.line().x(d=>xS(d.x)).y(d=>yPdf(d.y)).curve(d3.curveLinear)(pts))
.attr("fill","none").attr("stroke",C.pdf).attr("stroke-width",1.5).attr("opacity",0.2);
// Shaded area up to cursor
const shadePts=pts.filter(d=>d.x<=cx);
if(shadePts.length>1){
const last={x:cx,y:Math.min(gammaPdf(cx,a,b),capY)};
g.append("path")
.attr("d",d3.area().x(d=>xS(d.x)).y0(ih).y1(d=>yPdf(d.y)).curve(d3.curveLinear)([...shadePts,last]))
.attr("fill",C.pdfFill).attr("opacity",0.8);
}
// Active curve up to cursor
const activePts=pts.filter(d=>d.x<=cx);
if(activePts.length>1){
g.append("path")
.attr("d",d3.line().x(d=>xS(d.x)).y(d=>yPdf(d.y)).curve(d3.curveLinear)(activePts))
.attr("fill","none").attr("stroke",C.pdf).attr("stroke-width",2.5);
}
// Cursor line with dot
{
const pdfCapped=Math.min(pdfVal,capY);
g.append("line").attr("x1",cxPx).attr("x2",cxPx).attr("y1",yPdf(pdfCapped)).attr("y2",ih)
.attr("stroke",C.cursor).attr("stroke-width",2).attr("stroke-dasharray","5,3");
g.append("circle").attr("cx",cxPx).attr("cy",yPdf(pdfCapped)).attr("r",5)
.attr("fill",C.cursor).attr("stroke","#fff").attr("stroke-width",2);
// PDF height label
g.append("text").attr("x",cxPx+8).attr("y",yPdf(pdfCapped)-6)
.attr("font-size",11).attr("font-weight",800).attr("fill",C.cursor)
.attr("font-family",mono).text(`f(x)=${pdfCapped.toFixed(3)}`);
}
g.append("line").attr("x2",iw).attr("y1",ih).attr("y2",ih).attr("stroke",C.sub).attr("opacity",0.25);
svg.append("text").attr("x",mg.l+iw/2).attr("y",VH-2)
.attr("text-anchor","middle").attr("font-size",12).attr("font-weight",700).attr("fill",C.sub).text("x");
svg.append("text").attr("x",12).attr("y",mg.t+ih/2)
.attr("text-anchor","middle").attr("font-size",12).attr("font-weight",700).attr("fill",C.sub)
.attr("transform",`rotate(-90,12,${mg.t+ih/2})`).text("f(x)");
g.append("rect").attr("width",iw).attr("height",ih).attr("fill","transparent").attr("cursor","col-resize")
.on("mousedown",()=>{dragging=true;})
.on("mousemove",(e)=>{if(!dragging)return;const[mx]=d3.pointer(e);cursorX=xS.invert(mx);renderAll();})
.on("mouseup",()=>{dragging=false;})
.on("mouseleave",()=>{dragging=false;})
.on("click",(e)=>{const[mx]=d3.pointer(e);cursorX=xS.invert(mx);renderAll();});
}
// ── CDF panel ──
{
const{svg}=cdfPanel;svg.selectAll("*").remove();
const g=svg.append("g").attr("transform",`translate(${mg.l},${mg.t})`);
[0,0.25,0.5,0.75,1].forEach(v=>{
g.append("line").attr("x2",iw).attr("y1",yCdf(v)).attr("y2",yCdf(v)).attr("stroke",C.grid);
g.append("text").attr("x",-6).attr("y",yCdf(v)+4).attr("text-anchor","end")
.attr("font-size",11).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(2));
});
xS.ticks(6).forEach(v=>{
g.append("text").attr("x",xS(v)).attr("y",ih+18)
.attr("text-anchor","middle").attr("font-size",11).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(1));
});
// Full CDF dimmed
const fullCdf=curvePts(x=>gammaCdf(x,a,b),xLo,xHi,nPts);
g.append("path")
.attr("d",d3.line().x(d=>xS(d.x)).y(d=>yCdf(d.y)).curve(d3.curveLinear)(fullCdf))
.attr("fill","none").attr("stroke",C.cdf).attr("stroke-width",1.5).attr("opacity",0.15);
// Active CDF up to cursor
const activeCdf=fullCdf.filter(d=>d.x<=cx);
if(activeCdf.length>1){
// Filled area under CDF
g.append("path")
.attr("d",d3.area().x(d=>xS(d.x)).y0(ih).y1(d=>yCdf(d.y)).curve(d3.curveLinear)(activeCdf))
.attr("fill",C.cdfFill).attr("opacity",0.35);
// CDF line
g.append("path")
.attr("d",d3.line().x(d=>xS(d.x)).y(d=>yCdf(d.y)).curve(d3.curveLinear)(activeCdf))
.attr("fill","none").attr("stroke",C.cdf).attr("stroke-width",2.5);
}
// Cursor point on CDF
{
g.append("line").attr("x1",cxPx).attr("x2",cxPx).attr("y1",yCdf(cdfVal)).attr("y2",ih)
.attr("stroke",C.cursor).attr("stroke-width",2).attr("stroke-dasharray","5,3");
g.append("line").attr("x1",0).attr("y1",yCdf(cdfVal)).attr("x2",cxPx).attr("y2",yCdf(cdfVal))
.attr("stroke",C.cdf).attr("stroke-width",1.5).attr("stroke-dasharray","4,3").attr("opacity",0.5);
g.append("circle").attr("cx",cxPx).attr("cy",yCdf(cdfVal)).attr("r",5)
.attr("fill",C.cursor).attr("stroke","#fff").attr("stroke-width",2);
}
// Slope indicator: tangent line on CDF showing dF/dx = f(x)
{
const dydx_data = pdfVal;
const slopePx = dydx_data * (-ih / 1.05) * ((xHi - xLo) / iw);
const halfLen = 30;
const norm = Math.sqrt(1 + slopePx * slopePx);
const dx = halfLen / norm;
const dy = slopePx * dx;
const cy = yCdf(cdfVal);
g.append("line")
.attr("x1", cxPx - dx).attr("y1", cy - dy)
.attr("x2", cxPx + dx).attr("y2", cy + dy)
.attr("stroke",C.slope).attr("stroke-width",3).attr("stroke-linecap","round");
g.append("text").attr("x",cxPx+dx+6).attr("y",cy+dy+4)
.attr("font-size",11).attr("font-weight",800).attr("fill",C.slope)
.attr("font-family",mono).text(pdfVal.toFixed(3));
}
// CDF value pill
g.append("rect").attr("x",-50).attr("y",yCdf(cdfVal)-11).attr("width",44).attr("height",22)
.attr("rx",5).attr("fill",C.cdf);
g.append("text").attr("x",-28).attr("y",yCdf(cdfVal)+5)
.attr("text-anchor","middle").attr("font-size",11).attr("font-weight",800)
.attr("fill","#fff").attr("font-family",mono).text(cdfVal.toFixed(3));
g.append("line").attr("x2",iw).attr("y1",ih).attr("y2",ih).attr("stroke",C.sub).attr("opacity",0.25);
svg.append("text").attr("x",mg.l+iw/2).attr("y",VH-2)
.attr("text-anchor","middle").attr("font-size",12).attr("font-weight",700).attr("fill",C.sub).text("x");
svg.append("text").attr("x",12).attr("y",mg.t+ih/2)
.attr("text-anchor","middle").attr("font-size",12).attr("font-weight",700).attr("fill",C.sub)
.attr("transform",`rotate(-90,12,${mg.t+ih/2})`).text("F(x)");
g.append("rect").attr("width",iw).attr("height",ih).attr("fill","transparent").attr("cursor","col-resize")
.on("mousedown",()=>{dragging=true;})
.on("mousemove",(e)=>{if(!dragging)return;const[mx]=d3.pointer(e);cursorX=xS.invert(mx);renderAll();})
.on("mouseup",()=>{dragging=false;})
.on("mouseleave",()=>{dragging=false;})
.on("click",(e)=>{const[mx]=d3.pointer(e);cursorX=xS.invert(mx);renderAll();});
}
infoEl.innerHTML=`<span style="color:${C.cursor};">x = ${cx.toFixed(3)}</span> \u2502 `
+`f(x) = <span style="color:${C.pdf};">${Math.min(pdfVal,capY).toFixed(4)}</span> \u2502 `
+`F(x) = <span style="color:${C.cdf};">${cdfVal.toFixed(4)}</span>`;
}
// ═══════════════════ GLOBAL MOUSE UP ═══════════════════
document.addEventListener("mouseup",()=>{dragging=false;});
// ═══════════════════ EVENTS ═══════════════════
Object.values(SL).forEach(s=>s.input.addEventListener("input",()=>{cursorX=null;renderAll();}));
styleMode();
renderAll();
outer.value={};return outer;
}2.2.3 Hàm phân vị
Hàm phân vị (quantile function, kí hiệu là \(Q(p)\)) là hàm ngược của hàm phân phối tích lũy (cdf).
Sử dụng cùng một đường cong được định hình bởi tham số \(\theta\):
Ta dùng cdf để tìm xác suất cộng dồn \(p\) khi đã biết mốc giá trị \(x\):
\[\underbrace{p}_{\text{Xác suất cần tìm}} = F(\underbrace{x}_{\text{Đầu vào giá trị}} \mid \underbrace{\theta}_{\text{Tham số}})\]
Ngược lại, ta dùng hàm phân vị để tìm mốc giá trị \(x\) nhằm đạt được một xác suất mục tiêu \(p\) cho trước:
\[\underbrace{x}_{\text{Mốc giá trị cần tìm}} = Q(\underbrace{p}_{\text{Đầu vào xác suất}} \mid \underbrace{\theta}_{\text{Tham số}})\]
Giá trị \(1.96\) trong công thức tính khoảng tin cậy 95% \(\text{Ước lượng} \pm 1.96 \times \text{Sai số chuẩn (SE)}\) là mốc giá trị tính được từ hàm phân vị trên phân phối chuẩn tắc (standard normal distribution, là phân phối chuẩn với trung bình \(\mu = 0\) và độ lệch chuẩn \(\sigma = 1\)).
viewof normalQViz = {
const C = {
txt:'#16213E', sub:'#8294AA', light:'#E4E9F0',
card:'#fff', grid:'#F0F2F5',
pdf:'#0d9488',
cdf:'#6366f1',
quantLine:'#2563eb', quantFill:'#dbeafe',
symFill:'#fef3c7', symLine:'#d97706',
};
const mono = "'SF Mono',SFMono-Regular,Menlo,monospace";
// ═══════════════════ MATH ═══════════════════
const SQRT2PI = Math.sqrt(2*Math.PI);
const SQRT2 = Math.sqrt(2);
function normPdf(x,mu,s){ const z=(x-mu)/s; return Math.exp(-0.5*z*z)/(s*SQRT2PI); }
function erf_a(x){
const sg=x<0?-1:1, ax=Math.abs(x);
const t=1/(1+0.3275911*ax);
return sg*(1-(((((1.061405429*t-1.453152027)*t)+1.421413741)*t-0.284496736)*t+0.254829592)*t*Math.exp(-ax*ax));
}
function normCdf(x,mu,s){ return 0.5*(1+erf_a((x-mu)/(s*SQRT2))); }
function normQ(p){
if(p<=0)return-Infinity;if(p>=1)return Infinity;
const a=[-39.69683028665376,220.9460984245205,-275.9285104469687,138.3577518672690,-30.66479806614716,2.506628277459239];
const b=[-54.47609879822406,161.5858368580409,-155.6989798598866,66.80131188771972,-13.28068155288572];
const c=[-0.007784894002430293,-0.3223964580411365,-2.400758277161838,-2.549732539343734,4.374664141464968,2.938163982698783];
const d=[0.007784695709041462,0.3224671290700398,2.445134137142996,3.754408661907416];
if(p<0.02425){const q=Math.sqrt(-2*Math.log(p));return(((((c[0]*q+c[1])*q+c[2])*q+c[3])*q+c[4])*q+c[5])/(((((d[0]*q+d[1])*q+d[2])*q+d[3])*q+1));}
if(p<=0.97575){const q=p-0.5,r=q*q;return(((((a[0]*r+a[1])*r+a[2])*r+a[3])*r+a[4])*r+a[5])*q/(((((b[0]*r+b[1])*r+b[2])*r+b[3])*r+b[4])*r+1);}
const q=Math.sqrt(-2*Math.log(1-p));return-(((((c[0]*q+c[1])*q+c[2])*q+c[3])*q+c[4])*q+c[5])/(((((d[0]*q+d[1])*q+d[2])*q+d[3])*q+1));
}
function normQuantile(p,mu,s){ return mu+s*normQ(p); }
function curvePts(fn,lo,hi,n){
const p=[],dx=(hi-lo)/(n-1);
for(let i=0;i<n;i++){const x=lo+i*dx;p.push({x,y:fn(x)});}
return p;
}
// ═══════════════════ DOM ═══════════════════
const outer = document.createElement("div");
outer.style.cssText = `display:flex;flex-direction:column;gap:14px;
font-family:'Inter',-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
width:100%;max-width:960px;margin:0 auto;`;
if(!document.querySelector('link[href*="Inter"]')){
const lk=document.createElement('link');lk.rel='stylesheet';
lk.href='https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap';
document.head.appendChild(lk);
}
outer.appendChild(injectStyle());
// Hide spin buttons
const spinStyle = document.createElement("style");
spinStyle.textContent = `input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0;}
input[type=number]{-moz-appearance:textfield;}`;
outer.appendChild(spinStyle);
// ═══════════════════ EDITABLE SLIDER ═══════════════════
function createEditableSlider(label, min, max, sliderStep, val, decimals, color, cls){
const sl = createSlider(label, min, max, sliderStep, val, color, cls);
const fmt = v => v.toFixed(decimals);
const numInput = document.createElement("input");
numInput.type = "number";
numInput.min = min; numInput.max = max; numInput.step = "any";
numInput.value = fmt(val);
numInput.style.cssText = `width:72px;font-size:16px;font-weight:800;color:${color};
font-variant-numeric:tabular-nums;font-family:"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;
border:2px solid ${C.light};border-radius:8px;padding:3px 6px;text-align:right;
background:${C.card};outline:none;transition:border-color 0.15s;`;
numInput.addEventListener("focus",()=>{ numInput.style.borderColor=color; numInput.select(); });
numInput.addEventListener("blur",()=>{
numInput.style.borderColor=C.light;
let v=parseFloat(numInput.value); if(isNaN(v))v=val;
v=Math.max(min,Math.min(max,v));
numInput.value=fmt(v); sl.input.value=v; sl.sync();
sl.input.dispatchEvent(new Event("input"));
});
numInput.addEventListener("keydown",(e)=>{if(e.key==="Enter")numInput.blur();});
const origSync=sl.sync.bind(sl);
sl.sync=function(){ origSync(); if(document.activeElement!==numInput) numInput.value=fmt(+sl.input.value); };
sl.valSpan.replaceWith(numInput);
sl.numInput=numInput;
return sl;
}
// ═══════════════════ SLIDERS ═══════════════════
const SL = {};
SL.mu = createEditableSlider("Trung bình", -5, 5, 0.01, 0, 2, '#e11d48', "red");
SL.sigma = createEditableSlider("Độ lệch chuẩn", 0.1, 5, 0.01, 1, 2, C.pdf, "teal");
SL.q = createSlider("Phân vị", 0.005, 0.995, 0.005, 0.975, C.quantLine, "blue");
const slRow1 = document.createElement("div");
slRow1.style.cssText = "display:flex;gap:14px;width:100%;";
slRow1.append(SL.mu.el, SL.sigma.el);
outer.appendChild(slRow1);
outer.appendChild(SL.q.el);
// ═══════════════════ PANELS ═══════════════════
const panelRow = document.createElement("div");
panelRow.style.cssText = "display:grid;grid-template-columns:1fr 1fr;gap:14px;width:100%;";
outer.appendChild(panelRow);
function mkPanel(titleHtml, color){
const card = document.createElement("div");
card.style.cssText = `background:${C.card};border-radius:16px;border:1px solid ${C.light};
padding:14px;box-shadow:0 2px 16px rgba(0,0,0,.04);`;
const title = document.createElement("div");
title.style.cssText = `font-size:14px;font-weight:800;color:${color};margin-bottom:8px;text-align:center;`;
title.innerHTML = titleHtml;
card.appendChild(title);
const VW=460,VH=380;
const svg = d3.create("svg").attr("viewBox",[0,0,VW,VH])
.style("width","100%").style("display","block").style("border-radius","12px")
.style("background","#FAFBFD").style("border",`1px solid ${C.light}`);
card.appendChild(svg.node());
panelRow.appendChild(card);
return {svg,title,VW,VH};
}
const pdfPanel = mkPanel('PDF', C.pdf);
const cdfPanel = mkPanel('CDF', C.cdf);
// ═══════════════════ RENDER ═══════════════════
function renderAll(){
SL.mu.sync(); SL.sigma.sync(); SL.q.sync();
const mu=SL.mu.val(), sigma=SL.sigma.val(), q=SL.q.val();
const xQ = normQuantile(q, mu, sigma);
const xQL = normQuantile(1-q, mu, sigma); // lower symmetric quantile
const zQ = normQ(q); // standard normal quantile (z-value)
// Symmetric probability = area between xQL and xQ
const symProb = q - (1-q); // = 2q - 1
const xLo = mu - 4.2*sigma, xHi = mu + 4.2*sigma;
const nPts = 300;
// ═══════════════════ PDF PANEL ═══════════════════
{
const{svg,VW,VH}=pdfPanel;
svg.selectAll("*").remove();
const mg={l:52,r:14,t:16,b:44};
const iw=VW-mg.l-mg.r, ih=VH-mg.t-mg.b;
const g=svg.append("g").attr("transform",`translate(${mg.l},${mg.t})`);
const pts=curvePts(x=>normPdf(x,mu,sigma),xLo,xHi,nPts);
const yMax=normPdf(mu,mu,sigma)*1.12;
const xS=d3.scaleLinear().domain([xLo,xHi]).range([0,iw]);
const yS=d3.scaleLinear().domain([0,yMax]).range([ih,0]);
// Grid
yS.ticks(5).forEach(v=>{
g.append("line").attr("x2",iw).attr("y1",yS(v)).attr("y2",yS(v)).attr("stroke",C.grid);
g.append("text").attr("x",-6).attr("y",yS(v)+4).attr("text-anchor","end")
.attr("font-size",11).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(2));
});
xS.ticks(8).forEach(v=>{
g.append("text").attr("x",xS(v)).attr("y",ih+18)
.attr("text-anchor","middle").attr("font-size",11).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(1));
});
// ── Central symmetric shaded region (between xQL and xQ) ──
const symPts=pts.filter(d=>d.x>=xQL&&d.x<=xQ);
if(symPts.length>1){
const closed=[{x:xQL,y:normPdf(xQL,mu,sigma)},...symPts,{x:xQ,y:normPdf(xQ,mu,sigma)}];
g.append("path")
.attr("d",d3.area().x(d=>xS(d.x)).y0(ih).y1(d=>yS(d.y)).curve(d3.curveLinear)(closed))
.attr("fill",C.symFill).attr("opacity",0.8);
}
// ── Upper tail shaded region (up to quantile q from left) — shown as blue ──
// Actually show the full area up to xQ as a light blue under the symmetric gold
const leftPts=pts.filter(d=>d.x<=xQ);
if(leftPts.length>1){
const closed=[...leftPts,{x:xQ,y:normPdf(xQ,mu,sigma)}];
g.append("path")
.attr("d",d3.area().x(d=>xS(d.x)).y0(ih).y1(d=>yS(d.y)).curve(d3.curveLinear)(closed))
.attr("fill",C.quantFill).attr("opacity",0.35);
}
// Re-draw the symmetric region on top so it's visually distinct
if(symPts.length>1){
const closed=[{x:xQL,y:normPdf(xQL,mu,sigma)},...symPts,{x:xQ,y:normPdf(xQ,mu,sigma)}];
g.append("path")
.attr("d",d3.area().x(d=>xS(d.x)).y0(ih).y1(d=>yS(d.y)).curve(d3.curveLinear)(closed))
.attr("fill",C.symFill).attr("opacity",0.55);
}
// PDF curve
g.append("path")
.attr("d",d3.line().x(d=>xS(d.x)).y(d=>yS(d.y)).curve(d3.curveLinear)(pts))
.attr("fill","none").attr("stroke",C.pdf).attr("stroke-width",2.5);
// ── Quantile vertical lines (both symmetric) ──
// Upper quantile
{
const qPx=xS(xQ), qPdfY=normPdf(xQ,mu,sigma);
g.append("line").attr("x1",qPx).attr("x2",qPx).attr("y1",yS(qPdfY)).attr("y2",ih)
.attr("stroke",C.quantLine).attr("stroke-width",2).attr("stroke-dasharray","5,3");
const pillX=Math.max(0,Math.min(iw-60,qPx-30));
g.append("rect").attr("x",pillX).attr("y",ih+2).attr("width",60).attr("height",20)
.attr("rx",5).attr("fill",C.quantLine);
g.append("text").attr("x",pillX+30).attr("y",ih+15)
.attr("text-anchor","middle").attr("font-size",11).attr("font-weight",800)
.attr("fill","#fff").attr("font-family",mono).text(xQ.toFixed(2));
}
// Lower quantile (symmetric)
{
const qPx=xS(xQL), qPdfY=normPdf(xQL,mu,sigma);
g.append("line").attr("x1",qPx).attr("x2",qPx).attr("y1",yS(qPdfY)).attr("y2",ih)
.attr("stroke",C.symLine).attr("stroke-width",2).attr("stroke-dasharray","5,3");
const pillX=Math.max(0,Math.min(iw-60,qPx-30));
g.append("rect").attr("x",pillX).attr("y",ih+2).attr("width",60).attr("height",20)
.attr("rx",5).attr("fill",C.symLine);
g.append("text").attr("x",pillX+30).attr("y",ih+15)
.attr("text-anchor","middle").attr("font-size",11).attr("font-weight",800)
.attr("fill","#fff").attr("font-family",mono).text(xQL.toFixed(2));
}
// Central % label
g.append("text").attr("x",xS(mu)).attr("y",yS(yMax*0.45))
.attr("text-anchor","middle").attr("font-size",14).attr("font-weight",800)
.attr("fill",C.symLine).attr("opacity",0.8).attr("font-family",mono)
.text(`${(symProb*100).toFixed(1)}%`);
// Tail labels
const tailProb = 1-q;
g.append("text").attr("x",xS(xQ)+(iw-xS(xQ))/2).attr("y",yS(yMax*0.25))
.attr("text-anchor","middle").attr("font-size",11).attr("font-weight",700)
.attr("fill",C.quantLine).attr("opacity",0.6).attr("font-family",mono)
.text(`${(tailProb*100).toFixed(1)}%`);
g.append("text").attr("x",xS(xQL)/2).attr("y",yS(yMax*0.25))
.attr("text-anchor","middle").attr("font-size",11).attr("font-weight",700)
.attr("fill",C.symLine).attr("opacity",0.6).attr("font-family",mono)
.text(`${(tailProb*100).toFixed(1)}%`);
g.append("line").attr("x2",iw).attr("y1",ih).attr("y2",ih).attr("stroke",C.sub).attr("opacity",0.25);
svg.append("text").attr("x",mg.l+iw/2).attr("y",VH-2)
.attr("text-anchor","middle").attr("font-size",12).attr("font-weight",700).attr("fill",C.sub).text("x");
svg.append("text").attr("x",12).attr("y",mg.t+ih/2)
.attr("text-anchor","middle").attr("font-size",12).attr("font-weight",700).attr("fill",C.sub)
.attr("transform",`rotate(-90,12,${mg.t+ih/2})`).text("\u03C6(x)");
}
// ═══════════════════ CDF PANEL ═══════════════════
{
const{svg,VW,VH}=cdfPanel;
svg.selectAll("*").remove();
const mg={l:52,r:14,t:16,b:44};
const iw=VW-mg.l-mg.r, ih=VH-mg.t-mg.b;
const g=svg.append("g").attr("transform",`translate(${mg.l},${mg.t})`);
const pts=curvePts(x=>normCdf(x,mu,sigma),xLo,xHi,nPts);
const xS=d3.scaleLinear().domain([xLo,xHi]).range([0,iw]);
const yS=d3.scaleLinear().domain([0,1.05]).range([ih,0]);
[0,0.25,0.5,0.75,1].forEach(v=>{
g.append("line").attr("x2",iw).attr("y1",yS(v)).attr("y2",yS(v)).attr("stroke",C.grid);
g.append("text").attr("x",-6).attr("y",yS(v)+4).attr("text-anchor","end")
.attr("font-size",11).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(2));
});
xS.ticks(8).forEach(v=>{
g.append("text").attr("x",xS(v)).attr("y",ih+18)
.attr("text-anchor","middle").attr("font-size",11).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(1));
});
// Shaded CDF region
{
const qPx=xS(xQ), qPy=yS(q);
g.append("rect").attr("x",0).attr("y",qPy).attr("width",qPx).attr("height",ih-qPy)
.attr("fill",C.quantFill).attr("opacity",0.4);
}
// CDF curve
g.append("path")
.attr("d",d3.line().x(d=>xS(d.x)).y(d=>yS(d.y)).curve(d3.curveLinear)(pts))
.attr("fill","none").attr("stroke",C.cdf).attr("stroke-width",2.5);
// ── Upper quantile lookup ──
{
const qPx=xS(xQ), qPy=yS(q);
g.append("line").attr("x1",0).attr("y1",qPy).attr("x2",qPx).attr("y2",qPy)
.attr("stroke",C.quantLine).attr("stroke-width",2).attr("stroke-dasharray","6,4");
g.append("line").attr("x1",qPx).attr("y1",qPy).attr("x2",qPx).attr("y2",ih)
.attr("stroke",C.quantLine).attr("stroke-width",2).attr("stroke-dasharray","6,4");
// q pill
g.append("rect").attr("x",-48).attr("y",qPy-11).attr("width",42).attr("height",22)
.attr("rx",5).attr("fill",C.quantLine);
g.append("text").attr("x",-27).attr("y",qPy+5)
.attr("text-anchor","middle").attr("font-size",11).attr("font-weight",800)
.attr("fill","#fff").attr("font-family",mono).text(q.toFixed(3));
// dot
g.append("circle").attr("cx",qPx).attr("cy",qPy).attr("r",5.5)
.attr("fill",C.quantLine).attr("stroke","#fff").attr("stroke-width",2);
// x pill
const pillX=Math.max(0,Math.min(iw-60,qPx-30));
g.append("rect").attr("x",pillX).attr("y",ih+2).attr("width",60).attr("height",20)
.attr("rx",5).attr("fill",C.quantLine);
g.append("text").attr("x",pillX+30).attr("y",ih+15)
.attr("text-anchor","middle").attr("font-size",11).attr("font-weight",800)
.attr("fill","#fff").attr("font-family",mono).text(xQ.toFixed(2));
}
// ── Lower quantile lookup (symmetric) ──
{
const qPx=xS(xQL), qPy=yS(1-q);
g.append("line").attr("x1",0).attr("y1",qPy).attr("x2",qPx).attr("y2",qPy)
.attr("stroke",C.symLine).attr("stroke-width",1.5).attr("stroke-dasharray","6,4").attr("opacity",0.7);
g.append("line").attr("x1",qPx).attr("y1",qPy).attr("x2",qPx).attr("y2",ih)
.attr("stroke",C.symLine).attr("stroke-width",1.5).attr("stroke-dasharray","6,4").attr("opacity",0.7);
// dot
g.append("circle").attr("cx",qPx).attr("cy",qPy).attr("r",4.5)
.attr("fill",C.symLine).attr("stroke","#fff").attr("stroke-width",1.5);
// small label
g.append("rect").attr("x",-48).attr("y",qPy-11).attr("width",42).attr("height",22)
.attr("rx",5).attr("fill",C.symLine);
g.append("text").attr("x",-27).attr("y",qPy+5)
.attr("text-anchor","middle").attr("font-size",11).attr("font-weight",800)
.attr("fill","#fff").attr("font-family",mono).text((1-q).toFixed(3));
}
g.append("line").attr("x2",iw).attr("y1",ih).attr("y2",ih).attr("stroke",C.sub).attr("opacity",0.25);
svg.append("text").attr("x",mg.l+iw/2).attr("y",VH-2)
.attr("text-anchor","middle").attr("font-size",12).attr("font-weight",700).attr("fill",C.sub).text("x");
svg.append("text").attr("x",12).attr("y",mg.t+ih/2)
.attr("text-anchor","middle").attr("font-size",12).attr("font-weight",700).attr("fill",C.sub)
.attr("transform",`rotate(-90,12,${mg.t+ih/2})`).text("\u03A6(x)");
}
}
// ═══════════════════ EVENTS ═══════════════════
SL.mu.input.addEventListener("input",renderAll);
SL.sigma.input.addEventListener("input",renderAll);
SL.q.input.addEventListener("input",renderAll);
renderAll();
outer.value={}; return outer;
}2.2.4 Hệ thống hàm phân phối xác suất trong R
Ngôn ngữ R viết các hàm này theo quy luật:
[Tiền tố] + [Tên phân phối]
| Tiền tố (Chức năng) | Tên phân phối (Viết tắt) | Ví dụ ghép hàm trong R |
|---|---|---|
d: pmf/pdf |
binom: Nhị thức |
dbinom(), pbinom(), qbinom() |
p: cdf |
norm: Chuẩn |
dnorm(), pnorm(), qnorm() |
q: quantile |
gamma: Gamma |
dgamma(), pgamma(), qgamma() |
Bài tập:
Giả sử cân nặng của trẻ sơ sinh đủ tháng tại một quần thể tuân theo phân phối chuẩn với trung bình (\(\mu\)) là 3200 gram và độ lệch chuẩn (\(\sigma\)) là 400 gram.
Dùng hàm nào trong R để lấy giá trị 1.96 từ phân phối chuẩn tắc khi tính khoảng tin cậy 95%?
Dùng R để tính giá trị khoảng tin cậy 95% cân nặng trẻ sơ sinh của quần thể này
Tính tỷ lệ trẻ sinh ra bị xếp vào nhóm nhẹ cân (từ 2500 gram trở xuống)
2.2.5 Ứng dụng
Quyết định số ngày cách ly COVID-19
Tại sao trong thời gian đại dịch, chúng ta cách ly các ca tiếp xúc hoặc nhập cảnh trong 14 ngày? Phương pháp để tính ra con số này là sử dụng hàm phân vị dựa trên phân phối thời gian ủ bệnh. Thời gian cách ly là khoảng thời gian đủ dài để 99% người nhiễm virus bộc lộ triệu chứng.
Thời gian ủ bệnh (incubation period) của SARS-CoV-2 là một biến ngẫu nhiên thường được ước lượng bằng phân phối Gamma. Dựa theo bài báo của tác giả Backer cùng cộng sự, thời gian ủ bệnh khi được ước lượng bằng phân phối Gamma có trung bình \(\mu = 6.5\) và độ lệch chuẩn \(\sigma = 2.6\) (Backer, Klinkenberg, and Wallinga 2020).
Bài tập:
Hãy dùng các biểu đồ pdf và cdf bên dưới để tái hiện lại kết quả của các tác giả đã viết trong bài báo:
Nhập các giá trị tham số shape và rate phù hợp với phân phối thời gian ủ bệnh được báo cáo trong bài
Kéo thanh phân vị đến các mốc 95%, 97.5%, 99% và so sánh với kết quả trong bài báo
Dùng hàm
qgammatrong R để kiểm tra kết quả
Phân phối Gamma có hai tham số là shape và rate có thể được tính từ giá trị trung bình và độ lệch chuẩn theo công thức:
- Shape: \(\alpha = \frac{\mu^2}{\sigma^2}\)
- Rate: \(\beta = \frac{\mu}{\sigma^2}\)
viewof gammaViz = {
const C = {
txt:'#16213E', sub:'#8294AA', light:'#E4E9F0',
card:'#fff', grid:'#F0F2F5',
pdf:'#0d9488', pdfFill:'#ccfbf1',
cdf:'#6366f1', cdfFill:'#e0e7ff',
quant:'#1e40af', quantFill:'#dbeafe', quantLine:'#2563eb',
};
const mono = "'SF Mono',SFMono-Regular,Menlo,monospace";
// ═══════════════════ MATH ═══════════════════
function lnGamma(z){
if(z<0.5) return Math.log(Math.PI/Math.sin(Math.PI*z))-lnGamma(1-z);
z-=1;
const c=[0.99999999999980993,676.5203681218851,-1259.1392167224028,
771.32342877765313,-176.61502916214059,12.507343278686905,
-0.13857109526572012,9.9843695780195716e-6,1.5056327351493116e-7];
let x=c[0]; for(let i=1;i<9;i++) x+=c[i]/(z+i);
const t=z+7.5;
return 0.5*Math.log(2*Math.PI)+(z+0.5)*Math.log(t)-t+Math.log(x);
}
function gammaPdf(x,a,b){
if(x<=0) return a<1?Infinity:a===1?b:0;
if(x<1e-300) return 0;
return Math.exp(a*Math.log(b)-lnGamma(a)+(a-1)*Math.log(x)-b*x);
}
function gammainc(a,x){
if(x<=0) return 0;
if(x>a+40||x>200) return 1-gammainc_ucf(a,x);
let sum=1/a,term=1/a;
for(let n=1;n<300;n++){term*=x/(a+n);sum+=term;if(Math.abs(term)<Math.abs(sum)*1e-14)break;}
return Math.exp(-x+a*Math.log(x)-lnGamma(a))*sum;
}
function gammainc_ucf(a,x){
const lnp=-x+a*Math.log(x)-lnGamma(a);
let cf_d=x+1-a; if(Math.abs(cf_d)<1e-30) cf_d=1e-30;
cf_d=1/cf_d; let cf_f=cf_d, cf_c=1e-30;
for(let i=1;i<200;i++){
const an=i*(a-i), bn=x+2*i+1-a;
cf_d=bn+an*cf_d; if(Math.abs(cf_d)<1e-30) cf_d=1e-30; cf_d=1/cf_d;
cf_c=bn+an/cf_c; if(Math.abs(cf_c)<1e-30) cf_c=1e-30;
const delta=cf_c*cf_d; cf_f*=delta;
if(Math.abs(delta-1)<1e-14) break;
}
return Math.exp(lnp)*cf_f;
}
function gammaCdf(x,a,b){ return x<=0?0:gammainc(a,b*x); }
function normQ01(p){
if(p<=0)return-Infinity;if(p>=1)return Infinity;
const a=[-39.69683028665376,220.9460984245205,-275.9285104469687,138.3577518672690,-30.66479806614716,2.506628277459239];
const b=[-54.47609879822406,161.5858368580409,-155.6989798598866,66.80131188771972,-13.28068155288572];
const c=[-0.007784894002430293,-0.3223964580411365,-2.400758277161838,-2.549732539343734,4.374664141464968,2.938163982698783];
const d=[0.007784695709041462,0.3224671290700398,2.445134137142996,3.754408661907416];
if(p<0.02425){const q=Math.sqrt(-2*Math.log(p));return(((((c[0]*q+c[1])*q+c[2])*q+c[3])*q+c[4])*q+c[5])/(((((d[0]*q+d[1])*q+d[2])*q+d[3])*q+1));}
if(p<=0.97575){const q=p-0.5,r=q*q;return(((((a[0]*r+a[1])*r+a[2])*r+a[3])*r+a[4])*r+a[5])*q/(((((b[0]*r+b[1])*r+b[2])*r+b[3])*r+b[4])*r+1);}
const q=Math.sqrt(-2*Math.log(1-p));return-(((((c[0]*q+c[1])*q+c[2])*q+c[3])*q+c[4])*q+c[5])/(((((d[0]*q+d[1])*q+d[2])*q+d[3])*q+1));
}
function gammaQuantile(q,a,b){
if(q<=0)return 0;if(q>=1)return Infinity;
let x=a/b;
if(a>=1){const z=normQ01(q),s=Math.sqrt(a)/b;x=Math.max(1e-10,a/b+z*s);}
else x=Math.max(1e-10,Math.pow(q,1/a)/b);
for(let i=0;i<60;i++){
const cf=gammaCdf(x,a,b),pf=gammaPdf(x,a,b);
if(pf<1e-300){x*=cf<q?2:0.5;continue;}
const dx=(cf-q)/pf;x=Math.max(1e-15,x-dx);
if(Math.abs(dx)<x*1e-12)break;
}
return x;
}
function curvePts(fn,lo,hi,n){
const p=[],dx=(hi-lo)/(n-1);
for(let i=0;i<n;i++){const x=lo+i*dx,y=fn(x);if(isFinite(y))p.push({x,y});}
return p;
}
// ═══════════════════ DOM ═══════════════════
const outer = document.createElement("div");
outer.style.cssText = `display:flex;flex-direction:column;gap:14px;
font-family:'Inter',-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
width:100%;max-width:960px;margin:0 auto;`;
if(!document.querySelector('link[href*="Inter"]')){
const lk=document.createElement('link');lk.rel='stylesheet';
lk.href='https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap';
document.head.appendChild(lk);
}
outer.appendChild(injectStyle());
// Hide number input spin buttons globally
const spinStyle = document.createElement("style");
spinStyle.textContent = `input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0;}
input[type=number]{-moz-appearance:textfield;}`;
outer.appendChild(spinStyle);
// ═══════════════════ EDITABLE SLIDER ═══════════════════
// Slider for coarse dragging + number input for precise typing.
// The number input accepts ANY valid float within [min,max] — no step snapping on typed values.
// The slider still has a step for drag granularity.
function createEditableSlider(label, min, max, sliderStep, val, decimals, color, cls){
const sl = createSlider(label, min, max, sliderStep, val, color, cls);
const fmt = v => v.toFixed(decimals);
const numInput = document.createElement("input");
numInput.type = "number";
numInput.min = min; numInput.max = max;
numInput.step = "any"; // allow arbitrary precision typing
numInput.value = fmt(val);
numInput.style.cssText = `width:72px;font-size:16px;font-weight:800;color:${color};
font-variant-numeric:tabular-nums;font-family:"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;
border:2px solid ${C.light};border-radius:8px;padding:3px 6px;text-align:right;
background:${C.card};outline:none;transition:border-color 0.15s;`;
numInput.addEventListener("focus",()=>{ numInput.style.borderColor=color; numInput.select(); });
numInput.addEventListener("blur",()=>{
numInput.style.borderColor=C.light;
let v = parseFloat(numInput.value);
if(isNaN(v)) v = val;
v = Math.max(min, Math.min(max, v));
numInput.value = fmt(v);
sl.input.value = v;
sl.sync();
sl.input.dispatchEvent(new Event("input"));
});
numInput.addEventListener("keydown",(e)=>{ if(e.key==="Enter") numInput.blur(); });
// Override sync to keep numInput in sync when slider drags
const origSync = sl.sync.bind(sl);
sl.sync = function(){
origSync();
// Only update numInput if it's not focused (avoid overwriting while user types)
if(document.activeElement !== numInput){
numInput.value = fmt(+sl.input.value);
}
};
sl.valSpan.replaceWith(numInput);
sl.numInput = numInput;
return sl;
}
// ═══════════════════ SLIDERS ═══════════════════
const SL = {};
SL.a = createEditableSlider("shape", 0.10, 20, 0.01, 3, 2, C.pdf, "teal");
SL.b = createEditableSlider("rate", 0.01, 10, 0.001, 1, 3, C.cdf, "purple");
SL.q = createSlider("Phân vị", 0.005, 0.995, 0.005, 0.5, C.quantLine, "blue");
const slRow1 = document.createElement("div");
slRow1.style.cssText = "display:flex;gap:14px;width:100%;";
slRow1.append(SL.a.el, SL.b.el);
outer.appendChild(slRow1);
outer.appendChild(SL.q.el);
// ═══════════════════ PANELS ═══════════════════
const panelRow = document.createElement("div");
panelRow.style.cssText = "display:grid;grid-template-columns:1fr 1fr;gap:14px;width:100%;";
outer.appendChild(panelRow);
function mkPanel(titleHtml, color){
const card = document.createElement("div");
card.style.cssText = `background:${C.card};border-radius:16px;border:1px solid ${C.light};
padding:14px;box-shadow:0 2px 16px rgba(0,0,0,.04);`;
const title = document.createElement("div");
title.style.cssText = `font-size:14px;font-weight:800;color:${color};margin-bottom:8px;text-align:center;`;
title.innerHTML = titleHtml;
card.appendChild(title);
const VW=460,VH=360;
const svg = d3.create("svg").attr("viewBox",[0,0,VW,VH])
.style("width","100%").style("display","block").style("border-radius","12px")
.style("background","#FAFBFD").style("border",`1px solid ${C.light}`);
card.appendChild(svg.node());
panelRow.appendChild(card);
return {svg,title,VW,VH};
}
const pdfPanel = mkPanel('PDF', C.pdf);
const cdfPanel = mkPanel('CDF', C.cdf);
// ═══════════════════ RENDER ═══════════════════
function renderAll(){
SL.a.sync(); SL.b.sync(); SL.q.sync();
const a=SL.a.val(), b=SL.b.val(), q=SL.q.val();
const mean=a/b, sd=Math.sqrt(a/(b*b));
const xQ=gammaQuantile(q,a,b);
const xHi=Math.max(gammaQuantile(0.999,a,b), xQ*1.2, mean+4*sd);
const xLo=0, nPts=300;
// ═══════════════════ PDF ═══════════════════
{
const{svg,VW,VH}=pdfPanel;
svg.selectAll("*").remove();
const mg={l:52,r:14,t:16,b:42};
const iw=VW-mg.l-mg.r, ih=VH-mg.t-mg.b;
const g=svg.append("g").attr("transform",`translate(${mg.l},${mg.t})`);
const pts=curvePts(x=>gammaPdf(x,a,b),Math.max(1e-6,xLo),xHi,nPts);
const capY=a<1?Math.min(d3.max(pts,d=>d.y)||5,b*20):(d3.max(pts,d=>d.y)||1);
pts.forEach(d=>{if(d.y>capY)d.y=capY;});
const yMax=capY*1.1;
const xS=d3.scaleLinear().domain([xLo,xHi]).range([0,iw]);
const yS=d3.scaleLinear().domain([0,yMax]).range([ih,0]);
// Grid
yS.ticks(5).forEach(v=>{
g.append("line").attr("x2",iw).attr("y1",yS(v)).attr("y2",yS(v)).attr("stroke",C.grid);
g.append("text").attr("x",-6).attr("y",yS(v)+4).attr("text-anchor","end")
.attr("font-size",11).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(2));
});
xS.ticks(6).forEach(v=>{
g.append("text").attr("x",xS(v)).attr("y",ih+18)
.attr("text-anchor","middle").attr("font-size",11).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(1));
});
// Shaded area under PDF up to quantile
const shadePts=pts.filter(d=>d.x<=xQ);
if(shadePts.length>1){
const last={x:xQ,y:Math.min(gammaPdf(xQ,a,b),capY)};
g.append("path")
.attr("d",d3.area().x(d=>xS(d.x)).y0(ih).y1(d=>yS(d.y)).curve(d3.curveLinear)([...shadePts,last]))
.attr("fill",C.quantFill).attr("opacity",0.7);
g.append("path")
.attr("d",d3.area().x(d=>xS(d.x)).y0(ih).y1(d=>yS(d.y)).curve(d3.curveLinear)([...shadePts,last]))
.attr("fill","none").attr("stroke",C.quantLine).attr("stroke-width",1).attr("opacity",0.25);
}
// PDF curve
g.append("path")
.attr("d",d3.line().x(d=>xS(d.x)).y(d=>yS(d.y)).curve(d3.curveLinear)(pts))
.attr("fill","none").attr("stroke",C.pdf).attr("stroke-width",2.5);
// Quantile vertical line
{
const qPx=xS(xQ), qPdfY=Math.min(gammaPdf(xQ,a,b),capY);
g.append("line").attr("x1",qPx).attr("x2",qPx).attr("y1",yS(qPdfY)).attr("y2",ih)
.attr("stroke",C.quantLine).attr("stroke-width",2).attr("stroke-dasharray","5,3");
// x-value pill
const pillX=Math.max(0, Math.min(iw-60, qPx-30));
g.append("rect").attr("x",pillX).attr("y",ih+2).attr("width",60).attr("height",20)
.attr("rx",5).attr("fill",C.quantLine);
g.append("text").attr("x",pillX+30).attr("y",ih+15)
.attr("text-anchor","middle").attr("font-size",11).attr("font-weight",800)
.attr("fill","#fff").attr("font-family",mono).text(xQ.toFixed(2));
}
g.append("line").attr("x2",iw).attr("y1",ih).attr("y2",ih).attr("stroke",C.sub).attr("opacity",0.25);
svg.append("text").attr("x",mg.l+iw/2).attr("y",VH-2)
.attr("text-anchor","middle").attr("font-size",12).attr("font-weight",700).attr("fill",C.sub).text("x");
svg.append("text").attr("x",12).attr("y",mg.t+ih/2)
.attr("text-anchor","middle").attr("font-size",12).attr("font-weight",700).attr("fill",C.sub)
.attr("transform",`rotate(-90,12,${mg.t+ih/2})`).text("f(x)");
}
// ═══════════════════ CDF ═══════════════════
{
const{svg,VW,VH}=cdfPanel;
svg.selectAll("*").remove();
const mg={l:52,r:14,t:16,b:42};
const iw=VW-mg.l-mg.r, ih=VH-mg.t-mg.b;
const g=svg.append("g").attr("transform",`translate(${mg.l},${mg.t})`);
const pts=curvePts(x=>gammaCdf(x,a,b),xLo,xHi,nPts);
const xS=d3.scaleLinear().domain([xLo,xHi]).range([0,iw]);
const yS=d3.scaleLinear().domain([0,1.05]).range([ih,0]);
[0,0.25,0.5,0.75,1].forEach(v=>{
g.append("line").attr("x2",iw).attr("y1",yS(v)).attr("y2",yS(v)).attr("stroke",C.grid);
g.append("text").attr("x",-6).attr("y",yS(v)+4).attr("text-anchor","end")
.attr("font-size",11).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(2));
});
xS.ticks(6).forEach(v=>{
g.append("text").attr("x",xS(v)).attr("y",ih+18)
.attr("text-anchor","middle").attr("font-size",11).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(1));
});
// Shaded CDF region
{
const qPx=xS(xQ), qPy=yS(q);
g.append("rect").attr("x",0).attr("y",qPy).attr("width",qPx).attr("height",ih-qPy)
.attr("fill",C.quantFill).attr("opacity",0.5);
}
// CDF curve
g.append("path")
.attr("d",d3.line().x(d=>xS(d.x)).y(d=>yS(d.y)).curve(d3.curveLinear)(pts))
.attr("fill","none").attr("stroke",C.cdf).attr("stroke-width",2.5);
// Quantile lookup
{
const qPx=xS(xQ), qPy=yS(q);
g.append("line").attr("x1",0).attr("y1",qPy).attr("x2",qPx).attr("y2",qPy)
.attr("stroke",C.quantLine).attr("stroke-width",2).attr("stroke-dasharray","6,4");
g.append("line").attr("x1",qPx).attr("y1",qPy).attr("x2",qPx).attr("y2",ih)
.attr("stroke",C.quantLine).attr("stroke-width",2).attr("stroke-dasharray","6,4");
// q pill
g.append("rect").attr("x",-48).attr("y",qPy-11).attr("width",42).attr("height",22)
.attr("rx",5).attr("fill",C.quantLine);
g.append("text").attr("x",-27).attr("y",qPy+5)
.attr("text-anchor","middle").attr("font-size",11).attr("font-weight",800)
.attr("fill","#fff").attr("font-family",mono).text(q.toFixed(3));
// dot
g.append("circle").attr("cx",qPx).attr("cy",qPy).attr("r",5.5)
.attr("fill",C.quantLine).attr("stroke","#fff").attr("stroke-width",2);
// x pill
const pillX=Math.max(0, Math.min(iw-60, qPx-30));
g.append("rect").attr("x",pillX).attr("y",ih+2).attr("width",60).attr("height",20)
.attr("rx",5).attr("fill",C.quantLine);
g.append("text").attr("x",pillX+30).attr("y",ih+15)
.attr("text-anchor","middle").attr("font-size",11).attr("font-weight",800)
.attr("fill","#fff").attr("font-family",mono).text(xQ.toFixed(2));
}
g.append("line").attr("x2",iw).attr("y1",ih).attr("y2",ih).attr("stroke",C.sub).attr("opacity",0.25);
svg.append("text").attr("x",mg.l+iw/2).attr("y",VH-2)
.attr("text-anchor","middle").attr("font-size",12).attr("font-weight",700).attr("fill",C.sub).text("x");
svg.append("text").attr("x",12).attr("y",mg.t+ih/2)
.attr("text-anchor","middle").attr("font-size",12).attr("font-weight",700).attr("fill",C.sub)
.attr("transform",`rotate(-90,12,${mg.t+ih/2})`).text("F(x)");
}
}
// ═══════════════════ EVENTS ═══════════════════
SL.a.input.addEventListener("input",renderAll);
SL.b.input.addEventListener("input",renderAll);
SL.q.input.addEventListener("input",renderAll);
renderAll();
outer.value={}; return outer;
}Một số ứng dụng khác trong y tế công cộng:
Thời gian để công bố hết dịch của một số bệnh như Ebola, sốt Lassa… được WHO xác định là không có ca mới sau “2 lần thời gian ủ bệnh tối đa” (Djaafara et al. 2020)
Ngưỡng cảnh báo dịch là \(Q(0.95 \text{ hoặc } 0.99 \mid \theta)\) của phân phối số ca theo dữ liệu lịch sử
2.3 Moment
Hình dạng của một phân phối được xác định bởi các giá trị gọi là moment. Thuật ngữ này được mượn từ vật lý: mô-men (hay mô-men lực) là đại lượng đo khả năng làm quay của một lực quanh một điểm tựa.
Trong vật lý, moment (\(M\)) được tính bằng:
\[M = \underbrace{d}_{\text{Khoảng cách đến điểm tựa}} \times \underbrace{F}_{\text{Lực tác dụng}}\]
viewof moment_physic = {
const W = 780, H = 440;
const COL = {
beam: "#FFB300", beamStroke: "#F9A825",
pivot: "#F4511E", pivotStroke: "#D84315", pivotDrag: "#FF8A65",
stone: "#607D8B", stoneStroke: "#455A64",
stoneHi: "#EF5350", stoneHiStroke: "#C62828",
ghost: "#B0BEC5",
dist: "#1565C0", force: "#E91E63", moment: "#7B1FA2",
bg: "#FAFBFD", sub: "#90A4AE", txt: "#263238",
light: "#E0E0E0", bal: "#43A047",
};
const mono = "'SF Mono',SFMono-Regular,Menlo,monospace";
// ═══════════════════ DOM ═══════════════════
const outer = document.createElement("div");
outer.style.cssText = `display:flex;flex-direction:column;align-items:center;gap:12px;
font-family:'Inter',-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
width:100%;max-width:820px;margin:0 auto;`;
if (!document.querySelector('link[href*="Inter"]')) {
const lk = document.createElement('link'); lk.rel = 'stylesheet';
lk.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap';
document.head.appendChild(lk);
}
outer.appendChild(injectStyle());
const ctrlRow = document.createElement("div");
ctrlRow.style.cssText = "display:flex;gap:14px;width:100%;align-items:flex-end;";
const SL = {};
SL.weight = createSlider("Trọng lượng viên đá (N)", 10, 100, 5, 30, COL.force, "red");
ctrlRow.appendChild(SL.weight.el);
const btnClear = createButton("🗑 Xóa tất cả", "reset");
btnClear.el.style.cssText += ";min-width:120px;height:40px;font-size:13px;";
ctrlRow.appendChild(btnClear.el);
outer.appendChild(ctrlRow);
const svgCard = document.createElement("div");
svgCard.style.cssText = `background:#fff;border-radius:16px;border:1px solid ${COL.light};
padding:14px;box-shadow:0 2px 16px rgba(0,0,0,.04);width:100%;`;
const svg = d3.create("svg").attr("viewBox", [0, 0, W, H])
.style("width", "100%").style("display", "block");
svgCard.appendChild(svg.node());
outer.appendChild(svgCard);
const resultEl = document.createElement("div");
resultEl.style.cssText = `background:#fff;border-radius:14px;border:2px solid ${COL.light};
padding:14px 20px;font-family:${mono};font-size:14px;line-height:2;
text-align:center;color:${COL.txt};width:100%;`;
outer.appendChild(resultEl);
// ═══════════════════ LAYOUT ═══════════════════
const beamY = 270, beamH = 14;
const beamLeft = 40, beamRight = W - 40, beamLen = W - 80;
const pivotH = 60, pivotW = 50, maxM = 10;
const mToPx = m => beamLeft + (m / maxM) * beamLen;
const pxToM = px => ((px - beamLeft) / beamLen) * maxM;
const snap = m => Math.max(0, Math.min(maxM, Math.round(m * 2) / 2));
const stoneR = w => Math.max(9, 8 + Math.sqrt(w) * 1.3);
// ═══════════════════ STATE ═══════════════════
let stones = [];
let pivotM = 5;
let hovIdx = -1;
let ghostM = -1;
let dragPiv = false;
// ═══════════════════ PERSISTENT SVG LAYERS ═══════════════════
// Background (static)
svg.append("rect").attr("width", W).attr("height", H).attr("fill", COL.bg).attr("rx", 12);
svg.append("text").attr("x", W / 2).attr("y", 22)
.attr("text-anchor", "middle").attr("font-size", 15).attr("font-weight", 800)
.attr("fill", COL.txt).text("Mô-men = Lực × Khoảng cách");
const groundG = svg.append("g");
const pivotG = svg.append("g").style("cursor", "ew-resize");
const pivotLabel = svg.append("text")
.attr("text-anchor", "middle").attr("font-size", 11).attr("font-weight", 700)
.attr("fill", COL.pivot).attr("font-family", mono);
const sceneG = svg.append("g"); // tilts
// Inside scene: click zone → ghost → stones (z-order matters)
const clickZone = sceneG.append("rect")
.attr("x", beamLeft - 5).attr("y", 30)
.attr("width", beamLen + 10).attr("height", beamY - beamH / 2 - 30)
.attr("fill", "transparent").style("cursor", "crosshair");
// Beam (inside scene)
sceneG.append("rect")
.attr("x", beamLeft).attr("y", beamY - beamH / 2)
.attr("width", beamLen).attr("height", beamH)
.attr("rx", 5).attr("fill", COL.beam).attr("stroke", COL.beamStroke).attr("stroke-width", 1.5);
const ticksG = sceneG.append("g");
for (let m = 0; m <= maxM; m++) {
const tx = mToPx(m);
ticksG.append("line").attr("x1", tx).attr("x2", tx)
.attr("y1", beamY - beamH / 2 - 3).attr("y2", beamY - beamH / 2)
.attr("stroke", COL.beamStroke).attr("stroke-width", m % 5 === 0 ? 2 : 1);
ticksG.append("text").attr("x", tx).attr("y", beamY + beamH / 2 + 13)
.attr("text-anchor", "middle").attr("font-size", 10).attr("fill", COL.sub)
.attr("font-family", mono).text(m);
}
const ghostG = sceneG.append("g").style("pointer-events", "none");
const stonesG = sceneG.append("g");
const annoG = svg.append("g").style("pointer-events", "none"); // hover annotations (screen space)
// ═══════════════════ HELPERS ═══════════════════
function stackAt(m) { return stones.filter(s => Math.abs(s.x - m) < 0.01).length; }
function stoneCenterY(weight, stackIdx) {
const r = stoneR(weight);
return beamY - beamH / 2 - 4 - stackIdx * (r * 2 + 4) - r;
}
function tiltedToScreen(sx, sy, pivotPx, tiltDeg) {
const rad = tiltDeg * Math.PI / 180;
const dx = sx - pivotPx, dy = sy - beamY;
return [
pivotPx + dx * Math.cos(rad) - dy * Math.sin(rad),
beamY + dx * Math.sin(rad) + dy * Math.cos(rad)
];
}
function computeTilt() {
if (stones.length === 0) return 0;
let net = 0;
for (const s of stones) net += s.weight * (s.x - pivotM);
return Math.max(-14, Math.min(14, net * 0.06));
}
function buildStackMap() {
const stk = {};
stones.forEach((s, i) => {
const k = s.x.toFixed(1);
if (!stk[k]) stk[k] = [];
stk[k].push(i);
});
return stk;
}
// ═══════════════════ UPDATE: GROUND + PIVOT ═══════════════════
function updatePivot() {
const px = mToPx(pivotM);
const groundY = beamY + pivotH + 6;
groundG.selectAll("*").remove();
groundG.append("line").attr("x1", px - 60).attr("x2", px + 60)
.attr("y1", groundY).attr("y2", groundY)
.attr("stroke", COL.txt).attr("stroke-width", 3);
for (let hx = px - 56; hx <= px + 56; hx += 7) {
groundG.append("line").attr("x1", hx).attr("x2", hx - 5)
.attr("y1", groundY).attr("y2", groundY + 8)
.attr("stroke", COL.sub).attr("stroke-width", 1).attr("opacity", 0.35);
}
pivotG.selectAll("*").remove();
pivotG.append("polygon")
.attr("points", `${px},${beamY - 8} ${px - pivotW/2 - 18},${groundY + 8} ${px + pivotW/2 + 18},${groundY + 8}`)
.attr("fill", "transparent");
pivotG.append("polygon")
.attr("points", `${px},${beamY + 3} ${px - pivotW/2},${groundY - 2} ${px + pivotW/2},${groundY - 2}`)
.attr("fill", dragPiv ? COL.pivotDrag : COL.pivot)
.attr("stroke", COL.pivotStroke).attr("stroke-width", 1.5).attr("stroke-linejoin", "round");
pivotG.append("circle").attr("cx", px).attr("cy", beamY + 11)
.attr("r", 6).attr("fill", "#fff").attr("stroke", COL.pivotStroke).attr("stroke-width", 1.5);
pivotG.append("circle").attr("cx", px).attr("cy", beamY + 11)
.attr("r", 2.5).attr("fill", dragPiv ? COL.pivotDrag : COL.pivot);
pivotLabel.attr("x", px).attr("y", groundY + 20)
.text(`▲ Điểm tựa (${pivotM.toFixed(1)} m)`);
}
// ═══════════════════ UPDATE: SCENE TILT + STONES ═══════════════════
function updateScene() {
const pivotPx = mToPx(pivotM);
const tilt = computeTilt();
sceneG.attr("transform", `rotate(${tilt}, ${pivotPx}, ${beamY})`);
stonesG.selectAll("*").remove();
const stk = buildStackMap();
stones.forEach((s, i) => {
const r = stoneR(s.weight);
const sx = mToPx(s.x);
const k = s.x.toFixed(1);
const si = stk[k].indexOf(i);
const sy = stoneCenterY(s.weight, si);
const isH = (i === hovIdx);
const sg = stonesG.append("g").style("cursor", "pointer");
sg.append("circle").attr("cx", sx).attr("cy", sy).attr("r", r)
.attr("fill", isH ? COL.stoneHi : COL.stone)
.attr("stroke", isH ? COL.stoneHiStroke : COL.stoneStroke)
.attr("stroke-width", isH ? 2.5 : 1.5);
sg.append("text").attr("x", sx).attr("y", sy + 1)
.attr("text-anchor", "middle").attr("dominant-baseline", "middle")
.attr("font-size", r > 12 ? 9 : 7).attr("font-weight", 800)
.attr("fill", "#fff").attr("font-family", mono)
.style("pointer-events", "none").text(s.weight);
sg.on("mouseenter", () => { hovIdx = i; updateStoneStyles(); updateAnnotations(); });
sg.on("mouseleave", () => { hovIdx = -1; updateStoneStyles(); updateAnnotations(); });
});
}
// ═══════════════════ UPDATE: STONE STYLES ONLY ═══════════════════
function updateStoneStyles() {
const stk = buildStackMap();
stonesG.selectAll("g").each(function(_, i) {
const g = d3.select(this);
const isH = (i === hovIdx);
g.select("circle")
.attr("fill", isH ? COL.stoneHi : COL.stone)
.attr("stroke", isH ? COL.stoneHiStroke : COL.stoneStroke)
.attr("stroke-width", isH ? 2.5 : 1.5);
});
}
// ═══════════════════ UPDATE: GHOST ═══════════════════
function updateGhost() {
ghostG.selectAll("*").remove();
if (ghostM < 0 || dragPiv) return;
SL.weight.sync();
const curW = SL.weight.val();
const gx = mToPx(ghostM);
const gStack = stackAt(ghostM);
const gr = stoneR(curW);
const gy = stoneCenterY(curW, gStack);
ghostG.append("circle").attr("cx", gx).attr("cy", gy).attr("r", gr)
.attr("fill", COL.ghost).attr("opacity", 0.3)
.attr("stroke", COL.ghost).attr("stroke-width", 1.5);
ghostG.append("text").attr("x", gx).attr("y", gy + 1)
.attr("text-anchor", "middle").attr("dominant-baseline", "middle")
.attr("font-size", gr > 12 ? 9 : 7).attr("font-weight", 700)
.attr("fill", COL.ghost).attr("opacity", 0.45)
.attr("font-family", mono).text(curW);
}
// ═══════════════════ UPDATE: HOVER ANNOTATIONS ═══════════════════
function updateAnnotations() {
annoG.selectAll("*").remove();
if (hovIdx < 0 || hovIdx >= stones.length) return;
const s = stones[hovIdx];
const r = stoneR(s.weight);
const sx = mToPx(s.x);
const stk = buildStackMap();
const k = s.x.toFixed(1);
const si = stk[k] ? stk[k].indexOf(hovIdx) : 0;
const sy = stoneCenterY(s.weight, si);
const pivotPx = mToPx(pivotM);
const tilt = computeTilt();
const [scrX, scrY] = tiltedToScreen(sx, sy, pivotPx, tilt);
const dist = s.x - pivotM;
const absDist = Math.abs(dist);
const moment = s.weight * absDist;
const dir = dist > 0.01 ? "↻" : dist < -0.01 ? "↺" : "";
// 1) DISTANCE LINE
if (absDist > 0.2) {
const dLineY = scrY;
annoG.append("line")
.attr("x1", pivotPx).attr("y1", dLineY)
.attr("x2", scrX).attr("y2", dLineY)
.attr("stroke", COL.dist).attr("stroke-width", 2.5).attr("opacity", 0.8);
[pivotPx, scrX].forEach(cx => {
annoG.append("line").attr("x1", cx).attr("x2", cx)
.attr("y1", dLineY - 8).attr("y2", dLineY + 8)
.attr("stroke", COL.dist).attr("stroke-width", 2);
});
const midX = (pivotPx + scrX) / 2;
const dTxt = `d = ${absDist.toFixed(1)} m`;
const dw = dTxt.length * 7.5 + 14;
annoG.append("rect").attr("x", midX - dw / 2).attr("y", dLineY - 24)
.attr("width", dw).attr("height", 20).attr("rx", 6).attr("fill", COL.dist);
annoG.append("text").attr("x", midX).attr("y", dLineY - 14)
.attr("text-anchor", "middle").attr("dominant-baseline", "middle")
.attr("font-size", 11).attr("font-weight", 700).attr("fill", "#fff")
.attr("font-family", mono).text(dTxt);
}
// 2) FORCE ARROW
const faX = scrX;
const faTop = scrY - r - 50;
const faBot = scrY - r - 4;
annoG.append("line")
.attr("x1", faX).attr("y1", faTop).attr("x2", faX).attr("y2", faBot)
.attr("stroke", COL.force).attr("stroke-width", 3);
annoG.append("polygon")
.attr("points", `${faX},${faBot + 2} ${faX - 6},${faBot - 8} ${faX + 6},${faBot - 8}`)
.attr("fill", COL.force);
const fTxt = `F = ${s.weight} N`;
const fw = fTxt.length * 7.5 + 14;
annoG.append("rect").attr("x", faX - fw / 2).attr("y", faTop - 22)
.attr("width", fw).attr("height", 20).attr("rx", 6).attr("fill", COL.force);
annoG.append("text").attr("x", faX).attr("y", faTop - 12)
.attr("text-anchor", "middle").attr("dominant-baseline", "middle")
.attr("font-size", 11).attr("font-weight", 700).attr("fill", "#fff")
.attr("font-family", mono).text(fTxt);
// 3) MOMENT PILL
const mTxt = `M = ${s.weight} × ${absDist.toFixed(1)} = ${moment.toFixed(1)} N·m ${dir}`;
const mw = mTxt.length * 7 + 20;
const my = faTop - 44;
annoG.append("rect").attr("x", scrX - mw / 2).attr("y", my)
.attr("width", mw).attr("height", 22).attr("rx", 8)
.attr("fill", COL.moment).attr("opacity", 0.92);
annoG.append("text").attr("x", scrX).attr("y", my + 11)
.attr("text-anchor", "middle").attr("dominant-baseline", "middle")
.attr("font-size", 12).attr("font-weight", 800).attr("fill", "#fff")
.attr("font-family", mono).text(mTxt);
}
// ═══════════════════ UPDATE: RESULT PANEL ═══════════════════
function updateResult() {
if (stones.length === 0) {
resultEl.innerHTML = `<span style="color:${COL.sub};">Click để thả đá</span>`;
resultEl.style.borderColor = COL.light;
return;
}
let sumL = 0, sumR = 0;
for (const s of stones) {
const m = s.weight * (s.x - pivotM);
if (m < 0) sumL += Math.abs(m); else sumR += m;
}
const net = sumR - sumL;
const bal = Math.abs(net) < 0.5;
let html = `<span style="color:${COL.moment};">Σ Mô-men trái</span> = <b>${sumL.toFixed(1)}</b> N·m`;
html += ` │ `;
html += `<span style="color:${COL.moment};">Σ Mô-men phải</span> = <b>${sumR.toFixed(1)}</b> N·m`;
html += ` │ `;
if (bal) {
html += `<span style="color:${COL.bal};font-weight:900;">Cân bằng</span>`;
resultEl.style.borderColor = COL.bal;
} else {
html += `<span style="color:${COL.pivot};font-weight:900;">Nghiêng ${net > 0 ? "phải" : "trái"}</span>`;
resultEl.style.borderColor = COL.pivot;
}
resultEl.innerHTML = html;
}
// ═══════════════════ COMPOSITE UPDATES ═══════════════════
function fullUpdate() {
updatePivot();
updateScene();
updateGhost();
updateAnnotations();
updateResult();
}
// ═══════════════════ EVENTS ═══════════════════
// Click zone: ghost + drop
clickZone
.on("mousemove", function(ev) {
const [mx] = d3.pointer(ev);
const newG = snap(pxToM(mx));
if (newG !== ghostM) { ghostM = newG; updateGhost(); }
})
.on("mouseleave", () => { ghostM = -1; updateGhost(); })
.on("click", function(ev) {
const [mx] = d3.pointer(ev);
SL.weight.sync();
stones.push({ x: snap(pxToM(mx)), weight: SL.weight.val() });
ghostM = -1;
fullUpdate();
});
// Pivot drag
pivotG.call(d3.drag()
.on("start", () => { dragPiv = true; updatePivot(); })
.on("drag", (ev) => {
pivotM = snap(pxToM(ev.x));
updatePivot();
updateScene();
updateGhost();
updateAnnotations();
updateResult();
})
.on("end", () => { dragPiv = false; fullUpdate(); })
);
// Slider → update ghost instantly
SL.weight.input.addEventListener("input", () => {
SL.weight.sync();
updateGhost();
});
// Clear
btnClear.el.addEventListener("click", () => {
stones = []; hovIdx = -1; ghostM = -1;
fullUpdate();
});
// ═══════════════════ INIT ═══════════════════
fullUpdate();
outer.value = {}; return outer;
}Trong thống kê, hãy tưởng tượng trục số như một cái bập bênh. Mỗi giá trị \(x\) là một vị trí trên thanh đòn, và “sức nặng” tại điểm đó chính là xác suất xảy ra của nó. Khi đó, ta thấy sự tương đồng với moment trong vật lý:
Lực (\(F\)): Tương ứng với Xác suất \(f(x)\) (hoặc \(\mathbb{P}(X=x)\)). Giá trị nào có xác suất càng cao thì “lực” đè xuống càng nặng.
Khoảng cách đến điểm tựa (\(d\)): Tương ứng với \((x - a)\) là khoảng cách từ giá trị dữ liệu \(x\) tới một điểm mốc \(a\) (ví dụ: gốc 0 hoặc giá trị trung bình).
Tổng hợp lại, moment chính là tổng (hoặc tích phân) của các tích số giữa khoảng cách và xác suất. Trong ngôn ngữ thống kê, phép tổng có trọng số này chính là Kỳ vọng (\(E\)).
Công thức moment bậc \(n\) quanh điểm \(a\) được viết là:
\[E[(X - a)^n] = \begin{cases} \sum \underbrace{(x_i - a)^n}_{\text{Khoảng cách đến điểm tựa}} \overbrace{\mathbb{P}(x_i)}^{\text{Lực tác dụng}} & \text{(Rời rạc)} \\ \int_{-\infty}^{\infty} \underbrace{(x - a)^n}_{\text{Khoảng cách đến điểm tựa}} \overbrace{f(x)}^{\text{Lực tác dụng}} dx & \text{(Liên tục)} \end{cases}\]
Ký hiệu \(E[g(X)]\) chính là định nghĩa toán học của việc lấy giá trị \(g(X)\) nhân với xác suất rồi cộng lại. Bất cứ khi nào thấy “Lấy một giá trị, nhân với xác suất của nó, rồi cộng hết lại”, thì đó chính là Kỳ vọng (\(E\)).
Một cách tổng quát, với một biến ngẫu nhiên \(X\), moment bậc \(n\) quanh điểm \(a\) (the \(n\)-th moment about \(a\)) là:
\[E[(X - a)^n]\]
Có 2 loại moment:
- Moment gốc (raw moment, lấy mốc là điểm 0): khi \(a = 0\), moment là \(E[X^n]\).
- Moment tập trung (central moment, lấy mốc là giá trị trung bình): khi \(a = E[X]\), moment là \(E[(X - E[X])^n]\).
Bậc của moment có thể từ 0 đến \(\infty\). Chúng ta thường quan tâm tới 4 loại moment sau:
| Loại moment | Kí hiệu | Tên thường gọi | Ý nghĩa |
|---|---|---|---|
| Gốc bậc 1 | \(E[X]\) | Trung bình (Mean) | Vị trí (tâm của phân phối) |
| Tập trung bậc 2 | \(E[(X - E[X])^2]\) | Phương sai (Variance) | Độ phân tán (dữ liệu biến động thế nào) |
| Tập trung bậc 3 | \(E[(X - E[X])^3]\) | Độ lệch (Skewness)* | Tính bất đối xứng (bên nào có đuôi dài hơn) |
| Tập trung bậc 4 | \(E[(X - E[X])^4]\) | Độ nhọn (Kurtosis)* | Độ dày của đuôi |
*Độ lệch và Độ nhọn thường được chuẩn hóa (standardised) bằng cách chia cho độ lệch chuẩn \(\sigma\), ý nghĩa là gấp bao nhiêu lần độ lệch chuẩn.
viewof moment_seesaw = {
// ═══════════════════════════════════════════════════
// CONFIG
// ═══════════════════════════════════════════════════
const W = 780, H = 470;
const COL = {
beam: "#FFB300", beamStroke: "#F9A825",
pivot: "#F4511E", pivotStroke: "#D84315",
block: "#EF5350", blockStroke: "#C62828",
blockAlt: "#FF7043", blockAltStroke: "#E64A19",
dist: "#1565C0",
force: "#00897B",
moment: "#7B1FA2",
ref: "#E91E63",
bg: "#fff", sub: "#78909C", txt: "#263238",
snap: "#43A047",
};
// Distribution presets
const distributions = {
"Đối xứng (Symmetric)": [
{ x: 1, freq: 1 }, { x: 2, freq: 3 }, { x: 3, freq: 5 },
{ x: 4, freq: 7 }, { x: 5, freq: 5 }, { x: 6, freq: 3 }, { x: 7, freq: 1 },
],
"Lệch phải (Right-skewed)": [
{ x: 1, freq: 7 }, { x: 2, freq: 5 }, { x: 3, freq: 4 },
{ x: 4, freq: 3 }, { x: 5, freq: 2 }, { x: 7, freq: 1 },
],
"Lệch trái (Left-skewed)": [
{ x: 1, freq: 1 }, { x: 3, freq: 2 }, { x: 4, freq: 3 },
{ x: 5, freq: 4 }, { x: 6, freq: 5 }, { x: 7, freq: 7 },
],
"Đều (Uniform)": [
{ x: 1, freq: 4 }, { x: 2, freq: 4 }, { x: 3, freq: 4 },
{ x: 4, freq: 4 }, { x: 5, freq: 4 }, { x: 6, freq: 4 },
],
"Hai đỉnh (Bimodal)": [
{ x: 1, freq: 6 }, { x: 2, freq: 4 }, { x: 3, freq: 1 },
{ x: 5, freq: 1 }, { x: 6, freq: 4 }, { x: 7, freq: 6 },
],
"Đuôi nặng (Heavy-tailed)": [
{ x: 0, freq: 2 }, { x: 2, freq: 1 }, { x: 3, freq: 3 },
{ x: 4, freq: 6 }, { x: 5, freq: 3 }, { x: 6, freq: 1 }, { x: 8, freq: 2 },
],
};
let dataPoints = distributions["Lệch phải (Right-skewed)"].map(d => ({ ...d }));
const outer = document.createElement("div");
outer.style.cssText = `display:flex;flex-direction:column;align-items:center;
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
width:100%;max-width:820px;margin:0 auto;`;
outer.appendChild(injectStyle());
// ═══════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════
let useCenter = false;
let momentOrder = 1;
let pivotValue = 0;
let isDragging = false;
let activeDist = "Lệch phải (Right-skewed)";
// ═══════════════════════════════════════════════════
// CONTROLS
// ═══════════════════════════════════════════════════
// Distribution selector
const distRow = document.createElement("div");
distRow.style.cssText = "display:flex;gap:6px;width:100%;margin-bottom:10px;flex-wrap:wrap;align-items:center;";
const distLabel = document.createElement("span");
distLabel.style.cssText = `font-size:13px;font-weight:700;color:${COL.sub};margin-right:4px;`;
distLabel.textContent = "Dữ liệu:";
distRow.appendChild(distLabel);
const distBtns = {};
Object.keys(distributions).forEach(name => {
const b = document.createElement("button");
b.style.cssText = `padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;
font-family:inherit;cursor:pointer;transition:all 0.15s;border:1.5px solid #B0BEC5;
background:#fff;color:${COL.txt};`;
b.textContent = name;
b.addEventListener("click", () => {
activeDist = name;
dataPoints = distributions[name].map(d => ({ ...d }));
if (useCenter) pivotValue = computeMean();
else pivotValue = 0;
update();
});
distBtns[name] = b;
distRow.appendChild(b);
});
outer.appendChild(distRow);
function styleDistBtn(btn, active) {
btn.style.background = active ? COL.txt : "#fff";
btn.style.color = active ? "#fff" : COL.txt;
btn.style.borderColor = active ? COL.txt : "#B0BEC5";
}
// Mode row
const modeRow = document.createElement("div");
modeRow.style.cssText = "display:flex;gap:10px;width:100%;margin-bottom:8px;";
function mkModeBtn(text) {
const b = document.createElement("button");
b.style.cssText = `flex:1;padding:10px 14px;border-radius:8px;font-size:12px;font-weight:700;
font-family:inherit;cursor:pointer;transition:all 0.15s;border:2px solid ${COL.ref};
background:#fff;color:${COL.ref};`;
b.textContent = text;
return b;
}
const btnRaw = mkModeBtn("a = 0 (Moment gốc)");
const btnCentral = mkModeBtn("a = μ (Moment tập trung)");
modeRow.appendChild(btnRaw);
modeRow.appendChild(btnCentral);
outer.appendChild(modeRow);
function styleModeBtn(btn, active) {
btn.style.background = active ? COL.ref : "#fff";
btn.style.color = active ? "#fff" : COL.ref;
btn.style.borderColor = COL.ref;
}
// Order tabs (always shown)
const orderRow = document.createElement("div");
orderRow.style.cssText = "display:flex;gap:8px;width:100%;margin-bottom:8px;align-items:center;";
const orderLabel = document.createElement("span");
orderLabel.style.cssText = `font-size:13px;font-weight:700;color:${COL.sub};margin-right:4px;`;
orderLabel.textContent = "Bậc (Order):";
orderRow.appendChild(orderLabel);
const orderBtns = [];
const orderDefs = [
{ n: 1, label: "n = 1" },
{ n: 2, label: "n = 2" },
{ n: 3, label: "n = 3" },
{ n: 4, label: "n = 4" },
];
orderDefs.forEach(od => {
const b = document.createElement("button");
b.style.cssText = `padding:8px 14px;border-radius:8px;font-size:12px;font-weight:700;
font-family:inherit;cursor:pointer;transition:all 0.15s;border:2px solid ${COL.moment};
background:#fff;color:${COL.moment};`;
b.textContent = od.label;
b.addEventListener("click", () => {
if (!useCenter && od.n > 1) return; // raw mode: only n=1
if (useCenter && od.n < 2) return; // central mode: only n>=2
momentOrder = od.n;
update();
});
orderBtns.push({ el: b, n: od.n });
orderRow.appendChild(b);
});
outer.appendChild(orderRow);
function styleOrderBtn(btn, n, active, enabled) {
if (!enabled) {
btn.style.background = "#f5f5f5";
btn.style.color = "#ccc";
btn.style.borderColor = "#e0e0e0";
btn.style.cursor = "not-allowed";
} else if (active) {
btn.style.background = COL.moment;
btn.style.color = "#fff";
btn.style.borderColor = COL.moment;
btn.style.cursor = "pointer";
} else {
btn.style.background = "#fff";
btn.style.color = COL.moment;
btn.style.borderColor = COL.moment;
btn.style.cursor = "pointer";
}
}
// Mode events
btnRaw.addEventListener("click", () => {
useCenter = false; pivotValue = 0; momentOrder = 1;
update();
});
btnCentral.addEventListener("click", () => {
useCenter = true; pivotValue = computeMean();
if (momentOrder < 2) momentOrder = 2;
update();
});
// Drag hint
const hintRow = document.createElement("div");
hintRow.style.cssText = `font-size:12px;color:${COL.sub};margin-bottom:6px;width:100%;text-align:center;font-style:italic;`;
hintRow.textContent = "💡 Kéo chân đế của bập bênh để thay đổi điểm mốc";
outer.appendChild(hintRow);
// ═══════════════════════════════════════════════════
// SVG
// ═══════════════════════════════════════════════════
const svg = d3.create("svg")
.attr("viewBox", [0, 0, W, H])
.style("width", "100%").style("max-width", W + "px")
.style("background", COL.bg);
const defs = svg.append("defs");
// Arrow markers
function addArrow(id, color) {
defs.append("marker").attr("id", id)
.attr("viewBox", "0 0 10 7").attr("refX", 9).attr("refY", 3.5)
.attr("markerWidth", 10).attr("markerHeight", 7).attr("orient", "auto")
.append("polygon").attr("points", "0 0.5, 9 3.5, 0 6.5").attr("fill", color);
}
addArrow("arrDistR", COL.dist);
addArrow("arrForce", COL.force);
defs.append("marker").attr("id", "arrDistL")
.attr("viewBox", "0 0 10 7").attr("refX", 1).attr("refY", 3.5)
.attr("markerWidth", 10).attr("markerHeight", 7).attr("orient", "auto")
.append("polygon").attr("points", "10 0.5, 1 3.5, 10 6.5").attr("fill", COL.dist);
// ── LAYOUT ──
const beamCX = W / 2, beamCY = 290;
const beamHalfW = 330, beamH = 16;
const blockSize = 22;
const pivotH = 70, pivotW = 60;
// ── FORMULA (top) ──
const formulaText = svg.append("text")
.attr("x", W / 2).attr("y", 26)
.attr("text-anchor", "middle").attr("font-size", 18).attr("font-weight", 700)
.attr("font-family", "'SF Mono',SFMono-Regular,Menlo,monospace");
const formulaSub = svg.append("text")
.attr("x", W / 2).attr("y", 50)
.attr("text-anchor", "middle").attr("font-size", 14).attr("fill", COL.sub);
// ── BALANCE BADGE (top right) ──
const balanceG = svg.append("g").style("display", "none");
const balanceBg = balanceG.append("rect").attr("rx", 10).attr("fill", COL.snap);
const balanceTxt = balanceG.append("text").attr("text-anchor", "middle").attr("dominant-baseline", "middle")
.attr("fill", "#fff").attr("font-size", 14).attr("font-weight", 700).text("Cân bằng!");
// ── SCENE GROUP (rotates) ──
const sceneG = svg.append("g");
// Beam
sceneG.append("rect")
.attr("x", beamCX - beamHalfW).attr("y", beamCY - beamH / 2)
.attr("width", beamHalfW * 2).attr("height", beamH)
.attr("rx", 5).attr("fill", COL.beam).attr("stroke", COL.beamStroke).attr("stroke-width", 1.5);
const tickG = sceneG.append("g");
const blocksG = sceneG.append("g");
// ── PIVOT ──
const pivotTriG = svg.append("g").style("cursor", "ew-resize");
pivotTriG.append("polygon")
.attr("points", `0,0 ${-pivotW / 2 - 12},${pivotH + 12} ${pivotW / 2 + 12},${pivotH + 12}`)
.attr("fill", "transparent");
pivotTriG.append("polygon")
.attr("class", "pivot-tri")
.attr("points", `0,4 ${-pivotW / 2},${pivotH} ${pivotW / 2},${pivotH}`)
.attr("fill", COL.pivot).attr("stroke", COL.pivotStroke).attr("stroke-width", 1.5).attr("stroke-linejoin", "round");
pivotTriG.append("circle").attr("cx", 0).attr("cy", 8).attr("r", 8)
.attr("fill", "#fff").attr("stroke", COL.pivotStroke).attr("stroke-width", 1.5);
pivotTriG.append("circle").attr("cx", 0).attr("cy", 8).attr("r", 3.5).attr("fill", COL.pivot);
// ── ANNOTATIONS ──
const annoG = svg.append("g");
// Ref line & pill
const refLine = annoG.append("line").attr("stroke", COL.ref).attr("stroke-width", 1.5).attr("stroke-dasharray", "5,3");
const refPill = annoG.append("g");
const refPillBg = refPill.append("rect").attr("rx", 10).attr("fill", COL.ref);
const refPillTxt = refPill.append("text").attr("text-anchor", "middle").attr("dominant-baseline", "middle")
.attr("fill", "#fff").attr("font-size", 14).attr("font-weight", 700);
// Hover annotations
const distAnnoG = annoG.append("g").style("display", "none");
const distLine1 = distAnnoG.append("line").attr("stroke", COL.dist).attr("stroke-width", 2)
.attr("marker-start", "url(#arrDistL)").attr("marker-end", "url(#arrDistR)");
const distPill = distAnnoG.append("g");
const distPillBg = distPill.append("rect").attr("rx", 8).attr("fill", COL.dist);
const distPillTxt = distPill.append("text").attr("text-anchor", "middle").attr("dominant-baseline", "middle")
.attr("fill", "#fff").attr("font-size", 12).attr("font-weight", 700)
.attr("font-family", "'SF Mono',monospace");
const forceArrow = distAnnoG.append("line").attr("stroke", COL.force).attr("stroke-width", 2.5)
.attr("marker-end", "url(#arrForce)");
const forcePill = distAnnoG.append("g");
const forcePillBg = forcePill.append("rect").attr("rx", 8).attr("fill", COL.force);
const forcePillTxt = forcePill.append("text").attr("text-anchor", "middle").attr("dominant-baseline", "middle")
.attr("fill", "#fff").attr("font-size", 12).attr("font-weight", 700);
// ── CONTRIBUTION BARS ──
const contribG = svg.append("g");
outer.appendChild(svg.node());
// ═══════════════════════════════════════════════════
// X-SCALE
// ═══════════════════════════════════════════════════
let xScale;
function buildXScale() {
const allX = dataPoints.map(d => d.x);
const xMin = Math.min(0, ...allX) - 1;
const xMax = Math.max(...allX) + 1;
xScale = d3.scaleLinear()
.domain([xMin, xMax])
.range([beamCX - beamHalfW + 24, beamCX + beamHalfW - 24]);
}
// ═══════════════════════════════════════════════════
// COMPUTE
// ═══════════════════════════════════════════════════
function computeMean() {
let sumF = 0, sumFX = 0;
for (const d of dataPoints) { sumF += d.freq; sumFX += d.freq * d.x; }
return sumF > 0 ? sumFX / sumF : 0;
}
function computeMoment(n, center) {
let sumF = 0, sumM = 0;
for (const d of dataPoints) { sumF += d.freq; sumM += d.freq * Math.pow(d.x - center, n); }
return sumF > 0 ? sumM / sumF : 0;
}
// ═══════════════════════════════════════════════════
// HOVER
// ═══════════════════════════════════════════════════
function showAnnotation(d) {
const n = momentOrder;
const center = pivotValue;
const refPx = xScale(center);
const hlPx = xScale(d.x);
const topOfStack = beamCY - beamH / 2 - d.freq * (blockSize + 2) - 2;
const distY = topOfStack - 14;
if (Math.abs(hlPx - refPx) > 20) {
distLine1.attr("x1", refPx).attr("y1", distY).attr("x2", hlPx).attr("y2", distY).style("display", "");
const distMidX = (refPx + hlPx) / 2;
const distVal = (d.x - center);
const distTxt = n > 1
? `(${distVal.toFixed(1)})${["", "", "\u00B2", "\u00B3", "\u2074"][n]}`
: `${distVal.toFixed(1)}`;
distPillTxt.text(`d = ${distTxt}`);
const dtw = distTxt.length * 7 + 50;
distPillBg.attr("x", distMidX - dtw / 2).attr("y", distY - 22).attr("width", dtw).attr("height", 20);
distPillTxt.attr("x", distMidX).attr("y", distY - 12);
distPill.style("display", "");
} else {
distLine1.style("display", "none");
distPill.style("display", "none");
}
const forceArrowX = hlPx + blockSize / 2 + 10;
const forceY1 = topOfStack + 4;
const forceLen = 16 + d.freq * 10;
forceArrow.attr("x1", forceArrowX).attr("y1", forceY1)
.attr("x2", forceArrowX).attr("y2", forceY1 + forceLen).style("display", "");
forcePillTxt.text(`f = ${d.freq}`);
const ftw = 58;
forcePillBg.attr("x", forceArrowX - ftw / 2).attr("y", forceY1 + forceLen + 3).attr("width", ftw).attr("height", 20);
forcePillTxt.attr("x", forceArrowX).attr("y", forceY1 + forceLen + 13);
forcePill.style("display", "");
distAnnoG.style("display", "");
}
function hideAnnotation() {
distAnnoG.style("display", "none");
}
// ═══════════════════════════════════════════════════
// DRAG
// ═══════════════════════════════════════════════════
const pivotDrag = d3.drag()
.on("start", function () {
isDragging = true;
pivotTriG.select(".pivot-tri").attr("fill", "#FF8A65");
})
.on("drag", function (event) {
const px = Math.max(beamCX - beamHalfW + 24, Math.min(beamCX + beamHalfW - 24, event.x));
pivotValue = Math.round(xScale.invert(px) * 10) / 10;
update(true);
})
.on("end", function () {
isDragging = false;
pivotTriG.select(".pivot-tri").attr("fill", COL.pivot);
const mean = computeMean();
if (Math.abs(pivotValue - mean) < 0.2) {
pivotValue = mean; useCenter = true;
if (momentOrder < 2) momentOrder = 2;
} else if (Math.abs(pivotValue) < 0.2) {
pivotValue = 0; useCenter = false; momentOrder = 1;
}
update();
});
pivotTriG.call(pivotDrag);
// ═══════════════════════════════════════════════════
// UPDATE
// ═══════════════════════════════════════════════════
function update(noTransition) {
const n = momentOrder;
const mean = computeMean();
const center = pivotValue;
const momentVal = computeMoment(n, center);
const isExactlyRaw = Math.abs(center) < 0.001;
const isExactlyCentral = Math.abs(center - mean) < 0.001;
// Controls
styleModeBtn(btnRaw, !useCenter);
styleModeBtn(btnCentral, useCenter);
Object.entries(distBtns).forEach(([name, btn]) => styleDistBtn(btn, name === activeDist));
// Order buttons: enable/disable based on mode
orderBtns.forEach(ob => {
const enabled = useCenter ? ob.n >= 2 : ob.n === 1;
styleOrderBtn(ob.el, ob.n, ob.n === n, enabled);
});
buildXScale();
const allX = dataPoints.map(d => d.x);
const xMin = Math.min(0, ...allX) - 1;
const xMax = Math.max(...allX) + 1;
// Pivot
const pivotPx = xScale(center);
if (noTransition) {
pivotTriG.attr("transform", `translate(${pivotPx}, ${beamCY})`);
} else {
pivotTriG.transition().duration(500).ease(d3.easeCubicOut)
.attr("transform", `translate(${pivotPx}, ${beamCY})`);
}
// Tilt
const m1 = computeMoment(1, center);
const tiltDeg = Math.max(-12, Math.min(12, m1 * 4));
if (noTransition) {
sceneG.attr("transform", `rotate(${tiltDeg}, ${pivotPx}, ${beamCY})`);
} else {
sceneG.transition().duration(500).ease(d3.easeCubicOut)
.attr("transform", `rotate(${tiltDeg}, ${pivotPx}, ${beamCY})`);
}
// Balance badge — top right
const isBalanced = Math.abs(m1) < 0.05;
if (isBalanced) {
const bw = 110, bh = 26;
balanceBg.attr("x", W - bw - 14).attr("y", 12).attr("width", bw).attr("height", bh);
balanceTxt.attr("x", W - bw / 2 - 14).attr("y", 25);
balanceG.style("display", "");
} else {
balanceG.style("display", "none");
}
// Ticks
tickG.selectAll("*").remove();
for (let x = Math.ceil(xMin); x <= Math.floor(xMax); x++) {
const tx = xScale(x);
tickG.append("line").attr("x1", tx).attr("x2", tx)
.attr("y1", beamCY - beamH / 2 - 4).attr("y2", beamCY - beamH / 2)
.attr("stroke", COL.beamStroke).attr("stroke-width", 1.2);
tickG.append("text").attr("x", tx).attr("y", beamCY + beamH / 2 + 14)
.attr("text-anchor", "middle").attr("font-size", 12).attr("fill", COL.sub)
.attr("font-family", "'SF Mono',monospace").text(x);
}
// Blocks
blocksG.selectAll("*").remove();
dataPoints.forEach((d) => {
const bx = xScale(d.x);
for (let f = 0; f < d.freq; f++) {
const by = beamCY - beamH / 2 - (f + 1) * (blockSize + 2) - 2;
const g = blocksG.append("g").style("cursor", "pointer");
g.append("rect")
.attr("x", bx - blockSize / 2).attr("y", by)
.attr("width", blockSize).attr("height", blockSize)
.attr("rx", 4)
.attr("fill", COL.blockAlt).attr("stroke", COL.blockAltStroke).attr("stroke-width", 1.2);
g.on("mouseenter", () => {
blocksG.selectAll("g").each(function () {
const rect = d3.select(this).select("rect");
const rx = +rect.attr("x") + blockSize / 2;
if (Math.abs(rx - bx) < 1) {
rect.attr("fill", COL.block).attr("stroke", COL.blockStroke);
}
});
showAnnotation(d);
});
g.on("mouseleave", () => {
blocksG.selectAll("g").each(function () {
const rect = d3.select(this).select("rect");
rect.attr("fill", COL.blockAlt).attr("stroke", COL.blockAltStroke);
});
hideAnnotation();
});
}
});
// Ref line & pill
const refPx = xScale(center);
const refTopY = 78;
refLine.attr("x1", refPx).attr("x2", refPx).attr("y1", refTopY).attr("y2", beamCY + 6);
let refTxt = isExactlyCentral ? `a = μ = ${mean.toFixed(2)}`
: isExactlyRaw ? "c = 0"
: `a = ${center.toFixed(2)}`;
refPillTxt.text(refTxt);
const refTW = refTxt.length * 8 + 20;
refPillBg.attr("x", refPx - refTW / 2).attr("y", refTopY - 14).attr("width", refTW).attr("height", 26);
refPillTxt.attr("x", refPx).attr("y", refTopY - 1);
hideAnnotation();
// Contribution bars (n >= 2)
contribG.selectAll("*").remove();
if (n >= 2) {
const contributions = dataPoints.filter(d => d.freq > 0).map(d => {
const dist = d.x - center;
return { x: d.x, val: d.freq * Math.pow(dist, n) };
});
const maxAbs = Math.max(1e-9, ...contributions.map(c => Math.abs(c.val)));
const barMaxH = 45;
const barW = 18;
const barBaseY = beamCY + pivotH + 42;
const supN = ["", "\u00B9", "\u00B2", "\u00B3", "\u2074"][n];
contribG.append("line")
.attr("x1", xScale(xMin + 0.3)).attr("x2", xScale(xMax - 0.3))
.attr("y1", barBaseY).attr("y2", barBaseY)
.attr("stroke", "#CFD8DC").attr("stroke-width", 1);
contributions.forEach(c => {
const bx = xScale(c.x);
const h = (Math.abs(c.val) / maxAbs) * barMaxH;
const isPos = c.val >= 0;
const barColor = n === 3 ? (isPos ? "#42A5F5" : "#EF5350") : COL.moment;
contribG.append("rect")
.attr("x", bx - barW / 2)
.attr("y", isPos ? barBaseY - h : barBaseY)
.attr("width", barW).attr("height", Math.max(1, h))
.attr("rx", 3).attr("fill", barColor).attr("opacity", 0.75);
contribG.append("text")
.attr("x", bx).attr("y", isPos ? barBaseY - h - 4 : barBaseY + h + 11)
.attr("text-anchor", "middle").attr("font-size", 9).attr("fill", COL.sub)
.attr("font-family", "'SF Mono',monospace")
.text(c.val.toFixed(1));
});
}
// Formula
const supN = ["", "\u00B9", "\u00B2", "\u00B3", "\u2074"][n];
const typeName = isExactlyCentral ? "Moment tập trung" : isExactlyRaw ? "Moment gốc" : "Moment";
const momentNames = {
1: { c: "Trung bình (Mean)", r: "Trung bình (Mean)" },
2: { c: "Phương sai (Variance)", r: "" },
3: { c: "Độ lệch (Skewness)", r: "" },
4: { c: "Độ nhọn (Kurtosis)", r: "" }
};
const mName = isExactlyCentral ? (momentNames[n]?.c || "") : (momentNames[n]?.r || "");
formulaText.html("");
formulaText.append("tspan").attr("fill", COL.moment).text(`${typeName} (n=${n})`);
if (mName) formulaText.append("tspan").attr("fill", COL.sub).attr("font-size", 15).text(` — ${mName}`);
formulaSub.html("");
formulaSub.append("tspan").attr("fill", COL.moment).text("μ");
formulaSub.append("tspan").attr("fill", COL.moment).attr("font-size", 10).attr("dy", 3).text(n);
formulaSub.append("tspan").attr("dy", -3).attr("fill", COL.txt).text(" = Σ ");
formulaSub.append("tspan").attr("fill", COL.force).text("fᵢ");
formulaSub.append("tspan").attr("fill", COL.txt).text("·");
formulaSub.append("tspan").attr("fill", COL.dist).text("(xᵢ−c)");
formulaSub.append("tspan").attr("fill", COL.moment).attr("font-size", 10).attr("dy", -4).text(n);
formulaSub.append("tspan").attr("dy", 4).attr("fill", COL.txt).text(" / N");
formulaSub.append("tspan").attr("fill", COL.moment).attr("font-weight", 700).text(` = ${momentVal.toFixed(4)}`);
}
// ═══════════════════════════════════════════════════
// INIT
// ═══════════════════════════════════════════════════
update();
invalidation.then(() => {});
outer.value = {};
return outer;
}2.3.1 Moment gốc bậc 1
Cho biết vị trí trọng tâm của phân phối nằm ở đâu so với điểm 0.
\[E[X] = \frac{\sum^N_{i = 1}x_i}{N} = \sum^N_{i = 1}x_i \mathbb{P}(x_i)\]
2.3.2 Moment tập trung bậc 2
Cho biết khoảng cách của từng giá trị trong phân phối so với giá trị trung bình. Phép bình phương giúp ngăn chặn các độ lệch âm và dương triệt tiêu lẫn nhau.
\[E[(X - E[X])^2] = E[X^2] - (E[X])^2\]
2.3.3 Moment tập trung bậc 3
Thường được gọi là Độ lệch (Skewness). Nó phản ánh sự không đối xứng.
\[E\left[\left(\frac{X - E[X]}{\sigma}\right)^3\right]\]
Lũy thừa bậc 3 giữ nguyên dấu (số âm mũ 3 vẫn là âm)
2.3.4 Moment tập trung bậc 4
Thường được gọi là Độ nhọn (Kurtosis). Tên gọi này bắt nguồn từ tiếng Hy Lạp: κυρτός - kyrtos, nghĩa là “cong, vòm”.
\[E\left[\left(\frac{X - E[X]}{\sigma}\right)^4\right]\]
Lũy thừa bậc 4 làm cho những giá trị nhỏ (gần trung bình) trở nên siêu nhỏ, và những giá trị lớn (xa trung bình - outliers) trở nên lớn hơn nhiều lần. Moment này càng lớn thì đuôi của phân phối càng dày, nghĩa là xác suất xảy ra các sự kiện cực đoan càng lớn.
2.3.5 Ứng dụng
Một số kiểm định phân phối chuẩn đánh giá hình dáng dữ liệu thông qua độ lệch \(S\) và độ nhọn \(K\). Theo lý thuyết, phân phối chuẩn có độ lệch bằng 0 và độ nhọn bằng 3. Kiểm định Jarque-Bera (JB) sử dụng công thức sau để đo lường xem sự xê dịch của hai đại lượng này so với mức chuẩn có nằm trong mức độ chấp nhận được hay không:
\[JB = \frac{n}{6} \left( S^2 + \frac{(K-3)^2}{4} \right)\]
Chỉ số JB càng lớn nghĩa là độ lệch và độ nhọn càng không chuẩn. Chỉ số này tuân theo phân phối Chi bình phương (\(\chi^2\)) với 2 độ tự do. Chúng ta có thể xem cách nó hoạt động ở hình sau:
viewof jbViz = {
const C = {
txt:'#16213E', sub:'#8294AA', light:'#E4E9F0',
card:'#fff', grid:'#F0F2F5',
hist:'#0d9488', histFill:'#ccfbf1',
chi:'#6366f1', chiFill:'#ede9fe',
jb:'#e11d48', jbFill:'#ffe4e6',
pass:'#10b981', fail:'#ef4444',
skew:'#d97706', kurt:'#2563eb',
};
const mono = "'SF Mono',SFMono-Regular,Menlo,monospace";
// ═══════════════════ MATH ═══════════════════
function chi2cdf(x){ return x>0 ? 1-Math.exp(-x/2) : 0; }
function sampleStats(data){
const n=data.length;
if(n<4) return {mean:0,sd:1,skew:0,kurt:3,jb:0,pval:1,zi:[],zi3:[],zi4:[]};
const mean=data.reduce((a,b)=>a+b,0)/n;
const m2=data.reduce((a,x)=>a+(x-mean)**2,0)/n;
const sd=Math.sqrt(m2);
const zi=data.map(x=>sd>0?(x-mean)/sd:0);
const zi3=zi.map(z=>z**3);
const zi4=zi.map(z=>z**4);
const skew=zi3.reduce((a,b)=>a+b,0)/n;
const kurt=zi4.reduce((a,b)=>a+b,0)/n;
const exKurt=kurt-3;
const jb=(n/6)*(skew**2+(1/4)*exKurt**2);
const pval=1-chi2cdf(jb);
return {mean,sd,skew,kurt,exKurt,jb,pval,zi,zi3,zi4};
}
function boxMuller(n){
const o=[];for(let i=0;i<n;i+=2){
const u1=Math.random(),u2=Math.random();
o.push(Math.sqrt(-2*Math.log(u1))*Math.cos(2*Math.PI*u2));
if(o.length<n)o.push(Math.sqrt(-2*Math.log(u1))*Math.sin(2*Math.PI*u2));
}return o;
}
const distributions=[
{key:'normal',label:'Phân phối chuẩn',fn:n=>boxMuller(n),desc:'S\u22480, K\u22483'},
{key:'rskew',label:'Lệch phải',fn:n=>Array.from({length:n},()=>-Math.log(Math.random())),desc:'S > 0'},
{key:'lskew',label:'Lệch trái',fn:n=>Array.from({length:n},()=>Math.log(Math.random())),desc:'S < 0'},
{key:'heavy',label:'Đuôi dày',fn:n=>{const z=boxMuller(n);return z.map(zi=>{const v=boxMuller(3).reduce((a,b)=>a+b*b,0)/3;return zi/Math.sqrt(v);});},desc:'K >> 3'},
{key:'uniform',label:'Đều',fn:n=>Array.from({length:n},()=>Math.random()*2-1),desc:'K < 3'},
{key:'bimodal',label:'Hai đỉnh',fn:n=>Array.from({length:n},()=>Math.random()<0.5?boxMuller(1)[0]-2:boxMuller(1)[0]+2),desc:'K < 3'},
];
// ═══════════════════ DOM ═══════════════════
const outer=document.createElement("div");
outer.style.cssText=`display:flex;flex-direction:column;gap:14px;
font-family:'Inter',-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
width:100%;max-width:960px;margin:0 auto;`;
if(!document.querySelector('link[href*="Inter"]')){
const lk=document.createElement('link');lk.rel='stylesheet';
lk.href='https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap';
document.head.appendChild(lk);
}
outer.appendChild(injectStyle());
// ═══════════════════ DIST PICKER ═══════════════════
const distRow=document.createElement("div");
distRow.style.cssText="display:flex;gap:5px;flex-wrap:wrap;justify-content:center;";
let distIdx=0;
const distBtns=[];
distributions.forEach((d,i)=>{
const b=document.createElement("button");
b.style.cssText=`padding:8px 16px;border-radius:10px;border:2px solid ${C.light};
font-size:13px;font-weight:700;cursor:pointer;font-family:inherit;transition:all .15s;
display:flex;flex-direction:column;align-items:center;gap:2px;`;
b.innerHTML=`${d.label}<span style="font-size:11px;font-weight:500;color:${C.sub};font-family:${mono};">${d.desc}</span>`;
b.addEventListener('click',()=>{distIdx=i;styleDist();drawSample();});
distRow.appendChild(b);distBtns.push(b);
});
outer.appendChild(distRow);
function styleDist(){distBtns.forEach((b,i)=>{
const on=i===distIdx;
b.style.background=on?C.hist:C.card;b.style.color=on?'#fff':C.txt;b.style.borderColor=on?C.hist:C.light;
b.querySelector('span').style.color=on?'rgba(255,255,255,0.7)':C.sub;
});}
const SL={};
SL.n=createSlider("Kích thước mẫu (n)",20,500,10,100,C.hist,"teal");
const ctrlRow=document.createElement("div");
ctrlRow.style.cssText="display:flex;gap:14px;width:100%;align-items:flex-end;";
ctrlRow.appendChild(SL.n.el);
const btnDraw=createButton("\u{1F3B2} Lấy mẫu ngẫu nhiên","go");
btnDraw.el.style.cssText+=";min-width:150px;height:40px;font-size:14px;";
ctrlRow.appendChild(btnDraw.el);
outer.appendChild(ctrlRow);
// ═══════════════════ MAIN GRID ═══════════════════
const mainGrid=document.createElement("div");
mainGrid.style.cssText="display:grid;grid-template-columns:1fr 1fr;gap:14px;width:100%;align-items:start;";
outer.appendChild(mainGrid);
// ── LEFT: histogram + skewness strip + kurtosis strip ──
const leftCol=document.createElement("div");
leftCol.style.cssText="display:flex;flex-direction:column;gap:0;";
mainGrid.appendChild(leftCol);
// Shared x-range (will be set during render)
let sharedXLo=0, sharedXHi=1;
const LW=460;
// Histogram
const histCard=document.createElement("div");
histCard.style.cssText=`background:${C.card};border-radius:16px 16px 0 0;border:1px solid ${C.light};
border-bottom:none;padding:14px 14px 4px;`;
const histTitle=document.createElement("div");
histTitle.style.cssText=`font-size:14px;font-weight:800;color:${C.hist};margin-bottom:8px;text-align:center;`;
histTitle.textContent="Biểu đồ phân phối mẫu (Histogram)";
histCard.appendChild(histTitle);
const histSvg=d3.create("svg").attr("viewBox",[0,0,LW,220])
.style("width","100%").style("display","block");
histCard.appendChild(histSvg.node());
leftCol.appendChild(histCard);
// Skewness strip
function mkStrip(title,color,showBottom){
const card=document.createElement("div");
const br=showBottom?'0 0 16px 16px':'0';
card.style.cssText=`background:${C.card};border-radius:${br};border:1px solid ${C.light};
border-top:none;${showBottom?'':'border-bottom:none;'}padding:4px 14px ${showBottom?'14px':'4px'};`;
const head=document.createElement("div");
head.style.cssText=`display:flex;justify-content:space-between;align-items:center;margin-bottom:2px;`;
const lbl=document.createElement("div");
lbl.style.cssText=`font-size:12px;font-weight:800;color:${color};`;
lbl.textContent=title;
const valEl=document.createElement("div");
valEl.style.cssText=`font-size:16px;font-weight:900;color:${color};font-family:${mono};`;
head.append(lbl,valEl);card.appendChild(head);
const svg=d3.create("svg").attr("viewBox",[0,0,LW,80])
.style("width","100%").style("display","block");
card.appendChild(svg.node());
leftCol.appendChild(card);
return{svg,valEl,color};
}
const skewStrip=mkStrip("Độ lệch (S)",C.skew,false);
const kurtStrip=mkStrip("Độ nhọn (K)",C.kurt,true);
// ── RIGHT: CDF chi-sq + formula + verdict ──
const rightCol=document.createElement("div");
rightCol.style.cssText="display:flex;flex-direction:column;gap:14px;";
mainGrid.appendChild(rightCol);
const chiCard=document.createElement("div");
chiCard.style.cssText=`background:${C.card};border-radius:16px;border:1px solid ${C.light};
padding:14px;box-shadow:0 2px 16px rgba(0,0,0,.04);`;
const chiTitle=document.createElement("div");
chiTitle.style.cssText=`font-size:14px;font-weight:800;color:${C.chi};margin-bottom:8px;text-align:center;`;
chiTitle.textContent="Hàm phân phối tích lũy \u03C7\u00B2(2)";
chiCard.appendChild(chiTitle);
const CW=460,CH=320;
const chiSvg=d3.create("svg").attr("viewBox",[0,0,CW,CH])
.style("width","100%").style("display","block").style("border-radius","12px")
.style("background","#FAFBFD").style("border",`1px solid ${C.light}`);
chiCard.appendChild(chiSvg.node());
rightCol.appendChild(chiCard);
// Formula
const formulaEl=document.createElement("div");
formulaEl.style.cssText=`background:${C.card};border-radius:14px;border:1px solid ${C.light};
padding:14px 18px;font-family:${mono};font-size:14px;line-height:2.2;text-align:center;color:${C.txt};`;
rightCol.appendChild(formulaEl);
// Verdict
const resultEl=document.createElement("div");
resultEl.style.cssText=`font-size:15px;font-weight:700;text-align:center;padding:14px;
border-radius:14px;border:2px solid ${C.light};`;
rightCol.appendChild(resultEl);
// ═══════════════════ STATE ═══════════════════
let data=[],stats={mean:0,sd:1,skew:0,kurt:3,exKurt:0,jb:0,pval:1,zi:[],zi3:[],zi4:[]};
function drawSample(){
SL.n.sync();
data=distributions[distIdx].fn(SL.n.val());
stats=sampleStats(data);
renderAll();
}
// ═══════════════════ RENDER ═══════════════════
function renderAll(){
const n=data.length;
if(n<4) return;
// Shared x range for all left panels
const lo=d3.min(data),hi=d3.max(data);
const pad=Math.max((hi-lo)*0.08,stats.sd*0.3);
sharedXLo=lo-pad; sharedXHi=hi+pad;
// Formula
formulaEl.innerHTML=`JB = <sup style="font-size:12px;">n</sup>\u2044<sub style="font-size:12px;">6</sub>`
+` [ <span style="color:${C.skew};">S</span>\u00B2 + \u00BC(<span style="color:${C.kurt};">K</span>\u22123)\u00B2 ]`
+`<br>= <sup style="font-size:12px;">${n}</sup>\u2044<sub style="font-size:12px;">6</sub>`
+` [ <span style="color:${C.skew};">${stats.skew.toFixed(3)}</span>\u00B2`
+` + \u00BC(<span style="color:${C.kurt};">${stats.kurt.toFixed(3)}</span>\u22123)\u00B2 ]`
+` = <b style="color:${C.jb};font-size:17px;">${stats.jb.toFixed(3)}</b>`;
renderHist();
renderStrip(skewStrip, data, stats.zi3, stats.skew, 0);
renderStrip(kurtStrip, data, stats.zi4, stats.kurt, 0);
renderChi();
// Verdict
const reject=stats.pval<0.05;
resultEl.style.background=reject?C.jbFill:C.card;
resultEl.style.borderColor=reject?C.fail:C.pass;
resultEl.style.color=reject?C.fail:C.pass;
resultEl.innerHTML=reject
?`<b>\u2718 Bác bỏ H\u2080</b> \u2502 JB = ${stats.jb.toFixed(2)} > 5.99 \u2502 p = ${stats.pval<0.0001?stats.pval.toExponential(2):stats.pval.toFixed(4)}`
:`<b>\u2714 Không đủ cơ sở bác bỏ H\u2080</b> \u2502 JB = ${stats.jb.toFixed(2)} \u2264 5.99 \u2502 p = ${stats.pval.toFixed(4)}`;
}
// ═══════════════════ HISTOGRAM ═══════════════════
function renderHist(){
histSvg.selectAll("*").remove();
const n=data.length;
const HH=220;
const mg={l:44,r:12,t:8,b:14}; // minimal bottom (no x labels — shared with strips)
const iw=LW-mg.l-mg.r,ih=HH-mg.t-mg.b;
const g=histSvg.append("g").attr("transform",`translate(${mg.l},${mg.t})`);
const xS=d3.scaleLinear().domain([sharedXLo,sharedXHi]).range([0,iw]);
const sorted=[...data].sort((a,b)=>a-b);
const q1=sorted[Math.floor(n*0.25)],q3=sorted[Math.floor(n*0.75)];
const iqr=q3-q1;
const fdBinW=iqr>0?2*iqr*Math.pow(n,-1/3):((sharedXHi-sharedXLo)/10||1);
const nBins=Math.max(8,Math.min(50,Math.round((sharedXHi-sharedXLo)/fdBinW)));
const bins=d3.bin().domain([sharedXLo,sharedXHi]).thresholds(nBins)(data);
const binW_=bins[0]?(bins[0].x1-bins[0].x0):1;
const maxDens=d3.max(bins,b=>b.length/(n*binW_));
const yS=d3.scaleLinear().domain([0,maxDens*1.15]).range([ih,0]);
yS.ticks(3).forEach(v=>{
g.append("line").attr("x2",iw).attr("y1",yS(v)).attr("y2",yS(v)).attr("stroke",C.grid);
g.append("text").attr("x",-6).attr("y",yS(v)+4).attr("text-anchor","end")
.attr("font-size",11).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(2));
});
bins.forEach(bin=>{
const dens=bin.length/(n*binW_);
if(dens===0)return;
const bx=xS(bin.x0),bw=xS(bin.x1)-xS(bin.x0);
g.append("rect").attr("x",bx+0.5).attr("y",yS(dens))
.attr("width",Math.max(1,bw-1)).attr("height",ih-yS(dens))
.attr("rx",2).attr("fill",C.hist).attr("opacity",0.45);
});
// Mean line
const meanPx=xS(stats.mean);
g.append("line").attr("x1",meanPx).attr("x2",meanPx).attr("y1",0).attr("y2",ih)
.attr("stroke",C.jb).attr("stroke-width",2).attr("stroke-dasharray","5,3");
g.append("text").attr("x",meanPx+5).attr("y",12)
.attr("font-size",12).attr("font-weight",800).attr("fill",C.jb)
.attr("font-family",mono).text(`x\u0304=${stats.mean.toFixed(2)}`);
g.append("line").attr("x2",iw).attr("y1",ih).attr("y2",ih).attr("stroke",C.sub).attr("opacity",0.3);
}
// ═══════════════════ MOMENT STRIP (shared x-axis) ═══════════════════
function renderStrip(panel, rawData, momentVals, summary, refLine){
const{svg,valEl,color}=panel;
svg.selectAll("*").remove();
valEl.textContent=summary.toFixed(4);
const SH=80;
const mg={l:44,r:12,t:4,b:16};
const iw=LW-mg.l-mg.r,ih=SH-mg.t-mg.b;
const g=svg.append("g").attr("transform",`translate(${mg.l},${mg.t})`);
const xS=d3.scaleLinear().domain([sharedXLo,sharedXHi]).range([0,iw]);
const n=rawData.length;
const maxAbs=Math.max(d3.max(momentVals,d=>Math.abs(d)),0.1);
const yS=d3.scaleLinear().domain([-maxAbs*1.1,maxAbs*1.1]).range([ih,0]);
const zeroY=yS(0);
// Zero line
g.append("line").attr("x2",iw).attr("y1",zeroY).attr("y2",zeroY)
.attr("stroke",C.sub).attr("stroke-width",0.8).attr("opacity",0.5);
// Bars at actual x positions
const barHalf=Math.max(0.8, iw/(n*2.5));
for(let i=0;i<n;i++){
const bx=xS(rawData[i]);
const v=momentVals[i];
g.append("line").attr("x1",bx).attr("x2",bx)
.attr("y1",zeroY).attr("y2",yS(v))
.attr("stroke",v>=0?color:C.sub).attr("stroke-width",barHalf).attr("opacity",0.45)
.attr("stroke-linecap","round");
}
// Mean line (= the summary statistic)
const clampedSummary=Math.max(-maxAbs*1.1,Math.min(maxAbs*1.1,summary));
g.append("line").attr("x2",iw).attr("y1",yS(clampedSummary)).attr("y2",yS(clampedSummary))
.attr("stroke",color).attr("stroke-width",2).attr("stroke-dasharray","5,3");
// Y labels
g.append("text").attr("x",-6).attr("y",yS(maxAbs*0.7)+4).attr("text-anchor","end")
.attr("font-size",10).attr("fill",C.sub).attr("font-family",mono).text(`+`);
g.append("text").attr("x",-6).attr("y",yS(-maxAbs*0.7)+4).attr("text-anchor","end")
.attr("font-size",10).attr("fill",C.sub).attr("font-family",mono).text(`\u2212`);
// X axis ticks (only on kurtosis strip — the bottom one)
if(panel===kurtStrip){
g.append("line").attr("x2",iw).attr("y1",ih).attr("y2",ih).attr("stroke",C.sub).attr("opacity",0.3);
xS.ticks(6).forEach(v=>{
g.append("text").attr("x",xS(v)).attr("y",ih+12).attr("text-anchor","middle")
.attr("font-size",11).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(1));
});
}
}
// ═══════════════════ CHI-SQ CDF PANEL ═══════════════════
function renderChi(){
chiSvg.selectAll("*").remove();
const mg={l:52,r:16,t:16,b:44};
const iw=CW-mg.l-mg.r,ih=CH-mg.t-mg.b;
const g=chiSvg.append("g").attr("transform",`translate(${mg.l},${mg.t})`);
const crit=5.991;
const xHi=Math.max(14,stats.jb*1.3,crit*2);
const xS=d3.scaleLinear().domain([0,xHi]).range([0,iw]);
const yS=d3.scaleLinear().domain([0,1.05]).range([ih,0]);
// Grid
[0,0.25,0.5,0.75,1].forEach(v=>{
g.append("line").attr("x2",iw).attr("y1",yS(v)).attr("y2",yS(v)).attr("stroke",C.grid);
g.append("text").attr("x",-8).attr("y",yS(v)+5).attr("text-anchor","end")
.attr("font-size",12).attr("fill",C.sub).attr("font-family",mono).text(v.toFixed(2));
});
// CDF curve
const pts=[];
for(let i=0;i<300;i++){const x=xHi*i/299;pts.push({x,y:chi2cdf(x)});}
g.append("path")
.attr("d",d3.line().x(d=>xS(d.x)).y(d=>yS(d.y)).curve(d3.curveLinear)(pts))
.attr("fill","none").attr("stroke",C.chi).attr("stroke-width",3);
// Critical value vertical
g.append("line").attr("x1",xS(crit)).attr("x2",xS(crit)).attr("y1",0).attr("y2",ih)
.attr("stroke",C.sub).attr("stroke-width",1.5).attr("stroke-dasharray","5,3");
g.append("text").attr("x",xS(crit)).attr("y",-6).attr("text-anchor","middle")
.attr("font-size",12).attr("font-weight",700).attr("fill",C.sub).attr("font-family",mono)
.text("5.991");
// Significance level line at 0.95
g.append("line").attr("x1",0).attr("x2",xS(crit)).attr("y1",yS(0.95)).attr("y2",yS(0.95))
.attr("stroke",C.sub).attr("stroke-width",1).attr("stroke-dasharray","4,3").attr("opacity",0.5);
g.append("text").attr("x",-8).attr("y",yS(0.95)+5).attr("text-anchor","end")
.attr("font-size",11).attr("fill",C.fail).attr("font-family",mono).attr("font-weight",700).text("0.95");
// JB lookup
if(data.length>=4){
const jbClamp=Math.min(stats.jb,xHi*0.98);
const jbPx=xS(jbClamp);
const cdfVal=chi2cdf(jbClamp);
const cdfPy=yS(cdfVal);
const pval=1-cdfVal;
// Vertical from x-axis to curve
g.append("line").attr("x1",jbPx).attr("x2",jbPx).attr("y1",cdfPy).attr("y2",ih)
.attr("stroke",C.jb).attr("stroke-width",2.5).attr("stroke-dasharray","6,4");
// Horizontal from curve to y-axis
g.append("line").attr("x1",0).attr("x2",jbPx).attr("y1",cdfPy).attr("y2",cdfPy)
.attr("stroke",C.jb).attr("stroke-width",2).attr("stroke-dasharray","6,4");
// Dot on curve
g.append("circle").attr("cx",jbPx).attr("cy",cdfPy).attr("r",6)
.attr("fill",C.jb).attr("stroke","#fff").attr("stroke-width",2);
// JB pill on x-axis
const pillX=Math.max(0,Math.min(iw-64,jbPx-32));
g.append("rect").attr("x",pillX).attr("y",ih+4).attr("width",64).attr("height",22)
.attr("rx",6).attr("fill",C.jb);
g.append("text").attr("x",pillX+32).attr("y",ih+18)
.attr("text-anchor","middle").attr("font-size",12).attr("font-weight",800)
.attr("fill","#fff").attr("font-family",mono).text(`JB=${stats.jb.toFixed(2)}`);
// CDF value pill on y-axis
g.append("rect").attr("x",-52).attr("y",cdfPy-12).attr("width",44).attr("height",24)
.attr("rx",6).attr("fill",C.jb);
g.append("text").attr("x",-30).attr("y",cdfPy+4)
.attr("text-anchor","middle").attr("font-size",11).attr("font-weight",800)
.attr("fill","#fff").attr("font-family",mono).text(cdfVal.toFixed(3));
// p-value annotation: bracket from CDF value to 1.0
const bracketX=jbPx+16;
g.append("line").attr("x1",bracketX).attr("x2",bracketX)
.attr("y1",yS(1)).attr("y2",cdfPy)
.attr("stroke",C.fail).attr("stroke-width",2.5);
g.append("line").attr("x1",bracketX-4).attr("x2",bracketX+4)
.attr("y1",yS(1)).attr("y2",yS(1))
.attr("stroke",C.fail).attr("stroke-width",2);
g.append("line").attr("x1",bracketX-4).attr("x2",bracketX+4)
.attr("y1",cdfPy).attr("y2",cdfPy)
.attr("stroke",C.fail).attr("stroke-width",2);
g.append("text").attr("x",bracketX+8).attr("y",(yS(1)+cdfPy)/2+5)
.attr("font-size",13).attr("font-weight",800).attr("fill",C.fail)
.attr("font-family",mono).text(`p=${pval<0.0001?pval.toExponential(1):pval.toFixed(3)}`);
}
// X axis
g.append("line").attr("x2",iw).attr("y1",ih).attr("y2",ih).attr("stroke",C.sub).attr("opacity",0.25);
xS.ticks(6).forEach(v=>{
g.append("text").attr("x",xS(v)).attr("y",ih+18).attr("text-anchor","middle")
.attr("font-size",12).attr("fill",C.sub).attr("font-family",mono)
.text(Number.isInteger(v)?v:v.toFixed(1));
});
chiSvg.append("text").attr("x",mg.l+iw/2).attr("y",CH-2)
.attr("text-anchor","middle").attr("font-size",13).attr("font-weight",700).attr("fill",C.sub).text("Giá trị thống kê JB");
chiSvg.append("text").attr("x",12).attr("y",mg.t+ih/2)
.attr("text-anchor","middle").attr("font-size",13).attr("font-weight",700).attr("fill",C.sub)
.attr("transform",`rotate(-90,12,${mg.t+ih/2})`).text("F(x) = P(\u03C7\u00B2 \u2264 x)");
}
// ═══════════════════ EVENTS ═══════════════════
btnDraw.el.addEventListener("click",drawSample);
SL.n.input.addEventListener("input",drawSample);
styleDist();drawSample();
outer.value={};return outer;
}Một ứng dụng khác của moments là phương pháp “method of moments” dùng để xác định điểm bắt đầu của tham số ước lượng khi chúng ta sử dụng maximum likelihood estimation (Bolker 2008). Sẽ được hướng dẫn chi tiết và áp dụng ở bài likelihood.