How it’s calculated
F1 is the harmonic mean of precision and recall: F1 = 2 × precision × recall ÷ (precision + recall). With 80% precision and 60% recall, F1 is 2 × 0.8 × 0.6 ÷ 1.4 ≈ 0.69.
Why not a simple average? The average of 80% and 60% is 70%, close to F1. But take a model with 100% precision and 2% recall: it flags one case, correctly, and misses everything else. Its simple average is 51%, which sounds acceptable. Its F1 is about 0.04, which correctly says it’s useless. The harmonic mean is pulled down by the weaker of the two numbers, so you can’t hide a terrible recall behind a perfect precision.
When to use it
- The positive class is rare, such as fraud, defects or spam, and accuracy would look good no matter what.
- Both kinds of mistake matter and you don’t have clear costs for each yet.
- You want one number to compare models, for example while trying different features.
When not to use it
If you know what each mistake costs, use those costs instead: pick the threshold with the lowest total cost, or set a minimum precision and maximize recall. F1 treats precision and recall as equally important, which is rarely exactly true. It also ignores true negatives entirely, which is fine for rare-event problems but less useful when both classes matter equally.
Macro F1 for several classes
With more than two classes, such as positive, neutral and negative reviews, compute F1 for each class and average them. That’s macro F1. Each class counts equally, so a model that ignores a small class gets a low score, even if it’s right about the big one. A model that always predicts the most common class has a macro F1 close to zero for every other class, which makes it a good way to expose a lazy model.
CodeF1 and macro F1 with scikit-learn
from sklearn.metrics import f1_score
y_true = ["pos", "pos", "neg", "neu", "pos", "neg"]
y_pred = ["pos", "pos", "neg", "pos", "pos", "pos"]
print("Macro F1:", round(f1_score(y_true, y_pred, average="macro"), 3))
always_pos = ["pos"] * len(y_true)
print("Always 'pos':", round(f1_score(y_true, always_pos, average="macro", zero_division=0), 3))