导入相关库
# 数据格式处理
import numpy as np
import pandas as pd
# 分析模型
import lightgbm as lgb
from sklearn.model_selection import StratifiedKFold, KFold, GroupKFold
from sklearn.metrics import mean_squared_error, mean_absolute_error
# 可视化工具
import matplotlib.pyplot as plt
# 防警告跳出
import warnings
warnings.filterwarnings('ignore')
数据预处理
数据准备:数据载入与简略观测
# 设置数据集路径
path = r'E:\编程\电动汽车充电站充电量预测'
# 读取数据:载入训练集和测试集数据,后续同步处理
train_power_forecast_history = pd.read_csv(path + r'\训练集\power_forecast_history.csv')
train_power = pd.read_csv(path + r'\训练集\power.csv')
train_stub_info = pd.read_csv(path + r'\训练集\stub_info.csv')
test_power_forecast_history = pd.read_csv(path + r'\测试集\power_forecast_history.csv')
test_stub_info = pd.read_csv(path + r'\测试集\stub_info.csv')
# 观察数据
train_power_forecast_history.head(10)
train_power_forecast_history.tail(10)
观察发现,训练集的站点运营数据表共有约有356.6万条记录,其中,场站编码'id_encode' 范围是0~499,表中还包含 小时'hour',电费‘ele_price',服务费'ser_price',折后服务费'after_ser_price',总费用'total_price',三个业务指标'f',日期'ds' 共10个字段。
train_power.head(10)
train_power.tail(10)
站点充电量数据表中有一个新字段 充电量'power',是我们的预测目标字段,只在训练集中出现。
train_stub_info.head() # 默认参数为5
train_stub_info.tail()
站点静态数据表中新出现的字段有 交流桩额定功率'ac_equipment_kw', 直流桩额定功率'dc_equipment_kw',分层地理编码'h3'(将地球划分为不同的六边形网格),场站标签'flag'(脱敏字段,无描述的有效数据),停车收费'parking_free'(0:收费,1~9:免费停车时长/h)。
数据准备:数据集成
# 聚合数据
'''groupby.head(1):将'id_encode''ds'字段(列)值均相同的记录(行)放入一个组,
生成一个新的表格(dataframe),并纵向拼接新表,且只取新表第一条记录拼接'''
train_df = train_power_forecast_history.groupby(['id_encode', 'ds']).head(1)
del train_df['hour'] # 删除字段 'hour'
test_df = test_power_forecast_history.groupby(['id_encode', 'ds']).head(1)
del test_df['hour']
'''groudby;将站点充电量数据表'id_encode''ds'组合作为标签,统计分组的 'power' 字段值的和'''
tmp_df = train_power.groupby(['id_encode', 'ds'])['power'].sum()
tmp_df.columns = ['id_encode', 'ds', 'power'] # 字段重命名
# 合并充电量数据
'''merge:以左参数train_df的'id_encode'-'ds'组合字段为索引行,与右参数tmp_df合并'''
train_df = train_df.merge(tmp_df, on=['id_encode','ds'], how='left')
# 合并站点静态数据
train_df = train_df.merge(train_stub_info, on='id_encode', how='left')
test_df = test_df.merge(test_stub_info, on='id_encode', how='left')
数据探索:可视化
# 选中要绘制的列
cols = ['power']
# 遍历id_encode的五个值
for ie in [0, 1, 2, 3, 4]:
# 获取train_df中id_encode为当前值ie的所有行,并重置索引(不添新列)
tmp_df = train_df[train_df['id_encode'] == ie].reset_index(drop=True)
# 两次重置索引,第二次新添了 'index' 列
tmp_df = tmp_df.reset_index(drop=True).reset_index()
# 遍历要绘制的列
'''enumerate:将一个可迭代对象重组为索引序列,列出数据及其下标,常用于for循环结构'''
for num, col in enumerate(cols): # num为按日期排序的索引,col为数值
# 设置折线图的大小
plt.figure(figsize=(20, 10))
# 创建子图,将整个图像窗口分为4行1列,当前为第num+1个子图
plt.subplot(2, 2, num+1)
# 绘制折线图:x轴为 'index',y轴为当前列的值
plt.plot(tmp_df['index'], tmp_df[col])
# 子图标题设置为当前列的名称
plt.title(col)
# 显示图形
plt.show()
以上是0~5场站编码分组的充电量随时间变化的折线图。
数据清洗:数据变换
# 数据变换:对含义不明的二元特征,规范化处理成数值,便于模型计算
'''map:对选中列的所有数据进行检索,用定义的哈希表(字典)中的值覆盖相应的键'''
train_df['flag'] = train_df['flag'].map({'A': 0, 'B': 1})
test_df['flag'] = test_df['flag'].map({'A': 0, 'B': 1})
特征工程:提取时序特征
# 定义提取时间戳的方法
def get_time_feature(df, col):
# 浅拷贝原表,保护原数据
df_copy = df.copy()
# 统一格式:ds_
prefix = col + "_"
# 命名新字段,用于储存日期数据
df_copy['new_' + col] = df_copy[col].astype(str)
# 统一格式:new_ds
col = 'new_' + col
# 将MySQL类型日期数据转化为’年月日‘日期格式
df_copy[col] = pd.to_datetime(df_copy[col], format='%Y%m%d')
df_copy[prefix + 'year'] = df_copy[col].dt.year # 从日期字段提取年特征
df_copy[prefix + 'month'] = df_copy[col].dt.month # 月
df_copy[prefix + 'day'] = df_copy[col].dt.day # 日
# 星期:1=日,2=星期一,3=星期二,4=星期三,5=星期四,6=星期五,7=星期六
df_copy[prefix + 'dayofweek'] = df_copy[col].dt.dayofweek
df_copy[prefix + 'is_wknd'] = df_copy[col].dt.dayofweek // 6 # 周末判断:1=周末
df_copy[prefix + 'quarter'] = df_copy[col].dt.quarter # 季度:1~4
# 月初判断:返回值为布尔值,再转化为int整数类型
df_copy[prefix + 'is_month_start'] = df_copy[col].dt.is_month_start.astype(int)
# 月末判断
df_copy[prefix + 'is_month_end'] = df_copy[col].dt.is_month_end.astype(int)
del df_copy[col] # 删除日期字段
return df_copy
# 提取时间特征
train_df = get_time_feature(train_df, 'ds')
test_df = get_time_feature(test_df, 'ds')
# 选中测试集中'ds', 'power', 'h3'以外的所有字段
cols = [f for f in test_df.columns if f not in ['ds', 'power', 'h3']]
模型训练与验证
# 使用K折交叉验证训练和验证模型
'''定义模型训练方法,参数为:
所训模型,训练集中分割出的训练集、测试集,测试集中用于预测的数据,随机种子'''
def cv_model(clf, train_x, train_y, test_x, seed=2023):
# 定义折数并初始化KFold
folds = 5
kf = KFold(n_splits=folds, shuffle=True, random_state=seed)
# 初始化oof预测和测试集预测
oof = np.zeros(train_x.shape[0])
test_predict = np.zeros(test_x.shape[0])
cv_scores = []
# KFold交叉验证:训练数据为train划分的两个数据集
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]
# 转换数据为lightgbm数据格式
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': 'rmse',
'min_child_weight': 5,
'num_leaves': 2 ** 7,
'lambda_l2': 10,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'bagging_freq': 4,
'learning_rate': 0.1,
'seed': 2023,
'nthread': 16,
'verbose': -1,
# 'device':'gpu'
}
# 训练模型
model = clf.train(params, train_matrix, 3000, valid_sets=[train_matrix, valid_matrix], categorical_feature=[])
# 获取验证和测试集的预测值
val_pred = model.predict(val_x, num_iteration=model.best_iteration)
test_pred = model.predict(test_x, num_iteration=model.best_iteration)
oof[valid_index] = val_pred
test_predict += test_pred / kf.n_splits
# 计算并打印当前折的分数
score = np.sqrt(mean_squared_error(val_pred, val_y))
cv_scores.append(score)
print(cv_scores)
return oof, test_predict
# 调用上面的函数进行模型训练和预测
lgb_oof, lgb_test = cv_model(lgb, train_df[cols], train_df['power'], test_df[cols])
提交结果
# 输出赛题提交格式的结果
test_df['power'] = lgb_test
test_df['power'] = test_df['power'].apply(lambda x: 0 if x < 0 else x)
test_df[['id_encode', 'ds', 'power']].to_csv('result.csv', index=False)