statcpp
C++17 Header-Only Statistics Library
Loading...
Searching...
No Matches
linear_regression.hpp
Go to the documentation of this file.
1
10#pragma once
11
15
16#include <cmath>
17#include <cstddef>
18#include <limits>
19#include <stdexcept>
20#include <string>
21#include <vector>
22
23namespace statcpp {
24
25// ============================================================================
26// Linear Regression Result Structures
27// ============================================================================
28
36 double intercept;
37 double slope;
38 double intercept_se;
39 double slope_se;
40 double intercept_t;
41 double slope_t;
42 double intercept_p;
43 double slope_p;
44 double r_squared;
46 double residual_se;
47 double f_statistic;
48 double f_p_value;
50 double df_residual;
51 double ss_total;
53 double ss_residual;
54};
55
63 std::vector<double> coefficients;
64 std::vector<double> coefficient_se;
65 std::vector<double> t_statistics;
66 std::vector<double> p_values;
67 double r_squared;
69 double residual_se;
70 double f_statistic;
71 double f_p_value;
73 double df_residual;
74 double ss_total;
76 double ss_residual;
77};
78
85 double prediction;
86 double lower;
87 double upper;
89};
90
98 std::vector<double> residuals;
99 std::vector<double> standardized_residuals;
100 std::vector<double> studentized_residuals;
101 std::vector<double> hat_values;
102 std::vector<double> cooks_distance;
104};
105
106// ============================================================================
107// Simple Linear Regression
108// ============================================================================
109
127template <typename IteratorX, typename IteratorY>
128simple_regression_result simple_linear_regression(IteratorX x_first, IteratorX x_last,
129 IteratorY y_first, IteratorY y_last)
130{
131 auto n_x = statcpp::count(x_first, x_last);
132 auto n_y = statcpp::count(y_first, y_last);
133
134 if (n_x != n_y) {
135 throw std::invalid_argument("statcpp::simple_linear_regression: x and y must have same length");
136 }
137 if (n_x < 3) {
138 throw std::invalid_argument("statcpp::simple_linear_regression: need at least 3 observations");
139 }
140
141 std::size_t n = n_x;
142 double n_d = static_cast<double>(n);
143
144 // Calculate means
145 double mean_x = statcpp::mean(x_first, x_last);
146 double mean_y = statcpp::mean(y_first, y_last);
147
148 // Calculate Sxx, Syy, Sxy
149 double sxx = 0.0, syy = 0.0, sxy = 0.0;
150 auto it_x = x_first;
151 auto it_y = y_first;
152 for (; it_x != x_last; ++it_x, ++it_y) {
153 double dx = static_cast<double>(*it_x) - mean_x;
154 double dy = static_cast<double>(*it_y) - mean_y;
155 sxx += dx * dx;
156 syy += dy * dy;
157 sxy += dx * dy;
158 }
159
160 if (sxx == 0.0) {
161 throw std::invalid_argument("statcpp::simple_linear_regression: zero variance in x");
162 }
163
164 // Regression coefficients
165 double slope = sxy / sxx;
166 double intercept = mean_y - slope * mean_x;
167
168 // Sum of squares
169 double ss_total = syy;
170 double ss_regression = slope * sxy;
171 double ss_residual = std::max(0.0, ss_total - ss_regression);
172
173 if (ss_total == 0.0) {
174 throw std::invalid_argument("statcpp::simple_linear_regression: zero variance in y (constant response)");
175 }
176
177 // Residual variance and standard error
178 double df_reg = 1.0;
179 double df_res = n_d - 2.0;
180 double mse = ss_residual / df_res;
181 double residual_se = std::sqrt(mse);
182
183 // Standard errors of coefficients
184 double slope_se = residual_se / std::sqrt(sxx);
185 double intercept_se = residual_se * std::sqrt(1.0 / n_d + mean_x * mean_x / sxx);
186
187 // t-statistics and p-values
188 double slope_t = slope / slope_se;
189 double intercept_t = intercept / intercept_se;
190 double slope_p = 2.0 * (1.0 - t_cdf(std::abs(slope_t), df_res));
191 double intercept_p = 2.0 * (1.0 - t_cdf(std::abs(intercept_t), df_res));
192
193 // Coefficient of determination
194 double r_squared = ss_regression / ss_total;
195 double adj_r_squared = 1.0 - (1.0 - r_squared) * (n_d - 1.0) / df_res;
196
197 // F-statistic
198 double f_statistic = (ss_regression / df_reg) / mse;
199 double f_p_value = 1.0 - f_cdf(f_statistic, df_reg, df_res);
200
201 return {
202 intercept, slope,
203 intercept_se, slope_se,
204 intercept_t, slope_t,
205 intercept_p, slope_p,
206 r_squared, adj_r_squared,
207 residual_se,
208 f_statistic, f_p_value,
209 df_reg, df_res,
210 ss_total, ss_regression, ss_residual
211 };
212}
213
214// ============================================================================
215// Multiple Linear Regression
216// ============================================================================
217
218namespace detail {
219
231inline void validate_matrix_structure(const std::vector<std::vector<double>>& data,
232 const char* func_name)
233{
234 if (data.empty()) {
235 std::string msg = "statcpp::";
236 msg += func_name;
237 msg += ": empty data";
238 throw std::invalid_argument(msg);
239 }
240
241 std::size_t p = data[0].size();
242 if (p == 0) {
243 std::string msg = "statcpp::";
244 msg += func_name;
245 msg += ": first row is empty (0 columns)";
246 throw std::invalid_argument(msg);
247 }
248
249 // Verify all rows have the same number of columns
250 for (std::size_t i = 1; i < data.size(); ++i) {
251 if (data[i].size() != p) {
252 std::string msg = "statcpp::";
253 msg += func_name;
254 msg += ": inconsistent row dimensions (row 0 has ";
255 msg += std::to_string(p);
256 msg += " columns, but row ";
257 msg += std::to_string(i);
258 msg += " has ";
259 msg += std::to_string(data[i].size());
260 msg += " columns)";
261 throw std::invalid_argument(msg);
262 }
263 }
264}
265
276inline void validate_no_intercept_column(const std::vector<std::vector<double>>& X, const char* func_name)
277{
278 if (X.empty()) {
279 return;
280 }
281
282 std::size_t n = X.size();
283 std::size_t p = X[0].size();
284
285 // Check if first column is all 1.0
286 if (p > 0) {
287 bool all_ones = true;
288 for (std::size_t i = 0; i < n; ++i) {
289 if (X[i].size() == 0 || std::abs(X[i][0] - 1.0) > 1e-10) {
290 all_ones = false;
291 break;
292 }
293 }
294
295 if (all_ones) {
296 std::string msg = "statcpp::";
297 msg += func_name;
298 msg += ": X should not contain intercept column (all 1s in first column detected). ";
299 msg += "The intercept is added automatically.";
300 throw std::invalid_argument(msg);
301 }
302 }
303}
304
305// Simple matrix operations (for small matrices)
306
314inline std::vector<std::vector<double>> transpose(const std::vector<std::vector<double>>& A)
315{
316 if (A.empty()) return {};
317
318 // Validate matrix structure (all rows have same size)
319 std::size_t rows = A.size();
320 std::size_t cols = A[0].size();
321 for (std::size_t i = 1; i < rows; ++i) {
322 if (A[i].size() != cols) {
323 throw std::invalid_argument("statcpp::detail::transpose: inconsistent row dimensions");
324 }
325 }
326
327 std::vector<std::vector<double>> result(cols, std::vector<double>(rows));
328 for (std::size_t i = 0; i < rows; ++i) {
329 for (std::size_t j = 0; j < cols; ++j) {
330 result[j][i] = A[i][j];
331 }
332 }
333 return result;
334}
335
345inline std::vector<std::vector<double>> matrix_multiply(
346 const std::vector<std::vector<double>>& A,
347 const std::vector<std::vector<double>>& B)
348{
349 if (A.empty() || B.empty()) return {};
350 std::size_t m = A.size();
351 std::size_t n = A[0].size();
352 std::size_t p = B[0].size();
353
354 if (n != B.size()) {
355 throw std::invalid_argument("statcpp::detail::matrix_multiply: incompatible dimensions");
356 }
357
358 // Check that each row has consistent size
359 for (std::size_t i = 0; i < m; ++i) {
360 if (A[i].size() != n) {
361 throw std::invalid_argument("statcpp::detail::matrix_multiply: matrix A has inconsistent row dimensions");
362 }
363 }
364 for (std::size_t k = 0; k < n; ++k) {
365 if (B[k].size() != p) {
366 throw std::invalid_argument("statcpp::detail::matrix_multiply: matrix B has inconsistent row dimensions");
367 }
368 }
369
370 std::vector<std::vector<double>> result(m, std::vector<double>(p, 0.0));
371 for (std::size_t i = 0; i < m; ++i) {
372 for (std::size_t j = 0; j < p; ++j) {
373 for (std::size_t k = 0; k < n; ++k) {
374 result[i][j] += A[i][k] * B[k][j];
375 }
376 }
377 }
378 return result;
379}
380
389inline std::vector<double> matrix_vector_multiply(
390 const std::vector<std::vector<double>>& A,
391 const std::vector<double>& v)
392{
393 if (A.empty()) return {};
394 std::size_t m = A.size();
395 std::size_t n = A[0].size();
396
397 if (n != v.size()) {
398 throw std::invalid_argument("statcpp::detail::matrix_vector_multiply: incompatible dimensions");
399 }
400
401 std::vector<double> result(m, 0.0);
402 for (std::size_t i = 0; i < m; ++i) {
403 for (std::size_t j = 0; j < n; ++j) {
404 result[i] += A[i][j] * v[j];
405 }
406 }
407 return result;
408}
409
419inline std::vector<std::vector<double>> cholesky(const std::vector<std::vector<double>>& A)
420{
421 std::size_t n = A.size();
422 std::vector<std::vector<double>> L(n, std::vector<double>(n, 0.0));
423
424 for (std::size_t i = 0; i < n; ++i) {
425 for (std::size_t j = 0; j <= i; ++j) {
426 double sum = 0.0;
427 for (std::size_t k = 0; k < j; ++k) {
428 sum += L[i][k] * L[j][k];
429 }
430 if (i == j) {
431 double val = A[i][i] - sum;
432 if (val <= 0.0) {
433 throw std::runtime_error("statcpp::detail::cholesky: matrix is not positive definite");
434 }
435 L[i][j] = std::sqrt(val);
436 } else {
437 L[i][j] = (A[i][j] - sum) / L[j][j];
438 }
439 }
440 }
441 return L;
442}
443
454inline std::vector<double> solve_cholesky(
455 const std::vector<std::vector<double>>& L,
456 const std::vector<double>& b)
457{
458 std::size_t n = L.size();
459
460 // Forward substitution: L * y = b
461 std::vector<double> y(n);
462 for (std::size_t i = 0; i < n; ++i) {
463 double sum = 0.0;
464 for (std::size_t j = 0; j < i; ++j) {
465 sum += L[i][j] * y[j];
466 }
467 y[i] = (b[i] - sum) / L[i][i];
468 }
469
470 // Back substitution: L^T * x = y
471 std::vector<double> x(n);
472 for (std::size_t i = n; i > 0; --i) {
473 std::size_t idx = i - 1;
474 double sum = 0.0;
475 for (std::size_t j = idx + 1; j < n; ++j) {
476 sum += L[j][idx] * x[j];
477 }
478 x[idx] = (y[idx] - sum) / L[idx][idx];
479 }
480 return x;
481}
482
489inline std::vector<std::vector<double>> inverse_cholesky(
490 const std::vector<std::vector<double>>& L)
491{
492 std::size_t n = L.size();
493 std::vector<std::vector<double>> inv(n, std::vector<double>(n, 0.0));
494
495 // Solve each column
496 for (std::size_t j = 0; j < n; ++j) {
497 std::vector<double> e(n, 0.0);
498 e[j] = 1.0;
499 std::vector<double> col = solve_cholesky(L, e);
500 for (std::size_t i = 0; i < n; ++i) {
501 inv[i][j] = col[i];
502 }
503 }
504 return inv;
505}
506
507} // namespace detail
508
524 const std::vector<std::vector<double>>& X,
525 const std::vector<double>& y)
526{
527 // Verify that X does not contain an intercept column
528 detail::validate_no_intercept_column(X, "multiple_linear_regression");
529
530 std::size_t n = X.size();
531 if (n == 0) {
532 throw std::invalid_argument("statcpp::multiple_linear_regression: empty data");
533 }
534 if (n != y.size()) {
535 throw std::invalid_argument("statcpp::multiple_linear_regression: X and y must have same number of observations");
536 }
537
538 std::size_t p = X[0].size(); // Number of predictors (excluding intercept)
539 for (const auto& row : X) {
540 if (row.size() != p) {
541 throw std::invalid_argument("statcpp::multiple_linear_regression: inconsistent number of predictors");
542 }
543 }
544
545 std::size_t p_full = p + 1; // Number of coefficients including intercept
546 if (n <= p_full) {
547 throw std::invalid_argument("statcpp::multiple_linear_regression: need more observations than predictors");
548 }
549
550 double n_d = static_cast<double>(n);
551
552 // Create design matrix with intercept term
553 std::vector<std::vector<double>> X_design(n, std::vector<double>(p_full));
554 for (std::size_t i = 0; i < n; ++i) {
555 X_design[i][0] = 1.0; // Intercept
556 for (std::size_t j = 0; j < p; ++j) {
557 X_design[i][j + 1] = X[i][j];
558 }
559 }
560
561 // Calculate X^T * X
562 auto Xt = detail::transpose(X_design);
563 auto XtX = detail::matrix_multiply(Xt, X_design);
564
565 // Calculate X^T * y
566 auto Xty = detail::matrix_vector_multiply(Xt, y);
567
568 // Solve for coefficients using Cholesky decomposition
569 auto L = detail::cholesky(XtX);
570 auto coefficients = detail::solve_cholesky(L, Xty);
571
572 // Calculate (X^T X)^{-1}
573 auto XtX_inv = detail::inverse_cholesky(L);
574
575 // Calculate predicted values and residuals
576 std::vector<double> y_hat(n);
577 std::vector<double> residuals(n);
578 double mean_y = statcpp::mean(y.begin(), y.end());
579
580 double ss_total = 0.0;
581 double ss_residual = 0.0;
582
583 for (std::size_t i = 0; i < n; ++i) {
584 double pred = 0.0;
585 for (std::size_t j = 0; j < p_full; ++j) {
586 pred += X_design[i][j] * coefficients[j];
587 }
588 y_hat[i] = pred;
589 residuals[i] = y[i] - pred;
590
591 ss_total += (y[i] - mean_y) * (y[i] - mean_y);
592 ss_residual += residuals[i] * residuals[i];
593 }
594
595 double ss_regression = ss_total - ss_residual;
596
597 if (ss_total == 0.0) {
598 throw std::invalid_argument("statcpp::multiple_linear_regression: zero variance in y (constant response)");
599 }
600
601 // Degrees of freedom
602 double df_reg = static_cast<double>(p);
603 double df_res = n_d - static_cast<double>(p_full);
604
605 // Residual variance
606 double mse = ss_residual / df_res;
607 double residual_se = std::sqrt(mse);
608
609 // Standard errors, t-statistics, and p-values of coefficients
610 std::vector<double> coefficient_se(p_full);
611 std::vector<double> t_statistics(p_full);
612 std::vector<double> p_values(p_full);
613
614 for (std::size_t j = 0; j < p_full; ++j) {
615 coefficient_se[j] = std::sqrt(mse * XtX_inv[j][j]);
616 t_statistics[j] = coefficients[j] / coefficient_se[j];
617 p_values[j] = 2.0 * (1.0 - t_cdf(std::abs(t_statistics[j]), df_res));
618 }
619
620 // Coefficient of determination
621 double r_squared = ss_regression / ss_total;
622 double adj_r_squared = 1.0 - (1.0 - r_squared) * (n_d - 1.0) / df_res;
623
624 // F-statistic
625 double f_statistic = (ss_regression / df_reg) / mse;
626 double f_p_value = 1.0 - f_cdf(f_statistic, df_reg, df_res);
627
628 return {
629 coefficients, coefficient_se, t_statistics, p_values,
630 r_squared, adj_r_squared,
631 residual_se,
632 f_statistic, f_p_value,
633 df_reg, df_res,
634 ss_total, ss_regression, ss_residual
635 };
636}
637
638// ============================================================================
639// Prediction
640// ============================================================================
641
649inline double predict(const simple_regression_result& model, double x)
650{
651 return model.intercept + model.slope * x;
652}
653
662inline double predict(const multiple_regression_result& model, const std::vector<double>& x)
663{
664 if (x.size() + 1 != model.coefficients.size()) {
665 throw std::invalid_argument("statcpp::predict: x dimension mismatch");
666 }
667
668 double pred = model.coefficients[0]; // Intercept
669 for (std::size_t i = 0; i < x.size(); ++i) {
670 pred += model.coefficients[i + 1] * x[i];
671 }
672 return pred;
673}
674
675// ============================================================================
676// Prediction Interval
677// ============================================================================
678
694template <typename IteratorX>
696 const simple_regression_result& model,
697 IteratorX x_first, IteratorX x_last,
698 double x_new,
699 double confidence = 0.95)
700{
701 if (confidence <= 0.0 || confidence >= 1.0) {
702 throw std::invalid_argument("statcpp::prediction_interval_simple: confidence must be in (0, 1)");
703 }
704
705 auto n = statcpp::count(x_first, x_last);
706 double n_d = static_cast<double>(n);
707 double mean_x = statcpp::mean(x_first, x_last);
708
709 // Calculate Sxx
710 double sxx = 0.0;
711 for (auto it = x_first; it != x_last; ++it) {
712 double dx = static_cast<double>(*it) - mean_x;
713 sxx += dx * dx;
714 }
715
716 double y_hat = predict(model, x_new);
717
718 // Standard error of prediction (for new observation)
719 double dx_new = x_new - mean_x;
720 double se_pred = model.residual_se * std::sqrt(1.0 + 1.0 / n_d + dx_new * dx_new / sxx);
721
722 double t_crit = t_quantile(1.0 - (1.0 - confidence) / 2.0, model.df_residual);
723 double margin = t_crit * se_pred;
724
725 return {y_hat, y_hat - margin, y_hat + margin, se_pred};
726}
727
743template <typename IteratorX>
745 const simple_regression_result& model,
746 IteratorX x_first, IteratorX x_last,
747 double x_new,
748 double confidence = 0.95)
749{
750 if (confidence <= 0.0 || confidence >= 1.0) {
751 throw std::invalid_argument("statcpp::confidence_interval_mean: confidence must be in (0, 1)");
752 }
753
754 auto n = statcpp::count(x_first, x_last);
755 double n_d = static_cast<double>(n);
756 double mean_x = statcpp::mean(x_first, x_last);
757
758 // Calculate Sxx
759 double sxx = 0.0;
760 for (auto it = x_first; it != x_last; ++it) {
761 double dx = static_cast<double>(*it) - mean_x;
762 sxx += dx * dx;
763 }
764
765 double y_hat = predict(model, x_new);
766
767 // Standard error of mean prediction
768 double dx_new = x_new - mean_x;
769 double se_mean = model.residual_se * std::sqrt(1.0 / n_d + dx_new * dx_new / sxx);
770
771 double t_crit = t_quantile(1.0 - (1.0 - confidence) / 2.0, model.df_residual);
772 double margin = t_crit * se_mean;
773
774 return {y_hat, y_hat - margin, y_hat + margin, se_mean};
775}
776
777// ============================================================================
778// Residual Diagnostics
779// ============================================================================
780
797template <typename IteratorX, typename IteratorY>
799 const simple_regression_result& model,
800 IteratorX x_first, IteratorX x_last,
801 IteratorY y_first, IteratorY y_last)
802{
803 auto n = statcpp::count(x_first, x_last);
804 if (n != statcpp::count(y_first, y_last)) {
805 throw std::invalid_argument("statcpp::compute_residual_diagnostics: x and y must have same length");
806 }
807
808 double n_d = static_cast<double>(n);
809 double mean_x = statcpp::mean(x_first, x_last);
810
811 // Calculate Sxx
812 double sxx = 0.0;
813 for (auto it = x_first; it != x_last; ++it) {
814 double dx = static_cast<double>(*it) - mean_x;
815 sxx += dx * dx;
816 }
817
818 std::vector<double> residuals(n);
819 std::vector<double> hat_values(n);
820 std::vector<double> standardized_residuals(n);
821
822 auto it_x = x_first;
823 auto it_y = y_first;
824 for (std::size_t i = 0; it_x != x_last; ++it_x, ++it_y, ++i) {
825 double x_i = static_cast<double>(*it_x);
826 double y_i = static_cast<double>(*it_y);
827 double y_hat = predict(model, x_i);
828 residuals[i] = y_i - y_hat;
829
830 // Leverage h_ii = 1/n + (x_i - mean_x)^2 / Sxx
831 double dx = x_i - mean_x;
832 hat_values[i] = 1.0 / n_d + dx * dx / sxx;
833
834 // Standardized residuals
835 standardized_residuals[i] = residuals[i] / model.residual_se;
836 }
837
838 // Studentized residuals and Cook's distance
839 std::vector<double> studentized_residuals(n);
840 std::vector<double> cooks_distance(n);
841 double p = 2.0; // Number of coefficients (intercept + slope)
842
843 for (std::size_t i = 0; i < n; ++i) {
844 double h_i = hat_values[i];
845 double se_i = model.residual_se * std::sqrt(1.0 - h_i);
846 studentized_residuals[i] = (se_i > 0.0) ? residuals[i] / se_i : 0.0;
847 cooks_distance[i] = (standardized_residuals[i] * standardized_residuals[i] / p)
848 * (h_i / ((1.0 - h_i) * (1.0 - h_i)));
849 }
850
851 // Durbin-Watson statistic
852 double dw_num = 0.0;
853 double dw_den = 0.0;
854 for (std::size_t i = 0; i < n; ++i) {
855 dw_den += residuals[i] * residuals[i];
856 if (i > 0) {
857 double diff = residuals[i] - residuals[i - 1];
858 dw_num += diff * diff;
859 }
860 }
861 double durbin_watson = (dw_den > 0.0) ? dw_num / dw_den : 0.0;
862
863 return {residuals, standardized_residuals, studentized_residuals,
864 hat_values, cooks_distance, durbin_watson};
865}
866
880 const multiple_regression_result& model,
881 const std::vector<std::vector<double>>& X,
882 const std::vector<double>& y)
883{
884 std::size_t n = X.size();
885 if (n != y.size()) {
886 throw std::invalid_argument("statcpp::compute_residual_diagnostics: X and y must have same length");
887 }
888
889 std::size_t p = X[0].size();
890 std::size_t p_full = p + 1;
891
892 // Create design matrix
893 std::vector<std::vector<double>> X_design(n, std::vector<double>(p_full));
894 for (std::size_t i = 0; i < n; ++i) {
895 X_design[i][0] = 1.0;
896 for (std::size_t j = 0; j < p; ++j) {
897 X_design[i][j + 1] = X[i][j];
898 }
899 }
900
901 // Calculate (X^T X)^{-1}
902 auto Xt = detail::transpose(X_design);
903 auto XtX = detail::matrix_multiply(Xt, X_design);
904 auto L = detail::cholesky(XtX);
905 auto XtX_inv = detail::inverse_cholesky(L);
906
907 // H = X(X^T X)^{-1}X^T leverage values (diagonal elements only)
908 std::vector<double> hat_values(n);
909 for (std::size_t i = 0; i < n; ++i) {
910 double h_ii = 0.0;
911 for (std::size_t j = 0; j < p_full; ++j) {
912 for (std::size_t k = 0; k < p_full; ++k) {
913 h_ii += X_design[i][j] * XtX_inv[j][k] * X_design[i][k];
914 }
915 }
916 hat_values[i] = h_ii;
917 }
918
919 // Calculate residuals
920 std::vector<double> residuals(n);
921 for (std::size_t i = 0; i < n; ++i) {
922 double pred = predict(model, X[i]);
923 residuals[i] = y[i] - pred;
924 }
925
926 // Standardized residuals, studentized residuals, Cook's distance
927 std::vector<double> standardized_residuals(n);
928 std::vector<double> studentized_residuals(n);
929 std::vector<double> cooks_distance(n);
930
931 double p_d = static_cast<double>(p_full);
932
933 for (std::size_t i = 0; i < n; ++i) {
934 standardized_residuals[i] = residuals[i] / model.residual_se;
935
936 double h_i = hat_values[i];
937 double se_i = model.residual_se * std::sqrt(1.0 - h_i);
938 studentized_residuals[i] = (se_i > 0.0) ? residuals[i] / se_i : 0.0;
939
940 cooks_distance[i] = (standardized_residuals[i] * standardized_residuals[i] / p_d)
941 * (h_i / ((1.0 - h_i) * (1.0 - h_i)));
942 }
943
944 // Durbin-Watson statistic
945 double dw_num = 0.0;
946 double dw_den = 0.0;
947 for (std::size_t i = 0; i < n; ++i) {
948 dw_den += residuals[i] * residuals[i];
949 if (i > 0) {
950 double diff = residuals[i] - residuals[i - 1];
951 dw_num += diff * diff;
952 }
953 }
954 double durbin_watson = (dw_den > 0.0) ? dw_num / dw_den : 0.0;
955
956 return {residuals, standardized_residuals, studentized_residuals,
957 hat_values, cooks_distance, durbin_watson};
958}
959
960// ============================================================================
961// VIF (Variance Inflation Factor)
962// ============================================================================
963
976inline std::vector<double> compute_vif(const std::vector<std::vector<double>>& X)
977{
978 std::size_t n = X.size();
979 if (n < 3) {
980 throw std::invalid_argument("statcpp::compute_vif: need at least 3 observations");
981 }
982
983 std::size_t p = X[0].size();
984 if (p < 2) {
985 throw std::invalid_argument("statcpp::compute_vif: need at least 2 predictors");
986 }
987
988 std::vector<double> vif(p);
989
990 for (std::size_t j = 0; j < p; ++j) {
991 // Regress j-th variable as response on other variables as predictors
992 std::vector<double> y_j(n);
993 std::vector<std::vector<double>> X_others(n, std::vector<double>(p - 1));
994
995 for (std::size_t i = 0; i < n; ++i) {
996 y_j[i] = X[i][j];
997 std::size_t col = 0;
998 for (std::size_t k = 0; k < p; ++k) {
999 if (k != j) {
1000 X_others[i][col++] = X[i][k];
1001 }
1002 }
1003 }
1004
1005 auto result = multiple_linear_regression(X_others, y_j);
1006 double r_sq = result.r_squared;
1007
1008 // VIF = 1 / (1 - R^2)
1009 if (r_sq >= 1.0) {
1010 vif[j] = std::numeric_limits<double>::infinity();
1011 } else {
1012 vif[j] = 1.0 / (1.0 - r_sq);
1013 }
1014 }
1015
1016 return vif;
1017}
1018
1019// ============================================================================
1020// Multicollinearity Diagnostics (Extended)
1021// ============================================================================
1022
1035inline double correlation_matrix_determinant(const std::vector<std::vector<double>>& X)
1036{
1037 std::size_t n = X.size();
1038 if (n < 2) {
1039 throw std::invalid_argument("statcpp::correlation_matrix_determinant: need at least 2 observations");
1040 }
1041
1042 std::size_t p = X[0].size();
1043 if (p < 2) {
1044 throw std::invalid_argument("statcpp::correlation_matrix_determinant: need at least 2 predictors");
1045 }
1046
1047 // Calculate correlation matrix
1048 std::vector<std::vector<double>> corr_matrix(p, std::vector<double>(p));
1049 for (std::size_t i = 0; i < p; ++i) {
1050 for (std::size_t j = 0; j < p; ++j) {
1051 if (i == j) {
1052 corr_matrix[i][j] = 1.0;
1053 } else if (j > i) {
1054 // Extract each column
1055 std::vector<double> col_i(n), col_j(n);
1056 for (std::size_t k = 0; k < n; ++k) {
1057 col_i[k] = X[k][i];
1058 col_j[k] = X[k][j];
1059 }
1060 double corr = pearson_correlation(col_i.begin(), col_i.end(),
1061 col_j.begin(), col_j.end());
1062 corr_matrix[i][j] = corr;
1063 corr_matrix[j][i] = corr;
1064 }
1065 }
1066 }
1067
1068 // Calculate determinant (only supports small matrices)
1069 if (p == 2) {
1070 // 2x2 matrix: det = a11*a22 - a12*a21
1071 return corr_matrix[0][0] * corr_matrix[1][1] - corr_matrix[0][1] * corr_matrix[1][0];
1072 } else if (p == 3) {
1073 // 3x3 matrix: Sarrus' rule
1074 double a = corr_matrix[0][0] * corr_matrix[1][1] * corr_matrix[2][2];
1075 double b = corr_matrix[0][1] * corr_matrix[1][2] * corr_matrix[2][0];
1076 double c = corr_matrix[0][2] * corr_matrix[1][0] * corr_matrix[2][1];
1077 double d = corr_matrix[0][2] * corr_matrix[1][1] * corr_matrix[2][0];
1078 double e = corr_matrix[0][0] * corr_matrix[1][2] * corr_matrix[2][1];
1079 double f = corr_matrix[0][1] * corr_matrix[1][0] * corr_matrix[2][2];
1080 return a + b + c - d - e - f;
1081 } else {
1082 // Larger matrices would require LU decomposition, not supported here
1083 throw std::invalid_argument("statcpp::correlation_matrix_determinant: only 2 or 3 predictors supported");
1084 }
1085}
1086
1098inline double multicollinearity_score(const std::vector<std::vector<double>>& X)
1099{
1100 double det = correlation_matrix_determinant(X);
1101 // Determinant can be negative, but what matters is how close to 0
1102 return 1.0 - std::abs(det);
1103}
1104
1105// ============================================================================
1106// R-squared and Related Measures
1107// ============================================================================
1108
1124template <typename IteratorY, typename IteratorPred>
1125double r_squared(IteratorY y_first, IteratorY y_last,
1126 IteratorPred pred_first, IteratorPred pred_last)
1127{
1128 auto n_y = statcpp::count(y_first, y_last);
1129 auto n_pred = statcpp::count(pred_first, pred_last);
1130
1131 if (n_y != n_pred) {
1132 throw std::invalid_argument("statcpp::r_squared: y and predictions must have same length");
1133 }
1134 if (n_y < 2) {
1135 throw std::invalid_argument("statcpp::r_squared: need at least 2 observations");
1136 }
1137
1138 double mean_y = statcpp::mean(y_first, y_last);
1139
1140 double ss_total = 0.0;
1141 double ss_residual = 0.0;
1142
1143 auto it_y = y_first;
1144 auto it_pred = pred_first;
1145 for (; it_y != y_last; ++it_y, ++it_pred) {
1146 double y_i = static_cast<double>(*it_y);
1147 double pred_i = static_cast<double>(*it_pred);
1148 ss_total += (y_i - mean_y) * (y_i - mean_y);
1149 ss_residual += (y_i - pred_i) * (y_i - pred_i);
1150 }
1151
1152 if (ss_total == 0.0) {
1153 return 1.0; // Completely constant case
1154 }
1155
1156 return 1.0 - ss_residual / ss_total;
1157}
1158
1176template <typename IteratorY, typename IteratorPred>
1177double adjusted_r_squared(IteratorY y_first, IteratorY y_last,
1178 IteratorPred pred_first, IteratorPred pred_last,
1179 std::size_t num_predictors)
1180{
1181 auto n = statcpp::count(y_first, y_last);
1182 double n_d = static_cast<double>(n);
1183 double p = static_cast<double>(num_predictors);
1184
1185 if (n_d <= p + 1.0) {
1186 throw std::invalid_argument("statcpp::adjusted_r_squared: need more observations than predictors");
1187 }
1188
1189 double r_sq = r_squared(y_first, y_last, pred_first, pred_last);
1190 return 1.0 - (1.0 - r_sq) * (n_d - 1.0) / (n_d - p - 1.0);
1191}
1192
1193} // namespace statcpp
Basic statistical computation functions.
Continuous distribution functions.
Correlation and covariance computation functions.
std::vector< std::vector< double > > matrix_multiply(const std::vector< std::vector< double > > &A, const std::vector< std::vector< double > > &B)
Calculate matrix product.
std::vector< std::vector< double > > inverse_cholesky(const std::vector< std::vector< double > > &L)
Calculate inverse matrix using Cholesky decomposition.
std::vector< std::vector< double > > transpose(const std::vector< std::vector< double > > &A)
Calculate transpose matrix.
std::vector< double > matrix_vector_multiply(const std::vector< std::vector< double > > &A, const std::vector< double > &v)
Calculate matrix-vector product.
std::vector< double > solve_cholesky(const std::vector< std::vector< double > > &L, const std::vector< double > &b)
Solve system of equations using Cholesky decomposition.
std::vector< std::vector< double > > cholesky(const std::vector< std::vector< double > > &A)
Perform Cholesky decomposition.
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.
void validate_matrix_structure(const std::vector< std::vector< double > > &data, const char *func_name)
Validate 2D matrix structure.
double multicollinearity_score(const std::vector< std::vector< double > > &X)
Calculate multicollinearity score.
prediction_interval prediction_interval_simple(const simple_regression_result &model, IteratorX x_first, IteratorX x_last, double x_new, double confidence=0.95)
Calculate prediction interval for simple regression model.
residual_diagnostics compute_residual_diagnostics(const simple_regression_result &model, IteratorX x_first, IteratorX x_last, IteratorY y_first, IteratorY y_last)
Perform residual diagnostics for simple regression model.
auto sum(Iterator first, Iterator last)
Sum.
double t_cdf(double x, double df)
t-distribution cumulative distribution function (CDF)
simple_regression_result simple_linear_regression(IteratorX x_first, IteratorX x_last, IteratorY y_first, IteratorY y_last)
Perform simple linear regression.
std::vector< double > cooks_distance(const std::vector< double > &residuals, const std::vector< double > &hat_values, double mse, std::size_t p)
Calculate Cook's Distance.
Definition robust.hpp:363
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 mean(Iterator first, Iterator last)
Arithmetic mean.
double t_quantile(double p, double df)
t-distribution quantile function (Newton-Raphson method)
std::vector< double > diff(Iterator first, Iterator last, std::size_t order=1)
Difference series (first-order or d-th order differencing)
prediction_interval confidence_interval_mean(const simple_regression_result &model, IteratorX x_first, IteratorX x_last, double x_new, double confidence=0.95)
Calculate confidence interval for mean of simple regression model.
double r_squared(IteratorY y_first, IteratorY y_last, IteratorPred pred_first, IteratorPred pred_last)
Calculate coefficient of determination from observed and predicted values.
std::vector< double > compute_vif(const std::vector< std::vector< double > > &X)
Calculate VIF (Variance Inflation Factor) for each predictor.
double mse(Iterator1 first1, Iterator1 last1, Iterator2 first2)
Mean Squared Error (MSE)
double adjusted_r_squared(IteratorY y_first, IteratorY y_last, IteratorPred pred_first, IteratorPred pred_last, std::size_t num_predictors)
Calculate adjusted coefficient of determination.
double pearson_correlation(Iterator1 first1, Iterator1 last1, Iterator2 first2, Iterator2 last2)
Pearson correlation coefficient.
double correlation_matrix_determinant(const std::vector< std::vector< double > > &X)
Calculate determinant of correlation matrix.
std::size_t count(Iterator first, Iterator last)
Data count.
double f_cdf(double x, double df1, double df2)
F-distribution cumulative distribution function (CDF)
Structure to store multiple regression analysis results.
double ss_total
Total sum of squares.
std::vector< double > coefficients
Regression coefficients (b0, b1, ..., bp)
std::vector< double > coefficient_se
Standard errors of coefficients.
double ss_residual
Residual sum of squares.
std::vector< double > p_values
p-values
double df_regression
Regression degrees of freedom.
double residual_se
Residual standard error.
double ss_regression
Regression sum of squares.
double r_squared
Coefficient of determination R^2.
std::vector< double > t_statistics
t-statistics
double df_residual
Residual degrees of freedom.
Structure to store prediction interval results.
double prediction
Predicted value.
double se_prediction
Standard error of prediction.
Structure to store residual diagnostics results.
std::vector< double > cooks_distance
Cook's distance.
double durbin_watson
Durbin-Watson statistic.
std::vector< double > studentized_residuals
Studentized residuals.
std::vector< double > standardized_residuals
Standardized residuals.
std::vector< double > residuals
Residuals.
std::vector< double > hat_values
Leverage values.
Structure to store simple regression analysis results.
double r_squared
Coefficient of determination R^2.
double slope_se
Standard error of slope.
double df_residual
Residual degrees of freedom.
double intercept_se
Standard error of intercept.
double residual_se
Residual standard error.
double slope_t
t-statistic for slope
double ss_regression
Regression sum of squares.
double intercept_t
t-statistic for intercept
double ss_total
Total sum of squares.
double df_regression
Regression degrees of freedom.
double intercept_p
p-value for intercept
double ss_residual
Residual sum of squares.