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).Both hypotheses must be exact, distinct, in-range integer component indices: a margin of a hypothesis against itself is 0 by construction and is a caller error, not evidence, so it is rejected rather than reported as a confident tie.
- Raises:
TypeError – if either hypothesis is not an exact non-Boolean integer.
IndexError – if either hypothesis is outside
range(len(model.components)).ValueError – if both hypotheses name the same component.
- Parameters:
- Return type:
Explanation
- 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.
min_zis a robust z-score cutoff (finite,>= 0) andco_occurrence_thresholdis a rate in[0, 1]; out-of-range values are rejected rather than silently turning both comparisons into tautologies that prescribe anadd_edgebacked by no co-anomalous case at all.