Datawhale AI夏令营 第三期 Task3 进阶上分方向讨论——阿里云天池平台“第二届世界科学智能大赛社会科学赛道:市场博弈和价格预测” 学习笔记

目录

1.baseline3理解

1.1数据及特征

①导入包和读取数据

②特征工程

③节假日特征

④窗口特征

⑤其他时序特征

1.2模型训练

1.3模型融合

2.自我尝试

2.1节假日特征构建

2.2缺失值填充填充

2.3未完待续


Task3提供了三个主要的上分方向:

  • 时间序列挖掘(较容易)

  • ABM报价策略优化(难度中等偏上)

  • 强化学习(难度高)

 此笔记主要围绕时间序列挖掘展开。

1.baseline3理解

1.1数据及特征

①导入包和读取数据

同Baseline1

②特征工程

特征工程是数据预处理的重要步骤,其重要性如下:

  • 提高模型性能:通过选择和构建更有信息量的特征,可以提升模型的预测准确性。
  • 简化模型:减少冗余或无关特征,能够使模型更简洁、更易于理解和解释。
  • 提升训练效率:合适的特征选择可以加快模型的训练速度,减少计算资源消耗。

这里构造了以下时间序列特征:

  • 时间戳特征:年、月、日、小时

  • 其他信息:季度、星期

  • 数据相关的特殊信息:是否风季(按月份判断)、是否低谷时段(按小时判断)

# 将电价数据的时间索引信息添加到训练数据中

# 提取时间索引的小时信息,并添加到训练数据中,创建 "hour" 列
train_data["hour"] = electricity_price.index.hour

# 提取时间索引的日期信息,并添加到训练数据中,创建 "day" 列
train_data["day"] = electricity_price.index.day

# 提取时间索引的月份信息,并添加到训练数据中,创建 "month" 列
train_data["month"] = electricity_price.index.month

# 提取时间索引的年份信息,并添加到训练数据中,创建 "year" 列
train_data["year"] = electricity_price.index.year

# 提取时间索引的星期信息,并添加到训练数据中,创建 "weekday" 列
train_data["weekday"] = electricity_price.index.weekday

# 根据月份信息,判断是否为风季(1-5月和9-12月),创建布尔型 "is_windy_season" 列
train_data["is_windy_season"] = electricity_price.index.month.isin([1, 2, 3, 4, 5, 9, 10, 11, 12])

# 根据小时信息,判断是否为低谷时段(10-15点),创建布尔型 "is_valley" 列
train_data["is_valley"] = electricity_price.index.hour.isin([10, 11, 12, 13, 14, 15])

# 提取时间索引的季度信息,并添加到训练数据中,创建 "quarter" 列
train_data["quarter"] = electricity_price.index.quarter

# 对时间特征进行独热编码(One-Hot Encoding),删除第一列以避免多重共线性
train_data = pd.get_dummies(
    data=train_data,        # 需要进行独热编码的 DataFrame
    columns=["hour", "day", "month", "year", "weekday"],  # 需要独热编码的列
    drop_first=True         # 删除第一列以避免多重共线性
)

最后对时间特征进行独热编码(One-Hot Encoding)是为了将分类特征转化为模型可以使用的数值特征。独热编码将每个分类变量转换为多个二进制变量(0或1),这样模型可以更好地处理这些分类数据。

删除第一列是为了避免多重共线性。多重共线性指的是特征之间存在高度相关性,这可能导致模型在训练时出现问题。删除第一列是常用的技术,以避免这种情况发生。

总结来说,通过这些步骤,可以将时间索引的各种信息转化为特征,帮助模型捕捉到电价数据中的时间模式和季节性变化。独热编码将这些分类特征转化为数值特征,使模型能够处理它们并提高预测准确性。

③节假日特征

基于Task2中的EDA,发现节假日中电力价格更容易为负值,因此构造了典型的春节和劳动节的节假日特征。

这里,通过观察EDA的结果,我也尝试构造了清明节特征。

# 节假日特征
def generate_holiday_dates(start_dates, duration):
    """
    生成一系列节假日日期列表。

    参数:
    start_dates (list): 节假日开始日期的列表,格式为字符串,例如 ["2022-01-31", "2023-01-21"]。
    duration (int): 每个节假日的持续天数。

    返回:
    list: 包含所有节假日日期的列表。
    """
    holidays = []  # 初始化一个空列表,用于存储节假日日期
    for start_date in start_dates:  # 遍历每个节假日的开始日期
        # 生成从 start_date 开始的日期范围,持续时间为 duration 天
        holidays.extend(pd.date_range(start=start_date, periods=duration).tolist())
    return holidays  # 返回所有节假日日期的列表

# 春节的开始日期列表
spring_festival_start_dates = ["2022-01-31", "2023-01-21", "2024-02-10"]

# 劳动节的开始日期列表
labor_start_dates = ["2022-04-30", "2023-04-29"]

# 清明节的开始日期列表
qingming_festival_start_dates = ["2022-04-03", "2024-04-04"]

# 生成春节的所有日期,持续时间为 7 天
spring_festivals = generate_holiday_dates(spring_festival_start_dates, 7)

# 生成劳动节的所有日期,持续时间为 5 天
labor = generate_holiday_dates(labor_start_dates, 5)

# 生成清明节的所有日期,持续时间为 3 天
qingming = generate_holiday_dates(qingming_festival_start_dates, 3)

# 判断训练数据的索引是否在春节日期列表中,生成布尔型列 "is_spring_festival"
train_data["is_spring_festival"] = train_data.index.isin(spring_festivals)

# 判断训练数据的索引是否在劳动节日期列表中,生成布尔型列 "is_labor"
train_data["is_labor"] = train_data.index.isin(labor)

# 判断训练数据的索引是否在清明节日期列表中,生成布尔型列 "is_labor"
train_data["is_labor"] = train_data.index.isin(qingming)

④窗口特征

可以发现一段时间内,总负荷如果有下降趋势,则下一个出清价格也可能下降。因此联想到构造基于总需求的窗口特征。

利用pandas非常好用的agg聚合函数,其可以将一列根据选择的函数转换为统计值(例如取一列标准差、均值),这里由于是对每个窗口agg,相当于取每个窗口的特定统计值。

在agg函数定义中,参数接受一列值(为pandas的Series),做某种计算后返回单个值。

# 构造基于demand的窗口特征
# 可以发现一段时间内,总负荷如果有下降趋势,则下一个出清价格也可能下降。因此联想到构造基于总需求的窗口特征。

def cal_range(x):
    """
    计算极差(最大值和最小值之差)。

    参数:
    x (pd.Series): 输入的时间序列数据。

    返回:
    float: 极差值。

    示例:
    >>> import pandas as pd
    >>> x = pd.Series([1, 2, 3, 4, 5])
    >>> cal_range(x)
    4
    """
    return x.max() - x.min()


def increase_num(x):
    """
    计算序列中发生增长的次数。

    参数:
    x (pd.Series): 输入的时间序列数据。

    返回:
    int: 序列中增长的次数。

    示例:
    >>> x = pd.Series([1, 2, 3, 2, 4])
    >>> increase_num(x)
    3
    """
    return (x.diff() > 0).sum()


def decrease_num(x):
    """
    计算序列中发生下降的次数。

    参数:
    x (pd.Series): 输入的时间序列数据。

    返回:
    int: 序列中下降的次数。

    示例:
    >>> x = pd.Series([1, 2, 1, 3, 2])
    >>> decrease_num(x)
    2
    """
    return (x.diff() < 0).sum()


def increase_mean(x):
    """
    计算序列中上升部分的均值。

    参数:
    x (pd.Series): 输入的时间序列数据。

    返回:
    float: 序列中上升部分的均值。

    示例:
    >>> x = pd.Series([1, 2, 3, 2, 4])
    >>> diff = x.diff()
    >>> diff
    0    NaN
    1    1.0
    2    1.0
    3   -1.0
    4    2.0
    dtype: float64
    >>> increase_mean(x)
    1.33
    """
    diff = x.diff()
    return diff[diff > 0].mean()


def decrease_mean(x):
    """
    计算序列中下降的均值(取绝对值)。

    参数:
    x (pd.Series): 输入的时间序列数据。

    返回:
    float: 序列中下降的均值(绝对值)。

    示例:
    >>> import pandas as pd
    >>> x = pd.Series([4, 3, 5, 2, 6])
    >>> decrease_mean(x)
    2.0
    """
    diff = x.diff()
    return diff[diff < 0].abs().mean()


def increase_std(x):
    """
    计算序列中上升部分的标准差。

    参数:
    x (pd.Series): 输入的时间序列数据。

    返回:
    float: 序列中上升部分的标准差。

    示例:
    >>> import pandas as pd
    >>> x = pd.Series([1, 2, 3, 2, 4])
    >>> increase_std(x)
    0.5773502691896257
    """
    diff = x.diff()
    return diff[diff > 0].std()


def decrease_std(x):
    """
    计算序列中下降部分的标准差。

    参数:
    x (pd.Series): 输入的时间序列数据。

    返回:
    float: 序列中下降部分的标准差。

    示例:
    >>> import pandas as pd
    >>> x = pd.Series([4, 3, 5, 2, 6])
    >>> decrease_std(x)
    1.4142135623730951
    """
    diff = x.diff()
    return diff[diff < 0].std()

from tqdm import tqdm  # 导入 tqdm 库用于显示进度条

# 定义滚动窗口大小的列表
window_sizes = [4, 12, 24]

# 遍历每个窗口大小
with tqdm(window_sizes) as pbar:
    for window_size in pbar:
        # 定义要应用的聚合函数列表
        functions = ["mean", "std", "min", "max", cal_range, increase_num,
                     decrease_num, increase_mean, decrease_mean, increase_std, decrease_std]

        # 遍历每个聚合函数
        for func in functions:
            # 获取函数名称,如果是字符串则直接使用,否则使用函数的 __name__ 属性
            func_name = func if type(func) == str else func.__name__

            # 生成新列名,格式为 demand_rolling_{window_size}_{func_name}
            column_name = f"demand_rolling_{window_size}_{func_name}"

            # 计算滚动窗口的聚合值,并将结果添加到 train_data 中
            train_data[column_name] = train_data["demand"].rolling(
                window=window_size,        # 滚动窗口大小
                min_periods=window_size//2,  # 最小观测值数
                closed="left"         # 滚动窗口在左侧闭合
            ).agg(func)              # 应用聚合函数

            pbar.set_postfix({"window_size": window_size, "func": func_name})

⑤其他时序特征

例如demand的滞后特征,差分特征,百分比特征等。

通常我们会尝试构造尽可能多的特征,随后在特征筛选中留下对结果有显著影响的特征。

# 添加新的特征列:demand_shift_1,表示将 demand 列中的值向后移动一位
# shift(1) 的结果是当前行的值等于前一行的值,第一行的值为 NaN
train_data["demand_shift_1"] = train_data["demand"].shift(1)

# 添加新的特征列:demand_diff_1,表示 demand 列中相邻值的差
# diff(1) 的结果是当前行的值减去前一行的值,第一行的值为 NaN
train_data["demand_diff_1"] = train_data["demand"].diff(1)

# 添加新的特征列:demand_pct_1,表示 demand 列中相邻值的百分比变化
# pct_change(1) 的结果是当前行的值减去前一行的值再除以前一行的值,第一行的值为 NaN
train_data["demand_pct_1"] = train_data["demand"].pct_change(1)

获取时序特征会形成缺失值,最后进行填充。

# 从 train_data 中创建训练集和测试集特征数据 (X) 和目标数据 (y)

# 创建训练集特征数据 X_train
# 1. 从 train_data 中选择前 train_length 行,去除 "price" 列
# 2. 使用 bfill 方法向后填充缺失值
# 3. 使用 ffill 方法向前填充缺失值
X_train = train_data.iloc[:train_length].drop(columns=["price"]).bfill().ffill()

# 创建测试集特征数据 X_test
X_test = train_data.iloc[train_length:].drop(columns=["price"]).bfill().ffill()

# 创建训练集目标数据 y_train
y_train = train_data.iloc[:train_length][["price"]]

1.2模型训练

一般数据科学比赛中,线性模型和树模型是比较常用的两种回归模型,在这里将线性回归LightGBM作为Baseline的两个模型,并不进行特别的超参数调优。

from sklearn.linear_model import LinearRegression
from lightgbm import LGBMRegressor

# 创建 LGBMRegressor 模型对象,设置参数
# num_leaves:树的叶子数,控制模型复杂度
# n_estimators:迭代次数,即树的数量
# verbose:日志信息级别,-1 表示不输出日志信息
lgb_model = LGBMRegressor(num_leaves=2**5-1, n_estimators=300, verbose=-1)

# 创建线性回归模型对象
linear_model = LinearRegression()

# 使用训练集数据训练 LGBMRegressor 模型
# X_train:训练集特征数据
# y_train:训练集目标数据
lgb_model.fit(X_train, y_train)

# 使用训练集数据中的 "demand" 特征训练线性回归模型
# X_train[["demand"]]:训练集特征数据中仅包含 "demand" 列
# y_train:训练集目标数据
linear_model.fit(X_train[["demand"]], y_train)

# 使用训练好的 LGBMRegressor 模型预测测试集特征数据
# X_test:测试集特征数据
# 返回预测的目标值
lgb_pred = lgb_model.predict(X_test)

# 使用训练好的线性回归模型预测测试集特征数据中的 "demand" 列
# X_test[["demand"]]:测试集特征数据中仅包含 "demand" 列
# 返回预测的目标值,并将结果展平为一维数组
linear_pred = linear_model.predict(X_test[["demand"]]).flatten()

可以尝试其他模型例如:

  • 基于深度学习的方法:LSTM、NBeats、Transformer……

  • 基于时序模型的方法:ARIMA、Prophet、VARMAX……

  • 其他线性模型:Ridge、ElasticNet、Lasso……

  • 其他树模型:CatBoost、XGBoost、LightGBM

1.3模型融合

考虑到demand对价格的线性程度高,但直接线性回归无法捕捉到时序中的一些非线性特征,因此我们融合树模型和线性模型的结果,由于我们没有其他先验信息,选择直接取均值。

还记得我们在Task2中提到,由于鸭子曲线,火电厂在低谷期时的价格会越来越低,并且整体火电价格也会下降,因此我们可以对最终预测结果进行一个整体上的缩放,保证符合未来的趋势。

# 简单求均值
y_pred = (lgb_pred+linear_pred)/2
y_pred *= 0.95  # 进行少量修正

2.自我尝试

2.1节假日特征构建

通过观察EDA的结果,我也尝试构造了清明节特征。具体见1.1的④。

2.2缺失值填充填充

使用KNN插值填充。

from sklearn.impute import KNNImputer

# 初始化 KNNImputer
# n_neighbors:用于插值的邻居数量
knn_imputer = KNNImputer(n_neighbors=5)

# 创建训练集特征数据 X_train
# 1. 从 train_data 中选择前 train_length 行,去除 "price" 列
X_train = train_data.iloc[:train_length].drop(columns=["price"])

# 创建测试集特征数据 X_test
X_test = train_data.iloc[train_length:].drop(columns=["price"])

# 使用 KNNImputer 填充训练集和测试集的缺失值
X_train_imputed = knn_imputer.fit_transform(X_train)
X_test_imputed = knn_imputer.transform(X_test)

# 将填充后的数据转换为 DataFrame 并恢复列名
X_train = pd.DataFrame(X_train_imputed, index=X_train.index, columns=X_train.columns)
X_test = pd.DataFrame(X_test_imputed, index=X_test.index, columns=X_test.columns)

# 创建训练集目标数据 y_train
y_train = train_data.iloc[:train_length][["price"]]

2.3未完待续

。。。

  • 7
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值