{
  "cells": [
    {
      "cell_type": "code",
      "execution_count": 1,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 330
        },
        "id": "KmKX79qPmrao",
        "outputId": "6fe4b7ad-4cfe-4ef4-86f7-3abea192ee17"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Requirement already satisfied: openpyxl in d:\\timepassprojects\\col\\finl1\\.venv\\lib\\site-packages (3.1.5)\n",
            "Requirement already satisfied: et-xmlfile in d:\\timepassprojects\\col\\finl1\\.venv\\lib\\site-packages (from openpyxl) (2.0.0)\n",
            "Note: you may need to restart the kernel to use updated packages.\n"
          ]
        },
        {
          "data": {
            "text/html": [
              "<div>\n",
              "<style scoped>\n",
              "    .dataframe tbody tr th:only-of-type {\n",
              "        vertical-align: middle;\n",
              "    }\n",
              "\n",
              "    .dataframe tbody tr th {\n",
              "        vertical-align: top;\n",
              "    }\n",
              "\n",
              "    .dataframe thead th {\n",
              "        text-align: right;\n",
              "    }\n",
              "</style>\n",
              "<table border=\"1\" class=\"dataframe\">\n",
              "  <thead>\n",
              "    <tr style=\"text-align: right;\">\n",
              "      <th></th>\n",
              "      <th>Market</th>\n",
              "      <th>Sale Type</th>\n",
              "      <th>Dealer Code</th>\n",
              "      <th>Sale Date</th>\n",
              "      <th>Sale Month</th>\n",
              "      <th>Sale Year</th>\n",
              "      <th>Dummy ID</th>\n",
              "      <th>Part Qty.</th>\n",
              "      <th>Dealer Price</th>\n",
              "      <th>PRC_UNIT_WEIGHT</th>\n",
              "      <th>PRC_UNIT_CUBE</th>\n",
              "    </tr>\n",
              "  </thead>\n",
              "  <tbody>\n",
              "    <tr>\n",
              "      <th>0</th>\n",
              "      <td>NZL</td>\n",
              "      <td>OTC</td>\n",
              "      <td>22598</td>\n",
              "      <td>2023-09-05</td>\n",
              "      <td>9</td>\n",
              "      <td>2023</td>\n",
              "      <td>PartNo1</td>\n",
              "      <td>2</td>\n",
              "      <td>18.3792</td>\n",
              "      <td>0.02</td>\n",
              "      <td>0.000616</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>1</th>\n",
              "      <td>NZL</td>\n",
              "      <td>RO</td>\n",
              "      <td>34561</td>\n",
              "      <td>2025-07-16</td>\n",
              "      <td>7</td>\n",
              "      <td>2025</td>\n",
              "      <td>PartNo2</td>\n",
              "      <td>1</td>\n",
              "      <td>150.7110</td>\n",
              "      <td>0.19</td>\n",
              "      <td>0.001029</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>2</th>\n",
              "      <td>NZL</td>\n",
              "      <td>RO</td>\n",
              "      <td>34561</td>\n",
              "      <td>2025-07-16</td>\n",
              "      <td>7</td>\n",
              "      <td>2025</td>\n",
              "      <td>PartNo2</td>\n",
              "      <td>1</td>\n",
              "      <td>150.7110</td>\n",
              "      <td>0.19</td>\n",
              "      <td>0.001029</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>3</th>\n",
              "      <td>NZL</td>\n",
              "      <td>RO</td>\n",
              "      <td>34561</td>\n",
              "      <td>2025-07-16</td>\n",
              "      <td>7</td>\n",
              "      <td>2025</td>\n",
              "      <td>PartNo2</td>\n",
              "      <td>1</td>\n",
              "      <td>150.7110</td>\n",
              "      <td>0.19</td>\n",
              "      <td>0.001029</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>4</th>\n",
              "      <td>NZL</td>\n",
              "      <td>RO</td>\n",
              "      <td>45611</td>\n",
              "      <td>2025-07-09</td>\n",
              "      <td>7</td>\n",
              "      <td>2025</td>\n",
              "      <td>PartNo2</td>\n",
              "      <td>1</td>\n",
              "      <td>150.7110</td>\n",
              "      <td>0.19</td>\n",
              "      <td>0.001029</td>\n",
              "    </tr>\n",
              "  </tbody>\n",
              "</table>\n",
              "</div>"
            ],
            "text/plain": [
              "  Market Sale Type  Dealer Code  Sale Date  Sale Month  Sale Year Dummy ID  \\\n",
              "0    NZL       OTC        22598 2023-09-05           9       2023  PartNo1   \n",
              "1    NZL        RO        34561 2025-07-16           7       2025  PartNo2   \n",
              "2    NZL        RO        34561 2025-07-16           7       2025  PartNo2   \n",
              "3    NZL        RO        34561 2025-07-16           7       2025  PartNo2   \n",
              "4    NZL        RO        45611 2025-07-09           7       2025  PartNo2   \n",
              "\n",
              "   Part Qty.  Dealer Price  PRC_UNIT_WEIGHT  PRC_UNIT_CUBE  \n",
              "0          2       18.3792             0.02       0.000616  \n",
              "1          1      150.7110             0.19       0.001029  \n",
              "2          1      150.7110             0.19       0.001029  \n",
              "3          1      150.7110             0.19       0.001029  \n",
              "4          1      150.7110             0.19       0.001029  "
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "%pip install openpyxl\n",
        "import pandas as pd\n",
        "\n",
        "file_path = \"D:\\\\TimePassProjects\\\\col\\\\tr1\\\\data_without_key.xlsx\"\n",
        "df = pd.read_excel(file_path)\n",
        "display(df.head())"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "DIb7ntFmmz6H",
        "outputId": "f714191e-ff6f-4ab1-ee9a-d0617f001cc6"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Number of duplicate rows found: 706142\n",
            "Number of rows after removing duplicates: 342433\n"
          ]
        }
      ],
      "source": [
        "num_duplicates = df.duplicated().sum()\n",
        "print(f\"Number of duplicate rows found: {num_duplicates}\")\n",
        "\n",
        "df_cleaned = df.drop_duplicates()\n",
        "print(f\"Number of rows after removing duplicates: {len(df_cleaned)}\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 660
        },
        "id": "rMYr7Pp9m1rC",
        "outputId": "a7407183-395a-40ad-f3cd-5c3facb6ccbb"
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Number of rows after filtering: 319045\n"
          ]
        },
        {
          "data": {
            "text/html": [
              "<div>\n",
              "<style scoped>\n",
              "    .dataframe tbody tr th:only-of-type {\n",
              "        vertical-align: middle;\n",
              "    }\n",
              "\n",
              "    .dataframe tbody tr th {\n",
              "        vertical-align: top;\n",
              "    }\n",
              "\n",
              "    .dataframe thead th {\n",
              "        text-align: right;\n",
              "    }\n",
              "</style>\n",
              "<table border=\"1\" class=\"dataframe\">\n",
              "  <thead>\n",
              "    <tr style=\"text-align: right;\">\n",
              "      <th></th>\n",
              "      <th>Market</th>\n",
              "      <th>Sale Type</th>\n",
              "      <th>Dealer Code</th>\n",
              "      <th>Sale Date</th>\n",
              "      <th>Sale Month</th>\n",
              "      <th>Sale Year</th>\n",
              "      <th>Dummy ID</th>\n",
              "      <th>Part Qty.</th>\n",
              "      <th>Dealer Price</th>\n",
              "      <th>PRC_UNIT_WEIGHT</th>\n",
              "      <th>PRC_UNIT_CUBE</th>\n",
              "    </tr>\n",
              "  </thead>\n",
              "  <tbody>\n",
              "    <tr>\n",
              "      <th>0</th>\n",
              "      <td>NZL</td>\n",
              "      <td>OTC</td>\n",
              "      <td>22598</td>\n",
              "      <td>2023-09-05</td>\n",
              "      <td>9</td>\n",
              "      <td>2023</td>\n",
              "      <td>PartNo1</td>\n",
              "      <td>2</td>\n",
              "      <td>18.3792</td>\n",
              "      <td>0.02</td>\n",
              "      <td>0.000616</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>1</th>\n",
              "      <td>NZL</td>\n",
              "      <td>RO</td>\n",
              "      <td>34561</td>\n",
              "      <td>2025-07-16</td>\n",
              "      <td>7</td>\n",
              "      <td>2025</td>\n",
              "      <td>PartNo2</td>\n",
              "      <td>1</td>\n",
              "      <td>150.7110</td>\n",
              "      <td>0.19</td>\n",
              "      <td>0.001029</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>4</th>\n",
              "      <td>NZL</td>\n",
              "      <td>RO</td>\n",
              "      <td>45611</td>\n",
              "      <td>2025-07-09</td>\n",
              "      <td>7</td>\n",
              "      <td>2025</td>\n",
              "      <td>PartNo2</td>\n",
              "      <td>1</td>\n",
              "      <td>150.7110</td>\n",
              "      <td>0.19</td>\n",
              "      <td>0.001029</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>7</th>\n",
              "      <td>NZL</td>\n",
              "      <td>RO</td>\n",
              "      <td>44242</td>\n",
              "      <td>2025-03-26</td>\n",
              "      <td>3</td>\n",
              "      <td>2025</td>\n",
              "      <td>PartNo2</td>\n",
              "      <td>1</td>\n",
              "      <td>150.7110</td>\n",
              "      <td>0.19</td>\n",
              "      <td>0.001029</td>\n",
              "    </tr>\n",
              "    <tr>\n",
              "      <th>10</th>\n",
              "      <td>NZL</td>\n",
              "      <td>RO</td>\n",
              "      <td>22591</td>\n",
              "      <td>2025-03-25</td>\n",
              "      <td>3</td>\n",
              "      <td>2025</td>\n",
              "      <td>PartNo2</td>\n",
              "      <td>1</td>\n",
              "      <td>150.7110</td>\n",
              "      <td>0.19</td>\n",
              "      <td>0.001029</td>\n",
              "    </tr>\n",
              "  </tbody>\n",
              "</table>\n",
              "</div>"
            ],
            "text/plain": [
              "   Market Sale Type  Dealer Code  Sale Date  Sale Month  Sale Year Dummy ID  \\\n",
              "0     NZL       OTC        22598 2023-09-05           9       2023  PartNo1   \n",
              "1     NZL        RO        34561 2025-07-16           7       2025  PartNo2   \n",
              "4     NZL        RO        45611 2025-07-09           7       2025  PartNo2   \n",
              "7     NZL        RO        44242 2025-03-26           3       2025  PartNo2   \n",
              "10    NZL        RO        22591 2025-03-25           3       2025  PartNo2   \n",
              "\n",
              "    Part Qty.  Dealer Price  PRC_UNIT_WEIGHT  PRC_UNIT_CUBE  \n",
              "0           2       18.3792             0.02       0.000616  \n",
              "1           1      150.7110             0.19       0.001029  \n",
              "4           1      150.7110             0.19       0.001029  \n",
              "7           1      150.7110             0.19       0.001029  \n",
              "10          1      150.7110             0.19       0.001029  "
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "<class 'pandas.core.frame.DataFrame'>\n",
            "Index: 319045 entries, 0 to 1048487\n",
            "Data columns (total 11 columns):\n",
            " #   Column           Non-Null Count   Dtype         \n",
            "---  ------           --------------   -----         \n",
            " 0   Market           319045 non-null  object        \n",
            " 1   Sale Type        319045 non-null  object        \n",
            " 2   Dealer Code      319045 non-null  int64         \n",
            " 3   Sale Date        319045 non-null  datetime64[ns]\n",
            " 4   Sale Month       319045 non-null  int64         \n",
            " 5   Sale Year        319045 non-null  int64         \n",
            " 6   Dummy ID         319045 non-null  object        \n",
            " 7   Part Qty.        319045 non-null  int64         \n",
            " 8   Dealer Price     319045 non-null  float64       \n",
            " 9   PRC_UNIT_WEIGHT  319045 non-null  float64       \n",
            " 10  PRC_UNIT_CUBE    319045 non-null  float64       \n",
            "dtypes: datetime64[ns](1), float64(3), int64(4), object(3)\n",
            "memory usage: 29.2+ MB\n"
          ]
        }
      ],
      "source": [
        "df_filtered = df_cleaned[(df_cleaned['Part Qty.'] >= 0) &\n",
        "                         (df_cleaned['PRC_UNIT_WEIGHT'] > 0) &\n",
        "                         (df_cleaned['PRC_UNIT_CUBE'] > 0) &\n",
        "                         (df_cleaned['Dealer Price'].notna())]\n",
        "\n",
        "print(f\"Number of rows after filtering: {len(df_filtered)}\")\n",
        "display(df_filtered.head())\n",
        "df_filtered.info()"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[NAIVE ZERO] Aggregating and Padding Data...\n",
            "[INFO] Aggregating data to monthly level...\n",
            "[INFO] Aggregated to 171111 monthly records.\n",
            "[INFO] Padding series to monthly grid and filtering inactive pairs...\n",
            "[INFO] Date range: 2023-08 to 2025-08. Total months: 25\n",
            "[INFO] Filtered from 171111 to 170827 records after removing inactive pairs.\n",
            "[INFO] Padding complete. Final shape: (1404700, 9)\n",
            "[NAIVE ZERO] Running CV on 11 folds...\n",
            "\n",
            "========================================\n",
            "   NAIVE ZERO BASELINE RESULTS\n",
            "========================================\n",
            "MAE:   0.3492 ± 0.0137\n",
            "RMSE:  2.6557 ± 0.0643\n",
            "WMAPE: 100.0000% ± 0.0000%\n",
            "R2:    -0.0176 ± 0.0014\n",
            "========================================\n",
            "\n",
            "[INTERPRETATION FOR PAPER]\n",
            " > Naive R2 is negative (-0.0176). This confirms the model captures variance.\n",
            " > Use the Naive MAE (0.3492) in Table 1 to prove your +SHOS model isn't just predicting zero.\n"
          ]
        }
      ],
      "source": [
        "import pandas as pd\n",
        "import numpy as np\n",
        "from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score\n",
        "import math\n",
        "\n",
        "# --- 1. Metric Helper Functions (Redefined here for safety) ---\n",
        "def rmse_metric(y_true, y_pred):\n",
        "    return math.sqrt(mean_squared_error(y_true, y_pred))\n",
        "\n",
        "def wmape_metric(y_true, y_pred):\n",
        "    denom = np.sum(np.abs(y_true))\n",
        "    return np.sum(np.abs(y_true - y_pred)) / denom * 100.0 if denom != 0 else np.nan\n",
        "\n",
        "# --- 2. Required Helper Functions ---\n",
        "def aggregate_monthly(df: pd.DataFrame) -> pd.DataFrame:\n",
        "    print(\"[INFO] Aggregating data to monthly level...\")\n",
        "    df = df.copy()\n",
        "    colmap = {}\n",
        "    for c in df.columns:\n",
        "        lc = c.lower().strip()\n",
        "        if lc in (\"dealer code\", \"dealercode\", \"dealer_code\"):\n",
        "            colmap[c] = \"DealerCode\"\n",
        "        if lc in (\"dummy id\", \"dummyid\", \"partno\", \"part_no\", \"dummy_id\"):\n",
        "            colmap[c] = \"DummyID\"\n",
        "        if lc in (\"sale date\", \"saledate\", \"sale_date\"):\n",
        "            colmap[c] = \"SaleDate\"\n",
        "        if lc in (\"part qty.\", \"part qty\", \"partqty\", \"qty\", \"quantity\"):\n",
        "            colmap[c] = \"PartQty\"\n",
        "        if lc in (\"sale type\", \"saletype\", \"sale_type\"):\n",
        "            colmap[c] = \"SaleType\"\n",
        "        if lc in (\"dealer price\", \"dealerprice\", \"price\"):\n",
        "            colmap[c] = \"DealerPrice\"\n",
        "        if lc in (\"prc_unit_weight\", \"weight\", \"prc unit weight\"):\n",
        "            colmap[c] = \"PRC_UNIT_WEIGHT\"\n",
        "        if lc in (\"prc_unit_cube\", \"cube\", \"prc unit cube\"):\n",
        "            colmap[c] = \"PRC_UNIT_CUBE\"\n",
        "        if lc in (\"otc_qty\",\"otc qty\",\"otcqty\"):\n",
        "            colmap[c] = \"OTC_Qty\"\n",
        "    df = df.rename(columns=colmap)\n",
        "    if \"SaleDate\" not in df.columns:\n",
        "        raise ValueError(\"Input must have 'SaleDate' column.\")\n",
        "    df[\"SaleDate\"] = pd.to_datetime(df[\"SaleDate\"])\n",
        "    df[\"MonthStart\"] = df[\"SaleDate\"].dt.to_period(\"M\").dt.to_timestamp()\n",
        "    agg = df.groupby([\"DealerCode\", \"DummyID\", \"MonthStart\"], as_index=False).agg(\n",
        "        PartQty=(\"PartQty\", \"sum\"),\n",
        "        OTC_Qty=(\"PartQty\", lambda x: x[df.loc[x.index, \"SaleType\"].astype(str).str.upper() == \"OTC\"].sum() if \"SaleType\" in df.columns else 0),\n",
        "        DealerPrice=(\"DealerPrice\", \"mean\"),\n",
        "        PRC_UNIT_WEIGHT=(\"PRC_UNIT_WEIGHT\", \"mean\"),\n",
        "        PRC_UNIT_CUBE=(\"PRC_UNIT_CUBE\", \"mean\"),\n",
        "    )\n",
        "    agg[\"OTC_Qty\"] = agg[\"OTC_Qty\"].fillna(0).astype(int)\n",
        "    agg[\"OTC_Flag\"] = (agg[\"OTC_Qty\"] > 0).astype(int)\n",
        "    agg[[\"DealerPrice\", \"PRC_UNIT_WEIGHT\", \"PRC_UNIT_CUBE\"]] = agg[[\"DealerPrice\", \"PRC_UNIT_WEIGHT\", \"PRC_UNIT_CUBE\"]].fillna(0.0)\n",
        "    print(f\"[INFO] Aggregated to {len(agg)} monthly records.\")\n",
        "    return agg\n",
        "\n",
        "def pad_monthly_series_and_filter_inactive(df: pd.DataFrame) -> pd.DataFrame:\n",
        "    print(\"[INFO] Padding series to monthly grid and filtering inactive pairs...\")\n",
        "    df = df.copy()\n",
        "    df[\"MonthStart\"] = pd.to_datetime(df[\"MonthStart\"])\n",
        "    min_date = df[\"MonthStart\"].min()\n",
        "    max_date = df[\"MonthStart\"].max()\n",
        "    all_months = pd.date_range(start=min_date, end=max_date, freq=\"MS\")\n",
        "    print(f\"[INFO] Date range: {min_date.strftime('%Y-%m')} to {max_date.strftime('%Y-%m')}. Total months: {len(all_months)}\")\n",
        "    df_active = df.groupby([\"DealerCode\", \"DummyID\"]).filter(lambda g: g[\"PartQty\"].sum() > 0)\n",
        "    print(f\"[INFO] Filtered from {df.shape[0]} to {df_active.shape[0]} records after removing inactive pairs.\")\n",
        "    if df_active.empty:\n",
        "        print(\"[WARNING] No active series found after filtering.\")\n",
        "        return df_active\n",
        "    full_index = pd.MultiIndex.from_product(\n",
        "        [df_active[\"DealerCode\"].unique(), df_active[\"DummyID\"].unique(), all_months],\n",
        "        names=[\"DealerCode\", \"DummyID\", \"MonthStart\"]\n",
        "    )\n",
        "    df_indexed = df_active.set_index([\"DealerCode\", \"DummyID\", \"MonthStart\"])\n",
        "    df_padded = df_indexed.reindex(full_index, fill_value=0).reset_index()\n",
        "    df_padded[\"OTC_Flag\"] = (df_padded[\"OTC_Qty\"] > 0).astype(int)\n",
        "    for col in [\"PartQty\", \"OTC_Qty\", \"DealerPrice\", \"PRC_UNIT_WEIGHT\", \"PRC_UNIT_CUBE\"]:\n",
        "        df_padded[col] = pd.to_numeric(df_padded[col], errors=\"coerce\").fillna(0)\n",
        "    df_final = df_padded.groupby([\"DealerCode\", \"DummyID\"]).filter(lambda g: g[\"PartQty\"].sum() > 0).reset_index(drop=True)\n",
        "    print(f\"[INFO] Padding complete. Final shape: {df_final.shape}\")\n",
        "    return df_final\n",
        "\n",
        "# --- 3. Prepare Data (Exact same preprocessing as Main Pipeline) ---\n",
        "print(\"[NAIVE ZERO] Aggregating and Padding Data...\")\n",
        "df_ag_naive = aggregate_monthly(df_filtered)\n",
        "df_padded_naive = pad_monthly_series_and_filter_inactive(df_ag_naive)\n",
        "\n",
        "# --- 3. Setup Cross-Validation Logic ---\n",
        "# Sort to ensure temporal order matches main pipeline\n",
        "df_padded_naive = df_padded_naive.sort_values([\"MonthStart\", \"DealerCode\", \"DummyID\"]).reset_index(drop=True)\n",
        "all_months = sorted(df_padded_naive[\"MonthStart\"].unique())\n",
        "\n",
        "# Default windows from your pipeline\n",
        "train_window = 12\n",
        "test_window = 3\n",
        "total_folds = len(all_months) - train_window - test_window + 1\n",
        "\n",
        "print(f\"[NAIVE ZERO] Running CV on {total_folds} folds...\")\n",
        "\n",
        "naive_scores = {\"MAE\": [], \"RMSE\": [], \"WMAPE\": [], \"R2\": []}\n",
        "\n",
        "for i in range(total_folds):\n",
        "    # Identify test months for this fold\n",
        "    test_months = all_months[i + train_window : i + train_window + test_window]\n",
        "    \n",
        "    # Slice the dataframe for test period\n",
        "    test_df_fold = df_padded_naive[df_padded_naive[\"MonthStart\"].isin(test_months)]\n",
        "    \n",
        "    if test_df_fold.empty:\n",
        "        continue\n",
        "        \n",
        "    # Actuals\n",
        "    y_test = test_df_fold[\"PartQty\"].values\n",
        "    \n",
        "    # Naive Zero Prediction\n",
        "    y_pred = np.zeros_like(y_test)\n",
        "    \n",
        "    # Calculate Metrics\n",
        "    mae = mean_absolute_error(y_test, y_pred)\n",
        "    rmse_val = rmse_metric(y_test, y_pred)\n",
        "    wmape_val = wmape_metric(y_test, y_pred)\n",
        "    r2_val = r2_score(y_test, y_pred)\n",
        "    \n",
        "    # Store\n",
        "    naive_scores[\"MAE\"].append(mae)\n",
        "    naive_scores[\"RMSE\"].append(rmse_val)\n",
        "    naive_scores[\"WMAPE\"].append(wmape_val)\n",
        "    naive_scores[\"R2\"].append(r2_val)\n",
        "\n",
        "# --- 4. Aggregate and Display Results ---\n",
        "naive_results = {}\n",
        "for metric in naive_scores:\n",
        "    arr = np.array(naive_scores[metric])\n",
        "    naive_results[f\"{metric}_mean\"] = np.nanmean(arr)\n",
        "    naive_results[f\"{metric}_std\"] = np.nanstd(arr)\n",
        "\n",
        "print(\"\\n\" + \"=\"*40)\n",
        "print(\"   NAIVE ZERO BASELINE RESULTS\")\n",
        "print(\"=\"*40)\n",
        "print(f\"MAE:   {naive_results['MAE_mean']:.4f} ± {naive_results['MAE_std']:.4f}\")\n",
        "print(f\"RMSE:  {naive_results['RMSE_mean']:.4f} ± {naive_results['RMSE_std']:.4f}\")\n",
        "print(f\"WMAPE: {naive_results['WMAPE_mean']:.4f}% ± {naive_results['WMAPE_std']:.4f}%\")\n",
        "print(f\"R2:    {naive_results['R2_mean']:.4f} ± {naive_results['R2_std']:.4f}\")\n",
        "print(\"=\"*40)\n",
        "\n",
        "# Quick comparison logic for your paper\n",
        "print(\"\\n[INTERPRETATION FOR PAPER]\")\n",
        "if naive_results['R2_mean'] < 0:\n",
        "    print(f\" > Naive R2 is negative ({naive_results['R2_mean']:.4f}). This confirms the model captures variance.\")\n",
        "else:\n",
        "    print(f\" > Naive R2 is {naive_results['R2_mean']:.4f}.\")\n",
        "\n",
        "print(f\" > Use the Naive MAE ({naive_results['MAE_mean']:.4f}) in Table 1 to prove your +SHOS model isn't just predicting zero.\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[INFO] LightGBM is available.\n",
            "[INFO] Loading data from: D:\\\\TimePassProjects\\\\col\\\\tr1\\\\data_without_key.xlsx\n",
            "Original number of rows: 1048575\n",
            "Number of duplicate rows found: 706142\n",
            "Number of rows after removing duplicates: 342433\n",
            "Number of rows after filtering: 319045\n",
            "--- Filtered Data Head ---\n",
            "   Market Sale Type  Dealer Code  Sale Date  Sale Month  Sale Year Dummy ID  \\\n",
            "0     NZL       OTC        22598 2023-09-05           9       2023  PartNo1   \n",
            "1     NZL        RO        34561 2025-07-16           7       2025  PartNo2   \n",
            "4     NZL        RO        45611 2025-07-09           7       2025  PartNo2   \n",
            "7     NZL        RO        44242 2025-03-26           3       2025  PartNo2   \n",
            "10    NZL        RO        22591 2025-03-25           3       2025  PartNo2   \n",
            "\n",
            "    Part Qty.  Dealer Price  PRC_UNIT_WEIGHT  PRC_UNIT_CUBE  \n",
            "0           2       18.3792             0.02       0.000616  \n",
            "1           1      150.7110             0.19       0.001029  \n",
            "4           1      150.7110             0.19       0.001029  \n",
            "7           1      150.7110             0.19       0.001029  \n",
            "10          1      150.7110             0.19       0.001029  \n",
            "--- Filtered Data Info ---\n",
            "<class 'pandas.core.frame.DataFrame'>\n",
            "Index: 319045 entries, 0 to 1048487\n",
            "Data columns (total 11 columns):\n",
            " #   Column           Non-Null Count   Dtype         \n",
            "---  ------           --------------   -----         \n",
            " 0   Market           319045 non-null  object        \n",
            " 1   Sale Type        319045 non-null  object        \n",
            " 2   Dealer Code      319045 non-null  int64         \n",
            " 3   Sale Date        319045 non-null  datetime64[ns]\n",
            " 4   Sale Month       319045 non-null  int64         \n",
            " 5   Sale Year        319045 non-null  int64         \n",
            " 6   Dummy ID         319045 non-null  object        \n",
            " 7   Part Qty.        319045 non-null  int64         \n",
            " 8   Dealer Price     319045 non-null  float64       \n",
            " 9   PRC_UNIT_WEIGHT  319045 non-null  float64       \n",
            " 10  PRC_UNIT_CUBE    319045 non-null  float64       \n",
            "dtypes: datetime64[ns](1), float64(3), int64(4), object(3)\n",
            "memory usage: 29.2+ MB\n",
            "[INFO] Aggregating data to monthly level...\n",
            "[INFO] Aggregated to 171111 monthly records.\n",
            "[INFO] Padding series to monthly grid and filtering inactive pairs...\n",
            "[INFO] Date range: 2023-08 to 2025-08. Total months: 25\n",
            "[INFO] Filtered from 171111 to 170827 records after removing inactive pairs.\n",
            "[INFO] Padding complete. Final shape: (1404700, 9)\n",
            "\n",
            "[INFO] Data preparation summary:\n",
            "   DealerCode     DummyID MonthStart  PartQty  OTC_Qty  DealerPrice  \\\n",
            "0       10231  PartNo1008 2023-08-01        3        2       18.538   \n",
            "1       10231  PartNo1008 2023-09-01        0        0        0.000   \n",
            "2       10231  PartNo1008 2023-10-01        0        0        0.000   \n",
            "3       10231  PartNo1008 2023-11-01        0        0        0.000   \n",
            "4       10231  PartNo1008 2023-12-01        0        0        0.000   \n",
            "\n",
            "   PRC_UNIT_WEIGHT  PRC_UNIT_CUBE  OTC_Flag  \n",
            "0             0.03       0.000063         1  \n",
            "1             0.00       0.000000         0  \n",
            "2             0.00       0.000000         0  \n",
            "3             0.00       0.000000         0  \n",
            "4             0.00       0.000000         0  \n",
            "\n",
            "Total rows in processed data: 1404700\n",
            "[INFO] Starting K-Sensitivity Analysis on first fold...\n",
            "--- Testing k = 0.1 ---\n",
            "[INFO] Building fold features with sparsity_k=0.1...\n",
            "[INFO] Building base feature matrix...\n",
            "[INFO] Adding time series features (max_lag=6)...\n",
            "[INFO] Base features include 24 columns.\n",
            "[INFO] Building base feature matrix...\n",
            "[INFO] Adding time series features (max_lag=6)...\n",
            "[INFO] Base features include 24 columns.\n",
            "[INFO] Adding SHOS features with train-only sparsity (k=0.1)...\n",
            "[INFO] Adding SHOS features with train-only sparsity (k=0.1)...\n",
            "[INFO] Building low-rank embeddings (n_components=12)...\n",
            "[INFO] Fold features: 24 base, 24 embeddings, 50 total.\n",
            "[INFO] Tuning Regressor LightGBM...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.051852 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6289\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 50\n",
            "[LightGBM] [Info] Start training from score 0.324317\n",
            "[INFO] Tuning LightGBM took 258.51s. Best score: 0.0674\n",
            "[RESULT] k = 0.1, MAE = 0.6647, RMSE = 3.8791\n",
            "--- Testing k = 0.3 ---\n",
            "[INFO] Building fold features with sparsity_k=0.3...\n",
            "[INFO] Building base feature matrix...\n",
            "[INFO] Adding time series features (max_lag=6)...\n",
            "[INFO] Base features include 24 columns.\n",
            "[INFO] Building base feature matrix...\n",
            "[INFO] Adding time series features (max_lag=6)...\n",
            "[INFO] Base features include 24 columns.\n",
            "[INFO] Adding SHOS features with train-only sparsity (k=0.3)...\n",
            "[INFO] Adding SHOS features with train-only sparsity (k=0.3)...\n",
            "[INFO] Building low-rank embeddings (n_components=12)...\n",
            "[INFO] Fold features: 24 base, 24 embeddings, 50 total.\n",
            "[INFO] Tuning Regressor LightGBM...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.051291 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6289\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 50\n",
            "[LightGBM] [Info] Start training from score 0.324317\n",
            "[INFO] Tuning LightGBM took 226.23s. Best score: 0.0720\n",
            "[RESULT] k = 0.3, MAE = 0.6654, RMSE = 3.8713\n",
            "--- Testing k = 0.5 ---\n",
            "[INFO] Building fold features with sparsity_k=0.5...\n",
            "[INFO] Building base feature matrix...\n",
            "[INFO] Adding time series features (max_lag=6)...\n",
            "[INFO] Base features include 24 columns.\n",
            "[INFO] Building base feature matrix...\n",
            "[INFO] Adding time series features (max_lag=6)...\n",
            "[INFO] Base features include 24 columns.\n",
            "[INFO] Adding SHOS features with train-only sparsity (k=0.5)...\n",
            "[INFO] Adding SHOS features with train-only sparsity (k=0.5)...\n",
            "[INFO] Building low-rank embeddings (n_components=12)...\n",
            "[INFO] Fold features: 24 base, 24 embeddings, 50 total.\n",
            "[INFO] Tuning Regressor LightGBM...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.047191 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6289\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 50\n",
            "[LightGBM] [Info] Start training from score 0.324317\n",
            "[INFO] Tuning LightGBM took 162.03s. Best score: 0.0749\n",
            "[RESULT] k = 0.5, MAE = 0.6657, RMSE = 3.8743\n",
            "--- Testing k = 0.7 ---\n",
            "[INFO] Building fold features with sparsity_k=0.7...\n",
            "[INFO] Building base feature matrix...\n",
            "[INFO] Adding time series features (max_lag=6)...\n",
            "[INFO] Base features include 24 columns.\n",
            "[INFO] Building base feature matrix...\n",
            "[INFO] Adding time series features (max_lag=6)...\n",
            "[INFO] Base features include 24 columns.\n",
            "[INFO] Adding SHOS features with train-only sparsity (k=0.7)...\n",
            "[INFO] Adding SHOS features with train-only sparsity (k=0.7)...\n",
            "[INFO] Building low-rank embeddings (n_components=12)...\n",
            "[INFO] Fold features: 24 base, 24 embeddings, 50 total.\n",
            "[INFO] Tuning Regressor LightGBM...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.062311 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6286\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 49\n",
            "[LightGBM] [Info] Start training from score 0.324317\n",
            "[INFO] Tuning LightGBM took 219.15s. Best score: 0.0797\n",
            "[RESULT] k = 0.7, MAE = 0.6611, RMSE = 3.8416\n",
            "--- Testing k = 0.9 ---\n",
            "[INFO] Building fold features with sparsity_k=0.9...\n",
            "[INFO] Building base feature matrix...\n",
            "[INFO] Adding time series features (max_lag=6)...\n",
            "[INFO] Base features include 24 columns.\n",
            "[INFO] Building base feature matrix...\n",
            "[INFO] Adding time series features (max_lag=6)...\n",
            "[INFO] Base features include 24 columns.\n",
            "[INFO] Adding SHOS features with train-only sparsity (k=0.9)...\n",
            "[INFO] Adding SHOS features with train-only sparsity (k=0.9)...\n",
            "[INFO] Building low-rank embeddings (n_components=12)...\n",
            "[INFO] Fold features: 24 base, 24 embeddings, 50 total.\n",
            "[INFO] Tuning Regressor LightGBM...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.050367 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6289\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 50\n",
            "[LightGBM] [Info] Start training from score 0.324317\n",
            "[INFO] Tuning LightGBM took 196.24s. Best score: 0.0798\n",
            "[RESULT] k = 0.9, MAE = 0.6665, RMSE = 3.9195\n",
            "\n",
            "--- K-Sensitivity Analysis Results ---\n",
            "     k       MAE      RMSE\n",
            "0  0.1  0.664705  3.879130\n",
            "1  0.3  0.665408  3.871311\n",
            "2  0.5  0.665700  3.874287\n",
            "3  0.7  0.661114  3.841630\n",
            "4  0.9  0.666453  3.919457\n",
            "\n",
            "Best k based on MAE: 0.7 (MAE: 0.6611)\n",
            "K-Sensitivity plot saved to ./k_sensitivity_results\\k_sensitivity_plot.png\n",
            "\n",
            "[INFO] SENSITIVITY ANALYSIS COMPLETE. The best 'k' value is: 0.7\n"
          ]
        },
        {
          "data": {
            "image/png": "iVBORw0KGgoAAAANSUhEUgAAA90AAAJOCAYAAACqS2TfAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAXIJJREFUeJzt3QecHGX9P/Dn0hNIQichhCJBCSU0BQNK70j5yQ+RIh3/KqCAghQVAlIUEFE6CAEVQapKb4b8MEEggFIjoYWS0IRUklyS/b++A3vs9bvkJjd3eb91uN3Z2d1n55m93GeeMlWlUqmUAAAAgDbXpe1fEgAAABC6AQAAIEdaugEAACAnQjcAAADkROgGAACAnAjdAAAAkBOhGwAAAHIidAMAAEBOhG4AAADIidANQGbkyJGpqqoqvfbaa83ukVGjRmXbxs/W2mqrrbKlSB5//PG02WabpSWWWCL7XE8//XQqmtVWWy0dfPDBC/Tc+EynnXZam5dpcRP78aijjmrvYgDQwQjdAIvQM888k/73f/83rbrqqqlXr15p0KBBafvtt0+//e1vC1kPl1xySRbG8/T2229ngbC9gm51dXXae++903//+990wQUXpN///vdZ/TR1siGWP/zhDw1us/nmm2ePr7vuuqkjiZMt5c/W0HLOOeekIhozZkx2/Hz00Udt+rrxmnGiY0H3Y3MnpCqPpVi6d++ePve5z6UDDzwwvfLKK6kze/7557P925ITfACdQbf2LgDA4iLCwdZbb51WWWWVdMQRR6QBAwakN954Iz366KPpwgsvTEcffXS7lu9b3/pW+uY3v5l69uxZK3Qvt9xy9VpYt9hii/Txxx+nHj16tPp97rvvvnqhe8SIEVnA2WCDDdKi9vLLL6fXX389XXnllenwww9v0XPihMn111+fDjjggFrrI0REPcfjHdW+++6bdtlll3rrN9xww1REsb/j+IljdKmllkodzfe///30pS99KTv58+STT6Yrrrgi3XnnndkJupVWWil11tAddRY9XhbkxAZARyN0AywiZ555Zurfv3/WlbluOHj33XfbvR66du2aLS3RpUuXBQ6WCxLU81Te960JbBFK//rXv6b3338/OylRFkF8xRVXTGuuuWb68MMPU0e00UYb1TuZ0JxSqZRmzZqVevfuXe+xWB91HsfMgpoxY0bW9b8z+upXv5r1fgmHHHJI+vznP58F8WuvvTaddNJJC/y6TdVJZ9WZjxOgY9O9HGARtqius846DYa7FVZYod666L688cYbZ380L7PMMlkrdLSMV4qWoujGHC1H0Yrep0+frMv6L3/5y3qvF13Y4/1jm6WXXjp98YtfzEJiY2O6owXqueeeSw8//HBNF9jyWOy6Y7pjnOuSSy6ZZs6c2WDLabTqz5s3r6bMla8TrXzlwFF+nyjLqaeemnW5fe+99+q95re//e1sP0aoaMpDDz2UhZr4Qzy232OPPdILL7xQ83i0jm655ZbZ7ehiXvkZmxKvEz0CbrrpplrrY39+4xvfaPDkxdy5c9MZZ5yR1lhjjey5sX9PPvnkNHv27Hph6ec//3laeeWVs7qKeo16aEh0qT7mmGPS4MGDs9ccMmRI+sUvfpHmz5+f8hRl/9rXvpbuvffe7DiKY/Tyyy+vOS5uuOGG9JOf/CQ7FuMzTJ06NXte7K/yMR0nKyLcv/XWW7VeO+okjqX4vsTJjb59+6b999+/wXJEF+Xjjz8+u7366qvXHD/lY7il+3xhRF3FCYW2GiKyzTbbZD9fffXV7Oc111yTrYvfEfEZ1l577XTppZe2uE4W5DWiHsuvsd5669V8z2+99dbsfpxwi3p86qmn6r3Giy++mJ1EiN9ZsV28TpygKovvdnzXQhzb5Tqr7I5/991313xvo/533XXXet+B1hwnAO1N6AZYRGKc8Lhx49Kzzz7bolbxGNsZLaa/+tWvsmD14IMPZt26645djRbVnXbaKa2//vrp/PPPT2uttVb68Y9/nP3hWhZdp6P1LP7Y/vWvf5117Yyu3P/85z8bLUNsF8EvXi/GOcdyyimnNLjtPvvsk7UyRbfYShHC//a3v2V/hDcURIcOHZpOP/30miBdfp/4nNHdPULTjTfeWOs5c+bMSTfffHPaa6+9mmxtf+CBB9KOO+6YtWRHODvuuOOyrsgx5rocyv7f//t/WQgLsX+a+oyVIkhG8P7Tn/5Us+5f//pXFgz222+/Bp8TXdd/9rOfZS3JMXY8wv7ZZ5+dnUypFNv89Kc/zerz3HPPzcb57rDDDtn+rbtv4zXi5EwcK7/5zW+yzxato/FZF1S8brTg112iLiqNHz8+O6EScxLE8IjKoQERdONY+NGPfpTOOuusrKU7wlb5hER87hhiESHuK1/5Sr1jOt4r6i5C4nnnnZfVdUO+/vWvZ2UI5fH4sSy//PKt2ucLKk4sxOtHuG2r4SERIsOyyy6b/YxwHL874jiN73ecYPne976XLr744nrPbaxOWvMaEyZMyI7h3XbbLdtX8fslbv/xj39Mxx57bHaiJH5/RDmjPitP8MTx/+Uvfzk7sXXiiSdm7xXBec8990y33XZbtk18t+O7FqI85TqL3wUhbkfIjkAdJ5DiuxAnFeM4qTsGvKXHCUC7KwGwSNx3332lrl27Zsvw4cNLJ5xwQunee+8tzZkzp9Z2r732WrbNmWeeWWv9M888U+rWrVut9VtuuWUpfpVfd911Netmz55dGjBgQGmvvfaqWbfHHnuU1llnnSbLd80112Sv9eqrr9asi+fEe9T197//Pds2fob58+eXBg0aVOs9w5///Odsu9GjR9cqc+VrPv7449k28f51xX7adNNNa6279dZba713YzbYYIPSCiusUPrggw9q1v3rX/8qdenSpXTggQfW+yw33XRTk69Xd9s77rijVFVVVZo4cWL22PHHH1/63Oc+V/MZK/f3008/nT3v8MMPr/V6P/rRj7L1Dz30UHb/3XffLfXo0aO06667Zvu07OSTT862O+igg2rWnXHGGaUlllii9J///KfWa5544onZ8VMuV4jnnnrqqU1+tqj32K6xZezYsTXbrrrqqtm6e+65p8H9E/th5syZNevjGI+6WHfddUsff/xxzfrYh7H9z372s5p18RljXXyOljj33HPrHbet2eetEc878sgjs9s//OEPs2Np5MiRpQVR3ldXX3116b333iu9/fbbpTvvvLO02mqrZcdVfC9C5X4s23HHHWuOtebqZEFeY8yYMTXr4ndUrOvdu3fp9ddfr1l/+eWX1/sebrvttqX11luvNGvWrJp1cRxvttlmpTXXXLNmXXx/GvoOT5s2rbTUUkuVjjjiiFrrJ0+eXOrfv3+t9a09TgDak5ZugEUkWp/Gjh2bdt9996xVNLqARytNdMGt7H4ZrX/RehStSJUtjdFFO1q+//73v9d63WgRqhyDG62Km2yySa0ZkKNr9ZtvvpmNJ89DdA+NLqN33XVXmj59es36aKWOzxetVAsiWnCjNb7c+heixS1a6srdwhsyadKkbDb06IIa3VzLhg0bltVDlHNhRetzvHZ0pY48Fj/Lra51ld+vbgv0D3/4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2zd7rHypr7qXDKt0yimnZI/FZ2tI3UuGhb/85S+ltddeO7skWkOXD3vsscey9TvssEOpNR544IHS5ptvnl3uqF+/fqXddtut9Pzzz9faZkEvGdaUhuqlurq6NGLEiNLqq69e6t69e2nw4MGlk046qdbllcK8efOy7QYOHJiVe6uttio9++yzWX1WXjKsfImleI3Y13E8Lbfcctnlmc4777xal6Jri0uGVb53lCWOi9bunxtvvLG04YYbZsfgMsssU9p///1Lb775Zq1t4n3ieG+NuHxaXLIuLuFVeQy3dJ8vyCXDKo/dOG732WefrO5aqqXH0l//+tfSsGHDSr169couJ/aLX/wiu8xY3e9qY3XSFq/R0OcuHy9xybZKL7/8cnZJvrhsYezzqJevfe1rpZtvvrnWdldeeWV2ybK4vF3d3yVxOy5pFpcJizLH76uDDz649MQTTyzUcQLQXqriP+0d/AGgKdFCFq1jMXN0U924AQCKxphuAAovusPGrO0x3h0AoCMxphuAwoqZl59//vlsXO5RRx3VJjOOAwAsSrqXA1BYMclWTMQUsxrHtXvzujQTAEBehG4AAADIiTHdAAAAkBOhGwAAAHKy2E2kNn/+/PT2229n4wKrqqrauzgAAAB0QHH17WnTpqWVVlopdenSeHv2Yhe6I3APHjy4vYsBAABAJ/DGG2+klVdeudHHF7vQXZ75NnZMv379UlFVV1en++67L+2www6pe/fu7V0c6lA/xaeOik39FJ86Kjb1U2zqp/jUUbFVd5AsNHXq1KxBt7mrqyx2obvcpTwCd9FDd58+fbIyFvlAW1ypn+JTR8WmfopPHRWb+ik29VN86qjYqjtYFmpu2LKJ1AAAACAnQjcAAADkROgGAACAnAjdAAAA0NlD9znnnJMNQD/mmGOa3O6mm25Ka621VurVq1dab7310l133bXIyggAAAAdLnQ//vjj6fLLL0/Dhg1rcrsxY8akfffdNx122GHpqaeeSnvuuWe2PPvss4usrAAAANBhQvf06dPT/vvvn6688sq09NJLN7nthRdemHbaaad0/PHHp6FDh6YzzjgjbbTRRumiiy5aZOUFAACADhO6jzzyyLTrrrum7bbbrtltx44dW2+7HXfcMVsPAAAARdOtPd/8hhtuSE8++WTWvbwlJk+enFZcccVa6+J+rG/M7Nmzs6Vs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7dixY9NLL72UjQOfMmVKOu+889Lo0aPTc889l3VVb8hpp52WRowYUW99vE+fPn3a/PMAAADQ+c2cOTPtt99+WTaNYdCFDd1z5sxJEydOzAp68803p6uuuio9/PDDae21127RmYWhQ4emfffdN51xxhktbukePHhwev/995vcMe0tPtv999+ftt9++9S9e/f2Lg51qJ/iU0fFpn6KTx0Vm/opNvVTfOqo2Ko7SBaKbLnccss1G7rbvXt5jx490pAhQ7LbG2+8cXr88cfThRdemC6//PJmnxsVsOGGG6YJEyY0uk3Pnj2zpaHnFrkCO1o5F1fqp/jUUbGpn+JTR8WmfopN/RSfOiq27gXPQi0tW+Gu0x1jtStbppsbBx7d0wcOHJh7uQAAAKC12rWl+6STTko777xzWmWVVdK0adOycdajRo1K9957b/b4gQcemAYNGpTOPvvs7P7pp5+evvzlL2ct4x999FE699xz0+uvv54OP/zw9vwYAAAAULzQ/e6772bBetKkSal///7ZBGkRuKPvfoix3l26fNYY/+GHH6YjjjgiTZ48OS299NJZd/QxY8a0aPw3AAAALFah+3e/+12Tj0erd6ULLrggWwAAAKAjKNyYbgAAAOgshG4AAADIidANAAAAORG6AQAAICdCNwAAAORE6AYAAICcCN0AAACQE6EbAAAAciJ0AwAAQE6EbgAAAMiJ0A0AAAA5EboBAAAgJ0I3AAAA5EToBgAAgJwI3QAAAJAToRsAAAByInQDAABAToRuAAAAyInQDQAAADkRugEAACAnQjcAAADkROgGAACAnAjdAAAAkBOhGwAAAHIidAMAAEBOhG4AAADIidANAAAAORG6AQAAICdCNwAAAORE6AYAAICcCN0AAACQE6EbAAAAciJ0AwAAQE6EbgAAAMiJ0A0AAAA5EboBAAAgJ0I3AAAA5EToBgAAgJwI3QAAAJAToRsAAAByInQDAABAToRuAAAAyInQDQAAADkRugEAACAnQjcAAADkROgGAACAnAjdAAAAkBOhGwAAAHIidAMAAEBOhG4AAADIidANAAAAORG6AQAAICdCNwAAAORE6AYAAICcCN0AAACQE6EbAAAAciJ0AwAAQE6EbgAAAMiJ0A0AAACdMXRfeumladiwYalfv37ZMnz48HT33Xc3+ZybbroprbXWWqlXr15pvfXWS3fdddciKy8AAAB0mNC98sorp3POOSeNGzcuPfHEE2mbbbZJe+yxR3ruueca3H7MmDFp3333TYcddlh66qmn0p577pktzz777CIvOwAAABQ6dO+2225pl112SWuuuWb6/Oc/n84888y05JJLpkcffbTB7S+88MK00047peOPPz4NHTo0nXHGGWmjjTZKF1100SIvOwAAAHSYMd3z5s1LN9xwQ5oxY0bWzbwhY8eOTdttt12tdTvuuGO2HgAAAIqmW3sX4JlnnslC9qxZs7JW7ttuuy2tvfbaDW47efLktOKKK9ZaF/djfWNmz56dLWVTp07NflZXV2dLUZXLVuQyLs7UT/Gpo2JTP8WnjopN/RSb+ik+dVRs1R0kC7W0fFWlUqmU2tGcOXPSxIkT05QpU9LNN9+crrrqqvTwww83GLx79OiRrr322mxcd9kll1ySRowYkd55550GX/+0007LHq/r+uuvT3369GnjTwMAAMDiYObMmWm//fbLsmxMDF7Ylu4I0kOGDMlub7zxxunxxx/Pxm5ffvnl9bYdMGBAvXAd92N9Y0466aR03HHH1WrpHjx4cNphhx2a3DFFOGty//33p+233z517969vYtDHeqn+NRRsamf4lNHxaZ+ik39FJ86KrbqDpKFyr2om9Puobuu+fPn1+oOXim6oT/44IPpmGOOqVkXldHYGPDQs2fPbKkrKq/IFdjRyrm4Uj/Fp46KTf0UnzoqNvVTbOqn+NRRsXUveBZqadnaNXRHK/TOO++cVllllTRt2rSsy/eoUaPSvffemz1+4IEHpkGDBqWzzz47u/+DH/wgbbnllun8889Pu+66azbxWlxq7IorrmjPjwEAAADFC93vvvtuFqwnTZqU+vfvn4YNG5YF7uhGEGKsd5cun02wvtlmm2XB/Cc/+Uk6+eSTs0uN3X777Wnddddtx08BAAAABQzdv/vd75p8PFq969p7772zBQAAAIquMNfpBgAAgM5G6AYAAICcCN0AAACQE6EbAAAAciJ0AwAAQE6EbgAAAMiJ0A0AAAA5EboBAAAgJ0I3AAAA5EToBgAAgJwI3QAAAJAToRsAAAByInQDAABAToRuAAAAyInQDQAAADkRugEAAEDoBgAAgI5FSzcAAADkROgGAACAnAjdAAAAkBOhGwAAAHIidAMAAEBOhG4AAADIidANAAAAORG6AQAAICdCNwAAAORE6AYAAICcCN0AAACQE6EbAAAAciJ0AwAAQE6EbgAAAMiJ0A0AAAA5EboBAAAgJ0I3AAAA5EToBgAAgJwI3QAAAJAToRsAAAByInQDAABAToRuAAAAyInQDQAAADkRugEAACAnQjcAAADkROgGAACAnAjdAAAAkBOhGwAAAHIidAMAAEBOhG4AAADIidANAAAAORG6AQAAICdCNwAAAORE6AYAAICcCN0AAACQE6EbAAAAciJ0AwAAQE6EbgAAAMiJ0A0AAAA5EboBAAAgJ0I3AAAA5EToBgAAgJwI3QAAANAZQ/fZZ5+dvvSlL6W+ffumFVZYIe25555p/PjxTT5n5MiRqaqqqtbSq1evRVZmAAAA6BCh++GHH05HHnlkevTRR9P999+fqqur0w477JBmzJjR5PP69euXJk2aVLO8/vrri6zMAAAA0FLdUju655576rViR4v3uHHj0hZbbNHo86J1e8CAAYughAAAANBBQ3ddU6ZMyX4us8wyTW43ffr0tOqqq6b58+enjTbaKJ111llpnXXWaXDb2bNnZ0vZ1KlTs5/Rqh5LUZXLVuQyLs7UT/Gpo2JTP8WnjopN/RSb+ik+dVRs1R0kC7W0fFWlUqmUCiAC9O67754++uij9MgjjzS63dixY9NLL72Uhg0bloX08847L40ePTo999xzaeWVV663/WmnnZZGjBhRb/3111+f+vTp0+afAwAAgM5v5syZab/99styaQyBLnzo/u53v5vuvvvuLHA3FJ6bOrswdOjQtO+++6YzzjijRS3dgwcPTu+//36TO6a9xeeKce7bb7996t69e3sXhzrUT/Gpo2JTP8WnjopN/RSb+ik+dVRs1R0kC0W2XG655ZoN3YXoXn7UUUelO+64I2uxbk3gDlEJG264YZowYUKDj/fs2TNbGnpekSuwo5VzcaV+ik8dFZv6KT51VGzqp9jUT/Gpo2LrXvAs1NKytevs5dHIHoH7tttuSw899FBaffXVW/0a8+bNS88880waOHBgLmUEAACABdWuLd1xubAYW/2Xv/wlu1b35MmTs/X9+/dPvXv3zm4feOCBadCgQdk1vcPpp5+evvzlL6chQ4Zk47/PPffc7JJhhx9+eHt+FAAAAChW6L700kuzn1tttVWt9ddcc006+OCDs9sTJ05MXbp81iD/4YcfpiOOOCIL6EsvvXTaeOON05gxY9Laa6+9iEsPAAAABQ7dLZnDbdSoUbXuX3DBBdkCAADAJ7lq7ty52dDbzjKRWrdu3dKsWbPa9TN17do1K0dVVdVCvU4hJlIDAACg9ebMmZMmTZqUXb6qM51EGDBgQHrjjTcWOvAurLjMdMwf1qNHjwV+DaEbAACgA5o/f3569dVXsxbZlVZaKQuG7R1S2+pzTZ8+PS255JK1hhov6uAfJzTee++9bB+vueaaC1wWoRsAAKADilAYAXXw4MFZi2xnEZ8pPluvXr3aLXSHmNw7LgsWE3eXy7Mg2vWSYQAAACyc9gymnV2XNti3agcAAAByInQDAABAToRuAACAxdi8+aU09uUP0l+efiv7GffzdvDBB2eTvn3nO9+p99hRRx2Vll566XTIIYfUWj927Nhs0rhdd9213nNee+217PUaWh599NHUnkykBgAAsJi659lJacTfnk+TpsyqWTewf6906m5rp53WHZjrew8ePDjdcMMN6YILLsgmLQtxbe4//elPaeWVV663/e9+97t09NFHZz/ffvvtbMb2uh544IG0zjrr1Fq37LLLpvakpRsAAGAxDdzf/cOTtQJ3mDxlVrY+Hs/TRhttlAXvW2+9tWZd3F5llVXSsGHDam0blxC78cYb03e/+92spXvkyJENvmYE7LjGd+USM5B3qNBdXV2dunXrlp599tl8SgQAAMACXVt65py5LVqmzapOp/71udRQR/LyutP++ny2XUteL957QRx66KHpmmuuqbl/9dVXZ13P6/rzn/+c1lprrfSFL3whHXDAAdl2C/qehe9eHmcJ4szDvHnz8ikRAAAArfZx9by09s/ubZM9F3F28tRZab3T7mvR9s+fvmPq06P1o5cPOOCAdNJJJ2XXwg7/+Mc/0vXXX591E68UXcpj27DTTjulKVOmpIcffjhttdVWtbbbbLPN6l3mK1rJO9yY7lNOOSWdfPLJ6fe//31aZpll2r5UAAAAdHrLL798TXfxaLmO28stt1ytbcaPH58ee+yxdNttt2X3o+f1PvvskwXxuqE7uqAPHTo0FckChe6LLrooTZgwIRu4vuqqq6Ylllii1uNPPvlkW5UPAACAFujdvWvW4twSj73633TwNY83u93IQ76UNll9mRa994I69NBDsxnLw8UXX1zv8QjXc+fOrTVxWgT0nj17Ztm0f//+NetjjPiQIUNShw/de+65Z9uXBAAAgAUWl8dqaRfvr665fDZLeUya1tDI6KqU0oD+vbLtunaJe/nZaaed0pw5c7Ly77hj7ZMGEbavu+66dP7556cddtihXi6Nmc4buuxYhw/dp556atuXBAAAgEUignRcFixmKY9IXRm8yxE7Hs87cIe49vYLL7xQc3v+/Pmp7I477kgffvhhOuyww2q1aIe99torawWvDN0ffPBBmjx5cq3tllpqqdSrV6/UIS8ZNm7cuPSHP/whW5566qm2KxUAAAC5iutwX3rARlmLdqW4H+vzvk53pX79+mVLXRGqt9tuu3qBuxy6n3jiifTvf/+7Zl1sO3DgwFrL7bffnjpcS/e7776bvvnNb6ZRo0ZlZw3CRx99lLbeeuvs4uYxGB4AAIBii2C9/doDsjHe706blVbo2ysbw513C/fIRq6zXfbHP/4xC+F1ZyKvtMkmm9S6bFhRLyG2QC3dRx99dJo2bVp67rnn0n//+99siet2T506NX3/+99v+1ICAACQiwjYw9dYNu2xwaDs56LoUr44WaCW7nvuuSe7blrlVOxrr712NtNc3cHtAAAAsLhaoJbuGNjevXv3eutjXeWgdwAAAFicLVDo3mabbdIPfvCD9Pbbb9ese+utt9Kxxx6btt1227YsHwAAACxeoTsuQB7jt1dbbbW0xhprZMvqq6+erfvtb3/b9qUEAACAxWVM9+DBg9OTTz6Zjet+8cUXs3UxvjumZwcAAAAWMHRXV1en3r17p6effjptv/322QIAAAC0QffymCxtlVVWSfPmzWvtUwEAAGCxskBjuk855ZR08sknZ9fnBgAAANpwTHdMpDZhwoS00korpVVXXTUtscQStR6P8d4AAACwuFug0L3nnnu2fUkAAABYdD56I6WZHzT+eJ9lU1pqcC5vffDBB6drr702u92tW7e08sorp7333judfvrpqUePHtn6rl27Zj/Hjh2bvvzlL9c8d/bs2VkDcPS8/vvf/5622mqrbP3DDz+cRowYkc0/NmvWrDRo0KC02WabpSuvvDJ7zVGjRqWtt966wfJMmjQpDRgwoBihe+7cuamqqiodeuih2Y4BAACgAwbuizZOae7sxrfp1jOlo8blFrx32mmndM0112STdY8bNy4ddNBBWdY8++yza105K7apDN233XZbWnLJJWsNd37++eez1zv66KPTb37zm2zy75deeindcsst9eYjGz9+fOrXr1+tdSussEIqzJjuOAtx7rnnZuEbAACADihauJsK3CEeb6olfCH17Nkza12OYB29qeMS1Pfff3+tbSKI33DDDenjjz+uWXf11Vdn6yvdd9992Wv98pe/TOuuu25aY401shAerdwRwOsG7Ni2cunSZYGmO2uRBXrlbbbZJmu6BwAAoCBKpZTmzGjZMvezENuk2K4lrxfvvRCeffbZNGbMmJqu5WUbb7xxWm211bIW6zBx4sQ0evTo9K1vfavWdhGco4t4PNYpxnTvvPPO6cQTT0zPPPNMthPqTqS2++67t1X5AAAAaInqmSmdtVLb7qurd2rZdie/nVKP2rmwOXfccUfWTTx6Ucc47Whtjkm764qhzdG6fcABB6SRI0emXXbZJS2//PK1tonx4Pfee2/acsstswAe3dG33XbbdOCBB9brSl53mHRMDv7cc8+lQoXu733ve9nPX/3qV/Ueiz74ruENAABAU2JSs0svvTTNmDEjXXDBBdlQ5r322ivNnz+/1nYRtqPR95VXXslCd4zZrismXYux3z//+c/TQw89lP75z3+ms846K/3iF79Ijz32WBo4cGDNtv/3f/+X+vbtW3O/e/fuKU8LFLrr7gQAAADaWfc+n7Q4t8Tkf7esFfvQe1IaMKxl791KSyyxRBoyZEh2O1qy119//fS73/0uHXLIIbW2W3bZZdPXvva1dNhhh2WzkkfP62nTpjX4mjFjeXQ9j+WMM85In//859Nll12WzWpetvrqq6ellloqLSqtGtMdzfhTpkypuX/OOeekjz76qOb+Bx98kNZee+22LSEAAADNq6r6pIt3S5ZutScXa1Rs15LXi/deCF26dEknn3xy+slPflJr0rTKLuZxya/oLl6+lFhzll566ayFO1rS21OrQnf0kY++9mXRXF85TXv0xY/p1wEAAKA1Ylx2BOpLLrmk3mMxE/l7772XXce7IZdffnn67ne/m81i/vLLL2djtH/84x9nP3fbbbda27777rtp8uTJtZa4bFkhQnepzox0de8DAADQAfRZ9pPrcDclHo/tFpFu3bqlo446KrtEdd3W6Zg7bLnllqs3u3nZJptskqZPn56+853vpHXWWSebUO3RRx9Nt99+e3a70he+8IWsBbxyieuE5/a5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AAA5EboBAAAgJ0I3AAAA5EToBgAAgJwI3QAAAJAToRsAWmGrrbZKxxxzTKv32U9/+tP07W9/u9D7etSoUamqqip99NFHqSNZbbXV0q9//esWb3/ZZZel3XbbLdcyAUCZ0A0AOZs8eXK68MIL0ymnnFLofb3ZZpulSZMmpf79+2f3R44cmZZaaqnU2Rx66KHpySefTP/3f//X3kUBYDEgdANAzq666qos0K666qrtuq+rq6ubfLxHjx5pwIABWWt3Zxafc7/99ku/+c1v2rsoACwGhG4AWAh33nln1jL8xz/+sdFtbrjhhnrdmW+++ea03nrrpd69e6dll102bbfddmnGjBnZYwcffHDac88904gRI9Lyyy+f+vXrl77zne+kOXPm1Dz/nnvuSV/5yleyluh4/te+9rX08ssv1zz+2muvZeH5xhtvTFtuuWXq1atXVsbXX389K8vSSy+dllhiibTOOuuku+66q1738rh9yCGHpClTpmTrYjnttNPS6aefntZdd916n3GDDTbIutA35MMPP0z7779/9lni86655prpmmuuqXn8zTffTPvuu29aZpllsjJ98YtfTP/85z+zx+Iz7bHHHmnFFVdMSy65ZPrSl76UHnjggSbrJMp/+OGH1+y7bbbZJv3rX/+qtU3sg7/+9a/p448/bvK1AGBhdVvoVwCAxdT111+fheH4GaG3If/973/T888/nwXJsujCHSHzl7/8Zfqf//mfNG3atKyrc6lUqtnmwQcfzIJyhN8I0BGAI1yfeeaZ2eMR0I877rg0bNiwNH369PSzn/0se62nn346deny2Tn1E088MZ1//vlpww03zF7viCOOyML76NGjs4AbZYswW1e0zMc46Xjd8ePHZ+tiuwi0cTLg8ccfzwJweOqpp9K///3vdOuttza4DyKMx/vcfffdabnllksTJkyoCbtR9jgpMGjQoCwER0t7dP2eP39+zeO77LJL9rl79uyZrrvuuiwwR5lWWWWVBt9v7733zsJ9vF+cELn88svTtttum/7zn/9kwT5EfcydOzcL9zFOHwDyInQDwAK4+OKLszHaf/vb37LQ2JiJEydmYXqllVaqFboj8H3961+v6XIerd51u0BfffXVqU+fPllrdLQwH3/88emMM87IQvVee+1Va/vYNlp2I9xWtkTHpG/xPpXlieeW3+9zn/tcg+WO94/AGi3cEYTLInjvuOOOWUt1OXTH7dgHjb1WvGeE/vKJh5j4rCxOWLz33ntZiC8H4iFDhtQ8vv7662dLWXz+2267LQvoRx11VL33euSRR9Jjjz2W3n333Sykh/POOy/dfvvtWe+C8mR2sV/j80XLPwDkSfdyAGilCG/HHntsuv/++5sM3KHcohutzGURIqPlNYJvtMpeeeWVWRfsSrFNBMOy4cOHZ62+b7zxRnb/pZdeylrLI+hGF+pykI2AW6myhT18//vfTz//+c/T5ptvnk499dSshbq1orX8T3/6U5o1a1bWah7BOSYna8x3v/vdrIt9dEE/4YQT0pgxY2oei5b5COTlwF1XfOYf/ehHaejQoVlX+gj9L7zwQr3PWRbdyOM50Ssgti0vr776aq3u9yFaw2fOnNnqzw8ArSF0A0ArRUiMVuVoXa7sEt6Q6E4dKkN1165ds8Ae3Z/XXnvt9Nvf/jZ94QtfyIJhS0UX6+i6HoE9ukiXx0BXjvsO0YW8Uox1fuWVV9K3vvWt9Mwzz2ShPN6/NeK9oxU5WpyjpT8maPvf//3fRrffeeedsxblOFHx9ttvZyccIkiXg29TYrt4n7POOivrgh8hPU5W1P2cZRG4Bw4cmG1XuUR39OgpUCn2X9QjAORJ6AaAVlpjjTXS3//+9/SXv/wlHX300c1uGy3R0e27UnTbjtbmGB8dY6KjO3eEy8oW28pJvh599NGsxXbw4MHpgw8+yELkT37ykyzARitw3ZbypsRrxFj0GIP9wx/+MAvuDYkyzZs3r976bt26pYMOOijrVh7LN7/5zWbDc4TbeM4f/vCHbKz4FVdcka2PMekRiiMAN+Qf//hHNrFcjFePsB1d3WOMe2M22mij7BJtUcbopl65lE+AhGj1jpb6OIECAHkSugFgAXz+85/Pgvctt9ySjZtu9B/aLl2ymcljrHFZtEpHy+0TTzyRdZOO8BvjmiM8l0VL7mGHHZaF9ZhdPLqCxxjmeL2YeTy6T0dwjUnJHnrooWxStZaIst57771Zq3pMWBafofJ9K0WX9Wg5jknd3n///VpdsaPFPN43ZlFvqmt5iMnY4gRFlPW5555Ld9xxR817Rhf5CNIxW3sE7GiFj306duzY7PGY6Tz2TwTzOBERl/oqT7LWkNjX0RU/Xu++++7LAnp0Z4/x97G/y6LVPLrmx0kRAMiT0A0ACyi6hEfwjPHN0WLcmAioMaa5HBaj5TtmD49ZuSO8R4t1zDAe3bDLogU7AucWW2yR9tlnn7T77rtnl+zK/vHu0iV7vXHjxmWTpkW37XPPPbdFZY6W6yOPPDILvTvttFP2/pdcckmD28YM5tEiHu8fLdUx23pZlC0eX2uttdKmm27a5HtGi/lJJ52UtWrH54nu9VH+8mMRjldYYYVsf0Rr9jnnnJNtE371q19lJxnivaJbe0ziFq3ZjYkeBHGSIt4nZnyPzxct8dG9PS47VhZ1FmPTASBvVaXmBqMBAAsl/qmNYBrhOFp2mxPdqePSXDHjdpE/UwTv733vey1uZS+KaG2Pa3fHJcRiBnMAyJOWbgDIWbS+RlfwuExYZxBd4S+66KJs7HS0Jnc0ccm2uN63wA3AouA63QCwCMTlsmLpDKIreExKFicSout3RxPjvgFgUdG9HAAAAHKiezkAAADkROgGAACAnAjdAAAAkBOhGwAAAHIidAMAAEBOhG4AAADIidANAAAAORG6AQAAICdCNwAAAKR8/H+QUCrPDknQHAAAAABJRU5ErkJggg==",
            "text/plain": [
              "<Figure size 1000x600 with 1 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "import os\n",
        "import math\n",
        "import pickle\n",
        "import warnings\n",
        "import time\n",
        "from typing import Optional, Dict, Any, Tuple, List\n",
        "warnings.filterwarnings(\"ignore\")\n",
        "import numpy as np\n",
        "import pandas as pd\n",
        "from sklearn.decomposition import TruncatedSVD\n",
        "from sklearn.linear_model import ElasticNet, Ridge\n",
        "from sklearn.ensemble import RandomForestRegressor\n",
        "from sklearn.model_selection import RandomizedSearchCV, TimeSeriesSplit\n",
        "from sklearn.metrics import (\n",
        "    mean_squared_error, mean_absolute_error, r2_score\n",
        ")\n",
        "import matplotlib.pyplot as plt\n",
        "import seaborn as sns\n",
        "\n",
        "# Optional model libraries\n",
        "try:\n",
        "    import lightgbm as lgb\n",
        "    LGB_AVAILABLE = True\n",
        "    print(\"[INFO] LightGBM is available.\")\n",
        "except Exception:\n",
        "    LGB_AVAILABLE = False\n",
        "    print(\"[INFO] LightGBM is NOT available.\")\n",
        "\n",
        "# ----------------------------- Utilities -----------------------------\n",
        "def safe_mkdir(path: str):\n",
        "    os.makedirs(path, exist_ok=True)\n",
        "\n",
        "def rmse(y_true, y_pred):\n",
        "    return math.sqrt(mean_squared_error(y_true, y_pred))\n",
        "\n",
        "# ----------------------------- Data Prep (Aggregate + Pad) -----------------------------\n",
        "def aggregate_monthly(df: pd.DataFrame) -> pd.DataFrame:\n",
        "    print(\"[INFO] Aggregating data to monthly level...\")\n",
        "    df = df.copy()\n",
        "    colmap = {}\n",
        "    for c in df.columns:\n",
        "        lc = c.lower().strip()\n",
        "        if lc in (\"dealer code\", \"dealercode\", \"dealer_code\", \"dealercode\"):\n",
        "            colmap[c] = \"DealerCode\"\n",
        "        if lc in (\"dummy id\", \"dummyid\", \"partno\", \"part_no\", \"dummy_id\"):\n",
        "            colmap[c] = \"DummyID\"\n",
        "        if lc in (\"sale date\", \"saledate\", \"sale_date\"):\n",
        "            colmap[c] = \"SaleDate\"\n",
        "        if lc in (\"part qty.\", \"part qty\", \"partqty\", \"qty\", \"quantity\"):\n",
        "            colmap[c] = \"PartQty\"\n",
        "        if lc in (\"sale type\", \"saletype\", \"sale_type\"):\n",
        "            colmap[c] = \"SaleType\"\n",
        "        if lc in (\"dealer price\", \"dealerprice\", \"price\"):\n",
        "            colmap[c] = \"DealerPrice\"\n",
        "        if lc in (\"prc_unit_weight\", \"weight\", \"prc unit weight\"):\n",
        "            colmap[c] = \"PRC_UNIT_WEIGHT\"\n",
        "        if lc in (\"prc_unit_cube\", \"cube\", \"prc unit cube\"):\n",
        "            colmap[c] = \"PRC_UNIT_CUBE\"\n",
        "        if lc in (\"otc_qty\",\"otc qty\",\"otcqty\"):\n",
        "            colmap[c] = \"OTC_Qty\"\n",
        "    df = df.rename(columns=colmap)\n",
        "    if \"SaleDate\" not in df.columns:\n",
        "        raise ValueError(\"Input must have 'SaleDate' column.\")\n",
        "    \n",
        "    df[\"SaleDate\"] = pd.to_datetime(df[\"SaleDate\"])\n",
        "    df[\"MonthStart\"] = df[\"SaleDate\"].dt.to_period(\"M\").dt.to_timestamp()\n",
        "    agg = df.groupby([\"DealerCode\", \"DummyID\", \"MonthStart\"], as_index=False).agg(\n",
        "        PartQty=(\"PartQty\", \"sum\"),\n",
        "        OTC_Qty=(\"PartQty\", lambda x: x[df.loc[x.index, \"SaleType\"].astype(str).str.upper() == \"OTC\"].sum() if \"SaleType\" in df.columns else 0),\n",
        "        DealerPrice=(\"DealerPrice\", \"mean\"),\n",
        "        PRC_UNIT_WEIGHT=(\"PRC_UNIT_WEIGHT\", \"mean\"),\n",
        "        PRC_UNIT_CUBE=(\"PRC_UNIT_CUBE\", \"mean\"),\n",
        "    )\n",
        "    agg[\"OTC_Qty\"] = agg[\"OTC_Qty\"].fillna(0).astype(int)\n",
        "    agg[\"OTC_Flag\"] = (agg[\"OTC_Qty\"] > 0).astype(int)\n",
        "    agg[[\"DealerPrice\", \"PRC_UNIT_WEIGHT\", \"PRC_UNIT_CUBE\"]] = agg[[\"DealerPrice\", \"PRC_UNIT_WEIGHT\", \"PRC_UNIT_CUBE\"]].fillna(0.0)\n",
        "    print(f\"[INFO] Aggregated to {len(agg)} monthly records.\")\n",
        "    return agg\n",
        "\n",
        "def pad_monthly_series_and_filter_inactive(df: pd.DataFrame) -> pd.DataFrame:\n",
        "    print(\"[INFO] Padding series to monthly grid and filtering inactive pairs...\")\n",
        "    df = df.copy()\n",
        "    df[\"MonthStart\"] = pd.to_datetime(df[\"MonthStart\"])\n",
        "    min_date = df[\"MonthStart\"].min()\n",
        "    max_date = df[\"MonthStart\"].max()\n",
        "    all_months = pd.date_range(start=min_date, end=max_date, freq=\"MS\")\n",
        "    print(f\"[INFO] Date range: {min_date.strftime('%Y-%m')} to {max_date.strftime('%Y-%m')}. Total months: {len(all_months)}\")\n",
        "    df_active = df.groupby([\"DealerCode\", \"DummyID\"]).filter(lambda g: g[\"PartQty\"].sum() > 0)\n",
        "    print(f\"[INFO] Filtered from {df.shape[0]} to {df_active.shape[0]} records after removing inactive pairs.\")\n",
        "    if df_active.empty:\n",
        "        print(\"[WARNING] No active series found after filtering.\")\n",
        "        return df_active\n",
        "\n",
        "    full_index = pd.MultiIndex.from_product(\n",
        "        [df_active[\"DealerCode\"].unique(), df_active[\"DummyID\"].unique(), all_months],\n",
        "        names=[\"DealerCode\", \"DummyID\", \"MonthStart\"]\n",
        "    )\n",
        "    df_indexed = df_active.set_index([\"DealerCode\", \"DummyID\", \"MonthStart\"])\n",
        "    df_padded = df_indexed.reindex(full_index, fill_value=0).reset_index()\n",
        "    df_padded[\"OTC_Flag\"] = (df_padded[\"OTC_Qty\"] > 0).astype(int)\n",
        "    for col in [\"PartQty\", \"OTC_Qty\", \"DealerPrice\", \"PRC_UNIT_WEIGHT\", \"PRC_UNIT_CUBE\"]:\n",
        "        df_padded[col] = pd.to_numeric(df_padded[col], errors=\"coerce\").fillna(0)\n",
        "    df_final = df_padded.groupby([\"DealerCode\", \"DummyID\"]).filter(lambda g: g[\"PartQty\"].sum() > 0).reset_index(drop=True)\n",
        "    print(f\"[INFO] Padding complete. Final shape: {df_final.shape}\")\n",
        "    return df_final\n",
        "\n",
        "# ----------------------------- Feature Engineering Functions -----------------------------\n",
        "def add_time_series_features(df: pd.DataFrame, max_lag: int = 12) -> pd.DataFrame:\n",
        "    print(f\"[INFO] Adding time series features (max_lag={max_lag})...\")\n",
        "    df = df.sort_values([\"DealerCode\", \"DummyID\", \"MonthStart\"]).copy()\n",
        "    rows = []\n",
        "    grp = df.groupby([\"DealerCode\", \"DummyID\"])\n",
        "    for (d, p), g in grp:\n",
        "        g = g.sort_values(\"MonthStart\").copy()\n",
        "        for lag in range(1, max_lag+1):\n",
        "            g[f\"qty_lag_{lag}\"] = g[\"PartQty\"].shift(lag).fillna(0)\n",
        "            g[f\"otc_lag_{lag}\"] = g[\"OTC_Flag\"].shift(lag).fillna(0)\n",
        "        g[\"qty_sum_1_3\"] = g[\"PartQty\"].rolling(3, min_periods=1).sum().shift(1).fillna(0)\n",
        "        g[\"qty_sum_1_6\"] = g[\"PartQty\"].rolling(6, min_periods=1).sum().shift(1).fillna(0)\n",
        "        g[\"nonzero_count_6\"] = g[\"PartQty\"].rolling(6, min_periods=1).apply(lambda x: np.sum(x>0)).shift(1).fillna(0)\n",
        "        last_pos = -1\n",
        "        t_since = []\n",
        "        for idx, val in enumerate(g[\"PartQty\"].values):\n",
        "            if val > 0:\n",
        "                last_pos = idx\n",
        "                t_since.append(0)\n",
        "            else:\n",
        "                t_since.append(idx - last_pos if last_pos != -1 else g.shape[0]+1)\n",
        "        g[\"time_since_last_sale\"] = pd.Series(t_since).astype(float)\n",
        "        g[\"qty_ma_3\"] = g[\"PartQty\"].rolling(3, min_periods=1).mean().shift(1).fillna(0)\n",
        "        g[\"qty_ma_6\"] = g[\"PartQty\"].rolling(6, min_periods=1).mean().shift(1).fillna(0)\n",
        "        g[\"otc_share_slope_3\"] = g[\"OTC_Flag\"].rolling(3, min_periods=1).apply(lambda x: np.polyfit(np.arange(len(x)), x, 1)[0] if len(x)>1 else 0).shift(1).fillna(0)\n",
        "        rows.append(g)\n",
        "    out = pd.concat(rows, axis=0).reset_index(drop=True)\n",
        "    out.fillna(0, inplace=True)\n",
        "    return out\n",
        "\n",
        "def compute_shos_for_series(y: np.ndarray, alpha_base: float = 0.2, beta_base: float = 0.2,\n",
        "                            adaptivity: bool = True, sparsity_k: float = 0.5,\n",
        "                            n_train: Optional[int] = None) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:\n",
        "    n = len(y)\n",
        "    if n == 0:\n",
        "        return np.zeros(0), np.zeros(0), np.zeros(0)\n",
        "\n",
        "    if n_train is not None and n_train > 0:\n",
        "        y_sparsity = y[:n_train]\n",
        "    else:\n",
        "        y_sparsity = y\n",
        "    \n",
        "    nonzero_count = np.sum(y_sparsity > 0)\n",
        "    sparsity = 1.0 - (nonzero_count / max(1, len(y_sparsity)))\n",
        "\n",
        "    if adaptivity:\n",
        "        alpha = alpha_base * (1.0 - sparsity_k * sparsity)\n",
        "        beta = beta_base * (1.0 - sparsity_k * sparsity)\n",
        "        last_pos_all = np.where(y > 0)[0]\n",
        "        if len(last_pos_all) > 0 and (n - 1 - last_pos_all[-1]) <= 3:\n",
        "            alpha = min(0.9, alpha * 1.2)\n",
        "            beta = min(0.9, beta * 1.2)\n",
        "    else:\n",
        "        alpha, beta = alpha_base, beta_base\n",
        "\n",
        "    if n >= 3:\n",
        "        init_window = 3\n",
        "        y_init_window = y[:init_window]\n",
        "        q0 = float(np.sum(y_init_window > 0)) / max(1, init_window)\n",
        "        positive_y_init = y_init_window[y_init_window > 0]\n",
        "        if len(positive_y_init) > 0:\n",
        "            z0 = float(np.mean(positive_y_init))\n",
        "        else:\n",
        "            positive_y_all = y[y > 0]\n",
        "            if len(positive_y_all) > 0:\n",
        "                z0 = float(np.mean(positive_y_all))\n",
        "            else:\n",
        "                z0 = 1.0\n",
        "    else:\n",
        "        q0 = float(np.sum(y > 0)) / max(1, n)\n",
        "        positive_y_all = y[y > 0]\n",
        "        if len(positive_y_all) > 0:\n",
        "            z0 = float(np.mean(positive_y_all))\n",
        "        else:\n",
        "            z0 = 1.0\n",
        "\n",
        "    q_prev, z_prev = q0, z0\n",
        "    q, z, f = np.zeros(n), np.zeros(n), np.zeros(n)\n",
        "\n",
        "    for t in range(n):\n",
        "        yt = y[t]\n",
        "        if yt > 0:\n",
        "            z_curr = alpha * yt + (1 - alpha) * z_prev\n",
        "            q_curr = beta * 1.0 + (1 - beta) * q_prev\n",
        "        else:\n",
        "            z_curr = z_prev\n",
        "            q_curr = beta * 0.0 + (1 - beta) * q_prev\n",
        "        q[t], z[t], f[t] = q_curr, z_curr, q_curr * z_curr\n",
        "        q_prev, z_prev = q_curr, z_curr\n",
        "\n",
        "    return q, z, f\n",
        "\n",
        "def add_shos_features_with_ntrain(df: pd.DataFrame, n_train_per_series: Dict[Tuple, int],\n",
        "                                  alpha_base: float = 0.2, beta_base: float = 0.2,\n",
        "                                  adaptivity: bool = True, sparsity_k: float = 0.5) -> pd.DataFrame:\n",
        "    print(f\"[INFO] Adding SHOS features with train-only sparsity (k={sparsity_k})...\")\n",
        "    rows = []\n",
        "    for (d, p), g in df.groupby([\"DealerCode\", \"DummyID\"]):\n",
        "        g = g.sort_values(\"MonthStart\").copy()\n",
        "        y = g[\"PartQty\"].values.astype(float)\n",
        "        n_train = n_train_per_series.get((d, p), len(y))\n",
        "        q, z, f = compute_shos_for_series(y, alpha_base, beta_base, adaptivity, sparsity_k, n_train)\n",
        "        g[\"q_shos\"] = q\n",
        "        g[\"z_shos\"] = z\n",
        "        g[\"shos_forecast\"] = f\n",
        "        rows.append(g)\n",
        "    out = pd.concat(rows, axis=0).reset_index(drop=True).fillna(0)\n",
        "    return out\n",
        "\n",
        "def build_low_rank_embeddings(df: pd.DataFrame, n_components: int = 16):\n",
        "    print(f\"[INFO] Building low-rank embeddings (n_components={n_components})...\")\n",
        "    occ = df.groupby([\"DealerCode\", \"DummyID\"]).agg(months_active=(\"PartQty\", lambda x: (x>0).sum())).reset_index()\n",
        "    pivot = occ.pivot(index=\"DealerCode\", columns=\"DummyID\", values=\"months_active\").fillna(0)\n",
        "    if pivot.shape[0] < 2 or pivot.shape[1] < 2:\n",
        "        print(\"[INFO] Pivot too small for SVD, creating empty embeddings.\")\n",
        "        dealer_embed = pd.DataFrame({\"DealerCode\": sorted(df[\"DealerCode\"].unique())})\n",
        "        part_embed = pd.DataFrame({\"DummyID\": sorted(df[\"DummyID\"].unique())})\n",
        "        return dealer_embed, part_embed, []\n",
        "    k = min(n_components, min(pivot.shape)-1)\n",
        "    svd = TruncatedSVD(n_components=k, random_state=0)\n",
        "    U = svd.fit_transform(pivot)\n",
        "    V = svd.components_.T\n",
        "    dealer_index = list(pivot.index)\n",
        "    part_index = list(pivot.columns)\n",
        "    dealer_embed = pd.DataFrame(U, index=dealer_index).reset_index().rename(columns={\"index\":\"DealerCode\"})\n",
        "    part_embed = pd.DataFrame(V, index=part_index).reset_index().rename(columns={\"index\":\"DummyID\"})\n",
        "    emb_cols = []\n",
        "    for i in range(U.shape[1]):\n",
        "        dealer_embed.rename(columns={i: f\"emb_dealer_{i}\"}, inplace=True)\n",
        "        emb_cols.append(f\"emb_dealer_{i}\")\n",
        "    for i in range(V.shape[1]):\n",
        "        part_embed.rename(columns={i: f\"emb_part_{i}\"}, inplace=True)\n",
        "        emb_cols.append(f\"emb_part_{i}\")\n",
        "    return dealer_embed, part_embed, emb_cols\n",
        "\n",
        "def build_base_features(df: pd.DataFrame, max_lag: int = 6):\n",
        "    print(\"[INFO] Building base feature matrix...\")\n",
        "    df2 = add_time_series_features(df, max_lag=max_lag)\n",
        "    lag_cols = [c for c in df2.columns if c.startswith((\"qty_lag_\", \"otc_lag_\"))]\n",
        "    base_cols = [\n",
        "        \"DealerPrice\", \"PRC_UNIT_WEIGHT\", \"PRC_UNIT_CUBE\",\n",
        "        \"qty_sum_1_3\", \"qty_sum_1_6\", \"nonzero_count_6\", \"time_since_last_sale\",\n",
        "        \"qty_ma_3\", \"qty_ma_6\", \"otc_share_slope_3\"\n",
        "    ] + lag_cols\n",
        "    dealer_stats = df2.groupby(\"DealerCode\").agg(dealer_qty_mean=(\"PartQty\", \"mean\")).reset_index()\n",
        "    part_stats = df2.groupby(\"DummyID\").agg(part_qty_mean=(\"PartQty\", \"mean\")).reset_index()\n",
        "    df2 = df2.merge(dealer_stats, on=\"DealerCode\", how=\"left\")\n",
        "    df2 = df2.merge(part_stats, on=\"DummyID\", how=\"left\")\n",
        "    extra_cols = [\"dealer_qty_mean\", \"part_qty_mean\"]\n",
        "    base_cols += extra_cols\n",
        "    print(f\"[INFO] Base features include {len(base_cols)} columns.\")\n",
        "    return df2, base_cols\n",
        "\n",
        "def build_fold_features(train_df: pd.DataFrame, test_df: pd.DataFrame, max_lag: int = 6, svd_components: int = 12, sparsity_k: float = 0.5):\n",
        "    print(f\"[INFO] Building fold features with sparsity_k={sparsity_k}...\")\n",
        "    train_base, base_cols = build_base_features(train_df, max_lag)\n",
        "    test_base, _ = build_base_features(test_df, max_lag)\n",
        "    n_train_map = train_df.groupby([\"DealerCode\", \"DummyID\"]).size().to_dict()\n",
        "    train_shos = add_shos_features_with_ntrain(train_base, n_train_map, sparsity_k=sparsity_k)\n",
        "    test_shos = add_shos_features_with_ntrain(test_base, n_train_map, sparsity_k=sparsity_k)\n",
        "    dealer_embed, part_embed, emb_cols = build_low_rank_embeddings(train_shos, svd_components)\n",
        "    if emb_cols:\n",
        "        train_shos = train_shos.merge(dealer_embed, on=\"DealerCode\", how=\"left\")\n",
        "        train_shos = train_shos.merge(part_embed, on=\"DummyID\", how=\"left\")\n",
        "        test_shos = test_shos.merge(dealer_embed, on=\"DealerCode\", how=\"left\")\n",
        "        test_shos = test_shos.merge(part_embed, on=\"DummyID\", how=\"left\")\n",
        "        for c in emb_cols:\n",
        "            train_shos[c] = train_shos[c].fillna(0)\n",
        "            test_shos[c] = test_shos[c].fillna(0)\n",
        "    full_cols = base_cols + [\"q_shos\", \"z_shos\"] + emb_cols\n",
        "    print(f\"[INFO] Fold features: {len(base_cols)} base, {len(emb_cols)} embeddings, {len(full_cols)} total.\")\n",
        "    return train_shos, test_shos, base_cols, full_cols\n",
        "\n",
        "# ----------------------------- Model Tuning -----------------------------\n",
        "def get_tuned_regressor(model_name: str, X_train, y_train, random_state=0):\n",
        "    \"\"\"Tunes a regressor model (for quantity)\"\"\"\n",
        "    print(f\"[INFO] Tuning Regressor {model_name}...\")\n",
        "    cv = TimeSeriesSplit(n_splits=3)\n",
        "    if model_name == \"LightGBM\" and LGB_AVAILABLE:\n",
        "        param_dist = {\"n_estimators\": [200, 500], \"learning_rate\": [0.01, 0.05, 0.1], \"num_leaves\": [15, 31, 63], \"min_child_samples\": [10, 20, 50]}\n",
        "        estimator = lgb.LGBMRegressor(random_state=random_state)\n",
        "\n",
        "    else:\n",
        "        raise ValueError(f\"Unknown model: {model_name}\")\n",
        "    \n",
        "    search = RandomizedSearchCV(estimator, param_dist, n_iter=10, cv=cv, scoring=\"neg_mean_absolute_error\", n_jobs=-1)\n",
        "    start_time = time.time()\n",
        "    search.fit(X_train, y_train)\n",
        "    print(f\"[INFO] Tuning {model_name} took {time.time() - start_time:.2f}s. Best score: {-search.best_score_:.4f}\")\n",
        "    return search.best_estimator_, search.best_params_\n",
        "\n",
        "# ----------------------------- K-Sensitivity Analysis Function -----------------------------\n",
        "def run_k_sensitivity(df_processed: pd.DataFrame, max_lag=6, svd_components=12, train_window=12, test_window=3):\n",
        "    print(\"[INFO] Starting K-Sensitivity Analysis on first fold...\")\n",
        "    df_processed = df_processed.sort_values([\"MonthStart\", \"DealerCode\", \"DummyID\"]).reset_index(drop=True)\n",
        "    all_months = sorted(df_processed[\"MonthStart\"].unique())\n",
        "    \n",
        "    # Get the first fold's data\n",
        "    train_months = all_months[0:train_window]\n",
        "    test_months = all_months[train_window:train_window + test_window]\n",
        "    train_df_fold = df_processed[df_processed[\"MonthStart\"].isin(train_months)].copy()\n",
        "    test_df_fold = df_processed[df_processed[\"MonthStart\"].isin(test_months)].copy()\n",
        "    \n",
        "    if train_df_fold.empty or test_df_fold.empty:\n",
        "        print(\"[ERROR] Cannot run sensitivity analysis, first fold data is empty.\")\n",
        "        return\n",
        "    \n",
        "    k_vals = [0.1, 0.3, 0.5, 0.7, 0.9]\n",
        "    results = []\n",
        "\n",
        "    y_test = test_df_fold[\"PartQty\"].values # Get actuals once\n",
        "\n",
        "    for k in k_vals:\n",
        "        print(f\"--- Testing k = {k} ---\")\n",
        "        \n",
        "        # 1. Build features using the specific 'k'\n",
        "        train_f, test_f, base_cols, full_cols = build_fold_features(\n",
        "            train_df_fold, test_df_fold, max_lag, svd_components, sparsity_k=k\n",
        "        )\n",
        "        \n",
        "        # 2. Prepare data for the '+SHOS' model\n",
        "        X_train_full = train_f[full_cols].fillna(0).values\n",
        "        y_train_raw = train_f[\"PartQty\"].values\n",
        "        X_test_full = test_f[full_cols].fillna(0).values\n",
        "        \n",
        "        # 3. Tune the '+SHOS' model (LGBM on full features, raw target)\n",
        "        # This is the most robust way to test 'k', as the best params might change\n",
        "        reg_shos, _ = get_tuned_regressor(\"LightGBM\", X_train_full, y_train_raw)\n",
        "        \n",
        "        # 4. Predict and Score\n",
        "        y_pred = reg_shos.predict(X_test_full)\n",
        "        mae = mean_absolute_error(y_test, y_pred)\n",
        "        rmse_val = rmse(y_test, y_pred)\n",
        "        \n",
        "        print(f\"[RESULT] k = {k}, MAE = {mae:.4f}, RMSE = {rmse_val:.4f}\")\n",
        "        results.append({\"k\": k, \"MAE\": mae, \"RMSE\": rmse_val})\n",
        "\n",
        "    # --- Process and plot results ---\n",
        "    results_df = pd.DataFrame(results)\n",
        "    print(\"\\n--- K-Sensitivity Analysis Results ---\")\n",
        "    print(results_df)\n",
        "\n",
        "    best_k_mae = results_df.loc[results_df['MAE'].idxmin()]\n",
        "    print(f\"\\nBest k based on MAE: {best_k_mae['k']} (MAE: {best_k_mae['MAE']:.4f})\")\n",
        "    \n",
        "    # Plotting\n",
        "    outdir = \"./k_sensitivity_results\"\n",
        "    safe_mkdir(outdir)\n",
        "    \n",
        "    plt.figure(figsize=(10, 6))\n",
        "    plt.plot(results_df[\"k\"], results_df[\"MAE\"], marker='o', label='MAE')\n",
        "    plt.plot(results_df[\"k\"], results_df[\"RMSE\"], marker='s', label='RMSE')\n",
        "    plt.xlabel(\"k (sparsity scale)\")\n",
        "    plt.ylabel(\"Error\")\n",
        "    plt.title(\"Sensitivity of Model Error to 'k' Parameter\")\n",
        "    plt.legend()\n",
        "    plt.grid(True)\n",
        "    plt.tight_layout()\n",
        "    plt.savefig(os.path.join(outdir, \"k_sensitivity_plot.png\"))\n",
        "    \n",
        "    print(f\"K-Sensitivity plot saved to {os.path.join(outdir, 'k_sensitivity_plot.png')}\")\n",
        "    \n",
        "    return best_k_mae['k']\n",
        "\n",
        "# ----------------------------- Entry Point -----------------------------\n",
        "if __name__ == \"__main__\":\n",
        "    \n",
        "    # --- 1. SET YOUR FILENAME HERE ---\n",
        "    # Use a raw string (r\"...\") for Windows paths to avoid errors\n",
        "    data_filename = r\"D:\\\\TimePassProjects\\\\col\\\\tr1\\\\data_without_key.xlsx\"\n",
        "    \n",
        "    # --- 2. Load the raw data ---\n",
        "    print(f\"[INFO] Loading data from: {data_filename}\")\n",
        "    try:\n",
        "        # Use read_excel for .xlsx files. Requires 'openpyxl' library\n",
        "        # Run 'pip install openpyxl' if you haven't already\n",
        "        df_raw = pd.read_excel(data_filename)\n",
        "    except FileNotFoundError:\n",
        "        print(f\"[ERROR] File not found: {data_filename}\")\n",
        "        print(\"Please make sure the file path is correct.\")\n",
        "        exit()\n",
        "    except ImportError:\n",
        "        print(\"[ERROR] 'openpyxl' library not found. Please run: pip install openpyxl\")\n",
        "        exit()\n",
        "    except Exception as e:\n",
        "        print(f\"[ERROR] Could not load data. Error: {e}\")\n",
        "        exit()\n",
        "\n",
        "    # --- 3. Clean and filter the raw data (as provided by user) ---\n",
        "    print(f\"Original number of rows: {len(df_raw)}\")\n",
        "    \n",
        "    num_duplicates = df_raw.duplicated().sum()\n",
        "    print(f\"Number of duplicate rows found: {num_duplicates}\")\n",
        "\n",
        "    df_cleaned = df_raw.drop_duplicates()\n",
        "    print(f\"Number of rows after removing duplicates: {len(df_cleaned)}\")\n",
        "\n",
        "    try:\n",
        "        # Using the exact column names from your snippet\n",
        "        df_filtered = df_cleaned[(df_cleaned['Part Qty.'] >= 0) &\n",
        "                                (df_cleaned['PRC_UNIT_WEIGHT'] > 0) &\n",
        "                                (df_cleaned['PRC_UNIT_CUBE'] > 0) &\n",
        "                                (df_cleaned['Dealer Price'].notna())]\n",
        "    except KeyError as e:\n",
        "        print(f\"\\n[ERROR] A column for filtering is missing: {e}\")\n",
        "        print(\"Please check the column names in your Excel file ('Part Qty.', 'PRC_UNIT_WEIGHT', etc.)\")\n",
        "        print(\"and make sure they match the script.\\n\")\n",
        "        exit()\n",
        "    \n",
        "    print(f\"Number of rows after filtering: {len(df_filtered)}\")\n",
        "    print(\"--- Filtered Data Head ---\")\n",
        "    print(df_filtered.head()) # Use print() instead of display() for .py script\n",
        "    print(\"--- Filtered Data Info ---\")\n",
        "    df_filtered.info()\n",
        "\n",
        "    # --- 4. Run the full preparation pipeline ---\n",
        "    # aggregate_monthly will handle the column name normalization (e.g., 'Part Qty.' -> 'PartQty')\n",
        "    df_agg = aggregate_monthly(df_filtered)\n",
        "    df_padded = pad_monthly_series_and_filter_inactive(df_agg)\n",
        "    \n",
        "    print(\"\\n[INFO] Data preparation summary:\")\n",
        "    print(df_padded.head())\n",
        "    print(f\"\\nTotal rows in processed data: {len(df_padded)}\")\n",
        "\n",
        "    # --- 5. Run K-Sensitivity Analysis ---\n",
        "    if not df_padded.empty:\n",
        "        best_k = run_k_sensitivity(df_padded, train_window=12, test_window=3)\n",
        "        print(f\"\\n[INFO] SENSITIVITY ANALYSIS COMPLETE. The best 'k' value is: {best_k}\")\n",
        "    else:\n",
        "        print(\"[ERROR] No data to evaluate after processing. Exiting.\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[INFO] LightGBM is available.\n",
            "[INFO] XGBoost is available.\n",
            "[INFO] SHAP is available.\n",
            "[INFO] Starting test pipeline...\n",
            "[INFO] 1. Aggregating and padding data...\n",
            "[INFO] Aggregating data to monthly level...\n",
            "[INFO] Aggregated to 171111 monthly records.\n",
            "[INFO] Padding series to monthly grid and filtering inactive pairs...\n",
            "[INFO] Date range: 2023-08 to 2025-08. Total months: 25\n",
            "[INFO] Filtered from 171111 to 170827 records after removing inactive pairs.\n",
            "[INFO] Padding complete. Final shape: (1404700, 9)\n",
            "[INFO] 2. Computing intermittency metrics...\n",
            "[INFO] Computing intermittency metrics (ADI, CV²)...\n",
            "[INFO] Computed metrics for 56188 active series. Median ADI: 25.00, Median CV²: 24.00\n",
            "[INFO] 3. Using pre-loaded tuned parameters...\n",
            "[INFO] 4. Building feature matrix once...\n",
            "[INFO] Building feature matrix ONCE for all folds...\n",
            "[INFO] Adding time series features (max_lag=6)...\n",
            "[INFO] Added 12 lag features.\n",
            "[INFO] Adding Croston features...\n",
            "[INFO] Added Croston features for 56188 unique series.\n",
            "[INFO] Adding SHOS features with train-only sparsity (k=0.1)...\n",
            "[INFO] Added SHOS features for 56188 unique series.\n",
            "[INFO] Building low-rank embeddings (n_components=12)...\n",
            "[INFO] Created 24 embedding features for 59 dealers and 7702 parts.\n",
            "[INFO] Final feature matrix has 50 columns and shape (1404700, 59).\n",
            "[INFO] 5. Running main cross-validation with optimized features...\n",
            "[INFO] Starting optimized cross-validation with 12-month train, 3-month test windows...\n",
            "[INFO] Using SHOS k=0.1, w=0.7\n",
            "[INFO] Total possible folds: 11\n",
            "[INFO] Processing Fold 1: Train 2023-08 to 2024-07, Test 2024-08 to 2024-10\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.040851 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2026\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 23\n",
            "[LightGBM] [Info] Start training from score -1.126032\n",
            "[INFO] Training LightGBM took 6.38s.\n",
            "[INFO] Training XGBoost with pre-tuned parameters...\n",
            "[INFO] Training XGBoost took 18.62s.\n",
            "[INFO] Training ElasticNet with pre-tuned parameters...\n",
            "[INFO] Training ElasticNet took 121.74s.\n",
            "[INFO] Training Ridge with pre-tuned parameters...\n",
            "[INFO] Training Ridge took 0.48s.\n",
            "[INFO] Training RandomForest with pre-tuned parameters...\n",
            "[INFO] Training RandomForest took 222.75s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.031934 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2026\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 23\n",
            "[LightGBM] [Info] Start training from score 0.324317\n",
            "[INFO] Training LightGBM took 4.99s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.061249 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6319\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 50\n",
            "[LightGBM] [Info] Start training from score -1.126032\n",
            "[INFO] Training LightGBM took 6.37s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 82284, number of negative: 591972\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.035102 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2026\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 23\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.122037 -> initscore=-1.973283\n",
            "[LightGBM] [Info] Start training from score -1.973283\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.004558 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2466\n",
            "[LightGBM] [Info] Number of data points in the train set: 82284, number of used features: 23\n",
            "[LightGBM] [Info] Start training from score 0.977401\n",
            "[INFO] Training Hurdle Model took 7.06s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 82284, number of negative: 591972\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.058169 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6316\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 49\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.122037 -> initscore=-1.973283\n",
            "[LightGBM] [Info] Start training from score -1.973283\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.007200 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6675\n",
            "[LightGBM] [Info] Number of data points in the train set: 82284, number of used features: 49\n",
            "[LightGBM] [Info] Start training from score 0.977401\n",
            "[INFO] Training Hurdle Model took 8.53s.\n",
            "[INFO] Processing Fold 2: Train 2023-09 to 2024-08, Test 2024-09 to 2024-11\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.046626 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2066\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score -1.129808\n",
            "[INFO] Training LightGBM took 3.63s.\n",
            "[INFO] Training XGBoost with pre-tuned parameters...\n",
            "[INFO] Training XGBoost took 15.86s.\n",
            "[INFO] Training ElasticNet with pre-tuned parameters...\n",
            "[INFO] Training ElasticNet took 115.41s.\n",
            "[INFO] Training Ridge with pre-tuned parameters...\n",
            "[INFO] Training Ridge took 0.15s.\n",
            "[INFO] Training RandomForest with pre-tuned parameters...\n",
            "[INFO] Training RandomForest took 227.37s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.041915 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2066\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score 0.323095\n",
            "[INFO] Training LightGBM took 4.98s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.061512 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6356\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 50\n",
            "[LightGBM] [Info] Start training from score -1.129808\n",
            "[INFO] Training LightGBM took 6.46s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 81660, number of negative: 592596\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.041026 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2066\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.121111 -> initscore=-1.981949\n",
            "[LightGBM] [Info] Start training from score -1.981949\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.004633 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2502\n",
            "[LightGBM] [Info] Number of data points in the train set: 81660, number of used features: 23\n",
            "[LightGBM] [Info] Start training from score 0.981238\n",
            "[INFO] Training Hurdle Model took 6.91s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 81660, number of negative: 592596\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.057936 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6353\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 49\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.121111 -> initscore=-1.981949\n",
            "[LightGBM] [Info] Start training from score -1.981949\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.007722 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6713\n",
            "[LightGBM] [Info] Number of data points in the train set: 81660, number of used features: 49\n",
            "[LightGBM] [Info] Start training from score 0.981238\n",
            "[INFO] Training Hurdle Model took 8.66s.\n",
            "[INFO] Processing Fold 3: Train 2023-10 to 2024-09, Test 2024-10 to 2024-12\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.042146 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2073\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score -1.130487\n",
            "[INFO] Training LightGBM took 3.63s.\n",
            "[INFO] Training XGBoost with pre-tuned parameters...\n",
            "[INFO] Training XGBoost took 15.67s.\n",
            "[INFO] Training ElasticNet with pre-tuned parameters...\n",
            "[INFO] Training ElasticNet took 108.35s.\n",
            "[INFO] Training Ridge with pre-tuned parameters...\n",
            "[INFO] Training Ridge took 0.15s.\n",
            "[INFO] Training RandomForest with pre-tuned parameters...\n",
            "[INFO] Training RandomForest took 230.75s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.048524 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2073\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score 0.322876\n",
            "[INFO] Training LightGBM took 4.89s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.060471 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6363\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 50\n",
            "[LightGBM] [Info] Start training from score -1.130487\n",
            "[INFO] Training LightGBM took 6.50s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 80860, number of negative: 593396\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.041016 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2073\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.119925 -> initscore=-1.993143\n",
            "[LightGBM] [Info] Start training from score -1.993143\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.003686 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2523\n",
            "[LightGBM] [Info] Number of data points in the train set: 80860, number of used features: 23\n",
            "[LightGBM] [Info] Start training from score 0.990403\n",
            "[INFO] Training Hurdle Model took 7.14s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 80860, number of negative: 593396\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.051972 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6360\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 49\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.119925 -> initscore=-1.993143\n",
            "[LightGBM] [Info] Start training from score -1.993143\n",
            "[LightGBM] [Info] Auto-choosing col-wise multi-threading, the overhead of testing was 0.016279 seconds.\n",
            "You can set `force_col_wise=true` to remove the overhead.\n",
            "[LightGBM] [Info] Total Bins 6733\n",
            "[LightGBM] [Info] Number of data points in the train set: 80860, number of used features: 49\n",
            "[LightGBM] [Info] Start training from score 0.990403\n",
            "[INFO] Training Hurdle Model took 9.63s.\n",
            "[INFO] Processing Fold 4: Train 2023-11 to 2024-10, Test 2024-11 to 2025-01\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.051413 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2070\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score -1.126261\n",
            "[INFO] Training LightGBM took 3.80s.\n",
            "[INFO] Training XGBoost with pre-tuned parameters...\n",
            "[INFO] Training XGBoost took 16.63s.\n",
            "[INFO] Training ElasticNet with pre-tuned parameters...\n",
            "[INFO] Training ElasticNet took 117.71s.\n",
            "[INFO] Training Ridge with pre-tuned parameters...\n",
            "[INFO] Training Ridge took 0.15s.\n",
            "[INFO] Training RandomForest with pre-tuned parameters...\n",
            "[INFO] Training RandomForest took 238.43s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.044320 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2070\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score 0.324243\n",
            "[INFO] Training LightGBM took 5.09s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.057862 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6360\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 50\n",
            "[LightGBM] [Info] Start training from score -1.126261\n",
            "[INFO] Training LightGBM took 6.56s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 80563, number of negative: 593693\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.044827 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2070\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.119484 -> initscore=-1.997323\n",
            "[LightGBM] [Info] Start training from score -1.997323\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.003765 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2598\n",
            "[LightGBM] [Info] Number of data points in the train set: 80563, number of used features: 23\n",
            "[LightGBM] [Info] Start training from score 0.998309\n",
            "[INFO] Training Hurdle Model took 7.29s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 80563, number of negative: 593693\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.059769 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6357\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 49\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.119484 -> initscore=-1.997323\n",
            "[LightGBM] [Info] Start training from score -1.997323\n",
            "[LightGBM] [Info] Auto-choosing col-wise multi-threading, the overhead of testing was 0.013160 seconds.\n",
            "You can set `force_col_wise=true` to remove the overhead.\n",
            "[LightGBM] [Info] Total Bins 6834\n",
            "[LightGBM] [Info] Number of data points in the train set: 80563, number of used features: 49\n",
            "[LightGBM] [Info] Start training from score 0.998309\n",
            "[INFO] Training Hurdle Model took 9.30s.\n",
            "[INFO] Processing Fold 5: Train 2023-12 to 2024-11, Test 2024-12 to 2025-02\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.029772 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2081\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score -1.120796\n",
            "[INFO] Training LightGBM took 2.88s.\n",
            "[INFO] Training XGBoost with pre-tuned parameters...\n",
            "[INFO] Training XGBoost took 12.90s.\n",
            "[INFO] Training ElasticNet with pre-tuned parameters...\n",
            "[INFO] Training ElasticNet took 74.40s.\n",
            "[INFO] Training Ridge with pre-tuned parameters...\n",
            "[INFO] Training Ridge took 0.13s.\n",
            "[INFO] Training RandomForest with pre-tuned parameters...\n",
            "[INFO] Training RandomForest took 213.21s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.029458 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2081\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score 0.326020\n",
            "[INFO] Training LightGBM took 3.92s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.041663 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6371\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 50\n",
            "[LightGBM] [Info] Start training from score -1.120796\n",
            "[INFO] Training LightGBM took 5.09s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 80149, number of negative: 594107\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.027990 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2081\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.118870 -> initscore=-2.003172\n",
            "[LightGBM] [Info] Start training from score -2.003172\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.003622 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2616\n",
            "[LightGBM] [Info] Number of data points in the train set: 80149, number of used features: 23\n",
            "[LightGBM] [Info] Start training from score 1.008926\n",
            "[INFO] Training Hurdle Model took 5.61s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 80149, number of negative: 594107\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.040685 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6368\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 49\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.118870 -> initscore=-2.003172\n",
            "[LightGBM] [Info] Start training from score -2.003172\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.005407 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6859\n",
            "[LightGBM] [Info] Number of data points in the train set: 80149, number of used features: 49\n",
            "[LightGBM] [Info] Start training from score 1.008926\n",
            "[INFO] Training Hurdle Model took 7.10s.\n",
            "[INFO] Processing Fold 6: Train 2024-01 to 2024-12, Test 2025-01 to 2025-03\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.028267 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2088\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score -1.110554\n",
            "[INFO] Training LightGBM took 2.94s.\n",
            "[INFO] Training XGBoost with pre-tuned parameters...\n",
            "[INFO] Training XGBoost took 12.89s.\n",
            "[INFO] Training ElasticNet with pre-tuned parameters...\n",
            "[INFO] Training ElasticNet took 65.32s.\n",
            "[INFO] Training Ridge with pre-tuned parameters...\n",
            "[INFO] Training Ridge took 0.13s.\n",
            "[INFO] Training RandomForest with pre-tuned parameters...\n",
            "[INFO] Training RandomForest took 218.98s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.027695 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2088\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score 0.329376\n",
            "[INFO] Training LightGBM took 3.98s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.042181 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6378\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 50\n",
            "[LightGBM] [Info] Start training from score -1.110554\n",
            "[INFO] Training LightGBM took 4.97s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 80070, number of negative: 594186\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.028561 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2088\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.118753 -> initscore=-2.004291\n",
            "[LightGBM] [Info] Start training from score -2.004291\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.003501 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2634\n",
            "[LightGBM] [Info] Number of data points in the train set: 80070, number of used features: 23\n",
            "[LightGBM] [Info] Start training from score 1.020154\n",
            "[INFO] Training Hurdle Model took 5.53s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 80070, number of negative: 594186\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.046940 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6375\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 49\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.118753 -> initscore=-2.004291\n",
            "[LightGBM] [Info] Start training from score -2.004291\n",
            "[LightGBM] [Info] Auto-choosing col-wise multi-threading, the overhead of testing was 0.010304 seconds.\n",
            "You can set `force_col_wise=true` to remove the overhead.\n",
            "[LightGBM] [Info] Total Bins 6890\n",
            "[LightGBM] [Info] Number of data points in the train set: 80070, number of used features: 49\n",
            "[LightGBM] [Info] Start training from score 1.020154\n",
            "[INFO] Training Hurdle Model took 6.94s.\n",
            "[INFO] Processing Fold 7: Train 2024-02 to 2025-01, Test 2025-02 to 2025-04\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.030336 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2013\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score -1.099115\n",
            "[INFO] Training LightGBM took 3.20s.\n",
            "[INFO] Training XGBoost with pre-tuned parameters...\n",
            "[INFO] Training XGBoost took 14.87s.\n",
            "[INFO] Training ElasticNet with pre-tuned parameters...\n",
            "[INFO] Training ElasticNet took 98.23s.\n",
            "[INFO] Training Ridge with pre-tuned parameters...\n",
            "[INFO] Training Ridge took 0.14s.\n",
            "[INFO] Training RandomForest with pre-tuned parameters...\n",
            "[INFO] Training RandomForest took 228.50s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.030327 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2013\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score 0.333166\n",
            "[INFO] Training LightGBM took 4.38s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.043314 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6303\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 50\n",
            "[LightGBM] [Info] Start training from score -1.099115\n",
            "[INFO] Training LightGBM took 5.42s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 80195, number of negative: 594061\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.030442 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2013\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.118939 -> initscore=-2.002521\n",
            "[LightGBM] [Info] Start training from score -2.002521\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.004683 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2645\n",
            "[LightGBM] [Info] Number of data points in the train set: 80195, number of used features: 23\n",
            "[LightGBM] [Info] Start training from score 1.030034\n",
            "[INFO] Training Hurdle Model took 6.33s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 80195, number of negative: 594061\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.042406 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6300\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 49\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.118939 -> initscore=-2.002521\n",
            "[LightGBM] [Info] Start training from score -2.002521\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.006714 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6898\n",
            "[LightGBM] [Info] Number of data points in the train set: 80195, number of used features: 49\n",
            "[LightGBM] [Info] Start training from score 1.030034\n",
            "[INFO] Training Hurdle Model took 8.21s.\n",
            "[INFO] Processing Fold 8: Train 2024-03 to 2025-02, Test 2025-03 to 2025-05\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.038521 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2025\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score -1.098190\n",
            "[INFO] Training LightGBM took 3.76s.\n",
            "[INFO] Training XGBoost with pre-tuned parameters...\n",
            "[INFO] Training XGBoost took 15.33s.\n",
            "[INFO] Training ElasticNet with pre-tuned parameters...\n",
            "[INFO] Training ElasticNet took 105.26s.\n",
            "[INFO] Training Ridge with pre-tuned parameters...\n",
            "[INFO] Training Ridge took 0.14s.\n",
            "[INFO] Training RandomForest with pre-tuned parameters...\n",
            "[INFO] Training RandomForest took 234.05s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.039933 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2025\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score 0.333474\n",
            "[INFO] Training LightGBM took 4.87s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.053675 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6315\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 50\n",
            "[LightGBM] [Info] Start training from score -1.098190\n",
            "[INFO] Training LightGBM took 6.44s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 79959, number of negative: 594297\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.034461 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2025\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.118588 -> initscore=-2.005865\n",
            "[LightGBM] [Info] Start training from score -2.005865\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.004880 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2511\n",
            "[LightGBM] [Info] Number of data points in the train set: 79959, number of used features: 23\n",
            "[LightGBM] [Info] Start training from score 1.033906\n",
            "[INFO] Training Hurdle Model took 6.95s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 79959, number of negative: 594297\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.057744 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6312\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 49\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.118588 -> initscore=-2.005865\n",
            "[LightGBM] [Info] Start training from score -2.005865\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.006178 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6764\n",
            "[LightGBM] [Info] Number of data points in the train set: 79959, number of used features: 49\n",
            "[LightGBM] [Info] Start training from score 1.033906\n",
            "[INFO] Training Hurdle Model took 8.88s.\n",
            "[INFO] Processing Fold 9: Train 2024-04 to 2025-03, Test 2025-04 to 2025-06\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.027367 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2035\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score -1.084712\n",
            "[INFO] Training LightGBM took 3.17s.\n",
            "[INFO] Training XGBoost with pre-tuned parameters...\n",
            "[INFO] Training XGBoost took 14.11s.\n",
            "[INFO] Training ElasticNet with pre-tuned parameters...\n",
            "[INFO] Training ElasticNet took 92.72s.\n",
            "[INFO] Training Ridge with pre-tuned parameters...\n",
            "[INFO] Training Ridge took 0.14s.\n",
            "[INFO] Training RandomForest with pre-tuned parameters...\n",
            "[INFO] Training RandomForest took 229.33s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.034500 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2035\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score 0.337999\n",
            "[INFO] Training LightGBM took 4.41s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.043203 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6325\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 50\n",
            "[LightGBM] [Info] Start training from score -1.084712\n",
            "[INFO] Training LightGBM took 5.20s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 80413, number of negative: 593843\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.028857 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2035\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.119262 -> initscore=-1.999439\n",
            "[LightGBM] [Info] Start training from score -1.999439\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.004317 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2661\n",
            "[LightGBM] [Info] Number of data points in the train set: 80413, number of used features: 23\n",
            "[LightGBM] [Info] Start training from score 1.041722\n",
            "[INFO] Training Hurdle Model took 5.79s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 80413, number of negative: 593843\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.042589 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6322\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 49\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.119262 -> initscore=-1.999439\n",
            "[LightGBM] [Info] Start training from score -1.999439\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.005587 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6913\n",
            "[LightGBM] [Info] Number of data points in the train set: 80413, number of used features: 49\n",
            "[LightGBM] [Info] Start training from score 1.041722\n",
            "[INFO] Training Hurdle Model took 7.31s.\n",
            "[INFO] Processing Fold 10: Train 2024-05 to 2025-04, Test 2025-05 to 2025-07\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.028142 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2045\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score -1.077711\n",
            "[INFO] Training LightGBM took 3.13s.\n",
            "[INFO] Training XGBoost with pre-tuned parameters...\n",
            "[INFO] Training XGBoost took 14.01s.\n",
            "[INFO] Training ElasticNet with pre-tuned parameters...\n",
            "[INFO] Training ElasticNet took 93.49s.\n",
            "[INFO] Training Ridge with pre-tuned parameters...\n",
            "[INFO] Training Ridge took 0.14s.\n",
            "[INFO] Training RandomForest with pre-tuned parameters...\n",
            "[INFO] Training RandomForest took 233.04s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.028000 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2045\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score 0.340374\n",
            "[INFO] Training LightGBM took 4.19s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.043294 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6335\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 50\n",
            "[LightGBM] [Info] Start training from score -1.077711\n",
            "[INFO] Training LightGBM took 5.29s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 80567, number of negative: 593689\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.037610 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2045\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.119490 -> initscore=-1.997266\n",
            "[LightGBM] [Info] Start training from score -1.997266\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.003982 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2659\n",
            "[LightGBM] [Info] Number of data points in the train set: 80567, number of used features: 23\n",
            "[LightGBM] [Info] Start training from score 1.046810\n",
            "[INFO] Training Hurdle Model took 6.22s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 80567, number of negative: 593689\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.042867 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6332\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 49\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.119490 -> initscore=-1.997266\n",
            "[LightGBM] [Info] Start training from score -1.997266\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.005372 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6915\n",
            "[LightGBM] [Info] Number of data points in the train set: 80567, number of used features: 49\n",
            "[LightGBM] [Info] Start training from score 1.046810\n",
            "[INFO] Training Hurdle Model took 6.93s.\n",
            "[INFO] Processing Fold 11: Train 2024-06 to 2025-05, Test 2025-06 to 2025-08\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.038369 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2054\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score -1.068598\n",
            "[INFO] Training LightGBM took 3.82s.\n",
            "[INFO] Training XGBoost with pre-tuned parameters...\n",
            "[INFO] Training XGBoost took 16.81s.\n",
            "[INFO] Training ElasticNet with pre-tuned parameters...\n",
            "[INFO] Training ElasticNet took 115.68s.\n",
            "[INFO] Training Ridge with pre-tuned parameters...\n",
            "[INFO] Training Ridge took 0.15s.\n",
            "[INFO] Training RandomForest with pre-tuned parameters...\n",
            "[INFO] Training RandomForest took 242.43s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.040794 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2054\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] Start training from score 0.343490\n",
            "[INFO] Training LightGBM took 5.11s.\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.049797 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6344\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 50\n",
            "[LightGBM] [Info] Start training from score -1.068598\n",
            "[INFO] Training LightGBM took 6.80s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 80568, number of negative: 593688\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.039171 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2054\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 24\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.119492 -> initscore=-1.997252\n",
            "[LightGBM] [Info] Start training from score -1.997252\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.004001 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2521\n",
            "[LightGBM] [Info] Number of data points in the train set: 80568, number of used features: 23\n",
            "[LightGBM] [Info] Start training from score 1.055910\n",
            "[INFO] Training Hurdle Model took 7.15s.\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 80568, number of negative: 593688\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.052262 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6341\n",
            "[LightGBM] [Info] Number of data points in the train set: 674256, number of used features: 49\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.119492 -> initscore=-1.997252\n",
            "[LightGBM] [Info] Start training from score -1.997252\n",
            "[LightGBM] [Info] Auto-choosing col-wise multi-threading, the overhead of testing was 0.015758 seconds.\n",
            "You can set `force_col_wise=true` to remove the overhead.\n",
            "[LightGBM] [Info] Total Bins 6782\n",
            "[LightGBM] [Info] Number of data points in the train set: 80568, number of used features: 49\n",
            "[LightGBM] [Info] Start training from score 1.055910\n",
            "[INFO] Training Hurdle Model took 9.51s.\n",
            "[INFO] Cross-validation completed. Estimated valid folds per model (approx): 11\n",
            "--- OVERFITTING DIAGNOSIS ---\n",
            "                 Test_MAE  Train_MAE  Diff_MAE   Test_R2  Train_R2   Diff_R2\n",
            "LightGBM         0.132648   0.107749  0.024899  0.757824  0.890840 -0.133017\n",
            "XGBoost          0.141281   0.074983  0.066298  0.715633  0.977526 -0.261893\n",
            "ElasticNet       0.343950   0.344555 -0.000605  0.668485  0.653922  0.014563\n",
            "Ridge            0.343882   0.344444 -0.000562  0.668474  0.653925  0.014549\n",
            "RandomForest     0.136390   0.075880  0.060509  0.737699  0.911559 -0.173859\n",
            "Baseline         0.141702   0.113799  0.027903  0.727942  0.877069 -0.149127\n",
            "+SHOS            0.071165   0.049946  0.021218  0.842978  0.953598 -0.110620\n",
            "Hurdle_Baseline  0.134077   0.108923  0.025154  0.757683  0.892857 -0.135175\n",
            "Hurdle_SHOS      0.070148   0.036661  0.033487  0.818597  0.985684 -0.167087\n",
            "Croston          1.168591        NaN       NaN -1.220974       NaN       NaN\n",
            "TSB              0.273764        NaN       NaN  0.795732       NaN       NaN\n",
            "Interpretation: Positive Diff_MAE or negative Diff_R2 indicates possible overfitting (Test worse than Train).\n",
            "[INFO] 6. Saving results...\n",
            " --- TEST RESULTS (Mean ± Std) --- \n",
            "                 MAE_mean  MAE_std  RMSE_mean  RMSE_std  WMAPE_mean  \\\n",
            "LightGBM           0.1326   0.0046     1.2900    0.1079     37.9990   \n",
            "XGBoost            0.1413   0.0045     1.3960    0.1074     40.4895   \n",
            "ElasticNet         0.3439   0.0134     1.5101    0.1160     98.5927   \n",
            "Ridge              0.3439   0.0134     1.5101    0.1161     98.5736   \n",
            "RandomForest       0.1364   0.0046     1.3411    0.1057     39.0798   \n",
            "Baseline           0.1417   0.0048     1.3657    0.1206     40.6006   \n",
            "+SHOS              0.0712   0.0028     1.0310    0.1416     20.3874   \n",
            "Hurdle_Baseline    0.1341   0.0050     1.2911    0.0934     38.4060   \n",
            "Hurdle_SHOS        0.0701   0.0023     1.1136    0.1102     20.0988   \n",
            "Croston            1.1686   0.0070     3.9205    0.0597    335.1457   \n",
            "TSB                0.2738   0.0085     1.1855    0.0785     78.4779   \n",
            "\n",
            "                 WMAPE_std  MAPE_mean  MAPE_std  R2_mean  R2_std  \n",
            "LightGBM            0.6784    38.2541    1.2010   0.7578  0.0418  \n",
            "XGBoost             1.2591    40.4059    1.3697   0.7156  0.0494  \n",
            "ElasticNet          4.2591    69.9993    0.6101   0.6685  0.0535  \n",
            "Ridge               4.2761    69.9890    0.6063   0.6685  0.0535  \n",
            "RandomForest        1.0259    40.2763    1.5365   0.7377  0.0484  \n",
            "Baseline            1.0377    41.5587    1.4505   0.7279  0.0507  \n",
            "+SHOS               0.5724    17.7271    0.4695   0.8430  0.0460  \n",
            "Hurdle_Baseline     0.7119    37.9096    1.2898   0.7577  0.0379  \n",
            "Hurdle_SHOS         0.4707    16.8419    0.4387   0.8186  0.0413  \n",
            "Croston            12.6582    84.0465    2.0539  -1.2210  0.1002  \n",
            "TSB                 2.9513    58.8357    0.7874   0.7957  0.0306  \n",
            " --- TRAIN RESULTS (Mean ± Std) --- \n",
            "                 MAE_mean  MAE_std  RMSE_mean  RMSE_std  WMAPE_mean  \\\n",
            "LightGBM           0.1077   0.0022     0.8976    0.0384     32.5839   \n",
            "XGBoost            0.0750   0.0028     0.4065    0.0287     22.6806   \n",
            "ElasticNet         0.3446   0.0067     1.5978    0.0662    104.2312   \n",
            "Ridge              0.3444   0.0067     1.5978    0.0662    104.1980   \n",
            "RandomForest       0.0759   0.0017     0.8078    0.0337     22.9508   \n",
            "Baseline           0.1138   0.0023     0.9524    0.0386     34.4157   \n",
            "+SHOS              0.0499   0.0035     0.5847    0.0421     15.0881   \n",
            "Hurdle_Baseline    0.1089   0.0022     0.8891    0.0386     32.9392   \n",
            "Hurdle_SHOS        0.0367   0.0030     0.3251    0.0156     11.0721   \n",
            "Croston               NaN      NaN        NaN       NaN         NaN   \n",
            "TSB                   NaN      NaN        NaN       NaN         NaN   \n",
            "\n",
            "                 WMAPE_std  MAPE_mean  MAPE_std  R2_mean  R2_std  \n",
            "LightGBM            0.7284    34.4420    0.5832   0.8908  0.0097  \n",
            "XGBoost             1.0240    29.6174    0.9430   0.9775  0.0035  \n",
            "ElasticNet          3.5102    70.2440    0.5913   0.6539  0.0311  \n",
            "Ridge               3.5196    70.2345    0.5916   0.6539  0.0311  \n",
            "RandomForest        0.7036    23.0789    0.5368   0.9116  0.0080  \n",
            "Baseline            0.8628    38.0955    0.6089   0.8771  0.0109  \n",
            "+SHOS               0.7964    13.8786    1.1428   0.9536  0.0061  \n",
            "Hurdle_Baseline     0.7300    34.0314    0.5661   0.8929  0.0101  \n",
            "Hurdle_SHOS         0.7121    11.0480    1.1253   0.9857  0.0012  \n",
            "Croston                NaN        NaN       NaN      NaN     NaN  \n",
            "TSB                    NaN        NaN       NaN      NaN     NaN  \n",
            " --- OVERFITTING DIAGNOSIS (Test - Train) --- \n",
            "                 Test_MAE  Train_MAE  Diff_MAE  Test_R2  Train_R2  Diff_R2\n",
            "LightGBM           0.1326     0.1077    0.0249   0.7578    0.8908  -0.1330\n",
            "XGBoost            0.1413     0.0750    0.0663   0.7156    0.9775  -0.2619\n",
            "ElasticNet         0.3439     0.3446   -0.0006   0.6685    0.6539   0.0146\n",
            "Ridge              0.3439     0.3444   -0.0006   0.6685    0.6539   0.0145\n",
            "RandomForest       0.1364     0.0759    0.0605   0.7377    0.9116  -0.1739\n",
            "Baseline           0.1417     0.1138    0.0279   0.7279    0.8771  -0.1491\n",
            "+SHOS              0.0712     0.0499    0.0212   0.8430    0.9536  -0.1106\n",
            "Hurdle_Baseline    0.1341     0.1089    0.0252   0.7577    0.8929  -0.1352\n",
            "Hurdle_SHOS        0.0701     0.0367    0.0335   0.8186    0.9857  -0.1671\n",
            "Croston            1.1686        NaN       NaN  -1.2210       NaN      NaN\n",
            "TSB                0.2738        NaN       NaN   0.7957       NaN      NaN\n",
            "[INFO] 7. Generating figures...\n"
          ]
        },
        {
          "ename": "ValueError",
          "evalue": "shape mismatch: objects cannot be broadcast to a single shape.  Mismatch is between arg 0 with shape (9,) and arg 1 with shape (11,).",
          "output_type": "error",
          "traceback": [
            "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
            "\u001b[31mValueError\u001b[39m                                Traceback (most recent call last)",
            "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[5]\u001b[39m\u001b[32m, line 1130\u001b[39m\n\u001b[32m   1126\u001b[39m \u001b[38;5;66;03m# ----------------------------- Entry Point -----------------------------\u001b[39;00m\n\u001b[32m   1127\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[34m__name__\u001b[39m == \u001b[33m\"\u001b[39m\u001b[33m__main__\u001b[39m\u001b[33m\"\u001b[39m:\n\u001b[32m   1128\u001b[39m     \u001b[38;5;66;03m# Example usage:\u001b[39;00m\n\u001b[32m   1129\u001b[39m     \u001b[38;5;66;03m# df_filtered = pd.read_csv(\"your_data.csv\")\u001b[39;00m\n\u001b[32m-> \u001b[39m\u001b[32m1130\u001b[39m     \u001b[43mrun_test_pipeline\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdf_filtered\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43moutdir\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43m./result_test1\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mshos_k\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m0.1\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mshos_w\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m0.7\u001b[39;49m\u001b[43m)\u001b[49m \u001b[38;5;66;03m# Provide your best k, w\u001b[39;00m\n\u001b[32m   1131\u001b[39m     \u001b[38;5;28;01mpass\u001b[39;00m\n",
            "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[5]\u001b[39m\u001b[32m, line 1081\u001b[39m, in \u001b[36mrun_test_pipeline\u001b[39m\u001b[34m(df_filtered, outdir, shos_k, shos_w)\u001b[39m\n\u001b[32m   1079\u001b[39m \u001b[38;5;66;03m# Figure 5: Overfitting Bar Chart (Difference)\u001b[39;00m\n\u001b[32m   1080\u001b[39m fig, ax = plt.subplots(figsize=(\u001b[32m12\u001b[39m, \u001b[32m6\u001b[39m))\n\u001b[32m-> \u001b[39m\u001b[32m1081\u001b[39m \u001b[43max\u001b[49m\u001b[43m.\u001b[49m\u001b[43mbar\u001b[49m\u001b[43m(\u001b[49m\u001b[43mx\u001b[49m\u001b[43m \u001b[49m\u001b[43m-\u001b[49m\u001b[43m \u001b[49m\u001b[43mwidth\u001b[49m\u001b[43m/\u001b[49m\u001b[32;43m2\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43moverfitting_df\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mDiff_MAE\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mwidth\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlabel\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mTest MAE - Train MAE\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43malpha\u001b[49m\u001b[43m=\u001b[49m\u001b[32;43m0.8\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m   1082\u001b[39m ax.axhline(\u001b[32m0\u001b[39m, color=\u001b[33m'\u001b[39m\u001b[33mblack\u001b[39m\u001b[33m'\u001b[39m, linewidth=\u001b[32m0.8\u001b[39m) \u001b[38;5;66;03m# Add a horizontal line at 0\u001b[39;00m\n\u001b[32m   1083\u001b[39m ax.set_xticks(x)\n",
            "\u001b[36mFile \u001b[39m\u001b[32md:\\TimePassProjects\\col\\finl1\\.venv\\Lib\\site-packages\\matplotlib\\__init__.py:1524\u001b[39m, in \u001b[36m_preprocess_data.<locals>.inner\u001b[39m\u001b[34m(ax, data, *args, **kwargs)\u001b[39m\n\u001b[32m   1521\u001b[39m \u001b[38;5;129m@functools\u001b[39m.wraps(func)\n\u001b[32m   1522\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34minner\u001b[39m(ax, *args, data=\u001b[38;5;28;01mNone\u001b[39;00m, **kwargs):\n\u001b[32m   1523\u001b[39m     \u001b[38;5;28;01mif\u001b[39;00m data \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m-> \u001b[39m\u001b[32m1524\u001b[39m         \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mfunc\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m   1525\u001b[39m \u001b[43m            \u001b[49m\u001b[43max\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m   1526\u001b[39m \u001b[43m            \u001b[49m\u001b[43m*\u001b[49m\u001b[38;5;28;43mmap\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mcbook\u001b[49m\u001b[43m.\u001b[49m\u001b[43msanitize_sequence\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43margs\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m   1527\u001b[39m \u001b[43m            \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43m{\u001b[49m\u001b[43mk\u001b[49m\u001b[43m:\u001b[49m\u001b[43m \u001b[49m\u001b[43mcbook\u001b[49m\u001b[43m.\u001b[49m\u001b[43msanitize_sequence\u001b[49m\u001b[43m(\u001b[49m\u001b[43mv\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mfor\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mk\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mv\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;129;43;01min\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m.\u001b[49m\u001b[43mitems\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m}\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m   1529\u001b[39m     bound = new_sig.bind(ax, *args, **kwargs)\n\u001b[32m   1530\u001b[39m     auto_label = (bound.arguments.get(label_namer)\n\u001b[32m   1531\u001b[39m                   \u001b[38;5;129;01mor\u001b[39;00m bound.kwargs.get(label_namer))\n",
            "\u001b[36mFile \u001b[39m\u001b[32md:\\TimePassProjects\\col\\finl1\\.venv\\Lib\\site-packages\\matplotlib\\axes\\_axes.py:2583\u001b[39m, in \u001b[36mAxes.bar\u001b[39m\u001b[34m(self, x, height, width, bottom, align, **kwargs)\u001b[39m\n\u001b[32m   2580\u001b[39m     \u001b[38;5;28;01mif\u001b[39;00m yerr \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m   2581\u001b[39m         yerr = \u001b[38;5;28mself\u001b[39m._convert_dx(yerr, y0, y, \u001b[38;5;28mself\u001b[39m.convert_yunits)\n\u001b[32m-> \u001b[39m\u001b[32m2583\u001b[39m x, height, width, y, linewidth, hatch = \u001b[43mnp\u001b[49m\u001b[43m.\u001b[49m\u001b[43mbroadcast_arrays\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m   2584\u001b[39m \u001b[43m    \u001b[49m\u001b[38;5;66;43;03m# Make args iterable too.\u001b[39;49;00m\n\u001b[32m   2585\u001b[39m \u001b[43m    \u001b[49m\u001b[43mnp\u001b[49m\u001b[43m.\u001b[49m\u001b[43matleast_1d\u001b[49m\u001b[43m(\u001b[49m\u001b[43mx\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mheight\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mwidth\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43my\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlinewidth\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mhatch\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m   2587\u001b[39m \u001b[38;5;66;03m# Now that units have been converted, set the tick locations.\u001b[39;00m\n\u001b[32m   2588\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m orientation == \u001b[33m'\u001b[39m\u001b[33mvertical\u001b[39m\u001b[33m'\u001b[39m:\n",
            "\u001b[36mFile \u001b[39m\u001b[32md:\\TimePassProjects\\col\\finl1\\.venv\\Lib\\site-packages\\numpy\\lib\\_stride_tricks_impl.py:544\u001b[39m, in \u001b[36mbroadcast_arrays\u001b[39m\u001b[34m(subok, *args)\u001b[39m\n\u001b[32m    537\u001b[39m \u001b[38;5;66;03m# nditer is not used here to avoid the limit of 32 arrays.\u001b[39;00m\n\u001b[32m    538\u001b[39m \u001b[38;5;66;03m# Otherwise, something like the following one-liner would suffice:\u001b[39;00m\n\u001b[32m    539\u001b[39m \u001b[38;5;66;03m# return np.nditer(args, flags=['multi_index', 'zerosize_ok'],\u001b[39;00m\n\u001b[32m    540\u001b[39m \u001b[38;5;66;03m#                  order='C').itviews\u001b[39;00m\n\u001b[32m    542\u001b[39m args = [np.array(_m, copy=\u001b[38;5;28;01mNone\u001b[39;00m, subok=subok) \u001b[38;5;28;01mfor\u001b[39;00m _m \u001b[38;5;129;01min\u001b[39;00m args]\n\u001b[32m--> \u001b[39m\u001b[32m544\u001b[39m shape = \u001b[43m_broadcast_shape\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m    546\u001b[39m result = [array \u001b[38;5;28;01mif\u001b[39;00m array.shape == shape\n\u001b[32m    547\u001b[39m           \u001b[38;5;28;01melse\u001b[39;00m _broadcast_to(array, shape, subok=subok, readonly=\u001b[38;5;28;01mFalse\u001b[39;00m)\n\u001b[32m    548\u001b[39m                           \u001b[38;5;28;01mfor\u001b[39;00m array \u001b[38;5;129;01min\u001b[39;00m args]\n\u001b[32m    549\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mtuple\u001b[39m(result)\n",
            "\u001b[36mFile \u001b[39m\u001b[32md:\\TimePassProjects\\col\\finl1\\.venv\\Lib\\site-packages\\numpy\\lib\\_stride_tricks_impl.py:419\u001b[39m, in \u001b[36m_broadcast_shape\u001b[39m\u001b[34m(*args)\u001b[39m\n\u001b[32m    414\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"Returns the shape of the arrays that would result from broadcasting the\u001b[39;00m\n\u001b[32m    415\u001b[39m \u001b[33;03msupplied arrays against each other.\u001b[39;00m\n\u001b[32m    416\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m    417\u001b[39m \u001b[38;5;66;03m# use the old-iterator because np.nditer does not handle size 0 arrays\u001b[39;00m\n\u001b[32m    418\u001b[39m \u001b[38;5;66;03m# consistently\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m419\u001b[39m b = \u001b[43mnp\u001b[49m\u001b[43m.\u001b[49m\u001b[43mbroadcast\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m[\u001b[49m\u001b[43m:\u001b[49m\u001b[32;43m32\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m    420\u001b[39m \u001b[38;5;66;03m# unfortunately, it cannot handle 32 or more arguments directly\u001b[39;00m\n\u001b[32m    421\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m pos \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mrange\u001b[39m(\u001b[32m32\u001b[39m, \u001b[38;5;28mlen\u001b[39m(args), \u001b[32m31\u001b[39m):\n\u001b[32m    422\u001b[39m     \u001b[38;5;66;03m# ironically, np.broadcast does not properly handle np.broadcast\u001b[39;00m\n\u001b[32m    423\u001b[39m     \u001b[38;5;66;03m# objects (it treats them as scalars)\u001b[39;00m\n\u001b[32m    424\u001b[39m     \u001b[38;5;66;03m# use broadcasting to avoid allocating the full array\u001b[39;00m\n",
            "\u001b[31mValueError\u001b[39m: shape mismatch: objects cannot be broadcast to a single shape.  Mismatch is between arg 0 with shape (9,) and arg 1 with shape (11,)."
          ]
        },
        {
          "data": {
            "image/png": "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2LBhliFDhtPW0c8YP1FHk8hFZaaUNGvWzIYOHeo2sMrx1Ctq7ty5webnmzdvjvPHoIBVx44drWzZsnbdddfZzJkz7a233rKHHnrIva6o35o1a+z222+3K664wtq0aeOysb766iuXEQUAAAAAAPBvlOTiPbJly+YyhrznmphNva4/+eQTF29QrGHx4sUuKaZRo0YunqGg1TXXXGOfffbZv5bvxcTE2Lhx4+zOO++0TJkyWYkSJeyDDz4468a59dZb3eRxX3/9dXDZxIkTrX79+gkGkTZt2mTffPONC2IpVjJr1qwE31ffG/qz66EWSlEblJJOnTrZ77//7jKfvv32W6tatWqcFDJF/zwDBgywjRs32l9//WV//PGH+6UqsOXJmDGjzZs3z3bv3m0nTpyw3377zV5//fXTZvgDAAAAAAC4UArwqIRu/fr1boI1VW7dfPPNLltp1apVdtNNN9ltt93mkm3+Tb9+/axp06YuwUbff++997p4x9naIWm9N998M7hMsZMHH3wwwfW1nkr7FGC77777XNZUJAh7+R4AIAWkVJ8I/O/rgQXM0sVYONKwAQAAgMTy7LPPWr169YLPc+bMaRUqVAg+79+/v7333nsu80nJOGfywAMPWIsWLdzXzz33nI0cOdKWLVvmglr/RgGomjVr2ksvvWQrVqxwE7cpgyp+Pyn14VbA6uWXX3bPmzdv7iaNU/ZUkSJF4qx72WWXxXleuHBhW7t2rSUVglIAECax3T5OtPc6deJY8OvSveZaqnSn14dfiN8S521ShOSxPe9JlPchyAgAAGCu33UoZUopIPTxxx+7NkT//POPq/I6W6ZU+fLlg19nzpzZ9XRS9dfZKACmcr93333XFi5caPfff3+Cfa3mz5/vWiEpC0ty587tgmnjx493gbNQan2k0kRP2rRpk3RTE5QCAAAAAAA4TwoghXrqqadcAEh9szXZmtoLNWnSxLUW+jdp4wV+1GdK2U3nQtlSo0aNsnXr1rnsqoSoVE/lgPo8Hr2/ygVVOhjaM0qZU9mzZze/EJQCAAAAAAC4SGo6rlI8NS33MqfU5zop3XPPPS4YpqypMmXKnPb6vn377P3337dp06a5CeM8mi2wRo0a9umnn561TDApEZQCAAAAAAC4SCql06x2am6ubKdevXqdc8bThcqRI4crFTxTmd3kyZMtV65crpG6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qG31+++/u2yYTz75xGVb6HiqCygtGzdunOuNgJS7L6v5spexob5S2q91blVmsi6svcCUjts0P/ePWhXoumfKlCkuY1wBKWVMiSaY0PZQ82xEJu1b2q/Ur61SpUouU/ypp56y5557zl3D6qEMcrYhdCxeu3aty1LVgK7uczRbo87TCoK88sorbpAA4eNdU+m+5sorr3TXudoutWrVctdRV199tduGCiiWLl3aJVko2EjSxP9Ho3MkK7lz53Yz7KkESDew2tGVXaMTtrJtlHWxfPlyl0mlA7VGDnQzTD1+ZFGTRqW16sa3Xbt2weVqnlumTBl3w9OzZ0+X4qqR4O+++871ykDyCjpq2yoApSwonXybNm3qLqI0EqyGj/pXJ2pt36+++oomkD4JzU7zJoS49tpr3TZS0EEjeh06dHDraMReF8B16tTx6+MhwvblWbNmuUBlnz594pxLte8qaKks1yeffNINAunYTalR0jhTI+NixYq5Y2m9evVcEMqjDAoFO9SrEZFBg2saXNWjQIEC7ia1UaNG7lpW2araVjome9tbrSoUdNQNLo2sU46E+m0qW1XXU5MnT3blerr3UY9O7+9K90HZs2cP0yeGhB6fNcGPkifUM1fnTc08rQCz9mdRhqSy0NOmTcsvz6NG50BysnPnzsCgQYMCV199daBnz55u2RtvvBFo3bp1ICYmJpA3b97A3r17g+ufOnXK/Xvy5MmwfWbE9dZbbwVq1Kjhvv7jjz8Cb7/9duDmm28OXHrppW7byp9//hnYsWNHYM+ePfz6kpkffvghcPnllwfGjx/vnh8+fNjtm/369Quus3379sDMmTMDzzzzTODHH38M46dNOUKPgXPnzg2kT58+sHbtWvd8/fr1geLFiwfKlCkTWLFiReD48eOBzZs3Bxo2bBioWrVq4O+//w7jJ0e4fP/994FChQq5c2z8fdzzn//8xx3Pr7nmmsCqVavC8Cmjn3cd8+mnnwY6d+4caNu2bXAb/Pe//3X7bYMGDQKjRo0KfPjhh4GOHTsGsmXLFli9enWYPzk8a9asCVSoUME9dJ2aKlWqQPny5QPPP//8GY/XvXr1Clx22WWBn376iV9kChF6ntY+vmHDhsDRo0eD+3/27NkD119/fWDTpk3BeyJdP1933XWBf/75J2yfOyX74osv3H58++23B5566qnApEmTgq+F3o96Dhw4EKhXr16gT58+Pn/SyEZQChFPF1Uff/xxYMmSJYH9+/e7Zbt27QoMHjw4cMUVVwQDU/L+++8Hb3BDD+zeBR3CJ3QbLFiwwAUpFJDQifS2224LPPzww4GBAwe65StXrmRTJQPePhb/QujLL78MXHvttcFghwJUDz30UPD1jRs3EiT2WejxcMqUKYHu3bu7fa1o0aLBQIL+1bbSjVLBggUD1apVC1SpUiVw4sQJ9zoXvCnPZ599FihVqpS7KVJgcuzYsYHatWsH8uXL547bHp2TDx48GNbPGu10HZQxY0Z3A6p9NHPmzIGpU6e615YtWxZo3ry5CyAq6KEbHgWrEBl0HlQwoUuXLu4add++fYGFCxe6fSlXrlyBbt26xVlfA3Xt27cP5MmTh+uhFEp/EwpeFi5c2AWdf/nlF7f8vffeC+TMmdMNAug4UL16dTdIz3k6PHROzJ8/f+Cmm24K3HLLLe7aSQN+d911lxvcE29QT/9qQFbH8EqVKjHYFw9BKUQ0jc4WKVLEXWiVKFHCRZW9EQNFnxWYKl26dOCxxx6L830EoSKHty2OHTsW5+Z42LBh7uJZo+zKzNB6euhE+80334T1M+PcaQRX2W1bt24NLps+fXqgcuXKLttNF1Qa1fe2u25ytb/qJhb+0yieton2P+172gcLFCjg9kHR6KtufkeOHBmYN29eMBBFplTK9N133wXKli3rLrD1t6KRYP3dfPTRRy7TI3REGEl3/lTATwNwr7/+evC1xx9/PJAuXbrA5MmT3XPdlOqYq8eRI0fYHBFC1z533313nIGZ0PNn06ZN3U3tO++8ExzU0TlTy9etWxeGT4xwDxwpI0r3PZ988klg1qxZLgsyd+7cgeXLlweD0BMmTAj07t07MG3aNM7TYaJtc8kll7h996+//nLLfvvtN5c1pUEDnS89Oi536tQpUL9+fRdIJIh4OoJSiFhjxowJpE2b1pV66YZXKesKQHmRZy9tVYGpK6+8MvDkk0+G9fPizBfUOrHed999gTp16gSeeOIJl8Yu8UfWlb1RrFgxV7aH5EElesq46du3rxsBkkOHDgVKlizplnfo0OG0oEjdunXdSDH83Q91g6Mgv4JOHmWgeqN73n4ZHxlSKevvZPfu3YEtW7a40mova0PHbx2fvZtkXYDXrFnTlYEicSmor2sbjzKHlWVz1VVXudK8UF5gStvIu8lBZNF1jgK6Xjm7gg/eIJyoPEvH3zZt2rjnusbdtm2bK/FByqPAs7JvXnzxxeAyXS81atTIZdV5A0jxcZ72j/Zd7adNmjQJ9OjRI7jMo6qel156yR2b+/fv75Zpn1Y1iJ4z2JcwglKISOPGjXMBqdmzZweX/frrr66U5JVXXnFRaJ3IRRkXQ4YMcWnOGt1HZFFJpVJZFVS8//773YiPRhaUuu5RRoZ6gmkkiNK9yBc/E3H06NFum6r/hU68omCyAlPa5rq41oWU0tHV50R9apC0GjduHOjatWucZdoG2hcXL14cZ/nnn3/u9r3Y2Nhgnxp68KXMfVqlISqp1ii9SsCefvrpBNfVCL1KPX///fcwfNropH1OZV46RqqfW+jye+65xwX5vd5eocdgDcjptXfffTcsnxv/ToNsuj5Vv6/4287LQFUWnM6XXosKpAw61irjyaOBgHLlyrn92Tv2en8veu3OO+90pdMaTEJ4afBVWecJHZO9/V69v2688cbgstDrKoKIpyMohYjc0VWqpxuk0BR0NdxVirOa7up1Ba28EQONKuommJ08sigYoYPys88+G1ymm5h27dq5C2/1vFA5pkaGmjVrFqd5LiJP/EBF6HMFixWY0sW1Lp7U3FzbVZk5GuVXlqNG+mmEnPR0HBw6dKg7Rg4YMCC4XDc8ughWLzdtn9D1deGk3kHq0/fzzz/78CkRaZT1pJ5FGuHVsVgjuro50oQEoYMMKkOi103i8wIUXmBCwXsvyK/XWrRoEciRI4drqhufRusV0ELkXPt416/anrp5VTnemWjQzuvDiJRD/R291hYe7ce631HQ32uL4AU8VAKma2plNyP8mVLaRvH7wYVeG7/88suu/5cy3RjoOzuCUohIuiDWSVwHZpUJKEVSfS3UIFLP9bpK9lQG5NXxeghMRQ6VgWhGPS9t3TuYq2+NSvlU8uVdtIXeJCNy6YKpVatWgfnz55/WSFeBKdXR6wbJK/3R/qmbXX2f/h7gD93Evvbaay4wpZTx0JsfNUVVNqpXCq39T5lVGrFVr4PnnnsuTnkJop/Omwo2ebMBKQNZ2VLqHxVq4sSJgUceeYReN4lM+6P2P+8GVcdKBQRVNumVReumRtdCuslJKDCFyKAMCQWYXn311WAJns6N2p4K+Ip3g+oFIpUprhkT9ZzjbvRT5UfodtYAgHc97PUa09+QZsT1Snm99TVwT4AjPL799ltX2i66Z9FEBWpYHjo7Zui1k7ap7lNxbghKIWLoZBzaTFe9KxTQyJAhgwtIhZYJ6ICsmyg1X0XkCT3ZakRHvRJ0Ig2lmZu0DZF8aORXJbS6uFaKuaaq1jbUjazXB0wNH7NmzRro168fpT1hEBqUX7p0aeDRRx9120uZU5577703ULFixcAdd9zhAlAKRNWoUcO9posslQoh5Rynvf5uKq1WKa6CIDr3KqPVW0d9jjRrqniTjSDxtoP2Pw206fjpBabUTF7XP2p67WVMeYEpZY1r0ghEJjU41vbUgJz2FwV5vRJMZa+qGbLs2bPHlcIqm5im5imDgvq6dgrNGldv3PjnaQU6vMoQb2KY0GtrAlP+Uga5JvBRxqMXmFIbktSpUwceeOABN9geSj3+dE7VNRjOTSoDIsDcuXOte/fu1qRJE/vjjz8ULLXSpUvbZ599ZkWLFrVs2bK5h+eff/6xP//804oVKxbWz43/0TaTU6dOuYenVq1a9u2339rUqVPt6NGjweVZs2a1AgUK2MmTJ4Pfi8iWLl0669mzp5UqVcp9PW3aNLdfzpw506666ir3OHbsmFWrVs1ef/11GzNmjO3cuTPcHztFSZ06tfu3W7du1rZtW9u/f78VL17cunTpYv369XOvvfXWW/bggw9a+vTp7cMPP7TY2FibP3++ey179uzuuPp/g1Zh/VmQtGJiYuzdd9+1du3a2S+//OK2+/Lly+26666zhg0buv1X6xw4cMDmzZtnq1evdsfrjBkzsmkSifYx/Y4XLlzornUGDhxos2fPdufKW265xX09fvx469u3r23fvt1SpUpl06dPtwoVKrjt9tdff7EtIoj2D3n//fetYsWKNnjwYLe9cubM6bZtx44drXfv3la9enUrUqSINW7c2CZNmmQLFixw17yIfoMGDbJMmTK5/XfVqlXuGNC1a1cbOXKkO0+/8MILbr0SJUq4c3WePHncOVz3PDpWeHQsgH90frz33ntt165d1qNHD9u8ebPVrl3bXnzxRZsyZYp16NDB3nzzTdu9e7ctWrTI7rzzTtu2bZsNGzbMfT/XU+fgHINXQJLRSFLRokXdFOVKYfd4ZXkaPdLMJGq6qnpqufnmm12mhpdZRbpzeHm/f5VpKQtD2RaaZc8b+dPIkEYNmzdv7razRn7Vf2jt2rVh/uS4kEycOXPmuGyoxx57LLj8gw8+cCPASmW+5ppr3Kifsi00Egx/KcMiS5Ysga+//trtm8p80WQQqVKlchlsobzjrI6lKrvU7D7eJBKI7uO1ykLU601lRt6ob5o0adyx2iu/1br6u1CPR3qNJY3QmZiUWawsRk3z7vUk0nlVo/E6b4aW8nn9ZhBZQmeIVpa/9rE333wzODuiSoAGDRrkGlkrM47JAlIO755FpV/KgFIpvSb38Y7JmqxJ1046X3vU+kD7Pq1Jwsf73Ws7aWZE9fVSGwuvtHLWrFmuv5QmktH202ybOpZ7+zzb7twQlEJYTZ061TVW1Yk5dKfVRbCCVd5FmYIbSne96aabXH2uZilhZ48saoCr6U9VqqeAlG5iVJLgTT+vXgpeb7Bbb731tH5ESB43sV4ZprarAlMKNIZS4Fg3S5rOOLTOHklDfYBCp48XzQajoH1osF7TkqvBuS6Y1Hwzfn8L7ZsaHGD2y5RBM56q7FZlB6FTz6tMT8FLTT+umZ685tr8Xfh306pemgkFplTKp+3hlUojMqjXqWatlNAWFDoHKshfq1YtN4GErmm9fY2B1JTLu3fRtZR6RiUUmFIQOrSUz0NwI3wB5tD+xbq+yps3b6Bly5bBUj4NGGigXedWXVPF7xuHsyMohbDRjasa+YWOCHi9hnTjpIbJkyZNCvavUGBKF8fFihULHtTZ2cNPJ1L1JNG2VF28RzfK2paa7euXX36Jc3Mcf7YRRC7vQmn27NkuIKzZYnSjpBOuMqbUC0PZcR72Sf+oz4SCB97x0KMm9JkyZQosX748zvKvvvrKXezq+Bo6+YAoqyp+TwREr+eff979HSgL2buo9vb1RYsWuSxIBSqVWUfmXNLwft8KMu3duzeYieZlTGm0PTQwpWxUBTm8bClExjbUPqJ9SdvKo0woXa8uXrzYPdc5UoOpamavayCkLPH7P3n7vv4WNBgUPzClgSP9Tel6C+GhARpVeXj9vDzqKaWglGYV18D7/fffH9i8eXOC70Hfr/NDUApho0yZAgUKBL755pvgsrfeesultCq1VRfFurFS2rM3M5vSnENT3REZdNGsko8xY8a4595Nsg7mKuHq2bNnmD8hLoZuhjRKrwCyRoA8uoDyAlNKZYZ/4o+0KzvKCyop4K9yZ90IrV69OriOggua5UlZjRw/odkZdeOjwQTv4tn7u+Ji2p/9V/titWrVXAZxqVKlXAl0/FI+3Rx510DMUht5lB3cvXt3l2Go5vOaaU8zJCpjIpTOkcqu0HUumVIpR+ixdMaMGS6YoQlGPv3002BgKqGMKVWQcJ4OHwWZdX5U5Yd33NXEPjpWe9np2tcVmNK+TRnuxSMohbD55JNPXOleaImPUps1YhhamqIa3fizkpDCGj46gWpUIDSVVdutTJkygaeeeip4EvYCUzpY33333WH7vDg/ofuWLo60PyrbLTQLLvRCS/9qX9bJW7N1ITz7pDIodFHr9Zl5++233XZTKZDKpDVir/Jnlc56F71c8KYM3vbev39/sPTLW6aMDt1M6+I6oe9B0tFxU8F+/e7XrFnjtoWOo14wQ/unZnErXLhwYObMmWyXCKLsQvXu86gUSz2itP2Ujaq+UQmdU9u3b09vthSqS5curu+QZr1V2Zf+VpQ5Jwp6qMRT/ThD/3aE87S/dO/iXd+qV5S2U7du3dx20+B7/OCTejIqC7J///4+f9LoQ1AKvgo9OevAq51dJXrxL4K9g7DKUG688cbg9LkIrx9++CFQs2ZNN6KrIJQ30iMa/dPNjTI2QumiulOnTmH4tDhfyoTybn48upFVyey7776b4Pd4pQj6W6DMxx+ff/65KxlRTyAdI0WBYu2TmrLYK+/RBZVSy3WTpGa7ml7aCxYTdEh55bfKyFFvRk1EoQtoryxMo/Y6dnsNz+HPdlEQ38si1o2OjrMKWoh3U6T9VeUioSXwCP910FVXXeWC/sqMCj1XDhw40O1L8YOIBBZSNp2LVTWwdOlS91wDRfHL6BXY1ARAymZGeOg6VpMxqTeUdwzWta+2lQYQQnsrhu7T2t9Jlrh4BKXgm9A+QtqxtQMr8pwnTx53kpfQnVq9pJS+rkas3ECFn8qAdMJUjbVGeOvUqeOCU9620Q2OLrB18NY6yqxRMEqzgDHLXuTTdlQPGS+w5G1XjQhny5YtGGzUidh7TRmMkydPDt7cIulp5hcdM9XfS6OuhQoVCjbZ1bbSPqkZEEP7zqisj8abKY93PlXmjTKOVTai8hFNRlGlShU3Wu/1bHzhhRfcsVt/X/Dneqh8+fKuPYEyjXXDqiCVd2xVgFCzISKy6FpVvaKUFZXQRB4q5VNGjAJTXo8prl+hQIfXe1MBDF0Xv/766+659n9lSoqOxwQ3wkMBQl1PKSjo9eP09l1N7KPzo6pBdu/eHfye+MFmtt3FISgFX2g0X2Ukol5RGs3XyVsXXRrd102WRv+9mb1WrVoVqF+/vkuV9HZ6TuzhoxOm+nspMyN0mlpNi7ps2TL3ukYJvREgpSBrVL5BgwbMspcMxN+31BBbF9TeDevjjz/uGiLHv0nScmXCefstkpYCBprhUr0mRMdMBQwVuPdG9RSY8soAvAbWoegVFL28bRtaAq/Zg5Qt9+ijjwaX6Zw6evRodx4OneFJsz7FL5VH4h5jVfbufd2rVy8XGNSx9eGHHw5uPx13tVzlfKGDAAgvXeMoO0o9ZuIL7fWl7aWbVwWCvfIspBwJnWN1bNVxWP3hFJBSPz+Plulvas+ePcFlBDf8pfsWTa6lbRE6G614x18N6Cgwpeve0MAUEg9BKfhygNaJWSOzKiHRKNPGjRuDO7uybhTc0M6uUX7NRKFZZzSNrldqwgE6fHSA1g2uRhBCaTRQ6axFihRxzTurV68e7JWgzBltWzJoIpu2kXfCDZ3BTYEmlZLoJKx9TwHIZs2aue380ksvuRtaZcMpIKIJC5D0FBDUMVJle6F0Q6tGm9pPvSCi+kqpQbIyqeLPHIPovhFSRqvOoaF9STQLaosWLeKsr/1eyzRwgKTlHWN1raObz++//z5Y8q5ySpXVegFkBaF69OgRiI2NpfdQhFGJpY6rGogLHcDRYJ2uj9TaYMSIES4LTg9liqvhObPtpcyAlP42/vjjD/e1+o/pmkp9dHUN5dGAngbsNViP8NA1koLN8fsq6nrqu+++c4Pu3sCrronTpEnjsqmUWIHElcqAJJYqVSpr1aqVxcbG2oYNG+yqq66y4sWLu9diYmLspptuso8++sjGjRtnjRs3tnvvvdcGDBhgCxYssLRp09o///xjqVOnZjuFUevWre3UqVP28MMPu+fDhg2z119/3d5880374osvrH///rZ9+3YbOXKkHT9+3NKnT++2bcaMGdluEUqDEtpG+/btc8+1r2mf++abb2zWrFlWtmxZtx/Onj3bSpUqZUOGDLH27du7bT969Gj76aef7Msvv7Ty5cuH+0dJES699FKrUaOGrVixwr777ju37K677rI9e/ZYtmzZrGHDhtakSRO3zQ4dOmRTp061+vXrW65cucL90ZHEdGzWefa///2vXXvttda8eXOrUqWK28dPnjxpRYsWtc2bN7tjtNYV7fu1a9e2bdu22YEDB9hGSUi/ax1TmzVrZhkyZLB06dK55brWefTRR23Xrl3uGun+++9364wZM8atX6xYMbZLBNH+tGnTJneOFF3vPPHEE+48qO2XI0cOGzt2rC1fvtxdAz333HO2fv16u+SSS8L90eHT34eOw9KzZ0/3N/H555+7Y/Att9zi7m/+/vtvdz+jvxGdx7Vsx44dNnTo0OB7wF+6x9y9e7e75vXoGNyyZUu75ppr7Pbbb3fXVvv377e7777bJkyY4K5/s2bNyqZKbIkc5AJOGx1UpoUizsquUDq6RvWVieGNHoVmaMRHhlTkpK2r3lpllspi07+LFi2Ks45GCTUij+RDo3jKqujdu3dgzpw5LhPngw8+iDMlufqehE5NrNIglQQxNbn/1MNEM+hpu+g4qtn2lKmm7aFSPh1jlbmobNTQyQU4jkb/yLxXYq2SsPiUOaeMjebNm8cp6VQPI2VKhc6kisSndgTKMg1taixeFoWa63bt2tXNjKmsGyaMiEy6VlXfTJX5FC9e3JVSP//888F+QDpH6tir3m1IuXQMzpcvnzsn79u3L85rKv0qV66cm3xELS7UG5KKkPBSlnn+/Pnd+VBZkOqtqm3Utm1btw3HjBnjroOV4Ra/NJN2CImLoBSSROhNkA64oT0R1NhT6erxe9EoyMGNbmTQjYtKC5555plg8FDbRttOQYx69eqd1sBeNzz/+c9/6IGRjOimSM02s2bN6koxvRn2vJtULzClQKTSlr3yMIQ3MKULWZVOqv9BQgFkHUsJRKUcmp1WAeUHH3wwznJNNtG3b9/g5CK5cuUKXHvttS6wqeO1Jq5QuR+S1ty5c115iK539NB5VPuw+mlqtr34PUwQuXbs2OFuVNUTyGtDITreasDmhhtuCLz99tth/YwI77WzZmb0Zl9UnygFLTXwt2DBArds586dgSVLljD5SATwgkoqr8yePbsrm1YASgMFXt8oXVOVLFnytNYJSHyU7yFReWnNXrndoEGD7NZbb3VlJDNmzAimrHfs2NGVnSglUunNDRo0sMGDB1umTJnYImH2ww8/WKNGjVxZnko9vNTzzJkzu+W9evWyNWvWWLt27dxypalr2fz58912TZMmjStXQORTucGVV17pyr1k3bp17l+VmBw7dsxtS5XvFS5c2J566imbN29emD8xSpQo4conVaalNPLFixfHSUNXKV+tWrXcMVhlA4h+uXPntrx587qSAh2bReUgffv2terVq7vnKptXeV+9evXcunp8++23VqFChTB/+ugUWoajkp2lS5e6axyV4Ko0r3Tp0nbffffZp59+GizHReRtO6/cVf/q+Jo/f3674YYbXCsDrw2F6Hircr6tW7cG9zmkPLpuUqm02iGodK9bt272wAMP2Pjx4+2RRx5xZfX58uVz5+8iRYq4cj/9belaC/7zyi1VXrlx40b7+OOPg+fJPHnyuNdOnDjhtlmhQoXYREktCQJdSKEmTZrkRmu90fvnnnvOlXk9+eSTgbvvvtu9NmTIkGD2lNZTxlSBAgXcKOK/lfHBH2vXrnWjBUpRD53BacqUKYEff/wxOGqgkV6lJ2tGJ43GK8tmxYoVbKZkwstc1LbcvHlz4Ouvv3Ypytr2yo7zeBlTGgW+7777Ar/88kvYPjMSLuXTY/Hixfx6UigvI04ZrWqkq7JOlYgoK8obmQ9dL7SsHonP+/3GnzFP10PKDu/cuXOw0bmuedQ4e968eWyKCKRynffeey/OtamyEpU1HEr7mRrY6/ypUk2kDGeaFVNZqDr+qqm5/i40wYFowqDQGawR+dtS90Eqq1apJefMpEdoFolGWTRPP/20y4TSqJGaeWpUoE6dOm6EqWbNmvb444+7USitpywpZVApOl2pUiUXsdZ6jBiEx59//ulG/9TIT43LPRrd7dGjh+XMmdNlZajp9Z133um2lzKjjh496po2Xn311WH65LiQBueaXGDy5Mn22GOPuZFdbde//vrLZVdo2z777LMuY0rN7PWa1kVkZUx5jXZ1XH3jjTdoOp8CeRlxymhduXKlG4F/6aWX7LXXXrMbb7wxznoJjRAj8Y+tanz9/vvvu+uZK664wmVIdO/e3TXKzZ49e3D9fv36uSzVMmXKsBkijLJXtB21DZX1okyKnTt3WsWKFV0zel0nycyZM9258eDBg/bVV1+5zGOknMkl5Mcff3TZNNq3lU2j+x5llSvTJvS6OEuWLO6ByBO/ukP3Q5q0QBUjmohiyZIlwXMtE28lIR8CX0hB1C/h6aefDqRKlco19vzss8/ivP7yyy+7Bn9exlQootDhpd4iZcuWdf0SPOoxpN41kydPdqO82qbr1q0L9iNS74Sff/45jJ8aF2LWrFmun4z6HCjjxqNt+uKLL7oeU8qM0qi+9lcvSw6RR/ujthMNN1PmyK56+oWO8uocfMUVV7gsZLI2wnNs1TlTx09lTFx55ZWBNm3aBF/XfqqG5w899FAgd+7crtcXIou3P6mnorZd06ZNAxMmTAhceumlrm9m6LWqeghp2niv/wyiX+i5VlUFynbU9dTNN9/sKgdC6Xi8fv1615tTzbO9CWMQ2b755hu333t9coVtl/QISuGixb8ZUhlB//79XbneqFGj3LLQi2Yt02sqCUP4aeYumTp1qjux/v7778HXvvrqq+DMMmrOqDRWpSSr2ee/pbwicinAVKhQIVeu59F21IxPKucTNbmvXLly4MYbb+TGNhkhMJUyeMddNWdt0aKFOy6rub2a6ooaZ2tCCu3DNDL3z/Lly12jXM2CKboZVQsDzdKmGZ0848aNC9x5552uXB6RKTTw1KxZM3fdo8CCN7ELx1qo8bX2b5Xfqum9V7YX2gJBbUo0uYSupZhlL/zO9Z5F62nmRMrd/UX+Ni420y6Ywvr222/b7t27XRnBo48+al27drX//Oc/Nn369DipkSr5evfdd61p06b89sNMpZMDBgxwXyut+PDhw65Jo0dNWcuVK+e+VqO/Fi1aWMmSJYMNlGlonvwcOXLENWtVWa3KSV5++WVX5lO7dm275557XLNzleCqCbKanKtcAckDJVkpg467KqVu3ry5m4BC+3Hjxo1dqa0aLWfNmtVWrVrlSsNUJq/JK5B4vObXuv7xvhZN2qKWBO3bt3fn0ZtvvtlN9PLqq6+6cum2bdu69dq0aeNKvijbi3wq49G+psk+VLazcOFC17hex9rQhuhIWTQ5gUp033nnHbfP//777/bBBx/Ydddd5+6F1P5AdFzu2bOnm9BAZaAqB6X8yz+hx+fzuWfRvq2WJdzj+IugFC5qZ/d2WNXaayYZBaP27t3rLoqfeeYZN2OXbnQVmBLvJH7XXXe53lE6QCN8dGH81ltvua91MlX9u7ahF5hSnXzogV29o4oWLepm+ELyEXrxrAsjzdDVu3dvF3BasGCBm61t+PDhrjeCZh4RXXR7My8CiCwKPqk3o/peqJeN+ha98sor7njuBaYUWNY+rMAVEreXjGY61LlSQT/NdCj333+/PfTQQ27QRoEp9dHUrFvqt6leM+r7pushYabhyKbAga6D1BNM23j16tVuxkrtb3PmzHHXrtywplyatVT7smY11Sx7GshTj0cdfy+77DIbMmSIOz7o70j9yLx+RPTMDU/fr4kTJ7rem+qbq6SIc0220LlVCCT6g6AULkjoTqubWzVHVrBixowZbvrTffv2ucybXr16WZcuXdzFmkZx45/EOUCHN0ihBtdqZn38+HHLkSOH207KdtNIrm5s1KzeGy1Uo1Yd2DUCRLPG5LWdveCiLqTViFUZUAos6gZKTZG1/yoLThlU+lsAEJn7sjKgPvzwQ9fUPHfu3HGaZuvcq6wcNdrVDbX28RUrVripx5F4NzkK3CuLWOfI9OnTu4lAnn/+ebfONddc45Zv2bLFHnzwQbdM31O1alWbNGmSDRw40C0joBHZFEB48cUXXVPzF154wW1nTR6ga1Zd53KeTDkWLVrkMsp1jNW1sDe4pyCHgv8a3FVQqmXLlm4QoGzZsm7AT5MAhWbqENjwl3ePqsEb3YuqEkTbS1U6GkiIn0UVOlmFjBkzxg3Y6pwLn/hcLogo88ILLwRy5MgR+PLLLwPLli1zTT7z5cvnpinXVJpeo7+HH37YTVWNyKI+QuqV8Omnn8apk7/ssstcs1ZNZ3vvvfcGbrvttkDBggVpypqMeLXw8+fPD7Rs2TLQqFGjQNeuXYMNWY8cORJn/R49erhGrps2bQrL5wXw72bOnBnIkCGDa2Suvoz16tVzU9SH0vE7c+bMbsIC9cWh71/i8HoI/fe//3XnTB0vveWdOnUKPP7444G//vor2H+xePHibrn69HXr1s1NB08z7OTF67EY2mNKzY63b98exk8FP40dOzaQP39+d/+iPn0lS5Z0fXM9Or5Wr1490KpVq2CPVjXInjhxYvDYyzHYX6H93nRvc/nll7vG5fLJJ5+4c+ebb7552veFbif1BdS9rSZ7gn9i9B+/AmBI3jQ9rjJrvOwm/elodECjtUpbDa21btiwoUtdV6RZ06JqxEAZOfQ8Ca/ffvvNpRrfcMMNljFjRlczrZHd/v372+233x5c75NPPnHZNBpl13rqOaQsquLFi4f18+PsQkd6tA213dTLRPvesmXL3AiweiFov9VIkTIY58+f76a+VVmC0tEBRJZt27a5Po3XX3+9m45e51aVitx0002ud6N63ngGDx7sepmUKFEirJ852ij7SSXuOn8qW8aj3l4qfT527JjFxsa69gTq3acMG2VHKFNV51SOrdFTDoTo9/rrr7seuNrX69WrF+y3qesnZUN5VD0wa9YsK126tG3fvt31+FMmq/b90OsxJK1hw4bZk08+GWdfVXaqtp96+s2cOdNlE2u9du3a2YEDB2zTpk0uqy1039a5VdlVKr3WeRQ+8jEAhmSsT58+bjQgNJKskaPrr7/ezSITukw01byi0ZpxxptGU9/LiEH4aARHszQp40mZUJqO+p577nHb6Y477nCzh/z6669xvsebLYTtFvkOHz4c57lG9EuXLh2cZU+zKhYoUMCN/pQtWzY4U5dm7dL05MqaAxB5VqxY4c6z9evXD2zdujVOprKmI1eWTuisqUgayiJVxtPtt98eWLx4sVs2aNCgQKZMmdyMw5pVr1SpUoEKFSq47PF169YF3n///cDmzZvZJEAyMmPGDHdtHJopo0zIEiVKBDp06OBm03vjjTcCf/zxR2DLli2BZ5991l1fP/DAA8yyFwaaZVbbSzNlhnrnnXfctlJmlGYXf+2114Kvvffee272WmW2el555ZVAzpw5yZAKE4JSOGdecEk3r16a+ttvv+1KfqZMmRJn3ddff92ls+bNmzfQrl07fssRwks7Xrlypdt2Q4YMCZQpU8YdzBWoUppynTp1Avfff3/g5ZdfDnz33XdufYJSke25555zZZY7duwILluwYEGgTZs27mvdsBYrVsw9nzt3rtvO1apVC56MFbAEEJlUSqBAsi6Wf/rppzivKTClQEnbtm0JfvhAv3+1J1BgSsF8XeNoSniPyil1PtU1EIDkea+jYIXK9ULLvDR4q/sdleVq/9d+PmDAgNNKxrz3gL9UqqfBdpVPepYvXx6oUqVKIH369G4AwXP06NHALbfc4q6JvfsbravS7OnTp7PpwoSgFM7KO9hqx1Vk2Rs9UFaURgBbt24duO6661wNtSgDQz2IRo4c6UYSlJnz448/8puOAAkFlxSYuu+++wKrVq1yB3X1ymjYsGHg2muvPe0GCJG5b3788cduv9QIXmhgav369e5fnaSVFedlKyrDUevrZpa+M0Dkmzx5cuDKK690N0PKag2lUXrt06Ejvkg6up5RPy/dwAwdOtQt03FVmcXKZCtfvrwbofeWA0helAGlwVnd2ygwpUxVZUCGVhMoK0pBEK9/rod93j+6d9H1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",
            "text/plain": [
              "<Figure size 1200x600 with 1 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "image/png": "iVBORw0KGgoAAAANSUhEUgAABKUAAAJOCAYAAABm7rQwAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAc8hJREFUeJzt3QeYVOX1OP6XIiAqYEcNir333mKXqFFTrLGiokaNBTWKBVBU7DX2Ggv2mthFTexg770X7KKigsL8n/P+/ne+u4gKcbkzu/v5PM+4O3dml+vcve2855y3TaVSqSQAAAAAKFHbMv8xAAAAAAiCUgAAAACUTlAKAAAAgNIJSgEAAABQOkEpAAAAAEonKAUAAABA6QSlAAAAACidoBQAAAAApWufWplx48al999/P00zzTSpTZs2tV4dAAAAgBalUqmkr776Ks0666ypbdufzodqdUGpCEj16NGj1qsBAAAA0KK988476Te/+c1Pvt7qglKRIVV8MF26dKn16gAAAAC0KF9++WVOCCpiMD+l1QWlipK9CEgJSgEAAABMHr/UNkmjcwAAAABKJygFAAAAQOkEpQAAAAAoXavrKTWxxo4dm77//vtarwYT0KFDh5+dUhIAAACof4JS46lUKmnEiBHpiy++qM0W4RdFQGrOOefMwSkAAACgeRKUGk8RkJpppplS586df7FTPOUaN25cev/999MHH3yQZp99dtsHAAAAmilBqfFK9oqA1PTTT1+7rcLPmnHGGXNg6ocffkhTTDGFTwsAAACaIY15Gih6SEWGFPWrKNuLICIAAADQPAlKTYCSvfpm+wAAAEDzV9Og1H//+9+04YYbpllnnTUHGm644YZf/Jl77703LbXUUqljx45pnnnmSRdddFEp6woAAABACwlKjRo1Ki2++OLp9NNPn6j3v/HGG2mDDTZIa6yxRnryySfT3nvvnXbaaad0++23T/Z1BQAAAKCFNDpfb7318mNinXXWWWnOOedMJ5xwQn6+4IILpvvvvz+ddNJJqVevXpNxTVPqeeDNqUxvHr1Bk5WzDRgwIA0cOPB/Wo/43ddff336wx/+MNHrMM0006T5558/HXLIIWnjjTeuLr/uuuvSmWeemQOKo0ePTgsvvHBer8m97QAAAID606x6Sj300ENp7bXXbrQsAhqx/KdE8OPLL79s9GhpPvjgg+rj5JNPTl26dGm0bL/99itlPS688ML87z366KNp5ZVXTptsskl65plnGpVrrrPOOumWW25Jjz32WM54i/LNJ554opT1AwAAAOpHswpKjRgxIs0888yNlsXzCDR9++23E/yZwYMHp65du1YfPXr0SC1N9+7dq4/4f4yspYbLrrjiipxV1qlTp7TAAgukM844o/qzY8aMSXvssUeaZZZZ8utzzDFH/sxCz54989c//vGP+XcWz39Kt27d8r8333zzpUGDBqUffvgh3XPPPdXXI2D297//PS277LJp3nnnTUcddVT++q9//WuyfTYAAABAfapp+V4Z+vXrl/r27Vt9HgGslhiY+imXXXZZ6t+/f/rHP/6RllxyyZyV1KdPnzTVVFOl7bbbLp166qnppptuSldddVWaffbZ0zvvvJMfYfjw4WmmmWbKGVC/+93vUrt27Sbq34xg1Pnnn5+/79Chw0++b9y4cemrr75K0003XRP93wIAAADNRbMKSkUWzocffthoWTyPcrUpp5xygj8Ts/TFo7WKflLRg+tPf/pTfh49uZ5//vl09tln56DU22+/nbOVVllllZwNFZlShRlnnLFRBtQv2XLLLXPgKrLWIuAUmVWbbbbZT77/+OOPT19//fXPvgcAAABomZpV+d6KK66Yhg4d2mjZnXfemZcz4dkNX3vttbTjjjumqaeeuvo44ogj8vKw/fbb58bj0Zh8zz33THfcccf//FFGw/n4XbfeemtaaKGF0nnnnfeTWVBDhgxJhx12WM7QimwsAAAAoHWpaaZUZMm8+uqr1edvvPFGDmpEICNKyaL07r333ksXX3xxfn3XXXfNZWjRl2iHHXZId999dw5q3HxzuTPjNRfx+YZzzz03Lb/88o1eK0rxllpqqfy5RyDprrvuyllL0Uz+mmuumeR/L7Kp5plnnvyIkr/1118/Z2WNH3SKHlc77bRTuvrqq3/UuB4AAABoHWqaKRWztEWfo3iE6P0U30cPpBAzuUV5WSFKzyIAFdlRiy++eC5Li2ycmIGPH4sm8LPOOmt6/fXXq8Gi4hGfZSHKHzfffPMcvLryyivTtddemz777LP82hRTTJHGjh07yR/vcsstl5Zeeul05JFHNlp++eWXp969e+evG2ywgc0GAAAArVRNM6VWX331VKlUfvL1iy66aII/E826mThRIhdleTErXzQrHz16dA4Gfv755zkIeOKJJ+aZ9yIY2LZt25y9FBlP0UcqRF+oKJlceeWVc2+uaaeddqI/+r333jvP3BeZbbPNNlsu2Ys+VqecckrO3IrZFEP0A4v1AwAAgObSLifa4xRVSjGZGC280TmTLsrkOnfunI477ri0//775x1l0UUXzQGjMM0006Rjjz02vfLKK7mkb9lll0233HJLDlCFyEaL4FVkUUVg6c0335zofzuCYJGRFdlSZ5xxRjrnnHPyzHy77757fhQiUDWhACQAAAA0mYFNmAwxpkGCzZGzpNShTdP83oEjU2vSpvJzqUot0JdffpmzckaOHJnL1hr67rvvcn+lCKR06tSpZuvIz7OdAAAAqGlQanIZOLLFx16a7ex7QMtLeW3Tpk1+xPcAAAC0HoJSAAAAAJROTymgdmmvk6sOuwWlvQIAALRUglJAzUzVoU2qDPjp+mIAAABaLuV7AAAAAJROUAoAAACA0glKAQAAAFA6QSkAAAAASicoxU/q2bNnOvnkk31CAAAAQJMTlGoB2rRp87OPgQMH/k+/d/jw4WnnnXf+Veu2+uqrV9ejU6dOab755kuDBw9OlUql+p6nnnoqbbnllqlHjx5pyimnTAsuuGA65ZRTftW/CwAAANS39rVegWZjYNeS/72RE/3WDz74oPr9lVdemfr3759eeuml6rKpp566+n0Eg8aOHZvat//lTT/jjDOmptCnT590+OGHp9GjR6e77747B7q6deuW/vrXv+bXH3vssTTTTDOlSy+9NAemHnzwwfyedu3apT322KNJ1gEAAJqLUaNGVa/hv/766zTVVFPVepUAJguZUi1A9+7dq4+uXbvmrKTi+YsvvpimmWaadOutt6all146dezYMd1///3ptddeSxtvvHGaeeaZ8wlv2WWXTXfdddfPlu/F7z3vvPPSH//4x9S5c+c077zzpptuuukX1y/eG+syxxxzpN69e6fFFlss3XnnndXXd9hhh5wZtdpqq6W55porbb311vl91113XRN/UgAAAEC9EJRqJQ488MB09NFHpxdeeCEHhWLEZf31109Dhw5NTzzxRPrd736XNtxww/T222//7O857LDD0mabbZaefvrp/PNbbbVV+uyzzyZqHSJL67777suBsg4dOvzse0eOHJmmm266Sfp/BAAAAJoP5XutRJTPrbPOOtXnEfBZ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+KyzzpreeuutvOzkk09ON954Y1OvHwAAAAAt0CQHpc4888zcoHz99ddPX3zxRRo7dmxe3q1btxyYAgAAAIAmD0qddtppeca8gw8+OLVr1666fJlllknPPPPMpP46AAAAAFqhSQ5KvfHGG2nJJZf80fKOHTumUaNGNdV6AQAAANCCTXJQas4550xPPvnkj5bfdtttacEFF2yq9QIAAACgBWs/qT8Q/aR233339N1336VKpZKGDRuWLr/88jR48OB03nnnTZ61BAAAAKB1B6V22mmnNOWUU6ZDDjkkffPNN+kvf/lLnoXvlFNOSVtsscXkWUsAAAAAWm9Q6ocffkhDhgxJvXr1SltttVUOSn399ddppplmmnxrCAAAAEDr7inVvn37tOuuu+bSvdC5c2cBKQAAAAAmf6Pz5ZZbLj3xxBOT/i8BAAAAwP/aU2q33XZL++67b3r33XfT0ksvnaaaaqpGry+22GKT+isBAAAAaGUmOShVNDPfc889q8vatGmTZ+KLr2PHjm3aNQQAAACgxZnkoNQbb7wxedYEAAAAgFZjkoNSc8wxx+RZEwAAAABajUkOSoXXXnstnXzyyemFF17IzxdaaKG01157pbnnnrup1w8AAACAFmiSZ9+7/fbbcxBq2LBhual5PB555JG08MILpzvvvHPyrCUAAAAArTtT6sADD0z77LNPOvroo3+0/IADDkjrrLNOU64fAAAAAC3QJGdKRcnejjvu+KPlO+ywQ3r++eebar0AAAAAaMEmOSg144wzpieffPJHy2PZTDPN1FTrBQAAAEALNsnle3369Ek777xzev3119NKK62Ulz3wwAPpmGOOSX379p0c6wgAAABAaw9KHXrooWmaaaZJJ5xwQurXr19eNuuss6aBAwemPffcc3KsIwAAAACtPSjVpk2b3Og8Hl999VVeFkEqAAAAAJhsQak33ngj/fDDD2neeedtFIx65ZVX0hRTTJF69uw5qb8SAAAAgFZmkhudb7/99unBBx/80fJHHnkkvwYAAAAATR6UeuKJJ9LKK6/8o+UrrLDCBGflAwAAAIBfHZSKnlJFL6mGRo4cmcaOHTupvw4AAACAVmiSg1K//e1v0+DBgxsFoOL7WLbKKqs09foBAAAA0AJNcqPzY445Jgem5p9//rTqqqvmZffdd1/68ssv09133z051hEAAACA1p4ptdBCC6Wnn346bbbZZumjjz7KpXzbbrttevHFF9MiiywyedYSAAAAgNadKRVmnXXWdNRRRzX92gAAAADQKkx0ptQnn3yS3nrrrUbLnnvuudS7d++cNTVkyJDJsX4AAAAAtOag1N/+9rd06qmnVp9H6V70lBo+fHgaPXp02n777dMll1wyudYTAAAAgNYYlHr44YfTRhttVH1+8cUXp+mmmy49+eST6cYbb8zlfKeffvrkWk8AAAAAWmNQasSIEalnz57V5zHT3p/+9KfUvv3/a0sVAatXXnll8qwlAAAAAK0zKNWlS5f0xRdfVJ8PGzYsLb/88tXnbdq0yWV8AAAAANBkQakVVlgh95QaN25cuuaaa9JXX32V1lxzzerrL7/8curRo8fE/joAAAAAWrH/V3s3EQYNGpTWWmutdOmll6YffvghHXTQQWnaaaetvn7FFVek1VZbbXKtJwAAAACtMSi12GKLpRdeeCE98MADqXv37o1K98IWW2yRFlpoocmxjgAAAAC01qBUmGGGGdLGG288wdc22GCDplonAAAAAFq4ie4pBQAAAABNRVAKAAAAgNIJSgEAAABQOkEpAAAAAEonKAUAAABA/Qalvv/++/T3v/89zTPPPGm55ZZLF1xwQaPXP/zww9SuXbvJsY4AAAAAtNag1JFHHpkuvvjitOuuu6Z111039e3bN+2yyy6N3lOpVCbHOgIAAADQwrSf2Ddedtll6bzzzku///3v8/Ptt98+rbfeeql3797VrKk2bdpMvjUFAAAAoPVlSr333ntpkUUWqT6PMr577703Pfjgg2mbbbZJY8eOnVzrCAAAAEBrDUp17949vfbaa42WzTbbbOmee+5Jw4cPz5lTAAAAANCkQak111wzDRky5EfLZ5111nT33XenN954Y2J/FQAAAACt3ET3lDr00EPTiy++OMHXImPqP//5T7rzzjubct0AAAAAaO1BqTnmmCM/fkpkTG222WZNtV4AAAAAtGATXb73c0aPHp1OOOGENOecczbFrwMAAACghWs7KYGnfv36pWWWWSattNJK6YYbbsjLL7zwwhyMOvnkk9M+++wzOdcVAAAAgNZWvte/f/909tlnp7XXXjs9+OCDadNNN029e/dODz/8cDrxxBPz83bt2k3etQUAAACgdQWlrr766nTxxRenjTbaKD377LNpscUWSz/88EN66qmnUps2bSbvWgIAAADQOsv33n333bT00kvn7xdZZJHUsWPHXK4nIAUAAADAZAtKjR07NnXo0KH6vH379mnqqaee5H8QAAAAACa6fK9SqaTtt98+Z0iF7777Lu26665pqqmmavS+6667zqcKAAAAQNMEpbbbbrtGz7feeuuJ/VEAAAAA+N+CUhdeeOHEvhUAAAAAmqanFAAAAAA0FUEpAAAAAEonKAUAAABA6QSlAAAAACidoBQAAAAApROUAgAAAKB0glIAAAAAlE5QCgAAAIDSCUoBAAAAUDpBKQAAAABKJygFAAAAQOkEpQAAAAAonaAUAAAAAKUTlAIAAACgdIJSAAAAAJROUAoAAACA0glKAQAAAFA6QSkAAAAAWmdQ6vTTT089e/ZMnTp1Sssvv3waNmzYRP3cFVdckdq0aZP+8Ic/TPZ1BAAAAKAFBaWuvPLK1Ldv3zRgwID0+OOPp8UXXzz16tUrffTRRz/7c2+++Wbab7/90qqrrlraugIAAADQQoJSJ554YurTp0/q3bt3WmihhdJZZ52VOnfunC644IKf/JmxY8emrbbaKh122GFprrnmKnV9AQAAAGjmQakxY8akxx57LK299tr/t0Jt2+bnDz300E/+3OGHH55mmmmmtOOOO/7ivzF69Oj05ZdfNnoAAAAA0IqDUp988knOepp55pkbLY/nI0aMmODP3H///en8889P55577kT9G4MHD05du3atPnr06NEk6w4AAABAMy7fmxRfffVV2mabbXJAaoYZZpion+nXr18aOXJk9fHOO+9M9vUEAAAA4Oe1TzUUgaV27dqlDz/8sNHyeN69e/cfvf+1117LDc433HDD6rJx48blr+3bt08vvfRSmnvuuRv9TMeOHfMDAAAAgPpR00ypDh06pKWXXjoNHTq0UZApnq+44oo/ev8CCyyQnnnmmfTkk09WHxtttFFaY4018vdK8wAAAACah5pmSoW+ffum7bbbLi2zzDJpueWWSyeffHIaNWpUno0vbLvttmm22WbLvaE6deqUFllkkUY/361bt/x1/OUAAAAA1K+aB6U233zz9PHHH6f+/fvn5uZLLLFEuu2226rNz99+++08Ix8AAAAALUfNg1Jhjz32yI8Juffee3/2Zy+66KLJtFYAAAAATC5SkAAAAAAonaAUAAAAAKUTlAIAAACgdIJSAAAAAJROUAoAAACA0glKAQAAAFA6QSkAAAAASicoBQAAAEDpBKUAAAAAKJ2gFAAAAAClE5QCAAAAoHSCUgAAAACUTlAKAAAAgNIJSgEAAABQOkEpAAAAAEonKAUAAABA6QSlAAAAACidoBQAAAAApROUAgAAAKB0glIAAAAAlE5QCgAAAIDSCUoBAAAAUDpBKQAAAABKJygFAAAAQOkEpQAAAAAonaAUAAAAAKUTlAIAAACgdIJSAAAAAJROUAoAAACA0glKAQAAAFA6QSkAAAAASicoBQAAAEDpBKUAAAAAKJ2gFAAAAAClE5QCAAAAoHSCUgAAAACUTlAKAAAAgNIJSgEAAABQOkEpAAAAAEonKAUAAABA6QSlAAAAACidoBQAAAAApROUAgAAAKB0glIAAAAAlE5QCgAAAIDSCUoBAAAAUDpBKQAAAABKJygFAAAAQOkEpQAAAAAonaAUAAAAAKUTlAIAAACgdIJSAAAAAJROUAoAAACA0glKAQAAAFA6QSkAAAAASicoBQAAAEDpBKUAAAAAKJ2gFAAAAAClE5QCAAAAoHSCUgAAAACUTlAKAAAAgNIJSgEAAABQOkEpAAAAAEonKAUAAABA6QSlAAAAACidoBQAAAAApROUAgAAAKB0glIAAAAAlE5QCgAAAIDSCUoBAAAAUDpBKQAAAABKJygFAAAAQOkEpQAAAAAonaAUAAAAAKUTlAIAAACgdIJSAAAAAJROUAoAAACA0glKAQAAAFA6QSkAAAAASicoBQAAAEDrDEqdfvrpqWfPnqlTp05p+eWXT8OGDfvJ95577rlp1VVXTdNOO21+rL322j/7fgAAAADqT82DUldeeWXq27dvGjBgQHr88cfT4osvnnr16pU++uijCb7/3nvvTVtuuWW655570kMPPZR69OiR1l133fTee++Vvu4AAAAANNOg1Iknnpj69OmTevfunRZaaKF01llnpc6dO6cLLrhggu+/7LLL0m677ZaWWGKJtMACC6TzzjsvjRs3Lg0dOrT0dQcAAACgGQalxowZkx577LFcglddobZt8/PIgpoY33zzTfr+++/TdNNNN8HXR48enb788stGDwAAAABacVDqk08+SWPHjk0zzzxzo+XxfMSIERP1Ow444IA066yzNgpsNTR48ODUtWvX6iPK/QAAAABo5eV7v8bRRx+drrjiinT99dfnJukT0q9fvzRy5Mjq45133il9PQEAAABorH2qoRlmmCG1a9cuffjhh42Wx/Pu3bv/7M8ef/zxOSh11113pcUWW+wn39exY8f8AAAAAKB+1DRTqkOHDmnppZdu1KS8aFq+4oor/uTPHXvssWnQoEHptttuS8sss0xJawsAAABAi8iUCn379k3bbbddDi4tt9xy6eSTT06jRo3Ks/GFbbfdNs0222y5N1Q45phjUv/+/dOQIUNSz549q72npp566vwAAAAAoP7VPCi1+eabp48//jgHmiLAtMQSS+QMqKL5+dtvv51n5CuceeaZeda+TTbZpNHvGTBgQBo4cGDp6w8AAABAMwxKhT322CM/JuTee+9t9PzNN98saa0AAAAAmFya9ex7AAAAADRPglIAAAAAlE5QCgAAAIDSCUoBAAAAUDpBKQAAAABKJygFAAAAQOkEpQAAAAAonaAUAAAAAKUTlAIAAACgdIJSAAAAAJROUAoAAACA0glKAQAAAFA6QSkAAAAASicoBQAAAEDpBKUAAAAAKJ2gFAAAAAClE5QCAAAAoHSCUgAAAACUTlAKAAAAgNIJSgEAAABQOkEpAAAAAEonKAUAAABA6QSlAAAAACidoBQAAAAApROUAgAAAKB0glIAAAAAlE5QCgAAAIDSCUoBAAAAUDpBKQAAAABKJygFAAAAQOkEpQAAAAAonaAUAAAAAKUTlAIAAACgdIJSAAAAAJROUAoAAACA0glKAQAAAFA6QSkAAAAASicoBQAAAEDpBKUAAAAAKJ2gFAAAAAClE5QCAAAAoHSCUgAAAACUTlAKAAAAgNIJSgEAAABQOkEpAAAAAEonKAUAAABA6QSlAAAAACidoBQAAAAApROUAgAAAKB0glIAAAAAlE5QCgAAAIDSCUoBAAAAUDpBKQAAAABKJygFAAAAQOkEpQAAAAAonaAUAAAAAKUTlAIAAACgdIJSAAAAAJROUAoAAACA0glKAQAAAFA6QSkAAAAASicoBQAAAEDpBKUAAAAAKJ2gFAAAAAClE5QCAAAAoHSCUgAAAACUTlAKAAAAgNIJSgEAAABQOkEpAAAAAEonKAUAAABA6QSlAAAAACidoBQAAAAApROUAgAAAKB0glIAAAAAlE5QCgAAAIDSCUoBAAAAUDpBKQAAAABKJygFAAAAQOkEpQAAAAAonaAUAAAAAK0zKHX66aennj17pk6dOqXll18+DRs27Gfff/XVV6cFFlggv3/RRRdNt9xyS2nrCgAAAEALCEpdeeWVqW/fvmnAgAHp8ccfT4svvnjq1atX+uijjyb4/gcffDBtueWWaccdd0xPPPFE+sMf/pAfzz77bOnrDgAAAEAzDUqdeOKJqU+fPql3795poYUWSmeddVbq3LlzuuCCCyb4/lNOOSX97ne/S/vvv39acMEF06BBg9JSSy2V/vGPf5S+7gAAAAD8b9qnGhozZkx67LHHUr9+/arL2rZtm9Zee+300EMPTfBnYnlkVjUUmVU33HDDBN8/evTo/CiMHDkyf/3yyy9TSzBu9Dep3n3ZppLqXh39PdimTcQ2nbSPy346SeynTcR+Omkfl/10kthPW9Y+2hy2qX20ZW3PYJtOGtu0ZR57/1dFzKVSqdRvUOqTTz5JY8eOTTPPPHOj5fH8xRdfnODPjBgxYoLvj+UTMnjw4HTYYYf9aHmPHj1+1boz8bo2hw/r6GaxlnWjWXxatukksU1bHtu05bFNW56636bOpS1rewbbdJLYpi2PbVq+r776KnXt2rU+g1JliCyshplV48aNS5999lmafvrpU5s2bWq6bq1BREcjAPjOO++kLl261Hp1aAK2actjm7Y8tmnLY5u2PLZpy2J7tjy2actjm5YrMqQiIDXrrLP+7PtqGpSaYYYZUrt27dKHH37YaHk87969+wR/JpZPyvs7duyYHw1169btV687kyYCUoJSLYtt2vLYpi2Pbdry2KYtj23astieLY9t2vLYpuX5uQypumh03qFDh7T00kunoUOHNspkiucrrrjiBH8mljd8f7jzzjt/8v0AAAAA1J+al+9Fad12222XlllmmbTccsulk08+OY0aNSrPxhe23XbbNNtss+XeUGGvvfZKq622WjrhhBPSBhtskK644or06KOPpnPOOafG/ycAAAAANJug1Oabb54+/vjj1L9//9ysfIkllki33XZbtZn522+/nWfkK6y00kppyJAh6ZBDDkkHHXRQmnfeefPMe4ssskgN/y/4KVE6OWDAgB+VUNJ82aYtj23a8timLY9t2vLYpi2L7dny2KYtj21an9pUfml+PgAAAABoYjXtKQUAAABA6yQoBQAAAEDpBKUAAAAAKJ2gFAAAAAClE5QCAAAAoHSCUjQb33//ff46bty4Wq8Kv8LYsWN9fgBAq1dMgm4ydKA1E5SiWbjiiivS1ltvnT755JPUtm1bgalm6Kuvvspf27Vrlx599NE0evToWq8STWRCgeIvv/zS5wstQMObZYNC9bttBDWanzfffDMNHjw4ffDBB6lNmza1Xh2gicW18A8//OBznQiCUjSLk/auu+6a/vWvf6W//e1v6cMPPxSYambefffdtP3226c77rgjXXvttWm55ZZLjz/+eK1XiyYSgeI33ngjB4/DVVddlQ444IA0cuRIn3Ed+qnAgoADDYMbcTH97bff5pvlOHa/+uqreV+nfrZRw302tpPAVPPxzDPPpF69eqX//Oc/6bbbbqv16lDHfurcbH+vbzfddFPaa6+90tChQw3ET4T2E/MmqKVOnTql+eefv3oA3n333dMZZ5yRZppppnygdpFc/7755pv02Wef5UDFCy+8kP75z3+mFVdc0fZrIcaMGZNOP/30HJR6+OGH06mnnpouvPDC1LVr11qvGuNpeMx84IEHcuCwffv2ad11160G+x1TGTFiRFp++eXT2WefnY/dkal8ww03pHnmmceHU2NxHRQBqLvvvjsfc0eNGpWvh0466STZNs3Eiy++mNZcc820ww47pAMPPDBNO+20E9zG0PCc/MQTT+TrrXi+7LLLVgPR/lbqz/nnn58OOuigtNVWW6WpppoqdezYsdarVPfaVIRZqWPFwTYuvPr165e22GKL9Mgjj6Tpp58+B6ZmnHFGN1HNZBteeumlOVtqvvnmS8cff3xaf/318+tugluGd955J+24447prrvuSrvssks688wz83Lbtz5FgDhG8aLHWxxH48b2/vvvT1NPPXWtV406ETfMkdn69ddf5+DUTjvtVOtV4v93/fXXp2233TafU3v06JEDUnPOOWf697//naabbjqfUx2LoEJsuy5duqRzzjmnujxaGnz++ef59RjQMahDw4BTBDhi/46BpAhCzz777Pn4TP2JAZzevXvn/XuTTTYRNJxI8rCpS3FSbpiyutJKK6VVVlklrbzyyvnGN26AI2Pq448/VsrXDE6ocePbs2fPdNZZZ6W55porX0BfffXV+T16hLUMMdI7zTTTpFVXXTX997//TRdffHFebvvWn9NOOy1dcMEFOWPx5ZdfTn/+85/T008/nTOnCsarWq9iMoo99tgj9wLs0KFD6t69e/ruu+9qvWqklFsYHH744fkR+3JksUWvxsUXX7xRQMo+XL+T9rz++uu5jUHh9ttvT/vss09aYIEF0qKLLpp22223XN5H61YEpI477rgc4IjBgag2WG+99XJguuE5m/rZv6+88srcdmbTTTetbsNoYxLb7PLLL8/JFfyYoBR155prrsk3SQ8++GC1J02MCHTu3Dkdc8wxORUyLpbff//9/LUITLkAq8+AVPQi2XPPPdPCCy+cR9ojSyouoOPkWozyxPa7+eab1Vw3Mw33uciwiRPxRRddlANTRx55ZKPAVIh9ldpvs2effTb1798/3xTdeOONaeDAgXl/jP4mkTEVlAO03r+POD5HdlSU6j300EM5GyeylONvZUKBKb3Iyi+Hj15fEbiI66DYj3//+99Xs1NvueWW/NU+XJ/iWjYGceJ8GQOsgwYNytdIn376aTrllFNy+fuwYcPSnXfeWetVpU6CHE8++WQ64YQTctuLyEaPv5E4Z8dAfRwLqB/R1DzKc2Mgp3DsscemPn36pC233DLtv//++XwaWW80JihFXXnppZfyhVYEKCL18e9//3s64ogj8msRkIp05whkRGBqu+22yyOGsXNHyrMLsPoS2yO21eabb56mnHLK9Morr+TlMRJ44okn5j42kTkVJ9q4Kd5www3TRx99VOvVZhKDjhE8jpuhONHGiXi22WbLaeZrrLFGOvroo3M2TjjssMPyw6yL5SoCBkUAMbbZW2+9lbNhbr311pxlEcfWuGCKZeedd17uhUDr3aej6XKRqRF9pWL/jvNsZCnHhXQRmIrjt+bn5Yv2BXEtdNlll+Us8ghIRcZUiAknYrtE82zqU+xjsX9F37all146Z47vt99+6aijjsrXtfGIjHLbkOK4/Nxzz+WM1RjkjfufmLExztkRAPnHP/6RBwyoreIaK+53FllkkXz9G9tmtdVWy9dVSy21VN6OEVRccMEFcwJGBBwlVPwfjc6pKzPMMEOeYS/Kf+LmNXbmyKyJk3Nk2kTGxfDhw3MmVRyQY4QgboTV3tefaMgY6atxw7vzzjtXl0fT3IUWWijf6BxyyCE5lTVGfh999NHcG4PmFXSMbRsBqMiCipPsZpttli+YYuQ3GjvG1zghx/a97777NHssWcMstWJyiBVWWCFvqwg6xAjeX//61/yeGKmPi9611lqr7NWkTvbp6667LgcqBwwY0Oi8GvtwBC0j23XffffNA0JxDFdiNHn9VBPjueeeOx9b11lnnRyEKkT2RAQ7oncj9SEG22LANR6zzDJLvjndeOON8/VtZK3Gtopjc7G9o31FBB3jxlYT69ZlQj04I3M1rrEuueSSXK4X90TRt7P424r7o27dutVojSk0PE7HxD+RWBG9dOM8GjNSR6A59usQmZKRlT7FFFP4ABuKRudQT0aMGFEZPHhwZamllqoccsghedn5559f6d27d6VNmzaVmWaaqfLJJ59U3z9u3Lj8dezYsTVbZ37s0ksvrayyyir5+88++6wyZMiQyvrrr1+ZbbbZ8vYNn3/+eeWDDz6ofPzxxz7CZubZZ5+tzD777JULLrggP//666/z/nnYYYdV3/P+++9Xrr322srBBx9ceemll2q4tq1Pw+PhbbfdVunYsWPlueeey89feOGFyjzzzFNZaKGFKo899lhl9OjRlbfffruy3nrrVZZffvnK999/X8M1p1aeeeaZSo8ePfL5dvx9vfC3v/0tH9eXXXbZyhNPPFGDtWw9imubO+64o9K3b99Knz59qtviqaeeyvtvr169KqeffnrlX//6V2W33XardO3atfLkk0/WeM0pPP3005XFF188P+LatW3btpXFFluscswxx/zkcfvQQw+t/OY3v6m8/PLLPshWpOE5O/bzF198sfLNN99UjwHdunWr/Pa3v6288cYb1XuluKZeeeWVKz/88EPN1ru1+89//pP354022qiy3377VS6++OLqaw3vVQsjR46srLPOOpUBAwaUvKb1T1CKmosLqJtvvrny0EMPVb744ou87MMPP6wcffTRlfnmm68amAo33nhj9ea24QG8uHijthpuh6FDh+YgRQQk4qS54YYbVnbdddfKkUcemZc//vjjNV1XJk6xn41/0fPf//63ssIKK1SDHBGg2mmnnaqvv/LKKwLFNdLw2HjZZZdV+vXrl/e5ueaaqxpIiK+xzeIGadZZZ62suOKKleWWW64yZsyY/LqL3NbnrrvuqiywwAL5RigCk+eee25l9dVXr8w888z5+F2I8/OXX35Z03VtLeLaaMopp8w3n7GvTjXVVJXLL788vzZs2LDKFltskQOJEfSIG50IVlEf4rwYgYT9998/X7d++umnlXvuuSfvU9NPP33lwAMPbPT+GLjbZZddKjPOOKPro1Ys/i4igDnHHHPkwPNrr72Wl19//fWV6aabLg8IxLFgpZVWyoP3ztm1E+fI7t27V373u99VNthgg3wtFQOAf/rTn/JgXygG+eJrDNTGsXzppZc2+DcBglLUVIzIzjnnnPmiat55582R42JkICLMEZhacMEFK3vttVejnxOEqi/F9vjuu+8a3RSfcMIJ+WI5RtcjIyPeF484qT744IM1XWcmXozYRnbbu+++W1125ZVXVpZZZpmc7RYXTzGKX2z3uLmNfTZuXqmdGLWLbRP7YeyDsS/OMssseV8MMeIaN72nnnpq5fbbb68GomRKtU6PPvpoZeGFF84X1PG3EiO/8Xfz73//O2d4NBwBZvKfTyPwF4Ny55xzTvW1vffeu9KhQ4fKJZdckp/HDWkcg+MxatQom6VOxLXQpptu2migpuH5dLPNNss3s1dffXV1kCfOobH8+eefr8EaUw+DSJERFfdDt956a+W6667LmZAzzDBDZfjw4dVA9EUXXVTp379/5YorrnDOrqHYPtNMM03eh7/99tu87M0338xZUzF4EOfPQhyf99hjj8q6666bg4kCiRMmKEXNnH322ZUpppgil3nFzW6kp0cAqoguF+mpEZhaZJFFKvvuu6+tVccX0HES3XrrrStrrbVWZZ999slp62H8EfXI2ph77rlz2R7NQ5ToRabNwIED80hP+Oqrryrzzz9/Xv7Xv/71R8GQtddeO48MU5v9MW5sIuAfQadCZKMWo3nF/jk+GVKt6+/ko48+qrzzzju5xLrI1ojjeByni5vjuOBeddVVcxkok0cE+eN6pxCZxJFls+SSS+bSvIaKwFRsq+LmhvoS1z0R2C3K2yPwUAzKhSjNiuPwjjvumJ/Hde97772XS3tonSL4HJk3J510UnVZXENtvPHGObOuGEwan3N2uWIfjv11k002qRx00EHVZYWo+DnllFPyMXrQoEF5WezbUSUSzw3+/TRBKWrivPPOywGpG264obrs9ddfz+Uj//jHP3KkOU7aIbItjj322JzSHCP61J8oq4yU1QgsbrPNNnl0J0YQIlW9EJkY0RcsRn2U7tW/8bMRzzrrrLxNo99FnGBDBJQjMBXbPC6m46IpUs+jr0n0p6Ecf/7znysHHHBAo2WxLWKfvP/++xstv/vuu/M+2LNnz2p/Gv34Wue+HeUgUVodI/NR+vX3v/99gu+NUfko9XzrrbdqsLYtW+x7UeYVx8zo69Zw+V/+8pcc9C96fDU8JscgXbx2zTXX1GS9+Xkx6BbXrNHva/xtV2SiRhZcnD+LthW0HnHcjYynQgwKLLroonmfLo7Dxd9MvPbHP/4xl1HHwBK1F4OykYU+oWNzsf9H/68111yzuqzhdZZA4oQJSlGTnTlK9eKmqGG6eTTZjXTmaLQbr0fQqhgZiBHEuAG2I9efCEbEwffwww+vLoubl5133jlfaEePiyjJjFGgzTffvFHTXOrP+AGKhs8jYByBqbiYjgulaG4e2zUycmJUPzIdY2RfA+TyxDHx+OOPz8fLI444oro8bnTiwjd6usV2avj+uFCK3kHRs+/VV18tcW2pF5H1FL2KYkQ3jskxghs3RDExQcPBhig/0uNm8ikCFEVgIoL5RdA/Xttyyy0r0047bW6mO74YpY+AFvVzLVRc08b2jJvWKMf7KTGIV/RlpHWJXo9Fu4tC7MtxHxQDAEWrhCLYEeVfcZ0dmc7UR6ZUbKfx+8I1vGY+7bTTcg+wyHYz8DdxBKWoibgIjhN2HICjNCDSIKOXRTSDjOfxepTsRQlQUatbEJiqL1H+ETPqFWnqxUE7+tVEKV+UfBUXaQ1vjqlfcXG03XbbVe68884fNc6NwFTUy8cNUVHyE/to3OTGz8XfA+WKm9czzzwzB6YiRbzhTU80Qo3M1KIsOvbDyKyKUdrobXDUUUc1Kiuh5YtzaASbitl/Ihs5sqWif1RD//znPyu77767HjeTSeyXsR8WN6dx7IzAYJRPFmXScTMT10dxczOhwBT1ITIjIsB0xhlnVEvw4lwZ2zMCv6G4MS0CkZE5HjMmxnPH39YhKkIabusYDCiukYt+Y/F3FLPjFuW8xftjQF9wo3YeeeSRXOoe4l4mJiyIhuUNZ8lseC0V2zXuYZl4glKUJk68DRvoRr+KCGZ06tQpB6QalgbEgTdunKLhKvWp4Yk1Rm+iN0KcNBuKGZtiO9J8xEhvlNHGxXSkk8fU1LEN4wa26AMWjR27dOlSOeyww5T01FDDAP3DDz9c2XPPPfN2i8ypwlZbbVVZYoklKn/4wx9yACoCUausskp+LS6qokSI1nO8Lvq8RYl1lORG8CPOw5HZWrwn+hvF7KmhmHiEpt8esR/G4FscT4vAVDSVj2uiaHpdZEwVganIJI9JJKhP0dg4tmcM0MV+E8HeogQzslijCXL4+OOPc0lsZBdrat56RIA/rqcaZpJHz9zxz9kR5CgqRorJYhpebwtMlS8yymNin8h8LAJT0Z6kXbt2le233z4PwjcUvf7iHBvXZEy8tglKcNttt6V+/fqlTTbZJH322WcRDE0LLrhguuuuu9Jcc82Vunbtmh+FH374IX3++edp7rnntn3qSGy3MG7cuPworLbaaumRRx5Jl19+efrmm2+qy7t06ZJmmWWWNHbs2OrPUt86dOiQDjnkkLTAAgvk76+44oq8b1577bVpySWXzI/vvvsurbjiiumcc85JZ599dhoxYkStV7tVateuXf564IEHpj59+qQvvvgizTPPPGn//fdPhx12WH7t0ksvTTvssEPq2LFj+te//pV69uyZ7rzzzvxat27d8jH2/x+gqun/C5NXmzZt0jXXXJN23nnn9Nprr+XtPnz48LTyyiun9dZbL+/H8Z6RI0em22+/PT355JP5uD3llFPaNE0s9rX4rO+55558/XPkkUemG264IZ87N9hgg/z9BRdckAYOHJjef//91LZt23TllVemxRdfPG+/b7/91japI7GfhBtvvDEtscQS6eijj87ba7rppsvbdrfddkv9+/dPK620UppzzjnTn//853TxxRenoUOH5utgWofBgwenzp075334iSeeyMeBAw44IJ166qn5nH3cccfl980777z5vD3jjDPm83ncC8XxohDHA8oV58utttoqffjhh+mggw5Kb7/9dlp99dXTSSedlC677LL017/+NV144YXpo48+Svfee2/64x//mN577710wgkn5J93fTWRJiGABf+TGDWaa6658rTkka5eKMryYqQoZiGJRqtRNx3WX3/9nKVRZFZJba69YhtEmVZkX0SWRcyyV4z0xShQjBJuscUWeVvHSG/0H3ruuedqvOb8Lxk4t9xyS86G2muvvarLb7rppjziGynLyy67bB7hiyyLGPmlNiKzYuqpp6488MADeR+NzJeYGKJt27Y5k62h4pgbx9Uov4wZfYoJJWjZx+0oBYmeb1FeVIzytm/fPh+zizLceG/8XUS/R73GJq+GMzBFpnFkM8YU70VPojjPxih8nEcblvIVvWaoLw1njY7M/9jXLrzwwursiFH6M3jw4NzEOjLjTBrQuhT3MlH2FRlQUVYfE/4Ux+eY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GyGPf1MUyyMTrth3KUcEDGKmy+gvEeL4GYHDCOIXo3gRmCpS/4sG1g3pFdRyFdu2YTl8zBYU2XJ77rlndVmcX88666x8Tm44q1PM9DR+2TyT55gbZfDF94ceemgOEMaxdtddd61uxzgOx/Io52s4KEBtxTVPZEdFb5nxNez1FdsrblojIFyUZtG6TOh8G8fZOCZHj7gISEVvv0Isi7+rjz/+uLpMYKN8cT8TE2/F9mg4O20ojsMxwBOBqbgebhiYomkJStEkB+I4CcdobJSNxIjSK6+8Ut2hI+MmAhuxQ8fIfsw4ETPMxJS5RXmJA3FtxYE4bmxjpKChGP2LtNU555wzN+tcaaWVqr0RInMmtq8MmvoW26g4sTacuS0CTVE6Eifb2P8iALn55pvn7XzKKafkG9nIhotASExaQHkiMBjHyyjbayhuZKOxZuyvRTAx+kpFY+TIpBp/phha9s1PZLbG+bRhL5KYDXXLLbds9P7Y/2NZDCBQjuKYG9c/ceP5zDPPVEvgo6wyymuLQHIEoQ466KBKz5499R6qM1FiGcfXGJhrOKATg3dxvRStDk4++eScBRePyByPhudm22u9Aan4+/jss8/y99GDLK6zor9uXFcVYpAvBvJjEJ/aiWumCDqP32cxrq8effTRPBhfDMjGtXL79u1zNlUkXdD02ib4ldq2bZu222671LNnz/Tiiy+mJZdcMs0zzzz5tTZt2qTf/e536d///nc677zz0p///Oe01VZbpSOOOCINHTo0TTHFFOmHH35I7dq1sx1qrHfv3mncuHFp1113zc9POOGEdM4556QLL7ww/ec//0mDBg1K77//fjr11FPT6NGjU8eOHfP2nXLKKWu96vyEGHiIbfTpp5/m57G/xX734IMPpuuuuy4tvPDCeV+84YYb0gILLJCOPfbYtMsuu+Rtf9ZZZ6WXX345/fe//02LLbaYz7hEs802W1pllVXSY489lh599NG87E9/+lP6+OOPU9euXdN6662XNtlkk7ztvvrqq3T55ZenddddN00//fS2UwsXx+g45z711FNphRVWSFtssUVabrnl8r4+duzYNNdcc6W33347H6vjvSGOAauvvnp677330siRI2v9v9AqxGcex9jNN988derUKXXo0CEvj+ufPffcM3344Yf5ummbbbbJ7zn77LPz++eee+5arzoNxH71xhtv5HNmiOufffbZJ58XY/tNO+206dxzz03Dhw/P10RHHXVUeuGFF9I000zjc2xFfyNxTA6HHHJI/ru4++678/F4gw02yPc933//fb7Pib+TOKfHsg8++CAdf/zx1d9B+eL+86OPPsrXwoU4Fm+77bZp2WWXTRtttFG+1vriiy/Spptumi666KJ8XdylSxeba3KYDIEuWtlIYGRZRFQ5Misi9TxG8iMLoxgpapidMT4ZUvWVph511VFqGZls8fXee+9t9J4YFYyReJqPGLGLbIr+/ftXbrnllpyBc9NNNzWagjz6nDSchjhKgqIUyFTktRO9S2IGvdg+cUyN2fYiYy22S5TyxfE2MhgjM7XhJAOOqS1/NL4otY5SsPFF5lxkamyxxRaNSjqjd1FkSjWcUZXJJ1oURNZpw4bGocigiKa6BxxwQJ4hM7JuTCBRn+L6NfpoRnnPPPPMk0uqjznmmGovoDhnxjE4erjRusXxeOaZZ87n508//bTRa1H2teiii+aJSKLtRfSJVClSe5F13r1793x+jGzI6Lka26lPnz55O5599tn5+jiy3MYvz9QeoekJSvE/aXjjEwfWhv0PoolnpKaP34cmAhxucutH3LBEKcHBBx9cDSDG9ontF0GMddZZ50dN7ONG529/+5ueF81I3ARFY80uXbrkUsxihr3i5rQITEUgMtKTi7Iw6iMwFRevUUIZ/Q4mFEiO46pAVOsRM9VGYHmHHXZotDwmnRg4cGB1opHpp5++ssIKK+TAZhy3YwKLKPejHLfddlsuC4lroHjEeTX25eixGbPtjd+7hPr1wQcf5BvU6AdUtKYIcdyNAZw11lijMmTIkJquI7W/no7ZGYsZGKNPVAQuYzBw6NChedmIESMqDz30kIlI6kQRVIoSy27duuXy6QhAxYBB0TcqrrHmn3/+H7VSYPJQvsckKVKYi3K7wYMHp9///ve5dOSqq66qpqfvtttuudQk0h4jlblXr17p6KOPTp07d/aJ14Fnn302bbzxxrksL0o8ilTzqaaaKi8/9NBD09NPP5123nnnvDzS0mPZnXfembdt+/btc3kC9S/KCxZZZJFc5hWef/75/DVKSr777ru8LaN8b4455kj77bdfuv3222u8xhTmnXfeXEYZZVqRNn7//fc3SjuPUr7VVlstH4+jVICWb4YZZkgzzTRTLiGIY3SI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h+tbBA3iuih6gsU2fvLJJ/PMlbHf3XLLLfl61o1q6xYzmMb+HDOcxix7MbgX/R7jWPyb3/wmHXvssfkYEX9L0ZOs6EWkl27ten/985//zL04o59uJExMbCJGnGuDYGJ5BKX4xR0zbmyjMXIEKq666qo8zemnn36as24OPfTQtP/+++cLsxi5Hf+E7UBc+yBFNLiOZtajR49O0047bd5WkfEWI7dxQxMN64vRwWjMGgfwGO3RmLF5beciuBgXztF4NTKgIrAYN0zRDDn24ciCiwyq+FsA6nufjgyof/3rX7mp+QwzzNCoWXachyMbJ5rrxo107OuPPfZYnm6cpr+5iUB+ZBXHObNjx455YpBjjjkmv2fZZZfNy9955520ww475GXxM8svv3y6+OKL05FHHpmXCWjUtwgenHTSSbmp+XHHHZe3c0wiENexce3rvNm63HvvvTnLPI63cX1cDPhFgCMGAmLAN4JS2267bR4QWHjhhfMgYEwM1DBLR1CjfMX9awzmxH1qVIjENosKnhhQGD+LquGkFeHss8/OA7lxDqZENSgZpBk57rjjKtNOO23lv//9b2XYsGG5oefMM8+cpyaP6TKLhn677rprnp6a+hN9hKI3wh133NGoJv43v/lNbs4aU9dutdVWlQ033LAy66yzasLajBQ173feeWdl2223rWy88caVAw44oNqAddSoUY3ef9BBB+XGrW+88UZN1heYONdee22lU6dOuZF59GhcZ5118tT0DcVxfKqppsoTF0Q/HP3/mlbRQ+ipp57K59A4fhbL99hjj8ree+9d+fbbb6v9GOeZZ568PPr2HXjggXkqeM2wm5ei52LDHlPR5Pj999+v4VpRtnPPPbfSvXv3fF8TPfvmn3/+3E+3EMfalVZaqbLddttV+7ZGc+x//vOf1eOw43H5GvZ9i3ue2WefPTcuD7feems+l1544YU/+rmG2yr6A8Z9b0wCRbnaxH/KDIJRv2Iq3MiqKbKb4k8jRgFihDbSUxvWVK+33no5TT2iyTH9aYwMRDaOPie19+abb+a04jXWWCNNOeWUuTY6RnIHDRqUNtpoo+r7br311pxNE6Pr8b7oORRZVPPMM09N159f1nBEJ7ZhbLfoXRL737Bhw/KIb/Q9iH03RoQii/HOO+/MU9xGGUKkngP16b333ss9G3/729/maejjPBvlIb/73e9yH8fodVM4+uijc/+Seeedt6br3FJF9lOUvMf5NLJlCtHjK0qhv/vuu9SzZ8/csiB6+UWGTWRGROZqnGMda1tOGRCtwznnnJN748b+vs4661R7cMY1VWRDFaKi4LrrrksLLrhgev/993O/v8hqjf2/4TUak98JJ5yQ9t1330b7bGSpxjaM3n7XXnttzi6O9+28885p5MiR6Y033siZbQ338TjXRnZVlGDHeZWSlRwEo04NGDAgR/0bRotjlOi3v/1tnjGm4bIQ08xHxDlmlymmyoyfNTJQWzFaE7MzRcZTZELF9NN/+ctf8rb6wx/+kGcKef311xv9TDEziG1X/77++utGz2MEf8EFF6zOshezKs4yyyx5lGfhhReuztAVs3XFdOSRNQfUr8ceeyyfc9ddd93Ku+++2yhrOaYgj+ychrOnMnlFVmlkPG200UaV+++/Py8bPHhwpXPnznkW4phVb4EFFqgsvvjiOaP8+eefr9x4442Vt99+26aBZuaqq67K18sNs2QiG3Leeeet/PWvf82z6Z1//vmVzz77rPLOO+9UDj/88HzNvf3225tlr0Zi1tnYZjFjZkNXX3113l6RGRWzjp955pnV166//vo8m21kuBb+8Y9/VKabbjoZUjUkKEVVEVyKG9ciJX3IkCG53Oeyyy5r9Emdc845OW11pplmquy8884+xTpSpBg//vjjefsde+yxlYUWWigftCNQFSnJa621VmWbbbapnHbaaZVHH300v19Qqr4dddRRuczygw8+qC4bOnRoZccdd8zfx43q3HPPnZ/fdttteTuvuOKK1ZNuBCyB+halAxFQjovjl19+udFrEZiKAEmfPn0EPUoU2yFaFkRgKoL7cd0T08EXoqwyzq9xXQQ033ugCFREuV7DEq8Y0I37oCjNjWNA7OtHHHHEj8rFit9B+aJULwbho4SyMHz48Mpyyy1X6dixYx5IKHzzzTeVDTbYIF8rF/c98d4o0b7yyittvhoSlKJ6UI2dM6LHxShBZEXFaF/v3r0rK6+8cq6VDpF9Ef2HTj311DxiEFk5L730kk+yTkwouBSBqa233rryxBNP5IN39MZYb731KiussMKPbnyoz/3z5ptvzvtmjNY1DEy98MIL+WucjCMrrshYjCzHeH/cxOo3A83HJZdcUllkkUXyDVBktzYUI/Oxbzcc4WXyi2uc6OsVNy7HH398XhbH2cg0joy2xRZbLI/MF8uB5icyoGLANu55IjAVWauRBdmwwiCyoiIAUvTVLdjvyxX3NHFdXIh7mxjM2XTTTavLTjjhhFw9sNdee+WBhHhEFnIcrxtW+cS2fO6550r+P2B8glL8SKRAxo4dTc3DM888kxuZR1PsOeecMz/igjlEmnpkZ7hArv+U5G7dujUqB5lQORj1GZCKkpBXX301l4e0bds274/vvfde9X1xQo0Lp0svvTQ//+6773IwOTLlxt/mQH0obmIiyzFueuJcW4gbolVXXTWfj1977bVGP/fpp5+Wvq5U8jE4bmhiQCeOxYVDDz00Xxcp2YPmq2hPEoGpyJiK7KgIaMR+3zDb/MQTT6wsv/zy+X3U7ty5/vrr51YVUTEwfmDqT3/6U3XZkUceWVl77bUr7dq1y8HGSKoo2pbIbKsvglKt2PhR/oY7Z2RcRA1uEZiKkrDoXxNR5yjlK94b0efVV1+98vnnn5e89kzKwTuyaXr06FE9uRYnXyM79R+Qiuy26F8SmYnFDCIRmNpll12qGVNxgo2TbdwwPf3005X9998/9zkRkIL6VBx7Y5a92Ffj+BxZxzFKX5xPIxM5AlNRtiujtb5K+Xr16pVL5I855pg8S2J8D7SM666YhTFalCyzzDK5LLcIYsS1c+z7RVY6tROff2RLdenS5RcDUyNHjsytaWJAp9huAlL1R1CqlYpRvggm/ec//2m0vAhWjB+YKnpMFWLn3nPPPXP2VASrqH8xpW1Mc0vzuTCKBo4RkIrpxUNxMr3rrruqgaliquq4uY1sqeglFT0RomEyUL/uueeeXA4WExXceeedOfN4xhlnzM1Zv/rqq/yeaKQdpQY77LBD9caI2gemorlx9JaaYoopqn0ZgZZz/RWDA5ExFb05IzAVQYzIsokerQ1LvyhXw/vUqBhYbbXVcqZqnE/HD0zFZFwTMn4vMOpDm/hP2TP+UXsxlfEuu+ySpp566tSvX7+08sorV1+L6eRjStOw9dZbp9tuuy2ddtppeXrMDh06pO+//z5ddNFF6fbbb0/9+/dPiy22WA3/T/glxdS0MTX1+uuvn4488kgfWh0rpqd9+umn04orrpj23nvvRtssphlfc80103//+988RfxOO+2UjjnmmNStW7f06aefptdffz316NEjde/evab/H8D/KfbLKaaYorps4MCBeQrxm266qbrs7bffTksttVT6wx/+kM4777y87NJLL02rrrpqmmOOOXykdXQNFVOHH3XUUY2miQdaznXY559/nvbYY4/0zjvv5GNzx44d07PPPpuP4z/88ENq3759rVe11Yrj77Bhw/L96vDhw/O2ufLKK/P1cbjzzjvzPWzco8b31D9BqVbslVdeSXvuuWcOWhx66KHVwFQ8jwNyEZiKC+FlllkmXXvttdWfjcDUt99+m7p06VKz9WfSnHnmmfnGZpFFFvHR1bm4AIob0zi5xkm2cMQRR6SzzjorB4TjRii+RqAxAsyxD88yyyw1XW/gx66++uq0xRZbpH/9619p3XXXzTcycZ7ddttt843Of/7zn/y+0aNH5wvrIUOG5P05LqTnmmsuH2mdiuughkFGoOUFpr744ot8rP7qq6/SHXfcISBVByIxIu5fhw4dmnr27JmDhwcddFB+HveqRWDq3//+dzrjjDPy19iW1DdbqBWbd95506mnnpqzaAYNGpTuv//+vDyeR0Dq3XffTRtttFGONF911VXVn4uL6bgQE5BqXnbddVcBqWYishXnnHPO9N1336UHHnggLzv66KPTKaeckrMnIiAV7+nVq1e65ZZbcqAqsqViGVBfNt1002pWYwSaxowZk8+zm2yySXruuefSddddl98XAanQqVOnfA6OTGbql4AUNF+/VCgUQYwITEUW+uWXX54DHjKkaiO2Q0MxmLPSSiulZZddNs0444xpvvnmS//85z9zcsU222yTKwnC73//+3yNXGxL6pugVCvXMDAVWRjFDfCHH36Yttpqq/TMM8/kEoO4QC5ueOO9ND+2W/MRIz+XXXZZvnk99thj084775xOPPHEvCxubkMx6hPZb08++WTOliqyG4H6EPtwuPnmm9Nyyy2XA1N33XVXXh4ZyJHpGMHmIjAVJSGPPvpovhES9ABoGuMHJSbmmjius+KYPNVUU1WvuVxLl6/47E8++eTcpiLOjU899VT19WIbRUbbBx98kFZfffX02GOPTfB3UL+U79GolC8Otn/9619zD6nIlIqd3sgA1MbLL7+c+xlEFmNkM+67777V0b3YVw855JB0wQUX5P03TshAffb0iwGeESNGpPXWWy+P6kaQOQLMceEc59sbb7wxZ0dGdlT0LIlR+egDCEDTlOKFyKh54okncib6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O++8PkOAX2lCgaUomT7hhBPSs88+m4YMGZKWXnrpHMyIsuqYZCJKwI499liffTMTs+9FCWaUZAomtm6CUgAA480Q1KFDh1ySd+ONN+bZ9iJbKsr14qbo1FNPzU3NY5rxa6+91mcH0MTGn8E0sqIiCBWBjMsvv7yaMdWvX79conf22Wc36j9F8yLLrXUTlAIAaODll19Oe+yxR7rvvvvSoEGDchPzhj799NNquZ4MKYBf7/bbb88ZqZGNGjPpHX/88XlQoGfPno0ypg455JAcjIoS64UXXjh99tlneTKKyLQx6yk0T4JSAAATmCEopq6OZqsNZwj6/vvvTTEO0ISiT98KK6yQJ5N44oknckleNMGeYoop0gUXXNAoMHXSSSelfffdNweiIkgVM6AGmTbQfOkEBwAwgRmC/vGPf+SR9yOOOKI6Q1DcJAHQdKaZZprcxHz66adPa665Zg5QHXbYYTnQtN1226XXX3+9+t555pknbbrpprnvX8NMVT2JoPkSlAIAmIC44Yn+URGIihK+GL0HoOlE4CnMN998uUdfZKdutNFGuWdU//798/OtttoqZ1B98MEH6Z///Gc+Nh944IH5tbFjx9oc0MwJSgEA/IS4+YmZgX7zm9+kWWed1ecE0ASiN1++GW3bNo0ZMyY3K59jjjnSDDPMkO6///609tpr58BU9PWLhucx494aa6yRXnzxxTRw4MD8s5HJGoEpoHnTUwoA4BfETVPMyAfArxOTSEQWVJTo/fa3v60uj7K8l156KfeN2n///XPQ6Z577sn9o2666aYcwFpvvfWqGVICUtAyCEoBAABQigg87bLLLmmqqaZKhx9+eM6C2mSTTXIW1K233pp69OiRXnjhhVy2F8GnmO00+k0VBKSgZRGUAgAAoDSvvPJK2nPPPXO208iRI9OoUaPSdddd12imvQhSrbPOOjmb6rLLLrN1oIUSlAIAAKD0wNRuu+2Whg8fns4999xcvlc0Py9m03vrrbdyTz+letByCUoBAABQutdeey3tvvvuOQh10EEHpVVWWeVHgamgZA9aLkEpAAAAalrKFw455JC08sor2xLQivxf+BkAAABKNO+886ZTTz01l+jtvffe6emnn/b5QysiKAUAAEBNA1PHHXdcbmq+yCKL2BLQiijfAwAAoG6M31MKaLkEpQAAAAAonfAzAAAAAKUTlAIAAACgdIJSAAAAAJROUAoAAACA0glKAQAAAFA6QSkAAAAASicoBQAAAEDpBKUAAAAAKJ2gFAAAAAClE5QCAAAAIJXt/wOUevjPFYeD7AAAAABJRU5ErkJggg==",
            "text/plain": [
              "<Figure size 1200x600 with 1 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "image/png": "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y5YN89onX6FlOCrZWbJkibveURmuSvNKlChhDz74oH3xxRfBklxE1nbzyl31VcfXPHnyWPXq1V07A68dheh4q3K+33//PbjPITrpGkrl0mqNoNK9zp07W/PmzW3s2LH25JNPutL63Llzu3N44cKFXbmf/r503YXw8EouVWK5ceNG+/TTT4Pny5w5c7rXTp065bZbgQIF2EyJLRECXYgSEyZMcKO03qj9gAEDXInXM888E2jQoIF7bfDgwcHsKb1PGVN58+Z1I4d/V8YH/6xdu9aNECg9PXT2pokTJwbWr18fHCnQCK/SkjWbk0bilWmzcuVKNlUS4GUuajtu3bo1sGjRIpeSrO2uzDiPlzGl0d8HH3wwsGnTprCtM/6+lE+PhQsX8jFFKS8jTlmtaqKrsk6VhygryhuVD31faHk9Eof3GcedMU/XRsoU79ChQ7DRua5/1Dx79uzZbI4IozKdDz/8MNY1qrISlT0cSvuZmtfrPKoyTUSPC82KqUxUHYPV1Fx/G5rgQDRxUOhM1kga21P3RCqtVrkl587ER3gWF00ZNM8995zLhNJokRp4aiSgZs2abmTpxhtvtHbt2rnRJ71PWVLKoFI0uly5ci5CrfcxShA++/fvdyN/at6nxuUejep27drVsmXL5jIy1Pj6nnvucdtMmVHHjh1zzRqvu+66MK494tPgXJMLvPPOO/b000+7EV1t0+PHj7usCm3XPn36uIwpNbLXa3ovIjNjymu0q+PrmDFjaD4fhbyMOGW1fvfdd270/ZVXXrE33njDatSoEet95xsZRuIcZ9X8+qOPPnLXNldeeaXLkOjSpYtrkps1a9bg+3v37u0yVq+++mo2RQRR5oq2obafMl6UQbFz50675pprXCN6XSvJtGnT3Dny0KFD9s0337gMZETXBBOyfv16l0mjfVuZNLoHUoa5smxCr48vueQS90BkilvpoXsjTV6g6hFNRrF48eLgOZeJuBKRD4EvJGPqkfDcc88FUqRI4Zp5fvnll7FeHzFihGvq52VMhSLqHH7qK1KyZEnXK8GjPkPqW/POO++40V1t159++inYk0h9E3755ZcwrjXia/r06a6PjHobKNPGo+05bNgw12NKmVEaydf+6mXIIXJpn9T2otFmdI7oqq9f6OiuzsVXXnmly0YmayN8x1mdO3UsVcZEqVKlAi1atAi+rn1V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",
            "text/plain": [
              "<Figure size 1200x600 with 1 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "image/png": "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rdcIeKtWraJXr14xb9682mXbtm0r5/v161fnNl988cW3IrtFixblr7tq/9atW5dvcPsJAAAAmsw14IXiEWTFiHfv3r2jT58+5TO+N23aVN4VvTBixIjo0qVLeRp5YciQIeWd008//fTymeHvv/9+3HrrreXymhAHAACAxq7eAT5s2LBYu3ZtTJgwIaqqqqJnz54xd+7c2huzrVy5cocR71tuuSWaNWtW/vrJJ5/Ej370ozK+77777n37TgAAAKCxXAPemM69BwAAgAP2GnAAAABg7whwAAAASCDAAQAAIIEABwAAgAQCHAAAABIIcAAAAEggwAEAACCBAAcAAIAEAhwAAAASCHAAAABIIMABAAAggQAHAACABAIcAAAAEghwAAAASCDAAQAAIIEABwAAgAQCHAAAABIIcAAAAEggwAEAACCBAAcAAIAEAhwAAAASCHAAAABIIMABAAAggQAHAACABAIcAAAAEghwAAAASCDAAQAAIIEABwAAgAQCHAAAABIIcAAAAEggwAEAACCBAAcAAIAEAhwAAAASCHAAAABIIMABAAAggQAHAACABAIcAAAAEghwAAAASCDAAQAAIIEABwAAgAQCHAAAABIIcAAAAEggwAEAACCBAAcAAIAEAhwAAAASCHAAAABIIMABAAAggQAHAACABAIcAAAAEghwAAAASCDAAQAAIIEABwAAgAQCHAAAABIIcAAAAEggwAEAACCBAAcAAIAEAhwAAAASCHAAAABIIMABAAAggQAHAACABAIcAAAAEghwAAAASCDAAQAAIIEABwAAgAQCHAAAABIIcAAAAEggwAEAACCBAAcAAIAEAhwAAAASCHAAAABIIMABAAAggQAHAACABAIcAAAAEghwAAAASCDAAQAAIIEABwAAgAQCHAAAABIIcAAAAEggwAEAACCBAAcAAIAEAhwAAAASCHAAAAA4UAN8ypQp0a1bt2jTpk307ds3Fi1atNv1169fH9dcc00cffTR0bp16zjhhBNizpw5e7vPAAAAcNBpWd8NZsyYEWPGjImpU6eW8T158uQYNGhQLF++PDp06PCt9bds2RLnn39++bWZM2dGly5d4uOPP44jjjhiX70HAAAAOOA1q1QqlfpsUET3mWeeGQ8//HA5v23btujatWtce+21MW7cuG+tX4T6/fffH8uWLYtDDjlkr3ayuro62rVrFxs2bIi2bdvu1fcAAACA/dmh9ToFvRjNXrx4cQwcOPB/36B583J+4cKFdW7z4osvRr9+/cpT0Dt27Bjdu3ePe+65J7Zu3brL19m8eXP5ZrefAAAA4GBWrwBft25dGc5FSG+vmK+qqqpzmxUrVpSnnhfbFdd933rrrfHggw/GXXfdtcvXmTRpUvlJQ81UjLADAADAwazB74JenKJeXP/92GOPRa9evWLYsGFx8803l6em78r48ePLYf6aadWqVQ29mwAAAHDg3IStffv20aJFi1i9evUOy4v5Tp061blNcefz4trvYrsaJ598cjliXpzS3qpVq29tU9wpvZgAAACgSY6AF7FcjGLPmzdvhxHuYr64zrsuAwYMiPfff79cr8Z7771Xhnld8Q0AAACNUb1PQS8eQTZt2rR4+umnY+nSpfG73/0uNm3aFKNGjSq/PmLEiPIU8hrF1z/77LO47rrryvCePXt2eRO24qZsAAAA0FTU+zngxTXca9eujQkTJpSnkffs2TPmzp1be2O2lStXlndGr1HcQO3ll1+O0aNHx2mnnVY+B7yI8bFjx+7bdwIAAACN6Tng+4PngAMAANCkngMOAAAA7B0BDgAAAAkEOAAAACQQ4AAAAJBAgAMAAEACAQ4AAAAJBDgAAAAkEOAAAACQQIADAABAAgEOAAAACQQ4AAAAJBDgAAAAkECAAwAAQAIBDgAAAAkEOAAAACQQ4AAAAJBAgAMAAEACAQ4AAAAJBDgAAAAkEOAAAACQQIADAABAAgEOAAAACQQ4AAAAJBDgAAAAkECAAwAAQAIBDgAAAAkEOAAAACQQ4AAAAJBAgAMAAEACAQ4AAAAJBDgAAAAkEOAAAACQQIADAABAAgEOAAAACQQ4AAAAJBDgAAAAkECAAwAAQAIBDgAAAAkEOAAAACQQ4AAAAJBAgAMAAEACAQ4AAAAJBDgAAAAkEOAAAACQQIADAABAAgEOAAAACQQ4AAAAJBDgAAAAkECAAwAAQAIBDgAAAAkEOAAAACQQ4AAAAJBAgAMAAEACAQ4AAAAJBDgAAAAkEOAAAACQQIADAABAAgEOAAAACQQ4AAAAJBDgAAAAkECAAwAAQAIBDgAAAAkEOAAAACQQ4AAAAJBAgAMAAEACAQ4AAAAJBDgAAAAkEOAAAACQQIADAABAAgEOAAAACQQ4AAAAJBDgAAAAkECAAwAAQAIBDgAAAAkEOAAAACQQ4AAAAJBAgAMAAEACAQ4AAAAJBDgAAAAkEOAAAACQQIADAADAgRrgU6ZMiW7dukWbNm2ib9++sWjRoj3abvr06dGsWbMYOnTo3rwsAAAANJ0AnzFjRowZMyYmTpwYS5YsiR49esSgQYNizZo1u93uo48+ihtuuCHOOeec77O/AAAA0DQC/KGHHoorrrgiRo0aFaecckpMnTo1DjvssHjyySd3uc3WrVvjkksuidtvvz2OPfbY77vPAAAA0LgDfMuWLbF48eIYOHDg/75B8+bl/MKFC3e53R133BEdOnSIyy67bI9eZ/PmzVFdXb3DBAAAAE0mwNetW1eOZnfs2HGH5cV8VVVVndu88cYb8cQTT8S0adP2+HUmTZoU7dq1q526du1an90EAACApnUX9I0bN8all15axnf79u33eLvx48fHhg0baqdVq1Y15G4CAABAg2tZn5WLiG7RokWsXr16h+XFfKdOnb61/gcffFDefG3IkCG1y7Zt2/b/L9yyZSxfvjyOO+64b23XunXrcgIAAIAmOQLeqlWr6NWrV8ybN2+HoC7m+/Xr9631TzrppHjnnXfi7bffrp0uuuiiOO+888r/dmo5AAAATUW9RsALxSPIRo4cGb17944+ffrE5MmTY9OmTeVd0QsjRoyILl26lNdxF88J7969+w7bH3HEEeWvOy8HAACAxqzeAT5s2LBYu3ZtTJgwobzxWs+ePWPu3Lm1N2ZbuXJleWd0AAAA4H+aVSqVShzgiseQFXdDL27I1rZt2/29OwAAADRy1Q3QoYaqAQAAIIEABwAAgAQCHAAAABIIcAAAAEggwAEAACCBAAcAAIAEAhwAAAASCHAAAABIIMABAAAggQAHAACABAIcAAAAEghwAAAASCDAAQAAIIEABwAAgAQCHAAAABIIcAAAAEggwAEAACCBAAcAAIAEAhwAAAASCHAAAABIIMABAAAggQAHAACABAIcAAAABDgAAAA0DkbAAQAAIIEABwAAgAQCHAAAABIIcAAAAEggwAEAACCBAAcAAIAEAhwAAAASCHAAAABIIMABAAAggQAHAACABAIcAAAAEghwAAAASCDAAQAAIIEABwAAgAQCHAAAABIIcAAAAEggwAEAACCBAAcAAIAEAhwAAAASCHAAAABIIMABAAAggQAHAACABAIcAAAAEghwAAAASCDAAQAAIIEABwAAgAQCHAAAABIIcAAAAEggwAEAACCBAAcAAIAEAhwAAAASCHAAAAAQ4AAAANA4GAEHAACABAIcAAAAEghwAAAASCDAAQAAIIEABwAAgAQCHAAAABIIcAAAAEggwAEAACCBAAcAAIAEAhwAAAASCHAAAABIIMABAAAggQAHAACABAIcAAAAEghwAAAASCDAAQAAIIEABwAAgAQCHAAAABIIcAAAAEggwAEAACCBAAcAAIAEAhwAAAASCHAAAABIIMABAAAggQAHAACAAzXAp0yZEt26dYs2bdpE3759Y9GiRbtcd9q0aXHOOefEkUceWU4DBw7c7foAAADQGNU7wGfMmBFjxoyJiRMnxpIlS6JHjx4xaNCgWLNmTZ3rz58/P4YPHx6vvfZaLFy4MLp27RoXXHBBfPLJJ/ti/wEAAOCg0KxSqVTqs0Ex4n3mmWfGww8/XM5v27atjOprr702xo0b953bb926tRwJL7YfMWLEHr1mdXV1tGvXLjZs2BBt27atz+4CAABAvTVEh9ZrBHzLli2xePHi8jTy2m/QvHk5X4xu74kvvvgivv766zjqqKN2uc7mzZvLN7v9BAAAAAezegX4unXryhHsjh077rC8mK+qqtqj7zF27Njo3LnzDhG/s0mTJpWfNNRMxQg7AAAAHMxS74J+7733xvTp02PWrFnlDdx2Zfz48eUwf820atWqzN0EAACAfa5lfVZu3759tGjRIlavXr3D8mK+U6dOu932gQceKAP81VdfjdNOO22367Zu3bqcAAAAoLGo1wh4q1atolevXjFv3rzaZcVN2Ir5fv367XK7++67L+68886YO3du9O7d+/vtMQAAADT2EfBC8QiykSNHliHdp0+fmDx5cmzatClGjRpVfr24s3mXLl3K67gLf/zjH2PChAnxzDPPlM8Or7lW/Ac/+EE5AQAAQFNQ7wAfNmxYrF27tozqIqZ79uxZjmzX3Jht5cqV5Z3Razz66KPl3dN/9atf7fB9iueI33bbbfviPQAAAEDjew74/uA54AAAADSp54ADAAAAe0eAAwAAQAIBDgAAAAkEOAAAACQQ4AAAAJBAgAMAAEACAQ4AAAAJBDgAAAAkEOAAAACQQIADAABAAgEOAAAACQQ4AAAAJBDgAAAAkECAAwAAQAIBDgAAAAkE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8EK/9TQKC/dRg7aM/WzdunXlP0DFP0jbK+aXLVtW5zZVVVV1rl8shwPR3hznOxs7dmx5jcrOf/DhYD7O33jjjfI0xeJULmisx3kRIv/4xz/ikksuKYPk/fffj6uvvrr8ULU4XRcaw3F+8cUXl9udffbZxRm28c0338RVV13lFHQajapdNGh1dXV8+eWX5b0P9sR+HwEHvtu9995b3qBq1qxZ5Y1QoDHYuHFjXHrppeW1sO3bt9/fuwMNZtu2beVZHo899lj06tUrhg0bFjfffHNMnTrV7zqNRnHvmuLMjkceeaS8Zvz555+P2bNnx5133rm/dw0OKPt9BLz4oatFixaxevXqHZYX8506dapzm2J5fdaHg/E4r/HAAw+UAf7qq6/Gaaed1sB7CnnH+QcffFDelKq4A+n2oVJo2bJleaOq4447zv8lHPR/nxd3Pj/kkEPK7WqcfPLJ5WhKcapvq1atGny/oaGP81tvvbX8ULW4KVWheEpRcZOrK6+8svzAqTiFHQ5mu2rQtm3b7vHod2G//0ko/tEpPg2eN2/eDj+AFfPF9VJ1KZZvv37hlVde2eX6cDAe54X77ruv/OR47ty50bt376S9hZzjvHiU5DvvvFOefl4zXXTRRbV3Fy3u/A+N4e/zAQMGlKed13zAVHjvvffKMBffNJbjvLhXzc6RXfOh0//f4woObvusQSsHgOnTp1dat25deeqppyr//ve/K1deeWXliCOOqFRVVZVfv/TSSyvjxo2rXf+f//xnpWXLlpUHHnigsnTp0srEiRMrhxxySOWdd97Zj+8C9u1xfu+991ZatWpVmTlzZuXTTz+tnTZu3Oi3mkZznO/MXdBpjMf5ypUry6dY/P73v68sX7688tJLL1U6dOhQueuuu/bju4B9e5wXP48Xx/lf//rXyooVKyp///vfK8cdd1z59CI4EG3cuLHy1ltvlVORxQ899FD53x9//HH59eL4Lo7zGsVxfdhhh1X+8Ic/lA06ZcqUSosWLSpz586t1+seEAFe+NOf/lQ55phjyuAoHnvwr3/9q/Zr5557bvlD2faeffbZygknnFCuX9wOfvbs2fthr6HhjvOf/OQn5V8GO0/FP3BwIKvv3+fbE+A01uN8wYIF5SNTi6ApHkl29913l4/gg8ZynH/99deV2267rYzuNm3aVLp27Vq5+uqrK//973/3097D7r322mt1/qxdc1wXvxbH+c7b9OzZs/wzUfxd/uc//7lSX82K/9m3g/MAAADAAXcNOAAAADQFAhwAAAASCHAAAABIIMABAAAggQAHAACABAIcAAAAEghwAAAASCDAAQAAIIEABwAAgAQCHAAAABIIcAAAAEggwAEAACAa3v8BzudE16EhTHoAAAAASUVORK5CYII=",
            "text/plain": [
              "<Figure size 1200x600 with 1 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "import os\n",
        "import math\n",
        "import pickle\n",
        "import warnings\n",
        "import time\n",
        "from typing import Optional, Dict, Any, Tuple, List\n",
        "warnings.filterwarnings(\"ignore\")\n",
        "import numpy as np\n",
        "import pandas as pd\n",
        "from sklearn.decomposition import TruncatedSVD\n",
        "from sklearn.calibration import CalibratedClassifierCV\n",
        "from sklearn.linear_model import ElasticNet, Ridge, LogisticRegression\n",
        "from sklearn.ensemble import RandomForestRegressor\n",
        "# Note: MLPRegressor and CatBoost imports removed globally\n",
        "from sklearn.model_selection import TimeSeriesSplit # Still needed for internal SHOS CV if applicable, but not for main loop\n",
        "from sklearn.metrics import (\n",
        "    mean_squared_error, mean_absolute_error, r2_score\n",
        ")\n",
        "from sklearn.preprocessing import StandardScaler\n",
        "import matplotlib.pyplot as plt\n",
        "import seaborn as sns\n",
        "\n",
        "# Optional model libraries (LightGBM, XGBoost)\n",
        "try:\n",
        "    import lightgbm as lgb\n",
        "    LGB_AVAILABLE = True\n",
        "    print(\"[INFO] LightGBM is available.\")\n",
        "except Exception:\n",
        "    LGB_AVAILABLE = False\n",
        "    print(\"[INFO] LightGBM is NOT available.\")\n",
        "\n",
        "try:\n",
        "    import xgboost as xgb\n",
        "    XGB_AVAILABLE = True\n",
        "    print(\"[INFO] XGBoost is available.\")\n",
        "except Exception:\n",
        "    XGB_AVAILABLE = False\n",
        "    print(\"[INFO] XGBoost is NOT available.\")\n",
        "\n",
        "# Optional SHAP library\n",
        "try:\n",
        "    import shap\n",
        "    SHAP_AVAILABLE = True\n",
        "    print(\"[INFO] SHAP is available.\")\n",
        "except ImportError:\n",
        "    SHAP_AVAILABLE = False\n",
        "    print(\"[INFO] SHAP is NOT available.\")\n",
        "\n",
        "# ----------------------------- Utilities -----------------------------\n",
        "def safe_mkdir(path: str):\n",
        "    os.makedirs(path, exist_ok=True)\n",
        "\n",
        "def rmse(y_true, y_pred):\n",
        "    return math.sqrt(mean_squared_error(y_true, y_pred))\n",
        "\n",
        "def wmape(y_true, y_pred):\n",
        "    denom = np.sum(np.abs(y_true))\n",
        "    return np.sum(np.abs(y_true - y_pred)) / denom * 100.0 if denom != 0 else np.nan\n",
        "\n",
        "def mape(y_true, y_pred):\n",
        "    mask = y_true != 0\n",
        "    return np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100.0 if mask.sum() > 0 else np.nan\n",
        "\n",
        "# ----------------------------- Padding with Inactive Filtering (Moved to Top) -----------------------------\n",
        "def pad_monthly_series_and_filter_inactive(df: pd.DataFrame) -> pd.DataFrame:\n",
        "    print(\"[INFO] Padding series to monthly grid and filtering inactive pairs...\")\n",
        "    df = df.copy()\n",
        "    df[\"MonthStart\"] = pd.to_datetime(df[\"MonthStart\"])\n",
        "    min_date = df[\"MonthStart\"].min()\n",
        "    max_date = df[\"MonthStart\"].max()\n",
        "    all_months = pd.date_range(start=min_date, end=max_date, freq=\"MS\")\n",
        "    print(f\"[INFO] Date range: {min_date.strftime('%Y-%m')} to {max_date.strftime('%Y-%m')}. Total months: {len(all_months)}\")\n",
        "    df_active = df.groupby([\"DealerCode\", \"DummyID\"]).filter(lambda g: g[\"PartQty\"].sum() > 0)\n",
        "    print(f\"[INFO] Filtered from {df.shape[0]} to {df_active.shape[0]} records after removing inactive pairs.\")\n",
        "    if df_active.empty:\n",
        "        print(\"[WARNING] No active series found after filtering.\")\n",
        "        return df_active\n",
        "\n",
        "    full_index = pd.MultiIndex.from_product(\n",
        "        [df_active[\"DealerCode\"].unique(), df_active[\"DummyID\"].unique(), all_months],\n",
        "        names=[\"DealerCode\", \"DummyID\", \"MonthStart\"]\n",
        "    )\n",
        "    df_indexed = df_active.set_index([\"DealerCode\", \"DummyID\", \"MonthStart\"])\n",
        "    df_padded = df_indexed.reindex(full_index, fill_value=0).reset_index()\n",
        "    df_padded[\"OTC_Flag\"] = (df_padded[\"OTC_Qty\"] > 0).astype(int)\n",
        "    for col in [\"PartQty\", \"OTC_Qty\", \"DealerPrice\", \"PRC_UNIT_WEIGHT\", \"PRC_UNIT_CUBE\"]:\n",
        "        df_padded[col] = pd.to_numeric(df_padded[col], errors=\"coerce\").fillna(0)\n",
        "    df_final = df_padded.groupby([\"DealerCode\", \"DummyID\"]).filter(lambda g: g[\"PartQty\"].sum() > 0).reset_index(drop=True)\n",
        "    print(f\"[INFO] Padding complete. Final shape: {df_final.shape}\")\n",
        "    return df_final\n",
        "\n",
        "# ----------------------------- Data helpers (Same as before) -----------------------------\n",
        "def aggregate_monthly(df: pd.DataFrame) -> pd.DataFrame:\n",
        "    print(\"[INFO] Aggregating data to monthly level...\")\n",
        "    df = df.copy()\n",
        "    colmap = {}\n",
        "    for c in df.columns:\n",
        "        lc = c.lower().strip()\n",
        "        if lc in (\"dealer code\", \"dealercode\", \"dealer_code\", \"dealercode\"):\n",
        "            colmap[c] = \"DealerCode\"\n",
        "        if lc in (\"dummy id\", \"dummyid\", \"partno\", \"part_no\", \"dummy_id\"):\n",
        "            colmap[c] = \"DummyID\"\n",
        "        if lc in (\"sale date\", \"saledate\", \"sale_date\"):\n",
        "            colmap[c] = \"SaleDate\"\n",
        "        if lc in (\"part qty.\", \"part qty\", \"partqty\", \"qty\", \"quantity\"):\n",
        "            colmap[c] = \"PartQty\"\n",
        "        if lc in (\"sale type\", \"saletype\", \"sale_type\"):\n",
        "            colmap[c] = \"SaleType\"\n",
        "        if lc in (\"dealer price\", \"dealerprice\", \"price\"):\n",
        "            colmap[c] = \"DealerPrice\"\n",
        "        if lc in (\"prc_unit_weight\", \"weight\", \"prc unit weight\"):\n",
        "            colmap[c] = \"PRC_UNIT_WEIGHT\"\n",
        "        if lc in (\"prc_unit_cube\", \"cube\", \"prc unit cube\"):\n",
        "            colmap[c] = \"PRC_UNIT_CUBE\"\n",
        "        if lc in (\"otc_qty\",\"otc qty\",\"otcqty\"):\n",
        "            colmap[c] = \"OTC_Qty\"\n",
        "    df = df.rename(columns=colmap)\n",
        "    if \"MonthStart\" in df.columns and \"PartQty\" in df.columns:\n",
        "        out = df.copy()\n",
        "        out[\"MonthStart\"] = pd.to_datetime(out[\"MonthStart\"])\n",
        "        if \"OTC_Qty\" not in out.columns:\n",
        "            if \"SaleType\" in out.columns:\n",
        "                out[\"OTC_Qty\"] = out[\"PartQty\"] * 0\n",
        "                mask = out[\"SaleType\"].astype(str).str.upper() == \"OTC\"\n",
        "                out.loc[mask, \"OTC_Qty\"] = out.loc[mask, \"PartQty\"]\n",
        "            else:\n",
        "                out[\"OTC_Qty\"] = 0\n",
        "        out[\"OTC_Flag\"] = (out[\"OTC_Qty\"] > 0).astype(int)\n",
        "        out[\"DealerPrice\"] = out.get(\"DealerPrice\", 0.0)\n",
        "        out[\"PRC_UNIT_WEIGHT\"] = out.get(\"PRC_UNIT_WEIGHT\", 0.0)\n",
        "        out[\"PRC_UNIT_CUBE\"] = out.get(\"PRC_UNIT_CUBE\", 0.0)\n",
        "        return out[[\"DealerCode\", \"DummyID\", \"MonthStart\", \"PartQty\", \"OTC_Qty\", \"OTC_Flag\", \"DealerPrice\", \"PRC_UNIT_WEIGHT\", \"PRC_UNIT_CUBE\"]]\n",
        "    if \"SaleDate\" not in df.columns:\n",
        "        raise ValueError(\"Input must have 'SaleDate' or 'MonthStart' + 'PartQty'.\")\n",
        "    df[\"SaleDate\"] = pd.to_datetime(df[\"SaleDate\"])\n",
        "    df[\"MonthStart\"] = df[\"SaleDate\"].dt.to_period(\"M\").dt.to_timestamp()\n",
        "    agg = df.groupby([\"DealerCode\", \"DummyID\", \"MonthStart\"], as_index=False).agg(\n",
        "        PartQty=(\"PartQty\", \"sum\"),\n",
        "        OTC_Qty=(\"PartQty\", lambda x: x[df.loc[x.index, \"SaleType\"].astype(str).str.upper() == \"OTC\"].sum() if \"SaleType\" in df.columns else 0),\n",
        "        DealerPrice=(\"DealerPrice\", \"mean\"),\n",
        "        PRC_UNIT_WEIGHT=(\"PRC_UNIT_WEIGHT\", \"mean\"),\n",
        "        PRC_UNIT_CUBE=(\"PRC_UNIT_CUBE\", \"mean\"),\n",
        "    )\n",
        "    agg[\"OTC_Qty\"] = agg[\"OTC_Qty\"].fillna(0).astype(int)\n",
        "    agg[\"OTC_Flag\"] = (agg[\"OTC_Qty\"] > 0).astype(int)\n",
        "    agg[[\"DealerPrice\", \"PRC_UNIT_WEIGHT\", \"PRC_UNIT_CUBE\"]] = agg[[\"DealerPrice\", \"PRC_UNIT_WEIGHT\", \"PRC_UNIT_CUBE\"]].fillna(0.0)\n",
        "    print(f\"[INFO] Aggregated to {len(agg)} monthly records.\")\n",
        "    return agg\n",
        "\n",
        "def add_time_series_features(df: pd.DataFrame, max_lag: int = 12) -> pd.DataFrame:\n",
        "    print(f\"[INFO] Adding time series features (max_lag={max_lag})...\")\n",
        "    df = df.sort_values([\"DealerCode\", \"DummyID\", \"MonthStart\"]).copy()\n",
        "    rows = []\n",
        "    grp = df.groupby([\"DealerCode\", \"DummyID\"])\n",
        "    for (d, p), g in grp:\n",
        "        g = g.sort_values(\"MonthStart\").copy()\n",
        "        for lag in range(1, max_lag+1):\n",
        "            g[f\"qty_lag_{lag}\"] = g[\"PartQty\"].shift(lag).fillna(0)\n",
        "            g[f\"otc_lag_{lag}\"] = g[\"OTC_Flag\"].shift(lag).fillna(0)\n",
        "        g[\"qty_sum_1_3\"] = g[\"PartQty\"].rolling(3, min_periods=1).sum().shift(1).fillna(0)\n",
        "        g[\"qty_sum_1_6\"] = g[\"PartQty\"].rolling(6, min_periods=1).sum().shift(1).fillna(0)\n",
        "        g[\"nonzero_count_6\"] = g[\"PartQty\"].rolling(6, min_periods=1).apply(lambda x: np.sum(x>0)).shift(1).fillna(0)\n",
        "        last_pos = -1\n",
        "        t_since = []\n",
        "        for idx, val in enumerate(g[\"PartQty\"].values):\n",
        "            if val > 0:\n",
        "                last_pos = idx\n",
        "                t_since.append(0)\n",
        "            else:\n",
        "                t_since.append(idx - last_pos if last_pos != -1 else g.shape[0]+1)\n",
        "        g[\"time_since_last_sale\"] = pd.Series(t_since).astype(float)\n",
        "        g[\"qty_ma_3\"] = g[\"PartQty\"].rolling(3, min_periods=1).mean().shift(1).fillna(0)\n",
        "        g[\"qty_ma_6\"] = g[\"PartQty\"].rolling(6, min_periods=1).mean().shift(1).fillna(0)\n",
        "        g[\"otc_share_slope_3\"] = g[\"OTC_Flag\"].rolling(3, min_periods=1).apply(lambda x: np.polyfit(np.arange(len(x)), x, 1)[0] if len(x)>1 else 0).shift(1).fillna(0)\n",
        "        rows.append(g)\n",
        "    out = pd.concat(rows, axis=0).reset_index(drop=True)\n",
        "    out.fillna(0, inplace=True)\n",
        "    print(f\"[INFO] Added {len([c for c in out.columns if c.startswith(('qty_lag_', 'otc_lag_'))])} lag features.\")\n",
        "    return out\n",
        "\n",
        "def croston_forecast(series: np.ndarray, alpha: float = 0.3) -> np.ndarray:\n",
        "    n = len(series)\n",
        "    if n == 0:\n",
        "        return np.zeros(0)\n",
        "    demand = np.asarray(series, dtype=float)\n",
        "    forecast = np.zeros(n)\n",
        "    nz_idx = np.where(demand > 0)[0]\n",
        "    if len(nz_idx) == 0:\n",
        "        return forecast\n",
        "    last = nz_idx[0]\n",
        "    z = demand[last]\n",
        "    p = 1.0\n",
        "    for t in range(last+1, n):\n",
        "        if demand[t] > 0:\n",
        "            interval = t - last\n",
        "            z = alpha * demand[t] + (1-alpha) * z\n",
        "            p = alpha * interval + (1-alpha) * p\n",
        "            last = t\n",
        "        forecast[t] = z / max(1e-9, p)\n",
        "    forecast[:last+1] = forecast[last+1] if last+1 < n else forecast[last]\n",
        "    return forecast\n",
        "\n",
        "def add_croston_features(df: pd.DataFrame) -> pd.DataFrame:\n",
        "    print(\"[INFO] Adding Croston features...\")\n",
        "    rows = []\n",
        "    for (d,p), g in df.groupby([\"DealerCode\",\"DummyID\"]):\n",
        "        g = g.sort_values(\"MonthStart\").copy()\n",
        "        g[\"croston_forecast\"] = croston_forecast(g[\"PartQty\"].values, alpha=0.3)\n",
        "        g[\"croston_ratio\"] = np.where(g[\"qty_sum_1_3\"]!=0, g[\"croston_forecast\"]/g[\"qty_sum_1_3\"], 0.0)\n",
        "        rows.append(g)\n",
        "    out = pd.concat(rows, axis=0).reset_index(drop=True)\n",
        "    out.fillna(0, inplace=True)\n",
        "    print(f\"[INFO] Added Croston features for {len(rows)} unique series.\")\n",
        "    return out\n",
        "\n",
        "# ----------------------------- SHOS with Train-Only Sparsity (Same as before) -----------------------------\n",
        "def compute_shos_for_series(y: np.ndarray, alpha_base: float = 0.2, beta_base: float = 0.2,\n",
        "                            adaptivity: bool = True, sparsity_k: float = 0.5,\n",
        "                            n_train: Optional[int] = None) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:\n",
        "    n = len(y)\n",
        "    if n == 0:\n",
        "        return np.zeros(0), np.zeros(0), np.zeros(0)\n",
        "\n",
        "    if n_train is not None and n_train > 0:\n",
        "        y_sparsity = y[:n_train]\n",
        "    else:\n",
        "        y_sparsity = y\n",
        "\n",
        "    nonzero_count = np.sum(y_sparsity > 0)\n",
        "    sparsity = 1.0 - (nonzero_count / max(1, len(y_sparsity)))\n",
        "\n",
        "    if adaptivity:\n",
        "        alpha = alpha_base * (1.0 - sparsity_k * sparsity)\n",
        "        beta = beta_base * (1.0 - sparsity_k * sparsity)\n",
        "        last_pos_all = np.where(y > 0)[0]\n",
        "        if len(last_pos_all) > 0 and (n - 1 - last_pos_all[-1]) <= 3:\n",
        "            alpha = min(0.9, alpha * 1.2)\n",
        "            beta = min(0.9, beta * 1.2)\n",
        "    else:\n",
        "        alpha, beta = alpha_base, beta_base\n",
        "\n",
        "    # --- IMPROVED INITIALIZATION (Point 6) ---\n",
        "    if n >= 3:\n",
        "        init_window = 3\n",
        "        y_init_window = y[:init_window]\n",
        "        q0 = float(np.sum(y_init_window > 0)) / max(1, init_window)\n",
        "        positive_y_init = y_init_window[y_init_window > 0]\n",
        "        if len(positive_y_init) > 0:\n",
        "            z0 = float(np.mean(positive_y_init))\n",
        "        else:\n",
        "            positive_y_all = y[y > 0]\n",
        "            if len(positive_y_all) > 0:\n",
        "                z0 = float(np.mean(positive_y_all))\n",
        "            else:\n",
        "                z0 = 1.0\n",
        "    else:\n",
        "        q0 = float(np.sum(y > 0)) / max(1, n)\n",
        "        positive_y_all = y[y > 0]\n",
        "        if len(positive_y_all) > 0:\n",
        "            z0 = float(np.mean(positive_y_all))\n",
        "        else:\n",
        "            z0 = 1.0\n",
        "    # --- END IMPROVED INITIALIZATION ---\n",
        "\n",
        "    q_prev, z_prev = q0, z0\n",
        "    q, z, f = np.zeros(n), np.zeros(n), np.zeros(n)\n",
        "\n",
        "    for t in range(n):\n",
        "        yt = y[t]\n",
        "        if yt > 0:\n",
        "            z_curr = alpha * yt + (1 - alpha) * z_prev\n",
        "            q_curr = beta * 1.0 + (1 - beta) * q_prev\n",
        "        else:\n",
        "            z_curr = z_prev\n",
        "            q_curr = beta * 0.0 + (1 - beta) * q_prev\n",
        "        q[t], z[t], f[t] = q_curr, z_curr, q_curr * z_curr\n",
        "        q_prev, z_prev = q_curr, z_curr\n",
        "    return q, z, f\n",
        "\n",
        "def add_shos_features_with_ntrain(df: pd.DataFrame, n_train_per_series: Dict[Tuple, int],\n",
        "                                  alpha_base: float = 0.2, beta_base: float = 0.2,\n",
        "                                  adaptivity: bool = True, sparsity_k: float = 0.5) -> pd.DataFrame:\n",
        "    print(f\"[INFO] Adding SHOS features with train-only sparsity (k={sparsity_k})...\")\n",
        "    rows = []\n",
        "    for (d, p), g in df.groupby([\"DealerCode\", \"DummyID\"]):\n",
        "        g = g.sort_values(\"MonthStart\").copy()\n",
        "        y = g[\"PartQty\"].values.astype(float)\n",
        "        n_train = n_train_per_series.get((d, p), len(y))\n",
        "        q, z, f = compute_shos_for_series(y, alpha_base, beta_base, adaptivity, sparsity_k, n_train)\n",
        "        g[\"q_shos\"] = q\n",
        "        g[\"z_shos\"] = z\n",
        "        g[\"shos_forecast\"] = f\n",
        "        rows.append(g)\n",
        "    out = pd.concat(rows, axis=0).reset_index(drop=True).fillna(0)\n",
        "    print(f\"[INFO] Added SHOS features for {len(rows)} unique series.\")\n",
        "    return out\n",
        "\n",
        "# ----------------------------- Embeddings (Same as before) -----------------------------\n",
        "def build_low_rank_embeddings(df: pd.DataFrame, n_components: int = 16):\n",
        "    print(f\"[INFO] Building low-rank embeddings (n_components={n_components})...\")\n",
        "    occ = df.groupby([\"DealerCode\", \"DummyID\"]).agg(months_active=(\"PartQty\", lambda x: (x>0).sum())).reset_index()\n",
        "    pivot = occ.pivot(index=\"DealerCode\", columns=\"DummyID\", values=\"months_active\").fillna(0)\n",
        "    if pivot.shape[0] < 2 or pivot.shape[1] < 2:\n",
        "        print(\"[INFO] Pivot too small for SVD, creating empty embeddings.\")\n",
        "        dealer_embed = pd.DataFrame({\"DealerCode\": sorted(df[\"DealerCode\"].unique())})\n",
        "        part_embed = pd.DataFrame({\"DummyID\": sorted(df[\"DummyID\"].unique())})\n",
        "        return dealer_embed, part_embed, []\n",
        "    k = min(n_components, min(pivot.shape)-1)\n",
        "    svd = TruncatedSVD(n_components=k, random_state=0)\n",
        "    U = svd.fit_transform(pivot)\n",
        "    V = svd.components_.T\n",
        "    dealer_index = list(pivot.index)\n",
        "    part_index = list(pivot.columns)\n",
        "    dealer_embed = pd.DataFrame(U, index=dealer_index).reset_index().rename(columns={\"index\":\"DealerCode\"})\n",
        "    part_embed = pd.DataFrame(V, index=part_index).reset_index().rename(columns={\"index\":\"DummyID\"})\n",
        "    emb_cols = []\n",
        "    for i in range(U.shape[1]):\n",
        "        dealer_embed.rename(columns={i: f\"emb_dealer_{i}\"}, inplace=True)\n",
        "        emb_cols.append(f\"emb_dealer_{i}\")\n",
        "    for i in range(V.shape[1]):\n",
        "        part_embed.rename(columns={i: f\"emb_part_{i}\"}, inplace=True)\n",
        "        emb_cols.append(f\"emb_part_{i}\")\n",
        "    print(f\"[INFO] Created {len(emb_cols)} embedding features for {U.shape[0]} dealers and {V.shape[0]} parts.\")\n",
        "    return dealer_embed, part_embed, emb_cols\n",
        "\n",
        "# ----------------------------- Feature Sets (NO CROSTON in ML Baselines, YES in Ablation) -----------------------------\n",
        "def build_base_features(df: pd.DataFrame, max_lag: int = 6):\n",
        "    print(\"[INFO] Building base feature matrix...\")\n",
        "    df2 = add_time_series_features(df, max_lag=max_lag)\n",
        "    # Croston features are NOT added here for fairness in ML baselines\n",
        "    lag_cols = [c for c in df2.columns if c.startswith((\"qty_lag_\", \"otc_lag_\"))]\n",
        "    base_cols = [\n",
        "        \"DealerPrice\", \"PRC_UNIT_WEIGHT\", \"PRC_UNIT_CUBE\",\n",
        "        \"qty_sum_1_3\", \"qty_sum_1_6\", \"nonzero_count_6\", \"time_since_last_sale\",\n",
        "        \"qty_ma_3\", \"qty_ma_6\", \"otc_share_slope_3\"\n",
        "    ] + lag_cols\n",
        "    dealer_stats = df2.groupby(\"DealerCode\").agg(dealer_qty_mean=(\"PartQty\", \"mean\")).reset_index()\n",
        "    part_stats = df2.groupby(\"DummyID\").agg(part_qty_mean=(\"PartQty\", \"mean\")).reset_index()\n",
        "    df2 = df2.merge(dealer_stats, on=\"DealerCode\", how=\"left\")\n",
        "    df2 = df2.merge(part_stats, on=\"DummyID\", how=\"left\")\n",
        "    extra_cols = [\"dealer_qty_mean\", \"part_qty_mean\"]\n",
        "    base_cols += extra_cols\n",
        "    print(f\"[INFO] Base features include {len(base_cols)} columns.\")\n",
        "    return df2, base_cols\n",
        "\n",
        "def build_fold_features(train_df: pd.DataFrame, test_df: pd.DataFrame, max_lag: int = 6, svd_components: int = 12, sparsity_k: float = 0.5):\n",
        "    print(f\"[INFO] Building fold features with sparsity_k={sparsity_k}...\")\n",
        "    train_base, base_cols = build_base_features(train_df, max_lag)\n",
        "    test_base, _ = build_base_features(test_df, max_lag)\n",
        "    # Add Croston features to both train and test folds BEFORE SHOS\n",
        "    train_croston = add_croston_features(train_base)\n",
        "    test_croston = add_croston_features(test_base)\n",
        "    # Pass the Croston-augmented base to SHOS calculation\n",
        "    n_train_map = train_df.groupby([\"DealerCode\", \"DummyID\"]).size().to_dict()\n",
        "    train_shos = add_shos_features_with_ntrain(train_croston, n_train_map, sparsity_k=sparsity_k) # Pass train_croston\n",
        "    test_shos = add_shos_features_with_ntrain(test_croston, n_train_map, sparsity_k=sparsity_k) # Pass test_croston\n",
        "    # Calculate embeddings based on the SHOS-augmented training data\n",
        "    dealer_embed, part_embed, emb_cols = build_low_rank_embeddings(train_shos, svd_components)\n",
        "    if emb_cols:\n",
        "        train_shos = train_shos.merge(dealer_embed, on=\"DealerCode\", how=\"left\")\n",
        "        train_shos = train_shos.merge(part_embed, on=\"DummyID\", how=\"left\")\n",
        "        test_shos = test_shos.merge(dealer_embed, on=\"DealerCode\", how=\"left\")\n",
        "        test_shos = test_shos.merge(part_embed, on=\"DummyID\", how=\"left\")\n",
        "        for c in emb_cols:\n",
        "            train_shos[c] = train_shos[c].fillna(0)\n",
        "            test_shos[c] = test_shos[c].fillna(0)\n",
        "    # Define feature lists\n",
        "    full_cols = base_cols + [\"q_shos\", \"z_shos\"] + emb_cols\n",
        "    print(f\"[INFO] Fold features: {len(base_cols)} base, {len(emb_cols)} embeddings, {len(full_cols)} total.\")\n",
        "    return train_shos, test_shos, base_cols, full_cols\n",
        "\n",
        "# ----------------------------- Intermittency Analysis (Same as before) -----------------------------\n",
        "def compute_intermittency(df_processed):\n",
        "    print(\"[INFO] Computing intermittency metrics (ADI, CV²)...\")\n",
        "    stats = []\n",
        "    for (d, p), g in df_processed.groupby([\"DealerCode\", \"DummyID\"]):\n",
        "        y = g[\"PartQty\"].values\n",
        "        if np.sum(y > 0) == 0:\n",
        "            continue\n",
        "        intervals = np.diff(np.where(y > 0)[0])\n",
        "        adi = np.mean(intervals) if len(intervals) > 0 else len(y)\n",
        "        mean_y = np.mean(y)\n",
        "        cv2 = (np.std(y) / mean_y) ** 2 if mean_y > 0 else np.inf\n",
        "        stats.append({\"ADI\": adi, \"CV2\": cv2})\n",
        "    stats_df = pd.DataFrame(stats)\n",
        "    print(f\"[INFO] Computed metrics for {len(stats_df)} active series. Median ADI: {stats_df['ADI'].median():.2f}, Median CV²: {stats_df['CV2'].median():.2f}\")\n",
        "    return stats_df\n",
        "\n",
        "# ----------------------------- Train Model with Pre-tuned Parameters (Test Run) -----------------------------\n",
        "def train_with_params(model_name: str, X_train, y_train, params, random_state=0, objective=None):\n",
        "    \"\"\"Train a model using pre-defined parameters.\"\"\"\n",
        "    print(f\"[INFO] Training {model_name} with pre-tuned parameters...\")\n",
        "    start_time = time.time()\n",
        "    if model_name == \"LightGBM\" and LGB_AVAILABLE:\n",
        "        if objective:\n",
        "            estimator = lgb.LGBMRegressor(random_state=random_state, objective=objective, **params)\n",
        "        else:\n",
        "            estimator = lgb.LGBMRegressor(random_state=random_state, **params)\n",
        "    elif model_name == \"XGBoost\" and XGB_AVAILABLE:\n",
        "        estimator = xgb.XGBRegressor(random_state=random_state, **params)\n",
        "    elif model_name == \"ElasticNet\":\n",
        "        estimator = ElasticNet(max_iter=2000, random_state=random_state, **params)\n",
        "    elif model_name == \"Ridge\":\n",
        "        estimator = Ridge(random_state=random_state, **params)\n",
        "    elif model_name == \"RandomForest\":\n",
        "        estimator = RandomForestRegressor(random_state=random_state, **params)\n",
        "    else:\n",
        "        raise ValueError(f\"Unknown model: {model_name} or library not available.\")\n",
        "    estimator.fit(X_train, y_train)\n",
        "    print(f\"[INFO] Training {model_name} took {time.time() - start_time:.2f}s.\")\n",
        "    return estimator\n",
        "\n",
        "def train_hurdle_model_optimized(X_train_occ, y_train_occ_binary, X_train_qty_pos, y_train_qty_pos, params_occ, params_qty, random_state=0):\n",
        "    \"\"\"Train a two-stage (hurdle) model using pre-defined parameters.\"\"\"\n",
        "    print(\"[INFO] Training Hurdle Model (Occurrence + Quantity)...\")\n",
        "    start_time = time.time()\n",
        "    \n",
        "    # Stage 1: Occurrence Classifier (binary)\n",
        "    clf_occ = lgb.LGBMClassifier(random_state=random_state, **params_occ)\n",
        "    clf_occ.fit(X_train_occ, y_train_occ_binary)\n",
        "    \n",
        "    # Stage 2: Quantity Regressor (only on positive examples, using Tweedie)\n",
        "    reg_qty = None\n",
        "    if len(y_train_qty_pos) > 0:\n",
        "        reg_qty = lgb.LGBMRegressor(random_state=random_state, objective='tweedie', **params_qty)\n",
        "        reg_qty.fit(X_train_qty_pos, y_train_qty_pos)\n",
        "    else:\n",
        "        print(\"[WARNING] No positive examples for quantity regression in hurdle model.\")\n",
        "    \n",
        "    print(f\"[INFO] Training Hurdle Model took {time.time() - start_time:.2f}s.\")\n",
        "    return clf_occ, reg_qty\n",
        "\n",
        "# ----------------------------- Build Feature Matrix Once (Optimized for CV) -----------------------------\n",
        "def build_feature_matrix_optimized(df: pd.DataFrame, max_lag=6, svd_components=12, shos_k=0.5):\n",
        "    print(\"[INFO] Building feature matrix ONCE for all folds...\")\n",
        "    df2 = add_time_series_features(df, max_lag=max_lag)\n",
        "    df2 = add_croston_features(df2)\n",
        "    # Calculate n_train_map for the *entire* dataset (will be used later if needed per fold, but we calculate SHOS once here)\n",
        "    # For this optimized version, we calculate SHOS once on the full series, using the provided k.\n",
        "    # This means the sparsity used for alpha/beta is based on the *full* series length for each (d, p).\n",
        "    # This is a trade-off for speed. For strict train-only sparsity per fold, calculation must happen inside the loop.\n",
        "    # Using full-series sparsity is often a reasonable approximation.\n",
        "    n_full_map = df2.groupby([\"DealerCode\", \"DummyID\"]).size().to_dict()\n",
        "    df2 = add_shos_features_with_ntrain(df2, n_full_map, sparsity_k=shos_k) # Calculate SHOS once using full series length\n",
        "    df2.fillna(0, inplace=True)\n",
        "\n",
        "    # Define baseline features (excluding Croston/SHOS for fairness in base ML models)\n",
        "    lag_cols = [c for c in df2.columns if c.startswith((\"qty_lag_\", \"otc_lag_\"))]\n",
        "    base_cols = [\n",
        "        \"DealerPrice\", \"PRC_UNIT_WEIGHT\", \"PRC_UNIT_CUBE\",\n",
        "        \"qty_sum_1_3\", \"qty_sum_1_6\", \"nonzero_count_6\", \"time_since_last_sale\",\n",
        "        \"qty_ma_3\", \"qty_ma_6\", \"otc_share_slope_3\"\n",
        "    ] + lag_cols\n",
        "    dealer_stats = df2.groupby(\"DealerCode\").agg(dealer_qty_mean=(\"PartQty\", \"mean\")).reset_index()\n",
        "    part_stats = df2.groupby(\"DummyID\").agg(part_qty_mean=(\"PartQty\", \"mean\")).reset_index()\n",
        "    df2 = df2.merge(dealer_stats, on=\"DealerCode\", how=\"left\")\n",
        "    df2 = df2.merge(part_stats, on=\"DummyID\", how=\"left\")\n",
        "    extra_cols = [\"dealer_qty_mean\", \"part_qty_mean\"]\n",
        "    base_cols += extra_cols\n",
        "\n",
        "    # Calculate embeddings based on the full processed dataset (including SHOS features for context)\n",
        "    dealer_embed, part_embed, emb_cols = build_low_rank_embeddings(df2, n_components=svd_components)\n",
        "    if emb_cols:\n",
        "        df2 = df2.merge(dealer_embed, on=\"DealerCode\", how=\"left\")\n",
        "        df2 = df2.merge(part_embed, on=\"DummyID\", how=\"left\")\n",
        "        for c in emb_cols:\n",
        "            df2[c] = df2[c].fillna(0)\n",
        "    \n",
        "    full_cols = base_cols + [\"q_shos\", \"z_shos\"] + emb_cols\n",
        "    print(f\"[INFO] Final feature matrix has {len(full_cols)} columns and shape {df2.shape}.\")\n",
        "    return df2, base_cols, full_cols\n",
        "\n",
        "# ----------------------------- Cross-validation with Optimized Features (LightGBM, +SHOS, Hurdle Baseline, Hurdle +SHOS) -----------------------------\n",
        "def run_cross_validation_optimized(df_processed: pd.DataFrame, base_cols: List[str], full_cols: List[str], tuned_params: Dict[str, Dict], train_window=12, test_window=3, shos_k=0.1, shos_w=0.7):\n",
        "    print(f\"[INFO] Starting optimized cross-validation with {train_window}-month train, {test_window}-month test windows...\")\n",
        "    print(f\"[INFO] Using SHOS k={shos_k}, w={shos_w}\")\n",
        "    df_processed = df_processed.sort_values([\"MonthStart\", \"DealerCode\", \"DummyID\"]).reset_index(drop=True)\n",
        "    all_months = sorted(df_processed[\"MonthStart\"].unique())\n",
        "    \n",
        "    # Define models to evaluate (removed MLP, CatBoost)\n",
        "    model_names = [\"LightGBM\", \"XGBoost\", \"ElasticNet\", \"Ridge\", \"RandomForest\"] # Removed MLP, CatBoost\n",
        "    ablation_names = [\"Baseline\", \"+SHOS\", \"Hurdle_Baseline\", \"Hurdle_SHOS\"] # Include Hurdle models\n",
        "    all_names = model_names + ablation_names + [\"Croston\", \"TSB\"]\n",
        "    metrics = {name: {\"MAE\": [], \"RMSE\": [], \"WMAPE\": [], \"MAPE\": [], \"R2\": []} for name in all_names}\n",
        "    fold_train_metrics = {name: {\"MAE\": [], \"RMSE\": [], \"WMAPE\": [], \"MAPE\": [], \"R2\": []} for name in all_names} # NEW: Store train metrics\n",
        "    fold1_actual, fold1_preds = None, {}\n",
        "\n",
        "    total_folds = len(all_months) - train_window - test_window + 1\n",
        "    print(f\"[INFO] Total possible folds: {total_folds}\")\n",
        "\n",
        "    for i in range(total_folds):\n",
        "        train_months = all_months[i:i + train_window]\n",
        "        test_months = all_months[i + train_window:i + train_window + test_window]\n",
        "        # --- OPTIMIZATION: Use pre-computed features and slice them ---\n",
        "        train_df_fold = df_processed[df_processed[\"MonthStart\"].isin(train_months)].copy()\n",
        "        test_df_fold = df_processed[df_processed[\"MonthStart\"].isin(test_months)].copy()\n",
        "        if train_df_fold.empty or test_df_fold.empty:\n",
        "            print(f\"[INFO] Skipping fold {i+1} due to empty train/test set.\")\n",
        "            continue\n",
        "\n",
        "        print(f\"[INFO] Processing Fold {i+1}: Train {train_months[0].strftime('%Y-%m')} to {train_months[-1].strftime('%Y-%m')}, Test {test_months[0].strftime('%Y-%m')} to {test_months[-1].strftime('%Y-%m')}\")\n",
        "\n",
        "        # Slice pre-computed features\n",
        "        train_f = df_processed[df_processed[\"MonthStart\"].isin(train_months)].copy()\n",
        "        test_f = df_processed[df_processed[\"MonthStart\"].isin(test_months)].copy()\n",
        "        # Ensure features are aligned\n",
        "        X_train_base = train_f[base_cols].fillna(0).values\n",
        "        X_test_base = test_f[base_cols].fillna(0).values\n",
        "        X_train_full = train_f[full_cols].fillna(0).values\n",
        "        X_test_full = test_f[full_cols].fillna(0).values\n",
        "        y_train_actual = train_f[\"PartQty\"].values\n",
        "        y_test = test_f[\"PartQty\"].values\n",
        "\n",
        "        # --- MODEL TRAINING AND PREDICTION ---\n",
        "        model_predictions = {}\n",
        "        model_test_scores = {}\n",
        "        model_train_scores = {} # NEW: Store train scores for this fold\n",
        "\n",
        "        # Baselines: Croston & TSB (predictions are based on training logic, so calculated per fold)\n",
        "        def compute_croston_on_train_for_test(train_series, test_len):\n",
        "            full_series = np.concatenate([train_series, np.zeros(test_len)])\n",
        "            forecast = croston_forecast(full_series, alpha=0.3)\n",
        "            return forecast[-test_len:]\n",
        "\n",
        "        croston_preds_list = []\n",
        "        for (d, p), test_g in test_df_fold.groupby([\"DealerCode\", \"DummyID\"]):\n",
        "            train_g = train_df_fold[(train_df_fold[\"DealerCode\"]==d) & (train_df_fold[\"DummyID\"]==p)]\n",
        "            train_y = train_g[\"PartQty\"].values if not train_g.empty else np.array([])\n",
        "            test_len = len(test_g)\n",
        "            if len(train_y) > 0:\n",
        "                croston_for_test = compute_croston_on_train_for_test(train_y, test_len)\n",
        "            else:\n",
        "                croston_for_test = np.zeros(test_len)\n",
        "            croston_preds_list.append(croston_for_test)\n",
        "        croston_pred = np.concatenate(croston_preds_list)\n",
        "\n",
        "        tsb_pred = test_f[\"shos_forecast\"].values # This is the pre-computed SHOS forecast for the test period\n",
        "\n",
        "        model_predictions[\"Croston\"] = croston_pred\n",
        "        model_predictions[\"TSB\"] = tsb_pred\n",
        "\n",
        "        # Calculate TEST scores for baselines\n",
        "        for name in [\"Croston\", \"TSB\"]:\n",
        "            y_pred = model_predictions[name]\n",
        "            try:\n",
        "                test_mae = mean_absolute_error(y_test, y_pred)\n",
        "                test_rmse = rmse(y_test, y_pred)\n",
        "                test_wmape = wmape(y_test, y_pred)\n",
        "                test_mape = mape(y_test, y_pred)\n",
        "                test_r2 = r2_score(y_test, y_pred)\n",
        "                \n",
        "                model_test_scores[name] = {\n",
        "                    \"MAE\": test_mae,\n",
        "                    \"RMSE\": test_rmse,\n",
        "                    \"WMAPE\": test_wmape,\n",
        "                    \"MAPE\": test_mape,\n",
        "                    \"R2\": test_r2\n",
        "                }\n",
        "                # Baselines don't have a meaningful train prediction in this context for *these specific metrics*\n",
        "                # So we assign NaN for train metrics for Croston/TSB.\n",
        "                model_train_scores[name] = {k: np.nan for k in [\"MAE\", \"RMSE\", \"WMAPE\", \"MAPE\", \"R2\"]}\n",
        "            except Exception as e:\n",
        "                print(f\"[WARNING] Error calculating test metrics for {name}: {e}\")\n",
        "                model_test_scores[name] = {k: np.nan for k in [\"MAE\", \"RMSE\", \"WMAPE\", \"MAPE\", \"R2\"]}\n",
        "                model_train_scores[name] = {k: np.nan for k in [\"MAE\", \"RMSE\", \"WMAPE\", \"MAPE\", \"R2\"]}\n",
        "\n",
        "\n",
        "        # --- ML MODELS (using pre-tuned parameters on raw target) ---\n",
        "        for name in model_names:\n",
        "            params = tuned_params.get(name)\n",
        "            if params is not None:\n",
        "                try:\n",
        "                    model = train_with_params(name, X_train_base, y_train_actual, params, objective='tweedie' if name in [\"LightGBM\", \"XGBoost\"] else None) # Apply Tweedie if specified in params or logic\n",
        "                    y_pred_test = model.predict(X_test_base)\n",
        "                    y_pred_train = model.predict(X_train_base) # NEW: Predict on train\n",
        "\n",
        "                    # Calculate TEST Metrics\n",
        "                    test_mae = mean_absolute_error(y_test, y_pred_test)\n",
        "                    test_rmse = rmse(y_test, y_pred_test)\n",
        "                    test_wmape = wmape(y_test, y_pred_test)\n",
        "                    test_mape = mape(y_test, y_pred_test)\n",
        "                    test_r2 = r2_score(y_test, y_pred_test)\n",
        "                    \n",
        "                    # Calculate TRAIN Metrics\n",
        "                    train_mae = mean_absolute_error(y_train_actual, y_pred_train)\n",
        "                    train_rmse = rmse(y_train_actual, y_pred_train)\n",
        "                    train_wmape = wmape(y_train_actual, y_pred_train)\n",
        "                    train_mape = mape(y_train_actual, y_pred_train)\n",
        "                    train_r2 = r2_score(y_train_actual, y_pred_train)\n",
        "\n",
        "                    model_predictions[name] = y_pred_test\n",
        "                    model_test_scores[name] = {\n",
        "                        \"MAE\": test_mae,\n",
        "                        \"RMSE\": test_rmse,\n",
        "                        \"WMAPE\": test_wmape,\n",
        "                        \"MAPE\": test_mape,\n",
        "                        \"R2\": test_r2\n",
        "                    }\n",
        "                    model_train_scores[name] = { # Store train scores\n",
        "                        \"MAE\": train_mae,\n",
        "                        \"RMSE\": train_rmse,\n",
        "                        \"WMAPE\": train_wmape,\n",
        "                        \"MAPE\": train_mape,\n",
        "                        \"R2\": train_r2\n",
        "                    }\n",
        "                except Exception as e:\n",
        "                    print(f\"[WARNING] Error training/predicting {name}: {e}\")\n",
        "                    model_predictions[name] = np.zeros_like(y_test)\n",
        "                    for metric_name in [\"MAE\", \"RMSE\", \"WMAPE\", \"MAPE\", \"R2\"]:\n",
        "                        model_test_scores[name][metric_name] = np.nan\n",
        "                        model_train_scores[name][metric_name] = np.nan # Store NaN for train too\n",
        "            else:\n",
        "                print(f\"[WARNING] No tuned params for {name}, using zeros.\")\n",
        "                model_predictions[name] = np.zeros_like(y_test)\n",
        "                for metric_name in [\"MAE\", \"RMSE\", \"WMAPE\", \"MAPE\", \"R2\"]:\n",
        "                    model_test_scores[name][metric_name] = np.nan\n",
        "                    model_train_scores[name][metric_name] = np.nan # Store NaN for train too\n",
        "\n",
        "        # --- ABLATION MODELS (using pre-tuned parameters on their specific targets) ---\n",
        "\n",
        "        # A. Baseline (LightGBM on raw target, base features) - WITH TWEEDIE\n",
        "        baseline_params = tuned_params.get(\"LightGBM\") # Use LightGBM params\n",
        "        if baseline_params:\n",
        "            try:\n",
        "                reg_base = train_with_params(\"LightGBM\", X_train_base, y_train_actual, baseline_params) # Use Tweedie\n",
        "                y_pred_A_test = reg_base.predict(X_test_base)\n",
        "                y_pred_A_train = reg_base.predict(X_train_base) # NEW: Predict on train\n",
        "                \n",
        "                model_predictions[\"Baseline\"] = y_pred_A_test\n",
        "                \n",
        "                # Calculate TEST Metrics for Baseline\n",
        "                test_mae_A = mean_absolute_error(y_test, y_pred_A_test)\n",
        "                test_rmse_A = rmse(y_test, y_pred_A_test)\n",
        "                test_wmape_A = wmape(y_test, y_pred_A_test)\n",
        "                test_mape_A = mape(y_test, y_pred_A_test)\n",
        "                test_r2_A = r2_score(y_test, y_pred_A_test)\n",
        "                \n",
        "                # Calculate TRAIN Metrics for Baseline\n",
        "                train_mae_A = mean_absolute_error(y_train_actual, y_pred_A_train)\n",
        "                train_rmse_A = rmse(y_train_actual, y_pred_A_train)\n",
        "                train_wmape_A = wmape(y_train_actual, y_pred_A_train)\n",
        "                train_mape_A = mape(y_train_actual, y_pred_A_train)\n",
        "                train_r2_A = r2_score(y_train_actual, y_pred_A_train)\n",
        "\n",
        "                model_test_scores[\"Baseline\"] = {\n",
        "                    \"MAE\": test_mae_A,\n",
        "                    \"RMSE\": test_rmse_A,\n",
        "                    \"WMAPE\": test_wmape_A,\n",
        "                    \"MAPE\": test_mape_A,\n",
        "                    \"R2\": test_r2_A\n",
        "                }\n",
        "                model_train_scores[\"Baseline\"] = { # Store train scores\n",
        "                    \"MAE\": train_mae_A,\n",
        "                    \"RMSE\": train_rmse_A,\n",
        "                    \"WMAPE\": train_wmape_A,\n",
        "                    \"MAPE\": train_mape_A,\n",
        "                    \"R2\": train_r2_A\n",
        "                }\n",
        "            except Exception as e:\n",
        "                print(f\"[WARNING] Error in Baseline model (A): {e}\")\n",
        "                model_predictions[\"Baseline\"] = np.zeros_like(y_test)\n",
        "                for metric_name in [\"MAE\", \"RMSE\", \"WMAPE\", \"MAPE\", \"R2\"]:\n",
        "                    model_test_scores[\"Baseline\"][metric_name] = np.nan\n",
        "                    model_train_scores[\"Baseline\"][metric_name] = np.nan # Store NaN for train too\n",
        "        else:\n",
        "            model_predictions[\"Baseline\"] = np.zeros_like(y_test)\n",
        "            for metric_name in [\"MAE\", \"RMSE\", \"WMAPE\", \"MAPE\", \"R2\"]:\n",
        "                model_test_scores[\"Baseline\"][metric_name] = np.nan\n",
        "                model_train_scores[\"Baseline\"][metric_name] = np.nan # Store NaN for train too\n",
        "\n",
        "        # B. +SHOS (LightGBM on raw target, full features) - WITH TWEEDIE\n",
        "        shos_params = tuned_params.get(\"+SHOS\")\n",
        "        if shos_params:\n",
        "            try:\n",
        "                reg_shos = train_with_params(\"LightGBM\", X_train_full, y_train_actual, shos_params, objective='tweedie') # Use Tweedie\n",
        "                y_pred_B_test = reg_shos.predict(X_test_full)\n",
        "                y_pred_B_train = reg_shos.predict(X_train_full) # NEW: Predict on train\n",
        "                \n",
        "                model_predictions[\"+SHOS\"] = y_pred_B_test\n",
        "                \n",
        "                # Calculate TEST Metrics for +SHOS\n",
        "                test_mae_B = mean_absolute_error(y_test, y_pred_B_test)\n",
        "                test_rmse_B = rmse(y_test, y_pred_B_test)\n",
        "                test_wmape_B = wmape(y_test, y_pred_B_test)\n",
        "                test_mape_B = mape(y_test, y_pred_B_test)\n",
        "                test_r2_B = r2_score(y_test, y_pred_B_test)\n",
        "\n",
        "                # Calculate TRAIN Metrics for +SHOS\n",
        "                train_mae_B = mean_absolute_error(y_train_actual, y_pred_B_train)\n",
        "                train_rmse_B = rmse(y_train_actual, y_pred_B_train)\n",
        "                train_wmape_B = wmape(y_train_actual, y_pred_B_train)\n",
        "                train_mape_B = mape(y_train_actual, y_pred_B_train)\n",
        "                train_r2_B = r2_score(y_train_actual, y_pred_B_train)\n",
        "\n",
        "                model_test_scores[\"+SHOS\"] = {\n",
        "                    \"MAE\": test_mae_B,\n",
        "                    \"RMSE\": test_rmse_B,\n",
        "                    \"WMAPE\": test_wmape_B,\n",
        "                    \"MAPE\": test_mape_B,\n",
        "                    \"R2\": test_r2_B\n",
        "                }\n",
        "                model_train_scores[\"+SHOS\"] = { # Store train scores\n",
        "                    \"MAE\": train_mae_B,\n",
        "                    \"RMSE\": train_rmse_B,\n",
        "                    \"WMAPE\": train_wmape_B,\n",
        "                    \"MAPE\": train_mape_B,\n",
        "                    \"R2\": train_r2_B\n",
        "                }\n",
        "            except Exception as e:\n",
        "                print(f\"[WARNING] Error in +SHOS model (B): {e}\")\n",
        "                model_predictions[\"+SHOS\"] = np.zeros_like(y_test)\n",
        "                for metric_name in [\"MAE\", \"RMSE\", \"WMAPE\", \"MAPE\", \"R2\"]:\n",
        "                    model_test_scores[\"+SHOS\"][metric_name] = np.nan\n",
        "                    model_train_scores[\"+SHOS\"][metric_name] = np.nan # Store NaN for train too\n",
        "        else:\n",
        "            model_predictions[\"+SHOS\"] = np.zeros_like(y_test)\n",
        "            for metric_name in [\"MAE\", \"RMSE\", \"WMAPE\", \"MAPE\", \"R2\"]:\n",
        "                model_test_scores[\"+SHOS\"][metric_name] = np.nan\n",
        "                model_train_scores[\"+SHOS\"][metric_name] = np.nan # Store NaN for train too\n",
        "\n",
        "        # C. Hurdle Baseline (Classifier on occurrence, Regressor on quantity using base features) - WITH TWEEDIE\n",
        "        hurdle_base_occ_params = tuned_params.get(\"Hurdle_Baseline_Occurrence\", tuned_params.get(\"LightGBM\", {})) # Fallback to LightGBM params\n",
        "        hurdle_base_qty_params = tuned_params.get(\"Hurdle_Baseline_Quantity\", tuned_params.get(\"LightGBM\", {})) # Fallback to LightGBM params\n",
        "        try:\n",
        "            # Prepare data for Hurdle training\n",
        "            y_train_occ_binary = (y_train_actual > 0).astype(int)\n",
        "            pos_mask_qty_base = y_train_actual > 0\n",
        "            if pos_mask_qty_base.sum() > 0:\n",
        "                X_train_qty_pos_base = X_train_base[pos_mask_qty_base]\n",
        "                y_train_qty_pos_base = y_train_actual[pos_mask_qty_base]\n",
        "            else:\n",
        "                print(f\"[WARNING] No positive examples for quantity regression in Hurdle Baseline for fold {i+1}.\")\n",
        "                X_train_qty_pos_base, y_train_qty_pos_base = np.array([]).reshape(0, X_train_base.shape[1]), np.array([])\n",
        "            \n",
        "            clf_occ_base, reg_qty_base = train_hurdle_model_optimized(\n",
        "                X_train_base, y_train_occ_binary,\n",
        "                X_train_qty_pos_base, y_train_qty_pos_base,\n",
        "                hurdle_base_occ_params, hurdle_base_qty_params\n",
        "            )\n",
        "            \n",
        "            # Predict probabilities and quantities\n",
        "            prob_occ_test = clf_occ_base.predict_proba(X_test_base)[:, 1]\n",
        "            if reg_qty_base is not None:\n",
        "                pred_qty_given_occ_test = reg_qty_base.predict(X_test_base)\n",
        "                pred_qty_given_occ_test = np.clip(pred_qty_given_occ_test, 0, None) # Ensure non-negative\n",
        "            else:\n",
        "                pred_qty_given_occ_test = np.zeros_like(prob_occ_test) # Fallback if regressor failed\n",
        "            \n",
        "            y_pred_C_test = prob_occ_test * pred_qty_given_occ_test\n",
        "            \n",
        "            # Predict for TRAIN metrics\n",
        "            prob_occ_train = clf_occ_base.predict_proba(X_train_base)[:, 1]\n",
        "            if reg_qty_base is not None:\n",
        "                pred_qty_given_occ_train = reg_qty_base.predict(X_train_base)\n",
        "                pred_qty_given_occ_train = np.clip(pred_qty_given_occ_train, 0, None) # Ensure non-negative\n",
        "            else:\n",
        "                pred_qty_given_occ_train = np.zeros_like(prob_occ_train) # Fallback if regressor failed\n",
        "            y_pred_C_train = prob_occ_train * pred_qty_given_occ_train\n",
        "            \n",
        "            model_predictions[\"Hurdle_Baseline\"] = y_pred_C_test\n",
        "            \n",
        "            # Calculate TEST Metrics for Hurdle Baseline\n",
        "            test_mae_C = mean_absolute_error(y_test, y_pred_C_test)\n",
        "            test_rmse_C = rmse(y_test, y_pred_C_test)\n",
        "            test_wmape_C = wmape(y_test, y_pred_C_test)\n",
        "            test_mape_C = mape(y_test, y_pred_C_test)\n",
        "            test_r2_C = r2_score(y_test, y_pred_C_test)\n",
        "\n",
        "            # Calculate TRAIN Metrics for Hurdle Baseline\n",
        "            train_mae_C = mean_absolute_error(y_train_actual, y_pred_C_train)\n",
        "            train_rmse_C = rmse(y_train_actual, y_pred_C_train)\n",
        "            train_wmape_C = wmape(y_train_actual, y_pred_C_train)\n",
        "            train_mape_C = mape(y_train_actual, y_pred_C_train)\n",
        "            train_r2_C = r2_score(y_train_actual, y_pred_C_train)\n",
        "\n",
        "            model_test_scores[\"Hurdle_Baseline\"] = {\n",
        "                \"MAE\": test_mae_C,\n",
        "                \"RMSE\": test_rmse_C,\n",
        "                \"WMAPE\": test_wmape_C,\n",
        "                \"MAPE\": test_mape_C,\n",
        "                \"R2\": test_r2_C\n",
        "            }\n",
        "            model_train_scores[\"Hurdle_Baseline\"] = { # Store train scores\n",
        "                \"MAE\": train_mae_C,\n",
        "                \"RMSE\": train_rmse_C,\n",
        "                \"WMAPE\": train_wmape_C,\n",
        "                \"MAPE\": train_mape_C,\n",
        "                \"R2\": train_r2_C\n",
        "            }\n",
        "        except Exception as e:\n",
        "            print(f\"[WARNING] Error in Hurdle Baseline model (C): {e}\")\n",
        "            model_predictions[\"Hurdle_Baseline\"] = np.zeros_like(y_test)\n",
        "            for metric_name in [\"MAE\", \"RMSE\", \"WMAPE\", \"MAPE\", \"R2\"]:\n",
        "                model_test_scores[\"Hurdle_Baseline\"][metric_name] = np.nan\n",
        "                model_train_scores[\"Hurdle_Baseline\"][metric_name] = np.nan # Store NaN for train too\n",
        "\n",
        "        # D. Hurdle +SHOS (Classifier on occurrence, Regressor on quantity using full features) - WITH TWEEDIE\n",
        "        hurdle_shos_occ_params = tuned_params.get(\"Hurdle_SHOS_Occurrence\", tuned_params.get(\"+SHOS\", {})) # Fallback to +SHOS params\n",
        "        hurdle_shos_qty_params = tuned_params.get(\"Hurdle_SHOS_Quantity\", tuned_params.get(\"LightGBM\", {})) # Fallback to LightGBM params\n",
        "        try:\n",
        "            # Prepare data for Hurdle +SHOS training\n",
        "            y_train_occ_binary_d = (y_train_actual > 0).astype(int)\n",
        "            pos_mask_qty_shos = y_train_actual > 0\n",
        "            if pos_mask_qty_shos.sum() > 0:\n",
        "                X_train_qty_pos_shos = X_train_full[pos_mask_qty_shos]\n",
        "                y_train_qty_pos_shos = y_train_actual[pos_mask_qty_shos]\n",
        "            else:\n",
        "                print(f\"[WARNING] No positive examples for quantity regression in Hurdle +SHOS for fold {i+1}.\")\n",
        "                X_train_qty_pos_shos, y_train_qty_pos_shos = np.array([]).reshape(0, X_train_full.shape[1]), np.array([])\n",
        "            \n",
        "            clf_occ_shos, reg_qty_shos = train_hurdle_model_optimized(\n",
        "                X_train_full, y_train_occ_binary_d,\n",
        "                X_train_qty_pos_shos, y_train_qty_pos_shos,\n",
        "                hurdle_shos_occ_params, hurdle_shos_qty_params\n",
        "            )\n",
        "            \n",
        "            # Predict probabilities and quantities\n",
        "            prob_occ_test_d = clf_occ_shos.predict_proba(X_test_full)[:, 1]\n",
        "            if reg_qty_shos is not None:\n",
        "                pred_qty_given_occ_test_d = reg_qty_shos.predict(X_test_full)\n",
        "                pred_qty_given_occ_test_d = np.clip(pred_qty_given_occ_test_d, 0, None) # Ensure non-negative\n",
        "            else:\n",
        "                pred_qty_given_occ_test_d = np.zeros_like(prob_occ_test_d) # Fallback if regressor failed\n",
        "            \n",
        "            y_pred_D_test = prob_occ_test_d * pred_qty_given_occ_test_d\n",
        "            \n",
        "            # Predict for TRAIN metrics\n",
        "            prob_occ_train_d = clf_occ_shos.predict_proba(X_train_full)[:, 1]\n",
        "            if reg_qty_shos is not None:\n",
        "                pred_qty_given_occ_train_d = reg_qty_shos.predict(X_train_full)\n",
        "                pred_qty_given_occ_train_d = np.clip(pred_qty_given_occ_train_d, 0, None) # Ensure non-negative\n",
        "            else:\n",
        "                pred_qty_given_occ_train_d = np.zeros_like(prob_occ_train_d) # Fallback if regressor failed\n",
        "            y_pred_D_train = prob_occ_train_d * pred_qty_given_occ_train_d\n",
        "            \n",
        "            model_predictions[\"Hurdle_SHOS\"] = y_pred_D_test\n",
        "            \n",
        "            # Calculate TEST Metrics for Hurdle +SHOS\n",
        "            test_mae_D = mean_absolute_error(y_test, y_pred_D_test)\n",
        "            test_rmse_D = rmse(y_test, y_pred_D_test)\n",
        "            test_wmape_D = wmape(y_test, y_pred_D_test)\n",
        "            test_mape_D = mape(y_test, y_pred_D_test)\n",
        "            test_r2_D = r2_score(y_test, y_pred_D_test)\n",
        "\n",
        "            # Calculate TRAIN Metrics for Hurdle +SHOS\n",
        "            train_mae_D = mean_absolute_error(y_train_actual, y_pred_D_train)\n",
        "            train_rmse_D = rmse(y_train_actual, y_pred_D_train)\n",
        "            train_wmape_D = wmape(y_train_actual, y_pred_D_train)\n",
        "            train_mape_D = mape(y_train_actual, y_pred_D_train)\n",
        "            train_r2_D = r2_score(y_train_actual, y_pred_D_train)\n",
        "\n",
        "            model_test_scores[\"Hurdle_SHOS\"] = {\n",
        "                \"MAE\": test_mae_D,\n",
        "                \"RMSE\": test_rmse_D,\n",
        "                \"WMAPE\": test_wmape_D,\n",
        "                \"MAPE\": test_mape_D,\n",
        "                \"R2\": test_r2_D\n",
        "            }\n",
        "            model_train_scores[\"Hurdle_SHOS\"] = { # Store train scores\n",
        "                \"MAE\": train_mae_D,\n",
        "                \"RMSE\": train_rmse_D,\n",
        "                \"WMAPE\": train_wmape_D,\n",
        "                \"MAPE\": train_mape_D,\n",
        "                \"R2\": train_r2_D\n",
        "            }\n",
        "        except Exception as e:\n",
        "            print(f\"[WARNING] Error in Hurdle +SHOS model (D): {e}\")\n",
        "            model_predictions[\"Hurdle_SHOS\"] = np.zeros_like(y_test)\n",
        "            for metric_name in [\"MAE\", \"RMSE\", \"WMAPE\", \"MAPE\", \"R2\"]:\n",
        "                model_test_scores[\"Hurdle_SHOS\"][metric_name] = np.nan\n",
        "                model_train_scores[\"Hurdle_SHOS\"][metric_name] = np.nan # Store NaN for train too\n",
        "\n",
        "        # --- STORE METRICS FOR THIS FOLD ---\n",
        "        for name in all_names:\n",
        "            for metric_name in [\"MAE\", \"RMSE\", \"WMAPE\", \"MAPE\", \"R2\"]:\n",
        "                # Append the scores calculated for this fold\n",
        "                metrics[name][metric_name].append(model_test_scores[name][metric_name])\n",
        "                fold_train_metrics[name][metric_name].append(model_train_scores[name][metric_name]) # Append train scores\n",
        "\n",
        "        # Store first fold results for plotting\n",
        "        if fold1_actual is None:\n",
        "            fold1_actual = y_test\n",
        "            fold1_preds = {\n",
        "                \"Croston\": croston_pred,\n",
        "                \"TSB\": tsb_pred,\n",
        "                \"Hurdle_SHOS\": y_pred_D_test, # Example: use Hurdle +SHOS prediction\n",
        "                \"LightGBM\": y_pred_A_test # Use the baseline prediction\n",
        "            }\n",
        "            # Store the 'Hurdle_SHOS' model and its features from the first fold for SHAP (optional)\n",
        "            # This requires re-training it here specifically if needed for SHAP, or storing the trained models per fold (more complex).\n",
        "            # For now, let's just pass the prediction data for potential SHAP later if needed.\n",
        "            # The actual model object is not stored here for SHAP, as it's trained inside the loop.\n",
        "\n",
        "    valid_folds = 0\n",
        "    # find a model name and metric list to count non-nan entries (safe method)\n",
        "    any_model = next(iter(metrics))\n",
        "    valid_folds = int(np.nanmax([len([x for x in metrics[m]['MAE'] if not (x is None or (isinstance(x, float) and np.isnan(x)))]) for m in metrics]) if metrics else 0)\n",
        "    print(f\"[INFO] Cross-validation completed. Estimated valid folds per model (approx): {valid_folds}\")\n",
        "    results = {}\n",
        "    train_results = {} # NEW: Store aggregated train results\n",
        "    for name in all_names: # Iterate over all model names\n",
        "        results[name] = {}\n",
        "        train_results[name] = {} # NEW: Initialize train results dict\n",
        "        for metric in [\"MAE\", \"RMSE\", \"WMAPE\", \"MAPE\", \"R2\"]: # Iterate over metric names\n",
        "            test_arr = np.array(metrics[name][metric])\n",
        "            train_arr = np.array(fold_train_metrics[name][metric]) # NEW: Get train array\n",
        "            results[name][f\"{metric}_mean\"] = np.nanmean(test_arr)\n",
        "            results[name][f\"{metric}_std\"] = np.nanstd(test_arr)\n",
        "            train_results[name][f\"{metric}_mean\"] = np.nanmean(train_arr) # NEW: Calculate train mean\n",
        "            train_results[name][f\"{metric}_std\"] = np.nanstd(train_arr) # NEW: Calculate train std\n",
        "\n",
        "    # --- OVERFITTING DIAGNOSIS ---\n",
        " # --- AGGREGATE RESULTS INTO DATAFRAMES (safe conversion) ---\n",
        "    results_df = pd.DataFrame(results).T if isinstance(results, dict) else pd.DataFrame(results)\n",
        "    train_df = pd.DataFrame(train_results).T if isinstance(train_results, dict) else pd.DataFrame(train_results)\n",
        "\n",
        "    # Ensure columns follow the \"<METRIC>_mean\" pattern\n",
        "    required_cols = [\"MAE_mean\",\"MAE_std\",\"RMSE_mean\",\"RMSE_std\",\"WMAPE_mean\",\"WMAPE_std\",\"MAPE_mean\",\"MAPE_std\",\"R2_mean\",\"R2_std\"]\n",
        "    # If some are missing, fill with NaN\n",
        "    for c in required_cols:\n",
        "        if c not in results_df.columns:\n",
        "            results_df[c] = np.nan\n",
        "        if c not in train_df.columns:\n",
        "            train_df[c] = np.nan\n",
        "\n",
        "    # Overfitting diagnostics: Test - Train\n",
        "    overfitting_df = pd.DataFrame(index=results_df.index)\n",
        "    overfitting_df[\"Test_MAE\"] = results_df[\"MAE_mean\"]\n",
        "    overfitting_df[\"Train_MAE\"] = train_df[\"MAE_mean\"]\n",
        "    overfitting_df[\"Diff_MAE\"] = overfitting_df[\"Test_MAE\"] - overfitting_df[\"Train_MAE\"]\n",
        "    overfitting_df[\"Test_R2\"] = results_df[\"R2_mean\"]\n",
        "    overfitting_df[\"Train_R2\"] = train_df[\"R2_mean\"]\n",
        "    overfitting_df[\"Diff_R2\"] = overfitting_df[\"Test_R2\"] - overfitting_df[\"Train_R2\"]\n",
        "\n",
        "    # Print\n",
        "    print(\"--- OVERFITTING DIAGNOSIS ---\")\n",
        "    print(overfitting_df.round(6))\n",
        "    print(\"Interpretation: Positive Diff_MAE or negative Diff_R2 indicates possible overfitting (Test worse than Train).\")\n",
        "    return results_df.to_dict(orient=\"index\"), train_df.to_dict(orient=\"index\"), overfitting_df, fold1_actual, fold1_preds\n",
        "\n",
        "# ----------------------------- SHAP Analysis (Point 4) -----------------------------\n",
        "def run_shap_analysis(model, X_test, feature_names, outdir):\n",
        "    if not SHAP_AVAILABLE:\n",
        "        print(\"[WARNING] SHAP not available, skipping SHAP analysis.\")\n",
        "        return\n",
        "    if model is None:\n",
        "        print(\"[WARNING] Model object is None, skipping SHAP analysis.\")\n",
        "        return\n",
        "    print(f\"[INFO] Calculating SHAP values for model...\")\n",
        "    try:\n",
        "        explainer = shap.TreeExplainer(model)\n",
        "        # Use a sample for speed if X_test is large\n",
        "        sample_size = min(1000, X_test.shape[0])\n",
        "        X_test_sample = X_test[:sample_size]\n",
        "        shap_values = explainer.shap_values(X_test_sample)\n",
        "        shap.summary_plot(shap_values, X_test_sample, feature_names=feature_names, show=False, plot_size=(10, 8))\n",
        "        plt.title(\"SHAP Feature Importance (Top 10)\")\n",
        "        plt.tight_layout()\n",
        "        plt.savefig(os.path.join(outdir, \"figures\", \"fig6_shap.png\"))\n",
        "        plt.close()\n",
        "        print(f\"[INFO] SHAP plot saved to {os.path.join(outdir, 'figures', 'fig6_shap.png')}\")\n",
        "    except Exception as e:\n",
        "        print(f\"[WARNING] Could not generate SHAP plot: {e}\")\n",
        "\n",
        "# ----------------------------- Main Pipeline (Test Run) -----------------------------\n",
        "def run_test_pipeline(df_filtered, outdir=\"./result_test\", shos_k=0.1, shos_w=0.7):\n",
        "    print(\"[INFO] Starting test pipeline...\")\n",
        "    start_time = time.time()\n",
        "    safe_mkdir(outdir)\n",
        "    safe_mkdir(os.path.join(outdir, \"figures\")) # Ensure figures dir exists\n",
        "    \n",
        "    print(\"[INFO] 1. Aggregating and padding data...\")\n",
        "    df_ag = aggregate_monthly(df_filtered)\n",
        "    df_padded = pad_monthly_series_and_filter_inactive(df_ag)\n",
        "    \n",
        "    print(\"[INFO] 2. Computing intermittency metrics...\")\n",
        "    intermittency_df = compute_intermittency(df_padded)\n",
        "    intermittency_df.to_csv(os.path.join(outdir, \"intermittency_stats.csv\"), index=False)\n",
        "    \n",
        "    print(\"[INFO] 3. Using pre-loaded tuned parameters...\")\n",
        "    # --- HARDCODED PRE-TUNED PARAMETERS FROM LOG (Including Hurdle, Using Tweedie) ---\n",
        "    # Removed MLP, CatBoost params\n",
        "    tuned_params = {\n",
        "        \"LightGBM\": {'num_leaves': 63, 'n_estimators': 200, 'min_child_samples': 20, 'learning_rate': 0.05},\n",
        "        \"XGBoost\": {'n_estimators': 500, 'max_depth': 9, 'learning_rate': 0.05},\n",
        "        \"ElasticNet\": {'l1_ratio': 0.9, 'alpha': 0.0001},\n",
        "        \"Ridge\": {'alpha': 0.01},\n",
        "        \"RandomForest\": {'n_estimators': 200, 'min_samples_split': 10, 'max_depth': None},\n",
        "        \"+SHOS\": {'num_leaves': 63, 'n_estimators': 200, 'min_child_samples': 10, 'learning_rate': 0.05},\n",
        "        # Hurdle parameters using defaults or existing model params as fallbacks\n",
        "        \"Hurdle_Baseline_Occurrence\": {'num_leaves': 15, 'n_estimators': 500, 'min_child_samples': 20, 'learning_rate': 0.01}, # Use default or LightGBM params\n",
        "        \"Hurdle_Baseline_Quantity\": {'num_leaves': 63, 'n_estimators': 200, 'min_child_samples': 50, 'learning_rate': 0.05}, # Use default or LightGBM params\n",
        "        \"Hurdle_SHOS_Occurrence\": {'num_leaves': 15, 'n_estimators': 200, 'min_child_samples': 50, 'learning_rate': 0.05}, # Use default or +SHOS params\n",
        "        \"Hurdle_SHOS_Quantity\": {'num_leaves': 63, 'n_estimators': 500, 'min_child_samples': 20, 'learning_rate': 0.05} # Use default or +SHOS params\n",
        "        # Removed \"+Hybrid\", \"Full\", \"Tweedie\" as requested\n",
        "    }\n",
        "    # --- END HARDCODED PARAMETERS ---\n",
        "\n",
        "    print(\"[INFO] 4. Building feature matrix once...\")\n",
        "    df_features, base_cols, full_cols = build_feature_matrix_optimized(df_padded, shos_k=shos_k) # Pass shos_k for SHOS calc\n",
        "\n",
        "    print(\"[INFO] 5. Running main cross-validation with optimized features...\")\n",
        "    cv_results, train_results, overfitting_df, fold1_actual, fold1_preds = run_cross_validation_optimized(\n",
        "        df_features, base_cols, full_cols, tuned_params, shos_k=shos_k, shos_w=shos_w # Pass pre-computed features and k, w\n",
        "    )\n",
        "    \n",
        "    print(\"[INFO] 6. Saving results...\")\n",
        "    test_scores_df = pd.DataFrame(cv_results).T\n",
        "    test_scores_df.to_csv(os.path.join(outdir, \"test_results.csv\"))\n",
        "    \n",
        "    train_scores_df = pd.DataFrame(train_results).T # NEW: Save train results\n",
        "    train_scores_df.to_csv(os.path.join(outdir, \"train_results.csv\"))\n",
        "    \n",
        "    overfitting_df.to_csv(os.path.join(outdir, \"overfitting_diagnosis.csv\")) # Save overfitting results\n",
        "    print(\" --- TEST RESULTS (Mean ± Std) --- \")\n",
        "    print(test_scores_df.round(4))\n",
        "    print(\" --- TRAIN RESULTS (Mean ± Std) --- \") # NEW: Print train results\n",
        "    print(train_scores_df.round(4))\n",
        "    print(\" --- OVERFITTING DIAGNOSIS (Test - Train) --- \")\n",
        "    print(overfitting_df.round(4))\n",
        "\n",
        "    print(\"[INFO] 7. Generating figures...\")\n",
        "    out_fig = os.path.join(outdir, \"figures\")\n",
        "    safe_mkdir(out_fig)\n",
        "    \n",
        "    # Filter models for plotting (exclude TSB/Croston if too many, include Hurdle models)\n",
        "    plot_models = [m for m in test_scores_df.index if m not in [\"TSB\", \"Croston\"]] # Or include them if desired\n",
        "    test_mae_means = [test_scores_df.loc[m, \"MAE_mean\"] for m in plot_models]\n",
        "    test_mae_stds = [test_scores_df.loc[m, \"MAE_std\"] for m in plot_models]\n",
        "    test_rmse_means = [test_scores_df.loc[m, \"RMSE_mean\"] for m in plot_models]\n",
        "    test_rmse_stds = [test_scores_df.loc[m, \"RMSE_std\"] for m in plot_models]\n",
        "    test_wmape_means = [test_scores_df.loc[m, \"WMAPE_mean\"] for m in plot_models]\n",
        "    test_wmape_stds = [test_scores_df.loc[m, \"WMAPE_std\"] for m in plot_models]\n",
        "    test_mape_means = [test_scores_df.loc[m, \"MAPE_mean\"] for m in plot_models]\n",
        "    test_mape_stds = [test_scores_df.loc[m, \"MAPE_std\"] for m in plot_models]\n",
        "    test_r2_means = [test_scores_df.loc[m, \"R2_mean\"] for m in plot_models]\n",
        "    test_r2_stds = [test_scores_df.loc[m, \"R2_std\"] for m in plot_models]\n",
        "    \n",
        "    # NEW: Get train metrics for plotting\n",
        "    train_mae_means = [train_scores_df.loc[m, \"MAE_mean\"] for m in plot_models]\n",
        "    train_mae_stds = [train_scores_df.loc[m, \"MAE_std\"] for m in plot_models]\n",
        "    train_r2_means = [train_scores_df.loc[m, \"R2_mean\"] for m in plot_models]\n",
        "    train_r2_stds = [train_scores_df.loc[m, \"R2_std\"] for m in plot_models]\n",
        "    \n",
        "    x = np.arange(len(plot_models))\n",
        "    width = 0.35\n",
        "    \n",
        "    # Figure 1: MAE (Train vs Test)\n",
        "    fig, ax = plt.subplots(figsize=(12, 6))\n",
        "    ax.bar(x - width/2, test_mae_means, width, yerr=test_mae_stds, label='Test MAE')\n",
        "    ax.bar(x + width/2, train_mae_means, width, yerr=train_mae_stds, label='Train MAE') # NEW: Add train bars\n",
        "    ax.set_xticks(x)\n",
        "    ax.set_xticklabels(plot_models, rotation=45)\n",
        "    ax.set_ylabel(\"MAE\"); ax.legend()\n",
        "    plt.tight_layout()\n",
        "    plt.savefig(os.path.join(out_fig, \"fig1_mae_train_test.png\"))\n",
        "    \n",
        "    # Figure 2: R2 (Train vs Test)\n",
        "    fig, ax = plt.subplots(figsize=(12, 6))\n",
        "    ax.bar(x - width/2, test_r2_means, width, yerr=test_r2_stds, label='Test R2')\n",
        "    ax.bar(x + width/2, train_r2_means, width, yerr=train_r2_stds, label='Train R2') # NEW: Add train bars\n",
        "    ax.set_xticks(x)\n",
        "    ax.set_xticklabels(plot_models, rotation=45)\n",
        "    ax.set_ylabel(\"R2 Score\"); ax.legend()\n",
        "    plt.tight_layout()\n",
        "    plt.savefig(os.path.join(out_fig, \"fig2_r2_train_test.png\"))\n",
        "\n",
        "    # Figure 3: WMAPE/MAPE (Test only, as before)\n",
        "    fig, ax = plt.subplots(figsize=(12, 6))\n",
        "    ax.bar(x - width/2, test_wmape_means, width, yerr=test_wmape_stds, label='Test WMAPE')\n",
        "    ax.bar(x + width/2, test_mape_means, width, yerr=test_mape_stds, label='Test MAPE')\n",
        "    ax.set_xticks(x)\n",
        "    ax.set_xticklabels(plot_models, rotation=45)\n",
        "    ax.set_ylabel(\"Percentage Error\"); ax.legend()\n",
        "    plt.tight_layout()\n",
        "    plt.savefig(os.path.join(out_fig, \"fig3_wmape_mape_test.png\"))\n",
        "\n",
        "  \n",
        "    # Figure 5: Overfitting Bar Chart (Difference)\n",
        "    fig, ax = plt.subplots(figsize=(12, 6))\n",
        "    ax.bar(x - width/2, overfitting_df['Diff_MAE'], width, label='Test MAE - Train MAE', alpha=0.8)\n",
        "    ax.axhline(0, color='black', linewidth=0.8) # Add a horizontal line at 0\n",
        "    ax.set_xticks(x)\n",
        "    ax.set_xticklabels(plot_models, rotation=45)\n",
        "    ax.set_ylabel(\"Difference in Metric (Test - Train)\"); ax.legend()\n",
        "    plt.tight_layout()\n",
        "    plt.savefig(os.path.join(out_fig, \"fig5_overfitting_diff_mae.png\"))\n",
        "\n",
        "    # Figure 6: Overfitting R2 Chart (Difference)\n",
        "    fig, ax = plt.subplots(figsize=(12, 6))\n",
        "    ax.bar(x - width/2, overfitting_df['Diff_R2'], width, label='Test R2 - Train R2', alpha=0.8, color='orange')\n",
        "    ax.axhline(0, color='black', linewidth=0.8) # Add a horizontal line at 0\n",
        "    ax.set_xticks(x)\n",
        "    ax.set_xticklabels(plot_models, rotation=45)\n",
        "    ax.set_ylabel(\"Difference in R2 (Test - Train)\"); ax.legend()\n",
        "    plt.tight_layout()\n",
        "    plt.savefig(os.path.join(out_fig, \"fig6_overfitting_diff_r2.png\"))\n",
        "\n",
        "    # Intermittency Histograms\n",
        "    fig, axes = plt.subplots(1, 2, figsize=(12, 5))\n",
        "    axes[0].hist(intermittency_df[\"ADI\"], bins=50, color=\"skyblue\")\n",
        "    axes[0].set_title(f\"ADI (median={intermittency_df['ADI'].median():.2f})\")\n",
        "    axes[1].hist(intermittency_df[\"CV2\"].clip(0, 10), bins=50, color=\"salmon\")\n",
        "    axes[1].set_title(f\"CV² (median={intermittency_df['CV2'].median():.2f})\")\n",
        "    plt.tight_layout()\n",
        "    plt.savefig(os.path.join(out_fig, \"fig7_intermittency.png\"))\n",
        "\n",
        "    print(f\"[INFO] Pipeline completed in {time.time() - start_time:.2f} seconds. Results saved to: {outdir}\")\n",
        "    \n",
        "    # Print summary\n",
        "    print(\"--- SUMMARY ---\")\n",
        "    print(\"Test Results (Mean ± Std):\")\n",
        "    print(test_scores_df[[\"MAE_mean\", \"RMSE_mean\", \"WMAPE_mean\", \"MAPE_mean\", \"R2_mean\"]].round(4))\n",
        "    best_test_mae_model = test_scores_df['MAE_mean'].idxmin()\n",
        "    best_test_r2_model = test_scores_df['R2_mean'].idxmax()\n",
        "    print(f\"Best Test MAE: {test_scores_df.loc[best_test_mae_model, 'MAE_mean']:.4f} ({best_test_mae_model})\")\n",
        "    print(f\"Best Test R2: {test_scores_df.loc[best_test_r2_model, 'R2_mean']:.4f} ({best_test_r2_model})\")\n",
        "    print(\"Train Results (Mean ± Std):\") # NEW: Print train summary\n",
        "    print(train_scores_df[[\"MAE_mean\", \"RMSE_mean\", \"WMAPE_mean\", \"MAPE_mean\", \"R2_mean\"]].round(4))\n",
        "    print(\"Overfitting Diagnosis (Mean Difference: Test - Train):\") # Print overfitting summary\n",
        "    print(overfitting_df[[\"Diff_MAE\", \"Diff_R2\"]].round(4))\n",
        "    print(f\"Median ADI: {intermittency_df['ADI'].median():.2f}\")\n",
        "    print(f\"Median CV²: {intermittency_df['CV2'].median():.2f}\")\n",
        "    return cv_results, train_results # Return both test and train results\n",
        "\n",
        "# ----------------------------- Entry Point -----------------------------\n",
        "if __name__ == \"__main__\":\n",
        "    # Example usage:\n",
        "    # df_filtered = pd.read_csv(\"your_data.csv\")\n",
        "    run_test_pipeline(df_filtered, outdir=\"./result_test1\", shos_k=0.1, shos_w=0.7) # Provide your best k, w\n",
        "    pass"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[INFO] Aggregating data to monthly level...\n",
            "[INFO] Aggregated to 171111 monthly records.\n",
            "[INFO] Padding series to monthly grid and filtering inactive pairs...\n",
            "[INFO] Date range: 2023-08 to 2025-08. Total months: 25\n",
            "[INFO] Filtered from 171111 to 170827 records after removing inactive pairs.\n",
            "[INFO] Padding complete. Final shape: (1404700, 9)\n",
            "[INFO] Building feature matrix ONCE for all folds...\n",
            "[INFO] Adding time series features (max_lag=6)...\n",
            "[INFO] Added 12 lag features.\n",
            "[INFO] Adding Croston features...\n",
            "[INFO] Added Croston features for 56188 unique series.\n",
            "[INFO] Adding SHOS features with train-only sparsity (k=0.1)...\n",
            "[INFO] Added SHOS features for 56188 unique series.\n",
            "[INFO] Building low-rank embeddings (n_components=12)...\n",
            "[INFO] Created 24 embedding features for 59 dealers and 7702 parts.\n",
            "[INFO] Final feature matrix has 50 columns and shape (1404700, 59).\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.079001 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2056\n",
            "[LightGBM] [Info] Number of data points in the train set: 1292324, number of used features: 23\n",
            "[LightGBM] [Info] Start training from score -1.093818\n",
            "[INFO] Training LightGBM took 6.83s.\n",
            "[INFO] Saved SHAP for LightGBM\n",
            "[INFO] Training XGBoost with pre-tuned parameters...\n",
            "[INFO] Training XGBoost took 35.25s.\n",
            "[INFO] Saved SHAP for XGBoost\n",
            "[INFO] Training ElasticNet with pre-tuned parameters...\n",
            "[INFO] Training ElasticNet took 274.66s.\n",
            "[INFO] Saved SHAP for ElasticNet\n",
            "[INFO] Training Ridge with pre-tuned parameters...\n",
            "[INFO] Training Ridge took 0.77s.\n",
            "[INFO] Saved SHAP for Ridge\n",
            "[INFO] Training RandomForest with pre-tuned parameters...\n",
            "[INFO] Training RandomForest took 452.74s.\n",
            "[INFO] Saved SHAP for RandomForest\n",
            "[INFO] Training LightGBM with pre-tuned parameters...\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.111043 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6348\n",
            "[LightGBM] [Info] Number of data points in the train set: 1292324, number of used features: 50\n",
            "[LightGBM] [Info] Start training from score -1.093818\n",
            "[INFO] Training LightGBM took 11.53s.\n",
            "[INFO] Saved SHAP for +SHOS\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 156086, number of negative: 1136238\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.058062 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2056\n",
            "[LightGBM] [Info] Number of data points in the train set: 1292324, number of used features: 23\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.120779 -> initscore=-1.985071\n",
            "[LightGBM] [Info] Start training from score -1.985071\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.007161 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 2791\n",
            "[LightGBM] [Info] Number of data points in the train set: 156086, number of used features: 23\n",
            "[LightGBM] [Info] Start training from score 1.019973\n",
            "[INFO] Training Hurdle Model took 10.26s.\n",
            "[INFO] Saved SHAP for Hurdle_Baseline_occ\n",
            "[INFO] Saved SHAP for Hurdle_Baseline_qty\n",
            "[INFO] Training Hurdle Model (Occurrence + Quantity)...\n",
            "[LightGBM] [Info] Number of positive: 156086, number of negative: 1136238\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.082715 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 6346\n",
            "[LightGBM] [Info] Number of data points in the train set: 1292324, number of used features: 49\n",
            "[LightGBM] [Info] [binary:BoostFromScore]: pavg=0.120779 -> initscore=-1.985071\n",
            "[LightGBM] [Info] Start training from score -1.985071\n",
            "[LightGBM] [Info] Auto-choosing row-wise multi-threading, the overhead of testing was 0.010085 seconds.\n",
            "You can set `force_row_wise=true` to remove the overhead.\n",
            "And if memory is not enough, you can set `force_col_wise=true`.\n",
            "[LightGBM] [Info] Total Bins 7045\n",
            "[LightGBM] [Info] Number of data points in the train set: 156086, number of used features: 49\n",
            "[LightGBM] [Info] Start training from score 1.019973\n",
            "[INFO] Training Hurdle Model took 11.07s.\n",
            "[INFO] Saved SHAP for Hurdle_SHOS_occ\n",
            "[INFO] Saved SHAP for Hurdle_SHOS_qty\n",
            "[INFO] SHAP computation completed. Summaries in ./shap_results/summary, raw arrays in ./shap_results/raw\n"
          ]
        }
      ],
      "source": [
        "# Cell 7: compute and save SHAP values for each model\n",
        "# Uses existing functions/vars in notebook (aggregate_monthly, pad_monthly_series_and_filter_inactive,\n",
        "# build_feature_matrix_optimized, train_with_params, train_hurdle_model_optimized, etc.)\n",
        "# Saves per-model mean(|SHAP|) per feature and raw SHAP arrays for a test sample.\n",
        "\n",
        "outdir = \"./shap_results\"\n",
        "safe_mkdir(outdir)\n",
        "safe_mkdir(os.path.join(outdir, \"raw\"))\n",
        "safe_mkdir(os.path.join(outdir, \"summary\"))\n",
        "\n",
        "# 1) Prepare data / features (reuse pipeline logic)\n",
        "df_ag = aggregate_monthly(df_filtered)\n",
        "df_padded = pad_monthly_series_and_filter_inactive(df_ag)\n",
        "df_features, base_cols, full_cols = build_feature_matrix_optimized(df_padded, shos_k=0.1, svd_components=12)\n",
        "\n",
        "# 2) Train/Test split: last 3 months as test (fallback to last month if <3)\n",
        "all_months = sorted(df_features[\"MonthStart\"].unique())\n",
        "n_test_months = min(3, max(1, len(all_months)//10))  # at least 1, up to 3\n",
        "test_months = all_months[-n_test_months:]\n",
        "train_months = [m for m in all_months if m not in test_months]\n",
        "\n",
        "train_f = df_features[df_features[\"MonthStart\"].isin(train_months)].copy()\n",
        "test_f = df_features[df_features[\"MonthStart\"].isin(test_months)].copy()\n",
        "\n",
        "X_train_base = train_f[base_cols].fillna(0).values\n",
        "X_test_base = test_f[base_cols].fillna(0).values\n",
        "X_train_full = train_f[full_cols].fillna(0).values\n",
        "X_test_full = test_f[full_cols].fillna(0).values\n",
        "y_train = train_f[\"PartQty\"].values\n",
        "y_test = test_f[\"PartQty\"].values\n",
        "\n",
        "# sample for SHAP (speed/memory) - use smaller sample to avoid memory issues\n",
        "sample_size = min(200, max(50, X_test_full.shape[0] // 100))  # aggressive reduction\n",
        "Xshap_sample_base = X_test_base[:sample_size]\n",
        "Xshap_sample_full = X_test_full[:sample_size]\n",
        "\n",
        "# 3) Tuned params (reuse same hardcoded tuned params as in optimized pipeline)\n",
        "tuned_params = {\n",
        "    \"LightGBM\": {'num_leaves': 63, 'n_estimators': 200, 'min_child_samples': 20, 'learning_rate': 0.05},\n",
        "    \"XGBoost\": {'n_estimators': 500, 'max_depth': 9, 'learning_rate': 0.05},\n",
        "    \"ElasticNet\": {'l1_ratio': 0.9, 'alpha': 0.0001},\n",
        "    \"Ridge\": {'alpha': 0.01},\n",
        "    \"RandomForest\": {'n_estimators': 200, 'min_samples_split': 10, 'max_depth': None},\n",
        "    \"+SHOS\": {'num_leaves': 63, 'n_estimators': 200, 'min_child_samples': 10, 'learning_rate': 0.05},\n",
        "    \"Hurdle_Baseline_Occurrence\": {'num_leaves': 15, 'n_estimators': 500, 'min_child_samples': 20, 'learning_rate': 0.01},\n",
        "    \"Hurdle_Baseline_Quantity\": {'num_leaves': 63, 'n_estimators': 200, 'min_child_samples': 50, 'learning_rate': 0.05},\n",
        "    \"Hurdle_SHOS_Occurrence\": {'num_leaves': 15, 'n_estimators': 200, 'min_child_samples': 50, 'learning_rate': 0.05},\n",
        "    \"Hurdle_SHOS_Quantity\": {'num_leaves': 63, 'n_estimators': 500, 'min_child_samples': 20, 'learning_rate': 0.05}\n",
        "}\n",
        "\n",
        "# 4) Helper for computing & saving SHAP for a given model\n",
        "def compute_and_save_shap(model, X_sample, feature_names, model_name, part=\"\"):\n",
        "    if model is None:\n",
        "        print(f\"[INFO] {model_name} {part} model is None, skipping SHAP.\")\n",
        "        return None\n",
        "    try:\n",
        "        if hasattr(shap, \"TreeExplainer\") and (type(model).__name__.lower().startswith((\"lgb\", \"xgb\", \"randomforest\", \"histgradientboosting\", \"catboost\")) or \"LGBM\" in type(model).__name__ or \"XGB\" in type(model).__name__):\n",
        "            explainer = shap.TreeExplainer(model)\n",
        "            shap_values = explainer.shap_values(X_sample)\n",
        "        elif hasattr(shap, \"LinearExplainer\") and (\"coef_\" in dir(model) or \"intercept_\" in dir(model)):\n",
        "            explainer = shap.LinearExplainer(model, X_train_base[:min(100, X_train_base.shape[0])], feature_perturbation=\"interventional\")\n",
        "            shap_values = explainer.shap_values(X_sample)\n",
        "        else:\n",
        "            # fallback to KernelExplainer (slower): use small background\n",
        "            bg_size = min(100, X_train_base.shape[0])\n",
        "            bg = X_train_base[np.random.choice(X_train_base.shape[0], bg_size, replace=False)]\n",
        "            explainer = shap.KernelExplainer(lambda x: model.predict(x), bg)\n",
        "            shap_values = explainer.shap_values(X_sample, nsamples=50)\n",
        "        # ensure numpy array\n",
        "        shap_arr = np.array(shap_values)\n",
        "        # compute mean absolute shap per feature\n",
        "        if shap_arr.ndim == 3:  # multiclass case -> average over classes\n",
        "            shap_abs_mean = np.mean(np.abs(shap_arr), axis=(0,1))\n",
        "        else:\n",
        "            shap_abs_mean = np.mean(np.abs(shap_arr), axis=0)\n",
        "        df_shap_summary = pd.DataFrame({\"feature\": feature_names, \"mean_abs_shap\": shap_abs_mean})\n",
        "        fname_base = f\"{model_name}{('_'+part) if part else ''}\"\n",
        "        df_shap_summary.sort_values(\"mean_abs_shap\", ascending=False).to_csv(os.path.join(outdir, \"summary\", f\"{fname_base}_shap_summary.csv\"), index=False)\n",
        "        # save raw shap array (compressed)\n",
        "        np.savez_compressed(os.path.join(outdir, \"raw\", f\"{fname_base}_shap_raw.npz\"), shap=shap_arr, features=np.array(feature_names))\n",
        "        print(f\"[INFO] Saved SHAP for {fname_base}\")\n",
        "        return df_shap_summary\n",
        "    except Exception as e:\n",
        "        print(f\"[WARNING] Could not compute SHAP for {model_name} {part}: {e}\")\n",
        "        return None\n",
        "\n",
        "# 5) Train models and compute SHAPs\n",
        "shap_results = {}\n",
        "\n",
        "# -- Baseline tree / ML models on base features\n",
        "model_objs = {}\n",
        "for name in [\"LightGBM\", \"XGBoost\", \"ElasticNet\", \"Ridge\", \"RandomForest\"]:\n",
        "    params = tuned_params.get(name)\n",
        "    try:\n",
        "        if name in [\"LightGBM\", \"XGBoost\"]:\n",
        "            model = train_with_params(name, X_train_base, y_train, params, objective='tweedie')\n",
        "        else:\n",
        "            model = train_with_params(name, X_train_base, y_train, params)\n",
        "        model_objs[name] = model\n",
        "        shap_results[name] = compute_and_save_shap(model, Xshap_sample_base, base_cols, name)\n",
        "    except Exception as e:\n",
        "        print(f\"[WARNING] Could not train or SHAP {name}: {e}\")\n",
        "        model_objs[name] = None\n",
        "        shap_results[name] = None\n",
        "\n",
        "# -- +SHOS (LightGBM on full features)\n",
        "try:\n",
        "    model_shos = train_with_params(\"LightGBM\", X_train_full, y_train, tuned_params.get(\"+SHOS\", {}), objective='tweedie')\n",
        "    shap_results[\"+SHOS\"] = compute_and_save_shap(model_shos, Xshap_sample_full, full_cols, \"+SHOS\")\n",
        "    model_objs[\"+SHOS\"] = model_shos\n",
        "except Exception as e:\n",
        "    print(f\"[WARNING] +SHOS train/SHAP failed: {e}\")\n",
        "    shap_results[\"+SHOS\"] = None\n",
        "    model_objs[\"+SHOS\"] = None\n",
        "\n",
        "# -- Hurdle models: get SHAP for occurrence classifier and quantity regressor separately\n",
        "# Hurdle Baseline (occurrence on base, qty on base)\n",
        "try:\n",
        "    y_occ = (y_train > 0).astype(int)\n",
        "    pos_mask = y_train > 0\n",
        "    X_train_qty_base = X_train_base[pos_mask] if pos_mask.sum() > 0 else np.empty((0, X_train_base.shape[1]))\n",
        "    y_train_qty_base = y_train[pos_mask] if pos_mask.sum() > 0 else np.array([])\n",
        "    clf_occ_base, reg_qty_base = train_hurdle_model_optimized(\n",
        "        X_train_base, y_occ,\n",
        "        X_train_qty_base, y_train_qty_base,\n",
        "        tuned_params.get(\"Hurdle_Baseline_Occurrence\", {}), tuned_params.get(\"Hurdle_Baseline_Quantity\", {})\n",
        "    )\n",
        "    shap_results[\"Hurdle_Baseline_occ\"] = compute_and_save_shap(clf_occ_base, Xshap_sample_base, base_cols, \"Hurdle_Baseline\", part=\"occ\")\n",
        "    # for quantity regressor explain using full base sample (but reg trained on base features)\n",
        "    shap_results[\"Hurdle_Baseline_qty\"] = compute_and_save_shap(reg_qty_base, Xshap_sample_base, base_cols, \"Hurdle_Baseline\", part=\"qty\")\n",
        "    model_objs[\"Hurdle_Baseline_occ\"] = clf_occ_base\n",
        "    model_objs[\"Hurdle_Baseline_qty\"] = reg_qty_base\n",
        "except Exception as e:\n",
        "    print(f\"[WARNING] Hurdle_Baseline train/SHAP failed: {e}\")\n",
        "    shap_results[\"Hurdle_Baseline_occ\"] = None\n",
        "    shap_results[\"Hurdle_Baseline_qty\"] = None\n",
        "\n",
        "# Hurdle +SHOS (occurrence on full, qty on full)\n",
        "try:\n",
        "    y_occ = (y_train > 0).astype(int)\n",
        "    pos_mask = y_train > 0\n",
        "    X_train_qty_full = X_train_full[pos_mask] if pos_mask.sum() > 0 else np.empty((0, X_train_full.shape[1]))\n",
        "    y_train_qty_full = y_train[pos_mask] if pos_mask.sum() > 0 else np.array([])\n",
        "    clf_occ_shos, reg_qty_shos = train_hurdle_model_optimized(\n",
        "        X_train_full, y_occ,\n",
        "        X_train_qty_full, y_train_qty_full,\n",
        "        tuned_params.get(\"Hurdle_SHOS_Occurrence\", tuned_params.get(\"+SHOS\", {})),\n",
        "        tuned_params.get(\"Hurdle_SHOS_Quantity\", tuned_params.get(\"LightGBM\", {}))\n",
        "    )\n",
        "    shap_results[\"Hurdle_SHOS_occ\"] = compute_and_save_shap(clf_occ_shos, Xshap_sample_full, full_cols, \"Hurdle_SHOS\", part=\"occ\")\n",
        "    shap_results[\"Hurdle_SHOS_qty\"] = compute_and_save_shap(reg_qty_shos, Xshap_sample_full, full_cols, \"Hurdle_SHOS\", part=\"qty\")\n",
        "    model_objs[\"Hurdle_SHOS_occ\"] = clf_occ_shos\n",
        "    model_objs[\"Hurdle_SHOS_qty\"] = reg_qty_shos\n",
        "except Exception as e:\n",
        "    print(f\"[WARNING] Hurdle_SHOS train/SHAP failed: {e}\")\n",
        "    shap_results[\"Hurdle_SHOS_occ\"] = None\n",
        "    shap_results[\"Hurdle_SHOS_qty\"] = None\n",
        "\n",
        "# 6) Save a small manifest summarizing available SHAP outputs\n",
        "manifest = []\n",
        "for k, v in shap_results.items():\n",
        "    manifest.append({\"model\": k, \"has_summary\": v is not None})\n",
        "pd.DataFrame(manifest).to_csv(os.path.join(outdir, \"shap_manifest.csv\"), index=False)\n",
        "\n",
        "print(\"[INFO] SHAP computation completed. Summaries in ./shap_results/summary, raw arrays in ./shap_results/raw\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[INFO] Loaded SHAP summaries for 10 models\n",
            "[INFO] Saved: shap_top10_per_model.png\n",
            "[INFO] Saved: shap_feature_heatmap.png\n",
            "[INFO] Saved: shap_global_top15.png\n",
            "[INFO] Saved: shap_hurdle_comparison.png\n",
            "[INFO] Saved: shap_base_vs_full.png\n",
            "\n",
            "=== SHAP Feature Importance Summary ===\n",
            "\n",
            "+SHOS:\n",
            "  - DealerPrice: 2.725103\n",
            "  - q_shos: 0.772304\n",
            "  - PRC_UNIT_CUBE: 0.118492\n",
            "\n",
            "ElasticNet:\n",
            "  - qty_sum_1_3: 0.351707\n",
            "  - qty_lag_3: 0.147318\n",
            "  - qty_ma_3: 0.120269\n",
            "\n",
            "Hurdle_Baseline_occ:\n",
            "  - DealerPrice: 2.041005\n",
            "  - dealer_qty_mean: 0.017449\n",
            "  - qty_ma_6: 0.002336\n",
            "\n",
            "Hurdle_Baseline_qty:\n",
            "  - part_qty_mean: 0.367302\n",
            "  - DealerPrice: 0.110572\n",
            "  - PRC_UNIT_CUBE: 0.109983\n",
            "\n",
            "Hurdle_SHOS_occ:\n",
            "  - DealerPrice: 2.698617\n",
            "  - q_shos: 0.829780\n",
            "  - emb_dealer_3: 0.138114\n",
            "\n",
            "Hurdle_SHOS_qty:\n",
            "  - z_shos: 0.507182\n",
            "  - nonzero_count_6: 0.040668\n",
            "  - qty_ma_6: 0.036943\n",
            "\n",
            "LightGBM:\n",
            "  - DealerPrice: 3.451574\n",
            "  - PRC_UNIT_WEIGHT: 0.160784\n",
            "  - PRC_UNIT_CUBE: 0.065278\n",
            "\n",
            "RandomForest:\n",
            "  - PRC_UNIT_CUBE: 0.293602\n",
            "  - qty_ma_6: 0.145102\n",
            "  - part_qty_mean: 0.113264\n",
            "\n",
            "Ridge:\n",
            "  - qty_ma_3: 0.121856\n",
            "  - qty_sum_1_6: 0.106875\n",
            "  - nonzero_count_6: 0.102708\n",
            "\n",
            "XGBoost:\n",
            "  - PRC_UNIT_WEIGHT: 0.228567\n",
            "  - part_qty_mean: 0.124469\n",
            "  - qty_ma_6: 0.092904\n",
            "\n",
            "[INFO] Saved: shap_global_importance.csv\n",
            "\n",
            "[INFO] All SHAP visualizations saved to: ./shap_results\\figures\n"
          ]
        }
      ],
      "source": [
        "import os\n",
        "import pandas as pd\n",
        "import numpy as np\n",
        "import seaborn as sns\n",
        "\n",
        "# Cell 8: Generate SHAP visualizations from saved results\n",
        "import matplotlib.pyplot as plt\n",
        "\n",
        "outdir = \"./shap_results\"\n",
        "summary_dir = os.path.join(outdir, \"summary\")\n",
        "figures_dir = os.path.join(outdir, \"figures\")\n",
        "safe_mkdir(figures_dir)\n",
        "\n",
        "# 1) Load all SHAP summaries and create comparison plots\n",
        "summary_files = [f for f in os.listdir(summary_dir) if f.endswith(\"_shap_summary.csv\")]\n",
        "shap_data = {}\n",
        "\n",
        "for fname in summary_files:\n",
        "    model_name = fname.replace(\"_shap_summary.csv\", \"\")\n",
        "    df = pd.read_csv(os.path.join(summary_dir, fname))\n",
        "    shap_data[model_name] = df.sort_values(\"mean_abs_shap\", ascending=False)\n",
        "\n",
        "print(f\"[INFO] Loaded SHAP summaries for {len(shap_data)} models\")\n",
        "\n",
        "# 2) Plot 1: Top 10 features per model (side-by-side bar charts)\n",
        "fig, axes = plt.subplots(2, 3, figsize=(16, 10))\n",
        "axes = axes.flatten()\n",
        "\n",
        "model_list = sorted(shap_data.keys())[:6]  # Limit to 6 for readability\n",
        "for idx, model_name in enumerate(model_list):\n",
        "    ax = axes[idx]\n",
        "    df_top = shap_data[model_name].head(10)\n",
        "    ax.barh(range(len(df_top)), df_top[\"mean_abs_shap\"].values, color=\"steelblue\")\n",
        "    ax.set_yticks(range(len(df_top)))\n",
        "    ax.set_yticklabels(df_top[\"feature\"].values, fontsize=9)\n",
        "    ax.set_xlabel(\"Mean |SHAP|\")\n",
        "    ax.set_title(f\"{model_name}\")\n",
        "    ax.invert_yaxis()\n",
        "\n",
        "plt.tight_layout()\n",
        "plt.savefig(os.path.join(figures_dir, \"shap_top10_per_model.png\"), dpi=150, bbox_inches=\"tight\")\n",
        "plt.close()\n",
        "print(\"[INFO] Saved: shap_top10_per_model.png\")\n",
        "\n",
        "# 3) Plot 2: Feature importance across all models (heatmap)\n",
        "# Create a matrix of top features x models\n",
        "all_features = set()\n",
        "for df in shap_data.values():\n",
        "    all_features.update(df[\"feature\"].head(15).values)\n",
        "all_features = sorted(list(all_features))\n",
        "\n",
        "importance_matrix = []\n",
        "for model_name in sorted(shap_data.keys()):\n",
        "    df = shap_data[model_name]\n",
        "    feature_dict = dict(zip(df[\"feature\"], df[\"mean_abs_shap\"]))\n",
        "    importance_matrix.append([feature_dict.get(f, 0.0) for f in all_features])\n",
        "\n",
        "importance_matrix = np.array(importance_matrix)\n",
        "# Normalize by column for better visibility\n",
        "importance_matrix_norm = importance_matrix / (importance_matrix.max(axis=0) + 1e-6)\n",
        "\n",
        "fig, ax = plt.subplots(figsize=(14, 8))\n",
        "sns.heatmap(importance_matrix_norm, \n",
        "            xticklabels=all_features, \n",
        "            yticklabels=sorted(shap_data.keys()),\n",
        "            cmap=\"YlOrRd\", \n",
        "            ax=ax, \n",
        "            cbar_kws={\"label\": \"Normalized Mean |SHAP|\"})\n",
        "ax.set_title(\"SHAP Feature Importance Across Models (Normalized)\")\n",
        "plt.xticks(rotation=45, ha=\"right\")\n",
        "plt.tight_layout()\n",
        "plt.savefig(os.path.join(figures_dir, \"shap_feature_heatmap.png\"), dpi=150, bbox_inches=\"tight\")\n",
        "plt.close()\n",
        "print(\"[INFO] Saved: shap_feature_heatmap.png\")\n",
        "\n",
        "# 4) Plot 3: Top 15 features globally (aggregated across all models)\n",
        "all_model_features = pd.concat([df.assign(model=name) for name, df in shap_data.items()])\n",
        "global_importance = all_model_features.groupby(\"feature\")[\"mean_abs_shap\"].mean().sort_values(ascending=False).head(15)\n",
        "\n",
        "fig, ax = plt.subplots(figsize=(10, 6))\n",
        "ax.barh(range(len(global_importance)), global_importance.values, color=\"coral\")\n",
        "ax.set_yticks(range(len(global_importance)))\n",
        "ax.set_yticklabels(global_importance.index, fontsize=10)\n",
        "ax.set_xlabel(\"Mean |SHAP| (Aggregated Across Models)\")\n",
        "ax.set_title(\"Top 15 Most Important Features (Globally)\")\n",
        "ax.invert_yaxis()\n",
        "plt.tight_layout()\n",
        "plt.savefig(os.path.join(figures_dir, \"shap_global_top15.png\"), dpi=150, bbox_inches=\"tight\")\n",
        "plt.close()\n",
        "print(\"[INFO] Saved: shap_global_top15.png\")\n",
        "\n",
        "# 5) Plot 4: Hurdle models comparison (Occurrence vs Quantity)\n",
        "hurdle_models = [m for m in shap_data.keys() if \"hurdle\" in m.lower()]\n",
        "if len(hurdle_models) > 0:\n",
        "    fig, axes = plt.subplots(1, len(hurdle_models), figsize=(5*len(hurdle_models), 6))\n",
        "    if len(hurdle_models) == 1:\n",
        "        axes = [axes]\n",
        "    \n",
        "    for idx, model_name in enumerate(sorted(hurdle_models)):\n",
        "        ax = axes[idx]\n",
        "        df_top = shap_data[model_name].head(12)\n",
        "        colors = [\"green\" if \"occ\" in model_name else \"purple\" for _ in df_top]\n",
        "        ax.barh(range(len(df_top)), df_top[\"mean_abs_shap\"].values, color=colors, alpha=0.7)\n",
        "        ax.set_yticks(range(len(df_top)))\n",
        "        ax.set_yticklabels(df_top[\"feature\"].values, fontsize=9)\n",
        "        ax.set_xlabel(\"Mean |SHAP|\")\n",
        "        ax.set_title(f\"{model_name}\")\n",
        "        ax.invert_yaxis()\n",
        "    \n",
        "    plt.tight_layout()\n",
        "    plt.savefig(os.path.join(figures_dir, \"shap_hurdle_comparison.png\"), dpi=150, bbox_inches=\"tight\")\n",
        "    plt.close()\n",
        "    print(\"[INFO] Saved: shap_hurdle_comparison.png\")\n",
        "\n",
        "# 6) Plot 5: Base vs Full feature sets (LightGBM vs +SHOS)\n",
        "if \"LightGBM\" in shap_data and \"+SHOS\" in shap_data:\n",
        "    fig, axes = plt.subplots(1, 2, figsize=(14, 6))\n",
        "    \n",
        "    # LightGBM (base features)\n",
        "    df_base = shap_data[\"LightGBM\"].head(12)\n",
        "    axes[0].barh(range(len(df_base)), df_base[\"mean_abs_shap\"].values, color=\"skyblue\")\n",
        "    axes[0].set_yticks(range(len(df_base)))\n",
        "    axes[0].set_yticklabels(df_base[\"feature\"].values, fontsize=9)\n",
        "    axes[0].set_xlabel(\"Mean |SHAP|\")\n",
        "    axes[0].set_title(\"LightGBM (Base Features)\")\n",
        "    axes[0].invert_yaxis()\n",
        "    \n",
        "    # +SHOS (full features)\n",
        "    df_full = shap_data[\"+SHOS\"].head(12)\n",
        "    axes[1].barh(range(len(df_full)), df_full[\"mean_abs_shap\"].values, color=\"orange\")\n",
        "    axes[1].set_yticks(range(len(df_full)))\n",
        "    axes[1].set_yticklabels(df_full[\"feature\"].values, fontsize=9)\n",
        "    axes[1].set_xlabel(\"Mean |SHAP|\")\n",
        "    axes[1].set_title(\"+SHOS (Full Features with SHOS)\")\n",
        "    axes[1].invert_yaxis()\n",
        "    \n",
        "    plt.tight_layout()\n",
        "    plt.savefig(os.path.join(figures_dir, \"shap_base_vs_full.png\"), dpi=150, bbox_inches=\"tight\")\n",
        "    plt.close()\n",
        "    print(\"[INFO] Saved: shap_base_vs_full.png\")\n",
        "\n",
        "# 7) Summary statistics table\n",
        "print(\"\\n=== SHAP Feature Importance Summary ===\")\n",
        "for model_name in sorted(shap_data.keys()):\n",
        "    df = shap_data[model_name]\n",
        "    top3 = df.head(3)[[\"feature\", \"mean_abs_shap\"]].values\n",
        "    print(f\"\\n{model_name}:\")\n",
        "    for feat, shap_val in top3:\n",
        "        print(f\"  - {feat}: {shap_val:.6f}\")\n",
        "\n",
        "# 8) Save aggregated summary CSV\n",
        "global_importance.to_csv(os.path.join(outdir, \"shap_global_importance.csv\"))\n",
        "print(\"\\n[INFO] Saved: shap_global_importance.csv\")\n",
        "\n",
        "print(f\"\\n[INFO] All SHAP visualizations saved to: {figures_dir}\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "[INFO] Saved LightGBM Actual vs Predicted to: ./shap_results\\figures_individual\\actual_vs_pred_LightGBM.png\n",
            "[INFO] Saved XGBoost Actual vs Predicted to: ./shap_results\\figures_individual\\actual_vs_pred_XGBoost.png\n",
            "[INFO] Saved ElasticNet Actual vs Predicted to: ./shap_results\\figures_individual\\actual_vs_pred_ElasticNet.png\n",
            "[INFO] Saved Ridge Actual vs Predicted to: ./shap_results\\figures_individual\\actual_vs_pred_Ridge.png\n",
            "[INFO] Saved RandomForest Actual vs Predicted to: ./shap_results\\figures_individual\\actual_vs_pred_RandomForest.png\n",
            "[INFO] Saved +SHOS Actual vs Predicted to: ./shap_results\\figures_individual\\actual_vs_pred_plusSHOS.png\n",
            "[INFO] Saved Hurdle_Baseline Actual vs Predicted to: ./shap_results\\figures_individual\\actual_vs_pred_Hurdle_Baseline.png\n",
            "[INFO] Saved Hurdle_SHOS Actual vs Predicted to: ./shap_results\\figures_individual\\actual_vs_pred_Hurdle_SHOS.png\n",
            "[INFO] Saved Pearson R bar chart to: ./shap_results\\figures_individual\\pearson_r_per_model.png\n"
          ]
        }
      ],
      "source": [
        "import os\n",
        "import numpy as np\n",
        "import matplotlib.pyplot as plt\n",
        "import seaborn as sns\n",
        "from sklearn.metrics import r2_score\n",
        "\n",
        "safe_mkdir(os.path.join(outdir, \"figures_individual\"))\n",
        "\n",
        "valid_models = [m for m in plot_models if (m in preds and preds[m] is not None)]\n",
        "if len(valid_models) == 0:\n",
        "    print(\"[INFO] No valid model predictions found for individual plotting.\")\n",
        "else:\n",
        "    for name in valid_models:\n",
        "        y_pred = preds[name]\n",
        "        y_true = y_test\n",
        "\n",
        "        # sampling for clarity\n",
        "        N = len(y_true)\n",
        "        if N > max_pts:\n",
        "            idx = np.random.RandomState(0).choice(N, max_pts, replace=False)\n",
        "            yt = y_true[idx]\n",
        "            yp = y_pred[idx]\n",
        "        else:\n",
        "            yt = y_true\n",
        "            yp = y_pred\n",
        "\n",
        "        fig, ax = plt.subplots(figsize=(6, 5))\n",
        "        ax.scatter(yt, yp, alpha=0.3, s=8)\n",
        "        # 1:1 line\n",
        "        mn = min(yt.min() if len(yt)>0 else 0, yp.min() if len(yp)>0 else 0)\n",
        "        mx = max(yt.max() if len(yt)>0 else 0, yp.max() if len(yp)>0 else 0)\n",
        "        ax.plot([mn, mx], [mn, mx], 'r--', linewidth=1)\n",
        "\n",
        "        # optional best-fit line\n",
        "        try:\n",
        "            slope, intercept = np.polyfit(yt, yp, 1)\n",
        "            ax.plot([mn, mx], [slope*mn + intercept, slope*mx + intercept], color='C1', linewidth=1)\n",
        "        except Exception:\n",
        "            pass\n",
        "\n",
        "        # metrics\n",
        "        try:\n",
        "            r = np.corrcoef(y_true, y_pred)[0, 1]\n",
        "            if np.isnan(r):\n",
        "                r = 0.0\n",
        "        except Exception:\n",
        "            r = 0.0\n",
        "        try:\n",
        "            r2 = r2_score(y_true, y_pred)\n",
        "        except Exception:\n",
        "            r2 = float(\"nan\")\n",
        "\n",
        "        ax.set_title(f\"{name}\\nPearson R={r:.3f}, R2={r2:.3f}\")\n",
        "        ax.set_xlabel(\"Actual\")\n",
        "        ax.set_ylabel(\"Predicted\")\n",
        "        sns.despine(ax=ax)\n",
        "        plt.tight_layout()\n",
        "\n",
        "        safe_name = name.replace(\" \", \"_\").replace(\"+\", \"plus\")\n",
        "        fname = os.path.join(outdir, \"figures_individual\", f\"actual_vs_pred_{safe_name}.png\")\n",
        "        plt.savefig(fname, dpi=150, bbox_inches=\"tight\")\n",
        "        plt.close(fig)\n",
        "        print(f\"[INFO] Saved {name} Actual vs Predicted to: {fname}\")\n",
        "\n",
        "    # Pearson R bar chart (same as before)\n",
        "    r_values = []\n",
        "    for name in valid_models:\n",
        "        try:\n",
        "            r = np.corrcoef(y_test, preds[name])[0, 1]\n",
        "            if np.isnan(r):\n",
        "                r = 0.0\n",
        "        except Exception:\n",
        "            r = 0.0\n",
        "        r_values.append((name, float(r)))\n",
        "\n",
        "    names, rvals = zip(*r_values)\n",
        "    fig, ax = plt.subplots(figsize=(10, 4))\n",
        "    ax.bar(names, rvals, color=\"tab:blue\", alpha=0.8)\n",
        "    ax.set_ylabel(\"Pearson R (Actual vs Pred)\")\n",
        "    ax.set_ylim(-1, 1)\n",
        "    ax.set_xticklabels(names, rotation=45, ha=\"right\")\n",
        "    plt.tight_layout()\n",
        "    fname2 = os.path.join(outdir, \"figures_individual\", \"pearson_r_per_model.png\")\n",
        "    plt.savefig(fname2, dpi=150, bbox_inches=\"tight\")\n",
        "    plt.close(fig)\n",
        "    print(f\"[INFO] Saved Pearson R bar chart to: {fname2}\")\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Saved MAE/RMSE chart to ./my_plots_filtered\\mae_rmse_chart.png\n",
            "Saved MAPE/WMAPE chart to ./my_plots_filtered\\mape_wmape_chart.png\n",
            "Saved R² chart to ./my_plots_filtered\\r2_chart.png\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "<Figure size 1400x700 with 0 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "import matplotlib.pyplot as plt\n",
        "import pandas as pd\n",
        "import numpy as np\n",
        "import seaborn as sns\n",
        "import os\n",
        "\n",
        "def plot_performance_metrics(results_data, outdir=\"./figures\"):\n",
        "    \"\"\"\n",
        "    Generates performance charts for specific models with Error Bars (Standard Deviation).\n",
        "    Models: +SHOS, Hurdle_SHOS, Hurdle_Baseline, LightGBM, XGBoost, RandomForest, Ridge, ElasticNet\n",
        "    \n",
        "    Charts:\n",
        "    1. MAE and RMSE (Grouped Bar Chart with Error Bars)\n",
        "    2. MAPE and WMAPE (Grouped Bar Chart with Error Bars)\n",
        "    3. R-squared (Bar Chart with Error Bars)\n",
        "\n",
        "    Args:\n",
        "        results_data (pd.DataFrame or dict): DataFrame containing model results.\n",
        "                                             Must have index as model names and columns:\n",
        "                                             ['MAE_mean', 'MAE_std', 'RMSE_mean', 'RMSE_std', \n",
        "                                              'MAPE_mean', 'MAPE_std', 'WMAPE_mean', 'WMAPE_std', \n",
        "                                              'R2_mean', 'R2_std']\n",
        "        outdir (str): Directory to save figures.\n",
        "    \"\"\"\n",
        "    \n",
        "    # Ensure output directory exists\n",
        "    if not os.path.exists(outdir):\n",
        "        os.makedirs(outdir)\n",
        "\n",
        "    # Convert dict to DataFrame if necessary\n",
        "    if isinstance(results_data, dict):\n",
        "        df = pd.DataFrame(results_data).T\n",
        "    else:\n",
        "        df = results_data.copy()\n",
        "\n",
        "    # Reset index to get 'Model' column for plotting if it's in the index\n",
        "    if 'Model' not in df.columns:\n",
        "        df = df.reset_index().rename(columns={'index': 'Model'})\n",
        "\n",
        "    # --- 1. Filter for Specific Models ---\n",
        "    # Define the exact list of models you want to plot\n",
        "    target_models = [\n",
        "        \"+SHOS\", \"Hurdle_SHOS\", \"Hurdle_Baseline\", \n",
        "        \"LightGBM\", \"XGBoost\", \"RandomForest\", \n",
        "        \"Ridge\", \"ElasticNet\"\n",
        "    ]\n",
        "    \n",
        "    # Filter the DataFrame to include only these models\n",
        "    # We use set intersection to avoid errors if a model name is slightly different or missing\n",
        "    df_filtered = df[df['Model'].isin(target_models)].copy()\n",
        "    \n",
        "    # Sort the DataFrame to match your specific order (optional but nice for consistency)\n",
        "    df_filtered['Model'] = pd.Categorical(df_filtered['Model'], categories=target_models, ordered=True)\n",
        "    df_filtered = df_filtered.sort_values('Model')\n",
        "\n",
        "    if df_filtered.empty:\n",
        "        print(\"[ERROR] No matching models found in results to plot. Check model names.\")\n",
        "        return\n",
        "\n",
        "    # Set plotting style\n",
        "    sns.set(style=\"whitegrid\")\n",
        "    \n",
        "    # --- Chart 1: MAE and RMSE with Error Bars ---\n",
        "    plt.figure(figsize=(14, 7))\n",
        "    \n",
        "    # Prepare data for MAE/RMSE plot\n",
        "    # We need to handle error bars manually in seaborn or use matplotlib directly for grouped bars with errors\n",
        "    # Using matplotlib directly gives easier control over error bars in grouped plots\n",
        "    \n",
        "    x = np.arange(len(df_filtered['Model']))\n",
        "    width = 0.35\n",
        "    \n",
        "    fig, ax = plt.subplots(figsize=(12, 6))\n",
        "    \n",
        "    # Plot MAE bars\n",
        "    rects1 = ax.bar(x - width/2, df_filtered['MAE_mean'], width, \n",
        "                    yerr=df_filtered['MAE_std'], label='MAE', capsize=5, color='skyblue', edgecolor='black')\n",
        "    \n",
        "    # Plot RMSE bars\n",
        "    rects2 = ax.bar(x + width/2, df_filtered['RMSE_mean'], width, \n",
        "                    yerr=df_filtered['RMSE_std'], label='RMSE', capsize=5, color='salmon', edgecolor='black')\n",
        "    \n",
        "    ax.set_xlabel('Model', fontsize=12)\n",
        "    ax.set_ylabel('Error Value', fontsize=12)\n",
        "    ax.set_title('Absolute Error Metrics (MAE & RMSE) with Std Dev', fontsize=16)\n",
        "    ax.set_xticks(x)\n",
        "    ax.set_xticklabels(df_filtered['Model'], rotation=45, ha='right')\n",
        "    ax.legend()\n",
        "    \n",
        "    plt.tight_layout()\n",
        "    plt.savefig(os.path.join(outdir, \"mae_rmse_chart.png\"))\n",
        "    print(f\"Saved MAE/RMSE chart to {os.path.join(outdir, 'mae_rmse_chart.png')}\")\n",
        "    plt.close()\n",
        "\n",
        "    # --- Chart 2: MAPE and WMAPE with Error Bars ---\n",
        "    fig, ax = plt.subplots(figsize=(12, 6))\n",
        "    \n",
        "    # Plot MAPE bars\n",
        "    rects1 = ax.bar(x - width/2, df_filtered['MAPE_mean'], width, \n",
        "                    yerr=df_filtered['MAPE_std'], label='MAPE', capsize=5, color='lightgreen', edgecolor='black')\n",
        "    \n",
        "    # Plot WMAPE bars\n",
        "    rects2 = ax.bar(x + width/2, df_filtered['WMAPE_mean'], width, \n",
        "                    yerr=df_filtered['WMAPE_std'], label='WMAPE', capsize=5, color='gold', edgecolor='black')\n",
        "    \n",
        "    ax.set_xlabel('Model', fontsize=12)\n",
        "    ax.set_ylabel('Percentage (%)', fontsize=12)\n",
        "    ax.set_title('Percentage Error Metrics (MAPE & WMAPE) with Std Dev', fontsize=16)\n",
        "    ax.set_xticks(x)\n",
        "    ax.set_xticklabels(df_filtered['Model'], rotation=45, ha='right')\n",
        "    ax.legend()\n",
        "    \n",
        "    plt.tight_layout()\n",
        "    plt.savefig(os.path.join(outdir, \"mape_wmape_chart.png\"))\n",
        "    print(f\"Saved MAPE/WMAPE chart to {os.path.join(outdir, 'mape_wmape_chart.png')}\")\n",
        "    plt.close()\n",
        "\n",
        "    # --- Chart 3: R-squared (R2) with Error Bars ---\n",
        "    plt.figure(figsize=(10, 6))\n",
        "    \n",
        "    # Use seaborn for single metric bar chart, it handles error bars (ci='sd') if we had raw data, \n",
        "    # but since we have pre-aggregated mean/std, we use matplotlib bar with yerr\n",
        "    \n",
        "    bars = plt.bar(df_filtered['Model'], df_filtered['R2_mean'], \n",
        "                   yerr=df_filtered['R2_std'], capsize=5, \n",
        "                   color='cornflowerblue', edgecolor='black', width=0.6)\n",
        "    \n",
        "    plt.title('R-Squared (R²) Score with Std Dev', fontsize=16)\n",
        "    plt.ylabel('R² Score', fontsize=12)\n",
        "    plt.xlabel('Model', fontsize=12)\n",
        "    plt.xticks(rotation=45, ha='right')\n",
        "    plt.ylim(bottom=min(df_filtered['R2_mean'].min(), 0) - 0.1, top=1.05) # Adjust limits\n",
        "    \n",
        "    # Add labels\n",
        "    # ax.bar_label(bars, fmt='%.2f') # Simple labels\n",
        "    \n",
        "    plt.tight_layout()\n",
        "    plt.savefig(os.path.join(outdir, \"r2_chart.png\"))\n",
        "    print(f\"Saved R² chart to {os.path.join(outdir, 'r2_chart.png')}\")\n",
        "    plt.close()\n",
        "\n",
        "# --- Example Usage with Mock Data ---\n",
        "if __name__ == \"__main__\":\n",
        "    # Mock data reflecting your structure with Standard Deviations (randomly generated for demo)\n",
        "    data = {\n",
        "        'Model': ['LightGBM', 'XGBoost', 'ElasticNet', 'Ridge', 'RandomForest', '+SHOS', 'Hurdle_SHOS', 'Hurdle_Baseline'],\n",
        "        'MAE_mean': [0.1326, 0.1413, 0.3439, 0.3439, 0.1364, 0.0712, 0.0701, 0.1341],\n",
        "        'MAE_std': [0.0046, 0.0045, 0.0134, 0.0134, 0.0046, 0.0028, 0.0023, 0.0050], # Example SDs\n",
        "        'RMSE_mean': [1.2900, 1.3960, 1.5101, 1.5101, 1.3411, 1.0310, 1.1136, 1.2911],\n",
        "        'RMSE_std': [0.1079, 0.1074, 0.1160, 0.1161, 0.1057, 0.1416, 0.1102, 0.0934],\n",
        "        'MAPE_mean': [38.25, 40.41, 69.99, 69.98, 40.27, 17.72, 16.84, 37.91],\n",
        "        'MAPE_std': [1.20, 1.37, 0.61, 0.60, 1.53, 0.47, 0.44, 1.29],\n",
        "        'WMAPE_mean': [37.99, 40.49, 98.59, 98.57, 39.08, 20.39, 20.09, 38.41],\n",
        "        'WMAPE_std': [0.67, 1.26, 4.26, 4.27, 1.02, 0.57, 0.47, 0.71],\n",
        "        'R2_mean': [0.7578, 0.7156, 0.6685, 0.6685, 0.7377, 0.8430, 0.8186, 0.7577],\n",
        "        'R2_std': [0.0418, 0.0494, 0.0535, 0.0535, 0.0484, 0.0460, 0.0413, 0.0379]\n",
        "    }\n",
        "    \n",
        "    results_df = pd.DataFrame(data)\n",
        "    results_df.set_index('Model', inplace=True) \n",
        "\n",
        "    # Call the function\n",
        "    plot_performance_metrics(results_df, outdir=\"./my_plots_filtered\")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Saved MAE/RMSE chart to ./my_plots_filtered\\mae_rmse_chart.png\n",
            "Saved MAPE/WMAPE chart to ./my_plots_filtered\\mape_wmape_chart.png\n",
            "Saved R² chart to ./my_plots_filtered\\r2_chart.png\n",
            "Saved Overfitting chart to ./my_plots_filtered\\overfitting_analysis.png\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "<Figure size 1400x700 with 0 Axes>"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "import matplotlib.pyplot as plt\n",
        "import pandas as pd\n",
        "import numpy as np\n",
        "import seaborn as sns\n",
        "import os\n",
        "\n",
        "def plot_performance_metrics(results_data, overfitting_data=None, outdir=\"./figures\"):\n",
        "    \"\"\"\n",
        "    Generates performance charts for specific models with Error Bars (Standard Deviation).\n",
        "    Models: +SHOS, Hurdle_SHOS, Hurdle_Baseline, LightGBM, XGBoost, RandomForest, Ridge, ElasticNet\n",
        "    \n",
        "    Charts:\n",
        "    1. MAE and RMSE (Grouped Bar Chart with Error Bars)\n",
        "    2. MAPE and WMAPE (Grouped Bar Chart with Error Bars)\n",
        "    3. R-squared (Bar Chart with Error Bars)\n",
        "    4. Overfitting Analysis (Train vs Test MAE Gap) - IF overfitting_data is provided\n",
        "\n",
        "    Args:\n",
        "        results_data (pd.DataFrame or dict): DataFrame containing model results.\n",
        "                                             Must have index as model names and columns:\n",
        "                                             ['MAE_mean', 'MAE_std', 'RMSE_mean', 'RMSE_std', \n",
        "                                              'MAPE_mean', 'MAPE_std', 'WMAPE_mean', 'WMAPE_std', \n",
        "                                              'R2_mean', 'R2_std']\n",
        "        overfitting_data (pd.DataFrame, optional): DataFrame with columns ['Train_MAE', 'Test_MAE']\n",
        "        outdir (str): Directory to save figures.\n",
        "    \"\"\"\n",
        "    \n",
        "    # Ensure output directory exists\n",
        "    if not os.path.exists(outdir):\n",
        "        os.makedirs(outdir)\n",
        "\n",
        "    # Convert dict to DataFrame if necessary\n",
        "    if isinstance(results_data, dict):\n",
        "        df = pd.DataFrame(results_data).T\n",
        "    else:\n",
        "        df = results_data.copy()\n",
        "\n",
        "    # Reset index to get 'Model' column for plotting if it's in the index\n",
        "    if 'Model' not in df.columns:\n",
        "        df = df.reset_index().rename(columns={'index': 'Model'})\n",
        "\n",
        "    # --- 1. Filter for Specific Models ---\n",
        "    # Define the exact list of models you want to plot\n",
        "    target_models = [\n",
        "        \"+SHOS\", \"Hurdle_SHOS\", \"Hurdle_Baseline\", \n",
        "        \"LightGBM\", \"XGBoost\", \"RandomForest\", \n",
        "        \"Ridge\", \"ElasticNet\"\n",
        "    ]\n",
        "    \n",
        "    # Filter the DataFrame to include only these models\n",
        "    df_filtered = df[df['Model'].isin(target_models)].copy()\n",
        "    \n",
        "    # Sort the DataFrame to match your specific order\n",
        "    df_filtered['Model'] = pd.Categorical(df_filtered['Model'], categories=target_models, ordered=True)\n",
        "    df_filtered = df_filtered.sort_values('Model')\n",
        "\n",
        "    if df_filtered.empty:\n",
        "        print(\"[ERROR] No matching models found in results to plot. Check model names.\")\n",
        "        return\n",
        "\n",
        "    # Set plotting style\n",
        "    sns.set(style=\"whitegrid\")\n",
        "    \n",
        "    # --- Chart 1: MAE and RMSE with Error Bars ---\n",
        "    plt.figure(figsize=(14, 7))\n",
        "    x = np.arange(len(df_filtered['Model']))\n",
        "    width = 0.35\n",
        "    \n",
        "    fig, ax = plt.subplots(figsize=(12, 6))\n",
        "    \n",
        "    # Plot MAE bars\n",
        "    rects1 = ax.bar(x - width/2, df_filtered['MAE_mean'], width, \n",
        "                    yerr=df_filtered['MAE_std'], label='MAE', capsize=5, color='skyblue', edgecolor='black')\n",
        "    \n",
        "    # Plot RMSE bars\n",
        "    rects2 = ax.bar(x + width/2, df_filtered['RMSE_mean'], width, \n",
        "                    yerr=df_filtered['RMSE_std'], label='RMSE', capsize=5, color='salmon', edgecolor='black')\n",
        "    \n",
        "    ax.set_xlabel('Model', fontsize=12)\n",
        "    ax.set_ylabel('Error Value', fontsize=12)\n",
        "    ax.set_title('Absolute Error Metrics (MAE & RMSE) with Std Dev', fontsize=16)\n",
        "    ax.set_xticks(x)\n",
        "    ax.set_xticklabels(df_filtered['Model'], rotation=45, ha='right')\n",
        "    ax.legend()\n",
        "    \n",
        "    plt.tight_layout()\n",
        "    plt.savefig(os.path.join(outdir, \"mae_rmse_chart.png\"))\n",
        "    print(f\"Saved MAE/RMSE chart to {os.path.join(outdir, 'mae_rmse_chart.png')}\")\n",
        "    plt.close()\n",
        "\n",
        "    # --- Chart 2: MAPE and WMAPE with Error Bars ---\n",
        "    fig, ax = plt.subplots(figsize=(12, 6))\n",
        "    \n",
        "    # Plot MAPE bars\n",
        "    rects1 = ax.bar(x - width/2, df_filtered['MAPE_mean'], width, \n",
        "                    yerr=df_filtered['MAPE_std'], label='MAPE', capsize=5, color='lightgreen', edgecolor='black')\n",
        "    \n",
        "    # Plot WMAPE bars\n",
        "    rects2 = ax.bar(x + width/2, df_filtered['WMAPE_mean'], width, \n",
        "                    yerr=df_filtered['WMAPE_std'], label='WMAPE', capsize=5, color='gold', edgecolor='black')\n",
        "    \n",
        "    ax.set_xlabel('Model', fontsize=12)\n",
        "    ax.set_ylabel('Percentage (%)', fontsize=12)\n",
        "    ax.set_title('Percentage Error Metrics (MAPE & WMAPE) with Std Dev', fontsize=16)\n",
        "    ax.set_xticks(x)\n",
        "    ax.set_xticklabels(df_filtered['Model'], rotation=45, ha='right')\n",
        "    ax.legend()\n",
        "    \n",
        "    plt.tight_layout()\n",
        "    plt.savefig(os.path.join(outdir, \"mape_wmape_chart.png\"))\n",
        "    print(f\"Saved MAPE/WMAPE chart to {os.path.join(outdir, 'mape_wmape_chart.png')}\")\n",
        "    plt.close()\n",
        "\n",
        "    # --- Chart 3: R-squared (R2) with Error Bars ---\n",
        "    plt.figure(figsize=(10, 6))\n",
        "    \n",
        "    bars = plt.bar(df_filtered['Model'], df_filtered['R2_mean'], \n",
        "                   yerr=df_filtered['R2_std'], capsize=5, \n",
        "                   color='cornflowerblue', edgecolor='black', width=0.6)\n",
        "    \n",
        "    plt.title('R-Squared (R²) Score with Std Dev', fontsize=16)\n",
        "    plt.ylabel('R² Score', fontsize=12)\n",
        "    plt.xlabel('Model', fontsize=12)\n",
        "    plt.xticks(rotation=45, ha='right')\n",
        "    plt.ylim(bottom=min(df_filtered['R2_mean'].min(), 0) - 0.1, top=1.05) \n",
        "    \n",
        "    plt.tight_layout()\n",
        "    plt.savefig(os.path.join(outdir, \"r2_chart.png\"))\n",
        "    print(f\"Saved R² chart to {os.path.join(outdir, 'r2_chart.png')}\")\n",
        "    plt.close()\n",
        "\n",
        "    # --- Chart 4: Overfitting Analysis (Train vs Test MAE) ---\n",
        "    if overfitting_data is not None:\n",
        "        # Filter overfitting data to match target models\n",
        "        ov_df = overfitting_data.copy()\n",
        "        if 'Model' not in ov_df.columns:\n",
        "             ov_df = ov_df.reset_index().rename(columns={'index': 'Model'})\n",
        "        \n",
        "        ov_df = ov_df[ov_df['Model'].isin(target_models)].copy()\n",
        "        ov_df['Model'] = pd.Categorical(ov_df['Model'], categories=target_models, ordered=True)\n",
        "        ov_df = ov_df.sort_values('Model')\n",
        "        \n",
        "        fig, ax = plt.subplots(figsize=(12, 6))\n",
        "        x = np.arange(len(ov_df['Model']))\n",
        "        width = 0.35\n",
        "        \n",
        "        # Plot Train MAE\n",
        "        rects1 = ax.bar(x - width/2, ov_df['Train_MAE'], width, label='Train MAE', color='lightgray', edgecolor='black')\n",
        "        # Plot Test MAE\n",
        "        rects2 = ax.bar(x + width/2, ov_df['Test_MAE'], width, label='Test MAE', color='firebrick', edgecolor='black')\n",
        "        \n",
        "        ax.set_ylabel('MAE', fontsize=12)\n",
        "        ax.set_title('Overfitting Diagnosis: Train vs Test MAE', fontsize=16)\n",
        "        ax.set_xticks(x)\n",
        "        ax.set_xticklabels(ov_df['Model'], rotation=45, ha='right')\n",
        "        ax.legend()\n",
        "        \n",
        "        # Highlight the gap\n",
        "        for i in range(len(x)):\n",
        "            gap = ov_df['Test_MAE'].iloc[i] - ov_df['Train_MAE'].iloc[i]\n",
        "            if gap > 0.05:  # Only annotate significant gaps\n",
        "                ax.annotate(f'+{gap:.2f}', \n",
        "                            xy=(x[i], max(ov_df['Test_MAE'].iloc[i], ov_df['Train_MAE'].iloc[i])),\n",
        "                            xytext=(0, 3), textcoords=\"offset points\",\n",
        "                            ha='center', va='bottom', fontsize=9, color='red')\n",
        "\n",
        "        plt.tight_layout()\n",
        "        plt.savefig(os.path.join(outdir, \"overfitting_analysis.png\"))\n",
        "        print(f\"Saved Overfitting chart to {os.path.join(outdir, 'overfitting_analysis.png')}\")\n",
        "        plt.close()\n",
        "\n",
        "# --- Example Usage ---\n",
        "if __name__ == \"__main__\":\n",
        "    # Mock Results Data (Replace with your actual results)\n",
        "    results_data = {\n",
        "        'Model': ['LightGBM', 'XGBoost', 'ElasticNet', 'Ridge', 'RandomForest', '+SHOS', 'Hurdle_SHOS', 'Hurdle_Baseline'],\n",
        "        'MAE_mean': [0.1326, 0.1413, 0.3439, 0.3439, 0.1364, 0.0712, 0.0701, 0.1341],\n",
        "        'MAE_std': [0.0046, 0.0045, 0.0134, 0.0134, 0.0046, 0.0028, 0.0023, 0.0050], \n",
        "        'RMSE_mean': [1.2900, 1.3960, 1.5101, 1.5101, 1.3411, 1.0310, 1.1136, 1.2911],\n",
        "        'RMSE_std': [0.1079, 0.1074, 0.1160, 0.1161, 0.1057, 0.1416, 0.1102, 0.0934],\n",
        "        'MAPE_mean': [38.25, 40.41, 69.99, 69.98, 40.27, 17.72, 16.84, 37.91],\n",
        "        'MAPE_std': [1.20, 1.37, 0.61, 0.60, 1.53, 0.47, 0.44, 1.29],\n",
        "        'WMAPE_mean': [37.99, 40.49, 98.59, 98.57, 39.08, 20.39, 20.09, 38.41],\n",
        "        'WMAPE_std': [0.67, 1.26, 4.26, 4.27, 1.02, 0.57, 0.47, 0.71],\n",
        "        'R2_mean': [0.7578, 0.7156, 0.6685, 0.6685, 0.7377, 0.8430, 0.8186, 0.7577],\n",
        "        'R2_std': [0.0418, 0.0494, 0.0535, 0.0535, 0.0484, 0.0460, 0.0413, 0.0379]\n",
        "    }\n",
        "    \n",
        "    # Mock Overfitting Data (Replace with your overfitting_df)\n",
        "    # Notice the larger gap for XGBoost vs +SHOS\n",
        "    overfitting_data = {\n",
        "        'Model': ['LightGBM', 'XGBoost', 'ElasticNet', 'Ridge', 'RandomForest', '+SHOS', 'Hurdle_SHOS', 'Hurdle_Baseline'],\n",
        "        'Train_MAE': [0.1077, 0.0750, 0.3446, 0.3444, 0.0759, 0.0499, 0.0367, 0.1089],\n",
        "        'Test_MAE': [0.1326, 0.1413, 0.3439, 0.3439, 0.1364, 0.0712, 0.0701, 0.1341]\n",
        "    }\n",
        "    \n",
        "    results_df = pd.DataFrame(results_data)\n",
        "    results_df.set_index('Model', inplace=True) \n",
        "    \n",
        "    overfitting_df = pd.DataFrame(overfitting_data)\n",
        "    overfitting_df.set_index('Model', inplace=True)\n",
        "\n",
        "    # Call the function\n",
        "    plot_performance_metrics(results_df, overfitting_df, outdir=\"./my_plots_filtered\")"
      ]
    }
  ],
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "display_name": ".venv (3.14.0)",
      "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.14.0"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 0
}
