
简介这套航班价格预测实战资源面向机器学习入门者与想提升回归模型调优能力的开发者围绕航班票价结构化数据从数据读取、EDA可视化到特征工程、多种回归模型训练与效果对比完整演示一个回归分析项目如何落地。资源共23个文件包含19个Python源码与3个CSV数据文件合计约53MB另附1个说明文档其中代码覆盖数据预处理、缺失值填充、标签编码、独热编码、多模型基准比较、随机森林/集成学习/网格搜索与随机搜索调参、部分依赖图分析等环节压缩包仅3.93MB便于下载。目前已有131人学习下载。除模型核心脚本外还提供数据分析与可视化、特征选择如多重共线性VIF、模型持久化pickle/joblib等细节代码读者可对照数据集逐步运行也可直接迁移到自己的航班价格或类似回归任务中。1. 航班价格预测为什么这套数据与源码包值得先跑一遍航班价格预测是机器学习实操里少有的「数据集干净、目标清晰、当天能出结果」的项目一张航班信息表加上价格目标列用 Python 做特征工程再交给回归模型预测票价链路短、反馈快。这套资源里的 53.07 MB 航班价格数据集和 19 个源代码文件正好覆盖了从 CSV 清洗、特征拆解到模型训练与预测导出的完整闭环。它适合刚学完 Python 基础、想用真实数据做第一个完整机器学习项目的新手也适合想在时间特征、评估口径上快速验证思路的熟手。看多了 AI 大模型的花活之后回到这种「小数据集 经典回归」项目里把基本功补一补是挺值得的一件事。2. 航班价格数据集拆解从字段语义到预处理流水线这份资源不是「下载完直接 train_test_split 就能出漂亮结果」的玩具集。它里面有缺失值、有异常票价、有时间戳字段不预处理直接建模后面调参就全是玄学。以下按我拿到这类数据集的习惯流程来拆。2.1 先看字段口径哪些列能当特征哪列是预测目标解压后先定位数据集源文件通常是一个 CSV行数在十万级、列数十几列。先读 shape 和 dtype别急着跑模型第一步要搞清楚每个字段是「建模时点就能拿到的特征」还是「要预测的目标」——这个边界决定了后面会不会出现数据泄漏。字段类型业务含义预处理方向airlinestr航空公司代码类别编码one-hot 或频次flight_datedatetime起飞日期拆分年/月/周/星期不放连续天数departure_timeint起飞时刻点分箱为业务时段arrival_timeint到达时刻点与起飞时刻组合成飞行时段特征durationfloat飞行时长小时清理 0 与负值后保留stopsint经停次数按 0/1/2 取值稀疏时转类别route_citystr出发城市-到达城市高频航线编码低频归为其他seat_leftint剩余座位数与价格常呈负相关保留pricefloat价格元目标变量检查离群值后作为 y先跑 df.describe() 看 price 的 min、max 和分位数。常见状况是经济舱集中在几百到几千元尾部有几万元的商务/头等舱票。这类离群值不能直接当噪声删掉要看清楚它是不是「少数但真实存在的高价产品」否则后面 MAE 会被它拉得很离谱。统计口径建议以分位数为准p25、p50、p75 讲了大多数票价的范围max 只代表极端产品。另外要留意 route_city 的组合数量。几十个城市两两配对后可能有上百条航线直接 one-hot 会让特征矩阵非常稀疏。常见做法是只对频次排前 30 的航线做编码其余统一归为一个「other」类别——这一刀能显著降低训练噪声。如果你发现 airline 的取值也只有十来种那就没必要做频次编码直接 one-hot 就够了。2.2 时间与类别特征转换周期性拆解和频次编码时间字段是航班价格预测里信息密度最高的一列也是最容易泄漏的一列。日期本身不能直接进模型要拆成周期性成分让模型学到「旺季、周末、一天中的早中晚」这类业务规律而不是「日期越往后价格越高」这种伪规律。import pandas as pd df pd.read_csv(data/flight_prices.csv, parse_dates[flight_date]) df[month] df[flight_date].dt.month df[day_of_week] df[flight_date].dt.dayofweek df[is_weekend] df[day_of_week].isin([5, 6]).astype(int) df[week_of_year] df[flight_date].dt.isocalendar().week df[departure_block] pd.cut( df[departure_time], bins[-1, 6, 11, 13, 17, 20, 24], labels[深夜, 早班, 午前, 午后, 晚高峰, 夜间] )第一行的 parse_dates 让 pandas 直接把 flight_date 识别成 datetime 类型后面才能用 .dt 访问器拆出 month、day_of_week 这些周期特征。day_of_week 取值范围是 0 到 65、6 对应周末用 isin 转成 0/1 的 is_weekend。week_of_year 用的是 isocalendar 返回的 ISO 周数比直接取「第几周」更稳因为它正确处理了跨年边界。departure_block 的 bins 我按行业习惯切-1 到 6 是深夜红眼6 到 11 早班11 到 13 午前13 到 17 午后17 到 20 晚高峰20 之后夜间。这个切法比原始小时数值更适合后续模型切分——树模型对连续小时也能切但业务时段能减少过拟合也方便做分组分析。pd.cut 的 bins 左开右闭所以边界值 6 会被分到「早班」而不是「深夜」写边界时脑子里要过一遍这个逻辑。类别特征的处理我建议分开走airline 这种数量少的字段直接 one-hotroute_city 数量多就走频次编码。departure_block 这种有序时段进树模型前映射成 0~5 的有序整数比 one-hot 更贴合「早中晚」的顺序语义。airline_dummies pd.get_dummies(df[airline], prefixairline) df[route_freq] df[route_city].map(df[route_city].value_counts()) df[departure_block_id] df[departure_block].cat.codes df_encoded pd.concat([df, airline_dummies], axis1)get_dummies 会把航空公司拆成一列一个 0/1 变量prefix 参数控制列名前缀避免和原列重名。route_freq 是把每条航线出现的次数映射回每一行相当于告诉模型「这条航线是热门还是小众」——这个特征在很多场景下比航线 one-hot 泛化能力更好因为测试集里完全没出现过的航线也能靠频率特征兜底。cat.codes 把有序时段转成有序整数但要注意它按类别字母序编码不是按业务顺序如果你要严格控制顺序要手动写映射字典。2.3 缺失值、异常价格与特征清单落盘缺失值处理上价格列不能随便填出现缺失直接删行duration 出现 0 或负值多半是录入问题我一般 clip 到 0.5seat_left 缺失可以用中位数填空。异常价格过滤要谨慎低于 50 元多为促销价或数据错误高于 80000 元多为包机或特殊产品这两个边界外的样本先标记出来建模时单独观察不要一上来就全局删掉。import pandas as pd df_clean df_encoded.copy() df_clean[duration] df_clean[duration].clip(lower0.5) df_clean[seat_left] df_clean[seat_left].fillna(df_clean[seat_left].median()) df_clean df_clean[(df_clean[price] 50) (df_clean[price] 80000)] feature_cols [c for c in df_clean.columns if c not in [price, flight_date, route_city]] df_clean[feature_cols [price]].to_csv(data/flight_features.csv, indexFalse)clip 的下限设成 0.5 是防止极端小值在后续处理时把特征范围拉得更歪fillna 用中位数而不是均值因为席位分布通常右偏中位数更稳健。价格过滤写在加载脚本里而不是 notebook 里是为了保证每次重跑结果一致。最后把特征列列表单独存一份feature_cols 这个列表在训练与预测时都要复用。提示特征列清单一定要单独保存预测新数据时先按这份清单做列过滤。少一列或多一列模型预测时就会报特征数量不一致。到这里一份能直接进模型的特征表就算成型了。接下来进入建模环节先定评估口径再谈模型选型。3. 机器学习建模评估口径、模型对比与超参数收敛建模阶段最容易犯的错不是模型选得不够高级而是目标函数和评估口径没定清楚就开始调参。航班价格预测本质是回归问题但「回归」只是大方向用哪个指标评价模型直接决定后续调参方向。3.1 评估口径先定MAE 为何比 RMSE 更适合票价场景先看代码from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score y_pred model.predict(X_test) mae mean_absolute_error(y_test, y_pred) rmse mean_squared_error(y_test, y_pred, squaredFalse) r2 r2_score(y_test, y_pred) print(fMAE: {mae:.2f} 元) print(fRMSE: {rmse:.2f} 元) print(fR²: {r2:.4f})三个指标各有各的脾气。MAE 是「平均差多少钱」单位是元业务人员能直接听懂RMSE 把误差平方后开根会给大误差更高的权重所以只要测试集里有几张几万元的头等舱票RMSE 就会被拉得很高看起来模型效果很差R² 只看拟合比例样本分布变化时容易虚高。我的建议是报告以 MAE 为主指标RMSE 作为参考R² 作为辅助。如果模型在经济舱样本上预测得很好但测试集混进了少量头等舱MAE 还能保持稳定RMSE 一上去你还没查特征先会怀疑整个模型坏了白白浪费时间。这是血泪经验先想清楚你要向谁汇报结果再选指标不要默认全都要。3.2 模型选型线性基线到梯度提升的参数收敛模型选择上我坚持一个原则先跑线性回归做基线再上树模型。线性回归虽然大概率跑不过树模型但它适合用来验证特征和数据的逻辑一致性——如果线性基线 MAE 离谱到几千元多半是特征里有泄漏或异常值没洗干净这时候停下来比换模型更划算。from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.ensemble import GradientBoostingRegressor X pd.read_csv(data/flight_features.csv) y X.pop(price) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) baseline LinearRegression() baseline.fit(X_train, y_train) baseline_mae mean_absolute_error(y_test, baseline.predict(X_test)) print(fLinearRegression MAE: {baseline_mae:.2f} 元)注意我在这里用了随机切分目的是快速验证全链路有没有 bug正式评估必须换成时间切分或分组切分这个差异在第 5 章和第 6 章专门讲。random_state42 并不代表试几次会「更准」它只是让每次切分结果一致方便对比不同模型在同一个测试集上的表现。如果不固定它每跑一次代码测试集都会变模型对比就失去了基准。跑完基线再上 GBDT。航班价格数据里特征之间很少是简单线性关系梯度提升树天然能拟合非线性、处理不平衡尺度是这类表格数据性价比最高的选择。参数组合我一般这样起步gbr GradientBoostingRegressor( n_estimators300, learning_rate0.05, max_depth4, subsample0.8, random_state42 ) gbr.fit(X_train, y_train) gbr_mae mean_absolute_error(y_test, gbr.predict(X_test)) print(fGradientBoosting MAE: {gbr_mae:.2f} 元)这批参数的取舍逻辑参数取值调整思路n_estimators300越大越容易过拟合需要配合早停观察验证集误差learning_rate0.05调小能提升精度但树的数量要相应增加max_depth4表格数据 3~5 通常够用太深会把噪声也学进去subsample0.8每棵树用 80% 样本增加随机性、降低过拟合树的数量 300 配合学习率 0.05是 GBDT 一个比较稳妥的起步组合。如果你把库换成 XGBoost 或 LightGBM参数名会变learning_rate 对应 eta但调参思路一样先固定深度再调学习率和树数量不要一上来就网格搜索全部参数——那是把调参当玄学。调参建议分三档第一档用小规模数据跑通全链路确认代码没 bug第二档在验证集上做粗调观察 n_estimators 从 100 加到 300 时 MAE 是否还在下降第三档固定 n_estimators 后微调 learning_rate。这套源码里大约四分之一的脚本是围绕这个流程展开的训练时你会看到中间结果被逐步打印出来。模型能跑通、指标能对比之后下一步是看这套源码怎么把「能跑」变成「能落地」。4. 19 个源代码的落地路径加载、训练、预测全链路这份资源叫「19 个源代码 数据集」新手拿到手最容易犯的错是挨个打开每个 .py 文件右键运行运行到第 7 个文件报错就慌了。事实上这个体量的项目源文件是按数据处理流水线拆的运行顺序本身就是流水线顺序。4.1 源码结构文件编号、模块依赖与运行顺序一套典型的划分是前几个文件负责数据加载与清洗中间是特征工程与探索性分析然后是模型训练与评估最后是预测导出。可以对照下面的结构理解你看到的文件模块文件职责运行顺序数据层读取原始 CSV、字段重命名、类型转换01、02清洗层缺失值、异常价格、时长清理03、04特征层时间拆分、类别编码、特征列落盘05、06建模层基线模型、GBDT 训练、参数对比07、08、09评估层MAE/RMSE 计算、特征重要性、误差分布10、11预测层加载模型、对新数据预测、导出 CSV12辅助层绘图、结果汇总、配置常量其余文件如果文件名带了编号就按编号从小到大跑没带编号的按上面的依赖顺序自己排。每个文件头部通常会有一段注释说明它的输入文件和输出文件先读注释再运行。这 19 个文件不是 19 个并列的算法而是一条流水线的不同环节跳过清洗层直接跑训练脚本十有八九会因为列名对不上而报错。4.2 训练脚本走读列清单校验与模型保存训练脚本是整套源码里最值得细读的部分。它一般会先加载特征 CSV再加载特征列清单做列过滤之后训练并保存模型。我按常见写法还原一个骨架import joblib import pandas as pd from sklearn.ensemble import GradientBoostingRegressor from sklearn.model_selection import train_test_split from sklearn.metrics import mean_absolute_error df pd.read_csv(data/flight_features.csv) with open(data/feature_cols.txt, r) as f: feature_cols [line.strip() for line in f if line.strip()] X df[feature_cols] y df[price] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) model GradientBoostingRegressor( n_estimators300, learning_rate0.05, max_depth4, subsample0.8, random_state42 ) model.fit(X_train, y_train) mae mean_absolute_error(y_test, model.predict(X_test)) print(fvalidation MAE: {mae:.2f}) joblib.dump(model, models/price_gbdt.pkl)从 feature_cols.txt 读列清单这一步值得强调它保证训练和预测永远使用同一批特征哪怕中间有人改了特征工程脚本也不会因为列变动而静默出错。joblib.dump 保存的是完整模型对象包括训练时的参数和训练好的树结构预测时不需要重新训练。注意 X_train 的数据类型最好是 float64 或 int64 的数值矩阵如果里面有 object 类型的列没编码sklearn 的 GBDT 会直接抛 ValueError。另外如果源码里训练脚本用的是随机切分而你准备拿这个模型做正式业务预测我建议你把切分方式改成按时间顺序或按航班 ID 分组否则后面评估数字会偏乐观。这一点我放在避坑章展开。4.3 预测脚本与结果导出一个断言防翻车训练脚本保存模型后预测脚本的逻辑就很简单了读模型读新数据走和训练时完全相同的特征工程然后 predict。注意「完全相同」四个字——新数据预处理必须复用训练时的清洗函数不能手抄一份。import joblib import pandas as pd model joblib.load(models/price_gbdt.pkl) with open(data/feature_cols.txt, r) as f: feature_cols [line.strip() for line in f if line.strip()] new_df pd.read_csv(data/new_flight_requests.csv) new_X new_df[feature_cols] assert set(feature_cols) set(new_X.columns), 预测数据特征列与训练不一致 new_df[predicted_price] model.predict(new_X) new_df.to_csv(output/predicted_prices.csv, indexFalse)这里有个常见翻车点如果 new_flight_requests.csv 里多了一列 bonus_price直接用 new_df[feature_cols] 会抛 KeyError如果少了一列程序会静默把列顺序打乱整批预测结果直接报废。所以预测脚本里我习惯加一行断言校验列是否完全一致。assert 只在列数量差很多时能拦住问题更严谨的做法是再校验数据类型比如 new_X 里的 duration 列必须是 float一旦是字符串GBDT 预测会报不可解释的内存错。提示模型文件、特征列清单、清洗脚本这三样要放在一起管理。只给别人一个 .pkl 模型文件、不给特征清单的代码包基本等于只给钥匙不给锁孔。到这步你已经有了「训练出模型 → 保存模型 → 对新数据预测」的完整链路。接下来讲讲链路跑通之后最容易栽的四个坑每一个我都实际踩过。5. 航班价格预测避坑指南数据泄漏、时间穿越与评估失真航班价格数据带着明显的时间属性普通机器学习流程套上去会触发好几个隐蔽问题。下面四个坑按出现频率排现象和解决都来自实际跑数据的过程。5.1 坑一把出发日期转成数值天数模型学会了「时间穿越」现象模型在测试集上的 MAE 低得离谱比验证时还好一截但换一批新日期数据预测就崩。原因有人把 flight_date 直接转成 Timestamp 的整数形式当成数值特征。航班价格天然带着随日期波动的趋势模型学到的其实是「第几天对应什么价格」的查表式记忆。测试集日期整体在训练集之后模型相当于顺着时间外推看似准确实则是时间穿越。这在时间序列预测里是最常见的泄漏源。解决日期转成周期性特征后再进模型用 month、day_of_week、week_of_year不要用连续天数。如果你确实想保留长期趋势就显式构造「距离节假日天数」这类有业务含义的特征。判断方法也简单把模型的特征重要性打印出来如果某个「日期数值」特征重要性排第一基本可以断定踩坑了。5.2 坑二同一航班的多条记录同时落进训练集和测试集现象随机切分下测试集 MAE 非常稳定但模型上线后对新请求预测误差明显变大。原因同一趟航班在原始数据里可能有多条记录来自不同渠道或不同舱位剩余。随机切分把它们拆散训练集和测试集里都有同一航班的样本模型在测试集上是在「回忆」而不是「预测」。它见过了同一航班其他渠道的价格测试自然漂亮真要预测一个新航班的票价时没有类似记忆可用误差立刻现形。解决切分前先按 flight_id 或日期航线航班号组合去重保证同一航班的记录只出现在一侧。用 sklearn 的 GroupShuffleSplit 按航班 ID 分组切分而不是普通 train_test_split。排查方法切分后分别取训练集和测试集的航班 ID 集合计算交集大小如果重叠比例不为 0就要重新切。from sklearn.model_selection import GroupShuffleSplit groups df[flight_id] gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(gss.split(X, y, groups))GroupShuffleSplit 的 groups 参数传航班 ID 列表它保证同一个 ID 的所有行只出现在 train_idx 或 test_idx 中的一侧。坏处是测试集的大小不再精确等于总样本的 20%因为它是按组切而不是按行切评估时要以实际测试行数为准。5.3 坑三只用 R² 判断效果被头等舱离群价格带偏现象R² 显示 0.9 以上模型看起来很好但对经济舱做分组评估MAE 却高得离谱。原因R² 对均值附近的大多数样本很宽容几万元的头等舱样本即使预测偏了五千元平方项也会贡献很大方差反而让 R² 显得高。整个模型为了照顾这些高价样本可能把经济舱价格也预测偏高。指标选错结论全反。解决按舱位或价格区间分组计算 MAE。经济舱看一组商务/头等舱看一组两个 MAE 分开报告。只要经济舱 MAE 在可接受区间、头等舱误差大但数量少这样的模型在业务上就是可接受的。价格区间划分建议按分位数来做比如 p75 以上算高价组这样两组样本量不会太悬殊。5.4 坑四先归一化再切分测试集信息提前进了训练集现象把 StandardScaler 的 fit_transform 用在全部数据上然后再切训练集和测试集测试集分数异常高。原因归一化时 fit 到了全量数据的均值和标准差相当于测试集的分布信息已经透传给了训练过程。数据泄漏不只发生在特征选择阶段预处理阶段同样会。树模型对尺度不敏感所以不受影响但如果你上了线性回归或 KNN这个问题会真实放大误差。解决先切分再对训练集 fit、对测试集只 transform或者用 sklearn 的 Pipeline 把预处理和模型封装成一条流水线。最稳妥的写法是这样from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler pipe make_pipeline(StandardScaler(), GradientBoostingRegressor(max_depth4, random_state42)) pipe.fit(X_train, y_train)Pipeline 会把 StandardScaler 的 fit 限制在训练集内测试时只做 transform从机制上杜绝这类预处理泄漏。注意 GBDT 本身不需要标准化这个例子是为了演示 Pipeline 的用法实际场景里如果你用线性模型管道里换成 LinearRegression 即可。注意这四个坑的共同根源是「把普通机器学习流程直接套在带时间性质的业务数据上」。航班价格天然是时间序列数据处理方式必须和静态表格区分开。6. 用滚动回测验证预测稳定性给模型一个航班趋势的考验常规 train_test_split 只验证「模型在这份数据上拟合得好不好」验证不了「模型对未来的预测稳不稳」。航班价格有季节、节假日、航线冷热变化一个只在随机样本上表现好的模型未必能扛住时间推移。6.1 用 TimeSeriesSplit 做滚动切分把数据按时间排序后用前一段训练、后一段验证再往下一步这就是滚动回测。sklearn 自带 TimeSeriesSplit可以直接用from sklearn.model_selection import TimeSeriesSplit df_sorted df.sort_values(flight_date) X_sorted df_sorted[feature_cols] y_sorted df_sorted[price] tscv TimeSeriesSplit(n_splits5) for fold, (train_idx, test_idx) in enumerate(tscv.split(X_sorted)): X_tr, X_te X_sorted.iloc[train_idx], X_sorted.iloc[test_idx] y_tr, y_te y_sorted.iloc[train_idx], y_sorted.iloc[test_idx] model GradientBoostingRegressor( n_estimators300, learning_rate0.05, max_depth4, subsample0.8, random_state42 ) model.fit(X_tr, y_tr) mae mean_absolute_error(y_te, model.predict(X_te)) print(ffold {fold 1} MAE: {mae:.2f})TimeSeriesSplit 每次都在历史数据上训练、在后续数据上验证测试集永远晚于训练集从机制上杜绝了「时间穿越」。n_splits5 会把数据切成 5 份递进验证每一折的 MAE 单独打印最后取平均。重点看每折 MAE 的走向如果 5 折误差越来越低说明模型在不断利用更多历史数据、预测更稳如果忽高忽低或最后一折暴涨说明模型对近期价格波动不敏感可能是特征里缺节假日或航线热度信号。这个判断比单个测试集 MAE 值更有说服力。如果最后一折暴涨我一般会回查最后一折对应的日期段看是不是覆盖了国庆或春节这种特殊时段再决定要不要加节假日特征。6.2 固定随机种子与一份可复现实验记录跑回测之前先把 random_state 和依赖版本定格。sklearn 模型、pandas 的 sample、numpy 的随机数都要各自固定种子依赖版本建议生成一份 requirements.txt把 pandas、scikit-learn、joblib 的版本号锁住。模型训练完用 joblib 保存每个折的模型文件回测结果写进 CSV方便后面回溯。从那以后我每次拿到这类航班价格预测资源都强制自己先跑一遍滚动回测再看常规 MAE——如果滚动回测的最终折误差比随机切分高出一大截就说明之前那个「漂亮数字」里有水分要么是时间穿越要么是样本重叠。这套源码和数据集里能让我反复验证的正是回测这一步。希望帮到你。本文还有配套的精品资源点击获取