2023年我接过一个小区充电负荷评估的需求。客户那边只给了一个Excel,里面是三百多辆私家车的品牌型号,外加一句“帮我们看看变压器会不会过载”。我第一反应是:这事不能靠经验拍脑袋,因为充电负荷和空调负荷不一样,它完全由人的行为驱动——几点到家、今天跑了多远、插不插枪、插上充多久,全是随机事件。后来我用蒙特卡洛把这件事拆成了“随机抽样+大量模拟+统计输出”的标准范式,在Matlab里用不到两百行代码就跑完了500次仿真。
这篇文章就是把那次完整的思路和代码拆开来讲。内容包括:蒙特卡洛在电动汽车充电负荷预测里到底解决了什么问题、输入参数怎么从历史数据里标定、Matlab代码怎么写、模拟次数怎么选、结果怎么解读,以及我实测中踩过的几个坑。适合正在做微电网、充电站规划、小区配变容量评估这类工作的人参考。你只要能跑通Matlab基础脚本,就能跟着复现。
1. 充电负荷为什么这么难预测:蒙特卡洛要解决的随机性根源
1.1 一条看似简单的充电曲线,背后藏着至少四层随机性
一台电动车慢充一小时大概消耗7度电,一百台车同时充就是700kW的负荷,这个数字可以让一台普通台区变压器直接告警。问题在于,这100台车永远不会“整齐划一”地同时充。
理论上,我们可以把充电负荷拆成“车辆接入时间 × 充电功率 × 充电时长”这三件事的乘积。可这三件事每一件都带着随机性:
- 车辆接入时间:用户几点回到家、几点把充电枪插上,取决于下班时间、路上拥堵程度、是否要去超市买菜。
- 起始SOC和需充电量:今天跑了30公里还是200公里,直接决定了电池还需要吃进多少电。车主不可能每天跑出完全相同的里程。
- 充电功率:慢充桩有3.3kW、7kW,快充桩有60kW、120kW;就算同一个桩,电池在不同SOC区间的接受功率也不一样。
- 充电持续时间:由需充电量和充电功率共同决定,本质上是前两个随机变量的函数。
所以说,充电负荷预测不能叫“预测”,它本质上是在算“多个概率分布的卷积”。你想用一条确定的公式去描述它,基本不可能;你能做的,是描述它“大概落在哪个范围、有多大可能超出某个阈值”。这正是蒙特卡洛方法的用武之地。
1.2 确定性方法为什么算不准:蒙特卡洛的思路转变
传统的估算方法很粗暴:取一个平均日行驶里程,除以平均百公里电耗,得到平均充电电量;再乘以一个“同时率系数”,最后摊到一天24小时上。这样做的好处是快,坏处是它只输出一个“均值场景”,完全抹掉了极端场景。
举个例子。某个工作日大家正常通勤,晚上7点左右回家,负荷高峰在20:00。但如果某天是长假前一天,很多人第二天要跑长途,晚上10点还会集中去充电,此时负荷峰值可能比均值场景高30%以上。更麻烦的是,这种极端场景不是小概率事件,它每年总会出现那么几十次,而配变扩容、储能配置恰恰要按这种场景去校核。
蒙特卡洛的思路完全不同:它不去求唯一答案,而是按照每个随机变量的概率分布去大量抽样,一组样本就是“一个可能发生的日子”。把几千个“可能发生的日子”叠加起来,就能看到完整的结果分布——均值是多少、5%分位在哪、95%分位在哪。
一句话概括:确定性方法回答“未来大概是什么样”,蒙特卡洛回答“未来所有可能长什么样,以及每种可能有多大”。对于充电负荷这种受用户行为支配的对象,第二种回答方式显然更接近工程决策的真实需求。
2. 建模前的数据准备:用Matlab把模糊场景拧成概率分布
2.1 车辆参数与用户行为参数怎么定
蒙特卡洛仿真的输入不是单一数字,而是一组概率分布。参数怎么定义,直接决定结果可信度。我习惯把参数分成两类:一类是“车自身说了算”的硬件参数,另一类是“用户行为决定”的随机参数。
硬件参数相对好办,从车辆铭牌和充电桩类型就能拿到。随机参数则需要靠历史数据或合理假设来标定。下面是我在居民区慢充场景下常用的一套参数表,你可以当成起点,换成自己的数据:
| 随机要素 | 分布假设 | 典型参数 | 说明 |
|---|---|---|---|
| 日行驶里程 | 对数正态 | 期望30km,对数标准差0.4 | 里程恒为正数,且有少量一天跑200km以上的长尾 |
| 到家时刻 | 正态分布 | 均值18:00,标准差1.8h | 如果数据更细,可用分段正态或双峰高斯 |
| 插枪延迟 | 正态分布 | 均值0.5h,标准差0.3h | 用户到家后不一定马上充电 |
| 充电功率 | 离散分布 | 7kW慢充为主 | 快充桩需单独建模型 |
| 充电效率 | 常数 | 0.9 | AC-DC转换和电池内阻损耗 |
为什么日行驶里程要用对数正态?因为行驶里程不可能为负,而且分布有明显的右拖尾——大多数通勤日跑20到50公里,但偶尔会有长途出行跑到一两百公里。对数正态分布天然满足“正数+右偏”这两个特征,而且Matlab里用lognrnd抽样非常方便。
为什么到家时刻用均值18:00的正态分布?因为大部分居民区用户是下班回家充电,晚高峰集中在17点到20点。如果研究的是商业区或高速服务区,这个分布要完全换一套参数,甚至要用多个正态分布叠加成多峰分布。
2.2 用fitdist做参数拟合:从历史数据到分布对象
如果手里有历史充电订单数据,不要直接拍参数。Matlab的fitdist函数可以直接从数据里拟合出分布对象,比自己猜参数靠谱得多。
% 假设 mileageData 是从订单系统里导出的日行驶里程列向量 pd_mileage = fitdist(mileageData, 'lognormal'); mu_fit = pd_mileage.mu; % 对数均值 sigma_fit = pd_mileage.sigma; % 对数标准差 % 可视化看一眼拟合效果 figure; histfit(mileageData, 30, 'lognormal');拟合完之后,要用histfit或者绘制经验CDF和理论CDF的对比图,重点看分布尾部。我实测踩过一次坑:某个网约车车队的行驶里程分布根本不是标准对数正态,而是“工作日短、休息日长”的双峰结构。这种情况下强行套lognormal,结果中段看着还行,尾部完全失真。正确做法是用高斯混合模型:
gm = fitgmdist(mileageData, 2); % 两个簇拟合双峰双峰模型会让深夜充电场景的出现概率更真实,后续模拟的95%分位带也会宽不少。
2.3 边界场景怎么处理:快充桩、跨天充电、同时率
很多人建模时只盯着“平均值”,忽略边界场景,结果仿真算出来的负荷曲线和实际台区数据对不上。有三个边界问题必须提前界定清楚。
第一,充电桩类型绝对不能混用。慢充桩7kW和快充桩120kW完全是两个物种。研究小区配变,慢充为主;研究高速服务区,快充占绝对主导。如果在同一个模型里混着抽样,负荷峰值会失真。我的做法是分开建模,最后按比例叠加。
第二,跨天充电必须显式处理。晚上11点插上枪,充到凌晨1点,负荷会落在第二天的曲线上。代码里如果一刀切截断到当天24点,会系统性低估凌晨负荷。所以要么把模拟区间拉长到36小时,要么对跨天的部分单独存到第二天。
第三,同时率不需要额外假设。确定性方法里还要拍一个同时率系数,而蒙特卡洛建模时只要正确抽样,100辆车同时充电的场景会自动以一定概率出现。这就是概率方法的优势——你不用拍脑袋假设“同时率是0.3”,而是让分布自己说话。
3. 蒙特卡洛主流程与Matlab代码实现
3.1 代码结构总览:一个脚本拆成三段
我写这类仿真通常不搞复杂的面向对象封装,而是按照“参数配置—单日抽样—蒙特卡洛循环—统计输出”的顺序组织脚本。好处是调试方便,改一个参数不用翻三层继承关系;坏处是代码稍显冗长,但对一百多行规模来说完全可以接受。
整体逻辑是这样的:
- 设定车型参数、充电桩参数、分布参数、模拟次数。
- 在每次模拟中,为所有车辆抽样出“今天”的日行驶里程、到家时刻、插枪延迟。
- 根据抽样结果计算每辆车的充电开始时间、充电时长,生成一条单日充电功率曲线。
- 把所有车辆的曲线叠加,得到这一天的总负荷曲线。
- 重复步骤2到4,跑够设定的模拟次数。
- 对多次模拟的结果做统计,输出预期曲线和置信区间。
代码里我最强调的是一个rng固定随机种子,后面会细说。
3.2 参数配置与单日抽样
%% 固定随机种子,保证结果可复现 rng(2026); %% 时间与场景参数 dt = 15/60; % 时间步长,15分钟一个点 T = 24/dt; % 一天96个点 sim_case_num = 500; % 蒙特卡洛重复次数 car_num = 100; % 研究区域的电动汽车数量 %% 车辆与充电桩硬件参数 C_bat = 60; % 电池容量 kWh P_charge = 7; % 家用慢充功率 kW eff = 0.9; % 充电效率 E_100 = 15; % 百公里电耗 kWh/100km %% 用户行为分布参数 mu_mileage = 30; % 日均行驶里程期望 km sigma_mileage = 0.4; % 行驶里程对数标准差 arrive_mu = 18; % 到家时刻均值,单位小时 arrive_sigma = 1.8; % 到家时刻标准差 delay_mu = 0.5; % 到家到插枪的延迟均值 delay_sigma = 0.3; % 延迟标准差rng(2026)不是玄学。蒙特卡洛方法依赖伪随机数生成器,如果每次运行不固定种子,结果会每次不一样,评审或者同事复算你的结果时没法比对。固定种子后,500次模拟的结果可以一比一复现。这一点在工程项目里非常重要。
3.3 单辆车的充电负荷曲线生成与叠加
先抽一辆车的“今日状态”,然后计算充电参数:
%% 抽样:每辆车的充电需求 daily_mileage = lognrnd(log(mu_mileage), sigma_mileage, car_num, 1); arrive_hour = arrive_mu + arrive_sigma * randn(car_num, 1); delay_hour = delay_mu + delay_sigma * randn(car_num, 1); % 充电开始时刻 = 到家时刻 + 插枪延迟 start_hour = arrive_hour + delay_hour; % 需要的电量 = 行驶里程 / 100 * 百公里电耗;再考虑充电效率 energy_need = daily_mileage / 100 * E_100; duration_hour = energy_need / (P_charge * eff);这里三个抽样向量化了一次生成了100辆车的全部参数,速度很快,也方便后续调试。arrive_hour有可能抽出早上的值,这是正态分布的尾部,在真实中确实存在小部分夜班或凌晨回家的车主,因此不需要人为裁剪。
然后生成单辆车的充电曲线并叠加:
%% 生成单日总负荷曲线 load_profile = zeros(1, T); for i = 1:car_num % 计算充电起始点和持续的步数 start_step = max(1, min(T, round(start_hour(i) / dt) + 1)); dur_step = round(duration_hour(i) / dt); end_step = min(T, start_step + dur_step - 1); % 固定功率充电,累加到总曲线上 load_profile(start_step:end_step) = load_profile(start_step:end_step) + P_charge; end这段代码的关键是边界条件。start_step做了上下限保护,避免凌晨到家算成负数、或深夜到家超出96步;end_step做了一个截断,如果超出当天,就直接切到当天结束。截断会低估凌晨跨天充电的负荷,我在5.2节再展开说怎么补。
我在这里用的是固定功率充电模型,也就是从插枪开始就以7kW持续充电,直到电量充满。实际锂电池在恒流恒压(CC-CV)模式下,末端功率会逐步下降。但在15分钟步长下,我实测固定功率的误差很小,工程上够用;如果步长细化到1分钟,末端下降就不可忽略了。
3.4 多次模拟与统计结果输出
单日曲线只是“一个可能的日子”。把上述过程放进蒙特卡洛循环,跑完500次,再对结果做统计:
%% 蒙特卡洛主循环 all_profiles = zeros(sim_case_num, T); for sim = 1:sim_case_num % 重新抽样每一天的车辆状态(代码同上一节) daily_mileage = lognrnd(log(mu_mileage), sigma_mileage, car_num, 1); arrive_hour = arrive_mu + arrive_sigma * randn(car_num, 1); delay_hour = delay_mu + delay_sigma * randn(car_num, 1); start_hour = arrive_hour + delay_hour; energy_need = daily_mileage / 100 * E_100; duration_hour = energy_need / (P_charge * eff); load_profile = zeros(1, T); for i = 1:car_num start_step = max(1, min(T, round(start_hour(i) / dt) + 1)); dur_step = round(duration_hour(i) / dt); end_step = min(T, start_step + dur_step - 1); load_profile(start_step:end_step) = load_profile(start_step:end_step) + P_charge; end all_profiles(sim, :) = load_profile; end %% 统计结果:均值曲线、5%与95%分位带、峰值分布 profile_mean = mean(all_profiles, 1); profile_p5 = prctile(all_profiles, 5, 1); profile_p95 = prctile(all_profiles, 95, 1); peak_all = max(all_profiles, [], 2); peak_mean = mean(peak_all); peak_p5 = prctile(peak_all, 5); peak_p95 = prctile(peak_all, 95); %% 绘图 t_hour = (0:T-1) * dt; figure; hold on; fill([t_hour, fliplr(t_hour)], [profile_p5, fliplr(profile_p95)], ... [0.85 0.9 0.95], 'EdgeColor', 'none'); plot(t_hour, profile_mean, 'b-', 'LineWidth', 1.6); xlabel('时刻(h)'); ylabel('充电负荷(kW)'); legend({'5%~95%分位带', '均值曲线'}, 'Location', 'NorthWest'); grid on;prctile(all_profiles, 5, 1)的第3个参数1是让Matlab沿着“模拟次数”那一维求分位数,返回的是一条96点的曲线。这个细节容易写错,写成缺省的话,Matlab会沿着第一维也就是96个时间点求,结果就完全反了。
跑完上面这段代码,500次模拟在普通笔记本上大约两秒出头。如果车数量上万,建议把内层for i循环改成矩阵切片操作,不然循环会慢到让人怀疑人生。
4. 模拟次数怎么定、结果怎么看:一次仿真实测复盘
4.1 从毛糙到收敛:模拟次数对结果的影响
蒙特卡洛次数不是越大越好,也不是越小越省事。太小时,结果曲线毛糙,峰值的随机波动大,你没法判断这个结果到底是真实趋势还是抽样噪声。太大时,计算时间线性增长,收益却微乎其微。
我用上一节的参数做过一次收敛性测试,结果大致如下:
| 模拟次数 | 峰值均值(kW) | 峰值标准差(kW) | 相对用时 |
|---|---|---|---|
| 50 | 405 | 55 | 0.2x |
| 100 | 412 | 38 | 0.45x |
| 300 | 414 | 29 | 1.2x |
| 500 | 415 | 27 | 2x |
| 1000 | 415 | 27 | 4x |
这组数据是特定参数下的示例,不同分布参数会有差异,但趋势一致:次数从50加到500,峰值均值变化不到3%,峰值标准差从55降到了27;再加到1000,几乎不再变化。500次已经进入了“平台期”。
需要强调的是,收敛判断不能只看均值,还要看95%分位。我遇到过均值曲线早就平了,但peak_p95还在来回跳的情况。这说明当前抽到的样本对极端场景覆盖不足,解决办法是增加模拟次数,或者对极端场景做分层抽样,确保尾部事件至少出现个几十次。
4.2 峰值时刻与分位带解读
拿上节的参数跑完500次,典型的结果是这样:负荷从傍晚18点开始抬升,20点到23点之间达到峰值,峰值均值大约在400多kW的量级;5%到95%的分位带宽度大约在80到100kW之间。
这里有个反直觉的点:负荷峰值并不出现在“所有人同时到家”的18点,而是延后了两三个小时。原因不难理解,用户到家的时间本身就分散在17点到20点,再加上插枪延迟,真正开始充电的时间被整体向右平移;而充电时长又普遍在2到4个小时,所以功率曲线拖得很长。做配变容量规划时,如果只盯晚高峰基荷曲线,很容易漏掉20点之后的充电尖峰。
分位带的解读也要说清楚。5%到95%分位带表示“在当前的随机性假设下,90%的日子里负荷会落在这个区间”。工程上做扩容方案,要盯95%分位甚至更高,因为配变过载的代价远大于提前扩容的成本;做充电费用结算或成本收益分析,看均值曲线就够了。
4.3 数据不好看时,先查参数不查代码
仿真跑完,如果发现结果曲线和实际监测数据对不上,我踩过的经验是:先怀疑分布参数,再怀疑边界条件处理,最后才怀疑代码逻辑。
95%的情况是分布参数没校准。举例来说,如果你调研的社区其实是网约车司机聚集区,用户深夜返回比例偏高,而你还在用arrive_mu = 18去抽样,结果肯定偏低。这种偏差反映在曲线上不是整体平移,而是“晚高峰凸起不够、深夜尾巴偏低”的形态差异。
边界条件问题也很常见。跨天充电截断会导致凌晨负荷被低估,而凌晨恰恰是谷电时段,很多用户设置了预约充电,实际负荷可能比均值模型高。这个问题需要回到5.2节专门处理。
代码逻辑错误反倒少见,因为一旦变量引用错了,负载曲线会出现明显突变,比如某个时段突然掉到零或者翻倍,这种一眼就能看出来。
5. 从“能出数”到“能落地”:三个常见坑和处理经验
5.1 随机变量被当成独立样本,忽略真实相关性
这是初学者最容易掉进去的坑,而且结果看起来还挺合理的。
抽样时,我独立生成了daily_mileage和arrive_hour。但真实生活中,这两个变量存在相关性:上下班通勤的人通常傍晚回家,行驶里程稳定在二三十公里;跑长途的人回家时间更随机,里程也更大。如果完全独立抽样,会出现“今天跑了300公里长途、还18点准时回家”这种物理上说得通但概率极低的组合,等于给仿真注入了不存在的噪声。
处理办法有三种,按成本从低到高排序。
第一种,简单过滤。抽样完成后,对明显不合理的组合做剔除,比如行驶里程超过200公里但到家时间早于16点的样本。这种办法粗暴但有效,适合变量相关性不强的场景。
第二种,用copula建模相关性。Matlab的copularnd可以直接生成带相关性的多变量样本,然后在边缘分布上做逆变换。适合对精度要求比较高的研究场景。
第三种,也是最容易被忽视的:先算一下相关系数。用历史数据把daily_mileage和arrive_hour做一次corrcoef,如果相关系数绝对值小于0.2,独立抽样完全够用,没必要花大代价上Copula。
5.2 到家时间不等于充电开始时间,插枪延迟必须建模
我第一次搭建模型时,图省事直接让start_hour = arrive_hour,结果仿真出来的负荷峰值比实际监测数据早了一截,晚高峰的曲线形态也不对。后来翻了真实的充电订单时间戳,才发现问题出在“插枪延迟”上。
所谓插枪延迟,是指用户到家之后到真正把充电枪插上之间隔的时间。这个时间在居民区场景里很可观:有人要先吃饭,有人要陪孩子,还有人专门等到谷电时段才开始充。我统计过的样本里,平均延迟在半小时左右,标准差也不小,节假日和周末甚至会超过1小时。
把delay_hour加进来后,负荷曲线的峰谷形状立刻对上了。这件事给我的教训是:不要用直觉替代数据,哪怕只是多花十分钟拉一下历史时间戳,也比在会议室里争论“用户到底几点插枪”有用得多。
另外,有些地区出台了峰谷电价政策,用户会自动把充电时间挪到晚上10点以后。这种“延迟”不是随机的,而是强制的。建模时要在分布里加一个“谷电时段预约充电比例”参数,否则你会严重高估晚高峰负荷。
5.3 温度、电池非线性这些“学术参数”什么时候必须考虑
固定功率模型、固定百公里电耗,这种简化在大多数工程场景下够用,但有两个场景必须升级模型。
第一是温度影响。北方冬天电池活性下降,加上车内采暖,百公里电耗比常温高出20%以上。同一个用户,夏天日均充电15度,冬天可能要20度。如果项目是跨季节的全年评估,需要把E_100设成温度的函数,或者至少按“冬季参数”和“非冬季参数”分两套跑,然后按月加权。南方地区可以忽略这一项,但储能系统配置最好还是留出温度裕量。
第二是充电末端的恒流恒压过程。固定功率模型假设从插枪到充满一直是7kW,实际上锂电池充到80%以后会转入恒压阶段,功率逐渐降低。在15分钟步长下,这个误差很小;但如果你把步长细化到1分钟,或者模拟的是快充桩,末端功率下降就不能忽略。处理办法是把充电功率写成SOC的分段函数,效果更好一点的做法是用厂商给的充电曲线插值。
我在实际项目里的折中方案是:默认场景用固定功率模型,校核极端场景时再切换到分段功率模型,对比峰值差异是否在可接受范围内。大多数时候差异在5%以内,但对快充占比高的场景,差异能到10%甚至更高。
再强调一遍整个项目我最大的体会:蒙特卡洛不是一根“点石成金”的魔杖,它的上限完全由输入分布的质量决定。把时间花在拟合历史数据、理解用户行为上,远比纠结选哪种随机抽样算法更值得。每改一次参数,拿一个已知月份的历史负荷曲线做回测,偏差在5%以内再继续往下走。这套流程看着不惊艳,但比任何高级算法都管用。