"""生成消融结算、信息价值、选格、押的因子的预期值 JSON(ablation.json)。 用法(在仓库根目录): python3 tests/fixtures/rules/gen_ablation.py 只用标准库;依赖同目录的 rules_ref.py。网格积分是纯 Python,整份要跑几分钟。重跑结果逐位相同。 对应 docs/方法.md: 6.6 消融结算(撑几成、相对交互量、标签、确认/否定、分不开、五种结局) 6.7 方向收益、成分与权重、信息价值、价值减成本、选格 6.8 押的因子 4.3 ⑰ 消融中发现(采纳线以 B = f(完整) 算) 以及约定表第 17–30 条。 信息价值的两种算法: - 全测:一维精确。本脚本不照约定 26 的"1−∏F 逐段积分",而是在有理数上用 Stieltjes 积分 E_j[min_p X_p](越小越好时 max)精确求,再换成收益(收益是 x 的同一个仿射函数)。 - 部分格:约定 27 的网格,是方法规定的确定近似,只能照规定做(浮点)。 - 另给两个只用于核对的参考:全测时也用网格算一份(gridFullVOI),只测一格时给出精确值 (exactK1VOI,相邻折点之间后验权重是常数,max(一次函数, 常数) 可以精确积分)。 生成的文件:ablation.json,格式: { "generator": "gen_ablation.py", "tolerance": {...}, "models": {模型名: {params, input}}, # 几个场景共用的消融输入 "scenarios": [ {"name", "section", "params", "input", "expected", "note"} ] } - 模型 input:{"B": 基线分数, "full": 完整分数, "knownBest": K* 或 null, "direction", "cells": ["p1", ...], "explanations": [{"id", "author", "cells": {"p1": {"q1","q5","q9"}, ...}}]} params:{"eps", "lambda", "u", "r", "batchCap"} - 场景 input 里的 "model" 字段指向 models 里的名字。 - 成分顺序 = explanations 的顺序,最后一个是开放预留(名字 "reserve")。 - 分数一律是原值;收益、VOI 是收益刻度(已乘 h、除以 G)。 """ import json import math import os import sys import time from fractions import Fraction as Fr sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import rules_ref as R # noqa: E402 HERE = os.path.dirname(os.path.abspath(__file__)) F = float EPS, LAM, U, RR, KCAP = 0.01, 0.1, 0.5, 0.5, 2 PARAMS = {"eps": EPS, "lambda": LAM, "u": U, "r": RR, "batchCap": KCAP} def tri(t): return {"q1": t[0], "q5": t[1], "q9": t[2]} # ------------------------------------------------------------------ 输入模型 MODELS = {} # A:参考项目 calc15 的输入(只用 input 部分)。p1 = 关掉网格起点,p2 = 关掉求解流程。 C15 = [("e1", "researcher-1", (1.4, 1.75, 2.1), (1.9, 2.05, 2.18)), ("e2", "researcher-1", (1.9, 2.3, 2.55), (1.8, 1.95, 2.1)), ("e3", "researcher-1", (1.65, 2.05, 2.4), (1.8, 1.98, 2.15)), ("e4", "researcher-2", (2.3, 2.45, 2.58), (1.85, 2.02, 2.18)), ("e5", "researcher-2", (1.5, 1.95, 2.35), (1.9, 2.06, 2.2)), ("e6", "researcher-2", (1.8, 2.05, 2.3), (1.85, 2, 2.15)), ("e7", "researcher-3", (1.9, 2.15, 2.4), (1.95, 2.1, 2.2)), ("e8", "researcher-3", (2.25, 2.45, 2.55), (1.7, 1.9, 2.05)), ("e9", "researcher-3", (1.7, 1.95, 2.2), (1.85, 2, 2.15))] MODELS["calc15"] = { "B": 0.9597642169962064, "full": 2.6276819772628, "knownBest": 2.635, "direction": "maximize", "cells": ["p1", "p2"], "explanations": [{"id": i, "author": a, "cells": {"p1": tri(c1), "p2": tri(c2)}} for i, a, c1, c2 in C15]} C15_VALID = {"p1": 2.5989997592071172, "p2": 1.7602769492805097} # B:15-real-1 第 1 轮真实消融(三格、三人各两条解释) R1 = { "p1": [(1.2, 2, 2.44), (1.22, 1.4, 1.52), (1.05, 1.35, 1.95), (0.95, 1.3, 2.1), (1.15, 1.36, 1.55), (1.2, 1.42, 2.46)], "p2": [(1.49, 1.54, 1.58), (1.49, 1.54, 1.58), (1.44, 1.53, 1.59), (1.4, 1.52, 1.62), (1.49, 1.535, 1.58), (1.5, 1.53, 1.58)], "p3": [(2.3, 2.44, 2.45), (1.2, 1.42, 1.55), (0.85, 2.4, 2.45), (0.6, 1.45, 2.45), (1.8, 2.42, 2.58), (1.9, 2.44, 2.57)], } R1_AUTH = ["researcher-1", "researcher-1", "researcher-2", "researcher-2", "researcher-3", "researcher-3"] R1_IDS = ["r1-e1-1", "r1-e1-2", "r1-e2-1", "r1-e2-2", "r1-e3-1", "r1-e3-2"] MODELS["r1real"] = { "B": 0.9597642169962064, "full": 1.552176048093269, "knownBest": 2.635983, "direction": "maximize", "cells": ["p1", "p2", "p3"], "explanations": [{"id": R1_IDS[i], "author": R1_AUTH[i], "cells": {p: tri(R1[p][i]) for p in R1}} for i in range(6)]} R1_VALID = {"p1": 2.250011787062861, "p2": 1.9535929459502979, "p3": 2.4389662413796342} # C:越小越好的合成例(两人、三条解释) MODELS["minimize"] = { "B": 0.40, "full": 0.35, "knownBest": 0.33, "direction": "minimize", "cells": ["p1", "p2"], "explanations": [ {"id": "e1", "author": "ra", "cells": {"p1": tri((0.36, 0.37, 0.39)), "p2": tri((0.35, 0.36, 0.37))}}, {"id": "e2", "author": "ra", "cells": {"p1": tri((0.35, 0.355, 0.36)), "p2": tri((0.36, 0.38, 0.40))}}, {"id": "e3", "author": "rb", "cells": {"p1": tri((0.37, 0.38, 0.40)), "p2": tri((0.355, 0.365, 0.38))}}]} MIN_VALID = {"p1": 0.372, "p2": 0.361} # D:四格合成例(calc15 的两格,再派生两格:p3 = p1 − 0.3 + 0.0013·n,p4 = p2 + 0.1 + 0.0007·n,n 是解释序号), # p3、p4 各有 47 个不同折点、46 段 > m₄ = 44,全测网格走到"整格等宽切 m 份"(s = 0)的分支 MODELS["fourCells"] = { "B": 0.9597642169962064, "full": 2.6276819772628, "knownBest": 2.635, "direction": "maximize", "cells": ["p1", "p2", "p3", "p4"], "explanations": [{"id": i, "author": a, "cells": { "p1": tri(c1), "p2": tri(c2), "p3": tri(tuple(round(x - 0.3 + 0.0013 * n, 6) for x in c1)), "p4": tri(tuple(round(x + 0.1 + 0.0007 * n, 6) for x in c2))}} for n, (i, a, c1, c2) in enumerate(C15)]} # E:收益不为正(p1 实测正好等于完整,p2 各成分都预计"关掉更好");F:同一组解释,实测落在全部成分之外 EF_EXPL = [ {"id": "e1", "author": "ra", "cells": {"p1": tri((1.9, 2.0, 2.1)), "p2": tri((2.1, 2.2, 2.3))}}, {"id": "e2", "author": "rb", "cells": {"p1": tri((1.95, 2.0, 2.05)), "p2": tri((2.05, 2.15, 2.25))}}] MODELS["gainNotPositive"] = {"B": 1.0, "full": 2.0, "knownBest": None, "direction": "maximize", "cells": ["p1", "p2"], "explanations": EF_EXPL} # G:ĝ₀ < 0。多数解释预测关掉因子后分数反而比完整高,各因子事前期望收益都为负;成本按 max(0, ĝ₀) = 0 算。 MODELS["negativeG0"] = {"B": 1.0, "full": 2.0, "knownBest": None, "direction": "maximize", "cells": ["p1", "p2"], "explanations": [ {"id": "e1", "author": "ra", "cells": {"p1": tri((2.05, 2.15, 2.3)), "p2": tri((1.9, 2.1, 2.2))}}, {"id": "e2", "author": "rb", "cells": {"p1": tri((2.0, 2.1, 2.2)), "p2": tri((2.02, 2.08, 2.15))}}, {"id": "e3", "author": "rb", "cells": {"p1": tri((1.95, 2.05, 2.1)), "p2": tri((1.85, 2.0, 2.2))}}]} def build(name): m = MODELS[name] expl = [{"id": e["id"], "author": e["author"], "cells": {p: (c["q1"], c["q5"], c["q9"]) for p, c in e["cells"].items()}} for e in m["explanations"]] return R.AblationModel(m["B"], m["full"], m["knownBest"], EPS, LAM, m["direction"], m["cells"], expl) S = [] REPORT = {} def fl(d): return {k: F(v) for k, v in d.items()} # ------------------------------------------------------------------ 6. 撑几成、i、标签 def settle_case(name, B, full, direction, cells, note, section="6.6;约定 17–19"): offs = {c["factor"]: (c["value"] if c["kind"] == "valid" else None) for c in cells} try: G, sup = R.supports(B, full, offs, direction) except ValueError: S.append({"name": name, "section": section, "params": {}, "input": {"B": B, "full": full, "direction": direction, "cells": cells}, "expected": {"error": "totalGainNotPositive"}, "note": note}) return lab, i = R.label(sup) S.append({"name": name, "section": section, "params": {}, "input": {"B": B, "full": full, "direction": direction, "cells": cells}, "expected": {"G": F(G), "supports": {p: (None if v is None else F(v)) for p, v in sup.items()}, "interaction": None if i is None else F(i), "label": lab}, "note": note}) V = "valid" settle_case("label_method_example_independent", 1.00, 1.50, "maximize", [{"factor": "p1", "kind": V, "value": 1.45}, {"factor": "p2", "kind": V, "value": 1.10}], "方法 6.6 示例:撑 0.10、0.80,i = −0.10,各管各(independent)。") settle_case("label_calc15_substitute", 0.9597642169962064, 2.6276819772628, "maximize", [{"factor": "p1", "kind": V, "value": C15_VALID["p1"]}, {"factor": "p2", "kind": V, "value": C15_VALID["p2"]}], "calc15 实测:撑 0.017、0.518,i ≈ −0.465 < −0.2,替代(substitute)。") settle_case("label_r1_negative", 0.9597642169962064, 1.552176048093269, "maximize", [{"factor": p, "kind": V, "value": v} for p, v in R1_VALID.items()], "真实第 1 轮:三格关掉后分数都比完整高,撑全 < 0,negative(不贴标签,i 不给)。") settle_case("label_incomplete_with_other_outcomes", 1.00, 1.50, "maximize", [{"factor": "p1", "kind": V, "value": 1.45}, {"factor": "p2", "kind": "insufficient"}, {"factor": "p3", "kind": "joint", "value": 1.2}, {"factor": "p4", "kind": "untested"}, {"factor": "p5", "kind": "cannotRun"}], "只有有效分数进标签:p2–p5 都不是 valid(joint 有分数也不算),有因子没有有效格 → incomplete。") settle_case("label_negative_beats_incomplete", 1.00, 1.50, "maximize", [{"factor": "p1", "kind": V, "value": 1.6}, {"factor": "p2", "kind": "insufficient"}], "表从上往下判:任一有效格撑 < 0 先命中 negative,即使另一格没有有效分数。") settle_case("label_indispensable", 1.0, 2.0, "maximize", [{"factor": "p1", "kind": V, "value": 1.05}, {"factor": "p2", "kind": V, "value": 1.0}], "撑 0.95、1.0,都 > 0.9:缺一不可(indispensable),虽然 i = 0.95 > 0.2 也先命中这一条。") settle_case("label_complementary", 1.0, 2.0, "maximize", [{"factor": "p1", "kind": V, "value": 1.3}, {"factor": "p2", "kind": V, "value": 1.4}], "撑 0.7、0.6,i = 0.3 > 0.2:互补(complementary)。") settle_case("label_boundary_exact_0.9", 1.0, 2.0, "maximize", [{"factor": "p1", "kind": V, "value": 1.0}, {"factor": "p2", "kind": V, "value": 1.125}], "撑 1.0、0.875;0.875 不 > 0.9,不是缺一不可;i = 0.875 > 0.2 → 互补。用二进制精确数避免舍入。") settle_case("label_minimize_substitute", 0.40, 0.35, "minimize", [{"factor": "p1", "kind": V, "value": MIN_VALID["p1"]}, {"factor": "p2", "kind": V, "value": MIN_VALID["p2"]}], "越小越好:G = 0.40−0.35;撑(p) = (x − 0.35)/G = 0.44、0.22;i = −0.34 → 替代。") settle_case("label_G_not_positive", 1.50, 1.50, "maximize", [{"factor": "p1", "kind": V, "value": 1.45}], "G = 0 时报错。") settle_case("label_G_negative_minimize", 0.40, 0.41, "minimize", [{"factor": "p1", "kind": V, "value": 0.405}], "越小越好时完整比基线还大,G < 0,报错。") # ------------------------------------------------------------------ 确认/否定、分不开、消融中发现 def confirm_case(name, mname, valid, note): m = MODELS[mname] cells = sorted(valid, key=R.factor_key) seqs = {} table = {} for e in m["explanations"]: row = {} for p in cells: c = e["cells"][p] row[p] = "confirmed" if c["q1"] <= valid[p] <= c["q9"] else "refuted" table[e["id"]] = row seqs.setdefault(tuple(row[p] for p in cells), []).append(e["id"]) groups = [ids for ids in seqs.values() if len(ids) >= 2] S.append({"name": name, "section": "6.6 确认与否定、现有测法分不开;约定 21–22", "params": {}, "input": {"model": mname, "valid": valid}, "expected": {"verdicts": table, "inseparableGroups": groups}, "note": note}) confirm_case("confirm_r1_real", "r1real", R1_VALID, "落在 [q1,q9](含两端)记确认。分不开:在全部 valid 格上确认/否定序列相同的两条以上解释为一组(组内按解释顺序)。") confirm_case("confirm_calc15", "calc15", C15_VALID, "calc15 两格都有效。") confirm_case("confirm_endpoint_inclusive", "gainNotPositive", {"p1": 2.1, "p2": 2.05}, "端点:e1 的 p1 上沿正好 2.1、e2 的 p2 下沿正好 2.05,都算确认;e2 在 p1(上沿 2.05)否定,e1 在 p2(下沿 2.1)否定。") def discovery(mname, cells, note): m = MODELS[mname] line = R.adoption_line(EPS, m["full"], m["knownBest"], m["direction"]) over = [] for c in cells: if c["kind"] == "valid" or (c["kind"] == "joint" and c.get("value") is not None): g = R.improve(c["value"], m["full"], m["direction"]) if g > line: over.append(c) best = None for c in sorted(over, key=lambda c: R.factor_key(c["factor"])): if best is None or R.improve(c["value"], best["value"], m["direction"]) > 0: best = c S.append({"name": "discovery_" + mname, "section": "4.3 ⑰、6.5;约定 23", "params": {"eps": EPS}, "input": {"model": mname, "cells": cells}, "expected": {"line": F(line), "overLine": [c["factor"] for c in over], "newBaseline": None if best is None else best["factor"]}, "note": note}) discovery("r1real", [{"factor": p, "kind": "valid", "value": v} for p, v in R1_VALID.items()], "B 取 f(完整)=1.5522,采纳线 min(0.01,(2.635983−1.5522)/4)=0.01。三格都过线,取分数最好的 p3。") discovery("calc15", [{"factor": "p1", "kind": "valid", "value": C15_VALID["p1"]}, {"factor": "p2", "kind": "joint", "value": 2.64}], "采纳线 = (2.635−2.62768)/4 ≈ 0.00183;p1 低于完整不过线;p2 是构造的一起关格、分数 2.64(不是真实数据),参与并过线。") discovery("gainNotPositive", [{"factor": "p1", "kind": "valid", "value": 2.5}, {"factor": "p2", "kind": "valid", "value": 2.5}], "无已知最好,采纳线 ε;两格同分 2.5 都过线,取因子编号小的 p1。") # ------------------------------------------------------------------ 7. 成分、h、先验、ĝ₀、VOI def model_basics(name, mod): return { "G": F(mod.G), "h": F(mod.h), "componentNames": mod.names, "weights": [F(w) for w in mod.weights], "reserve": {p: [F(lo), F(hi)] for p, (lo, hi) in mod.reserve.items()}, "componentExpectedPayoff": {n: fl(E) for n, E in zip(mod.names, mod.Epay)}, "priorExpectedPayoff": fl(mod.prior_payoffs()), "g0": F(mod.g0()), } def subsets(cells): from itertools import combinations out = [] for n in range(1, len(cells) + 1): out += [list(c) for c in combinations(cells, n)] return out VOI_CACHE = {} def voi_of(name, mod, S_): key = (name, tuple(S_)) if key in VOI_CACHE: return VOI_CACHE[key] if len(S_) == len(mod.cells): v = (F(mod.voi_full_exact()), {"method": "exact1D"}) else: t = time.time() g, info = mod.voi_grid(S_) info["method"] = "grid" print(f" grid {name} {S_}: {time.time() - t:.1f}s", flush=True) v = (g, info) VOI_CACHE[key] = v return v def voi_scenario(name, extra_grid_full=True, partial=None): mod = build(name) exp = model_basics(name, mod) full_exact = mod.voi_full_exact() exp["voiFullExact"] = F(full_exact) VOI_CACHE[(name, tuple(mod.cells))] = (F(full_exact), {"method": "exact1D"}) parts = [] for S_ in (partial if partial is not None else subsets(mod.cells)[:-1]): v, info = voi_of(name, mod, S_) row = {"cells": S_, "voiGrid": v, "grid": info} if len(S_) == 1: ex = F(mod.voi_partial_exact_k1(S_[0])) row["exactK1VOI"] = ex row["gridMinusExact"] = v - ex parts.append(row) exp["partial"] = parts if extra_grid_full: t = time.time() g, info = mod.voi_grid(mod.cells) print(f" grid-full {name}: {time.time() - t:.1f}s", flush=True) exp["gridFullVOI"] = g exp["gridFullInfo"] = info exp["gridFullMinusExact"] = g - F(full_exact) REPORT[name] = {"exact": F(full_exact), "grid": g, "diff": g - F(full_exact), "rel": (g - F(full_exact)) / F(full_exact)} S.append({"name": "voi_" + name, "section": "6.7 方向收益、成分与权重、信息价值;约定 24–27", "params": PARAMS, "input": {"model": name}, "expected": exp, "note": "weights/componentExpectedPayoff 的成分顺序 = explanations 顺序 + reserve。" "voiFullExact:全部格都测,一维精确(有理数 Stieltjes 积分)。partial[].voiGrid:照约定 27 的网格。" "partial[].exactK1VOI:只测一格时的精确值,只用来看网格误差,实现不需要算。" "gridFullVOI:全测时也用网格算一份,与 voiFullExact 对照(gridFullMinusExact)。" "grid.cells 给出每格折点数、段数、s、小段数:网格折点在浮点上按 q1-(q5-q1)、q9+(q9-q5)、" "L-W/2、U+W/2 计算后去重,实现应先核对这几个数一致(差一个 ulp 就会多出一段宽约 2e-16 的段," "例如 calc15 p1 里 1.0499999999999998 与 1.05 并存)。"}) return mod print("VOI ...", flush=True) mods = {} mods["calc15"] = voi_scenario("calc15") mods["r1real"] = voi_scenario("r1real") mods["minimize"] = voi_scenario("minimize") mods["fourCells"] = voi_scenario("fourCells", partial=[["p1", "p2", "p3"]]) mods["negativeG0"] = voi_scenario("negativeG0") assert mods["negativeG0"].g0() < 0, "negativeG0 的 ĝ₀ 应为负" # ------------------------------------------------------------------ 8. 价值、成本、净值、选格 def net_table(name, mod, bal): g0 = mod.g0() def net(combo): combo = list(combo) v, _ = voi_of(name, mod, combo) Rh, mp = R.m_prime(bal, len(combo), KCAP, RR) value = U * F(mp) * v c = F(R.cost(g0, len(combo), KCAP)) return value - c return net def selection_scenario(name, balances, note): mod = mods[name] out = [] for bal in balances: net = net_table(name, mod, bal) sel, bestnet, table, uns = R.select_cells(mod.cells, bal, net) rows = [] for combo, n in table: v, _ = voi_of(name, mod, list(combo)) Rh, mp = R.m_prime(bal, len(combo), KCAP, RR) rows.append({"combo": list(combo), "voi": v, "Rhat": Rh, "mPrime": F(mp), "value": U * F(mp) * v, "cost": F(R.cost(mod.g0(), len(combo), KCAP)), "net": n}) # 全测规则 lim = max(0, bal - 1) full_sel = mod.cells[:lim] out.append({"balance": bal, "combos": rows, "selected": sel, "selectedNet": bestnet if sel else 0.0, "unselected": uns, "fullRule": {"selected": full_sel, "insufficientBudget": [c for c in mod.cells if c not in full_sel]}}) S.append({"name": "select_" + name, "section": "6.7 价值减成本、选格;约定 28–29、文末", "params": PARAMS, "input": {"model": name, "balances": balances}, "expected": {"g0": F(mod.g0()), "byBalance": out}, "note": note}) print("selection ...", flush=True) selection_scenario("calc15", [0, 1, 2, 3, 10], "余额 0、1 不做消融,各格 reason=insufficientBudget、bestCombo=null;余额 2 只能选单格;" "余额 3 起可以两格全测(VOI 用一维精确值),部分格用网格。" "R̂ = ⌈(余额−|S|)/每批上限⌉,m′ = Σ_{j 1e-12,入选;{p1,p2} = 3.0e-12 只比当前最好多 0.5e-12(不超过 1e-12),视为相同,保留格数少的 p1。") pure_select("select_pure_tie_chain", ["p1", "p2", "p3"], 4, {"p1": 0.01, "p2": 0.01 + 0.8e-12, "p3": 0.01 + 1.6e-12, "p1,p2": 0.0, "p1,p3": 0.0, "p2,p3": 0.0, "p1,p2,p3": -1.0}, "最大净值 M = p3 = 0.01+1.6e-12;净值 ≥ M − 1e-12 的 p2、p3 视为相同,同格数按编号取小的 p2。" "(预期值曾按“逐个扫描、比当前最好多 1e-12 才替换”的接力写法选 p3;审阅指出方法 6.7 是先求最大净值、" "再在差不到 1e-12 的组合里按格数和编号挑,改后选 p2。)") pure_select("select_pure_filter_before_max", ["p1", "p2"], 3, {"p1": 0.6e-12, "p2": -1.0, "p1,p2": 1.5e-12}, "第二轮审阅的反例:先只留净值 > 1e-12 的组合(只剩 {p1,p2}),再在其中求最大值和容差窗口,选 {p1,p2}。" "若先在全部组合里求 M = 1.5e-12、窗口 ≥ 0.5e-12,{p1}(0.6e-12)会因格数少被选中,而它的净值并不 > 1e-12。" "p1、p2 都被选中,没有没测的格。") pure_select("select_pure_tolerance_relay", ["p1", "p2"], 3, {"p1": 1.0, "p2": 1.0 + 0.9e-12, "p1,p2": 1.0 + 1.8e-12}, "审阅给的接力反例:M = {p1,p2} = 1+1.8e-12,窗口 ≥ 1+0.8e-12 内有 {p2} 和 {p1,p2},先取格数少的 {p2}。" "接力写法会因 {p2} 比 {p1} 只多 0.9e-12 不替换、{p1,p2} 比 {p1} 多 1.8e-12 替换而选 {p1,p2}。" "p1 没选中,含它的组合 {p1}、{p1,p2} 里同样按这条挑:M = 1+1.8e-12,窗口内只有 {p1,p2}。") pure_select("select_pure_budget_reason", ["p1", "p2", "p3"], 2, {"p1": 0.002, "p2": 0.003, "p3": -0.001}, "余额 2 只能测 1 格:选 p2;p1、p3 再加进来就超过余额−1,reason = insufficientBudget。") # ------------------------------------------------------------------ 9. 押的因子 def bet_case(name, mname, outcomes, note): mod = mods.get(mname) or build(mname) res = R.bet(mod, outcomes) S.append({"name": name, "section": "6.8;约定 30", "params": PARAMS, "input": {"model": mname, "outcomes": outcomes}, "expected": res, "note": note}) bet_case("bet_calc15_all_valid", "calc15", {"p1": {"kind": "valid", "value": C15_VALID["p1"]}, "p2": {"kind": "valid", "value": C15_VALID["p2"]}}, "两格都有效:收益用实测换算,押 p2。weights 是按两格密度更新后的成分权重(只用于没有有效分数的格,这里只是记录)。") bet_case("bet_calc15_p2_insufficient", "calc15", {"p1": {"kind": "valid", "value": C15_VALID["p1"]}, "p2": {"kind": "insufficient"}}, "p2 证据不足:权重 ∝ 起始权重 × 各成分在 p1 实测处的密度,再用新权重估 p2 的期望收益。") bet_case("bet_calc15_joint_ignored", "calc15", {"p1": {"kind": "valid", "value": C15_VALID["p1"]}, "p2": {"kind": "joint", "value": 1.76}}, "p2 是一起关,它的分数不参与权重更新,也不当实测用;结果应与 p2 证据不足那一例完全相同。") bet_case("bet_r1_negative_support", "r1real", {p: {"kind": "valid", "value": v} for p, v in R1_VALID.items()}, "三格撑都 < 0:不押,negativeFactors 记“关掉反而更好”的分数(x − 完整,按方向,正数)。挑出的因子与预测收益照记。" "另外:p2 实测 1.9536 落在 p2 所有成分(含预留 [1.06,1.94])之外,各成分在三格密度连乘全为 0," "权重无从更新、保持起始权重(weightsUpdated=false)——真实数据里就出现了这种情况。") bet_case("bet_gain_not_positive", "gainNotPositive", {"p1": {"kind": "valid", "value": 2.0}, "p2": {"kind": "untested"}}, "p1 实测正好等于完整,收益 0(撑 0,不是负);p2 各成分都预计关掉更好,估计收益为负。最大收益 0 ≤ 0,不押。") bet_case("bet_weights_not_updatable", "gainNotPositive", {"p1": {"kind": "valid", "value": -5.0}, "p2": {"kind": "untested"}}, "p1 实测 −5 落在所有成分(含预留)之外,密度全为 0,权重无从更新:保持起始权重,weightsUpdated=false。") bet_case("bet_minimize", "minimize", {"p1": {"kind": "valid", "value": MIN_VALID["p1"]}, "p2": {"kind": "cannotRun"}}, "越小越好:收益 = h·(x − 完整)/G,h = max(完整 − K*, ε/100) = 0.02。") doc = {"generator": "gen_ablation.py", "tolerance": { "exact": "有理数精确算出的量(G、撑、i、h、权重、成分期望收益、ĝ₀、voiFullExact、exactK1VOI、成本、m′):绝对误差 1e-12", "grid": "网格 VOI 及由它得到的价值、净值:|差| ≤ 1e-12 + 1e-9·|值|(逐项浮点累加顺序不同,约 1e-13 量级)", "discrete": "标签、选中组合、原因、押的因子、状态:必须完全相同", "gridVsExact": "同一输入全测时 gridFullVOI 与 voiFullExact 的对照(两种算法之间,不是实现与本脚本之间):" "k ≤ 3 时 |差| ≤ 5e-4·|精确值|,k = 4 时 ≤ 3e-3·|精确值|;本脚本实测相对差 7.3e-6(calc15,k=2)、" "4.7e-5(minimize,k=2)、1.05e-4(r1real,k=3,s=5–7)、1.0e-3(fourCells,k=4,s=1 和 0)。" "理由:网格用小段中点代表整段,误差随小段宽度平方缩小、随格数增多(m 变小)变大;" "s ≥ 1 时各成分在小段内是均匀分布,max 是凸函数,按 Jensen 不等式网格值不会超过精确值," "所以还可以断言 gridFullVOI ≤ voiFullExact + 1e-12(fourCells 有 s = 0 的格,不适用)。"}, "models": {k: {"params": PARAMS, "input": v} for k, v in MODELS.items()}, "scenarios": S} with open(os.path.join(HERE, "ablation.json"), "w", encoding="utf-8") as f: json.dump(doc, f, ensure_ascii=False, indent=1) f.write("\n") print(json.dumps(REPORT, indent=1))