news 2026/10/3 3:23:27

基于Scala与MySQL的交通拥堵预测:数据库课设高分实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Scala与MySQL的交通拥堵预测:数据库课设高分实战解析

简介:这是一份基于Scala的交通拥堵预测源码,源自大三学期的高分数据库课程设计,适合计算机科学与技术、大数据、人工智能等专业学生用作课程设计、期末大作业或项目实战演练。压缩包共50个文件,以18个Scala源文件为核心,辅以8个XML配置、6个Markdown文档、6个IntelliJ模块描述文件、4个properties配置及2个HTML页面等,整体仅73KB;目录按tf_consumer、tf_producer、tf_prediction、tf_modeling等模块清晰划分,方便顺着业务链路阅读理解。这份资源已有106人学习浏览,代码完整且功能已验证可稳定运行。项目中不仅包含完整的消费、生产、预测与建模实现,还在docs目录整理了秋招笔试题笔记,既能帮助理解Scala与数据库结合的应用场景,也可在将项目路径改为英文名后直接运行,适合在此基础之上进行二次开发与功能扩展。

1. 为什么一份"交通拥堵预测源码"值不值的看,先看它怎么组织数据

拿到“基于Scala的交通拥堵预测源码(高分数据库课程设计).zip”这份资源,第一反应别急着解压跑起来。数据库课程设计的高分,从来不是算法多炫,而是你能不能讲清楚“数据从哪来、怎么存、怎么被算、算完怎么回去”。这份课设的亮点在命名里已经暴露了:Scala是计算层的主角,交通拥堵预测是业务场景,而数据库课设才是它真正的评分框架。说白了,这是一个用Scala做实时或批量路况预测的完整工程,适合正在做数据库课程设计、想拿高分又不想只做个增删改查管理系统的人。

整个方案的定位很明确:你在课程设计里要证明的不是“我写了预测算法”,而是“我设计了一套从MySQL到Spark RDD再到结果回写的完整数据闭环”。读完这篇,你能知道课设的评分点在哪、每个文件为什么要存在、参数怎么调、答辩时老师会追着问哪些坑。

2. 课程设计的骨架:为什么选Scala、数据库选型与三层架构

2.1 为什么用Scala而不用Python写课设

很多人的第一反应是:用Python写预测不是更简单吗?确实,pandas加sklearn三小时能出结果,但这是数据库课程设计,不是机器学习课程设计。老师的评分表里通常列着“数据库设计是否规范、事务与并发控制是否到位、数据量级是否真实、技术栈是否有门槛”。Scala进入这个场景,靠的是它能名正言顺地把计算任务拆到数据库之外——用Spark的RDD做分布式数据集变换,再把结果写回MySQL,整个过程中数据库既要承担存储,又要配合计算层完成查询和写入,这正好卡在数据库课设的考察范围内。

从实际落地角度看,Scala在课设里的优势有三点:第一,类型安全。你定义一个TrafficRecord样例类,字段类型写死,数据从数据库拉出来就不会出现Python里那种字符串当数字用的翻车;第二,集合算子表达力强。filter、groupBy、reduceByKey写下来,比Java的循环嵌套短一半,答辩时讲代码也更好讲;第三,Spark的RDD天然支持分区和缓存,你可以在答辩时直接回答“数据量到百万级为什么还能跑”这种追问。

2.2 数据库端与计算端的分工:谁负责存、谁负责算

如果你把预测逻辑全写进数据库存储过程,那叫数据库编程,不叫数据库课程设计;如果你把数据全拉进Scala内存里算,那数据库就沦落成一个文件柜,同样拿不到高分。合理的分工是:MySQL负责结构化存储、按时间范围过滤、聚合后写回,Scala/Spark负责特征加工、滑动窗口计算、预测模型的批量推理。

具体的边界用一句话概括:所有能下推到数据库的过滤和聚合,用SQL做;所有需要跨多行做状态计算的滑动窗口逻辑,用Scala做。这样分工的原因很实际——MySQL的索引在时间范围和路段ID过滤上效率极高,但你让它对每个路段算过去7天的加权平均值,SQL写出来又臭又长,而且很难解释清楚。RDD擅长的是“对一批数据反复变换”,你从MySQL查出一周的历史流量,转成RDD后做map、reduce、sort,每一步都能打印出来佐证结果,答辩时就站得住。

2.3 数据模型设计:三张核心表与ER关系

数据库课设的高分基础是ER图和数据表设计。交通拥堵预测场景里,我一般会设计三张表,不多不少正好覆盖本课设的考察点:

  • road_info(路段表):road_id主键、road_name、road_length、lane_count、speed_limit。这条表是维度表,支撑“某条路在某个时段平均车速”的查询。
  • traffic_flow(流量表):flow_id自增主键、road_id外键、record_time(精确到分钟)、vehicle_count、avg_speed、congestion_level(0畅通/1缓行/2拥堵)。这是核心事实表,也是数据量大头。
  • weather_info(天气表):record_time主键、weather_type、temperature、visibility。天气表目的是给预测模型加一个特征维度,也方便答辩时讲“天气如何影响路况”。

三张表的ER关系就一条:road_info和traffic_flow是一对多,weather_info和traffic_flow通过record_time关联。注意不要把weather_type直接冗余进traffic_flow,虽然查询快一点,但会让表结构显得不专业,数据库范式那部分分数就丢了。

2.4 连接池与主键设计:底层不翻车,上层才稳

课设里最常见的低级错误是每次查询都新建Connection。评委看到这种代码基本直接扣分。这里用标准的HikariCP连接池,参数按课设级别配置就够:

# application.conf hikaricp { jdbcUrl = "jdbc:mysql://localhost:3306/traffic_db?useSSL=false&serverTimezone=Asia/Shanghai" username = "root" password = "root" maximumPoolSize = 10 minimumIdle = 2 connectionTimeout = 30000 }

连接池的核心理由是:预测计算需要频繁读取流量表和写回结果,如果每次都新建连接,MySQL的线程创建开销会让Spark作业变慢一个数量级。maximumPoolSize设10,minimumIdle设2,对课设的数据量来讲绰绰有余。再说主键,traffic_flow表建议用自增ID做主键,然后用(road_id, record_time)建联合索引。自增主键的好处是InnoDB的聚簇索引按顺序插入,不会产生页分裂;联合索引支撑的是这条最频繁的SQL——“查某条路某段时间的流量”。

3. 从原始数据到预测输入:数据导入与预处理链路怎么写

3.1 建库建表与导入CSV:别手工录数据,写脚本灌

课设数据基本来自公开数据集,格式多为CSV,字段大概有“路段编号、时间戳、车流量、平均速度”。手工一行行录不现实,写一个批量导入脚本才是工程化做法。先建库建表:

CREATE DATABASE IF NOT EXISTS traffic_db DEFAULT CHARACTER SET utf8mb4; CREATE TABLE traffic_flow ( flow_id BIGINT AUTO_INCREMENT PRIMARY KEY, road_id VARCHAR(20) NOT NULL, record_time DATETIME NOT NULL, vehicle_count INT NOT NULL, avg_speed DECIMAL(5,1) NOT NULL, congestion_level TINYINT DEFAULT 0, INDEX idx_road_time (road_id, record_time) ) ENGINE=InnoDB;

这里有两个容易被忽视的参数:字符集用utf8mb4而不是utf8,因为路段名可能含特殊字符,utf8mb4才是完整的UTF-8支持;索引idx_road_time的设计直接决定后续所有预测SQL的速度。灌数据时用LOAD DATA比逐条INSERT快几十倍,CSV里时间格式注意统一成yyyy-MM-dd HH:mm:ss。

LOAD DATA LOCAL INFILE '/path/to/traffic_data.csv' INTO TABLE traffic_flow FIELDS TERMINATED BY ',' ENCLOSED BY '"' LINES TERMINATED BY '\n' (road_id, @record_time, vehicle_count, avg_speed) SET record_time = STR_TO_DATE(@record_time, '%Y-%m-%d %H:%M:%S');

这段SQL的逻辑是:CSV文件里时间是字符串,直接插入DATETIME字段会报错或隐式转换出错,所以先用变量接收,再用STR_TO_DATE显式转换。LOAD DATA的坑在于LOCAL关键字——MySQL 8.0默认禁用了local_infile,如果报错,要在连接串里加allowLoadLocalInfile=true,或者在MySQL端执行SET GLOBAL local_infile = 1。

3.2 从MySQL到Scala:RDD创建与分区设置

数据进库后,用Scala读取的核心操作是JDBC读表转成RDD。代码骨架如下:

import org.apache.spark.{SparkConf, SparkContext} import java.sql.{DriverManager, ResultSet} case class TrafficRecord(roadId: String, recordTime: java.sql.Timestamp, vehicleCount: Int, avgSpeed: Double, congestionLevel: Int) val conf = new SparkConf().setAppName("TrafficPrediction").setMaster("local[*]") val sc = new SparkContext(conf) val jdbcUrl = "jdbc:mysql://localhost:3306/traffic_db?useSSL=false&serverTimezone=Asia/Shanghai" val conn = DriverManager.getConnection(jdbcUrl, "root", "root") val stmt = conn.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY) stmt.setFetchSize(500) // 关键:分批拉取,防止OOM val rs = stmt.executeQuery("SELECT road_id, record_time, vehicle_count, avg_speed, congestion_level FROM traffic_flow WHERE record_time >= '2024-01-01' AND record_time < '2024-02-01'") val dataBuffer = scala.collection.mutable.ArrayBuffer[TrafficRecord]() while (rs.next()) { dataBuffer += TrafficRecord(rs.getString("road_id"), rs.getTimestamp("record_time"), rs.getInt("vehicle_count"), rs.getDouble("avg_speed"), rs.getInt("congestion_level")) } rs.close(); stmt.close(); conn.close() val rdd = sc.parallelize(dataBuffer.toSeq, numSlices = 8)

这段代码的逻辑分三步:先用setFetchSize(500)告诉MySQL驱动每次从服务器取500行,避免一次性拉回百万级数据撑爆内存;然后遍历ResultSet填充ArrayBuffer;最后用parallelize转成RDD。numSlices参数设8,是因为local模式下的并行度等于CPU核心数,设太少会导致单分区数据量过大,设太多又会让任务调度开销压过计算收益。

3.3 时间窗口聚合:特征加工与脏数据剔除

原始流量表是分钟级的记录,直接预测没有意义,必须先按“路段 + 15分钟窗口”聚合成特征数据。实际操作里有两类脏数据必须处理:一是车辆数或速度出现负值,这是传感器故障产生的;二是同一时间戳出现重复记录,这会导致窗口聚合后数值虚高。

val cleanRdd = rdd.filter { r => r.vehicleCount >= 0 && r.avgSpeed > 0 && r.avgSpeed < 200 }.map { r => val bucket = (r.recordTime.getTime / (15 * 60 * 1000)).toLong val windowStart = new java.sql.Timestamp(bucket * 15 * 60 * 1000) ((r.roadId, windowStart), (r.vehicleCount, r.avgSpeed, r.congestionLevel, 1)) }.reduceByKey { (a, b) => (a._1 + b._1, a._2 + b._2, math.max(a._3, b._3), a._4 + b._4) }.map { case ((roadId, windowStart), (totalCnt, totalSpeed, maxLevel, cnt)) => val avgVehicle = totalCnt.toDouble / cnt val avgSpeedVal = totalSpeed / cnt (roadId, windowStart, avgVehicle, avgSpeedVal, maxLevel) }

这个地方的细节值得展开。时间桶的计算方式是把时间戳除以窗口长度再乘回窗口长度,本质上就是整数除法取整,这样做是为了让聚合的key变成“路段 + 15分钟起点”,这样同一时间窗口内的多条记录就能被reduceByKey合并。取maxLevel而不是平均或求和,是因为拥堵等级是枚举语义,取最大值代表这个窗口期内最差的路况,对预测更有利。整个链路的注释逻辑很清晰:先清洗,再分桶,再聚合。

3.4 结果写回:事务批量更新与幂等控制

预测结果要写回数据库,这步最能体现数据库功底。常见的翻车是每条预测结果单独UPDATE,百万条数据能把连接池打爆。批量写回用JDBC的addBatch和executeBatch,并控制每批500条;对主键冲突做幂等更新,保证重复跑不会插入重复数据。

import java.sql.{Connection, PreparedStatement} def writePredictions(conn: Connection, preds: Seq[PredictionResult]): Unit = { val sql = """ INSERT INTO traffic_prediction (road_id, predict_time, pred_vehicle_count, pred_avg_speed, pred_level) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE pred_vehicle_count = VALUES(pred_vehicle_count), pred_avg_speed = VALUES(pred_avg_speed), pred_level = VALUES(pred_level) """ val pstmt = conn.prepareStatement(sql) var batchCount = 0 preds.foreach { p => pstmt.setString(1, p.roadId) pstmt.setTimestamp(2, p.predictTime) pstmt.setDouble(3, p.predVehicleCount) pstmt.setDouble(4, p.predAvgSpeed) pstmt.setInt(5, p.predLevel) pstmt.addBatch() batchCount += 1 if (batchCount % 500 == 0) { pstmt.executeBatch() conn.commit() } } pstmt.executeBatch() conn.commit() pstmt.close() }

这段代码里最重要的是ON DUPLICATE KEY UPDATE语义:traffic_prediction表的主键设计成(road_id, predict_time),当同一路段同一时刻的预测已经存在时,只更新数值不新增行。这样即使Spark任务因为内存溢出失败后重启重跑,也不会产生重复记录。executeBatch每500条提交一次事务,是IO耗时和事务粒度的折中点——批太小数据库频繁提交,批太大出错回滚成本高。

4. 组装一套能跑的预测系统:代码结构、算法与可视化

4.1 源码目录该有的模样:用包结构撑起答辩的“工程感”

解压源码后,组织方式的优劣一眼就能看出来。如果所有类堆在同一个包下,老师会怀疑这是练习作品而不是课程设计。常见的工程化目录结构如下:

src/main/scala/com/course/traffic/ ├── model/ // 样例类:TrafficRecord, PredictionResult ├── dao/ // 数据库访问:ConnectionPool, TrafficFlowDAO ├── service/ // 业务逻辑:DataCleanService, PredictService ├── spark/ // 计算层:RDDBuilder, WindowAggregator └── Main.scala // 入口:读取配置、串联全流程 src/main/resources/ ├── application.conf // 连接池与参数配置 └── log4j.properties

提示:包结构不是装饰,它决定了老师问“这个模块职责是什么”时你能不能十秒内答出来。

入口Main.scala的职责只有三件事:加载配置、调用数据读取与清洗、触发预测和写回。预测和可视化分开设计,Main不直接依赖任何具体DAO的实现类,这样答辩时你可以说“这里用了简单的分层思想”。

4.2 滑动平均预测:课设级别的算法既要有用也要能讲透

预测算法不需要上LSTM和Prophet,课程设计时间有限,而且深度学习在答辩时容易引来“不可解释”的追问。滑动平均加时间衰减权重是最稳的方案:用过去4周同一时段的历史数据,加上权重随时间衰减,预测下一个15分钟的车流量和均速。

def predictNextWindow(history: Seq[(Long, Double)], targetWindow: Long): Double = { // history: 每个元素是(时间戳, 车流量),按时间升序排列 // targetWindow: 下一个15分钟窗口的起始时间戳 val weights = Array(0.4, 0.3, 0.2, 0.1) // 时间衰减权重:越近越重要 val sorted = history.sortBy(_._1) val recent4 = sorted.takeRight(4) // 只取最近4个同类型窗口 require(recent4.length == 4, "历史窗口不足4个,无法预测") recent4.zip(weights).map { case ((_, value), w) => value * w }.sum }

这段代码的算法逻辑是:排序后取最近4个时间窗口的数据,分别乘上0.4、0.3、0.2、0.1的权重再求和。为什么权重这样设?因为交通流量的自相关性随时间间隔变大而衰减,昨天的同时段比一周前的同时段更有参考价值。zip(weights)让每个历史值和权重配对,map后sum得到加权和。注意require的边界检查——如果历史数据不足4个窗口,直接报错而不是给个垃圾预测值,这样设计在答辩时也能解释为“算法的健壮性处理”。

4.3 从控制台到图表:让评委看到你的结果

课设的结果只有控制台打印远远不够,至少要提供一个“可视化”的输出文件。常见做法是让Scala程序把预测结果导出为CSV或JSON,再用ECharts渲染成折线图。这一步能加的分数超乎想象——大多数同学还在表格里看数字,你拿一张实际路段的“真实值vs预测值”曲线出来,评委对工程完成度的判断直接不同。

[ { "roadId": "R001", "time": "2024-01-15 08:00", "actual": 320, "predicted": 305 }, { "roadId": "R001", "time": "2024-01-15 08:15", "actual": 355, "predicted": 342 } ]

生成这个JSON的Scala代码不复杂,但要确保两个关键点:字段名和前端约定一致;时间格式ISO 8601,不要输出时间戳数字,不然前端没法直接用。如果你不会ECharts,直接输出CSV用Excel画图也行,目的只是让结果可读。

4.4 性能与边界参数:哪些参数必须公开、哪些必须调

运行整个预测任务时,有三个参数直接影响成败。第一是前文提到的fetchSize,500行一批适合课设的数据量级;如果数据量超过千万,配合分页SQL做“时间分片”更稳。第二是parallelize的分区数,8是经验值;数据量更大时可以上调到核心数的2倍。第三是连接池的maximumPoolSize,10已经是Spark单机模式的极限,设大了MySQL那边会报“Too many connections”。

另一个容易被忽略的点是时间参数的配置。预测“下一个15分钟”窗口,程序里不能写死时间,要从配置里读:

prediction.windowMinutes = 15 prediction.historyWeeks = 4 prediction.output.path = "./output/prediction_result.json"

写进配置文件的好处是答辩演示时可以直接改windowMinutes从15改成30,展示系统和代码解耦的程度,这比什么话术都管用。

5. 课设里真实踩过的坑:从运行报错到答辩追问

5.1 mysql-connector版本不匹配导致连接失败

现象:程序启动时报“Unknown system variable 'query_cache_size'”或“Communications link failure”。原因:MySQL Connector/J 8.x连接MySQL 5.7和8.0的行为不一样,8.0版本的驱动识别到服务器不支持的变量后会直接抛异常。解决:把驱动版本固定到和数据库大版本匹配的8.0.33,连接串里加上useSSL=false,避免SSL握手阶段不明不白地失败。

5.2 时间戳被Mysql默认时区转出8小时偏差

现象:从MySQL读出来的时间比数据库里实际时间多了8小时或少了8小时。原因:JDBC连接串没指定serverTimezone,驱动用了系统默认时区(可能是GMT)做转换。解决:连接串里显式加serverTimezone=Asia/Shanghai,或者直接在配置里设置connectionTimeZone=LOCAL,这样时间读写都不经过时区转换。

5.3 窗口聚合时少算了一个边界窗口

现象:预测结果里每天第一个15分钟窗口总是偏小。原因:分桶用的时间戳除法在跨天时出问题——如果用epoch毫秒做除法,凌晨0点整的窗口会被归到前一天最后一个桶。解决:分桶前先统一减掉当天0点的偏移量,或者在SQL里用DATE_FORMAT(record_time, '%Y-%m-%d %H:%i')直接截断到15分钟粒度,再做GROUP BY。

5.4 连接池配置不当导致Spark任务假死

现象:Spark任务跑到某个stage突然卡住不动,日志里全是“等待连接”。原因:前面有代码打开了Connection但是没关闭,连接池连接被耗尽,后面的任务在排队等连接超时。解决:把使用连接的代码块用try/finally包住,finally里关闭Connection;同时在日志里开启HikariCP的泄漏检测,设置leakDetectionThreshold=10000,能直接打出行号。

5.5 答辩必问:你的预测值和真实值差了30%,为什么还说它准

现象:老师抽查某个路段某个时段,发现预测值误差30%,直接怀疑结论可信度。原因:滑动平均在早晚高峰的突变点确实误差大,这是算法本身的局限。解决思路不是回避,而是主动展示误差分析:算一下全量数据的MAPE(平均绝对百分比误差),画出误差分布,在结论里写明“模型在平稳时段误差小于10%,在早晚高峰突变时段误差最大到30%”。这不会扣分,反而证明你做了评估,比拿个号称“准确率95%”但不解释怎么算出来的要好得多。

6. 让这份课设变成简历亮点:把预测装进可运行的服务

那个zip解压完、跑通、写完报告之后,忍不住想跟你多说一句:如果时间来得及,给这个项目加一个小小的HTTP接口,用Akka HTTP或者纯Java的HttpServer都行,暴露两个接口——按路段查最新预测、按时间段回放历史真值。这不复杂,几十行代码的事,但意义完全不同:课设演示时从命令行输出变成浏览器里看图表,你讲的就不是“我写了段脚本”,而是“我搭了一个简单的预测服务”。

我的习惯是保留一份“能跑的数据”和“跑完的日志”。日志里要能看到每个阶段处理了多少条记录、用了多少秒、预测结果的MAPE是多少。这些数字是答辩现场最有力的证据。数据的话,课程设计用2024年1月整月数据就好,太大反而拖慢演示节奏。

如果你打算把这个方向做深,下一步可以换数据集验证模型稳定性,也可以把滑动平均换成随机森林回归,对比不同算法在同一份数据上的误差。这些都留给学有余力的时候,但第一步永远是让你手头这套Scala链路稳定、可复现、能讲明白。毕竟课设拿高分的关键,从来不是项目名多唬人,而是老师随便挑一张表、一条SQL、一个RDD算子,你都能讲清楚它为什么在那里、它做了什么、问题来了怎么排查。

希望这篇笔记能帮你把那份zip变成真正属于你的工程能力,答辩顺利。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 3:22:38

Spark+Hive交通智能研判系统源码解析与实战避坑指南

简介&#xff1a;《基于SparkHive的交通智能研判系统》是一套用于毕业设计和课程设计场景的大数据实践项目&#xff0c;基于Spark与Hive两大组件构建&#xff0c;面向交通流量实时计算、历史数据管理和智能研判分析等问题&#xff0c;适合正在学习或开发分布式数据处理系统的读…

作者头像 李华
网站建设 2026/10/3 3:22:20

AI Agent多智能体仿真:用大模型构建舆论推演沙盘

开门见山说一个事&#xff1a;把“社会舆论”放进AI里做思想实验&#xff0c;现在已经不是科幻设定&#xff0c;而是一套可以落地的工程方案。核心思路很简单——用大模型驱动一批虚拟智能体&#xff0c;让它们拥有不同身份、立场、信息渠道和表达习惯&#xff0c;在一个人造的…

作者头像 李华
网站建设 2026/10/3 3:22:02

S7-1200 PID_Temp恒温控制实战:从组态到参数整定

1. 做恒温控制&#xff0c;为什么我盯上了PID_Temp这条工艺指令做工业自动化这些年&#xff0c;温控项目几乎每个工程师都会碰到。从塑料挤出机的料筒加热&#xff0c;到烘箱、干燥罐、反应釜&#xff0c;再到实验室的小型恒温槽&#xff0c;场景千千万万&#xff0c;但控制逻辑…

作者头像 李华
网站建设 2026/10/3 3:21:42

CST时域仿真网格设置本质是时空耦合博弈

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 3:21:41

SpringCloud插件化架构:大文件加密分块传输实战

入行做汽车制造IT快十年&#xff0c;我亲眼见过一套2.3GB的CATIA总成图纸是怎么把供应链搞得鸡飞狗跳的。把SpringCloud、插件机制和加密分块传输三个词凑到一起&#xff0c;并不是为了造概念&#xff0c;而是被一张2.3GB的CATIA总成图纸逼出来的。今天聊的这套方案&#xff0c…

作者头像 李华
网站建设 2026/10/3 3:21:20

SPSS五步实现多指标联合诊断:Logistic回归+ROC曲线实战

直接开工。临床上做诊断试验&#xff0c;单指标的ROC曲线大家都熟&#xff0c;但真到了写论文或做课题汇报的时候&#xff0c;评审一句“你这个指标的AUC才0.7&#xff0c;太低了”就能把你噎住。这时候你就得换个思路&#xff1a;既然一个指标不够&#xff0c;那就把几个指标联…

作者头像 李华