Skip to content

Instantly share code, notes, and snippets.

@StaffanBetner
Last active October 1, 2025 13:56
Show Gist options
  • Select an option

  • Save StaffanBetner/24dbe48c4c7f5bcfa9afa19663df722b to your computer and use it in GitHub Desktop.

Select an option

Save StaffanBetner/24dbe48c4c7f5bcfa9afa19663df722b to your computer and use it in GitHub Desktop.
Interactive Confusion Matrix
<!DOCTYPE html>
<!-- Vibe coded with Gemini 2.5 Pro -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Confusion Matrix</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
background-color: #f8f9fa;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
color: #212529;
}
.calc-table-wrapper {
overflow-x: auto;
padding: 1rem;
}
.calc-table {
border-collapse: collapse;
width: 900px;
margin: 0.5rem auto;
font-size: 0.8rem;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
border: 1px solid #d1d5db;
}
.calc-table th, .calc-table td {
border: 1px solid #d1d5db;
padding: 0.1rem;
text-align: center;
vertical-align: middle;
position: relative;
}
.calc-table td.no-border, .calc-table th.no-border {
border: none !important;
}
.input-val {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
text-align: center;
font-size: 1rem;
max-width: 120px;
margin: 0 auto;
background-color: #fff;
}
.input-val:focus {
outline: 2px solid #3b82f6;
border-color: #3b82f6;
}
.texhtml {
font-family: serif;
font-style: italic;
}
.sfrac { white-space: nowrap; }
.sfrac .tion { display: inline-block; vertical-align: -0.5em; font-size: 85%; text-align: center; }
.sfrac .num { display: block; line-height: 1em; margin: 0.0em 0.1em; border-bottom: 1px solid; }
.sfrac .den { display: block; line-height: 1em; margin: 0.1em 0.1em; }
.nowrap { white-space: nowrap; }
.vertical-header {
width: 40px;
padding: 0.5rem;
position: relative;
}
.vertical-header div {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(-90deg);
white-space: nowrap;
}
a {
color: #0d6efd;
text-decoration: none;
}
.calc-table a {
color: #0d6efd;
}
a:hover {
text-decoration: underline;
}
.result {
font-weight: bold;
font-size: 1rem;
min-height: 1.5rem;
display: inline-block;
}
/* --- NEW: TOOLTIP AND FOOTNOTE STYLES --- */
sup {
font-size: 0.7em;
vertical-align: super;
margin-left: 2px;
color: #0d6efd;
cursor: help;
}
.tooltip-container {
position: relative;
display: inline-block;
}
.tooltip-text {
visibility: hidden;
width: 220px;
background-color: #333;
color: #fff;
text-align: center;
border-radius: 6px;
padding: 8px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -110px; /* Use half of the width to center */
opacity: 0;
transition: opacity 0.3s;
font-size: 0.9rem;
font-weight: normal;
line-height: 1.4;
}
.tooltip-text::after {
content: "";
position: absolute;
top: 100%;
left: 50%;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: #333 transparent transparent transparent;
}
.tooltip-container:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
.footnotes {
max-width: 900px;
margin: 2rem auto;
padding: 0 1rem;
font-size: 0.9rem;
color: #495057;
}
.footnotes p {
margin-bottom: 0.5rem;
line-height: 1.5;
}
/* --- END OF NEW STYLES --- */
</style>
</head>
<body class="antialiased">
<header class="text-center my-4 px-4">
<h1 class="text-4xl font-bold text-gray-900">Interactive Confusion Matrix</h1>
<p class="text-lg text-gray-600 mt-2 mb-0">An interactive version of <a href="https://en.wikipedia.org/wiki/Confusion_matrix#Table_of_confusion" target="_blank" class="text-blue-600 hover:underline">the Wikipedia table</a> for evaluating binary classifiers.</p>
</header>
<div class="calc-table-wrapper">
<table class="calc-table">
<tbody>
<tr>
<td class="no-border" rowspan="2"></td>
<td class="no-border"></td>
<td style="background:#4ad2d260;" colspan="2"><b>Predicted condition</b></td>
<td class="no-border" colspan="2"></td>
</tr>
<tr>
<td style="background:#c9c9c950;">Total population <br><span class="texhtml">= P + N</span><br><span id="total_population" class="result"></span></td>
<td style="background:#78ffff60;"><b>Predicted positive</b><br><span id="predicted_positive" class="result"></span></td>
<td style="background:#1da5a560;"><b>Predicted negative</b><br><span id="predicted_negative" class="result"></span></td>
<td style="border-left:double #a2a9b1; background:#ffffff0c;"><a href="https://en.wikipedia.org/wiki/Youden%27s_J_statistic" target="_blank">Informedness</a>, <span style="font-size: 85%;">bookmaker informedness (BM)</span> <br><span class="texhtml">= TPR + TNR − 1</span><br><span id="informedness" class="result"></span></td>
<td style="background:#ffffff0c;"><a href="https://en.wikipedia.org/wiki/Prevalence_threshold" target="_blank">Prevalence threshold</a> (PT) <br><span class="texhtml"><span class="sfrac">⁠<span class="tion"><span class="num"><span class="nowrap">√<span style="border-top:1px solid; padding:0 0.1em;">TPR × FPR</span></span> − FPR</span><span class="den">TPR − FPR</span></span>⁠</span></span><br><span id="pt" class="result"></span></td>
</tr>
<tr>
<td class="vertical-header" rowspan="2" style="background:#d2d23d60;"><div><b>Actual condition</b></div></td>
<!-- NEW: Footnote [a] added -->
<td style="background:#ffff7860;"><b>Real Positive (P)</b><span class="tooltip-container"><sup>[a]</sup><span class="tooltip-text">the number of real positive cases in the data</span></span><br><span id="p_val" class="result"></span></td>
<!-- NEW: Footnote [b] added -->
<td style="background:#78ff7860;"><b><a href="https://en.wikipedia.org/wiki/True_and_false_positives#True_positive" target="_blank">True positive</a></b> (TP), <br><span style="font-size: 85%;">hit</span><span class="tooltip-container"><sup>[b]</sup><span class="tooltip-text">A test result that correctly indicates the presence of a condition or characteristic</span></span><br><input id="tp" type="number" min="0" class="input-val" oninput="calculate()"></td>
<td style="background:#ffa5a560;"><b><a href="https://en.wikipedia.org/wiki/True_and_false_positives#False_negative" target="_blank">False negative</a></b> (FN), <br><span style="font-size: 85%;">miss, underestimation</span><br><input id="fn" type="number" min="0" class="input-val" oninput="calculate()"></td>
<td style="background:#a5ffa530;"><a href="https://en.wikipedia.org/wiki/Sensitivity_(test)" target="_blank">True positive rate</a> (TPR), recall, sensitivity (SEN) <br><span class="texhtml">= <span class="sfrac">⁠<span class="tion"><span class="num">TP</span><span class="den">P</span></span>⁠</span></span> <span class="texhtml">= 1 − FNR</span><br><span id="tpr" class="result"></span></td>
<!-- NEW: Footnote [c] added -->
<td style="background:#ffa5a530;"><a href="https://en.wikipedia.org/wiki/False_negative_rate" target="_blank">False negative rate</a> (FNR), miss rate <br> type II error<span class="tooltip-container"><sup>[c]</sup><span class="tooltip-text">Type II error: A test result which wrongly indicates that a particular condition or attribute is absent</span></span><br><span class="texhtml">= <span class="sfrac">⁠<span class="tion"><span class="num">FN</span><span class="den">P</span></span>⁠</span></span> <span class="texhtml">= 1 − TPR</span><br><span id="fnr" class="result"></span></td>
</tr>
<tr>
<!-- NEW: Footnote [d] added -->
<td style="background:#a5a51d60;"><b>Real Negative (N)</b><span class="tooltip-container"><sup>[d]</sup><span class="tooltip-text">the number of real negative cases in the data</span></span><br><span id="n_val" class="result"></span></td>
<td style="background:#ff787860;"><b><a href="https://en.wikipedia.org/wiki/True_and_false_positives#False_positive" target="_blank">False positive</a></b> (FP), <br><span style="font-size: 85%;">false alarm, overestimation</span><br><input id="fp" type="number" min="0" class="input-val" oninput="calculate()"></td>
<!-- NEW: Footnote [e] added -->
<td style="background:#3dd23d60;"><b><a href="https://en.wikipedia.org/wiki/True_and_false_positives#True_negative" target="_blank">True negative</a></b> (TN), <br><span style="font-size: 85%;">correct rejection</span><span class="tooltip-container"><sup>[e]</sup><span class="tooltip-text">A test result that correctly indicates the absence of a condition or characteristic</span></span><br><input id="tn" type="number" min="0" class="input-val" oninput="calculate()"></td>
<!-- NEW: Footnote [f] added -->
<td style="background:#a54a4a30;"><a href="https://en.wikipedia.org/wiki/False_positive_rate" target="_blank">False positive rate</a> (FPR), fall-out <br> type I error<span class="tooltip-container"><sup>[f]</sup><span class="tooltip-text">Type I error: A test result which wrongly indicates that a particular condition or attribute is present</span></span><br><span class="texhtml">= <span class="sfrac">⁠<span class="tion"><span class="num">FP</span><span class="den">N</span></span>⁠</span></span> <span class="texhtml">= 1 − TNR</span><br><span id="fpr" class="result"></span></td>
<td style="background:#4aa54a30;"><a href="https://en.wikipedia.org/wiki/Specificity_(test)" target="_blank">True negative rate</a> (TNR), specificity (SPC) <br><span class="texhtml">= <span class="sfrac">⁠<span class="tion"><span class="num">TN</span><span class="den">N</span></span>⁠</span></span> <span class="texhtml">= 1 − FPR</span><br><span id="tnr" class="result"></span></td>
</tr>
<tr>
<td class="no-border" rowspan="3"></td>
<td style="border-top:double #a2a9b1; border-right:double #a2a9b1; background:#ffffff0c;"><a href="https://en.wikipedia.org/wiki/Prevalence" target="_blank">Prevalence</a> <br><span class="texhtml">= <span class="sfrac">⁠<span class="tion"><span class="num">P</span><span class="den">P + N</span></span>⁠</span></span><br><span id="prevalence" class="result"></span></td>
<td style="background:#a5ffa530;"><a href="https://en.wikipedia.org/wiki/Positive_and_negative_predictive_values#Positive_predictive_value" target="_blank">Positive predictive value</a> (PPV), precision <br><span class="texhtml">= <span class="sfrac">⁠<span class="tion"><span class="num">TP</span><span class="den">TP + FP</span></span>⁠</span></span> <span class="texhtml">= 1 − FDR</span><br><span id="ppv" class="result"></span></td>
<td style="background:#ffa5a530; border-right:double #a2a9b1;"><a href="https://en.wikipedia.org/wiki/False_omission_rate" target="_blank">False omission rate</a> (FOR) <br><span class="texhtml">= <span class="sfrac">⁠<span class="tion"><span class="num">FN</span><span class="den">TN + FN</span></span>⁠</span></span> <span class="texhtml">= 1 − NPV</span><br><span id="for_val" class="result"></span></td>
<td style="background:#a5a5ff30;"><a href="https://en.wikipedia.org/wiki/Likelihood_ratios_in_diagnostic_testing#Positive_likelihood_ratio" target="_blank">Positive likelihood ratio</a> (LR+) <br><span class="texhtml">= <span class="sfrac">⁠<span class="tion"><span class="num">TPR</span><span class="den">FPR</span></span>⁠</span></span><br><span id="lr_plus" class="result"></span></td>
<td style="background:#a5a5ff30;"><a href="https://en.wikipedia.org/wiki/Likelihood_ratios_in_diagnostic_testing#Negative_likelihood_ratio" target="_blank">Negative likelihood ratio</a> (LR−) <br><span class="texhtml">= <span class="sfrac">⁠<span class="tion"><span class="num">FNR</span><span class="den">TNR</span></span>⁠</span></span><br><span id="lr_minus" class="result"></span></td>
</tr>
<tr>
<td style="border-right:double #a2a9b1; background:#ffffff0c;"><a href="https://en.wikipedia.org/wiki/Accuracy_and_precision#In_binary_classification" target="_blank">Accuracy</a> (ACC) <br><span class="texhtml">= <span class="sfrac">⁠<span class="tion"><span class="num">TP + TN</span><span class="den">P + N</span></span>⁠</span></span><br><span id="acc" class="result"></span></td>
<td style="background:#a54a4a30;"><a href="https://en.wikipedia.org/wiki/False_discovery_rate" target="_blank">False discovery rate</a> (FDR) <br><span class="texhtml">= <span class="sfrac">⁠<span class="tion"><span class="num">FP</span><span class="den">TP + FP</span></span>⁠</span></span> <span class="texhtml">= 1 − PPV</span><br><span id="fdr" class="result"></span></td>
<td style="background:#4aa54a30;"><a href="https://en.wikipedia.org/wiki/Positive_and_negative_predictive_values#Negative_predictive_value" target="_blank">Negative predictive value</a> (NPV) <br><span class="texhtml">= <span class="sfrac">⁠<span class="tion"><span class="num">TN</span><span class="den">TN + FN</span></span>⁠</span></span> <span class="texhtml">= 1 − FOR</span><br><span id="npv" class="result"></span></td>
<td style="border-top:double #a2a9b1; border-right:double #a2a9b1; background:#ffffff0c;"><a href="https://en.wikipedia.org/wiki/Markedness" target="_blank">Markedness</a> (MK) <br><span class="texhtml">= PPV + NPV − 1</span><br><span id="mk" class="result"></span></td>
<td style="background:#a5a5ff30;"><a href="https://en.wikipedia.org/wiki/Diagnostic_odds_ratio" target="_blank">Diagnostic odds ratio</a> (DOR) <br><span class="texhtml">= <span class="sfrac">⁠<span class="tion"><span class="num">LR+</span><span class="den">LR−</span></span>⁠</span></span><br><span id="dor" class="result"></span></td>
</tr>
<tr>
<td style="background:#ffffff0c;"><a href="https://en.wikipedia.org/wiki/Balanced_accuracy" target="_blank">Balanced accuracy</a> (BA) <br><span class="texhtml">= <span class="sfrac">⁠<span class="tion"><span class="num">TPR + TNR</span><span class="den">2</span></span>⁠</span></span><br><span id="ba" class="result"></span></td>
<td style="border-top:double #a2a9b1; background:#ffffff0c;"><a href="https://en.wikipedia.org/wiki/F-score" target="_blank">F<sub>1</sub> score</a> <br><span class="texhtml">= <span class="sfrac">⁠<span class="tion"><span class="num">2 TP</span><span class="den">2 TP + FP + FN</span></span>⁠</span></span><br><span id="f1" class="result"></span></td>
<td style="border-top:double #a2a9b1; background:#ffffff0c;"><a href="https://en.wikipedia.org/wiki/Fowlkes%E2%80%93Mallows_index" target="_blank">Fowlkes–Mallows index</a> (FM) <br><span class="texhtml">= √<span style="border-top:1px solid; padding:0 0.1em;">PPV × TPR</span></span><br><span id="fm" class="result"></span></td>
<td style="border-top:double #a2a9b1; background:#ffffff0c;"><a href="https://en.wikipedia.org/wiki/Phi_coefficient" target="_blank">Matthews correlation coefficient</a> (MCC) <br><span class="texhtml" style="font-size:80%;">= <span class="sfrac">⁠<span class="tion"><span class="num">TP×TN − FP×FN</span><span class="den">√(TP+FP)(TP+FN)(TN+FP)(TN+FN)</span></span>⁠</span></span><br><span id="mcc" class="result"></span></td>
<td style="border-top:double #a2a9b1; background:#ffffff0c;"><a href="https://en.wikipedia.org/wiki/Jaccard_index#Jaccard_index_in_binary_classification_confusion_matrices" target="_blank">Threat score</a> (TS), Jaccard index <br><span class="texhtml">= <span class="sfrac">⁠<span class="tion"><span class="num">TP</span><span class="den">TP + FN + FP</span></span>⁠</span></span><br><span id="ts" class="result"></span></td>
</tr>
</tbody>
</table>
</div>
<!-- NEW: VISIBLE FOOTNOTES LIST -->
<div class="footnotes">
<p><b>a.</b> ^ the number of real positive cases in the data</p>
<p><b>b.</b> ^ A test result that correctly indicates the presence of a condition or characteristic</p>
<p><b>c.</b> ^ Type II error: A test result which wrongly indicates that a particular condition or attribute is absent</p>
<p><b>d.</b> ^ the number of real negative cases in the data</p>
<p><b>e.</b> ^ A test result that correctly indicates the absence of a condition or characteristic</p>
<p><b>f.</b> ^ Type I error: A test result which wrongly indicates that a particular condition or attribute is present</p>
</div>
<footer class="text-center my-6">
<p class="text-gray-500 text-sm">Vibe coded with Gemini 2.5 Pro, by Staffan Betnér</p>
</footer>
<script>
// Function to safely get a numeric value from an input field
function getNumericValue(id) {
const value = document.getElementById(id).value;
// Return null if empty, otherwise parse as a float.
return value.trim() === '' ? null : parseFloat(value);
}
// Function to format numbers for display
function formatResult(value) {
// If value is null, NaN, or infinite, display a dash.
if (value === null || isNaN(value) || !isFinite(value)) {
return '-';
}
// If it's a whole number, display as is. Otherwise, show 3 decimal places.
return value % 1 === 0 ? value.toString() : value.toFixed(3);
}
// Function to update a specific element's text content
function updateElement(id, value) {
document.getElementById(id).textContent = formatResult(value);
}
// Main calculation function
function calculate() {
const tp = getNumericValue('tp');
const fn = getNumericValue('fn');
const fp = getNumericValue('fp');
const tn = getNumericValue('tn');
// --- Intermediate calculations ---
const p = (tp !== null && fn !== null) ? tp + fn : null;
const n = (fp !== null && tn !== null) ? fp + tn : null;
const predicted_positive = (tp !== null && fp !== null) ? tp + fp : null;
const predicted_negative = (fn !== null && tn !== null) ? fn + tn : null;
const total_population = (p !== null && n !== null) ? p + n : null;
updateElement('p_val', p);
updateElement('n_val', n);
updateElement('predicted_positive', predicted_positive);
updateElement('predicted_negative', predicted_negative);
updateElement('total_population', total_population);
// --- Core Metrics ---
const tpr = (p !== null && p > 0 && tp !== null) ? tp / p : null; // Sensitivity, Recall
const tnr = (n !== null && n > 0 && tn !== null) ? tn / n : null; // Specificity
const ppv = (predicted_positive !== null && predicted_positive > 0 && tp !== null) ? tp / predicted_positive : null; // Precision
const npv = (predicted_negative !== null && predicted_negative > 0 && tn !== null) ? tn / predicted_negative : null;
const fnr = (p !== null && p > 0 && fn !== null) ? fn / p : null;
const fpr = (n !== null && n > 0 && fp !== null) ? fp / n : null;
const fdr = (predicted_positive !== null && predicted_positive > 0 && fp !== null) ? fp / predicted_positive : null;
const for_val = (predicted_negative !== null && predicted_negative > 0 && fn !== null) ? fn / predicted_negative : null;
const acc = (total_population !== null && total_population > 0 && tp !== null && tn !== null) ? (tp + tn) / total_population : null;
const prevalence = (total_population !== null && total_population > 0 && p !== null) ? p / total_population : null;
updateElement('tpr', tpr);
updateElement('tnr', tnr);
updateElement('ppv', ppv);
updateElement('npv', npv);
updateElement('fnr', fnr);
updateElement('fpr', fpr);
updateElement('fdr', fdr);
updateElement('for_val', for_val);
updateElement('acc', acc);
updateElement('prevalence', prevalence);
// --- Advanced Metrics ---
const informedness = (tpr !== null && tnr !== null) ? tpr + tnr - 1 : null;
const pt = (tpr !== null && fpr !== null && tpr - fpr !== 0) ? (Math.sqrt(tpr * fpr) - fpr) / (tpr - fpr) : null;
const lr_plus = (fpr !== null && fpr > 0 && tpr !== null) ? tpr / fpr : null;
const lr_minus = (tnr !== null && tnr > 0 && fnr !== null) ? fnr / tnr : null;
const mk = (ppv !== null && npv !== null) ? ppv + npv - 1 : null;
const dor = (lr_plus !== null && lr_minus !== null && lr_minus > 0) ? lr_plus / lr_minus : null;
const ba = (tpr !== null && tnr !== null) ? (tpr + tnr) / 2 : null;
const f1 = (tp !== null && fp !== null && fn !== null && (2 * tp + fp + fn > 0)) ? (2 * tp) / (2 * tp + fp + fn) : null;
const fm = (ppv !== null && tpr !== null) ? Math.sqrt(ppv * tpr) : null;
const mcc_numerator = (tp !== null && tn !== null && fp !== null && fn !== null) ? (tp * tn) - (fp * fn) : null;
const mcc_denominator_val = (tp !== null && fp !== null && fn !== null && tn !== null) ? (tp + fp) * (tp + fn) * (tn + fp) * (tn + fn) : null;
const mcc_denominator = (mcc_denominator_val !== null && mcc_denominator_val > 0) ? Math.sqrt(mcc_denominator_val) : null;
const mcc = (mcc_numerator !== null && mcc_denominator !== null && mcc_denominator > 0) ? mcc_numerator / mcc_denominator : null;
const ts_denominator = (tp !== null && fn !== null && fp !== null) ? tp + fn + fp : null;
const ts = (ts_denominator !== null && ts_denominator > 0 && tp !== null) ? tp / ts_denominator : null;
updateElement('informedness', informedness);
updateElement('pt', pt);
updateElement('lr_plus', lr_plus);
updateElement('lr_minus', lr_minus);
updateElement('mk', mk);
updateElement('dor', dor);
updateElement('ba', ba);
updateElement('f1', f1);
updateElement('fm', fm);
updateElement('mcc', mcc);
updateElement('ts', ts);
}
// Initial call to set the board with dashes on page load
calculate();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment