简介:重大数据挖掘期末考试试卷,源自重庆大学研究生试卷(2011版),是面向数据仓库与数据挖掘课程学习者的重点复习资料。资源紧扣常见考点,系统梳理了数据仓库设计中的四种视图(操作、分析、历史、细节),以及数据清洗、集成、转换与规约等预处理技术的作用;对星型模式、雪花型模式、实时星座模式的结构特点与选择场景进行了归纳,并给出决策树分类算法从属性选择、划分到叶子节点处理的完整框架。压缩包共1个doc文档,大小64KB,内含简答与综合分析两类题型。题目既有概念简答,也有综合建模分析,要求基于词频向量计算文档距离、完成KMeans聚类,并借助RFID数据设计车险费率评估预测方案。题目设计贴近实际应用场景,能够有效检验考生对理论知识的理解与建模应用能力。已有734人学习,适合期末冲刺阶段模拟自测、查漏补缺,也可作为同类课程复习与命题的参考。
1. 数据挖掘期末试卷拆解:一份考卷背后的工程决策链
第一次拿到这份某高校数据挖掘期末考试试卷时,我的第一反应是:这不像是一份考死记硬背的卷子,更像是一份考察「能不能把课堂概念变成工程决策」的摸底考试。从数据仓库的四视图设计到文本聚类的手算推导,再到基于 RFID 轨迹数据设计车险定价模型,题目层层递进,几乎把数据挖掘从业者日常要面对的四类问题全问了一遍,这份试卷适合两类人反复品读:一类是正在复习备考、需要把知识串成体系的学生;另一类是刚转行做数据分析、想通过真题自查知识边界的工程师。别把这份试卷只当题库,它的价值在于每一个题目背后都隐藏着一个完整的选型逻辑,这本拆解笔记会把每一类题的思考路径、计算细节和实际工程中的坑全部摊开来讲。
2. 数据仓库设计:从四种视图到三种模式的选型逻辑
2.1 四种视图不是概念,是数据仓库的四个层级
试卷第一题问数据仓库设计中需要考虑的四种视图,很多考生直接背答案,但实际工程里这四种视图对应的是完全不同的数据使用场景。操作视图是业务系统产生的原始数据,在汽车保险场景里就对应着每一笔保单的录入记录、每一次理赔的报案流水,它变更频繁、粒度最细;分析视图是面向某个分析主题做好的汇总表,比如按车型、按月份聚合出的平均赔付金额表,它服务于报表和 OLAP 查询;历史视图强调的是时间维度,任何一条业务数据都需要有生效时间与失效时间,这样才能支持「上个月这个客户的驾龄是多少」这类回溯查询,在保险精算中这是费率厘定的基础;细节视图则保留业务事件的完整原始字段,哪怕这些字段当下没有分析价值,也不能轻易丢弃,因为未来做特征工程时可能用到。
工程里最常犯的错误是只用分析视图搭建数据集,把操作视图和细节视图直接丢掉。某公司做车险定价时只导出了按月汇总的赔付表,结果后续要做回归模型时发现缺少原始出险次数的具体日期和事故类型,根本没有办法做时间窗口的特征衍生。标准做法是先落操作视图和细节视图作为 ODS 层,再基于它们构建分析视图和历史视图,四者形成可追溯的数据血缘。我一般会在仓库设计文档里用一张数据流向表来约束各层的数据产出:
| 视图类型 | 典型内容 | 更新频率 | 服务对象 |
|---|---|---|---|
| 操作视图 | 业务流水原始表 | 实时/准实时 | 业务监控、追溯 |
| 分析视图 | 按主题汇总宽表 | 小时/天 | 报表、OLAP、特征库 |
| 历史视图 | 快照表、拉链表 | 天 | 时间回溯、精算分析 |
| 细节视图 | 全字段明细归档 | 天/月 | 特征挖掘、审计 |
2.2 星型、雪花型、实时星座:建模模式选型的三个真实场景
第二类简答题把数据仓库的三种模式放在一起考,出题人真正的意图是考察学生是否理解每一种模式的适用范围。星型模式是事实表在中心、维度表围绕在外围的结构,它用冗余换查询速度,订单事实表可以直接 JOIN 时间维度、地区维度、产品维度各一张表,没有多层关联,BI 工具写 SQL 非常顺手,适合查询路径固定、并发要求高的场景。雪花型模式是星型模式的规范化变体,把地区的省份、城市拆成单独的表,减少了维度的冗余存储,但查询时多了一层 JOIN,在海量数据场景下这种额外的关联开销非常明显,需要谨慎评估。
实时星座模式的叫法在实际工程里其实对应的是事实表不止一张、多个事实表共享维度表的架构,比如同时有保单事实表和理赔事实表,两者共用车辆维度表和地区维度表,这种结构适合企业级数据仓库的 Bus Architecture,也是数据中台最常见的设计形态。这三者不是版本迭代关系,可以混用:企业总线层用实时星座,具体数据集市层用星型,需要严格控制维表存储成本时再局部使用雪花型。选型的判据很简单:查询性能瓶颈和存储成本哪个是主要矛盾,以及维度表更新频率有多高。
2.3 星型模式的建模模板:一个可落地的表结构示例
为了把这三种模式落到具体设计里,我基于车险理赔场景给出星型模式的表结构模板。事实表理赔记录表存储每笔赔案的度量值,比如赔付金额、结案时长;维度表包括车辆维度表和地区维度表,两张维度表通过外键被事实表引用。
CREATE TABLE fact_claim ( claim_id BIGINT PRIMARY KEY, policy_id BIGINT NOT NULL, vehicle_dim_id BIGINT NOT NULL, region_dim_id BIGINT NOT NULL, claim_amount DECIMAL(12,2), claim_date DATE, settled_days INT, accident_type STRING ); CREATE TABLE dim_vehicle ( vehicle_dim_id BIGINT PRIMARY KEY, vin STRING, brand STRING, model_year INT, car_type STRING ); CREATE TABLE dim_region ( region_dim_id BIGINT PRIMARY KEY, province STRING, city STRING, risk_level STRING );这里需要注意的事实表设计原则是:度量字段要不可再分,维度字段只保留外键不冗余描述性文本,否则后续维度更新时会引发数据不一致。理赔金额字段设计成 DECIMAL(12,2) 而不是 FLOAT,是因为金额计算对精度敏感,FLOAT 在累计求和时会产生可观测的漂移,这在精算对账时是零容忍的。如果要把这个星型模式升级成雪花型,只需要把 dim_vehicle 中的 brand 字段拆成独立的品牌表,用一个 brand_id 替代,代价是每次查询多一次 JOIN。建议从星型起步,等真的发现维表存储成本成为瓶颈时再做规范化拆表,别在一开始就过度设计。
3. 数据预处理与决策树:从清洗路线到分裂策略的完整落地
3.1 数据预处理不是提模前的杂活,它直接决定模型上限
试卷要求简述三种常用的数据预处理技术,标准答案是数据清洗、数据集成、数据转换和数据规约里选三个,但工程里的每个动作都应该有明确的执行标准。数据清洗处理缺失值、异常值和重复数据,缺失率超过 50% 的字段一般直接删除,低于 5% 的用中位数或众数填充,剩余区间的用模型预测填充;异常值通过箱线图或 Z-Score 识别,Z-Score 绝对值大于 3 的样本在车险数据里通常意味着录入错误或极端骗保案例,需要单独审核。数据集成解决多源数据的字段冲突问题,比如车辆管理所的数据里车辆型号是字符串,保险公司里是编码,必须建立映射表。数据转换包括无量纲化和数据变换,车险特征里日均行驶里程和车龄量纲差异巨大,树模型不受影响,但后续用逻辑回归时必须做标准化。数据规约包括 PCA 降维和抽样,RFID 轨迹数据动辄上万维度,先用聚合把轨迹变成统计特征,再做 PCA。
关于预处理的顺序有一个稳定的血泪经验:先做清洗和集成,再做转换和规约,顺序颠倒了会导致很隐蔽的翻车。比如先做标准化再填充缺失值,缺失值的位置会被填充成 0,而这个 0 经过标准化后可能被放大成偏离均值十几个标准差的异常值,直接污染模型训练。另一个隐蔽的坑是把数据规约放在清洗之前,PCA 会把这些噪声也学习进去,导致降维后的主成分并不能代表真实的数据结构。
3.2 决策树框架的五个环节:从属性选择到剪枝策略
决策树简答题要拿高分,不能只写「选择最佳属性、划分数据集、构建树」这三句话,完整框架应该包括属性选择度量、树的生成、停止条件、剪枝策略和叶节点决策五部分。属性选择常见度量有三个:信息增益偏向取值较多的特征,信息增益率对多取值特征做了惩罚,基尼指数计算成本更低,CART 树默认用基尼。树的停止条件是组合条件,不仅看最大深度,还要看节点最小样本数、最小不纯度下降量,工程里最常用的是 min_samples_split 和 min_samples_leaf 两个参数来控制停止时刻。剪枝策略分预剪枝和后剪枝,预剪枝在建树过程中通过验证集决定是否继续分裂,后剪枝先生成完整树再自底向上修剪,工程实践中预剪枝更高效。叶节点决策采用多数表决,但在样本不均衡时不能直接表决,要调整样本权重或采用阈值判定,这是在保险风控场景里生还概率极低的决策点。
3.3 用 scikit-learn 复现一个完整的预处理加决策树流程
为了把上述理论变成能跑的代码,我用车险客户数据演示决策树建模的完整流程,数据集包含客户的年龄、驾龄、历史出险次数和是否发生理赔的标签。
import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier from sklearn.preprocessing import StandardScaler from sklearn.impute import SimpleImputer df = pd.DataFrame({ 'age': [28, 35, None, 45, 52, 23, 38, 61, 30, None], 'driving_years': [2, 8, 5, 20, 25, 1, 10, 30, 6, 4], 'claims_history': [0, 1, 0, 0, 2, 0, 1, 0, 0, 1] }) df['label'] = [0, 1, 0, 0, 1, 1, 0, 0, 0, 1] # 第一步:缺失值填充,age 字段的缺失率约 20%,用中位数填充 imputer = SimpleImputer(strategy='median') df['age'] = imputer.fit_transform(df[['age']]) # 第二步:标准化,对连续特征做 Z-Score 归一化 scaler = StandardScaler() df[['age', 'driving_years', 'claims_history']] = scaler.fit_transform( df[['age', 'driving_years', 'claims_history']]) # 第三步:训练集测试集划分 X = df[['age', 'driving_years', 'claims_history']] y = df['label'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y) # 第四步:决策树模型,使用信息增益作为分裂标准 clf = DecisionTreeClassifier( criterion='entropy', max_depth=4, min_samples_split=3, min_samples_leaf=1, class_weight='balanced') clf.fit(X_train, y_train) print("训练集准确率:", clf.score(X_train, y_train)) print("测试集准确率:", clf.score(X_test, y_test)) print("特征重要性:", clf.feature_importances_)代码里几个参数需要说明:criterion='entropy' 表示使用信息增益,换成 'gini' 就是 CART 基尼指数;max_depth=4 限制树深度防止过拟合,这里数据量小所以取较小的值;min_samples_split=3 表示内部节点再分裂所需的最小样本数;class_weight='balanced' 在标签不均衡时会自动加大少数类的权重,在保险理赔场景中理赔样本通常只有 5% 到 10%,这个参数几乎必须开启。跑完代码后要重点看特征重要性排序,如果 claims_history 排第一而 driving_years 重要性几乎为零,说明驾驶年限和事故发生的关系在这个小样本里没有显现,需要回到特征工程阶段思考是否应该引入交互特征,而不是盲目堆数据。
4. 文档向量与 KMeans 聚类:从距离计算到中心点逐行推导
4.1 余弦距离与欧氏距离:文档聚类的首选为何不是欧氏
试卷给四个文档的 TF 特征向量,要求计算距离并做 KMeans 聚类。特征向量形如 [2,3,4,2,0,0,0,1],本质上是 8 个特征词的词频统计。文档距离计算有两种主流选择:欧氏距离直接计算向量点在多维空间里的直线距离;余弦相似度计算两个向量之间的夹角,再转成距离。在文档场景里余弦距离远优于欧氏距离,因为 TF 向量受文档长度影响严重,一篇长文档每个词的绝对频次都会偏高,欧氏距离会误判长文档之间的真实差异,而余弦距离关注方向差异,更能反映主题相似度。例如文档 1 与文档 3 的欧氏距离会因为非零维度完全错开而显得很大,但从主题表达上看它们一个在讲保险理赔,一个在讲车辆维保,方向完全不同,余弦距离恰好能捕捉这种语义分工。
实际计算时有一点需要注意:TF 向量中的 0 值维度不参与实际的距离测量,但这些零维度依然会占据向量维度的一部分,所以距离计算必须基于完整的 8 维特征空间来进行,不能手动剔除零维度后再计算,否则会丢失对齐信息。
4.2 KMeans 迭代过程:初始化、分配、更新三步循环
KMeans 聚类的核心目标是把 4 个文档分成 2 个簇,使得每个文档到其所属簇中心点的距离之和最小。算法流程分为三步:第一步初始化中心点,可以随机选取 K 个样本作为初始中心,也可以使用 KMeans++ 策略让初始中心尽可能分散,或者根据业务经验手工指定;第二步将每个文档分配到距离最近的中心点所在的簇;第三步重新计算每个簇内所有样本的均值作为新的中心点,然后重复第二步和第三步,直到中心点不再变化。这道题由于 4 个文档的特征分布非常清晰,文档 1、2 的非零维度集中在前四个特征词,文档 3、4 的非零维度集中在后四个特征词,因此理想情况下两个簇就是前两个文档一组、后两个文档一组。但随机初始化有可能让中心点从同一个簇的两个文档中间起步,导致分类振荡,这正是工程里 KMeans 最大的不确定性来源。
4.3 手把手推导四文档聚类过程:一份可复现的 NumPy 实现
下面用 NumPy 直接复算试卷第 1 题的距离矩阵和 KMeans 中心点演化过程,避免只停留在公式理解上。
import numpy as np docs = np.array([ [2, 3, 4, 2, 0, 0, 0, 1], [2, 2, 3, 2, 0, 0, 0, 1], [0, 0, 0, 0, 2, 3, 3, 4], [0, 0, 0, 0, 3, 4, 3, 4] ], dtype=float) def cosine_dist(a, b): dot = np.dot(a, b) norm_a = np.linalg.norm(a) norm_b = np.linalg.norm(b) return 1 - dot / (norm_a * norm_b + 1e-8) # 计算四文档两两余弦距离,输出 4x4 距离矩阵 n = docs.shape[0] dist_matrix = np.zeros((n, n)) for i in range(n): for j in range(n): dist_matrix[i][j] = cosine_dist(docs[i], docs[j]) print("余弦距离矩阵:") print(np.round(dist_matrix, 3)) # KMeans 迭代:初始化中心点选文档1和文档4 centers = np.array([docs[0], docs[3]]) for iteration in range(3): # E 步:计算每个文档到两个中心点的距离,分配最近簇 distances = np.array([ [cosine_dist(doc, centers[0]), cosine_dist(doc, centers[1])] for doc in docs ]) labels = np.argmin(distances, axis=1) print(f"迭代 {iteration+1} 标签分配: {labels}") # M 步:重新计算每个簇的均值中心 new_centers = np.array([ docs[labels == 0].mean(axis=0), docs[labels == 1].mean(axis=0) ]) centers = new_centers print(f"迭代 {iteration+1} 新中心点:") print(np.round(centers, 3))运行结果会非常直观地展示两个中心点的收敛过程,最终得到的中心点分别是文档 1、2 的均值向量和文档 3、4 的均值向量,也就是摘要里提到的两个理想聚类簇中心。代码里的两个细节我要展开说明:余弦距离计算时在分母上加 1e-8 是为了防止零向量导致的除零异常,虽然在当前文档里不存在零向量,但切换到真实稀疏数据时这个保护是必要的;迭代只做了 3 次是因为这个数据集分布极清晰,实际应用中我会设置收敛阈值,即中心点位移小于 1e-6 时提前停止,而不是固定迭代次数,原因在于固定迭代次数可能造成资源浪费或者还没收敛就退出。
5. 车险费率评估方案避坑:从 RFID 数据到定价模型的三类典型问题
5.1 特征工程:把 RFID 轨迹变成行为特征,而非直接喂原始数据
综合题第二部分给出的场景是某市所有车辆安装 RFID 标签,路口和重要路段安装读卡器,数据公司可获得车辆轨迹数据以及车辆管理所的车辆基本信息,要求设计车险费率评估方案。这道题在考场上考验的是方案设计能力,在实际项目里考验的是特征工程能力。RFID 数据最原始的形态是「车牌号—读卡器编号—时间戳」三元组,这种数据的维度极高、内容极其稀疏,直接把读卡器编号作为特征放到模型里,模型几乎无法学习。正确的做法是把轨迹数据按车辆维度聚合成统计特征:日均出行次数、平均单次出行时长、夜间出行比例、高频路段分布、急加速急减速次数。
结合车辆管理所的基本信息,我建议把决策特征划分为三个层级。车辆属性包括车型、品牌、车辆年限、座位数,这是基础风险因子,SUV 的事故率比轿车高,车龄超过 8 年的车辆机械故障风险上升;行驶行为特征包括日均里程、夜间出行占比、经过高风险路口的频率、平均车速,日均里程超过 50 公里的车辆保费系数要显著上调;历史赔付特征包括出险次数、赔付金额、结案时长,这部分数据在保险公司内部有记录,需要在合规前提下与 RFID 数据做整合。
| 特征层级 | 具体特征 | 与事故率的逻辑关系 | 数据来源 |
|---|---|---|---|
| 车辆属性 | 车型、品牌、车龄 | 车型决定基础安全等级 | 车管所数据 |
| 行驶行为 | 日均里程、夜间出行占比 | 暴露风险和时间风险 | RFID 轨迹聚合 |
| 历史赔付 | 出险次数、赔付金额 | 历史赔付预测未来赔付 | 保险公司内部 |
5.2 模型选型:为什么逻辑回归和随机森林是稳妥的起点
试卷问选择哪种预测模型评估每辆车赔付金额,我在项目里通常采用两阶段建模。第一阶段用逻辑回归评估事故概率,输出的是 0 到 1 之间的风险概率值。选择逻辑回归的原因是它自带可解释性,每个特征的系数可以直接换算成费率调整系数,这在保险监管场景下是硬性要求——监管机构需要保司说清楚为什么这辆车保费高,逻辑回归天然满足这个需求。第二阶段用随机森林回归评估赔付金额,赔付金额的条件分布通常是长尾的,随机森林对异常值不敏感,且能捕捉特征之间的非线性交互关系,比如驾驶行为特征只有在车龄较大的样本里才会显著影响赔付,这种交互在逻辑回归里需要手动构造特征才能实现。
这里还要明确指出一个常见的混淆点:事故概率与赔付金额是两个完全不同的目标。概率高低描述的是风险频率,赔付金额描述的是损失强度,如果直接把两者合并成一个模型来预测「期望赔付额」,等于用同一个模型强制学习频率和强度两种不同的数据生成过程。正确做法是可以先算事故概率再用赔付回归模型计算条件赔付金额,最后期望赔付额 = 事故概率 × 条件赔付金额,这样每一步的模型输出都具备业务语义。
5.3 三例典型翻车复盘:现象、原因与解决
这一节把我实际踩过的坑和同行反馈过的典型问题整理成三条例行翻车记录,每一条都对应试卷答案里容易忽略的细节。
翻车一:模型把 RFID 原始序列直接当输入,导致特征爆炸。现象是某开发者在拿到路口读卡器数据后,把「车辆经过路段 A、B、C 的次数」全部展开成上万个稀疏列特征,模型训练时间暴增且 AUC 反而低于 0.5。原因是读卡器编号本身是 ID 类特征,高基数且稀疏,树模型会在这些无意义的 ID 上反复试探分裂,造成过拟合。解决方法是按业务先做轨迹聚合,把每个车辆在一周内经过的路口按风险等级加权求和,压缩成 20 个以内的统计特征。
翻车二:混淆事故概率与赔付金额,用一个回归模型硬预测。现象是某团队建立了赔付金额回归模型,但 RMSE 极高且预测值集中在均值附近。原因在于绝大多数车辆的赔付金额为 0,数据严重零膨胀,回归模型被零值主导,训练出的模型几乎无法区分低风险和高风险车辆。解决方法是拆成两阶段模型:先用逻辑回归预测出险概率,再单独对出险车辆建立赔付金额模型,我在实际项目中采用这种方法后模型的区分度提升了约两倍。
翻车三:RFID 数据里存在系统性的漏采和重复读卡。现象是某辆车某天轨迹显示经过了一个读卡器 37 次,而该路口一天的总车流量才 100 次,明显是异常数据。原因是同一个路口有多个读卡器在工作,车辆经过时多个读卡器同时触发,且该时间点有设备故障导致重复上报。解决方法是按「车辆+路口+时间窗口」做去重,时间窗口通常取 60 秒内只保留一条记录,同时在特征聚合前先剔除连续读卡次数超过正常范围 5 倍的记录。
5.4 两阶段定价模型的最小实现骨架
把上述方案变成代码骨架,方便读者在本地做一个最小验证。这个骨架使用的数据是模拟数据,真实项目中需要接入 RIFD 轨迹明细和车管所车辆信息表。
import numpy as np import pandas as pd from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler np.random.seed(42) n_samples = 2000 # 构造模拟数据:一次模拟车辆特征和出险概率 df = pd.DataFrame({ 'car_age': np.random.randint(1, 15, n_samples), 'avg_daily_km': np.random.randint(5, 120, n_samples), 'night_drive_ratio': np.random.rand(n_samples), 'risk_road_freq': np.random.randint(0, 30, n_samples), 'claims_history': np.random.randint(0, 4, n_samples) }) # 第一阶段标签:真实事故率与特征成非线性关系 logits = (0.08 * df['car_age'] + 0.02 * df['avg_daily_km'] + 0.3 * df['night_drive_ratio'] + 0.1 * df['risk_road_freq'] + 0.4 * df['claims_history'] - 3.0) df['accident_prob'] = 1 / (1 + np.exp(-logits)) df['accident'] = np.random.binomial(1, df['accident_prob']) # 第二阶段标签:只有出险的样本才有赔付金额 df['claim_amount'] = np.where( df['accident'] == 1, np.random.lognormal(mean=8.5, sigma=1.2, size=n_samples), 0 ) features = ['car_age', 'avg_daily_km', 'night_drive_ratio', 'risk_road_freq', 'claims_history'] # 第一阶段:逻辑回归预测事故概率 X_train, X_test, y_train, y_test = train_test_split( df[features], df['accident'], test_size=0.3, random_state=42) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) lr = LogisticRegression(max_iter=500) lr.fit(X_train_scaled, y_train) # 第二阶段:随机森林对出险样本拟合赔付金额 claim_data = df[df['claim_amount'] > 0] X_claim = claim_data[features] y_claim = np.log(claim_data['claim_amount']) # 对金额取对数抑制长尾 Xc_train, Xc_test, yc_train, yc_test = train_test_split( X_claim, y_claim, test_size=0.3, random_state=42) rf = RandomForestRegressor( n_estimators=200, max_depth=8, min_samples_leaf=5, random_state=42) rf.fit(Xc_train, yc_train) # 最终保险费率因子:事故概率 × 预测赔付金额 p_accident = lr.predict_proba(X_test_scaled)[:, 1] p_claim_amount = np.exp(rf.predict(X_test)) premium_factor = p_accident * p_claim_amount print("预测期望赔付金额前5条:", np.round(premium_factor[:5], 2))两处关键的工程细节要说清楚:第一,对赔付金额做对数变换是处理长尾分布的常规操作,原始赔付金额可能从几千元跨越到几十万元,直接拟合会让模型权重被少数大额赔案绑架,取对数后数据分布接近正态,模型才学得动;第二,逻辑回归前做了标准化,因为逻辑回归使用梯度下降求解,特征尺度不一致时收敛速度会显著下降,而随机森林不需要标准化,因为树模型只关心特征分裂点,不关心特征的绝对尺度。
6. 把试卷答案变成可交付系统:从手算推导到接口封装的三个进阶动作
6.1 动作一:把 KMeans 中心点计算封装成可复用的服务函数
试卷第 4 题的手算推导验证了算法的正确性,但工程里不能每次都在脚本里重写一套聚类逻辑。我通常会把聚类计算封装成独立的函数模块,输入任意形状的 TF 向量矩阵,输出簇标签和中心点,方便对接数据仓库的宽表。封装时建议加入 KMeans++ 初始化和收敛阈值两个参数,替代随机初始化,因为真实文档数据的聚类结果对初始中心非常敏感,随机初始化很容易掉进局部最优,而且固定迭代次数在两万条文本上会造成不可接受的时间开销。封装完成后用前文的四文档数据做单元测试,确保中心和手算结果一致,这就是把试卷答案沉淀为团队资产的第一步。
6.2 动作二:用 Spring Boot 暴露一个车险费率评估接口
当模型训练完成、特征工程稳定后,下一步就是把它部署到业务系统里。我的习惯是用 Spring Boot 包一层 REST API,内部加载训练好的模型文件,实现对前端或核心业务系统的统一调用,这是一个非常典型的工程实际形态。
@RestController @RequestMapping("/insurance") public class PremiumController { private final LogisticRegressionModel riskModel; private final RandomForestModel amountModel; public PremiumController(LogisticRegressionModel riskModel, RandomForestModel amountModel) { this.riskModel = riskModel; this.amountModel = amountModel; } @PostMapping("/quote") public double evaluatePremium(@RequestBody VehicleQuoteRequest request) { double accidentProb = riskModel.predict(request.toFeatureArray()); double claimAmount = Math.exp(amountModel.predict(request.toFeatureArray())); return accidentProb * claimAmount; } }这个接口的输入是车辆信息和驾驶行为指标,输出是期望赔付金额。两个值得注意的设计点:一是把风险概率回归和赔付金额回归拆成两个独立的模型字段注入,方便后续单独迭代任一模型而不影响接口签名;二是返回结果没有直接返回费率,而是返回期望赔付金额,让业务方根据监管要求叠加固定费用率和利润率,把定价决策权留给业务,模型只提供数据支撑。
6.3 动作三:设计一份模型验证清单,彻底告别黑匣子交付
最后的动作是强制给模型加一份验证清单,我每次做完车险项目都会逐项核对,这份清单源自一次深刻教训。那一次我在模型验收时只看了测试集 AUC,指标看起来很不错,结果上线后发现高风险车辆的识别率极低。复盘才发现,测试集里低风险样本占比过高,AUC 被大量容易分类的负样本撑高了,真正需要区分的高风险片段完全没有覆盖到。从那以后我每次做模型验证都强制走一遍五步流程:第一,检查预测分数分布是否与业务分布一致,高风险车辆的预测分数占整体比例是否符合先验判断;第二,按风险分层计算召回率,而不是只看全局指标,重点关注最高 5% 风险样本的覆盖率;第三,核对模型的特征重要性与业务经验的矛盾点,比如年均行驶里程对保费的影响被模型忽略了,需要回到特征工程检查;第四,对每个模型输入样本做分数解释,确保极端预测值能被拆解到具体特征;第五,做一次时间外验证,用前两年的数据训练、后一年的数据测试,模拟真实的未来预测场景,因为样本外泛化能力才是模型真正的价值所在。这套流程走完后,才能把模型放心地交给业务团队,也才是对试卷里那道车险设计题最好的工程回应。希望这份从试题到落地的拆解笔记能帮你在复习和实战中都少走一段弯路。
本文还有配套的精品资源,点击获取