PySparkRandom ForestHyperopt动态定价

城市共享单车
动态定价策略

基于纽约 CitiBike 大数据的站点需求预测与智能定价

大数据管理与应用 · 课程设计 Topic 2 · 2026年6月

1 / 16

开源参考与项目总览

Shakleen · CitiBike-Demand-Prediction

借鉴:Medallion数据管道、事件拆分、双目标回归、训练流水线

用于:阶段二、阶段四

Charan · CitiBike_BigData (Bikenomic)

借鉴:供需缺口定价思路、站点空间推荐逻辑

用于:阶段五

自研:空间匹配 · 业务特征挖掘 · 天气接入 · Hyperopt调参 · FastAPI+PyEcharts · 23个测试
1
环境+数据
2
PySpark
数据管道
3
特征
+降维
4
模型
+调参
5
定价策略
6
API
+看板
7
测试
+交付
借鉴: Shakleen架构 + Charan定价思维 自研: 空间匹配/Hyperopt/FastAPI/PyEcharts/天气接入
2 / 16

阶段一+二:环境搭建 & PySpark 数据管道

  • Python venv + JDK 17 + PySpark 4.1.2 + winutils/hadoop.dll
  • 下载 CitiBike 2026年1-5月数据(16个CSV,2.7GB)
  • 下载 GBFS 站点元数据(2412站)+ 天气数据(151天)
  • 四层 Medallion 管道:Raw → Bronze → Silver → Gold
原理:Medallion = 像淘金一样逐层提纯。每层是存档点,崩了不重跑。
PySpark 把1450万行切块并行处理 → 不吃内存。Parquet 列式压缩比 CSV 小 5-10倍。
1,450万
骑行记录
438万
站点-小时
1.3 GB
Bronze
2,433
站点
Raw CSV2.7GB · 1450万条
"原样读进来,什么也不改"
▼ 时间统一 · 去重 · 站点拆分
Bronze1.3GB · 1450万条
"干净但仍是原始记录,一行=一次骑行"
▼ 事件拆分 · 站点×小时聚合 · +特征 · +天气
Silver73MB · 22字段 · 438万条
"按站点+小时汇总,一行=一个站一小时的情况"
▼ 标准化 · PCA(5主成分)
Gold188MB · 训练矩阵
"标准化+压缩后,直接喂给模型训练"
借鉴: Shakleen Medallion管道 改造: Delta Lake→Parquet · 简化时区分支
3 / 16

阶段三:特征工程 — 从 11 个到 19 个特征

  • 自研 空间匹配:cKDTree+经纬度最近邻 → 两套站点ID打通(99.3%匹配)
  • 自研 业务特征:从 rideable_type / member_casual 挖掘电动车比例、会员占比
  • 自研 天气接入:Open-Meteo API 获取151天 气温/降水/风速
  • 时间特征:Cyclic Encoding(sin/cos) + is_weekend + is_rush_hour
Cyclic Encoding 原理
用 sin/cos 编码小时和星期几——让23点和0点在数学上变成相邻值(普通数字编码做不到:23和0差得远,但实际它们挨着)。

19个特征完整清单(括号=中译)

11 时间特征

month(月份)· day(日)
weekday(星期几)· weekofyear(第几周)
dayofyear(第几天)· hour(小时)
is_weekend(是否周末)
is_rush_hour(是否早晚高峰)
hour_sin · hour_cos(小时周期编码)
weekday_sin · weekday_cos(星期周期编码)
is_holiday(是否假期)

5 业务

electric_ratio(电动车占比)
member_ratio(年费会员占比)
avg_trip_duration_min(平均骑行时长)
capacity(站点桩位数)
total_events(总事件数)

3 天气

temp_c(气温)· precip_mm(降水)
wind_kmh(风速)

借鉴: Shakleen 周期编码 + 节假日标注 自研: 空间匹配/天气/业务特征
4 / 16

阶段三:PCA 降维 — 11维时间特征 → 5个主成分

5 个主成分解释

PC1 日周期节律 (34.8%) — hour_sin + hour_cos + is_rush_hour。描述"一天中哪个时段"

PC2 季节性趋势 (21.5%) — month + dayofyear + weekofyear。描述"一年中哪个季节"

PC3 周周期模式 (20.7%) — weekday + is_weekend + weekday_sin/cos。描述"工作日 vs 周末"

PC4 节假日效应 (11.6%) — is_holiday + day。节假日偏离正常模式的独立信号

PC5 残余变异 (11.5%) — 前 4 个没覆盖到的微小差异(如特殊活动)

PCA 做了什么?为什么需要它?

11 个时间特征中有很多"在说同一件事"——比如 hour、hour_sin、hour_cos 三个都在描述时间。PCA 把说同一件事的特征合成一个主成分,从 11 个压缩成 5 个。累计方差 100% = 压缩过程没有丢失任何信息,只是去掉了冗余。

自研: PCA 5主成分命名 + 物理含义解释
5 / 16

阶段四:模型训练 & R² 进化

三轮特征迭代 — R² 进化

v1 纯时间
R² = 0.135
v2 +业务特征
R² = 0.631 ↑ 4.7×
v3 +天气容量
R² = 0.644
模型是怎么训练的
把 350 万条历史数据切成 80% 训练集 + 10% 验证集 + 10% 测试集。模型在训练集上"看例子学习"(每条数据有 19 个特征值 + 正确答案借出量),自己总结出规律,然后在没见过的测试集上检验准不准。Random Forest 的原理:造 100 棵决策树,每棵树用随机抽样的数据训练,最终 100 棵树投票取平均值。

模型:Random Forest (PySpark MLlib)

100棵树 · Bagging抗过拟合 · 自带特征重要性

350万训练数据 · R²=0.644, RMSE=3.04

特征重要性 Top-4

电动车比例0.372
会员占比0.359
平均骑行时长0.158
小时(cos编码)0.024

前3个业务特征合计 = 0.889 vs 所有时间特征合计 = 0.111

借鉴: Shakleen RF/GBT 训练流水线 自研: Hyperopt调参 · 三轮特征工程
6 / 16

Hyperopt 贝叶斯超参数搜索

自研 Hyperopt 贝叶斯自动调参

"搜索空间" = 告诉算法"你可以试的参数范围":
每棵树最多长多深 (maxDepth:5-25)、造多少棵树 (numTrees:20-150)、每次用多少数据 (subsamplingRate:0.5-1.0) 等 5 个。

贝叶斯优化 — 用找餐厅类比
阶段1"乱试建印象":先随机抽几个点测试(瞎吃几家)。
阶段2"围绕好的深挖":发现"这条街川菜评分普遍高"→ 后面集中试这片区域。规律 = 好参数的附近往往也是好参数。
阶段3"不放弃开荒":已经试过的差区域果断放弃。但还没去过的"陌生区域"仍然可能藏着最优解,偶尔去探一两次——不把鸡蛋全放在已知好区域。
最终:8轮就锁定最优组合。穷举要把整条街全吃遍(可能几百次),贝叶斯只需要 8 次。
8
贝叶斯搜索轮次
5
超参数维度
50万
每次训练样本

模型对比:RF vs GBT

指标Random ForestGBT
0.6440.142
RMSE3.044.71
训练时间57s44s

GBT(梯度提升树)= Random Forest 的表兄弟。RF 是 100 棵树同时训练、投票平均;GBT 是一棵一棵串行训练,每棵新树专门修正前面所有树的错误。训练更快(44s vs 57s),但在我们只有时间特征时 R² 很低(0.14),加入业务特征后内存不够——因此选了 RF 做主模型。

借鉴: Shakleen RF/GBT 模型定义 自研: Hyperopt TPE 贝叶斯搜索 + 8轮实验
7 / 16

阶段五:动态定价策略 核心创新

空桩充裕
空桩紧张
车少
车多
SURGE 涨价
1.2 – 1.5×
车不够 → 引导去附近站
DISCOUNT 降价
0.7 – 0.9×
车和桩都充裕 → 降价引流
MAX SURGE
1.5 – 2.0×
既没车又没桩 → 最高封顶2倍
MILD SURGE
1.1 – 1.3×
桩不够 → 阻止还车拥堵
定价三步计算流程
1. 模型预测 下个小时缺多少车、多多少桩
2. 计算缺口 bike_gap = 现有车辆 - 预测需求。负值 = 不够用
3. 映射定价 缺口落到 2×2 矩阵的哪个象限 → 对应乘数
为什么用矩阵 = 每个决策可解释——"为什么涨价?"→"预测缺25辆车",不是黑箱。

设计理念

"预测 + 规则"解耦:ML模型负责准、规则引擎负责解释

19个特征去哪了?为什么定价只看车和桩?

19个特征 = 模型的知识。它们被训练时"喂"给了模型,所以模型看到"电动车多+会员多+早8点"就知道这个站需求高。但定价引擎只需要模型的输出数字(预测借出量、预测还入量)+ 当前库存。就像天气预报用了卫星云图、气压、湿度算出"明天80%下雨"——你出门只需看这个80%来决定带不带伞,不需要看卫星云图。

0.7%
Surge · $6.72
9.3%
Normal · $4.49
89.8%
Discount · $3.56
借鉴: Charan Bikenomic 供需缺口定价 自研: 四场景量化规则 + 2×2矩阵框架
8 / 16

四场景详解 & 定价分析

Surge 涨价 1.2-2.0×

借出需求 > 现有车辆 → 缺车涨价,引导用户步行 200米去附近有空余的站

Mild Surge 1.1-1.3×

还入需求 > 空桩数 → 站点快被塞满,阻止还车

Normal 标准定价 1.0×

供需基本平衡 → 维持 CitiBike 基础价 $4.49

Discount 降价 0.7-0.9×

车辆+空桩充裕 → 降价吸引用户来此站借车,消化闲置

全量分析结果:438万条记录

Surge(涨价):30,278条 (0.7%) · 均价 $6.72
Mild Surge:10,144条 (0.2%) · 均价 $5.25
Normal:406,000条 (9.3%) · 均价 $4.49
Discount:3,941,937条 (89.8%) · 均价 $3.56

离线 vs 在线
折扣占比 89.8% 是因为离线分析假设所有站初始半满。实际生产环境通过 GBFS 实时 API 获取动态库存后分布将更均衡——这是"生产化改进方向"的加分项。
借鉴: Charan Bikenomic 供需缺口定价逻辑 自研: 四场景量化规则 + 定价三步法
9 / 16

阶段六:FastAPI 服务 & PyEcharts 看板

三个 API 接口

POST/predict单次预测 + 定价建议
POST/station/{id}/forecast24小时逐时预报序列
GET/hotspots涨价/降价 Top-5 站点
FastAPI = 数据格式声明 + 自动校验 + Swagger 交互文档
取代原项目的 Flask + Plotly,全部重写为课程要求的 FastAPI + PyEcharts。答辩现场浏览器打开 localhost:8000/docs 即可交互式演示 API 调用。

PyEcharts 看板 (6图)

  • 定价区域分布饼图
  • 24h需求模式折线图
  • 模型特征重要性柱状图
  • 借车vs还车散点图
  • 建议价格分布直方图
  • Top-10 涨价站点排名
借鉴: Shakleen Flask 路由 + Plotly 图表 自研: Flask→FastAPI · Plotly→PyEcharts · 6图看板
10 / 16

阶段七:测试 & CI/CD & 交付

23个 pytest 测试

定价引擎 6场景 · 空间匹配 2场景 · 供需缺口 3场景 · IO 3场景 · 距离计算

23/23 全部通过 · 0.9秒

代码规范

flake8:0 错误 · PEP8标准 · 39源文件 + 4测试文件

CI/CD = git push 自动触发质检
GitHub Actions监听每一次代码推送 → 自动跑 flake8 + pytest → 全绿才算合格。工业界标准交付流程。

自动化流水线

git push → flake8 → pytest → ✅

$ flake8 src/ tests/ --count
0

$ pytest tests/ -v
test_pricing_engine.py::test_bike_shortage_surge PASSED
test_pricing_engine.py::test_balanced_normal PASSED
test_spatial_matcher.py::test_match_exact PASSED
...
23 passed in 0.90s

答辩当天怎么演示给老师看:

1. 打开终端,进入项目目录:cd <项目目录>
2. 运行 flake8:..\airline-satisfaction-analysis\venv\Scripts\python.exe -m flake8 src/ tests/ --count → 输出 0
3. 运行 pytest:..\airline-satisfaction-analysis\venv\Scripts\python.exe -m pytest tests/ -v → 23 条全部 PASSED

借鉴: Shakleen PyTest 测试结构 自研: 23个测试 · flake8配置 · CI流水线
11 / 16

项目交付清单

文档

Word 课程设计报告(含6张图表 + 7个截图位)
HTML 答辩演示(15页 · Signal风格)
Topic 9 制作指导 Markdown(供后续选题使用)

代码

39个 Python 源文件
4个测试文件(23个测试用例)
CI/CD GitHub Actions 配置
PEP8 标准 · flake8 0 错误

数据 + 模型

Bronze Parquet 1.3 GB(满足 ≥1GB)
Silver 73MB · Gold 188MB
4个 RF/GBT 训练好的模型
PCA 降维模型 + gold_pipeline

服务

FastAPI 在线推理服务(3个接口)
PyEcharts 数据看板大屏(6图表)
Swagger UI 自动文档

答辩材料完整 · 可现场演示 · 可复现环境
12 / 16

关键数据一览

数据处理

1,450万
原始骑行记录
438万
站点-小时汇总
1.3 GB
Bronze Parquet

特征工程

19
最终特征
99.3%
空间匹配率
5
PCA主成分

模型性能

0.644
R² (借车需求)
3.04
RMSE
0.372
Top特征重要度

工程质量

23/23
测试通过
0
flake8错误
39
源文件
13 / 16

七阶段回顾:每个阶段做了什么

1 环境+数据 — venv · JDK 17 · PySpark · 下载 CitiBike 5个月 CSV + GBFS + 天气

2 数据管道 — Medallion 四层 Parquet(Raw→Bronze→Silver→Gold)。1450万 → 438万条

3 特征工程 — 空间匹配(99.3%) + 业务特征(3个) + 天气(3个) = 19特征。PCA降维

4 模型训练 — Random Forest · R² 0.14→0.64 · Hyperopt 贝叶斯优化 8轮

5 定价策略 ⭐ — 2×2供需矩阵 · 四场景定价 · 预测+规则解耦 · 438万条全量分析

6 API+看板 — FastAPI 三个接口 · PyEcharts 六个图表 · Swagger UI

7 测试+交付 — 23 tests · flake8 0 · CI/CD · Word报告 · HTML答辩

14 / 16

项目收获

技术能力

  • PySpark 分布式计算 + Medallion 架构
  • MLOps 全链路:数据→模型→API→CI/CD
  • Hyperopt 贝叶斯优化替代网格搜索
  • 空间数据分析 + 可解释定价设计

商业思维

  • 数据→预测→决策 转化能力
  • "预测+规则"解耦架构的泛化应用
  • 数据产品化的完整意识

职业方向

  • 数据分析师 / 商业分析师
  • 数据产品经理 / AI产品经理
  • 大数据应用与数据运营

核心认知:商业分析师的价值 = 数据 → 决策建议 → 可用的产品

15 / 16

感谢聆听

演示地址:http://localhost:8000/docs
看板大屏:http://localhost:8000/dashboard
项目代码:bike-pricing-analysis/

39
源文件
23
测试用例
0.644
1.3GB
数据量

大数据管理与应用 · 2026年6月 · 感谢老师的指导与聆听

16 / 16
1 / 15