机器学习时间特征处理:循环编码(Cyclical Encoding)与其在预测模型中的应用

简介: 处理时间特征时,直接使用线性数值会引发“午夜悖论”,导致模型在时间断点处表现失真。本文详解如何用正弦和余弦函数将时间映射为循环特征,解决23:59与00:01的断裂问题,提升模型对周期性模式的理解,适用于小时、星期、月份等场景,显著优化预测效果。

做过电力负荷预测或者交通预测朋友,大概率都处理过时间特征。这里最直接的做法通常是把时间(比如分钟或小时)直接扔进模型里。这看起来逻辑自洽,但存在这一个大坑,就是“午夜悖论”。

比如说你的模型面对两个时间点:23:59(一天的第1439分钟) 和 00:01(一天的第1分钟)。在我们的认知里,这俩只差两分钟,但在模型的逻辑里1439 和 1 可是不一样的。大多数机器学习算法(线性回归、KNN、SVM 甚至神经网络)在处理数值时,默认遵循线性逻辑:数值越大,代表的量级越高。它们理解不了“时间是循环的”这个概念。对它们来说午夜不是终点回到起点的闭环,而是一个断崖。

这就是为什么你加了时间特征,模型却在日期变更线附近表现拉胯的根本原因。

传统编码方式的局限性

处理时间特征,最常见的路数无非两种,但这两种都有硬伤。

整数编码(Integer Encoding)

把 0 到 23 点编码成数字 0-23。这就人为制造了一个断层:23 到 0 的跳跃,被模型视作全天最大的波动。但实际上,晚上 11 点到午夜的变化,跟晚上 9 点到 10 点有什么本质区别吗?完全没有。

下面是这种线性模式下,时间特征在数据层面的表现。

 # Generate data
date_today = pd.to_datetime('today').normalize()
datetime_24_hours = pd.date_range(start=date_today, periods=24, freq='h')
df = pd.DataFrame({'dt': datetime_24_hours})
df['hour'] = df['dt'].dt.hour

# Calculate Sin and Cosine
df["hour_sin"] = np.sin(2 * np.pi * df["hour"] / 24)
df["hour_cos"] = np.cos(2 * np.pi * df["hour"] / 24)

# Plot the Hours in Linear mode
plt.figure(figsize=(15, 5))
plt.plot(df['hour'], [1]*24, linewidth=3)
plt.title('Hours in Linear Mode')
plt.xlabel('Hour')
plt.xticks(np.arange(0, 24, 1))
plt.ylabel('Value')
 plt.show()

线性模式下的小时表示。

独热编码(One-hot Encoding)

既然连续数字有问题,那拆成 24 个独立的列呢?断层是没了但丢失了更重要的东西:邻近性(Proximity)。在独热编码下,凌晨 2 点和 3 点的距离,跟它和晚上 10 点的距离是一样的。模型失去了“时间相邻”这个上下文信息,更别提这会让特征维度爆炸,树模型处理起来效率低,线性模型跑起来也费劲。

解决方案:三角函数映射(Trigonometric Mapping)

解决这个问题的核心在于思维视角的转换:不要把时间看作一条直线,而要看作一个圆。

24小时是一个闭环,我们的编码方式也得闭环。把每一个小时想象成圆周上均匀分布的点,要确定圆上一个点的位置单靠一个数值是不够的,我们需要两个坐标: x and y

这就是正弦(Sine)和余弦(Cosine)发挥作用的地方。

几何原理

圆周上的任意角度都可以通过正弦和余弦映射到一个唯一的坐标点。这种映射赋予了模型一个平滑、连续的时间表示。

 plt.figure(figsize=(5, 5))
 plt.scatter(df['hour_sin'], df['hour_cos'], linewidth=3)
 plt.title('Hours in Cyclical Mode')
 plt.xlabel('Hour')

经过正弦和余弦转换后的循环模式。

计算公式非常简单:

2 * π * hour / 24

:先把小时数值转化成弧度角度。在这个体系下,午夜和晚上 11 点的角度非常接近,通过

sin

cos

将角度投影到两个坐标轴上。

这两个值结合在一起唯一确定了当前的小时,23:00 和 00:00 在特征空间里的距离就被拉得很近了,这正是我们想要的效果。

这套逻辑同样适用于分钟、星期、月份等任何具有周期性的特征。

代码实战

我们拿 UCI 的 Appliances Energy Prediction 数据集来跑个对比实验。模型选用随机森林回归器(Random Forest Regressor)。

Candanedo, L. (2017). Appliances Energy Prediction [Dataset]. UCI Machine Learning Repository. https://doi.org/10.24432/C5VC8G. Creative Commons 4.0 License.

 # Imports
 from sklearn.ensemble import RandomForestRegressor
 from sklearn.model_selection import train_test_split
 from sklearn.metrics import root_mean_squared_error
 from ucimlrepo import fetch_ucirepo

获取数据:

 # fetch dataset 
appliances_energy_prediction = fetch_ucirepo(id=374) 

# data (as pandas dataframes) 
X = appliances_energy_prediction.data.features 
y = appliances_energy_prediction.data.targets 

# To Pandas
df = pd.concat([X, y], axis=1)
df['date'] = df['date'].apply(lambda x: x[:10] + ' ' + x[11:])
df['date'] = pd.to_datetime(df['date'])
df['month'] = df['date'].dt.month
df['day'] = df['date'].dt.day
df['hour'] = df['date'].dt.hour
df.head(3)

先建立一个基准模型(Baseline),使用未处理的线性时间特征。

 # X and y
# X = df.drop(['Appliances', 'rv1', 'rv2', 'date'], axis=1)
X = df[['hour', 'day', 'T1', 'RH_1', 'T_out', 'Press_mm_hg', 'RH_out', 'Windspeed', 'Visibility', 'Tdewpoint']]
y = df['Appliances']

# Train Test Split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Fit the model
lr = RandomForestRegressor().fit(X_train, y_train)

# Score
print(f'Score: {lr.score(X_train, y_train)}')

# Test RMSE
y_pred = lr.predict(X_test)
rmse = root_mean_squared_error(y_test, y_pred)
print(f'RMSE: {rmse}')

基准结果如下:

 Score: 0.9395797670166536
 RMSE: 63.60964667197874

接下来我们对

hour

day

进行循环编码,替换掉原来的线性特征然后重新训练模型。

 # Add cyclical hours sin and cosine
df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24)
df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)
df['day_sin'] = np.sin(2 * np.pi * df['day'] / 31)
df['day_cos'] = np.cos(2 * np.pi * df['day'] / 31)

# X and y
X = df[['hour_sin', 'hour_cos', 'day_sin', 'day_cos','T1', 'RH_1', 'T_out', 'Press_mm_hg', 'RH_out', 'Windspeed', 'Visibility', 'Tdewpoint']]
y = df['Appliances']

# Train Test Split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# Fit the model
lr_cycle = RandomForestRegressor().fit(X_train, y_train)

# Score
print(f'Score: {lr_cycle.score(X_train, y_train)}')

# Test RMSE
y_pred = lr_cycle.predict(X_test)
rmse = root_mean_squared_error(y_test, y_pred)
print(f'RMSE: {rmse}')

结果显示,Score 提升了约 1%,RMSE 下降了 1 个点。

 Score: 0.9416365489096074
 RMSE: 62.87008070927842

看着提升不大?这只是一个简单的 Toy Example,也没做任何精细的数据清洗。但这里的提升完全来自于正弦和余弦变换带来的特征表达能力的增强。

本质上这让模型“看懂”了现实世界中电力需求的连续性,它并不会因为时钟跳过 0 点就突然归零。

为什么 Sin 和 Cos 缺一不可?

很多人可能会想,只用 Sin 这一列行不行?还能省点特征维度。答案是不行。这会破坏对称性。在一个 24 小时的圆周上,早上 6 点和晚上 6 点的 Sine 值可能是一样的。如果只给模型一个值,它就会混淆这两个截然不同的时间段(比如早高峰和晚高峰)。必须同时使用 Sin 和 Cos就像定位必须要有经度和纬度一样。只有这样圆上的每一个小时才能拥有唯一的“特征指纹”。

实际应用中的收益

这套方法在不同模型下的收益是不一样的:

  • 基于距离的模型(KNN, SVMs):这是最大的受益者。循环编码消除了边界上的伪“长距离”,让数据点之间的距离计算回归真实。
  • 神经网络(Neural Networks):平滑的特征空间有助于网络更快的收敛和更稳定的训练表现,消除了午夜那种剧烈的数值跳变。
  • 树模型(Tree-based models):虽然像 XGBoost 或 LightGBM 这种强力模型最终也能通过不断分裂学到这种模式,但提供循环编码特征相当于给了它们一个极佳的先验知识(Inductive Bias),在追求极致性能和解释性时非常有用。

适用场景

使用这套方法的判断标准很简单,问自己一个问题:这个特征是循环往复的吗?如果是,那就试试。常见的例子包括:

  • 一天中的小时(0-23)
  • 一周中的星期(1-7)
  • 一年中的月份(1-12)
  • 风向(0-360度)

总结

时间在数据科学里不应该只是一个冰冷的数字,它本质上是圆周上的坐标。如果你执意把它当直线处理,模型在周期边界处跌倒是迟早的事。使用正弦和余弦进行循环编码,是一种优雅且低成本的修正手段。它保留了数据的邻近性,消除了人工伪影,能让模型学得更快、更准。下次如果你的模型预测曲线在日期交界处出现诡异的跳变,不妨试试这个方法。

https://avoid.overfit.cn/post/5fea3ffcb7ac4b27a3a0d7bb55b9bd39

作者:Gustavo Santos

目录
相关文章
|
8天前
|
数据采集 人工智能 安全
|
17天前
|
云安全 监控 安全
|
3天前
|
机器学习/深度学习 人工智能 前端开发
构建AI智能体:七十、小树成林,聚沙成塔:随机森林与大模型的协同进化
随机森林是一种基于决策树的集成学习算法,通过构建多棵决策树并结合它们的预测结果来提高准确性和稳定性。其核心思想包括两个随机性:Bootstrap采样(每棵树使用不同的训练子集)和特征随机选择(每棵树分裂时只考虑部分特征)。这种方法能有效处理大规模高维数据,避免过拟合,并评估特征重要性。随机森林的超参数如树的数量、最大深度等可通过网格搜索优化。该算法兼具强大预测能力和工程化优势,是机器学习中的常用基础模型。
292 164
|
2天前
|
机器学习/深度学习 自然语言处理 机器人
阿里云百炼大模型赋能|打造企业级电话智能体与智能呼叫中心完整方案
畅信达基于阿里云百炼大模型推出MVB2000V5智能呼叫中心方案,融合LLM与MRCP+WebSocket技术,实现语音识别率超95%、低延迟交互。通过电话智能体与座席助手协同,自动化处理80%咨询,降本增效显著,适配金融、电商、医疗等多行业场景。
303 155
|
4天前
|
机器学习/深度学习 人工智能 前端开发
构建AI智能体:六十九、Bootstrap采样在大模型评估中的应用:从置信区间到模型稳定性
Bootstrap采样是一种通过有放回重抽样来评估模型性能的统计方法。它通过从原始数据集中随机抽取样本形成多个Bootstrap数据集,计算统计量(如均值、标准差)的分布,适用于小样本和非参数场景。该方法能估计标准误、构建置信区间,并量化模型不确定性,但对计算资源要求较高。Bootstrap特别适合评估大模型的泛化能力和稳定性,在集成学习、假设检验等领域也有广泛应用。与传统方法相比,Bootstrap不依赖分布假设,在非正态数据中表现更稳健。
233 113
|
11天前
|
SQL 自然语言处理 调度
Agent Skills 的一次工程实践
**本文采用 Agent Skills 实现整体智能体**,开发框架采用 AgentScope,模型使用 **qwen3-max**。Agent Skills 是 Anthropic 新推出的一种有别于mcp server的一种开发方式,用于为 AI **引入可共享的专业技能**。经验封装到**可发现、可复用的能力单元**中,每个技能以文件夹形式存在,包含特定任务的指导性说明(SKILL.md 文件)、脚本代码和资源等 。大模型可以根据需要动态加载这些技能,从而扩展自身的功能。目前不少国内外的一些框架也开始支持此种的开发方式,详细介绍如下。
809 6