第一次打开本地文件夹时看到Wk.2_HW这个名字,我脑子里蹦出来的第一句话是:又到了交作业的时候。Wk.2 是第二周,HW 是 Homework,说白了就是第二次实践作业。这份作业的任务书里给了一份模拟学习行为数据,要求完成从数据清洗、探索性分析到线性回归建模预测的完整流程。下面记录的就是我当时从拆解需求、写代码、踩坑到最终提交的完整经历。如果你也正在做类似的数据分析作业,或者只是想知道一份合格的Wk.2_HW应该长什么样,这篇内容应该能给你一些参考。
1. 拿到 Wk.2_HW 后我先做了什么
1.1 文件名里其实藏着不少信息
很多课程作业的命名方式都是类似的:周次缩写加任务缩写。Wk.2_HW 一眼看上去平平无奇,但它至少告诉了三件事:这是第二周的任务,它是独立作业,大概率需要提交一份可运行工程和一份说明文档。不过真正决定工作量的是作业要求本身。我拿到的任务书里写了几条硬性要求:第一,将原始 CSV 数据清洗成可用于分析的格式;第二,对关键字段做不少于 5 个维度的探索性可视化;第三,用线性回归模型完成对连续目标的预测,并评估模型效果;第四,整理成一份包含图表和结论的分析报告。
这些要求放在第一周刚结束的背景下,难度其实卡得很准:不要求上复杂算法,但要求把最基础的数据处理链路走通。如果只是照着教程敲一遍代码,也能跑通,但会出现很多隐蔽问题,比如字段类型不对、可视化结果拿不出手、训练集和测试集泄露等。所以我拿到题目的第一反应不是开 Notebook 直接写,而是先花半小时拆需求、列检查清单,把所有交付物边界想清楚再动手。这一步看起来浪费时间,实际是避免返工性价比最高的做法。
1.2 作业需求还原与任务拆解
把模糊需求转成可执行任务,是我做这个作业过程中收获最大的一部分。我先把任务书里的四个要求拆成了具体子任务:
- 数据清洗:检查缺失值、重复行、异常值、字段类型,统一时间格式,处理设备字段中的脏值。
- 探索性分析:对学习时长、完成率、测验分数三个连续变量做分布分析,对设备类型做分组比较,对学习时段做趋势分析。
- 特征工程:从原始时间戳中拆出周几、时段,对设备类型做编码,将学习时长按周聚合作为序列特征。
- 建模与评估:用线性回归模型预测测验分数,合理划分训练集和测试集,用 R² 和 RMSE 评估效果。
这四块内容听起来都不难,但把它们串在一起时,很多环节会互相牵连。比如特征工程搞晚了,可视化的部分就得重新生成;数据清洗时把缺失值处理策略选错,后面模型结果会直接受影响。因此我建议每一步都留下过程记录,哪怕只是 Notebook 里的 Markdown 单元格,也要写下“为什么这样做”。作业提交之后,这些记录比代码本身更能说明你会不会思考。
2. 环境和数据准备:开工前要做的三件事
2.1 数据字段说明与观察
这次作业使用的数据是一份模拟学习行为记录,文件名叫student_learning_log.csv,字段大致包括:用户编号、课程编号、学习日期、学习时长(分钟)、完成率、测验分数、设备类型、地区代号。拿到数据后我先做了三件小事:
- 用
df.info()查看字段类型和缺失情况; - 用
df.head()观察前几行,确认表头和业务含义; - 用
df.describe()查看数值列的量纲和分布范围。
做完这三步,我注意到了一个非常经典的问题:学习日期列被读成了 object 类型,而不是 datetime。很多新手忽略这一步,后面做月份聚合、按周排序时才发现,届时要么转换失败,要么排序完全错乱。我把这份数据的字段整理成了一张表格,便于后续对照:
| 字段名 | 含义 | 实际类型 | 注意事项 |
|---|---|---|---|
| user_id | 用户编号 | 整数 | 有少量重复记录 |
| course_id | 课程编号 | 字符串 | 类别多于 10 种 |
| study_date | 学习日期 | 字符串 | 含多种格式,需统一 |
| study_minutes | 学习时长 | 浮点数 | 存在 0 值和超大值 |
| completion_rate | 完成率 | 浮点数 | 范围 0~1,有缺失 |
| quiz_score | 测验分数 | 浮点数 | 目标变量,缺失比例约 8% |
| device_type | 设备类型 | 字符串 | 有别名脏值 |
| region | 地区代号 | 字符串 | 虚构代号,无需处理 |
2.2 开发环境与依赖清单
这个作业我用的是最常规的 Python 数据分析环境,Python 3.10,pandas 2.0,scikit-learn 1.3,Matplotlib 3.7,Seaborn 0.12。选择这些版本的原因很简单:稳定,资料多,遇到报错时网上能直接搜索到解决方案。我所有的代码都写在同一个 Notebook 里,但导包放在最前面,并且固定了随机种子random_state=42,保证每次运行结果一致。
import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from sklearn.model_selection import train_test_split, cross_val_score from sklearn.linear_model import LinearRegression, Ridge from sklearn.metrics import r2_score, mean_squared_error from sklearn.preprocessing import OneHotEncoder, StandardScaler plt.rcParams['figure.dpi'] = 120 sns.set_theme(style='whitegrid')固定随机种子这一点特别重要,作业里如果涉及随机划分数据,不固定种子的话,你每次重跑结果都会不同,最后报告里的数字就不稳定,容易被质疑结果不可复现。另一个小经验是:不要把绘图的字体、尺寸等设置在代码的中间,统一放开头,否则每次画图都要重新配置。
2.3 数据加载和初步检查代码
数据加载阶段我用了pd.read_csv(),但注意一个细节:原始 CSV 里可能有 BOM 头,如果不加encoding='utf-8-sig',第一列列名会出现\ufeff或者乱码。我在这次作业里没有遇到,但还是在代码注释里提醒自己。初步检查我写了这样一段代码:
df = pd.read_csv('student_learning_log.csv', encoding='utf-8-sig') print(df.shape) print(df.dtypes) print(df.isna().sum()) print(df.duplicated().sum())执行后我看到结果是 15000 行、8 列,其中缺失值比较集中,重复行有 37 条。这一段已经足够让我决定下一步清数据的方向:先删除重复行,再填充缺失值,最后处理异常值。处理顺序不能乱,如果先填充缺失再删重,可能会保留一些带着脏值的重复样本。
3. 核心实现流程:从脏数据到可用模型
3.1 数据清洗:缺失值、异常值和重复记录的处理
清洗这一步我分了四个动作,每个动作背后都有明确原因。
第一,删除完全重复行。重复数据会导致训练样本权重被放大,影响模型评估的客观性。我用df.drop_duplicates()直接删除,删完后检查发现只减掉了 37 行。
第二,统一学习日期格式。原始数据里混杂了2024/01/05、2024-01-05、2024.01.05三种格式。我用pd.to_datetime()转换,设置errors='coerce',转换失败的值变成NaT,再结合业务逻辑判断,比如日期为空或时间早于开课日期,直接剔除。
第三,处理学习时长的异常值。有些记录的学习时长是 0 分钟,有些高达 1440 分钟。测验分数和学习时长一起看,就会发现 0 分钟的学习记录里测验分数反而很高,这不符合业务逻辑,大概率是测试数据,我直接剔除。1440 分钟理论上可能是 24 小时不中断学习,但结合其他字段看,多是重复导入导致,我同样删除,避免拉偏模型。
第四,填充缺失值。完成率缺失比例约 5%,测验分数缺失比例约 8%。完成率用用户所在课程组的均值填充,测验分数作为目标变量,不能随便用全样本均值填充,否则会削弱预测意义。我采用的做法是用同一学习时长区间内的测验分数均值填充,虽然不完美,但比粗暴填 0 靠谱。
清洗结束后,我重新检查了数据量,从 14963 行降到 14120 行,缺失值全部处理完毕。整个过程都在 Notebook 中有记录,每一步删了多少行、为什么删,都在报告中附了表格。
3.2 探索性分析与基础可视化
清洗后的数据,首先要回答几个业务问题:学习时长和测验分数什么关系,完成率如何影响测验表现,不同设备类型之间有没有差异,星期几的学习行为有没有规律。
我画了五张图:学习时长的直方图、测验分数的直方图、学习时长与测验分数的散点图(加回归拟合线)、不同设备类型的平均测验分数柱状图、完成率与测验分数的联合分布图。这些图不用花哨,但每一张都要能解决一个具体问题。
观察散点图时我发现,学习时长和测验分数并不是简单的线性关系,在 0 到 60 分钟区间内,分数增长比较快;超过 90 分钟后,增长趋于平缓。这个现象提示我后续建模时不能只拿原始时长做特征,可以考虑把时长分箱,或者加一个时长平方项尝试二次项拟合。可视化不只是为了报告好看,它会给建模路线提线索,这是这次作业最重要的体验之一。
设备类型方面,我清理了别名脏值,比如mobile和Mobile被统一为mobile,pc和desktop统一为desktop。清理后的结果显示,使用移动设备学习的用户测验平均分略低,但标准差更大,说明个体差异明显。这个差别后面并没有显著提升模型效果,但可以在报告中作为业务洞察提一下。
3.3 特征工程和建模评估
建模前,我构造了三个新特征:从日期里提取 “星期几” 当作周期特征,从学习时段划分出上午、下午、晚上三个类别,以及把同一用户一周内的学习总时长聚合成周学习强度。设备类型做独热编码,地区字段因为属于虚构的高基类别,且样本量稀疏,我没有选入模型。
由于目标是连续变量,我选择了线性回归作为基线模型。划分数据集时用train_test_split按 7:3 切分,保持随机种子一致。预测目标为测验分数,特征集合包括:周学习强度、学习时长、完成率、星期几、学习时段、设备类型。因为特征量纲差异很大,我对数值特征做了标准化,避免模型训练时受大数值特征主导。
num_cols = ['study_minutes', 'completion_rate', 'weekly_study_minutes'] cat_cols = ['weekday', 'time_period', 'device_type'] X = df_clean[num_cols + cat_cols] y = df_clean['quiz_score'] X = pd.get_dummies(X, columns=cat_cols, drop_first=True) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42 ) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) model = LinearRegression() model.fit(X_train_scaled, y_train) pred = model.predict(X_test_scaled) print('R2:', r2_score(y_test, pred)) print('RMSE:', mean_squared_error(y_test, pred, squared=False))第一次跑出的基线模型 R² 只有 0.37,RMSE 是 12.8,对于学习行为预测而言,这个结果不算惊艳但能说明问题:测验分数受到很多不可观测因素影响,仅仅靠学习行为日志,很难预测得很准。这份作业的目的并不是让我们调出高分模型,而是让我们学会分析误差来自哪里、怎么改进。
为了看模型稳定性,我对训练集做了 5 折交叉验证,得到的 R² 均值为 0.35,和测试集结果接近,说明模型没有严重过拟合。这个步骤在报告里很重要,因为它证明了当前结果不是偶然形成的。
4. 实操中的典型问题与排查实录
4.1 时间字段为什么总是解析失败
处理时间字段时,我一开始直接用了pd.to_datetime(df['study_date']),结果报了好几条解析错误。原因很简单:原始数据里有空字符串、有2024/01/05 08:00这种带时间的格式,还有20240105这种纯数字格式。直接转当然会失败。后来我改成两步:
df['study_date'] = df['study_date'].astype(str).str.strip() df['study_date'] = pd.to_datetime(df['study_date'], errors='coerce')先转成字符串统一处理,再设置errors='coerce'让无法解析的值变成NaT,之后再检查NaT的占比并决定是否剔除。这个小改动节省了很多排查时间。
4.2 缺失值比例不高,但集中在一个分组怎么办
完成率的缺失比例虽然只有 5%,但我发现缺失并不是随机分布的:某些课程编号下缺失比例高达 20%。如果直接用整体均值填充,会让这些课程的完成率整体偏向均值,破坏数据的真实分布。
我的处理方式是分组填充,先按课程编号分组,再用组内中位数填充。这比全样本均值更合理,因为不同课程的学习强度本来就不同。如果你在做作业时遇到类似情况,不妨先画一个缺失值热力图,看看缺失是否集中在某些类别里,再选择填充策略。切忌拿到数据就开始fillna(df.mean())。
4.3 模型过拟合不明显,但正则化仍然值得试
基线模型的交叉验证和测试集 R² 接近,说明过拟合不明显,但我在调整特征组合时发现一个问题:加入过多高基数类别变量后,训练集 R² 提升明显,测试集却保持不变甚至下降。这就是轻微过拟合的信号。
我试用了岭回归,用GridSearchCV搜索一个比较合适的 alpha。因为特征已经标准化,岭回归的惩罚项才有一致的作用。代码如下:
from sklearn.model_selection import GridSearchCV param_grid = {'alpha': [0.1, 1, 10, 50, 100]} ridge = Ridge() grid = GridSearchCV(ridge, param_grid, cv=5, scoring='r2') grid.fit(X_train_scaled, y_train) print(grid.best_params_)搜索结果里最佳 alpha 是 10,但测试集 R² 只比普通线性回归高了 0.01。也就是说,正则化并没有带来质的提升。这时候就不要过度调参了,把结果如实写进报告,比硬凑一个好看的数字更有价值。
5. 复盘与改进空间:做完这份作业后的思考
5.1 这份作业到底训练了哪些能力
Wk.2_HW 看起来只是一个课内作业,但它设计得很聪明。它没有让你直接用现成的干净数据跑模型,而是逼着你经历一遍真实数据会遇到的繁琐问题:格式混乱、缺失值、异常值、类别字段脏值。这些内容在教程里都只是提一下,但实际动手时才知道每一步都会消耗时间。
更重要的是,它训练了“结果可解释”的能力。提交报告时,除了代码,还需要说明为什么删掉某些行、为什么选择某种填充策略、为什么模型效果一般。这些“为什么”才是区分作业完成度和理解深度的关键。如果你只是把代码跑通,没有解释决策过程,作业反馈大概率会建议你加强数据分析思维。
5.2 如果重做一遍,我会在哪些地方调整
重新回顾整个实现过程,我会在三个地方做改进。
第一,在特征工程阶段,不应该只做线性特征。从可视化的结果来看,学习时长和分数有明显的非线性趋势,我会尝试加入学习时长的平方项或者做分箱处理,看是否能提升模型表现。第二,预测目标可以换一个角度思考,与其直接预测分数,不如先预测用户能否完成学习任务,也就是把回归问题改成二分类,再在子集中预测分数,两阶段模型的业务可解释性更强。第三,代码组织方式可以再工程化一些,把数据清洗、特征工程、建模分别封装成函数,而不是全部平铺在 Notebook 单元格里,这样后期改参数时不会因为重跑顺序出错。
5.3 提交流程中的小技巧
最后分享一个实际提交过程中发现的小技巧:交作业前,把一个空的 Notebook 按报告逻辑重新跑一遍。什么意思呢?就是不要用自己写代码时的顺序,而是按照报告中的讲解顺序重新整理一遍代码块,确认每个变量在被引用前都已经被定义。很多人写着写着会跳到前面补充一个清洗步骤,导致后面的代码顺序依赖混乱。重新跑一遍能发现很多“我本地明明能跑”的问题。
另外一个技巧是,所有图表都要把轴标签和图例写清楚。作业评分和真实工作汇报一样,图里的信息需要让读者一眼读懂。我这次有张散点图包含回归拟合线,但没有标出图例,后来反馈里被提醒图例缺失。虽然只是小细节,却会影响整体观感。
做完 Wk.2_HW 之后,我最大的感受是:这种作业的名字虽然简单,但内容完全不像文件名那么随意。数据清洗的每一步都在积累经验,建模评估的每一个指标都在倒逼你理解模型行为。如果你正在做类似的作业,建议耐下心把每一段过程记录下来,重点写清楚“为什么选这个方案”而不是“我用了什么方案”。最后你会发现自己收获的不只是一个分数,而是一套完整的数据分析思路。