{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "Copyright (c) Microsoft Corporation. All rights reserved.\n", "\n", "Licensed under the MIT License." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "![Impressions](https://PixelServer20190423114238.azurewebsites.net/api/impressions/MachineLearningNotebooks/how-to-use-azureml/automated-machine-learning/forecasting-recipes-univariate/1_determine_experiment_settings.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "!Important!
This notebook is outdated and is not supported by the AutoML Team. Please use the supported version ([link](https://github.com/Azure/azureml-examples/tree/main/sdk/python/jobs/automl-standalone-jobs/automl-forecasting-recipes-univariate)).
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this notebook we will explore the univariate time-series data to determine the settings for an automated ML experiment. We will follow the thought process depicted in the following diagram:
\n", "![Forecasting after training](figures/univariate_settings_map_20210408.jpg)\n", "\n", "The objective is to answer the following questions:\n", "\n", "
    \n", "
  1. Is there a seasonal pattern in the data?
  2. \n", " \n", "
  3. Is the data stationary?
  4. \n", " \n", "
  5. Is there a detectable auto-regressive pattern in the stationary data?
  6. \n", " \n", "
\n", "\n", "The answers to these questions will help determine the appropriate settings for the automated ML experiment.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "import warnings\n", "import pandas as pd\n", "\n", "from statsmodels.graphics.tsaplots import plot_acf, plot_pacf\n", "import matplotlib.pyplot as plt\n", "from pandas.plotting import register_matplotlib_converters\n", "\n", "register_matplotlib_converters() # fixes the future warning issue\n", "\n", "from helper_functions import unit_root_test_wrapper\n", "from statsmodels.tools.sm_exceptions import InterpolationWarning\n", "\n", "warnings.simplefilter(\"ignore\", InterpolationWarning)\n", "\n", "\n", "# set printing options\n", "pd.set_option(\"display.max_columns\", 500)\n", "pd.set_option(\"display.width\", 1000)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# load data\n", "main_data_loc = \"data\"\n", "train_file_name = \"S4248SM144SCEN.csv\"\n", "\n", "TARGET_COLNAME = \"S4248SM144SCEN\"\n", "TIME_COLNAME = \"observation_date\"\n", "COVID_PERIOD_START = \"2020-03-01\"\n", "\n", "df = pd.read_csv(os.path.join(main_data_loc, train_file_name))\n", "df[TIME_COLNAME] = pd.to_datetime(df[TIME_COLNAME], format=\"%Y-%m-%d\")\n", "df.sort_values(by=TIME_COLNAME, inplace=True)\n", "df.set_index(TIME_COLNAME, inplace=True)\n", "df.head(2)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# plot the entire dataset\n", "fig, ax = plt.subplots(figsize=(6, 2), dpi=180)\n", "ax.plot(df)\n", "ax.title.set_text(\"Original Data Series\")\n", "locs, labels = plt.xticks()\n", "plt.xticks(rotation=45)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The graph plots the alcohol sales in the United States. Because the data is trending, it can be difficult to see cycles, seasonality or other interesting behaviors due to the scaling issues. For example, if there is a seasonal pattern, which we will discuss later, we cannot see them on the trending data. In such case, it is worth plotting the same data in first differences." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# plot the entire dataset in first differences\n", "fig, ax = plt.subplots(figsize=(6, 2), dpi=180)\n", "ax.plot(df.diff().dropna())\n", "ax.title.set_text(\"Data in first differences\")\n", "locs, labels = plt.xticks()\n", "plt.xticks(rotation=45)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the previous plot we observe that the data is more volatile towards the end of the series. This period coincides with the Covid-19 period, so we will exclude it from our experiment. Since in this example there are no user-provided features it is hard to make an argument that a model trained on the less volatile pre-covid data will be able to accurately predict the covid period." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# 1. Seasonality\n", "\n", "#### Questions that need to be answered in this section:\n", "1. Is there a seasonality?\n", "2. If it's seasonal, does the data exhibit a trend (up or down)?\n", "\n", "It is hard to visually detect seasonality when the data is trending. The reason being is scale of seasonal fluctuations is dwarfed by the range of the trend in the data. One way to deal with this is to de-trend the data by taking the first differences. We will discuss this in more detail in the next section." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# plot the entire dataset in first differences\n", "fig, ax = plt.subplots(figsize=(6, 2), dpi=180)\n", "ax.plot(df.diff().dropna())\n", "ax.title.set_text(\"Data in first differences\")\n", "locs, labels = plt.xticks()\n", "plt.xticks(rotation=45)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For the next plot, we will exclude the Covid period again. We will also shorten the length of data because plotting a very long time series may prevent us from seeing seasonal patterns, if there are any, because the plot may look like a random walk." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# remove COVID period\n", "df = df[:COVID_PERIOD_START]\n", "\n", "# plot the entire dataset in first differences\n", "fig, ax = plt.subplots(figsize=(6, 2), dpi=180)\n", "ax.plot(df[\"2015-01-01\":].diff().dropna())\n", "ax.title.set_text(\"Data in first differences\")\n", "locs, labels = plt.xticks()\n", "plt.xticks(rotation=45)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

Conclusion

\n", "\n", "Visual examination does not suggest clear seasonal patterns. We will set the STL_TYPE = None, and we will move to the next section that examines stationarity. \n", "\n", "\n", "Say, we are working with a different data set that shows clear patterns of seasonality, we have several options for setting the settings:is hard to say which option will work best in your case, hence you will need to run both options to see which one results in more accurate forecasts. \n", "
    \n", "
  1. If the data does not appear to be trending, set DIFFERENCE_SERIES=False, TARGET_LAGS=None and STL_TYPE = \"season\"
  2. \n", "
  3. If the data appears to be trending, consider one of the following two settings:\n", " \n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# 2. Stationarity\n", "If the data does not exhibit seasonal patterns, we would like to see if the data is non-stationary. Particularly, we want to see if there is a clear trending behavior. If such behavior is observed, we would like to first difference the data and examine the plot of an auto-correlation function (ACF) known as correlogram. If the data is seasonal, differencing it will not get rid off the seasonality and this will be shown on the correlogram as well.\n", "\n", "\n", "\n", "\n", "\n", "\n", "#### Questions that need to be answered in this section:\n", "
    \n", "
  1. Is the data stationary?
  2. \n", "
  3. Does the stationarized data (either the original or the differenced series) exhibit a clear auto-regressive pattern?
  4. \n", "
\n", "\n", "To answer the first question, we run a series of tests (we call them unit root tests)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# unit root tests\n", "test = unit_root_test_wrapper(df[TARGET_COLNAME])\n", "print(\"---------------\", \"\\n\")\n", "print(\"Summary table\", \"\\n\", test[\"summary\"], \"\\n\")\n", "print(\"Is the {} series stationary?: {}\".format(TARGET_COLNAME, test[\"stationary\"]))\n", "print(\"---------------\", \"\\n\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the previous cell, we ran a series of unit root tests. The summary table contains the following columns:\n", "\n", "\n", "Each of the tests shows that the original time series is non-stationary. The final decision is based on the majority rule. If, there is a split decision, the algorithm will claim it is stationary. We run a series of tests because each test by itself may not be accurate. In many cases when there are conflicting test results, the user needs to make determination if the series is stationary or not.\n", "\n", "Since we found the series to be non-stationary, we will difference it and then test if the differenced series is stationary." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# unit root tests\n", "test = unit_root_test_wrapper(df[TARGET_COLNAME].diff().dropna())\n", "print(\"---------------\", \"\\n\")\n", "print(\"Summary table\", \"\\n\", test[\"summary\"], \"\\n\")\n", "print(\"Is the {} series stationary?: {}\".format(TARGET_COLNAME, test[\"stationary\"]))\n", "print(\"---------------\", \"\\n\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Four out of five tests show that the series in first differences is stationary. Notice that this decision is not unanimous. Next, let's plot the original series in first-differences to illustrate the difference between non-stationary (unit root) process vs the stationary one." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# plot original and stationary data\n", "fig = plt.figure(figsize=(10, 10))\n", "ax1 = fig.add_subplot(211)\n", "ax1.plot(df[TARGET_COLNAME], \"-b\")\n", "ax2 = fig.add_subplot(212)\n", "ax2.plot(df[TARGET_COLNAME].diff().dropna(), \"-b\")\n", "ax1.title.set_text(\"Original data\")\n", "ax2.title.set_text(\"Data in first differences\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you were asked a question \"What is the mean of the series before and after 2008?\", for the series titled \"Original data\" the mean values will be significantly different. This implies that the first moment of the series (in this case, it is the mean) is time dependent, i.e., mean changes depending on the interval one is looking at. Thus, the series is deemed to be non-stationary. On the other hand, for the series titled \"Data in first differences\" the means for both periods are roughly the same. Hence, the first moment is time invariant; meaning it does not depend on the interval of time one is looking at. In this example it is easy to visually distinguish between stationary and non-stationary data. Often this distinction is not easy to make, therefore we rely on the statistical tests described above to help us make an informed decision. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "

Conclusion

\n", "Since we found the original process to be non-stationary (contains unit root), we will have to model the data in first differences. As a result, we will set the DIFFERENCE_SERIES parameter to True." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# 3 Check if there is a clear auto-regressive pattern\n", "We need to determine if we should include lags of the target variable as features in order to improve forecast accuracy. To do this, we will examine the ACF and partial ACF (PACF) plots of the stationary series. In our case, it is a series in first differences.\n", "\n", "