const controls = { classMode: document.getElementById("classMode"), threshold: document.getElementById("threshold"), thresholdValue: document.getElementById("thresholdValue"), ruleText: document.getElementById("ruleText"), checkBtn: document.getElementById("checkBtn"), showSolutionBtn: document.getElementById("showSolutionBtn"), newSamplesBtn: document.getElementById("newSamplesBtn"), resetBtn: document.getElementById("resetBtn"), }; const sampleArea = document.getElementById("sampleArea"); const matrixArea = document.getElementById("matrixArea"); const metricArea = document.getElementById("metricArea"); const feedbackStats = document.getElementById("feedbackStats"); const solutionBlock = document.getElementById("solutionBlock"); const solutionText = document.getElementById("solutionText"); let binarySamples = []; let threeClassSamples = []; function fmt(num, digits = 2) { return Number(num).toFixed(digits); } function round3(num) { return Number(num).toFixed(3); } function round2(num) { return Math.round(num * 100) / 100; } function rand(min, max) { return min + Math.random() * (max - min); } function generateBinarySamples() { const actualPattern = ["Positive", "Negative", "Positive", "Negative", "Positive", "Negative"]; const samples = []; for (let i = 0; i < 6; i += 1) { const actual = actualPattern[i]; const feature = round2(rand(0.2, 2.6)); const pPos = actual === "Positive" ? round2(rand(0.38, 0.92)) : round2(rand(0.12, 0.78)); samples.push({ id: `S${i + 1}`, feature, actual, pPos }); } binarySamples = samples; } function generateThreeProbabilities(actual) { const labels = ["A", "B", "C"]; const weights = { A: rand(0.1, 0.45), B: rand(0.1, 0.45), C: rand(0.1, 0.45), }; weights[actual] += rand(0.18, 0.5); const total = labels.reduce((sum, label) => sum + weights[label], 0); let pA = round2(weights.A / total); let pB = round2(weights.B / total); if (pA + pB > 0.98) { const scale = 0.98 / (pA + pB); pA = round2(pA * scale); pB = round2(pB * scale); } const pC = round2(1 - pA - pB); return { pA, pB, pC }; } function generateThreeClassSamples() { const actualPattern = ["A", "B", "C", "B", "C", "A"]; const samples = []; for (let i = 0; i < 6; i += 1) { const actual = actualPattern[i]; const feature = round2(rand(0.5, 2.4)); const probs = generateThreeProbabilities(actual); samples.push({ id: `S${i + 1}`, feature, actual, ...probs }); } threeClassSamples = samples; } function generateAllSamples() { generateBinarySamples(); generateThreeClassSamples(); } function computeBinaryPrediction(sample, threshold) { return sample.pPos >= threshold ? "Positive" : "Negative"; } function computeThreePrediction(sample, threshold) { if (sample.pA >= threshold) return "A"; if (sample.pB >= threshold) return "B"; return "C"; } function computeBinaryResults(threshold) { const rows = binarySamples.map((sample) => { const predicted = computeBinaryPrediction(sample, threshold); return { ...sample, predicted }; }); let tp = 0; let fp = 0; let tn = 0; let fn = 0; for (const row of rows) { if (row.actual === "Positive" && row.predicted === "Positive") tp += 1; if (row.actual === "Negative" && row.predicted === "Positive") fp += 1; if (row.actual === "Negative" && row.predicted === "Negative") tn += 1; if (row.actual === "Positive" && row.predicted === "Negative") fn += 1; } const precision = tp + fp === 0 ? 0 : tp / (tp + fp); const recall = tp + fn === 0 ? 0 : tp / (tp + fn); const sensitivity = recall; const specificity = tn + fp === 0 ? 0 : tn / (tn + fp); return { rows, matrix: { Positive: { Positive: tp, Negative: fn }, Negative: { Positive: fp, Negative: tn }, }, metrics: { precision, recall, sensitivity, specificity }, }; } function getOneVsRestMetrics(matrix, className) { const classes = ["A", "B", "C"]; const tp = matrix[className][className]; let fp = 0; let fn = 0; for (const c of classes) { if (c !== className) { fp += matrix[c][className]; fn += matrix[className][c]; } } const total = classes.reduce((sum, r) => sum + classes.reduce((acc, c) => acc + matrix[r][c], 0), 0); const tn = total - tp - fp - fn; const precision = tp + fp === 0 ? 0 : tp / (tp + fp); const recall = tp + fn === 0 ? 0 : tp / (tp + fn); const specificity = tn + fp === 0 ? 0 : tn / (tn + fp); return { precision, recall, sensitivity: recall, specificity }; } function computeThreeResults(threshold) { const rows = threeClassSamples.map((sample) => { const predicted = computeThreePrediction(sample, threshold); return { ...sample, predicted }; }); const classes = ["A", "B", "C"]; const matrix = { A: { A: 0, B: 0, C: 0 }, B: { A: 0, B: 0, C: 0 }, C: { A: 0, B: 0, C: 0 } }; for (const row of rows) { matrix[row.actual][row.predicted] += 1; } const perClass = classes.map((className) => getOneVsRestMetrics(matrix, className)); const macro = { precision: perClass.reduce((s, m) => s + m.precision, 0) / classes.length, recall: perClass.reduce((s, m) => s + m.recall, 0) / classes.length, sensitivity: perClass.reduce((s, m) => s + m.sensitivity, 0) / classes.length, specificity: perClass.reduce((s, m) => s + m.specificity, 0) / classes.length, }; return { rows, matrix, metrics: macro, perClass }; } function getCurrentResults() { const mode = controls.classMode.value; const t = Number(controls.threshold.value); return mode === "binary" ? computeBinaryResults(t) : computeThreeResults(t); } function samplePredictionSelect(rowId, options) { return ``; } function renderSamples(results) { const mode = controls.classMode.value; const rows = results.rows; if (mode === "binary") { sampleArea.innerHTML = `
| Sample | Feature x | Actual Class | p(Positive) | Your Predicted Class |
|---|---|---|---|---|
| ${row.id} | ${fmt(row.feature)} | ${row.actual} | ${fmt(row.pPos)} | ${row.predicted} (worked example) |
| ${row.id} | ${fmt(row.feature)} | ${row.actual} | ${fmt(row.pPos)} | ${samplePredictionSelect(row.id, ["Positive", "Negative"])} |
| Sample | Feature x | Actual Class | p(A) | p(B) | p(C) | Your Predicted Class |
|---|---|---|---|---|---|---|
| ${row.id} | ${fmt(row.feature)} | ${row.actual} | ${fmt(row.pA)} | ${fmt(row.pB)} | ${fmt(row.pC)} | ${row.predicted} (worked example) |
| ${row.id} | ${fmt(row.feature)} | ${row.actual} | ${fmt(row.pA)} | ${fmt(row.pB)} | ${fmt(row.pC)} | ${samplePredictionSelect(row.id, ["A", "B", "C"])} |
Rows = Actual class, Columns = Predicted class.
| Actual \ Predicted | Positive | Negative |
|---|---|---|
| Positive | ||
| Negative |
Rows = Actual class, Columns = Predicted class.
| Actual \ Predicted | A | B | C |
|---|---|---|---|
| A | |||
| B | |||
| C |
Enter decimal values (e.g., 0.667).
| Metric | Your value |
|---|---|
| Precision | |
| Recall | |
| Sensitivity | |
| Specificity |
TP=${results.matrix.Positive.Positive}, FN=${results.matrix.Positive.Negative}, FP=${results.matrix.Negative.Positive}, TN=${results.matrix.Negative.Negative}
| Actual \\ Predicted | Positive | Negative |
|---|---|---|
| Positive | ${results.matrix.Positive.Positive} | ${results.matrix.Positive.Negative} |
| Negative | ${results.matrix.Negative.Positive} | ${results.matrix.Negative.Negative} |
Precision = TP/(TP+FP) = ${results.matrix.Positive.Positive}/(${results.matrix.Positive.Positive}+${results.matrix.Negative.Positive}) = ${round3( results.metrics.precision, )}
Recall = TP/(TP+FN) = ${results.matrix.Positive.Positive}/(${results.matrix.Positive.Positive}+${results.matrix.Positive.Negative}) = ${round3( results.metrics.recall, )}
Sensitivity = Recall = ${round3(results.metrics.sensitivity)}
Specificity = TN/(TN+FP) = ${results.matrix.Negative.Negative}/(${results.matrix.Negative.Negative}+${results.matrix.Negative.Positive}) = ${round3( results.metrics.specificity, )}
`; } function renderThreeSolution(results) { const threshold = Number(controls.threshold.value); const steps = results.rows .map((row) => { return `| Actual \\ Predicted | A | B | C |
|---|---|---|---|
| A | ${results.matrix.A.A} | ${results.matrix.A.B} | ${results.matrix.A.C} |
| B | ${results.matrix.B.A} | ${results.matrix.B.B} | ${results.matrix.B.C} |
| C | ${results.matrix.C.A} | ${results.matrix.C.B} | ${results.matrix.C.C} |
Macro Precision = ${round3(results.metrics.precision)}
Macro Recall = ${round3(results.metrics.recall)}
Macro Sensitivity = ${round3(results.metrics.sensitivity)}
Macro Specificity = ${round3(results.metrics.specificity)}
`; } function showSolution() { const results = getCurrentResults(); if (controls.classMode.value === "binary") { renderBinarySolution(results); } else { renderThreeSolution(results); } solutionBlock.hidden = false; } function resetInputs() { renderPage(); solutionBlock.hidden = true; } function regenerateSamples() { generateAllSamples(); resetInputs(); } controls.classMode.addEventListener("change", resetInputs); controls.threshold.addEventListener("input", resetInputs); controls.checkBtn.addEventListener("click", checkAnswers); controls.showSolutionBtn.addEventListener("click", showSolution); controls.newSamplesBtn.addEventListener("click", regenerateSamples); controls.resetBtn.addEventListener("click", resetInputs); generateAllSamples(); renderPage();