Last active
March 22, 2026 10:42
-
-
Save leonmak/13e6cf14b4b3cfa47f8273e62ad69dda to your computer and use it in GitHub Desktop.
Confusion Matrix - small explainer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!-- 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