简介一套面向数据科学与人工智能初学者的Kaggle经典入门项目聚焦城市自行车共享系统使用状况分析及租赁需求预测适合作为课程设计、毕业设计或算法实战练手。项目提供完整Python源码与说明文档代码覆盖数据探索、特征工程、回归建模、神经网络预测等核心环节并以Jupyter Notebook分步呈现分析思路可帮助读者理解从数据清洗到结果评估的完整流程。压缩包共8个文件包含2个Python脚本、2个Jupyter Notebook、3个CSV数据集及1份项目说明Markdown文件整体大小约823KB。其中CSV为Kaggle官方赛题数据Python脚本可直接运行训练Notebook则适合逐步学习调参项目说明文档对目录结构和复现方式作了梳理方便快速上手。目前已有520人学习下载。内容均经过测试运行成功功能正常读者既可整体复用作为课程设计或毕业设计参考也可基于现有代码扩展特征工程或尝试不同预测模型尤其适合计算机、数据科学等相关专业学生借鉴学习。1. 城市自行车共享系统预测为什么拿它练手不亏Kaggle 入门题里性价比最高的项目我首推城市自行车共享系统使用状况分析及预测Bike Sharing Demand。它数据量不到两万行没有脏文本、没有图像预测目标只有一个 count 字段但想拿到像样的分数必须同时处理时间特征、天气特征和 RMSLE 评分规则恰好覆盖一个完整预测算法的全部环节。这份资源包提供的是跑通过的 Python 源码、带输出的 Notebook、CSV 训练集和 README解压后按 README 的顺序读脚本就能复现。适合两类人一是拿它做课程设计或毕业设计的计算机专业学生二是想从零跑通 Kaggle 全流程、但不想一上来就啃复杂赛题的初学者。项目不难但每一步都有值得抠的细节。2. 把数据看明白再动手Bike-Sharing-Dataset 的字段、目标与评分规则2.1 字段表12 列里哪些是特征哪些是泄漏打开kaggle_bike_competition_train.csv这是 2011 到 2012 两年间每小时一条的自行车租赁记录约 1.7 万行。比赛任务是预测每小时租出的自行车总数 count提交结果按 RMSLE 评分。字段一览字段含义类型与取值范围能否当特征datetime小时时间戳2011-01-01 00:00 起逐小时拆成数值特征后用season季节1春 2夏 3秋 4冬可以holiday是否节假日0/1可以workingday是否工作日0/1可以weather天气1晴 2雾 3雨雪 4极端可以temp归一化温度0~1可以atemp体感温度0~1可以humidity相对湿度0~100可以windspeed风速0~67可以casual非注册用户租借数0~367泄漏列删registered注册用户租借数0~886泄漏列删count总租借数casualregistered预测目标这里最关键的是最后三行。casual 和 registered 加起来正好等于 count测试集里根本没有这两列一旦把它们当特征喂给模型本地得分会漂亮到离谱但正式提交立刻露馅。我见过的新手翻车一大半都栽在这一条上。再补充两个容易忽视的点。season、holiday、workingday、weather 这四列虽然都是整数本质是类别特征season 的 1 到 4 是四季而不是大小关系后面我会讲直接数值输入和 one-hot 的差别。数据规模只有 1.7 万行、12 个数值特征这个量级决定了模型选型——不需要上复杂网络把特征工程做扎实全连接网络就足够。2.2 时间特征datetime 一拆预测精度先涨一截共享单车的使用量有非常强的时间规律工作日早晨 8 点和傍晚 17~18 点两个通勤高峰周末则变成中午到下午的平缓波峰冬季整体比夏季少节假日和普通工作日的形态差异很大。datetime 字段如果不处理对模型只是一个字符串等于把最重要的信息全扔了。第一步永远是把它解析成 pandas 的 datetime 类型再抽出小时、星期、月份、年份。我是这么拆的import pandas as pd df pd.read_csv(kaggle_bike_competition_train.csv) df[datetime] pd.to_datetime(df[datetime]) df[hour] df[datetime].dt.hour df[dayofweek] df[datetime].dt.dayofweek df[month] df[datetime].dt.month df[year] df[datetime].dt.yearpd.to_datetime把字符串时间转成真正的时间对象.dt.hour、.dt.dayofweek、.dt.month、.dt.year分别抽出数值。拆完以后原始 datetime 列的信息已经被这四个数值特征完全承接可以从特征表里删掉了。有个细节值得单独说dayofweek 和 workingday 要配合使用。dayofweek 告诉模型这是周几workingday 告诉模型这天是不是工作日两者一起才能区分工作日的早晨和周末的早晨。只留其中一个模型就学不到通勤高峰的完整形态。如果你画一下 hour 和 count 的关系通常会发现 hour 是所有特征里和 count 相关性最高的一个。2.3 评分规则RMSLE 逼你先做 log1pKaggle 对这场比赛用的是 RMSLERoot Mean Squared Logarithmic Error公式是RMSLE sqrt( mean( (log(pred1) - log(actual1))² ) )为什么用 log 而不用原始差值因为 count 的分布从 0 到接近 1000跨度太大。预测误差 50对真实值 30 的样本是灾难对真实值 800 的样本则无所谓取对数后大数值的差距被压缩模型会把更多注意力放在相对误差上。还有个容易被忽略的性质RMSLE 对低估的惩罚比对高估更重。同样差 10 个pred90、actual100 的 log 差距比 pred110、actual100 更大因为 log 曲线在取值小的区间更陡。所以模型会倾向于稍微高估而不是低估早停时如果发现验证集分数总在高估方向偏不用太紧张。这直接决定了训练时的目标函数。你不需要手写 RMSLE 的求导常见做法是对真实标签做np.log1p(count)然后用普通 MSE 训练预测时再expm1还原——两者优化方向完全一致。这个套路在后面的训练脚本里会完整呈现。提示log1p 就是 ln(1x)x 取 0 时结果是 0天然规避了 log(0) 的未定义问题expm1 是它的逆运算。3. 数据预处理全流程从 CSV 到神经网络张量的脚本写法3.1 读取 CSV类型修正与缺失检查zip 解压后你会看到几个文件BikeSharingDemand.py是可直接运行的入口脚本神经网络之预测共享单车使用情况.ipynb是带输出的 Notebook适合逐格阅读城市自行车共享系统使用状况.py是另一个版本的脚本实现。我建议先别急着跑把预处理逻辑自己过一遍后面所有调参都建立在这块数据管道上。import numpy as np import pandas as pd df pd.read_csv(kaggle_bike_competition_train.csv) print(df.shape) print(df.isnull().sum()) print(df.dtypes)这份训练集是完整的isnull().sum()的结果应该全零。打印 dtypes 是为了确认 season、holiday、workingday、weather 这几列虽然取值是数字但本质是类别属性后续特征处理心里得有数。天气字段值得单独看一眼print(df[weather].value_counts())正常会看到 weather1 占大部分weather4 只有很少几行对应暴雪、沙尘暴这类极端天气。样本太少模型很难学出规律但不用特意删——全连接网络会把它当成低频组合来对待只要你的分析代码里没有按 weather 分组的逻辑就行真有的话记得先看这一组是不是空。3.2 特征构造与标准化先按时间排序再做 scaler标准化这里有个顺序问题我踩过坑之后固定成了下面这个写法先按时间排序再取特征矩阵然后只用训练部分去 fit 标准化器。顺序反了验证集和测试集的数据分布会混进 scaler 的均值和方差虽然对 StandardScaler 影响不算大但在时间序列任务里属于脏操作。from sklearn.preprocessing import StandardScaler df df.sort_values(datetime).reset_index(dropTrue) feature_cols [ season, holiday, workingday, weather, temp, atemp, humidity, windspeed, hour, dayofweek, month, year ] X_raw df[feature_cols].values.astype(np.float32) y_raw df[count].values.astype(np.float32) split_idx int(len(df) * 0.8) scaler StandardScaler().fit(X_raw[:split_idx]) X_train scaler.transform(X_raw[:split_idx]) X_val scaler.transform(X_raw[split_idx:]) y_train np.log1p(y_raw[:split_idx]) y_val np.log1p(y_raw[split_idx:])sort_values(datetime).reset_index(dropTrue)把数据按时间排正保证后面的切分严格按时间先后进行X_raw[:split_idx]取前 80% 做 scaler 的 fit只用这部分数据的均值和标准差去 transform 全部数据np.log1p加在 y 上训练目标从预测 count变成预测 log1p(count)对应第 2.3 节讲的评分规则。temp、atemp、humidity、windspeed 和 hour、year 的量纲完全不同不标准化直接喂给神经网络梯度更新会被量纲大的特征带偏。标准化后所有特征均值为 0、方差为 1全连接网络的第一层权重更新会均衡很多。3.3 特征选择与时间切片删泄漏列按时间走 8:2特征矩阵里为什么没有 casual、registered、datetime前面说过casual 加 registered 就是 count属于完全泄漏datetime 字符串不能直接当数值特征信息已经被 hour、dayofweek、month、year 承接。这三类特征有一个共同的判断标准测试集里有没有测试集没有的一律不进特征矩阵。类别特征的处理方式我给出一个可选的 one-hot 方案。season、weather 这种取值只有 4 个的类别直接传数值给网络模型可能误认为 4 比 1 大one-hot 能消除这种误导但对 1.7 万行的数据量来说影响不大。想更稳一点可以这样from sklearn.preprocessing import OneHotEncoder cat_cols [season, weather] num_cols [holiday, workingday, temp, atemp, humidity, windspeed, hour, dayofweek, month, year] enc OneHotEncoder(sparse_outputFalse, handle_unknownignore) X_cat enc.fit_transform(df[cat_cols]) num_scaler StandardScaler().fit(X_raw[:split_idx][:, [c for c in range(len(feature_cols))]])不过主干流程里我仍然用的是数值直接传入原因有两个一是 season 的 1 到 4 本身带有顺序含义模型能学出夏秋多于冬春的方向性关系二是少一层编码器后续调参和复现都更简单。等你的 baseline 稳定了想压分再回来实验 one-hot 也不迟。切分比例上8:2 是入门阶段的安全选择。前 80% 覆盖了 2011 全年加 2012 年的大部分后 20% 刚好是 2012 年最后一个季度包含冬季样本验证结果更有参考价值。想更严格可以切 7:3但训练集少于 70% 时模型见不到完整的季节周期验证分数会显现出额外的偏差。4. 神经网络训练实战三层全连接网络与早停机制4.1 网络结构128-64-1 全连接为什么够用资源里的 Notebook 用神经网络做预测方向是对的。1.7 万行、12 个数值特征这个量级下不需要上 LSTM 或 Transformer——数据行之间虽然有时间顺序但每行本身是独立的特征向量把今天上午 9 点、晴天、湿度 60%映射到一个数值这是标准的回归问题全连接网络就是够用的工具。我常用的结构是这样from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Dropout from tensorflow.keras.optimizers import Adam model Sequential() model.add(Dense(128, activationrelu, input_shape(X_train.shape[1],))) model.add(Dropout(0.3)) model.add(Dense(64, activationrelu)) model.add(Dropout(0.2)) model.add(Dense(1))第一层 128 个神经元把 12 维输入映射到高维空间让模型有能力组合出工作日的早晨夏季的傍晚这类非线性关系Dropout(0.3) 随机丢弃 30% 的神经元防止模型死记训练集里的极端天气样本第二层 64 个神经元进一步压缩特征最后一层只有 1 个神经元、不加激活函数因为这是回归任务输出值域没有上限约束。换成 512-256-1 这种更大的网络会怎样大概率训练集分数更好、验证集分数更差——数据量摆在那里过拟合会比欠拟合来得更早。这个判断我直接用一句话说这个赛题拼的是特征工程不是网络层数。4.2 训练配置Adam、早停与学习率衰减的参数选择训练配置我习惯这样写from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau model.compile(optimizerAdam(learning_rate1e-3), lossmse) early_stop EarlyStopping( monitorval_loss, patience15, restore_best_weightsTrue ) reduce_lr ReduceLROnPlateau( monitorval_loss, factor0.5, patience5, min_lr1e-5 ) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs200, batch_size256, callbacks[early_stop, reduce_lr], verbose1 )几个参数的含义值得记一下。learning_rate1e-3是 Adam 的常规起点降到 1e-4 收敛太慢升到 1e-2 会在 loss 曲线上看到剧烈震荡patience15表示验证集损失连续 15 轮不下降就停给模型留出足够的平台期ReduceLROnPlateau是早停的缓兵之计——验证损失 5 轮不动学习率先减半再等 15 轮。batch_size256对这个体量刚合适每轮约 54 个 batch梯度更新速度和稳定性比较平衡。epochs200不用担心跑满——早停回调会在验证损失触底后自动拉停。真正需要盯的是下面两个信号。4.3 训练评估损失曲线、RMSLE 怎么换算训练结束后别只看最后一个 epoch 的数字把曲线打出来import matplotlib.pyplot as plt plt.plot(history.history[loss], labeltrain) plt.plot(history.history[val_loss], labelval) plt.legend() plt.yscale(log) plt.savefig(training_curve.png)我看这条曲线主要关注三个信号。train loss 和 val loss 是否一起下降如果 train 降、val 不降是过拟合加大 Dropout 或缩小网络如果两者都高且不降说明特征有问题回去检查是不是漏了时间特征如果 val loss 在训练后期出现锯齿说明学习率太大ReduceLROnPlateau 会在中途纠正。验证集上的最终得分要换算回 RMSLEfrom sklearn.metrics import mean_squared_error preds_log model.predict(X_val, verbose0).flatten() preds np.expm1(preds_log) preds np.clip(preds, 0, None) val_rmsle np.sqrt(mean_squared_error(np.log1p(preds), y_val)) print(val RMSLE:, val_rmsle)这段把三件事一次做完expm1把模型输出的 log 空间还原成真实 count 预测np.clip(preds, 0, None)把所有负预测值裁到 0这是 Kaggle 的官方规定负数预测直接按 0 处理最后对预测值取log1p和y_val直接做 RMSE——因为y_val在预处理阶段已经取了 log1p所以这一步算出的就是 RMSLE。5. 避坑与排查三轮改参踩过的四条坑5.1 泄漏特征casual 和 registered 让本地分数失真现象本地验证 RMSLE 只有 0.05 上下几乎完美但一看测试集规则发现根本没有 casual 和 registered 这两列。原因casual 加 registered 就是 count。模型不用学任何规律把这两列相加再取 log1p 就能拿到接近满分的本地分数。这是典型的特征泄漏模型没有变强只是抄到了答案。解决在任何代码跑起来之前先把特征列表里这两列划掉。我现在的习惯是在每个预处理脚本开头用注释写明feature_cols 不含 casual/registered原因是测试集无此列防止隔几天改参时手滑加回去。5.2 不做 log1p负预测值让 RMSLE 直接爆掉现象直接用 count 原始值当训练目标网络也能训练但验证集 RMSLE 稳定在 1 以上预测结果里还经常出现负数和极端大值。原因count 分布严重右偏0 到接近 1000 的区间里大部分样本集中在 100 以下。MSE 损失被少数的 800、900 大值样本主导模型把精力全放在拟合极端值上中小值样本的预测就变得很不稳定。解决训练目标改成np.log1p(count)预测时再expm1还原。这一改通常能把验证 RMSLE 从 1.2 左右压到 0.5 以内。顺便说一句预测值出现负数也大概率是不做 log1p 的锅——线性输出没有下界约束越过 0 是常事。5.3 随机划分验证集时间序列必须按时间切现象用train_test_split(random_state42)划分后验证 RMSLE 漂亮得惊人换成按时间切分后分数立刻差了一截不知道信哪个。原因数据整体递增随机切分把 2012 年的高分样本混进了训练集验证集分布和数据整体分布几乎一样模型相当于偷看了未来的数据。做预测建模时间序列数据按时间切分是铁律。解决一律用第 3.2 节的时间切片方式保持同一个分割点。我在做这个项目时就是因为随机切分的假象一度以为网络调得很好了按时间一切真实水平立刻暴露。5.4 提交验证码captcha 组件被浏览器插件拦截现象在 Kaggle 页面点提交按钮页面报错提示 captcha must be filled out但页面上根本看不到可输入的验证码框。原因Kaggle 的提交表单里带了一个反自动化验证码组件经常被浏览器的广告拦截扩展或部分隐私插件过滤掉导致表单校验时读不到验证码字段。解决换一个无痕窗口优先关掉广告拦截类扩展刷新页面让它重新加载验证码还不行就在浏览器站点设置里单独把 kaggle.com 的扩展关闭。我那次是无痕窗口一次就过了没再折腾。6. 提交与验证最后一公里把预测值变成提交文件6.1 模拟测试集与提交文件原版 Kaggle 比赛会把数据拆成 train.csv 和 test.csv但这份资源包只提供了kaggle_bike_competition_train.csv。拿不到官方测试集就把按时间切出的后 20% 当作模拟测试集走一遍完整的提交流程test_df df.iloc[split_idx:].copy() X_test scaler.transform(test_df[feature_cols].values.astype(np.float32)) test_preds_log model.predict(X_test, verbose0).flatten() test_preds np.clip(np.expm1(test_preds_log), 0, None) submission pd.DataFrame({ datetime: test_df[datetime], count: test_preds }) submission.to_csv(submission.csv, indexFalse)输出文件只有两列datetime 和 count。行数必须和测试行数完全一致列名必须严格匹配多一列少一列都会被评分系统判错。如果以后参加正式比赛拿到官方 test.csv把这里的test_df换成测试数据预处理和训练脚本原样复用即可。6.2 提交前的强制自查清单我给自己定了个规矩任何预测类方案提交前必须过这四关检查项判定标准常见翻车点列名与行数datetime/count 两列行数与测试集一致忘了 reset_index预测值非负min(preds) 0输出层越过 0log1p 对称性y 用 log1ppreds 用 expm1 还原只做了半边特征管线一致训练和测试走同一个 scaler测试集单独 fit 了 scaler这四关都过完本地 RMSLE 才有参考价值否则只是自娱自乐的数字。本地验证 RMSLE 落在 0.38~0.45 区间是正常水平再往上压就得靠更细的特征组合比如把 hour 和 workingday 相乘构造通勤高峰标志、把 weekend 单独抽出来当特征这些都是低成本高收益的进阶动作。6.3 我的收尾习惯做完这个项目我养成了一个强迫症每次训练完模型不管多急都会重新打开代码从头到尾过一遍预处理流程确认泄漏列没加回来、log1p 没断、时间切片没被随机划分替换、预测值没有负数。这个项目让我栽过的跟头几乎全在这四件事上。后来做毕业设计里的预测算法和别的 Kaggle 赛题我也一直保留这套检查习惯翻车的概率小了很多。希望这份源码和笔记能帮你少走几步弯路。本文还有配套的精品资源点击获取