资讯动态

Python数据分析实战:公交站点设置优化与客流覆盖建模

发布时间:2026/9/29 20:58:30 来源:尧图企业网站定制
简介这份PDF教程面向具备一定Python基础、希望用数据分析解决实际问题的学习者以公交站点设置优化为完整案例串联数据清洗、客流量统计、站点间距计算、DBSCAN聚类与可视化等环节帮助读者掌握从原始GPS与刷卡数据到优化建议的全流程分析方法。资源包共1个PDF文件约144KB内容涵盖数据预处理、站点客流量分析、站点间距离计算、聚类分析、结果解读与优化策略等模块并配有pandas、geopy、sklearn、matplotlib等库的示例代码便于对照练习。目前已有178人学习。读者可借此理解如何将统计与机器学习方法落地到公共交通场景获得可复用的分析思路、代码片段与排错参考适合课程设计、项目实战或数据分析入门进阶使用。1. 公交站点设置优化为什么“拍脑袋挪站”总在早晚高峰翻车早高峰的公交调度室里最常见的争论不是“加几辆车”而是“这个站到底该不该挪”。运营方觉得某站离路口太近、进站排队堵住右转车道居民却觉得撤了站要多走四百米。两边都有理可谁也拿不出数据。Python 数据分析在公交站点设置优化里的价值恰恰是把这种“凭感觉吵”变成“拿客流和步行距离算”。它要回答的核心问题很具体现有站点覆盖了多少真实出行需求、哪些站点间距过密或过疏、挪动或合并之后总社会成本是升还是降。这套分析适合三类人做交通/规划方向的数据分析从业者手里有 IC 卡刷卡或 GPS 轨迹数据但不知道怎么落地城市规划、公交运营岗位的工程师想用可复现的脚本替代经验判断以及正在找数据分析项目练手的同学公交数据字段干净、业务闭环清晰比很多“玩具数据集”更接近真实决策。整条链路不复杂清洗刷卡与站点数据、算客流与覆盖、建优化目标、跑候选方案、做敏感性验证。下面按这个顺序拆开讲每一步都给能直接抄的代码和参数。2. 数据准备与站点客流画像从刷卡记录到“每站每小时上车人数”站点优化的第一步不是建模是把原始数据变成“每个站点、每个时段、每个方向的上车人数”。这一步做不干净后面所有优化都是空中楼阁。公交数据常见的来源有三类IC 卡/乘车码刷卡记录含卡号、线路、车辆、时间、站点编号、车辆 GPS 轨迹含经纬度、时间、速度、以及站点基础表站点编号、名称、经纬度、所属线路。三张表靠线路号、车辆号、站点编号关联。2.1 刷卡数据清洗三个必须处理的脏数据刷卡记录里最影响客流统计的是三类问题重复刷卡同一卡号同一车辆几十秒内多次记录、时间戳异常跨天、时区错乱、设备重启导致的乱序、以及站点编号缺失GPS 漂移或司机未报站。处理原则是先标记再决定丢弃还是修补不要一上来就dropna否则高峰时段的缺失会被系统性低估。import pandas as pd import numpy as np # 读取刷卡记录字段按实际列名调整 df pd.read_csv(card_records.csv, parse_dates[tap_time]) # 1. 去重同一卡号车辆站点60秒内只保留第一条 df df.sort_values(tap_time) df[gap] df.groupby([card_id, bus_id])[tap_time].diff().dt.total_seconds() df df[(df[gap].isna()) | (df[gap] 60)].drop(columnsgap) # 2. 时间异常只保留运营时段 05:00-24:00 df df[(df[tap_time].dt.hour 5) (df[tap_time].dt.hour 24)] # 3. 站点缺失按同车辆前后记录的站点编号做线性插值仅限相邻两条 df[stop_id] df.groupby(bus_id)[stop_id].ffill(limit1) # 输出每站每小时上车人数 df[hour] df[tap_time].dt.hour flow df.groupby([stop_id, hour]).size().reset_index(nameboardings) flow.to_csv(stop_hourly_flow.csv, indexFalse)逻辑说明去重用的是“同卡同车 60 秒窗口”这个阈值来自常见刷卡设备的重复上报间隔设太小会漏掉真实换乘设太大比如 300 秒会把同一乘客中途下车再上车的记录误删。时间过滤卡在 05:00–24:00 是多数城市公交运营时段夜班线路要单独放宽。站点插值只做limit1因为连续缺失两条以上说明 GPS 或报站系统有系统性问题插值会造假这种情况应该回退到 GPS 轨迹匹配。参数上gap 60的 60 是经验值如果你的数据设备上报频率是 30 秒可以调到 45ffill(limit1)的 1 表示最多补一条超过就保留 NaN 并在后续统计里单独标记。2.2 站点覆盖分析用步行半径算“有效服务人口”客流只说明“有多少人在这上车”不说明“这个站位置合不合理”。判断合理性要看覆盖以站点为圆心、步行可达距离为半径圈内覆盖了多少居住/就业/商业 POI。常见做法是用站点经纬度做缓冲区和人口栅格或 POI 点做空间叠加。没有 GIS 环境时用geopandas加一个投影坐标系就能算。import geopandas as gpd from shapely.geometry import Point # 站点表stop_id, lon, lat stops pd.read_csv(stops.csv) stops_gdf gpd.GeoDataFrame( stops, geometrygpd.points_from_xy(stops[lon], stops[lat]), crsEPSG:4326 ).to_crs(EPSG:3857) # 投影到米制坐标缓冲区半径才能用米 # 步行半径常规站 500m换乘枢纽 800m stops_gdf[buffer_r] np.where(stops_gdf[is_hub], 800, 500) stops_gdf[geometry] stops_gdf.apply( lambda r: r[geometry].buffer(r[buffer_r]), axis1 ) # 人口栅格点或 POI 点叠加 pop gpd.read_file(population_points.geojson).to_crs(EPSG:3857) joined gpd.sjoin(pop, stops_gdf, predicatewithin) coverage joined.groupby(stop_id)[population].sum().reset_index() coverage.to_csv(stop_coverage.csv, indexFalse)逻辑说明投影到 EPSG:3857 是为了让buffer(500)的单位是米而不是度这是新手最容易翻车的地方——直接用经纬度做 500 的缓冲实际半径会随纬度变化在高纬度城市能差出几百米。is_hub区分枢纽站和普通站枢纽覆盖半径大是因为乘客愿意为换乘多走一段。sjoin的predicatewithin表示人口点落在站点缓冲区内才算覆盖反过来用contains结果一样但语义更绕。参数上500 米是国内多数城市公交站点的常规服务半径参考值山地或老城区可以降到 300800 米用于轨道交通接驳站。人口点如果太稀疏覆盖数会偏低这时应该换成 100m×100m 的人口栅格再转点。2.3 站点间距与客流匹配找出“过密”和“过疏”的候选有了客流和覆盖就能做第一轮筛选间距小于 300 米且两站客流都低的是合并候选间距大于 800 米且中间有覆盖盲区的是新增候选。这一步用线路走向上的站点序列算相邻间距。# 按线路和方向排序后算相邻站间距 stops_sorted stops_gdf.sort_values([route_id, direction, seq]) stops_sorted[prev_geom] stops_sorted.groupby( [route_id, direction])[geometry].shift(1) stops_sorted[dist_m] stops_sorted.apply( lambda r: r[geometry].distance(r[prev_geom]) if r[prev_geom] else np.nan, axis1 ) # 合并候选间距300m 且两站日均客流都低于线路中位数 median_flow flow.groupby(stop_id)[boardings].sum().median() candidates stops_sorted[ (stops_sorted[dist_m] 300) (stops_sorted[stop_id].map(flow.groupby(stop_id)[boardings].sum()) median_flow) ]逻辑说明shift(1)拿到同线路同方向的前一站distance算的是投影后的米制距离。合并候选的两个条件必须同时满足——只看间距会把两个都繁忙的短间距站误判为可合并只看客流会把相隔很远的小站也列进来。median_flow用线路中位数而不是全局中位数是因为不同线路客流基数差异大跨线路比较没有意义。到这里数据准备和画像就完成了。产出的stop_hourly_flow.csv、stop_coverage.csv和候选站点表是后面优化模型的输入。这一步的常见返工是站点编号不统一不同线路对同一物理站用不同编号需要在站点基础表里先做一次“物理站合并”否则同一位置会被算成两个站间距和覆盖全错。3. 优化模型怎么建把“挪站”翻译成可求解的目标函数数据画像回答“现状如何”优化模型回答“怎么改更好”。公交站点设置优化本质上是一个带约束的选址问题在候选位置里选一组站点使得总出行成本最小同时满足站间距、覆盖率、线路长度等约束。不要一上来就上复杂的混合整数规划先用可解释的目标函数把问题框清楚再决定要不要加求解器。3.1 目标函数乘客步行成本 运营停站成本一个实用的目标函数由两部分组成乘客侧的总步行时间站点覆盖范围内乘客到站距离之和和运营侧的停站时间成本站点越多每趟车停站越多全程时间越长。两者是矛盾的——站多覆盖好但运营慢站少运营快但乘客走得远。把它们加权求和权重反映决策偏好。# 简化目标对每个候选站点集合 S # cost w_walk * sum(乘客到最近站距离) w_stop * |S| * 平均停站时间 def total_cost(selected_stops, demand_points, w_walk1.0, w_stop0.5, stop_time30): # demand_points: 每个需求点的 (x, y, 人数) walk_cost 0 for _, dp in demand_points.iterrows(): d min( ((dp[x] - s[x])**2 (dp[y] - s[y])**2) ** 0.5 for s in selected_stops ) walk_cost d * dp[population] stop_cost len(selected_stops) * stop_time * w_stop return w_walk * walk_cost stop_cost逻辑说明walk_cost是需求点到最近选中站点的欧氏距离乘以人数单位是“人·米”代表总步行负担。stop_cost用站点数乘以平均停站时间再乘权重代表运营侧代价。w_walk和w_stop是决策权重调大w_walk会倾向多设站调大w_stop会倾向少设站。stop_time30是每站平均停靠秒数的经验值含开关门和加减速。参数上w_walk和w_stop没有标准值建议先各取 1.0 跑一版看选出的站点数和现状差多少再根据运营方和乘客代表的意见调整。stop_time在拥堵路段可以设到 45畅通路段 20。3.2 候选站点生成不要在全城网格上硬搜直接在全城连续空间上优化计算量巨大常见做法是先离散化把现状站点、合并候选、新增候选、以及线路沿线每隔 200 米的点作为候选集再在候选集里做选择。这样既保留了现实可行性站点要落在路边又把搜索空间压到可解规模。# 候选集 现状站 合并候选 沿线加密点 candidates pd.concat([ stops_gdf[[stop_id, geometry]].assign(srcexisting), merge_candidates[[stop_id, geometry]].assign(srcmerge), densify_points[[stop_id, geometry]].assign(srcdensify), ]).drop_duplicates(subsetgeometry) # 用贪心做初始解每次加入能最大降低总成本的候选 selected [] remaining list(candidates.index) for _ in range(max_stops): best, best_gain None, -np.inf for idx in remaining: trial selected [candidates.loc[idx]] gain total_cost(selected, demand) - total_cost(trial, demand) if gain best_gain: best, best_gain idx, gain if best_gain 0: break selected.append(candidates.loc[best]) remaining.remove(best)逻辑说明贪心算法每一步选“加入后总成本下降最多”的候选直到没有正收益或达到站点数上限。它不保证全局最优但速度快、结果可解释适合做第一版方案给业务方看。max_stops是站点数上限可以设成现状站点数的 1.2 倍避免方案过于激进。参数上densify的间隔 200 米是经验值太密候选集大、求解慢太疏会漏掉合理位置。贪心的终止条件best_gain 0表示再加站已经不划算这是权重设置下的自然结果。3.3 约束条件站间距、覆盖率、线路时长没有约束的优化会给出“把站全撤了”这种数学上最优、现实中不可行的解。必须加三类约束站间距下限比如不小于 300 米、覆盖率下限比如 90% 需求点步行 500 米内可达、线路单程时长上限比如不超过现状的 1.1 倍。def is_feasible(selected, demand, min_gap300, min_cover0.9, max_time_ratio1.1): # 站间距约束 for i in range(len(selected)): for j in range(i 1, len(selected)): if selected[i][geometry].distance(selected[j][geometry]) min_gap: return False # 覆盖率约束 covered sum( 1 for _, dp in demand.iterrows() if min( ((dp[x] - s[x])**2 (dp[y] - s[y])**2) ** 0.5 for s in selected ) 500 ) if covered / len(demand) min_cover: return False # 线路时长约束用站点数近似 if len(selected) len(stops_gdf) * max_time_ratio: return False return True逻辑说明min_gap防止两站过近min_cover保证大部分乘客仍有站可到max_time_ratio用站点数比例近似线路时长比例因为停站时间是线路时长的主要变量。三个约束在贪心过程中作为过滤条件不满足的候选直接跳过。参数上min_gap300是常规下限快速公交可以设 500min_cover0.9是覆盖率底线低于这个值方案很难通过评审max_time_ratio1.1表示允许线路变慢 10%超过就要重新评估。模型建好后跑出来的是一组站点集合。但数学最优不等于现实可落地下一章讲怎么把结果翻译成可执行的调整方案以及怎么验证。4. 从模型结果到落地方案候选站排序、影响评估与灰度验证模型给出的是“选哪些站”落地要回答的是“先动哪个、动了会怎样、怎么证明有效”。这一步是把分析结果转成运营语言核心是三件事给候选调整排优先级、评估每个调整的影响面、设计小范围验证。4.1 候选调整排序用“净收益/影响人数”做优先级每个候选调整合并、迁移、新增都有收益和代价。收益是总成本下降代价是受影响乘客数。优先级用“单位影响人数的净收益”排序比单纯按收益排更稳因为影响人数少的调整更容易推进。# 对每个候选调整算净收益和影响人数 adjustments [] for cand in candidate_adjustments: cost_before total_cost(current_stops, demand) cost_after total_cost(apply_adjustment(current_stops, cand), demand) net_gain cost_before - cost_after affected count_affected_passengers(cand, demand) # 步行距离变化100m 的乘客 adjustments.append({ adjust_id: cand[id], type: cand[type], net_gain: net_gain, affected: affected, priority: net_gain / max(affected, 1) }) adj_df pd.DataFrame(adjustments).sort_values(priority, ascendingFalse)逻辑说明net_gain是调整前后的总成本差正数表示改善。affected统计步行距离变化超过 100 米的乘客数这个阈值表示“明显感知到变化”。priority用净收益除以影响人数避免“收益大但得罪一大片人”的调整排在最前。参数上100 米的感知阈值可以按城市调整老年乘客多的区域可以降到 50 米。max(affected, 1)防止除零。4.2 影响评估分时段、分人群看谁受益谁受损一个调整在早高峰可能改善在平峰可能恶化。影响评估要分时段做还要看人群结构——通勤客对时间敏感老年客对步行距离敏感。用前面的stop_hourly_flow按时段拆开算。# 分时段评估某个调整 for period in [morning_peak, off_peak, evening_peak]: demand_p demand[demand[period] period] gain_p total_cost(current_stops, demand_p) - total_cost(adjusted_stops, demand_p) print(f{period}: net_gain{gain_p:.0f})逻辑说明按时段拆开后如果某个调整只在平峰有收益、高峰反而变差就要谨慎因为高峰是公交系统的核心服务时段。输出直接给业务方看比一个总数更有说服力。4.3 灰度验证先改一个站用两周数据回测方案再漂亮也要小范围验证。选优先级最高、影响人数最少的一个调整先做改站后收集两周刷卡数据和改站前同口径对比。验证指标就三个该区域总上车人数变化、平均步行距离变化、线路单程时长变化。# 改站前后对比同口径同线路、同方向、同时段 before flow[(flow[stop_id].isin(affected_stops)) (flow[week] before)] after flow[(flow[stop_id].isin(affected_stops)) (flow[week] after)] compare before.groupby(hour)[boardings].sum() - after.groupby(hour)[boardings].sum()逻辑说明对比必须同口径否则天气、节假日、线路调整都会干扰。affected_stops是受调整影响的站点集合包括被撤的站和承接客流的邻站。如果改站后总上车人数没降、步行距离没明显增加说明调整可行可以推广到下一个候选。灰度验证是这套方法里最容易被跳过、也最不该跳过的一步。模型是黑匣子现实有太多模型没考虑的因素——小区出入口位置、马路隔离栏、乘客习惯。两周数据不贵但能避免一次全线路调整翻车。5. 避坑与排查公交站点优化里最容易翻车的五件事这一章是血泪经验合集。下面五条都是实际做这类分析时反复出现的问题每条按现象、原因、解决写。现象一站点编号不统一同一物理站被算成两个。原因不同线路对同一位置用不同编号或者上下行站台分开编号。解决先做物理站合并用经纬度距离小于 50 米且站名相似度高的规则聚类生成统一的physical_stop_id所有分析基于物理站做。不做这一步间距和覆盖全错。现象二刷卡数据里的“上车人数”不等于“出行需求”。原因刷卡只记录付费乘客老人卡、员工卡、现金乘客可能不在同一张表里换乘乘客会被重复计数。解决先确认数据覆盖范围换乘用“同卡号 30 分钟内第二次刷卡”标记并去重缺失的免费卡群体用抽样调查比例做扩样。直接拿刷卡数当需求会系统性低估老年人和低收入群体。现象三缓冲区半径用经纬度直接算覆盖范围随纬度漂移。原因EPSG:4326 的单位是度1 度经度对应的米数随纬度变化。解决所有距离和缓冲区计算前先投影到米制坐标系EPSG:3857 或当地高斯投影。这个坑在低纬度城市不明显在北方城市能差出 30% 以上。现象四优化权重拍脑袋定方案忽左忽右。原因w_walk和w_stop没有业务依据调一下结果就大变。解决用现状方案反推权重——让现状站点集合在目标函数下接近最优得到的权重作为基准再做敏感性分析看权重变化 ±20% 时方案是否稳定。不稳定的方案不要推。现象五忽略线路运营约束方案数学最优但排班排不出来。原因只优化了站点没考虑单程时长、车辆周转、司机工时。解决把线路单程时长作为硬约束用站点数和平均停站时间估算超过现状 10% 的方案直接过滤。公交是运营系统不是纯数学题。提示这五条里前两条是数据问题中间两条是方法问题最后一条是业务问题。数据问题不解决后面全白做业务问题不解决方案推不动。6. 进阶技巧用敏感性分析判断方案稳不稳以及一个我常犯的错方案做完最后一关是问自己如果输入数据变一点结论还成立吗这就是敏感性分析。公交数据里最不确定的是需求点分布和权重设置把这两个变量各扰动 ±20%看选出的站点集合变化多大。变化小说明方案稳变化大说明结论依赖特定假设要谨慎。import numpy as np def sensitivity_analysis(demand, base_weights, n_runs50): results [] for _ in range(n_runs): # 需求点人数扰动 ±20% d demand.copy() d[population] * np.random.uniform(0.8, 1.2, len(d)) # 权重扰动 ±20% w_walk base_weights[w_walk] * np.random.uniform(0.8, 1.2) w_stop base_weights[w_stop] * np.random.uniform(0.8, 1.2) selected greedy_select(d, w_walk, w_stop) results.append(set(selected)) # 统计每个站点被选中的频率 freq {} for s in set().union(*results): freq[s] sum(1 for r in results if s in r) / n_runs return freq逻辑说明每次随机扰动需求人数和权重重跑贪心选择统计每个候选站点在 50 次运行里被选中的频率。频率高于 0.8 的站点是“稳健选择”低于 0.3 的是“边缘选择”。落地方案优先动稳健站点边缘站点留作备选。参数上n_runs50是精度和耗时的折中候选集大时可以降到 30。扰动幅度 ±20% 是经验值数据质量差时可以放宽到 ±30%。我常犯的一个错是过早追求“最优解”。刚做这类分析时总想上整数规划求解器把全局最优跑出来结果模型跑了几小时给出的方案因为一个约束没考虑周全被业务方一句话否掉。后来我改成先用贪心出可解释的初始方案和业务方对齐约束和目标再决定要不要上求解器精调。大部分场景下贪心加人工微调的结果和全局最优差不了几个百分点但沟通成本低一个数量级。公交站点优化是决策支持不是数学竞赛方案能被理解和执行比数学上最优更重要。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑