"""rules 预期值的独立参考实现(共享库,本身不生成文件)。 用途:被同目录的 gen_forward_info.py、gen_ablation.py 导入,算出预期值写成 JSON, 供 TypeScript 的 rules 测试读取。只用 Python 标准库。 独立性:本文件只照 docs/方法.md 第二部分、4.2、第六、七部分的文字, 以及用户确认的约定表(scratchpad/conventions.md,下称"约定 N")写成, 没有读过 src/rules/ 和参考项目的实现代码。 算法上刻意和"照约定逐段积分 1−∏F"的写法分开: - 分布全部用有理数(fractions.Fraction,浮点输入先精确转成有理数)表示; - 期望一律用 Stieltjes 积分 E[g(M)] = ∫ g(t)·F_M'(t)dt 精确求, 其中 M 是若干独立变量的最大值,F_M = ∏F(不是 ∫[1−∏F]dt 那条公式); - 最小值通过取负号转成最大值; - 部分格信息价值的网格算法是方法规定的确定近似(6.7、约定 27),只能照规定做,用浮点。 记号:一条"点列"是 [(x, y), ...],x 严格递增、y 从 0 到 1 不减。三个数严格递增(方法.md 第五部分), 折线连续,"低于 x"与"不高于 x"的把握相同。 """ from fractions import Fraction as Fr from itertools import combinations import bisect import functools import math import re # ---------------------------------------------------------------- 基础 def fr(x): """浮点或整数精确转成有理数。""" return x if isinstance(x, Fr) else Fr(x) def to_float(x): return float(x) def polyline(q1, q5, q9): """6.2:三个数 → 五点折线(有理数)。""" q1, q5, q9 = fr(q1), fr(q5), fr(q9) if not (q1 < q5 < q9): raise ValueError("三个数必须严格递增 q1 < q5 < q9(方法.md 第五部分)") return [(q1 - (q5 - q1), Fr(0)), (q1, Fr(1, 10)), (q5, Fr(1, 2)), (q9, Fr(9, 10)), (q9 + (q9 - q5), Fr(1))] def uniform_pts(lo, hi): """[lo, hi] 上均匀分布的点列。""" return [(fr(lo), Fr(0)), (fr(hi), Fr(1))] def reflect(pts, b): """X → b − X 的点列:P(b−X < y) = 1 − P(X ≤ b−y)。点反序,横坐标 b−x,纵坐标 1−y。""" b = fr(b) return [(b - x, 1 - y) for (x, y) in reversed(pts)] def shift(pts, b): """X → X − b。""" b = fr(b) return [(x - b, y) for (x, y) in pts] def negate(pts): """X → −X。""" return reflect(pts, 0) # 单条点列"低于 x"的把握 F(x)。 def pts_left(pts, x): xs = [p[0] for p in pts] i = bisect.bisect_left(xs, x) - 1 # 最后一个 x_i < x if i < 0: return Fr(0) if i == len(pts) - 1: return Fr(1) (x0, y0), (x1, y1) = pts[i], pts[i + 1] return y0 + (y1 - y0) * (x - x0) / (x1 - x0) # x0 < x ≤ x1 def pts_density(pts, x): """约定 4:所在段斜率,段左闭右开(落在折点上取右边那段);范围外 0。""" x = fr(x) for (x0, y0), (x1, y1) in zip(pts, pts[1:]): if x0 <= x < x1: return (y1 - y0) / (x1 - x0) return Fr(0) class Dist: """若干点列的加权混合(权重和为 1)。""" def __init__(self, comps): self.comps = [(fr(w), p) for (w, p) in comps if fr(w) != 0] def left(self, x): x = fr(x) return sum((w * pts_left(p, x) for w, p in self.comps), Fr(0)) def breakpoints(self): s = set() for _, p in self.comps: for x, _ in p: s.add(x) return s def lo(self): return min(self.breakpoints()) def hi(self): return max(self.breakpoints()) def single(pts): return Dist([(1, pts)]) # ---------------------------------------------------------------- 多项式(有理系数,低次在前) def pmul(a, b): out = [Fr(0)] * (len(a) + len(b) - 1) for i, ai in enumerate(a): if ai == 0: continue for j, bj in enumerate(b): out[i + j] += ai * bj return out def pderiv(a): return [i * a[i] for i in range(1, len(a))] or [Fr(0)] def pint(a, lo, hi): """∫_lo^hi a(t) dt。""" s = Fr(0) for i, c in enumerate(a): s += c * (hi ** (i + 1) - lo ** (i + 1)) / (i + 1) return s def expect_max(dists, floor0=False): """E[max_i X_i](各 X_i 独立),floor0=True 时求 E[max(0, max_i X_i)]。 Stieltjes 精确积分:F_M = ∏F_i 连续;相邻折点之间各 F_i 都是一次函数,∏ 是多项式,对 t·(∏)' 精确积分。 floor0 时 0 以下的把握都算在 0 上,贡献 0。 """ bps = sorted(set().union(*[d.breakpoints() for d in dists])) total = Fr(0) for a, b in zip(bps, bps[1:]): if floor0 and b <= 0: continue # 用平移后的变量 u = t − a 降低有理数的大小 w = b - a poly = [Fr(1)] for d in dists: fa, fb = d.left(a), d.left(b) poly = pmul(poly, [fa, (fb - fa) / w]) dp = pderiv(poly) lo = Fr(0) if floor0 and a < 0: lo = -a # t = u + a integrand = pmul([a, Fr(1)], dp) total += pint(integrand, lo, w) return total def expect_min(dists): """E[min_i X_i] = −E[max_i (−X_i)]。""" neg = [Dist([(w, negate(p)) for w, p in d.comps]) for d in dists] return -expect_max(neg) def mean(d): return expect_max([d]) def quantile(d, p): """约定 8:inf{x : F(x) ≥ p},精确求。F 连续、逐段一次;几条折线之间有空当时 F 走平,取平段左端。""" p = fr(p) bps = sorted(d.breakpoints()) for i, a in enumerate(bps): if d.left(a) >= p: return a b = bps[i + 1] fa, fb = d.left(a), d.left(b) if fb >= p: return a + (p - fa) * (b - a) / (fb - fa) return bps[-1] # ---------------------------------------------------------------- 方向与合并 def improvement_pts(raw_pts, b, direction): """约定 5:分数原值的点列 → 改进量 Y 的点列。""" if direction == "maximize": return shift(raw_pts, b) if direction == "minimize": return reflect(raw_pts, b) raise ValueError(direction) def improve(x, ref, direction): """x 相对 ref 的改进量(按方向):越大越好时 x − ref。""" x, ref = fr(x), fr(ref) return x - ref if direction == "maximize" else ref - x def reserve_bounds(pts_list): """6.3 / 约定 6:全部点列的最低首点 L、最高末点 U,开放预留铺在 [L−W/2, U+W/2]。""" L = min(p[0][0] for p in pts_list) U = max(p[-1][0] for p in pts_list) W = U - L return L - W / 2, U + W / 2 def forward_dists(cands, b, direction, lam): """正向合并:cands = {候选编号: [每位研究者的三个数]},返回 (改进量上的合并分布, 原值上的合并分布, 预留端点)。 每人一份、等权;开放预留取本次选批全部候选、全部人的折线(含池外候选)。 """ lam = fr(lam) raw = {c: [polyline(*t) for t in triples] for c, triples in cands.items()} allraw = [p for ps in raw.values() for p in ps] rlo, rhi = reserve_bounds(allraw) ylist = [improvement_pts(p, b, direction) for p in allraw] ylo, yhi = reserve_bounds(ylist) out_y, out_raw = {}, {} for c, ps in raw.items(): n = len(ps) out_y[c] = Dist([((1 - lam) / n, improvement_pts(p, b, direction)) for p in ps] + [(lam, uniform_pts(ylo, yhi))]) out_raw[c] = Dist([((1 - lam) / n, p) for p in ps] + [(lam, uniform_pts(rlo, rhi))]) return out_y, out_raw, (ylo, yhi), (rlo, rhi) def explanation_weights(authors, lam): """6.7 / 约定 24:解释合起来 1−λ,研究者等权,同一人几条平分。返回与 authors 同序的权重。""" lam = fr(lam) n_auth = len(set(authors)) cnt = {a: authors.count(a) for a in set(authors)} return [(1 - lam) / n_auth / cnt[a] for a in authors] def cell_dist(triples, authors, lam): """消融一格的成分混合(原值):各解释折线 + 这一格自己的开放预留。""" pts = [polyline(*t) for t in triples] lo, hi = reserve_bounds(pts) ws = explanation_weights(authors, lam) return Dist([(w, p) for w, p in zip(ws, pts)] + [(fr(lam), uniform_pts(lo, hi))]), (lo, hi) # ---------------------------------------------------------------- 正向:入池、V、选批、读数 def pool_prob(dy, eps): """约定 9:P(Y < −ε)。""" return dy.left(-fr(eps)) def V(dys): """6.4:批内最好期望 E[max(0, max_c Y_c)]。空集为 0。""" if not dys: return Fr(0) return expect_max(dys, floor0=True) def _str_cmp(a, b): """JS 默认字符串比较(UTF-16 码元)。""" x, y = a.encode("utf-16-be"), b.encode("utf-16-be") return -1 if x < y else (1 if x > y else 0) def _seg_cmp(x, y): if not (x[0].isdigit() and y[0].isdigit()): return _str_cmp(x, y) tx, ty = x.lstrip("0") or "0", y.lstrip("0") or "0" if len(tx) != len(ty): return -1 if len(tx) < len(ty) else 1 return _str_cmp(tx, ty) def cmp_ids(a, b): """编号的自然序(方法"编号最小"指数字意义上的大小):c2 < c10,researcher-2 < researcher-10,p2 < p10。 编号切成数字段和非数字段逐段比:都是数字段时先比去掉前导零后的位数、再比字符串;否则按字符串比; 段先用完的排前面;逐段都相同时按整串字符串比。候选、研究者、入口、因子、解释编号都用它。 """ sa, sb = re.findall(r"\d+|\D+", a), re.findall(r"\d+|\D+", b) for x, y in zip(sa, sb): c = _seg_cmp(x, y) if c: return c if len(sa) != len(sb): return -1 if len(sa) < len(sb) else 1 return _str_cmp(a, b) id_key = functools.cmp_to_key(cmp_ids) def combo_key(combo): """组合的先后:格数少的在前,再按编号逐个比。""" return (len(combo), [id_key(c) for c in combo]) def select_batch(dys, pool, k): """约定 11:枚举池中所有 min(k,|池|) 组合,V 最大;V 完全相等时按排序后编号逐个比(自然序),小者优先。""" size = min(k, len(pool)) ids = sorted(pool, key=id_key) table = [] for combo in combinations(ids, size): table.append((list(combo), V([dys[c] for c in combo]))) best = None for combo, v in table: if best is None or v > best[1]: best = (combo, v) elif v == best[1] and combo_key(combo) < combo_key(best[0]): best = (combo, v) return table, best def adoption_line(eps, B, Kstar, direction): """6.5 / 约定 15。""" eps = fr(eps) if Kstar is None: return eps room = improve(Kstar, B, direction) # 越大越好时 K* − B line = min(eps, room / 4) return max(line, eps / 100) # ---------------------------------------------------------------- 消融结算 def supports(B, full, offs, direction): """6.6 / 约定 17:撑(p) = improvement(关掉 p, 完整)/G;G ≤ 0 抛错。offs = {因子: 分数或 None}。""" G = improve(full, B, direction) if G <= 0: raise ValueError("G ≤ 0") return G, {p: (None if x is None else improve(full, x, direction) / G) for p, x in offs.items()} def label(sup): """6.6 表,从上往下。sup = {因子: 撑 或 None(没有有效分数)}。""" vals = [v for v in sup.values() if v is not None] if any(v < 0 for v in vals): return "negative", None if any(v is None for v in sup.values()): return "incomplete", None i = sum(vals) - 1 if all(v > Fr(9, 10) for v in vals): return "indispensable", i if i > Fr(1, 5): return "complementary", i if i < -Fr(1, 5): return "substitute", i return "independent", i def factor_key(p): """因子编号也按自然序。""" return id_key(p) # ---------------------------------------------------------------- 信息价值 class AblationModel: """一次消融的成分模型。cells: 因子编号列表(p1…);expl: [{id, author, cells: {p: (q1,q5,q9)}}]。""" def __init__(self, B, full, Kstar, eps, lam, direction, cells, expl): self.B, self.full, self.eps, self.lam, self.dir = fr(B), fr(full), fr(eps), fr(lam), direction self.Kstar = None if Kstar is None else fr(Kstar) self.cells = sorted(cells, key=factor_key) self.G = improve(self.full, self.B, direction) if self.G <= 0: raise ValueError("G ≤ 0") self.h = Fr(1) if self.Kstar is None else max(improve(self.Kstar, self.full, direction), self.eps / 100) authors = [e["author"] for e in expl] self.weights = explanation_weights(authors, lam) + [self.lam] self.names = [e["id"] for e in expl] + ["reserve"] # comp_pts[j][p]:成分 j 在格 p 的点列 self.comp_pts = [{p: polyline(*e["cells"][p]) for p in self.cells} for e in expl] self.triples = {p: [tuple(float(q) for q in e["cells"][p]) for e in expl] for p in self.cells} res = {} self.reserve = {} for p in self.cells: lo, hi = reserve_bounds([c[p] for c in self.comp_pts]) self.reserve[p] = (lo, hi) res[p] = uniform_pts(lo, hi) self.comp_pts.append(res) # 成分在每格的期望收益(精确) self.Epay = [{p: self.payoff(mean(single(c[p]))) for p in self.cells} for c in self.comp_pts] def payoff(self, x): """约定 25:收益 = h·improvement(x, 完整)/G。""" return self.h * improve(self.full, x, self.dir) / self.G def prior_payoffs(self, weights=None): w = self.weights if weights is None else weights return {p: sum(wj * E[p] for wj, E in zip(w, self.Epay)) for p in self.cells} def g0(self): return max(self.prior_payoffs().values()) def voi_full_exact(self): """全测 VOI,一维精确:Σ_j w_j E_j[max_p 收益_p] − max_p Σ_j w_j E_j[收益_p]。 收益是 x 的同一个仿射函数,max 收益对应 min x(越大越好)或 max x(越小越好)。 """ first = Fr(0) for wj, c in zip(self.weights, self.comp_pts): ds = [single(c[p]) for p in self.cells] ext = expect_min(ds) if self.dir == "maximize" else expect_max(ds) first += wj * self.payoff(ext) return first - self.g0() # ---- 网格(约定 27),浮点 @staticmethod def m_for(k): m = int(round(4_000_000 ** (1 / k))) # 先取浮点近似,再用整数运算校正 while m ** k > 4_000_000: m -= 1 while (m + 1) ** k <= 4_000_000: m += 1 return m def cell_grid(self, p, m): """一格的小段:返回 [(中点, [各成分质量])] 与小段说明。""" fpts = self.float_pts(p) bps = sorted(set(x for pts in fpts for x, _ in pts)) nseg = len(bps) - 1 s = min(20, m // nseg) edges = [] if s >= 1: for a, b in zip(bps, bps[1:]): for i in range(s): edges.append(a + (b - a) * i / s) edges.append(bps[-1]) else: a, b = bps[0], bps[-1] for i in range(m): edges.append(a + (b - a) * i / m) edges.append(b) out = [] n = len(edges) - 1 for i in range(n): lo, hi = edges[i], edges[i + 1] mass = [] for fp in fpts: mass.append(f_left(fp, hi) - f_left(fp, lo)) out.append(((lo + hi) / 2, mass)) return out, {"breakpoints": len(bps), "segments": nseg, "s": s, "subsegments": n} def float_pts(self, p): """网格用的浮点点列:照方法原式在浮点上算尾巴 q1−(q5−q1)、q9+(q9−q5) 与预留 L−W/2、U+W/2。 网格的折点个数决定 s,横坐标差一个 ulp 就可能多出或少掉一段(例如 1.4−(1.75−1.4) 得到 1.0499999999999998,与另一条折线的 1.05 并存),所以这里必须和实现在同一套浮点式子上取折点。 """ out = [] for (q1, q5, q9) in self.triples[p]: out.append([(q1 - (q5 - q1), 0.0), (q1, 0.1), (q5, 0.5), (q9, 0.9), (q9 + (q9 - q5), 1.0)]) L = min(o[0][0] for o in out) U = max(o[-1][0] for o in out) W = U - L out.append([(L - W / 2, 0.0), (U + W / 2, 1.0)]) return out def voi_grid(self, S): """约定 27 的网格 VOI(浮点)。S:测到的格(因子编号列表)。""" S = sorted(S, key=factor_key) k = len(S) m = self.m_for(k) grids, info = [], {} for p in S: g, inf = self.cell_grid(p, m) grids.append(g) info[p] = inf J = len(self.weights) w = [float(x) for x in self.weights] unmeas = [p for p in self.cells if p not in S] Eun = [[float(self.Epay[j][q]) for j in range(J)] for q in unmeas] pay = [[float(self.payoff(fr(mid))) for mid, _ in g] for g in grids] total = 0.0 # 逐层展开组合:partial = (各成分联合质量向量, 已测格中点收益的最大值) def rec(level, joint, bestpay): nonlocal total g = grids[level] pl = pay[level] if level == k - 1: for idx, (_, mass) in enumerate(g): jv = [joint[j] * mass[j] for j in range(J)] marg = sum(jv) if marg == 0: continue best = max(bestpay, pl[idx]) for E in Eun: v = sum(jv[j] * E[j] for j in range(J)) / marg if v > best: best = v total += best * marg else: for idx, (_, mass) in enumerate(g): jv = [joint[j] * mass[j] for j in range(J)] if not any(jv): continue rec(level + 1, jv, max(bestpay, pl[idx])) rec(0, w, -math.inf) prior = max(float(v) for v in self.prior_payoffs().values()) return total - prior, {"k": k, "m": m, "cells": info} def voi_partial_exact_k1(self, p): """只测一格时的精确参考值(不是方法规定的算法,只用来估网格误差)。 相邻折点之间各成分密度是常数,后验权重不随 x 变,没测格的估计收益是常数 c; 测到的格收益是 x 的一次函数 A(x)。∫ max(A(x), c)·marg(x) dx 精确求。 """ J = len(self.weights) others = [q for q in self.cells if q != p] bps = sorted(set(x for c in self.comp_pts for x, _ in c[p])) total = Fr(0) def best_const(wv): s = sum(wv) return max(sum(wv[j] * self.Epay[j][q] for j in range(J)) / s for q in others) if others else None for a, b in zip(bps, bps[1:]): wv = [self.weights[j] * pts_density(self.comp_pts[j][p], a) for j in range(J)] dens = sum(wv) if dens == 0: continue c = best_const(wv) # A(x) = alpha + beta x pa, pb = self.payoff(a), self.payoff(b) beta = (pb - pa) / (b - a) alpha = pa - beta * a def integ(lo, hi, poly): return dens * pint(poly, lo, hi) if c is None: total += integ(a, b, [alpha, beta]) continue if beta == 0: total += dens * (b - a) * max(alpha, c) continue xc = (c - alpha) / beta # A(xc) = c cuts = [a] + ([xc] if a < xc < b else []) + [b] for lo, hi in zip(cuts, cuts[1:]): midA = alpha + beta * (lo + hi) / 2 if midA >= c: total += integ(lo, hi, [alpha, beta]) else: total += dens * (hi - lo) * c return total - self.g0() def f_left(fp, x): xs = [q[0] for q in fp] i = bisect.bisect_left(xs, x) - 1 if i < 0: return 0.0 if i == len(fp) - 1: return 1.0 (x0, y0), (x1, y1) = fp[i], fp[i + 1] return y0 + (y1 - y0) * (x - x0) / (x1 - x0) # ---------------------------------------------------------------- 价值、成本、选格 def m_prime(balance, size, K, r): num = balance - size if num <= 0: return 0, Fr(0) R = -(-num // K) r = fr(r) return R, sum((r ** j for j in range(R)), Fr(0)) def cost(g0, size, K): g = max(Fr(0), fr(g0)) return 2 * g * (Fr(K + size, K + size + 1) - Fr(K, K + 1)) TOL = 1e-12 def top_combo(rows): """6.7 选格第 2 条:最大净值 M;净值 ≥ M − 1e-12 的视为相同,先取格数少的,再按因子编号逐个比取小的。""" M = max(n for _, n in rows) near = [r for r in rows if r[1] >= M - TOL] return min(near, key=lambda r: combo_key(list(r[0]))) def select_cells(cells, balance, net_of): """约定 29 / 方法 6.7 选格:信息价值减成本选格。net_of(组合元组) → 净值(浮点)。 先只留净值 > 1e-12 的组合,一个都没有就不测;在留下的组合里按 top_combo 取。 “含这一格的净值最大组合”只是记录,不受 > 1e-12 的限制,在含这一格的全部组合里按 top_combo 取。 返回 (选中组合, 选中净值, 各组合净值表, 没选中格的说明)。 """ cells = sorted(cells, key=factor_key) maxn = min(len(cells), balance - 1) table = [] for n in range(1, maxn + 1): for combo in combinations(cells, n): table.append((combo, net_of(combo))) best, bestnet = (), 0.0 eligible = [r for r in table if r[1] > TOL] if eligible: best, bestnet = top_combo(eligible) unselected = {} for c in cells: if c in best: continue reason = "insufficientBudget" if len(best) + 1 > balance - 1 else "notWorthCost" cand = [(combo, net) for combo, net in table if c in combo] if not cand: unselected[c] = {"reason": reason, "bestCombo": None, "bestNet": None} else: bc, bn = top_combo(cand) unselected[c] = {"reason": reason, "bestCombo": list(bc), "bestNet": bn} return list(best), bestnet, table, unselected # ---------------------------------------------------------------- 押的因子 def bet(model, outcomes): """6.8 / 约定 30。outcomes = {因子: {"kind": ..., "value": 分数或缺}}。""" valid = {p: fr(o["value"]) for p, o in outcomes.items() if o["kind"] == "valid"} J = len(model.weights) raw = [] for j in range(J): v = model.weights[j] for p, x in valid.items(): v *= pts_density(model.comp_pts[j][p], x) raw.append(v) s = sum(raw) updated = s != 0 w = [v / s for v in raw] if updated else list(model.weights) est = model.prior_payoffs(w) pay = {} for p in model.cells: pay[p] = model.payoff(valid[p]) if p in valid else est[p] pick = None for p in model.cells: if pick is None or pay[p] > pay[pick]: pick = p neg = [] for p in model.cells: if p in valid and improve(model.full, valid[p], model.dir) < 0: neg.append({"factor": p, "offBetterBy": float(improve(model.full, valid[p], model.dir) * -1)}) if neg: status = "noBetNegativeSupport" elif pay[pick] <= 0: status = "noBetNonPositiveGain" else: status = "bet" return { "weightsUpdated": updated, "weights": {n: float(x) for n, x in zip(model.names, w)}, "payoffs": {p: float(v) for p, v in pay.items()}, "picked": pick, "predictedGain": float(pay[pick]), "status": status, "negativeFactors": neg, } # ---------------------------------------------------------------- 信息账 def edge(b, delta, j): """约定 31/32:小格边界一律按浮点 b + j·Δ 算(和实现用同一套边界)。""" return b + j * delta def bin_of(v, b, delta): j = math.floor((v - b) / delta) while edge(b, delta, j) > v: j -= 1 while edge(b, delta, j + 1) <= v: j += 1 return j def bin_masses(d, b, delta): """覆盖分布全部折点的小格质量(有理数),返回 [(j, p)]。""" lo, hi = float(d.lo()), float(d.hi()) j0 = bin_of(lo, b, delta) j1 = bin_of(hi, b, delta) # 浮点的 lo/hi 与有理数端点可能差一点;两侧各多切一格,质量为 0 的格不影响熵 out = [] prev = d.left(edge(b, delta, j0 - 1)) for j in range(j0 - 1, j1 + 2): cur = d.left(edge(b, delta, j + 1)) out.append((j, cur - prev)) prev = cur assert d.left(edge(b, delta, j0 - 1)) == 0 and prev == 1, "切格范围没有覆盖全部质量" return out def world_bits(d, b, delta): terms = [] for _, p in bin_masses(d, b, delta): if p > 0: pf = float(p) terms.append(-pf * math.log2(pf)) return math.fsum(terms) def surprise(d, v, b, delta): j = bin_of(v, b, delta) p = d.left(edge(b, delta, j + 1)) - d.left(edge(b, delta, j)) if p == 0: return {"status": "priorZero", "bin": j, "p": 0.0} return {"status": "ok", "bin": j, "p": float(p), "surprise": -math.log2(float(p))} def disagreement(person_dists, b, delta): avg = Dist([(Fr(1, len(person_dists)) * w, p) for d in person_dists for w, p in d.comps]) hb = world_bits(avg, b, delta) each = [world_bits(d, b, delta) for d in person_dists] return hb - math.fsum(each) / len(each), hb, each def landing(q1, q5, q9, v): """约定 34。""" if v < q1: return "below10" if v < q5: return "from10to50" if v <= q9: return "from50to90" return "above90" # ---------------------------------------------------------------- 入口分配、余额、种子 def allocate_entries(researchers, entries, prefs): """4.2:researchers 研究者编号;entries = [{id, author}];prefs = {研究者: [入口编号…]}。 研究者和入口都先按编号(自然序)排好,“编号最小”按编号判,与传入次序无关。 """ researchers = sorted(researchers, key=id_key) entries = sorted(entries, key=lambda e: id_key(e["id"])) author = {e["id"]: e["author"] for e in entries} taken = {} result = {} maxlen = max((len(v) for v in prefs.values()), default=0) if len(researchers) == 1: r = researchers[0] return {r: {"entry": prefs[r][0], "choice": 1}} for k in range(maxlen): want = {} for r in researchers: if r in result or k >= len(prefs[r]): continue e = prefs[r][k] if e in taken: continue want.setdefault(e, []).append(r) for e in [x["id"] for x in entries]: if e not in want: continue group = want[e] if len(group) == 1: win = group[0] elif author[e] in group: win = author[e] else: win = min(group, key=id_key) result[win] = {"entry": e, "choice": k + 1} taken[e] = win for r in researchers: if r in result: continue left = [x["id"] for x in entries if x["id"] not in taken] e = left[0] result[r] = {"entry": e, "choice": None} taken[e] = r return {r: result[r] for r in researchers} def balance(total, runs): started = sum(1 for s in runs if s == "started") reserved = sum(1 for s in runs if s == "reserved") return {"charged": started, "occupied": started + reserved, "balance": total - started - reserved}