ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

日期时间变量特征工程实战:从解析到滞后特征的完整指南

日期时间变量特征工程实战:从解析到滞后特征的完整指南 如果问你表格里有一列是日期你会怎么处理很多人第一反应是直接删掉或者粗暴地转成时间戳丢给模型。但日期时间变量在机器学习特征工程里其实是信息密度非常高的一类数据来源。它不只是“年月日”三个数字背后还藏着趋势、季节性、周期性、节假日影响甚至业务节奏。在 CampusX 的 100 天机器学习系列中第 34 天专门聚焦日期和时间变量的处理这个选题本身就说明它不是一个可以简单带过的边角料问题。这篇文章会用一套完整思路帮你彻底搞懂日期时间变量它为什么不能直接喂给模型、有哪些常见处理手段、每种手段解决什么问题、适合什么场景以及在真实项目里最容易踩的坑。文章会附带大量可复制的 Python 代码全部基于 Pandas 和 NumPy你可以直接拿来改造自己的数据集。读完之后你不但知道怎么处理日期列还能在建模时把“日期特征工程”这件事讲得有理有据。1. 日期时间变量为什么不能直接丢给模型先看一个很常见的场景。你拿到一份电商平台的每日销售数据字段包括日期、销售额、是否促销、是否节假日。现在要预测明天的销售额。很多初学者的做法是日期这列用不了删掉或者干脆转成 Excel 里的序列号也就是从 1900 年 1 月 1 日以来的天数然后直接作为特征。这两种做法的问题都很明显。第一如果直接把日期字符串删掉等于把数据里最核心的时间信息全部丢弃。销量是否有周周期性月末、月初是否有差异双十一、节假日是否有暴增模型完全看不到这些规律特征工程这一步就输在了起跑线上。第二如果转成时间戳或者天数数字形式上是数值了但数值背后的假设是“日期和目标是线性单调关系”。实际业务中日期和目标往往是周期性关系。比如 1 月销量低2 月稍高7 月达到峰值12 月又有回落。如果用一个大数字表示日期线性模型很难学到这种周期模式树模型也只能盲目地做区间切分效果同样不好。第三字符串本身参与不了任何数学运算。逻辑回归、SVM、神经网络、距离计算全部要求输入是数值。就连树模型sklearn 里也是不接受字符串特征的。所以日期时间变量处理的核心目标可以概括成两句话第一条把日期字符串转成真正的时间对象让工具能够理解它。第二条从时间对象中衍生出多个数值特征把隐藏在日期里的周期性、趋势性、区间性信息显式地暴露给模型。这里还有一个重要判断日期时间变量在建模任务中往往有两种角色。一种角色是“特征”比如预测销量时月份、星期几、是否节假日都是有用特征。另一种角色是“索引”比如在时间序列交叉验证、按周聚合、补全缺失日期时我们需要把日期列设置成 DataFrame 的索引。很多初学者分不清这两种角色导致代码里一会儿把日期当普通列一会儿又想按日期排序重采样结果处理得很乱。后面我会分别演示这两种用法。2. 处理日期时间变量的整体思路在动手写代码之前先建立一张“地图”。日期时间变量的处理手段大体上可以分成四类。第一类是基础解析与类型转换。把字符串、Excel 序列号、时间戳统一转成 Pandas 的 datetime64 类型。这一步是一切操作的前提转换失败后面全部免谈。第二类是时间特征分解。从一个日期中拆出年、月、日、星期几、第几周、第几季度、年内第几天、是否月初、是否月末、是否周末等。这类特征适合做回归、分类、树模型、线性模型几乎通用。第三类是周期特征编码。最典型的是用 sin 和 cos 把“月、星期、小时”这类循环变量映射到圆上。为什么要这样做因为 12 月到 1 月、周日到周一、23 点到 0 点在真实世界里是相邻的但数字 12 和 1、7 和 1、23 和 0 之间的距离却很大。sin/cos 编码可以让模型感知到这种环形相邻关系。第四类是时间差与滞后特征。时间是连续的样本之间往往不是独立的。今天的销量和昨天的销量高度相关7 天前的销量可能和今天有周期性关联。滞后特征就是把过去某个时间点的目标值挪到当前行作为特征使用。时间差特征则是计算某个样本距离锚点日期、上一笔交易、最近一次促销等事件的天数或小时数。这四类手段不是每次都要全部用上而是要根据任务选择。比如普通分类/回归任务数据是横截面重点做特征分解。销量预测、天气预测这类强时序任务重点做滞后特征、滚动统计特征和时间差特征。周期性明显的场景比如用电量、流量、店铺客流重点做周期编码。下面分别讲解每一类手段的代码实现。3. 环境准备与基础解析本文的代码基于 Python 3 Pandas NumPy。建议创建独立环境在终端中执行下面的命令python -m venv ml_dt_env source ml_dt_env/bin/activate # Windows 下是 ml_dt_env\Scripts\activate pip install pandas numpy scikit-learn版本方面Pandas 建议在 1.5 以上本文使用的写法在 Pandas 2.x 也兼容。如果你使用的是 Jupyter Notebook直接在单元格里运行即可。为了演示方便我们构造一份 2023 年每日销售数据的示例数据集。数据包括日期、销售额、是否促销、是否节假日四个字段。import pandas as pd import numpy as np # 构造2023年每日数据的示例 date_rng pd.date_range(start2023-01-01, end2023-12-31, freqD) np.random.seed(42) n len(date_rng) sales_base 200 50 * np.sin(np.linspace(0, 12 * np.pi, n)) sales_trend np.linspace(0, 30, n) promotion np.random.choice([0, 1], sizen, p[0.9, 0.1]) holiday np.zeros(n, dtypeint) # 简单模拟几个节假日 holiday[pd.Series(date_rng).dt.strftime(%m-%d).isin([01-01, 02-10, 05-01, 10-01, 12-25]).values] 1 sales sales_base sales_trend promotion * 80 holiday * 60 np.random.normal(0, 10, n) df pd.DataFrame({ date: date_rng.strftime(%Y-%m-%d), # 故意存成字符串模拟真实数据 sales: sales, promotion: promotion, holiday: holiday }) print(df.head())输出结果如下date sales promotion holiday 0 2023-01-01 259.379459 0 1 1 2023-01-02 201.731333 0 0 2 2023-01-03 209.448305 0 0 3 2023-01-04 208.511101 0 0 4 2023-01-05 222.458173 0 0可以看到date 列目前是字符串格式。在使用 Pandas 的dt访问器之前必须先把它转成 datetime64 类型df[date] pd.to_datetime(df[date]) print(df.dtypes)输出结果date datetime64[ns] sales float64 promotion int32 holiday int32 dtype: object这里pd.to_datetime是 Pandas 中最常用的日期解析函数。如果数据量很大建议指定format参数可以显著提升解析速度也能避免个别行格式不统一导致的解析错误df[date] pd.to_datetime(df[date], format%Y-%m-%d)如果数据中混杂了无法解析的日期可以设置errorscoerce转换失败的行会变成NaT之后再统一处理缺失值df[date] pd.to_datetime(df[date], format%Y-%m-%d, errorscoerce)还有一类常见情况是从 CSV 中直接读取日期列此时不需要手动转换读取时加parse_dates参数即可df pd.read_csv(sales.csv, parse_dates[date])这一步虽然看起来简单却是后面所有特征工程的基础。日期解析不稳后续的分钟级、小时级、时区操作都会出错。4. 日期时间特征分解从日期字符串到结构化特征把 date 列转成 datetime64 之后就可以用dt访问器提取各种时间成分。这是日期时间变量处理中最直观、最常用的一步。df[year] df[date].dt.year df[month] df[date].dt.month df[day] df[date].dt.day df[dayofweek] df[date].dt.dayofweek # 0周一6周日 df[weekday_name] df[date].dt.day_name() df[quarter] df[date].dt.quarter df[dayofyear] df[date].dt.dayofyear df[is_month_start] df[date].dt.is_month_start.astype(int) df[is_month_end] df[date].dt.is_month_end.astype(int) df[is_weekend] df[date].dt.dayofweek.isin([5, 6]).astype(int) df[week_of_year] df[date].dt.isocalendar().week.astype(int) print(df.head())输出结果date sales promotion holiday year month day dayofweek weekday_name quarter dayofyear is_month_start is_month_end is_weekend week_of_year 0 2023-01-01 259.379459 0 1 2023 1 1 6 Sunday 1 1 1 0 1 52 1 2023-01-02 201.731333 0 0 2023 1 2 0 Monday 1 2 0 0 0 1 2 2023-01-03 209.448305 0 0 2023 1 3 1 Tuesday 1 3 0 0 0 1 3 2023-01-04 208.511101 0 0 2023 1 4 2 Wednesday 1 4 0 0 0 1 4 2023-01-05 222.458173 0 0 2023 1 5 3 Thursday 1 5 0 0 0 1这里每个特征都有自己的含义和价值理解dayofweek的约定非常关键它是从 0 到 6 的整数0 代表周一6 代表周日。如果你习惯把周一当 1可以执行df[dayofweek] 1或者用dt.weekday 1。week_of_year使用了isocalendar().week这是 ISO 标准的周编号比旧 APIdt.weekofyear更可靠后者在 Pandas 2.x 中已经弃用。在特征工程实践中有几个特征需要特别关注dayofweek和is_weekend适合捕捉周末效应比如零售、游戏、出行类业务。month和quarter适合捕捉季节性波动比如空调销量夏季高、衣服销量换季波动。is_month_start和is_month_end适合捕捉发薪日效应。很多消费类业务在月初和月末有明显上升。dayofyear适合捕捉长期趋势中的平滑变化它能表达“一年中的第几天”这样一个单调递增的位置信息。还要提醒一点不一定把上面所有特征全部加进模型。特征不是越多越好加入大量弱相关特征不但会拖慢训练速度还可能带来噪声。建议先用几个核心特征做一轮基线模型然后通过特征重要性或逐项替换来验证哪个日期特征真正有效。5. 循环时间特征利用 sin/cos 编码解决周期性问题特征分解虽然能提取出月、星期、小时但这里有一个隐患离散数字表达会误导模型。以月份为例12 月和 1 月在现实中紧紧相邻但数字 12 和 1 的欧式距离是 11模型会认为这两个月份差异巨大。同样的问题也出现在小时上23 点应该和 0 点很接近但数值距离是 23。解决思路是把时间变量映射到一个圆上。假设一个周期长度为 period那么对这个周期内的某个值 x可以构造两个新特征sin_x sin(2 * pi * x / period) cos_x cos(2 * pi * x / period)为什么需要两个维度而不是一个因为只用 sin 值时sin(30°) 和 sin(150°) 都等于 0.5无法区分 2 点和 10 点加入 cos 值后(sin, cos) 就成了圆上的一个点每个时间点都有唯一的二维坐标模型可以同时感知相近性和方向性。下面是小时、星期、月份的循环编码示例# 假设 df 中已有 hour、dayofweek、month 三个列 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[dayofweek] / 7) df[weekday_cos] np.cos(2 * np.pi * df[dayofweek] / 7) df[month_sin] np.sin(2 * np.pi * df[month] / 12) df[month_cos] np.cos(2 * np.pi * df[month] / 12) print(df[[hour_sin, hour_cos, weekday_sin, weekday_cos, month_sin, month_cos]].head())注意我们构造的示例数据本身只有日期没有小时所以如果你是从零执行上面的代码需要先用下面这行构造一个 hour 列df[hour] df[date].dt.hour不过在本文的日粒度示例中hour 恒为 0实际业务中小时特征通常用于小时级预测、运营分析等场景。循环编码对线性模型、神经网络、KNN 这类依赖距离计算的模型很有帮助对树模型的作用则相对有限。树模型本质上是做条件切分它完全可以自行学习“月份落入哪几个区间”的规律并不要求特征在数值上平滑连续。但加上循环编码通常不会损失信息而且在做特征可视化、距离计算时更符合直觉。再强调一个容易混淆的点循环编码适合的是“本质上是循环角度”的特征包括小时、星期、月份、季度但不适合表示“从某一天算起已经过了多少天”这种单调递增的时间差变量。你不需要对days_from_start做 sin/cos那样反而会把连续的时间变成周期性抖动丢失趋势信息。6. 时间差特征、滞后特征与时间聚合现实业务中样本与样本之间往往不是孤立的。尤其是销量、流量、温度这类时序数据昨天的值对今天的影响非常大。为了把这种前后依赖关系显式地交到模型手里我们需要构造时间差特征和滞后特征。6.1 时间差特征时间差特征的本质是计算当前样本与某个关键时间点的距离。常见的场景包括距离数据集中最早日期的天数用来表达时间趋势。距离用户上次登录、上次购买的小时数。距离某个固定事件例如上次促销、上次活动的天数。df[days_from_start] (df[date] - df[date].min()).dt.days # 模拟距离最近一次大促的天数 promo_dates pd.to_datetime([2023-03-01, 2023-06-18, 2023-11-11]) def days_since_last_promo(row_date): past_promos promo_dates[promo_dates row_date] if len(past_promos) 0: return 999 # 没有发生过促销给一个较大值 return (row_date - past_promos.max()).days df[days_since_promo] df[date].apply(days_since_last_promo) print(df[[date, days_from_start, days_since_promo]].head(10))这里有一个业务判断当某个事件还没发生时时间差特征应该填什么值用较大的数如 999是一种方式标记为 -1 是另一种方式。但要注意缺失标记本身也是一种信息。如果填 999线性模型可能被这个极端值带偏树模型则可能基于“是否大于某个阈值”做分裂。更稳妥的做法是单独加一个布尔特征has_promo_occurred告诉模型事件有没有发生。6.2 滞后特征与滚动统计特征滞后特征就是把目标变量过去的值搬到当前行。这里最需要注意的是顺序构造时必须保证日期已经按升序排好否则shift结果没有意义。df df.sort_values(date).reset_index(dropTrue) df[sales_lag_1] df[sales].shift(1) df[sales_lag_7] df[sales].shift(7) # 滚动窗口统计量 df[sales_rolling_mean_7] df[sales].rolling(window7).mean() df[sales_rolling_std_7] df[sales].rolling(window7).std() print(df[[date, sales, sales_lag_1, sales_lag_7, sales_rolling_mean_7]].head(12))输出会显示前几行的滞后特征为 NaN。这是因为数据集中没有更早的历史数据。在训练模型之前需要把包含 NaN 的行删掉或者用其他方式填充。直接删除是最稳妥的因为sales_lag_1对应的是真实存在的昨天数据不能用不存在的值来造假。滞后阶数怎么选最常用的是滞后 1 期反映惯性和滞后 7 期反映周周期。如果你处理的是月度数据可以尝试滞后 12 期。更进阶的方法是绘制自相关图ACF和偏自相关图PACF观察哪些滞后阶数与目标的相关性显著。滚动统计特征则能缓解单日波动带来的噪声。7 天滚动均值可以看作“平滑后的近期水平”7 天滚动标准差可以表达“近期波动程度”。这类特征对销量预测、异常检测都很有帮助。滚动窗口的长度可以根据业务周期选择日数据常用 7、14、30小时数据常用 24、72、168。6.3 时间聚合resample 和 groupby如果原始数据是小时级或天级我们经常需要把数据聚合到更高粒度比如求和成日级、周级、月级。这时需要把日期列设为索引然后使用resample。# 先模拟一份带有小时级数据的数据集 hourly_rng pd.date_range(start2023-01-01, end2023-01-07 23:00:00, freqH) hourly_df pd.DataFrame({ date: hourly_rng, traffic: np.random.randint(50, 300, sizelen(hourly_rng)) }) hourly_df hourly_df.set_index(date) # 按天聚合 daily_traffic hourly_df.resample(D).sum() print(daily_traffic.head())resample的频率参数常见的有频率含义H小时D天W周M月末Q季度末Y年末聚合函数可以是sum、mean、max、min、count等。注意W默认是按周日作为一周的结束如果需要按周一开始可以使用W-MON。如果你的数据还包含分组维度比如多个店铺、多个城市那么除了resample还经常用到groupby加resample的组合# store_id 表示店铺ID按店铺和日期分组聚合 hourly_df[store_id] 1 store_daily hourly_df.groupby(store_id).resample(D).sum()时间聚合的价值在于一方面可以把数据变成适合模型输入的格式另一方面也可以通过不同粒度观察业务节奏为特征工程提供灵感。我在实际项目里通常不会只分析原始粒度而是会同时做天级、周级、月级聚合对比不同时间尺度上的规律再决定在哪个粒度建模最合适。7. 完整代码实战电商日销量特征的构造与模型训练下面把所有日期处理手段整合到一个完整流程中。假设我们要用 2023 年的每日销售数据预测下一天的销量。这里只做特征构造示范不对超参数做精细调优。import pandas as pd import numpy as np from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split from sklearn.metrics import mean_absolute_error # ---------- 1. 构造示例数据 ---------- date_rng pd.date_range(start2023-01-01, end2023-12-31, freqD) n len(date_rng) sales_base 200 50 * np.sin(np.linspace(0, 12 * np.pi, n)) sales_trend np.linspace(0, 30, n) promotion np.random.choice([0, 1], sizen, p[0.9, 0.1]) holiday np.zeros(n, dtypeint) holiday[pd.Series(date_rng).dt.strftime(%m-%d).isin([01-01, 02-10, 05-01, 10-01, 12-25]).values] 1 sales sales_base sales_trend promotion * 80 holiday * 60 np.random.normal(0, 10, n) df pd.DataFrame({ date: date_rng, sales: sales, promotion: promotion, holiday: holiday }) # ---------- 2. 基础日期解析 ---------- df[date] pd.to_datetime(df[date]) # ---------- 3. 日期特征分解 ---------- df[month] df[date].dt.month df[day] df[date].dt.day df[dayofweek] df[date].dt.dayofweek df[quarter] df[date].dt.quarter df[dayofyear] df[date].dt.dayofyear df[is_weekend] df[date].dt.dayofweek.isin([5, 6]).astype(int) df[is_month_start] df[date].dt.is_month_start.astype(int) df[is_month_end] df[date].dt.is_month_end.astype(int) # ---------- 4. 循环特征编码 ---------- df[month_sin] np.sin(2 * np.pi * df[month] / 12) df[month_cos] np.cos(2 * np.pi * df[month] / 12) df[weekday_sin] np.sin(2 * np.pi * df[dayofweek] / 7) df[weekday_cos] np.cos(2 * np.pi * df[dayofweek] / 7) # ---------- 5. 时间差特征 ---------- df[days_from_start] (df[date] - df[date].min()).dt.days # ---------- 6. 滞后与滚动特征 ---------- df df.sort_values(date).reset_index(dropTrue) df[sales_lag_1] df[sales].shift(1) df[sales_lag_7] df[sales].shift(7) df[sales_rolling_mean_7] df[sales].rolling(window7).mean() df[sales_rolling_std_7] df[sales].rolling(window7).std() # 删除因滞后特征产生的缺失行 df df.dropna().reset_index(dropTrue) # ---------- 7. 训练模型 ---------- features [ month, day, dayofweek, quarter, dayofyear, is_weekend, is_month_start, is_month_end, month_sin, month_cos, weekday_sin, weekday_cos, days_from_start, promotion, holiday, sales_lag_1, sales_lag_7, sales_rolling_mean_7, sales_rolling_std_7 ] X df[features] y df[sales] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, shuffleFalse ) model RandomForestRegressor(n_estimators200, random_state42) model.fit(X_train, y_train) y_pred model.predict(X_test) print(MAE:, mean_absolute_error(y_test, y_pred)) # 输出特征重要性 importance_df pd.DataFrame({ feature: features, importance: model.feature_importances_ }).sort_values(importance, ascendingFalse) print(importance_df.head(10))运行这个脚本会得到类似下面的输出MAE: 9.277848116973244 feature importance 0 sales_lag_1 0.537389 1 sales_rolling_mean_7 0.094625 2 sales_lag_7 0.082004 3 days_from_start 0.074147 4 sales_rolling_std_7 0.052182 ...从特征重要性中可以看到sales_lag_1对销量预测的贡献最大其次是滚动均值和滞后 7 天特征。这说明在日销量预测这种强时序任务中近期历史值本身就携带大量预测信息日期分解类特征的相对重要性反而偏低。这里的代码中有几个细节值得注意第一是数据集划分时用了shuffleFalse。时间序列数据不能随机打乱否则相当于把未来信息泄露给训练集。预测未来数据时必须以时间顺序切分训练集和验证集这也是日期时间变量在建模中的另一个核心作用。第二是删除 NaN 行。因为滞后特征导致数据前面 7 行无值直接删除是最干净的做法。如果你用填充的方法要确保填充方式不会把未来信息带进去。比如用全局均值填充在时间序列里就不太合理。第三是特征重要性排名会随数据随机性略有波动但sales_lag_1和滚动均值这类特征大概率占据前列。这提醒我们构造特征时不要只盯着日期本身还要思考目标变量自己的历史规律。8. 常见问题与排查方法日期时间变量处理过程看起来 api 很多实际使用时经常会遇到各种报错和异常结果。下面整理一张问题排查表。问题现象可能原因排查方式解决方案pd.to_datetime报错 Unknown string format字符串格式不统一混入了2023/01/01与01-01-2023等打印唯一值检查格式先统一数据格式再指定format参数或加errorscoerce转换后出现大量 NaT存在空字符串、非法日期、时区偏移量异常执行df[df[date].isna()]查看异常行根据业务规则填充或删除必要时先做字符串清洗dt.weekofyear出现 DeprecationWarningPandas 2.x 已弃用该属性查看警告信息改用df[date].dt.isocalendar().week滞后特征排完序后仍错乱数据存在重复日期或者没有先按日期排序检查df[date].is_monotonic_increasing先sort_values(date)再reset_index(dropTrue)时间序列预测中验证效果很好上线后效果崩了训练/验证划分时随机打乱造成数据泄露检查划分参数输出训练集和测试集日期区间使用shuffleFalse或按时间点切分resample报错 DatetimeIndex 不存在日期列只是普通列没有设为索引查看df.index类型执行df df.set_index(date)并确保索引是 datetime64小时特征全部为 0原始数据是日粒度日期的.dt.hour本来就是 0检查数据原始粒度确认建模粒度小时级数据才使用 hour 特征滞后特征产生 NaN训练时 X 和 y 长度不一致shift 后的 NaN 行没有被删除查看df.isna().sum()在执行特征构造后dropna()看得多了会发现大多数问题都出在“日期类型没有真正转换成功”和“排序、切分没有保持时间顺序”这两类。建议拿到任何带日期的数据后第一件事就是验证两句话print(df[date].dtype) # 必须是 datetime64 print(df[date].is_monotonic_increasing) # 是否按时间升序这两行简单代码能帮你省掉大量调试时间。9. 最佳实践与工程建议日期时间变量看起来零碎但实际工程中有一套稳定的处理套路。下面这些实践建议来自我自己的项目经验希望对你有参考价值。9.1 把特征工程写成可复用函数不要在每个 Notebook 里复制粘贴一大段特征处理代码。建议把日期特征处理封装成函数输入 DataFrame 和日期列名输出扩展后的 DataFrame。这样在训练、验证、上线预测时只要调用同一个函数就能保证特征口径一致。def add_datetime_features(df, date_coldate): dt pd.to_datetime(df[date_col]) df df.copy() df[year] dt.dt.year df[month] dt.dt.month df[day] dt.dt.day df[dayofweek] dt.dt.dayofweek df[quarter] dt.dt.quarter df[dayofyear] dt.dt.dayofyear df[is_weekend] dt.dt.dayofweek.isin([5, 6]).astype(int) df[month_sin] np.sin(2 * np.pi * df[month] / 12) df[month_cos] np.cos(2 * np.pi * df[month] / 12) df[weekday_sin] np.sin(2 * np.pi * df[dayofweek] / 7) df[weekday_cos] np.cos(2 * np.pi * df[dayofweek] / 7) return df这个习惯看起来普通但在真正上线模型时价值很大。训练和推理的特征口径一旦不一致线上效果会莫名其妙下跌排查起来特别麻烦。9.2 区分“索引型日期”和“特征型日期”日期可以作为特征列参与训练也可以作为 DataFrame 的索引用于排序、切分、聚合。很多新手把两者混在一起既想保留日期列又想按日期切分最后代码很乱。建议在建模阶段保留日期列作为特征来源同时在进入模型之前把日期列单独拆出来作为验证集的切分依据避免模型把原始日期字符串当成可以计算的量。如果使用train_test_split建议用下面的方式按时间点切分而不是用默认的随机切分split_date pd.Timestamp(2023-10-01) train df[df[date] split_date] test df[df[date] split_date]这种切分方式更贴近真实业务。尤其在序列预测场景中我们永远只能拿过去预测未来。9.3 注意树模型和线性模型的差异如果你使用的是 XGBoost、LightGBM、随机森林日期分解后的 month、day、dayofweek 等原始整数特征通常够用树模型可以自己找到切分点不必为了“距离”而强行构造大量循环特征。但如果你使用的是线性回归、逻辑回归、SVM、神经网络那么标准化、循环编码、独热编码都需要更认真对待因为这些模型对特征的尺度和距离非常敏感。判断方法很简单先跑一个基线树模型看日期分解特征的 feature importance再换线性模型比较效果。多数项目中树模型在时间序列类问题上表现优于线性模型但也有例外不要凭印象选模型。9.4 防止目标泄露构造滞后特征时最危险的一步是泄露。如果原始数据包含多个实体比如多个店铺那么滞后特征必须按店铺分组计算不能用全局的shift。否则会把店铺 A 的数据挪到店铺 B 的当前行结果看起来效果很好实际上信息已经错了。# 多个店铺时的正确做法 df df.sort_values([store_id, date]) df[sales_lag_1] df.groupby(store_id)[sales].shift(1)另一个容易泄露的地方是滚动均值。滚动窗口只能包含过去的数据不能包含未来数据。Pandas 的rolling(window7)默认中心点是窗口右侧因此默认只使用了窗口内左侧的历史数据这一点是符合时序要求的。但如果你用了centerTrue或shift(-k)就可能在特征构造过程中把未来信息带入一定要警惕。9.5 记录特征语义日期特征一多后面很容易遗忘每个特征的含义。比如dayofweek的取值范围、is_month_start的语义、days_since_promo中的 999 代表什么。建议在特征工程代码中写清楚文档字符串或在数据字典里维护一张特征表。项目越往后推进特征对不上的代价越高。9.6 日期缺失与非法值处理真实数据几乎没有完全干净的日期列。可能的脏数据包括空值、时间戳与字符串混存、不同时区的偏移量不一致、未来日期、异常年份如 1900-01-00。统一的处理策略是先pd.to_datetime(..., errorscoerce)把错误值统一变成 NaT。然后对 NaT 做统计若占比很低直接删除若占比高需要回查数据来源。对明显的未来日期如果发生在“训练时点之后”也要特别小心那很可能就是脏数据。日期处理看似简单但它直接影响模型的数据基础。脏数据不清理后面所有特征工程都是空中楼阁。10. 总结日期和时间变量是机器学习特征工程里一个非常特殊的存在。它既不像数值变量可以直接参与运算也不像类别变量可以简单做编码但它包含了趋势、周期、节假日、业务节奏等高价值信息。本文里讲到的核心内容可以总结成一句话先把日期字符串解析为 datetime 类型再把日期分解成结构化特征对周期型特征做 sin/cos 循环编码在时序任务里构造时间差、滞后和滚动统计特征最后在切分数据集时严格保持时间顺序。这篇文章以 CampusX 100 天机器学习系列第 34 天的主题为切入口把这些内容串成了一个完整的实践路径。你可以先把文章里的代码在 Jupyter Notebook 中跑一遍然后把add_datetime_features函数用到自己的数据集上。建议重点关注滞后特征和时间序列切分这两块它们既是最容易提升模型效果的地方也是最容易发生数据泄露的雷区。日期时间变量的处理没有统一的黄金模板但有了这套思路你至少不会在拿到日期列的那一刻第一反应是删掉它了。
返回列表