1 Xác suất
- Hiểu các khái niệm và có thể chuyển một phép thử ngẫu nhiên từ ngôn ngữ nói sang biểu diễn bằng tập hợp toán học
- Hiểu khái niệm và cách tính xác suất có điều kiện
- Nắm rõ mối quan hệ giữa các biến cố: xung khắc, phụ thuộc, độc lập
- Biết cách áp dụng các phép tính xác suất trong các vấn đề thực tế
- Hiểu chính xác khái niệm biến ngẫu nhiên
1.1 Nguồn gốc
Lý thuyết xác suất ra đời để giải quyết một vấn đề trong cờ bạc. Vấn đề cụ thể khai sinh ra lĩnh vực này được gọi là “Bài toán Chia điểm” (Problem of Points).
Năm 1654, nhà văn kiêm tay cờ bạc Antoine Gombaud viết thư cho nhà toán học Blaise Pascal để hỏi cách giải quyết một ván bài dang dở:
- Hai người A và B chơi một trò hoàn toàn may rủi (ví dụ tung đồng xu, kéo-búa-bao), ai thắng 3 ván trước sẽ là người chiến thắng cuối cùng
- Mỗi người đặt 50$, tổng là 100$, ai thắng sẽ được toàn bộ tiền thưởng 100$
- Trò chơi phải dừng đột ngột khi A đang dẫn trước 2-1
Câu hỏi: Làm sao chia 100$ cho công bằng?
Tại sao đây là một bài toán khó?
Vào thời đó, con người chưa biết cách xử lý “tương lai” bằng toán học. Có người đề nghị: “A được 2 điểm, B được 1 điểm. Vậy chia 100$ làm 3, A được 67$, B được 33$.” Cách này không công bằng vì chỉ nhìn vào quá khứ mà phớt lờ lợi thế của A: A chỉ cần thắng thêm đúng 1 ván là xong, trong khi B phải thắng liên tiếp 2 ván.
Pascal thấy bài toán này rất thú vị và gửi nó cho nhà toán học Pierre de Fermat. Họ giải quyết vấn đề bằng cách thay đổi góc nhìn. Thay vì nhìn vào quá khứ (những gì đã xảy ra trong ván bài), họ nhìn vào tương lai (các khả năng sẽ xảy ra của trò chơi).
Lời giải:
Fermat tưởng tượng xem trò chơi có thể diễn ra như thế nào nếu tiếp tục chơi cho đến cùng. Trò chơi sẽ kết thúc tối đa trong 2 ván nữa (vì A thắng 1 ván là xong, hoặc B thắng 2 ván là xong). Để cho công bằng, ta cho A và B chơi thêm 2 ván.
Có 4 kịch bản của 2 ván này:
| Ván 1 | Ván 2 | Chung cuộc | Tỉ số A:B |
|---|---|---|---|
| A thắng | A thắng | A thắng | 4-1 |
| B thắng | A thắng | 3-2 | |
| B thắng | A thắng | A thắng | 3-2 |
| B thắng | B thắng | 2-3 |
Kết quả:
Nếu tiếp tục chơi, chỉ có 4 kịch bản có thể xảy ra, trong đó A thắng 3 lần, B thắng 1 lần. Vậy khả năng A thắng là 3/4 trường hợp, và B thắng 1/4 trường hợp. Vậy A nên được chia 3/4 của 100$ là 75$, và B được chia 25$.
Nhu cầu “xử lý tương lai” là rất lớn. Khi đánh bài, nên bỏ bài, theo, hay cược hết (all-in) luôn? Nhà cái cần tính toán luật chơi sao cho họ luôn có lợi thế. Nếu mua mã cổ phiếu này thì sau 1 tháng, 3 tháng, 6 tháng có lên giá không? Vì tương lai chưa xảy ra nên không có câu trả lời chắc chắn, nhưng không có nghĩa là ta phải nhắm mắt làm liều. Xác suất ra đời để giải quyết vấn đề này, bằng cách gán một con số để phản ánh mức độ chắc chắn.
Các nhà toán học và thống kê hiểu rõ xác suất. Họ biết các trò chơi ở sòng bài đều được thiết kế để có lợi cho nhà cái, vì vậy họ thường không chơi cờ bạc. Trừ khi họ tìm ra cách để tận dụng lỗ hổng của trò chơi, như một số trường hợp đặc biệt saư:
GS. Edward Thorp (MIT) nghĩ ra phương pháp đếm bài để thắng trò xì dách liên tục. Năm 1961, ông từng thắng 11,000 USD (115,000 USD theo giá trị hiện nay) trong một cuối tuần. Nhưng rồi ông bị các sòng bài cấm chơi, ông chuyển sang đầu tư chứng khoán và hiện có tài sản 800 triệu USD.
GS. Richard Jarecki (ĐH Heidelberg) tìm ra phương pháp để thắng trò roulette và đã kiếm được hơn 8 triệu USD. Ông cũng bị các sòng bài cấm chơi và họ phải thay thế toàn bộ thiết bị của trò chơi này.
TS. Joan Ginther (PhD ở ĐH Stanford) đã thắng xổ số 4 lần từ năm 1993-2010 với số tiền lên đến 20 triệu USD. Xác suất để xảy ra điều này là \(\frac{1}{18 \times 10^{24}}\) nên người ta tin rằng bà đã thu thập dữ liệu và tìm ra thuật toán để chiến thắng.
1.2 Định nghĩa
Trong “Bài toán Chia điểm” trên, khả năng thắng cuộc chính là xác suất.
Xác suất (Probability): là một con số nằm trong khoảng từ 0 đến 1, dùng để đo lường khả năng xảy ra của một sự kiện.
- 0 = chắc chắn không xảy ra
- 1 = chắc chắn sẽ xảy ra
Có 2 cách tiếp cận xác suất:
- Tần suất (frequentist):
\[\text{Xác suất} = \frac{\text{Số lần sự kiện xảy ra}}{\text{Tổng số lần quan sát}}\]
- Niềm tin (degree of belief, Bayesian): mức độ tin tưởng của người đánh giá về khả năng xảy ra của sự kiện.
1.3 Các khái niệm cơ bản
1.3.1 Phép thử ngẫu nhiên (random experiment)
Là một thử nghiệm mà chúng ta không biết trước kết quả cho đến khi nó thực sự diễn ra.
Ví dụ: tung đồng xu (không biết sẽ ra mặt sấp hay mặt ngửa), làm xét nghiệm cho một người (không biết là âm tính hay dương tính)
1.3.2 Không gian mẫu (sample space)
Là tập hợp chứa tất cả các kết quả có thể xảy ra của một phép thử ngẫu nhiên, mỗi kết quả liệt kê đúng một lần duy nhất. Không gian mẫu thường được kí hiệu là \(\Omega\).
Ví dụ: không gian mẫu của tung đồng xu là \(\Omega = \{ \text{sấp}, \text{ngửa} \}\)
Không gian mẫu là 1 tập hợp, nên có thể sử dụng các phép toán của tập hợp cho không gian mẫu (\(\cup\) hay \(\cap\)).
Có hai loại không gian mẫu:
Rời rạc (discrete): Là khi có “khoảng trống” giữa các giá trị. Ví dụ: 1, 2, 3…
Liên tục (continuous): Là khi không có khoảng trống giữa các giá trị. Ví dụ: \([0,1]\)
viewof radialFlood = {
// ═══════════════════ CONFIG ═══════════════════
const N=100, CX=0.5, CY=0.4;
const C={sel:"#1565C0",selLight:"#90CAF9",unsel:"#CFD8DC",
flood:"#1565C0",txt:"#212121",sub:"#9E9E9E",border:"#E0E0E0"};
const mono="'SF Mono',SFMono-Regular,Menlo,monospace";
// ═══════════════════ DATA ═══════════════════
const rng=d3.randomLcg(42);
const people=Array.from({length:N},(_,i)=>{
const x=rng(), y=rng();
const dx=x-CX, dy=y-CY;
return {id:i,x,y,dist:dx*dx+dy*dy};
}).sort((a,b)=>a.dist-b.dist);
// ═══════════════════ WRAPPER ═══════════════════
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:860px;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&display=swap';
document.head.appendChild(lk);
}
outer.appendChild(injectStyle());
// ═══════════════════ SLIDER ═══════════════════
const SL=createSlider("Xác suất",0,1,0.01,0.30,C.sel,"blue");
const slRow=document.createElement("div");
slRow.style.cssText="width:100%;max-width:340px;";
slRow.appendChild(SL.el);
outer.appendChild(slRow);
// ═══════════════════ SVG ═══════════════════
const W=820,H=380,pad=30;
const svg=d3.create("svg").attr("viewBox",`0 0 ${W} ${H}`)
.style("width","100%").style("max-width",W+"px")
.style("border-radius","14px").style("background","#FAFAFA")
.style("border",`1.5px solid ${C.border}`);
const xS=d3.scaleLinear().domain([0,1]).range([pad,W-pad]);
const yS=d3.scaleLinear().domain([0,1]).range([pad,H-pad]);
// Clip to box so flood circle doesn't overflow
const clipId="flood-clip-"+Math.random().toString(36).slice(2,8);
svg.append("defs").append("clipPath").attr("id",clipId)
.append("rect").attr("x",0).attr("y",0).attr("width",W).attr("height",H)
.attr("rx",14);
// Radial gradient for smooth continuous feel
const gradId="flood-grad-"+Math.random().toString(36).slice(2,8);
const grad=svg.select("defs").append("radialGradient").attr("id",gradId);
grad.append("stop").attr("offset","0%").attr("stop-color",C.flood).attr("stop-opacity",0.32);
grad.append("stop").attr("offset","55%").attr("stop-color",C.flood).attr("stop-opacity",0.18);
grad.append("stop").attr("offset","85%").attr("stop-color",C.flood).attr("stop-opacity",0.08);
grad.append("stop").attr("offset","100%").attr("stop-color",C.flood).attr("stop-opacity",0);
// ── CONTINUOUS LAYER: expanding radial shade ──
const cxPx=xS(CX), cyPx=yS(CY);
const corners=[[0,0],[W,0],[0,H],[W,H]];
const maxR=Math.max(...corners.map(([x,y])=>Math.sqrt((x-cxPx)**2+(y-cyPx)**2)));
const floodCircle=svg.append("circle")
.attr("cx",cxPx).attr("cy",cyPx).attr("r",0)
.attr("fill",`url(#${gradId})`).attr("clip-path",`url(#${clipId})`);
// ── DISCRETE LAYER: countable dots ──
const dots=svg.append("g").selectAll("circle")
.data(people).join("circle")
.attr("cx",d=>xS(d.x)).attr("cy",d=>yS(d.y))
.attr("stroke","#fff").attr("stroke-width",1.5);
// ── LABELS ──
svg.append("text").attr("x",W-22).attr("y",38)
.attr("text-anchor","end")
.attr("font-size",28).attr("font-weight",800).attr("fill","#BDBDBD")
.text("Ω");
const lblDiscrete=svg.append("text").attr("x",22).attr("y",H-18)
.attr("font-size",14).attr("font-weight",700)
.attr("fill",C.txt).attr("font-family",mono);
const lblContinuous=svg.append("text").attr("x",W-22).attr("y",H-18)
.attr("text-anchor","end").attr("font-size",14).attr("font-weight",700)
.attr("fill",C.flood).attr("font-family",mono);
// ═══════════════════ UPDATE ═══════════════════
function update(){
const p=SL.val();
const cutoff=Math.round(p*N);
const selectedSet=new Set(people.slice(0,cutoff).map(d=>d.id));
// Discrete: highlight selected dots
dots
.attr("fill",d=>selectedSet.has(d.id)?C.sel:C.unsel)
.attr("r",d=>selectedSet.has(d.id)?5.5:4)
.attr("opacity",d=>selectedSet.has(d.id)?0.9:0.45);
// Continuous: expand shade — area proportional to p
const r=maxR*Math.sqrt(p);
floodCircle.attr("r",r);
// Labels
lblDiscrete.text(`Rời rạc: ${cutoff}/${N} điểm`);
lblContinuous.text(`Liên tục: ${(p*100).toFixed(1)}%`);
}
SL.input.addEventListener("input",()=>{SL.sync();update();});
update();
outer.appendChild(svg.node());
return outer;
}1.3.3 Điểm mẫu (sample point)
Là một phần tử của không gian mẫu.
Ví dụ: không gian mẫu của tung đồng xu là \(\Omega = \{ \text{sấp}, \text{ngửa} \}\) thì \(\{ \text{sấp} \}\) hoặc \(\{ \text{ngửa} \}\) là một điểm mẫu.
1.3.4 Kết quả (outcome)
Là điểm mẫu quan sát được, khi chúng ta cho thực hiện phép thử ngẫu nhiên.
1.3.5 Biến cố/Sự kiện (event)
Là một tập hợp con của không gian mẫu. Bất kỳ tập hợp nào chứa các kết quả đều tạo thành một biến cố.
Ví dụ: Tung một đồng xu hai lần. Không gian mẫu: \(\Omega = \{ SS, SN, NS, NN \}\).
Gọi biến cố \(A\) là “có đúng một mặt ngửa”, \(A = \{ SN, NS \}\).
\(A\) là một tập con của \(\Omega\) (\(A \subset \Omega\)).
Biến cố \(A\) được gọi là xảy ra (occurs) nếu chúng ta quan sát được một kết quả là phần tử của tập hợp \(A\).
1.3.6 Ví dụ
Phép thử ngẫu nhiên: “Tung một đồng xu hai lần”
viewof sample_space = (() => {
// ══════════════════════════════════════════════════════
// 1. DATA
// ══════════════════════════════════════════════════════
const pts = [
{ id: 0, c1: "N", c2: "N", label: "NN", heads: 2 },
{ id: 1, c1: "N", c2: "S", label: "NS", heads: 1 },
{ id: 2, c1: "S", c2: "N", label: "SN", heads: 1 },
{ id: 3, c1: "S", c2: "S", label: "SS", heads: 0 },
];
const events = [
{ name: "Đúng 1 mặt Ngửa", color: "#7c3aed", test: p => p.heads === 1 },
{ name: "Ít nhất 1 mặt Ngửa", color: "#3b82f6", test: p => p.heads >= 1 },
{ name: "Hai mặt giống nhau", color: "#d97706", test: p => p.c1 === p.c2 },
{ name: "Không có mặt Ngửa", color: "#dc2626", test: p => p.heads === 0 },
];
// ══════════════════════════════════════════════════════
// 2. WRAPPER
// ══════════════════════════════════════════════════════
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:700px;margin:0 auto;`;
wrapper.appendChild(injectStyle());
const style = document.createElement("style");
style.textContent = `
.ev-card{padding:10px 14px;border-radius:10px;border:2px solid #e2e8f0;
background:#fff;cursor:pointer;transition:all 0.15s;user-select:none;text-align:center;flex:1;min-width:100px;}
.ev-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,0.08);}
.ev-card.active{border-width:3px;}
.ev-card-name{font-size:13px;font-weight:700;margin-bottom:2px;}
.ev-card-prob{font-size:11px;color:#64748b;font-family:"SF Mono",monospace;}
@keyframes coinFlip {
0% { transform: rotateX(0deg) translateY(0); }
25% { transform: rotateX(360deg) translateY(-40px); }
50% { transform: rotateX(720deg) translateY(-60px); }
75% { transform: rotateX(1080deg) translateY(-30px); }
100% { transform: rotateX(1440deg) translateY(0); }
}
.coin-anim { animation: coinFlip 0.8s ease-out; }
.coin-container { perspective: 300px; display:inline-block; }
`;
wrapper.appendChild(style);
// ══════════════════════════════════════════════════════
// 3. SVG DIAGRAM
// ══════════════════════════════════════════════════════
const NS = "http://www.w3.org/2000/svg";
const DW = 660, DH = 340;
function mkEl(tag, a) { const e = document.createElementNS(NS, tag); if (a) for (const [k,v] of Object.entries(a)) e.setAttribute(k,v); return e; }
const svg = document.createElementNS(NS, "svg");
svg.setAttribute("viewBox", `0 0 ${DW} ${DH}`);
svg.style.cssText = `width:100%;max-width:${DW}px;border-radius:12px;border:1px solid #e2e8f0;margin-bottom:10px;cursor:default;overflow:visible;`;
// Background (whole SVG = omega hover zone)
const bg = mkEl("rect", { x: "0", y: "0", width: String(DW), height: String(DH), fill: "#fafbfc", rx: "12" });
svg.appendChild(bg);
// Omega box
const omX = 30, omY = 30, omW = DW - 60, omH = DH - 60;
const omRect = mkEl("rect", { x: String(omX), y: String(omY), width: String(omW), height: String(omH), rx: "16", fill: "#f8fafc", stroke: "#1e293b", "stroke-width": "2.5" });
svg.appendChild(omRect);
// Omega label (hoverable)
const omLabelG = mkEl("g"); omLabelG.style.cursor = "pointer";
const omLabelBg = mkEl("rect", { x: String(omX), y: String(omY), width: "210", height: "32", rx: "8", fill: "transparent" });
omLabelG.appendChild(omLabelBg);
const omLabel = mkEl("text", { x: String(omX + 12), y: String(omY + 22), fill: "#1e293b", "font-size": "16", "font-weight": "700" });
omLabel.textContent = "\u03A9 = { NN, NS, SN, SS }"; omLabelG.appendChild(omLabel);
svg.appendChild(omLabelG);
// Event region (dashed rect, behind dots)
const evRegionRect = mkEl("rect", { rx: "22", fill: "transparent", stroke: "transparent", "stroke-width": "3", "stroke-dasharray": "8,4", opacity: "0.9" });
evRegionRect.style.display = "none"; svg.appendChild(evRegionRect);
const evRegionFill = mkEl("rect", { rx: "22", fill: "transparent", opacity: "0.08" });
evRegionFill.style.display = "none"; svg.appendChild(evRegionFill);
// Dot positions: spread nicely
const dotPositions = [
{ x: 170, y: 140 }, // NN
{ x: 340, y: 110 }, // NS
{ x: 340, y: 220 }, // SN
{ x: 510, y: 175 }, // SS
];
// Dots
const dotEls = [];
for (let i = 0; i < 4; i++) {
const p = pts[i], pos = dotPositions[i];
const g = mkEl("g"); g.style.cursor = "pointer";
// Outer ring (for highlight)
const ring = mkEl("circle", { cx: String(pos.x), cy: String(pos.y), r: "44", fill: "none", stroke: "transparent", "stroke-width": "3" });
g.appendChild(ring);
// Main circle
const c = mkEl("circle", { cx: String(pos.x), cy: String(pos.y), r: "38", fill: "#fff", stroke: "#cbd5e1", "stroke-width": "2.5" });
g.appendChild(c);
// Coin icons: two small circles
const coinY1 = pos.y - 10, coinY2 = pos.y + 10;
const coinR = 8;
for (let ci = 0; ci < 2; ci++) {
const face = ci === 0 ? p.c1 : p.c2;
const cy = ci === 0 ? coinY1 : coinY2;
const coinBg = mkEl("circle", { cx: String(pos.x - 10), cy: String(cy), r: String(coinR), fill: face === "N" ? "#fef3c7" : "#e0e7ff", stroke: face === "N" ? "#d97706" : "#6366f1", "stroke-width": "1.5" });
g.appendChild(coinBg);
const coinTxt = mkEl("text", { x: String(pos.x - 10), y: String(cy + 1), "text-anchor": "middle", "dominant-baseline": "middle", fill: face === "N" ? "#92400e" : "#4338ca", "font-size": "9", "font-weight": "700", "font-family": "'SF Mono',monospace" });
coinTxt.textContent = face; g.appendChild(coinTxt);
}
// Label
const t = mkEl("text", { x: String(pos.x + 10), y: String(pos.y + 1), "text-anchor": "start", "dominant-baseline": "middle", fill: "#334155", "font-size": "16", "font-weight": "700", "font-family": "'SF Mono',monospace" });
t.textContent = p.label; g.appendChild(t);
svg.appendChild(g);
dotEls.push({ g, c, t, ring, pos });
// Hover on dot = highlight as sample point
g.addEventListener("mouseenter", () => { highlightPoint(i); });
g.addEventListener("mouseleave", () => { if (!outcomeIdx >= 0 || activeEvent < 0) clearHighlight(); });
}
// Outcome ring (on top)
const outRing = mkEl("circle", { r: "48", fill: "none", stroke: "#dc2626", "stroke-width": "4", "stroke-dasharray": "6,3" });
outRing.style.display = "none"; svg.appendChild(outRing);
const outLabel = mkEl("text", { "text-anchor": "middle", fill: "#dc2626", "font-size": "13", "font-weight": "700" });
outLabel.style.display = "none"; svg.appendChild(outLabel);
wrapper.appendChild(svg);
// ══════════════════════════════════════════════════════
// 4. EXPLANATION BOX
// ══════════════════════════════════════════════════════
const explainBox = document.createElement("div");
explainBox.style.cssText = `width:100%;padding:12px 18px;border-radius:10px;margin-bottom:14px;
background:#fff;border:2px solid #e2e8f0;font-size:14px;color:#334155;line-height:1.7;
text-align:center;min-height:24px;transition:all 0.2s;`;
explainBox.innerHTML = `Bấm vào biểu đồ để tìm hiểu. <b>\u03A9</b> = không gian mẫu, mỗi vòng tròn = điểm mẫu <b>\u03C9</b>.`;
wrapper.appendChild(explainBox);
// ══════════════════════════════════════════════════════
// 5. COIN TOSS ANIMATION + BUTTON
// ══════════════════════════════════════════════════════
const tossRow = document.createElement("div");
tossRow.style.cssText = "display:flex;align-items:center;gap:16px;margin-bottom:14px;justify-content:center;";
const coinDisplay = document.createElement("div");
coinDisplay.style.cssText = "display:flex;gap:10px;min-width:100px;justify-content:center;";
function makeCoinEl() {
const outer = document.createElement("div"); outer.className = "coin-container";
const coin = document.createElement("div");
coin.style.cssText = `width:48px;height:48px;border-radius:50%;display:flex;align-items:center;justify-content:center;
font-size:20px;font-weight:800;font-family:"SF Mono",monospace;border:3px solid #d1d5db;background:#f8fafc;color:#94a3b8;`;
coin.textContent = "?";
outer.appendChild(coin);
return { outer, coin };
}
const coin1 = makeCoinEl(), coin2 = makeCoinEl();
coinDisplay.appendChild(coin1.outer); coinDisplay.appendChild(coin2.outer);
const tossBtn = document.createElement("button");
tossBtn.style.cssText = `padding:12px 28px;border-radius:12px;border:2px solid #dc2626;
background:#fef2f2;color:#dc2626;font-size:16px;font-weight:700;
cursor:pointer;font-family:inherit;transition:all 0.15s;`;
tossBtn.textContent = "\uD83E\uDE99 Tung 2 lần";
tossBtn.addEventListener("mouseenter", () => tossBtn.style.background = "#fee2e2");
tossBtn.addEventListener("mouseleave", () => tossBtn.style.background = "#fef2f2");
tossRow.appendChild(coinDisplay); tossRow.appendChild(tossBtn);
wrapper.appendChild(tossRow);
// ══════════════════════════════════════════════════════
// 6. EVENT CARDS
// ══════════════════════════════════════════════════════
const evTitle = document.createElement("div");
evTitle.style.cssText = "font-size:12px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:0.3px;margin-bottom:8px;width:100%;text-align:left;";
evTitle.textContent = "Các biến cố (tập con của \u03A9)";
wrapper.appendChild(evTitle);
const evRow = document.createElement("div"); evRow.style.cssText = "display:flex;gap:10px;width:100%;margin-bottom:10px;flex-wrap:wrap;";
let activeEvent = -1, outcomeIdx = -1;
for (let ei = 0; ei < events.length; ei++) {
const ev = events[ei];
const count = pts.filter(ev.test).length;
const card = document.createElement("div"); card.className = "ev-card";
const nm = document.createElement("div"); nm.className = "ev-card-name"; nm.style.color = ev.color; nm.textContent = ev.name;
const pr = document.createElement("div"); pr.className = "ev-card-prob"; pr.textContent = `${count}/4 = ${(count/4*100).toFixed(0)}%`;
card.appendChild(nm); card.appendChild(pr);
card.addEventListener("click", () => {
activeEvent = activeEvent === ei ? -1 : ei;
highlightEvent();
});
evRow.appendChild(card);
ev._card = card;
}
wrapper.appendChild(evRow);
// ══════════════════════════════════════════════════════
// 7. INTERACTION LOGIC
// ══════════════════════════════════════════════════════
function resetDots() {
for (let i = 0; i < 4; i++) {
dotEls[i].c.setAttribute("fill", "#fff"); dotEls[i].c.setAttribute("stroke", "#cbd5e1");
dotEls[i].c.setAttribute("stroke-width", "2.5"); dotEls[i].c.setAttribute("r", "38");
dotEls[i].t.setAttribute("fill", "#334155");
dotEls[i].ring.setAttribute("stroke", "transparent");
}
evRegionRect.style.display = "none"; evRegionFill.style.display = "none";
outRing.style.display = "none"; outLabel.style.display = "none";
omRect.setAttribute("stroke-width", "2.5"); omRect.setAttribute("fill", "#f8fafc");
}
function highlightOmega() {
resetDots();
omRect.setAttribute("stroke-width", "4"); omRect.setAttribute("fill", "#eef2ff");
for (let i = 0; i < 4; i++) {
dotEls[i].c.setAttribute("fill", "#eef2ff"); dotEls[i].c.setAttribute("stroke", "#1e293b");
}
explainBox.style.borderColor = "#1e293b";
explainBox.innerHTML = `<b style="color:#1e293b">\u03A9 (Không gian mẫu)</b> = {NN, NS, SN, SS}: tập hợp <b>tất cả 4 kết quả có thể xảy ra</b> khi tung đồng xu hai lần.`;
}
function highlightPoint(i) {
resetDots();
showOutcome(); // keep outcome visible if present
const p = pts[i], pos = dotPositions[i];
dotEls[i].c.setAttribute("fill", "#dcfce7"); dotEls[i].c.setAttribute("stroke", "#16a34a");
dotEls[i].c.setAttribute("stroke-width", "3"); dotEls[i].ring.setAttribute("stroke", "#16a34a");
dotEls[i].t.setAttribute("fill", "#16a34a");
explainBox.style.borderColor = "#16a34a";
explainBox.innerHTML = `<b style="color:#16a34a">\u03C9 = ${p.label}</b> là một <b>điểm mẫu</b>: một phần tử của \u03A9. Lần 1 = ${p.c1}, Lần 2 = ${p.c2} (${p.heads} mặt Ngửa).`;
}
function highlightEvent() {
resetDots();
// Card states
for (let ei = 0; ei < events.length; ei++) {
const ev = events[ei];
ev._card.style.borderColor = activeEvent === ei ? ev.color : "#e2e8f0";
ev._card.style.background = activeEvent === ei ? ev.color + "0a" : "#fff";
}
if (activeEvent < 0) {
explainBox.style.borderColor = "#e2e8f0";
explainBox.innerHTML = `Bấm vào một biến cố để xem cách thể hiện trên biểu đồ.`;
showOutcome();
return;
}
const ev = events[activeEvent];
const members = [], nonMembers = [];
for (let i = 0; i < 4; i++) {
if (ev.test(pts[i])) members.push(i); else nonMembers.push(i);
}
// Fade non-members
for (const i of nonMembers) {
dotEls[i].c.setAttribute("fill", "#f1f5f9"); dotEls[i].c.setAttribute("stroke", "#e2e8f0");
dotEls[i].t.setAttribute("fill", "#cbd5e1");
}
// Highlight members
for (const i of members) {
dotEls[i].c.setAttribute("fill", ev.color + "18"); dotEls[i].c.setAttribute("stroke", ev.color);
dotEls[i].c.setAttribute("stroke-width", "3"); dotEls[i].t.setAttribute("fill", ev.color);
}
// Enclosing region
if (members.length > 0) {
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
for (const i of members) { const pos = dotPositions[i]; x0 = Math.min(x0, pos.x); y0 = Math.min(y0, pos.y); x1 = Math.max(x1, pos.x); y1 = Math.max(y1, pos.y); }
const pad = 52;
evRegionRect.setAttribute("x", x0 - pad); evRegionRect.setAttribute("y", y0 - pad);
evRegionRect.setAttribute("width", x1 - x0 + 2 * pad); evRegionRect.setAttribute("height", y1 - y0 + 2 * pad);
evRegionRect.setAttribute("stroke", ev.color); evRegionRect.style.display = "";
evRegionFill.setAttribute("x", x0 - pad); evRegionFill.setAttribute("y", y0 - pad);
evRegionFill.setAttribute("width", x1 - x0 + 2 * pad); evRegionFill.setAttribute("height", y1 - y0 + 2 * pad);
evRegionFill.setAttribute("rx", "22"); evRegionFill.setAttribute("fill", ev.color);
evRegionFill.style.display = "";
}
// Check if outcome is in this event
let outcomeNote = "";
if (outcomeIdx >= 0) {
showOutcome();
const inEv = ev.test(pts[outcomeIdx]);
outcomeNote = ` Kết quả <b>${pts[outcomeIdx].label}</b> của bạn ${inEv ? `<b style="color:${ev.color}">\u2208 thuộc biến cố này</b>` : `<b>\u2209 không thuộc biến cố này</b>`}.`;
}
explainBox.style.borderColor = ev.color;
explainBox.innerHTML = `<b style="color:${ev.color}">Biến cố: "${ev.name}"</b> = {${members.map(i => pts[i].label).join(", ")}}: ` +
`<b>${members.length}</b> trong số 4 điểm mẫu. P = ${members.length}/4 = <b>${(members.length/4).toFixed(2)}</b>.${outcomeNote}`;
}
function clearHighlight() {
resetDots();
showOutcome();
if (activeEvent >= 0) { highlightEvent(); return; }
explainBox.style.borderColor = "#e2e8f0";
explainBox.innerHTML = `Bấm vào biểu đồ để xem chi tiết. <b>\u03A9</b> = không gian mẫu, mỗi vòng tròn = điểm mẫu <b>\u03C9</b>.`;
}
function showOutcome() {
if (outcomeIdx < 0) return;
const pos = dotPositions[outcomeIdx];
outRing.setAttribute("cx", pos.x); outRing.setAttribute("cy", pos.y); outRing.style.display = "";
outLabel.setAttribute("x", pos.x); outLabel.setAttribute("y", pos.y - 56);
outLabel.textContent = `Kết quả: ${pts[outcomeIdx].label}`; outLabel.style.display = "";
}
// Omega hover (on the label or the background margins)
omLabelG.addEventListener("mouseenter", highlightOmega);
omLabelG.addEventListener("mouseleave", clearHighlight);
// Also hovering the border area of omega rect
omRect.addEventListener("mouseenter", highlightOmega);
omRect.addEventListener("mouseleave", clearHighlight);
// ══════════════════════════════════════════════════════
// 8. COIN TOSS
// ══════════════════════════════════════════════════════
function setCoin(coinEl, face) {
coinEl.coin.textContent = face;
if (face === "N") {
coinEl.coin.style.background = "#fef3c7"; coinEl.coin.style.borderColor = "#d97706"; coinEl.coin.style.color = "#92400e";
} else {
coinEl.coin.style.background = "#e0e7ff"; coinEl.coin.style.borderColor = "#6366f1"; coinEl.coin.style.color = "#4338ca";
}
}
function animateCoin(coinEl, face, delay) {
return new Promise(resolve => {
setTimeout(() => {
coinEl.coin.textContent = "?"; coinEl.coin.style.background = "#f8fafc";
coinEl.coin.style.borderColor = "#d1d5db"; coinEl.coin.style.color = "#94a3b8";
coinEl.coin.classList.remove("coin-anim");
void coinEl.coin.offsetWidth; // reflow
coinEl.coin.classList.add("coin-anim");
setTimeout(() => {
setCoin(coinEl, face);
coinEl.coin.classList.remove("coin-anim");
resolve();
}, 800);
}, delay);
});
}
let tossing = false;
tossBtn.addEventListener("click", async () => {
if (tossing) return;
tossing = true;
tossBtn.style.opacity = "0.5"; tossBtn.style.pointerEvents = "none";
const r = Math.floor(Math.random() * 4);
const p = pts[r];
// Animate both coins
await Promise.all([
animateCoin(coin1, p.c1, 0),
animateCoin(coin2, p.c2, 200),
]);
outcomeIdx = r;
activeEvent = -1;
resetDots();
// Highlight outcome
const pos = dotPositions[r];
dotEls[r].c.setAttribute("fill", "#fef2f2"); dotEls[r].c.setAttribute("stroke", "#dc2626");
dotEls[r].c.setAttribute("stroke-width", "3");
dotEls[r].t.setAttribute("fill", "#dc2626");
showOutcome();
// Which events contain this outcome
const inEvents = events.filter(ev => ev.test(p)).map(ev => ev.name);
const evStr = inEvents.length > 0 ? ` Thuộc biến cố: <b>${inEvents.join(", ")}</b>.` : " Không thuộc bất kỳ biến cố nào đã định nghĩa.";
explainBox.style.borderColor = "#dc2626";
explainBox.innerHTML = `<b style="color:#dc2626">Kết quả: ${p.label}</b> Lần 1 = ${p.c1}, Lần 2 = ${p.c2} (${p.heads} mặt Ngửa). ` +
`Đây là điểm mẫu <b>thực sự đã xảy ra</b>.${evStr}`;
// Update event card highlights
for (let ei = 0; ei < events.length; ei++) {
events[ei]._card.style.borderColor = "#e2e8f0"; events[ei]._card.style.background = "#fff";
}
tossing = false;
tossBtn.style.opacity = "1"; tossBtn.style.pointerEvents = "auto";
});
// ══════════════════════════════════════════════════════
// 9. INIT
// ══════════════════════════════════════════════════════
clearHighlight();
invalidation.then(() => {});
wrapper.value = {};
return wrapper;
})()Xác suất là con số thể hiện khả năng xảy ra của từng điểm mẫu hay biến cố trong \(\Omega\). Lưu ý: Xác suất luôn gắn liền với không gian mẫu. Xác suất sẽ thay đổi trong các không gian mẫu khác nhau.
Chọn ngẫu nhiên 3 bệnh nhân trong khu cách ly và xét nghiệm. Hãy mô tả các khái niệm đã học cho tình huống này:
- Tên phép thử ngẫu nhiên là gì?
- Không gian mẫu \(\Omega\) được viết như thế nào?
- Tự định nghĩa 1 biến cố bất kỳ, biến cố này chứa những điểm mẫu nào?
1.4 Xác suất có điều kiện
Xác suất có điều kiện thực chất là thu nhỏ không gian mẫu.
Ví dụ, tại một ngôi trường có 3 lớp A, B, C với số lượng học sinh như sau:
| Nam | Nữ | Tổng | |
|---|---|---|---|
| Lớp A | 30 | 10 | 40 |
| Lớp B | 20 | 20 | 40 |
| Lớp C | 15 | 25 | 40 |
| Tổng | 65 | 55 | 120 |
Chọn ngẫu nhiên 1 học sinh trong trường.
1. Xác suất không điều kiện
Tính xác suất học sinh này là Nữ?
Lúc này không gian mẫu là toàn bộ ngôi trường.
\[\mathbb{P}(\text{Nữ}) = \frac{\overbrace{55}^{\text{Tổng số Nữ toàn trường}}}{\underbrace{120}_{\text{Tổng số học sinh toàn trường}}} \approx 45.8\%\]
2. Xác suất có điều kiện
Biết rằng học sinh được chọn ở Lớp A. Tính xác suất học sinh này là Nữ?
Thông tin “Học sinh Lớp A” đã làm thu nhỏ không gian mẫu. Không gian mẫu lúc này chỉ là Học sinh lớp A thôi.
\[\mathbb{P}(\text{Nữ} \mid \text{Lớp A}) = \frac{\overbrace{10}^{\text{Số Nữ trong Lớp A}}}{\underbrace{40}_{\text{Tổng sĩ số Lớp A}}} = 25.0\%\]
viewof classroom_prob_sharp = (() => {
// --- 1. SETUP ---
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;
width: 100%;
max-width: 800px;
margin-left: auto;
margin-right: auto;
`;
// --- 2. DATA ---
const CLASSES = {
A: { boys: 30, girls: 10, color: "#ef4444" },
B: { boys: 20, girls: 20, color: "#3b82f6" },
C: { boys: 15, girls: 25, color: "#10b981" }
};
const students = [];
Object.keys(CLASSES).forEach(cls => {
const data = CLASSES[cls];
for(let i=0; i<data.boys; i++) students.push({ id: `b-${cls}-${i}`, cls: cls, gender: "Nam", color: data.color });
for(let i=0; i<data.girls; i++) students.push({ id: `g-${cls}-${i}`, cls: cls, gender: "Nữ", color: data.color });
});
// --- 3. CONTROLS ---
const controlPanel = document.createElement("div");
controlPanel.style.cssText = `
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 16px;
margin-bottom: 24px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 12px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
width: 100%;
box-sizing: border-box;
`;
function createSelect(options) {
const sel = document.createElement("select");
sel.style.cssText = `
padding: 8px 12px;
border-radius: 8px;
border: 1px solid #d1d5db;
background-color: #f9fafb;
font-size: 14px;
font-weight: 600;
color: #1e293b;
cursor: pointer;
`;
options.forEach(opt => {
const el = document.createElement("option");
el.value = opt.val;
el.textContent = opt.label;
sel.appendChild(el);
});
return sel;
}
const targetOpts = [
{val: "Nam", label: "Học sinh Nam"},
{val: "Nữ", label: "Học sinh Nữ"},
{val: "A", label: "Học sinh Lớp A"},
{val: "B", label: "Học sinh Lớp B"},
{val: "C", label: "Học sinh Lớp C"}
];
const givenOpts = [
{val: "Universe", label: "Toàn trường"},
{val: "A", label: "Học sinh Lớp A"},
{val: "B", label: "Học sinh Lớp B"},
{val: "C", label: "Học sinh Lớp C"},
{val: "Nam", label: "Đó là Nam"},
{val: "Nữ", label: "Đó là Nữ"}
];
const selTarget = createSelect(targetOpts);
const selGiven = createSelect(givenOpts);
selTarget.value = "Nữ";
selGiven.value = "A";
const t1 = document.createElement("span"); t1.textContent = "Tính xác suất chọn"; t1.style.color = "#334155";
const t2 = document.createElement("span"); t2.textContent = "biết rằng"; t2.style.color = "#334155";
controlPanel.appendChild(t1);
controlPanel.appendChild(selTarget);
controlPanel.appendChild(t2);
controlPanel.appendChild(selGiven);
wrapper.appendChild(controlPanel);
// --- 4. CANVAS (High Resolution) ---
const canvas = document.createElement("canvas");
const logicalW = 800;
const logicalH = 340;
// Set Display Size (CSS)
canvas.style.width = `${logicalW}px`;
canvas.style.height = `${logicalH}px`;
// Responsive Max Width
canvas.style.maxWidth = "100%";
canvas.style.height = "auto";
// Set Actual Pixel Size (High DPI)
const dpr = window.devicePixelRatio || 1;
canvas.width = logicalW * dpr;
canvas.height = logicalH * dpr;
const ctx = canvas.getContext("2d");
// Scale drawing operations so we can use logical coordinates
ctx.scale(dpr, dpr);
wrapper.appendChild(canvas);
// --- 5. MATH PANEL ---
const mathDiv = document.createElement("div");
mathDiv.style.cssText = `
font-size: 18px;
margin-top: 20px;
margin-bottom: 10px;
text-align: center;
line-height: 1.6;
width: 100%;
`;
wrapper.appendChild(mathDiv);
// --- 6. GEOMETRY PRE-CALCULATION ---
const boxW = 180;
const boxH = 300;
const gap = 24;
const startX = (logicalW - (3 * boxW + 2 * gap)) / 2;
const startY = 10;
const dotCols = 5;
const dotDiam = 13;
const dotGap = 13;
const gridWidth = (dotCols * dotDiam) + ((dotCols - 1) * dotGap);
const dotRad = dotDiam / 2;
const posMap = {};
['A', 'B', 'C'].forEach((cls, i) => {
const clsStudents = students.filter(s => s.cls === cls);
const bx = startX + i * (boxW + gap);
const by = startY;
const gridStartX = bx + (boxW - gridWidth) / 2 + dotRad;
const gridStartY = by + 80;
clsStudents.forEach((s, idx) => {
const c = idx % dotCols;
const r = Math.floor(idx / dotCols);
posMap[s.id] = {
x: gridStartX + c * (dotDiam + dotGap),
y: gridStartY + r * (dotDiam + dotGap),
bx: bx, by: by
};
});
});
// --- 7. LOGIC ---
function check(student, condition) {
if (condition === "Universe") return true;
if (condition === "A" || condition === "B" || condition === "C") return student.cls === condition;
if (condition === "Nam" || condition === "Nữ") return student.gender === condition;
return false;
}
function update() {
const targetCond = selTarget.value;
const givenCond = selGiven.value;
const validUniverse = students.filter(s => check(s, givenCond));
const validTarget = validUniverse.filter(s => check(s, targetCond));
const num = validTarget.length;
const den = validUniverse.length;
const prob = den === 0 ? 0 : ((num / den) * 100).toFixed(1);
const dict = {
"Nam": "Nam", "Nữ": "Nữ",
"A": "Lớp A", "B": "Lớp B", "C": "Lớp C",
"Universe": "Toàn trường"
};
// Cleaned up text, 18px base size
mathDiv.innerHTML = `
<div style="display:inline-block; padding: 15px 30px; background:#f8fafc; border-radius:12px; border:1px solid #e2e8f0;">
<div style="font-size: 18px; color:#0f172a; line-height: 1.4;">
P(${dict[targetCond] || targetCond} | ${dict[givenCond] || givenCond}) =
<span style="color: #be185d; font-weight: bold;">${num}</span>
/
<span style="color: #0f172a; font-weight: bold;">${den}</span>
= <b>${prob}%</b>
</div>
</div>
`;
drawScene(givenCond, targetCond, validUniverse, validTarget);
}
function drawScene(given, target, universeSet, targetSet) {
ctx.clearRect(0, 0, logicalW, logicalH);
// Draw Boxes
['A', 'B', 'C'].forEach((cls, i) => {
const bx = startX + i * (boxW + gap);
const by = startY;
const boxHasUniverse = universeSet.some(s => s.cls === cls);
if (given === cls) {
ctx.fillStyle = "#eff6ff"; ctx.strokeStyle = "#3b82f6"; ctx.lineWidth = 3;
ctx.shadowColor = "rgba(59, 130, 246, 0.15)"; ctx.shadowBlur = 15; ctx.shadowOffsetY = 4;
} else if (boxHasUniverse) {
ctx.fillStyle = "#ffffff"; ctx.strokeStyle = "#cbd5e1"; ctx.lineWidth = 1.5;
ctx.shadowColor = "rgba(0,0,0,0)";
} else {
ctx.fillStyle = "#f8fafc"; ctx.strokeStyle = "#e2e8f0"; ctx.lineWidth = 1;
ctx.shadowColor = "rgba(0,0,0,0)";
}
ctx.beginPath();
ctx.roundRect(bx, by, boxW, boxH, 16);
ctx.fill();
ctx.stroke();
ctx.shadowColor = "rgba(0,0,0,0)";
// Text
ctx.textAlign = "center";
ctx.fillStyle = boxHasUniverse ? "#1e293b" : "#cbd5e1";
ctx.font = "bold 20px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
ctx.fillText(`Lớp ${cls}`, bx + boxW/2, by + 35);
const count = CLASSES[cls];
ctx.font = "500 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
ctx.fillStyle = boxHasUniverse ? "#64748b" : "#e2e8f0";
ctx.fillText(`${count.boys} Nam • ${count.girls} Nữ`, bx + boxW/2, by + 56);
if (boxHasUniverse) {
ctx.beginPath(); ctx.strokeStyle = "#f1f5f9"; ctx.lineWidth = 1;
ctx.moveTo(bx + 30, by + 68); ctx.lineTo(bx + boxW - 30, by + 68); ctx.stroke();
}
});
// Draw Students
students.forEach(s => {
const pos = posMap[s.id];
const isUniverse = universeSet.includes(s);
const isTarget = targetSet.includes(s);
let alpha = 0.1;
let radius = 6;
let color = "#e5e7eb";
if (isUniverse) {
alpha = 0.4;
color = s.gender === "Nam" ? "#60a5fa" : "#f472b6";
}
if (isTarget) {
alpha = 1.0;
radius = 7.5;
color = s.gender === "Nam" ? "#2563eb" : "#db2777";
} else if (!isUniverse) {
color = "#f1f5f9";
}
ctx.globalAlpha = alpha;
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(pos.x, pos.y, radius, 0, Math.PI*2);
ctx.fill();
if (isTarget) {
ctx.lineWidth = 2;
ctx.strokeStyle = "#ffffff";
ctx.stroke();
ctx.beginPath();
ctx.arc(pos.x, pos.y, radius + 1.5, 0, Math.PI*2);
ctx.strokeStyle = color;
ctx.lineWidth = 1;
ctx.globalAlpha = 0.4;
ctx.stroke();
}
});
ctx.globalAlpha = 1.0;
}
selTarget.addEventListener("change", update);
selGiven.addEventListener("change", update);
update();
return wrapper;
})();1.5 Quan hệ giữa hai biến cố
1.5.1 Xung khắc (mutually exclusive)
Hai biến cố \(A\) và \(B\) được gọi là xung khắc (mutually exclusive) khi không có điểm chung nào (\(A \cap B = \emptyset\)). Chúng không thể cùng xảy ra tại một thời điểm.
\[\mathbb{P}(A \cap B) = 0\]
Nếu \(A\) xảy ra thì chắc chắn \(B\) không xảy ra (và ngược lại).
\[\mathbb{P}(A \mid B) = 0\]
\[\mathbb{P}(B \mid A) = 0\]
1.5.2 Phụ thuộc (dependent)
Hai biến cố \(A\) và \(B\) phụ thuộc nhau khi chúng có mối liên hệ với nhau (\(A \cap B \neq \emptyset\)). Việc biết biến cố này xảy ra sẽ làm thay đổi xác suất của biến cố kia.
\[\mathbb{P}(A \mid B) \neq \mathbb{P}(A)\]
\[\mathbb{P}(B \mid A) \neq \mathbb{P}(B)\]
Đây là trường hợp phổ biến nhất trong thực tế.
1.5.3 Độc lập (independent)
Hai biến cố \(A\) và \(B\) độc lập nghĩa là việc biết \(B\) xảy ra không cung cấp thêm thông tin gì về khả năng xảy ra của \(A\) và ngược lại.
\[\mathbb{P}(A \mid B) = \mathbb{P}(A)\]
\[\mathbb{P}(B \mid A) = \mathbb{P}(B)\]
Quan hệ giữa hai biến cố có thể được minh họa bằng biểu đồ Venn như sau:
viewof venn = (() => {
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:700px;margin:0 auto;`;
wrapper.appendChild(injectStyle());
// ══════════════════════════════════════════════════════
// CONTROLS
// ══════════════════════════════════════════════════════
const SL = {};
SL.pA = createSlider("P(A)", 0.01, 0.99, 0.01, 0.05, "#5b9bd5", "blue");
SL.pB = createSlider("P(B)", 0.01, 0.99, 0.01, 0.05, "#e8915a", "amber");
const r1 = document.createElement("div"); r1.style.cssText = "display:flex;gap:20px;width:100%;margin-bottom:16px;";
r1.appendChild(SL.pA.el); r1.appendChild(SL.pB.el);
wrapper.appendChild(r1);
// ══════════════════════════════════════════════════════
// SVG
// ══════════════════════════════════════════════════════
const NS = "http://www.w3.org/2000/svg";
const W = 660, H = 460;
// Ω box inset
const OX = 20, OY = 50, OW = W - 40, OH = H - 70;
const OMEGA_AREA = OW * OH;
function mkEl(tag, a) {
const e = document.createElementNS(NS, tag);
if (a) for (const [k,v] of Object.entries(a)) e.setAttribute(k,v);
return e;
}
const svg = document.createElementNS(NS, "svg");
svg.setAttribute("viewBox", `0 0 ${W} ${H}`);
svg.style.cssText = `width:100%;max-width:${W}px;border-radius:12px;border:1px solid #e2e8f0;margin-bottom:12px;`;
// Outer background
svg.appendChild(mkEl("rect", { x: "0", y: "0", width: String(W), height: String(H), fill: "#f1f5f9", rx: "12" }));
// Ω box
svg.appendChild(mkEl("rect", { x: String(OX), y: String(OY), width: String(OW), height: String(OH), rx: "12", fill: "#fafbfc", stroke: "#1e293b", "stroke-width": "2.5" }));
// Title
const titleText = mkEl("text", { x: String(W / 2), y: "34", "text-anchor": "middle", fill: "#dc2626", "font-size": "22", "font-weight": "700" });
svg.appendChild(titleText);
// Ω label
const omLabel = mkEl("text", { x: String(OX + OW - 10), y: String(OY + 22), "text-anchor": "end", fill: "#1e293b", "font-size": "22", "font-weight": "700" });
omLabel.textContent = "\u03A9"; svg.appendChild(omLabel);
// Clip to Ω box so circles don't overflow
const defs = mkEl("defs");
const clipOmega = mkEl("clipPath", { id: "clipOmega" });
clipOmega.appendChild(mkEl("rect", { x: String(OX), y: String(OY), width: String(OW), height: String(OH), rx: "12" }));
defs.appendChild(clipOmega);
// Clip for intersection (circle A shape)
const clipA = mkEl("clipPath", { id: "clipA" });
const clipACirc = mkEl("circle"); clipA.appendChild(clipACirc);
defs.appendChild(clipA);
svg.appendChild(defs);
// Group clipped to Ω
const gClipped = mkEl("g", { "clip-path": "url(#clipOmega)" });
svg.appendChild(gClipped);
// Circle A
const circA = mkEl("circle", { fill: "#5b9bd5", opacity: "0.25", stroke: "#5b9bd5", "stroke-width": "2.5" });
gClipped.appendChild(circA);
// Circle B
const circB = mkEl("circle", { fill: "#f4b183", opacity: "0.25", stroke: "#e8915a", "stroke-width": "2.5" });
gClipped.appendChild(circB);
// Intersection: B clipped to A
const interCirc = mkEl("circle", { fill: "#16a34a", opacity: "0.28", "clip-path": "url(#clipA)" });
gClipped.appendChild(interCirc);
// Labels (outside clip so always visible)
const lblA = mkEl("text", { "text-anchor": "middle", fill: "#1e5a9e", "font-size": "17", "font-weight": "700" });
svg.appendChild(lblA);
const lblB = mkEl("text", { "text-anchor": "middle", fill: "#b35a1f", "font-size": "17", "font-weight": "700" });
svg.appendChild(lblB);
wrapper.appendChild(svg);
// ══════════════════════════════════════════════════════
// STATS
// ══════════════════════════════════════════════════════
const statsRow = document.createElement("div");
statsRow.style.cssText = "display:flex;gap:16px;width:100%;flex-wrap:wrap;margin-bottom:12px;";
function makeStatBox(borderColor, bgColor) {
const box = document.createElement("div");
box.style.cssText = `flex:1;min-width:200px;padding:12px 16px;border-radius:10px;
background:${bgColor};border:2px solid ${borderColor};
font-size:15px;line-height:2.0;
font-family:"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;`;
return box;
}
const statLeft = makeStatBox("#bfdbfe", "#eff6ff");
const statRight = makeStatBox("#fed7aa", "#fff7ed");
statsRow.appendChild(statLeft); statsRow.appendChild(statRight);
wrapper.appendChild(statsRow);
// ══════════════════════════════════════════════════════
// GEOMETRY
// ══════════════════════════════════════════════════════
// Fixed centers: A at 1/3, B at 2/3 of the Ω box
const AX = OX + OW * 0.37;
const BX = OX + OW * 0.63;
const CY = OY + OH * 0.5;
const DIST = BX - AX;
function radiusFromP(p) {
// Circle area = p × OMEGA_AREA → visually proportional to probability
return Math.sqrt(p * OMEGA_AREA / Math.PI);
}
function circleOverlapArea(r1, r2, d) {
if (d >= r1 + r2) return 0;
if (d <= Math.abs(r1 - r2)) return Math.PI * Math.min(r1, r2) ** 2;
const a = (r1 * r1 - r2 * r2 + d * d) / (2 * d);
const b = d - a;
return r1 * r1 * Math.acos(Math.max(-1, Math.min(1, a / r1)))
- a * Math.sqrt(Math.max(0, r1 * r1 - a * a))
+ r2 * r2 * Math.acos(Math.max(-1, Math.min(1, b / r2)))
- b * Math.sqrt(Math.max(0, r2 * r2 - b * b));
}
// ══════════════════════════════════════════════════════
// UPDATE
// ══════════════════════════════════════════════════════
function update() {
const pA = SL.pA.val();
const pB = SL.pB.val();
const rA = radiusFromP(pA);
const rB = radiusFromP(pB);
// Set circles
circA.setAttribute("cx", AX); circA.setAttribute("cy", CY); circA.setAttribute("r", rA);
circB.setAttribute("cx", BX); circB.setAttribute("cy", CY); circB.setAttribute("r", rB);
clipACirc.setAttribute("cx", AX); clipACirc.setAttribute("cy", CY); clipACirc.setAttribute("r", rA);
interCirc.setAttribute("cx", BX); interCirc.setAttribute("cy", CY); interCirc.setAttribute("r", rB);
// Compute overlap
const overlapPx = circleOverlapArea(rA, rB, DIST);
const pAB = overlapPx / OMEGA_AREA;
interCirc.style.display = pAB > 0.001 ? "" : "none";
// Label positions: shift outward if overlapping
const hasOverlap = pAB > 0.01;
const aLblX = hasOverlap ? Math.max(OX + 40, AX - rA * 0.45) : AX;
const bLblX = hasOverlap ? Math.min(OX + OW - 40, BX + rB * 0.45) : BX;
lblA.setAttribute("x", aLblX); lblA.setAttribute("y", CY + 5);
lblA.textContent = `P(A) = ${pA.toFixed(2)}`;
lblB.setAttribute("x", bLblX); lblB.setAttribute("y", CY + 5);
lblB.textContent = `P(B) = ${pB.toFixed(2)}`;
// ── Stats ──
const pA_B = pB > 0.001 ? pAB / pB : 0;
const pB_A = pA > 0.001 ? pAB / pA : 0;
const pApB = pA * pB;
const fmt = v => v.toFixed(2);
statLeft.innerHTML =
`<span style="color:#dc2626;font-weight:700">P(A | B)</span> = ${fmt(pA_B)}<br>` +
`<span style="color:#d97706;font-weight:700">P(B | A)</span> = ${fmt(pB_A)}`;
statRight.innerHTML =
`<span style="color:#16a34a;font-weight:700">P(A \u2229 B)</span> = ${fmt(pAB)}<br>` +
`<span style="color:#64748b;font-weight:700">P(A)\u00D7P(B)</span> = ${fmt(pApB)}`;
// ── Relationship ──
const eps = 0.002;
const isMutEx = pAB < eps;
const isIndep = Math.abs(pAB - pApB) < eps && !isMutEx;
if (isMutEx) {
titleText.textContent = "Xung khắc";
titleText.setAttribute("fill", "#dc2626");
} else if (isIndep) {
titleText.textContent = "Độc lập";
titleText.setAttribute("fill", "#16a34a");
} else {
titleText.textContent = "Phụ thuộc";
titleText.setAttribute("fill", "#d97706");
}
}
// ══════════════════════════════════════════════════════
// EVENTS – enforce P(A) + P(B) ≤ 1
// ══════════════════════════════════════════════════════
function onInputA() {
SL.pA.sync();
const pA = SL.pA.val();
const maxB = Math.max(0.01, +(1 - pA).toFixed(2));
const curB = SL.pB.val();
// Clamp P(B) if the sum exceeds 1
if (curB > maxB) {
SL.pB.update(maxB, null, maxB);
} else {
// Just update the max so the slider range shrinks
SL.pB.update(curB, null, maxB);
}
update();
}
function onInputB() {
SL.pB.sync();
const pB = SL.pB.val();
const maxA = Math.max(0.01, +(1 - pB).toFixed(2));
const curA = SL.pA.val();
// Clamp P(A) if the sum exceeds 1
if (curA > maxA) {
SL.pA.update(maxA, null, maxA);
} else {
SL.pA.update(curA, null, maxA);
}
update();
}
SL.pA.input.addEventListener("input", onInputA);
SL.pB.input.addEventListener("input", onInputB);
// Initial constraint setup
onInputA();
invalidation.then(() => {
SL.pA.input.removeEventListener("input", onInputA);
SL.pB.input.removeEventListener("input", onInputB);
});
wrapper.value = {};
return wrapper;
})()1.6 Các phép tính xác suất
1.6.1 Phép nhân (Và)
Xác suất để các biến cố cùng xảy ra đồng thời hoặc nối tiếp nhau (\(A\) và \(B\)).
\[\mathbb{P}(A \cap B) = \mathbb{P}(A) \times \mathbb{P}(B|A)\]
(Xác suất \(A\) xảy ra, nhân với xác suất \(B\) xảy ra với điều kiện \(A\) đã xảy ra).
Khi hai biến cố là độc lập thì \(\mathbb{P}(B|A) = \mathbb{P}(B)\):
\[\mathbb{P}(A \cap B) = \mathbb{P}(A) \times \mathbb{P}(B)\]
1.6.2 Phép cộng (Hoặc)
Xác suất để ít nhất một trong các biến cố xảy ra (\(A\) hoặc \(B\))
\[\mathbb{P}(A \cup B) = \mathbb{P}(A) + \mathbb{P}(B) - \mathbb{P}(A \cap B)\]
(Tổng xác suất của \(A\) và \(B\) trừ cho trường hợp chúng xảy ra cùng lúc).
Khi hai biến cố xung khắc (không thể xảy ra cùng lúc) thì \(\mathbb{P}(A \cap B) = 0\):
\[\mathbb{P}(A \cup B) = \mathbb{P}(A) + \mathbb{P}(B)\]
1.6.3 Phần bù (Không)
Xác suất để một biến cố không xảy ra.
\[\mathbb{P}(\bar{A}) = 1 - \mathbb{P}(A)\]
Chúng ta cũng có thể dùng biểu đồ Venn để minh họa các phép tính này:
viewof venn2 = (() => {
const wrapper2 = document.createElement("div");
wrapper2.style.cssText = `display:flex;flex-direction:column;align-items:center;
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
width:100%;max-width:760px;margin:0 auto;`;
wrapper2.appendChild(injectStyle());
// ══════════════════════════════════════════════════════
// STATE
// ══════════════════════════════════════════════════════
let activeRegion2 = "inter"; // "inter" | "union" | "compA" | "compB"
// ══════════════════════════════════════════════════════
// CONTROLS – sliders
// ══════════════════════════════════════════════════════
const SL2 = {};
SL2.pA = createSlider("P(A)", 0.01, 0.99, 0.01, 0.30, "#3b82f6", "blue");
SL2.pB = createSlider("P(B)", 0.01, 0.99, 0.01, 0.25, "#f59e0b", "amber");
const r1_2 = document.createElement("div");
r1_2.style.cssText = "display:flex;gap:24px;width:100%;margin-bottom:16px;";
r1_2.appendChild(SL2.pA.el); r1_2.appendChild(SL2.pB.el);
wrapper2.appendChild(r1_2);
// ══════════════════════════════════════════════════════
// CONTROLS – region toggle buttons
// ══════════════════════════════════════════════════════
const btnRow2 = document.createElement("div");
btnRow2.style.cssText = `display:flex;gap:12px;width:100%;margin-bottom:20px;`;
const overA2 = `<span style="text-decoration:overline;">A</span>`;
const overB2 = `<span style="text-decoration:overline;">B</span>`;
const regionDefs2 = [
{ id: "inter", labelHTML: "A ∩ B", color: "#16a34a", bgActive: "#dcfce7", border: "#86efac" },
{ id: "union", labelHTML: "A ∪ B", color: "#7c3aed", bgActive: "#ede9fe", border: "#c4b5fd" },
{ id: "compA", labelHTML: overA2, color: "#475569", bgActive: "#f1f5f9", border: "#94a3b8" },
{ id: "compB", labelHTML: overB2, color: "#57534e", bgActive: "#f5f5f4", border: "#a8a29e" },
];
const regionBtns2 = {};
regionDefs2.forEach(rd2 => {
const btn2 = document.createElement("button");
btn2.innerHTML = rd2.labelHTML;
btn2.dataset.rid = rd2.id;
btn2.style.cssText = `flex:1;padding:10px 0;border-radius:10px;font-size:15px;font-weight:700;
cursor:pointer;transition:all 0.2s ease;font-family:inherit;text-align:center;
border:2px solid #e2e8f0;background:#fff;color:#475569;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);`;
btnRow2.appendChild(btn2);
regionBtns2[rd2.id] = btn2;
});
wrapper2.appendChild(btnRow2);
function styleButtons2() {
regionDefs2.forEach(rd2 => {
const btn2 = regionBtns2[rd2.id];
if (rd2.id === activeRegion2) {
btn2.style.background = rd2.bgActive;
btn2.style.color = rd2.color;
btn2.style.borderColor = rd2.border;
btn2.style.boxShadow = `0 0 0 3px ${rd2.border}33`;
} else {
btn2.style.background = "#fff";
btn2.style.color = "#475569";
btn2.style.borderColor = "#e2e8f0";
btn2.style.boxShadow = "0 1px 2px 0 rgba(0, 0, 0, 0.05)";
}
});
}
// ══════════════════════════════════════════════════════
// SVG SETUP
// ══════════════════════════════════════════════════════
const NS2 = "http://www.w3.org/2000/svg";
const W2 = 700, H2 = 460;
const OX2 = 20, OY2 = 50, OW2 = W2 - 40, OH2 = H2 - 70;
const OMEGA_AREA2 = OW2 * OH2;
function mkEl2(tag, a) {
const e = document.createElementNS(NS2, tag);
if (a) for (const [k,v] of Object.entries(a)) e.setAttribute(k, v);
// Smooth transitions only for highlight/color changes, not layout/geometry (prevents dragging lag)
e.style.transition = "opacity 0.25s ease, fill 0.25s ease";
return e;
}
const svg2 = document.createElementNS(NS2, "svg");
svg2.setAttribute("viewBox", `0 0 ${W2} ${H2}`);
svg2.style.cssText = `width:100%;max-width:${W2}px;border-radius:16px;
border:1px solid #e2e8f0;margin-bottom:20px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);`;
// Outer background
svg2.appendChild(mkEl2("rect", { x:"0", y:"0", width:String(W2), height:String(H2), fill:"#f8fafc", rx:"16" }));
// Ω box
svg2.appendChild(mkEl2("rect", { x:String(OX2), y:String(OY2), width:String(OW2), height:String(OH2), rx:"12", fill:"#ffffff", stroke:"#cbd5e1", "stroke-width":"2" }));
// Title text (relationship status)
const titleText2 = mkEl2("text", { x:String(W2/2), y:"34", "text-anchor":"middle", fill:"#dc2626", "font-size":"20", "font-weight":"700" });
svg2.appendChild(titleText2);
// Ω label
const omLabel2 = mkEl2("text", { x:String(OX2+OW2-12), y:String(OY2+24), "text-anchor":"end", fill:"#64748b", "font-size":"20", "font-weight":"700" });
omLabel2.textContent = "\u03A9"; svg2.appendChild(omLabel2);
// ── Defs: clip paths ──
const defs2 = mkEl2("defs");
// Clip to Ω box
const clipOmega2 = mkEl2("clipPath", { id:"clipOmega2" });
clipOmega2.appendChild(mkEl2("rect", { x:String(OX2), y:String(OY2), width:String(OW2), height:String(OH2), rx:"12" }));
defs2.appendChild(clipOmega2);
// Clip A
const clipA2 = mkEl2("clipPath", { id:"clipA2" });
const clipACirc2 = mkEl2("circle"); clipA2.appendChild(clipACirc2);
defs2.appendChild(clipA2);
// Clip B
const clipB2 = mkEl2("clipPath", { id:"clipB2" });
const clipBCirc2 = mkEl2("circle"); clipB2.appendChild(clipBCirc2);
defs2.appendChild(clipB2);
// Inverse clip A
const clipNotA2 = mkEl2("clipPath", { id:"clipNotA2" });
const pathNotA2 = mkEl2("path");
clipNotA2.appendChild(pathNotA2);
defs2.appendChild(clipNotA2);
// Inverse clip B
const clipNotB2 = mkEl2("clipPath", { id:"clipNotB2" });
const pathNotB2 = mkEl2("path");
clipNotB2.appendChild(pathNotB2);
defs2.appendChild(clipNotB2);
svg2.appendChild(defs2);
// ── Clipped group ──
const gClipped2 = mkEl2("g", { "clip-path":"url(#clipOmega2)" });
svg2.appendChild(gClipped2);
// Region highlight rect (for complement fills covering the whole Ω)
const regionOmegaFill2 = mkEl2("rect", { x:String(OX2), y:String(OY2), width:String(OW2), height:String(OH2), fill:"#94a3b8", opacity:"0" });
gClipped2.appendChild(regionOmegaFill2);
// Circle A base
const circA2 = mkEl2("circle", { fill:"#3b82f6", opacity:"0.15", stroke:"none" });
gClipped2.appendChild(circA2);
// Circle B base
const circB2 = mkEl2("circle", { fill:"#f59e0b", opacity:"0.15", stroke:"none" });
gClipped2.appendChild(circB2);
// Intersection highlight
const interFill2 = mkEl2("circle", { fill:"#10b981", opacity:"0", "clip-path":"url(#clipA2)" });
gClipped2.appendChild(interFill2);
// Union highlight overlays
const unionFillA2 = mkEl2("circle", { fill:"#8b5cf6", opacity:"0" });
gClipped2.appendChild(unionFillA2);
const unionFillB2 = mkEl2("circle", { fill:"#8b5cf6", opacity:"0" });
gClipped2.appendChild(unionFillB2);
// Complement highlights
const compAFill2 = mkEl2("rect", { x:String(OX2), y:String(OY2), width:String(OW2), height:String(OH2), fill:"#64748b", opacity:"0", "clip-path":"url(#clipNotA2)" });
gClipped2.appendChild(compAFill2);
const compBFill2 = mkEl2("rect", { x:String(OX2), y:String(OY2), width:String(OW2), height:String(OH2), fill:"#78716c", opacity:"0", "clip-path":"url(#clipNotB2)" });
gClipped2.appendChild(compBFill2);
// Outlines
const circAOutline2 = mkEl2("circle", { fill:"none", stroke:"#3b82f6", "stroke-width":"2" });
gClipped2.appendChild(circAOutline2);
const circBOutline2 = mkEl2("circle", { fill:"none", stroke:"#f59e0b", "stroke-width":"2" });
gClipped2.appendChild(circBOutline2);
// Labels
const lblA2 = mkEl2("text", { "text-anchor":"middle", fill:"#2563eb", "font-size":"16", "font-weight":"700" });
svg2.appendChild(lblA2);
const lblB2 = mkEl2("text", { "text-anchor":"middle", fill:"#d97706", "font-size":"16", "font-weight":"700" });
svg2.appendChild(lblB2);
// Region probability label
const regionLbl2 = mkEl2("text", { "text-anchor":"middle", fill:"#fff", "font-size":"18", "font-weight":"800", "paint-order":"stroke", stroke:"rgba(0,0,0,0.4)", "stroke-width":"3" });
svg2.appendChild(regionLbl2);
wrapper2.appendChild(svg2);
// ══════════════════════════════════════════════════════
// STATS & FORMULA
// ══════════════════════════════════════════════════════
const statsRow2 = document.createElement("div");
statsRow2.style.cssText = "display:flex;gap:16px;width:100%;flex-wrap:wrap;margin-bottom:16px;";
function makeStatBox2(borderColor, bgColor) {
const box2 = document.createElement("div");
box2.style.cssText = `flex:1;min-width:200px;padding:14px 18px;border-radius:12px;
background:${bgColor};border:2px solid ${borderColor};
font-size:15px;line-height:2.0;
font-family:"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);`;
return box2;
}
const statLeft2 = makeStatBox2("#dbeafe", "#eff6ff");
const statRight2 = makeStatBox2("#fef3c7", "#fffbeb");
statsRow2.appendChild(statLeft2); statsRow2.appendChild(statRight2);
wrapper2.appendChild(statsRow2);
const formulaBox2 = document.createElement("div");
formulaBox2.style.cssText = `width:100%;padding:18px 22px;border-radius:12px;
background:#fff;border:2px solid #e2e8f0;margin-bottom:12px;
font-size:15px;line-height:2.2;
font-family:"SF Mono",SFMono-Regular,Menlo,Consolas,monospace;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);`;
wrapper2.appendChild(formulaBox2);
// ══════════════════════════════════════════════════════
// GEOMETRY
// ══════════════════════════════════════════════════════
const AX2 = OX2 + OW2 * 0.37;
const BX2 = OX2 + OW2 * 0.63;
const CY2 = OY2 + OH2 * 0.5;
const DIST2 = BX2 - AX2;
function radiusFromP2(p) {
return Math.sqrt(p * OMEGA_AREA2 / Math.PI);
}
function circleOverlapArea2(r1, r2, d) {
if (d >= r1 + r2) return 0;
if (d <= Math.abs(r1 - r2)) return Math.PI * Math.min(r1, r2) ** 2;
const a = (r1 * r1 - r2 * r2 + d * d) / (2 * d);
const b = d - a;
return r1 * r1 * Math.acos(Math.max(-1, Math.min(1, a / r1)))
- a * Math.sqrt(Math.max(0, r1 * r1 - a * a))
+ r2 * r2 * Math.acos(Math.max(-1, Math.min(1, b / r2)))
- b * Math.sqrt(Math.max(0, r2 * r2 - b * b));
}
function omegaMinusCirclePath2(cx, cy, r) {
const x1 = OX2, y1 = OY2, x2 = OX2 + OW2, y2 = OY2 + OH2;
return `M${x1},${y1} L${x2},${y1} L${x2},${y2} L${x1},${y2} Z ` +
`M${cx + r},${cy} ` +
`A${r},${r} 0 1,0 ${cx - r},${cy} ` +
`A${r},${r} 0 1,0 ${cx + r},${cy} Z`;
}
// ══════════════════════════════════════════════════════
// UPDATE
// ══════════════════════════════════════════════════════
const fmt2 = v => v.toFixed(2);
const fmt3_2 = v => v.toFixed(3);
function update2() {
const pA2 = SL2.pA.val();
const pB2 = SL2.pB.val();
const rA2 = radiusFromP2(pA2);
const rB2 = radiusFromP2(pB2);
// Set circle positions/sizes
[circA2, circAOutline2, unionFillA2].forEach(el2 => {
el2.setAttribute("cx", AX2); el2.setAttribute("cy", CY2); el2.setAttribute("r", rA2);
});
[circB2, circBOutline2, unionFillB2].forEach(el2 => {
el2.setAttribute("cx", BX2); el2.setAttribute("cy", CY2); el2.setAttribute("r", rB2);
});
clipACirc2.setAttribute("cx", AX2); clipACirc2.setAttribute("cy", CY2); clipACirc2.setAttribute("r", rA2);
clipBCirc2.setAttribute("cx", BX2); clipBCirc2.setAttribute("cy", CY2); clipBCirc2.setAttribute("r", rB2);
interFill2.setAttribute("cx", BX2); interFill2.setAttribute("cy", CY2); interFill2.setAttribute("r", rB2);
pathNotA2.setAttribute("d", omegaMinusCirclePath2(AX2, CY2, rA2));
pathNotA2.setAttribute("fill-rule", "evenodd");
pathNotB2.setAttribute("d", omegaMinusCirclePath2(BX2, CY2, rB2));
pathNotB2.setAttribute("fill-rule", "evenodd");
const overlapPx2 = circleOverlapArea2(rA2, rB2, DIST2);
const pAB2 = overlapPx2 / OMEGA_AREA2;
// Visibility toggles for elegant complement views
// If exploring complement of A, hide B entirely to reduce visual noise.
const hideB2 = activeRegion2 === "compA";
const hideA2 = activeRegion2 === "compB";
circA2.style.opacity = hideA2 ? "0" : "0.15";
circAOutline2.style.opacity = hideA2 ? "0" : "1";
lblA2.style.opacity = hideA2 ? "0" : "1";
circB2.style.opacity = hideB2 ? "0" : "0.15";
circBOutline2.style.opacity = hideB2 ? "0" : "1";
lblB2.style.opacity = hideB2 ? "0" : "1";
const HOPA2 = 0.4;
interFill2.style.opacity = activeRegion2 === "inter" ? HOPA2 : "0";
unionFillA2.style.opacity = activeRegion2 === "union" ? HOPA2 : "0";
unionFillB2.style.opacity = activeRegion2 === "union" ? HOPA2 : "0";
compAFill2.style.opacity = activeRegion2 === "compA" ? 0.12 : "0";
compBFill2.style.opacity = activeRegion2 === "compB" ? 0.12 : "0";
const hasOverlap2 = pAB2 > 0.01;
const aLblX2 = hasOverlap2 ? Math.max(OX2 + 45, AX2 - rA2 * 0.45) : AX2;
const bLblX2 = hasOverlap2 ? Math.min(OX2 + OW2 - 45, BX2 + rB2 * 0.45) : BX2;
lblA2.setAttribute("x", aLblX2); lblA2.setAttribute("y", CY2 + 5);
lblA2.textContent = `P(A) = ${fmt2(pA2)}`;
lblB2.setAttribute("x", bLblX2); lblB2.setAttribute("y", CY2 + 5);
lblB2.textContent = `P(B) = ${fmt2(pB2)}`;
const pUnion2 = pA2 + pB2 - pAB2;
const pCompA2 = 1 - pA2;
const pCompB2 = 1 - pB2;
let regionVal2, regionX2, regionY2;
if (activeRegion2 === "inter") {
regionVal2 = pAB2;
regionX2 = (AX2 + BX2) / 2;
regionY2 = CY2 - 20;
} else if (activeRegion2 === "union") {
regionVal2 = pUnion2;
regionX2 = (AX2 + BX2) / 2;
regionY2 = OY2 + 85;
} else if (activeRegion2 === "compA") {
regionVal2 = pCompA2;
regionX2 = OX2 + OW2 - 70;
regionY2 = OY2 + 85;
} else {
regionVal2 = pCompB2;
regionX2 = OX2 + 70;
regionY2 = OY2 + 85;
}
regionLbl2.setAttribute("x", regionX2);
regionLbl2.setAttribute("y", regionY2);
regionLbl2.style.opacity = regionVal2 < 0.001 ? "0" : "1";
regionLbl2.textContent = fmt3_2(regionVal2);
// Conditional probabilities
const pA_B2 = pB2 > 0.001 ? pAB2 / pB2 : 0;
const pB_A2 = pA2 > 0.001 ? pAB2 / pA2 : 0;
const pApB2 = pA2 * pB2;
statLeft2.innerHTML =
`<span style="color:#ef4444;font-weight:700">P(A | B)</span> = ${fmt2(pA_B2)}<br>` +
`<span style="color:#f59e0b;font-weight:700">P(B | A)</span> = ${fmt2(pB_A2)}`;
statRight2.innerHTML =
`<span style="color:#10b981;font-weight:700">P(A ∩ B)</span> = ${fmt2(pAB2)}<br>` +
`<span style="color:#8b5cf6;font-weight:700">P(A ∪ B)</span> = ${fmt2(pUnion2)}`;
// Formula HTML
let formulaHTML2 = "";
if (activeRegion2 === "inter") {
formulaHTML2 =
`<div style="margin-bottom:8px;font-weight:800;color:#10b981;font-size:13px;text-transform:uppercase;letter-spacing:0.8px;">Quy tắc nhân (Multiplication Rule)</div>` +
`<span style="color:#10b981;font-weight:700">P(A ∩ B)</span> = ` +
`<span style="color:#ef4444">P(A | B)</span> × <span style="color:#f59e0b">P(B)</span><br>` +
`<span style="color:#10b981;font-weight:700">${fmt3_2(pAB2)}</span> = ` +
`<span style="color:#ef4444">${fmt3_2(pA_B2)}</span> × <span style="color:#f59e0b">${fmt3_2(pB2)}</span>` +
`<br><br>` +
`<span style="color:#10b981;font-weight:700">P(A ∩ B)</span> = ` +
`<span style="color:#f59e0b">P(B | A)</span> × <span style="color:#3b82f6">P(A)</span><br>` +
`<span style="color:#10b981;font-weight:700">${fmt3_2(pAB2)}</span> = ` +
`<span style="color:#f59e0b">${fmt3_2(pB_A2)}</span> × <span style="color:#3b82f6">${fmt3_2(pA2)}</span>`;
} else if (activeRegion2 === "union") {
formulaHTML2 =
`<div style="margin-bottom:8px;font-weight:800;color:#8b5cf6;font-size:13px;text-transform:uppercase;letter-spacing:0.8px;">Quy tắc cộng (Addition Rule)</div>` +
`<span style="color:#8b5cf6;font-weight:700">P(A ∪ B)</span> = ` +
`<span style="color:#3b82f6">P(A)</span> + <span style="color:#f59e0b">P(B)</span> − <span style="color:#10b981">P(A ∩ B)</span><br>` +
`<span style="color:#8b5cf6;font-weight:700">${fmt3_2(pUnion2)}</span> = ` +
`<span style="color:#3b82f6">${fmt3_2(pA2)}</span> + <span style="color:#f59e0b">${fmt3_2(pB2)}</span> − <span style="color:#10b981">${fmt3_2(pAB2)}</span>`;
if (pAB2 < 0.002) {
formulaHTML2 +=
`<br><br><div style="color:#ef4444;font-weight:700;font-size:13px;">⚠ Xung khắc: P(A ∩ B) = 0</div>` +
`<span style="color:#8b5cf6;font-weight:700">P(A ∪ B)</span> = ` +
`<span style="color:#3b82f6">P(A)</span> + <span style="color:#f59e0b">P(B)</span> = ${fmt3_2(pA2 + pB2)}`;
}
} else if (activeRegion2 === "compA") {
formulaHTML2 =
`<div style="margin-bottom:8px;font-weight:800;color:#475569;font-size:13px;text-transform:uppercase;letter-spacing:0.8px;">Phần bù (Complement)</div>` +
`<span style="color:#475569;font-weight:700">P(${overA2})</span> = 1 − <span style="color:#3b82f6">P(A)</span><br>` +
`<span style="color:#475569;font-weight:700">${fmt3_2(pCompA2)}</span> = 1 − <span style="color:#3b82f6">${fmt3_2(pA2)}</span>`;
} else {
formulaHTML2 =
`<div style="margin-bottom:8px;font-weight:800;color:#57534e;font-size:13px;text-transform:uppercase;letter-spacing:0.8px;">Phần bù (Complement)</div>` +
`<span style="color:#57534e;font-weight:700">P(${overB2})</span> = 1 − <span style="color:#f59e0b">P(B)</span><br>` +
`<span style="color:#57534e;font-weight:700">${fmt3_2(pCompB2)}</span> = 1 − <span style="color:#f59e0b">${fmt3_2(pB2)}</span>`;
}
formulaBox2.innerHTML = formulaHTML2;
// Relationship title
const eps2 = 0.002;
const isMutEx2 = pAB2 < eps2;
const isIndep2 = Math.abs(pAB2 - pApB2) < eps2 && !isMutEx2;
if (isMutEx2) {
titleText2.textContent = "Xung khắc (Mutually Exclusive)";
titleText2.setAttribute("fill", "#ef4444");
} else if (isIndep2) {
titleText2.textContent = "Độc lập (Independent)";
titleText2.setAttribute("fill", "#10b981");
} else {
titleText2.textContent = "Phụ thuộc (Dependent)";
titleText2.setAttribute("fill", "#d97706");
}
}
// ══════════════════════════════════════════════════════
// EVENTS
// ══════════════════════════════════════════════════════
function onInputA2() {
SL2.pA.sync();
const pA2 = SL2.pA.val();
const maxB2 = Math.max(0.01, +(1 - pA2).toFixed(2));
const curB2 = SL2.pB.val();
if (curB2 > maxB2) {
SL2.pB.update(maxB2, null, maxB2);
} else {
SL2.pB.update(curB2, null, maxB2);
}
update2();
}
function onInputB2() {
SL2.pB.sync();
const pB2 = SL2.pB.val();
const maxA2 = Math.max(0.01, +(1 - pB2).toFixed(2));
const curA2 = SL2.pA.val();
if (curA2 > maxA2) {
SL2.pA.update(maxA2, null, maxA2);
} else {
SL2.pA.update(curA2, null, maxA2);
}
update2();
}
SL2.pA.input.addEventListener("input", onInputA2);
SL2.pB.input.addEventListener("input", onInputB2);
regionDefs2.forEach(rd2 => {
regionBtns2[rd2.id].addEventListener("click", () => {
activeRegion2 = rd2.id;
styleButtons2();
update2();
});
});
styleButtons2();
onInputA2();
invalidation.then(() => {
SL2.pA.input.removeEventListener("input", onInputA2);
SL2.pB.input.removeEventListener("input", onInputB2);
});
wrapper2.value = {};
return wrapper2;
})()1.6.4 Ứng dụng
viewof probSampling = {
// ═══════════════════ PALETTE ═══════════════════
const C = {
pos:'#E53935', posLight:'#FFCDD2',
neg:'#1E88E5', negLight:'#BBDEFB',
txt:'#1A1A2E', sub:'#78909C', light:'#E8EDF2',
card:'#FFFFFF', bg:'#F7F8FC',
mult:'#6A1B9A', multLight:'#E1BEE7',
add:'#00796B', addLight:'#B2DFDB',
gold:'#F9A825',
};
const mono = "'SF Mono',SFMono-Regular,Menlo,monospace";
// ═══════════════════ CRYPTO RNG ═══════════════════
function rndFloat(){const a=new Uint32Array(1);crypto.getRandomValues(a);return a[0]/4294967296;}
// ═══════════════════ HELPERS ═══════════════════
function choose(n,k){
if(k>n||k<0)return 0;if(k===0||k===n)return 1;
let r=1;for(let i=0;i<k;i++)r=r*(n-i)/(i+1);return Math.round(r);
}
function fmtP(v){
if(v>=0.0001) return v.toFixed(6);
return v.toExponential(3);
}
// ═══════════════════ STATE ═══════════════════
const POP = 100;
let prevalence = 0.30;
let enrolled = [];
let population = [];
// SVG layout: population zone (top) + sequence zone (bottom)
const gridW = 880;
const popZoneH = 300; // population area height
const seqZoneY = popZoneH + 30; // where sequence starts
const seqZoneH = 40; // sequence area height
const gridH = seqZoneY + seqZoneH + 10; // total SVG height
const circleRadius = 10;
const seqRadius = 12; // slightly larger in sequence
const seqSpacing = 30; // spacing between sequence circles
const seqStartX = 30; // left margin for sequence
let simulation = null;
function regeneratePopulation() {
const nSick = Math.round(prevalence * POP);
population = Array.from({length:POP}, (_,i) => ({
id:i, sick: i < nSick,
x: circleRadius + rndFloat()*(gridW - 2*circleRadius),
y: circleRadius + rndFloat()*(popZoneH - 2*circleRadius)
}));
for(let i=population.length-1;i>0;i--){
const j=Math.floor(rndFloat()*(i+1));
const tmp=population[i].sick;
population[i].sick=population[j].sick;
population[j].sick=tmp;
}
}
regeneratePopulation();
// ═══════════════════ OUTER ═══════════════════
const outer = document.createElement("div");
outer.style.cssText = `display:flex;flex-direction:column;gap:16px;
font-family:'Inter',-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
width:100%;max-width:920px;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());
if(!document.getElementById('probAnim')){
const st=document.createElement('style');st.id='probAnim';
st.textContent=`
@keyframes chipPop{0%{transform:scale(0);opacity:0}60%{transform:scale(1.15)}100%{transform:scale(1);opacity:1}}
@keyframes pulseRing{0%{r:10;opacity:1}100%{r:28;opacity:0}}
@keyframes shake{0%,100%{transform:translateX(0)}20%,60%{transform:translateX(-4px)}40%,80%{transform:translateX(4px)}}
`;
document.head.appendChild(st);
}
// ═══════════════════ SLIDER ═══════════════════
const SL = {};
SL.prev = createSlider("Tỷ lệ bệnh (p)", 0.05, 0.80, 0.01, 0.30, C.pos, "red");
const slRow = document.createElement("div");
slRow.style.cssText = "display:flex;gap:14px;width:100%;";
slRow.append(SL.prev.el);
outer.appendChild(slRow);
// ═══════════════════ COMBINED SVG CARD ═══════════════════
const popCard = document.createElement("div");
popCard.style.cssText = `background:${C.card};border-radius:16px;border:1px solid ${C.light};
padding:14px;box-shadow:0 2px 12px rgba(0,0,0,0.04);`;
const popHeader = document.createElement("div");
popHeader.style.cssText = `display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;flex-wrap:wrap;gap:6px;`;
const popTitle = document.createElement("div");
popTitle.style.cssText = `font-size:13px;font-weight:800;color:${C.txt};`;
const popLegend = document.createElement("div");
popLegend.style.cssText = `font-size:10px;color:${C.sub};font-weight:600;display:flex;gap:10px;align-items:center;`;
popLegend.innerHTML = `<span style="display:flex;align-items:center;gap:3px;"><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${C.neg};"></span> Khỏe mạnh</span>`
+`<span style="display:flex;align-items:center;gap:3px;"><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${C.pos};"></span> Mắc bệnh</span>`;
popHeader.append(popTitle, popLegend);
const popSvg = d3.create("svg").attr("viewBox",[0,0,gridW,gridH])
.style("width","100%").style("height","auto").style("border-radius","10px").style("background","#FAFBFD");
// Sequence zone divider and label
popSvg.append("line")
.attr("x1", 10).attr("y1", popZoneH + 10).attr("x2", gridW-10).attr("y2", popZoneH + 10)
.attr("stroke", C.light).attr("stroke-width", 1).attr("stroke-dasharray","6,4");
popSvg.append("text")
.attr("x", 14).attr("y", popZoneH + 26)
.attr("fill", C.sub).attr("font-size","15px").attr("font-weight","700")
.attr("font-family","Inter,sans-serif").text("Đã chọn →");
popCard.append(popHeader, popSvg.node());
outer.appendChild(popCard);
// ═══════════════════ CONTROLS ═══════════════════
const ctrlRow = document.createElement("div");
ctrlRow.style.cssText = "display:flex;gap:8px;justify-content:center;flex-wrap:wrap;align-items:center;";
function mkBtn(text, color) {
const b = document.createElement("button");
b.style.cssText = `padding:10px 24px;border-radius:12px;border:none;
background:linear-gradient(135deg,${color},${color}dd);color:#fff;
font-size:14px;font-weight:800;cursor:pointer;font-family:inherit;
transition:all 0.15s;box-shadow:0 3px 12px ${color}44;letter-spacing:0.3px;`;
b.textContent = text;
b.addEventListener('mouseenter',()=>b.style.transform='translateY(-2px)');
b.addEventListener('mouseleave',()=>b.style.transform='');
return b;
}
const MAX_ENROLL = 7;
const btnEnroll = mkBtn("Lấy mẫu tiếp", C.pos);
const btnReset = mkBtn("Bắt đầu lại", "#90A4AE");
ctrlRow.append(btnEnroll, btnReset);
outer.appendChild(ctrlRow);
function updateEnrollBtn(){
if(enrolled.length>=MAX_ENROLL){
btnEnroll.style.opacity='0.5';
btnEnroll.style.cursor='not-allowed';
btnEnroll.style.boxShadow='none';
} else {
btnEnroll.style.opacity='1';
btnEnroll.style.cursor='pointer';
btnEnroll.style.boxShadow=`0 3px 12px ${C.pos}44`;
}
}
// ═══════════════════ SAMPLE COUNTS BAR ═══════════════════
const barCard = document.createElement("div");
barCard.style.cssText = `background:${C.card};border-radius:16px;border:1px solid ${C.light};
padding:14px 18px;box-shadow:0 2px 12px rgba(0,0,0,0.04);`;
const barTitle = document.createElement("div");
barTitle.style.cssText = `font-size:13px;font-weight:800;color:${C.txt};margin-bottom:10px;`;
barTitle.textContent = "Số lượng mẫu";
const barChartW = 800, barChartH = 90;
const barSvg = d3.create("svg").attr("viewBox",[0,0,barChartW,barChartH])
.style("width","100%").style("height","auto").style("overflow","visible");
barCard.append(barTitle, barSvg.node());
outer.appendChild(barCard);
// Persistent bar elements
const labelW=110, barH_=34, barGap=18, chartBarW=barChartW-labelW-60;
const bY1=10, bY2=bY1+barH_+barGap;
barSvg.append("text").attr("x",labelW-10).attr("y",bY1+barH_/2+1)
.attr("text-anchor","end").attr("dominant-baseline","middle")
.attr("fill",C.pos).attr("font-size","18px").attr("font-weight","700")
.attr("font-family","Inter,sans-serif").text("Mắc bệnh");
const barD = barSvg.append("rect").attr("x",labelW).attr("y",bY1)
.attr("width",0).attr("height",barH_).attr("rx",8).attr("fill",C.pos).attr("opacity",0.85);
const barDText = barSvg.append("text").attr("x",labelW+10).attr("y",bY1+barH_/2+1)
.attr("dominant-baseline","middle").attr("fill",C.txt)
.attr("font-size","18px").attr("font-weight","800").attr("font-family",mono).text("0");
barSvg.append("text").attr("x",labelW-10).attr("y",bY2+barH_/2+1)
.attr("text-anchor","end").attr("dominant-baseline","middle")
.attr("fill",C.neg).attr("font-size","18px").attr("font-weight","700")
.attr("font-family","Inter,sans-serif").text("Khỏe mạnh");
const barHRect = barSvg.append("rect").attr("x",labelW).attr("y",bY2)
.attr("width",0).attr("height",barH_).attr("rx",8).attr("fill",C.neg).attr("opacity",0.85);
const barHText = barSvg.append("text").attr("x",labelW+10).attr("y",bY2+barH_/2+1)
.attr("dominant-baseline","middle").attr("fill",C.txt)
.attr("font-size","18px").attr("font-weight","800").attr("font-family",mono).text("0");
// ═══════════════════ MULTIPLICATION CARD ═══════════════════
const multCard = document.createElement("div");
multCard.style.cssText = `background:${C.card};border-radius:16px;border:2px solid ${C.multLight};
padding:16px 20px;box-shadow:0 2px 12px rgba(106,27,154,0.06);`;
const multHeader = document.createElement("div");
multHeader.style.cssText = `font-size:13px;font-weight:800;color:${C.mult};margin-bottom:4px;`;
multHeader.innerHTML = "Quy tắc nhân — P(đúng trình tự này)";
const multSub = document.createElement("div");
multSub.style.cssText = `font-size:11px;color:${C.sub};margin-bottom:8px;line-height:1.5;`;
multSub.textContent = "Xác suất của một trình tự kết quả cụ thể là tích của các xác suất từng bước.";
const multFormula = document.createElement("div");
multFormula.style.cssText = `font-family:${mono};font-size:13px;color:${C.txt};line-height:2;overflow-x:auto;`;
multCard.append(multHeader, multSub, multFormula);
outer.appendChild(multCard);
// ═══════════════════ ADDITION CARD ═══════════════════
const addCard = document.createElement("div");
addCard.style.cssText = `background:${C.card};border-radius:16px;border:2px solid ${C.addLight};
padding:16px 20px;box-shadow:0 2px 12px rgba(0,121,107,0.06);position:relative;`;
const addHeader = document.createElement("div");
addHeader.style.cssText = `font-size:13px;font-weight:800;color:${C.add};margin-bottom:4px;`;
addHeader.innerHTML = "Quy tắc cộng — P(k người bệnh trong n mẫu)";
const addSub = document.createElement("div");
addSub.style.cssText = `font-size:11px;color:${C.sub};margin-bottom:10px;line-height:1.5;`;
addSub.textContent = "Nhiều trình tự khác nhau cho cùng một số lượng. Các kết quả này xung khắc với nhau, nên ta cộng các xác suất lại.";
const addBody = document.createElement("div");
addCard.append(addHeader, addSub, addBody);
outer.appendChild(addCard);
// ═══════════════════ FORCE SIMULATION ═══════════════════
function initForceSimulation(){
if(simulation) simulation.stop();
const sz = circleRadius;
simulation = d3.forceSimulation(population)
.force('charge', d3.forceManyBody().strength(0.5))
.force('collide', d3.forceCollide(sz + 2).strength(1).iterations(3))
.force('x', d3.forceX(gridW/2).strength(0.02))
.force('y', d3.forceY(popZoneH/2).strength(0.03))
.force('bounds', ()=>{
population.forEach(p=>{
p.x = Math.max(sz, Math.min(gridW-sz, p.x));
p.y = Math.max(sz, Math.min(popZoneH-sz, p.y));
});
})
.alphaDecay(0.02)
.on('tick', ()=>{
popSvg.selectAll('circle.person').data(population, d=>d.id)
.attr('cx', d=>d.x).attr('cy', d=>d.y);
});
}
// ═══════════════════ DRAW POPULATION ═══════════════════
function drawPopGrid(){
const sz = circleRadius;
const sampledSet = new Set(enrolled.map(e=>e.id));
// Tooltip
const tooltip = d3.select(popSvg.node().parentNode).selectAll('.pop-tooltip').data([0]);
const ttEnter = tooltip.enter().append('div').attr('class','pop-tooltip')
.style('position','absolute').style('pointer-events','none')
.style('background','#fff').style('color',C.txt)
.style('padding','5px 12px').style('border-radius','10px')
.style('font-size','12px').style('font-weight','700')
.style('opacity','0').style('transition','opacity 0.15s')
.style('white-space','nowrap').style('z-index','10')
.style('box-shadow','0 2px 12px rgba(0,0,0,0.15)').style('border','1px solid #eee');
const tt = tooltip.merge(ttEnter);
d3.select(popSvg.node().parentNode).style('position','relative');
// Data join for population circles
const circles = popSvg.selectAll('circle.person').data(population, d=>d.id);
circles.exit().remove();
const enter = circles.enter().append('circle').attr('class','person')
.attr('cx', d=>d.x).attr('cy', d=>d.y).attr('r', sz);
const all = enter.merge(circles);
all
.attr('fill', d=>d.sick?C.pos:C.neg)
.attr('opacity', d=>sampledSet.has(d.id)?0.15:0.75)
.style('cursor','pointer');
all
.on('mouseenter', function(event, p){
const label = p.sick ? 'Mắc bệnh' : 'Khỏe mạnh';
const color = p.sick ? C.pos : C.neg;
tt.html(`<span style="color:${color};">● </span>${label}`)
.style('opacity','1');
const svgRect = popSvg.node().getBoundingClientRect();
const parentRect = popSvg.node().parentNode.getBoundingClientRect();
const scaleX = svgRect.width / gridW;
const scaleY = svgRect.height / gridH;
const px = svgRect.left - parentRect.left + p.x * scaleX;
const py = svgRect.top - parentRect.top + p.y * scaleY - sz * scaleY - 10;
tt.style('left', px + 'px').style('top', py + 'px')
.style('transform','translate(-50%,-100%)');
})
.on('mouseleave', function(){
tt.style('opacity','0');
});
const nSick=population.filter(p=>p.sick).length;
popTitle.textContent=`Quần thể: ${POP} | Bệnh: ${nSick} (${(nSick/POP*100).toFixed(0)}%) | Đã chọn: ${enrolled.length}/${MAX_ENROLL}`;
}
// ═══════════════════ DRAW SEQUENCE IN SVG ═══════════════════
function drawSequence(){
// Static sequence circles in the bottom zone
const seqCircles = popSvg.selectAll('circle.seq').data(enrolled, (d,i)=>i);
seqCircles.exit().remove();
seqCircles.enter().append('circle').attr('class','seq')
.attr('cx', (d,i)=> seqStartX + i * seqSpacing)
.attr('cy', seqZoneY + seqZoneH/2)
.attr('r', seqRadius)
.attr('fill', d=>d.sick?C.pos:C.neg)
.attr('opacity', 0.9);
// Update positions for existing
popSvg.selectAll('circle.seq').data(enrolled, (d,i)=>i)
.attr('cx', (d,i)=> seqStartX + i * seqSpacing)
.attr('cy', seqZoneY + seqZoneH/2)
.attr('fill', d=>d.sick?C.pos:C.neg);
}
// Animate a circle flying from population position to its sequence slot
function animateEnroll(person, seqIndex){
const fromX = person.x;
const fromY = person.y;
const toX = seqStartX + seqIndex * seqSpacing;
const toY = seqZoneY + seqZoneH/2;
// Remove the static seq circle for this index (we'll animate into it)
// Create a flying circle
const flyer = popSvg.append('circle').attr('class','flyer')
.attr('cx', fromX).attr('cy', fromY).attr('r', circleRadius)
.attr('fill', person.sick?C.pos:C.neg)
.attr('opacity', 0.9)
.attr('stroke', '#fff').attr('stroke-width', 2);
flyer.transition().duration(500).ease(d3.easeCubicInOut)
.attr('cx', toX).attr('cy', toY).attr('r', seqRadius)
.attr('stroke-width', 0)
.on('end', function(){
d3.select(this).remove();
// Make sure the static seq circle is visible
drawSequence();
});
}
// ═══════════════════ RENDER BAR CHART ═══════════════════
function renderBarChart(){
const n=enrolled.length;
const k=enrolled.filter(e=>e.sick).length;
const h=n-k;
const maxVal=Math.max(n, 1);
const wD = n>0 ? Math.max(4,(k/maxVal)*chartBarW) : 0;
const wH = n>0 ? Math.max(4,(h/maxVal)*chartBarW) : 0;
barD.transition().duration(350).ease(d3.easeCubicOut).attr("width", wD);
barDText.transition().duration(350).ease(d3.easeCubicOut)
.attr("x", labelW + wD + 10)
.tween("text", function(){ const cur=+this.textContent; const i=d3.interpolateRound(cur, k); return t=>{this.textContent=i(t);}; });
barHRect.transition().duration(350).ease(d3.easeCubicOut).attr("width", wH);
barHText.transition().duration(350).ease(d3.easeCubicOut)
.attr("x", labelW + wH + 10)
.tween("text", function(){ const cur=+this.textContent; const i=d3.interpolateRound(cur, h); return t=>{this.textContent=i(t);}; });
}
// ═══════════════════ RENDER MULTIPLICATION ═══════════════════
function renderMultiplication(){
const p=prevalence, q=1-p;
const n=enrolled.length;
if(n===0){
multFormula.innerHTML=`<span style="color:${C.sub};font-style:italic;">Bấm lấy mẫu để xem phép tính quy tắc nhân.</span>`;
return;
}
let product=1;
const terms=enrolled.map(e=>{
const pi=e.sick?p:q;
product*=pi;
return {val:pi, color:e.sick?C.pos:C.neg, label:e.sick?'p':'q'};
});
let html=`<div style="margin-bottom:4px;">`;
html+=`<span style="color:${C.mult};font-weight:800;">P(</span>`;
html+=enrolled.map(e=>`<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${e.sick?C.pos:C.neg};vertical-align:middle;margin:0 1px;"></span>`).join('');
html+=`<span style="color:${C.mult};font-weight:800;">)</span> = `;
html+=terms.map(t=>`<span style="color:${t.color};">${t.label}</span>`).join(' <span style="color:#bbb;">×</span> ');
html+=`</div>`;
html+=`<div>= `;
html+=terms.map(t=>`<span style="color:${t.color};">${t.val.toFixed(2)}</span>`).join(' <span style="color:#bbb;">×</span> ');
html+=` = <span style="color:${C.mult};font-weight:900;font-size:15px;">${fmtP(product)}</span></div>`;
multFormula.innerHTML=html;
}
// ═══════════════════ RENDER ADDITION ═══════════════════
function getArrangements(n,k,maxShow){
const total=choose(n,k);
const arr=[];
if(n>14||total>maxShow){return{arr:null,total};}
function gen(start,chosen){
if(chosen.length===k){arr.push([...chosen]);return;}
if(start>=n||arr.length>=maxShow)return;
gen(start+1,[...chosen,start]);
gen(start+1,chosen);
}
gen(0,[]);
return{arr,total};
}
function renderAddition(){
const p=prevalence, q=1-p;
const n=enrolled.length;
const k=enrolled.filter(e=>e.sick).length;
if(n===0){
addBody.innerHTML=`<div style="font-family:${mono};font-size:12px;color:${C.sub};font-style:italic;">Bấm lấy mẫu để xem phép tính quy tắc cộng.</div>`;
return;
}
const nCk=choose(n,k);
const pSeq=Math.pow(p,k)*Math.pow(q,n-k);
const pTotal=nCk*pSeq;
const currentPattern=enrolled.map(e=>e.sick?1:0);
let html='';
// Question
html+=`<div style="font-family:${mono};font-size:13px;color:${C.txt};margin-bottom:10px;">`;
html+=`<span style="color:${C.add};font-weight:800;">P(${k} người bệnh trong ${n} mẫu)</span> = ?`;
html+=`</div>`;
// All arrangements
const maxShow=40;
const{arr,total}=getArrangements(n,k,maxShow);
html+=`<div style="font-size:11px;font-weight:700;color:${C.add};margin-bottom:6px;">`;
if(total<=maxShow) html+=`Tất cả ${total} khả năng có ${k} người bệnh và ${n-k} khỏe mạnh:`;
else html+=`Hiển thị ${arr?arr.length:Math.min(15,total)} / ${total.toLocaleString()} khả năng:`;
html+=`</div>`;
html+=`<div style="display:flex;flex-wrap:wrap;gap:4px;max-height:220px;overflow-y:auto;padding:4px 0;">`;
if(arr){
arr.forEach(combo=>{
const pattern=Array(n).fill(0);
combo.forEach(idx=>pattern[idx]=1);
const isCurrent=pattern.every((v,i)=>v===currentPattern[i]);
html+=`<div class="arr-card" data-pattern="${pattern.join('')}" data-result="${fmtP(pSeq)}"
style="display:flex;align-items:center;gap:1px;padding:4px 6px;border-radius:8px;cursor:pointer;
border:2px solid ${isCurrent?C.gold:C.light};transition:all 0.15s;
background:${isCurrent?'#FFF8E1':C.card};
${isCurrent?'box-shadow:0 0 8px rgba(249,168,37,0.35);':''}">`;
pattern.forEach(v=>{
html+=`<span style="display:inline-block;width:9px;height:9px;border-radius:50%;
background:${v?C.pos:C.neg};margin:0 1px;"></span>`;
});
if(isCurrent) html+=`<span style="font-size:9px;margin-left:2px;">⭐</span>`;
html+=`</div>`;
});
} else {
const shown=Math.min(15,total);
for(let ci=0;ci<shown;ci++){
const pattern=Array(n).fill(0);
const slots=[...Array(n).keys()];
for(let j=slots.length-1;j>0;j--){const r=Math.floor(rndFloat()*(j+1));[slots[j],slots[r]]=[slots[r],slots[j]];}
slots.slice(0,k).forEach(idx=>pattern[idx]=1);
html+=`<div class="arr-card" data-pattern="${pattern.join('')}" data-result="${fmtP(pSeq)}"
style="display:flex;align-items:center;gap:1px;padding:4px 6px;border-radius:8px;cursor:pointer;
border:1px solid ${C.light};background:${C.card};transition:all 0.15s;">`;
pattern.forEach(v=>{
html+=`<span style="display:inline-block;width:9px;height:9px;border-radius:50%;
background:${v?C.pos:C.neg};margin:0 1px;"></span>`;
});
html+=`</div>`;
}
if(total>shown) html+=`<div style="font-size:11px;color:${C.sub};padding:4px 8px;font-weight:600;">…và ${(total-shown).toLocaleString()} trình tự khác</div>`;
}
html+=`</div>`;
// Each arrangement has probability
html+=`<div style="margin-top:12px;padding-top:10px;border-top:1px solid ${C.addLight};font-family:${mono};font-size:12px;color:${C.txt};line-height:1.9;">`;
html+=`<div>Mỗi khả năng có xác suất: <span style="color:${C.pos};">p</span><sup>${k}</sup> × <span style="color:${C.neg};">q</span><sup>${n-k}</sup> = `;
html+=`<span style="color:${C.pos};">${p.toFixed(2)}</span><sup>${k}</sup> × <span style="color:${C.neg};">${q.toFixed(2)}</span><sup>${n-k}</sup>`;
html+=` = <span style="font-weight:900;color:${C.mult};">${fmtP(pSeq)}</span></div>`;
html+=`<div style="margin-top:4px;">Tổng số tổ hợp khả năng: <span style="font-weight:900;color:${C.add};">C(${n},${k}) = ${nCk.toLocaleString()}</span></div>`;
html+=`</div>`;
// Sum
html+=`<div style="margin-top:10px;padding:10px 14px;border-radius:10px;background:${C.addLight}22;border:1px solid ${C.addLight};font-family:${mono};font-size:14px;">`;
html+=`<span style="color:${C.add};font-weight:800;">P(${k} người bệnh trong ${n} mẫu)</span> = ${nCk.toLocaleString()} × ${fmtP(pSeq)} = `;
html+=`<span style="color:${C.add};font-weight:900;font-size:17px;">${fmtP(pTotal)}</span>`;
html+=`</div>`;
addBody.innerHTML=html;
// Hover events on arrangement cards
const arrCards = addBody.querySelectorAll('.arr-card');
let arrTip = addCard.querySelector('.arr-tooltip');
if(!arrTip){
arrTip = document.createElement('div');
arrTip.className='arr-tooltip';
arrTip.style.cssText=`position:absolute;pointer-events:none;background:#fff;color:${C.txt};
padding:8px 14px;border-radius:10px;font-size:11px;font-weight:600;font-family:${mono};
opacity:0;transition:opacity 0.15s;white-space:nowrap;z-index:20;
box-shadow:0 4px 16px rgba(0,0,0,0.15);border:1px solid ${C.addLight};line-height:1.7;`;
addCard.appendChild(arrTip);
}
arrCards.forEach(card=>{
card.addEventListener('mouseenter', function(){
const pat=this.dataset.pattern;
const result=this.dataset.result;
const coloredTerms=[...pat].map(v=>{
const val=v==='1'?p.toFixed(2):q.toFixed(2);
const col=v==='1'?C.pos:C.neg;
return `<span style="color:${col};">${val}</span>`;
}).join(' <span style="color:#bbb;">×</span> ');
arrTip.innerHTML=`P = ${coloredTerms}<br>= <span style="font-weight:900;color:${C.mult};">${result}</span>`;
arrTip.style.opacity='1';
const rect=this.getBoundingClientRect();
const parentRect=addCard.getBoundingClientRect();
arrTip.style.left=(rect.left - parentRect.left + rect.width/2)+'px';
arrTip.style.top=(rect.top - parentRect.top - 8)+'px';
arrTip.style.transform='translate(-50%,-100%)';
this.style.transform='translateY(-2px)';
this.style.boxShadow='0 4px 12px rgba(0,0,0,0.12)';
});
card.addEventListener('mouseleave', function(){
arrTip.style.opacity='0';
this.style.transform='';
this.style.boxShadow='';
});
});
}
// ═══════════════════ ENROLL ═══════════════════
function enrollOne(){
if(enrolled.length>=MAX_ENROLL){
// Shake the button to indicate limit
btnEnroll.style.animation='none';
btnEnroll.offsetHeight; // reflow
btnEnroll.style.animation='shake 0.3s ease';
return;
}
const sampledIds=new Set(enrolled.map(e=>e.id));
const remaining=population.filter(p=>!sampledIds.has(p.id));
if(remaining.length===0)return;
const person=remaining[Math.floor(rndFloat()*remaining.length)];
const seqIdx = enrolled.length;
enrolled.push({id:person.id, sick:person.sick});
// Update everything except sequence (animated separately)
drawPopGrid();
renderBarChart();
renderMultiplication();
renderAddition();
// Animate the circle flying to the sequence
animateEnroll(person, seqIdx);
}
function renderAll(){
drawPopGrid();drawSequence();renderBarChart();renderMultiplication();renderAddition();updateEnrollBtn();
}
// ═══════════════════ EVENTS ═══════════════════
btnEnroll.addEventListener('click',()=>enrollOne());
btnReset.addEventListener('click',()=>{
enrolled=[];SL.prev.sync();prevalence=SL.prev.val();
// Clear sequence circles and flyers
popSvg.selectAll('circle.seq').remove();
popSvg.selectAll('circle.flyer').remove();
regeneratePopulation();initForceSimulation();renderAll();
});
SL.prev.input.addEventListener("input",()=>{
SL.prev.sync();prevalence=SL.prev.val();
enrolled=[];
popSvg.selectAll('circle.seq').remove();
popSvg.selectAll('circle.flyer').remove();
regeneratePopulation();initForceSimulation();renderAll();
});
initForceSimulation();
renderAll();
outer.value={};return outer;
}1.7 Biến ngẫu nhiên
Trong các phép thử ngẫu nhiên, kết quả của không gian mẫu (\(\Omega\)) thường là những khái niệm mang tính mô tả.
Ví dụ:
Khi tung một đồng xu \(\Omega = \{\text{Sấp}, \text{Ngửa}\}\)
Khi làm một xét nghiệm \(\Omega = \{\text{Dương tính}, \text{Âm tính}\}\)
Về bản chất, hai ví dụ trên có chung đặc điểm là một phép thử có thể xảy ra hai trường hợp. Chúng ta có thể viết \(\Omega = \{0, 1\}\) để tổng quát hóa cả hai ví dụ trên.
Cái tên “Biến ngẫu nhiên” là một thuật ngữ lịch sử rất dễ gây hiểu lầm. Bản chất của nó không phải là một “biến” (như biến \(x\) trong phương trình đại số), và bản thân nó cũng không hề “ngẫu nhiên”.
Biến ngẫu nhiên một hàm số (function) có nhiệm vụ gán một số thực cho mỗi điểm mẫu trong không gian mẫu của một phép thử ngẫu nhiên.
\[X: \Omega \rightarrow \mathbb{R}\] Đo lường biến ngẫu nhiên
Biến ngẫu nhiên được chia thành 4 loại dựa theo loại phép tính được thực hiện mà không làm sai lệch ý nghĩa của dữ liệu (Stevens 1946; Daniel and Cross 2018).
| Biến | Phép toán cơ bản | Ý nghĩa thống kê | Ví dụ |
|---|---|---|---|
| Danh định (nominal) | Xác định sự bằng nhau \(=, \ne\) | Phân loại | Giới tính, nhóm máu |
| Thứ bậc (ordinal) | Xác định lớn hơn, nhỏ hơn \(>, <\) | Xếp hạng | Mức độ đau, thang Likert |
| Khoảng (interval) | Xác định khoảng cách chênh lệch \(+, -\) | Chênh lệch | Nhiệt độ, điểm IQ |
| Tỉ số (ratio) | Xác định gấp bao nhiêu lần \(\times, \div\) | Gấp lần | Số lượng bạch cầu, cân nặng, chiều cao |
Thang đo cao hơn sẽ giữ lại tất cả đặc tính của thang đo thấp hơn và thêm vào đặc tính mới. Ví dụ: Khoảng thời gian là biến tỉ số, người A chạy mất 30 phút, người B chạy mất 10 phút:
- 30 phút \(\neq\) 10 phút (danh định)
- 30 phút \(>\) 10 phút (thứ bậc)
- A chạy chậm hơn B 20 phút (khoảng)
- A chạy chậm gấp 3 lần B (tỉ số)
Vì biến cấp cao hơn chứa mọi đặc tính của biến cấp thấp hơn, nên luôn có thể hạ cấp dữ liệu (biến đổi từ cao xuống thấp), nhưng không thể làm ngược lại.
Luôn thu thập dữ liệu ở thang đo cao nhất có thể (tỉ số hoặc khoảng), vì từ thang cao có thể quy đổi xuống thấp tùy thích, nhưng không làm ngược lại được.
Ví dụ: Thu thập tuổi thay vì chia thành nhóm tuổi (0-5, 5-10, >10), hay danh định (trẻ em, người lớn).
1.8 Ứng dụng
viewof bauCua = {
const SYM = [
{id:'deer', emoji:'🦌', bg:'#EFEBE9', bdr:'#A1887F'},
{id:'gourd', emoji:'🍐', bg:'#FFF3E0', bdr:'#FB8C00'},
{id:'cock', emoji:'🐓', bg:'#FFEBEE', bdr:'#E53935'},
{id:'fish', emoji:'🐟', bg:'#E3F2FD', bdr:'#1E88E5'},
{id:'crab', emoji:'🦀', bg:'#E0F7FA', bdr:'#00ACC1'},
{id:'prawn', emoji:'🦐', bg:'#FFFDE7', bdr:'#FDD835'},
];
const C={red:'#C41E3A',gold:'#D4A017',goldL:'#F5D061',
win:'#2E7D32',lose:'#C62828',felt:'#1a5c2a',feltL:'#2d7a3e',
txt:'#212121',sub:'#9E9E9E',border:'#E0E0E0'};
const mono="'SF Mono',SFMono-Regular,Menlo,monospace";
const K=10; // unit = 10K
function fmt(v){return v+'K';}
// ═══════════════════ CRYPTO RNG ═══════════════════
function rndInt(n){const a=new Uint32Array(1);crypto.getRandomValues(a);return a[0]%n;}
function rndDie(){return rndInt(6);}
// ═══════════════════ STATE ═══════════════════
let balance=1000,netProfit=0,bets={},rolling=false,autoPlaying=false,autoTimer=null;
let stats={games:0,wagered:0,returned:0};
let chartData=[];
const SPEED_MS=[700,250,50];
// ═══════════════════ 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:860px;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());
// ═══════════════════ WALLET ═══════════════════
const walletRow=document.createElement("div");
walletRow.style.cssText="display:flex;gap:10px;justify-content:center;flex-wrap:wrap;";
function mkChip(icon,color){
const d=document.createElement("div");
d.style.cssText=`padding:6px 16px;border-radius:30px;font-weight:800;font-size:14px;
background:#fff;border:2.5px solid ${color};color:${color};
box-shadow:0 2px 8px rgba(0,0,0,0.06);font-family:${mono};display:flex;gap:5px;align-items:center;`;
const lbl=document.createElement("span");lbl.textContent=icon;lbl.style.fontSize='13px';
const val=document.createElement("span");val.style.cssText=`color:${C.txt};font-size:16px;`;val.textContent='0';
d.append(lbl,val);return{el:d,val};
}
const chipBal=mkChip('💰',C.gold);
const chipBet=mkChip('🎯',C.red);
const chipNet=mkChip('📊 Ròng:','#43A047');
walletRow.append(chipBal.el,chipBet.el,chipNet.el);
outer.appendChild(walletRow);
// ═══════════════════ SLIDERS ═══════════════════
const SL={};
SL.unit=createSlider("Cược mỗi lần",1,10,1,5,C.red,"red");
// Override display format
const baseSync=SL.unit.sync;
SL.unit.sync=()=>{baseSync();SL.unit.valSpan.textContent=(SL.unit.val()*10)+'K';};
SL.unit.sync();
SL.speed=createSlider("Tốc độ tự động",1,3,1,2,"#6A1B9A","purple");
const slRow=document.createElement("div");slRow.style.cssText="display:flex;gap:14px;width:100%;";
slRow.append(SL.unit.el,SL.speed.el);
outer.appendChild(slRow);
// ═══════════════════ BOARD ═══════════════════
const boardWrap=document.createElement("div");
boardWrap.style.cssText=`background:linear-gradient(145deg,${C.feltL},${C.felt});
border-radius:18px;border:4px solid ${C.gold};padding:12px;
box-shadow:0 0 0 2px #8B6914,0 6px 24px rgba(0,0,0,0.15);`;
const boardGrid=document.createElement("div");
boardGrid.style.cssText="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;";
boardWrap.appendChild(boardGrid);
outer.appendChild(boardWrap);
const cells={};
SYM.forEach(s=>{
const cell=document.createElement("div");
cell.style.cssText=`background:${s.bg};border-radius:14px;border:3px solid ${s.bdr};
display:flex;flex-direction:column;align-items:center;justify-content:center;
padding:8px 6px 6px;position:relative;user-select:none;
box-shadow:0 2px 8px rgba(0,0,0,0.1);transition:box-shadow 0.3s,border-color 0.2s;
min-height:100px;cursor:pointer;`;
cell.addEventListener('mouseenter',()=>{if(!rolling)cell.style.transform='translateY(-2px)';});
cell.addEventListener('mouseleave',()=>{cell.style.transform='';});
const em=document.createElement("div");
em.style.cssText="font-size:42px;line-height:1;margin-bottom:6px;";
em.textContent=s.emoji;
const betRow=document.createElement("div");
betRow.style.cssText="display:flex;align-items:center;gap:3px;";
function mkSBtn(label,bg,action){
const b=document.createElement("button");
b.style.cssText=`width:30px;height:28px;border-radius:8px;border:none;
background:${bg};color:#fff;font-size:17px;font-weight:800;cursor:pointer;
display:flex;align-items:center;justify-content:center;transition:all 0.1s;
box-shadow:0 1px 4px rgba(0,0,0,0.12);line-height:1;`;
b.textContent=label;
b.addEventListener('click',e=>{e.stopPropagation();action();});
b.addEventListener('mouseenter',()=>b.style.transform='scale(1.12)');
b.addEventListener('mouseleave',()=>b.style.transform='');
return b;
}
const btnMinus=mkSBtn('−','#90A4AE',()=>removeBet(s.id));
const btnPlus=mkSBtn('+',C.red,()=>placeBet(s.id));
const betDisp=document.createElement("div");
betDisp.style.cssText=`min-width:44px;text-align:center;font-family:${mono};
font-size:15px;font-weight:800;color:${C.sub};padding:3px 6px;
background:rgba(255,255,255,0.7);border-radius:8px;border:1.5px solid #E0E0E0;`;
betDisp.textContent='—';
betRow.append(btnMinus,betDisp,btnPlus);
const payLabel=document.createElement("div");
payLabel.style.cssText=`position:absolute;top:5px;right:5px;font-size:12px;font-weight:800;
padding:2px 8px;border-radius:10px;display:none;font-family:${mono};
box-shadow:0 2px 6px rgba(0,0,0,0.15);`;
const matchLabel=document.createElement("div");
matchLabel.style.cssText=`position:absolute;top:5px;left:5px;font-size:12px;font-weight:800;display:none;
padding:1px 6px;border-radius:6px;background:${C.win};color:#fff;font-family:${mono};`;
cell.append(em,betRow,payLabel,matchLabel);
cell.addEventListener('click',()=>placeBet(s.id));
boardGrid.appendChild(cell);
cells[s.id]={cell,betDisp,payLabel,matchLabel};
});
// ═══════════════════ DICE ═══════════════════
const diceRow=document.createElement("div");
diceRow.style.cssText="display:flex;justify-content:center;gap:16px;min-height:80px;align-items:center;padding:4px 0;";
const diceEls=[0,1,2].map(()=>{
const d=document.createElement("div");
d.style.cssText=`width:72px;height:72px;background:linear-gradient(145deg,#fff,#f0ece0);
border-radius:14px;border:3px solid #c9a84c;display:flex;align-items:center;justify-content:center;
font-size:38px;box-shadow:0 4px 12px rgba(0,0,0,0.15);transition:transform 0.08s;`;
d.textContent='🎲';diceRow.appendChild(d);return d;
});
outer.appendChild(diceRow);
// ═══════════════════ RESULT ═══════════════════
const resultEl=document.createElement("div");
resultEl.style.cssText=`text-align:center;font-weight:800;font-size:16px;min-height:26px;font-family:${mono};`;
outer.appendChild(resultEl);
// ═══════════════════ CONTROLS ═══════════════════
const ctrlRow=document.createElement("div");
ctrlRow.style.cssText="display:flex;gap:8px;justify-content:center;flex-wrap:wrap;align-items:center;";
function mkBtn(text,color,big){
const b=document.createElement("button");
b.style.cssText=`padding:${big?'12px 36px':'8px 18px'};border-radius:12px;border:none;
background:linear-gradient(135deg,${color},${color}dd);color:#fff;
font-size:${big?16:13}px;font-weight:800;cursor:pointer;font-family:inherit;
transition:all 0.15s;box-shadow:0 3px 12px ${color}44;letter-spacing:0.3px;`;
b.textContent=text;
b.addEventListener('mouseenter',()=>b.style.transform='translateY(-2px)');
b.addEventListener('mouseleave',()=>b.style.transform='');
return b;
}
const btnRoll=mkBtn("🎲 LẮC!",C.red,true);
const btnAuto=mkBtn("⏩ Tự động","#6A1B9A",false);
const stratSel=document.createElement("select");
stratSel.style.cssText=`padding:8px 12px;border-radius:10px;border:2px solid #E0E0E0;
font-family:inherit;font-size:12px;font-weight:600;background:#fff;color:${C.txt};`;
[['random1','Cược 1 ngẫu nhiên'],['random2','Cược 2 ngẫu nhiên'],['all6','Cược tất cả 6'],
['single','Luôn cược 🦌']].forEach(([v,t])=>{
const o=document.createElement("option");o.value=v;o.textContent=t;stratSel.appendChild(o);
});
const btnReset=mkBtn("↺ Đặt lại","#546E7A",false);
ctrlRow.append(btnRoll,btnAuto,stratSel,btnReset);
outer.appendChild(ctrlRow);
// ═══════════════════ STATS ═══════════════════
const statsCard=document.createElement("div");
statsCard.style.cssText=`background:#fff;border-radius:16px;border:1px solid ${C.border};
padding:16px;box-shadow:0 2px 12px rgba(0,0,0,0.04);`;
const statsTitle=document.createElement("div");
statsTitle.style.cssText=`font-size:15px;font-weight:800;color:${C.txt};margin-bottom:10px;text-align:center;`;
statsTitle.textContent='Thống kê';
statsCard.appendChild(statsTitle);
const statsGrid=document.createElement("div");
statsGrid.style.cssText="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:14px;";
const stRefs={};
function mkStat(label,key){
const d=document.createElement("div");
d.style.cssText=`background:#FAFAFA;border-radius:10px;padding:8px;text-align:center;border:1px solid #F0F0F0;`;
const lb=document.createElement("div");
lb.style.cssText="font-size:10px;font-weight:600;color:#999;text-transform:uppercase;letter-spacing:0.5px;";
lb.textContent=label;
const vl=document.createElement("div");
vl.style.cssText=`font-size:17px;font-weight:800;color:${C.txt};margin-top:2px;font-family:${mono};`;
vl.textContent='—';
d.append(lb,vl);stRefs[key]=vl;return d;
}
statsGrid.append(
mkStat('Ván','games'),mkStat('Tiền cược','wager'),mkStat('Tiền về','ret'),
mkStat('Lãi/Lỗ ròng','net'),mkStat('Hoàn vốn / ₫','actRet')
);
// Custom tooltip for Hoàn vốn
const hvBox=stRefs.actRet.parentNode;
hvBox.style.position="relative";
hvBox.style.cursor="help";
const tip=document.createElement("div");
tip.style.cssText=`position:absolute;bottom:100%;left:50%;transform:translateX(-50%);
background:#333;color:#fff;padding:6px 10px;border-radius:6px;font-size:12px;
white-space:nowrap;pointer-events:none;opacity:0;transition:opacity 0.2s;
margin-bottom:8px;box-shadow:0 2px 8px rgba(0,0,0,0.2);z-index:10;font-weight:600;`;
tip.textContent="Tổng tiền về / Tổng tiền cược";
// Arrow
const arr=document.createElement("div");
arr.style.cssText=`position:absolute;top:100%;left:50%;margin-left:-5px;
border-width:5px;border-style:solid;border-color:#333 transparent transparent transparent;`;
tip.appendChild(arr);
hvBox.appendChild(tip);
hvBox.addEventListener("mouseenter",()=>tip.style.opacity="1");
hvBox.addEventListener("mouseleave",()=>tip.style.opacity="0");
statsCard.appendChild(statsGrid);
// ── d3 SVG chart ──
const chartW=820,chartH=150;
const chartSvg=d3.create("svg").attr("viewBox",[0,0,chartW,chartH])
.style("width","100%").style("max-width",chartW+"px")
.style("border-radius","10px").style("background","#FAFAFA")
.style("border","1px solid #F0F0F0");
statsCard.appendChild(chartSvg.node());
const chartLbl=document.createElement("div");
chartLbl.style.cssText=`font-size:10px;color:${C.sub};text-align:center;margin-top:4px;font-weight:600;`;
chartLbl.textContent="";
statsCard.appendChild(chartLbl);
outer.appendChild(statsCard);
// ═══════════════════ LOGIC ═══════════════════
function totalBet(){return Object.values(bets).reduce((a,b)=>a+b,0);}
function placeBet(id){
if(rolling)return;SL.unit.sync();
const unit=SL.unit.val()*K;
if(balance<unit)return;
bets[id]=(bets[id]||0)+unit;balance-=unit;
clearPayouts();render();
}
function removeBet(id){
if(rolling)return;SL.unit.sync();
const unit=SL.unit.val()*K;
const cur=bets[id]||0;if(cur<=0)return;
const rem=Math.min(unit,cur);
bets[id]=cur-rem;if(bets[id]<=0)delete bets[id];
balance+=rem;clearPayouts();render();
}
function clearPayouts(){
SYM.forEach(s=>{
const c=cells[s.id];c.payLabel.style.display='none';c.matchLabel.style.display='none';
c.cell.style.boxShadow='0 2px 8px rgba(0,0,0,0.1)';
});
resultEl.textContent='';
}
function render(){
chipBal.val.textContent=fmt(balance);
const tb=totalBet();
chipBet.val.textContent=fmt(tb);chipBet.el.style.borderColor=tb>0?C.red:'#E0E0E0';chipBet.val.style.color=tb>0?C.red:C.sub;
chipNet.val.textContent=(netProfit>=0?'+':'')+fmt(netProfit);
chipNet.el.style.borderColor=netProfit>=0?'#43A047':C.lose;chipNet.val.style.color=netProfit>=0?'#43A047':C.lose;
SYM.forEach(s=>{
const c=cells[s.id],bet=bets[s.id]||0;
c.betDisp.textContent=bet>0?fmt(bet):'—';
c.betDisp.style.color=bet>0?C.red:C.sub;
c.betDisp.style.borderColor=bet>0?C.red:'#E0E0E0';
c.cell.style.borderColor=bet>0?C.red:s.bdr;
c.cell.style.borderWidth=bet>0?'4px':'3px';
});
btnRoll.disabled=tb===0||rolling;
btnRoll.style.opacity=btnRoll.disabled?'0.4':'1';
btnRoll.style.cursor=btnRoll.disabled?'not-allowed':'pointer';
stRefs.games.textContent=stats.games||'—';
stRefs.wager.textContent=stats.wagered?fmt(stats.wagered):'—';
stRefs.ret.textContent=stats.returned?fmt(stats.returned):'—';
const net=stats.returned-stats.wagered;
stRefs.net.textContent=stats.games?(net>=0?'+':'')+fmt(net):'—';
stRefs.net.style.color=net>=0?C.win:C.lose;
if(stats.wagered>0){
stRefs.actRet.textContent=(stats.returned/stats.wagered).toFixed(4);
stRefs.actRet.style.color=stats.returned/stats.wagered>=1?C.win:C.lose;
}
}
// ═══════════════════ ROLL ═══════════════════
function rollDice(skipAnim){
return new Promise(resolve=>{
rolling=true;clearPayouts();render();
const results=[rndDie(),rndDie(),rndDie()];
if(skipAnim){
diceEls.forEach((d,i)=>{d.textContent=SYM[results[i]].emoji;d.style.transform='';});
rolling=false;resolve(results.map(i=>SYM[i].id));return;
}
let ticks=0;const maxT=12;
const iv=setInterval(()=>{
diceEls.forEach(d=>{
d.textContent=SYM[rndDie()].emoji;
d.style.transform=ticks%2?'rotate(-7deg) scale(1.06)':'rotate(7deg) scale(0.94)';
});
ticks++;
if(ticks>=maxT){
clearInterval(iv);
diceEls.forEach((d,i)=>{d.textContent=SYM[results[i]].emoji;d.style.transform='';});
rolling=false;resolve(results.map(i=>SYM[i].id));
}
},55);
});
}
async function play(skipAnim){
if(rolling||totalBet()===0)return;
const results=await rollDice(skipAnim);
const counts={};results.forEach(id=>{counts[id]=(counts[id]||0)+1;});
let rW=0,rR=0;
for(const id in bets){
const bet=bets[id];rW+=bet;
const m=counts[id]||0;const c=cells[id];
if(m>0){
const pay=bet+bet*m;rR+=pay;balance+=pay;
c.payLabel.textContent=`+${fmt(bet*m)}`;c.payLabel.style.display='block';
c.payLabel.style.background=C.win;c.payLabel.style.color='#fff';
c.matchLabel.style.display='block';c.matchLabel.textContent=`×${m}`;
c.cell.style.boxShadow=m===1?'0 0 16px rgba(76,175,80,0.45)':
m===2?'0 0 22px rgba(255,193,7,0.55)':'0 0 28px rgba(244,67,54,0.65)';
} else {
c.payLabel.textContent=`-${fmt(bet)}`;c.payLabel.style.display='block';
c.payLabel.style.background=C.lose;c.payLabel.style.color='#fff';
}
}
const profit=rR-rW;netProfit+=profit;
stats.games++;stats.wagered+=rW;stats.returned+=rR;
if(!skipAnim){
resultEl.textContent=profit>0?`🎉 Thắng +${fmt(profit)}!`:profit===0?'↔ Hòa vốn':`💸 Thua ${fmt(Math.abs(profit))}`;
resultEl.style.color=profit>0?C.win:profit===0?C.sub:C.lose;
}
chartData.push({g:stats.games,r:stats.wagered>0?stats.returned/stats.wagered:1});
// Cap memory: thin older data when array exceeds 10k entries
if(chartData.length>10000){
const half=chartData.filter((_,i)=>i%2===0||i===chartData.length-1);
chartData.length=0;chartData.push(...half);
}
bets={};render();
const spd=SL.speed.val();
if(!autoPlaying||spd<3||chartData.length%5===0)drawChart();
}
// ═══════════════════ AUTO ═══════════════════
function toggleAuto(){
if(autoPlaying){
autoPlaying=false;clearTimeout(autoTimer);
btnAuto.textContent='⏩ Tự động';btnAuto.style.background='linear-gradient(135deg,#6A1B9A,#6A1B9Add)';
drawChart();render();return;
}
autoPlaying=true;
btnAuto.textContent='⏸ Dừng';btnAuto.style.background=`linear-gradient(135deg,${C.lose},${C.lose}dd)`;
function tick(){
if(!autoPlaying)return;
if(rolling){autoTimer=setTimeout(tick,50);return;}
SL.unit.sync();SL.speed.sync();
const unit=SL.unit.val()*K;
const st=stratSel.value;
const nBets=st==='all6'?6:st==='random2'?2:1;
if(balance<nBets*unit){toggleAuto();return;}
bets={};
if(st==='random1'){const s=SYM[rndDie()];bets[s.id]=unit;balance-=unit;}
else if(st==='random2'){
const ix=new Set();while(ix.size<2)ix.add(rndDie());
for(const i of ix){bets[SYM[i].id]=unit;balance-=unit;}
} else if(st==='all6'){SYM.forEach(s=>{bets[s.id]=unit;balance-=unit;});}
else {bets['deer']=unit;balance-=unit;}
render();
const spd=SL.speed.val();
play(spd>=3).then(()=>{if(autoPlaying)autoTimer=setTimeout(tick,SPEED_MS[spd-1]);});
}
tick();
}
// ═══════════════════ CHART ═══════════════════
const cmg={l:46,r:12,t:14,b:22};
const ciw=chartW-cmg.l-cmg.r,cih=chartH-cmg.t-cmg.b;
const chartG=chartSvg.append("g").attr("transform",`translate(${cmg.l},${cmg.t})`);
const cGridG=chartG.append("g"),cLineG=chartG.append("g"),cAxisG=chartG.append("g");
function drawChart(){
cGridG.selectAll("*").remove();cLineG.selectAll("*").remove();cAxisG.selectAll("*").remove();
if(chartData.length<2)return;
const vals=chartData.map(d=>d.r);
let yMin=Math.min(d3.min(vals),0.82),yMax=Math.max(d3.max(vals),1.08);
const pad=(yMax-yMin)*0.1;yMin-=pad;yMax+=pad;
const xS=d3.scaleLinear().domain([0,chartData.length-1]).range([0,ciw]);
const yS=d3.scaleLinear().domain([yMin,yMax]).range([cih,0]);
// Sparse y ticks: every 0.20
for(let v=Math.ceil(yMin*10)/10;v<=yMax;v+=0.20){
cGridG.append("line").attr("x2",ciw).attr("y1",yS(v)).attr("y2",yS(v)).attr("stroke","#E8E8E8");
cAxisG.append("text").attr("x",-6).attr("y",yS(v)+3)
.attr("text-anchor","end").attr("font-size",12).attr("fill","#777").attr("font-family",mono).text(v.toFixed(2));
}
if(yMin<1&&yMax>1){
cGridG.append("line").attr("x2",ciw).attr("y1",yS(1)).attr("y2",yS(1))
.attr("stroke","#BDBDBD").attr("stroke-dasharray","4,4");
cGridG.append("text").attr("x",4).attr("y",yS(1)-5).attr("font-size",11).attr("fill","#999").text("Hòa vốn");
}
let pts=chartData;
if(pts.length>600){const step=Math.ceil(pts.length/600);pts=pts.filter((_,i)=>i%step===0||i===chartData.length-1);}
const xSp=d3.scaleLinear().domain([0,pts.length-1]).range([0,ciw]);
cLineG.append("path").attr("d",d3.area().x((_,i)=>xSp(i)).y0(cih).y1(d=>yS(d.r))(pts))
.attr("fill",C.gold).attr("opacity",0.1);
cLineG.append("path").attr("d",d3.line().x((_,i)=>xSp(i)).y(d=>yS(d.r))(pts))
.attr("fill","none").attr("stroke",C.gold).attr("stroke-width",2);
const last=chartData[chartData.length-1];
cLineG.append("circle").attr("cx",xS(chartData.length-1)).attr("cy",yS(last.r))
.attr("r",3.5).attr("fill",C.gold).attr("stroke","#fff").attr("stroke-width",1.5);
const xStep=Math.max(1,Math.floor(chartData.length/7));
for(let i=0;i<chartData.length;i+=xStep)
cAxisG.append("text").attr("x",xS(i)).attr("y",cih+16)
.attr("text-anchor","middle").attr("font-size",11).attr("fill","#999").attr("font-family",mono).text(chartData[i].g);
}
// ═══════════════════ EVENTS ═══════════════════
btnRoll.addEventListener('click',()=>play(false));
btnAuto.addEventListener('click',toggleAuto);
SL.unit.input.addEventListener("input",()=>SL.unit.sync());
SL.speed.input.addEventListener("input",()=>SL.speed.sync());
btnReset.addEventListener('click',()=>{
if(autoPlaying)toggleAuto();
balance=1000;netProfit=0;bets={};stats={games:0,wagered:0,returned:0};chartData=[];rolling=false;
diceEls.forEach(d=>{d.textContent='🎲';d.style.transform='';});
clearPayouts();render();drawChart();
});
render();
invalidation.then(()=>{autoPlaying=false;clearTimeout(autoTimer);});
outer.value={};return outer;
}Luật chơi:
- Bàn cược có 6 ô: Bầu, Cua, Tôm, Cá, Gà, Nai.
- Người chơi đặt tiền vào một hoặc nhiều ô dự đoán, nhà cái lắc 3 viên xí ngầu
- Trả thưởng:
- Ô đã chọn không xuất hiện: mất tiền cược
- Xuất hiện 1 lần: hoàn vốn + thưởng x1
- Xuất hiện 2 lần: hoàn vốn + thưởng x2
- Xuất hiện 3 lần: hoàn vốn + thưởng x3
Đây có phải 1 trò chơi công bằng không? Tại sao?
Nếu bắt đầu với 1 triệu đồng, mỗi ván cược 50K, thì sau bao nhiêu ván bạn sẽ mất toàn bộ 1 triệu hoặc thu được thêm 1 triệu đem về?
Nếu nhà cái muốn thu hút người chơi hơn bằng cách tăng thưởng: Khi ra 3 con vật giống nhau, thay vì trả 1:3, nhà cái trả 1:10. Hỏi nhà cái có bị lỗ không?