Skip to content

Commit 398e882

Browse files
10.0.0
1 parent 86d87d0 commit 398e882

File tree

10 files changed

+59
-65
lines changed

10 files changed

+59
-65
lines changed

API_REFERENCE_FOR_REGRESSION.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,9 +312,9 @@ A numpy matrix with predictor values.
312312
***Returns the validation_tuning_metric used.***
313313

314314

315-
## Method: get_coefficient_shape_function(predictor_index:int)
315+
## Method: get_main_effect_shape(predictor_index:int)
316316

317-
***For the predictor in X specified by predictor_index, get_coefficient_shape_function returns a dictionary with keys equal to predictor values and values equal to coefficient. For each predictor value, the coefficient denotes the change in the linear predictor given that the predictor value increases by one unit (interactions with other predictors are ignored). This function makes it easier to interpret APLR models as one can quickly see how the main effects work across relevant values of the predictor. Predictor values lower than the lowest predictor value in the dictionary have the same coefficient that the lowest predictor value in the dictionary has. Predictor values higher than the highest predictor value in the dictionary have the same coefficient that the highest predictor value in the dictionary has.***
317+
***For the predictor in X specified by predictor_index, get_main_effect_shape returns a dictionary with keys equal to predictor values and values equal to the corresponding contribution to the linear predictor (interactions with other predictors are ignored). This method makes it easier to interpret main effects.***
318318

319319
### Parameters
320320

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ Please see the two example Python scripts [here](https://github.com/ottenbreit-d
1717
Please consider sponsoring Ottenbreit Data Science by clicking on the Sponsor button. Sufficient funding will enable maintenance of APLR and further development.
1818

1919
# API reference
20-
Please see the [api reference for regression](https://github.com/ottenbreit-data-science/aplr/blob/main/API_REFERENCE_FOR_REGRESSION.md) and [api reference for classification](https://github.com/ottenbreit-data-science/aplr/blob/main/API_REFERENCE_FOR_CLASSIFICATION.md).
20+
Please see the [API reference for regression](https://github.com/ottenbreit-data-science/aplr/blob/main/API_REFERENCE_FOR_REGRESSION.md) and [API reference for classification](https://github.com/ottenbreit-data-science/aplr/blob/main/API_REFERENCE_FOR_CLASSIFICATION.md).

aplr/aplr.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -277,10 +277,8 @@ def get_optimal_m(self) -> int:
277277
def get_validation_tuning_metric(self) -> str:
278278
return self.APLRRegressor.get_validation_tuning_metric()
279279

280-
def get_coefficient_shape_function(
281-
self, predictor_index: int
282-
) -> Dict[float, float]:
283-
return self.APLRRegressor.get_coefficient_shape_function(predictor_index)
280+
def get_main_effect_shape(self, predictor_index: int) -> Dict[float, float]:
281+
return self.APLRRegressor.get_main_effect_shape(predictor_index)
284282

285283
def get_cv_error(self) -> float:
286284
return self.APLRRegressor.get_cv_error()

cpp/APLRRegressor.h

Lines changed: 32 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class APLRRegressor
100100
void preprocess_predictor_learning_rates_and_penalties(const MatrixXd &X, const std::vector<double> &predictor_learning_rates,
101101
const std::vector<double> &predictor_penalties_for_non_linearity,
102102
const std::vector<double> &predictor_penalties_for_interactions);
103+
void calculate_min_and_max_predictor_values_in_training(const MatrixXd &X);
103104
std::vector<double> preprocess_predictor_learning_rate_or_penalty(const MatrixXd &X, double general_value,
104105
const std::vector<double> &predictor_specific_values);
105106
void fit_model_for_cv_fold(const MatrixXd &X, const VectorXd &y, const VectorXd &sample_weight,
@@ -233,6 +234,8 @@ class APLRRegressor
233234
double penalty_for_non_linearity;
234235
double penalty_for_interactions;
235236
size_t max_terms;
237+
VectorXd min_predictor_values_in_training;
238+
VectorXd max_predictor_values_in_training;
236239

237240
APLRRegressor(size_t m = 3000, double v = 0.1, uint_fast32_t random_state = std::numeric_limits<uint_fast32_t>::lowest(), std::string loss_function = "mse",
238241
std::string link_function = "identity", size_t n_jobs = 0, size_t cv_folds = 5,
@@ -274,7 +277,7 @@ class APLRRegressor
274277
double get_intercept();
275278
size_t get_optimal_m();
276279
std::string get_validation_tuning_metric();
277-
std::map<double, double> get_coefficient_shape_function(size_t predictor_index);
280+
std::map<double, double> get_main_effect_shape(size_t predictor_index);
278281
double get_cv_error();
279282

280283
friend class APLRClassifier;
@@ -336,7 +339,8 @@ APLRRegressor::APLRRegressor(const APLRRegressor &other)
336339
early_stopping_rounds{other.early_stopping_rounds},
337340
num_first_steps_with_linear_effects_only{other.num_first_steps_with_linear_effects_only},
338341
penalty_for_non_linearity{other.penalty_for_non_linearity}, penalty_for_interactions{other.penalty_for_interactions},
339-
max_terms{other.max_terms}
342+
max_terms{other.max_terms}, min_predictor_values_in_training{other.min_predictor_values_in_training},
343+
max_predictor_values_in_training{other.max_predictor_values_in_training}
340344
{
341345
}
342346

@@ -364,6 +368,7 @@ void APLRRegressor::fit(const MatrixXd &X, const VectorXd &y, const VectorXd &sa
364368
preprocess_penalties();
365369
preprocess_predictor_learning_rates_and_penalties(X, predictor_learning_rates, predictor_penalties_for_non_linearity,
366370
predictor_penalties_for_interactions);
371+
calculate_min_and_max_predictor_values_in_training(X);
367372
cv_fold_models.resize(cv_observations_used.cols());
368373
for (Eigen::Index i = 0; i < cv_observations_used.cols(); ++i)
369374
{
@@ -447,6 +452,17 @@ std::vector<double> APLRRegressor::preprocess_predictor_learning_rate_or_penalty
447452
return output;
448453
}
449454

455+
void APLRRegressor::calculate_min_and_max_predictor_values_in_training(const MatrixXd &X)
456+
{
457+
min_predictor_values_in_training = VectorXd(X.cols());
458+
max_predictor_values_in_training = VectorXd(X.cols());
459+
for (Eigen::Index i = 0; i < X.cols(); ++i)
460+
{
461+
min_predictor_values_in_training[i] = X.col(i).minCoeff();
462+
max_predictor_values_in_training[i] = X.col(i).maxCoeff();
463+
}
464+
}
465+
450466
void APLRRegressor::fit_model_for_cv_fold(const MatrixXd &X, const VectorXd &y, const VectorXd &sample_weight,
451467
const std::vector<std::string> &X_names, const VectorXi &cv_observations_in_fold,
452468
const std::vector<int> &monotonic_constraints, const VectorXi &group, const MatrixXd &other_data,
@@ -575,8 +591,8 @@ void APLRRegressor::validate_input_to_fit(const MatrixXd &X, const VectorXd &y,
575591
{
576592
Eigen::Index rows_with_ones{(cv_observations.col(i).array() == 1).count()};
577593
Eigen::Index rows_with_minus_ones{(cv_observations.col(i).array() == -1).count()};
578-
if (rows_with_ones < min_obserations_in_a_cv_fold || rows_with_minus_ones < min_obserations_in_a_cv_fold)
579-
throw std::runtime_error("Each column in cv_observations must contain at least " + std::to_string(min_obserations_in_a_cv_fold) + " observations for each of the values 1 and -1.");
594+
if (rows_with_ones < MIN_OBSERATIONS_IN_A_CV_FOLD || rows_with_minus_ones < MIN_OBSERATIONS_IN_A_CV_FOLD)
595+
throw std::runtime_error("Each column in cv_observations must contain at least " + std::to_string(MIN_OBSERATIONS_IN_A_CV_FOLD) + " observations for each of the values 1 and -1.");
580596
}
581597
}
582598
bool group_is_of_incorrect_size{(loss_function == "group_mse" || validation_tuning_metric == "group_mse") && group.rows() != y.rows()};
@@ -744,7 +760,7 @@ MatrixXi APLRRegressor::preprocess_cv_observations(const MatrixXi &cv_observatio
744760
{
745761
Eigen::Index rows_with_ones{(output.col(i).array() == 1).count()};
746762
Eigen::Index rows_with_minus_ones{(output.col(i).array() == -1).count()};
747-
if (rows_with_ones < min_obserations_in_a_cv_fold || rows_with_minus_ones < min_obserations_in_a_cv_fold)
763+
if (rows_with_ones < MIN_OBSERATIONS_IN_A_CV_FOLD || rows_with_minus_ones < MIN_OBSERATIONS_IN_A_CV_FOLD)
748764
throw std::runtime_error("Did not generate enough observations in a fold. Please try again with a different random_state and/or change cv_folds.");
749765
}
750766
}
@@ -2366,20 +2382,21 @@ std::string APLRRegressor::get_validation_tuning_metric()
23662382
return validation_tuning_metric;
23672383
}
23682384

2369-
std::map<double, double> APLRRegressor::get_coefficient_shape_function(size_t predictor_index)
2385+
std::map<double, double> APLRRegressor::get_main_effect_shape(size_t predictor_index)
23702386
{
23712387
if (model_has_not_been_trained())
2372-
throw std::runtime_error("The model must have been trained before using get_coefficient_shape_function().");
2388+
throw std::runtime_error("The model must have been trained before using get_main_effect_shape().");
23732389

2374-
std::map<double, double> coefficient_shape_function;
2390+
std::map<double, double> main_effect_shape;
23752391

23762392
std::vector<size_t> relevant_term_indexes{compute_relevant_term_indexes(predictor_index)};
23772393
bool relevant_term_indexes_do_not_exist{relevant_term_indexes.size() == 0};
23782394
if (relevant_term_indexes_do_not_exist)
2379-
return coefficient_shape_function;
2395+
return main_effect_shape;
23802396

23812397
std::vector<double> split_points;
2382-
split_points.reserve(relevant_term_indexes.size() * 4);
2398+
size_t max_potential_split_points{relevant_term_indexes.size() * 3 + 2};
2399+
split_points.reserve(max_potential_split_points);
23832400
for (auto &relevant_term_index : relevant_term_indexes)
23842401
{
23852402
bool split_point_exits{std::isfinite(terms[relevant_term_index].split_point)};
@@ -2396,35 +2413,8 @@ std::map<double, double> APLRRegressor::get_coefficient_shape_function(size_t pr
23962413
}
23972414
}
23982415
}
2399-
bool no_split_points{split_points.size() == 0};
2400-
if (no_split_points)
2401-
{
2402-
split_points.push_back(0);
2403-
split_points.push_back(1);
2404-
}
2405-
split_points = remove_duplicate_elements_from_vector(split_points);
2406-
bool one_split_point{split_points.size() == 1};
2407-
if (one_split_point)
2408-
{
2409-
split_points.push_back(split_points[0] - 1);
2410-
split_points = remove_duplicate_elements_from_vector(split_points);
2411-
}
2412-
2413-
VectorXd split_point_increments{VectorXd(split_points.size() - 1)};
2414-
for (Eigen::Index i = 0; i < split_point_increments.size(); ++i)
2415-
{
2416-
split_point_increments[i] = split_points[i + 1] - split_points[i];
2417-
}
2418-
double minimum_split_point_increment{split_point_increments.minCoeff()};
2419-
double increment_around_split_points{minimum_split_point_increment / DIVISOR_IN_GET_COEFFICIENT_SHAPE_FUNCTION};
2420-
2421-
size_t num_split_points{split_points.size()};
2422-
for (size_t i = 0; i < num_split_points; ++i)
2423-
{
2424-
split_points.push_back(split_points[i] - increment_around_split_points);
2425-
split_points.push_back(split_points[i] + increment_around_split_points);
2426-
}
2427-
split_points.push_back(split_points[split_points.size() - 1] + increment_around_split_points);
2416+
split_points.push_back(min_predictor_values_in_training[predictor_index]);
2417+
split_points.push_back(max_predictor_values_in_training[predictor_index]);
24282418
split_points = remove_duplicate_elements_from_vector(split_points);
24292419
split_points.shrink_to_fit();
24302420

@@ -2435,12 +2425,12 @@ std::map<double, double> APLRRegressor::get_coefficient_shape_function(size_t pr
24352425
}
24362426

24372427
VectorXd contribution_to_linear_predictor{calculate_local_contribution_from_selected_terms(X, {predictor_index})};
2438-
for (size_t i = 0; i < split_points.size() - 1; ++i)
2428+
for (size_t i = 0; i < split_points.size(); ++i)
24392429
{
2440-
coefficient_shape_function[split_points[i]] = (contribution_to_linear_predictor[i + 1] - contribution_to_linear_predictor[i]) / (split_points[i + 1] - split_points[i]);
2430+
main_effect_shape[split_points[i]] = contribution_to_linear_predictor[i];
24412431
}
24422432

2443-
return coefficient_shape_function;
2433+
return main_effect_shape;
24442434
}
24452435

24462436
std::vector<size_t> APLRRegressor::compute_relevant_term_indexes(size_t predictor_index)

cpp/constants.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,4 @@ const double NAN_DOUBLE{std::numeric_limits<double>::quiet_NaN()};
55
const int MAX_ABS_EXPONENT_TO_APPLY_ON_LINEAR_PREDICTOR_IN_LOGIT_MODEL{std::min(16, std::numeric_limits<double>::max_exponent10)};
66
const std::string MSE_LOSS_FUNCTION{"mse"};
77
const size_t MIN_CATEGORIES_IN_CLASSIFIER{2};
8-
const double DIVISOR_IN_GET_COEFFICIENT_SHAPE_FUNCTION{1000.0};
9-
const Eigen::Index min_obserations_in_a_cv_fold{2};
8+
const Eigen::Index MIN_OBSERATIONS_IN_A_CV_FOLD{2};

cpp/pythonbinding.cpp

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ PYBIND11_MODULE(aplr_cpp, m)
7070
.def("get_intercept", &APLRRegressor::get_intercept)
7171
.def("get_optimal_m", &APLRRegressor::get_optimal_m)
7272
.def("get_validation_tuning_metric", &APLRRegressor::get_validation_tuning_metric)
73-
.def("get_coefficient_shape_function", &APLRRegressor::get_coefficient_shape_function, py::arg("predictor_index"))
73+
.def("get_main_effect_shape", &APLRRegressor::get_main_effect_shape, py::arg("predictor_index"))
7474
.def("get_cv_error", &APLRRegressor::get_cv_error)
7575
.def_readwrite("intercept", &APLRRegressor::intercept)
7676
.def_readwrite("m", &APLRRegressor::m)
@@ -118,6 +118,8 @@ PYBIND11_MODULE(aplr_cpp, m)
118118
.def_readwrite("penalty_for_non_linearity", &APLRRegressor::penalty_for_non_linearity)
119119
.def_readwrite("penalty_for_interactions", &APLRRegressor::penalty_for_interactions)
120120
.def_readwrite("max_terms", &APLRRegressor::max_terms)
121+
.def_readwrite("min_predictor_values_in_training", &APLRRegressor::min_predictor_values_in_training)
122+
.def_readwrite("max_predictor_values_in_training", &APLRRegressor::max_predictor_values_in_training)
121123
.def(py::pickle(
122124
[](const APLRRegressor &a) { // __getstate__
123125
/* Return a tuple that fully encodes the state of the object */
@@ -130,10 +132,11 @@ PYBIND11_MODULE(aplr_cpp, m)
130132
a.monotonic_constraints_ignore_interactions, a.group_mse_by_prediction_bins,
131133
a.group_mse_cycle_min_obs_in_bin, a.cv_error, a.term_importance, a.term_main_predictor_indexes,
132134
a.term_interaction_levels, a.early_stopping_rounds, a.num_first_steps_with_linear_effects_only,
133-
a.penalty_for_non_linearity, a.penalty_for_interactions, a.max_terms);
135+
a.penalty_for_non_linearity, a.penalty_for_interactions, a.max_terms,
136+
a.min_predictor_values_in_training, a.max_predictor_values_in_training);
134137
},
135138
[](py::tuple t) { // __setstate__
136-
if (t.size() != 41)
139+
if (t.size() != 43)
137140
throw std::runtime_error("Invalid state!");
138141

139142
/* Create a new C++ instance */
@@ -178,6 +181,8 @@ PYBIND11_MODULE(aplr_cpp, m)
178181
double penalty_for_non_linearity = t[38].cast<double>();
179182
double penalty_for_interactions = t[39].cast<double>();
180183
size_t max_terms = t[40].cast<size_t>();
184+
VectorXd min_predictor_values_in_training = t[41].cast<VectorXd>();
185+
VectorXd max_predictor_values_in_training = t[42].cast<VectorXd>();
181186

182187
APLRRegressor a(m, v, random_state, loss_function, link_function, n_jobs, cv_folds, 100, bins, verbosity, max_interaction_level,
183188
max_interactions, min_observations_in_split, ineligible_boosting_steps_added, max_eligible_terms, dispersion_parameter,
@@ -206,6 +211,8 @@ PYBIND11_MODULE(aplr_cpp, m)
206211
a.penalty_for_non_linearity = penalty_for_non_linearity;
207212
a.penalty_for_interactions = penalty_for_interactions;
208213
a.max_terms = max_terms;
214+
a.min_predictor_values_in_training = min_predictor_values_in_training;
215+
a.max_predictor_values_in_training = max_predictor_values_in_training;
209216

210217
return a;
211218
}));

cpp/tests.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1605,13 +1605,13 @@ class Tests
16051605
std::cout << predictions.mean() << "\n\n";
16061606
tests.push_back(is_approximately_equal(predictions.mean(), 23.7035, 0.00001));
16071607

1608-
std::map<double, double> coefficient_shape_function = model.get_coefficient_shape_function(1);
1609-
bool coefficient_shape_function_has_correct_length{coefficient_shape_function.size() == 27};
1610-
bool coefficient_shape_function_value_test{is_approximately_equal(coefficient_shape_function.begin()->second, 0.04175, 0.00001)};
1608+
std::map<double, double> main_effect_shape = model.get_main_effect_shape(1);
1609+
bool main_effect_shape_has_correct_length{main_effect_shape.size() == 11};
1610+
bool main_effect_shape_value_test{is_approximately_equal(main_effect_shape.begin()->second, -0.44924570143235887)};
16111611
bool li_for_particular_terms_has_correct_size{li_for_particular_terms.rows() == X_train.rows()};
16121612
bool li_for_particular_terms_mean_is_correct{is_approximately_equal(li_for_particular_terms.mean(), 0.30321952178814915)};
1613-
tests.push_back(coefficient_shape_function_has_correct_length);
1614-
tests.push_back(coefficient_shape_function_value_test);
1613+
tests.push_back(main_effect_shape_has_correct_length);
1614+
tests.push_back(main_effect_shape_value_test);
16151615
tests.push_back(li_for_particular_terms_has_correct_size);
16161616
tests.push_back(li_for_particular_terms_mean_is_correct);
16171617
}
Binary file not shown.

examples/train_aplr_regression.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,12 @@
102102
}
103103
)
104104

105-
# Coefficient shape for the third predictor. Will be empty if the third predictor is not used as a main effect in the model.
106-
coefficient_shape = best_model.get_coefficient_shape_function(predictor_index=2)
107-
coefficient_shape = pd.DataFrame(
105+
# Main effect shape for the third predictor. Will be empty if the third predictor is not used as a main effect in the model.
106+
main_effect_shape = best_model.get_main_effect_shape(predictor_index=2)
107+
main_effect_shape = pd.DataFrame(
108108
{
109-
"predictor_value": coefficient_shape.keys(),
110-
"coefficient": coefficient_shape.values(),
109+
"predictor_value": main_effect_shape.keys(),
110+
"coefficient": main_effect_shape.values(),
111111
}
112112
)
113113

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525

2626
setuptools.setup(
2727
name="aplr",
28-
version="9.10.1",
28+
version="10.0.0",
2929
description="Automatic Piecewise Linear Regression",
3030
ext_modules=[sfc_module],
3131
author="Mathias von Ottenbreit",

0 commit comments

Comments
 (0)