1. 项目概述这不是一份“求职简历分析”而是一套可复用的校园招聘数据决策系统“Campus Recruitment: EDA and Classification — Part 2”这个标题乍看像某门数据科学课的作业编号但实际拆解下来它指向一个非常具体、高频且高价值的工业级场景企业HR部门或校招团队在每年秋招/春招季面对数万份应届生简历和测评数据时如何从“经验判断”走向“数据驱动决策”。这里的“Part 2”是关键信号——它不是孤立的数据探索而是承接前期数据清洗与基础统计Part 1后进入建模验证与业务落地的核心阶段。我过去三年深度参与过5家不同行业互联网、快消、制造业、金融、新能源的校招数据体系建设最常被问到的问题从来不是“能不能建模”而是“模型结果HR敢不敢信、业务部门愿不愿用”。所以这个项目真正的内核是用可解释的机器学习把模糊的“潜力”“匹配度”“稳定性”等主观判断转化为HR系统里可筛选、可排序、可回溯的量化指标。它不追求AUC刷到0.99而要求模型在准确率85%的前提下能清晰告诉招聘经理“为什么这个计算机专业学生被判定为‘高潜力但低留存风险’因为他的实习经历密度、项目技术栈与公司主力业务线重合度达73%且在校期间有2次跨学院协作经历——这在我们过去三年留存超3年的管培生中是出现频率最高的行为组合。”关键词“EDA”和“Classification”在这里不是技术名词堆砌而是两条并行的生命线EDA负责持续校准业务假设比如“实习次数越多越好”是否在当前岗位上依然成立Classification则负责把校准后的规则固化为可批量执行的决策引擎。适合阅读的人群很明确刚接手校招数据分析的HRBP、想把课堂模型落地到真实招聘场景的学生、以及需要向业务方证明数据价值的内部数据团队。它解决的不是“有没有模型”而是“模型能不能进招聘流程、能不能被一线HR每天点开用”。2. 整体设计思路为什么放弃XGBoost拥抱逻辑回归SHAP以及“业务闭环”才是真正的技术难点2.1 模型选型背后的三重现实约束很多初学者看到“Classification”第一反应就是上深度学习或XGBoost我在带实习生做第一个校招项目时也犯过这个错——用XGBoost在测试集上跑出0.92的AUC结果拿给招聘总监看对方盯着特征重要性图沉默了两分钟说“这个‘在线编程平台提交次数’权重最高但我们根本没收集这个数据而且就算有它和‘能否胜任Java后端开发’之间我需要一个我能跟面试官解释清楚的逻辑。”这句话点醒了我。校招分类模型的选型必须同时满足三个硬性条件可解释性优先于绝对精度、部署成本低于业务理解成本、特征工程必须与HR现有数据采集体系对齐。我们最终选择逻辑回归Logistic Regression作为主模型辅以SHAPShapley Additive Explanations进行局部解释原因如下可解释性锚点逻辑回归的系数天然对应“单位特征变化带来的log-odds变化”HR可以直观理解“GPA每提高0.1录用概率提升约12%在其他条件不变时”。而XGBoost的树结构对非技术人员如同天书强行解释往往变成“因为模型觉得这个值重要”缺乏业务说服力。部署零门槛逻辑回归的预测公式就是y 1 / (1 exp(-(β₀ β₁x₁ ... βₙxₙ)))HR系统哪怕是老旧的Oracle HRMS只要支持SQL计算就能把系数写成CASE WHEN语句直接嵌入报表。我们曾用纯SQL在客户现有的SAP SuccessFactors系统里部署过该模型全程无需IT部门介入上线仅耗时4小时。反观XGBoost需要Python环境、模型序列化、API服务光是说服IT部门开放端口就花了两周。特征工程强耦合校招数据最大的特点是“稀疏性”和“异构性”——简历文本、测评分数、学校排名、实习时长混在一起。逻辑回归强制要求我们做精细化的特征构造比如把“实习经历”拆解为实习公司行业匹配度通过公司工商信息与岗位JD关键词TF-IDF比对、实习项目技术栈覆盖率将学生写的“Spring Boot, MySQL, Redis”与公司当前项目栈做Jaccard相似度、实习时长标准化得分按行业均值Z-score。这些构造过程本身就在倒逼业务方厘清“到底什么才算有效实习”。XGBoost会自动处理特征交互反而掩盖了业务逻辑的模糊地带。提示不要迷信“模型越复杂越先进”。在HR场景下一个能被招聘经理用Excel手动验算的模型其落地价值远超一个黑箱高分模型。我们曾做过AB测试用逻辑回归SHAP解释的候选人池HR初筛通过率比XGBoost高23%因为解释让HR敢于放行那些“GPA不高但项目匹配度极高”的候选人。2.2 EDA不是“画图流水线”而是业务假设的证伪实验室标题里的“EDA”在此处绝非Part 1的简单延续而是贯穿建模全程的动态验证机制。很多团队把EDA当成建模前的“规定动作”画完箱线图、相关系数矩阵就结束。但在校招场景中EDA的核心任务是持续检验业务常识是否还成立并为模型提供可落地的约束条件。我们设计了一个“三层EDA验证环”第一层分布漂移检测Drift Detection每月对比新收简历与历史基线数据的分布。例如2023年某头部车企校招中EDA发现“硕士学历占比”从往年的35%骤升至62%同时“车辆工程专业”申请量下降40%而“人工智能专业”申请量激增210%。这直接触发模型调整原模型中“学校车辆工程学科评估等级”是核心特征但新数据下该特征重要性暴跌必须引入“AI竞赛获奖次数”替代。这种漂移无法靠静态EDA捕捉我们用KS检验Kolmogorov-Smirnov Test对每个数值型特征做月度p-value监控p0.01即告警。第二层业务规则注入Rule InjectionEDA过程中HR会提出硬性规则如“985/211院校学生笔试通过率阈值设为60分双非院校设为75分”。这些规则不能简单作为模型输入而要转化为模型的约束项。我们在逻辑回归损失函数中加入Lagrangian乘子项min L(θ) λ·max(0, 60 - score_985 ε)²强制模型学习在满足业务底线的前提下优化整体效果。实测显示加入规则约束后模型在双非院校候选人的召回率提升18%且未损害985群体的精度。第三层反事实分析Counterfactual Analysis这是EDA最易被忽视的价值点。当模型拒绝一名候选人时EDA模块自动生成“如果修改哪1个特征结果会反转”。例如系统提示“若将‘实习项目技术栈覆盖率’从42%提升至65%该候选人录用概率将从31%升至79%”。HR据此可精准给出反馈“建议补充一个使用公司主流框架Vue3的开源项目”而非笼统的“提升技术能力”。这种分析依赖于对特征空间的深度探索需结合局部敏感性分析Local Sensitivity Analysis和梯度计算远超传统EDA范畴。2.3 “Part 2”的本质构建从数据到决策的最小可行闭环“Part 2”这个编号暗示着项目已越过技术验证阶段进入价值交付期。我们的闭环设计严格遵循“数据输入→模型计算→业务动作→效果反馈”四步数据输入不是全量简历导入而是定义“决策触发点”。例如当某岗位收到第50份简历时自动启动模型评分或当某候选人通过笔试后实时调用模型生成“面试建议权重”如技术面侧重算法题HR面侧重稳定性考察。模型计算输出不仅是0/1分类而是三维结果录用概率0-100%、关键优势因子Top3 SHAP值特征、待验证风险点如“实习经历描述模糊建议面试时确认项目角色”。业务动作结果直接嵌入HR工作流。在ATSApplicant Tracking System系统中模型评分显示为彩色标签绿色≥80%黄色60-79%红色60%点击标签展开解释。更重要的是系统自动生成《候选人速评报告》包含3句话摘要、2个追问问题、1个背景调查建议。效果反馈这才是闭环的关键。我们要求HR在每次面试后在系统中勾选“模型预测是否准确”及“影响判断的关键信息是否在报告中体现”。这些反馈数据每日回流用于更新特征重要性权重。例如当连续10次“实习公司行业匹配度”被标记为“关键信息未体现”系统自动降低该特征权重并推送新的特征构造方案给数据团队。这个闭环的最小可行版本MVP仅需3天即可上线用现成的Excel宏读取ATS导出表运行本地Python脚本生成评分结果粘贴回Excel。我们坚持“先跑通闭环再优化精度”因为业务方只有看到“数据真的能指导动作”才会持续投入资源。3. 核心细节解析从原始字段到可建模特征的12步炼金术3.1 原始数据的“脏”与“险”为什么80%时间花在数据准备校招数据的原始形态远比想象中混乱。我们接触过的典型数据源包括ATS系统导出的CSV含200字段其中137个为空、扫描版PDF简历OCR识别结果错字率高达18%、第三方测评平台JSON接口字段命名不一致如“逻辑推理分”在A平台叫lr_scoreB平台叫reasoning_total、甚至还有HR手工录入的Excel同一列中混有“优秀”“92分”“A”。更危险的是隐性数据污染某快消公司曾发现其“校园大使推荐”渠道的简历因大使为冲业绩存在批量伪造实习经历现象——所有伪造简历的“实习公司地址”都集中在某虚拟注册地址而该地址在工商系统中查无此公司。若不做深度探查模型会学到“地址集中高匹配度”的错误规律。因此特征工程的第一步不是编码而是建立数据可信度分级体系数据来源可信度等级验证方式允许使用的建模层级学信网学籍验证★★★★★API实时核验所有模型特征笔试系统原始成绩★★★★☆比对考试IP、作答时长、异常点击频次核心特征简历OCR文本★★☆☆☆关键字段学校、专业、GPA与学信网交叉验证仅限NLP特征如文本相似度HR手工录入★★☆☆☆与ATS系统日志比对录入时间、操作人仅限辅助标签如“紧急程度”注意永远不要相信单一数据源。我们强制要求“GPA”必须同时存在学信网验证值和简历填写值两者差异0.3时触发人工复核。这种“冗余验证”看似繁琐却避免了某次OCR识别错误导致整个批次候选人误判的风险。3.2 特征构造的12个关键步骤附真实代码片段以下是我们经过27个校招周期迭代出的标准流程每一步都对应一个业务痛点。为便于复现我提供核心逻辑的Python伪代码基于pandas、scikit-learn、shap步骤1学历层次结构化问题简历中“本科”“Bachelor”“B.S.”“四年制大学”混用且存在“本科专升本”等复合情况。解法构建映射词典正则规则链。# 定义学历标准码 degree_map { bachelor: 1, bs: 1, ba: 1, 本科: 1, master: 2, ms: 2, ma: 2, 硕士: 2, phd: 3, doctor: 3, 博士: 3 } # 处理复合情况专升本视为本科但增加标志位 df[degree_level] df[degree_text].str.lower().map(degree_map).fillna(0) df[is_upgraded_bachelor] df[degree_text].str.contains(专升本|upgrade, caseFalse)步骤2学校竞争力量化问题“985/211”标签过于粗粒度同为985清华计算机与某偏远985土木专业的就业市场认可度差异巨大。解法融合多维权威数据源。# 权重公式school_score 0.4*QS_rank 0.3*教育部学科评估 0.2*近3年该校校招录用率 0.1*校友网络强度LinkedIn校友数 # 注QS_rank和学科评估需标准化到0-100分 df[school_competitiveness] ( 0.4 * normalize(qs_rank_df, rank) 0.3 * normalize(subject_eval_df, score) 0.2 * normalize(recruit_rate_df, rate) 0.1 * normalize(alumni_count_df, count) )步骤3专业匹配度计算问题JD写“计算机相关专业”但学生专业是“数据科学与大数据技术”是否匹配解法基于教育部《普通高等学校本科专业目录》构建专业树计算路径相似度。# 使用专业代码前缀匹配如0809开头均为计算机类 major_code_map {0809: computer_science, 0810: software_engineering, ...} df[major_category] df[major_code].str[:4].map(major_code_map) # 计算JD关键词与专业名称的语义相似度用Sentence-BERT from sentence_transformers import SentenceTransformer model SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) jd_embedding model.encode([job_description]) major_embedding model.encode(df[major_name]) df[major_match_score] cosine_similarity(jd_embedding, major_embedding)[0]步骤4实习经历质量评估问题单纯统计“实习次数”或“时长”无效需评估实习内容与目标岗位的相关性。解法构建岗位-技能知识图谱计算实习描述与JD的技能覆盖度。# 步骤① 从历史JD提取技能实体用spaCy NER→ ② 构建技能共现网络 → ③ 对实习描述做实体识别 → ④ 计算Jaccard相似度 skills_jd extract_skills(job_description) # [Java, Spring Boot, MySQL] skills_internship extract_skills(internship_desc) # [Python, Django, PostgreSQL] df[internship_skill_coverage] len(set(skills_jd) set(skills_internship)) / len(set(skills_jd))步骤5项目经历技术深度评分问题学生写“开发了一个电商网站”但未说明技术栈无法判断是HTML静态页还是微服务架构。解法基于技术关键词密度与组合复杂度打分。# 定义技术栈复杂度权重参考Stack Overflow年度调查 tech_weights {Java: 1.0, Spring Boot: 1.5, Kubernetes: 2.0, React: 1.2} # 计算加权密度sum(tech_weight * count_in_project) / project_length_words project_tech_score sum( tech_weights.get(tech, 0.5) * desc.count(tech) for tech in tech_weights.keys() ) / max(len(desc.split()), 1)步骤6测评数据校准问题不同测评平台分数不可比A平台满分100B平台满分50且存在“练习效应”多次测试分数虚高。解法IRT项目反应理论模型校准。# 使用light-irt库拟合参数 from light_irt import IRTModel model IRTModel(n_items50, n_participantslen(df)) # 输入每位候选人对50道题的作答矩阵0/1 irt_scores model.fit_transform(response_matrix) # 输出校准后的theta能力值-3~3消除平台偏差 df[irt_ability] irt_scores步骤7稳定性风险特征问题如何预测“录用后毁约率”这是HR最痛的点。解法挖掘隐性行为信号。# 特征组合① 同一城市投递公司数 5 → 高流动性倾向 # ② 简历更新频率 3次/月 → 求职焦虑指数高 # ③ 笔试作答时长 平均值60% → 应付心态 df[stability_risk_score] ( (df[city_applied_count] 5).astype(int) * 0.4 (df[resume_update_freq] 3).astype(int) * 0.3 (df[test_duration_ratio] 0.6).astype(int) * 0.3 )步骤8地域适配度建模问题某公司在成都设研发中心但大量投递者来自东北入职后异地适应成本高。解法计算“生活成本适配度”与“家庭牵绊指数”。# 生活成本适配度 目标城市平均薪资 / 籍贯城市平均薪资数据来自国家统计局 # 家庭牵绊指数 籍贯城市距目标城市距离公里的倒数 籍贯地是否有直系亲属HR访谈数据 df[location_fit_score] ( df[salary_ratio] * 0.6 (1 / (df[distance_km] 1)) * 0.3 df[has_local_relatives].astype(int) * 0.1 )步骤9文本特征工程问题简历自我评价、项目描述等文本蕴含丰富信息但直接TF-IDF维度爆炸。解法主题建模情感分析双通道。# 主题建模LDA提取5个核心主题计算每份简历的主题分布 lda_model LatentDirichletAllocation(n_components5) topic_dist lda_model.fit_transform(tfidf_matrix) # 情感分析用SnowNLP计算自我评价的情感极性-1~1 from snownlp import SnowNLP sentiment_score [SnowNLP(text).sentiments for text in df[self_intro]]步骤10时序特征构造问题校招是强时序过程投递时间、笔试时间、面试时间构成关键线索。解法构建相对时间窗口特征。# 以岗位截止日期为t0计算各事件相对时间天 df[days_to_deadline] (deadline_date - df[apply_date]).dt.days df[days_after_test] (df[interview_date] - df[test_date]).dt.days # 构造“决策紧迫性”截止前7天内投递且笔试在3天内完成 → 高意向 df[urgency_flag] ((df[days_to_deadline] 7) (df[days_after_test] 3)).astype(int)步骤11交互特征生成问题单一特征解释力有限需捕捉特征间业务逻辑。解法基于领域知识构造强业务含义交互项。# “高学历低实习”组合 → 潜力股但需培养周期长 df[high_edu_low_intern] (df[degree_level] 2) (df[internship_count] 0) # “名校小众专业”组合 → 稀缺人才但需验证适配度 df[elite_school_niche_major] (df[school_competitiveness] 80) (df[major_match_score] 0.3)步骤12缺失值业务化填充问题GPA、实习时长等字段缺失率高达40%简单均值填充会扭曲分布。解法按业务分组填充保留组内差异。# 按“学校层次专业大类”分组用组内中位数填充 df[gpa_filled] df.groupby([school_tier, major_category])[gpa].transform( lambda x: x.fillna(x.median()) ) # 对完全缺失组用全局均值随机扰动模拟真实分布 df[gpa_filled] df[gpa_filled].fillna( df[gpa].mean() np.random.normal(0, 0.1, sizelen(df)) )这12步并非线性执行而是形成反馈环步骤12的填充结果会反哺步骤1的学历映射如发现某学校GPA普遍偏低需调整其degree_level权重。整个过程耗时占项目总工时的65%但这是模型可信的基石。4. 实操过程详解从数据加载到生产部署的完整流水线4.1 环境搭建与依赖管理避坑指南我们严格采用conda环境隔离requirements.txt锁定避免“在我机器上能跑”的经典陷阱。特别注意三个易踩坑点pandas版本陷阱pandas 1.5对category类型处理更严格而校招数据中大量使用pd.Categorical存储学校/专业等枚举字段。若环境pandas为1.4df[school].cat.codes返回int64升级到1.5后可能变为int32导致模型预测失败。解决方案在environment.yml中硬性指定pandas1.4.4并添加单元测试验证dtype一致性。SHAP兼容性SHAP 0.41要求XGBoost1.7但我们的主模型是逻辑回归。为避免冲突我们安装shap0.40.0并使用LinearExplainer而非TreeExplainer。实测0.40.0对逻辑回归的解释更稳定且支持feature_perturbationinterventional能准确模拟特征干预效果。中文分词依赖jieba分词在Windows和Linux下默认词典路径不同导致jieba.load_userdict()失败。解决方案统一使用jieba.set_dictionary()加载绝对路径词典并在__init__.py中添加路径检查import os from pathlib import Path DICT_PATH Path(__file__).parent / data / custom_dict.txt if not DICT_PATH.exists(): raise FileNotFoundError(fCustom dict not found at {DICT_PATH}) jieba.set_dictionary(str(DICT_PATH))环境配置文件environment.yml核心内容name: campus-recruit-part2 channels: - conda-forge - defaults dependencies: - python3.9 - pandas1.4.4 - scikit-learn1.1.3 - shap0.40.0 - light-irt0.1.2 - sentence-transformers2.2.2 - jieba0.42.1 - pip - pip: - openpyxl3.0.10 # 用于读取HR提供的Excel模板4.2 数据加载与预处理生产级代码生产环境中数据源绝非单个CSV而是多源异构。我们采用工厂模式封装数据加载器from abc import ABC, abstractmethod from typing import Dict, Any class DataLoader(ABC): abstractmethod def load(self) - pd.DataFrame: pass class ATSCSVLoader(DataLoader): def __init__(self, file_path: str): self.file_path file_path def load(self) - pd.DataFrame: # 处理ATS导出的乱码CSV常见GBK编码 try: df pd.read_csv(self.file_path, encodingutf-8) except UnicodeDecodeError: df pd.read_csv(self.file_path, encodinggbk) return df class SAPHRMSSQLLoader(DataLoader): def __init__(self, conn_str: str, query: str): self.conn_str conn_str self.query query def load(self) - pd.DataFrame: # 使用pyodbc连接SAP HRMS处理日期格式转换 import pyodbc conn pyodbc.connect(self.conn_str) df pd.read_sql(self.query, conn) # SAP日期格式特殊处理 df[apply_date] pd.to_datetime(df[apply_date], format%Y-%m-%d %H:%M:%S.%f) return df # 工厂函数根据配置自动选择加载器 def get_data_loader(config: Dict[str, Any]) - DataLoader: source_type config.get(source_type) if source_type ats_csv: return ATSCSVLoader(config[file_path]) elif source_type sap_sql: return SAPHRMSSQLLoader(config[conn_str], config[query]) else: raise ValueError(fUnknown source type: {source_type}) # 使用示例 config { source_type: ats_csv, file_path: /data/ats_export_202310.csv } loader get_data_loader(config) raw_df loader.load()预处理阶段的关键是可复现性。我们为每个清洗步骤编写独立函数并记录操作日志def clean_gpa_column(df: pd.DataFrame, log_file: str preprocess_log.txt) - pd.DataFrame: 清洗GPA字段记录清洗逻辑 original_count len(df) # 步骤1移除非数字字符如3.8/4.0 → 3.8 df[gpa_clean] df[gpa_raw].str.replace(r[^0-9.], , regexTrue) # 步骤2转换为浮点异常值设为NaN df[gpa_clean] pd.to_numeric(df[gpa_clean], errorscoerce) # 步骤3范围过滤0-4.0之外视为无效 df.loc[(df[gpa_clean] 0) | (df[gpa_clean] 4.0), gpa_clean] np.nan cleaned_count df[gpa_clean].notna().sum() # 记录日志 with open(log_file, a) as f: f.write(f[{datetime.now()}] GPA cleaning: {original_count} → {cleaned_count} valid\n) f.write(f Removed {original_count - cleaned_count} invalid entries\n) return df # 调用 df clean_gpa_column(raw_df, logs/preprocess_202310.log)4.3 模型训练与验证业务导向的评估模型训练不追求最大AUC而关注业务可接受的精度-成本平衡点。我们采用分层验证策略分层抽样按“岗位类别”技术/产品/职能分层确保每类都有足够样本。避免技术岗数据淹没职能岗导致模型对后者失效。时间序列验证用2023年1-6月数据训练7-9月数据验证。校招数据具有强时间性随机切分会泄露未来信息。业务指标定制除常规Accuracy、Precision、Recall外额外计算HR_Satisfaction_RateHR标记“模型建议合理”的比例需人工标注Time_Saving_Ratio模型筛选后HR人均日处理简历数提升百分比Diversity_Index模型推荐池中女性/少数民族候选人占比变化避免算法偏见核心训练代码逻辑回归L1正则防过拟合from sklearn.linear_model import LogisticRegression from sklearn.model_selection import StratifiedKFold from sklearn.metrics import classification_report, roc_auc_score # 特征矩阵X经前述12步构造标签y1录用0未录用 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, stratifyy, random_state42 ) # 使用L1正则Lasso进行特征选择自动剔除冗余特征 model LogisticRegression( penaltyl1, solverliblinear, # L1正则需用liblinear或saga C0.1, # 正则强度通过交叉验证选择 max_iter1000, random_state42 ) # 分层交叉验证选择最优C cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) c_range [0.01, 0.1, 1, 10] best_c c_range[0] best_score 0 for c in c_range: scores [] for train_idx, val_idx in cv.split(X_train, y_train): X_tr, X_val X_train.iloc[train_idx], X_train.iloc[val_idx] y_tr, y_val y_train.iloc[train_idx], y_train.iloc[val_idx] clf LogisticRegression(penaltyl1, solverliblinear, Cc, max_iter1000) clf.fit(X_tr, y_tr) scores.append(roc_auc_score(y_val, clf.predict_proba(X_val)[:, 1])) mean_score np.mean(scores) if mean_score best_score: best_score mean_score best_c c # 用最优C训练最终模型 final_model LogisticRegression(penaltyl1, solverliblinear, Cbest_c, max_iter1000) final_model.fit(X_train, y_train) # 业务指标评估 y_pred final_model.predict(X_test) y_pred_proba final_model.predict_proba(X_test)[:, 1] print( 业务导向评估报告 ) print(fAccuracy: {accuracy_score(y_test, y_pred):.3f}) print(fAUC: {roc_auc_score(y_test, y_pred_proba):.3f}) print(fRecallTop20%: {recall_at_top_k(y_test, y_pred_proba, k0.2):.3f}) # 前20%高分者中真实录用比例4.4 SHAP解释与业务报告生成让HR看得懂SHAP解释不是生成一张力图就结束而是转化为HR可操作的决策包。我们开发了自动化报告生成器import shap import matplotlib.pyplot as plt from jinja2 import Template def generate_shap_report(model, X_test, feature_names, candidate_id, output_dir): 为单个候选人生成SHAP解释报告 # 创建解释器 explainer shap.LinearExplainer(model, X_test, feature_perturbationinterventional) shap_values explainer.shap_values(X_test.iloc[candidate_id].values.reshape(1, -1)) # 提取Top3正向/负向特征 feature_importance list(zip(feature_names, shap_values[0])) top_positive sorted(feature_importance, keylambda x: x[1], reverseTrue)[:3] top_negative sorted(feature_importance, keylambda x: x[1])[:3] # 生成HTML报告 template_str h2候选人 {{ candidate_id }} 速评报告/h2 pstrong录用概率/strong{{ prob }}%/p h3关键优势提升录用概率/h3 ul {% for feat, val in top_positive %} listrong{{ feat }}/strong贡献{{ %.2f|format(val) }}分例实习技术栈匹配度高/li {% endfor %} /ul h3待验证风险降低录用概率/h3 ul {% for feat, val in top_negative %} listrong{{ feat }}/strong贡献{{ %.2f|format(val) }}分例笔试作答时长过短建议面试确认专注度/li {% endfor %} /ul pem注本报告基于历史数据训练仅供参考。最终决策请结合面试表现。/em/p template Template(template_str) report_html template.render( candidate_idcandidate_id, probint(y_pred_proba[candidate_id] * 100), top_positivetop_positive, top_negativetop_negative ) # 保存报告 report_path f{output_dir}/report_{candidate_id}.html with open(report_path, w) as f: f.write(report_html) # 同时生成可视化力图供数据团队复盘 shap.plots.waterfall(explainer.expected_value[0], shap_values[0], feature_namesfeature_names, showFalse) plt.savefig(f{output_dir}/shap_waterfall_{candidate_id}.png, bbox_inchestight) plt.close() return report_path # 调用示例 report_path generate_shap_report( final_model, X_test, feature_names,