mixle.inference.explain module¶
Exact per-part attribution for scores and decision margins.
explain decomposes a model’s score for one observation.
explain_margin and explain_margin_mixture decompose the decision margin
between two named hypotheses for evidence ledgers and receipts.
Because mixle models are generative and structured, a prediction’s score decomposes exactly — no surrogate models, no sampling approximations:
a Composite / Record factorizes over fields:
log p(x) = sum_i log p_i(x_i)a learned Bayesian network factorizes over nodes:
log p(x) = sum_i log P(x_i | parents)a Mixture adds the latent view: per-component responsibilities, then the winner’s field breakdown – plus an explicit
correctionterm for the logsumexp normalizer, since a mixture’s total log-density is not a sum of any single component’s parts (the one place these structures are not purely additive). The ledger always satisfiessum(v for _, v in parts) + correction == totalto machine precision – that identity is the point, not an approximation of it.
explain(model, x) returns those parts with their exact log-likelihood contributions, sorted so the
most suspicious part (lowest contribution) is first: “which field makes this record unlikely” is read
straight off the model rather than estimated:
ex = explain(model, record)
ex.parts # [(name, log-contribution), ...] ascending (most anomalous first)
ex.total # == model.log_density(record), exactly
ex.correction # 0 for Composite/BN (purely additive); the logsumexp residual for Mixture
ex.responsibilities # mixtures: posterior over components
explain_margin(model, answer, runner_up) decomposes log p(answer) - log p(runner_up) – the
decision margin between two named hypotheses – into the same kind of per-factor ledger:
Composite / BN:
answer/runner_upare two full candidate records; a field/factor that does not differ between them contributes exactly 0, so a single corrupted field is visible as the one part that does not collapse to 0.Mixture used as a generative classifier (
explain_margin_mixture):answer/runner_upare component indices (e.g. class labels) at the same observedx; the margin is(log_w[a] + log p_a(x)) - (log_w[b] + log p_b(x))– the mixture’s logsumexp normalizer cancels exactly in this subtraction, so the margin ledger needs no correction term (it is computed and asserted at 0.0, not assumed).
- class Explanation(total, parts=<factory>, correction=0.0, responsibilities=None, component=None)[source]
Bases:
objectExact additive attribution of a log-density (or a decision margin between two hypotheses).
correctionis the explicitly named non-additive residual – 0.0 for purely additive structures (Composite, Bayesian network, and any margin comparison, since the normalizer cancels there); the logsumexp term for a Mixture’s absolutelog p(x).sum(v for _, v in parts) + correctionequalstotalexactly, for every supported structure – this is asserted to machine precision in tests.- Parameters:
- most_anomalous(k=3)[source]
Return the top contribution terms by anomaly score order.
- ledger_sum()[source]
sum(parts) + correction– should equaltotalto machine precision; see class docstring.- Return type:
- is_exact(atol=1e-9)[source]
Return whether ledger parts plus correction reconstruct the total.
- explain(model, x)[source]
Exact per-part attribution of
model.log_density(x)(see module docstring).
- explain_margin(model, answer, runner_up)[source]
Exact per-part attribution of the decision margin
log p(answer) - log p(runner_up).answer/runner_upare two full candidate records for Composite/Bayesian-network models, so a field that is identical between them contributes exactly 0 – the diagnostic for a single corrupted field. For a Mixture used as a generative classifier, useexplain_margin_mixture()instead (the margin there needs the observed point plus two component indices, not two full records).
- explain_margin_mixture(model, x, answer, runner_up)[source]
Exact per-part attribution of the decision margin between two Mixture components at one point
x.answer/runner_upare component indices (e.g. class labels) of a Mixture used as a generative classifier: the margin is(log_w[answer] + log p_answer(x)) - (log_w[runner_up] + log p_runner_up(x)). The mixture’s logsumexp normalizer – the same termexplain()must name as a correction for the absolutelog p(x)– cancels exactly in this subtraction, so the margin ledger needs no correction (computed and asserted at 0.0, not assumed away).
- class FaultReport(dominant, evidence=<factory>, suggested_fix='', receipt=<factory>)[source]
Bases:
objectStructural diagnosis built from
explain()ledgers over failing cases.dominantnames the structural element most responsible, when one rises above ordinary case-to-case variability.suggested_fixis one of_FIX_VOCABor empty.evidenceranks elements by adverse contribution.
- diagnose(model, cases, *, background=None, min_z=1.0, co_occurrence_threshold=0.5)[source]
Aggregate
explain()ledgers into a structural fault report.Each case’s per-part contributions are compared to
background(a reference sample of typical cases; defaults tocasesthemselves, though a real, separately-supplied background is needed to detect a fault that is systematic across every case, since self-baselining against the failing set cancels a shift common to all of it) via a robust z-score (median/MAD per part name), so “adverse” is relative to that part’s own normal variability, not a raw log-density magnitude.A single part scoring far from baseline is not, by itself, evidence of a structural defect. The closed-vocabulary fault this function actively detects is a missing dependency: two parts that are adverse on the same cases more often than chance predicts, indicating a possible unmodeled edge. When no such co-anomalous pair is found, the report remains empty/low-severity rather than guessing a fix.