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+经停站”筛选,只保留两个目标城市之间的实际开行车次;再区分“日常开行”和“高峰线/临时线”。日常开行是贯穿全周期的稳定运力,临时开行则是节假日加班,如果不区分,月度对比会出现很大噪声。
下面是一个示例通道在高铁开通后的运力变化表:
| 年份 | 高铁日均车次 | 民航日均航班 |
|---|---|---|
| 2015 | 0 | 36 |
| 2016 | 28 | 31 |
| 2017 | 36 | 27 |
| 2018 | 41 | 26 |
这张表背后有个细节:高铁车次统计的是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, where="post") plt.xlabel("年份") plt.ylabel("累计开通里程(公里)") plt.title("2003-2025 高铁累计开通里程变化")代码很简单,但实际操作有几个坑。第一,分段开通的线路,mileage字段填的是本段里程,不是线路总里程,否则累计值会重复计算。第二,阶梯图用where="post",代表开通当天之后里程生效,比默认对齐方式更贴近业务逻辑。第三,如果某条线路在同一集中开通多条段,需要先按日期排序,再累加。
除了阶梯图,我还画了“每年新增线路数量柱状图”和“列车开行数量时间序列图”。柱状图能直观看出哪几年是建设高峰,时间序列图能看出运行图调整带来的一次次跳变。可视化不是越多越好,每张图都要对应一个能从数据里回答的问题。
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年高铁和民航的演变就能真正被数据讲清楚。