{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# default_exp models.naive2.naive2"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#hide\n",
    "%load_ext autoreload\n",
    "%autoreload 2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Naive2 model\n",
    "> Common benchmark model."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "import numpy as np\n",
    "from numpy.random import seed\n",
    "seed(1)\n",
    "\n",
    "import pandas as pd\n",
    "from math import sqrt"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Naive Benchmark Model\n",
    "\n",
    "> API details."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "class Naive:\n",
    "    \"\"\"\n",
    "    Naive model.\n",
    "    This benchmark model produces a forecast that is equal to\n",
    "    the last observed value for a given time series.\n",
    "    \"\"\"\n",
    "    def __init__(self):\n",
    "        pass\n",
    "    \n",
    "    def fit(self, ts_init):\n",
    "        \"\"\"\n",
    "        ts_init: the original time series\n",
    "        ts_naive: last observations of time series\n",
    "        \"\"\"\n",
    "        self.ts_naive = [ts_init[-1]]\n",
    "        return self\n",
    "\n",
    "    def predict(self, h):\n",
    "        return np.array(self.ts_naive * h)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Seasonal Naive Benchmark Model\n",
    "\n",
    "> API details."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "class SeasonalNaive:\n",
    "    \"\"\"\n",
    "    Seasonal Naive model.\n",
    "    This benchmark model produces a forecast that is equal to\n",
    "    the last observed value of the same season for a given time \n",
    "    series.\n",
    "    \"\"\"\n",
    "    def __init__(self):\n",
    "        pass\n",
    "    \n",
    "    def fit(self, ts_init, seasonality):\n",
    "        \"\"\"\n",
    "        ts_init: the original time series\n",
    "        frcy: frequency of the time series\n",
    "        ts_naive: last observations of time series\n",
    "        \"\"\"\n",
    "        self.ts_seasonal_naive = ts_init[-seasonality:]\n",
    "        return self\n",
    "\n",
    "    def predict(self, h):\n",
    "        repetitions = int(np.ceil(h/len(self.ts_seasonal_naive)))\n",
    "        y_hat = np.tile(self.ts_seasonal_naive, reps=repetitions)[:h]\n",
    "        return y_hat"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Naive2 Benchmark Model\n",
    "\n",
    "> API details.\n",
    "\n",
    "The Naive2 benchmark model, as explained in the [M4 guide](https://www.m4.unic.ac.cy/wp-content/uploads/2018/03/M4-Competitors-Guide.pdf) is determined by the following process:\n",
    "- Test the time-series for seasonal patterns by performing a 90% auto-correlation.\n",
    "- Based on the test select between a Naive benchmark (random walk) or a spline detrended seasonal forecast."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#export\n",
    "def detrend(insample_data):\n",
    "    \"\"\"\n",
    "    Calculates a & b parameters of LRL\n",
    "    :param insample_data:\n",
    "    :return:\n",
    "    \"\"\"\n",
    "    x = np.arange(len(insample_data))\n",
    "    a, b = np.polyfit(x, insample_data, 1)\n",
    "    return a, b\n",
    "\n",
    "def deseasonalize(original_ts, ppy):\n",
    "    \"\"\"\n",
    "    Calculates and returns seasonal indices\n",
    "    :param original_ts: original data\n",
    "    :param ppy: periods per year\n",
    "    :return:\n",
    "    \"\"\"\n",
    "    \"\"\"\n",
    "    # === get in-sample data\n",
    "    original_ts = original_ts[:-out_of_sample]\n",
    "    \"\"\"\n",
    "    if seasonality_test(original_ts, ppy):\n",
    "        # ==== get moving averages\n",
    "        ma_ts = moving_averages(original_ts, ppy)\n",
    "\n",
    "        # ==== get seasonality indices\n",
    "        le_ts = original_ts * 100 / ma_ts\n",
    "        le_ts = np.hstack((le_ts, np.full((ppy - (len(le_ts) % ppy)), np.nan)))\n",
    "        le_ts = np.reshape(le_ts, (-1, ppy))\n",
    "        si = np.nanmean(le_ts, 0)\n",
    "        norm = np.sum(si) / (ppy * 100)\n",
    "        si = si / norm\n",
    "    else:\n",
    "        si = np.ones(ppy)\n",
    "\n",
    "    return si\n",
    "\n",
    "def moving_averages(ts_init, window):\n",
    "    \"\"\"\n",
    "    Calculates the moving averages for a given TS\n",
    "    :param ts_init: the original time series\n",
    "    :param window: window length\n",
    "    :return: moving averages ts\n",
    "    \"\"\"\n",
    "    \"\"\"\n",
    "    As noted by Professor Isidro Lloret Galiana:\n",
    "    line 82:\n",
    "    if len(ts_init) % 2 == 0:\n",
    "    \n",
    "    should be changed to\n",
    "    if window % 2 == 0:\n",
    "    \n",
    "    This change has a minor (less then 0.05%) impact on the calculations of the seasonal indices\n",
    "    In order for the results to be fully replicable this change is not incorporated into the code below\n",
    "    \"\"\"\n",
    "    ts_init = pd.Series(ts_init)\n",
    "    \n",
    "    if len(ts_init) % 2 == 0:\n",
    "        ts_ma = ts_init.rolling(window, center=True).mean()\n",
    "        ts_ma = ts_ma.rolling(2, center=True).mean()\n",
    "        ts_ma = np.roll(ts_ma, -1)\n",
    "    else:\n",
    "        ts_ma = ts_init.rolling(window, center=True).mean()\n",
    "\n",
    "    return ts_ma\n",
    "\n",
    "def seasonality_test(original_ts, ppy):\n",
    "    \"\"\"\n",
    "    Seasonality test\n",
    "    :param original_ts: time series\n",
    "    :param ppy: periods per year\n",
    "    :return: boolean value: whether the TS is seasonal\n",
    "    \"\"\"\n",
    "    s = acf(original_ts, 1)\n",
    "    for i in range(2, ppy):\n",
    "        s = s + (acf(original_ts, i) ** 2)\n",
    "\n",
    "    limit = 1.645 * (sqrt((1 + 2 * s) / len(original_ts)))\n",
    "\n",
    "    return (abs(acf(original_ts, ppy))) > limit\n",
    "\n",
    "def acf(data, k):\n",
    "    \"\"\"\n",
    "    Autocorrelation function\n",
    "    :param data: time series\n",
    "    :param k: lag\n",
    "    :return:\n",
    "    \"\"\"\n",
    "    m = np.mean(data)\n",
    "    s1 = 0\n",
    "    for i in range(k, len(data)):\n",
    "        s1 = s1 + ((data[i] - m) * (data[i - k] - m))\n",
    "\n",
    "    s2 = 0\n",
    "    for i in range(0, len(data)):\n",
    "        s2 = s2 + ((data[i] - m) ** 2)\n",
    "\n",
    "    return float(s1 / s2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# export\n",
    "class Naive2:\n",
    "    \"\"\"\n",
    "    Naive2 model.\n",
    "    Popular benchmark model for time series forecasting that automatically adapts\n",
    "    to the potential seasonality of a series based on an autocorrelation test.\n",
    "    If the series is seasonal the model composes the predictions of Naive and SeasonalNaive,\n",
    "    else the model predicts on the simple Naive.\n",
    "    \"\"\"\n",
    "    def __init__(self, seasonality):\n",
    "        self.seasonality = seasonality\n",
    "        \n",
    "    def fit(self, ts_init):\n",
    "        seasonality_in = deseasonalize(ts_init, ppy=self.seasonality)\n",
    "        windows = int(np.ceil(len(ts_init) / self.seasonality))\n",
    "        \n",
    "        self.ts_init = ts_init\n",
    "        self.s_hat = np.tile(seasonality_in, reps=windows)[:len(ts_init)]\n",
    "        self.ts_des = ts_init / self.s_hat\n",
    "                        \n",
    "        return self\n",
    "        \n",
    "    def predict(self, h):\n",
    "        s_hat = SeasonalNaive().fit(self.s_hat, \n",
    "                                    seasonality=self.seasonality).predict(h)\n",
    "        r_hat = Naive().fit(self.ts_des).predict(h)              \n",
    "        y_hat = s_hat * r_hat\n",
    "        return y_hat"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Naive Benchmark Usage Example"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "100%|██████████| 2.12M/2.12M [00:00<00:00, 3.30MiB/s]\n",
      "INFO:nixtla.data.datasets.utils:Successfully downloaded NP.csv, 2118914, bytes.\n",
      "100%|██████████| 2.53M/2.53M [00:00<00:00, 3.46MiB/s]\n",
      "INFO:nixtla.data.datasets.utils:Successfully downloaded PJM.csv, 2530311, bytes.\n",
      "100%|██████████| 1.97M/1.97M [00:00<00:00, 2.74MiB/s]\n",
      "INFO:nixtla.data.datasets.utils:Successfully downloaded BE.csv, 1974555, bytes.\n",
      "100%|██████████| 2.00M/2.00M [00:00<00:00, 3.15MiB/s]\n",
      "INFO:nixtla.data.datasets.utils:Successfully downloaded FR.csv, 1996871, bytes.\n",
      "100%|██████████| 2.10M/2.10M [00:00<00:00, 2.82MiB/s]\n",
      "INFO:nixtla.data.datasets.utils:Successfully downloaded DE.csv, 2095831, bytes.\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 55240 characters]\n",
      "text/plain": [
       "<Figure size 1080x432 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import pandas as pd\n",
    "from nixtlats.data.datasets.epf import EPF, EPFInfo\n",
    "from nixtlats.data.tsloader import TimeSeriesLoader\n",
    "\n",
    "import pylab as plt\n",
    "from pylab import rcParams\n",
    "plt.style.use('seaborn-whitegrid')\n",
    "plt.rcParams['font.family'] = 'serif'\n",
    "\n",
    "FONTSIZE = 17\n",
    "\n",
    "# Load and plot data\n",
    "Y_df, X_df, S_df = EPF.load_groups(directory='./data', groups=['NP'])\n",
    "\n",
    "fig = plt.figure(figsize=(15, 6))\n",
    "plt.plot(Y_df.ds, Y_df.y.values, color='#628793', linewidth=0.4)\n",
    "plt.ylabel('Price [EUR/MWh]', fontsize=15)\n",
    "plt.xlabel('Date', fontsize=15)\n",
    "# plt.savefig('./results/NP.png', bbox_inches = 'tight')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from itertools import chain\n",
    "from multiprocessing import Pool\n",
    "from numpy.lib.stride_tricks import sliding_window_view\n",
    "from nixtlats.losses.numpy import mae\n",
    "\n",
    "def fit_predict_naive2(Y, output_size=24, seasonality=24):\n",
    "    y_hat_list = []\n",
    "    for idx in range(len(Y)):\n",
    "        y_id = Y[idx, :]\n",
    "        y_hat_id = Naive2(seasonality).fit(y_id).predict(output_size)\n",
    "        y_hat_list += [y_hat_id]\n",
    "    return y_hat_list\n",
    "\n",
    "def parallelized_naive2(Y, n_cores=32):\n",
    "    # Y\n",
    "    n_partitions = n_cores**3//4  # number of partitions to split the array\n",
    "    Y_split  = np.array_split(Y, n_partitions)\n",
    "    pool = Pool(n_cores)\n",
    "    Y_hat_list = list(chain.from_iterable(pool.map(fit_predict_naive2, Y_split)))\n",
    "    pool.close()\n",
    "    pool.join()\n",
    "    Y_hat = np.vstack(Y_hat_list)\n",
    "    return Y_hat"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Y_windows.shape (2178, 168)\n"
     ]
    }
   ],
   "source": [
    "Y_windows = sliding_window_view(Y_df.y.values, window_shape=24*7)\n",
    "day_idxs = (np.arange(len(Y_windows)) % 24) == 0\n",
    "Y_windows = Y_windows[day_idxs, :]\n",
    "print(\"Y_windows.shape\", Y_windows.shape)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(52272,)"
      ]
     },
     "execution_count": null,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Y_hat = parallelized_naive2(Y=Y_windows)\n",
    "Y_hat = Y_hat.reshape(-1)\n",
    "Y_hat.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 67448 characters]\n",
      "text/plain": [
       "<Figure size 1080x432 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "fig = plt.figure(figsize=(15, 6))\n",
    "plt.plot(Y_df.ds[168:], Y_df.y.values[168:], color='#628793', linewidth=0.4, label='true')\n",
    "plt.plot(Y_df.ds[168:], Y_hat[:len(Y_hat)-24], color='peru', linewidth=0.4, label='forecast')\n",
    "plt.ylabel('Price [EUR/MWh]', fontsize=15)\n",
    "plt.xlabel('Date', fontsize=15)\n",
    "plt.legend()\n",
    "# plt.savefig('./results/NP.png', bbox_inches = 'tight')\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "MAE: 2.5901857898968275\n"
     ]
    }
   ],
   "source": [
    "naive2_mae = mae(y=Y_df.y.values[168:], y_hat=Y_hat[:len(Y_hat)-24])\n",
    "print(f'MAE: {naive2_mae}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "nixtla",
   "language": "python",
   "name": "nixtla"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
