ARTICLE DETAIL

资讯详情

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

机器学习特征工程:日期时间变量处理与时间特征提取实战

机器学习特征工程:日期时间变量处理与时间特征提取实战 作为一个做机器学习项目的开发者你可能有过这样的经历拿到一份数据发现里面有“下单时间”“注册日期”“最后一次登录时间”这几列心里第一个反应是“这有什么用直接删掉算了”或者干脆把它们当成普通字符串丢给模型去训练。等模型跑完效果不理想又不知道问题出在哪里。处理日期和时间变量是特征工程里最容易被低估、也最容易做错的环节之一。它不像归一化、独热编码那样有明确套路也不像缺失值填充那样一个问题对应一个方案。很多教程只是轻描淡写一句“把日期拆成年月日”但如果真按这个思路去做你会发现拆完之后模型没变好反而引入了大量无用特征或者你精心构造的时间特征在训练集和测试集上分布完全不一致模型上线后效果崩盘。这篇教程不打算重复“把日期拆成年月日”这种泛泛而谈的内容。我会从日期时间变量的本质出发讲清楚它为什么对机器学习重要在什么场景下必须处理以及真正在工程里可落地的一套处理流程。内容对应 100 天机器学习 CampusX 课程第 34 天的主题面向正在学习特征工程的初学者也适合建模时经常被时间变量坑到的进阶读者。读完这篇文章你会知道如何用 Python 正确解析日期时间数据如何从时间戳中提取真正有用的特征为什么“时间差”往往比“绝对时间”更有价值以及循环时间特征如何处理才能被线性模型和树模型同时接受。1. 这篇文章真正要解决的问题先说一个很多人没有意识到的判断日期时间变量不是普通特征它是一类同时包含“连续性”“周期性”“事件性”三种信息形态的高密度变量。这句话什么意思我们拆开看。普通数值特征比如“用户年龄”“商品价格”它们的取值是连续的数值大小本身有意义模型可以直接拿去做切分点和距离计算。普通类别特征比如“商品分类”“用户城市”它们的取值是离散的需要通过独热编码或标签编码转成模型能懂的形式。日期时间变量比这两类都复杂。它首先是一个数值比如2024-01-15 14:30:00转成时间戳之后是一个很大的整数但它又不是普通数值因为“2024年1月15日”和“2024年1月16日”之间只差一天而在时间戳数值上这两者相差 86400这个差距的意义取决于你的业务场景。它有周期性一年 12 个月循环、一周 7 天循环、一天 24 小时循环单纯的数值编码无法表达“周一和周日是同类、周二和周六也是同类”这种关系。它还有事件性比如“下单时间距离上一个促销活动过去了多少小时”这是一种由领域知识驱动的特征无法靠自动特征工程生成。正因为它有这么多种信息形态处理起来才特别容易出问题。如果什么都不做直接把原始时间戳丢给模型大部分模型会把它当成一个大数值结果就是模型把 2015 年之前的数据全部判为一类2015 年之后的数据判为另一类完全学不到时间背后的规律。如果只是简单拆成年月日又会遇到一个新问题日期特征在模型训练集和测试集之间的分布可能差异巨大——训练集全部在上半年测试集在下半年拆出来的“月”特征在训练集上只有 1 到 6测试集上却有 7 到 12模型根本没法处理这种分布外取值。所以这篇文章要帮你解决的核心问题是在 Python 中建立一套从原始日期时间到可用机器学习特征的完整处理流程并理解每一步背后的原因而不是给你二十个零散的代码片段。什么样的读者最应该认真读这篇文章正在做 Kaggle 比赛或课程项目数据里有时间字段但不知道怎么用的人。做推荐系统、风控模型、销量预测、用户行为分析需要从日志时间戳里构造特征的人。学完基础特征工程发现时间变量这块总是“道理都懂一上手就乱”的人。如果你只是随便跑几个 sklearn 内置数据集练手感觉时间变量暂时用不上那这篇文章也可以先收藏等你第一次在真实项目里遇到“时间字段到底要不要进模型”这个问题时再翻出来看。2. 日期时间变量的基础概念与内在属性在进入代码之前我们必须先把基本概念理清楚。机器学习的特征工程不是写业务代码你对数据形态的理解直接决定后面怎么做特征。2.1 日期Date、时间Time和时间戳Timestamp这三个词在中文里经常混用但在数据处理层面有严格区别。日期Date指的是“年-月-日”比如2024-03-15颗粒度是天。时间Time指的是“时:分:秒”比如14:30:00它通常和日期绑定在一起才有业务含义。时间戳Timestamp是完整的时间点比如2024-03-15 14:30:00在很多数据库和日志系统里它可能以字符串形式存在也可能以 Unix 时间戳从 1970 年 1 月 1 日 0 时 0 分 0 秒起计算的总秒数形式存在。在 Python 里这三者有对应的数据类型datetime.date只存储日期。datetime.time只存储时间。datetime.datetime日期加时间。pandas.Timestamppandas 中的时间标量类型功能比datetime.datetime更丰富是表格数据处理的主力。一个很容易犯的错误是用字符串类型直接存储时间。比如 CSV 文件里读进来某些列的类型是object里面全是2024-03-15 14:30:00这样的文本。在字符串状态下你没法做任何时间运算也没法比较大小只能把它当成类别特征这对模型毫无帮助。2.2 为什么时间变量不能直接当数值用很多人会有这样的疑问时间戳本质是一个数字为什么不能直接把它当作数值特征喂给机器学习模型这个问题的答案取决于模型类型。对于线性回归、逻辑回归这类模型特征是连续数值才能参与加权求和时间戳确实可以被当成数值但它的问题在于时间戳的真实物理含义是“从某个基准点算起的秒数”这个数值的尺度和变化幅度与业务目标的数值尺度很可能不在同一个量级。举个例子一个用户行为数据集的记录时间为 2021 年到 2024 年对应的时间戳数值是 16 亿到 17 亿这个量级。如果你直接把这个数字和用户年龄、点击次数等特征一起喂给线性模型模型会把几乎所有权重都花在时间戳这个天文数字上其他特征全部被压制。就算你做了标准化时间戳变成 0 到 1 之间的数值模型学习到的也只是“记录越晚的用户行为模式越不一样”这个线性假设可时间对业务的影响往往是周期性的、非线性的这种假设过于粗糙。对于决策树、随机森林、XGBoost、LightGBM 这类树模型情况稍微好一些因为树模型对特征尺度不敏感只需要找到一个切分点。但问题依然存在树模型只能做“大于某个时间戳”和“小于某个时间戳”的切分它学到的边界是某个具体时间点。这个时间点的选择完全由训练数据的分布决定如果训练数据和线上数据的时间范围不一致这个切分点就没有意义。所以把原始时间戳直接喂给模型本质上是在强迫模型用最原始的方式去理解时间。更好的做法是我们人先把时间背后的规律提取出来再用特征的形式交给模型。2.3 时间变量的三个基本属性我把它归纳成三个属性方便你后面做特征工程时对照着思考。第一个是连续性。时间是一个从过去指向未来的箭头所有事件在时间轴上都有先后顺序。这种属性意味着“最近 X 天下单次数”“距上次登录的小时数”这类差值特征是有意义的它们本身就是连续变量。第二个是周期性。地球绕着太阳转一圈是 365 天这是一个循环一天 24 小时也是一个循环。很多业务行为天然带有周期模式比如工作日和周末的流量差异、早晚高峰的下单差异、节假日和普通日的销量差异。周期属性不能用简单的数值编码表达因为“23 点和 1 点在数值上差得很远但在业务上非常接近”。第三个是事件性。某些时间点本身带有事件含义比如“2024-01-01”是元旦“2024-02-10”是春节“2024-11-11”是大促日。这种事件属性是领域知识的一部分需要业务经验来标注单纯靠数据解析无法获得。3. 环境准备与前置条件实操部分我们统一使用 Python 生态需要准备的环境和库如下Python 3.8 及以上版本版本请以你的实际环境为准本文代码在 Python 3.10 下验证可用。pandas 库版本 1.5 及以上用于表格数据读取和日期时间处理。numpy 库用于数值计算。matplotlib 库用于可视化时间特征分布。scikit-learn 库用于特征编码和模型训练验证。如果你还没有安装相关依赖可以创建一个干净的虚拟环境然后执行以下命令pip install pandas numpy matplotlib scikit-learn为了演示我们使用一份模拟的电商订单数据。这个数据集的字段包括字段名含义order_id订单编号user_id用户编号order_time下单时间字符串格式delivery_time送达时间字符串格式order_amount订单金额元原始数据以 CSV 格式存储内容大致如下order_id,user_id,order_time,delivery_time,order_amount 1001,U001,2024-01-03 09:24:15,2024-01-04 18:30:42,156.80 1002,U002,2024-01-03 10:12:07,2024-01-03 21:45:12,89.50 1003,U001,2024-01-05 20:36:51,2024-01-06 15:20:08,230.00 1004,U003,2024-01-07 08:15:33,2024-01-07 12:40:29,45.90 1005,U002,2024-01-10 23:58:10,2024-01-12 10:05:00,312.40请注意这里order_time和delivery_time在 CSV 中是以字符串形式存储的需要先用 pandas 解析成真正的日期时间类型才能做后续处理。4. 日期时间变量的读取与预处理这一节是整个处理流程的第一步也是最容易出错的一步。如果原始时间数据没有正确解析后面所有特征工程都是空中楼阁。4.1 用 pandas 读取并识别日期时间列首先读取 CSV 文件然后查看每一列的数据类型import pandas as pd df pd.read_csv(order_data.csv) print(df.dtypes)输出结果中order_time和delivery_time应该是object类型也就是字符串。这意味着我们无法直接进行时间运算比如计算配送时长、比较先后顺序、提取星期几等等。接下来用pd.to_datetime()对这两列进行转换df[order_time] pd.to_datetime(df[order_time]) df[delivery_time] pd.to_datetime(df[delivery_time]) print(df.dtypes)转换后可以看到order_time和delivery_time的数据类型变为datetime64[ns]这才是 pandas 真正识别的时间类型。这里有一个常见坑to_datetime()默认使用%Y-%m-%d %H:%M:%S这样的格式来解析字符串。如果 CSV 文件里的时间格式不是这种标准格式比如2024/03/15 14:30或15-03-2024 14:30:00直接调用to_datetime()可能报错或解析失败。推荐的做法是在调用时显式指定format参数df[order_time] pd.to_datetime( df[order_time], format%Y-%m-%d %H:%M:%S )指定format有两个好处一是解析速度比自动推断快得多二是遇到不符合格式的数据会立刻报错不会悄悄地把错误数据解析成缺失值。如果遇到多种格式混杂的情况可以先看一下到底有哪几种格式再逐个处理不要试图用一个format参数解决所有问题。4.2 时间索引的真实作用有些教程会建议你把日期列设为 DataFrame 的索引理由是“时间序列数据处理更方便”。但这个建议并不总是适用于机器学习建模。在机器学习任务中日期列通常是特征而不是索引。设置成索引之后如果你做特征工程时不小心用了reset_index()或按索引对齐很可能会引入不易察觉的 bug。更稳妥的工程实践是保留原始时间列作为普通列同时复制一份作为显式特征列来使用。这样既能追溯原始数据又能避免索引操作带来的风险。4.3 非法值、缺失时区和时间精度问题处理时间变量时最隐蔽的错误是非法值和时区问题。非法值指的是类似2024-02-30这样的日期它是不存在的日期。pd.to_datetime()默认遇到这种值会抛出异常。如果你希望解析时不要中断整个流程可以在to_datetime()中加上errorscoerce参数这样非法值会被转换成NaTNot a Timepandas 中时间缺失值的表示。df[order_time] pd.to_datetime( df[order_time], format%Y-%m-%d %H:%M:%S, errorscoerce )之后要检查是否存在NaTprint(df[order_time].isna().sum())如果确实存在缺失值需要根据业务场景决定是删除还是填充。比如下单时间缺失这属于核心字段建议删除该行如果是某些辅助字段可以考虑用其他时间列填充。另外是时区问题。如果你的数据来自多个地区或者涉及跨国业务时间列可能带有UTC08:00这样的后缀。pandas 在解析带时区的时间字符串时需要先统一时区否则会出现不同类型不能直接比较的报错。df[order_time] pd.to_datetime(df[order_time], utcTrue)统一成 UTC 后再根据业务需求转换为目标时区df[order_time] df[order_time].dt.tz_convert(Asia/Shanghai)需要特别提醒时区转换不是简单的字符串替换它涉及夏令时、历史时区变化等复杂规则建议优先使用成熟的库来处理不要自己写时区偏移逻辑。5. 从日期时间变量中提取特征处理好基础解析之后进入整个内容的核心部分特征提取。处理日期和时间变量的核心任务就是把这些时间点转化为对模型有区分能力的数值或类别特征。5.1 时间顺序特征时间戳特征在某些模型中可以保留时间戳的原始数值形式但需要经过恰当处理。最直接的做法是把时间戳转换成 Unix 时间戳从 1970 年 1 月 1 日开始算起的秒数df[order_timestamp] df[order_time].astype(int64) // 10**9但正如前文讨论的这个特征的尺度太大一般需要做缩放或标准化后再使用。从特征工程的角度看更推荐的方式是设定一个参考时间计算“距离参考时间经过了多少天”。参考时间可以是数据集中最早的时间也可以是某个有业务含义的时间点df[days_from_start] (df[order_time] - df[order_time].min()).dt.days这样得到的特征分布在 0 到几百的范围内对线性模型友好得多同时保留了“事件发生先后顺序”的信息。这种特征适合的场景是业务规律随时间线性变化比如用户忠诚度随时间稳步增长、平台活跃度随时间缓慢上升等。5.2 时间成分特征年、月、日、时等这是最常用的日期特征提取方式。从日期时间中提取出年月日、时分秒等时间成分让模型能够感知到时间模式。df[year] df[order_time].dt.year df[month] df[order_time].dt.month df[day] df[order_time].dt.day df[hour] df[order_time].dt.hour df[minute] df[order_time].dt.minute df[dayofweek] df[order_time].dt.dayofweek df[day_name] df[order_time].dt.day_name()其中dt.year返回年份比如 2024。dt.month返回月份取值范围 1 到 12。dt.day返回日期取值范围 1 到 31。dt.hour返回小时取值范围 0 到 23。dt.minute返回分钟取值范围 0 到 59。dt.dayofweek返回星期几0 表示周一6 表示周日。dt.day_name()返回星期的英文名称比如 “Monday”这个特征适合作为类别特征做标签编码或独热编码。关键问题来了这些成分特征应该当数值特征用还是当类别特征用我的建议是year包含趋势信息可以当数值特征使用但要注意年份跨度如果很大模型可能过拟合某一年数据。month、hour、dayofweek这类周期性较强的成分最好不要单纯当数值特征。原因很简单如果把hour当数值模型会学到“凌晨 0 点和 1 点差别不大23 点和 0 点差别最大”但真实业务是“0 点和 23 点都是深夜时段用户行为很接近”。数值编码扭曲了这种周期性。month可以当类别特征使用也可以配合后面的循环特征使用day单独使用意义不大除非你有明确理由相信月初、月中、月末有不同业务模式。5.3 时间差特征绝对时间没有意义相对时间才有这是日期时间特征工程中价值最高的一类特征。在很多实际业务中“某个时间点本身”并不重要重要的是“两个时间点之间的差值”。最典型的例子就是配送时长从下单到送达用了多少个小时。这个特征是用户非常关心的指标也是物流公司优化运营的核心目标。df[delivery_duration_hours] ( df[delivery_time] - df[order_time] ).dt.total_seconds() / 3600.0这里用total_seconds()得到秒数再除以 3600 换算成小时。值得特别注意的是dt.days和dt.total_seconds()是不同的。当两个时间差跨度较大时dt.days只返回天数的整数部分如果差值不是整天数dt.days会忽略小数部分这在你需要精确时长时不适用。再比如如果数据中有同一用户的多笔订单我们可以计算该用户当前订单距离上一笔订单的时间间隔df.sort_values(order_time, inplaceTrue) df[previous_order_time] df.groupby(user_id)[order_time].shift(1) df[gap_from_previous_order_hours] ( df[order_time] - df[previous_order_time] ).dt.total_seconds() / 3600.0 df.loc[df[previous_order_time].isna(), gap_from_previous_order_hours] -1这段代码的逻辑是先按时间排序然后按user_id分组把同一用户上一笔订单的下单时间移到当前行再计算两个时间的差。这里有一个容易被忽视的坑gap_from_previous_order_hours对于每个用户的第一个订单是没有值的。很多人在这一步直接填 0这是不对的。0 表示“距离上一笔订单 0 小时”有“上一笔订单和当前订单同时发生”的含义对模型是一种错误引导。更合理的做法是填一个特殊的负数比如 -1或者单独用一个布尔特征 “是否为该用户的首笔订单” 来标记df[is_first_order] df[previous_order_time].isna().astype(int) df.fillna({gap_from_previous_order_hours: -1}, inplaceTrue)这种先分组、再排序、再计算差值的模式在用户行为分析、推荐系统、反欺诈模型中非常实用。5.4 循环时间特征用 sin 和 cos 编码时间周期性现在回到之前提到过的周期性问题。我们把hour直接当数值特征使用时模型学到的是“1 点和 23 点在数值上相距很远”但从业务角度来看这两者都是深夜行为模式非常相似。有没有办法在数值上体现这种“循环接近”的关系答案是循环时间特征。核心思想是把时间映射到单位圆上用正弦和余弦函数来表示时间的周期性。具体做法import numpy as np def encode_cyclical_feature(values, period): 将具有周期性的数值特征转换为 sin/cos 两列特征。 values np.asarray(values, dtypefloat) sin_feat np.sin(2 * np.pi * values / period) cos_feat np.cos(2 * np.pi * values / period) return sin_feat, cos_feat df[hour_sin], df[hour_cos] encode_cyclical_feature(df[hour], 24) df[month_sin], df[month_cos] encode_cyclical_feature(df[month], 12) df[dayofweek_sin], df[dayofweek_cos] encode_cyclical_feature(df[dayofweek], 7)转换之后原来的hour一个特征变成了hour_sin和hour_cos两个特征。这两个特征形成一个二维坐标落在单位圆上。0 点对应 (0, 1)6 点对应 (1, 0)12 点对应 (0, -1)18 点对应 (-1, 0)。通过这种转换“1 点和 23 点”在二维平面上会落在几乎相同的位置因为两者的角度非常接近。这个属性对线性模型和神经网络非常有价值因为它们可以学习到这两个特征的加权组合从而表达出圆形空间中的距离关系。树模型也能从这个编码中受益最直接的体现在于树模型需要找到一个切分点来区分“白天”和“夜晚”如果只有原始hour值切分节点可能是hour 7有了sin和cos之后树模型可以组合“hour_sin小于某个值且hour_cos大于某个值”这种落在二维空间的手段能更精细地捕捉时间段。不过有一点要提醒加入循环特征不代表原始数值特征必须废弃。在很多比赛中同时保留原始hour数值特征和sin/cos特征树模型反而获得了更多信息。5.5 业务时间特征是否周末、是否节假日、是否工作时间日期时间变量中的第三类特征是事件性也就是业务时间特征。这一类特征完全由领域知识驱动。最简单的两个特征是df[is_weekend] df[dayofweek].isin([5, 6]).astype(int) df[is_night] df[hour].isin([22, 23, 0, 1, 2, 3, 4, 5]).astype(int)is_weekend表示是否周末适合电商、外卖、娱乐类业务。工作日和周末的用户行为模式差异通常非常大。is_night表示是否夜间时段。比如生鲜配送、外卖服务等夜间的订单规模会大幅下降但这个特征对这些业务的供需预测非常重要。更复杂的业务特征需要结合外部数据。比如节假日特征简单的办法是维护一个节假日列表holidays pd.to_datetime([ 2024-01-01, # 元旦 2024-02-10, # 春节 2024-04-04, # 清明节 2024-05-01, # 劳动节 2024-06-10, # 端午节 2024-09-17, # 中秋节 2024-10-01, # 国庆节 ]) df[is_holiday] df[order_time].dt.normalize().isin(holidays).astype(int)这里用dt.normalize()把时间归一化到当天零点再去匹配节假日列表。如果有精力还可以计算“距离最近节假日的天数”这个特征对销量预测非常有用。比如距离大促还有 3 天用户开始提前加购大促结束了 2 天销量开始回落。# 简化示例取 2024 年全年日期计算与最近节假日的间隔 all_days pd.date_range(2024-01-01, 2024-12-31, freqD) holiday_flags all_days.isin(holidays).astype(int) # 找到最近一个节假日的偏移天数正数表示距离之后最近的节假日 nearest_holiday_diff [] for day in all_days: diff (holidays - day).days future_diffs diff[diff 0] past_diffs abs(diff[diff 0]) if len(future_diffs) 0: nearest_future future_diffs.min() else: nearest_future 999 if len(past_diffs) 0: nearest_past past_diffs.min() else: nearest_past 999 nearest_holiday_diff.append(min(nearest_future, nearest_past)) holiday_offset_map dict(zip(all_days, nearest_holiday_diff)) df[days_to_nearest_holiday] df[order_time].dt.normalize().map(holiday_offset_map)这类特征需要你对自己的业务场景有足够理解不能机械套用。盲目的做法是试图把所有节假日都做成特征结果导致特征爆炸。5.6 聚合时间特征按用户或类别统计时间段特征最后一类时间特征是通过聚合方法生成的。不是对单条时间记录做变换而是对某类对象在某个时间段内的行为做统计。比如计算每个用户在“最近 7 天”的下单次数df.sort_values(order_time, inplaceTrue) df[user_recent_7d_orders] ( df.groupby(user_id)[order_time] .rolling(7D, min_periods1) .count() .reset_index(level0, dropTrue) )这里的.rolling(7D)是 pandas 的一种时间窗口滚动功能只能在索引是日期时间类型时使用。如果你没有把order_time设为索引会报错如果设了索引groupby之后的 rolling 操作又会变得复杂容易出错。需要提醒的是类似这种“最近 7 天下单次数”的特征在训练集和测试集之间要特别注意数据泄漏问题。如果测试集的时间范围紧跟训练集之后用训练集最后 7 天数据计算出的聚合特征无法直接用于测试集第一天的预测因为你还没有那天的“未来数据”。在工程上通常需要构建一个离线特征库按时间窗口定时更新统计结果。6. 完整示例从订单时间到可建模特征集现在我们用一个完整的示例把前面的内容串起来。这个示例的目标是输入一份带有原始时间字段的订单表输出一份可以被机器学习模型直接使用的特征表。假设我们的任务是预测“订单配送时长”也就是从下单到送达的小时数。用户特征和订单特征中都有时间字段我们要把它们统一转换成模型特征。完整代码如下import pandas as pd import numpy as np # 1. 读取数据解析时间列 df pd.read_csv(order_data.csv, parse_dates[order_time, delivery_time]) # 2. 基础时间成分特征 df[year] df[order_time].dt.year df[month] df[order_time].dt.month df[day] df[order_time].dt.day df[hour] df[order_time].dt.hour df[dayofweek] df[order_time].dt.dayofweek # 3. 循环特征 def encode_cyclical_feature(values, period): values np.asarray(values, dtypefloat) return ( np.sin(2 * np.pi * values / period), np.cos(2 * np.pi * values / period), ) df[hour_sin], df[hour_cos] encode_cyclical_feature(df[hour], 24) df[month_sin], df[month_cos] encode_cyclical_feature(df[month], 12) df[dayofweek_sin], df[dayofweek_cos] encode_cyclical_feature(df[dayofweek], 7) # 4. 业务时间特征 df[is_weekend] df[dayofweek].isin([5, 6]).astype(int) df[is_night] df[hour].isin([22, 23, 0, 1, 2, 3, 4, 5]).astype(int) # 5. 时间差特征 df[delivery_duration_hours] ( df[delivery_time] - df[order_time] ).dt.total_seconds() / 3600.0 # 6. 用户维度时间特征每个用户的历史平均配送时长作为示例特征 df.sort_values(order_time, inplaceTrue) df[prev_delivery_duration] df.groupby(user_id)[delivery_duration_hours].shift(1) df[user_avg_delivery_duration] ( df.groupby(user_id)[delivery_duration_hours] .transform(lambda x: x.expanding().mean().shift(1)) ) # 7. 整理最终特征 feature_cols [ order_amount, year, month, day, hour, dayofweek, hour_sin, hour_cos, month_sin, month_cos, dayofweek_sin, dayofweek_cos, is_weekend, is_night, prev_delivery_duration, user_avg_delivery_duration, ] X df[feature_cols].copy() y df[delivery_duration_hours].copy() print(X.head()) print(y.head())这段代码的关键逻辑说明如下parse_dates是read_csv的参数可以在读取阶段就把指定列解析为datetime64类型省去后续to_datetime的步骤。但如果你需要统一format或处理非法值建议还是用显式的to_datetime。第 6 步中expanding().mean().shift(1)计算的是“到当前订单之前该用户所有历史订单的平均配送时长”。shift(1)非常关键它保证我们只使用历史数据不会把当前订单本身的信息泄漏到特征中。这种特征构造方式在机器学习中称为滞后特征Lag Feature在时间序列预测和用户行为预测中非常常用。运行之后X是一个包含 16 个特征的 DataFramey是目标变量配送时长。此时数据已经可以送入机器学习模型进行训练。当然如果目标变量存在明显的长尾分布可以考虑做对数变换但那是特征工程之后的话题。7. 运行结果与效果验证7.1 验证特征构造是否正确运行完上面的代码后你第一件要确认的事情不是“模型效果好不好”而是“特征构造得对不对”。需要检查的内容包括df[delivery_time] - df[order_time]得到的时间差应该全部是正数如果出现负数说明源数据中送达时间早于下单时间这种数据需要排查。prev_delivery_duration对于每个用户的第一条订单应该是NaN这是正常的。user_avg_delivery_duration的第一条订单也应该是NaN。查看X.isna().sum()确认哪些特征存在缺失值。print(X.isna().sum()) print(df[delivery_duration_hours].describe())如果prev_delivery_duration和user_avg_delivery_duration存在较多缺失值需要采取填充策略。常见做法是用全量数据的均值或中位数填充X[prev_delivery_duration].fillna(X[prev_delivery_duration].median(), inplaceTrue) X[user_avg_delivery_duration].fillna(X[user_avg_delivery_duration].median(), inplaceTrue)7.2 用简单的线性模型做基线验证接下来用这些特征跑一个最简单的线性回归模型验证特征的有效性。from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) model LinearRegression() model.fit(X_train, y_train) y_pred model.predict(X_test) rmse mean_squared_error(y_test, y_pred, squaredFalse) print(fRMSE: {rmse:.3f} 小时)如果 RMSE 比直接使用“全量平均配送时长”作为预测结果要低说明我们构造的特征确实包含了对目标的预测信息。更稳妥的做法是同时计算一个基线结果baseline_pred np.full_like(y_test, y_train.mean()) baseline_rmse mean_squared_error(y_test, baseline_pred, squaredFalse) print(fBaseline RMSE: {baseline_rmse:.3f} 小时)如果特征工程的 RMSE 明显低于基线 RMSE你就有底气说处理日期和时间变量是有价值的。7.3 训练集和测试集的时间一致性检查这是时间特征工程中最重要、也最容易忽略的一步。如果原始数据集是按时间顺序排列的你又用了train_test_split默认的随机拆分方式那么训练集和测试集会混有不同时间段的数据这会掩盖真正的时间分布不一致问题。在做时间相关特征时强烈建议手动按时间先后顺序拆分数据而不是随机拆分X X.sort_index() split_time df[order_time].quantile(0.8) train_mask df[order_time] split_time test_mask df[order_time] split_time X_train, X_test X[train_mask], X[test_mask] y_train, y_test y[train_mask], y[test_mask]这样拆分之后训练集的时间范围都在测试集之前模拟的是真实场景用历史数据训练预测未来数据。为什么要特别强调这一点因为时间特征和普通特征的一个根本区别是普通特征在训练集和测试集中的分布是可迁移的时间特征的分布往往不可迁移。如果你用随机拆分的方式模型可能在“训练集里有 6 月数据测试集里也有 6 月数据”的条件下表现很好但线上部署时模型面对的几乎全是未来的数据分布完全不同效果很容易崩盘。7.4 树模型中的特征重要性验证除了线性模型的 RMSE 外还可以用树模型输出特征重要性判断时间相关特征对模型预测的贡献。from sklearn.ensemble import RandomForestRegressor rf_model RandomForestRegressor(n_estimators100, random_state42) rf_model.fit(X_train, y_train) importance_df pd.DataFrame({ feature: feature_cols, importance: rf_model.feature_importances_, }).sort_values(importance, ascendingFalse) print(importance_df)运行后你可以直观看到哪些时间特征被模型认为最重要。通常来说prev_delivery_duration、user_avg_delivery_duration、order_amount这类行为性特征的重要性会比单独的month、day高。如果后发现year的重要性异常高大概率是数据里混入了未来的信息或者特征泄漏需要警惕。8. 常见问题与排查思路处理日期和时间变量时以下这些坑是我在实际项目里遇到最频繁的整理成一张表方便你直接对照排查。问题现象可能原因排查方式解决方案pd.to_datetime()报错无法解析字符串格式不是标准 ISO 格式打印出列中不相同的字符串样例配合errorscoerce参数查看哪些行变成NaT再针对特殊格式显式指定format解析后出现大量NaT数据中混入缺失值或非法日期检查isna().sum()并打印NaT的前几行根据业务决定删除或填充不要放任缺失值进入模型时间差计算出来是负数源数据中的结束时间早于开始时间查看这些记录对应的业务含义根据业务规则判断是时间记录错乱还是跨时区问题必要时删除或修正时间字段被当作字符串显示读取 CSV 时没有指定parse_dates查看df.dtypes使用pd.to_datetime()显式转换rolling(7D)报错时间列未设置为索引或者索引不是datetime64类型检查df.index的类型使用df.set_index(order_time)后再做滚动窗口操作或者改用groupby加自定义窗口逻辑模型特征重要性中年份占比异常数据泄漏目标变量间接包含未来信息检查特征构造过程是否使用到了未来数据在构造滞后特征时使用shift()并用时间顺序拆分训练集和测试集周末/节假日特征全部是 0节假日列表数据范围不匹配打印holidays.dtype和order_time.dtype确认两者都是datetime64类型且时区一致加了大量时间特征后模型效果反而下降特征冗余或引入噪声查看特征相关性矩阵和特征重要性先做简单时间特征再逐步增加复杂特征通过验证集效果决定保留或删除其中我特别想强调第一个问题。很多初学者遇到to_datetime报错第一反应是“我的数据格式是不是不对”然后去改数据而不是改解析逻辑。这方向反了。工程上更多时候是数据格式不够规范正确的做法是先不指定format调用to_datetime让它自动解析如果报错用errorscoerce定位哪些行无法解析然后针对无法解析的行单独检查格式再写对应的解析规则。千万不要为了让代码跑通把所有时间列都强制转为字符串那样后面的特征工程全都没有意义。9. 最佳实践与工程建议9.1 先从“绝对时间”与“相对时间”两个维度思考处理日期时间变量时不妨先把所有时间字段按照“绝对时间”和“相对时间”两个维度分类。绝对时间指的是订单时间、注册时间、登录时间本身承载的是“事件发生在什么时刻”的信息。相对时间指的是两次事件之间的间隔、距离当前时间的时长承载的是“事件之间的先后顺序和间隔关系”。在大多数业务模型中相对时间特征比绝对时间特征更稳定。原因是绝对时间的分布会随训练数据的变化而变化比如今年训练的数据和明年线上数据的年份、月份分布完全不同而相对时间只要业务规则不变特征分布就不会发生剧烈变化。做特征工程时优先挖掘相对时间特征几乎总是正确的策略。9.2 日期时间解析阶段不要用函数式串联常见的错误写法是在读取数据时写这样的代码df pd.read_csv(data.csv) df[order_time] df[order_time].apply(lambda x: pd.to_datetime(x))用apply循环解析时间性能非常差。pandas 的to_datetime是向量化操作应该直接传入整个 Series。正确写法是df[order_time] pd.to_datetime(df[order_time])如果担心格式问题显式指定format参数即可。9.3 时间特征命名规范建议统一使用英文小写加下划线的风格并且要能看出特征的时间口径。比如order_time_year订单时间的年份。delivery_duration_hours配送时长小时。user_first_order_time用户首单时间。days_since_last_order距离上一笔订单的天数。这种命名方式有两个好处一是团队协作时其他人能一眼看出特征的含义和单位二是在做特征监控时能快速定位到异常特征对应的业务含义。9.4 所有特征构造都要考虑时间泄漏时间泄漏是时间特征工程中最致命的问题。它指的是你在构造训练特征时不小心用到了当前时间点之后的数据。最常见的泄漏场景是按用户分组计算统计值时没有使用shift()导致当前订单的统计特征包含了当前订单自己的信息模型性能虚高。判断是否存在时间泄漏的方法是用时间顺序拆分训练集和测试集然后检查模型在测试集上的效果和随机拆分的差异。如果随机拆分的效果远好于时间顺序拆分的效果一定要怀疑是否存在时间泄漏。9.5 训练集和测试集的拆分规则在包含时间特征的建模任务中强烈建议按时间拆分而不是随机拆分。这一点在之前已经反复强调。具体做法是cutoff_date df[order_time].quantile(0.8) train_df df[df[order_time] cutoff_date].copy() test_df df[df[order_time] cutoff_date].copy()之后再做特征工程时只能使用train_df的数据来拟合编码器、计算聚合统计值和填充缺失值否则还是会造成信息泄漏。例如如果你用全量数据的中位数来填充缺失值再按时间拆分训练集和测试集测试集中缺失值的信息其实已经被“泄漏”到了填充值中。正确做法是fill_value train_df[delivery_duration_hours].median() train_df[delivery_duration_hours].fillna(fill_value, inplaceTrue) test_df[delivery_duration_hours].fillna(fill_value, inplaceTrue)9.6 保留原始时间列不要覆盖最后一条工程建议无论做多少时间特征原始时间列一定要保留。原因有两个。第一随时可能需要回溯检查原始时间数据和某个特征之间的对应关系。如果一开始就把order_time覆盖成了年份特征后面再想验证“2024 年上半年是否有异常值”就得重新读取原始数据。第二后续调试时可能需要重新构造特征这时最方便的做法是回到原始时间列重新开始。10. 总结与后续学习方向这一节不打算做那种“本文主要介绍了……”的流水账总结只说两件重要的事。第一处理日期和时间变量没有“万能模板”但有一条清晰的思维主线先确认时间数据的类型和格式再解析成datetime64然后根据业务场景判断需要提取“绝对时间特征”“相对时间特征”还是“周期特征”最后验证特征是否存在泄漏、在训练集和测试集之间分布是否稳定。你只要沿着这条主线走就不会出现“拿到时间列不知道该怎么办”的情况。第二对于 100 天机器学习这条学习路径来说第 34 天真正想让你建立的不是某个具体的日期特征提取函数而是“时间变量是一种需要单独对待的特殊数据类型”这个意识。它和普通数值、类别变量的处理思路完全不同。掌握了这一点你再看那些使用时间特征拿到高分的数据科学比赛方案会觉得思路清晰很多。接下来可以深入研究的方向包括时间序列建模中的滞后特征和滚动统计特征、外部数据源天气、节假日、经济指标的时间特征融合、以及 pyspark 等大数据环境下如何对海量时间特征做分布式处理。如果你正在系统学习机器学习建议把这些内容作为第 34 天之后的延伸练习找一份真实数据集按照本文的流程完整跑一遍效果验证然后再去对比“完全不处理时间变量”和“手工构造时间特征”的模型效果差异。只有亲手跑过一遍这些方法才能真正变成你自己的技能。
返回列表