赛事概要
一、赛题背景
随着全球经济的快速发展和城市化进程的加速,电力系统面临着越来越大的挑战。电力需求的准确预测对于电网的稳定运行、能源的有效管理以及可再生能源的整合至关重要。然而,电力需求受到多种因素的影响,为了提高电力需求预测的准确性和可靠性,推动智能电网和可持续能源系统的发展,本场以“电力需求预测”为赛题的数据算法挑战赛。选手需要根据历史数据构建有效的模型,能够准确的预测未来电力需求。
二、赛题任务
给定多个房屋对应电力消耗历史N天的相关序列数据等信息,预测房屋对应电力的消耗。
三、评审规则
1.数据说明
赛题数据由训练集和测试集组成,为了保证比赛的公平性,将每日日期进行脱敏,用1-N进行标识,即1为数据集最近一天,其中1-10为测试集数据。数据集由字段id(房屋id)、 dt(日标识)、type(房屋类型)、target(实际电力消耗)组成。
2.评审规则
预测结果以 mean square error 作为评判标准,具体公式如下:
其中,
是真实电力消耗,
是预测电力消耗。
Task 1:小白入门跑通Baseline(经验模型)
本赛题是一个典型的时间序列问题,时间序列问题是指对按时间顺序排列的数据点进行分析和预测的问题,往往用来做未来的趋势预测。比如,基于历史股票每天的股价,预测未来股票的价格走向。简单来说,本次赛题的目标很简单清晰——【训练时序预测模型助力电力需求预测】
时间序列预测问题
一、定义
时间序列预测是指利用获得的数据按时间顺序排成序列,分析其变化方向和程度,从而对未来若干时期可能达到的水平进行推测。这种方法基于统计学术语,将时间序列视为一个随机变量的样本,运用概率统计的方法,尽可能减少偶然因素的影响,以实现对未来的预测。
二、原理
时间序列预测法的原理基于以下几点:
事物发展的延续性:任何事物的发展总是与其过去有着密切的联系,因此,通过运用过去时间序列的数据进行统计分析,能够推测事物的发展趋势。
随机性处理:考虑到事物发展过程中的偶然因素,利用历史数据,通过统计分析、加权平均等方法对数据进行适当的处理,以消除随机波动的影响,从而进行趋势预测
常见的时间序列场景有
- 金融领域:股票价格预测、利率变动、汇率预测等。
- 气象领域:温度、降水量、风速等气候指标的预测。
- 销售预测:产品或服务的未来销售额预测。
- 库存管理:预测库存需求,优化库存水平。
- 能源领域:电力需求预测、石油价格预测等。
- 医疗领域:疾病爆发趋势预测、医疗资源需求预测。
时间序列问题的数据往往有如下特点
- 时间依赖性:数据点之间存在时间上的连续性和依赖性。
- 非平稳性:数据的统计特性(如均值、方差)随时间变化。
- 季节性:数据表现出周期性的模式,如年度、月度或周度。
- 趋势:数据随时间推移呈现长期上升或下降的趋势。
- 周期性:数据可能存在非固定周期的波动。
- 随****机波动:数据可能受到随机事件的影响,表现出不确定性。
时间序列预测问题可以通过多种建模方法来解决,包括传统的时间序列模型、机器学习模型和深度学习模型。
以下是这三种方法的建模思路、优缺点对比:
模型
建模思路
优点
缺点
传统时间序列模型
基于时间序列数据的统计特性,如自相关性、季节性等。
使用ARIMA、季节性ARIMA(SARIMA)、指数平滑等模型。
通过识别数据的趋势和季节性成分来构建模型。
模型结构简单,易于理解和解释。
计算效率高,适合于数据量较小的问题。
直接针对时间序列数据设计,能够很好地处理数据的季节性和趋势。
对于非线性模式和复杂的时间序列数据,预测能力有限。
需要手动进行参数选择和模型调整。
对数据的平稳性有严格要求,非平稳数据需要差分等预处理。
机器学习模型
将时间序列数据转换为监督学习问题,使用历史数据作为特征,未来值作为标签。
使用决策树、随机森林、梯度提升树等模型。
通过特征工程来提取时间序列数据中的有用信息。
能够处理非线性关系和复杂的数据模式。
通过特征工程可以引入额外的解释性变量。
模型选择多样,可以进行模型融合以提高预测性能。
对于时间序列数据的内在时间结构和季节性可能不够敏感。
需要大量的特征工程工作。
模型的解释性可能不如传统时间序列模型。
深度学习模型
使用循环神经网络(RNN)、长短期记忆网络(LSTM)或一维卷积神经网络(1D-CNN)等模型。
能够捕捉时间序列数据中的长期依赖关系。
通过训练大量的参数来学习数据的复杂模式。
能够处理非常复杂的数据模式和长期依赖关系。
适用于大量数据,可以自动提取特征。
模型的灵活性和适应性强。
需要大量的数据和计算资源。
模型训练和调优可能比较复杂和耗时。
模型的解释性较差,难以理解预测结果的原因。
对比总结
- 适用性:传统模型适合数据量较小、模式简单的问题;机器学习模型适合中等复杂度的问题,可以引入额外变量;深度学习模型适合数据量大、模式复杂的任务。
- 解释性:传统时间序列模型通常具有较好的解释性;机器学习模型的解释性取决于特征工程;深度学习模型的解释性通常较差。
- 计算资源:传统模型计算效率最高;机器学习模型次之;深度学习模型通常需要最多的计算资源。
- 预测能力:深度学习模型在捕捉复杂模式方面具有优势,但需要大量数据支持;传统和机器学习模型在数据量较小或模式较简单时可能更有效。
在实际应用中,选择哪种模型取决于具体问题的需求、数据的特性以及可用的计算资源。有时,结合多种方法的混合模型也可以提供更好的预测性能。
首先尝试使用python代码构建一个经验模型(使用均值作为结果数据)
Baseline代码如下:
# 1. 导入需要用到的相关库
# 导入 pandas 库,用于数据处理和分析
import pandas as pd
# 导入 numpy 库,用于科学计算和多维数组操作
import numpy as np
# 2. 读取训练集和测试集
# 使用 read_csv() 函数从文件中读取训练集数据,文件名为 'train.csv'
train = pd.read_csv('train.csv')
# 使用 read_csv() 函数从文件中读取测试集数据,文件名为 'test.csv'
test = pd.read_csv('test.csv')
# 3. 计算训练数据最近11-20单位时间内对应id的目标均值
target_mean = train[train['dt']<=20].groupby(['id'])['target'].mean().reset_index()
# 4. 将target_mean作为测试集结果进行合并
test = test.merge(target_mean, on=['id'], how='left')
# 5. 保存结果文件到本地
test[['id','dt','target']].to_csv('submit.csv', index=None)
- 导入库:首先,代码导入了需要用到的库,包括
pandas—
用于数据处理和分析,提供高效的数据结构(如DataFrame)和数据分析工具,numpy—
一个科学计算库,特别是针对数组和矩阵的操作。import pandas as pdimport numpy as np
pandas 和 numPy 通常一起使用,因为 pandas 的设计初衷就是为了与 numPy 无缝集成,利用 numPy 的数组结构来提高性能。
读取数据:代码通过使用
pd.read_csv
函数从文件中读取训练集和测试集数据,并将其存储在train.csv
和test.csv
两个数据框中。train = pd.read_csv('train.csv')test = pd.read_csv('test.csv'
计算最近时间的用电均值:计算训练数据最近11-20单位时间内对应id的目标均值,可以用来反映最近的用电情况。
target_mean = train[train['dt']<=20].groupby(['id'])['target'].mean().reset_index()
这行代码首先筛选出train DataFrame中dt列值小于或等于20的行,这代表选择了最近11-20 单位时间内的数据。然后,它使用groupby(['id'])对这些行按id列进行分组。接着,它计算每个id组内的target列的平均值。最后,reset_index()函数被调用以将结果转换回DataFrame
格式,其中包含id和计算出的target均值作为列。
groupby用法:
grouped = df.groupby(by=None, axis=0, level=None, as_index=True, sort=True, group_keys=True, squeeze=False, observed=False)
by
:用于分组的列名或列名的列表/数组,或者用于分组的函数/字典等。如果不指定,则尝试使用 DataFrame 的索引。axis
:指定分组的轴,默认为 0(即沿着行的方向分组)。level
:如果轴是 MultiIndex(多级索引),则按级别分组。as_index
:布尔值,默认为 True。如果为 True,则返回的对象的索引将是分组键。如果为 False,则返回的索引将是原始索引。sort
:布尔值,默认为 True。对于分组键进行排序。注意:从 pandas 1.0.0 开始,sort
参数默认为 True,但在未来的版本中可能会更改默认值。group_keys
:布尔值,默认为 True。如果为 True,则返回的 DataFrame 将包含分组键作为索引的一部分。squeeze
:布尔值或 'auto',默认为 False。如果为 True,并且分组只返回一个 Series,则返回一个 Series 而不是 DataFrame。observed
:布尔值,默认为 False。如果为 True,则仅考虑在 DataFrame 中实际观察到的类别进行分组。最常见的用法之一是对分组后的数据进行聚合操作,如求和、平均值、最大值、最小值等。
- 将用电均值直接作为预测结果:这里使用
merge
函数根据'id'
列将test
和target_mean
两个 DataFrame进行左连接(how='left'),这意味着测试集的所有行都会保留。
test = test.merge(target_mean, on=['id'], how='left')
merge用法:
pandas.merge(left, right, how='inner', on=None, left_on=None, right_on=None, left_index=False, right_index=False, sort=True, suffixes=('_x', '_y'), copy=True, indicator=False, validate=None)
- left:要合并的左侧 DataFrame 对象。
- right:要合并的右侧 DataFrame 对象。
- how:指定合并的方式,默认为 'inner'。可选值为 'left'、'right'、'outer'、'inner'。- 'inner':内连接,只保留两个 DataFrame 中都有的键的行。- 'left':左连接,保留左侧 DataFrame 的所有键,如果右侧 DataFrame 中没有匹配的键,则对应列填充为 NaN。- 'right':右连接,保留右侧 DataFrame 的所有键,如果左侧 DataFrame 中没有匹配的键,则对应列填充为 NaN。- 'outer':外连接,保留两个 DataFrame 的所有键,如果没有匹配的键,则对应列填充为 NaN。
- on:指定用于合并的列名。如果指定了该参数,则 left 和 right DataFrame 对象都必须包含该列。如果不指定,则默认使用两个DataFrame中的公共列进行合并。
- left_on:左侧 DataFrame 中用作连接键的列名。
- right_on:右侧 DataFrame 中用作连接键的列名。
- left_index:如果为 True,则使用左侧 DataFrame 中的索引(行标签)作为其连接键的列。
- right_index:如果为 True,则使用右侧 DataFrame 中的索引(行标签)作为其连接键的列。
- sort:是否对结果进行排序。如果为 True,则按照连接键对结果进行排序。默认为 True,但设置为 False 可以在很多情况下显著提高性能。
- suffixes:用于重叠列名的后缀。如果两个 DataFrame 对象中都有相同名称的列,则会在列名后面添加指定的后缀以区分它们。默认为 ('_x', '_y')。
- copy:是否复制数据。如果为 False,则避免复制数据以提高性能。注意,在较新版本的 Pandas 中,此参数可能已被弃用。
- indicator:是否添加一个名为 '_merge' 的列,用于指示每一行的来源。如果为 True,则添加该列。
- validate:验证连接键是否唯一。可以是 'one_to_one'、'one_to_many' 或 'many_to_one'。这有助于发现可能的合并错误。
- 保存结果文件到本地:使用
to_csv()
函数将测试集的'id'
、'dt'
和'target'
列保存为CSV文 件,文件名为'submit.csv'
。index=None
参数表示在保存时不包含行索引。
test[['id','dt','target']].to_csv('submit.csv', index=None)
思考体验
Baseline难度不大,通过大佬的直播讲解更加深刻理解最基础的机器学习思维方式,代码所体现的预测方式是基于11-20天内的均值,方法十分基础,但不是最优解。
Task 2:入门lightgbm,开始特征工程
进阶:使用机器学习模型解决本次问题,模型使用简单,数据不需要过多预处理;
使用机器学习方法一般主要需要从 获取数据&增强、特征提取和**模型 **三个方面下手。
基础概念入门
GBDT
GBDT (Gradient Boosting Decision Tree) 是机器学习中一个长盛不衰的模型,其主要思想是利用弱分类器(决策树)迭代训练以得到最优模型,该模型具有训练效果好、不易过拟合等优点。
GBDT不仅在工业界应用广泛,通常被用于多分类、点击率预测、搜索排序等任务;在各种数据挖掘竞赛中也是致命武器。
LightGBM
LightGBM(Light Gradient Boosting Machine)是一个实现GBDT算法的框架,支持高效率的并行训练,并且具有更快的训练速度、更低的内存消耗、更好的准确率、支持分布式可以快速处理海量数据等优点。
LightGBM 框架中还包括随机森林和逻辑回归等模型。通常应用于二分类、多分类和排序等场景。
例如:在个性化商品推荐场景中,通常需要做点击预估模型。使用用户过往的行为(点击、曝光未点击、购买等)作为训练数据,来预测用户点击或购买的概率。根据用户行为和用户属性提取一些特征,包括:
- 类别特征(Categorical Feature):字符串类型,如性别(男/女)。
- 物品类型:服饰、玩具和电子等。
- 数值特征(Numrical Feature):整型或浮点型,如用户活跃度或商品价格等。
进阶代码如下:
import numpy as np
import pandas as pd
import lightgbm as lgb
from sklearn.metrics import mean_squared_log_error, mean_absolute_error, mean_squared_error
import tqdm
import sys
import os
import gc
import argparse
import warnings
warnings.filterwarnings('ignore')
train = pd.read_csv('./data/data283931/train.csv')
test = pd.read_csv('./data/data283931/test.csv')
# 合并训练数据和测试数据,并进行排序
data = pd.concat([test, train], axis=0, ignore_index=True)
data = data.sort_values(['id','dt'], ascending=False).reset_index(drop=True)
# 历史平移
for i in range(10,30):
data[f'last{i}_target'] = data.groupby(['id'])['target'].shift(i)
# 窗口统计
data[f'win3_mean_target'] = (data['last10_target'] + data['last11_target'] + data['last12_target']) / 3
# 进行数据切分
train = data[data.target.notnull()].reset_index(drop=True)
test = data[data.target.isnull()].reset_index(drop=True)
# 确定输入特征
train_cols = [f for f in data.columns if f not in ['id','target']]
def time_model(lgb, train_df, test_df, cols):
# 训练集和验证集切分
trn_x, trn_y = train_df[train_df.dt>=31][cols], train_df[train_df.dt>=31]['target']
val_x, val_y = train_df[train_df.dt<=30][cols], train_df[train_df.dt<=30]['target']
# 构建模型输入数据
train_matrix = lgb.Dataset(trn_x, label=trn_y)
valid_matrix = lgb.Dataset(val_x, label=val_y)
# lightgbm参数
lgb_params = {
'boosting_type': 'gbdt',
'objective': 'regression',
'metric': 'mse',
'device_type' : 'gpu',
'min_child_weight': 5,
'num_leaves': 2 ** 5,
'lambda_l2': 10,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'bagging_freq': 4,
'learning_rate': 0.05,
'seed': 2024,
'nthread' : 16,
'verbose' : -1,
}
# 训练模型
from lightgbm.callback import log_evaluation
model = lgb.train(lgb_params, train_matrix, 50000,
valid_sets=[train_matrix, valid_matrix], categorical_feature=[], callbacks=[lgb.early_stopping(500), lgb.log_evaluation(500)])
# 验证集和测试集结果预测
val_pred = model.predict(val_x, num_iteration=model.best_iteration)
test_pred = model.predict(test_df[cols], num_iteration=model.best_iteration)
return val_pred, test_pred
lgb_oof, lgb_test = time_model(lgb, train, test, train_cols)
# 保存结果文件到本地
test['target'] = lgb_test
test[['id','dt','target']].to_csv('submit.csv', index=None)
- 导入库:
# 导入NumPy库,并简称为np。NumPy是Python中用于科学计算的基础库,提供了高性能的多维数组对象和相关工具。
import numpy as np
# 导入Pandas库,并简称为pd。Pandas是Python的一个开源数据分析库,提供了快速、灵活和表达式丰富的数据结构,旨在使“关系”或“标签”数据的处理工作变得既简单又直观。
import pandas as pd
# 导入LightGBM库,并简称为lgb。LightGBM是一个基于梯度提升框架的高效实现,用于排序、分类、回归等多种机器学习任务,支持高效的并行和分布式训练。
import lightgbm as lgb
# 从sklearn.metrics模块中导入三个用于评估回归模型性能的指标:均方对数误差、平均绝对误差和均方误差。
from sklearn.metrics import mean_squared_log_error, mean_absolute_error, mean_squared_error
# 导入tqdm库,用于在长时间运行的循环中添加进度条,提升用户体验。
import tqdm
# 导入sys模块,该模块提供了一些变量和函数,用以操作Python运行时环境。
import sys
# 导入os模块,该模块提供了许多与操作系统交互的功能,比如文件和目录管理。
import os
# 导入gc模块,该模块提供了垃圾收集器的接口,允许你强制执行垃圾收集,或者获取垃圾收集器的相关信息。
import gc
# 导入argparse模块,用于编写用户友好的命令行接口程序。
import argparse
# 使用warnings模块来忽略所有的警告信息。这通常用于避免在输出中显示不重要的警告,但请小心使用,因为有时警告可能包含有用的信息。
warnings.filterwarnings('ignore')
探索性数据分析(EDA)
在数据准备阶段,主要读取训练数据和测试数据,并进行基本的数据展示。
train = pd.read_csv('./data/train.csv')
test = pd.read_csv('./data/test.csv')
下面进行简单的可视化分析,帮助我们对数据有个简单的了解。
** 不同type类型对应target的柱状图**
import matplotlib.pyplot as plt
# 不同type类型对应target的柱状图
type_target_df = train.groupby('type')['target'].mean().reset_index()
plt.figure(figsize=(8, 4))
plt.bar(type_target_df['type'], type_target_df['target'], color=['blue', 'green'])
plt.xlabel('Type')
plt.ylabel('Average Target Value')
plt.title('Bar Chart of Target by Type')
plt.show()
** id为00037f39cf的按dt为序列关于target的折线图**
specific_id_df = train[train['id'] == '00037f39cf']
plt.figure(figsize=(10, 5))
plt.plot(specific_id_df['dt'], specific_id_df['target'], marker='o', linestyle='-')
plt.xlabel('DateTime')
plt.ylabel('Target Value')
plt.title("Line Chart of Target for ID '00037f39cf'")
plt.show()
特征工程
这里主要构建了 **历史平移特征**和 **窗口统计特征**;每种特征都是有理可据的,具体说明如下:
** 历史平移特征:**通过历史平移获取上个阶段的信息;如下图所示,可以将d-1时间的信息给到d时间,d时间信息给到d+1时间,这样就实现了平移一个单位的特征构建。
** 窗口统计特征:**窗口统计可以构建不同的窗口大小,然后基于窗口范围进统计均值、最大值、最小值、中位数、方差的信息,可以反映最近阶段数据的变化情况。如下图所示,可以将d时刻之前的三个时间单位的信息进行统计构建特征给我d时刻。
完整代码如下:
# 合并训练数据和测试数据,并进行排序
data = pd.concat([test, train], axis=0, ignore_index=True)
data = data.sort_values(['id','dt'], ascending=False).reset_index(drop=True)
# 历史平移
for i in range(10,30):
data[f'last{i}_target'] = data.groupby(['id'])['target'].shift(i)
# 窗口统计
data[f'win3_mean_target'] = (data['last10_target'] + data['last11_target'] + data['last12_target']) / 3
# 进行数据切分
train = data[data.target.notnull()].reset_index(drop=True)
test = data[data.target.isnull()].reset_index(drop=True)
# 确定输入特征
train_cols = [f for f in data.columns if f not in ['id','target']]
合并训练数据和测试数据,并进行排序
data = pd.concat([test, train], axis=0, ignore_index=True) data = data.sort_values(['id','dt'], ascending=False).reset_index(drop=True)
pd.concat([test, train], axis=0, ignore_index=True)
:使用pandas
的concat
函数将test
和train
两个DataFrame沿着行方向(axis=0
)合并成一个新的DataFramedata
。ignore_index=True
表示合并后的DataFrame将不使用原索引,而是重新生成一个新的索引。data.sort_values(['id','dt'], ascending=False)
:对合并后的DataFramedata
按照id
和dt
两列进行降序排序。这里假设id
是用户或实例的唯一标识符,而dt
是时间戳或日期。降序排序可能是为了确保数据按时间从新到旧排列,这在进行时间序列分析时很常见。.reset_index(drop=True)
:重置索引,drop=True
表示不保留旧的索引作为列。历史平移(滞后特征)
for i in range(10,30): data[f'last{i}_target'] = data.groupby(['id'])['target'].shift(i)
- 这段代码通过循环创建了新的列,这些列是基于
target
列的滞后值。对于每个id
组,它计算target
列的第i
个滞后值(即向前看i
个时间单位之前的target
值),并将这些值存储在新的列last{i}_target
中。这种特征常用于时间序列预测,因为它们提供了关于过去目标值的信息。窗口统计
data[f'win3_mean_target'] = (data['last10_target'] + data['last11_target'] + data['last12_target']) / 3
- 这行代码计算了最近三个滞后目标值(
last10_target
、last11_target
、last12_target
)的平均值,并将这个平均值存储在新的列win3_mean_target
中。这种窗口统计特征提供了关于过去一段时间内目标值趋势的平滑表示。进行数据切分
train = data[data.target.notnull()].reset_index(drop=True) test = data[data.target.isnull()].reset_index(drop=True)
- 在合并和预处理了数据之后,代码再次将数据集切分为训练集
train
和测试集test
。这是通过检查target
列的值是否非空来实现的。如果target
列的值非空,则该行被分配到训练集;如果为空,则被分配到测试集。.reset_index(drop=True)
用于重置索引,确保训练集和测试集的索引是连续的,并且不保留旧的索引作为列。确定输入特征
train_cols = [f for f in data.columns if f not in ['id','target']]
- 最后,这行代码生成了一个包含所有非
id
和target
列的列表train_cols
。这些列将作为模型的输入特征。注意,这里使用的是data
的列名,但在实际应用中,应该使用train
的列名来确保只包含训练集中实际存在的特征。不过,由于train
和data
在切分之前共享相同的列,所以这里使用data.columns
也是可以的。不过,为了代码的清晰和准确性,更好的做法是使用train.columns
。
- 模型训练与测试集预测
这里选择使用Lightgbm模型,也是通常作为数据挖掘比赛的基线模型,在不需要过程调参的情况的也能得到比较稳定的分数。
另外需要注意的训练集和验证集的构建:因为数据存在时序关系,所以需要严格按照时序进行切分,
- 这里选择原始给出训练数据集中dt为30之后的数据作为训练数据,之前的数据作为验证数据,
- 这样保证了数据不存在穿越问题(不使用未来数据预测历史数据)。
# 定义一个函数,用于训练LightGBM模型并返回验证集和测试集的预测结果def time_model(lgb, train_df, test_df, cols): # 根据日期dt将训练集分为训练集和验证集 # 假设dt大于等于31的作为训练集,小于等于30的作为验证集 trn_x, trn_y = train_df[train_df.dt>=31][cols], train_df[train_df.dt>=31]['target'] val_x, val_y = train_df[train_df.dt<=30][cols], train_df[train_df.dt<=30]['target'] # 构建LightGBM的训练和验证数据格式 train_matrix = lgb.Dataset(trn_x, label=trn_y) valid_matrix = lgb.Dataset(val_x, label=val_y) # 设置LightGBM的参数 lgb_params = { 'boosting_type': 'gbdt', # 使用梯度提升决策树 'objective': 'regression', # 目标函数为回归 'metric': 'mse', # 评估指标为均方误差 'device_type' : 'gpu', # 使用GPU进行训练 'min_child_weight': 5, # 叶子节点上最小样本权重和 'num_leaves': 2 ** 5, # 叶子节点数 'lambda_l2': 10, # L2正则化项 'feature_fraction': 0.8, # 随机选择80%的特征进行训练 'bagging_fraction': 0.8, # 随机选择80%的数据进行训练 'bagging_freq': 4, # 每4次迭代进行一次bagging 'learning_rate': 0.05, # 学习率 'seed': 2024, # 随机数种子 'nthread' : 16, # 线程数 'verbose' : -1, # 不输出训练过程中的信息 } # 导入LightGBM的回调函数 from lightgbm.callback import log_evaluation # 训练模型,使用early_stopping和log_evaluation作为回调函数 model = lgb.train(lgb_params, train_matrix, 50000, valid_sets=[train_matrix, valid_matrix], categorical_feature=[], callbacks=[lgb.early_stopping(500), lgb.log_evaluation(500)]) # 使用训练好的模型对验证集和测试集进行预测 val_pred = model.predict(val_x, num_iteration=model.best_iteration) test_pred = model.predict(test_df[cols], num_iteration=model.best_iteration) # 返回验证集和测试集的预测结果 return val_pred, test_pred# 调用time_model函数,传入LightGBM的实例、训练集、测试集和特征列lgb_oof, lgb_test = time_model(lgb, train, test, train_cols)# 将测试集的预测结果保存到DataFrame的'target'列test['target'] = lgb_test# 选择'id'、'dt'和'target'列,保存到CSV文件test[['id','dt','target']].to_csv('submit.csv', index=None)
> 添加注释增强理解LightGBM的参数。
思考体验
进阶代码使用Lightgbm完成了基本的模型训练,并且添加了时序问题中常见的特征提取方式,
通过特征工程挖掘特征可以很快的提升模型预测效果,对LightGBM有了更深的理解。
Task 3:尝试使用深度学习方案
时间序列预测特征提取和分析方法
在进行时间序列分析时,特征提取是一个至关重要的步骤,因为它直接影响到模型的性能。以下是关键特征提取和分析方式的详细介绍:
- 日期变量:时间序列数据通常包含日期或时间信息。这可以细分为不同的时间尺度,如年、月、周、日、小时、分钟等。在特征提取时,可以将这些日期变量转换为数值型特征,以便于模型处理。
- 周期性:许多时间序列数据表现出周期性,例如,一天中的小时数、一周中的天数、一年中的月份等。识别并利用这些周期性特征可以帮助模型捕捉数据的内在规律。
- 趋势性:趋势性是指时间序列数据随时间推移呈现的上升或下降的总体模式。这可以通过诸如移动平均或线性回归等方法来提取,并作为特征输入模型。
- 距离某天的时间差:这涉及到从特定日期(如产品发布日、重要事件日等)计算时间差。这种特征可以帮助模型了解数据点与特定事件的相对位置。
- 时间特征组合:将不同的时间单位组合起来(如年和周、月和日)可以提供更丰富的时间上下文信息,有助于揭示数据中的复杂模式。
- 特殊日期:识别时间序列中的特殊日期或事件(如节假日、促销活动等)并将其作为特征,可以帮助模型解释与这些事件相关的数据波动。
- 异常点:时间序列中可能存在异常点,这些点与其他数据点显著不同。正确识别并处理这些异常点对于提高预测精度至关重要。
- 时序相关特征:- 历史平移:将过去的值作为当前值的函数,例如,使用前一天的值来预测后一天的值。- 滑窗统计:使用时间窗口内的统计数据(如平均值、中位数、标准差等)作为特征,这有助于捕捉局部时间范围内的数据特性。
- 强相关特征:识别与目标变量强烈相关的特征,并利用这些特征来构建预测模型。
补充:
这张图片提供了一系列关于时间序列预测中构建关键特征的要点,包括强相关性特征、趋势性特征、周期性特征和异常点特征。以下是对这些特征的详细介绍:
- 强相关性特征: 强相关性特征是指与目标变量有明显线性或非线性关系的输入特征。在时间序列中,这些特征可能包括: 1. 滚动统计特征:计算时间序列的滚动窗口内的统计量,如平均值、最大值、最小值、总和等。2. 滞后特征:使用过去的值作为当前预测的特征,例如,使用前一天的销售数据来预测后一天的销售。
- 趋势性特征: 趋势性特征反映了时间序列随时间推移的长期变化方向或速率。构建趋势性特征的方法包括: 1. 多项式拟合:拟合一个多项式模型来捕捉趋势。2. 移动平均:使用时间序列的移动平均值来平滑短期波动,突出长期趋势。3. 时间戳转换:将时间戳转换为数值,如从时间戳中提取年份、月份、星期等。
- 周期性特征: 周期性特征反映了时间序列数据在固定时间间隔内重复出现的模式。构建周期性特征的方法包括: 1. 周期性函数:使用正弦和余弦函数来模拟周期性变化。2. 季节性分解:使用季节性分解方法来识别和提取时间序列的季节性成分。3. 时间戳的周期性转换:将时间戳转换为周期性变量,如一周中的星期几、一月中的日子等。
- 异常点特征: 异常点是指与正常数据模式显著不同的数据点,它们可能由错误、突变或特殊事件引起。处理异常点的方法包括: 1. 修正:更正异常点,如果它们是由于可识别的错误造成的。2. 剔除:从数据集中删除异常点,特别是在它们可能影响模型训练的情况下。3. 简单标注:在数据集中标记异常点,以便在分析时考虑。
- 特殊事件特征: 特殊事件如“双十一”、“618”、“春节”等,会在时间序列中产生显著的峰值。构建特殊事件特征的方法包括: 1. 事件前后的时间窗口:考虑事件前后的时间窗口,以捕捉事件的影响。2. 事件指示器:创建一个二进制特征,当时间序列中的点与特殊事件对应时,该特征值为1,否则为0。
- 上下时段信息: 上下时段信息指的是在时间序列中,特定时间段之前或之后的数据点。这可以通过创建特征来表示数据点与特定时间段的距离来实现。
- 存在峰值与峰值距离: 识别时间序列中的峰值点,并计算其他数据点与这些峰值点的距离,可以作为特征输入模型。
- 时间尺度特征: 根据需要预测的时间尺度(如1天、3天、5天等),创建相应的滞后特征和滚动统计特征。
通过结合这些特征,可以构建一个强大的时间序列预测模型。在实际应用中,需要根据具体问题和数据特性来选择和调整特征工程的策略。此外,特征的选择和构建应该与模型的选择和调优过程相结合,以实现最佳的预测性能。
优化方案详解
在原有代码基础上进行优化,一般优化思路,从特征工程与模型中来思考。
优化方法建议:
- 提取更多特征:在数据挖掘比赛中,特征总是最终制胜法宝,去思考什么信息可以帮助我们提高预测精准度,然后将其转化为特征输入到模型。
- 尝试不同的模型:****模型间存在很大的差异,预测结果也会不一样,比赛的过程就是不断的实验和试错的过程,通过不断的实验寻找最佳模型,同时帮助自身加强模型的理解能力。
特征优化
这里主要构建了历史平移特征、差分特征、和窗口统计特征;每种特征都是有理可据的,具体说明如下:
- 历史平移特征:通过历史平移获取上个阶段的信息(有助于捕捉数据在不同时间点的相互依赖关系);
- 差分特征:可以帮助获取相邻阶段的增长差异,描述数据的涨减变化情况。在此基础上还可以构建相邻数据比值变化、二阶差分等;
- 窗口统计特征:窗口统计可以构建不同的窗口大小,然后基于窗口范围进统计均值、最大值、最小值、中位数、方差的信息,可以反映最近阶段数据的变化情况。
历史平移特征(捕捉时间依赖性、增强模型预测能力)
- 在实际操作中,可以根据业务需求和数据特性选择合适的时间步长进行平移。例如,在本次电力需求预测中,可以将前一周、前两周或前一个月的电力需求数据作为历史平移特征。
- 需要注意的是,历史平移特征可能会导致数据之间的重叠和冗余,因此在构建特征时需要谨慎选择时间步长,并考虑使用特征选择或降维技术来减少冗余。
差分特征(揭示数据变化趋势、提高模型稳定性)
- 在实际操作中,可以根据业务需求和数据特性选择合适的时间间隔进行差分计算。例如,在股票价格预测中,可以计算日收益率(即相邻两天收盘价的差值)作为差分特征。
- 需要注意的是,差分特征可能会放大数据中的噪声和异常值,因此在构建特征时需要进行适当的平滑处理或异常值检测。
窗口统计特征(反映数据分布情况、增强模型鲁棒性)
- 在实际操作中,需要根据业务需求和数据特性选择合适的窗口大小和统计函数。例如,在交通流量预测中,可以使用不同大小的滑动窗口来计算交通流量的均值、最大值和方差等统计特征。
- 需要注意的是,窗口统计特征可能会导致数据之间的重叠和冗余,因此在构建特征时需要谨慎选择窗口大小和统计函数,并考虑使用特征选择或降维技术来减少冗余。
代码如下:
# 合并训练数据和测试数据
# 使用pandas的concat函数将训练集train和测试集test在垂直方向(axis=0)上合并,并重置索引
data = pd.concat([train, test], axis=0).reset_index(drop=True)
# 对合并后的数据按照'id'和'dt'列进行降序排序,并重置索引
# 这是为了确保数据在id内部是按照时间顺序由远及近排列的,尽管'dt'可能已经是排序的
data = data.sort_values(['id','dt'], ascending=False).reset_index(drop=True)
# 历史平移
# 为每个id组内的'target'列生成一系列的历史平移特征
# shift(i)表示将'target'列中的每个值向上移动i个位置,形成新的列
for i in range(10,36):
data[f'target_shift{i}'] = data.groupby('id')['target'].shift(i)
# 历史平移 + 差分特征
# 对历史平移后的第一个特征(target_shift10)进行差分操作
# diff(i)表示计算与前i个值的差异
for i in range(1,4):
data[f'target_shift10_diff{i}'] = data.groupby('id')['target_shift10'].diff(i)
# 窗口统计
# 对'target'列进行窗口统计,包括均值、最大值、最小值和标准差
# rolling(window=win, ...)用于生成滑动窗口,min_periods=3表示窗口内至少需要3个非NA值
for win in [15,30,50,70]:
data[f'target_win{win}_mean'] = data.groupby('id')['target'].rolling(window=win, min_periods=3, closed='left').mean().values
data[f'target_win{win}_max'] = data.groupby('id')['target'].rolling(window=win, min_periods=3, closed='left').max().values
data[f'target_win{win}_min'] = data.groupby('id')['target'].rolling(window=win, min_periods=3, closed='left').min().values
data[f'target_win{win}_std'] = data.groupby('id')['target'].rolling(window=win, min_periods=3, closed='left').std().values
# 历史平移 + 窗口统计
# 对历史平移后的target_shift10特征进行窗口统计
for win in [7,14,28,35,50,70]:
data[f'target_shift10_win{win}_mean'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').mean().values
data[f'target_shift10_win{win}_max'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').max().values
data[f'target_shift10_win{win}_min'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').min().values
data[f'target_shift10_win{win}_sum'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').sum().values
data[f'target_shift10_win{win}_std'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').std().values
for win in [15,30,50,70]: data[f'target_win{win}_mean'] = data.groupby('id')['target'].rolling(window=win, min_periods=3, closed='left').mean().values data[f'target_win{win}_max'] = data.groupby('id')['target'].rolling(window=win, min_periods=3, closed='left').max().values data[f'target_win{win}_min'] = data.groupby('id')['target'].rolling(window=win, min_periods=3, closed='left').min().values data[f'target_win{win}_std'] = data.groupby('id')['target'].rolling(window=win, min_periods=3, closed='left').std().values
这段代码遍历了一个窗口大小列表
[15, 30, 50, 70]
,并对
data
DataFrame中的
'target'
列执行了以下操作:
- 对于每个
id
组,计算'target'
列的滚动窗口平均值、最大值、最小值和标准差。- 滚动窗口的大小由
win
变量指定,min_periods=3
表示在计算统计量之前,窗口中至少需要有3个观测值。closed='left'
参数指定窗口是左闭的,即窗口包括索引的当前值。- 计算的统计量(平均值、最大值、最小值和标准差)被赋值给
data
DataFrame的新列,列名由前缀'target_win'
、窗口大小win
和统计量类型('mean'
、'max'
、'min'
、'std'
)组成。- 注意,这里使用了
.values
属性来获取计算结果的NumPy数组,并将其直接赋值给data
DataFrame的新列。这通常不是处理Pandas Series以扩展DataFrame的推荐方式,因为它可能会丢失索引信息,但在这种情况下,由于groupby
和rolling
操作已经按id
分组并处理了索引,所以这样做是可行的。for win in [7,14,28,35,50,70]: data[f'target_shift10_win{win}_mean'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').mean().values data[f'target_shift10_win{win}_max'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').max().values data[f'target_shift10_win{win}_min'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').min().values data[f'target_shift10_win{win}_sum'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').sum().values data[f'target_shift10_win{win}_std'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').std().values
这段代码与第一段代码非常相似,但它操作的是
data
DataFrame中的
'target_shift10'
列,而不是
'target'
列。它计算了滚动窗口的平均值、最大值、最小值和总和(注意这里使用了
.sum()
而不是
.std()
来计算总和),并将结果存储在新的列中。列名的前缀是
'target_shift10_win'
,以区分它们是对
'target_shift10'
列的操作结果。
结合Task 2 LightGBM改进后的代码:
import os
import numpy as np
import pandas as pd
import lightgbm as lgb
from sklearn.metrics import mean_squared_log_error, mean_absolute_error, mean_squared_error
import tqdm
import sys
import os
import gc
import argparse
import warnings
os.environ['KMP DUPLICATE LIB OK']='True'
warnings.filterwarnings('ignore')
train = pd.read_csv('./data/data283931/train.csv')
test = pd.read_csv('./data/data283931/test.csv')
# 合并训练数据和测试数据
data = pd.concat([train, test], axis=0).reset_index(drop=True)
data = data.sort_values(['id','dt'], ascending=False).reset_index(drop=True)
# 历史平移
for i in range(10,36):
data[f'target_shift{i}'] = data.groupby('id')['target'].shift(i)
# 历史平移 + 差分特征
for i in range(1,4):
data[f'target_shift10_diff{i}'] = data.groupby('id')['target_shift10'].diff(i)
# 窗口统计
for win in [15,30,50,70]:
data[f'target_win{win}_mean'] = data.groupby('id')['target'].rolling(window=win, min_periods=3, closed='left').mean().values
data[f'target_win{win}_max'] = data.groupby('id')['target'].rolling(window=win, min_periods=3, closed='left').max().values
data[f'target_win{win}_min'] = data.groupby('id')['target'].rolling(window=win, min_periods=3, closed='left').min().values
data[f'target_win{win}_std'] = data.groupby('id')['target'].rolling(window=win, min_periods=3, closed='left').std().values
# 历史平移 + 窗口统计
for win in [7,14,28,35,50,70]:
data[f'target_shift10_win{win}_mean'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').mean().values
data[f'target_shift10_win{win}_max'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').max().values
data[f'target_shift10_win{win}_min'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').min().values
data[f'target_shift10_win{win}_sum'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').sum().values
data[f'target_shift10_win{win}_std'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').std().values
# 进行数据切分
train = data[data.target.notnull()].reset_index(drop=True)
test = data[data.target.isnull()].reset_index(drop=True)
# 确定输入特征
train_cols = [f for f in data.columns if f not in ['id','target']]
def time_model(lgb, train_df, test_df, cols):
# 训练集和验证集切分
trn_x, trn_y = train_df[train_df.dt>=31][cols], train_df[train_df.dt>=31]['target']
val_x, val_y = train_df[train_df.dt<=30][cols], train_df[train_df.dt<=30]['target']
# 构建模型输入数据
train_matrix = lgb.Dataset(trn_x, label=trn_y)
valid_matrix = lgb.Dataset(val_x, label=val_y)
# lightgbm参数
lgb_params = {
'boosting_type': 'gbdt',
'device_type' : 'gpu',
'objective': 'regression',
'metric': 'mse',
'min_child_weight': 5,
'num_leaves': 2 ** 5,
'lambda_l2': 10,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'bagging_freq': 4,
'learning_rate': 0.05,
'seed': 2024,
'nthread' : 16,
'verbose' : -1,
}
# 训练模型
from lightgbm.callback import log_evaluation
model = lgb.train(lgb_params, train_matrix, 50000,
valid_sets=[train_matrix, valid_matrix], categorical_feature=[], callbacks=[lgb.early_stopping(500), lgb.log_evaluation(500)])
# 验证集和测试集结果预测
val_pred = model.predict(val_x, num_iteration=model.best_iteration)
test_pred = model.predict(test_df[cols], num_iteration=model.best_iteration)
return val_pred, test_pred
lgb_oof, lgb_test = time_model(lgb, train, test, train_cols)
# 保存结果文件到本地
test['target'] = lgb_test
test[['id','dt','target']].to_csv('submit.csv', index=None)
【稍有改进,但不多】
模型融合
加权平均融合
进行模型融合的前提是有多个模型的输出结果,比如使用catboost、xgboost和lightgbm三个模型分别输出三个结果,这时就可以将三个结果进行融合,最常见的是将结果直接进行加权平均融合。
下面我们构建了cv_model函数,内部可以选择使用lightgbm、xgboost和catboost模型,可以依次跑完这三个模型,然后将三个模型的结果进行取平均进行融合。对于每个模型均选择经典的K折交叉验证方法进行离线评估,大体流程如下:
- K折交叉验证会把样本数据随机的分成K份;
- 每次随机的选择K-1份作为训练集,剩下的1份做验证集;
- 当这一轮完成后,重新随机选择K-1份来训练数据;
- 最后将K折预测结果取平均作为最终提交结果。
参考代码如下:
# 导入所需的库
from sklearn.model_selection import StratifiedKFold, KFold, GroupKFold # 导入交叉验证的折叠策略
import lightgbm as lgb # 导入LightGBM库
import xgboost as xgb # 导入XGBoost库
from catboost import CatBoostRegressor # 导入CatBoost的回归模型
from sklearn.metrics import mean_squared_error, mean_absolute_error # 导入评估指标
# 定义一个函数cv_model,用于交叉验证模型
def cv_model(clf, train_x, train_y, test_x, clf_name, seed = 2024):
'''
clf:调用模型
train_x:训练数据
train_y:训练数据对应标签
test_x:测试数据
clf_name:选择使用模型名
seed:随机种子
'''
# 设置交叉验证的折数
folds = 5
# 使用KFold进行数据划分,打乱数据,设置随机种子
kf = KFold(n_splits=folds, shuffle=True, random_state=seed)
# 初始化训练集的预测值和测试集的预测值
oof = np.zeros(train_x.shape[0])
test_predict = np.zeros(test_x.shape[0])
# 初始化交叉验证分数列表
cv_scores = []
# 遍历每个折
for i, (train_index, valid_index) in enumerate(kf.split(train_x, train_y)):
# 打印当前折的编号
print('************************************ {} ************************************'.format(str(i+1)))
# 根据索引划分训练集和验证集
trn_x, trn_y, val_x, val_y = train_x.iloc[train_index], train_y[train_index], train_x.iloc[valid_index], train_y[valid_index]
# 根据模型名称选择不同的模型参数和训练方式
if clf_name == "lgb":
train_matrix = clf.Dataset(trn_x, label=trn_y)
valid_matrix = clf.Dataset(val_x, label=val_y)
# 设置LightGBM的参数
params = {
'boosting_type': 'gbdt',
'objective': 'regression',
'metric': 'mae',
'min_child_weight': 6,
'num_leaves': 2 ** 6,
'lambda_l2': 10,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'bagging_freq': 4,
'learning_rate': 0.1,
'seed': 2023,
'nthread' : 16,
'verbose' : -1,
}
# 训练模型
model = clf.train(params, train_matrix, 1000, valid_sets=[train_matrix, valid_matrix],
categorical_feature=[], verbose_eval=200, early_stopping_rounds=100)
# 预测验证集和测试集
val_pred = model.predict(val_x, num_iteration=model.best_iteration)
test_pred = model.predict(test_x, num_iteration=model.best_iteration)
if clf_name == "xgb":
xgb_params = {
'booster': 'gbtree',
'objective': 'reg:squarederror',
'eval_metric': 'mae',
'max_depth': 5,
'lambda': 10,
'subsample': 0.7,
'colsample_bytree': 0.7,
'colsample_bylevel': 0.7,
'eta': 0.1,
'tree_method': 'hist',
'seed': 520,
'nthread': 16
}
# 将数据转换为DMatrix格式
train_matrix = clf.DMatrix(trn_x , label=trn_y)
valid_matrix = clf.DMatrix(val_x , label=val_y)
test_matrix = clf.DMatrix(test_x)
# 设置训练和验证的监视列表
watchlist = [(train_matrix, 'train'),(valid_matrix, 'eval')]
# 训练模型
model = clf.train(xgb_params, train_matrix, num_boost_round=1000, evals=watchlist, verbose_eval=200, early_stopping_rounds=100)
# 预测验证集和测试集
val_pred = model.predict(valid_matrix)
test_pred = model.predict(test_matrix)
if clf_name == "cat":
params = {'learning_rate': 0.1, 'depth': 5, 'bootstrap_type':'Bernoulli','random_seed':2023,
'od_type': 'Iter', 'od_wait': 100, 'random_seed': 11, 'allow_writing_files': False}
# 初始化CatBoost模型
model = clf(iterations=1000, **params)
# 训练模型
model.fit(trn_x, trn_y, eval_set=(val_x, val_y),
metric_period=200,
use_best_model=True,
cat_features=[],
verbose=1)
# 预测验证集和测试集
val_pred = model.predict(val_x)
test_pred = model.predict(test_x)
# 将验证集的预测值存储在oof中,并计算测试集的预测值的平均值
oof[valid_index] = val_pred
test_predict += test_pred / kf.n_splits
# 计算验证集的MAE分数并添加到cv_scores列表
score = mean_absolute_error(val_y, val_pred)
cv_scores.append(score)
# 打印交叉验证分数
print(cv_scores)
# 返回训练集的预测值和测试集的预测值
return oof, test_predict
# 选择lightgbm模型
lgb_oof, lgb_test = cv_model(lgb, train[train_cols], train['target'], test[train_cols], 'lgb')
# 选择xgboost模型
xgb_oof, xgb_test = cv_model(xgb, train[train_cols], train['target'], test[train_cols], 'xgb')
# 选择catboost模型
cat_oof, cat_test = cv_model(CatBoostRegressor, train[train_cols], train['target'], test[train_cols], 'cat')
# 进行取平均融合
final_test = (lgb_test + xgb_test + cat_test) / 3
补充
xgboost 详解:XGBoost详解(原理篇)-CSDN博客
catbooost 详解:深度探索:机器学习CatBoost算法原理及其应用-CSDN博客
stacking融合
另外一种就是stacking融合,stacking是一种分层模型集成框架。以两层为例,第一层由多个基学习器组成,其输入为原始训练集,第二层的模型则是以第一层基学习器的输出作为特征加入训练集进行再训练,从而得到完整的stacking模型。
第一层:(类比cv_model函数)
- 划分训练数据为K折(5折为例,每次选择其中四份作为训练集,一份作为验证集);
- 针对各个模型RF、ET、GBDT、XGB,分别进行5次训练,每次训练保留一份样本用作训练时的验证,训练完成后分别对Validation set,Test set进行预测,对于Test set一个模型会对应5个预测结果,将这5个结果取平均;对于Validation set一个模型经过5次交叉验证后,所有验证集数据都含有一个标签。此步骤结束后:5个验证集(总数相当于训练集全部)在每个模型下分别有一个预测标签,每行数据共有4个标签(4个算法模型),测试集每行数据也拥有四个标签(4个模型分别预测得到的)
第二层:(类比stack_model函数)
将训练集中的四个标签外加真实标签当作**五列新的特征作为新的训练集**,选取一个训练模型,根据新的训练集进行训练,然后应用**测试集的四个标签组成的测试集**进行预测作为最终的result。
参考代码如下:
def stack_model(oof_1, oof_2, oof_3, predictions_1, predictions_2, predictions_3, y):
'''
输入的oof_1, oof_2, oof_3可以对应lgb_oof,xgb_oof,cat_oof
这些是交叉验证过程中得到的每个模型在训练集上的预测值。
predictions_1, predictions_2, predictions_3对应lgb_test,xgb_test,cat_test
这些是每个模型在测试集上的预测值。
'''
# 将三个模型在训练集上的预测值合并为一个新的DataFrame
train_stack = pd.concat([oof_1, oof_2, oof_3], axis=1)
# 将三个模型在测试集上的预测值合并为一个新的DataFrame
test_stack = pd.concat([predictions_1, predictions_2, predictions_3], axis=1)
# 初始化训练集堆叠的预测值数组和测试集堆叠的预测值数组
oof = np.zeros((train_stack.shape[0],))
predictions = np.zeros((test_stack.shape[0],))
# 初始化交叉验证分数列表
scores = []
# 从sklearn.model_selection导入RepeatedKFold
from sklearn.model_selection import RepeatedKFold
# 设置交叉验证的折数和重复次数,并设置随机种子
folds = RepeatedKFold(n_splits=5, n_repeats=2, random_state=2021)
# 遍历每个交叉验证折
for fold_, (trn_idx, val_idx) in enumerate(folds.split(train_stack, train_stack)):
print("fold n°{}".format(fold_+1))
# 根据索引划分训练集和验证集
trn_data, trn_y = train_stack.loc[trn_idx], y[trn_idx]
val_data, val_y = train_stack.loc[val_idx], y[val_idx]
# 从sklearn.linear_model导入Ridge
from sklearn.linear_model import Ridge
# 初始化Ridge回归模型
clf = Ridge(random_state=2021)
# 训练模型
clf.fit(trn_data, trn_y)
# 预测验证集的堆叠预测值
oof[val_idx] = clf.predict(val_data)
# 预测测试集的堆叠预测值,并累加
predictions += clf.predict(test_stack) / (5 * 2)
# 计算当前折的MAE分数,并添加到scores列表
score_single = mean_absolute_error(val_y, oof[val_idx])
scores.append(score_single)
print(f'{fold_+1}/{5}', score_single)
# 打印所有折的平均MAE分数
print('mean: ',np.mean(scores))
# 返回训练集和测试集的堆叠预测值
return oof, predictions
# 调用stack_model函数,传入训练集和测试集的预测值,以及训练集的目标值
stack_oof, stack_pred = stack_model(pd.DataFrame(lgb_oof), pd.DataFrame(xgb_oof), pd.DataFrame(cat_oof),
pd.DataFrame(lgb_test), pd.DataFrame(xgb_test), pd.DataFrame(cat_test), train['target'])
深度学习方案尝试(使用Keras库构建和训练LSTM(长短期记忆网络)模型)
先附上参考代码:
import numpy as np
import pandas as pd
from sklearn.preprocessing import MinMaxScaler
from keras.models import Sequential
from keras.layers import LSTM, Dense, RepeatVector, TimeDistributed
from keras.optimizers import Adam
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
# 数据预处理
def preprocess_data(df, look_back=100):
# 将数据按照id进行分组
grouped = df.groupby('id')
datasets = {}
for id, group in grouped:
datasets[id] = group.values
# 准备训练数据集
X, Y = [], []
for id, data in datasets.items():
for i in range(10, 15): # 每个id构建5个序列
a = data[i:(i + look_back), 3]
a = np.append(a, np.array([0]*(100-len(a))))
X.append(a[::-1])
Y.append(data[i-10:i, 3][::-1])
# 准备测试数据集
OOT = []
for id, data in datasets.items():
a = data[:100, 3]
a = np.append(a, np.array([0]*(100-len(a))))
OOT.append(a[::-1])
return np.array(X, dtype=np.float64), np.array(Y, dtype=np.float64), np.array(OOT, dtype=np.float64)
# 定义模型
def build_model(look_back, n_features, n_output):
model = Sequential()
model.add(LSTM(50, input_shape=(look_back, n_features)))
model.add(RepeatVector(n_output))
model.add(LSTM(50, return_sequences=True))
model.add(TimeDistributed(Dense(1)))
model.compile(loss='mean_squared_error', optimizer=Adam(0.001))
return model
# 构建和训练模型
look_back = 100 # 序列长度
n_features = 1 # 假设每个时间点只有一个特征
n_output = 10 # 预测未来10个时间单位的值
# 预处理数据
X, Y, OOT = preprocess_data(train, look_back=look_back)
# 构建模型
model = build_model(look_back, n_features, n_output)
# 训练模型
model.fit(X, Y, epochs=10, batch_size=64, verbose=1)
# 进行预测
predicted_values = model.predict(OOT)
Test = predicted_values.flatten()
test['target'] = Test
test[['id','dt','target']].to_csv('submit.csv', index=None)
以下是代码的详细解释:
导入库:代码开始处导入了所需的库,包括数据处理库NumPy和Pandas,以及用于构建LSTM模型的Keras库。
import numpy as np # 导入numpy库,用于数学运算import pandas as pd # 导入pandas库,用于数据处理from sklearn.preprocessing import MinMaxScaler # 导入MinMaxScaler,用于数据标准化from keras.models import Sequential # 导入Sequential模型,用于构建模型from keras.layers import LSTM, Dense, RepeatVector, TimeDistributed # 导入LSTM、Dense、RepeatVector和TimeDistributed层from keras.optimizers import Adam # 导入Adam优化器
读取数据:使用Pandas的
read_csv
函数读取训练数据集train.csv
和测试数据集test.csv
。train = pd.read_csv('train.csv') # 读取训练数据集test = pd.read_csv('test.csv') # 读取测试数据集
定义数据预处理函数:1.
preprocess_data
函数用于准备训练和测试数据。2. 数据按id
列进行分组。3. 对于每个id
,提取特定列(索引为3的列)的数据作为时间序列。4. 创建多个序列,每个序列长度为look_back
,默认为100。5. 序列被反转,并且如果序列长度不足100,则用0填充。6. 训练数据集的标签Y
是序列的一部分,同样进行反转。7. 测试数据集(OOT,Out-Of-Time)也被反转并用0填充以满足序列长度要求。def preprocess_data(df, look_back=100): # 将数据按照id进行分组 grouped = df.groupby('id') datasets = {} for id, group in grouped: datasets[id] = group.values # 将每个id对应的数据存储在datasets字典中 # 准备训练数据集 X, Y = [], [] for id, data in datasets.items(): for i in range(10, 15): # 每个id构建5个序列 a = data[i:(i + look_back), 3] # 从第i个时间点开始,取look_back长度的序列 a = np.append(a, np.array([0]*(100-len(a)))) # 如果序列长度不足look_back,则用0填充 X.append(a[::-1]) # 将序列逆序 Y.append(data[i-10:i, 3][::-1]) # 取目标值序列,并逆序 # 准备测试数据集 OOT = [] for id, data in datasets.items(): a = data[:100, 3] # 取前100个时间点的序列 a = np.append(a, np.array([0]*(100-len(a)))) # 如果序列长度不足100,则用0填充 OOT.append(a[::-1]) # 将序列逆序 return np.array(X, dtype=np.float64), np.array(Y, dtype=np.float64), np.array(OOT, dtype=np.float64) # 返回训练数据集和测试数据集
定义模型构建函数:1.
build_model
函数用于构建LSTM模型。2. 模型包括一个LSTM层,用于学习时间序列数据的特征,以及一个重复向量层RepeatVector
,用于复制上一个LSTM层的输出以供下一个LSTM层使用。3. 再次使用LSTM层和时间分布的密集层来预测序列的下一个值。4. 模型使用均方误差作为损失函数,并使用Adam优化器。def build_model(look_back, n_features, n_output): model = Sequential() # 初始化Sequential模型 model.add(LSTM(50, input_shape=(look_back, n_features))) # 添加LSTM层,设置神经元数量为50,输入形状为(look_back, n_features) model.add(RepeatVector(n_output)) # 添加RepeatVector层,将LSTM层的输出重复n_output次 model.add(LSTM(50, return_sequences=True)) # 添加LSTM层,设置神经元数量为50,返回序列 model.add(TimeDistributed(Dense(1))) # 添加TimeDistributed层,将Dense层的输出应用到每个时间步 model.compile(loss='mean_squared_error', optimizer=Adam(0.001)) # 编译模型,设置损失函数为均方误差,优化器为Adam,学习率为0.001 return model
构建和训练模型:1. 设置序列长度
look_back
、特征数n_features
和输出时间单位数n_output
。2. 调用preprocess_data
函数预处理训练数据。3. 使用build_model
函数构建模型。4. 使用模型的fit
方法训练模型,指定迭代次数(epochs)和批量大小(batch_size)。# 构建和训练模型look_back = 100 # 序列长度n_features = 1 # 假设每个时间点只有一个特征n_output = 10 # 预测未来10个时间单位的值# 预处理数据X, Y, OOT = preprocess_data(train, look_back=look_back)# 构建模型model = build_model(look_back, n_features, n_output)# 训练模型model.fit(X, Y, epochs=10, batch_size=64, verbose=1) # 训练模型,设置训练轮数为10,批量大小为64,显示训练过
进行预测:使用训练好的模型对测试数据集进行预测。
predicted_values = model.predict(OOT) # 使用模型对测试数据集进行预测
改为提交格式:使用flatten展开。
Test = predicted_values.flatten()test['target'] = Testtest[['id','dt','target']].to_csv('submit.csv', index=None)
【分数不降反增?猜测过拟合了(汗)】
思考体验
Task 3难度有所增加,内容也更丰富,学习到更多的改进方式,受益匪浅。
总结
时间序列预测是一个不断发展的领域,随着技术的进步,我们可以期待更多的优化方法和模型的出现。深度学习模型,特别是LSTM和其变体,已经在许多时间序列预测任务中显示出了优越的性能。未来的研究可能会集中在以下几个方面:
- 更复杂的模型结构:如引入注意力机制的LSTM模型,以更好地捕捉时间序列中的长期依赖关系。
- 多模态数据融合:结合时间序列数据和其他类型的数据,如文本或图像,以提供更全面的分析。
- 模型解释性:提高模型的可解释性,以便更好地理解预测结果。
- 自动化特征工程:开发自动化的特征工程工具,以减少手动特征提取的工作量。
- 实时预测:提高模型在实时数据流上的预测能力。
- 模型鲁棒性:提高模型对异常值和噪声的鲁棒性。
随着技术的不断发展,我们可以期待时间序列预测在准确性、效率和应用范围上都会有显著的提升。
(最后修改:对代码漏洞修补,后期测试在线提交经常因服务器问题异常退出后改为本地测试 24.07.20)
版权归原作者 JeasonLi ——Wenrui 所有, 如有侵权,请联系我们删除。