Skip to content

Commit 0284683

Browse files
10.13.0
1 parent 8c75abf commit 0284683

12 files changed

+200
-128
lines changed

API_REFERENCE_FOR_REGRESSION.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,25 @@ Used for two-way or higher-order interactions. Specifies the number of evenly sp
373373
#### value
374374
A float representing the new intercept.
375375

376+
## Method: plot_affiliation_shape(affiliation:str, plot:bool = True, save:bool = False, path:str = "")
377+
378+
***Plots or saves the shape of a given unique term affiliation. For main effects, it produces a line plot. For two-way interactions, it produces a heatmap. Plotting for higher-order interactions is not supported. This method provides a convenient way to visualize model components.***
379+
380+
### Parameters
381+
382+
#### affiliation
383+
A string specifying which unique_term_affiliation to use.
384+
385+
#### plot (default = True)
386+
If True, displays the plot.
387+
388+
#### save (default = False)
389+
If True, saves the plot to a file.
390+
391+
#### path (default = "")
392+
The file path to save the plot. If empty and save is True, a default path will be used, for example "shape_of_my_predictor.png".
393+
394+
376395
## Method: remove_provided_custom_functions()
377396

378397
***Removes any custom functions provided for calculating the loss, negative gradient, or validation error. This is useful after model training with custom functions, ensuring that the APLRRegressor object no longer depends on these functions—so they do not need to be present in the Python environment when loading a saved model.***

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ To install APLR, use the following command:
1313
pip install aplr
1414
```
1515

16+
To include dependencies for plotting, use this command instead:
17+
18+
```bash
19+
pip install aplr[plots]
20+
```
21+
1622
## Availability
1723
APLR is available for Windows, most Linux distributions, and macOS.
1824

aplr/aplr.py

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,108 @@ def get_cv_error(self) -> float:
315315
def set_intercept(self, value: float):
316316
self.APLRRegressor.set_intercept(value)
317317

318+
def plot_affiliation_shape(
319+
self,
320+
affiliation: str,
321+
plot: bool = True,
322+
save: bool = False,
323+
path: str = "",
324+
):
325+
"""
326+
Plots or saves the shape of a given unique term affiliation.
327+
328+
For main effects, it produces a line plot. For two-way interactions, it produces a heatmap.
329+
Plotting for higher-order interactions is not supported.
330+
331+
:param affiliation: A string specifying which unique_term_affiliation to use.
332+
:param plot: If True, displays the plot.
333+
:param save: If True, saves the plot to a file.
334+
:param path: The file path to save the plot. If empty and save is True, a default path will be used.
335+
"""
336+
try:
337+
import pandas as pd
338+
import matplotlib.pyplot as plt
339+
except ImportError:
340+
raise ImportError(
341+
"pandas and matplotlib are required for plotting. Please install them."
342+
)
343+
344+
all_affiliations = self.get_unique_term_affiliations()
345+
if affiliation not in all_affiliations:
346+
raise ValueError(
347+
f"Affiliation '{affiliation}' not found in model. "
348+
f"Available affiliations are: {all_affiliations}"
349+
)
350+
351+
affiliation_index = all_affiliations.index(affiliation)
352+
353+
predictors_in_each_affiliation = (
354+
self.get_base_predictors_in_each_unique_term_affiliation()
355+
)
356+
predictor_indexes_used = predictors_in_each_affiliation[affiliation_index]
357+
358+
shape = self.get_unique_term_affiliation_shape(affiliation)
359+
if shape.shape[0] == 0:
360+
print(f"No shape data available for affiliation '{affiliation}'.")
361+
return
362+
363+
predictor_names = affiliation.split(" & ")
364+
365+
shape_df = pd.DataFrame(shape, columns=predictor_names + ["contribution"])
366+
367+
is_main_effect: bool = len(predictor_indexes_used) == 1
368+
is_two_way_interaction: bool = len(predictor_indexes_used) == 2
369+
370+
if is_main_effect:
371+
fig = plt.figure()
372+
plt.plot(shape_df.iloc[:, 0], shape_df.iloc[:, 1])
373+
plt.xlabel(shape_df.columns[0])
374+
plt.ylabel("Contribution to linear predictor")
375+
plt.title(f"Main effect of {shape_df.columns[0]}")
376+
plt.grid(True)
377+
elif is_two_way_interaction:
378+
fig = plt.figure(figsize=(8, 6))
379+
pivot_table = shape_df.pivot_table(
380+
index=shape_df.columns[0],
381+
columns=shape_df.columns[1],
382+
values=shape_df.columns[2],
383+
aggfunc="mean",
384+
)
385+
plt.imshow(
386+
pivot_table.values,
387+
aspect="auto",
388+
origin="lower",
389+
extent=[
390+
pivot_table.columns.min(),
391+
pivot_table.columns.max(),
392+
pivot_table.index.min(),
393+
pivot_table.index.max(),
394+
],
395+
cmap="Blues_r",
396+
)
397+
plt.colorbar(label="Contribution to the linear predictor")
398+
plt.xlabel(shape_df.columns[1])
399+
plt.ylabel(shape_df.columns[0])
400+
plt.title(
401+
f"Interaction between {shape_df.columns[0]} and {shape_df.columns[1]}"
402+
)
403+
else:
404+
print(
405+
f"Plotting for interaction level > 2 is not supported. Affiliation: {affiliation}"
406+
)
407+
return
408+
409+
if save:
410+
save_path = (
411+
path if path else f"shape_of_{affiliation.replace(' & ', '_')}.png"
412+
)
413+
plt.savefig(save_path)
414+
415+
if plot:
416+
plt.show()
417+
418+
plt.close(fig)
419+
318420
def remove_provided_custom_functions(self):
319421
self.APLRRegressor.remove_provided_custom_functions()
320422
self.calculate_custom_validation_error_function = None
@@ -504,7 +606,36 @@ def get_categories(self) -> List[str]:
504606
return self.APLRClassifier.get_categories()
505607

506608
def get_logit_model(self, category: str) -> APLRRegressor:
507-
return self.APLRClassifier.get_logit_model(category)
609+
logit_model_cpp = self.APLRClassifier.get_logit_model(category)
610+
611+
logit_model_py = APLRRegressor(
612+
m=self.m,
613+
v=self.v,
614+
random_state=self.random_state,
615+
loss_function="binomial",
616+
link_function="logit",
617+
n_jobs=self.n_jobs,
618+
cv_folds=self.cv_folds,
619+
bins=self.bins,
620+
max_interaction_level=self.max_interaction_level,
621+
max_interactions=self.max_interactions,
622+
min_observations_in_split=self.min_observations_in_split,
623+
ineligible_boosting_steps_added=self.ineligible_boosting_steps_added,
624+
max_eligible_terms=self.max_eligible_terms,
625+
verbosity=self.verbosity,
626+
boosting_steps_before_interactions_are_allowed=self.boosting_steps_before_interactions_are_allowed,
627+
monotonic_constraints_ignore_interactions=self.monotonic_constraints_ignore_interactions,
628+
early_stopping_rounds=self.early_stopping_rounds,
629+
num_first_steps_with_linear_effects_only=self.num_first_steps_with_linear_effects_only,
630+
penalty_for_non_linearity=self.penalty_for_non_linearity,
631+
penalty_for_interactions=self.penalty_for_interactions,
632+
max_terms=self.max_terms,
633+
ridge_penalty=self.ridge_penalty,
634+
)
635+
636+
logit_model_py.APLRRegressor = logit_model_cpp
637+
638+
return logit_model_py
508639

509640
def get_validation_error_steps(self) -> FloatMatrix:
510641
return self.APLRClassifier.get_validation_error_steps()

documentation/APLR 10.12.1.pdf

-129 KB
Binary file not shown.

documentation/APLR 10.13.0.pdf

134 KB
Binary file not shown.

documentation/model_interpretation_for_classification.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,10 @@
33
## Feature importance
44
Use the ***get_feature_importance*** method as shown in this [example](https://github.com/ottenbreit-data-science/aplr/blob/main/examples/train_aplr_classification.py).
55

6+
## Local feature contribution
7+
Use the ***calculate_local_feature_contribution*** method, for example on test data or new data. Usage of this method is demonstrated in this [example](https://github.com/ottenbreit-data-science/aplr/blob/main/examples/train_aplr_classification.py).
8+
69
## Main effects and interactions
7-
For best interpretability of interactions, do not use a higher ***max_interaction_level*** than 1. Use the ***calculate_local_feature_contribution*** method to interpret main effects and interactions as shown in this [example](https://github.com/ottenbreit-data-science/aplr/blob/main/examples/train_aplr_classification.py). You may also use the ***get_logit_model*** method to access the underlying APLR regression models as shown in this [example](https://github.com/ottenbreit-data-science/aplr/blob/main/examples/train_aplr_classification.py). You can interpret these models in the same way as described in this [example](https://github.com/ottenbreit-data-science/aplr/blob/main/examples/train_aplr_regression.py).
10+
For each category, you can interpret the main effects and interactions of its underlying logit model. For best interpretability of interactions, do not use a higher ***max_interaction_level*** than 1.
11+
12+
A convenient way to visualize the model components is to first use the ***get_logit_model*** method to access the underlying `APLRRegressor` model for a specific category. Then, you can use the ***plot_affiliation_shape*** method on that logit model to generate plots for its main effects (line plots) and two-way interactions (heatmaps). This is demonstrated in this [example](https://github.com/ottenbreit-data-science/aplr/blob/main/examples/train_aplr_classification.py).

documentation/model_interpretation_for_regression.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ Use the ***get_feature_importance*** method as shown in this [example](https://g
77
Use the ***calculate_feature_importance*** method or the ***calculate_local_feature_contribution*** method, for example on test data or new data. Usage of these methods is demonstrated in this [example](https://github.com/ottenbreit-data-science/aplr/blob/main/examples/train_aplr_regression.py).
88

99
## Main effects
10-
Use the ***get_main_effect_shape*** method or the ***get_unique_term_affiliation_shape*** method to interpret main effects as shown in this [example](https://github.com/ottenbreit-data-science/aplr/blob/main/examples/train_aplr_regression.py). For each main effect, you may plot the output in a line plot.
10+
Use the ***plot_affiliation_shape*** method to easily plot main effects, as shown in this [example](https://github.com/ottenbreit-data-science/aplr/blob/main/examples/train_aplr_regression.py). Alternatively, use the ***get_main_effect_shape*** or ***get_unique_term_affiliation_shape*** methods to get the data for a custom plot.
1111

1212
## Interactions
13-
For best interpretability of interactions, do not use a higher ***max_interaction_level*** than 1. Use the ***get_unique_term_affiliation_shape*** method to interpret interactions as shown in this [example](https://github.com/ottenbreit-data-science/aplr/blob/main/examples/train_aplr_regression.py). For each two-way interaction of interest you may plot the output in a 3D surface plot.
13+
For best interpretability of interactions, do not use a higher ***max_interaction_level*** than 1. Use the ***plot_affiliation_shape*** method to easily plot two-way interactions as a heatmap, as shown in this [example](https://github.com/ottenbreit-data-science/aplr/blob/main/examples/train_aplr_regression.py). Alternatively, use the ***get_unique_term_affiliation_shape*** method to get the data for a custom plot, for example a 3D surface plot.
1414

1515
## Interpretation of model terms and their regression coefficients
1616
The above interpretations of main effects and interactions are sufficient to interpret an APLR model. However, it is possible to also inspect the underlying terms for those who wish to do so. For an example on how to interpret the terms in an APLR model, please see ***Section 5.1.3*** in the published article about APLR. You can find this article on [https://link.springer.com/article/10.1007/s00180-024-01475-4](https://link.springer.com/article/10.1007/s00180-024-01475-4) and [https://rdcu.be/dz7bF](https://rdcu.be/dz7bF).

examples/train_aplr_classification.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
v=0.5,
5353
num_first_steps_with_linear_effects_only=0, # Increasing this will increase interpretabilty but may decrease predictiveness.
5454
boosting_steps_before_interactions_are_allowed=0, # Increasing this will increase interpretabilty but may decrease predictiveness.
55-
**params
55+
**params,
5656
)
5757
model.fit(
5858
data_train[predictors].values, data_train[response].values, X_names=predictors
@@ -95,10 +95,20 @@
9595
by="importance", ascending=False
9696
)
9797

98+
# Generate and save plots of main effects and two-way interactions for each category. This is probably the most useful method for model interpretation.
99+
for category in categories:
100+
logit_model = best_model.get_logit_model(category)
101+
for affiliation in logit_model.get_unique_term_affiliations():
102+
logit_model.plot_affiliation_shape(
103+
affiliation,
104+
plot=False,
105+
save=True,
106+
path=f"shape of {affiliation} for category {category}.png",
107+
)
108+
98109
# Local feature contribution for each prediction. For each prediction, uses calculate_local_feature_contribution() in the logit APLRRegressor model
99110
# for the category that corresponds to the prediction. Example in this data: If a prediction is "2" then using calculate_local_feature_contribution()
100-
# in the logit model that predicts whether an observation belongs to class "2" or not. This can be used to interpret the model, for example
101-
# by creating 3D surface plots against predictor values to interpret two-way interactions. This method can also be used on new data.
111+
# in the logit model that predicts whether an observation belongs to class "2" or not. This method can also be used on new data.
102112
local_feature_contribution = pd.DataFrame(
103113
best_model.calculate_local_feature_contribution(data_train[predictors]),
104114
columns=best_model.get_unique_term_affiliations(),

examples/train_aplr_classification_using_aplr_tuner.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,20 @@
8686
by="importance", ascending=False
8787
)
8888

89+
# Generate and save plots of main effects and two-way interactions for each category. This is probably the most useful method for model interpretation.
90+
for category in categories:
91+
logit_model = best_model.get_logit_model(category)
92+
for affiliation in logit_model.get_unique_term_affiliations():
93+
logit_model.plot_affiliation_shape(
94+
affiliation,
95+
plot=False,
96+
save=True,
97+
path=f"shape of {affiliation} for category {category}.png",
98+
)
99+
89100
# Local feature contribution for each prediction. For each prediction, uses calculate_local_feature_contribution() in the logit APLRRegressor model
90101
# for the category that corresponds to the prediction. Example in this data: If a prediction is "2" then using calculate_local_feature_contribution()
91-
# in the logit model that predicts whether an observation belongs to class "2" or not. This can be used to interpret the model, for example
92-
# by creating 3D surface plots against predictor values to interpret two-way interactions. This method can also be used on new data.
102+
# in the logit model that predicts whether an observation belongs to class "2" or not. This method can also be used on new data.
93103
local_feature_contribution = pd.DataFrame(
94104
best_model.calculate_local_feature_contribution(data_train[predictors]),
95105
columns=best_model.get_unique_term_affiliations(),

examples/train_aplr_regression.py

Lines changed: 4 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -104,66 +104,11 @@
104104
by="importance", ascending=False
105105
)
106106

107-
# Shapes for all term affiliations in the model. For each term affiliation, shape_df contains predictor values and the corresponding
108-
# contributions to the linear predictor. Plots are created for main effects and two-way interactions.
109-
# This is probably the most useful method to use for understanding how the model works.
110-
predictors_in_each_affiliation = (
111-
best_model.get_base_predictors_in_each_unique_term_affiliation()
112-
)
113-
for affiliation_index, affiliation in enumerate(
114-
best_model.get_unique_term_affiliations()
115-
):
116-
shape = best_model.get_unique_term_affiliation_shape(affiliation)
117-
predictor_indexes_used = predictors_in_each_affiliation[affiliation_index]
118-
shape_df = pd.DataFrame(
119-
shape,
120-
columns=[predictors[i] for i in predictor_indexes_used] + ["contribution"],
107+
# Generate and save plots of main effects and two-way interactions. This is probably the most useful method for model interpretation.
108+
for affiliation in best_model.get_unique_term_affiliations():
109+
best_model.plot_affiliation_shape(
110+
affiliation, plot=False, save=True, path=f"shape of {affiliation}.png"
121111
)
122-
is_main_effect: bool = len(predictor_indexes_used) == 1
123-
is_two_way_interaction: bool = len(predictor_indexes_used) == 2
124-
if is_main_effect:
125-
plt.plot(shape_df.iloc[:, 0], shape_df.iloc[:, 1])
126-
plt.xlabel(shape_df.columns[0])
127-
plt.ylabel(shape_df.columns[1])
128-
plt.title("Contribution to the linear predictor")
129-
plt.savefig(f"shape of {affiliation}.png")
130-
plt.close()
131-
elif is_two_way_interaction:
132-
pivot_table = shape_df.pivot_table(
133-
index=shape_df.columns[0],
134-
columns=shape_df.columns[1],
135-
values=shape_df.columns[2],
136-
aggfunc="mean",
137-
)
138-
plt.figure(figsize=(8, 6))
139-
plt.imshow(
140-
pivot_table.values,
141-
aspect="auto",
142-
origin="lower",
143-
extent=[
144-
pivot_table.columns.min(),
145-
pivot_table.columns.max(),
146-
pivot_table.index.min(),
147-
pivot_table.index.max(),
148-
],
149-
cmap="Blues_r",
150-
)
151-
plt.colorbar(label="contribution")
152-
plt.xlabel(shape_df.columns[1])
153-
plt.ylabel(shape_df.columns[0])
154-
plt.title("Contribution to the linear predictor")
155-
plt.savefig(f"shape of {affiliation}.png")
156-
plt.close()
157-
158-
# Main effect shape for the third predictor. This can be visualized in a line plot.
159-
# Will be empty if the third predictor is not used as a main effect in the model.
160-
main_effect_shape = best_model.get_main_effect_shape(predictor_index=2)
161-
main_effect_shape = pd.DataFrame(
162-
{
163-
"predictor_value": main_effect_shape.keys(),
164-
"contribution_to_linear_predictor": main_effect_shape.values(),
165-
}
166-
)
167112

168113
# Local contribution to the linear predictor for each prediction in the training data. This can be used to interpret the model,
169114
# for example by visualizing two-way interactions versus predictor values in a 3D surface plot. This method can also be used on new data.

0 commit comments

Comments
 (0)