statcpp
C++17 Header-Only Statistics Library
Loading...
Searching...
No Matches
model_selection.hpp
Go to the documentation of this file.
1
9#pragma once
10
15
16#include <algorithm>
17#include <cmath>
18#include <cstddef>
19#include <iterator>
20#include <limits>
21#include <numeric>
22#include <stdexcept>
23#include <utility>
24#include <vector>
25
26namespace statcpp {
27
28// ============================================================================
29// Model Selection Criteria
30// ============================================================================
31
41inline double aic(double log_likelihood, std::size_t k)
42{
43 return -2.0 * log_likelihood + 2.0 * static_cast<double>(k);
44}
45
55inline double aic_linear(const simple_regression_result& model, std::size_t n)
56{
57 // sigma^2 = SS_res / n (MLE version)
58 double sigma2 = model.ss_residual / static_cast<double>(n);
59 double n_d = static_cast<double>(n);
60
61 // Log-likelihood: -n/2 * (log(2*pi) + log(sigma^2) + 1)
62 double ll = -0.5 * n_d * (std::log(2.0 * pi) + std::log(sigma2) + 1.0);
63
64 return aic(ll, 3); // k = 2 (coefficients) + 1 (sigma^2)
65}
66
76inline double aic_linear(const multiple_regression_result& model, std::size_t n)
77{
78 double sigma2 = model.ss_residual / static_cast<double>(n);
79 double n_d = static_cast<double>(n);
80
81 double ll = -0.5 * n_d * (std::log(2.0 * pi) + std::log(sigma2) + 1.0);
82
83 std::size_t k = model.coefficients.size() + 1; // coefficients + sigma^2
84 return aic(ll, k);
85}
86
99inline double aicc(double log_likelihood, std::size_t n, std::size_t k)
100{
101 double n_d = static_cast<double>(n);
102 double k_d = static_cast<double>(k);
103
104 if (n_d <= k_d + 1.0) {
105 throw std::invalid_argument("statcpp::aicc: n must be greater than k + 1");
106 }
107
108 return aic(log_likelihood, k) + (2.0 * k_d * (k_d + 1.0)) / (n_d - k_d - 1.0);
109}
110
122inline double bic(double log_likelihood, std::size_t n, std::size_t k)
123{
124 return -2.0 * log_likelihood + static_cast<double>(k) * std::log(static_cast<double>(n));
125}
126
136inline double bic_linear(const simple_regression_result& model, std::size_t n)
137{
138 double sigma2 = model.ss_residual / static_cast<double>(n);
139 double n_d = static_cast<double>(n);
140
141 double ll = -0.5 * n_d * (std::log(2.0 * pi) + std::log(sigma2) + 1.0);
142
143 return bic(ll, n, 3);
144}
145
155inline double bic_linear(const multiple_regression_result& model, std::size_t n)
156{
157 double sigma2 = model.ss_residual / static_cast<double>(n);
158 double n_d = static_cast<double>(n);
159
160 double ll = -0.5 * n_d * (std::log(2.0 * pi) + std::log(sigma2) + 1.0);
161
162 std::size_t k = model.coefficients.size() + 1;
163 return bic(ll, n, k);
164}
165
182template <typename IteratorX, typename IteratorY>
183double press_statistic(IteratorX x_first, IteratorX x_last,
184 IteratorY y_first, IteratorY y_last,
185 const simple_regression_result& model)
186{
187 auto n = statcpp::count(x_first, x_last);
188 auto n_y = statcpp::count(y_first, y_last);
189 if (n != n_y) {
190 throw std::invalid_argument("statcpp::press_statistic: x and y must have same length");
191 }
192
193 double n_d = static_cast<double>(n);
194 double mean_x = statcpp::mean(x_first, x_last);
195
196 // Calculate Sxx
197 double sxx = 0.0;
198 for (auto it = x_first; it != x_last; ++it) {
199 double dx = static_cast<double>(*it) - mean_x;
200 sxx += dx * dx;
201 }
202
203 double press = 0.0;
204 auto it_x = x_first;
205 auto it_y = y_first;
206 for (; it_x != x_last; ++it_x, ++it_y) {
207 double x_i = static_cast<double>(*it_x);
208 double y_i = static_cast<double>(*it_y);
209
210 double y_hat = predict(model, x_i);
211 double residual = y_i - y_hat;
212
213 // Leverage h_ii
214 double dx = x_i - mean_x;
215 double h_ii = 1.0 / n_d + dx * dx / sxx;
216
217 // PRESS residual = residual / (1 - h_ii)
218 double press_residual = residual / (1.0 - h_ii);
219 press += press_residual * press_residual;
220 }
221
222 return press;
223}
224
225// ============================================================================
226// Cross-Validation
227// ============================================================================
228
232struct cv_result {
233 double mean_error;
234 double se_error;
235 std::vector<double> fold_errors;
236 std::size_t n_folds;
237};
238
250inline std::vector<std::vector<std::size_t>> create_cv_folds(
251 std::size_t n, std::size_t k, bool shuffle = true)
252{
253 if (k < 2) {
254 throw std::invalid_argument("statcpp::create_cv_folds: k must be at least 2");
255 }
256 if (k > n) {
257 throw std::invalid_argument("statcpp::create_cv_folds: k cannot exceed n");
258 }
259
260 std::vector<std::size_t> indices(n);
261 std::iota(indices.begin(), indices.end(), 0);
262
263 if (shuffle) {
264 std::shuffle(indices.begin(), indices.end(), get_random_engine());
265 }
266
267 std::vector<std::vector<std::size_t>> folds(k);
268 std::size_t fold_size = n / k;
269 std::size_t remainder = n % k;
270
271 std::size_t current = 0;
272 for (std::size_t i = 0; i < k; ++i) {
273 std::size_t this_fold_size = fold_size + (i < remainder ? 1 : 0);
274 for (std::size_t j = 0; j < this_fold_size; ++j) {
275 folds[i].push_back(indices[current++]);
276 }
277 }
278
279 return folds;
280}
281
299 const std::vector<std::vector<double>>& X,
300 const std::vector<double>& y,
301 std::size_t k = 5,
302 bool shuffle = true)
303{
304 std::size_t n = X.size();
305 if (n != y.size()) {
306 throw std::invalid_argument("statcpp::cross_validate_linear: X and y must have same size");
307 }
308
309 auto folds = create_cv_folds(n, k, shuffle);
310 std::vector<double> fold_errors(k);
311
312 for (std::size_t fold = 0; fold < k; ++fold) {
313 // Separate test and training sets
314 std::vector<std::size_t> test_idx = folds[fold];
315 std::vector<std::size_t> train_idx;
316 for (std::size_t f = 0; f < k; ++f) {
317 if (f != fold) {
318 train_idx.insert(train_idx.end(), folds[f].begin(), folds[f].end());
319 }
320 }
321
322 // Training data
323 std::vector<std::vector<double>> X_train(train_idx.size());
324 std::vector<double> y_train(train_idx.size());
325 for (std::size_t i = 0; i < train_idx.size(); ++i) {
326 X_train[i] = X[train_idx[i]];
327 y_train[i] = y[train_idx[i]];
328 }
329
330 // Test data
331 std::vector<std::vector<double>> X_test(test_idx.size());
332 std::vector<double> y_test(test_idx.size());
333 for (std::size_t i = 0; i < test_idx.size(); ++i) {
334 X_test[i] = X[test_idx[i]];
335 y_test[i] = y[test_idx[i]];
336 }
337
338 // Fit model
339 try {
340 auto model = multiple_linear_regression(X_train, y_train);
341
342 // Calculate test error
343 double mse = 0.0;
344 for (std::size_t i = 0; i < test_idx.size(); ++i) {
345 double pred = predict(model, X_test[i]);
346 double err = y_test[i] - pred;
347 mse += err * err;
348 }
349 fold_errors[fold] = mse / static_cast<double>(test_idx.size());
350 } catch (...) {
351 fold_errors[fold] = std::numeric_limits<double>::infinity();
352 }
353 }
354
355 double mean_error = statcpp::mean(fold_errors.begin(), fold_errors.end());
356 double se_error = statcpp::sample_stddev(fold_errors.begin(), fold_errors.end())
357 / std::sqrt(static_cast<double>(k));
358
359 return {mean_error, se_error, fold_errors, k};
360}
361
372 const std::vector<std::vector<double>>& X,
373 const std::vector<double>& y)
374{
375 return cross_validate_linear(X, y, X.size());
376}
377
378// ============================================================================
379// Regularized Regression
380// ============================================================================
381
386 std::vector<double> coefficients;
387 double lambda;
388 double mse;
389 std::size_t iterations;
391};
392
393namespace detail {
394
399 const std::vector<std::vector<double>>& X,
400 std::vector<std::vector<double>>& X_scaled,
401 std::vector<double>& X_mean,
402 std::vector<double>& X_std,
403 std::size_t n, std::size_t p)
404{
405 for (std::size_t j = 0; j < p; ++j) {
406 double sum = 0.0;
407 for (std::size_t i = 0; i < n; ++i) {
408 sum += X[i][j];
409 }
410 X_mean[j] = sum / static_cast<double>(n);
411
412 double ss = 0.0;
413 for (std::size_t i = 0; i < n; ++i) {
414 double d = X[i][j] - X_mean[j];
415 ss += d * d;
416 }
417 X_std[j] = std::sqrt(ss / static_cast<double>(n));
418 if (X_std[j] < 1e-10) X_std[j] = 1.0;
419
420 for (std::size_t i = 0; i < n; ++i) {
421 X_scaled[i][j] = (X[i][j] - X_mean[j]) / X_std[j];
422 }
423 }
424}
425
429inline std::vector<double> rescale_coefficients(
430 const std::vector<double>& beta,
431 const std::vector<double>& X_mean,
432 const std::vector<double>& X_std,
433 double y_mean, std::size_t p, bool standardize)
434{
435 std::vector<double> coefficients(p + 1);
436 if (standardize) {
437 coefficients[0] = y_mean;
438 for (std::size_t j = 0; j < p; ++j) {
439 coefficients[j + 1] = beta[j] / X_std[j];
440 coefficients[0] -= coefficients[j + 1] * X_mean[j];
441 }
442 } else {
443 coefficients[0] = y_mean;
444 for (std::size_t j = 0; j < p; ++j) {
445 coefficients[j + 1] = beta[j];
446 }
447 }
448 return coefficients;
449}
450
451} // namespace detail
452
469 const std::vector<std::vector<double>>& X,
470 const std::vector<double>& y,
471 double lambda,
472 bool standardize = true,
473 std::size_t max_iter = 1000,
474 double tol = 1e-6)
475{
476 // Verify that X does not contain an intercept column (from linear_regression.hpp)
478
479 if (lambda < 0.0) {
480 throw std::invalid_argument("statcpp::ridge_regression: lambda must be non-negative");
481 }
482
483 std::size_t n = X.size();
484 if (n == 0) {
485 throw std::invalid_argument("statcpp::ridge_regression: empty data");
486 }
487 if (n != y.size()) {
488 throw std::invalid_argument("statcpp::ridge_regression: X and y must have same size");
489 }
490
491 std::size_t p = X[0].size();
492
493 // Data standardization
494 std::vector<double> X_mean(p, 0.0);
495 std::vector<double> X_std(p, 1.0);
496 double y_mean = statcpp::mean(y.begin(), y.end());
497
498 std::vector<std::vector<double>> X_scaled = X;
499 std::vector<double> y_centered(n);
500
501 if (standardize) {
502 detail::standardize_features(X, X_scaled, X_mean, X_std, n, p);
503 }
504
505 for (std::size_t i = 0; i < n; ++i) {
506 y_centered[i] = y[i] - y_mean;
507 }
508
509 // Ridge closed-form solution: beta = (X'X + lambda*I)^{-1} X'y
510 // Solve with coordinate descent
511 std::vector<double> beta(p, 0.0);
512 std::vector<double> residuals = y_centered;
513
514 std::size_t iter = 0;
515 bool converged = false;
516
517 for (iter = 0; iter < max_iter; ++iter) {
518 double max_change = 0.0;
519
520 for (std::size_t j = 0; j < p; ++j) {
521 // Add back the contribution of this variable to residuals
522 for (std::size_t i = 0; i < n; ++i) {
523 residuals[i] += X_scaled[i][j] * beta[j];
524 }
525
526 // Calculate X_j'r
527 double xr = 0.0;
528 double xx = 0.0;
529 for (std::size_t i = 0; i < n; ++i) {
530 xr += X_scaled[i][j] * residuals[i];
531 xx += X_scaled[i][j] * X_scaled[i][j];
532 }
533
534 // Ridge update
535 double beta_new = xr / (xx + lambda);
536 double change = std::abs(beta_new - beta[j]);
537 max_change = std::max(max_change, change);
538
539 beta[j] = beta_new;
540
541 // Update residuals
542 for (std::size_t i = 0; i < n; ++i) {
543 residuals[i] -= X_scaled[i][j] * beta[j];
544 }
545 }
546
547 if (max_change < tol) {
548 converged = true;
549 ++iter;
550 break;
551 }
552 }
553
554 // Transform coefficients back to original scale
555 auto coefficients = detail::rescale_coefficients(beta, X_mean, X_std, y_mean, p, standardize);
556
557 // Calculate MSE
558 double mse = 0.0;
559 for (std::size_t i = 0; i < n; ++i) {
560 double pred = coefficients[0];
561 for (std::size_t j = 0; j < p; ++j) {
562 pred += coefficients[j + 1] * X[i][j];
563 }
564 double err = y[i] - pred;
565 mse += err * err;
566 }
567 mse /= static_cast<double>(n);
568
569 return {coefficients, lambda, mse, iter, converged};
570}
571
588 const std::vector<std::vector<double>>& X,
589 const std::vector<double>& y,
590 double lambda,
591 bool standardize = true,
592 std::size_t max_iter = 1000,
593 double tol = 1e-6)
594{
595 // Verify that X does not contain an intercept column
597
598 if (lambda < 0.0) {
599 throw std::invalid_argument("statcpp::lasso_regression: lambda must be non-negative");
600 }
601
602 std::size_t n = X.size();
603 if (n == 0) {
604 throw std::invalid_argument("statcpp::lasso_regression: empty data");
605 }
606 if (n != y.size()) {
607 throw std::invalid_argument("statcpp::lasso_regression: X and y must have same size");
608 }
609
610 std::size_t p = X[0].size();
611
612 // Data standardization
613 std::vector<double> X_mean(p, 0.0);
614 std::vector<double> X_std(p, 1.0);
615 double y_mean = statcpp::mean(y.begin(), y.end());
616
617 std::vector<std::vector<double>> X_scaled = X;
618 std::vector<double> y_centered(n);
619
620 if (standardize) {
621 detail::standardize_features(X, X_scaled, X_mean, X_std, n, p);
622 }
623
624 for (std::size_t i = 0; i < n; ++i) {
625 y_centered[i] = y[i] - y_mean;
626 }
627
628 // Coordinate descent
629 std::vector<double> beta(p, 0.0);
630 std::vector<double> residuals = y_centered;
631
632 // Soft thresholding function
633 auto soft_threshold = [](double x, double t) -> double {
634 if (x > t) return x - t;
635 if (x < -t) return x + t;
636 return 0.0;
637 };
638
639 std::size_t iter = 0;
640 bool converged = false;
641
642 for (iter = 0; iter < max_iter; ++iter) {
643 double max_change = 0.0;
644
645 for (std::size_t j = 0; j < p; ++j) {
646 // Add back the contribution of this variable to residuals
647 for (std::size_t i = 0; i < n; ++i) {
648 residuals[i] += X_scaled[i][j] * beta[j];
649 }
650
651 // Calculate X_j'r
652 double xr = 0.0;
653 double xx = 0.0;
654 for (std::size_t i = 0; i < n; ++i) {
655 xr += X_scaled[i][j] * residuals[i];
656 xx += X_scaled[i][j] * X_scaled[i][j];
657 }
658
659 // Lasso update (soft thresholding)
660 double beta_new = soft_threshold(xr, lambda) / xx;
661 double change = std::abs(beta_new - beta[j]);
662 max_change = std::max(max_change, change);
663
664 beta[j] = beta_new;
665
666 // Update residuals
667 for (std::size_t i = 0; i < n; ++i) {
668 residuals[i] -= X_scaled[i][j] * beta[j];
669 }
670 }
671
672 if (max_change < tol) {
673 converged = true;
674 ++iter;
675 break;
676 }
677 }
678
679 // Transform coefficients back to original scale
680 auto coefficients = detail::rescale_coefficients(beta, X_mean, X_std, y_mean, p, standardize);
681
682 // Calculate MSE
683 double mse = 0.0;
684 for (std::size_t i = 0; i < n; ++i) {
685 double pred = coefficients[0];
686 for (std::size_t j = 0; j < p; ++j) {
687 pred += coefficients[j + 1] * X[i][j];
688 }
689 double err = y[i] - pred;
690 mse += err * err;
691 }
692 mse /= static_cast<double>(n);
693
694 return {coefficients, lambda, mse, iter, converged};
695}
696
714 const std::vector<std::vector<double>>& X,
715 const std::vector<double>& y,
716 double lambda,
717 double alpha = 0.5, // L1 ratio (0 = Ridge, 1 = Lasso)
718 bool standardize = true,
719 std::size_t max_iter = 1000,
720 double tol = 1e-6)
721{
722 // Verify that X does not contain an intercept column
723 statcpp::detail::validate_no_intercept_column(X, "elastic_net_regression");
724
725 if (lambda < 0.0) {
726 throw std::invalid_argument("statcpp::elastic_net_regression: lambda must be non-negative");
727 }
728 if (alpha < 0.0 || alpha > 1.0) {
729 throw std::invalid_argument("statcpp::elastic_net_regression: alpha must be in [0, 1]");
730 }
731
732 std::size_t n = X.size();
733 if (n == 0) {
734 throw std::invalid_argument("statcpp::elastic_net_regression: empty data");
735 }
736 if (n != y.size()) {
737 throw std::invalid_argument("statcpp::elastic_net_regression: X and y must have same size");
738 }
739
740 std::size_t p = X[0].size();
741
742 // Data standardization
743 std::vector<double> X_mean(p, 0.0);
744 std::vector<double> X_std(p, 1.0);
745 double y_mean = statcpp::mean(y.begin(), y.end());
746
747 std::vector<std::vector<double>> X_scaled = X;
748 std::vector<double> y_centered(n);
749
750 if (standardize) {
751 detail::standardize_features(X, X_scaled, X_mean, X_std, n, p);
752 }
753
754 for (std::size_t i = 0; i < n; ++i) {
755 y_centered[i] = y[i] - y_mean;
756 }
757
758 // Coordinate descent
759 std::vector<double> beta(p, 0.0);
760 std::vector<double> residuals = y_centered;
761
762 auto soft_threshold = [](double x, double t) -> double {
763 if (x > t) return x - t;
764 if (x < -t) return x + t;
765 return 0.0;
766 };
767
768 double lambda1 = alpha * lambda; // L1 penalty
769 double lambda2 = (1.0 - alpha) * lambda; // L2 penalty
770
771 std::size_t iter = 0;
772 bool converged = false;
773
774 for (iter = 0; iter < max_iter; ++iter) {
775 double max_change = 0.0;
776
777 for (std::size_t j = 0; j < p; ++j) {
778 for (std::size_t i = 0; i < n; ++i) {
779 residuals[i] += X_scaled[i][j] * beta[j];
780 }
781
782 double xr = 0.0;
783 double xx = 0.0;
784 for (std::size_t i = 0; i < n; ++i) {
785 xr += X_scaled[i][j] * residuals[i];
786 xx += X_scaled[i][j] * X_scaled[i][j];
787 }
788
789 // Elastic Net update
790 double beta_new = soft_threshold(xr, lambda1) / (xx + lambda2);
791 double change = std::abs(beta_new - beta[j]);
792 max_change = std::max(max_change, change);
793
794 beta[j] = beta_new;
795
796 for (std::size_t i = 0; i < n; ++i) {
797 residuals[i] -= X_scaled[i][j] * beta[j];
798 }
799 }
800
801 if (max_change < tol) {
802 converged = true;
803 ++iter;
804 break;
805 }
806 }
807
808 // Transform coefficients back to original scale
809 auto coefficients = detail::rescale_coefficients(beta, X_mean, X_std, y_mean, p, standardize);
810
811 // Calculate MSE
812 double mse = 0.0;
813 for (std::size_t i = 0; i < n; ++i) {
814 double pred = coefficients[0];
815 for (std::size_t j = 0; j < p; ++j) {
816 pred += coefficients[j + 1] * X[i][j];
817 }
818 double err = y[i] - pred;
819 mse += err * err;
820 }
821 mse /= static_cast<double>(n);
822
823 return {coefficients, lambda, mse, iter, converged};
824}
825
826// ============================================================================
827// Lambda Selection (Regularization Parameter Selection)
828// ============================================================================
829
842inline std::pair<double, std::vector<double>> cv_ridge(
843 const std::vector<std::vector<double>>& X,
844 const std::vector<double>& y,
845 const std::vector<double>& lambda_grid,
846 std::size_t k = 5,
847 bool shuffle = true)
848{
849 std::vector<double> cv_errors(lambda_grid.size());
850
851 for (std::size_t l = 0; l < lambda_grid.size(); ++l) {
852 double lambda = lambda_grid[l];
853 auto folds = create_cv_folds(X.size(), k, shuffle);
854
855 double total_error = 0.0;
856 for (std::size_t fold = 0; fold < k; ++fold) {
857 std::vector<std::size_t> test_idx = folds[fold];
858 std::vector<std::size_t> train_idx;
859 for (std::size_t f = 0; f < k; ++f) {
860 if (f != fold) {
861 train_idx.insert(train_idx.end(), folds[f].begin(), folds[f].end());
862 }
863 }
864
865 std::vector<std::vector<double>> X_train(train_idx.size());
866 std::vector<double> y_train(train_idx.size());
867 for (std::size_t i = 0; i < train_idx.size(); ++i) {
868 X_train[i] = X[train_idx[i]];
869 y_train[i] = y[train_idx[i]];
870 }
871
872 try {
873 auto model = ridge_regression(X_train, y_train, lambda);
874
875 double mse = 0.0;
876 for (std::size_t i : test_idx) {
877 double pred = model.coefficients[0];
878 for (std::size_t j = 0; j < X[i].size(); ++j) {
879 pred += model.coefficients[j + 1] * X[i][j];
880 }
881 double err = y[i] - pred;
882 mse += err * err;
883 }
884 total_error += mse / static_cast<double>(test_idx.size());
885 } catch (...) {
886 total_error += std::numeric_limits<double>::infinity();
887 }
888 }
889 cv_errors[l] = total_error / static_cast<double>(k);
890 }
891
892 // Select lambda with minimum error
893 auto min_it = std::min_element(cv_errors.begin(), cv_errors.end());
894 double best_lambda = lambda_grid[std::distance(cv_errors.begin(), min_it)];
895
896 return {best_lambda, cv_errors};
897}
898
911inline std::pair<double, std::vector<double>> cv_lasso(
912 const std::vector<std::vector<double>>& X,
913 const std::vector<double>& y,
914 const std::vector<double>& lambda_grid,
915 std::size_t k = 5,
916 bool shuffle = true)
917{
918 std::vector<double> cv_errors(lambda_grid.size());
919
920 for (std::size_t l = 0; l < lambda_grid.size(); ++l) {
921 double lambda = lambda_grid[l];
922 auto folds = create_cv_folds(X.size(), k, shuffle);
923
924 double total_error = 0.0;
925 for (std::size_t fold = 0; fold < k; ++fold) {
926 std::vector<std::size_t> test_idx = folds[fold];
927 std::vector<std::size_t> train_idx;
928 for (std::size_t f = 0; f < k; ++f) {
929 if (f != fold) {
930 train_idx.insert(train_idx.end(), folds[f].begin(), folds[f].end());
931 }
932 }
933
934 std::vector<std::vector<double>> X_train(train_idx.size());
935 std::vector<double> y_train(train_idx.size());
936 for (std::size_t i = 0; i < train_idx.size(); ++i) {
937 X_train[i] = X[train_idx[i]];
938 y_train[i] = y[train_idx[i]];
939 }
940
941 try {
942 auto model = lasso_regression(X_train, y_train, lambda);
943
944 double mse = 0.0;
945 for (std::size_t i : test_idx) {
946 double pred = model.coefficients[0];
947 for (std::size_t j = 0; j < X[i].size(); ++j) {
948 pred += model.coefficients[j + 1] * X[i][j];
949 }
950 double err = y[i] - pred;
951 mse += err * err;
952 }
953 total_error += mse / static_cast<double>(test_idx.size());
954 } catch (...) {
955 total_error += std::numeric_limits<double>::infinity();
956 }
957 }
958 cv_errors[l] = total_error / static_cast<double>(k);
959 }
960
961 auto min_it = std::min_element(cv_errors.begin(), cv_errors.end());
962 double best_lambda = lambda_grid[std::distance(cv_errors.begin(), min_it)];
963
964 return {best_lambda, cv_errors};
965}
966
979inline std::vector<double> generate_lambda_grid(
980 const std::vector<std::vector<double>>& X,
981 const std::vector<double>& y,
982 std::size_t n_lambda = 100,
983 double lambda_min_ratio = 0.0001)
984{
985 std::size_t n = X.size();
986 std::size_t p = X[0].size();
987
988 // Center y
989 double y_mean = statcpp::mean(y.begin(), y.end());
990
991 // Calculate lambda_max (lambda where all coefficients are zero)
992 double lambda_max = 0.0;
993 for (std::size_t j = 0; j < p; ++j) {
994 double xy = 0.0;
995 for (std::size_t i = 0; i < n; ++i) {
996 xy += X[i][j] * (y[i] - y_mean);
997 }
998 lambda_max = std::max(lambda_max, std::abs(xy) / static_cast<double>(n));
999 }
1000
1001 if (lambda_max <= 0.0) {
1002 throw std::invalid_argument(
1003 "statcpp::generate_lambda_grid: lambda_max must be positive (data may be constant)");
1004 }
1005
1006 if (n_lambda <= 1) {
1007 return {lambda_max};
1008 }
1009
1010 double lambda_min = lambda_max * lambda_min_ratio;
1011
1012 // Generate grid on logarithmic scale
1013 std::vector<double> grid(n_lambda);
1014 double log_max = std::log(lambda_max);
1015 double log_min = std::log(lambda_min);
1016 double step = (log_max - log_min) / static_cast<double>(n_lambda - 1);
1017
1018 for (std::size_t i = 0; i < n_lambda; ++i) {
1019 grid[i] = std::exp(log_max - static_cast<double>(i) * step);
1020 }
1021
1022 return grid;
1023}
1024
1025} // namespace statcpp
Basic statistical computation functions.
Dispersion and variance calculation functions.
Linear regression analysis.
void standardize_features(const std::vector< std::vector< double > > &X, std::vector< std::vector< double > > &X_scaled, std::vector< double > &X_mean, std::vector< double > &X_std, std::size_t n, std::size_t p)
特徴量の標準化 (平均0, 標準偏差1)
std::vector< double > rescale_coefficients(const std::vector< double > &beta, const std::vector< double > &X_mean, const std::vector< double > &X_std, double y_mean, std::size_t p, bool standardize)
標準化済み係数を元のスケールに逆変換する
void validate_no_intercept_column(const std::vector< std::vector< double > > &X, const char *func_name)
Check that X data does not contain an intercept column.
regularized_regression_result ridge_regression(const std::vector< std::vector< double > > &X, const std::vector< double > &y, double lambda, bool standardize=true, std::size_t max_iter=1000, double tol=1e-6)
Perform Ridge regression (L2 regularization)
double aicc(double log_likelihood, std::size_t n, std::size_t k)
Calculate AICc (corrected AIC)
double sample_stddev(Iterator first, Iterator last)
Sample standard deviation.
constexpr double pi
Pi constant.
cv_result loocv_linear(const std::vector< std::vector< double > > &X, const std::vector< double > &y)
Perform leave-one-out cross-validation.
regularized_regression_result lasso_regression(const std::vector< std::vector< double > > &X, const std::vector< double > &y, double lambda, bool standardize=true, std::size_t max_iter=1000, double tol=1e-6)
Perform Lasso regression (L1 regularization)
auto sum(Iterator first, Iterator last)
Sum.
cv_result cross_validate_linear(const std::vector< std::vector< double > > &X, const std::vector< double > &y, std::size_t k=5, bool shuffle=true)
Perform k-fold cross-validation for multiple regression model.
std::pair< double, std::vector< double > > cv_lasso(const std::vector< std::vector< double > > &X, const std::vector< double > &y, const std::vector< double > &lambda_grid, std::size_t k=5, bool shuffle=true)
Select optimal lambda for Lasso regression using cross-validation.
multiple_regression_result multiple_linear_regression(const std::vector< std::vector< double > > &X, const std::vector< double > &y)
Perform multiple linear regression.
double predict(const simple_regression_result &model, double x)
Make prediction using simple regression model.
double beta(double a, double b)
Beta function.
std::vector< std::vector< double > > standardize(const std::vector< std::vector< double > > &data)
Z-score standardization.
double bic_linear(const simple_regression_result &model, std::size_t n)
Calculate BIC from simple regression model.
double mean(Iterator first, Iterator last)
Arithmetic mean.
std::vector< double > generate_lambda_grid(const std::vector< std::vector< double > > &X, const std::vector< double > &y, std::size_t n_lambda=100, double lambda_min_ratio=0.0001)
Automatically generate lambda grid for regularized regression.
double bic(double log_likelihood, std::size_t n, std::size_t k)
Calculate BIC (Bayesian Information Criterion)
double aic(double log_likelihood, std::size_t k)
Calculate AIC (Akaike Information Criterion)
default_random_engine & get_random_engine()
Singleton accessor for global random engine.
std::vector< std::vector< std::size_t > > create_cv_folds(std::size_t n, std::size_t k, bool shuffle=true)
Generate indices for k-fold cross-validation.
double press_statistic(IteratorX x_first, IteratorX x_last, IteratorY y_first, IteratorY y_last, const simple_regression_result &model)
Calculate PRESS statistic.
double mse(Iterator1 first1, Iterator1 last1, Iterator2 first2)
Mean Squared Error (MSE)
std::pair< double, std::vector< double > > cv_ridge(const std::vector< std::vector< double > > &X, const std::vector< double > &y, const std::vector< double > &lambda_grid, std::size_t k=5, bool shuffle=true)
Select optimal lambda for Ridge regression using cross-validation.
std::size_t count(Iterator first, Iterator last)
Data count.
double aic_linear(const simple_regression_result &model, std::size_t n)
Calculate AIC from simple regression model.
regularized_regression_result elastic_net_regression(const std::vector< std::vector< double > > &X, const std::vector< double > &y, double lambda, double alpha=0.5, bool standardize=true, std::size_t max_iter=1000, double tol=1e-6)
Perform Elastic Net regression (L1 + L2 regularization)
Random engine wrapper and utilities.
Structure to store cross-validation results.
std::vector< double > fold_errors
Structure to store multiple regression analysis results.
std::vector< double > coefficients
Regression coefficients (b0, b1, ..., bp)
double ss_residual
Residual sum of squares.
Structure to store regularized regression results.
Structure to store simple regression analysis results.
double ss_residual
Residual sum of squares.