Skip to content

Instantly share code, notes, and snippets.

@leonmak
Last active March 22, 2026 10:42
Show Gist options
  • Select an option

  • Save leonmak/13e6cf14b4b3cfa47f8273e62ad69dda to your computer and use it in GitHub Desktop.

Select an option

Save leonmak/13e6cf14b4b3cfa47f8273e62ad69dda to your computer and use it in GitHub Desktop.
Confusion Matrix - small explainer
<!-- saved from url=(0053)file:///Users/leonmak/Downloads/confusion-matrix.html -->
<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Confusion Matrix Visualization</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
}
canvas {
border: 1px solid #ccc;
display: block;
margin-top: 20px;
}
.slider-container {
margin: 10px 0;
}
label {
display: block;
margin-bottom: 5px;
}
</style>
</head>
<body>
<h2>Confusion Matrix Visualization</h2>
<p>
Model outputs: Positive / Negative
<br>
Reality is True / False
</p>
<p>
<strong>Legend:</strong> T = Positive prediction, F = Negative
prediction<br>
<strong>Colors:</strong> Black = Correct prediction, Red = Incorrect
prediction
</p>
<div class="slider-container">
<label>True Positives (TP): <span id="tpVal">50</span>
<input type="range" min="0" max="100" value="50" id="tpSlider">
</label>
</div>
<div class="slider-container">
<label>False Positives (FP): <span id="fpVal">10</span>
<input type="range" min="0" max="100" value="10" id="fpSlider">
</label>
</div>
<div class="slider-container">
<label>False Negatives (FN): <span id="fnVal">30</span>
<input type="range" min="0" max="100" value="30" id="fnSlider">
</label>
</div>
<div class="slider-container">
<label>True Negatives (TN): <span id="tnVal">100</span>
<input type="range" min="0" max="1000" value="100" id="tnSlider">
</label>
</div>
<div>
<b>Precision:</b> <span id="precision">0.83</span> | <b>Recall:</b>
<span id="recall">0.62</span> | <b>F1 Score:</b>
<span id="f1">0.71</span> | <b>Accuracy:</b>
<span id="accuracy">0.83</span>
</div>
<canvas id="confMatrix" width="420" height="420"></canvas>
<script>
const canvas = document.getElementById('confMatrix');
const ctx = canvas.getContext('2d');
const tpSlider = document.getElementById('tpSlider');
const fpSlider = document.getElementById('fpSlider');
const fnSlider = document.getElementById('fnSlider');
const tnSlider = document.getElementById('tnSlider');
function draw() {
const tp = +tpSlider.value;
const fp = +fpSlider.value;
const fn = +fnSlider.value;
const tn = +tnSlider.value;
// Update displayed values
document.getElementById('tpVal').textContent = tp;
document.getElementById('fpVal').textContent = fp;
document.getElementById('fnVal').textContent = fn;
document.getElementById('tnVal').textContent = tn;
// Compute metrics
const total = tp + fp + fn + tn;
const precision = tp + fp > 0 ? tp / (tp + fp) : 0;
const recall = tp + fn > 0 ? tp / (tp + fn) : 0;
const f1 = precision + recall > 0 ? 2 * precision * recall / (precision + recall) : 0;
const accuracy = total > 0 ? (tp + tn) / total : 0;
document.getElementById('precision').textContent = precision.toFixed(2);
document.getElementById('recall').textContent = recall.toFixed(2);
document.getElementById('f1').textContent = f1.toFixed(2);
document.getElementById('accuracy').textContent = accuracy.toFixed(2);
// Draw matrix
ctx.clearRect(0, 0, canvas.width, canvas.height);
const offsetX = 100;
const offsetY = 60;
const cellW = 150;
const cellH = 150;
// Column headers (Predicted)
ctx.fillStyle = '#333';
ctx.font = 'bold 14px Arial';
ctx.textAlign = 'center';
ctx.fillText('Predicted', offsetX + cellW, offsetY - 40);
ctx.font = '13px Arial';
ctx.fillText('Positive (T)', offsetX + cellW * 0.5, offsetY - 10);
ctx.fillText('Negative (F)', offsetX + cellW * 1.5, offsetY - 10);
// Row headers (Actual)
ctx.save();
ctx.translate(offsetX - 40, offsetY + cellH);
ctx.rotate(-Math.PI / 2);
ctx.font = 'bold 14px Arial';
ctx.fillText('Actual', 0, 0);
ctx.restore();
ctx.textAlign = 'right';
ctx.font = '13px Arial';
ctx.fillText('Positive', offsetX - 10, offsetY + cellH * 0.5 + 5);
ctx.fillText('Negative', offsetX - 10, offsetY + cellH * 1.5 + 5);
// Cells: [[TP, FN], [FP, TN]]
const cells = [
[{ val: tp, label: 'TP', correct: true }, { val: fn, label: 'FN', correct: false }],
[{ val: fp, label: 'FP', correct: false }, { val: tn, label: 'TN', correct: true }],
];
for (let r = 0; r < 2; r++) {
for (let c = 0; c < 2; c++) {
const cell = cells[r][c];
const x = offsetX + c * cellW;
const y = offsetY + r * cellH;
// Background
ctx.fillStyle = cell.correct ? '#d4edda' : '#f8d7da';
ctx.fillRect(x, y, cellW, cellH);
// Border
ctx.strokeStyle = '#999';
ctx.lineWidth = 1;
ctx.strokeRect(x, y, cellW, cellH);
// Text
ctx.textAlign = 'center';
ctx.fillStyle = cell.correct ? '#000' : '#c00';
ctx.font = 'bold 28px Arial';
ctx.fillText(cell.val, x + cellW / 2, y + cellH / 2 - 5);
ctx.font = '14px Arial';
ctx.fillText(cell.label, x + cellW / 2, y + cellH / 2 + 22);
}
}
}
tpSlider.addEventListener('input', draw);
fpSlider.addEventListener('input', draw);
fnSlider.addEventListener('input', draw);
tnSlider.addEventListener('input', draw);
draw();
</script>
<p>Loans → False Positives matter (financial loss risk).</p>
<p>
Fraud / Cancer Screening → False Negatives matter (missed diagnosis, health risk, FP retryable / reversible).
</p>
<hr style="margin: 20px 0; border: none; border-top: 1px solid #ccc;">
<h3>What is class imbalance?</h3>
<p>Class imbalance means one class vastly outnumbers the other. For example, in credit card fraud detection, 99.7% of transactions are legitimate and only 0.3% are fraudulent. The dataset is "imbalanced" because negatives dominate.</p>
<h3>Why accuracy is misleading with imbalanced data</h3>
<p>Imagine 1,000 transactions where only 3 are fraud. A lazy model that predicts <em>"not fraud"</em> for everything gets:</p>
<table border="1" cellpadding="8" cellspacing="0" style="border-collapse: collapse; margin: 10px 0; font-size: 14px;">
<tr style="background: #f0f0f0;"><th></th><th>Predicted Fraud</th><th>Predicted Not Fraud</th></tr>
<tr><th style="background: #f0f0f0;">Actual Fraud</th><td style="background: #f8d7da; color: #c00; text-align: center;">TP = 0</td><td style="background: #f8d7da; color: #c00; text-align: center;">FN = 3</td></tr>
<tr><th style="background: #f0f0f0;">Actual Not Fraud</th><td style="text-align: center;">FP = 0</td><td style="background: #d4edda; text-align: center;">TN = 997</td></tr>
</table>
<ul>
<li><strong>Accuracy = 99.7%</strong> — looks great, but the model catches zero fraud!</li>
<li><strong>Precision = 0/0</strong> — undefined (never predicted positive)</li>
<li><strong>Recall = 0/3 = 0%</strong> — missed every fraud case</li>
<li><strong>F1 = 0%</strong> — correctly reflects the model is useless</li>
</ul>
<h3>What F1 does</h3>
<p>F1 is the <em>harmonic mean</em> of Precision and Recall. It only scores high when <strong>both</strong> are high — you can't cheat it by being good at just one.</p>
<p style="font-family: monospace; background: #f5f5f5; padding: 8px 12px; display: inline-block; border-radius: 4px;">F1 = 2 × (Precision × Recall) / (Precision + Recall)</p>
<p>Use F1 when: (1) your classes are imbalanced, and (2) both false positives and false negatives matter roughly equally. If one type of error is much worse than the other, focus on Precision or Recall directly instead.</p>
<p><strong>Try it above:</strong> Set TP=0, FP=0, FN=3, TN=997 to see the "lazy model" example. Then increase TP and watch F1 respond.</p>
</body></html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment