蒙特卡洛模拟做充电负荷预测这活儿听起来挺唬人其实就是把“电动汽车用户群体”这头大象用统计学的方式切成一片片然后扔进计算机里模拟出几万种可能的日常最后把这些日常叠在一起看整体效果。我做这个项目的时候最开始也走了不少弯路。比如一上来就想着用复杂的交通流仿真、时空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, axis0) std_load np.std(all_results, axis0) p95_low np.percentile(all_results, 2.5, axis0) p95_high np.percentile(all_results, 97.5, axis0) # 时间轴小时 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时刻点均值负荷4200kW95%置信区间是[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组曲线而不是只做一个典型日。如果还想再深入一层把蒙特卡洛的输出接上潮流计算就能评估充电负荷对配电网节点电压和网损的影响。那要从负荷曲线扩展到单台变压器覆盖范围内的充电桩接入位置和功率用蒙特卡洛批量生成不同接入方案逐一跑潮流。这一步计算量大但做完了就是完整的“源—网—荷”协同分析价值非常高。最后补一句实操体会蒙特卡洛这种工具真正考验人的地方从来不是数学推导而是“怎么把真实世界的物理约束和用户习惯映射成分布参数”。模型跑得快不快、代码写得好不好是次要的参数标定和合理性校验才决定了预测结果是参考还是数字垃圾。我第一次做的时候光概率分布参数是否正确、模拟结果和实际统计数据能否对上就花了一半以上的时间。这块一定要舍得花精力。
