共享单车需求预测这道题,在数学建模竞赛里出现的频率非常高,国赛、华为杯、美赛都考过类似的背景。它属于典型的时空预测问题,看着门槛不高,但想拿高分并不容易——很多队伍在“预测”这一步就翻车了,导致后面调度、定价、投放策略全是空中楼盘。我当年带队打比赛时也在这个题上踩过不少坑,今天专门把“建模篇”这部分摊开讲清楚,从问题定义、数据处理、模型选型到实操代码和避坑指南,一次性说透。
这篇内容适合正在备战国赛、华为杯或美赛的同学,也适合想用机器学习做城市级需求预测的从业者。不管你打算用传统时间序列、LightGBM这类表格模型,还是想尝试图神经网络,这篇文章都能给你一条可以直接落地的路线。
1. 先想清楚:这道题到底在预测什么
1.1 需求预测的三个层级
很多队伍拿到“共享单车需求预测”的题,第一反应就是“用LSTM跑一跑”。这个想法本身没问题,但问题是你得先搞清楚要预测的是哪个粒度的需求。
从比赛和实际业务的角度来看,需求预测通常分三个层级:
- 站点级别:预测某个具体站点的借车量或还车量。难度最高,数据稀疏,受单点因素影响大。
- 网格/区域级别:把城市划分成网格,预测每个格子里的需求。中等难度,是很多城市级调度系统的实际做法。
- 城市总量级别:预测全城或者某个大区的总需求。难度最低,适合做宏观趋势分析,但很难支撑具体调度决策。
读题时第一件事就是判断题目要求落在哪个层级。我之前见过有队伍把题目理解错,题目要求预测站点级租借量,他们直接对全城总量建模,最后评委问“你这预测结果怎么指导单站点调度”,彻底答不上来。
1.2 一个容易忽略的边界问题:时间粒度怎么选
时间粒度直接决定建模方式。共享单车数据常见的粒度有:
- 逐小时(24点/天)
- 高峰期粒度(早晚高峰单独建模)
- 逐15分钟(高频数据)
逐小时是大多数比赛的标准粒度。但这里有个隐藏问题:你要预测的是一天的某个时段,还是未来多天同一时段?是滚动预测还是单步预测?这决定了你的特征矩阵怎么构建、验证集怎么划分。
我的经验是:先把预测目标固定下来,写成一句话。比如“基于过去14天的逐小时数据,预测未来24小时内每个站点的借车数量”。目标越明确,后面每一步决策越不容易跑偏。
1.3 读题阶段就要画好的“预测闭环”
建模不只是建一个模型。完整的预测闭环包含:
- 原始数据清洗与对齐
- 特征工程(时间、天气、空间、历史统计)
- 模型训练与调参
- 预测结果输出与后处理
- 误差分析(分站点、分时段、分天气)
比赛时间紧,很多人会把精力全砸在第3步,结果特征没做好,模型再强也白搭。真正拿高分的队伍,往往在前两步和最后一步花了大功夫。
我在备赛时习惯先画一张“数据流图”,把每一步输入输出写清楚。别小看这个环节,它能帮全队对齐思路,省掉后期大量返工。
2. 建模前的数据工程:决定上限的不是算法
2.1 共享单车数据集里有哪些“坑”
共享单车数据坑很多,最典型的几类:
- 时间戳不是标准格式,有的精确到秒,有的精确到分钟,需统一对齐。
- 站点经纬度缺失或漂移,有的站点位置明显有误。
- 天气数据粒度不一致,气象站数据是逐小时的,但偶尔有空档。
- 站点新建/拆除导致ID不连续,数据分布突变。
- 异常值:极少数站点的订单量突然变成0,可能是因为设备故障而不是真没人骑。
这些坑如果不在建模前处理干净,后面所有分析都会失真。我见过有队伍预测出的某个站点全天需求为0,就是因为原始数据里这个站点当天有大量空值,他们直接删行处理了。
2.2 构建特征:让模型认识“潮汐”和“天气”
共享单车需求最明显的模式是“潮汐现象”:早高峰从住宅区流向办公区,晚高峰反向流动。模型不知道这个规律,但我们可以把规律“翻译”成特征。
我用过的特征体系大致分三类:
第一类是时间特征。小时(0-23)、星期几、是否周末、是否节假日、距离最近节假日的天数、一年中的第几天。这些看起来简单,但对模型区分“工作日早高峰”和“周末下午闲逛”至关重要。
第二类是天气特征。温度、体感温度、风速、降水、天气类型(晴/雨/雪)。天气对骑行影响极大,尤其是降雨。我建议把降水做成二值特征(是否降水)和一个连续特征(降水量)同时喂给模型,效果比单一的降水等级好很多。
第三类是历史统计特征。过去7天同一小时的平均需求、过去24小时的累计需求、前一天同一站点同一时段的需求量。这相当于给模型“抄作业”的机会,因为周期性是共享单车需求最强的信号。
2.3 时空特征怎么落到表格里
很多同学一听到“时空数据”就以为必须用张量或者图结构。其实对于比赛来说,先把时空特征展平成表格,用LightGBM就能跑出不错的结果。
具体做法是:每一行是一个(站点,小时)对,列包括该站点的经纬度、该站点历史需求、周边POI数量(如果数据提供)、该时刻的时间特征和天气特征。
如果你拿到的数据里有站点之间的骑行OD(起终点),那还能构建图结构:站点是节点,OD流量是边权。这种数据可以喂给GNN模型,但对大多数比赛来说不是必须的。
2.4 训练集/验证集划分的时序陷阱
这是很多新手最容易犯的错误:直接用随机划分的方式切分训练集和验证集。但时间序列数据一旦随机打乱,模型会“偷看未来”,验证集上的表现会虚高,真实场景下完全不可复现。
正确的做法是按时间顺序划分,比如用前21天训练、第22到28天验证、最后几天测试。如果数据量足够,还可以做多折时序交叉验证,每一折的训练集都严格早于验证集。
另外还要注意一个细节:如果预测目标是多个站点,划分时不能把所有时刻随机划分,必须保证同一时刻的所有样本都进同一侧,否则会引入信息泄漏。
3. 三套主流建模路线对比
共享单车需求预测的方法论,这些年基本收敛为三套路线,下面我逐一拆解它们的适用场景和优劣。
3.1 路线一:统计基线法——Holt-Winters和SARIMA
别觉得统计方法过时,它们最大的意义是提供“基线”。在比赛中,基线分数决定了你后续模型到底是真有效还是“自我感动”。
Holt-Winters适合捕捉趋势和季节性,SARIMA适合有明确周期的时间序列。共享单车数据有以“天”为周期的强季节性,所以SARIMA的(季节性部分)设置对结果影响很大。
这类方法的优点是计算快、可解释性强,评委容易看懂。缺点是难以加入天气、节假日等外部变量(不是不能加,但很麻烦),而且对站点级别的稀疏数据非常不友好。
我的建议是:不管最后用什么高级模型,先跑一个SARIMA或者周期性均值填充作为Base。如果LSTM或GBDT连这个Base都打不过,说明你的特征或实现有问题,及时止损。
3.2 路线二:机器学习表格流——LightGBM和XGBoost
这是我认为比赛性价比最高的路线。LightGBM对表格数据极其友好,训练快、不容易过拟合、还能自动处理缺失值。XGBoost效果类似,但LightGBM在大数据量下更快。
表格流的核心优势是特征工程灵活。你可以轻松加入天气、节假日、站点属性、历史统计等各种特征。我试过在同样的数据上,LightGBM加好特征工程之后,比单纯LSTM提升10%以上的精度。
具体参数方面,我个人用得比较顺的LightGBM配置大致是:n_estimators 1000左右,learning_rate 0.05,num_leaves 31,max_depth 7,subsample 0.9,colsample_bytree 0.9。当然具体还是要靠验证集调。
3.3 路线三:时空图网络——什么时候才值得上
如果把站点当节点、骑行流量当边,共享单车系统天生就是一个动态图。用GNN(图神经网络)可以显式建模站点之间的空间依赖,这是LightGBM做不到的。
但这部分我劝大家谨慎。GNN的调参难度和训练成本都不是比赛阶段能轻松驾驭的。如果数据量不大,GNN的优势根本体现不出来;如果代码不熟,很容易在这上面浪费大量时间。
我的建议是:如果题目明确给了OD数据,或者站点规模超过500个,且你们队伍有余力,可以考虑把GNN作为一个“亮点模型”写进论文里。否则,用LightGBM作为主模型,把空间信息通过“周边站点平均需求”这种特征表达出来,性价比更高。
3.4 三条路线的取舍与组合策略
最优的做法不是选一条路线死磕到底,而是“多模型融合”。我常用的组合是:
- 用SARIMA生成一列预测值,作为LightGBM的一个额外特征。
- 用LightGBM跑出主预测结果。
- 如果时间充裕,用LSTM或者GNN生成第二个预测值,再做加权平均。
模型融合的提升往往比单纯调参靠谱得多。但要注意,融合的模型之间差异要大才有意义,两个结构几乎一样的模型融合纯属浪费算力。
4. 从0到1手把手跑通一个LightGBM预测流程
4.1 整体流程拆解
下面我以一个模拟的逐小时站点级预测任务为例,演示完整的建模流程。假设我们有:
- 订单表:每一行是一次骑行记录,包含开始时间、开始站点、结束时间、结束站点。
- 站点表:站点ID、经纬度。
- 天气表:逐小时温度、降水、风速。
我们要预测的是:每个站点在每个整点时刻的借车数量(未来24小时,滚动预测)。
整体流程分五步:
- 数据聚合:把订单表按站点和小时聚合成“每小时借车量”。
- 合并特征:把时间、天气、历史统计特征拼接到聚合表上。
- 时序切分:按时间顺序划分训练集和验证集。
- LightGBM训练与调参。
- 预测并反推回原格式。
4.2 数据聚合与特征构建的代码实现
import pandas as pd import numpy as np from datetime import datetime # 1. 读取原始订单数据 orders = pd.read_csv('orders.csv', parse_dates=['start_time']) stations = pd.read_csv('stations.csv') weather = pd.read_csv('weather.csv', parse_dates=['time']) # 2. 聚合:按站点+小时统计借车量 orders['hour'] = orders['start_time'].dt.floor('H') rental_demand = orders.groupby(['station_id', 'hour']).size().reset_index(name='cnt') # 3. 合并站点静态信息 rental_demand = rental_demand.merge( stations[['station_id', 'longitude', 'latitude']], on='station_id', how='left' ) # 4. 提取时间特征 rental_demand['hour_of_day'] = rental_demand['hour'].dt.hour rental_demand['day_of_week'] = rental_demand['hour'].dt.dayofweek rental_demand['is_weekend'] = (rental_demand['day_of_week'] >= 5).astype(int) # 5. 合并天气特征 weather['hour'] = weather['time'].dt.floor('H') rental_demand = rental_demand.merge( weather[['hour', 'temperature', 'precipitation']], on='hour', how='left' ) # 6. 构造历史统计特征:过去7天同一小时同站点的需求量 rental_demand['hour_lag'] = rental_demand['hour'] - pd.Timedelta(days=7) past_data = rental_demand[['station_id', 'hour_lag', 'cnt']].rename( columns={'hour_lag': 'hour', 'cnt': 'cnt_lag7'} ) rental_demand = rental_demand.merge(past_data, on=['station_id', 'hour'], how='left')这段代码里有几个细节值得说。第一,使用floor('H')把时间对齐到整点,避免同一个骑行记录前后相差几分钟导致聚合不一致。第二,历史特征我用了“过去7天同一小时”,这是因为共享单车的周周期性非常强,周期特征比原始滞后特征更稳。第三,天气数据的时间列也必须做同样的 floor 处理,否则 join 的时候会大量掉线。
4.3 训练集/验证集切分与模型训练
from sklearn.model_selection import TimeSeriesSplit import lightgbm as lgb from sklearn.metrics import mean_absolute_error # 按时间排序并指定特征列 rental_demand = rental_demand.sort_values(['hour', 'station_id']).reset_index(drop=True) feat_cols = ['hour_of_day', 'day_of_week', 'is_weekend', 'temperature', 'precipitation', 'longitude', 'latitude', 'cnt_lag7'] # 时序切分:最后7天作为验证集 cutoff = rental_demand['hour'].max() - pd.Timedelta(days=7) train = rental_demand[rental_demand['hour'] <= cutoff].dropna(subset=feat_cols) valid = rental_demand[rental_demand['hour'] > cutoff].dropna(subset=feat_cols) # 训练LightGBM model = lgb.LGBMRegressor( n_estimators=1000, learning_rate=0.05, num_leaves=31, max_depth=7, subsample=0.9, colsample_bytree=0.9, random_state=42 ) model.fit( train[feat_cols], train['cnt'], eval_set=[(valid[feat_cols], valid['cnt'])], callbacks=[lgb.early_stopping(50), lgb.log_evaluation(100)] ) # 验证集评估 pred = model.predict(valid[feat_cols]) mae = mean_absolute_error(valid['cnt'], pred) print(f'Validation MAE: {mae:.4f}')这里我用了dropna而不是填充。原因是cnt_lag7如果缺失,说明过去7天该站点没有历史数据,这类样本的特征本身不可靠,直接剔除比填0更干净。如果历史数据丢失的站点较多,再考虑用所有站点的中位数填充。
early_stopping(50)这个参数非常重要,它能在验证集指标连续50轮不提升时自动停止训练,防止过拟合,也帮你省掉手动找最优迭代次数的麻烦。
4.4 结果解读与后处理
预测结果出来后,别急着写论文。先做两件检查:
第一,检查预测值是否有负值。LightGBM输出的回归值理论上可能为负,但租车量不可能小于0。直接把负值截断为0是标准做法。
第二,分时段看误差。比如早晚高峰的误差是否远大于夜间?如果高峰误差大,说明特征里还缺“站点承载能力”或者“周边竞争站点”的信息。再比如雨天误差大,说明天气特征可能需要更细的粒度。
我把这几步走完,通常在竞赛类数据上能把MAE压到基线的60%左右。这时候再去优化结构或者上深度学习,才是合理的节奏。
5. 竞赛实战中的常见问题与排查
5.1 指标老是上不去?先查这三个地方
模型预测分数一直提不上去时,很多人第一反应是换模型或者调参。但根据我的经验,应该按以下顺序排查:
第一个排查点是特征泄漏。检查验证集划分是否混入未来信息,检查历史统计特征是否包含预测时刻之后的数据。特征泄漏会让训练集和验证集上的表现差异产生矛盾——训练集得分异常好,验证集却很差。
第二个排查点是目标分布。如果某些站点需求极度稀疏(比如绝大多数小时都是0),模型会倾向于全预测0,因为这种预测的平均误差最小。这种情况下,可以考虑分站点建模,或者用泊松回归设置适合“计数型”目标的损失函数。
第三个排查点是特征粒度。共享单车需求的空间关联性很强,如果你的特征矩阵里没有“周边站点需求均值”或“最近站点距离”,空间信息就完全缺失了。这时可以构造一个“站点热度”特征:以该站点为中心,半径500米内所有站点在该小时的平均需求,效果非常明显。
5.2 特征泄漏的典型表现和自查方法
特征泄漏是比赛中容易丢分又不容易察觉的问题。我总结几个典型表现:
- 用未来的天气数据做预测。题目如果只要求预测未来24小时,但天气数据给了未来72小时,你可以用,但必须说明“假设未来天气可精确预报”。如果不说明,评委大概率当你不严谨。
- 用预测时段内的实际需求构造历史统计特征。比如你想预测第28天,却用了第28天当天中午的数据来构造早高峰特征,这属于铁板钉钉的泄漏。
- 用目标值本身做标准化。有些队伍把整个数据集的均值方差存下来,对测试集也用训练集的统计量,这本没错,但如果不小心用验证集和测试集的数据一起算统计量,就是泄漏。
自查方法很简单:把特征列一个个过一遍,问自己“如果在预测时刻,这个特征的数值是否已经真实存在”。只要答案是“否”,就必须删除或替换。
5.3 参数调优心得
LightGBM调参不需要狂跑随机搜索。我通常按这个顺序调:
先调num_leaves和max_depth,这两个控制模型复杂度。num_leaves太大容易过拟合,尤其在训练数据少的情况下。再调learning_rate和n_estimators,它们是一对组合,低学习率加早停通常能拿到稳定结果。最后调subsample和colsample_bytree,这俩是防过拟合的调节阀。
另外分享一个细节:如果你发现模型在验证集上存在“高峰时段误差大、夜间误差小”的现象——注意文本中的“高峰”是指峰值时段——不需要强行调参,可以针对高峰时段单独训练一个模型,然后用加权平均融合,效果往往比全局调整好。
5.4 优秀论文里的“加分写法”
模型跑完只完成了一半工作。比赛是“算法50%+论文50%”,论文写不好,模型再强也可能被埋没。
我在几篇获奖论文里看到过几个共性写法,值得借鉴:
第一,画图要抓核心矛盾。模型精度用时间序列折线图展示,不用全画,抽一周数据,把真实值和预测值叠在一起,突出高峰时段的拟合效果。再画一张站点预测误差热力图,让评委一眼看出误差集中在哪里。
第二,把“为什么选这个模型”写清楚,不要只是甩形状。比如“因为共享单车需求具有强周期性,而LightGBM可以通过滞后特征有效捕捉这种周期,因此选用表格模型而非深度网络”,这比“LightGBM效果很好”有说服力得多。
第三,写清楚你的方法在计算资源上的成本。评委很看重实用性,如果你们用了GNN,要写明训练时长和推理时长;如果用了融合模型,要写明各模型的权重及确定过程。这些细节能看出队伍真正想清楚了问题。
写在最后的几条实战体会
这套流程我自己带队伍用过两届,也在复盘其他获奖论文时反复对照过。最大的体会是:共享单车需求预测这道题,真正的难点不是模型有多深多新,而是你能不能在没有明确指导的情况下,自己把数据到预测结果的链路走通。建模只是一个环节,数据理解了会占到一半以上的工作量。
再分享一个小技巧:预测结果出来后,可以多做一个“可视化巡检”而不是只看指标分数。把某个站点一周的真实需求和预测值画在一张图上,肉眼看一遍,模型哪些地方学到了、哪些地方偷懒没学,立刻一目了然。这一步花不了多少时间,但往往能发现指标看不出的问题。
如果你比赛时间还宽裕,可以在这个思路上再加一步:把预测结果作为调度模型的输入,做一个简单的“站点调度模拟”,验证你的预测值放到实际业务场景里是否合理。这样你的论文就不只是一篇预测报告,而是真正讲完了一个“预测-决策”的完整故事。