欢迎光临
我们一直在努力

从成绩宽表到学生画像:用 Python 挖出“隐形偏科生”的完整实战

目录

  • 1. 项目背景
  • 2. 技术栈与数据说明
  • 3. 整体分析流程
  • 4. 数据清洗:只保留有效成绩
  • 5. 构建学生画像指标体系
  • 6. 画像标签生成:把数值变成可解释标签
  • 7. 偏科识别:不能只看总分,要看学科差异
  • 8. K-Means 聚类:划分典型学生群体
  • 9. 偏科与非偏科学生对比
  • 10. 用 Cohen’s d 展示组间差异
  • 11. 可视化设计
    • 11.1 学生群体画像热力图
    • 11.2 PCA 聚类与偏科风险二维分布图
    • 11.3 学业水平—学科差异偏科风险矩阵
    • 11.4 弱势学科与群体分布热力图
  • 12. 随机森林解释:哪些因素与偏科风险更相关
  • 13. 主程序整合
  • 14. 结果总结
  • 15. 项目亮点
  • 16. 结语

1. 项目背景

在教育数据分析场景中,单纯看总分或平均分往往不够。一个学生总成绩不低,并不代表各科发展均衡;同样,一个学生成绩波动较大,也可能和消费、早餐、考勤、教师变动等行为特征存在一定关联。

本项目围绕“学生画像 + 偏科识别”展开,目标是基于学生成绩明细宽表和学生级行为特征表,为每位学生生成多维度画像,并进一步识别偏科风险学生。

项目主要完成两个任务:

  • 构建学生画像 从基本信息、成绩水平、成绩稳定性、学科均衡性、消费水平、早餐习惯、考勤可靠性、教师关联复杂度等维度刻画学生。

  • 识别偏科学生并总结特点 基于各学科标准分差异,构造偏科指标,识别偏科风险学生,并对比偏科与非偏科学生的差异。

  • 最终分析结果显示,有效学生样本共 1566 人,其中偏科风险学生 760 人,占比 48.53%。偏科风险学生主要表现为弱势学科数量较多、标准分波动较大、学科极差较高,弱势学科集中在英语、化学、物理等科目。


    2. 技术栈与数据说明

    本项目使用 Python 完成数据处理、建模和可视化。

    模块技术
    数据处理 pandas、numpy
    特征标准化 StandardScaler
    学生聚类 MiniBatchKMeans
    降维展示 PCA
    特征解释 RandomForestClassifier
    可视化 matplotlib

    项目目录结构如下:

    student_b_project/
    ├─ data/
    │ ├─ analysis_wide_detail.csv # 明细宽表:学生 × 考试 × 学科
    │ └─ analysis_wide_modeling.csv # 学生级行为表:消费、早餐、迟到等
    ├─ student_b_pipeline.py # 主流程:清洗、画像、偏科识别、聚类
    ├─ advanced_student_b_charts.py # 高级可视化脚本
    └─ output_student_b/
    ├─ 02_student_profile_cluster_bias_result.csv
    ├─ 03_cluster_center_summary.csv
    ├─ 05_partial_vs_nonpartial_numeric.csv
    ├─ 07_partial_feature_importance.csv
    └─ advanced_figures/


    3. 整体分析流程

    整个任务可以拆成八个步骤: 在这里插入图片描述

    最关键的一点是:成绩明细表是一名学生在不同考试、不同学科下的多行记录,不能直接拿来做学生画像,必须先聚合成“一人一行”的学生级特征表。


    4. 数据清洗:只保留有效成绩

    原始成绩中可能存在缺考、异常编码或无效成绩。如果直接使用这些数据,会影响平均分、标准分和偏科指标的计算。因此,代码中只保留 score_status == "正常" 的记录,并使用 score_clean 和 mes_Z_Score 作为主要成绩字段。

    def clean_detail(detail):
    """
    清洗成绩明细表:
    1. 检查必要字段是否存在;
    2. 将成绩字段转换为数值;
    3. 只保留正常成绩;
    4. 去除学生ID、学科、成绩、标准分为空的记录。
    """

    need = ["student_id", "mes_sub_name", "score_clean", "mes_Z_Score"]
    miss = [c for c in need if c not in detail.columns]
    if miss:
    raise ValueError(f"明细宽表缺少必要字段:{miss}")

    detail = detail.copy()

    detail = to_num(detail, [
    "student_id", "mes_Score", "score_clean", "mes_Z_Score",
    "mes_T_Score", "mes_dengdi", "age", "exam_number", "mes_TestID"
    ])

    for c in detail.columns:
    if detail[c].dtype == "object":
    detail[c] = detail[c].astype("string").str.strip()

    valid = detail.copy()

    # 核心处理:只保留正常成绩
    if "score_status" in valid.columns:
    valid = valid[valid["score_status"] == "正常"]

    valid = valid[
    valid["student_id"].notna()
    & valid["mes_sub_name"].notna()
    & valid["score_clean"].notna()
    & valid["mes_Z_Score"].notna()
    & (valid["score_clean"] >= 0)
    ].copy()

    print("有效成绩记录数:", len(valid))
    print("有效学生数:", valid["student_id"].nunique())
    print("有效学科数:", valid["mes_sub_name"].nunique())

    return detail, valid

    这样做的好处是:后续所有成绩指标都建立在有效成绩基础上,避免缺考、异常值或无效编码干扰分析结果。


    5. 构建学生画像指标体系

    学生画像不是单一字段,而是一组能够描述学生状态的标签和指标。

    画像维度指标含义
    成绩水平 avg_z_score 学生整体学业表现
    成绩稳定性 score_std、z_score_std 成绩波动程度
    学科均衡性 subject_z_range、subject_z_std 各学科发展是否均衡
    行为特征 total_consume、breakfast_count、late_count 消费、早餐、考勤情况
    教师关联 teacher_count 任课教师关联复杂度

    核心聚合代码如下:

    def build_student_profile(detail, valid):
    """
    将成绩明细表聚合为学生画像基础表。
    输入:
    detail:原始成绩明细表
    valid:清洗后的有效成绩表
    输出:
    profile:一人一行的学生画像表
    """

    basic_cols = [
    "bf_Name", "bf_sex", "bf_nation", "bf_BornDate", "cla_Name",
    "bf_NativePlace", "Bf_ResidenceType", "bf_policy", "cla_id",
    "cla_term", "bf_zhusu", "bf_leaveSchool", "bf_qinshihao",
    "campus", "age"
    ]
    basic_cols = [c for c in basic_cols if c in detail.columns]

    # 只从有有效成绩的学生中生成基础画像
    if basic_cols:
    basic = valid.groupby("student_id")[basic_cols].agg(first_not_null).reset_index()
    else:
    basic = pd.DataFrame({"student_id": valid["student_id"].unique()})

    agg = valid.groupby("student_id").agg(
    avg_score=("score_clean", "mean"),
    score_std=("score_clean", "std"),
    max_score=("score_clean", "max"),
    min_score=("score_clean", "min"),
    avg_z_score=("mes_Z_Score", "mean"),
    z_score_std=("mes_Z_Score", "std"),
    max_z_score=("mes_Z_Score", "max"),
    min_z_score=("mes_Z_Score", "min"),
    valid_score_count=("score_clean", "count"),
    subject_count=("mes_sub_name", nunique_not_null)
    ).reset_index()

    if "exam_number" in valid.columns:
    exam_count = valid.groupby("student_id").agg(
    exam_count=("exam_number", nunique_not_null)
    ).reset_index()
    agg = agg.merge(exam_count, on="student_id", how="left")

    if "bas_Name" in valid.columns:
    teacher = valid.groupby("student_id").agg(
    teacher_count=("bas_Name", nunique_not_null),
    teacher_list=("bas_Name", unique_join)
    ).reset_index()
    agg = agg.merge(teacher, on="student_id", how="left")

    if "score_status" in detail.columns:
    tmp = detail.copy()
    tmp["abnormal_score_flag"] = np.where(tmp["score_status"] == "正常", 0, 1)

    abnormal = tmp.groupby("student_id").agg(
    total_record_count=("student_id", "count"),
    abnormal_score_count=("abnormal_score_flag", "sum")
    ).reset_index()

    abnormal["normal_rate"] = 1 abnormal["abnormal_score_count"] / abnormal["total_record_count"]
    agg = agg.merge(abnormal, on="student_id", how="left")

    profile = basic.merge(agg, on="student_id", how="left")
    profile[["score_std", "z_score_std"]] = profile[["score_std", "z_score_std"]].fillna(0)

    return profile


    6. 画像标签生成:把数值变成可解释标签

    只有数值指标还不够直观。为了方便展示和解释,可以将连续指标转换为标签,例如:

    • 学业优势型
    • 良好发展型
    • 中等发展型
    • 学业风险型
    • 成绩稳定型
    • 高波动型
    • 早餐规律型
    • 考勤风险型

    def add_profile_labels(profile):
    """
    根据画像指标生成可解释标签。
    """

    p = profile.copy()

    def academic(v):
    if pd.isna(v):
    return "无数据"
    if v >= 0.8:
    return "学业优势型"
    if v >= 0.3:
    return "良好发展型"
    if v >= 0.3:
    return "中等发展型"
    return "学业风险型"

    p["academic_level"] = p["avg_z_score"].apply(academic)

    p["score_stability_label"] = q3_label(
    p["score_std"],
    "成绩稳定型",
    "一般波动型",
    "高波动型"
    )

    if "subject_z_range" in p.columns:
    p["subject_balance_label"] = q3_label(
    p["subject_z_range"],
    "学科均衡型",
    "轻度差异型",
    "明显偏科风险型"
    )
    else:
    p["subject_balance_label"] = "无数据"

    if "breakfast_count" in p.columns and "consume_count" in p.columns:
    p["breakfast_ratio"] = p["breakfast_count"] / (p["consume_count"] + 1e-6)
    p["breakfast_label"] = q3_label(
    p["breakfast_ratio"],
    "早餐不足型",
    "早餐一般型",
    "早餐规律型"
    )
    else:
    p["breakfast_ratio"] = np.nan
    p["breakfast_label"] = "无数据"

    if "late_count" in p.columns:
    late = pd.to_numeric(p["late_count"], errors="coerce")
    q75 = late.quantile(0.75)

    def att(v):
    if pd.isna(v):
    return "无数据"
    if v == 0:
    return "高可靠型"
    if v <= q75:
    return "一般可靠型"
    return "考勤风险型"

    p["attendance_reliability_label"] = late.apply(att)
    else:
    p["attendance_reliability_label"] = "无数据"

    return p

    画像标签的价值在于:它可以把复杂的数据字段转换成更适合汇报和教学管理使用的描述性结论。


    7. 偏科识别:不能只看总分,要看学科差异

    偏科识别的核心思想是:先计算每名学生在各学科上的平均标准分,再比较学生不同学科之间的差距。

    本项目构造了以下偏科指标:

    指标含义
    subject_z_range 学科标准分极差
    subject_z_std 学科标准分标准差
    subject_z_cv 学科标准分变异系数
    weak_subject_count 弱势学科数量
    strong_subject_count 优势学科数量
    best_subject 优势学科
    weakest_subject 最弱学科

    如果学生满足以下条件之一,则判定为偏科风险学生:

    学科标准分极差 >= 75% 分位数
    或 学科标准分标准差 >= 75% 分位数
    或 弱势学科数量 >= 2

    同时要求学生参与偏科判断的学科数量不少于 3。

    def build_subject_and_bias(valid):
    """
    生成学生-学科特征表,并计算偏科风险指标。
    """

    subject = valid.groupby(["student_id", "mes_sub_name"]).agg(
    subject_avg_score=("score_clean", "mean"),
    subject_score_std=("score_clean", "std"),
    subject_exam_count=("score_clean", "count"),
    subject_avg_z=("mes_Z_Score", "mean"),
    subject_z_std=("mes_Z_Score", "std")
    ).reset_index()

    subject["subject_score_std"] = subject["subject_score_std"].fillna(0)
    subject["subject_z_std"] = subject["subject_z_std"].fillna(0)

    bias = subject.groupby("student_id").agg(
    subject_z_mean=("subject_avg_z", "mean"),
    subject_z_std=("subject_avg_z", "std"),
    subject_z_max=("subject_avg_z", "max"),
    subject_z_min=("subject_avg_z", "min"),
    subject_count_for_bias=("mes_sub_name", "count"),
    weak_subject_count=("subject_avg_z", lambda x: int((x < 0.5).sum())),
    strong_subject_count=("subject_avg_z", lambda x: int((x > 0.5).sum()))
    ).reset_index()

    bias["subject_z_std"] = bias["subject_z_std"].fillna(0)
    bias["subject_z_range"] = bias["subject_z_max"] bias["subject_z_min"]
    bias["subject_z_cv"] = bias["subject_z_std"] / (bias["subject_z_mean"].abs() + 1e-6)

    best = subject.loc[
    subject.groupby("student_id")["subject_avg_z"].idxmax(),
    ["student_id", "mes_sub_name", "subject_avg_z"]
    ].rename(columns={"mes_sub_name": "best_subject", "subject_avg_z": "best_subject_z"})

    worst = subject.loc[
    subject.groupby("student_id")["subject_avg_z"].idxmin(),
    ["student_id", "mes_sub_name", "subject_avg_z"]
    ].rename(columns={"mes_sub_name": "weakest_subject", "subject_avg_z": "weakest_subject_z"})

    bias = bias.merge(best, on="student_id", how="left")
    bias = bias.merge(worst, on="student_id", how="left")

    range_q75 = bias["subject_z_range"].quantile(0.75)
    std_q75 = bias["subject_z_std"].quantile(0.75)

    bias["is_partial_subject"] = np.where(
    (bias["subject_count_for_bias"] >= MIN_SUBJECT_COUNT)
    & (
    (bias["subject_z_range"] >= range_q75)
    | (bias["subject_z_std"] >= std_q75)
    | (bias["weak_subject_count"] >= 2)
    ),
    1,
    0
    )

    return subject, bias

    这里使用标准分而不是原始分,是因为不同学科试卷难度和满分结构可能不同。标准分可以把不同学科放到相对统一的尺度上比较。


    8. K-Means 聚类:划分典型学生群体

    完成学生画像后,可以使用 K-Means 将学生划分为不同类型。项目中设置 N_CLUSTERS = 4,最终将学生分为四类:

  • 学科波动型
  • 行为波动型
  • 全面优势型
  • 学业风险型
  • def run_cluster(profile):
    """
    使用 K-Means 对学生画像进行聚类。
    """

    p = profile.copy()

    features = [
    "avg_z_score", "score_std", "z_score_std", "normal_rate",
    "subject_count", "subject_z_range", "subject_z_std", "subject_z_cv",
    "weak_subject_count", "strong_subject_count", "teacher_count",
    "total_consume", "avg_consume", "consume_count",
    "breakfast_count", "breakfast_ratio", "late_count"
    ]
    features = [c for c in features if c in p.columns]

    X = p[features].apply(pd.to_numeric, errors="coerce")
    useful = [c for c in X.columns if X[c].notna().sum() >= 3 and X[c].nunique(dropna=True) > 1]
    X = X[useful]

    X = X.fillna(X.median(numeric_only=True))

    scaler = StandardScaler()
    Xs = scaler.fit_transform(X)

    model = MiniBatchKMeans(
    n_clusters=4,
    random_state=42,
    n_init=5,
    batch_size=256,
    max_iter=100
    )

    p["cluster_id"] = model.fit_predict(Xs)

    centers = pd.DataFrame(
    scaler.inverse_transform(model.cluster_centers_),
    columns=useful
    )
    centers.insert(0, "cluster_id", range(4))

    size = p["cluster_id"].value_counts().rename_axis("cluster_id").reset_index(name="cluster_size")
    centers = centers.merge(size, on="cluster_id", how="left")

    centers["cluster_name"] = "稳定中等型"
    used = set()

    if "avg_z_score" in centers.columns:
    risk_id = centers.loc[centers["avg_z_score"].idxmin(), "cluster_id"]
    centers.loc[centers["cluster_id"] == risk_id, "cluster_name"] = "学业风险型"
    used.add(risk_id)

    if "subject_z_range" in centers.columns:
    cand = centers[~centers["cluster_id"].isin(used)]
    if len(cand):
    partial_id = cand.loc[cand["subject_z_range"].idxmax(), "cluster_id"]
    centers.loc[centers["cluster_id"] == partial_id, "cluster_name"] = "学科波动型"
    used.add(partial_id)

    if "avg_z_score" in centers.columns:
    cand = centers[~centers["cluster_id"].isin(used)]
    if len(cand):
    good_id = cand.loc[cand["avg_z_score"].idxmax(), "cluster_id"]
    centers.loc[centers["cluster_id"] == good_id, "cluster_name"] = "全面优势型"
    used.add(good_id)

    if "consume_count" in centers.columns:
    cand = centers[~centers["cluster_id"].isin(used)]
    if len(cand):
    active_id = cand.loc[cand["consume_count"].idxmax(), "cluster_id"]
    centers.loc[centers["cluster_id"] == active_id, "cluster_name"] = "行为波动型"
    used.add(active_id)

    name_map = dict(zip(centers["cluster_id"], centers["cluster_name"]))
    p["cluster_name"] = p["cluster_id"].map(name_map)

    return p, centers

    K-Means 的价值不是为了预测,而是为了把学生群体划分成更容易解释的典型类型,便于后续针对不同群体制定干预策略。


    9. 偏科与非偏科学生对比

    识别偏科学生之后,还需要回答一个问题:偏科学生到底有什么特点?

    因此,代码中对偏科风险组和非偏科风险组进行数值指标对比。

    def compare_groups(profile):
    """
    对比偏科风险组与非偏科风险组的差异。
    """

    profile = normalize_partial_label(profile)

    num_cols = [
    "avg_score", "score_std", "avg_z_score", "z_score_std",
    "subject_z_range", "subject_z_std", "weak_subject_count",
    "teacher_count", "total_consume", "avg_consume", "consume_count",
    "breakfast_count", "breakfast_ratio", "late_count", "normal_rate"
    ]
    num_cols = [c for c in num_cols if c in profile.columns]

    rows = []
    for c in num_cols:
    x = pd.to_numeric(profile[c], errors="coerce")
    non_mean = x[profile["is_partial_subject"] == 0].mean()
    par_mean = x[profile["is_partial_subject"] == 1].mean()

    rows.append({
    "feature": c,
    "non_partial_mean": non_mean,
    "partial_mean": par_mean,
    "difference": par_mean non_mean
    })

    num_compare = pd.DataFrame(rows)
    return num_compare

    分析结果表明:

    • 偏科风险学生平均标准分更低;
    • 偏科风险学生标准分波动更大;
    • 偏科风险学生学科极差更大;
    • 偏科风险学生弱势学科更多;
    • 消费、早餐、考勤可以作为辅助解释变量,但不是偏科识别的核心依据;
    • 教师关联复杂度可作为补充解释因素。

    这说明偏科风险不是简单的“成绩差”,而是“学科之间发展不均衡”。


    10. 用 Cohen’s d 展示组间差异

    在可视化中,如果直接比较原始均值,会遇到量纲问题。例如:

    • 总消费金额可能是几百或几千;
    • 平均标准分可能在 -2 到 2 之间;
    • 迟到次数可能只有个位数。

    如果放在同一张图中,消费金额会把其他指标压缩得不明显。因此高级可视化脚本使用 Cohen’s d 标准化差异进行展示。

    Cohen’s d 的含义是:

    d > 0:偏科风险组更高
    d < 0:非偏科风险组更高
    |d| 越大:两组差异越明显

    def plot_partial_compare_dumbbell(profile):
    """
    使用 Cohen's d 展示偏科风险组与非偏科风险组的标准化差异。
    """

    metric_map = {
    "avg_z_score": "平均标准分",
    "avg_score": "平均分",
    "score_std": "成绩波动",
    "z_score_std": "标准分波动",
    "subject_z_range": "学科极差",
    "subject_z_std": "学科波动",
    "weak_subject_count": "弱势学科数",
    "strong_subject_count": "优势学科数",
    "teacher_count": "教师关联数",
    "total_consume": "总消费",
    "breakfast_ratio": "早餐比例",
    "late_count": "迟到次数",
    "normal_rate": "成绩正常率"
    }

    rows = []
    y = pd.to_numeric(profile["is_partial_subject"], errors="coerce").fillna(0).astype(int)

    for m in metric_map:
    if m not in profile.columns:
    continue

    x = pd.to_numeric(profile[m], errors="coerce")
    x0 = x[y == 0].dropna()
    x1 = x[y == 1].dropna()

    if len(x0) < 3 or len(x1) < 3:
    continue

    mean0, mean1 = x0.mean(), x1.mean()
    std0, std1 = x0.std(ddof=1), x1.std(ddof=1)
    n0, n1 = len(x0), len(x1)

    pooled_std = np.sqrt(
    ((n0 1) * std0 ** 2 + (n1 1) * std1 ** 2)
    / max(n0 + n1 2, 1)
    )

    if pooled_std == 0 or np.isnan(pooled_std):
    continue

    effect = (mean1 mean0) / pooled_std

    rows.append({
    "feature": m,
    "feature_cn": metric_map[m],
    "effect_size": effect,
    "abs_effect": abs(effect)
    })

    df = pd.DataFrame(rows)
    df = df.sort_values("abs_effect", ascending=False).head(10)
    df = df.sort_values("effect_size")

    fig, ax = plt.subplots(figsize=(9.8, 6.2))

    colors = np.where(df["effect_size"] >= 0, "#F58518", "#4C78A8")
    ax.barh(df["feature_cn"], df["effect_size"], color=colors, alpha=0.88)
    ax.axvline(0, color="#333333", linewidth=1)

    ax.set_title("偏科风险组与非偏科风险组关键指标标准化差异")
    ax.set_xlabel("标准化差异(Cohen's d)")
    ax.grid(axis="x", linestyle="–", alpha=0.25)

    plt.tight_layout()
    plt.show()

    从结果上看,偏科风险组在弱势学科数、标准分波动、学科波动、学科极差等指标上差异最明显;非偏科风险组则在平均标准分、优势学科数、平均分等指标上更高。


    11. 可视化设计

    项目中生成了多张图,用于支撑学生画像和偏科识别结论。

    11.1 学生群体画像热力图

    热力图使用聚类中心的标准化结果,适合解释每一类学生为什么这样命名。 在这里插入图片描述

    11.2 PCA 聚类与偏科风险二维分布图

    PCA 可以把多个画像指标压缩到二维空间,便于观察不同学生群体和偏科风险学生的分布关系。 在这里插入图片描述

    11.3 学业水平—学科差异偏科风险矩阵

    这张图最适合说明一个核心观点:偏科风险不只存在于低成绩学生中,中等或较好成绩学生也可能存在明显学科短板。 在这里插入图片描述

    11.4 弱势学科与群体分布热力图

    该图可以展示不同学生群体中弱势学科的分布差异。 在这里插入图片描述


    12. 随机森林解释:哪些因素与偏科风险更相关

    为了进一步解释偏科风险,可以使用随机森林模型做特征重要性分析。

    需要注意的是:这里不是为了构建一个严格的预测模型,而是为了从特征重要性角度辅助解释“哪些画像特征与偏科风险更相关”。

    def rf_importance(profile):
    """
    使用随机森林分析偏科风险相关特征的重要性。
    """

    profile = normalize_partial_label(profile)
    profile["is_partial_subject"] = profile["is_partial_subject"].astype(int)

    if profile["is_partial_subject"].nunique() < 2:
    return pd.DataFrame()

    num = [
    "avg_score", "score_std", "avg_z_score", "z_score_std",
    "subject_count", "teacher_count", "total_consume", "avg_consume",
    "consume_count", "breakfast_count", "breakfast_ratio", "late_count", "normal_rate"
    ]
    num = [c for c in num if c in profile.columns]

    cat = [
    "bf_sex", "campus", "Bf_ResidenceType", "bf_policy",
    "academic_level", "score_stability_label", "consume_label",
    "breakfast_label", "attendance_reliability_label",
    "teacher_complexity_label", "cluster_name"
    ]
    cat = [c for c in cat if c in profile.columns]

    X_num = profile[num].apply(pd.to_numeric, errors="coerce")
    X_num = X_num.fillna(X_num.median(numeric_only=True))

    X_cat = pd.get_dummies(profile[cat].fillna("缺失")) if cat else pd.DataFrame(index=profile.index)

    X = pd.concat([X_num, X_cat], axis=1)
    X = X.loc[:, X.nunique(dropna=True) > 1]

    y = profile["is_partial_subject"].astype(int)

    rf = RandomForestClassifier(
    n_estimators=300,
    max_depth=6,
    min_samples_leaf=5,
    random_state=42,
    class_weight="balanced"
    )

    rf.fit(X, y)

    imp = pd.DataFrame({
    "feature": X.columns,
    "importance": rf.feature_importances_
    }).sort_values("importance", ascending=False)

    return imp

    结果显示,平均标准分、标准分波动、学业水平标签和聚类群体标签具有较高重要性,说明偏科风险与学生整体学业水平、成绩稳定性和画像群体特征密切相关。


    13. 主程序整合

    完整主流程可以概括为:

    def main():
    """
    学生画像 + 偏科识别主流程。
    """

    OUT_DIR.mkdir(parents=True, exist_ok=True)
    FIG_DIR.mkdir(parents=True, exist_ok=True)

    # 1. 读取数据
    detail, modeling = load_data()

    # 2. 清洗成绩明细表
    detail, valid = clean_detail(detail)

    # 3. 构建学生画像基础表
    profile = build_student_profile(detail, valid)

    # 4. 偏科识别:默认剔除体育、音乐、美术等非文化课
    valid_for_bias = filter_bias_subjects(valid)
    subject_feature, bias = build_subject_and_bias(valid_for_bias)

    # 5. 合并偏科结果
    profile = profile.merge(bias, on="student_id", how="left")
    profile = normalize_partial_label(profile)

    # 6. 合并消费、早餐、迟到等行为特征
    profile = merge_behavior_from_modeling(profile, modeling)

    # 7. 过滤有效分析样本
    profile = filter_valid_analysis_sample(profile, modeling)

    # 8. 生成画像标签
    profile = add_profile_labels(profile)

    # 9. K-Means 聚类
    profile, centers = run_cluster(profile)

    # 10. 偏科与非偏科对比
    num_compare = compare_groups(profile)

    # 11. 随机森林特征重要性
    importance = rf_importance(profile)

    # 12. 保存结果
    profile.to_csv(
    OUT_DIR / "02_student_profile_cluster_bias_result.csv",
    index=False,
    encoding="utf-8-sig"
    )

    centers.to_csv(
    OUT_DIR / "03_cluster_center_summary.csv",
    index=False,
    encoding="utf-8-sig"
    )

    num_compare.to_csv(
    OUT_DIR / "05_partial_vs_nonpartial_numeric.csv",
    index=False,
    encoding="utf-8-sig"
    )

    importance.to_csv(
    OUT_DIR / "07_partial_feature_importance.csv",
    index=False,
    encoding="utf-8-sig"
    )

    print("运行完成,结果保存在:", OUT_DIR.resolve())

    运行命令:

    pip install pandas numpy scikit-learn matplotlib
    python student_b_pipeline.py
    python advanced_student_b_charts.py


    14. 结果总结

    本项目完成了从学生成绩宽表到学生画像建模、偏科风险识别、聚类分群和结果解释的完整流程。

    主要结论如下:

  • 学生画像应从多个维度构建,不能只看成绩。 成绩水平、成绩稳定性、学科均衡性、消费行为、早餐习惯、考勤情况和教师关联复杂度都可以作为画像指标。

  • 偏科风险不等同于成绩低。 部分总成绩中等甚至较好的学生,也可能存在明显学科短板。

  • 偏科风险学生的核心特征是学科发展不均衡。 他们通常表现为弱势学科数量更多、标准分波动更大、学科极差更高。

  • K-Means 聚类可以辅助识别典型学生群体。 本项目将学生划分为学科波动型、行为波动型、全面优势型和学业风险型四类。

  • 教学干预应分层进行。 对学业风险型学生,应重点关注整体学业补弱;对学科波动型学生,应重点针对具体弱势学科进行辅导;对行为波动型学生,可以结合考勤、早餐和消费等行为特征进行辅助分析。


  • 15. 项目亮点

    本项目的亮点在于:

    • 使用学生级聚合,避免明细宽表重复统计;
    • 使用标准分而不是原始分进行跨学科比较;
    • 使用动态分位数阈值识别偏科风险;
    • 将偏科识别与学生画像结合,而不是孤立分析成绩;
    • 使用 K-Means 聚类形成典型学生群体;
    • 使用 Cohen’s d 解决不同量纲指标难以对比的问题;
    • 使用随机森林特征重要性增强结果解释性。

    16. 结语

    学生画像的价值不在于给学生贴标签,而在于从多维数据中发现需要关注的学生群体。偏科识别也不应只看总分,而应关注不同学科之间的相对差异。

    通过本项目,可以形成一套较完整的分析思路: 在这里插入图片描述

    这套流程不仅适用于学生画像任务,也可以扩展到学业预警、精准辅导、班级管理和教育数据分析等场景。

    赞(0)
    未经允许不得转载:171主机测评 » 从成绩宽表到学生画像:用 Python 挖出“隐形偏科生”的完整实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址