教师离职后的接任教师匹配度量化分析:一种两阶段残差建模方法

业务场景:某班某科任课老师离职,需要从当前在岗、教同一学科的其他老师中挑选一位接任。本文用机器学习量化预测**「每位候选老师如果来教这个班,这一科的班级平均 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.csv7_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_zstd_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 局限性

  1. 样本量有限:仅一个学期 247 条(班级×学科),教师特征 9 维,不足以训练高精度模型。
  2. 教师水平特征依赖同期数据t_concurrent_z 是新任教师才有可能获取的指标,无法评估从未在本校教过该学科的外聘教师。
  3. 未考虑教学风格、学生适配等软因素:模型仅基于过往成绩数据,实际选聘还需结合面试、学生反馈等。
  4. 残差预测 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
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐