ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

全国列车数据获取与可视化分析:从抓取到清洗的完整链路

全国列车数据获取与可视化分析:从抓取到清洗的完整链路 简介这份资源是面向计算机相关专业学生与项目实战学习者的Python数据分析与可视化期末大作业完整源码围绕全国列车数据的获取与可视化分析展开难度适中适合正在做大作业或需要练手数据分析流程的同学参考。压缩包共32个文件约4.57MB以json数据文件、py爬虫与处理脚本、html与js前端页面、ipynb分析笔记及csv数据集为主另含少量map与md说明文件覆盖数据采集、清洗处理到可视化展示的完整链路。项目源码均经本地编译运行与严格调试并获导师指导认可评审分98分内容经助教审定可直接下载使用。目前已有329人学习。读者可从中获得一套结构清晰的列车数据爬取与可视化实现方案包括站点地理信息、车次信息等模块的代码组织方式与排错思路便于快速理解并迁移到自己的课程设计或实战练习中。1. 全国列车数据获取与可视化一份期末大作业的完整落地路径期末大作业选“全国列车数据获取与可视化分析”很多人第一反应是去网上找一份现成源码交差。但真正动手做过的人知道这类作业的评分点从来不在“有没有跑出图”而在数据从哪来、清洗逻辑是否站得住、可视化能不能讲出一个完整结论。我见过太多同学拿着爬来的车次表用pd.read_csv读进去df.plot()出两张折线图就交卷结果答辩时被问“你的数据覆盖多少车站、时间跨度多长、缺失值怎么处理”直接卡壳。这份笔记就按一线做数据分析项目的思路把“全国列车数据获取与可视化分析”从数据源选型、抓取、清洗、存储到可视化呈现拆成一条能复现、能答辩、能写进报告的完整链路。适合正在做 Python 数据分析与可视化课程设计、需要一份可运行源码思路的本科生也适合想拿真实交通数据练手 pandas 和 matplotlib 的入门者。2. 列车数据从哪来三种数据源选型与抓取方案对比做全国列车数据第一步不是写代码而是决定数据从哪来。这一步选错后面清洗和可视化全是白费功夫。常见的三类数据源各有边界我一般会先列一张对比表再决定用哪种。2.1 三类数据源的覆盖范围与获取成本第一类是公开的铁路时刻表数据接口通常返回 JSON 格式字段包含车次、始发站、终点站、发车时间、到达时间、历时、里程等。优点是结构规整、字段稳定缺点是往往只覆盖特定日期或特定线路全国全量需要分页或按车站遍历。第二类是静态的列车时刻表数据集网上有整理好的 CSV 或 Excel优点是拿来即用缺点是更新滞后、字段命名混乱、不同来源合并时对不齐。第三类是自己写爬虫从公开页面抓取优点是能拿到最新数据缺点是页面结构一变就翻车而且需要处理反爬和请求频率。数据源类型字段完整度更新频率获取难度适合场景公开接口 JSON高较高中需要结构化字段、做统计分析静态数据集中低低快速验证可视化流程自写爬虫高高高需要最新数据、能接受维护成本选型建议很直接如果作业只要求“获取与可视化”优先用公开接口或静态数据集把精力放在清洗和分析上如果老师明确要求“爬虫”再上第三类但一定要控制请求频率别把人家服务器打挂。2.2 用 requests 抓取列车时刻表的最小可用脚本下面这段代码演示从公开接口按车站拉取列车数据的最小流程。注意实际接口地址和参数需要根据你选的数据源替换这里只保留结构。import requests import pandas as pd import time # 目标按车站编码拉取经过该站的车次列表 BASE_URL https://example-rail-api.com/trains # 替换为实际可用接口 HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 } def fetch_trains_by_station(station_code, page1, page_size50): 按车站编码分页拉取车次数据返回 JSON 列表 params { station: station_code, page: page, size: page_size } resp requests.get(BASE_URL, headersHEADERS, paramsparams, timeout10) resp.raise_for_status() # 非 200 直接抛异常避免把错误页当数据 data resp.json() return data.get(trains, []) def collect_all_pages(station_code, max_pages20): 循环翻页直到没有数据或达到上限 all_rows [] for p in range(1, max_pages 1): rows fetch_trains_by_station(station_code, pagep) if not rows: break all_rows.extend(rows) time.sleep(0.5) # 控制频率避免触发限流 return all_rows if __name__ __main__: raw collect_all_pages(BJP) # 示例车站编码 df pd.DataFrame(raw) print(df.head()) print(总记录数, len(df))逻辑说明fetch_trains_by_station负责单页请求raise_for_status是关键很多人省掉这行结果接口返回 403 页面也被json()解析报一堆看不懂的错。collect_all_pages做翻页控制time.sleep(0.5)是血泪经验不加的话连续请求几十页很容易被临时封 IP。参数方面page_size不要设太大50 到 100 比较稳max_pages是保险丝防止接口异常时无限循环。2.3 抓取阶段必须记录的两个元信息抓取时除了车次字段一定要额外记两个东西抓取时间戳和数据来源标识。抓取时间戳用于后续判断数据新鲜度数据来源标识用于合并多来源时区分优先级。我一般会在 DataFrame 里加两列from datetime import datetime df[crawl_time] datetime.now().strftime(%Y-%m-%d %H:%M:%S) df[source] rail_api_v1别小看这两列后面做数据去重和版本对比时它们能帮你省掉大量排查时间。很多同学合并两份数据后发现车次重复就是因为没有来源标识根本分不清哪条该保留。3. 数据清洗把原始车次表变成能分析的 DataFrame抓下来的数据直接扔进可视化图能出来但结论不可信。清洗这一步决定你的分析有没有说服力。全国列车数据的脏法很有规律时间字段格式不统一、车站名有别名、里程缺失、车次类型混杂。下面按我实际处理的顺序讲。3.1 时间字段的三种脏格式与统一解析列车数据里的时间字段通常有三种写法08:30、2024-01-01 08:30:00、8:30。直接pd.to_datetime会有一部分解析失败。稳妥做法是先统一成标准格式再转换。import pandas as pd import re def normalize_time(t): 把各种时间写法统一成 HH:MM 格式 if pd.isna(t): return None t str(t).strip() # 提取形如 8:30 或 08:30 的部分 m re.search(r(\d{1,2}):(\d{2}), t) if not m: return None h, mi int(m.group(1)), int(m.group(2)) return f{h:02d}:{mi:02d} df[depart_norm] df[depart_time].apply(normalize_time) df[arrive_norm] df[arrive_time].apply(normalize_time)逻辑说明用正则提取小时和分钟再格式化成两位小时。这样8:30和08:30都会变成08:30。参数上正则\d{1,2}兼容一位和两位小时\d{2}要求分钟必须是两位避免把8:3这种残缺数据误判为有效。解析失败返回None后续统一按缺失值处理。3.2 车站名别名归一与里程缺失填充车站名别名是合并数据时最大的坑。比如“北京西”和“北京西站”、“上海虹桥”和“上海虹桥站”不归一就会导致同一个车站在统计里出现两次。我一般建一个映射字典把常见后缀去掉。STATION_ALIAS { 北京西站: 北京西, 上海虹桥站: 上海虹桥, 广州南站: 广州南, # 按你实际数据补充 } def normalize_station(name): if pd.isna(name): return None name str(name).strip() name STATION_ALIAS.get(name, name) # 去掉末尾的“站”字 if name.endswith(站): name name[:-1] return name df[from_station] df[from_station].apply(normalize_station) df[to_station] df[to_station].apply(normalize_station)里程缺失的处理要看分析目标。如果只是做车次数量统计里程缺失可以忽略如果要算平均旅行速度里程就是必需字段。常见做法是用同线路其他车次的里程中位数填充或者直接剔除缺失行并在报告里说明剔除比例。# 按出发-到达站分组用组内中位数填充里程 df[distance] df.groupby([from_station, to_station])[distance] \ .transform(lambda x: x.fillna(x.median())) # 仍然缺失的标记后剔除 df df.dropna(subset[distance])3.3 车次类型拆分与派生字段构造车次号前缀隐含车型信息G 代表高铁D 代表动车K 代表普快T 代表特快Z 代表直达。把前缀拆出来作为分类字段后面可视化按车型分组会方便很多。def train_type(train_no): if pd.isna(train_no): return 未知 prefix str(train_no).strip()[0].upper() mapping {G: 高铁, D: 动车, K: 普快, T: 特快, Z: 直达} return mapping.get(prefix, 其他) df[train_type] df[train_no].apply(train_type)派生字段还包括旅行时长。用到达时间减发车时间注意跨天的情况如果到达时间小于发车时间说明跨天要加 24 小时。def travel_minutes(depart, arrive): if not depart or not arrive: return None dh, dm map(int, depart.split(:)) ah, am map(int, arrive.split(:)) dep_min dh * 60 dm arr_min ah * 60 am if arr_min dep_min: arr_min 24 * 60 # 跨天 return arr_min - dep_min df[travel_min] df.apply( lambda r: travel_minutes(r[depart_norm], r[arrive_norm]), axis1 )这一步做完你手里才是一张能拿去做分析的干净表。清洗逻辑建议单独写成一个clean.py和抓取脚本分开方便复现和调试。4. 可视化分析用 matplotlib 和 seaborn 讲出三个结论可视化不是把字段挨个画一遍而是围绕结论选图。全国列车数据我一般会讲三个结论车型分布、热门线路、发车时段规律。每个结论配一张主图图后写一句能写进报告的解读。4.1 车型分布用条形图看高铁占比import matplotlib.pyplot as plt import seaborn as sns plt.rcParams[font.sans-serif] [SimHei] # 中文显示 plt.rcParams[axes.unicode_minus] False type_counts df[train_type].value_counts() fig, ax plt.subplots(figsize(8, 5)) sns.barplot(xtype_counts.index, ytype_counts.values, axax, paletteBlues_d) ax.set_title(全国列车车型分布) ax.set_xlabel(车型) ax.set_ylabel(车次数量) for i, v in enumerate(type_counts.values): ax.text(i, v 5, str(v), hacenter) plt.tight_layout() plt.savefig(train_type_dist.png, dpi150) plt.show()逻辑说明value_counts统计各车型数量barplot出图ax.text在柱顶标数值。参数上font.sans-serif设成SimHei是为了中文不乱码Linux 环境可能需要换成WenQuanYi Micro Hei。dpi150保证保存的图在报告里清晰。4.2 热门线路用横向条形图看 Top 10 区间route_counts df.groupby([from_station, to_station]).size() \ .reset_index(namecount) \ .sort_values(count, ascendingFalse) \ .head(10) route_counts[route] route_counts[from_station] → route_counts[to_station] fig, ax plt.subplots(figsize(10, 6)) sns.barplot(dataroute_counts, yroute, xcount, axax, paletteviridis) ax.set_title(车次数量 Top 10 线路区间) ax.set_xlabel(车次数量) ax.set_ylabel(线路区间) plt.tight_layout() plt.savefig(top_routes.png, dpi150) plt.show()逻辑说明先按出发-到达分组计数再排序取前 10。横向条形图适合展示带中文的类别名避免 x 轴标签挤在一起。参数上ascendingFalse保证数量从大到小head(10)控制图的高度可读。4.3 发车时段用直方图看全天分布规律df[depart_hour] df[depart_norm].str[:2].astype(float) fig, ax plt.subplots(figsize(10, 5)) sns.histplot(df[depart_hour].dropna(), bins24, kdeTrue, axax, colorsteelblue) ax.set_title(全国列车发车时段分布) ax.set_xlabel(发车小时) ax.set_ylabel(车次数量) ax.set_xticks(range(0, 24, 2)) plt.tight_layout() plt.savefig(depart_hour_dist.png, dpi150) plt.show()逻辑说明从标准化时间取前两位作为小时histplot画分布kdeTrue加一条密度曲线看趋势。参数上bins24对应 24 小时set_xticks每两小时标一个刻度避免 x 轴太密。这张图通常能看出早高峰和晚高峰两个峰值写报告时可以直接引用。三张图做完你的可视化部分就有骨架了。如果老师要求“可视化大屏”效果可以把这三张图用subplots拼成一张大图或者用pyecharts做交互版本但核心结论不变。5. 避坑与排查列车数据项目里最容易翻车的五个点这一章按“现象 → 原因 → 解决”写都是我实际踩过的坑你大概率也会遇到。5.1 抓取时返回空数据但状态码是 200现象脚本跑完没报错但 DataFrame 是空的。原因接口对未登录或频率过高的请求返回 200 加空列表而不是 403。解决在解析后加一层判断如果连续多页为空就打印警告并停止同时检查请求头里的User-Agent和Referer是否完整。5.2 时间解析后出现大量 None现象normalize_time之后发现一半以上是 None。原因原始数据里时间字段混入了“次日”“1”等文字正则没匹配到。解决先打印几条原始值看看真实格式再调整正则比如加上r(\d{1,2}):(\d{2})之前先re.sub(r[^\d:], , t)去掉非数字非冒号字符。5.3 车站名归一后仍然有重复现象明明做了别名映射分组统计还是出现“北京西”和“北京西站”两条。原因映射字典没覆盖全或者数据里还有“北京西始发”这种带括号的写法。解决归一函数里加一步去掉括号及内容re.sub(r.*?|\(.*?\), , name)再走别名映射。5.4 跨天车次时长算成负数现象travel_min出现负值。原因到达时间小于发车时间时没有加 24 小时。解决在travel_minutes里判断arr_min dep_min就加24 * 60同时注意有些车次历时超过 24 小时需要根据业务判断是否合理。5.5 中文图表乱码或负号显示异常现象图里中文变成方框负号变成方块。原因matplotlib 默认字体不支持中文。解决在绘图前设置plt.rcParams[font.sans-serif] [SimHei]和plt.rcParams[axes.unicode_minus] False。如果系统没有 SimHei换成Microsoft YaHei或WenQuanYi Micro Hei用matplotlib.font_manager查一下可用字体。6. 从作业到可复现项目把脚本拆成模块并加一层校验如果你想让这份大作业不只是“能跑”而是“别人拿到也能跑”最后这一步值得做把抓取、清洗、可视化拆成三个模块再加一个数据校验函数。我一般会这样组织目录train_analysis/ ├── fetch.py # 抓取与保存原始数据 ├── clean.py # 清洗与派生字段 ├── visualize.py # 绘图与保存图片 ├── validate.py # 数据质量校验 └── data/ ├── raw.csv └── cleaned.csvvalidate.py里放几个关键校验比如记录数是否大于阈值、时间字段缺失率是否低于 10%、车站名是否都在预期集合内。这样每次跑完清洗先过校验再出图能提前拦住大部分脏数据问题。def validate(df): 返回校验结果字典便于打印或写日志 report {} report[row_count] len(df) report[time_missing_rate] df[depart_norm].isna().mean() report[station_unique] df[from_station].nunique() report[distance_missing_rate] df[distance].isna().mean() # 阈值判断 report[pass] ( report[row_count] 100 and report[time_missing_rate] 0.1 and report[distance_missing_rate] 0.2 ) return report参数说明row_count 100是防止抓取失败后拿到空表还继续跑time_missing_rate 0.1是时间字段的底线超过 10% 缺失说明解析逻辑有问题distance_missing_rate 0.2是里程字段的容忍度超过就考虑换填充策略。这三个阈值可以根据你的数据源调整但一定要有不能裸奔。最后说个我自己的习惯每次改完清洗逻辑先跑validate看报告再跑可视化。图好看不代表数据对校验通过才是后悔药。这套流程走下来你的期末大作业就不只是一份源码而是一条能讲清楚、能复现、能答辩的完整分析链路。希望帮到你。本文还有配套的精品资源点击获取
返回列表