Easy-Vibe Task02学习笔记:从零跑通一个完整的机器学习小项目
花了一个周末把 Easy-Vibe 的 Task02 啃完了。这个任务跟 Task01 的纯环境准备和语法热身完全不一样,它直接把你丢进一个“小型但五脏俱全”的机器学习项目里:数据清洗、特征工程、模型训练、结果评估,整套流程要自己走一遍。这篇文章把我在 Task02 里的完整思路、实操步骤和踩坑记录整理出来,给正在做或准备做这个任务的同学一个参考。
Task02 的设计目标很明确:让你在不用啃完整本理论书的前提下,先亲手跑通一次“从原始数据到预测结果”的完整链路。它选了一个非常经典的入门场景——商品销量预测。为什么选这个场景?因为销量数据足够“脏”、足够真实,里面既有数值型字段(价格、库存),又有类别型字段(品类、门店区域),还有时间字段(销售日期),几乎把日常业务数据里最常见的坑都覆盖了一遍。等这个任务做完,你基本就掌握了 Pandas 做数据预处理、Scikit-learn 建模评估、Matplotlib 画可视化图表这一整套数据科学入门兵器。
如果你现在处于“Python 语法大概懂,但不知道学了能干嘛”的阶段,这个任务就是帮你把语法变成能力的那道坎。接下来我按自己的实际操作顺序,把整个 Task02 拆开讲清楚。
1. 任务背景拆解:Task02 到底在训练什么能力
1.1 Easy-Vibe 的学习设计逻辑
Easy-Vibe 的 Task 系列不是按知识点平铺的,而是按“能力渐进”来设计的。Task01 解决的是“跑得起来”的问题,也就是环境、依赖、基础语法;到了 Task02,目标直接切换成“用起来”。
我做完 Task02 之后的体会是,这个任务真正想训练的核心能力有三块:
第一块是“数据敏感性”。原始数据不会像教科书里那样干干净净等你去建模,它会有缺失、有异常、有格式不统一的情况。Task02 在数据里埋了不少这类问题,逼着你去处理,而不是跳过。第二块是“流程完整性”。从加载数据、清洗、特征处理到建模评估,每一步都要亲自动手,不能只跑别人的现成代码。第三块是“结果解释能力”。模型跑完不代表任务结束,你要能看懂评估指标,能画图说明模型效果,能回答“我这个模型到底靠不靠谱”。
这三块能力恰好对应了真实工作中数据科学岗位的日常:大多数时候你不是在调参,而是在处理脏数据、理解业务口径、把模型结果讲给别人听。
1.2 Task02 的选题为什么是“销量预测”
销量预测是业界最常见的机器学习入门项目选题,没有之一。原因很简单:它足够贴近生活,你不需要任何行业背景就能理解业务目标——根据历史销售数据预测未来某段时间的销量。同时,它的数据形态非常典型:有数值特征(价格、折扣率)、有类别特征(品类、门店)、有时间特征(日期、星期、月份),这让你在做特征工程时有足够的发挥空间。
从技术角度看,销量预测涉及的模型也很有代表性:线性回归适合做基线,决策树和随机森林能处理非线性关系,如果你愿意还可以尝试 XGBoost 或 LightGBM 看看效果差异。这种“同一个数据集,多种模型对比”的体验,对建立模型选择的直觉特别有帮助。
另外,销量预测还有一个隐性收益:它的评估指标非常直观。你不需要理解复杂的业务术语,RMSE 大就说明预测误差大,RMSE 小就说明预测比较准。这种即时的反馈感,对新手保持学习动力很重要。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据准备与预处理:建模前最容易被低估的一步
2.1 初始数据集长什么样
Task02 给的数据集是一个模拟的零售商店销售记录,包含大约 8500 条数据。每一行代表一条销售记录,核心字段包括销售日期、商品品类、销售价格、成本、销量、门店位置、库存量、促销标记等。
我的第一步永远是做“数据体检”,也就是用一行代码看整体概况:
python复制import pandas as pd
df = pd.read_csv('sales_data.csv')
print(df.info())
print(df.describe())
print(df.head())
df.info() 能告诉你每个字段的类型和非空数量,df.describe() 给出数值型字段的统计摘要。这一步大概 30 秒,但能帮你快速建立对数据集的整体感知:哪些字段缺失明显、哪些字段有异常极值、哪些字段需要做类型转换。
我做完体检后的初步发现是:价格字段有少量缺失值,库存字段有一个离谱的负值,日期字段被读成了字符串而不是时间类型。这些都是很典型的“真实数据”问题,需要逐一处理。
2.2 处理缺失值与异常值
缺失值的处理方案取决于缺失比例和字段的业务含义。Task02 数据集里价格字段缺失了大约 2%,这个规模的缺失对整体影响不大。我选择用中位数填充,而不是均值,原因是价格分布存在右偏,少数高价商品会拉高均值,用中位数更稳健。
python复制# 用中位数填充价格缺失值
price_median = df['price'].median()
df['price'].fillna(price_median, inplace=True)
异常值处理这块,我遇到的是库存字段出现了一个 -500 的值。库存不可能是负数,这大概率是录入错误。直接删除这一行会丢失一条完整的销售记录,但保留它又会影响后续特征计算。考虑到只有一条异常记录,直接删除是最省事且安全的做法。
日期字段的转换也是关键一步:
python复制df['date'] = pd.to_datetime(df['date'])
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['day_of_week'] = df['date'].dt.dayofweek
df['is_weekend'] = df['date'].dt.dayofweek >= 5
为什么要做这个转换?因为“日期”本身对模型来说不是一个有效数值,但“月份”“星期几”“是否周末”这些派生字段却有很强的规律性——周末销量明显高于工作日,这是零售数据的常识。把日期拆解成这些特征,模型才有机会学到这些规律。
2.3 特征编码与数值化
类别特征的处理是新手最容易忽略的环节。Task02 里的“商品品类”和“门店位置”都是字符串形式的类别字段,而 sklearn 里的模型只能处理数值。所以必须把文本变成数字。
我这里的处理方式是分情况使用两种编码:
对于“门店位置”这种没有天然顺序的类别,使用独热编码(One-Hot Encoding):
python复制df = pd.get_dummies(df, columns=['store_location'], drop_first=True)
drop_first=True 的作用是删掉第一个类别对应的哑变量。为什么要删?因为如果不删,K 个类别会生成 K 个哑变量,这 K 个哑变量之间存在完全共线性,会影响线性回归这类模型的稳定性。删掉一个后变成 K-1 个,既避免了共线性问题,又完整保留了信息。
对于“是否促销”这类本身就是二分类的字段,直接映射成 0 和 1 即可:
python复制df['promotion'] = df['promotion'].map({'yes': 1, 'no': 0})
这里要特别提醒一个容易出错的点:字符串里的空格。真实数据里经常出现 'yes ' 和 'yes' 不一致的情况,所以做映射前最好先执行 df['promotion'].unique() 查看一下有哪些不重复值,确保没有隐藏的空格或大小写差异。
3. 特征工程:真正拉开模型上限的地方
3.1 从原始字段里挖出有效特征
在 Task02 里,如果只用原始字段直接建模,模型效果会比较平庸。特征工程就是想办法从已有数据中挖掘出更多信息量。
我在这个任务里主要做了三类特征扩展:
第一类是时间维度特征。除了前面提到的年月日和星期几,我还加了“是否为月初”“是否为月底”这类很有意义的业务特征。零售业的常识是,月初和月底往往是发薪日附近的消费高峰期,销量会有周期性波动。这些特征加起来,模型就能捕捉到不同时间粒度上的周期性。
第二类是价格相关特征。我构造了“毛利率”字段,用 (价格 - 成本) / 价格 计算。为什么毛利率比单纯的价格或成本更有用?因为毛利率反映了产品利润空间,而利润空间的商品往往有更多促销资源支持,销量表现也会不同。模型学习“毛利率”会比分别学习“价格”和“成本”更容易抓住这个规律。
第三类是数据偏态处理。销量字段本身可能严重右偏,少数爆款商品的销量会拉高均值。这种偏态分布会让模型的误差在大数值样本上被放大。通常做法是做对数变换:
python复制import numpy as np
df['log_sales'] = np.log1p(df['sales'])
训练时用 log_sales 作为目标变量,预测后再用 np.expm1() 还原成真实销量。这个小技巧在很多竞赛项目里都是标配操作。
3.2 相关性分析与多重共线性
特征不是越多越好,特征之间如果高度相关,反而会引入噪声和共线性问题。所以在把全部特征交给模型之前,我先做了一次相关性分析。
python复制import seaborn as sns
import matplotlib.pyplot as plt
corr_matrix = df.corr()
plt.figure(figsize=(12, 10))
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm')
plt.show()
相关性热力图的作用有两个:一是看特征与目标变量(销量/对数销量)的相关性,识别出哪些特征最有预测力;二是看特征之间的相关性,如果两个特征相关系数超过 0.8,就建议只保留其中一个,避免共线性问题。
我在这个步骤里发现“价格”和“成本”的相关系数高达 0.85。这其实符合直觉——价格通常是在成本基础上加成定的。于是我在后续建模时去掉了“成本”字段,只保留“价格”和“毛利率”。这样既减少了冗余,又避免了线性回归模型因共线性导致的系数不稳定问题。
4. 模型训练与评估:跑通流程比调参重要
4.1 从基线模型开始
Task02 的精髓之一,是它反复强调“先跑通一个最简模型,再考虑优化”。很多新手一上来就想着用高级模型,结果被各种配置问题卡住,反而连最基础的框架都没学会。
我的做法是先建一个只用数值特征的线性回归模型作为基线。不做过多的特征处理,不调任何超参数,先把代码链路跑通,得到一个“最差但合理”的结果,作为后续所有优化的参照物。
python复制from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
features = ['price', 'stock', 'month', 'day_of_week', 'is_weekend', 'promotion']
X = df[features]
y = df['log_sales']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
rmse = mean_squared_error(y_test, y_pred, squared=False)
print(f'Baseline RMSE: {rmse:.4f}')
这个基线模型跑出来后,RMSE 大概是 0.58 左右。这个数字先记着,后面所有优化的目标都是降低这个值。如果你一开始就做复杂的特征工程和模型调优,就很难判断到底是哪个环节带来了提升。
4.2 评估指标要怎么理解
Task02 里用的评估指标主要是 RMSE(均方根误差)。我刚开始学的时候总觉得 RMSE 不如准确率那么直观,后来慢慢理解了它的含义:RMSE 衡量的是预测值与真实值之间的平均偏差,单位跟目标变量一致。
因为我这里是对数变换后的销量做的建模,所以 RMSE = 0.58 的含义是:在对数空间上,预测值与真实值平均偏差 0.58。换算回真实销量,大概是 expm1(0.58) 约等于 0.79,也就是说平均偏差在 79 件商品左右。这个数字具体大不大,要看业务背景。对一个小型门店的日均销量来说,这个误差显然还有优化空间。
这也引出一个重要的认知:评估指标不是越小越好,而是要在业务语境下有可解释性。脱离业务谈指标,没有任何意义。
4.3 模型对比与交叉验证
基线跑通之后,我把模型从线性回归换成了随机森林,效果立刻有明显提升。随机森林能捕捉非线性关系,对多特征交互(比如“周末 + 促销 + 特定品类”)的学习能力比线性回归强很多。
python复制from sklearn.ensemble import RandomForestRegressor
rf_model = RandomForestRegressor(n_estimators=200, max_depth=10, random_state=42)
rf_model.fit(X_train, y_train)
y_pred_rf = rf_model.predict(X_test)
rmse_rf = mean_squared_error(y_test, y_pred_rf, squared=False)
print(f'Random Forest RMSE: {rmse_rf:.4f}')
同一套特征,随机森林的 RMSE 降到了 0.38 左右。这个提升幅度非常可观,也直观地说明了一个道理:模型选型有时候比调参更重要。
但只用一次训练-测试划分来对比模型是冒险的。随机森林在这个划分下效果好,不代表它对数据整体就有稳定的泛化能力。所以我又加了一个 5 折交叉验证,把数据分成 5 份,轮流用 4 份训练、1 份验证,最终取 5 次结果的平均值,这样评估结果更可信。
python复制from sklearn.model_selection import cross_val_score
scores = cross_val_score(rf_model, X, y, cv=5, scoring='neg_root_mean_squared_error')
print(f'CV RMSE: {-scores.mean():.4f} (±{scores.std():.4f})')
交叉验证的另一个价值是看方差。如果 5 折的结果标准差很大,说明模型在不同数据子集上表现波动明显,可能存在过拟合或数据分布不均匀的问题。我在这个任务里计算出来的标准差在 0.05 左右,属于可接受的范围。
4.4 特征重要性分析
随机森林有一个线性回归不具备的优势:可以直接输出特征重要性。这能帮你理解“模型到底靠哪些特征做判断”。
python复制importance_df = pd.DataFrame({
'feature': X.columns,
'importance': rf_model.feature_importances_
}).sort_values('importance', ascending=False)
print(importance_df)
从重要性排序来看,stock(库存)、price(价格)、month(月份)排在前三位。这个结果符合业务直觉:库存直接影响可售量,价格影响需求,月份反映季节性波动。反而是“是否周末”这个特征的重要性偏低,说明在这个数据集里,季节性和价格因素比每周的周期性更重要。
特征重要性还有一个更实用的用途:如果某些特征的权重极低(比如小于 0.01),可以尝试直接删掉它们,再用精简特征集训练,有时反而能减少过拟合、提升泛化性能。
5. 常见问题与排查技巧实录
Task02 做完之后,我把过程中遇到的典型问题整理成了一张速查表,方便以后自己回看,也供正在做这个任务的同学排查参考。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
df.info() 显示日期字段是 object 类型 |
没做时间类型转换 | 用 pd.to_datetime() 转换后重新派生时间特征 |
| 类别字段无法传入模型 | 字符串没有编码为数值 | 用 pd.get_dummies() 或 LabelEncoder 处理 |
| 模型评分出现 NaN | 特征中有缺失值或无穷值 | 检查 df.isna().sum(),填充或删除 |
| 训练集效果很好但测试集很差 | 过拟合,特征过多或模型复杂度过高 | 增加交叉验证,尝试剪枝参数调整,删除低重要性特征 |
| 价格字段读出来有乱码 | 数据文件编码不匹配 | 读取时指定 encoding='utf-8' 或 'gbk' 重试 |
| 预测结果出现负数 | 对 log_sales 预测后忘了还原 |
用 np.expm1() 还原后再检查值域 |
| 独热编码后特征数爆炸 | 类别字段基数过高 | 改用 LabelEncoder 或先对低频类别做合并 |
5.1 我踩过的三个坑
第一个坑是日期格式化问题。Task02 数据里的日期格式是 2023/5/3 而不是 2023-05-03,pd.to_datetime() 如果不指定格式也能自动解析,但在遇到混合格式时会慢很多甚至报错。后来我查了一下,最快的做法是读取时直接指定 parse_dates=['date'] 参数,让 Pandas 在加载阶段就完成日期解析,后续就不用手动转换了。
第二个坑是独热编码后列名变化导致模型输入不一致。我在训练时用了 pd.get_dummies(),得到的列名是 store_location_A、store_location_B 这种带前缀的。但在预测新数据时,如果新数据里只有 store_location_A 而没有 B,列就对齐不上,模型直接报错。解决办法是训练时把列名保存下来,预测前用 X_test = X_test.reindex(columns=training_columns, fill_value=0) 对齐列。
第三个坑是交叉验证的分层问题。刚开始我直接用 cross_val_score 做随机森林的交叉验证,结果每折的 RMSE 波动很大。后来发现是数据里不同“品类”的数量不均衡,某些折里可能正好没有销量最高的品类样本。解决办法是把交叉验证改成按品类分层抽样,具体做法是在分类问题中用 StratifiedKFold,回归问题中可以先离散化目标变量再分层,或者直接用 GroupKFold 按品类分组。
5.2 一个关于数据泄露的提醒
Task02 的样例数据比较简单,但有一个点值得所有做数据科学的朋友注意:数据泄露。所谓数据泄露,就是在训练时不小心把“未来信息”混进了特征里。
具体到这个任务,如果你在构建特征时用了“全量数据的销量均值”或“全量数据的某种统计量”去填充,那实际上就已经把测试集的信息暴露给了模型。这样训练出来的模型在测试集上表现很好,但在真实场景中会崩得很惨,因为真实场景里你不会有未来数据的统计量。
正确处理方式是在训练集上计算填充值,然后把这个值应用到测试集上。比如训练集销量中位数是 20,那测试集的缺失值也统一用 20 填充,而不是测试集单独算一个中位数。这个细节看似微小,但对模型泛化能力的影响极大。
6. 一些个人体会和后续扩展
Easy-Vibe Task02 做完,最直观的感受是:以前看别人写的建模教程觉得自己都懂了,但真正自己动手做一遍才发现有一百个小问题等着你。每一步都很“琐碎”,但正是这些琐碎构成了真实的数据科学工作。
如果你完成了 Task02,还能从几个方向继续发挥这个数据集的价值:
第一是尝试用 XGBoost 或 LightGBM 替代随机森林,对比 GBDT 系列模型在同一数据上的表现差异。这一步能帮你理解不同“树模型”家族之间的差异。第二是对“促销标记”做更细粒度的特征工程,比如把“促销力度 = 原价 - 促销价”作为一个新特征,往往能带来意想不到的增益。第三是尝试做一个简单的时序分析,按周汇总销量后看趋势和季节性,再引入滞后特征(如“上周同期销量”)来预测本周销量。
我个人在反复跑这个任务之后最大的体会是:特征工程的价值往往大于调参。我用默认参数的随机森林,只要特征做得好,其 RMSE 就远好于精心调参的线性回归。这背后的逻辑也很简单——模型只是在现有信息里寻找规律,如果信息本身不够,再复杂的模型也倒不出来。
Task02 对我而言最大的意义,不在于学会了某个具体的算法,而在于建立了“数据问题 -> 处理方案 -> 建模评估 -> 结果解读”的完整思维框架。这个框架以后不管换什么数据集、什么业务场景,都能复用。把这个思路打开,后续任务里的猫腻基本就藏不住了。
