第一次正经做时间序列预测,我把一大半时间都耗在手动特征工程上。滑动均值、窗口标准差、滞后差分,一个个算完还不能确定到底哪个特征对模型有用。后来我把 tsfresh 和 AutoML 组合起来,让前者负责批量提取和时间相关的统计特征,后者负责自动调模型和超参,整条预测管线才真正轻松下来。这篇是我自己项目的落地记录,讲清楚这套组合的用法、踩过的坑,以及推荐设置。
1. 为什么时间序列特征工程是个硬骨头
1.1 手动特征工程的隐性成本
做时间序列预测的人,刚开始都会尝试手动堆特征。滑动平均、窗口标准差、滞后几期、一阶差分、周期性编码,再配合节假日哑变量,这已经算标准的“基本款”配置。麻烦不在于函数不会写,而在于每加一个特征,你都要验证它对不同序列、不同预测周期是否稳定。比如我有个项目要同时预测几十个门店的销量,每个门店的销售节奏差异很大,有的周末冲高,有的工作日平稳,手工写特征就等于把这些节奏挨个猜一遍,猜不中就白费。
更隐蔽的问题是竞争力天花板太低。你手写出来的特征,往往是自己“已经想到”的特征。但真实时间序列里还有频域信息、自相关系数、近似熵、趋势突变点等结构——这些不是盯着曲线肉眼就能发现的,手工算又慢又容易漏。等模型效果卡住的时候,你想加几个新特征,又得从代码、验证、对比开始重来一轮,整体节奏就被拖住了。
1.2 自动特征提取不是偷懒,是把精力留给建模
我第一次接触 tsfresh 时其实有怀疑:一个库能自动生成几百个特征,里面得有多少无效特征?用下来之后,我的看法变成了:正因为会自动生成很多,反而更需要一套自动筛选机制来把关。tsfresh 正好把“提取”和“筛选”两件事都包了,提取完还会用统计检验帮你挑出和目标相关的特征。
这让时间序列特征工程从“手工打磨”变成了“设定参数”。配合 AutoML 做后续建模,等于把特征提取、特征选择、模型选择、超参调整一连串环节全部管道化。整个过程我只需要操心数据质量、预测目标定义和时间切分是否正确。以我自己的体验来说,这套组合最大的价值不是让你变得更强,而是把容易出错、特别耗时、又很难复盘的环节标准化。
2. tsfresh 到底替你做了什么
2.1 特征提取器的组成和工作原理
tsfresh 的输入要求不复杂:一张长表,每行是一个时点上的观测,用column_id指定属于哪条时序(比如门店编号),用column_sort指定时间顺序,用column_value指定用来算特征的数值字段。它会按 id 分组,在每组内部按时间顺序计算大量特征。
这些特征大致能分几类,我整理了一份常用清单:
| 特征类别 | 说明 | 常见特征举例 |
|---|---|---|
| 基础统计 | 描述序列整体分布 | 均值、标准差、偏度、峰度、分位数、极值 |
| 时间依赖 | 刻画序列前后相关性 | 滞后自相关、差分统计、变化率 |
| 频域特征 | 从频谱角度看周期规律 | FFT 系数、频谱能量、频谱熵、峰值频率 |
| 非线性特征 | 捕捉复杂动态结构 | 近似熵、样本熵、非线性趋势项 |
| 趋势与突变 | 检测水平变化与漂移 | 逐步变化点、趋势强度、均值突变 |
你不需要一个一个手工算。调用extract_features之后,每个 id 各自生成一份特征矩阵,每行对应一条时间序列(或者一个滚动窗口)。这个宽表就是后续喂给 AutoML 的特征输入。
2.2 关键参数决定了跑得快不快、结果稳不稳
extract_features的参数看着多,真正影响体验的就是下面这五个。这张表是我在实际项目里反复用到的配置参考:
| 参数 | 作用 | 我的推荐设置 |
|---|---|---|
column_id | 指定时序分组字段 | 门店编号、设备编号等 |
column_sort | 指定时间顺序字段 | 日期列,注意先排序 |
column_value | 指定待提取特征的目标列 | 销量、流量、温度等 |
default_fc_parameters | 控制特征计算器的丰富程度 | 先用"efficient",后按需升级 |
n_jobs/chunksize | 并行度和内存控制 | 根据核数在 128 到 1024 之间调 chunk |
default_fc_parameters有三个常用档位:"minimal"、"efficient"、"comprehensive",特征数量逐级增加,计算耗时也逐级上升。我一开始用"comprehensive"直接把内存打满了,后来改成先用"efficient"跑通流程,再针对个别序列用"comprehensive"做深度尝试。这个“先瘦身后加餐”的思路,能省掉大量无谓等待。
2.3 筛选机制:为什么要做统计检验
提取出来几百个特征,不能全塞给模型。一方面冗余特征会让 AutoML 的搜索空间变大,训练时间拉长;另一方面随机噪声特征多了,模型容易过拟合。tsfresh 的select_features做的是把每个特征和目标变量做一次统计检验——回归任务用相关性检验,分类任务用组间差异检验——再通过多重检验校正来控制假阳性率。默认使用 Benjamini-Yekutieli 校正,fdr_level默认 0.05。
这个逻辑很像“先广撒网,再收紧网口”。同一组数据,筛选前可能 800 个特征,筛选后往往只剩 50 到 150 个,而且线下评估效果通常不降反升。这里有个重要的原则:筛选必须基于训练数据,不能拿全量数据做筛选后再切分,否则特征选择过程会把测试集信息带进模型,后面的评估指标再好看都是假的。
3. 接入 AutoML 前必须先避开的三个坑
3.1 特征泄漏:时间序列特有的坑
普通表格任务里,特征泄漏常常是因为不小心用了未来字段;时间序列里的泄漏更隐蔽——你算特征的时候用了整段序列,包括预测目标所在的那一段。比如你想预测明天的销量,但特征里的“过去 7 天均值”把明天也算进去了,这等于把答案提前告诉模型。
解决方法是把特征提取限制在严格按时间推进的窗口内。我常用的做法:先把原始长表按窗口切成多个样本,每个样本只取窗口[t-7, t)内的数据算特征,目标设为t+1的实际值。这样训练和预测时的信息边界是一致的。切记不要在切分之后,又拿全序列统一计算均值、方差之类的全局统计量当特征,除非你确认它们不包含未来信息。
还有一个容易忽略的点:如果你用了标准化或归一化,一定要在训练窗口里拟合缩放器,再应用到测试窗口。整条序列一起标准化,同样会引入未来分布的信息。
3.2 别一开始就用“全量套餐”
我第一次跑default_fc_parameters="comprehensive",数据规模不算大,但内存直接被打满。原因是 comprehensive 配置会启用几十种特征计算器,有些还要做傅里叶变换、熵估计这类相对重的计算,中间结果非常多。
后来我养成的习惯是:先用"minimal"或"efficient"配置快速跑通流程,确认数据和目标定义没问题,再针对一小部分序列用 comprehensive 配置做深度尝试。这个“先瘦身后加餐”的思路,能省掉大量无谓的等待时间。
这里的本质问题是计算复杂度。特征数量和时间序列长度、窗口数量近似线性相关,稍不留神就会翻好几倍。AutoML 本身也要占用大量训练时间,如果前面特征提取就花了半小时,迭代效率就很低了。
3.3 id 和时间列的类型问题
提取结果莫名其妙全是零方差,多半是数据本身的坑。比如column_id列混入了 null,或者column_sort列有空值、类型不统一,tsfresh 在分组和排序时会把这些样本当作“坏样本”处理,特征算出来要么缺失,要么恒等于某个值。
我的检查顺序是:先df.dtypes()看 id 和 time 列类型,再df[df[sort_col].isna()]看缺失,最后按 id 分组检查时间列是否严格递增。这三个动作做一遍,能排除我踩过的绝大多数低级问题。排序这一点尤其容易被忽略:id 列相同但时间列乱序,会让很多依赖历史顺序的特征彻底失去意义。
4. 一条可直接落地的 tsfresh + AutoML 管线搭建记录
4.1 从长表到特征矩阵
假设我们要做一个门店销量预测,原始数据大概是:store_id、date、sales,还有一个标签列future_sales表示未来某一天的销量。先把数据整理成长表,然后调用提取和筛选:
import pandas as pd from tsfresh import extract_features, select_features df = pd.read_csv("sales_long.csv") df["date"] = pd.to_datetime(df["date"]) # 按门店、按时间排序,保证时间顺序正确 df = df.sort_values(["store_id", "date"]).reset_index(drop=True) # 特征提取 X = extract_features( df, column_id="store_id", column_sort="date", column_value="sales", default_fc_parameters="efficient", n_jobs=-1, chunksize=256, impute=True, ) print("提取后特征数:", X.shape[1]) # 特征筛选 y = df.groupby("store_id")["future_sales"].last() X_filtered = select_features(X, y, fdr_level=0.05) print("筛选后特征数:", X_filtered.shape[1])这里有几个细节值得说清楚。
impute=True会处理计算过程中产生的缺失值,避免把 NaN 直接传给后续模型。特征计算为什么会产生缺失?因为某些特征需要至少 N 个观测,短序列不满足条件时,计算结果就会缺失,这是正常现象,靠 impute 来兜底。
chunksize控制每个工作进程一次处理的行数,设得太大反而容易卡内存。我会根据机器情况在 128 到 1024 之间调。n_jobs=-1代表吃满 CPU 核数,但在共享云环境里我会刻意限制核数,防止并行带来的内存峰值把容器打崩。
4.2 筛选结果怎么和 AutoML 对接
筛选完成后,真正喂给 AutoML 的只有X_filtered里剩下的那些列。很多人会把X全量塞给 AutoML,理由是“让模型自己挑”。AutoML 确实能处理高维特征,但高维噪声特征会让它的搜索时间成倍增加,解释性也差。先用 tsfresh 剪一遍,再让 AutoML 在干净的搜索空间里找模型和超参,效率会高很多。
对接代码用伪代码表示,不同 AutoML 框架的 API 略有差异:
# 伪代码:换成你常用的 AutoML 框架即可 from automl_api import AutoML selected_cols = X_filtered.columns.tolist() automl = AutoML(time_limit=600, metric="mae", random_state=42) automl.fit(X_filtered, y) X_test = extract_features(test_df, column_id="store_id", column_sort="date", ...) X_test = X_test[selected_cols] pred = automl.predict(X_test)注意预测阶段有两个容易出错的地方。第一,测试集的特征提取参数必须和训练时完全一致,包括default_fc_parameters、column_id、column_sort;第二,筛选出来的特征列清单要保存下来,测试集直接按这份清单取列,不做二次筛选。这个清单我一般直接存成文本文件,随模型一起归档。
4.3 一组演示数据上的效果对比
下面这张表是我在一组模拟数据集上跑出来的结果,不算绝对标准,但能说明趋势。数据包含 60 个门店、每天一条销量记录,预测未来第 1 天的销量:
| 方案 | 特征数量 | 线下 MAE | 线上 MAE | 训练耗时 |
|---|---|---|---|---|
| 手工基础特征(均值、滞后、差分) | 8 | 3.62 | 4.05 | 2 分 30 秒 |
| tsfresh 提取 + 筛选 | 76 | 2.94 | 3.10 | 8 分 20 秒 |
| tsfresh 提取 + 筛选 + AutoML 调优 | 76 | 2.87 | 2.98 | 15 分钟 |
手工特征那组我也套了 AutoML,但特征空间太小,模型天花板就摆在那里。tsfresh 那组多出来的特征,让模型能捕捉到更细的周期结构和变化模式,线上指标明显更好。15 分钟的整体流程放在一个日更的预测任务里,是可以接受的。
5. 跑完一轮之后,我的调优顺序和注意事项
5.1 先调数据,再调参数,最后调配置
很多人拿到 tsfresh 就开始调default_fc_parameters,我建议反着来。先确认原始数据质量:有没有重复 id、时间列有没有乱序、目标定义有没有泄漏。这些问题参数救不回来。
数据没问题之后,调参顺序我也固定了。先调fdr_level,也就是特征筛选的阈值,默认 0.05。如果筛选后特征太少,只有个位数,我会适当放宽到 0.1,观察指标变化;如果特征还是太多,我会收紧到 0.01。这个阈值直接影响后续 AutoML 的输入维度,值得花时间尝试。
接下来再看default_fc_parameters。我一般不会直接上 comprehensive,而是把 efficient 和 minimal 各跑一遍,对比筛选后的特征数量。如果两者预测结果几乎一样,那就不需要更重的配置,省下的计算时间可以留给 AutoML 做更多模型搜索。
5.2 多时序和自定义特征场景的实操建议
如果你面对的是单条长序列,tsfresh 按 id 提取出来的特征矩阵只有一行,模型没有足够样本量去学习。这个场景要搭配滚动窗口使用:把长序列切成多个窗口样本,每个窗口提取特征,再用窗口之后真实值作为目标。窗口大小和目标步长是这个场景里最关键的两个超参,我会在 AutoML 之外单独做一次小范围网格搜索,因为它会重塑整个特征空间。
当你有多个不同含义的字段时,用kind_to_fc_parameters给不同字段指定不同特征集合。比如销量字段用丰富的频域特征,促销标记字段用基础统计特征就够了。这个语法不复杂,但能有效控制提取耗时:
from tsfresh.feature_extraction import MinimalFCParameters, EfficientFCParameters custom_params = { "sales": EfficientFCParameters(), "promo": MinimalFCParameters(), } X_custom = extract_features( df, column_id="store_id", column_sort="date", kind_to_fc_parameters=custom_params, n_jobs=-1, )如果一个字段被指定了过多特征计算器,而它的数据本身又没有足够的变化,往往会拖慢整个进程。所以我一般先看字段取值分布,再决定它的特征级别。
5.3 关于稳定性的几句体己话
管道化之后,最怕的不是效果没有提升,而是效果时好时坏。我做这几轮实验最深的体会是:把“特征提取配置”和“特征列清单”固定下来,比什么都重要。AutoML 本身有随机性,同一个配置未必每次结果都一模一样;但特征生成层面必须稳定。所以我在项目里会额外做一件事:提取完成后,把特征列清单存成文件,每次复跑都校验当前列清单和之前是否一致。列变了,模型对比就没有意义了。
最后再分享一个小技巧。当你要判断 tsfresh 特征到底有没有用的时候,先跑一个只用原始滞后值的手工特征基线,再跑加了 tsfresh 特征的模型。两次对比里,特征生成的增量价值一目了然,也能帮你向团队解释为什么要多花这几分钟的特征提取时间。任何时候,能落到指标差异上的经验,都比“听起来有道理”的经验值钱得多。