比率拆解的总数闭合核对:证据面板的输入一步按拆解的模式认 headline 对着哪个数(_HEADLINE_TOTALS 分可加、比率两套:可加对各段之和 base_total / target_total,比率对整体比率即各段分子之和 ÷ 分母之和 r_base_total / r_target_total),closure 带上 mode;比率拆解的两格角色写「基期整体比率」「本期整体比率」。以前只认 base_total / target_total,比率的 headline 在证据面板不显示闭合核对。导出页(report_export/runtime.js)比率的写「整体比率 x 与各段分子之和 ÷ 分母之和 y 闭合 / 对不上」、那一格叫「整体比率那一格」,可加的照旧。测试:可加拆解的 closure 带 mode、角色叫法不变,比率拆解两格各自闭合、复算一致

This commit is contained in:
ThearchyHelios
2026-10-21 15:37:20 +08:00
parent b6efee81fd
commit 496dc27419
3 changed files with 62 additions and 15 deletions
+17 -9
View File
@@ -1006,11 +1006,16 @@ _OP_SETTINGS: dict[str, tuple[str, ...]] = {
"differ": ("differ_alpha", "min_n", "min_expected"),
"comparable": (),
}
#: 输入的角色(analysis.contract.AnalysisInput.role)给人看的叫法
#: 输入的角色(analysis.contract.AnalysisInput.role)给人看的叫法;比率拆解的 headline 是整体比率,不是总数
_INPUT_ROLES = {"data": "数据", "base": "基期", "target": "本期", "headline_base": "基期总数",
"headline_target": "本期总数", "parent": "上一层拆解"}
#: 闭合核对:headline 的那一格对着结论里的哪个总数(细案 01 §2.1)
_HEADLINE_TOTALS = {"headline_base": "base_total", "headline_target": "target_total"}
_RATIO_ROLES = {"headline_base": "基期整体比率", "headline_target": "本期整体比率"}
#: 闭合核对:headline 的那一格对着结论里的哪个数(细案 01 §2.1、§2.2)。可加拆解对各段之和(base_total / target_total),
#: 比率拆解对整体比率,即各段分子之和 ÷ 分母之和(r_base_total / r_target_total)
_HEADLINE_TOTALS = {
"additive": {"headline_base": "base_total", "headline_target": "target_total"},
"ratio": {"headline_base": "r_base_total", "headline_target": "r_target_total"},
}
#: 计算一步最多给结果表的几行(前 table_top_k 段,加上被引用的那一行前后各 WINDOW 行)
MAX_ANALYSIS_ROWS = MAX_WINDOW_ROWS
@@ -1181,11 +1186,15 @@ async def _analysis_inputs(alias: str, result: dict[str, Any], catalog: dict[str
sealed: _Sealed, masks: _Masks, *, queries: bool = True
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
"""输入:每份一步(analysis_input),再给每份输入快照一个查询步骤(_query_steps,高亮用到的列;整份用到的给前
MAX_WINDOW_ROWS 行;queries 为假时不给)。headline 的那一格另核对和结论里的总数闭不闭合(照实写判定是不是
unclosed,不在这里重算容差);下钻的上一层落到报告目录的哪个 A。"""
MAX_WINDOW_ROWS 行;queries 为假时不给)。headline 的那一格另核对和结论里的数闭不闭合(_HEADLINE_TOTALS:可加拆解是
各段之和,比率拆解是整体比率;closure.mode 写明是哪一种,照实写判定是不是 unclosed,不在这里重算容差);下钻的上一层
落到报告目录的哪个 A。"""
values = result.get("values") if isinstance(result.get("values"), dict) else {}
verdict = result.get("verdict") if isinstance(result.get("verdict"), dict) else {}
unclosed = verdict.get("code") == "unclosed"
mode = str(result.get("mode") or "")
totals = _HEADLINE_TOTALS.get(mode, {}) if result.get("op") == "decompose" else {}
roles = {**_INPUT_ROLES, **(_RATIO_ROLES if mode == "ratio" else {})}
steps: list[dict[str, Any]] = []
cells: dict[str, list[tuple[Any, Any]]] = {}
for item in result.get("inputs") or []:
@@ -1195,7 +1204,7 @@ async def _analysis_inputs(alias: str, result: dict[str, Any], catalog: dict[str
role = str(item.get("role") or "")
step: dict[str, Any] = {
"step": "analysis_input", "of": alias, "ref": item.get("ref"), "role": role,
"role_label": _INPUT_ROLES.get(role, role or "输入"), "artifact": artifact or None,
"role_label": roles.get(role, role or "输入"), "artifact": artifact or None,
**{k: item[k] for k in ("columns", "rows", "row", "column", "value") if k in item},
}
if role == "parent":
@@ -1208,10 +1217,9 @@ async def _analysis_inputs(alias: str, result: dict[str, Any], catalog: dict[str
query = sealed.queries.get(artifact)
step.update(query=_catalog_alias_of(catalog, artifact), node_id=(query or {}).get("node_id"),
sealed=sealed.trusted and query is not None)
if role in _HEADLINE_TOTALS and _HEADLINE_TOTALS[role] in values:
total = values[_HEADLINE_TOTALS[role]]
if role in totals and totals[role] in values:
with contextlib.suppress(RenderError):
step["closure"] = {"total_key": _HEADLINE_TOTALS[role], "total": render_cell(total),
step["closure"] = {"mode": mode, "total_key": totals[role], "total": render_cell(values[totals[role]]),
"headline": render_cell(item.get("value")), "closed": not unclosed}
steps.append(step)
if artifact and query is not None:
+9 -4
View File
@@ -95,8 +95,10 @@
calc: '计算', dValues: '具名的数', dFormulas: '关键数的代入式', dResultTable: '结果表',
resultRows: (shown, total) => `列出 ${shown} 行,共 ${total} 行`,
analysisInput: '输入', dRole: '角色', dRef: '调用里写的名字', dColumnsUsed: '用到的列', dRowsUsed: '用到的行',
allRows: '全部行', dHeadline: '总数那一格', dClosure: '闭合核对',
allRows: '全部行', dHeadline: '总数那一格', dHeadlineRatio: '整体比率那一格', dClosure: '闭合核对',
closureOk: (h, t) => `总数 ${h} 与各段之和 ${t} 闭合`, closureBad: (h, t) => `总数 ${h} 与各段之和 ${t} 对不上`,
ratioClosureOk: (h, t) => `整体比率 ${h} 与各段分子之和 ÷ 分母之和 ${t} 闭合`,
ratioClosureBad: (h, t) => `整体比率 ${h} 与各段分子之和 ÷ 分母之和 ${t} 对不上`,
dParent: '下钻自', recompute: '复算', seal: '封存', inputOfAnalysis: (alias) => `分析结论 ${alias} 的输入`,
inLedger: '封存范围内的台账记过这份分析结论', notInLedger: '封存范围内的台账里没有这份分析结论',
viaCard: '记在口径卡的指标集里,随卡封存(不进台账)',
@@ -541,10 +543,13 @@
return h('section', { class: 'part', 'data-step': 'analysis_calc' }, h('h3', {}, T.calc), h('div', { class: 'part-body' }, ...body))
}
/** 分析结论 · 输入:角色、落到哪个 Q、用到的列和行、总数那一格和闭合核对、下钻的上一层 */
/** 分析结论 · 输入:角色、落到哪个 Q、用到的列和行、总数(比率拆解是整体比率)那一格和闭合核对、下钻的上一层 */
function analysisInputPart(step) {
const rows = Array.isArray(step.rows) ? step.rows.map((r) => `第 ${Number(r) + 1} 行`).join('、') : step.rows === 'all' ? T.allRows : null
const closure = isObj(step.closure) ? step.closure : null
const ratio = closure !== null && closure.mode === 'ratio'
const closed = closure && (ratio ? (closure.closed ? T.ratioClosureOk : T.ratioClosureBad)
: (closure.closed ? T.closureOk : T.closureBad))
const body = [kv([
[T.dRole, String(step.role_label || step.role || NONE)],
step.ref ? [T.dRef, h('code', {}, String(step.ref))] : null,
@@ -553,8 +558,8 @@
step.node_id ? [T.dNode, nodeLabel(step.node_id)] : null,
Array.isArray(step.columns) && step.columns.length ? [T.dColumnsUsed, step.columns.join('、')] : null,
rows ? [T.dRowsUsed, rows] : null,
'value' in step ? [T.dHeadline, `${step.column ? `第 ${Number(step.row) + 1} 行「${step.column}」 = ` : ''}${valueText(step.value)}`] : null,
closure ? [T.dClosure, closure.closed ? T.closureOk(closure.headline, closure.total) : T.closureBad(closure.headline, closure.total)] : null,
'value' in step ? [ratio ? T.dHeadlineRatio : T.dHeadline, `${step.column ? `第 ${Number(step.row) + 1} 行「${step.column}」 = ` : ''}${valueText(step.value)}`] : null,
closure ? [T.dClosure, closed(closure.headline, closure.total)] : null,
step.phrase ? [T.dPhrase, String(step.phrase)] : null,
])]
const bits = integrity(step)
+36 -2
View File
@@ -305,12 +305,46 @@ async def test_headline_inputs_show_the_closure(client):
body = (await client.get(f"/api/runs/{made['run']}/evidence/segments/{seg_id(made['doc'], 'a:A1')}")).json()
inputs = [s for s in body["chain"] if s["step"] == "analysis_input"]
assert [s["role"] for s in inputs] == ["headline_base", "headline_target", "data"]
assert inputs[0]["closure"] == {"total_key": "base_total", "total": "2,220", "headline": "2,220", "closed": True}
assert inputs[1]["closure"] == {"total_key": "target_total", "total": "2,026", "headline": "2,026", "closed": True}
assert inputs[0]["closure"] == {"mode": "additive", "total_key": "base_total", "total": "2,220", "headline": "2,220",
"closed": True}
assert inputs[1]["closure"] == {"mode": "additive", "total_key": "target_total", "total": "2,026",
"headline": "2,026", "closed": True}
assert [s["role_label"] for s in inputs[:2]] == ["基期总数", "本期总数"]
assert inputs[1]["query"] == "Q2" and inputs[1]["sealed"] is True and inputs[2]["query"] == "Q1"
assert next(s for s in body["chain"] if s["step"] == "analysis_recompute")["status"] == "same"
async def test_a_ratio_headline_closes_against_the_overall_ratio(client):
"""比率拆解给了整体比率的两格(headline):闭合核对对着结论里的整体比率,即各段分子之和 ÷ 分母之和(r_base_total、
r_target_total),角色写「基期整体比率」「本期整体比率」,closure.mode 写明是比率(导出页照它说「各段分子之和 ÷ 分母
之和」,不说「各段之和」)。以前只认可加拆解的总数键,比率拆解的 headline 不显示闭合核对。"""
rates = {"columns": ["channel", "week", "refunds", "orders"],
"column_types": {"channel": "text", "week": "text", "refunds": "number", "orders": "number"},
"rows": [["线上", "W41", 50, 1000], ["线上", "W42", 80, 1000], ["线下", "W41", 20, 1000],
["线下", "W42", 26, 1000]],
"sql": "SELECT channel, week, SUM(refund_flag) AS refunds, COUNT(*) AS orders FROM orders GROUP BY 1, 2",
"source": "shop", "truncated": False, "row_count": 4}
head = {"columns": ["week", "rate"], "column_types": {"week": "text", "rate": "number"},
"rows": [["W41", "0.035"], ["W42", "0.053"]],
"sql": "SELECT week, SUM(refund_flag) * 1.0 / COUNT(*) AS rate FROM orders GROUP BY week",
"source": "shop", "truncated": False, "row_count": 2}
r, h = (artifact_store.content_hash(artifact_store.canonical_json(c)) for c in (rates, head))
result = await analyzed({"Q2": (rates, r), "Q5": (head, h)}, {"op": "decompose", "mode": "ratio", "params": {}},
query="Q2", by=["channel"], period="week", base="W41", target="W42", numerator="refunds",
denominator="orders", label="退款率", headline={"base": "Q5.r0.rate", "target": "Q5.r1.rate"})
assert result["status"] == "ok" and result["verdict"]["code"] != "unclosed"
made = await sealed_run("[[a:A1]]。", result=result, queries=[rates, head])
body = (await client.get(f"/api/runs/{made['run']}/evidence/segments/{seg_id(made['doc'], 'a:A1')}")).json()
inputs = {s["role"]: s for s in body["chain"] if s["step"] == "analysis_input"}
assert inputs["headline_base"]["closure"] == {"mode": "ratio", "total_key": "r_base_total", "total": "0.035",
"headline": "0.035", "closed": True}
assert inputs["headline_target"]["closure"] == {"mode": "ratio", "total_key": "r_target_total", "total": "0.053",
"headline": "0.053", "closed": True}
assert inputs["headline_base"]["role_label"] == "基期整体比率"
assert inputs["headline_target"]["role_label"] == "本期整体比率"
assert next(s for s in body["chain"] if s["step"] == "analysis_recompute")["status"] == "same"
async def test_a_drill_down_points_at_its_parent_and_recomputes_from_it(client):
"""下钻(华东按渠道再拆):输入里有上一层结论(parent),落到报告目录的 A1;复算按 sha256 取回台账记过的上一层。"""
q = artifact_store.content_hash(artifact_store.canonical_json(SNAPSHOT))