"""Retention (spec ยง7): R = s / t and chance-corrected R_c = (s - c) / (t - c), per task and as means.""" from __future__ import annotations def chance_for(task: str, chance: dict) -> float: return float(chance.get(task, chance.get("default", 0.0))) def retention(student: dict[str, float], teacher: dict[str, float], chance: dict) -> dict: rows = {} for task in student: if task not in teacher: continue s, t, c = student[task], teacher[task], chance_for(task, chance) rows[task] = {"student": s, "teacher": t, "chance": c, "R": s / t if t else float("nan"), "R_c": (s - c) / (t - c) if t != c else float("nan")} n = len(rows) mean = {"R": sum(r["R"] for r in rows.values()) / n, "R_c": sum(r["R_c"] for r in rows.values()) / n} if n else {} return {"tasks": rows, "mean": mean} def markdown(ret: dict, title: str) -> str: lines = [f"## {title}", "", "| Task | Teacher | Student | R | R_c |", "|---|---:|---:|---:|---:|"] for task, r in ret["tasks"].items(): lines.append(f"| {task} | {100 * r['teacher']:.1f} | {100 * r['student']:.1f} | {r['R']:.3f} | {r['R_c']:.3f} |") if ret["mean"]: lines.append(f"| **mean** | | | **{ret['mean']['R']:.3f}** | **{ret['mean']['R_c']:.3f}** |") return "\n".join(lines) + "\n"