Precision, Recall and F1
Two of the four confusion-matrix cells matter for the positive class, and two ratios built from them answer different questions. Precision asks: "of everything I flagged positive, how much was really positive?" Recall asks: "of everything that was really positive, how much did I flag?" Both are 100% only when there are zero errors of the corresponding type, and in practice you trade one against the other by moving the decision threshold.
Definitions
precision = TP / (TP + FP) - trustworthiness of a positive flag
recall = TP / (TP + FN) - coverage of the real positives
f1 = 2 * precision * recall / (precision + recall) - their harmonic mean
The F1 score is the harmonic mean, which penalizes lopsidedness: a model with precision 1.0 and recall 0.1 scores F1 = 0.18, not the arithmetic 0.55. Use F1 when you want one number that respects both, and weighted-F1 for multiclass reports.
In Python
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import precision_recall_fscore_support
from sklearn.model_selection import train_test_split
X, y = make_classification(n_samples=1000, weights=[0.85, 0.15],
n_informative=5, random_state=5)
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.3, random_state=0)
model = LogisticRegression(max_iter=2000).fit(Xtr, ytr)
p, r, f1, support = precision_recall_fscore_support(
yte, model.predict(Xte), average="binary", zero_division=0)
print(f"precision={p:.3f} recall={r:.3f} f1={f1:.3f} positives={support[1]:.0f}")
When Each Metric Matters
- Precision-first - spam filters, alert fatigue: a false positive is annoying (an important email in spam, a paged engineer with nothing wrong). You would rather miss some real items than flood the queue. Optimize precision; tolerate lower recall.
- Recall-first - cancer screening, fraud with heavy losses, missed defects: a false negative is expensive (a late diagnosis, a stolen card). Optimize recall; accept more false alarms downstream.
- Balanced - when both error types cost about the same, F1 or the ROC area is the fair single number.
The Threshold Trade-Off
proba = model.predict_proba(Xte)[:, 1]
for thr in [0.1, 0.3, 0.5, 0.7, 0.9]:
pred = (proba >= thr).astype(int)
p, r, f1, _ = precision_recall_fscore_support(yte, pred, average="binary")
print(f"thr={thr:.1f} precision={p:.3f} recall={r:.3f} f1={f1:.3f}")
Lower the threshold and recall climbs while precision falls; raise it and the reverse. The curve of that trade-off is the precision-recall curve, and the right operating point is set by the business - the model only supplies the probabilities.
Summary
Precision is trust in your flags, recall is coverage of the real positives, and F1 is their balanced compromise. Choose the metric from the cost of each error type, scan thresholds to see the trade-off, and remember that on imbalanced data these numbers - not accuracy - are the honest report.