☰
充电站选址优化:混合整数规划与遗传算法的工程实践
2026/10/14 8:50:27 网站建设 项目流程

简介:这是一份围绕电动汽车充电站选址优化问题的学术论文,聚焦新能源汽车配套设施规划中的关键问题,适合交通规划、电气工程及新能源领域的研究人员、相关专业学生和从业者参考。文档从充电需求点分布、服务半径与最大服务数量等角度出发,梳理影响选址布局的因素与原则,提出以充电站收益最大化为目标的非线性函数,并利用遗传算法进行求解优化,最后通过仿真得出充电站位置和服务范围,案例与建模过程可为同类选址项目提供方法借鉴。资源为单个PDF文件,压缩包大小约327KB,内容包含摘要、引言、模型构建、算法设计与仿真结果等完整期刊论文结构,便于直接阅读与引用。目前已有215人学习/下载,对于正在开展电动汽车充电设施规划、选址建模或相关课程设计的研究者而言,是一份较紧凑的参考文献。

1. 充电站选址优化不是拍脑袋:认清这是设施选址问题再动手

充电站选址这件事,外行看是“哪里车多建哪里”,内行知道它是一个几十万到上千万投资额的决策问题:站建偏了,利用率上不去,回本周期被无限拉长;站建密了,自家几个场站先互相抢客流。电动汽车充电站选址优化研究,本质上是把“在哪些候选地块建站、建几个、服务哪些需求点”变成数学模型,用需求分布、建设成本、服务半径和容量约束,算出最优建站组合。适合正在做充电网络规划的运营工程师、做投资测算的咨询人员,以及写相关课题但缺落地思路的学生。这篇文章我不打算复述论文式的抽象推导,而是按我做过方案的路子来讲:先把模型立住,再给最小可跑的代码和参数表,最后把最容易翻车的几个坑一个个拆开。

2. 选址建模:三类经典模型、目标函数与参数标定

2.1 三类经典选址模型:P-中位、最大覆盖与 P-中心怎么选

先别急着写代码,选址优化最怕的就是模型选错。同一个城市数据,P-中位模型会把站往市中心挤,最大覆盖模型则会优先填满外围空白区,结果能差出几条街。

P-中位模型追求的是所有需求点到最近站点的加权距离总和最小。它衡量的是“平均出行成本”,适合城市里的快充网络做均摊覆盖,因为你的用户大多是通勤顺路补电和日常随机充电,不希望在某个方向绕太远。代价是它允许个别偏远需求点距离很差,只要总加权距离小就行,所以纯用这个模型,郊区可能被牺牲。

最大覆盖模型则是在给定建站数量内,覆盖尽可能多的需求权重。它适合早期扩张阶段,比如运营商第一年铺网络,预算只够建 10 个站,目标就是把城市的主要居住区和商圈先盖住,至于已经被覆盖的需求点到站多远,模型不太关心。这个模型产出的方案往往更“好看”,因为覆盖率指标容易汇报。

P-中心模型和前面两个都不一样,它要把“最远那个需求点到最近站的距离”压到最小,是典型的 min-max 问题。适合高速服务区、城际走廊这类“最坏情况不能太远”的场景。城市充电站如果直接套 P-中心,结果会让你多建很多不必要的站,因为只是为了照顾某一个极端偏远点。

选型我一般看两个问题:业务目标是效率还是公平,是预算内覆盖还是兜底保障。城市快充网络我首选最大覆盖或 P-中位,两者结合也行——第一轮用最大覆盖定骨架,第二轮在已覆盖区域内用 P-中位微调具体点位。

2.2 一个可落地的混合整数规划:变量、约束与目标函数

拿到实际问题后,我习惯把它建模成混合整数规划(MILP),因为这类问题有成熟的求解器可以直接跑,不需要一开始就上启发式算法。

模型变量应包含两类:一是候选站是否建设的 0-1 变量,二是需求点是否分配给某个候选站的 0-1 变量。目标函数至少拆成两段:建设成本和用户行驶成本。建设成本来自候选站列表里的土地、变压器、充电桩和施工费用;用户行驶成本用需求点的权重乘到最近站点的距离再求和,权重越大代表这个片区越需要被认真对待。

约束条件至少有四条:每个需求点最多由一个站服务;未建设的站不能分配任何需求;超出覆盖半径的分配必须被禁止;总建站数量不超过规划上限。如果想让模型更接近真实运营,还需要再加一条容量约束:某个站服务的总需求权重不能超过它的实际服务能力,比如 8 个 120kW 桩,一天最多服务多少车次。加了容量约束之后,问题的求解难度会明显上一个台阶,这个在第四章会展开讲。

提示:目标函数里如果只有成本而没有覆盖率约束,很多情况下最优解是一个站都不建。必须额外加“被覆盖的需求权重不低于总需求的一定比例”这类硬约束,模型才会给出有业务意义的方案。

2.3 参数标定:覆盖半径、需求权重与容量上限的初值

参数写得太随意,模型再漂亮也算不出可用的站址。我的习惯是先按业务经验定初值,再用敏感性分析调整,而不是一上来就搞复杂的标定算法。

覆盖半径 R 是最关键的一个参数,它的取值直接决定一个站“管多宽”。城市内公共快充站,我一般取 2 到 3 公里;郊区取 5 到 8 公里;高速服务区场景不叫半径,而是沿着走廊每隔 30 到 50 公里布局一个节点。这个半径不是拍脑袋定的,它跟用户到站后愿意接受的绕行时间有关,城市里 3 公里大约对应 10 分钟车程,再远用户就宁可用慢充了。

需求权重 w 的来源常见是三类:兴趣点热度、人口居住密度、已有充电订单分布。三类数据各自归一化之后,按 0.3、0.3、0.4 加权合并成一个综合权重。有历史订单时,权重直接用订单量标定最可靠,而不是用熵权法对 POI 做纯数学加权。容量参数按“桩数 × 日均服务车次 × 高峰集中系数”估算,例如一个站 8 个桩、每桩日均服务 8 车次、高峰集中系数取 1.5,那这个站一天的实际服务能力就是 96 车次。成本参数直接来自候选地块报价和电网接入勘察,没有勘察数据时,用同城市已建站点的决算平均值估算,不要拍脑袋。

3. 最小可行求解:用 Python + PuLP 跑通 50 个候选站的选址实验

3.1 准备四类数据:从 POI、人口到路网与电网接入点

选址模型需要的数据没那么玄,关键是四种数据要对齐坐标系、对齐时间范围。第一类是需求侧数据,包括兴趣点、小区人口分布、已有充电订单,这是生成需求栅格的原料;第二类是候选站数据,包括可用地块经纬度、预估建设成本、可容纳桩数,可以由规划同事提供初筛名单;第三类是距离数据,候选站到每个需求点的行驶距离或时间,量大优先用地图 API 批量计算,预算有限时先用米制坐标下的欧氏距离乘 1.3 的绕路系数近似;第四类是边界条件,包括规划建站上限、可用总投资、电网接入容量。

数据格式上我建议全部整理成 CSV 或 GeoJSON,坐标系统一到 GCJ-02,避免因为坐标系错位导致站址落到马路上。表格字段至少包含:候选站的 id、lng、lat、build_cost、capacity,需求点的 gid、demand_w、center_lng、center_lat。距离矩阵单独存成一个稀疏关系表:cand_id、gid、dist_m。

3.2 用 Python 生成需求栅格:500 米网格如何聚合权重

需求侧原始数据是一堆散点,不能直接当需求点进模型,否则变量数量爆炸。我一般先把城市按 500 米 × 500 米网格切分,把散点聚合成网格中心点作为需求点。网格大小影响求解规模:500 米适合主城区,1 公里适合全市范围粗选。先把经纬度转成米制坐标再切网格,这样格子才是正方形。

import pandas as pd import numpy as np # 读取兴趣点数据,字段至少包含 lng、lat、weight poi = pd.read_csv('poi_points.csv') # 转成 Web Mercator 米制坐标,避免直接用经纬度相减算距离 def web_mercator(lng, lat): x = lng * 20037583.34 / 180.0 y = np.log(np.tan((90.0 + lat) * np.pi / 360.0)) / (np.pi / 180.0) y = y * 20037583.34 / 180.0 return x, y poi['x'], poi['y'] = zip(*poi.apply(lambda r: web_mercator(r['lng'], r['lat']), axis=1)) # 500 米网格聚合成需求栅格 GRID_SIZE = 500 poi['gx'] = (poi['x'] // GRID_SIZE).astype(int) poi['gy'] = (poi['y'] // GRID_SIZE).astype(int) # 同一个格子内的权重累加,得到需求点的需求权重 demand = poi.groupby(['gx', 'gy']).agg( demand_w=('weight', 'sum'), center_lng=('lng', 'median'), center_lat=('lat', 'median') ).reset_index() # 剔除权重过低的空格子,减少后续求解规模 demand = demand[demand['demand_w'] >= 1.0] print(f"需求栅格数: {len(demand)}") demand.to_csv('demand_grid.csv', index=False)

这段代码的核心是把散点兴趣点聚合成可以直接参与选址模型的需求点。web_mercator函数解决的是坐标系问题:经纬度的 1 度在不同纬度代表的实际距离不一样,直接相减算距离会产生系统性偏差。GRID_SIZE是网格边长,取值越小需求点越密集,求解变量越多;如果数据覆盖的是全市而非主城区,我建议改成 1000 再跑一次对比。最后过滤掉demand_w < 1.0的格子,是为了去掉偶然出现的孤立点,避免模型为了覆盖一个权重极低的需求点而多建一个站。

3.3 用 PuLP 求解选址实例:覆盖率约束与候选站上限

数据就绪后,用 PuLP 加内置 CBC 求解器就能处理小规模实例。下面的代码假设候选站 50 个以内、需求点 300 个以内,变量规模在可控范围内。

import pandas as pd import pulp # 读取三张表 candidates = pd.read_csv('candidate_sites.csv') # id, lng, lat, build_cost, capacity demand = pd.read_csv('demand_grid.csv') # gid, demand_w, center_lng, center_lat dist = pd.read_csv('distance_matrix.csv') # cand_id, gid, dist_m R = 3000 # 覆盖半径,单位米,按市区快充经验值取 3km P = 10 # 规划最多建站数量 MIN_COVER = 0.8 # 需求覆盖率下限 I = candidates['id'].tolist() J = demand['gid'].tolist() c = dict(zip(candidates['id'], candidates['build_cost'])) cap = dict(zip(candidates['id'], candidates['capacity'])) w = dict(zip(demand['gid'], demand['demand_w'])) d = {(r['cand_id'], r['gid']): r['dist_m'] for _, r in dist.iterrows()} prob = pulp.LpProblem("ev_charging_site_selection", pulp.LpMinimize) # 决策变量:候选站是否建设、需求点是否分配给某个站、是否被覆盖 y = pulp.LpVariable.dicts("build", I, cat="Binary") x = pulp.LpVariable.dicts("assign", (I, J), cat="Binary") cov = pulp.LpVariable.dicts("covered", J, cat="Binary") # 目标:建设成本 + 加权距离成本(0.001 是元/米尺度系数,避免距离项反向压倒成本项) prob += ( pulp.lpSum(c[i] * y[i] for i in I) + pulp.lpSum(0.001 * w[j] * d[(i, j)] * x[i][j] for i in I for j in J) ) # 每个需求点最多分配到一个候选站,且只有建站才能分配 for j in J: prob += pulp.lpSum(x[i][j] for i in I) <= 1 prob += cov[j] == pulp.lpSum(x[i][j] for i in I) for i in I: for j in J: prob += x[i][j] <= y[i] # 超出覆盖半径的分配直接禁止 for i in I: for j in J: if d[(i, j)] > R: prob += x[i][j] == 0 # 容量约束:站服务的需求权重不超过其日服务能力 for i in I: prob += pulp.lpSum(w[j] * x[i][j] for j in J) <= cap[i] * y[i] # 覆盖率约束:被覆盖需求权重不低于总权重的一定比例 prob += pulp.lpSum(w[j] * cov[j] for j in J) >= MIN_COVER * pulp.lpSum(w[j] for j in J) # 建站数量上限 prob += pulp.lpSum(y[i] for i in I) <= P solver = pulp.PULP_CBC_CMD(msg=False) prob.solve(solver) print("求解状态:", pulp.LpStatus[prob.status]) selected = [i for i in I if y[i].value() and y[i].value() > 0.5] print("选中站点:", selected) # 计算并输出实际覆盖率,方便和约束目标对比 covered_w = sum(w[j] * cov[j].value() for j in J) total_w = sum(w.values()) print(f"实际覆盖率: {covered_w / total_w:.2%}")

这段代码里,cov[j] == pulp.lpSum(x[i][j] for i in I)是关键,它把一个需求点是否被覆盖定义成“是否被任意一个站分配”,避免覆盖率和分配脱节。目标函数里的0.001经常被忽略,但非常重要:建设成本单位是万元,距离成本单位是元,如果直接用原始数值相加,距离项会小到可以忽略,模型就退化成纯成本模型。覆盖率 0.8、最多建 10 个站、容量约束三条共同防止模型给出“只建一个最便宜站”的极端解。如果求解状态是 Infeasible,优先检查是不是覆盖半径太小或容量上限太低。

3.4 结果不理想时先调哪个参数

模型跑完不等于工作结束。我拿到结果第一眼先看三样东西:选中站点是否集中在少数几个热点片区、实际覆盖率是贴着 0.8 还是超出很多、有没有某个站的容量利用率超过 90%。如果站点扎堆,通常是距离成本系数 0.001 设得太小,导致模型宁愿扎堆也不愿意照顾外围需求,这时候把系数上调到 0.003 再跑。如果覆盖率贴着 0.8 不动,说明 MIN_COVER 成了起作用的约束,可以把这个值提高到 0.85 或 0.9,观察成本增加多少。如果个别站容量爆表,则说明候选站名单本身有问题,比如某个地块虽然成本低,但它恰好在一圈高需求点的正中心,需要人工权衡是否把它拆成两个小站。

4. 候选点一多就换启发式:遗传算法编码、适应度与参数表

4.1 精确求解的黑匣子:变量数、求解时间与规模阈值

上一章的例子只有几千个变量,CBC 求解器几秒钟就出结果。但当候选站上升到 500 个、需求点上升到 2000 个,分配变量就是 100 万个二元变量,带容量约束之后,CBC 经常跑一两个小时还不见收敛,输出一个还凑合的可行解就停在那里。这不是代码写错了,而是混合整数规划本身的组合爆炸。

我的经验阈值大致是:无容量约束、变量数 5 万以下,开源求解器可以当作常规武器;变量数超过 20 万,就该考虑换商业求解器或者上启发式算法。这里提醒一点,商业求解器不是万能后悔药,哪怕是 Gurobi,遇到带容量约束的大规模选址,仍然会把大量时间花在证明最优性上。实际项目中我们往往只需要一个“比人工方案好 15% 且能被业务接受”的解,所以启发式算法不是退而求其次,而是更聪明的做法。

4.2 遗传算法编码与约束修补:染色体怎么设计才算合理

用遗传算法求解选址问题,最自然的编码方式是二进制染色体:染色体长度等于候选站数量,某位为 1 表示建设该站,0 表示不建。但直接随机生成初始种群会制造大量不满足“最多建 P 个站”的个体,如果靠适应度函数惩罚这些个体,搜索效率会很差。常见做法是在生成个体时做约束修补:先随机选若干个位点置 1,数量控制在 P 以内,其余全部置 0,这样每个个体天然满足数量上限。

适应度函数里要同时处理覆盖率和容量约束。覆盖率低于下限时加大惩罚项,容量超限时按超出比例线性惩罚。下面是一段适应度函数的核心逻辑,可以套进任意 Python 遗传算法框架中。

def fitness(individual, sites, demand, dist, R, P, min_cover, alpha=0.7): # individual: 0/1 列表,长度等于候选站数量 build_cost = sum(sites[i].build_cost for i in range(len(individual)) if individual[i] == 1) if sum(individual) > P: return 1e9 # 超过建站数量上限,直接给极大惩罚 total_w = demand['demand_w'].sum() covered_w = 0.0 dist_cost = 0.0 for _, r in demand.iterrows(): in_range = [ i for i in range(len(individual)) if individual[i] == 1 and dist[(i, r['gid'])] <= R ] if not in_range: continue # 如果有多个站都在半径内,按最近的那个计距离成本 nearest = min(dist[(i, r['gid'])] for i in in_range) covered_w += r['demand_w'] dist_cost += r['demand_w'] * nearest coverage = covered_w / total_w penalty = 0.0 if coverage < min_cover: penalty = (min_cover - coverage) * 1e6 # 覆盖率惩罚项 return alpha * build_cost + (1 - alpha) * dist_cost + penalty

这段代码里alpha是成本与距离的权重,0.7 意味着更看重建设成本控制,适合预算紧张的项目;反过来 0.4 则更看重用户体验,布局会更分散。penalty的1e6是经验值,必须比正常目标值大一个数量级,才能把不满足覆盖率的个体淘汰掉。距离矩阵dist用的是上一章算好的关系表,遗传算法里需要频繁查dist[(i, r['gid'])],建议预存成字典而不是在循环里反复读 CSV,否则一次适应度评估就会慢到无法接受。

4.3 适应度函数与关键参数:种群、交叉、变异怎么定

遗传算法不是黑匣子,参数设对了效果立竿见影。下面的参数表是我在充电站选址场景里常用的初值,可以直接抄。

参数经验取值调整方向
种群规模40 到 100候选站多时取大,否则 40 够用
最大迭代代数100 到 300看适应度曲线是否到平台期
交叉概率0.7 到 0.9偏低会让种群很快同质化
变异概率1/L 到 3/L,L 为染色体长度1/L 是底线,小于它等于没变异
选择方式二元锦标赛简单稳定,不容易被超强个体主导
精英保留数2 到 5保留前几名,防止最好解丢失
终止条件连续 30 代最优适应度无改进比死等 300 代更省时间

需要特别说明的是变异概率。很多初学者把变异概率设成 0.01 以下,对于长度 500 的染色体,这意味着平均每代只有 5 个基因位会翻转,搜索基本停滞。我一般按染色体长度的倒数取,比如 500 个候选站就把变异率设到 0.002 到 0.006。种群规模不需要太大,因为容量约束会把大量随机个体判死,有效的多样性更多来自变异而不是种群大小。

5. 选址老翻车的 5 个坑:距离、权重与容量约束都踩过

5.1 直接用经纬度算直线距离,选出来的站全偏到中心

现象:用同一份数据跑模型,无论怎么调参数,选中站点总是往城市几何中心聚集,且和实际车流热力对不上。

原因:经纬度坐标在不同纬度下代表的实际距离完全不同,直接用经纬度差值当距离,相当于把高纬度的 1 度经度当成赤道的 1 度,距离被系统性拉近,导致模型误以为外围需求点离中心很近。更隐蔽的是,国内地图数据普遍是 GCJ-02 坐标系,而部分开源数据是 WGS-84,两套坐标系混用会让站点偏移几百米到几公里。

解决:所有坐标统一到同一坐标系,先按章节 3.2 的 Web Mercator 公式转成米制,再算距离。市区内如果有路网数据,优先用最短路距离;拿不到路网时,米制欧氏距离乘 1.3 的绕路系数,比直接经纬度算直线更接近真实。

5.2 用出行需求顶替充电需求,覆盖率虚高一倍

现象:模型算出的覆盖率高达 90%,但建完站后发现部分场站日订单量只有预期的三分之一。

原因:充电需求和出行需求是两回事。一个片区白天人流量大,只能说明有人经过,不代表他们会停下来充电。充电行为大多发生在目的地停留期间:住宅过夜、商场购物、办公楼上班。如果直接把人口热力或 POI 总量当需求权重,会把地铁站这种人流大但停车时间短的地方定义成高需求点。

解决:给不同类型 POI 设置不同的充电折算系数,而不是统一当作一个权重。我常用的折算系数表如下:

POI 类型充电折算系数理由
居住区0.2 到 0.3过夜慢充为主,快充需求集中在早高峰前
办公区0.15 到 0.25白天停车时间长,快充需求中等
商超综合体0.3 到 0.4停留 1 到 2 小时,是高价值快充场景
交通枢纽0.1 到 0.2车流大但停留短,快充需求反而低
高速服务区0.5 到 0.6停留目的性强,快充需求占比高

5.3 容量约束只写上限、不写排队,桩数变成纸面数据

现象:模型输出某个站配置 6 个桩,理论产能充足,但实际高峰时段排队半小时,用户差评如潮。

原因:容量约束写的是“一天最多服务多少车次”,这是个总量指标,没考虑充电需求的时间分布。充电高峰集中在傍晚和周末,峰值小时需求可能是平均值的 3 到 4 倍。一个站在高峰期可能出现排队,而总容量数据看起来还是够的。

解决:在容量约束里打一个高峰集中系数。比如日均服务能力 96 车次,峰值系数取 1.5,则模型里该站的容量上限设为 64 车次,相当于强制预留 50% 的裕量。如果项目要求排队等待时间不超过 10 分钟,就要引入 M/M/C 排队模型的近似公式,把等待时间折算成容量折减。第一阶段用峰值系数足够,第二阶段再上排队仿真。

5.4 成本权重拍脑袋,算法结果运营不接受

现象:算法跑出一版“全局最优”方案,业务部门看到之后说成本高、位置偏,直接推翻重做。

原因:目标函数里建设成本、距离成本、覆盖率三者的权重是主观定的,不同权重组合能产出完全不同的站址组合。如果建设成本权重给得过高,模型会优先选郊区便宜地块;如果距离成本权重过高,模型又把站往租金最贵的市中心挤。运营部门关注的其实是单站盈亏,而不是全局最优。

解决:不要一次性给死权重。先把纯成本模型和纯覆盖模型各跑一遍,得到两个极端解,看总成本和覆盖率的差值范围。再结合实际投资预算圈定权重区间。我习惯把建设成本权重固定在 0.6 到 0.7 之间,然后用距离成本权重做敏感性分析表,给业务方展示“多花 200 万能把平均距离从 5.2 公里压到 4.1 公里”,让他们拍板,而不是算法替他们拍板。

5.5 只做静态快照不做增长预留,第二年就得返工

现象:按当前需求分布算出的最优方案,第二年就出现部分站点排队严重,而另一些新建小区周边又没有站。

原因:充电需求不是静态的。新能源车保有量在增长,单车充电功率从 60kW 往 120kW、240kW 走,需求重心也在跟着城市扩张移动。静态快照模型只优化当前时刻,没有考虑站点的可扩展性和电网预留容量。

解决:做至少三套需求情景:保守、基准、激进,分别对应未来 3 到 5 年的保有量增速和平均功率提升幅度。在候选站数据里增加一个“可扩展性”字段,比如可扩容桩数、电网接入余量,然后在约束里对高增长区域设置最低覆盖率要求。这样算出的站址不会在第二年就被迫废弃,至少保留了扩容空间。

6. 选址结果的闭环验证:敏感性分析、模型校准与决策输出

模型跑完不是终点,还要验证它是否值得信任。我最常用的验证方式是把已有运营站点的历史订单数据拿出来,让模型只在这些站点里做一次重算,对比模型输出的覆盖权重和真实订单量的相关性。如果相关系数低于 0.6,说明需求权重定义有问题,回去先调 POI 折算系数,而不是继续调求解器参数。

敏感性分析是给决策层看的第二张牌。固定其他参数,单独把建站数量上限从 5 逐步增加到 15,记录每个方案的总成本和覆盖率,画成一张曲线表。你会看到覆盖率从 0.6 涨到 0.8 时成本增长平缓,再从 0.8 涨到 0.9 时成本陡增。曲线拐点附近就是预算和体验平衡的位置。同样方法可以用于覆盖半径和距离成本权重。

最后一步,把算法输出整理成三档候选站清单:必选站、备选站、淘汰站。必选站是多个情景下都稳定出现的站,这类站点不存在争议,直接进建设计划;备选站是在基准情景下成立、但激进情景下需要观察的站,放进储备库,等需求数据更新后再评估;淘汰站则可以明确告诉业务方不建议投资的理由。这样输出的结果不是一堆坐标,而是一份决策依据。

我自己的习惯是每份选址报告附一页“模型未考虑因素”,例如峰谷电价政策变化、周边竞争场站的价格战、土地租期风险。这页内容常常比模型本身还重要。选址优化研究的价值不只是算出站址,而是帮你把模糊的直觉变成可以比较、可以推翻、可以迭代的决策过程,希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询