简介这是一份面向计算机、数据科学及相关专业学习者与从业者的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 每一行是一条小时级的租借记录字段表如下字段取值范围含义datetime2011-01-01 00:00 起逐小时租借时刻season1~4春、夏、秋、冬holiday0/1是否节假日workingday0/1是否工作日weather1~4天气等级1 最好4 最差temp归一化到 0~1实际温度atemp归一化到 0~1体感温度humidity0~100相对湿度windspeed归一化到 0~1风速casual非负整数临时注册用户租借量registered非负整数正式注册用户租借量count非负整数总租借量 casual registeredtest.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], bins50, edgecolorwhite) axes[0].set_title(count 原始分布) axes[1].hist(np.log1p(df[count]), bins50, edgecolorwhite) 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(dropTrue) split_point df[datetime].max() - pd.Timedelta(days21) train_part df[df[datetime] split_point] valid_part df[df[datetime] split_point] print(train_part.shape, valid_part.shape)注意 reset_index(dropTrue) 这一步不能省否则后面索引错位会让你 debug 到怀疑人生。21 天是一个自然周期数包含三周的工作日与周末覆盖了 weekday 和 hour 的全部组合验证结果比较可信。3. 特征工程与基线模型从 datetime 里挖出有效特征3.1 拆解 datetimehour、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 评估函数特征准备好了先别上花哨模型用随机森林跑一个基线。随机森林对特征尺度不敏感不用归一化能直接处理数值和类别特征是这类表格型任务最好的起步模型。目标值记得做 log1pfrom 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_estimators200, max_depthNone, min_samples_leaf1, random_state42, 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_estimators200 是起步值之后可以加大到 500 甚至 1000 看分数是否继续下降max_depthNone 表示让树完全生长随机森林本身靠样本和特征随机性控制过拟合不需要像 GBDT 那样限制深度min_samples_leaf1 表示叶子节点不设最小样本数在入门阶段先用默认值过拟合风险交给后验验证集来判断random_state42 保证每次运行结果一致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_splits5) 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_estimators200, max_depthNone, min_samples_leaf1, random_state42, 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_splits5 会生成 5 组训练集和验证集训练集一次比一次大验证集始终在训练集之后严格保持时间顺序。这比手动切一个验证集更稳因为你可以在 5 段不同时期上验证模型稳定性避免某一时期的天气异常造成的偶然偏差。4.2 三个必调参数影响最直接的超参数随机森林在这个项目上真正值得调的超参数就是下面三个其他参数在入门阶段动了也白动。参数建议范围作用与调整方向n_estimators100~1000树的数量。越多越稳但收益递减调到分数不再变化即可max_depth10~30树的最大深度。限制深度可以抑制过拟合数据量大时效果明显min_samples_leaf2~10叶子节点最少样本数。调大能让模型更平滑对抗噪声调整顺序我一般这么来先固定 n_estimators200扫一遍 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_estimators200, max_depthdepth, min_samples_leaf5, random_state42, 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(fdepth{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_estimators300, max_depth6, learning_rate0.05, min_samples_leaf5, random_state42, ) model_rf RandomForestRegressor( n_estimators500, max_depth20, min_samples_leaf5, random_state42, 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 weather4 的样本极少模型把极端天气当噪声现象weather 等级为 4 的时段预测严重偏高明明下暴雨模型还预测出两三百的租借量。原因整个训练集里 weather4 的样本寥寥无几树模型在分裂时几乎不会选中这个条件等于完全忽视了极端天气。解决先把 weather4 合并到 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(ascendingFalse).head(5) print(worst_hours)从分组表里我很容易发现模型在早晚高峰的预测普遍偏低因为高峰时段受天气影响波动大反而是平峰时段预测很稳。这个结论反过来指导我调参与其再死磕全局参数不如去优化高峰时段特征。我还养成了一个习惯每次调完参都做一次这类分组验证而不是只看一个总分这样模型出了问题能立刻定位到具体时间段。另一个提交前的细节是索引对齐test.csv 的 datetime 顺序不能动预测结果按行号写回去就行别做任何重排。这个项目跑完你对 pandas 操作、特征工程、交叉验证、模型融合这套流程就有了整套手感再去做其他表格类竞赛骨架都是现成的。希望帮到你。本文还有配套的精品资源点击获取