蒙特卡洛模拟做充电负荷预测,这活儿听起来挺唬人,其实就是把“电动汽车用户群体”这头大象,用统计学的方式切成一片片,然后扔进计算机里模拟出几万种可能的日常,最后把这些日常叠在一起看整体效果。
我做这个项目的时候,最开始也走了不少弯路。比如一上来就想着用复杂的交通流仿真、时空OD矩阵,结果数据搞不到,模型也调不通。后来想明白了,做区域级、宏观层面的充电负荷预测,压根不需要那么复杂,关键是抓住用户充电行为的概率分布,蒙特卡洛天然就是干这个的。
这篇文章我把当时整个项目的思路、模型搭建、代码实现和踩过的坑都梳理一遍,希望能帮你少走点弯路。
1. 整体设计思路:为什么蒙特卡洛适合干这个活
1.1 充电负荷问题的本质是“群体行为叠加”
单个电动车啥时候充电、充多久、功率多大,完全是随机事件。但是一万辆电动车呢?十万辆呢?就会发现群体行为呈现明显的统计规律:下班后18点到20点是私家车充电高峰,凌晨低谷,白天商用快充桩有一波小高峰。
我们要预测的不是“某辆车几点充电”,而是“整个区域在某个时刻的总充电功率”。这是一个典型的随机过程叠加问题,解析方法很难处理,因为各种随机因素——起始充电时间、日行驶里程、电池SOC、充电功率、充电时长——相互纠缠,很难用一两个公式描述清楚。
蒙特卡洛的思路就简单粗暴:既然解析解难求,那就把每个车主的充电行为按照概率分布随机抽样,模拟成千上万次,每次模拟都得到一条负荷曲线,最后把所有模拟结果取平均或者看概率区间。模拟次数足够多的时候,结果会收敛到真实情况的近似解。
1.2 蒙特卡洛的适用边界
用蒙特卡洛之前得想清楚:你要算的是区域级总量负荷,还是某条线路、某个小区配电台区的负荷?
- 区域级/城市级充电负荷预测:非常适合,因为地域广、车辆多,个体随机性在统计意义上被抹平,收敛速度快。
- 单个小区/停车场负荷:勉强能用,但需要精细的车辆到达数据,否则误差很大。
- 单台充电桩负荷:完全不适合,蒙特卡洛模拟的是分布,不是单个样本的精确行为。
我做的是城市某区域的充电负荷预测,模拟区域内有约1.2万辆电动车,这个尺度用蒙特卡洛正合适。
1.3 核心变量梳理
蒙特卡洛模拟的质量完全取决于输入概率分布的准确性。我把它拆成四类变量:
车辆状态变量
- 电池容量(kWh):不同类型车差异巨大,家用车40~80kWh,出租车/网约车50~90kWh,公交车150~350kWh。
- 初始SOC(%):充电起始时的剩余电量,受前一天驾驶习惯影响。
- 百公里电耗(kWh/100km):影响充电需求量的关键参数。
用户行为变量
- 到达充电站/开始充电时间:城市居民通勤习惯决定。
- 日行驶里程:可以用出行调查数据拟合,跟车辆用途强相关。
- 充电方式偏好:快充还是慢充,和车辆剩余电量、时间紧迫程度有关。
充电设施参数
- 充电功率等级:交流慢充7kW,直流快充60kW、120kW甚至更高。
- 充电效率:一般0.9~0.95之间,交流略低直流略高。
- 充电桩数量与分布:决定排队概率,但这个属于后续拓展。
时间与空间参数
- 模拟时间粒度:一般15分钟或30分钟一个点。
- 模拟区域范围:影响车辆规模与充电行为特征。
- 季节/温度修正:冬季续航衰减明显。
2. 模型搭建:概率分布选取是关键
2.1 车辆分类不能偷懒
把1.2万辆车当成一个整体来处理是不行的。私家车、出租车、网约车、公交车,充电行为完全两个世界:
| 车辆类型 | 保有量占比 | 充电时段特征 | 主要充电方式 | 电池容量典型值 |
|---|---|---|---|---|
| 电动私家车 | 约65% | 夜间18:00~次日8:00为主 | 目的地慢充 | 60kWh |
| 出租车/网约车 | 约25% | 白天随时补电,夜间换班充电 | 快充为主 | 60~80kWh |
| 电动公交车 | 约10% | 夜间集中充电,白天快补 | 场站慢充+快充 | 250kWh |
类型不分开,模拟出来的负荷曲线会四不像——没有晚高峰,也没有白天小高峰,就是一团糊。
2.2 起始充电时间的分布拟合
这是整个模型最重要的输入参数,直接决定负荷曲线的形状。
私家车:符合双峰分布。晚高峰(下班到家)占大头,符合正态分布,均值在19:00左右,标准差约2小时;早高峰(到单位补电)是次峰,主要集中在8:00~10:00。实践中用高斯混合模型(GMM)拟合历史充电数据。
出租车/网约车:几乎全天均匀,但在14:00~16:00有一个换班/交班补电高峰,凌晨2:00~4:00有一个收车后的集中充电高峰。
公交车:非常规律,集中在22:00到次日4:00,基本可以按固定时段处理,只加小幅度随机抖动。
2.3 日行驶里程的对数正态分布
行驶里程直接决定充电需求量。不同地区数据差异大,必须用本地数据。我用的拟合结果:
- 私家车:对数正态分布,均值约35公里,标准差约18公里。周末里程通常高于工作日。
- 出租车/网约车:均值约250公里,标准差约60公里,基本每天要充一次或两次。
- 公交车:固定线路,约180~200公里,方差很小。
注意:如果当地没有实际调研数据,可以参考同类城市的公开出行报告,但一定注明数据来源和适用场景。乱编一个分布参数会导致模型输出完全失真。
2.4 初始SOC的推算逻辑
初始SOC不是独立抽样得到的,它是“日行驶里程”和“电池容量”联动的结果:
初始SOC = 1 - (日行驶里程 × 百公里电耗) / 电池容量也就是说,里程开得多、电池小、电耗高,初始SOC就低,充电需求就大。这就是为什么不能把每个变量独立抽样的原因——变量之间存在物理约束关系。
还得加一个下限截止:如果算出来SOC低于10%(极端情况,比如跑太多电耗太高),就按10%算,因为车主通常不会把电彻底跑光。
当天充满后,第二天初始SOC回满,模拟按天滚动进行。我这里是做单日稳态模拟,没有做逐日滚动,因为区域级负荷曲线更关心日内的形态和总量,逐日滚动更多用于长期进化模拟。
2.5 充电功率的选择逻辑
充电功率不是单纯按车类型分的,还跟充电场景强相关。我引入了“充电行为蒙特卡洛”里的一个关键概率矩阵:给定当前SOC和目标SOC,用户偏好选择快充或慢充。
简化做法是:
- SOC < 30%:大概率选择快充(可能是途中紧急补电),快充概率约0.7。
- 30% < SOC < 60%:快充概率约0.4。
- SOC > 60%:快充概率约0.1,基本都是慢充。
这个概率矩阵是对公桩快充、私桩慢充场景下的经验修正。不同城市、不同场景要把这个矩阵重新标定。
3. 蒙特卡洛实现:代码架构与核心细节
3.1 整体流程
我用Python实现,核心架构分四步:
- 参数初始化:输入车辆规模、车型占比、概率分布参数、充电桩配置。
- 单日模拟:为每辆车抽样行为变量,计算充电开始时间和充电曲线,累加到日负荷曲线上。
- 多次重复:重复模拟N次(我取500次),每次随机种子独立。
- 统计输出:计算各时刻负荷均值、标准差、95%置信区间。
3.2 核心代码框架
车辆行为抽样的核心逻辑如下(这是简化可运行的版本):
import numpy as np import pandas as pd import matplotlib.pyplot as plt # 固定随机种子,保证结果可复现 np.random.seed(42) # ---------- 参数配置 ---------- SIM_TIMES = 500 # 蒙特卡洛模拟次数 TIME_POINTS = 96 # 24小时,每15分钟一个采样点 EV_COUNT = 12000 # 区域电动车保有量 # 车型占比(私家车、出租/网约、公交) CAR_SHARE = {'private': 0.65, 'taxi': 0.25, 'bus': 0.10} # 电池容量典型值(kWh) BATTERY_CAP = {'private': 60, 'taxi': 70, 'bus': 250} # 百公里电耗(kWh/100km) ENERGY_CONSUMPTION = {'private': 15, 'taxi': 16, 'bus': 120} # 充电功率档位(kW) CHARGE_POWER = {'slow': 7, 'fast': 60} # 充电效率 EFFICIENCY = 0.92 # ---------- 概率分布参数 ---------- # 私家车开始充电时间:高斯混合模型三参数 PRIVATE_TIME_MEAN1, PRIVATE_TIME_STD1 = 19.0, 2.0 PRIVATE_TIME_MEAN2, PRIVATE_TIME_STD2 = 8.5, 1.5 PRIVATE_GMM_WEIGHT = 0.75 # 晚峰占比 # 出租车开始充电时间:全天均匀 + 16点小高峰 TAXI_PEAK_TIME = 16.0 # 日行驶里程对数正态分布参数(均值、标准差均是实际里程,做log转化) DAILY_MILEAGE = { 'private': {'mu': np.log(35), 'sigma': 0.45}, 'taxi': {'mu': np.log(250), 'sigma': 0.25}, 'bus': {'mu': np.log(190), 'sigma': 0.10}, } def sample_start_time(car_type): """抽样起始充电时间(小时),返回0-24之间的浮点数""" if car_type == 'private': # 高斯混合模型 if np.random.rand() < PRIVATE_GMM_WEIGHT: t = np.random.normal(PRIVATE_TIME_MEAN1, PRIVATE_TIME_STD1) else: t = np.random.normal(PRIVATE_TIME_MEAN2, PRIVATE_TIME_STD2) # 截断到0-24 return np.clip(t, 0, 24) elif car_type == 'taxi': # 全天均匀 + 16点附近的高峰 base = np.random.uniform(0, 24) peak_add = np.random.normal(TAXI_PEAK_TIME, 1.2) # 50%的概率叠加小高峰 if np.random.rand() < 0.4: return np.clip(peak_add, 0, 24) return base else: # bus # 夜间集中充电,22点-次日4点 t = np.random.uniform(22, 28) # 超过24的统一减24 return np.clip(t - 24 if t > 24 else t, 0, 24) def sample_daily_mileage(car_type): """抽样日行驶里程""" params = DAILY_MILEAGE[car_type] return np.random.lognormal(params['mu'], params['sigma']) def simulate_single_day(): """模拟一天内所有车辆的充电负荷曲线""" load_curve = np.zeros(TIME_POINTS) for car_type, share in CAR_SHARE.items(): car_count = int(EV_COUNT * share) cap = BATTERY_CAP[car_type] consumption = ENERGY_CONSUMPTION[car_type] for _ in range(car_count): # 1. 抽样起始充电时间 start_hour = sample_start_time(car_type) # 2. 抽样日行驶里程 mileage = max(sample_daily_mileage(car_type), 0.1) # 3. 计算初始SOC soc_consumed = (mileage * consumption / 100) / cap init_soc = max(1 - soc_consumed, 0.1) # 4. 计算充电需求电量 target_soc = 0.95 # 目标充到95% energy_needed = (target_soc - init_soc) * cap # 5. 选择充电功率 if init_soc < 0.3: power = CHARGE_POWER['fast'] if np.random.rand() < 0.7 else CHARGE_POWER['slow'] elif init_soc < 0.6: power = CHARGE_POWER['fast'] if np.random.rand() < 0.4 else CHARGE_POWER['slow'] else: power = CHARGE_POWER['slow'] # 6. 计算充电时长 charge_hours = energy_needed / power / EFFICIENCY charge_interval = int(charge_hours * 4) # 换算成15分钟间隔数 # 7. 叠加到负荷曲线 start_idx = int(start_hour * 4) % TIME_POINTS for i in range(charge_interval): idx = (start_idx + i) % TIME_POINTS load_curve[idx] += power # 简化处理:实际中充到目标SOC后会停止 return load_curve # ---------- 蒙特卡洛主循环 ---------- all_results = np.zeros((SIM_TIMES, TIME_POINTS)) for sim in range(SIM_TIMES): all_results[sim] = simulate_single_day() if (sim + 1) % 100 == 0: print(f"已完成 {sim + 1} 次模拟") # ---------- 统计结果 ---------- mean_load = np.mean(all_results, axis=0) std_load = np.std(all_results, axis=0) p95_low = np.percentile(all_results, 2.5, axis=0) p95_high = np.percentile(all_results, 97.5, axis=0) # 时间轴(小时) time_axis = np.arange(0, 24, 0.25)3.3 关键细节:为什么充电时长要按“功率下取整”
上面代码里有个细节:charge_interval = int(charge_hours * 4)。这个取整会让模拟负荷略偏小,因为小数部分被丢掉了。严谨做法是用循环逐15分钟判断,但那样计算量会大不少。
实际工程中我用了更稳妥的写法:先算确切需要的充电时间段数,然后按概率随机向上或向下取整,这样可以消除系统性偏差。代码改造也不复杂:
import math fractional_interval = charge_hours * 4 base_interval = int(fractional_interval) if np.random.rand() < (fractional_interval - base_interval): base_interval += 1这只是个小细节,但当模拟次数上千、车辆上万时,就会变成几十千瓦甚至几百千瓦的系统偏差,不能忽略。
还有一个容易踩的坑:充电跨零点。上面代码用% TIME_POINTS做了取模回绕,很多新手会忽略这一点,导致23:00开始充电的车直接丢失后半夜的负荷。这在夜间充电主导的私家车场景下是致命错误。
3.4 收敛性判断:跑多少次合适
蒙特卡洛最常被问的问题就是“要跑多少次”。答案不是拍脑袋定的,要看输出收敛情况。
我的做法是:每次模拟结束后记录当天总充电量(kWh),画累积均值曲线,观察波动幅度是否随模拟次数增加收窄到可接受范围。
以这个项目为例:总充电量约21.5万kWh/日。跑了500次模拟,发现标准差基本稳定在0.8%以内,也就是约±1700kWh。这个精度对区域级配电网规划完全够用了。如果你想更保守,跑1000次,误差还能再压一半,但计算时间翻倍。
对于负荷曲线的每个时刻点,波动程度不一样——负荷高峰时段(比如19:00)波动最大,低峰时段波动小。所以如果要输出95%置信区间,应该按每个时刻点分别统计。
# 检验收敛:看第200次和第500次的差异 diff = np.abs(mean_load_at_200 - mean_load_at_500).max()只要这个差值远小于负荷绝对量(比如小于2%),就可以认为收敛了。
4. 结果分析与可视化
4.1 日均负荷曲线形态解读
跑完500次模拟后,我得到了这样一张典型的负荷曲线:
- 凌晨01:00~05:00:公交车集中充电期,负荷稳定在1000~1800kW,出租车也有少量补电。
- 早晨07:00~09:00:私家车到单位后的慢充小高峰,加上出租车早班补电,出现第一个小凸起。
- 下午14:00~16:00:出租车换班补电高峰,负荷明显抬升。
- 傍晚18:00~22:00:最大高峰,下班回家的私家车大规模插枪,负荷直接冲顶,峰值约4500kW。
- 深夜22:00之后:负荷缓慢回落,但公交车充电开始接力,所以不会降到很低。
这个“早晚双峰、夜间有底”的形态,是典型的城市混合车型充电负荷结构。
4.2 区分车型看贡献
我单独把每种车型的负荷曲线拉出来看,发现一个很有意思的现象:私家车虽然数量占65%,但它的负荷贡献峰值却占了总负荷的60%以上,原因就是它集中在傍晚时段充电;公交车数量只有10%,但每辆车充电功率高、电池大,夜间贡献也不容小觑;出租车倒是均匀散布,像背景噪音。
做配电网规划的人要特别留意这个贡献拆分——同样是5000kW的峰值,如果主要是私家车贡献的,那晚高峰叠加居民用电,变压器压力巨大;如果主要是公交车贡献的,那夜间负荷对电网反而友好。
4.3 概率区间的工程价值
均值曲线只是“平均状况”,实际某一天可能高可能低。我同时输出了95%置信区间带,做规划的人应该拿置信区间上沿做容量校核,而不是拿均值。
举个具体数字:19:30时刻点,均值负荷4200kW,95%置信区间是[3900, 4550]kW。如果你用均值去设计箱变容量,大概率在极端天气日出现重载。而置信区间上沿能帮你预留更充足的安全裕度。
5. 常见问题与排查技巧实录
5.1 晚高峰负荷尖峰“毛刺”太多,不光滑
表现:模拟曲线的峰值附近锯齿感极强,物理上不合理。
原因:电动汽车充电功率是不连续的——一辆车插上就是7kW或60kW,拔掉就是0。当模拟车辆数不够多的时候,每辆车“插拔”造成的阶跃变化会清晰可见。
解决:
- 加大模拟次数(从100次到500次,毛刺明显变浅)。
- 加大车辆规模(如果只模拟几百辆车,曲线必然难看,这是蒙特卡洛的统计特性决定的)。
- 用核平滑对输出曲线做后处理,但要注意:平滑会削峰,规划场景下慎用。
5.2 负荷总量比实际统计数据高出一大截
原因排查:大概率是“充电频率设定”不合理。我的模型默认每辆车每天都充电,但实际上不是所有车每天都充——私家车平均2~3天充一次,续航焦虑没那么严重。
修正:引入充电概率参数。私家车工作日充电概率约0.5~0.6,出租车约0.9,公交车约1.0。在抽样时先按概率判断“今天是否充电”,不充电的车直接跳过。
这个修正对总充电量影响极大。我之前没加这个参数时,模拟总充电量比电网实际统计高了40%,加了之后误差缩小到5%以内。
5.3 模拟结果对“初始SOC下限”过于敏感
表现:把SOC下限从10%改成15%,总充电量变化幅度很大。
原因:SOC下限直接决定了目标充电量。下限设得越低,计算出的需求电量越大。
经验:不要拍脑袋设这个参数。如果你是结合行程规划类APP的统计数据——比如车主导航时显示的剩余电量分布——就按实测分布赋值;没有实测数据就做敏感性分析,把下限从5%扫到20%,给决策者一个“乐观—中性—保守”三档结果,而不是只给一个数。
5.4 三种车型的充电时间分布混在一起,曲线反而“太平”
现象:总负荷曲线没有明显峰谷,像个平顶山。
原因:间分布选得过于均匀,或者车型之间没有拉开差异。比如出租车起始时间设成全天均匀、私家车高峰标准差设置太大(超过3小时),叠加后就成了一条“时间摊平”的曲线,这不符合实际。
修正:回到GMM拟合的原始数据,确认两个峰的均值、权重是否匹配城市通勤特点。晚高峰均值在19:00,标准差2小时,是符合中国大城市作息的;如果你套用北欧城市的参数——那里下班早、通勤模式不同,曲线形态自然诡异。
5.5 充电效率放在哪里计算容易搞混
不少初稿代码是:先算“电池需要多少电”,再算“充电桩要花多少时间”,然后把充电桩输出功率直接叠加。忽略效率就会导致负荷虚高约8%~10%。
放法很简单:需要从电网取的电量 = 电池需求电量 / 充电效率;对应等效充电时长 = 电网取电量 / 充电功率。这样算出来的功率直接叠加就是电网侧负荷,不需要再在叠加时乘效率。
6. 蒙特卡洛输出如何落地到实际业务
说到底,模拟只是工具,模拟出来的数字要能用起来,才能真正产生价值。我这次项目做完,输出了三样东西,每样对应的使用方都不同。
- 给配电网规划部门:日负荷曲线均值+95%置信区间上沿,用于评估区域内变压器容量是否足够、需不需要扩容、充电桩接入会不会导致电压越限。这是最核心的产出。
- 给充电站运营商:分车型的充电负荷时段分布,帮助判断快充桩和慢充桩的配比合理性,以及是否需要引入分时定价引导错峰充电。
- 给政策制定参考:不同渗透率情景下的负荷预测(比如明年电动车翻倍,负荷怎么变)。这一步只需要把
EV_COUNT参数改一下重跑即可,模型框架完全复用。
在渗透率拓展上,我强烈建议不要简单线性放大车辆数。车辆多了以后,充电行为分布可能改变——比如公共桩不够用,排队概率上升;比如小区私桩普及率提高,慢充比例增加。如果模型允许,可以把充电功率选择矩阵做成渗透率的函数,更贴近实际演化路径。
还有个经验:不同季节的曲线要分开做。冬天由于续航衰减和空调制热,耗电量平均上升15%~20%,充电起始SOC更低,负荷曲线整体抬升,晚高峰延长。做的扎实一点,就按月份跑一遍模拟,输出12组曲线,而不是只做一个典型日。
如果还想再深入一层,把蒙特卡洛的输出接上潮流计算,就能评估充电负荷对配电网节点电压和网损的影响。那要从负荷曲线扩展到单台变压器覆盖范围内的充电桩接入位置和功率,用蒙特卡洛批量生成不同接入方案,逐一跑潮流。这一步计算量大,但做完了就是完整的“源—网—荷”协同分析,价值非常高。
最后补一句实操体会:蒙特卡洛这种工具,真正考验人的地方从来不是数学推导,而是“怎么把真实世界的物理约束和用户习惯映射成分布参数”。模型跑得快不快、代码写得好不好是次要的,参数标定和合理性校验才决定了预测结果是参考还是数字垃圾。我第一次做的时候,光概率分布参数是否正确、模拟结果和实际统计数据能否对上,就花了一半以上的时间。这块一定要舍得花精力。