solvi.counterfactual¶
Counterfactual explanations: the smallest change of a decision's given inputs that changes its answer, re-run through the deterministic flow with the models' recorded proposals.
Counterfactual explanations: the smallest change of a decision's given inputs that changes its answer — "approve if amount ≤ 1000 (now 1200)", "refund if purchase_date ≥ 2026-08-20 (now 2026-08-10)" — for adverse-action reasons in lending and clear answers in support.
How: the recorded decision is re-run on changed inputs through the deterministic flow only. The flow is the recorded one (same keys, so the same plan); every model-backed part — an extractor, a model decision, a learned answer head, a part whose provenance is decided / learned / proposed — is replaced by the proposal it recorded in this trace, so no model is ever called: the answer is "what the code would decide if the models said what they said". A model part that did not run in the recorded decision (skipped after a hard check failed) has no proposal: on inputs that need it the question abstains, and such inputs are not counted as changing the answer. Learned rule lists (learn_rule) are code and are re-run.
The search, per input:
numbers (int, float) and dates — probe outward from the current value in both directions with doubling steps, then
bisect between the last unchanged and the first changed value: the nearest threshold crossing for inputs the answer
is monotone in. A direction where no probe changes the answer is tried again on an even grid between the current
value and the farthest probe within the range, which finds a band of a non-monotone input ("alert unless within 5
of -20") — a band narrower than the grid can still be missed, so "no change was found" is what the result says.
Non-negative inputs stay non-negative unless a domain says otherwise; a domain (lo, hi) that does not contain the
current value is refused for that input (listed as not searched); float bounds are shown at the shortest decimal
that still holds;
booleans, Enums and Literal-typed inputs (System(input_model=...)) — every other value;
anything else — only with domains={fact: [values]}.
Two inputs together only when no single one changes the answer: one input's candidates (its values, or probe points) with
a search over the other, then each bound tightened with the other change made. Changes are ranked by count, then by size
(relative change of a number; days moved over 30 — or over the domain's width — for a date; 1 for an enumerated value).
Change
dataclass
¶
One change of a given input in a counterfactual: the fact, its value now, the value tried (op "=" or a
boundary "≤", "<", "≥", ">" for a number) and its cost.
Counterfactual
dataclass
¶
Counterfactual(answer: object, changes: list, status: str = 'ok', why: str = '', kind: str | None = None)
The answer after a set of Changes (str: "cost (the sum
of the changes' costs); to_dict() for JSON.
Rerun ¶
The recorded decision as a function of its given inputs: rerun({fact: value}) → the question's Result, through the
recorded flow with every model-backed part held at its recorded proposal.
search ¶
search(res, question, max_changes=2, over=None, target=None, domains=None, system=None, max_evals=5000)
See Response.counterfactual.