"""标准库指标演示。边界不可计算返回 None；不得把 None 当成零分或满分。"""
from math import log2
from random import Random


def retrieval_metrics(ranking, relevant, k):
    if k <= 0:
        raise ValueError("k 必须大于零")
    if len(ranking) != len(set(ranking)):
        raise ValueError("先按明确的证据粒度去重，不能用重复 ID 增加命中")
    top = ranking[:k]
    hits = len(set(top) & set(relevant))
    first = next((i for i, item in enumerate(top, 1) if item in relevant), None)
    return {"recall": hits / len(relevant) if relevant else None,
            # 本例按固定 k 计分，返回不足 k 的空位置视为未命中。
            "precision": hits / k, "hit": int(hits > 0),
            "reciprocal_rank": 1 / first if first else 0}


def ndcg(ranking, graded_relevance, k):
    if k <= 0 or len(ranking) != len(set(ranking)):
        raise ValueError("k 必须正数，ranking 不允许重复")
    def dcg(grades):
        return sum((2 ** g - 1) / log2(i + 1) for i, g in enumerate(grades, 1))
    # 这里约定未标注为 0；只有候选池标注足够完整时才适合使用该约定。
    actual = dcg([graded_relevance.get(item, 0) for item in ranking[:k]])
    ideal = dcg(sorted(graded_relevance.values(), reverse=True)[:k])
    return actual / ideal if ideal else None


def edit_error_rate(reference, hypothesis):
    """输入词/字符列表。动态规划计算替换、删除和插入的最小总次数。"""
    if not reference:
        return None  # 空参考应单列插入情况，不定义普通 WER/CER。
    previous = list(range(len(hypothesis) + 1))
    for i, ref in enumerate(reference, 1):
        current = [i]
        for j, hyp in enumerate(hypothesis, 1):
            current.append(min(previous[j] + 1, current[j - 1] + 1,
                               previous[j - 1] + (ref != hyp)))
        previous = current
    return previous[-1] / len(reference)


def paired_bootstrap(a, b, repeats=5000, seed=42):
    if not a or len(a) != len(b) or repeats < 100:
        raise ValueError("必须有等长配对样本，且重采样次数至少为 100")
    differences = [new - old for old, new in zip(a, b)]
    rng = Random(seed)
    means = sorted(sum(rng.choice(differences) for _ in differences) / len(differences)
                   for _ in range(repeats))
    # 简化的百分位区间；有重复任务时应先按任务聚合或按组重采样。
    return {"difference": sum(differences) / len(differences),
            "interval": [means[int(repeats * .025)], means[int(repeats * .975) - 1]]}


if __name__ == "__main__":
    print(retrieval_metrics(["d1", "d2", "d4"], {"d1", "d3"}, 3))
    print(ndcg(["d2", "d1", "d3"], {"d1": 2, "d2": 0, "d3": 1}, 3))
    print(edit_error_rate("refund two items".split(), "refund three items now".split()))
    print(paired_bootstrap([1] * 75 + [0] * 10 + [1] * 5 + [0] * 10,
                           [1] * 75 + [1] * 10 + [0] * 5 + [0] * 10))
