简介:面向希望入门大数据与NoSQL工程实践的人群,这份课程设计资源围绕交通拥堵预测场景,演示了从数据生产、预处理、非关系型存储到模型训练与预测的完整链路。项目采用Kafka模拟实时交通监测数据,经消费清洗后存入非关系型数据库,再从Redis读取特征完成建模,并将模型持久化至HDFS供预测阶段调用。资源共包含31个文件,以Scala源码、Maven的pom.xml配置、IDEA工程文件(iml)及properties配置文件为主,压缩包总大小仅60KB,便于直接导入IDEA查看与修改。项目内按数据生产者、消费者、建模、预测四个模块划分,并附有环境版本清单(Hadoop 2.7.2、Spark 3.0.5、Kafka 0.8.2.1、Redis 2.11.8等),可帮助复现实验环境。目前已有177人学习浏览,适合作为大数据课程设计、毕业设计或初期工程实训的参考蓝本,能快速掌握Kafka+NoSQL+Spark协同完成实时预测任务的基本思路与代码组织方式。
1. 交通拥堵预测的课程设计:为什么非关系型数据库是天然主场
“大数据非关系型数据库课程设计-交通拥堵预测”这个题目,几乎是给 MongoDB 和 Redis 量身定做的。课程设计最容易翻车的不是模型,而是数据根本喂不动:几百万条 GPS 轨迹点用关系型数据库存,建索引、按时间窗聚合、再喂给预测模型,每一步都在等查询。交通拥堵预测的核心数据是带时间戳的路况记录,高频写入、按路段和时间窗口聚合、实时读取,这些恰好是非关系型数据库的主场。这篇笔记按课程设计的完整链路走一遍:存储选型、数据建模、聚合出特征、训练预测模型,再到答辩验证。适合正在做课程设计或毕业设计、想用 MongoDB 落地一个能讲清楚的大数据应用的同学。如果你只是想了解概念,这篇可能太细;如果你要动手做出来,照着改就能跑。
2. 选型与数据落地:用 MongoDB 存轨迹、用 Redis 存实时路况
2.1 数据特征决定存储选型:时序、空间与键值的分工
交通拥堵预测的数据来源通常有三种:车载 GPS 轨迹点(车在哪、速度多少)、卡口过车记录(一辆车在某个路口被拍到)、路况上报事件(事故或施工)。它们有一个共同特点——每条数据都带时间戳和空间位置,写入频率高且只追加不修改。这个特征决定了关系型数据库在这里很别扭:要按时间范围频繁扫描,表数据量大了之后分表分库是脏活累活;而 MongoDB 的文档模型天然适合一条轨迹点存一个文档,字段可以随意扩展,不用提前设计表结构。Redis 则负责另一件事——实时路况查询。答辩时你要展示“当前拥堵指数”,如果每次都去 MongoDB 里算聚合,少说几百毫秒,多则几秒,体验很差。常见做法是 MongoDB 存原始明细和特征结果,Redis 存最近几个时间窗口的拥堵指数,一个负责持久化,一个负责热数据访问。
选型对比用一张表就足够支撑报告里的“为什么用非关系型数据库”小节:
| 存储 | 适合放什么 | 理由 | 课程设计中的角色 |
|---|---|---|---|
| MongoDB | 原始轨迹点、聚合特征、预测结果 | 文档模型灵活,复合索引 + TTL 能处理时序写入 | 数据主库 |
| Redis | 最近 N 个窗口的拥堵指数 | setex 带过期时间,读延迟亚毫秒 | 实时缓存 |
| MySQL | 几乎不适合本场景 | 千万级时序写入要分表,按时间聚合慢 | 不建议用 |
课程设计不一定需要上集群,单机 MongoDB 配合索引就能撑住百万到千万级数据。大数据集群部署策略是另一个话题,在这个题目下,开三个节点做分片反而给自己增加网络和运维负担,除非老师明确要求。我一般建议先把单机跑通,再把“为什么单机够用、什么量级需要分片”写进报告,答辩时反而是加分项。
2.2 在 MongoDB 里设计拥堵事件集合:索引与 TTL 才是核心
集合设计不需要太花哨,一个 traffic_records 就够了。关键在字段类型和索引。timestamp 必须存成 Date 类型而不是字符串,否则后面所有按时间聚合和 TTL 清理都做不了;speed 存数字;road_id 建议带上路段前缀(比如 R01),方便演示时按路名过滤。状态字段 status 可以预留,用来标记这条记录是正常行驶还是拥堵上报。
索引是课程设计里最容易被忽略、影响最大的一步。查询模式决定索引:我们绝大多数查询是“某个路段在某个时间范围内”的记录,所以复合索引 {road_id: 1, timestamp: -1} 要建,字段顺序是等值在前、范围在后。另外建一个 timestamp 单字段的 TTL 索引,让数据自动过期,避免存储无限增长。
from pymongo import MongoClient client = MongoClient("mongodb://localhost:27017/") db = client["traffic_db"] col = db["traffic_records"] # 复合索引:road_id 等值过滤 + timestamp 范围排序 col.create_index([("road_id", 1), ("timestamp", -1)], name="road_time_idx") # TTL 索引:保留 7 天,到期自动清理 col.create_index({"timestamp": 1}, expireAfterSeconds=7 * 24 * 3600)复合索引的参数里,1 和 -1 只影响同一字段内排序方向,对“等值 + 范围”的查询来说,范围字段选 -1 是因为我们通常取最近数据,这样 MongoDB 按索引顺序扫描时能更快命中头部。TTL 索引的 expireAfterSeconds 单位是秒,7 天即 604800 秒。有一点必须注意:TTL 不是精确到秒删除的,后台线程大约每分钟扫一次,而且只删超过“当前时间减去 expireAfterSeconds”的文档。你要测 TTL 效果,插入一条时间戳为三天前的数据,然后等一分钟左右再查。
注意:如果 CSV 里的时间字段是字符串,导入后类型不对,TTL 索引永远不会生效。先插入一条记录,用 isinstance 或者直接在 Mongo Shell 里 db.traffic_records.findOne() 确认 timestamp 是 ISODate 而不是字符串,再建索引。
2.3 从 CSV 批量导入到实时写入:数据接入的脚本骨架
课程设计一般拿不到真实实时流,常见的坑是只做批量导入,没有“实时”的观感。我的做法是分两步:先把 CSV 批量导入,再写一个模拟实时写入的循环。批量导入用 insert_many,一次攒 1000 条再写,比循环 insert_one 快一个数量级。
import csv from datetime import datetime from pymongo import MongoClient client = MongoClient("mongodb://localhost:27017/") col = client["traffic_db"]["traffic_records"] def import_csv(path: str, batch_size: int = 1000): batch = [] with open(path, "r", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: # CSV 里时间是字符串,必须转成 Date,否则 TTL 和聚合都会失效 row["timestamp"] = datetime.fromisoformat(row["timestamp"]) row["speed"] = float(row["speed"]) row["road_id"] = row["road_id"].strip() batch.append(row) if len(batch) >= batch_size: col.insert_many(batch, ordered=False) batch.clear() if batch: col.insert_many(batch, ordered=False) import_csv("traffic_records.csv")ordered=False 的含义是某条数据失败时跳过继续写,而不是整批回滚——批量导入时我们更关心吞吐而不是单条准确性,脏数据可以在聚合阶段被 $match 过滤掉。batch_size 太小网络往返多,太大占用内存高,1000 是一个不容易出错的默认值。模拟实时写入时,把 insert_one 换成攒批 insert_many,每 2 秒写一批,既能看到数据量在涨,又不会让 CPU 飙满:
import time, random from datetime import datetime def simulate_realtime(interval: float = 2.0, batch_size: int = 500): roads = [f"R{i:02d}" for i in range(1, 51)] while True: batch = [] for _ in range(batch_size): batch.append({ "road_id": random.choice(roads), "lon": 116.30 + random.random(), "lat": 39.90 + random.random(), "speed": round(random.uniform(5, 80), 1), "status": "normal", "timestamp": datetime.utcnow() }) col.insert_many(batch, ordered=False) time.sleep(interval) simulate_realtime(interval=2.0)这段代码里 road_id 从 50 个路段里随机选,速度在 5 到 80 之间均匀分布。注意这里是均匀分布,真实数据应该是早晚高峰偏低、平峰偏高,后面做预测时会因为分布太均匀而不好学,所以只用来演示写入,真正训练还是用 CSV 里的仿真数据。
这里有一个容易踩的空间数据治理问题:GPS 坐标可能超出城市范围、速度为负、时间乱序,批量导入前要顺手洗一遍。在导入函数里加一个过滤条件(比如 speed 必须大于 0、坐标在合理范围内),比导入后再清理省事得多。
3. 把原始数据变成预测特征:聚合管道与时间窗口
原始轨迹点是一条条的,预测模型没法直接用。模型需要的是“路段在某个时间窗口内平均车速多少、车流量多少”。这里就是非关系型数据库发挥价值的地方——直接在 MongoDB 里做时间窗口聚合,算完的特征再送到模型里。
3.1 MongoDB 聚合管道:按 5 分钟窗口统计路段平均车速
时间窗口怎么定?课程设计建议用 5 分钟。窗口太大(30 分钟)预测粒度太粗,答辩时展示不出短时预测的价值;窗口太小(1 分钟)数据噪声大,平均速度抖动严重。5 分钟是交通流量预测里最常用的粒度,也和很多公开数据集保持一致,报告里写“参考了已有研究的通用窗口”不会被质疑。
聚合管道的核心是 $group 阶段把时间戳归入窗口。窗口起始时间用 Unix 秒整除 300 得到:先把 timestamp 转成长整型的毫秒数,除以 1000 得到秒,减去“对 300 取余”的结果,就是窗口起点。
from datetime import datetime, timedelta from pymongo import MongoClient client = MongoClient("mongodb://localhost:27017/") db = client["traffic_db"] col = db["traffic_records"] feature_col = db["feature_5min"] end = datetime.utcnow().replace(second=0, microsecond=0) start = end - timedelta(hours=2) pipeline = [ # 第一步一定要过滤,否则聚合管道会扫描全表 {"$match": {"timestamp": {"$gte": start, "$lt": end}}}, {"$group": { "_id": { "road_id": "$road_id", "window_ts": { "$subtract": [ {"$toLong": "$timestamp"}, {"$mod": [{"$toLong": "$timestamp"}, 300]} ] } }, "avg_speed": {"$avg": "$speed"}, "min_speed": {"$min": "$speed"}, "max_speed": {"$max": "$speed"}, "volume": {"$sum": 1} }}, {"$project": { "road_id": "$_id.road_id", "window_ts": "$_id.window_ts", "avg_speed": 1, "min_speed": 1, "max_speed": 1, "volume": 1, "_id": 0 }}, {"$sort": {"window_ts": 1}} ] results = list(col.aggregate(pipeline, allowDiskUse=True))这里要解释几个参数:$toLong 在 MongoDB 4.0 之后可用,把 Date 转成 Unix 毫秒;$mod 对 300 取余得到偏移量,相减得到窗口起始的毫秒时间戳。allowDiskUse=True 是因为聚合如果超过 100MB 内存会报错,加上它可以借用磁盘,对课程设计这种动辄上百万条的集合很有必要。$match 必须在管道最前面,这也是血泪经验——放在后面意味着先处理全表数据再过滤,性能差几十倍。跑完之后把结果写进 feature_5min 集合,作为下一步特征拼接的基础:
feature_col.drop() feature_col.insert_many(results)3.2 与 Redis 配合:实时拥堵指数如何缓存与过期
MongoDB 聚合适合离线算特征,但“当前这个路段堵不堵”这个查询如果每次都跑聚合,延迟不可控。我的做法是:启动一个后台任务,每分钟用上面那段聚合管道算一次最近 5 分钟窗口的数据,把结果写进 Redis,key 设计为 “road:路段ID:窗口起始时间戳”,value 用 JSON 放 avg_speed 和 volume,TTL 设置为 10 分钟。这样前端查实时路况只打 Redis,不碰 MongoDB,演示时可以明显感觉到响应速度差异。
import redis import json r = redis.Redis(host="127.0.0.1", port=6379, db=0, decode_responses=True) def write_road_cache(road_id: str, window_ts: int, data: dict, ttl: int = 600): key = f"road:{road_id}:{window_ts}" r.setex(key, ttl, json.dumps(data, ensure_ascii=False)) return key def get_road_cache(road_id: str, window_ts: int): key = f"road:{road_id}:{window_ts}" raw = r.get(key) return json.loads(raw) if raw else NoneTTL 为什么设 600 秒也就是 10 分钟?因为窗口本身是 5 分钟,10 分钟意味着允许读到一个过期窗口的数据,同时保证不会堆积到内存爆炸。前端拿当前时间整数倍窗口去查,如果 Redis 没命中,再回退查 MongoDB,这叫 cache-aside 模式,报告里写一句就够加分。decode_responses=True 这个参数很多人会忘,它让写入的字符串在读取时直接是 str 而不是 bytes,否则 json.loads 前还要 decode 一次,纯属给自己添麻烦。
再补充一个场景:你要展示“实时趋势”,可以定时把每个窗口的指数画成折线图,Redis 里保留最近 12 个窗口(即最近 1 小时),每次更新只覆盖旧 key。窗口过期后,Redis 自动清理,比手动删 key 可靠。这段逻辑就构成了整个系统里“实时”的部分。注意不要把原始轨迹也塞进 Redis——课程设计数据量大,Redis 是内存数据库,存原始明细很快就把内存吃光,它的定位是热数据索引,不是主存储。
4. 避坑:课程设计中踩过的 5 个真实翻车点
这一章攒的都是我自己和学生做这个题目时真正翻过车的点。每一条都按“现象 → 原因 → 解决”写,排查时可以直接对号入座。
4.1 数据量到 500 万条后,查询从 50ms 变成 5s
现象:一开始对着几万条数据测试,查询又快又顺;演示前灌了完整数据集,同样的 find 查询从 50ms 暴涨到 5 秒,页面直接转圈。
原因:建了索引,但查询条件是“路段 + 时间范围”,只建了 road_id 单字段索引,时间过滤部分还是全表扫;更常见的是压根没建索引,MongoDB 全表扫描。课程设计的数据量级到百万条后,全表扫描的时间就会变得肉眼可见。
解决:按复合索引 {road_id:1, timestamp:-1} 重建,然后用 explain() 验证——重点看 docsExamined 是否接近返回条数,如果 docsExamined 接近全表文档数,说明索引没生效。
db.traffic_records.find({ road_id: "R01", timestamp: { $gte: ISODate("2024-05-01T00:00:00Z"), $lt: ISODate("2024-05-01T01:00:00Z") } }).explain("executionStats")看输出里的 docsExamined 和 nReturned。如果二者相等,索引命中;如果前者是后者几十倍,重建索引。注意一个反直觉点:对 timestamp 字段做函数运算(比如 $toLong 后再比较)会让索引失效,过滤条件里要保持原始字段比较。
4.2 TTL 索引建了,但磁盘占用还在涨
现象:mongod 的磁盘占用曲线一路上涨,明明建了 TTL 索引,数据就是不被清理。
原因:索引建在了一个“看起来像时间”但其实是字符串的字段上;或者建在了时间字段上但文档里存的是字符串。MongoDB 的 TTL 线程只认 Date 类型,字符串时间它不会碰。
解决:插入前强制 datetime.fromisoformat(),在 create_index 之前先检查一两条文档确认类型。用 db.traffic_records.getIndexes() 查看 TTL 索引的 expireAfterSeconds 是否正确,也可以用 collMod 命令修改:
db.runCommand({ collMod: "traffic_records", index: { keyPattern: { timestamp: 1 }, expireAfterSeconds: 604800 } })另外 TTL 线程约 60 秒跑一次,插入几天前的数据后等一分钟再查,不要以为索引坏了就反复重建。
4.3 聚合管道第一次跑要 40 秒,答辩现场直接卡住
现象:在测试环境跑聚合管道只要 1 秒,原因是数据只有几十万条;答辩前导入了完整数据,同一个聚合管道跑了 40 秒没出结果,现场只能干等。
原因:$match 没放在管道第一个 stage,导致 $group 先对全表分组。这是聚合管道最容易忽略的性能规则。聚合是全内存操作,超过 100MB 内存限制会直接报错。
解决:管道第一个 stage 永远写 $match,把时间和路段过滤放进去。如果过滤字段有索引,MongoDB 会自动用上。再加 allowDiskUse=True 兜底,防止内存超限。注意 $sort 在 $group 之后只对分组结果排序,成本低;但如果把 $sort 放在 $match 之前,就是对全表排序,极其耗内存。
4.4 预测准确率 0.95,老师一问怎么来的就心虚
现象:随机森林训练完,准确率高达 0.95,我自己都怀疑哪里出了问题。老师追问“你的特征窗口和预测窗口怎么对齐的”,答不上来。
原因:数据发生了泄露——构造特征时把同一时间窗口的 avg_speed 同时用作了特征和标签,模型相当于直接抄答案;或者随机打乱了时间序列再划分训练测试集,让模型见过“未来”的数据。这种看似高性能在答辩时最致命。
解决:特征时间窗口必须严格早于预测目标窗口。比如用 t-1、t-2、t-3 窗口的速度和流量预测 t 窗口是否拥堵,标签取 t 窗口的 avg_speed 是否低于 20 km/h。划分数据用 TimeSeriesSplit 或按日期切分,绝不使用 train_test_split 的默认随机模式。这一点在答辩前要自己主动核对一遍,不然就是当场翻车。
4.5 模拟实时写入程序越跑越慢,最后内存爆了
现象:模拟实时数据写入的程序跑了几分钟,Python 进程内存占用持续走高,最后 OOM 被杀,MongoDB 里数据量却只有几万条。
原因:用 insert_one 循环写,每条记录一次网络往返,写入速度远低于生成速度,积压的数据排队在内存里,越堆越多。
解决:攒批 insert_many,每批 500~1000 条;或者用 bulk_write 做批量写。控制模拟频率,interval 设 2 秒一批足够演示,不要用 0.1 秒去仿真,那不是实时,是给自己制造压力。
from pymongo import InsertOne requests = [InsertOne(doc) for doc in batch] col.bulk_write(requests, ordered=False)批量写入时 ordered=False 可以让单条脏数据不影响整批,排查脏数据时再单独扫 status 字段标记的记录。
5. 拥堵预测模型:从 MongoDB 取特征到决策树训练与回测
5.1 特征表怎么来:把聚合结果导出成 model_input 集合
聚合管道算出的 feature_5min 只有当前窗口的 avg_speed 和 volume,预测需要的是“前几个窗口”的序列特征。在 Python 侧按 road_id 分组做滞后特征,这是时间序列预测的标准操作。
特征工程的具体做法:对每个 (road_id, window_ts),构造 speed_lag1(上一个窗口平均速度)、speed_lag3(前三个窗口平均速度)、speed_std(近三个窗口速度标准差)、volume(当前窗口车流量)、is_peak(是否早晚高峰)。标签是下一窗口的平均速度是否低于阈值。窗口排序必须按 (road_id, window_ts),否则 shift 会跨路段错位。
import pandas as pd from pymongo import MongoClient client = MongoClient("mongodb://localhost:27017/") db = client["traffic_db"] df = pd.DataFrame(list(db["feature_5min"].find())) # 排序是特征构造的生命线,先路段后时间 df = df.sort_values(["road_id", "window_ts"]).reset_index(drop=True) # 按路段分组做滞后特征,shift 才能对齐同一路段的历史窗口 g = df.groupby("road_id", group_keys=False) df["speed_lag1"] = g["avg_speed"].shift(1) df["speed_lag3"] = g["avg_speed"].shift(3) df["speed_std"] = g["avg_speed"].transform(lambda x: x.rolling(3).std()) # 下一窗口平均速度,作为预测目标 df["avg_speed_next"] = g["avg_speed"].shift(-1) # 高峰期:早 7-9 点、晚 17-19 点 hours = pd.to_datetime(df["window_ts"], unit="ms").dt.hour df["is_peak"] = ((hours >= 7) & (hours <= 9) | (hours >= 17) & (hours <= 19)).astype(int) # 丢弃没有历史或没有未来的边界行 df = df.dropna(subset=["speed_lag1", "speed_lag3", "speed_std", "avg_speed_next"]) db["model_input"].drop() db["model_input"].insert_many(df.to_dict("records"))这段代码里 group_keys=False 很关键:pandas 默认会给分组后的分组列加回列,导致 shift 结果里混入多余列,整体错位。rolling(3).std() 的窗口是当前行加前两行,正好对应“近三个窗口”,不含未来。lag 和 shift(-1) 的组合保证了特征时间严格早于标签时间。
5.2 用 Python 训练 15 分钟拥堵预测模型:最小可运行代码
模型选型上用随机森林而不是深度学习,原因有三:课程设计的样本量通常只有几万条,深度学习容易过拟合还难解释;随机森林的 feature_importances_ 能直接画图,答辩时说明“车速滞后特征最重要”很有说服力;调参只有 n_estimators 和 max_depth 两个关键旋钮,容错率高。
标签定义:avg_speed_next < 20 km/h 记为拥堵。这个阈值可以按数据分布调整,但报告里要说明依据——20 km/h 约等于城市地面道路的严重拥堵临界速度,写进去不会被质疑。
import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import classification_report client = MongoClient("mongodb://localhost:27017/") db = client["traffic_db"] df = pd.DataFrame(list(db["model_input"].find())) df["label"] = (df["avg_speed_next"] < 20).astype(int) features = ["speed_lag1", "speed_lag3", "speed_std", "volume", "is_peak"] X = df[features] y = df["label"] # 时间序列必须用 TimeSeriesSplit,不能用随机打乱 tscv = TimeSeriesSplit(n_splits=5) for train_idx, test_idx in tscv.split(X): X_train, X_test = X.iloc[train_idx], X.iloc[test_idx] y_train, y_test = y.iloc[train_idx], y.iloc[test_idx] clf = RandomForestClassifier( n_estimators=100, max_depth=6, class_weight="balanced", random_state=42 ) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) print(classification_report(y_test, y_pred, target_names=["畅通", "拥堵"], digits=3))TimeSeriesSplit 是 sklearn 里专为时间序列设计的交叉验证器,它只按顺序切分,训练集永远在测试集之前,不会出现“用未来预测过去”的数据泄露。class_weight="balanced" 是处理拥堵样本占比低的关键参数——如果数据里只有 10% 的窗口是拥堵,不设这个参数模型为了整体准确率会把拥堵全部判成畅通。
max_depth=6 的限制让每棵树只长 6 层,课程设计要的是泛化能力不是拟合能力;n_estimators=100 是性能和安全性的平衡点,再大收益很小,训练时间翻倍。
5.3 回测与阈值调整:预测“堵/不堵”和预测“拥堵指数”是两件事
有些同学把任务做成回归,预测下一窗口的具体速度值。我不是很推荐:回归的评估指标(MSE、MAE)对非专业评委来说很难解读,你展示“均方误差 3.2”老师不知道是好是坏。二分类的“准确率、召回率、误报率”三张表就直观得多。
分类的任务里,默认阈值 0.5 可能不是最优的。当拥堵样本占比只有 10% 时,0.5 的阈值容易把少数类全部压掉。回测时用 predict_proba 拿到概率分数,然后扫阈值:
proba = clf.predict_proba(X_test)[:, 1] y_pred = (proba >= 0.3).astype(int) # 调低阈值,提高拥堵召回率 print(classification_report(y_test, y_pred, target_names=["畅通", "拥堵"], digits=3))阈值从 0.5 降到 0.3,会牺牲一部分“畅通判拥堵”的准确率,换来“真拥堵被抓住”的比例。前者是误报,后者是漏报。交通拥堵场景里漏报的代价更高——没有提前预警,拥堵已经发生了才通知用户,这套系统就失去了价值。答辩时主动说出“我做了阈值扫描,选择 0.3 是为了优先保障召回率”,是一个很加分的成熟回答。
回测还建议做滚动窗口:训练集用第 1-7 天,测试第 8 天;下一次用第 1-8 天训练,测第 9 天。这样可以模拟模型上线后每天遇到的情况,比一次性切分更能暴露稳定性问题。实现上用 TimeSeriesSplit 的 n_splits 参数控制滚动次数即可。
6. 答辩与验证:用三个指标证明你的预测真的有用
答辩时不要对着模型结构讲,老师更关心的是“你的预测凭什么可信”。我建议只摆三个指标:整体准确率、拥堵类召回率、误报率。准确率说明模型整体水平,召回率说明预警能力,误报率说明系统会不会惹人烦。三者关系用一张对比预测和实际的时间轴图最直观——横轴是时间窗口,纵轴是路段,把预测拥堵和实际拥堵的窗口分别标色,重合的部分就是模型真正值钱的地方。
数据可视化这一步别省。用 matplotlib 把连续三天每个 5 分钟窗口的 avg_speed 画成折线,再叠上预测的拥堵区间阴影,早晚高峰的形态一眼就能看出模型学到的规律。这也是老师问题最多的点:“为什么这个窗口预测堵但实际没堵?”你可以回到特征表里查 speed_lag1 和 volume,大概率是前一个窗口的速度已经很低、车流量很大,属于“合理误报”。能即时解释到这个颗粒度,比背十页报告都有说服力。
最后是一个我自己的教训。当年我做完这套系统,测试准确率 0.93,自信满满去答辩。老师只看了一眼就问:“你的特征窗口用的是 t-1 到 t-3,标签是 t,但数据是同一文件里抽出来的,你保证训练集第一天之前没有 t 窗口的数据吗?”我当场愣住。后来改成按天切分,把测试集严格设为最后两天,准确率降到 0.87,但这次我敢说每个数字都经得起追问。从那以后我做任何时序模型的习惯都是:先画一条时间轴,标清楚特征区间、标签区间和切分点,再开始训练。
做这个题目的价值不在于模型多先进,而在于你能把一条数据从打点、入库、聚合到变成预测结果的完整链路讲清楚。老师的追问往往就是大数据面试题里最经典的几个场景题——索引为什么失效、聚合为什么慢、时序为什么不能随机切分。希望这份笔记能帮你少走一点弯路,把这些坑提前填上。
本文还有配套的精品资源,点击获取