简介:这是一份面向计算机、数据科学及相关专业学习者与从业者的Kaggle入门实战资源,围绕城市自行车共享系统使用状况展开数据分析与需求预测。项目基于赛题数据完成探索性分析与建模,借助Python脚本和Jupyter Notebook实现共享单车租赁量预测,适合作为课程设计、毕业设计或算法入门练习。压缩包共8个文件,其中数据文件3个、Python脚本2个、Notebook笔记2个、说明文档1份,整体仅823KB,结构紧凑,便于快速下载与本地复现。资源目前已有520人学习浏览,内容覆盖数据读取、特征探索、神经网络建模及结果可视化等环节,并配有项目说明文档,可帮助读者理解从数据清洗到预测输出的完整流程。对于希望快速上手机器学习项目、积累Kaggle竞赛经验或完成相似课题的同学,这份资源提供了可直接运行的代码与清晰的思路说明。
1. 城市自行车共享系统分析预测:入门 Kaggle 的第一道完整闭环
很多人拿到类似“python源码+项目说明”的压缩包后,第一反应是直接跑一遍脚本,看到打印出来的分数就开始惆怅:分数到底是什么意思?换几个参数还能不能涨?这个标题指向的项目,正好是 Kaggle 入门里少见的“数据量适中、业务含义清晰、评价指标明确”的回归任务:预测城市公共自行车系统按小时计的租借数量 count。它不需要图像、不需要 GPU、不需要分布式,只需要你熟悉 pandas 和 sklearn 就能走通从 csv 到提交文件的完整流程。这篇文章不逐行念源码包里的代码,而是讲清楚一个做数据挖掘的人拿到这个题后会怎么做——特征怎么构造、验证集怎么切、哪些参数值得调、哪些细节最容易让新手翻车。
2. 读懂字段与业务含义:预测目标到底是什么
2.1 先看字段表:一条租车记录里藏了哪些信息
解压资源包后,一般会看到数据文件 train.csv、test.csv 以及项目说明文档。train.csv 每一行是一条小时级的租借记录,字段表如下:
| 字段 | 取值范围 | 含义 |
|---|---|---|
| datetime | 2011-01-01 00:00 起逐小时 | 租借时刻 |
| season | 1~4 | 春、夏、秋、冬 |
| holiday | 0/1 | 是否节假日 |
| workingday | 0/1 | 是否工作日 |
| weather | 1~4 | 天气等级,1 最好,4 最差 |
| temp | 归一化到 0~1 | 实际温度 |
| atemp | 归一化到 0~1 | 体感温度 |
| humidity | 0~100 | 相对湿度 |
| windspeed | 归一化到 0~1 | 风速 |
| casual | 非负整数 | 临时注册用户租借量 |
| registered | 非负整数 | 正式注册用户租借量 |
| count | 非负整数 | 总租借量 = casual + registered |
test.csv 里没有 casual、registered 和 count,这三列中 count 才是你的预测目标,casual 和 registered 在测试集里根本不存在,建议直接丢弃而不是拿来做特征。很多人第一次犯的错就是想把 casual 和 registered 也当特征送进模型,结果训练集和测试集特征不一致,直接报错。
2.2 画一下 count 的分布:为什么评价指标偏偏用 RMSLE
这个项目的官方评价指标是 RMSLE,即 Root Mean Squared Logarithmic Error。为什么要用对数?先画一下 count 的分布就明白了。
import pandas as pd import numpy as np import matplotlib.pyplot as plt df = pd.read_csv('train.csv') df['datetime'] = pd.to_datetime(df['datetime']) fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].hist(df['count'], bins=50, edgecolor='white') axes[0].set_title('count 原始分布') axes[1].hist(np.log1p(df['count']), bins=50, edgecolor='white') axes[1].set_title('log1p(count) 分布') plt.show()原始 count 严重右偏,大量低租借量样本集中在 0~100,少数极端值冲到 900 以上。对这种分布直接算 RMSE,模型会拼命拟合那几条大值尾巴,而忽略大多数普通时段。取 log1p 之后分布近似正态,模型会更均衡。log1p 就是 log(1 + x),加 1 是为了避免 log(0) 报错,因为租借量可以取 0,而 log(0) 是负无穷。
所以整个建模主线应该是:对训练集的目标 count 做 log1p 变换,模型预测的是对数空间的值,提交前再用 expm1 还原成真实租借量。RMSLE 天然惩罚“低估”比“高估”更狠,因此宁可预测略高一点,也不要预测为 0,这一点对后面处理极端天气时段很关键。
2.3 时间序列数据不能随机切分:按最后 21 天做验证集
train.csv 的时间跨度是逐小时连续的,日期本身有强趋势和周期性。如果用 sklearn 默认的 train_test_split 随机切分,模型会“偷看”未来数据,验证集分数很好看,一旦提交到测试集就现原形。常见做法是先把 datetime 排序,再按时间顺序切出最后 21 天作为验证集。
df = df.sort_values('datetime').reset_index(drop=True) split_point = df['datetime'].max() - pd.Timedelta(days=21) train_part = df[df['datetime'] <= split_point] valid_part = df[df['datetime'] > split_point] print(train_part.shape, valid_part.shape)注意 reset_index(drop=True) 这一步不能省,否则后面索引错位会让你 debug 到怀疑人生。21 天是一个自然周期数,包含三周的工作日与周末,覆盖了 weekday 和 hour 的全部组合,验证结果比较可信。
3. 特征工程与基线模型:从 datetime 里挖出有效特征
3.1 拆解 datetime:hour、weekday、month、year 一个都不能少
原始数据只有一个 datetime 字符串,但自行车租借量对“几点钟、星期几、几月份”极其敏感,这些信息不会自己变成列,必须手动拆。这里用 pandas 的 dt 访问器一步到位:
def parse_time(df): df = df.copy() dt = pd.to_datetime(df['datetime']) df['hour'] = dt.dt.hour df['weekday'] = dt.dt.weekday df['month'] = dt.dt.month df['year'] = dt.dt.year return df train_part = parse_time(train_part) valid_part = parse_time(valid_part)hour 的取值范围是 0~23,代表一天中的小时,这是最重要的周期性特征;weekday 的 0 是周一、6 是周日;month 代表季节内的月份趋势;year 代表整体增长趋势,因为城市共享单车系统在按年扩张,租借量一年比一年高。把这四个字段加进特征列表,再用随机森林跑一轮基线,基本能拿到一个不错的初分。
不要试图把 datetime 本身当数值特征送进模型,它的时间戳数值巨大且无单调语义,对树模型只会造成无意义的分裂。我习惯的做法是拆出这四个字段后,把原始的 datetime 列保留在 DataFrame 里用于最后对齐,但绝不选入特征集。
3.2 业务特征:早晚高峰标记与极端天气合并
只靠拆时间还不够,因为租借量背后是通勤行为。工作日的早高峰 7~9 点和晚高峰 17~19 点会形成两个明显尖峰,而周末的高峰出现在午后。直接构造一个 rush_hour 标记能把树模型的分裂效率提一大截:
def make_business_features(df): df = df.copy() df['rush_hour'] = ( (df['workingday'] == 1) & df['hour'].isin([7, 8, 9, 17, 18, 19]) ).astype(int) df['weekend_afternoon'] = ( (df['workingday'] == 0) & df['hour'].between(12, 16) ).astype(int) df['weather'] = df['weather'].replace(4, 3) return dfweather 字段的含义是天气等级,4 代表暴雨、暴雪等极端天气。但这个等级在训练集里样本极少,模型学不到规律,反而会被它带偏。常见做法是把 4 合并到 3,统一当成“差天气”处理。temp 和 atemp 高度相关,可以二选一,也可以保留让树模型自己决定,随机森林对冗余特征并不敏感。
还有一个容易被忽略的点:humidity 和 windspeed 有时会有极端值,比如 windspeed 为 0 不代表无风,而是该小时风速低于某个阈值被归零,不需要做缺失填充。把这些业务特征加上后,特征列表大概长这样:
feature_cols = [ 'season', 'holiday', 'workingday', 'weather', 'temp', 'atemp', 'humidity', 'windspeed', 'hour', 'weekday', 'month', 'year', 'rush_hour', 'weekend_afternoon' ]3.3 基线模型:随机森林和自定义 RMSLE 评估函数
特征准备好了,先别上花哨模型,用随机森林跑一个基线。随机森林对特征尺度不敏感,不用归一化,能直接处理数值和类别特征,是这类表格型任务最好的起步模型。目标值记得做 log1p:
from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error def rmsle(y_true, y_pred): y_pred = np.clip(y_pred, 0, None) return np.sqrt( mean_squared_error(np.log1p(y_true), np.log1p(y_pred)) ) model = RandomForestRegressor( n_estimators=200, max_depth=None, min_samples_leaf=1, random_state=42, n_jobs=-1, ) model.fit(train_part[feature_cols], np.log1p(train_part['count'])) pred_log = model.predict(valid_part[feature_cols]) pred = np.expm1(pred_log) print('RMSLE:', rmsle(valid_part['count'], pred))这里有几个参数需要说明。n_estimators=200 是起步值,之后可以加大到 500 甚至 1000 看分数是否继续下降;max_depth=None 表示让树完全生长,随机森林本身靠样本和特征随机性控制过拟合,不需要像 GBDT 那样限制深度;min_samples_leaf=1 表示叶子节点不设最小样本数,在入门阶段先用默认值,过拟合风险交给后验验证集来判断;random_state=42 保证每次运行结果一致;n_jobs=-1 让所有 CPU 核心并行训练。rmsle 函数里先用 clip 把负预测值切成 0,再取 log1p,这一步防止预测出负租借量导致 log 报错。
4. 交叉验证与调参:三个必调参数和一套有效流程
4.1 时间序列交叉验证:为什么 KFold 在这里不适用
很多人用随机森林调参时习惯直接套 GridSearchCV 加 KFold,但在这种逐小时连续的数据上,KFold 随机打乱会造成数据泄露,验证分数会虚高。正确做法是用 TimeSeriesSplit,它按时间顺序切出递增的训练窗口:
from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) scores = [] for train_idx, valid_idx in tscv.split(df): X_tr = df.iloc[train_idx][feature_cols] y_tr = np.log1p(df.iloc[train_idx]['count']) X_va = df.iloc[valid_idx][feature_cols] y_va = np.log1p(df.iloc[valid_idx]['count']) model = RandomForestRegressor( n_estimators=200, max_depth=None, min_samples_leaf=1, random_state=42, n_jobs=-1, ) model.fit(X_tr, y_tr) pred = np.expm1(model.predict(X_va)) scores.append(rmsle(np.expm1(y_va), pred)) print('各折 RMSLE:', np.round(scores, 4)) print('平均 RMSLE:', round(np.mean(scores), 4))TimeSeriesSplit 的 n_splits=5 会生成 5 组训练集和验证集,训练集一次比一次大,验证集始终在训练集之后,严格保持时间顺序。这比手动切一个验证集更稳,因为你可以在 5 段不同时期上验证模型稳定性,避免某一时期的天气异常造成的偶然偏差。
4.2 三个必调参数:影响最直接的超参数
随机森林在这个项目上真正值得调的超参数就是下面三个,其他参数在入门阶段动了也白动。
| 参数 | 建议范围 | 作用与调整方向 |
|---|---|---|
| n_estimators | 100~1000 | 树的数量。越多越稳,但收益递减,调到分数不再变化即可 |
| max_depth | 10~30 | 树的最大深度。限制深度可以抑制过拟合,数据量大时效果明显 |
| min_samples_leaf | 2~10 | 叶子节点最少样本数。调大能让模型更平滑,对抗噪声 |
调整顺序我一般这么来:先固定 n_estimators=200,扫一遍 max_depth 观察验证分数;然后固定最优 depth,扫 min_samples_leaf;最后加大 n_estimators 到分数不再明显下降。不要一上来就 GridSearchCV 把所有参数一起搜,网格搜索的组合爆炸会让你等上几个小时还未必有结果。
4.3 手动调参循环:一份可以照着跑的模板
以下是我常用的一组手动调参代码,重点看循环结构和输出方式,你可以直接套用:
depth_list = [10, 15, 20, 25, None] for depth in depth_list: scores = [] for train_idx, valid_idx in tscv.split(df): X_tr = df.iloc[train_idx][feature_cols] y_tr = np.log1p(df.iloc[train_idx]['count']) X_va = df.iloc[valid_idx][feature_cols] y_va = np.log1p(df.iloc[valid_idx]['count']) model = RandomForestRegressor( n_estimators=200, max_depth=depth, min_samples_leaf=5, random_state=42, n_jobs=-1, ) model.fit(X_tr, y_tr) pred = np.expm1(model.predict(X_va)) scores.append(rmsle(np.expm1(y_va), pred)) print(f'depth={depth}: 平均 RMSLE={np.mean(scores):.4f}')跑完这组循环,你会看到 max_depth 从 10 加到 20 时分数明显下降,之后趋于平稳甚至反弹,那个拐点就是当前数据量下的最优深度。min_samples_leaf 的扫描同理,把上面代码里的 depth_list 换成 [1, 2, 5, 10] 即可。
纯手动循环看起来土,但这正是这个项目最值得做的部分:你能直观看到每个参数改变后分数怎么变,建立起“参数-性能”的直觉。等这套流程跑熟了,再去用 Optuna 或者 GridSearchCV 自动化也不迟。
4.4 模型融合:把随机森林和梯度提升树加权平均
单模型调到一定程度后分数会卡住,常见做法是引入第二个模型梯度提升树 GBDT,和随机森林做加权平均。GBDT 对异常值和噪声更敏感,但往往能捕捉到随机森林忽略的局部模式。
from sklearn.ensemble import GradientBoostingRegressor model_gbdt = GradientBoostingRegressor( n_estimators=300, max_depth=6, learning_rate=0.05, min_samples_leaf=5, random_state=42, ) model_rf = RandomForestRegressor( n_estimators=500, max_depth=20, min_samples_leaf=5, random_state=42, n_jobs=-1, ) model_rf.fit(X_tr, y_tr) model_gbdt.fit(X_tr, y_tr) pred_rf = np.expm1(model_rf.predict(X_va)) pred_gbdt = np.expm1(model_gbdt.predict(X_va)) pred_blend = 0.6 * pred_rf + 0.4 * pred_gbdt权重 0.6/0.4 是根据两个模型在验证集上的单模型分数估算的,随机森林一般略优,所以权重大一些。融合后 RMSLE 通常能再降 0.01~0.02,别小看这点提升,竞赛排名往往就差在这。GBDT 的训练速度比随机森林慢很多,调参时要有点耐心,learning_rate 调小后 n_estimators 必须加大才能保证效果。
5. 常见问题与避坑:5 个让分数翻车的细节
5.1 随机划分验证集导致分数虚高,提交后大跌
现象:用 train_test_split 随机切分,验证集 RMSLE 只有 0.6 左右,开心地提交,线上分数直接涨到 1.0 以上。
原因:这是时间序列数据,随机切分让模型在训练时看到了验证集时间段的信息,尤其是节假日和天气模式,模型等于开卷考试。
解决:统一用 TimeSeriesSplit 或按时间手动切分,验证分数才有参考意义。拿这个项目练手时,我吃过一次亏,之后凡是带时间列的数据,我默认先按时间排序再切分,再也不信任随机划分的结果。
5.2 log 变换与 expm1 还原不对称导致预测值出现负数
现象:验证集上计算 RMSLE 时报错,提示 log 遇到负值,检查发现预测值里有负数。
原因:模型在 log1p 空间训练,但预测时忘了还原,或者在还原前没有做 clip。极端天气下,模型可能预测出很小的值,expm1 还原后接近 0,但偶尔会有负值。
解决:在自定义 RMSLE 里先 np.clip(y_pred, 0, None),提交前同样对所有预测值做 clip。这个细节不处理,轻则报错,重则提交文件里出现负数租借量,直接得零分。
5.3 season 和 month 信息高度重叠,特征重要性严重重复
现象:模型输出特征重要性排行榜,season 排在前面,但把它删掉后分数几乎不变。
原因:season 只有 1~4 四个取值,本质上就是从 month 粗粒度分出来的,两个字段携带的信息大部分重叠。树模型会把特征在不同深度重复使用,造成冗余。
解决:保留 month 删除 season,或者保留 season 做 one-hot 删除 month,二选一。我试过同时保留,RMSLE 没有改善,还多了一列无意义的特征,不如直接删掉 season 让模型把精力放在 hour 和 weekday 上。
5.4 weather=4 的样本极少,模型把极端天气当噪声
现象:weather 等级为 4 的时段预测严重偏高,明明下暴雨,模型还预测出两三百的租借量。
原因:整个训练集里 weather=4 的样本寥寥无几,树模型在分裂时几乎不会选中这个条件,等于完全忽视了极端天气。
解决:先把 weather=4 合并到 3,保证每个类别都有足够样本。同时可以额外构造一个 bad_weather 二元特征,把 3 和 4 标为 1,让模型至少知道这种天气对租借量有明显的抑制作用。
5.5 周末和工作日的行为规律完全不同,一个模型吃不下两种模式
现象:验证集上工作日时段的预测误差大,周末时段的预测也有系统性偏差,模型整体分数卡在 0.7 上不去。
原因:工作日租借量主要来自通勤,早晚高峰极陡;周末是休闲骑行,高峰午后平缓。把这两类模式塞进同一个模型,树模型需要多学一倍的分裂规则才能勉强兼顾。
解决:构造 hour 与 workingday 的交互特征 hour_workingday,把工作日的 24 小时和周末的 24 小时编码成 48 个取值,让树模型直接按“周几+几点”分裂。更进一步的做法是分别建模,两个模型各管一类,但会对特征工程和调参工作量翻倍,入门阶段先做交互特征就够用了。
6. 进阶技巧与验证:用预测结果反推业务洞察
模型调完不是终点,拿验证集上的预测结果做一次业务向的分析,才能体现这个项目的完整价值。按 weekday 和 hour 分组,对比真实值与预测值的平均租借量,可以直观看出模型在哪类时段上不够准,也能得出一些有意思的结论:
result = valid_part[['datetime', 'count']].copy() result['pred'] = pred result['weekday'] = result['datetime'].dt.weekday result['hour'] = result['datetime'].dt.hour summary = result.groupby(['weekday', 'hour'])[['count', 'pred']].mean() summary['error'] = summary['pred'] - summary['count'] worst_hours = summary['error'].abs().sort_values(ascending=False).head(5) print(worst_hours)从分组表里我很容易发现,模型在早晚高峰的预测普遍偏低,因为高峰时段受天气影响波动大;反而是平峰时段预测很稳。这个结论反过来指导我调参:与其再死磕全局参数,不如去优化高峰时段特征。我还养成了一个习惯,每次调完参都做一次这类分组验证,而不是只看一个总分,这样模型出了问题能立刻定位到具体时间段。另一个提交前的细节是索引对齐,test.csv 的 datetime 顺序不能动,预测结果按行号写回去就行,别做任何重排。这个项目跑完,你对 pandas 操作、特征工程、交叉验证、模型融合这套流程就有了整套手感,再去做其他表格类竞赛,骨架都是现成的。希望帮到你。
本文还有配套的精品资源,点击获取