fastglmm
Massively scalable generalized linear mixed models
Loading...
Searching...
No Matches
linearRegression.h
Go to the documentation of this file.
1/***************************************************************
2 * @file linearRegression.h
3 * @author Gabriel Hoffman
4 * @email gabriel.hoffman@mssm.edu
5 * @brief Evaluate linear regression with Armadillo library
6 * Copyright (C) 2024 Gabriel Hoffman
7 **************************************************************/
8
9
10#ifndef LINEAR_REGRESSION_H_
11#define LINEAR_REGRESSION_H_
12
13#include <tuple>
14#include <type_traits>
15
16// if -D USE_R, use RcppArmadillo library
17#ifdef USE_R
18// [[Rcpp::depends(RcppParallel)]]
19// [[Rcpp::depends(RcppParallel)]]
20#include <RcppArmadillo.h>
21#include <RcppParallel.h>
22#else
23#include <armadillo>
24#endif
25
26#include "misc.h"
27#include "CleanData.h"
28#include "ModelFit.h"
29
30using namespace arma;
31using namespace std;
32
33namespace fastglmmLib {
34
37struct LMWork {
38 mat Q, R, V;
40 LMWork() {}
41};
42
56static ModelFit lm(
57 const arma::mat& X,
58 const arma::colvec& y,
59 const ModelDetail md = LOW,
60 const double &lambda = 0,
61 const double &rdf_offset = 0,
62 LMWork *work = nullptr,
63 const bool estimateDispersion = true,
64 const bool scaleByDispersion = true) {
65
66 int n = X.n_rows, k = X.n_cols;
67
68 // allocate work, if not already alloc'd
69 bool alloc_local = false;
70 if( work == nullptr ){
71 alloc_local = true;
72 work = new LMWork();
73 }
74
75 // QR decomp
76 // success0 indicates QR was successful and R is valid
77 bool success0 = qr_econ(work->Q, work->R, X);
78
79 // test if X is full rank
80 double minDiagR = abs(diagvec(work->R)).min();
81 double tol = 1e-12;
82 if( success0 && (minDiagR < tol)){
83 success0 = false;
84 }
85
86 vec beta;
87 bool success1 = false;
88
89 // cross product for ridge and vcov
90 mat A = work->R.t() * work->R;
91
92 // if QR succeed, compute beta
93 if( success0 ){
94 if( lambda == 0.0 ){
95 // OLS
96 success1 = solve(beta, work->R, trans(work->Q) * y, solve_opts::no_approx);
97 }else{
98 // Ridge penalty,
99 // A.diag() += lambda;
100 // but not on intercept
101 for( uword i=1; i<A.n_rows; ++i){
102 A(i,i) += lambda;
103 }
104
105 success1 = solve(beta, A, work->R.t() * work->Q.t() * y, arma::solve_opts::fast + arma::solve_opts::likely_sympd);
106 }
107 }
108
109 // if system is singular,
110 // set beta to NaN
111 if( ! success0 || ! success1){
112 beta = vec(work->R.n_cols);
113 beta.fill(datum::nan);
114 }
115
116 // if md != LEAST, compute residuals
117 vec stderr;
118 double rdf = n - k - rdf_offset;
119 double dispersion = 1.0;
120 bool success2 = true;
121 if( md != LEAST ){
122
123 if( success1 ){
124 // only compute inverse if solve() above succeeded
125 // V = solve(t(R)*R)
126 success2 = inv_sympd(work->V, A);
127 }else{
128 success2 = false;
129 }
130
131 // residuals
132 work->residuals = y - X*beta;
133
134 // for linear regression
135 if( estimateDispersion ){
136 // std.errors of coefficients
137 dispersion = dot(work->residuals, work->residuals) / rdf;
138 }else{
139 // for GLM, don't scale by residual variance
140 dispersion = 1.0;
141 }
142
143 if( success0 && success1 && success2 ){
144 stderr = sqrt(dispersion * diagvec(work->V));
145 }else{
146 stderr = vec(k, fill::value(datum::nan));
147 beta.fill(datum::nan);
148 }
149 }
150
151 bool success = success0 && success1 && success2;
152
153 // return results with specified level of detail
154 ModelFit fit;
155 switch( md ){
156 case LEAST:
157 fit = ModelFit( success, beta );
158 break;
159
160 case LOW:
161 fit = ModelFit( success, beta, stderr, dispersion, rdf);
162 break;
163
164 case MEDIUM:
165 fit = ModelFit( success, beta, stderr, dispersion, rdf, work->V * dispersion);
166 break;
167
168 case HIGH:
169 fit = ModelFit( success, beta, stderr, dispersion, rdf, work->V * dispersion, work->residuals);
170 break;
171
172 case MOST:
173 case MAX:
174 vec hatvalues = diagvec(work->Q * trans(work->Q));
175 fit = ModelFit( success, beta, stderr, dispersion, rdf, work->V * dispersion, work->residuals, hatvalues);
176 fit.setFittedValues( X*beta );
177 break;
178 }
179
180 // free work if allocated in this function
181 if( alloc_local) delete work;
182
183 return fit;
184}
185
194template <typename T>
195static tuple<vec, T> preprojection(
196 const vec &y,
197 const mat &X_design,
198 const T &X_features,
199 const vec &weights = {}){
200
201 // naive calculation
202 // vec y_proj = y - X_design * inv(trans(X_design) * X_design) * trans(X_design) * y;
203 // mat X_proj = X_features - X_design * inv(trans(X_design) * X_design) * trans(X_design) * X_features;
204
205 // if weights is empty, set w to ones
206 vec w;
207 if( ! weights.is_empty() ){
208 w = weights;
209 }else{
210 w = vec(y.n_elem).ones();
211 }
212 arma::colvec wsqrt = sqrt(w / mean(w));
213
214 // apply weights in computation to y, X_design, and X_features
215 mat X_design_wsqrt = X_design.each_col() % wsqrt;
216 vec y_wsqrt = y % wsqrt;
217 T X_features_wsqrt = scaleEachCol(X_features, wsqrt);
218
219 // Use QR decomp of X_design, and recycle pre-computed values
220 mat Q, R;
221 qr_econ(Q, R, X_design_wsqrt);
222
223 // back solve
224 vec beta = solve(R, trans(Q) * y_wsqrt);
225 vec y_proj = y_wsqrt - X_design_wsqrt * beta;
226
227 // back solve
228 // use constructor T() to subtract matricies of the same type
229 mat gamma = solve(R, trans(Q) * X_features_wsqrt);
230 T X_proj;
231
232 // cast X_design_wsqrt * gamma to type T if needed
233 if( is_same_v<decltype(X_features_wsqrt), decltype(X_design_wsqrt)> ){
234 X_proj = X_features_wsqrt - X_design_wsqrt * gamma;
235 }else{
236 X_proj = X_features_wsqrt - T(X_design_wsqrt * gamma);
237 }
238
239 return {y_proj, X_proj};
240}
241
242
250template <typename T>
251static tuple<vec, T> preprojection(
252 const vec &y,
253 const sp_mat &X_design,
254 const T &X_features,
255 const vec &weights = {}){
256
257 // naive calculation
258 // vec y_proj = y - X_design * inv(trans(X_design) * X_design) * trans(X_design) * y;
259 // mat X_proj = X_features - X_design * inv(trans(X_design) * X_design) * trans(X_design) * X_features;
260
261 // if weights is empty, set w to ones
262 vec w;
263 if( ! weights.is_empty() ){
264 w = weights;
265 }else{
266 w = vec(y.n_elem).ones();
267 }
268 arma::colvec wsqrt = sqrt(w / mean(w));
269
270 // apply weights in computation to y, X_design, and X_features
271 sp_mat X_design_wsqrt = scaleEachCol(X_design, wsqrt);
272 vec y_wsqrt = y % wsqrt;
273 T X_features_wsqrt = scaleEachCol(X_features, wsqrt);
274
275 // recycle sparse crossprod
276 // spsolve uses lapack after converting V to dense matrix
277 // minimal penalty practical V dimensions
278 bool success;
279 vec beta;
280 mat gamma;
281
282 sp_mat V = trans(X_design_wsqrt) * X_design_wsqrt;
283 success = spsolve(beta, V, vec(trans(X_design_wsqrt) * y_wsqrt), "lapack");
284 if( ! success ) beta.fill(datum::nan);
285 vec y_proj = y_wsqrt - X_design_wsqrt * beta;
286
287 success = spsolve(gamma, V, mat(trans(X_design_wsqrt) * X_features_wsqrt), "lapack");
288 if( ! success ) gamma.fill(datum::nan);
289
290 T X_proj;
291 // cast X_design_wsqrt * gamma to type T if needed
292 if( is_same_v<decltype(X_features_wsqrt), decltype(X_design_wsqrt)> ){
293 X_proj = X_features_wsqrt - X_design_wsqrt * gamma;
294 }else{
295 X_proj = X_features_wsqrt - T(X_design_wsqrt * gamma);
296 }
297
298 return {y_proj, X_proj};
299}
300
301
302
314static ModelFit wlm(
315 const arma::mat& X,
316 const arma::colvec& y,
317 const arma::colvec& w = {},
318 const ModelDetail md = LOW,
319 const double &lambda = 0,
320 const double &rdf_offset = 0,
321 LMWork *work = nullptr) {
322
323 ModelFit fit;
324
325 if( w.is_empty() ){
326 fit = lm( X, y, md, lambda, rdf_offset, work);
327 }else{
328 arma::colvec wsqrt = sqrt(w / mean(w));
329 fit = lm( X.each_col() % wsqrt, y % wsqrt, md, lambda, rdf_offset, work);
330
331 if( md >= HIGH){
332 // Rescale residuals by weights afterward
333 // since input X and y are scaled before lm()
334 fit.residuals /= wsqrt;
335 }
336 }
337
338 return fit;
339}
340
352static vector<ModelFit> lmFitFeatures_standard(
353 const arma::vec &y,
354 const arma::mat &X_design,
355 const arma::mat &X_features,
356 const vector<string> &ids,
357 const arma::vec &weights = {},
358 const ModelDetail md = LOW,
359 const double &lambda = 0,
360 const int &nthreads = 1){
361
362 int n_covs = X_design.n_cols;
363
364 if( X_features.n_cols == 0){
365 throw invalid_argument("X_features has 0 columns");
366 }
367
368 vector<ModelFit> fitList(X_features.n_cols, ModelFit());
369
370 // Parallel part using Thread Building Blocks
371 tbb::task_arena limited_arena(nthreads);
372 limited_arena.execute([&] {
373 tbb::parallel_for(
374 tbb::blocked_range<int>(0, X_features.n_cols, 100),
375 [&](const tbb::blocked_range<int>& r){
376 // create design matrix with jth feature in the last column
377 // X = cbind(X_design, X_features[,0])
378 arma::mat X(X_design);
379 X.insert_cols(n_covs, X_features.col(0));
380
381 LMWork *work = new LMWork();
382
383 // iterate through responses
384 for (int j = r.begin(); j != r.end(); ++j) {
385
386 // Create design matrix with intercept as first column
387 X.col(n_covs) = X_features.col(j);
388
389 // linear regression
390 ModelFit fit = wlm(X, y, weights, md, lambda, 0, work);
391 fit.ID = ids[j];
392
393 // save result to list
394 fitList.at(j) = fit;
395 }
396 delete work;
397 }); });
398
399 return fitList;
400}
401
402
403
415template <typename T1, typename T2>
416static ModelFitList lmFitFeatures_preproj(
417 const arma::vec &y,
418 const T1 &X_design,
419 const T2 &X_features,
420 const vector<string> &ids,
421 const arma::vec &weights = {},
422 const ModelDetail md = LOW,
423 const double &lambda = 0,
424 const int &nthreads = 1){
425
426 if( X_features.n_cols == 0){
427 throw invalid_argument("X_features has 0 columns");
428 }
429
430 ModelFitList fitList(X_features.n_cols, ModelFit());
431
432 // pre-projection to regress out the covariates first
433 // set rdf_offset to the number of covariates projected out
434 // auto [y_proj, X_proj] = preprojection(y, X_design, X_features, weights);
435 vec y_proj;
436 mat X_proj;
437 tie(y_proj, X_proj) = preprojection(y, X_design, X_features, weights);
438 double rdf_offset = X_design.n_cols;
439
440 vec wsqrt = sqrt(weights);
441
442 // Parallel part using Thread Building Blocks
443 tbb::task_arena limited_arena(nthreads);
444 limited_arena.execute([&] {
445 tbb::parallel_for(
446 tbb::blocked_range<int>(0, X_proj.n_cols, 100),
447 [&](const tbb::blocked_range<int>& r){
448
449 disable_parallel_blas();
450
451 LMWork *work = new LMWork();
452
453 for (int j = r.begin(); j != r.end(); ++j) {
454
455 // linear regression
456 ModelFit fit = lm(X_proj.col(j), y_proj, md, lambda, rdf_offset, work );
457
458 fit.ID = ids[j];
459
460 if( md >= HIGH){
461 // Rescale residuals by weights afterward
462 // since input X and y are scaled before lm()
463 fit.residuals /= wsqrt;
464 }
465
466 // save result to list
467 fitList.at(j) = fit;
468 }
469 delete work;
470 }); });
471
472 return fitList;
473}
474
487template <typename T1, typename T2>
488static ModelFitList lmFitFeatures(
489 const arma::vec &y,
490 const T1 &X_design,
491 const T2 &X_features,
492 const vector<string> &ids,
493 const arma::vec &weights = {},
494 const ModelDetail md = LOW,
495 const double &lambda = 0,
496 const bool &preprojection = true,
497 const int &nthreads = 1){
498
499 ModelFitList fitList;
500
501 if( preprojection ){
502 // supports mat and sp_mat
503 fitList = lmFitFeatures_preproj(y, X_design, X_features, ids, weights, md, lambda, nthreads);
504 }else{
505 // only supports mat
506 fitList = lmFitFeatures_standard(y, mat(X_design), mat(X_features), ids, weights, md, lambda, nthreads);
507 }
508
509 return fitList;
510}
511
512
513
514
515
516
528static ModelFitList lmFitResponses(
529 const arma::mat &Y,
530 const arma::mat &X,
531 const vector<string> &ids,
532 const arma::mat &Weights,
533 const ModelDetail md = LOW,
534 const double &lambda = 0,
535 const int &nthreads = 1){
536
537 ModelFitList fitList(Y.n_cols, ModelFit());
538
539 CleanData data(Y, X, Weights);
540
541 mat X_clean = data.get_X();
542 mat Wsqrt = sqrt(data.get_W());
543 mat Yw = data.get_Y() % Wsqrt;
544
545 // Parallel part using Thread Building Blocks
546 tbb::task_arena limited_arena(nthreads);
547 limited_arena.execute([&] {
548 tbb::parallel_for(
549 tbb::blocked_range<int>(0, Y.n_cols, 10),
550 [&](const tbb::blocked_range<int>& r){
551
552 disable_parallel_blas();
553
554 for (int j = r.begin(); j != r.end(); ++j) {
555
556 // Reduce residual degrees of freedom by the number of
557 // entries with zero weights
558 int rdf_offset = accu(Wsqrt.col(j) == 0.0);
559
560 // linear regression
561 // ModelFit fit = wlm(X, Y.col(j), Weights.col(j));
562 ModelFit fit = lm(X_clean.each_col() % Wsqrt.col(j),
563 Yw.col(j), md, lambda, rdf_offset);
564
565 fit.ID = ids[j];
566
567 if( md >= HIGH){
568 // Rescale residuals by weights afterward
569 // since input X and y are scaled before lm()
570 fit.residuals /= Wsqrt.col(j);
571 fit.mu /= Wsqrt.col(j);
572 }
573
574 // save result to list
575 fitList.at(j) = fit;
576 }
577 }); });
578
579 return fitList;
580}
581
582}
583
584
585
586#endif
Definition ModelFit.h:37
mat scaleEachCol(const mat &X, const vec &w)
Definition misc.h:16
Definition CleanData.h:17
ModelDetail
Definition ModelFit.h:26
@ MOST
Definition ModelFit.h:31
@ MEDIUM
Definition ModelFit.h:29
@ LEAST
Definition ModelFit.h:27
@ HIGH
Definition ModelFit.h:30
@ MAX
Definition ModelFit.h:32
@ LOW
Definition ModelFit.h:28
vector< ModelFit > ModelFitList
Definition ModelFit.h:393
Definition linearRegression.h:37
mat R
Definition linearRegression.h:38
vec residuals
Definition linearRegression.h:39
mat V
Definition linearRegression.h:38
LMWork()
Definition linearRegression.h:40
mat Q
Definition linearRegression.h:38