☰
中国高铁航线数据库CRAD(2003-2022)的构建实践与数据清洗经验
2026/9/30 3:45:03 网站建设 项目流程

做城市网络、区域经济或者出行行为研究的人,应该都有过这种体会:最耗时间的往往不是最后跑回归、画图表的环节,而是前期找数据、洗数据。尤其是高铁相关的分析,线路开通时间、站点经纬度、速度等级、里程这些信息散落在各种公告和百科词条里,想拼出一张干净的面孔,难度不亚于自己做一次小型普查。我这几年陆续整理过几版高铁数据,最近终于把中国高铁航线数据库CRAD(2003-2022年)完整更新到了2022年底,心里这块石头算是落了地。这篇文章就把这套数据库从设计思路到实操过程做个复盘,把我踩过的坑、绕过的弯都写出来,给同样需要这类基础设施数据的朋友参考。

CRAD这个名字,意思是China Railway Airline Database,把它叫“航线数据库”,是因为在分析模型里,高铁线路和航空航线在OD(起讫点)结构上非常相似——都是连接两个或多个城市节点的“边”。以年为粒度记录每条线路的开通时间、途经站点、速度等级和里程,就能很自然地支撑网络演化、区域连通性、城市可达性等一系列研究工作。这套数据覆盖从2003年秦沈客运专线开通到2022年底,包含线路、站点、连接关系三层结构,既可以按线路聚合统计,也可以拆到站点粒度做网络分析,算是我目前见过的时间跨度最完整、结构最规整的一套高铁数据集。

需要说明的是,这篇文章里提到的字段定义、清洗流程和建表逻辑,是基于我个人做这套数据库时的实践经验总结出来的,具体实现上不同研究者可能各有习惯,但核心思路——把高铁网络抽象成“线-站-连接”的三层模型——应该是有通用价值的。

1. CRAD里装的是什么:字段定义与数据边界

先说清楚这套数据库到底长什么样。我设计CRAD的时候没有套用现成的交通数据库模板,而是直接从研究需求倒推:分析高铁网络最常用到哪些维度?答案无非是空间位置、时间属性、线路属性和运行特征。所以整个数据库围绕两张核心表加一张关联表展开,建表结构在设计上刻意保持简单,数据都按第三范式切分,为的是后面做统计分析时不用反复纠结数据冗余的问题。

1.1 线路表的字段设计

线路表可以命名为rail_line,每条高铁线路作为一条独立记录,主键用整型自增ID。核心字段包括线路名称、线路代码、起点站、终点站、设计时速、运营时速、里程、开通日期和运营状态。这里有个容易踩坑的细节——很多线路并不是一次性全线贯通的,比如某条干线可能先开通东段,过两年再开通西段。如果把整条线路当作一条记录,时间维度上就失真了。我的处理方式是在线路表中保留一条整合记录,同时用一个segment字段标明该记录是否为分段开通的母线,详细的分段信息则全部放进连接表。

设计时速和运营时速我做了区分。设计时速是建设时定下的最高标准,运营时速是实际跑图排班时的上限,两者之间往往有差异,尤其是早期线路,设计时速和运营时速的差距能拉开50公里以上。里程字段我统一采用正线里程,不含联络线和动车走行线,这个口径在数据说明注释里要写清楚,否则和别人手里另一份数据做对比时会对不上数。

1.2 站点表与空间粒度的取舍

站点表rail_station记录所有出现在线路上的车站,字段包括站点名称、所属城市、行政区划代码、经度和纬度。经纬度是后期统一补的,用地理编码服务批量处理后再逐个人工抽查,因为国内站名存在大量同名情况,比如“南站”“东站”这种派生站名,单纯靠文本匹配很容易张冠李戴。

关于空间粒度,我最终选择的粒度是“站点”,而不是“城市”。原因很简单:同一座城市往往有多个高铁站,比如北京就有北京南、北京西、北京丰台等多个站点,在城市粒度上它们会被合并成同一个节点,但真实网络中这些站点之间的换乘关系、不同线路共站的情况就被抹掉了。做网络分析时,如果把北京南和北京西合并成一个节点,就会丢失经停线路分属不同站点的拓扑细节。所以我宁可让站点粒度复杂一点,也不愿意为了省事牺牲结构精度。

1.3 连接表如何表达动态网络

连接表rail_link是整套库的精髓,它表达的是“某条线路在某个时间段内经停了哪些站点”。每个连接记录包含线路ID、站点序号、站点ID、连接类型(起点/中间站/终点)、启用日期和撤销日期。为什么要有撤销日期?因为高铁运行图调整后,部分车站可能不再办理客运业务,或者某条线路延伸后原终点站变成了中间站。这类变化如果不记录时间范围,历史截面就没法重建。

举个例子,某条线路在2015年开通时终点站是A站,2019年延伸到B站之后,A站就从终点变成了中间站。我的做法是给A站这条连接记录设置撤销日期为2019年某日,同时新增一条A站作为中间站的连接记录,启用日期为同一天。每次做时间截面分析时,只需要按启用日期 <= 目标年份 <= 撤销日期过滤,就能准确重建当年的网络拓扑。这套设计一开始看起来有点绕,但实际用下来非常顺手,尤其是做面板数据回归时,不同年份的网络特征值可以直接从这里算出来。

1.4 数据覆盖范围与版本演进

2003年的起点不是随意定的——秦沈客运专线在2003年开通,虽然它后来被纳入京哈铁路的一部分,但作为中国第一条设计时速200公里以上的客运专线,它是高铁时代公认的开端。从2003年到2022年,CRAD覆盖的高铁线路从早期屈指可数的几条,扩张到覆盖全国主要城市群的网络,数据规模也从最初几百条记录增加到数千条站点记录。

这次更新到2022年底,相比上一版主要做了三方面补强:一是把2022年底前新开通的线路补录完整;二是对部分既有线路的经停站点变化做了修订,修正了几处站点撤销时间错误;三是统一了所有字段的编码口径,比如日期格式全部改为YYYY-MM-DD,城市名称全部采用当前行政区划名称。这些看似琐碎的整理工作,恰恰是数据库能够长期用下去的关键。

2. 建库全过程:从公开信息清洗出二十年高铁记录

这一节说说CRAD的数据是怎么一步步从零散公开信息变成结构化记录的。整个过程可以拆成信息采集、清洗规范、去重合并、坐标补全和抽样校验五个阶段,每个阶段都有值得记录的细节。

2.1 采集渠道的优先级与交叉验证

数据的原始来源主要分几类:铁路部门公开发布的新线开通公告、地方政府的交通规划报告、权威百科的词条信息,以及地理编码服务提供的坐标数据。采集时我有两个原则:第一,开通时间以首发运营公告为准,这类信息最权威;第二,规划类和百科类信息只作交叉验证,不做唯一依据。

为什么必须交叉验证?因为不同渠道的数据经常互相矛盾。同一条线路,A渠道说2019年12月开通,B渠道说2020年1月才正式运营,有些是因为试运营和正式运营的时间差,有些纯粹是信息更新不及时导致的错误。我的标准是:以实际开行首趟正式旅客列车的日期为准,试运行、联调联试阶段一律不计入开通时间。这个口径能在最大程度上保证时间序列的一致性。

2.2 站点名称标准化:一个看似简单实则麻烦的环节

站名清洗是整套流程里最琐碎的部分。高铁站名的写法非常不统一:有的用“北京南”,有的用“北京南站”,全称简称混杂;有些车站历史上改过名,比如某站早年叫“XX北”,改造后改名“XX西”;还有不少站名包含方位词,容易和同一城市其他车站混淆。我的标准化策略是统一去除“站”字后缀,保留官方运营名称作为标准名,同时建立一个别名映射表,把常见的不规范写法全部映射到标准名上。

这一步工作量很大但价值极高。建库初期没有做别名表,导致后来统计途经某城市的线路时反复出现一条线路被算成两条的情况——因为“北京南”和“北京南站”被当成两个站了。后来专门花了两天时间把所有别名梳理清楚,才彻底解决了这类问题。

2.3 去重与合并的决策逻辑

数据整理过程中最常遇到的情况是同一条线路在不同来源里名称不同,或者同一时期有多个开工/开通信息。我的去重逻辑分两种:一是同名线路合并,确认是同一条线路后,保留信息最完整的一条,其余作为历史记录存档;二是同线路不同批次信息合并,比如一条线路的分段开通公告分多次发布,就需要把这些信息整合到同一条母线记录下,再在连接表里拆分各段的具体开通时间。

还有一种容易忽略的情况:枢纽联络线算不算独立线路?比如连接两条干线之间的短程联络线,长度只有几十公里,不直接连接大城市,但它确实构成了网络拓扑中的关键一环。我的处理原则是:只要作为独立线路运营且有正式名称,就纳入数据库,不以长度长短为门槛。因为网络分析中,哪怕是短联络线,也会影响节点之间的最短路径计算。

2.4 经纬度补全的实际操作

站点经纬度的补全,我用了三步走。第一步,批量调用公开地理编码接口,用站名加城市名作为查询关键词,返回候选坐标;第二步,对返回结果做合理性检查——比如站点坐标是否在城市行政区划范围内、是否偏离主要道路太远——这一条能过滤掉相当比例的错误结果;第三步,对异常记录人工复核,我在操作中发现,市中心的老站和郊区的新站混在一起的情况特别常见,仅仅靠名称很容易搞混。

坐标精度上,我保留六位小数,对应误差在米级,对网络拓扑分析来说足够了。没有刻意追求更高精度,因为高铁站点之间的距离动辄几十上百公里,米级误差对宏观分析的影响可以忽略。

2.5 抽样校验:如何确认数据没有大问题

数据全部录入后,我做了一轮抽样校验。抽样规则是:按年份分层,每年随机抽查不少于3条线路,核对开通日期、途经站点顺序和里程这三个关键维度。校验方式是人工逐个对照原始公告和地图服务确认。

这轮校验发现的问题比预想的多,主要集中在两个方面:一是早期线路的途经站点顺序有误,二是部分线路的里程和官方公布值有出入。站点顺序问题来自采集时信息源的排序不统一,后来在连接表里增加了站点序号字段,按实际行车方向强制排序,这个问题才被根除。里程出入则多数是计价口径不同(营业里程与正线里程的差异),统一改为正线里程后,误差明显收窄。

3. 表结构与查询设计:把线路数据变成可分析的资产

建库的终点不是把数据存起来,而是能被高效地取出来用。这一节我把CRAD的建表SQL和几个高频查询示例写出来,同时讲一讲查询设计上的一些取舍和背后的原因。

3.1 核心建表语句与字段类型选择

下面是三张核心表的简化版建表语句,我把注释直接写在SQL里,方便理解每个字段的用途。

-- 线路表 CREATE TABLE rail_line ( line_id INT PRIMARY KEY AUTO_INCREMENT, line_name VARCHAR(100) NOT NULL, -- 线路标准名称 line_code VARCHAR(50), -- 线路编号/代码 origin_station VARCHAR(100), -- 起点站 terminal_station VARCHAR(100), -- 终点站 design_speed INT, -- 设计时速(km/h) operation_speed INT, -- 运营时速(km/h) mileage DECIMAL(10,2), -- 正线里程(km) open_date DATE NOT NULL, -- 开通日期 status TINYINT DEFAULT 1, -- 运营状态: 1运营中 0停运 remark VARCHAR(255) -- 备注 ); -- 站点表 CREATE TABLE rail_station ( station_id INT PRIMARY KEY AUTO_INCREMENT, station_name VARCHAR(100) NOT NULL, -- 标准站名 city_name VARCHAR(100) NOT NULL, -- 所属城市 ad_code VARCHAR(12), -- 行政区划代码 longitude DECIMAL(10,6), -- 经度 latitude DECIMAL(10,6) -- 纬度 ); -- 连接表:表达线路-站点-时间的动态关系 CREATE TABLE rail_link ( link_id INT PRIMARY KEY AUTO_INCREMENT, line_id INT NOT NULL, -- 线路ID station_id INT NOT NULL, -- 站点ID station_order INT NOT NULL, -- 站点在线路上的顺序 link_type TINYINT NOT NULL, -- 1起点 2中间站 3终点 start_date DATE NOT NULL, -- 该连接生效日期 end_date DATE DEFAULT '9999-12-31', -- 该连接失效日期 FOREIGN KEY (line_id) REFERENCES rail_line(line_id), FOREIGN KEY (station_id) REFERENCES rail_station(station_id) );

字段类型的选择上有几个细节值得说。里程字段用DECIMAL而不是FLOAT,是因为浮点数累计计算会引入误差,对几十公里的里程来说可能不明显,但汇总到全国总里程时误差就会积累到令人尴尬的程度。日期全部用DATE类型,不存字符串日期,这样在SQL里做范围过滤和格式化都方便。经纬度用DECIMAL(10,6),精度足够且比FLOAT更可控。

3.2 高频查询示例:按需取数不再折腾

下面这几个查询是我使用频率最高的,基本覆盖了日常分析的大部分需求。

查询某一年新开通了哪些线路:

SELECT line_name, origin_station, terminal_station, design_speed, mileage, open_date FROM rail_line WHERE YEAR(open_date) = 2020 ORDER BY open_date;

查询经过某座城市的全部高铁线路:

SELECT DISTINCT l.line_name, l.open_date FROM rail_link lk JOIN rail_line l ON lk.line_id = l.line_id JOIN rail_station s ON lk.station_id = s.station_id WHERE s.city_name = '武汉' AND lk.start_date <= '2022-12-31' AND lk.end_date >= '2022-12-31' ORDER BY l.open_date;

查询某条线路在2022年底的完整站点序列:

SELECT s.station_name, lk.station_order, lk.link_type FROM rail_link lk JOIN rail_station s ON lk.station_id = s.station_id WHERE lk.line_id = 12 AND lk.start_date <= '2022-12-31' AND lk.end_date >= '2022-12-31' ORDER BY lk.station_order;

这类查询在数据量不大的时候性能没有问题,但一旦把数据加载到分析工具里做网络特征值计算,就会发现逐条查太慢了。我的经验是:对于网络分析场景,优先把三张表JOIN成一张宽表,一次性导出所有连接关系,再在内存里构建网络对象处理。SQL擅长的是过滤和聚合,真正的网络算法还是要靠Python或者R。

3.3 索引、并发与连接池:多人使用时的性能保障

CRAD的体量虽然算不上大数据,但多人并发查询时如果不做优化,一样会卡到怀疑人生。我在实践中做了三件事:第一,对rail_link表的三个关键过滤字段——line_id、station_id、start_date/end_date——建立复合索引,查询性能提升非常明显;第二,把常用查询封装成视图,相当于给使用方提供了一套预定义接口,避免每个人各写一套五花八门的SQL;第三,部署时给数据库连接配置了连接池,比如用HikariCP或者Druid,把并发连接数控制在合理范围。

说到连接池,这里有个亲身踩过的坑。有一次把数据放到服务器上供团队使用,因为连接池最大连接数配置过高,导致大量空闲连接占满数据库进程,新请求反而进不来。后来把最大连接数调小、空闲超时收紧,问题立刻消失。多用户的场景下,连接池参数永远需要根据实际并发量动态调整,不是越大越好。

3.4 导入大批量数据时的事务与锁机制

如果你需要把外部整理好的CSV批量导入CRAD,我用的是分批提交的策略。一次性INSERT几千条记录看起来很爽,但遇到线上并发访问时会长时间锁表,而且万一中途报错,回滚成本极高。稳妥的做法是每500条提交一次事务,配合INSERT ... ON DUPLICATE KEY UPDATE处理重复记录,既保证了效率,又能将锁粒度降到最低。

这里想额外提一句数据库锁的优先级问题。很多人一遇到死锁就急着改SQL、加索引,但很少先检查事务隔离级别。国产数据库和MySQL的默认隔离级别并不完全相同,如果用的是达梦、人大金仓这类数据库,导入前最好先确认当前会话的隔离级别和锁等待超时时间,否则并发导入时特别容易撞上锁等待超时。这个坑我在工作中踩过一次,调整隔离级别为读已提交并设置合理的锁等待超时后,批量导入就顺畅了。

4. 用CRAD看二十年网络演进:几个朴素但有效的统计视角

数据库建好不能只当仓库吃灰,我拿它跑了一些基础统计,看看2003到2022年这二十年,中国高铁网络到底经历了怎样的变化。这里不搞花哨的模型,就用几个最朴素的指标,演示一下CRAD能支撑什么样的分析。

4.1 年份维度:新开线路的分阶段节奏

按年份统计新开通线路数量,可以明显看到几个阶段。2003到2008年是起步期,新开通线路数量有限,运营时速集中在250公里上下;2009到2015年是爆发期,新线密集投入运营,最高设计时速开始普遍追求350公里;2016年之后进入加密期,新建线路不再只是骨架干线的延伸,更多是路网加密线、城际连接线和枢纽联络线。

这种分阶段特征对研究很有价值。如果直接把二十年的数据混在一起回归,早期线和中后期线的特征差异会被淹没。至少应该按时间分段或者加入开通年份虚拟变量,才能捕捉到政策环境变化带来的结构性影响。

4.2 空间维度:从干线骨架到网络化覆盖

把CRAD里的线路逐年画在地图上,能看到非常清晰的空间演化逻辑。早期线路主要解决东部沿海和中部核心城市之间的长途快速连接,线路走向基本是“一字型”或“十字型”;到了中后期,新增线路开始大量出现在城市群内部,形态变成“网格型”和“放射型”,节点城市之间的冗余连接明显增多。

这种从“线”到“网”的演变,单看某一条线路的开通记录是觉察不到的,必须把整个连接表导入网络分析工具,逐年计算网络密度、平均最短路径、连通分量这些指标,才能量化描述。CRAD的站点粒度连接表恰好就能支撑这类计算。

4.3 节点维度:谁是网络中的关键枢纽

基于CRAD的连接关系,可以统计每个站点在特定年份“接触”过多少条线路——其实就是节点的度。我拿2022年底的数据做了个简单统计,度排名靠前的站点基本符合直觉,集中在几个大型枢纽城市。但如果只看度,会忽略一个结构性问题:有些节点度并不高,但它是某条唯一的西部干线上的必经之地,一旦这个节点失效,大片区域的可达性都会崩溃。

这类“关键性”节点的识别需要做节点删除模拟,或者计算介数中心性。CRAD提供了完整的连接关系和站点坐标,这类计算在NetworkX里几百行代码就能完成,这也是我坚持做站点粒度数据而不是城市粒度数据的原因——城市粒度会把跨站换乘的结构特征抹平,介数计算的意义就大打折扣了。

4.4 速度维度:设计时速与运营时速的差距变化

我还做了一个有意思的统计:对比设计时速和运营时速的差距如何随年份变化。早期线路的设计值普遍高于实际运营值,说明早期运营策略偏保守,实际跑图没有完全吃满设计余量;2015年之后的线路,两者差距明显缩小,工程水平和运营组织能力的同步提升在这个指标上体现得很直观。

这类指标用来做技术演进的量化研究还挺有意思的,CRAD里同时保存了两个速度字段,直接导出即可计算。

4.5 一个完整的时间截面重建示例

最后展示一下怎么用CRAD重建某个历史年份的网络。比如我想看2015年底的网络结构,只需要对rail_link表执行过滤:

SELECT lk.line_id, lk.station_id, s.station_name, s.longitude, s.latitude FROM rail_link lk JOIN rail_line l ON lk.line_id = l.line_id JOIN rail_station s ON lk.station_id = s.station_id WHERE lk.start_date <= '2015-12-31' AND lk.end_date >= '2015-12-31'

拿到这个结果后,按线路ID分组整理成边列表,导入NetworkX构建图对象,当年的网络拓扑就重建出来了。配合每年的数据反复执行,二十年逐年的网络指标序列就能一键生成。这种“时间旅行”能力是CRAD设计时最核心的诉求,也是它区别于静态统计表的最大价值。

5. 实际使用中的坑与对策:部署、编码与数据校验

最后这部分是我在实际使用CRAD过程中遇到的问题汇总,有些是部署层面的,有些是数据管理层面的,一个一个拆开说清楚。

5.1 数据库选型与字符集:中文站名不乱码的底线

CRAD的主体内容是中文站名和城市名,字符集配置不对,轻则查询时出现乱码,重则连建表都报错。我的建议是直接用UTF-8MB4字符集,虽然比UTF-8多占一点空间,但能完整支持中文和生僻字,避免后续遇到特殊字符时抓瞎。MySQL里建库时明确指定:

CREATE DATABASE crad CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

排序规则选择utf8mb4_unicode_ci而不是默认的utf8mb4_general_ci,主要是因为中文环境下按拼音排序的需求偶尔会出现,前者对Unicode排序的支持更规范。这个细节如果不提前设置,后期改库非常麻烦。

5.2 导入数据时的编码和格式坑

CSV导入是最常见的入库方式,但CSV的编码格式五花八门。最常见的问题是:Excel另存的CSV默认使用GBK或GB18030编码,直接用LOAD DATA导入会造成中文乱码或导入失败。正确做法是先把CSV转成UTF-8无BOM格式再导入,或者导入时显式指定字符集。

另外还有一个小坑值得注意:有些线路的站点名称中含特殊字符,比如“·”或者括号全半角不统一,这会导致站名匹配时出现意想不到的错误。我后来把所有半角括号统一转成中文括号,并且在别名表里做了全半角映射,这类问题才基本绝迹。

5.3 多部门常用时如何维护数据一致性

如果你的CRAD会被团队成员同时使用,数据维护的一致性问题很快会浮现。我在实践中逐渐形成了几个约定:版本更新记录必须在专门的变更日志表里登记,每次新增或修改数据都要写明原因、日期和操作人;核心表的线上修改统一走审批链路,不允许成员直接改正式库;定期备份并保留历史快照,万一改出问题还能回滚。

这套规则看上去麻烦,但对维持数据的可信度是必要的。毕竟用这套数据写论文、出报告,数据的规范性直接影响结论的可靠性。

5.4 高频查询的缓存策略

当CRAD被用于在线分析报告时,反复跑同一个查询会浪费大量资源。我建议把常用统计结果做成聚合表,比如按年度预计算各城市通达线路数、各线路累计里程,查询时直接读聚合表,而不是实时扫全量数据。这种“空间换时间”的做法在数据量增大之后收益非常明显。

我用过一次比较极端的情况:某次给领导汇报需要即时展示历史二十年的网络密度变化图,后台算了两分钟才出结果。后来把年度的网络指标提前算好存入MySQL,前端直接查表出图,响应时间从两分钟降到两秒内。遇到这种实际生产需求,聚合表的价值才会真正体现出来。

5.5 校验清单:每次更新后必须检查的几条

每当CRAD更新一版,我都会跑一遍下面这组逻辑校验,确保数据质量没有因为更新而回退:

  • 全库线路数量与公开渠道汇总数的一致性
  • 每条线路的起点终点必须存在对应的站点记录
  • 连接表中每条线路的起点/终点各且仅有一个
  • 站点经度范围在73°E到135°E之间,纬度范围在18°N到54°N之间
  • 所有开通日期不得晚于2022-12-31且不得早于2003-01-01
  • 抽查当年新开线路的站点顺序是否与运行图一致

这组校验规则并不复杂,但能把绝大多数低级错误挡在门外。数据更新的效率和质量,很大程度上取决于这套校验清单执行得是否彻底。我的体会是:宁可多花半小时做校验,也不要带着有问题的数据往下游跑,返工成本永远比检查成本高得多。

从2003到2022,中国高铁网络从零星几条线长成一张覆盖全国的大网,CRAD把这二十年的演化过程完整地记录了下来。对我个人而言,这套数据库最大的意义不只是提供了一份干净、可复现的数据源,更让我重新理解了“结构化数据”这四个字的分量——站点名称的统一、时间的精确切分、拓扑关系的完整表达,每一项看上去都是小事情,但它们共同决定了数据库能不能真正支撑起严肃的研究分析。数据整理的尽头没有惊天动地的技巧,有的只是把每一件枯燥的小事做到位,在需要它的时候才知道这一切都值得。

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

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

立即咨询