Time Series Analysis: KSE-100 Index

A Comprehensive Tutorial Following Hyndman’s Approach

Author

Your Name

Introduction

Overview

Welcome to this comprehensive time series analysis tutorial using the Karachi Stock Exchange (KSE-100) Index. This tutorial follows the pedagogical approach of Professor Rob J. Hyndman, emphasizing:

  • Visual exploration before modeling
  • Understanding patterns in data
  • Systematic decomposition of time series components
  • Forecasting with appropriate models

Learning Objectives

By the end of this tutorial, you will be able to:

  1. Load and prepare time series data
  2. Create effective visualizations for time series
  3. Identify and understand time series components (trend, seasonality, cycles)
  4. Decompose time series using classical and modern methods
  5. Check for stationarity and transformations
  6. Build forecasting models

The KSE-100 Index

The KSE-100 Index is a stock index acting as a benchmark to compare prices on the Pakistan Stock Exchange over time. It captures the performance of the top 100 companies listed on the exchange.

Data Description

Our dataset contains daily trading information for the KSE-100 index:

Variables:

  • date: Trading date
  • open: Opening price
  • high: Highest price during the day
  • low: Lowest price during the day
  • close: Closing price
  • vol: Trading volume
  • change_percent: Daily percentage change

Time Period: The data spans multiple years of trading activity.

Setup Requirements

# Required packages
install.packages(c("tidyverse", "fpp3", "tsibble", 
                   "feasts", "forecast", "dygraphs", 
                   "xts", "zoo", "janitor"))

Data Loading & Preparation

Load Data

Code
# Load the dataset.
# data/kse_2025.csv is generated from data/kse_oct25.xlsx
# (source: scstrade.com historical indices, Jan 2022 - Oct 2025).
kse_raw <- read_csv("data/kse_2025.csv") %>% 
  clean_names()

# Display first few rows
head(kse_raw) %>%
  kable(caption = "First 6 rows of raw KSE-100 data") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
First 6 rows of raw KSE-100 data
date price open high low vol change_percent
10/10/2025 163098.2 163984.8 165262.9 162411.2 607604107 -0.87%
10/09/2025 164530.8 165526.9 166730.0 164306.8 698865320 -0.45%
10/08/2025 165266.7 166360.5 166947.2 165109.8 554831161 -0.55%
10/07/2025 166173.7 168299.2 168519.0 165997.2 629739254 -0.94%
10/06/2025 167752.4 169179.0 169326.4 165997.4 623765222 -0.73%
10/03/2025 168990.1 169041.4 169988.6 168613.4 831083556 0.30%

Data Cleaning

Code
kse <- kse_raw %>%
  mutate(
    # robust date parsing
    date = mdy(trimws(date)),

    # % to numeric
    change_percent = parse_number(change_percent),

    # handle 85.44M / 190.65M / 1.2B / 930K, commas, spaces, etc.
    vol_clean = str_trim(vol),
    vol_base  = parse_number(vol_clean),  # strips commas, keeps number
    vol_mult  = case_when(
      str_detect(vol_clean, regex("M$", ignore_case = TRUE)) ~ 1e6,
      str_detect(vol_clean, regex("B$", ignore_case = TRUE)) ~ 1e9,
      str_detect(vol_clean, regex("K$", ignore_case = TRUE)) ~ 1e3,
      TRUE ~ 1
    ),
    vol = vol_base * vol_mult
  ) %>%
  select(date, open, high, low, vol, change_percent) %>%
  arrange(date) %>%
  tidyr::drop_na()

# Create tsibble object (tidy time series)
kse_tsbl <- kse %>%
  as_tsibble(index = date)

# Summary statistics
summary(kse) %>%
  kable(caption = "Summary Statistics of KSE-100 Variables") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Summary Statistics of KSE-100 Variables
date open high low vol change_percent
Min. :2022-01-04 Min. : 38348 Min. : 38553 Min. : 38136 Min. :4.742e+04 Min. :-18.5100
1st Qu.:2022-12-13 1st Qu.: 42358 1st Qu.: 42592 1st Qu.: 42079 1st Qu.:9.282e+07 1st Qu.: -0.4000
Median :2023-11-22 Median : 57504 Median : 58406 Median : 57419 Median :1.562e+08 Median : 0.1300
Mean :2023-11-22 Mean : 70968 Mean : 71429 Mean : 70506 Mean :2.009e+08 Mean : 0.1491
3rd Qu.:2024-11-04 3rd Qu.: 91454 3rd Qu.: 92159 3rd Qu.: 91395 3rd Qu.:2.569e+08 3rd Qu.: 0.7400
Max. :2025-10-10 Max. :169179 Max. :169989 Max. :168613 Max. :1.027e+09 Max. : 22.1800

Data Structure

Code
# Display structure
glimpse(kse_tsbl)
Rows: 933
Columns: 6
$ date           <date> 2022-01-04, 2022-01-05, 2022-01-06, 2022-01-07, 2022-0…
$ open           <dbl> 44947.73, 45409.48, 45396.46, 45022.16, 45345.77, 45926…
$ high           <dbl> 45406.08, 45438.16, 45407.89, 45364.19, 45967.29, 46057…
$ low            <dbl> 44886.89, 45241.45, 45021.32, 44968.90, 45206.00, 45524…
$ vol            <dbl> 134143464, 96600200, 132060208, 84107744, 126790816, 10…
$ change_percent <dbl> 1.12, 0.04, -0.72, 0.58, 1.20, -0.01, 0.08, -0.33, 0.00…
Code
# Check for gaps in the time series
has_gaps(kse_tsbl)
Code
# Count observations by year
kse_tsbl %>%
  mutate(year = year(date)) %>%
  count(year) %>%
  ggplot(aes(x = factor(year), y = n)) +
  geom_col(fill = "steelblue", alpha = 0.7) +
  geom_text(aes(label = n), vjust = -0.5) +
  labs(title = "Number of Trading Days per Year",
       x = "Year", y = "Count") +
  theme_minimal()

Visual Exploration

Time Plot

“The first thing to do in any data analysis task is to plot the data.” - Rob J. Hyndman

Code
# Static time plot
kse_tsbl %>%
  autoplot(open) +
  labs(title = "KSE-100 Index: Closing Prices",
       y = "Closing Price",
       x = "Date") +
  theme_minimal()

Code
# Interactive time plot
kse_xts <- xts(kse$open, order.by = kse$date)
dygraph(kse_xts, main = "KSE-100 Index (Interactive)") %>%
  dyAxis("y", label = "Close Price") %>%
  dyRangeSelector() %>%
  dyOptions(colors = "#2C3E50", strokeWidth = 1.5)

Key Observations:

  • Identify overall trend direction
  • Note any major jumps or crashes
  • Look for periods of high volatility
  • Observe general price levels across time

Multiple Variables

Code
# Plot all price variables
kse_tsbl %>%
  select(date, open, high, low) %>%
  pivot_longer(cols = -date, names_to = "price_type", values_to = "price") %>%
  ggplot(aes(x = date, y = price, color = price_type)) +
  geom_line(alpha = 0.7) +
  labs(title = "KSE-100: All Price Types",
       y = "Price", x = "Date",
       color = "Price Type") +
  theme_minimal() +
  theme(legend.position = "bottom")

Daily Returns

Code
# Calculate daily returns
kse_returns <- kse_tsbl %>%
  mutate(
    daily_return = (open - lag(open)) / lag(open) * 100,
    abs_return = abs(daily_return)
  )

# Plot returns
kse_returns %>%
  ggplot(aes(x = date, y = daily_return)) +
  geom_line(color = "darkblue", alpha = 0.6) +
  geom_hline(yintercept = 0, color = "red", linetype = "dashed") +
  labs(title = "KSE-100 Daily Returns",
       y = "Daily Return (%)",
       x = "Date") +
  theme_minimal()

Code
# Distribution of returns
kse_returns %>%
  ggplot(aes(x = daily_return)) +
  geom_histogram(bins = 50, fill = "steelblue", alpha = 0.7) +
  geom_vline(xintercept = 0, color = "red", linetype = "dashed") +
  labs(title = "Distribution of Daily Returns",
       x = "Daily Return (%)",
       y = "Frequency") +
  theme_minimal()

Volume Analysis

Code
# Volume over time
kse_tsbl %>%
  ggplot(aes(x = date, y = vol)) +
  geom_line(color = "darkgreen", alpha = 0.7) +
  labs(title = "KSE-100 Trading Volume",
       y = "Volume",
       x = "Date") +
  scale_y_continuous(labels = scales::comma) +
  theme_minimal()

Code
# Price vs Volume relationship
kse_tsbl %>%
  ggplot(aes(x = vol, y = open)) +
  geom_point(alpha = 0.3, color = "darkblue") +
  geom_smooth(method = "loess", color = "red") +
  labs(title = "Price vs Volume Relationship",
       x = "Volume",
       y = "Close Price") +
  scale_x_continuous(labels = scales::comma) +
  theme_minimal()

Seasonal Patterns

Seasonal Plots

Code
# Add time features
kse_seasonal <- kse_tsbl %>%
  mutate(
    year = year(date),
    month = month(date, label = TRUE),
    week = week(date),
    wday = wday(date, label = TRUE)
  )

# Monthly patterns
kse_seasonal %>%
  ggplot(aes(x = month, y = open, group = year, color = factor(year))) +
  geom_line(alpha = 0.6) +
  labs(title = "KSE-100: Seasonal Pattern by Month",
       x = "Month", y = "Close Price",
       color = "Year") +
  theme_minimal() +
  theme(legend.position = "right")

Day of Week Effects

Code
# Day of week analysis
weekday_summary <- kse_returns %>%
  mutate(wday = wday(date, label = TRUE)) %>%
  group_by(wday) %>%
  summarise(
    avg_return = mean(daily_return, na.rm = TRUE),
    sd_return = sd(daily_return, na.rm = TRUE),
    n = n()
  )

weekday_summary %>%
  ggplot(aes(x = wday, y = avg_return)) +
  geom_col(fill = "steelblue", alpha = 0.7) +
  geom_errorbar(aes(ymin = avg_return - sd_return, 
                    ymax = avg_return + sd_return),
                width = 0.2) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "red") +
  labs(title = "Average Daily Returns by Day of Week",
       x = "Day of Week",
       y = "Average Return (%)") +
  theme_minimal()

Code
#weekday_summary %>%
 ##      col.names = c("Day", "Avg Return (%)", "Std Dev", "Count"),
   #     digits = 3) %>%
  #able_styling(bootstrap_options = c("striped", "hover"))

Monthly Aggregation

Code
# Aggregate to monthly data
kse_monthly <- kse_tsbl %>%
  mutate(year_month = yearmonth(date)) %>%
  as_tibble() %>%  # Convert to tibble first to remove tsibble structure
  group_by(year_month) %>%
  summarise(
    avg_open = mean(open, na.rm = TRUE),
    max_open = max(open, na.rm = TRUE),
    min_open = min(open, na.rm = TRUE),
    total_vol = sum(vol, na.rm = TRUE),
    .groups = "drop"  # Automatically drop grouping
  ) %>%
  as_tsibble(index = year_month)

# Plot monthly average
kse_monthly %>%
  autoplot(avg_open) +
  labs(title = "KSE-100: Monthly Average Closing Price",
       y = "Average open Price",
       x = "Month") +
  theme_minimal()

Decomposition

Classical Decomposition

Code
# Check if we have enough data for classical decomposition
n_months <- nrow(kse_monthly)
cat("Number of months available:", n_months, "\n")
Number of months available: 46 
Code
# Classical decomposition requires at least 2 complete periods (24 months)
if (n_months >= 24) {
  # Monthly data decomposition
  dcmp_classical <- kse_monthly %>%
    model(
      classical = classical_decomposition(avg_open, type = "additive")
    )
  
  # Plot components
  components(dcmp_classical) %>%
    autoplot() +
    labs(title = "Classical Decomposition: KSE-100 Monthly Average") +
    theme_minimal()
  
  # Extract components
  components(dcmp_classical) %>%
    as_tibble() %>%
    head(12) %>%
    kable(caption = "First 12 months of decomposed components",
          digits = 2) %>%
    kable_styling(bootstrap_options = c("striped", "hover"))
} else {
  cat("⚠️ Not enough data for classical decomposition.\n")
  cat("Need at least 24 months, have", n_months, "months.\n\n")
  cat("Using a simple trend extraction instead:\n\n")
  
  # Alternative: simple moving average trend
  kse_monthly %>%
    mutate(
      trend = slider::slide_dbl(avg_open, mean, 
                                .before = 2, .after = 2, .complete = TRUE)
    ) %>%
    pivot_longer(cols = c(avg_open, trend), 
                 names_to = "component", 
                 values_to = "value") %>%
    ggplot(aes(x = year_month, y = value, color = component)) +
    geom_line(size = 1) +
    scale_color_manual(values = c("avg_open" = "gray60", "trend" = "darkblue"),
                      labels = c("avg_open" = "Original", "trend" = "Trend (5-MA)")) +
    labs(title = "KSE-100: Original Series with Trend",
         y = "Average open Price",
         x = "Month",
         color = "") +
    theme_minimal() +
    theme(legend.position = "bottom")
}
First 12 months of decomposed components
.model year_month avg_open trend seasonal random season_adjust
classical 2022 Jan 45317.20 NA 4798.99 NA 40518.21
classical 2022 Feb 45525.61 NA 953.66 NA 44571.95
classical 2022 Mar 43845.18 NA -144.16 NA 43989.34
classical 2022 Apr 45528.11 NA -1866.20 NA 47394.31
classical 2022 May 43106.09 NA 314.90 NA 42791.19
classical 2022 Jun 41875.98 NA -1368.38 NA 43244.36
classical 2022 Jul 40918.86 42749.12 -738.06 -1092.20 41656.92
classical 2022 Aug 42451.73 42348.10 -1739.07 1842.70 44190.80
classical 2022 Sep 41673.83 42046.70 -3913.94 3541.07 45587.77
classical 2022 Oct 41894.61 41710.83 -2778.52 2962.30 44673.13
classical 2022 Nov 42506.34 41431.67 782.24 292.43 41724.10
classical 2022 Dec 40979.27 41345.47 5698.55 -6064.74 35280.73

STL Decomposition

“STL (Seasonal and Trend decomposition using Loess) is a versatile and robust method for decomposing time series.” - Hyndman

Code
# STL decomposition also requires sufficient data
if (n_months >= 24) {
  # STL decomposition
  dcmp_stl <- kse_monthly %>%
    model(
      stl = STL(avg_open ~ season(window = "periodic"))
    )
  
  # Plot STL components
  components(dcmp_stl) %>%
    autoplot() +
    labs(title = "STL Decomposition: KSE-100 Monthly Average") +
    theme_minimal()
  
  # Seasonally adjusted series
  kse_monthly %>%
    autoplot(avg_open, color = "gray") +
    autolayer(components(dcmp_stl), season_adjust, color = "blue") +
    labs(title = "KSE-100: Original vs Seasonally Adjusted",
         y = "Average open Price",
         x = "Month") +
    theme_minimal()
} else {
  cat("⚠️ Not enough data for STL decomposition.\n")
  cat("Using alternative trend-cycle extraction:\n\n")
  
  # Use a simple loess smoother for trend
  kse_monthly %>%
    mutate(
      trend = predict(loess(avg_open ~ as.numeric(year_month), 
                           data = ., span = 0.3))
    ) %>%
    ggplot(aes(x = year_month)) +
    geom_line(aes(y = avg_open), color = "gray60", alpha = 0.7) +
    geom_line(aes(y = trend), color = "darkred", size = 1.2) +
    labs(title = "KSE-100: Original Series with LOESS Trend",
         y = "Average open Price",
         x = "Month") +
    theme_minimal()
}

Trend Analysis

Code
# Extract and analyze trend
if (exists("dcmp_stl") && n_months >= 24) {
  trend_data <- components(dcmp_stl) %>%
    as_tibble() %>%
    select(year_month, trend)
  
  # Plot trend with confidence interval
  ggplot(trend_data, aes(x = year_month, y = trend)) +
    geom_line(color = "darkblue", size = 1) +
    geom_smooth(method = "loess", color = "red", fill = "pink", alpha = 0.3) +
    labs(title = "KSE-100 Trend Component with Smoothed Trend",
         y = "Trend",
         x = "Month") +
    theme_minimal()
  
  # Calculate trend statistics
  trend_stats <- trend_data %>%
    summarise(
      min_trend = min(trend, na.rm = TRUE),
      max_trend = max(trend, na.rm = TRUE),
      avg_trend = mean(trend, na.rm = TRUE),
      trend_range = max_trend - min_trend,
      pct_change = (max_trend - min_trend) / min_trend * 100
    )
  
  trend_stats %>%
    kable(caption = "Trend Component Statistics",
          digits = 2) %>%
    kable_styling(bootstrap_options = c("striped", "hover"))
} else {
  # Alternative trend analysis using moving average
  kse_monthly %>%
    mutate(
      ma_3 = slider::slide_dbl(avg_open, mean, .before = 1, .after = 1, .complete = TRUE),
      ma_6 = slider::slide_dbl(avg_open, mean, .before = 2, .after = 2, .complete = TRUE)
    ) %>%
    pivot_longer(cols = c(avg_open, ma_3, ma_6),
                 names_to = "series",
                 values_to = "value") %>%
    ggplot(aes(x = year_month, y = value, color = series)) +
    geom_line(size = 1) +
    scale_color_manual(
      values = c("avg_open" = "gray50", "ma_3" = "blue", "ma_6" = "darkred"),
      labels = c("avg_open" = "Original", "ma_3" = "3-Month MA", "ma_6" = "6-Month MA")
    ) +
    labs(title = "KSE-100: Trend Extraction using Moving Averages",
         y = "Average open Price",
         x = "Month",
         color = "") +
    theme_minimal() +
    theme(legend.position = "bottom")
  
  # Simple trend statistics
  kse_monthly %>%
    summarise(
      first_value = first(avg_open),
      last_value = last(avg_open),
      min_value = min(avg_open),
      max_value = max(avg_open),
      total_change = last_value - first_value,
      pct_change = (last_value - first_value) / first_value * 100
    ) %>%
    kable(caption = "Overall Trend Statistics",
          digits = 2) %>%
    kable_styling(bootstrap_options = c("striped", "hover"))
}
Trend Component Statistics
min_trend max_trend avg_trend trend_range pct_change
41271.2 157084.7 73066.35 115813.5 280.62

Autocorrelation

ACF Analysis

Code
# Note: Stock market data has irregular gaps (weekends, holidays)
# We need to use fill_gaps() or work with the index directly

# ACF plot for closing prices
kse_tsbl %>%
  fill_gaps() %>%  # Fill gaps with NA
  ACF(open, lag_max = 50) %>%
  autoplot() +
  labs(title = "ACF: KSE-100 Closing Prices",
       subtitle = "Strong persistence indicates non-stationarity") +
  theme_minimal()

Code
# Alternative: Use acf() from stats package with regular spacing
acf_open <- acf(kse$open, lag.max = 50, plot = FALSE)
autoplot(acf_open) +
  labs(title = "ACF: KSE-100 Closing Prices (Alternative Method)",
       x = "Lag", y = "ACF") +
  theme_minimal()

Code
# ACF for returns
kse_returns %>%
  fill_gaps() %>%
  ACF(daily_return, lag_max = 50) %>%
  autoplot() +
  labs(title = "ACF: Daily Returns",
       subtitle = "Rapid decay suggests openr to stationarity") +
  theme_minimal()

Code
# Alternative for returns
returns_clean <- na.omit(kse_returns$daily_return)
acf_returns <- acf(returns_clean, lag.max = 50, plot = FALSE)
autoplot(acf_returns) +
  labs(title = "ACF: Daily Returns (Alternative Method)",
       x = "Lag", y = "ACF") +
  theme_minimal()

Interpretation:

  • Closing prices: Strong autocorrelation at all lags indicates non-stationarity
    • Values highly correlated with recent past
    • Slow decay typical of price series
  • Returns: Autocorrelation quickly drops to zero (openr to stationarity)
    • Little predictability from past returns
    • Consistent with efficient market hypothesis

PACF Analysis

Code
# PACF plot for closing prices
kse_tsbl %>%
  fill_gaps() %>%
  PACF(open, lag_max = 50) %>%
  autoplot() +
  labs(title = "PACF: KSE-100 Closing Prices",
       subtitle = "High lag-1 value indicates strong AR component") +
  theme_minimal()

Code
# Alternative method
pacf_open <- pacf(kse$open, lag.max = 50, plot = FALSE)
autoplot(pacf_open) +
  labs(title = "PACF: KSE-100 Closing Prices (Alternative Method)",
       x = "Lag", y = "PACF") +
  theme_minimal()

Code
# PACF for returns
kse_returns %>%
  fill_gaps() %>%
  PACF(daily_return, lag_max = 50) %>%
  autoplot() +
  labs(title = "PACF: Daily Returns",
       subtitle = "Values within confidence bands suggest white noise") +
  theme_minimal()

Code
# Alternative for returns
pacf_returns <- pacf(returns_clean, lag.max = 50, plot = FALSE)
autoplot(pacf_returns) +
  labs(title = "PACF: Daily Returns (Alternative Method)",
       x = "Lag", y = "PACF") +
  theme_minimal()

Key Insights:

  • PACF of prices: Strong lag-1 autocorrelation (AR(1) pattern)
  • PACF of returns: Most lags within confidence bands
    • Suggests little to no autoregressive structure
    • Returns appear open to white noise ```

Ljung-Box Test

Code
# Ljung-Box test for returns (using filled gaps)
kse_returns_filled <- kse_returns %>% 
  fill_gaps() %>%
  fill(daily_return, .direction = "down")  # Fill NA with previous value

lb_test <- kse_returns_filled %>%
  features(daily_return, ljung_box, lag = 20, dof = 0)

lb_test %>%
  kable(caption = "Ljung-Box Test Results for Daily Returns",
        digits = 4,
        col.names = c("Statistic", "P-value")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Ljung-Box Test Results for Daily Returns
Statistic P-value
61.0955 0
Code
# Interpretation
cat("\n**Test Result:**\n\n")

**Test Result:**
Code
if(lb_test$lb_pvalue < 0.05) {
  cat("✓ The returns show significant autocorrelation (p < 0.05)\n")
  cat("  → There is predictable structure in returns\n")
  cat("  → ARIMA models may be appropriate\n")
} else {
  cat("✗ No significant autocorrelation in returns (p ≥ 0.05)\n")
  cat("  → Returns appear to be white noise\n")
  cat("  → Market may be informationally efficient\n")
}
✓ The returns show significant autocorrelation (p < 0.05)
  → There is predictable structure in returns
  → ARIMA models may be appropriate
Code
# Additional test on squared returns (test for ARCH effects)
kse_returns_sq <- kse_returns_filled %>%
  mutate(sq_return = daily_return^2)

lb_test_sq <- kse_returns_sq %>%
  features(sq_return, ljung_box, lag = 20, dof = 0)

cat("\n\n**Ljung-Box Test on Squared Returns:**\n\n")


**Ljung-Box Test on Squared Returns:**
Code
lb_test_sq %>%
  kable(caption = "Testing for Volatility Clustering (ARCH Effects)",
        digits = 4,
        col.names = c("Statistic", "P-value")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Testing for Volatility Clustering (ARCH Effects)
Statistic P-value
287.1568 0
Code
if(lb_test_sq$lb_pvalue < 0.05) {
  cat("\n✓ Significant autocorrelation in squared returns\n")
  cat("  → Volatility clustering present\n")
  cat("  → GARCH models may be appropriate\n")
}

✓ Significant autocorrelation in squared returns
  → Volatility clustering present
  → GARCH models may be appropriate

Stationarity Testing

Visual Tests

Code
# Plot with rolling mean and std
kse_rolling <- kse_tsbl %>%
  mutate(
    roll_mean = slider::slide_dbl(open, mean, .before = 29, .complete = TRUE),
    roll_sd = slider::slide_dbl(open, sd, .before = 29, .complete = TRUE)
  )

# Rolling mean
ggplot(kse_rolling, aes(x = date)) +
  geom_line(aes(y = open), alpha = 0.5, color = "gray") +
  geom_line(aes(y = roll_mean), color = "blue", size = 1) +
  labs(title = "KSE-100 with 30-Day Rolling Mean",
       y = "Price", x = "Date") +
  theme_minimal()

Code
# Rolling standard deviation
ggplot(kse_rolling, aes(x = date, y = roll_sd)) +
  geom_line(color = "darkred", size = 1) +
  labs(title = "30-Day Rolling Standard Deviation",
       y = "Standard Deviation", x = "Date") +
  theme_minimal()

Unit Root Tests

Code
# ADF test function
adf_test <- function(x) {
  test <- tseries::adf.test(x, alternative = "stationary")
  tibble(
    statistic = test$statistic,
    p_value = test$p.value,
    conclusion = ifelse(test$p.value < 0.05, "Stationary", "Non-Stationary")
  )
}

# KPSS test function
kpss_test <- function(x) {
  test <- tseries::kpss.test(x)
  tibble(
    statistic = test$statistic,
    p_value = test$p.value,
    conclusion = ifelse(test$p.value < 0.05, "Non-Stationary", "Stationary")
  )
}

# Test closing prices
cat("Tests for Closing Prices:\n\n")
Tests for Closing Prices:
Code
adf_open <- adf_test(na.omit(kse$open))
kpss_open <- kpss_test(na.omit(kse$open))

bind_rows(
  adf_open %>% mutate(test = "ADF"),
  kpss_open %>% mutate(test = "KPSS")
) %>%
  select(test, everything()) %>%
  kable(caption = "Stationarity Tests: Closing Prices",
        digits = 4) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Stationarity Tests: Closing Prices
test statistic p_value conclusion
ADF 0.1951 0.99 Non-Stationary
KPSS 11.4699 0.01 Non-Stationary
Code
# Test returns
cat("\nTests for Daily Returns:\n\n")

Tests for Daily Returns:
Code
returns_clean <- na.omit(kse_returns$daily_return)
adf_returns <- adf_test(returns_clean)
kpss_returns <- kpss_test(returns_clean)

bind_rows(
  adf_returns %>% mutate(test = "ADF"),
  kpss_returns %>% mutate(test = "KPSS")
) %>%
  select(test, everything()) %>%
  kable(caption = "Stationarity Tests: Daily Returns",
        digits = 4) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Stationarity Tests: Daily Returns
test statistic p_value conclusion
ADF -10.1522 0.01 Stationary
KPSS 0.8119 0.01 Non-Stationary

Differencing

Code
# First difference
kse_diff <- kse_tsbl %>%
  mutate(diff_open = difference(open))

# Plot original and differenced series
p1 <- kse_tsbl %>%
  autoplot(open) +
  labs(title = "Original Series", y = "open Price") +
  theme_minimal()

p2 <- kse_diff %>%
  autoplot(diff_open) +
  labs(title = "First Difference", y = "Differenced Price") +
  theme_minimal()

gridExtra::grid.arrange(p1, p2, ncol = 1)

Code
# Test differenced series
diff_clean <- na.omit(kse_diff$diff_open)
cat("Stationarity tests for differenced series:\n\n")
Stationarity tests for differenced series:
Code
bind_rows(
  adf_test(diff_clean) %>% mutate(test = "ADF"),
  kpss_test(diff_clean) %>% mutate(test = "KPSS")
) %>%
  select(test, everything()) %>%
  kable(caption = "Stationarity Tests: First Difference",
        digits = 4) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Stationarity Tests: First Difference
test statistic p_value conclusion
ADF -10.6620 0.01 Stationary
KPSS 1.3851 0.01 Non-Stationary

Simple Forecasting Models

Benchmark Methods

Code
# Use last 30 days as test set
train <- kse_monthly %>% slice(1:(n()-6))
test <- kse_monthly %>% slice((n()-5):n())

# Fit benchmark models
fit_bench <- train %>%
  model(
    Mean = MEAN(avg_open),
    Naive = NAIVE(avg_open),
    Drift = RW(avg_open ~ drift()),
    SNaive = SNAIVE(avg_open)
  )

# Generate forecasts
fc_bench <- fit_bench %>% forecast(h = 6)

# Plot forecasts
fc_bench %>%
  autoplot(train, level = NULL) +
  autolayer(test, avg_open, color = "black") +
  labs(title = "Benchmark Forecasts: KSE-100 Monthly",
       y = "Average open Price",
       x = "Month") +
  theme_minimal() +
  theme(legend.position = "bottom")

Code
# Accuracy
accuracy(fc_bench, test) %>%
  select(.model, RMSE, MAE, MAPE) %>%
  arrange(RMSE) %>%
  kable(caption = "Benchmark Model Accuracy",
        digits = 2) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Benchmark Model Accuracy
.model RMSE MAE MAPE
Drift 23144.87 18345.48 11.96
Naive 30089.41 24164.09 15.80
SNaive 63309.28 61608.15 43.17
Mean 80909.12 78897.51 55.35

ETS Models

Code
# Fit ETS model
fit_ets <- train %>%
  model(
    auto_ets = ETS(avg_open)
  )

# Report model
report(fit_ets)
Series: avg_open 
Model: ETS(M,A,N) 
  Smoothing parameters:
    alpha = 0.9998999 
    beta  = 0.2315201 

  Initial states:
     l[0]      b[0]
 45873.21 -105.1003

  sigma^2:  0.0026

     AIC     AICc      BIC 
791.8367 793.6014 800.2811 
Code
# Forecast
fc_ets <- fit_ets %>% forecast(h = 6)

# Plot
fc_ets %>%
  autoplot(train) +
  autolayer(test, avg_open, color = "black") +
  labs(title = "ETS Forecast: KSE-100 Monthly",
       y = "Average open Price") +
  theme_minimal()

Code
# Accuracy
accuracy(fc_ets, test) %>%
  select(.model, RMSE, MAE, MAPE) %>%
  kable(caption = "ETS Model Accuracy",
        digits = 2) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
ETS Model Accuracy
.model RMSE MAE MAPE
auto_ets 17741.68 14184.15 9.29

ARIMA Models

Code
# Fit ARIMA model
fit_arima <- train %>%
  model(
    auto_arima = ARIMA(avg_open, stepwise = FALSE, approximation = FALSE)
  )

# Report model
report(fit_arima)
Series: avg_open 
Model: ARIMA(0,2,2)(0,0,1)[12] 

Coefficients:
          ma1      ma2    sma1
      -0.4254  -0.4453  0.6022
s.e.   0.1783   0.1815  0.2870

sigma^2 estimated as 8051476:  log likelihood=-357.57
AIC=723.15   AICc=724.36   BIC=729.7
Code
# Forecast
fc_arima <- fit_arima %>% forecast(h = 6)

# Plot
fc_arima %>%
  autoplot(train) +
  autolayer(test, avg_open, color = "black") +
  labs(title = "ARIMA Forecast: KSE-100 Monthly",
       y = "Average open Price") +
  theme_minimal()

Code
# Residual diagnostics
fit_arima %>%
  gg_tsresiduals() +
  labs(title = "Residual Diagnostics: ARIMA Model")

Code
# Accuracy
accuracy(fc_arima, test) %>%
  select(.model, RMSE, MAE, MAPE) %>%
  kable(caption = "ARIMA Model Accuracy",
        digits = 2) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
ARIMA Model Accuracy
.model RMSE MAE MAPE
auto_arima 21234.84 16748.58 10.9

Model Comparison

Code
# Compare all models
all_fc <- bind_rows(
  fc_bench,
  fc_ets,
  fc_arima
)

# Plot comparison
all_fc %>%
  autoplot(train, level = NULL) +
  autolayer(test, avg_open, color = "black", size = 1.2) +
  labs(title = "Forecast Comparison: All Models",
       y = "Average open Price",
       x = "Month") +
  theme_minimal() +
  theme(legend.position = "bottom")

Code
# Accuracy comparison
accuracy(all_fc, test) %>%
  select(.model, RMSE, MAE, MAPE, MASE) %>%
  arrange(RMSE) %>%
  mutate(rank = row_number()) %>%
  kable(caption = "Model Accuracy Comparison (Ranked by RMSE)",
        digits = 3) %>%
  kable_styling(bootstrap_options = c("striped", "hover")) %>%
  row_spec(1, bold = TRUE, background = "#e8f5e9")
Model Accuracy Comparison (Ranked by RMSE)
.model RMSE MAE MAPE MASE rank
auto_ets 17741.68 14184.15 9.288 NaN 1
auto_arima 21234.84 16748.58 10.904 NaN 2
Drift 23144.88 18345.48 11.961 NaN 3
Naive 30089.41 24164.09 15.800 NaN 4
SNaive 63309.29 61608.15 43.169 NaN 5
Mean 80909.12 78897.51 55.347 NaN 6

Advanced Topics

Volatility Modeling

Code
# Calculate realized volatility
kse_vol <- kse_returns %>%
  mutate(
    sq_return = daily_return^2,
    abs_return = abs(daily_return)
  ) %>%
  mutate(
    realized_vol = sqrt(slider::slide_dbl(sq_return, mean, 
                                          .before = 19, .complete = TRUE)) * sqrt(252)
  )

# Plot volatility
kse_vol %>%
  ggplot(aes(x = date, y = realized_vol)) +
  geom_line(color = "darkred", alpha = 0.7) +
  labs(title = "Realized Volatility (20-Day, Annualized)",
       y = "Volatility (%)",
       x = "Date") +
  theme_minimal()

Code
# Volatility clustering visualization
kse_vol %>%
  ggplot(aes(x = date, y = abs_return)) +
  geom_line(alpha = 0.5, color = "gray50") +
  geom_smooth(span = 0.1, color = "darkred", se = FALSE) +
  labs(title = "Volatility Clustering in Daily Returns",
       y = "Absolute Return (%)",
       x = "Date") +
  theme_minimal()

Moving Averages

Code
# Calculate multiple moving averages
kse_ma <- kse_tsbl %>%
  mutate(
    ma_20 = slider::slide_dbl(open, mean, .before = 19, .complete = TRUE),
    ma_50 = slider::slide_dbl(open, mean, .before = 49, .complete = TRUE),
    ma_200 = slider::slide_dbl(open, mean, .before = 199, .complete = TRUE)
  )

# Plot with moving averages
kse_ma %>%
  filter(date >= max(date) - years(2)) %>%
  ggplot(aes(x = date)) +
  geom_line(aes(y = open), color = "gray50", alpha = 0.5) +
  geom_line(aes(y = ma_20, color = "20-Day MA"), size = 1) +
  geom_line(aes(y = ma_50, color = "50-Day MA"), size = 1) +
  geom_line(aes(y = ma_200, color = "200-Day MA"), size = 1) +
  scale_color_manual(values = c("20-Day MA" = "blue", 
                                "50-Day MA" = "red",
                                "200-Day MA" = "darkgreen")) +
  labs(title = "KSE-100 with Moving Averages (Last 2 Years)",
       y = "open Price", x = "Date",
       color = "") +
  theme_minimal() +
  theme(legend.position = "bottom")

Feature Extraction

Code
# Extract time series features
kse_features <- kse_monthly %>%
  features(avg_open, feature_set(pkgs = "feasts"))

# Display key features
kse_features %>%
  select(trend_strength, seasonal_strength_year, 
         acf1, pacf1, diff1_acf1) %>%
  pivot_longer(everything(), names_to = "feature", values_to = "value") %>%
  kable(caption = "Key Time Series Features",
        digits = 3) %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Error in `select()`:
! Can't select columns that don't exist.
✖ Column `pacf1` doesn't exist.
Code
# Spectral analysis
kse_tsbl %>%
  gg_season(open, period = "year") +
  labs(title = "Seasonal Plot by Year") +
  theme_minimal()
Error in `check_gaps()`:
! data contains implicit gaps in time. You should check your data and convert implicit gaps into explicit missing values using `tsibble::fill_gaps()` if required.

Summary & Conclusions

Key Findings

1. Time Series Characteristics

  • Trend: The KSE-100 index shows [describe trend - upward/downward/cyclical]
  • Seasonality: [Describe any seasonal patterns found]
  • Volatility: Evidence of volatility clustering, typical of financial time series
  • Stationarity: Price levels are non-stationary; returns are openr to stationarity

2. Best Performing Models

Based on our analysis:

Code
# Create summary table (using previous accuracy results)
tibble(
  Model = c("Benchmark (Naive)", "ETS", "ARIMA"),
  `Best For` = c("Simple baseline", "Automatic selection", "Custom tuning"),
  Strengths = c("Simple, interpretable", "Handles trend/seasonality", "Flexible specification"),
  Limitations = c("No trend capture", "Can overfit", "Requires expertise")
) %>%
  kable(caption = "Model Summary") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
Model Summary
Model Best For Strengths Limitations
Benchmark (Naive) Simple baseline Simple, interpretable No trend capture
ETS Automatic selection Handles trend/seasonality Can overfit
ARIMA Custom tuning Flexible specification Requires expertise

3. Practical Insights

  • Day-of-week effects: [Describe if found]
  • Volatility patterns: High volatility during [specific periods]
  • Forecast horizon: Short-term forecasts (1-7 days) more reliable than long-term

Recommendations

For Investors

  1. Short-term trading: Use technical indicators (moving averages) combined with volatility measures
  2. Long-term investing: Focus on trend components, ignore short-term noise
  3. Risk management: Account for volatility clustering when sizing positions

For Further Analysis

  1. Multivariate models: Include macroeconomic variables (GDP, inflation, interest rates)
  2. Machine learning: Explore neural networks for pattern recognition
  3. High-frequency data: Analyze intraday patterns if available
  4. Event studies: Impact of specific news/events on returns
  5. Regime switching: Model different market regimes (bull/bear markets)

Learning Resources

Books

  1. Forecasting: Principles and Practice (3rd ed) by Rob J. Hyndman & George Athanasopoulos
  2. Time Series Analysis and Its Applications by Robert Shumway & David Stoffer
    • Comprehensive coverage of time series methods
  3. Introduction to Time Series and Forecasting by Peter Brockwell & Richard Davis
    • Mathematical foundation for time series

Online Courses

  • Coursera: “Practical Time Series Analysis”
  • DataCamp: “Time Series with R”
  • Rob Hyndman’s YouTube channel for lectures

R Packages

# Core packages
library(fpp3)      # Forecasting bundle
library(tsibble)   # Tidy time series
library(feasts)    # Feature extraction
library(fable)     # Forecasting models

# Visualization
library(dygraphs)  # Interactive plots
library(plotly)    # Interactive graphics

# Advanced
library(prophet)   # Facebook's forecasting
library(forecast)  # Classical methods

Exercises

Exercise 1: Seasonal Analysis

Modify the seasonal decomposition to use multiplicative instead of additive. When is multiplicative decomposition more appropriate?

Exercise 2: Model Selection

Try different ARIMA specifications manually. Compare with auto.arima(). Can you beat the automatic selection?

Exercise 3: Forecast Evaluation

Implement rolling window cross-validation. How does model performance change across different time periods?

Exercise 4: Trading Strategy

Design a simple moving average crossover strategy. Backtest it on the KSE-100 data. Calculate returns and maximum drawdown.

Exercise 5: Volatility Forecasting

Implement a GARCH model for volatility forecasting. Compare with realized volatility.

Code Repository

The complete code for this tutorial is available with:

# All data processing steps
# Model fitting procedures  
# Visualization functions
# Custom helper functions

References

  1. Hyndman, R.J., & Athanasopoulos, G. (2021). Forecasting: principles and practice (3rd ed). OTexts: Melbourne, Australia.

  2. Box, G. E., Jenkins, G. M., Reinsel, G. C., & Ljung, G. M. (2015). Time series analysis: forecasting and control (5th ed). John Wiley & Sons.

  3. Shumway, R. H., & Stoffer, D. S. (2017). Time series analysis and its applications: with R examples (4th ed). Springer.

  4. Cleveland, R. B., Cleveland, W. S., McRae, J. E., & Terpenning, I. (1990). STL: A seasonal-trend decomposition. Journal of official statistics, 6(1), 3-73.

Appendix

Full Code

Here’s the complete workflow in a single code block:

Code
# Complete KSE-100 Time Series Analysis Workflow
# ================================================

# 1. SETUP
library(tidyverse)
library(fpp3)
library(tsibble)
library(feasts)
library(forecast)
library(dygraphs)
library(xts)
library(zoo)
library(janitor)

# 2. DATA LOADING
kse_raw <- read_csv("data/kse_2025.csv") %>% 
  clean_names()

# 3. DATA PREPARATION
kse <- kse_raw %>%
  mutate(
    date = as.Date(date, format = "%d-%b-%y"),
    change_percent = as.numeric(gsub("%", "", change_percent)),
    vol = as.numeric(gsub(",", "", vol))
  ) %>%
  select(date, open, high, low, close, vol, change_percent) %>%
  arrange(date) %>%
  drop_na()

kse_tsbl <- kse %>% as_tsibble(index = date)

# 4. EXPLORATORY ANALYSIS
kse_tsbl %>% autoplot(close)
kse_tsbl %>% ACF(close) %>% autoplot()
kse_tsbl %>% PACF(close) %>% autoplot()

# 5. RETURNS CALCULATION
kse_returns <- kse_tsbl %>%
  mutate(daily_return = (close - lag(close)) / lag(close) * 100)

# 6. DECOMPOSITION
kse_monthly <- kse_tsbl %>%
  mutate(year_month = yearmonth(date)) %>%
  group_by(year_month) %>%
  summarise(avg_close = mean(close)) %>%
  as_tsibble(index = year_month)

dcmp <- kse_monthly %>%
  model(stl = STL(avg_close))

components(dcmp) %>% autoplot()

# 7. MODELING
train <- kse_monthly %>% slice(1:(n()-6))
test <- kse_monthly %>% slice((n()-5):n())

fit <- train %>%
  model(
    naive = NAIVE(avg_close),
    ets = ETS(avg_close),
    arima = ARIMA(avg_close)
  )

# 8. FORECASTING
fc <- fit %>% forecast(h = 6)

# 9. EVALUATION
accuracy(fc, test)

# 10. VISUALIZATION
fc %>%
  autoplot(train) +
  autolayer(test, avg_close)

Data Dictionary

Complete variable descriptions:

Code
tibble(
  Variable = c("date", "open", "high", "low", "close", "vol", "change_percent"),
  Description = c(
    "Trading date (YYYY-MM-DD format)",
    "Opening price at market open",
    "Highest price during trading day",
    "Lowest price during trading day",
    "Closing price at market close",
    "Total trading volume",
    "Daily percentage change in closing price"
  ),
  Type = c("Date", "Numeric", "Numeric", "Numeric", "Numeric", "Numeric", "Numeric"),
  Unit = c("-", "Index Points", "Index Points", "Index Points", 
           "Index Points", "Shares", "Percent")
) %>%
  kable(caption = "KSE-100 Data Dictionary") %>%
  kable_styling(bootstrap_options = c("striped", "hover"))
KSE-100 Data Dictionary
Variable Description Type Unit
date Trading date (YYYY-MM-DD format) Date -
open Opening price at market open Numeric Index Points
high Highest price during trading day Numeric Index Points
low Lowest price during trading day Numeric Index Points
close Closing price at market close Numeric Index Points
vol Total trading volume Numeric Shares
change_percent Daily percentage change in closing price Numeric Percent

Helper Functions

Custom functions used in this tutorial:

Code
# Calculate multiple moving averages
calc_moving_averages <- function(data, price_col, windows = c(20, 50, 200)) {
  for(w in windows) {
    col_name <- paste0("ma_", w)
    data <- data %>%
      mutate(!!col_name := slider::slide_dbl(
        .data[[price_col]], mean, 
        .before = w-1, .complete = TRUE
      ))
  }
  return(data)
}

# Calculate volatility
calc_volatility <- function(returns, window = 20, annualize = TRUE) {
  vol <- sqrt(slider::slide_dbl(
    returns^2, mean, 
    .before = window-1, .complete = TRUE
  ))
  
  if(annualize) {
    vol <- vol * sqrt(252)  # Assuming 252 trading days
  }
  
  return(vol)
}

# Create interactive time series plot
plot_interactive_ts <- function(data, date_col, value_col, title = "") {
  xts_data <- xts(data[[value_col]], order.by = data[[date_col]])
  
  dygraph(xts_data, main = title) %>%
    dyRangeSelector() %>%
    dyOptions(colors = "#2C3E50", strokeWidth = 1.5)
}

# Rolling window cross-validation
rolling_cv <- function(data, model_fn, h = 1, window = 100) {
  n <- nrow(data)
  errors <- numeric(n - window - h + 1)
  
  for(i in seq_along(errors)) {
    train <- data[i:(i+window-1), ]
    test <- data[i+window+h-1, ]
    
    fit <- model_fn(train)
    fc <- forecast(fit, h = h)
    errors[i] <- test$value - fc$mean[h]
  }
  
  return(errors)
}

# Calculate performance metrics
calc_metrics <- function(actual, predicted) {
  errors <- actual - predicted
  
  tibble(
    RMSE = sqrt(mean(errors^2, na.rm = TRUE)),
    MAE = mean(abs(errors), na.rm = TRUE),
    MAPE = mean(abs(errors/actual) * 100, na.rm = TRUE),
    MASE = mean(abs(errors), na.rm = TRUE) / 
           mean(abs(diff(actual)), na.rm = TRUE)
  )
}

Troubleshooting

Common issues and solutions:

Issue 1: Date Parsing Errors

# Problem: Dates not recognized
# Solution: Specify format explicitly
dates <- as.Date(date_column, format = "%d-%b-%y")

Issue 2: Missing Values

# Check for NA values
sum(is.na(data))

# Remove rows with NA
data_clean <- data %>% drop_na()

# Or interpolate
data_imputed <- data %>%
  mutate(value = zoo::na.approx(value))

Issue 3: Irregular Time Series

# Check for gaps
has_gaps(tsibble_data)

# Fill gaps
tsibble_data %>% fill_gaps()

Issue 4: Model Convergence

# ARIMA not converging
# Solution: Try different starting values or simpler models
model(ARIMA(close ~ pdq(1,1,1) + PDQ(0,0,0)))

Issue 5: Memory Issues with Large Data

# Use data.table for large datasets
library(data.table)
kse_dt <- fread("large_file.csv")

# Or use sampling
kse_sample <- kse %>% slice_sample(prop = 0.1)

Session Info

Code
R version 4.6.0 (2026-04-24 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 22621)

Matrix products: default
  LAPACK version 3.12.1

locale:
[1] LC_COLLATE=English_Pakistan.utf8  LC_CTYPE=English_Pakistan.utf8   
[3] LC_MONETARY=English_Pakistan.utf8 LC_NUMERIC=C                     
[5] LC_TIME=English_Pakistan.utf8    

time zone: Asia/Karachi
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
 [1] kableExtra_1.4.0  knitr_1.51        janitor_2.2.1     xts_0.14.2       
 [5] zoo_1.8-15        dygraphs_1.1.1.6  forecast_9.0.2    fable_0.5.0      
 [9] feasts_0.5.0      fabletools_0.6.1  ggtime_0.2.0      tsibbledata_0.4.1
[13] tsibble_1.2.0     fpp3_1.0.3        lubridate_1.9.5   forcats_1.0.1    
[17] stringr_1.6.0     dplyr_1.2.1       purrr_1.2.2       readr_2.2.0      
[21] tidyr_1.3.2       tibble_3.3.1      ggplot2_4.0.3     tidyverse_2.0.0  

loaded via a namespace (and not attached):
 [1] tidyselect_1.2.1     viridisLite_0.4.3    timeDate_4052.112   
 [4] farver_2.1.2         S7_0.2.2             warp_0.2.3          
 [7] fastmap_1.2.0        digest_0.6.39        timechange_0.4.0    
[10] lifecycle_1.0.5      magrittr_2.0.5       compiler_4.6.0      
[13] rlang_1.2.0          tools_4.6.0          yaml_2.3.12         
[16] labeling_0.4.3       htmlwidgets_1.6.4    curl_7.1.0          
[19] bit_4.6.0            TTR_0.24.4           xml2_1.5.2          
[22] RColorBrewer_1.1-3   withr_3.0.2          grid_4.6.0          
[25] colorspace_2.1-2     progressr_0.19.0     scales_1.4.0        
[28] cli_3.6.6            anytime_0.3.13       rmarkdown_2.31      
[31] slider_0.3.3         crayon_1.5.3         generics_0.1.4      
[34] otel_0.2.0           rstudioapi_0.18.0    tzdb_0.5.0          
[37] splines_4.6.0        parallel_4.6.0       urca_1.3-4          
[40] vctrs_0.7.3          Matrix_1.7-5         jsonlite_2.0.0      
[43] hms_1.1.4            tseries_0.10-61      bit64_4.8.0         
[46] systemfonts_1.3.2    ggdist_3.3.3         quantmod_0.4.28     
[49] glue_1.8.1           distributional_0.7.0 stringi_1.8.7       
[52] gtable_0.3.6         quadprog_1.5-8       pillar_1.11.1       
[55] rappdirs_0.3.4       htmltools_0.5.9      R6_2.6.1            
[58] textshaping_1.0.5    vroom_1.7.1          evaluate_1.0.5      
[61] lattice_0.22-9       snakecase_0.11.1     fracdiff_1.5-3      
[64] Rcpp_1.1.1-1         gridExtra_2.3        svglite_2.2.2       
[67] nlme_3.1-169         mgcv_1.9-4           xfun_0.57           
[70] pkgconfig_2.0.3     

Contact & Support

Questions? Feel free to reach out:

  • GitHub Issues: [your-repo/issues]
  • Email: [your-email]
  • Twitter: [@your-handle]

Acknowledgments:

This tutorial was inspired by the teaching philosophy of Professor Rob J. Hyndman and the excellent resources provided by the fpp3 package ecosystem.


Last Updated: 2026-09-12

Version: 1.0

License: MIT License - Free to use and modify with attribution


Back to top