Skip to content

Commit d3fe6c4

Browse files
10.12.0
1 parent dfe5620 commit d3fe6c4

File tree

6 files changed

+252
-3
lines changed

6 files changed

+252
-3
lines changed

API_REFERENCE_FOR_REGRESSION.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ Limits 1) the number of terms already in the model that can be considered as int
5050
Specifies the variance power when ***loss_function*** is "tweedie". Specifies a dispersion parameter when ***loss_function*** is "negative_binomial", "cauchy" or "weibull".
5151

5252
#### validation_tuning_metric (default = "default")
53-
Specifies which metric to use for validating the model and tuning ***m***. Available options are "default" (using the same methodology as when calculating the training error), "mse", "mae", "negative_gini" (normalized), "group_mse", "group_mse_by_prediction" and "custom_function". The default is often a choice that fits well with respect to the ***loss_function*** chosen. However, if you want to use ***loss_function*** or ***dispersion_parameter*** as tuning parameters then the default is not suitable. "group_mse" requires that the "group" argument in the ***fit*** method is provided. "group_mse_by_prediction" groups predictions by up to ***group_mse_by_prediction_bins*** groups and calculates groupwise mse. For "custom_function" see ***calculate_custom_validation_error_function*** below.
53+
Specifies which metric to use for validating the model and tuning ***m***. The model will try to minimize the validation metric. Available options are "default" (using the same methodology as when calculating the training error), "mse", "mae", "negative_gini" (normalized), "group_mse", "group_mse_by_prediction", "neg_top_quantile_mean_response", "bottom_quantile_mean_response" and "custom_function". The default is often a choice that fits well with respect to the ***loss_function*** chosen. However, if you want to use ***loss_function*** or ***dispersion_parameter*** as tuning parameters then the default is not suitable. "group_mse" requires that the "group" argument in the ***fit*** method is provided. "group_mse_by_prediction" groups predictions by up to ***group_mse_by_prediction_bins*** groups and calculates groupwise mse. "neg_top_quantile_mean_response" calculates the negative of the sample weighted mean response for observations with predictions in the top quantile (as specified by the ***quantile*** parameter). For example, if ***quantile*** is 0.95, this metric will be the negative of the sample weighted mean response for the 5% of observations with the highest predictions. "bottom_quantile_mean_response" calculates the sample weighted mean response for observations with predictions in the bottom quantile (as specified by the ***quantile*** parameter). For example, if ***quantile*** is 0.05, this metric will be the sample weighted mean response for the 5% of observations with the lowest predictions. For "custom_function" see ***calculate_custom_validation_error_function*** below.
5454

5555
#### quantile (default = 0.5)
56-
Specifies the quantile to use when ***loss_function*** is "quantile".
56+
Specifies the quantile to use when ***loss_function*** is "quantile" or when ***validation_tuning_metric*** is "neg_top_quantile_mean_response" or "bottom_quantile_mean_response".
5757

5858
#### calculate_custom_validation_error_function (default = None)
5959
A Python function that calculates validation error if ***validation_tuning_metric*** is "custom_function". Example:

cpp/APLRRegressor.h

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ class APLRRegressor
143143
void update_gradient_and_errors();
144144
void add_new_term(size_t boosting_step);
145145
void update_coefficient_steps(size_t boosting_step);
146+
double calculate_quantile_mean_response(const VectorXd &predictions, bool top_quantile);
146147
void calculate_and_validate_validation_error(size_t boosting_step);
147148
double calculate_validation_error(const VectorXd &predictions);
148149
double calculate_group_mse_by_prediction_validation_error(const VectorXd &predictions);
@@ -190,6 +191,7 @@ class APLRRegressor
190191
std::string compute_raw_base_term_name(const Term &term, const std::string &X_name);
191192
void throw_error_if_m_is_invalid();
192193
bool model_has_not_been_trained();
194+
void throw_error_if_quantile_is_invalid();
193195
std::vector<size_t> compute_relevant_term_indexes(const std::string &unique_term_affiliation);
194196
std::vector<double> compute_split_points(size_t predictor_index, const std::vector<size_t> &relevant_term_indexes);
195197
VectorXd compute_contribution_to_linear_predictor_from_specific_terms(const MatrixXd &X, const std::vector<size_t> &term_indexes,
@@ -390,6 +392,7 @@ void APLRRegressor::fit(const MatrixXd &X, const VectorXd &y, const VectorXd &sa
390392
throw_error_if_loss_function_does_not_exist();
391393
throw_error_if_link_function_does_not_exist();
392394
throw_error_if_dispersion_parameter_is_invalid();
395+
throw_error_if_quantile_is_invalid();
393396
throw_error_if_m_is_invalid();
394397
validate_input_to_fit(X, y, sample_weight, X_names, cv_observations, prioritized_predictors_indexes, monotonic_constraints, group,
395398
interaction_constraints, other_data, predictor_learning_rates, predictor_penalties_for_non_linearity,
@@ -606,6 +609,17 @@ void APLRRegressor::throw_error_if_m_is_invalid()
606609
throw std::runtime_error("The maximum number of boosting steps, m, must be at least 1.");
607610
}
608611

612+
void APLRRegressor::throw_error_if_quantile_is_invalid()
613+
{
614+
if (loss_function == "quantile" || validation_tuning_metric == "neg_top_quantile_mean_response" || validation_tuning_metric == "bottom_quantile_mean_response")
615+
{
616+
if (quantile < 0.0 || quantile > 1.0)
617+
{
618+
throw std::runtime_error("Quantile must be between 0.0 and 1.0.");
619+
}
620+
}
621+
}
622+
609623
void APLRRegressor::validate_input_to_fit(const MatrixXd &X, const VectorXd &y, const VectorXd &sample_weight,
610624
const std::vector<std::string> &X_names, const MatrixXi &cv_observations,
611625
const std::vector<size_t> &prioritized_predictors_indexes, const std::vector<int> &monotonic_constraints, const VectorXi &group,
@@ -1720,6 +1734,31 @@ void APLRRegressor::update_coefficient_steps(size_t boosting_step)
17201734
}
17211735
}
17221736

1737+
double APLRRegressor::calculate_quantile_mean_response(const VectorXd &predictions, bool top_quantile)
1738+
{
1739+
double quantile_value{calculate_quantile(predictions, quantile, sample_weight_validation)};
1740+
1741+
VectorXd predictions_in_quantile;
1742+
if (top_quantile)
1743+
{
1744+
predictions_in_quantile = (predictions.array() >= quantile_value).cast<double>();
1745+
}
1746+
else
1747+
{
1748+
predictions_in_quantile = (predictions.array() <= quantile_value).cast<double>();
1749+
}
1750+
1751+
VectorXd y_in_quantile{y_validation.array() * predictions_in_quantile.array()};
1752+
VectorXd weights_in_quantile{sample_weight_validation.array() * predictions_in_quantile.array()};
1753+
1754+
double mean_response{calculate_weighted_average(y_in_quantile, weights_in_quantile)};
1755+
1756+
if (std::isnan(mean_response))
1757+
return std::numeric_limits<double>::infinity();
1758+
1759+
return mean_response;
1760+
}
1761+
17231762
void APLRRegressor::calculate_and_validate_validation_error(size_t boosting_step)
17241763
{
17251764
validation_error_steps.col(0)[boosting_step] = calculate_validation_error(predictions_current_validation);
@@ -1784,6 +1823,19 @@ double APLRRegressor::calculate_validation_error(const VectorXd &predictions)
17841823
throw std::runtime_error(error_msg);
17851824
}
17861825
}
1826+
else if (validation_tuning_metric == "neg_top_quantile_mean_response")
1827+
{
1828+
double mean_response{calculate_quantile_mean_response(predictions, true)};
1829+
if (std::isinf(mean_response))
1830+
{
1831+
return mean_response;
1832+
}
1833+
return -mean_response;
1834+
}
1835+
else if (validation_tuning_metric == "bottom_quantile_mean_response")
1836+
{
1837+
return calculate_quantile_mean_response(predictions, false);
1838+
}
17871839
else
17881840
throw std::runtime_error(validation_tuning_metric + " is an invalid validation_tuning_metric.");
17891841
}

cpp/functions.h

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,27 @@ double calculate_mean_error(const VectorXd &errors, const VectorXd &sample_weigh
280280
return error;
281281
}
282282

283+
double calculate_weighted_average(const VectorXd &values, const VectorXd &weights)
284+
{
285+
if (values.size() != weights.size())
286+
{
287+
throw std::runtime_error("Values and weights must have the same size for weighted average calculation.");
288+
}
289+
if (values.size() == 0)
290+
{
291+
return NAN_DOUBLE;
292+
}
293+
294+
double total_weight = weights.sum();
295+
if (is_approximately_zero(total_weight))
296+
{
297+
return NAN_DOUBLE;
298+
}
299+
300+
double weighted_sum = (values.array() * weights.array()).sum();
301+
return weighted_sum / total_weight;
302+
}
303+
283304
double calculate_sum_error(const VectorXd &errors)
284305
{
285306
double error{errors.sum()};
@@ -566,4 +587,96 @@ MatrixXd generate_combinations_and_one_additional_column(const std::vector<std::
566587
}
567588
}
568589
return result;
590+
}
591+
592+
double calculate_quantile(const VectorXd &vector, double quantile, const VectorXd &sample_weight = VectorXd(0))
593+
{
594+
if (quantile < 0.0 || quantile > 1.0)
595+
{
596+
throw std::runtime_error("Quantile must be between 0.0 and 1.0.");
597+
}
598+
599+
const Eigen::Index n = vector.size();
600+
if (n == 0)
601+
{
602+
return NAN_DOUBLE;
603+
}
604+
605+
VectorXd sample_weight_used;
606+
if (sample_weight.size() > 0)
607+
{
608+
if (sample_weight.size() != n)
609+
{
610+
throw std::runtime_error("Vector and sample_weight must have the same size.");
611+
}
612+
sample_weight_used = sample_weight;
613+
}
614+
else
615+
{
616+
sample_weight_used = VectorXd::Constant(n, 1.0);
617+
}
618+
619+
if ((sample_weight_used.array() < 0.0).any())
620+
{
621+
throw std::runtime_error("Sample weights must be non-negative.");
622+
}
623+
624+
double total_weight = sample_weight_used.sum();
625+
if (is_approximately_zero(total_weight))
626+
{
627+
return NAN_DOUBLE;
628+
}
629+
630+
if (n == 1)
631+
{
632+
return vector[0];
633+
}
634+
635+
std::vector<std::pair<double, double>> weighted_values(n);
636+
for (Eigen::Index i = 0; i < n; ++i)
637+
{
638+
weighted_values[i] = {vector[i], sample_weight_used[i]};
639+
}
640+
641+
std::sort(weighted_values.begin(), weighted_values.end(),
642+
[](const auto &a, const auto &b)
643+
{
644+
return a.first < b.first;
645+
});
646+
647+
VectorXd quantile_positions(n);
648+
double cum_weight = 0.0;
649+
for (Eigen::Index i = 0; i < n; ++i)
650+
{
651+
double current_weight = weighted_values[i].second;
652+
cum_weight += current_weight;
653+
quantile_positions[i] = (cum_weight - 0.5 * current_weight) / total_weight;
654+
}
655+
656+
auto it = std::upper_bound(quantile_positions.data(), quantile_positions.data() + n, quantile);
657+
Eigen::Index upper_index = std::distance(quantile_positions.data(), it);
658+
659+
if (upper_index == 0)
660+
{
661+
return weighted_values[0].first;
662+
}
663+
if (upper_index >= n)
664+
{
665+
return weighted_values[n - 1].first;
666+
}
667+
668+
Eigen::Index lower_index = upper_index - 1;
669+
670+
double q_lower = quantile_positions[lower_index];
671+
double q_upper = quantile_positions[upper_index];
672+
double val_lower = weighted_values[lower_index].first;
673+
double val_upper = weighted_values[upper_index].first;
674+
675+
if (is_approximately_equal(q_lower, q_upper))
676+
{
677+
return val_lower;
678+
}
679+
680+
double fraction = (quantile - q_lower) / (q_upper - q_lower);
681+
return val_lower + fraction * (val_upper - val_lower);
569682
}

cpp/tests.cpp

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,6 +1637,88 @@ class Tests
16371637
tests.push_back(is_approximately_equal(predictions.mean(), 23.646255799722155));
16381638
}
16391639

1640+
void test_aplrregressor_neg_top_quantile_mean_response()
1641+
{
1642+
// Model
1643+
APLRRegressor model{APLRRegressor()};
1644+
model.m = 100;
1645+
model.v = 1.0;
1646+
model.bins = 10;
1647+
model.n_jobs = 1;
1648+
model.loss_function = "mse";
1649+
model.validation_tuning_metric = "neg_top_quantile_mean_response";
1650+
model.verbosity = 3;
1651+
model.max_interaction_level = 100;
1652+
model.max_interactions = 30;
1653+
model.min_observations_in_split = 50;
1654+
model.ineligible_boosting_steps_added = 10;
1655+
model.max_eligible_terms = 5;
1656+
model.quantile = 0.8;
1657+
model.ridge_penalty = 0.0;
1658+
1659+
// Data
1660+
MatrixXd X_train{load_csv_into_eigen_matrix<MatrixXd>("data/X_train.csv")};
1661+
MatrixXd X_test{load_csv_into_eigen_matrix<MatrixXd>("data/X_test.csv")};
1662+
VectorXd y_train{load_csv_into_eigen_matrix<MatrixXd>("data/y_train.csv")};
1663+
VectorXd y_test{load_csv_into_eigen_matrix<MatrixXd>("data/y_test.csv")};
1664+
1665+
VectorXd sample_weight{VectorXd::Constant(y_train.size(), 0.5)};
1666+
1667+
// Fitting
1668+
model.fit(X_train, y_train, sample_weight);
1669+
std::cout << "feature importance\n"
1670+
<< model.feature_importance << "\n\n";
1671+
1672+
VectorXd predictions{model.predict(X_test)};
1673+
1674+
// Saving results
1675+
save_as_csv_file("data/output.csv", predictions);
1676+
1677+
std::cout << predictions.mean() << "\n\n";
1678+
tests.push_back(is_approximately_equal(predictions.mean(), 23.609343969688034));
1679+
}
1680+
1681+
void test_aplrregressor_bottom_quantile_mean_response()
1682+
{
1683+
// Model
1684+
APLRRegressor model{APLRRegressor()};
1685+
model.m = 100;
1686+
model.v = 1.0;
1687+
model.bins = 10;
1688+
model.n_jobs = 1;
1689+
model.loss_function = "mse";
1690+
model.validation_tuning_metric = "bottom_quantile_mean_response";
1691+
model.verbosity = 3;
1692+
model.max_interaction_level = 100;
1693+
model.max_interactions = 30;
1694+
model.min_observations_in_split = 50;
1695+
model.ineligible_boosting_steps_added = 10;
1696+
model.max_eligible_terms = 5;
1697+
model.quantile = 0.2;
1698+
model.ridge_penalty = 0.0;
1699+
1700+
// Data
1701+
MatrixXd X_train{load_csv_into_eigen_matrix<MatrixXd>("data/X_train.csv")};
1702+
MatrixXd X_test{load_csv_into_eigen_matrix<MatrixXd>("data/X_test.csv")};
1703+
VectorXd y_train{load_csv_into_eigen_matrix<MatrixXd>("data/y_train.csv")};
1704+
VectorXd y_test{load_csv_into_eigen_matrix<MatrixXd>("data/y_test.csv")};
1705+
1706+
VectorXd sample_weight{VectorXd::Constant(y_train.size(), 0.5)};
1707+
1708+
// Fitting
1709+
model.fit(X_train, y_train, sample_weight);
1710+
std::cout << "feature importance\n"
1711+
<< model.feature_importance << "\n\n";
1712+
1713+
VectorXd predictions{model.predict(X_test)};
1714+
1715+
// Saving results
1716+
save_as_csv_file("data/output.csv", predictions);
1717+
1718+
std::cout << predictions.mean() << "\n\n";
1719+
tests.push_back(is_approximately_equal(predictions.mean(), 23.273887245225175));
1720+
}
1721+
16401722
void test_aplrregressor_weibull()
16411723
{
16421724
// Model
@@ -2685,6 +2767,8 @@ int main()
26852767
tests.test_aplrregressor_poisson();
26862768
tests.test_aplrregressor_poissongamma();
26872769
tests.test_aplrregressor_quantile();
2770+
tests.test_aplrregressor_neg_top_quantile_mean_response();
2771+
tests.test_aplrregressor_bottom_quantile_mean_response();
26882772
tests.test_aplrregressor_weibull();
26892773
tests.test_aplrregressor();
26902774
tests.test_aplr_classifier_multi_class_other_params();
Binary file not shown.

setup.py

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

2929
setuptools.setup(
3030
name="aplr",
31-
version="10.11.1",
31+
version="10.12.0",
3232
description="Automatic Piecewise Linear Regression",
3333
ext_modules=[sfc_module],
3434
author="Mathias von Ottenbreit",

0 commit comments

Comments
 (0)