赛事官网:challenge.xfyun.cn/h5/detail?t…
这次的赛事属于时间序列分析,也是回归分析的范畴.能够取得不错效果的话,常规思路一般为使用机器学习模型,如LightGBM、XGBoost,或者使用深度学习模型(神经网络等)进行实践,在模型的搭建上就比较复杂,需要自己构建模型结构,对于数值数据需要进行标准化处理.
常见的时间序列场景有:
- 金融领域:股票价格预测、利率变动、汇率预测等。
- 气象领域:温度、降水量、风速等气候指标的预测。
- 销售预测:产品或服务的未来销售额预测。
- 库存管理:预测库存需求,优化库存水平。
- 能源领域:电力需求预测、石油价格预测等。
- 医疗领域:疾病爆发趋势预测、医疗资源需求预测。
时间序列问题的数据往往有如下特点:
- 时间依赖性:数据点之间存在时间上的连续性和依赖性。
- 非平稳性:数据的统计特性(如均值、方差)随时间变化。
- 季节性:数据表现出周期性的模式,如年度、月度或周度。
- 趋势:数据随时间推移呈现长期上升或下降的趋势。
- 周期性:数据可能存在非固定周期的波动。
- 随 机波动:数据可能受到随机事件的影响,表现出不确定性。
LightGBM
LightGBM(Light Gradient Boosting Machine)是一个实现GBDT算法的框架,支持高效率的并行训练,并且具有更快的训练速度、更低的内存消耗、更好的准确率、支持分布式可以快速处理海量数据等优点。
LightGBM 框架中还包括随机森林和逻辑回归等模型。通常应用于二分类、多分类和排序等场景。
例如:在个性化商品推荐场景中,通常需要做点击预估模型。使用用户过往的行为(点击、曝光未点击、购买等)作为训练数据,来预测用户点击或购买的概率。根据用户行为和用户属性提取一些特征,包括:
- 类别特征(Categorical Feature):字符串类型,如性别(男/女)。
- 物品类型:服饰、玩具和电子等。
- 数值特征(Numrical Feature):整型或浮点型,如用户活跃度或商品价格等。
baseline 完整代码
(1)导入模块
此部分包含代码所需的模块
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')
(2)探索性数据分析(EDA)
在数据准备阶段,主要读取训练数据和测试数据,并进行基本的数据展示。
train = pd.read_csv('./data/train.csv')
test = pd.read_csv('./data/test.csv')
数据简单介绍:
- 其中id为房屋id,
- dt为日标识,训练数据dt最小为11,不同id对应序列长度不同;
- 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()
(3)特征工程
这里主要构建了 历史平移特征 和 窗口统计特征;每种特征都是有理可据的,具体说明如下:
- 历史平移特征: 通过历史平移获取上个阶段的信息;如下图所示,可以将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']]
(4)模型训练与测试集预测
这里选择使用Lightgbm模型,也是通常作为数据挖掘比赛的基线模型,在不需要过程调参的情况的也能得到比较稳定的分数。
另外需要注意的训练集和验证集的构建:因为数据存在时序关系,所以需要严格按照时序进行切分,
- 这里选择原始给出训练数据集中dt为30之后的数据作为训练数据,之前的数据作为验证数据,
- 这样保证了数据不存在穿越问题(不使用未来数据预测历史数据) 。
注意:在最新的lightgbm模型中,已经不支持直接定义早停参数(early_stopping_rounds).为了避免这个问题,可以使用回调函数来实现早停功能。
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',
'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,
}
# 使用回调函数实现早停
callbacks = [lgb.early_stopping(stopping_rounds=500)]
# 训练模型
model = lgb.train(lgb_params, train_matrix, num_boost_round=50000, valid_sets=[train_matrix, valid_matrix],
valid_names=['train', 'valid'], callbacks=callbacks)
# 验证集和测试集结果预测
val_pred = model.predict(val_x, num_iteration=model.best_iteration)
test_pred = model.predict(test_df[cols], num_iteration=model.best_iteration)
# 离线分数评估
score = mean_squared_error(val_pred, val_y)
print(f'Validation MSE: {score}')
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('D:/desktop/submit.csv', index=None)
运行完成后,生成结果文件"submit.csv",单元格输出如下
Training until validation scores don't improve for 500 rounds
Early stopping, best iteration is:
[956] train's l2: 171.445 valid's l2: 183.573
Validation MSE: 183.57327141293402`
提交"submit.csv"后,平台返回分数(预测结果以 mean square error (MSE) 作为评判标准,MSE 越小越好).
方案提升
在原有Baseline基础上做更多优化,一般优化思路,从特征工程与模型中来思考。
这里主要构建了历史平移特征、差分特征、和窗口统计特征,使用catboost、xgboost和lightgbm三个模型分别输出三个结果,这时就可以将三个结果进行融合,将结果直接进行加权平均融合。优化后的完整代码如下:
import pandas as pd
# 加载数据集
train = pd.read_csv('D:/desktop/dataset/train.csv')
test = pd.read_csv('D:/desktop/dataset/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_shift710win{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']]
import numpy as np
from sklearn.model_selection import StratifiedKFold, KFold, GroupKFold
import lightgbm as lgb
import xgboost as xgb
from catboost import CatBoostRegressor
from sklearn.metrics import mean_squared_error, mean_absolute_error
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
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)
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,
}
# 使用回调函数实现早停
callbacks = [lgb.early_stopping(stopping_rounds=100)]
model = clf.train(params, train_matrix, 1000, valid_sets=[train_matrix, valid_matrix],
categorical_feature=[], valid_names=['train', 'valid'], callbacks = callbacks)
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
}
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}
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[valid_index] = val_pred
test_predict += test_pred / kf.n_splits
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
# 保存结果文件到本地
test['target'] = final_test
test[['id', 'dt', 'target']].to_csv('D:/desktop/submit.csv', index=None)
提交后结果在236.7,相较于前面的baseline基础版本,有明显提升.
感悟
时间序列预测是一个不断发展的领域,随着技术的进步,我们可以期待更多的优化方法和模型的出现。纸上得来终觉浅,绝知此事要躬行.如何将学习到的方法应用到实践工程中,需要我们不断的尝试与改进, 没有一个模型是一步所得,持续努力、向最后的冠军冲击~
参考
datawhaler.feishu.cn/wiki/CuhBw9…