最近我在帮朋友算园区充电桩到底怎么配,被问得最多的一个问题就是:到底装几个7kW慢充、装几个快充才够用?说实话,凭经验拍脑袋很容易翻车。每台车几点到家、还剩多少电、要充多久才能走,全是随机变量。与其继续拍脑袋,不如直接用Python把这套东西做成概率模型,扔进蒙特卡洛模拟里跑,输出一条完整的充电负荷曲线。今天就先不整虚的,直接上代码,从最硬核的行驶里程生成器开始,把电动汽车充放电行为的概率模型一步步搭起来。这篇文章适合刚接触负荷仿真、想用Python做充电行为建模的人,你可以一边看一边跟着敲,跑完就能得到一天24小时的充电负荷分布。
1. 为什么这套模型要从“行驶里程生成器”开始,而不是直接画负荷曲线
很多朋友第一次做电动汽车充电负荷仿真,上来就想画曲线,结果卡在第一步:曲线是怎么来的?答案其实是一条因果链,行驶里程是所有后续步骤的最上游输入。
1.1 充电需求本质上是“行驶里程的投影”
电动汽车和燃油车有一个特别大的区别:燃油车加油往往是“有空就去加”,而绝大多数家用电车的充电行为是“回家插枪”。也就是说,今天这辆车跑了多少公里,决定了它消耗了多少电池电量,也决定了它回家之后要充多久、充多少电。
整个模型的逻辑链条是这样的:
行驶里程 → 单位电耗 → 消耗电量 → 剩余电量 → 充电需求 → 开始充电时刻 → 充电时长 → 聚合负荷曲线
如果你把行驶里程生成器做错了,后面所有的蒙特卡洛结果都会跟着错。我见过不少人用固定的日均里程 40 公里去算,算出来的负荷曲线整齐得像刀切的一样,现实中根本不是这样。真实情况是一辆车今天跑了 5 公里,明天跑了 100 公里,后天又跑了 60 公里,每天的耗电量都不一样,所以充电时长也不是固定值。
1.2 个体随机,群体有规律:概率模型的核心逻辑
单独看一辆车,它的行驶里程几乎没法预测。但是你把一万辆车放在一起看,每天的行驶里程统计就会呈现一个非常稳定的分布:大多数人每天通勤 30 到 60 公里,少数人跑 100 公里以上,还有一小撮网约车司机每天能跑到两三百公里。概率模型不关心某一个具体车主今天跑了多少,它只关心“这个群体”的分布长什么样。
我们平时从调查报告里拿到的日均行驶里程数据,比如“某城市私家车日均行驶里程 40 公里”,其实是一个群体的统计平均值。要还原出这 40 公里背后每个人怎么分布,就必须给里程加一个分布形态,然后用蒙特卡洛抽样去模拟一整群车。
1.3 这套模型能解决什么问题
你要是想估算一个小区需要多少充电桩、评估一台变压器会不会过载、或者对比无序充电和有序充电对电网的影响,这套模型都能直接复用。核心思路就一句话:每一台车都是从一个概率分布里抽出来的随机样本,把几百上千台车的结果累加在一起,就是这群电动爹的整体充电规律。
2. 行驶里程生成器:为什么选对数正态分布,参数怎么标定
2.1 正态分布的问题:负数里程和对称曲线
很多新手上来就用np.random.normal(40, 20)生成行驶里程,这个做法有两个明显的坑。
第一,正态分布会生成负数。你要是把正态分布直接套在里程上,会抽到“今天开了负 3 公里”这种荒唐数据。虽然可以把负值截断成 0,但截断之后分布形态已经变了,均值也会偏移。
第二,真实的行驶里程分布是右偏的。大部分人的里程集中在均值以下,少部分人的里程特别长,这就导致分布有一个很长的右尾巴。正态分布是对称的,无法描述这种形态。
对数正态分布恰好解决这两个问题。它只生成正数,天然不会出现负数;它的曲线是先快速上升、再缓慢下降的右偏形态,非常贴合出行调研数据。
2.2 从统计调查数据里提炼参数
这里要强调一句:如果你手头有真实的车辆行驶数据,最理想的做法不是拟合对数正态,而是直接用经验分布抽样,也就是统计每个里程区间出现的频次,再用np.random.choice按频次抽。但大多数时候我们手头只有报告里的均值和方差,这时候用对数正态来近似是公认的可行方案。
比如国外交通统计里比较常用的私家车日均行驶里程均值是 43.5 英里,标准差是 22.5 英里。换算成公里,约等于均值 70 公里,标准差 36 公里。如果用的是国内某城市的统计值,比如均值 35 公里、标准差 25 公里,那直接把单位统一输入就行。
问题来了,numpy.random.lognormal需要的参数是mean和sigma,但这里的mean并不是观测到的均值 43.5,而是取对数之后的均值。你不能直接把 43.5 传进去,否则生成出来的数据会大到离谱。正确的换算公式是:
设观测均值为 m,标准差为 s。先算一个中间量 phi = sqrt(s² + m²),然后:
mu = ln(m² / phi),sigma = sqrt(ln(phi² / m²))
其中 mu 和 sigma 才是传给np.random.lognormal的参数。这个换算我第一次做的时候也绕了很久,后来干脆把公式直接封装成函数。
2.3 直接上代码:行驶里程生成器
下面这段代码就是标题里说的“硬核生成器”,输入的是你能从调查报告里拿到的里程均值和标准差,输出的是符合对数正态分布的一批日行驶里程。
import numpy as np def generate_daily_mileage(n, mean=43.5, std=22.5): """ 生成 n 辆车的单日行驶里程。 参数是英里制,换成公里制时保持单位一致即可。 """ mean_sq = mean ** 2 phi = np.sqrt(std ** 2 + mean_sq) mu = np.log(mean_sq / phi) sigma = np.sqrt(np.log(phi ** 2 / mean_sq)) mileage_miles = np.random.lognormal(mean=mu, sigma=sigma, size=n) return mileage_miles跑一下看分布:
np.random.seed(42) mileage = generate_daily_mileage(10000) print(f"均值: {mileage.mean():.2f} 英里") print(f"标准差: {mileage.std():.2f} 英里") print(f"最大值: {mileage.max():.2f} 英里") print(f"最小值: {mileage.min():.2f} 英里")如果参数标定正确,输出的均值应该非常接近 43.5 英里,标准差接近 22.5 英里。这就是一个可靠的概率模型输入。注意,我这里用了np.random.seed(42),目的是保证每次跑出来的结果可复现。后面做策略对比时,随机种子不固定会导致两次仿真的差距根本说不清是策略变化带来的,还是随机波动带来的。
2.4 公里与英里的换算
国内读者一般习惯用公里。两种做法都可以:第一种是把 mean 和 std 直接换成公里数,比如 mean=70.0, std=36.0,然后代入同一个函数;第二种是保持英里参数不变,生成之后再统一乘 1.60934。
我个人更推荐第一种,因为参数直接对应你手头的数据,不容易混淆。代码里只需要把两处参数替换就行:
mileage_km = generate_daily_mileage(n, mean=35.0, std=25.0)不需要额外做单位换算。
3. 充电行为建模:到家时刻、充电功率与能耗计算
行驶里程生成器解决的是“一天消耗了多少电”的问题。接下来要处理的,是这群车回到家里之后,什么时候开始充电、用什么功率充、充多久。
3.1 到家时刻不是均匀分布,是“下班回家先插枪”
绝大多数私家电动车用户回到家之后,会选择立即插枪充电,所以车辆到家的时刻分布就直接决定了充电负荷在傍晚到夜间怎么堆积。
实际数据里,到家时刻通常在傍晚 17 点到 22 点之间最集中,峰值大约出现在 19 点到 20 点。如果直接用普通正态分布去套,会抽出凌晨 3 点回家充电的样本,这在私家车场景下非常不合理。更合适的办法是使用截断正态分布,只允许抽取 16 点到 24 点之间的值。
用scipy.stats.truncnorm可以很方便地实现:
from scipy.stats import truncnorm def generate_arrival_hour(n, peak_hour=19.5, spread=1.5, low=16, high=24): """ 生成 n 辆车的到家时刻。 peak_hour: 峰值时刻 spread: 分散程度 low/high: 截断区间 """ a = (low - peak_hour) / spread b = (high - peak_hour) / spread return truncnorm.rvs(a, b, loc=peak_hour, scale=spread, size=n)这样生成出来的时刻基本集中在 17 点到 23 点之间,且不会出现凌晨充电的怪异样本。如果你有真实的后台数据,完全可以统计出充电开始时刻的频次,再用经验分布采样,这比任何理论分布都更贴近实际情况。
3.2 充电功率:慢充为主、快充为辅
常见的家充桩功率一般是 7kW 或者 11kW,公共快充桩功率则从 30kW 到 120kW 不等。在做负荷仿真时,不需要精确到每一款车型的充电协议,只需要把充电桩分成慢充和快充两大类,再按比例随机分配。
比如我在这套模型里,默认 80% 的用户使用 7kW 家充,20% 的用户当天有快充需求,快充桩功率取 50kW。这一比例可以根据场景调整,小区夜间负荷评估可以设成 95% 慢充加 5% 快充,公共快充站场景则反过来。
3.3 能耗、充电概率与SOC限制
单位能耗参数也很关键。纯电轿车综合百公里电耗大约在 15 到 20 kWh 之间,也就是每公里 0.15 到 0.20 kWh。冬天开空调或者高速行驶时能耗会更高,夏天开空调也会影响,所以实际模型里最好把能耗系数设成一个随机变量,而不是固定值。
我常用的做法是:
energy_consumption = np.random.normal(0.18, 0.02, n)这样每辆车每公里的能耗略有差异,更贴近现实。
还有一个重要问题是“不是每台车回家都充电”。有些用户电还很多,习惯每隔两天才充一次;有些则是续航焦虑重度患者,每天回家必插枪。更合理的做法是引入一个充电概率。如果简单地按 100% 都充电来算,负荷峰值会被高估,尤其是在小区这种场景下。
我通常用这样一个规则:初始 SOC 低于某个阈值,或者当天跑的里程带来的耗电量超过了某个比例,就触发充电。初始 SOC 可以用均匀分布来模拟,比如 20% 到 70% 之间随机抽取。SOC 上下限分别用 20% 和 90%,因为大多数车企会锁电,也不建议满充满放。
3.4 充电时长怎么算
充电时长本质上是一个除法问题:
充电时长 = 需要充入的电量 / 充电功率 / 充电效率
效率一般取 0.9 左右,也就是充进去 1 度电,实际插座上可能消耗 1.1 度电。需要充入的电量由行驶里程、电耗和初始 SOC 共同计算。
这里有一个边界条件容易被忽略:电池容量有限,SOC 到了上限就必须停止充电。即便公式算出来需要充 8 小时,但 5 小时就充满了,那负荷只能在充电的前 5 小时出现。所以每台车的充电时长都要加上一个最小值截断:
energy_needed = mileage_km * energy_consumption / charging_efficiency energy_needed = np.minimum(energy_needed, battery_capacity * (soc_max - soc_initial)) charge_duration = energy_needed / charging_power这样处理之后,电池容量和 SOC 限制就自然嵌进模型里了。
4. 蒙特卡洛模拟主循环:怎么把一堆概率分布叠成一条负荷曲线
4.1 蒙特卡洛不是玄学,是“重复抽样求统计”
蒙特卡洛模拟说起来其实不复杂。你不是要解析地推导出一个复杂的多变量概率密度函数吗?那就不推导了,直接从每个变量的概率分布里抽样,抽出一大堆模拟样本,每个样本就是一个具体场景。把所有场景的结果叠加取平均,就得到期望行为。
放到充电负荷场景里,就是不断重复“抽一台车、算这台车怎么充、把功率加到对应时间段”这个动作。抽一万台车,就模拟了一万台车同时在一个住宅区过夜充电的结果。
4.2 单台车的一天:五步走流程
每一台车的模拟逻辑都是一样的:
第一步,从行驶里程分布里抽一个日行驶里程。 第二步,根据单位电耗算出当天消耗的电量。 第三步,结合初始 SOC,判断当天是否需要充电。 第四步,如果需要充电,就在允许的时间区间里抽一个开始充电时刻。 第五步,根据充电功率和 SOC 上限算出充电时长。
这五步做完,一台车当天的“充电事件”就确定了。它简而言之就是:某辆车在 19 点 15 分开始以 7kW 功率充电,充了 2.5 小时,所以从 19 点 15 分到 21 点 45 分这段的负荷上,都要叠加 7kW。
4.3 聚合思路:15分钟一个时间片
电网负荷数据通常按 15 分钟一个点来做,一天就是 96 个点。要把每台车的充电功率叠加到负荷曲线上,就需要把开始时刻和结束时刻换算成时间片索引。
比如一天 96 个点,每个点 15 分钟。一台车从 19 点 15 分开始充,对应的时间片索引就是 77;充到 21 点 45 分,对应索引是 87。那么第 77 到 86 个片段的负荷都要加上这台车的充电功率。
代码实现如下:
def aggregate_load(arrival_hours, durations, powers, time_slots=96): """ 把每台车的充电事件累加到 96 点负荷序列上。 arrival_hours: 开始充电时刻(小时) durations: 充电时长(小时) powers: 充电功率(kW) """ load = np.zeros(time_slots) slot_length = 24.0 / time_slots for hour, duration, power in zip(arrival_hours, durations, powers): start = int(round(hour / slot_length)) % time_slots end = int(round((hour + duration) / slot_length)) % time_slots if end > start: load[start:end] += power elif end < start: # 跨天充电 load[start:] += power load[:end] += power else: load[start] += power return load注意这里处理了跨天的情况。有些人到家晚,充电时长又长,可能会充到第二天凌晨。如果只按 start 到 end 叠加,数组尾部到 96 之间会漏掉一段负荷,所以要把尾部到开头的负荷也补上。
4.4 主循环跑起来:单日样本与多日样本
跑单日样本很直观,一次抽样产生一台车的参数,然后把这些参数传给上面的聚合函数。但你别只跑一次就下结论,因为单日结果是一条特定随机样本,不是期望。
标准的蒙特卡洛做法是:同一天生成 N 台车,得到一条总负荷曲线;然后重复 M 天,得到 M 条总负荷曲线;最后对 M 条曲线逐点求均值,并计算标准差或置信区间。这样才能比较稳健地评估。
5. 完整可运行的Python代码与15分钟粒度负荷曲线
前面已经把所有关键函数拆开了,这里把它们组合成一个完整的、可以直接运行的脚本。参数我也放到一起,方便你根据自己的场景改。
import numpy as np from scipy.stats import truncnorm import matplotlib.pyplot as plt # ---------------------------- 参数区 ---------------------------- np.random.seed(42) N_CARS = 5000 # 模拟车辆数 DAYS = 20 # 蒙特卡洛重复天数 MILEAGE_MEAN = 35.0 # 日均行驶里程均值(公里) MILEAGE_STD = 25.0 # 日均行驶里程标准差(公里) BATTERY_CAPACITY = 60.0 # 电池容量(kWh) ENERGY_MEAN = 0.18 # 平均单位电耗(kWh/km) ENERGY_STD = 0.02 # 单位电耗波动 CHARGER_POWER_SLOW = 7.0 # 慢充功率(kW) CHARGER_POWER_FAST = 50.0 # 快充功率(kW) FAST_CHARGER_RATIO = 0.15 # 快充占比 CHARGING_EFFICIENCY = 0.9 # 充电效率 SOC_MIN = 0.2 # 起始SOC下限 SOC_MAX = 0.9 # 充电SOC上限 # ---------------------------- 行驶里程生成器 ---------------------------- def generate_daily_mileage(n, mean, std): mean_sq = mean ** 2 phi = np.sqrt(std ** 2 + mean_sq) mu = np.log(mean_sq / phi) sigma = np.sqrt(np.log(phi ** 2 / mean_sq)) return np.random.lognormal(mu, sigma, n) # ---------------------------- 到家时刻生成器 ---------------------------- def generate_arrival_hour(n, peak_hour=19.5, spread=1.5, low=16, high=24): a = (low - peak_hour) / spread b = (high - peak_hour) / spread return truncnorm.rvs(a, b, loc=peak_hour, scale=spread, size=n) # ---------------------------- 单日模拟 ---------------------------- def simulate_one_day(n_cars): # 1. 行驶里程 mileage = generate_daily_mileage(n_cars, MILEAGE_MEAN, MILEAGE_STD) # 2. 单位电耗 energy_rate = np.random.normal(ENERGY_MEAN, ENERGY_STD, n_cars) energy_rate = np.maximum(energy_rate, 0.05) # 3. 初始SOC soc_start = np.random.uniform(SOC_MIN, 0.8, n_cars) # 4. 消耗电量 energy_used = mileage * energy_rate # 5. 是否需要充电:耗电量超过剩余可用电量就充 available_capacity = BATTERY_CAPACITY * (SOC_MAX - soc_start) need_charge = energy_used > available_capacity * 0.3 # 6. 需要充电的那些车,计算所需充电量,并限制在SOC上限内 energy_needed = energy_used[need_charge] / CHARGING_EFFICIENCY energy_needed = np.minimum( energy_needed, BATTERY_CAPACITY * (SOC_MAX - soc_start[need_charge]) ) # 7. 充电功率分配:随机抽快充/慢充 n_need = energy_needed.shape[0] charger_power = np.full(n_need, CHARGER_POWER_SLOW) fast_mask = np.random.rand(n_need) < FAST_CHARGER_RATIO charger_power[fast_mask] = CHARGER_POWER_FAST # 8. 充电时长 durations = energy_needed / charger_power # 9. 到家时刻 arrival_hours = generate_arrival_hour(n_need) return arrival_hours, durations, charger_power # ---------------------------- 负荷聚合 ---------------------------- def aggregate_load(arrival_hours, durations, powers, time_slots=96): load = np.zeros(time_slots) slot_length = 24.0 / time_slots for hour, duration, power in zip(arrival_hours, durations, powers): start = int(round(hour / slot_length)) % time_slots end = int(round((hour + duration) / slot_length)) % time_slots if end == start: load[start] += power elif end > start: load[start:end] += power else: load[start:] += power load[:end] += power return load # ---------------------------- 蒙特卡洛主循环 ---------------------------- all_loads = [] for _ in range(DAYS): arrivals, durations, powers = simulate_one_day(N_CARS) daily_load = aggregate_load(arrivals, durations, powers) / 1000.0 # 换算成MW all_loads.append(daily_load) all_loads = np.array(all_loads) mean_load = all_loads.mean(axis=0) std_load = all_loads.std(axis=0) # ---------------------------- 输出与绘图 ---------------------------- time_axis = np.arange(96) * 15 / 60.0 print(f"模拟车辆数: {N_CARS}, 重复天数: {DAYS}") print(f"峰值负荷: {mean_load.max():.2f} MW") print(f"峰值出现时刻: {time_axis[np.argmax(mean_load)]:.2f} 点") print(f"夜间平均负荷(19-23点): {mean_load[int(19 * 4):int(23 * 4)].mean():.2f} MW") plt.figure(figsize=(12, 5)) plt.plot(time_axis, mean_load, label="平均负荷") plt.fill_between( time_axis, mean_load - 1.96 * std_load / np.sqrt(DAYS), mean_load + 1.96 * std_load / np.sqrt(DAYS), alpha=0.3, label="95%置信区间", ) plt.xlabel("时刻(小时)") plt.ylabel("负荷(MW)") plt.title("电动汽车夜间充电负荷曲线(蒙特卡洛模拟)") plt.grid(True) plt.legend() plt.show()脚本里我默认生成 5000 台车、重复 20 天。运行后在终端里能看到峰值负荷和峰值出现时刻,同时画出一张带置信区间的负荷曲线图。
你可能会问,把 5000 台车重复跑 20 天,和跑一次 5000 台车有什么区别?区别在于置信区间。后者的曲线是一次随机实现,峰值上下波动很大;前者对这个波动做了一次平均,曲线更平滑,也更能代表长期期望水平。做方案对比时,必须用后者。
6. 仿真结果怎么读,以及我踩过的四个坑
6.1 负荷曲线长什么样,峰谷如何解读
用上面这组参数跑完,你会发现负荷曲线的特征非常明显:傍晚 18 点之前几乎是一条零线,18 点开始缓慢抬升,19 点到 21 点快速冲到峰值,22 点之后缓慢回落,凌晨 2 点到 5 点基本归零。这个形状很符合实际的小区充电规律。
如果快充比例调高,峰值会变得更高更尖锐;如果慢充比例调高,曲线会变得更宽更平缓,但持续时间更长。这就是为什么做充电桩规划时,快慢充配比是核心参数,因为它直接决定负荷曲线的形状。
6.2 坑一:np.random.lognormal的参数不是观测均值和标准差
这个坑我几乎每次带人做都会遇到。np.random.lognormal(mean=43.5, sigma=22.5)看起来没毛病,但跑出来数值会大到离谱,因为这里的mean和sigma是取对数之后的参数,不是原始观测数据的均值和标准差。如果用错了,日均里程会变成 10 的几十次方公里这种天文数字,整个模型直接废掉。
所以一定要先换算。换算公式虽然看起来啰嗦,但封装成函数之后就一劳永逸了。
6.3 坑二:到家时刻抽出凌晨三点
如果你图省事,直接用np.random.normal(19.5, 1.5)来生成到家时刻,确实能跑出结果,但你会看到一部分车在凌晨 2 点、3 点开始充电。这种样本在城市居民区里几乎不成立。
解决方案有两个:用truncnorm截断到合理区间,或者直接拿真实充电记录做经验分布抽样。前者适合快速原型,后者适合做严谨工程评估。
6.4 坑三:只跑一次随机实验就开始下结论
蒙特卡洛模拟输出的是“一次随机实现”,不是“统计规律”。你跑一次 5000 辆车,得到的峰值是 3.71MW;再跑一次,可能是 3.64MW。这两次之间的差异不是真实方案的差异,而是随机波动。
正确做法是固定随机种子,然后用多日重复取平均。我在上面的脚本里用np.random.seed(42)就是保证你可以完整复现我这次的结果。每次改完参数对比方案时,也要在完全相同的随机种子下做,否则对比结果没有意义。
6.5 坑四:忘记SOC上限和充电效率
有两种“理想化错误”经常出现。第一种是忽略了充电效率,直接用消耗电量除以功率得到时长,算出来充电量偏小,负荷被低估。第二种是忽略SOC上限,让一辆本来已经 90% 的车还能继续充到 100%,这在很多实际车型里根本不可能。把这两个边界条件加上,结果会更接近现实,也更能解释为什么实网负荷总比简单估算低一点。
| 常见坑 | 原因 | 修正方法 |
|---|---|---|
| lognormal参数传错 | 把观测均值直接当对数均值 | 用公式换算mu和sigma |
| 到家时刻出现凌晨 | 正态分布无截断 | 用截断正态或经验分布 |
| 单次结果当结论 | 随机波动未平均 | 固定种子,多日重复取均值 |
| 忽略SOC上限和效率 | 模型过于理想化 | 加入SOC上下限和0.9效率系数 |
7. 下一步扩展:有序充电、多场景参数与放电行为
基础的充电负荷模型跑通之后,你可以往两个方向扩展:一是做“有序充电”策略验证,二是做不同充电场景的组合建模。
7.1 有序充电本质上是改“开始充电时刻”的分布
无序充电时,车到家就充,因此负荷峰值和晚高峰几乎重叠。有序充电的思路很简单,把一部分车辆的充电开始时刻往后推迟。你可以把generate_arrival_hour替换成generate_schedule_hour,只在 23 点到次日 6 点之间允许充电。
把原来的负荷曲线和有序充电曲线放在一起对比,你会直观看到峰被削低了多少、谷被填高了多少。这个模型改造起来成本极低,只需要改一个函数入口参数,非常适合用来评估“把充电时间从傍晚挪到凌晨”对小区配变的影响。
7.2 多场景组合:有桩、无桩、快充站、V2G
实际城市里用户不会全部在家充电。有人小区没有固定车位,只能去办公地充;有人依赖公共快充站;还有一批未来可能参与 V2G 反向放电。
建模时可以把车辆分成几个群组,每个群组独立配置功率、到家时刻分布、充电比例、SOC策略,然后把所有群组的负荷累加在一起,就是城市级充电负荷。这个方法不需要改变框架,只需要把simulate_one_day里的参数变成数组,分别对群组循环即可。
7.3 放电行为怎么加进去
标题里提到“充放电行为”,放电通常出现在 V2G 场景里:用户在家充电,在电网高峰时段反向放电。在模型里实现起来就是把 V2G 功率记成负的负荷,聚合时把负值加到对应时间段。
比如某台车在 19 点到 21 点以 6kW 放电,那么这两个时间片的聚合功率减去 6kW。最终负荷曲线就会在晚高峰出现一个凹陷,这就是“削峰填谷”的量化表现。
7.4 模块化设计的重要性
我把生成器、行为参数、聚合函数、主循环拆成相互独立的模块,原因很简单:以后大概率会换数据源、换策略、换功率配置。只要接口不变,替换任何一层都能跑。如果你后续要接入优化算法,比如用线性规划求解每辆车最优充电时刻,只需要把simulate_one_day里的充电决策部分替换成优化器的输出,主循环完全不用动。
我在实际项目里的习惯是,先固定住里程生成器和聚合函数,保证基础仿真可信,再去调策略。这样每次改参数,我都能快速判断变化来自策略还是来自随机波动。把这套基础跑通之后,再往上叠有序充电、V2G、分群建模都会顺手很多。