Explain failures in a UI¶
A failing rule is only useful when someone can see why. For every failing
record, explain returns an
Explanation: a sentence, the formula with the
reported values filled in, both sides of the comparison with their intervals,
and the cells involved. This guide turns that into a payload for a review
screen.
The report¶
from dpmxl import MemoryProvider, Number, TableLayout, evaluate_rule, explain
amounts = {
("0010", "0010"): 100,
("0020", "0010"): 60,
("0030", "0010"): 40,
("0010", "0020"): 1204,
("0020", "0020"): 1180,
("0030", "0020"): 31,
}
provider = MemoryProvider(
[TableLayout("C_01.00", rows=("0010", "0020", "0030"), columns=("0010", "0020"))],
{
("C_01.00", row, column, None, ()): Number.of(amount, 0)
for (row, column), amount in amounts.items()
},
)
expression = (
"with {tC_01.00, c*, default: 0, interval: true}: {r0010} = {r0020} + {r0030}"
)
result = evaluate_rule(expression, provider)
outcome = result.failures[0]
explanation = explain(result.evaluation, expression, outcome.key)
result.evaluation holds the intermediate values of every node, which
explain reads. Pass the same expression text the rule was evaluated from.
What an explanation holds¶
print(explanation.kind)
print(explanation.summary)
print(explanation.formula)
print(explanation.filled)
print(explanation.relation, "|", explanation.verb)
print(
explanation.difference,
explanation.gap,
explanation.tolerance,
explanation.direction,
)
comparison
1,204 should equal 1,211 (1,180 + 31): difference 7, at least 5.5 given the reported precision
{r0010} = {r0020} + {r0030}
1,204 = 1,180 + 31
= | should equal
7 5.5 1.5 too_low
| Field | Meaning |
|---|---|
kind |
comparison, or for other failures missing (a cell must be reported), not_empty (must be empty), membership (must be one of a set), format (must match a pattern) or condition. |
summary |
One sentence for the reviewer. |
formula, filled |
The failing statement's formula, and the same with values (1,204 = 1,180 + 31). Formulas are rendered from the tree, with uniform spacing, a typographic minus for negative terms and long runs of zeros collapsed to + 0 (x13). |
relation, verb |
The comparison symbol (=, ≠, ≤, …) and its wording (should equal). |
difference |
Left minus right, as text. |
tolerance |
How far apart the sides may be from reported precision alone (the sum of both sides' thresholds). |
gap |
The distance that remains after allowing for that precision, rounded down so that "at least" never overstates it. |
direction |
too_high when the left side exceeds the right, too_low otherwise. It says which side is larger, not which value is wrong. |
left, right |
Each side as a Side. |
operands |
Every leaf operand (cell, aggregate, parameter) as an Operand: its source text, value and cells. |
primary |
The cells on the left of the failing comparison: the values that disagree. |
Fields that do not apply are None; a failure that is not a comparison has
no sides, difference or gap.
Sides and intervals¶
for side in (explanation.left, explanation.right):
print(
side.formula,
"|",
side.filled,
"|",
side.value,
f"[{side.low}, {side.high}]",
side.terms,
)
{r0010} | 1,204 | 1,204 [1,203.5, 1,204.5] 1
{r0020} + {r0030} | 1,180 + 31 | 1,211 [1,210, 1,212] 2
With interval: true, each reported number stands for an interval (see
Interval arithmetic). The left side can be at
most 1,204.5 and the right side at least 1,210, so they are at least 5.5
apart: that is the gap.
Highlight the cells¶
Every data point carries the ref your provider gave it.
outcome.points are all the cells the record used; explanation.primary is
the subset that disagrees. MemoryProvider uses the cell key as its ref;
your provider can use a database id or a location in the report file.
def payload(rule_code, expression, result, outcome):
explanation = explain(result.evaluation, expression, outcome.key)
return {
"rule": rule_code,
"record": {name: str(value) for name, value in outcome.key.items()},
"summary": explanation.summary,
"formula": explanation.filled,
"highlight": [point.label() for point in explanation.primary],
"cells": [point.label() for point in outcome.points],
}
import json
print(json.dumps(payload("v0001", expression, result, outcome), indent=2))
{
"rule": "v0001",
"record": {
"c": "0020"
},
"summary": "1,204 should equal 1,211 (1,180 + 31): difference 7, at least 5.5 given the reported precision",
"formula": "1,204 = 1,180 + 31",
"highlight": [
"{tC_01.00, r0010, c0020}"
],
"cells": [
"{tC_01.00, r0010, c0020}",
"{tC_01.00, r0020, c0020}",
"{tC_01.00, r0030, c0020}"
]
}
DataPoint.label() renders a cell as a
DPM-XL selection; point.ref is the identity to look it up by.
Failures that are not comparisons¶
Presence checks, membership and pattern rules get their own sentence:
from dpmxl import Item
layout = TableLayout("T", rows=("0010", "0020", "0030", "0040"), columns=("0010",))
values = {
("T", "0010", "0010", None, ()): Number.of(5, 0),
("T", "0020", "0010", None, ()): "AB-12",
("T", "0030", "0010", None, ()): Item.parse("eba_qEC:qx9"),
}
report = MemoryProvider([layout], values)
for rule in (
"if not isnull({tT, r0010, c0010}) then not isnull({tT, r0040, c0010}) endif",
"isnull({tT, r0010, c0010})",
"{tT, r0030, c0010} in {[eba_qEC:qx1], [eba_qEC:qx2]}",
'match({tT, r0020, c0010}, "[0-9]+")',
):
failed = evaluate_rule(rule, report)
explained = explain(failed.evaluation, rule, failed.failures[0].key)
print(f"{explained.kind}: {explained.summary}")
missing: {tT, r0040, c0010} must be reported
not_empty: {tT, r0010, c0010} must be empty, but 5 is reported
membership: [qEC:qx9] is not one of [qEC:qx1], [qEC:qx2]
format: "AB-12" does not have the required format
Many failures on a large report¶
- Explaining is cheap per record, but a rule on a large open table can fail
thousands of times. Explain the first few hundred for the screen and count
the rest with
result.counts. evaluate_rule(..., trace=False)skips keeping intermediate values: faster and leaner when you only need statuses. Such a result cannot be explained.- To explain a few known failing records of a large closed selection, evaluate
again with
only_keys={"c": ["0020"]}, which keeps only those closed-axis codes in every selection. Do not use it for rules with aggregates, whose totals would cover only the kept records. - The columnar evaluator returns failing keys but no trace; explain its
failures by evaluating those rules again with
evaluate_rule(Evaluate large reports).
Explanation and error wording may improve in minor releases; the field names
and kind values may not (Stability).