更多预测内容,请访问 cienciadedatos.net
- 使用 Python 的 ARIMA 与 SARIMAX 模型
- 使用机器学习进行时间序列预测
- 使用梯度提升进行时间序列预测:XGBoost、LightGBM 与 CatBoost
- 使用机器学习预测能源需求
简介¶
梯度提升模型因其在广泛场景下(包括回归与分类任务)均能取得出色成绩,已在机器学习社区中获得广泛认可。尽管这类模型在预测领域的应用传统上并不多见,但它们可以发挥极其重要的作用。在预测场景中使用梯度提升模型的主要优势包括:
除了自回归变量之外,还可以方便地将外生变量纳入模型。
能够捕捉变量之间的非线性关系。
高度可扩展,模型可以处理大量数据。
部分实现允许直接引入类别变量,无需额外的编码处理(例如独热编码)。
尽管有上述优势,在预测中使用机器学习模型也面临若干挑战,这些挑战可能使分析师对其望而却步,主要包括:
需要对数据进行转换,使其成为适合回归问题的格式。
根据所需的未来预测步数(预测步长),可能需要一个迭代过程,其中每次预测都基于前一次的结果。
模型验证需要特定的策略,例如回测、前向验证或时间序列交叉验证。传统的交叉验证方法不可直接使用。
skforecast 库为上述挑战提供了自动化解决方案,使机器学习模型更易于应用于预测问题并进行验证。该库支持多种先进的梯度提升模型,包括 XGBoost、LightGBM、Catboost 以及 scikit-learn HistGradientBoostingRegressor。本文展示了如何使用这些模型构建高精度的预测模型。为确保顺畅的学习体验,文档首先对数据进行了初步探索,随后逐步讲解建模过程:从使用 LightGBM 估计器的递归模型开始,进而引入外生变量和多种编码策略。文档最后演示了其他梯度提升实现方式的使用,包括 XGBoost、CatBoost 以及 scikit-learn 的 HistGradientBoostingRegressor。
✏️ 注意
机器学习模型并不总是优于 AR、ARIMA 或指数平滑等统计学习模型。哪种模型表现更好,在很大程度上取决于所应用的具体场景特性。请访问 使用 skforecast 的 ARIMA 与 SARIMAX 模型,了解更多关于统计模型的内容。
有关如何将梯度提升模型用于预测的更多示例,可参考文章 使用机器学习预测能源需求 和 全局预测模型。
使用场景¶
共享单车是一种流行的共享交通服务,为用户提供短途出行所需的自行车。这类系统通常设有停车桩,骑手可以借车并将其还至同属该系统的任意停车桩。停车桩配备了专用的自行车锁架,通过计算机控制来固定或释放自行车。这类系统运营商面临的主要挑战之一,是需要在各停车桩之间重新分配车辆,既要确保各桩都有车可借,又要确保有空位可还。
为了改善自行车调度的规划与执行,本文提出构建一个能够预测未来 36 小时用车人数的模型。这样,负责管理系统的公司每天 12:00 就能提前预知当日剩余时段(12 小时)以及第二天(24 小时)的预期用户量。
出于演示目的,本示例仅对单个站点建模,但可以轻松扩展以覆盖多个站点,使用全局多序列预测,从而在更大规模上改善共享单车系统的管理。
库¶
本文使用的库。
# 数据处理
# ==============================================================================
import numpy as np
import pandas as pd
from astral.sun import sun
from astral import LocationInfo
from skforecast.datasets import fetch_dataset
from feature_engine.datetime import DatetimeFeatures
from feature_engine.creation import CyclicalFeatures
from feature_engine.timeseries.forecasting import WindowFeatures
# 图表绘制
# ==============================================================================
from skforecast.plot import set_dark_theme
import matplotlib.pyplot as plt
from statsmodels.graphics.tsaplots import plot_acf
from statsmodels.graphics.tsaplots import plot_pacf
from skforecast.plot import plot_residuals, calculate_lag_autocorrelation
import plotly.graph_objects as go
import plotly.io as pio
import plotly.offline as poff
pio.templates.default = "seaborn"
poff.init_notebook_mode(connected=True)
plt.style.use('seaborn-v0_8-darkgrid')
# 建模与预测
# ==============================================================================
import xgboost
import lightgbm
import catboost
import sklearn
import shap
from xgboost import XGBRegressor
from lightgbm import LGBMRegressor
from catboost import CatBoostRegressor
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.preprocessing import OneHotEncoder, PolynomialFeatures
from sklearn.feature_selection import RFECV
from sklearn.compose import make_column_transformer, make_column_selector
import skforecast
from skforecast.recursive import ForecasterEquivalentDate, ForecasterRecursive
from skforecast.model_selection import (
TimeSeriesFold,
OneStepAheadFold,
bayesian_search_forecaster,
backtesting_forecaster,
)
from skforecast.preprocessing import RollingFeatures
from skforecast.feature_selection import select_features
from skforecast.metrics import calculate_coverage
# 警告配置
# ==============================================================================
import warnings
warnings.filterwarnings('once')
color = '\033[1m\033[38;5;208m'
print(f"{color}Version skforecast: {skforecast.__version__}")
print(f"{color}Version scikit-learn: {sklearn.__version__}")
print(f"{color}Version lightgbm: {lightgbm.__version__}")
print(f"{color}Version xgboost: {xgboost.__version__}")
print(f"{color}Version catboost: {catboost.__version__}")
print(f"{color}Version pandas: {pd.__version__}")
print(f"{color}Version numpy: {np.__version__}")
Version skforecast: 0.22.0 Version scikit-learn: 1.7.2 Version lightgbm: 4.6.0 Version xgboost: 3.2.0 Version catboost: 1.2.10 Version pandas: 2.3.3 Version numpy: 2.1.3
数据¶
本文数据为华盛顿特区共享单车系统在 2011 年和 2012 年的每小时使用情况。除每小时的用户数量外,还包含天气状况和假期信息。原始数据来自 UCI 机器学习资源库,已经过预先清洗(代码),主要修改如下:
将列名重命名为更具描述性的名称。
对天气变量的类别进行重命名,并将
heavy_rain(强降雨)与rain(降雨)类别合并。对温度、湿度和风速变量进行了反归一化处理。
创建了
date_time变量并将其设置为索引。通过前向填充的方式填补了缺失值。
# 下载数据
# ==============================================================================
data = fetch_dataset('bike_sharing', raw=True)
╭───────────────────────────────── bike_sharing ──────────────────────────────────╮ │ Description: │ │ Hourly usage of the bike share system in the city of Washington D.C. during the │ │ years 2011 and 2012. In addition to the number of users per hour, information │ │ about weather conditions and holidays is available. │ │ │ │ Source: │ │ Fanaee-T,Hadi. (2013). Bike Sharing Dataset. UCI Machine Learning Repository. │ │ https://doi.org/10.24432/C5W894. │ │ │ │ URL: │ │ https://raw.githubusercontent.com/skforecast/skforecast- │ │ datasets/main/data/bike_sharing_dataset_clean.csv │ │ │ │ Shape: 17544 rows x 12 columns │ ╰─────────────────────────────────────────────────────────────────────────────────╯
# 数据预处理(设置索引与频率)
# ==============================================================================
data = data[['date_time', 'users', 'holiday', 'weather', 'temp', 'atemp', 'hum', 'windspeed']]
data['date_time'] = pd.to_datetime(data['date_time'], format='%Y-%m-%d %H:%M:%S')
data = data.set_index('date_time')
if pd.__version__ < '2.2':
data = data.asfreq('H')
else:
data = data.asfreq('h')
data = data.sort_index()
data.head()
| users | holiday | weather | temp | atemp | hum | windspeed | |
|---|---|---|---|---|---|---|---|
| date_time | |||||||
| 2011-01-01 00:00:00 | 16.0 | 0.0 | clear | 9.84 | 14.395 | 81.0 | 0.0 |
| 2011-01-01 01:00:00 | 40.0 | 0.0 | clear | 9.02 | 13.635 | 80.0 | 0.0 |
| 2011-01-01 02:00:00 | 32.0 | 0.0 | clear | 9.02 | 13.635 | 80.0 | 0.0 |
| 2011-01-01 03:00:00 | 13.0 | 0.0 | clear | 9.84 | 14.395 | 75.0 | 0.0 |
| 2011-01-01 04:00:00 | 1.0 | 0.0 | clear | 9.84 | 14.395 | 75.0 | 0.0 |
为便于模型训练、最优超参数搜索以及预测精度评估,数据被划分为三个独立的子集:训练集、验证集和测试集。
# 划分训练集-验证集-测试集
# ==============================================================================
end_train = '2012-04-30 23:59:00'
end_validation = '2012-08-31 23:59:00'
data_train = data.loc[: end_train, :]
data_val = data.loc[end_train:end_validation, :]
data_test = data.loc[end_validation:, :]
print(f"Dates train : {data_train.index.min()} --- {data_train.index.max()} (n={len(data_train)})")
print(f"Dates validacion : {data_val.index.min()} --- {data_val.index.max()} (n={len(data_val)})")
print(f"Dates test : {data_test.index.min()} --- {data_test.index.max()} (n={len(data_test)})")
Dates train : 2011-01-01 00:00:00 --- 2012-04-30 23:00:00 (n=11664) Dates validacion : 2012-05-01 00:00:00 --- 2012-08-31 23:00:00 (n=2952) Dates test : 2012-09-01 00:00:00 --- 2012-12-31 23:00:00 (n=2928)
数据探索¶
对时间序列进行图形化探索,是识别趋势、模式和季节性变化的有效手段。这有助于指导选择最合适的预测模型。
绘制时间序列¶
完整时间序列
# 时间序列交互图
# ==============================================================================
fig = go.Figure()
fig.add_trace(go.Scatter(x=data_train.index, y=data_train['users'], mode='lines', name='Train'))
fig.add_trace(go.Scatter(x=data_val.index, y=data_val['users'], mode='lines', name='Validation'))
fig.add_trace(go.Scatter(x=data_test.index, y=data_test['users'], mode='lines', name='Test'))
fig.update_layout(
title = 'Number of users',
xaxis_title="Time",
yaxis_title="Users",
legend_title="Partition:",
width=800,
height=400,
margin=dict(l=20, r=20, t=35, b=20),
legend=dict(orientation="h", yanchor="top", y=1, xanchor="left", x=0.001)
)
#fig.update_xaxes(rangeslider_visible=True)
fig.show()
# 带缩放功能的时间序列静态图
# ==============================================================================
zoom = ('2011-08-01 00:00:00', '2011-08-15 00:00:00')
fig, axs = plt.subplots(2, 1, figsize=(8, 4), gridspec_kw={"height_ratios": [1, 2]})
data_train['users'].plot(ax=axs[0], label='train', alpha=0.5)
data_val['users'].plot(ax=axs[0], label='validation', alpha=0.5)
data_test['users'].plot(ax=axs[0], label='test', alpha=0.5)
axs[0].axvspan(zoom[0], zoom[1], color="blue", alpha=0.7)
axs[0].set_title("Number of users")
axs[0].set_xlabel("")
# 局部放大图
data.loc[zoom[0] : zoom[1], "users"].plot(ax=axs[1], color="blue")
axs[1].set_title(f"Zoom: {zoom[0]} to {zoom[1]}", fontsize=10)
plt.tight_layout()
plt.show()
季节性图¶
季节性图是识别时间序列中季节性规律和趋势的有效工具。通过对各季节的值进行均值聚合并绘制成图,可以直观地呈现这些规律。
# 年度、周度与日度季节性
# ==============================================================================
set_dark_theme()
fig, axs = plt.subplots(2, 2, figsize=(8.5, 5.5), sharex=False, sharey=True)
axs = axs.ravel()
flierprops=dict(marker='o', markerfacecolor='white', markeredgecolor='black', markersize=4)
# Users distribution by month
data['month'] = data.index.month
data.boxplot(column='users', by='month', ax=axs[0], flierprops=flierprops)
data.groupby('month')['users'].median().plot(style='o-', linewidth=0.8, ax=axs[0])
axs[0].set_ylabel('Users')
axs[0].set_title('Users distribution by month', fontsize=10)
# Users distribution by week day
data['week_day'] = data.index.day_of_week + 1
data.boxplot(column='users', by='week_day', ax=axs[1], flierprops=flierprops)
data.groupby('week_day')['users'].median().plot(style='o-', linewidth=0.8, ax=axs[1])
axs[1].set_ylabel('Users')
axs[1].set_title('Users distribution by week day', fontsize=10)
# Users distribution by the hour of the day
data['hour_day'] = data.index.hour + 1
data.boxplot(column='users', by='hour_day', ax=axs[2], flierprops=flierprops)
data.groupby('hour_day')['users'].median().plot(style='o-', linewidth=0.8, ax=axs[2])
axs[2].set_ylabel('Users')
axs[2].set_title('Users distribution by the hour of the day', fontsize=10)
# Users distribution by week day and hour of the day
mean_day_hour = data.groupby(["week_day", "hour_day"])["users"].mean()
mean_day_hour.plot(ax=axs[3])
axs[3].set(
title = "Mean users during week",
xticks = [i * 24 for i in range(7)],
xticklabels = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
xlabel = "Day and hour",
ylabel = "Number of users"
)
axs[3].title.set_size(10)
fig.suptitle("Seasonality plots", fontsize=12)
fig.tight_layout()
工作日与周末之间存在明显差异,日内规律也十分突出——不同时间段的用户数量差异显著。
自相关图¶
自相关图是确定自回归模型阶数的有效工具。自相关函数(ACF)衡量时间序列与其自身滞后版本之间的相关性;偏自相关函数(PACF)则在控制所有更短滞后阶数的时间序列值后,衡量时间序列与其滞后版本之间的相关性。这些图有助于确定自回归模型中应纳入的滞后阶数。
# 自相关图
# ==============================================================================
fig, ax = plt.subplots(figsize=(5, 2))
plot_acf(data['users'], ax=ax, lags=72)
plt.show()
# 偏自相关图
# ==============================================================================
fig, ax = plt.subplots(figsize=(5, 2))
plot_pacf(data['users'], ax=ax, lags=72, method='ywm')
plt.show()
# 偏自相关绝对值最大的前 10 个滞后阶数
# ==============================================================================
calculate_lag_autocorrelation(
data = data['users'],
n_lags = 60,
sort_by = "partial_autocorrelation_abs"
).head(10)
| lag | partial_autocorrelation_abs | partial_autocorrelation | autocorrelation_abs | autocorrelation | |
|---|---|---|---|---|---|
| 0 | 1 | 0.845220 | 0.845220 | 0.845172 | 0.845172 |
| 1 | 2 | 0.408349 | -0.408349 | 0.597704 | 0.597704 |
| 2 | 23 | 0.355669 | 0.355669 | 0.708470 | 0.708470 |
| 3 | 22 | 0.343601 | 0.343601 | 0.520804 | 0.520804 |
| 4 | 25 | 0.332366 | -0.332366 | 0.711256 | 0.711256 |
| 5 | 10 | 0.272649 | -0.272649 | 0.046483 | -0.046483 |
| 6 | 17 | 0.241984 | 0.241984 | 0.057267 | -0.057267 |
| 7 | 19 | 0.199286 | 0.199286 | 0.159897 | 0.159897 |
| 8 | 21 | 0.193404 | 0.193404 | 0.373666 | 0.373666 |
| 9 | 3 | 0.182068 | 0.182068 | 0.409680 | 0.409680 |
自相关分析结果表明,前几小时以及之前数天的用户数量与未来的用户数量之间存在显著相关性。这意味着,了解过去特定时段的用户数量对于预测未来的用户数量具有重要价值。
基线¶
面对预测问题时,建立基线模型至关重要。基线模型通常是一个非常简单的模型,用于评估更复杂的模型是否值得实施。
Skforecast 通过 ForecasterEquivalentDate 类,可以便捷地创建基线模型。该模型也称为季节性朴素预测(Seasonal Naive Forecasting),通过直接返回上一个相同季节时段观测到的值来进行预测(例如,上周同一工作日的值、昨天同一小时的值等)。
基于上述探索性分析,本文的基线模型将使用前一天同一小时的值来预测当前小时的用户数量。
✏️ 注意
在接下来的代码单元格中,将对基线预测器进行训练,并通过回测流程评估其预测能力。如果您对这个概念还不熟悉,请不必担心,稍后将在文档中详细说明。目前只需知道,回测流程是指用一定量的数据训练模型,并使用模型未见过的数据评估其预测能力。误差指标将作为参考,用于评估后续实现的更复杂模型的预测能力。
# 创建基线:前一天同一小时的值
# ==============================================================================
forecaster = ForecasterEquivalentDate(
offset = pd.DateOffset(days=1),
n_offsets = 1
)
# 训练预测器
# ==============================================================================
forecaster.fit(y=data.loc[:end_validation, 'users'])
forecaster
ForecasterEquivalentDate
General Information
- Estimator: NoneType
- Offset:
- Number of offsets: 1
- Aggregation function: mean
- Window size: 24
- Creation date: 2026-04-23 12:18:29
- Last fit date: 2026-04-23 12:18:29
- Skforecast version: 0.22.0
- Python version: 3.13.12
- Forecaster id: None
Training Information
- Training range: [Timestamp('2011-01-01 00:00:00'), Timestamp('2012-08-31 23:00:00')]
- Training index type: DatetimeIndex
- Training index frequency:
# 回测
# ==============================================================================
cv = TimeSeriesFold(steps = 36, initial_train_size = len(data.loc[:end_validation]))
metric_baseline, predictions = backtesting_forecaster(
forecaster = forecaster,
y = data['users'],
cv = cv,
metric = 'mean_absolute_error'
)
metric_baseline
| mean_absolute_error | |
|---|---|
| 0 | 91.668716 |
基线模型的 MAE 将作为参考值,用于评估后续更复杂的模型是否值得实施。
使用 LightGBM 进行多步预测¶
LightGBM 是随机梯度提升算法的一种高效实现,已在机器学习领域成为基准。LightGBM 库提供了自有 API 以及与 scikit-learn 兼容的 API,因此可与 skforecast 无缝集成。
首先,使用过去的值(滞后值)和移动平均作为预测变量,训练一个 ForecasterRecursive 模型。随后,将外生变量加入模型并评估其对预测性能的提升效果。由于梯度提升模型拥有大量超参数,本文使用 bayesian_search_forecaster() 函数进行贝叶斯搜索,以找到超参数和滞后阶数的最优组合。最后,通过回测流程评估模型的预测能力。
预测器¶
# 创建预测器
# ==============================================================================
window_features = RollingFeatures(stats=["mean"], window_sizes=24 * 3)
forecaster = ForecasterRecursive(
estimator = LGBMRegressor(random_state=15926, verbose=-1),
lags = 24,
window_features = window_features
)
# 训练预测器
# ==============================================================================
forecaster.fit(y=data.loc[:end_validation, 'users'])
forecaster
ForecasterRecursive
General Information
- Estimator: LGBMRegressor
- Lags: [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24]
- Window features: ['roll_mean_72']
- Window size: 72
- Series name: users
- Exogenous included: False
- Categorical features: auto
- Weight function included: False
- Differentiation order: None
- Drop NaN from series: False
- Creation date: 2026-04-23 12:18:31
- Last fit date: 2026-04-23 12:18:31
- Skforecast version: 0.22.0
- Python version: 3.13.12
- Forecaster id: None
Exogenous Variables
None
Data Transformations
- Transformer for y: None
- Transformer for exog: None
Training Information
- Training range: [Timestamp('2011-01-01 00:00:00'), Timestamp('2012-08-31 23:00:00')]
- Training index type: DatetimeIndex
- Training index frequency:
Estimator Parameters
-
{'boosting_type': 'gbdt', 'class_weight': None, 'colsample_bytree': 1.0, 'importance_type': 'split', 'learning_rate': 0.1, 'max_depth': -1, 'min_child_samples': 20, 'min_child_weight': 0.001, 'min_split_gain': 0.0, 'n_estimators': 100, 'n_jobs': None, 'num_leaves': 31, 'objective': None, 'random_state': 15926, 'reg_alpha': 0.0, 'reg_lambda': 0.0, 'subsample': 1.0, 'subsample_for_bin': 200000, 'subsample_freq': 0, 'verbose': -1}
Fit Kwargs
-
{}
# 预测
# ==============================================================================
forecaster.predict(steps=10)
2012-09-01 00:00:00 108.331027 2012-09-01 01:00:00 68.562982 2012-09-01 02:00:00 33.499525 2012-09-01 03:00:00 10.027583 2012-09-01 04:00:00 3.037563 2012-09-01 05:00:00 17.162543 2012-09-01 06:00:00 51.059825 2012-09-01 07:00:00 146.940053 2012-09-01 08:00:00 344.320596 2012-09-01 09:00:00 439.738683 Freq: h, Name: pred, dtype: float64
回测¶
为了对模型的预测能力进行稳健评估,本文开展了回测流程。回测的核心是:对测试集中的每个观测值,按照与实际生产环境相同的流程生成预测,然后将预测值与真实值进行比较。
强烈建议查阅 backtesting_forecaster() 函数的文档,以深入理解其功能。这将有助于充分发挥其潜力,全面分析模型的预测能力。
# 在测试集上对模型进行回测
# ==============================================================================
cv = TimeSeriesFold(steps = 36, initial_train_size = len(data.loc[:end_validation]))
metric, predictions = backtesting_forecaster(
forecaster = forecaster,
y = data['users'],
cv = cv,
metric = 'mean_absolute_error'
)
predictions.head()
| fold | pred | |
|---|---|---|
| 2012-09-01 00:00:00 | 0 | 108.331027 |
| 2012-09-01 01:00:00 | 0 | 68.562982 |
| 2012-09-01 02:00:00 | 0 | 33.499525 |
| 2012-09-01 03:00:00 | 0 | 10.027583 |
| 2012-09-01 04:00:00 | 0 | 3.037563 |
# 回测误差
# ==============================================================================
metric
| mean_absolute_error | |
|---|---|
| 0 | 76.464247 |
自回归模型的预测误差 MAE 低于基线模型。
外生变量¶
到目前为止,模型仅使用了时间序列的滞后值作为预测变量。然而,也可以引入其他变量作为预测变量,这类变量称为外生变量(特征),其使用可以提升模型的预测能力。需要特别注意的是,外生变量的值必须在预测时刻已知。
常见的外生变量包括从日历信息中派生的变量,例如星期几、月份、年份或节假日;天气变量(如温度、湿度和风速);以及经济变量(如通货膨胀率和利率)。
⚠️ 警告
外生变量在进行预测时必须已知。例如,若将温度用作外生变量,则在进行预测时,下一小时的温度值必须是已知的。若该温度值未知,则无法进行预测。
使用天气变量时应谨慎。当模型部署到生产环境后,未来的天气状况是未知的,而是由气象服务机构预报的数值。由于这些数值本身存在误差,会将不确定性引入预测模型,导致模型预测精度下降。为了提前了解(而非规避)模型在实际生产中的预期表现,建议在训练模型时使用训练阶段可获取的天气预报数据,而非实际记录的天气条件。
接下来,将基于日历信息、日出与日落时间、温度以及节假日创建外生变量,并将这些新变量添加至训练集、验证集和测试集中,作为自回归模型的预测变量。
日历与气象特征¶
# 日历特征
# ==============================================================================
features_to_extract = [
'month',
'week',
'day_of_week',
'hour'
]
calendar_transformer = DatetimeFeatures(
variables ='index',
features_to_extract = features_to_extract,
drop_original = True,
)
calendar_features = calendar_transformer.fit_transform(data)[features_to_extract]
# 日照特征
# ==============================================================================
location = LocationInfo(
name = 'Washington DC',
region = 'USA',
timezone = 'US/Eastern',
latitude = 40.516666666666666,
longitude = -77.03333333333333
)
sunrise_hour = [
sun(location.observer, date=date, tzinfo=location.timezone)['sunrise'].hour
for date in data.index
]
sunset_hour = [
sun(location.observer, date=date, tzinfo=location.timezone)['sunset'].hour
for date in data.index
]
sun_light_features = pd.DataFrame({
'sunrise_hour': sunrise_hour,
'sunset_hour': sunset_hour},
index = data.index
)
sun_light_features['daylight_hours'] = (
sun_light_features['sunset_hour'] - sun_light_features['sunrise_hour']
)
sun_light_features["is_daylight"] = np.where(
(data.index.hour >= sun_light_features["sunrise_hour"])
& (data.index.hour < sun_light_features["sunset_hour"]),
1,
0,
)
# 节假日特征
# ==============================================================================
holiday_features = data[['holiday']].astype(int)
holiday_features['holiday_previous_day'] = holiday_features['holiday'].shift(24)
holiday_features['holiday_next_day'] = holiday_features['holiday'].shift(-24)
# 温度的滚动窗口
# ==============================================================================
wf_transformer = WindowFeatures(
variables = ["temp"],
window = ["1D", "7D"],
functions = ["mean", "max", "min"],
freq = "h",
)
temp_features = wf_transformer.fit_transform(data[['temp']])
# 合并所有外生变量
# ==============================================================================
assert all(calendar_features.index == sun_light_features.index)
assert all(calendar_features.index == temp_features.index)
assert all(calendar_features.index == holiday_features.index)
df_exogenous_features = pd.concat([
calendar_features,
sun_light_features,
temp_features,
holiday_features
], axis=1)
# 由于创建了移动平均,序列开头存在缺失值;
# 由于 holiday_next_day,序列末尾也存在缺失值。
df_exogenous_features = df_exogenous_features.iloc[7 * 24:, :]
df_exogenous_features = df_exogenous_features.iloc[:-24, :]
df_exogenous_features.head(3)
| month | week | day_of_week | hour | sunrise_hour | sunset_hour | daylight_hours | is_daylight | temp | temp_window_1D_mean | temp_window_1D_max | temp_window_1D_min | temp_window_7D_mean | temp_window_7D_max | temp_window_7D_min | holiday | holiday_previous_day | holiday_next_day | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| date_time | ||||||||||||||||||
| 2011-01-08 00:00:00 | 1 | 1 | 5 | 0 | 7 | 16 | 9 | 0 | 7.38 | 8.063333 | 9.02 | 6.56 | 10.127976 | 18.86 | 4.92 | 0 | 0.0 | 0.0 |
| 2011-01-08 01:00:00 | 1 | 1 | 5 | 1 | 7 | 16 | 9 | 0 | 7.38 | 8.029167 | 9.02 | 6.56 | 10.113333 | 18.86 | 4.92 | 0 | 0.0 | 0.0 |
| 2011-01-08 02:00:00 | 1 | 1 | 5 | 2 | 7 | 16 | 9 | 0 | 7.38 | 7.995000 | 9.02 | 6.56 | 10.103571 | 18.86 | 4.92 | 0 | 0.0 | 0.0 |
具有周期性规律的特征¶
日历中的某些要素(例如小时数或星期几)具有周期性。例如,一天中的小时从 0 循环到 23。处理此类特征有多种方式,各有优缺点。
一种方式是直接将特征作为数值使用,不做任何变换。这种方法可以避免生成大量新特征,但可能对特征值强加了错误的线性顺序。例如,某天的第 23 小时与下一天的第 0 小时在线性表示中相差甚远,而实际上两者之间只相差一个小时。
另一种方式是将周期性特征视为类别变量,以避免强加线性顺序。然而,此方法可能导致变量内在的周期性信息丢失。
还有第三种方式,通常优于前两种:使用特征周期的正弦和余弦进行变换。这种方法只生成两个新特征,比前两种方法更能准确地捕捉数据的周期性,因为它既保留了特征的自然顺序,又避免了强加线性顺序。
✏️ 注意
有关处理周期性特征不同方法的详细说明,请参阅 时间序列预测中的周期性特征 一文。
# 日历与日照特征的周期性编码
# ==============================================================================
features_to_encode = [
"month",
"week",
"day_of_week",
"hour",
"sunrise_hour",
"sunset_hour",
]
max_values = {
"month": 12,
"week": 52,
"day_of_week": 7,
"hour": 24,
"sunrise_hour": 24,
"sunset_hour": 24,
}
cyclical_encoder = CyclicalFeatures(
variables = features_to_encode,
max_values = max_values,
drop_original = False
)
df_exogenous_features = cyclical_encoder.fit_transform(df_exogenous_features)
df_exogenous_features.head(3)
| month | week | day_of_week | hour | sunrise_hour | sunset_hour | daylight_hours | is_daylight | temp | temp_window_1D_mean | ... | week_sin | week_cos | day_of_week_sin | day_of_week_cos | hour_sin | hour_cos | sunrise_hour_sin | sunrise_hour_cos | sunset_hour_sin | sunset_hour_cos | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| date_time | |||||||||||||||||||||
| 2011-01-08 00:00:00 | 1 | 1 | 5 | 0 | 7 | 16 | 9 | 0 | 7.38 | 8.063333 | ... | 0.120537 | 0.992709 | -0.974928 | -0.222521 | 0.000000 | 1.000000 | 0.965926 | -0.258819 | -0.866025 | -0.5 |
| 2011-01-08 01:00:00 | 1 | 1 | 5 | 1 | 7 | 16 | 9 | 0 | 7.38 | 8.029167 | ... | 0.120537 | 0.992709 | -0.974928 | -0.222521 | 0.258819 | 0.965926 | 0.965926 | -0.258819 | -0.866025 | -0.5 |
| 2011-01-08 02:00:00 | 1 | 1 | 5 | 2 | 7 | 16 | 9 | 0 | 7.38 | 7.995000 | ... | 0.120537 | 0.992709 | -0.974928 | -0.222521 | 0.500000 | 0.866025 | 0.965926 | -0.258819 | -0.866025 | -0.5 |
3 rows × 30 columns
特征交互¶
在许多情况下,外生变量并非相互独立——某个变量对目标变量的影响往往取决于其他变量的取值。例如,温度对电力需求的影响会因一天中的不同时段而有所不同。外生变量之间的交互关系可以通过将现有变量相乘而得到的新变量来捕捉。这些交互特征可以使用 scikit-learn 的 PolynomialFeatures 类轻松生成。
# 外生变量之间的交互
# ==============================================================================
transformer_poly = PolynomialFeatures(
degree = 2,
interaction_only = True,
include_bias = False
).set_output(transform="pandas")
poly_cols = [
'month_sin',
'month_cos',
'week_sin',
'week_cos',
'day_of_week_sin',
'day_of_week_cos',
'hour_sin',
'hour_cos',
'sunrise_hour_sin',
'sunrise_hour_cos',
'sunset_hour_sin',
'sunset_hour_cos',
'daylight_hours',
'is_daylight',
'holiday_previous_day',
'holiday_next_day',
'temp_window_1D_mean',
'temp_window_1D_min',
'temp_window_1D_max',
'temp_window_7D_mean',
'temp_window_7D_min',
'temp_window_7D_max',
'temp',
'holiday'
]
poly_features = transformer_poly.fit_transform(df_exogenous_features[poly_cols])
poly_features = poly_features.drop(columns=poly_cols)
poly_features.columns = [f"poly_{col}" for col in poly_features.columns]
poly_features.columns = poly_features.columns.str.replace(" ", "__")
assert all(poly_features.index == df_exogenous_features.index)
df_exogenous_features = pd.concat([df_exogenous_features, poly_features], axis=1)
df_exogenous_features.head(3)
| month | week | day_of_week | hour | sunrise_hour | sunset_hour | daylight_hours | is_daylight | temp | temp_window_1D_mean | ... | poly_temp_window_7D_mean__temp_window_7D_min | poly_temp_window_7D_mean__temp_window_7D_max | poly_temp_window_7D_mean__temp | poly_temp_window_7D_mean__holiday | poly_temp_window_7D_min__temp_window_7D_max | poly_temp_window_7D_min__temp | poly_temp_window_7D_min__holiday | poly_temp_window_7D_max__temp | poly_temp_window_7D_max__holiday | poly_temp__holiday | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| date_time | |||||||||||||||||||||
| 2011-01-08 00:00:00 | 1 | 1 | 5 | 0 | 7 | 16 | 9 | 0 | 7.38 | 8.063333 | ... | 49.829643 | 191.013631 | 74.744464 | 0.0 | 92.7912 | 36.3096 | 0.0 | 139.1868 | 0.0 | 0.0 |
| 2011-01-08 01:00:00 | 1 | 1 | 5 | 1 | 7 | 16 | 9 | 0 | 7.38 | 8.029167 | ... | 49.757600 | 190.737467 | 74.636400 | 0.0 | 92.7912 | 36.3096 | 0.0 | 139.1868 | 0.0 | 0.0 |
| 2011-01-08 02:00:00 | 1 | 1 | 5 | 2 | 7 | 16 | 9 | 0 | 7.38 | 7.995000 | ... | 49.709571 | 190.553357 | 74.564357 | 0.0 | 92.7912 | 36.3096 | 0.0 | 139.1868 | 0.0 | 0.0 |
3 rows × 306 columns
类别特征¶
将类别变量引入 LightGBM(以及其他梯度提升框架)有多种方式:
一种方式是通过数据变换,将类别值转换为数值,例如使用独热编码或序数编码。这种方式适用于所有机器学习模型。
另一种方式是由 LightGBM 在内部直接处理类别变量,无需预处理。
没有哪种方法在所有情况下都更优。通用规则如下:
当类别变量的基数较高(即不同取值较多)时,使用原生类别变量支持比使用独热编码效果更好。
使用独热编码的数据时,需要更多的分裂点(即更大的树深度)才能达到与原生处理单个分裂点等效的效果。
当类别变量通过独热编码转换为多个哑变量后,其重要性会被分散,导致特征重要性分析更加复杂,难以解读。
# 将类别变量存储为 category 类型
# ==============================================================================
data["weather"] = data["weather"].astype("category")
独热编码¶
scikit-learn 中的 ColumnTransformer 提供了一种强大的方式来定义变换并将其应用于特定特征。通过将这些变换封装在 ColumnTransformer 对象中,可以通过 transformer_exog 参数将其传递给预测器。
✏️ 注意
可以在预测器之外对整个数据集进行变换,但必须确保这些变换仅从训练数据中学习,以避免信息泄露。此外,在预测时,同样的变换也应应用于输入数据。因此,建议将变换纳入预测器内部处理,以确保变换应用的一致性并降低出错的可能性。
# 独热编码转换器
# ==============================================================================
one_hot_encoder = make_column_transformer(
(
OneHotEncoder(sparse_output=False, drop='if_binary'),
make_column_selector(dtype_include=['category', 'object']),
),
remainder="passthrough",
verbose_feature_names_out=False,
).set_output(transform="pandas")
# 创建带有外生特征转换器的预测器
# ==============================================================================
forecaster = ForecasterRecursive(
estimator = LGBMRegressor(random_state=15926, verbose=-1),
lags = 72,
window_features = window_features,
transformer_exog = one_hot_encoder
)
为了查看数据的变换方式,可以使用 create_train_X_y 方法来生成预测器在训练模型时所使用的矩阵。这一方法有助于了解训练过程中具体的数据处理操作。
# 查看训练矩阵
# ==============================================================================
exog_features = ['weather']
X_train, y_train = forecaster.create_train_X_y(
y = data.loc[:end_validation, 'users'],
exog = data.loc[:end_validation, exog_features]
)
X_train.head(3)
| lag_1 | lag_2 | lag_3 | lag_4 | lag_5 | lag_6 | lag_7 | lag_8 | lag_9 | lag_10 | ... | lag_67 | lag_68 | lag_69 | lag_70 | lag_71 | lag_72 | roll_mean_72 | weather_clear | weather_mist | weather_rain | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| date_time | |||||||||||||||||||||
| 2011-01-04 00:00:00 | 12.0 | 20.0 | 52.0 | 52.0 | 110.0 | 157.0 | 157.0 | 76.0 | 72.0 | 77.0 | ... | 1.0 | 1.0 | 13.0 | 32.0 | 40.0 | 16.0 | 43.638889 | 1.0 | 0.0 | 0.0 |
| 2011-01-04 01:00:00 | 5.0 | 12.0 | 20.0 | 52.0 | 52.0 | 110.0 | 157.0 | 157.0 | 76.0 | 72.0 | ... | 2.0 | 1.0 | 1.0 | 13.0 | 32.0 | 40.0 | 43.486111 | 1.0 | 0.0 | 0.0 |
| 2011-01-04 02:00:00 | 2.0 | 5.0 | 12.0 | 20.0 | 52.0 | 52.0 | 110.0 | 157.0 | 157.0 | 76.0 | ... | 3.0 | 2.0 | 1.0 | 1.0 | 13.0 | 32.0 | 42.958333 | 1.0 | 0.0 | 0.0 |
3 rows × 76 columns
上述独热编码策略仅作教学展示之用。在本文的其余部分,将使用原生的类别变量支持方式。
类别特征的原生实现¶
自 0.22.0 版本起,skforecast 提供了内置的 categorical_features 参数,可自动处理编码并为梯度提升估计器(XGBoost、LightGBM、CatBoost 和 HistGradientBoosting)进行原生配置,无需手动构建编码器流水线或设置特定于估计器的参数。这是大多数使用场景下推荐采用的方式,本文将对此进行详细介绍。
✏️ 注意
预测器内部会对类别特征应用 OrdinalEncoder,将其转换为整数编码,以避免内部估计器拟合时出错。对于原生支持类别特征的估计器(例如 LightGBM、XGBoost、CatBoost 和 HistGradientBoosting),预测器还会将类别列的索引列表传递给估计器,使其将这些列视为类别而非数值特征。对于其他所有估计器,整数编码后的值将直接传入并作为连续数值特征处理。
# 创建带有自动类别检测功能的预测器
# ==============================================================================
forecaster = ForecasterRecursive(
estimator = LGBMRegressor(random_state=15926, verbose=-1),
lags = 72,
categorical_features = 'auto'
)
这是本文后续部分将全程采用的策略。
使用外生特征评估模型¶
重新训练预测器,这次同时纳入外生变量作为预测变量,类别特征采用原生实现方式。
# 选择要纳入模型的外生变量
# ==============================================================================
exog_features = []
# 选择以 _sin 或 _cos 结尾的列
exog_features.extend(df_exogenous_features.filter(regex='_sin$|_cos$').columns.tolist())
# 选择以 temp_ 开头的列
exog_features.extend(df_exogenous_features.filter(regex='^temp_.*').columns.tolist())
# 选择以 holiday_ 开头的列
exog_features.extend(df_exogenous_features.filter(regex='^holiday_.*').columns.tolist())
exog_features.extend(['temp', 'holiday', 'weather'])
df_exogenous_features = df_exogenous_features.filter(exog_features, axis=1)
# 将目标变量与外生变量合并到同一数据框
# ==============================================================================
data = data[['users', 'weather']].merge(
df_exogenous_features,
left_index=True,
right_index=True,
how='inner' # To use only dates for which we have all the variables
)
data = data.astype({col: np.float32 for col in data.select_dtypes("number").columns})
data_train = data.loc[: end_train, :].copy()
data_val = data.loc[end_train:end_validation, :].copy()
data_test = data.loc[end_validation:, :].copy()
# 在测试集上对含外生变量的模型进行回测
# ==============================================================================
cv = TimeSeriesFold(steps = 36, initial_train_size = len(data.loc[:end_validation]))
metric, predictions = backtesting_forecaster(
forecaster = forecaster,
y = data['users'],
exog = data[exog_features],
cv = cv,
metric = 'mean_absolute_error',
n_jobs = 'auto',
verbose = False,
show_progress = True
)
metric
| mean_absolute_error | |
|---|---|
| 0 | 48.350039 |
将外生变量纳入模型作为预测变量,可以提升模型的预测能力。
超参数调优¶
超参数调优是构建高效机器学习模型的关键步骤。前面章节所使用的 ForecasterRecursive 包含了前 24 个滞后阶数,以及使用默认超参数的 LGBMRegressor 模型。然而,并没有理由认为这些值就是最合适的。为了找到最优超参数,使用 bayesian_search_forecaster() 函数进行贝叶斯搜索。该搜索采用与之前相同的回测流程,但每次使用不同的超参数和滞后阶数组合训练模型。需要注意的是,超参数搜索必须使用验证集进行,测试数据始终不参与搜索过程。
搜索流程按如下步骤进行:
仅使用训练集训练模型。
通过回测在验证集上评估模型。
选择使误差最小的超参数和滞后阶数组合。
使用找到的最优组合,结合训练集和验证集,重新训练模型。
遵循上述步骤,可以得到一个超参数经过优化、且能有效规避过拟合的模型。
✏️ 注意
超参数搜索可能耗时较长,尤其是在使用基于回测的验证策略(TimeSeriesFold)时。一种更快的替代方案是使用基于单步预测的验证策略(OneStepAheadFold)。尽管该策略速度更快,但其准确性可能不及基于回测的验证。有关两种策略优缺点的详细说明,请参阅 回测 vs. 单步预测 章节。
# 超参数搜索
# ==============================================================================
forecaster = ForecasterRecursive(
estimator = LGBMRegressor(random_state=15926, verbose=-1),
lags = 72,
window_features = window_features,
categorical_features = 'auto'
)
# 滞后阶数候选网格
lags_grid = [48, 72, [1, 2, 3, 23, 24, 25, 167, 168, 169]]
# 估计器超参数搜索空间
def search_space(trial):
search_space = {
'n_estimators' : trial.suggest_int('n_estimators', 300, 1000, step=100),
'max_depth' : trial.suggest_int('max_depth', 3, 10),
'min_data_in_leaf': trial.suggest_int('min_data_in_leaf', 25, 500),
'learning_rate' : trial.suggest_float('learning_rate', 0.01, 0.5),
'feature_fraction': trial.suggest_float('feature_fraction', 0.5, 1),
'max_bin' : trial.suggest_int('max_bin', 50, 250),
'reg_alpha' : trial.suggest_float('reg_alpha', 0, 1),
'reg_lambda' : trial.suggest_float('reg_lambda', 0, 1),
'lags' : trial.suggest_categorical('lags', lags_grid)
}
return search_space
# 训练与验证的折叠设置
cv_search = TimeSeriesFold(steps = 36, initial_train_size = len(data_train))
results_search, frozen_trial = bayesian_search_forecaster(
forecaster = forecaster,
y = data.loc[:end_validation, 'users'], # Test data not used
exog = data.loc[:end_validation, exog_features],
cv = cv_search,
search_space = search_space,
metric = 'mean_absolute_error',
n_trials = 20, # Increase this value for a more exhaustive search
return_best = True
)
best_params = results_search['params'].iat[0]
best_params = best_params | {'random_state': 15926, 'verbose': -1}
best_lags = results_search['lags'].iat[0]
# 搜索结果
# ==============================================================================
results_search.head(3)
| trial_number | lags | params | mean_absolute_error | n_estimators | max_depth | min_data_in_leaf | learning_rate | feature_fraction | max_bin | reg_alpha | reg_lambda | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 15 | [1, 2, 3, 23, 24, 25, 167, 168, 169] | {'n_estimators': 300, 'max_depth': 4, 'min_dat... | 54.836988 | 300.0 | 4.0 | 85.0 | 0.128572 | 0.856374 | 217.0 | 0.529752 | 0.184231 |
| 1 | 13 | [1, 2, 3, 23, 24, 25, 167, 168, 169] | {'n_estimators': 300, 'max_depth': 3, 'min_dat... | 54.980568 | 300.0 | 3.0 | 113.0 | 0.180900 | 0.845588 | 242.0 | 0.200898 | 0.157315 |
| 2 | 19 | [1, 2, 3, 23, 24, 25, 167, 168, 169] | {'n_estimators': 400, 'max_depth': 3, 'min_dat... | 55.694559 | 400.0 | 3.0 | 228.0 | 0.066660 | 0.784963 | 216.0 | 0.299242 | 0.653703 |
由于 return_best 已设置为 True,预测器对象将自动更新为搜索到的最优配置,并在完整数据集上完成训练。这个最终模型随后可用于对新数据进行未来预测。
一旦使用验证数据确定了最优超参数组合,便对模型在测试集上的预测能力进行评估。
# 回测模型
# ==============================================================================
cv = TimeSeriesFold(steps = 36, initial_train_size = len(data.loc[:end_validation]))
metric, predictions = backtesting_forecaster(
forecaster = forecaster,
y = data['users'],
exog = data[exog_features],
cv = cv,
metric = 'mean_absolute_error'
)
metric
| mean_absolute_error | |
|---|---|
| 0 | 46.877707 |
# 绘制预测值与真实值对比图
# ======================================================================================
fig = go.Figure()
trace1 = go.Scatter(x=data_test.index, y=data_test['users'], name="test", mode="lines")
trace2 = go.Scatter(x=predictions.index, y=predictions['pred'], name="prediction", mode="lines")
fig.add_trace(trace1)
fig.add_trace(trace2)
fig.update_layout(
title="Real value vs predicted in test data",
xaxis_title="Date time",
yaxis_title="Users",
width=800,
height=400,
margin=dict(l=20, r=20, t=35, b=20),
legend=dict(orientation="h", yanchor="top", y=1.1, xanchor="left", x=0.001)
)
fig.show()
特征选择¶
特征选择是从特征集中挑选出对模型构建最有价值的子集的过程,是机器学习流程中的重要步骤,有助于减少过拟合、提升模型精度并缩短训练时间。由于 skforecast 的底层回归器遵循 scikit-learn API,可以通过 select_features() 函数使用 scikit-learn 提供的特征选择方法。其中最常用的两种方法是递归特征消除和序贯特征选择。
💡 提示
特征选择是提升机器学习模型性能的有力工具,但计算开销较大,可能耗时较长。由于目标是找到最优特征子集而非最终模型,因此不必使用全量数据或高度复杂的模型。建议使用数据的一个小子集和一个简单模型。一旦确定了最优特征子集,再使用全量数据和更复杂的配置重新训练模型。
# 创建预测器
# ==============================================================================
estimator = LGBMRegressor(
n_estimators = 100,
max_depth = 5,
random_state = 15926,
verbose = -1
)
forecaster = ForecasterRecursive(
estimator = estimator,
lags = best_lags,
window_features = window_features,
categorical_features = 'auto'
)
# 递归特征消除与交叉验证
# ==============================================================================
warnings.filterwarnings("ignore", message="X does not have valid feature names.*")
selector = RFECV(
estimator = estimator,
step = 1,
cv = 3,
)
selected_lags, selected_window_features, selected_exog = select_features(
forecaster = forecaster,
selector = selector,
y = data_train['users'],
exog = data_train[exog_features],
select_only = None,
force_inclusion = None,
subsample = 0.5,
random_state = 123,
verbose = True,
)
Recursive feature elimination (RFECV)
-------------------------------------
Total number of records available: 11327
Total number of records used for feature selection: 5663
Number of features available: 99
Lags (n=9)
Window features (n=1)
Exog (n=89)
Number of features selected: 31
Lags (n=9) : [1, 2, 3, 23, 24, 25, 167, 168, 169]
Window features (n=1) : ['roll_mean_72']
Exog (n=21) : ['hour_sin', 'hour_cos', 'poly_month_sin__hour_cos', 'poly_week_sin__day_of_week_sin', 'poly_week_sin__day_of_week_cos', 'poly_week_sin__hour_sin', 'poly_week_sin__sunrise_hour_cos', 'poly_week_cos__day_of_week_cos', 'poly_week_cos__hour_sin', 'poly_week_cos__hour_cos', 'poly_day_of_week_sin__hour_sin', 'poly_day_of_week_sin__hour_cos', 'poly_day_of_week_cos__hour_sin', 'poly_day_of_week_cos__hour_cos', 'poly_hour_sin__hour_cos', 'poly_hour_sin__sunset_hour_sin', 'poly_hour_cos__sunset_hour_sin', 'temp_window_1D_mean', 'temp_window_7D_mean', 'temp', 'weather']
scikit-learn 的 RFECV 首先在初始特征集上训练模型,并获取每个特征的重要性(通过 coef_ 或 feature_importances_ 等属性)。随后,在每一轮迭代中,重要性最低的特征被逐步删除,并通过交叉验证评估保留特征后模型的性能。该过程持续进行,直到进一步删除特征无法提升模型性能(甚至导致性能下降),或者达到 min_features_to_select 为止。
最终结果是一个特征的最优子集,能够在模型简洁性与预测能力之间取得最佳平衡(由交叉验证结果决定)。
使用最优特征子集重新训练并评估预测器。
# 使用所选特征创建预测器
# ==============================================================================
forecaster = ForecasterRecursive(
estimator = LGBMRegressor(**best_params),
lags = selected_lags,
window_features = window_features,
categorical_features = 'auto'
)
# 在测试集上对含外生变量的模型进行回测
# ==============================================================================
cv = TimeSeriesFold(steps = 36, initial_train_size = len(data.loc[:end_validation]))
metric_lgbm, predictions = backtesting_forecaster(
forecaster = forecaster,
y = data['users'],
exog = data[selected_exog],
cv = cv,
metric = 'mean_absolute_error'
)
metric_lgbm
| mean_absolute_error | |
|---|---|
| 0 | 46.220081 |
模型性能与使用全部特征时的结果相当,但模型现在更加简洁,训练速度更快,且不易过拟合。在本文的后续部分,模型将仅使用最重要的特征进行训练。
# 更新外生变量
# ==============================================================================
exog_features = selected_exog
概率预测:预测区间¶
预测区间定义了目标变量真实值以给定概率落入的范围。Skforecast 实现了多种概率预测方法:
以下代码演示了如何使用共形预测生成预测区间。
使用
prediction_interval()方法预测未来 n 步的区间。使用
backtesting_forecaster()方法预测整个测试集的区间。
interval 参数用于指定预测区间的目标覆盖概率。本例中,interval 设置为 [5, 95],对应理论覆盖概率为 90%。
# 创建并训练预测器
# ==============================================================================
forecaster = ForecasterRecursive(
estimator = LGBMRegressor(**best_params),
lags = best_lags,
window_features = window_features,
categorical_features = 'auto',
binner_kwargs = {"n_bins": 5}
)
forecaster.fit(
y = data.loc[:end_train, 'users'],
exog = data.loc[:end_train, exog_features],
store_in_sample_residuals = True
)
# 预测区间
# ==============================================================================
# 由于模型使用了外生变量进行训练,预测时也必须提供这些变量。
predictions = forecaster.predict_interval(
exog = data.loc[end_train:, exog_features],
steps = 24,
interval = [5, 95],
method = 'conformal',
)
predictions.head()
| pred | lower_bound | upper_bound | |
|---|---|---|---|
| 2012-05-01 00:00:00 | 22.515512 | 14.114887 | 30.916137 |
| 2012-05-01 01:00:00 | 9.975362 | 1.574737 | 18.375987 |
| 2012-05-01 02:00:00 | 3.931467 | -4.469157 | 12.332092 |
| 2012-05-01 03:00:00 | 2.482508 | -5.918117 | 10.883133 |
| 2012-05-01 04:00:00 | 6.446478 | -1.954147 | 14.847103 |
默认情况下,区间使用样本内残差(训练集残差)计算,这可能导致区间过窄(过于乐观)。为了避免这一问题,可使用 set_out_sample_residuals() 方法,通过验证集的回测来指定样本外残差。
如果在调用 set_out_sample_residuals() 时同时传入预测值和残差,则在自助法过程中使用的残差可以根据预测值的范围进行条件筛选。这有助于在尽量保持区间紧凑的同时,提升估计区间的覆盖率。
# 在验证集上进行回测以获取样本外残差
# ==============================================================================
cv = TimeSeriesFold(steps = 36, initial_train_size = len(data.loc[:end_train]))
_, predictions_val = backtesting_forecaster(
forecaster = forecaster,
y = data.loc[:end_validation, 'users'],
exog = data.loc[:end_validation, exog_features],
cv = cv,
metric = 'mean_absolute_error'
)
# 样本外残差分布
# ==============================================================================
residuals = data.loc[predictions_val.index, 'users'] - predictions_val['pred']
print(pd.Series(np.where(residuals < 0, 'negative', 'positive')).value_counts())
plt.rcParams.update({'font.size': 8})
_ = plot_residuals(
y_true = data.loc[predictions_val.index, 'users'],
y_pred = predictions_val['pred'],
figsize=(7, 4)
)
positive 1819 negative 1133 Name: count, dtype: int64
# 将样本外残差存储到预测器中
# ==============================================================================
forecaster.set_out_sample_residuals(
y_true = data.loc[predictions_val.index, 'users'],
y_pred = predictions_val['pred']
)
随后运行回测流程,以估计测试集上的预测区间。将 use_in_sample_residuals 设置为 False,以使用之前存储的样本外残差;将 use_binned_residuals 设置为 True,使自助法过程中使用的残差根据预测值的范围进行条件筛选。
# 使用样本外残差在测试集上进行带预测区间的回测
# ==============================================================================
cv = TimeSeriesFold(steps = 36, initial_train_size = len(data.loc[:end_validation]))
metric, predictions = backtesting_forecaster(
forecaster = forecaster,
y = data['users'],
exog = data[exog_features],
cv = cv,
metric = 'mean_absolute_error',
interval = [5, 95], # 90% prediction interval
interval_method = 'conformal',
use_in_sample_residuals = False, # Use out-sample residuals
use_binned_residuals = True, # Use residuals conditioned on predicted values
)
predictions.head(5)
| fold | pred | lower_bound | upper_bound | |
|---|---|---|---|---|
| 2012-09-01 00:00:00 | 0 | 134.449743 | 67.950842 | 200.948644 |
| 2012-09-01 01:00:00 | 0 | 108.138740 | 41.639838 | 174.637641 |
| 2012-09-01 02:00:00 | 0 | 74.405440 | 37.416775 | 111.394106 |
| 2012-09-01 03:00:00 | 0 | 39.512342 | 2.523676 | 76.501008 |
| 2012-09-01 04:00:00 | 0 | 14.571119 | 3.017486 | 26.124752 |
# 绘制预测区间与真实值对比图
# ==============================================================================
fig = go.Figure([
go.Scatter(name='Prediction', x=predictions.index, y=predictions['pred'], mode='lines'),
go.Scatter(
name='Real value', x=data_test.index, y=data_test['users'], mode='lines',
),
go.Scatter(
name='Upper Bound', x=predictions.index, y=predictions['upper_bound'], mode='lines',
marker=dict(color="#444"), line=dict(width=0), showlegend=False
),
go.Scatter(
name='Lower Bound', x=predictions.index, y=predictions['lower_bound'], marker=dict(color="#444"),
line=dict(width=0), mode='lines', fillcolor='rgba(68, 68, 68, 0.3)', fill='tonexty', showlegend=False
)
])
fig.update_layout(
title="Real value vs predicted in test data",
xaxis_title="Date time",
yaxis_title="users",
width=800,
height=400,
margin=dict(l=20, r=20, t=35, b=20),
hovermode="x",
legend=dict(orientation="h", yanchor="top", y=1.1, xanchor="left", x=0.001),
# Initial zoom on x axis betwee 1 oct to 10 oct
xaxis=dict(range=['2012-10-01', '2012-10-10'])
)
fig.show()
# 预测区间覆盖率(测试集)
# ==============================================================================
coverage = calculate_coverage(
y_true = data.loc[end_validation:, "users"],
lower_bound = predictions["lower_bound"],
upper_bound = predictions["upper_bound"]
)
area = (predictions['upper_bound'] - predictions['lower_bound']).sum()
print(f"Total area of the interval: {round(area, 2)}")
print(f"Predicted interval coverage: {round(100 * coverage, 2)} %")
Total area of the interval: 555842.18 Predicted interval coverage: 86.6 %
测试集上的实际覆盖率略低于理论期望覆盖率(90%),这意味着预测区间包含真实值的概率低于预期。
✏️ 注意
有关 skforecast 概率预测功能的详细说明,请访问:使用机器学习进行概率预测。
模型可解释性¶
由于许多现代机器学习模型(如集成方法)结构复杂,它们通常被视为黑盒,难以理解为何会做出某一特定预测。可解释性技术旨在揭开这些模型的神秘面纱,深入了解其内部工作机制,并在各个领域帮助建立信任、提升透明度、满足监管要求。提升模型可解释性不仅有助于理解模型行为,还有助于识别偏差、改进模型性能,并使利益相关者能够基于机器学习的洞察做出更明智的决策。
Skforecast 与一些最流行的模型可解释性方法兼容:模型内置特征重要性、SHAP 值与偏依赖图。
# 创建并训练预测器
# ==============================================================================
forecaster = ForecasterRecursive(
estimator = LGBMRegressor(**best_params),
lags = [1, 2, 3, 23, 24, 25, 167, 168, 169],
window_features = window_features,
categorical_features = 'auto',
)
forecaster.fit(
y = data.loc[:end_validation, 'users'],
exog = data.loc[:end_validation, exog_features]
)
模型内置特征重要性¶
# 提取特征重要性
# ==============================================================================
importance = forecaster.get_feature_importances()
importance.head(10)
| feature | importance | |
|---|---|---|
| 0 | lag_1 | 370 |
| 7 | lag_168 | 345 |
| 8 | lag_169 | 229 |
| 4 | lag_24 | 202 |
| 6 | lag_167 | 178 |
| 1 | lag_2 | 175 |
| 2 | lag_3 | 159 |
| 3 | lag_23 | 140 |
| 9 | roll_mean_72 | 124 |
| 21 | poly_day_of_week_sin__hour_cos | 121 |
⚠️ 警告
仅当预测器的估计器具有 coef_ 或 feature_importances_ 属性(scikit-learn 中默认存在)时,get_feature_importances() 方法才会返回结果。
SHAP 值¶
SHAP(SHapley Additive exPlanations,沙普利加性解释)值是一种广受欢迎的机器学习模型解释方法,可以直观且定量地理解变量及其取值如何影响预测结果。
借助 skforecast 模型,只需两个关键要素即可生成 SHAP 值解释:
预测器的内部估计器。
由时间序列构建并用于训练预测器的训练矩阵。
利用这两个组件,用户可以为其 skforecast 模型创建富有洞察力且易于解读的说明。这些说明可用于验证模型的可靠性、识别对预测贡献最大的关键因素,以及深入理解输入变量与目标变量之间的底层关系。
# 预测器用于训练内部估计器的训练矩阵
# ==============================================================================
X_train, y_train = forecaster.create_train_X_y(
y = data.loc[:end_validation, 'users'],
exog = data.loc[:end_validation, exog_features]
)
display(X_train.head(3))
display(y_train.head(3))
| lag_1 | lag_2 | lag_3 | lag_23 | lag_24 | lag_25 | lag_167 | lag_168 | lag_169 | roll_mean_72 | ... | poly_day_of_week_sin__hour_cos | poly_day_of_week_cos__hour_sin | poly_day_of_week_cos__hour_cos | poly_hour_sin__hour_cos | poly_hour_sin__sunset_hour_sin | poly_hour_cos__sunset_hour_sin | temp_window_1D_mean | temp_window_7D_mean | temp | weather | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| date_time | |||||||||||||||||||||
| 2011-01-15 01:00:00 | 28.0 | 27.0 | 36.0 | 1.0 | 5.0 | 14.0 | 16.0 | 16.0 | 25.0 | 55.736111 | ... | -0.941708 | -0.057593 | -0.214939 | 0.250000 | -0.250000 | -0.933013 | 6.594167 | 6.535595 | 6.56 | 1.0 |
| 2011-01-15 02:00:00 | 20.0 | 28.0 | 27.0 | 1.0 | 1.0 | 5.0 | 7.0 | 16.0 | 16.0 | 55.930556 | ... | -0.844312 | -0.111260 | -0.192709 | 0.433013 | -0.482963 | -0.836516 | 6.696667 | 6.530715 | 6.56 | 1.0 |
| 2011-01-15 03:00:00 | 12.0 | 20.0 | 28.0 | 1.0 | 1.0 | 1.0 | 1.0 | 7.0 | 16.0 | 56.083333 | ... | -0.689378 | -0.157346 | -0.157346 | 0.500000 | -0.683013 | -0.683013 | 6.799167 | 6.525833 | 6.56 | 1.0 |
3 rows × 31 columns
date_time 2011-01-15 01:00:00 20.0 2011-01-15 02:00:00 12.0 2011-01-15 03:00:00 8.0 Freq: h, Name: y, dtype: float32
# 创建 SHAP 解释器(适用于树模型)
# ==============================================================================
explainer = shap.TreeExplainer(forecaster.estimator)
# 采样 50% 的数据以加快计算速度
rng = np.random.default_rng(seed=785412)
sample = rng.choice(X_train.index, size=int(len(X_train)*0.5), replace=False)
X_train_sample = X_train.loc[sample, :]
shap_values = explainer.shap_values(X_train_sample)
✏️ 注意
SHAP 库提供了多种解释器,每种解释器针对不同类型的模型设计。shap.TreeExplainer 适用于基于树的模型,例如本例中使用的 LGBMRegressor。更多信息请参阅 SHAP 文档。
# SHAP 汇总图(前 10 个特征)
# ==============================================================================
shap.initjs()
shap.summary_plot(shap_values, X_train_sample, max_display=10, show=False)
fig, ax = plt.gcf(), plt.gca()
ax.set_title("SHAP Summary plot")
ax.tick_params(labelsize=8, colors='white')
fig.set_size_inches(10, 4.5)
SHAP 值不仅可以解释模型的整体行为,还是分析单次预测的强大工具,在需要理解某一具体预测如何形成以及哪些变量对其有所贡献时尤为有用。
要进行此类分析,需要获取预测时刻的预测特征值——即本例中的滞后值。这可通过 create_predict_X 方法,或在 backtesting_forecaster 函数中启用 return_predictors=True 参数来实现。
假设需要理解回测过程中针对日期 2012-10-06 12:00:00 所得到的预测结果。
# 回测,并返回预测所用的特征值
# ==============================================================================
cv = TimeSeriesFold(steps = 36, initial_train_size = len(data.loc[:end_validation]))
metric, predictions = backtesting_forecaster(
forecaster = forecaster,
y = data['users'],
exog = data[exog_features],
cv = cv,
metric = 'mean_absolute_error',
return_predictors = True,
)
通过设置 return_predictors=True,可以获得一个 DataFrame,其中包含预测值('pred')、所属分区('fold'),以及用于生成每次预测的滞后值和外生变量的取值。
predictions.head(3)
| fold | pred | lag_1 | lag_2 | lag_3 | lag_23 | lag_24 | lag_25 | lag_167 | lag_168 | ... | poly_day_of_week_sin__hour_cos | poly_day_of_week_cos__hour_sin | poly_day_of_week_cos__hour_cos | poly_hour_sin__hour_cos | poly_hour_sin__sunset_hour_sin | poly_hour_cos__sunset_hour_sin | temp_window_1D_mean | temp_window_7D_mean | temp | weather | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2012-09-01 00:00:00 | 0 | 134.449743 | 174.000000 | 277.000000 | 303.0 | 32.0 | 82.0 | 152.0 | 115.0 | 135.0 | ... | -0.974928 | -0.000000 | -0.222521 | 0.000000 | -0.000000 | -0.965926 | 31.330833 | 28.714643 | 30.340000 | 0.0 |
| 2012-09-01 01:00:00 | 0 | 108.138740 | 134.449743 | 174.000000 | 277.0 | 20.0 | 32.0 | 82.0 | 79.0 | 115.0 | ... | -0.941708 | -0.057593 | -0.214939 | 0.250000 | -0.250000 | -0.933013 | 31.433332 | 28.724405 | 29.520000 | 0.0 |
| 2012-09-01 02:00:00 | 0 | 74.405440 | 108.138740 | 134.449743 | 174.0 | 5.0 | 20.0 | 32.0 | 38.0 | 79.0 | ... | -0.844312 | -0.111260 | -0.192709 | 0.433013 | -0.482963 | -0.836516 | 31.501667 | 28.734167 | 28.700001 | 0.0 |
3 rows × 33 columns
# 针对单次具体预测的瀑布图
# ==============================================================================
predictions = predictions.astype(data[exog_features].dtypes) # Ensure that the types are the same
iloc_predicted_date = predictions.index.get_loc('2012-10-06 12:00:00')
shap_values_single = explainer(predictions.iloc[:, 2:])
shap.plots.waterfall(shap_values_single[iloc_predicted_date], show=False)
fig, ax = plt.gcf(), plt.gca()
fig.set_size_inches(8, 3.5)
ax_list = fig.axes
ax = ax_list[0]
ax.tick_params(labelsize=8)
ax.set
plt.show()
# 针对单次具体预测的力图
# ==============================================================================
shap.force_plot(
base_value = shap_values_single.base_values[iloc_predicted_date],
shap_values = shap_values_single.values[iloc_predicted_date],
features = predictions.iloc[iloc_predicted_date, 2:],
)
Have you run `initjs()` in this notebook? If this notebook was from another user you must also trust this notebook (File -> Trust notebook). If you are viewing this notebook on github the Javascript has been stripped for security. If you are using JupyterLab this error is because a JupyterLab extension has not yet been written.
XGBoost、CatBoost 与 HistGradientBoostingRegressor¶
自梯度提升算法取得成功以来,涌现出多种实现方式。除 LightGBM 外,另外三种实现也非常流行:XGBoost、CatBoost 和 HistGradientBoostingRegressor,它们均与 skforecast 兼容。
XGBoost:由陈天奇开发。
HistGradientBoostingRegressor:由 scikit-learn 开发。
CatBoost:由 Yandex 开发。
以下各节展示如何使用这些实现构建预测模型,重点介绍其对类别特征的原生支持。本次采用单步前瞻验证策略以加快超参数搜索速度。
# 用于超参数搜索和回测的折叠设置
# ==============================================================================
cv_search = OneStepAheadFold(initial_train_size = len(data_train))
cv_backtesting = TimeSeriesFold(steps = 36, initial_train_size = len(data[:end_validation]))
XGBoost¶
# 创建预测器
# ==============================================================================
forecaster = ForecasterRecursive(
estimator = XGBRegressor(tree_method='hist', random_state=123),
lags = 24,
window_features = window_features,
categorical_features = 'auto'
)
# 超参数搜索
# ==============================================================================
# 滞后阶数候选网格
lags_grid = [48, 72, [1, 2, 3, 23, 24, 25, 167, 168, 169]]
# 估计器超参数搜索空间
def search_space(trial):
search_space = {
'n_estimators' : trial.suggest_int('n_estimators', 300, 1000, step=100),
'max_depth' : trial.suggest_int('max_depth', 3, 10),
'learning_rate' : trial.suggest_float('learning_rate', 0.01, 1),
'subsample' : trial.suggest_float('subsample', 0.1, 1),
'colsample_bytree': trial.suggest_float('colsample_bytree', 0.1, 1),
'gamma' : trial.suggest_float('gamma', 0, 1),
'reg_alpha' : trial.suggest_float('reg_alpha', 0, 1),
'reg_lambda' : trial.suggest_float('reg_lambda', 0, 1),
'lags' : trial.suggest_categorical('lags', lags_grid)
}
return search_space
results_search, frozen_trial = bayesian_search_forecaster(
forecaster = forecaster,
y = data.loc[:end_validation, 'users'],
exog = data.loc[:end_validation, exog_features],
cv = cv_search,
search_space = search_space,
metric = 'mean_absolute_error',
n_trials = 20
)
╭─────────────────────────── OneStepAheadValidationWarning ────────────────────────────╮ │ One-step-ahead predictions are used for faster model comparison, but they may not │ │ fully represent multi-step prediction performance. It is recommended to backtest the │ │ final model for a more accurate multi-step performance estimate. │ │ │ │ Category : skforecast.exceptions.OneStepAheadValidationWarning │ │ Location : │ │ /home/ubuntu/anaconda3/envs/skforecast_22_py13/lib/python3.13/site-packages/skforeca │ │ st/model_selection/_utils.py:640 │ │ Suppress : warnings.simplefilter('ignore', category=OneStepAheadValidationWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────── IgnoredArgumentWarning ───────────────────────────────╮ │ The estimator's `feature_types` and `enable_categorical` parameters have been set to │ │ handle categorical features. Previous values: feature_types=['q', 'q', 'q', 'q', │ │ 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', │ │ 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', │ │ 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', │ │ 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'c'], │ │ enable_categorical=True. │ │ │ │ Category : skforecast.exceptions.IgnoredArgumentWarning │ │ Location : │ │ /home/ubuntu/anaconda3/envs/skforecast_22_py13/lib/python3.13/site-packages/skforeca │ │ st/utils/utils.py:610 │ │ Suppress : warnings.simplefilter('ignore', category=IgnoredArgumentWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────── IgnoredArgumentWarning ───────────────────────────────╮ │ The estimator's `feature_types` and `enable_categorical` parameters have been set to │ │ handle categorical features. Previous values: feature_types=['q', 'q', 'q', 'q', │ │ 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', │ │ 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'c'], enable_categorical=True. │ │ │ │ Category : skforecast.exceptions.IgnoredArgumentWarning │ │ Location : │ │ /home/ubuntu/anaconda3/envs/skforecast_22_py13/lib/python3.13/site-packages/skforeca │ │ st/utils/utils.py:610 │ │ Suppress : warnings.simplefilter('ignore', category=IgnoredArgumentWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
# 在测试集上对含外生变量的模型进行回测
# ==============================================================================
metric_xgboost, predictions = backtesting_forecaster(
forecaster = forecaster,
y = data['users'],
exog = data[exog_features],
cv = cv_backtesting,
metric = 'mean_absolute_error'
)
metric_xgboost
╭─────────────────────────────── IgnoredArgumentWarning ───────────────────────────────╮ │ The estimator's `feature_types` and `enable_categorical` parameters have been set to │ │ handle categorical features. Previous values: feature_types=['q', 'q', 'q', 'q', │ │ 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', │ │ 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', │ │ 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', │ │ 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'q', 'c'], │ │ enable_categorical=True. │ │ │ │ Category : skforecast.exceptions.IgnoredArgumentWarning │ │ Location : │ │ /home/ubuntu/anaconda3/envs/skforecast_22_py13/lib/python3.13/site-packages/skforeca │ │ st/utils/utils.py:610 │ │ Suppress : warnings.simplefilter('ignore', category=IgnoredArgumentWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
| mean_absolute_error | |
|---|---|
| 0 | 49.534337 |
HistGradientBoostingRegressor¶
# 创建预测器
# ==============================================================================
forecaster = ForecasterRecursive(
estimator = HistGradientBoostingRegressor(random_state=123),
lags = 24,
window_features = window_features,
categorical_features = 'auto'
)
# 超参数搜索
# ==============================================================================
lags_grid = [48, 72, [1, 2, 3, 23, 24, 25, 167, 168, 169]]
def search_space(trial):
search_space = {
'max_iter' : trial.suggest_int('max_iter', 300, 1000, step=100),
'max_depth' : trial.suggest_int('max_depth', 3, 10),
'learning_rate' : trial.suggest_float('learning_rate', 0.01, 1),
'min_samples_leaf' : trial.suggest_int('min_samples_leaf', 1, 20),
'l2_regularization' : trial.suggest_float('l2_regularization', 0, 1),
'lags' : trial.suggest_categorical('lags', lags_grid)
}
return search_space
results_search, frozen_trial = bayesian_search_forecaster(
forecaster = forecaster,
y = data.loc[:end_validation, 'users'],
exog = data.loc[:end_validation, exog_features],
cv = cv_search,
search_space = search_space,
metric = 'mean_absolute_error',
n_trials = 20
)
╭─────────────────────────── OneStepAheadValidationWarning ────────────────────────────╮ │ One-step-ahead predictions are used for faster model comparison, but they may not │ │ fully represent multi-step prediction performance. It is recommended to backtest the │ │ final model for a more accurate multi-step performance estimate. │ │ │ │ Category : skforecast.exceptions.OneStepAheadValidationWarning │ │ Location : │ │ /home/ubuntu/anaconda3/envs/skforecast_22_py13/lib/python3.13/site-packages/skforeca │ │ st/model_selection/_utils.py:640 │ │ Suppress : warnings.simplefilter('ignore', category=OneStepAheadValidationWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────── IgnoredArgumentWarning ───────────────────────────────╮ │ The estimator's `categorical_features` parameter has been set to handle categorical │ │ features. Previous value: `categorical_features=[93]`. │ │ │ │ Category : skforecast.exceptions.IgnoredArgumentWarning │ │ Location : │ │ /home/ubuntu/anaconda3/envs/skforecast_22_py13/lib/python3.13/site-packages/skforeca │ │ st/utils/utils.py:626 │ │ Suppress : warnings.simplefilter('ignore', category=IgnoredArgumentWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────── IgnoredArgumentWarning ───────────────────────────────╮ │ The estimator's `categorical_features` parameter has been set to handle categorical │ │ features. Previous value: `categorical_features=[30]`. │ │ │ │ Category : skforecast.exceptions.IgnoredArgumentWarning │ │ Location : │ │ /home/ubuntu/anaconda3/envs/skforecast_22_py13/lib/python3.13/site-packages/skforeca │ │ st/utils/utils.py:626 │ │ Suppress : warnings.simplefilter('ignore', category=IgnoredArgumentWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
# 在测试集上对含外生变量的模型进行回测
# ==============================================================================
metric_histgb, predictions = backtesting_forecaster(
forecaster = forecaster,
y = data['users'],
exog = data[exog_features],
cv = cv_backtesting,
metric = 'mean_absolute_error'
)
metric_histgb
╭─────────────────────────────── IgnoredArgumentWarning ───────────────────────────────╮ │ The estimator's `categorical_features` parameter has been set to handle categorical │ │ features. Previous value: `categorical_features=[30]`. │ │ │ │ Category : skforecast.exceptions.IgnoredArgumentWarning │ │ Location : │ │ /home/ubuntu/anaconda3/envs/skforecast_22_py13/lib/python3.13/site-packages/skforeca │ │ st/utils/utils.py:626 │ │ Suppress : warnings.simplefilter('ignore', category=IgnoredArgumentWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
| mean_absolute_error | |
|---|---|
| 0 | 48.743042 |
CatBoost¶
# 创建预测器
# ==============================================================================
forecaster = ForecasterRecursive(
estimator = CatBoostRegressor(
random_state=123,
silent=True,
allow_writing_files=False,
boosting_type = 'Plain', # 更快的训练速度
leaf_estimation_iterations = 3, # 更快的训练速度
),
lags = 24,
window_features = window_features,
categorical_features = 'auto'
)
# 超参数搜索
# ==============================================================================
lags_grid = [48, 72, [1, 2, 3, 23, 24, 25, 167, 168, 169]]
def search_space(trial):
search_space = {
'n_estimators' : trial.suggest_int('n_estimators', 100, 1000, step=100),
'max_depth' : trial.suggest_int('max_depth', 3, 10),
'learning_rate' : trial.suggest_float('learning_rate', 0.01, 1),
'lags' : trial.suggest_categorical('lags', lags_grid)
}
return search_space
results_search, frozen_trial = bayesian_search_forecaster(
forecaster = forecaster,
y = data.loc[:end_validation, 'users'],
exog = data.loc[:end_validation, exog_features],
cv = cv_search,
search_space = search_space,
metric = 'mean_absolute_error',
n_trials = 20
)
╭─────────────────────────── OneStepAheadValidationWarning ────────────────────────────╮ │ One-step-ahead predictions are used for faster model comparison, but they may not │ │ fully represent multi-step prediction performance. It is recommended to backtest the │ │ final model for a more accurate multi-step performance estimate. │ │ │ │ Category : skforecast.exceptions.OneStepAheadValidationWarning │ │ Location : │ │ /home/ubuntu/anaconda3/envs/skforecast_22_py13/lib/python3.13/site-packages/skforeca │ │ st/model_selection/_utils.py:640 │ │ Suppress : warnings.simplefilter('ignore', category=OneStepAheadValidationWarning) │ ╰──────────────────────────────────────────────────────────────────────────────────────╯
# 在测试集上进行回测
# ==============================================================================
metric_catboost, predictions = backtesting_forecaster(
forecaster = forecaster,
y = data['users'],
exog = data[exog_features],
cv = cv_backtesting,
metric = 'mean_absolute_error'
)
metric_catboost
| mean_absolute_error | |
|---|---|
| 0 | 45.696174 |
结论¶
借助 skforecast 提供的功能,在预测问题中使用梯度提升模型变得非常简便。
如本文所示,将外生变量纳入预测器可以显著提升预测性能。
类别特征可以直接作为外生变量使用,无需预处理。这得益于 LightGBM、XGBoost 和 HistGradientBoostingRegressor 对类别特征的原生支持。
metrics = pd.concat(
[metric_baseline, metric_lgbm, metric_xgboost, metric_histgb, metric_catboost]
)
metrics.index = [
"Baseline",
"LGBMRegressor",
"XGBRegressor",
"HistGradientBoostingRegressor",
"CatBoostRegressor",
]
metrics.round(2).sort_values(by="mean_absolute_error")
| mean_absolute_error | |
|---|---|
| CatBoostRegressor | 45.70 |
| LGBMRegressor | 46.22 |
| HistGradientBoostingRegressor | 48.74 |
| XGBRegressor | 49.53 |
| Baseline | 91.67 |
✏️ 注意
为便于说明流程,本示例中的超参数搜索规模较小。若要获得最优结果,可能需要对每个模型进行更大规模的搜索。
会话信息¶
import session_info
session_info.show(html=False)
----- astral 3.2 catboost 1.2.10 feature_engine 1.9.4 lightgbm 4.6.0 matplotlib 3.10.8 numpy 2.1.3 optuna 4.8.0 pandas 2.3.3 plotly 6.6.0 session_info v1.0.1 shap 0.51.0 skforecast 0.22.0 sklearn 1.7.2 statsmodels 0.14.6 xgboost 3.2.0 ----- IPython 9.11.0 jupyter_client 8.8.0 jupyter_core 5.9.1 ----- Python 3.13.12 | packaged by Anaconda, Inc. | (main, Feb 24 2026, 16:13:31) [GCC 14.3.0] Linux-5.15.0-1084-aws-x86_64-with-glibc2.31 ----- Session information updated at 2026-04-23 12:25
引用¶
如何引用本文
如果您使用了本文或其中任何部分,请注明来源,谢谢!
使用梯度提升进行时间序列预测:Skforecast、XGBoost、LightGBM 与 CatBoost,作者:Joaquín Amat Rodrigo 与 Javier Escobar Ortiz,遵循 署名-非商业性使用-相同方式共享 4.0 国际许可协议,发布于 https://www.cienciadedatos.net/documentos/py39-forecasting-time-series-with-skforecast-xgboost-lightgbm-catboost.html
如何引用 skforecast
如果您在发表的作品中使用了 skforecast,欢迎引用已发布的软件。
Zenodo:
Amat Rodrigo, Joaquin, & Escobar Ortiz, Javier. (2024). skforecast (v0.22.0). Zenodo. https://doi.org/10.5281/zenodo.8382788
APA:
Amat Rodrigo, J., & Escobar Ortiz, J. (2024). skforecast (Version 0.22.0) [Computer software]. https://doi.org/10.5281/zenodo.8382788
BibTeX:
@software{skforecast, author = {Amat Rodrigo, Joaquin and Escobar Ortiz, Javier}, title = {skforecast}, version = {0.22.0}, month = {03}, year = {2026}, license = {BSD-3-Clause}, url = {https://skforecast.org/}, doi = {10.5281/zenodo.8382788} }
喜欢这篇文章吗?您的支持非常重要
您的捐助将帮助我持续创作免费教育内容。衷心感谢!😊
本作品由 Joaquín Amat Rodrigo 与 Javier Escobar Ortiz 创作,遵循 署名-非商业性使用-相同方式共享 4.0 国际许可协议。
允许:
-
共享:以任何媒介或格式复制、发行本作品。
-
演绎:对本作品进行混合、改编、在本作品基础上进行创作。
须遵守以下条款:
-
署名:您必须给予适当的署名,提供指向本许可协议的链接,并说明是否对原作进行了改动。您可以用任何合理的方式进行署名,但不得以任何方式暗示许可人为您或您的使用方式背书。
-
非商业性使用:您不得将本作品用于商业目的。
-
相同方式共享:如果您对本作品进行了混合、改编或在其基础上进行创作,您必须以与原作相同的许可协议发布您的贡献。
