🎯 What You'll Learn
- Fill in a confusion matrix from a description of a detector's behaviour, without looking anything up
- Compute precision, recall, specificity and accuracy from four numbers
- Explain why accuracy is the most misleading metric in security, using the always-say-benign detector
- Work the base-rate arithmetic that turns a "99% accurate" detector into a thousand false alarms a day
- Apply a one-line napkin rule in a live meeting to sanity-check any detection claim
- Raise a detector's precision roughly forty-eight-fold without touching the model, by changing the population it runs on
Where this page sits
Level: Beginner. The only prerequisite is multiplication and division. Bring a pen.
This is rung two of Track C. If the words supervised learning, inference and model artefact mean nothing yet, do Fundamentals of AI first. If you already compute precision at low prevalence in your head, skip to Thresholds and Confidence.
This is the most important page in the whole curriculum. Not the most advanced — the most important. Everything else you will learn about AI in security is judged against the arithmetic on this page. A practitioner who understands base rates and nothing else about AI is more useful in a detection review than one who can derive backpropagation and cannot. And it really is only arithmetic: multiplication, division, and the discipline to write down the denominator before you argue about the numerator.
Four cells, and the convention that trips everyone
A detector makes a binary call on every item it sees. Reality also has a binary answer. Cross those two and you get four cells. That is a confusion matrix, and it is the entire foundation.
The convention that trips people: "positive" means the thing you are hunting for, not the thing you want. A positive is malicious. A true positive is a caught attacker, which is a good outcome described by a word that sounds bad. Get this straight now, because a reversed convention silently inverts every number downstream.
| Actually malicious | Actually benign | |
|---|---|---|
| Flagged as malicious | TP — true positive: caught it | FP — false positive: false alarm |
| Not flagged | FN — false negative: missed it | TN — true negative: correctly ignored |
Read the four cells as four human outcomes, because that is how you will have to argue about them:
- TP — an analyst investigates and finds a real attack. The detector earned its keep.
- FP — an analyst investigates and finds nothing. Time gone, and a little trust with it.
- FN — nobody investigates, because nobody was told. This is the cell you find out about from a breach notification.
- TN — silence. The overwhelming majority of everything, and invisible by design.
Every metric in this field is a ratio of those four numbers. There are no others. The reason the field feels complicated is that the same four numbers get sliced along different denominators, and the names do not tell you which denominator is in play.
The five metrics, and what each denominator means
Write these out once by hand and you will never need to look them up again. In each case, the thing to fix in your memory is what is on the bottom.
| Metric | Formula | Denominator is… | Answers the question |
|---|---|---|---|
| Precision | TP / (TP + FP) | everything you flagged | "When it fires, how often is it right?" |
| Recall (TPR, sensitivity) | TP / (TP + FN) | everything actually malicious | "Of the real attacks, how many did we catch?" |
| Specificity (TNR) | TN / (TN + FP) | everything actually benign | "Of the harmless things, how many did we leave alone?" |
| False positive rate | FP / (TN + FP) = 1 − specificity | everything actually benign | "What fraction of harmless things do we bother an analyst about?" |
| Accuracy | (TP + TN) / all | everything | "How often is it right about anything?" — almost always useless here |
Precision and recall have different denominators, and that single fact is the source of nearly every argument about detection quality. Precision is the analyst's metric: it decides whether the queue is worth reading. Recall is the risk owner's metric: it decides how much gets through. They trade against each other, and a claim that quotes only one of them is not a claim, it is an advertisement.
The vocabulary decoder. Recall, sensitivity, true positive rate and detection rate are four names for the same number. Specificity and true negative rate are two names for one number. Fall-out and false positive rate are the same. This field renamed everything three times; the arithmetic never changed.
Why accuracy is a lie: the always-benign detector
Here is a detector. Its source code is complete:
def detect(email):
return "benign" # every time, no exceptionsIt has no model, no features, no training data and no cost. Now measure it on a realistic population: 100,000 emails a day, of which 1 in 10,000 is malicious.
That is 10 malicious emails and 99,990 benign ones. Fill in the matrix:
| Actually malicious | Actually benign | |
|---|---|---|
| Flagged | TP = 0 | FP = 0 |
| Not flagged | FN = 10 | TN = 99,990 |
Accuracy = (0 + 99,990) / 100,000 = 99.99%.
A detector that has never detected anything, and never could, scores 99.99% accuracy. Its recall is 0/10 = 0%. Its precision is 0/0, which is undefined — it never fires, so the question "when it fires, is it right?" has no answer.
This is not a party trick. It is the reason accuracy must never be the headline number for a security detector, and it is the reason you should treat any accuracy figure in a vendor deck as a signal that the interesting numbers were left out. When the thing you are looking for is rare, the majority class dominates accuracy completely, and the majority class is "nothing is happening".
The main event: a 99% accurate detector, worked
Now a real-seeming detector. The claim on the slide is "99% accurate". First, pin down what that even means, because the phrase is ambiguous and the ambiguity is where the trouble hides. Take the most flattering reading — the vendor means both of the following:
- Recall = 99%: it catches 99 of every 100 malicious emails.
- Specificity = 99%: it correctly ignores 99 of every 100 benign emails, so the false positive rate is 1%.
Those are genuinely good numbers for a classifier. Now apply them to the same population.
Write down the population before anything else
100,000 emails per day. Base rate (prevalence) = 1 in 10,000 = 0.0001.
Malicious = 100,000 × 0.0001 = 10 Benign = 100,000 − 10 = 99,990
Apply recall to the malicious column
TP = 10 × 0.99 = 9.9 FN = 10 × 0.01 = 0.1
Apply the false positive rate to the benign column
FP = 99,990 × 0.01 = 999.9 TN = 99,990 × 0.99 = 98,990.1
Now compute precision — the analyst's number
Precision = TP / (TP + FP) = 9.9 / (9.9 + 999.9) = 9.9 / 1009.8 = 0.0098 = 0.98%
| Actually malicious | Actually benign | Row total | |
|---|---|---|---|
| Flagged | TP = 9.9 | FP = 999.9 | 1,009.8 alerts/day |
| Not flagged | FN = 0.1 | TN = 98,990.1 | 98,990.2 |
| Column total | 10 | 99,990 | 100,000 |
Fewer than one alert in a hundred is real. The analyst opens roughly a hundred tickets before finding one that matters. The detector's accuracy, by the way, is (9.9 + 98,990.1) / 100,000 = 99.0% — exactly as advertised, and completely uninformative.
Push it into hours and the picture gets worse. At an optimistic five minutes of triage per alert, 1,010 alerts is over 84 person-hours of triage per day. To keep up you would need more than ten analysts doing nothing but this queue, for one detector, on one data source. That is not a staffing problem you can solve. It is an arithmetic problem that must be solved before the tool is deployed.
The real-world consequence is not overtime, it is alert fatigue. A queue that is 99% noise trains its readers to close tickets without looking. The nine-point-nine real detections in that table are, in practice, at serious risk of being closed as noise along with everything else — which converts hard-won true positives into functional false negatives. A low-precision detector is not merely useless; it actively degrades the detections you already had.
The napkin method
You will not have a spreadsheet in the meeting where the claim is made. You need one line you can do in your head. Here it is:
False alarms per real alarm ≈ FPR ÷ prevalence
(valid whenever recall is decent and prevalence is small — which is every security detection problem)
Check it against the worked example: FPR = 0.01, prevalence = 0.0001. Ratio = 0.01 / 0.0001 = 100 false alarms for every real one. The full arithmetic gave 999.9 / 9.9 = 101. The napkin is accurate to one percent and took two seconds.
Two corollaries fall straight out of it, and they are the ones worth memorising:
Corollary 1 — for precision above 50%, you need FPR below the prevalence. If one email in ten thousand is malicious, your detector must produce fewer than one false positive per ten thousand benign emails before most of its alerts are real. Say that out loud. It is a brutal bar and almost nothing clears it unaided.
Corollary 2 — for precision of 90%, you need FPR ≈ prevalence ÷ 9. At a prevalence of 0.0001 that is an FPR of about 0.0000111: roughly one false positive per ninety thousand benign items. Derivation, for completeness, with p = prevalence and recall ≈ 1:
precision ≈ p / (p + FPR)
set precision = 0.90:
0.9 = p / (p + FPR)
0.9p + 0.9·FPR = p
0.9·FPR = 0.1p
FPR = p / 9Run the napkin over a few prevalences and the shape of the whole field becomes obvious. Same detector, FPR fixed at 1%, only the base rate changing:
| Population you run it on | Prevalence | False alarms per real one | Precision |
|---|---|---|---|
| All inbound mail | 1 in 10,000 | ~100 | ~1% |
| Mail with an executable attachment | 1 in 1,000 | ~10 | ~9% |
| Mail already quarantined by another control | 1 in 100 | ~1 | ~50% |
| Mail from a known-bad sender domain | 1 in 10 | ~0.1 | ~90% |
The model is identical in every row. Only the denominator changed. This table is the single most actionable thing on the page.
Changing the denominator: forty-eight-fold precision, no retraining
Read the previous table as an instruction rather than an observation. If precision is governed by the population you run on, then narrowing the population is a detection-engineering lever that costs no compute and no model work. Work it properly.
Same 100,000 emails, same 10 malicious, same detector at 99% recall and 1% FPR. Now put a cheap deterministic pre-filter in front: only score mail that is from an external sender and carries an attachment and failed DMARC. Suppose that filter keeps 1% of all mail and, because attackers need those properties, it retains 90% of the malicious mail.
The new population
Filtered volume = 100,000 × 0.01 = 1,000 emails Malicious retained = 10 × 0.90 = 9 Benign in the filtered set = 1,000 − 9 = 991
New prevalence = 9 / 1,000 = 0.009 — ninety times higher than before.
Run the same model over it
TP = 9 × 0.99 = 8.91 FN (inside the filter) = 0.09; plus 1 malicious email the filter discarded FP = 991 × 0.01 = 9.91
Score it
Precision = 8.91 / (8.91 + 9.91) = 8.91 / 18.82 = 47.3% Recall against all malicious mail = 8.91 / 10 = 89.1% Alerts per day = 19, down from 1,010.
Precision went from 0.98% to 47.3% — a factor of roughly forty-eight — and the price was recall falling from 99% to 89%. Nineteen alerts a day is a queue a single analyst reads properly. A thousand is a queue nobody reads at all.
That trade is the actual craft of detection engineering, and it is worth being explicit about what was traded: you accepted that one malicious email in ten now bypasses this detector entirely, in exchange for a queue that gets read. Whether that is the right call depends on what else is in the stack — and that is a risk conversation, informed by arithmetic, rather than an arithmetic problem with one answer. The mistake is not making the trade; the mistake is making it without writing the numbers down.
You run the grader, and it runs on your machine. kalirange does not host these labs, watch your terminal, or receive your work — the script below is printed here for you to create and run locally, and nothing is submitted anywhere. That is deliberate: the lab works offline, on a laptop, with no account and no data leaving it.
Run it yourself
Everything above is reproducible in pure Python with no dependencies and no model. Grade yourself against the numbers on this page.
# docker-compose.yml — CPU-ONLY. No GPU, no model, no network after build.
services:
lab:
image: python:3.12-slim
working_dir: /work
volumes:
- ./:/work
command: python -m pytest -q test_matrix.py# matrix.py — implement these four functions. Standard library only.
def confusion(population, prevalence, recall, fpr):
"""Return (tp, fp, fn, tn) as floats for a population of `population` items."""
malicious = population * prevalence
benign = population - malicious
tp = malicious * recall
fn = malicious - tp
fp = benign * fpr
tn = benign - fp
return tp, fp, fn, tn
def precision(tp, fp):
return tp / (tp + fp) if (tp + fp) else float("nan")
def napkin_false_per_true(fpr, prevalence):
"""The one-line rule: false alarms per real alarm."""
return fpr / prevalence
def fpr_for_target_precision(prevalence, target):
"""What FPR do I need for this precision, assuming recall is near 1?"""
return prevalence * (1 - target) / target# test_matrix.py — the auto-grader. Run: docker compose up
from matrix import confusion, precision, napkin_false_per_true, fpr_for_target_precision
TOL = 0.01 # relative tolerance; this is arithmetic, so it is tight on purpose
def close(a, b, tol=TOL):
return abs(a - b) <= tol * abs(b)
def test_the_worked_example():
tp, fp, fn, tn = confusion(100_000, 0.0001, 0.99, 0.01)
assert close(tp, 9.9) and close(fp, 999.9)
assert close(fn, 0.1, 0.05) and close(tn, 98_990.1)
assert close(precision(tp, fp), 0.0098, 0.02)
def test_always_benign_scores_99_99_percent_accuracy():
tp, fp, fn, tn = confusion(100_000, 0.0001, 0.0, 0.0)
accuracy = (tp + tn) / 100_000
assert close(accuracy, 0.9999)
assert tp + fp == 0 # precision is undefined, not 100%
def test_napkin_matches_the_full_arithmetic():
tp, fp, fn, tn = confusion(100_000, 0.0001, 0.99, 0.01)
assert close(napkin_false_per_true(0.01, 0.0001), fp / tp, 0.02)
def test_corollary_two():
assert close(fpr_for_target_precision(0.0001, 0.90), 0.0001 / 9)
def test_narrowing_the_population_beats_retraining():
tp, fp, fn, tn = confusion(1_000, 0.009, 0.99, 0.01)
assert precision(tp, fp) > 0.40 # was under 0.01 on the full population
assert (tp / 10) > 0.85 # recall against all 10 maliciousGrading mechanism: hidden pytest, metric thresholds, deterministic. The grader is test_matrix.py; it passes only when all five tests pass, and the exit code of pytest is the verdict. There is no model in this lab, so there is no flakiness to absorb: the same inputs produce the same numbers every run. The assertions are written as thresholds and relative tolerances rather than exact float equality, because comparing floats for equality is a bug regardless of the subject matter.
What this lab does not prove. It proves you can do the arithmetic. It proves nothing about any real detector. Every input here — the prevalence, the recall, the false positive rate — was supplied by you, and in production every one of them is an estimate with error bars that you must measure on your traffic. In particular, prevalence is not a constant: it moves with campaigns, seasons, and whoever is currently targeting you, and a precision figure computed against last quarter's base rate can be wrong by an order of magnitude today.
Questions to ask when someone shows you a detection metric
Written as a checklist because that is how you will use it.
What is the base rate of the population you measured on?
If the answer is a balanced test set — half malicious, half benign — the precision figure does not transfer to production and the person quoting it may not know that.
Precision and recall, both, or neither.
A single number is not an answer. "99% detection rate" is recall with no precision; "high fidelity" is precision with no recall.
How many alerts per day does that produce here?
Convert the percentages into a queue length against your own volume. This is the napkin rule and it takes ten seconds.
Was the test set drawn from the same distribution as our traffic?
A detector tuned on someone else's mail, network or endpoints has an unmeasured error on yours.
What happens to the misses?
Every FN is a silent failure. Ask what other control is expected to catch what this one drops, and confirm that control is real.
Cheat sheet
| Quantity | Formula | Note |
|---|---|---|
| Precision | TP / (TP + FP) | The analyst's metric |
| Recall / TPR | TP / (TP + FN) | The risk owner's metric |
| Specificity / TNR | TN / (TN + FP) | 1 − FPR |
| FPR | FP / (FP + TN) | Drives alert volume |
| Accuracy | (TP + TN) / total | Ignore it at low prevalence |
| F1 | 2·(P·R)/(P+R) | Hides which of the two is bad — read both instead |
| Napkin | false-per-true ≈ FPR / prevalence | The one to memorise |
| Precision at low prevalence | ≈ p / (p + FPR) | Assumes recall ≈ 1 |
FPR needed for precision t | ≈ p·(1−t)/t | t = 0.9 gives p/9 |
Where to go next
| Next | Why |
|---|---|
| AI Literacy: Thresholds and Confidence | The FPR and recall you just used are not fixed properties of a model — they move together as you slide a threshold. That page shows the trade in motion. |
| AI Literacy: Embeddings and Retrieval | Where similarity search comes from, and the groundwork for RAG security. |
| Fundamentals of AI | Go back a rung if "supervised" and "inference" were unfamiliar here. |
Rung complete. You can fill in a confusion matrix, compute precision and recall from four numbers, explain why accuracy lies when the target is rare, and estimate a detector's real alert volume in your head with FPR ÷ prevalence. That last one will be useful in a meeting this month.