简介这是一份面向时间序列预测与机器学习入门者的Xgboost实战案例包兼顾时间序列预测和分类两类任务适合希望理解从数据分析、特征工程到模型训练与评估完整流程的学习者。压缩包共3个文件由2个Python脚本和1个CSV数据集组成整体约407KB脚本可直接运行数据文件用于配套实验。目前已有6033人学习下载。内容围绕Xgboost这一梯度提升树算法展开包含模型训练、本地保存、未来数值预测及预测值与实际观测值的对比评估并附有较清晰的分步注释便于读者梳理输入特征选择、序列生成和性能验证等关键环节。通过阅读与运行代码可快速搭建可复用的时间序列实验骨架理解分类与预测任务之间的差异并积累数据分析与特征工程的实际操作经验。1. 从一份能点开就跑的案例包说起XGBoost 做时间序列到底靠不靠谱时间序列预测模型实战案例XGBoost这类 Python 机器学习软件/插件式资源最怕变成只贴一段 model.fit 的演示。我拿到这份案例包时先看它有没有把预测和分类拆成两个可运行入口一份 notebook 从原始时间列构造滞后特征另一份把连续目标改成类别标签再训练 XGBoost。适合已经有表格数据、想用树模型快速搭 baseline 的人也适合被 ARIMA 调参折磨过、想换一条路验证特征工程的人。它不解决所有时序问题但能把“点开就能跑”落到依赖、数据切分和评估口径上。2. 先把时间序列改成监督学习表滞后特征、滑窗与时间戳拆解2.1 为什么 XGBoost 不能直接吃原始时间列XGBoost 本质上是一个表格型集成模型它擅长在特征空间里找分裂点但不自带时间依赖建模能力。把timestamp直接丢进去树模型只能学到“某个时间戳附近的目标值偏高”这类记忆性规律遇到未来时间点就失效。常见做法是先把时间序列改造成监督学习表每一行代表一个预测时刻目标列是当前时刻的y特征列只允许使用当前时刻之前的信息例如lag_1、lag_7、roll_mean_7、小时、星期几。这个转换过程决定了模型的上限。很多人一上来就调max_depth、learning_rate但时间序列任务里特征泄漏才是最大的黑匣子滚动窗口如果包含了当前时刻的y验证集指标会好得离谱上线后直接翻车。我的习惯是先把“当前行能用哪些历史信息”写清楚再写特征函数最后才碰模型参数。这份案例包把预测和分类都放在同一个特征构造逻辑下目的就是让两条任务线共用一套时间边界。另一个选型理由是速度。中小规模表格数据上XGBoost 训练和调参的反馈周期短特征重要性也能直接看适合先跑出一个可解释的 baseline。如果数据量很大、序列很长再考虑深度时序模型但在“先验证问题是否可预测”的阶段XGBoost 往往更顺手。2.2 滞后特征与滚动窗口的代码落地先写一个可复用的滞后特征函数。关键点是shift必须放在rolling之前确保滚动窗口只看到过去值不把当前行的目标值卷进去。下面这段代码可以直接放进 notebook 里跑import pandas as pd import numpy as np def make_lag_features(df, target_col, lags(1, 2, 3, 7, 14), windows(7, 14)): df df.sort_values(timestamp).reset_index(dropTrue).copy() # 滞后特征lag_1 表示上一时刻的目标值 for lag in lags: df[flag_{lag}] df[target_col].shift(lag) # 滚动特征先 shift(1) 再 rolling避免当前值进入窗口 shifted df[target_col].shift(1) for w in windows: df[froll_mean_{w}] shifted.rolling(windoww, min_periodsw).mean() df[froll_std_{w}] shifted.rolling(windoww, min_periodsw).std() # 一阶差分描述最近两个历史点的变化方向 df[diff_1] df[target_col].shift(1).diff(1) return df这段代码的逻辑是先按时间排序保证shift取到的是真实上一行lags控制回看深度windows控制滚动统计的窗口长度。min_periodsw会让窗口不足时产生NaNXGBoost 原生支持缺失值所以不需要急着填零。diff_1用的是shift(1).diff(1)等价于lag_1 - lag_2不是当前值减上一值避免泄漏。参数上lags(1,2,3,7,14)适合有明显周周期的数据如果数据是小时级可以加24、48。windows(7,14)不要一上来铺太大窗口越长开头缺失行越多训练样本越少。我的习惯是先跑lag_1加roll_mean_7的最小组合确认模型能学到东西再逐步加特征。2.3 时间戳特征和外部变量的拼接边界时间戳本身可以拆成小时、星期、月份、是否周末。树模型对“小时23”和“小时0”的数值距离不敏感直接给整数也能用但周期性强的场景建议加 sin/cos 编码。下面这段代码把时间特征补齐def add_time_features(df): ts pd.to_datetime(df[timestamp]) df[hour] ts.dt.hour df[weekday] ts.dt.weekday df[month] ts.dt.month df[is_weekend] (ts.dt.weekday 5).astype(int) df[dayofyear] ts.dt.dayofyear # 小时和星期的周期编码减少边界处的数值跳变 df[hour_sin] np.sin(2 * np.pi * df[hour] / 24) df[hour_cos] np.cos(2 * np.pi * df[hour] / 24) df[weekday_sin] np.sin(2 * np.pi * df[weekday] / 7) df[weekday_cos] np.cos(2 * np.pi * df[weekday] / 7) return df这里的参数没有玄学hour按 24 周期编码weekday按 7 周期编码。is_weekend是布尔转 0/1适合树模型直接分裂。dayofyear适合有季节性的数据但如果训练集只覆盖半年它可能让模型记住具体日期验证时要看特征重要性是否异常高。外部变量拼接要守住“未来可知”边界。促销计划、节假日标记、天气预报这类在预测时刻已知的变量可以拼同期销量、同期流量这类未来不可知的变量不能拼。常见做法是用merge_asof做向后对齐def merge_exog(df, exog, ontimestamp): df df.copy() exog exog.copy() df[on] pd.to_datetime(df[on]) exog[on] pd.to_datetime(exog[on]) return pd.merge_asof( df.sort_values(on), exog.sort_values(on), onon, directionbackward, tolerancepd.Timedelta(1h) )directionbackward表示只取当前时刻及之前最近的一条外部记录tolerance限制最大对齐间隔避免把很久以前的外部变量硬拼到当前行。这个函数放在特征构造之后、切分之前都可以但外部变量的缺失值要单独检查不能默认 XGBoost 能处理所有脏数据。提示先画一张“时间切分图”标出训练段、验证段和测试段再检查每个特征列在切分点之后是否还引用了未来行。这个动作比调参更能避免假高分。3. 预测任务从单步回归到滚动回测的完整训练链路3.1 切分不能随机按时间切、留出最后一段时间序列的验证集必须比训练集晚。随机train_test_split会让模型看到未来信息指标虚高这是血泪经验里最常见的一条。下面这段切分代码把最后 20% 作为验证段前面全部用来训练import pandas as pd import numpy as np from xgboost import XGBRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error def time_split(df, valid_ratio0.2): n len(df) cut int(n * (1 - valid_ratio)) train df.iloc[:cut].copy() valid df.iloc[cut:].copy() return train, valid df make_lag_features(df, target_coly) df add_time_features(df) train, valid time_split(df, valid_ratio0.2) feature_cols [ c for c in df.columns if c.startswith((lag_, roll_, diff_, hour, weekday, month, is_weekend, dayofyear)) ]valid_ratio0.2不是固定值。数据量小的时候可以留 10% 到 15%数据量大且季节性强的时候至少留一个完整周期。feature_cols用前缀筛选避免把timestamp、y或外部 ID 列误送进模型。注意特征构造要在排序后做切分前不要打乱行顺序。如果要做更稳的评估用TimeSeriesSplit做滚动回测。它的每一折都保证训练折在验证折之前test_size是每折验证样本数不是比例from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5, test_size24) for fold, (tr_idx, va_idx) in enumerate(tscv.split(df)): tr, va df.iloc[tr_idx], df.iloc[va_idx] # 这里重新训练模型记录每折 MAE/RMSEn_splits5表示切 5 折test_size24适合小时级数据留一天做验证。如果数据是日级可以改成 7 或 14。滚动回测比单次留出更接近上线后的滚动预测场景但训练次数多调参阶段可以先单次留出最终验收再跑回测。3.2 XGBRegressor 参数与早停回归任务用reg:squarederror作为目标函数配合早停控制树的数量。下面是一组我常用的起点参数model XGBRegressor( objectivereg:squarederror, n_estimators2000, learning_rate0.03, max_depth6, min_child_weight5, subsample0.8, colsample_bytree0.8, reg_lambda1.0, random_state42, n_jobs-1, ) model.fit( train[feature_cols], train[y], eval_set[(valid[feature_cols], valid[y])], early_stopping_rounds100, verboseFalse, ) pred model.predict(valid[feature_cols])这段代码的逻辑是n_estimators2000给一个较大的上限early_stopping_rounds100让验证集指标连续 100 轮不提升就停下实际树数量由早停决定。learning_rate0.03偏保守适合先看稳定效果如果训练太慢可以提到 0.05 到 0.1。max_depth6控制单棵树深度min_child_weight5防止叶子节点样本太少。subsample和colsample_bytree分别做行采样和列采样降低过拟合。参数作用我常用起点n_estimators树的数量上限1000 到 3000learning_rate每棵树的贡献权重0.03 到 0.1max_depth单棵树最大深度4 到 8min_child_weight叶子节点最小样本权重3 到 10subsample行采样比例0.7 到 0.9colsample_bytree列采样比例0.7 到 0.9reg_lambdaL2 正则强度1 到 10注意不同 XGBoost 版本对early_stopping_rounds的传参位置有差异有的版本放在fit里有的版本放在构造器里。资源包的 notebook 如果点开报这个错先看版本差异不要急着改数据。3.3 评估指标与残差检查回归任务不要只看 RMSE。目标值量纲不同RMSE 很难跨数据集比较目标值接近零时MAPE 会爆炸。我一般同时看 MAE、RMSE 和 MAPE并且检查残差按时间段的分布def mape(y_true, y_pred, eps1e-6): y_true np.asarray(y_true) y_pred np.asarray(y_pred) return np.mean(np.abs((y_true - y_pred) / np.maximum(np.abs(y_true), eps))) * 100 rmse np.sqrt(mean_squared_error(valid[y], pred)) print(MAE, mean_absolute_error(valid[y], pred)) print(RMSE, rmse) print(MAPE%, mape(valid[y], pred)) resid valid[y].values - pred resid_df pd.DataFrame({ timestamp: pd.to_datetime(valid[timestamp].values), resid: resid }) print(resid_df.groupby(resid_df[timestamp].dt.hour)[resid].mean())mape里加eps是防止真实值为零时除零。resid_df.groupby(...).mean()看的是残差均值如果某个小时段的残差长期为正或长期为负说明模型在该时段有系统偏差时间特征或滞后特征没吃进去。残差图比单个指标更能暴露问题尤其是周期性数据。4. 分类任务把连续值变成标签后XGBoost 分类器怎么不翻车4.1 标签怎么造阈值、分位数与三分类分类任务的第一步不是选模型而是定义标签。连续目标可以按未来一期涨跌做二分类也可以按分位数切三档。下面这段代码先构造未来收益再在训练段上算分位数阈值避免用验证集分布定标签def add_future_return(df, target_col, horizon1): df df.copy() future df[target_col].shift(-horizon) df[future_ret] (future - df[target_col]) / df[target_col].replace(0, np.nan) return df df add_future_return(df, target_coly, horizon1) train_df, valid_df time_split(df, valid_ratio0.2) # 分位数阈值只用训练段计算 low, high train_df[future_ret].quantile([0.33, 0.67]) def to_three_class(s): return np.select([s low, s high], [0, 2], default1) train_df train_df.dropna(subset[future_ret]).copy() valid_df valid_df.dropna(subset[future_ret]).copy() train_df[label] to_three_class(train_df[future_ret]) valid_df[label] to_three_class(valid_df[future_ret])horizon1表示预测下一期q(0.33,0.67)表示按训练段未来收益的 33% 和 67% 分位切三档。dropna是为了去掉最后horizon行没有未来值的样本。关键点是分位数阈值只能来自训练段否则验证集分布会泄漏到标签定义里指标看起来好看但不真实。如果做二分类直接把future_ret 0转成 0/1不要用全量数据的中位数当阈值。标签边界附近的样本噪声很大后面评估时要单独看这部分表现。4.2 类别不平衡与评估指标分类任务常见问题是类别不平衡。二分类可以用scale_pos_weight调整正类权重多分类可以用sample_weight给少数类更高权重from xgboost import XGBClassifier from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score neg int((train_df[label] 0).sum()) pos int((train_df[label] 1).sum()) scale_pos_weight neg / max(pos, 1) clf XGBClassifier( objectivebinary:logistic, n_estimators800, learning_rate0.05, max_depth5, min_child_weight3, subsample0.8, colsample_bytree0.8, scale_pos_weightscale_pos_weight, eval_metriclogloss, random_state42, n_jobs-1, ) clf.fit( train_df[feature_cols], train_df[label], eval_set[(valid_df[feature_cols], valid_df[label])], early_stopping_rounds50, verboseFalse, ) proba clf.predict_proba(valid_df[feature_cols])[:, 1] pred_label (proba 0.5).astype(int) print(confusion_matrix(valid_df[label], pred_label)) print(classification_report(valid_df[label], pred_label)) print(AUC, roc_auc_score(valid_df[label], proba))scale_pos_weightneg/pos只在二分类里有意义正类少的时候给它更高权重。eval_metriclogloss适合概率输出early_stopping_rounds50比回归任务可以短一些因为分类指标波动更大。predict_proba[:,1]取正类概率阈值 0.5 不是固定答案要根据业务成本和验证集 ROC/PR 曲线调整。评估不要只看 accuracy。类别不平衡时模型全预测多数类也能有高 accuracy但 recall 会很差。classification_report看每类 precision/recall/f1confusion_matrix看具体错分方向roc_auc_score看排序能力。多分类把objective改成multi:softprob加num_class3评估看 macro f1 和每类 recall。4.3 概率输出与标签边界检查分类模型输出概率后别急着按 0.5 一刀切。先看概率分布和标签边界样本valid_df valid_df.copy() valid_df[proba] proba valid_df[is_border] valid_df[future_ret].abs() valid_df[future_ret].abs().quantile(0.1) print(valid_df.groupby(is_border)[proba].describe())这段代码把未来收益绝对值最小的 10% 标成边界样本观察模型给它们的概率是否集中在中段。如果边界样本概率接近 0.5说明模型本身也在犹豫这部分样本的标签噪声大评估时最好单独报告。常见做法是训练时保留边界样本但汇报指标时拆开看避免被边界样本拉低整体判断。注意分类标签来自未来目标值特征构造必须停在当前时刻。不要把future_ret或任何由未来目标值派生的列放进feature_cols否则模型会直接看到答案。5. 避坑与排查时间序列 XGBoost 最常见的五处翻车5.1 特征泄漏滚动窗口把未来值卷进来现象验证集 MAE 低得离谱特征重要性里roll_mean_7排第一但上线后第一周预测就崩。原因通常是滚动窗口直接对原始目标列做rolling没有先shift(1)当前行的y被卷进窗口模型在训练时看到了答案。解决所有滚动统计先shift(1)再做rolling所有滞后特征用正数shift特征构造完打印前 20 行人工检查lag_1是否等于上一行目标值。代码层面可以加一条断言检查lag_1与y.shift(1)是否一致。5.2 时间切分做错随机 shuffle 导致指标虚高现象交叉验证分数很漂亮换到最后一段真实时间上预测误差翻倍。原因是用了随机train_test_split或默认 shuffle 的交叉验证训练集里混入了未来时间点。解决按时间排序后切分验证集取最后一段滚动回测用TimeSeriesSplit不要用KFold。如果数据有多个实体先按实体内时间切分再拼接不能把同一时刻的不同实体随机打散。5.3 缺失值与无穷大XGBoost 能处理缺失但不能处理 inf现象训练报错Input contains infinity或损失变成nan。原因是特征里有除以零产生的infXGBoost 对缺失值有默认处理但对无穷大不买账。解决在特征构造后用replace([np.inf, -np.inf], np.nan)统一转成缺失再让模型处理目标值也要检查是否为零导致比率特征爆炸。常见做法是给分母加eps或者直接用差分代替比率。5.4 多步预测直接递归导致误差累积现象单步预测很准连续预测 7 步后曲线越来越平或越来越偏。原因是递归多步把上一轮预测值写回目标列再重新构造滞后特征误差一步步放大。解决能直接多步建模就训练多个模型每个模型预测一个步长必须递归时限制步长监控每步残差并保留一个 naive baseline 做对照。滚动回测时每折只预测固定步长不要一次性递归很远。5.5 分类标签边界抖动与评估口径不一致现象同一份数据换个随机种子分类 recall 波动很大或者离线 AUC 很高上线后正类抓不住。原因是标签边界附近的样本本身噪声大阈值 0.5 也不一定符合业务成本。解决固定标签定义和分位数阈值阈值只在训练段计算评估时同时看 AUC、PR 曲线和固定 recall 下的 precision边界样本单独统计。分类任务不要只报一个 accuracy尤其类别不平衡时。6. 固化与验证把 notebook 变成可复现的滚动预测脚本notebook 里跑通只是第一步真正落地要把模型、特征列和滚动预测逻辑打包。我一般用 joblib 存一个 bundle把模型和特征列一起保存避免加载后特征顺序错位import joblib import pandas as pd bundle { model: model, feature_cols: feature_cols, target_col: y, } joblib.dump(bundle, xgb_ts_bundle.joblib) def forecast_next(bundle, history_df, steps1): model bundle[model] feature_cols bundle[feature_cols] hist history_df.copy() out [] for _ in range(steps): hist make_lag_features(hist, target_colbundle[target_col]) hist add_time_features(hist) last hist.iloc[[-1]] yhat float(model.predict(last[feature_cols])[0]) out.append(yhat) next_ts pd.to_datetime(last[timestamp].iloc[0]) pd.Timedelta(hours1) hist pd.concat([ hist, pd.DataFrame([{timestamp: next_ts, bundle[target_col]: yhat}]) ], ignore_indexTrue) return out这段代码的关键不是joblib.dump而是滚动预测时每一轮都重新构造滞后特征和 Sorting 后取最后一行。steps控制递归步长feature_cols必须和训练时完全一致否则预测会悄悄错列。生产环境里我会把特征构造做成增量缓存不在每轮重算全表但验证阶段先用这个版本对齐结果。验证方法很简单拿最后一段历史数据手动遮住未来 N 步用forecast_next递归预测再和真实值比 MAE。如果递归 3 步后误差明显跳升先检查滞后特征是否每轮都更新再检查时间戳步长是否和数据频率一致。从那以后我每次拿到时间序列数据都先画时间切分图、再跑一遍 naive baseline确认没有泄漏才调参。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?