闭合容差:整数的浮点按个位、下钻对上一层的精确值按精确比(复核 R15):总数那一格是整数的浮点(SQLite 的 ROUND() 交回 2120.0)时按取到个位算容差(差 0.4 也闭合),最短写法里的「.0」不再当一位小数;下钻时对上一层那一段的值是上一层算出来的精确值、没有取整过,只容浮点算出来的最后几位(相对 1e-9),820.4 对 820、700.3 对 700 不再算闭合。可加、比率两种下钻同一套

This commit is contained in:
ThearchyHelios
2026-10-21 15:59:11 +08:00
parent fcbcfc9aef
commit efff2ac4f8
3 changed files with 33 additions and 6 deletions
+8 -4
View File
@@ -107,22 +107,26 @@ def need(raw: Mapping[str, Any], key: str, what: str) -> Any:
def written_places(raw: Any) -> int:
"""快照里一格写到第几位小数(headline 是取整过的,按它的书写精度容差):数字文本按字面,浮点按最短写法,整数为 0。"""
"""快照里一格写到第几位小数(headline 是取整过的,按它的书写精度容差):数字文本按字面,浮点按最短写法,整数为 0。是整数
的浮点(SQLite 的 ROUND() 交回 2120.0)是取到个位的,也为 0——最短写法里的「.0」不是书写精度。"""
if isinstance(raw, bool) or raw is None:
return 0
if isinstance(raw, float) and raw.is_integer():
return 0
text = repr(raw) if isinstance(raw, float) else str(raw).strip()
if "e" in text.lower():
return 0
return len(text.split(".", 1)[1]) if "." in text else 0
def closes(total: Fraction, cell: Fraction, raw: Any) -> bool:
def closes(total: Fraction, cell: Fraction, raw: Any, *, exact: bool = False) -> bool:
"""各段之和(或合起来的比率)和总数那一格对得上:十进制逐位相等;总数取整过的,差在它书写精度的半个末位以内也算;浮点
算出来的差在最后几位的也算(相对误差 1e-9 以内,同 engine/compare._equal)。"""
算出来的差在最后几位的也算(相对误差 1e-9 以内,同 engine/compare._equal)。exact:对的是上一层那一段算出来的精确值
(下钻),没有取整,只容浮点的最后几位。"""
if total == cell:
return True
gap = abs(total - cell)
if gap <= Fraction(1, 2) / Fraction(10) ** written_places(raw):
if not exact and gap <= Fraction(1, 2) / Fraction(10) ** written_places(raw):
return True
scale = max(abs(total), abs(cell))
return scale != 0 and gap <= scale * Fraction(1, 10 ** 9)
+2 -2
View File
@@ -345,7 +345,7 @@ def _additive_result(env: Env, params: dict[str, Any], used: list[str], segs: li
gaps[side] = heads[side][0] - total
if parent_row is not None:
expect = to_fraction(parent_row.get(side))
if expect is None or not closes(total, expect, parent_row.get(side)):
if expect is None or not closes(total, expect, None, exact=True):
gaps.setdefault(side, (expect or Fraction(0)) - total)
closed = not gaps
deltas = {x.key: x.target - x.base for x in segs}
@@ -547,7 +547,7 @@ def _ratio_result(env: Env, params: dict[str, Any], used: list[str], segs: list[
"d_target": sum((x.target[1] for x in live), Fraction(0))}
for col, total in sums.items():
expect = to_fraction(parent_row.get(col))
if expect is None or not closes(total, expect, parent_row.get(col)):
if expect is None or not closes(total, expect, None, exact=True):
gaps.setdefault("base" if col.endswith("base") else "target", (expect or Fraction(0)) - total)
closed = not gaps
mix_total = sum((e[4] for e in effects), Fraction(0))
+23
View File
@@ -127,6 +127,29 @@ async def test_a_rounded_headline_closes_within_its_written_precision():
assert (result["verdict"]["code"] != "unclosed") is closes, written
async def test_a_headline_rounded_by_the_database_to_a_whole_number_closes_within_half_a_unit():
# SQLite 的 ROUND() 交回浮点 2120.0:取到了个位,最短写法里的「.0」不是一位小数的书写精度
rows = [["华东", "2026-W41", "1120.2"], ["华东", "2026-W42", "700"], ["华北", "2026-W41", "1000.2"],
["华北", "2026-W42", "600"]]
for written, closes in ((2120.0, True), (2121.0, False), (2120.5, False)):
head = snap("Q1", "SELECT ROUND(SUM(amount)) AS gmv FROM orders", ["gmv_base", "gmv"], [[written, 1300.0]])
result = await additive(rows, snaps=[weekly(rows), head], headline=["Q1.r0.gmv_base", "Q1.r0.gmv"])
assert (result["verdict"]["code"] != "unclosed") is closes, written
async def test_a_drill_closes_against_the_exact_value_of_the_parent_segment():
top = await additive() # 华东 820 → 700
near = snap("Q4", "SELECT province, week, SUM(amount) AS gmv FROM orders WHERE region = '华东' GROUP BY 1, 2",
["province", "week", "gmv"],
[["沪", "2026-W41", "500.4"], ["沪", "2026-W42", "410"], ["苏", "2026-W41", 320],
["苏", "2026-W42", "290.3"]])
params = {"query": "Q4", "by": ["province"], "period": "week", "base": "2026-W41", "target": "2026-W42",
"measure": "gmv", "parent": {"of": "A3", "key": ["华东"]}}
off = await analyze("decompose", "additive", snaps=[near], parents={"A3": top}, **params)
# 上一层那一段是算出来的精确值,不是取整过的总数:820.4 对 820、700.3 对 700 都不闭合
assert off["verdict"]["code"] == "unclosed" and off["values"]["gap"] == "-0.4"
async def test_drilling_down_checks_closure_against_the_parent_segment():
top = await additive()
province = snap("Q4", "SELECT province, week, SUM(amount) AS gmv FROM orders WHERE region = '华东' GROUP BY 1, 2",