
简介这份资源是一篇基于机器学习的机票价格预测研究论文面向计算机、数据分析相关专业的在校师生、毕业生及算法初学者可用于毕业设计、课程设计参考与项目思路借鉴。论文围绕民航市场机票价格波动问题系统讲解了数据获取、特征工程、模型训练与融合的完整流程重点采用随机森林与XGBoost算法构建预测模型并尝试对经典模型进行改进以提升预测精度。资源包内含1个docx文档压缩包约78KB篇幅完整、写作规范、逻辑紧密适合作为论文写作与项目开发的参考范本。文中详细介绍了通过中国航信API与爬虫获取数据、应对IP限制与验证码的技术策略、基于业务理解的特征选取以及MAE评价与五折交叉验证等实验细节能帮助读者理解回归预测的完整链路与业务落地思路。目前已有113人学习建议下载后仔细研读切勿完全照抄。1. 机票价格预测这个毕设为什么值得用机器学习认真做一遍同一趟航班提前三周看是 860提前一周变成 1240起飞前两天又掉回 990。这种反复横跳不是航司在随机调价背后是舱位库存、竞品价格、节假日需求、上座率预测共同作用的结果。机票价格预测这个题目本质上是把「一条航线的历史报价 时间特征 市场特征」喂给回归模型让它输出未来某天的价格估计。它比房价预测、销量预测更适合做课设和毕设因为数据获取路径清晰、特征工程有发挥空间、模型对比结论明显而且随机森林和 XGBoost 这类传统机器学习算法在这个场景里表现稳定不需要 GPU 也能跑出能写进论文的结果。适合谁做机器学习入门阶段、需要一份完整课设或毕设、手上有 Python 基础但没做过端到端项目的人。读完你应该能自己搭出一条从数据构造、特征工程、随机森林基线、XGBoost 调参到结果验证的完整链路并且知道哪些地方最容易翻车。2. 先想清楚预测目标回归还是分类标签怎么定2.1 把「预测价格」翻译成模型能学的标签很多人一上来就model.fit(X, y)y 直接塞原始票价结果模型学出来一堆噪声。机票价格预测的标签定义决定了整个项目的上限。常见做法有三种第一种是直接回归票价数值标签就是price损失函数用 MSE 或 MAE。优点是直观缺点是票价跨度大几百到几千模型容易被高价样本带偏。第二种是预测价格变化方向标签是二分类will_rise未来 N 天是否涨价。这种适合做「该不该现在买」的决策辅助但丢失了幅度信息。第三种是预测相对涨跌幅标签是(future_price - current_price) / current_price。这个做法在论文里比较讨巧因为做了归一化模型更稳。我一般会主做回归、辅做分类论文里两个结果都放对比着写。回归看 MAE 和 R²分类看 AUC 和 F1这样工作量够结论也立体。2.2 时间窗口的设定提前几天预测第几天的价格这是最容易被忽略但最影响结果的一步。你需要明确用截止到 T 时刻的信息预测 Tk 时刻的价格。k 取多少常见做法是 k1预测明天、k3、k7。如果 k 太小价格变化不明显模型学不到东西k 太大特征和标签之间的关联被稀释MAE 会很难看。课设里建议 k3 或 k7论文里可以做成多组实验对比说明窗口选择对误差的影响。提示划分训练集和测试集时绝对不能随机打乱。机票价格是时间序列随机划分会导致未来信息泄漏到训练集测试集 MAE 会虚低答辩时被问一句就露馅。正确做法是按时间切分比如前 80% 的时间段做训练后 20% 做测试。2.3 评价指标怎么选才不会被质疑回归任务常用 MAE、RMSE、MAPE、R²。机票价格预测里MAPE平均绝对百分比误差最直观因为它告诉你「平均预测偏差是票价的百分之几」。如果 MAPE 在 8% 以内论文里可以写「具备一定实用参考价值」。但要注意票价接近 0 的样本会让 MAPE 爆炸。清洗数据时把异常低价比如 50 元以下的占位价格剔掉或者在计算 MAPE 时加一个最小价格阈值。import numpy as np def mape(y_true, y_pred, threshold100): 计算MAPE过滤掉真实价格过低的样本避免除零和爆炸 mask y_true threshold return np.mean(np.abs((y_true[mask] - y_pred[mask]) / y_true[mask])) * 100 def evaluate(y_true, y_pred): mae np.mean(np.abs(y_true - y_pred)) rmse np.sqrt(np.mean((y_true - y_pred) ** 2)) r2 1 - np.sum((y_true - y_pred) ** 2) / np.sum((y_true - np.mean(y_true)) ** 2) return {MAE: round(mae, 2), RMSE: round(rmse, 2), R2: round(r2, 4), MAPE: round(mape(y_true, y_pred), 2)}这段代码定义了四个指标的计算逻辑。threshold100是为了过滤掉极端低价样本实际用的时候根据你的数据分布调整一般取票价的 10% 分位数比较合理。evaluate函数返回字典方便后面随机森林和 XGBoost 共用同一套评价标准。3. 特征工程机票价格预测的胜负手在这里3.1 时间特征不只是把日期拆成年月日机票价格的周期性非常强。周中便宜、周五周日贵这是常识。但模型不会自己知道「周五」和「周日」比「周二」贵你得把这种信息编码进去。基础时间特征包括星期几1-7、月份1-12、是否周末、是否节假日前后。进阶一点的做法是加「提前天数」——也就是当前日期距离出发日期还有多少天。这个特征在机票场景里极其重要因为航司的定价策略和提前天数强相关。import pandas as pd def build_time_features(df, date_colquery_date, depart_coldepart_date): 从查询日期和出发日期构造时间特征 df df.copy() df[date_col] pd.to_datetime(df[date_col]) df[depart_col] pd.to_datetime(df[depart_col]) # 查询日期的日历特征 df[day_of_week] df[date_col].dt.dayofweek # 0周一, 6周日 df[is_weekend] (df[day_of_week] 5).astype(int) df[month] df[date_col].dt.month df[day_of_month] df[date_col].dt.day # 提前天数核心特征 df[days_ahead] (df[depart_col] - df[date_col]).dt.days # 出发日期的星期几影响需求 df[depart_dow] df[depart_col].dt.dayofweek return dfdays_ahead是这条链路里最重要的特征之一。depart_dow单独拎出来是因为出发日是周五还是周二需求差异很大价格自然不同。is_weekend用 0/1 编码比直接丢星期几给树模型更干净。3.2 航线与航司特征类别变量怎么处理才不翻车航线和航司是典型的类别特征。一条航线可能有几十个航司在飞直接 One-Hot 会导致维度爆炸。树模型对类别编码不敏感但也不能乱来。常见做法有三种Label Encoding给每个航司一个整数编号、Target Encoding用该航司的平均价格替换、Frequency Encoding用该航司出现次数替换。课设里我一般用 Label Encoding 树模型简单且效果不差。如果要做论文对比可以三种都试用交叉验证选最好的。from sklearn.preprocessing import LabelEncoder def encode_categorical(df, cat_cols): 对类别特征做Label Encoding保存编码器方便后续推理 encoders {} df df.copy() for col in cat_cols: le LabelEncoder() df[col _encoded] le.fit_transform(df[col].astype(str)) encoders[col] le return df, encoders # 使用示例 cat_features [airline, origin, destination, cabin_class] df, encoders encode_categorical(df, cat_features)encoders字典一定要保存下来。训练时对训练集 fit测试集和后续新数据只能用 transform不能重新 fit否则类别编号对不上模型输出全是错的。这是血泪经验很多人训练完模型部署时才发现编码不一致。3.3 价格滞后特征用历史价格预测未来价格如果数据里同一条航线有连续多天的报价记录那滞后价格特征lag features是提升效果最明显的。比如「该航线 1 天前的价格」「3 天前的价格」「7 天前的均价」。但这里有个坑滞后特征只能在时间序列连续的情况下用。如果你的数据是稀疏采样的比如每隔三天才有一条记录lag 特征会大量缺失填充策略就变得很关键。我一般用前向填充ffill然后加一个「是否填充」的标记列让模型自己学。def add_lag_features(df, group_cols, price_colprice, lags[1, 3, 7]): 按航线和出发日期分组构造价格滞后特征 df df.sort_values(query_date) for lag in lags: df[fprice_lag_{lag}] df.groupby(group_cols)[price_col].shift(lag) # 前向填充缺失值 lag_cols [fprice_lag_{l} for l in lags] df[lag_cols] df.groupby(group_cols)[lag_cols].ffill() return dfgroup_cols一般是[origin, destination, depart_date]确保是同一条航线同一天出发的连续报价。shift(lag)取的是前 lag 条记录的价格不是前 lag 天的价格如果采样不规律要注意换算。4. 随机森林基线先把一条能跑的链路搭起来4.1 为什么先跑随机森林而不是直接上 XGBoost随机森林回归算法对新手最友好的地方在于几乎不需要调参就能出一个合理结果对缺失值和异常值有一定容忍度而且特征重要性可以直接输出方便你写论文里的特征分析章节。我一般的工作流是随机森林跑通全流程 → 拿到 baseline MAE → 再上 XGBoost 对比提升。这样论文里「模型对比」章节有东西写而且能说明「为什么选 XGBoost」——因为它在相同特征下 MAE 更低。from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import mean_absolute_error # 假设 X_train, y_train, X_test, y_test 已经准备好 rf RandomForestRegressor( n_estimators300, # 树的数量300-500之间通常够用 max_depth15, # 限制深度防止过拟合 min_samples_leaf5, # 叶子最少样本数越大越保守 random_state42, n_jobs-1 # 用满所有CPU核心 ) rf.fit(X_train, y_train) y_pred_rf rf.predict(X_test) print(RF MAE:, mean_absolute_error(y_test, y_pred_rf))n_estimators从 100 加到 300MAE 通常会降但边际递减。max_depth不设的话树会一直长训练集 R² 接近 1 但测试集很差。min_samples_leaf5是我常用的保守值数据量小的时候可以调到 3数据量大可以调到 10。4.2 特征重要性怎么读才有意义随机森林的feature_importances_输出的是每个特征对降低不纯度的贡献。但要注意这个值对高基数类别特征比如航线编号有偏好不能完全当真。import pandas as pd importance pd.DataFrame({ feature: X_train.columns, importance: rf.feature_importances_ }).sort_values(importance, ascendingFalse) print(importance.head(15))如果days_ahead排在第一说明提前天数是核心驱动因素这符合业务直觉。如果某个编码后的航线 ID 排得很高要警惕是不是数据泄漏——比如某些航线的价格恰好和标签高度相关但实际预测时这个特征不可用。4.3 用时间序列交叉验证代替单次划分单次 train/test split 的结果波动很大论文里只报一个数字容易被质疑。用TimeSeriesSplit做 5 折交叉验证报告均值和标准差说服力强很多。from sklearn.model_selection import TimeSeriesSplit import numpy as np tscv TimeSeriesSplit(n_splits5) mae_scores [] for train_idx, val_idx in tscv.split(X_train): X_tr, X_val X_train.iloc[train_idx], X_train.iloc[val_idx] y_tr, y_val y_train.iloc[train_idx], y_train.iloc[val_idx] rf_cv RandomForestRegressor(n_estimators200, max_depth15, random_state42, n_jobs-1) rf_cv.fit(X_tr, y_tr) mae_scores.append(mean_absolute_error(y_val, rf_cv.predict(X_val))) print(fCV MAE: {np.mean(mae_scores):.2f} ± {np.std(mae_scores):.2f})TimeSeriesSplit保证每一折的训练集都在验证集之前不会出现未来数据泄漏。n_splits5是常用值数据量少可以降到 3。5. XGBoost 调参与避坑从能跑到跑好5.1 XGBoost 回归模型的核心参数怎么设XGBoost 在机票价格预测里通常比随机森林低 5%-15% 的 MAE但它参数多乱设反而更差。我一般按这个顺序调先定n_estimators和learning_rate。learning_rate0.05配n_estimators500是稳妥起点。然后调max_depth机票数据一般 4-8 层就够太深容易过拟合。再调subsample和colsample_bytree0.8 左右通常不错。最后加正则reg_alpha和reg_lambda。import xgboost as xgb xgb_model xgb.XGBRegressor( n_estimators500, learning_rate0.05, max_depth6, subsample0.8, colsample_bytree0.8, reg_alpha0.1, # L1正则 reg_lambda1.0, # L2正则 random_state42, n_jobs-1, early_stopping_rounds50 # 验证集不提升就停 ) xgb_model.fit( X_train, y_train, eval_set[(X_test, y_test)], verboseFalse ) y_pred_xgb xgb_model.predict(X_test) print(XGB MAE:, mean_absolute_error(y_test, y_pred_xgb))early_stopping_rounds50是后悔药如果验证集 50 轮不提升就自动停防止白跑。reg_alpha和reg_lambda在特征多的时候尤其重要能压住过拟合。5.2 用 RandomizedSearchCV 自动搜参的实操写法手动调参效率低RandomizedSearchCV比GridSearchCV快得多适合课设时间紧的情况。关键是参数分布要设合理别在没意义的范围里搜。from sklearn.model_selection import RandomizedSearchCV param_dist { n_estimators: [300, 500, 800], max_depth: [4, 6, 8, 10], learning_rate: [0.01, 0.05, 0.1], subsample: [0.7, 0.8, 0.9], colsample_bytree: [0.6, 0.8, 1.0], reg_alpha: [0, 0.1, 1.0], reg_lambda: [0.5, 1.0, 2.0] } search RandomizedSearchCV( xgb.XGBRegressor(random_state42, n_jobs-1), param_distributionsparam_dist, n_iter40, # 随机采样40组 scoringneg_mean_absolute_error, cv3, random_state42, n_jobs-1, verbose1 ) search.fit(X_train, y_train) print(Best params:, search.best_params_) print(Best MAE:, -search.best_score_)n_iter40是时间和效果的折中参数组合空间大的话可以加到 60。cv3在数据量不大时够用数据多可以上 5。scoring用负 MAE 是因为 sklearn 的交叉验证默认越大越好MAE 越小越好所以取负。5.3 避坑机票价格预测里最容易翻车的 5 个地方现象一测试集 MAE 低得离谱论文里不敢写。原因随机划分数据导致时间泄漏未来价格信息混进了训练集。 解决改用TimeSeriesSplit或按时间切分确保训练集时间早于测试集。现象二模型在训练集上 R²0.99测试集 R²0.3。原因树太深或特征太多模型把训练集的噪声也学了。 解决降max_depth到 6 以下加min_samples_leaf开reg_alpha和reg_lambda。现象三类别编码后预测结果完全不对。原因训练集和测试集用了不同的 LabelEncoder同一航司编号不一致。 解决fit 只在训练集做测试集用同一个 encoder transform保存 encoder 到文件。现象四滞后特征大量缺失填充后模型效果反而变差。原因前向填充引入了未来信息或者填充值本身有偏。 解决加「是否填充」标记列或者改用中位数填充并做缺失指示。现象五MAPE 算出几万论文里没法解释。原因真实价格里有接近 0 的异常值除法爆炸。 解决清洗掉低于阈值如 100 元的样本或在 MAPE 计算时加 mask。6. 让结果站得住验证方法与论文里能写的对比实验6.1 三个必须做的对比实验论文里只报一个模型的结果会被追问「为什么不用别的」。我一般做三组对比第一组线性回归 vs 随机森林 vs XGBoost。证明非线性模型确实更好这是最基本的。第二组不加滞后特征 vs 加滞后特征。证明特征工程有效这是工作量体现。第三组默认参数 vs 调参后。证明调参有收益这是技术深度体现。每组都用相同的训练/测试划分和评价指标结果放一张表里一目了然。实验组模型MAERMSEMAPER²基线线性回归186.3245.715.2%0.42基线随机森林132.5178.410.8%0.67基线XGBoost118.7162.19.6%0.73特征XGBoost滞后102.4141.38.3%0.79调参XGBoost调参95.8133.67.7%0.82这张表里的数字是示意实际跑出来是多少就写多少。关键是趋势要合理XGBoost 优于随机森林加特征有提升调参有收益。6.2 残差分析找出模型在哪里翻车光看 MAE 不够还要看残差分布。如果残差在某个价格区间特别大说明模型在那个区间有问题。import matplotlib.pyplot as plt residuals y_test - y_pred_xgb fig, axes plt.subplots(1, 2, figsize(12, 4)) axes[0].scatter(y_pred_xgb, residuals, alpha0.3, s10) axes[0].axhline(0, colorred, linestyle--) axes[0].set_xlabel(Predicted Price) axes[0].set_ylabel(Residual) axes[0].set_title(Residual vs Predicted) axes[1].hist(residuals, bins50, edgecolorblack) axes[1].set_xlabel(Residual) axes[1].set_title(Residual Distribution) plt.tight_layout() plt.show()如果残差图呈现喇叭形预测值越大残差越大说明模型对高价样本欠拟合可以考虑对价格取对数后再训练。如果残差直方图明显偏斜说明有系统性偏差检查是不是漏了关键特征。6.3 一个能写进论文的进阶技巧分位数回归看价格区间普通回归只给一个点估计但实际业务里「明天价格大概在 900 到 1100 之间」比「明天价格是 1000」更有用。XGBoost 支持分位数回归可以输出 10%、50%、90% 分位数的预测。# 训练三个分位数模型 quantiles [0.1, 0.5, 0.9] models {} for q in quantiles: m xgb.XGBRegressor( objectivereg:quantileerror, quantile_alphaq, n_estimators500, learning_rate0.05, max_depth6, random_state42, n_jobs-1 ) m.fit(X_train, y_train) models[q] m # 预测区间 lower models[0.1].predict(X_test) median models[0.5].predict(X_test) upper models[0.9].predict(X_test) # 覆盖率真实值落在区间内的比例 coverage np.mean((y_test lower) (y_test upper)) print(f80% prediction interval coverage: {coverage:.2%})quantile_alpha设 0.1 和 0.9 就是 80% 预测区间。coverage应该接近 80%如果只有 60% 说明区间太窄模型低估了不确定性。这个技巧在论文里是加分项能体现你对业务需求的理解。我自己做这类项目最大的习惯是先把随机森林跑通确认整条链路没有泄漏再上 XGBoost 调参。每次改特征或参数都固定随机种子跑三遍取平均避免被单次波动骗了。论文里的每个数字都要能追溯到某次具体实验别凭印象写。希望帮到你。本文还有配套的精品资源点击获取