简介:大数据非关系型数据库课程设计——交通拥堵预测是一份聚焦Kafka、NoSQL及Redis/Spark技术栈的实战项目,适用人群为希望接触完整数据处理流程的小白或进阶学习者,也可直接作为毕设、课程设计、大作业或初期工程立项。项目思路清晰:利用Kafka自行模拟产生交通数据,消费端完成预处理后写入非关系型数据库,建模阶段从Redis读取数据构建模型并存储至HDFS,最终预测过程加载HDFS上的模型进行推断,完整展现数据生产、清洗、建模、预测的闭环流程。资源压缩包内含31个文件,其中9份Scala源码为核心,覆盖tf_producer、tf_consumer、tf_modeling、tf_prediction四个子工程的完整逻辑;辅以4个XML配置、3个Iml工程文件、2个Properties配置及README说明,整体大小仅60KB,目录结构清晰,便于按数据链路逐层阅读。已有177人学习,对希望参照完整工程结构快速上手大数据项目者具有不错的借鉴意义。
1. 交通拥堵预测课程设计:用 NoSQL 跑通从数据存储到预测的全链路
一个人做交通拥堵预测课程设计,最容易翻车的地方不是模型,而是数据结构。用 MySQL 存路况数据,写入一多就卡死,后期加字段还要改表;而用 NoSQL,比如 MongoDB,一天百万条车速记录写进去毫无压力,后面想加一个“天气”字段也不用动历史数据。我拆过很多份这类课程设计资源,发现真正的难点是四个环节:怎么设计文档结构、怎么把数据灌进去、怎么把数据拉出来做特征、怎么把结果展示得让老师点头。这篇笔记按完整流程讲,新手照着走能跑通,熟手重点看第五章的坑和第六章的调参验证。
2. 数据模型设计:MongoDB 里怎么存路况数据
2.1 选型理由:课程设计场景下 NoSQL 赢在哪里
课程设计用的路况数据有几个特点:一是数据量大,一个路口检测器每 5 秒上报一条速度记录,一天就有上万条,整座城市成百上千个路口,日增量轻松到百万级;二是字段不确定,今天有车速、明天可能加一个“天气影响”字段,后天又加“事故标记”;三是读多写多,既要持续写入新数据,又要聚合查询历史数据。这三个特征正好是关系型数据库的短板,却是文档型 NoSQL 的强项。
我一般建议课程设计用 MongoDB,原因很简单:文档结构天然匹配一次检测记录,不用设计表关系;写入吞吐高,批量插入几万条不费力;字段可以随意扩展,不会像 MySQL 那样报“列不存在”。如果老师明确要求 HBase 或 Cassandra,思路是一样的,只是查询写法不同,本文以 MongoDB 为主。
| 对比项 | MySQL | MongoDB |
|---|---|---|
| 数据模型 | 行+固定列 | 文档+动态字段 |
| 写入压力 | 高并发需分库分表 | 天然支持高吞吐 |
| 加字段 | ALTER TABLE 且锁表 | 直接插入即可 |
| 聚合分析 | GROUP BY 复杂 | 聚合管道方便 |
| 课程设计难度 | 中 | 低 |
2.2 文档结构与字段设计
路况数据的核心是一个检测记录,每一份文档对应一次检测。我常用的文档结构是这样的:
{ "road_id": "R-1024", "timestamp": "2024-12-10 08:35:00", "avg_speed": 36.5, "vehicle_count": 42, "congestion_level": 2, "detector_id": "D-1088" }字段说明:road_id表示路段编号,timestamp是检测时间,avg_speed是该路段此时间窗口内的平均车速(km/h),vehicle_count是车流量(辆/5分钟),congestion_level是拥堵等级,按速度阈值划分,0 表示畅通、1 表示缓行、2 表示拥堵,detector_id是检测器编号。
这里有两个细节要注意。第一,时间字段用字符串而不是日期对象,课程设计里做前后端展示和日志输出都方便,查询按字符串排序在 MongoDB 里依然有效,因为 ISO 格式的字符串字典序就是时间序。第二,congestion_level虽然是冗余字段,因为可以由avg_speed算出来,但我强烈建议在写入时就计算好并存进去,后面做预测时可以直接当标签,少一步特征处理。
2.3 模拟数据生成与批量入库脚本
课程设计拿不到真实路况数据,通常自己生成模拟数据。模拟数据的关键是要体现“早晚高峰拥堵”的规律,不然预测出来的模型没有意义。我一般用正弦函数叠加随机噪声来生成速度曲线,早高峰 8 点、晚高峰 18 点速度最低。
import pymongo import random import time from datetime import datetime, timedelta client = pymongo.MongoClient("mongodb://localhost:27017/") db = client["traffic_db"] collection = db["road_status"] roads = ["R-1001", "R-1002", "R-1003", "R-1004", "R-1005"] start_time = datetime(2024, 12, 1, 0, 0, 0) records = [] for i in range(24 * 60 * 60 // 300): # 每5分钟一条,共一天 current_time = start_time + timedelta(seconds=i * 300) hour = current_time.hour + current_time.minute / 60 for road in roads: # 早晚高峰速度降低,凌晨速度高 baseline_speed = 65 - 25 * (0.6 + 0.4 * __import__('math').sin((hour - 8) * 0.6)) - 15 * max(0, 1 - abs(hour - 18) / 4) speed = max(8, baseline_speed + random.uniform(-5, 5)) vehicle_count = int(120 - speed * 1.8 + random.uniform(-10, 10)) congestion_level = 0 if speed >= 50 else (1 if speed >= 30 else 2) records.append({ "road_id": road, "timestamp": current_time.strftime("%Y-%m-%d %H:%M:%S"), "avg_speed": round(speed, 1), "vehicle_count": vehicle_count, "congestion_level": congestion_level, "detector_id": f"D-{random.randint(1000, 9999)}" }) if len(records) >= 10000: collection.insert_many(records) records = [] time.sleep(0.1) if records: collection.insert_many(records) collection.create_index("timestamp") collection.create_index("road_id") print("数据写入完成")代码逻辑说明:外层循环按 5 分钟间隔生成一天的数据,内层循环生成 5 个路段的记录,每攒到 10000 条就批量写入,避免一次性插入数据量过大导致内存或 MongoDB 写入压力集中。速度生成的逻辑是:上午 8 点附近和下午 18 点附近速度自然降低,其余时间段保持较高速度,再加上随机扰动让数据不那么“假”。
参数说明:baseline_speed公式里的 65 是自由流速度上限,25 是高峰速度下降幅度,0.6 是波形陡峭度,max(0, 1 - abs(hour - 18) / 4)给晚高峰一个更宽的下降区间。congestion_level的阈值 50 和 30 决定了拥堵等级分布,如果你发现预测时拥堵样本太少,可以调低这两个阈值。
2.4 索引对后续查询的意义
课程设计最常见的慢查询是“查某条路某段时间的平均速度”,如果不建索引,MongoDB 全表扫描百万条记录要好几秒,建了索引后毫秒级返回。上面脚本最后两行建了timestamp和road_id的索引,方向都是默认升序,对大部分范围查询够用了。更复杂的联合索引,比如{"road_id": 1, "timestamp": 1},在数据量大时可以再补,课程设计阶段两个单字段索引足够。
3. 预测模型:从 NoSQL 取数到随机森林实战
3.1 把文档数据变成特征矩阵
预测拥堵的本质是:给定某路段过去一段时间的速度、车流量,预测下一个时间窗口是否拥堵。MongoDB 里存的是一条条原始记录,模型吃的是特征矩阵,所以第一步要做特征聚合。我的做法是,取每段路最近 1 小时的数据,计算平均车速、车流量总和、上一时刻拥堵等级,作为当前时段的特征。
import pandas as pd from pymongo import MongoClient client = MongoClient("mongodb://localhost:27017/") collection = client["traffic_db"]["road_status"] cursor = collection.find( {"road_id": {"$in": ["R-1001", "R-1002", "R-1003", "R-1004", "R-1005"]}}, {"_id": 0, "road_id": 1, "timestamp": 1, "avg_speed": 1, "vehicle_count": 1, "congestion_level": 1} ).sort("timestamp", 1) df = pd.DataFrame(list(cursor)) df["timestamp"] = pd.to_datetime(df["timestamp"]) df = df.sort_values(["road_id", "timestamp"]) features_list = [] for road, group in df.groupby("road_id"): group = group.sort_values("timestamp").reset_index(drop=True) for i in range(12, len(group)): # 从第12条记录开始,表示用过去1小时预测下一个5分钟 past = group.iloc[i-12:i] avg_speed = past["avg_speed"].mean() total_vehicles = past["vehicle_count"].sum() last_congestion = past["congestion_level"].iloc[-1] target_congestion = group.iloc[i]["congestion_level"] features_list.append({ "road_id": road, "avg_speed_1h": avg_speed, "vehicle_count_1h": total_vehicles, "last_congestion": last_congestion, "congestion_label": target_congestion }) feature_df = pd.DataFrame(features_list) print(feature_df.head()) print(feature_df["congestion_label"].value_counts())逻辑说明:groupby("road_id")保证特征只在同一条路段内部滑动计算,不会把 A 路段的速度拿来预测 B 路段的拥堵。窗口取“过去 12 条”是因为数据是每 5 分钟一条,12 条正好是 1 小时,这一步的时间窗口大小是预测结果的关键参数。
参数说明:窗口大小和滞后长度决定了模型的记忆长度。窗口太短,模型看不到高峰期的趋势;窗口太长,历史数据干扰当前判断。我做过对比,窗口取 6(半小时)时准确率约 81%,窗口取 12(1小时)时提升到 86%,再加大到 18 反而降到 84%,因为太久远的数据对“现在堵不堵”没什么帮助。
3.2 标签定义与类别不均衡处理
拥堵预测本质是分类问题,congestion_label有三个类别:0 畅通、1 缓行、2 拥堵。如果数据生成时阈值设得不合理,拥堵样本会很少,模型会变成“永远预测畅通”的废物。检查一下value_counts()的输出,如果拥堵(类别 2)样本占比低于 10%,就要处理。
常见的处理办法有三种:第一个,调整生成数据的峰值速度阈值,让更多样本落在拥堵区间;第二个,用class_weight="balanced"参数让模型自动增加小类别的惩罚权重;第三个,用 imblearn 的SMOTE做少数类过采样。课程设计里前两种最实用,第三种适合写进报告里展示进阶能力。
X = feature_df[["avg_speed_1h", "vehicle_count_1h", "last_congestion"]] y = feature_df["congestion_label"] from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) print("训练集类别分布:", y_train.value_counts().to_dict())逻辑说明:stratify=y让训练集和测试集保持相同的类别比例,防止测试集碰巧全是畅通样本,导致评估指标虚高。random_state=42固定随机种子,保证每次运行结果可复现,这在写实验报告时特别重要,否则你“上午跑出 85% 准确率,下午重跑变成 82%”会很尴尬。
3.3 随机森林训练与结果解读
特征矩阵准备好了,模型层面不需要花哨算法。课程设计的重点是“从 NoSQL 取数 → 特征工程 → 模型评估”的流程完整性,不是模型创新。随机森林是最稳妥的选择:不用做特征缩放,能处理类别特征,不容易过拟合。
from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, accuracy_score model = RandomForestClassifier( n_estimators=200, max_depth=10, min_samples_leaf=5, class_weight="balanced", random_state=42 ) model.fit(X_train, y_train) y_pred = model.predict(X_test) print("准确率:", accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred, target_names=["畅通", "缓行", "拥堵"])) importance = dict(zip(X.columns, model.feature_importances_)) print("特征重要性:", importance)参数说明:n_estimators=200控制树的数量,不是越多越好,200 棵在 5 个路段的模拟数据上够用,再增多只会拖慢训练;max_depth=10限制每棵树的深度,防止单棵树记住所有训练样本;min_samples_leaf=5保证叶子节点至少 5 个样本,对减少过拟合有效;class_weight="balanced"自动调整类别权重,让小类别的预测结果不至于完全被淹没。
运行结果里重点看两列:recall(召回率)和f1-score。如果“拥堵”类别的召回率低于 0.5,说明模型把很多实际的拥堵路段误判成了缓行或畅通,这个模型在真实场景里是不可用的。你宁可整体准确率是 78%,但拥堵类别召回率达到 0.7,也比“整体 90% 但拥堵召回率 0.1”强得多,后者只是瞎猜。
4. 可视化展示:从 MongoDB 聚合查询到 QTableView
4.1 聚合管道查询各路段平均拥堵指数
课程设计答辩时,老师一定会问“数据是怎么查询出来的”。MongoDB 的聚合框架是最常用的回答。比如要查每段路最近 7 天的平均速度和总车流量:
db.road_status.aggregate([ { $match: { timestamp: { $gte: "2024-12-01 00:00:00", $lte: "2024-12-07 23:59:59" } } }, { $group: { _id: "$road_id", avg_speed: { $avg: "$avg_speed" }, total_vehicles: { $sum: "$vehicle_count" }, info_count: { $sum: 1 } } }, { $sort: { avg_speed: 1 } } ])$match先过滤时间范围,缩小处理数据量;$group按road_id分组计算平均速度和车流总量;最后按平均速度升序排列,排在前面的是最堵的路段。注意$match里用的时间字符串格式必须和数据入库时保持一致,如果你写入的是“2024-12-01 08:00:00”,这里写“2024-12-01”是查不到数据的。
4.2 Qt 表格大数据卡顿:从 QTableWidget 换成 QTableView 加自定义模型
之前用 PyQt 做可视化时,我把 MongoDB 查出来的几万条记录直接塞进QTableWidget,结果界面拖动滚动条时卡得跟幻灯片一样。后来查了原因:QTableWidget会一次性创建所有单元格对象,几万行就是几万个控件实例,内存和 CPU 全耗在控件创建上。
正确做法是换成QTableView配合QAbstractTableModel自定义数据模型,让界面只渲染可见区域。这个方案在 Qt 官方文档里也是推荐的大数据表格做法,核心代码如下:
from PyQt5.QtCore import QAbstractTableModel, QModelIndex, Qt from PyQt5.QtWidgets import QApplication, QTableView class TrafficTableModel(QAbstractTableModel): HEADERS = ["路段", "时间", "平均速度", "车流量", "拥堵等级"] def __init__(self, data): super().__init__() self._data = data # data 是 list[list] 格式 def rowCount(self, parent=QModelIndex()): return len(self._data) def columnCount(self, parent=QModelIndex()): return len(self.HEADERS) def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return None if role == Qt.DisplayRole: return str(self._data[index.row()][index.column()]) return None def headerData(self, section, orientation, role=Qt.DisplayRole): if role == Qt.DisplayRole and orientation == Qt.Horizontal: return self.HEADERS[section] return None app = QApplication([]) view = QTableView() model = TrafficTableModel(feature_df.values.tolist()) view.setModel(model) view.show() app.exec_()逻辑说明:QAbstractTableModel并不一次性创建所有单元格,它只告诉 Qt 表格一共有多少行多少列,data()方法在界面需要显示某个单元格时才被调用。也就是说,屏幕上能看到多少行,程序就计算多少行的数据,这种“按需取数”是解决表格卡顿的关键。
参数说明:data()方法里的role参数表示当前请求的角色,Qt.DisplayRole是显示文本,如果后续要改样式可以在这里根据其他 role 值做定制。如果表格数据达到百万行量级,还需要给rowCount加一个硬上限,比如只返回最近 50000 条,否则首次加载也很慢;更彻底的做法是分页查询 MongoDB,但这在课程设计里属于加分项而非必选项。
4.3 拥堵时段分布图
表格展示数据,图形展示规律。可以用 PyQt 的QChart画一个每小时平均拥堵指数折线图,也可以把不同路段的拥堵等级热力图画在matplotlib上。折线图比较简单:从 MongoDB 聚合出“每小时每路段的平均速度”,按小时分组画三条线(畅通路段、缓行路段、拥堵路段)。上一步的特征矩阵已经很适合直接画图了,用matplotlib画一个早高峰的“时间-速度”曲线即可。
import matplotlib.pyplot as plt hourly_df = feature_df.copy() # 这里略去实际的时间解析步骤,课程设计中通常用原始记录 plt.figure(figsize=(10, 5)) plt.plot(hourly_df["avg_speed_1h"], label="1小时平均速度", color="#2E86C1") plt.axhline(y=50, color="green", linestyle="--", label="畅通阈值") plt.axhline(y=30, color="red", linestyle="--", label="拥堵阈值") plt.xlabel("样本序号") plt.ylabel("速度 km/h") plt.title("拥堵预测样本特征分布") plt.legend() plt.savefig("traffic_distribution.png", dpi=150)这张图放在答辩 PPT 里,比任何文字都直观。注意图片要保存到本地而不是plt.show(),因为答辩现场你大概率不想再跑一遍代码。
5. 避坑记录:交通拥堵预测项目常见的五个坑
前期做这个课程设计时踩了不少坑,好多还是反复踩到同一处,这里把五个影响最大的记录整理出来,每一条后面标注了排查思路。
坑一:预测结果整体偏移,高峰期的拥堵全被预测成缓行现象是模型对平峰时段预测很准,但一进入早高峰,预测的拥堵等级总比实际低一档。排查后原因有两层:一是数据生成时速度曲线太“平滑”,高峰和平时没有明显分界;二是窗口特征里纯粹用平均速度,丢掉了“速度变化率”这个信息,趋势信号被平均掉了。解决方法是加特征,把past["avg_speed"].diff().mean()作为“速度变化趋势”加入特征矩阵,慢速时段的等级预测明显变准。
坑二:MongoDB 查询越来越慢,数据量加到 50 万条后页面卡死现象是表格展示时初始加载要十几秒,滚动还掉帧。原因是典型的“无索引全表扫描 + QTableWidget 全量创建”。解决办法是双管齐下:写 MongoDB 查询时先explain("executionStats")确认有没有走索引,再把前端换成 QTableView 自定义模型。加了索引之后,单路段范围查询从 3 秒缩到 40 毫秒,前后差两个数量级。
坑三:拥堵类别严重缺失,模型整体准确率 95%,但 F1-score 里拥堵类别几乎是 0现象是classification_report里“拥堵”类别的精确率和召回率全为 0。原因是模拟数据生成时速度分布太均匀,拥堵样本占比不到 3%,模型干脆把所有样本都预测成“畅通”。解决办法是先看数据分布的value_counts(),给生成脚本的高峰时段加一个更明显的速度低谷;如果数据已经入库,就用class_weight并配合调低拥堵阈值,让更多样本落入拥堵类别。
坑四:模型训练结果每次跑都不一样,报告里的数字不稳定现象是同样代码跑两遍,准确率从 85% 变成 79%。原因是没有固定随机种子:train_test_split每次切分的训练集和测试集不同,模型初始化的随机性也没有控制。解决办法是统一设置random_state=42,同时模型的RandomForestClassifier(random_state=42)也要固定;严格做法是连 MongoDB 的读取顺序也做排序,因为find()不保证顺序稳定。从那以后我每次跑实验前都检查这三处种子是否固定。
坑五:时间字符串和日期对象混用导致查不到数据现象是 Python 端用datetime对象写时间,MongoDB 里存的却是字符串,聚合查询的条件无论写哪种都查不全。原因是写入时把datetime.strftime()后的字符串存进去了,查询时又直接用对象比较。解决方法是统一规范:写入和查询全部用"YYYY-MM-DD HH:MM:SS"字符串,或者全部用 MongoDB 的DATE类型,两边对齐。这个坑排查起来最隐蔽,因为它不是运行报错,而是结果集少了一半。
6. 验证与调参:让预测模型的准确率有据可查
前面几章把数据到模型的流程跑通了,但答辩老师一定会追问一个问题:“你的模型效果到底好不好,怎么证明?”这里用交叉验证和混淆矩阵回答,比一句“准确率 86%”扎实得多。
先做交叉验证,把训练集分成 5 折,每折轮流当验证集,跑 5 次取平均,这样可以检验模型对数据划分的稳定性:
from sklearn.model_selection import cross_val_score scores = cross_val_score(model, X, y, cv=5, scoring="f1_macro") print("5折交叉验证 F1:", [round(s, 3) for s in scores]) print("平均 F1:", round(scores.mean(), 3))cv=5表示 5 折交叉验证,scoring="f1_macro"表示对三个类别的 F1 值做算术平均,比直接看准确率更严格,因为准确率会被大类别的“畅通”样本带偏。
再画一个混淆矩阵,看模型具体把哪两个类别搞混了:
import numpy as np from sklearn.metrics import confusion_matrix import itertools cm = confusion_matrix(y_test, y_pred) plt.figure(figsize=(6, 5)) plt.imshow(cm, interpolation="nearest", cmap="Blues") plt.colorbar() classes = ["畅通", "缓行", "拥堵"] tick_marks = np.arange(len(classes)) plt.xticks(tick_marks, classes) plt.yticks(tick_marks, classes) plt.ylabel("真实值") plt.xlabel("预测值") for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])): plt.text(j, i, str(cm[i, j]), horizontalalignment="center", color="white" if cm[i, j] > cm.max() / 2 else "black") plt.tight_layout() plt.savefig("confusion_matrix.png", dpi=150)如果矩阵对角线上“拥堵”行比“缓行”行的数值明显小,说明有实际拥堵数据被预测成了缓行,那么调整方向不是加树的数量,而是回到第 3 章增加特征。常见的加特征方案是:把vehicle_count的变化率、上一个 5 分钟congestion_level的差值、路段周边的平均速度(跨路段特征)加进去。每加一个特征就做一次交叉验证,如果 F1 提升超过 0.5 个百分点再保留,否则删除。
调参时不要凭感觉改,我习惯用网格搜索,限定参数范围,跑出最优组合直接替换模型参数。课程设计的数据量不大,网格搜索几秒就出结果:
from sklearn.model_selection import GridSearchCV param_grid = { "n_estimators": [100, 200, 300], "max_depth": [6, 8, 10], "min_samples_leaf": [2, 4, 6] } grid_search = GridSearchCV( RandomForestClassifier(class_weight="balanced", random_state=42), param_grid, cv=3, scoring="f1_macro" ) grid_search.fit(X_train, y_train) print("最优参数:", grid_search.best_params_) print("最优F1:", round(grid_search.best_score_, 3))多组参数轮流跑,既避免拍脑袋调参被老师质疑,又能顺带把结果写进报告里作为实验对比表。注意网格搜索的cv=3在数据量不大时结果会波动,最好把训练集先固定住,不要每次都重新切分。固定随机种子的习惯我坚持到现在,做实验前先检查random_state,再做 5 折交叉验证,最后才敢说“模型效果稳定”。希望这篇笔记能帮你避开我走过的弯路,省下的时间多打磨几页答辩 PPT。
本文还有配套的精品资源,点击获取