mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 18:41:59 +00:00
比率拆解的总数闭合核对:证据面板的输入一步按拆解的模式认 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:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user