测试用的假结论按各算子的形状给(复核 R31):testing.fake_result 不再一律拿可加拆解的结果表——波动基线(期次、value、change、role)、趋势(期次、value、change)、差异检验(a、b、a_value……p_adj、flag)、可比性检查(check、status、base、target、note)各给自己形状的结果表(带列类型)、具名标量、参数和输入(可比性检查是基期、本期两份),数是编的、和判定语对得上;差异检验没写模式时按比率。变化拆解那一份照旧。测试对着真算子核对列和具名标量的键

This commit is contained in:
ThearchyHelios
2026-10-21 16:16:40 +08:00
parent 19bdcf55d8
commit f5e6e77ea9
2 changed files with 117 additions and 16 deletions
+105 -16
View File
@@ -1,5 +1,8 @@
"""测试用的分析结论(只给测试用):第 1 波各包并行开发时,证据接入、入口、说法核对的测试不等算子写完,按契约的形状
拿一份假的结论。算子落地后,端到端的测试改用 analysis.run 真算。"""
拿一份假的结论。算子落地后,端到端的测试改用 analysis.run 真算。
每个算子的假结论带它自己形状的结果表、具名标量、参数和输入(列 id、叫法、类型照各算子的实现:变化拆解按维度、波动基线和
趋势按期次、差异检验按对、可比性检查按检查项),数是编的、彼此对得上判定语。"""
from __future__ import annotations
from typing import Any
@@ -18,14 +21,11 @@ _PHRASES = {
"incomparable": "两期不可比:本期只有 3 天数据",
}
_WEEKS = ["2026-07-20", "2026-07-27", "2026-08-03", "2026-08-10", "2026-08-17", "2026-08-24", "2026-08-31",
"2026-09-07", "2026-09-14", "2026-09-21", "2026-09-28", "2026-10-05", "2026-10-12"]
def fake_result(op: str = "decompose", mode: str | None = "additive", verdict: str | None = "driver", *,
subjects: list[list[str]] | None = None, direction: str | None = "down", status: str = "ok",
reason_code: str | None = None, inputs: list[dict[str, Any]] | None = None,
**overrides: Any) -> AnalysisResult:
"""一份形状完整的假结论。缺省是「销售额下降主要来自华东」的可加拆解。"""
if op not in ("decompose", "differ"):
mode = None
def _decompose() -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], list[str]]:
table = {
"columns": ["region", "base", "target", "delta", "pct", "share", "flag"],
"labels": {"region": "地区", "base": "基期", "target": "本期", "delta": "变化", "pct": "变化率",
@@ -35,26 +35,115 @@ def fake_result(op: str = "decompose", mode: str | None = "additive", verdict: s
["华南", "700", "666", "-34", "-0.0486", "0.1753", None],
["西南", "100", "100", "0", "0", "0", None]],
}
values = {"base_total": "2220", "target_total": "2026", "delta_total": "-194", "pct_total": "-0.0874",
"top_key": "华东", "top_share": "0.6186", "segments": "4"}
params = {"query": "Q2", "by": ["region"], "period": "week", "base": "W41", "target": "W42", "measure": "gmv",
"label": "销售额"}
return table, values, params, ["region", "week", "gmv"]
def _baseline() -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], list[str]]:
changes = ["0.012", "-0.021", "0.034", "-0.008", "0.019", "-0.045", "0.027", "-0.015", "0.006", "0.041", "-0.032",
"-0.004"]
levels = ["2210", "2164", "2238", "2220", "2262", "2160", "2218", "2185", "2198", "2288", "2215", "2206"]
columns = ["week", "value", "change", "role"]
rows = [[w, v, c, "历史"] for w, v, c in zip(_WEEKS[:-1], levels, changes)] + [[_WEEKS[-1], "2138", "-0.031", "本期"]]
table = {"columns": columns, "labels": {"week": "周", "value": "销售额", "change": "变化率", "role": "说明"},
"rows": rows, "column_types": {"week": "text", "value": "number", "change": "number", "role": "text"}}
values = {"x": "-0.031", "value": "2138", "median": "0.001", "band_lo": "-0.061", "band_hi": "0.053", "z": "-1.12",
"rank": "11", "n_history": "12", "hist_min": "-0.045", "hist_max": "0.041"}
params = {"query": "Q1", "period": "week", "measure": "gmv", "label": "销售额", "grain": "week", "basis": "change",
"lag": 1, "target": _WEEKS[-1], "today": "2026-10-21"}
return table, values, params, ["week", "gmv"]
def _trend() -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], list[str]]:
levels = [100, 103, 106, 109, 113, 116, 119, 123, 127, 130]
rows = [[w, str(v), str(v - levels[i - 1]) if i else None] for i, (w, v) in enumerate(zip(_WEEKS[-10:], levels))]
table = {"columns": ["week", "value", "change"], "labels": {"week": "周", "value": "线上销售额", "change": "比上一期"},
"rows": rows, "column_types": {"week": "text", "value": "number", "change": "number"}}
values = {"slope": "3.333333333333", "rel_slope": "0.03", "p": "0.000002755732", "s": "45", "n": "10",
"run_length": "9", "run_direction": "up"}
params = {"query": "Q1", "period": "week", "measure": "gmv", "label": "线上销售额", "grain": "week",
"today": "2026-10-21"}
return table, values, params, ["week", "gmv"]
def _differ() -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], list[str]]:
columns = ["a", "b", "a_value", "b_value", "diff", "ci_lo", "ci_hi", "a_n", "b_n", "test", "p", "p_adj", "flag"]
table = {"columns": columns,
"labels": {"a": "组", "b": "对照组", "a_value": "组的比率", "b_value": "对照组的比率", "diff": "差",
"ci_lo": "差的区间下限", "ci_hi": "差的区间上限", "a_n": "组的样本量", "b_n": "对照组的样本量",
"test": "检验", "p": "p 值", "p_adj": "校正后的 p 值", "flag": "说明"},
"rows": [["西南", "华东", "0.05", "0.021739130435", "0.028260869565", "-0.026", "0.137", "40", "230",
"Fisher 精确检验", "0.27", "0.27", "样本少,用了精确检验"]],
"column_types": {"a": "text", "b": "text", **{c: "number" for c in columns[2:9]}, "test": "text",
"p": "number", "p_adj": "number", "flag": "text"}}
values = {"a": "0.05", "b": "0.021739130435", "diff": "0.028260869565", "ci_lo": "-0.026", "ci_hi": "0.137",
"p": "0.27", "p_adj": "0.27", "n_a": "40", "n_b": "230", "test": "fisher", "pairs": "1", "differs": "0"}
params = {"query": "Q3", "by": ["region"], "numerator": "refunds", "denominator": "orders", "label": "退款率",
"pairs": [["西南", "华东"]]}
return table, values, params, ["region", "refunds", "orders"]
def _comparable() -> tuple[dict[str, Any], dict[str, Any], dict[str, Any], list[str]]:
columns = ["check", "status", "base", "target", "note"]
table = {"columns": columns,
"labels": {"check": "检查项", "status": "结果", "base": "基期", "target": "本期", "note": "说明"},
"rows": [["同口径", "通过", None, None, None],
["窗口长度", "通过", "2026-10-05 到 2026-10-11(7 天)", "2026-10-12 到 2026-10-18(7 天)", None],
["整期", "通过", "通过", "通过", None], ["星期构成", "通过", None, None, None],
["数据问题", "通过", None, None, None]],
"column_types": dict.fromkeys(columns, "text")}
values = {"same_query": "pass", "window": "pass", "complete": "pass", "weekdays": "pass", "data": "pass",
"base_start": "2026-10-05", "base_end": "2026-10-11", "base_days": "7", "base_weekend": "2",
"target_start": "2026-10-12", "target_end": "2026-10-18", "target_days": "7", "target_weekend": "2"}
params = {"base_query": "Q4", "target_query": "Q5", "measure": "gmv", "today": "2026-10-21"}
return table, values, params, ["gmv"]
_SHAPES = {"decompose": _decompose, "baseline": _baseline, "trend": _trend, "differ": _differ,
"comparable": _comparable}
_CRITERIA = {"decompose": "一段占总变化一半以上算主要来源",
"baseline": "本期和过去 12 周的中位数比,差不到 2 倍稳健标准差算常规波动",
"trend": "Mann–Kendall 检验的双侧 p 值小于 0.05 时按 Theil–Sen 斜率的正负判上升或下降",
"differ": "双侧 p 值小于 0.05 算差异站得住",
"comparable": "只差日期、窗口一样长、是整期、星期构成相同才算可比"}
def fake_result(op: str = "decompose", mode: str | None = "additive", verdict: str | None = "driver", *,
subjects: list[list[str]] | None = None, direction: str | None = "down", status: str = "ok",
reason_code: str | None = None, inputs: list[dict[str, Any]] | None = None,
**overrides: Any) -> AnalysisResult:
"""一份形状完整的假结论。缺省是「销售额下降主要来自华东」的可加拆解;别的算子给各自形状的结果表、具名标量和参数。"""
if op not in ("decompose", "differ"):
mode = None
elif op == "differ" and mode not in ("rate", "mean", "rows"):
mode = "rate"
table, values, params, columns = _SHAPES.get(op, _decompose)()
if op == "comparable":
default_inputs = [{"ref": ref, "artifact": "0" * 64, "role": role, "columns": columns, "rows": "all"}
for ref, role in (("Q4", "base"), ("Q5", "target"))]
else:
default_inputs = [{"ref": str(params.get("query") or "Q2"), "artifact": "0" * 64, "role": "data",
"columns": columns, "rows": "all"}]
result: AnalysisResult = {
"op": op,
"mode": mode,
"method": METHODS.get((op, mode), f"{op}@1"),
"params": {"query": "Q2", "by": ["region"], "period": "week", "base": "W41", "target": "W42",
"measure": "gmv", "label": "销售额"},
"params": params,
"settings": effective_settings(None),
"inputs": inputs if inputs is not None else [
{"ref": "Q2", "artifact": "0" * 64, "role": "data", "columns": ["region", "week", "gmv"], "rows": "all"}],
"inputs": inputs if inputs is not None else default_inputs,
"probes": [],
"status": status, # type: ignore[typeddict-item]
"reason_code": reason_code,
"reason": None if status == "ok" else "历史只有 5 期,至少要 8 期",
"table": table if status == "ok" else None,
"values": {"base_total": "2220", "target_total": "2026", "delta_total": "-194", "pct_total": "-0.0874",
"top_key": "华东", "top_share": "0.6186", "segments": "4"} if status == "ok" else {},
"table": table if status == "ok" else None, # type: ignore[typeddict-item]
"values": values if status == "ok" else {},
"verdict": ({"code": verdict, "subjects": subjects if subjects is not None else
([["华东"]] if verdict == "driver" else []), "direction": direction} # type: ignore[typeddict-item]
if status == "ok" and verdict else None),
"criterion": "一段占总变化一半以上算主要来源",
"criterion": _CRITERIA.get(op, _CRITERIA["decompose"]),
"phrase": _PHRASES.get(verdict or "", "") if status == "ok" else "变化拆解没有做成:历史只有 5 期,至少要 8 期",
"caveats": [],
"hypothesis": "地区拖累",
+12
View File
@@ -247,3 +247,15 @@ async def test_structural_call_errors_still_fail_even_when_data_errors_are_refus
with pytest.raises(CallError) as e:
await analyze("decompose", "additive", snaps=REFUSALS, refuse_data_errors=True, check=False, **params)
assert e.value.code not in analysis.DATA_ERRORS and e.value.reason is None
@pytest.mark.parametrize("op,mode,params", [CALLS[0], *CALLS[2:]])
async def test_the_fake_results_for_tests_have_each_operators_shape(op, mode, params):
from app.analysis.testing import fake_result
real = await _run(op, mode, params)
fake = fake_result(op, mode, real["verdict"]["code"])
assert fake["method"] == real["method"] and fake["table"]["columns"] == real["table"]["columns"]
assert set(fake["table"]["labels"]) == set(fake["table"]["columns"])
assert set(fake["values"]) <= set(real["values"]) and {i["role"] for i in fake["inputs"]} == \
{i["role"] for i in real["inputs"]}