Skip to content

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
def __init__(self, ebm: Any, *, lam: float = 1.0) -> None:
    if getattr(ebm, "term_scores_", None) is None:
        raise ValueError(
            "model is not a fitted interpret EBM (no term_scores_); "
            "fit an ExplainableBoostingClassifier first (pip install triadxai[ebm])"
        )
    intercept = np.asarray(ebm.intercept_, dtype=float).ravel()
    if intercept.size != 1:
        raise ValueError("multiclass EBMs are not supported in v0.1; fit a binary classifier")
    terms: list[TermData] = []
    for i, term_features in enumerate(ebm.term_features_):
        sds = ebm.standard_deviations_[i]
        if sds is None:
            raise ValueError(
                f"term {i} has no standard_deviations_ (monotonized model?); "
                "TRIAD needs outer-bag variances"
            )
        terms.append(
            TermData(
                term_idx=i,
                feature_idxs=tuple(term_features),
                f=np.asarray(ebm.term_scores_[i], dtype=float),
                v=np.asarray(sds, dtype=float) ** 2,
                n=np.asarray(ebm.bin_weights_[i], dtype=float),
            )
        )
    if all(np.all(t.v == 0.0) for t in terms):
        logger.warning(
            "all outer-bag SDs are zero (outer_bags=1?); the D channel will be degenerate"
        )
    self.model_data = ModelData(
        terms=tuple(terms),
        intercept=float(intercept[0]),
        feature_names=tuple(ebm.feature_names_in_),
    )
    self._lam = float(lam)
    bounds = np.asarray(ebm.feature_bounds_, dtype=float)
    self._features = tuple(
        self._feature_info(kind, levels, bounds[j], self._main_term_for(terms, j))
        for j, (kind, levels) in enumerate(zip(ebm.feature_types_in_, ebm.bins_, strict=True))
    )

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
def bin(self, X: pd.DataFrame) -> list[BinnedTerm]:
    """Bin new instances into each term's tensor layout."""
    columns = {j: self._clean_column(X, j) for j in range(len(self.model_data.feature_names))}
    out: list[BinnedTerm] = []
    n_rows = len(X)
    for term in self.model_data.terms:
        ndim = len(term.feature_idxs)
        idxs: list[np.ndarray] = []
        missing = np.zeros(n_rows, dtype=bool)
        oov = np.zeros(n_rows, dtype=bool)
        decay = np.ones(n_rows)
        for dim, j in enumerate(term.feature_idxs):
            values, miss = columns[j]
            level = min(len(self._features[j].levels), ndim) - 1
            idx, dim_oov, dim_decay = self._bin_one(j, level, values, miss, term.f.shape[dim])
            idxs.append(idx)
            missing |= miss
            oov |= dim_oov
            decay = decay * dim_decay
        out.append(BinnedTerm(term.term_idx, tuple(idxs), missing, oov, decay))
    return out