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.
- validbutwrong/__init__.py +9 -0
- validbutwrong/arms.py +67 -0
- validbutwrong/cli.py +155 -0
- validbutwrong/data/SOURCES.md +9 -0
- validbutwrong/data/bfcl_v3_simple.jsonl +400 -0
- validbutwrong/data/bfcl_v3_simple_possible_answer.jsonl +400 -0
- validbutwrong/data/gsm8k_test_first200.jsonl +200 -0
- validbutwrong/engines.py +149 -0
- validbutwrong/metrics.py +122 -0
- validbutwrong/replays/bfcl-simple.jsonl +201 -0
- validbutwrong/replays/gsm8k.jsonl +201 -0
- validbutwrong/report.py +139 -0
- validbutwrong/run.py +96 -0
- validbutwrong/schema.py +66 -0
- validbutwrong/tasks.py +277 -0
- validbutwrong-0.1.0.dist-info/METADATA +264 -0
- validbutwrong-0.1.0.dist-info/RECORD +22 -0
- validbutwrong-0.1.0.dist-info/WHEEL +5 -0
- validbutwrong-0.1.0.dist-info/entry_points.txt +2 -0
- validbutwrong-0.1.0.dist-info/licenses/LICENSE +21 -0
- validbutwrong-0.1.0.dist-info/licenses/NOTICE +20 -0
- validbutwrong-0.1.0.dist-info/top_level.txt +1 -0
validbutwrong/metrics.py
ADDED
|
@@ -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)]
|