电力负荷案例之特征工程-添加上n小时(窗口字段)
1 课程概览
本课讲解电力负荷案例的特征工程第二步:添加上n小时的负荷特征(窗口字段)。使用shift函数提取前1、前2、前3个小时的负荷。
2 核心概念与定义
- 窗口字段:上n小时的负荷特征。
- shift函数:数据移位,获取前n个时间步的值。
- shift(1):上一个小时的负荷。
- shift(2):上两个小时的负荷。
- shift(3):前三个小时的负荷。
- NaN:移位后产生的空值。
3 算法与模型详解
3.1 窗口字段概念
定义:相近窗口中的负荷特征
说明:
- 当前小时的负荷
- 上一个小时的负荷
- 上两个小时的负荷
- 前三个小时的负荷
3.2 shift函数
功能:数据移位
语法:
feature_data['power_load'].shift(n)
参数:
n=1:上一个时间步n=2:上两个时间步n=3:前三个时间步
3.3 shift(1)详解
原始数据:
时间 power_load
10:00 800
11:00 900
12:00 1000
shift(1)后:
时间 power_load shift_1
10:00 800 NaN ← 第一个没有前一个
11:00 900 800 ← 上一个小时是800
12:00 1000 900 ← 上一个小时是900
说明:
- 第一行没有前一个,所以是NaN
- 第二行的上一个小时是第一行的800
- 第三行的上一个小时是第二行的900
3.4 shift(2)详解
shift(2)后:
时间 power_load shift_2
10:00 800 NaN ← 第一个没有上上一个
11:00 900 NaN ← 第二个没有上上一个
12:00 1000 800 ← 上两个小时是800
说明:
- 前两行没有上上一个,所以是NaN
- 第三行的上两个小时是第一行的800
3.5 排队类比
类比:体育课排队
- 第一个人:前面没有人
- 第二个人:前面是第一个人
- 第三个人:前面是第二个人,前前面是第一个人
对应:
- shift(1):前一个
- shift(2):前两个
- shift(3):前三个
3.6 添加窗口字段
代码:
feature_data['prev_1'] = feature_data['power_load'].shift(1)
feature_data['prev_2'] = feature_data['power_load'].shift(2)
feature_data['prev_3'] = feature_data['power_load'].shift(3)
说明:
prev_1:上一个小时的负荷prev_2:上两个小时的负荷prev_3:前三个小时的负荷
3.7 数据拼接
说明:将shift后的数据拼接到原数据
方法:pd.concat()或直接赋值
4 代码示例
import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import logging
# 解决中文乱码
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 1. 日志工具类
print("=== 1. 日志工具类 ===")
class LogUtils:
"""日志工具类"""
def __init__(self, root_path='./', log_name='project', level=logging.INFO):
self.logger = logging.getLogger(log_name)
self.logger.setLevel(level)
if not self.logger.handlers:
log_dir = os.path.join(root_path, 'log')
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, f'{log_name}.log')
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setLevel(level)
console_handler = logging.StreamHandler()
console_handler.setLevel(level)
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
self.logger.addHandler(file_handler)
self.logger.addHandler(console_handler)
def get_log(self):
return self.logger
# 2. 生成模拟数据
print("\n=== 2. 生成模拟数据 ===")
def generate_power_load_data(n_days=7):
"""生成电力负荷数据"""
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=n_days*24, freq='h')
power_load = []
for date in dates:
hour = date.hour
dayofweek = date.dayofweek
if 0 <= hour < 6:
base_load = 500
elif 6 <= hour < 9:
base_load = 800
elif 9 <= hour < 17:
base_load = 1000
elif 17 <= hour < 21:
base_load = 1200
else:
base_load = 700
if dayofweek >= 5:
base_load *= 0.8
load = base_load + np.random.normal(0, 50)
power_load.append(max(100, load))
data = pd.DataFrame({
'time': dates,
'power_load': power_load
})
return data
data = generate_power_load_data(n_days=7)
print(f"数据形状: {data.shape}")
# 3. shift函数详解
print("\n=== 3. shift函数详解 ===")
def shift_explanation(data):
"""shift函数详解"""
feature_data = data.copy()
print("原始数据(前10行):")
print(feature_data[['time', 'power_load']].head(10))
# shift(1)
print("\n--- shift(1): 上一个小时 ---")
temp_df = feature_data[['power_load']].copy()
temp_df['shift_1'] = temp_df['power_load'].shift(1)
print(temp_df.head(10))
# shift(2)
print("\n--- shift(2): 上两个小时 ---")
temp_df = feature_data[['power_load']].copy()
temp_df['shift_2'] = temp_df['power_load'].shift(2)
print(temp_df.head(10))
# shift(3)
print("\n--- shift(3): 前三个小时 ---")
temp_df = feature_data[['power_load']].copy()
temp_df['shift_3'] = temp_df['power_load'].shift(3)
print(temp_df.head(10))
shift_explanation(data)
# 4. 添加窗口字段
print("\n=== 4. 添加窗口字段 ===")
def add_window_features(data, logger=None):
"""
添加窗口字段(上n小时负荷)
Args:
data: 数据源
logger: 日志对象
Returns:
feature_data: 处理后的数据
"""
if logger:
logger.info("开始添加窗口字段")
feature_data = data.copy()
# 添加前1、前2、前3个小时的负荷
feature_data['prev_1'] = feature_data['power_load'].shift(1)
feature_data['prev_2'] = feature_data['power_load'].shift(2)
feature_data['prev_3'] = feature_data['power_load'].shift(3)
if logger:
logger.info("窗口字段添加完成")
print("添加窗口字段后(前10行):")
print(feature_data[['time', 'power_load', 'prev_1', 'prev_2', 'prev_3']].head(10))
return feature_data
# 创建日志对象
log_utils = LogUtils(root_path='./', log_name='window_features')
logger = log_utils.get_log()
feature_data = add_window_features(data, logger)
# 5. shift函数原理
print("\n=== 5. shift函数原理 ===")
def shift_principle():
"""shift函数原理"""
print("""
shift函数原理:
1. shift(1)
- 将数据向下移动1位
- 第一行变成NaN(没有前一个)
- 第二行的值是第一行的原值
2. shift(2)
- 将数据向下移动2位
- 前两行变成NaN
- 第三行的值是第一行的原值
3. shift(n)
- 将数据向下移动n位
- 前n行变成NaN
- 第n+1行的值是第一行的原值
示例:
原始: shift(1): shift(2): shift(3):
800 NaN NaN NaN
900 800 NaN NaN
1000 900 800 NaN
1100 1000 900 800
1200 1100 1000 900
""")
shift_principle()
# 6. 排队类比
print("\n=== 6. 排队类比 ===")
def queue_analogy():
"""排队类比"""
print("""
排队类比:
场景: 体育课排队
第1个人: 前面没有人 → shift(1)=NaN, shift(2)=NaN
第2个人: 前面是第1个人 → shift(1)=第1个, shift(2)=NaN
第3个人: 前面是第2个人 → shift(1)=第2个, shift(2)=第1个
第4个人: 前面是第3个人 → shift(1)=第3个, shift(2)=第2个
对应:
- shift(1): 前一个
- shift(2): 前两个
- shift(3): 前三个
""")
queue_analogy()
# 7. 处理NaN值
print("\n=== 7. 处理NaN值 ===")
def handle_nan(data):
"""处理NaN值"""
feature_data = data.copy()
# 添加窗口字段
feature_data['prev_1'] = feature_data['power_load'].shift(1)
feature_data['prev_2'] = feature_data['power_load'].shift(2)
feature_data['prev_3'] = feature_data['power_load'].shift(3)
print(f"处理前NaN数量:")
print(feature_data.isnull().sum())
# 方法1:删除NaN
feature_data_drop = feature_data.dropna()
print(f"\n删除NaN后形状: {feature_data_drop.shape}")
# 方法2:填充NaN
feature_data_fill = feature_data.fillna(method='bfill') # 向后填充
print(f"填充NaN后形状: {feature_data_fill.shape}")
return feature_data_drop
clean_data = handle_nan(data)
# 8. 完整特征工程(含窗口字段)
print("\n=== 8. 完整特征工程(含窗口字段)===")
def complete_feature_engineering(data, logger=None):
"""
完整特征工程
Args:
data: 数据源
logger: 日志对象
Returns:
feature_data: 处理后的数据
"""
if logger:
logger.info("开始特征工程")
feature_data = data.copy()
# 1. 提取hour和month
feature_data['hour'] = feature_data['time'].dt.hour
feature_data['month'] = feature_data['time'].dt.month
# 2. One-Hot编码
feature_data = pd.get_dummies(feature_data, columns=['hour', 'month'])
# 3. 添加窗口字段
feature_data['prev_1'] = feature_data['power_load'].shift(1)
feature_data['prev_2'] = feature_data['power_load'].shift(2)
feature_data['prev_3'] = feature_data['power_load'].shift(3)
# 4. 删除NaN
feature_data = feature_data.dropna().reset_index(drop=True)
if logger:
logger.info(f"特征工程完成,形状: {feature_data.shape}")
print(f"特征工程后形状: {feature_data.shape}")
print(f"所有列名: {list(feature_data.columns)}")
return feature_data
# 重新生成数据(datetime格式)
data = generate_power_load_data(n_days=7)
final_data = complete_feature_engineering(data, logger)
# 9. shift函数应用场景
print("\n=== 9. shift函数应用场景 ===")
def shift_applications():
"""shift函数应用场景"""
print("""
shift函数应用场景:
1. 时序预测
- 获取前n个时间步的值
- 作为特征输入模型
2. 计算变化率
- 当前值 - 前一个值
- 增长率 = (当前 - 前一个) / 前一个
3. 移动平均
- shift(1) + shift(2) + shift(3) / 3
- 前3个时间步的平均值
4. 滞后特征
- shift(n) 创建滞后n步的特征
- 捕捉时间依赖性
""")
shift_applications()
# 10. 完整代码
print("\n=== 10. 完整代码 ===")
def complete_code():
"""完整代码"""
print("""
def add_window_features(feature_data, logger):
\"\"\"添加窗口字段\"\"\"
logger.info("开始添加窗口字段")
# 添加前1、前2、前3个小时的负荷
feature_data['prev_1'] = feature_data['power_load'].shift(1)
feature_data['prev_2'] = feature_data['power_load'].shift(2)
feature_data['prev_3'] = feature_data['power_load'].shift(3)
logger.info("窗口字段添加完成")
return feature_data
# 调用
feature_data = add_window_features(feature_data, logger)
""")
complete_code()
# 11. 总结
def window_features_summary():
"""窗口字段总结"""
print("=" * 50)
print("窗口字段总结 - 添加上n小时负荷")
print("=" * 50)
print("\n1. 概念")
print(" 窗口字段: 上n小时的负荷特征")
print("\n2. 函数")
print(" shift(n): 数据移位n步")
print("\n3. 使用")
print(" prev_1 = shift(1): 上一个小时")
print(" prev_2 = shift(2): 上两个小时")
print(" prev_3 = shift(3): 前三个小时")
print("\n4. NaN处理")
print(" shift产生NaN")
print(" dropna()删除")
print(" fillna()填充")
print("\n" + "=" * 50)
print("窗口字段添加完成!")
print("=" * 50)
window_features_summary()
输出示例:
=== 3. shift函数详解 ===
原始数据(前10行):
time power_load
0 2024-01-01 00:00:00 524.56
1 2024-01-01 01:00:00 489.12
2 2024-01-01 02:00:00 456.78
...
--- shift(1): 上一个小时 ---
power_load shift_1
0 524.56 NaN
1 489.12 524.56
2 456.78 489.12
...
=== 4. 添加窗口字段 ===
添加窗口字段后(前10行):
time power_load prev_1 prev_2 prev_3
0 2024-01-01 00:00:00 524.56 NaN NaN NaN
1 2024-01-01 01:00:00 489.12 524.56 NaN NaN
2 2024-01-01 02:00:00 456.78 489.12 524.56 NaN
3 2024-01-01 03:00:00 423.45 456.78 489.12 524.56
...
5 重难点与易错提醒
- ❗重点:shift函数用于数据移位。
- ❗重点:shift(1)是上一个时间步,shift(n)是前n个时间步。
- ❗重点:shift会产生NaN,需要处理。
- ❗重点:窗口字段是时序预测的重要特征。
- ⚠️易错:shift方向理解错误(向下移动)。
- ⚠️易错:忘记处理NaN值。
- 💡深入理解:窗口字段捕捉时间依赖性。
6 课堂问答精选
Q: shift函数的作用是什么?
A: shift函数用于数据移位:
- shift(1):将数据向下移动1位,获取上一个时间步的值
- shift(2):将数据向下移动2位,获取上两个时间步的值
- shift(n):将数据向下移动n位,获取前n个时间步的值
示例:
原始: shift(1):
800 NaN
900 800
1000 900
Q: shift后为什么有NaN?
A: 原因:
- shift(1):第一行没有前一个,所以是NaN
- shift(2):前两行没有上上一个,所以是NaN
- shift(n):前n行没有前n个,所以是NaN
处理方法:
dropna():删除NaNfillna():填充NaN
7 本课小结
- 概念:窗口字段是上n小时的负荷。
- 函数:
shift(n)数据移位。 - 使用:shift(1)、shift(2)、shift(3)。
- NaN:shift产生NaN,需处理。
- 类比:排队,第一个人前面没人。
8 延伸思考与实践
- 实践:运行shift函数示例。
- 预习:昨日同时刻负荷。
- 思考:窗口大小如何选择?