2003年国内第一条高铁开通到2025年二十多年时间里高铁网络和民航网络的每一次调整其实都写在公开数据里。高铁开通时间、高铁线路信息、高铁列车信息、飞机航线信息数据这些名词单独看都很好理解可真要把它们整理成一份能用于研究“高铁开通后同通道航线怎么变”的数据集会碰到一堆意想不到的问题。这篇内容就以我整理2003-2025年交通数据的过程为例聊聊字段怎么设计、脏数据怎么洗、分析怎么跑、图表怎么画如果你也想做类似的数据项目可以直接照着这套思路走。做数据项目和写爬虫不是一回事。最核心的不是代码能力而是对业务的理解。高铁和民航看似都是“出行方式”背后却遵循完全不同的运营逻辑。高铁有固定线路、固定车站、固定运行图民航有航路、机场、航班计划。想把两套体系放进同一个分析框架第一步不是急着抓数据而是先把数据集的范围和字段设计好。下面从我的实际操作经验出发按项目推进顺序拆开讲。1. 先想清楚再动手数据集的范围与字段设计1.1 为什么非要把高铁和民航数据放在一起单独整理高铁数据或者民航数据都有现成的方法。可一旦要研究“高铁对民航的影响”就必须先把两边数据对齐到同一条走廊上。人的直觉是高铁开通后同线路的飞机会变少但“少多少”“什么时候开始少”“有没有恢复”这些问题必须靠数据回答。我之前被问得最多的问题就两个某条线路是什么时候开通的某两个城市之间每天有多少趟高铁、多少班飞机这两个问题看似简单背后分别对应线路时间表和运营时刻表两套完全不同的数据体系。高铁开通时间数据通常分散在开通新闻、百科词条、地方交通志里而且很多线路是分期开通的。比如一条线上先开通市中心到某个中间站过两年再开通剩余路段如果不按段落记录后面画时间轴就会出错。高铁列车信息则来自运行图和票务系统车次、始发终到、经停站、运行时长这些字段变化频率高还带有明显的季节性调整。飞机航线信息同样复杂航班计划分冬春和夏秋两季航班号、班期、执飞机型会变。这些数据单独拿出来都是常规整理工作但放一起就需要统一的结构。所以我这个数据集项目从第一天起就定了一个原则用“线路”作为高铁数据的骨架用“通道”作为连接高铁和民航的桥梁。线路是物理存在通道是分析概念。一条高铁线路可能经过多个城市一条民航航线也可能连接两个没有高铁直达的城市。只有把数据都归到“通道”这个虚拟维度上才能做公平对比。这个设计思路直接决定了后面所有的表结构。1.2 开通时间、线路、列车、航线四张表怎么设计才不打架我最终采用四张核心表结构每张表解决一个特定问题。表名主要字段解决的问题line_info线路ID、线路名称、起点城市、终点城市、主要途经点、设计时速、里程、线路类型、当前状态高铁线路的基本档案line_opening线路ID、段落名称、开通时间、关闭时间、备注一条线路不同段落分别何时开通train_run车次、线路ID、始发站、终到站、出发时间、到达时间、运行时长、经停站数、开行日期类型某条线路每天实际开行哪些车air_route航线ID、起飞机场、到达机场、航司代码、航班号、周班次、航距、机型类别某条航线每周有多少班飞机字段设计里有几个容易忽略的细节我一个个说。第一车次必须是字符串不能是数字。高铁车次有字母前缀像G、D、C后面跟数字如果存成整数字母就丢了。就算你只想统计数量也要保留车次的完整文本因为后续经常要靠车次前缀判断列车等级。第二线路ID和车次一样用自增整数就行但我建议用“HY-202”这种带业务含义的编码调试时一眼能看懂是高铁线路还是别的。千万不要直接用线路名称作为主键因为线路会出现改名。第三line_opening表记录的是“开通事件”不是“线路状态”。一条线可能分五段通车每段都有一条记录这样就能画出精确的扩展时间轴。如果只在line_info表里放一个“开通日期”遇到分段开通就只能写最早或最晚的日期信息就丢了。第四民航航线表里我建议把往返程合并成一条航线记录用“周班次”字段表示双向总班次的一半。比如某航线每天双向共14班那周班次是7天乘以7班而不是把去程和回程各算一次。如果不做合并统计总班次时会翻倍分析结果会失真。第五所有日期字段统一用YYYY-MM-DD格式所有时刻字段统一用24小时制HH:MM文本或分钟数。跨天到达的列车和航班到达日期要单独加一个“1”标识而不是只写一个没有日期的时刻。这一条看起来基础实际清洗时最容易翻车。这套结构搭好之后数据采集和清洗才有明确目标。否则数据抓到一半发现字段对不上再回头改表结构代价非常大。我在第一版就吃过这个亏后来每次项目都先把字段定义写进说明文档让采集代码按照同一套字段名输出。2. 数据采集与清洗一份靠谱交通数据是怎么磨出来的2.1 数据源怎么选交叉验证怎么做做交通类数据最忌讳只信单一来源。以高铁开通时间为例公开新闻、百科类资料、票务平台、运营方的开通公告经常出现不一致。这不一定是谁错了很可能是“线路概念”和“实际运营段落”的差别。一条线路名下的不同段开通时间差上好几年是常事。我的做法是给来源分个优先级同时保留多条证据链。数据来源优点缺点适用场景官方开通公告和新闻权威性最高日期精确分散早期数据难找高铁开通时间、线路状态百科类资料聚合程度高字段全更新滞后可能把分段日期写成首段日期线路基础信息、途经点票务平台查询接口时效性最好能看到当日车次和航班字段可能不全反爬限制多列车时刻、航班班期民航航班计划资料航线、班期、机型信息规整获取门槛高需要整理版权飞机航线信息交叉验证规则很简单关键字段至少要两个独立来源一致才采纳不一致时按官方公告优先暂时无法验证的字段就在备注里标“待核实”不要为了填满表格硬写一个值。举一个我在项目里遇到的例子。某条干线百科写的是2012年开通但票务平台里能查到2010年就有这条线路上的车次。后来查官方开通公告才发现该线路先开通了市区到枢纽站的一段2012年才全线贯通。两边都没错只是记录粒度不同。如果当初直接采信百科的“2012年”整条线路的影响分析就会晚算两年。2.2 三类最磨人的脏数据问题采集完原始数据后真正耗时间的不是采集是清洗。我整理2003-2025年高铁和民航数据时遇到最多的是下面三类问题。第一类日期格式混乱。一个数据集里可能会出现“2015年6月1日”“2015-06-01”“20150601”“2015/6/1”四种写法。如果不统一排序和筛选会出大问题。我写了一段清洗函数先把所有日期字符串转成时间戳再格式化成标准日期遇到解析不了的统一标记为缺失不自动猜测。第二类分段开通和线路改名。线路更名特别常见尤其是城际铁路并入骨干网络后名称往往会调整。我的处理方式是给每条线路一个稳定ID名称变成属性字段。另建一张别名表记录“某年某月之前叫旧名之后叫新名”。分析时全部用线路ID关联不用名称关联这样无论名字怎么变历史记录都能对上。第三类机场和车站的代码变动。飞机航线数据里经常出现同一个机场在不同年份使用不同代码或简称的情况。我的解决方案是维护一张站点映射表把实际名称、简称、代码都映射到统一的站点ID。高铁车站也一样新站开通、旧站停运都需要在这个映射表里标记生效时间段。2.3 质量校验用规则把错误数据拦在门外清洗完之后不能直接进入分析还要过一遍规则校验。这部分看起来简单但能挡住绝大多数低级错误。我总结的校验规则主要有这么几条开通时间必须落在2003年到2025年区间超出区间直接报错。线路里程不能小于起终点直线距离。如果出现“直线距离640公里、线路里程却只有380公里”这种数据基本可以判定源头写错了铁路里程不可能比直线还短。车次格式要匹配字母加数字的规律比如G开头、D开头、C开头。不符合的车次单独抽出来人工核对。民航航班号同样要匹配航司代码加数字的结构。同一条记录里到达时间必须晚于出发时间跨天记录必须带1标识。这条主要防手工录入错误。我习惯把这些规则写成脚本每天更新完数据后自动跑一遍。脚本输出的异常清单再人工做二次确认。这一步看起来费时间实际上能省掉分析阶段的大量返工。有一次我发现某条线路月均班次突然翻倍排查后才知道是采集脚本把某个城际线的临客也算进去了。如果没有校验规则这个错误会在最终图表里出现而且很难发现。3. 用数据看高铁和民航的“此消彼长”关系3.1 对齐时间轴影响分析的第一步数据整理完成后最自然的分析方向就是看高铁开通对同通道民航的影响。这里的关键不是算数而是先把两条时间轴对齐。我挑了一条典型的A市到B市通道来跑示例分析。这条通道的铁路距离大约760公里正好处于高铁和民航竞争最激烈的区间。高铁开通时间是2016年7月。我取的样本窗口是开通前12个月和开通后24个月分别统计该通道的航班班次。时段平均每周航班量开通前12个月138班开通后第1季度118班开通后第4季度102班开通后第8季度90班这个趋势看着很漂亮但实际操作中必须注意季节性。航班有暑运、春运高峰如果拿开通前某个夏天的数据对比开通后某个冬天的数据差异会被人为放大。所以我对比的是“同比”同是4月就都取4月同是10月就都取10月。这一点在做影响分析时比算法本身重要得多。3.2 列车与航线的密度对比怎么算高铁列车信息和民航航线信息不能只看“趟数”还要看统计口径。高铁车次里有些车是本线全程车有些只是借道经过。如果某条线路上每天运行图里有100趟车但真正服务A市到B市这一段且中途停靠的只有60趟统计密度时就不能用100。我在项目里是这样处理的先把列车运行数据按“线路ID经停站”筛选只保留两个目标城市之间的实际开行车次再区分“日常开行”和“高峰线/临时线”。日常开行是贯穿全周期的稳定运力临时开行则是节假日加班如果不区分月度对比会出现很大噪声。下面是一个示例通道在高铁开通后的运力变化表年份高铁日均车次民航日均航班2015036201628312017362720184126这张表背后有个细节高铁车次统计的是A市到B市方向的车次民航航班统计的也是A市到B市方向的航班数两边都是单方向口径。有些资料喜欢用“高铁对数”表示往返一对车和“航班班次”放一起比较时会差一倍必须统一口径。3.3 基于样本数据的Python分析流程写一段示例分析代码可以帮助你理解完整的跑数流程。假设我已经把清洗后的列车运行数据和航线数据分别存成了两个CSV文件。import pandas as pd train pd.read_csv(train_run.csv, parse_dates[run_date]) air pd.read_csv(air_route.csv, parse_dates[flight_date]) # 筛选某条线路 line_train train[train[line_id] HY-202].copy() # 按日期统计高铁车次 daily_train line_train.groupby(run_date).size() # 按周统计民航航班 air[week] air[flight_date].dt.to_period(W) weekly_air air.groupby(week).size() # 两边都按周重采样便于对比 train_weekly daily_train.resample(W).mean()代码本身不复杂但有几个容易错的地方。第一CSV里的日期字段必须指定parse_dates否则pandas会当成字符串。第二高铁车次日历和民航航班日历可能都有缺失日期要先用resample把时间轴补全再计算均值。第三“周”的起始日不同会导致边界偏差我习惯统一用周一作为一周起点。这段流程跑通后就可以得到类似上面的运力对比表。如果要进一步算高铁开通前后的降幅只需要把时间窗口切成两段做均值差。但注意这不等于因果关系因为民航可能同时因为其他因素调整班次。做严肃研究时需要加入更多控制变量我这里主要是演示数据整合与统计方法。4. 可视化与报告让2003-2025的交通演变看得见4.1 开通时间轴与扩展里程阶梯图怎么做数据整理和基础分析完成之后下一步是可视化。对于2003-2025这种跨度很长的数据最有冲击力的图是“累计开通里程阶梯图”。横轴是年份纵轴是累计高铁里程每开通一段线路阶梯就往上跳一段。我用的工具是matplotlib核心代码如下opening pd.read_csv(line_opening.csv, parse_dates[open_date]) opening opening.sort_values(open_date) cumulative_km opening[mileage].cumsum() plt.step(opening[open_date], cumulative_km, wherepost) plt.xlabel(年份) plt.ylabel(累计开通里程公里) plt.title(2003-2025 高铁累计开通里程变化)代码很简单但实际操作有几个坑。第一分段开通的线路mileage字段填的是本段里程不是线路总里程否则累计值会重复计算。第二阶梯图用wherepost代表开通当天之后里程生效比默认对齐方式更贴近业务逻辑。第三如果某条线路在同一集中开通多条段需要先按日期排序再累加。除了阶梯图我还画了“每年新增线路数量柱状图”和“列车开行数量时间序列图”。柱状图能直观看出哪几年是建设高峰时间序列图能看出运行图调整带来的一次次跳变。可视化不是越多越好每张图都要对应一个能从数据里回答的问题。4.2 高铁与民航对比图的几个易错点画对比图是最容易出错的环节。我在项目里踩过几个坑总结出来供你参考。第一时间范围要对齐。高铁运行图和民航航班计划都有季节调整如果你只用某一年的1月数据对比另一年的8月数据画出来的趋势线毫无意义。我一般先把数据统一成“周”或“月”粒度再按年份做同比。第二班次和运力不能混用。高铁G字头列车和D字头列车定员不同C字头城际列车又不一样民航宽体机、窄体机、支线机的座位数差异更大。如果只用“班次”比较会低估大容量运力的影响。只要条件允许尽量用“座位数”或“可提供座位公里”这类运力指标。第三往返程统计别搞混。高铁车次表里G1次和G2次可能是一对往返车民航去程和回程也是两个航班。你可以在表格里用“单方向班次”做比较也可以用“往返班次”做比较但一定要统一。我见过不少分析把高铁“对数”乘以2再把民航“班次”直接拿来比结果图上的差异全被口径污染了。第四变化率图慎用对数轴。当民航航班量很少时变化率会剧烈波动看起来像是断崖式下跌。如果样本基数太小我建议只展示绝对变化量不展示百分比。5. 高频问题排查与数据更新经验5.1 线路改名、站点合并怎么处理任何长期维护的数据集都会遇到实体改名问题。高铁线路和车站、民航机场都可能改名。我在实际维护中发现直接把旧数据里的名称改成新名称是最危险的因为历史记录必须保留历史叫法分析中才追踪得到变化过程。我维护了一张别名表结构是实体ID、旧名称、新名称、生效日期。所有业务表只存实体ID不存名称。做报表时再通过别名表关联到当时的名称。站点合并也一样一个老站停运并入另一个新站两条记录在站点表里分别标记状态和启用日期。这样不管什么时候查历史都能还原当时的场景。5.2 列车运行图大调整后如何同步列车运行图一年会调整多次每次调整都会有车次停运、新开、改点、加站、减站。刚开始我图省事每次调图后直接覆盖旧数据后来发现历史回溯就做不了了。比如别人问“2019年这条线路一天开行多少车”我拿到的总是最新版本根本答不上来。后来我改成增量更新法。每次调图后用新抓到的全量时刻表和上一版做diff把变化拆成三类新增车次、停运车次、既有车次变更。每类都带上生效日期写入新表旧表记录不做修改。这样任意一天的运行状态都能按日期区间查询出来。代码上可以给train_run表增加valid_from和valid_to两个字段我查“某一天有多少车”时只需要筛选valid_from小于等于该日期且valid_to大于等于该日期的记录。5.3 后续更新与自动化数据项目最怕“整理一次就完事”。高铁和民航数据是持续变化的所以我从第二版开始就搭了一套比较轻量的更新流程。第一原始数据快照单独存放。每次采集后的原始文件压缩存到raw目录文件名带日期戳后续绝不修改原始文件。第二清洗脚本和配置分离。清洗规则放在配置文件里脚本本身不写死参数。第三质量校验脚本每次更新后自动跑异常清单发到群里人工确认。第四发布出去的版本在数据目录里写一个VERSION文件记录数据版本号、更新日期、变更说明。我自己用下来最受益的习惯是“永远保留原始快照”。清洗后的数据即使被改坏了也可以回滚到原始数据重新处理。如果当初图省事只保存清洗结果后面的数据修复成本会非常高。最后再分享一点个人觉得最值得记住的经验这类交通数据项目真正的门槛不是采集和计算而是对命名、版本、口径的管理。你面对的不只是一张表而是过去二十多年里持续变化的真实运营网络。先想清楚字段和主键再动手采集先在小范围跑通更新流程再铺到全量年份先建好别名表和版本目录再做任何分析。把基础打牢2003-2025年高铁和民航的演变就能真正被数据讲清楚。