API reference¶
Explainer¶
TriadExplainer / TriadExplanation — the main entry point and its result.
Mode-dispatching TRIAD explainer (spec sections 7.1 and 11.1).
TriadExplainer accepts a fit_lgbm_gam result, a raw LightGBM booster,
or a fitted binary EBM, resolves exact vs approximate (SHAP) mode, and
produces TriadExplanation objects: a long channels frame, epistemic
scores (ES, ES_signed, KR), reason codes and waterfall plots.
TriadExplainer ¶
TriadExplainer(model: Any, *, mode: str | None = None, tau_method: str = 'mom', pair_split: str = 'half', lam: float = 1.0, X_ref: DataFrame | None = None, bag_boosters: tuple[Any, ...] | list[Any] | None = None)
Decompose model predictions into I/D/M channels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
Any
|
|
required |
mode
|
str | None
|
|
None
|
X_ref
|
DataFrame | None
|
Reference population: training data for bin counts (exact mode on a raw booster) and for the SHAP-mode tau^2 estimate. |
None
|
bag_boosters
|
tuple[Any, ...] | list[Any] | None
|
Bootstrap replicas providing epistemic variance in SHAP mode. |
None
|
Source code in src/triadxai/local.py
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | |
explain ¶
explain(X: DataFrame) -> TriadExplanation
Decompose the model score for each row of X.
Source code in src/triadxai/local.py
201 202 203 204 205 | |
TriadExplanation
dataclass
¶
TriadExplanation(channels: DataFrame, term_channels: DataFrame | None, score: ndarray, intercept: float, mode: str, approximate: bool)
Channel decomposition for a batch of instances.
epistemic_score ¶
epistemic_score() -> pd.DataFrame
Per-instance ES, ES_signed and knowledge ratio KR (spec 7.1).
Source code in src/triadxai/local.py
40 41 42 43 44 45 46 47 48 49 50 51 52 53 | |
plot_waterfall ¶
plot_waterfall(i: int = 0, **kwargs: Any) -> Any
Three-channel waterfall for one instance (needs triadxai[viz]).
Source code in src/triadxai/local.py
66 67 68 69 70 | |
reasons ¶
reasons(*, orientation: str = 'higher_is_better', k: int = 4, **kwargs: Any) -> list[list[Any]]
Ranked, group-tagged adverse-action reasons per instance (spec 8).
Source code in src/triadxai/local.py
55 56 57 58 59 60 61 62 63 64 | |
Contracts¶
Adapter-agnostic data contracts (TermData, BinnedTerm, ShapData).
Adapter-agnostic data contracts consumed by the TRIAD core.
Exact mode: TermData (per-term bin tensors: missing bin at index 0,
unknown bin at index -1 along each axis) plus BinnedTerm (per-instance
bin lookups). Approximate mode: ShapData (dense per-instance attributions
from the deployed model plus bag-replica variance).
BinnedTerm
dataclass
¶
BinnedTerm(term_idx: int, bin_idx: tuple[ndarray, ...], missing: ndarray, oov: ndarray, decay: ndarray)
Per-instance bin lookups for one term at explanation time.
ModelData
dataclass
¶
ModelData(terms: tuple[TermData, ...], intercept: float, feature_names: tuple[str, ...])
Everything the exact-mode core needs from a fitted additive model.
ShapData
dataclass
¶
ShapData(phi: ndarray, base: float, v: ndarray, missing: ndarray, oov: ndarray, feature_names: tuple[str, ...])
Approximate-mode contract: deployed-model attributions + replica variance.
phi comes from the deployed model (e.g. LightGBM pred_contrib,
base column excluded); v is the per-instance, per-feature variance of
attributions across bag replicas (ddof=1). C1/C2 hold on phi; v
only enters through the support weight.
TermData
dataclass
¶
TermData(term_idx: int, feature_idxs: tuple[int, ...], f: ndarray, v: ndarray, n: ndarray)
Fitted bin-level data for one additive term.
Tensors f (centered bin scores), v (per-bin sampling variance)
and n (per-bin training mass) share one shape; along each axis,
index 0 is the missing bin and index -1 the unknown bin.
Shrinkage¶
Empirical-Bayes τ² estimation (mom / mle / SHAP-mode).
Empirical-Bayes shrinkage strength estimation (spec section 4).
The support weight w = tau^2 / (tau^2 + v) is the posterior weight an empirical-Bayes observer puts on a fitted bin value versus the population prior 0 (terms are centered). tau^2 is the between-bin signal variance, estimated per term; v is the bin's sampling variance.
Shrinkage
dataclass
¶
Shrinkage(tau2: float, sigma2: float, k: float)
Per-term shrinkage summary (audit payload, spec section 4.4).
estimate_tau2 ¶
estimate_tau2(f: ndarray, v: ndarray, n: ndarray, *, method: str = 'mom') -> float
Estimate the between-bin signal variance tau^2 for one term.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
f
|
ndarray
|
Bin values, bin sampling variances, bin training masses (any shape, flattened internally). |
required |
v
|
ndarray
|
Bin values, bin sampling variances, bin training masses (any shape, flattened internally). |
required |
n
|
ndarray
|
Bin values, bin sampling variances, bin training masses (any shape, flattened internally). |
required |
method
|
str
|
|
'mom'
|
Source code in src/triadxai/shrinkage.py
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | |
estimate_tau2_shap ¶
estimate_tau2_shap(phi_ref: ndarray, v_ref: ndarray) -> np.ndarray
Per-feature tau^2 for approximate mode, from a reference population.
Method of moments about the zero prior:
tau2_j = max(0, mean(phi_j^2) - mean(v_j)). SHAP attributions are
centered on the training population, so a strongly nonzero mean signals
a reference-population mismatch and is logged as a warning.
Source code in src/triadxai/shrinkage.py
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | |
fit_shrinkage ¶
fit_shrinkage(term: TermData, *, method: str = 'mom') -> Shrinkage
Estimate tau^2 and the count-form shrinkage strength k for one term.
Source code in src/triadxai/shrinkage.py
74 75 76 77 78 79 80 81 82 83 84 85 | |
Decompose¶
Exact-mode I/D/M channel math.
Exact-mode channel decomposition (spec sections 3 and 6).
Each term's exact contribution f(x) is reallocated into I (supported), D (under-supported residual) and M (missing-bin routing). D is computed as the residual f - w*f so completeness (C1) holds to float roundoff.
TermChannels
dataclass
¶
TermChannels(term_idx: int, feature_idxs: tuple[int, ...], f: ndarray, w: ndarray, I: ndarray, D: ndarray, M: ndarray)
Per-instance channel values for one term.
aggregate_features ¶
aggregate_features(channels: Sequence[TermChannels], binned: Sequence[BinnedTerm], n_features: int, *, pair_split: str = 'half') -> dict[str, np.ndarray]
Aggregate term-level channels to (n_samples, n_features) matrices.
Pair terms split their channels equally between the two participating
features (spec 6, pair_split="half"). The oov matrix is the OR
over all terms a feature participates in.
Source code in src/triadxai/decompose.py
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | |
decompose_term ¶
decompose_term(term: TermData, binned: BinnedTerm, tau2: float) -> TermChannels
Reallocate one term's exact contributions into I/D/M channels.
Source code in src/triadxai/decompose.py
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | |
support_weights ¶
support_weights(term: TermData, binned: BinnedTerm, tau2: float) -> np.ndarray
Effective support weight w per instance (variance form, spec 3.1).
Empty/unseen bins (zero training mass) get w = 0 regardless of their nominal variance; out-of-range instances are decayed (spec 3.4).
Source code in src/triadxai/decompose.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | |
Decompose (SHAP)¶
Approximate-mode reallocation of TreeSHAP attributions.
Approximate-mode channel decomposition (approximate TRIAD, spec section 12).
Per-feature SHAP attributions phi from the deployed model are reallocated into I/D/M with the same routing as exact mode: the support weight uses the per-instance variance of phi across bag replicas instead of per-bin sampling variance. D is the residual phi - I - M, so C1 holds exactly.
ShapChannels
dataclass
¶
ShapChannels(I: ndarray, D: ndarray, M: ndarray, w: ndarray)
Dense (n_samples, n_features) channel matrices.
decompose_shap ¶
decompose_shap(data: ShapData, tau2: ndarray) -> ShapChannels
Reallocate deployed-model attributions into I/D/M channels.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
ShapData
|
Approximate-mode contract (attributions, replica variance, masks). |
required |
tau2
|
ndarray
|
Per-feature signal variance, shape (n_features,), from
:func: |
required |
Source code in src/triadxai/decompose_shap.py
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | |
Bagging¶
Bootstrap replicas as the epistemic-variance source.
Generic bootstrap bagging — the default epistemic-variance source.
No mainstream GBM exposes usable per-feature epistemic variance natively (see docs/DECISIONS.md), so TRIAD refits K bootstrap replicas and reads variance from their disagreement: per-bin shape variance in exact mode, per-instance attribution variance in approximate mode.
fit_bagged ¶
fit_bagged(fit_fn: Callable[[DataFrame, ndarray, int], ModelT], X: DataFrame, y: ndarray, *, n_bags: int = 8, seed: int = 0) -> list[ModelT]
Fit n_bags replicas of a model on bootstrap resamples of (X, y).
fit_fn(X_boot, y_boot, bag_seed) receives a distinct seed per bag.
Bootstraps that collapse to a single class (binary targets with rare
positives) are redrawn.
Source code in src/triadxai/bagging.py
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | |
phi_variance ¶
phi_variance(predict_contrib_fns: Sequence[Callable[[ndarray], ndarray]], X: ndarray) -> np.ndarray
Per-instance, per-feature variance of attributions across replicas.
Each callable maps X to an (n_samples, n_features) attribution matrix; the result is the elementwise sample variance (ddof=1) across replicas.
Source code in src/triadxai/bagging.py
65 66 67 68 69 70 71 72 73 74 75 76 77 | |
LightGBM¶
LightGBM adapter (both modes) + fit_lgbm_gam.
LightGBM adapter: exact GAM mode and approximate SHAP mode.
Exact mode requires an additive-constrained booster (every tree splits on at
most one feature); shape functions are reconstructed from dump_model()
and reconcile with predict(raw_score=True) exactly. Approximate mode uses
LightGBM's native TreeSHAP (pred_contrib=True) on any booster.
Per-bin / per-instance variances come from bagged replicas.
Boosters trained with init_score or zero_as_missing are not supported.
LGBMGamAdapter ¶
LGBMGamAdapter(result: LGBMGamResult, *, lam: float = 1.0)
Exact-mode extraction from an additive-constrained LightGBM booster.
Source code in src/triadxai/lgbm.py
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | |
bin ¶
bin(X: DataFrame) -> list[BinnedTerm]
Bin new instances into each term's TermData layout.
Source code in src/triadxai/lgbm.py
292 293 294 295 296 297 298 299 300 301 302 | |
LGBMGamResult
dataclass
¶
LGBMGamResult(booster: Booster, bag_boosters: tuple[Booster, ...], X_train: DataFrame)
A fitted additive-constrained booster plus its bag replicas.
fit_lgbm_gam ¶
fit_lgbm_gam(X: DataFrame, y: ndarray, *, n_bags: int = 8, seed: int = 0, **lgbm_params: Any) -> LGBMGamResult
Fit an additive-constrained (GAM-mode) LightGBM classifier with bags.
Every tree is restricted to a single feature via interaction constraints, so the booster is an additive model and exact-mode TRIAD applies.
Source code in src/triadxai/lgbm.py
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | |
is_additive ¶
is_additive(booster: Any) -> bool
True iff every tree in the booster splits on at most one feature.
Source code in src/triadxai/lgbm.py
111 112 113 114 115 116 117 118 119 | |
lgbm_shap_data ¶
lgbm_shap_data(booster: Any, X: DataFrame, *, bag_boosters: tuple[Any, ...] | list[Any]) -> ShapData
Approximate-mode contract from any LightGBM booster via native TreeSHAP.
Source code in src/triadxai/lgbm.py
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 | |
EBM¶
InterpretML EBM adapter (triadxai[ebm]).
InterpretML EBM adapter (optional extra: pip install triadxai[ebm]).
Reads fitted attributes only, so this module never imports interpret;
the extra is needed to fit EBMs. Verified against interpret 0.6.16:
term_scores_ tensors carry the missing bin at index 0 and the unknown
bin at index -1 per axis; standard_deviations_ (outer-bag SDs) and
bin_weights_ are aligned 1:1 with them; manual binning via
searchsorted(cuts, x, side="right") + 1 reproduces eval_terms.
EBMAdapter ¶
EBMAdapter(ebm: Any, *, lam: float = 1.0)
Exact-mode extraction from a fitted binary EBM classifier/regressor.
Source code in src/triadxai/ebm.py
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | |
bin ¶
bin(X: DataFrame) -> list[BinnedTerm]
Bin new instances into each term's tensor layout.
Source code in src/triadxai/ebm.py
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | |
Reasons¶
Reg-B-style reason-code mapping.
Reason-code mapping layer (spec section 8).
Deterministic post-processor from a local decomposition to ranked adverse-action reasons: Group A (derogatory information, driven by the I channel) versus Group B (insufficient information, driven by D + M). The mapping table ships configurable; legally operative wording stays with the lender's compliance function.
ReasonCode
dataclass
¶
ReasonCode(feature: str, group: str, subtype: str, contribution: float, text: str)
One ranked adverse-action reason.
map_reasons ¶
map_reasons(instance_channels: DataFrame, *, dictionary: Mapping[str, str] | None = None, orientation: str = 'higher_is_better', k: int = 4, theta_abs: float = 0.05, theta_rel: float = 0.5) -> list[ReasonCode]
Map one instance's channels to ranked, group-tagged reason codes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instance_channels
|
DataFrame
|
Frame with columns |
required |
dictionary
|
Mapping[str, str] | None
|
Optional feature -> approved reason text mapping; features not in the dictionary fall back to subtype templates. |
None
|
orientation
|
str
|
|
'higher_is_better'
|
k
|
int
|
Maximum number of reasons emitted. |
4
|
theta_abs
|
float
|
Group B is emitted only when |
0.05
|
theta_rel
|
float
|
Group B is emitted only when |
0.05
|
Source code in src/triadxai/reasons.py
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | |
Waterfall¶
Three-channel waterfall plot (triadxai[viz]).
Three-channel waterfall plot (spec sections 7.1 and 9.3).
I segments are solid, D segments hatched and translucent (the direction of
an under-supported contribution is the model's guess), M segments gray.
Requires the viz extra (matplotlib).
plot_waterfall ¶
plot_waterfall(explanation: Any, i: int = 0, *, max_features: int = 10, ax: Any = None) -> Any
Plot one instance's I/D/M decomposition as a stacked waterfall.
Source code in src/triadxai/waterfall.py
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | |
Synthetic¶
Validation DGPs.
Synthetic data-generating processes for validation studies (spec 10.1).
All generators share an additive logistic base DGP with known shape functions; each study carves a controllable truth into it (density gap, missingness mechanism, pure-noise feature, interaction term).
SyntheticData
dataclass
¶
SyntheticData(X: DataFrame, y: ndarray, eta: ndarray, meta: Mapping[str, Any])
Generated design matrix, labels, true logit and DGP metadata.
make_additive ¶
make_additive(n: int = 5000, *, seed: int = 0) -> SyntheticData
Additive logistic base DGP with three known shape functions.
Source code in src/triadxai/synthetic.py
55 56 57 58 59 60 61 62 63 64 65 | |
make_density_gap ¶
make_density_gap(n: int = 5000, *, gap: tuple[float, float] = (1.0, 2.5), keep_frac: float = 0.02, seed: int = 0) -> SyntheticData
Base DGP with training mass carved out of a region of x0 (study 10.1.1).
Source code in src/triadxai/synthetic.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | |
make_interacting ¶
make_interacting(n: int = 5000, *, gap: tuple[float, float] = (1.0, 2.5), seed: int = 0) -> SyntheticData
Non-additive DGP (x0*x1 interaction) with a density gap in x0.
Ground truth for the approximate-mode study: an unconstrained booster must model the interaction, and bag disagreement should concentrate the D channel inside the carved gap.
Source code in src/triadxai/synthetic.py
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | |
make_missingness ¶
make_missingness(n: int = 5000, *, mechanism: str = 'MNAR', missing_rate: float = 0.3, missing_effect: float = 1.0, seed: int = 0) -> SyntheticData
Base DGP with MCAR / MAR / MNAR missingness injected into x0 (study 10.1.2).
MNAR plants missing_effect on the logit of masked rows (informative
missingness); meta["eta_base"] retains the pre-effect logit so tests
can recover the planted effect exactly.
Source code in src/triadxai/synthetic.py
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | |
make_noise_feature ¶
make_noise_feature(n: int = 5000, *, seed: int = 0) -> SyntheticData
Base DGP plus a pure-noise feature with zero coefficient (study 10.1.3).
Source code in src/triadxai/synthetic.py
144 145 146 147 148 149 150 151 152 153 154 155 | |
Metrics¶
Reconciliation / stability metrics.
Validation metrics: reconciliation, channel mass, bootstrap stability.
bootstrap_stability ¶
bootstrap_stability(fit_fn: Callable[[DataFrame, ndarray, int], Any], X: DataFrame, y: ndarray, X_eval: DataFrame, *, n_boot: int = 10, seed: int = 0) -> pd.DataFrame
Across-refit SD of per-feature mean channel values (H3 protocol, spec 10.2).
fit_fn(X_boot, y_boot, boot_seed) must return a model consumable by
:class:triadxai.local.TriadExplainer.
Source code in src/triadxai/metrics.py
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | |
channel_mass ¶
channel_mass(explanation: TriadExplanation) -> pd.Series
Portfolio share of mean |I|, |D|, |M| (sums to 1).
Source code in src/triadxai/metrics.py
33 34 35 36 37 38 39 | |
reconciliation_error ¶
reconciliation_error(explanation: TriadExplanation, model: Any, X: DataFrame) -> float
Max |sum of channels + intercept - model raw score| (C2; 0 by construction).
Source code in src/triadxai/metrics.py
27 28 29 30 | |