{
 "cells": [
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "# 预测建模\n",
    "北京市空气质量指数预测（推荐难度系数10）\n",
    "\n",
    "这个数据集是北京市2022年11月1日至2023年10月31日期间空气质量相关数据。\n",
    "根据这个数据集，回答以下问题"
   ],
   "id": "b610f839dca4877"
  },
  {
   "metadata": {
    "collapsed": true,
    "ExecuteTime": {
     "end_time": "2025-03-26T02:28:36.097058Z",
     "start_time": "2025-03-26T02:28:35.825206Z"
    }
   },
   "cell_type": "code",
   "source": [
    "import os\n",
    "import sys\n",
    "\n",
    "#导入基础包\n",
    "import pandas as pd\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "from statsmodels.graphics.tsaplots import plot_acf\n",
    "import matplotlib.font_manager as fm\n",
    "\n",
    "# 导入主成分分析相关包\n",
    "from factor_analyzer import Rotator\n",
    "from factor_analyzer.factor_analyzer import calculate_bartlett_sphericity, calculate_kmo\n",
    "\n",
    "# 导入SARIMA相关包\n",
    "from statsmodels.tsa.statespace.sarimax import SARIMAX\n",
    "from sklearn.metrics import mean_absolute_error, mean_squared_error\n",
    "from pmdarima import auto_arima\n",
    "import pmdarima as pm\n",
    "\n",
    "# 导入XGBOOST相关包\n",
    "from xgboost import XGBRegressor\n",
    "from scipy.stats import randint, uniform\n",
    "from sklearn.model_selection import RandomizedSearchCV\n",
    "from matplotlib.dates import DateFormatter, HourLocator\n",
    "\n",
    "# 导入单独写的函数\n",
    "from calculate import *\n",
    "from heatmap import *\n",
    "from sort_matrix import *"
   ],
   "id": "initial_id",
   "outputs": [],
   "execution_count": 36
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-26T02:28:36.900743Z",
     "start_time": "2025-03-26T02:28:36.202964Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# 设置字体\n",
    "if sys.platform == 'darwin':  # macOS\n",
    "    font_path = '/System/Library/Fonts/STHeiti Light.ttc'\n",
    "elif sys.platform == 'win32': # Windows\n",
    "    plt.rcParams['font.sans-serif'] = ['SimHei']  # Windows系统自带黑体\n",
    "    plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题\n",
    "else: # Linux/其他系统\n",
    "    font_path = '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc'  # 文泉驿字体\n",
    "\n",
    "# 仅非Windows系统需要加载字体文件\n",
    "if sys.platform != 'win32':\n",
    "    try:\n",
    "        font_prop = fm.FontProperties(fname=font_path)\n",
    "        plt.rcParams['font.family'] = font_prop.get_name()\n",
    "    except:\n",
    "        print(f\"警告：{font_path} 字体加载失败，请检查路径有效性\")\n",
    "\n",
    "try:\n",
    "    os.mkdir('./images')\n",
    "except FileExistsError:\n",
    "    pass\n",
    "try:\n",
    "    os.mkdir('./results')\n",
    "except FileExistsError:\n",
    "    pass\n",
    "#读取数据\n",
    "data=pd.read_excel('北京市空气质量指数与气象数据.xlsx')\n",
    "data.head()"
   ],
   "id": "92ea7ba1218799cd",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "        date  hour        AQI        CO        NO2         O3       PM10  \\\n",
       "0 2022-11-01     2  18.371429  0.211429  23.771429  29.057143  13.257143   \n",
       "1 2022-11-01     5  21.914286  0.180000  26.571429  20.142857  18.914286   \n",
       "2 2022-11-01     8  28.628571  0.311429  30.028571  14.285714  27.942857   \n",
       "3 2022-11-01    11  19.000000  0.237143  17.971429  40.529412  17.852941   \n",
       "4 2022-11-01    14  21.742857  0.252941  15.588235  53.617647  20.941176   \n",
       "\n",
       "      PM2.5       SO2     T  ...      P   Pa     U   Ff   Tn    Tx    VV   Td  \\\n",
       "0  3.057143  2.628571   6.7  ...  770.5  0.1  36.0  1.0  5.3  17.3  30.0 -7.3   \n",
       "1  3.771429  2.542857   2.0  ...  770.8  0.3  62.0  0.0  1.9  17.3   7.0 -4.5   \n",
       "2  6.857143  2.400000   6.6  ...  771.7  0.9  56.0  0.0  0.9  17.3  10.0 -7.1   \n",
       "3  5.914286  2.176471  13.5  ...  771.3 -0.4  19.0  2.0  0.9  17.3  30.0 -9.7   \n",
       "4  6.742857  2.000000  15.7  ...  768.6 -2.7  19.0  2.0  0.9  17.3  30.0 -7.9   \n",
       "\n",
       "   RRR  tR  \n",
       "0  0.0  12  \n",
       "1  0.0  12  \n",
       "2  0.0  12  \n",
       "3  0.0  12  \n",
       "4  0.0  12  \n",
       "\n",
       "[5 rows x 21 columns]"
      ],
      "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>date</th>\n",
       "      <th>hour</th>\n",
       "      <th>AQI</th>\n",
       "      <th>CO</th>\n",
       "      <th>NO2</th>\n",
       "      <th>O3</th>\n",
       "      <th>PM10</th>\n",
       "      <th>PM2.5</th>\n",
       "      <th>SO2</th>\n",
       "      <th>T</th>\n",
       "      <th>...</th>\n",
       "      <th>P</th>\n",
       "      <th>Pa</th>\n",
       "      <th>U</th>\n",
       "      <th>Ff</th>\n",
       "      <th>Tn</th>\n",
       "      <th>Tx</th>\n",
       "      <th>VV</th>\n",
       "      <th>Td</th>\n",
       "      <th>RRR</th>\n",
       "      <th>tR</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2022-11-01</td>\n",
       "      <td>2</td>\n",
       "      <td>18.371429</td>\n",
       "      <td>0.211429</td>\n",
       "      <td>23.771429</td>\n",
       "      <td>29.057143</td>\n",
       "      <td>13.257143</td>\n",
       "      <td>3.057143</td>\n",
       "      <td>2.628571</td>\n",
       "      <td>6.7</td>\n",
       "      <td>...</td>\n",
       "      <td>770.5</td>\n",
       "      <td>0.1</td>\n",
       "      <td>36.0</td>\n",
       "      <td>1.0</td>\n",
       "      <td>5.3</td>\n",
       "      <td>17.3</td>\n",
       "      <td>30.0</td>\n",
       "      <td>-7.3</td>\n",
       "      <td>0.0</td>\n",
       "      <td>12</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2022-11-01</td>\n",
       "      <td>5</td>\n",
       "      <td>21.914286</td>\n",
       "      <td>0.180000</td>\n",
       "      <td>26.571429</td>\n",
       "      <td>20.142857</td>\n",
       "      <td>18.914286</td>\n",
       "      <td>3.771429</td>\n",
       "      <td>2.542857</td>\n",
       "      <td>2.0</td>\n",
       "      <td>...</td>\n",
       "      <td>770.8</td>\n",
       "      <td>0.3</td>\n",
       "      <td>62.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>1.9</td>\n",
       "      <td>17.3</td>\n",
       "      <td>7.0</td>\n",
       "      <td>-4.5</td>\n",
       "      <td>0.0</td>\n",
       "      <td>12</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2022-11-01</td>\n",
       "      <td>8</td>\n",
       "      <td>28.628571</td>\n",
       "      <td>0.311429</td>\n",
       "      <td>30.028571</td>\n",
       "      <td>14.285714</td>\n",
       "      <td>27.942857</td>\n",
       "      <td>6.857143</td>\n",
       "      <td>2.400000</td>\n",
       "      <td>6.6</td>\n",
       "      <td>...</td>\n",
       "      <td>771.7</td>\n",
       "      <td>0.9</td>\n",
       "      <td>56.0</td>\n",
       "      <td>0.0</td>\n",
       "      <td>0.9</td>\n",
       "      <td>17.3</td>\n",
       "      <td>10.0</td>\n",
       "      <td>-7.1</td>\n",
       "      <td>0.0</td>\n",
       "      <td>12</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>2022-11-01</td>\n",
       "      <td>11</td>\n",
       "      <td>19.000000</td>\n",
       "      <td>0.237143</td>\n",
       "      <td>17.971429</td>\n",
       "      <td>40.529412</td>\n",
       "      <td>17.852941</td>\n",
       "      <td>5.914286</td>\n",
       "      <td>2.176471</td>\n",
       "      <td>13.5</td>\n",
       "      <td>...</td>\n",
       "      <td>771.3</td>\n",
       "      <td>-0.4</td>\n",
       "      <td>19.0</td>\n",
       "      <td>2.0</td>\n",
       "      <td>0.9</td>\n",
       "      <td>17.3</td>\n",
       "      <td>30.0</td>\n",
       "      <td>-9.7</td>\n",
       "      <td>0.0</td>\n",
       "      <td>12</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2022-11-01</td>\n",
       "      <td>14</td>\n",
       "      <td>21.742857</td>\n",
       "      <td>0.252941</td>\n",
       "      <td>15.588235</td>\n",
       "      <td>53.617647</td>\n",
       "      <td>20.941176</td>\n",
       "      <td>6.742857</td>\n",
       "      <td>2.000000</td>\n",
       "      <td>15.7</td>\n",
       "      <td>...</td>\n",
       "      <td>768.6</td>\n",
       "      <td>-2.7</td>\n",
       "      <td>19.0</td>\n",
       "      <td>2.0</td>\n",
       "      <td>0.9</td>\n",
       "      <td>17.3</td>\n",
       "      <td>30.0</td>\n",
       "      <td>-7.9</td>\n",
       "      <td>0.0</td>\n",
       "      <td>12</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "<p>5 rows × 21 columns</p>\n",
       "</div>"
      ]
     },
     "execution_count": 37,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 37
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "## 题目1\n",
    "研究单日内空气质量指数与各项指标的变化趋势，这种趋势是否具有周期性？"
   ],
   "id": "bca65e544d8bef55"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-26T02:28:37.148177Z",
     "start_time": "2025-03-26T02:28:37.014883Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# 数据预处理：将数据按小时分组，计算每个小时各指标的平均值\n",
    "# 转换Excel日期序列值为实际日期并分组\n",
    "data['datetime'] = pd.to_datetime(data['date']) + pd.to_timedelta(data['hour'], unit='h')\n",
    "valid_hours = sorted(data['hour'].unique())\n",
    "hourly_data = data.groupby('hour').mean().loc[valid_hours]\n",
    "plt.figure(figsize=(12, 8))\n",
    "indicators = ['AQI', 'PM2.5', 'PM10', 'CO', 'NO2', 'O3','SO2']\n",
    "colors = ['#2d87bb', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf', '#1f77b4', '#ffbb78', '#98df8a', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf', '#1f77b4', '#ffbb78', '#98df8a', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf', '#1f77b4', '#ffbb78', '#98df8a', '#d62728',]\n",
    "\n",
    "normalized = (hourly_data[indicators] - hourly_data[indicators].mean(axis=0)) / hourly_data[indicators].std(axis=0)"
   ],
   "id": "118b1b48e798a7ba",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 1200x800 with 0 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 38
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-26T02:28:46.540048Z",
     "start_time": "2025-03-26T02:28:37.257554Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# 绘制各指标小时均值变化趋势（标准化后）折线图\n",
    "for i, indicator in enumerate(indicators):\n",
    "    plt.plot(normalized.index, normalized[indicator], \n",
    "             marker='o',label=indicator, color=colors[i], linewidth=2)\n",
    "\n",
    "plt.title('各指标小时均值变化趋势（标准化后）', fontsize=14)\n",
    "plt.xlabel('小时', fontsize=12)\n",
    "plt.ylabel('标准化值', fontsize=12)\n",
    "plt.xticks(range(0, 24))\n",
    "plt.grid(alpha=0.3)\n",
    "plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')\n",
    "plt.tight_layout()\n",
    "\n",
    "# 新增保存代码\n",
    "plt.savefig('images/hourly_trends_combined.png', dpi=300, bbox_inches='tight')  # 保存组合大图\n",
    "plt.show()\n",
    "\n",
    "# 新增保存子图代码\n",
    "for i, indicator in enumerate(indicators):\n",
    "    plt.figure(figsize=(8, 5))\n",
    "    plt.plot(normalized.index, normalized[indicator], \n",
    "             marker='o', color=colors[i], linewidth=2)\n",
    "    plt.title(f'{indicator}小时均值变化趋势（标准化后）')\n",
    "    plt.xlabel('小时')\n",
    "    plt.ylabel('标准化值')\n",
    "    plt.xticks(range(0, 24))\n",
    "    plt.grid(alpha=0.3)\n",
    "    plt.tight_layout()\n",
    "    plt.savefig(f'images/hourly_{indicator}.png', dpi=300)  # 保存单个指标子图\n",
    "    plt.close()"
   ],
   "id": "57dedbd9b7bbe12d",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ],
      "image/png": "[encoded data, 125960 characters]"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 39
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-26T02:28:55.021371Z",
     "start_time": "2025-03-26T02:28:46.711520Z"
    }
   },
   "cell_type": "code",
   "source": [
    "# ACF检验周期性\n",
    "# 创建完整时间序列（每小时一个样本，缺失值用线性插值）\n",
    "full_idx = pd.date_range(start=data['datetime'].min(),\n",
    "                         end=data['datetime'].max(),\n",
    "                         freq='h')\n",
    "full_series = data.set_index('datetime').reindex(full_idx)\n",
    "interpolated = full_series[indicators].interpolate(method='time')\n",
    "\n",
    "# 绘制ACF图，检验3天周期（24*3）\n",
    "plt.figure(figsize=(60, 20))  # 调整整体画布尺寸\n",
    "for i, indicator in enumerate(indicators):\n",
    "    ax = plt.subplot(2, 4, i+1)  # 创建2行4列的子图布局\n",
    "    plot_acf(interpolated[indicator].dropna(),\n",
    "             lags=72,\n",
    "             alpha=0.05,\n",
    "             title=f'{indicator}',\n",
    "             color=colors[i],\n",
    "             ax=ax)\n",
    "    plt.xticks(np.arange(0, 73, 12))\n",
    "plt.tight_layout()\n",
    "plt.savefig('./images/all_acf_subplots.png', dpi=200, bbox_inches='tight')\n",
    "plt.show()\n",
    "\n",
    "for i, indicator in enumerate(indicators):\n",
    "    plt.figure(figsize=(12, 6))\n",
    "    plot_acf(interpolated[indicator].dropna(),\n",
    "             lags=72,\n",
    "             alpha=0.05,\n",
    "             title=f'{indicator} ACF',\n",
    "             color=colors[i])\n",
    "    plt.xticks(np.arange(0, 73, 12))\n",
    "    plt.savefig(f'./images/acf_{indicator}.png', dpi=200, bbox_inches='tight')\n",
    "    plt.close()\n"
   ],
   "id": "5f8e89a8d1561e4f",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 6000x2000 with 7 Axes>"
      ],
      "image/png": "[encoded data, 260224 characters]"
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<Figure size 1200x600 with 0 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<Figure size 1200x600 with 0 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<Figure size 1200x600 with 0 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<Figure size 1200x600 with 0 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<Figure size 1200x600 with 0 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<Figure size 1200x600 with 0 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<Figure size 1200x600 with 0 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 40
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "\n",
    "\n",
    "## 题目2\n",
    "简述各项指标间的相互关系。"
   ],
   "id": "59e20f3463e819a6"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-26T02:28:57.136686Z",
     "start_time": "2025-03-26T02:28:55.596922Z"
    }
   },
   "cell_type": "code",
   "source": [
    "#计算相关系数矩阵\n",
    "correlation_matrix = data.iloc[:, 1:].corr()\n",
    "#绘制热力图\n",
    "plot_heatmap(correlation_matrix,20,16,title=\"Correlation Matrix Heatmap\",save_path=\"./images/correlation_heatmap.png\")"
   ],
   "id": "c917d14115569bcd",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 2000x1600 with 2 Axes>"
      ],
      "image/png": "[encoded data, 428896 characters]"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 41
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-26T02:28:59.213730Z",
     "start_time": "2025-03-26T02:28:57.655272Z"
    }
   },
   "cell_type": "code",
   "source": [
    "#主成分分析（PCA)\n",
    "data=pd.read_excel('北京市空气质量指数与气象数据.xlsx')\n",
    "PCA_data=data.iloc[:,2:]#去除日期列\n",
    "\n",
    "# 计算KMO值\n",
    "kmo_all, kmo_model = calculate_kmo(PCA_data)\n",
    "print(f\"KMO值: {kmo_model.round(3)}\")\n",
    "# 进行巴赫利特检验\n",
    "chi_square_value, p_value = calculate_bartlett_sphericity(PCA_data)\n",
    "print(f\"巴赫利特检验卡方值: {chi_square_value.round(3)}, p值: {p_value}\")\n",
    "#判断\n",
    "if kmo_model>0.7 and p_value<0.05:\n",
    "    print(\"数据适合进行主成分分析\",'\\n')\n",
    "else:\n",
    "    print(\"数据不适合进行主成分分析\",'\\n')\n",
    "\n",
    "# 数据标准化\n",
    "scaled_data = (PCA_data - PCA_data.mean()) / PCA_data.std()\n",
    "scaled_data = scaled_data.dropna()#去除空值\n",
    "\n",
    "# 计算协方差矩阵\n",
    "cov_matrix = np.cov(scaled_data, rowvar=False)\n",
    "\n",
    "# 计算特征值和特征向量\n",
    "eigenvalues, eigenvectors = np.linalg.eigh(cov_matrix)\n",
    "sorted_indices = np.argsort(eigenvalues)[::-1]\n",
    "sorted_eigenvalues = eigenvalues[sorted_indices]\n",
    "sorted_eigenvectors = eigenvectors[:, sorted_indices]\n",
    "\n",
    "# 绘制累计方差解释比例图\n",
    "explained_variance_ratio = sorted_eigenvalues / np.sum(sorted_eigenvalues)\n",
    "cumulative_explained_variance = np.cumsum(explained_variance_ratio)\n",
    "print(\"累计方差解释比例:\", [f\"{cum * 100:.2f}%\" for cum in cumulative_explained_variance])\n",
    "\n",
    "plt.plot(range(1, len(cumulative_explained_variance) + 1), cumulative_explained_variance, marker='o')\n",
    "plt.xlabel('主成分数量')\n",
    "plt.ylabel('累计方差解释比例')\n",
    "plt.title('PCA 累计方差解释比例')\n",
    "plt.savefig('./images/PCA_cumulative_explained_variance.png', dpi=200, bbox_inches='tight')\n",
    "plt.show()\n",
    "\n",
    "# 选择特征值大于1的作为主成分\n",
    "mask = sorted_eigenvalues > 1\n",
    "selected_eigenvectors = sorted_eigenvectors[:, mask]\n",
    "\n",
    "# 计算因子载荷矩阵\n",
    "loadings = selected_eigenvectors * np.sqrt(sorted_eigenvalues[mask])\n",
    "\n",
    "# 使用Varimax旋转载荷矩阵\n",
    "rotator = Rotator(method='varimax')\n",
    "rotated_loadings = rotator.fit_transform(loadings)\n",
    "\n",
    "# 输出旋转后的成分矩阵\n",
    "rotated_components_df = pd.DataFrame(rotated_loadings,\n",
    "                                     index=PCA_data.columns,\n",
    "                                     columns=[f'Factor{i+1}' for i in range(rotated_loadings.shape[1])])\n",
    "rotated_components_df = rotated_components_df.round(3)\n",
    "\n",
    "# 输出排序后的载荷矩阵\n",
    "rotated_components_df=sort_matrix_by_diag(rotated_components_df)\n",
    "print(\"旋转后的载荷矩阵（排序后）：\\n\", rotated_components_df)\n",
    "plot_heatmap(rotated_components_df, 4, 8,save_path=\"./images/components_heatmap.png\")"
   ],
   "id": "509d783a82bbdcb2",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "KMO值: 0.762\n",
      "巴赫利特检验卡方值: 90424.712, p值: 0.0\n",
      "数据适合进行主成分分析 \n",
      "\n",
      "累计方差解释比例: ['31.41%', '54.60%', '66.53%', '73.02%', '78.89%', '84.04%', '88.27%', '91.46%', '93.59%', '95.70%', '97.14%', '98.29%', '98.91%', '99.26%', '99.55%', '99.79%', '99.96%', '100.00%', '100.00%']\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ],
      "image/png": "[encoded data, 38104 characters]"
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "旋转后的载荷矩阵（排序后）：\n",
      "        Factor1  Factor3  Factor2  Factor4  Factor5\n",
      "Tn      -0.963    0.035    0.071   -0.079    0.035\n",
      "T       -0.958   -0.138    0.033    0.074   -0.028\n",
      "Tx      -0.954   -0.014    0.045   -0.052   -0.063\n",
      "P        0.924   -0.029    0.071   -0.032   -0.000\n",
      "Po       0.921   -0.029    0.073   -0.033   -0.000\n",
      "Td      -0.898    0.366    0.043   -0.013    0.076\n",
      "O3      -0.637   -0.529   -0.030    0.239   -0.084\n",
      "U       -0.322    0.824   -0.008   -0.156    0.229\n",
      "Ff      -0.045   -0.772   -0.126    0.024    0.172\n",
      "NO2      0.300    0.728   -0.290    0.110   -0.202\n",
      "CO      -0.101    0.695   -0.449    0.298   -0.007\n",
      "VV       0.153   -0.667    0.531   -0.093   -0.175\n",
      "AQI     -0.017    0.038   -0.967    0.025   -0.029\n",
      "PM10     0.037   -0.060   -0.933   -0.092    0.003\n",
      "PM2.5    0.049    0.359   -0.879    0.149   -0.007\n",
      "Pa       0.006    0.055   -0.147   -0.747   -0.130\n",
      "SO2     -0.035    0.099   -0.208    0.694   -0.065\n",
      "RRR     -0.139    0.094    0.103   -0.077    0.819\n",
      "tR       0.163   -0.120   -0.087    0.131    0.512\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<Figure size 400x800 with 2 Axes>"
      ],
      "image/png": "[encoded data, 121016 characters]"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 42
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "## 题目3\n",
    "令2022年11月1日至2023年9月30日的空气质量数据为训练集，剩余数据为测试集。基于训练集，尝试使用两种不同的方法构建空气质量指数预测模型，并在测试集上测试。比较所选模型的预测效果。"
   ],
   "id": "3f89fa62a897a3e3"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-26T06:49:34.947179Z",
     "start_time": "2025-03-26T06:49:33.890890Z"
    }
   },
   "cell_type": "code",
   "source": [
    "#重新读取数据\n",
    "data = pd.read_excel('北京市空气质量指数与气象数据.xlsx')\n",
    "data['date_hour'] = pd.to_datetime(data['date']) + pd.to_timedelta(data['hour'], unit='h')\n",
    "data = data[['date_hour', 'date', 'hour', 'AQI']].set_index('date_hour')\n",
    "data = data.asfreq('3h')\n",
    "data['AQI'] = data['AQI'].ffill()"
   ],
   "id": "d1bdac1e4e1562f2",
   "outputs": [],
   "execution_count": 55
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "### (1)SARIMA模型\n",
   "id": "1fc53937767d55fd"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-26T06:55:01.678905Z",
     "start_time": "2025-03-26T06:53:55.743487Z"
    }
   },
   "cell_type": "code",
   "source": [
    "\"\"\"\n",
    "该模型在假设不知道测试集其他指标的情况下，仅使用AQI历史数据预测未来AQI\n",
    "\"\"\"\n",
    "# 数据切分\n",
    "train_end = pd.Timestamp('2023-09-30 23:00:00')\n",
    "test_start = pd.Timestamp('2023-10-01 02:00:00')\n",
    "train = data.loc[:train_end, 'AQI']\n",
    "test = data.loc[test_start:, 'AQI']\n",
    "\n",
    "# 自动参数搜索\n",
    "print(\"开始自动参数搜索(请耐心等待）...\")\n",
    "model = auto_arima(\n",
    "    train,\n",
    "    start_p=0, start_q=0,\n",
    "    max_p=1, max_q=1,\n",
    "    seasonal=True,\n",
    "    m=8,\n",
    "    d=1,\n",
    "    D=1,\n",
    "    trace=False,\n",
    "    error_action='ignore',\n",
    "    suppress_warnings=True,\n",
    "    stepwise=True\n",
    ")\n",
    "print(f\"最优参数组合：Order{model.order} Seasonal{model.seasonal_order}\")\n",
    "\n",
    "# 初始化模型\n",
    "current_model = SARIMAX(train, order=model.order, seasonal_order=model.seasonal_order)\n",
    "current_results = current_model.fit(disp=False)\n",
    "\n",
    "# 预测\n",
    "predictions = []\n",
    "lower_bounds = []\n",
    "upper_bounds = []\n",
    "\n",
    "for t in test.index:\n",
    "    forecast = current_results.get_forecast(steps=1)\n",
    "    pred_mean = forecast.predicted_mean.iloc[0]\n",
    "    pred_ci = forecast.conf_int().iloc[0]\n",
    "\n",
    "    predictions.append(pred_mean)\n",
    "    lower_bounds.append(pred_ci.iloc[0])\n",
    "    upper_bounds.append(pred_ci.iloc[1])\n",
    "\n",
    "    current_results = current_results.append(test.loc[[t]], refit=False)\n",
    "\n",
    "forecast_df = pd.DataFrame({\n",
    "    'predicted': predictions,\n",
    "    'lower': lower_bounds,\n",
    "    'upper': upper_bounds\n",
    "}, index=test.index)\n",
    "\n",
    "# 结果处理\n",
    "valid_mask = forecast_df['predicted'].notna()\n",
    "y_actual_valid = test[valid_mask]\n",
    "y_pred_valid = forecast_df.loc[valid_mask, 'predicted']\n",
    "\n",
    "# 可视化\n",
    "plt.figure(figsize=(15, 6))\n",
    "train_last_3days = train.loc[train.index[-24]:]\n",
    "train_last_3days.plot(label='训练集（最后3天）', alpha=0.7)\n",
    "test.plot(label='实际值', color='green', alpha=0.7)\n",
    "forecast_df['predicted'].plot(style='--', marker='o', markersize=5, label='单步预测值', color='red')\n",
    "plt.fill_between(forecast_df.index,\n",
    "                 forecast_df['lower'],\n",
    "                 forecast_df['upper'],\n",
    "                 color='pink', alpha=0.3, label='95%置信区间')\n",
    "plt.axvline(test_start, color='gray', linestyle='--', alpha=0.6)\n",
    "plt.title('AQI单步滚动预测结果 (SARIMA模型)')\n",
    "plt.xlabel('时间')\n",
    "plt.ylabel('AQI')\n",
    "plt.legend()\n",
    "plt.grid(alpha=0.3)\n",
    "plt.tight_layout()\n",
    "plt.savefig('./images/AQI-SARIMA.png', dpi=200, bbox_inches='tight')\n",
    "plt.show()\n",
    "\n",
    "# 评估指标\n",
    "metrics = cal_metrics(y_actual_valid, y_pred_valid)\n",
    "print(\"\\n单步预测评估结果：\")\n",
    "print(f\"有效预测点数: {len(y_actual_valid)}/{len(test)}\")\n",
    "for k, v in metrics.items():\n",
    "    print(f\"{k}: {v:.3f}\")"
   ],
   "id": "5fb8d128fcab57d1",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "开始自动参数搜索(请耐心等待）...\n",
      "最优参数组合：Order(0, 1, 1) Seasonal(2, 1, 0, 8)\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<Figure size 1500x600 with 1 Axes>"
      ],
      "image/png": "[encoded data, 200052 characters]"
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "单步预测评估结果：\n",
      "有效预测点数: 248/248\n",
      "RMSE: 11.893\n",
      "R-squared: 0.932\n",
      "MAE: 7.744\n"
     ]
    }
   ],
   "execution_count": 57
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "",
   "id": "bb1765c239b38d1c"
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "### (2)XGBOOST模型",
   "id": "345e80b2d1b4204b"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-26T02:30:11.107569Z",
     "start_time": "2025-03-26T02:30:11.063882Z"
    }
   },
   "cell_type": "code",
   "source": [
    "\"\"\"\n",
    "该模型在假设不考虑测试集其他指标的情况下，仅使用AQI数据对未来AQI进行<单步预测>，即每次预测都是根据之前时间点的真实AQI值进行的。\n",
    "整体运行时间约为25s，请耐心等待。\n",
    "\"\"\"\n",
    "#特征工程\n",
    "data=data[['AQI']]\n",
    "data_processed = data.copy()\n",
    "\n",
    "#时间分解特征\n",
    "# 基础特征\n",
    "data_processed['hour'] = data_processed.index.hour\n",
    "data_processed['day_of_week'] = data_processed.index.dayofweek\n",
    "data_processed['month'] = data_processed.index.month\n",
    "\n",
    "# 周期性编码\n",
    "data_processed['hour_sin'] = np.sin(2 * np.pi * data_processed['hour'] / 24)\n",
    "data_processed['hour_cos'] = np.cos(2 * np.pi * data_processed['hour'] / 24)\n",
    "data_processed['week_sin'] = np.sin(2 * np.pi * data_processed['day_of_week'] / 7)\n",
    "data_processed['week_cos'] = np.cos(2 * np.pi * data_processed['day_of_week'] / 7)\n",
    "\n",
    "#滞后特征\n",
    "# 生成3小时粒度的滞后特征（最多7天）\n",
    "lags = [i for i in range(1, 7 * 8 + 1)]  # 7天*每天8个时间点（3小时间隔）\n",
    "for lag in lags:\n",
    "    data_processed[f'AQI_lag_{lag}'] = data_processed['AQI'].shift(lag)\n",
    "\n",
    "# 划分数据集\n",
    "train_data = data_processed.loc['2022-11-01':'2023-09-30']\n",
    "test_data = data_processed.loc['2023-10-01':]\n",
    "\n",
    "# 特征选择\n",
    "features = [col for col in train_data.columns if col != 'AQI']\n",
    "X_train, y_train = train_data[features], train_data['AQI']\n",
    "X_test, y_test = test_data[features], test_data['AQI']"
   ],
   "id": "8920b904e0069cf7",
   "outputs": [],
   "execution_count": 45
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-26T02:30:37.832084Z",
     "start_time": "2025-03-26T02:30:11.212688Z"
    }
   },
   "cell_type": "code",
   "source": [
    "#随机搜索法参数调优（这里耗时较长，请耐心等待）\n",
    "param_dist = {\n",
    "    'n_estimators': [100, 200, 300],\n",
    "    'max_depth': randint(5, 10),\n",
    "    'learning_rate': uniform(0.01, 0.2),\n",
    "    'subsample': uniform(0.7, 0.3),\n",
    "    'colsample_bytree': uniform(0.7, 0.3),\n",
    "    'gamma': uniform(0, 0.3)\n",
    "}\n",
    "\n",
    "search = RandomizedSearchCV(\n",
    "    XGBRegressor(n_jobs=-1, random_state=42),\n",
    "    param_distributions=param_dist,\n",
    "    n_iter=10,\n",
    "    cv=3,\n",
    "    scoring='neg_mean_absolute_error',\n",
    "    verbose=1,\n",
    "    random_state=42\n",
    ")\n",
    "search.fit(X_train, y_train)"
   ],
   "id": "199aa487e826c1ac",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Fitting 3 folds for each of 10 candidates, totalling 30 fits\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "RandomizedSearchCV(cv=3,\n",
       "                   estimator=XGBRegressor(base_score=None, booster=None,\n",
       "                                          callbacks=None,\n",
       "                                          colsample_bylevel=None,\n",
       "                                          colsample_bynode=None,\n",
       "                                          colsample_bytree=None, device=None,\n",
       "                                          early_stopping_rounds=None,\n",
       "                                          enable_categorical=False,\n",
       "                                          eval_metric=None, feature_types=None,\n",
       "                                          feature_weights=None, gamma=None,\n",
       "                                          grow_policy=None,\n",
       "                                          importance_type=None,\n",
       "                                          interaction_constraint...\n",
       "                                        'learning_rate': <scipy.stats._distn_infrastructure.rv_continuous_frozen object at 0x153c7e510>,\n",
       "                                        'max_depth': <scipy.stats._distn_infrastructure.rv_discrete_frozen object at 0x153c7e810>,\n",
       "                                        'n_estimators': [100, 200, 300],\n",
       "                                        'subsample': <scipy.stats._distn_infrastructure.rv_continuous_frozen object at 0x153ce6b40>},\n",
       "                   random_state=42, scoring='neg_mean_absolute_error',\n",
       "                   verbose=1)"
      ],
      "text/html": [
       "<style>#sk-container-id-1 {\n",
       "  /* Definition of color scheme common for light and dark mode */\n",
       "  --sklearn-color-text: black;\n",
       "  --sklearn-color-line: gray;\n",
       "  /* Definition of color scheme for unfitted estimators */\n",
       "  --sklearn-color-unfitted-level-0: #fff5e6;\n",
       "  --sklearn-color-unfitted-level-1: #f6e4d2;\n",
       "  --sklearn-color-unfitted-level-2: #ffe0b3;\n",
       "  --sklearn-color-unfitted-level-3: chocolate;\n",
       "  /* Definition of color scheme for fitted estimators */\n",
       "  --sklearn-color-fitted-level-0: #f0f8ff;\n",
       "  --sklearn-color-fitted-level-1: #d4ebff;\n",
       "  --sklearn-color-fitted-level-2: #b3dbfd;\n",
       "  --sklearn-color-fitted-level-3: cornflowerblue;\n",
       "\n",
       "  /* Specific color for light theme */\n",
       "  --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
       "  --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, white)));\n",
       "  --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
       "  --sklearn-color-icon: #696969;\n",
       "\n",
       "  @media (prefers-color-scheme: dark) {\n",
       "    /* Redefinition of color scheme for dark theme */\n",
       "    --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
       "    --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, #111)));\n",
       "    --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
       "    --sklearn-color-icon: #878787;\n",
       "  }\n",
       "}\n",
       "\n",
       "#sk-container-id-1 {\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 pre {\n",
       "  padding: 0;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 input.sk-hidden--visually {\n",
       "  border: 0;\n",
       "  clip: rect(1px 1px 1px 1px);\n",
       "  clip: rect(1px, 1px, 1px, 1px);\n",
       "  height: 1px;\n",
       "  margin: -1px;\n",
       "  overflow: hidden;\n",
       "  padding: 0;\n",
       "  position: absolute;\n",
       "  width: 1px;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-dashed-wrapped {\n",
       "  border: 1px dashed var(--sklearn-color-line);\n",
       "  margin: 0 0.4em 0.5em 0.4em;\n",
       "  box-sizing: border-box;\n",
       "  padding-bottom: 0.4em;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-container {\n",
       "  /* jupyter's `normalize.less` sets `[hidden] { display: none; }`\n",
       "     but bootstrap.min.css set `[hidden] { display: none !important; }`\n",
       "     so we also need the `!important` here to be able to override the\n",
       "     default hidden behavior on the sphinx rendered scikit-learn.org.\n",
       "     See: https://github.com/scikit-learn/scikit-learn/issues/21755 */\n",
       "  display: inline-block !important;\n",
       "  position: relative;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-text-repr-fallback {\n",
       "  display: none;\n",
       "}\n",
       "\n",
       "div.sk-parallel-item,\n",
       "div.sk-serial,\n",
       "div.sk-item {\n",
       "  /* draw centered vertical line to link estimators */\n",
       "  background-image: linear-gradient(var(--sklearn-color-text-on-default-background), var(--sklearn-color-text-on-default-background));\n",
       "  background-size: 2px 100%;\n",
       "  background-repeat: no-repeat;\n",
       "  background-position: center center;\n",
       "}\n",
       "\n",
       "/* Parallel-specific style estimator block */\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item::after {\n",
       "  content: \"\";\n",
       "  width: 100%;\n",
       "  border-bottom: 2px solid var(--sklearn-color-text-on-default-background);\n",
       "  flex-grow: 1;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel {\n",
       "  display: flex;\n",
       "  align-items: stretch;\n",
       "  justify-content: center;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  position: relative;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item {\n",
       "  display: flex;\n",
       "  flex-direction: column;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item:first-child::after {\n",
       "  align-self: flex-end;\n",
       "  width: 50%;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item:last-child::after {\n",
       "  align-self: flex-start;\n",
       "  width: 50%;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item:only-child::after {\n",
       "  width: 0;\n",
       "}\n",
       "\n",
       "/* Serial-specific style estimator block */\n",
       "\n",
       "#sk-container-id-1 div.sk-serial {\n",
       "  display: flex;\n",
       "  flex-direction: column;\n",
       "  align-items: center;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  padding-right: 1em;\n",
       "  padding-left: 1em;\n",
       "}\n",
       "\n",
       "\n",
       "/* Toggleable style: style used for estimator/Pipeline/ColumnTransformer box that is\n",
       "clickable and can be expanded/collapsed.\n",
       "- Pipeline and ColumnTransformer use this feature and define the default style\n",
       "- Estimators will overwrite some part of the style using the `sk-estimator` class\n",
       "*/\n",
       "\n",
       "/* Pipeline and ColumnTransformer style (default) */\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable {\n",
       "  /* Default theme specific background. It is overwritten whether we have a\n",
       "  specific estimator or a Pipeline/ColumnTransformer */\n",
       "  background-color: var(--sklearn-color-background);\n",
       "}\n",
       "\n",
       "/* Toggleable label */\n",
       "#sk-container-id-1 label.sk-toggleable__label {\n",
       "  cursor: pointer;\n",
       "  display: block;\n",
       "  width: 100%;\n",
       "  margin-bottom: 0;\n",
       "  padding: 0.5em;\n",
       "  box-sizing: border-box;\n",
       "  text-align: center;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 label.sk-toggleable__label-arrow:before {\n",
       "  /* Arrow on the left of the label */\n",
       "  content: \"▸\";\n",
       "  float: left;\n",
       "  margin-right: 0.25em;\n",
       "  color: var(--sklearn-color-icon);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 label.sk-toggleable__label-arrow:hover:before {\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       "/* Toggleable content - dropdown */\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content {\n",
       "  max-height: 0;\n",
       "  max-width: 0;\n",
       "  overflow: hidden;\n",
       "  text-align: left;\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content pre {\n",
       "  margin: 0.2em;\n",
       "  border-radius: 0.25em;\n",
       "  color: var(--sklearn-color-text);\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content.fitted pre {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 input.sk-toggleable__control:checked~div.sk-toggleable__content {\n",
       "  /* Expand drop-down */\n",
       "  max-height: 200px;\n",
       "  max-width: 100%;\n",
       "  overflow: auto;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 input.sk-toggleable__control:checked~label.sk-toggleable__label-arrow:before {\n",
       "  content: \"▾\";\n",
       "}\n",
       "\n",
       "/* Pipeline/ColumnTransformer-specific style */\n",
       "\n",
       "#sk-container-id-1 div.sk-label input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-label.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Estimator-specific style */\n",
       "\n",
       "/* Colorize estimator box */\n",
       "#sk-container-id-1 div.sk-estimator input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-estimator.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-label label.sk-toggleable__label,\n",
       "#sk-container-id-1 div.sk-label label {\n",
       "  /* The background is the default theme color */\n",
       "  color: var(--sklearn-color-text-on-default-background);\n",
       "}\n",
       "\n",
       "/* On hover, darken the color of the background */\n",
       "#sk-container-id-1 div.sk-label:hover label.sk-toggleable__label {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "/* Label box, darken color on hover, fitted */\n",
       "#sk-container-id-1 div.sk-label.fitted:hover label.sk-toggleable__label.fitted {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Estimator label */\n",
       "\n",
       "#sk-container-id-1 div.sk-label label {\n",
       "  font-family: monospace;\n",
       "  font-weight: bold;\n",
       "  display: inline-block;\n",
       "  line-height: 1.2em;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-label-container {\n",
       "  text-align: center;\n",
       "}\n",
       "\n",
       "/* Estimator-specific */\n",
       "#sk-container-id-1 div.sk-estimator {\n",
       "  font-family: monospace;\n",
       "  border: 1px dotted var(--sklearn-color-border-box);\n",
       "  border-radius: 0.25em;\n",
       "  box-sizing: border-box;\n",
       "  margin-bottom: 0.5em;\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-estimator.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "/* on hover */\n",
       "#sk-container-id-1 div.sk-estimator:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-estimator.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Specification for estimator info (e.g. \"i\" and \"?\") */\n",
       "\n",
       "/* Common style for \"i\" and \"?\" */\n",
       "\n",
       ".sk-estimator-doc-link,\n",
       "a:link.sk-estimator-doc-link,\n",
       "a:visited.sk-estimator-doc-link {\n",
       "  float: right;\n",
       "  font-size: smaller;\n",
       "  line-height: 1em;\n",
       "  font-family: monospace;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  border-radius: 1em;\n",
       "  height: 1em;\n",
       "  width: 1em;\n",
       "  text-decoration: none !important;\n",
       "  margin-left: 1ex;\n",
       "  /* unfitted */\n",
       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-unfitted-level-1);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link.fitted,\n",
       "a:link.sk-estimator-doc-link.fitted,\n",
       "a:visited.sk-estimator-doc-link.fitted {\n",
       "  /* fitted */\n",
       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-1);\n",
       "}\n",
       "\n",
       "/* On hover */\n",
       "div.sk-estimator:hover .sk-estimator-doc-link:hover,\n",
       ".sk-estimator-doc-link:hover,\n",
       "div.sk-label-container:hover .sk-estimator-doc-link:hover,\n",
       ".sk-estimator-doc-link:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "div.sk-estimator.fitted:hover .sk-estimator-doc-link.fitted:hover,\n",
       ".sk-estimator-doc-link.fitted:hover,\n",
       "div.sk-label-container:hover .sk-estimator-doc-link.fitted:hover,\n",
       ".sk-estimator-doc-link.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "/* Span, style for the box shown on hovering the info icon */\n",
       ".sk-estimator-doc-link span {\n",
       "  display: none;\n",
       "  z-index: 9999;\n",
       "  position: relative;\n",
       "  font-weight: normal;\n",
       "  right: .2ex;\n",
       "  padding: .5ex;\n",
       "  margin: .5ex;\n",
       "  width: min-content;\n",
       "  min-width: 20ex;\n",
       "  max-width: 50ex;\n",
       "  color: var(--sklearn-color-text);\n",
       "  box-shadow: 2pt 2pt 4pt #999;\n",
       "  /* unfitted */\n",
       "  background: var(--sklearn-color-unfitted-level-0);\n",
       "  border: .5pt solid var(--sklearn-color-unfitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link.fitted span {\n",
       "  /* fitted */\n",
       "  background: var(--sklearn-color-fitted-level-0);\n",
       "  border: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link:hover span {\n",
       "  display: block;\n",
       "}\n",
       "\n",
       "/* \"?\"-specific style due to the `<a>` HTML tag */\n",
       "\n",
       "#sk-container-id-1 a.estimator_doc_link {\n",
       "  float: right;\n",
       "  font-size: 1rem;\n",
       "  line-height: 1em;\n",
       "  font-family: monospace;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  border-radius: 1rem;\n",
       "  height: 1rem;\n",
       "  width: 1rem;\n",
       "  text-decoration: none;\n",
       "  /* unfitted */\n",
       "  color: var(--sklearn-color-unfitted-level-1);\n",
       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 a.estimator_doc_link.fitted {\n",
       "  /* fitted */\n",
       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-1);\n",
       "}\n",
       "\n",
       "/* On hover */\n",
       "#sk-container-id-1 a.estimator_doc_link:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 a.estimator_doc_link.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "</style><div id=\"sk-container-id-1\" class=\"sk-top-container\"><div class=\"sk-text-repr-fallback\"><pre>RandomizedSearchCV(cv=3,\n",
       "                   estimator=XGBRegressor(base_score=None, booster=None,\n",
       "                                          callbacks=None,\n",
       "                                          colsample_bylevel=None,\n",
       "                                          colsample_bynode=None,\n",
       "                                          colsample_bytree=None, device=None,\n",
       "                                          early_stopping_rounds=None,\n",
       "                                          enable_categorical=False,\n",
       "                                          eval_metric=None, feature_types=None,\n",
       "                                          feature_weights=None, gamma=None,\n",
       "                                          grow_policy=None,\n",
       "                                          importance_type=None,\n",
       "                                          interaction_constraint...\n",
       "                                        &#x27;learning_rate&#x27;: &lt;scipy.stats._distn_infrastructure.rv_continuous_frozen object at 0x153c7e510&gt;,\n",
       "                                        &#x27;max_depth&#x27;: &lt;scipy.stats._distn_infrastructure.rv_discrete_frozen object at 0x153c7e810&gt;,\n",
       "                                        &#x27;n_estimators&#x27;: [100, 200, 300],\n",
       "                                        &#x27;subsample&#x27;: &lt;scipy.stats._distn_infrastructure.rv_continuous_frozen object at 0x153ce6b40&gt;},\n",
       "                   random_state=42, scoring=&#x27;neg_mean_absolute_error&#x27;,\n",
       "                   verbose=1)</pre><b>In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. <br />On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.</b></div><div class=\"sk-container\" hidden><div class=\"sk-item sk-dashed-wrapped\"><div class=\"sk-label-container\"><div class=\"sk-label fitted sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-1\" type=\"checkbox\" ><label for=\"sk-estimator-id-1\" class=\"sk-toggleable__label fitted sk-toggleable__label-arrow fitted\">&nbsp;&nbsp;RandomizedSearchCV<a class=\"sk-estimator-doc-link fitted\" rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.4/modules/generated/sklearn.model_selection.RandomizedSearchCV.html\">?<span>Documentation for RandomizedSearchCV</span></a><span class=\"sk-estimator-doc-link fitted\">i<span>Fitted</span></span></label><div class=\"sk-toggleable__content fitted\"><pre>RandomizedSearchCV(cv=3,\n",
       "                   estimator=XGBRegressor(base_score=None, booster=None,\n",
       "                                          callbacks=None,\n",
       "                                          colsample_bylevel=None,\n",
       "                                          colsample_bynode=None,\n",
       "                                          colsample_bytree=None, device=None,\n",
       "                                          early_stopping_rounds=None,\n",
       "                                          enable_categorical=False,\n",
       "                                          eval_metric=None, feature_types=None,\n",
       "                                          feature_weights=None, gamma=None,\n",
       "                                          grow_policy=None,\n",
       "                                          importance_type=None,\n",
       "                                          interaction_constraint...\n",
       "                                        &#x27;learning_rate&#x27;: &lt;scipy.stats._distn_infrastructure.rv_continuous_frozen object at 0x153c7e510&gt;,\n",
       "                                        &#x27;max_depth&#x27;: &lt;scipy.stats._distn_infrastructure.rv_discrete_frozen object at 0x153c7e810&gt;,\n",
       "                                        &#x27;n_estimators&#x27;: [100, 200, 300],\n",
       "                                        &#x27;subsample&#x27;: &lt;scipy.stats._distn_infrastructure.rv_continuous_frozen object at 0x153ce6b40&gt;},\n",
       "                   random_state=42, scoring=&#x27;neg_mean_absolute_error&#x27;,\n",
       "                   verbose=1)</pre></div> </div></div><div class=\"sk-parallel\"><div class=\"sk-parallel-item\"><div class=\"sk-item\"><div class=\"sk-label-container\"><div class=\"sk-label fitted sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-2\" type=\"checkbox\" ><label for=\"sk-estimator-id-2\" class=\"sk-toggleable__label fitted sk-toggleable__label-arrow fitted\">estimator: XGBRegressor</label><div class=\"sk-toggleable__content fitted\"><pre>XGBRegressor(base_score=None, booster=None, callbacks=None,\n",
       "             colsample_bylevel=None, colsample_bynode=None,\n",
       "             colsample_bytree=None, device=None, early_stopping_rounds=None,\n",
       "             enable_categorical=False, eval_metric=None, feature_types=None,\n",
       "             feature_weights=None, gamma=None, grow_policy=None,\n",
       "             importance_type=None, interaction_constraints=None,\n",
       "             learning_rate=None, max_bin=None, max_cat_threshold=None,\n",
       "             max_cat_to_onehot=None, max_delta_step=None, max_depth=None,\n",
       "             max_leaves=None, min_child_weight=None, missing=nan,\n",
       "             monotone_constraints=None, multi_strategy=None, n_estimators=None,\n",
       "             n_jobs=-1, num_parallel_tree=None, ...)</pre></div> </div></div><div class=\"sk-serial\"><div class=\"sk-item\"><div class=\"sk-estimator fitted sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-3\" type=\"checkbox\" ><label for=\"sk-estimator-id-3\" class=\"sk-toggleable__label fitted sk-toggleable__label-arrow fitted\">&nbsp;XGBRegressor<a class=\"sk-estimator-doc-link fitted\" rel=\"noreferrer\" target=\"_blank\" href=\"https://xgboost.readthedocs.io/en/release_3.0.0/python/python_api.html#xgboost.XGBRegressor\">?<span>Documentation for XGBRegressor</span></a></label><div class=\"sk-toggleable__content fitted\"><pre>XGBRegressor(base_score=None, booster=None, callbacks=None,\n",
       "             colsample_bylevel=None, colsample_bynode=None,\n",
       "             colsample_bytree=None, device=None, early_stopping_rounds=None,\n",
       "             enable_categorical=False, eval_metric=None, feature_types=None,\n",
       "             feature_weights=None, gamma=None, grow_policy=None,\n",
       "             importance_type=None, interaction_constraints=None,\n",
       "             learning_rate=None, max_bin=None, max_cat_threshold=None,\n",
       "             max_cat_to_onehot=None, max_delta_step=None, max_depth=None,\n",
       "             max_leaves=None, min_child_weight=None, missing=nan,\n",
       "             monotone_constraints=None, multi_strategy=None, n_estimators=None,\n",
       "             n_jobs=-1, num_parallel_tree=None, ...)</pre></div> </div></div></div></div></div></div></div></div></div>"
      ]
     },
     "execution_count": 46,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 46
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-26T02:30:38.024387Z",
     "start_time": "2025-03-26T02:30:38.009441Z"
    }
   },
   "cell_type": "code",
   "source": [
    "#模型预测\n",
    "best_model = search.best_estimator_\n",
    "y_pred = best_model.predict(X_test)\n",
    "#评估指标\n",
    "metrics=cal_metrics(y_pred, y_test)\n",
    "#输出结果\n",
    "print(\"最佳参数组合：\", search.best_params_)\n",
    "print(\"评估指标：\")\n",
    "for k, v in metrics.items():\n",
    "    print(f\"{k}: {v:.3f}\")"
   ],
   "id": "fe076794bae89ccb",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "最佳参数组合： {'colsample_bytree': 0.9826605267054558, 'gamma': 0.16898646535366177, 'learning_rate': 0.08708330050798323, 'max_depth': 6, 'n_estimators': 100, 'subsample': 0.7692681476866446}\n",
      "评估指标：\n",
      "RMSE: 12.332\n",
      "R-squared: 0.922\n",
      "MAE: 8.107\n"
     ]
    }
   ],
   "execution_count": 47
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-03-26T02:30:40.218109Z",
     "start_time": "2025-03-26T02:30:38.179832Z"
    }
   },
   "cell_type": "code",
   "source": [
    "#预测结果可视化\n",
    "def plot_results(y_true, y_pred, timestamps):\n",
    "    plt.figure(figsize=(18, 8))\n",
    "    ax = plt.gca()\n",
    "\n",
    "    # 绘制预测曲线\n",
    "    ax.plot(timestamps, y_true, label='真实值',\n",
    "            marker='o', markersize=4, linewidth=1, alpha=0.8)\n",
    "    ax.plot(timestamps, y_pred, label='预测值',\n",
    "            linestyle='--', marker='x', markersize=5, alpha=0.9)\n",
    "\n",
    "    # 设置时间轴格式\n",
    "    ax.xaxis.set_major_locator(HourLocator(interval=12))\n",
    "    ax.xaxis.set_minor_locator(HourLocator(interval=3))\n",
    "    ax.xaxis.set_major_formatter(DateFormatter(\"%m-%d %H:%M\"))\n",
    "\n",
    "    # 增强可视化元素\n",
    "    plt.title(f'AQI预测效果对比（MAE={metrics[\"MAE\"]:.2f}, R-squared={metrics[\"R-squared\"]:.2f}）',\n",
    "              fontsize=14, pad=20)\n",
    "    plt.xlabel('时间', fontsize=12)\n",
    "    plt.ylabel('AQI', fontsize=12)\n",
    "    plt.grid(True, which='both', linestyle='--', alpha=0.5)\n",
    "    plt.legend()\n",
    "\n",
    "    # 自动调整标签\n",
    "    plt.xticks(rotation=45, ha='right')\n",
    "    plt.tight_layout()\n",
    "    plt.savefig('./images/xg_by_step.png', dpi=200, bbox_inches='tight')\n",
    "    plt.show()\n",
    "\n",
    "plot_results(y_test, y_pred, test_data.index)\n",
    "\n",
    "#特征重要性可视化\n",
    "def plot_importance(model, features, top_n=20):\n",
    "    importance = pd.Series(model.feature_importances_, index=features)\n",
    "    top_features = importance.sort_values(ascending=False)[:top_n]\n",
    "\n",
    "    plt.figure(figsize=(12, 8))\n",
    "    ax = top_features.sort_values().plot.barh()\n",
    "\n",
    "    # 添加数据标签\n",
    "    for i in ax.patches:\n",
    "        ax.text(i.get_width() + 0.02, i.get_y() + 0.2,\n",
    "                f'{i.get_width():.2f}',\n",
    "                fontsize=10, color='dimgrey')\n",
    "\n",
    "    plt.title('Top {} 重要特征'.format(top_n), fontsize=14)\n",
    "    plt.xlabel('特征重要性', fontsize=12)\n",
    "    plt.tight_layout()\n",
    "    plt.savefig('./images/xg_feature_importance.png', dpi=200, bbox_inches='tight')\n",
    "    plt.show()\n",
    "\n",
    "np.random.seed(42)\n",
    "plot_importance(best_model, features)"
   ],
   "id": "2551eec52baeb4cb",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 1800x800 with 1 Axes>"
      ],
      "image/png": "[encoded data, 313084 characters]"
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<Figure size 1200x800 with 1 Axes>"
      ],
      "image/png": "[encoded data, 85096 characters]"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 48
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
