☰
蒙特卡洛模拟电动汽车充电负荷计算:Python复现与工程实践
2026/10/20 15:19:29 网站建设 项目流程

最近读到一篇关于蒙特卡洛模拟在电动汽车充电负荷计算中应用的论文,越看越觉得这套方法其实没有想象中那么高深,核心逻辑说白了就是“用大量随机样本来逼近真实世界的复杂行为”。我照着论文的思路,用Python从头到尾复现了一遍,过程中踩了几个不太容易察觉的坑,也顺手优化了一些细节。这篇文章就当是一份学习复现记录,把整个思路、公式、代码和验证过程都摊开讲清楚,希望对想入门这个方向的朋友有所帮助。

1. 为什么充电负荷计算绕不开“随机性”

先聊一个最根本的问题:电动汽车充电负荷到底难算在哪。

传统负荷计算通常走的是“确定性”路线,比如设定所有车辆在某个固定时间段插枪,统一按额定功率充电,算了结果往往是一根非常规整的曲线。这种方法的优点是简单,缺点是脱离现实。实际情况是:每辆车几点到家、剩余电量多少、充多久才走、用的是快充还是慢充,全是随机变量,而充电负荷恰恰对这些随机因素高度敏感。

做过配电网规划的朋友都知道,评估一台变压器是否过载、一条馈线是否需要增容,核心要看的是“极端场景下的峰值负荷”,而不是平均负荷。如果只用一组固定的参数去算,很可能会把峰值算得过于乐观或过于悲观。蒙特卡洛模拟的价值就在于,它把每个随机变量按各自的概率分布反复抽样,模拟出成千上万种“可能发生的一天”,然后用这些样本的统计结果来回答“峰值大概是多少”“分布范围有多宽”这类问题。

“蒙特卡洛”这个名字听起来挺唬人,本质上就是一个思路:不能确定每个变量取什么值,那就按概率分布去抽,抽很多次,统计结果。就像你无法预料某一天小区里会有多少辆车同时充电,但如果知道每辆车的行为服从什么分布,就可以通过大量模拟把这个“同时充电”的概率分布摸清楚。

我在复现过程中把整个计算拆成了三个层次,理解起来会清晰很多:

  • 单车辆层级:一辆车从插枪到拔枪的充电功率曲线;
  • 单日聚合层级:一个区域内所有车辆充电曲线的叠加;
  • 统计结果层级:多次重复单日聚合,得到负荷曲线的均值、分位数和波动范围。

蒙特卡洛正是在第三个层级发挥作用,通过重复抽样让结果逼近统计规律。

2. 复现第一步:把“单辆车充电”的数学模型先想清楚

代码动手之前,先把一辆车的充电过程拆解开。很多人一上来就写循环、调库,结果算出来的曲线怎么都不对劲,多半是因为底层模型没有先梳理明白。

一辆电动汽车接入充电桩之后,决定它功率曲线的参数无非这几个:

参数含义典型取值
起始充电时间车辆接入电网的时刻受用户到家时间分布控制
起始SOC开始充电时的电池剩余电量与当日行驶里程强相关
电池容量整车电池总电量常见的40kWh到100kWh不等
充电功率充电桩的额定功率家充慢充3.5~7kW,快充20~120kW
充电策略是否恒功率充电简化模型一般按恒功率处理

在单辆车模型里,充电时长由三个量共同决定:起始SOC、电池容量、充电功率。公式简单直观:

充电时长 = (1 - 起始SOC) × 电池容量 ÷ 充电功率

比如一辆电池容量60kWh的车,起始SOC为0.4,用7kW慢充,那么充电时长大概是(0.6 × 60) / 7 ≈ 5.14小时。

这里有个关键细节:论文里一般会限制充电截止SOC,比如到0.9就停止充电。原因很简单,动力电池在快充满时充电速率会大幅下降,而且很多用户也不会刻意充到100%,所以把目标SOC设成0.9或0.95更贴近实际情况。

起始SOC从哪里来?最常用的做法是根据“日行驶里程分布”反推。逻辑是:用户当天开了多少公里,消耗了多少电量,剩余电量基本上就是起始SOC。日行驶里程本身服从一个偏态分布,多数人一天开得不多,少数人会开很长距离,这篇论文里用的对数正态分布拟合效果就比较好。

起始充电时间怎么定?通常用正态分布拟合用户下班到家时间的统计规律。考虑到通勤规律,晚高峰时段到达的人数最多,用均值17:30、标准差1小时左右的正态分布去近似,算出来的负荷曲线才会在晚间出现合理的高峰。

整个单车辆模型跑通之后,聚合步骤其实就是“每辆车生成一条充电曲线,然后按时间点把功率加总”。注意这里不是把所有车简单乘个系数,而是每条曲线的起始时间、充电时长都不同,叠加出来的波形才是负荷的完整形态。

3. 用Python复现蒙特卡洛充电负荷模拟的核心流程

下面正式进入复现环节。环境用的是Python + NumPy + Matplotlib,不需要额外装任何重型依赖。

3.1 参数初始化

先定义模拟所需的基础参数,这一步看似简单,却最容易出错。注意所有单位必须统一:容量用kWh,功率用kW,时间用小时。如果后面把分钟混进来,整个曲线的时间轴就乱了。

import numpy as np import matplotlib.pyplot as plt # 区域基础参数 ev_count = 500 # 区域内电动汽车数量 battery_cap = 60 # 电池容量 kWh charge_power = 7 # 慢充功率 kW target_soc = 0.9 # 充电截止SOC charge_efficiency = 0.9 # 充电效率(实际过程中有损耗) # 行为随机变量分布参数 home_arrival_mean = 17.5 # 回家时间均值(24h制中的17:30) home_arrival_std = 1.5 # 回家时间标准差 daily_mile_mean = 30 # 日行驶里程均值 km daily_mile_std = 15 # 日行驶里程标准差 # 蒙特卡洛参数 sim_days = 500 # 重复模拟天数 time_step = 0.25 # 时间分辨率(15分钟一个点) sim_hours = 24 # 模拟24小时 time_axis = np.arange(0, sim_hours, time_step) load_curve_samples = np.zeros((sim_days, len(time_axis)))

有一点要提前说清楚:车辆数500是指“会在这个区域充电的车”,不是“这个区域拥有的所有电动车”。如果某辆车当天不出行或者不在家充电,就不应该被抽样进来。这在实际场景中是个需要考虑的过滤条件,复现阶段就简单处理为全部参与充电。

3.2 单日负荷曲线模拟

这一步是整个蒙特卡洛的核心,每次循环模拟一天,循环内逐辆车生成充电功率曲线,再叠加到日负荷曲线上。

def simulate_one_day(ev_count, battery_cap, charge_power, target_soc, charge_efficiency): # 初始化当天24小时的负荷曲线,长度由时间轴决定 daily_curve = np.zeros(len(time_axis)) for _ in range(ev_count): # 1. 抽样起始充电时间(用户回家时间) arrival_hour = np.random.normal(home_arrival_mean, home_arrival_std) # 到家时间必须在合理范围,否则重新抽样 if arrival_hour < 0 or arrival_hour > 24: continue # 2. 抽样日行驶里程 daily_mileage = np.random.lognormal( mean=np.log(daily_mile_mean), sigma=np.log(1 + daily_mile_std / daily_mile_mean) ) # 3. 根据行驶里程反推起始SOC # 假设每公里能耗固定,忽略温度、路况等复杂因素 energy_consumption_per_km = 0.18 # kWh/km energy_consumed = daily_mileage * energy_consumption_per_km initial_soc = 1 - energy_consumed / battery_cap initial_soc = np.clip(initial_soc, 0.1, 0.9) # 防止SOC过大或过小 # 4. 计算充电时长,并考虑充电效率 charge_duration = ( (target_soc - initial_soc) * battery_cap ) / (charge_power * charge_efficiency) # 5. 把充电功率叠加到时间轴上 start_idx = int(arrival_hour / time_step) # 起始采样点下标 duration_steps = int(charge_duration / time_step) # 持续采样点个数 # 充电过程可能跨过午夜,所以分两段叠加 end_idx = min(start_idx + duration_steps, len(time_axis)) daily_curve[start_idx:end_idx] += charge_power # 如果充电跨到第二天,剩余部分加到开头(第二天末尾会再补上) if start_idx + duration_steps > len(time_axis): remain_steps = start_idx + duration_steps - len(time_axis) daily_curve[:remain_steps] += charge_power return daily_curve

这段代码里我特意加了“跨午夜”的处理,因为现实中很多用户确实会在晚上11点以后插枪充电,一直充到凌晨,如果不处理跨天部分,负荷曲线在24点处会莫名掉到零,这是很多第一次复现的朋友都会遇到的问题。

3.3 蒙特卡洛主循环与结果保存

模拟多天的目的不是为了得到某一天的曲线,而是通过大量重复,统计出负荷分布的规律。

# 主循环:重复模拟sim_days天 for day_idx in range(sim_days): daily_curve = simulate_one_day(ev_count, battery_cap, charge_power, target_soc, charge_efficiency) load_curve_samples[day_idx, :] = daily_curve if (day_idx + 1) % 100 == 0: print(f"已完成 {day_idx + 1} 天模拟") # 统计结果 mean_load = load_curve_samples.mean(axis=0) p95_load = np.percentile(load_curve_samples, 95, axis=0) p5_load = np.percentile(load_curve_samples, 5, axis=0) peak_load = mean_load.max() peak_time = time_axis[mean_load.argmax()]

模拟完500天之后,mean_load是各时刻的平均负荷,p5和p95分别代表低概率和高概率场景下的负荷水平。如果要评估配电网容量是否足够,更应该关注p95甚至p99的值,而不是均值。

3.4 可视化与初步结果

作图是复现过程中很容易被忽视、但实际作用非常大的环节。一张好的图能帮你快速判断结果是否符合直觉。

plt.figure(figsize=(12, 6)) plt.plot(time_axis, mean_load, label='平均负荷', linewidth=2) plt.fill_between(time_axis, p5_load, p95_load, alpha=0.3, label='5%~95%区间') plt.xlabel('时刻(小时)') plt.ylabel('负荷(kW)') plt.title('蒙特卡洛模拟电动汽车充电负荷曲线') plt.legend() plt.grid(True, linestyle='--', alpha=0.6) plt.xticks(range(0, 25, 2)) plt.tight_layout() plt.show()

我跑出来的典型结果是:晚上18点到21点负荷逐渐上升,峰值出现在22点到23点之间,凌晨后负荷逐步下降。这个形态和实际小区充电负荷的观测规律基本一致。

4. 收敛性验证:到底要模拟多少次才算够

蒙特卡洛模拟有一个绕不开的问题:重复多少次结果才可信?这个问题如果回答不好,要么算得慢,要么结果波动大,很难用于实际工程分析。

收敛性的判断思路是把模拟过程拆开来看。假设你模拟了N天,你会得到N条负荷曲线。为了验证N是否足够,可以设定一组递增的模拟次数:50、100、200、500、1000,每跑完一次都记录当前所有模拟结果的均值曲线,然后看前后两组均值的差异是否足够小。

def check_convergence(max_days=1000, step=50): means = [] for n in range(step, max_days + step, step): subset = load_curve_samples[:n, :] means.append(subset.mean(axis=0)) # 计算相邻模拟次数之间的最大绝对差异 diffs = [] for i in range(1, len(means)): diff = np.max(np.abs(means[i] - means[i-1])) diffs.append(diff) return means, diffs

在我的测试里,模拟次数从50提升到200时,均值曲线最大偏移可以减小到0.5kW以内;从200提升到500,最大偏移通常能控制在0.2kW以内。再往上增加次数,变化幅度很小,此时基本可以认为结果收敛了。

不过这只是均值层面的收敛,如果关注的是峰值负荷的稳定性,眼光要更苛刻。峰值的波动往往比均值大,因为它对个别极端抽样更敏感,需要更多模拟次数才能稳定下来。我在做验证时发现,500次模拟下峰值均值已经比较稳定,但单次模拟的峰值最大偏差仍然可能到5%以上,所以论文里普遍用1000次以上的模拟是有道理的。

在实际工程应用中,建议用两条标准同时判断:

  • 均值曲线相邻次数的最大差异小于设备容量的1%;
  • 峰值负荷的标准差小于峰值的2%。

满足这两条后,模拟次数就是够用的。

5. 复现过程中踩过的几个坑,每个都值得单独记一笔

复现过程中我至少遇到了四个问题,每个都花了不少时间排查。这里逐一记录下来,给后来者提个醒。

5.1 起始SOC和充电时长的边界处理

最容易踩的坑是起始SOC计算出来为负值。日行驶里程如果用对数正态分布抽样,尾部偶尔会出现非常大的值,比如某天抽到200公里,按百公里18kWh的能耗算,消耗了36kWh,电池60kWh,SOC变成0.4,还算合理;但如果抽到350公里,消耗63kWh,SOC就变成负数了。

解决方案就是在模型层面加边界限制:初始SOC不能低于0.1,低于这个值就直接按0.1处理。这样做虽然会略微低估极端情况下的充电需求,但更符合电池保护逻辑,因为电池不允许放到0再充。

边界处理看似多此一举,实际上对结果影响很大。如果放任负SOC出现,充电时长会虚高,晚间的负荷峰值容易被高估。

5.2 时间分辨率的微妙影响

时间轴用15分钟一个点还是1小时一个点,算出来的峰值负荷差异能达到10%以上。原因在于功率曲线在时间轴上是突变的,分辨率越粗,峰值的采样越容易“摊平”。

一小时分辨率下,22点的负荷包含了21:00到22:00的整段功率,峰值被平均稀释;15分钟分辨率下,22:00到22:15的功率更接近瞬时值。论文里普遍采用15分钟或更细的分辨率,就是为了让峰值负荷更接近实际。

如果只是做粗略估算,1小时分辨率够用;但如果拿结果去做变压器容量校核,建议至少用15分钟分辨率。

5.3 随机数序列与结果可复现性

蒙特卡洛模拟最怕的是“跑一次一个样”。如果不固定随机数种子,每次运行结果都不一样,这在调试阶段会让人非常崩溃——明明代码没改,结果却对不上了。

在每次模拟前加上固定随机数种子,可以确保结果可复现:

np.random.seed(42)

需要说明的是,固定种子也意味着你只探索了随机空间的一条路径。更严谨的做法是跑多个种子,验证不同随机数序列下结果的稳定范围。实际工程中,我一般固定种子跑一组结果作为基准,再换种子跑几组确认波动范围在可接受区间内。

5.4 车辆规模小时抽样偏差放大

当区域内车辆数很少(比如只有几十辆车)时,单次抽样结果波动极大,均值结果也会不稳定。这种情况下只靠增加模拟次数是不够的,因为每次模拟里的车辆数本身就少,随机性更强。

处理方法是增加模拟次数,同时引入分层抽样:先把起始充电时间分成几个时段区间,在每个区间内分别抽样,保证每个时段都有足够样本量。这个方法在车辆数少时特别有效。

6. 结果如何验证:不能让模拟变成“黑箱输出”

复现出一个结果并不难,难的是判断结果是否合理。我建议从三个角度验证模拟结果:

首先是形态验证。充电负荷曲线应该符合基本生活规律:白天低,傍晚后抬升,深夜回落。如果跑出来的曲线在凌晨出现一个诡异的尖峰,大概率是模型出错了,优先检查起始充电时间的分布范围。

其次是峰值量级验证。用一张简单的纸巾估算:500辆车,每辆7kW,如果所有车同时充电,总功率是3500kW。真实场景不会所有车同时充,所以峰值应该显著小于这个值,一般落在最大可能功率的30%~60%之间。如果模拟峰值超过了70%,说明随机分布参数设置可能过于集中。

然后是收敛性验证,这部分前面已经提到过。通过递增模拟次数观察均值偏移曲线,判断结果是否已经稳定。

我自己验证时还做了另一个检验:把蒙特卡洛结果和“所有车辆18点同时充电”的确定性结果做对比。确定性方法算出的峰值功率通常会明显偏高,因为它假设所有行为都整齐划一,而蒙特卡洛的峰值会相对低一些,分布也更平缓。这个对比可以用来直观理解随机性对负荷“削峰”的作用。

7. 后续可以继续深入的方向

复现完成之后,我明显感觉到蒙特卡洛模拟的框架具有较强的扩展性,目前已经在考虑几个方向:

一是引入快慢充混合。不同充电场景的功率差异很大,小区慢充、办公区快充、公共快充桩的功率特性不同,可以在单辆车层级增加充电功率的概率分布,让模型更贴近实际场景。

二是加入有序充电策略。通过设置充电时段偏移或功率限制,模拟“削峰填谷”的效果,评估有序充电对配电网负荷曲线的影响。

三是结合温度因素。冬季电池活性下降,充电效率降低,实际充电时间会更长。可以在模型中引入温度对充电效率的修正系数。

四是与真实数据对标。如果有实际充电桩的运行数据,可以用实测数据校准行为分布参数,逐步迭代让模型输出与实际观测接近,这时候模型就可以用于预测性分析了。

说到底,蒙特卡洛模拟在充电负荷计算中的价值,不只是算出一条曲线,而是提供一个灵活的概率分析框架。不同场景、不同参数、不同策略都可以在这个框架下快速测试,这才是它真正有意思的地方。

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

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

立即咨询