Skip to content

Printing and plotting

Summary

A fitted CALM is its own explanation: score = baseline + one curve value per feature, the curve being the one of the region the row falls in. Five methods show it. The ones named print_… print, the ones named plot_… draw.

method answers
print_summary() what is this model: size, features by importance, partitions; with data, where it stands between a GAM and the black box
plot_curves() what does each feature do, and where: one panel per feature, one curve per region
plot_importance() which features carry the model
print_prediction(x) why this number, for one row
plot_prediction(x) the same row as a waterfall

Three more return the numbers behind them: curves(), contributions(X) and importances().

CALMClassifier has the same methods.


Usage

calm.print_summary()                  # the model card
calm.print_summary(X_test, y_test)    # … with GAM / CALM / black box on that data

calm.plot_curves()                    # the most important features
calm.plot_curves("hour")              # one feature, large
calm.plot_importance()

row = X_test.iloc[0]
calm.print_prediction(row)            # the row's ledger
calm.plot_prediction(row)             # … as a waterfall
calm.plot_curves(at=row)              # … marked on the curves

fig, axes = calm.plot_curves(show_plot=False)   # to save or edit

What the views say

Everything is read from the fitted model. Curves, contributions and importances come from the curves fit produced, not from the teacher, and they are exact: for every row,

calm.contributions(X).sum(axis=1) + calm.baseline_   # == the model's score

The score is the prediction for regression. For a classifier it is the log-odds of the positive class, the scale the model is additive on; the ledger's last lines map it to the probability and the class.

A curve is a feature's whole contribution inside its region, centred so that the feature's contributions average zero over the training rows. The baseline is therefore the average score. Curves of the same feature sit at different heights when their regions differ in level.

Curves are drawn where the data is. Each curve covers only the values its region has training rows for, and the strip under a panel shows how many (one row of bars per region). Categorical features are drawn as one mark per level, with the level names on the axis.

Importance is the mean |contribution| of a feature over the training rows, in the units of the score. The bar of a feature with a partition is cut into one segment per region.

Where it stands. print_summary(X, y) scores three models on the data you pass: a GAM (the same curve fitter, with no partitions), this CALM, and the teacher. The GAM is fitted on the training data the first time it is needed and kept as calm.gam_. Pass held-out data: on its own training rows the teacher looks better than it is.

Why the model has this structure

These views describe the model. Why a partition was kept or rejected is the selection ledger, calm.chain_.show(), and what the teacher showed for a feature is calm.effect_.plot("hour"); both are effector objects.

The look

Figures use effector's active theme: effector.set_theme("dark") or "paper" changes their colours. A schema's scale_x_list / scale_y is applied to the views (not to contributions, importances and baseline_, which stay in the model's units).

API

Bases: _CALMBase

CALM for regression.

calm = CALMRegressor(max_partition_depth=2)
calm.fit(X_train, y_train)              # numpy array or pandas DataFrame
calm.predict(X_test)
calm.print_summary(X_test, y_test)      # the model card, and where it stands
calm.plot_curves()                      # one panel per feature, one curve per region
calm.print_prediction(X_test.iloc[0])   # why this prediction
calm.chain_.show()                      # why each partition was kept or rejected

One fit runs the whole pipeline: fit the teacher, read it with a feature-effect method, propose and select the partitions, then fit one curve per (feature, region). Every argument is keyword-only and has a default.

Parameters:

Name Type Description Default
max_conditional_interactions Optional[int]

(K in the paper) the most conditional interactions the model may have — one per (feature, conditioning feature) pair, so x1 | x2 and x2 | x1 are two. A feature's partition is kept whole or not at all, so selection stops before the partition that would exceed the limit. Default 10; None for no limit. Needs selection="r2_gain".

10
max_partitions Optional[int]

the most features that may get a partition; selection stops once it is reached. Default None, no limit. Needs selection="r2_gain".

None
max_partition_depth int

(d_max in the paper) depth of each feature's partition tree, so a feature gets at most 2**max_partition_depth curves and a region's rule has at most that many conditions. Default 2.

2
min_heterogeneity_drop float

(ε in the paper) inside one partition tree, the fraction by which a split must reduce the feature's heterogeneity to be made. Default 0.05. Higher means fewer splits.

0.05
min_r2_gain float

smallest R² gain (fraction of the target's variance, default 0.01 = 1 pt) a feature's partition must add, on top of the partitions already kept, to be kept. Higher means fewer partitions.

0.01
teacher

the black-box model the partitions are read from. A registered name ("xgb", default), an unfitted model or class (fitted here), a fitted model (used as is), or a plain callable X -> scores.

'xgb'
effect Union[str, object]

how the teacher is read to detect interactions — "pdp" (default), "ale", "shap", "rhale", or a fitted effector effect.

'pdp'
partitioner Union[str, object]

what proposes a partition tree per feature — "best" (default), "best_level_wise", or an effector finder object.

'best'
partition_features Union[str, list]

which features may get a partition — "heterogeneous" (default; heterogeneity at or above the median), "all", or a list.

'heterogeneous'
conditioning_features Union[str, list]

which features may appear in the region rules — "all" (default), "numeric", or a list.

'all'
selection str

which proposed partitions are kept — "r2_gain" (default) keeps only those that make an additive surrogate explain the target noticeably better (see min_r2_gain), up to max_conditional_interactions; "all" keeps every one and takes no limit.

'r2_gain'
curve_fitter

what is fitted inside the regions — "ebm" (default; an EBM without interactions, refit on the labels), "surrogate" (no refit: the teacher's regional curves are frozen into a model), or a MaskedFitter.

'ebm'

Other Parameters:

Name Type Description
selection_target str

what the R² of selection is measured against — the teacher's predictions ("teacher", default) or the labels ("y").

min_relative_r2_gain Optional[float]

optional relaxed gate for nearly saturated models — the threshold becomes min(min_r2_gain, min_relative_r2_gain * (1 - R²)).

teacher_params Optional[dict]

keyword arguments for a teacher given by name or class, e.g. {"n_estimators": 1000} for "xgb".

effect_params Optional[dict]

effector's settings for an effect given by name — constructor or fit arguments, e.g. {"nof_instances": 10_000} (CALM's default is all rows) or {"model_jac": f} for "rhale".

partitioner_params Optional[dict]

effector's settings for a partitioner given by name, e.g. {"min_samples_leaf": 50}. The depth and the heterogeneity drop are set by the two arguments above.

curve_fitter_params Optional[dict]

keyword arguments for a fitter given by name, e.g. {"max_bins": 64} for "ebm". The EBM's interactions is always 0.

schema

feature names and types for a numpy X. A DataFrame brings its own.

random_state Optional[int]

one seed, passed to every component built by name. Default 42.

verbose int

0 (default) is silent; 1 shows a progress bar over the stages of fit (teacher, effect, propose, select, curves) with the time each took.

Attributes:

Name Type Description
chain_

the decision ledger (CalmSequence); None when selection="all".

partitions_

{feature index: Partition} for the split features.

model_

the fitted additive model (ExecutableCALM).

teacher_

the fitted teacher.

effect_

the effector effect the partitions were read from.

n_conditional_interactions_

number of (feature, conditioning feature) pairs in the model; x1 | x2 and x2 | x1 are two.

interactions_info_

regions per feature and their conditioning features.

n_features_in_

number of features seen in fit.

feature_names_in_

their names — the DataFrame's columns, the schema's feature_names, or x_0, x_1, ….

baseline_ float

the average score over the training rows; a prediction is this plus the row's contributions.

gam_ float

the GAM reference (the same curve fitter, no partitions) — set by the first print_summary(X, y).

Methods:

Name Description
print_summary

Print the model card: what this model is, on one screen.

plot_curves

Draw the model: one panel per feature, one curve per region.

plot_importance

Draw the features by importance, one bar each.

print_prediction

Print why the model predicts what it does for one row.

plot_prediction

Draw one prediction as a waterfall: from the baseline, one bar per

curves

The fitted curves as arrays, grouped by feature — what plot_curves draws.

contributions

What each feature adds to the baseline, row by row.

importances

Each feature's importance: its mean |contribution| over the

Source code in calm_additive/engine.py
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
def __init__(
    self,
    *,
    # hyperparameters
    max_conditional_interactions: Optional[int] = 10,
    max_partitions: Optional[int] = None,
    max_partition_depth: int = 2,
    min_heterogeneity_drop: float = 0.05,
    min_r2_gain: float = 0.01,
    # choices
    teacher="xgb",
    effect: Union[str, object] = "pdp",
    partitioner: Union[str, object] = "best",
    partition_features: Union[str, list] = "heterogeneous",
    conditioning_features: Union[str, list] = "all",
    selection: str = "r2_gain",
    curve_fitter="ebm",
    # advanced
    selection_target: str = "teacher",
    min_relative_r2_gain: Optional[float] = None,
    teacher_params: Optional[dict] = None,
    effect_params: Optional[dict] = None,
    partitioner_params: Optional[dict] = None,
    curve_fitter_params: Optional[dict] = None,
    schema=None,
    random_state: Optional[int] = 42,
    verbose: int = 0,
):
    self.max_conditional_interactions = max_conditional_interactions
    self.max_partitions = max_partitions
    self.max_partition_depth = max_partition_depth
    self.min_heterogeneity_drop = min_heterogeneity_drop
    self.min_r2_gain = min_r2_gain
    self.teacher = teacher
    self.effect = effect
    self.partitioner = partitioner
    self.partition_features = partition_features
    self.conditioning_features = conditioning_features
    self.selection = selection
    self.curve_fitter = curve_fitter
    self.selection_target = selection_target
    self.min_relative_r2_gain = min_relative_r2_gain
    self.teacher_params = teacher_params
    self.effect_params = effect_params
    self.partitioner_params = partitioner_params
    self.curve_fitter_params = curve_fitter_params
    self.schema = schema
    self.random_state = random_state
    self.verbose = verbose

baseline_ property

The average score over the training rows; every prediction is this plus the row's contributions.

print_summary(X=None, y=None, *, max_features=20)

Print the model card: what this model is, on one screen.

calm.print_summary()                  # size, features by importance, partitions
calm.print_summary(X_test, y_test)    # … and where it stands on that data

The card lists the model's size (curves, partitions, conditional interactions), every feature with its importance and the features its curves depend on, and each partition as a tree. Importance is the mean |contribution| of the feature over the training rows, read from the fitted curves.

With data, a where it stands block scores three models on it: a GAM (the same curve fitter with no partitions), this CALM, and the black-box teacher — and says how much of the GAM → black-box distance CALM covers. The GAM is fitted on the training data the first time it is needed and kept as gam_.

Parameters:

Name Type Description Default
X

(N, D) array or DataFrame to score on, with the columns fit saw; held-out data is the honest choice. Default None: no scores.

None
y

(N,) targets for X.

None
max_features int

rows of the feature table; the rest are summed up.

20
Source code in calm_additive/engine.py
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
def print_summary(self, X=None, y=None, *, max_features: int = 20):
    """Print the model card: what this model is, on one screen.

    ```python
    calm.print_summary()                  # size, features by importance, partitions
    calm.print_summary(X_test, y_test)    # … and where it stands on that data
    ```

    The card lists the model's size (curves, partitions, conditional
    interactions), every feature with its importance and the features
    its curves depend on, and each partition as a tree. Importance is
    the mean |contribution| of the feature over the training rows, read
    from the fitted curves.

    With data, a *where it stands* block scores three models on it: a
    GAM (the same curve fitter with no partitions), this CALM, and the
    black-box teacher — and says how much of the GAM → black-box
    distance CALM covers. The GAM is fitted on the training data the
    first time it is needed and kept as `gam_`.

    Args:
        X: `(N, D)` array or DataFrame to score on, with the columns
            `fit` saw; held-out data is the honest choice. Default
            `None`: no scores.
        y: `(N,)` targets for `X`.
        max_features: rows of the feature table; the rest are summed up.
    """
    from calm_additive.render import text

    if (X is None) != (y is None):
        raise ValueError("pass both X and y to score the model, or neither")
    builder = self._view_builder()
    scores = None if X is None else self._scores(X, y)
    print(text.model_card(builder.model_view(scores), max_features), end="")

plot_curves(features=None, *, at=None, ncols=3, share_y=False, max_features=12, show_plot=True)

Draw the model: one panel per feature, one curve per region.

calm.plot_curves()                      # the most important features
calm.plot_curves("hour")                # one feature, large
calm.plot_curves(at=X_test.iloc[0])     # … with one row marked on its curves

A curve is the feature's contribution to the prediction inside its region (the legend gives the region's rule and its number of training rows) and is drawn only where that region has data; the strip under a panel shows where the rows are. A prediction is the baseline plus one curve value per feature. Categorical features are drawn as one mark per level.

Parameters:

Name Type Description Default
features

a name or index (one large panel), a list of them, or None (default): the max_features most important ones.

None
at

one row (as in print_prediction). Its curve is highlighted in every panel, with a dot at the row's value.

None
ncols int

panels per row.

3
share_y bool

if True, all panels share the y-axis, so a feature that matters little looks flat. Default False.

False
max_features int

how many features features=None draws.

12
show_plot bool

if True, show the figure and return None; else return (fig, axes) — (fig, ax) for a single feature.

True
Source code in calm_additive/engine.py
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
def plot_curves(
    self,
    features=None,
    *,
    at=None,
    ncols: int = 3,
    share_y: bool = False,
    max_features: int = 12,
    show_plot: bool = True,
):
    """Draw the model: one panel per feature, one curve per region.

    ```python
    calm.plot_curves()                      # the most important features
    calm.plot_curves("hour")                # one feature, large
    calm.plot_curves(at=X_test.iloc[0])     # … with one row marked on its curves
    ```

    A curve is the feature's contribution to the prediction inside its
    region (the legend gives the region's rule and its number of
    training rows) and is drawn only where that region has data; the
    strip under a panel shows where the rows are. A prediction is the
    baseline plus one curve value per feature. Categorical features are
    drawn as one mark per level.

    Args:
        features: a name or index (one large panel), a list of them, or
            `None` (default): the `max_features` most important ones.
        at: one row (as in `print_prediction`). Its curve is
            highlighted in every panel, with a dot at the row's value.
        ncols: panels per row.
        share_y: if `True`, all panels share the y-axis, so a feature
            that matters little looks flat. Default `False`.
        max_features: how many features `features=None` draws.
        show_plot: if `True`, show the figure and return `None`; else
            return `(fig, axes)` — `(fig, ax)` for a single feature.
    """
    from calm_additive.render import mpl

    builder = self._view_builder()
    view = builder.model_view()
    single = isinstance(features, (str, int, np.integer))
    if features is None:
        chosen = list(view.order[:max_features])
    else:
        chosen = [
            self.effect_._resolve_feature(f)
            for f in ([features] if single else features)
        ]
    row = None if at is None else builder.prediction_view(self._encode_row(at))
    return mpl.plot_curves(
        view,
        chosen,
        at=row,
        ncols=ncols,
        share_y=share_y,
        single=single,
        show_plot=show_plot,
    )

plot_importance(*, max_features=15, show_plot=True)

Draw the features by importance, one bar each.

Importance is the mean |contribution| of the feature over the training rows, in the units of the prediction (log-odds for a classifier). The bar of a feature with a partition is cut into one segment per region: how much of its importance each region carries.

Parameters:

Name Type Description Default
max_features int

bars drawn, most important first.

15
show_plot bool

if True, show the figure and return None; else return (fig, ax).

True
Source code in calm_additive/engine.py
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
def plot_importance(self, *, max_features: int = 15, show_plot: bool = True):
    """Draw the features by importance, one bar each.

    Importance is the mean |contribution| of the feature over the
    training rows, in the units of the prediction (log-odds for a
    classifier). The bar of a feature with a partition is cut into one
    segment per region: how much of its importance each region carries.

    Args:
        max_features: bars drawn, most important first.
        show_plot: if `True`, show the figure and return `None`; else
            return `(fig, ax)`.
    """
    from calm_additive.render import mpl

    view = self._view_builder().model_view()
    return mpl.plot_importance(view, max_features=max_features, show_plot=show_plot)

print_prediction(x, *, max_features=10)

Print why the model predicts what it does for one row.

calm.print_prediction(X_test.iloc[0])

A ledger with one line per feature: its value, the rule that selected its curve (for a feature with a partition), and what that curve adds to the baseline. The lines add up to the prediction exactly: baseline + Σ contributions. A classifier's lines are in log-odds, the scale it is additive on, and the last line maps their sum to the probability.

Parameters:

Name Type Description Default
x

one row — a (D,) array, a pandas Series, or a one-row array / DataFrame, with the columns fit saw.

required
max_features int

lines shown, largest |contribution| first; the rest are summed in one line.

10
Source code in calm_additive/engine.py
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
def print_prediction(self, x, *, max_features: int = 10):
    """Print why the model predicts what it does for one row.

    ```python
    calm.print_prediction(X_test.iloc[0])
    ```

    A ledger with one line per feature: its value, the rule that
    selected its curve (for a feature with a partition), and what that
    curve adds to the baseline. The lines add up to the prediction
    exactly: `baseline + Σ contributions`. A classifier's lines are in
    log-odds, the scale it is additive on, and the last line maps their
    sum to the probability.

    Args:
        x: one row — a `(D,)` array, a pandas Series, or a one-row
            array / DataFrame, with the columns `fit` saw.
        max_features: lines shown, largest |contribution| first; the
            rest are summed in one line.
    """
    from calm_additive.render import text

    view = self._view_builder().prediction_view(self._encode_row(x))
    print(text.prediction(view, max_features), end="")

plot_prediction(x, *, max_features=10, show_plot=True)

Draw one prediction as a waterfall: from the baseline, one bar per feature, to the prediction.

The figure of print_prediction: the same numbers, largest |contribution| first, each bar labelled with the feature's value and the rule that selected its curve.

Parameters:

Name Type Description Default
x

one row — a (D,) array, a pandas Series, or a one-row array / DataFrame, with the columns fit saw.

required
max_features int

bars drawn; the rest are summed in one bar.

10
show_plot bool

if True, show the figure and return None; else return (fig, ax).

True
Source code in calm_additive/engine.py
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
def plot_prediction(self, x, *, max_features: int = 10, show_plot: bool = True):
    """Draw one prediction as a waterfall: from the baseline, one bar per
    feature, to the prediction.

    The figure of `print_prediction`: the same numbers, largest
    |contribution| first, each bar labelled with the feature's value and
    the rule that selected its curve.

    Args:
        x: one row — a `(D,)` array, a pandas Series, or a one-row
            array / DataFrame, with the columns `fit` saw.
        max_features: bars drawn; the rest are summed in one bar.
        show_plot: if `True`, show the figure and return `None`; else
            return `(fig, ax)`.
    """
    from calm_additive.render import mpl

    view = self._view_builder().prediction_view(self._encode_row(x))
    return mpl.plot_prediction(view, max_features=max_features, show_plot=show_plot)

curves()

The fitted curves as arrays, grouped by feature — what plot_curves draws.

Returns:

Type Description
dict

{feature_name: [{"region", "x", "y", "n"}, ...]}, one entry per region of the feature (a single one, with region "everywhere", when it has no partition). x, y trace the curve over the part of the axis where the region has training rows (a step curve repeats its cut points; a categorical feature has one point per level, x being the level's place on the axis and "levels" their names); n is the region's number of training rows.

Source code in calm_additive/engine.py
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
def curves(self) -> dict:
    """The fitted curves as arrays, grouped by feature — what `plot_curves` draws.

    Returns:
        `{feature_name: [{"region", "x", "y", "n"}, ...]}`, one entry per
            region of the feature (a single one, with region
            `"everywhere"`, when it has no partition). `x`, `y` trace
            the curve over the part of the axis where the region has
            training rows (a step curve repeats its cut points; a
            categorical feature has one point per level, `x` being the
            level's place on the axis and `"levels"` their names); `n` is the
            region's number of training rows.
    """
    out = {}
    for f in self._view_builder().model_view().features:
        entries = []
        for r in f.regions:
            entry = {"region": r.label, "x": r.x, "y": r.y, "n": r.n}
            if f.is_categorical:
                at = np.searchsorted(f.level_positions, r.x)
                entry["levels"] = [f.level_names[i] for i in at]
            entries.append(entry)
        out[f.name] = entries
    return out

contributions(X)

What each feature adds to the baseline, row by row.

c = calm.contributions(X_test)
c.sum(axis=1) + calm.baseline_      # the model's score for every row

The score is the prediction for regression and the log-odds of the positive class for a classifier (curve_fitter="ebm").

Parameters:

Name Type Description Default
X

(N, D) array or DataFrame, with the columns fit saw.

required

Returns:

Type Description
ndarray

(N, D) array, one column per feature in feature_names_in_.

Source code in calm_additive/engine.py
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
def contributions(self, X) -> np.ndarray:
    """What each feature adds to the baseline, row by row.

    ```python
    c = calm.contributions(X_test)
    c.sum(axis=1) + calm.baseline_      # the model's score for every row
    ```

    The score is the prediction for regression and the log-odds of the
    positive class for a classifier (`curve_fitter="ebm"`).

    Args:
        X: `(N, D)` array or DataFrame, with the columns `fit` saw.

    Returns:
        `(N, D)` array, one column per feature in `feature_names_in_`.
    """
    return self._view_builder().contributions(self._encode(X))

importances()

Each feature's importance: its mean |contribution| over the training rows, read from the fitted curves.

Returns:

Type Description
ndarray

(D,) array, one value per feature in feature_names_in_.

Source code in calm_additive/engine.py
1185
1186
1187
1188
1189
1190
1191
1192
def importances(self) -> np.ndarray:
    """Each feature's importance: its mean |contribution| over the
    training rows, read from the fitted curves.

    Returns:
        `(D,)` array, one value per feature in `feature_names_in_`.
    """
    return self._view_builder().importances()