蒙特卡洛模拟电动汽车充电负荷:从行驶里程生成器到概率模型 📅 发布时间:2026/9/9 4:50:26 👁 浏览次数: 说实话我刚拿到这个题目的时候想的第一件事不是“蒙特卡洛怎么实现”而是“你到底有没有想过一堆电动车同时开始充电会让台区晚高峰变成什么鬼样子”。电动汽车充放电行为的概率模型本质上就是回答这个问题的一套工具。而要用好这套工具最关键的是先把每天跑多少公里这件事搞明白——因为车辆一天的行驶里程直接决定回到家还剩下多少电也决定今晚到底插不插枪、要充多久。所以我决定按照标题说的那样直接上代码先讲行驶里程生成器再把整个蒙特卡洛模拟框架串起来。我以前给配电侧做负荷分析那会儿最不想回答的一个问题就是“你手上有没有这片区域电动车的实测充电数据”。实话实说车企有车端数据但不关注你家的台区电网侧有户号电量却没有每辆车的SOC充电桩运营商有几万条订单记录却不清楚车主第二天早上几点出门。真正做网格规划或者小区配变评估的时候你手上的数据基本是残缺的。于是唯一靠谱的办法反而是让电脑去“演”出一批虚拟车主用Python搭一套概率模型用蒙特卡洛模拟生成足够多的充电场景再对这些场景做统计分析。今天就聊聊这个模型怎么落地顺便把最容易翻车的行驶里程生成器掰开揉碎讲清楚。1. 先泼盆冷水为什么不直接去抄实测数据而是要自己用蒙特卡洛模拟生成充电负荷1.1 实测数据分散而且未来场景根本没有实测数据你可能会说现在装了那么多充电桩找几个小区抄一两个月数据不就行了问题是你要评估的是“未来三年如果这个小区电动车的渗透率从5%涨到30%变压器还扛不扛得住”。未来的场景没有实测数据哪怕你把今天的充电订单翻烂了也抄不出三年后的负荷曲线。即便退一步只评估当前状态数据也一样难凑齐。充电行为涉及的数据分散在三类主体手里第一类是车辆本身的数据包括每天的行驶里程、出发和到达时间、电池剩余电量这类数据在车企手里但基本不对外开放第二类是充电设施的数据什么时间启动、充了多久、多少功率这类数据在充电桩运营商手里但往往缺少对应车辆第二天的使用计划第三类是配电侧数据户变关系、台区负荷曲线都有但就是不知道这台区里哪些车今晚会充电。三份数据放在一起发现时间戳对不上、口径不一致、隐私也不允许随便拼所以想从实测直接推结论非常痛苦。模拟数据的优势就在于你不用依赖任何一家机构的内部数据只需要给定一个大家都认可的概率假设比如“通勤车辆日均行驶里程近似服从对数正态分布”“下班到家时间集中在18点前后”“80%的车主有家用慢充条件”然后让蒙特卡洛把这些假设变成成千上万个具体场景再把场景汇总成统计结果。你输出的是趋势和范围而不是某个车主的隐私。1.2 蒙特卡洛不是掷骰子它模拟的是一整群车主的“行为概率”蒙特卡洛模拟经常被误认为是一堆随机数乱撞。实际上它做的事情非常朴素先为每个随机变量指定一个概率分布然后反复从中抽样得到海量组合最后统计这些组合的结果。你可以这样理解统计部门想了解一万户居民的出行规律可以挨家挨户调查蒙特卡洛不入户它只做一件事——从“到家的时间分布”里抽一个时刻从“每日里程分布”里抽一个里程从“充电功率分布”里抽一个功率抽一万个虚拟车主出来。这不等于让你猜某个车主明天干什么而是让你在给定概率规则的前提下把整群车主的集体行为复现出来。蒙特卡洛有个很容易被忽略的性质同一组参数下跑两次结果并不完全一致。小区A和小区B虽然统计特征相似但具体车主构成不同晚高峰充电功率也会不同。所以真正专业的做法不只是给一条平均曲线还要重复多次模拟算出一条带有置信区间的负荷范围。后面我在结果可视化里会专门演示。1.3 先决定要抽哪些随机变量才能决定模型能回答什么问题动手写代码之前一定要先梳理清楚一个普通的通勤车主从下班回家到第二天开车走人哪些环节直接影响充电负荷随机变量典型分布假设对充电负荷的影响每日行驶里程对数正态分布决定剩余电量决定是否需要充电到家时刻正态分布峰值约18:00决定充电起始时间离家时刻正态分布峰值约7:30决定夜间可充电窗口长度是否具备家充条件离散概率没有家充桩的车不参与本模型计算充电功率离散概率选择3.3kW / 7kW / 11kW决定负荷曲线单车的“高度”是否执行有序充电二元概率或意愿阈值可扩展评估削峰填谷这张表也是整套代码的骨架。你会发现除里程之外其他变量主要决定“什么时候充、以多大功率充”而行驶里程决定了“到底充不充、需要充多少”。这就是为什么这篇文章要先拿行驶里程生成器开刀它是后面所有判断的地基。2. 行驶里程生成器用对数正态分布给每天的“电动爹”行程建模2.1 为什么偏偏是“对数正态分布”它和正态分布差在哪如果把一个城市几千名通勤车主的每日行驶里程拉出来做直方图你大概率会看到右侧拖着一条长尾大量车每天只跑十几公里到四五十公里少数车因为跑业务或者跨城通勤一天能跑两三百公里。这种分布形态用正态分布去拟合会很别扭因为正态分布是左右对称的而且理论上会给出负的里程——一个“每天平均开-3公里”的车主没有任何物理意义。对数正态分布天生适合这类数据。它的意思是里程本身不服从正态分布但里程取对数之后服从正态分布。这带来两个好处一是抽样结果永远非负二是分布形状自然右偏少数高里程车主会体现在长尾里比较符合实际出行数据。早年间很多电动汽车充电负荷文献在建立模型时用的就是NHTS全美家庭出行调查数据拟合出的对数正态结论后来国内一些研究也发现通勤里程分布同样可以近似描述。需要说明的是这不是唯一选择用伽马分布也能拟合出类似右偏形态。如果你手里有一份自己小区的出行调研数据完全可以直接拟合然后做KS检验选更好看的那个分布。这里用对数正态是为了跟主流文献对得上也方便你复现别人的结果。2.2 均值60公里、标准差30公里怎么转换成分布参数这里有一个新手基本都会踩的坑。很多人在代码里顺手就写成np.random.lognormal(mean60, sigma30)这个写法从数学上讲是灾难np.random.lognormal的第一个参数不是里程样本的均值而是“里程取对数后”那个正态分布的均值第二个参数也不是里程样本的标准差而是对数标准差。正确的做法是先给定你希望通过模型得到的里程均值和标准差然后反推出对数域的mu和sigma。转换公式是mu ln(mean^2 / sqrt(std^2 mean^2))sigma sqrt(ln(1 std^2 / mean^2))用代码实现更直观import numpy as np def get_lognorm_params_from_moments(mean_, std_): 由样本均值和标准差反算对数正态分布参数 mu_ np.log(mean_**2 / np.sqrt(std_**2 mean_**2)) sigma_ np.sqrt(np.log(1 std_**2 / mean_**2)) return mu_, sigma_ def generate_daily_mileage(n_vehicles, mean_km60.0, std_km30.0): mu, sigma get_lognorm_params_from_moments(mean_km, std_km) # 这里抽样得到的是“日行驶里程”单位公里 mileage np.random.lognormal(meanmu, sigmasigma, sizen_vehicles) # 少数极端样本会造成SOC为负建议统一截断 mileage np.clip(mileage, 0.0, 350.0) return mileage实际跑一下生成一万个样本后打印np.mean(mileage)和np.std(mileage)你会发现结果很接近你设定的60公里和30公里。如果均值稍微低一点点大概率是因为你对尾部做了350公里的截断把极少数每天开几百公里的狂热样本削掉了这在工程上完全可接受。画个直方图看得更清楚import matplotlib.pyplot as plt samples generate_daily_mileage(10000) plt.hist(samples, bins80, densityTrue, alpha0.6) plt.axvline(np.mean(samples), colorred, linestyle--, labelmean) plt.axvline(np.median(samples), colorblue, linestyle--, labelmedian) plt.xlabel(daily mileage (km)) plt.ylabel(density) plt.legend() plt.show()你大概率会看到一条右偏长尾曲线红色均值线在蓝色中位数线右边。这个特征非常关键大量车主每天跑得比平均里程少只有少部分长途用户把均值拉高。理解了这一点后面算SOC时才不会因为“平均才60公里为什么有车SOC快见底了”而困惑。2.3 从里程算SOC能耗、可用容量和下保护都别漏拿到里程之后下一步是算车主到家时电池还剩多少电。这一步看着简单却有三个非常容易漏掉的细节。第一每百公里电耗。不同车型差异很大特斯拉能耗可能13度/百公里老款电动车可能接近20度/百公里。模型里先用一个统一的经验值比如16kWh/100km也就是每公里0.16度电。真实项目里可以做成按车型混合分布的随机量。第二电池不是满放满充。为了保护电池多数车会用DOD放电深度限制实际可用电量通常是标称容量的90%左右。也就是说60kWh的电池包你大概只能用54kWh。如果你在代码里直接用60作为分母SOC会算得偏乐观。第三SOC必须有下保护。你不能真让一辆车开到电量变负数所以遇到里程特别大的样本要截断。代码如下energy_per_km 0.16 # 16kWh/100km约等于0.16 kWh/km battery_capacity 60.0 # 电池标称容量kWh usable_ratio 0.9 # 可用电量比例 soc_lower_bound 0.1 # 强制保留10%电量 def mileage_to_soc(mileage): energy_used mileage * energy_per_km soc_arrival 1.0 - energy_used / (battery_capacity * usable_ratio) return np.clip(soc_arrival, soc_lower_bound, 1.0)举个例子一辆车今天跑了40公里耗电6.4度。可用电池能量是60×0.954度那么SOC就是1-6.4/54大约0.88。也就是说回家还有88%的电这种车大概率今晚不需要充电。这正好引出一个关键点并不是所有车每天回家都会插枪充电。3. 完整的充电行为链到家时间、充电功率、出发时刻和“今晚充不充”的随机逻辑3.1 到家时间和出发时间两个正态分布但边界处理不同充电行为的第一个时间变量是“到家时刻”。工作日通勤场景里下班到家时间集中在17:30到20:00之间用均值18.0、标准差1.5小时的正态分布去近似是比较常见的选择。晚高峰的后半段本身是从18点半到21点如果很多人都在这个时间开始充电负荷曲线极容易跟原有晚高峰叠在一起这就是后来说的“峰上加峰”问题的根源。到家时刻的抽样代码很简单arrive_hour np.random.normal(loc18.0, scale1.5) arrive_hour np.clip(arrive_hour, 16.0, 23.0)第二个时间变量是“出发时刻”。第二天早上出门时间同样可以用正态分布描述均值7.5小时、标准差0.8小时。要注意的是出发时刻会影响充电窗口长度。如果你在回家4小时后开始充电而设定的充电时长需要6小时但第二天早上6点半就要出门实际就充不满。所以主循环里要把离家时间纳入计算而不是盲目假设每次都能充满自停。出发时刻抽样depart_hour np.random.normal(loc7.5, scale0.8) depart_hour np.clip(depart_hour, 5.0, 10.0)虽然两处都用正态分布但边界处理不同——到家的分布不能低于16点或高于23点因为大部分人最晚也要回家睡觉出发时间的边界则约束在5到10点之间太早或太晚都脱离通勤规律。3.2 充电功率与是否插枪不是每辆车回来都会充电很多初版模型犯的最大错误就是假设所有车每天回来都充电。这样算出来的负荷曲线会严重高估因为现实中很多通勤车到家还有60%甚至80%的电车主根本懒得插枪。所以这个模型里至少要加两层筛选。第一层是“有没有家充条件”没家充的车主会去公共充电桩这属于另一套场景本文先不展开先用85%的概率表示有家充桩。第二层是“今天想不想充”给定一个与剩余电量相关的经验概率——电量越低越愿意充电量很充足时大部分人不充。在代码里我习惯这样表达# 经验规则非实测参数具体项目请用自己数据校准 has_charger np.random.random() 0.85 if not has_charger: # 只能依赖公共桩本模型暂不展开 return if soc_arrival 0.3: charge_today True elif soc_arrival 0.7: charge_today np.random.random() 0.8 else: charge_today np.random.random() 0.25这个判断背后的逻辑是SOC只有三成时车主通常不敢不充SOC在五成上下时看明天行程有七八成概率会补电SOC还有八成多时只有计划第二天跑长途的车主才充电。如果你有真实调研数据可以把这些概率替换成实际结果。功率分配方面家用慢充桩无非三种常见规格3.3kW、7kW、11kW。可以按0.2、0.7、0.1的比例随机抽样charge_power_kw np.random.choice([3.3, 7.0, 11.0], p[0.2, 0.7, 0.1])3.3 充电时长计算充满自停而不是简单充n小时充电时长不是拍脑袋随机抽的。它取决于“缺多少电”和“用多大功率充”的比值。前面算了到家SOC之后需要的能量就是energy_needed_kwh (1 - soc_arrival) * battery_capacity * usable_ratio duration_h energy_needed_kwh / charge_power_kw举个例子如果到家SOC是0.55可用容量54度那么需要充0.45×5424.3度电。用3.3kW慢充得7.36小时用7kW快一点也得3.47小时。如果从19点开始充用3.3kW会一路充到凌晨2点多用7kW则晚上10点半左右就满了。但真正插入模型时还要考虑两个约束。一个是不要超过离家时间也就是说充电时长不能无限延长另一个是如果时间窗口不够那车离车时电量可能低于满电这其实很真实不是所有电动车车主每天都能满电出门。处理时把离家前预留半小时作为硬约束比较稳妥latest_end_hour depart_hour - 0.5 max_duration_h (latest_end_hour - arrive_hour) % 24 actual_duration_h min(duration_h, max_duration_h)这里用% 24是为了处理跨零点的场景。如果车晚上11点半到家、第二天早上6点出发差值为负模24小时后就变成6.5小时窗口计算是对的。4. 蒙特卡洛主循环代码让一万名虚拟车主在你的CPU上过完一天4.1 为什么要用96个时段描述一天充电负荷曲线本质上是一个时间序列。工程上习惯把一天切成96个时段每个时段15分钟。选15分钟是因为不少充电桩采集终端和负荷监测系统就是按15分钟冻结数据的同时15分钟也足够分辨晚高峰的形状。如果你切成1分钟看起来更精细但一万辆车、每辆车几十个时段累加时循环次数会飙升如果切成1小时晚高峰在19点还是20点是看不出来的。在代码里不需要真的存时间戳只有一个长度为96的数组索引0代表0点到0点15分索引1代表0点15分到0点30分依此类推。充电负荷就累加到这个数组上。4.2 主循环抽里程、算SOC、插枪、充电、回写负荷下面给出一个可以直接跑通的蒙特卡洛主循环示例。代码逐段看不要嫌啰嗦因为每一步都有它存在的意义import numpy as np def simulate_one_day(n_vehicles10000, seed42): # 参数区 slots_per_day 96 load_profile np.zeros(slots_per_day) mean_km, std_km 60.0, 30.0 energy_per_km 0.16 battery_capacity 60.0 usable_ratio 0.9 soc_lower_bound 0.1 rng np.random.default_rng(seed) # 由里程均值和标准差换算对数正态参数 mu np.log(mean_km**2 / np.sqrt(std_km**2 mean_km**