今天我们的任务是尝试构建深度学习模型来解决赛题。
你可以从中学会
-
如何进行时间序列的进阶特征提取与分析
-
如何构建深度学习方案
时间序列预测特征提取和分析方法
-
时间特征组合:将不同的时间单位组合起来(如年和周、月和日)可以提供更丰富的时间上下文信息,有助于揭示数据中的复杂模式。
-
特殊日期:识别时间序列中的特殊日期或事件(如节假日、促销活动等)并将其作为特征,可以帮助模型解释与这些事件相关的数据波动。
-
异常点:时间序列中可能存在异常点,这些点与其他数据点显著不同。正确识别并处理这些异常点对于提高预测精度至关重要。
-
时序相关特征:
-
历史平移:将过去的值作为当前值的函数,例如,使用前一天的值来预测后一天的值。
-
滑窗统计:使用时间窗口内的统计数据(如平均值、中位数、标准差等)作为特征,这有助于捕捉局部时间范围内的数据特性。
-
-
强相关特征:识别与目标变量强烈相关的特征,并利用这些特征来构建预测模型。
这张图片提供了一系列关于时间序列预测中构建关键特征的要点,包括强相关性特征、趋势性特征、周期性特征和异常点特征。以下是对这些特征的详细介绍:
-
强相关性特征:
-
强相关性特征是指与目标变量有明显线性或非线性关系的输入特征。在时间序列中,这些特征可能包括:
-
滞后特征:使用过去的值作为当前预测的特征,例如,使用前一天的销售数据来预测后一天的销售。
-
滚动统计特征:计算时间序列的滚动窗口内的统计量,如平均值、最大值、最小值、总和等。
-
-
-
趋势性特征:
-
趋势性特征反映了时间序列随时间推移的长期变化方向或速率。
-
构建趋势性特征的方法包括:
-
时间戳转换:将时间戳转换为数值,如从时间戳中提取年份、月份、星期等。
-
移动平均:使用时间序列的移动平均值来平滑短期波动,突出长期趋势。
-
多项式拟合:拟合一个多项式模型来捕捉趋势。
-
-
-
周期性特征:
-
周期性特征反映了时间序列数据在固定时间间隔内重复出现的模式。
-
构建周期性特征的方法包括:
-
时间戳的周期性转换:将时间戳转换为周期性变量,如一周中的星期几、一月中的日子等。
-
季节性分解:使用季节性分解方法来识别和提取时间序列的季节性成分。
-
周期性函数:使用正弦和余弦函数来模拟周期性变化。
-
-
-
异常点特征:
-
异常点是指与正常数据模式显著不同的数据点,它们可能由错误、突变或特殊事件引起。
-
处理异常点的方法包括:
-
简单标注:在数据集中标记异常点,以便在分析时考虑。
-
剔除:从数据集中删除异常点,特别是在它们可能影响模型训练的情况下。
-
修正:更正异常点,如果它们是由于可识别的错误造成的。
-
-
-
特殊事件特征:
-
特殊事件如“双十一”、“618”、“春节”等,会在时间序列中产生显著的峰值。
-
构建特殊事件特征的方法包括:
-
事件指示器:创建一个二进制特征,当时间序列中的点与特殊事件对应时,该特征值为1,否则为0。
-
事件前后的时间窗口:考虑事件前后的时间窗口,以捕捉事件的影响。
-
-
-
上下时段信息:
-
上下时段信息指的是在时间序列中,特定时间段之前或之后的数据点。
-
这可以通过创建特征来表示数据点与特定时间段的距离来实现。
-
-
存在峰值与峰值距离:
-
识别时间序列中的峰值点,并计算其他数据点与这些峰值点的距离,可以作为特征输入模型。
-
-
时间尺度特征:
-
根据需要预测的时间尺度(如1天、3天、5天等),创建相应的滞后特征和滚动统计特征。
-
通过结合这些特征,可以构建一个强大的时间序列预测模型。在实际应用中,需要根据具体问题和数据特性来选择和调整特征工程的策略。此外,特征的选择和构建应该与模型的选择和调优过程相结合,以实现最佳的预测性能。
优化方案详解
在提分实践部分,将在原有Baseline基础上做更多优化,一般优化思路,从特征工程与模型中来思考。
优化方法建议:
-
提取更多特征:在数据挖掘比赛中,特征总是最终制胜法宝,去思考什么信息可以帮助我们提高预测精准度,然后将其转化为特征输入到模型。
-
尝试不同的模型:模型间存在很大的差异,预测结果也会不一样,比赛的过程就是不断的实验和试错的过程,通过不断的实验寻找最佳模型,同时帮助自身加强模型的理解能力。
特征优化
这里主要构建了历史平移特征、差分特征、和窗口统计特征;每种特征都是有理可据的,具体说明如下:
(1)历史平移特征:通过历史平移获取上个阶段的信息;
(2)差分特征:可以帮助获取相邻阶段的增长差异,描述数据的涨减变化情况。在此基础上还可以构建相邻数据比值变化、二阶差分等;
(3)窗口统计特征:窗口统计可以构建不同的窗口大小,然后基于窗口范围进统计均值、最大值、最小值、中位数、方差的信息,可以反映最近阶段数据的变化情况。
参考代码如下:
# 合并训练数据和测试数据
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']]
-
合并训练数据和测试数据:
-
data = pd.concat([train, test], axis=0).reset_index(drop=True) data = data.sort_values(['id','dt'], ascending=False).reset_index(drop=True)
train
和test
是原始的训练数据和测试数据。pd.concat
将它们垂直堆叠,reset_index(drop=True)
重新设置索引。- 根据
'id'
和'dt'
排序数据,确保后续处理按照时间顺序进行。
-
历史平移:
for i in range(10, 36): data[f'target_shift{i}'] = data.groupby('id')['target'].shift(i)
- 对每个
id
的'target'
列进行历史平移操作,生成不同时间窗口的特征。
- 对每个
-
历史平移 + 差分特征:
for i in range(1, 4): data[f'target_shift10_diff{i}'] = data.groupby('id')['target_shift10'].diff(i)
- 对
target_shift10
列进行差分操作,生成差分特征。
- 对
-
窗口统计:
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
列进行滚动窗口计算,计算每个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_shift710win{win}_std'] = data.groupby('id')['target_shift10'].rolling(window=win, min_periods=3, closed='left').std().values
- 对
target_shift10
列进行滚动窗口计算,计算每个id
在历史平移后,不同窗口大小下的均值、最大值、最小值、总和和标准差。
- 对
-
数据切分:
train = data[data.target.notnull()].reset_index(drop=True) test = data[data.target.isnull()].reset_index(drop=True)
- 将处理后的数据根据
'target'
列是否为空切分为训练集和测试集。
- 将处理后的数据根据
-
确定输入特征:
train_cols = [f for f in data.columns if f not in ['id','target']]
- 确定用于训练模型的特征列,排除了
'id'
和'target'
列。
- 确定用于训练模型的特征列,排除了
模型融合
进行模型融合的前提是有多个模型的输出结果,比如使用catboost、xgboost和lightgbm三个模型分别输出三个结果,这时就可以将三个结果进行融合,最常见的是将结果直接进行加权平均融合。
下面我们构建了cv_model函数,内部可以选择使用lightgbm、xgboost和catboost模型,可以依次跑完这三个模型,然后将三个模型的结果进行取平均进行融合。
对于每个模型均选择经典的K折交叉验证方法进行离线评估,大体流程如下:
1、K折交叉验证会把样本数据随机的分成K份;
2、每次随机的选择K-1份作为训练集,剩下的1份做验证集;
3、当这一轮完成后,重新随机选择K-1份来训练数据;
4、最后将K折预测结果取平均作为最终提交结果。
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,
}
model = lgb.train(params, train_matrix, 1000,
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_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('submit.csv', index=None)
函数 cv_model
这个函数用于交叉验证和预测,根据传入的模型类型不同(clf_name
),选择不同的模型进行训练和预测。
-
参数说明:
clf
:传入的模型类对象,可以是lgb
、xgb
、CatBoostRegressor
中的任何一个。train_x
:训练数据特征。train_y
:训练数据标签。test_x
:测试数据特征。clf_name
:选择的模型名称,用于区分不同的模型。seed
:随机种子,默认为 2024。
-
交叉验证过程:
- 使用
KFold
进行数据集的划分,共分成folds
折。 - 每一折中,将数据划分为训练集 (
trn_x
,trn_y
) 和验证集 (val_x
,val_y
)。 - 根据
clf_name
使用不同的模型进行训练和预测:- LightGBM (
lgb
):使用lgb.train
进行训练,预测时使用model.predict
。 - XGBoost (
xgb
):使用clf.train
进行训练,预测时使用model.predict
。 - CatBoost (
cat
):使用clf
的构造函数初始化模型,使用model.fit
进行训练,预测时使用model.predict
。
- LightGBM (
- 使用
-
结果保存和输出:
oof
:每折的验证集预测结果,用于后续模型融合。test_predict
:每个模型对测试集的预测结果进行累加平均,用于最终输出。cv_scores
:每折的验证集均方误差(MAE)评分,用于评估模型性能。
模型选择和集成
- 使用
cv_model
函数分别对 LightGBM、XGBoost 和 CatBoost 进行交叉验证和预测。 - 每个模型的预测结果
lgb_test
、xgb_test
、cat_test
取平均作为最终的测试集预测结果final_test
。 - 将最终结果
test['target']
写入到文件submit.csv
中,包括'id'
、'dt'
和'target'
。
上传结果
运行时间:13261.9s
运行结果太长了,让我们直接看分数:
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。
Stacking参考代码:
这部分我没能实现,只是大概了解了一下。
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
'''
train_stack = pd.concat([oof_1, oof_2, oof_3], axis=1)
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 = []
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]
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)
score_single = mean_absolute_error(val_y, oof[val_idx])
scores.append(score_single)
print(f'{fold_+1}/{5}', score_single)
print('mean: ',np.mean(scores))
return oof, predictions
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'])
深度学习方案尝试
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)
这部分还没有正确实现,正在努力中。