Skip to content

Commit 4cb2140

Browse files
int_constraints
1 parent 7cb9dfa commit 4cb2140

File tree

8 files changed

+140
-17
lines changed

8 files changed

+140
-17
lines changed

API_REFERENCE.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Specifies which metric to use for validating the model and tuning ***m***. Avail
5959
Specifies the quantile to use when ***family*** is "quantile".
6060

6161

62-
## Method: fit(X:npt.ArrayLike, y:npt.ArrayLike, sample_weight:npt.ArrayLike = np.empty(0), X_names:List[str]=[], validation_set_indexes:List[int]=[], prioritized_predictors_indexes:List[int]=[], monotonic_constraints:List[int]=[], group:npt.ArrayLike = np.empty(0))
62+
## Method: fit(X:npt.ArrayLike, y:npt.ArrayLike, sample_weight:npt.ArrayLike = np.empty(0), X_names:List[str]=[], validation_set_indexes:List[int]=[], prioritized_predictors_indexes:List[int]=[], monotonic_constraints:List[int]=[], group:npt.ArrayLike = np.empty(0), interaction_constraints:List[int]=[])
6363

6464
***This method fits the model to data.***
6565

@@ -84,11 +84,14 @@ An optional list of integers specifying the indexes of observations to be used f
8484
An optional list of integers specifying the indexes of predictors (columns) in ***X*** that should be prioritized. Terms of the prioritized predictors will enter the model as long as they reduce the training error and do not contain too few effective observations. They will also be updated more often.
8585

8686
#### monotonic_constraints
87-
An optional list of integers specifying monotonic constraints on model terms. For example, if there are three predictors in ***X***, then monotonic_constraints = [1,0,-1] means that 1) the first predictor in ***X*** cannot be used in interaction terms and all terms using the first predictor in ***X*** as a main effect must have positive regression coefficients, 2) there are no monotonic constraints on terms using the second predictor in ***X***, and 3) the third predictor in ***X*** cannot be used in interaction terms and all terms using the third predictor in ***X*** as a main effect must have negative regression coefficients.
87+
An optional list of integers specifying monotonic constraints on model terms. For example, if there are three predictors in ***X***, then monotonic_constraints = [1,0,-1] means that 1) the first predictor in ***X*** cannot be used in interaction terms as a secondary effect and all terms using the first predictor in ***X*** as a main effect must have positive regression coefficients, 2) there are no monotonic constraints on terms using the second predictor in ***X***, and 3) the third predictor in ***X*** cannot be used in interaction terms as a secondary effect and all terms using the third predictor in ***X*** as a main effect must have negative regression coefficients.
8888

8989
#### group
9090
A numpy vector of integers that is used when ***family*** is "group_gaussian". For example, ***group*** may represent year (could be useful in a time series model).
9191

92+
#### interaction_constraints
93+
An optional list of integers specifying interaction constraints on model terms. For example, if there are three predictors in ***X***, then interaction_constraints = [1,0,2] means that 1) the first predictor in ***X*** cannot be used in interaction terms as a secondary effect, 2) there are no interaction constraints on terms using the second predictor in ***X***, and 3) the third predictor in ***X*** cannot be used in any interaction terms.
94+
9295

9396
## Method: predict(X:npt.ArrayLike, cap_predictions_to_minmax_in_training:bool=True)
9497

aplr/aplr.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ def __set_params_cpp(self):
5050
self.APLRRegressor.validation_tuning_metric=self.validation_tuning_metric
5151
self.APLRRegressor.quantile=self.quantile
5252

53-
def fit(self, X:npt.ArrayLike, y:npt.ArrayLike, sample_weight:npt.ArrayLike = np.empty(0), X_names:List[str]=[], validation_set_indexes:List[int]=[], prioritized_predictors_indexes:List[int]=[], monotonic_constraints:List[int]=[],group:npt.ArrayLike = np.empty(0)):
53+
def fit(self, X:npt.ArrayLike, y:npt.ArrayLike, sample_weight:npt.ArrayLike = np.empty(0), X_names:List[str]=[], validation_set_indexes:List[int]=[], prioritized_predictors_indexes:List[int]=[], monotonic_constraints:List[int]=[], group:npt.ArrayLike = np.empty(0), interaction_constraints:List[int]=[]):
5454
self.__set_params_cpp()
55-
self.APLRRegressor.fit(X,y,sample_weight,X_names,validation_set_indexes,prioritized_predictors_indexes,monotonic_constraints,group)
55+
self.APLRRegressor.fit(X,y,sample_weight,X_names,validation_set_indexes,prioritized_predictors_indexes,monotonic_constraints,group,interaction_constraints)
5656

5757
def predict(self, X:npt.ArrayLike, cap_predictions_to_minmax_in_training:bool=True)->npt.ArrayLike:
5858
return self.APLRRegressor.predict(X, cap_predictions_to_minmax_in_training)

cpp/APLRRegressor.h

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,20 @@ class APLRRegressor
5050
VectorXi group_validation;
5151
std::set<int> unique_groups_train;
5252
std::set<int> unique_groups_validation;
53+
std::vector<int> interaction_constraints;
5354

5455
//Methods
5556
void validate_input_to_fit(const MatrixXd &X,const VectorXd &y,const VectorXd &sample_weight,const std::vector<std::string> &X_names,
5657
const std::vector<size_t> &validation_set_indexes, const std::vector<size_t> &prioritized_predictors_indexes,
57-
const std::vector<int> &monotonic_constraints, const VectorXi &group);
58+
const std::vector<int> &monotonic_constraints, const VectorXi &group, const std::vector<int> &interaction_constraints);
5859
void throw_error_if_validation_set_indexes_has_invalid_indexes(const VectorXd &y, const std::vector<size_t> &validation_set_indexes);
5960
void throw_error_if_prioritized_predictors_indexes_has_invalid_indexes(const MatrixXd &X, const std::vector<size_t> &prioritized_predictors_indexes);
6061
void throw_error_if_monotonic_constraints_has_invalid_indexes(const MatrixXd &X, const std::vector<int> &monotonic_constraints);
62+
void throw_error_if_interaction_constraints_has_invalid_indexes(const MatrixXd &X, const std::vector<int> &interaction_constraints);
6163
void define_training_and_validation_sets(const MatrixXd &X,const VectorXd &y,const VectorXd &sample_weight,
6264
const std::vector<size_t> &validation_set_indexes, const VectorXi &group);
63-
void initialize(const std::vector<size_t> &prioritized_predictors_indexes, const std::vector<int> &monotonic_constraints);
65+
void initialize(const std::vector<size_t> &prioritized_predictors_indexes, const std::vector<int> &monotonic_constraints,
66+
const std::vector<int> &interaction_constraints);
6467
bool check_if_base_term_has_only_one_unique_value(size_t base_term);
6568
void add_term_to_terms_eligible_current(Term &term);
6669
VectorXd calculate_neg_gradient_current(const VectorXd &sample_weight_train);
@@ -153,7 +156,7 @@ class APLRRegressor
153156
~APLRRegressor();
154157
void fit(const MatrixXd &X,const VectorXd &y,const VectorXd &sample_weight=VectorXd(0),const std::vector<std::string> &X_names={},const std::vector<size_t> &validation_set_indexes={},
155158
const std::vector<size_t> &prioritized_predictors_indexes={}, const std::vector<int> &monotonic_constraints={},
156-
const VectorXi &group=VectorXi(0));
159+
const VectorXi &group=VectorXi(0), const std::vector<int> &interaction_constraints={});
157160
VectorXd predict(const MatrixXd &X, bool cap_predictions_to_minmax_in_training=true);
158161
void set_term_names(const std::vector<std::string> &X_names);
159162
MatrixXd calculate_local_feature_importance(const MatrixXd &X);
@@ -214,15 +217,16 @@ APLRRegressor::~APLRRegressor()
214217
//invalidating validation_ratio. The rest of indexes are used to train.
215218
void APLRRegressor::fit(const MatrixXd &X,const VectorXd &y,const VectorXd &sample_weight,const std::vector<std::string> &X_names,
216219
const std::vector<size_t> &validation_set_indexes,const std::vector<size_t> &prioritized_predictors_indexes,
217-
const std::vector<int> &monotonic_constraints, const VectorXi &group)
220+
const std::vector<int> &monotonic_constraints, const VectorXi &group, const std::vector<int> &interaction_constraints)
218221
{
219222
throw_error_if_family_does_not_exist();
220223
throw_error_if_link_function_does_not_exist();
221224
throw_error_if_tweedie_power_is_invalid();
222-
validate_input_to_fit(X,y,sample_weight,X_names,validation_set_indexes,prioritized_predictors_indexes,monotonic_constraints,group);
225+
validate_input_to_fit(X,y,sample_weight,X_names,validation_set_indexes,prioritized_predictors_indexes,monotonic_constraints,group,
226+
interaction_constraints);
223227
define_training_and_validation_sets(X,y,sample_weight,validation_set_indexes,group);
224228
scale_training_observations_if_using_log_link_function();
225-
initialize(prioritized_predictors_indexes, monotonic_constraints);
229+
initialize(prioritized_predictors_indexes, monotonic_constraints, interaction_constraints);
226230
execute_boosting_steps();
227231
update_coefficients_for_all_steps();
228232
print_final_summary();
@@ -281,7 +285,8 @@ void APLRRegressor::throw_error_if_tweedie_power_is_invalid()
281285

282286
void APLRRegressor::validate_input_to_fit(const MatrixXd &X,const VectorXd &y,const VectorXd &sample_weight,
283287
const std::vector<std::string> &X_names, const std::vector<size_t> &validation_set_indexes,
284-
const std::vector<size_t> &prioritized_predictors_indexes, const std::vector<int> &monotonic_constraints, const VectorXi &group)
288+
const std::vector<size_t> &prioritized_predictors_indexes, const std::vector<int> &monotonic_constraints, const VectorXi &group,
289+
const std::vector<int> &interaction_constraints)
285290
{
286291
if(X.rows()!=y.size()) throw std::runtime_error("X and y must have the same number of rows.");
287292
if(X.rows()<2) throw std::runtime_error("X and y cannot have less than two rows.");
@@ -292,6 +297,7 @@ void APLRRegressor::validate_input_to_fit(const MatrixXd &X,const VectorXd &y,co
292297
throw_error_if_validation_set_indexes_has_invalid_indexes(y, validation_set_indexes);
293298
throw_error_if_prioritized_predictors_indexes_has_invalid_indexes(X, prioritized_predictors_indexes);
294299
throw_error_if_monotonic_constraints_has_invalid_indexes(X, monotonic_constraints);
300+
throw_error_if_interaction_constraints_has_invalid_indexes(X, interaction_constraints);
295301
throw_error_if_response_contains_invalid_values(y);
296302
throw_error_if_sample_weight_contains_invalid_values(y, sample_weight);
297303
bool group_is_of_incorrect_size{family=="group_gaussian" && group.rows()!=y.rows()};
@@ -329,6 +335,24 @@ void APLRRegressor::throw_error_if_monotonic_constraints_has_invalid_indexes(con
329335
throw std::runtime_error("monotonic_constraints must either be empty or a vector with one integer for each column in X.");
330336
}
331337

338+
void APLRRegressor::throw_error_if_interaction_constraints_has_invalid_indexes(const MatrixXd &X, const std::vector<int> &interaction_constraints)
339+
{
340+
bool dimension_error{interaction_constraints.size()>0 && interaction_constraints.size() != X.cols()};
341+
if(dimension_error)
342+
throw std::runtime_error("interaction_constraints must either be empty or a vector with one integer for each column in X.");
343+
bool value_error{false};
344+
for(auto &constraint:interaction_constraints)
345+
{
346+
if(constraint<0 || constraint>2)
347+
{
348+
value_error=true;
349+
break;
350+
}
351+
}
352+
if(value_error)
353+
throw std::runtime_error("The value of each element in interaction_constraints must be 0 (no constraint), 1 (the predictor can only appear as a main effect in an interaction), or 2 (the predictor cannot appear in any interaction).");
354+
}
355+
332356
void APLRRegressor::throw_error_if_response_contains_invalid_values(const VectorXd &y)
333357
{
334358
if(link_function=="logit" || family=="binomial")
@@ -507,7 +531,8 @@ void APLRRegressor::scale_training_observations_if_using_log_link_function()
507531
}
508532
}
509533

510-
void APLRRegressor::initialize(const std::vector<size_t> &prioritized_predictors_indexes, const std::vector<int> &monotonic_constraints)
534+
void APLRRegressor::initialize(const std::vector<size_t> &prioritized_predictors_indexes, const std::vector<int> &monotonic_constraints,
535+
const std::vector<int> &interaction_constraints)
511536
{
512537
number_of_base_terms=static_cast<size_t>(X_train.cols());
513538

@@ -547,6 +572,16 @@ void APLRRegressor::initialize(const std::vector<size_t> &prioritized_predictors
547572
}
548573
}
549574

575+
this->interaction_constraints=interaction_constraints;
576+
bool interaction_constraints_provided{interaction_constraints.size()>0};
577+
if(interaction_constraints_provided)
578+
{
579+
for(auto &term_eligible_current:terms_eligible_current)
580+
{
581+
term_eligible_current.set_interaction_constraint(interaction_constraints[term_eligible_current.base_term]);
582+
}
583+
}
584+
550585
linear_predictor_current=VectorXd::Constant(y_train.size(),intercept);
551586
linear_predictor_null_model=linear_predictor_current;
552587
linear_predictor_current_validation=VectorXd::Constant(y_validation.size(),intercept);
@@ -811,6 +846,7 @@ void APLRRegressor::determine_interactions_to_consider(const std::vector<size_t>
811846
interactions_to_consider=std::vector<Term>();
812847
interactions_to_consider.reserve(static_cast<size_t>(X_train.cols())*terms.size());
813848
bool monotonic_constraints_provided{monotonic_constraints.size()>0};
849+
bool interaction_constraints_provided{interaction_constraints.size()>0};
814850

815851
VectorXi indexes_for_terms_to_consider_as_interaction_partners{find_indexes_for_terms_to_consider_as_interaction_partners()};
816852
for (auto &model_term_index:indexes_for_terms_to_consider_as_interaction_partners)
@@ -823,11 +859,19 @@ void APLRRegressor::determine_interactions_to_consider(const std::vector<size_t>
823859
Term interaction{Term(new_term_index)};
824860
if(monotonic_constraints_provided)
825861
interaction.set_monotonic_constraint(monotonic_constraints[new_term_index]);
862+
if(interaction_constraints_provided)
863+
{
864+
interaction.set_interaction_constraint(interaction_constraints[new_term_index]);
865+
bool new_term_cannot_be_an_interaction{interaction.get_interaction_constraint()==2};
866+
if(new_term_cannot_be_an_interaction)
867+
continue;
868+
}
826869
Term model_term_without_given_terms{terms[model_term_index]};
827870
model_term_without_given_terms.given_terms.clear();
828871
model_term_without_given_terms.cleanup_when_this_term_was_added_as_a_given_term();
829872
Term model_term_with_added_given_term{terms[model_term_index]};
830-
bool model_term_without_given_terms_can_be_a_given_term{model_term_without_given_terms.get_monotonic_constraint()==0};
873+
bool model_term_without_given_terms_can_be_a_given_term{model_term_without_given_terms.get_monotonic_constraint()==0
874+
&& model_term_without_given_terms.get_interaction_constraint()==0};
831875
if(model_term_without_given_terms_can_be_a_given_term)
832876
model_term_with_added_given_term.given_terms.push_back(model_term_without_given_terms);
833877
add_necessary_given_terms_to_interaction(interaction, model_term_with_added_given_term);
@@ -1345,6 +1389,7 @@ void APLRRegressor::cleanup_after_fit()
13451389
group_validation.resize(0);
13461390
unique_groups_train.clear();
13471391
unique_groups_validation.clear();
1392+
interaction_constraints.clear();
13481393
}
13491394

13501395
VectorXd APLRRegressor::predict(const MatrixXd &X, bool cap_predictions_to_minmax_in_training)

cpp/pythonbinding.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ PYBIND11_MODULE(aplr_cpp, m) {
2424
)
2525
.def("fit", &APLRRegressor::fit,py::arg("X"),py::arg("y"),py::arg("sample_weight")=VectorXd(0),py::arg("X_names")=std::vector<std::string>(),
2626
py::arg("validation_set_indexes")=std::vector<size_t>(),py::arg("prioritized_predictors_indexes")=std::vector<size_t>(),
27-
py::arg("monotonic_constraints")=std::vector<int>(),py::arg("group")=VectorXi(0),
27+
py::arg("monotonic_constraints")=std::vector<int>(),py::arg("group")=VectorXi(0), py::arg("interaction_constraints")=std::vector<int>(),
2828
py::call_guard<py::scoped_ostream_redirect,py::scoped_estream_redirect>())
2929
.def("predict", &APLRRegressor::predict,py::arg("X"),py::arg("bool cap_predictions_to_minmax_in_training")=true)
3030
.def("set_term_names", &APLRRegressor::set_term_names,py::arg("X_names"))

cpp/term.h

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class Term
3838
VectorXd negative_gradient_discretized;
3939
std::vector<size_t> observations_in_bins;
4040
int monotonic_constraint;
41+
int interaction_constraint;
4142

4243
void calculate_error_where_given_terms_are_zero(const VectorXd &negative_gradient, const VectorXd &sample_weight);
4344
void initialize_parameters_in_estimate_split_point(size_t bins,double v,size_t min_observations_in_split);
@@ -88,6 +89,8 @@ class Term
8889
bool get_can_be_used_as_a_given_term();
8990
void set_monotonic_constraint(int constraint);
9091
int get_monotonic_constraint();
92+
void set_interaction_constraint(int constraint);
93+
int get_interaction_constraint();
9194

9295
friend bool operator== (const Term &p1, const Term &p2);
9396
friend class APLRRegressor;
@@ -96,14 +99,15 @@ class Term
9699
Term::Term(size_t base_term,const std::vector<Term> &given_terms,double split_point,bool direction_right,double coefficient):
97100
name{""},base_term{base_term},given_terms{given_terms},split_point{split_point},direction_right{direction_right},coefficient{coefficient},
98101
split_point_search_errors_sum{std::numeric_limits<double>::infinity()},ineligible_boosting_steps{0},can_be_used_as_a_given_term{false},
99-
monotonic_constraint{0}
102+
monotonic_constraint{0},interaction_constraint{0}
100103
{
101104
}
102105

103106
Term::Term(const Term &other):
104107
name{other.name},base_term{other.base_term},given_terms{other.given_terms},split_point{other.split_point},direction_right{other.direction_right},
105108
coefficient{other.coefficient},coefficient_steps{other.coefficient_steps},split_point_search_errors_sum{other.split_point_search_errors_sum},
106-
ineligible_boosting_steps{0},can_be_used_as_a_given_term{other.can_be_used_as_a_given_term},monotonic_constraint{other.monotonic_constraint}
109+
ineligible_boosting_steps{0},can_be_used_as_a_given_term{other.can_be_used_as_a_given_term},monotonic_constraint{other.monotonic_constraint},
110+
interaction_constraint{other.interaction_constraint}
107111
{
108112
}
109113

@@ -679,6 +683,16 @@ int Term::get_monotonic_constraint()
679683
return monotonic_constraint;
680684
}
681685

686+
void Term::set_interaction_constraint(int constraint)
687+
{
688+
interaction_constraint = constraint;
689+
}
690+
691+
int Term::get_interaction_constraint()
692+
{
693+
return interaction_constraint;
694+
}
695+
682696

683697
std::vector<std::vector<size_t>> distribute_terms_indexes_to_cores(std::vector<size_t> &term_indexes,size_t n_jobs)
684698
{

0 commit comments

Comments
 (0)