Standard Forecasting
Executive Summary

Executive Summary

Where #Passengers is heading over the next 24 months

Observations
144
Frequency
monthly
Forecast Horizon
24
Projected Change (%)
17.38
Holdout MAPE (%)
2.52
Method
Holt-Winters exponential smoothing (level + trend + additive seasonality)
#passengers is projected to rise by 17.4% over the next 24 months (from 432 to 507.06). The underlying trend is upward (+3.13 per month). A repeating seasonal pattern was detected, swinging #Passengers by about 240.7 peak-to-trough within each year. On a 12-month holdout the model's average error was 2.5% (MAPE) — highly reliable.
What this means

The short answer

Airline passengers are projected to rise 17.4% over the next 24 months, from 432 to 507.06, with an underlying upward trend of +3.13 per month. The model was 2.5% accurate on a 12-month holdout test, confirming high reliability for planning.

The detail

Projected change: 17.38%. Forecast endpoint: 507.06 passengers. Last observed value: 432. Underlying trend: +3.13 per month. Repeating seasonal pattern: 240.7 peak-to-trough swing within each year. Holdout MAPE (12-month backtest): 2.52%. This error rate places the model in the "excellent" range for business forecasting (under 5% is excellent; under 15% is workable).

What this can't tell you

The 2.5% error rate reflects the model's performance on the holdout window; actual future error may differ if the coming 24 months contain patterns not represented in the historical 144 months.

Overview

Analysis Overview

Monthly forecast of #Passengers over 24 months.

N Observations144
Horizon24
Frequencymonthly
Seasonal ModelTrue
What this means

The short answer

A Holt-Winters seasonal model was chosen to forecast 24 months of monthly airline passengers because the 144-month history (12 full years) is long enough to reliably estimate a repeating seasonal pattern. The model splits passenger movement into trend, level, and seasonal swing.

The detail

The dataset contains 144 monthly observations of #Passengers, parsed and regularized into a regular monthly series with no gaps, duplicates, or interpolation needed. Holt-Winters exponential smoothing (level + trend + additive seasonality) was selected because the data spans sufficient full years to estimate a stable seasonal pattern. The model produces 24-month forecasts with 80% and 95% prediction intervals; the 95% band quantifies uncertainty and widens as the horizon extends. The method assumes historical patterns persist and cannot anticipate one-off events.

What this can't tell you

The forecast cannot account for structural breaks, policy changes, or external shocks that fall outside the patterns observed in the 144-month window. Forecasts beyond 24 months would require explicit extrapolation logic not present in the model.

Data Preparation

Data Preparation

How the raw rows became a regular time series.

Initial Rows144
Final Rows144
N Duplicate Rows0
N Interpolated0
N Dropped0
Frequencymonthly
What this means

The short answer

All 144 rows loaded cleanly: each date appeared exactly once, no gaps existed, and the series was already regular at monthly frequency. No aggregation, interpolation, or row dropping was needed.

The detail

Initial rows: 144. Duplicate rows: 0. Interpolated points: 0. Dropped rows: 0. Final series: 144 monthly observations. The cadence of Month was inferred as monthly from the median gap between consecutive dates. The series required no cleaning or transformation before modeling.

What this can't tell you

This export contains only the aggregated monthly totals; transaction-level or daily data would reveal whether seasonal patterns are driven by specific day-of-week or booking-window effects, but that granularity is not present here.

Visualization

Forecast

#passengers history and 24-month projection with 95% prediction band.

What this means

The short answer

The forecast projects 507.06 passengers at the 24-month horizon, with a 95% prediction band spanning from 451 to 563.13 — a width of 112.13 passengers. This wide band reflects real uncertainty compounding over two years; plan against the range, not the point estimate alone.

The detail

The observed history runs through the data series shown; the 24-month projection extends from the last observed month. At the horizon, the forecast point estimate is 507.06 passengers, with lower bound 451 and upper bound 563.13, giving a band width of 112.13 passengers. The prediction interval widens as the forecast extends, a standard feature of exponential smoothing models where uncertainty grows with distance from the training data.

What this can't tell you

The band width reflects statistical uncertainty in the model's parameters and error variance, not external shocks (economic downturns, fuel price spikes, regulatory changes) that could shift demand outside these bounds. The forecast assumes the seasonal and trend patterns observed in the history persist; structural breaks in the market would require retraining.

Data Table

Trend & Seasonality

The series decomposed into level, trend, and seasonal swing.

ComponentDescriptionMagnitude
LevelEstimated current level of #Passengers477.8
TrendAverage change in #Passengers per month (upward)3.128
SeasonalityPeak-to-trough swing of the repeating monthly pattern within a year240.7
What this means

The short answer

The current level is 477.83 passengers, rising at 3.13 per month, but the seasonal swing of 240.66 peak-to-trough dominates the metric's movement. Which month you forecast matters far more than how much time passes.

The detail

The decomposition yields: Level 477.828, Trend +3.1276 per month, and Seasonality 240.661 peak-to-trough. The seasonal swing (240.661) is substantially larger than the per-month trend (3.1276), meaning the repeating annual pattern — peaks in summer, troughs in winter — accounts for most of the month-to-month variation. Over a full year, trend adds roughly 37.5 passengers; within that same year, seasonality swings 240.661 passengers from low to high.

What this can't tell you

The decomposition does not project how the trend or seasonality may evolve; it reports their current estimated magnitudes. If travel patterns shift (e.g., reduced seasonality due to remote work adoption, or a steeper trend from capacity expansion), these components would need to be reestimated on new data.

Visualization

Seasonal Pattern

Within-year seasonal effects on #Passengers.

What this means

The short answer

July is the strongest season (+152.89 passengers above trend) and November is the weakest (−87.77 below trend), a total swing of 240.66. Summer months (June through August) run well above trend; winter months (January, February, November, December) run below it.

The detail

Seasonal effects by month: January −27.46, February −54.69, March −20.18, April +12.92, May +18.87, June +75.29, July +152.89, August +134.61, September +33.78, October −18.38, November −87.77, December −45.83. The strongest season is July at +152.89 and the weakest is November at −87.77, yielding a peak-to-trough gap of 240.66 passengers. These seasonal effects are already embedded in the forward forecast.

What this can't tell you

The seasonal pattern reflects the historical average for each month; individual years may deviate if holidays shift or special events occur outside the typical seasonal window.

Data Table

Forecast Accuracy

Backtested holdout error — how far off this model tends to be.

MetricValueInterpretation
ModelHolt-Winters exponential smoothing (level + trend + additive seasonality)
History points144Regularized monthly observations of #Passengers used to fit the model
Forecast horizon24Months projected beyond the last observed Month
Holdout points12Most recent points withheld, then predicted by a model that never saw them
Holdout MAPE (%)2.52Average prediction error of 2.5% on unseen data — highly reliable
Holdout MAE11.56Average absolute error in units of #Passengers
What this means

The short answer

The model achieved 2.52% MAPE on held-out recent months (11.564 passengers MAE), placing it in the highly reliable range for business forecasting. This level of accuracy supports both directional and quantitative planning.

The detail

The model was Holt-Winters exponential smoothing (level + trend + additive seasonality), fitted on 144 history points. The last 12 months were withheld and predicted without ever seeing them; on those 12 holdout points, the model achieved MAPE of 2.52% and MAE of 11.564 passengers. The forecast horizon is 24 months. By standard business thresholds, MAPE under 5% is excellent; this result comfortably meets that bar.

What this can't tell you

Holdout error on the 12 most recent months does not guarantee accuracy beyond 24 months; the prediction band widens to reflect this growing uncertainty. The test assumes the future resembles the past; if structural changes occur (new routes, fleet changes, demand shocks), the historical error rate may not hold.

Rate this report Was this the answer you needed?
The exact source that produced this report — yours to keep, read, and re-run.
Download PDF
How this was computed method · R source · citation
The code that did it

Time Series Forecast — See Where Your Numbers Are Heading

Forecasts a single metric from its own history: parses and regularizes the dates, infers the frequency (daily/weekly/monthly), fits a Holt-Winters exponential smoothing model (additive seasonal when the history supports it, non-seasonal or ARIMA(1,1,1) otherwise), projects forward with 80%/95% prediction intervals, and backtests itself on a holdout window (MAPE/MAE).

Why This Method?

Holt-Winters is the workhorse of business forecasting: it learns level, trend, and a repeating seasonal pattern directly from the series, needs no external drivers, and degrades gracefully — when the history is too short for seasonality the model simply drops that component. Base R only (stats::HoltWinters + stats::arima); no forecast/prophet packages.

What This Analysis Covers

  • Forecast with 95% prediction band (chart)
  • Trend + seasonality decomposition
  • Within-cycle seasonal pattern
  • Backtested holdout accuracy (MAPE / MAE)

Standard Library

Platform standard-library module (LAT-1441): runs on ANY dataset via the semantic mapping {date, value}. All narrative is derived from the user's own column names and computed values.

suppressPackageStartupMessages(library(DT))
suppressPackageStartupMessages(library(htmlwidgets))
suppressPackageStartupMessages(library(arrow))
suppressPackageStartupMessages(library(knitr))
suppressPackageStartupMessages(library(rmarkdown))
suppressPackageStartupMessages(library(dplyr))
suppressPackageStartupMessages(library(tidyr))
suppressPackageStartupMessages(library(ggplot2))
suppressPackageStartupMessages(library(stringr))
suppressPackageStartupMessages(library(lubridate))
suppressPackageStartupMessages(library(broom))
suppressPackageStartupMessages(library(Matrix))
suppressPackageStartupMessages(library(cluster))
suppressPackageStartupMessages(library(data.table))

Core Analysis Pipeline

Step 1: Parse dates robustly (init() may already have Date-coerced)

raw_date <- df$date
  non_blank <- !(is.na(raw_date) | !nzchar(trimws(as.character(raw_date))))
  d <- parse_dates_robust(raw_date)
  n_nonblank <- sum(non_blank)
  if (n_nonblank == 0) {
    stop(sprintf("The &#x27;%s' column is empty — no dates to forecast from.", date_name))
  }
  n_unparsed <- sum(non_blank & is.na(d))
  if (n_unparsed / n_nonblank > 0.05) {
    stop(sprintf(
      "%d of %d values in &#x27;%s' (%.0f%%) could not be read as dates. Please use a recognizable date format (e.g. 2024-01-31 or 01/31/2024).",
      n_unparsed, n_nonblank, date_name, 100 * n_unparsed / n_nonblank))
  }

Step 2: Coerce the metric (95% rule)

v <- df$value
  if (!is.numeric(v)) {
    conv <- suppressWarnings(as.numeric(as.character(v)))
    n_orig <- sum(!is.na(v) & nzchar(as.character(v)))
    if (n_orig > 0 && sum(!is.na(conv)) >= 0.95 * n_orig) {
      v <- conv
    } else {
      stop(sprintf("The &#x27;%s' column is not numeric — a forecast needs a numeric metric.",
                   value_name))
    }
  }
  v <- as.numeric(v)

  keep <- !is.na(d) & !is.na(v)
  n_dropped_rows <- initial_rows - sum(keep)
  work <- data.frame(date = d[keep], value = v[keep])

Step 4: Infer frequency from the median date gap

gaps <- as.numeric(diff(agg$date))
  med_gap <- median(gaps)
  if (med_gap <= 1.5) {
    freq_label <- "daily";   step_days <- 1;  freq <- 7;  period_word <- "day";   cycle_word <- "week"
  } else if (med_gap >= 5.5 && med_gap <= 8.5) {
    freq_label <- "weekly";  step_days <- 7;  freq <- 52; period_word <- "week";  cycle_word <- "year"
  } else if (med_gap >= 26 && med_gap <= 35) {
    freq_label <- "monthly"; step_days <- NA; freq <- 12; period_word <- "month"; cycle_word <- "year"
  } else {
    freq_label <- "irregular"; step_days <- max(1, round(med_gap)); freq <- 1
    period_word <- sprintf("%d-day period", step_days); cycle_word <- "cycle"
  }

Step 5: Regular grid + linear interpolation of small gaps

if (freq_label == "monthly") {
    snapped <- as.Date(format(agg$date, "%Y-%m-01"))
    agg <- aggregate(value ~ date, data = data.frame(date = snapped, value = agg$value),
                     FUN = sum)
    agg <- agg[order(agg$date), , drop = FALSE]
    grid <- seq(min(agg$date), max(agg$date), by = "month")
  } else {
    grid <- seq(min(agg$date), max(agg$date), by = step_days)
  }
  interp <- approx(x = as.numeric(agg$date), y = agg$value,
                   xout = as.numeric(grid), rule = 2)
  values <- interp$y
  n_interp <- sum(!(as.numeric(grid) %in% as.numeric(agg$date)))
  if (freq_label != "irregular" && n_interp / length(grid) > 0.5) {
    stop(sprintf(
      "More than half of the %s periods between the first and last &#x27;%s' are missing — too sparse to forecast reliably.",
      freq_label, date_name))
  }

  n_points <- length(values)
  final_rows <- n_points
  rows_removed <- max(0, initial_rows - sum(keep))

Step 6: Fit (seasonal HW -> HW trend -> ARIMA) + forecast horizon

horizon <- max(2, min(floor(0.25 * n_points), 2 * max(freq, 3), 30))
  fitres <- fit_chain(values, freq, horizon)
  method <- fitres$method
  method_label <- fitres$method_label
  seasonal_fit <- isTRUE(fitres$seasonal)

  if (freq_label == "monthly") {
    future_dates <- seq(grid[length(grid)], by = "month", length.out = horizon + 1)[-1]
  } else {
    future_dates <- grid[length(grid)] + step_days * seq_len(horizon)
  }

Step 7: Backtest on a holdout window (skipped gracefully if too short)

mape <- NA_real_; mae <- NA_real_; holdout_n <- 0L
  backtest_note <- NULL
  holdout_target <- min(max(freq, 6), floor(n_points / 3))
  if (holdout_target >= 4 && (n_points - holdout_target) >= 10) {
    bt <- tryCatch({
      tr <- values[seq_len(n_points - holdout_target)]
      fit_chain(tr, freq, holdout_target)
    }, error = function(e) NULL)
    if (!is.null(bt)) {
      act <- values[(n_points - holdout_target + 1):n_points]
      pred <- bt$mean
      mae <- mean(abs(act - pred))
      nz <- abs(act) > 1e-9
      if (any(nz)) mape <- mean(abs((act[nz] - pred[nz]) / act[nz])) * 100
      holdout_n <- holdout_target
    } else {
      backtest_note <- "Backtest skipped — the model could not be refitted on the shortened training window."
    }
  } else {
    backtest_note <- "Backtest skipped — the series is too short to hold out a meaningful test window."
  }

Step 8: Headline numbers

last_actual <- values[n_points]
  fc_last <- fitres$mean[horizon]
  pct_change <- if (abs(last_actual) > 1e-9) {
    (fc_last - last_actual) / abs(last_actual) * 100
  } else NA_real_
  direction_word <- if (is.na(pct_change)) "move" else if (pct_change > 2) "rise" else if (pct_change < -2) "fall" else "hold roughly steady"

Step 9: Components (level / trend / seasonality)

idx <- seq_len(n_points)
  trend_slope <- as.numeric(coef(lm(values ~ idx))[2])
  level_now <- last_actual
  seasonal_amplitude <- 0
  seasonal_coefs <- numeric(0)
  if (seasonal_fit && !is.null(fitres$fit)) {
    co <- coef(fitres$fit)
    if ("a" %in% names(co)) level_now <- as.numeric(co["a"])
    if ("b" %in% names(co)) trend_slope <- as.numeric(co["b"])
    seasonal_coefs <- as.numeric(co[grep("^s[0-9]+$", names(co))])
    if (length(seasonal_coefs) > 0) {
      seasonal_amplitude <- max(seasonal_coefs) - min(seasonal_coefs)
    }
  } else if (method == "hw_trend" && !is.null(fitres$fit)) {
    co <- coef(fitres$fit)
    if ("a" %in% names(co)) level_now <- as.numeric(co["a"])
    if ("b" %in% names(co)) trend_slope <- as.numeric(co["b"])
  }
Your data has more stories to tell.Run any analysis on your own data — validated R modules, interactive reports, AI insights, and PDF export. 500 free credits on signup.
Try Free — No SignupSign Up Free

Cite this analysis

Report an Issue

Tell us what's wrong. You'll get a free re-run of this analysis so you can try again with different parameters. If the re-run still doesn't meet your expectations, we'll refund your credits.

Want to run this analysis on your own data? Upload CSV — Free Analysis See Pricing