validbutwrong 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,122 @@
1
+ """Scoring from stored text: per-item states, metrics, transitions, paired bootstrap.
2
+
3
+ Every metric is a rate over ALL N items of the run (never over the valid subset), so the
4
+ arms are compared on the same denominator:
5
+
6
+ * schema validity output contains a JSON object that validates against the arm's schema
7
+ * answer accuracy the extracted answer is right (task-defined; AST match for BFCL)
8
+ * executable accuracy only for tasks that supply an execution check; otherwise "not computed"
9
+ * wrong-but-valid rate valid AND wrong: the share that "100% valid" hides
10
+
11
+ ``unconstrained`` has no output contract, so its validity is "n/a", not 0.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import random
17
+ from typing import Any
18
+
19
+ from .schema import first_json_object, is_valid
20
+ from .tasks import Item, Task
21
+
22
+ N_BOOT = 2000
23
+ SEED = 42
24
+
25
+
26
+ def score_arm(task: Task, item: Item, key: str, text: str) -> dict:
27
+ if key == "unconstrained":
28
+ ans = task.extract_free(text, item)
29
+ return {"valid": None, "correct": task.is_correct(ans, item), "answer": ans}
30
+ schema_name = key[key.index("[") + 1:-1]
31
+ schema = task.schemas_for(item)[schema_name]
32
+ obj = first_json_object(text)
33
+ ans = task.extract_json(obj, item)
34
+ return {"valid": is_valid(obj, schema), "correct": task.is_correct(ans, item), "answer": ans}
35
+
36
+
37
+ def excluded_ids(records: list[dict]) -> list[str]:
38
+ """Items where the backend could not compile some arm's schema. They are dropped from EVERY
39
+ arm (keeping comparisons paired) and their count is reported, never silently."""
40
+ return [r["id"] for r in records if any("unsupported_schema" in a for a in r["arms"].values())]
41
+
42
+
43
+ def score_run(task: Task, items: list[Item], records: list[dict]) -> dict[str, list[dict]]:
44
+ """-> {arm_key: [state per scored item, in item order]} (excluded items are skipped)"""
45
+ by_id = {r["id"]: r for r in records}
46
+ skip = set(excluded_ids(records))
47
+ states: dict[str, list[dict]] = {}
48
+ for it in items:
49
+ if it.id in skip:
50
+ continue
51
+ for key, arm in by_id[it.id]["arms"].items():
52
+ states.setdefault(key, []).append(score_arm(task, it, key, arm["text"]))
53
+ return states
54
+
55
+
56
+ def _rate(xs: list[float]) -> float:
57
+ return sum(xs) / len(xs) if xs else float("nan")
58
+
59
+
60
+ def _indicators(states: list[dict]) -> dict[str, list[float] | None]:
61
+ valid = None if states[0]["valid"] is None else [float(s["valid"]) for s in states]
62
+ correct = [float(s["correct"]) for s in states]
63
+ wbv = None if valid is None else [float(s["valid"] and not s["correct"]) for s in states]
64
+ return {"schema_validity": valid, "answer_accuracy": correct, "wrong_but_valid": wbv}
65
+
66
+
67
+ def bootstrap_ci(xs: list[float], ys: list[float] | None = None, n_boot: int = N_BOOT,
68
+ seed: int = SEED) -> tuple[float, float]:
69
+ """95% percentile CI of mean(xs), or of the PAIRED difference mean(ys - xs)."""
70
+ rng = random.Random(seed)
71
+ n = len(xs)
72
+ diffs = xs if ys is None else [y - x for x, y in zip(xs, ys)]
73
+ stats = sorted(_rate([diffs[rng.randrange(n)] for _ in range(n)]) for _ in range(n_boot))
74
+ return stats[int(0.025 * n_boot)], stats[int(0.975 * n_boot) - 1]
75
+
76
+
77
+ def quadrant(states: list[dict]) -> dict[str, int] | None:
78
+ if states[0]["valid"] is None:
79
+ return None
80
+ q = {"valid_right": 0, "valid_wrong": 0, "invalid_right": 0, "invalid_wrong": 0}
81
+ for s in states:
82
+ q[f"{'valid' if s['valid'] else 'invalid'}_{'right' if s['correct'] else 'wrong'}"] += 1
83
+ return q
84
+
85
+
86
+ def arm_metrics(states: list[dict]) -> dict[str, Any]:
87
+ out: dict[str, Any] = {"n": len(states)}
88
+ for name, xs in _indicators(states).items():
89
+ out[name] = None if xs is None else {"rate": _rate(xs), "ci95": bootstrap_ci(xs)}
90
+ out["executable_accuracy"] = None # filled only by tasks with an execution check
91
+ out["quadrant"] = quadrant(states)
92
+ return out
93
+
94
+
95
+ def _state(s: dict) -> str:
96
+ v = "n/a" if s["valid"] is None else ("valid" if s["valid"] else "invalid")
97
+ return f"{v}/{'right' if s['correct'] else 'wrong'}"
98
+
99
+
100
+ def compare(a: list[dict], b: list[dict], a_key: str, b_key: str) -> dict[str, Any]:
101
+ """Paired comparison a -> b on the same items: transitions + CI on each metric difference."""
102
+ ia, ib = _indicators(a), _indicators(b)
103
+ diffs = {}
104
+ for m in ("schema_validity", "answer_accuracy", "wrong_but_valid"):
105
+ if ia[m] is None or ib[m] is None:
106
+ continue
107
+ lo, hi = bootstrap_ci(ia[m], ib[m])
108
+ diffs[m] = {"delta": _rate(ib[m]) - _rate(ia[m]), "ci95": (lo, hi), "excludes_0": lo > 0 or hi < 0}
109
+ trans: dict[str, int] = {}
110
+ for sa, sb in zip(a, b):
111
+ k = f"{_state(sa)} -> {_state(sb)}"
112
+ trans[k] = trans.get(k, 0) + 1
113
+ right_to_wrong = sum(1 for sa, sb in zip(a, b) if sa["correct"] and not sb["correct"])
114
+ wrong_to_right = sum(1 for sa, sb in zip(a, b) if not sa["correct"] and sb["correct"])
115
+ return {"from": a_key, "to": b_key, "n": len(a), "deltas": diffs, "transitions": trans,
116
+ "flips": {"right_to_wrong": right_to_wrong, "wrong_to_right": wrong_to_right}}
117
+
118
+
119
+ def ledger(items: list[Item], states: dict[str, list[dict]]) -> list[dict]:
120
+ """Per-item transition ledger: every item's (valid?, right?) state in every arm.
121
+ ``items`` must be the scored items (excluded ones removed), in the same order as ``states``."""
122
+ return [{"id": it.id, **{k: _state(v[i]) for k, v in states.items()}} for i, it in enumerate(items)]