60c21ae346
rules 是纯计算,src/rules 共 1761 行:方法.md 第六部分的全部公式、第七部分的余额与扣次、4.2 的入口分配,以及种子与规则登记表。 预期值有三个来源:方法.md 的手算例;tests/fixtures/rules/ 下不看实现写的 Python 脚本;参考项目 calc15、calc14b 里与现行方法一致的部分。审阅做了两轮,每轮都换一个全新上下文的子 Agent,提出的问题都已修复或写明不修的理由。 用户确认的约定(原话"1–8 都同意"):入池、采纳、消融中发现都用严格不等号;成本用 max(0, ĝ₀);没测的格附"含它的最好组合"的净值;战绩在分数原值上判。三个数改为严格递增后,删掉了尖峰的全部专门处理(bfb96ba)。vocab 里 Range 的说明同步改为严格递增。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
495 lines
29 KiB
Python
495 lines
29 KiB
Python
"""生成消融结算、信息价值、选格、押的因子的预期值 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<R̂} 0.5^j,成本 = 2·max(0,ĝ₀)·[(K+|S|)/(K+|S|+1) − K/(K+1)]。"
|
||
"unselected 里附“含这一格的可选组合中净值最大的那个”。fullRule = 全测规则(格数超过余额−1 时按编号取前余额−1 格)。")
|
||
selection_scenario("r1real", [0, 1, 2, 4, 12],
|
||
"真实第 1 轮三格。余额 4 时可选到三格全测;12 时 R̂ 更大、价值更高。")
|
||
selection_scenario("minimize", [2, 3, 6], "越小越好的合成例。")
|
||
selection_scenario("negativeG0", [3, 5],
|
||
"ĝ₀ < 0(各因子事前期望收益都为负):g0 照记原始负值,成本 = 2·max(0, ĝ₀)·[…] = 0,净值 = 价值。")
|
||
|
||
# 价值、成本的纯算例:R̂、m′、成本(含 余额−|S| = 0 与 ĝ₀ < 0)
|
||
vc = []
|
||
for bal, size, K, g0, voi in [(5, 1, 2, 0.01, 0.004), (13, 2, 2, 0.01, 0.004), (3, 3, 2, 0.01, 0.004),
|
||
(4, 1, 3, 0.02, 0.01), (6, 2, 2, -0.005, 0.003)]:
|
||
Rh, mp = R.m_prime(bal, size, K, RR)
|
||
value = Fr(U) * mp * R.fr(voi)
|
||
c = R.cost(g0, size, K)
|
||
vc.append({"input": {"balance": bal, "size": size, "batchCap": K, "g0": g0, "voi": voi},
|
||
"expected": {"Rhat": Rh, "mPrime": F(mp), "value": F(value), "cost": F(c), "net": F(value - c)}})
|
||
S.append({"name": "value_cost_pure", "section": "6.7 价值减成本;约定 28",
|
||
"params": {"u": U, "r": RR}, "input": {"cases": [x["input"] for x in vc]},
|
||
"expected": {"cases": [x["expected"] for x in vc]},
|
||
"note": "R̂ = ⌈(余额−|S|)/每批上限⌉,余额−|S| = 0 时 R̂ = 0、m′ = 0;m′ = Σ_{j<R̂} r^j;"
|
||
"成本 = 2·max(0,ĝ₀)·[(K+|S|)/(K+|S|+1) − K/(K+1)],ĝ₀ < 0 时成本 0。例:余额 13、|S| 2、K 2 → R̂ 6、m′ 1.96875。"})
|
||
|
||
|
||
# 纯选格规则:给定净值表(构造),测 1e-12 容差与平局
|
||
def pure_select(name, cells, bal, nets, note):
|
||
def net(combo):
|
||
return nets[",".join(combo)]
|
||
sel, bestnet, table, uns = R.select_cells(cells, bal, net)
|
||
S.append({"name": name, "section": "6.7 选格第 2–3 条;约定 29",
|
||
"params": {"tolerance": 1e-12},
|
||
"input": {"cells": cells, "balance": bal, "nets": nets},
|
||
"expected": {"selected": sel, "selectedNet": bestnet if sel else 0.0, "unselected": uns},
|
||
"note": note})
|
||
|
||
|
||
pure_select("select_pure_net_at_tolerance", ["p1", "p2"], 3,
|
||
{"p1": 1e-12, "p2": 0.9e-12, "p1,p2": 1e-12},
|
||
"净值都不大于 1e-12:一格都不测;各格 notWorthCost,附各自含它的最大净值组合(同一条规则:差不到 1e-12 视为相同,"
|
||
"先取格数少的)。p2 的附带组合曾是 {p1,p2}(1e-12,按“严格更大才替换”);改后 {p2}(0.9e-12)与它差不到 1e-12,"
|
||
"格数少,取 {p2}。")
|
||
pure_select("select_pure_just_above_tolerance", ["p1", "p2"], 3,
|
||
{"p1": 2.5e-12, "p2": 1e-12, "p1,p2": 3.0e-12},
|
||
"p1 = 2.5e-12 > 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))
|