Solving tips
- Get the confusion-matrix counts first: TP, FP, FN. Everything else is a ratio of these.
- Precision's denominator is TP+FP (predicted positives); recall's is TP+FN (actual positives). Don't swap them.
- Guard every division: return 0.0 when the denominator is 0 instead of producing nan.
Implement precision, recall, and F1 from scratch with NumPy for a binary classifier, without calling a library metric function. These three numbers summarize how a model trades off catching positives against being right when it says positive, and interviewers use them to check that you know the confusion matrix cold and handle the empty-denominator edge cases.
Definition
Given binary labels (positive class is 1), form the confusion-matrix counts:
- TP (true positives): predicted 1 and actually 1
- FP (false positives): predicted 1 but actually 0
- FN (false negatives): predicted 0 but actually 1
Then:
precision = TP / (TP + FP)
recall = TP / (TP + FN)
F1 = 2 * precision * recall / (precision + recall)
Task
Complete precision_recall_f1(y_true, y_pred) so it returns the tuple (precision, recall, f1) as floats. Both inputs are 1-D NumPy arrays of the same length holding 0/1 labels. Whenever a denominator is 0, return 0.0 for that quantity instead of dividing. Do not call sklearn or any built-in precision/recall/F1 helper.
Example
y_true = np.array([1, 1, 0, 0, 1, 0])
y_pred = np.array([1, 0, 0, 1, 1, 0])
precision_recall_f1(y_true, y_pred) # -> (0.6666666666666666, 0.6666666666666666, 0.6666666666666666)
Here TP = 2, FP = 1, FN = 1, so precision = 2/3, recall = 2/3, and F1 = 2/3.
Constraints
1 <= n <= 10^6; use vectorized NumPy, not a Python loop.
- Every label is exactly 0 or 1; the positive class is 1.
- If
TP + FP == 0, precision is 0.0; if TP + FN == 0, recall is 0.0; if precision + recall == 0, F1 is 0.0.
Approach
Reduce the two label arrays to three scalar counts with boolean masks: TP, FP, and FN. Precision and recall are each one guarded ratio of those counts, and F1 is the guarded harmonic mean of precision and recall. A small helper does the divide-or-zero so the three formulas stay one line each.
Solution
import numpy as np
def precision_recall_f1(y_true, y_pred):
y_true = np.asarray(y_true).astype(bool)
y_pred = np.asarray(y_pred).astype(bool)
tp = int(np.sum(y_pred & y_true))
fp = int(np.sum(y_pred & ~y_true))
fn = int(np.sum(~y_pred & y_true))
def safe_div(num, den):
return num / den if den != 0 else 0.0
precision = safe_div(tp, tp + fp)
recall = safe_div(tp, tp + fn)
f1 = safe_div(2 * precision * recall, precision + recall)
return (precision, recall, f1)
Walkthrough
On the example y_true = [1, 1, 0, 0, 1, 0], y_pred = [1, 0, 0, 1, 1, 0]:
- Cast to bool, then form masks.
y_pred & y_true is True at indices 0 and 4, so tp = 2.
y_pred & ~y_true is True only at index 3 (predicted 1, actual 0), so fp = 1.
~y_pred & y_true is True only at index 1 (predicted 0, actual 1), so fn = 1.
precision = 2 / (2 + 1) = 0.6667, recall = 2 / (2 + 1) = 0.6667.
f1 = 2 * 0.6667 * 0.6667 / (0.6667 + 0.6667) = 0.6667.
- Returns
(0.6667, 0.6667, 0.6667).
Complexity & notes
- Time O(n), space O(n) — each count is one vectorized boolean pass over the arrays.
- Casting to
bool lets &, |, and ~ act as elementwise logical operators; on integer arrays ~ would be bitwise negation (~1 == -2) and give wrong masks, so the cast matters.
- The
safe_div guard is the crux of the problem. A model that predicts all negatives has TP + FP = 0 (precision 0.0) and a model with no actual positives has TP + FN = 0 (recall 0.0); returning 0.0 avoids nan. F1 also collapses to 0.0 when both precision and recall are 0.
- F1 is the harmonic mean of precision and recall, so it stays low unless both are high — that is why it is preferred over accuracy on imbalanced data.
- Note that TN is never needed for any of the three metrics; they all ignore true negatives, which is what makes them useful when the negative class dominates.