Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Cross-Validation vs Bayesian Regularization for Ridge Regression

We look at an example of time series data yty_t. We fit the model:

yt=μt+ϵt\begin{align*} y_t = \mu_t + \epsilon_t \end{align*}

where ϵt∼i.i.dN(0,σ2)\epsilon_t \overset{\text{i.i.d}}{\sim} N(0, \sigma^2), and

μt=β0+β1(t−1)+∑j=2n−2βj(t−j)+.\begin{align*} \mu_t = \beta_0 + \beta_1 (t - 1) + \sum_{j=2}^{n-2} \beta_j (t - j)_+. \end{align*}

Estimation of β0,…,βn−1\beta_0, \dots, \beta_{n-1} will be done via ridge regression:

minimize[∑t=1n(yt−β0−β1(t−1)−∑j=2n−1βj(t−j)+)2+λ∑j=2n−1βj2].\begin{align*} \text{minimize} \left[\sum_{t=1}^n \left(y_t - \beta_0 - \beta_1(t - 1) - \sum_{j=2}^{n-1} \beta_j (t - j)_+\right)^2 + \lambda \sum_{j=2}^{n-1} \beta_j^2 \right]. \end{align*}

for a fixed λ>0\lambda > 0. The choice of λ\lambda is done either via 5-fold Cross-Valication (for a particular selection of folds), or via the Bayesian method discussed in Lectures 12-13. The Bayesian method also yields an alternative estimate of β0,…,βn−1\beta_0, \dots, \beta_{n-1} (along with uncertainty quantification).

We work with the FRED data on weekly initial unemployment claims (from https://fred.stlouisfed.org/series/ICSA).

  observation_date    ICSA
0       1967-01-07  208000
1       1967-01-14  207000
2       1967-01-21  217000
3       1967-01-28  204000
4       1967-02-04  216000
5       1967-02-11  229000
6       1967-02-18  229000
7       1967-02-25  242000
8       1967-03-04  310000
9       1967-03-11  241000
     observation_date    ICSA
3108       2026-08-01  200000
3109       2026-08-08  212000
3110       2026-08-15  207000
3111       2026-08-22  204000
3112       2026-08-29  207000
3113       2026-09-05  207000
3114       2026-09-12  198000
3115       2026-09-19  198000
3116       2026-09-26  199000
3117       2026-10-03  197000
<Figure size 600x400 with 1 Axes>

We use all available data before March 2020 so that the exceptional surge in unemployment claims during COVID-19 does not dominate the analysis.

2774
<Figure size 640x480 with 1 Axes>

We will apply our model to this data, and obtain estimates of the underlying trend. Note that, in the context of our model, trend means the fitted function:

β^0+β^1(t−1)+∑j=2n−1β^j(t−j)+.\begin{align*} \hat{\beta}_0 + \hat{\beta}_1 (t - 1) + \sum_{j=2}^{n-1} \hat{\beta}_j (t - j)_+. \end{align*}

Recall the following function for fitting ridge regressions.

Given λ\lambda, the ridge regression estimate is computed as follows.

2774
[[1.000e+00 0.000e+00 0.000e+00 ... 0.000e+00 0.000e+00 0.000e+00]
 [1.000e+00 1.000e+00 0.000e+00 ... 0.000e+00 0.000e+00 0.000e+00]
 [1.000e+00 2.000e+00 1.000e+00 ... 0.000e+00 0.000e+00 0.000e+00]
 ...
 [1.000e+00 2.771e+03 2.770e+03 ... 1.000e+00 0.000e+00 0.000e+00]
 [1.000e+00 2.772e+03 2.771e+03 ... 2.000e+00 1.000e+00 0.000e+00]
 [1.000e+00 2.773e+03 2.772e+03 ... 3.000e+00 2.000e+00 1.000e+00]]
<Figure size 640x480 with 1 Axes>

It is clear that when λ=λmin⁡:=10−2\lambda = \lambda_{\min} := 10^{-2}, the fitted trend function nearly reproduces the data, while when λ=λmax⁡:=1013\lambda = \lambda_{\max} := 10^{13}, the fitted trend function fits a line to the data. Below we see which value of λ\lambda in the range [λmin⁡,λmax⁡][\lambda_{\min}, \lambda_{\max}] will be picked by the various methods.

[1.00000000e-02 4.21696503e-02 1.77827941e-01 7.49894209e-01
 3.16227766e+00 1.33352143e+01 5.62341325e+01 2.37137371e+02
 1.00000000e+03 4.21696503e+03 1.77827941e+04 7.49894209e+04
 3.16227766e+05 1.33352143e+06 5.62341325e+06 2.37137371e+07
 1.00000000e+08 4.21696503e+08 1.77827941e+09 7.49894209e+09
 3.16227766e+10 1.33352143e+11 5.62341325e+11 2.37137371e+12
 1.00000000e+13]

First, we use the Cross-Validation method that we used in lectures.

31622776601.683792
Lambda = 0.01, CV Error = 6044800264801.299805
Lambda = 0.04, CV Error = 4583338220512.932617
Lambda = 0.18, CV Error = 2972813447403.703125
Lambda = 0.75, CV Error = 1150648448062.330322
Lambda = 3.16, CV Error = 223170347690.249420
Lambda = 13.34, CV Error = 71945038833.736679
Lambda = 56.23, CV Error = 138940419802.570526
Lambda = 237.14, CV Error = 269596496587.779053
Lambda = 1000.00, CV Error = 122089139443.817184
Lambda = 4216.97, CV Error = 58547640767.912560
Lambda = 17782.79, CV Error = 372394665563.129211
Lambda = 74989.42, CV Error = 683332286501.163452
Lambda = 316227.77, CV Error = 672604194167.156494
Lambda = 1333521.43, CV Error = 459667639455.585693
Lambda = 5623413.25, CV Error = 253188813519.846924
Lambda = 23713737.06, CV Error = 110470558664.877991
Lambda = 100000000.00, CV Error = 38630679318.691208
Lambda = 421696503.43, CV Error = 18114264777.164085
Lambda = 1778279410.04, CV Error = 14903490337.725771
Lambda = 7498942093.32, CV Error = 13620464671.235716
Lambda = 31622776601.68, CV Error = 12470803123.056614
Lambda = 133352143216.33, CV Error = 12624112836.277460
Lambda = 562341325190.35, CV Error = 13234583451.785759
Lambda = 2371373705661.66, CV Error = 13532078908.740025
Lambda = 10000000000000.00, CV Error = 13618517077.366573

The λ\lambda value selected by this method is on the higher side. So the fitted trend function will be very smooth.

<Figure size 640x480 with 1 Axes>

Obviously this is a poor estimate of the underlying trend. This CV method uses consecutive data points as folds. The five folds used by CV can be visualized as follows.

<Figure size 1200x400 with 1 Axes>

If we fix a fold (for testing), and fit the model on the other 4 folds, the fitted function can be visualized (for a fixed λ\lambda) as follows.

<Figure size 1200x400 with 1 Axes>

Suppose the test fold is the 3rd fold. It should be clear from the above that if λ\lambda is chosen small, then the fitted function looks quite off compared to the actual data for fold = 3. This is the reason why a large λ\lambda is being chosen by CV.

Now we change the CV method by taking the five folds randomly. This was basically the CV method used in the last lab. We can fit this CV also with the same function ‘ridge_cv’ by permuting the rows of XX and yy before calling ‘ridge_cv’. Indeed, the original function makes consecutive folds in the row order it receives. Supplying a random permutation of the paired rows therefore creates random folds in calendar time. We do not alter ridge_cv (or solve_ridge_formula).

Note that it we should permute both X and y with the same indices. Shuffling only y, or constructing a new time basis after shuffling it, would change the regression problem. The original X, y, and calendar years below remain chronological for final fitting and plotting.

Random-fold CV choice: 3.16228
Lambda = 0.01, CV Error = 272745490.619259
Lambda = 0.04, CV Error = 262306220.845535
Lambda = 0.18, CV Error = 244923818.592328
Lambda = 0.75, CV Error = 229469165.409373
Lambda = 3.16, CV Error = 221911915.121373
Lambda = 13.34, CV Error = 224097584.509332
Lambda = 56.23, CV Error = 234316090.221303
Lambda = 237.14, CV Error = 255078837.166333
Lambda = 1000.00, CV Error = 290830557.667755
Lambda = 4216.97, CV Error = 345683295.627062
Lambda = 17782.79, CV Error = 435432552.065656
Lambda = 74989.42, CV Error = 591952498.679296
Lambda = 316227.77, CV Error = 853250442.777322
Lambda = 1333521.43, CV Error = 1232515535.905905
Lambda = 5623413.25, CV Error = 1777389144.216505
Lambda = 23713737.06, CV Error = 2458629596.932100
Lambda = 100000000.00, CV Error = 3133899885.518766
Lambda = 421696503.43, CV Error = 3637188609.825331
Lambda = 1778279410.04, CV Error = 4145433433.561262
Lambda = 7498942093.32, CV Error = 5037328266.327239
Lambda = 31622776601.68, CV Error = 5914365271.629758
Lambda = 133352143216.33, CV Error = 6706082913.173039
Lambda = 562341325190.35, CV Error = 7447422817.471208
Lambda = 2371373705661.66, CV Error = 7789050881.362502
Lambda = 10000000000000.00, CV Error = 7888459615.891314

The λ\lambda now chosen is quite small. This should lead to estimates that are much more noisy (compared to the previous case).

<Figure size 640x480 with 1 Axes>

Perhaps, the trend estimate now is too smooth.

So, depending on how the folds are chosen, CV can give λ\lambda that is quite high or quite low. Below, we plot the folds used in the second CV (random folds).

<Figure size 1200x400 with 1 Axes>

The distinct folds are hard to see now because they are randomly present all over the data.

<Figure size 1200x400 with 1 Axes>

Here is some intuition for why the two CV methods are behaving so differently. This is because the two CV schemes assess different prediction tasks. With consecutive folds, we remove a long stretch of observations, so the fitted curve must predict across a large gap or extrapolate beyond the training data. Flexible (overfitting) fits can perform poorly in these regions, leading CV to favor a large λ\lambda (smooth fit). With random folds, most held-out observations have nearby training observations. A flexible fit can use these neighbors to track the data and predict the held-out values accurately, favoring a small λ\lambda. Thus, the preferred amount of smoothing depends strongly on whether we assess prediction across long gaps or interpolation between nearby observations.

Next let us try Bayesian regularization for this problem.

The λ\lambda-values along with the log posterior and posterior probabilities are printed below.

Lambda = 1.000000e-02, Log posterior = -39349.146181, Posterior probability = 0.000000
Lambda = 4.216965e-02, Log posterior = -39213.268461, Posterior probability = 0.000000
Lambda = 1.778279e-01, Log posterior = -38944.516186, Posterior probability = 0.000000
Lambda = 7.498942e-01, Log posterior = -38633.681110, Posterior probability = 0.000000
Lambda = 3.162278e+00, Log posterior = -38380.291737, Posterior probability = 0.000000
Lambda = 1.333521e+01, Log posterior = -38220.433532, Posterior probability = 0.000000
Lambda = 5.623413e+01, Log posterior = -38163.001885, Posterior probability = 1.000000
Lambda = 2.371374e+02, Log posterior = -38210.543166, Posterior probability = 0.000000
Lambda = 1.000000e+03, Log posterior = -38355.714596, Posterior probability = 0.000000
Lambda = 4.216965e+03, Log posterior = -38595.649078, Posterior probability = 0.000000
Lambda = 1.778279e+04, Log posterior = -38942.510041, Posterior probability = 0.000000
Lambda = 7.498942e+04, Log posterior = -39384.280411, Posterior probability = 0.000000
Lambda = 3.162278e+05, Log posterior = -39868.593712, Posterior probability = 0.000000
Lambda = 1.333521e+06, Log posterior = -40346.644559, Posterior probability = 0.000000
Lambda = 5.623413e+06, Log posterior = -40794.918791, Posterior probability = 0.000000
Lambda = 2.371374e+07, Log posterior = -41165.867257, Posterior probability = 0.000000
Lambda = 1.000000e+08, Log posterior = -41433.508475, Posterior probability = 0.000000
Lambda = 4.216965e+08, Log posterior = -41628.055974, Posterior probability = 0.000000
Lambda = 1.778279e+09, Log posterior = -41841.266384, Posterior probability = 0.000000
Lambda = 7.498942e+09, Log posterior = -42086.277359, Posterior probability = 0.000000
Lambda = 3.162278e+10, Log posterior = -42277.271138, Posterior probability = 0.000000
Lambda = 1.333521e+11, Log posterior = -42431.218946, Posterior probability = 0.000000
Lambda = 5.623413e+11, Log posterior = -42535.174098, Posterior probability = 0.000000
Lambda = 2.371374e+12, Log posterior = -42575.332041, Posterior probability = 0.000000
Lambda = 1.000000e+13, Log posterior = -42586.400519, Posterior probability = 0.000000

Here we are only using 25 λ\lambda values (as in CV). The posterior probability is 1 exactly for one λ\lambda (and zero for all other λ\lambda’s). This λ\lambda value is the posterior maximizer. This value is small but not as small as the λ\lambda selected by the random-fold CV. So the fitted function will still be noisy (tracking the original data)but not as noisy as the ridge estimate obtained by the random-fold CV method.

Bayesian lambda:        5.623413e+01
Ridge CV lambda:        3.162278e+10
Ridge random-fold CV:   3.162278e+00

Below are the estimates of the trend functions corresponding to each of these λ\lambda choices.

<Figure size 1200x500 with 1 Axes>

It is hard to spot the differences between the random CV estimate and the Bayesian estimate. One can zoom in to see that the Bayesian estimate is indeed smoother.

<Figure size 1200x400 with 1 Axes>

One can also run the full Bayesian method to get posterior samples of β0,…,βn−1\beta_0, \dots, \beta_{n-1}, which also tracks uncertainty in λ\lambda. The relevant formulae here are given here. We work with the following prior:

β0,β1∼i.i.dN(0,C)   and   β2,…,βn−1∼i.i.dN(0,σ2/λ).\begin{align*} \beta_0, \beta_1 \overset{\text{i.i.d}}{\sim} N(0, C) ~~ \text{ and } ~~ \beta_2, \dots, \beta_{n-1} \overset{\text{i.i.d}}{\sim} N(0, \sigma^2/\lambda). \end{align*}

We also treat σ\sigma and λ\lambda as unknown parameters and assign the prior:

λ∼fλ(λ)   and   log⁡σ∼unif(−C,C)\begin{align*} \lambda \sim f_{\lambda}(\lambda) ~~ \text{ and } ~~ \log \sigma \sim \text{unif}(-C, C) \end{align*}

where

fλ(λ)∝I(λmin⁡≤λ≤λmax⁡)λ  which corresponds to log⁡λ∼unif(log⁡λmin⁡,log⁡λmax⁡).\begin{align*} f_{\lambda}(\lambda) \propto \frac{I(\lambda_{\min} \leq \lambda \leq \lambda_{\max})}{\lambda} ~\text{ which corresponds to}~ \log \lambda \sim \text{unif}(\log \lambda_{\min}, \log \lambda_{\max}). \end{align*}

As derived in the lecture, the posterior of β,σ,λ\beta, \sigma, \lambda then becomes:

β∣data,σ,λ∼N((XTX+λJ)−1XTy,σ2(XTX+λJ)−1)\begin{align*} \beta \mid \text{data}, \sigma, \lambda \sim N\left((X^TX + \lambda J)^{-1} X^T y, \sigma^2 (X^T X + \lambda J)^{-1} \right) \end{align*}

and

1σ2∣data,λ∼Gamma(n2−1,yTy−yTX(XTX+λJ)−1XTy2).\begin{align*} \frac{1}{\sigma^2} \mid \text{data}, \lambda \sim \text{Gamma} \left(\frac{n}{2} - 1, \frac{y^T y - y^T X(X^T X + \lambda J)^{-1} X^T y}{2} \right). \end{align*}

and

fλ∣data(λ)∝fλ(λ)λ(n−2)/2∣XTX+λJ∣−1/2(yTy−yTX(XTX+λJ)−1XTy)(n/2)−1\begin{align*} f_{\lambda\mid\text{data}}(\lambda) \propto f_{\lambda}(\lambda) \frac{\lambda^{(n-2)/2}|X^TX+\lambda J|^{-1/2}} {\left(y^Ty-y^TX(X^TX+\lambda J)^{-1}X^Ty\right)^{(n/2)-1}} \end{align*}

The key here is the posterior for λ\lambda. We compute this posterior numerically below by taking a grid of values for log⁡λ\log \lambda. The computation proceeds more stably if we evaluate the posterior density of log⁡λ\log \lambda:

flog⁡λ∣data(x)∝e(n/2−1)x∣XTX+exJ∣−1/2(yTy−yTX(XTX+exJ)−1XTy)n/2−1I{log⁡λmin⁡≤x≤log⁡λmax⁡}.\begin{align*} f_{\log \lambda \mid \text{data}}(x) \propto \frac{e^{(n/2 - 1)x} |X^T X + e^x J|^{-1/2}}{\left( y^T y - y^T X (X^TX + e^x J)^{-1} X^T y \right)^{n/2 - 1}} I\{\log \lambda_{\min} \leq x \leq \log \lambda_{\max}\}. \end{align*}

The code for doing all this computation is given below. Explanation for how the code relates to the above formulae is explained after the code.

We need to select a large number of λ\lambda values, and evaluate the posterior at these values. Depending on the number of λ\lambda values chosen, and the number of posterior samples we want to obtain, this can be computationally slower.

Here is an explanation of the above code, and how it relates to the posterior formulae.

Write

Aλ=XTX+λJ,b=XTy,c=yTy,a=n2−1.A_\lambda=X^TX+\lambda J,\qquad b=X^Ty,\qquad c=y^Ty,\qquad a=\frac{n}{2}-1.

The quantities XTXX^TX, bb, and cc do not depend on λ\lambda, so the code computes them only once. For each value of λ\lambda, we need

mλ=Aλ−1b,qλ=c−bTmλ,log⁡∣Aλ∣.m_\lambda=A_\lambda^{-1}b,\qquad q_\lambda=c-b^Tm_\lambda,\qquad \log|A_\lambda|.

Here mλm_\lambda is the conditional posterior mean of β\beta, and qλ/2q_\lambda/2 is the rate parameter in the conditional Gamma distribution of 1/σ21/\sigma^2.

Cholesky decomposition

Although these formulas involve a matrix inverse, the code does not construct that inverse. Instead, it uses a Cholesky decomposition:

Aλ=LλLλT.A_\lambda=L_\lambda L_\lambda^T.

The matrix LλL_\lambda is lower triangular, meaning that all entries above its diagonal are zero. Its diagonal entries are positive: $$ L_\lambda=

(ℓ110⋯0ℓ21ℓ22⋯0⋮⋮⋱⋮ℓn1ℓn2⋯ℓnn)\begin{pmatrix} \ell_{11} & 0 & \cdots & 0\\ \ell_{21} & \ell_{22} & \cdots & 0\\ \vdots & \vdots & \ddots & \vdots\\ \ell_{n1} & \ell_{n2} & \cdots & \ell_{nn} \end{pmatrix}

$$

Such a factorization exists for every real symmetric positive definite matrix. In our problem, AλA_\lambda has this property because XX has full column rank and, for every nonzero vector vv,

vTAλv=∥Xv∥2+λvTJv>0.v^TA_\lambda v=\|Xv\|^2+\lambda v^TJv>0.

The command L = np.linalg.cholesky(XTX + lam * J) computes this lower triangular factor.

The advantage of the factorization is that equations involving triangular matrices are easy to solve. For example, to solve Lλu=bL_\lambda u=b, we first obtain u1u_1 from the first equation:

u1=b1ℓ11.u_1=\frac{b_1}{\ell_{11}}.

The second equation then gives

u2=b2−ℓ21u1ℓ22.u_2=\frac{b_2-\ell_{21}u_1}{\ell_{22}}.

We continue down the rows, finding one new unknown at each step. This is called forward substitution. Equations involving LλTL_\lambda^T, which is upper triangular, can similarly be solved starting from the last row and working upward. This is called back substitution.

Computing the posterior mean

To compute mλ=Aλ−1bm_\lambda=A_\lambda^{-1}b, we solve Aλmλ=bA_\lambda m_\lambda=b. Using the factorization, this becomes

LλLλTmλ=b.L_\lambda L_\lambda^T m_\lambda=b.

We solve this through two triangular systems:

Lλu=b,LλTmλ=u.L_\lambda u=b,\qquad L_\lambda^T m_\lambda=u.

The command cho_solve((L, True), XTy) performs these two steps. The argument True specifies that the supplied factor is lower triangular. This computes the desired posterior mean without explicitly forming Aλ−1A_\lambda^{-1}.

Computing the determinant

The same Cholesky factor also gives the determinant. The determinant of a triangular matrix is the product of its diagonal entries, so

∣Aλ∣=∣Lλ∣ ∣LλT∣=(∏j=1nℓjj)2.|A_\lambda| =|L_\lambda|\,|L_\lambda^T| =\left(\prod_{j=1}^n\ell_{jj}\right)^2.

Consequently,

log⁡∣Aλ∣=2∑j=1nlog⁡ℓjj.\log|A_\lambda|=2\sum_{j=1}^n\log\ell_{jj}.

The code therefore obtains −12log⁡∣Aλ∣-\frac12\log|A_\lambda| as -np.log(np.diag(L)).sum(). Working with a sum of logarithms avoids forming a product that might be extremely large or small.

Once mλm_\lambda is available, the remaining quantity is

qλ=c−bTmλ.q_\lambda=c-b^Tm_\lambda.

The code stores mλm_\lambda in a column of beta_means and stores qλ/2q_\lambda/2 in rates. These quantities will be needed again during posterior sampling, so saving them avoids repeating their calculation.

Computing the posterior grid weights

Let xg=log⁡λgx_g=\log\lambda_g, for g=1,…,Gg=1,\ldots,G, be equally spaced grid points between log⁡λmin⁡\log\lambda_{\min} and log⁡λmax⁡\log\lambda_{\max}.

The logarithm of the posterior density of log⁡λ\log\lambda, up to an additive constant that does not depend on gg, is

hg=axg−12log⁡∣Aλg∣−alog⁡qλg=axg−∑j=1nlog⁡(Lλg)jj−alog⁡qλg.\begin{aligned} h_g &=a x_g-\frac12\log|A_{\lambda_g}|-a\log q_{\lambda_g}\\ &=a x_g-\sum_{j=1}^n\log(L_{\lambda_g})_{jj} -a\log q_{\lambda_g}. \end{aligned}

These values are stored in logpost_loglambda.

To turn them into posterior grid weights, the code computes

wg=exp⁡(hg−hmax⁡)∑k=1Gexp⁡(hk−hmax⁡),hmax⁡=max⁡1≤k≤Ghk.w_g= \frac{\exp(h_g-h_{\max})} {\sum_{k=1}^G\exp(h_k-h_{\max})}, \qquad h_{\max}=\max_{1\leq k\leq G}h_k.

Subtracting hmax⁡h_{\max} does not change the normalized weights: it multiplies every unnormalized weight by the same constant. It improves numerical stability by making the largest exponential equal to one.

Because the grid is equally spaced in log⁡λ\log\lambda, its common spacing cancels when the weights are normalized. The resulting weights sum to one and approximate posterior masses associated with the grid points. They are stored in prob_loglambda.

If the grid spacing is Δx\Delta x, then wg/Δxw_g/\Delta x approximates the posterior density of log⁡λ\log\lambda at xgx_g. These density values are stored in post_loglambda.

Sampling from the posterior

Posterior sampling follows the factorization $$ p(\beta,\sigma,\lambda\mid y)

p(\lambda\mid y), p(\sigma\mid\lambda,y), p(\beta\mid\sigma,\lambda,y). $$

First, the code samples grid indices using probabilities w1,…,wGw_1,\ldots,w_G. A sampled index gg corresponds to λ=λg\lambda=\lambda_g and gives immediate access to its saved mean mλgm_{\lambda_g} and rate rg=qλg/2r_g=q_{\lambda_g}/2.

Next, conditional on this sampled λg\lambda_g, we draw

T=1σ2∼Gamma⁡(shape=a,rate=rg),σ=T−1/2.T=\frac{1}{\sigma^2} \sim\operatorname{Gamma}(\text{shape}=a,\text{rate}=r_g), \qquad \sigma=T^{-1/2}.

NumPy specifies a Gamma distribution using its shape and scale, rather than its shape and rate. Since scale is the reciprocal of rate, the appropriate call is np.random.gamma(shape, 1/rate). The code draws all the required values of σ\sigma at once.

Finally, we must sample

β∣σ,λ,y∼N(mλ,σ2Aλ−1).\beta\mid\sigma,\lambda,y \sim N(m_\lambda,\sigma^2A_\lambda^{-1}).

To do this, generate a vector zz of independent standard normal variables and solve

LλTu=z.L_\lambda^T u=z.

Then set

β=mλ+σu.\beta=m_\lambda+\sigma u.

To verify this construction, observe that

u=Lλ−Tzu=L_\lambda^{-T}z

and hence

Cov⁡(u)=Lλ−TLλ−1=(LλLλT)−1=Aλ−1.\operatorname{Cov}(u) =L_\lambda^{-T}L_\lambda^{-1} =(L_\lambda L_\lambda^T)^{-1} =A_\lambda^{-1}.

Therefore, the constructed vector has mean mλm_\lambda and covariance σ2Aλ−1\sigma^2A_\lambda^{-1}, as required.

The code computes uu using solve_triangular(L.T, z, lower=False). The transpose matters: we solve with LλTL_\lambda^T, which is upper triangular.

Avoiding repeated calculations

Several posterior draws may select the same grid value of λ\lambda. The sampling loop therefore visits each distinct sampled grid index only once. For that index, it recomputes one Cholesky factor and uses it for all the corresponding draws.

The independent standard normal vectors are arranged as columns of a matrix, allowing the triangular solver to process them together. Each column uses its own sampled σ\sigma.

Thus, the code saves the coefficient means and Gamma rates from the grid calculation, while recomputing Cholesky factors only for the distinct grid values selected during sampling. This avoids storing a large collection of matrix factors and avoids a separate factorization for every posterior draw.

A Cholesky factorization requires order n3n^3 operations, whereas solving a triangular system for one vector requires order n2n^2 operations. Reusing the factors can therefore substantially reduce computation.

Constructing the posterior curves

For each sampled coefficient vector β(s)\beta^{(s)}, the corresponding mean curve is

μ(s)=Xβ(s).\mu^{(s)}=X\beta^{(s)}.

The command muhats = X @ betahats computes all these curves together. Their average,

μ^=1N∑s=1Nμ(s),\widehat{\mu} =\frac{1}{N}\sum_{s=1}^N\mu^{(s)},

approximates the posterior mean E[μ∣y]E[\mu\mid y].

The plot shows the observations in light gray, the sampled mean curves in light coral, and their average in black. The computation has a grid approximation from discretizing λ\lambda and Monte Carlo error from using finitely many posterior draws.

<Figure size 1400x500 with 1 Axes>

To visualize these posterior samples better, we can zoom into certain shorter time intervals.

<Figure size 1200x400 with 1 Axes>

This notebook compares cross-validation and Bayesian regularization for estimating the trend in weekly U.S. initial unemployment claims before March 2020. Consecutive-fold CV selects a large λ\lambda and produces a very smooth fit, while random-fold CV selects a much smaller λ\lambda and follows more short-term variation. Bayesian tuning gives an intermediate amount of smoothing in this example. These differences illustrate how the choice of validation folds and modeling assumptions influences the estimated trend. Full Bayesian inference additionally accounts for uncertainty in λ\lambda, σ\sigma, and β\beta, producing posterior samples of the trend. Cholesky decomposition and reuse of intermediate calculations make this computation more efficient.