当我们在纸上理清了时间序列特征的理论逻辑,接下来最关键的一步,便是将这些逻辑转化为代码 —— 毕竟 AI 无法直接读懂我们的 “时间思维”,它需要我们用数据工具将规律 “翻译” 成数字语言。今天我们将以一份真实的销售时间序列数据为样本,用 Pandas 亲手构建基本特征、滞后特征、窗口统计特征与趋势特征,再通过 LightGBM 模型的实战对比,直观感受 “好特征” 对预测效果的改变。在开始前,我们先明确实战的核心目标:用代码实现理论中的四类特征,并验证特征构造对模型性能的提升。即使你对 Python 语法不熟悉,也不必担心 —— 我们会像拆解机械零件一样,一步步解释每行代码的作用,让 “代码逻辑” 与 “时间规律” 对应起来。

一、准备工作

首先,我们需要 “原材料” 与 “工具”:

  • 数据:一份某连锁超市 2022-2023 年的日销售数据,包含 “日期”(date)与 “当日销售额”(sales)两列,共 730 条记录(正好两年)。
  • 工具:Python 的 Pandas 库(用于数据处理与特征构造)、LightGBM 库(用于时间序列预测)、Matplotlib 库(用于结果可视化)、Scikit-learn 库(用于模型评估)。

我们先通过 Pandas 读取数据,看看原始数据的样子:

# 导入所需库
import pandas as pd
import numpy as np
import lightgbm as lgb
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import mean_absolute_error, r2_score
import matplotlib.pyplot as plt

# 读取数据(假设数据保存在sales_data.csv文件中)
df = pd.read_csv("sales_data.csv")

# 关键步骤:将“日期”列转换为Pandas的datetime格式(让电脑理解“时间”)
df["date"] = pd.to_datetime(df["date"])

# 将“日期”设为索引(方便后续按时间操作)
df = df.set_index("date")

# 查看数据前5行与基本信息
print("原始数据前5行:")
print(df.head())
print("\n数据基本信息:")
print(df.info())

运行代码后,我们会看到这样的结果:

原始数据前5行:
            sales
date             
2022-01-01  12500
2022-01-02  13200
2022-01-03  14100
2022-01-04  11800
2022-01-05  12900

数据基本信息:
DatetimeIndex: 730 entries, 2022-01-01 to 2023-12-31
Data columns (total 1 columns):
 #   Column  Non-Null Count  Dtype
---  ------  --------------  -----
 0   sales   730 non-null     int64
dtypes: int64(1)
memory usage: 11.4 KB

此时的原始数据,就像一串 “只有数字的日记”—— 我们只知道每天的销售额,却不知道 “这一天是不是周末”“是不是节假日”“和前几天的销量有什么关系”。接下来,我们就用 Pandas 的工具,为这串数字 “补充细节”。

二、特征构造实战

1. 基本特征

基本特征的核心是 “提取时间中的隐藏信息”,比如星期几、是否周末、是否节假日等。Pandas 的dt属性(datetime 属性)可以轻松实现这一点:

# 1. 构造基本时间特征
# 提取年、月、日
df["year"] = df.index.year  # 年份(如2022)
df["month"] = df.index.month  # 月份(1-12)
df["day"] = df.index.day  # 日期(1-31)
df["dayofweek"] = df.index.dayofweek  # 星期几(0=周一,6=周日)

# 构造“是否周末”特征(周日=6、周六=5,设为1;其他为0)
df["is_weekend"] = np.where(df["dayofweek"].isin([5, 6]), 1, 0)

# 构造“是否月初/月末”特征(1号=月初,最后一天=月末,设为1;其他为0)
# 月初:day=1
df["is_month_start"] = np.where(df["day"] == 1, 1, 0)
# 月末:判断是否为当月最后一天(通过对比“下一天的月份”是否变化)
df["is_month_end"] = np.where(df.index + pd.Timedelta(days=1).month != df.index.month, 1, 0)

# 构造“是否节假日”特征(以2022-2023年部分节假日为例,实际可扩展)
holidays = ["2022-01-01", "2022-02-01", "2022-04-05", "2023-01-01", "2023-01-22"]
holidays = pd.to_datetime(holidays)  # 转换为datetime格式
df["is_holiday"] = np.where(df.index.isin(holidays), 1, 0)

# 查看构造的基本特征
print("基本特征构造后的数据前5行:")
print(df[["sales", "year", "month", "dayofweek", "is_weekend", "is_holiday"]].head())

运行后,数据会新增 7 列基本特征,比如 2022-01-01(周六)会被标记为 “is_weekend=1”“is_holiday=1”,而 2022-01-04(周二)则是 “is_weekend=0”“is_holiday=0”。这些标签就像给 AI 的 “提示”:“这一天是周末,销量可能更高;这一天是节假日,销量可能有特殊波动”。

2. 滞后特征

滞后特征的核心是 “引用过去某时刻的数据”,比如 “前 1 天的销量”“前 3 天的销量”。Pandas 的shift(n)函数是实现这一功能的关键 ——shift(1)表示 “将数据向下移动 1 行”,相当于 “当前行对应前 1 天的值”。

# 2. 构造滞后特征(选择滞后1天、3天、7天,对应t-1、t-3、t-7)
# 滞后1天:当天销量 = 前1天销量(t-1)
df["lag_1"] = df["sales"].shift(1)
# 滞后3天:当天销量 = 前3天销量(t-3)
df["lag_3"] = df["sales"].shift(3)
# 滞后7天:当天销量 = 前7天销量(t-7)(考虑周度规律)
df["lag_7"] = df["sales"].shift(7)

# 注意:shift后会产生NaN(前1天没有“前1天的数据”),需要删除或填充
df = df.dropna()  # 简单处理:删除包含NaN的行(实际可根据需求填充)

# 查看构造的滞后特征
print("滞后特征构造后的数据前5行:")
print(df[["sales", "lag_1", "lag_3", "lag_7"]].head())

运行后,我们会看到:2022-01-08 的 “lag_1” 是 2022-01-07 的销量,“lag_7” 是 2022-01-01 的销量。这就像给 AI 提供了 “历史参考”:“想预测今天卖多少,先看看昨天、3 天前、1 周前卖了多少”。

为什么选择滞后 1、3、7 天?这不是随意的 —— 滞后 1 天对应 “短期惯性”(昨天卖得多,今天可能也多),滞后 3 天对应 “中期波动”,滞后 7 天对应 “周度规律”(上周今天的销量对本周今天有参考意义)。实际场景中,可根据数据的 “自相关性” 调整滞后步数(比如用 ACF 图判断)。

3. 窗口统计特征

窗口统计特征的核心是 “计算过去 N 个时间单位的统计量”,比如 “过去 7 天的平均销量”“过去 30 天的最大销量”。Pandas 的rolling(window=n)函数是核心工具 ——rolling(7)表示 “以 7 天为一个窗口”,再结合mean()(均值)、std()(标准差)、max()(最大值)等函数,就能得到窗口统计结果。

# 3. 构造窗口统计特征(选择7天窗口和30天窗口,对应周度和月度规律)
# 过去7天的滚动均值(周平均销量)
df["roll_7_mean"] = df["sales"].rolling(window=7).mean()
# 过去7天的滚动标准差(周销量波动大小)
df["roll_7_std"] = df["sales"].rolling(window=7).std()
# 过去30天的滚动最大值(月内最高销量)
df["roll_30_max"] = df["sales"].rolling(window=30).max()
# 过去30天的滚动最小值(月内最低销量)
df["roll_30_min"] = df["sales"].rolling(window=30).min()

# 同样需要删除rolling产生的NaN(前6天没有7天窗口数据,前29天没有30天窗口数据)
df = df.dropna()

# 查看构造的窗口统计特征
print("窗口统计特征构造后的数据前5行:")
print(df[["sales", "roll_7_mean", "roll_7_std", "roll_30_max", "roll_30_min"]].head())

以 “roll_7_mean” 为例,2022-01-14 的 “roll_7_mean” 是 2022-01-08 至 2022-01-14 这 7 天的销量平均值。这就像给 AI 提供了 “阶段性总结”:“想知道今天的销量是否正常,先看看过去一周的平均水平;想知道本月的销售上限,看看过去 30 天的最高销量”。

窗口大小的选择同样有讲究:7 天窗口对应 “周度规律”(适合零售、餐饮等周内波动明显的场景),30 天窗口对应 “月度规律”(适合家电、汽车等月度波动明显的场景)。如果窗口太小,统计结果会受短期噪声影响;如果窗口太大,又会忽略近期趋势 —— 这需要结合业务场景调整。

4. 趋势 / 季节性特征

趋势特征的核心是 “捕捉销量的上升或下降趋势”,我们可以通过 “不同窗口的移动平均线之差” 实现;季节性特征则可以通过 “与去年同期销量的比值” 实现(同比)。

# 4. 构造趋势/季节性特征
# 趋势特征:过去30天移动平均 - 过去7天移动平均(差值为正=上升趋势,负=下降趋势)
df["trend_30_7"] = df["sales"].rolling(window=30).mean() - df["sales"].rolling(window=7).mean()

# 季节性特征:与去年同期销量的比值(同比)
# 步骤1:构造“去年同期的销量”(shift(365),因为一年约365天)
df["sales_last_year"] = df["sales"].shift(365)
# 步骤2:计算同比比值(当前销量 / 去年同期销量,比值>1=同比增长,<1=同比下降)
df["yoy_ratio"] = df["sales"] / df["sales_last_year"]

# 删除NaN(shift(365)会产生前365天的NaN)
df = df.dropna()

# 查看构造的趋势/季节性特征
print("趋势/季节性特征构造后的数据前5行:")
print(df[["sales", "trend_30_7", "sales_last_year", "yoy_ratio"]].head())

运行后,2023-01-01 的 “sales_last_year” 是 2022-01-01 的销量,“yoy_ratio” 则是 2023 年元旦销量与 2022 年元旦销量的比值 —— 如果比值是 1.2,说明 2023 年元旦销量同比增长 20%。而 “trend_30_7” 如果是正数,说明过去 30 天的平均销量高于过去 7 天,整体趋势在上升(比如春节前的备货期,销量可能逐渐上升)。

至此,我们已经用 Pandas 构造了4 类共 15 个特征(7 个基本特征 + 3 个滞后特征 + 4 个窗口统计特征 + 1 个趋势特征 + 1 个季节性特征)。此时的数据,已经从 “只有数字的日记” 变成了 “有标签、有历史、有总结、有趋势的完整记录”—— 接下来,我们就用 LightGBM 模型,对比 “只用原始数据” 和 “用构造特征” 的预测效果。

三、模型实战

时间序列预测与普通预测的最大区别是:不能随机划分训练集和测试集(比如不能把 2023 年的数据放到训练集,2022 年的数据放到测试集),必须 “按时间顺序划分”—— 用过去的数据训练,用未来的数据测试。我们用TimeSeriesSplit实现这一逻辑,并用 “平均绝对误差(MAE)” 和 “决定系数(R²)” 评估模型性能(MAE 越小越好,R² 越接近 1 越好)。

1. 数据划分

我们选择 2022-2023 年的最后 30 天作为测试集(2023-12-01 至 2023-12-31),之前的数据作为训练集:

# 划分训练集与测试集(测试集为最后30天)
test_size = 30  # 测试集大小:30天
train_df = df.iloc[:-test_size]  # 训练集:除最后30天外的所有数据
test_df = df.iloc[-test_size:]   # 测试集:最后30天的数据

# 定义“只用原始数据”的特征(仅sales的滞后特征,模拟无特征构造的情况)
# 这里用滞后1、3、7天的销量作为原始特征(因为纯原始数据只有sales,无法直接训练)
raw_features = ["lag_1", "lag_3", "lag_7"]
# 定义“用构造特征”的特征(所有构造的特征)
constructed_features = [
    "year", "month", "dayofweek", "is_weekend", "is_holiday",  # 基本特征
    "lag_1", "lag_3", "lag_7",  # 滞后特征
    "roll_7_mean", "roll_7_std", "roll_30_max", "roll_30_min",  # 窗口统计特征
    "trend_30_7", "yoy_ratio"  # 趋势/季节性特征
]

# 准备训练集与测试集的X(特征)和y(目标变量:sales)
# 模型1:只用原始数据
X_train_raw = train_df[raw_features]
y_train_raw = train_df["sales"]
X_test_raw = test_df[raw_features]
y_test_raw = test_df["sales"]

# 模型2:用构造特征
X_train_constructed = train_df[constructed_features]
y_train_constructed = train_df["sales"]
X_test_constructed = test_df[constructed_features]
y_test_constructed = test_df["sales"]

2. 训练 LightGBM 模型

LightGBM 是时间序列预测中常用的模型(效率高、对非线性关系拟合好),我们用相同的参数训练两个模型,确保对比的公平性:

# 定义LightGBM的参数(固定参数,确保对比公平)
lgb_params = {
    "objective": "regression",  # 回归任务(预测销量)
    "metric": "mae",            # 评估指标:MAE
    "boosting_type": "gbdt",    # 梯度提升树
    "learning_rate": 0.05,      # 学习率
    "num_leaves": 31,           # 叶子节点数
    "verbose": -1               # 不输出日志
}

# 模型1:只用原始数据
# 转换为LightGBM的数据集格式
lgb_train_raw = lgb.Dataset(X_train_raw, y_train_raw)
lgb_test_raw = lgb.Dataset(X_test_raw, y_test_raw, reference=lgb_train_raw)

# 训练模型
model_raw = lgb.train(
    params=lgb_params,
    train_set=lgb_train_raw,
    num_boost_round=100,  # 迭代次数
    valid_sets=[lgb_test_raw],
    early_stopping_rounds=10,  # 早停:连续10轮无提升则停止
    verbose_eval=False
)

# 模型1预测
y_pred_raw = model_raw.predict(X_test_raw, num_iteration=model_raw.best_iteration)

# 模型2:用构造特征
# 转换为LightGBM的数据集格式
lgb_train_constructed = lgb.Dataset(X_train_constructed, y_train_constructed)
lgb_test_constructed = lgb.Dataset(X_test_constructed, y_test_constructed, reference=lgb_train_constructed)

# 训练模型
model_constructed = lgb.train(
    params=lgb_params,
    train_set=lgb_train_constructed,
    num_boost_round=100,
    valid_sets=[lgb_test_constructed],
    early_stopping_rounds=10,
    verbose_eval=False
)

# 模型2预测
y_pred_constructed = model_constructed.predict(X_test_constructed, num_iteration=model_constructed.best_iteration)

3. 结果评估

我们先计算两个模型的 MAE 和 R²,再用图表可视化预测值与真实值的对比:

# 计算模型性能指标
def evaluate_model(y_true, y_pred, model_name):
    mae = mean_absolute_error(y_true, y_pred)
    r2 = r2_score(y_true, y_pred)
    print(f"\n{model_name}性能:")
    print(f"MAE(平均绝对误差):{mae:.2f}")
    print(f"R²(决定系数):{r2:.4f}")
    return mae, r2

# 评估两个模型
mae_raw, r2_raw = evaluate_model(y_test_raw, y_pred_raw, "只用原始数据的模型")
mae_constructed, r2_constructed = evaluate_model(y_test_constructed, y_pred_constructed, "用构造特征的模型")

# 可视化结果(对比真实值、原始数据模型预测值、构造特征模型预测值)
plt.figure(figsize=(12, 6))
plt.plot(test_df.index, y_test_constructed, label="真实销量", color="blue", linewidth=2)
plt.plot(test_df.index, y_pred_raw, label="原始数据模型预测", color="red", linestyle="--", linewidth=1.5)
plt.plot(test_df.index, y_pred_constructed, label="构造特征模型预测", color="green", linestyle="-.", linewidth=1.5)
plt.title("时间序列预测结果对比(测试集:最后30天)")
plt.xlabel("日期")
plt.ylabel("销售额")
plt.legend()
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
预期结果解读:
  • MAE 对比:只用原始数据的模型 MAE 可能在 1500 左右,而用构造特征的模型 MAE 可能降至 800 左右 —— 这意味着构造特征让预测的 “平均误差” 减少了近一半。
  • R² 对比:只用原始数据的模型 R² 可能在 0.6 左右(说明模型只能解释 60% 的销量变化),而用构造特征的模型 R² 可能升至 0.85 左右(能解释 85% 的销量变化)。
  • 图表对比:绿色的 “构造特征模型预测线” 会更贴近蓝色的 “真实销量线”,尤其是在节假日(如 2023-12-25 圣诞节)或月末(2023-12-31),红色的 “原始数据模型预测线” 会出现明显偏差,而绿色线能更好地捕捉这些特殊波动。

四、最后小结

通过这次实战,我们不仅学会了用 Pandas 的shift()rolling()等函数构造时间序列特征,更重要的是理解了 “特征构造的本质”:

  • “术” 的层面shift(n)对应滞后特征,rolling(window).mean()对应窗口统计特征,dt.dayofweek对应基本特征 —— 这些是实现工具,需要熟练掌握,但不必死记硬背,关键是理解 “函数作用与时间规律的对应关系”。
  • “道” 的层面:特征构造不是 “越多越好”,而是 “越贴合业务规律越好”。比如零售场景的 “周末特征”、餐饮场景的 “午晚高峰特征”、电商场景的 “大促特征”,都是基于业务理解的 “精准构造”。如果脱离业务,盲目构造几十上百个特征,反而会让模型 “迷失方向”(过拟合)。

同时,我们也看到了 “好特征” 的价值:它能让简单的模型发挥出更好的效果,甚至比 “复杂模型 + 差特征” 的组合更优。在时间序列预测中,“数据预处理 + 特征构造” 往往占据了 70% 的工作量,而这 70% 的工作量,直接决定了最终模型的上限。

这个章节不必纠结于代码的细节,只需记住:时间序列的规律藏在 “时间标签”“历史数据”“阶段总结”“长期趋势” 里,而特征构造就是把这些规律 “告诉 AI” 的过程;你也可以尝试在这个基础上扩展 —— 比如用傅里叶变换提取季节性、用差分法提取趋势、用注意力机制捕捉长短期依赖,让特征更贴合你的业务数据。时间从不说话,但只要我们用对工具,就能读懂它留下的密码 —— 而特征构造,就是打开这扇密码门的第一把钥匙。未完待续............

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐