Skip to content

Precision, Recall, and F1-Score

What you’ll learn

  • precision and recall in words a stakeholder would use
  • why the two cannot both be maximised, and what the dial between them is
  • why F1F_1 uses a harmonic mean, and what would go wrong with an arithmetic one
  • FβF_\beta, for when recall genuinely matters more than precision
  • how to choose a threshold to hit a target precision or a target recall
  • the precision-recall curve, average precision, and when to prefer them to F1F_1

Intuition

You are building a filter that flags fraudulent transactions.

  • Precision answers: of the transactions I flagged, how many were actually fraud? Low precision means you are crying wolf, and everyone stops listening.
  • Recall answers: of the fraud that occurred, how much did I catch? Low recall means fraud is slipping through while your dashboard looks calm.

They pull against each other. Flag everything and you catch all the fraud — recall 1.0, precision near zero. Flag only the single most suspicious transaction and you are probably right — precision 1.0, recall near zero. Everything useful lives in between, and the dial between them is the decision threshold.

diagram Diagram mermaid

The math

precision=TPTP+FPrecall=TPTP+FN\text{precision} = \frac{TP}{TP + FP} \qquad\qquad \text{recall} = \frac{TP}{TP + FN}

Same numerator, different denominators. Precision divides by what you predicted positive; recall divides by what is positive.

F1F_1: why harmonic?

F1=2precisionrecallprecision+recall=2TP2TP+FP+FNF_1 = 2 \cdot \frac{\text{precision} \cdot \text{recall}}{\text{precision} + \text{recall}} = \frac{2 \cdot TP}{2 \cdot TP + FP + FN}

That is the harmonic mean of the two. The arithmetic mean would let one value rescue the other: precision 1.0 with recall 0.0 averages to 0.50, which reads as a mediocre model rather than a useless one. The harmonic mean gives:

F1=21.0×0.01.0+0.0=0F_1 = 2\cdot\frac{1.0 \times 0.0}{1.0 + 0.0} = 0

The harmonic mean is dominated by the smaller value. It always sits at or below the arithmetic mean, and the gap grows as the two inputs diverge:

PrecisionRecallArithmetic meanF1F_1 (harmonic)
0.900.900.9000.900
0.950.850.9000.897
1.000.500.7500.667
1.000.100.5500.182
1.000.000.5000.000

A model can only score highly on F1F_1 by being good at both, which is exactly the property you want from a single summary number.

FβF_\beta: when they are not equally important

Fβ=(1+β2)precisionrecallβ2precision+recallF_\beta = \left(1 + \beta^2\right)\cdot\frac{\text{precision} \cdot \text{recall}}{\beta^2 \cdot \text{precision} + \text{recall}}

β\beta is how many times more important recall is than precision:

β\betaMeaningTypical use
0.5Precision twice as importantSpam filtering, automated blocking
1EqualThe default
2Recall twice as importantDisease screening, fraud, safety recalls

F2F_2 is the honest metric for screening problems, and it is one keyword away: fbeta_score(y, pred, beta=2)fbeta_score(y, pred, beta=2).

Worked example by hand

The ten-row example, at threshold 0.5: TP = 4, FP = 2, FN = 1, TN = 3.

Step 1 — precision. Of the 6 flagged, 4 were right:

precision=44+2=230.667\text{precision} = \frac{4}{4+2} = \frac{2}{3} \approx 0.667

Step 2 — recall. Of the 5 real positives, 4 were caught:

recall=44+1=0.800\text{recall} = \frac{4}{4+1} = 0.800

Step 3 — F1F_1.

F1=20.667×0.8000.667+0.800=20.53331.4667=0.727F_1 = 2\cdot\frac{0.667 \times 0.800}{0.667 + 0.800} = 2\cdot\frac{0.5333}{1.4667} = 0.727

Or straight from the cells: F1=2(4)/(2(4)+2+1)=8/11=0.727F_1 = 2(4) / (2(4) + 2 + 1) = 8/11 = 0.727.

Step 4 — F2F_2, weighting recall double.

F2=50.667×0.8004(0.667)+0.800=50.53333.4667=0.769F_2 = 5\cdot\frac{0.667 \times 0.800}{4(0.667) + 0.800} = 5\cdot\frac{0.5333}{3.4667} = 0.769

F2>F1F_2 > F_1 because this model’s recall (0.800) is better than its precision (0.667), and F2F_2 rewards exactly that.

Step 5 — raise the threshold to 0.65. Now TP = 3, FP = 1, FN = 2:

precision=34=0.750recall=35=0.600F1=2(3)2(3)+1+2=69=0.667\text{precision} = \frac{3}{4} = 0.750 \qquad \text{recall} = \frac{3}{5} = 0.600 \qquad F_1 = \frac{2(3)}{2(3)+1+2} = \frac{6}{9} = 0.667

Precision rose, recall fell, and F1F_1 dropped from 0.727 to 0.667 — so for a balanced objective 0.5 was the better threshold on this data. Had the objective been F2F_2, the ranking might reverse; the metric decides the threshold, not the other way around.

See it move

Steps 1 to 5 are two points on a curve. The sketch walks the threshold down through all ten scores and plots the whole precision–recall trajectory, marking where each FβF_\beta peaks. The β\beta slider is the point: the same model has a different optimal threshold for every objective.

sketch Every threshold, and where each objective peaks p5.js
The precision-recall curve for the ten-row example, walked one threshold at a time. Precision moves in jagged steps while recall falls monotonically. Markers show the threshold that maximises F0.5, F1 and F2, and they are not the same threshold. Click to cycle which beta is highlighted.

The three peak markers are three different answers to “what threshold should I ship?”, and on this data two of them genuinely disagree:

ObjectivePeaks at thresholdValue thereTP / FP / FN
F0.5F_{0.5} (precision-leaning)0.760.8823 / 0 / 2
F1F_10.310.8335 / 2 / 0
F2F_2 (recall-leaning)0.310.9265 / 2 / 0

Note what F0.5F_{0.5} chose: a threshold that misses two of the five positives, because at 0.76 it makes no false accusations at all. F1F_1 and F2F_2 both prefer catching every positive and eating two false alarms. Same ten predictions, same model, opposite operating points — and neither is a mistake. You cannot choose a threshold without first choosing β\beta, and β\beta is a claim about the relative cost of the two errors, not about the model.

Also worth noticing: the 0.5 threshold used in steps 1–4 is not the peak of any of the three objectives. The default is a convention inherited from predictpredict, not a decision.

The trade-off on real data

figureTwo views of the same trade-offmatplotlib
Left: precision and recall plotted against the decision threshold, crossing near 0.5. Right: the precision-recall curve, high across most of its range, with a dashed horizontal line marking the positive class rate.Left: precision and recall plotted against the decision threshold, crossing near 0.5. Right: the precision-recall curve, high across most of its range, with a dashed horizontal line marking the positive class rate.
Left, as a function of the knob you control. Right, plotted against each other, which removes the threshold and shows the model's whole capability at once.

Reading the plot

ThresholdPrecisionRecallF1F_1Reading
0.10.8180.9840.894Catches 63 of 64 cancers, 14 false alarms
0.30.8590.9530.904Cautious triage
0.50.9380.9380.938The default
0.71.0000.9220.959Best F1F_1 — no false alarms at all
0.91.0000.8750.933Too conservative; misses 8 cases
  1. The curves cross near 0.5, by coincidence. Nothing forces that; on imbalanced data the crossing point is usually far from the default.
  2. The best F1F_1 is at 0.7, not 0.5. A free 2-point gain, available with a one-line change.
  3. The PR curve’s baseline is the positive rate, here 64/171 = 0.374 — that is what a random classifier achieves, not 0.5. A PR curve hugging the top-right corner means the model is genuinely separating the classes.

Choosing a threshold deliberately

precision_recall_curveprecision_recall_curve returns the full trade-off, so you can pick the operating point your problem requires rather than accepting 0.5:

target_precision.py
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import precision_recall_curve, precision_score, recall_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
 
X, y = load_breast_cancer(return_X_y=True)
y = 1 - y
X_tr, X_te, y_tr, y_te = train_test_split(
    X, y, test_size=0.3, random_state=0, stratify=y
)
 
model = make_pipeline(StandardScaler(), LogisticRegression(max_iter=5000))
model.fit(X_tr, y_tr)
proba = model.predict_proba(X_te)[:, 1]
 
precision, recall, thresholds = precision_recall_curve(y_te, proba)
 
# The lowest threshold that still reaches 99% precision — lowest keeps recall high
idx = np.argmax(precision[:-1] >= 0.99)
chosen = thresholds[idx]
 
pred = (proba >= chosen).astype(int)
print(f"threshold  {chosen:.4f}")
print(f"precision  {precision_score(y_te, pred):.4f}")
print(f"recall     {recall_score(y_te, pred):.4f}")
target_precision.py
import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import precision_recall_curve, precision_score, recall_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
 
X, y = load_breast_cancer(return_X_y=True)
y = 1 - y
X_tr, X_te, y_tr, y_te = train_test_split(
    X, y, test_size=0.3, random_state=0, stratify=y
)
 
model = make_pipeline(StandardScaler(), LogisticRegression(max_iter=5000))
model.fit(X_tr, y_tr)
proba = model.predict_proba(X_te)[:, 1]
 
precision, recall, thresholds = precision_recall_curve(y_te, proba)
 
# The lowest threshold that still reaches 99% precision — lowest keeps recall high
idx = np.argmax(precision[:-1] >= 0.99)
chosen = thresholds[idx]
 
pred = (proba >= chosen).astype(int)
print(f"threshold  {chosen:.4f}")
print(f"precision  {precision_score(y_te, pred):.4f}")
print(f"recall     {recall_score(y_te, pred):.4f}")

The pattern generalises. For a target recall instead, scan recall >= targetrecall >= target from the other end. State the requirement first — “we must catch 95% of fraud” — then derive the threshold, rather than reporting whatever 0.5 happens to give.

Average precision

F1F_1 describes a single operating point. Average precision summarises the entire curve:

AP=k(RkRk1)Pk\text{AP} = \sum_{k} \left(R_k - R_{k-1}\right)P_k

the precision-weighted area under the PR curve. On the breast-cancer model, AP = 0.989, meaning precision stays near 1 across essentially the whole recall range.

MetricDescribesUse when
PrecisionOne operating pointFalse alarms are the expensive error
RecallOne operating pointMisses are the expensive error
F1F_1One operating pointBoth matter roughly equally
FβF_\betaOne operating pointOne matters β2\beta^2 times more
Average precisionThe whole curveComparing models before fixing a threshold
ROC-AUCThe whole curveClasses are roughly balanced — see the next page

Use a curve metric while choosing between models, and a point metric once you have committed to an operating point.

Pitfalls

quizCheck yourself
  1. Your fraud model flags 100 transactions and 30 are genuinely fraudulent, out of 50 frauds in total. What are precision and recall?

    Show answer

    A — Precision 0.30, recall 0.60 — Precision = 30/100 = 0.30 (of what you flagged). Recall = 30/50 = 0.60 (of what existed). Same numerator, different denominators.

  2. Why does F1 use a harmonic mean rather than an arithmetic one?

    Show answer

    B — The harmonic mean is dominated by the smaller value, so a model cannot score well by excelling at one and failing the other — Precision 1.0 with recall 0.0 gives an arithmetic mean of 0.50 — misleadingly respectable. The harmonic mean gives exactly 0.

  3. You are screening for a serious disease. Which metric should you optimise?

    Show answer

    B — F-beta with beta = 2, which weights recall twice as heavily as precision — A missed diagnosis is far worse than a follow-up test. F2 encodes that asymmetry explicitly, rather than pretending the two errors cost the same.

  4. On a dataset with 1% positives, your model achieves average precision 0.15. Is that good?

    Show answer

    B — Yes — the random baseline for AP is the positive rate, 0.01, so this is roughly a fifteenfold improvement — Unlike ROC-AUC, the PR baseline moves with class balance. Always compare AP against the positive rate rather than against 0.5.

🧪 Try It Yourself

Exercise 1 – Precision and recall from counts

Exercise 2 – Harmonic beats arithmetic

Exercise 3 – Weight recall with F-beta

Exercise 4 – Find the best threshold for F1

Exercise 5 – Hit a target precision

Recap

  • Precision is “of what I flagged, how much was right”; recall is “of what existed, how much did I catch”.
  • The threshold is the dial between them, and moving it needs no retraining.
  • F1F_1 is their harmonic mean, so it is dominated by the weaker of the two: precision 1.0 with recall 0.0 gives F1=0F_1 = 0, not 0.5.
  • The ten-row example: precision 0.667, recall 0.800, F1F_1 0.727, F2F_2 0.769.
  • FβF_\beta encodes which error is worse; β=2\beta = 2 for screening, β=0.5\beta = 0.5 for blocking.
  • Derive the threshold from a stated requirement, on validation data, and freeze it before touching the test set.
  • Average precision summarises the whole curve, and its baseline is the positive rate, not 0.5.

Exercise 6 – Let each objective pick its own threshold

Next

Continue to The ROC Curve and AUC — the other curve metric, what it measures that precision-recall does not, and the imbalanced case where it will flatter a model that deserves no flattery.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did