项目介绍
我在这个课程项目中尝试解决两个相连的问题:一个站点在指定小时会借出、还入多少辆车,以及根据这些需求和现有供给,可以给出怎样的价格建议。项目使用官方历史骑行记录,完成了数据整理、模型训练、价格规则和网页演示,也保留了代码、看板与课程报告。离线预测实验和接口演示分别实现,当前接口尚未调用训练好的 Spark 模型。
项目阶段
- 01
读取骑行记录,分阶段保存数据
项目整理了 2026 年 1—5 月的 CitiBike 官方骑行记录。约 1,453 万行数据记录的是一次次骑行,包括借出和归还时间、起终点站及车辆类型。要讨论一个站点下一小时的供需,首先需要把这些记录整理成可以重复处理的数据。
查看处理方法与依据收起技术细节
- 02
把一次骑行转换为站点的借还车事件
清洗代码先转换时间字段,按 ride_id 去除重复骑行,并移除缺少起点或终点站编号的记录。随后,一次骑行被拆成两个事件:出发时刻所属站点增加一次借车,到达时刻所属站点增加一次还车。两次事件可能落在不同小时。
查看处理方法与依据收起技术细节
- 03
补充时间、容量和天气信息
小时表加入月份、星期、小时、周末、节假日和高峰时段等信息。小时等周期字段用正弦、余弦编码,让模型能够表达一天结束后又回到下一天开始的关系。站点容量通过经纬度与站点信息表进行邻近匹配,代码设置了 100 米的匹配距离上限。
查看处理方法与依据收起技术细节
- 04
分别训练借车量与还车量模型
借车与还车分别作为预测目标。仓库提供随机森林和梯度提升树两类回归模型:前者组合多棵决策树的结果,后者逐步补充模型以减少预测误差。训练、验证和测试使用不同的数据,测试指标用于比较预测值与实际数量的差距。
查看处理方法与依据收起技术细节
- 05
把供需数量转换成有理由可查的价格建议
价格计算接收借还车需求、当前可用车辆数和空车位数,分别计算车辆与车位的剩余或缺口。规则根据缺口相对于现有供给的大小,区分缺车、还车拥挤、供给充足和基本平衡等情况,给出价格倍率及对应解释。
查看处理方法与依据收起技术细节
- 06
用接口和看板展示需求与定价的使用方式
FastAPI 将输入与输出组织成网页或其他程序可以调用的接口,Pydantic 检查请求中的字段。页面提供单次预测、站点逐时预测和热点站点入口;看板与答辩展示用于阅读需求分布和价格建议。
查看处理方法与依据收起技术细节
- 07
用三组对照检查字段与时间划分的影响
项目完成后,工作台读取清洗、聚合并补充特征后的Silver小时表,先做输入质量检查,再计算参考预测并审查结果依据。随后项目补充三组修复实验:原字段配随机划分、移除风险字段后配同一随机划分、移除风险字段后按时间划分。同一个小时的全部站点在时间划分中只能进入同一个集合。
查看处理方法与依据收起技术细节
滚轮缩放 · 拖动空白处移动 · 点击节点阅读
读取骑行记录,分阶段保存数据
项目整理了 2026 年 1—5 月的 CitiBike 官方骑行记录。约 1,453 万行数据记录的是一次次骑行,包括借出和归还时间、起终点站及车辆类型。要讨论一个站点下一小时的供需,首先需要把这些记录整理成可以重复处理的数据。
查看处理方法与依据收起技术细节
先看数据怎样变化,再看模型、价格与展示。图形展示主要变化,处理方法和代码依据可以按需展开。
阶段一至三:将骑行记录整理成模型输入
阶段一至三参考 Medallion 分层数据处理方法:按处理程度分别保存原始、清洗和汇总结果。
一行是一次骑行
保留原始文件一行仍是一次骑行
保存清洗结果一行是一个站点的一小时
保存汇总结果1读取并清洗骑行记录
程序读取约 1,453 万条官方骑行记录,统一时间、按骑行编号去重,并处理缺少起终点站的记录。清洗后每行仍代表一次骑行,结果保存为 Bronze 文件,供后续处理继续读取。
PySpark 负责批量读取和清洗 · Parquet 按列保存中间结果
查看处理方法与依据收起处理方法与依据
一行仍是一次骑行
骑行表通过编号关联
| 处理对象 | 具体处理 | 目的 |
|---|---|---|
| 借出、归还时间 | 将 started_at、ended_at 转为时间类型。 | 计算骑行时长,归入对应小时。 |
| 骑行编号 | 按 ride_id 去重。 | 避免同一次骑行重复计数。 |
| 起终点站编号 | 删除任意一端站点编号缺失的记录。 | 让借还事件能够归到具体站点。 |
| 站名与经纬度 | 提取到站点表,骑行表保留站点编号。 | 减少重复保存的站点资料。 |
PySpark 怎样处理? 使用 Python 编写处理逻辑,由 Spark 按分区执行读取、清洗和汇总。
为什么分阶段保存? 参考 Shakleen 项目的 Medallion 方法,按处理程度保存数据,后续实验可以从已经完成的步骤继续。Parquet 按列保存,后续可以选择读取需要的字段。
2按站点和小时统计借还数量
程序将一次骑行拆成借出、还入两个事件,分别计入对应站点和小时,得到约 438.8 万条站点小时记录。跨小时骑行的借出与归还会进入不同的统计时段。
PySpark 负责事件拆分 · 按“站点编号+小时”分组求和
查看处理方法与依据收起处理方法与依据
| 站点 | 时段 | 借出次数 | 还入次数 |
|---|---|---|---|
| A 站 | 08:00—09:00 | +1 | 0 |
| B 站 | 09:00—10:00 | 0 | +1 |
PySpark 读取清洗后的 4 个 Parquet 数据文件,将事件时间归到所在小时,再按站点和小时分组求和。同一站点、同一小时的事件即使分散在不同文件,也会一起统计。
原 Silver 根据实际发生的借还事件汇总,没有完整补齐所有站点、所有小时的零需求记录。
3补充信息,准备模型输入
小时表补充时间、天气和站点容量等信息,训练入口再选择需要的字段。项目还生成过标准化和 PCA 处理后的 Gold 文件,但最终训练实际读取 Silver。
PySpark 添加与关联字段 · 经纬度邻近匹配站点 · 标准化与 PCA 处理 Gold
查看处理方法与依据收起处理方法与依据
| 信息类别 | 来源与处理方式 | 原版训练 |
|---|---|---|
| 时间信息 | 从时间提取月份、星期、小时、高峰等,并添加小时、星期的周期编码。 | 使用 13 个字段。 |
| 骑行统计 | 按站点和小时计算电动车比例、会员比例和平均骑行时长。 | 原版使用 3 个,后续修复移除。 |
| 天气信息 | 按日期关联平均气温、累计降水和最大风速。 | 使用 3 个字段。 |
| 站点容量 | 按经纬度邻近关系匹配站点资料,取得容量。 | 保存在数据中,最终训练未使用。 |
电动车比例该小时借出的电动车次数 ÷ 总借出次数
会员比例该小时会员借车次数 ÷ 总借出次数
平均骑行时长该小时借出骑行的总时长 ÷ 总借出次数
没有借出事件时,代码将这三个统计值设为 0。三个字段依赖目标小时的完整骑行结果,因此后续修复将它们从预测输入中移除。
用 sin/cos 编码表达时间的循环关系,使 23 点与 0 点在数值表示上也能体现相邻关系。
4尝试自动调参,再用预设参数完成训练
尝试多组参数,比较验证集误差
完成部分搜索,后续因内存不足中断固定参数,分别训练借出与还入模型
完成训练、评价并保存模型先尝试用 Hyperopt 搜索参数,后续运行因内存不足中断,最终使用预设参数完成训练。分别预测站点每小时的借出量和还入量。
PySpark MLlib 训练模型 · Hyperopt 搜索参数 · RMSE 与 R² 评价结果
查看处理方法与依据收起处理方法与依据
先尝试自动搜索参数
Hyperopt 在代码设定的范围内尝试参数;每次用训练集训练,用验证集评价。
- 1
抽样并留出验证数据
Silver → 抽取约 50 万条记录80% 训练集用于训练模型20% 验证集用于比较参数按比例抽样,实际行数会浮动。保存的早期搜索记录使用 11 个时间字段。
- 2
设置允许搜索的参数范围
Hyperopt 从这些范围中选择参数 参数 随机森林 梯度提升树 控制什么? 树数量/提升轮数 20—150,间隔 10 20—100,间隔 10 树的数量或提升轮数 最大深度 5—25 5—20 树最多划分多少层 最大分箱数 16—48 16—48 连续字段最多分成多少个区间 子节点最少样本数 2—50 2—50 划分后每个子节点至少保留的记录数 训练采样比例 0.5—1.0 0.5—1.0 每棵树或每轮训练使用的采样比例 深度、分箱数和样本数取整数。“间隔 10”表示树数量可以取 20、30、40……,这些范围由代码预先设置。
- 3
反复训练,比较验证集误差
提出一组参数例如 90 棵树、最大深度 14↓使用训练集训练借出和还入是独立的搜索任务↓预测验证集计算 RMSE,并记录 R²↓记录参数和误差以验证集 RMSE 最小为目标↶ 继续尝试下一组参数RMSE 是预测数量与实际数量的误差指标,越小越好;具体计算过程见下方“怎样评价模型”。
- 4
保存已完成的搜索结果
四类任务留下了三份完整搜索结果 模型 预测借出量 预测还入量 随机森林 8 次试验 8 次试验 梯度提升树 8 次试验 没有训练结果 查看已保存的最佳参数
搜索任务 树数量/轮数 深度 分箱数 最少样本数 采样比例 随机森林 · 借出 90 14 29 23 约 50.93% 随机森林 · 还入 80 13 30 38 约 50.64% 梯度提升树 · 借出 90 6 26 14 约 74.31% 后续尝试因内存不足中断。最终训练使用了预设参数。
改用预设参数,完成最终训练
使用预设参数训练两个模型
读取 Silver 的 19 个输入字段,分别学习同一小时的借出量与还入量。
- 1
将完整数据随机分成三组
约 438.8 万条 Silver 站点小时记录80% 训练集约 351 万条10% 验证集约 44 万条10% 测试集约 44 万条集合 用途 训练集 学习字段与借还量之间的关系。 验证集 检查预测表现,供方案比较。 测试集 独立评价最终模型。 按整行随机分配:输入字段与实际借还量一起进入同一组。随机种子固定为 42,实际行数略有浮动。
- 2
用同一组预设参数,分别训练借出与还入模型
最终随机森林使用的参数 参数 实际设置 含义 每个模型的树数量 50 棵 借出、还入模型各有 50 棵树 最大深度 10 限制一棵树的划分深度 最大分箱数 24 限制连续字段的分箱数量 子节点最少样本数 10 条 每个子节点至少保留 10 条记录 训练采样比例 80% 与训练/验证/测试分组的比例不同 借出模型19 个输入字段 → 借出量50 棵回归树还入模型19 个输入字段 → 还入量50 棵回归树每棵树怎样学习?
回归树是预测数值的决策树。程序在训练记录中尝试字段和切分位置,选择能够减少分组内借还量差异的划分;最后用末端分组的平均数量给出预测。
假设示例:是否处于高峰时段?否 → 再根据其他字段划分是 → 再根据其他字段划分每棵树给出数量预测 → 随机森林取平均
每棵树可以使用多个字段,没有固定的“时间树”或“天气树”。字段候选具有随机性,具体分支根据训练数据选择;图示不代表保存模型的真实分支。
- 3
计算预测误差,保存模型和结果
验证集和测试集 → 分别预测 → 分别计算 RMSE、R²原版随机森林的评价结果 模型 验证 RMSE 验证 R² 测试 RMSE 测试 R² 借出模型 3.043 0.641 3.038 0.644 还入模型 3.844 0.436 3.848 0.436 RMSE 和 R² 怎样评价模型?
每条记录的预测值 − 实际值↓将每个差值平方↓求平均,再开平方↓得到 RMSE,越小表示误差越小RMSE 与预测数量使用相同单位,较大的误差受到更重的影响。R² 衡量模型相对“总是预测实际值均值”的改善程度,它不是准确率,也可能为负数。
这些成绩对应原版字段和随机切分。后续字段与切分修复见阶段七 ↗
早期搜索记录使用 11 个时间字段,最终训练使用 19 个字段。这是先后进行的两次训练尝试,分数差异不能只归因于参数选择方式。
5将供需数量转换成价格建议
选择一个场景,查看输入与价格建议。
可用 10 辆、空车位 20 个;需求参考为借出 20 辆、还入 5 辆。
车辆缺口比例为 −1,按缺车规则计算并取上限 1.50 倍。
根据车辆与空车位的供需差,按预设公式计算价格倍率,返回建议价格和调整原因。
Python 计算供需差 · 阈值与上下限约束价格倍率
查看处理方法与依据收起处理方法与依据
先比较供需,再计算倍率和价格
价格规则由程序预先设置。当前接口从历史数据取得借还需求参考,再结合提交的车辆数和空车位数计算建议价格。
b = 车辆供需差 ÷ max(当前车辆数, 1);d = 车位供需差 ÷ max(当前空车位数, 1)。max 表示取较大值,供给为零时用 1 作分母。负值表示不足,正值表示有余量。
程序采用第一条满足条件的规则
| 顺序 | 情况 | 触发条件 | 倍率公式 | 限制 |
|---|---|---|---|---|
| 1 | 车辆与车位同时不足 | b < −0.5,且 d < −0.5 | 1.3 + |b + d| ÷ 2 × 0.7 | 最高 2.0 |
| 2 | 缺车 | b < −0.3 | 1 + |b| × 0.6 | 最高 1.5 |
| 3 | 车位不足 | d < −0.3 | 1 + |d| × 0.4 | 最高 1.3 |
| 4 | 车辆、车位都充足 | b > 0.5,且 d > 0.5 | 1 − min(b, d) × 0.25 | 最低 0.7 |
| 5 | 车辆充足 | b > 0.5 | 1 − b × 0.15 | 最低 0.8 |
| 6 | 其余情况 | 以上条件均未满足 | 1.0 | 保持基础价格 |
| | 表示绝对值,min 表示取较小值。程序先将倍率保留两位小数,再乘以基础价格,将金额保留两位小数。
看一次完整计算:10 辆车,借出需求为 20 辆
$4.49 是项目设定的基础价格。当前完成了价格规则演示,尚未通过实际运营实验检验调价效果。
6通过接口和看板展示结果
接收一次查询
↓计算并返回价格建议
汇总已有数据
↓展示需求、价格与站点差异
演示一次查询 → 查看返回内容
以下使用假设输入说明请求和返回内容。
接口负责接收查询并返回需求参考和价格建议;看板负责把已有分析结果画成图,方便观察不同时间、站点的情况。
FastAPI 提供接口 · Pydantic 检查请求字段 · Pandas 读取数据 · Pyecharts 制作图表
查看处理方法与依据收起处理方法与依据
一次查询怎样得到价格建议
FastAPI 提供程序调用入口;Pydantic 检查请求字段的类型和约束;Pandas 读取与筛选历史数据。当前接口使用历史数据提供需求参考。
| 功能 | 提交什么? | 得到什么? |
|---|---|---|
| 查询一次价格建议 | 站点、时间、当前车辆数和空车位数 | 借还需求参考、倍率、建议价格和原因 |
| 查看未来 24 小时的演示结果 | 站点、当前库存和站点容量 | 逐小时的需求参考和价格建议 |
| 查看站点排行 | 发起排行查询 | 历史分析数据中,平均价格倍率最高和最低的各 5 个站点 |
24 小时演示怎样更新库存?
程序用这个方式模拟库存变化;需求参考仍来自历史数据。历史文件不存在或为空时,接口使用高峰、周末等预设时段规则。
看板把已有结果展示成图
接口方便其他程序提交查询;看板方便使用者观察整体结果。模型训练与当前接口分别实现,接口收到请求时使用已有数据计算价格。
7复核数据,修复后做冒烟测试
保留原来 19 个字段
随机切分
↓作为本轮比较起点
移除 3 个字段
随机切分
↓观察删除字段的影响
移除 3 个字段
时间切分
↓检查按时间训练的表现
复核后,我移除了三个需要等目标小时结束才能计算的字段,并加入时间切分。随后用同一批 2.4 万条记录进行了三组对照,检查修复后的训练流程和预测表现。
移除三个字段 · 按时间切分 · PySpark 运行三组对照
查看处理方法与依据收起处理方法与依据
移除三个需要事后统计的字段
| 字段 | 统计的是什么? | 为什么移除? |
|---|---|---|
| 电动车比例 | 该小时借出骑行中,电动车的占比 | 需要该小时完整的借出记录,小时开始前无法得到。 |
| 会员比例 | 该小时借出骑行中,会员的占比 | |
| 平均骑行时长 | 该小时借出骑行的平均时长 | 还需要等待相应骑行结束。 |
移除三个字段后,模型保留 16 个输入字段。三组都使用相同的预设随机森林参数,使比较重点放在字段与分组方式上。
第三组按小时先后划分数据
| 集合 | 使用的时间段 | 记录数 |
|---|---|---|
| 训练集 | 1 月 1 日至 5 月 2 日 23:00 | 18,343 |
| 验证集 | 5 月 3 日 00:00 至 5 月 17 日 12:00 | 2,844 |
| 测试集 | 5 月 17 日 13:00 至 5 月 31 日 23:00 | 2,813 |
比例按小时数划分。不同小时的记录数不同,因此记录数不必恰好为 80%、10%、10%。这样用较早的数据训练,再用较晚的数据评价。
用三组结果检查修复后的变化
| 组别 | 字段与切分方式 | 借出量测试 R² | 还入量测试 R² |
|---|---|---|---|
| 第一组 | 19 个字段 · 随机切分 | 0.633 | 0.464 |
| 第二组 | 16 个字段 · 相同随机切分 | 0.121 | 0.116 |
| 第三组 | 16 个字段 · 时间切分 | 0.095 | 0.101 |
第一、二组使用相同记录进行训练和评价,删除三个字段后分数明显下降。第三组进一步检查按时间训练的表现;它使用了不同的测试记录。
冒烟测试用于确认修改后的关键流程能够完整运行。本次没有保存新模型,也没有替换原接口和看板;表中成绩对应这批样本。
图片与材料
点击文件夹展开,再点击图片放大。
旧版产品看板:保留完整业务演示,但明确标注为Legacy课程版。
分析结果
- 01
第一版随机森林的测试集R²为借出量0.644、还入量0.436;梯度提升树分别为0.142和0.139。在当次字段、参数与随机划分条件下,随机森林的两项结果均更高。R²用于衡量预测对实际数量变化的解释程度,不能直接当作准确率。
- 02
第一版随机森林的借出量RMSE约为3.04,还入量约为3.85。RMSE是预测误差平方平均后再开方的结果,单位为次/站点小时;这两项数值记录了原实验中的误差水平。
- 03
第一版形成了按车辆与车位供需缺口计算价格倍率、建议价格和理由的策略结果,并通过看板、接口及答辩展示呈现需求与定价的使用方式。价格建议属于规则模拟,尚无真实运营收益验证。
- 04
后续修复删除三个同小时统计字段,改用时间切分,并完成2.4万行三组冒烟测试;该结果用于核对字段与切分的影响,尚未完成全量正式重训。
复核与修复记录
复核发现,电动车比例、会员比例和平均骑行时长取自目标小时的完整骑行统计,第一版又采用随机切分,因此原指标只能按该实验条件解释。修复分支删除这三个字段,改按时间切分,并完成2.4万行三组冒烟测试。全量模型重训和旧接口、看板更新尚未完成;保留的天气字段还需核对预测时刻是否可取得。
我与 AI 如何完成项目
首版:Claude Code + DeepSeek Pro|复核修复:Codex + ChatGPT-sol
项目由本人独立完成。我负责确定业务问题、判断数据粒度、选择技术路线、拆分阶段、检查模型指标、解释工作台疑点、决定修复范围,并确认哪些结果能够公开。AI负责调研参考项目、展开代码实现、生成测试与文档初稿;最终的指标解释、错误修正和停止条件由本人确认。
首版方案怎样形成
我先把课程要求和动态定价想法交给Claude Code与DeepSeek Pro,再让AI查找需求预测、站点供需和价格激励的已有项目。方案经过讨论后被拆成数据整理、特征工程、模型训练、策略计算、API和展示页面。
AI在实施阶段负责什么
ai主要生成PySpark、Pandas、scikit-learn、FastAPI和可视化代码,并参与方案讨论与解释。我逐阶段阅读输出、运行程序、检查文件和指标,并决定是否进入下一阶段。
使用工作台对数据进行复核
Codex 在旧项目代码中识别出三个依赖目标小时统计的风险字段,工作台按配置清单检查 Silver 小时表,再计算参考预测并审查依据。这次工作台运行没有调用内置 AI;字段判断、代码修复和结果解释由外部代码核对与后续讨论完成。
我怎样决定停止位置
修复版完成了安全字段筛选、时间切分和冒烟测试以后,我保留了三组比较结果及修复记录。全量重训、天气字段可用时间的核对,以及旧API与看板更新仍列为后续工作,当前展示明确说明已完成的范围。
参考与使用说明
Shakleen / CitiBike-Demand-Prediction ↗
参考了大规模CitiBike数据处理、需求预测和Web接口的整体技术路线;本项目重新选择了数据时间范围、聚合口径、特征和模型。
Bikenomics — NYC Citibike Optimization
本地保留了该项目的README快照,参考了站点供需、价格激励和用户引导的产品思路。原仓库地址没有保留,因此网站暂不提供外部链接。
结果适用范围
- 2.4万行结果用于验证修复后的代码、数据切分和指标计算能够完整运行,不能代表千万级数据完整重训后的最终精度。
- 当前FastAPI和旧可视化仍用于展示课程版产品链路,页面中会明确标注Legacy边界。
- 动态定价属于策略模拟,不代表真实商业环境中的因果收益。