验收复核查出的三处:有量纲比率的效应值(结构效应、自身效应、合计影响、变化)照原值显示,不再乘一百说成百分点(客单价影响 -11.12 元 / 单以前显示 -1,112.1 个百分点);只读一张表、目录还没维护时分析范围照实说目录没维护,不说成对不上目录里的列;写作规则补一条不写重复判定语开头的引言

This commit is contained in:
ThearchyHelios
2026-10-21 18:08:59 +00:00
parent 9219df8190
commit cf30f999f6
5 changed files with 35 additions and 9 deletions
+10 -6
View File
@@ -693,15 +693,19 @@ def _ratio_result(env: Env, params: dict[str, Any], used: list[str], segs: list[
values["gap"] = canon(max(gaps.values(), key=abs))
verdict = {"code": code, "subjects": [_key_strings(x.key) for x in subjects],
"direction": sign_word(delta) if code in ("mix_driven", "rate_driven", "both") else None}
# 显示格式按「这一段(这个数)是占比还是带量纲的比率」分:
# - 各段分子都不超过分母时,整体和各段的比率是个占比(退款率):比率按百分数,结构 / 自身 / 合计影响是比率的变化,
# 按百分点
# - 否则比率本身带量纲(客单价,元 / 单,可以大于 1):比率照原值;影响也是同一个量纲的变化(元 / 单),照原值,
# 不能乘一百说成「个百分点」——否则判定语写「客单价下降 9.89」、表里同一件事写「-988.7 个百分点」
ratio_rate = ("r_base", "r_target") if proportion else ()
points = ("mix", "rate", "total") if proportion else ()
named_points = ("delta", "mix_total", "rate_total") if proportion else ()
return build(env, params, used, table=table, values=values, verdict=verdict, criterion=criterion,
phrase=ph.with_caveats(main, caveats, skip=("comparability",) if outcome is None else ()),
caveats=caveats,
# 权重(占比)按百分数;结构效应、自身效应、合计影响是比率的变化,按百分点。各段的比率和整体比率本身:
# 各段分子都不超过分母时它是个占比(退款率),按百分数;否则它是有量纲的比率(客单价,元 / 单,可以大于 1),
# 照原值显示——一律套百分数会把客单价 160.76 显示成 16,076.0%
formats=_rate_formats(columns, rate=("w_base", "w_target") + (("r_base", "r_target") if proportion else ()),
points=("mix", "rate", "total")),
named={k: CONV_POINTS for k in ("delta", "mix_total", "rate_total")}
formats=_rate_formats(columns, rate=("w_base", "w_target") + ratio_rate, points=points),
named={k: CONV_POINTS for k in named_points}
| ({k: CONV_PCT for k in ("r_base_total", "r_target_total")} if proportion else {})
| {"mix_share": CONV_PCT, "rate_share": CONV_PCT})
+3 -1
View File
@@ -1130,7 +1130,9 @@ ANALYSIS_RULES_PROMPT = (
"否则写成待核实的推测,比如「可能与促销结束有关,待核实」。\n"
"- 判定语整句引用([[a:A1]])时一句只引用一份结论,不要把几份不同维度、不同期间的判定语用逗号拼成一句;"
"要说两件事就分成两句,各自挂各自的依据。\n"
"- 「核对不了」「未能核对」是系统对核对情况的说明,不是结论:不要把它写进正文当结论,也不要据此下判断。"
"- 「核对不了」「未能核对」是系统对核对情况的说明,不是结论:不要把它写进正文当结论,也不要据此下判断。\n"
"- 引用判定语时不要再在它前面写一句把它开头重复一遍的引言(「过去 26 周过去 26 周退款率…」「波动基线没有做成:"
"波动基线没有做成:…」):要么直接写判定语,要么写一句自己的话再引用。"
)
#: 写作目录里没有分析结论时,拼在写作规则后面
ANALYSIS_NONE_PROMPT = (
+6
View File
@@ -225,6 +225,7 @@ def _undecomposed(decomposes: list[dict[str, Any]], catalog: Mapping[str, Any],
if not snapshots:
return None, NOTE_NO_QUERY
known: dict[str, tuple[str, str, str]] = {} # 这份拆解能对上的列:name_key → (源, 表, 列)
bare = False # 目录还没维护(没有任何列说明):对不上是目录的事,不是数据的事
for artifact in snapshots:
try:
snapshot = load(str(artifact))
@@ -245,6 +246,7 @@ def _undecomposed(decomposes: list[dict[str, Any]], catalog: Mapping[str, Any],
# 目录还没维护(表类型、字段的度量类型都没标)时说清楚是目录没维护,不说成「认不出事实表」——
# 后者听着像数据的问题,实际上补一下目录就能列
return None, NOTE_NO_FACT if _maintained(tables) else NOTE_NOT_MAINTAINED
bare = bare or not any((n.get("columns") or {}) for _, n in facts)
for table, notes in [*facts, *(r for t, n in facts for r in _related(n, tables))]:
for column in (notes.get("columns") or {}):
known.setdefault(name_key(str(column)), (source, table, name_key(str(column))))
@@ -253,6 +255,10 @@ def _undecomposed(decomposes: list[dict[str, Any]], catalog: Mapping[str, Any],
for dim, label in zip(_dims(result), _dim_labels(result)):
hit = known.get(name_key(dim))
if hit is None:
# 目录一列说明都没有时,「对不上」是目录没维护造成的(单表查询时 _fact_tables 会兜底认出那张表,
# 走不到上面 NOTE_NOT_MAINTAINED 那一条),照实说目录没维护,不说成数据对不上
if bare or not _maintained(tables):
return None, NOTE_NOT_MAINTAINED
return None, f"候选维度核对不了:拆解按的「{label}」对不上数据目录里的列"
used.add(hit)
return [label for key, label in pool.items() if key not in used], None
@@ -369,7 +369,8 @@ def test_a_ratio_with_a_unit_is_not_shown_as_a_percentage():
result = run_sync(analyze("decompose", "ratio", snaps=[snaps], query="Q5", by=["region"], period="week",
base="2026-W41", target="2026-W42", numerator="gmv", denominator="orders",
label="客单价"))
assert result["table"]["formats"] == {"w_base": CONV_PCT, "w_target": CONV_PCT, "mix": CONV_POINTS,
"rate": CONV_POINTS, "total": CONV_POINTS}
assert result["table"]["formats"] == {"w_base": CONV_PCT, "w_target": CONV_PCT}
assert "r_base_total" not in result["value_formats"] and "r_target_total" not in result["value_formats"]
# 影响也是同一个量纲的变化(元 / 单),不能乘一百说成「个百分点」:raw 1.174 不能显示 +117.4 个百分点
assert [k for k in ("delta", "mix_total", "rate_total") if k in result["value_formats"]] == []
assert result["value_formats"]["mix_share"] == CONV_PCT
+13
View File
@@ -204,3 +204,16 @@ async def test_the_report_reads_the_catalog_through_the_run_view(shop):
assert block["not_decomposed"] == ["渠道", "订单状态", "门店类型", "城市"]
block = await scope.scope_of(catalog, {"mode": "off"}, load=lambda a: snapshot)
assert block["not_decomposed"] is None and block["note"] == scope.NOTE_NO_CATALOG
def test_an_unmaintained_single_table_catalog_says_so_instead_of_a_column_mismatch():
"""V15(验收复核):只读一张表、目录还没维护时,_fact_tables 会兜底认出那张表,于是「对不上数据目录里的列」这条
把「目录没维护」说成了数据对不上。目录一列说明都没有时照实说目录没维护。"""
from app.engine.analysis_scope import NOTE_NOT_MAINTAINED, _undecomposed
result = {"op": "decompose", "mode": "additive", "inputs": [{"role": "data", "artifact": "a" * 64}],
"params": {"by": ["channel"]}, "table": {"columns": ["channel", "base"], "labels": {"channel": "渠道"}}}
sources = {"源": {"tables": {"orders": {}}, "masked": []}}
snapshots = {"a" * 64: {"source": "源", "sql": "SELECT channel, SUM(amount) AS amt FROM orders GROUP BY channel",
"columns": ["channel", "amt"]}}
assert _undecomposed([result], {}, sources, snapshots.get) == (None, NOTE_NOT_MAINTAINED)