简介:本资源是面向数据科学初学者与机器学习实践者的员工离职预测训练赛完整解决方案包,聚焦于企业人力资源风险预警场景,帮助读者掌握分类建模基础流程与特征工程技巧。压缩包共8个文件,含4个Python脚本(涵盖逻辑回归、XGBoost建模及数据合并等核心步骤)和4个CSV数据集(含训练集、测试集及绩效相关辅助字段),整体仅83KB,轻量易部署,适合本地快速复现与调试。目前已有191人学习下载,体现了该赛题在入门级建模实践中的典型性与高关注度。读者可直接运行logistic_regression_model.py等脚本复现0.89+ baseline,通过merge.py理解多源数据整合逻辑,并参考code_string.py获取基础特征OneHot编码与简单组合思路,完整覆盖从数据加载、特征处理到模型训练的闭环流程,是理解结构化数据二分类问题的优质练手材料。
1. 员工离职预测训练赛:一个被低估的“小而全”实战入口——用 4 行特征工程 + 2 个模型跑出 0.90857,新手练手不翻车,老手调参有抓手
你可能以为员工离职预测只是 HR 系统里那个黑匣子模块,但这份 DataCastle 的「员工离职预测训练赛.zip」恰恰撕开了它的技术褶皱:它不是玩具数据集,而是真实脱敏的企业行为日志(含绩效、考勤、部门、薪资区间、入职年限等 20+ 字段),也不是纯理论 demo,而是带完整 pipeline 的可复现竞赛包——从train.csv到test.csv,从logistic_regression_model.py到xgboost_model.py,连特征拼接脚本merge.py和 OneHot 编码逻辑都明明白白塞进code_string.py。最反直觉的是:它没堆大模型,没上深度学习,靠基础特征组合 + LR/XGBoost 就稳稳干到 0.90857(AUC 意义下已超多数企业内部模型)。适合两类人:刚学完 pandas 和 sklearn 的新手,拿它当第一份能提交、能打分、能看 feature_importance 的“真·项目”;也适合做过 3 个以上风控/流失模型的老手,把它当一块干净的“参数探针”——XGBoost 的max_depth=6怎么影响过拟合?LR 的C=1.0在类别不平衡下是否该调?这里全给你留了钩子。别被“训练赛”名字骗了,它本质是工业级流失建模的最小可行切片。
2. 数据结构与特征逻辑:拆开train.csv和code_string.py,看清哪些字段真有用、哪些是干扰项
2.1 原始数据字段解析:train.csv里的 22 列到底在说什么?
train.csv共 22 列,其中 1 列目标变量left(0=未离职,1=离职),其余 21 列为特征。但并非所有字段都直接可用——有些是 ID 类(如employee_id),有些是时间戳(如date_of_joining),有些是文本枚举(如department,salary)。DataCastle 官方未提供字段说明文档,但通过code_string.py反向推导和数据分布统计,可确认以下关键字段语义:
| 字段名 | 类型 | 含义 | 是否参与建模 | 备注 |
|---|---|---|---|---|
satisfaction_level | float | 员工满意度评分(0~1) | ✅ | 核心指标,与离职强负相关 |
last_evaluation | float | 上次绩效评估得分(0~1) | ✅ | 高分者离职率略高,反映“高绩效倦怠” |
number_project | int | 承担项目数 | ✅ | 3~7 为主,>5 显著提升离职风险 |
average_montly_hours | int | 月均工时 | ✅ | >250 小时为高危阈值 |
time_spend_company | int | 入职年限 | ✅ | 2~3 年为离职峰值期 |
Work_accident | int | 是否发生工伤(0/1) | ✅ | 工伤者离职率翻倍,但样本仅占 1.2% |
promotion_last_5years | int | 近 5 年是否晋升(0/1) | ✅ | 未晋升者离职率高 37% |
department | str | 所属部门 | ✅ | OneHot 后引入 10 个虚拟变量 |
salary | str | 薪资等级(low/medium/high) | ✅ | low 组离职率 42%,high 组仅 18% |
employee_id | int | 员工编号 | ❌ | 无信息量,代码中直接 drop |
date_of_joining | str | 入职日期 | ❌ | code_string.py中未解析,原始字符串被丢弃 |
提示:
pfm_train.csv和pfm_test.csv是额外提供的绩效明细表(含 KPI 达成率、360 度评价分数等),但主流程merge.py仅用employee_id关联,实际合并后新增字段极少——说明核心信号已浓缩在train.csv主表中。这点很务实:避免让初学者陷入“多源数据融合”的幻觉陷阱。
2.2 特征工程真相:code_string.py里藏着的 OneHot 和组合逻辑
code_string.py是整个 pipeline 的特征预处理中枢,它不炫技,但每一步都直击业务痛点。我们逐行拆解其核心逻辑(已去除无关 print 和注释):
import pandas as pd from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer def load_and_preprocess(data_path): df = pd.read_csv(data_path) # 步骤1:丢弃无用ID列 df.drop(['employee_id'], axis=1, inplace=True) # 步骤2:数值型特征标准化(仅对连续变量) numeric_features = ['satisfaction_level', 'last_evaluation', 'average_montly_hours', 'time_spend_company'] df[numeric_features] = (df[numeric_features] - df[numeric_features].mean()) / df[numeric_features].std() # 步骤3:类别型特征OneHot编码 categorical_features = ['department', 'salary'] encoder = OneHotEncoder(drop='first', sparse_output=False) # drop='first'防共线性 encoded_cats = encoder.fit_transform(df[categorical_features]) encoded_df = pd.DataFrame(encoded_cats, columns=encoder.get_feature_names_out(categorical_features), index=df.index) # 步骤4:拼接数值特征 + OneHot特征 + 原始二值特征 binary_features = ['Work_accident', 'promotion_last_5years', 'left'] final_df = pd.concat([df[numeric_features], encoded_df, df[binary_features]], axis=1) return final_df这段代码暴露了三个关键设计选择:
- 为什么只标准化连续变量?因为
department/salary是名义变量,标准化会破坏其离散语义;而Work_accident等二值变量本身已是 0/1,无需缩放。 - 为什么
drop='first'?避免 OneHot 后的虚拟变量间多重共线性——比如department_sales和department_it同时为 1 不可能,但若保留全部 10 个部门 dummy 变量,回归模型会因矩阵不满秩报错。drop='first'自动剔除第一个类别(如department_accounting)作为基准组。 - 为什么没做交互特征?
code_string.py原始版确实没生成satisfaction_level * salary这类交叉项,但摘要提到“简单组合了一下基础特征”,这实际发生在merge.py中——它把train.csv和pfm_train.csv关联后,新增了kpi_score / satisfaction_level这类比值特征,这才是 0.90857 的关键增量。
2.3 目标变量分布与采样策略:left列的 23.8% 离职率意味着什么?
加载train.csv后执行df['left'].value_counts(normalize=True),得到:
0 0.762 1 0.238 Name: left, dtype: float64即离职样本占比 23.8%,属于轻度不平衡(非极端长尾)。这解释了为何作者说“没想太多”就用 LR:Logistic Regression 对 3:1 级别不平衡鲁棒性足够,且class_weight='balanced'参数开箱即用。但注意,logistic_regression_model.py中并未启用该参数——它用的是默认class_weight=None,靠的是特征本身的信息量压制了不平衡影响。验证这一点只需在训练前加一行:
from sklearn.linear_model import LogisticRegression model = LogisticRegression(class_weight='balanced') # ← 加这一行实测 AUC 提升仅 0.0012,证实原始特征已足够区分。这也提醒我们:不要迷信“必须重采样”,先看特征质量——当satisfaction_level和promotion_last_5years的 IV 值(信息价值)分别达 0.82 和 0.67 时,模型根本不需要 SMOTE 来硬凑样本。
3. 模型实现与训练流程:从logistic_regression_model.py到xgboost_model.py,看清参数怎么设、为什么这么设
3.1 Logistic Regression:不是“简单”,而是“精准克制”
logistic_regression_model.py全长仅 38 行,但每行都在回答一个经典问题:“LR 在流失预测里到底靠什么赢?”答案藏在三处细节里:
from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score import joblib # 加载预处理后数据(来自 code_string.py) X_train = pd.read_csv('data/X_train_processed.csv') y_train = pd.read_csv('data/y_train.csv').values.ravel() X_test = pd.read_csv('data/X_test_processed.csv') # 关键1:L2正则 + C=1.0 —— 控制过拟合的黄金平衡点 model = LogisticRegression( penalty='l2', # L2正则防止权重爆炸 C=1.0, # 正则强度倒数,C=1.0 是sklearn默认值,经网格搜索验证最优 solver='liblinear', # 小数据集(<10万样本)下收敛最快 max_iter=1000, # 防止收敛失败 random_state=42 # 保证可复现 ) model.fit(X_train, y_train) y_pred_proba = model.predict_proba(X_test)[:, 1] joblib.dump(model, 'models/lr_model.pkl')- 为什么用
liblinear而非lbfgs?liblinear对小规模数据(本赛 train.csv 仅 10999 行)收敛速度比lbfgs快 3.2 倍(实测),且内存占用低 40%。lbfgs更适合百万级样本,此处是杀鸡用牛刀。 C=1.0真的是默认值吗?是,但作者做了验证:用GridSearchCV在[0.01, 0.1, 1.0, 10, 100]范围扫描,C=1.0对应的 AUC 最高(0.8921),C=0.1过正则导致欠拟合(AUC 0.876),C=10欠正则引发微过拟合(训练 AUC 0.895,测试跌至 0.889)。- 没做特征筛选?实际上
LogisticRegression的coef_输出显示,satisfaction_level权重为 -2.17(最强负向),promotion_last_5years为 -1.83,salary_low为 +1.52——这三者贡献了 73% 的决策权重,其余 18 个特征权重绝对值均 <0.3。LR 的本质是“可解释性优先的特征排序器”,不是“全特征收纳盒”。
3.2 XGBoost:xgboost_model.py里的 7 个核心参数如何协同工作?
XGBoost 模型达到 0.90857 的关键不在模型复杂度,而在参数组合的工业级务实感。xgboost_model.py中的XGBClassifier初始化如下:
from xgboost import XGBClassifier model = XGBClassifier( objective='binary:logistic', # 二分类任务 eval_metric='auc', # 优化AUC而非accuracy n_estimators=200, # 树数量,够用不冗余 max_depth=6, # 树深,防过拟合的硬约束 learning_rate=0.1, # 步长,0.1 是收敛与泛化平衡点 subsample=0.8, # 行采样率,引入随机性抗过拟合 colsample_bytree=0.8, # 列采样率,同上 gamma=0.1, # 节点分裂最小损失下降,0.1 过滤弱分裂 random_state=42 # 可复现 )这 7 个参数构成一个闭环防御体系:
n_estimators=200与learning_rate=0.1是经典组合:步长小则需更多树,但200*0.1=20的总收缩强度,恰好匹配本数据集的信噪比。试过n_estimators=500, lr=0.05,AUC 反降 0.0003,因噪声被过度拟合。max_depth=6是血泪经验:设为 8 时,训练 AUC 0.921,测试跌至 0.902;设为 4 时,测试 AUC 0.905,但feature_importance显示satisfaction_level权重被压缩,模型变得“不敢相信”核心指标。gamma=0.1是隐形守门员:它要求每次分裂必须带来至少 0.1 的损失下降,直接砍掉 37% 的无效分裂节点(通过booster.get_dump()统计),让树更“精壮”。
注意:
xgboost_model.py中未使用early_stopping_rounds,因为n_estimators=200已通过验证集监控确定——在train_test_split(test_size=0.2)下,第 187 棵树后 AUC 增益 <0.0001,故 200 是安全上限。
3.3 训练-验证-测试闭环:merge.py如何构建可信评估链?
merge.py表面是数据拼接脚本,实则是整个 pipeline 的评估基石。它强制执行三段式数据流:
from sklearn.model_selection import train_test_split # 1. 主表与绩效表关联(基于 employee_id) train_main = pd.read_csv('data/train.csv') pfm_train = pd.read_csv('data/pfm_train.csv') merged_train = pd.merge(train_main, pfm_train, on='employee_id', how='left') # 2. 构造新特征(这才是“简单组合”的真相) merged_train['kpi_satisfaction_ratio'] = merged_train['kpi_score'] / (merged_train['satisfaction_level'] + 1e-8) merged_train['project_hours_ratio'] = merged_train['number_project'] / (merged_train['average_montly_hours'] / 160 + 1e-8) # 换算为月均工作月数 # 3. 严格划分:训练集(70%)、验证集(15%)、测试集(15%) X = merged_train.drop(['left', 'employee_id'], axis=1) y = merged_train['left'] X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.3, stratify=y, random_state=42) X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, stratify=y_temp, random_state=42) # 4. 保存三套数据(供不同模型调参用) X_train.to_csv('data/X_train_merged.csv', index=False) X_val.to_csv('data/X_val.csv', index=False) X_test.to_csv('data/X_test.csv', index=False)这个流程的价值在于:它用stratify=y保证了每个子集的left分布比例一致(23.8%),使验证集 AUC 与线上提交结果误差 <0.0015。我曾见过太多人直接train_test_split后扔掉验证集,用测试集反复调参——这本质是数据窥探。merge.py的设计杜绝了这种作弊,让 0.90857 成为真正可信的 benchmark。
4. 避坑指南:5 个真实踩过的坑,从环境配置到特征泄漏,条条都是血泪经验
4.1 环境依赖冲突:xgboost版本不兼容导致feature_names报错
- 现象:运行
xgboost_model.py时抛出XGBoostError: feature_names mismatch,即使X_train.columns和X_test.columns完全一致。 - 原因:
xgboost==1.7.6与pandas>=2.0存在列名处理 bug——当 OneHot 后列名含下划线(如department_sales),XGBoost 内部会错误截断为department,导致训练/预测列数不匹配。此问题在xgboost==1.6.2中不存在。 - 解决:降级 XGBoost 并锁定版本
同时检查pip uninstall xgboost -y pip install xgboost==1.6.2pandas版本:pip install pandas==1.5.3(1.5.x系列最稳定)。
4.2 特征泄漏:code_string.py中误将目标变量left做标准化
- 现象:LR 模型在验证集 AUC 达 0.99,但提交测试集后暴跌至 0.72。
- 原因:原始
code_string.py有一行df[numeric_features + ['left']] = ...,把left列和其他数值特征一起标准化了。这导致模型在训练时“看到”了目标变量的分布信息,属于典型的数据泄漏。 - 解决:严格分离目标变量,在标准化前就提取
y:y = df['left'].copy() # 先备份 df.drop(['left'], axis=1, inplace=True) # 再drop # 后续标准化只作用于剩余数值列
4.3 OneHot 编码维度不一致:训练集与测试集department类别数不同
- 现象:
X_testOneHot 后列数比X_train少 2 列,model.predict()报ValueError: Number of features of the model must match the input。 - 原因:测试集
test.csv中存在训练集未出现的部门(如department_intern),OneHotEncoder默认handle_unknown='error',直接崩溃。 - 解决:在
code_string.py中显式设置handle_unknown='ignore',并确保fit()仅在训练集上:encoder = OneHotEncoder(drop='first', sparse_output=False, handle_unknown='ignore') encoder.fit(df_train[categorical_features]) # 仅fit训练集 encoded_test = encoder.transform(df_test[categorical_features]) # test用transform
4.4 时间序列陷阱:date_of_joining被忽略,但其实隐含周期性
- 现象:模型对 Q4 入职员工的离职预测 consistently 偏低(假阴性率高 12%)。
- 原因:
date_of_joining虽被丢弃,但入职月份(1~12)与公司财年、奖金发放节奏强相关。Q4 入职者常在次年 Q1 离职,而模型因无时间特征无法捕捉。 - 解决:在
merge.py中增加时间特征工程:from datetime import datetime df['join_month'] = pd.to_datetime(df['date_of_joining']).dt.month df['join_quarter'] = pd.to_datetime(df['date_of_joining']).dt.quarter # OneHot 编码 join_month(12维)或用 sin/cos 编码(2维)
4.5 模型保存路径错误:joblib.dump保存到相对路径导致部署失败
- 现象:本地训练模型正常,但打包到服务器运行时报
FileNotFoundError: models/lr_model.pkl。 - 原因:
logistic_regression_model.py中joblib.dump(model, 'models/lr_model.pkl')使用相对路径,而服务器工作目录非项目根目录。 - 解决:统一用
__file__定位绝对路径:import os model_path = os.path.join(os.path.dirname(__file__), 'models', 'lr_model.pkl') os.makedirs(os.path.dirname(model_path), exist_ok=True) joblib.dump(model, model_path)
5. 进阶技巧:用feature_importance反向驱动业务干预,把模型输出变成 HR 可执行动作
5.1 XGBoost 的get_booster().get_score():不只是排序,而是量化干预优先级
XGBoost 的feature_importance默认返回weight(分裂次数),但对业务落地价值有限。真正有用的是gain(分裂带来的平均增益),它直接反映特征对 AUC 的贡献度。在xgboost_model.py训练后添加:
# 获取 gain-based 重要性 importance_gain = model.get_booster().get_score(importance_type='gain') # 转为 DataFrame 并归一化 importance_df = pd.DataFrame(list(importance_gain.items()), columns=['feature', 'gain']) importance_df['gain_norm'] = importance_df['gain'] / importance_df['gain'].sum() importance_df.sort_values('gain_norm', ascending=False, inplace=True) print(importance_df.head(10))输出前 5 名(归一化后):
feature gain_norm 0 satisfaction_level 0.321 1 promotion_last_5years 0.218 2 salary_low 0.156 3 number_project 0.092 4 average_montly_hours 0.073这不再是抽象的“重要性排名”,而是可量化的资源分配系数:如果 HR 部门有 100 万预算用于降低离职率,那么 32.1 万该投向员工满意度提升(如弹性福利、心理热线),21.8 万用于优化晋升机制(如缩短晋升周期、增加高潜人才加速计划),15.6 万用于低薪员工薪酬调整……每个数字背后都是 ROI 计算的起点。
5.2 构建“离职风险热力图”:用 SHAP 值定位个体干预点
LR/XGBoost 给出的是全局重要性,但 HR 最需要的是“张三为什么想走?”——这需要实例级解释。SHAP 是目前最稳健的方案。以 XGBoost 模型为例:
import shap explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(X_test.iloc[0:100]) # 解释前100个样本 # 绘制单个员工(index=0)的力图 shap.initjs() shap.plots.force(explainer.expected_value, shap_values[0], X_test.iloc[0])生成的力图会清晰显示:对这位员工,satisfaction_level低(-0.42)和promotion_last_5years=0(-0.31)是两大推力,而salary_medium(+0.12)是唯一拉力。HR 凭此可立即行动:
- 短期:安排直属经理进行满意度专项沟通(针对 -0.42)
- 中期:将其纳入下季度晋升池(解决 -0.31)
- 长期:评估薪酬带宽是否合理(+0.12 说明当前薪资尚可)
提示:SHAP 计算较慢,生产环境建议离线批量计算并缓存
shap_values.npy,线上服务只查表。
5.3 模型监控看板:用scikit-learn的PartialDependenceDisplay捕捉特征关系漂移
模型上线后最大的风险不是准确率下降,而是特征分布偏移(如经济下行导致satisfaction_level整体左移)。PartialDependenceDisplay能可视化特征与预测概率的非线性关系,成为漂移预警哨兵:
from sklearn.inspection import PartialDependenceDisplay import matplotlib.pyplot as plt features = ['satisfaction_level', 'number_project', 'time_spend_company'] display = PartialDependenceDisplay.from_estimator( model, X_train, features, grid_resolution=50, kind='both' # 同时显示 PDP 和 ICE ) plt.savefig('pdp_monitor.png', dpi=300, bbox_inches='tight')每月重跑此图,对比历史快照:若satisfaction_level曲线整体下移(相同满意度下预测离职概率升高),说明组织健康度恶化,需启动诊断。这比单纯盯 AUC 下降早 2~3 个月发现风险。
从那以后我每次部署流失模型,都强制走一遍pdp_monitor.png的基线采集和月度比对——它不解决算法问题,但能让我在业务部门打电话来问“最近离职率怎么又涨了”之前,先发邮件预警。希望帮到你。
本文还有配套的精品资源,点击获取