1. 从零开始:为什么我们要预测学生成绩?

你可能觉得,预测学生成绩听起来像是老师或者教务系统该干的事儿,跟咱们搞技术的有什么关系?其实这事儿特别有意思,也特别有现实意义。我刚开始接触这个项目的时候,也这么想,但后来发现,这简直是一个完美的机器学习“练手场”。

想象一下,你是一个教育机构的负责人,或者是一个在线学习平台的产品经理。如果能提前预测一个学生未来的成绩走势,是不是就能在他成绩下滑之前,提前给他推送一些针对性的学习资料?或者,当你分析一个班级的整体数据时,发现“每周学习时间”和“家庭支持程度”这两个因素,对成绩的影响远超其他,你是不是就能更有针对性地设计教学方案?这就是数据驱动决策的魅力所在。

对学生成绩预测这个任务来说,它本质上是一个回归问题——我们要预测的是一个连续的数值(比如0到20分的最终成绩G3)。它用到的数据集通常不大,特征也相对清晰(比如学生的年龄、家庭情况、学习习惯等),非常适合机器学习新手来理解一个完整的数据分析流程:从拿到一堆原始数据,到清洗、探索、建模,最后评估模型效果。整个过程就像搭积木,每一步你都能看到清晰的反馈。

更重要的是,这个项目里藏着很多机器学习实战中必须掌握的“通用技能包”。比如,数据里既有数字(年龄、学习时间),也有文字(学校、家庭地址),你怎么把它们统一变成模型能“吃”下去的格式?这就是特征工程。又比如,有几十个特征,是不是全扔给模型就好?当然不是,你得学会找出哪些特征真正有用,这就是特征选择。最后,模型跑出来了,你怎么知道它是不是在“瞎猜”?你需要一个客观的标尺,这就是模型评估。

所以,别小看这个“学生成绩预测”,它麻雀虽小,五脏俱全。跟着我走完这一趟,你不仅能得到一个可以运行的预测模型,更能掌握一套处理真实数据的标准动作。下面,我就手把手带你,用一份真实的学生数据集,把整个流程跑一遍。我会把每个步骤为什么这么做、可能会踩什么坑、怎么调优都讲清楚,保证你看完就能自己动手复现。

2. 实战第一步:认识你的数据与环境

俗话说“巧妇难为无米之炊”,咱们搞机器学习,数据就是“米”。我们这次用的数据集,是教育领域一个非常经典的公开数据集,很多学术论文和入门教程都用它。它记录了葡萄牙某中学数百名学生的各种属性,以及他们三门课(我们这里用数学课)的期中、期末成绩。

2.1 搭建你的Python工作环境

工欲善其事,必先利其器。我强烈建议你使用 Anaconda 来管理Python环境,它能帮你轻松处理各种包依赖,避免版本冲突的“玄学”问题。如果你还没安装,去Anaconda官网下载安装,一路下一步就行。

安装好后,打开你的终端(Windows叫Anaconda Prompt,Mac/Linux叫Terminal),我们创建一个专属这个项目的环境:

conda create -n student_score python=3.8
conda activate student_score

然后,安装我们这次需要的几个核心“武器”:

pip install pandas numpy scikit-learn matplotlib seaborn jupyter

简单解释一下这几个库是干嘛的:

  • pandas:数据处理的瑞士军刀,读数据、清洗数据、分析数据全靠它。
  • numpy:进行科学计算的基础,很多数学运算的底层都是它。
  • scikit-learn:机器学习核心库,提供了各种现成的模型和工具,我们这次用的模型都从这里来。
  • matplotlib 和 seaborn:画图神器,数据探索阶段可视化全靠它们,能让数据“开口说话”。
  • jupyter:交互式笔记本,特别适合做数据分析,可以一段代码一段代码地运行和观察。

我习惯用Jupyter Notebook来一步步探索,你也可以用任何你喜欢的IDE(比如PyCharm、VSCode)。接下来,我们就启动Jupyter,开始和数据“面对面”。

2.2 初探数据集:第一印象很重要

首先,我们把数据加载进来,看看它长什么样。你可以把下载好的 student-mat.csv 文件放在和你的代码同一个目录下。

import pandas as pd
import numpy as np

# 加载数据
student = pd.read_csv('./student-mat.csv')

# 先看看数据的前几行,有个直观感受
print("数据的前5行:")
print(student.head())

# 再看看数据的“体检报告”:有多少行、多少列、每列的数据类型
print("\n数据的形状(行数,列数):", student.shape)
print("\n数据的列信息:")
print(student.info())

# 重点看看我们要预测的目标——第三次成绩G3
print("\n目标变量 G3 的描述性统计:")
print(student['G3'].describe())

运行这几行代码,你就能得到一堆输出。我带你解读一下:

  1. student.head():这就像书的目录,让你快速浏览数据里有哪些列。你会看到 school(学校)、age(年龄)、studytime(每周学习时间)、failures(不及格次数)、G1、G2、G3(三次成绩)等等。既有数字,也有像 Mjob(母亲职业)这样的文字。
  2. student.shape:这会告诉你数据集有(多少行,多少列)。比如可能是 (395, 33),意味着有395个学生样本,33个特征(包括最后的成绩G3)。
  3. student.info():这是数据的“健康检查”。它会列出每一列的名字、非空值的数量以及数据类型(int64是整数,float64是小数,object通常是文本)。这里一定要仔细看有没有缺失值(Non-Null Count是否等于总行数)。幸运的是,这个数据集通常很干净,没有缺失值,省去了我们处理缺失值的一大步。
  4. student['G3'].describe():专门针对我们要预测的最终成绩G3。它会输出计数、平均值、标准差、最小值、四分位数和最大值。比如,你可能会看到平均分是10.9分,最低0分,最高20分。这让你心里有个数:我们预测的值大概在什么范围。

这一步千万别跳过。我见过很多新手朋友,拿到数据直接就往模型里塞,结果模型效果稀烂,回头一看才发现数据里有一堆奇怪的值或者格式错误。花10分钟了解你的数据,能为你后面节省好几个小时的调试时间。

3. 数据预处理:把“生米”煮成“熟饭”

原始数据就像刚从地里摘回来的菜,上面可能沾着泥(异常值),有不同品种(不同类型的数据),不能直接下锅炒。预处理,就是洗菜、切配的过程。

3.1 处理分类变量:给文字贴上数字标签

机器学习模型本质上是数学公式,它只认识数字,不认识“男”、“女”或者“教师”、“医生”这样的文字。所以,我们必须把所有的文字特征(分类变量)转换成数字。最常用、也最不容易出错的方法就是 One-Hot编码,也叫哑变量编码。

它的思想很简单:如果一个特征有N种可能的值(比如“家庭地址”有“城市”和“农村”两种),我们就为它创建N个新的二进制特征(0或1)。对于某个学生,如果他家在城市,那么“地址_城市”这个特征就是1,“地址_农村”就是0。

# 首先,我们把要预测的标签(G3)单独拿出来,避免把它也编码了
labels = student['G3']  # 这是我们最终要预测的目标

# 然后,我们决定在特征中删除一些列。为什么?
# 1. ‘school’:数据可能只来自一两所学校,这个特征可能区分度不大,或者我们不想让模型依赖学校信息。
# 2. ‘G1’, ‘G2’:这是学生第一次和第二次的成绩。注意!用前两次成绩预测第三次,虽然很准,但有点“作弊”嫌疑。
#    在实际教育干预场景,我们可能想在学期初就预测期末成绩,那时还没有G1和G2。所以这里我们先不用,看看仅凭其他特征能预测得多准。
#    这是一个重要的建模决策点!
features = student.drop(['school', 'G1', 'G2', 'G3'], axis=1)

# 现在,对剩下的特征进行One-Hot编码
# pandas的get_dummies函数会自动识别所有object类型的列(文本列),并为它们创建哑变量
features_encoded = pd.get_dummies(features)

print("编码前的特征形状:", features.shape)
print("编码后的特征形状:", features_encoded.shape)
print("\n编码后新增的一些特征列名:")
print(features_encoded.columns.tolist()[-10:]) # 看看最后10个新列名

执行完这段代码,你会发现特征的数量(列数)一下子变多了很多。原来一列“母亲职业(Mjob)”,现在变成了“Mjob_at_home”, “Mjob_health”, “Mjob_other”, “Mjob_services”, “Mjob_teacher”五列。这就是One-Hot编码。

3.2 特征相关性分析:找出“关键先生”

现在我们有了一大堆特征(可能超过50个)。是不是全部喂给模型就好了?并不是。特征太多,一方面计算慢,另一方面可能引入噪声,导致模型过拟合(在训练集上表现好,在测试集上表现差)。我们需要做特征选择,找出那些和最终成绩G3最相关的特征。

最直观的方法就是计算皮尔逊相关系数。这个系数的值在-1到1之间。越接近1,表示正相关越强(比如学习时间越长,成绩可能越高);越接近-1,表示负相关越强(比如不及格次数越多,成绩可能越低);接近0,表示基本没啥线性关系。

# 将编码后的特征和标签G3合并,方便计算相关性
data_for_corr = features_encoded.copy()
data_for_corr['G3'] = labels

# 计算所有特征与G3的相关性绝对值,并排序
correlation_with_target = data_for_corr.corr()['G3'].abs().sort_values(ascending=False)

print("与G3最相关的前10个特征:")
print(correlation_with_target.head(10))

# 我们可以选择相关性最高的前K个特征。K取多少?这是一个超参数,可以尝试。
# 这里我们选择除了G3自身外,相关性最高的8个特征。
top_k = 8
# 注意:correlation_with_target的第一个值肯定是G3和它自己的相关性(为1.0)
selected_features = correlation_with_target.index[1:top_k+1].tolist() # 跳过G3本身

print(f"\n我们选择的前{top_k}个特征是:")
print(selected_features)

# 从编码后的特征中,只保留这些选中的特征
final_features = features_encoded[selected_features]

跑完这段代码,你可能会发现,“G2”(第二次成绩)如果没被删除,它和G3的相关性会非常高,这很合理。但我们之前决定不用它。那么剩下的特征里,可能是“G1”(第一次成绩,如果我们没删)、“failures”(不及格次数)、“Medu”(母亲教育程度)、“studytime”(学习时间)等排在前列。这个分析结果本身就很有价值,它告诉我们哪些因素对成绩影响最大。

4. 构建与评估模型:是骡子是马拉出来遛遛

数据准备好了,特征也选好了,现在进入最激动人心的环节——建模。但别急,在请出各位“机器学习大神”之前,我们先要建立一个基线模型。

4.1 建立基线模型:先有个“傻瓜”参照物

基线模型通常是一个极其简单、甚至不需要机器学习的预测方法。常用的基线是中位数预测或均值预测。也就是,我们直接用训练集里G3的中位数,来预测测试集里所有学生的成绩。

为什么要这么做?这是为了树立一个评判标准。如果你的复杂模型(比如随机森林)的预测效果,还不如简单粗暴的“永远猜中位数”,那这个复杂模型就没有任何价值,我们的工作也白费了。基线模型是我们必须超越的底线。

from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error, mean_squared_error

# 1. 划分训练集和测试集
# 注意:这里我们用筛选后的特征 final_features 和 标签 labels
# test_size=0.25 表示25%的数据作为测试集,75%用于训练
# random_state=42 是随机种子,固定它能让每次划分的结果都一样,便于复现结果
X_train, X_test, y_train, y_test = train_test_split(final_features, labels, test_size=0.25, random_state=42)

print(f"训练集样本数:{X_train.shape[0]}, 测试集样本数:{X_test.shape[0]}")

# 2. 定义评估函数
def evaluate_predictions(predictions, true_values):
    """计算并打印MAE和RMSE"""
    mae = mean_absolute_error(true_values, predictions)
    rmse = np.sqrt(mean_squared_error(true_values, predictions)) # RMSE就是MSE开根号
    return mae, rmse

# 3. 基线模型:预测所有测试样本的成绩都为训练集中位值
median_guess = np.median(y_train)
print(f"\n训练集G3的中位数是:{median_guess}")
# 生成一个和测试集一样长的列表,里面全是中位值
baseline_predictions = [median_guess] * len(y_test)

# 4. 评估基线模型
baseline_mae, baseline_rmse = evaluate_predictions(baseline_predictions, y_test)
print(f"基线模型(中位数预测)的 MAE: {baseline_mae:.4f}")
print(f"基线模型(中位数预测)的 RMSE: {baseline_rmse:.4f}")

# 顺便看看测试集真实值的分布,对比一下
print(f"\n测试集G3的真实描述:")
print(pd.Series(y_test).describe())

解读MAE和RMSE:

  • MAE(平均绝对误差):把所有预测误差的绝对值求平均。非常直观,比如MAE=2,就平均每个预测差了2分。
  • RMSE(均方根误差):先把误差平方,再平均,最后开方。它对大的误差惩罚更重(因为平方放大了大误差)。通常RMSE会比MAE大一点。 这两个值都是越小越好。现在你得到了基线模型的性能,记下这两个数字。

4.2 训练多个机器学习模型并对比

现在,我们请出几位常见的回归模型选手,让它们同台竞技。我们用同样的训练集训练它们,用同样的测试集评估它们。

from sklearn.linear_model import LinearRegression, ElasticNet
from sklearn.ensemble import RandomForestRegressor, ExtraTreesRegressor, GradientBoostingRegressor
from sklearn.svm import SVR

# 初始化模型,这里我稍微调了一下个别模型的默认参数,让它们更有可比性,你也可以不改
models = {
    '线性回归': LinearRegression(),
    '弹性网络': ElasticNet(alpha=0.01, l1_ratio=0.5, random_state=42), # 加了点正则化防止过拟合
    '随机森林': RandomForestRegressor(n_estimators=100, random_state=42), # 100棵树
    '极端随机树': ExtraTreesRegressor(n_estimators=100, random_state=42),
    '支持向量机回归': SVR(C=1.0, kernel='rbf'), # 使用径向基核函数
    '梯度提升树': GradientBoostingRegressor(n_estimators=100, learning_rate=0.1, random_state=42)
}

# 准备一个字典来存储结果
results = {}

# 循环训练和评估每个模型
for name, model in models.items():
    # 1. 训练模型
    model.fit(X_train, y_train)
    # 2. 在测试集上预测
    y_pred = model.predict(X_test)
    # 3. 评估
    mae, rmse = evaluate_predictions(y_pred, y_test)
    results[name] = {'MAE': mae, 'RMSE': rmse}
    print(f"{name:15s} - MAE: {mae:.4f}, RMSE: {rmse:.4f}")

# 把基线模型的结果也加进去比较
results['基线(中位数)'] = {'MAE': baseline_mae, 'RMSE': baseline_rmse}
print(f"\n基线(中位数) - MAE: {baseline_mae:.4f}, RMSE: {baseline_rmse:.4f}")

# 将结果转换成DataFrame,方便查看和画图
results_df = pd.DataFrame(results).T  # .T是转置,让模型名做行索引
print("\n===== 所有模型性能对比 =====")
print(results_df.sort_values(by='MAE')) # 按MAE从小到大排序

跑完这段代码,你会看到一个模型性能的排行榜。通常,像随机森林、梯度提升树这类集成树模型,在这种结构化数据上表现会很好,它们的MAE和RMSE应该会显著低于基线模型。线性回归可能表现一般,因为特征和目标之间的关系不一定是简单的线性。SVM在小数据集上有时效果不错,但调参比较麻烦。

4.3 结果可视化:让对比一目了然

数字看完了,我们把它画出来,更直观。

import matplotlib.pyplot as plt
import seaborn as sns

# 设置画布
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# 按MAE排序的结果
results_df_sorted_mae = results_df.sort_values('MAE', ascending=True)

# 左图:MAE对比
axes[0].barh(range(len(results_df_sorted_mae)), results_df_sorted_mae['MAE'], color='skyblue')
axes[0].set_yticks(range(len(results_df_sorted_mae)))
axes[0].set_yticklabels(results_df_sorted_mae.index)
axes[0].set_xlabel('平均绝对误差 (MAE)')
axes[0].set_title('模型MAE对比(越低越好)')
axes[0].invert_yaxis() # 让最好的模型显示在最上面

# 右图:RMSE对比
results_df_sorted_rmse = results_df.sort_values('RMSE', ascending=True)
axes[1].barh(range(len(results_df_sorted_rmse)), results_df_sorted_rmse['RMSE'], color='lightcoral')
axes[1].set_yticks(range(len(results_df_sorted_rmse)))
axes[1].set_yticklabels(results_df_sorted_rmse.index)
axes[1].set_xlabel('均方根误差 (RMSE)')
axes[1].set_title('模型RMSE对比(越低越好)')
axes[1].invert_yaxis()

plt.tight_layout()
plt.show()

这张图能清晰地告诉你哪个模型是当前的“冠军”。比如,随机森林的柱子最短,说明它的误差最小。到这里,一个完整的学生成绩预测机器学习流程就走通了。你已经从数据出发,建立了一个能打败“瞎猜”的预测模型。

5. 深入优化与思考:从“能用”到“好用”

做到上一步,你已经成功了80%。但如果你想做得更好,让模型更稳健、更可靠,下面这些进阶步骤值得你花时间琢磨。这也是区分新手和有经验者的地方。

5.1 模型调优:给冠军模型“微调”

假设我们发现随机森林表现最好,但它是不是已经达到最佳状态了呢?不一定。模型的默认参数(比如n_estimators树的数量、max_depth树的最大深度)是通用设置。我们可以通过网格搜索或随机搜索,为我们的特定数据寻找一组更优的参数。

from sklearn.model_selection import GridSearchCV

# 定义我们要尝试的参数组合网格
param_grid = {
    'n_estimators': [50, 100, 200], # 树的数量
    'max_depth': [None, 10, 20, 30], # 树的最大深度,None表示不限制
    'min_samples_split': [2, 5, 10], # 分裂内部节点所需的最小样本数
    'min_samples_leaf': [1, 2, 4] # 叶节点所需的最小样本数
}

# 创建基础模型
rf = RandomForestRegressor(random_state=42)

# 创建网格搜索对象,使用5折交叉验证,以负的MAE作为评分标准(sklearn要求最大化,所以用负值)
grid_search = GridSearchCV(estimator=rf, param_grid=param_grid,
                           cv=5, scoring='neg_mean_absolute_error',
                           verbose=1, n_jobs=-1) # n_jobs=-1使用所有CPU核心

# 在训练集上执行网格搜索
grid_search.fit(X_train, y_train)

# 输出最佳参数和最佳分数
print(f"找到的最佳参数组合:{grid_search.best_params_}")
print(f"对应的最佳(负)MAE分数:{grid_search.best_score_:.4f}")
# 注意:best_score_是交叉验证的平均分,且是负的MAE,所以取负号得到正的MAE
best_mae_cv = -grid_search.best_score_
print(f"交叉验证下的最佳MAE:{best_mae_cv:.4f}")

# 用最佳参数模型在测试集上最终评估
best_rf = grid_search.best_estimator_
y_pred_tuned = best_rf.predict(X_test)
mae_tuned, rmse_tuned = evaluate_predictions(y_pred_tuned, y_test)
print(f"\n调优后的随机森林在测试集上的表现:")
print(f"MAE: {mae_tuned:.4f}, RMSE: {rmse_tuned:.4f}")

# 和调优前对比一下
print(f"\n调优前测试集MAE: {results['随机森林']['MAE']:.4f}")
print(f"调优后测试集MAE: {mae_tuned:.4f}")
print(f"MAE降低了:{results['随机森林']['MAE'] - mae_tuned:.4f}")

调参过程可能比较耗时,但很多时候能带来明显的提升。它帮你理解模型参数如何影响性能,是机器学习实践中不可或缺的一环。

5.2 特征重要性分析:模型是怎么“想”的?

树模型有一个很棒的特性:它们可以告诉我们哪个特征在做出决策时更重要。这能帮助我们验证之前的相关性分析,甚至发现一些相关性不高但组合起来很有用的特征。

# 使用我们调优后的最佳模型
best_model = grid_search.best_estimator_

# 获取特征重要性
importances = best_model.feature_importances_
feature_names = X_train.columns

# 将重要性和特征名组合,并排序
feature_importance_df = pd.DataFrame({
    'feature': feature_names,
    'importance': importances
}).sort_values('importance', ascending=False)

print("特征重要性排名:")
print(feature_importance_df)

# 可视化
plt.figure(figsize=(10, 6))
plt.barh(feature_importance_df['feature'][:10], feature_importance_df['importance'][:10]) # 只看前10
plt.xlabel('特征重要性')
plt.title('随机森林模型特征重要性 Top 10')
plt.gca().invert_yaxis()
plt.tight_layout()
plt.show()

看看这个排名,是不是和之前相关性分析的结果大体一致?通常,“failures”(不及格次数)、“Medu”(母亲教育程度)、“studytime”(学习时间)会排在前列。这不仅能增强你对模型的信任,也能为实际的教育干预提供洞见——比如,提高家庭支持(对应母亲教育程度)可能比单纯增加学习时间更有效。

5.3 误差分析:模型在哪里“翻车”了?

最后,我们不应该只满足于一个总的误差分数。拆开看看模型在哪些样本上预测得特别差,能帮助我们找到模型的弱点。

# 计算每个测试样本的绝对误差
errors = np.abs(y_pred_tuned - y_test)

# 找出误差最大的前N个样本
top_error_indices = errors.argsort()[-5:][::-1] # 误差最大的5个样本的索引

print("预测误差最大的5个学生样本:")
for idx in top_error_indices:
    print(f"\n学生索引(在原始测试集中): {idx}")
    print(f"  真实成绩 G3: {y_test.iloc[idx]}")
    print(f"  预测成绩: {y_pred_tuned[idx]:.2f}")
    print(f"  绝对误差: {errors.iloc[idx]:.2f}")
    # 可以进一步查看这个学生的特征
    print(f"  部分特征值: {X_test.iloc[idx][:5].to_dict()}") # 打印前5个特征

分析这些“困难样本”,你可能会发现一些规律:是不是这些学生都有某些极端特征值?或者他们属于某个特定的、数据量很少的群体(比如“父亲职业是农民”且“家庭关系极差”)?这种分析能指导你后续改进,比如是否需要收集更多这类学生的数据,或者是否需要构造新的特征来描述他们。

走完这完整的一圈,你不仅完成了一个预测项目,更重要的,你掌握了处理一个真实机器学习问题的标准思维框架:理解问题、探索数据、清洗转换、建立基线、尝试模型、评估优化、分析结果。这个框架,可以套用到无数的其他预测问题上,这才是本次实战最大的收获。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐