
简介这份资源是面向计算机相关专业学生与初学者的Kaggle入门实战项目围绕城市自行车共享系统的使用状况展开数据分析与需求预测适合用作课程设计、大作业或毕业设计的参考案例也便于零基础读者通过完整流程熟悉机器学习建模思路。压缩包共8个文件包含3个csv数据集、2个py脚本、2个ipynb笔记本和1个md说明文档整体约823KB其中csv提供训练与测试数据py与ipynb分别以脚本和交互式笔记本两种形式实现神经网络预测共享单车使用情况md则梳理项目背景与运行说明。目前已有519人学习下载具备一定的参考热度。读者可借此掌握数据清洗、特征分析、模型搭建与结果预测的完整链路并对照两种代码形式理解神经网络在回归预测任务中的落地方式为后续参加Kaggle竞赛或开展同类项目积累可复用的经验。1. 从一份 Kaggle 入门项目说起城市自行车共享系统使用状况分析及预测到底在做什么城市自行车共享系统的运营方每天都会面对一个很现实的问题明天早上 8 点国贸地铁口那个站点该放多少辆车放少了用户扫码发现无车可骑直接流失放多了晚高峰一过一堆车堆在人行道上运维调度成本飙升。Kaggle 上这个「城市自行车共享系统使用状况分析及预测」入门项目本质上就是拿一份真实的历史租借记录用 Python 把「什么时间、什么天气、什么季节、有多少人租车」这件事量化出来再训练一个模型去预测未来某个小时的租借量。它适合刚接触 Kaggle、想跑通一个完整数据分析 回归预测流程的人也适合做运营策略、调度排班、运力配置的从业者拿来当模板。整条链路不复杂读数据、清洗、特征工程、建模、评估、输出预测但每一步都有值得抠的细节。下面我按自己实际跑这类项目的顺序把源码结构、参数设置和踩过的坑一次讲清楚。2. 数据读进来先别急着建模字段含义与清洗顺序决定后面一半的成败2.1 这份数据长什么样哪些字段是「预测时拿不到的」城市自行车共享数据集通常包含两个文件一个是按小时或按天聚合的租借记录另一个是天气记录。核心字段一般有datetime、season、holiday、workingday、weather、temp、atemp、humidity、windspeed、casual、registered、count。其中casual是未注册用户租借量registered是注册用户租借量count是两者之和也是我们要预测的目标。这里有一个新手最容易翻车的地方casual和registered在预测阶段是拿不到的因为你要预测的就是未来的总量。如果你在特征里保留了这两个字段模型在训练集上表现会好得离谱一到真实预测就废掉。我一般会在读数据后立刻把这两列单独存一份用于分析然后从特征矩阵里删掉。import pandas as pd import numpy as np # 读取训练集和测试集注意日期列要解析成 datetime train pd.read_csv(train.csv, parse_dates[datetime]) test pd.read_csv(test.csv, parse_dates[datetime]) # 先看一眼字段类型和缺失情况 print(train.dtypes) print(train.isnull().sum()) # 把目标拆解列单独保存分析用不进特征 target_parts train[[casual, registered]].copy() # 从特征里剔除避免数据泄漏 drop_cols [casual, registered, count] feature_cols [c for c in train.columns if c not in drop_cols [datetime]] print(可用特征列, feature_cols)这段代码做了三件事解析日期、检查缺失、隔离泄漏字段。参数上parse_dates必须指定否则datetime只是字符串后面提取小时、星期几都会报错。drop_cols里把count也去掉是因为它是标签不能出现在特征里。很多人会忘记把casual和registered从feature_cols里排除结果模型学到的全是「因为注册用户多所以总量多」这种废话。2.2 时间字段拆解小时、星期、月份怎么切才有预测价值原始datetime是一个完整时间戳直接扔给模型没有意义。需要拆成hour、dayofweek、month、year这些离散特征。但拆完之后怎么用有讲究。hour是最强特征因为通勤早晚高峰非常明显。dayofweek区分工作日和周末month捕捉季节趋势。我一般还会加一个is_weekend布尔特征因为周末的租借曲线和工作日完全不同。注意不要用season字段直接替代月份因为season是人为划分的月份更细。def extract_time_features(df): df df.copy() df[hour] df[datetime].dt.hour df[dayofweek] df[datetime].dt.dayofweek df[month] df[datetime].dt.month df[year] df[datetime].dt.year df[is_weekend] df[dayofweek].isin([5, 6]).astype(int) # 把小时做成周期性特征避免 23 点和 0 点被当成距离很远 df[hour_sin] np.sin(2 * np.pi * df[hour] / 24) df[hour_cos] np.cos(2 * np.pi * df[hour] / 24) return df train extract_time_features(train) test extract_time_features(test)这里hour_sin和hour_cos是周期性编码把 0 到 23 的小时映射到单位圆上。为什么这么做因为线性模型会把 23 和 0 当成相差 23 个单位实际上它们只差 1 小时。树模型对周期性不敏感但加上这两个特征对线性模型和神经网络帮助很大。参数上2 * np.pi是完整周期除以 24 是小时数。同样的逻辑可以套用到dayofweek和month上但小时是最关键的。2.3 天气与温度字段连续值分箱还是保留原值temp、atemp、humidity、windspeed都是连续值。新手常问要不要分箱我的经验是树模型直接保留连续值就行它会自己找切分点线性模型可以考虑标准化或者分箱。但weather字段是类别型的1 到 4 代表天气从好到坏直接当数值用会引入「4 是 1 的四倍差」这种错误假设应该做 one-hot 或者至少当成有序类别处理。# 天气类别做 one-hot避免模型误以为 4 和 1 有倍数关系 weather_dummies pd.get_dummies(train[weather], prefixweather) train pd.concat([train, weather_dummies], axis1) test pd.concat([test, pd.get_dummies(test[weather], prefixweather)], axis1) # 对齐列防止测试集缺少某个天气类别导致列数不一致 missing_cols set(train.columns) - set(test.columns) for c in missing_cols: test[c] 0 test test[train.columns]pd.get_dummies之后一定要做列对齐因为测试集可能没有出现训练集里的某个天气类别直接拼接会导致特征维度不一致模型预测时报错。这个坑我在第一次跑 Kaggle 时就踩过当时排查了半天才发现是列顺序和列数量对不上。3. 特征工程做完再谈模型把「时间 天气 节假日」组合出可解释的强特征3.1 高峰时段标记与交互特征单纯的小时特征只能告诉模型「8 点租借量高」但模型不知道 8 点高是因为通勤。可以手动加一个is_rush_hour特征把 7 到 9 点和 17 到 19 点标记出来。更进一步把hour和workingday做交互生成hour_workingday组合特征让模型能区分「工作日的 8 点」和「周末的 8 点」。def add_rush_hour(df): df df.copy() df[is_rush_hour] df[hour].isin([7, 8, 9, 17, 18, 19]).astype(int) # 交互特征小时乘以是否工作日放大通勤信号 df[hour_x_workingday] df[hour] * df[workingday] return df train add_rush_hour(train) test add_rush_hour(test)hour_x_workingday这个交互特征看起来简单但在树模型里能显著提升分裂效率。因为树模型每次只能在一个特征上切分如果它先切workingday再切hour需要两层有了交互特征一层就能把「工作日早高峰」这个组合切出来。参数上workingday是 0 或 1乘以小时数后工作日的 8 点变成 8周末的 8 点变成 0区分度一下就出来了。3.2 目标变量做对数变换为什么 count 要取 log1p租借量count的分布通常是右偏的少数高峰时段数值极大大部分时段数值较小。直接回归会让模型被极端值带偏。常见做法是对count做log1p变换也就是log(1 count)训练完再expm1还原。这样既压缩了极端值又避免了log(0)报错。# 对目标做 log1p 变换 y_train np.log1p(train[count]) # 训练完预测后还原 # y_pred np.expm1(model.predict(X_test))注意log1p和expm1是配对使用的不要用np.log和np.exp因为count可能为 0log(0)是负无穷。这个细节在 Kaggle 的评分指标 RMSLE 下尤其重要因为 RMSLE 本身就是在对数尺度上算误差做log1p变换后直接用 RMSE 训练和评分指标是对齐的。3.3 训练集验证集怎么切时间序列不能随机切这是整个项目里最容易被忽视、也最致命的一点。城市自行车数据是时间序列如果你用train_test_split随机切分模型会看到未来的数据去预测过去验证分数虚高。正确做法是按时间顺序切比如前 18 个月做训练后 1 个月做验证。# 按时间排序后切分不能用随机切分 train train.sort_values(datetime).reset_index(dropTrue) split_idx int(len(train) * 0.8) train_set train.iloc[:split_idx] valid_set train.iloc[split_idx:] X_train train_set[feature_cols] y_train np.log1p(train_set[count]) X_valid valid_set[feature_cols] y_valid np.log1p(valid_set[count])参数上0.8是切分比例可以根据数据量调整。如果数据只有一年建议留最后两个月做验证如果有两年以上留最后三个月。关键是验证集必须在时间上晚于训练集模拟真实预测场景。我见过有人随机切分后模型 RMSE 只有 0.3一上 Kaggle 排行榜直接掉到 0.5 开外就是这个问题。4. 模型选型与调参从线性回归到 LightGBM哪个更适合这份数据4.1 基线模型先跑通线性回归和决策树不要一上来就上复杂模型。先用线性回归跑一个基线看看特征和目标之间大概是什么关系。线性回归对连续特征敏感所以要先做标准化。决策树不需要标准化但容易过拟合可以作为第二个基线。from sklearn.linear_model import Ridge from sklearn.tree import DecisionTreeRegressor from sklearn.preprocessing import StandardScaler from sklearn.metrics import mean_squared_error # 线性回归基线先标准化 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_valid_scaled scaler.transform(X_valid) ridge Ridge(alpha1.0) ridge.fit(X_train_scaled, y_train) pred_ridge ridge.predict(X_valid_scaled) rmse_ridge mean_squared_error(y_valid, pred_ridge, squaredFalse) print(Ridge RMSE:, rmse_ridge) # 决策树基线 tree DecisionTreeRegressor(max_depth8, random_state42) tree.fit(X_train, y_train) pred_tree tree.predict(X_valid) rmse_tree mean_squared_error(y_valid, pred_tree, squaredFalse) print(DecisionTree RMSE:, rmse_tree)Ridge的alpha是正则化强度越大越不容易过拟合。DecisionTreeRegressor的max_depth控制树深8 层是一个比较保守的起点。这两个基线跑完你心里就有数了如果 Ridge 的 RMSE 在 0.5 左右决策树在 0.4 左右那后面用集成模型目标就是压到 0.35 以下。4.2 LightGBM 的关键参数num_leaves、learning_rate、n_estimatorsLightGBM 是这类表格数据的主力模型速度快、精度高。但参数多新手容易调乱。我一般只关注三个核心参数num_leaves控制单棵树复杂度learning_rate控制每棵树的学习步长n_estimators控制树的数量。这三个参数互相制约learning_rate小就需要更多n_estimators。import lightgbm as lgb params { objective: regression, metric: rmse, num_leaves: 31, learning_rate: 0.05, n_estimators: 1000, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 5, verbose: -1, random_state: 42 } lgb_train lgb.Dataset(X_train, y_train) lgb_valid lgb.Dataset(X_valid, y_valid, referencelgb_train) model lgb.train( params, lgb_train, valid_sets[lgb_valid], callbacks[lgb.early_stopping(stopping_rounds50), lgb.log_evaluation(period100)] ) pred_lgb model.predict(X_valid, num_iterationmodel.best_iteration) rmse_lgb mean_squared_error(y_valid, pred_lgb, squaredFalse) print(LightGBM RMSE:, rmse_lgb)num_leaves31是默认值数据量小可以降到 15 到 20防止过拟合。learning_rate0.05配合n_estimators1000是比较稳的组合early_stopping会在验证集连续 50 轮不提升时自动停避免手动试轮数。feature_fraction和bagging_fraction都是 0.8相当于每次迭代随机用 80% 的特征和 80% 的样本增加模型多样性。这套参数在我的经验里对城市自行车数据能稳定跑到 RMSE 0.35 左右。4.3 交叉验证与模型融合把 RMSE 再压 0.02 的实操单模型调完之后可以用 K 折交叉验证看模型稳定性再把 LightGBM 和 XGBoost 的预测结果做加权平均。加权系数不用太复杂0.6 和 0.4 这种经验值就够。from sklearn.model_selection import TimeSeriesSplit import xgboost as xgb # 时间序列交叉验证 tscv TimeSeriesSplit(n_splits5) cv_scores [] for train_idx, val_idx in tscv.split(X_train): X_tr, X_val X_train.iloc[train_idx], X_train.iloc[val_idx] y_tr, y_val y_train.iloc[train_idx], y_train.iloc[val_idx] m lgb.train(params, lgb.Dataset(X_tr, y_tr), valid_sets[lgb.Dataset(X_val, y_val)], callbacks[lgb.early_stopping(50)]) pred m.predict(X_val, num_iterationm.best_iteration) cv_scores.append(mean_squared_error(y_val, pred, squaredFalse)) print(CV RMSE 均值:, np.mean(cv_scores)) # 简单加权融合 xgb_model xgb.XGBRegressor(n_estimators800, learning_rate0.05, max_depth6, random_state42) xgb_model.fit(X_train, y_train) pred_xgb xgb_model.predict(X_valid) pred_blend 0.6 * pred_lgb 0.4 * pred_xgb rmse_blend mean_squared_error(y_valid, pred_blend, squaredFalse) print(Blend RMSE:, rmse_blend)TimeSeriesSplit和普通 K 折的区别在于它保证每次训练集都在验证集之前不会用到未来数据。n_splits5表示切 5 份每份轮流做验证。融合时0.6和0.4是权重可以根据验证集表现微调但不要调得太细否则容易过拟合验证集。一般来说融合能把 RMSE 再压 0.01 到 0.02提升不大但稳定。5. 避坑与排查这份数据里最容易翻车的 5 个地方5.1 现象验证集 RMSE 很低Kaggle 提交后分数差很多原因随机切分验证集导致模型看到了未来数据。时间序列数据必须按时间切不能随机切。解决用TimeSeriesSplit或者手动按时间排序后切分确保验证集时间晚于训练集。5.2 现象模型预测出负数租借量原因线性回归或未加约束的模型在极端特征下会输出负值但租借量不可能为负。解决预测后做np.maximum(pred, 0)截断或者用log1p变换训练还原后自然非负。5.3 现象测试集特征列和训练集对不上预测报错原因pd.get_dummies后训练集和测试集的类别列不一致比如训练集有weather_4而测试集没有。解决拼接后做列对齐缺失的列补 0多余列删掉保证X_test.columns和X_train.columns完全一致。5.4 现象LightGBM 训练到一半 early_stopping 触发但验证集分数还在波动原因stopping_rounds设得太小模型还没收敛就停了。解决把stopping_rounds从 50 调到 100 或 200同时观察learning_rate是否太大。如果learning_rate0.1可以降到 0.05 再试。5.5 现象casual和registered字段忘了删模型训练分数异常高原因这两个字段是目标的一部分保留在特征里等于泄漏答案。解决读数据后立刻把casual、registered、count从特征列里排除只保留天气、时间、节假日等外部特征。6. 进阶技巧用「分时段建模」把早晚高峰预测误差再降一档前面整套流程跑下来RMSE 大概在 0.35 左右。如果你还想再进一步可以试试分时段建模。思路很简单早晚高峰的租借模式和平峰完全不同用一个模型拟合所有时段模型会在高峰和平峰之间妥协。把数据按小时分成三组——早高峰7 到 9 点、晚高峰17 到 19 点、平峰其余时段每组单独训练一个 LightGBM预测时按小时路由到对应模型。def train_by_period(df, feature_cols): periods { morning: df[df[hour].isin([7, 8, 9])], evening: df[df[hour].isin([17, 18, 19])], offpeak: df[~df[hour].isin([7, 8, 9, 17, 18, 19])] } models {} for name, subset in periods.items(): X subset[feature_cols] y np.log1p(subset[count]) m lgb.train(params, lgb.Dataset(X, y), num_boost_round500) models[name] m return models # 预测时按小时路由 def predict_by_period(models, df, feature_cols): preds np.zeros(len(df)) for i, row in df.iterrows(): hour row[hour] if hour in [7, 8, 9]: m models[morning] elif hour in [17, 18, 19]: m models[evening] else: m models[offpeak] preds[i] m.predict(df.loc[[i], feature_cols])[0] return np.expm1(preds)这个做法在验证集上通常能把早高峰时段的 RMSE 从 0.4 压到 0.32 左右代价是训练和推理都变复杂需要维护三个模型。如果你的业务场景对早高峰预测精度要求特别高比如调度排班直接挂钩成本那这个投入值得。如果只是做个入门项目交作业单模型就够了。另外一个小技巧是给hour特征加权重。LightGBM 支持weight参数你可以给高峰时段的样本更高权重让模型更关注高峰预测误差。权重怎么设我一般给早高峰 2.0晚高峰 1.8平峰 1.0。这个权重不需要精确试两三组就能找到感觉。最后说一个我自己的习惯每次跑完模型我都会把验证集里预测误差最大的 20 条记录单独拉出来看。十有八九是极端天气或者节假日这些样本本身噪声就大强行拟合反而伤模型。看到这种样本我一般会检查特征里有没有对应的标记如果没有就加一个is_extreme_weather或者is_holiday的布尔特征让模型知道「这种情况特殊别硬套平时的规律」。这个习惯帮我省了很多次盲目调参的时间。希望帮到你。本文还有配套的精品资源点击获取