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 correction term 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 satisfies sum(v for _, v in parts) + correction == total to 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_up are 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_up are component indices (e.g. class labels) at the same observed x; 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: object

Exact additive attribution of a log-density (or a decision margin between two hypotheses).

correction is 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 absolute log p(x). sum(v for _, v in parts) + correction equals total exactly, 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.

Parameters:

k (int)

Return type:

list[tuple[str, float]]

ledger_sum()[source]

sum(parts) + correction – should equal total to machine precision; see class docstring.

Return type:

float

is_exact(atol=1e-9)[source]

Return whether ledger parts plus correction reconstruct the total.

Parameters:

atol (float)

Return type:

bool

summary()[source]

Render a human-readable anomaly ledger summary.

Return type:

str

explain(model, x)[source]

Exact per-part attribution of model.log_density(x) (see module docstring).

Parameters:
Return type:

Explanation

explain_margin(model, answer, runner_up)[source]

Exact per-part attribution of the decision margin log p(answer) - log p(runner_up).

answer/runner_up are 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, use explain_margin_mixture() instead (the margin there needs the observed point plus two component indices, not two full records).

Parameters:
Return type:

Explanation

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_up are 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 term explain() must name as a correction for the absolute log p(x) – cancels exactly in this subtraction, so the margin ledger needs no correction (computed and asserted at 0.0, not assumed away).

Parameters:
Return type:

Explanation

class FaultReport(dominant, evidence=<factory>, suggested_fix='', receipt=<factory>)[source]

Bases: object

Structural diagnosis built from explain() ledgers over failing cases.

dominant names the structural element most responsible, when one rises above ordinary case-to-case variability. suggested_fix is one of _FIX_VOCAB or empty. evidence ranks elements by adverse contribution.

Parameters:
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 to cases themselves, 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.

Parameters:
Return type:

FaultReport