Skip to content

local

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

LGBMGamResult (exact mode), a raw LightGBM booster (exact if additive and X_ref given, else SHAP mode with bag_boosters), or a fitted binary EBM (exact mode).

required
mode str | None

"exact", "shap" or None (inferred).

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
def __init__(
    self,
    model: Any,
    *,
    mode: str | None = None,
    tau_method: str = "mom",
    pair_split: str = "half",
    lam: float = 1.0,
    X_ref: pd.DataFrame | None = None,
    bag_boosters: tuple[Any, ...] | list[Any] | None = None,
) -> None:
    if mode not in (None, "exact", "shap"):
        raise ValueError(f"unknown mode {mode!r}; expected 'exact', 'shap' or None")
    self._pair_split = pair_split
    if isinstance(model, LGBMGamResult):
        if (mode or "exact") == "exact":
            self._init_exact_lgbm(model, tau_method, lam)
        else:
            self._init_shap(
                model.booster,
                model.bag_boosters,
                model.X_train if X_ref is None else X_ref,
            )
    elif getattr(model, "term_scores_", None) is not None:
        if mode == "shap":
            raise ValueError("EBM models support exact mode only")
        self._init_exact_ebm(model, tau_method, lam)
    elif hasattr(model, "dump_model"):
        self._init_raw_booster(model, mode, tau_method, lam, X_ref, bag_boosters)
    else:
        raise ValueError(
            "unsupported model type: expected LGBMGamResult, a LightGBM Booster "
            "or a fitted EBM"
        )

shrinkage property

shrinkage: DataFrame

Audit frame of shrinkage parameters (spec 4.4).

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
def explain(self, X: pd.DataFrame) -> TriadExplanation:
    """Decompose the model score for each row of X."""
    if self.mode == "exact":
        return self._explain_exact(X)
    return self._explain_shap(X)

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
def epistemic_score(self) -> pd.DataFrame:
    """Per-instance ES, ES_signed and knowledge ratio KR (spec 7.1)."""
    tmp = self.channels.assign(
        abs_i=self.channels["I"].abs(),
        abs_dm=self.channels["D"].abs() + self.channels["M"].abs(),
        dm=self.channels["D"] + self.channels["M"],
    )
    grouped = tmp.groupby("instance")[["abs_i", "abs_dm", "dm"]].sum()
    es = grouped["abs_dm"]
    denom = grouped["abs_i"] + es
    kr = (grouped["abs_i"] / denom.where(denom > 0)).rename("KR")
    return pd.DataFrame({"ES": es, "ES_signed": grouped["dm"], "KR": kr}).rename_axis(
        "instance"
    )

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
def plot_waterfall(self, i: int = 0, **kwargs: Any) -> Any:
    """Three-channel waterfall for one instance (needs triadxai[viz])."""
    from .waterfall import plot_waterfall

    return plot_waterfall(self, i=i, **kwargs)

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
def reasons(
    self, *, orientation: str = "higher_is_better", k: int = 4, **kwargs: Any
) -> list[list[Any]]:
    """Ranked, group-tagged adverse-action reasons per instance (spec 8)."""
    from .reasons import map_reasons

    return [
        map_reasons(group[["feature", "I", "D", "M"]], orientation=orientation, k=k, **kwargs)
        for _, group in self.channels.groupby("instance")
    ]