statcpp
C++17 Header-Only Statistics Library
Loading...
Searching...
No Matches
glm.hpp
Go to the documentation of this file.
1
9#pragma once
10
14
15#include <algorithm>
16#include <cmath>
17#include <cstddef>
18#include <exception>
19#include <limits>
20#include <stdexcept>
21#include <utility>
22#include <vector>
23
24namespace statcpp {
25
26// ============================================================================
27// GLM Result Structures
28// ============================================================================
29
35enum class link_function {
36 identity,
37 logit,
38 probit,
39 log,
40 inverse,
41 cloglog
42};
43
50 gaussian,
51 binomial,
52 poisson,
54};
55
61struct glm_result {
62 std::vector<double> coefficients;
63 std::vector<double> coefficient_se;
64 std::vector<double> z_statistics;
65 std::vector<double> p_values;
68 double df_null;
69 double df_residual;
70 double aic;
71 double bic;
74 std::size_t iterations;
75 bool converged;
78};
79
80// ============================================================================
81// Link Functions
82// ============================================================================
83
84namespace detail {
85
95inline double link_transform(double mu, link_function link)
96{
97 switch (link) {
99 return mu;
101 mu = std::max(1e-10, std::min(1.0 - 1e-10, mu));
102 return std::log(mu / (1.0 - mu));
104 mu = std::max(1e-10, std::min(1.0 - 1e-10, mu));
105 return norm_quantile(mu);
107 return std::log(std::max(1e-10, mu));
109 return 1.0 / std::max(1e-10, mu);
111 mu = std::max(1e-10, std::min(1.0 - 1e-10, mu));
112 return std::log(-std::log(1.0 - mu));
113 default:
114 return mu;
115 }
116}
117
127inline double inverse_link(double eta, link_function link)
128{
129 switch (link) {
131 return eta;
133 return 1.0 / (1.0 + std::exp(-eta));
135 return norm_cdf(eta);
137 return std::min(std::exp(eta), 1e300);
139 if (std::abs(eta) < 1e-10) return 1e10;
140 double result = 1.0 / eta;
141 return std::max(result, 1e-10);
142 }
144 return 1.0 - std::exp(-std::exp(eta));
145 default:
146 return eta;
147 }
148}
149
160inline double link_derivative(double mu, link_function link)
161{
162 switch (link) {
164 return 1.0;
166 mu = std::max(1e-10, std::min(1.0 - 1e-10, mu));
167 return 1.0 / (mu * (1.0 - mu));
169 mu = std::max(1e-10, std::min(1.0 - 1e-10, mu));
170 return 1.0 / normal_pdf(norm_quantile(mu));
172 return 1.0 / std::max(1e-10, mu);
174 mu = std::max(1e-10, mu);
175 return -1.0 / (mu * mu);
177 mu = std::max(1e-8, std::min(1.0 - 1e-8, mu));
178 double neg_log_term = -std::log(1.0 - mu); // -log(1-mu) > 0 for 0 < mu < 1
179 // Protection when -log(1-mu) is close to 0 (mu near 0)
180 if (neg_log_term < 1e-10) {
181 throw std::runtime_error("statcpp::link_derivative: cloglog derivative undefined near mu=0");
182 }
183 // g'(mu) = 1 / ((1-mu) * (-log(1-mu)))
184 return 1.0 / ((1.0 - mu) * neg_log_term);
185 }
186 default:
187 return 1.0;
188 }
189}
190
200inline double variance_function(double mu, distribution_family family)
201{
202 switch (family) {
204 return 1.0;
206 mu = std::max(1e-10, std::min(1.0 - 1e-10, mu));
207 return mu * (1.0 - mu);
209 return std::max(1e-10, mu);
211 mu = std::max(1e-10, mu);
212 return mu * mu;
213 default:
214 return 1.0;
215 }
216}
217
228inline double deviance_residual(double y, double mu, distribution_family family)
229{
230 switch (family) {
232 return (y - mu) * (y - mu);
234 {
235 mu = std::max(1e-10, std::min(1.0 - 1e-10, mu));
236 double d = 0.0;
237 if (y > 0.0) {
238 d += y * std::log(y / mu);
239 }
240 if (y < 1.0) {
241 d += (1.0 - y) * std::log((1.0 - y) / (1.0 - mu));
242 }
243 return 2.0 * d;
244 }
246 {
247 mu = std::max(1e-10, mu);
248 if (y > 0.0) {
249 return 2.0 * (y * std::log(y / mu) - (y - mu));
250 } else {
251 return 2.0 * mu;
252 }
253 }
255 {
256 mu = std::max(1e-10, mu);
257 return 2.0 * ((y - mu) / mu - std::log(y / mu));
258 }
259 default:
260 return (y - mu) * (y - mu);
261 }
262}
263
276inline std::vector<double> solve_weighted_least_squares(
277 const std::vector<std::vector<double>>& X,
278 const std::vector<double>& z,
279 const std::vector<double>& w,
280 std::vector<std::vector<double>>& XtWX_inv) // Output: (X'WX)^{-1}
281{
282 std::size_t n = X.size();
283 std::size_t p = X[0].size();
284
285 // Calculate X'WX
286 std::vector<std::vector<double>> XtWX(p, std::vector<double>(p, 0.0));
287 for (std::size_t j = 0; j < p; ++j) {
288 for (std::size_t k = 0; k < p; ++k) {
289 for (std::size_t i = 0; i < n; ++i) {
290 XtWX[j][k] += X[i][j] * w[i] * X[i][k];
291 }
292 }
293 }
294
295 // Calculate X'Wz
296 std::vector<double> XtWz(p, 0.0);
297 for (std::size_t j = 0; j < p; ++j) {
298 for (std::size_t i = 0; i < n; ++i) {
299 XtWz[j] += X[i][j] * w[i] * z[i];
300 }
301 }
302
303 // Solve using Cholesky decomposition
304 std::vector<std::vector<double>> L(p, std::vector<double>(p, 0.0));
305 for (std::size_t i = 0; i < p; ++i) {
306 for (std::size_t j = 0; j <= i; ++j) {
307 double sum = 0.0;
308 for (std::size_t k = 0; k < j; ++k) {
309 sum += L[i][k] * L[j][k];
310 }
311 if (i == j) {
312 double val = XtWX[i][i] - sum;
313 if (val <= 0.0) {
314 throw std::runtime_error("statcpp::glm: matrix is not positive definite");
315 }
316 L[i][j] = std::sqrt(val);
317 } else {
318 L[i][j] = (XtWX[i][j] - sum) / L[j][j];
319 }
320 }
321 }
322
323 // Forward substitution
324 std::vector<double> y(p);
325 for (std::size_t i = 0; i < p; ++i) {
326 double sum = 0.0;
327 for (std::size_t j = 0; j < i; ++j) {
328 sum += L[i][j] * y[j];
329 }
330 y[i] = (XtWz[i] - sum) / L[i][i];
331 }
332
333 // Back substitution
334 std::vector<double> beta(p);
335 for (std::size_t i = p; i > 0; --i) {
336 std::size_t idx = i - 1;
337 double sum = 0.0;
338 for (std::size_t j = idx + 1; j < p; ++j) {
339 sum += L[j][idx] * beta[j];
340 }
341 beta[idx] = (y[idx] - sum) / L[idx][idx];
342 }
343
344 // Calculate (X'WX)^{-1}
345 XtWX_inv.assign(p, std::vector<double>(p, 0.0));
346 for (std::size_t col = 0; col < p; ++col) {
347 std::vector<double> e(p, 0.0);
348 e[col] = 1.0;
349
350 // Forward substitution
351 std::vector<double> y_inv(p);
352 for (std::size_t i = 0; i < p; ++i) {
353 double sum = 0.0;
354 for (std::size_t j = 0; j < i; ++j) {
355 sum += L[i][j] * y_inv[j];
356 }
357 y_inv[i] = (e[i] - sum) / L[i][i];
358 }
359
360 // Back substitution
361 for (std::size_t i = p; i > 0; --i) {
362 std::size_t idx = i - 1;
363 double sum = 0.0;
364 for (std::size_t j = idx + 1; j < p; ++j) {
365 sum += L[j][idx] * XtWX_inv[j][col];
366 }
367 XtWX_inv[idx][col] = (y_inv[idx] - sum) / L[idx][idx];
368 }
369 }
370
371 return beta;
372}
373
374} // namespace detail
375
376// ============================================================================
377// IRLS Algorithm (Iteratively Reweighted Least Squares)
378// ============================================================================
379
402 const std::vector<std::vector<double>>& X,
403 const std::vector<double>& y,
406 std::size_t max_iter = 100,
407 double tol = 1e-8)
408{
409 std::size_t n = X.size();
410 if (n == 0) {
411 throw std::invalid_argument("statcpp::glm_fit: empty data");
412 }
413 if (n != y.size()) {
414 throw std::invalid_argument("statcpp::glm_fit: X and y must have same number of observations");
415 }
416
417 std::size_t p = X[0].size();
418 for (const auto& row : X) {
419 if (row.size() != p) {
420 throw std::invalid_argument("statcpp::glm_fit: inconsistent number of predictors");
421 }
422 }
423
424 std::size_t p_full = p + 1; // Including intercept
425 if (n <= p_full) {
426 throw std::invalid_argument("statcpp::glm_fit: need more observations than predictors");
427 }
428
429 // Design matrix (add intercept)
430 std::vector<std::vector<double>> X_design(n, std::vector<double>(p_full));
431 for (std::size_t i = 0; i < n; ++i) {
432 X_design[i][0] = 1.0;
433 for (std::size_t j = 0; j < p; ++j) {
434 X_design[i][j + 1] = X[i][j];
435 }
436 }
437
438 // Set initial values
439 std::vector<double> mu(n);
440 std::vector<double> eta(n);
441
442 // Set initial values from mean of response variable
443 double y_mean = statcpp::mean(y.begin(), y.end());
444 double y_mean_original = y_mean; // Preserve original mean for null deviance
445 double eta_init;
446
447 switch (family) {
449 y_mean = std::max(0.01, std::min(0.99, y_mean));
450 eta_init = detail::link_transform(y_mean, link);
451 break;
453 y_mean = std::max(0.1, y_mean);
454 eta_init = detail::link_transform(y_mean, link);
455 break;
456 default:
457 eta_init = y_mean;
458 break;
459 }
460
461 for (std::size_t i = 0; i < n; ++i) {
462 eta[i] = eta_init;
463 mu[i] = detail::inverse_link(eta[i], link);
464 }
465
466 // Initial coefficients
467 std::vector<double> beta(p_full, 0.0);
468 beta[0] = eta_init;
469
470 std::vector<std::vector<double>> XtWX_inv;
471 bool converged = false;
472 std::size_t iter = 0;
473
474 // IRLS iteration
475 for (iter = 0; iter < max_iter; ++iter) {
476 // Calculate weights and working variable
477 std::vector<double> w(n);
478 std::vector<double> z(n);
479
480 for (std::size_t i = 0; i < n; ++i) {
481 double var_i = detail::variance_function(mu[i], family);
482 double g_prime = detail::link_derivative(mu[i], link);
483
484 // Weight w_i = 1 / (V(mu_i) * g'(mu_i)^2)
485 w[i] = 1.0 / (var_i * g_prime * g_prime);
486
487 // Working variable z_i = eta_i + (y_i - mu_i) * g'(mu_i)
488 z[i] = eta[i] + (y[i] - mu[i]) * g_prime;
489 }
490
491 // Solve weighted least squares
492 std::vector<double> beta_new;
493 try {
494 beta_new = detail::solve_weighted_least_squares(X_design, z, w, XtWX_inv);
495 } catch (const std::exception&) {
496 break; // Exit if numerically unstable
497 }
498
499 // Convergence check
500 double max_change = 0.0;
501 for (std::size_t j = 0; j < p_full; ++j) {
502 double change = std::abs(beta_new[j] - beta[j]);
503 if (std::abs(beta[j]) > 1.0) {
504 change /= std::abs(beta[j]);
505 }
506 max_change = std::max(max_change, change);
507 }
508
509 beta = beta_new;
510
511 // Update eta and mu
512 eta = detail::matrix_vector_multiply(X_design, beta);
513 for (std::size_t i = 0; i < n; ++i) {
514 mu[i] = detail::inverse_link(eta[i], link);
515 }
516
517 if (max_change < tol) {
518 converged = true;
519 ++iter;
520 break;
521 }
522 }
523
524 // Calculate standard errors
525 std::vector<double> coefficient_se(p_full, std::numeric_limits<double>::quiet_NaN());
526 std::vector<double> z_statistics(p_full, std::numeric_limits<double>::quiet_NaN());
527 std::vector<double> p_values(p_full, std::numeric_limits<double>::quiet_NaN());
528
529 // Dispersion parameter phi. Fixed at 1 for binomial/poisson; estimated by the
530 // Pearson statistic divided by the residual df for gaussian/gamma (as in R's
531 // summary.glm). The coefficient covariance is phi * (X^T W X)^{-1}.
532 double dispersion = 1.0;
534 double pearson_chi2 = 0.0;
535 for (std::size_t i = 0; i < n; ++i) {
536 double resid = y[i] - mu[i];
537 pearson_chi2 += resid * resid / detail::variance_function(mu[i], family);
538 }
539 double df_resid = static_cast<double>(n) - static_cast<double>(p_full);
540 dispersion = (df_resid > 0.0) ? pearson_chi2 / df_resid
541 : std::numeric_limits<double>::quiet_NaN();
542 }
543
544 if (!XtWX_inv.empty()) {
545 for (std::size_t j = 0; j < p_full; ++j) {
546 coefficient_se[j] = std::sqrt(dispersion * XtWX_inv[j][j]);
547 }
548
549 // z-statistics and p-values
550 for (std::size_t j = 0; j < p_full; ++j) {
551 z_statistics[j] = beta[j] / coefficient_se[j];
552 p_values[j] = 2.0 * (1.0 - norm_cdf(std::abs(z_statistics[j])));
553 }
554 }
555
556 // Calculate deviance
557 double residual_deviance = 0.0;
558 for (std::size_t i = 0; i < n; ++i) {
559 residual_deviance += detail::deviance_residual(y[i], mu[i], family);
560 }
561
562 // Null deviance (intercept-only model)
563 double null_deviance = 0.0;
564 for (std::size_t i = 0; i < n; ++i) {
565 null_deviance += detail::deviance_residual(y[i], y_mean_original, family);
566 }
567
568 // Null log-likelihood (intercept-only model)
569 double null_log_likelihood = 0.0;
570 switch (family) {
572 for (std::size_t i = 0; i < n; ++i) {
573 double resid = y[i] - y_mean_original;
574 null_log_likelihood += -0.5 * resid * resid;
575 }
576 break;
578 for (std::size_t i = 0; i < n; ++i) {
579 double p_null = std::max(1e-10, std::min(1.0 - 1e-10, y_mean_original));
580 null_log_likelihood += y[i] * std::log(p_null) + (1.0 - y[i]) * std::log(1.0 - p_null);
581 }
582 break;
584 for (std::size_t i = 0; i < n; ++i) {
585 double mu_null = std::max(1e-10, y_mean_original);
586 null_log_likelihood += y[i] * std::log(mu_null) - mu_null - std::lgamma(y[i] + 1.0);
587 }
588 break;
589 default:
590 null_log_likelihood = -0.5 * null_deviance;
591 break;
592 }
593
594 // Calculate log-likelihood
595 double log_likelihood = 0.0;
596 switch (family) {
598 {
599 double sigma2 = residual_deviance / static_cast<double>(n);
600 log_likelihood = -0.5 * static_cast<double>(n) *
601 (std::log(2.0 * pi) + std::log(sigma2) + 1.0);
602 }
603 break;
605 for (std::size_t i = 0; i < n; ++i) {
606 double mu_i = std::max(1e-10, std::min(1.0 - 1e-10, mu[i]));
607 if (y[i] > 0.0) {
608 log_likelihood += y[i] * std::log(mu_i);
609 }
610 if (y[i] < 1.0) {
611 log_likelihood += (1.0 - y[i]) * std::log(1.0 - mu_i);
612 }
613 }
614 break;
616 for (std::size_t i = 0; i < n; ++i) {
617 log_likelihood += y[i] * std::log(std::max(1e-10, mu[i])) - mu[i]
618 - std::lgamma(y[i] + 1.0);
619 }
620 break;
622 // Exact gamma log-density given the dispersion estimate: the shape nu is
623 // approximated by 1/phi with phi = deviance / (n - p_full); the per-observation
624 // density term itself is the exact gamma(shape=nu, mean=mu) log-likelihood.
625 double phi = residual_deviance / std::max(1.0, static_cast<double>(n - p_full));
626 for (std::size_t i = 0; i < n; ++i) {
627 if (mu[i] > 0.0 && y[i] > 0.0) {
628 double nu = 1.0 / phi;
629 log_likelihood += nu * std::log(nu / mu[i]) - std::lgamma(nu)
630 + (nu - 1.0) * std::log(y[i]) - nu * y[i] / mu[i];
631 }
632 }
633 break;
634 }
635 default:
636 log_likelihood = -0.5 * residual_deviance;
637 break;
638 }
639
640 // AIC and BIC
641 double n_d = static_cast<double>(n);
642 double k = static_cast<double>(p_full);
643 if (family == distribution_family::gaussian) {
644 k += 1.0; // sigma^2 is also an estimated parameter
645 }
646 double aic = -2.0 * log_likelihood + 2.0 * k;
647 double bic = -2.0 * log_likelihood + k * std::log(n_d);
648
649 return {
650 beta, coefficient_se, z_statistics, p_values,
651 null_deviance, residual_deviance,
652 static_cast<double>(n - 1), static_cast<double>(n - p_full),
653 aic, bic, log_likelihood, null_log_likelihood,
654 iter, converged,
655 link, family
656 };
657}
658
659// ============================================================================
660// Logistic Regression
661// ============================================================================
662
676 const std::vector<std::vector<double>>& X,
677 const std::vector<double>& y,
678 std::size_t max_iter = 100,
679 double tol = 1e-8)
680{
681 // Verify that X does not contain an intercept column (from linear_regression.hpp)
682 statcpp::detail::validate_no_intercept_column(X, "logistic_regression");
683
684 // Verify that y is in [0, 1] range
685 for (double yi : y) {
686 if (yi < 0.0 || yi > 1.0) {
687 throw std::invalid_argument("statcpp::logistic_regression: y must be in [0, 1]");
688 }
689 }
690
691 return glm_fit(X, y, distribution_family::binomial, link_function::logit, max_iter, tol);
692}
693
705inline double predict_probability(const glm_result& model, const std::vector<double>& x)
706{
708 throw std::invalid_argument("statcpp::predict_probability: model must be binomial");
709 }
710 if (x.size() + 1 != model.coefficients.size()) {
711 throw std::invalid_argument("statcpp::predict_probability: x dimension mismatch");
712 }
713
714 double eta = model.coefficients[0];
715 for (std::size_t i = 0; i < x.size(); ++i) {
716 eta += model.coefficients[i + 1] * x[i];
717 }
718
719 return detail::inverse_link(eta, model.link);
720}
721
731inline std::vector<double> odds_ratios(const glm_result& model)
732{
734 throw std::invalid_argument("statcpp::odds_ratios: requires logistic regression model");
735 }
736
737 std::vector<double> or_values(model.coefficients.size() - 1);
738 for (std::size_t i = 1; i < model.coefficients.size(); ++i) {
739 or_values[i - 1] = std::exp(model.coefficients[i]);
740 }
741 return or_values;
742}
743
754inline std::vector<std::pair<double, double>> odds_ratios_ci(
755 const glm_result& model, double confidence = 0.95)
756{
758 throw std::invalid_argument("statcpp::odds_ratios_ci: requires logistic regression model");
759 }
760 if (confidence <= 0.0 || confidence >= 1.0) {
761 throw std::invalid_argument("statcpp::odds_ratios_ci: confidence must be in (0, 1)");
762 }
763
764 double z = norm_quantile(1.0 - (1.0 - confidence) / 2.0);
765
766 std::vector<std::pair<double, double>> ci(model.coefficients.size() - 1);
767 for (std::size_t i = 1; i < model.coefficients.size(); ++i) {
768 double beta = model.coefficients[i];
769 double se = model.coefficient_se[i];
770 double lower = std::exp(beta - z * se);
771 double upper = std::exp(beta + z * se);
772 ci[i - 1] = {lower, upper};
773 }
774 return ci;
775}
776
777// ============================================================================
778// Poisson Regression
779// ============================================================================
780
795 const std::vector<std::vector<double>>& X,
796 const std::vector<double>& y,
797 std::size_t max_iter = 100,
798 double tol = 1e-8)
799{
800 // Verify that X does not contain an intercept column
801 statcpp::detail::validate_no_intercept_column(X, "poisson_regression");
802
803 // Verify that y is non-negative
804 for (double yi : y) {
805 if (yi < 0.0) {
806 throw std::invalid_argument("statcpp::poisson_regression: y must be non-negative");
807 }
808 }
809
810 return glm_fit(X, y, distribution_family::poisson, link_function::log, max_iter, tol);
811}
812
824inline double predict_count(const glm_result& model, const std::vector<double>& x)
825{
827 throw std::invalid_argument("statcpp::predict_count: model must be Poisson");
828 }
829 if (x.size() + 1 != model.coefficients.size()) {
830 throw std::invalid_argument("statcpp::predict_count: x dimension mismatch");
831 }
832
833 double eta = model.coefficients[0];
834 for (std::size_t i = 0; i < x.size(); ++i) {
835 eta += model.coefficients[i + 1] * x[i];
836 }
837
838 return detail::inverse_link(eta, model.link);
839}
840
850inline std::vector<double> incidence_rate_ratios(const glm_result& model)
851{
853 throw std::invalid_argument("statcpp::incidence_rate_ratios: requires Poisson regression model");
854 }
855
856 std::vector<double> irr(model.coefficients.size() - 1);
857 for (std::size_t i = 1; i < model.coefficients.size(); ++i) {
858 irr[i - 1] = std::exp(model.coefficients[i]);
859 }
860 return irr;
861}
862
863// ============================================================================
864// GLM Diagnostics
865// ============================================================================
866
873 std::vector<double> response;
874 std::vector<double> pearson;
875 std::vector<double> deviance;
876 std::vector<double> working;
877};
878
891 const glm_result& model,
892 const std::vector<std::vector<double>>& X,
893 const std::vector<double>& y)
894{
895 std::size_t n = X.size();
896 if (n != y.size()) {
897 throw std::invalid_argument("statcpp::compute_glm_residuals: X and y must have same length");
898 }
899
900 std::vector<double> response(n);
901 std::vector<double> pearson(n);
902 std::vector<double> deviance_res(n);
903 std::vector<double> working(n);
904
905 for (std::size_t i = 0; i < n; ++i) {
906 // Calculate linear predictor
907 double eta = model.coefficients[0];
908 for (std::size_t j = 0; j < X[i].size(); ++j) {
909 eta += model.coefficients[j + 1] * X[i][j];
910 }
911
912 double mu = detail::inverse_link(eta, model.link);
913
914 // Response residuals
915 response[i] = y[i] - mu;
916
917 // Pearson residuals
918 double var = detail::variance_function(mu, model.family);
919 pearson[i] = response[i] / std::sqrt(var);
920
921 // Deviance residuals
922 double d = detail::deviance_residual(y[i], mu, model.family);
923 int sign = (y[i] >= mu) ? 1 : -1;
924 deviance_res[i] = sign * std::sqrt(d);
925
926 // Working residuals
927 double g_prime = detail::link_derivative(mu, model.link);
928 working[i] = response[i] * g_prime;
929 }
930
931 return {response, pearson, deviance_res, working};
932}
933
946inline double overdispersion_test(const glm_result& model,
947 const std::vector<std::vector<double>>& X,
948 const std::vector<double>& y)
949{
951 throw std::invalid_argument("statcpp::overdispersion_test: requires Poisson model");
952 }
953
954 auto residuals = compute_glm_residuals(model, X, y);
955
956 // Pearson chi-square statistic
957 double pearson_chi2 = 0.0;
958 for (double r : residuals.pearson) {
959 pearson_chi2 += r * r;
960 }
961
962 // Variance estimate (overdispersion parameter)
963 double dispersion = pearson_chi2 / model.df_residual;
964
965 return dispersion;
966}
967
977inline double pseudo_r_squared_mcfadden(const glm_result& model)
978{
979 if (model.null_log_likelihood == 0.0) return 0.0;
980 return 1.0 - model.log_likelihood / model.null_log_likelihood;
981}
982
999inline double pseudo_r_squared_nagelkerke(const glm_result& model,
1000 const std::vector<double>& y,
1001 std::size_t n)
1002{
1003 double n_d = static_cast<double>(n);
1004 double ll_model = model.log_likelihood;
1005
1006 // Compute saturated model log-likelihood
1007 double ll_saturated = 0.0;
1008 switch (model.family) {
1010 // For Gaussian, saturated LL = 0 (perfect fit, residual = 0)
1011 // Not exactly 0 but the deviance relationship gives us:
1012 // ll_null = ll_saturated - null_deviance/2
1013 // For Gaussian with MLE sigma^2: ll_saturated ≈ -n/2*(log(2*pi) + 1) when sigma -> 0
1014 // Use the simpler relationship directly
1015 break;
1017 // Saturated LL for 0/1 data is 0
1018 ll_saturated = 0.0;
1019 break;
1021 for (std::size_t i = 0; i < n; ++i) {
1022 if (y[i] > 0.0) {
1023 ll_saturated += y[i] * std::log(y[i]) - y[i] - std::lgamma(y[i] + 1.0);
1024 }
1025 // y[i] == 0 contributes 0
1026 }
1027 break;
1028 default:
1029 break;
1030 }
1031
1032 double ll_null;
1034 // For Gaussian: use -null_deviance/2 as approximation (exact for Gaussian)
1035 ll_null = -model.null_deviance / 2.0;
1036 } else {
1037 ll_null = ll_saturated - model.null_deviance / 2.0;
1038 }
1039
1040 double r2_cox_snell = 1.0 - std::exp(2.0 * (ll_null - ll_model) / n_d);
1041 double r2_max = 1.0 - std::exp(2.0 * ll_null / n_d);
1042
1043 if (r2_max == 0.0) return 0.0;
1044
1045 return r2_cox_snell / r2_max;
1046}
1047
1048} // namespace statcpp
Basic statistical computation functions.
Continuous distribution functions.
Linear regression analysis.
double link_derivative(double mu, link_function link)
Derivative of link function d(eta)/d(mu) = g'(mu)
Definition glm.hpp:160
double variance_function(double mu, distribution_family family)
Variance function V(mu)
Definition glm.hpp:200
double link_transform(double mu, link_function link)
Link function g(mu) -> eta.
Definition glm.hpp:95
double deviance_residual(double y, double mu, distribution_family family)
Calculate deviance (for a single observation)
Definition glm.hpp:228
double inverse_link(double eta, link_function link)
Inverse link function g^{-1}(eta) -> mu.
Definition glm.hpp:127
std::vector< double > solve_weighted_least_squares(const std::vector< std::vector< double > > &X, const std::vector< double > &z, const std::vector< double > &w, std::vector< std::vector< double > > &XtWX_inv)
Solve weighted least squares.
Definition glm.hpp:276
std::vector< double > matrix_vector_multiply(const std::vector< std::vector< double > > &A, const std::vector< double > &v)
Calculate matrix-vector product.
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.
glm_residuals compute_glm_residuals(const glm_result &model, const std::vector< std::vector< double > > &X, const std::vector< double > &y)
Calculate GLM residuals.
Definition glm.hpp:890
double predict_probability(const glm_result &model, const std::vector< double > &x)
Probability prediction with logistic regression.
Definition glm.hpp:705
constexpr double pi
Pi constant.
double normal_pdf(double x, double mu=0.0, double sigma=1.0)
Normal distribution probability density function (PDF)
auto sum(Iterator first, Iterator last)
Sum.
double var(Iterator first, Iterator last, std::size_t ddof=0)
Variance (ddof = Delta Degrees of Freedom)
double norm_cdf(double x)
Standard normal CDF.
double norm_quantile(double p)
Standard normal quantile function.
glm_result poisson_regression(const std::vector< std::vector< double > > &X, const std::vector< double > &y, std::size_t max_iter=100, double tol=1e-8)
Poisson regression.
Definition glm.hpp:794
double beta(double a, double b)
Beta function.
std::vector< double > odds_ratios(const glm_result &model)
Calculate odds ratios.
Definition glm.hpp:731
glm_result glm_fit(const std::vector< std::vector< double > > &X, const std::vector< double > &y, distribution_family family=distribution_family::gaussian, link_function link=link_function::identity, std::size_t max_iter=100, double tol=1e-8)
Fit a generalized linear model.
Definition glm.hpp:401
double mean(Iterator first, Iterator last)
Arithmetic mean.
double bic(double log_likelihood, std::size_t n, std::size_t k)
Calculate BIC (Bayesian Information Criterion)
double predict_count(const glm_result &model, const std::vector< double > &x)
Expected count prediction with Poisson regression.
Definition glm.hpp:824
double overdispersion_test(const glm_result &model, const std::vector< std::vector< double > > &X, const std::vector< double > &y)
Overdispersion test (for Poisson regression)
Definition glm.hpp:946
double aic(double log_likelihood, std::size_t k)
Calculate AIC (Akaike Information Criterion)
std::vector< double > incidence_rate_ratios(const glm_result &model)
Calculate Incidence Rate Ratios.
Definition glm.hpp:850
glm_result logistic_regression(const std::vector< std::vector< double > > &X, const std::vector< double > &y, std::size_t max_iter=100, double tol=1e-8)
Logistic regression.
Definition glm.hpp:675
distribution_family
Distribution family.
Definition glm.hpp:49
@ poisson
Poisson distribution.
@ gaussian
Gaussian (normal) distribution.
@ gamma_family
Gamma distribution (gamma_family because gamma is a reserved word)
@ binomial
Binomial distribution.
double pseudo_r_squared_nagelkerke(const glm_result &model, const std::vector< double > &y, std::size_t n)
Nagelkerke's pseudo R-squared.
Definition glm.hpp:999
std::vector< std::pair< double, double > > odds_ratios_ci(const glm_result &model, double confidence=0.95)
Confidence intervals for odds ratios.
Definition glm.hpp:754
link_function
Link function types.
Definition glm.hpp:35
@ logit
Logit link (logistic regression)
@ probit
Probit link.
@ inverse
Inverse link (Gamma regression)
@ cloglog
Complementary log-log link.
@ log
Log link (Poisson regression)
@ identity
Identity link (linear regression)
double pseudo_r_squared_mcfadden(const glm_result &model)
McFadden's pseudo R-squared.
Definition glm.hpp:977
GLM residuals structure.
Definition glm.hpp:872
std::vector< double > deviance
Deviance residuals.
Definition glm.hpp:875
std::vector< double > working
Working residuals.
Definition glm.hpp:876
std::vector< double > response
Response residuals (y - mu)
Definition glm.hpp:873
std::vector< double > pearson
Pearson residuals.
Definition glm.hpp:874
GLM result structure.
Definition glm.hpp:61
double bic
BIC.
Definition glm.hpp:71
std::vector< double > z_statistics
z-statistics (or Wald statistics)
Definition glm.hpp:64
std::size_t iterations
Number of iterations until convergence.
Definition glm.hpp:74
link_function link
Link function used.
Definition glm.hpp:76
double df_residual
Residual degrees of freedom.
Definition glm.hpp:69
distribution_family family
Distribution family used.
Definition glm.hpp:77
std::vector< double > coefficient_se
Standard errors of coefficients.
Definition glm.hpp:63
std::vector< double > coefficients
Regression coefficients.
Definition glm.hpp:62
bool converged
Whether convergence was achieved.
Definition glm.hpp:75
double null_deviance
Null deviance.
Definition glm.hpp:66
std::vector< double > p_values
p-values
Definition glm.hpp:65
double df_null
Null model degrees of freedom.
Definition glm.hpp:68
double residual_deviance
Residual deviance.
Definition glm.hpp:67
double null_log_likelihood
Null model log-likelihood.
Definition glm.hpp:73
double log_likelihood
Log-likelihood.
Definition glm.hpp:72
double aic
AIC.
Definition glm.hpp:70