Kaggle共享单车预测实战:从数据清洗到特征工程的完整流程

发布时间:2026/10/12 15:41:14
Kaggle共享单车预测实战:从数据清洗到特征工程的完整流程
简介这是一份面向数据科学初学者的Kaggle入门项目围绕城市自行车共享系统使用状况数据完成从数据探索、特征工程到需求预测的完整分析流程。资源适合计算机、数据科学与大数据技术、人工智能、物联网等专业的学生用于课程设计、毕业设计或初期演示也适合希望快速上手实战的算法初学者。压缩包共8个文件包含3个CSV格式的数据集、2个Python源码脚本、2个Jupyter Notebook交互式分析文档和1份Markdown说明整包仅823KB轻量但结构完整。其中Python脚本实现了可直接运行的预测管线Notebook则逐步展示神经网络模型的构建、训练与调优细节项目说明文档梳理了项目背景与实现思路方便对照学习。目前已有519人浏览学习这套程式化的赛题流程和可复用代码框架能帮助读者快速入门自行车共享需求预测这类典型回归任务。1. Kaggle入门项目到底在练什么从一辆共享单车看到预测建模全流程第一次在Kaggle上打开城市自行车共享系统的题目我对着hour.csv里的上万条骑行记录愣了很久。这个项目之所以被当作入门经典是因为它把数据清洗、特征工程、模型选型和评估指标串成了一条完整的流水线。你要做的是用华盛顿某共享单车系统的历史租赁数据预测每个小时会有多少人租车。它不要求深度学习背景Python基础加pandas、sklearn就够用但想拿高分又需要你认真处理时间特征和验证集切分。对刚注册Kaggle的新手来说这是第一个能把手绕到发酸但确实能出成绩的实战项目。本文会顺着这份“python源码项目说明”的常规结构拆解从数据加载到模型提交的每一步以及那些让新手反复翻车的坑。2. 先让数据开口说话自行车共享数据的加载与探索性分析2.1 数据文件长什么样hour.csv与day.csv的字段差异常见项目源码包里会带一份数据说明告诉你原始数据来自UCI的Bike Sharing Dataset。它由两个CSV文件组成hour.csv是逐小时粒度day.csv是逐日粒度。很多新手一上来就把day.csv当主表结果预测的粒度完全不对。实际上官方题目要求预测的是“每小时”的租车量所以hour.csv才是主战场。day.csv只有731行小时数据有17379行样本量上hour.csv也更适合训练机器学习模型。先用pandas把两份数据读进来确认字段结构。你不需要额外安装包pandas和numpy是跑这种项目最基础的两件套。import pandas as pd import numpy as np # 加载小时级数据sep默认是逗号 hour pd.read_csv(hour.csv) day pd.read_csv(day.csv) # 先看形状和表头 print(hour shape:, hour.shape) print(day shape:, day.shape) print(hour.head())从输出能看到hour.csv里包含dteday、hr、season、yr、mnth、holiday、weekday、workingday、weathersit、temp、atemp、hum、windspeed、casual、registered、cnt字段。instant是行号dteday是日期hr是0到23的小时。season用1到4表示春夏秋冬weathersit用1到4表示晴、雾、小雨、暴雨等天气等级。这两个字段本质上都是分类变量不能直接当数字喂给模型。为了后续统一处理我会先构造一个真正的datetime列同时检查缺失值。UCI这份数据一般没有缺失但检查这一步不能省尤其是hum和windspeed如果出现0那往往是测量下限而不是缺失不要顺手填充掉。# 拼出完整时间戳方便后续按时间排序和切分 hour[datetime] pd.to_datetime(hour[dteday].astype(str) hour[hr].astype(str) :00:00) # 缺失值检查正常情况每个字段都是0 print(hour.isnull().sum())字段级业务含义会影响你的特征工程决策下面这个表是这份数据里最关键的几个字段我把建模时的处理建议也一并列出来。字段含义原始类型建模处理建议instant记录序号int直接删除dteday日期str转为datetime后派生特征hr小时0-23int周期编码或类别编码season季节1-4int转为类别特征yr年份0/1int可作为普通数值mnth月份1-12int转为类别特征holiday是否节假日int转为类别特征weekday星期几0-6int转为类别特征workingday是否工作日int转为类别特征weathersit天气状况1-4int转为类别特征temp归一化温度float保留数值特征atemp体感温度float与temp高度相关二选一hum归一化湿度float保留数值特征windspeed归一化风速float保留数值特征casual临时用户数int预测时必须删除registered注册用户数int预测时必须删除cnt总租赁数int目标变量这份表格里最容易埋雷的是casual和registered。它们俩加和正好等于cnt如果把它们留在特征里模型等于直接看到了答案。后面第5章会专门说这个坑。2.2 用pandas快速查看目标分布与周期规律拿到数据后不要急着建模先花半小时做探索性分析。共享单车的需求不是随机波动它明显受一天内的早晚高峰、一周内的工作日/周末、一年内的季节变化影响。用直方图和分组均值就能看出这些规律。import matplotlib.pyplot as plt # 目标变量cnt的分布长尾明显多数时候租车量在0附近 plt.figure(figsize(10, 4)) plt.hist(hour[cnt], bins60, edgecolork, alpha0.8) plt.xlabel(total rentals per hour) plt.ylabel(count) plt.title(Distribution of cnt) plt.show() # 按小时和工作日分组画平均租车量曲线 hour.pivot_table(indexhr, columnsworkingday, valuescnt, aggfuncmean).plot() plt.ylabel(average cnt) plt.legend([non-workingday, workingday]) plt.show()这个pivot_table是整份数据里最值得看的图。工作日的cnt曲线在上午8点和下午18点会出现两个高峰非工作日则是午后平缓的一条弧线。这直接证明hour是强特征而且工作日与非工作日的行为模式不同后面可以让模型自己学这种交互也可以在特征工程里手动加“早高峰”这类标志。再算一下数值特征与目标的相关性能给特征筛选做参考。温度、体感温度与租车量正相关湿度和风速有负相关趋势但相关性不意味着因果关系尤其是hum和windspeed之间可能存在交互作用线性模型里要小心多重共线性。# 温度、湿度等数值特征与cnt的相关性 corr hour[[temp, atemp, hum, windspeed, cnt]].corr() print(corr[cnt].sort_values(ascendingFalse))2.3 先按时间切分训练集和测试集别让随机切分毁掉结果许多新手习惯直接调用train_test_split这在普通分类题上没毛病但共享单车是时间序列预测。官方给的测试区间通常是一段连续的末尾日期如果你用随机切分训练集里混入未来数据模型等于提前看到了答案本地验证分数虚高一提交就现原形。正确做法是按时间顺序把最后一段数据留作验证。from sklearn.model_selection import train_test_split # 错误做法随机切分 X hour.drop([cnt], axis1) y hour[cnt] X_train_wrong, X_test_wrong, y_train_wrong, y_test_wrong train_test_split( X, y, test_size0.2, random_state42) # 正确做法按datetime排序用连续时间点切分 hour hour.sort_values(datetime).reset_index(dropTrue) split_date 2012-10-01 train hour[hour[datetime] split_date].copy() test hour[hour[datetime] split_date].copy() print(ftrain rows: {len(train)}, test rows: {len(test)})这里train包含2011-01-01到2012-09-30的数据test是从2012-10-01开始到最后两者时间完全不相交。之后所有特征构造、滞后统计都只允许在train上计算再merge到test。这是时间序列项目的第一条铁律。3. 特征工程是这题的胜负手时间特征与滞后特征怎么造3.1 把日期拆成小时、星期、月份并做周期编码前面的探索已经说明时间因素决定租车量所以特征工程的第一步就是把datetime里的信息拆成模型认识的数值。注意train和test要用同一套逻辑最好封装成函数避免两边特征列不一致。def extract_time_features(df): df df.copy() df[hour] df[datetime].dt.hour df[weekday] df[datetime].dt.weekday df[month] df[datetime].dt.month df[is_weekend] (df[weekday] 5).astype(int) return df train extract_time_features(train) test extract_time_features(test)拆出来的hour、weekday、month可以直接当类别特征也可以做周期编码。hour取值0到2323点和0点在数值上相差23但实际需求上它们很接近——深夜都是低谷清晨刚开始回升。线性模型无法理解这种循环边界因此对线性回归和神经网络必须把周期变量展开成sin和cos。# 周期编码把小时转成二维坐标 train[hour_sin] np.sin(2 * np.pi * train[hour] / 24) train[hour_cos] np.cos(2 * np.pi * train[hour] / 24) train[month_sin] np.sin(2 * np.pi * train[month] / 12) train[month_cos] np.cos(2 * np.pi * train[month] / 12) test[hour_sin] np.sin(2 * np.pi * test[hour] / 24) test[hour_cos] np.cos(2 * np.pi * test[hour] / 24) test[month_sin] np.sin(2 * np.pi * test[month] / 12) test[month_cos] np.cos(2 * np.pi * test[month] / 12)树模型不一定要周期编码因为决策树是逐点切分的没准能从原始hour数值里直接学出“早上8点”和“傍晚18点”这两个切分点。但线性模型非常依赖这种编码如果你打算先跑一个线性回归基线周期特征几乎是必须的。3.2 分类变量处理season、weathersit的独热编码与业务顺序season和weathersit虽然写成数字1到4但数字大小没有数学意义季节从1到4不等于数值翻倍。对于这类无序类别常见做法是用pd.get_dummies做独热编码。hour数据里类别字段不多独热后维度增加也不大可以放心用。# 先转成字符串再独热避免pandas把数值类别误判成连续变量 cat_cols [season, weathersit, holiday, workingday, weekday] for col in cat_cols: train[col] train[col].astype(category) test[col] test[col].astype(category) # 独热编码drop_firstFalse保留全部类别 train pd.get_dummies(train, columnscat_cols, drop_firstFalse) test pd.get_dummies(test, columnscat_cols, drop_firstFalse)注意drop_firstFalse意味着每个类别都会生成一列这在线性回归里会造成多重共线性但对树模型影响不大。如果你用线性回归并且发现系数不稳定可以把drop_first改成True。另一个容易被忽略的点是train和test做完get_dummies后列名必须完全对齐。用pd.get_dummies时如果某个类别只出现在test里就会多出一列后面建模时会报错。一个简单的处理办法是join后reindex。# 确保train和test特征列一致 X_train train.drop([cnt, datetime, dteday, hr, casual, registered], axis1) X_test test.drop([cnt, datetime, dteday, hr, casual, registered], axis1) X_test X_test.reindex(columnsX_train.columns, fill_value0)这里用reindex把test里缺失的列补0出现的多余列剔除掉是一个很实用的对格式技巧。3.3 滞后特征与滑动统计最容易泄漏的地方想要把预测做准光靠天气和时间还不够。共享单车有一个强惯性今天下午5点的租车量和昨天下午5点、上周一下午5点有很强的相关性。所以常见做法是构造滞后特征和滚动均值。但滞后特征在时间序列里非常容易踩中数据泄漏需要特别小心。最常见的错误是对全量数据直接shift。例如# 错误示范对全量数据shift测试集会用到未来真实值 wrong hour.copy() wrong[cnt_lag1] wrong[cnt].shift(1)这样构造的cnt_lag1在测试集里包含了训练集最后一天的真实成交量但线上预测时测试起始时刻的上一小时数据可能根本拿不到。正确方式是在train上计算滞后统计量再merge到test。# 在train上按时间排序构造滞后列 train train.sort_values(datetime).reset_index(dropTrue) for lag in [1, 2, 24, 168]: train[fcnt_lag_{lag}] train[cnt].shift(lag) # 前168行会出现NaN用训练集整体均值填掉 train train.fillna({cnt_lag_1: train[cnt].mean(), cnt_lag_2: train[cnt].mean(), cnt_lag_24: train[cnt].mean(), cnt_lag_168: train[cnt].mean()})lag1是上一小时lag24是昨天同一小时lag168是上周同一小时。这几个特征能大幅拉低RMSLE但前提是测试集也能获得这些滞后值。真实比赛中官方一般会提供测试期间的过去天气特征但不会给你过去一小时的cnt所以lag特征只能作为训练时的辅助不能直接作为测试集的预测输入。更稳妥的做法是用“星期小时”的历史均值作为冷启动特征。这种特征从train统计得到测试时可以直接用不会泄漏。# 计算每个(星期, 小时)组合在训练集上的平均租车量 temp_agg train.groupby([weekday, hour])[cnt].mean().reset_index() temp_agg.columns [weekday, hour, mean_cnt_weekday_hour] train train.merge(temp_agg, on[weekday, hour], howleft) test test.merge(temp_agg, on[weekday, hour], howleft)这个mean_cnt_weekday_hour特征相当于给模型一个“基准温度”工作日早8点平均租车量是多少深夜0点平均又是多少。测试集没有真实cnt也能用而且效果往往比单纯shift的lag特征更稳定因为它不会被离群点带偏。4. 预测模型选型与训练从线性回归到梯度提升的落地路径4.1 基线模型用线性回归先摸清数据量纲拿到特征后第一个模型不需要太复杂。线性回归虽然打不进排行榜前列但能帮你验证特征构造是否正确、数据量纲是否合理。因为cnt长尾分布严重直接拟合原始数值容易被大值主导一个常见处理是对目标做log1p变换让模型预测对数空间最后再还原。from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_log_error # 这里直接用上一章处理好的X_train和X_test y_train train[cnt] y_test test[cnt] model LinearRegression() model.fit(X_train, np.log1p(y_train)) pred_lr model.predict(X_test) pred_lr np.expm1(pred_lr) pred_lr np.clip(pred_lr, 0, None) # 防止负值 print(LR RMSLE:, mean_squared_log_error(y_test, pred_lr))注意np.log1p和np.expm1是一对互逆操作np.log1p是log(x1)因为cnt可能为0直接log0会得负无穷。线性回归的预测值在对数空间里可能是负的还原后也可能为负所以必须clip到0。这段代码里X_train包含所有独热、周期和冷启动特征如果特征工程正确RMSLE应该落在1.0到1.3左右不算好但已经不是瞎猜。线性回归的短板很明显它学不了早晚高峰这种非线性交互。哪怕加了周期编码模型表达能力仍然有限。所以基线跑通后下一步就把树模型搬上来。4.2 树模型上场随机森林与XGBoost的调参边界随机森林对异常值不敏感也不需要特征标准化非常适合这种类别较多的表格数据。树模型在Kaggle里被用得烂大街但参数设置不当同样会翻车。下面是我常用的随机森林配置。from sklearn.ensemble import RandomForestRegressor rf RandomForestRegressor( n_estimators300, max_depth12, min_samples_leaf3, n_jobs-1, random_state42 ) rf.fit(X_train, y_train) pred_rf rf.predict(X_test) pred_rf np.clip(pred_rf, 0, None) print(RF RMSLE:, mean_squared_log_error(y_test, pred_rf))n_estimators设为300再大收益甚微还拖慢训练max_depth限制为12防止树在17379个样本上学到过于琐碎的噪声min_samples_leaf3保证叶子节点至少3个样本这个参数对稳定性影响很大。随机森林不需要对数变换目标可以直接拟合原始cnt。如果希望进一步压低分数就得上梯度提升。XGBoost在这类表格数据上几乎是标配。我用它时会关注三个参数learning_rate、subsample和colsample_bytree。import xgboost as xgb xgb_model xgb.XGBRegressor( n_estimators500, learning_rate0.05, max_depth6, subsample0.8, colsample_bytree0.8, objectivereg:squarederror, random_state42 ) xgb_model.fit(X_train, y_train, eval_set[(X_test, y_test)], verboseFalse) pred_xgb xgb_model.predict(X_test) pred_xgb np.clip(pred_xgb, 0, None) print(XGB RMSLE:, mean_squared_log_error(y_test, pred_xgb))learning_rate0.05搭配500棵树的组合是经验值。learning_rate调大后树数要相应减少否则过拟合subsample0.8表示每棵树只用80%的样本colsample_bytree0.8表示每棵树只用80%的特征这两个参数能有效抑制过拟合。XGBoost默认目标函数是reg:squarederror也就是普通MSE。如果你想让误差在log空间更均衡可以把目标改成reg:squaredlogerror但必须先熟练默认目标函数。4.3 评估指标RMSLE为什么比RMSE更适合计数型目标Kaggle这个项目用的评估指标是RMSLE全称是Root Mean Squared Logarithmic Error。很多新手第一次用sklearn的mean_squared_log_error但没搞懂它背后的意义。公式是sqrt(mean((log(pred1) - log(true1))^2))相当于在log空间里算RMSE。这个指标对“大值预测错一点”和“小值预测错一点”的处理完全不同。高峰期租车量300预测成400RMSE是100凌晨租车量10预测成40RMSE是30。从RMSLE来看前者是log(301)-log(401)约等于0.28后者是log(11)-log(41)约等于1.35后者惩罚重得多。这符合业务直觉低峰的绝对误差虽然小但相对误差大代表模型完全误判了需求低谷。因此做这个项目时不要用平均绝对误差或RMSE做本地验证。邮件列表里常有人把验证集RMSE降得很低但提交后排名反而不理想就是因为优化方向错了。确认指标用RMSLE之后还可以做一个简单的交叉验证把训练集按时间切成三段每段分别算RMSLE取平均。但务必保持时间顺序不能打乱。5. 避坑指南共享单车预测项目里最常见的5个翻车现场5.1 现象训练集RMSLE很漂亮提交分数却崩盘本地验证RMSLE在0.3以下提交到Kaggle直接变成1.5这在时间序列题里最常见。原因基本只有一个验证集切分方式错了。很多人直接使用train_test_split随机打乱数据让模型在训练时看到了未来数据。解决方法是把训练集最后20%按时间留出来不要有任何随机因素。我用split_date2012-10-01切分后本地分数才开始和线上保持一致。5.2 现象模型预测出现负值RMSLE直接报错线性回归在预测非负计数时经常输出负值尤其是凌晨时段。原因是模型没有约束下限而训练数据长尾又重直线拟合可能在某些低值区间压到0以下。解决方法是每次预测后都加np.clip(pred, 0, None)。如果在用sklearn的mean_squared_log_error它内部要求预测值非负负值会直接报错。还有一个根治方案把目标变换到log空间就像4.1那样负值问题会小很多但依然要clip。5.3 现象把casual和registered留在特征里自嗨式高分有些初学者没仔细看字段说明把casual和registered也当成特征训练时RMSLE能低到0.05几乎完美。这是因为cnt casual registered模型直接学会了加法运算等于开卷考试。线上预测时根本没有casual和registered这两个输入所以分数必然崩溃。解决方法是建模前把所有与目标线性相关的字段全部删掉包括instant、dteday、hr这类行号和原始时间戳。检查方法很简单训练完打印特征重要性如果casual或registered排在最前面肯定是泄漏了。5.4 现象时间特征直接当数值线性回归的预测曲线难看到哭有人把hour、month直接当成连续数值特征喂给线性回归得到的预测曲线是一条斜线完全看不到早晚高峰的起伏。原因是线性模型假设数值大小与目标单调相关但小时0到23是一个循环变量16点不一定比15点高。解决方法是把hour做周期编码sin/cos或者干脆转成类别特征并用独热编码。树模型可以不处理但线性模型必须处理。5.5 现象滞回特征用了未来数据模型在测试阶段“开天眼”构造cnt_lag_1时如果对全量数据shift测试集的cnt_lag_1会包含训练集最后一天的真实值。本地验证时模型能用这个过去值而线上评估时这些值不可用导致本地和线上差异巨大。解决方法是只在训练集上构造lag特征测试集使用merge过来的历史统计量例如“星期小时”的平均cnt。另外训练集内做lag特征后要丢弃前几行NaN否则fillna会引入训练集全局均值虽然不泄漏但会污染早期样本。6. 收尾技巧让源码和项目说明成为能放进简历的作品6.1 生成提交文件别让结果烂在Jupyter里很多人做完模型只在Notebook里打印一个RMSLE就结束了这浪费了项目价值。Kaggle项目的产出应当是能提交的CSV和一份干净的源码。常见提交格式是两列datetime和predicted_cnt。如果你之前切分test时保留了datetime直接输出即可。submission pd.DataFrame({ datetime: test[datetime], predicted_cnt: np.clip(pred_xgb, 0, None) }) submission.to_csv(submission.csv, indexFalse)写提交文件时用整数还是浮点数没有硬性要求RMSLE在log空间计算浮点数对结果影响极小。但submission.csv的列名必须和官方要求完全一致否则提交后会报“invalid submission”。这份CSV保存好项目说明里附上模型参数和本地RMSLE就能形成一份完整的作品集材料。6.2 用特征重要性反向检查数据泄漏树模型和XGBoost都自带feature_importances它可以帮你验证特征工程是否合理。实际跑完XGBoost后我会打印前10个特征重点关注两类情况一是casual和registered这类泄漏特征是否出现二是滞后特征是否排在榜首。如果mean_cnt_weekday_hour排在第一位这是好事如果cnt_lag_1排第一你就要警惕测试集是否真的能拿到这个值。特征重要性也是调参的航标比如hour_sin比hour_cos重要很多说明小时周期编码在模型决策里确实起了作用。做完以上这些这个项目就已经具备一个可复现、可说明的完整闭环了。我自己的习惯是最后写一段注释放在源码顶部记录数据来源、切分日期、关键参数和最终RMSLE。这样过三个月再回来看还能想起当时为什么把learning_rate从0.1改到0.05。这个项目最大的价值不在于刷一个多高的排名而是让你在错误的数据泄漏、错误的时间切分、错误的评估指标里栽过跟头以后再做任何时间序列任务都会先条件反射地去看一眼验证集切得对不对。希望帮到你。本文还有配套的精品资源点击获取