YAOTU INSIGHTS

航班价格预测实战:特征工程与时间序列建模

航班价格预测实战:特征工程与时间序列建模
简介一套完整的航班价格预测实战资源包包含19个Python源代码与3个CSV数据集经济舱、商务舱及清洗后的完整航班数据外加1份说明文档共23个文件。压缩包大小约3.93MB解压后数据集合计约53.07MB适合机器学习、数据挖掘课程作业或航班价格预测相关竞赛练习。代码手工整理、无语法错误覆盖从EDA可视化、特征工程到模型训练与评估的全流程。模型方面整合了线性回归、岭回归、Lasso、随机森林、梯度提升、AdaBoost、XGBoost、LightGBM、CatBoost、SVR、K近邻、Bagging等十余种算法并包含GridSearchCV/RandomizedSearchCV超参数调优、交叉验证、VIF多重共线性检验、LIME可解释性分析等进阶内容。此外还提供特征变换、缺失值处理、Pipeline封装等工具脚本可直接复用。目前已有131人学习浏览适合希望系统掌握回归预测建模与模型对比的读者。1. 航班价格数据集分析预测拿到.zip先别急着解压先想清楚你要预测什么航班价格数据集分析预测这类AI实战项目最容易被低估的是那份53.07 MB的数据集和19个源代码。多数人拿到.zip后的第一反应是解压、跑模型结果被数据格式、时间特征和验证集口径磨掉一整天。这套项目看着像普通回归任务实际上是在做“价格随距离出发日的时间演变”预测核心不是调精度而是把日期、航班时段、提前购买天数变成模型真正能吃的特征。适合正在学数据分析与预测、又想拿完整案例练手的从业者也适合做时间序列表格式预测的人快速复用特征工程套路。先花十分钟把压缩包和字段结构理清后面能少走半天弯路。2. 解开.zip和摸清数据底细从压缩包到能跑通的DataLoader2.1 先确认压缩包完整性别在zip伪加密上浪费时间拿到“航班价格数据集分析预测实例.zip”这个包我一般不会直接双击解压。第一步先用命令行验证压缩包完整性顺便看看有没有被网盘或者传输过程弄坏。很多人一遇到解压要求输密码就开始搜“zip伪加密”“zip密码移除”实际上大多数公开分享的数据包要么没加密要么只是改了加密标志位根本不用破解。unzip -t AI实战-航班价格数据集分析预测实例.zip-t是 test integrity逐文件校验 CRC。如果输出到处都是OK说明包没问题。接着解压到固定目录unzip -o AI实战-航班价格数据集分析预测实例.zip -d ./flight_price cd ./flight_price ls -lh-o表示覆盖已有文件-d指定输出目录。执行完能看到典型的项目布局data/里放原始 CSVsrc/里放 19 个 Python 源代码通常按01_load_data.py、02_clean_data.py这样编号README 里解释字段含义。解压时如果unzip提示 need password先别急着找破解工具查一下加密标志位zipinfo -v flight_price.zip | grep -i encryption如果输出显示encryption: none但解压却要求密码那就是伪加密。原因是分享者为了绕过某些平台的文件检测手动改了 zip 头的加密标志实际数据字节没有加密。常见做法是用 7-Zip 直接解压或者用 Python 的zipfile模块尝试读取伪加密包经常能直接读出内容。真正加密的包文件头部的加密标志是有效且数据被真实变换的这类包不存在通用后悔药只能回来源找密码。花在这种包上的时间远不如重新找一份干净的数据集下载。2.2 用pandas读入航班价格表先看形状和缺失解压完成不代表能直接建模。航班价格数据集最常见的字段是航班号、出发日期、出发时间、到达时间、舱位、提前购买天数、当前价格有时还带出发城市和到达城市。第一步不是画图而是用 pandas 把数据读进来看形状、列类型和缺失情况。import pandas as pd df pd.read_csv(data/flight_price.csv, encodingutf-8) print(df.shape) print(df.dtypes) print(df.isnull().sum()) print(df.head(3))df.shape告诉你行数和列数先建立体感。df.dtypes检查每列类型这个很重要因为价格列经常被读成 object 而不是 float多半是数据里混了千分位逗号或者人民币符号。df.isnull().sum()统计缺失方便决定是丢弃还是填充。一个常见坑是价格列里有¥1,234这样的脏值直接pd.to_numeric会报错。处理办法是先把非数值字符去掉再强制转类型df[price] ( df[price] .astype(str) .str.replace(¥, ) .str.replace(,, ) .astype(float) )这里的关键在于用errorscoerce把无法解析的值变成 NaN后续统一填充而不是让整个脚本中断。月份和星期这类数字列如果被读成 float也要确认是否真的是类别比如星期几只有 0-6不该出现 3.5。数据集的列名在不同版本里可能叫query_date、depart_date也可能叫search_date、flight_date先读前几行确认语义别想当然。2.3 把日期和航班时段拆成预测能用的特征少做一步后面全白搭航班价格预测的特征工程核心是时间。出发日期是星期几、是否临近节假日、提前购买天数这三项直接决定了价格高低。另一个容易漏掉的是出发时刻早班机通常比晚班机便宜红眼航班更是价格洼地。我一般会把原始日期时间拆成下面这些特征df[depart_datetime] pd.to_datetime(df[depart_date]) df[weekday] df[depart_datetime].dt.weekday df[month] df[depart_datetime].dt.month df[is_weekend] df[weekday].isin([5, 6]).astype(int) df[advance_days] ( pd.to_datetime(df[depart_date]) - pd.to_datetime(df[query_date]) ).dt.days df[hour] df[depart_time].str.split(:).str[0].astype(int) def time_bin(h): if h 6: return 0 # 凌晨 elif h 12: return 1 # 上午 elif h 18: return 2 # 下午 else: return 3 # 晚上 df[time_bin] df[hour].map(time_bin) df[route] df[dep_city] _ df[arr_city]advance_days是预定日期和出发日期的差这个特征几乎是航价预测里最重要的变量提前 1 天买和提前 30 天买的价格逻辑完全不同。但要小心它也是数据泄漏高发区后面验证集切分时必须保证同一个depart_date的样本不会被拆到两边。time_bin把 24 小时压成 4 段避免模型把每个小时当成独立类别降低稀疏性。route把出发城市和到达城市拼成一条航线比单独编码两个城市更贴合定价逻辑北京到上海和上海到北京在部分数据集里是两条不同价格曲线concat 出来可以直接喂给类别型特征。处理完这些特征我通常会顺手把query_date和depart_date从训练特征里去掉只保留派生特征。原始日期是 datetime 类型LightGBM 这类树模型不能直接消费而且保留原始日期容易让模型学到“某一天的价格”而不是“某种时间规律下的价格”。3. 航班价格预测的建模主路不是调参玄学是特征和验证的配合3.1 先定预测目标预测离港前N天的价格还是当天价格很多人拿到数据集就开始训练但第一个问题其实是你到底要预测哪个时点的价格回归任务不能只写一句“预测航班价格”你得定义清楚是在查询日预测未来某天的价格还是预测离港当天的最终成交价。这类数据集里一般同时有query_date和depart_date代表用户查询航班时的价格快照而depart_date当天的价格可能包含了临买临走的高溢价。我建议默认把目标设成“在查询日这个时间点预测该航班在出发当天的价格”。也就是说query_date是你能观察到的信息depart_date是未来事件。建模时特征只能取自query_date及之前的信息比如提前购买天数、查询日的星期几但绝不能使用depart_date当天的真实价格作为特征否则就是拿答案做输入。验证集切分也要跟着这个口径走。不能随机打乱后切因为同一条航线在连续日期上的价格高度相关。随机切分会让模型在训练时看到验证集日期的邻居样本相当于偷偷看了未来数据。正确做法是按时序切分from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5) for fold, (tr_idx, va_idx) in enumerate(tscv.split(df)): print(ffold {fold}: train {len(tr_idx)} valid {len(va_idx)})TimeSeriesSplit保证训练集永远在验证集之前前 80% 做训练、后 20% 做验证下一折继续前移。这样做出来的指标才接近真实上线后的表现。如果你拿到的数据集里同一天有多条查询记录还要额外保证同一个depart_date的所有行都落到同一侧否则会出现同一班飞机在训练和验证里各出现一次数据泄漏比随机切分更隐蔽。3.2 用LightGBM做基线为什么比深度学习先跑表格数据预测价格我几乎不会第一轮就上深度学习。航价数据里类别特征多、数值分布复杂、样本量也就几万到几十万LightGBM 在速度和可解释性上都占优。树模型对缺失值天然有处理策略对异常值也相对鲁棒而且能直接输出特征重要性方便我们判断哪一步特征工程真正起了作用。import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit features [weekday, month, is_weekend, advance_days, time_bin, route] X df[features].copy() y df[price].astype(float) for col in [route, weekday, time_bin]: X[col] X[col].astype(category) tscv TimeSeriesSplit(n_splits5) tr_idx, va_idx next(tscv.split(X)) model lgb.LGBMRegressor( n_estimators300, learning_rate0.05, max_depth-1, num_leaves31, min_child_samples20, subsample0.8, colsample_bytree0.8, random_state42, ) model.fit( X.iloc[tr_idx], y.iloc[tr_idx], eval_set[(X.iloc[va_idx], y.iloc[va_idx])], callbacks[lgb.early_stopping(50), lgb.log_evaluation(50)], )参数里max_depth-1表示使用 leaf-wise 生长num_leaves31控制单棵树的复杂度min_child_samples20防止叶子节点样本太少而过拟合subsample0.8和colsample_bytree0.8是行采样和列采样相当于给每棵树加了随机性。early_stopping(50)的意思是验证集连续 50 轮不下降就停止训练这个回调必须加否则 300 棵树跑满一定过拟合。如果训练时发现 LightGBM 报categorical_features相关错误是因为 pandas 的 category 类型没传对。新版 lightgbm 可以直接接收 category 列但需要确保列名在categorical_feature参数里也声明了或者像我上面那样在 DataFrame 里显式转类型。训练完成后看model.feature_importances_如果advance_days和route排在最前说明方向是对的如果month意外地很高要怀疑是不是日期特征里混了未来信息。3.3 价格预测的评估指标MAE、MAPE和“差多少钱能接受”航价预测不能只看一个 R²。价格是绝对数值业务方更关心“预测差了多少元”和“差了多少百分比”。我习惯同时算 MAE、RMSE 和 95 分位绝对误差三者的组合能暴露不同问题import numpy as np from sklearn.metrics import mean_absolute_error, mean_squared_error pred model.predict(X.iloc[va_idx]) y_true y.iloc[va_idx] mae mean_absolute_error(y_true, pred) rmse np.sqrt(mean_squared_error(y_true, pred)) q95 np.percentile(np.abs(y_true - pred), 95) print(fMAE{mae:.2f}, RMSE{rmse:.2f}, q95_abs_err{q95:.2f})MAE 是平均绝对误差能直观告诉业务方平均差多少钱。RMSE 对极端误差做了平方放大如果 RMSE 明显大于 MAE说明存在一批预测差得离谱的样本。q95看最差 5% 的样本误差有多大比如 MAE 是 80 元但 95 分位误差到了 400 元那意味着大部分航班预测还准但小部分冷门航线完全失控。MAPE 也是常用指标但航价数据里低价票占比大一张 50 元的特价票差 20 元就是 40% 的误差而一张 2000 元的高价票差 100 元只有 5%。所以我看 MAPE 时一定会按价格分桶看把真实价格按 0-200、200-500、500-1000、1000 以上分四组分别统计误差低价格段的 MAPE 高并不一定代表模型差可能是价格基数低。如果业务方需要“预测价格能不能用于自动化报价”我一般会把“预测误差在 ±10% 以内”作为可用标准但这条标准只对中等价格区间成立。4. 从单个模型到可交付的预测实例把19个源代码串成一条流水线4.1 常见的19个源码文件是怎么分工的标题里说含 19 个源代码这数字看起来多实际拆开看就是一条完整的从数据到预测的流水线。我见过的惯例是01_load_data.py负责读 CSV02_clean_data.py处理脏值03_feature_engineering.py生成时间特征和航线特征04_split_data.py做时序切分05_train_lgb.py训练模型06_evaluate.py输出指标07_visualize.py画价格分布和误差图。剩下的文件可能是超参搜索、特征重要性分析、单条预测 demo 和结果导出。这不是固定的但 19 个文件一定不会都是模型。刚开始接触项目的人最容易犯的错是每个脚本单独跑一遍最后不知道预测结果从哪来。正确做法是把整个流程按依赖顺序串起来每个脚本只做一件事输出到processed/或output/目录。比如01_load_data.py只输出data_processed.csv04_split_data.py只输出训练验证索引后面的脚本从文件读取而不是重新加载原始数据。如果你拿到手的包里的文件命名和上面的完全不一样也正常。判断一份源代码能不能用我只看两件事第一特征工程里有没有生成advance_days和route第二数据切分有没有按时间顺序而不是随机抽取。这两点只要有一个不对后面模型再漂亮也是假把式。4.2 用Pipeline把预处理和模型装进同一个对象19 个源码文件里的最大陷阱是预处理和模型分离。如果预测时对未来的新航班手动做一遍df[time_bin] df[hour].map(time_bin)很容易漏掉某一步或者漏掉某列填补逻辑。我一般会把整个预处理和模型塞进 sklearn 的 Pipeline保证训练和预测走的是同一套代码。from sklearn.pipeline import Pipeline from sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder, StandardScaler num_features [advance_days, hour] cat_features [route, weekday, time_bin] preprocessor ColumnTransformer( transformers[ (num, StandardScaler(), num_features), (cat, OneHotEncoder(handle_unknownignore), cat_features), ] ) pipe Pipeline(steps[ (prep, preprocessor), (model, lgb.LGBMRegressor(n_estimators200, learning_rate0.05)), ]) pipe.fit(X.iloc[tr_idx], y.iloc[tr_idx])ColumnTransformer对数值列做标准化对类别列做 one-hot。handle_unknownignore很关键它能保证未来出现训练集里没见过的新航线时one-hot 不会报错而是把那一列全部置 0。Pipeline的fit会先跑预处理再训练模型预测时调用pipe.predict也会自动走同样的预处理不需要手动 transform。这里要注意不要把hour拿去StandardScaler因为hour是周期性特征0 点和 23 点的距离在数值上很大但实际只隔 3 小时。如果想让模型理解时间周期性应该拆成sin(hour * 2pi / 24)和cos(hour * 2pi / 24)或者直接让树模型用原始值树模型对单调变换不敏感标准化主要收益在线性模型。在 LightGBM 里hour不标准化也行但放进 Pipeline 能让代码在模型之间切换时少改逻辑。4.3 做预测与回写预测结果要能落到Excel里模型训练完最后一步是把预测结果导出不然前面的工作只能停留在 Jupiter Notebook 里自娱自乐。业务方要的是一张表格航线和出发日期对应什么预测价格。导出 Excel 或 CSV 时注意中文编码直接用utf-8-sig可以避免 Excel 打开乱码。future pd.DataFrame({ route: [北京_上海, 广州_成都], weekday: [3, 5], time_bin: [2, 1], advance_days: [7, 15], hour: [14, 9], }) future[pred_price] pipe.predict(future[features]) future.to_csv(prediction_result.csv, indexFalse, encodingutf-8-sig)pipe.predict需要的future必须包含训练时用到的全部特征列顺序不重要因为 Pipeline 内部按列名取数。输出文件里pred_price是模型预测的数值我习惯同时加上训练集误差的标准差生成一个参考区间。做法是算出训练集残差的标准差然后pred_price ± 1.28 * std作为 80% 置信区间这样导出后才有人敢拿去做决策。很多 19 个源码的项目会有一个predict_oneday.py专门读入一条新航线信息并打印预测价格。这个脚本的价值在于测试接口但你最终还是要写一个批量导出脚本把成千上万条候选航班的预测结果一次性生成。在导出前务必检查route是否存在于训练集如果不存在模型只能靠其他特征硬猜价格误差会明显加大。5. 航价预测项目的避坑清单数据泄漏、时区、伪加密和黑匣子5.1 数据泄漏把整段航班历史直接切train/test是经典翻车现象验证集上 MAE 只有 30 元模型看起来非常完美一上线预测未来航班就变成 150 元。原因切分时用了随机打乱同一个depart_date的航班一部分落在训练集一部分落在验证集。模型在训练时见过同一个航班的邻近日期的价格验证时等于开卷考试。解决改用TimeSeriesSplit并且按depart_date去重后切分。有一个土办法先把所有航班按出发日期排序取前 80% 的日期区间作为训练后 20% 的日期区间作为验证保证日期之间没有任何重叠。注意如果数据里每天有多条查询记录排序时不要把同一天的记录切到两边要按日期整体切。5.2 时区和日期边界凌晨航班的价格特征错位现象预测凌晨 00:30 起飞的航班时误差比白天航班大很多。原因数据里depart_time是字符串depart_date是日期拼接时直接用字符串相加2024-01-01 00:30被 pandas 解析成了 2024-01-01 的 00:30但航班实际属于 1 月 1 日的凌晨查询日可能是前一天晚上。如果出发地和目的地跨越时区更复杂日期差会整体错位。解决在构造depart_datetime时显式指定时区或者统一按东八区处理再计算advance_days时用同一个时区的query_date。另外凌晨航班最好单独做一个is_red_eye特征hour 5置 1模型会更容易学出红眼航班的低价规律。5.3 zip伪加密网上说的密码移除大多不用做现象解压时报错提示输入密码和一些平台下载页面说“免密”不符。原因这个 zip 文件被修改过加密标志位数据本身没加密解压逻辑只是被伪加密标志卡住。网上大量“zip密码移除”工具就是针对这种情况真正加密的文件靠暴力破解几乎不可能因为 AES 加密的 zip 根本没有通用后悔药。解决先用zipinfo -v看加密方式如果显示encryption: none直接用 7-Zip 或 Pythonzipfile读取很多时候能直接读出文件列表。如果unzip仍然要密码可以试试7z x -p123随便填一个密码伪加密包可能直接就解出来了。记住一个原则来源页面没给密码优先怀疑伪加密来源页面给了密码老老实实输密码别花时间研究破解。5.4 价格离群值特价机票和故障数据把模型带偏现象RMSE 是 MAE 的 4 倍以上验证集散点图右上角飘着几个点。原因数据里混了 0 元故障价、1 元促销价或者价格单位出错比如把 520 写成 5200。这些离群值数量少但平方误差被极端值放大模型为了迁就它们反而牺牲了主体样本。解决先按航线分组做 IQR 过滤price超过Q3 1.5 * IQR的样本删掉或标记再训练模型。过滤时保留一份完整备份因为后面判断“特价区间”时这些离群值其实也是有效业务信号。更好的做法是对price取对数训练把极端价格压缩预测完再exp回来但这样 MAE 的语义会变成相对误差业务解释起来更费劲。6. 验证误差分布把预测结果变成可用的业务判断6.1 按航线和时段看误差而不是只看一个MAE训练结束后我会把验证集预测结果合并回原数据按route和time_bin分组看误差中位数。如果某条航线误差明显高于其他航线说明这条线的价格波动受突发事件影响大模型缺少对应特征。如果凌晨时段误差大大概率是红眼航班样本少且价格变化规律和白天不同。这个分组的输出直接决定下一轮迭代方向比反复调num_leaves有用得多。6.2 给预测价格加一个残差区间业务方不关心你的 AUC 或 R²他们只想知道“这个价格我敢不敢用”。我习惯把验证集残差的标准差resid_std算出来然后给每条预测结果加一个pred ± 1.28 * resid_std代表 80% 区间。如果下一轮能拿到真实价格这个区间的覆盖率应该在 80% 左右这是验证里最不依赖主观感觉的检测方法。区间过宽说明模型方差大需要加特征区间单侧偏说明模型有系统性高估或低估需要检查是不是时间切分出了问题。我自己每次跑完航价项目都会把误差分布和区间预测存成一个 CSV下一次迭代直接对比。与其上来就换模型不如先看误差集中在哪里。这套动作做到位19 个源代码才真正变成你自己能复用的预测实例。希望帮到你。本文还有配套的精品资源点击获取