简介:这份资源面向希望上手机器学习实战的初学者与数据分析从业者,围绕员工绩效与薪资数据集,提供从数据预处理、特征工程到建模预测的完整分析链路,帮助读者理解回归任务在真实业务场景中的落地方式。压缩包共23个文件,以20个Python源代码为主,另含1个CSV数据集、1份PDF任务说明与1份readme文本,整体约266KB,代码与数据分离存放,便于按步骤复现。代码覆盖numpy、pandas、seaborn、matplotlib等数据处理与可视化模块,并调用sklearn的MinMaxScaler、train_test_split、RandomForestRegressor与mean_squared_error完成归一化、切分、随机森林回归及误差评估,同时涉及warnings、os、datetime等辅助模块。已有67人学习,读者可借此掌握数据合并、探索性分析、模型训练与效果评估的完整流程,并参考PDF中的任务清单逐项练习,适合作为课程作业或自学项目的实操素材。
1. 员工绩效数据集分析预测:从 273.98 KB 的 Excel 到可解释的离职风险评分
很多团队第一次做员工绩效预测,都是从一个几十万字节的 Excel 开始的——273.98 KB,20 个源代码文件,字段无非是工号、部门、绩效评分、加班时长、满意度、离职标签。数据量不大,但坑一点不少:类别不平衡、特征泄漏、评分口径不统一,随便踩一个都能让模型 AUC 虚高到 0.99,上线后直接翻车。这个标题讲的不是「用 AI 预测员工绩效」这种空话,而是一套能跑通、能解释、能复现的最小闭环:把这份数据集读进来,做特征工程,训练一个可解释的模型,输出每个员工的绩效等级或离职风险分,并且知道哪些特征在真正起作用。适合两类人:一是手里有类似 HR 数据集、想快速验证可行性的数据工程师;二是需要向业务方解释「模型为什么这么判」的算法从业者。下面按数据探查、特征处理、模型训练、避坑、进阶验证五步走,代码可以直接抄。
2. 数据探查与字段口径对齐:先搞清楚 273.98 KB 里到底有什么
拿到数据集的第一件事不是read_csv,而是先确认字段含义和分布。员工绩效类数据集通常包含三类字段:身份类(工号、部门、职位)、行为类(加班时长、项目数、考勤)、结果类(绩效评分、离职标签)。273.98 KB 的体量大概对应几百到一千行、十几到二十列,属于典型的小样本表格数据。小样本最大的风险是「看起来能跑,其实在背答案」,所以探查阶段必须把每列的取值分布、缺失率、类别数全部打出来。
2.1 用 pandas 做一次完整的数据体检
import pandas as pd import numpy as np # 读取数据集,注意编码,HR 系统导出的 Excel 常见 gbk 或 utf-8-sig df = pd.read_csv("employee_performance.csv", encoding="utf-8-sig") # 基础信息:行数、列数、每列类型、非空数量 print("shape:", df.shape) print(df.info()) # 缺失率排序,优先处理缺失超过 30% 的列 missing = df.isnull().mean().sort_values(ascending=False) print(missing[missing > 0]) # 类别型字段的取值分布,重点看部门、职位、绩效等级 for col in df.select_dtypes(include="object").columns: print(f"\n{col} 取值数: {df[col].nunique()}") print(df[col].value_counts().head(10)) # 数值型字段的描述统计,关注加班时长、满意度这类可能有异常值的列 print(df.describe().T[["mean", "std", "min", "50%", "max"]])这段代码的逻辑是先看整体形状和类型,再分别处理缺失、类别分布、数值分布。参数上,encoding一定要试,HR 系统导出的文件经常是gbk,用默认utf-8会直接报UnicodeDecodeError。value_counts().head(10)是为了防止某个字段有上百个取值把输出刷屏。重点看两个信号:一是绩效等级或离职标签是否严重不平衡(比如离职只占 5%),二是加班时长、满意度这类字段有没有明显超出合理范围的异常值(比如满意度出现 0 到 1 之外的数)。
2.2 字段口径对齐:绩效评分到底是谁打的
这一步最容易被跳过,但它是后面所有工作的地基。员工绩效数据集里,「绩效评分」可能来自三种口径:自评、上级评、系统综合分。如果数据集里同时有多个评分列,必须先确认用哪个作为标签。常见做法是看列名后缀,比如performance_score_self、performance_score_manager,然后和业务方确认预测目标。如果目标是预测「最终绩效等级」,那就用综合分或上级评分;如果目标是预测「离职风险」,那绩效评分应该作为特征而不是标签。这个区分决定了后面是分类任务还是回归任务,也决定了特征里能不能放绩效评分——放了就是特征泄漏,模型准确率会虚高到没有意义。
提示:如果数据集里没有明确的标签列,只有绩效评分,可以把评分离散化成「高/中/低」三档做分类,阈值用分位数而不是固定值,避免业务口径变化导致标签漂移。
3. 特征工程与类别不平衡处理:让模型学到真信号而不是背答案
探查完之后,真正决定模型上限的是特征工程。员工绩效数据集的原始字段通常很粗糙:部门是字符串、职位是字符串、加班时长是连续值、满意度是 0 到 1 的小数。直接丢给模型不是不行,但树模型对类别编码敏感,线性模型对量纲敏感,所以这一步要做三件事:类别编码、数值分箱、不平衡处理。
3.1 类别特征编码:独热还是目标编码
部门、职位这类低基数类别(取值小于 15 个)用独热编码就够了,高基数类别(比如岗位名称有几十上百种)用目标编码或频率编码更合适。员工绩效数据集里,部门通常不超过 10 个,职位可能多一点,但也不会太夸张。
from sklearn.preprocessing import OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline # 低基数类别列,独热编码 low_card_cols = ["department", "gender", "marital_status"] # 高基数类别列,用频率编码替代,避免维度爆炸 high_card_cols = ["job_role", "education_field"] # 频率编码:用取值出现的频率替换原始类别 for col in high_card_cols: freq = df[col].value_counts(normalize=True) df[col + "_freq"] = df[col].map(freq) # 构建预处理管道 preprocessor = ColumnTransformer( transformers=[ ("onehot", OneHotEncoder(handle_unknown="ignore"), low_card_cols), ("num", "passthrough", ["age", "tenure_years", "overtime_hours", "satisfaction_score", "job_role_freq", "education_field_freq"]) ], remainder="drop" )这段代码的关键点是handle_unknown="ignore",它保证线上出现训练集没见过的部门时不会报错,而是编码成全零向量。频率编码用value_counts(normalize=True)得到的是比例而不是计数,这样不同规模的数据集之间可以复用。remainder="drop"是显式丢弃没列出的列,防止不小心把工号这种 ID 列带进模型——ID 列进模型是典型的噪声来源,树模型可能会用它做无意义的切分。
3.2 类别不平衡:不要只会 SMOTE
员工绩效数据集里,高绩效和离职标签通常是不平衡的。比如离职率 10%,高绩效占比 15%。很多人第一反应是上 SMOTE 过采样,但小样本数据上 SMOTE 容易生成不真实的合成样本,导致模型在真实数据上表现下降。更稳的做法是先用class_weight="balanced",如果效果不够再考虑欠采样或组合采样。
from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, roc_auc_score # 假设标签列是 attrition,1 表示离职 X = df.drop(columns=["employee_id", "attrition"]) y = df["attrition"] # 分层切分,保证训练集和测试集里正负样本比例一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) # 用 class_weight 处理不平衡,比 SMOTE 更保守 clf = Pipeline([ ("prep", preprocessor), ("model", RandomForestClassifier( n_estimators=300, max_depth=8, class_weight="balanced", random_state=42 )) ]) clf.fit(X_train, y_train) y_prob = clf.predict_proba(X_test)[:, 1] print("AUC:", roc_auc_score(y_test, y_prob)) print(classification_report(y_test, clf.predict(X_test)))参数上,stratify=y是必须的,否则小样本下测试集可能一个正样本都没有。max_depth=8是防止树长得太深记住训练集,小样本数据上树深超过 10 基本就开始过拟合了。class_weight="balanced"会自动按类别频率反比加权,效果通常比 SMOTE 稳定,而且没有合成样本带来的分布偏移问题。如果 AUC 在 0.75 到 0.85 之间,说明模型学到了真信号;如果超过 0.95,先检查有没有特征泄漏,尤其是绩效评分、离职日期这类字段。
3.3 特征重要性:业务方只信能解释的模型
训练完模型,业务方第一个问题一定是「哪些因素在影响绩效」。树模型自带feature_importances_,但更推荐用 SHAP 值,因为它能给出每个样本每个特征的贡献方向。
import shap # 取预处理后的特征名 feature_names = (clf.named_steps["prep"] .get_feature_names_out()) # 用树模型解释器 explainer = shap.TreeExplainer(clf.named_steps["model"]) # 对测试集做解释,注意要先 transform X_test_transformed = clf.named_steps["prep"].transform(X_test) shap_values = explainer.shap_values(X_test_transformed) # 输出全局重要性前 10 shap.summary_plot(shap_values[1], X_test_transformed, feature_names=feature_names, plot_type="bar")这段代码里shap_values[1]取的是正类(离职)的贡献值。summary_plot的plot_type="bar"输出全局重要性排序,去掉这个参数会输出蜂群图,能看到每个特征对每个样本的正负影响。实际用的时候,如果发现overtime_hours排第一且方向为正,说明加班越多离职风险越高,这个结论业务方容易接受;如果发现某个编码后的类别特征排第一,要回去检查是不是编码方式引入了虚假相关。
4. 模型训练与验证:小样本下怎么判断模型是真的能用
小样本表格数据的验证不能只看一次train_test_split的结果,因为随机切分带来的方差可能比模型之间的差异还大。正确做法是交叉验证加多指标评估,同时留一个时间维度的切分做最终验证(如果数据里有时间字段)。
4.1 交叉验证:用 StratifiedKFold 而不是 KFold
from sklearn.model_selection import StratifiedKFold, cross_val_score cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) # 用 AUC 做评估指标,因为不平衡数据下准确率没有意义 scores = cross_val_score(clf, X, y, cv=cv, scoring="roc_auc") print("AUC 均值: %.4f, 标准差: %.4f" % (scores.mean(), scores.std()))StratifiedKFold保证每一折里正负样本比例和整体一致,shuffle=True打乱顺序避免数据按某个隐藏维度排序。scoring="roc_auc"是因为不平衡数据下准确率会被多数类主导,一个全预测「不离职」的模型准确率也有 90%,但 AUC 只有 0.5。标准差很重要,如果标准差超过 0.05,说明模型在不同折之间表现不稳定,需要检查特征或增加数据。
4.2 时间维度验证:如果有入职日期或评估日期
如果数据集里有时间字段,比如hire_date或review_date,一定要做一次时间切分验证:用早期数据训练,用后期数据测试。这是最接近线上真实场景的验证方式。
# 假设有 review_year 字段 train_mask = df["review_year"] < 2023 test_mask = df["review_year"] >= 2023 X_train_t, y_train_t = X[train_mask], y[train_mask] X_test_t, y_test_t = X[test_mask], y[test_mask] clf.fit(X_train_t, y_train_t) y_prob_t = clf.predict_proba(X_test_t)[:, 1] print("时间切分 AUC:", roc_auc_score(y_test_t, y_prob_t))时间切分的结果通常会比随机切分低 0.05 到 0.15,这是正常的,因为线上数据分布会漂移。如果时间切分 AUC 掉到 0.6 以下,说明模型学到的规律不稳定,需要重新审视特征工程,尤其是那些和时间强相关的特征(比如工龄、司龄)。
4.3 模型对比:不要只试一个算法
小样本数据上,逻辑回归、随机森林、梯度提升树的表现差异可能不大,但可解释性和训练速度差异明显。建议至少对比三个模型,用同一套交叉验证。
| 模型 | 优点 | 缺点 | 小样本建议 |
|---|---|---|---|
| 逻辑回归 | 可解释性强,训练快 | 对非线性关系拟合弱 | 先跑 baseline |
| 随机森林 | 抗过拟合,特征重要性直观 | 小样本下方差大 | 限制树深 |
| XGBoost/LightGBM | 精度通常最高 | 参数多,容易过拟合 | 强正则化 |
实际项目中,我一般先用逻辑回归跑一个 baseline,如果 AUC 已经到 0.75 以上,说明特征工程做到位了;再用随机森林或 LightGBM 提升 3 到 5 个点。如果逻辑回归只有 0.6,换复杂模型也救不回来,问题在特征不在模型。
5. 避坑与排查:员工绩效预测里最容易翻车的 5 个地方
这一章是血泪经验合集。员工绩效数据集看起来简单,但每个字段背后都有业务含义,不理解业务直接跑模型,结果就是「离线指标漂亮,上线没人用」。
5.1 现象:AUC 高到 0.98,业务方不信
原因:特征泄漏。最常见的是把绩效评分、离职日期、离职面谈记录放进了特征。绩效评分如果是标签,就不能同时做特征;离职日期如果已知,那模型只是在背答案。
解决:训练前做一次特征审计,把所有和标签在时间上重叠的字段列出来,逐个确认是否在预测时点可得。预测离职风险时,只能用员工当前和历史的行为数据,不能用未来的结果数据。
5.2 现象:模型在测试集上很好,换一批数据就崩
原因:过拟合加分布漂移。小样本数据上,模型很容易记住训练集的噪声;同时不同部门的绩效评分口径可能不同,训练集里某个部门的样本多,模型就偏向那个部门的规律。
解决:做部门维度的分组交叉验证,用GroupKFold按部门分组,确保每个部门都在测试集里出现过。如果某个部门的 AUC 明显低于其他部门,说明模型对该部门不公平,需要单独建模或增加该部门的样本权重。
5.3 现象:特征重要性里工号排第一
原因:工号被当成了数值特征。工号是随机分配的,和绩效没有因果关系,但如果它恰好和某个隐藏变量相关(比如入职顺序),树模型就会用它做切分。
解决:训练前显式丢弃所有 ID 类字段,包括工号、员工编号、记录 ID。在ColumnTransformer里用remainder="drop"或者显式指定drop列。
5.4 现象:加班时长和绩效的关系时正时负
原因:多重共线性或交互效应。加班时长单独看可能和绩效正相关(加班多的人产出多),但和满意度一起看又负相关(加班多的人满意度低,绩效反而下降)。模型在不同折里学到不同的关系。
解决:检查特征之间的相关性矩阵,相关系数超过 0.7 的成对特征只保留一个,或者用 PCA 降维。更简单的做法是构造交互特征,比如overtime_hours * satisfaction_score,让模型显式学到这种关系。
5.5 现象:模型预测全是「不离职」
原因:类别极度不平衡加阈值未调整。如果离职率只有 5%,模型默认阈值 0.5 会导致所有样本都被判为负类。
解决:不要用默认阈值,用验证集上的 F1 或召回率来选阈值。或者直接用predict_proba输出概率,让业务方按概率排序处理,而不是二分类。
注意:员工绩效数据涉及个人隐私,做分析和建模时务必脱敏,工号、姓名、身份证号等字段在进入模型前就要替换成匿名 ID,输出结果只保留聚合结论,不要落到个人。
6. 进阶技巧:用 SHAP 单样本解释做绩效面谈辅助
模型跑通之后,真正有价值的不是那个 AUC 数字,而是能不能对每个员工给出「为什么他绩效高/低」的解释。SHAP 的单样本解释可以做到这一点:对某个员工,输出每个特征对他预测结果的贡献值,正贡献表示拉高绩效,负贡献表示拉低绩效。这个能力可以直接用在绩效面谈里,让管理者有数据支撑地沟通,而不是凭感觉。
# 对测试集里第 5 个员工做单样本解释 idx = 5 single_shap = explainer.shap_values(X_test_transformed[idx:idx+1]) # 输出每个特征的贡献值,按绝对值排序 contributions = pd.DataFrame({ "feature": feature_names, "shap_value": single_shap[1][0] }) contributions["abs_shap"] = contributions["shap_value"].abs() contributions = contributions.sort_values("abs_shap", ascending=False) print(contributions.head(10))这段代码输出的是该员工每个特征对离职风险的贡献。shap_value为正表示增加离职风险,为负表示降低。实际用的时候,我会把feature_names映射回业务可读的名称,比如overtime_hours显示成「月均加班时长」,然后取绝对值前 5 个特征生成一段话:「该员工离职风险主要受加班时长(+0.12)和满意度(-0.08)影响,建议关注工作负荷。」这种解释比一个冷冰冰的概率值有用得多。
验证解释是否靠谱的方法很简单:找几个已知结果的员工,看 SHAP 解释是否符合业务直觉。如果模型说「加班多导致离职风险高」,但该员工实际是主动离职去创业,那解释就是错的,需要检查特征或模型。我一般会抽 10 个样本人工核对,准确率超过 7 个才敢把解释交给业务方。
还有一个实用技巧是「反事实解释」:对某个高风险员工,找到需要改变哪些特征才能把风险降到阈值以下。比如「如果满意度从 0.4 提升到 0.7,离职风险从 0.65 降到 0.35」。这个用 SHAP 的force_plot或者简单的特征扰动都能做,但要注意反事实解释只能作为参考,不能承诺「改了就一定不走」。
最后说一个我自己的习惯:每次做完员工绩效预测,我都会把模型的特征重要性排序和业务方对齐一次,问他们「这个排序符合你们的直觉吗」。如果业务方说「加班时长不可能排第一」,那要么是数据有问题,要么是业务理解有偏差,两种都值得深挖。模型不是用来替代判断的,是用来暴露那些被忽略的信号的。希望帮到你。
本文还有配套的精品资源,点击获取