mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:21:58 +00:00
377 lines
22 KiB
Python
377 lines
22 KiB
Python
"""分析结论里的占比、变化率、权重按原始小数显示(第 2 波复核 S1,显示规则 4 版)。
|
||
|
||
复核实测:结果表里占变化的比重、变化率以比率存(0.618556701031),正文引用 [[a:A1.top_share]]、[[v:A1.r0.share]]、
|
||
[[table:A1 cols=region,pct,share]] 时照原样写「占比 3.5190412373」;判定语写「占 351.9%」,正文写「占比 3.5190412373」,
|
||
读的人把 0.62 当成 62% 的百分之一。现在算子给结果表的、以比率存的列记换算(ResultTable.formats:占比、变化率、权重记 pct,
|
||
比率的变化记 pp),结论的 values 里同样记一份(value_formats);引用、整表、写作目录、证据面板、导出、可验证报告都按它渲染,
|
||
实际用的换算记在引用上(导出页、可验证报告照它渲染)。写作者自己写了换算的照他的。3 版及以前组装的文档照当年原样显示,
|
||
复核不因此判对不上。
|
||
|
||
数据全是虚构的(华东、华南、华北、西南,销售额、订单数,虚构零售库 shop)。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from typing import Any
|
||
|
||
import pytest
|
||
from httpx import ASGITransport, AsyncClient
|
||
|
||
from app.analysis.contract import CONV_PCT, CONV_POINTS, ledger_entry
|
||
from app.core import artifact_store
|
||
from app.engine import evidence as ev
|
||
from app.engine.evidence import build_catalog, compose_doc, iter_segments, render_cell, resolve_ref, verify_doc, \
|
||
writer_catalog
|
||
from app.main import app as asgi
|
||
from tests.analysis_samples import analyze, snap
|
||
from tests.test_analysis_bundle import Files, GRAPH, RUN, RUN_ID, bundle_of, item, verdicts
|
||
from tests.test_analysis_evidence import SNAPSHOT, Store, real_result
|
||
from tests.test_analysis_evidence_api import seg_id, sealed_run
|
||
|
||
WEEKS = [["华东", "2026-W41", 820], ["华东", "2026-W42", 700], ["华北", "2026-W41", 600], ["华北", "2026-W42", 560],
|
||
["华南", "2026-W41", 700], ["华南", "2026-W42", 666], ["西南", "2026-W41", 100], ["西南", "2026-W42", 100]]
|
||
REGIONS = snap("Q2", "SELECT region, week, SUM(amount) AS gmv FROM orders GROUP BY region, week",
|
||
["region", "week", "gmv"], WEEKS, types={"region": "text", "week": "text", "gmv": "number"})
|
||
|
||
|
||
def run_sync(coro: Any) -> Any:
|
||
from tests.test_analysis_evidence import run_sync as wait
|
||
|
||
return wait(coro)
|
||
|
||
|
||
def analyzed(op: str, mode: str | None, **params: Any) -> dict[str, Any]:
|
||
"""真算一份结论(同 tests/analysis_samples.analyze:跑一次、复算一遍,逐字段相同)。"""
|
||
return run_sync(analyze(op, mode, snaps=[REGIONS], query="Q2", **params))
|
||
|
||
|
||
def catalog_of(*, result: dict[str, Any] | None = None) -> tuple[Store, dict[str, Any]]:
|
||
"""一次查询(Q1)加一份真算的按地区拆的分析结论(A1):(工件库, 写作目录)。结论带算子记的格式。"""
|
||
store = Store()
|
||
q = store.put(SNAPSHOT)
|
||
res = result or real_result("Q2")
|
||
a = store.put(res)
|
||
ledger = [{"kind": "query", "node_id": "fetch", "exec": 1, "artifact": q, "tool": "db_query__shop",
|
||
"source": "shop", "columns": ["region", "week", "gmv"], "rows": 8, "truncated": False},
|
||
ledger_entry(res, node_id="an", exec_no=1, artifact=a, item="drop")]
|
||
return store, build_catalog(nodes={}, ledger=ledger, loader=store.load)
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 换算的写法:pct、pp
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.mark.parametrize("value, conv, shown", [
|
||
("0.618556701", "pct", "61.9%"),
|
||
("3.51904123734", "pct", "351.9%"),
|
||
("-0.0142977693", "pct", "-1.4%"),
|
||
("0.0123", "pp", "+1.2 个百分点"),
|
||
("-0.121944594212", "pp", "-12.2 个百分点"),
|
||
("0", "pp", "0.0 个百分点"),
|
||
("0.00004", "pp", "+0.004 个百分点"), # 不是 0 的不写成 0.0
|
||
])
|
||
def test_pct_and_percentage_points_are_written_the_way_the_verdict_phrase_writes_them(value, conv, shown):
|
||
"""pct 和 phrases.pct 一个写法(一位小数、不带正号);pp 是百分点(一位小数,不是 0 的 0.0 多给小数位),
|
||
和判定语里「上升 0.4 个百分点」同一个说法。"""
|
||
assert render_cell(value, conv) == shown
|
||
|
||
|
||
def test_a_conversion_that_shows_a_nonzero_value_as_zero_still_fails():
|
||
with pytest.raises(ev.RenderError, match="显示为 0"):
|
||
render_cell("0.0000001", "pp")
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 各算子记的格式
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
def test_the_additive_decomposition_formats_shares_and_rates():
|
||
result = analyzed("decompose", "additive", by=["region"], period="week", base="2026-W41", target="2026-W42",
|
||
measure="gmv", label="销售额")
|
||
assert result["table"]["formats"] == {"pct": CONV_PCT, "share": CONV_PCT}
|
||
assert result["value_formats"] == {"pct_total": CONV_PCT, "top_share": CONV_PCT, "top_k_share": CONV_PCT}
|
||
# 维度列、量(基期、本期、变化)没有换算
|
||
assert set(result["table"]["formats"]) & {"region", "base", "target", "delta"} == set()
|
||
|
||
|
||
def test_the_ratio_decomposition_formats_weights_as_percent_and_effects_as_points():
|
||
rows = [["华东", "2026-W41", 8, 100], ["华东", "2026-W42", 10, 120], ["华南", "2026-W41", 5, 130],
|
||
["华南", "2026-W42", 4, 140]]
|
||
rates = snap("Q3", "SELECT region, week, refunds, orders FROM refunds GROUP BY region, week",
|
||
["region", "week", "refunds", "orders"], rows,
|
||
types={"region": "text", "week": "text", "refunds": "number", "orders": "number"})
|
||
result = run_sync(analyze("decompose", "ratio", snaps=[rates], query="Q3", by=["region"], period="week",
|
||
base="2026-W41", target="2026-W42", numerator="refunds", denominator="orders",
|
||
label="退款率"))
|
||
# 权重是占比、各段分子都不超过分母,比率本身也是占比 → pct;结构效应、自身效应、合计影响是比率的变化 → pp
|
||
assert result["table"]["formats"] == {"w_base": CONV_PCT, "w_target": CONV_PCT, "r_base": CONV_PCT,
|
||
"r_target": CONV_PCT, "mix": CONV_POINTS, "rate": CONV_POINTS,
|
||
"total": CONV_POINTS}
|
||
assert result["value_formats"]["r_base_total"] == CONV_PCT and result["value_formats"]["r_target_total"] == CONV_PCT
|
||
assert {result["value_formats"][k] for k in ("delta", "mix_total", "rate_total")} == {CONV_POINTS}
|
||
assert result["value_formats"]["mix_share"] == CONV_PCT
|
||
|
||
|
||
def test_the_factor_decomposition_formats_change_rates_and_shares():
|
||
product = snap("Q4", "SELECT week, gmv, users, price FROM orders", ["week", "gmv", "users", "price"],
|
||
[["2026-W41", 1000, 100, 10], ["2026-W42", 1188, 110, 10.8]],
|
||
types={"week": "text", "gmv": "number", "users": "number", "price": "number"})
|
||
result = run_sync(analyze("decompose", "factor", snaps=[product], query="Q4", period="week", base="2026-W41",
|
||
target="2026-W42", product="gmv", factors=["users", "price"], label="销售额"))
|
||
assert result["table"]["formats"] == {"pct": CONV_PCT, "share": CONV_PCT}
|
||
assert result["value_formats"] == {"pct_total": CONV_PCT, "top_share": CONV_PCT}
|
||
|
||
|
||
def test_the_baseline_formats_change_rates_only_when_it_compares_changes():
|
||
weekly = snap("Q1", "SELECT week, SUM(amount) AS gmv FROM orders GROUP BY week", ["week", "gmv"],
|
||
[[f"2026-W{k:02d}", 2200 + (k % 5) * 10] for k in range(30, 42)] + [["2026-W42", 2138]],
|
||
types={"week": "text", "gmv": "number"})
|
||
changes = run_sync(analyze("baseline", None, snaps=[weekly], query="Q1", period="week", measure="gmv",
|
||
label="销售额", grain="week"))
|
||
assert changes["table"]["formats"] == {"change": CONV_PCT}
|
||
assert {"x", "median", "band_lo", "band_hi"} <= set(changes["value_formats"])
|
||
levels = run_sync(analyze("baseline", None, snaps=[weekly], query="Q1", period="week", measure="gmv",
|
||
label="销售额", grain="week", basis="level"))
|
||
# 按值本身比时是量,照原样显示(没有换算)
|
||
assert "formats" not in levels["table"] and "value_formats" not in levels
|
||
|
||
|
||
def test_the_trend_formats_the_relative_slope():
|
||
weekly = snap("Q1", "SELECT week, SUM(amount) AS gmv FROM orders GROUP BY week", ["week", "gmv"],
|
||
[[f"2026-W{k:02d}", 100 + k] for k in range(30, 42)], types={"week": "text", "gmv": "number"})
|
||
result = run_sync(analyze("trend", None, snaps=[weekly], query="Q1", period="week", measure="gmv",
|
||
label="销售额", grain="week"))
|
||
assert result["value_formats"] == {"rel_slope": CONV_PCT}
|
||
|
||
|
||
def test_the_differ_formats_rates_as_percent_and_differences_as_points():
|
||
groups = snap("Q3", "SELECT region, refunds, orders FROM refunds GROUP BY region", ["region", "refunds", "orders"],
|
||
[["华东", 100, 2000], ["华南", 120, 1800], ["华北", 90, 2100], ["西南", 40, 1900]],
|
||
types={"region": "text", "refunds": "number", "orders": "number"})
|
||
result = run_sync(analyze("differ", "rate", snaps=[groups], query="Q3", by=["region"], numerator="refunds",
|
||
denominator="orders", label="退款率"))
|
||
assert result["table"]["formats"] == {"a_value": CONV_PCT, "b_value": CONV_PCT, "diff": CONV_POINTS,
|
||
"ci_lo": CONV_POINTS, "ci_hi": CONV_POINTS}
|
||
assert {result["value_formats"][k] for k in ("a", "b")} == {CONV_PCT}
|
||
assert {result["value_formats"][k] for k in ("diff", "ci_lo", "ci_hi")} == {CONV_POINTS}
|
||
|
||
|
||
def test_a_run_twice_is_byte_for_byte_the_same_and_recompute_matches():
|
||
"""格式是结论的一部分:同样的输入两次 run 逐字节相同,复算和封存的逐字段相同(analysis_samples.analyze 顺带核对复算)。"""
|
||
first = analyzed("decompose", "additive", by=["region"], period="week", base="2026-W41", target="2026-W42",
|
||
measure="gmv", label="销售额")
|
||
second = analyzed("decompose", "additive", by=["region"], period="week", base="2026-W41", target="2026-W42",
|
||
measure="gmv", label="销售额")
|
||
assert artifact_store.canonical_json(first) == artifact_store.canonical_json(second)
|
||
assert first["table"]["formats"] == second["table"]["formats"]
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 引用:没写换算时取列 / 数的格式,写了照写作者的
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
def test_citing_a_share_or_rate_without_a_conversion_uses_the_conclusion_format():
|
||
store, catalog = catalog_of()
|
||
doc = compose_doc("华东占 [[a:A1.top_share]],变化率 [[v:A1.r0.pct]],占比 [[v:A1.r0.share]],"
|
||
"变化量 [[v:A1.r0.delta]]。[[see:A1]]", catalog, loader=store.load)
|
||
shown = {s["ref"]: s["text"] for s in iter_segments(doc) if s.get("ref")}
|
||
assert shown["a:A1.top_share"] == "61.9%"
|
||
assert shown["v:A1.r0.pct"] == "-14.6%" and shown["v:A1.r0.share"] == "61.9%"
|
||
assert shown["v:A1.r0.delta"] == "-120" # 量照原样
|
||
cites = {s["ref"]: s["cite"] for s in iter_segments(doc) if s.get("ref")}
|
||
# 实际用的换算记在引用上(导出页、可验证报告照它渲染)
|
||
assert cites["a:A1.top_share"]["conv"] == "pct" and cites["v:A1.r0.share"]["conv"] == "pct"
|
||
assert "conv" not in cites["v:A1.r0.delta"]
|
||
assert doc["cell_display"] == 4 and doc["violations"] == []
|
||
assert verify_doc(doc, catalog, loader=store.load)["ok"]
|
||
|
||
|
||
def test_a_writer_supplied_conversion_wins_over_the_conclusion_format():
|
||
store, catalog = catalog_of()
|
||
cite = resolve_ref("a:A1.top_share|.2", catalog, loader=store.load)
|
||
assert cite["rendered"] == "0.62" and cite["conv"] == ".2"
|
||
cell = resolve_ref("v:A1.r0.share|pct", catalog, loader=store.load)
|
||
assert cell["rendered"] == "61.9%"
|
||
|
||
|
||
def test_a_whole_table_is_expanded_with_the_conclusion_formats():
|
||
store, catalog = catalog_of()
|
||
doc = compose_doc("[[table:A1 cols=region,delta,pct,share rows=0-1]]", catalog, loader=store.load)
|
||
assert "| region | 变化 | 变化率 | 占变化的比重 |" in doc["markdown"]
|
||
assert "| 华东 | -120 | -14.6% | 61.9% |" in doc["markdown"]
|
||
assert "| 华北 | -40 | -6.7% | 20.6% |" in doc["markdown"]
|
||
assert verify_doc(doc, catalog, loader=store.load)["ok"]
|
||
|
||
|
||
def test_an_old_document_keeps_the_raw_decimals_and_still_verifies(monkeypatch):
|
||
"""3 版及以前组装的文档:引用上是 0.618556701 这样的原样,复核按记下的版本渲染,不判对不上;按新规则重渲染会判对不上。"""
|
||
store, catalog = catalog_of()
|
||
monkeypatch.setattr(ev, "CELL_DISPLAY", 3)
|
||
old = compose_doc("占比 [[a:A1.top_share]],[[v:A1.r0.share]]。[[see:A1]]", catalog, loader=store.load)
|
||
monkeypatch.undo()
|
||
assert "cell_display" not in old
|
||
shown = {s["ref"]: s["text"] for s in iter_segments(old) if s.get("ref")}
|
||
assert shown["a:A1.top_share"] == "0.618556701" and shown["v:A1.r0.share"] == "0.618556701"
|
||
assert verify_doc(old, catalog, loader=store.load)["ok"]
|
||
# 拿 4 版复核这份老文档:引用上的字还是原样,照样对得上(引用上没记换算,按引用上的字渲染)
|
||
assert {s["ref"]: s["cite"].get("conv") for s in iter_segments(old) if s.get("ref")} == \
|
||
{"a:A1.top_share": None, "v:A1.r0.share": None}
|
||
# 反过来:4 版组装的文档按 3 版复核,引用上的字是 61.9%,老规则渲染出来是原样,判对不上
|
||
now = compose_doc("占比 [[a:A1.top_share]]。[[see:A1]]", catalog, loader=store.load)
|
||
mismatched = verify_doc({**now, "cell_display": 3}, catalog, loader=store.load)
|
||
assert mismatched["ok"] is False
|
||
assert [v["code"] for v in mismatched["violations"]] == ["render_mismatch"]
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 写作目录
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
def test_the_writer_catalog_shows_the_shares_as_percentages():
|
||
store, catalog = catalog_of()
|
||
text = writer_catalog(catalog, loader=store.load).text
|
||
section = text[text.index("分析结论("):text.index("查询结果(")]
|
||
assert "top_share=61.9%" in section and "top_share=0.618" not in section
|
||
assert "r0:region=华东,base=820,target=700,delta=-120,pct=-14.6%,share=61.9%" in section
|
||
assert "份额、变化率不写换算也按百分数显示" in section or "不写换算也按百分数显示" in section
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 证据面板接口
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
@pytest.fixture
|
||
async def client():
|
||
async with AsyncClient(transport=ASGITransport(app=asgi), base_url="http://test") as c:
|
||
yield c
|
||
|
||
|
||
async def test_the_panel_shows_the_named_value_and_the_table_cells_as_written(client):
|
||
made = await sealed_run("[[a:A1]]。占比 [[a:A1.top_share]],[[v:A1.r0.share]]。[[see:A1]]")
|
||
body = (await client.get(
|
||
f"/api/runs/{made['run']}/evidence/segments/{seg_id(made['doc'], 'a:A1.top_share')}")).json()
|
||
calc = body["chain"][2]
|
||
top = next(v for v in calc["values"] if v["key"] == "top_share")
|
||
assert top["raw"] == "0.618556701031" and top["rendered"] == "61.9%"
|
||
rows = calc["table"]["rows"]
|
||
share = calc["table"]["columns"].index("share")
|
||
rate = calc["table"]["columns"].index("pct")
|
||
flag = calc["table"]["columns"].index("flag")
|
||
assert [r[share] for r in rows] == ["61.9%", "20.6%", "17.5%", "0.0%"]
|
||
assert rows[0][rate] == "-14.6%" and rows[0][flag] == "—" # 空格写「—」,不写 null
|
||
assert body["chain"][0]["render_ok"] is True
|
||
|
||
|
||
# --------------------------------------------------------------------------
|
||
# 导出页、可验证报告
|
||
# --------------------------------------------------------------------------
|
||
|
||
|
||
NODE_NS = {"__name__": "node-check"}
|
||
try:
|
||
import shutil
|
||
import subprocess
|
||
from pathlib import Path
|
||
|
||
import app.bundle
|
||
import app.report_export
|
||
NODE = shutil.which("node")
|
||
except Exception: # pragma: no cover
|
||
NODE = None
|
||
|
||
RUNTIME = Path(app.report_export.__file__).parent / "runtime.js" if NODE else None
|
||
VERIFIER = Path(app.bundle.__file__).parent / "verifier.js" if NODE else None
|
||
DOM = Path(__file__).parent.joinpath("test_analysis_pages.py").read_text(encoding="utf-8").split('DOM = r"""', 1)[1] \
|
||
.split('"""', 1)[0]
|
||
|
||
|
||
def run_node(script: str, path: Path, payload: dict[str, Any]) -> Any:
|
||
done = subprocess.run([NODE, "-e", DOM + script, str(path)], input=json.dumps(payload), capture_output=True,
|
||
text=True, timeout=120)
|
||
assert done.returncode == 0, done.stderr[-3000:]
|
||
return json.loads(done.stdout)
|
||
|
||
|
||
RUN_RUNTIME = r"""
|
||
document.getElementById('report-data').textContent = input.data
|
||
new Function(fs.readFileSync(process.argv[1], 'utf8'))()
|
||
const out = {}
|
||
for (const seg of input.segs) {
|
||
const el = new El_('button')
|
||
for (const [k, v] of Object.entries(seg)) el.setAttribute(`data-${k}`, v)
|
||
el.textContent = seg.text
|
||
for (const f of document.getElementById('report').listeners.click || []) f({ target: el })
|
||
out[seg.seg] = document.getElementById('detail-body').textContent
|
||
}
|
||
process.stdout.write(JSON.stringify(out))
|
||
"""
|
||
|
||
RUN_VERIFIER = r"""
|
||
document.getElementById('bundle-data').textContent = input.data
|
||
document.getElementById('seal-hash').textContent = input.seal
|
||
new Function(fs.readFileSync(process.argv[1], 'utf8'))()
|
||
setTimeout(() => {
|
||
const reports = document.getElementById('reports')
|
||
const out = { opened: {} }
|
||
for (const btn of find(reports, (n) => n.attrs['data-num'])) {
|
||
for (const f of btn.listeners.click || []) f()
|
||
out.opened[btn.attrs['data-num']] = document.getElementById('detail-body').textContent
|
||
}
|
||
out.verified = document.documentElement.attrs['data-verified'] ?? null
|
||
process.stdout.write(JSON.stringify(out))
|
||
}, 200)
|
||
"""
|
||
|
||
|
||
@pytest.mark.skipif(NODE is None, reason="没有 node,无法执行页面上的脚本")
|
||
async def test_the_export_page_shows_percentages_in_the_calculation_step():
|
||
made = await sealed_run("[[a:A1]]。占比 [[a:A1.top_share]],[[v:A1.r0.share]]。[[see:A1]]")
|
||
async with AsyncClient(transport=ASGITransport(app=asgi), base_url="http://test") as c:
|
||
html = (await c.get(f"/api/runs/{made['run']}/report.html")).text
|
||
import re
|
||
|
||
data = re.search(r'<script type="application/json" id="report-data">(.*?)</script>', html, re.S).group(1)
|
||
phrase = seg_id(made["doc"], "a:A1")
|
||
out = run_node(RUN_RUNTIME, RUNTIME, {"data": data, "segs": [
|
||
{"seg": phrase, "kind": "analysis", "state": "deterministic", "text": made["result"]["phrase"]}]})
|
||
body = out[phrase].replace(" ", "").replace(" ", "")
|
||
# 具名的数、结果表的格按结论记的格式写:top_share 61.9%、变化率 -14.6%,没有原始小数、没有 null
|
||
assert "top_share61.9%" in body and "top_k_share82.5%" in body
|
||
assert "占变化的比重" in body and "-14.6%" in body
|
||
assert "0.618556701" not in body and "null" not in body
|
||
|
||
|
||
@pytest.mark.skipif(NODE is None, reason="没有 node,无法执行页面上的脚本")
|
||
def test_the_verifiable_report_renders_the_conclusion_format():
|
||
bundle, _, _ = bundle_of("## 异动\n\n[[a:A1]]。华东占 [[a:A1.top_share]],占比 [[v:A1.r0.share]]。[[see:A1]]")
|
||
out = run_node(RUN_VERIFIER, VERIFIER, {"data": json.dumps(bundle), "seal": bundle["meta"]["manifest_hash"]})
|
||
assert out["verified"] == ""
|
||
share = next(text for text in out["opened"].values() if "具名的数「top_share」" in text)
|
||
assert "正文显示61.9%(与按显示规则渲染的结果一致)" in share
|
||
# 出处面板写结果表记的列名(「占变化的比重」),不把列 id 当叫法
|
||
cell = next(text for text in out["opened"].values() if "结果表 · 第 1 行 · 占变化的比重" in text)
|
||
assert "正文显示61.9%" in cell
|
||
|
||
def test_a_ratio_with_a_unit_is_not_shown_as_a_percentage():
|
||
"""比率自身是有量纲的比率(客单价 = 销售额 / 订单数,元 / 单,可以大于 1)时不套百分数:以前整体比率 160.76 显示成
|
||
16,076.0%,各段的 160.62 显示成 16,062.5%,判定语里却是「客单价下降 9.89(-6.2%)」。各段分子都不超过分母的(退款率这种
|
||
占比)照旧按百分数。"""
|
||
rows = [["华东", "2026-W41", 160_000, 1000], ["华东", "2026-W42", 141_000, 1000],
|
||
["华南", "2026-W41", 180_000, 1100], ["华南", "2026-W42", 175_000, 1100]]
|
||
snaps = snap("Q5", "SELECT region, week, gmv, orders FROM sales GROUP BY region, week",
|
||
["region", "week", "gmv", "orders"], rows,
|
||
types={"region": "text", "week": "text", "gmv": "number", "orders": "number"})
|
||
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}
|
||
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
|