From bea1119a6c54788bd38d93aec5ef8a3b5e156dc3 Mon Sep 17 00:00:00 2001 From: ThearchyHelios Date: Wed, 21 Oct 2026 13:16:48 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=B4=E6=B3=95=E6=A0=B8=E5=AF=B9=E6=8C=89?= =?UTF-8?q?=E7=AE=97=E5=AD=90=E7=9A=84=E7=9C=9F=E5=AE=9E=E7=BB=93=E8=AE=BA?= =?UTF-8?q?=E5=BD=A2=E7=8A=B6=E5=AF=B9=E9=BD=90=EF=BC=9A=E7=82=B9=E5=90=8D?= =?UTF-8?q?=E7=9A=84=E6=AE=B5=E5=8F=AA=E8=AE=A4=E6=8B=86=E8=A7=A3=E7=9A=84?= =?UTF-8?q?=E7=BB=B4=E5=BA=A6=E5=88=97=EF=BC=88params.by=EF=BC=8C=E4=B9=98?= =?UTF-8?q?=E7=A7=AF=E6=8B=86=E8=A7=A3=E6=98=AF=E5=9B=A0=E5=AD=90=E5=8F=AB?= =?UTF-8?q?=E6=B3=95=EF=BC=89=E5=92=8C=E5=B7=AE=E5=BC=82=E6=A3=80=E9=AA=8C?= =?UTF-8?q?=E7=9A=84=20a=E3=80=81b=20=E4=B8=A4=E7=BB=84=EF=BC=8C=E5=88=AB?= =?UTF-8?q?=E7=9A=84=E7=AE=97=E5=AD=90=E6=B2=A1=E6=9C=89=E6=AE=B5=EF=BC=9B?= =?UTF-8?q?=E6=AF=94=E7=8E=87=E6=8B=86=E8=A7=A3=E7=9A=84=20n=5Fbase?= =?UTF-8?q?=E3=80=81n=5Ftarget=20=E6=98=AF=E5=BA=A6=E9=87=8F=E5=88=97?= =?UTF-8?q?=EF=BC=9B=E3=80=8C=E6=8A=B5=E6=B6=88=E3=80=8D=E9=99=A4=E4=BA=86?= =?UTF-8?q?=20offset=20=E4=B9=9F=E8=AE=A4=E5=88=A4=E5=AE=9A=E4=B8=BA?= =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E6=8C=81=E5=B9=B3=E3=80=81=E5=90=84=E6=AE=B5?= =?UTF-8?q?=E6=9C=89=E6=B6=A8=E6=9C=89=E8=B7=8C=E7=9A=84=EF=BC=88=E6=8C=81?= =?UTF-8?q?=E5=B9=B3=E7=9A=84=E5=88=A4=E5=AE=9A=E8=AF=AD=E6=9C=AC=E8=BA=AB?= =?UTF-8?q?=E8=AF=B4=E4=BA=86=E6=AD=A4=E6=B6=88=E5=BD=BC=E9=95=BF=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/engine/analysis_claims.py | 34 ++++++++++++++++++++------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/backend/app/engine/analysis_claims.py b/backend/app/engine/analysis_claims.py index 88faeb11..7cef1666 100644 --- a/backend/app/engine/analysis_claims.py +++ b/backend/app/engine/analysis_claims.py @@ -199,10 +199,10 @@ LEXICONS: dict[int, Lexicon] = {1: _V1} #: 有这几类判断的分句里,因果词并进它们核(「下降主要由华东造成」:说的是记账意义上的来源) _SOURCE_KEYS = frozenset({"source", "source_not", "dispersed", "offset", "mix"}) -#: 结果表里不是维度的列(按维度认点名的段时跳过) +#: 拆解结果表里不是维度的列(params.by 对不上时按「不是度量列」认维度,跳过这些) _MEASURE_COLUMNS = frozenset({ - "base", "target", "delta", "pct", "share", "flag", "d_base", "d_target", "w_base", "w_target", "r_base", - "r_target", "mix", "rate", "total", "contrib", + "base", "target", "delta", "pct", "share", "flag", "n_base", "n_target", "d_base", "d_target", "w_base", + "w_target", "r_base", "r_target", "mix", "rate", "total", "contrib", }) #: 一段的变化记在哪一列:可加拆解的 delta、比率拆解的 total(结构 + 自身)、乘积拆解的 contrib _DELTA_COLUMNS = ("delta", "total", "contrib") @@ -350,13 +350,17 @@ def _named_value(value: Any) -> str | None: def _dims(result: Mapping[str, Any]) -> list[int]: - """结果表里维度列的位置:拆解按 params.by,乘积拆解是 factor 列,别的按「不是度量列」认。""" + """结果表里各段名字所在的列:拆解按 params.by(照输入的列名),乘积拆解是 factor 列(因子的叫法),差异检验是 a、b 两组; + 拆解的 by 对不上表头时按「不是度量列」认。别的算子没有段。""" table = result.get("table") or {} columns = [str(c) for c in table.get("columns") or []] - params = result.get("params") or {} + if result.get("op") == "differ": + return [columns.index(c) for c in ("a", "b") if c in columns] + if result.get("op") != "decompose": + return [] if result.get("mode") == "factor" and "factor" in columns: return [columns.index("factor")] - by = params.get("by") + by = (result.get("params") or {}).get("by") wanted = by if isinstance(by, list) else [by] if isinstance(by, str) else [] picked = [columns.index(str(b)) for b in wanted if str(b) in columns] return picked or [i for i, c in enumerate(columns) if c not in _MEASURE_COLUMNS] @@ -437,6 +441,18 @@ def _segment_change(result: Mapping[str, Any], name: str) -> tuple[Decimal | Non return total, share +def _both_ways(result: Mapping[str, Any]) -> bool: + """结果表里各段的变化有涨有跌。""" + table = result.get("table") or {} + columns = [str(c) for c in table.get("columns") or []] + col = next((columns.index(c) for c in _DELTA_COLUMNS if c in columns), None) + if col is None: + return False + signs = {d > 0 for row in table.get("rows") or [] if isinstance(row, list) and col < len(row) + and (d := _decimal(row[col])) is not None and d != 0} + return signs == {True, False} + + def _caveats(result: Mapping[str, Any]) -> set[str]: return {str(c.get("code")) for c in result.get("caveats") or [] if isinstance(c, dict)} @@ -526,8 +542,10 @@ def _check(lex: Lexicon, hit: Hit, result: Mapping[str, Any], names: str) -> _Mi if key == "dispersed": return None if code == "dispersed" else _verdict_miss(result) if key == "offset": - return None if "offset" in _caveats(result) else _Miss( - "没有记下此消彼长(各段的变化方向大多一致)", "这份结论没有记下各段此消彼长") + # 抵消记在 offset 里;判定为基本持平的,判定语本身就说了此消彼长(不一定再接 offset),各段有涨有跌就算 + if "offset" in _caveats(result) or (code == "flat" and _both_ways(result)): + return None + return _Miss("没有记下此消彼长(各段的变化方向大多一致)", "这份结论没有记下各段此消彼长") if key == "mix": if result.get("mode") != "ratio": return _Miss("是按维度的可加拆解,说明不了结构变化:结构变化要挂比率拆解",