The Baregg Tunnel dataset contains daily traffic volume measurements recorded at the Baregg Tunnel in Switzerland. This dataset spans from November 1, 2003, to November 16, 2005, providing approximately two years of daily observations. Each record captures the total number of vehicles passing through the tunnel on a given day.
The primary objective of this analysis is to develop and evaluate forecasting models capable of accurately predicting daily traffic volumes at the Baregg Tunnel. Accurate traffic forecasts are essential for:
We will compare two forecasting approaches: a Naïve model (benchmark) and a Linear Regression model with trend and weekly seasonality (TSLM). Our goal is to determine which approach provides superior forecast accuracy and to identify opportunities for further improvement.
# Load the fpp3 package suite (includes tsibble, fable, feasts, and ggplot2)
library(fpp3)
# Read CSV and create a tsibble object
baregg <- read.csv("BareggTunnel.csv") |>
mutate(Day = dmy(Day)) |>
as_tsibble(index = Day)
# Display first few rows
head(baregg)
# Summary statistics
summary(baregg$Number.of.vehicles)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 53098 98794 108204 106568 115477 139840
# Create a comprehensive time plot of the entire dataset
autoplot(baregg, Number.of.vehicles) +
labs(
title = "Baregg Tunnel: Daily Traffic Volume (Nov 2003 - Nov 2005)",
y = "Number of Vehicles",
x = "Date"
) +
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
From the time series plot above, we observe several key characteristics:
Trend: The data exhibits a relatively stable pattern with no strong upward or downward trend over the observation period.
Seasonality: There is clear weekly seasonality evident in the data. Traffic volumes show regular patterns that repeat every seven days, likely reflecting differences between weekday and weekend traffic.
Volatility: The series shows consistent variability with regular oscillations, suggesting predictable day-of-week effects.
Outliers: There appear to be some periods with notably lower traffic volumes, which could correspond to holidays or special events.
To properly evaluate forecast accuracy, we partition the data into two subsets:
This partitioning allows us to fit models on historical data and test their predictive performance on a hold-out sample that was not used during model estimation.
# Split data into training and validation sets
train <- baregg |>
filter(Day < ymd("2005-07-01"))
valid <- baregg |>
filter(Day >= ymd("2005-07-01"))
cat("Training observations:", nrow(train), "\n")
## Training observations: 608
cat("Validation observations:", nrow(valid), "\n")
## Validation observations: 139
The Naïve forecasting method is the simplest possible approach: it assumes that the forecast for any future period equals the last observed value. Mathematically:
\[\hat{y}_{t+h|t} = y_t\]
Where \(\hat{y}_{t+h|t}\) is the forecast for time \(t+h\) made at time \(t\), and \(y_t\) is the last observed value.
Why use the Naïve model? The Naïve method serves as a benchmark against which more sophisticated models should be compared. If a complex model cannot outperform the Naïve forecast, it suggests the added complexity provides no practical benefit. For many business applications, the Naïve method is surprisingly effective, especially for stable time series.
The Time Series Linear Model (TSLM) uses ordinary least squares (OLS) regression with time-based predictors:
\[y_t = \beta_0 + \beta_1 \cdot \text{trend}_t + \sum_{d=1}^{6} \beta_d \cdot \text{Day}_d + \varepsilon_t\]
Where:
This model can capture both long-term trends and weekly seasonal patterns, making it more flexible than the Naïve approach.
# Fit the Naïve model
fit_naive <- train |>
model(Naive = NAIVE(Number.of.vehicles))
# Fit the Linear Regression model with trend and weekly seasonality
fit_lm <- train |>
model(
LinReg = TSLM(Number.of.vehicles ~ trend() + season("week"))
)
# Display regression coefficients
report(fit_lm)
## Series: Number.of.vehicles
## Model: TSLM
##
## Residuals:
## Min 1Q Median 3Q Max
## -51399 -3345 2441 5081 18641
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 102993.045 1049.249 98.159 < 2e-16 ***
## trend() 24.250 1.886 12.858 < 2e-16 ***
## season("week")season_72 6161.250 1241.087 4.964 8.99e-07 ***
## season("week")season_73 -9373.072 1237.522 -7.574 1.37e-13 ***
## season("week")season_74 -21859.736 1237.509 -17.664 < 2e-16 ***
## season("week")season_75 -6685.066 1237.499 -5.402 9.51e-08 ***
## season("week")season_76 -4152.765 1237.492 -3.356 0.000841 ***
## season("week")season_77 -310.601 1237.488 -0.251 0.801905
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 8162 on 600 degrees of freedom
## Multiple R-squared: 0.5689, Adjusted R-squared: 0.5638
## F-statistic: 113.1 on 7 and 600 DF, p-value: < 2.22e-16
The report() output shows the estimated coefficients for
the linear regression model. The coefficients reveal:
# Generate forecasts from the Naïve model
fc_naive <- fit_naive |>
forecast(new_data = valid)
# Generate forecasts from the Linear Regression model
fc_lm <- fit_lm |>
forecast(new_data = valid)
# Display first few forecasts from each model
cat("First 10 Naïve Forecasts:\n")
## First 10 Naïve Forecasts:
head(fc_naive$.mean, 10)
## [1] 125353 125353 125353 125353 125353 125353 125353 125353 125353 125353
cat("\nFirst 10 Linear Regression Forecasts:\n")
##
## First 10 Linear Regression Forecasts:
head(fc_lm$.mean, 10)
## [1] 123922.50 108412.42 95950.01 111148.93 113705.48 117571.89 117906.74
## [8] 124092.24 108582.17 96119.76
# Create overlay plot for Naïve model
p1 <- fc_naive |>
autoplot(baregg, level = NULL) +
labs(
title = "Naïve Model: Forecast vs Actual",
y = "Number of Vehicles",
x = "Date"
) +
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
# Create overlay plot for Linear Regression model
p2 <- fc_lm |>
autoplot(baregg, level = NULL) +
labs(
title = "Linear Regression Model: Forecast vs Actual",
y = "Number of Vehicles",
x = "Date"
) +
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5, face = "bold"))
# Display both plots
print(p1)
print(p2)
# Combine both forecasts into a single dataset for comparison
fc_combined <- bind_rows(
fc_naive |> mutate(Model = "Naïve"),
fc_lm |> mutate(Model = "Linear Regression")
)
# Create overlay plot showing both forecasts against actual data
fc_combined |>
autoplot(baregg, level = NULL) +
autolayer(valid, Number.of.vehicles, color = "black", size = 1) +
labs(
title = "Forecast Comparison: Naïve vs Linear Regression",
subtitle = "Validation Period: July - November 2005",
y = "Number of Vehicles",
x = "Date",
color = "Model"
) +
theme_minimal() +
theme(
plot.title = element_text(hjust = 0.5, face = "bold"),
plot.subtitle = element_text(hjust = 0.5),
legend.position = "bottom"
)
# Compute accuracy metrics for both models
acc_naive <- accuracy(fc_naive, baregg)
acc_lm <- accuracy(fc_lm, baregg)
# Combine into a comparison table
accuracy_table <- bind_rows(
acc_naive |> mutate(Model = "Naïve"),
acc_lm |> mutate(Model = "Linear Regression")
) |>
select(Model, ME, RMSE, MAE, MAPE, MASE)
# Display the table
print(accuracy_table)
## # A tibble: 2 × 6
## Model ME RMSE MAE MAPE MASE
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Naïve -12734. 16821. 13606. 13.1 2.78
## 2 Linear Regression -1603. 5869. 3900. 3.70 0.798
The Mean Absolute Scaled Error (MASE) scales the forecast errors relative to the Naïve model’s in-sample performance. It is calculated as:
\[\text{MASE} = \frac{\text{MAE}_{\text{forecast}}}{\text{MAE}_{\text{naive-train}}}\]
Where: - \(\text{MAE}_{\text{forecast}}\) is the mean absolute error of the forecast on validation data - \(\text{MAE}_{\text{naive-train}}\) is the mean absolute error of the Naïve method on training data
Interpretation: - MASE < 1: The
model outperforms the Naïve benchmark - MASE = 1: The
model performs equally to the Naïve benchmark
- MASE > 1: The model underperforms the Naïve
benchmark
# Calculate MAE for Naïve model on training data (in-sample)
# For daily data, we use first differences
mae_naive_train <- mean(abs(diff(train$Number.of.vehicles)), na.rm = TRUE)
cat("In-sample MAE (Naïve benchmark):", round(mae_naive_train, 2), "\n\n")
## In-sample MAE (Naïve benchmark): 9420.08
# Calculate MASE for Naïve model on validation data
mae_naive_valid <- mean(abs(valid$Number.of.vehicles - fc_naive$.mean))
mase_naive <- mae_naive_valid / mae_naive_train
cat("Naïve Model:\n")
## Naïve Model:
cat(" MAE on validation:", round(mae_naive_valid, 2), "\n")
## MAE on validation: 13605.53
cat(" MASE:", round(mase_naive, 3), "\n\n")
## MASE: 1.444
# Calculate MASE for Linear Regression model on validation data
mae_lm_valid <- mean(abs(valid$Number.of.vehicles - fc_lm$.mean))
mase_lm <- mae_lm_valid / mae_naive_train
cat("Linear Regression Model:\n")
## Linear Regression Model:
cat(" MAE on validation:", round(mae_lm_valid, 2), "\n")
## MAE on validation: 3899.74
cat(" MASE:", round(mase_lm, 3), "\n\n")
## MASE: 0.414
cat("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
## ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
cat("Interpretation: If MASE < 1 → beats naïve benchmark\n")
## Interpretation: If MASE < 1 → beats naïve benchmark
cat("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n")
## ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Based on the accuracy metrics computed above, we can evaluate both models:
Key Metrics Explained:
ME (Mean Error): Measures bias. Values close to zero indicate unbiased forecasts. Positive values mean the model tends to over-forecast; negative values indicate under-forecasting.
RMSE (Root Mean Squared Error): Measures overall forecast accuracy, penalizing large errors more heavily. Lower values indicate better performance.
MAE (Mean Absolute Error): Measures average magnitude of forecast errors. More interpretable than RMSE as it’s in the same units as the data.
MAPE (Mean Absolute Percentage Error): Expresses accuracy as a percentage, useful for understanding relative error magnitude.
MASE (Mean Absolute Scaled Error): Scale-independent measure comparing forecast accuracy to the Naïve benchmark. MASE < 1 indicates superior performance to the Naïve method.
Comparison: (The specific comparison will depend on your actual results)
Looking at the accuracy table, we observe that Linear Regression model demonstrates superior performance across multiple metrics. Specifically:
Examining forecast residuals helps us understand whether our models are capturing the underlying patterns in the data or if systematic errors remain.
# Generate comprehensive diagnostic plots for Naïve model
fit_naive |>
gg_tsresiduals() +
labs(title = "Residual Diagnostics: Naïve Model") +
theme_minimal()
# Generate comprehensive diagnostic plots for Linear Regression model
fit_lm |>
gg_tsresiduals() +
labs(title = "Residual Diagnostics: Linear Regression Model") +
theme_minimal()
Answer:
Using the forecast overlay plots for the validation period, we can see how closely each model tracks the day-to-day movements in traffic.
Naïve Model: The Naïve forecast is essentially flat at the last training value during the validation window, so it does not follow the ups and downs visible in the actual series.
Linear Regression Model: The Linear Regression forecasts show a repeating weekly pattern, so they move up and down in a way that broadly matches the observed weekly cycles, although individual peaks and troughs may be slightly misaligned.
Conclusion: Overall, the Linear Regression model follows the actual pattern of ups and downs much better than the Naïve model.
Answer:
From the accuracy table, the Mean Error (ME) for each model on the validation data is:
If a model’s ME is clearly positive, it tends to over-forecast; if clearly negative, it tends to under-forecast.
Interpretation: In the results, the Naïve model ME is strongly negative, indicating it under-forecasts on average by about 12734 vehicles per day. The Linear Regression model ME is closer to zero, so it appears less biased than the Naïve model. This suggests that Linear Regression is closer to being unbiased.
Implication: Bias indicates that the model is missing a systematic component. An unbiased model may still have large errors, but those errors are not directionally consistent.
Answer:
The residual time plots (top panel of each
gg_tsresiduals output) show how errors evolve over
time.
Naïve Model: The residuals display a clear repeating pattern, with systematic swings that line up with the weekly cycle in the data. This indicates that important structure (weekly seasonality) is not captured, so the errors are not random.
Linear Regression Model: The residuals fluctuate around zero without a strong repeating pattern, although there may still be some small clusters. This looks much closer to random noise.
Conclusion: Therefore, the Linear Regression model has more random residuals over time, which indicates a better fit to the underlying structure of the series.
Answer:
The bottom panel of each gg_tsresiduals plot shows the
residual histogram.
Naïve Model: The histogram is Roughly symmetric and appears to be shifted away from zero, suggesting some directional bias.
Linear Regression Model: The histogram is more symmetric and more clearly centered around zero, which is consistent with an approximately unbiased model.
Conclusion: In terms of centering, the Linear Regression model’s histogram is closer to zero than the Naïve model’s, so it shows less bias overall.
Answer:
The ACF plot (middle panel of gg_tsresiduals) shows
whether residuals are autocorrelated at different lags. Spikes outside
the dashed blue bounds are statistically significant.
Naïve Model: The residual ACF shows clear significant spikes at lags around 7, 14, 21, etc., which correspond to the weekly pattern in the data. This tells us that the Naïve model leaves strong, systematic weekly structure in the residuals and is therefore inadequate.
Linear Regression Model: The residual ACF has few significant spikes, with most autocorrelations lying inside the bounds. There may be a small spike at lag 7 that suggests a bit of remaining structure, but the overall autocorrelation is much weaker.
Conclusion: These ACF results indicate that the Linear Regression model captures most of the serial dependence in the data, while the Naïve model leaves strong autocorrelation and thus misses important patterns.
Based on our comprehensive analysis of forecast accuracy and residual diagnostics, we can draw the following conclusions:
Winner: Linear Regression
Justification:
Why this model performs better:
The Linear Regression model outperforms the Naïve forecast because it explicitly models weekly seasonality through day-of-week dummy variables. Since traffic patterns at the Baregg Tunnel exhibit strong weekly cycles, capturing this seasonal component dramatically improves forecast accuracy. The Naïve model, by contrast, simply repeats the last observed value and cannot anticipate these regular fluctuations.
While the [winning model] demonstrates reasonable forecast accuracy, several enhancements could further improve performance:
Incorporate Holiday Effects: Traffic patterns likely differ on public holidays. Adding dummy variables for holidays could reduce forecast errors during these periods.
Explore More Sophisticated Seasonal Models: Methods like Seasonal ARIMA (SARIMA) or STL decomposition could capture seasonal patterns more flexibly than simple dummy variables.
Add External Regressors: Weather conditions, gas prices, or regional events could influence traffic volumes. Including these as predictors might improve accuracy.
Try Exponential Smoothing Methods: Models like Holt-Winters can adaptively weight recent observations and may capture trends and seasonality more effectively.
Consider Ensemble Methods: Combining forecasts from multiple models often outperforms any single model.
Longer Validation Period: Testing on a longer hold-out sample (e.g., 12 months) would provide more robust accuracy estimates.
Forecast Combination: Averaging the Naïve and Linear Regression forecasts might reduce overall error.
For tunnel operators and transportation planners, these findings suggest:
All code used in this analysis is included above and can be executed sequentially to reproduce the results. The analysis requires:
fpp3 (which loads
tsibble, fable, feasts, and
ggplot2)BareggTunnel.csv (must be
in the working directory)To reproduce this analysis:
install.packages("fpp3")BareggTunnel.csvThis analysis was completed independently with guidance from the course materials and assignment instructions provided in SP 26 Business Forecasting.
External Resources Used:
fpp3 suite (Hyndman et al., 2021)AI Tool Disclosure:
sessionInfo()
## R version 4.5.1 (2025-06-13 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 10 x64 (build 19045)
##
## Matrix products: default
## LAPACK version 3.12.1
##
## locale:
## [1] LC_COLLATE=English_United States.utf8
## [2] LC_CTYPE=English_United States.utf8
## [3] LC_MONETARY=English_United States.utf8
## [4] LC_NUMERIC=C
## [5] LC_TIME=English_United States.utf8
##
## time zone: America/New_York
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] fable_0.5.0 feasts_0.4.2 fabletools_0.5.1 ggtime_0.2.0
## [5] tsibbledata_0.4.1 tsibble_1.1.6 ggplot2_3.5.2 lubridate_1.9.4
## [9] tidyr_1.3.1 dplyr_1.1.4 tibble_3.3.0 fpp3_1.0.3
##
## loaded via a namespace (and not attached):
## [1] ggdist_3.3.3 rappdirs_0.3.3 sass_0.4.10
## [4] utf8_1.2.6 generics_0.1.4 anytime_0.3.12
## [7] digest_0.6.37 magrittr_2.0.3 evaluate_1.0.4
## [10] grid_4.5.1 timechange_0.3.0 RColorBrewer_1.1-3
## [13] fastmap_1.2.0 jsonlite_2.0.0 purrr_1.1.0
## [16] scales_1.4.0 jquerylib_0.1.4 cli_3.6.5
## [19] rlang_1.1.6 crayon_1.5.3 ellipsis_0.3.2
## [22] withr_3.0.2 cachem_1.1.0 yaml_2.3.10
## [25] tools_4.5.1 vctrs_0.6.5 R6_2.6.1
## [28] lifecycle_1.0.4 pkgconfig_2.0.3 progressr_0.18.0
## [31] pillar_1.11.0 bslib_0.9.0 gtable_0.3.6
## [34] glue_1.8.0 Rcpp_1.1.1 xfun_0.52
## [37] tidyselect_1.2.1 rstudioapi_0.17.1 knitr_1.50
## [40] farver_2.1.2 htmltools_0.5.8.1 rmarkdown_2.29
## [43] labeling_0.4.3 compiler_4.5.1 distributional_0.6.0
Report prepared by: Aashraya Rai Student
ID: 00974536 Course: Business
Forecasting
Assignment: #1 - Baregg Tunnel Traffic
Forecasting
Submission Date: February 16, 2026