判定语照方向接末尾连续期数、辛普森的结构变化只说一遍(复核 R14、R21):趋势末尾连续同向的期数和趋势同向时接「,已连续 3 周下降」,反向时接「,但最近已连续 3 周上升」,不再出现「呈下降趋势……已连续 3 周上升」。比率拆解判在结构变化上(mix_driven)时正文已经说了结构变化、点了那一段,辛普森那一句只说「各段的退款率大多在下降,整体却上升」;别的判定照旧带上哪一段占比从多少变到多少

This commit is contained in:
ThearchyHelios
2026-10-21 15:57:51 +08:00
parent 427a6df23c
commit fcbcfc9aef
5 changed files with 39 additions and 8 deletions
+3 -1
View File
@@ -577,9 +577,11 @@ def _ratio_result(env: Env, params: dict[str, Any], used: list[str], segs: list[
against = sum(((by_key[x.key][0] + by_key[x.key][1]) / 2 for x in eligible
if (by_key[x.key][3] - by_key[x.key][2]) * sign < 0), Fraction(0))
if weight > 0 and against / weight >= SIMPSON_SHARE:
# 判在结构变化上(mix_driven)时判定语正文已经说了结构变化、点了这一段,附带说明只说各段和整体反着变
star = max(live, key=lambda x: sign * by_key[x.key][4])
caveats.append(ph.caveat("simpson", ph.simpson_text(label, delta > 0, ph.key_text(star.key),
by_key[star.key][0], by_key[star.key][1])))
by_key[star.key][0], by_key[star.key][1],
shift=code != "mix_driven")))
if small:
caveats.append(ph.caveat("small_denominator", ph.small_denominator_text(
[ph.key_text(x.key) for x in small], den_label, int(env.settings["min_denominator"]))))
+10 -5
View File
@@ -301,13 +301,16 @@ def offset_text(delta_total: Any, gross_up: Any, gross_down: Any) -> str:
return f"下降的段合计 {down},上升的段合计 {up},互相抵消了一部分"
def simpson_text(label: str, overall_up: bool, subject: str, w_base: Any, w_target: Any) -> str:
"""辛普森:各段的比率大多和整体反着变,整体的变化来自结构。"""
def simpson_text(label: str, overall_up: bool, subject: str, w_base: Any, w_target: Any, *, shift: bool = True) -> str:
"""辛普森:各段的比率大多和整体反着变,整体的变化来自结构。判定语正文已经说了结构变化、点了那一段(mix_driven)时
shift 为假,只说各段和整体反着变,结构变化不说第二遍。"""
seg = "上升" if not overall_up else "下降"
whole = "上升" if overall_up else "下降"
head = say("各段的", label, f"大多在{seg},整体却{whole}")
if not shift:
return head
move = "升" if _q(w_target) > _q(w_base) else "降"
return say(f"各段的", label, f"大多在{seg},整体却{whole}:变化来自结构,", subject,
f"占比从 {pct(w_base, signed=False)} {move}到 {pct(w_target, signed=False)}")
return say(head, ":变化来自结构,", subject, f"占比从 {pct(w_base, signed=False)} {move}到 {pct(w_target, signed=False)}")
def small_denominator_text(segments: Sequence[str], denominator: str, minimum: int) -> str:
@@ -478,13 +481,15 @@ def baseline_flat_phrase(verdict: str, label: str, current: str, n: int, unit: s
def trend_phrase(verdict: str, label: str, n: int, unit: str, per: str, slope: str | None,
run: tuple[int, str] | None) -> str:
"""趋势。末尾连续同向的期数(run)和趋势同向时接「,已连续 3 周下降」,反向时接「,但最近已连续 3 周上升」。"""
if verdict == "no_trend":
return say(f"过去 {n} {unit}", label, "没有明显的上升或下降趋势")
word = "上升" if verdict == "rising" else "下降"
text = say(f"过去 {n} {unit}", label, f"呈{word}趋势") + (f"(每{per}约 {slope})" if slope else "")
if run is not None:
length, direction = run
text += f",已连续 {length} {unit}{'上升' if direction == 'up' else '下降'}"
same = (direction == "up") == (verdict == "rising")
text += f",{'' if same else '但最近'}已连续 {length} {unit}{'上升' if direction == 'up' else '下降'}"
return text
+5 -2
View File
@@ -301,9 +301,12 @@ async def test_simpson_each_channel_improves_but_the_mix_makes_the_whole_worse()
rows = [["线上", "W41", 50, 500], ["线上", "W42", 90, 1000], ["线下", "W41", 10, 1000], ["线下", "W42", 4, 500]]
result = await ratio(rows, label="退款率")
assert result["verdict"] == {"code": "mix_driven", "subjects": [["线上"]], "direction": "up"}
assert result["phrase"].startswith("退款率上升 2.3 个百分点(4.0% 到 6.3%),主要来自结构变化:线上占比从 33.3% 到 66.7%")
# 结构变化只说一遍:正文说了结构变化、点了线上,辛普森那一句只说各段和整体反着变
assert result["phrase"] == ("退款率上升 2.3 个百分点(4.0% 到 6.3%),主要来自结构变化:线上占比从 33.3% 到 66.7%;"
"各段的退款率大多在下降,整体却上升")
simpson = next(c for c in result["caveats"] if c["code"] == "simpson")
assert simpson["text"] == "各段的退款率大多在下降,整体却上升:变化来自结构,线上占比从 33.3% 升到 66.7%"
assert simpson["text"] == "各段的退款率大多在下降,整体却上升"
assert result["phrase"].count("结构") == 1
values = result["values"]
assert Fraction(values["mix_total"]) + Fraction(values["rate_total"]) == pytest.approx(Fraction(values["delta"]))
assert result["table"]["columns"][:5] == ["channel", "n_base", "n_target", "d_base", "d_target"]
+15
View File
@@ -141,3 +141,18 @@ def test_criteria_are_written_with_the_actual_thresholds():
assert "3 倍以上算明显超出" in ph.criterion_baseline(DEFAULTS, 12, "周")
assert "小于 0.05" in ph.criterion_trend(DEFAULTS, 10, "周")
assert "Fisher" in ph.criterion_differ(DEFAULTS, "rate") and "Welch" in ph.criterion_differ(DEFAULTS, "mean")
def test_the_run_at_the_end_of_a_trend_says_whether_it_goes_the_same_way():
assert ph.trend_phrase("falling", "销售额", 10, "周", "周", "-2.0%", (3, "down")) == \
"过去 10 周销售额呈下降趋势(每周约 -2.0%),已连续 3 周下降"
assert ph.trend_phrase("falling", "销售额", 10, "周", "周", "-2.0%", (3, "up")) == \
"过去 10 周销售额呈下降趋势(每周约 -2.0%),但最近已连续 3 周上升"
assert ph.trend_phrase("rising", "订单数", 8, "个月", "月", None, (4, "down")) == \
"过去 8 个月订单数呈上升趋势,但最近已连续 4 个月下降"
def test_simpson_says_the_shift_only_when_the_phrase_does_not():
assert ph.simpson_text("退款率", True, "线上", 0.3, 0.5) == "各段的退款率大多在下降,整体却上升:变化来自结构,线上占比从 30.0% 升到 50.0%"
assert ph.simpson_text("退款率", True, "线上", 0.3, 0.5, shift=False) == "各段的退款率大多在下降,整体却上升"
+6
View File
@@ -313,3 +313,9 @@ async def test_sequence_periods_say_completeness_cannot_be_checked():
result = await baseline(rows, basis="level")
assert {"code": "comparability", "text": "没能核对本期的数据是否到齐(期次不是日期,不知道是哪几天)"} \
in result["caveats"]
async def test_a_falling_trend_whose_last_weeks_rise_says_but():
falling = await trend(weeks([200, 196, 190, 188, 181, 176, 170, 165, 160, 162, 165, 168]))
assert falling["verdict"]["code"] == "falling" and falling["values"]["run_direction"] == "up"
assert falling["phrase"].endswith(",但最近已连续 3 周上升")