3 Phân phối rời rạc
3.1 Phép thử Bernoulli (Bernoulli trial, hay binomial trial)
Là một lần thử nghiệm duy nhất, không gian mẫu chỉ có đúng 2 kết quả: Thành công (Success) hoặc Thất bại (Failure)
Ví dụ: tung đồng xu (mặt sấp, mặt ngửa), xét nghiệm một ca nghi ngờ (dương tính, âm tính)
Khi ta thực hiện phép thử Bernoulli nhiều lần liên tiếp (ví dụ: tung một đồng xu nhiều lần, dùng một loại xét nghiệm để test nhiều người), ta đang thực hiện 1 chuỗi Bernoulli (Bernoulli process). Chuỗi Bernoulli chuẩn cần thỏa 2 điều kiện:
- Độc lập (Independent): Kết quả của lần thử này không ảnh hưởng đến lần thử khác.
- Xác suất không đổi: Xác suất thành công (\(p\)) phải giống hệt nhau ở mọi lần thử.
Hầu hết các phân phối rời rạc đều xuất phát từ chuỗi Bernoulli với các mục tiêu khác nhau.
3.2 Đếm số lần thành công
Chúng ta xác định trước cỡ mẫu (\(n\) phép thử), sau đó đếm số biến cố mục tiêu thu được (\(k\) ca thành công). Nếu lấy \(\frac{k}{n}\) ta được tỉ lệ. Vì vậy các loại phân phối với mục tiêu này có thể được dùng để ước lượng tỉ lệ trong quần thể.
Hai phân phối đại diện cho nhóm này là Nhị thức (Binomial) và Siêu bội (Hypergeometric). Xét bài toán: Thực hiện một khảo sát ngẫu nhiên để ước lượng tỷ lệ tiêm chủng trong dân số:
Phân phối Siêu bội: Quần thể mục tiêu có quy mô xác định là \(N\) người, trong đó có \(K\) người đã tiêm chủng. Chúng ta tiến hành chọn mẫu khảo sát \(n\) người.
Phân phối Nhị thức: Quy mô quần thể là vô hạn (hoặc rất lớn). Giả định tỷ lệ tiêm chủng chung trong quần thể là \(p\), tương đương với xác suất chọn ngẫu nhiên được một người đã tiêm chủng là \(p\). Chúng ta tiến hành chọn mẫu khảo sát \(n\) người.
viewof app = {
const svgNS = "http://www.w3.org/2000/svg";
function lnF(n){let s=0;for(let i=2;i<=n;i++)s+=Math.log(i);return s}
function lnC(n,k){return(k<0||k>n)?-Infinity:lnF(n)-lnF(k)-lnF(n-k)}
function hPMF(k,N,K,n){return Math.exp(lnC(K,k)+lnC(N-K,n-k)-lnC(N,n))}
function bPMF(k,n,p){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 sampleHyper(N,K,n){let k=0,Ni=N,Ki=K;for(let i=0;i<n;i++){if(Math.random()<Ki/Ni){k++;Ki--}Ni--}return k}
function sampleBinom(n,p){let k=0;for(let i=0;i<n;i++)if(Math.random()<p)k++;return k}
const C={pd:"#e63946",pds:"#a4262f",pu:"#f4a6ad",pus:"#e07980",nd:"#457b9d",nds:"#1d3557",nu:"#a8c8dd",nus:"#7ba7c2"};
// Flood fill from seed
function floodBlock(total,cols,n,seed,validSet){
if(n<=0)return[];
let vs=validSet;if(!vs){vs=new Set();for(let i=0;i<total;i++)vs.add(i);}
if(n>=vs.size)return [...vs];
const rows=Math.ceil(total/cols);
if(!vs.has(seed)){const a=[...vs];if(a.length>0)seed=a[0];}
const chosen=new Set([seed]),inF=new Set(),frontier=[];
const push=i=>{const r=Math.floor(i/cols),c=i%cols;
for(const[dr,dc]of[[0,-1],[0,1],[-1,0],[1,0]]){
const nr=r+dr,nc=c+dc,ni=nr*cols+nc;
if(nr>=0&&nr<rows&&nc>=0&&nc<cols&&ni<total&&vs.has(ni)&&!chosen.has(ni)&&!inF.has(ni)){frontier.push(ni);inF.add(ni)}}};
push(seed);
while(chosen.size<n&&frontier.length>0){
let maxN=-1,bestFi=[];for(let i=0;i<frontier.length;i++){const f=frontier[i];let cD=0;const r=Math.floor(f/cols),c=f%cols;if(c>0&&chosen.has(f-1))cD++;if(c<cols-1&&chosen.has(f+1))cD++;if(r>0&&chosen.has(f-cols))cD++;if(r<rows-1&&chosen.has(f+cols))cD++;if(cD>maxN){maxN=cD;bestFi=[i];}else if(cD===maxN)bestFi.push(i);}
const fi=bestFi[Math.floor(Math.random()*bestFi.length)];
const pick=frontier[fi];frontier[fi]=frontier[frontier.length-1];frontier.pop();inF.delete(pick);
if(!chosen.has(pick)){chosen.add(pick);push(pick)}}
if(chosen.size<n){for(const i of vs)if(!chosen.has(i)){chosen.add(i);if(chosen.size>=n)break}}
return[...chosen];
}
// Balanced block: exactly kPos positive + kNeg negative cells, straddling divider
function pickBalancedBlock(N,cols,rows,posSet,divCol,kPos,kNeg,validSet){
const total=kPos+kNeg;
if(total<=0)return[];
let vs=validSet;if(!vs){vs=new Set();for(let i=0;i<N;i++)vs.add(i);}
// All one type
if(kPos===0){const seeds=[];for(const i of vs)if(!posSet.has(i))seeds.push(i);
const s=seeds[Math.floor(Math.random()*seeds.length)]||(seeds[0]||0);return floodBlock(N,cols,kNeg,s,vs)}
if(kNeg===0){const seeds=[];for(const i of vs)if(posSet.has(i))seeds.push(i);
const s=seeds[Math.floor(Math.random()*seeds.length)]||(seeds[0]||0);return floodBlock(N,cols,kPos,s,vs)}
// Mixed: grow from divider, respecting quotas
const seedRow=Math.floor(Math.random()*rows);
let seedCell=Math.min(seedRow*cols+divCol,N-1);
if(!vs.has(seedCell)){const a=[...vs];if(a.length>0)seedCell=a[Math.floor(Math.random()*a.length)];}
const chosen=new Set();let gP=0,gN=0;
chosen.add(seedCell);if(posSet.has(seedCell))gP++;else gN++;
const inF=new Set(),frontier=[];
const push=i=>{const r=Math.floor(i/cols),c=i%cols;
for(const[dr,dc]of[[0,-1],[0,1],[-1,0],[1,0]]){
const nr=r+dr,nc=c+dc,ni=nr*cols+nc;
if(nr>=0&&nr<rows&&nc>=0&&nc<cols&&ni<N&&vs.has(ni)&&!chosen.has(ni)&&!inF.has(ni)){frontier.push(ni);inF.add(ni)}}};
push(seedCell);
let safety=N*3;
while(chosen.size<total&&frontier.length>0&&safety-->0){
let maxN=-1,bestFi=[];for(let i=0;i<frontier.length;i++){const f=frontier[i];let cD=0;const r=Math.floor(f/cols),c=f%cols;if(c>0&&chosen.has(f-1))cD++;if(c<cols-1&&chosen.has(f+1))cD++;if(r>0&&chosen.has(f-cols))cD++;if(r<rows-1&&chosen.has(f+cols))cD++;if(cD>maxN){maxN=cD;bestFi=[i];}else if(cD===maxN)bestFi.push(i);}
const fi=bestFi[Math.floor(Math.random()*bestFi.length)];
const pick=frontier[fi];frontier[fi]=frontier[frontier.length-1];frontier.pop();inF.delete(pick);
if(chosen.has(pick))continue;
const isP=posSet.has(pick);
if(isP&&gP>=kPos){continue}
if(!isP&&gN>=kNeg){continue}
chosen.add(pick);if(isP)gP++;else gN++;push(pick)}
// Fallback: if didn't fill, relax constraints
while(chosen.size<total&&frontier.length>0){
let maxN=-1,bestFi=[];for(let i=0;i<frontier.length;i++){const f=frontier[i];let cD=0;const r=Math.floor(f/cols),c=f%cols;if(c>0&&chosen.has(f-1))cD++;if(c<cols-1&&chosen.has(f+1))cD++;if(r>0&&chosen.has(f-cols))cD++;if(r<rows-1&&chosen.has(f+cols))cD++;if(cD>maxN){maxN=cD;bestFi=[i];}else if(cD===maxN)bestFi.push(i);}
const fi=bestFi[Math.floor(Math.random()*bestFi.length)];
const pick=frontier[fi];frontier[fi]=frontier[frontier.length-1];frontier.pop();inF.delete(pick);
if(!chosen.has(pick)){chosen.add(pick);push(pick)}}
if(chosen.size<total){for(const i of vs){if(!chosen.has(i)){chosen.add(i);if(chosen.size>=total)break;}}}
return[...chosen];
}
// Squared boundary with rounded corners
function cellBoundaryPath(indices,cols,cellW,cellH,ox,oy,pad,radius){
const cells=new Set();
for(const i of indices)cells.add(Math.floor(i/cols)+","+i%cols);
const has=(r,c)=>cells.has(r+","+c);
const edges=[];
for(const key of cells){
const[r,c]=key.split(",").map(Number);
const l=ox+c*cellW,t=oy+r*cellH,ri=ox+(c+1)*cellW,b=oy+(r+1)*cellH;
if(!has(r-1,c))edges.push([l,t,ri,t]);
if(!has(r,c+1))edges.push([ri,t,ri,b]);
if(!has(r+1,c))edges.push([ri,b,l,b]);
if(!has(r,c-1))edges.push([l,b,l,t])}
if(edges.length<3)return"";
const eps=0.5,eq=(a,b)=>Math.abs(a-b)<eps;
const used=new Array(edges.length).fill(false),chains=[];
for(let start=0;start<edges.length;start++){
if(used[start])continue;const chain=[start];used[start]=true;
for(let s2=0;s2<edges.length+2;s2++){
const last=edges[chain[chain.length-1]],ex=last[2],ey=last[3];
const first=edges[chain[0]];
if(chain.length>2&&eq(ex,first[0])&&eq(ey,first[1]))break;
let found=false;
for(let j=0;j<edges.length;j++){if(used[j])continue;
if(eq(ex,edges[j][0])&&eq(ey,edges[j][1])){chain.push(j);used[j]=true;found=true;break}}
if(!found)break}
if(chain.length>=3)chains.push(chain)}
if(chains.length===0)return"";
const R=Math.min(radius,cellW*.35,cellH*.35);
let d="";
for(let cIdx=0;cIdx<chains.length;cIdx++){
const pts=chains[cIdx].map(i=>[edges[i][0],edges[i][1]]);
const len=pts.length;
const offset_pts=[];
for(let i=0;i<len;i++){
const prev=pts[(i-1+len)%len],cur=pts[i],next=pts[(i+1)%len];
const dx1=cur[0]-prev[0],dy1=cur[1]-prev[1];
const dx2=next[0]-cur[0],dy2=next[1]-cur[1];
const l1=Math.hypot(dx1,dy1),l2=Math.hypot(dx2,dy2);
if(l1===0||l2===0){offset_pts.push(cur);continue;}
const nx1=dy1/l1,ny1=-dx1/l1,nx2=dy2/l2,ny2=-dx2/l2;
offset_pts.push([cur[0]+pad*(nx1+nx2),cur[1]+pad*(ny1+ny2)]);
}
for(let i=0;i<len;i++){
const prev=offset_pts[(i-1+len)%len],cur=offset_pts[i],next=offset_pts[(i+1)%len];
const dx1=cur[0]-prev[0],dy1=cur[1]-prev[1],dx2=next[0]-cur[0],dy2=next[1]-cur[1];
const l1=Math.hypot(dx1,dy1),l2=Math.hypot(dx2,dy2);
if(l1===0||l2===0)continue;
const r2=Math.min(R,l1/2,l2/2);
const bx=cur[0]-(dx1/l1)*r2,by2=cur[1]-(dy1/l1)*r2;
const ax=cur[0]+(dx2/l2)*r2,ay=cur[1]+(dy2/l2)*r2;
if(i===0)d+=`M${bx.toFixed(1)},${by2.toFixed(1)} `;
else d+=`L${bx.toFixed(1)},${by2.toFixed(1)} `;
d+=`Q${cur[0].toFixed(1)},${cur[1].toFixed(1)} ${ax.toFixed(1)},${ay.toFixed(1)} `}
d+="Z ";
}
return d}
const root=document.createElement("div");root.className="hb-root";
const css = document.createElement("style");
css.textContent = `@import url('https://fonts.googleapis.com/css2?family=Alegreya:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&family=Alegreya+Sans:ital,wght@0,400;0,500;0,700;1,400&family=Inter:wght@400;500;600;700&display=swap');
.hb-root {
--f-text: 'Alegreya Sans', system-ui, sans-serif;
--f-display: 'Alegreya', Georgia, serif;
--f-mono: 'Inter', system-ui, sans-serif;
--pos-drawn: #e63946;
--pos-drawn-s: #a4262f;
--pos-undrawn: #f4a6ad;
--pos-undrawn-s: #e07980;
--neg-drawn: #457b9d;
--neg-drawn-s: #1d3557;
--neg-undrawn: #a8c8dd;
--neg-undrawn-s: #7ba7c2;
--c-bg: #f8fafc;
--c-card: #ffffff;
--c-border: #e2e8f0;
--c-muted: #64748b;
--c-text: #0f172a;
max-width: 1280px;
margin: 0 auto;
padding: 24px 16px 60px;
font-family: var(--f-text);
color: var(--c-text);
background: var(--c-bg);
border-radius: 24px;
}
.hb-bar{background:var(--c-card);border:1px solid var(--c-border);border-radius:20px;padding:20px 24px;margin-bottom:24px;box-shadow:0 10px 30px -10px rgba(0,0,0,0.06),0 4px 6px -4px rgba(0,0,0,0.03);display:flex;flex-direction:column;gap:16px}
.hb-bar-row{display:flex;gap:20px;flex-wrap:wrap;justify-content:center}
.hb-bar-row>div{flex:1 1 140px;min-width:120px;max-width:250px}
.hb-slider-disabled{opacity:.5;pointer-events:none}
.hb-panels{display:grid;grid-template-columns:1fr 1fr;gap:28px;align-items:start}
.hb-panel{background:var(--c-card);border:1px solid var(--c-border);border-radius:20px;overflow:hidden;box-shadow:0 12px 36px -12px rgba(0,0,0,0.08),0 4px 8px -4px rgba(0,0,0,0.04)}
.hb-panel-head{padding:14px 20px 12px;border-bottom:1px solid var(--c-border);display:flex;align-items:baseline;gap:10px;background:linear-gradient(to bottom,#ffffff,#fafbfc)}
.hb-ptag{font-family:var(--f-mono);font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:1px;padding:4px 10px;border-radius:6px}
.hb-ptag-h{background:#eff6ff;color:#1d4ed8}
.hb-ptag-b{background:#faf5ff;color:#7e22ce}
.hb-ptitle{font-family:var(--f-display);font-size:18px;font-weight:600;color:#1e293b}
.hb-panel-btns{display:flex;gap:8px;padding:10px 20px;border-bottom:1px solid var(--c-border);background:#f8fafc;align-items:center;flex-wrap:wrap}
.hb-panel-btns button{flex:1;min-width:80px;padding:8px 12px;font-weight:600;border-radius:8px;transition:color 0.15s, border-color 0.15s;background-color:#ffffff;border:1px solid #cbd5e1;color:#475569;cursor:pointer}
.hb-panel-btns button:hover{color:#0f172a;border-color:#94a3b8}
.hb-panel-count{font-family:var(--f-mono);font-size:12px;color:var(--c-muted);white-space:nowrap;margin-left:8px;font-weight:500}
.hb-viz{padding:16px 16px 8px}
.hb-pmf{padding:8px 16px 16px;border-top:1px solid var(--c-border);background:#fdfdfe}
.hb-pmf-lbl{font-family:var(--f-mono);font-size:10px;color:var(--c-muted);text-transform:uppercase;letter-spacing:1px;font-weight:600;margin-bottom:6px;padding-top:8px;text-align:center}
.hb-tip{position:fixed;background:rgba(15,23,42,0.95);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);color:#f8fafc;font-family:var(--f-text);font-size:14px;line-height:1.5;padding:12px 18px;border-radius:12px;max-width:300px;pointer-events:none;z-index:1000;box-shadow:0 12px 36px rgba(0,0,0,.3);border:1px solid rgba(255,255,255,.1);display:none;transition:opacity 0.1s}
.hb-tip strong{font-family:var(--f-display);font-size:16px;display:block;margin-bottom:4px;letter-spacing:0.2px}
.hb-tip b{color:#fbbf24}.hb-tip em{font-style:italic;color:#38bdf8}.hb-tip .warn{color:#f87171;font-weight:600}
.hb-legend{display:flex;gap:18px;justify-content:center;flex-wrap:wrap;margin-top:24px;padding:14px 20px;background:var(--c-card);border:1px solid var(--c-border);border-radius:16px;font-size:13px;color:#475569;font-weight:500;box-shadow:0 4px 12px -8px rgba(0,0,0,0.05)}
.hb-legend-item{display:flex;align-items:center;gap:6px;white-space:nowrap}
.hb-ldot{width:14px;height:14px;border-radius:50%;flex-shrink:0}
@media(max-width:1024px){.hb-panels{grid-template-columns:1fr}}
@media(max-width:640px){.hb-root{padding:12px 0px 30px;border-radius:0;background:transparent}.hb-bar{padding:16px;gap:12px}.hb-bar-row{gap:12px}.hb-bar-row>div{flex:1 1 100%;max-width:none}.hb-panel{border-radius:16px}.hb-panel-head{padding:12px 16px 10px}.hb-panel-btns{padding:10px 16px}.hb-viz{padding:12px 8px 4px}.hb-pmf{padding:6px 8px 14px}.hb-legend{gap:12px;font-size:12px;padding:14px;flex-direction:column;align-items:center}}`;
root.appendChild(css);
const tip=document.createElement("div");tip.className="hb-tip";root.appendChild(tip);
function showTip(e,html){tip.innerHTML=html;tip.style.display="block";let x=e.clientX+14,y=e.clientY-8;if(x+290>window.innerWidth)x=e.clientX-296;if(y+160>window.innerHeight)y=e.clientY-164;if(y<4)y=4;if(x<4)x=4;tip.style.left=x+"px";tip.style.top=y+"px"}
function hideTip(){tip.style.display="none"}
const bar=document.createElement("div");bar.className="hb-bar";root.appendChild(bar);bar.appendChild(injectStyle());
const row1=document.createElement("div");row1.className="hb-bar-row";bar.appendChild(row1);
const row2=document.createElement("div");row2.className="hb-bar-row";bar.appendChild(row2);
const SL={};
SL.N=createSlider("\u{1D441} population",10,200,1,60,"#1d2d3f","dark");
SL.K=createSlider("\u{1D43E} positive",0,60,1,18,"#e63946","red");
SL.p=createSlider("\u{1D45D} = \u{1D43E} / \u{1D441}",0,1,0.001,0.300,"#7b2d8e","purple");
SL.n=createSlider("\u{1D45B} draws",0,60,1,15,"#457b9d","blue");
row1.appendChild(SL.N.el);row1.appendChild(SL.K.el);row2.appendChild(SL.p.el);row2.appendChild(SL.n.el);
SL.p.el.classList.add("hb-slider-disabled");SL.p.input.tabIndex=-1;
for(const s of[SL.N,SL.K,SL.n,SL.p]){const lbl=s.el.querySelector("span");lbl.style.fontFamily="'SF Mono',monospace";lbl.style.textTransform="none";lbl.style.fontSize="14px";lbl.style.letterSpacing="0";s.valSpan.style.fontFamily="'SF Mono',monospace"}
function syncP(){const N=SL.N.val(),K=SL.K.val();SL.p.update(N>0?Math.round(K/N*1000)/1000:0,0,1);SL.p.sync()}
function clamp(){const N=SL.N.val();if(SL.K.val()>N)SL.K.update(N,0,N);else SL.K.update(SL.K.val(),0,N);if(SL.n.val()>N)SL.n.update(N,0,N);else SL.n.update(SL.n.val(),0,N);SL.K.sync();SL.n.sync();syncP()}
const ST={hK:null,hDrawn:false,bK:null,bDrawn:false,hHist:{},bHist:{},hN:0,bN:0,hAutoId:null,bAutoId:null};
function resetH(){ST.hK=null;ST.hDrawn=false;ST.hHist={};ST.hN=0;if(ST.hAutoId){clearInterval(ST.hAutoId);ST.hAutoId=null;hAutoBtn.setText("\u23E9 Auto")}}
function resetB(){ST.bK=null;ST.bDrawn=false;ST.bHist={};ST.bN=0;if(ST.bAutoId){clearInterval(ST.bAutoId);ST.bAutoId=null;bAutoBtn.setText("\u23E9 Auto")}}
function mkPanel(tag,tagCls,title){const p=document.createElement("div");p.className="hb-panel";const hd=document.createElement("div");hd.className="hb-panel-head";hd.innerHTML=`<span class="hb-ptag ${tagCls}">${tag}</span><span class="hb-ptitle">${title}</span>`;p.appendChild(hd);const btns=document.createElement("div");btns.className="hb-panel-btns";p.appendChild(btns);const viz=document.createElement("div");viz.className="hb-viz";p.appendChild(viz);const pmf=document.createElement("div");pmf.className="hb-pmf";const lbl=document.createElement("div");lbl.className="hb-pmf-lbl";pmf.appendChild(lbl);const psvg=document.createElementNS(svgNS,"svg");psvg.setAttribute("viewBox","0 0 540 170");psvg.style.cssText="width:100%;height:auto;display:block;";pmf.appendChild(psvg);p.appendChild(pmf);return{panel:p,btns,viz,pmfLbl:lbl,pmfSvg:psvg}}
const panels=document.createElement("div");panels.className="hb-panels";root.appendChild(panels);
// LEFT
const L=mkPanel("Finite","hb-ptag-h","Hypergeometric");panels.appendChild(L.panel);L.pmfLbl.textContent="PMF P(X = k) \u2014 Hypergeometric";
const hDrawBtn=createButton("\u{1F3B2} Draw","go"),hAutoBtn=createButton("\u23E9 Auto","auto"),hResetBtn=createButton("\u21BA Reset","reset");
L.btns.appendChild(hDrawBtn.el);L.btns.appendChild(hAutoBtn.el);L.btns.appendChild(hResetBtn.el);
const hSvg=document.createElementNS(svgNS,"svg");hSvg.setAttribute("viewBox","0 0 540 390");hSvg.style.cssText="width:100%;height:auto;display:block;";L.viz.appendChild(hSvg);
function renderH(){
const N=SL.N.val(),K=SL.K.val(),n=SL.n.val();
while(hSvg.firstChild)hSvg.removeChild(hSvg.firstChild);
const bx=16,by=28,bw=508,bh=345;
const baseCols=Math.max(1,Math.ceil(Math.sqrt(N*(bw/bh))));
const rows=Math.ceil(N/baseCols);
const kCols=Math.ceil(K/rows),nCols=Math.ceil((N-K)/rows);
const cols=kCols+nCols;
const cellW=(bw-16)/Math.max(1,cols),cellH=(bh-28)/rows;
const dotR=Math.max(2.5,Math.min(Math.min(cellW,cellH)*.34,9));
const ox=bx+8,oy=by+8;
const hBg=document.createElementNS(svgNS,"rect");
hBg.setAttribute("x",bx);hBg.setAttribute("y",by);hBg.setAttribute("width",bw);hBg.setAttribute("height",bh);hBg.setAttribute("rx",8);hBg.setAttribute("ry",8);
hBg.setAttribute("fill","rgba(0,0,0,0)");hBg.setAttribute("stroke","#b0b8c4");hBg.setAttribute("stroke-width",1.8);
if(ST.hDrawn)hBg.dataset.cat="undrawn";
hSvg.appendChild(hBg);
// Separate items perfectly by columns with gap
const validSet=new Set();
const posSet=new Set();
for(let i=0;i<K;i++){const r=i%rows,c=(kCols-1)-Math.floor(i/rows);const idx=r*cols+c;validSet.add(idx);posSet.add(idx);}
for(let i=0;i<N-K;i++){const r=i%rows,c=kCols+Math.floor(i/rows);const idx=r*cols+c;validSet.add(idx);}
let divCol=kCols;
if(K>0&&K<N){
const divX=ox+divCol*cellW;
mkLine(hSvg,divX,by+6,divX,by+bh-6,"#cdd3da",1,"4 3");}
let drawnCells=null;
if(ST.hDrawn){
drawnCells=pickBalancedBlock(cols*rows,cols,rows,posSet,divCol,ST.hK,n-ST.hK,validSet);
const pathD=cellBoundaryPath(drawnCells,cols,cellW,cellH,ox,oy,3,7);
if(pathD){const path=document.createElementNS(svgNS,"path");path.setAttribute("d",pathD);path.setAttribute("fill","rgba(26,111,160,0.07)");path.setAttribute("stroke","rgba(26,111,160,0.5)");path.setAttribute("stroke-width",2.2);path.setAttribute("stroke-dasharray","8 4");path.dataset.cat="drawn";path.dataset.origOp="1";hSvg.appendChild(path)}
const ys=drawnCells.map(i=>oy+Math.floor(i/cols)*cellH);const xs=drawnCells.map(i=>ox+(i%cols)*cellW);}
const drawnSet=drawnCells?new Set(drawnCells):null;
for(const i of validSet){const r=Math.floor(i/cols),c2=i%cols;
const cx=ox+c2*cellW+cellW/2,cy=oy+r*cellH+cellH/2;
const isP=posSet.has(i),isDrawn=drawnSet?drawnSet.has(i):false;
const st=dotStyle(isP,isDrawn,ST.hDrawn);
const ci=mkCircle(hSvg,cx,cy,dotR,st.fill,st.stroke,st.sw,st.op);
ci.dataset.cat=ST.hDrawn?(isDrawn?"drawn":"undrawn"):(isP?"pre_pos":"pre_neg");}
attachHover(hSvg,cat=>hTip(cat,n,N,K),()=>resetOp(hSvg));
renderPMF_H(N,K,n)}
function hTip(cat,n,N,K){
const k=ST.hK;
return({
pre_pos:"<strong style='color:"+C.pd+"'>Positive in population</strong><br><b>"+K+"</b> of <b>"+N+"</b> are positive.",
pre_neg:"<strong style='color:"+C.nd+"'>Negative in population</strong><br><b>"+(N-K)+"</b> are negative.",
drawn:"<strong style='color:"+C.pd+"'>Drawn Sample</strong><br><b>\u{1D458} = "+k+"</b> positive, <b>\u{1D45B} = "+n+"</b> samples.<br>This is the sample we observed.",
undrawn:"<strong style='color:"+C.nd+"'>Unobserved Population</strong><br><span class='warn'>\u26A0 This is the source of uncertainty.</span>"
})[cat]||"";
}
function renderPMF_H(N,K,n){const svg=L.pmfSvg;while(svg.firstChild)svg.removeChild(svg.firstChild);const kMin=Math.max(0,n-(N-K)),kMax=Math.min(n,K);if(kMax<kMin||n===0)return;const theo=[];for(let kk=kMin;kk<=kMax;kk++)theo.push({k:kk,p:hPMF(kk,N,K,n)});drawPMFChart(svg,theo,ST.hHist,ST.hN,ST.hK,"#e63946","#c0515c",kMin,kMax)}
function doDrawH(){const N=SL.N.val(),K=SL.K.val(),n=SL.n.val();if(n===0)return;ST.hK=sampleHyper(N,K,n);ST.hDrawn=true;ST.hHist[ST.hK]=(ST.hHist[ST.hK]||0)+1;ST.hN++;renderH()}
hDrawBtn.el.addEventListener("click",doDrawH);hResetBtn.el.addEventListener("click",()=>{resetH();renderH()});
hAutoBtn.el.addEventListener("click",()=>{if(ST.hAutoId){clearInterval(ST.hAutoId);ST.hAutoId=null;hAutoBtn.setText("\u23E9 Auto")}else{ST.hAutoId=setInterval(doDrawH,200);hAutoBtn.setText("\u23F8 Pause")}});
// RIGHT
const R=mkPanel("\u221E pool","hb-ptag-b","Binomial");panels.appendChild(R.panel);R.pmfLbl.textContent="PMF P(X = k) \u2014 Binomial";
const bDrawBtn=createButton("\u{1F3B2} Draw","go"),bAutoBtn=createButton("\u23E9 Auto","auto"),bResetBtn=createButton("\u21BA Reset","reset");
R.btns.appendChild(bDrawBtn.el);R.btns.appendChild(bAutoBtn.el);R.btns.appendChild(bResetBtn.el);
const bSvg=document.createElementNS(svgNS,"svg");bSvg.setAttribute("viewBox","0 0 540 390");bSvg.style.cssText="width:100%;height:auto;display:block;";R.viz.appendChild(bSvg);
const POOL=150,PCOLS=15;
function renderB(){
const N=SL.N.val(),K=SL.K.val(),n=SL.n.val(),p=N>0?K/N:0;
while(bSvg.firstChild)bSvg.removeChild(bSvg.firstChild);
const padX=16,padY=28,areaW=508,areaH=345;
const pRows=Math.ceil(POOL/PCOLS),cellW=areaW/PCOLS,cellH=areaH/pRows;
const poolR=Math.max(2.5,Math.min(Math.min(cellW,cellH)*.34,9));
const defs=document.createElementNS(svgNS,"defs");
defs.innerHTML='<linearGradient id="fl"><stop offset="0%" stop-color="#f7f8fa" stop-opacity="1"/><stop offset="100%" stop-color="#f7f8fa" stop-opacity="0"/></linearGradient><linearGradient id="fr"><stop offset="0%" stop-color="#f7f8fa" stop-opacity="0"/><stop offset="100%" stop-color="#f7f8fa" stop-opacity="1"/></linearGradient><linearGradient id="ft" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#f7f8fa" stop-opacity=".85"/><stop offset="100%" stop-color="#f7f8fa" stop-opacity="0"/></linearGradient><linearGradient id="fb" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#f7f8fa" stop-opacity="0"/><stop offset="100%" stop-color="#f7f8fa" stop-opacity=".85"/></linearGradient>';
bSvg.appendChild(defs);
const bBg=document.createElementNS(svgNS,"rect");
bBg.setAttribute("x",padX);bBg.setAttribute("y",padY);bBg.setAttribute("width",areaW);bBg.setAttribute("height",areaH);bBg.setAttribute("rx",10);bBg.setAttribute("ry",10);
bBg.setAttribute("fill","rgba(0,0,0,0)");
bBg.dataset.cat=ST.bDrawn?"undrawn":"b_pool";
bSvg.appendChild(bBg);
let drawnSet=null,drawnPosSet=null;
if(ST.bDrawn){const k=ST.bK;
const seedIdx=Math.floor(Math.random()*POOL);
const blockIdx=floodBlock(POOL,PCOLS,n,seedIdx);drawnSet=new Set(blockIdx);
const shuf=[...blockIdx];for(let i=shuf.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[shuf[i],shuf[j]]=[shuf[j],shuf[i]]}
drawnPosSet=new Set(shuf.slice(0,k));
const pathD=cellBoundaryPath(blockIdx,PCOLS,cellW,cellH,padX,padY,3,7);
if(pathD){const path=document.createElementNS(svgNS,"path");path.setAttribute("d",pathD);path.setAttribute("fill","rgba(100,50,150,0.07)");path.setAttribute("stroke","rgba(100,50,150,0.45)");path.setAttribute("stroke-width",2.2);path.setAttribute("stroke-dasharray","8 4");path.dataset.cat="drawn";path.dataset.origOp="1";bSvg.appendChild(path)}
const ys=blockIdx.map(i=>padY+Math.floor(i/PCOLS)*cellH),xs=blockIdx.map(i=>padX+(i%PCOLS)*cellW);}
for(let i=0;i<POOL;i++){const c=i%PCOLS,ro=Math.floor(i/PCOLS);
const cx=padX+c*cellW+cellW/2,cy=padY+ro*cellH+cellH/2;
const isDrawn=drawnSet?drawnSet.has(i):false;
if(!ST.bDrawn){const ci=mkCircle(bSvg,cx,cy,poolR,C.nu,C.nus,.7,.4);ci.dataset.cat="b_pool";
if(p>0&&p<1){const angle=p*2*Math.PI,x1=cx,y1=cy-poolR,x2=cx+poolR*Math.sin(angle),y2=cy-poolR*Math.cos(angle),la=angle>Math.PI?1:0;const pie=document.createElementNS(svgNS,"path");pie.setAttribute("d","M"+cx+" "+cy+" L"+x1+" "+y1+" A"+poolR+" "+poolR+" 0 "+la+" 1 "+x2+" "+y2+" Z");pie.setAttribute("fill",C.pu);pie.setAttribute("opacity",".45");pie.dataset.origOp=".45";pie.dataset.cat="b_pool";bSvg.appendChild(pie)}
}else if(isDrawn){const isP=drawnPosSet.has(i);const ci=mkCircle(bSvg,cx,cy,poolR,isP?C.pd:C.nd,isP?C.pds:C.nds,1.8,1);ci.dataset.cat="drawn";
}else{const ci=mkCircle(bSvg,cx,cy,poolR,C.nu,C.nus,.6,.18);ci.dataset.cat="undrawn";
if(p>0&&p<1){const angle=p*2*Math.PI,x1=cx,y1=cy-poolR,x2=cx+poolR*Math.sin(angle),y2=cy-poolR*Math.cos(angle),la=angle>Math.PI?1:0;const pie=document.createElementNS(svgNS,"path");pie.setAttribute("d","M"+cx+" "+cy+" L"+x1+" "+y1+" A"+poolR+" "+poolR+" 0 "+la+" 1 "+x2+" "+y2+" Z");pie.setAttribute("fill",C.pu);pie.setAttribute("opacity",".18");pie.dataset.origOp=".18";pie.dataset.cat="undrawn";bSvg.appendChild(pie)}}}
mkRect(bSvg,0,padY-4,24,areaH+8,0,"url(#fl)","none",0);mkRect(bSvg,516,padY-4,24,areaH+8,0,"url(#fr)","none",0);mkRect(bSvg,0,padY-8,540,20,0,"url(#ft)","none",0);mkRect(bSvg,0,padY+areaH-12,540,20,0,"url(#fb)","none",0);
attachHover(bSvg,cat=>bTip(cat,n,p),()=>resetOp(bSvg));renderPMF_B(p,n)}
function bTip(cat,n,p){
const k=ST.bK;
return({
b_pool:"<strong style='color:#7b2d8e'>Infinite population</strong><br>Probability <b>\u{1D45D} = "+p.toFixed(3)+"</b> per draw.",
drawn:"<strong style='color:"+C.pd+"'>Drawn Sample</strong><br><b>\u{1D458} = "+k+"</b> positive, <b>\u{1D45B} = "+n+"</b> samples.<br>This is the sample we observed.",
undrawn:"<strong style='color:#7b2d8e'>Infinite Pool</strong><br><span class='warn'>\u26A0 The source of uncertainty is the infinite pool.</span>"
})[cat]||"";
}
function renderPMF_B(p,n){const svg=R.pmfSvg;while(svg.firstChild)svg.removeChild(svg.firstChild);if(p<=0||p>=1||n===0)return;const theo=[];for(let kk=0;kk<=n;kk++)theo.push({k:kk,p:bPMF(kk,n,p)});drawPMFChart(svg,theo,ST.bHist,ST.bN,ST.bK,"#7b2d8e","#9b59b6",0,n)}
function doDrawB(){const N=SL.N.val(),K=SL.K.val(),n=SL.n.val();if(n===0)return;const p=N>0?K/N:0;ST.bK=sampleBinom(n,p);ST.bDrawn=true;ST.bHist[ST.bK]=(ST.bHist[ST.bK]||0)+1;ST.bN++;renderB()}
bDrawBtn.el.addEventListener("click",doDrawB);bResetBtn.el.addEventListener("click",()=>{resetB();renderB()});
bAutoBtn.el.addEventListener("click",()=>{if(ST.bAutoId){clearInterval(ST.bAutoId);ST.bAutoId=null;bAutoBtn.setText("\u23E9 Auto")}else{ST.bAutoId=setInterval(doDrawB,200);bAutoBtn.setText("\u23F8 Pause")}});
for(const s of[SL.N,SL.K,SL.n]){s.input.addEventListener("input",()=>{s.sync();clamp();resetH();resetB();renderH();renderB()})}
function drawPMFChart(svg,theo,hist,nDraws,obsK,lineClr,barClr,kMin,kMax){const pL=38,pR=10,pT=8,pB=26,cW=540-pL-pR,cH=170-pT-pB;const nBins=kMax-kMin+1;if(nBins<=0)return;const barW=Math.min(20,(cW*.85)/nBins),gap=nBins>1?(cW-barW*nBins)/(nBins+1):(cW-barW)/2;let maxP=0;for(const t of theo)if(t.p>maxP)maxP=t.p;let maxFreq=0;if(nDraws>0)for(let kk=kMin;kk<=kMax;kk++){const f=(hist[kk]||0)/nDraws;if(f>maxFreq)maxFreq=f}const yMax=Math.max(maxP,maxFreq)*1.1;if(yMax<=0)return;mkLine(svg,pL,pT+cH,540-pR,pT+cH,"#dde2e9",1);if(nDraws>0){for(let i=0;i<nBins;i++){const freq=(hist[kMin+i]||0)/nDraws,x=pL+gap+i*(barW+gap),h=(freq/yMax)*cH;const r=document.createElementNS(svgNS,"rect");r.setAttribute("x",x);r.setAttribute("y",pT+cH-h);r.setAttribute("width",barW);r.setAttribute("height",Math.max(0,h));r.setAttribute("rx",2);r.setAttribute("fill",barClr);r.setAttribute("opacity",.22);svg.appendChild(r)}}const pts=[];for(let i=0;i<nBins;i++)pts.push([pL+gap+i*(barW+gap)+barW/2,pT+cH-(theo[i].p/yMax)*cH]);if(pts.length>1){const ld=pts.map((pp,i)=>(i===0?"M":"L")+pp[0].toFixed(1)+","+pp[1].toFixed(1)).join(" ");const line=document.createElementNS(svgNS,"path");line.setAttribute("d",ld);line.setAttribute("fill","none");line.setAttribute("stroke",lineClr);line.setAttribute("stroke-width",2);line.setAttribute("stroke-linejoin","round");line.setAttribute("opacity",.8);svg.appendChild(line)}for(const[x,y]of pts)mkCircle(svg,x,y,2.5,lineClr,lineClr,0,.85);if(obsK!==null){const i=obsK-kMin;if(i>=0&&i<pts.length){const[x,y]=pts[i];mkCircle(svg,x,y,5.5,"none",lineClr,2.2,1);const lbl=document.createElementNS(svgNS,"text");lbl.setAttribute("x",x);lbl.setAttribute("y",y-9);lbl.setAttribute("text-anchor","middle");lbl.setAttribute("font-size","13");lbl.setAttribute("font-weight","700");lbl.setAttribute("fill",lineClr);lbl.setAttribute("font-family","var(--f-mono)");lbl.textContent=theo[i].p.toFixed(4);svg.appendChild(lbl)}}const step=nBins>35?Math.ceil(nBins/25):1;for(let i=0;i<nBins;i+=step)svgText(svg,pL+gap+i*(barW+gap)+barW/2,pT+cH+16,String(kMin+i),12,"#8e99a9");const yl=document.createElementNS(svgNS,"text");yl.setAttribute("x",14);yl.setAttribute("y",pT+cH/2);yl.setAttribute("text-anchor","middle");yl.setAttribute("transform","rotate(-90,14,"+(pT+cH/2)+")");yl.setAttribute("font-size","13");yl.setAttribute("fill","#8e99a9");yl.setAttribute("font-family","var(--f-mono)");yl.textContent="P(X=k)";svg.appendChild(yl);}
function mkCircle(par,cx,cy,r,fill,stroke,sw,op){const c=document.createElementNS(svgNS,"circle");c.setAttribute("cx",cx);c.setAttribute("cy",cy);c.setAttribute("r",r);c.setAttribute("fill",fill);c.setAttribute("stroke",stroke);c.setAttribute("stroke-width",sw);c.setAttribute("opacity",op);c.dataset.origOp=op;c.style.transition="opacity .12s";par.appendChild(c);return c}
function mkRect(par,x,y,w,h,rx,fill,stroke,sw){const r=document.createElementNS(svgNS,"rect");r.setAttribute("x",x);r.setAttribute("y",y);r.setAttribute("width",w);r.setAttribute("height",h);r.setAttribute("rx",rx);r.setAttribute("fill",fill);if(stroke!=="none"){r.setAttribute("stroke",stroke);r.setAttribute("stroke-width",sw)}par.appendChild(r);return r}
function mkLine(par,x1,y1,x2,y2,stroke,sw,dash){const l=document.createElementNS(svgNS,"line");l.setAttribute("x1",x1);l.setAttribute("y1",y1);l.setAttribute("x2",x2);l.setAttribute("y2",y2);l.setAttribute("stroke",stroke);l.setAttribute("stroke-width",sw);if(dash)l.setAttribute("stroke-dasharray",dash);par.appendChild(l);return l}
function svgText(par,x,y,text,size,fill){const t=document.createElementNS(svgNS,"text");t.setAttribute("x",x);t.setAttribute("y",y);t.setAttribute("text-anchor","middle");t.setAttribute("font-size",size||13);t.setAttribute("fill",fill||"#8e99a9");t.setAttribute("font-family","var(--f-mono)");t.setAttribute("font-weight","500");t.textContent=text;par.appendChild(t);return t}
function dotStyle(isP,isDrawn,hasDrawn){if(!hasDrawn)return isP?{fill:C.pu,stroke:C.pus,sw:1.2,op:1,cat:"pre_pos"}:{fill:C.nu,stroke:C.nus,sw:1.2,op:1,cat:"pre_neg"};if(isDrawn&&isP)return{fill:C.pd,stroke:C.pds,sw:1.8,op:1,cat:"drawn_pos"};if(isDrawn&&!isP)return{fill:C.nd,stroke:C.nds,sw:1.8,op:1,cat:"drawn_neg"};if(!isDrawn&&isP)return{fill:C.pu,stroke:C.pus,sw:1,op:.35,cat:"undrawn_pos"};return{fill:C.nu,stroke:C.nus,sw:.8,op:.2,cat:"undrawn_neg"}}
function attachHover(svg,tipFn,resetFn){
svg.onmousemove=(e)=>{
const target=e.target.closest("[data-cat]");
const cat=target?target.dataset.cat:null;
if(cat){
for(const el of svg.querySelectorAll("[data-cat]")){
if(!el.dataset.origOp)continue;
const orig=parseFloat(el.dataset.origOp);
if(cat==="drawn"||cat==="undrawn"){
if(el.dataset.cat===cat){
el.setAttribute("opacity", cat==="undrawn" ? Math.min(1, orig*3.2) : orig);
}else{
el.setAttribute("opacity", (cat==="undrawn"&&el.nodeName==="path"&&el.dataset.cat==="drawn") ? orig : orig*0.2);
}
}else{
if(el.dataset.cat===cat) el.setAttribute("opacity", Math.min(1, orig*3.2));
else el.setAttribute("opacity", orig*0.2);
}
}
showTip(e,tipFn(cat));
}else{resetFn();hideTip()}
};
svg.onmouseleave=()=>{resetFn();hideTip()}
}
function resetOp(svg){for(const el of svg.querySelectorAll("[data-cat]"))if(el.dataset.origOp)el.setAttribute("opacity",el.dataset.origOp)}
function svgPt(svg,e){const p=svg.createSVGPoint();p.x=e.clientX;p.y=e.clientY;return p.matrixTransform(svg.getScreenCTM().inverse())}
invalidation.then(()=>{
if(ST.hAutoId)clearInterval(ST.hAutoId);
if(ST.bAutoId)clearInterval(ST.bAutoId);
});
clamp();renderH();renderB();return root;
}3.2.1 Phân phối Nhị thức (Binomial distribution)
Đếm số lần thành công (\(k\)) trong một số lượng cố định các phép thử Bernoulli độc lập (\(n\)).
Xác suất để đạt được đúng \(k\) lần thành công là:
\[\mathbb{P}(X = k) = \binom{n}{k} p^k (1-p)^{n-k}\]
Với:
- \(n\): Tổng số lần thực hiện phép thử
- \(p\): Xác suất thành công của mỗi lần thử (không đổi)
\[\underbrace{\binom{n}{k}}_{\text{Số cách chọn}} \cdot \underbrace{p^k}_{k \text{ lần thành công}} \cdot \underbrace{(1-p)^{n-k}}_{n-k \text{ lần thất bại}}\]
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 = {};
SL.n = createSlider("n (trials)", 1, 100, 1, 20, "#1e293b", "dark");
SL.p = createSlider("p (success probability)", 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;
const mg = {t:16, r:20, b:48, l:56};
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
const yLabels=[];
for(let i=0;i<=4;i++){const t=el("text",{"text-anchor":"end",fill:"#94a3b8","font-size":"10","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);
const t=el("text",{"text-anchor":"middle",fill:"#64748b","font-size":"11","font-family":"'SF Mono',monospace",dy:"16"});t.style.display="none";svg.appendChild(t);
xTicks.push({ln,t});
}
const xLabel=el("text",{"text-anchor":"middle",fill:"#64748b","font-size":"12",y:String(H-4)});
xLabel.textContent="Number of successes (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"}));
const yLabel=el("text",{"text-anchor":"middle",fill:"#64748b","font-size":"12",x:"14",y:String(mg.t+ch/2),transform:`rotate(-90,14,${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"});
const tipText1=el("text",{fill:"#fff","font-size":"12","font-weight":"700","font-family":"'SF Mono',monospace"});
const tipText2=el("text",{fill:"#94a3b8","font-size":"11","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)*7.5+16;
const th=40;
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+16);tipText1.textContent=t1;
tipText2.setAttribute("x",tx+8);tipText2.setAttribute("y",ty+32);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;
})()3.2.1.1 Bảng Galton
Bảng Galton (Galton board) là một tấm bảng dựng đứng, được đóng các hàng đinh so le nhau theo hình tam giác. Ta thả những viên bi nhỏ từ đỉnh tháp. Khi một viên bi chạm vào một chiếc đinh, nó sẽ nảy sang trái hoặc sang phải. Sau khi đi hết các hàng đinh, viên bi sẽ rơi vào các ô chứa để hứng bi. Số lượng bi trong các ô chứa này tuân theo phân phối Nhị thức.
viewof galton_board = (() => {
const TAU = 6.283185307;
// ══════════════════════════════════════════════════════
// 1. 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:520px;margin:0 auto;
`;
const style = document.createElement("style");
style.textContent = `
.gt-slider{position:relative;height:8px;border-radius:4px;background:#e2e8f0;}
.gt-slider-fill{position:absolute;left:0;top:0;height:100%;border-radius:4px;transition:width 0.04s;}
.gt-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;
}
.gt-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;
}
.gt-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;
}
.gt-slider input[type=range]::-webkit-slider-runnable-track{height:8px;background:transparent;}
.gt-slider input[type=range]::-moz-range-track{height:8px;background:transparent;}
.gt-slider-blue input[type=range]::-webkit-slider-thumb{background:#3b82f6;}
.gt-slider-blue input[type=range]::-moz-range-thumb{background:#3b82f6;}
.gt-slider-amber input[type=range]::-webkit-slider-thumb{background:#d97706;}
.gt-slider-amber input[type=range]::-moz-range-thumb{background:#d97706;}
.gt-slider-dark input[type=range]::-webkit-slider-thumb{background:#1e293b;}
.gt-slider-dark input[type=range]::-moz-range-thumb{background:#1e293b;}
.gt-btn{
flex:1;padding:8px 0;border-radius:8px;border:1px solid #d1d5db;
background:#fff;color:#374151;font-size:13px;font-weight:600;
cursor:pointer;transition:background 0.1s;font-family:inherit;text-align:center;
}
.gt-btn:hover{background:#f3f4f6;}
.gt-btn-go{background:#3b82f6;color:#fff;border-color:#2563eb;}
.gt-btn-go:hover{background:#2563eb;color:#fff;}
.gt-btn-reset{background:#fef2f2;color:#dc2626;border-color:#fecaca;}
.gt-btn-reset:hover{background:#fee2e2;color:#dc2626;}
`;
wrapper.appendChild(style);
// ══════════════════════════════════════════════════════
// 2. SLIDER FACTORY
// ══════════════════════════════════════════════════════
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:120px;";
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:11px;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:17px;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(1) : val;
head.appendChild(lbl); head.appendChild(valSpan);
const track = document.createElement("div");
track.className = "gt-slider gt-slider-" + cls;
const fill = document.createElement("div");
fill.className = "gt-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(1) : String(v);
fill.style.width = ((v - min) / (max - min)) * 100 + "%";
}
};
}
const SL = {};
SL.rows = createSlider("Peg rows", 8, 20, 1, 10, "#1e293b", "dark");
SL.total = createSlider("Balls", 50, 800, 10, 200, "#3b82f6", "blue");
SL.speed = createSlider("Speed", 0.5, 3, 0.1, 2.0, "#d97706", "amber");
// Row 1: peg rows + balls (2 equal columns)
const r1 = document.createElement("div");
r1.style.cssText = "display:flex;gap:24px;width:100%;margin-bottom:10px;";
r1.appendChild(SL.rows.el); r1.appendChild(SL.total.el);
wrapper.appendChild(r1);
// Row 2: speed — matched to first column width via same flex layout + invisible spacer
const r2 = document.createElement("div");
r2.style.cssText = "display:flex;gap:24px;width:100%;margin-bottom:12px;";
r2.appendChild(SL.speed.el);
const spacer = document.createElement("div");
spacer.style.cssText = "flex:1;min-width:120px;";
r2.appendChild(spacer);
wrapper.appendChild(r2);
// Row 3: buttons
const r3 = document.createElement("div");
r3.style.cssText = "display:flex;gap:10px;width:100%;margin-bottom:14px;padding-bottom:14px;border-bottom:1px solid #e2e8f0;";
const btnGo = document.createElement("button");
btnGo.className = "gt-btn gt-btn-go"; btnGo.textContent = "▶ Start";
const btnStop = document.createElement("button");
btnStop.className = "gt-btn"; btnStop.textContent = "⏸ Pause";
const btnReset = document.createElement("button");
btnReset.className = "gt-btn gt-btn-reset"; btnReset.textContent = "↺ Reset";
r3.appendChild(btnGo); r3.appendChild(btnStop); r3.appendChild(btnReset);
wrapper.appendChild(r3);
// ══════════════════════════════════════════════════════
// 3. RESPONSIVE CANVAS (fits on screen)
// ══════════════════════════════════════════════════════
const LW = 380, LH = 600, ASPECT = LH / LW;
const canvasWrap = document.createElement("div");
// max-height: 62vh ensures the canvas + controls all fit on one screen
// width derived from height to maintain aspect ratio
canvasWrap.style.cssText = `
width:100%;
display:flex;justify-content:center;
`;
const canvas = document.createElement("canvas");
canvas.style.cssText = `
max-height:62vh;
width:auto;
height:auto;
border-radius:10px;
border:1px solid #e2e8f0;
display:block;
`;
canvasWrap.appendChild(canvas);
wrapper.appendChild(canvasWrap);
const ctx = canvas.getContext("2d", { alpha: false });
function resizeCanvas() {
// Determine available height: 62vh, then width from aspect
const maxH = window.innerHeight * 0.62;
const maxW = canvasWrap.getBoundingClientRect().width;
// Height-limited: derive width from maxH
let cssH = maxH;
let cssW = cssH / ASPECT;
// If that's wider than container, clamp to container
if (cssW > maxW) {
cssW = maxW;
cssH = cssW * ASPECT;
}
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.round(cssW * dpr);
canvas.height = Math.round(cssH * dpr);
canvas.style.width = cssW + "px";
canvas.style.height = cssH + "px";
ctx.setTransform((cssW * dpr) / LW, 0, 0, (cssW * dpr) / LW, 0, 0);
}
const ro = new ResizeObserver(resizeCanvas);
ro.observe(canvasWrap);
resizeCanvas();
// ══════════════════════════════════════════════════════
// 4. SIMULATION STATE
// ══════════════════════════════════════════════════════
let CFG = {}, pegGrid = [], S = {};
function initSim() {
const rows = SL.rows.val(), maxBalls = SL.total.val();
const pegSpacing = LW / (rows + 3);
const pegR = pegSpacing * 0.12;
const ballR = pegSpacing * 0.23;
const rowH = pegSpacing * 0.85;
const startY = 60;
const numBins = rows + 1;
const binW = pegSpacing;
const totalBinW = numBins * binW;
const binsX0 = (LW - totalBinW) / 2;
const wallsTopY = startY + rows * rowH + 15;
const floorY = LH - 10;
const stackStep = ballR * 0.6;
CFG = { rows, maxBalls, pegSpacing, pegR, ballR, rowH, startY,
numBins, binW, totalBinW, binsX0, wallsTopY, floorY, stackStep };
pegGrid = [];
for (let r = 0; r < rows; r++) {
const n = r+1, rw = (n-1)*pegSpacing, x0 = (LW-rw)/2;
const arr = new Float64Array(n*2);
const y = startY + r * rowH;
for (let p = 0; p < n; p++) { arr[p*2]=x0+p*pegSpacing; arr[p*2+1]=y; }
pegGrid.push(arr);
}
if (S.pool) S.pool.length = 0;
if (S.active) S.active.length = 0;
S = { pool:[], active:[], bins:new Int32Array(numBins),
spawned:0, running:false, done:false };
}
// ══════════════════════════════════════════════════════
// 5. BALL LOGIC (object pool)
// ══════════════════════════════════════════════════════
function allocBall() {
if (S.pool.length) return S.pool.pop();
return { pathX:new Float64Array(30), pathY:new Float64Array(30),
pathLen:0,seg:0,t:0,x:0,y:0,binIdx:0 };
}
function buildPath(b) {
const {rows,pegSpacing,pegR,ballR,rowH,startY,binW,binsX0,wallsTopY,floorY}=CFG;
let idx=0,pi=0;
b.pathX[pi]=LW/2; b.pathY[pi]=startY-20; pi++;
for(let r=0;r<rows;r++){
const n=r+1,rw=(n-1)*pegSpacing,x0=(LW-rw)/2;
const px=x0+idx*pegSpacing,py=startY+r*rowH;
const right=Math.random()>0.5;
b.pathX[pi]=px+(right?1:-1)*(pegR+ballR)*0.85;
b.pathY[pi]=py; pi++;
if(right)idx++;
}
const cx=binsX0+idx*binW+binW/2;
b.pathX[pi]=cx;b.pathY[pi]=wallsTopY;pi++;
b.pathX[pi]=cx;b.pathY[pi]=floorY-ballR;pi++;
b.pathLen=pi;b.seg=0;b.t=0;
b.x=b.pathX[0];b.y=b.pathY[0];b.binIdx=idx;
}
function spawn(){
if(S.spawned>=CFG.maxBalls)return;
const b=allocBall();buildPath(b);
S.active.push(b);S.spawned++;
}
function update(dt){
if(!S.running)return;
const speed=SL.speed.val();
if(S.spawned<CFG.maxBalls&&Math.random()<0.25*speed)spawn();
const mv=3.0*speed*dt;
let wi=0;
for(let i=0;i<S.active.length;i++){
const b=S.active[i];
b.t+=mv;
while(b.t>=1&&b.seg<b.pathLen-2){b.t-=1;b.seg++;}
if(b.t>=1&&b.seg>=b.pathLen-2){S.bins[b.binIdx]++;S.pool.push(b);continue;}
const s=b.seg,t=b.t,ease=t*t;
b.x=b.pathX[s]+(b.pathX[s+1]-b.pathX[s])*t;
b.y=b.pathY[s]+(b.pathY[s+1]-b.pathY[s])*ease;
S.active[wi++]=b;
}
S.active.length=wi;
if(S.spawned>=CFG.maxBalls&&S.active.length===0){S.running=false;S.done=true;}
}
// ══════════════════════════════════════════════════════
// 6. DRAW — FLAT
// ══════════════════════════════════════════════════════
function draw(){
const {pegR,ballR,numBins,binW,totalBinW,binsX0,wallsTopY,floorY,stackStep}=CFG;
ctx.fillStyle="#f8f9fa";
ctx.fillRect(0,0,LW,LH);
// Pegs
ctx.fillStyle="#94a3b8";
ctx.beginPath();
for(let r=0;r<pegGrid.length;r++){
const a=pegGrid[r];
for(let p=0;p<a.length;p+=2){
ctx.moveTo(a[p]+pegR,a[p+1]);
ctx.arc(a[p],a[p+1],pegR,0,TAU);
}
}
ctx.fill();
// Walls & floor
ctx.strokeStyle="#cbd5e1";
ctx.lineWidth=1.5;
ctx.beginPath();
ctx.moveTo(binsX0,floorY);
ctx.lineTo(binsX0+totalBinW,floorY);
for(let i=0;i<=numBins;i++){
const x=binsX0+i*binW;
ctx.moveTo(x,wallsTopY);ctx.lineTo(x,floorY);
}
ctx.stroke();
// Stacked balls
ctx.fillStyle="#3b82f6";
ctx.strokeStyle="#2563eb";
ctx.lineWidth=0.7;
ctx.beginPath();
for(let i=0;i<numBins;i++){
const count=S.bins[i];
if(!count)continue;
const cx=binsX0+i*binW+binW/2;
for(let j=0;j<count;j++){
const cy=floorY-ballR-j*stackStep;
if(cy<wallsTopY)break;
ctx.moveTo(cx+ballR,cy);
ctx.arc(cx,cy,ballR,0,TAU);
}
}
ctx.fill();
ctx.stroke();
// Active balls
ctx.fillStyle="#3b82f6";
ctx.beginPath();
for(let i=0;i<S.active.length;i++){
const b=S.active[i];
ctx.moveTo(b.x+ballR,b.y);
ctx.arc(b.x,b.y,ballR,0,TAU);
}
ctx.fill();
// Counter pill
ctx.font="bold 12px 'SF Mono',SFMono-Regular,Menlo,monospace";
const txt=S.spawned+" / "+CFG.maxBalls;
const tw=ctx.measureText(txt).width;
const bx=12,by=14,bh=20,bpw=tw+16;
ctx.fillStyle="rgba(255,255,255,0.8)";
ctx.beginPath();ctx.roundRect(bx,by,bpw,bh,10);ctx.fill();
ctx.strokeStyle="#e2e8f0";ctx.lineWidth=1;
ctx.beginPath();ctx.roundRect(bx,by,bpw,bh,10);ctx.stroke();
ctx.fillStyle="#334155";ctx.textAlign="left";
ctx.fillText(txt,bx+8,by+14);
if(S.done){
const d="\u2713 Complete";
const dw=ctx.measureText(d).width;
ctx.fillStyle="rgba(240,253,244,0.9)";
ctx.beginPath();ctx.roundRect(bx+bpw+6,by,dw+16,bh,10);ctx.fill();
ctx.strokeStyle="#bbf7d0";ctx.lineWidth=1;
ctx.beginPath();ctx.roundRect(bx+bpw+6,by,dw+16,bh,10);ctx.stroke();
ctx.fillStyle="#16a34a";
ctx.fillText(d,bx+bpw+14,by+14);
}
}
// ══════════════════════════════════════════════════════
// 7. LOOP
// ══════════════════════════════════════════════════════
let lastT=0,rafId=0;
function tick(now){
const dt=Math.min((now-lastT)/1000,0.06);
lastT=now;update(dt);draw();
rafId=requestAnimationFrame(tick);
}
// ══════════════════════════════════════════════════════
// 8. EVENTS
// ══════════════════════════════════════════════════════
function fullReset(){
cancelAnimationFrame(rafId);initSim();
SL.rows.sync();SL.speed.sync();SL.total.sync();
lastT=performance.now();rafId=requestAnimationFrame(tick);
}
function onStart(){if(S.done)fullReset();S.running=true;}
function onStop(){S.running=false;}
function onRows(){SL.rows.sync();if(!S.running)fullReset();}
function onTotal(){SL.total.sync();if(!S.running)fullReset();}
function onSpeed(){SL.speed.sync();}
btnGo.addEventListener("click",onStart);
btnStop.addEventListener("click",onStop);
btnReset.addEventListener("click",fullReset);
SL.rows.input.addEventListener("input",onRows);
SL.total.input.addEventListener("input",onTotal);
SL.speed.input.addEventListener("input",onSpeed);
// ══════════════════════════════════════════════════════
// 9. INIT & CLEANUP
// ══════════════════════════════════════════════════════
initSim();lastT=performance.now();rafId=requestAnimationFrame(tick);
invalidation.then(()=>{
cancelAnimationFrame(rafId);
ro.disconnect();
btnGo.removeEventListener("click",onStart);
btnStop.removeEventListener("click",onStop);
btnReset.removeEventListener("click",fullReset);
SL.rows.input.removeEventListener("input",onRows);
SL.total.input.removeEventListener("input",onTotal);
SL.speed.input.removeEventListener("input",onSpeed);
if(S.pool)S.pool.length=0;
if(S.active)S.active.length=0;
S={};CFG={};pegGrid=[];
});
wrapper.value={};
return wrapper;
})()Tại sao lại là phân phối Nhị thức?
- Mỗi lần viên bi chạm vào chiếc đinh, có hai trường hợp xảy ra: viên bi sẽ nảy sang trái hoặc sang phải, xác suất nảy sang mỗi bên là \(p = 0.5\), giống như tung đồng xu. Đây là một phép thử Bernoulli.
- Nếu bảng có 10 hàng đinh, viên bi sẽ thực hiện chuỗi Bernoulli có 10 phép thử liên tiếp.
- Các ô chứa tương ứng với số lần viên bi nảy về bên phải. Để vào ô giữa, viên bi phải nảy sang bên phải 5 lần (và bên trái 5 lần), để vào ô cuối cùng, viên bi phải nảy sang bên phải 10 lần (và bên trái 0 lần).
3.2.2 Phân phối Siêu bội (Hypergeometric distribution)
Nếu phân phối Nhị thức là việc tung đồng xu, thì Phân phối Siêu bội giống như việc chia bài.
Tình huống: Bạn có bộ bài 52 lá, trong đó có 4 lá Át. Bạn rút 10 lá và hỏi “Tôi rút được bao nhiêu lá Át?”.
Luật chơi:
- Số lần thử cố định (\(n\))
- Các lần thử phụ thuộc nhau
- Lấy mẫu không hoàn lại từ một quần thể hữu hạn (\(N\))
Khi bạn rút được một lá Át ra khỏi bộ bài và giữ nó lại, xác suất rút được lá Át tiếp theo sẽ thay đổi (vì trong bộ bài giờ đây thiếu mất 1 lá Át và tổng số lá cũng giảm đi 1). Đây là đặc trưng của việc lấy mẫu không hoàn lại.
Phân phối Siêu bội mô tả xác suất lấy được đúng \(k\) phần tử thành công trong \(n\) lần lấy mẫu từ một quần thể hữu hạn gồm \(N\) phần tử, mà không có sự thay thế (không hoàn lại).
\[\mathbb{P}(X=k) = \frac{\binom{K}{k} \binom{N-K}{n-k}}{\binom{N}{n}}\]
- \(N\): Tổng kích thước quần thể (Ví dụ: 52 lá bài)
- \(n\): Kích thước mẫu lấy ra (Ví dụ: Rút 5 lá trên tay)
- \(K\): Tổng số phần tử thành công có trong quần thể (Ví dụ: 4 lá Át)
- \(k\): Số lượng thành công mong muốn trong mẫu (Ví dụ: Muốn có đúng 2 lá Át)
- \(N-K\): Số lượng phần tử thất bại trong quần thể (Ví dụ: 48 lá bài còn lại)
- \(n-k\): Số lượng thất bại phải lấy trong mẫu (Ví dụ: 3 lá còn lại trong tay phải là lá thường).
\[\frac{ \overbrace{\binom{K}{k}}^{\text{Chọn } k \text{ thành công}} \cdot \overbrace{\binom{N-K}{n-k}}^{\text{Chọn } n-k \text{ thất bại}} }{ \underbrace{\binom{N}{n}}_{\text{Tổng số cách chọn ra } n \text{ mẫu}} }\]
viewof ace_sim_manual = (() => {
// --- 1. SETUP DOM & STYLES ---
const container = document.createElement("div");
container.style.cssText = `
width: 400px;
height: 440px;
margin: 0 auto;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
border: 1px solid transparent; /* Prevents margin collapse */
`;
// Inject UI HTML directly (Total Control)
container.innerHTML = `
<style>
.ace-controls-row {
width: 100%;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
margin-bottom: 5px;
}
.ace-btn {
padding: 6px 14px;
border-radius: 6px;
border: 1px solid #d1d5db;
background-color: white;
color: #374151;
font-size: 14px;
font-weight: 600;
cursor: pointer;
line-height: 1.2;
}
.ace-btn:hover { background-color: #f3f4f6; }
.ace-btn-primary {
background-color: #3b82f6;
color: white;
border-color: #2563eb;
}
.ace-btn-primary:hover { background-color: #2563eb; color: white; }
.ace-toggle {
display: flex; align-items: center; gap: 6px;
font-size: 14px; font-weight: 500; color: #4b5563;
cursor: pointer; user-select: none;
}
.ace-toggle input { width: 16px; height: 16px; accent-color: #10b981; cursor: pointer; }
canvas { display: block; } /* Removes ghost margin */
</style>
<div class="ace-controls-row">
<button id="btn-draw" class="ace-btn ace-btn-primary">Rút 10 lá</button>
<label class="ace-toggle">
<input type="checkbox" id="chk-auto">
Tự động chạy
</label>
<button id="btn-reset" class="ace-btn">Làm lại</button>
</div>
`;
// --- 2. CANVAS SETUP ---
const width = 400;
const height = 380;
const canvas = document.createElement("canvas");
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
const ctx = canvas.getContext("2d", { alpha: false });
ctx.scale(dpr, dpr);
container.appendChild(canvas);
// --- 3. STATE & CONSTANTS ---
const SUITS = ["♠", "♥", "♣", "♦"];
const RANKS = ["2","3","4","5","6","7","8","9","10","J","Q","K","A"];
const CARD_W = 30, CARD_H = 45;
// Layout Constants
const Y_HEADER = 45;
const Y_CARDS = 85;
const Y_CHART_BASE = 320;
const CHART_H = 60;
// Colors
const BG_COLOR="#fff", TEXT_COLOR="#333333", ACE_COLOR="#d35400", ACE_BG="#fffbe6";
const CARD_BG="#ffffff", CARD_BORDER="#cccccc", RED="#e74c3c", BLACK="#2c3e50";
const CHART_THEO="rgba(44, 62, 80, 0.1)", CHART_ACT="#009688", CHART_ACTIVE="#00796b";
const DECK_BACK="#c0392b";
function hypergeom(k, N, K, n) {
function nCr(n, r) {
if (r < 0 || r > n) return 0;
let res = 1;
for(let i=0; i<r; i++) res = res * (n - i) / (i + 1);
return res;
}
return (nCr(K, k) * nCr(N - K, n - k)) / nCr(N, n);
}
const THEORETICAL = [];
for(let k=0; k<=4; k++) THEORETICAL[k] = hypergeom(k, 52, 4, 10);
const S = {
counts: [0, 0, 0, 0, 0],
totalDeals: 0,
lastHand: [],
animation: 0,
cooldown: 0,
isAuto: false
};
// --- 4. LOGIC ---
function runDeal() {
let deck = [];
for(let s of SUITS) {
for(let r of RANKS) deck.push({ rank: r, suit: s, isAce: r === "A" });
}
for(let i=deck.length-1; i>0; i--) {
const j = Math.floor(Math.random() * (i+1));
[deck[i], deck[j]] = [deck[j], deck[i]];
}
const hand = deck.slice(0, 10);
const aceCount = hand.filter(c => c.isAce).length;
S.counts[aceCount]++;
S.totalDeals++;
// Animation targets
const startX = (width - (5 * (CARD_W + 8))) / 2 + 4;
S.lastHand = hand.map((c, i) => {
const row = Math.floor(i / 5);
const col = i % 5;
return {
...c,
x: 30, y: 30,
targetX: startX + col * (CARD_W + 8),
targetY: Y_CARDS + row * (CARD_H + 12),
t: 0
};
});
S.animation = 1;
S.cooldown = 0;
}
function reset() {
S.counts = [0,0,0,0,0];
S.totalDeals = 0;
S.lastHand = [];
S.animation = 0;
}
// --- 5. LOOP ---
function update() {
if (S.isAuto && S.animation === 0) {
S.cooldown++;
if (S.cooldown > 40) runDeal();
}
if (S.animation === 1) {
let done = true;
for (let c of S.lastHand) {
if (c.t < 1) {
c.t += 0.1;
if (c.t > 1) c.t = 1;
const ease = 1 - Math.pow(1 - c.t, 3);
c.x = 30 + (c.targetX - 30) * ease;
c.y = 30 + (c.targetY - 30) * ease;
done = false;
}
}
if (done) S.animation = 0;
}
}
function draw() {
ctx.fillStyle = BG_COLOR;
ctx.fillRect(0, 0, width, height);
ctx.save();
ctx.font = "bold 14px -apple-system, BlinkMacSystemFont, sans-serif";
ctx.fontVariantNumeric = "tabular-nums";
// Header
ctx.textAlign = "left"; ctx.fillStyle = TEXT_COLOR;
ctx.fillText(`Lần rút: ${S.totalDeals}`, 75, Y_HEADER);
if (S.isAuto) {
ctx.textAlign = "center"; ctx.fillStyle = CHART_ACT;
ctx.fillText("● Đang chạy...", width/2, Y_HEADER);
}
if (S.lastHand.length > 0) {
const aces = S.lastHand.filter(c=>c.isAce).length;
ctx.textAlign = "right"; ctx.fillStyle = ACE_COLOR;
ctx.fillText(`Kết quả: ${aces} lá Át`, width - 30, Y_HEADER);
}
ctx.restore();
// Deck
for(let i=0; i<3; i++) {
ctx.fillStyle = "white"; ctx.strokeStyle = "#ccc";
ctx.fillRect(30-i*2, 30-i*2, CARD_W, CARD_H);
ctx.strokeRect(30-i*2, 30-i*2, CARD_W, CARD_H);
}
ctx.fillStyle = DECK_BACK; ctx.fillRect(30, 30, CARD_W, CARD_H);
ctx.strokeStyle = "white"; ctx.lineWidth = 2; ctx.strokeRect(34, 34, CARD_W-8, CARD_H-8);
ctx.lineWidth = 1;
// Hand
for (let c of S.lastHand) {
ctx.fillStyle = c.isAce ? ACE_BG : CARD_BG;
ctx.fillRect(c.x, c.y, CARD_W, CARD_H);
ctx.strokeStyle = c.isAce ? ACE_COLOR : CARD_BORDER;
ctx.strokeRect(c.x, c.y, CARD_W, CARD_H);
const isRed = (c.suit === "♥" || c.suit === "♦");
ctx.fillStyle = c.isAce ? ACE_COLOR : (isRed ? RED : BLACK);
ctx.textAlign = "left"; ctx.textBaseline = "top";
ctx.font = "bold 14px sans-serif";
ctx.fillText(c.rank, c.x + 3, c.y + 4);
ctx.font = "16px sans-serif";
ctx.fillText(c.suit, c.x + 3, c.y + 20);
}
// Chart
const chartX = 50, barW = 40, gap = 25;
ctx.beginPath(); ctx.strokeStyle = "#bdc3c7"; ctx.lineWidth = 1;
ctx.moveTo(chartX - 10, Y_CHART_BASE); ctx.lineTo(width - 20, Y_CHART_BASE); ctx.stroke();
ctx.save();
ctx.fillStyle = TEXT_COLOR; ctx.font = "12px sans-serif"; ctx.textAlign = "center";
ctx.fillText("Số lá Át rút được (k)", width/2 + 15, Y_CHART_BASE + 35);
ctx.translate(20, Y_CHART_BASE - CHART_H/2);
ctx.rotate(-Math.PI/2);
ctx.fillStyle = "#7f8c8d";
ctx.fillText("Tần suất", 0, 0);
ctx.restore();
const activeAces = S.lastHand.filter(c=>c.isAce).length;
for (let k = 0; k <= 4; k++) {
const x = chartX + k * (barW + gap);
const Y_SCALE = 0.5;
const theoH = (THEORETICAL[k] / Y_SCALE) * CHART_H;
ctx.fillStyle = CHART_THEO;
ctx.fillRect(x, Y_CHART_BASE - theoH, barW, theoH);
let count = S.counts[k];
let pct = S.totalDeals > 0 ? count / S.totalDeals : 0;
let barH = (pct / Y_SCALE) * CHART_H;
if (barH > CHART_H + 10) barH = CHART_H + 10;
const isActive = (S.animation === 0 && S.totalDeals > 0 && k === activeAces);
ctx.fillStyle = isActive ? CHART_ACTIVE : CHART_ACT;
ctx.fillRect(x, Y_CHART_BASE - barH, barW, barH);
ctx.save();
ctx.fontVariantNumeric = "tabular-nums";
ctx.fillStyle = TEXT_COLOR; ctx.font = "12px sans-serif"; ctx.textAlign = "center";
ctx.fillText(k, x + barW/2, Y_CHART_BASE + 15);
ctx.restore();
}
}
// --- 6. BINDINGS ---
const btnDraw = container.querySelector("#btn-draw");
const btnReset = container.querySelector("#btn-reset");
const chkAuto = container.querySelector("#chk-auto");
btnDraw.onclick = () => runDeal();
btnReset.onclick = () => reset();
chkAuto.onchange = (e) => { S.isAuto = e.target.checked; };
let frameId;
function tick() {
update();
draw();
frameId = requestAnimationFrame(tick);
}
tick();
// Clean up
if (this && this.invalidation) this.invalidation.then(() => cancelAnimationFrame(frameId));
return container;
})();3.3 Đếm số lần thử
Chúng ta quyết định trước phần thưởng mình muốn đạt được (\(k\) thành công), và đếm xem phải tốn bao nhiêu công sức (\(n\) lần thử) để đạt được nó. Nhánh này mô hình hóa sự “kiên trì” hoặc “sức chịu đựng”.
3.3.1 Phân phối hình học (geometric distribution)
Tình huống: Một gia đình phong kiến trọng nam khinh nữ muốn có một đứa con trai, họ cứ sinh con cho đến khi có con trai thì thôi
Câu hỏi: Họ phải sinh bao nhiêu con cho đến khi có con trai?
Đặc tính: Không có bộ nhớ (Memoryless). Dù đã sinh 10 con gái liên tiếp, xác suất đứa tiếp theo là trai vẫn y như lúc bắt đầu.
3.3.2 Phân phối Nhị thức âm (negative binomial distribution)
Tình huống: Gia đình phong kiến đã sinh được con trai nhưng vẫn muốn có con trai nữa, thầy bói nói nhà phải có 3 đứa con trai mới giàu được
Câu hỏi: Họ sẽ sinh bao nhiêu con trước khi có được đứa con trai thứ 3?
Về lý thuyết, đây là tổng của \(k\) biến thuộc phân phối hình học.
Trong sinh học thực tế (ví dụ: đếm số ký sinh trùng, ấu trùng muỗi), phương sai thường lớn hơn trung bình rất nhiều (do hiện tượng tụ đám/clumping). Phân phối Nhị thức Âm khớp với kiểu dữ liệu này tốt hơn nhiều so với Poisson hay Nhị thức.
3.4 Đếm sự kiện khi biết tốc độ trung bình
3.4.1 Phân phối Poisson
Khác với những phân phối kể trên, phân phối Poisson không dựa trên chuỗi Bernoulli, mà dựa trên quá trình Poisson (Poisson process).
Quá trình Poisson là 1 cơ chế xảy ra biến cố với các điều kiện:
- Các biến cố xảy ra độc lập nhau (independent): biến cố này xảy ra không ảnh hưởng đến xác suất xảy ra của biến cố tiếp theo
- Các biến cố xảy ra với 1 tốc độ trung bình không đổi: Số lượng sự kiện trung bình trên một đơn vị thời gian là hằng số (ví dụ: trung bình có 5 chiếc xe chạy trên con đường này mỗi giờ)
- Các biến cố không xảy ra cùng 1 lúc
Phân phối Poisson dùng để đếm số lượng biến cố ngẫu nhiên xảy ra trong 1 khoảng thời gian cố định (với điều kiện \(\lambda\) không thay đổi trong khoảng thời gian này) của quá trình Poisson.
\[P(X=k) = \frac{e^{-\lambda} \lambda^k}{k!}\]
Trong đó:
- \(\lambda\): Tốc độ trung bình (kỳ vọng) số sự kiện trong khoảng thời gian đó.
- \(k\): Số sự kiện muốn tính xác suất.
3.4.2 Ví dụ
Tại một phòng cấp cứu, trung bình cứ 1 giờ lại có 3 bệnh nhân đến khám (\(\lambda = 3\)). Xác suất để trong 1 giờ tới có chính xác 5 ca đến khám là bao nhiêu?
Áp dụng công thức với \(\lambda = 3\) và \(k = 5\):
\[P(X=5) = \frac{e^{-3} \cdot 3^5}{5!} = \frac{0.0498 \times 243}{120} \approx 0.1008 \quad (\approx 10\%)\]
Poisson là giới hạn của Nhị thức khi \(n \to \infty\) và xác suất thành công \(p \to 0\), sao cho \(np = \lambda\)
Hãy tưởng tượng bạn đang ngồi đợi tin nhắn điện thoại trong 1 giờ. Dựa vào kinh nghiệm, trung bình bạn nhận được \(\lambda = 5\) tin nhắn/giờ. Xác suất trong 1 giờ tới bạn chỉ nhận được 2 tin nhắn là bao nhiêu?
- Bạn chia 1 giờ thành 60 phút. Mỗi phút là một phép thử Bernoulli (có tin nhắn/không tin nhắn). Bạn có \(n = 60\) phép thử liên tiếp.
- Bạn chia nhỏ thời gian ra nữa. Lúc này số phép thử \(n\) trở nên vô cùng lớn, và xác suất có tin nhắn trong một khoảnh khắc siêu nhỏ (\(p\)) trở nên vô cùng nhỏ. Càng chia nhỏ, bạn sẽ thấy phân phối nhị thức càng gần với Poisson.
viewof poisson_visual_final = (() => {
// --- 1. CONFIGURATION ---
const lambda = 5;
// Input Slider (Width 400px to match binning bar)
const form = Inputs.range([10, 360], {
value: 10,
step: 5,
label: "Chia 1 giờ thành n khoảng (n)"
});
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;
margin-top: 20px;
`;
// --- 2. TIME BIN VISUALIZATION (Canvas) ---
const timeCanvas = document.createElement("canvas");
const timeW = 400; // REDUCED WIDTH
const timeH = 50;
const dpr = window.devicePixelRatio || 1;
timeCanvas.width = timeW * dpr;
timeCanvas.height = timeH * dpr;
timeCanvas.style.width = `${timeW}px`;
timeCanvas.style.height = `${timeH}px`;
timeCanvas.style.marginTop = "10px";
timeCanvas.style.marginBottom = "5px";
const tCtx = timeCanvas.getContext("2d");
tCtx.scale(dpr, dpr);
// --- 3. STATS DISPLAY ---
const statsDiv = document.createElement("div");
statsDiv.style.cssText = `
margin-bottom: 5px;
font-size: 16px;
color: #333;
font-weight: 500;
`;
// --- 4. APPEND ELEMENTS (Order: Input -> Binning -> Stats -> Plot) ---
wrapper.appendChild(form);
wrapper.appendChild(timeCanvas);
wrapper.appendChild(statsDiv);
const plotContainer = document.createElement("div");
wrapper.appendChild(plotContainer);
// --- 5. MATH HELPERS ---
function getBinomialData(n, p) {
const data = [];
let prob = Math.pow(1 - p, n);
for (let k = 0; k <= 16; k++) {
data.push({k, type: "Binomial", prob});
if (k < n) {
prob = prob * ((n - k) / (k + 1)) * (p / (1 - p));
} else {
prob = 0;
}
}
return data;
}
const poissonData = [];
{
let prob = Math.exp(-lambda);
for (let k = 0; k <= 16; k++) {
poissonData.push({k, type: "Poisson", prob});
prob = prob * lambda / (k + 1);
}
}
// --- 6. RENDER LOOPS ---
function drawTimeBins(n) {
tCtx.clearRect(0, 0, timeW, timeH);
const barX = 0; // Start at 0 since canvas is now exact width
const barY = 15;
const barW = timeW;
const barH = 20;
// Background
tCtx.fillStyle = "#f3f4f6";
tCtx.fillRect(barX, barY, barW, barH);
tCtx.strokeStyle = "#9ca3af";
tCtx.lineWidth = 1;
tCtx.strokeRect(barX, barY, barW, barH);
// Draw Dividers
tCtx.beginPath();
tCtx.strokeStyle = n > 150 ? "rgba(107, 114, 128, 0.5)" : "#6b7280";
tCtx.lineWidth = n > 200 ? 0.5 : 1;
const binSize = barW / n;
// Draw internal lines
for (let i = 1; i < n; i++) {
const x = Math.floor(barX + i * binSize) + 0.5;
tCtx.moveTo(x, barY);
tCtx.lineTo(x, barY + barH);
}
tCtx.stroke();
// Labels
tCtx.fillStyle = "#374151";
tCtx.font = "bold 12px sans-serif";
tCtx.textAlign = "left";
tCtx.fillText("0", barX, barY + barH + 14);
tCtx.textAlign = "right";
tCtx.fillText("1h", barX + barW, barY + barH + 14);
// Center Label (Changed Text)
tCtx.textAlign = "center";
tCtx.fillStyle = "#3b82f6";
tCtx.fillText(`${n} phép thử liên tiếp`, timeW / 2, barY - 5);
}
function update() {
const n = form.value;
const p = lambda / n;
// Update Stats
statsDiv.innerHTML = `n = ${n} | p = ${p.toFixed(4)} | np = ${(n*p).toFixed(1)}`;
// Update Top Canvas
drawTimeBins(n);
// Update Plot
const binomData = getBinomialData(n, p);
const combinedData = poissonData.concat(binomData);
const xDomain = Array.from({length: 17}, (_, i) => i);
const chart = Plot.plot({
width: 600,
height: 320,
marginTop: 20,
x: {
type: "band",
domain: xDomain,
label: "Số tin nhắn (k)",
padding: 0.1
},
y: { label: "Xác suất", domain: [0, 0.2] },
marks: [
// Binomial Bars
Plot.barY(combinedData.filter(d => d.type === "Binomial"), {
x: "k",
y: "prob",
fill: "#3b82f6",
fillOpacity: 0.6,
title: d => `Binomial: ${d.prob.toFixed(4)}`
}),
// Poisson Line
Plot.line(combinedData.filter(d => d.type === "Poisson"), {
x: "k",
y: "prob",
stroke: "#be185d",
strokeWidth: 3,
curve: "monotone-x"
}),
// Poisson Dots
Plot.dot(combinedData.filter(d => d.type === "Poisson"), {
x: "k",
y: "prob",
fill: "#be185d",
r: 4
}),
Plot.ruleY([0])
]
});
plotContainer.innerHTML = "";
plotContainer.appendChild(chart);
}
form.addEventListener("input", update);
update();
return wrapper;
})();3.5 Ứng dụng
Dữ liệu dạng đếm (ví dụ: số ca bệnh trong 1 tuần):
- Poisson
- Negative binomial
Dữ liệu là tỉ lệ:
Các phân phối phức tạp có thể được tạo ra bằng cách cộng các phân phối đơn giản:
| Nếu cộng | Sẽ có | Điều kiện |
|---|---|---|
| Nhiều Bernoulli | Nhị thức | Cùng \(p\). |
| Nhiều Nhị thức | Nhị thức | \(X+Y \sim \text{Bin}(n+m, p)\) |
| Nhiều Hình học | Nhị thức âm | Chờ \(k\) thành công thực chất là chờ 1 thành công, lặp lại \(k\) lần |
| Nhiều Nhị thức âm | Nhị thức âm | \(X+Y \sim \text{NB}(r_1+r_2, p)\) |
| Nhiều Poisson | Poisson | \(X+Y \sim \text{Pois}(\lambda_1 + \lambda_2)\) |