☰
新能源充电站用电量预测:从数据清洗到LightGBM基线模型实战
2026/10/20 8:57:18 网站建设 项目流程

简介:新能源充电站用电量预测数据集面向电动车充电负荷分析场景,服务于研究人员、算法工程师及电网运营人员,可用于时间序列预测、负荷特征挖掘与站点运营优化等任务。包体内含32个文件,以Python脚本、CSV表格及备份文件为主,其中Python代码覆盖数据加载、模型构建、特征工程与评估环节,CSV提供珀斯、帕洛阿尔托、博尔德等多地充电站负荷记录,并附带说明文档与负荷曲线图,便于快速上手复现预测流程。压缩包整体1.17MB,结构清晰,已吸引116人学习下载。借助该数据集,读者可开展多站点用电量对比、节假日与气象因素影响分析,并尝试搭建LSTM、注意力机制等预测模型,从而深化对充电负荷规律的理解,为充电站资源调配与电网负载均衡提供数据支撑。

1. 这份「新能源充电站用电量预测数据集」到底能算出什么

新能源充电站用电量预测这个任务,难的不是算法,而是数据集质量与影响因素分析的颗粒度。我拆过一份来自某区域充电运营平台的逐小时用电量数据集,字段从电量、订单数、电价一路延伸到天气、节假日和区域类型,配套脚本把清洗、特征加工、影响因素分析和基线建模整条链路都搭好了。它能回答三个具体问题:这座站的用电曲线为什么长这样、下一个月的某天大概充多少电、扩容时该按哪个分位数留冗余。适合做充电负荷预测、容量规划或峰谷电价模拟的从业者,也适合想在时间序列上练手的数据分析师。先提醒一句:这份数据不是拿来就跑算法的,字段理不顺,后面全是坑。

2. 读懂原始时间序列表:字段含义、粒度选择与时区修正

数据集不是一坨 CSV 倒进来就能建模。拿到手先花半天把表结构、采样粒度和时间戳的时区问题理清楚,后面能少走很多弯路。这一章把原始字段逐一拆开,再给出粒度选择的理由和时区修正的标准做法。

2.1 原始表字段说明

原始数据是一张大宽表,一行就是一个站点在一个小时内的统计记录。字段设计参考了实际充电运营平台的常见导出结构,整理成下表。

字段名类型示例含义
station_idstrST-017站点编码
ts_localdatetime2024-03-15 14:00:00本地时间,小时整点
energy_kwhfloat34.6该小时全站充电量,单位 kWh
order_cntint12该小时启动的充电订单数
active_pilesint8该小时平均在线充电桩数
avg_power_kwfloat42.3该小时订单平均充电功率
price_yuanfloat1.02该小时执行的售电价,元/kWh
temp_cfloat18.5整点气温
precip_mmfloat0.0小时累计降水量
wind_speed_msfloat2.3小时平均风速
is_workdayint1修正后是否工作日
is_holidayint0是否法定节假日
area_typestroffice区域类型:office/residential/highway/commercial

几个隐蔽点提前说。order_cnt 统计的是「启动订单」而不是「完成订单」,一辆车跨小时充满时,订单归属在启动时刻,这会导致整点边界上电量与订单数不完全对齐。active_piles 会随着桩故障、维护而波动,分析单桩平均电量时必须先按站点对总量做归一化,否则桩数下降会让单桩电量虚高。price_yuan 在一天内是分时段跳变的,不是平滑曲线,做特征时最好把时段边界也拆出来,而不是只用一个连续数值。

2.2 为什么从小时粒度开始

原始数据里其实还有 15 分钟粒度的版本,但我的建议是先把预测任务定在小时级。15 分钟数据噪音大到什么程度:一辆出租车突然进站,功率拉到 80kW,半小时后走,那个 15 分钟窗口电量可能就是 30 度,旁边两个窗口只有 2-3 度,相邻窗口波动超过 5 倍。再加上订单计费周期和平台上报延迟,跨整点的电量归属经常是乱的,用这种数据建模,模型会把大量精力花在拟合随机抖动上。

日粒度倒是平稳,但一天一个点,日内削峰填谷的信息全丢了,分时定价模拟完全没法做。小时粒度是折中方案:既能看见 10 点到 11 点、18 点到 20 点这类行为尖峰,又不会被单笔订单的随机性带偏。

粒度波动性信息量适合任务
15 分钟高细桩级调度、功率控制
1 小时中中用电量预测、扩容规划
1 天低低长期容量趋势判断

2.3 时间戳时区:第一个隐形的脏数据来源

不少平台的底层数据存的是世界时,展示时才转成当地标准时间。如果直接用原始时间戳做日期映射,所有峰值都会整体偏移 8 小时,节假日标记也会错位。我一般会在数据读入后第一时间做时区转换,并顺手拆出 hour 和 weekday 两个衍生字段。

import pandas as pd # 原始时间戳为世界时,先转成当地标准时间,再存为不带时区的datetime df['ts_local'] = (pd.to_datetime(df['ts_utc']) .dt.tz_localize('UTC') .dt.tz_convert('Asia/Shanghai') .dt.tz_localize(None)) df['hour'] = df['ts_local'].dt.hour df['weekday'] = df['ts_local'].dt.weekday

这段代码的逻辑是:tz_localize('UTC') 先声明原始字符串是世界时,tz_convert('Asia/Shanghai') 完成时区换算,最后的 tz_localize(None) 把时区信息剥离,避免后续与不带时区的天气表合并时抛异常。hour 和 weekday 必须在时区修正之后拆,否则早晚高峰会整体错位,节假日特征也废掉。遇到夏令时地区还得额外处理,国内倒是没有这个问题。

3. 预处理与特征加工:把原始电量变成能训练的时间序列样本

字段理清后,下一步是清脏数据、合并外部特征、构造滞后特征。这一章给出一套我常用的加工逻辑,代码按步骤走,可以直接套在类似数据集上。

3.1 缺失值填充:区分「夜里没人充」和「宕机缺数」

充电站的数据缺失通常有两种来源:一种是站点宕机或网络中断导致整小时没有任何上报,另一种是站点本身没车充,电量本来就是 0 或接近 0。这两者处理方式完全不同。把宕机时段误填成 0,模型会学会「这个站深夜经常低谷」,但真实情况是那几小时有需求却没服务;把深夜低电量当成缺失去插值,又会凭空造出一段平滑曲线,掩盖夜间需求几乎为零的事实。

我的做法是:先看原始值,真 0 与 NaN 分开标记,NaN 用前 7 天同时刻的中位数填充,同时加一列 is_missing 让模型知道这个值是补出来的。

# 按站分组,用前7天同时刻的中位数填充缺失值 df = df.sort_values(['station_id', 'ts_local']) df['is_missing'] = 0 for sid, g in df.groupby('station_id'): mask = g['energy_kwh'].isna() med = (g.set_index('ts_local')['energy_kwh'] .rolling('7D', min_periods=3, center=False) .median()) df.loc[mask, 'energy_kwh'] = med[mask] df.loc[mask, 'is_missing'] = 1

rolling 窗口写成 '7D' 而不是固定行数,是因为序列里本身有 NaN,按行数滚动会导致窗口内样本数不对。min_periods=3 保证早期数据不足 7 天时也能算出中位数。center=False 表示只用过去的数据,避免引入未来信息。连续缺失超过 24 小时的情况不要插值,直接把该站点这一段剔除,或者单独建一个宕机事件标记,否则插值会把一次完整宕机平滑成低负荷假象,模型会在关键时刻给出乐观预测。

3.2 天气与节假日特征合并

天气数据来自气象站整点观测,充电数据是整点快照,直接按时间戳合并即可。合并时有三个细节值得注意。

温度直接进模型没问题,但我通常额外加一列 temp_sq 二次项。原因是充电负荷与温度的关系不是线性的,空调负荷在高温和低温两端才启动,中间温度段基本无感,关于这一点第 5 章会展开讲。节假日标记必须做调休修正,补班日要标成工作日而不是休息日,否则节前节后的预测会系统性偏低。跨年数据要更新调休表,上一年的放假安排不能直接套到下一年。

# 天气表按观测时间对齐 weather = weather.rename(columns={'obs_time': 'ts_local'}) df = df.merge( weather[['ts_local', 'temp_c', 'precip_mm', 'wind_speed_ms']], on='ts_local', how='left' ) df['temp_sq'] = df['temp_c'] ** 2 # 调休表手动维护,包含date, is_workday, is_holiday三列 df = df.merge( holiday_cal[['ts_local', 'is_workday', 'is_holiday']], on='ts_local', how='left' )

两个 merge 都用了 how='left',保证充电站记录一条不丢。调休表建议每年初根据当年的放假通知手工整理,别完全依赖通用日历包,通用日历对调休补班日的处理经常滞后。合并后检查一下 is_workday 与 is_holiday 是否互斥,正常情况两者不能同时为 1,如果出现就说明调休逻辑有冲突。

3.3 滞后特征与滑动窗口:短期惯性是最强信号

时间序列的自相关经常被低估。我在类似数据上算过,当前小时电量对上一小时电量的相关性在 0.75 到 0.9 之间,远超天气、价格这些外部特征。不构造滞后特征,模型只能学到天气和工作日的平均水平,预测结果是一条平滑曲线,遇到尖峰日必然翻车。

滞后特征的构造要按站点分组后 shift,不能直接在整个表上 shift,否则跨站点的数据会串位。

# 按站构造滞后与滚动特征 df['lag1'] = df.groupby('station_id')['energy_kwh'].shift(1) df['lag2'] = df.groupby('station_id')['energy_kwh'].shift(2) df['lag24'] = df.groupby('station_id')['energy_kwh'].shift(24) df['rolling24_mean'] = (df.groupby('station_id')['energy_kwh'] .transform(lambda x: x.rolling(24, min_periods=1).mean())) df['rolling7d_max'] = (df.groupby('station_id')['energy_kwh'] .transform(lambda x: x.rolling(24 * 7, min_periods=1).max()))

lag1 和 lag2 捕捉短期惯性,lag24 是对应「昨天同一时刻」的基准值,rolling24_mean 相当于前一天的负荷水平,用来修正趋势漂移。rolling7d_max 是过去一周的峰值,对扩容场景特别有用。

滞后特征是双刃剑:如果预测目标是未来 24 小时,训练时 lag1 可以用,但真正上线做多步预测时,未来 24 小时的上一小时值是未知的,你得用递归预测方式,先用预测值当 lag1 再推下一步。所以上游特征准备好之后,先想清楚预测步长,再决定保留哪些滞后项。常见做法是多保留 lag24 和滚动均值,少依赖 lag1,预测稳定性会好很多。

4. 影响因素分析:相关性矩阵、分区域拆解与基线模型

数据集配套的分析报告把「哪些因素真的影响用电量」拆成了三件事:全局相关性、分区域曲线、以及一个能跑的基线模型。先有基线和结论,再谈调优,这是我一直坚持的顺序。

4.1 相关性矩阵:三个反直觉发现

我在清洗后的数据上算过 Pearson 相关性,有几个发现值得记下来。

温度与电量的线性相关接近 0,但把样本按夏季和冬季分组,平均电量差异远大于 0.1 的相关系数对应的水平。这是非线性关系的典型表现,高温和低温两端才有空调负荷,中间 15-25 度是平台期,线性相关被平台期稀释了。第二个发现是价格与电量负相关但很弱,约 -0.15,说明充电需求对电价的短期弹性很低,车主主要还是受续航焦虑驱动,分时电价影响的是「什么时候充」而不是「充不充」。第三个发现是上一小时电量与当前电量的相关性最高,超过 0.8,秒杀所有外部特征。

特征与 energy_kwh 的 Pearson r判断
lag10.84极强,主要预测力
is_workday0.28中等
price_yuan-0.15弱
temp_c0.02弱,但不能直接丢弃
precip_mm-0.08弱

相关性不等于因果,但可以用来定特征优先级。看到 lag1 这么强,第一直觉应该是把序列模型的方向做扎实,而不是急着堆外部特征。

4.2 分区域拆解:办公、居民、高速、商业的曲线完全不同

全局相关性掩盖了站点差异。我把 area_type 分组后画平均日负荷曲线,四种区域的形态差异非常明显。

办公区的工作日白天有 10-11 点和 14-15 点两个小高峰,周末电量掉到工作日的四成左右,典型的通勤充电结构。居民区是 18 点到 22 点一个宽峰,凌晨 0 点到 6 点几乎贴地,很多用户习惯下班到家后插枪。高速服务区全天平缓,昼夜波动不大,但节假日会出现翻倍尖峰,工作日和周末反而差不多。商业区则是周末 14 点到 20 点明显抬升,工作日一般。

拿全局模型去拟合这四种曲线,结果必然是「平均站」——每个站都预测得不好不坏。常见做法有两种:一是把 area_type 作为分类特征丢给树模型,让模型自己分裂;二是按区域类型各训一个模型再对比。数据量够的时候,我一般先按区域切三份,对比全局模型与分区模型的 RMSE,分区明显占优就改成多模型方案。

4.3 基线模型:LightGBM 加时间切分

基线模型我选了 LightGBM,原因很直接:能处理分类特征,对非线性拟合好,训练快,还自带特征重要性。切分必须按时间顺序,不能随机打散。

from lightgbm import LGBMRegressor feats = ['price_yuan', 'temp_c', 'temp_sq', 'precip_mm', 'wind_speed_ms', 'is_workday', 'is_holiday', 'hour', 'weekday', 'area_type', 'lag1', 'lag24', 'rolling24_mean', 'rolling7d_max'] split = int(len(df) * 0.8) df_train = df.iloc[:split] df_test = df.iloc[split:] model = LGBMRegressor( n_estimators=600, learning_rate=0.05, num_leaves=31, subsample=0.8, colsample_bytree=0.8, random_state=42 ) model.fit(df_train[feats], df_train['energy_kwh']) imp = pd.Series(model.feature_importances_, index=feats) print(imp.sort_values(ascending=False).head(10))

n_estimators=600 配合 learning_rate=0.05,是防止一上来就过拟合的常规组合。num_leaves=31 是默认复杂度,对这个量级的小时数据足够。subsample 和 colsample_bytree 都设 0.8,让每棵树看的样本和特征略少一点,增强泛化。area_type 要转成 pandas 的 category 类型再传给 LightGBM,否则会被当成普通数值。

评估也用时间一致的原则,直接在测试集上算 RMSE 和 R²。

from sklearn.metrics import mean_squared_error, r2_score pred = model.predict(df_test[feats]) rmse = mean_squared_error(df_test['energy_kwh'], pred, squared=False) r2 = r2_score(df_test['energy_kwh'], pred) print('RMSE(kWh/h):', round(rmse, 2)) print('R2:', round(r2, 3))

站点混合数据能做到 R² 0.85-0.9、RMSE 在 5-15 之间,就算合格基线。但注意不同站的水平不一样,有的站平均每小时才 5 度电,有的站 40 度电,混合算 RMSE 会被大站主导,所以下一步一定要分站看误差。

5. 避坑与常见问题:五个让我预测结果虚高的翻车点

这一章是血泪经验汇总。每个坑都在真实数据上遇到过,按「现象 → 原因 → 解决」写,对照检查能省很多返工时间。

5.1 随机切分导致数据泄露,验证指标虚高

现象:把数据集随机打散成 80% 训练、20% 测试,R² 直接到 0.97,信心满满上线,结果掉到 0.75。

原因:时间序列样本之间存在强自相关,随机切分会让同一周相邻小时的样本分别出现在训练集和测试集里,模型相当于「见过」测试时段附近的形态。更糟的是滞后特征会把未来信息带进训练集,lag1 在当前样本中包含了上一小时的真实电量,如果上一小时碰巧在测试集里,信息就泄露了。

解决:切分必须按时间顺序,前 80% 训练、后 20% 测试。更严格的做法是滚动验证,每次只允许用截止时间之前的数据训练,从第 8 天开始逐天后移,评估多步预测稳定性。从那以后我每次写评估代码,第一行就先确认有没有 sort_values(['station_id', 'ts_local']),再决定 split 的位置。

5.2 节假日表没做调休修正,节前节后残差尖峰

现象:长假前一周的周末补班日,模型预测值比实际低 30%,分析师第一反应是「节前效应」,实际上是日历标错了。

原因:通用日历包把补班日标成休息日,is_workday=0。模型以为大家不上班,实际通勤充电照常,电量一点没少。

解决:手动维护当年的调休明细表,字段包括日期、修正后的 is_workday、is_holiday。规则是补班日一律 is_workday=1,法定假期按实际放假日期置 is_holiday=1,并且补班判定优先级要高于节假日判定。简单检查方法是看国庆前一周的周末残差是否连续为正,如果是,多半是调休表没更新。

5.3 温度相关性接近 0,不等于温度不重要

现象:某次分析中看到 temp_c 与电量的 Pearson r 只有 0.02,顺手把温度特征删了,结果夏季预测全面低估,冬季又高估。

原因:空调负荷只在高温和低温两端启动,中间 15-25 度是平台期,线性相关被大量平台期样本稀释。U 型关系用线性相关系数表达不出来,数值上看起来「无关」,实际影响很大。

解决:加 temp_sq 二次项,把 U 型关系暴露给模型;或者不处理,直接让树模型自己找分裂阈值。验证时按季度分组看误差,夏季误差明显偏大,就要回到温度特征检查是否只用了线性项。我一般会在特征清单里保留 temp_c 和 temp_sq 两个字段,让模型自己决定用哪个、怎么组合。

5.4 站点突发结构变化,全局模型把突变平滑掉

现象:某站从 8 月开始残差连续为正,单站 RMSE 是全局水平的三倍,全局模型对该站几乎是废的。

原因:站点附近新开了商场、修路封闭了通道、或者隔壁另一个站停运,都可能导致需求结构永久改变。模型用历史均值外推,自然追不上新形态。

解决:训练前按站点做突变检测,计算每个站点 7 日均值与 30 日均值的比值,超过 1.5 倍就标记为突变点。突变之后的站点只用突变后的数据重新训练,或者在特征里加一个 event_dummy 事件标记。数据量少的站点,宁可放弃长期历史,也别让旧分布污染新分布。

5.5 MAPE 在深夜低电量时爆炸,整体指标报不了数

现象:某站 0 点到 4 点每小时电量只有 0.03-0.5 kWh,预测 0.1,MAPE 直接 300%,整体 MAPE 被拉垮,白天预测得再好也显不出来。

原因:MAPE 的分母是真实值,低电量小时除出来就是巨大百分比,个别异常样本能把整体指标拖垮。

解决:按站点过滤 energy_kwh 小于 1 kWh 的样本后再算 MAPE;或者换 WAPE,分母用所有样本真实值之和,不会被单个小值绑架。更稳的做法是白天和夜间分开报指标,白天看 RMSE,夜间看平均绝对误差,各说各话。评估报告里我一般先给整体 RMSE,再给「白天 RMSE」和「夜间 MAE」两个分项,这样报告才有说服力。

6. 验证与进阶:残差分析决定模型能不能上线

模型训练完,第一步不是看 R²,而是看残差。我习惯先做分时残差和分站残差两张检查,这决定了模型是能上线还是要回炉。

df_test = df_test.copy() df_test['pred'] = model.predict(df_test[feats]) df_test['resid'] = df_test['energy_kwh'] - df_test['pred'] hour_err = df_test.groupby('hour')['resid'].mean().round(2) station_err = df_test.groupby('station_id')['resid'].mean().round(2) print(hour_err) print(station_err)

hour_err 告诉你系统性偏差:如果某些小时残差持续为正,说明模型在这些时段系统性低估,可能是通勤特征没表达好。station_err 告诉你哪座站需要单独处理:某站残差均值超过 5 kWh,直接对它单独建模,别拖累全局。

进阶一步是滚动回测。先把数据按站点拆好,以 7 天为步长,训练集从第 1 天滚到第 N-7 天,每次预测未来 24 小时,把多步预测结果拼起来再算整体误差。这个流程比一次性切分更能反映真实上线表现,因为上线时你永远不会知道「明天真实的 lag1」。

从那以后我每次交付充电负荷预测脚本,都强制走一遍「分时残差 + 分站残差 + 滚动回测」三件套,看到残差在 0 附近均匀波动才敢写结论。新能源充电站用电量预测这个场景,数据集质量决定了预测上限,影响因素的取舍决定了你离上限有多远。这份数据集的字段说明、清洗脚本和基线模型代码都收在下载包里,照着跑一遍,你也能快速判断自己手头的数据能不能支撑同样的分析。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询