目录

1. 项目背景

2. 任务目标与技术栈

3. 整体分析流程

4. 特征工程

4.1 消费数据处理

4.2 时段特征与就餐规律

4.3 经济稳定性特征

5. 核心方案:双层筛选模型与算法优势

第一层:放宽金额初筛

第二层:生活困难指数精选

双层筛选可视化

5.1 全貌对比:单一指标 vs 多维画像

5.2 多维度特征对比

5.3 模型验证:孤立森林交叉验证

6. 困难学生深度画像与可视化解读

6.1 三餐省在哪里?——省钱部位定位

6.2 营养规律性:缺早餐率分析

6.3 经济稳定性:消费波动与持续低消

6.4 月度消费趋势:压力敏感窗口识别

7. 精准资助:三层托底体系

7.1 补贴金额测算

7.2 三层资助体系可视化

8. 结果总结

8.1 贫困识别结论

8.2 资助方案结论

9. 结语

1. 项目背景

在传统的校园资助工作中,我们通常面临两大痛点:一是"边缘学生漏检"——有些学生虽然总体消费金额不低,但为了省钱常年不吃早餐,这种隐性贫困容易被忽视;二是"资助手段单一"——通常是统一发放固定金额,难以满足不同学生的具体困难。

传统方法依赖学生主动申报或班主任经验判断,存在遗漏或主观偏差。本项目的核心逻辑在于非入侵式识别:利用Python对校园卡后台流水进行高维度的行为建模,从"金额导向"迈入"行为建模"。我们通过分析消费结构与稳定性,最大程度保护学生隐私与自尊,精准区分"主动节俭"与"被动拮据"。

2. 任务目标与技术栈

  • 任务目标

    1. 基于消费数据,构建多维度贫困识别模型,精准锁定困难学生;

    2. 对困难群体进行生活画像分析,发现其具体的省钱模式;

    3. 提供个性化的分层补贴建议,实现精准滴灌。

  • 技术栈与环境

    • 语言:Python(Pandas、NumPy、Matplotlib、Seaborn、Scikit-learn)

    • 算法:Min-Max标准化、孤立森林(Isolation Forest)

    • 开发环境:Jupyter Notebook / VS Code

  • 数据来源

    • 7_consumption.csv:本学年消费记录,共463,904条(含消费时间、金额、学生ID)

    • 2_student_info.csv:学生基本信息,共1,765条(含性别、籍贯、班级等)

3. 整体分析流程

本项目采用"漏斗式"分析流程,如下图所示:

数据加载与清洗 → 特征工程(11项指标) → 第一层:金额初筛(15%分位线)
    → 第二层:困难指数精选(5指标等权加权) → 孤立森林交叉验证
        → 困难群体画像分析 → 三层资助体系设计 → 结果导出

核心步骤说明:

  1. 特征工程:从46万条消费记录中,按学生聚合出日均消费、三餐规律、经济稳定性、校园依赖度四大维度的11项特征;

  2. 双层筛选:第一层用15%分位线进行金额初筛(260人入池),第二层用生活困难指数精选核心群体(130人锁定);

  3. 交叉验证:用孤立森林对全体学生进行异常检测,验证分层识别的稳健性;

  4. 画像分析:深度剖析困难学生的省钱部位、营养规律、经济稳定性、校园依赖度;

  5. 方案设计:基于画像结果,设计基础补贴+早餐专项+月末应急的三层托底体系。

4. 特征工程

识别逻辑的优劣取决于特征设计的精细度。我们不仅关注消费总量,更关注消费的结构与稳定性。从消费明细中按学生聚合,提取了以下11项特征:

特征 含义 与贫困的关系
avg_daily_cost 日均消费金额 越低越可能贫困
breakfast_rate 早餐就餐频率 越低越节省
miss_breakfast_rate 缺早餐率(=1-早餐频率) 越高越节省
lunch_avg 午餐平均消费 越低越拮据
dinner_avg 晚餐平均消费 越低越拮据
cost_std 消费标准差 反映波动性
cv_cost 消费变异系数(=标准差/均值) 越高越不稳定
weekend_ratio 周末消费占比 反映校园依赖度
month_end_ratio 月底消费占比 异常可能反映资金紧张
max_low_days 最长连续低消费天数 越高反映持续性经济压力
total_days 有消费记录的总天数 反映在校时长

4.1 消费数据处理

这里有一个关键的技术细节:如果不先按学生+日期汇总每日总消费,直接对所有消费记录取平均,会导致"一天刷多次卡的学生"日均消费被高估。因此我们做了如下处理:

# 第一步:按学生+日期计算每日总消费
daily_cost = df_consumption.groupby(['bf_StudentID', 'date'])['cost'].sum().reset_index(name='daily_cost')

# 第二步:基于每日总消费计算日均值和标准差
student_cost = daily_cost.groupby('bf_StudentID').agg(
    total_cost=('daily_cost', 'sum'),
    total_days=('date', 'nunique'),
    avg_daily_cost=('daily_cost', 'mean'),
    cost_std=('daily_cost', 'std'),
).reset_index()

4.2 时段特征与就餐规律

通过提取三餐就餐率和各时段平均消费,我们发现困难群体存在严重的"早餐缺失"现象:

extra_features = df_consumption.groupby('bf_StudentID').agg(
    breakfast_rate=('hour', lambda x: ((x>=6) & (x<8)).sum() / len(x)),
    lunch_avg=('cost', lambda x: x[(df_consumption.loc[x.index, 'hour']>=11) & 
                                   (df_consumption.loc[x.index, 'hour']<13)].mean()),
    dinner_avg=('cost', lambda x: x[(df_consumption.loc[x.index, 'hour']>=17) & 
                                    (df_consumption.loc[x.index, 'hour']<19)].mean()),
    weekend_ratio=('weekday', lambda x: (x>=5).sum() / len(x)),
    month_end_ratio=('is_month_end', 'mean')
).reset_index()

# 衍生特征
student_cost['miss_breakfast_rate'] = 1 - student_cost['breakfast_rate']
student_cost['cv_cost'] = student_cost['cost_std'] / (student_cost['avg_daily_cost'] + 0.01)

4.3 经济稳定性特征

引入最长连续低消费天数这一关键指标,捕捉持续性的生存压力:

def max_consecutive_low(grp):
    grp = grp.sort_values('date')
    low = (grp['daily_cost'] < poverty_line_simple).astype(int)
    max_len, cur = 0, 0
    for v in low:
        if v == 1:
            cur += 1
            max_len = max(max_len, cur)
        else:
            cur = 0
    return max_len

stu_max_low = daily_cost.groupby('bf_StudentID').apply(max_consecutive_low).reset_index(name='max_low_days')

5. 核心方案:双层筛选模型与算法优势

为了确保资助的公平性与覆盖度,我们采用双层筛选模型——这比传统的单指标一刀切或单一算法检测更加精准。

第一层:放宽金额初筛

采用全校日均消费的15%分位线(约16.30元/天)作为入池门槛,而非传统的10%。这一设计极具人文关怀,有效降低了"早饭省下、午饭正常"这类边缘学生的漏检率。最终筛出260人进入待选池。

第二层:生活困难指数精选

在低消费池内,选取日均消费、晚餐均价、消费变异系数、月底消费占比、最长连续低消费天数五项指标。经Min-Max标准化后等权加权,构建"生活困难指数",取中位数以上者为核心困难学生,最终锁定130人(占全校7.5%)。

# %% 分层识别:先金额筛选,再生活困难指数排序
# 第一层:消费金额筛选(使用15%分位线,比10%稍宽,避免漏掉边缘学生)
money_line = student_cost['avg_daily_cost'].quantile(0.15)
print(f"第一层筛选线(15%分位):{money_line:.2f} 元/天")

low_consumption = student_cost[student_cost['avg_daily_cost'] < money_line].copy()
print(f"第一层筛选出消费较低学生:{len(low_consumption)} 人")

# 第二层:在低消费群体中构建生活困难指数
# 选取更能反映“拮据程度”的指标(去掉缺失率,因为在这个群体内不缺分度)
features_v2 = ['avg_daily_cost', 'dinner_avg', 'cv_cost', 'month_end_ratio', 'max_low_days']
X_v2 = low_consumption[features_v2].copy()
scaler_v2 = MinMaxScaler()
X_scaled_v2 = pd.DataFrame(scaler_v2.fit_transform(X_v2), columns=features_v2, index=X_v2.index)

# 方向处理:avg_daily_cost, dinner_avg 越低越困难
for col in ['avg_daily_cost', 'dinner_avg']:
    X_scaled_v2[col] = 1 - X_scaled_v2[col]

# 等权
low_consumption['hardship_score'] = X_scaled_v2.mean(axis=1)

# 取前50%作为“核心困难群体”(可根据学校预算调整)
threshold = low_consumption['hardship_score'].median()
low_consumption['is_hardship'] = (low_consumption['hardship_score'] >= threshold).astype(int)

hardship_students = low_consumption[low_consumption['is_hardship'] == 1].copy()
hardship_ids = hardship_students['bf_StudentID'].tolist()

# 给全体 student_cost 打标签
student_cost['is_hardship'] = student_cost['bf_StudentID'].isin(hardship_ids).astype(int)
student_cost['hardship_score'] = 0
student_cost.loc[student_cost['bf_StudentID'].isin(low_consumption['bf_StudentID']), 'hardship_score'] = low_consumption['hardship_score']

print(f"第二层筛选出核心困难学生:{len(hardship_students)} 人")
print(f"占总人数比例:{len(hardship_students)/len(student_cost):.1%}")

双层筛选可视化

解读要点:

  • 上层:15%分位线(红线左侧)划出低消费池260人——这一步是"粗筛",不漏掉边缘学生;

  • 下层:生活困难指数中位数(红线右侧130人)锁定核心困难群体——这一步是"精选",多维度确认贫困程度。

5.1 全貌对比:单一指标 vs 多维画像

在做分层识别之前,我们先看看全校的消费分布,以及"仅用单一金额指标"会有怎样的局限:

# %% 全体学生消费分布概览(简单贫困线对比)
plt.figure(figsize=(10,5))
sns.histplot(student_cost['avg_daily_cost'], bins=50, kde=True, color='skyblue')  # bins从50加到80,柱子更细
plt.axvline(poverty_line_simple, color='red', linestyle='--', linewidth=2,
            label=f'10%分位线 {poverty_line_simple:.2f} 元')
plt.title('全体学生日均消费分布')
plt.xlabel('日均消费 (元)')
plt.xlim(0, 70)
plt.xticks(range(0, 71, 5))
plt.legend()
plt.show()

# 简单金额线人数
simple_poor_count = (student_cost['avg_daily_cost'] < poverty_line_simple).sum()
print(f"若只用日均消费<10%分位线,贫困生人数:{simple_poor_count}")

解读要点:

  • 全校日均消费呈近似正态分布,均值约23元;

  • 若仅以10%分位线(14.58元)一刀切,可识别173人——但单一金额指标无法反映消费稳定性、营养规律等生活质量维度。

5.2 多维度特征对比

下图将"10%分位线以下学生"和"其他学生"在四个核心维度上进行对比:

打简单标签
student_cost['simple_poor'] = (student_cost['avg_daily_cost'] < poverty_line_10).astype(int)

compare_features = ['avg_daily_cost', 'miss_breakfast_rate', 'cv_cost', 'max_low_days']
compare_labels = ['日均消费(元)', '缺早餐率', '消费变异系数', '最长连续低消天数']
compare_colors = ['#3498db', '#e74c3c']

fig, axes = plt.subplots(2, 2, figsize=(12, 10))
for ax, feat, label in zip(axes.flatten(), compare_features, compare_labels):
    data_poor = student_cost[student_cost['simple_poor'] == 1][feat]
    data_other = student_cost[student_cost['simple_poor'] == 0][feat]

    # 用中位数+四分位距做简化箱线
    stats = [
        [data_other.median(), data_other.quantile(0.25), data_other.quantile(0.75)],
        [data_poor.median(), data_poor.quantile(0.25), data_poor.quantile(0.75)]
    ]

    x = [0, 1]
    for i, (med, q1, q3) in enumerate(stats):
        ax.bar(i, med, 0.5, color=compare_colors[i], alpha=0.8, zorder=3)
        ax.plot([i, i], [q1, q3], 'k-', linewidth=2, zorder=4)
        ax.plot([i - 0.1, i + 0.1], [q1, q1], 'k-', linewidth=1.5)
        ax.plot([i - 0.1, i + 0.1], [q3, q3], 'k-', linewidth=1.5)

    ax.set_xticks([0, 1])
    ax.set_xticklabels(['其他学生', '10%线以下'])
    ax.set_title(label, fontsize=12, fontweight='bold')
    ax.grid(axis='y', alpha=0.3)

plt.suptitle('单一金额线 vs 多维度特征:同一群学生的不同面貌', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

解读要点:

  • 日均消费:两组差距显著(定义使然),但这只是冰山一角;

  • 缺早餐率:低消费组显著更高,说明"不吃早餐"是省钱的核心手段;

  • 消费变异系数:低消费组波动更大,暗示经济来源不稳定;

  • 最长连续低消天数:低消费组远超其他学生,存在持续性拮据。

5.3 模型验证:孤立森林交叉验证

我们将分层识别名单与孤立森林(污染率5%)的异常检测结果进行交叉比对,重叠率高达43.08%(远超随机概率5%)。这表明:分层法聚焦"经济拮据",孤立森林捕获"模式异常",两者互为补充,大幅提升了识别的可信度。

iso = IsolationForest(contamination=0.05, random_state=42)
features_iso = ['avg_daily_cost', 'dinner_avg', 'cv_cost', 'weekend_ratio', 'max_low_days']
X_iso_scaled = pd.DataFrame(MinMaxScaler().fit_transform(X_iso), columns=features_iso)
student_cost['iso_anomaly'] = iso.fit_predict(X_iso_scaled)

解读要点——四类学生着色:

  • 红色重叠区(核心确认):分层识别+孤立森林双重认定的困难学生,位于左上方(低消费+高波动),置信度最高;

  • 蓝色仅分层识别:日均消费低但消费模式不够"异常",可能是习惯性节俭;

  • 橙色仅孤立森林:消费金额不低但模式异常,可能是消费习惯特殊;

  • 15%分位线(红色虚线):第一层筛选门槛,左侧全部进入待选池。

6. 困难学生深度画像与可视化解读

6.1 三餐省在哪里?——省钱部位定位

我们对困难学生和其他学生在各时段的消费进行对比,发现三餐全方位节省,但午餐和晚餐是省钱的主要来源:

def time_slot(h):
    if 6<=h<8: return '早餐'
    elif 11<=h<13: return '午餐'
    elif 17<=h<19: return '晚餐'
    else: return '其他'

slot_compare = pd.DataFrame({
    '困难学生': poor_consume.groupby('slot')['cost'].mean(),
    '其他学生': non_poor_consume.groupby('slot')['cost'].mean()
}).drop('其他', errors='ignore')

为进一步拆解"省钱"行为,我们同时对就餐率单次消费金额进行了对比:

解读要点:

  • 左图(就餐率):困难学生早餐就餐率明显低于其他学生,说明他们倾向"省掉早餐"——这是一种主动压抑消费的行为;

  • 右图(单次消费金额):即便是就同一餐次而言,困难学生的人均消费也全面偏低,印证了"每一餐都在省钱";

  • 综合来看,午餐和晚餐的人均消费差异最大(合计少花约2.14元/天),是省钱的主要部位。

6.2 营养规律性:缺早餐率分析

困难学生中几乎不吃早餐(缺早餐率>80%)的比例高达95.4%,显著高于其他学生的65.3%。"不吃早餐"虽然在该校是普遍现象,但在困难学生中更为严重。

no_bf_poor = (hardship_students['miss_breakfast_rate'] > 0.8).mean()
no_bf_other = (student_cost[student_cost['is_hardship']==0]['miss_breakfast_rate'] > 0.8).mean()

解读要点:

  • 困难学生(红色)的缺早餐率分布明显右偏,集中在高缺失区间;

  • 95.4%的困难学生几乎不吃早餐——资助重点应放在早餐补贴,直接解决"不吃早餐"的经济动因。

6.3 经济稳定性:消费波动与持续低消

困难学生的日消费波动更大、持续低消时间更长,存在明显的"断粮"风险:

python

# 左侧:消费变异系数散点分布 + 均值标记
# 右侧:最长连续低消费天数 KDE + 均值线

解读要点:

  • 左图(消费变异系数):困难学生(红色)CV系数均值显著更高,菱形标记清晰展示了两组均值的差距,说明其日消费波动更大,经济来源可能不稳定;

  • 右图(连续低消费天数):困难学生(红色)分布明显右移,均值高达18.6天——意味着他们曾连续超过半个月处于极度节省状态,远超其他学生。

6.4 月度消费趋势:压力敏感窗口识别

从各月人均日总消费趋势来看,困难学生全年各月消费均低于其他学生:

monthly_trend = daily_cost_full.groupby(['month', 'is_hardship'])['daily_cost'].mean().unstack()

解读要点:

  • 开学季与长假前后是困难学生经济压力的敏感窗口——9月开学初学生自带物资较多,10月国庆长假离校,消费出现低谷;

  • 这一趋势提示学校,应在开学季与长假前后加强困难学生的生活保障。

7. 精准资助:三层托底体系

精准识别最终服务于精准资助。基于困难学生群体的画像特征(95.4%几乎不吃早餐、日均缺口约4.46元),我们设计了互补的三层方案。

7.1 补贴金额测算

以全校日均消费第15百分位(约16.30元/天)作为"体面生活线",困难学生当前日均消费中位数为11.84元,每日存在约4.46元的缺口:

living_line = student_cost['avg_daily_cost'].quantile(0.15)
hardship_students['subsidy_daily'] = np.maximum(living_line - hardship_students['current_avg'], 0)

7.2 三层资助体系可视化

解读要点:

  • 左图(三层资助体系):从上到下逐层精准——

    • 第一层基础营养补贴覆盖全部130人,补足体面生活线缺口;

    • 第二层早餐专项补贴聚焦124名缺早餐率>80%的学生,在早餐时段自动充值3-5元;

    • 第三层月末应急补助针对57名月底消费明显下降的学生,每月25日后发放20-30元。

  • 右图(补贴分布):展示了130名困难学生每日补贴金额的分布,三条参考线分别标注人均补贴、最高补贴和中位数缺口。

方案层级 覆盖人群 核心逻辑 预期目标 月度成本
基础营养补贴 全部130名困难生 补足体面生活线(16.3元)缺口 实现生活底线兜底 约18,300元
早餐专项补助 缺早餐率>80%者(124人) 早餐时段自动发放定向补贴 解决因贫困导致的"跳餐" 约1,860元
月末应急资助 月底消费低于中位者(57人) 每月25日后发放应急额度 对冲月末资金短缺风险 约1,710元

三项合计月均仅需约21,900元,即可实现对130名困难学生的精准生活托底,人均月补约168元。相较于该校全寄宿制的运营成本,这一投入负担极轻。

8. 结果总结

本研究基于某校全寄宿制高中46万余条消费记录,采用"双层筛选模型"从1,730名学生中精准识别出130名核心困难学生(占全校7.5%),并通过孤立森林交叉验证(重叠率43.08%),验证了识别结果的稳健性。主要结论如下:

8.1 贫困识别结论

  • 消费水平严重偏低:困难学生日均总消费中位数为11.84元,与全校体面生活线(16.30元/天)之间存在约4.46元的每日缺口;

  • 三餐全方位节省:困难学生在早餐、午餐、晚餐各时段的消费均低于其他学生,其中午餐和晚餐是省钱的主要来源(合计每日少花约2.14元);

  • 早餐缺失极其严重:95.4%的困难学生几乎不吃早餐(缺早餐率>80%),远超其他学生的65.3%,早餐补贴是最迫切的干预切入点;

  • 经济持续性不稳定:困难学生平均最长连续低消费天数高达18.6天,意味着他们曾连续超过半个月处于极度节省状态,存在明显的"断粮"风险;

  • 校园依赖度更高:困难学生周末消费占比为9.93%,明显高于其他学生的6.78%,校园是其主要生活空间;

  • 生源地呈现地域特征:困难生比例最高的地区为宁海(18.18%)、奉化(16.67%)、余姚(15.38%),呈现"周边区县高于中心城区"的特征。

8.2 资助方案结论

  • 三层托底体系:基础营养补贴(全覆盖)+ 早餐专项补助(缺早餐率>80%者)+ 月末应急资助(月底消费低于中位者),三项合计月均约21,900元,人均月补约168元;

  • 成本效益显著:相较于全校全寄宿制的运营成本,这一投入负担极轻,却能实现困难学生群体的生活底线保障。

9. 结语

从"经验判断"到"数据驱动",从"统一发放"到"分层滴灌",教育大数据正在让校园资助工作变得更精准、更有温度。本文构建的双层筛选模型和三层资助体系,仅是一个起点。

我们相信,每一份助学金都应当精准抵达最需要它的学生手中,每一顿早餐都不应因贫困而被迫省去。未来,随着更多维数据的接入(如家庭经济状况、学业表现、心理健康等),精准资助的画像将更加立体,教育的公平与温度也将更进一步。

让数据说话,让温暖落地。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐