ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python数据分析实战:公交站点优化案例教程

2026/9/29 15:55:25 拓冰建站 浏览量
Python数据分析实战:公交站点优化案例教程 简介这份PDF教程面向具备一定Python基础、希望用数据分析解决实际问题的学习者以公交站点设置优化为完整案例串联数据清洗、客流量统计、站点间距计算、DBSCAN聚类与可视化等环节帮助读者掌握从原始数据到优化建议的完整分析链路。资源包内仅含1个PDF文件大小约144KB内容以图文与代码示例为主涵盖GPS监控数据与刷卡数据的预处理、站点客流量分析、站点间距离计算、聚类建模及热力图、聚类分布图等可视化实现并给出合并低流量站点、增设高流量站点、调整站点位置等具体优化策略。目前已有178人学习适合想通过真实城市交通场景练习pandas、geopy、scikit-learn与matplotlib等工具的数据分析学习者参考。1. 公交站点优化这件事为什么值得用 Python 从头跑一遍早高峰在小区门口等车眼看着三辆车连着进站下一趟却要再等十五分钟——这种体验背后其实是站点设置和线路调度的问题。这份《Python数据分析实战公交车站点设置优化分析案例教程编程实例课程详解.pdf》就是拿真实公交刷卡与 GPS 数据走一遍从数据清洗、客流统计到站点合并建议的完整链路。它适合两类人一类是刚学完 Python 基础语法、想找一个有业务背景的数据分析项目练手的另一类是做交通、规划、运营相关工作需要一套能直接套到自己城市数据上的分析框架的。整份材料以 PDF 教程形式组织配套代码实例核心不是讲 pandas 语法而是讲怎么把一堆杂乱的上下车记录变成「哪个站该合并、哪个站该挪位置」的决策依据。下面我按自己复现时的顺序把关键步骤和踩过的坑拆开讲。2. 数据从哪来、长什么样先把公交 IC 卡与 GPS 两张表对齐2.1 公交数据的两个核心来源公交站点优化分析本质上要回答两个问题人在哪上车、在哪下车以及车实际停在哪。前者来自 IC 卡刷卡记录后者来自车载 GPS 轨迹。这两份数据在大多数城市的公交系统里是分开存储的字段命名和采集频率都不一样所以第一步不是写分析代码而是搞清楚手里有什么。常见的 IC 卡数据字段包括卡号或脱敏后的卡 ID、刷卡时间戳、线路编号、车辆编号、上下车标志、站点编号。注意很多城市的刷卡机只记录上车下车靠乘客二次刷卡或干脆没有这时候就得用「出行链推断」的方法补全下车站点——常见做法是按同一张卡当天相邻两次上车记录把第二次上车的前一站当作第一次的下车站。这个推断有误差但比直接丢掉一半数据强。GPS 数据一般是车辆每隔 10 到 30 秒上报一次经纬度、瞬时速度、方向角和线路编号。它的作用是确定每个站点的真实坐标以及车辆在两个站点之间的行驶时间。把 GPS 轨迹和站点编号匹配上才能算出站间距和区间运行时长。2.2 用 pandas 做字段对齐与时间戳统一拿到数据后第一件事是统一时间格式。IC 卡系统常用的是「YYYYMMDDHHMMSS」这种紧凑字符串GPS 可能是 Unix 时间戳或带毫秒的 ISO 格式。不统一的话后面按时间窗口聚合会直接错位。import pandas as pd import numpy as np # 读取 IC 卡刷卡记录假设是 CSV字段名按实际调整 ic pd.read_csv(ic_card.csv, dtype{card_id: str, line_id: str, stop_id: str}) # 时间戳统一把紧凑字符串转成标准 datetime ic[swipe_time] pd.to_datetime(ic[swipe_time], format%Y%m%d%H%M%S) # 读取 GPS 轨迹 gps pd.read_csv(gps_track.csv, dtype{line_id: str, vehicle_id: str}) gps[report_time] pd.to_datetime(gps[report_time], units) # Unix 秒级时间戳 # 按线路和车辆分组检查时间范围是否覆盖同一天 print(ic.groupby(line_id)[swipe_time].agg([min, max]).head()) print(gps.groupby(line_id)[report_time].agg([min, max]).head())这段代码做了三件事指定字符串类型防止卡号被读成科学计数法、把两种时间格式统一成 pandas 的 datetime、按线路检查两份数据的时间覆盖范围。参数说明dtype里把 ID 类字段强制设为str是血泪经验卡号一旦被当成数字前导零会丢后面关联全乱。units表示 GPS 时间是秒级 Unix 时间戳如果是毫秒就改成unitms。2.3 站点坐标匹配与站间距计算GPS 点本身不是站点需要把轨迹点聚类或按到站减速特征识别出站点位置。简单做法是对每条线路取所有 GPS 点中速度低于某个阈值比如 5 km/h且持续超过 10 秒的点簇簇中心就是站点坐标。然后把识别出的坐标和 IC 卡数据里的站点编号做最近邻匹配。from scipy.spatial import cKDTree # 假设 stops 是站点编号与经纬度的对照表 stops pd.read_csv(stops.csv) # 字段stop_id, lat, lon # 把 GPS 低速点聚成候选站点 slow gps[gps[speed] 5].copy() slow[lat_r] slow[lat].round(4) slow[lon_r] slow[lon].round(4) candidates slow.groupby([line_id, lat_r, lon_r]).size().reset_index(namecnt) candidates candidates[candidates[cnt] 3] # 至少出现 3 次才算稳定站点 # 用 KDTree 做最近邻匹配 tree cKDTree(stops[[lat, lon]].values) dist, idx tree.query(candidates[[lat_r, lon_r]].values, k1) candidates[matched_stop] stops.iloc[idx][stop_id].values candidates[dist_deg] dist逻辑说明先按经纬度四舍五入到小数点后四位约 11 米精度做粗聚类再过滤掉出现次数太少的候选点避免把等红灯的位置误判成站点。KDTree 查询返回的是欧氏距离单位是度实际工程里要转成米再设阈值一般超过 50 米就认为匹配不可靠需要人工核对。这一步是整个分析的地基站点坐标错了后面所有客流归属都会偏。3. 客流统计与站点热度从刷卡记录算出每个站的上下客量3.1 按时间窗口聚合上下客量有了对齐好的数据下一步是统计每个站点在每个时间段的上车人数和下车人数。上车直接按刷卡记录的站点编号分组计数即可下车如果数据里有下车站点字段就直接用没有就用 2.1 说的出行链推断补。# 上车量直接按站点和小时聚合 ic[hour] ic[swipe_time].dt.hour board ic.groupby([stop_id, hour]).size().reset_index(nameboard_cnt) # 下车量如果有下车站点字段 if alight_stop_id in ic.columns: alight ic.groupby([alight_stop_id, hour]).size().reset_index(namealight_cnt) alight.rename(columns{alight_stop_id: stop_id}, inplaceTrue) else: # 出行链推断同一张卡按时间排序下一次上车的前一站视为本次下车 ic_sorted ic.sort_values([card_id, swipe_time]) ic_sorted[next_stop] ic_sorted.groupby(card_id)[stop_id].shift(-1) alight ic_sorted.dropna(subset[next_stop]).groupby([next_stop, hour]).size().reset_index(namealight_cnt) alight.rename(columns{next_stop: stop_id}, inplaceTrue) # 合并上下客量 flow pd.merge(board, alight, on[stop_id, hour], howouter).fillna(0) flow[total] flow[board_cnt] flow[alight_cnt]参数说明dt.hour提取小时用于分时段统计如果想看早晚高峰更细的粒度可以改成dt.floor(15min)按 15 分钟聚合。出行链推断里shift(-1)取的是同一张卡的下一次上车记录dropna丢掉当天最后一次上车因为没有下一次记录无法推断下车站。这个推断对通勤乘客准确率较高但对一天只刷一次卡的乘客无效实际项目中通常只对推断置信度高的记录做分析。3.2 站点热度排序与可视化统计出每个站点的总客流后可以按总量排序找出 Top N 热点站也可以按小时看客流分布形态。常见做法是画一张「站点-小时」的热力图横轴是小时纵轴是站点颜色深浅代表客流量。import matplotlib.pyplot as plt import seaborn as sns # 取客流前 20 的站点 top_stops flow.groupby(stop_id)[total].sum().nlargest(20).index pivot flow[flow[stop_id].isin(top_stops)].pivot_table( indexstop_id, columnshour, valuestotal, fill_value0 ) plt.figure(figsize(14, 8)) sns.heatmap(pivot, cmapYlOrRd, linewidths0.5) plt.title(Top 20 站点分时客流热力图) plt.xlabel(小时) plt.ylabel(站点编号) plt.tight_layout() plt.savefig(stop_heatmap.png, dpi150)逻辑说明nlargest(20)取总客流最大的 20 个站点pivot_table把长表转成「站点×小时」的宽表fill_value0把没有记录的时段填零。热力图能一眼看出哪些站是早高峰单峰型住宅区、哪些是晚高峰单峰型商业区、哪些是全天平稳型医院、枢纽。这个形态分类对后面判断站点该不该合并很关键——两个都是早高峰单峰的站合并后可能加剧拥挤一个早高峰一个晚高峰的站合并反而能平衡运力。3.3 站间距与重复站点识别站点优化的核心动作之一是合并距离过近的站点。一般城市公交站间距在 500 到 800 米比较合理低于 300 米的相邻站就值得考虑合并。用前面匹配好的站点坐标计算同一条线路上相邻站点的实际距离。from math import radians, sin, cos, sqrt, atan2 def haversine(lat1, lon1, lat2, lon2): R 6371000 # 地球半径单位米 dlat radians(lat2 - lat1) dlon radians(lon2 - lon1) a sin(dlat/2)**2 cos(radians(lat1)) * cos(radians(lat2)) * sin(dlon/2)**2 return R * 2 * atan2(sqrt(a), sqrt(1-a)) # 按线路和站点顺序计算相邻站间距 stops_sorted stops.sort_values([line_id, stop_seq]) stops_sorted[next_lat] stops_sorted.groupby(line_id)[lat].shift(-1) stops_sorted[next_lon] stops_sorted.groupby(line_id)[lon].shift(-1) stops_sorted[dist_m] stops_sorted.apply( lambda r: haversine(r[lat], r[lon], r[next_lat], r[next_lon]) if pd.notna(r[next_lat]) else np.nan, axis1 ) # 找出间距小于 300 米的相邻站对 close_pairs stops_sorted[stops_sorted[dist_m] 300][[line_id, stop_id, next_stop_id, dist_m]]参数说明haversine公式算的是球面距离比直接用经纬度差乘系数准确尤其在纬度较高的城市。stop_seq是站点在线路上的顺序号没有这个字段的话需要按 GPS 轨迹的先后顺序自己排。shift(-1)取下一站最后一站没有下一站所以结果是 NaN用pd.notna过滤掉。300 米这个阈值不是固定的核心城区可以放宽到 250 米郊区可以放到 400 米要看当地居民的步行习惯和道路条件。4. 优化建议怎么落地合并方案、仿真验证与常见翻车点4.1 站点合并的决策逻辑识别出距离过近的站点对之后不能直接说「合并」。还要看两个站的客流是否互补、合并后新站的位置是否方便两边乘客。我一般会按下面这个优先级判断判断维度合并倾向不合并倾向站间距小于 300 米大于 500 米两站客流之和合并后不超过单站最大容量合并后严重超载客流时间分布一个早高峰一个晚高峰都是早高峰周边用地同一片区、无隔断被主干道或河流隔开换乘需求无其他线路换乘是重要换乘节点这张表不是拍脑袋定的是复现多个城市案例后总结的经验值。实际项目中我会把每个维度量化成分数加权求和后排序把得分最高的前几对作为优先合并候选再交给规划人员人工确认。4.2 用仿真思路验证合并效果合并方案提出后怎么知道它好不好最直接的办法是做一次简单的客流重分配仿真假设原来在 A 站上车的人合并后走到新站 C 上车步行距离增加部分乘客可能流失。可以用一个简化的 Logit 模型估算流失率。import numpy as np def simulate_merge(flow_a, flow_b, walk_extra_min, beta-0.1): flow_a, flow_b: 合并前两站各自客流 walk_extra_min: 合并后平均多走的步行时间分钟 beta: 步行时间敏感系数负值表示时间越长客流流失越多 total flow_a flow_b retention np.exp(beta * walk_extra_min) # 简化保留率 return total * retention # 示例A 站 500 人B 站 300 人合并后平均多走 3 分钟 new_flow simulate_merge(500, 300, 3) print(f合并后预计客流{new_flow:.0f} 人流失 {800 - new_flow:.0f} 人)参数说明beta是步行时间敏感系数取值一般在 -0.05 到 -0.15 之间绝对值越大表示乘客越不愿意多走路。这个值需要根据当地调研数据标定没有调研数据时可以用 -0.1 做敏感性分析。retention是保留率exp(beta * walk_extra_min)在步行增加 3 分钟、beta-0.1 时约为 0.74意味着约四分之一乘客会流失。这个模型很粗糙但能快速筛掉那些「合并后客流掉太多」的方案避免在明显不合理的方案上浪费时间。4.3 避坑与常见问题排查现象一刷卡记录里同一张卡同一时间出现多条记录。原因通常是刷卡机重复上传或数据同步延迟。解决方法是按card_id swipe_time去重保留第一条同时记录去重数量用于评估数据质量。现象二GPS 轨迹漂移导致站点坐标偏移几百米。原因是城市高楼区信号反射。解决方法是不要用单点定位而是取车辆停靠期间多个 GPS 点的均值或者用地图匹配算法把轨迹吸附到路网上再提取站点。现象三出行链推断的下车站点大量落在终点站。原因是很多乘客当天只刷一次卡shift(-1)取不到下一次记录被错误地归到了线路终点。解决方法是对只刷一次卡的记录单独标记不参与下车站推断或者用历史出行规律做补充推断。现象四合并方案在数据上成立实际执行被否决。原因往往是忽略了道路物理隔离、小区出入口位置、老年人步行能力等数据里看不到的因素。解决方法是把数据分析结果作为输入而不是结论最终方案一定要结合实地踏勘。现象五不同线路的站点编号体系不统一。同一物理站点在不同线路上可能有不同编号直接按编号合并会漏掉跨线路的重复站。解决方法是先用坐标做空间聚类给每个物理站点分配统一 ID再按统一 ID 聚合客流。5. 把分析脚本变成可复用的工具参数化与结果校验5.1 用配置文件管理分析参数每次换一个城市的数据站间距阈值、步行敏感系数、时间窗口粒度这些参数都要改。如果全写在代码里改一次就要翻一遍脚本。我习惯把参数抽到一个 YAML 或 JSON 文件里代码只读配置。import yaml with open(config.yaml, r, encodingutf-8) as f: cfg yaml.safe_load(f) # config.yaml 内容示例 # merge_distance_m: 300 # walk_beta: -0.1 # time_window: 15min # top_n_stops: 20 merge_threshold cfg[merge_distance_m] beta cfg[walk_beta]逻辑说明yaml.safe_load比eval安全不会执行任意代码。配置文件里还可以放数据路径、输出目录、线路白名单等。这样换项目时只改配置不改代码也方便把不同城市的参数存档对比。5.2 结果校验三个必须做的检查分析结果出来之后别急着出报告。我每次都会强制走一遍这三个检查第一总量守恒检查。合并前后的总客流应该基本一致考虑流失率后允许小幅下降如果合并后总客流反而增加说明统计逻辑有重复计数。第二空间合理性检查。把合并方案画在地图上看新站位置是否落在道路两侧都方便到达的地方有没有落在路口正中间或小区围墙后面。第三极端值检查。看单个站点的最大客流是否超过车辆额定载客量乘以班次数如果超了说明要么客流统计算重了要么该站确实需要加车而不是合并。# 总量守恒检查示例 before_total flow[total].sum() after_total sum(simulate_merge(row[flow_a], row[flow_b], row[walk_extra]) for _, row in merge_candidates.iterrows()) print(f合并前总客流{before_total:.0f}) print(f合并后预计总客流{after_total:.0f}) print(f变化率{(after_total - before_total) / before_total * 100:.1f}%)如果变化率超过 -15%就要回头检查是不是步行时间估得太高或者 beta 取值太激进。这个检查帮我拦下过好几次「看起来很美但实际会赶跑乘客」的方案。5.3 一个具体技巧用分位数代替平均值做站点分级很多教程用平均客流给站点分级但客流分布通常是长尾的平均值会被少数大站拉高导致中小站全被归到「低客流」一类。我习惯用分位数把站点按总客流排序前 20% 为一级站20% 到 50% 为二级站后 50% 为三级站。这样分级更稳定也不会因为某个枢纽站客流特别大就把其他站的级别压下去。flow_total flow.groupby(stop_id)[total].sum().reset_index() flow_total[grade] pd.qcut(flow_total[total], q[0, 0.5, 0.8, 1.0], labels[三级, 二级, 一级]) print(flow_total[grade].value_counts())参数说明pd.qcut按分位数切分q[0, 0.5, 0.8, 1.0]表示后 50% 为三级、50% 到 80% 为二级、前 20% 为一级。这个比例可以根据城市规模调整大城市枢纽多一级站比例可以放到 30%。分级结果直接决定了后续资源投入的优先级比单纯看绝对客流数字更有操作性。从那以后我每次做站点优化分析都会先把配置文件和这三个校验步骤跑一遍再去看具体的合并建议。数据能告诉你哪里有问题但告不告诉你合并之后乘客愿不愿意走那多出来的两百米得靠这套校验和实地判断一起兜底。希望帮到你。本文还有配套的精品资源点击获取