如果你是做毕设,方向选了数据分析加机器学习,又不想一上来就碰深度学习那种动辄几万参数、调参调到头皮发麻的模型,那“基于随机森林的飞机旅客满意度数据分析与可视化”这个选题,性价比确实高。我去年带过几届学生做类似题目,最后发现这类项目真正拉开差距的地方,不在模型本身,而在三件事:数据预处理干不干净、特征解释深不深入、可视化有没有能搬上答辩现场的展示效果。这篇文章我会把一个完整可复现的项目链路拆开讲,从数据集长什么样开始,到随机森林怎么调参,再到可视化大屏怎么做,最后落到论文整合和答辩准备上,尽量把关键步骤和跳过会踩坑的地方都说到位。
1. 为什么用随机森林来啃旅客满意度这个题目
1.1 这个题目到底在解决什么问题
说白了,航空公司关心的是这么一件事:旅客为什么会觉得服务不好?是座位太挤,还是网上值机流程太繁琐,又或者是行李处理太慢?如果能把不满意的那批旅客背后共同的特征找出来,航司就能按优先级去改服务,而不是眉毛胡子一把抓。
机器学习在这里的角色,就是根据历史问卷数据,训练一个分类模型,输入是旅客的年龄、舱位、飞行距离、各项服务评分等维度,输出是“满意”还是“不满意”。模型训练完之后,我们不只是要一个预测结果,更想要的是模型给出的“特征重要性”——也就是哪些因素最影响满意度。这恰好是随机森林的强项。
整个项目的输出通常是三样东西:一个能跑通的预测模型、一份特征重要性分析报告、一套可视化展示页面。论文和答辩PPT都围绕这三样展开,逻辑非常顺。
1.2 数据集的基本盘和核心字段
做这个题目,最常见的公开数据集源于某平台上的航空公司旅客满意度问卷数据,我用下来的感受是:它就像是为毕业设计量身定做的一样,规模不大不小,字段覆盖全面,脏数据也够练手。最常用的版本大约有2.6万条记录,20多个字段,标签列是满意度,只有两类:满意、中性或不满意。
核心特征可以分成三组:
- 旅客基础属性:性别、年龄、客户类型(忠诚旅客还是非忠诚)、旅行类型(商务出行还是个人出行)、舱位等级(经济舱、舒适经济舱、商务舱)。
- 航班相关:飞行距离、出发延误分钟数、到达延误分钟数。
- 服务体验评分:机上WiFi、在线预订便利度、登机服务、座位舒适度、机上娱乐、机上餐饮、行李处理、值机服务、机上服务、清洁程度等,大部分是1到5分的打分项。
1.3 选随机森林而不是深度学习的三点理由
我见过一些学生上来就说“毕设要用深度学习”,结果拿这个数据跑了个多层神经网络,最后的精度和随机森林差不多,但解释性差了一大截。这个数据集用深度学习的收益非常有限,原因有三点:
第一,样本量只有2万多条,深度学习在这个规模下优势很小,反而容易过拟合。第二,这个数据集的字段大多是问卷打分和类别变量,显式特征已经比较充分,不需要神经网络去自动提取复杂结构。第三,也是最关键的,毕业设计答辩时,随机森林可以直接输出特征重要性,你可以指着图表告诉评委“在线预订方便度是影响满意度的第三大因素”,而神经网络的权重解释起来绕一大圈还说不清楚。
所以结论是:用随机森林做主体模型是合理的,深度学习顶多作为对比实验锦上添花。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据的“脏乱差”处理:决定模型上限的那一步
2.1 缺失值:到达延误那几百条空缺别急着删
这个数据集里最容易踩的坑,是“到达延误”字段存在缺失。具体数量每个版本略有差异,我遇到过300多条、也有接近400条的情况。很多新手一上来就 dropna(),把缺失值所在的行全删掉,这其实很亏。因为缺失到达延误的行,出发延误可能是完整的,而且这两个延误时间之间存在很强的相关性——航班出发晚了,到达大概率也晚。你把这行删了,等于白白丢了其他十几个字段的有效信息。
我的处理思路是分两步:
python复制import pandas as pd
df = pd.read_csv('airline_passenger_satisfaction.csv')
# 第一步:用出发延误填充同一条记录的到达延误
mask = df['Arrival Delay in Minutes'].isnull()
df.loc[mask, 'Arrival Delay in Minutes'] = df.loc[mask, 'Departure Delay in Minutes']
# 第二步:仍然缺失的,用到达延误的中位数兜底
remain_missing = df['Arrival Delay in Minutes'].isnull()
df.loc[remain_missing, 'Arrival Delay in Minutes'] = df['Arrival Delay in Minutes'].median()
这里有个细节值得写进论文:为什么不用均值兜底?因为延误数据的分布是严重右偏的——大部分航班延误时间很短,少数航班延误很久,均值会被极端值带高。用中位数对偏态分布更鲁棒。我当时实测过,用中位数填充后模型AUC比用均值填充略高一点,虽然差距不大,但把这个理由写进论文会显得你考虑得很细致。
2.2 类别字段的编码与顺序问题
数据集里有一批字符串类型的类别特征:性别、客户类型、旅行类型、舱位等级。建模前必须转成数值。常见的做法有两种:LabelEncoder 适合二分类变量,比如性别;OrdinalEncoder 适合有顺序关系的变量,比如舱位等级。
这里我想专门说一下舱位等级的处理。有的资料里默认用 OneHotEncoder 把经济舱、舒适经济舱、商务舱各拆成一列,这样不是不行,但会带来两个问题:一是特征维度增加,随机森林虽然能容忍,但解释起来更碎;二是丢失了“商务舱高于舒适经济舱高于经济舱”的顺序信息。舱位本质上是有序变量,转换时保留顺序更合理。我的做法是手动映射成0、1、2,让模型直接感知到这个顺序关系。
python复制class_map = {'Eco': 0, 'Eco Plus': 1, 'Business': 2}
df['Class_enc'] = df['Class'].map(class_map)
年龄和飞行距离这两个数值字段,我建议先不做标准化。随机森林是基于树模型的,完全不关心特征的量纲,你在论文里可以明确写“树模型不需要标准化”,这也是省事又正确的地方。
2.3 类别不平衡检查与训练集划分
这个数据集的标签分布在不同版本里不太一样,有的版本满意和不满意比例接近55开,有的版本不满意占比略高。但总体看,不算严重失衡,不需要一上来就做SMOTE过采样。
我的习惯是先做一次基础训练,然后看混淆矩阵里“满意”和“不满意”两类各自的召回率。如果有一类明显被压制,再考虑用 class_weight='balanced' 给少数类加权;如果加权之后整体AUC没跌、少数类召回率明显涨了,就用加权方案,并在论文里做一组对比。对2万多条数据来说,随机森林加 class_weight 就足够了,引入SMOTE会增加复现复杂度,而且在这个数据集上提升有限。
划分训练集和测试集的时候,有一个参数容易漏掉,就是 stratify。它能让划分前后标签分布保持一致,避免出现测试集里恰好全是满意样本这种情况。代码很简单:
python复制from sklearn.model_selection import train_test_split
X = df.drop(columns=['Satisfaction'])
y = df['Satisfaction'].map({'satisfied': 1, 'neutral or dissatisfied': 0})
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
3. 随机森林搭建、调参与评估
3.1 基线模型先跑通再说
做调参之前,务必先用默认参数把整条链路跑通。这一步的目的不是追求最优效果,而是确认前面的数据预处理没有bug,评估代码能正常输出指标。我见过太多同学数据没处理完就开始调参,最后出来的结果忽高忽低,根本不知道问题出在数据、模型还是评估代码上。
默认随机森林在 n_jobs=-1 的情况下跑这个数据集,也就是几秒钟的事。基线输出通常已经比较可观:准确率能到0.94左右,AUC也能到0.97上下。这个成绩意味着这个数据集本身区分度很好,后续调参的空间可能只有零点几个百分点,不要期待从0.94调到0.99这种神话。
3.2 三步调参法,避免网格搜索变成时间黑洞
随机森林的超参数不少,但如果按顺序来,可以避免组合爆炸。我的顺序是:先调 max_depth 和 max_features,再调 min_samples_leaf,最后调 n_estimators。
第一步限定树的深度。默认的 max_depth=None 意味着树可以无限深,在特征维度不多的情况下过拟合风险没想象中大,但为了保险还是试一下深度10到20之间的表现。第二步调整每次分裂考虑的候选特征数,默认 sqrt 是经验值,有时候不敏感,但值得验证。第三步调叶子节点最小样本数,这也是防止过拟合的有效手段。
python复制from sklearn.model_selection import GridSearchCV
from sklearn.ensemble import RandomForestClassifier
param_grid = {
'max_depth': [10, 15, 20],
'max_features': ['sqrt', 'log2'],
'min_samples_leaf': [2, 5, 10],
'n_estimators': [200, 500]
}
rf = RandomForestClassifier(random_state=42, n_jobs=-1)
grid = GridSearchCV(
rf, param_grid,
cv=5,
scoring='roc_auc',
n_jobs=-1,
verbose=1
)
grid.fit(X_train, y_train)
print(grid.best_params_)
我把这个搜索空间算了一下:3乘2乘3乘2,共36组参数,每组再做5折交叉验证,也就是180次模型拟合。这个数据集规模小,总耗时大概5到10分钟,在你电脑上如果开了多核并行,属于可以接受的量级。但如果你把 n_estimators 加到1000,max_depth 再给20个候选,网格搜索就会变成时间黑洞,等待期间你什么都干不了。所以我的建议是,网格搜索之前先想想“这个参数真的值得试吗”,而不是把所有参数都堆上去。
另外提醒一点:网格搜索的评估指标建议用 roc_auc,不要只看准确率。因为如果两类样本比例稍微失衡,准确率会偏向多数类,而AUC能更客观地反映模型排序能力。这个数据集准确率普遍很高,AUC的差异更能区分模型好坏。
3.3 评估别只看准确率:混淆矩阵和AUC才是重点
模型训练完后,测试集上要输出四样东西:准确率、分类报告(精确率、召回率、F1)、混淆矩阵和ROC曲线的AUC值。我在教学时反复强调:毕设论文里不要只放一个准确率,显得非常单薄。放一张混淆矩阵热力图,再放一条ROC曲线,视觉上充实,学术上也站得住。
python复制import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import confusion_matrix, roc_auc_score, roc_curve
y_pred = grid.predict(X_test)
y_prob = grid.predict_proba(X_test)[:, 1]
cm = confusion_matrix(y_test, y_pred)
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.show()
fpr, tpr, _ = roc_curve(y_test, y_prob)
plt.plot(fpr, tpr, label=f'AUC = {roc_auc_score(y_test, y_prob):.4f}')
plt.plot([0, 1], [0, 1], 'r--')
plt.legend()
plt.show()
还有一个小细节:输出混淆矩阵的时候,建议把每一行的数字除以该行实际总数,换算成百分比,再和原始计数并排展示。因为原始计数受类别分布影响,读者看不出召回率,加了百分比一眼就能看出模型在哪个类别上表现不佳。
4. 特征重要性:从模型结果反推航司应该改哪里
4.1 Gini重要性怎么看,不能只盯排名
随机森林训练完成后,feature_importances_ 能直接给出每个特征对分类的贡献度。这个重要性的计算原理是:每一棵决策树在分裂时,都会计算当前节点的基尼不纯度下降值,把所有节点的不纯度下降按特征累加起来,再对所有树取平均,最后做归一化。通俗点说,就是看“哪个特征被用来分裂时,能让满意和不满意的旅客被分得最开”。
在这个数据集上跑下来,排名靠前的位置通常被几类特征占据:网上值机相关体验、在线预订便利度、座位舒适度、行李处理、餐饮服务等。但我要提醒你:别直接把 feature_importances_ 的排序表往论文里一贴就完事。这个排序是有偏差的,它偏向数值型特征和高基数特征。也就是说,数值类的打分项天然容易排得靠前,这不是因为它业务上更重要,而是因为数值特征在树的分裂中更容易被选中。所以论文里可以放这张图,但解读时要有分寸。
4.2 用排列重要性做交叉验证
如果你想对特征重要性排序更有信心,可以做一次排列重要性验证,英文叫Permutation Importance。它的原理非常直观:把某一个特征在测试集上的取值顺序随机打乱,破坏特征与标签的关系,再让模型预测,观察AUC掉多少。掉得越多,说明这个特征越重要。这和Gini重要性的角度不同,更贴近真实预测意义上的重要性。
python复制from sklearn.inspection import permutation_importance
result = permutation_importance(
grid.best_estimator_, X_test, y_test,
n_repeats=10, random_state=42, scoring='roc_auc'
)
sorted_idx = result.importances_mean.argsort()[::-1]
排列重要性的输出里通常还带标准差,可以做成带误差棒的水平条形图,论文里放这张图的档次感比单列排名高不少。我实测下来,这两种方法在这个数据集上选出的Top5特征基本重合,但个别中段特征的排名会有差异,这个差异本身就可以写进论文作为讨论点。
4.3 特征重要性的业务解读与改进建议
拿到特征重要性之后,最后一步是把技术结果翻译成业务建议。拿特征重要性 Top 1 的网上值机服务来说,业务解读是:这个环节的体验每提升1分,旅客从不满意转向满意的概率变化最大。对应的改进行动可以是优化在线值机系统流程、减少页面跳转、增加座位偏好记忆等。再比如行李处理排进Top5,说明旅客对行李托运和到达后提取的体验非常敏感,那航司在行李追踪、延误告知上做投入,逻辑上就是站得住的。
这一段建议单独写成一个小节,标题类似“特征重要性分析与业务建议”。论文需要体现“数据分析能指导实际决策”的价值,这部分就是最能打的证据。
5. 可视化落地:从静态图表到答辩级大屏
5.1 先做静态图:能说明问题的图表最有用
可视化这一步,很多同学容易走两个极端:要么就画两张最基础的柱状图交差,要么一上来就想搞炫酷的动态交互大屏结果卡在集成上。我的建议是分两步走:先用matplotlib和seaborn把核心结论画成静态图,用于论文插图;再基于这些结论搭一套交互式大屏,用于答辩演示。
静态图里,我认为最有价值的几张是:
- 满意度标签分布饼图或条形图,展示类别不平衡情况。
- 不同舱位等级下满意度比例的堆叠条形图,视觉上能明显看出商务舱满意比例远高于经济舱。
- 各服务评分维度的均值对比图,满意和不满意的旅客在各维度上的打分差异一目了然。
- 特征重要性Top10的水平条形图。
- 混淆矩阵热力图和ROC曲线。
python复制import matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(10, 6))
sns.barplot(
x='Value', y='Feature',
data=importance_top10,
palette='viridis'
)
plt.xlabel('Importance')
plt.ylabel('Feature')
plt.title('Top 10 Feature Importance')
plt.tight_layout()
plt.savefig('feature_importance.png', dpi=300)
注意保存图片时把 dpi 调高一点,300dpi以上,论文里才够清晰。配色上建议先试试 seaborn 自带色板,避免用默认的紫红色,那个打印出来很显脏。
5.2 用pyecharts搭交互式大屏
到了答辩展示环节,交互式大屏的视觉冲击力是静态图没法比的。我推荐的方案是 pyecharts 生成图表,再嵌到一个HTML页面里组合成大屏。这个方案的优点在于:纯Python就能完成,不需要额外学前端框架,产出是HTML文件,答辩时用浏览器打开即可,非常稳定。
大屏的核心图表我建议至少包含四块:左上角放满意度整体占比的环图,右上角放特征重要性Top10条形图,中间区域放飞行距离与满意度的关系散点图,底部放各服务维度打分的对比条形图。如果想加一张有“大数据感”的图,可以做一张旅客年龄分布与满意度关系的热力图。
用pyecharts画特征重要性条形图的骨架代码大概是这样的:
python复制from pyecharts.charts import Bar
from pyecharts import options as opts
bar = (
Bar()
.add_xaxis(feature_names[:10])
.add_yaxis('重要性', importance_values[:10])
.set_global_opts(
title_opts=opts.TitleOpts(title='特征重要性Top10'),
xaxis_opts=opts.AxisOpts(name='特征'),
yaxis_opts=opts.AxisOpts(name='重要性')
)
)
bar.render('importance_bar.html')
多个图表生成之后,用Grid布局拼到同一个页面。pyecharts的每个图表都是独立HTML,拼大屏需要把各个图表的 render 结果合并进一个模板页。实际操作时,我会先给每个图表定义一个div容器,然后用 Page 或手动布局把它们排列起来。这部分如果前端底子一般,不建议把布局调得太复杂,用 Page(layout=Page.SimplePageLayout) 按顺序排下来也很干净。
5.3 大屏布局和配色上的建议
做可视化大屏,最容易翻车的就是贪多。图表数量一多,信息密度不够,观众根本不知道看哪里。我的原则是:一屏只回答一个问题。既然这个项目的核心问题是“哪些因素影响旅客满意度”,那么大屏上的每一个图表都要围绕满意度来组织。
配色方面,建议定一个主色系,比如蓝青色系,满意用亮蓝色,不满意用橙色或灰色,对比明显又不刺眼。避免一屏上出现五六种完全不搭的颜色。标题建议统一放在左上角,副标题写数据来源和样本量,底部可以留一行小字放关键结论,比如“特征重要性最高的三项为网上值机服务、在线预订便利度、座位舒适度”。这一行小字往往能让评委觉得“这个学生真把数据读懂了”。
6. 从代码到论文:毕设整合与几个加分扩展
6.1 论文结构怎么排,图表放哪里
代码跑通了,可视化做完了,最后就是把这些内容组织进毕业论文。我给你的结构建议如下:
| 章节 | 内容安排 | 对应你的项目文件 |
|---|---|---|
| 绪论 | 研究背景、国内外研究现状、研究内容 | 选题背景部分 |
| 相关技术介绍 | 随机森林、决策树、数据预处理方法、可视化工具 | 理论部分 |
| 数据获取与预处理 | 数据集介绍、缺失值处理、编码、样本划分 | 数据预处理代码 |
| 模型构建与评估 | 特征选择、随机森林调参、评估指标分析 | 模型训练与评估 |
| 可视化实现 | 静态图表设计、大屏设计、交互功能 | 可视化代码 |
| 总结与展望 | 项目总结、不足与改进方向 | 结语部分 |
注意一个问题:很多同学在“相关技术介绍”这一章花了大量篇幅写随机森林原理,从决策树讲到基尼指数,写得非常细,这没问题,但容易让正文显得头重脚轻。我的建议是把算法原理压缩到能支撑后续分析即可,把重心放在你自己的数据预处理策略、调参过程和结果分析上。毕竟毕业设计考察的是“你能不能独立完成一个项目”,而不是“你能不能把教材抄一遍”。
6.2 三个低成本加分扩展
如果时间和精力允许,给这个项目增加以下扩展会让你在答辩时明显更有底气:
第一,增加模型对比实验。用逻辑回归、XGBoost、LightGBM分别跑一遍同样的数据,和随机森林的AUC做对比。逻辑回归训练只要几秒钟,XGBoost和LightGBM在这个数据量下也就是几十秒的事。对比结果可以用一张柱状图呈现,论文里就能多出一节“不同模型性能对比”,显得你视野更宽。
第二,增加旅客分群分析。用KMeans对数值型特征做聚类,看能分成几个典型旅客群体,再分析不同群体的满意度差异和主要不满意原因。这部分和随机森林无关,但能体现数据分析的综合性,也能给大屏增加一张有意思的散点图。
第三,增加交互式预测演示。用 streamlit 写一个简单页面,左侧放一个表单,让用户拖拽选择年龄、舱位、各服务评分,右侧实时显示模型预测的满意度结果和概率。这个功能技术上不复杂,但答辩现场演示效果很好,属于“看着很厉害、实现很简单”的典型。
6.3 答辩时最容易被问到的问题
根据我带学生答辩的经验,评委看到这个题目通常会问这么几个问题,提前准备好答案:
为什么用随机森林而不是单一决策树?这个问题要答出两个点:随机森林通过自助采样和随机特征选择引入了多样性,能显著降低单棵决策树过拟合的风险;同时多棵树投票能平滑掉单棵树的噪声,稳定性更好。
数据里的缺失值你是怎么处理的?为什么这么做?按我上面2.1节的做法,把两步填充逻辑说清楚,再补一句“因为延误分布右偏因此用中位数”,基本就能过关。
特征重要性是怎么算出来的?论文里如果有Gini重要性和排列重要性两个图,把两个原理分别讲清楚,就已经超出评委预期了。
如果数据量扩大到原来的100倍,随机森林方案会遇到什么瓶颈?这题看你怎么坦诚回答了:训练时间会显著增加,特征重要性计算也会变慢,可以考虑接入分布式计算平台,也可以考虑换用LightGBM这类梯度提升树模型,训练速度和内存占用都会更优。
最后再分享一个我个人的体会:做这类数据分析毕业设计,最忌讳的是把大量时间耗在调参和追求精度上,因为这个数据集的准确率天花板就摆在那里,多调出来的一两个百分点在答辩时没有区分度。真正让一个毕设项目出彩的,永远是“数据故事讲得清不清楚”和“分析结果有没有落到业务层面”。你把特征重要性背后的业务含义解释透了,把大屏做得简洁有重点,把每次处理数据的合理性说明白,这套项目就已经是优秀水平了。
