{ "cells": [ { "cell_type": "markdown", "id": "384ff7b6", "metadata": {}, "source": [ "# Week 11: Time Series Analysis and Forecasting\n", "\n", "*Kmex Consult — Data Science Fundamentals Course*\n", "\n", "Work through each code cell in order. Edit and re-run to experiment." ] }, { "cell_type": "markdown", "id": "412810cc", "metadata": {}, "source": [ "*Data Science Fundamentals Course*" ] }, { "cell_type": "markdown", "id": "bc4ed0e4", "metadata": {}, "source": [ "## Week 11 Overview\n", "Week 11 introduces time series analysis - a specialized area of data science dealing with data collected over time. Time series data is everywhere:\n", "\n", "Real-world applications:\n", "- Stock price prediction\n", "- Weather and climate forecasting\n", "- Demand forecasting for inventory\n", "- Sales and revenue projections\n", "- Traffic and transportation planning\n", "- Sensor data monitoring\n", "- Website traffic analysis\n", "- Epidemic forecasting\n", "- Energy consumption prediction\n", "\n", "Unique challenges of time series:\n", "- Data points are not independent (today depends on yesterday)\n", "- Violates many statistical assumptions\n", "- Need specialized methods and tests\n", "- Patterns change over time (non-stationary)\n", "- Multiple sources of variation (trend, seasonality, cycles)\n", "\n", "This week focuses on:\n", "- Understanding time series components\n", "- Making data stationary (required for many methods)\n", "- Forecasting methods: simple to advanced\n", "- Evaluating forecast accuracy\n", "- Real-world applications\n", "\n", "By the end of Week 11, you will be able to:\n", "- Identify time series components (trend, seasonality, noise)\n", "- Check and achieve stationarity (ADF test)\n", "- Apply exponential smoothing methods\n", "- Build ARIMA models\n", "- Evaluate and compare forecasts\n", "- Handle seasonality and trends\n", "- Avoid common time series mistakes\n", "\n", "Week 11 is divided into three 2-hour sessions:\n", "- Session 1: Time Series Fundamentals and Exploration\n", "- Session 2: Forecasting Methods (Exponential Smoothing and ARIMA)\n", "- Session 3: Advanced Methods and Model Evaluation" ] }, { "cell_type": "markdown", "id": "eaada705", "metadata": {}, "source": [ "## SESSION 1: Time Series Fundamentals and Exploration" ] }, { "cell_type": "markdown", "id": "7f96258d", "metadata": {}, "source": [ "### Duration: 2 hours" ] }, { "cell_type": "markdown", "id": "752f94b5", "metadata": {}, "source": [ "### 1.1 Understanding Time Series Data" ] }, { "cell_type": "code", "execution_count": null, "id": "9523daf2", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "from datetime import datetime, timedelta\n", "\n", "print(\"=\"*50)\n", "print(\"TIME SERIES DATA FUNDAMENTALS\")\n", "print(\"=\"*50)\n", "\n", "print(\"\"\"\n", "WHAT IS TIME SERIES DATA?\n", "- Measurements taken at regular intervals over time\n", "- Data points are DEPENDENT (not independent)\n", "- Order matters: can't shuffle randomly\n", "- Sequential patterns: patterns repeat, trends emerge\n", "\n", "EXAMPLES:\n", "Daily:\n", "- Stock closing prices\n", "- Temperature readings\n", "- Website page views\n", "\n", "Weekly:\n", "- Sales figures\n", "- Customer counts\n", "\n", "Monthly:\n", "- Unemployment rate\n", "- Retail sales\n", "\n", "Hourly:\n", "- Server load\n", "- Heart rate monitoring\n", "- Power consumption\n", "\"\"\")\n", "\n", "# Create sample time series\n", "print(\"\\nEXAMPLE: SALES DATA OVER TIME\")\n", "print(\"-\"*50)\n", "\n", "# Create date range\n", "dates = pd.date_range(start='2023-01-01', periods=365, freq='D')\n", "\n", "# Create synthetic sales data with trend and seasonality\n", "np.random.seed(42)\n", "trend = np.linspace(100, 150, 365) # Increasing trend\n", "seasonality = 20 * np.sin(np.arange(365) * 2 * np.pi / 365) # Yearly cycle\n", "noise = np.random.normal(0, 5, 365) # Random noise\n", "sales = trend + seasonality + noise\n", "\n", "# Create DataFrame\n", "ts_data = pd.DataFrame({\n", "'Date': dates,\n", "'Sales': sales\n", "})\n", "\n", "print(f\"\\nDataset: {len(ts_data)} daily sales observations\")\n", "print(f\"Date range: {ts_data['Date'].min()} to {ts_data['Date'].max()}\")\n", "print(f\"Sales range: ${ts_data['Sales'].min():.2f} to ${ts_data['Sales'].max():.2f}\")\n", "print(f\"\\nFirst few rows:\")\n", "print(ts_data.head(10))\n", "\n", "# Visualization\n", "fig, axes = plt.subplots(2, 2, figsize=(14, 10))\n", "\n", "# Plot 1: Full time series\n", "axes[0, 0].plot(ts_data['Date'], ts_data['Sales'], linewidth=1.5)\n", "axes[0, 0].set_xlabel('Date')\n", "axes[0, 0].set_ylabel('Sales ($)')\n", "axes[0, 0].set_title('Sales Over Time (Full Year)')\n", "axes[0, 0].grid(True, alpha=0.3)\n", "\n", "# Plot 2: Components (trend, seasonality, noise)\n", "axes[0, 1].plot(dates, trend, label='Trend', linewidth=2)\n", "axes[0, 1].plot(dates, seasonality, label='Seasonality', linewidth=1.5, alpha=0.7)\n", "axes[0, 1].plot(dates, noise, label='Noise', linewidth=1, alpha=0.5)\n", "axes[0, 1].set_xlabel('Date')\n", "axes[0, 1].set_ylabel('Component Value')\n", "axes[0, 1].set_title('Time Series Components')\n", "axes[0, 1].legend()\n", "axes[0, 1].grid(True, alpha=0.3)\n", "\n", "# Plot 3: First 90 days (zoom in)\n", "axes[1, 0].plot(ts_data['Date'][:90], ts_data['Sales'][:90], marker='o', linewidth=2, markersize=4)\n", "axes[1, 0].set_xlabel('Date')\n", "axes[1, 0].set_ylabel('Sales ($)')\n", "axes[1, 0].set_title('First 90 Days (Zoomed In)')\n", "axes[1, 0].grid(True, alpha=0.3)\n", "\n", "# Plot 4: Month comparison\n", "ts_data['Month'] = ts_data['Date'].dt.month\n", "ts_data.boxplot(column='Sales', by='Month', ax=axes[1, 1])\n", "axes[1, 1].set_xlabel('Month')\n", "axes[1, 1].set_ylabel('Sales ($)')\n", "axes[1, 1].set_title('Sales Distribution by Month')\n", "plt.sca(axes[1, 1])\n", "plt.xticks(range(1, 13), ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',\n", "'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'])\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "# Time series components\n", "print(\"\\n\" + \"=\"*50)\n", "print(\"TIME SERIES COMPONENTS\")\n", "print(\"=\"*50)\n", "\n", "print(\"\"\"\n", "EVERY TIME SERIES CAN BE DECOMPOSED INTO:\n", "\n", "1. TREND (Long-term direction)\n", "- Is data going up, down, or staying flat?\n", "- Often due to fundamental changes\n", "- Example: Sales growing due to business expansion\n", "- Can be linear or non-linear\n", "- Duration: Months to years\n", "\n", "2. SEASONALITY (Regular repeating patterns)\n", "- Patterns that repeat at fixed intervals\n", "- Can be: Daily, Weekly, Monthly, Yearly\n", "- Due to systematic factors\n", "- Examples:\n", "* Retail: Higher sales before holidays\n", "* Temperature: Hot in summer, cold in winter\n", "* Traffic: More cars during rush hours\n", "- Seasonal period: Time for pattern to repeat\n", "\n", "3. CYCLICAL (Long-term oscillations)\n", "- Rises and falls that are NOT fixed intervals\n", "- Longer than seasonality\n", "- Often tied to economic cycles\n", "- Difficult to forecast\n", "- Example: Business cycle, stock market cycles\n", "\n", "4. NOISE/IRREGULAR (Random variation)\n", "- Unexplained random fluctuations\n", "- Due to: Measurement error, unexpected events\n", "- No predictable pattern\n", "- Cannot be forecast\n", "\n", "TIME SERIES DECOMPOSITION FORMULA:\n", "Y_t = Trend_t + Seasonal_t + Cyclic_t + Noise_t\n", "or (multiplicative):\n", "Y_t = Trend_t × Seasonal_t × Noise_t\n", "\n", "ADDITIVE: Use when seasonal variation constant\n", "MULTIPLICATIVE: Use when seasonal variation increases with level\n", "\n", "IMPORTANCE FOR FORECASTING:\n", "- Understand each component\n", "- Forecast each separately\n", "- Combine forecasts\n", "- Better results than treating as one series\n", "\"\"\")\n", "\n", "# Autocorrelation\n", "print(\"\\n\" + \"=\"*50)\n", "print(\"AUTOCORRELATION - KEY CONCEPT FOR TIME SERIES\")\n", "print(\"=\"*50)\n", "\n", "print(\"\"\"\n", "AUTOCORRELATION (ACF):\n", "- Correlation of time series with itself at different lags\n", "- Lag: How many time steps back\n", "- ACF(1): Correlation with 1 step ago\n", "- ACF(2): Correlation with 2 steps ago\n", "- Range: -1 to 1 (like correlation)\n", "\n", "WHAT IT TELLS US:\n", "- How dependent today is on yesterday\n", "- Whether data has memory\n", "- Helps identify patterns\n", "\n", "PATTERNS:\n", "- High ACF at many lags: Strong trend (data not stationary)\n", "- ACF decays quickly: Stationary data (good for forecasting)\n", "- Spikes at specific lags: Seasonal pattern\n", "- Random low values: White noise (can't predict)\n", "\n", "PARTIAL AUTOCORRELATION (PACF):\n", "- Direct correlation at each lag\n", "- Removes intermediate lag effects\n", "- Used for ARIMA model identification\n", "\n", "IMPORTANCE:\n", "- First check when analyzing time series\n", "- Tells if data is stationary (needed for ARIMA)\n", "- Helps choose forecasting method\n", "\"\"\")\n", "\n", "# Calculate ACF manually\n", "from pandas.plotting import autocorrelation_plot\n", "\n", "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n", "\n", "# ACF plot\n", "from statsmodels.graphics.tsaplots import plot_acf\n", "plot_acf(ts_data['Sales'], lags=40, ax=axes[0])\n", "axes[0].set_title('Autocorrelation Function (ACF)')\n", "axes[0].set_xlabel('Lag (days)')\n", "\n", "# Manual lagged plot\n", "for lag in [1, 7, 30]:\n", "lagged_sales = ts_data['Sales'].shift(lag)\n", "correlation = ts_data['Sales'].corr(lagged_sales)\n", "print(f\"Correlation at lag {lag}: {correlation:.3f}\")\n", "\n", "# Seasonal subseries plot\n", "ts_data['DayOfWeek'] = ts_data['Date'].dt.dayofweek\n", "ts_data['Week'] = ts_data['Date'].dt.isocalendar().week\n", "\n", "axes[1].scatter(ts_data['DayOfWeek'], ts_data['Sales'], alpha=0.5)\n", "axes[1].set_xlabel('Day of Week (0=Monday)')\n", "axes[1].set_ylabel('Sales ($)')\n", "axes[1].set_title('Seasonal Pattern by Day of Week')\n", "axes[1].grid(True, alpha=0.3)\n", "\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "27cf2392", "metadata": {}, "source": [ "### 1.2 Stationarity and Differencing" ] }, { "cell_type": "code", "execution_count": null, "id": "902ff333", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "from statsmodels.tsa.stattools import adfuller, kpss\n", "import matplotlib.pyplot as plt\n", "\n", "print(\"=\"*50)\n", "print(\"STATIONARITY - CRITICAL FOR FORECASTING\")\n", "print(\"=\"*50)\n", "\n", "print(\"\"\"\n", "WHAT IS STATIONARY DATA?\n", "Stationary time series has:\n", "✓ Constant mean over time\n", "✓ Constant variance over time\n", "✓ No trend (flat over long term)\n", "✓ No systematic patterns\n", "\n", "Non-stationary data has:\n", "✗ Changing mean (upward/downward trend)\n", "✗ Changing variance (becoming more/less volatile)\n", "✗ Clear trend or drift\n", "✗ Seasonal patterns\n", "\n", "WHY STATIONARITY MATTERS:\n", "- Most forecasting methods (ARIMA, etc.) REQUIRE stationary data\n", "- Non-stationary → unreliable forecasts\n", "- Violations: Forecasts will be poor, confidence intervals wrong\n", "- Solution: Transform data to make stationary\n", "\n", "EXAMPLES:\n", "Stationary: Random walk around fixed value\n", "Non-stationary: Stock price (has trend)\n", "\n", "TESTING FOR STATIONARITY:\n", "1. Visual inspection: Plot and look for trend/changes\n", "2. Statistical tests:\n", "- Augmented Dickey-Fuller (ADF) test\n", "- KPSS test\n", "- Phillips-Perron test\n", "\"\"\")\n", "\n", "# Create stationary vs non-stationary data\n", "np.random.seed(42)\n", "n = 200\n", "\n", "# Non-stationary (random walk with drift)\n", "non_stationary = np.cumsum(np.random.normal(0.5, 1, n))\n", "\n", "# Stationary (mean-reverting)\n", "stationary = np.random.normal(0, 1, n)\n", "\n", "# Seasonal non-stationary\n", "t = np.arange(n)\n", "seasonal_trend = 0.1 * t + 10 * np.sin(2 * np.pi * t / 30) + np.random.normal(0, 1, n)\n", "\n", "# Visualization\n", "fig, axes = plt.subplots(3, 2, figsize=(14, 12))\n", "\n", "# Row 1: Non-stationary (random walk)\n", "axes[0, 0].plot(non_stationary, linewidth=1.5)\n", "axes[0, 0].set_title('Non-Stationary: Random Walk with Drift')\n", "axes[0, 0].set_ylabel('Value')\n", "axes[0, 0].grid(True, alpha=0.3)\n", "\n", "# Differences\n", "diff_non_stat = np.diff(non_stationary)\n", "axes[0, 1].plot(diff_non_stat, linewidth=1.5)\n", "axes[0, 1].set_title('Differenced (Random Walk) → Stationary')\n", "axes[0, 1].set_ylabel('Change')\n", "axes[0, 1].grid(True, alpha=0.3)\n", "\n", "# Row 2: Stationary\n", "axes[1, 0].plot(stationary, linewidth=1.5)\n", "axes[1, 0].set_title('Stationary: White Noise')\n", "axes[1, 0].set_ylabel('Value')\n", "axes[1, 0].grid(True, alpha=0.3)\n", "\n", "# Differences (shouldn't need)\n", "diff_stat = np.diff(stationary)\n", "axes[1, 1].plot(diff_stat, linewidth=1.5)\n", "axes[1, 1].set_title('Differenced (Already Stationary)')\n", "axes[1, 1].set_ylabel('Change')\n", "axes[1, 1].grid(True, alpha=0.3)\n", "\n", "# Row 3: Seasonal + trend\n", "axes[2, 0].plot(seasonal_trend, linewidth=1.5)\n", "axes[2, 0].set_title('Non-Stationary: Trend + Seasonality')\n", "axes[2, 0].set_ylabel('Value')\n", "axes[2, 0].grid(True, alpha=0.3)\n", "\n", "diff_seasonal = np.diff(seasonal_trend)\n", "axes[2, 1].plot(diff_seasonal, linewidth=1.5)\n", "axes[2, 1].set_title('Differenced: Removed Trend')\n", "axes[2, 1].set_ylabel('Change')\n", "axes[2, 1].grid(True, alpha=0.3)\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "# ADF test\n", "print(\"\\n\" + \"=\"*50)\n", "print(\"AUGMENTED DICKEY-FULLER (ADF) TEST\")\n", "print(\"=\"*50)\n", "\n", "print(\"\"\"\n", "HYPOTHESIS TEST FOR STATIONARITY:\n", "- H₀: Data is non-stationary (has unit root)\n", "- H₁: Data is stationary\n", "- If p < 0.05: Reject H₀ → Likely stationary\n", "- If p ≥ 0.05: Fail to reject → Likely non-stationary\n", "\n", "INTERPRETATION:\n", "- p < 0.05: Stationary (good for ARIMA)\n", "- p ≥ 0.05: Non-stationary (need to difference)\n", "\n", "WHEN TO USE:\n", "- First test on raw time series\n", "- If non-stationary: Difference once\n", "- Test again until stationary\n", "- Record number of differences needed (this is 'd' in ARIMA)\n", "\"\"\")\n", "\n", "print(\"\\nTesting example series:\")\n", "\n", "# Test non-stationary\n", "result_ns = adfuller(non_stationary)\n", "print(f\"\\nNon-stationary series (random walk):\")\n", "print(f\" ADF Statistic: {result_ns[0]:.4f}\")\n", "print(f\" P-value: {result_ns[1]:.4f}\")\n", "print(f\" Verdict: {'Stationary' if result_ns[1] < 0.05 else 'NON-STATIONARY'}\")\n", "\n", "# Test differences\n", "result_diff_ns = adfuller(diff_non_stat)\n", "print(f\"\\nAfter differencing:\")\n", "print(f\" ADF Statistic: {result_diff_ns[0]:.4f}\")\n", "print(f\" P-value: {result_diff_ns[1]:.4f}\")\n", "print(f\" Verdict: {'Stationary' if result_diff_ns[1] < 0.05 else 'NON-STATIONARY'}\")\n", "\n", "# Test stationary\n", "result_s = adfuller(stationary)\n", "print(f\"\\nStationary series (white noise):\")\n", "print(f\" ADF Statistic: {result_s[0]:.4f}\")\n", "print(f\" P-value: {result_s[1]:.4f}\")\n", "print(f\" Verdict: {'Stationary' if result_s[1] < 0.05 else 'NON-STATIONARY'}\")\n", "\n", "# Differencing guide\n", "print(\"\\n\" + \"=\"*50)\n", "print(\"DIFFERENCING STRATEGY\")\n", "print(\"=\"*50)\n", "\n", "print(\"\"\"\n", "DIFFERENCING:\n", "- Take difference between consecutive values\n", "- Y_diff = Y_t - Y_(t-1)\n", "- Removes trend from data\n", "- Usually makes non-stationary data stationary\n", "\n", "HOW TO KNOW HOW MANY DIFFERENCES NEEDED:\n", "1. Plot data: Is there trend?\n", "- If yes → Need differencing\n", "2. Apply 1st difference, test with ADF\n", "- If stationary (p < 0.05) → Done\n", "- If not → Apply 2nd difference\n", "3. Usually need 0-2 differences\n", "- 0: Already stationary\n", "- 1: Has trend\n", "- 2: Has trend in the trend (rare)\n", "\n", "WARNING: Over-differencing!\n", "- Too many differences → Creates false patterns\n", "- Lose information\n", "- Rule: Use least differences needed to achieve stationarity\n", "\n", "SEASONAL DIFFERENCING:\n", "- For seasonal data: Difference at seasonal lag\n", "- Example: Monthly data with yearly seasonality\n", "* Regular difference: Y_t - Y_(t-1)\n", "* Seasonal difference: Y_t - Y_(t-12)\n", "- Can combine both types\n", "- Usually need both for seasonal ARIMA\n", "\"\"\")\n", "\n", "print(\"\\nDifferencing example:\")\n", "print(f\"Original mean: {non_stationary.mean():.2f}\")\n", "print(f\"Original variance: {non_stationary.var():.2f}\")\n", "print(f\"\\nAfter 1st difference:\")\n", "print(f\"Differenced mean: {diff_non_stat.mean():.2f}\")\n", "print(f\"Differenced variance: {diff_non_stat.var():.2f}\")\n", "print(\"→ Mean now around 0 (drift removed)\")\n", "print(\"→ Likely stationary\")" ] }, { "cell_type": "markdown", "id": "ba5c80c8", "metadata": {}, "source": [ "## SESSION 2: Forecasting Methods - Exponential Smoothing and ARIMA" ] }, { "cell_type": "markdown", "id": "08b96e03", "metadata": {}, "source": [ "### Duration: 2 hours" ] }, { "cell_type": "markdown", "id": "a2d3ce2b", "metadata": {}, "source": [ "### 2.1 Exponential Smoothing Methods" ] }, { "cell_type": "code", "execution_count": null, "id": "1646cc18", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "from statsmodels.tsa.holtwinters import ExponentialSmoothing, SimpleExpSmoothing\n", "from sklearn.metrics import mean_squared_error, mean_absolute_error\n", "import matplotlib.pyplot as plt\n", "\n", "print(\"=\"*50)\n", "print(\"EXPONENTIAL SMOOTHING METHODS\")\n", "print(\"=\"*50)\n", "\n", "print(\"\"\"\n", "EXPONENTIAL SMOOTHING:\n", "- Family of forecasting methods\n", "- Give more weight to recent observations\n", "- Recent data trusted more than old data\n", "- Intuitive and easy to implement\n", "- Good for data without strong trend/seasonality\n", "\n", "TYPES:\n", "\n", "1. SIMPLE EXPONENTIAL SMOOTHING (SES)\n", "- For stationary data (no trend, no seasonality)\n", "- Works: Weighted average of past observations\n", "- Weights decay exponentially into past\n", "- Parameter α (0-1): smoothing parameter\n", "* α=0: Ignore new data\n", "* α=0.5: Equal weight to recent and past\n", "* α=1: Only use latest value (no smoothing)\n", "- Good when: Noise is main issue\n", "- Example: Stationary sales with noise\n", "\n", "2. HOLT'S LINEAR TREND METHOD\n", "- For data with trend (but no seasonality)\n", "- Tracks trend separately from level\n", "- Parameters: α (level), β (trend)\n", "- Forecasts into future: Includes trend\n", "- Good when: Clear upward or downward trend\n", "- Example: Growing sales\n", "\n", "3. HOLT-WINTERS SEASONAL METHOD\n", "- For data with both trend AND seasonality\n", "- Tracks: Level, Trend, Seasonal components\n", "- Two variants:\n", "* Additive: For constant seasonal variation\n", "* Multiplicative: For increasing seasonal variation\n", "- Parameters: α (level), β (trend), γ (seasonal)\n", "- Best overall for real-world data\n", "- Example: Retail sales (trend + holiday seasonality)\n", "\"\"\")\n", "\n", "# Create synthetic data with different characteristics\n", "np.random.seed(42)\n", "n = 200\n", "\n", "# 1. Stationary data\n", "stationary_data = 50 + np.random.normal(0, 3, n)\n", "\n", "# 2. Data with trend\n", "trend_data = 50 + 0.3 * np.arange(n) + np.random.normal(0, 3, n)\n", "\n", "# 3. Data with trend and seasonality\n", "t = np.arange(n)\n", "seasonal_data = (50 + 0.3 * t +\n", "10 * np.sin(2 * np.pi * t / 30) +\n", "np.random.normal(0, 2, n))\n", "\n", "# Fit models\n", "print(\"\\nFITTING MODELS TO DIFFERENT DATA TYPES:\")\n", "print(\"-\"*50)\n", "\n", "# Split into train/test\n", "train_size = int(0.8 * n)\n", "train_stat, test_stat = stationary_data[:train_size], stationary_data[train_size:]\n", "train_trend, test_trend = trend_data[:train_size], trend_data[train_size:]\n", "train_seas, test_seas = seasonal_data[:train_size], seasonal_data[train_size:]\n", "\n", "# 1. Simple Exponential Smoothing\n", "print(\"\\n1. Simple Exponential Smoothing (Stationary data):\")\n", "model_ses = SimpleExpSmoothing(train_stat).fit(smoothing_level=0.2)\n", "pred_ses = model_ses.forecast(steps=len(test_stat))\n", "rmse_ses = np.sqrt(mean_squared_error(test_stat, pred_ses))\n", "print(f\" RMSE: {rmse_ses:.2f}\")\n", "print(f\" Smoothing parameter (α): {model_ses.params['smoothing_level']:.3f}\")\n", "\n", "# 2. Holt's linear trend\n", "print(\"\\n2. Holt's Linear Trend (Trend data):\")\n", "from statsmodels.tsa.holtwinters import Holt\n", "model_holt = Holt(train_trend).fit()\n", "pred_holt = model_holt.forecast(steps=len(test_trend))\n", "rmse_holt = np.sqrt(mean_squared_error(test_trend, pred_holt))\n", "print(f\" RMSE: {rmse_holt:.2f}\")\n", "\n", "# 3. Holt-Winters\n", "print(\"\\n3. Holt-Winters (Trend + Seasonality):\")\n", "model_hw = ExponentialSmoothing(train_seas, seasonal_periods=30,\n", "trend='add', seasonal='add').fit()\n", "pred_hw = model_hw.forecast(steps=len(test_seas))\n", "rmse_hw = np.sqrt(mean_squared_error(test_seas, pred_hw))\n", "print(f\" RMSE: {rmse_hw:.2f}\")\n", "\n", "# Visualization\n", "fig, axes = plt.subplots(3, 1, figsize=(14, 12))\n", "\n", "# Plot 1: Stationary\n", "axes[0].plot(range(n), stationary_data, label='Actual', linewidth=1.5, alpha=0.7)\n", "axes[0].plot(range(train_size, n), pred_ses, label='SES Forecast',\n", "linewidth=2, color='red')\n", "axes[0].axvline(x=train_size, color='gray', linestyle='--', alpha=0.5)\n", "axes[0].set_title(f'Simple Exponential Smoothing (RMSE: {rmse_ses:.2f})')\n", "axes[0].set_ylabel('Value')\n", "axes[0].legend()\n", "axes[0].grid(True, alpha=0.3)\n", "\n", "# Plot 2: Trend\n", "axes[1].plot(range(n), trend_data, label='Actual', linewidth=1.5, alpha=0.7)\n", "axes[1].plot(range(train_size, n), pred_holt, label='Holt Forecast',\n", "linewidth=2, color='red')\n", "axes[1].axvline(x=train_size, color='gray', linestyle='--', alpha=0.5)\n", "axes[1].set_title(f\"Holt's Linear Trend (RMSE: {rmse_holt:.2f})\")\n", "axes[1].set_ylabel('Value')\n", "axes[1].legend()\n", "axes[1].grid(True, alpha=0.3)\n", "\n", "# Plot 3: Seasonal\n", "axes[2].plot(range(n), seasonal_data, label='Actual', linewidth=1.5, alpha=0.7)\n", "axes[2].plot(range(train_size, n), pred_hw, label='Holt-Winters Forecast',\n", "linewidth=2, color='red')\n", "axes[2].axvline(x=train_size, color='gray', linestyle='--', alpha=0.5)\n", "axes[2].set_title(f'Holt-Winters (RMSE: {rmse_hw:.2f})')\n", "axes[2].set_ylabel('Value')\n", "axes[2].legend()\n", "axes[2].grid(True, alpha=0.3)\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "print(\"\\n\" + \"=\"*50)\n", "print(\"CHOOSING EXPONENTIAL SMOOTHING METHOD\")\n", "print(\"=\"*50)\n", "\n", "print(\"\"\"\n", "DECISION GUIDE:\n", "\n", "1. Plot your data\n", "2. Check for trend:\n", "- No trend + no seasonality → Simple Exponential Smoothing\n", "- Trend present → Holt's Linear Trend\n", "- Trend + seasonality → Holt-Winters\n", "\n", "3. If using Holt-Winters:\n", "- Additive: Seasonal variation constant\n", "- Multiplicative: Seasonal variation increases with level\n", "\n", "ADVANTAGES:\n", "✓ Fast to implement\n", "✓ Good for short-term forecasts\n", "✓ Intuitive interpretation\n", "✓ Requires little data\n", "✓ Works well in practice\n", "\n", "DISADVANTAGES:\n", "✗ Limited to short-term (trend line extrapolated)\n", "✗ No uncertainty intervals (in simple form)\n", "✗ Can overfit with wrong parameters\n", "✗ Doesn't handle complex patterns\n", "\n", "WHEN TO USE:\n", "- Need quick forecast\n", "- Short time horizon (days/weeks)\n", "- Limited data\n", "- Trend and seasonality are stable\n", "\"\"\")" ] }, { "cell_type": "markdown", "id": "8477dae8", "metadata": {}, "source": [ "### 2.2 ARIMA Models" ] }, { "cell_type": "code", "execution_count": null, "id": "6dd7dbe8", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "from statsmodels.tsa.arima.model import ARIMA\n", "from statsmodels.graphics.tsaplots import plot_acf, plot_pacf\n", "from sklearn.metrics import mean_squared_error\n", "import matplotlib.pyplot as plt\n", "\n", "print(\"=\"*50)\n", "print(\"ARIMA MODELS\")\n", "print(\"=\"*50)\n", "\n", "print(\"\"\"\n", "ARIMA = AutoRegressive Integrated Moving Average\n", "\n", "COMPONENTS:\n", "\n", "1. AR (AutoRegressive): p\n", "- Use past values to predict future\n", "- Y_t = c + φ₁*Y_(t-1) + φ₂*Y_(t-2) + ... + ε\n", "- \"Regress on itself\"\n", "- p = how many past values to use\n", "\n", "2. I (Integrated): d\n", "- Number of differences to make stationary\n", "- d=0: Already stationary\n", "- d=1: One difference needed\n", "- d=2: Two differences needed (rare)\n", "\n", "3. MA (Moving Average): q\n", "- Use past forecast errors\n", "- Y_t = μ + ε_t + θ₁*ε_(t-1) + θ₂*ε_(t-2) + ...\n", "- Smooth out noise\n", "- q = how many past errors to use\n", "\n", "ARIMA(p,d,q) NOTATION:\n", "- (0,0,0): White noise (can't predict)\n", "- (1,0,0): AR(1) - depends on previous value\n", "- (0,1,0): Random walk - difference once to get constant\n", "- (0,0,1): MA(1) - depends on previous error\n", "- (1,1,1): Typical time series model\n", "\n", "HOW TO CHOOSE p, d, q:\n", "\n", "d (differencing):\n", "1. Plot data\n", "2. If trend: d=1\n", "3. If still non-stationary: d=2\n", "4. ADF test: Until p < 0.05\n", "\n", "p and q (from ACF/PACF plots):\n", "- ACF plot: Look for MA pattern (q)\n", "* Cuts off after lag q → MA(q)\n", "* Tails off → AR or ARMA\n", "\n", "- PACF plot: Look for AR pattern (p)\n", "* Cuts off after lag p → AR(p)\n", "* Tails off → MA or ARMA\n", "\n", "PRACTICAL APPROACH:\n", "1. Make data stationary (find d)\n", "2. Plot ACF/PACF on differenced data\n", "3. Try several (p,q) combinations\n", "4. Use AIC/BIC to choose best\n", "5. Check residuals (should be white noise)\n", "\"\"\")\n", "\n", "# Create example data\n", "np.random.seed(42)\n", "n = 200\n", "t = np.arange(n)\n", "\n", "# Non-stationary with trend\n", "data = 50 + 0.2*t + np.random.normal(0, 2, n)\n", "\n", "# Split\n", "train_size = int(0.8 * n)\n", "train, test = data[:train_size], data[train_size:]\n", "\n", "print(\"\\nFITTING ARIMA MODELS:\")\n", "print(\"-\"*50)\n", "\n", "# Try different ARIMA orders\n", "models_to_try = [(1,0,0), (1,1,0), (0,1,0), (1,1,1), (2,1,1)]\n", "\n", "results = []\n", "for order in models_to_try:\n", "try:\n", "model = ARIMA(train, order=order)\n", "fitted_model = model.fit()\n", "pred = fitted_model.forecast(steps=len(test))\n", "rmse = np.sqrt(mean_squared_error(test, pred))\n", "aic = fitted_model.aic\n", "\n", "results.append({\n", "'order': order,\n", "'rmse': rmse,\n", "'aic': aic\n", "})\n", "print(f\"ARIMA{order}: RMSE={rmse:.2f}, AIC={aic:.1f}\")\n", "except:\n", "print(f\"ARIMA{order}: Failed to fit\")\n", "\n", "# Find best model\n", "best_order = min(results, key=lambda x: x['aic'])['order']\n", "print(f\"\\nBest model: ARIMA{best_order}\")\n", "\n", "# Fit best model\n", "best_model = ARIMA(train, order=best_order).fit()\n", "best_pred = best_model.forecast(steps=len(test))\n", "best_rmse = np.sqrt(mean_squared_error(test, best_pred))\n", "\n", "print(f\"Best model RMSE: {best_rmse:.2f}\")\n", "print(f\"\\nModel Summary:\")\n", "print(best_model.summary())\n", "\n", "# Visualization\n", "fig, axes = plt.subplots(2, 2, figsize=(14, 10))\n", "\n", "# Plot 1: Original data and forecast\n", "axes[0, 0].plot(range(n), data, label='Actual', linewidth=1.5, alpha=0.7)\n", "axes[0, 0].plot(range(train_size, n), best_pred, label=f'ARIMA{best_order} Forecast',\n", "linewidth=2, color='red')\n", "axes[0, 0].axvline(x=train_size, color='gray', linestyle='--', alpha=0.5)\n", "axes[0, 0].set_title(f'ARIMA Model Forecast (RMSE: {best_rmse:.2f})')\n", "axes[0, 0].set_ylabel('Value')\n", "axes[0, 0].legend()\n", "axes[0, 0].grid(True, alpha=0.3)\n", "\n", "# Plot 2: ACF of original data\n", "plot_acf(train, lags=30, ax=axes[0, 1])\n", "axes[0, 1].set_title('ACF of Original Data')\n", "\n", "# Plot 3: Residuals\n", "residuals = best_model.resid\n", "axes[1, 0].plot(residuals, linewidth=1)\n", "axes[1, 0].axhline(y=0, color='red', linestyle='--')\n", "axes[1, 0].set_title('Residuals (Should be White Noise)')\n", "axes[1, 0].set_ylabel('Residual')\n", "axes[1, 0].grid(True, alpha=0.3)\n", "\n", "# Plot 4: Residual distribution\n", "axes[1, 1].hist(residuals, bins=20, edgecolor='black')\n", "axes[1, 1].set_title('Residual Distribution')\n", "axes[1, 1].set_xlabel('Residual')\n", "axes[1, 1].grid(True, alpha=0.3, axis='y')\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "print(\"\\n\" + \"=\"*50)\n", "print(\"CHECKING MODEL ADEQUACY\")\n", "print(\"=\"*50)\n", "\n", "print(\"\"\"\n", "RESIDUAL DIAGNOSTICS:\n", "After fitting ARIMA, check residuals:\n", "\n", "1. SHOULD LOOK LIKE WHITE NOISE:\n", "- Mean = 0\n", "- Constant variance\n", "- No patterns\n", "- Normally distributed\n", "\n", "2. ACF OF RESIDUALS:\n", "- Should not have significant spikes\n", "- If spikes → Model missing patterns\n", "- Solution: Try different (p,d,q)\n", "\n", "3. LJUNG-BOX TEST:\n", "- Tests if residuals are independent\n", "- H₀: Residuals are white noise\n", "- p > 0.05: Good (residuals are white noise)\n", "- p < 0.05: Bad (model missing something)\n", "\n", "4. FORECAST ERRORS:\n", "- Directional accuracy: % of correct up/down\n", "- Magnitude errors: RMSE, MAE\n", "- Forecast bias: Mean of errors\n", "\n", "IF DIAGNOSTICS BAD:\n", "→ Try different (p,d,q)\n", "→ Check for outliers\n", "→ Consider other methods\n", "→ May have structural breaks" ] }, { "cell_type": "markdown", "id": "4b64d968", "metadata": {}, "source": [ "## SESSION 3: Advanced Methods and Model Evaluation" ] }, { "cell_type": "markdown", "id": "536c0323", "metadata": {}, "source": [ "### Duration: 2 hours" ] }, { "cell_type": "markdown", "id": "e4d7220c", "metadata": {}, "source": [ "### 3.1 SARIMA and Seasonal Models" ] }, { "cell_type": "code", "execution_count": null, "id": "d452d699", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "from statsmodels.tsa.statespace.sarimax import SARIMAX\n", "from sklearn.metrics import mean_squared_error\n", "import matplotlib.pyplot as plt\n", "\n", "print(\"=\"*50)\n", "print(\"SARIMA - SEASONAL ARIMA\")\n", "print(\"=\"*50)\n", "\n", "print(\"\"\"\n", "SARIMA(p,d,q)(P,D,Q,s):\n", "- ARIMA + SEASONAL components\n", "- For data with clear seasonal patterns\n", "\n", "PARAMETERS:\n", "(p,d,q): Non-seasonal ARIMA (same as ARIMA)\n", "(P,D,Q,s): Seasonal ARIMA\n", "- P: Seasonal AR order\n", "- D: Seasonal differencing\n", "- Q: Seasonal MA order\n", "- s: Seasonal period (frequency of pattern)\n", "\n", "Examples:\n", "- Monthly data, yearly seasonality: s=12\n", "- Daily data, weekly seasonality: s=7\n", "- Quarterly data, yearly seasonality: s=4\n", "\n", "WHEN TO USE:\n", "- Clear seasonal patterns visible\n", "- ACF/PACF show spikes at seasonal lags\n", "- Holt-Winters worked but want ARIMA\n", "- Need probabilistic forecasts\n", "\n", "CHOOSING PARAMETERS:\n", "1. Determine seasonal period (s)\n", "- Look at data\n", "- Auto-detect with autocorrelation\n", "\n", "2. Regular differencing (d)\n", "- Until trend removed\n", "\n", "3. Seasonal differencing (D)\n", "- Usually 0 or 1\n", "\n", "4. Seasonal AR/MA (P, Q)\n", "- From ACF/PACF at seasonal lags\n", "\n", "5. Non-seasonal AR/MA (p, q)\n", "- From ACF/PACF of differenced data\n", "\n", "EXAMPLE: Electricity usage\n", "- Daily data (365 days/year)\n", "- Strong weekly seasonality (7 days)\n", "- Gradual growth trend\n", "→ SARIMA(1,1,1)(1,0,1,7)\n", "→ Try auto_arima()\n", "\"\"\")\n", "\n", "# Create seasonal data\n", "np.random.seed(42)\n", "n = 365\n", "t = np.arange(n)\n", "\n", "# Seasonal pattern (weekly)\n", "seasonal = 5 * np.sin(2 * np.pi * t / 7)\n", "\n", "# Trend\n", "trend = 0.01 * t\n", "\n", "# Noise\n", "noise = np.random.normal(0, 1, n)\n", "\n", "# Combined\n", "data_seasonal = 50 + trend + seasonal + noise\n", "\n", "# Split\n", "train_size = int(0.8 * n)\n", "train_s, test_s = data_seasonal[:train_size], data_seasonal[train_size:]\n", "\n", "print(\"\\nFITTING SARIMA MODELS:\")\n", "print(\"-\"*50)\n", "\n", "# Fit SARIMA\n", "model_sarima = SARIMAX(train_s, order=(1,1,1), seasonal_order=(1,0,1,7))\n", "fitted_sarima = model_sarima.fit(disp=False)\n", "\n", "pred_sarima = fitted_sarima.forecast(steps=len(test_s))\n", "rmse_sarima = np.sqrt(mean_squared_error(test_s, pred_sarima))\n", "\n", "print(f\"SARIMA(1,1,1)(1,0,1,7):\")\n", "print(f\" RMSE: {rmse_sarima:.2f}\")\n", "\n", "# Get confidence intervals\n", "pred_sarima_ci = fitted_sarima.get_forecast(steps=len(test_s))\n", "pred_ci = pred_sarima_ci.conf_int()\n", "\n", "# Visualization\n", "fig, axes = plt.subplots(2, 1, figsize=(14, 10))\n", "\n", "# Plot 1: Forecast with confidence intervals\n", "axes[0].plot(range(n), data_seasonal, label='Actual', linewidth=1.5, alpha=0.7)\n", "axes[0].plot(range(train_size, n), pred_sarima, label='SARIMA Forecast',\n", "linewidth=2, color='red')\n", "axes[0].fill_between(range(train_size, n),\n", "pred_ci.iloc[:, 0], pred_ci.iloc[:, 1],\n", "alpha=0.2, color='red', label='95% CI')\n", "axes[0].axvline(x=train_size, color='gray', linestyle='--', alpha=0.5)\n", "axes[0].set_title(f'SARIMA Forecast with Confidence Intervals (RMSE: {rmse_sarima:.2f})')\n", "axes[0].set_ylabel('Value')\n", "axes[0].legend()\n", "axes[0].grid(True, alpha=0.3)\n", "\n", "# Plot 2: Residuals\n", "residuals_s = fitted_sarima.resid\n", "axes[1].plot(residuals_s, linewidth=1)\n", "axes[1].axhline(y=0, color='red', linestyle='--')\n", "axes[1].set_title('SARIMA Residuals')\n", "axes[1].set_xlabel('Time')\n", "axes[1].set_ylabel('Residual')\n", "axes[1].grid(True, alpha=0.3)\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "print(\"\\nAUTO-ARIMA:\")\n", "print(\"-\"*50)\n", "\n", "print(\"\"\"\n", "AUTO_ARIMA: Automated parameter selection\n", "\n", "Advantages:\n", "✓ No need to manually choose p,d,q,P,D,Q\n", "✓ Tests stationarity\n", "✓ Searches parameter space\n", "✓ Returns best model by AIC/BIC\n", "\n", "Disadvantages:\n", "✗ Black box (you don't understand choice)\n", "✗ May be slow with large data\n", "✗ Not always best (automatic choices)\n", "\n", "Usage:\n", "from statsmodels.tsa.arima.auto_arima import auto_arima\n", "auto_model = auto_arima(data, seasonal=True, m=7)\n", "\n", "Common approach:\n", "1. Try auto_arima for quick baseline\n", "2. Manually tune based on ACF/PACF\n", "3. Compare models\n", "4. Choose best by validation\n", "\"\"\")" ] }, { "cell_type": "markdown", "id": "9cdd3d2e", "metadata": {}, "source": [ "### 3.2 Model Evaluation and Forecasting Best Practices" ] }, { "cell_type": "code", "execution_count": null, "id": "c5963cb8", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "from sklearn.metrics import mean_squared_error, mean_absolute_error, mean_absolute_percentage_error\n", "import matplotlib.pyplot as plt\n", "\n", "print(\"=\"*50)\n", "print(\"EVALUATING TIME SERIES FORECASTS\")\n", "print(\"=\"*50)\n", "\n", "print(\"\"\"\n", "EVALUATION METRICS:\n", "\n", "1. RMSE (Root Mean Squared Error)\n", "- √(Σ(actual - predicted)² / n)\n", "- Units: Same as original data\n", "- Penalizes large errors more\n", "- Range: 0 to ∞ (lower better)\n", "- Use: When all errors equally important\n", "\n", "2. MAE (Mean Absolute Error)\n", "- Average absolute error\n", "- More robust to outliers\n", "- Easy to interpret (average error)\n", "- Use: When few large errors shouldn't dominate\n", "\n", "3. MAPE (Mean Absolute Percentage Error)\n", "- Average percentage error\n", "- |actual - predicted| / |actual|\n", "- Scale-independent\n", "- Good for comparing across different scales\n", "- Use: For relative accuracy\n", "\n", "4. THEIL'S U STATISTIC\n", "- Compares naive forecast to model\n", "- U < 1: Model better than naive\n", "- U = 1: Model = naive\n", "- U > 1: Naive better\n", "\n", "5. DIRECTIONAL ACCURACY\n", "- % of time model predicts correct direction\n", "- Did price go up/down correctly?\n", "- Range: 0-100%\n", "- Business metric (catches trends)\n", "\n", "CHOOSING METRIC:\n", "- RMSE: Default, penalizes large errors\n", "- MAE: Robust, easy to explain\n", "- MAPE: Compare across different products\n", "- Directional: For trading/investment\n", "\"\"\")\n", "\n", "# Create comparison data\n", "np.random.seed(42)\n", "n = 100\n", "actual = 50 + 0.1*np.arange(n) + 10*np.sin(np.arange(n)/10) + np.random.normal(0,2,n)\n", "\n", "# Three forecasts (naive, simple model, good model)\n", "naive_forecast = np.concatenate([[actual[0]], actual[:-1]]) # Previous value\n", "\n", "simple_forecast = actual.mean() + 10*np.sin(np.arange(n)/10) # Only seasonal\n", "\n", "good_forecast = actual + np.random.normal(0,0.5,n) # Near perfect\n", "\n", "# Calculate metrics\n", "def calc_metrics(actual, pred):\n", "rmse = np.sqrt(mean_squared_error(actual, pred))\n", "mae = mean_absolute_error(actual, pred)\n", "mape = mean_absolute_percentage_error(actual, pred)\n", "\n", "# Directional accuracy\n", "actual_direction = np.sign(np.diff(actual))\n", "pred_direction = np.sign(np.diff(pred))\n", "dir_acc = np.mean(actual_direction == pred_direction) * 100\n", "\n", "return {'RMSE': rmse, 'MAE': mae, 'MAPE': mape, 'Dir. Acc': dir_acc}\n", "\n", "print(\"\\nMETRIC COMPARISON:\")\n", "print(\"-\"*50)\n", "\n", "metrics_naive = calc_metrics(actual, naive_forecast)\n", "metrics_simple = calc_metrics(actual, simple_forecast)\n", "metrics_good = calc_metrics(actual, good_forecast)\n", "\n", "print(\"\\nNaive (previous value):\")\n", "for k, v in metrics_naive.items():\n", "print(f\" {k}: {v:.2f}\")\n", "\n", "print(\"\\nSimple (seasonal only):\")\n", "for k, v in metrics_simple.items():\n", "print(f\" {k}: {v:.2f}\")\n", "\n", "print(\"\\nGood (near actual):\")\n", "for k, v in metrics_good.items():\n", "print(f\" {k}: {v:.2f}\")\n", "\n", "# Visualization\n", "fig, axes = plt.subplots(2, 2, figsize=(14, 10))\n", "\n", "# Plot 1: Forecasts comparison\n", "axes[0, 0].plot(actual, label='Actual', linewidth=2)\n", "axes[0, 0].plot(naive_forecast, label='Naive', alpha=0.7, linewidth=1.5)\n", "axes[0, 0].plot(simple_forecast, label='Simple', alpha=0.7, linewidth=1.5)\n", "axes[0, 0].plot(good_forecast, label='Good', alpha=0.7, linewidth=1.5)\n", "axes[0, 0].set_title('Forecast Comparison')\n", "axes[0, 0].set_ylabel('Value')\n", "axes[0, 0].legend()\n", "axes[0, 0].grid(True, alpha=0.3)\n", "\n", "# Plot 2: Errors by method\n", "errors = {\n", "'Naive': np.abs(actual - naive_forecast),\n", "'Simple': np.abs(actual - simple_forecast),\n", "'Good': np.abs(actual - good_forecast)\n", "}\n", "\n", "axes[0, 1].boxplot([errors['Naive'], errors['Simple'], errors['Good']],\n", "labels=['Naive', 'Simple', 'Good'])\n", "axes[0, 1].set_title('Forecast Errors Distribution')\n", "axes[0, 1].set_ylabel('Absolute Error')\n", "axes[0, 1].grid(True, alpha=0.3, axis='y')\n", "\n", "# Plot 3: Error over time (Naive)\n", "axes[1, 0].plot(np.abs(actual - naive_forecast), label='Naive', linewidth=1.5)\n", "axes[1, 0].plot(np.abs(actual - good_forecast), label='Good', linewidth=1.5)\n", "axes[1, 0].set_title('Forecast Error Over Time')\n", "axes[1, 0].set_ylabel('Absolute Error')\n", "axes[1, 0].set_xlabel('Time')\n", "axes[1, 0].legend()\n", "axes[1, 0].grid(True, alpha=0.3)\n", "\n", "# Plot 4: Scatter of actual vs predicted\n", "axes[1, 1].scatter(actual, good_forecast, alpha=0.6)\n", "axes[1, 1].plot([actual.min(), actual.max()],\n", "[actual.min(), actual.max()], 'r--', label='Perfect')\n", "axes[1, 1].set_xlabel('Actual')\n", "axes[1, 1].set_ylabel('Predicted')\n", "axes[1, 1].set_title('Actual vs Predicted (Good Model)')\n", "axes[1, 1].legend()\n", "axes[1, 1].grid(True, alpha=0.3)\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "print(\"\\n\" + \"=\"*50)\n", "print(\"TIME SERIES FORECASTING BEST PRACTICES\")\n", "print(\"=\"*50)\n", "\n", "print(\"\"\"\n", "1. PROPER DATA SPLITTING\n", "✓ Use time-based split (not random)\n", "✓ Train set: Earlier data\n", "✓ Test set: Later data\n", "✓ Validate on future data\n", "✗ Don't: Random shuffle, look ahead\n", "\n", "2. HANDLE MISSING DATA\n", "✓ Identify missing values\n", "✓ Interpolate if small gaps\n", "✓ Forward/backward fill if needed\n", "✓ Document approach\n", "✗ Don't: Ignore missing values\n", "\n", "3. OUTLIERS\n", "✓ Identify extreme values\n", "✓ Investigate: Errors or real events?\n", "✓ If errors: Correct or remove\n", "✓ If real: Keep (important for model)\n", "✗ Don't: Blindly remove outliers\n", "\n", "4. EVALUATE ON MULTIPLE METRICS\n", "✓ Use RMSE, MAE, MAPE, directional\n", "✓ Plot actual vs predicted\n", "✓ Check residuals (white noise)\n", "✓ Validate on unseen test data\n", "✗ Don't: Only look at one metric\n", "\n", "5. FORECAST UNCERTAINTY\n", "✓ Report confidence intervals\n", "✓ Further future = wider intervals\n", "✓ Consider prediction intervals (not just CI)\n", "✗ Don't: Report single point forecast\n", "\n", "6. MODEL COMPARISON\n", "✓ Try multiple methods\n", "✓ Simple baseline (naive, mean)\n", "✓ Linear models (exponential smoothing)\n", "✓ ARIMA for complex patterns\n", "✓ Machine learning if good predictors\n", "\n", "7. AVOID COMMON PITFALLS\n", "✗ DATA LEAKAGE: Using future info\n", "✗ OVERFITTING: Too complex for amount of data\n", "✗ NON-STATIONARITY: Not differencing\n", "✗ SEASONAL IGNORANCE: Ignoring seasonality\n", "✗ HORIZON: Forecasting too far into future\n", "\n", "8. MONITORING IN PRODUCTION\n", "✓ Track forecast accuracy over time\n", "✓ Detect performance degradation\n", "✓ Retrain with new data regularly\n", "✓ Adapt to structural changes\n", "✓ Update when accuracy drops\n", "\n", "WORKFLOW:\n", "1. Explore data → Visualize trends, seasonality\n", "2. Make stationary → Differencing, transformations\n", "3. Identify ARIMA parameters → ACF/PACF, ADF test\n", "4. Fit model → Train on historical data\n", "5. Validate → Evaluate on test data\n", "6. Check residuals → Diagnostics\n", "7. Forecast future → With uncertainty intervals\n", "8. Monitor → Track performance, retrain as needed\n", "\"\"\")" ] }, { "cell_type": "markdown", "id": "4140be64", "metadata": {}, "source": [ "## Week 11 Summary\n", "By completing Week 11, you have learned:\n", "- Time series data: characteristics and why it's different\n", "- Time series components: trend, seasonality, cycles, noise\n", "- Autocorrelation (ACF) and partial autocorrelation (PACF)\n", "- Stationarity: concept and importance\n", "- Augmented Dickey-Fuller (ADF) test for stationarity\n", "- Differencing: making data stationary\n", "- Seasonal differencing for seasonal data\n", "- Exponential Smoothing methods: Simple, Holt's, Holt-Winters\n", "- Choosing between additive and multiplicative seasonality\n", "- ARIMA models: AutoRegressive Integrated Moving Average\n", "- Identifying ARIMA parameters from ACF/PACF plots\n", "- SARIMA: Seasonal ARIMA for seasonal data\n", "- Auto-ARIMA: Automated parameter selection\n", "- Forecast evaluation metrics: RMSE, MAE, MAPE\n", "- Residual diagnostics and model checking\n", "- Confidence intervals and prediction uncertainty\n", "- Time series best practices and common pitfalls\n", "- Handling missing data and outliers in time series\n", "- Monitoring and retraining forecasts\n", "- Real-world time series applications" ] }, { "cell_type": "markdown", "id": "6c8a1aef", "metadata": {}, "source": [ "## Week 11 Assignments" ] }, { "cell_type": "markdown", "id": "6171fa4f", "metadata": {}, "source": [ "### Assignment 1: Time Series Exploration and Stationarity\n", "Complete analysis of time series dataset:\n", "- Load and visualize time series data\n", "- Identify components: trend, seasonality, noise\n", "- Plot ACF and PACF\n", "- Perform Augmented Dickey-Fuller test\n", "- Apply differencing until stationary\n", "- Document findings\n", "- Create visualizations for each step\n", "- Explain when stationarity is needed" ] }, { "cell_type": "markdown", "id": "e9f5260c", "metadata": {}, "source": [ "### Assignment 2: Building and Comparing Forecast Models\n", "Develop multiple forecasting approaches:\n", "- Split data into train/test sets (time-based)\n", "- Implement exponential smoothing methods\n", "- Build ARIMA models (manually and with auto_arima)\n", "- Build SARIMA model if seasonality present\n", "- Evaluate each model on test set\n", "- Use multiple metrics (RMSE, MAE, MAPE)\n", "- Create comparison visualizations\n", "- Select and justify best model\n", "- Generate forecasts with confidence intervals" ] }, { "cell_type": "markdown", "id": "e171f95f", "metadata": {}, "source": [ "### Assignment 3: Real-World Time Series Forecasting Project\n", "End-to-end forecasting project:\n", "- Find real dataset (stock prices, weather, sales, etc.)\n", "- Exploratory analysis with visualizations\n", "- Test and achieve stationarity\n", "- Identify patterns (ACF/PACF)\n", "- Build multiple models (exponential smoothing, ARIMA, SARIMA)\n", "- Validate on test data\n", "- Check residual diagnostics\n", "- Generate future forecasts\n", "- Report uncertainty (confidence intervals)\n", "- Compare to baseline (naive forecast)\n", "- Professional report with interpretation" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.11" } }, "nbformat": 4, "nbformat_minor": 5 }