Data Science & Machine Learning
ConceptIntermediate 28 min· Machine learning · 10 of 20

Metrics: accuracy, precision, recall, F1 and ROC

Accuracy can hide a useless model. Learn the confusion matrix, precision, recall, F1, thresholds and ROC AUC with runnable examples, and how to pick the right metric for fraud, spam or screening problems.

Suppose a bank builds a fraud detector. It checks 1,000 transactions, only 50 of which are fraud, and the model says "not fraud" every time. Its accuracy is 95%. It also catches zero fraud. The number looks good and the model is worthless.

This is why classification needs more than accuracy. In this lesson you learn the metrics that tell the real story, when to use each, and how to pick a threshold.

The confusion matrix: where every metric comes from

For a yes/no problem, call the thing you are looking for the positive class (fraud, disease, churn). Every prediction falls into one of four boxes:

Confusion matrix

Predicted positivePredicted negative
Actually positiveTrue positive (TP): caughtFalse negative (FN): missed
Actually negativeFalse positive (FP): false alarmTrue negative (TN): correctly ignored

scikit-learn prints the matrix with rows as actual classes and columns as predicted classes, in the order 0 then 1, so for a binary problem it reads [[TN, FP], [FN, TP]]. Always check that order before reading it.

The main metrics

Metrics at a glance

MetricFormulaQuestion it answers
Accuracy(TP + TN) / allOverall, how often is the model right?
PrecisionTP / (TP + FP)When it says positive, how often is it right?
Recall (sensitivity)TP / (TP + FN)Of all real positives, how many did it find?
F1 score2 x precision x recall / (precision + recall)One number balancing precision and recall
SpecificityTN / (TN + FP)Of all real negatives, how many did it correctly leave alone?

An easy way to remember: precision is about the quality of your alarms. Recall is about how few real cases slip past you. F1 is the harmonic mean of the two, which means it is low if either one is low. You cannot get a good F1 by being great at only one.

See them on real numbers

Here we create a fraud-like dataset where only about 5% of rows are positive, train logistic regression and compute everything. The data is synthetic, made with make_classification.

Metrics on an imbalanced dataset
import numpy as np
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (accuracy_score, confusion_matrix, f1_score,
                             precision_score, recall_score, roc_auc_score)
from sklearn.model_selection import train_test_split

# 5000 customers, about 5% of them are fraudulent (class 1)
X, y = make_classification(n_samples=5000, n_features=10, n_informative=5,
                           weights=[0.95, 0.05], random_state=42)
print(np.bincount(y))

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, random_state=42, stratify=y
)

model = LogisticRegression(max_iter=1000).fit(X_train, y_train)
pred = model.predict(X_test)
proba = model.predict_proba(X_test)[:, 1]

print(confusion_matrix(y_test, pred))
print("accuracy :", round(accuracy_score(y_test, pred), 3))
print("precision:", round(precision_score(y_test, pred), 3))
print("recall   :", round(recall_score(y_test, pred), 3))
print("f1       :", round(f1_score(y_test, pred), 3))
print("ROC AUC  :", round(roc_auc_score(y_test, proba), 3))

Prints the confusion matrix and five metrics.

Output
[4725  275]
[[1178    3]
 [  63    6]]
accuracy : 0.947
precision: 0.667
recall   : 0.087
f1       : 0.154
ROC AUC  : 0.836

Read the matrix: TN = 1178, FP = 3, FN = 63, TP = 6. Now compute by hand:

- Accuracy = (6 + 1178) / 1250 = 0.947. Looks great. - Precision = 6 / (6 + 3) = 0.667. When the model flags fraud, it is right two times out of three. - Recall = 6 / (6 + 63) = 0.087. It found fewer than 9 of every 100 real frauds.

The 94.7% accuracy is almost entirely the easy, common class. This model is nearly useless for its real job.

Compare with a model that does nothing
print("Always predict 'not fraud':", round(accuracy_score(y_test, np.zeros_like(y_test)), 3))

for threshold in [0.5, 0.3, 0.2, 0.1]:
    p = (proba >= threshold).astype(int)
    print(f"threshold {threshold}: precision {precision_score(y_test, p):.2f}  "
          f"recall {recall_score(y_test, p):.2f}  f1 {f1_score(y_test, p):.2f}")

Uses the variables from the previous cell. Also tries different thresholds.

Output
Always predict 'not fraud': 0.945
threshold 0.5: precision 0.67  recall 0.09  f1 0.15
threshold 0.3: precision 0.54  recall 0.28  f1 0.37
threshold 0.2: precision 0.39  recall 0.42  f1 0.40
threshold 0.1: precision 0.22  recall 0.65  f1 0.33

A model that predicts "not fraud" for everyone has 94.5% accuracy, only 0.2 points below ours. Accuracy simply cannot see the difference.

Thresholds: precision versus recall

Most classifiers output a probability. The default rule "positive if probability is at least 0.5" is just a convention, not a law. Lowering the threshold flags more cases: recall rises (fewer misses) and precision usually falls (more false alarms). Raising it does the opposite. The table above shows this: at 0.1 the model catches 65% of fraud but only 22% of its alerts are real.

So the right threshold depends on the cost of each mistake:

Choose by cost, not by habit

SituationWorse mistakeFavour
Cancer screeningMissing a patient (FN)High recall; accept extra follow-up tests
Spam filterSending a real email to spam (FP)High precision
Fraud alerts reviewed by a small teamToo many false alarms the team cannot handle (FP)A balance; pick a threshold the team can handle
Customer churn offer costing moneyWasting offers on people who would stay (FP)Precision, or a threshold set by the cost of an offer

ROC curve and AUC

The ROC curve shows what happens at every possible threshold. The vertical axis is the true positive rate (recall). The horizontal axis is the false positive rate: FP / (FP + TN), the share of real negatives wrongly flagged. A perfect model hugs the top-left corner. Random guessing is the diagonal line.

ROC AUC (area under the curve) squeezes it into one number between 0 and 1. A useful way to read it: pick one random positive and one random negative; the AUC is the chance the model gives the positive a higher score. 0.5 means no skill, 1.0 means perfect ranking. Its big advantage is that it does not depend on any single threshold.

Reading one point on the ROC curve
from sklearn.metrics import roc_curve

fpr, tpr, thresholds = roc_curve(y_test, proba)
# Pick the first threshold that catches at least 60% of fraud, and see its false positive rate
i = (tpr >= 0.60).argmax()
print(f"threshold {thresholds[i]:.3f}: recall (TPR) {tpr[i]:.2f}, false positive rate {fpr[i]:.2f}")

roc_curve returns the false positive rate, true positive rate and the threshold at each point.

Output
threshold 0.111: recall (TPR) 0.61, false positive rate 0.11

To catch 61% of fraud, we must accept flagging 11% of the genuine transactions. Whether that is acceptable is a business question the curve lets you ask clearly.

AUC and imbalance

With very rare positives, ROC AUC can look good even when precision is poor, because the huge number of negatives keeps the false positive rate small. In that case also look at the precision-recall curve and average_precision_score.

Handling imbalanced classes

Beyond choosing a good metric and threshold, you can tell the model to care more about the rare class. Most scikit-learn classifiers accept class_weight="balanced", which weights errors on the rare class more heavily.

class_weight='balanced'
balanced = LogisticRegression(max_iter=1000, class_weight="balanced").fit(X_train, y_train)
p = balanced.predict(X_test)
print("balanced weights: precision", round(precision_score(y_test, p), 2),
      "recall", round(recall_score(y_test, p), 2))

Uses the same split as before.

Output
balanced weights: precision 0.16 recall 0.84

Recall jumped from 0.09 to 0.84, but precision fell to 0.16. This is the same trade-off as moving the threshold, not a free improvement. Other options are collecting more positive examples or oversampling the rare class; always apply those to the training data only, never the test set.

Choosing a metric

Quick guide

Your situationUse
Balanced classes, all mistakes cost the sameAccuracy is fine
Rare positive classPrecision, recall, F1, precision-recall curve
Missing a positive is very costlyRecall (with a minimum precision)
False alarms are very costlyPrecision
Comparing models without fixing a thresholdROC AUC
More than two classesPer-class precision and recall, and macro-averaged F1 (average of per-class F1)

How this comes up in interviews

Expect: "Explain precision and recall with an example", "When is accuracy misleading?", "What is the ROC curve?", "How would you tune a model for fraud detection?" A strong answer names the cost of each type of mistake, mentions the threshold, and says what you would do with imbalance. Be careful to say which class is positive; many people mix up precision and recall under pressure.

Prompt · Practise metric interview questions
Act as an interviewer for a junior data scientist role. Ask me one question at a time about classification metrics (confusion matrix, precision, recall, F1, ROC AUC, thresholds, class imbalance). After each of my answers, tell me what was correct, what was missing, and then ask a harder follow-up. Start with an easy question.

What to expect: A good session gives you feedback after each answer and moves from definitions to trade-offs and real scenarios.

Try it: Compute by hand, then check

A spam filter classifies 200 emails. It marks 40 as spam, and 30 of those really are spam. In total 50 of the 200 emails are spam. Work out TP, FP, FN, TN, then precision, recall and accuracy. Check your answers with scikit-learn by building the label arrays with numpy.

Try it: Pick a threshold for a business rule

Using the fraud code above, find the highest threshold (from 0.05 to 0.5 in steps of 0.05) at which recall is at least 0.5. What is the precision there? Write one sentence on whether you would deploy it.

Key takeaways

  1. Accuracy hides failure on rare classes. Always compare against a do-nothing baseline.
  2. Precision: of my positive predictions, how many are right. Recall: of the real positives, how many I found.
  3. F1 is the harmonic mean of precision and recall; it is low if either is low.
  4. The 0.5 threshold is a default. Move it based on the cost of false alarms versus misses.
  5. ROC AUC measures ranking quality across all thresholds; with very rare positives also check precision-recall.
  6. class_weight and resampling shift the precision-recall balance; they are not free improvements.

Check yourself

Finishing the quiz marks the lesson as done.

1. A cancer screening test finds 90 of 100 patients with cancer and wrongly flags 100 healthy people. Which statement is correct?

2. You lower the classification threshold from 0.5 to 0.2. What usually happens?

3. In a dataset where 99% of rows are negative, a model that always predicts negative has what accuracy and recall for the positive class?

4. What does an ROC AUC of 0.5 mean?

5. An email spam filter must avoid putting real emails in the spam folder. Which metric should you protect most?

6. scikit-learn's confusion_matrix(y_true, y_pred) returns [[50, 5], [10, 35]] for labels 0 and 1. How many false negatives are there?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free