Skip to content

How TRIAD decomposes a score

TRIAD's promise is that the decomposition you read is the score the model produced. Every feature's contribution is split into three signed channels — Information, Density/epistemic, Missingness — that sum back exactly, and every explain call re-checks that the channel totals plus the intercept reproduce the model's raw score before anything is returned.

This article walks the whole pipeline for one batch of instances: how a fitted model becomes bin tensors, how empirical-Bayes shrinkage turns bag disagreement into a support weight, how the three channels are computed, and what the epistemic scores and reason codes summarize. Each stage links to a concept page that covers it in depth; design choices cite the design decisions record.

Running example

A thin-file credit applicant: the portfolio has informative (MNAR) missingness — 30% of x0 is masked, with the masking probability rising with x0 itself, and masked rows carry a planted +1.0 on their default logit. A lender must separate derogatory evidence (a genuinely bad value) from absent evidence (nothing on file). This is the DGP of the synthetic credit walkthrough (make_missingness(mechanism="MNAR", missing_effect=1.0)).

The pipeline, end to end — every stage below gets its own section:

flowchart TD
    subgraph setup ["Set up (once per TriadExplainer)"]
        MODEL["fit_lgbm_gam result /<br/>raw booster / fitted EBM"] -->|"resolve mode"| ADAPT["adapter<br/>exact: TermData bin tensors (f, v, n)<br/>approximate: TreeSHAP phi + replica variance"]
        BAGS["bag replicas"] -->|"disagreement"| ADAPT
        ADAPT --> TAU["empirical-Bayes tau-squared<br/>per term (mom / mle)"]
    end
    subgraph solve ["Explain (per batch)"]
        X["instances X"] --> BIN["bin lookup: f(x), v(x)<br/>missing bin / unknown slot routing"]
        BIN --> W["support weight w = tau2 / (tau2 + v)<br/>x out-of-range decay (exact mode)"]
        TAU --> W
        W --> CH["I = w·f on observed rows<br/>M = w·f on missing rows<br/>D = f − w·f (residual)"]
        CH --> GUARD{"C2 guard:<br/>channel totals + intercept<br/>= raw score?"}
    end
    GUARD -->|"yes"| OUT["TriadExplanation<br/>channels / ES, KR / reasons / waterfall"]
    GUARD -->|"no"| ERR["RuntimeError"]

The identity, stated precisely

For each feature \(j\) of instance \(x\), TRIAD produces three signed numbers with

\[ I_j + D_j + M_j \;=\; f_j(x_j), \]

where \(f_j(x_j)\) is the feature's exact contribution to the raw score (C1 — completeness), and across features

\[ \sum_j \bigl(I_j + D_j + M_j\bigr) + \beta_0 \;=\; s(x) \]

(C2 — score reconciliation). The channels are a reallocation of contributions the model already made, not a new attribution method: exact mode reallocates the term contributions of an additive model, approximate mode reallocates the model's own TreeSHAP attributions. Channels and guarantees lists the full invariant suite the test battery asserts over both modes.

One model language: bin tensors

Exact mode never touches the model object during decomposition. An adapter first extracts each additive term into a TermData contract: a tensor f of centered per-bin scores, a tensor v of per-bin sampling variances, and a tensor n of per-bin training mass — with the missing bin at index 0 and the unknown bin at index −1 along each axis.

  • For an additive-constrained LightGBM booster, shape functions are reconstructed from dump_model() — bins are the intervals between the feature's split thresholds — and reconcile with predict(raw_score=True) exactly (model adapters).
  • For an InterpretML EBM, term_scores_ tensors already carry this layout; outer-bag standard deviations become v and bin_weights_ become n.

Terms are centered: each term's training-mass-weighted mean is folded into the intercept, so a term's value is its deviation from the population average.

At explain time each instance is looked up in this layout: a missing value routes to the missing bin, an unseen categorical level to the unknown slot, and a numeric value to its threshold interval.

The support weight

The heart of TRIAD is a per-bin support weight

\[ w \;=\; \frac{\tau^2}{\tau^2 + v}, \]

the posterior weight an empirical-Bayes observer puts on the fitted bin value versus the population prior of 0 (shrinkage and support weights). Here \(v\) is the bin's sampling variance and \(\tau^2\) the between-bin signal variance of the term, estimated once per term by a mass-weighted method of moments (\(\max(0,\ \sum_b \pi_b f_b^2 - \sum_b \pi_b v_b)\)) or, optionally, 1-D marginal maximum likelihood. Two adjustments apply:

  • bins with zero training mass get \(w = 0\) regardless of their nominal variance;
  • values outside the training range are decayed by \(\exp(-\lambda \cdot \text{dist}/\text{IQR})\) (exact mode only — see decision 3), and flagged oov either way.

A term whose \(\hat\tau^2\) is exactly 0 is a pure-noise feature: its entire contribution is routed to D, with a warning.

Where does \(v\) come from? No mainstream GBM exposes usable per-feature epistemic variance natively (decision 2), so TRIAD refits K bootstrap replicas (default n_bags=8) and reads variance from their disagreement — per-bin shape variance in exact mode, per-instance attribution variance in approximate mode. EBMs are the exception: their outer-bag SDs are used directly. See epistemic variance.

The three channels

With \(f\) and \(w\) in hand per (instance, term), the channels are one line of algebra:

\[ I = \begin{cases} w f & \text{observed} \\ 0 & \text{missing} \end{cases} \qquad M = \begin{cases} 0 & \text{observed} \\ w f & \text{missing} \end{cases} \qquad D = f - w f . \]

D is deliberately computed as the residual \(f - w f\), never as \((1-w)f\), so completeness holds to float roundoff (decision 11); the test suite asserts term-level C1 at atol=1e-12. The M channel is the supported part of a contribution that arrived through missing-value handling — for the thin-file applicant, the planted +1.0 MNAR effect surfaces as positive M on x0.

Term channels are then aggregated to features. Single-feature terms map 1:1; a pair term (EBM interactions) splits its channels equally between its two features (pair_split="half"), which is why feature-level "observed ⇒ M = 0" weakens to term level for pair-bearing models (decision 14).

Approximate mode: the same routing on TreeSHAP

An unconstrained booster has no per-feature term contributions, so exact mode does not apply. Approximate mode keeps the identical channel algebra but starts from the booster's native TreeSHAP attributions \(\phi_j(x)\) (pred_contrib=True, no shap package): the support weight uses the per-instance variance of \(\phi_j\) across bag replicas, \(\tau^2\) is estimated per feature on a reference population, and D is the residual \(\phi - I - M\), so C1 holds exactly.

The label "approximate" is earned: \(\phi_j\) is not a function of \(x_j\) alone — interaction effects leak into per-feature attributions — and there are no per-bin audit tables (decision 5). No out-of-range decay is applied: in sparse regions the replicas disagree on their own, so bag disagreement subsumes what exact mode approximates with the decay heuristic (decision 3). On an additive booster the two modes coincide — LightGBM's pred_contrib equals the centered shape value to ~1e-14, an invariant the test suite exploits (decision 12). See approximate mode.

From channels to decisions

Per instance, the channels frame is summarized by three epistemic scores:

\[ \mathrm{ES} = \sum_j \bigl(|D_j| + |M_j|\bigr), \qquad \mathrm{ES}_{\text{signed}} = \sum_j \bigl(D_j + M_j\bigr), \qquad \mathrm{KR} = \frac{\sum_j |I_j|}{\sum_j |I_j| + \mathrm{ES}} . \]

ES measures how much of the score rests on weak support, ES_signed its net direction, and the knowledge ratio KR how evidence-based the score is overall — the thin-file applicant shows low KR.

reasons() then maps the channels to ranked adverse-action reasons: Group A (derogatory information, driven by I) versus Group B (insufficient information, driven by D + M, with a missing-vs-density subtype), with noise-suppression thresholds so tiny epistemic residue never becomes a customer-facing reason. The three-channel waterfall renders the same decomposition per instance — I solid, D hatched, M gray. See reason codes and waterfalls.

Nothing ships unreconciled

Every explain call recomputes the model's raw score and compares it against the channel totals plus the intercept; a discrepancy beyond 1e-6 raises RuntimeError("TRIAD reconciliation failed: ...") rather than returning a silently wrong decomposition. Externally, reconciliation_error verifies the same identity, and channel_mass reports the portfolio share of mean |I|, |D| and |M|.

Where to go next