基于机器学习进行教师离职后的接任教师匹配度量化分析:一种两阶段残差建模方法
教师离职后的接任教师匹配度量化分析:一种两阶段残差建模方法
业务场景:某班某科任课老师离职,需要从当前在岗、教同一学科的其他老师中挑选一位接任。本文用机器学习量化预测**「每位候选老师如果来教这个班,这一科的班级平均 Z‑score 能提升多少」**,按预测的“教师贡献”给出推荐顺序。
一、核心挑战:如何剥离班级生源效应?
直接预测班级平均 Z‑score(mean_z)会导致模型主要学到“班级生源好坏”——优等生班的成绩高低更多由学生自身决定,难以分离教师的真实贡献。
为了纯粹衡量教师能力,我们采用两阶段残差建模:
- 阶段1:
班级 mean_z = f(班级画像) + ε
用班级画像(生源结构)预测班级 mean_z,得到**“如果是一位平均水平老师”**的基线。 - 阶段2:
ε (残差) = g(教师特征) + δ
用教师特征预测残差(实际 mean_z − 基线),这才是真正的教师贡献度。
最终,候选教师的推荐预测 = 阶段1 基线 + 阶段2 教师贡献。
二、数据与特征工程
2.1 数据来源
5_chengji.csv:历次考试成绩(含 Z‑score、等第)1_teacher.csv:教师任课记录2_student_info.csv:学生基本信息3_kaoqin.csv、7_consumption.csv:考勤与消费数据(用于构建学生画像)
2.2 学生画像特征工程
profile_with_class = profile.merge(stu[['bf_StudentID','cla_Name']], on='bf_StudentID', how='left')
def parse_grade(c):
if pd.isna(c): return '未知'
if '高一' in c: return '高一'
if '高二' in c: return '高二'
if '高三' in c: return '高三'
return '其他'
profile_with_class['grade'] = profile_with_class['cla_Name'].apply(parse_grade)
fig, ax = plt.subplots(figsize=(10, 7))
colors = {'高一':'#3498db','高二':'#2ecc71','高三':'#e74c3c','其他':'#95a5a6','未知':'#bdc3c7'}
for g, c in colors.items():
sub = profile_with_class[profile_with_class['grade']==g]
if len(sub) == 0: continue
ax.scatter(sub['pc1'], sub['pc2'], s=18, alpha=0.55, c=c,
label=f'{g} (n={len(sub)})', edgecolor='none')
ax.set_xlabel(f'PC1 ({pca.explained_variance_ratio_[0]*100:.1f}%)')
ax.set_ylabel(f'PC2 ({pca.explained_variance_ratio_[1]*100:.1f}%)')
ax.set_title('学生画像 PCA 二维投影', fontsize=14)
ax.legend(); ax.grid(alpha=0.2)
plt.tight_layout()
plt.savefig(os.path.join(OUT_DIR, 'fig1_student_pca.png'), dpi=130)
plt.show()
- 各学科
avg_z、std_z(10 科共 20 维) - 性别、住宿、城镇/农村(3 维)
- 考勤分类统计(
迟到、旷课等,共 12 维) - 消费总金额、均值、标准差、次数(4 维)
对在校 1,557 名学生,PCA 降维至 8 个主成分(累计方差 74.3%),后续班级画像使用班级内各主成分的均值和标准差。
学生画像 PCA 二维投影(图1)

2.3. 学生分群
sub_for_radar = ['语文','数学','英语','物理','化学','生物','历史','地理','政治']
groups = profile['cluster_label'].unique()
fig, axes = plt.subplots(1, len(groups),
figsize=(4.2*len(groups), 4.5),
subplot_kw=dict(polar=True))
if not isinstance(axes, np.ndarray): axes = [axes]
colors_g = ['#e74c3c','#3498db','#2ecc71','#f39c12','#9b59b6']
for ax, (label, gp), c in zip(axes, profile.groupby('cluster_label'), colors_g):
sub_used = [s for s in sub_for_radar if f'avgz_{s}' in gp.columns]
cols_exist = [f'avgz_{s}' for s in sub_used]
ang = np.linspace(0, 2*np.pi, len(sub_used), endpoint=False).tolist()
vals = gp[cols_exist].mean().tolist()
ang += ang[:1]; vals += vals[:1]
ax.plot(ang, vals, 'o-', linewidth=2, color=c)
ax.fill(ang, vals, alpha=0.25, color=c)
ax.set_xticks(ang[:-1]); ax.set_xticklabels(sub_used, fontsize=9)
ax.set_title(f'{label} (n={len(gp)})', y=1.1, fontsize=11)
ax.set_ylim(-1.5, 1.5); ax.grid(True, alpha=0.4)
plt.suptitle('各学生群体的学科能力画像', fontsize=14, y=1.05)
plt.tight_layout()
plt.savefig(os.path.join(OUT_DIR, 'fig2_cluster_radar.png'), dpi=130, bbox_inches='tight')
plt.show()
各群学科能力雷达(图2)

:
2.4 班级画像(24维)
# 班级×学科 mean_z
cur_term = '2018-2019-1'
valid_cur = valid[valid['exam_term'] == cur_term]
valid_cur = valid_cur.merge(stu[['bf_StudentID','cla_id','cla_Name']],
left_on='mes_StudentID', right_on='bf_StudentID', how='inner')
cls_subj_z = (valid_cur.groupby(['cla_id','cla_Name','mes_sub_name'])
['mes_Z_Score'].mean().reset_index()
.rename(columns={'mes_Z_Score':'mean_z_after','mes_sub_name':'sub_Name'}))
print(f"班级×学科 mean_z 样本: {len(cls_subj_z)}")
# 关联当前学期教师
teacher_cur = teacher[teacher['term'] == cur_term].copy()
samples = cls_subj_z.merge(teacher_cur[['cla_id','sub_Name','bas_id','bas_Name']],
on=['cla_id','sub_Name'], how='inner')
print(f"训练样本: {len(samples)}")
- 班级内学生 PC1~PC8 的均值、标准差(16 维)
- 班级内优等生、中等生、偏文科、偏理科、后进生比例(5 维)
- 班级人数(1 维)
2.4 教师特征 —— 关键改进
# 拼到 df_stage1。注意:只对教师历史类列填 0,t_concurrent_z 保留 NaN
df_stage1 = df_stage1.merge(teacher_exp, on=['bas_id','sub_Name'], how='left')
hist_cols = [c for c in df_stage1.columns
if c.startswith('t_n_class_hist') or c.startswith('t_n_term') or c.startswith('t_pct_grade_')]
df_stage1[hist_cols] = df_stage1[hist_cols].fillna(0)
teacher_feat_cols = ['t_concurrent_z','t_concurrent_z_std','t_n_class_cur',
't_n_class_hist','t_n_term'] + [c for c in df_stage1.columns if c.startswith('t_pct_grade_')]
print(f"教师特征 {len(teacher_feat_cols)} 个: {teacher_feat_cols}")
# 排除 t_concurrent_z 缺失(教师只教1个班 → 没有同期其他班)的样本
df_stage2 = df_stage1.dropna(subset=['t_concurrent_z']).copy()
print(f"\n阶段2 样本(教师至少教2个班): {len(df_stage2)} / {len(df_stage1)}")
print(f"被排除样本(教师只教1个班): {len(df_stage1) - len(df_stage2)}")
为了避免“用 y 预测 y”的数据泄漏,我们采用 leave‑one‑out(LOO) 方式构造教师水平特征:
t_concurrent_z:教师当前学期所其他班(排除目标班)该学科的mean_z均值 → 教师能力的直接观测t_concurrent_z_std:同上,标准差 → 稳定性t_n_class_cur:当前学期该教师任课班数- 历史经验:历史任教学期数(
t_n_term)、历史任教班数(t_n_class_hist)、各年级任教占比(t_pct_grade_*)
反泄漏保证:计算
t_concurrent_z时严格排除了当前样本所在班级,确保特征与目标值独立。
教师"贡献度"(剥离班级效应后的残差)分布(图3)

三、阶段1:班级基线模型
3.1 模型设定
- 训练数据:2018‑2019‑1 学期 247 条(班级 × 学科)
- 特征:24 维班级画像
- 算法:
RandomForestRegressor(5 折交叉验证,out‑of‑fold 预测)
3.2 结果
- OOF R² = 0.724:班级画像解释了 72.4% 的班级成绩差异 → 生源效应占主导
- 残差分布:均值 ≈ 0,标准差 0.238,范围 [-0.81, 0.79] → 教师可改变的空间有限但存在
# 核心代码(示意)
kf = KFold(n_splits=5, shuffle=True)
for tr, vl in kf.split(X1):
model = RandomForestRegressor(n_estimators=200, max_depth=6)
model.fit(X1[tr], y1[tr])
oof_baseline[vl] = model.predict(X1[vl])
residual = y1 - oof_baseline
残差即为剥离班级效应后的教师贡献度(见图3左)。
四、阶段2:教师能力模型
用教师特征预测残差。样本限制:教师必须至少教 2 个班(否则无法计算 t_concurrent_z),最终得到 200 条样本。教师特征共 9 维。
X2 = df_stage2[teacher_feat_cols].values
y2 = df_stage2['residual'].values
X2_tr, X2_te, y2_tr, y2_te = train_test_split(X2, y2, test_size=0.25, random_state=42)
print(f"阶段2 训练 {X2_tr.shape}, 测试 {X2_te.shape}")
stage2_models = {
'随机森林': RandomForestRegressor(n_estimators=200, max_depth=6, min_samples_leaf=3,
random_state=42, n_jobs=-1),
'梯度提升': GradientBoostingRegressor(n_estimators=200, max_depth=3, learning_rate=0.05,
random_state=42),
}
4.1 模型对比
同时训练随机森林和梯度提升,用测试集(25%)评估。
| 模型 | 测试 R² | MAE | 5折CV R² |
|---|---|---|---|
| 随机森林 | -0.281 | 0.191 | -0.036±0.153 |
| 梯度提升 | -0.383 | 0.203 | -0.241±0.373 |
负 R² 是预期的:因为班级效应已被剥离,剩下的教师贡献信号本身较弱(残差标准差仅 0.24),且样本量 200、特征 9 维,模型难以精确拟合。但特征重要性和相关性分析显示方向性有效。
两模型 残差预测 vs 真实残差(图4):
fig, axes = plt.subplots(1, 2, figsize=(13, 5.5))
for ax, (name, res) in zip(axes, stage2_results.items()):
ax.scatter(y2_te, res['pred_test'], alpha=0.55, s=40, color='steelblue', edgecolor='black', linewidth=0.4)
lo = min(y2_te.min(), res['pred_test'].min())
hi = max(y2_te.max(), res['pred_test'].max())
ax.plot([lo, hi], [lo, hi], 'r--', alpha=0.6, label='完美预测')
ax.axhline(0, color='gray', linestyle=':', alpha=0.5)
ax.axvline(0, color='gray', linestyle=':', alpha=0.5)
ax.set_xlabel('真实残差(教师贡献)')
ax.set_ylabel('预测残差')
ax.set_title(f"{name}\nR²={res['r2_test']:.3f}, MAE={res['mae_test']:.3f}",
fontsize=12)
ax.grid(alpha=0.2); ax.legend()
plt.suptitle('阶段2: 教师能力模型 - 残差预测', fontsize=14, y=1.02)
plt.tight_layout()
plt.savefig(os.path.join(OUT_DIR, 'fig4_residual_pred.png'), dpi=130, bbox_inches='tight')
plt.show()

4.2 发现:t_concurrent_z 与残差显著相关
- 两者 Pearson 相关系数 r = 0.192 (p = 0.006)
- 说明教师在其他班的平均表现越好,他在新班级的贡献也倾向于越高 → 排名推荐有参考价值
4.3 特征重要性(图5)

无论随机森林还是梯度提升,t_concurrent_z 的重要性≈0.5,远超其他特征;t_concurrent_z_std 次之;历史经验类合计约占 0.3。这印证了残差建模的有效性——班级效应剥离后,教师当期水平特征成为主导信号。
五、应用案例:高三(05) 数学老师离职后的接任推荐
5.1 背景
- 目标班级:高三(05)
- 学科:数学
- 该班级生源构成:中等生 47.7%,后进生 52.3%,无优等生 → 生源较弱
5.2 预测分解
- 阶段1 班级基线:-0.188(若由平均水平老师任教,班级 mean_z 约 -0.19)
- 当前实际 mean_z:-0.257 → 现任老师的贡献 = -0.069(略低于基线)
最终预测 = 阶段1班级基线 + 阶段2教师贡献
阶段1: 班级基线
target_X1 = target_cls_row[class_pc_cols].values
class_baseline = float(stage1_model.predict(target_X1)[0])
print(f"\n阶段1班级基线预测 (中性老师下该班 mean_z): {class_baseline:.3f}")
# 当前实际 mean_z
cur_baseline_row = cls_subj_z[(cls_subj_z['cla_Name']==TARGET_CLASS) &
(cls_subj_z['sub_Name']==TARGET_SUB)]
cur_baseline = float(cur_baseline_row['mean_z_after'].iloc[0]) if len(cur_baseline_row) else 0.0
print(f"该班该学科 当前实际 mean_z: {cur_baseline:.3f}")
print(f" → 当前 = 基线 {class_baseline:+.3f} + 现任老师贡献 {cur_baseline-class_baseline:+.3f}")
阶段2: 用两个模型预测教师贡献
cand_X2 = candidates[teacher_feat_cols].values
candidates['contrib_rf'] = stage2_results['随机森林']['model'].predict(cand_X2)
candidates['contrib_gb'] = stage2_results['梯度提升']['model'].predict(cand_X2)
candidates['contrib_avg'] = (candidates['contrib_rf'] + candidates['contrib_gb']) / 2
# 总预测 = 班级基线 + 教师贡献
candidates['pred_total'] = class_baseline + candidates['contrib_avg']
candidates['vs_current'] = candidates['pred_total'] - cur_baseline
ranked = candidates.sort_values('pred_total', ascending=False).reset_index(drop=True)
ranked['rank'] = range(1, len(ranked)+1)
print(f"\n=== {TARGET_CLASS} {TARGET_SUB} 接任候选排名 ===")
print(f"班级基线: {class_baseline:.3f}, 当前实际: {cur_baseline:.3f}\n")
print(ranked[['rank','bas_Name','bas_id','t_concurrent_z','t_n_class_cur',
't_n_class_hist','contrib_rf','contrib_gb','contrib_avg',
'pred_total','vs_current']].head(10).to_string(index=False))
总预测 = 班级基线(-0.188) + 教师贡献。排名靠前的教师,其
t_concurrent_z多为正值,与贡献预测方向一致。
5.3 候选教师排名
从该学科 23 位候选教师中,用两个模型的平均贡献预测,得到排名前 10 的部分结果:
5.4 可视化推荐(图6)

堆叠条形图将推荐拆为两层:
- 灰色部分:班级基线(不变)
- 绿色/红色部分:候选教师的边际贡献(绿色为正,红色为负)
- 蓝色虚线:当前实际值(作为参照)
教务主任可以直观看到:“这位候选教师比平均水平好 0.11,比现任教师好 0.18”。
六、六张核心图表解读
| 图号 | 名称 | 关键结论 |
|---|---|---|
| 图1 | 学生画像 PCA 二维投影 | 高一、高二、高三学生在前两个主成分上呈一定分离,反映年级成长差异 |
| 图2 | 各学生群体的学科能力雷达图 | 优等生全科突出,偏理科生数理强而文史弱,后进生全科偏低(本数据无显著偏文科群) |
| 图3 | 教师贡献度分布 + 相关散点 | 残差分布对称;t_concurrent_z 与残差正相关(r=0.192) |
| 图4 | 两模型残差预测 vs 真实残差 | 散点图显示预测值与真实值线性关系弱(R²为负),但方向性可参考 |
| 图5 | 教师特征重要性 | t_concurrent_z 主导(~0.5),稳定性次之,历史经验合计约 0.3 |
| 图6 | 候选推荐排名分解 | 直观对比基线、现任、每位候选教师的预测总成绩及提升幅度 |
七、结论与业务价值
7.1 模型能做什么?
- 量化生源效应与教师效应:班级成绩差异中,约 72% 由生源决定,教师贡献占剩余部分。
- 提供方向可靠的排名:虽然教师贡献的绝对预测精度有限(R² 为负),但
t_concurrent_z与残差的显著相关保证了排名顺序可信。 - 可解释性强:推荐结果拆解为“这个班的基础值 + 这位老师的增值”,便于业务理解。
7.2 局限性
- 样本量有限:仅一个学期 247 条(班级×学科),教师特征 9 维,不足以训练高精度模型。
- 教师水平特征依赖同期数据:
t_concurrent_z是新任教师才有可能获取的指标,无法评估从未在本校教过该学科的外聘教师。 - 未考虑教学风格、学生适配等软因素:模型仅基于过往成绩数据,实际选聘还需结合面试、学生反馈等。
- 残差预测 R² 为负:说明模型无法可靠预测具体的贡献数值,推荐时应仅用作排序参考,不宜直接采信预测绝对值。
7.3 改进方向
- 积累多个学期的数据,扩大训练样本量。
- 引入教师背景特征(教龄、职称、获奖等)和课堂观察指标。
- 尝试更复杂的模型(如带正则化的线性模型、XGBoost)并采用贝叶斯方法估计不确定性。
八、代码说明
环境依赖
pandas numpy matplotlib seaborn scikit-learn scipy
文件结构
data/
├── 5_chengji.csv
├── 1_teacher.csv
├── 2_student_info.csv
├── 3_kaoqin.csv
├── 7_consumption.csv
└── out_topic2/ # 输出图片和推荐CSV
更多推荐


所有评论(0)