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
|