Python 编程题:算一个质检模型的误报率和漏报率(附参考答案)

题目:AI 视觉质检(或 OCR、缺陷检测)项目上线前,需要拿人工标注结果和模型预测结果做一次离线评估。给定两个等长列表 labels(真实标签,1=有缺陷/正样本,0=正常)和 preds(模型预测标签),实现函数 eval_cls,统计混淆矩阵并返回下列指标(浮点数保留 4 位小数):

  • tp / fp / fn / tn
  • precision(精确率)、recall(召回率)、f1
  • fpr(误报率 = FP/(FP+TN))、fnr(漏报率 = FN/(FN+TP))
  • accuracy

约束:① 两个列表长度不一致必须抛 ValueError;② 任何分母为 0 的指标返回 0.0,不许抛 ZeroDivisionError;③ 不许调用 sklearn 等第三方库。

代码框架:

def eval_cls(labels: list[int], preds: list[int], pos: int = 1) -> dict:
    """离线二分类评估:统计混淆矩阵并计算 P/R/F1/误报率/漏报率。

    Args:
        labels: 人工标注真实标签,取值 0/1
        preds:  模型预测标签,取值 0/1
        pos:    正样本标签值,默认 1
    Returns:
        dict,键为 tp/fp/fn/tn/precision/recall/f1/fpr/fnr/accuracy,
        指标保留 4 位小数,分母为 0 时该指标返回 0.0
    Raises:
        ValueError: labels 与 preds 长度不一致
    """
    pass

自测断言:

labels = [1, 1, 1, 1, 0, 0, 0, 0, 0, 0]
preds  = [1, 1, 1, 0, 1, 0, 0, 0, 0, 0]
r = eval_cls(labels, preds)
assert (r["tp"], r["fn"], r["fp"], r["tn"]) == (3, 1, 1, 5)
assert r["precision"] == 0.75 and r["recall"] == 0.75 and r["f1"] == 0.75
assert r["fpr"] == 0.1667 and r["fnr"] == 0.25 and r["accuracy"] == 0.8
assert eval_cls([0, 0], [0, 0])["precision"] == 0.0        # 无正预测,不能崩
assert eval_cls([1, 1, 0, 0], [1, 1, 0, 0])["f1"] == 1.0   # 完美预测
try:
    eval_cls([1, 0], [1])
    raise AssertionError("should raise")
except ValueError:
    pass

难度:中级
题型:笔试(编程题)

:point_right:参考答案/答题要点:

思路一(直观遍历解,O(n)):四类情况各扫一遍列表,再套公式。写起来最稳,面试官看重的是「分母判零」和「F1 用 2TP 形式」。

def eval_cls(labels, preds, pos=1):
    if len(labels) != len(preds):
        raise ValueError("labels 与 preds 长度必须一致")

    tp = fp = fn = tn = 0
    for i in range(len(labels)):
        if labels[i] == pos and preds[i] == pos:
            tp += 1
        elif labels[i] == pos and preds[i] != pos:
            fn += 1
        elif labels[i] != pos and preds[i] == pos:
            fp += 1
        else:
            tn += 1

    def safe_div(a, b):
        return round(a / b, 4) if b else 0.0

    return {
        "tp": tp, "fp": fp, "fn": fn, "tn": tn,
        "precision": safe_div(tp, tp + fp),
        "recall": safe_div(tp, tp + fn),
        "f1": safe_div(2 * tp, 2 * tp + fp + fn),
        "fpr": safe_div(fp, fp + tn),
        "fnr": safe_div(fn, fn + tp),
        "accuracy": safe_div(tp + tn, len(labels)),
    }

思路二(优雅解:Counter 一次遍历 + lambda 除零保护):把「真实是否正类 / 预测是否正类」当作一个二元组直接计数,代码量减半,也是面试现场最容易加分的写法。

from collections import Counter

def eval_v2(labels, preds, pos=1):
    if len(labels) != len(preds):
        raise ValueError("labels 与 preds 长度必须一致")
    c = Counter((y == pos, p == pos) for y, p in zip(labels, preds))
    tp, fp, fn, tn = c[(True, True)], c[(False, True)], c[(True, False)], c[(False, False)]
    d = lambda a, b: round(a / b, 4) if b else 0.0
    return {
        "tp": tp, "fp": fp, "fn": fn, "tn": tn,
        "precision": d(tp, tp + fp), "recall": d(tp, tp + fn),
        "f1": d(2 * tp, 2 * tp + fp + fn), "fpr": d(fp, fp + tn),
        "fnr": d(fn, fn + tp), "accuracy": d(tp + tn, len(labels)),
    }

思路三(加分解:只数正类,其余靠算术推导):真正需要遍历统计的只有 3 个数——真实正样本总数(TP+FN)、预测为正总数(TP+FP)、两者交集 TP,剩下的 FN、FP 用减法得到,TN = 总数 − TP − FN − FP。遍历次数最少,也最能体现对指标定义的理解。

def eval_v3(labels, preds, pos=1):
    n = len(labels)
    if n != len(preds):
        raise ValueError("labels 与 preds 长度必须一致")
    y_pos = sum(1 for y in labels if y == pos)                                  # TP + FN
    p_pos = sum(1 for p in preds if p == pos)                                   # TP + FP
    tp = sum(1 for y, p in zip(labels, preds) if y == pos and p == pos)         # TP
    fn = y_pos - tp
    fp = p_pos - tp
    tn = n - tp - fn - fp
    d = lambda a, b: round(a / b, 4) if b else 0.0
    return {
        "tp": tp, "fp": fp, "fn": fn, "tn": tn,
        "precision": d(tp, tp + fp), "recall": d(tp, tp + fn),
        "f1": d(2 * tp, 2 * tp + fp + fn), "fpr": d(fp, fp + tn),
        "fnr": d(fn, fn + tp), "accuracy": d(tp + tn, n),
    }

三种写法结果完全一致(已用 2000 组随机样本对拍验证)。

采分点:

  1. 混淆矩阵定义不能写反,fn(漏报)和 fp(误报)是本题最容易翻车的地方;
  2. 分母为 0 的保护(全负预测、全正预测场景必须返回 0.0 而不是抛异常);
  3. 长度不一致抛 ValueError,体现边界意识;
  4. F1 写成 2TP/(2TP+FP+FN) 优于先算 P、R 再做调和平均(少一次浮点误差累积);
  5. 指标语义能口述:质检场景漏报(fnr)代价远大于误报(fpr),阈值应往低漏报调;
  6. 保留 4 位小数用 round,不要输出 0.7499999999。