基于聚类分析与大语言模型的学生画像构建及个性化推荐研究
研究目标
随着教育数字化建设持续落地,各校沉淀了海量多源异构学生行为数据,涵盖考试分数、在校考勤、校园消费流水等多类信息。但如何从繁杂原始数据中挖掘有效学生特征,搭建兼具可读性与落地性的可解释学生画像,仍是当前教育数据分析领域的难点。
传统画像搭建手段高度依赖行业专家经验与固定规则引擎,依靠硬编码分箱阈值划分学生层级。该方案实现简单、可控性强,但存在标签粒度粗糙、缺少深层语义解读、无法挖掘多维度数据内在关联等固有短板。大语言模型(LLM)具备强大自然语言理解与文本生成能力,为学生智能画像建设提供全新技术思路。
本文核心研究目标:搭建一套融合无监督聚类算法、大语言模型、向量检索技术的一体化学生画像构建与个性化学习推荐方案,打通数据挖掘分析 — 语义画像解读 — 智能干预推荐全业务闭环。
研究问题
- 群体分层挖掘:在无人工预设标签前提下,K-Means 聚类能否依托多维度教育数据自动挖掘真实有区分度的学生群体结构?对比人工规则分箱,聚类算法是否能够挖掘更细粒度、多维度交叉的学生群体差异特征?
- 动态成长画像:学生学业时序演变特征(稳步上升、持续下滑、大幅波动)能否弥补静态画像缺失的成长维度?Mann-Kendall 趋势检验方法适配教育成绩时序数据的可行性如何?
- 混合智能推荐:将 SVD 矩阵分解、基于物品协同过滤、LLM 向量语义检索融合形成混合推荐策略,是否能够同时在预测准确度、推荐结果可解释性两项指标上优于传统单一推荐算法?
数据来源
实验采用公开 “数智教育数据集”,包含 7 张结构化数据表:教师基础信息表、学生档案表、考勤记录表、考勤类型字典表、考试成绩明细表、考试类型字典表、校园消费流水表,覆盖数千名在校学生全周期多维度行为记录。
技术路线
整体采用三阶段分层递进式实验架构,各阶段任务依次推进:
- 第一阶段(基础画像构建):原始数据清洗处理 → 多维特征工程提取 → PCA 特征降维压缩 → K-Means 无监督聚类 → 学生群体粗画像生成
- 第二阶段(LLM 语义增强):学业时序趋势量化分析 → 传统规则画像与 LLM 智能画像对照实验 → TF-IDF 文本语义量化评估
- 第三阶段(个性化推荐落地):Item-based 协同过滤建模 → SVD 矩阵分解预测 → RAG 检索增强混合推荐 → 多维度指标量化效果评估
章节导航
全文遵循数据预处理→特征构建→聚类分群→时序动态分析→画像对比实验→混合推荐建模→模型效果评估逻辑脉络划分八大章节,各章节核心内容整理如下:
| 章节 | 主题 | 核心方法 | 主要产出 |
|---|---|---|---|
| 一 | 数据工程与数据质量校验 | 缺失值统计、数据分布正态性检验 | 完整数据集质量体检报告 |
| 二 | 多维特征构建与降维处理 | PCA 主成分降维变换 | 去相关低维标准化特征空间 |
| 三 | 基于聚类的学生群体分层 | K-Means、肘部法则、轮廓系数评估 | 分群标签 + 群体特征画像 |
| 四 | 学生学业时序轨迹挖掘 | Mann-Kendall 趋势检验、层级迁移矩阵 | 学业趋势标签、学生层级流动规律 |
| 五 | 规则画像与 LLM 画像对照实验 | TF-IDF 语义量化、t-SNE 文本降维可视化 | 两类画像量化对比分析结论 |
| 六 | 多算法融合个性化推荐系统 | Item-CF、SVD 矩阵分解、RAG 检索增强 | 融合型智能推荐引擎 |
| 七 | 推荐模型多维度量化评测 | NDCG 排序指标、多样性指数、RMSE 误差 | 多维度模型效果评估报告 |
| 八 | 研究总结与未来拓展方向 | — | 核心实验结论、后续优化思路 |
一、数据工程与质量评估
数据分析工作的前置核心步骤为数据摸底与质量校验。本章整体工作内容分为四大模块:环境依赖部署、数据集批量读取、全表数据质量诊断、脏数据标记,为后续清洗与特征提取提供依据。
- 实验环境部署:批量导入数值计算、机器学习、自然语言处理相关 Python 工具库;配置图表中文渲染字体避免乱码;完成通义千问大模型 API 客户端初始化。
- 数据集加载:读取 “数智教育数据集” 内 7 份 CSV 数据表,统一完成数据表字段对齐与基础格式转换。
- 数据质量全量诊断:逐表统计数据体量、字段缺失占比、数值分布特征,识别异常脏数据(成绩异常负值、Z-Score 大面积缺失等),输出完整数据问题清单。
原始数据质量直接决定后续聚类、推荐模型输出结果可信度,若数据集存在大规模缺失、异常值,后续全部分析结论都会失去参考价值,因此正式开展特征工程前,需要对全部数据表完成全面数据体检
1.1 环境搭建与依赖加载
加载科学计算、机器学习、自然语言处理等方向的Python依赖库,配置中文字体以支持图表的正确显示。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
import seaborn as sns
import warnings, os, json, time, re
from collections import Counter, defaultdict
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score, silhouette_samples
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.manifold import TSNE
from pymannkendall import original_test as mannkendall
from scipy.sparse.linalg import svds
import chromadb
warnings.filterwarnings('ignore')
try:
get_ipython().run_line_magic('matplotlib', 'inline')
except:
plt.switch_backend('Agg')
font_file = './SimHei.ttf'
if os.path.exists(font_file):
zh_font = fm.FontProperties(fname=font_file)
plt.rcParams['font.sans-serif'] = ['SimHei']
fm.fontManager.addfont(font_file)
print(f'字体加载成功: {font_file}')
else:
zh_font = None
print(f'缺少字体文件: {font_file}')
sns.set_theme(style='whitegrid', palette='muted')
plt.rcParams.update({
'axes.unicode_minus': False,
'figure.dpi': 130,
'figure.figsize': [11, 6],
'font.size': 10
})
print('环境初始化完成')
运行输出:
字体加载成功: ./SimHei.ttf
环境初始化完成
1.2 配置LLM API
初始化通义千问API客户端,用于后续的智能画像生成和RAG推荐报告生成。如果未配置API Key,LLM模块将使用降级模式。
from openai import OpenAI
DASHSCOPE_KEY = os.environ.get('DASHSCOPE_API_KEY', '')
if DASHSCOPE_KEY:
llm_client = OpenAI(
api_key=DASHSCOPE_KEY,
base_url='https://dashscope.aliyuncs.com/compatible-mode/v1'
)
MODEL_ID = 'qwen3.7-max'
print(f'LLM就绪: {MODEL_ID}')
else:
llm_client = None
print('[警告] 未配置API Key, LLM模块将使用降级模式')
运行结果:
LLM就绪: qwen3.7-max
1.3 加载数据
从"数智教育数据集"中读取7张CSV数据表,包括教师表、学生档案表、考勤记录表、考勤类型字典表、考试成绩表、考试类型字典表和消费流水表。
DATA_DIR = './数智教育数据集'
df_teacher = pd.read_csv(f'{DATA_DIR}/1_teacher.csv')
df_student = pd.read_csv(f'{DATA_DIR}/2_student_info.csv')
df_kaoqin = pd.read_csv(f'{DATA_DIR}/3_kaoqin.csv')
df_kq_type = pd.read_csv(f'{DATA_DIR}/4_kaoqintype.csv', encoding='gbk', sep='\t')
df_chengji = pd.read_csv(f'{DATA_DIR}/5_chengji.csv')
df_exam = pd.read_csv(f'{DATA_DIR}/6_exam_type.csv')
df_consume = pd.read_csv(f'{DATA_DIR}/7_consumption.csv')
data_tables = {
'教师表': df_teacher, '学生档案': df_student,
'考勤记录': df_kaoqin, '考勤类型': df_kq_type,
'考试成绩': df_chengji, '考试类型': df_exam,
'消费流水': df_consume
}
print(f'{"数据表":>8s} | {"行数":>8s} | {"列数":>4s}')
print('-' * 30)
for tbl_name, tbl_df in data_tables.items():
print(f'{tbl_name:>8s} | {tbl_df.shape[0]:>8,d} | {tbl_df.shape[1]:>4d}')
print('\n=== 缺失率概览 ===')
for tbl_name, tbl_df in data_tables.items():
miss = tbl_df.isnull().mean().mean() * 100
if miss > 0.5:
print(f' {tbl_name}: 整体缺失 {miss:.1f}%')
运行输出:
数据表 | 行数 | 列数
------------------------------
教师表 | 3,088 | 8
学生档案 | 1,765 | 14
考勤记录 | 23,630 | 10
考勤类型 | 15 | 4
考试成绩 | 471,686 | 13
考试类型 | 21 | 2
消费流水 | 463,904 | 5
=== 缺失率概览 ===
学生档案: 整体缺失 16.4%
考试成绩: 整体缺失 11.4%
1.4 数据质量诊断
对各数据表的规模、缺失率、字段分布进行系统性检查,识别数据质量问题(如成绩字段的异常值、Z-Score的大面积缺失等),为后续的数据清洗提供依据。数据质量是画像构建可靠性的基础。
# 数据质量深入诊断
df_chengji['score'] = pd.to_numeric(df_chengji['mes_Score'], errors='coerce')
df_chengji.loc[df_chengji['score'] < 0, 'score'] = np.nan
df_chengji['z_val'] = pd.to_numeric(df_chengji['mes_Z_Score'], errors='coerce')
df_chengji['t_val'] = pd.to_numeric(df_chengji['mes_T_Score'], errors='coerce')
score_valid = df_chengji['score'].notna().sum()
score_invalid = df_chengji['score'].isna().sum()
df_consume['amount'] = df_consume['MonDeal'].abs()
df_consume['ts'] = pd.to_datetime(df_consume['DealTime'])
df_kaoqin['dt'] = pd.to_datetime(df_kaoqin['DataDateTime'], errors='coerce')
print(f'成绩: 有效{score_valid:,} / 缺失{score_invalid:,} (缺失率{score_invalid/(score_valid+score_invalid)*100:.1f}%)')
print(f'消费: {len(df_consume):,}条, 均值{df_consume["amount"].mean():.2f}元')
print(f'考勤: {len(df_kaoqin):,}条')
fig, axes = plt.subplots(1, 2, figsize=(15, 5))
ax = axes[0]
col_miss = df_chengji.isnull().mean().sort_values(ascending=False)
col_miss = col_miss[col_miss > 0]
if len(col_miss) > 0:
col_miss.plot.barh(ax=ax, color='#e15759', edgecolor='white')
ax.set_xlabel('缺失比例', fontproperties=zh_font)
ax.set_title('成绩表各字段缺失率', fontproperties=zh_font, fontsize=13)
ax.set_yticklabels(ax.get_yticklabels(), fontproperties=zh_font)
ax = axes[1]
valid_scores = df_chengji['score'].dropna()
ax.hist(valid_scores, bins=50, color='#4e79a7', edgecolor='white', alpha=0.85)
ax.axvline(valid_scores.mean(), color='red', ls='--', lw=1.5, label=f'均值={valid_scores.mean():.1f}')
ax.axvline(valid_scores.median(), color='orange', ls='--', lw=1.5, label=f'中位数={valid_scores.median():.1f}')
ax.set_xlabel('分数', fontproperties=zh_font)
ax.set_ylabel('频次', fontproperties=zh_font)
ax.set_title('全体成绩分布', fontproperties=zh_font, fontsize=13)
ax.legend(prop=zh_font)
plt.tight_layout()
os.makedirs('./output', exist_ok=True)
plt.savefig('./output/ch1_quality.png', bbox_inches='tight')
plt.show()
运行输出:
成绩: 有效422,705 / 缺失48,981 (缺失率10.4%)
消费: 463,904条, 均值8.37元
考勤: 23,630条

1.5 数据质量小结
综合诊断结果可得到三点关键结论:第一,成绩表mes_T_Score字段缺失占比接近 40%,后续特征构建阶段舍弃该字段,改用完整度更高的原始分数、Z 标准化分数参与计算;第二,全体学生分数轻微左偏,均值与中位数差距较小,不存在极端系统性数据偏差;第三,校园消费流水数据体量充足、时间字段完整,可稳定支撑学生消费行为特征提取。
以上数据缺陷直接确定特征工程处理方案,高缺失字段不纳入特征矩阵,规避无效特征干扰模型效果。
二、特征空间构建与降维
完成原始数据质量校验后,下一步将多表分散业务数据整合,统一构建以单名学生为粒度的标准化特征向量。本章从四大维度提取量化指标,再通过 PCA 主成分降维消除特征多重共线性,输出低维无相关特征集供给后续聚类算法使用。
四大维度特征提取逻辑
- 学业表现维度:聚合每名学生历次考试平均分、分数标准差、极值、考试参与次数、Z 分数均值,引入变异系数(标准差 / 平均分)量化成绩稳定程度;
- 学科偏好维度:生成学生 - 学科成绩透视表,划分理科、文科两大类科目,计算文理平均分差值量化学科偏向;
- 在校行为维度:统计每名学生迟到、旷课、早退等考勤异常总次数,作为学生自律水平量化指标;
- 消费行为维度:提取学生日均消费、消费波动变异系数、在校消费活跃天数,间接反映生活习惯与家庭经济水平。
PCA 降维必要性说明
四类维度合计提取 16 项数值特征,但指标间存在强相关性(平均分与 Z 分数、消费总额与消费次数高度耦合)。直接在高维空间聚类会造成同类特征重复加权,干扰分群效果。PCA 采用正交变换将原始特征映射至互不相关的主成分空间,实验选取前 3 个主成分作为聚类输入特征,保证信息留存的同时压缩特征维度。
2.1 学业表现与行为消费特征提取
从考试成绩表中聚合每个学生的均分、标准差、极差、考试次数、Z-Score均值等统计量,并计算变异系数(CV = 标准差/均值)来衡量成绩的相对波动性。同时构建学生×学科的成绩透视矩阵,区分理科和文科两大类别,计算文理均分差异来反映学科偏好。
行为规范维度从考勤数据中统计每个学生的异常次数(迟到、早退、旷课等),作为自律性的代理指标。消费模式维度从消费流水中提取日均消费金额、消费波动性、消费活跃天数等特征,反映学生的生活规律和经济状况。
# ==== 学业聚合特征 ====
g_agg = df_chengji.groupby('mes_StudentID').agg(
mean_score=('score', 'mean'),
std_score=('score', 'std'),
max_score=('score', 'max'),
min_score=('score', 'min'),
exam_cnt=('score', 'count'),
z_mean=('z_val', 'mean')
).reset_index()
g_agg.columns = ['sid', 'mean_score', 'std_score', 'max_score', 'min_score', 'exam_cnt', 'z_mean']
g_agg['score_range'] = g_agg['max_score'] - g_agg['min_score']
g_agg['cv'] = g_agg['std_score'] / g_agg['mean_score'].clip(lower=1)
# 学科透视表
sub_pivot = df_chengji.pivot_table(
index='mes_StudentID', columns='mes_sub_name', values='score', aggfunc='mean'
).reset_index()
sub_pivot.columns = ['sid'] + [f'sub_{c}' for c in sub_pivot.columns[1:]]
# 文理分科
sci_subs = [c for c in sub_pivot.columns if any(k in c for k in ['数学','物理','化学','生物'])]
art_subs = [c for c in sub_pivot.columns if any(k in c for k in ['语文','英语','政治','历史','地理'])]
sub_pivot['sci_avg'] = sub_pivot[sci_subs].mean(axis=1)
sub_pivot['art_avg'] = sub_pivot[art_subs].mean(axis=1)
sub_pivot['sci_art_gap'] = sub_pivot['sci_avg'] - sub_pivot['art_avg']
print(f'学业聚合: {g_agg.shape[0]}人 × {g_agg.shape[1]}指标')
print(f'学科矩阵: {sub_pivot.shape[0]}人 × {len(sci_subs)+len(art_subs)}学科(文理)')
# ==== 考勤特征 ====
att_agg = df_kaoqin.groupby('bf_studentID').size().reset_index(name='att_violations')
att_agg.columns = ['sid', 'att_violations']
# ==== 消费特征 ====
sp_agg = df_consume.groupby('bf_StudentID').agg(
sp_total=('amount', 'sum'),
sp_mean=('amount', 'mean'),
sp_std=('amount', 'std'),
sp_max=('amount', 'max'),
sp_cnt=('amount', 'count')
).reset_index()
sp_agg.columns = ['sid', 'sp_total', 'sp_mean', 'sp_std', 'sp_max', 'sp_cnt']
sp_agg['sp_cv'] = sp_agg['sp_std'] / sp_agg['sp_mean'].clip(lower=0.1)
sp_days = df_consume.groupby('bf_StudentID')['ts'].apply(
lambda x: x.dt.date.nunique()
).reset_index(name='sp_days')
sp_days.columns = ['sid', 'sp_days']
sp_agg = sp_agg.merge(sp_days, on='sid', how='left')
print(f'考勤: {att_agg.shape[0]}人有记录')
print(f'消费: {sp_agg.shape[0]}人有记录')
运行输出:
学业聚合: 3869人 × 9指标
学科矩阵: 3862人 × 9学科(文理)
考勤: 3058人有记录
消费: 1730人有记录
2.2 特征合并与PCA降维
将四个维度的特征合并为统一的"学生特征向量",通过StandardScaler标准化后进行PCA降维。选取前3个主成分作为聚类的输入特征,并通过碎石图和相关性矩阵验证降维效果。
# ==== 合并主表 ====
master = df_student[['bf_StudentID', 'bf_Name', 'bf_sex', 'cla_Name', 'cla_id']].copy()
master.columns = ['sid', 'name', 'gender', 'class_name', 'class_id']
master = master.merge(g_agg, on='sid', how='left')
master = master.merge(sub_pivot.drop(columns=['sci_avg','art_avg','sci_art_gap'], errors='ignore'),
on='sid', how='left')
master = master.merge(sub_pivot[['sid','sci_avg','art_avg','sci_art_gap']], on='sid', how='left')
master = master.merge(att_agg, on='sid', how='left')
master = master.merge(sp_agg, on='sid', how='left')
master['att_violations'] = master['att_violations'].fillna(0).astype(int)
num_cols = ['mean_score','std_score','max_score','min_score','exam_cnt','z_mean',
'score_range','cv','sci_avg','art_avg','sci_art_gap',
'att_violations','sp_mean','sp_std','sp_cv','sp_days']
num_cols = [c for c in num_cols if c in master.columns]
print(f'主表: {master.shape[0]}人 × {master.shape[1]}列')
print(f'数值特征: {len(num_cols)}个')
print(f'覆盖率: 学业{master["mean_score"].notna().mean()*100:.0f}%, '
f'消费{master["sp_mean"].notna().mean()*100:.0f}%')
# ==== PCA降维 ====
pca_df = master[num_cols].dropna()
scaler = StandardScaler()
X_scaled = scaler.fit_transform(pca_df)
pca_full = PCA()
pca_full.fit(X_scaled)
cumvar = np.cumsum(pca_full.explained_variance_ratio_)
pca3 = PCA(n_components=3)
X_pca3 = pca3.fit_transform(X_scaled)
pca_df['pc1'] = X_pca3[:, 0]
pca_df['pc2'] = X_pca3[:, 1]
pca_df['pc3'] = X_pca3[:, 2]
print(f'\nPCA前3主成分累计方差解释率: {cumvar[2]*100:.1f}%')
for i in range(3):
print(f' PC{i+1}: {pca_full.explained_variance_ratio_[i]*100:.1f}%')
# ---- 可视化: 碎石图 + 相关性矩阵 ----
fig, axes = plt.subplots(1, 2, figsize=(15, 5))
ax = axes[0]
n_comp = min(10, len(pca_full.explained_variance_ratio_))
ax.bar(range(1, n_comp+1), pca_full.explained_variance_ratio_[:n_comp]*100,
color='#4e79a7', edgecolor='white', label='单成分')
ax.plot(range(1, n_comp+1), cumvar[:n_comp]*100, 'o-', color='#e15759', label='累计')
ax.axhline(80, color='gray', ls=':', lw=1)
ax.set_xlabel('主成分序号', fontproperties=zh_font)
ax.set_ylabel('方差解释率(%)', fontproperties=zh_font)
ax.set_title('PCA碎石图', fontproperties=zh_font, fontsize=13)
ax.legend(prop=zh_font)
ax.set_xticks(range(1, n_comp+1))
ax = axes[1]
corr = pd.DataFrame(X_scaled, columns=num_cols).corr()
mask = np.triu(np.ones_like(corr, dtype=bool), k=1)
sns.heatmap(corr, mask=mask, cmap='RdBu_r', center=0,
annot=True, fmt='.1f', square=True, ax=ax,
xticklabels=[c.replace('_','\n') for c in num_cols],
yticklabels=[c.replace('_','\n') for c in num_cols],
annot_kws={'size': 7})
ax.set_title('特征相关性矩阵', fontproperties=zh_font, fontsize=13)
plt.tight_layout()
plt.savefig('./output/ch2_pca_corr.png', bbox_inches='tight')
plt.show()
运行输出:
主表: 1765人 × 40列
数值特征: 16个
覆盖率: 学业89%, 消费98%
PCA前3主成分累计方差解释率: 62.9%
PC1: 28.7%
PC2: 22.5%
PC3: 11.6%

三、聚类驱动的学生分群
聚类算法相对人工规则分箱的优势说明
传统学生分层方案仅依托单一指标人工划分阈值(如根据 Z 分数划分为尖子生、中等生、学困生),该方法存在三点明显短板:
- 维度割裂:各指标独立划分层级,无法挖掘 “高分但成绩波动大”“文科优势但频繁缺勤” 这类多指标交叉特征群体;
- 阈值主观:分层边界完全依靠教师经验设定,无客观数据支撑;
- 分层粒度粗:通常仅划分 3-5 类,大量学生个体差异化特征被抹平。
K-Means 无监督聚类无需人工预设分层标签,基于多维低维主成分空间自动挖掘天然学生群体。实验组合肘部法则、轮廓系数两项指标综合确定最优分群数量;分群完成后计算各组原始特征均值,通过平行坐标图可视化群体差异,输出结构化文字群体画像。
3.1 肘部法则与轮廓系数确定最优K值
在PCA降维后的3维主成分空间中运行K-Means,通过肘部法则(Elbow Method)观察惯性随K值的衰减曲线,同时计算轮廓系数(Silhouette Score)评估聚类内部紧密度和聚类间分离度,两者结合确定最优聚类数。
# ==== 肘部法则 + 轮廓系数 ====
K_range = range(2, 11)
inertias = []
sil_scores = []
for k in K_range:
km = KMeans(n_clusters=k, n_init=10, random_state=42, max_iter=300)
labels = km.fit_predict(X_pca3)
inertias.append(km.inertia_)
sil_scores.append(silhouette_score(X_pca3, labels))
best_k = list(K_range)[np.argmax(sil_scores)]
print(f'最优K={best_k} (轮廓系数={max(sil_scores):.3f})')
# ==== 最终聚类 ====
K_final = best_k
km_final = KMeans(n_clusters=K_final, n_init=20, random_state=42, max_iter=500)
pca_df['cluster'] = km_final.fit_predict(X_pca3)
master = master.merge(pca_df[['cluster']], left_index=True, right_index=True, how='left')
print(f'\n=== 聚类结果(K={K_final}) ===')
for c in sorted(pca_df['cluster'].unique()):
cnt = (pca_df['cluster'] == c).sum()
print(f' 群集{c}: {cnt}人 ({cnt/len(pca_df)*100:.1f}%)')
# ---- 可视化: 肘部 + 轮廓 + 2D散点 ----
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
ax = axes[0]
ax.plot(list(K_range), inertias, 'o-', color='#4e79a7', lw=2)
ax.axvline(K_final, color='red', ls='--', lw=1.5, label=f'K={K_final}')
ax.set_xlabel('聚类数K', fontproperties=zh_font)
ax.set_ylabel('惯性(组内平方和)', fontproperties=zh_font)
ax.set_title('肘部法则', fontproperties=zh_font, fontsize=13)
ax.legend(prop=zh_font)
ax = axes[1]
ax.plot(list(K_range), sil_scores, 's-', color='#59a14f', lw=2)
ax.axvline(K_final, color='red', ls='--', lw=1.5, label=f'K={K_final}')
ax.set_xlabel('聚类数K', fontproperties=zh_font)
ax.set_ylabel('轮廓系数', fontproperties=zh_font)
ax.set_title('轮廓系数曲线', fontproperties=zh_font, fontsize=13)
ax.legend(prop=zh_font)
ax = axes[2]
cluster_colors = ['#4e79a7','#e15759','#76b7b2','#f28e2b','#59a14f',
'#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ac']
for c in sorted(pca_df['cluster'].unique()):
mask = pca_df['cluster'] == c
ax.scatter(pca_df.loc[mask, 'pc1'], pca_df.loc[mask, 'pc2'],
c=cluster_colors[c % len(cluster_colors)], s=25, alpha=0.6,
edgecolors='white', linewidth=0.3, label=f'群集{c}')
ax.set_xlabel('PC1', fontproperties=zh_font)
ax.set_ylabel('PC2', fontproperties=zh_font)
ax.set_title(f'PCA 2D聚类散点(K={K_final})', fontproperties=zh_font, fontsize=13)
ax.legend(prop=zh_font, fontsize=9)
plt.tight_layout()
plt.savefig('./output/ch3_clustering.png', bbox_inches='tight')
plt.show()
运行输出:
最优K=2 (轮廓系数=0.443)
=== 聚类结果(K=2) ===
群集0: 449人 (29.1%)
群集1: 1094人 (70.9%)

3.2 聚类结果解读
平行坐标图可直观对比两类群体全部特征差异:部分群体平均分、标准化分数更高,但考勤违规次数同步偏高,属于学业优秀但在校纪律波动的学生;另一类学生整体成绩中等,但分数变异系数极低,考试发挥稳定。
聚类算法核心优势在于挖掘多指标交叉特征,不只是单一维度分层,能够完整刻画学生综合特质。 针对 K-Means 中心点随机初始化不稳定问题,代码设置n_init=20,多次迭代选取最优聚类结果;轮廓系数数值越趋近 1,代表群体内部样本紧密、群体间区分度高,是量化分群质量的核心指标。
3.3 各群集特征画像
计算各群集在所有原始特征维度上的均值,生成平行坐标图直观展示各群集的特征轮廓,并为每个群集撰写结构化的文字描述。
# ==== 聚类画像: 各群集特征统计 ====
profile_cols = ['mean_score','std_score','z_mean','cv','sci_avg','art_avg',
'sci_art_gap','att_violations','sp_mean','sp_days']
profile_cols = [c for c in profile_cols if c in master.columns]
cluster_stats = master.groupby('cluster')[profile_cols].mean()
cluster_z = (cluster_stats - cluster_stats.mean()) / cluster_stats.std().clip(lower=0.01)
print('=== 各群集核心指标均值 ===')
display_cols = ['mean_score','z_mean','cv','sci_art_gap','att_violations','sp_mean']
display_cols = [c for c in display_cols if c in cluster_stats.columns]
print(cluster_stats[display_cols].round(2).to_string())
# ---- 平行坐标图 ----
fig, ax = plt.subplots(figsize=(14, 6))
pd_coords = cluster_z[display_cols].copy()
pd_coords['cluster'] = pd_coords.index
for c in sorted(pd_coords['cluster'].unique()):
row = pd_coords[pd_coords['cluster'] == c].iloc[0]
vals = [row[col] for col in display_cols]
color = cluster_colors[int(c) % len(cluster_colors)]
ax.plot(range(len(display_cols)), vals, 'o-', color=color, lw=2.5,
markersize=8, label=f'群集{c}')
ax.set_xticks(range(len(display_cols)))
ax.set_xticklabels([c.replace('_','\n') for c in display_cols], fontproperties=zh_font, fontsize=9)
ax.set_ylabel('标准化值', fontproperties=zh_font)
ax.set_title('各群集特征平行坐标图', fontproperties=zh_font, fontsize=14)
ax.legend(prop=zh_font, loc='upper right')
ax.axhline(0, color='gray', ls=':', lw=0.8)
plt.tight_layout()
plt.savefig('./output/ch3_parallel.png', bbox_inches='tight')
plt.show()
# ==== 聚类描述 ====
cluster_desc = {}
for c in sorted(master['cluster'].dropna().unique()):
c = int(c)
grp = master[master['cluster'] == c]
parts = [f'群集{c}({len(grp)}人)']
parts.append(f'均分{grp["mean_score"].mean():.1f}')
z = grp['z_mean'].mean()
parts.append(f'Z值{z:+.2f}')
gap = grp['sci_art_gap'].mean() if 'sci_art_gap' in grp else 0
if pd.notna(gap) and abs(gap) > 3:
parts.append('偏理' if gap > 0 else '偏文')
att = grp['att_violations'].mean()
parts.append(f'考勤异常{att:.1f}次')
sp = grp['sp_mean'].mean()
if pd.notna(sp):
parts.append(f'日均消费{sp:.1f}元')
cluster_desc[c] = ','.join(parts)
print(f' {cluster_desc[c]}')
运行结果:
=== 各群集核心指标均值 ===
mean_score z_mean cv sci_art_gap att_violations sp_mean
cluster
0.0 62.25 0.02 0.49 -3.33 8.12 9.45
1.0 53.28 -0.01 0.62 1.07 2.85 8.43
群集0(449人),均分62.3,Z值+0.02,偏文,考勤异常8.1次,日均消费9.5元
群集1(1094人),均分53.3,Z值-0.01,考勤异常2.9次,日均消费8.4元

四、学业轨迹时序分析
静态画像的盲区
前文聚类、规则分层生成的画像均属于静态快照,仅反映学生全周期平均水平,完全忽略成绩时序变化特征。 两名平均分完全相同的学生,一名持续稳步提升、一名连续下滑,静态画像会判定为同一类学生,但二者对应的教学干预方案完全不同。时序分析能够为学生画像补充成长性动态维度,让画像从单纯描述现状升级为具备诊断预判能力。
方法设计
本章采用两个互补的分析视角:
-
Mann-Kendall趋势检验: 这是一种非参数的趋势检测方法,不要求数据满足正态分布假设,非常适合成绩这种偏态分布的场景。它对每个学生的学期均分序列计算趋势方向和显著性p值,将学生分为"显著上升""显著下降""基本平稳""轻微波动"四类。我们设定至少4个学期的数据才进行趋势检验,以确保统计结论的可靠性。
-
学业层级迁移矩阵: 除了全局趋势,我们还关注学生在相邻学期之间的层级流动。将每个学期内的学生按成绩四分位数划分为"高/中高/中低/低"四个层级,然后计算相邻学期间的迁移概率矩阵。对角线值高表示层级稳定,非对角线值揭示进步或退步的流动方向和规模。
这两个分析共同为画像增加了"成长性"维度,使后续的教育干预能够区分"正在进步的学生"和"正在下滑的学生"。
4.1 Mann-Kendall趋势检验
将成绩数据按学期聚合,构建每个学生的学期均分时序序列。对每个学生的学期均分序列计算趋势方向和显著性p值,将学生分为"显著上升""显著下降""基本平稳""轻微波动"四类。设定至少4个学期的数据才进行趋势检验,以确保统计结论的可靠性。
# ==== 构建学期级成绩序列 ====
df_chengji['exam_date'] = pd.to_datetime(df_chengji['exam_sdate'], errors='coerce')
df_chengji['semester'] = df_chengji['exam_date'].dt.to_period('2Q').astype(str)
df_chengji_sorted = df_chengji.dropna(subset=['exam_date','score','semester']).sort_values('exam_date')
sem_sub = df_chengji.groupby(['mes_StudentID','mes_sub_name','semester'])['score'].mean().reset_index()
sem_sub.columns = ['sid','subject','semester','sem_score']
# Mann-Kendall趋势检验
student_sem = df_chengji_sorted.groupby(['mes_StudentID','semester'])['score'].mean().reset_index()
student_sem.columns = ['sid','semester','sem_avg']
trend_results = {}
for sid, grp in student_sem.groupby('sid'):
if len(grp) >= 4:
scores = grp.sort_values('semester')['sem_avg'].values
try:
mk = mannkendall(scores)
trend_results[sid] = {
'trend': mk.trend, 'p_value': mk.p,
'slope': mk.slope, 'sem_count': len(grp)
}
except:
pass
trend_df = pd.DataFrame(trend_results).T.reset_index()
trend_df.columns = ['sid','trend','p_value','slope','sem_count']
def classify_trend(row):
if row['p_value'] < 0.1:
if row['slope'] > 0.5: return '显著上升'
elif row['slope'] < -0.5: return '显著下降'
if abs(row['slope']) <= 0.5: return '基本平稳'
return '轻微波动'
trend_df['trend_label'] = trend_df.apply(classify_trend, axis=1)
master = master.merge(trend_df[['sid','trend','slope','trend_label']], on='sid', how='left')
print(f'趋势分析: {len(trend_df)}名学生(≥4学期)')
print('\n=== 趋势类型分布 ===')
for lbl, cnt in trend_df['trend_label'].value_counts().items():
print(f' {lbl}: {cnt}人 ({cnt/len(trend_df)*100:.1f}%)')
# ---- 可视化 ----
fig, axes = plt.subplots(1, 2, figsize=(15, 5))
ax = axes[0]
ax.hist(trend_df['slope'].dropna(), bins=40, color='#76b7b2', edgecolor='white', alpha=0.85)
ax.axvline(0, color='red', ls='--', lw=1.5)
ax.set_xlabel('Mann-Kendall斜率(每学期分数变化)', fontproperties=zh_font)
ax.set_ylabel('学生人数', fontproperties=zh_font)
ax.set_title('学业趋势斜率分布', fontproperties=zh_font, fontsize=13)
ax = axes[1]
trend_df['slope'] = pd.to_numeric(trend_df['slope'], errors='coerce')
up_students = trend_df[trend_df['trend_label']=='显著上升'].nlargest(3, 'slope')['sid'].tolist()
dn_students = trend_df[trend_df['trend_label']=='显著下降'].nsmallest(3, 'slope')['sid'].tolist()
for sid in up_students:
data = student_sem[student_sem['sid']==sid].sort_values('semester')
if len(data) >= 4:
ax.plot(range(len(data)), data['sem_avg'].values, 'o-', color='#59a14f', alpha=0.7, lw=1.5)
for sid in dn_students:
data = student_sem[student_sem['sid']==sid].sort_values('semester')
if len(data) >= 4:
ax.plot(range(len(data)), data['sem_avg'].values, 'o-', color='#e15759', alpha=0.7, lw=1.5)
ax.set_xlabel('学期序号', fontproperties=zh_font)
ax.set_ylabel('学期均分', fontproperties=zh_font)
ax.set_title('典型上升(绿)/下降(红)轨迹', fontproperties=zh_font, fontsize=13)
from matplotlib.lines import Line2D
legend_elements = [Line2D([0],[0],color='#59a14f',lw=2,label='上升趋势'),
Line2D([0],[0],color='#e15759',lw=2,label='下降趋势')]
ax.legend(handles=legend_elements, prop=zh_font)
plt.tight_layout()
plt.savefig('./output/ch4_trend.png', bbox_inches='tight')
plt.show()
运行输出:
趋势分析: 3321名学生(≥4学期)
=== 趋势类型分布 ===
轻微波动: 2265人 (68.2%)
显著上升: 615人 (18.5%)
基本平稳: 423人 (12.7%)
显著下降: 18人 (0.5%)

4.2 学业层级迁移矩阵
筛选近四个有效学期成绩数据,每学期按分数四分位划分四层学生;统计相邻学期学生层级流转频次,归一化得到迁移概率热力图,直观展示学生分层稳定性与流动趋势。
# ==== 学科间迁移分析 ====
semesters_avail = sorted(student_sem['semester'].unique())
recent_sems = semesters_avail[-4:] if len(semesters_avail) >= 4 else semesters_avail
sem_labels = {}
for sem in recent_sems:
sem_data = student_sem[student_sem['semester'] == sem]
if len(sem_data) < 20:
continue
quartiles = sem_data['sem_avg'].quantile([0.25, 0.5, 0.75])
def assign_level(s):
if s >= quartiles[0.75]: return 3
elif s >= quartiles[0.5]: return 2
elif s >= quartiles[0.25]: return 1
else: return 0
sem_labels[sem] = sem_data.set_index('sid')['sem_avg'].apply(assign_level)
trans_matrices = {}
for i in range(len(recent_sems)-1):
s1, s2 = recent_sems[i], recent_sems[i+1]
if s1 not in sem_labels or s2 not in sem_labels:
continue
common = sem_labels[s1].index.intersection(sem_labels[s2].index)
if len(common) < 10:
continue
mat = pd.crosstab(sem_labels[s1][common], sem_labels[s2][common], normalize='index')
trans_matrices[(s1, s2)] = mat
if trans_matrices:
n_plots = len(trans_matrices)
fig, axes = plt.subplots(1, n_plots, figsize=(6*n_plots, 5))
if n_plots == 1:
axes = [axes]
level_names = ['低','中低','中高','高']
for idx, ((s1,s2), mat) in enumerate(trans_matrices.items()):
ax = axes[idx]
sns.heatmap(mat, annot=True, fmt='.0%', cmap='YlGnBu',
xticklabels=level_names, yticklabels=level_names,
ax=ax, vmin=0, vmax=1, linewidths=0.5)
ax.set_title(f'{s1} → {s2}\n学业层级迁移概率', fontproperties=zh_font, fontsize=11)
ax.set_xlabel('后一学期', fontproperties=zh_font)
ax.set_ylabel('前一学期', fontproperties=zh_font)
ax.set_xticklabels(ax.get_xticklabels(), fontproperties=zh_font)
ax.set_yticklabels(ax.get_yticklabels(), fontproperties=zh_font)
plt.tight_layout()
plt.savefig('./output/ch4_transition.png', bbox_inches='tight')
plt.show()
else:
print('学期数据不足,无法构建转移矩阵')
print('\n-> 对角线值越高,说明学生层级越稳定;非对角线值揭示进步/退步流动')

4.3 时序分析小结
Mann-Kendall 检验聚焦单名学生长期成绩升降趋势,层级迁移矩阵侧重群体层级流转规律,二者形成互补分析视角。实验结果显示超六成学生成绩层级稳定,但仍存在可观比例学生跨层级升降,证明学业干预存在明确窗口期;成绩下滑学生若早期介入干预,效果远优于后期补救。
后续 LLM 生成学生画像时,会将上升 / 下降 / 平稳趋势标签作为核心输入字段,融合静态聚类特征、动态时序特征生成完整多维度学生画像。
五、规则画像与LLM画像对比
对比实验设计
在前面的章节中,我们已经通过聚类分析获得了学生群体标签,通过时序分析获得了成长性标签。本章将回到画像构建的核心问题:如何为每个学生生成一份有价值的画像描述?
我们设计了两组画像方法进行对比实验:
方法A:传统规则画像
采用if-else规则引擎,将数值特征映射为离散标签。例如:
-
学业维度:Z-Score ≥ 0.8 → "优秀",0 ≤ Z < 0.8 → "中等偏上",以此类推
-
行为维度:考勤异常 ≤ 2次 → "规范",3~6次 → "需关注",> 6次 → "预警"
最终画像为标签拼接形式,如"学业:中等偏上 | 行为:规范"。
方法B:LLM智能画像
利用通义千问大语言模型,将学生的多维数据(含聚类标签和趋势信息)以自然语言形式输入,要求模型输出结构化的JSON画像报告,包括核心概述、学业分析、行为分析、风险标记、优势识别和干预建议。
评估策略
除了定性对比两种画像的可读性和信息量外,我们还引入了TF-IDF文本分析来量化LLM画像的语义丰富度:对所有LLM画像文本进行TF-IDF向量化,提取高频关键词,验证其与教育领域的相关性;再通过t-SNE降维将画像文本映射到2维语义空间,观察不同群集的学生画像是否在语义上自然分离。
5.1 传统规则画像
基于 Z 分数、考勤违规次数搭建分层规则,生成简短拼接式标签画像;统计标签分布,绘制规则标签与聚类群体交叉热力图,对比两种分层逻辑差异。
# ==== 传统规则画像(简化版) ====
def rule_label(row):
z = row.get('z_mean', np.nan)
if pd.isna(z): return '无数据'
if z >= 0.8: return '优秀'
elif z >= 0: return '中等偏上'
elif z >= -0.8: return '中等偏下'
else: return '困难'
master['rule_tier'] = master.apply(rule_label, axis=1)
def rule_behavior(row):
v = row.get('att_violations', 0)
return '规范' if v <= 2 else '需关注' if v <= 6 else '预警'
master['rule_behavior'] = master.apply(rule_behavior, axis=1)
master['rule_profile'] = master.apply(
lambda r: f"学业:{r['rule_tier']} | 行为:{r['rule_behavior']}", axis=1)
print('=== 规则标签分布 ===')
print(master['rule_tier'].value_counts().to_string())
print()
print(master['rule_behavior'].value_counts().to_string())
# ---- 标签分布可视化 ----
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
ax = axes[0]
tier_order = ['困难','中等偏下','中等偏上','优秀','无数据']
tier_cnt = master['rule_tier'].value_counts().reindex(tier_order).dropna()
tier_cnt.plot.bar(ax=ax, color=['#e15759','#f28e2b','#76b7b2','#59a14f','#bab0ac'], edgecolor='white')
ax.set_title('规则方法: 学业标签分布', fontproperties=zh_font, fontsize=13)
ax.set_ylabel('人数', fontproperties=zh_font)
ax.set_xticklabels(ax.get_xticklabels(), fontproperties=zh_font, rotation=0)
ax = axes[1]
cross = pd.crosstab(master['cluster'], master['rule_tier'])
sns.heatmap(cross, annot=True, fmt='d', cmap='Blues', ax=ax)
ax.set_title('聚类标签 vs 规则标签 交叉热力图', fontproperties=zh_font, fontsize=13)
ax.set_xlabel('规则标签', fontproperties=zh_font)
ax.set_ylabel('聚类群集', fontproperties=zh_font)
ax.set_xticklabels(ax.get_xticklabels(), fontproperties=zh_font)
ax.set_yticklabels(ax.get_yticklabels(), fontproperties=zh_font)
plt.tight_layout()
plt.savefig('./output/ch5_rule_vs_cluster.png', bbox_inches='tight')
plt.show()
print('\n-> 聚类能发现规则方法遗漏的群体特征(如高均分但高波动群体)')
运行输出:
=== 规则标签分布 ===
rule_tier
中等偏上 834
中等偏下 593
无数据 199
困难 109
优秀 30
rule_behavior
规范 1104
需关注 355
预警 306

5.2 LLM智能画像
利用通义千问大语言模型,将学生的多维数据(含聚类标签和趋势信息)以自然语言形式输入,要求模型输出结构化的JSON画像报告,包括核心概述、学业分析、行为分析、风险标记、优势识别和干预建议。
# ==== LLM智能画像 ====
def build_data_text(row):
parts = [f"学生{row['name']}, {row['gender']}, {row['class_name']}。"]
if pd.notna(row.get('mean_score')):
parts.append(f"均分{row['mean_score']:.1f}, Z值{row.get('z_mean',0):+.2f}, "
f"分数极差{row.get('score_range',0):.1f}, 变异系数{row.get('cv',0):.2f}。")
if pd.notna(row.get('sci_art_gap')):
parts.append(f"理科均分{row['sci_avg']:.1f},文科均分{row['art_avg']:.1f},文理分差{row['sci_art_gap']:.1f}。")
parts.append(f"考勤异常{int(row.get('att_violations',0))}次。")
if pd.notna(row.get('sp_mean')):
parts.append(f"日均校园消费{row['sp_mean']:.1f}元,在校消费活跃{int(row.get('sp_days',0))}天。")
if pd.notna(row.get('cluster')):
parts.append(f"多维特征聚类分群:群集{int(row['cluster'])}。")
if pd.notna(row.get('trend_label')):
parts.append(f"学业长期变化趋势:{row['trend_label']},每学期分数变化斜率{row.get('slope',0):.2f}。")
return ''.join(parts)
LLM_SYS = (
"你是资深教育数据分析师。根据学生多维数据生成标准JSON格式学生画像。\n"
"固定JSON字段结构,不得新增、删减字段:\n"
'{\n'
' "summary": "80字以内核心画像总述,简洁概括学生综合特质",\n'
' "academic": "学业深度分析,结合分数稳定性、文理偏向、长期升降趋势解读",\n'
' "behavior": "在校考勤、消费行为综合解读,分析行为背后潜在状态",\n'
' "risks": ["风险点1","风险点2"],\n'
' "strengths": ["优势1","优势2"],\n'
' "interventions": ["针对性教学干预建议1","针对性教学干预建议2"]\n'
'}\n'
"要求:必须结合聚类群体特征与学业趋势关联推理,干预方案落地可执行,只输出纯JSON,无多余文字。"
)
def call_llm(data_text):
if llm_client is None:
return {
'summary':'降级模式无法生成深度画像',
'academic':'待分析','behavior':'待分析',
'risks':[],'strengths':[],'interventions':[]
}
try:
resp = llm_client.chat.completions.create(
model=MODEL_ID,
messages=[
{"role":"system","content":LLM_SYS},
{"role":"user","content":f"学生原始数据:\n{data_text}"}
],
temperature=0.2,
response_format={"type":"json_object"}
)
json_str = resp.choices[0].message.content.strip()
return json.loads(json_str)
except Exception as e:
print(f"大模型调用异常:{e}")
return None
# 分层抽样,每个聚类抽取最多3名学生
sample_ids = []
for c in sorted(master['cluster'].dropna().unique()):
group_df = master[master['cluster'] == c]
pick_num = min(3, len(group_df))
sample_ids.extend(group_df.sample(n=pick_num, random_state=99).index.tolist())
llm_portraits = {}
print(f"开始批量生成LLM画像,共{len(sample_ids)}名学生:")
for idx in sample_ids:
student_row = master.loc[idx]
student_name = student_row['name']
input_text = build_data_text(student_row)
print(f" 正在生成:{student_name}", end="...")
res = call_llm(input_text)
if res:
llm_portraits[idx] = res
print("OK")
else:
llm_portraits[idx] = {
'summary':'接口调用失败',
'academic':'N/A','behavior':'N/A',
'risks':[],'strengths':[],'interventions':[]
}
print("FAIL")
if llm_client:
time.sleep(0.4)
print(f"\n批量画像生成完成,共存储{len(llm_portraits)}份结构化画像")
运行结果:
采样6名学生进行LLM画像...
[1/6] 钱某某... OK
[2/6] 姜某某... OK
[3/6] 徐某某... OK
[4/6] 陈某某... OK
[5/6] 林某某... OK
[6/6] 刘某某... OK
完成: 6份画像
5.3 TF-IDF语义分析与t-SNE可视化
对所有LLM画像文本进行TF-IDF向量化,提取高频关键词验证其与教育领域的相关性;再通过t-SNE降维将画像文本映射到2维语义空间,观察不同群集的学生画像是否在语义上自然分离。
# ==== LLM画像展示 + TF-IDF语义分析 ====
print('=== LLM画像示例展示 ===')
show_count = 4
for pos, idx in enumerate(list(llm_portraits.keys())[:show_count]):
row_info = master.loc[idx]
portrait = llm_portraits[idx]
print(f'\n{"-"*55}')
print(f'{row_info["name"]}({row_info["gender"]},{row_info["class_name"]}) | 聚类群集:{int(row_info["cluster"])}')
print(f' 概述:{portrait.get("summary","无")}')
print(f' 学业分析:{portrait.get("academic","无")}')
print(f' 行为分析:{portrait.get("behavior","无")}')
print(f' 潜在风险:{"、".join(portrait.get("risks",[]))}')
print(f' 个人优势:{"、".join(portrait.get("strengths",[]))}')
print(f' 干预建议:{"、".join(portrait.get("interventions",[]))}')
# 整合全部画像文本用于TF-IDF计算
all_text_pool = []
student_name_list = []
student_cluster_label = []
for idx, portrait in llm_portraits.items():
full_text = ' '.join([
portrait.get('summary',''),
portrait.get('academic',''),
portrait.get('behavior',''),
' '.join(portrait.get('risks',[])),
' '.join(portrait.get('strengths',[])),
' '.join(portrait.get('interventions',[]))
])
all_text_pool.append(full_text)
student_name_list.append(master.loc[idx, 'name'])
student_cluster_label.append(int(master.loc[idx, 'cluster']))
# TF-IDF向量化
tfidf_vectorizer = TfidfVectorizer(max_features=200)
tfidf_matrix = tfidf_vectorizer.fit_transform(all_text_pool)
vocab_words = tfidf_vectorizer.get_feature_names_out()
avg_tfidf_weight = np.asarray(tfidf_matrix.mean(axis=0)).flatten()
# 提取权重前20关键词
word_weight_pair = list(zip(vocab_words, avg_tfidf_weight))
word_weight_pair.sort(key=lambda x:x[1], reverse=True)
top20_keywords = word_weight_pair[:20]
print(f'\n=== TF-IDF权重Top20教育关键词 ===')
for word, weight in top20_keywords:
print(f' {word}: {weight:.4f}')
# t-SNE二维降维可视化画像语义分布
if len(all_text_pool) >= 3:
tsne_model = TSNE(n_components=2, random_state=42, perplexity=min(5, len(all_text_pool)-1))
text_2d_embedding = tsne_model.fit_transform(tfidf_matrix.toarray())
fig, axes = plt.subplots(1, 2, figsize=(15, 5))
# 左图:关键词权重横向条形图
ax1 = axes[0]
top15_words = [i[0] for i in top20_keywords[:15]]
top15_weight = [i[1] for i in top20_keywords[:15]]
ax1.barh(top15_words[::-1], top15_weight[::-1], color='#59a14f', edgecolor='white')
ax1.set_xlabel('平均TF-IDF权重', fontproperties=zh_font)
ax1.set_title('LLM画像高频教育关键词', fontproperties=zh_font, fontsize=13)
ax1.set_yticklabels(top15_words[::-1], fontproperties=zh_font)
# 右图:t-SNE语义空间散点
ax2 = axes[1]
color_map = ['#4e79a7','#e15759','#76b7b2','#f28e2b','#59a14f']
unique_cluster = sorted(list(set(student_cluster_label)))
for cluster_id in unique_cluster:
mask = [cid == cluster_id for cid in student_cluster_label]
x = text_2d_embedding[mask, 0]
y = text_2d_embedding[mask, 1]
ax2.scatter(x, y, c=color_map[cluster_id % len(color_map)], s=110,
edgecolors='white', linewidth=1.2, label=f'群集{cluster_id}')
# 标注学生姓名
for i, name in enumerate(student_name_list):
ax2.annotate(name, (text_2d_embedding[i,0], text_2d_embedding[i,1]),
fontproperties=zh_font, fontsize=8)
ax2.set_title('LLM画像文本t-SNE语义空间分布', fontproperties=zh_font, fontsize=13)
ax2.set_xlabel('维度1', fontproperties=zh_font)
ax2.set_ylabel('维度2', fontproperties=zh_font)
ax2.legend(prop=zh_font)
plt.tight_layout()
plt.savefig('./output/ch5_tfidf_tsne.png', bbox_inches='tight')
plt.show()
=== LLM画像示例 ===
───────────────────────────────────────────────────────
钱某某(女,高三(10)) | 聚类:群集0
概述: 该生成绩中下且波动大,文科略优,近期轻微下滑。在校时间长、纪律好但效率偏低;生活极度节俭。需重点关注学法指导与生活关怀。
学业: 均分59.4(Z值-0.25)属中下水平,极差110与变异系数0.48凸显成绩极不稳定。文科(60.3)略优于理科(54.0)。趋势斜率-1.35表明成绩轻微下滑,高三复习期存在知识消化不良或学习瓶颈,呈现典型的'高耗时低产出'特征。
行为: 活跃104天且考勤异常仅2次,表明在校时间长、纪律性强,具备吃苦精神。但日均消费仅11.1元,生活极度节俭,可能反映家庭经济压力或饮食不规律,需警惕因营养不足导致的高三备考精力透支。
风险: 成绩持续下滑与高三备考焦虑风险, 极低消费引发的营养不良与精力透支风险
优势: 在校活跃度高且考勤纪律良好, 文科具备相对优势与提分空间
建议: 开展学情诊断与理科基础帮扶,优化学习方法以打破苦学无效困境, 私下排查低消费原因并提供隐性资助,加强心理疏导与备考精力管理
───────────────────────────────────────────────────────
姜某某(女,高三(08)) | 聚类:群集0
概述: 成绩中等偏下且极不稳定,呈轻微下滑趋势;文科略优于理科。考勤异常较多,日均消费极低,需重点关注其身心健康、经济状况及学习状态。
学业: 均分61.0处于中下游,极差125与变异系数0.48表明成绩极不稳定。文科(61.2)略优于理科(57.0)。斜率-2.80显示成绩呈轻微下滑趋势,高三复习期需警惕基础不牢导致的进一步滑坡。
行为: 考勤异常12次反映纪律松懈或存在厌学、逃避心理。日均消费仅8元远低于正常水平,提示潜在的经济困难、过度节食或校外就餐问题。活跃98天表明在校时间长,但学习专注度与有效性存疑。
风险: 成绩持续下滑与高考失利风险, 营养不良及潜在的心理或经济危机
优势: 文科相对较好,具备提分潜力, 在校活跃度高,未脱离学校环境,便于及时干预
建议: 立即开展谈心与家访,排查考勤异常原因并核实低消费情况,必要时提供经济资助或心理疏导, 制定“稳文补理”策略,强化理科基础训练,建立错题本以减小成绩波动,遏制下滑趋势
───────────────────────────────────────────────────────
徐某某(女,高三(09)) | 聚类:群集0
概述: 该生属中上游文科偏好型,成绩较好但波动大且微降。生活节俭,偶有考勤异常,需重点关注理科短板与高三心理状态。
学业: 成绩居年级中上游(Z值+0.79),文科优势明显(文理差-6.2)。但极差119、变异系数0.47显示成绩极不稳定。高三关键期呈轻微下滑趋势(斜率-1.34),理科薄弱(65.3分)是核心拖累,存在瓶颈期效应。
行为: 日均消费10.1元处于低位,生活节俭但需防范饮食不规律。考勤异常3次,结合成绩下滑,折射出高三高压下可能出现阶段性倦怠、作息紊乱或心理焦虑,在校专注度偶有下降。
风险: 理科短板与高变异系数(0.47)易导致大考发挥失常, 成绩下滑叠加考勤异常与低消费,潜藏心理焦虑或健康隐患
优势: 文科基础扎实,具备显著的学科比较优势, 整体学业基本盘稳固(Z值+0.79),具备冲刺更高目标的潜力
建议: 实施'稳文补理'策略,针对理科进行模块化专项训练以降低成绩波动, 班主任与心理教师联合介入,排查低消费与考勤异常原因,疏导高三备考压力
───────────────────────────────────────────────────────
陈某某(男,白-高二(05)) | 聚类:群集1
概述: 该生为高二中等生,理科略优,成绩整体微升但单次波动极大。出勤纪律极佳,生活节俭。需重点解决成绩不稳定问题,并关注其心理状态。
学业: 均分51.9(Z值+0.02)处于年级中游,理科(52.7)略优于文科(48.0)。整体趋势轻微上升(斜率+2.13),但极差112与变异系数0.64暴露出成绩极度不稳定,知识体系存在明显漏洞,受试题难度影响大。
行为: 考勤0异常且活跃110天,表明该生纪律性强、在校参与度高,属于踏实守纪型学生。日均消费仅7元,生活极为节俭,性格可能偏内向,未见违纪或不良行为倾向。
风险: 成绩高波动风险:极差112与高变异系数显示知识掌握不系统,大考易发挥失常。, 文科短板与隐性心理风险:文科均分偏低易拖累总分;极低消费与守纪特征可能伴随社交孤立或隐性压力。
优势: 行为规范与自律性:考勤零异常,在校活跃度高,具备极强的规则意识和踏实的学习态度。, 理科潜力与微升趋势:理科表现优于文科,且学业整体呈轻微上升态势,具备进一步提分的基础。
建议: 实施错题归因与基础加固:针对高极差现象开展深度试卷分析,精准定位并修补知识漏洞,提升考试稳定性。, 优化选科规划与综合关怀:结合理科优势指导选科,制定文科基础保底策略;关注其低消费背后的生活状况,鼓励参与集体活动以拓宽社交。
=== TF-IDF Top20关键词 ===
z值: 0.0845
生活极度节俭: 0.0742
斜率: 0.0715
日均消费极低: 0.0605
79: 0.0527
文理均衡: 0.0518
略优于理科: 0.0499
文科: 0.0499
稳文补理: 0.0469
策略: 0.0469
在校活跃度高: 0.0465
趋势斜率: 0.0458
文理差: 0.0429
生活节俭: 0.0427
立即开展谈心与家访: 0.0309
日均消费仅8元远低于正常水平: 0.0309
极差125与变异系数0: 0.0309
未脱离学校环境: 0.0309
排查考勤异常原因并核实低消费情况: 0.0309
提示潜在的经济困难: 0.0309

六、多策略推荐系统
推荐问题的定义
在教育场景下,个性化推荐的核心问题是:给定一个学生的历史成绩数据,预测他在尚未修读的学科上的表现,从而为学习资源的分配和教学干预的优先级提供数据支撑。
与电商推荐(预测用户对商品的评分)不同,教育推荐有其独特属性:学科之间存在天然关联结构(数学与物理强相关、语文与英语强相关),且推荐结果必须具备可解释性,教师与家长需要清晰知晓 “为何向该学生推荐对应学科辅导资源”。
三种互补策略
本章搭建三套推荐算法,各有侧重,后续融合为混合推荐引擎:
- Item-based 协同过滤:基于学科之间余弦相似度做推荐。优势是学科相似度矩阵固定,无需随学生数据更新重复计算,计算速度快;
- SVD 矩阵分解:对学生 - 学科成绩矩阵做低秩分解,挖掘数理逻辑、语言感知等隐性能力因子,预测精度高;
- LLM + 向量检索 RAG 框架:将 LLM 结构化学生画像存入向量数据库,检索相似学生案例生成带完整解释的推荐方案,可解释性拉满。
6.1 Item-based协同过滤
基于学科间的余弦相似度进行推荐。与User-based CF不同,Item-based CF计算的是学科之间的相似度。学科相似度矩阵相对稳定,不随学生数量变化而重新计算。
# ==== Item-based协同过滤 ====
# 构建学生-学科成绩透视矩阵
rating_matrix = df_chengji.pivot_table(
index='mes_StudentID',
columns='mes_sub_name',
values='score',
aggfunc='mean'
)
print(f'学生-学科评分矩阵:{rating_matrix.shape[0]}学生 × {rating_matrix.shape[1]}学科')
print(f'矩阵稀疏度:{rating_matrix.isna().mean().mean()*100:.1f}%')
# 填充空值用于相似度计算
rating_fill = rating_matrix.fillna(rating_matrix.mean())
# 计算学科(Item)余弦相似度
item_similar = cosine_similarity(rating_fill.T)
item_sim_df = pd.DataFrame(
item_similar,
index=rating_matrix.columns,
columns=rating_matrix.columns
)
# 单学生单学科分数预测函数
def item_cf_predict(student_id, target_subject, topk=5):
if student_id not in rating_matrix.index:
return np.nan
student_score_series = rating_matrix.loc[student_id].dropna()
if target_subject not in item_sim_df.columns:
return np.nan
# 获取目标学科与其他学科相似度
sub_sim = item_sim_df[target_subject].loc[student_score_series.index]
# 取相似度最高topk门已学学科加权预测
top_sim_sub = sub_sim.nlargest(topk)
weight_sum = top_sim_sub.sum()
if weight_sum <= 0:
return np.nan
score_sum = 0
for sub, sim in top_sim_sub.items():
score_sum += sim * student_score_series[sub]
return score_sum / weight_sum
# 生成推荐列表
def item_cf_recommend(student_id, rec_num=3):
if student_id not in rating_matrix.index:
return []
student_all_sub = rating_matrix.columns.tolist()
student_learned = rating_matrix.loc[student_id].dropna().index.tolist()
student_unlearn = [s for s in student_all_sub if s not in student_learned]
pred_result = {}
for sub in student_unlearn:
pred_score = item_cf_predict(student_id, sub)
if pd.notna(pred_score):
pred_result[sub] = pred_score
# 预测分数从高到低排序返回
sort_rec = sorted(pred_result.items(), key=lambda x:x[1], reverse=True)
return sort_rec[:rec_num]
# 测试示例
demo_student_id = rating_matrix.index[10]
rec_list = item_cf_recommend(demo_student_id)
print(f'\n学生{demo_student_id} Item-CF推荐学科列表:')
for sub, pred_sc in rec_list:
print(f' {sub},预测均分:{pred_sc:.1f}')
# 学科相似度热力图
fig, ax = plt.subplots(figsize=(10, 8))
sns.heatmap(item_sim_df, annot=True, fmt='.2f', cmap='coolwarm', square=True, linewidths=0.4)
ax.set_title('学科间相似度矩阵(Item-CF)', fontproperties=zh_font, fontsize=14)
ax.set_xticklabels(ax.get_xticklabels(), fontproperties=zh_font, rotation=45)
ax.set_yticklabels(ax.get_yticklabels(), fontproperties=zh_font)
plt.tight_layout()
plt.savefig('./output/ch6_item_sim.png', bbox_inches='tight')
plt.show()
运行结果:
评分矩阵: 3862学生 × 16学科
稀疏度: 23.1%
学生10853的Item-CF推荐:
物理: 预测88.8分
化学: 预测88.7分
生物: 预测88.6分

6.2 SVD矩阵分解
对中心化学生 - 学科矩阵做奇异值分解,提取低维隐性特征向量,大幅提升分数预测精度。
# ==== SVD矩阵分解 ====
# 全局平均分
global_avg_score = rating_matrix.values[~np.isnan(rating_matrix.values)].mean()
# 空值填充全局均值
rating_filled = np.nan_to_num(rating_matrix.values, nan=global_avg_score)
# 行中心化(消除学生整体水平偏差)
row_mean = rating_filled.mean(axis=1, keepdims=True)
rating_centered = rating_filled - row_mean
# SVD分解,设定5维隐性因子
latent_dim = 5
U, sigma, Vt = svds(rating_centered, k=latent_dim)
sigma_diag = np.diag(sigma)
# 重构预测矩阵
pred_matrix = U @ sigma_diag @ Vt + row_mean
pred_df = pd.DataFrame(pred_matrix, index=rating_matrix.index, columns=rating_matrix.columns)
# 单学生推荐函数
def svd_recommend(student_id, rec_num=3):
if student_id not in pred_df.index:
return []
student_learned = rating_matrix.loc[student_id].dropna().index
all_subject = pred_df.columns
unlearn_sub = [s for s in all_subject if s not in student_learned]
pred_dict = {sub: pred_df.loc[student_id, sub] for sub in unlearn_sub}
sort_rec = sorted(pred_dict.items(), key=lambda x:x[1], reverse=True)
return sort_rec[:rec_num]
# 测试推荐
svd_rec = svd_recommend(demo_student_id)
print(f'\n学生{demo_student_id} SVD推荐列表:')
for sub, score in svd_rec:
print(f' {sub},预测分数:{score:.1f}')
# 可视化:不同隐向量维度下重构误差
dim_range = [2,3,4,5,6,7,8]
rmse_record = []
mask_valid = ~np.isnan(rating_matrix.values)
for d in dim_range:
u, s, vt = svds(rating_centered, k=d)
s_diag = np.diag(s)
rebuild = u @ s_diag @ vt + row_mean
error = np.sqrt(np.mean((rating_filled[mask_valid] - rebuild[mask_valid])**2))
rmse_record.append(error)
fig, axes = plt.subplots(1, 2, figsize=(14,5))
ax1 = axes[0]
ax1.plot(dim_range, rmse_record, 'o-', color='#e15759', lw=2)
ax1.set_xlabel('SVD隐性因子维度k', fontproperties=zh_font)
ax1.set_ylabel('重构RMSE误差', fontproperties=zh_font)
ax1.set_title('隐向量维度与预测误差关系', fontproperties=zh_font)
ax1.grid(alpha=0.3)
# 学科二维隐因子可视化(前两维)
ax2 = axes[1]
subject_vec = Vt.T
ax2.scatter(subject_vec[:,0], subject_vec[:,1], s=90, color='#4e79a7', edgecolor='white')
for idx, sub in enumerate(rating_matrix.columns):
ax2.annotate(sub, (subject_vec[idx,0], subject_vec[idx,1]), fontproperties=zh_font, fontsize=8)
ax2.set_xlabel('隐因子1', fontproperties=zh_font)
ax2.set_ylabel('隐因子2', fontproperties=zh_font)
ax2.set_title('学科隐性能力因子分布', fontproperties=zh_font)
plt.tight_layout()
plt.savefig('./output/ch6_svd_error_vec.png', bbox_inches='tight')
plt.show()
# 计算SVD整体预测RMSE用于后续对比
full_pred = U @ sigma_diag @ Vt + row_mean
valid_mask = ~np.isnan(rating_matrix.values)
svd_rmse = np.sqrt(np.mean((rating_filled[valid_mask] - full_pred[valid_mask])**2))
print(f'\nSVD k=5 整体预测RMSE:{svd_rmse:.2f}')
运行结果:
SVD矩阵分解(k=5):
重构RMSE: 4.42
潜在因子: 5维
学生10853的SVD推荐:
音乐: 预测75.3分
美术: 预测68.8分
技术: 预测68.5分

6.3 LLM+向量检索增强推荐(RAG框架)
将批量生成的 LLM 学生画像存入 Chroma 向量数据库,支持按学生特征语义检索相似学生,结合相似学生学科表现生成带完整解释的可解释推荐。
# ==== RAG向量检索融合推荐 ====
# 初始化向量库
chroma_client = chromadb.Client()
# 若集合存在先删除,避免重复数据
try:
chroma_client.delete_collection(name="student_portrait_store")
except:
pass
portrait_collection = chroma_client.create_collection(
name="student_portrait_store",
metadata={"hnsw:space": "cosine"}
)
# 写入向量库
doc_text_list = []
id_list = []
meta_info = []
for idx, portrait in llm_portraits.items():
student_row = master.loc[idx]
combine_text = f"{portrait['summary']} {portrait['academic']} {portrait['behavior']}"
doc_text_list.append(combine_text)
id_list.append(f"stu_{idx}")
meta_info.append({
"name": student_row['name'],
"cluster": int(student_row['cluster']),
"trend": student_row.get('trend_label','无'),
"sid": int(idx)
})
portrait_collection.add(
documents=doc_text_list,
ids=id_list,
metadatas=meta_info
)
print(f"向量数据库写入完成,共{len(doc_text_list)}条学生画像向量")
# RAG检索+大模型生成推荐方案
def rag_generate_recommend(student_index, retrieve_top=3):
target_stu = master.loc[student_index]
target_portrait = llm_portraits[student_index]
target_text = build_data_text(target_stu)
# 向量检索相似学生
query_text = f"学生画像:{target_portrait['summary']} {target_portrait['academic']}"
search_res = portrait_collection.query(
query_texts=[query_text],
n_results=retrieve_top
)
# 拼接检索到的相似学生案例上下文
case_context = ""
for rank in range(retrieve_top):
case_name = search_res['metadatas'][0][rank]['name']
case_cluster = search_res['metadatas'][0][rank]['cluster']
case_text = search_res['documents'][0][rank]
case_context += f"【相似学生案例{rank+1}:{case_name},聚类群集{case_cluster}】画像描述:{case_text}\n"
prompt = (
f"当前待分析学生基础信息:{target_text}\n"
f"检索到3名特征高度相似学生的画像案例:\n{case_context}\n"
"结合学生自身成绩、行为、学业趋势,以及相似学生的学习发展情况,输出JSON推荐方案:\n"
'{"resource_rec": ["推荐辅导学科1","推荐辅导学科2"], "reason": "完整可解释推荐依据,结合相似学生表现分析", "study_suggest": ["长期学习规划1","长期学习规划2"]}'
"仅输出JSON,无多余文字。"
)
if llm_client is None:
return {"resource_rec":[],"reason":"无大模型无法生成","study_suggest":[]}
try:
resp = llm_client.chat.completions.create(
model=MODEL_ID,
messages=[
{"role":"system","content":"你是中学智能学习推荐专家,基于学生画像与同类学生案例生成可解释学科辅导推荐。"},
{"role":"user","content":prompt}
],
temperature=0.3,
response_format={"type":"json_object"}
)
return json.loads(resp.choices[0].message.content.strip())
except Exception as e:
print(f"RAG推荐生成失败:{e}")
return {"resource_rec":[],"reason":"生成异常","study_suggest":[]}
# 测试RAG推荐
test_student_idx = list(llm_portraits.keys())[0]
rag_result = rag_generate_recommend(test_student_idx)
print(f"\nRAG语义融合推荐结果:")
print(f"推荐辅导学科:{'、'.join(rag_result['resource_rec'])}")
print(f"推荐依据:{rag_result['reason']}")
print(f"学习规划建议:{'、'.join(rag_result['study_suggest'])}")
# 绘制系统架构流程图
fig, ax = plt.subplots(figsize=(14, 6))
ax.set_xlim(0, 12)
ax.set_ylim(0, 5)
ax.axis('off')
ax.set_title('RAG混合智能推荐系统整体架构', fontproperties=zh_font, fontsize=15, weight='bold')
# 模块坐标、文字、配色
module_config = [
(1.5, 3.5, "多源学生原始数据", "#4e79a7"),
(1.5, 1.5, "LLM多维学生画像生成", "#59a14f"),
(4.5, 2.5, "Chroma向量数据库", "#e15759"),
(7.5, 3.5, "Item-CF协同过滤\n数值预测模块", "#76b7b2"),
(7.5, 1.5, "SVD矩阵分解\n隐性能力预测模块", "#f28e2b"),
(10.5, 2.5, "大模型RAG融合推理\n可解释推荐报告输出", "#b07aa1")
]
# 绘制模块矩形
rect_list = []
for x, y, text, color in module_config:
rect = plt.Rectangle((x-1, y-0.6), 2, 1.2, facecolor=color, alpha=0.35, edgecolor=color, linewidth=2)
ax.add_patch(rect)
ax.text(x, y, text, ha='center', va='center', fontproperties=zh_font, fontsize=10, weight='medium')
rect_list.append((x,y))
# 绘制连接线
connect_lines = [
(rect_list[0], rect_list[1]),
(rect_list[1], rect_list[2]),
(rect_list[2], rect_list[5]),
(rect_list[0], rect_list[3]),
(rect_list[0], rect_list[4]),
(rect_list[3], rect_list[5]),
(rect_list[4], rect_list[5])
]
for (x1,y1),(x2,y2) in connect_lines:
ax.annotate("", xy=(x2, y2), xytext=(x1, y1), arrowprops=dict(arrowstyle="->", color="#333333", lw=1.5))
plt.tight_layout()
plt.savefig('./output/ch6_rag_frame.png', bbox_inches='tight')
plt.show()
运行结果:
向量库: 6份画像已索引
=======================================================
检索到3份相关画像:
[1] 陈某某(群集1, 趋势:轻微波动, 相似度0.674)
[2] 林某某(群集1, 趋势:轻微波动, 相似度0.618)
[3] 钱某某(群集0, 趋势:轻微波动, 相似度0.617)
RAG推荐分析: 该高二学生面临“成绩波动大”与“偏科严重”两大核心问题。结合案例特征,成绩极度不稳定通常源于知识体系存在明显漏洞、受试题难度影响大或陷入“高耗时低产出”的学习瓶颈;偏科则表明文理学科发展失衡。此外,高二阶段学业压力增大,成绩起伏易引发心理波动,且需警惕潜在的考勤或心理隐患。因此,干预策略需聚焦于知识漏洞修补、弱势学科强化、学习效率提升及心理状态疏导。
推荐方案:
→ 开展精准学情诊断与知识漏洞修补:利用错题本和知识图谱定位导致成绩波动的核心薄弱点,进行专项突破与限时训练,降低试题难度变化对成绩的影响,提升考试状态的稳定性。
→ 制定弱势学科专项提升与时间管理计划:针对偏科问题,重新规划文理学习时间分配,采用“保优势、补弱势”策略,从弱势学科的基础知识抓起,逐步缩小文理分差,促进学科均衡发展。
→ 优化学习方法与加强心理生活关怀:指导学生改进学习策略,提高课堂与自习效率,打破“高耗时低产出”瓶颈;同时密切关注其心理状态、考勤及生活情况,及时疏导高二学业压力,提供必要的心理支持。

七、推荐效果量化评估
评估方法论
推荐系统的评估不能仅依赖主观感受或个案展示,需要建立系统的量化评估框架。本章从三个维度对推荐策略进行评测:
评估指标
-
RMSE(均方根误差): 衡量预测分数与实际分数之间的偏差。我们采用留出法:从每个学生的已知成绩中随机抽取2门作为测试集,剩余学科用于训练模型,然后比较模型预测值与测试集真实值的RMSE。RMSE越低,说明预测越准确。
-
NDCG@K(归一化折损累积增益): 这是信息检索领域的标准指标,衡量推荐列表的排序质量。核心思想是:真正优秀的学科应该排在推荐列表的前面。NDCG值为1表示推荐排序与理想排序完全一致。
-
Diversity@K(推荐多样性): 计算推荐列表中各学科之间的平均不相似度(1 - 学科间余弦相似度)。多样性高的推荐列表能避免"信息茧房"效应,鼓励学生探索不同学科方向。
对比基线
我们将Item-CF和SVD的推荐结果与一个简单基线进行对比:用全体学生的全局均分作为所有预测值。如果一个复杂方法的RMSE显著低于基线,说明该方法确实捕获了有意义的个性化信息。
7.1 评估指标计算
采用留出法划分测试样本:随机抽取 200 名学生,每人随机遮蔽 2 门已修学科分数作为测试真值,分别使用 Item-CF、SVD、全局均值基线做分数预测,计算 RMSE 误差;同时计算 NDCG@3 排序指标、推荐多样性 Diversity 指标。
# ==== 模型量化评估 ====
np.random.seed(2024)
test_sample_num = 200
test_student_ids = np.random.choice(rating_matrix.index, size=test_sample_num, replace=False)
test_pairs = []
global_score_avg = rating_matrix.values[~np.isnan(rating_matrix.values)].mean()
# 构造测试样本:遮蔽两门学科作为真实标签
for sid in test_student_ids:
score_series = rating_matrix.loc[sid].dropna()
if len(score_series) < 5:
continue
hide_subjects = score_series.sample(2, random_state=42).index.tolist()
for sub in hide_subjects:
true_score = score_series[sub]
test_pairs.append((sid, sub, true_score))
test_df = pd.DataFrame(test_pairs, columns=['sid','subject','true_score'])
print(f"评测测试集总样本量:{len(test_df)}条")
# 批量预测三种方案分数
def calc_rmse(pred_col, true_col):
valid_mask = ~np.isnan(pred_col)
pred = pred_col[valid_mask]
true = true_col[valid_mask]
return np.sqrt(np.mean((pred - true)**2))
# 全局均值基线预测
test_df['pred_baseline'] = global_score_avg
# Item-CF预测
test_df['pred_itemcf'] = test_df.apply(lambda row: item_cf_predict(row['sid'], row['subject']), axis=1)
# SVD预测
test_df['pred_svd'] = test_df.apply(lambda row: pred_df.loc[row['sid'], row['subject']], axis=1)
rmse_baseline = calc_rmse(test_df['pred_baseline'], test_df['true_score'])
rmse_itemcf = calc_rmse(test_df['pred_itemcf'], test_df['true_score'])
rmse_svd = calc_rmse(test_df['pred_svd'], test_df['true_score'])
print(f"\n=== 各算法RMSE预测误差对比 ===")
print(f"全局均值基线:{rmse_baseline:.2f}")
print(f"Item-based协同过滤:{rmse_itemcf:.2f}")
print(f"SVD矩阵分解:{rmse_svd:.2f}")
# NDCG@3与多样性计算
def ndcg_at_k(student_id, k=3, alg_type="svd"):
if alg_type == "svd":
rec_list = svd_recommend(student_id, rec_num=k)
elif alg_type == "itemcf":
rec_list = item_cf_recommend(student_id, rec_num=k)
else:
return 0
if len(rec_list) == 0:
return 0
# 真实分数作为相关性标签
rel_score = []
for sub, _ in rec_list:
if sub in rating_matrix.loc[student_id].index:
rel_score.append(rating_matrix.loc[student_id, sub])
else:
rel_score.append(0)
dcg = sum([rel_score[i] / np.log2(i+2) for i in range(len(rel_score))])
ideal_rel = sorted(rel_score, reverse=True)
idcg = sum([ideal_rel[i] / np.log2(i+2) for i in range(len(ideal_rel))])
return dcg / idcg if idcg > 0 else 0
# 多样性指标:推荐学科之间平均相似度越低,多样性越高
def diversity_calc(student_id, k=3):
rec_subs = [s[0] for s in svd_recommend(student_id, rec_num=k)]
if len(rec_subs) < 2:
return 0
sim_sum = 0
pair_cnt = 0
for i in range(len(rec_subs)):
for j in range(i+1, len(rec_subs)):
sim_sum += item_sim_df.loc[rec_subs[i], rec_subs[j]]
pair_cnt += 1
avg_sim = sim_sum / pair_cnt
return 1 - avg_sim
# 批量计算平均NDCG、多样性
ndcg_itemcf_list = []
ndcg_svd_list = []
div_list = []
for sid in test_student_ids[:100]:
ndcg_itemcf_list.append(ndcg_at_k(sid, alg_type="itemcf"))
ndcg_svd_list.append(ndcg_at_k(sid, alg_type="svd"))
div_list.append(diversity_calc(sid))
avg_ndcg_itemcf = np.mean(ndcg_itemcf_list)
avg_ndcg_svd = np.mean(ndcg_svd_list)
avg_diversity = np.mean(div_list)
print(f"\n=== 排序与多样性指标 ===")
print(f"Item-CF NDCG@3均值:{avg_ndcg_itemcf:.3f}")
print(f"SVD NDCG@3均值:{avg_ndcg_svd:.3f}")
print(f"SVD推荐平均多样性:{avg_diversity:.3f}")
# 多指标对比可视化
fig, axes = plt.subplots(1,2, figsize=(14,5))
ax1 = axes[0]
alg_name = ["全局均值基线","Item-CF","SVD矩阵分解"]
rmse_value = [rmse_baseline, rmse_itemcf, rmse_svd]
bar_color = ["#bab0ac","#4e79a7","#e15759"]
bars = ax1.bar(alg_name, rmse_value, color=bar_color, edgecolor='white')
for bar, val in zip(bars, rmse_value):
ax1.text(bar.get_x()+bar.get_width()/2, bar.get_height()+0.2, f"{val:.2f}", ha='center', fontproperties=zh_font)
ax1.set_ylabel("RMSE预测误差(越低越好)", fontproperties=zh_font)
ax1.set_title("三种算法预测误差对比", fontproperties=zh_font)
ax2 = axes[1]
ndcg_data = [avg_ndcg_itemcf, avg_ndcg_svd]
ndcg_name = ["Item-CF NDCG@3","SVD NDCG@3"]
bars2 = ax2.bar(ndcg_name, ndcg_data, color=["#76b7b2","#59a14f"], edgecolor='white')
for bar, val in zip(bars2, ndcg_data):
ax2.text(bar.get_x()+bar.get_width()/2, bar.get_height()+0.01, f"{val:.3f}", ha='center', fontproperties=zh_font)
ax2.set_ylabel("NDCG@3(越高越好)", fontproperties=zh_font)
ax2.set_title("推荐排序质量对比", fontproperties=zh_font)
plt.tight_layout()
plt.savefig('./output/ch7_metric_compare.png', bbox_inches='tight')
plt.show()
# 汇总对比表格
eval_summary = pd.DataFrame({
"评价指标":["RMSE分数预测误差","平均NDCG@3","推荐平均多样性","可解释性","计算速度"],
"全局基线":[f"{rmse_baseline:.2f}","-","-","极低","极快"],
"Item-CF协同过滤":[f"{rmse_itemcf:.2f}",f"{avg_ndcg_itemcf:.3f}",f"{avg_diversity:.3f}","中等","快"],
"SVD矩阵分解":[f"{rmse_svd:.2f}",f"{avg_ndcg_svd:.3f}",f"{avg_diversity:.3f}","低","中等"],
"LLM-RAG混合推荐":["无数值预测","-",f"{avg_diversity:.3f}","极高","慢"]
})
print("\n=== 全方案综合评测汇总表 ===")
print(eval_summary.to_string(index=False))
运行输出:
评测测试集总样本量:364条
=== 各算法RMSE预测误差对比 ===
全局均值基线:26.43
Item-based协同过滤:18.95
SVD矩阵分解:4.42
=== 排序与多样性指标 ===
Item-CF NDCG@3均值:0.614
SVD NDCG@3均值:0.782
SVD推荐平均多样性:0.641
=== 全方案综合评测汇总表 ===
评价指标 全局基线 Item-CF协同过滤 SVD矩阵分解 LLM-RAG混合推荐
RMSE分数预测误差 26.43 18.95 4.42 无数值预测
平均NDCG@3 - 0.614 0.782 -
推荐平均多样性 - 0.641 0.641 0.641
可解释性 极低 中等 低 极高
计算速度 极快 快 中等 慢

7.2 多方法对比总结
将Item-CF、SVD和全局均值基线的推荐结果进行系统对比,从RMSE、NDCG、多样性、冷启动处理、语义理解和可解释性六个维度评估各方法的优劣。
- 预测精度:SVD 矩阵分解误差远低于协同过滤与全局均值基线,依靠隐性能力因子捕捉学生内在学科潜力,数值预测效果最优;
- 排序质量:SVD 的 NDCG@3 显著高于 Item-CF,生成的推荐列表更贴合学生真实学科潜力;
- 多样性:两套数值推荐算法多样性指标一致,推荐学科覆盖范围均衡,不会高度集中同类科目;
- 可解释性短板:Item-CF、SVD 仅输出预测分数,无法说明推荐背后学生综合特征;LLM-RAG 弥补该缺陷,依托学生画像与相似案例给出完整文字推理依据,适配教师教学使用场景;
- 工程落地融合思路:线上服务采用分层混合方案,日常快速推荐使用 Item-CF 保证响应速度;需要精准学情诊断时调用 SVD;面向家长、班主任输出报告时启用 RAG 大模型生成可解释推荐方案。
八、总结与展望
8.1 核心发现
本研究围绕"数据驱动的学生画像构建与个性化推荐"这一主题,从方法创新、系统设计、效果验证三个层面开展了探索,取得了以下核心发现:
| 研究维度 | 方法 | 核心结论 |
|---|---|---|
| 学生分群 | K-Means + PCA | 无监督聚类在PCA降维后的特征空间中自动发现了多个具有显著差异的学生群体。与Z-Score规则分箱相比,聚类能够识别出"高均分但高波动""文理均衡但消费异常"等规则方法难以捕捉的复合模式。 |
| 趋势分析 | Mann-Kendall检验 | 约15%的学生存在统计显著的上升或下降趋势。这一发现表明,仅依赖静态均分构建画像会遗漏重要的成长性信息。时序分析为画像增加了"时间"这一关键维度。 |
| 推荐精度 | SVD矩阵分解 | 5维潜在因子即可将RMSE降低至优于Item-CF和全局均值基线的水平。SVD捕获了学科间非显式的隐含关联结构,如"数理逻辑因子"同时影响数学、物理和技术学科的表现。 |
| 画像语义 | LLM + TF-IDF | LLM生成的画像在语义信息量上远超规则标签的简单拼接。TF-IDF分析验证了画像关键词与教育领域的高度相关性,t-SNE可视化显示不同群集的画像在语义空间中自然分离。 |
| 推荐框架 | RAG | 检索增强生成框架成功地将画像语义与推荐推理结合,生成了带有案例佐证和原因分析的可解释推荐报告,适合面向教师和家长的实际使用场景。 |
8.2 技术路线
本研究的技术路线遵循"从群体到个体、从静态到动态、从数值到语义"的递进逻辑。聚类分析首先揭示群体结构,时序分析补充个体动态,LLM将数值特征翻译为自然语言画像,最后RAG框架将画像语义融入推荐推理。
8.3 方法论反思
- 聚类效果依赖完整多维度数据,消费、考勤记录缺失较多的学生样本会降低分群精准度;
- LLM 调用存在接口延迟与调用成本,大批量离线生成画像可行,高频实时场景需做缓存优化;
- SVD 仅建模成绩数据,未融合行为、时序特征,单独使用缺少学生综合视角,必须搭配画像系统才能完整支撑教学决策。
8.4 未来拓展方向
- 引入时序深度学习模型(LSTM/Transformer)对学生成绩序列建模,预测未来多学期分数变化;
- 微调领域教育专用大模型,替代通用大模型,进一步提升画像、推荐内容专业度;
- 构建在线实时数据流 Pipeline,接入实时考勤、考试数据,动态更新学生聚类标签与画像;
- 开发前端可视化平台,将聚类分群、学生趋势、推荐结果封装可视化大屏,直接交付学校教务使用。
参考文献
-
MacQueen J. Some methods for classification and analysis of multivariate observations. Berkeley Symposium on Mathematical Statistics and Probability, 1967.
-
Koren Y., Bell R., Volinsky C. Matrix Factorization Techniques for Recommender Systems. Computer, 42(8), 2009.
-
Mann H.B. Nonparametric tests against trend. Annals of Mathematical Statistics, 16(1): 106-116, 1945.
-
Lewis P. et al. Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks. NeurIPS, 2020.
-
Touvron H. et al. LLaMA: Open and Efficient Foundation Language Models. arXiv, 2023.
-
Sarwar B. et al. Item-based Collaborative Filtering Recommendation Algorithms. WWW, 2001.
更多推荐



所有评论(0)