Files
AgentLab/backend/tests/test_analysis_phrases.py
T

159 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""判定语、数的写法、label 消毒、判定标准(app/analysis/phrases.py、numbers.py):一种说法,只在这里定义。"""
from __future__ import annotations
from fractions import Fraction
from app.analysis import phrases as ph
from app.analysis.contract import DEFAULTS
from app.analysis.numbers import canon, fixed, round_sig, to_fraction
def test_canonical_decimal_strings_are_plain_and_stable():
assert canon(Fraction(-120)) == "-120"
assert canon("2220.00") == "2220" # 去掉多余的 0
assert canon(0.1 + 0.2) == "0.3" # 浮点留 12 位有效数字
assert canon(Fraction(1, 3)) == "0.333333333333"
assert canon(Fraction(7, 16)) == "0.4375" # 除得尽的照写
assert canon(-0.0) == "0" and canon(Fraction(0)) == "0"
assert canon(1e-30) == "0.000000000000000000000000000001" # 不带指数
assert canon(1e20) == "100000000000000000000"
assert canon("123456789012345678.25") == "123456789012345678.25" # 快照里的数和它们的和差照原样
assert canon(None) is None and canon("abc") is None and canon(True) is None
assert to_fraction(0.1) == Fraction(1, 10) and to_fraction("45678.50") == Fraction(4567850, 100)
assert round_sig(Fraction(2, 3), 3) == Fraction(667, 1000)
def test_fixed_rounds_half_up_like_render_cell():
assert fixed(Fraction(2675, 1000), 2) == "2.68" # 2.675 不走浮点的 round(会得 2.67)
assert fixed(Fraction(-5, 100), 1, strip=False) == "-0.1"
assert fixed(Fraction(-4, 100), 1, strip=False) == "0.0" # 不写 -0.0
assert fixed(Fraction(123456789, 100), 2, group=True) == "1,234,567.89"
def test_number_formats_in_phrases():
assert ph.pct(Fraction(-87, 1000)) == "-8.7%" and ph.pct(Fraction(3, 100)) == "+3.0%"
assert ph.pct(0) == "0.0%" and ph.pct(Fraction(6186, 10000), signed=False) == "61.9%"
assert ph.pct(Fraction(4, 100000)) == "+0.004%" # 不是 0 的不写成 0.0%
assert ph.amount(Fraction(1234567891, 1000)) == "1,234,567.89"
assert ph.amount(Fraction(120)) == "120" and ph.amount(Fraction(-120), signed=True) == "-120"
assert ph.amount(Fraction(40), signed=True) == "+40"
assert ph.amount(Fraction(21, 1000)) == "0.021" and ph.amount(Fraction(4567, 10 ** 7)) == "0.00046"
assert ph.change(Fraction(-194), Fraction(2220)) == "下降 194(-8.7%)"
assert ph.change(Fraction(120), Fraction(0)) == "上升 120" # 基期为 0 不给百分比
assert ph.change(0, 5) == "没有变化"
assert ph.ratio_change(Fraction(21, 1000), Fraction(25, 1000), proportion=True) == "上升 0.4 个百分点(2.1% 到 2.5%)"
assert ph.ratio_change(Fraction(82), Fraction(852, 10), proportion=False) == "上升 3.2(+3.9%)"
assert ph.threshold_pct(0.005) == "0.5%" and ph.share_words(0.5) == "一半" and ph.share_words(0.6) == "60%"
def test_labels_are_scrubbed_of_numbers_units_and_markers():
assert ph.clean_label("销售额(同比 +30%)") == "销售额(同比)"
assert ph.clean_label("近三成客户的退款率") == "近客户的退款率"
assert ph.clean_label("销售额[[v:Q1.r0.gmv]]") == "销售额"
assert ph.clean_label("¥ 销售额 3.5万") == "销售额"
assert ph.clean_label("gmv_2026") == "gmv_"
assert len(ph.clean_label("销售额" * 30)) == ph.LABEL_MAX
assert ph.clean_label("30%") == "" and ph.clean_label(None) == ""
# 消毒后为空就退回下一个来源(目录里的叫法、列名)
assert ph.pick_label("12.5%", None, "gmv") == "gmv"
assert ph.pick_label("", "实收金额", "gmv") == "实收金额"
assert ph.pick_label("2026", None, "2026") == "这一列"
def test_scrubbing_a_scrubbed_label_changes_nothing():
# 去掉一处可能拼出新的一处:去完数字的「销售额[[v:Q.r.gmv]]」又是一个引用标记,要接着去
assert ph.clean_label("销售额[ [v:Q1.r0.gmv] ]") == "销售额"
assert ph.clean_label("[ [see:A1] ]销售额") == "销售额" and ph.clean_label("销售额[1[v:x]2]") == "销售额"
for text in ("销售额[ [v:Q1.r0.gmv] ]", "销售额 [ [ a:A1 ] ]", "销售额( )(3)", "销售额(同比 +30%,3.5万)",
"近三成客户的退款率", "【线上】销售额", "退款率 (%)", "销售额(", " ,销售额。", "一[二]三",
"销售额" * 30, "[[a:A1]]主要来自[[v:Q1.r0.region]]"):
once = ph.clean_label(text)
assert ph.clean_label(once) == once, text
def test_labels_stop_at_the_first_clause_and_judgments_fall_back():
assert ph.clean_label("销售额,主要来自华东") == "销售额"
assert ph.clean_label("订单数; 销售额") == "订单数" and ph.clean_label("订单数、销售额") == "订单数"
assert ph.clean_label("销售额(含税,元)") == "销售额" # 截完没配上对的括号也不要
# 判定语不受说法核对:叫法里有判断词的整个不要,退回下一个来源(目录叫法、当叫法用的列名同样处理)
assert ph.clean_label("销售额主要来自华东") == "" and ph.clean_label("历史新高的销售额") == ""
assert ph.pick_label("销售额主要来自华东", "实收金额", "gmv") == "实收金额"
assert ph.pick_label(None, "历史新高的销售额", "gmv") == "gmv"
assert ph.pick_label(None, None, "主要来自华东") == "这一列"
def test_numbers_stuffed_into_a_label_never_reach_the_phrase():
label = ph.pick_label("销售额(同比 +30%,3.5万)", "销售额", "gmv")
phrase = ph.decompose_driver(label, ph.change(-194, 2220), "华东", "-120", Fraction(6186, 10000))
assert "30" not in phrase and "3.5" not in phrase and "万" not in phrase
assert phrase == "销售额(同比)下降 194(-8.7%),主要来自华东(-120,占 61.9%)"
def test_spacing_between_chinese_and_ascii():
assert ph.say("gmv", "下降 194") == "gmv 下降 194"
assert ph.say("主要来自", "online", "(", "-120") == "主要来自 online(-120"
assert ph.say("销售额", "下降") == "销售额下降"
assert ph.key_text(["华东", "线上"]) == "华东·线上" and ph.key_text([None]) == "(空)"
assert ph.names(["华东", "华北", "华南"], 2) == "华东、华北等 3 段"
def test_phrase_templates():
assert ph.decompose_concentrated("销售额", "下降 194(-8.7%)", "华东、华北", Fraction(85, 100)) == \
"销售额下降 194(-8.7%),集中在华东、华北(合计占 85.0%)"
assert ph.decompose_dispersed("销售额", "下降 194(-8.7%)", "地区", 0.5) == \
"销售额下降 194(-8.7%),分散在各地区,没有哪一段占到一半"
assert ph.decompose_dispersed("销售额", "下降 194(-8.7%)", "地区", 0.6).endswith("没有哪一段占到 60%")
assert ph.decompose_flat("销售额", Fraction(2, 1000), ("华东", "+120", "华北", "-118")) == \
"销售额基本持平(+0.2%),内部此消彼长:华东 +120、华北 -118"
assert ph.decompose_unclosed("地区", Fraction(-12), ratio=False) == "按地区拆开后各段之和和总数对不上(差 12),不判断变化来自哪里"
assert ph.baseline_phrase("outside_band", "销售额", "本期变化 -8.7%", 12, "周", "-6.1% 至 +5.3%") == \
"销售额本期变化 -8.7%,超出过去 12 周的常规波动范围(-6.1% 至 +5.3%)"
assert ph.trend_phrase("rising", "线上销售额", 10, "周", "周", "+3.0%", (4, "up")) == \
"过去 10 周线上销售额呈上升趋势(每周约 +3.0%),已连续 4 周上升"
assert ph.trend_phrase("no_trend", "销售额", 8, "个月", "月", None, None) == "过去 8 个月销售额没有明显的上升或下降趋势"
assert ph.differ_phrase([ph.differ_pair("西南", "华东", "退款率", "5.0%", "2.1%", True)], 1) == \
"西南的退款率(5.0%)高于华东(2.1%),差异站得住"
assert ph.no_evidence_phrase("西南", "华东", "退款率", ("40", "230"), 1) == "西南和华东的退款率差异可能只是样本波动(样本量 40、230)"
assert ph.comparable_phrase("incomparable", ["本期数据只到 2026-10-15,还差 3 天"], []) == \
"两期不可比:本期数据只到 2026-10-15,还差 3 天"
assert ph.comparable_phrase("comparable", [], ["整期"]) == "两期可比,整期核对不了"
assert ph.refused("baseline", "只有 5 期历史,至少要 8 期") == "波动基线没有做成:只有 5 期历史,至少要 8 期"
assert ph.current_text(Fraction(-31, 1000), basis="change", lag=7, grain="day") == "比上周同一天变化 -3.1%"
assert ph.current_text(Fraction(1, 10), basis="change", lag=52, grain="week") == "同比变化 +10.0%"
def test_caveats_follow_the_phrase_at_most_two_in_order():
caveats = [ph.caveat("measure_unlabeled", "目录没标"), ph.caveat("null_values", "西南为空"),
ph.caveat("offset", "互相抵消"), ph.caveat("comparability", "两期有条件可比:周末不同")]
assert ph.with_caveats("判定", caveats) == "判定;两期有条件可比:周末不同;互相抵消"
assert ph.with_caveats("判定", caveats, skip=("offset",)) == "判定;两期有条件可比:周末不同;西南为空"
assert ph.with_caveats("判定", [ph.caveat("headline_basis", "以各段之和为总数")]) == "判定"
def test_criteria_are_written_with_the_actual_thresholds():
text = ph.criterion_additive(DEFAULTS, top_k_max=2, summed=True)
assert text == ("一段占总变化的一半以上算主要来源,最多前 2 段合计占 80% 以上算集中,总变化不到基期的 0.5% 算基本持平,"
"以各段之和为总数")
custom = {**DEFAULTS, "main_share": 0.6, "flat_pct": 0.01}
assert "占总变化的 60% 以上" in ph.criterion_additive(custom, top_k_max=3, summed=False)
assert "1% 算基本持平" in ph.criterion_additive(custom, top_k_max=3, summed=False)
assert "差不到 2 倍稳健标准差" in ph.criterion_baseline(DEFAULTS, 12, "周")
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) == "各段的退款率大多在下降,整体却上升"