大家好,今天想和大家分享一个我最近做的教育数据分析项目。这个项目涉及几千名学生的画像数据,包括成绩、考勤、消费记录等信息。通过机器学习,我不仅完成了数据处理,还挖掘出一些有意思的学生行为规律。 代码和数据都已经过脱敏处理,请放心阅读。

 一、项目背景与数据初探


事情是这样的,我拿到了一个教育数据分析的任务,当我们小组的同学把所有初始文档都清洗后,文件夹里长这样:
教育数据分析/
├── 数据清洗情况/
│   ├── 学生画像文件/     # 几百个学生
的详细画像
│   ├── kaoqin_statistics.csv   # 班
级考勤统计
│   ├── result.csv             # 学
业成绩大表
│   └── merge_portraits.py      # 数
据合并脚本
```

数据有多丰富?先看看字段:
单个学生画像包含:

基本信息:学号、姓名、班级
考勤数据:操场考勤、早退、校服不达标、离校、进校、迟到
学业成绩:数学、语文、英语等各科平均分
行为标记:作弊次数、缺考次数
消费记录:2018-2019年消费金额
目标变量 :综合风险等级(低风险/中风险/高风险)
成绩数据更夸张: 每一科都有:加权平均分、方差、平均Z分数、综合T分数、平均等级——相当于给学生做了全方位的学业能力画像。

二、数据处理:pandas处理大文件的正确姿势


首先遇到的第一个挑战是: 如何高效合并几百个小CSV文件?

我写了一个脚本 merge_portraits.py ,核心逻辑是这样的:

```
import pandas as pd
import os

def main():
    portraits_dir = "学生画像文件"
    portrait_files = [f for f in os.
    listdir(portraits_dir) if f.
    endswith('.csv')]
    
    all_portraits = pd.DataFrame()
    
    for i, file_name in enumerate
    (portrait_files):
        file_path = os.path.join
        (portraits_dir, file_name)
        df = pd.read_csv(file_path, 
        encoding='utf-8-sig')
        all_portraits = pd.concat
        ([all_portraits, df], 
        ignore_index=True)
        
        if (i + 1) % 500 == 0:
            print(f"已处理 {i + 1}/
            {len(portrait_files)} 个
            文件")
```


经验分享

1. 使用 ignore_index=True 确保合并后索引连续
2. 每500个文件打印一次进度——处理几千个文件时,没有进度条真的会焦虑
3. encoding='utf-8-sig' 是处理中文CSV的标配,否则会乱码


 三、特征工程:成绩数据里的门道


3.1 从原始分数到标准化指标
原始成绩只是原始分,但不同科目难度不同,直接比较没有意义。我注意到数据里已经有了:

- Z分数 :标准化的分数,消除了科目间难度差异
- T分数 :进一步线性变换的结果,均值50,标准差10


这相当于数据提供方已经帮你做了标准化处理,省了不少功夫。

3.2 方差里的秘密
我注意到每个科目都有"方差"字段。 方差大意味着成绩不稳定 ,波动明显。通过分析方差数据,我发现:

- 高风险学生的成绩方差普遍偏高
- 某些"异常安静"的班级,方差反而很小——这可能意味着教学模式过于单一


3.3 考勤与成绩的关联
看这个考勤数据(部分):

一个有意思的发现: 高三班级普遍早退次数高于高一。这背后可能是学习压力、作息安排等多种因素的综合体现。

 四、机器学习建模实践

4.1 问题定义
这是一个 多分类问题 :给定学生的各项特征,预测其"综合风险等级"。

特征工程后的输入:

- 学业类:各科平均分、方差、Z分数、T分数
- 行为类:考勤次数、作弊次数、缺考次数
- 消费类:各年消费金额
输出: 低风险 / 中风险 / 高风险

4.2 模型选择

```
from sklearn.ensemble import 
RandomForestClassifier
from sklearn.model_selection import 
train_test_split
from sklearn.metrics import 
classification_report

# 划分训练集和测试集
X_train, X_test, y_train, y_test = 
train_test_split(
    features, labels, test_size=0.
    2, random_state=42
)

# 使用随机森林
model = RandomForestClassifier
(n_estimators=100, random_state=42)
model.fit(X_train, y_train)

y_pred = model.predict(X_test)
print(classification_report(y_pred, 
y_test))
```


为什么选择随机森林?

1. 可解释性强 :可以查看特征重要性,了解哪些因素最影响风险等级
2. 对缺失值鲁棒 :教育数据经常有缺考、缺消费记录等情况
3. 不需要太多调参 :默认参数往往就能取得不错的效果


4.3 特征重要性分析
训练完模型后,我最关心的就是: 到底什么因素最能预测学生的风险等级?

```
importances = model.
feature_importances_
feature_names = features.columns

# 排序并可视化
sorted_idx = importances.argsort()
[::-1]
for i in sorted_idx[:10]:
    print(f"{feature_names[i]}: 
    {importances[i]:.4f}")
```


我的发现(实际分析结果):

1. 缺考次数 是最强的预测因子 —— 频繁缺考的学生,往往风险等级更高
2. 各科成绩的方差 排名靠前 —— 成绩不稳定的學生需要更多关注
3. 迟到次数 的影响超出预期 —— 原以为只是小问题,实际关联性很强
4. 消费数据 反而没那么关键 —— 可能是因为很多学生消费为0(住校生)


 五、实战经验总结


 5.1 数据清洗的血泪教训
教训1:空值不等于没数据

在成绩数据中,很多学生某些科目显示为0或空。这可能是:

- 文科生没有选理科
- 艺术生没有普通科目成绩
错误做法: 直接填充0或删除 正确做法: 先分析空值分布,判断是"真缺考"还是"未选考"

教训2:班级名称的陷阱

原始数据里班级名称有不同前缀:

- 东- 开头
- 白- 开头
- 直接数字编号
混在一起统计时,差点翻车。后来做了统一清洗才解决。

5.2 模型调参的心得
参数 我尝试的值 推荐 n_estimators 50/100/200/500 100-200足够 max_depth None/10/20/30 数据量小可以用None min_samples_split 2/5/10 5是个不错的起点

经验: 教育数据量通常不大(几千条),模型太复杂反而容易过拟合。

5.3 一个反直觉的发现
我一直以为"成绩差"是风险等级高的最大原因,但模型告诉我: 行为特征(缺考、作弊)比成绩本身更能预测风险。

换句话说,一个成绩一般但从不缺考的学生,风险可能比一个成绩优秀但经常缺考的学生还低。

这个发现对教育工作者的启示是: 不要只盯着成绩,行为轨迹同样重要。

六、代码片段:完整的数据处理流程


这是我从项目中提炼出的核心处理逻辑:

```
import pandas as pd
import numpy as np

def process_student_data
(portrait_df, score_df, 
attendance_df):
    """
    合并三源数据并进行基础清洗
    """
    # 1. 合并成绩和画像数据
    merged = pd.merge(portrait_df, 
    score_df, on='学号', how='left')
    
    # 2. 处理空值:缺考次数为0表示无缺考
    记录
    merged['缺考次数'] = merged['缺考
    次数'].fillna(0)
    
    # 3. 过滤无效记录(退学生)
    merged = merged[merged['退学状态
    '] == '否']
    
    # 4. 构建特征
    features = [
        '数学_加权平均分', '语文_加权平
        均分', '英语_加权平均分',
        '作弊次数', '缺考次数', '迟到
        ', '早退',
        '2019年消费', '总消费'
    ]
    
    # 5. 填充缺失值(用中位数)
    for col in features:
        if merged[col].isnull().any
        ():
            merged[col] = merged
            [col].fillna(merged
            [col].median())
    
    return merged

# 调用示例
df = process_student_data
(portraits, scores, attendance)
print(f"处理完成,有效记录数: {len(df)}
")
```

 七、写在最后


做这个项目的最大感受是: 机器学习在教育领域的应用还处于非常初级的阶段 。数据多,但真正用起来的少。

一方面是因为教育数据的敏感性,很多学校不愿意开放;另一方面是教育问题的复杂性,单纯靠模型很难解决。

但这不妨碍我们去尝试、去探索。通过数据,我们可以发现以前凭经验发现不了的规律;通过模型,我们可以更科学地辅助教育决策。

如果你也在做类似的项目,欢迎在评论区交流!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐