☰
机票数据分析与可视化大作业:从数据清洗到ECharts交互看板
2026/10/2 3:03:01 网站建设 项目流程

简介:这是一份面向计算机相关专业学生与项目实战学习者的Python数据分析可视化大作业完整源码,聚焦机票价格数据分析与预测,并借助PyQt5实现可视化界面展示,适合正在准备课程设计、期末大作业或需要练手数据分析项目的人群。资源包共37个文件,压缩后约1.33MB,包含11个py源码文件、9个csv数据集、6个xml配置、6张png图表、2个ui界面文件及说明文档,覆盖数据预处理、回归分析、回归可视化、数据可视化等核心模块,目录结构清晰,便于按功能模块查阅与二次开发。目前已有402人学习下载。项目经导师指导并认可,评审分99分,代码完整可运行,读者可据此掌握从原始数据清洗到价格预测建模、再到PyQt5界面交互展示的完整流程,同时获得可复用的分析脚本与图表产出,适合作为课程设计参考或项目实战练习素材。

1. 机票数据分析与可视化:一份大作业怎么做出生产级质感

机票价格是典型的时序数据,却又比股票、气温难缠得多——同一条航线一天内可能被爬虫抓到十几个价位,航司放票策略、舱位等级、中转方案、退改签规则全搅在一起。很多同学拿到「机票数据分析与可视化」这个大作业标题,第一反应是找份 CSV,df.describe()跑一遍,再plt.plot()画条折线就交差。结果答辩时老师一句「你这个价格波动为什么在周二凌晨出现低谷」就能问穿。这份作业真正的价值不在图表数量,而在于你能不能把原始脏数据变成一条能自圆其说的分析链路:清洗掉重复抓取、对齐时间粒度、拆出航线维度、再让可视化替结论说话。适合正在做 Python 数据分析课程设计、需要一份能拿高分且经得起追问的从业者和学生,也适合想用真实业务数据练手 pandas 与 ECharts 的入门者。下面按我实际做过的顺序,把选型、清洗、分析、可视化到避坑完整走一遍。

2. 数据从哪来、字段怎么定:先想清楚分析目标再动手

2.1 机票数据的三个来源与取舍

做机票分析,数据来源直接决定后面能分析什么。常见做法有三类,我一般会先评估再选:

来源典型字段优点坑
公开数据集(Kaggle/天池类)航司、起降机场、价格、日期干净、可直接用字段少,缺舱位和中转
自己写爬虫抓取航班号、价格、抓取时间、舱位字段可控、时效新反爬、IP 封禁、数据重复
航司开放接口/模拟数据结构化、含余票稳定需申请,作业场景常拿不到

大作业场景我建议优先用公开数据集打底,再补一小段自己抓的实时数据做对比,这样既有规模又有「新鲜感」。如果标题里强调「可视化大屏」,那数据量至少要撑得起多图表联动,几千条起步比较稳。

2.2 字段设计:别等分析时才发现缺列

很多人抓完数据才发现没有「抓取时间」这一列,导致无法分析价格随时间的变化。我一般会强制保留这几列:

  • flight_no:航班号,用于区分同一航线不同班次
  • airline:航司,做航司维度对比
  • dep_city/arr_city:出发到达城市,航线分析的基础
  • dep_time/arr_time:起降时间,用于时段分析
  • price:价格,核心指标
  • cabin:舱位等级,经济舱/商务舱价格差异巨大
  • crawl_time:抓取时间戳,时序分析的关键
  • is_direct:是否直飞,中转会显著影响价格

字段定好后,用 pandas 读进来先看一眼结构和缺失情况:

import pandas as pd # 读取原始数据,注意编码,机票数据常含中文城市名 df = pd.read_csv("flights_raw.csv", encoding="utf-8") # 基础体检:形状、类型、缺失、重复 print(df.shape) print(df.dtypes) print(df.isnull().sum()) print(df.duplicated(subset=["flight_no", "crawl_time"]).sum())

这段代码的作用是先摸清数据底细。shape看规模,dtypes确认price是不是被读成了字符串(常见翻车点),isnull().sum()定位缺失列,duplicated按「航班号+抓取时间」判断重复抓取。参数上,subset一定要选能唯一标识一条记录的字段组合,否则会把正常的多条记录误判为重复。

2.3 时间字段解析:机票分析最容易翻车的地方

机票数据里的时间格式五花八门,2024/3/5 08:30、2024-03-05 8:30、甚至05-Mar-24都可能出现。统一解析是后续所有时序分析的前提:

# 统一时间格式,errors="coerce" 把解析失败的置为 NaT 而不是报错 df["dep_time"] = pd.to_datetime(df["dep_time"], errors="coerce") df["crawl_time"] = pd.to_datetime(df["crawl_time"], errors="coerce") # 解析失败的行要单独看,不能直接丢 bad_rows = df[df["dep_time"].isnull()] print(f"解析失败 {len(bad_rows)} 行") print(bad_rows[["flight_no", "dep_time"]].head()) # 派生字段:出发小时、星期、是否周末 df["dep_hour"] = df["dep_time"].dt.hour df["dep_weekday"] = df["dep_time"].dt.weekday df["is_weekend"] = df["dep_weekday"].isin([5, 6])

errors="coerce"是关键参数,它让解析失败变成NaT而不是抛异常中断整个流程。派生出的dep_hour、dep_weekday是后面分析「什么时段机票便宜」「周末是否更贵」的基础。这一步做完,数据才算真正可用。

3. 清洗与特征工程:把脏数据变成能分析的宽表

3.1 去重、异常价与缺失值处理

原始机票数据里,重复抓取和异常价格是两大污染源。同一航班同一抓取时间被记录两次,会让统计结果虚高;价格出现 0 或 99999 这种极端值,会毁掉均值。处理逻辑:

# 1. 按航班号+抓取时间去重,保留第一条 df = df.drop_duplicates(subset=["flight_no", "crawl_time"], keep="first") # 2. 价格异常值处理:用分位数截断,避免个别极端值带偏 q_low = df["price"].quantile(0.01) q_high = df["price"].quantile(0.99) df = df[(df["price"] >= q_low) & (df["price"] <= q_high)] # 3. 关键字段缺失直接删,非关键字段填充 df = df.dropna(subset=["price", "dep_city", "arr_city"]) df["cabin"] = df["cabin"].fillna("经济舱")

分位数截断比直接设阈值更稳,因为不同航线价格量级差异大,固定阈值会误伤。quantile(0.01)和quantile(0.99)把最极端的 2% 去掉,保留主体分布。这一步的参数要根据数据实际分布调,如果数据本身很干净,可以放宽到 0.005/0.995。

3.2 航线维度与价格区间特征

机票分析的核心维度是「航线」,也就是出发城市到到达城市的组合。把两个字段拼成一个新字段,后续 groupby 会方便很多:

# 构造航线字段 df["route"] = df["dep_city"] + "-" + df["arr_city"] # 价格分箱,用于分布分析 bins = [0, 500, 1000, 1500, 2000, 3000, 10000] labels = ["500以下", "500-1000", "1000-1500", "1500-2000", "2000-3000", "3000以上"] df["price_level"] = pd.cut(df["price"], bins=bins, labels=labels) # 看热门航线 Top10 top_routes = df["route"].value_counts().head(10) print(top_routes)

pd.cut做等距分箱,labels让结果可读。分箱边界要结合数据实际价格范围定,如果数据集中在 800-1200,那 bins 就要相应加密。value_counts()快速看出哪些航线数据量大,数据量太少的航线在后续分析里要谨慎下结论。

3.3 按航线和时间聚合:生成分析用的宽表

清洗完的明细数据还不能直接画图,需要聚合成「航线-日期-均价」这样的宽表:

# 按航线和出发日期聚合,算均价、最低价、航班数 route_daily = df.groupby(["route", df["dep_time"].dt.date]).agg( avg_price=("price", "mean"), min_price=("price", "min"), max_price=("price", "max"), flight_count=("flight_no", "count") ).reset_index() route_daily.columns = ["route", "dep_date", "avg_price", "min_price", "max_price", "flight_count"] print(route_daily.head()) # 按航司聚合,做航司对比 airline_stat = df.groupby("airline").agg( avg_price=("price", "mean"), total_flights=("flight_no", "count") ).sort_values("avg_price", ascending=False)

groupby后接agg可以一次算出多个统计量,比循环高效得多。reset_index()把分组键还原成列,方便后续导出和画图。这一步产出的route_daily就是可视化阶段的主力数据源,airline_stat用于航司对比图。

4. 可视化落地:从静态图到可交互看板

4.1 用 Matplotlib 快速出探索性图表

分析阶段先用 Matplotlib 快速看趋势,不用追求美观,重点是验证结论:

import matplotlib.pyplot as plt import matplotlib # 中文显示配置,不做这步中文全是方块 matplotlib.rcParams["font.sans-serif"] = ["SimHei"] matplotlib.rcParams["axes.unicode_minus"] = False # 选一条热门航线看价格趋势 sample = route_daily[route_daily["route"] == "北京-上海"].sort_values("dep_date") plt.figure(figsize=(12, 5)) plt.plot(sample["dep_date"], sample["avg_price"], marker="o", label="均价") plt.plot(sample["dep_date"], sample["min_price"], marker="s", label="最低价", linestyle="--") plt.xlabel("出发日期") plt.ylabel("价格(元)") plt.title("北京-上海航线价格趋势") plt.legend() plt.xticks(rotation=45) plt.tight_layout() plt.savefig("route_trend.png", dpi=150)

font.sans-serif设成SimHei是中文环境的标准操作,Linux 上可能要用WenQuanYi之类字体。tight_layout()防止标签被裁掉,dpi=150保证导出清晰度。这张图用来快速判断价格是否有周期性波动,是后续深入分析的方向指引。

4.2 用 ECharts 做可交互可视化大屏

大作业如果要求「可视化大屏」,ECharts 是性价比最高的选择。核心思路是后端把聚合数据转成 JSON,前端用 ECharts 渲染。先准备数据接口:

import json # 把航线均价数据转成 ECharts 需要的格式 route_avg = df.groupby("route")["price"].mean().sort_values(ascending=False).head(15) chart_data = { "routes": route_avg.index.tolist(), "prices": [round(p, 2) for p in route_avg.values.tolist()] } with open("route_avg.json", "w", encoding="utf-8") as f: json.dump(chart_data, f, ensure_ascii=False)

ensure_ascii=False保证中文正常写入,否则会变成\uXXXX转义。round(p, 2)控制小数位,避免前端显示一长串。这个 JSON 就是前端 ECharts 的数据源。

前端渲染部分:

// 读取后端生成的 JSON,渲染柱状图 fetch('route_avg.json') .then(res => res.json()) .then(data => { const chart = echarts.init(document.getElementById('routeChart')); chart.setOption({ title: { text: '热门航线均价 Top15' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: data.routes, axisLabel: { rotate: 45 } }, yAxis: { type: 'value', name: '均价(元)' }, series: [{ type: 'bar', data: data.prices, itemStyle: { color: '#5470c6' } }] }); });

echarts.init绑定容器,setOption里xAxis的rotate: 45防止航线名重叠。tooltip的trigger: 'axis'让鼠标悬停显示整列数据。这套结构可以复制到折线图、饼图,只改series.type即可。

4.3 时段热力图:把「什么时候买便宜」讲清楚

机票分析最有说服力的图表之一是「出发时段 vs 价格」的热力图。用 pandas 透视表生成矩阵,再交给 ECharts 的 heatmap:

# 按出发小时和星期做透视,值取均价 pivot = df.pivot_table( values="price", index="dep_weekday", columns="dep_hour", aggfunc="mean" ).round(0) # 转成 ECharts heatmap 需要的 [x, y, value] 格式 heat_data = [] for y, row in enumerate(pivot.values): for x, val in enumerate(row): if not pd.isna(val): heat_data.append([x, y, int(val)]) print(f"热力图数据点:{len(heat_data)}")

pivot_table的aggfunc="mean"算均价,round(0)取整。转成[x, y, value]三元组是 ECharts heatmap 的标准输入格式。pd.isna过滤掉没有数据的格子,否则前端会显示异常。这张图能直观回答「周二凌晨是不是真的便宜」这类答辩高频问题。

5. 避坑与排查:机票分析里那些血泪经验

5.1 价格字段被读成字符串,统计全错

现象:df["price"].mean()报错或者返回奇怪结果,describe()里 price 显示为 object 类型。

原因:原始 CSV 里价格带了货币符号(如¥1200)或千分位逗号(1,200),pandas 默认按字符串读入。

解决:读入时用dtype={"price": float}强制转换,或者读入后清洗:

df["price"] = df["price"].astype(str).str.replace(r"[¥,]", "", regex=True).astype(float)

str.replace用正则一次去掉货币符号和逗号,再转 float。这一步不做,后面所有价格统计都是错的。

5.2 时区不统一导致时序分析错位

现象:价格趋势图在凌晨出现莫名其妙的尖峰,或者同一天的数据被拆成两天。

原因:抓取时间用了 UTC,而起降时间是本地时间,两者混用导致时间对齐错误。

解决:统一到一个时区,通常用出发地本地时间:

df["crawl_time"] = pd.to_datetime(df["crawl_time"], utc=True).dt.tz_convert("Asia/Shanghai").dt.tz_localize(None)

tz_convert转时区,tz_localize(None)去掉时区信息变成 naive 时间,方便和起降时间对齐。时区问题不解决,所有按天的聚合都不可信。

5.3 重复抓取没去干净,均价被拉偏

现象:某条航线均价明显高于预期,检查发现同一航班被记录了多次。

原因:爬虫重试机制导致同一时间点抓了两次,或者drop_duplicates的 subset 选错。

解决:用「航班号+抓取时间+价格」三个字段联合去重,并检查去重前后行数变化:

before = len(df) df = df.drop_duplicates(subset=["flight_no", "crawl_time", "price"], keep="last") print(f"去重前 {before} 行,去重后 {len(df)} 行,删除 {before - len(df)} 行")

keep="last"保留最新一条,因为后抓的通常更准。打印删除行数是为了确认去重逻辑没有误删正常数据。

5.4 中文乱码让图表标题变方块

现象:Matplotlib 图表里中文全是方框,ECharts 里中文正常。

原因:Matplotlib 默认字体不含中文,需要手动指定。

解决:除了前面设font.sans-serif,Linux 服务器上还要确认字体已安装:

# 查看系统可用中文字体 fc-list :lang=zh

如果没有输出,需要安装中文字体包。Windows 上一般有SimHei,Mac 上用Arial Unicode MS。这个坑不解决,导出的图直接不能用。

5.5 数据量太大导致前端卡死

现象:ECharts 渲染几万个点时页面卡顿甚至崩溃。

原因:把明细数据直接丢给前端,没有做聚合。

解决:后端先聚合再传,前端只渲染聚合后的结果。比如热力图最多 7×24=168 个点,柱状图 Top15 就 15 个点。明细数据留在后端做分析,前端只负责展示结论。

6. 让这份作业真正拿高分的两个进阶技巧

第一个技巧是给分析加「对照组」。单独说「北京-上海均价 1200」没有说服力,但如果说「北京-上海均价 1200,比北京-广州高 18%,且周末溢价达 25%」,结论立刻立体。实现上就是多做几次 groupby 再算比值:

# 航线均价对比,算相对基准的溢价 base = df[df["route"] == "北京-上海"]["price"].mean() compare = df.groupby("route")["price"].mean().reset_index() compare["premium_pct"] = ((compare["price"] - base) / base * 100).round(1) compare = compare.sort_values("premium_pct", ascending=False) print(compare.head(10))

premium_pct就是相对基准航线的溢价百分比,答辩时这种相对指标比绝对值更能体现分析深度。

第二个技巧是加一个「预测」小模块。不用上复杂模型,用statsmodels做个简单的季节性分解或移动平均,就能让作业从「描述性分析」升级到「预测性分析」:

from statsmodels.tsa.seasonal import seasonal_decompose # 取一条航线的时间序列 ts = route_daily[route_daily["route"] == "北京-上海"].set_index("dep_date")["avg_price"] ts = ts.asfreq("D").interpolate() # 补齐缺失日期 # 季节性分解,period=7 表示按周周期 result = seasonal_decompose(ts, model="additive", period=7) result.plot()

asfreq("D")把时间序列对齐到每天,interpolate()补缺失值,period=7假设一周一个周期。分解出的趋势项和季节项能直接支撑「价格有周期性」的结论。这个模块代码量不大,但能让老师看到你懂时序分析的基本套路。

我自己做这类作业最大的教训是:别一上来就堆图表。先把数据清洗和时间对齐做扎实,再想「这张图要回答什么问题」。图表是结论的载体,不是装饰。一份能拿高分的机票分析,核心是让每个数字都有出处、每个结论都能追溯到某段代码。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询