We will now be using two different bootstrap methods: cases and
residuals.
4.5.1 Bootstrap Cases
Let’s run bootstrap cases with the transformed model to find the
confidence intervals for our regression coefficients.
## Fit final model (with interaction; remove * if you only want main effects)
mlr_model <- lm(EqpDamg_trans ~ CarsDer * TrkDamg, data = myData)
## Number of bootstrap replicates
B <- 1000
## Extract dimensions
num.p <- length(coef(mlr_model)) # number of parameters in the model
smpl.n <- nrow(myData) # sample size
## Zero matrix to store bootstrap coefficients
coef.mtrx <- matrix(0, nrow = B, ncol = num.p)
## Bootstrap loop
for (i in 1:B){
bootc.id <- sample(1:smpl.n, smpl.n, replace = TRUE) # resample cases
mlr_model_btc <- lm(EqpDamg_trans ~ CarsDer * TrkDamg,
data = myData[bootc.id, ])
coef.mtrx[i, ] <- coef(mlr_model_btc) # store bootstrap coefficients
}
## Label columns with coefficient names
colnames(coef.mtrx) <- names(coef(mlr_model))
## Save original OLS coefficients
coef_orig <- coef(mlr_model)
## Define histogram function
boot.hist <- function(cmtrx, bt.coef.mtrx, var.id, var.nm){
x_seq <- seq(min(bt.coef.mtrx[, var.id]),
max(bt.coef.mtrx[, var.id]), length = 300)
y_norm <- dnorm(x_seq,
mean(bt.coef.mtrx[, var.id]),
sd(bt.coef.mtrx[, var.id]))
highestbar <- max(hist(bt.coef.mtrx[, var.id], plot = FALSE)$density)
ylimit <- max(c(y_norm, highestbar))
hist(bt.coef.mtrx[, var.id],
probability = TRUE,
main = var.nm,
xlab = "",
col = "azure1",
ylim = c(0, ylimit),
border = "lightseagreen")
lines(x_seq, y_norm, col = "red3", lwd = 2)
lines(density(bt.coef.mtrx[, var.id], adjust = 2), col = "blue", lwd = 2)
abline(v = cmtrx[var.id], col = "darkgreen", lwd = 2, lty = 2)
legend("topright",
legend = c("Normal approx", "Kernel density", "OLS coefficient"),
col = c("red3", "blue", "darkgreen"),
lty = c(1,1,2), lwd = 2, bty = "n")
}
## Plot histograms for each coefficient
par(mfrow = c(2,2)) # 2x2 grid for 4 coefficients
boot.hist(coef_orig, coef.mtrx, 1, "Intercept")
boot.hist(coef_orig, coef.mtrx, 2, "CarsDer")
boot.hist(coef_orig, coef.mtrx, 3, "TrkDamg")
boot.hist(coef_orig, coef.mtrx, 4, "CarsDer:TrkDamg")

par(mfrow = c(1,1)) # reset
## Bootstrap confidence intervals
num.p <- ncol(coef.mtrx)
btc.ci <- character(num.p)
btc.wd <- numeric(num.p)
for (i in 1:num.p){
lci.025 <- quantile(coef.mtrx[, i], 0.025, type = 2)
uci.975 <- quantile(coef.mtrx[, i], 0.975, type = 2)
btc.wd[i] <- uci.975 - lci.025
btc.ci[i] <- paste0("[", round(lci.025, 4), ", ", round(uci.975, 4), "]")
}
btc_results <- data.frame(
Coefficient = colnames(coef.mtrx),
Estimate = round(apply(coef.mtrx, 2, mean), 4),
`95% CI` = btc.ci,
Width = round(btc.wd, 4)
)
kable(btc_results, caption = "Bootstrap Coefficient Estimates and 95% CIs")
Bootstrap Coefficient Estimates and 95% CIs
| (Intercept) |
(Intercept) |
7.0283 |
[6.8055, 7.27] |
0.4645 |
| CarsDer |
CarsDer |
0.4097 |
[0.3234, 0.4879] |
0.1645 |
| TrkDamg |
TrkDamg |
0.0000 |
[0, 0] |
0.0000 |
| CarsDer:TrkDamg |
CarsDer:TrkDamg |
0.0000 |
[0, 0] |
0.0000 |
The bootstrap analysis reinforces the earlier regression results. The
intercept is stable, with a narrow 95% confidence interval well above
zero, suggesting a reliable baseline level of transformed damage. The
number of Cars Derailed is the dominant predictor, with an estimate
around 0.41 and a tight confidence interval [0.33, 0.49], providing
strong evidence that derailments consistently increase damage. In
contrast, Track Damage hows an extremely small effect that rounds to
zero in the summary, indicating its influence is negligible compared to
Cars Derailed. Likewise, the interaction term’s bootstrap confidence
interval collapses to 0, confirming it does not contribute meaningfully.
Overall, the bootstrap results highlight that Equipment Damage is driven
almost entirely by the number of Cars Derailed, with little to no
additional explanatory power from Track Damage or the interaction.
4.5.2 Residual Bootstrap Regression
Now, we will perform the bootstrap regression method, Compared to the
cases technique, this method is much more robust. It resamples rows of
the data set with replacement.
hist(sort(mlr_model$residuals), n = 40,
xlab = "Residuals",
col = "lightblue",
border = "navy",
main = "Histogram of Residuals")

Here is the histogram of our residuals. As expected, it is not
normally distributed.
## Final model (with interaction; remove * if you only want main effects)
mlr_model <- lm(EqpDamg_trans ~ CarsDer * TrkDamg, data = myData)
## Residuals
model.resid <- mlr_model$residuals
## Number of bootstrap replicates
B <- 1000
num.p <- length(coef(mlr_model)) # number of parameters
samp.n <- nrow(myData) # sample size
## Zero matrix to store bootstrap coefficients
btr.mtrx <- matrix(0, nrow = B, ncol = num.p)
colnames(btr.mtrx) <- names(coef(mlr_model))
## Bootstrap loop
for (i in 1:B){
# Bootstrap response values = fitted + resampled residuals
bt.response <- mlr_model$fitted.values +
sample(model.resid, samp.n, replace = TRUE)
# Add bootstrap response to data
myData$bt.response <- bt.response
# Refit model with bootstrap response
btr.model <- lm(bt.response ~ CarsDer * TrkDamg, data = myData)
# Store bootstrap coefficients
btr.mtrx[i, ] <- coef(btr.model)
}
## Inspect results
head(btr.mtrx)
(Intercept) CarsDer TrkDamg CarsDer:TrkDamg
[1,] 6.963507 0.4278439 4.468685e-06 -8.602684e-08
[2,] 6.892562 0.4313595 2.316918e-06 -1.209087e-08
[3,] 7.104123 0.4295606 2.657571e-06 -4.243206e-08
[4,] 7.215222 0.3905643 2.615681e-06 1.940970e-08
[5,] 7.035613 0.4097809 4.631840e-06 -8.736683e-08
[6,] 7.026921 0.4023421 3.482149e-06 -7.678797e-10
apply(btr.mtrx, 2, mean) # bootstrap mean estimates
(Intercept) CarsDer TrkDamg CarsDer:TrkDamg
7.046363e+00 4.046779e-01 3.518416e-06 -2.475642e-08
apply(btr.mtrx, 2, sd) # bootstrap standard errors
(Intercept) CarsDer TrkDamg CarsDer:TrkDamg
8.019326e-02 1.641608e-02 9.145254e-07 4.724174e-08
boot.hist <- function(bt.coef.mtrx, var.id, var.nm){
## bt.coef.mtrx = matrix of bootstrap coefficient estimates
## var.id = column index of coefficient (1 = Intercept, 2 = CarsDer, etc.)
## var.nm = variable name for plot title
# sequence for normal curve
x_seq <- seq(min(bt.coef.mtrx[, var.id]),
max(bt.coef.mtrx[, var.id]),
length = 300)
# normal density based on bootstrap mean and sd
y_norm <- dnorm(x_seq,
mean(bt.coef.mtrx[, var.id]),
sd(bt.coef.mtrx[, var.id]))
# determine y-axis limit
highestbar <- max(hist(bt.coef.mtrx[, var.id], plot = FALSE)$density)
ylimit <- max(c(y_norm, highestbar))
# histogram
hist(bt.coef.mtrx[, var.id],
probability = TRUE,
main = var.nm,
xlab = "",
col = "azure1",
ylim = c(0, ylimit),
border = "lightseagreen")
# add normal density curve
lines(x_seq, y_norm, col = "red3", lwd = 2)
# add kernel density curve
lines(density(bt.coef.mtrx[, var.id], adjust = 2),
col = "blue", lwd = 2)
}
par(mfrow = c(2,2)) # 2x2 grid for 4 coefficients
boot.hist(bt.coef.mtrx = btr.mtrx, var.id = 1, var.nm = "Intercept")
boot.hist(bt.coef.mtrx = btr.mtrx, var.id = 2, var.nm = "CarsDer")
boot.hist(bt.coef.mtrx = btr.mtrx, var.id = 3, var.nm = "TrkDamg")
boot.hist(bt.coef.mtrx = btr.mtrx, var.id = 4, var.nm = "CarsDer:TrkDamg")

par(mfrow = c(1,1)) # reset layout
The bootstrap histograms indicate that the model’s coefficient
estimates are stable across resamples. The intercept is centered near 7,
providing a consistent baseline for the transformed measure of Equipment
Damage. The coefficient for CarsDer is tightly clustered around 0.40,
reinforcing that the number of Cars Derailed is the primary and most
reliable predictor of damage. In contrast, the coefficient for TrkDamg
is very close to zero, and the interaction term (CarsDer:TrkDamg) is
also centered at zero, suggesting that these variables do not contribute
meaningfully once CarsDer is included. Overall, the bootstrap results
confirm that Equipment Damage is driven mainly by the number of derailed
cars, while Track Damage and its interaction with CarsDer have little to
no explanatory value. Let’s finish off our boostrapping with regression
confidence intervals.
## number of parameters (coefficients)
num.p <- ncol(btr.mtrx)
## store CIs and widths
btr.ci <- character(num.p)
btr.wd <- numeric(num.p)
for (i in 1:num.p){
lci.025 <- quantile(btr.mtrx[, i], 0.025, type = 2)
uci.975 <- quantile(btr.mtrx[, i], 0.975, type = 2)
btr.wd[i] <- uci.975 - lci.025
btr.ci[i] <- paste0("[", round(lci.025, 4), ", ", round(uci.975, 4), "]")
}
## original OLS coefficients
coef_orig <- coef(mlr_model)
## results table
btr_results <- data.frame(
Coefficient = names(coef_orig),
Estimate = round(coef_orig, 4),
`95% CI` = btr.ci,
Width = round(btr.wd, 4)
)
kable(btr_results, caption = "Regression Coefficients with 95% Residual Bootstrap CI")
Regression Coefficients with 95% Residual Bootstrap
CI
| (Intercept) |
(Intercept) |
7.0467 |
[6.8948, 7.2126] |
0.3178 |
| CarsDer |
CarsDer |
0.4042 |
[0.3705, 0.4359] |
0.0654 |
| TrkDamg |
TrkDamg |
0.0000 |
[0, 0] |
0.0000 |
| CarsDer:TrkDamg |
CarsDer:TrkDamg |
0.0000 |
[0, 0] |
0.0000 |
The residual bootstrap results confirm the main findings of your
model. The intercept is estimated at about 7.05, with a fairly narrow
95% confidence interval [6.89, 7.21], showing it is stable and well
estimated. The coefficient for CarsDer is also highly precise, averaging
around 0.40 with a very tight confidence interval [0.37, 0.44]; this
reinforces that the number of Cars Derailed is the strongest and most
reliable predictor of Equipment Damage. In contrast, both TrkDamg and
the CarsDer:TrkDamg interaction collapse to zero, with their bootstrap
confidence intervals also equal to [0, 0], meaning they provide no
meaningful explanatory power in this model. Overall, the residual
bootstrap analysis underscores that Equipment Damage is overwhelmingly
explained by the number of derailed cars, while Track Damage and its
interaction contribute essentially nothing once CarsDer is included.
#5 Combining and Comparing
Now that we’ve created a handful of models, let’s put all the
inferential statistics into a single table and compare.
## OLS coefficients with p-values
cmtrx <- summary(mlr_model)$coefficients # Estimate, Std. Error, t value, Pr(>|t|)
## Combine with bootstrap CIs
final_results <- cbind(
round(cmtrx, 4), # OLS inferential stats
`Case Bootstrap 95% CI` = btc.ci,
`Residual Bootstrap 95% CI` = btr.ci
)
## Output clean table
kable(as.data.frame(final_results),
caption = "Final Combined Inferential Statistics: p-values and Bootstrap CIs")
Final Combined Inferential Statistics: p-values and Bootstrap
CIs
| (Intercept) |
7.0467 |
0.0811 |
86.9086 |
0 |
[6.8055, 7.27] |
[6.8948, 7.2126] |
| CarsDer |
0.4042 |
0.0166 |
24.4144 |
0 |
[0.3234, 0.4879] |
[0.3705, 0.4359] |
| TrkDamg |
0 |
0 |
3.9086 |
1e-04 |
[0, 0] |
[0, 0] |
| CarsDer:TrkDamg |
0 |
0 |
-0.5324 |
0.5945 |
[0, 0] |
[0, 0] |
This combined table shows how the classical regression results and
both bootstrap approaches line up. The intercept is highly significant
(t = 86.9, p < 0.001), with both the case bootstrap CI [6.81, 7.28]
and residual bootstrap CI [6.89, 7.21] confirming a stable and positive
baseline for transformed Equipment Damage. The coefficient for Cars
Derailed is also very strong and precise (estimate ≈ 0.40, t = 24.4, p
< 0.001), and both bootstrap methods give tight confidence intervals
that exclude zero, reinforcing it as the key predictor. By contrast,
Track Damage has a small but statistically significant t-test result (p
= 0.0001), yet both bootstrap methods collapse its confidence interval
to [0, 0], suggesting the classical test may be picking up noise rather
than a meaningful effect. Finally, the Cars Derailed × Track Damage
interaction is clearly not significant (p = 0.59) and both bootstrap CIs
confirm it contributes nothing. Overall, all three inferential
approaches converge on the conclusion that Cars Derailed is the only
variable with a consistent and meaningful effect, while Track Damage and
its interaction with derailments are negligible once the model is
properly checked with bootstrapping.