【DataWhale 夏令营】AI+经济--Task2

 一、深入理解赛题——探索性数据分析

首先我们先介绍一下什么是EDA:

探索性数据分析(Exploratory Data Analysis, EDA)是一组数据分析技术,旨在总结其主要特征,通常通过可视化手段来实现。EDA 的目标是通过数据的统计摘要和图形展示来发现数据的结构、异常值、模式、趋势、关系以及变量之间的相互作用

现在的数据挖掘类比赛中,模型和方法选择空间往往很小,同时存在不少自动机器学习框架(如AutoGulon、AutoSKLearn)会基于一定规则,自动构造特征,采用尽可能多的模型组合来获得好分数。因此最后的关键涨分点落在了对数据的理解上,并由此构造的强特征(对结果有关键影响的变量)。

二、统计指标分析

其次我们从单变量数值关系入手,探究其统计指标的表现。具体来说可以关注(其中df是pandas的dataframe,df[feature]能够选中一行):

  • 数据的集聚趋势

    • 均值:df[feature].mean()

    • 中位数 df[feature].median()

    • 最大值 df[feature].max()

    • 最小值 df[feature].min()

    • 众数 df[feature].mode()

  • 数据的变异程度

    • 标准差 df[feature].std()

    • 极差 df[feature].apply(lambda x: x.max() - x.min())

    • 四分位数 df[feature].quantile([0.25, 0.5, 0.75])

    • 变异系数 df[feature].std()/df[feature].mean()

    • 偏度和峰度 df[feature].skew(), df[feature].kurtosis()

  • 变化率:df[feature].pct_change() 计算公式为 $$\dfrac{x_2-x_1}{x_1$$

从总负荷和出清价格的数值分布来看,可以发现以下几个特点:

  • 总负荷基本服从正态分布,符合现实中的耗电规律

  • 出清价格中有大量的异常负价格

  • 1-100之间的低价格较多,表明这些时候火电有一定的异常高价(800以上)

三、分时统计指标分析

不同小时的总负荷和电价

既然是时间序列,我们会考虑到指标值在不同时间上的不同表现,去发现时间上的特征趋势。

首先分析不同年份下,一天不同小时的总负荷和电价。我们可以发现,数据中用电存在两个高峰期和一个低谷期

  • 早高峰:一般在6-9点

  • 晚高峰:一般在16-21点

  • 低谷期:10-15点

这似乎很反常,理论上10-15点都在进行大量的工业和商业活动,为什么反而电价和总负荷会更低呢,这里需要了解一个概念:“鸭子曲线”

鸭子曲线:由于火电和光伏发电互为替代品,当一天太阳出来后,太阳能逐渐开始替代火电,并在14点达到最大,进而导致火电受光伏发电竞争而降价。而在傍晚时太阳落山,光伏机组迅速减小发电,此时火电开始集中发电,价格迅速上升,形成了一天中典型的“两高峰,一低谷”的态势。

对比下图中加利福尼亚的数据和目前的数据,可以发现二者的形式几乎一致,这样我们又增加了几个先验信息:

  • 考虑其他新能源(尤其是光伏)的影响对预测价格意义重大

  • 光伏受天气、季节等因素影响,说明还需要借助外部天气数据辅助预测。

  • 随着时间推移和中国碳中和的发展,光伏必定会在更大程度上替代火电,因此可以猜测2024年的火电价格会进一步下降。

随着新能源不断发展,光伏发电在不断替代美国加州的火力发电,形成越来越深的鸭子曲线

四、baseline代码

(此处代码较长)

import pandas as pd
import seaborn as sns
import matplotlib.pylab as plt
from pathlib import Path
import warnings
warnings.filterwarnings('ignore')
plt.style.use('ggplot')
plt.rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei',"SimHei"]
plt.rcParams['axes.unicode_minus'] = False
base_path=Path("data")

electricity_price = pd.read_csv(base_path/"electricity_price_parsed.csv", parse_dates=["timestamp"], index_col=0)
electricity_price.columns = ["demand", "price"]
electricity_price.head()

# 创建测试集掩码,标记出所有价格为 NaN 的数据行
test_mask = electricity_price["price"].isna()

# 创建训练集掩码,标记出所有价格不为 NaN 的数据行,其中~代表布尔取反,即True和False互换。 `[True,False]`和 `~[False,True]`一致
train_mask = ~test_mask

# 打印训练集的范围和总长度
# 训练集范围从训练集中最早的日期到最新的日期
print(f"训练集范围:{electricity_price[train_mask].index.min()} --> {electricity_price[train_mask].index.max()}\t总长度{len(electricity_price[train_mask])}")

# 打印测试集的范围和总长度
# 测试集范围从测试集中最早的日期到最新的日期
print(f"测试集范围:{electricity_price[test_mask].index.min()} --> {electricity_price[test_mask].index.max()}\t总长度{len(electricity_price[test_mask])}")

electricity_price.info() 
electricity_price.head(12)

def check_repeated(data, repeat_count=4):    
    """
    检查给定数据序列中是否存在元素不断重复的情况。

    参数:
    data (list): 要检查的序列数据。
    repeat_count (int): 每个元素应重复的次数。默认值为4。

    返回:
    None
    """
    # 以步长 repeat_count 遍历 data 的索引
    for i in range(0, len(data), repeat_count):
        # 从索引 i 开始,取长度为 repeat_count 的子序列
        subsequence = data[i:i + repeat_count]
        
        # 如果子序列的独特元素数量不等于 1,则表示不是同一元素重复
        if len(set(subsequence)) != 1:
            print(f"序列数据不是元素不断重复 {repeat_count} 次")
            return  # 发现不满足条件的情况后,直接返回
        
    # 如果遍历完所有子序列,未发现不满足条件的情况,则输出满足条件的信息
    print(f"序列数据是元素不断重复 {repeat_count} 次")

# 调用函数并传入特定的电价数据
check_repeated(electricity_price[train_mask]["price"])

# 使用loc方法选择指定日期的数据,绘制价格图表
electricity_price.loc["2023-01-03"].plot(y="price", figsize=(18, 5), marker='o')

# 设置图表的标题
plt.title("2023年1月3日出清价格走势")

electricity_price.describe()

# 使用 Seaborn 库绘制总负荷数值的分布图
# sns.displot(...) 函数用于绘制数据的分布图
ax = sns.displot(
    electricity_price,  # 输入数据,包含绘制所需的列
    x="demand",         # 指定绘图的列,这里是 "demand" 列
    aspect=1.5,         # 图形的宽高比,1.5 表示宽度是高度的 1.5 倍
    height=5,           # 图形的高度设置为 5 英寸
    kde=True            # 启用核密度估计(KDE),用于绘制数据的平滑概率密度曲线
)

# 设置图形的标题
ax.set(title="总负荷数值分布")

ax=sns.displot(electricity_price,x="price",aspect=1.5,height=5,kde=True);
ax.set(title="出清价格数值分布")

# 从 DataFrame 的索引中提取时间信息,并添加为新的列
# 假设索引为 DatetimeIndex 类型

# 提取小时信息,并创建一个新列 "hour"
electricity_price["hour"] = electricity_price.index.hour
# 提取月份信息,并创建一个新列 "month"
electricity_price["month"] = electricity_price.index.month
# 提取日期信息,并创建一个新列 "day"
electricity_price["day"] = electricity_price.index.day
# 提取星期几的信息(0 = 周一, 6 = 周日),并创建一个新列 "weekday"
electricity_price["weekday"] = electricity_price.index.weekday
# 提取年份信息,并创建一个新列 "year"
electricity_price["year"] = electricity_price.index.year


# 创建一个包含两个子图的绘图区域,图形大小为 20x9 英寸,并启用约束布局
fig, ax = plt.subplots(
    1,                       # 子图的行数为 1
    2,                       # 子图的列数为 2
    figsize=(20, 9),         # 设置整个图形的大小为 20x9 英寸
    constrained_layout=True  # 启用约束布局以自动调整子图位置和大小
)

# 绘制不同年份的分时电价线图
sns.lineplot(
    electricity_price.groupby(["hour", "year"])["price"].mean().reset_index(),  # 按小时和年份分组,计算每组的平均电价,并重置索引
    x="hour",                  # x 轴数据为小时
    y="price",                 # y 轴数据为平均电价
    ax=ax[0],                  # 将图绘制到第一个子图 (ax[0])
    marker="o",                # 数据点标记为圆圈
    hue="year",                # 根据年份不同设置不同的颜色
    palette="Set1"             # 使用 "Set1" 调色板
)
ax[0].set_title("不同年份的分时电价")  # 设置第一个子图的标题
ax[0].set_xticks(range(24))         # 设置 x 轴刻度为 0 到 23(小时范围)

# 绘制不同年份的分时总负荷线图
sns.lineplot(
    electricity_price.groupby(["hour", "year"])["demand"].mean().reset_index(),  # 按小时和年份分组,计算每组的平均总负荷,并重置索引
    x="hour",                  # x 轴数据为小时
    y="demand",                # y 轴数据为平均总负荷
    ax=ax[1],                  # 将图绘制到第二个子图 (ax[1])
    marker="o",                # 数据点标记为圆圈
    hue="year",                # 根据年份不同设置不同的颜色
    palette="Set1"             # 使用 "Set1" 调色板
)
ax[1].set_title("不同年份的分时总负荷")  # 设置第二个子图的标题
ax[1].set_xticks(range(24));         # 设置 x 轴刻度为 0 到 23(小时范围)


# 创建一个透视表,计算不同月份和时间下的电价
pivot = pd.pivot_table(
    electricity_price,          # 输入的 DataFrame
    values="price",             # 透视表中要填充的值,这里是 "price"
    index="month",              # 设置行索引为月份
    columns="hour"              # 设置列索引为小时
)

# 将透视表中的数据类型转换为整数
pivot = pivot.astype(int)
pivot

# 创建一个图形,大小为 17x10 英寸
plt.figure(figsize=(17, 10))

# 绘制热图,显示不同月份和时间下的电价
sns.heatmap(
    pivot,                      # 透视表数据
    cmap="coolwarm",            # 使用 "coolwarm" 调色板,显示热图的颜色
    linewidths=0.5,             # 设置单元格之间的分隔线宽度为 0.5
    annot=True,                 # 启用单元格值的注释
    fmt=".0f",                  # 注释的格式为整数
    annot_kws={"size": 12,      # 注释文本的大小设置为 12
               "weight": "bold",  # 注释文本的字体加粗
               "color": "black"}  # 注释文本的颜色为黑色
)

# 设置图形的标题
plt.title("不同月份和时间下的电价")
minus_mask = electricity_price["price"] < 0
plt.figure(figsize=(12, 7))
ax = sns.countplot(electricity_price[minus_mask], x="hour")
ax.set(title="负电量频数出现的小时分布")
plt.figure(figsize=(12, 7))
ax = sns.countplot(electricity_price[minus_mask], x="month")
ax.set(title="负电量频数出现的月分布")
(
    # 选择不满足 minus_mask 条件的数据
    electricity_price[minus_mask]
    # 按月份和日期分组,计算每个组合的记录数量
    .groupby(["month", "day"])["price"]
    .size()                      # 计算每个分组的大小(即每个分组的记录数)
    .reset_index()              # 重置索引,使 groupby 结果成为 DataFrame,并保留分组字段为列
    .sort_values("price",       # 按 "price" 列排序
                 ascending=False)        # 降序排序
    .head(15)                   # 选择排序后的前 15 行
)
# 创建一个图形,大小为 20x8 英寸
plt.figure(figsize=(20, 8))

# 绘制从 2022年5月1日到2022年5月9日的电价数据的折线图
ax = sns.lineplot(
    electricity_price.loc["2022-05-01":"2022-05-09"]["price"],  # 从 DataFrame 中选择时间范围内的电价数据
    color="black"  # 设置折线的颜色为黑色
)

# 在每一天的 10:00 到 15:00 之间添加黄色半透明的高亮区域
for i in range(1, 10):
    ax.axvspan(
        f"2022-05-0{i} 10:00:00",  # 高亮区域的开始时间
        f"2022-05-0{i} 15:00:00",  # 高亮区域的结束时间
        color='yellow',            # 高亮区域的颜色为黄色
        alpha=0.2                  # 高亮区域的透明度设置为 0.2(0 完全透明,1 完全不透明)
    )

# 添加一条红色的虚线,y 值为 0,用于显示负电价的参考线
plt.axhline(
    y=0,                      # y 轴的值为 0
    color="red",              # 虚线的颜色为红色
    linestyle="--"           # 虚线的线型为虚线
)

# 设置 x 轴刻度标签的旋转角度为 45 度,并且水平对齐方式为右对齐
plt.setp(
    ax.get_xticklabels(),     # 获取 x 轴刻度标签
    rotation=45,              # 设置标签的旋转角度为 45 度
    ha='right'                # 设置标签的水平对齐方式为右对齐
)

# 设置图形的标题
plt.title("一段比较典型的负电价趋势:2022年5月1日 - 2022年5月9日")
plt.figure(figsize=(20, 8))
ax = sns.lineplot(electricity_price.loc["2023-01-21":"2023-02-02"]["price"], color="black")
plt.axhline(y=0, color="red", linestyle="--")
plt.setp(ax.get_xticklabels(), rotation=45, ha='right')
plt.title("假期对负电价的影响 2022年1月28日 - 2022年2月6日")
# 计算电价的上限阈值,使用 3 标准差原则来检测异常值
upper_threshold = (
    electricity_price["price"].mean() +      # 电价的均值
    3 * electricity_price["price"].std()      # 加上 3 倍的电价标准差
)

# 创建一个布尔型掩码,用于标识电价高于上限阈值的异常值
high_abnormal_mask = (
    electricity_price["price"] > upper_threshold  # 判断电价是否大于计算得到的上限阈值
)
plt.figure(figsize=(12, 7))
ax = sns.countplot(electricity_price[high_abnormal_mask], x="hour")
ax.set(title="高电量频数出现的小时分布")
plt.figure(figsize=(12,7))
ax=sns.countplot(electricity_price[high_abnormal_mask],x="month")
ax.set(title="高电量频数出现的月分布");
# 从电价数据中筛选出高于上限阈值的异常值
(
electricity_price[high_abnormal_mask]
    # 按月份和日期分组,并计算每个组合的异常值记录数量
    .groupby(["month", "day"])["price"]
    .size()                      # 计算每个分组的大小(即每个分组的记录数)
    .reset_index()              # 重置索引,将分组字段转换为 DataFrame 的列
    .sort_values("price",       # 按 "price" 列排序
        ascending=False)        # 降序排序
    .head(15)                   # 选择排序后的前 15 行
)
plt.figure(figsize=(20, 8))
ax = sns.lineplot(electricity_price.loc["2022-08-01":"2022-08-08"]["demand"], color="black")
plt.setp(ax.get_xticklabels(), rotation=45, ha='right')
plt.axvspan("2022-08-03", "2022-08-06", color="yellow", alpha=0.2)
plt.title("2022年8月1日 - 2022年8月8日 总需求")
plt.figure(figsize=(20, 8))
ax = sns.lineplot(electricity_price.loc["2022-08-01":"2022-08-08"]["price"], color="black")
plt.axhline(y=upper_threshold, color="red", linestyle="--")
plt.setp(ax.get_xticklabels(), rotation=45, ha='right')
plt.title("典型高电价时间段 2022年8月1日 - 2022年8月8日 中的3-6日")
electricity_price[["demand","price"]].corr()

五、优化结果

相比于Task1的baseline代码的性能还是有所提升,但是总体来说提升得较小,后续可以尝试其他方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值