mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 13:22:01 +00:00
159 lines
11 KiB
Python
159 lines
11 KiB
Python
"""判定语、数的写法、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) == "各段的退款率大多在下降,整体却上升"
|