Skip to content

Accuracy, F1, micro-F1, and macro-F1

  • Lesson
  • Beginner
  • 15 min read
  • Checked 15 Aug 2026

Imagine a model routes eight bank transactions to normal, fraud, or review. Five predictions are correct.

Rows are truth. Columns are predictions.

Actual ↓ / Predicted → normal fraud review Support
normal 3 1 0 4
fraud 1 1 0 2
review 1 0 1 2

The diagonal contains the five correct predictions. Every off-diagonal cell is a particular mistake that a product owner can name.

accuracy = correct predictions / all predictions
= 5 / 8
= 0.625

Accuracy is easy to explain. It becomes misleading when one common class dominates. A system can label every rare fraud case normal and still look accurate in a mostly normal dataset.

Treat fraud as positive and everything else as negative.

Count Meaning Here
TP Fraud correctly predicted as fraud 1
FP Non-fraud incorrectly predicted as fraud 1
FN Fraud incorrectly predicted as something else 1
fraud precision = TP / (TP + FP) = 1 / 2 = 0.50
fraud recall = TP / (TP + FN) = 1 / 2 = 0.50

Precision asks, “When the system said fraud, how often was it right?” Recall asks, “Of the fraud cases that existed, how many did it find?” The denominator is the difference.

F1 = 2 × precision × recall / (precision + recall)
= 2TP / (2TP + FP + FN)

For fraud, F1 is 0.50. A high precision cannot fully hide low recall, and high recall cannot fully hide low precision. F1 still does not include true negatives, probability calibration, or the different business cost of each error.

5. Micro, macro, and weighted answer different questions

Section titled “5. Micro, macro, and weighted answer different questions”

First calculate F1 for each class by treating that class as positive.

Class Precision Recall F1 Support
normal 0.60 0.75 0.667 4
fraud 0.50 0.50 0.500 2
review 1.00 0.50 0.667 2
Average Calculation Result What it emphasizes
Micro-F1 Add every class’s TP, FP, and FN, then calculate one F1 0.625 Every sample-class decision
Macro-F1 (0.667 + 0.500 + 0.667) / 3 0.611 Every class equally
Weighted-F1 Weight each class F1 by support: (4×.667 + 2×.500 + 2×.667) / 8 0.625 Common classes more heavily

For a single-label multiclass problem that includes every class, micro-precision, micro-recall, micro-F1, and accuracy are equal. This shortcut does not generally hold for multilabel classification, excluded labels, or unusual weighting.

Situation Start with Also inspect
Balanced classes and similar error costs Accuracy Per-class precision, recall, and confusion matrix
Rare classes matter Macro-F1 Rare-class recall and false-negative cases
Dataset frequency should affect the summary Weighted-F1 Macro-F1 so minority failure is not hidden
Missing positives is expensive Recall for the important class Precision, threshold curve, and cost of false positives
False alarms are expensive Precision for the important class Recall and the cases rejected incorrectly
One item may have several labels Micro/macro/sample averages Exact-match or subset accuracy

No average decides the product trade-off for you. In loan, fraud, medical, or safety workflows, report the important class separately and evaluate the human-review route too.

src/examples/evals/classification_metrics.py
from collections import Counter
TRUTH = ["normal", "normal", "normal", "normal", "fraud", "fraud", "review", "review"]
PREDICTED = ["normal", "normal", "normal", "fraud", "normal", "fraud", "review", "normal"]
LABELS = ["normal", "fraud", "review"]
def safe_divide(numerator: int, denominator: int) -> float:
return numerator / denominator if denominator else 0.0
def per_class_scores(truth: list[str], predicted: list[str]) -> dict[str, dict[str, float]]:
scores = {}
for label in LABELS:
tp = sum(actual == label and guess == label for actual, guess in zip(truth, predicted))
fp = sum(actual != label and guess == label for actual, guess in zip(truth, predicted))
fn = sum(actual == label and guess != label for actual, guess in zip(truth, predicted))
precision = safe_divide(tp, tp + fp)
recall = safe_divide(tp, tp + fn)
f1 = safe_divide(2 * tp, 2 * tp + fp + fn)
scores[label] = {"tp": tp, "fp": fp, "fn": fn, "precision": precision, "recall": recall, "f1": f1}
return scores
scores = per_class_scores(TRUTH, PREDICTED)
support = Counter(TRUTH)
accuracy = sum(actual == guess for actual, guess in zip(TRUTH, PREDICTED)) / len(TRUTH)
macro_f1 = sum(item["f1"] for item in scores.values()) / len(LABELS)
weighted_f1 = sum(scores[label]["f1"] * support[label] for label in LABELS) / len(TRUTH)
total_tp = sum(item["tp"] for item in scores.values())
total_fp = sum(item["fp"] for item in scores.values())
total_fn = sum(item["fn"] for item in scores.values())
micro_f1 = safe_divide(2 * total_tp, 2 * total_tp + total_fp + total_fn)
print(f"accuracy: {accuracy:.3f}")
print(f"micro F1: {micro_f1:.3f}")
print(f"macro F1: {macro_f1:.3f}")
print(f"weighted F1: {weighted_f1:.3f}")
for label, item in scores.items():
print(f"{label:>6}: P={item['precision']:.3f} R={item['recall']:.3f} F1={item['f1']:.3f} support={support[label]}")
assert round(accuracy, 3) == 0.625
assert micro_f1 == accuracy # True for this single-label multiclass task when every class is included.
assert round(macro_f1, 3) == 0.611
assert round(weighted_f1, 3) == 0.625

In scikit-learn, use classification_report for the table and f1_score(y_true, y_pred, average="micro" | "macro" | "weighted") for a chosen average.1

  1. Accuracy: How many rows were right?
  2. Precision: When I predicted this class, how often was I right?
  3. Recall: Of this class in reality, how much did I find?
  4. F1: Did precision and recall remain strong together?
  5. Micro: Pool decisions first.
  6. Macro: Score classes first, then give each one equal weight.
  7. Weighted: Score classes first, then weight by how often each occurs.
  1. scikit-learn, f1_score and classification metrics.