Datawhale AI 夏令营-电力需求挑战赛Task2

174 阅读10分钟

赛事官网challenge.xfyun.cn/h5/detail?t…

这次的赛事属于时间序列分析,也是回归分析的范畴.能够取得不错效果的话,常规思路一般为使用机器学习模型,如LightGBM、XGBoost,或者使用深度学习模型(神经网络等)进行实践,在模型的搭建上就比较复杂,需要自己构建模型结构,对于数值数据需要进行标准化处理.

常见的时间序列场景有:

  1. 金融领域:股票价格预测、利率变动、汇率预测等。
  2. 气象领域:温度、降水量、风速等气候指标的预测。
  3. 销售预测:产品或服务的未来销售额预测。
  4. 库存管理:预测库存需求,优化库存水平。
  5. 能源领域:电力需求预测、石油价格预测等。
  6. 医疗领域:疾病爆发趋势预测、医疗资源需求预测。

时间序列问题的数据往往有如下特点:

  1. 时间依赖性:数据点之间存在时间上的连续性和依赖性。
  2. 非平稳性:数据的统计特性(如均值、方差)随时间变化。
  3. 季节性:数据表现出周期性的模式,如年度、月度或周度。
  4. 趋势:数据随时间推移呈现长期上升或下降的趋势。
  5. 周期性:数据可能存在非固定周期的波动。
  6. 机波动:数据可能受到随机事件的影响,表现出不确定性。

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')

image.png

数据简单介绍:

  • 其中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()

image.png

# - 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()

image.png

(3)特征工程

这里主要构建了 历史平移特征窗口统计特征;每种特征都是有理可据的,具体说明如下:

  • 历史平移特征: 通过历史平移获取上个阶段的信息;如下图所示,可以将d-1时间的信息给到d时间,d时间信息给到d+1时间,这样就实现了平移一个单位的特征构建。

  • 窗口统计特征: 窗口统计可以构建不同的窗口大小,然后基于窗口范围进统计均值、最大值、最小值、中位数、方差的信息,可以反映最近阶段数据的变化情况。如下图所示,可以将d时刻之前的三个时间单位的信息进行统计构建特征给我d时刻。

image.png

完整代码如下:

# 合并训练数据和测试数据,并进行排序
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 越小越好).

image.png

方案提升

在原有Baseline基础上做更多优化,一般优化思路,从特征工程与模型中来思考。

image.png

这里主要构建了历史平移特征、差分特征、和窗口统计特征,使用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…

datawhaler.feishu.cn/wiki/G8Wywy…

lightgbm.readthedocs.io/en/latest/i…