knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE)

library(ggplot2)
library(glmnet)

Packages and data

This notebook uses the Hitters data from either ISLR2. The Hitters data record Major League Baseball players from the 1986 season. Salary is the response variable: annual salary in thousands of dollars.

data("Hitters", package = "ISLR2")
hitters <- na.omit(Hitters)

dim(hitters)
[1] 263  20
summary(hitters$Salary)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
   67.5   190.0   425.0   535.9   750.0  2460.0 

A quick look at the original predictors

There are 19 predictors in the original dataset, and 263 samples.

  • Variables such as AtBat, Hits, HmRun, Runs, RBI, and Walks describe recent batting performance.
  • Variables beginning with C are career totals.
  • PutOuts, Assists, and Errors describe fielding.
  • League, Division, and NewLeague are categorical indicators.
hitters
ggplot(hitters, aes(x = Years, y = Salary, color = League, size = Hits)) +
  geom_point(alpha = 0.7) +
  labs(
    title = "Hitters: salary versus years in the league",
    x = "Years",
    y = "Salary"
  ) +
  theme_minimal(base_size = 13)

num_vars <- c("Salary", "AtBat", "Hits", "HmRun", "Runs", "RBI", "Walks",
              "Years", "CAtBat", "CHits", "CHmRun", "CRuns", "CRBI", "CWalks")

corr_mat <- cor(hitters[, num_vars])

corr_df <- as.data.frame(as.table(corr_mat))
names(corr_df) <- c("Var1", "Var2", "Correlation")

ggplot(corr_df, aes(x = Var1, y = Var2, fill = Correlation)) +
  geom_tile() +
  scale_fill_gradient2(low = "#2166ac", mid = "white", high = "#b2182b",
                       midpoint = 0, limits = c(-1, 1)) +
  labs(
    title = "Correlation structure among selected quantitative variables",
    x = NULL,
    y = NULL
  ) +
  theme_minimal(base_size = 11) +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1),
    panel.grid = element_blank()
  )

There is substantial correlation among the career totals, and several batting variables are also strongly related. This is a good setting for regularization.

Create a richer design matrix

To make the prediction problem more genuinely high-dimensional, we add two-way interactions among the quantitative predictors while keeping the categorical variables as main effects only.

reg_formula <- Salary ~ League + Division + NewLeague +
  (AtBat + Hits + HmRun + Runs + RBI + Walks + Years +
   CAtBat + CHits + CHmRun + CRuns + CRBI + CWalks +
   PutOuts + Assists + Errors)^2

X <- model.matrix(reg_formula, data = hitters)[, -1]
y <- hitters$Salary

dim(X)
[1] 263 139
colnames(X)[1:12]
 [1] "LeagueN"    "DivisionW"  "NewLeagueN" "AtBat"      "Hits"       "HmRun"      "Runs"       "RBI"        "Walks"      "Years"      "CAtBat"     "CHits"     
qr(X)$rank
[1] 139

This creates a design matrix with 139 predictors in total.

Train/test split

set.seed(32950)
n <- nrow(X)
train_id <- sample(seq_len(n), size = floor(0.8 * n))
test_id <- setdiff(seq_len(n), train_id)

X_train <- X[train_id, , drop = FALSE]
X_test <- X[test_id, , drop = FALSE]
y_train <- y[train_id]
y_test <- y[test_id]

length(train_id)
[1] 210
length(test_id)
[1] 53

OLS on the same design

train_dat <- hitters[train_id, ]
test_dat <- hitters[test_id, ]

ols_fit <- lm(reg_formula, data = train_dat)
ols_pred <- predict(ols_fit, newdata = test_dat)
ols_mse <- mean((y_test - ols_pred)^2)

ols_mse
[1] 179377.1

Even without exact degeneracy, this expanded design makes OLS much less stable. This is exactly the kind of setting where regularization becomes useful.

Ridge, lasso, and elastic net

We will use the glmnet function to run Lasso, Ridge and Elastic Net. The glmnet functions will estimate an intercept automatically and will not penalize it.

We first use cv.glmnet to choose lambda. The function will then fit the model with the best lambda on the entire training dataset. By default, cv.glmnet uses the number of folds K = 10. We will also standardize all the predictors.

set.seed(32950)
cv_ridge <- cv.glmnet(X_train, y_train, alpha = 0, standardize = TRUE)

set.seed(32950)
cv_lasso <- cv.glmnet(X_train, y_train, alpha = 1, standardize = TRUE)

set.seed(32950)
cv_enet <- cv.glmnet(X_train, y_train, alpha = 0.5, standardize = TRUE)

Cross-validation curves

par(mfrow = c(1, 3))
plot(cv_ridge, main = "Ridge CV")
plot(cv_lasso, main = "Lasso CV")
plot(cv_enet, main = "Elastic net CV")
par(mfrow = c(1, 1))

Test data prediction error

Use the tuning parameters selected by CV on the whole training data, and then test the models on the test data.

ridge_pred <- predict(cv_ridge, newx = X_test, s = "lambda.min")
lasso_pred <- predict(cv_lasso, newx = X_test, s = "lambda.min")
enet_pred <- predict(cv_enet, newx = X_test, s = "lambda.min")

ridge_mse <- mean((y_test - ridge_pred)^2)
lasso_mse <- mean((y_test - lasso_pred)^2)
enet_mse <- mean((y_test - enet_pred)^2)

results <- data.frame(
  Method = c("OLS", "Ridge", "Lasso", "Elastic net"),
  Test_MSE = c(ols_mse, ridge_mse, lasso_mse, enet_mse)
)

results[order(results$Test_MSE), ]

Lasso, Elastic net and Ridge all predict the test data much better than OLS, with Lasso performing the best here.

Coefficient paths

We can also visualize how the coefficients change with lambda on the training data.

ridge_fit <- glmnet(X_train, y_train, alpha = 0, standardize = TRUE)
lasso_fit <- glmnet(X_train, y_train, alpha = 1, standardize = TRUE)
enet_fit <- glmnet(X_train, y_train, alpha = 0.5, standardize = TRUE)
par(mfrow = c(1, 3))
plot(ridge_fit, xvar = "lambda", label = FALSE, main = "Ridge path")
plot(lasso_fit, xvar = "lambda", label = FALSE, main = "Lasso path")
plot(enet_fit, xvar = "lambda", label = FALSE, main = "Elastic net path")
par(mfrow = c(1, 1))

The Lasso and Elastic net paths actually very similar. The Elastic net path is just smoother because of the \(\ell_2\) penalty added.

How sparse is the lasso fit?

lasso_coef <- coef(cv_lasso, s = "lambda.min")
enet_coef <- coef(cv_enet, s = "lambda.min")

sum(lasso_coef[-1, 1] != 0)
[1] 53
sum(enet_coef[-1, 1] != 0)
[1] 60
lasso_nonzero <- lasso_coef[lasso_coef[, 1] != 0, , drop = FALSE]
lasso_nonzero
54 x 1 sparse Matrix of class "dgCMatrix"
                   lambda.min
(Intercept)      5.348107e+02
LeagueN          8.602123e+00
DivisionW       -7.185711e+01
NewLeagueN       5.058167e+01
AtBat           -1.456531e+00
Hits            -2.925791e+00
HmRun            1.870931e+00
Runs            -2.139482e+00
RBI             -2.298497e+00
Walks           -4.853590e+00
CHits            3.329951e-01
CRuns            7.069482e-01
CRBI             1.895966e-01
CWalks           5.206373e-01
PutOuts          3.072278e-01
Assists          2.070375e+00
Errors          -7.617525e+00
AtBat:Hits       3.320834e-03
AtBat:Walks      3.748820e-04
AtBat:Years      5.824630e-02
AtBat:CWalks     1.332638e-03
AtBat:Assists   -1.280061e-03
Hits:HmRun       1.344806e-01
Hits:Walks       7.223271e-02
Hits:Assists    -1.465332e-03
Hits:Errors      2.194851e-05
HmRun:RBI       -3.128173e-02
HmRun:CAtBat    -5.534970e-03
HmRun:CHmRun     8.095777e-02
HmRun:CRBI       2.288763e-02
HmRun:CWalks    -4.280627e-02
HmRun:PutOuts   -3.850681e-03
HmRun:Assists   -1.492172e-02
HmRun:Errors    -3.231415e-01
Runs:CRBI        3.560147e-04
Runs:Errors      6.072413e-02
RBI:CRBI         3.964278e-03
Walks:Years     -2.285219e-03
Walks:PutOuts   -2.003278e-03
Walks:Assists   -6.429953e-03
Walks:Errors     3.312738e-02
Years:CAtBat    -1.496781e-02
Years:Errors    -2.030175e-01
CAtBat:CHmRun   -1.215814e-04
CAtBat:Assists   4.543147e-05
CHits:PutOuts    4.403663e-04
CHits:Errors     6.742580e-05
CHmRun:CRBI      2.900925e-07
CHmRun:PutOuts  -1.009738e-03
CHmRun:Errors    9.663381e-03
CRBI:Errors      2.646156e-02
CWalks:Assists  -2.295138e-03
PutOuts:Assists -1.357541e-03
Assists:Errors   7.169966e-03

Takeaway

LS0tCnRpdGxlOiAiTGVjdHVyZSAxMCBEZW1vOiBSZWd1bGFyaXphdGlvbiB3aXRoIHRoZSBIaXR0ZXJzIERhdGEiCm91dHB1dDogaHRtbF9ub3RlYm9vawotLS0KCmBgYHtyIHNldHVwfQprbml0cjo6b3B0c19jaHVuayRzZXQoZWNobyA9IFRSVUUsIG1lc3NhZ2UgPSBGQUxTRSwgd2FybmluZyA9IEZBTFNFKQoKbGlicmFyeShnZ3Bsb3QyKQpsaWJyYXJ5KGdsbW5ldCkKYGBgCgojIyBQYWNrYWdlcyBhbmQgZGF0YQoKVGhpcyBub3RlYm9vayB1c2VzIHRoZSBgSGl0dGVyc2AgZGF0YSBmcm9tIGVpdGhlciBgSVNMUjJgLiBUaGUgYEhpdHRlcnNgIGRhdGEgcmVjb3JkIE1ham9yIExlYWd1ZSBCYXNlYmFsbCBwbGF5ZXJzIGZyb20gdGhlIDE5ODYgc2Vhc29uLiBgU2FsYXJ5YCBpcyB0aGUgcmVzcG9uc2UgdmFyaWFibGU6IGFubnVhbCBzYWxhcnkgaW4gdGhvdXNhbmRzIG9mIGRvbGxhcnMuCgpgYGB7cn0KZGF0YSgiSGl0dGVycyIsIHBhY2thZ2UgPSAiSVNMUjIiKQpoaXR0ZXJzIDwtIG5hLm9taXQoSGl0dGVycykKCmRpbShoaXR0ZXJzKQpzdW1tYXJ5KGhpdHRlcnMkU2FsYXJ5KQpgYGAKCgoKIyMjIEEgcXVpY2sgbG9vayBhdCB0aGUgb3JpZ2luYWwgcHJlZGljdG9ycwoKVGhlcmUgYXJlIDE5IHByZWRpY3RvcnMgaW4gdGhlIG9yaWdpbmFsIGRhdGFzZXQsIGFuZCAyNjMgc2FtcGxlcy4gCgotIFZhcmlhYmxlcyBzdWNoIGFzIGBBdEJhdGAsIGBIaXRzYCwgYEhtUnVuYCwgYFJ1bnNgLCBgUkJJYCwgYW5kIGBXYWxrc2AgZGVzY3JpYmUgcmVjZW50IGJhdHRpbmcgcGVyZm9ybWFuY2UuCi0gVmFyaWFibGVzIGJlZ2lubmluZyB3aXRoIGBDYCBhcmUgY2FyZWVyIHRvdGFscy4KLSBgUHV0T3V0c2AsIGBBc3Npc3RzYCwgYW5kIGBFcnJvcnNgIGRlc2NyaWJlIGZpZWxkaW5nLgotIGBMZWFndWVgLCBgRGl2aXNpb25gLCBhbmQgYE5ld0xlYWd1ZWAgYXJlIGNhdGVnb3JpY2FsIGluZGljYXRvcnMuCgpgYGB7cn0KaGl0dGVycwpgYGAKCmBgYHtyfQpnZ3Bsb3QoaGl0dGVycywgYWVzKHggPSBZZWFycywgeSA9IFNhbGFyeSwgY29sb3IgPSBMZWFndWUsIHNpemUgPSBIaXRzKSkgKwogIGdlb21fcG9pbnQoYWxwaGEgPSAwLjcpICsKICBsYWJzKAogICAgdGl0bGUgPSAiSGl0dGVyczogc2FsYXJ5IHZlcnN1cyB5ZWFycyBpbiB0aGUgbGVhZ3VlIiwKICAgIHggPSAiWWVhcnMiLAogICAgeSA9ICJTYWxhcnkiCiAgKSArCiAgdGhlbWVfbWluaW1hbChiYXNlX3NpemUgPSAxMykKYGBgCgpgYGB7cn0KbnVtX3ZhcnMgPC0gYygiU2FsYXJ5IiwgIkF0QmF0IiwgIkhpdHMiLCAiSG1SdW4iLCAiUnVucyIsICJSQkkiLCAiV2Fsa3MiLAogICAgICAgICAgICAgICJZZWFycyIsICJDQXRCYXQiLCAiQ0hpdHMiLCAiQ0htUnVuIiwgIkNSdW5zIiwgIkNSQkkiLCAiQ1dhbGtzIikKCmNvcnJfbWF0IDwtIGNvcihoaXR0ZXJzWywgbnVtX3ZhcnNdKQoKY29ycl9kZiA8LSBhcy5kYXRhLmZyYW1lKGFzLnRhYmxlKGNvcnJfbWF0KSkKbmFtZXMoY29ycl9kZikgPC0gYygiVmFyMSIsICJWYXIyIiwgIkNvcnJlbGF0aW9uIikKCmdncGxvdChjb3JyX2RmLCBhZXMoeCA9IFZhcjEsIHkgPSBWYXIyLCBmaWxsID0gQ29ycmVsYXRpb24pKSArCiAgZ2VvbV90aWxlKCkgKwogIHNjYWxlX2ZpbGxfZ3JhZGllbnQyKGxvdyA9ICIjMjE2NmFjIiwgbWlkID0gIndoaXRlIiwgaGlnaCA9ICIjYjIxODJiIiwKICAgICAgICAgICAgICAgICAgICAgICBtaWRwb2ludCA9IDAsIGxpbWl0cyA9IGMoLTEsIDEpKSArCiAgbGFicygKICAgIHRpdGxlID0gIkNvcnJlbGF0aW9uIHN0cnVjdHVyZSBhbW9uZyBzZWxlY3RlZCBxdWFudGl0YXRpdmUgdmFyaWFibGVzIiwKICAgIHggPSBOVUxMLAogICAgeSA9IE5VTEwKICApICsKICB0aGVtZV9taW5pbWFsKGJhc2Vfc2l6ZSA9IDExKSArCiAgdGhlbWUoCiAgICBheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDQ1LCBoanVzdCA9IDEpLAogICAgcGFuZWwuZ3JpZCA9IGVsZW1lbnRfYmxhbmsoKQogICkKYGBgCgpUaGVyZSBpcyBzdWJzdGFudGlhbCBjb3JyZWxhdGlvbiBhbW9uZyB0aGUgY2FyZWVyIHRvdGFscywgYW5kIHNldmVyYWwgYmF0dGluZyB2YXJpYWJsZXMgYXJlIGFsc28gc3Ryb25nbHkgcmVsYXRlZC4gVGhpcyBpcyBhIGdvb2Qgc2V0dGluZyBmb3IgcmVndWxhcml6YXRpb24uCgojIyMgQ3JlYXRlIGEgcmljaGVyIGRlc2lnbiBtYXRyaXgKClRvIG1ha2UgdGhlIHByZWRpY3Rpb24gcHJvYmxlbSBtb3JlIGdlbnVpbmVseSBoaWdoLWRpbWVuc2lvbmFsLCB3ZSBhZGQgdHdvLXdheSBpbnRlcmFjdGlvbnMgYW1vbmcgdGhlIHF1YW50aXRhdGl2ZSBwcmVkaWN0b3JzIHdoaWxlIGtlZXBpbmcgdGhlIGNhdGVnb3JpY2FsIHZhcmlhYmxlcyBhcyBtYWluIGVmZmVjdHMgb25seS4KCmBgYHtyfQpyZWdfZm9ybXVsYSA8LSBTYWxhcnkgfiBMZWFndWUgKyBEaXZpc2lvbiArIE5ld0xlYWd1ZSArCiAgKEF0QmF0ICsgSGl0cyArIEhtUnVuICsgUnVucyArIFJCSSArIFdhbGtzICsgWWVhcnMgKwogICBDQXRCYXQgKyBDSGl0cyArIENIbVJ1biArIENSdW5zICsgQ1JCSSArIENXYWxrcyArCiAgIFB1dE91dHMgKyBBc3Npc3RzICsgRXJyb3JzKV4yCgpYIDwtIG1vZGVsLm1hdHJpeChyZWdfZm9ybXVsYSwgZGF0YSA9IGhpdHRlcnMpWywgLTFdCnkgPC0gaGl0dGVycyRTYWxhcnkKCmRpbShYKQpgYGAKClRoaXMgY3JlYXRlcyBhIGRlc2lnbiBtYXRyaXggd2l0aCAxMzkgcHJlZGljdG9ycyBpbiB0b3RhbC4gCgoKCiMjIFRyYWluL3Rlc3Qgc3BsaXQKCmBgYHtyfQpzZXQuc2VlZCgzMjk1MCkKbiA8LSBucm93KFgpCnRyYWluX2lkIDwtIHNhbXBsZShzZXFfbGVuKG4pLCBzaXplID0gZmxvb3IoMC44ICogbikpCnRlc3RfaWQgPC0gc2V0ZGlmZihzZXFfbGVuKG4pLCB0cmFpbl9pZCkKClhfdHJhaW4gPC0gWFt0cmFpbl9pZCwgLCBkcm9wID0gRkFMU0VdClhfdGVzdCA8LSBYW3Rlc3RfaWQsICwgZHJvcCA9IEZBTFNFXQp5X3RyYWluIDwtIHlbdHJhaW5faWRdCnlfdGVzdCA8LSB5W3Rlc3RfaWRdCgpsZW5ndGgodHJhaW5faWQpICMgbnVtYmVyIG9mIHRyYWluaW5nIHNhbXBsZXMKbGVuZ3RoKHRlc3RfaWQpICMgbnVtYmVyIG9mIHRlc3Qgc2FtcGxlcwpgYGAKCiMjIyBPTFMgb24gdGhlIHNhbWUgZGVzaWduCgpgYGB7cn0KdHJhaW5fZGF0IDwtIGhpdHRlcnNbdHJhaW5faWQsIF0KdGVzdF9kYXQgPC0gaGl0dGVyc1t0ZXN0X2lkLCBdCgpvbHNfZml0IDwtIGxtKHJlZ19mb3JtdWxhLCBkYXRhID0gdHJhaW5fZGF0KQpvbHNfcHJlZCA8LSBwcmVkaWN0KG9sc19maXQsIG5ld2RhdGEgPSB0ZXN0X2RhdCkKb2xzX21zZSA8LSBtZWFuKCh5X3Rlc3QgLSBvbHNfcHJlZCleMikKCm9sc19tc2UKYGBgCgpFdmVuIHdpdGhvdXQgZXhhY3QgZGVnZW5lcmFjeSwgdGhpcyBleHBhbmRlZCBkZXNpZ24gbWFrZXMgT0xTIG11Y2ggbGVzcyBzdGFibGUuIFRoaXMgaXMgZXhhY3RseSB0aGUga2luZCBvZiBzZXR0aW5nIHdoZXJlIHJlZ3VsYXJpemF0aW9uIGJlY29tZXMgdXNlZnVsLgoKIyMgUmlkZ2UsIGxhc3NvLCBhbmQgZWxhc3RpYyBuZXQKCldlIHdpbGwgdXNlIHRoZSBgZ2xtbmV0YCBmdW5jdGlvbiB0byBydW4gTGFzc28sIFJpZGdlIGFuZCBFbGFzdGljIE5ldC4gVGhlIGBnbG1uZXRgIGZ1bmN0aW9ucyB3aWxsIGVzdGltYXRlIGFuIGludGVyY2VwdCBhdXRvbWF0aWNhbGx5IGFuZCB3aWxsIG5vdCBwZW5hbGl6ZSBpdC4KCldlIGZpcnN0IHVzZSBgY3YuZ2xtbmV0YCB0byBjaG9vc2UgYGxhbWJkYWAuIFRoZSBmdW5jdGlvbiB3aWxsIHRoZW4gZml0IHRoZSBtb2RlbCB3aXRoIHRoZSBiZXN0IGBsYW1iZGFgIG9uIHRoZSBlbnRpcmUgdHJhaW5pbmcgZGF0YXNldC4gQnkgZGVmYXVsdCwgYGN2LmdsbW5ldGAgdXNlcyB0aGUgbnVtYmVyIG9mIGZvbGRzIGBLID0gMTBgLiBXZSB3aWxsIGFsc28gc3RhbmRhcmRpemUgYWxsIHRoZSBwcmVkaWN0b3JzLiAKCmBgYHtyfQpzZXQuc2VlZCgzMjk1MCkKY3ZfcmlkZ2UgPC0gY3YuZ2xtbmV0KFhfdHJhaW4sIHlfdHJhaW4sIGFscGhhID0gMCwgc3RhbmRhcmRpemUgPSBUUlVFKQoKc2V0LnNlZWQoMzI5NTApCmN2X2xhc3NvIDwtIGN2LmdsbW5ldChYX3RyYWluLCB5X3RyYWluLCBhbHBoYSA9IDEsIHN0YW5kYXJkaXplID0gVFJVRSkKCnNldC5zZWVkKDMyOTUwKQpjdl9lbmV0IDwtIGN2LmdsbW5ldChYX3RyYWluLCB5X3RyYWluLCBhbHBoYSA9IDAuNSwgc3RhbmRhcmRpemUgPSBUUlVFKQpgYGAKCiMjIyBDcm9zcy12YWxpZGF0aW9uIGN1cnZlcwoKYGBge3J9CnBhcihtZnJvdyA9IGMoMSwgMykpCnBsb3QoY3ZfcmlkZ2UsIG1haW4gPSAiUmlkZ2UgQ1YiKQpwbG90KGN2X2xhc3NvLCBtYWluID0gIkxhc3NvIENWIikKcGxvdChjdl9lbmV0LCBtYWluID0gIkVsYXN0aWMgbmV0IENWIikKcGFyKG1mcm93ID0gYygxLCAxKSkKYGBgCgojIyMgVGVzdCBkYXRhIHByZWRpY3Rpb24gZXJyb3IKClVzZSB0aGUgdHVuaW5nIHBhcmFtZXRlcnMgc2VsZWN0ZWQgYnkgQ1Ygb24gdGhlIHdob2xlIHRyYWluaW5nIGRhdGEsIGFuZCB0aGVuIHRlc3QgdGhlIG1vZGVscyBvbiB0aGUgdGVzdCBkYXRhLgoKYGBge3J9CnJpZGdlX3ByZWQgPC0gcHJlZGljdChjdl9yaWRnZSwgbmV3eCA9IFhfdGVzdCwgcyA9ICJsYW1iZGEubWluIikKbGFzc29fcHJlZCA8LSBwcmVkaWN0KGN2X2xhc3NvLCBuZXd4ID0gWF90ZXN0LCBzID0gImxhbWJkYS5taW4iKQplbmV0X3ByZWQgPC0gcHJlZGljdChjdl9lbmV0LCBuZXd4ID0gWF90ZXN0LCBzID0gImxhbWJkYS5taW4iKQoKcmlkZ2VfbXNlIDwtIG1lYW4oKHlfdGVzdCAtIHJpZGdlX3ByZWQpXjIpCmxhc3NvX21zZSA8LSBtZWFuKCh5X3Rlc3QgLSBsYXNzb19wcmVkKV4yKQplbmV0X21zZSA8LSBtZWFuKCh5X3Rlc3QgLSBlbmV0X3ByZWQpXjIpCgpyZXN1bHRzIDwtIGRhdGEuZnJhbWUoCiAgTWV0aG9kID0gYygiT0xTIiwgIlJpZGdlIiwgIkxhc3NvIiwgIkVsYXN0aWMgbmV0IiksCiAgVGVzdF9NU0UgPSBjKG9sc19tc2UsIHJpZGdlX21zZSwgbGFzc29fbXNlLCBlbmV0X21zZSkKKQoKcmVzdWx0c1tvcmRlcihyZXN1bHRzJFRlc3RfTVNFKSwgXQpgYGAKTGFzc28sIEVsYXN0aWMgbmV0IGFuZCBSaWRnZSBhbGwgcHJlZGljdCB0aGUgdGVzdCBkYXRhIG11Y2ggYmV0dGVyIHRoYW4gT0xTLCB3aXRoIExhc3NvIHBlcmZvcm1pbmcgdGhlIGJlc3QgaGVyZS4KCgojIyBDb2VmZmljaWVudCBwYXRocwoKV2UgY2FuIGFsc28gdmlzdWFsaXplIGhvdyB0aGUgY29lZmZpY2llbnRzIGNoYW5nZSB3aXRoIGBsYW1iZGFgIG9uIHRoZSB0cmFpbmluZyBkYXRhLgoKYGBge3J9CnJpZGdlX2ZpdCA8LSBnbG1uZXQoWF90cmFpbiwgeV90cmFpbiwgYWxwaGEgPSAwLCBzdGFuZGFyZGl6ZSA9IFRSVUUpCmxhc3NvX2ZpdCA8LSBnbG1uZXQoWF90cmFpbiwgeV90cmFpbiwgYWxwaGEgPSAxLCBzdGFuZGFyZGl6ZSA9IFRSVUUpCmVuZXRfZml0IDwtIGdsbW5ldChYX3RyYWluLCB5X3RyYWluLCBhbHBoYSA9IDAuNSwgc3RhbmRhcmRpemUgPSBUUlVFKQpgYGAKCmBgYHtyfQpwYXIobWZyb3cgPSBjKDEsIDMpKQpwbG90KHJpZGdlX2ZpdCwgeHZhciA9ICJsYW1iZGEiLCBsYWJlbCA9IEZBTFNFLCBtYWluID0gIlJpZGdlIHBhdGgiKQpwbG90KGxhc3NvX2ZpdCwgeHZhciA9ICJsYW1iZGEiLCBsYWJlbCA9IEZBTFNFLCBtYWluID0gIkxhc3NvIHBhdGgiKQpwbG90KGVuZXRfZml0LCB4dmFyID0gImxhbWJkYSIsIGxhYmVsID0gRkFMU0UsIG1haW4gPSAiRWxhc3RpYyBuZXQgcGF0aCIpCnBhcihtZnJvdyA9IGMoMSwgMSkpCmBgYAoKVGhlIExhc3NvIGFuZCBFbGFzdGljIG5ldCBwYXRocyBhY3R1YWxseSB2ZXJ5IHNpbWlsYXIuIFRoZSBFbGFzdGljIG5ldCBwYXRoIGlzIGp1c3Qgc21vb3RoZXIgYmVjYXVzZSBvZiB0aGUgJFxlbGxfMiQgcGVuYWx0eSBhZGRlZC4KCiMjIEhvdyBzcGFyc2UgaXMgdGhlIGxhc3NvIGZpdD8KCmBgYHtyfQpsYXNzb19jb2VmIDwtIGNvZWYoY3ZfbGFzc28sIHMgPSAibGFtYmRhLm1pbiIpCmVuZXRfY29lZiA8LSBjb2VmKGN2X2VuZXQsIHMgPSAibGFtYmRhLm1pbiIpCgpzdW0obGFzc29fY29lZlstMSwgMV0gIT0gMCkgIyByZW1vdmluZyB0aGUgaW50ZXJjZXB0LCBhbmQgY2FsY3VsYXRlIHRoZSBudW1iZXIgb2Ygbm9uemVybyBjb2VmZmljaWVudHMKc3VtKGVuZXRfY29lZlstMSwgMV0gIT0gMCkKYGBgCgpgYGB7cn0KbGFzc29fbm9uemVybyA8LSBsYXNzb19jb2VmW2xhc3NvX2NvZWZbLCAxXSAhPSAwLCAsIGRyb3AgPSBGQUxTRV0KbGFzc29fbm9uemVybwpgYGAKCiMjIFRha2Vhd2F5CgotIE9MUyBiZWNvbWVzIHVuc3RhYmxlIG9uIHRoZSBmdWxsIGludGVyYWN0aW9uLWV4cGFuZGVkIGRlc2lnbi4KLSBSaWRnZSBzdGFiaWxpemVzIHRoZSBmaXQgYnkgc2hyaW5raW5nIGFsbCBjb2VmZmljaWVudHMuCi0gTGFzc28gcGVyZm9ybXMgYm90aCBzaHJpbmthZ2UgYW5kIHZhcmlhYmxlIHNlbGVjdGlvbi4KLSBFbGFzdGljIG5ldCBnaXZlcyBhIGNvbXByb21pc2UgYmV0d2VlbiBzcGFyc2l0eSBhbmQgc3RhYmlsaXR5Lgo=