From 90d8e7dcd5c6590b3138b455e0e28464b37d9605 Mon Sep 17 00:00:00 2001 From: seblessa Date: Tue, 13 Aug 2024 18:10:21 +0100 Subject: [PATCH 1/5] TSMixer code --- forecasting/TSMixer/README.md | 18 + forecasting/TSMixer/notebook.ipynb | 668 +++++++++++++++++++++++++++ forecasting/TSMixer/requirements.txt | 11 + forecasting/TSMixer/utils.py | 154 ++++++ 4 files changed, 851 insertions(+) create mode 100644 forecasting/TSMixer/README.md create mode 100644 forecasting/TSMixer/notebook.ipynb create mode 100644 forecasting/TSMixer/requirements.txt create mode 100644 forecasting/TSMixer/utils.py diff --git a/forecasting/TSMixer/README.md b/forecasting/TSMixer/README.md new file mode 100644 index 0000000..3a6df37 --- /dev/null +++ b/forecasting/TSMixer/README.md @@ -0,0 +1,18 @@ +# TSMixer + +**Steps to run the code:** +1. Create a virtual environment with python + - `conda create --name myenv python=3.12.4` +3. Activate in your new virtual environment + - `conda activate myenv` +4. Install the required requirements + - `pip install -r requirements.txt` +5. Run the notebook + +## Folder Structure: + ├── TSMixer + │ + │──── requirements.txt <- package version for installing + │──── utils.py <- helper functions + └──── notebook.ipynb <- notebook to run the code +------------ diff --git a/forecasting/TSMixer/notebook.ipynb b/forecasting/TSMixer/notebook.ipynb new file mode 100644 index 0000000..bf6f708 --- /dev/null +++ b/forecasting/TSMixer/notebook.ipynb @@ -0,0 +1,668 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 13, + "id": "initial_id", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-12T17:06:51.518489Z", + "start_time": "2024-08-12T17:06:51.475836Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The autoreload extension is already loaded. To reload it, use:\n", + " %reload_ext autoreload\n" + ] + } + ], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "\n", + "from darts.dataprocessing.transformers import StaticCovariatesTransformer\n", + "from darts.utils.likelihood_models import QuantileRegression\n", + "from darts.dataprocessing.transformers import Scaler\n", + "from darts.dataprocessing.pipeline import Pipeline\n", + "from dateutil.relativedelta import relativedelta\n", + "from darts.models import TiDEModel, TSMixerModel\n", + "from darts import TimeSeries\n", + "import pandas as pd\n", + "pd.options.mode.chained_assignment = None\n", + "import torch\n", + "torch.set_float32_matmul_precision('high')\n", + "import utils\n", + "\n", + "import warnings\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "DATASET = \"hf://datasets/zaai-ai/time_series_datasets/data.csv\"\n", + "\n", + "NUM_FOLDS = 4\n", + "\n", + "TIME_COL = \"Date\"\n", + "TARGET = \"visits\"\n", + "STATIC_COV = [\"static_1\", \"static_2\", \"static_3\", \"static_4\"]\n", + "DYNAMIC_COV = [\"CPI\", \"Inflation_Rate\", \"GDP\"]\n", + "\n", + "FORECAST_HORIZON = 6 # months\n", + "FREQ = \"MS\"\n", + "\n", + "SCALER = Scaler()\n", + "TRANSFORMER = StaticCovariatesTransformer()\n", + "PIPELINE = Pipeline([SCALER, TRANSFORMER])" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "856a5062705237b4", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-12T17:06:52.643487Z", + "start_time": "2024-08-12T17:06:51.519547Z" + } + }, + "outputs": [], + "source": [ + "# load data\n", + "df = pd.read_csv(DATASET).drop(columns=[\"Unnamed: 0\"])\n", + "df[TIME_COL] = pd.to_datetime(df[TIME_COL])\n", + "df[TARGET] = df[TARGET].replace(0.0, 0.01)\n", + "df[TARGET] = df[TARGET].round(2)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "ee37c18f598b4fb7", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-12T17:06:52.712910Z", + "start_time": "2024-08-12T17:06:52.645815Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Distinct number of time series: 304\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Dateunique_idvisitsCPIInflation_RateGDPstatic_1static_2static_3static_4
01998-01-01AAAHol2015.4467.40.0093.996740e+11AAAHol
11998-02-01AAAHol514.3467.40.0093.996740e+11AAAHol
21998-03-01AAAHol532.1067.40.0093.996740e+11AAAHol
31998-04-01AAAHol534.0667.40.0093.996740e+11AAAHol
41998-05-01AAAHol505.2267.40.0093.996740e+11AAAHol
.................................
691092016-08-01GBDOth0.01109.10.0131.207580e+12GBDOth
691102016-09-01GBDOth0.01109.10.0131.207580e+12GBDOth
691112016-10-01GBDOth0.01109.10.0131.207580e+12GBDOth
691122016-11-01GBDOth0.01109.10.0131.207580e+12GBDOth
691132016-12-01GBDOth0.01109.10.0131.207580e+12GBDOth
\n", + "

69114 rows × 10 columns

\n", + "
" + ], + "text/plain": [ + " Date unique_id visits CPI Inflation_Rate GDP \\\n", + "0 1998-01-01 AAAHol 2015.44 67.4 0.009 3.996740e+11 \n", + "1 1998-02-01 AAAHol 514.34 67.4 0.009 3.996740e+11 \n", + "2 1998-03-01 AAAHol 532.10 67.4 0.009 3.996740e+11 \n", + "3 1998-04-01 AAAHol 534.06 67.4 0.009 3.996740e+11 \n", + "4 1998-05-01 AAAHol 505.22 67.4 0.009 3.996740e+11 \n", + "... ... ... ... ... ... ... \n", + "69109 2016-08-01 GBDOth 0.01 109.1 0.013 1.207580e+12 \n", + "69110 2016-09-01 GBDOth 0.01 109.1 0.013 1.207580e+12 \n", + "69111 2016-10-01 GBDOth 0.01 109.1 0.013 1.207580e+12 \n", + "69112 2016-11-01 GBDOth 0.01 109.1 0.013 1.207580e+12 \n", + "69113 2016-12-01 GBDOth 0.01 109.1 0.013 1.207580e+12 \n", + "\n", + " static_1 static_2 static_3 static_4 \n", + "0 A A A Hol \n", + "1 A A A Hol \n", + "2 A A A Hol \n", + "3 A A A Hol \n", + "4 A A A Hol \n", + "... ... ... ... ... \n", + "69109 G B D Oth \n", + "69110 G B D Oth \n", + "69111 G B D Oth \n", + "69112 G B D Oth \n", + "69113 G B D Oth \n", + "\n", + "[69114 rows x 10 columns]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "print(f\"Distinct number of time series: {len(df['unique_id'].unique())}\")\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "5da8eadbebf07550", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-12T17:06:52.768035Z", + "start_time": "2024-08-12T17:06:52.715116Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Initial date: 1998-01-01 00:00:00\n", + "End date: 2016-12-01 00:00:00\n", + "\n", + "25 percentile date: 2002-10-01 00:00:00\n", + "\n", + "Total of years: 18.92\n" + ] + } + ], + "source": [ + "print(f\"Initial date: {df[TIME_COL].min()}\")\n", + "end_date = df[TIME_COL].max()\n", + "print(f\"End date: {end_date}\\n\")\n", + "\n", + "print(f\"25 percentile date: {df[TIME_COL].quantile(0.25)}\\n\")\n", + "\n", + "# Total of years\n", + "print(f\"Total of years: {round((end_date-df[TIME_COL].min()).days / 365.25, 2)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "110d1e0edae3be8b", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-12T17:06:53.681403Z", + "start_time": "2024-08-12T17:06:52.769633Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Fold 1:\n", + "Months for training: 204 from 1998-01-01 to 2014-12-01\n", + "Months for testing: 6 from 2015-01-01 to 2015-06-01\n", + "\n", + "Fold 2:\n", + "Months for training: 210 from 1998-01-01 to 2015-06-01\n", + "Months for testing: 6 from 2015-07-01 to 2015-12-01\n", + "\n", + "Fold 3:\n", + "Months for training: 216 from 1998-01-01 to 2015-12-01\n", + "Months for testing: 6 from 2016-01-01 to 2016-06-01\n", + "\n", + "Fold 4:\n", + "Months for training: 222 from 1998-01-01 to 2016-06-01\n", + "Months for testing: 6 from 2016-07-01 to 2016-12-01\n", + "\n" + ] + } + ], + "source": [ + "folds = []\n", + "for i in range(NUM_FOLDS, 0, -1):\n", + " start_date = end_date - relativedelta(months=FORECAST_HORIZON*i)\n", + " end_date_fold = start_date + relativedelta(months=FORECAST_HORIZON)\n", + "\n", + " fold = {\n", + " \"start_date\": start_date,\n", + " \"end_date\": end_date_fold,\n", + " \"train\": None,\n", + " \"test\": None,\n", + " \"df\": None,\n", + " \"predictions\": {\n", + " \"tsmixer\": None,\n", + " \"tide\": None,\n", + " }\n", + " }\n", + "\n", + " fold[\"train\"] = df[df[TIME_COL] <= fold[\"start_date\"]]\n", + " fold[\"test\"] = df[(df[TIME_COL] > fold[\"start_date\"]) & (df[TIME_COL] <= fold[\"end_date\"])]\n", + " fold[\"df\"] = df[df[TIME_COL] <= fold[\"end_date\"]]\n", + " folds.append(fold)\n", + " \n", + " \n", + "for i,fold in enumerate(folds):\n", + " print(f\"Fold {i+1}:\")\n", + " print(f\"Months for training: {len(fold['train'][TIME_COL].unique())} from {min(fold['train'][TIME_COL]).date()} to {max(fold['train'][TIME_COL]).date()}\")\n", + " print(f\"Months for testing: {len(fold['test'][TIME_COL].unique())} from {min(fold['test'][TIME_COL]).date()} to {max(fold['test'][TIME_COL]).date()}\\n\")" + ] + }, + { + "cell_type": "markdown", + "id": "5b7cc71995e5c24e", + "metadata": {}, + "source": [ + "# TSMixer" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "67c23fdafff226ca", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-12T17:06:53.720896Z", + "start_time": "2024-08-12T17:06:53.682653Z" + } + }, + "outputs": [], + "source": [ + "# RMSE: 130.50653994006464\n", + "TSMixer_params = {\n", + " \"input_chunk_length\": 10,\n", + " \"output_chunk_length\": FORECAST_HORIZON,\n", + " \"hidden_size\": 16,\n", + " \"ff_size\": 8,\n", + " \"num_blocks\": 3,\n", + " \"activation\": 'LeakyReLU',\n", + " \"dropout\": 0.35,\n", + " \"normalize_before\": False,\n", + " \"batch_size\": 64,\n", + " \"n_epochs\": 30,\n", + " \"likelihood\": QuantileRegression(quantiles=[0.25, 0.5, 0.75]),\n", + " \"random_state\": 42,\n", + " \"use_static_covariates\": True,\n", + " \"optimizer_kwargs\": {'lr': 0.001},\n", + " \"use_reversible_instance_norm\": True,\n", + "}" + ] + }, + { + "cell_type": "code", + "id": "ec44c7d0083d0c9a", + "metadata": {}, + "source": [ + "print(\"Executing TSMixer folds predictions...\\n\")\n", + "for i,fold in enumerate(folds): \n", + " train_darts = TimeSeries.from_group_dataframe(\n", + " df=fold['train'],\n", + " group_cols=STATIC_COV,\n", + " time_col=TIME_COL,\n", + " value_cols=TARGET,\n", + " freq=FREQ,\n", + " fill_missing_dates=True,\n", + " fillna_value=0)\n", + " \n", + " dynamic_covariates = utils.create_dynamic_covariates(train_darts,fold['df'],FORECAST_HORIZON,DYNAMIC_COV)\n", + " \n", + " # scale covariates\n", + " dynamic_covariates_transformed = SCALER.fit_transform(dynamic_covariates)\n", + " \n", + " # scale data and transform static +covariates\n", + " data_transformed = PIPELINE.fit_transform(train_darts)\n", + " \n", + " tsmixer = TSMixerModel(**TSMixer_params)\n", + " tsmixer.fit(data_transformed, future_covariates=dynamic_covariates_transformed, verbose=False)\n", + " pred = PIPELINE.inverse_transform(tsmixer.predict(n=FORECAST_HORIZON, series=data_transformed, future_covariates=dynamic_covariates_transformed, num_samples=50)) \n", + " fold['predictions']['tsmixer'] = utils.transform_predictions_to_pandas(pred, TARGET, train_darts, [0.25, 0.5, 0.75])" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "66328f11677e0d17", + "metadata": {}, + "source": [ + "# TiDE" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "b61fc9fc13d71495", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-12T18:24:21.487396Z", + "start_time": "2024-08-12T18:24:21.448674Z" + } + }, + "outputs": [], + "source": [ + "# RMSE: 139.9541525028933\n", + "TiDE_params = {\n", + " \"input_chunk_length\": 4,\n", + " 'output_chunk_length': FORECAST_HORIZON,\n", + " \"num_encoder_layers\": 2,\n", + " \"num_decoder_layers\": 6,\n", + " \"decoder_output_dim\": 15,\n", + " \"hidden_size\": 16,\n", + " \"temporal_width_past\": 8,\n", + " \"temporal_width_future\": 8,\n", + " \"temporal_decoder_hidden\": 16,\n", + " \"dropout\": 0.15,\n", + " \"batch_size\": 64,\n", + " \"n_epochs\": 30,\n", + " 'likelihood': QuantileRegression(quantiles=[0.25, 0.5, 0.75]),\n", + " 'random_state': 42,\n", + " 'use_static_covariates': True,\n", + " \"optimizer_kwargs\": {'lr': 0.001},\n", + " \"use_reversible_instance_norm\": False,\n", + "}" + ] + }, + { + "cell_type": "code", + "id": "5d0b4745d20a8674", + "metadata": {}, + "source": [ + "print(\"Executing TiDE folds predictions...\\n\")\n", + "for i,fold in enumerate(folds): \n", + " train_darts = TimeSeries.from_group_dataframe(\n", + " df=fold['train'],\n", + " group_cols=STATIC_COV,\n", + " time_col=TIME_COL,\n", + " value_cols=TARGET,\n", + " freq=FREQ,\n", + " fill_missing_dates=True,\n", + " fillna_value=0)\n", + "\n", + " dynamic_covariates = utils.create_dynamic_covariates(train_darts,fold['df'],FORECAST_HORIZON,DYNAMIC_COV)\n", + " \n", + " # scale covariates\n", + " dynamic_covariates_transformed = SCALER.fit_transform(dynamic_covariates)\n", + " \n", + " # scale data and transform static +covariates\n", + " data_transformed = PIPELINE.fit_transform(train_darts)\n", + " \n", + " tide = TiDEModel(**TiDE_params)\n", + " tide.fit(data_transformed, future_covariates=dynamic_covariates_transformed, verbose=False)\n", + " pred = PIPELINE.inverse_transform(tide.predict(n=FORECAST_HORIZON, series=data_transformed, future_covariates=dynamic_covariates_transformed, num_samples=50)) \n", + " fold['predictions']['tide'] = utils.transform_predictions_to_pandas(pred, TARGET, train_darts, [0.25, 0.5, 0.75])" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "id": "e526aa836b31ea7e", + "metadata": {}, + "source": [ + "# Evaluation" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "3e09e09df8fe43fd", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-12T19:22:32.371359Z", + "start_time": "2024-08-12T19:22:32.324563Z" + } + }, + "outputs": [], + "source": [ + "tide_forecasts = []\n", + "tsmixer_forecasts = []\n", + "tests = []\n", + "\n", + "for i,fold in enumerate(folds):\n", + " tsmixer_forecasts.append(fold['predictions']['tsmixer'])\n", + " tide_forecasts.append(fold['predictions']['tide'])\n", + " tests.append(fold['test'])" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "a2724dcd7659812f", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-12T19:22:32.416725Z", + "start_time": "2024-08-12T19:22:32.372522Z" + } + }, + "outputs": [], + "source": [ + "top_25 = df.groupby(['unique_id']).agg({TARGET: 'sum'}).reset_index().sort_values(by=TARGET,ascending=False).head(25)" + ] + }, + { + "cell_type": "markdown", + "id": "ddb32d9011ac0fb7", + "metadata": {}, + "source": [ + "## Graph" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "8e36514d79757c8c", + "metadata": { + "ExecuteTime": { + "end_time": "2024-08-12T19:22:32.620890Z", + "start_time": "2024-08-12T19:22:32.418023Z" + } + }, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABIgAAAK8CAYAAACEF4RCAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/TGe4hAAAACXBIWXMAAA9hAAAPYQGoP6dpAABUuUlEQVR4nO3deZhWZf0/8PcMwz4IqCi4gIBLKn1dsLQAwRUFFS1BRRMwhXZySy0XTL+ilZqaJpqCCxhipeKWK26VSYum5gJukYJLriDrPL8/+jlfR0BndIYZPK/Xdc118dznPvfzOcNnEN7e5zxlpVKpFAAAAAAKq7yxCwAAAACgcQmIAAAAAApOQAQAAABQcAIiAAAAgIITEAEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAgEIZN25cysrK8tprrzV2KQ1i0qRJKSsry/PPP1/nc9//3jQVzz//fMrKyjJp0qTGLqVebLTRRtlrr70auwwAWCEBEQA0kPf/oV5WVpYHHnhgueOlUikbbrhhysrKmvw/GjfaaKOUlZVl1113XeHxSy+9tPpaZ86cucI5P/jBD1JWVpYDDjhghcffDwPe/2rWrFm6du2a/fbbL3//+99rzP3gvA9/feMb3/hU11pfBgwYkLKysmyyySYrPH7HHXdU13zdddet4uo+W2bMmFH9vbz66qtXOKdPnz4pKytLr169GrSWJ554IuPGjftEAR0ANKaKxi4AAD7rWrVqlSlTpqRv3741xu+9997MmTMnLVu2bKTK6qZVq1a55557Mnfu3HTu3LnGscmTJ6dVq1ZZuHDhCs8tlUq55pprstFGG2X69Ol555130q5duxXOPeiggzJo0KAsW7Ys//znP/PLX/4yt956a/70pz9l6623rp6322675dBDD13u/E033fSTX2Q9a9WqVWbNmpU///nP+eIXv1jj2Md9z6i793/WDjnkkBrjzz//fP7whz+kVatWDV7DE088kVNPPTUDBgzIRhtt1ODvBwD1xQ4iAGhggwYNyrRp07J06dIa41OmTEnv3r2XC1uaqj59+qSysjJTp06tMT5nzpzcf//9GTx48ErPnTFjRubMmZPLL788S5cuzW9/+9uVzt12221zyCGHZMSIETnzzDNz9dVXZ9GiRfnlL39ZY96mm26aQw45ZLmvDwcxjalnz57ZbLPNcs0119QYX7hwYX73u9995PeMuhs0aFDuuOOO5W4fnDJlStZdd91st912jVQZADR9AiIAaGAHHXRQXn/99dxxxx3VY4sXL851112X4cOHr/Ccqqqq/PznP8+WW26ZVq1aZd11182YMWPyxhtv1Jh3ww03ZPDgwVlvvfXSsmXL9OzZM6eddlqWLVtWY96AAQPSq1evPPHEE9lpp53Spk2brL/++vnJT35S6+to1apVvvKVr2TKlCk1xq+55pp07NgxAwcOXOm5kydPzhZbbJGddtopu+66ayZPnlzr9915552TJM8991ytz6mN1157LcOGDcsaa6yRtdZaK2PHjq2xm6d///7ZaqutVnjuZptt9pHX+0EHHXRQpk6dmqqqquqx6dOnZ8GCBRk2bNgKz/nb3/6WPffcM2ussUYqKyuzyy675E9/+tNy8x5//PHsvPPOad26dTbYYIOcfvrpNd7ng2699db069cvbdu2Tbt27TJ48OA8/vjjtbqGD7v//vszdOjQdO3aNS1btsyGG26YI488Mu+9916NeSNHjkxlZWX+/e9/Z999901lZWU6deqUY445ZrkeffPNNzNy5Mi0b98+HTp0yIgRI/Lmm2/Wqa4hQ4akZcuWmTZtWo3xKVOmZNiwYWnWrNly5yxdujSnnXZaevbsmZYtW2ajjTbKD3/4wyxatKjGvPefH/TAAw/ki1/8Ylq1apUePXrkyiuvrJ4zadKkDB06NEmy0047Vd/2NmPGjBprfdQaANBYBEQA0MA22mijfOlLX6qxi+TWW2/NW2+9lQMPPHCF54wZMybHHnts+vTpk/POOy+jRo3K5MmTM3DgwCxZsqR63qRJk1JZWZmjjjoq5513Xnr37p2TTz45xx9//HJrvvHGG9ljjz2y1VZb5eyzz87nPve5HHfccbn11ltrfS3Dhw/Pn//858yePbt6bMqUKdl///3TvHnzFZ6zaNGi/OY3v8lBBx2U5L+Byd133525c+fW6j3ff6+11lqrxvjChQvz2muvLfe1ePHiWq07bNiwLFy4MOPHj8+gQYNy/vnnZ/To0dXHv/a1r+XRRx/NY489VuO8hx9+OE8//fRytzGtzPDhw/Pyyy/XCAmmTJmSXXbZJeuss85y8x9//PH069cvjzzySH7wgx/kpJNOynPPPZcBAwbkoYceqp43d+7c7LTTTvn73/+e448/Pt///vdz5ZVX5rzzzltuzauuuiqDBw9OZWVlzjrrrJx00kl54okn0rdv30/0rJxp06ZlwYIF+eY3v5kLLrggAwcOzAUXXLDCW/6WLVuWgQMHZq211srPfvaz9O/fP2effXYuueSS6jmlUilDhgzJVVddlUMOOSSnn3565syZkxEjRtSprjZt2mTIkCE1ftYeeeSRPP744ysNYw8//PCcfPLJ2XbbbXPuueemf//+GT9+/Ap/NmfNmpX9998/u+22W84+++x07NgxI0eOrA7adtxxx3zve99Lkvzwhz/MVVddlauuuiqbb755rdcAgEZTAgAaxMSJE0tJSg8//HDpF7/4Raldu3alBQsWlEqlUmno0KGlnXbaqVQqlUrdunUrDR48uPq8+++/v5SkNHny5Brr3XbbbcuNv7/eB40ZM6bUpk2b0sKFC6vH+vfvX0pSuvLKK6vHFi1aVOrcuXPpq1/96sdey/s1Ll26tNS5c+fSaaedViqVSqUnnniilKR077331rjeD7ruuutKSUrPPPNMqVQqld5+++1Sq1atSueee26Nec8991wpSenUU08tvfrqq6W5c+eWZsyYUdpmm21KSUq/+c1vqucmWenXNddc85HXcsopp5SSlPbZZ58a49/61rdKSUqPPPJIqVQqld58881Sq1atSscdd1yNed/73vdKbdu2Lb377rsf+T79+/cvbbnllqVSqVTabrvtSl//+tdLpVKp9MYbb5RatGhRuuKKK0r33HNPKUlp2rRp1eftu+++pRYtWpRmz55dPfbSSy+V2rVrV9pxxx2rx77//e+XkpQeeuih6rFXXnml1L59+1KS0nPPPVcqlUqld955p9ShQ4fSEUccUaO+uXPnltq3b19j/P3vzcdZUd+NHz++VFZWVnrhhReqx0aMGFFKUvrxj39cY+4222xT6t27d/Xr66+/vpSk9JOf/KR6bOnSpaV+/fqVkpQmTpz4kfV88Pt40003lcrKykovvvhiqVQqlY499thSjx49SqVSzd+TUqlU+vvf/15KUjr88MNrrHfMMceUkpTuvvvu6rFu3bqVkpTuu+++6rFXXnml1LJly9LRRx9dPTZt2rRSktI999yzXJ21XQMAGoMdRACwCgwbNizvvfdebrrpprzzzju56aabVrqjYdq0aWnfvn122223GjtjevfuncrKytxzzz3Vc1u3bl3963feeSevvfZa+vXrlwULFuTJJ5+ssW5lZWWNXS8tWrTIF7/4xTz77LO1vo5mzZpl2LBh1Ts0Jk+enA033DD9+vVb6TmTJ0/Odtttl4033jhJqm9vWtltZqeccko6deqUzp07Z8CAAZk9e3bOOuusfOUrX6kxb8iQIbnjjjuW+9ppp51qdS3f/va3a7z+7ne/myS55ZZbkiTt27ev3o1SKpWS/Hc3zNSpU7Pvvvumbdu2tXqf5L+7iH77299W31rYrFmz7LfffsvNW7ZsWW6//fbsu+++6dGjR/V4ly5dMnz48DzwwAN5++23q+vcYYcdajxzqVOnTjn44INrrHnHHXfkzTffzEEHHVSjn5o1a5btt9++Rj/V1gf7bv78+Xnttdfy5S9/OaVSKX/729+Wm//hT5br169fjb675ZZbUlFRkW9+85vVY82aNav+PamL3XffPWuuuWZ+/etfp1Qq5de//nX17rUPe//3+qijjqoxfvTRRydJbr755hrjW2yxRY1e79SpUzbbbLM6/QzVxxoA0BB8ihkArAKdOnXKrrvumilTpmTBggVZtmxZ9t9//xXOfeaZZ/LWW2+t8PajJHnllVeqf/3444/nxBNPzN13310dHLzvrbfeqvF6gw02SFlZWY2xjh075tFHH63TtQwfPjznn39+HnnkkUyZMiUHHnjgcuu+780338wtt9yS73znO5k1a1b1eJ8+ffKb3/wmTz/99HKfOjZ69OgMHTo05eXl6dChQ7bccssVftLbBhtskF133bVOtX/Qhz9+vmfPnikvL69xy9Whhx6aqVOn5v7778+OO+6YO++8M/PmzcvXvva1Or3XgQcemGOOOSa33nprJk+enL322muFn+L26quvZsGCBdlss82WO7b55punqqoq//rXv7LlllvmhRdeyPbbb7/cvA+f+8wzzyT5v2c5fdgaa6xRp2tJkhdffDEnn3xybrzxxuWei/XhvmvVqlU6depUY6xjx441znvhhRfSpUuXVFZW1pi3ou/Dx2nevHmGDh2aKVOm5Itf/GL+9a9/rTSMfeGFF1JeXl4dXr6vc+fO6dChQ1544YUa4127dl1ujQ9fy8epjzUAoCEIiABgFRk+fHiOOOKIzJ07N3vuuWc6dOiwwnlVVVVZZ511VrrD5v1/bL/55pvp379/1lhjjfz4xz9Oz54906pVq/z1r3/Ncccdt9zDilf0gN4k1btjamv77bdPz5498/3vfz/PPffcSv/xnfx3N9SiRYty9tln5+yzz17u+OTJk3PqqafWGNtkk00+VfDzSa0o5Bo4cGDWXXfdXH311dlxxx1z9dVXp3PnznWur0uXLhkwYEDOPvvsPPjgg/nNb35TX2V/rPf74KqrrlrhJ+ZVVNTtr4PLli3Lbrvtlv/85z857rjj8rnPfS5t27bNv//974wcObLWfdeQhg8fnosvvjjjxo3LVlttlS222OIj568s4Pyw+vgZqq+fQwCobwIiAFhF9ttvv4wZMyZ/+tOflvuo+A/q2bNn7rzzzvTp06fGrTwfNmPGjLz++uv57W9/mx133LF6vL4/7WtFDjrooJx++unZfPPNs/XWW6903uTJk9OrV6+ccsopyx2bMGFCpkyZslxAtKo888wz6d69e/XrWbNmpaqqKhtttFH1WLNmzTJ8+PBMmjQpZ511Vq6//vocccQRnyj0GD58eA4//PB06NAhgwYNWuGcTp06pU2bNnnqqaeWO/bkk0+mvLw8G264YZKkW7du1buDPujD5/bs2TNJss4669RL8PaPf/wjTz/9dK644ooaD6X+4Kf01VW3bt1y11135d13362xi2hF34fa6Nu3b7p27ZoZM2bkrLPO+sj3raqqyjPPPFPjQdLz5s3Lm2++mW7dutX5vWsbNgFAU+MZRACwilRWVuaXv/xlxo0bl7333nul84YNG5Zly5bltNNOW+7Y0qVLqz/6+/2Q4oM7DxYvXpyLLrqofgtfgcMPPzynnHLKCncFve9f//pX7rvvvgwbNiz777//cl+jRo3KrFmzanwy16p04YUX1nh9wQUXJEn23HPPGuNf+9rX8sYbb2TMmDF59913a/3pZR+2//7755RTTslFF12UFi1arHBOs2bNsvvuu+eGG26ocavbvHnzMmXKlPTt27f6lrBBgwblT3/6U/785z9Xz3v11VeX23k2cODArLHGGjnjjDNqfALeB8+pixX1XalUWuGnp9XWoEGDsnTp0vzyl7+sHlu2bFn170ldlZWV5fzzz88pp5zykbcDvh/U/fznP68xfs455yRJBg8eXOf3fv/ZVO//nALA6sIOIgBYhWrzsd39+/fPmDFjMn78+Pz973/P7rvvnubNm+eZZ57JtGnTct5552X//ffPl7/85XTs2DEjRozI9773vZSVleWqq65aJbeqdOvWLePGjfvIOVOmTEmpVMo+++yzwuODBg1KRUVFJk+evMJn6Xycp59+OldfffVy4+uuu2522223jz3/ueeeyz777JM99tgjf/zjH3P11Vdn+PDh2WqrrWrM22abbdKrV69MmzYtm2++ebbddts615r896HXH/c9S5LTTz89d9xxR/r27ZtvfetbqaioyIQJE7Jo0aL85Cc/qZ73gx/8IFdddVX22GOPjB07Nm3bts0ll1ySbt261Xiu1BprrJFf/vKX+drXvpZtt902Bx54YDp16pQXX3wxN998c/r06ZNf/OIXtb6Oz33uc+nZs2eOOeaY/Pvf/84aa6yR3/zmN5/qGTp77713+vTpk+OPPz7PP/98tthii/z2t79d7nlGdTFkyJAMGTLkI+dstdVWGTFiRC655JLqWzb//Oc/54orrsi+++5b6weef9DWW2+dZs2a5ayzzspbb72Vli1bZuedd17pM8UAoKkQEAFAE3TxxRend+/emTBhQn74wx+moqIiG220UQ455JD06dMnSbLWWmvlpptuytFHH50TTzwxHTt2zCGHHJJddtklAwcObOQr+O/tZV27dl0ucHlfhw4d0rdv30ydOrV6x0ZdvP+pZR/Wv3//WgVEU6dOzcknn5zjjz8+FRUV+c53vpOf/vSnK5x76KGH5gc/+EGdH079SWy55Za5//77c8IJJ2T8+PGpqqrK9ttvn6uvvrpGkNalS5fcc889+e53v5szzzwza621Vr7xjW9kvfXWy9e//vUaaw4fPjzrrbdezjzzzPz0pz/NokWLsv7666dfv34ZNWpUnepr3rx5pk+fnu9973sZP358WrVqlf322y/f+c53Vvp7/XHKy8tz44035vvf/36uvvrqlJWVZZ999snZZ5+dbbbZ5hOtWVu/+tWv0qNHj0yaNCm/+93v0rlz55xwwgkrvC2yNjp37pyLL74448ePz9e//vUsW7Ys99xzj4AIgCavrOSJeAAAH+m8887LkUcemeeff36Fn0IFALC6ExABAHyEUqmUrbbaKmuttVbuueeexi4HAKBBuMUMAGAF5s+fnxtvvDH33HNP/vGPf+SGG25o7JIAABqMHUQAACvw/PPPp3v37unQoUO+9a1v5X//938buyQAgAYjIAIAAAAouPLGLgAAAACAxiUgAgAAACg4AREAAABAwQmIqKGqqirPPfdcqqqqGrsUWGX0PUWk7ykaPU8R6XuKSN9/cgIiAAAAgIITEAEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAghMQAQAAABScgAgAAACg4AREAAAAAAVX0dgFAAAAAKufeUNnr7L3Wndaz1X2Xp/UuHHjcv311+fvf/97Y5fyidhBBAAAAHxm9OjRI82aNUtZWdkKv8aNG5ck+d3vfpcddtgh7du3T7t27bLlllvm+9//fvU6kyZNSllZWTbffPPl3mPatGkpKyvLRhttVD12zDHH5K677mrgq2s4dhABAAAAnxkPPfRQNthgg5SXl2fq1Kk5+eST89RTT1Ufr6yszF133ZUDDjgg//u//5t99tknZWVleeKJJ3LHHXfUWKtt27Z55ZVX8sc//jFf+tKXqscvu+yydO3atcbcysrKVFZWNth1lUqlLFu2LBUVDRPl2EEEAAAAfGZ06tQpnTt3TufOndO+ffuUlZVVv+7cuXMqKyszffr09OnTJ8cee2w222yzbLrpptl3331z4YUX1liroqIiw4cPz+WXX149NmfOnMyYMSPDhw+vMXfcuHHZeuutkyQLFy7MlltumdGjR1cfnz17dtq1a1e9VlVVVcaPH5/u3bundevW2WqrrXLddddVz58xY0bKyspy6623pnfv3mnZsmUeeOCB+v52VRMQAQAAAIXSuXPnPP7443nsscc+du5hhx2Wa6+9NgsWLEjy31vP9thjj6y77rorPadVq1aZPHlyrrjiitxwww1ZtmxZDjnkkOy222457LDDkiTjx4/PlVdemYsvvjiPP/54jjzyyBxyyCG59957a6x1/PHH58wzz8w///nP/M///M+nuOqP5hYzAAAAoFC++93v5v7778/nP//5dOvWLTvssEN23333HHzwwWnZsmWNudtss0169OiR6667Ll/72tcyadKknHPOOXn22Wc/8j223nrrnH766Tn88MNz4IEH5oUXXshNN92UJFm0aFHOOOOM3HnnndW3rvXo0SMPPPBAJkyYkP79+1ev8+Mf/zi77bZbPX8HlmcHEQAAAFAobdu2zc0335xZs2blxBNPTGVlZY4++uh88YtfrN4p9EGHHXZYJk6cmHvvvTfz58/PoEGDavU+Rx99dDbddNP84he/yOWXX5611lorSTJr1qwsWLAgu+22W/WziyorK3PllVdm9uyanw633XbbffoLrgU7iAAAAIBC6tmzZ3r27JnDDz88P/rRj7Lppptm6tSpGTVqVI15Bx98cH7wgx9k3Lhx+drXvlbrB0W/8sorefrpp9OsWbM888wz2WOPPZIk7777bpLk5ptvzvrrr1/jnA/vYGrbtu0nvbw6ERABAAAAhbfRRhulTZs2mT9//nLH1lxzzeyzzz659tprc/HFF9d6zcMOOyyf//zn8/Wvfz1HHHFEdt1112y++ebZYost0rJly7z44os1bidrTAIiAAAAoFDGjRuXBQsWZNCgQenWrVvefPPNnH/++VmyZMlKn/czadKkXHTRRdW3iX2cCy+8MH/84x/z6KOPZsMNN8zNN9+cgw8+OH/605/Srl27HHPMMTnyyCNTVVWVvn375q233sqDDz6YNdZYIyNGjKjPy60VAREAAABQZ+tO69nYJXxi/fv3z4UXXphDDz008+bNS8eOHbPNNtvk9ttvz2abbbbCc1q3bp3WrVvXav0nn3wyxx57bC677LJsuOGGSZKLLroo//M//5OTTjopZ511Vk477bR06tQp48ePz7PPPpsOHTpk2223zQ9/+MN6u866KCuVSqVGeWeapKqqqrzwwgvp1q1byss9w5xi0PcUkb6naPQ8RaTvKSJ9/8n5bgEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDifYsaKnbhmsmT+p1/np0s+/Rqwquh7AACgoOwgAgAAACg4AREAAABAwQmIAAAAAApOQAQAAABQcB5SDQAAANTdsc1X3XvV8wfBjBw5Mm+++Wauv/76el13dWYHEQAAAPCZ0aNHjzRr1ixlZWUr/Bo3blzOO++8TJo0qfqckSNHVh9v3rx51l133ey22265/PLLU1VVVWP9jTbaaIXrnnnmmav4SuuXHUQAAADAZ8ZDDz2UDTbYIOXl5Zk6dWpOPvnkPPXUU9XHKysrU1lZudx5e+yxRyZOnJhly5Zl3rx5ue222zJ27Nhcd911ufHGG1NR8X8Ryo9//OMcccQRNc5v165dw13UKiAgAgAAAD4zOnXqlM6dO6e8vDzt27dPWVlZOnfuXGPOim4xa9myZfW89ddfP9tuu2122GGH7LLLLpk0aVIOP/zw6rnt2rVbbs3VnVvMAAAAAFZg5513zlZbbZXf/va3jV1KgxMQAQAAAKzE5z73uTz//PM1xo477rjqW9Xe/7r//vsbp8B64hYzAAAAgJUolUopKyurMXbsscdm5MiRNcbWX3/9VVhV/RMQAQAAAKzEP//5z3Tv3r3G2Nprr52NN964kSpqGG4xAwAAAFiBu+++O//4xz/y1a9+tbFLaXB2EAEAAACFt2jRosydO7fGx9yPHz8+e+21Vw499NAac995553MnTu3xlibNm2yxhprrMqS65WACAAAAKi7ny5p7Arq1W233ZYuXbqkoqIiHTt2zFZbbZXzzz8/I0aMSHl5zRuwTj755Jx88sk1xsaMGZOLL754VZZcrwREAAAAwGfSyJEjl3uYdJJMmjRpudcfHluZD3+i2WeFZxABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACg4AREAAABAwfkUs8+IeUNn18s6pZZJTpMbAgAAQJFIAgAAAAAKTkAEAAAAUHACIgAAAICCExABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACg4AREAAABAwQmIAAAAAApOQAQAAABQcAIiAAAAgIITEAEAAAAUXKMGRIsXL86pp56awYMHp3///hk5cmQeffTRJMn06dOz/fbbp1+/ftVfc+fOTZK89dZbGT16dHbeeedcfvnl1estXbo0I0aMqJ4HAAAAwMdr1IBo2bJlWW+99XLZZZflnnvuyUEHHZQjjzwyCxYsSJL07t07999/f/VX586dkyS/+93vstVWW2X69Om5+eab89prryVJpk2bln79+lXPAwAAAODjVTTmm7du3TpHHHFE9euBAwfm3HPPzQsvvPCR57388svZaaed0rZt22y22WaZO3duysvLc/PNN9fYUbQiixcvzuLFi2uMVVRUpEWLFp/8QpqAUsv6Xaeqedv6WbCqqn7WgQZU9f/7VN9TJNV9r18pCD1PEel7ikjfL6+8vHZ7gxo1IPqwF198MW+//XY23HDDzJo1K//4xz+yyy67ZM0118wBBxyQ/fffP0nSvXv3PPzww+nVq1dmzZqVDTbYIOeff35Gjx79sUHPxIkTc+mll9YYGzp0aIYNG9Zg17VKnFa/m8H+NfKB+lnoY8I+aEr0PUX0r3/9q7FLgFVKz1NE+p4i0vf/p3v37rWaV1YqlUoNXEutLFy4MGPGjEmfPn0yevTo/Pvf/05ZWVk6d+6cJ554Isccc0yOPfbY7LLLLlm4cGHGjx+fZ555JgceeGB69OiRyy67LCeffHLOOOOMvPXWWznssMOyww47LPc+n9UdRK8c+ly9rFNqmSw8sTwbTuqb8iXzP/2Cp//n068BDayqqir/+te/9D2Fou8pmuqe33DDWv+fVFjd6XuKSN8vb7XaQbR06dIcf/zx2XDDDatvOVt//fWrj/fq1SsHHnhg7rnnnuyyyy5p1apVTj311CRJqVTK6NGjc9JJJ2XixInZZZdd0rdv34waNSpTp05d7hvRokWL1T4MWpGyRfW7XvmS+fXzDwY/kKxG9D1FpO8pmvLycv9goHD0PUWk7+uu0b9bVVVVOemkk1JWVpZx48alrKxshfPKysqyos1ON954Y7beeut07do1zz//fDbffPNUVlamsrIyb7zxRkOXDwAAALDaa/SA6Iwzzsjrr7+eM888MxUV/7eh6Q9/+EN1wPPkk09m6tSp2XHHHWuc++6772batGk57LDDkiSdO3fOww8/nNdffz2vvPJK2rdvv+ouBAAAAGA11ai3mL388su5/vrr07Jly+y6667V4+eff34eeuihnHLKKXnvvfeyzjrr5NBDD83AgQNrnD9hwoQceuihad26dZJkxIgROeaYY3LxxRdn7NixNQInAAAAAFasUROULl26ZObMmSs8ts022+TII4/8yPOPPvroGq/XX3/9XHPNNfVWHwAAAEARNPotZgAAAAA0LgERAAAAQMEJiAAAAAAKTkAEAAAAUHACIgAAAICCExABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACg4AREAAABAwVU0dgEAAAAA9erENZMl8z/9Oj9d8unXWE3YQQQAAABQcAIiAAAAgIITEAEAAAAUnIAIAAAAoOA8pBoAAABoVPOGzq6XdUotk5xmL8wn4bsGAAAAUHACIgAAAICCExABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACg4AREAAABAwQmIAAAAAApOQAQAAABQcAIiAAAAgIKraOwCAAAAaEAnrpksmf/p1/npkk+/BtBk2UEEAAAAUHACIgAAAICCExABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACg4AREAAABAwQmIAAAAAApOQAQAAABQcAIiAAAAgIITEAEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAghMQAQAAABScgAgAAACg4AREAAAAAAVX0dgFAAC1N2/o7HpZp9QyyWn+PxEAAP/lb4YAAAAABScgAgAAACg4AREAAABAwQmIAAAAAApOQAQAAABQcAIiAAAAgIITEAEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAghMQAQAAABScgAgAAACg4AREAAAAAAUnIAIAAAAoOAERAAAAQMEJiAAAAAAKTkAEAAAAUHACIgAAAICCExABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACg4AREAAABAwVU0dgEAn9S8obPrZZ1SyySnycsBAIDi8i8iAAAAgIITEAEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAghMQAQAAABScgAgAAACg4AREAAAAAAUnIAIAAAAoOAERAAAAQMEJiAAAAAAKrqKxCwAAgFXmxDWTJfM//To/XfLp1wCAJsQOIgAAAICCExABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACg4AREAAABAwQmIAAAAAAquUQOixYsX59RTT83gwYPTv3//jBw5Mo8++mj18UmTJmXXXXfNzjvvnPPOOy+lUilJ8tZbb2X06NHZeeedc/nll1fPX7p0aUaMGJG5c+eu8msBAAAAWF01akC0bNmyrLfeernssstyzz335KCDDsqRRx6ZBQsW5IEHHsi0adMyadKkXHvttfnDH/6QG264IUnyu9/9LltttVWmT5+em2++Oa+99lqSZNq0aenXr186d+7cmJcFAAAAsFqpaMw3b926dY444ojq1wMHDsy5556bF154Ibfcckv222+/bLDBBkmSQw45JNOnT8++++6bl19+OTvttFPatm2bzTbbLHPnzk15eXluvvnmGjuKVmTx4sVZvHhxjbGKioq0aNGi/i9wFSq1rN91qpq3rZ8Fq6rqZx1YAX1PEel7+GSq/n+P6nmKRN+zOvF3nIZTXl67vUFlpffv22oCXnzxxRx44IG5/fbbc8QRR+Qb3/hG+vfvnyT55z//me985zu566678utf/zqvvvpqRo0alcMOOyyXXHJJzj///AwYMCA77rjjR77HhAkTcumll9YYGzp0aIYNG9Zg1wUAAADQGLp3716reY26g+iDFi5cmJNOOikjR45MZWVlFixYkLZt/y/xa9u2bd57770kyb777pvx48dn9OjROeSQQzJnzpy88cYb+fznP59jjz02b731Vg477LDssMMOy73PqFGjcvDBB9cY+yzsIHrl0OfqZZ1Sy2ThieXZcFLflC+Z/+kXPP0/n34NWAl9TxHpe/hkqqqq8q9//UvPUyj6ntWJv+M0viYREC1dujTHH398Ntxww+pbztq0aZP58//vN3P+/Plp3bp1kqRVq1Y59dRTkySlUimjR4/OSSedlIkTJ2aXXXZJ3759M2rUqEydOnW5rVQtWrRY7cOgFSlbVL/rlS+ZXz8/TLXcygafhL6niPQ9fDp6niLS96wO/B2n8TX6lVZVVeWkk05KWVlZxo0bl7KysiT/3QI1a9as6nmzZ89Oz549lzv/xhtvzNZbb52uXbvm+eefz+abb57KyspUVlbmjTfeWGXXAQAAALC6avSA6Iwzzsjrr7+eM888MxUV/7ehadCgQfntb3+bOXPm5PXXX8/kyZMzaNCgGue+++67mTZtWg477LAkSefOnfPwww/n9ddfzyuvvJL27duv0msBAAAAWB016i1mL7/8cq6//vq0bNkyu+66a/X4+eefn759+2b//ffPiBEjUlVVlX333TdDhgypcf6ECRNy6KGHVt96NmLEiBxzzDG5+OKLM3bs2BqBEwAAAAAr1qgJSpcuXTJz5syVHh81alRGjRq10uNHH310jdfrr79+rrnmmnqrDwAAAKAIGv0WMwAAAAAal4AIAAAAoOAERAAAAAAFJyACAAAAKDgf8wUAQJM2b+jsT71GqWWS0/y/UQBYGf+VBAAAACg4AREAAABAwQmIAAAAAApOQAQAAABQcAIiAAAAgIITEAEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAghMQAQAAABScgAgAAACg4AREAAAAAAUnIAIAAAAoOAERAAAAQMEJiAAAAAAKTkAEAAAAUHACIgAAAICCq2jsAgAAAPg/84bOrpd1Si2TnGZPAFA7/rQAAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAghMQAQAAABScgAgAAACg4AREAAAAAAUnIAIAAAAoOAERAAAAQMEJiAAAAAAKTkAEAAAAUHACIgAAAICCExABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACg4AREAAABAwQmIAAAAAApOQAQAAABQcAIiAAAAgIITEAEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAghMQAQAAABScgAgAAACg4AREAAAAAAUnIAIAAAAoOAERAAAAQMEJiAAAAAAKTkAEAAAAUHACIgAAAICCExABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACg4AREAAABAwQmIAAAAAApOQAQAAABQcAIiAAAAgIITEAEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAgqt1QHTOOefk17/+dfXrv/71r3nqqaeqX19wwQU59thj67c6AAAAABpcrQOia665Jrfffnv16zFjxuSss86qfv3Xv/419957b/1WBwAAAECDc4sZAAAAQMEJiAAAAAAKrqIukxcvXpy5c+emVCrVeP3+rwEAAABY/dQpIHr66aezzz77JEnKyspqvAYAAABg9VSngOj9nUMrU1ZW9qmKAQAAAGDVq3VAdMQRRzRkHQAAAAA0kloHRKNHj27IOgAAAABoJHW6xeyJJ57IbbfdliTZY489ssUWWzRIUQAAAACsOrUOiB599NGMGTMmy5YtS5JMmzYtl1xyST7/+c83WHEAAAAANLzy2k6cOHFili5dmlKplFKplKVLl2bixIkNWRsAAAAAq0CtA6KnnnoqFRUVOffcc3PuueemWbNmefLJJxuyNgAAAABWgVrfYvb6669nk002Sd++fZMkPXv2zKxZsxqsMAAAAABWjVrvIKqqqkqLFi2qX7do0SKlUqlBigIAAABg1anTp5g99dRTGTJkSJLktddeS5Lq1++74YYb6qk0AAAAAFaFOgVES5YsyUsvvVRj7IOvy8rK6qcqAAAAAFaZWgdE22yzjQAIAAAA4DOo1gHRJZdc0pB1AAAAANBIav2Q6o+yaNGi3HbbbfnOd75Tp/Ouu+66HHzwwdl+++0zYcKE6vGZM2fmC1/4Qvr161f99be//S3Jf29zO/roozNgwICceeaZNdYbO3ZsHnvssU9/QQAAAAAFUqdnEH3YI488kptuuil33nln5s+fX+fz11577YwePTq33XbbcsfWX3/9XH/99cuN33333WnTpk1uv/32fPvb385jjz2WXr16ZcaMGVl77bXTq1evT3IpAAAAAIVV54DolVdeyU033ZSbbropc+bMSZLqj7vfcMMN67TWgAEDkiQPPvhgrc95+eWXs80226RFixbZaqut8tJLL2WTTTbJr371q1xwwQV1en8AAAAA6hAQ3XbbbZk+fXpmzpyZUqlUHQol//30silTpmTjjTeut8LmzZuX3XbbLZWVlRk0aFAOO+ywNGvWLN27d8/tt9+ewYMH55FHHsnAgQNz1VVXZa+99krHjh0/dt3Fixdn8eLFNcYqKirSokWLequ9MZRa1u86Vc3b1s+CVVX1sw6sgL6niPQ9RVQffa/nWZ34s54i0vcNp7y8dk8XKit9MOn5CF/4whdSVlaWUqmUNddcM7vvvnsGDRqUb33rW5k/f37+/Oc/f+JizzjjjKy11loZM2ZMkuS1117Lu+++m65du+b555/P8ccfn3322SeHHHJIqqqqcu655+bhhx/OwIEDs+eee+aEE07IBRdckJ/97Gd56aWXMmTIkAwePHiF7zVhwoRceumlNcaGDh2aYcOGfeL6AQAAAJqi7t2712penQOizTffPCeddFL1bqGddtqp3gOiD/v973+fqVOn5vLLL1/u2PHHH59hw4bl6aefzoIFC/K1r30tI0eOzIUXXpgOHTosN/+zuoPolUOfq5d1Si2ThSeWZ8NJfVO+pO7PlVrO6f/59GvASuh7ikjfU0T10fd6ntWJP+spIn3fcGq7g6hOzyAqlUr55z//meHDh+dzn/tcBg0alKpVsN1qZRfz8MMPp1mzZtl2223z+9//PjvttFOaN2+ebt26Zc6cOSsMiFq0aLHah0ErUraoftcrXzK/fn6YatmI8Enoe4pI31NE9dn3ep7VgT/rKSJ93/hqfaU33HBDDj/88HTp0qU6KDrnnHPy3nvvJfnvR9a/9tprdXrzpUuXZtGiRamqqsqyZcuyaNGiLFu2LDNnzszcuXOTJC+++GIuu+yy7Ljjjsude+GFF2bs2LFJki5dumTmzJlZsGBBnnrqqXTu3LlOtQAAAAAUVa0DovXWWy9jxozJDTfckIsvvjiDBw9Oq1atqh9Y/ZOf/GSlz/1Zmcsuuyx9+vTJ9ddfn8svvzx9+vTJLbfckieffDKjRo1K3759853vfCcDBgzIIYccUuPca6+9NgMGDMg666yTJNlvv/3y6KOPZq+99sqgQYOy9tpr16kWAAAAgKKq88fcJ0nv3r3Tu3fvHHfccbnzzjtz00035W9/+1tq+TijamPGjFnpc4c+HAh92PDhw2u8bt++fS655JI6vT8AAAAAnzAgel/r1q2z9957Z++9986///3v3HzzzfVVFwAAAACrSK0DoiVLlnzk8XXWWSejRo361AUBAAAAsGrVOiDq06fPx84pKyvLQw899KkKAgAAAGDVqnVAVNfnCwEAAACweqjTM4jKyspSUVGRnXfeOV27dm2omgAAAABYhWodEB1yyCG58cYb8/bbb+eOO+7I9ttvn2HDhqVv374NWR8AAAAADay8thPHjh2bW265JT/60Y/So0eP/PGPf8xRRx2V/fbbL9dcc02qqqoask4AAAAAGkitA6IkadmyZfbdd99cc801Ofvss9OmTZvMmTMn5557bt55552GqhEAAACABlSnZxAlybPPPptrr702t956axYsWJBmzZplwIABadOmTUPUBwAAAEADq3VANGPGjFx77bWZOXNmSqVSOnbsmAMOOCD7779/1llnnYasEQAAAIAGVOuA6Nhjj63xKWa77757WrRokWeffTbPPvts9bwddtihQQoFAAAAoGHU+RazpUuX5vbbb8/tt9++3LGysrI89NBD9VIYAAAAAKtGnQKiUqnUUHUAAAAA0EhqHRBdfPHFDVkHAAAAAI2k1gFR7969G7IOAAAAABpJeWMXAAAAAEDjEhABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACi4Wn+K2Qe98MIL+ctf/pL//Oc/KZVKNY4dccQR9VIYAAAAAKtGnQOiG264IWecccZywdD7BEQAAAAAq5c6B0SXX355qqqqGqIWAAAAABpBnQOi119/PZWVlbn00kvTvXv3NGvWrCHqAgAAAGAVqfNDqrfbbrusscYa2XjjjYVDAAAAAJ8Bdd5BtOuuu+Z///d/c8IJJ2SPPfZIu3btahzfdttt6604AAAAABpenQOiU089NWVlZbnrrrty11131ThWVlaWhx56qN6KAwAAAKDhfaKPuV/ZJ5gBAAAAsPqpc0B04403NkQdAAAAADSSOgdEXbp0aYg6AAAAAGgkn+gWs1mzZuWuu+7Kq6++mmXLllWPl5WV5eSTT6634gAAAABoeHUOiP7whz/k6KOPrhEMJf99LpGACAAAAGD1U+eAaOLEiVm6dGnatGmTBQsWpHnz5ikrK0uzZs3SsWPHhqgRAAAAgAZUXtcTnn766bRp0yY33XRTkuRzn/tcrrvuujRv3jzHH398vRcIAAAAQMOqc0C0ePHidO3aNe3atUt5eXkWL16cLl26pFOnTvn5z3/eACUCAAAA0JDqfItZu3btMn/+/CRJ+/btM3v27EyaNCkvvPBCmjVrVu8FAgAAANCw6ryDqGvXrpk7d27efffdfP7zn8/SpUtz0UUXZdmyZdl4440bokYAAAAAGlCddxAdccQRefbZZ/Puu+9m7NixefbZZzNnzpyss846+cEPftAQNQIAAADQgOocEG2//fbZfvvtq1//7ne/y1tvvZX27dvXa2EAAAAArBp1DojeN3PmzPzjH/9Iu3btsscee2Tu3LlZc80106JFi/qsDwAAAIAGVueAaOHChTnqqKMyc+bMJEmvXr2y5ppr5vjjj8+3vvWtjBw5sr5rBAAAAKAB1fkh1b/85S/z8MMPp1QqpVQqJUn69u2b5s2b58EHH6z3AgEAAABoWHUOiO688860bNkyU6ZMqR5r0aJFOnfunBdffLFeiwMAAACg4dU5IPrPf/6Trl27ZpNNNqkxXlFRkXfeeafeCgMAAABg1ahzQLT22mvnxRdfzJw5c6rHnnrqqTz//PPp1KlTvRYHAAAAQMOrc0DUv3//LFq0KAcccEDKysry1FNPZcSIESmVStlxxx0bokYAAAAAGlCdA6JvfOMb2WSTTbJ48eKUSqUsXrw4y5Yty8Ybb5wxY8Y0RI0AAAAANKA6f8x9ZWVlrrzyytx22215/PHHkyRbbrllBg4cmObNm9d7gQAAAAA0rDoHRMl/H0i91157Za+99qrvegAAAABYxWodEF144YW1mvftb3/7ExcDAAAAwKpX64Bo0qRJKSsr+9h5AiIAAACA1UudbzErlUoNUQcAAAAAjaTWAVGLFi2yePHiNG/ePLvssksOOOCA9OrVqyFrAwAAAGAVqPXH3N9yyy359re/nbXWWiu33XZbDjvssIwYMSK33HJLli5d2pA1AgAAANCAah0QtW/fPiNHjswNN9yQn/zkJ+ndu3eeeOKJjBs3LoMHD86CBQsask4AAAAAGkitA6LqE8rL06tXr3z+859PmzZtUiqV8sYbb2TZsmUNUR8AAAAADaxOD6n++9//nqlTp2bGjBlZunRpWrduna985SsZNmxY2rVr11A1AgAAANCAah0QDR8+PLNmzUqSrLfeehk2bFj22WefVFZWNlhxAAAAADS8WgdEzzzzTMrKylJRUZEOHTrkzjvvzJ133rncvMsvv7xeCwQAAACgYdXpFrMkWbJkSZ544okkSalUqnGsrKysfqoCAAAAYJWpdUC0zTbbCIAAAAAAPoNqHRBdcsklDVkHAAAAAI2kzh9zDwAAAMBni4AIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAghMQAQAAABScgAgAAACg4AREAAAAAAUnIAIAAAAoOAERAAAAQMEJiAAAAAAKTkAEAAAAUHACIgAAAICCExABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACg4AREAAABAwQmIAAAAAApOQAQAAABQcAIiAAAAgIITEAEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAghMQAQAAABScgAgAAACg4AREAAAAAAUnIAIAAAAoOAERAAAAQMEJiAAAAAAKrlEDouuuuy4HH3xwtt9++0yYMKHGsenTp2fQoEHp379/Tj311CxZsiRJsmTJkhx99NEZMGBAzjzzzBrnjB07No899tgqqx8AAADgs6BRA6K11147o0ePzs4771xjfNasWTnnnHPy05/+NDfffHPmzZuXX/3qV0mSu+++O23atMntt9+e2bNnVwdCM2bMyNprr51evXqt8usAAAAAWJ1VNOabDxgwIEny4IMP1hi/7bbbsvPOO2fLLbdMkhx22GEZN25cvvnNb+bll1/ONttskxYtWmSrrbbKSy+9lE022SS/+tWvcsEFF3zsey5evDiLFy+uMVZRUZEWLVrUz0U1klLL+l2nqnnb+lmwqqp+1oEV0PcUkb6niOqj7/U8qxN/1lNE+r7hlJfXbm9QWalUKjVwLR/rjDPOyFprrZUxY8YkSY466qh88YtfzIEHHpgkefPNN7Prrrvmvvvuy8MPP5zbb789J598cr7zne/kBz/4Qe69995UVlZWz/8oEyZMyKWXXlpjbOjQoRk2bFj9XxgAAABAI+revXut5jXqDqKVee+999K27f+lfZWVlUmSBQsWpF+/fpk5c2ZGjBiRgQMHpl27dnnwwQdzwQUXZNy4cXnppZcyZMiQDB48eIVrjxo1KgcffHCNsc/CDqJXDn2uXtYptUwWnlieDSf1TfmS+Z9+wdP/8+nXgJXQ9xSRvqeI6qPv9TyrE3/WU0T6vvE1yYCodevWmT///34j33333SRJmzZtUl5enqOPPrr62PHHH5/vfve7uemmm9K1a9f86Ec/ysiRI9OnT5906NBhubVbtGix2odBK1K2qH7XK18yv35+mGq5lQ0+CX1PEel7iqg++17PszrwZz1FpO8bX5O80h49emTWrFnVr2fPnp3OnTunTZs2NeY9/PDDadasWbbddts899xz2WKLLdK8efN069Ytc+bMWdVlAwAAAKyWGjUgWrp0aRYtWpSqqqosW7YsixYtyrJly7LHHnvk7rvvzj//+c+8++67ufzyy5e7ZWzp0qW58MILM3bs2CRJly5dMnPmzCxYsCBPPfVUOnfu3BiXBAAAALDaadSA6LLLLkufPn1y/fXX5/LLL0+fPn1yyy23ZOONN86RRx6Zo446KoMGDUqnTp3y9a9/vca51157bQYMGJB11lknSbLffvvl0UcfzV577ZVBgwZl7bXXboxLAgAAAFjtNOoziMaMGVP9yWUftvfee2fvvfde6bnDhw+v8bp9+/a55JJL6rU+AAAAgCJoks8gAgAAAGDVERABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACg4AREAAABAwQmIAAAAAApOQAQAAABQcAIiAAAAgIITEAEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAghMQAQAAABScgAgAAACg4AREAAAAAAUnIAIAAAAoOAERAAAAQMEJiAAAAAAKTkAEAAAAUHACIgAAAICCExABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACg4AREAAABAwQmIAAAAAApOQAQAAABQcAIiAAAAgIITEAEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAghMQAQAAABScgAgAAACg4AREAAAAAAUnIAIAAAAoOAERAAAAQMEJiAAAAAAKTkAEAAAAUHACIgAAAICCExABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACg4AREAAABAwQmIAAAAAApOQAQAAABQcAIiAAAAgIITEAEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAghMQAQAAABScgAgAAACg4AREAAAAAAUnIAIAAAAoOAERAAAAQMEJiAAAAAAKTkAEAAAAUHACIgAAAICCExABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACg4AREAAABAwQmIAAAAAApOQAQAAABQcAIiAAAAgIITEAEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAghMQAQAAABScgAgAAACg4AREAAAAAAUnIAIAAAAoOAERAAAAQMEJiAAAAAAKTkAEAAAAUHACIgAAAICCExABAAAAFJyACAAAAKDgBEQAAAAABdekA6LRo0fny1/+cvr165d+/frle9/7XpLkwQcfzJ577pn99tsvTz75ZPX8Rx99NEceeWRjlQsAAACwWqpo7AI+zoknnphBgwbVGDvvvPNy0UUX5eWXX85FF12U888/P1VVVfn5z3+eH//4x41UKQAAAMDqqckHRCuyaNGidO/ePeuvv37OOeecJMn111+f7bbbLhtssMFHnrt48eIsXry4xlhFRUVatGjRYPWuCqWW9btOVfO29bNgVVX9rAMroO8pIn1PEdVH3+t5Vif+rKeI9H3DKS+v3c1jZaVSqdTAtXxio0ePzrPPPpsk2XTTTXPkkUdmk002yaGHHpqTTz458+bNy/XXX5+TTjop3/zmN3PZZZelVatWH7nmhAkTcumll9YYGzp0aIYNG9Zg1wEAAADQGLp3716reU06IHrsscfSo0ePlJeXZ+rUqfn1r3+d6667Ls8991zOOeectGzZMj/84Q8zZcqU9O7dO61atcqVV16Zjh075oQTTkiHDh2WW/OzuoPolUOfq5d1Si2ThSeWZ8NJfVO+ZP6nX/D0/3z6NWAl9D1FpO8povroez3P6sSf9RSRvm84td1B1KRvMevVq1f1r0eMGJEbb7wx//jHP7LDDjvk8ssvT5I888wzefHFF3PMMcdk2LBhueKKK3LffffliiuuyNixY5dbs0WLFqt9GLQiZYvqd73yJfPr54eplo0In4S+p4j0PUVUn32v51kd+LOeItL3jW+1utIVpV7nnHNOjjrqqLz55ptp3759Kisrs8UWW2T27NmNUCEAAADA6qfJBkTvvPNO/vSnP2Xx4sVZsmRJJk+enLfffrvGrqLbbrstG2+8cXr27Jn27dtn7ty5ef311zNz5sx06dKlEasHAAAAWH002VvMli5dmgsvvDAvvPBCKioqsummm+a8885LZWVlkuS9997L1VdfnYsvvjjJf58jNGbMmBxwwAFZZ5118rOf/awxywcAAABYbTTZgKhjx4656qqrVnq8devWufrqq2uMDRkyJEOGDGno0gAAAAA+U5rsLWYAAAAArBoCIgAAAICCExABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACg4AREAAABAwQmIAAAAAApOQAQAAABQcAIiAAAAgIITEAEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAghMQAQAAABScgAgAAACg4AREAAAAAAUnIAIAAAAoOAERAAAAQMEJiAAAAAAKTkAEAAAAUHACIgAAAICCExABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACg4AREAAABAwQmIAAAAAApOQAQAAABQcAIiAAAAgIITEAEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAghMQAQAAABScgAgAAACg4AREAAAAAAUnIAIAAAAoOAERAAAAQMEJiAAAAAAKTkAEAAAAUHACIgAAAICCExABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACg4AREAAABAwQmIAAAAAApOQAQAAABQcAIiAAAAgIITEAEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAghMQAQAAABScgAgAAACg4AREAAAAAAUnIAIAAAAoOAERAAAAQMEJiAAAAAAKTkAEAAAAUHACIgAAAICCExABAAAAFJyACAAAAKDgBEQAAAAABScgAgAAACg4AREAAABAwQmIAAAAAApOQAQAAABQcAIiAAAAgIITEAEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAghMQAQAAABScgAgAAACg4AREAAAAAAUnIAIAAAAoOAERAAAAQMEJiAAAAAAKTkAEAAAAUHACIgAAAICCExABAAAAFFyTDojeeOONjB07Nn379s1XvvKV/PnPf06SPPjgg9lzzz2z33775cknn6ye/+ijj+bII49srHIBAAAAVktNOiA666yzstZaa+XOO+/M2LFjc8IJJ+Stt97Keeedl4suuijHHntsLrrooiRJVVVVfv7zn+foo49u5KoBAAAAVi9NNiBasGBBZsyYkTFjxqRVq1bp379/evbsmXvvvTeLFi1K9+7ds9122+Wll15Kklx//fXZbrvtssEGGzRy5QAAAACrl4rGLmBlXnzxxbRp0ybrrrtu9djGG2+cZ599Nu3bt8+sWbMyb968dO/ePW+//XZ+85vf5LLLLvvYdRcvXpzFixfXGKuoqEiLFi3q/RpWpVLL+l2nqnnb+lmwqqp+1oEV0PcUkb6niOqj7/U8qxN/1lNE+r7hlJfXbm9QWalUKjVwLZ/I3/72t5x88smZPn169diFF16Yt956K/vss0/OOeectGzZMj/84Q8zZcqU9O7dO61atcqVV16Zjh075oQTTkiHDh2WW3fChAm59NJLa4wdccQRGTNmTENfEgAAAECT1GR3ELVu3Trz58+vMTZ//vy0adMmvXr1yuWXX54keeaZZ/Liiy/mmGOOybBhw3LFFVfkvvvuyxVXXJGxY8cut+6oUaNy8MEH1xhb3XcPAQAAAHwaTfYZRF27ds2CBQvyyiuvVI/Nnj07PXr0qDHvnHPOyVFHHZU333wz7du3T2VlZbbYYovMnj17heu2aNEilZWVNb4ERAAAAECRNdmAqE2bNunfv38mTJiQhQsX5v7778+sWbPSv3//6jm33XZbNt544/Ts2TPt27fP3Llz8/rrr2fmzJnp0qVLI1YPAAAAsPposs8gSpI33ngjp5xySv7yl79k3XXXzXHHHZftt98+SfLee+/liCOOyMUXX5zKysokyQ033JALLrgg66yzTn72s59lvfXWa8zyAQAAAFYLTTogAgAAAKDhNdlbzAAAAABYNQREAAAAAAUnIAIAAAAoOAERAAAAQMEJiKiTvffeO3//+98buwxYZfQ8RaTvKSJ9T9HoeYpI3380AdFqbO+9907fvn3z3nvvVY8tXLgwO+64Y/bee+9Pvf706dPzrW996xOf/9e//jWjR49Ov379Mnr06E9dDzT1nr/iiisydOjQ7LjjjvnKV76SG2+88VPXBE2976+77roMGTIk/fv3z+DBgzNx4sRPXRM09b5/37vvvpuBAwfWy1oUW1Pv+QkTJmT77bdPv379qr/g02rqfZ8kDzzwQA444ID07ds3Q4YMySOPPPKp62rKBESruXXWWSczZsyofj1jxoysvfbajVfQB7Rq1Sr77bdfDj/88MYuhc+QptzzZWVlOf3003PPPffkrLPOyi9+8Qv/h4J60ZT7/stf/nKuvvrq3Hvvvbniiity66235oEHHmjssvgMaMp9/74JEyZkgw02aOwy+Ixo6j2/11575f7776/+gvrQlPv+6aefzk9/+tOceOKJue+++3LJJZdkvfXWa+yyGpSAaDU3cODA3HrrrdWvb7311uyxxx415syePTuHH354BgwYkEMOOaTGP1j33nvvXH311dl///0zYMCAnHXWWUmSOXPmZPz48fnLX/6Sfv36ZdiwYdXnPPbYY8vNX5Etttgie+65Zzp37lxPVwtNu+cPPfTQbLbZZmnWrFk22WSTfOELX8hjjz1WT1dOkTXlvl9vvfXSrl276tdlZWWZM2fOp71kaNJ9nySzZs3KI488kn322acerhaafs9DQ2jKfT9x4sSMGjUqn//851NeXp511103nTp1qqcrb5oERKu57bbbLrNnz84bb7yRN954I7NmzcoXv/jF6uNLlizJUUcdlV133TV33nlnDj300Bx11FF5++23q+fMmDEjv/rVrzJ16tTceeedmTlzZjbYYIOccMIJ6d27d+6///5ce+21HzkfVpXVpeeXLl2axx57LD169KjfbwCF1NT7/rbbbsuOO+6YPfbYIwsXLsyuu+7aMN8ICqWp9/3PfvazHHnkkSkv99dp6kdT7/m77roru+yyS4YPH5677767Yb4JFE5T7vvHH388b775Zvbdd98MHjw4Z599dhYvXtxw34wmwH/RVnPNmjXLzjvvnNtvvz233357dt555zRr1qz6+GOPPZaqqqoceOCBqaioyO67755u3brlD3/4Q/Wcgw46KB06dMi6666b3r175+mnn/7I96zrfKhPq0vPn3vuuenSpUu+9KUvffKLhf+vqff9Hnvskfvuuy+//vWvs/fee6dNmzaf/qIpvKbc97///e/TsWPHbLPNNvVzsZCm3fO77bZbrrvuuvz+97/Pd7/73Zx66ql2SVMvmnLfv/LKK7nrrrvyq1/9KpMnT87jjz+eK6+8sn4uvIkSEH0G7Lnnnvn973+f2267bbnteK+++mrWXXfdGmOdO3fOa6+9Vv16rbXWqv51q1atsmDBgo98v7rOh/rW1Ht+4sSJefjhh3PWWWelrKzsY68HaqOp932SbLzxxmnVqlUuvfTSj50LtdEU+/69997LhAkTMnbs2DpdC9RGU+z5JOnRo0fWXnvtVFRU5Etf+lIGDhyYe++9t9bXBR+lqfZ9y5Ytc8ABB2TttddOhw4dcvDBB+fBBx+s9XWtjgREnwFbbLFF3nrrrbz99tvZcsstaxzr1KlT5s2bV2Ns3rx5tXrwl3/Y0lQ15Z6/9tprc/311+fCCy9M+/btP/V68L6m3PcftHTpUs8got40xb5/8cUX89JLL2XEiBEZOHBgfvazn+Vvf/tbvvKVr3ziNeF9TbHnV8StldSnptr3PXv2rNf1VgcVjV0A9eOnP/3pCsd79eqVJJk6dWq++tWvZsaMGXnuuefy5S9/+WPX7NixY+bNm5elS5emoqLurVJVVZUlS5Zk6dKlKZVKWbRoUZo1a/aJ1oIPa4o9f9NNN2XixIm59NJLP/MPsKNxNNW+//KXv5yOHTvmqaeeyrRp03LooYfWeR1YmabW9z179sxNN91U/fqOO+7I3Xff7QG/1Jum1vNJcu+996Z3795p06ZN/vKXv+TWW2/N+eefX+d1YGWaYt/vvffeufbaa/PlL385FRUVmTJlSvr06VPndVYn/qX+GbGyB+E2b94855xzTsaPH5+LLrooG2ywQc4555ysscYaH7vmF77whay33nrZbbfdsu666+bXv/51nWr661//mm984xvVr/v06ZO99tor48aNq9M6sCJNsecvueSSvPHGGznooIOqx0aNGpXDDjusTuvAyjTFvn/sscdy/vnn57333staa62Vr3zlKzU+KQQ+rabW9xUVFTX+z3VlZWWaN2+eNddcs9ZrwEdpaj2f/PfDCMaNG5dly5ZlvfXWy49+9KNstdVWdVoDPkpT7Pt99903L730UoYOHZpmzZplt912+8z/T7CyUqlUauwiAAAAAGg8bh4FAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAghMQAQAAABScgAgAAACg4AREAAAAAAUnIAIAWI1st9122W677TJ9+vTGLgUA+AwREAEAfMjo0aOrg5iDDjqoxrE333wzffr0qT5+wQUX1Pv7T58+vXp9AIBVQUAEAPARnnnmmfz1r3+tfn399ddn0aJFjVgRAED9ExABAKxERUVFkmTq1KlJkmXLluW6666rHv+gt956K2eddVYGDx6c7bffPrvvvntOOumkzJ07t3rOhAkTst1222XvvffOnXfema9+9avp27dvjjjiiDz//PNJknHjxuXUU0+tPuf9nUQTJkyo8X7vvvtuTj311PTv3z977rlnfvWrX9X35QMABSIgAgBYiU033TTrr79+ZsyYkXnz5uW+++7L3Llzs8suu9SYt2jRoowePTrTpk3La6+9lm7dumX+/Pm59dZbM2rUqLzxxhs15r/yyis58cQTU1ZWlkWLFuVvf/tbfvzjHydJNthgg6y//vrVc3v16pVevXpl3XXXrbHGL37xi/zxj39M8+bN8+qrr+biiy/On/70pwb6TgAAn3UCIgCAlSgvL8/QoUOrdw69v5PogAMOqDHv97//fWbPnp0kOeuss3LttdfmsssuS3l5eV599dVce+21NeYvW7YsP/nJT3LddddVP+Po0UcfzcKFC3P44Yfn8MMPr547adKkTJo0Kfvuu2+NNTbbbLNMnz69xo6mhx9+uF6vHwAoDgERAMBHGDJkSFq3bp1rr702M2fOzOabb57/+Z//qTHniSeeSJK0atUqAwYMSJJ87nOfS7du3Wocf19lZWV23HHHJEmPHj2qxz+80+ij7LrrrmnevHk6dOiQNddcM0nyn//8p24XBwDw/wmIAAA+Qrt27bLnnntm/vz5SZbfPfRJ13xfs2bNqn9dKpU+1Rp1OR8A4IMERAAAH2PYsGFJko4dO2b33Xdf7vgWW2yRJFm4cGFmzJiRJHnyySfzwgsv1DheW61atar+9XvvvfdJSgYAqJPlP4IDAIAaNt5449x1111p1qxZWrRosdzxgQMHZvLkyZk9e3aOO+64dOvWLf/+979TVVWVTp06VQdMtbXRRhtV/3ro0KFZe+218/3vfz9bb731p7wSAIAVs4MIAKAW2rdvn8rKyhUea9myZS655JLqMOeFF15ImzZtsueee2bixInp2LFjnd5rk002yeGHH5611lorc+fOzWOPPZZ33nmnPi4DAGCFykpuVgcAAAAoNDuIAAAAAApOQAQAAABQcAIiAAAAgIITEAEAAAAUnIAIAAAAoOAERAAAAAAFJyACAAAAKDgBEQAAAEDBCYgAAAAACk5ABAAAAFBwAiIAAACAgvt/BfOnTo+wvLEAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "utils.plot_model_comparison([\"TSMixer\", \"TiDE\"],\n", + " [tsmixer_forecasts, tide_forecasts],\n", + " tests, FORECAST_HORIZON,TARGET,\n", + " top = top_25)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/forecasting/TSMixer/requirements.txt b/forecasting/TSMixer/requirements.txt new file mode 100644 index 0000000..e0267f4 --- /dev/null +++ b/forecasting/TSMixer/requirements.txt @@ -0,0 +1,11 @@ +u8darts[all]==0.30.0 +tensorflow==2.16.1 +torch==2.3.1 +scikit-learn==1.5.0 +pandas==2.2.2 +dask[dataframe]==2024.6.2 +seaborn==0.13.2 +tqdm==4.66.4 +jupyter==1.0.0 +numpy==1.26.4 +datasets==2.20.0 \ No newline at end of file diff --git a/forecasting/TSMixer/utils.py b/forecasting/TSMixer/utils.py new file mode 100644 index 0000000..1a5121a --- /dev/null +++ b/forecasting/TSMixer/utils.py @@ -0,0 +1,154 @@ +from darts.utils.timeseries_generation import datetime_attribute_timeseries +from darts.dataprocessing.transformers import MissingValuesFiller +import matplotlib.pyplot as plt +from darts import TimeSeries +import pandas as pd +import numpy as np + + +def create_dynamic_covariates(train_darts: list, dataframe: pd.DataFrame, forecast_horizon: int, + dynamic_covariates_names) -> list: + dynamic_covariates = [] + + # Ensure the Date column is in datetime format in both dataframes + dataframe['Date'] = pd.to_datetime(dataframe['Date']) + + for serie in train_darts: + # Extract the unique_id from the series to filter the DataFrame + unique_id = "".join(str(serie.static_covariates[key].item()) for key in serie.static_covariates) + + # Filter the DataFrame for the current series based on unique_id + filtered_df = dataframe[dataframe['unique_id'] == unique_id] + + # Generate time-related covariates + covariate = datetime_attribute_timeseries( + serie, + attribute="month", + one_hot=True, + cyclic=False, + add_length=forecast_horizon + ) + + # Add dynamic covariates that need interpolation + dyn_cov_interp = TimeSeries.from_dataframe( + filtered_df, + time_col="Date", + value_cols=dynamic_covariates_names, + freq=serie.freq, + fill_missing_dates=True + ) + + covariate = covariate.stack(MissingValuesFiller().transform(dyn_cov_interp)) + + dynamic_covariates.append(covariate) + + return dynamic_covariates + + +def mape_evaluation(prediction: pd.DataFrame, actuals: pd.DataFrame, target: str) -> list: + # Convert 'Date' columns to datetime if they aren't already + prediction['Date'] = pd.to_datetime(prediction['Date']) + actuals['Date'] = pd.to_datetime(actuals['Date']) + + # Merging prediction and actual sales data on 'Date' and 'unique_id' + prediction_w_mape = pd.merge(prediction, actuals[['Date', target, 'unique_id']], + on=['Date', 'unique_id'], how='left') + + # Calculating MAPE + prediction_w_mape['MAPE'] = abs(prediction_w_mape['forecast'] - prediction_w_mape[target]) / \ + prediction_w_mape[target] + + # Group by 'Date' and calculate the mean MAPE for each group + weekly_mape = prediction_w_mape.groupby('Date')['MAPE'].mean().tolist() + + # Ensuring the list is rounded to two decimal places + weekly_mape = [round(x, 2) for x in weekly_mape] + + return weekly_mape + + +def plot_model_comparison(model_names, model_forecasts, actuals, forecast_horizon, target, top=None): + if len(model_forecasts) != len(model_names): + raise ValueError("The number of model forecasts must match the number of model names") + + num_windows = len(model_forecasts[0]) + iteration_mapes = np.zeros((forecast_horizon, len(model_names))) + + # Loop through each model's forecasts + for model_idx, forecasts in enumerate(model_forecasts): + for time_window_idx in range(num_windows): + # Select the correct prediction and actuals based on whether filtering is needed + model_prediction = forecasts if num_windows == 1 else forecasts[time_window_idx] + actual_window = actuals if num_windows == 1 else actuals[time_window_idx] + + if top is not None: + model_prediction = model_prediction[model_prediction['unique_id'].isin(top['unique_id'])] + actual_window = actual_window[actual_window['unique_id'].isin(top['unique_id'])] + + mape_values = mape_evaluation(model_prediction, actual_window, target) + iteration_mapes[:, model_idx] += np.array(mape_values) + + iteration_mapes[:, model_idx] /= num_windows + + # Plotting + fig, ax = plt.subplots(figsize=(14, 8)) + indices = np.arange(forecast_horizon) + bar_width = 0.1 + + colors = ['#e854dc', '#ff7404', 'royalblue'] + + for i, model in enumerate(model_names): + ax.bar(indices + i * bar_width, iteration_mapes[:, i], width=bar_width, label=model,color=colors[i]) + + ax.set_xlabel('Month') + ax.set_ylabel('Mean MAPE') + ax.set_title('Mean MAPE by Model and Month') + ax.set_yticklabels(['{:.0f}%'.format(x * 100) for x in ax.get_yticks()]) + ax.set_xticks(indices + bar_width * (len(model_names) - 1) / 2) + ax.set_xticklabels([f'Month {i + 1}' for i in range(forecast_horizon)]) + ax.legend() + plt.show() + + +def transform_predictions_to_pandas(predictions, target: str, pred_list: list, quantiles: list, + convert: bool = True) -> pd.DataFrame: + """ + Receives as list of predictions and transform it in a data frame + Args: + predictions (list): list with predictions + target (str): column to forecast + pred_list (list): list with test df to extract time series id + Returns + pd.DataFrame: data frame with date, forecast, forecast_lower, forecast_upper and id + :param convert: + """ + + pred_df_list = [] + + for p, pdf in zip(predictions, pred_list): + temp = ( + p.quantile_df(quantiles[1]) + .reset_index() + .rename(columns={f"{target}_{quantiles[1]}": "forecast"}) + ) + temp["forecast_lower"] = p.quantile_df(quantiles[0]).reset_index()[f"{target}_{quantiles[0]}"] + temp["forecast_upper"] = p.quantile_df(quantiles[2]).reset_index()[f"{target}_{quantiles[2]}"] + + # add unique id + unique_id = "".join(str(pdf.static_covariates[key].item()) for key in pdf.static_covariates) + + temp["unique_id"] = unique_id + + if convert: + # convert negative predictions into 0 + temp[["forecast", "forecast_lower", "forecast_upper"]] = temp[ + ["forecast", "forecast_lower", "forecast_upper"] + ].clip(lower=0) + + # Reorder columns to make unique_id the first column + columns_order = ['unique_id'] + [col for col in temp.columns if col != 'unique_id'] + temp = temp[columns_order] + + pred_df_list.append(temp) + + return pd.concat(pred_df_list) From 721fdefef6d4622e3d3f46e970062236242735fd Mon Sep 17 00:00:00 2001 From: seblessa Date: Tue, 13 Aug 2024 18:17:38 +0100 Subject: [PATCH 2/5] update --- forecasting/TSMixer/notebook.ipynb | 42 ++++++++++++++++++++-------- forecasting/TSMixer/requirements.txt | 2 +- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/forecasting/TSMixer/notebook.ipynb b/forecasting/TSMixer/notebook.ipynb index bf6f708..911b9f7 100644 --- a/forecasting/TSMixer/notebook.ipynb +++ b/forecasting/TSMixer/notebook.ipynb @@ -1,5 +1,15 @@ { "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "# TSMixer\n", + "\n", + "Importing the necessary libraries and setting global variables for later use:" + ], + "id": "b622a6d4736727c4" + }, { "cell_type": "code", "execution_count": 13, @@ -57,6 +67,12 @@ "PIPELINE = Pipeline([SCALER, TRANSFORMER])" ] }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Loading of the dataset and preprocessing", + "id": "7b7673475963ce11" + }, { "cell_type": "code", "execution_count": 14, @@ -351,6 +367,12 @@ "print(f\"Total of years: {round((end_date-df[TIME_COL].min()).days / 365.25, 2)}\")" ] }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Creating the folds for the cross-validation", + "id": "56334a6e0946ffe0" + }, { "cell_type": "code", "execution_count": 17, @@ -420,7 +442,9 @@ "id": "5b7cc71995e5c24e", "metadata": {}, "source": [ - "# TSMixer" + "# TSMixer\n", + "\n", + "Now we will execute the TSMixer model for each fold:" ] }, { @@ -492,7 +516,9 @@ "id": "66328f11677e0d17", "metadata": {}, "source": [ - "# TiDE" + "# TiDE\n", + "\n", + "To compare the results, we will use the TiDE model:" ] }, { @@ -566,7 +592,9 @@ "id": "e526aa836b31ea7e", "metadata": {}, "source": [ - "# Evaluation" + "## Evaluation\n", + "\n", + "Now let's both models and compare the results:" ] }, { @@ -606,14 +634,6 @@ "top_25 = df.groupby(['unique_id']).agg({TARGET: 'sum'}).reset_index().sort_values(by=TARGET,ascending=False).head(25)" ] }, - { - "cell_type": "markdown", - "id": "ddb32d9011ac0fb7", - "metadata": {}, - "source": [ - "## Graph" - ] - }, { "cell_type": "code", "execution_count": 24, diff --git a/forecasting/TSMixer/requirements.txt b/forecasting/TSMixer/requirements.txt index e0267f4..5851c44 100644 --- a/forecasting/TSMixer/requirements.txt +++ b/forecasting/TSMixer/requirements.txt @@ -8,4 +8,4 @@ seaborn==0.13.2 tqdm==4.66.4 jupyter==1.0.0 numpy==1.26.4 -datasets==2.20.0 \ No newline at end of file +datasets==2.20.0 From fcdcb7796b8b815d7f2370735f19c601c3ca516c Mon Sep 17 00:00:00 2001 From: seblessa Date: Wed, 28 Aug 2024 17:01:12 +0100 Subject: [PATCH 3/5] update --- forecasting/TSMixer/README.md | 8 ++++---- forecasting/TSMixer/utils.py | 37 +++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/forecasting/TSMixer/README.md b/forecasting/TSMixer/README.md index 3a6df37..728a354 100644 --- a/forecasting/TSMixer/README.md +++ b/forecasting/TSMixer/README.md @@ -3,16 +3,16 @@ **Steps to run the code:** 1. Create a virtual environment with python - `conda create --name myenv python=3.12.4` -3. Activate in your new virtual environment +2. Activate in your new virtual environment - `conda activate myenv` -4. Install the required requirements +3. Install the required requirements - `pip install -r requirements.txt` -5. Run the notebook +4. Run the notebook ## Folder Structure: ├── TSMixer │ │──── requirements.txt <- package version for installing │──── utils.py <- helper functions - └──── notebook.ipynb <- notebook to run the code + └──── notebook.ipynb <- notebook to run the code ------------ diff --git a/forecasting/TSMixer/utils.py b/forecasting/TSMixer/utils.py index 1a5121a..c2c2d72 100644 --- a/forecasting/TSMixer/utils.py +++ b/forecasting/TSMixer/utils.py @@ -8,6 +8,16 @@ def create_dynamic_covariates(train_darts: list, dataframe: pd.DataFrame, forecast_horizon: int, dynamic_covariates_names) -> list: + """ + Create dynamic covariates for each series in the training set + Args: + train_darts (list): list with training series + dataframe (pd.DataFrame): historical data + forecast_horizon (int): forecast horizon + dynamic_covariates_names (list): list with dynamic covariates names + Returns: + list: list with dynamic covariates for each series + """ dynamic_covariates = [] # Ensure the Date column is in datetime format in both dataframes @@ -46,6 +56,15 @@ def create_dynamic_covariates(train_darts: list, dataframe: pd.DataFrame, foreca def mape_evaluation(prediction: pd.DataFrame, actuals: pd.DataFrame, target: str) -> list: + """ + Calculate the Mean Absolute Percentage Error (MAPE) for each week + Args: + prediction (pd.DataFrame): forecast data + actuals (pd.DataFrame): actual data + target (str): target column + Returns: + list: list with MAPE values for each week + """ # Convert 'Date' columns to datetime if they aren't already prediction['Date'] = pd.to_datetime(prediction['Date']) actuals['Date'] = pd.to_datetime(actuals['Date']) @@ -68,9 +87,21 @@ def mape_evaluation(prediction: pd.DataFrame, actuals: pd.DataFrame, target: str def plot_model_comparison(model_names, model_forecasts, actuals, forecast_horizon, target, top=None): - if len(model_forecasts) != len(model_names): - raise ValueError("The number of model forecasts must match the number of model names") + """ + Plot the Mean Absolute Percentage Error (MAPE) for each model by month + Args: + model_names (list): list of model names + model_forecasts (list): list of model forecasts + actuals (pd.DataFrame): actual sales data + forecast_horizon (int): forecast horizon + target (str): target column + top (pd.DataFrame): top performing model + """ + if len(model_forecasts) != len(model_names): + raise ValueError( + "The number of model forecasts must match the number of model names" + ) num_windows = len(model_forecasts[0]) iteration_mapes = np.zeros((forecast_horizon, len(model_names))) @@ -118,6 +149,8 @@ def transform_predictions_to_pandas(predictions, target: str, pred_list: list, q predictions (list): list with predictions target (str): column to forecast pred_list (list): list with test df to extract time series id + quantiles (list): list with quantiles + convert (bool): convert negative predictions into 0 (default is True) Returns pd.DataFrame: data frame with date, forecast, forecast_lower, forecast_upper and id :param convert: From 43a2662ff212dfa8c4e85b4c5aab301653db3497 Mon Sep 17 00:00:00 2001 From: seblessa Date: Wed, 28 Aug 2024 17:04:57 +0100 Subject: [PATCH 4/5] update --- forecasting/TSMixer/utils.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/forecasting/TSMixer/utils.py b/forecasting/TSMixer/utils.py index c2c2d72..46281be 100644 --- a/forecasting/TSMixer/utils.py +++ b/forecasting/TSMixer/utils.py @@ -86,7 +86,14 @@ def mape_evaluation(prediction: pd.DataFrame, actuals: pd.DataFrame, target: str return weekly_mape -def plot_model_comparison(model_names, model_forecasts, actuals, forecast_horizon, target, top=None): +def plot_model_comparison( + model_names: list, + model_forecasts: list, + actuals: pd.DataFrame, + forecast_horizon: int, + target: str, + top: pd.DataFrame = None, +): """ Plot the Mean Absolute Percentage Error (MAPE) for each model by month Args: @@ -141,8 +148,13 @@ def plot_model_comparison(model_names, model_forecasts, actuals, forecast_horizo plt.show() -def transform_predictions_to_pandas(predictions, target: str, pred_list: list, quantiles: list, - convert: bool = True) -> pd.DataFrame: +def transform_predictions_to_pandas( + predictions: list, + target: str, + pred_list: list, + quantiles: list, + convert: bool = True, +) -> pd.DataFrame: """ Receives as list of predictions and transform it in a data frame Args: From 24dd543ab59d5e2b80b5fe36a0b699a33114e4c2 Mon Sep 17 00:00:00 2001 From: seblessa Date: Wed, 28 Aug 2024 17:16:25 +0100 Subject: [PATCH 5/5] update --- forecasting/TSMixer/utils.py | 46 +++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/forecasting/TSMixer/utils.py b/forecasting/TSMixer/utils.py index 46281be..63d6a6c 100644 --- a/forecasting/TSMixer/utils.py +++ b/forecasting/TSMixer/utils.py @@ -55,16 +55,19 @@ def create_dynamic_covariates(train_darts: list, dataframe: pd.DataFrame, foreca return dynamic_covariates -def mape_evaluation(prediction: pd.DataFrame, actuals: pd.DataFrame, target: str) -> list: +def mape_evaluation( + prediction: pd.DataFrame, + actuals: pd.DataFrame, + target: str) -> list: + """ + Calculate the Mean Absolute Percentage Error (MAPE) for each week + Args: + prediction (pd.DataFrame): forecast data + actuals (pd.DataFrame): actual data + target (str): target column + Returns: + list: list with MAPE values for each week """ - Calculate the Mean Absolute Percentage Error (MAPE) for each week - Args: - prediction (pd.DataFrame): forecast data - actuals (pd.DataFrame): actual data - target (str): target column - Returns: - list: list with MAPE values for each week - """ # Convert 'Date' columns to datetime if they aren't already prediction['Date'] = pd.to_datetime(prediction['Date']) actuals['Date'] = pd.to_datetime(actuals['Date']) @@ -87,13 +90,12 @@ def mape_evaluation(prediction: pd.DataFrame, actuals: pd.DataFrame, target: str def plot_model_comparison( - model_names: list, - model_forecasts: list, - actuals: pd.DataFrame, - forecast_horizon: int, - target: str, - top: pd.DataFrame = None, -): + model_names: list, + model_forecasts: list, + actuals: pd.DataFrame, + forecast_horizon: int, + target: str, + top: pd.DataFrame = None) -> None: """ Plot the Mean Absolute Percentage Error (MAPE) for each model by month Args: @@ -136,7 +138,7 @@ def plot_model_comparison( colors = ['#e854dc', '#ff7404', 'royalblue'] for i, model in enumerate(model_names): - ax.bar(indices + i * bar_width, iteration_mapes[:, i], width=bar_width, label=model,color=colors[i]) + ax.bar(indices + i * bar_width, iteration_mapes[:, i], width=bar_width, label=model, color=colors[i]) ax.set_xlabel('Month') ax.set_ylabel('Mean MAPE') @@ -149,11 +151,11 @@ def plot_model_comparison( def transform_predictions_to_pandas( - predictions: list, - target: str, - pred_list: list, - quantiles: list, - convert: bool = True, + predictions: list, + target: str, + pred_list: list, + quantiles: list, + convert: bool = True, ) -> pd.DataFrame: """ Receives as list of predictions and transform it in a data frame