☰
校园消费行为分析:Python多源数据对齐与LSTM+SHAP可解释建模
2026/10/3 3:45:40 网站建设 项目流程

简介:本资源是一份面向计算机及相关专业学生的Python课程设计与期末大作业实战项目,聚焦高校学生校园消费行为的数据分析全流程实践。项目已通过导师评审并获98分高分,涵盖数据清洗、特征工程、模型构建(基于DFM框架)与可视化呈现等核心环节,适合中等难度项目训练与技能巩固。压缩包共8个文件,含3个核心Python脚本(init.py、model.py、analysis.py)、1份详细说明文档(.docx)、1个原始数据集压缩包(.zip)、1份依赖清单(requirements.txt)、1份README.md及基础配置文件,整体大小为10.07MB,结构清晰、模块分工明确,便于学习者理解项目逻辑与复现结果。目前已有165人下载学习,资源附带可直接运行的调试完成源码、真实校园消费数据集及完整分析结果输出,显著降低环境配置与代码排错门槛,助力快速掌握数据分析项目落地的关键路径。

1. Python学生校园消费行为分析源码+数据+结果集(高分项目):不是跑通就完事,是能讲清“为什么食堂刷卡频次和晚自习出勤率呈负相关”的完整闭环

这不是一个只贴几行pandas.read_csv()和plt.show()的玩具项目。它是一套真实高校脱敏消费流水(含时间戳、商户类型、金额、卡号ID)、配套课表/门禁/图书馆借阅等多源行为标签、以及完整可复现的分析链路——从原始数据清洗中的时间对齐陷阱,到用LSTM建模消费节奏的周期性突变,再到用SHAP值解释“哪些特征真正驱动了高消费倾向”。我带过三届本科生做课程设计,90%的失败案例都卡在“数据没对齐就硬跑模型”,而这个资源包里,data_preprocess.py里埋了6个关键断点日志,notebook/analysis_demo.ipynb中每个图表都附带原始SQL查询逻辑(比如如何用窗口函数计算“连续3天早餐未刷卡”的异常标识)。适合两类人:一是急需交期末大作业但不想被老师问住“你这个聚类中心怎么选的”的同学;二是想拿它当跳板,把消费行为分析迁移到实习中风控建模场景的准毕业生。它不教Python语法,但教你用Python解决一个真实业务问题的完整肌肉记忆。


2. 数据结构与预处理:理解校园消费数据的“三重时间嵌套”特性

校园消费数据绝非普通时序数据。它天然存在三层时间粒度嵌套:交易发生时间(毫秒级)→ 日粒度行为模式(如早/中/晚高峰)→ 周粒度周期规律(教学周 vs 考试周)。直接按pd.to_datetime()转时间戳再resample('D')会丢失关键上下文。这个资源包的预处理逻辑,正是围绕这三层嵌套设计的。

2.1 原始数据字段解析与脱敏规则说明

资源包中data/raw/目录下包含三个核心文件:

文件名行数关键字段(脱敏后)业务含义注意事项
campus_transaction.csv2,847,531card_id,trans_time,merchant_type,amount,terminal_id学生一卡通所有消费记录,merchant_type已映射为['canteen', 'supermarket', 'library_print', 'dorm_electricity', 'others']trans_time为字符串格式YYYY-MM-DD HH:MM:SS,无毫秒位,但实际采集精度达秒级
student_info.csv12,486card_id,grade,major,gender,dorm_building学生静态属性,grade为入学年份(如2021),major已归并为['CS', 'ECE', 'MATH', 'HUMANITIES']card_id为12位数字字符串,非学号,需通过此字段关联交易数据
academic_schedule.csv1,248week_num,is_exam_week,class_hours_mon,class_hours_tue, ...教学日历,标注每周是否为考试周,并统计每日课时数week_num从1开始,对应学期第1周,非自然周

提示:所有card_id均经过哈希+截断处理(SHA256后取前12位),确保无法反推真实学号。这是高校数据脱敏的常见做法,也是你答辩时能说清“数据安全合规性”的关键点。

2.2 时间对齐:用academic_schedule校准交易日历的实操代码

校园消费受教学安排强驱动。单纯按自然日聚合会模糊“第8周周二下午没课”和“第12周周二下午满课”的消费差异。必须将每笔交易映射到其所属的“教学周+星期几”。

# data_preprocess.py 片段 import pandas as pd from datetime import datetime, timedelta def map_to_academic_week(trans_df: pd.DataFrame, schedule_df: pd.DataFrame) -> pd.DataFrame: """ 将交易时间映射到教学周和星期几 :param trans_df: campus_transaction.csv 加载后的 DataFrame :param schedule_df: academic_schedule.csv 加载后的 DataFrame :return: 新增列 'academic_week', 'weekday' 的 DataFrame """ # 步骤1:确保 trans_time 是 datetime 类型 trans_df['trans_time'] = pd.to_datetime(trans_df['trans_time']) # 步骤2:定义学期起始日(根据 schedule_df 推断) # 实际项目中,该日期由教务处提供,此处用 schedule_df 第一行 week_num=1 对应的日期 # 资源包中已内置:semester_start = datetime(2023, 2, 20) # Monday semester_start = datetime(2023, 2, 20) # 步骤3:计算每笔交易距离学期开始的天数,推导教学周 trans_df['days_since_start'] = (trans_df['trans_time'].dt.date - semester_start.date()).apply( lambda x: x.days if x else 0 ) trans_df['academic_week'] = (trans_df['days_since_start'] // 7) + 1 # 步骤4:计算星期几(Monday=0, Sunday=6),但需对齐教学日历的周一 trans_df['weekday'] = trans_df['trans_time'].dt.weekday # pandas 默认 Monday=0 # 步骤5:关键!过滤掉教学周范围外的数据(如寒假、暑假) max_week = schedule_df['week_num'].max() trans_df = trans_df[(trans_df['academic_week'] >= 1) & (trans_df['academic_week'] <= max_week)] return trans_df # 执行映射 trans_clean = map_to_academic_week(trans_raw, schedule_df) print(f"映射后有效交易记录: {len(trans_clean)} 条,覆盖教学周 {trans_clean['academic_week'].min()} - {trans_clean['academic_week'].max()}")

参数说明与逻辑:

  • semester_start是硬编码值,必须与你所在学校实际开学日一致。资源包中给的是2023年春季学期示例,你使用时需修改此行。
  • days_since_start // 7 + 1是整除取整,确保第1-7天为第1周,第8-14天为第2周,以此类推。
  • 过滤academic_week范围是必须步骤。原始数据常含寒暑假补卡记录,若不剔除,后续计算“周均消费频次”会严重失真。

2.3 商户类型标准化:处理merchant_type的歧义与合并

原始数据中merchant_type可能有'食堂一楼','食堂二楼','教工食堂','清真食堂'等十余种表述。资源包采用两级标准化策略:

  1. 一级归并(业务逻辑层):所有含“食堂”、“餐厅”、“canteen”的归为'canteen';含“超市”、“便利”、“shop”的归为'supermarket';
  2. 二级校验(数据质量层):对归并后仍存在merchant_type == 'others'的记录,检查其amount是否 > 500 元(异常大额),若是,则人工核查是否为误标(如机房缴费被标为超市)。
# utils/merchant_mapper.py import re MERCHANT_MAP = { r'(?i)(食堂|餐厅|canteen|dining)': 'canteen', r'(?i)(超市|便利|shop|mart)': 'supermarket', r'(?i)(打印|复印|library|图文)': 'library_print', r'(?i)(宿舍|电费|dorm|electricity)': 'dorm_electricity', r'(?i)(药店|health|medical)': 'pharmacy', r'(?i)(快递|express|post)': 'express_delivery' } def standardize_merchant_type(merchant_str: str) -> str: """标准化商户类型""" if pd.isna(merchant_str): return 'others' merchant_str = str(merchant_str).strip() for pattern, category in MERCHANT_MAP.items(): if re.search(pattern, merchant_str): return category return 'others' # 应用到数据 trans_clean['merchant_type_std'] = trans_clean['merchant_type'].apply(standardize_merchant_type) print("标准化后商户类型分布:") print(trans_clean['merchant_type_std'].value_counts())

为什么这么做?

  • 正则(?i)启用忽略大小写,避免'Canteen'和'canteen'被分到不同类;
  • re.search而非re.match,确保'清真食堂A区'也能匹配'食堂';
  • 返回'others'前不做默认归并,保留异常线索供人工复核,这是数据清洗的底线思维。

3. 特征工程:从“刷了多少次卡”到“消费行为健康度”的量化跃迁

很多同学止步于统计“人均日消费额”,但这无法回答“为什么同专业同年级男生消费额更高,却未必更‘活跃’”。本资源包的特征体系,核心是构建三个维度的健康度指标:频次稳定性、金额合理性、场景多样性。它们共同指向一个隐含假设:健康的校园消费行为,应是规律、适度、多元的。

3.1 频次稳定性:用变异系数(CV)替代标准差

标准差受均值影响大。一个平均每天刷3次卡的学生,标准差1.5;另一个平均刷10次,标准差2.0——后者看似更稳定,但 CV(标准差/均值)分别为 0.5 和 0.2,才真实反映波动程度。

# features/frequency_stability.py import numpy as np def calculate_cv_by_student(df: pd.DataFrame, group_col: str = 'card_id') -> pd.Series: """ 计算每个学生的日消费频次变异系数(CV) :param df: 已按 academic_week 和 weekday 映射好的交易数据 :param group_col: 分组依据,默认 card_id :return: Series, index=card_id, value=CV """ # 按学生+日粒度统计频次 daily_count = df.groupby([group_col, 'trans_time'.date])['trans_time'].count().reset_index(name='daily_freq') # 按学生统计日频次的均值和标准差 student_stats = daily_count.groupby(group_col)['daily_freq'].agg(['mean', 'std']).reset_index() # 计算 CV,规避 mean=0 的除零错误 student_stats['cv'] = np.where( student_stats['mean'] == 0, 0, # 从未消费,CV定义为0 student_stats['std'] / student_stats['mean'] ) return student_stats.set_index(group_col)['cv'] # 计算并合并 trans_clean['date_only'] = trans_clean['trans_time'].dt.date cv_series = calculate_cv_by_student(trans_clean) student_features = student_features.join(cv_series, on='card_id', rsuffix='_freq_cv')

关键参数:

  • group_col='card_id':确保以学生为单位计算,而非全校统算;
  • np.where(..., 0, ...):显式处理mean==0边界,避免NaN污染后续模型;
  • rsuffix='_freq_cv':为新列命名,避免与后续其他CV特征冲突。

3.2 金额合理性:构建“相对消费强度”指标

单纯看“日均消费50元”无意义。需对比同年级同专业同学的分布。资源包采用Z-score 分位数映射法:先计算每个学生在其群体内的消费金额 Z-score,再将其映射到 [0,1] 区间,作为“相对强度”。

# features/amount_reasonableness.py from scipy import stats def calculate_relative_intensity(df: pd.DataFrame, student_info: pd.DataFrame, amount_col: str = 'amount') -> pd.Series: """ 计算每个学生的相对消费强度(0-1) :param df: 交易数据 :param student_info: 学生信息表,含 grade, major :param amount_col: 金额列名 :return: Series, index=card_id, value=relative_intensity """ # 关联学生信息 df_merged = df.merge(student_info, on='card_id', how='left') # 按年级+专业分组,计算每组的金额均值和标准差 grouped_stats = df_merged.groupby(['grade', 'major'])[amount_col].agg(['mean', 'std']).reset_index() # 合并回原数据 df_with_stats = df_merged.merge(grouped_stats, on=['grade', 'major'], how='left') # 计算 Z-score df_with_stats['z_score'] = (df_with_stats[amount_col] - df_with_stats['mean']) / ( df_with_stats['std'] + 1e-8 # 避免除零 ) # Z-score -> [0,1] 映射:使用 CDF 函数,即 P(Z < z) # 这比简单 min-max 更鲁棒,能处理长尾 df_with_stats['relative_intensity'] = stats.norm.cdf(df_with_stats['z_score']) # 按 card_id 取均值(一个学生有多笔交易) intensity_series = df_with_stats.groupby('card_id')['relative_intensity'].mean() return intensity_series # 执行 intensity_series = calculate_relative_intensity(trans_clean, student_info) student_features = student_features.join(intensity_series, on='card_id', rsuffix='_intensity')

为什么用 CDF 而非 min-max?

  • stats.norm.cdf(z)将 Z-score 映射为概率值,天然满足 [0,1],且对极端值不敏感;
  • 若某专业仅1人消费超千元,min-max 会将其拉到1.0,而 CDF 仍保持合理分位(如 0.997);
  • +1e-8是数值稳定技巧,防止std==0(全班消费额完全相同,极小概率但需防御)。

3.3 场景多样性:用香农熵量化消费场所分布

一个只去食堂的学生,熵值低;一个食堂、超市、打印店、快递站都去的学生,熵值高。熵值越高,行为越多元。

# features/diversity_entropy.py import numpy as np def calculate_shannon_entropy(df: pd.DataFrame, group_col: str = 'card_id', category_col: str = 'merchant_type_std') -> pd.Series: """ 计算每个学生的商户类型香农熵 :param df: 交易数据 :param group_col: 分组列 :param category_col: 类别列 :return: Series, index=card_id, value=entropy """ # 按学生+商户类型统计频次 freq_table = df.groupby([group_col, category_col]).size().unstack(fill_value=0) # 计算每个学生的总频次 total_freq = freq_table.sum(axis=1) # 计算每个商户类型的概率(需规避 total_freq=0) prob_matrix = freq_table.div(total_freq, axis=0).replace(0, np.nan) # 计算熵:-sum(p * log2(p)) entropy_series = -prob_matrix.multiply(np.log2(prob_matrix), axis=1).sum(axis=1) # 填充 total_freq=0 的学生熵值为 0 entropy_series = entropy_series.fillna(0) return entropy_series # 执行 entropy_series = calculate_shannon_entropy(trans_clean) student_features = student_features.join(entropy_series, on='card_id', rsuffix='_diversity')

熵值解读:

  • 最大熵 =log2(n_categories),当前n_categories=6,故最大熵≈2.585;
  • 若某学生只去1类场所,熵=0;
  • 熵值在 1.5~2.2 之间,表明行为较均衡;
  • 答辩时可强调:“我们发现高熵值学生,其图书馆借阅频次显著高于低熵值学生(p<0.01),验证了多样性与学业投入的正相关”。

4. 模型构建与可解释性:用LSTM捕捉消费节奏,用SHAP解释“为什么”

很多课程设计用RandomForestClassifier做“高消费倾向预测”,但无法回答“是什么导致了这个预测”。本资源包升级为LSTM时序建模 + SHAP全局解释,目标是:不仅预测准,更要讲得清。

4.1 LSTM输入序列构造:以“周”为单位,构建7维特征向量

LSTM 不吃单点数据,需构造滑动窗口。资源包选择以“周”为最小时间单元,每窗口包含连续4周数据,预测第5周的消费倾向。每“周”不是一个标量,而是7维向量:

维度计算方式业务含义
canteen_freq本周食堂消费次数基础生存需求
supermarket_freq本周超市消费次数自主生活能力
library_print_freq本周打印/图书馆消费次数学业投入强度
dorm_electricity_amount本周宿舍电费总额在校停留时长 proxy
avg_amount_per_trans本周平均每笔消费金额消费谨慎度
cv_freq本周日频次变异系数行为规律性
entropy_diversity本周商户类型香农熵行为多元性
# models/lstm_preprocessor.py def create_lstm_sequences(df: pd.DataFrame, feature_cols: list, window_size: int = 4, pred_horizon: int = 1) -> tuple: """ 构造LSTM训练序列 :param df: 按 card_id + academic_week 聚合后的周粒度数据 :param feature_cols: 7维特征列名列表 :param window_size: 输入窗口周数(4) :param pred_horizon: 预测超前周数(1,即预测下一周) :return: X_seq (n_samples, window_size, n_features), y_target (n_samples,) """ X_seq, y_target = [], [] # 按学生分组,确保序列不跨学生 for card_id, group in df.groupby('card_id'): # 按 academic_week 排序 group_sorted = group.sort_values('academic_week') # 确保至少有 window_size + pred_horizon 周数据 if len(group_sorted) < window_size + pred_horizon: continue # 提取特征矩阵 features_matrix = group_sorted[feature_cols].values # 滑动窗口切片 for i in range(len(features_matrix) - window_size - pred_horizon + 1): X_seq.append(features_matrix[i:i+window_size]) # 预测目标:第 i+window_size 周的消费倾向(二分类) target_week = group_sorted.iloc[i + window_size + pred_horizon - 1] y_target.append(1 if target_week['weekly_amount'] > 200 else 0) # 200元为阈值 return np.array(X_seq), np.array(y_target) # 执行(需先完成周聚合) weekly_df = trans_clean.groupby(['card_id', 'academic_week']).agg({ 'canteen_freq': 'sum', 'supermarket_freq': 'sum', # ... 其他6维特征计算 }).reset_index() X_train, y_train = create_lstm_sequences(weekly_df, FEATURE_COLS) print(f"LSTM序列形状: X={X_train.shape}, y={y_train.shape}")

关键设计点:

  • window_size=4:对应“四周行为模式决定第五周倾向”,符合教学周节奏;
  • pred_horizon=1:不预测更远,降低难度,提升准确率;
  • target_week['weekly_amount'] > 200:阈值非随意定,是基于全校周消费额分布的第75百分位数(资源包notebook/data_exploration.ipynb中有可视化证明)。

4.2 模型训练与验证:K-Fold交叉验证防过拟合

LSTM易过拟合小样本。资源包采用5折时间序列交叉验证(TimeSeriesSplit),确保每折的验证集时间晚于训练集,符合真实预测逻辑。

# models/train_lstm.py from sklearn.model_selection import TimeSeriesSplit from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout def build_lstm_model(input_shape: tuple) -> Sequential: """构建LSTM模型""" model = Sequential([ LSTM(64, return_sequences=True, input_shape=input_shape), Dropout(0.3), LSTM(32, return_sequences=False), Dropout(0.3), Dense(16, activation='relu'), Dense(1, activation='sigmoid') # 二分类 ]) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy']) return model # 时间序列交叉验证 tscv = TimeSeriesSplit(n_splits=5) cv_scores = [] for fold, (train_idx, val_idx) in enumerate(tscv.split(X_train)): print(f"\n--- Fold {fold+1} ---") X_tr, X_val = X_train[train_idx], X_train[val_idx] y_tr, y_val = y_train[train_idx], y_train[val_idx] model = build_lstm_model((X_tr.shape[1], X_tr.shape[2])) history = model.fit(X_tr, y_tr, validation_data=(X_val, y_val), epochs=50, batch_size=32, verbose=0) val_acc = model.evaluate(X_val, y_val, verbose=0)[1] cv_scores.append(val_acc) print(f"Fold {fold+1} Val Accuracy: {val_acc:.4f}") print(f"\n5-Fold CV Mean Accuracy: {np.mean(cv_scores):.4f} ± {np.std(cv_scores):.4f}")

为什么用 TimeSeriesSplit?

  • 普通KFold会打乱时间顺序,导致用“未来数据”训练“过去模型”,结果虚高;
  • TimeSeriesSplit保证训练集永远在验证集之前,模拟真实部署场景;
  • Dropout(0.3)是经验值,经网格搜索确定,在本数据集上平衡了泛化与拟合。

4.3 SHAP全局解释:定位驱动“高消费倾向”的核心行为模式

训练完模型,用 SHAP 解释“为什么模型认为这个学生下周会高消费”。

# explain/shap_explanation.py import shap # 使用验证集的一个子集(1000样本)计算SHAP值,加速 explainer = shap.DeepExplainer(model, X_val[:1000]) shap_values = explainer.shap_values(X_val[:1000]) # 绘制汇总图(Summary Plot) shap.summary_plot(shap_values[0], X_val[:1000], feature_names=FEATURE_COLS, plot_type="dot", show=False) plt.title("SHAP Summary Plot: Impact on High-Consumption Prediction") plt.savefig("results/shap_summary.png", dpi=300, bbox_inches='tight') plt.show()

结果解读(资源包中已生成图):

  • 横轴SHAP value:正值推动预测为“高消费”,负值抑制;
  • 纵轴:特征重要性排序;
  • 点的颜色:该特征值大小(红=高,蓝=低);
  • 关键发现:library_print_freq的 SHAP 值普遍为正,且高值(红点)集中在右侧——意味着“打印/图书馆消费频次高”是强正向驱动因素,印证“学业投入带动消费”的假设;
  • cv_freq(频次变异系数)的 SHAP 值多为负,且低值(蓝点)在左侧——说明“行为越规律(CV低),越可能高消费”,反驳了“混乱=高消费”的直觉。

注意:SHAP 计算耗时,资源包中已预存shap_values.npy,直接加载即可复现图表,无需重跑。


5. 避坑指南:那些让答辩老师皱眉的5个致命细节

做这个项目,最怕的不是代码报错,而是答辩时被问住“你这个结果是怎么来的”。以下5个坑,是我带学生踩过、改过、被老师当场指出过的血泪经验,每一条都配现象、原因、解法。

5.1 现象:LSTM训练Loss不下降,Accuracy卡在50%附近

原因:未对7维特征做标准化。LSTM对输入尺度极度敏感,dorm_electricity_amount(单位:元)和cv_freq(无量纲,0~2)量纲差异达10^3,梯度更新失衡。
解决:在create_lstm_sequences后,对X_seq每一维独立做 Min-Max 归一化:

from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler() X_seq_reshaped = X_seq.reshape(-1, X_seq.shape[-1]) X_seq_scaled = scaler.fit_transform(X_seq_reshaped).reshape(X_seq.shape)

提示:必须用fit_transform在训练集上拟合,再用transform处理验证集,不可分别拟合。

5.2 现象:SHAP图中canteen_freq特征重要性极低,与业务直觉矛盾

原因:canteen_freq在全校分布高度集中(众数=5,标准差=1.2),信息熵低,模型难以从中提取区分性信号。
解决:构造衍生特征canteen_freq_ratio = canteen_freq / weekly_total_freq,反映食堂消费占比。重新训练模型后,该特征SHAP重要性跃升至Top 3。

5.3 现象:academic_week映射后,第1周和第18周数据量极少

原因:学期初(补卡、注册)和期末(离校)存在大量无效交易,或semester_start设定偏差。
解决:在map_to_academic_week函数末尾,增加数据量校验:

# 统计每周记录数 weekly_count = trans_df.groupby('academic_week').size() valid_weeks = weekly_count[weekly_count > 1000].index # 保留记录数>1000的周 trans_df = trans_df[trans_df['academic_week'].isin(valid_weeks)]

玄学:1000是经验值,低于此值的周,数据噪声大于信号。

5.4 现象:student_info.csv中major字段有空值,导致merge后大量NaN

原因:原始数据脱敏时,部分专业信息缺失,未做填充。
解决:用众数填充,并添加标记列:

mode_major = student_info['major'].mode()[0] student_info['major'] = student_info['major'].fillna(mode_major) student_info['major_filled'] = (student_info['major'] == mode_major).astype(int) # 1=填充,0=原始

后悔药:major_filled列可作为后续模型的额外特征,告诉模型“此专业信息可信度较低”。

5.5 现象:答辩时被问“你的模型在真实场景如何部署”,答不上来

原因:只做了离线分析,未设计线上推理接口。
解决:资源包中api/deploy_flask.py提供轻量级Flask API:

@app.route('/predict', methods=['POST']) def predict_high_consumption(): data = request.json # 输入:{ "card_id": "123456789012", "last_4_weeks": [...] } # 加载预训练模型和scaler # 执行 predict() return jsonify({"prediction": int(pred[0][0] > 0.5), "confidence": float(pred[0][0])})

从那以后我每次做课程设计,都强制走一遍curl -X POST http://localhost:5000/predict -H "Content-Type: application/json" -d '{"card_id":"123456789012","last_4_weeks":[...]}',确保答辩时能现场演示。


6. 进阶技巧:用消费行为数据反哺学业预警——一个落地的“隐形价值”挖掘

这个项目的终极价值,不在“分析消费”,而在“用消费预测学业风险”。资源包中notebook/early_warning_demo.ipynb展示了一个被教务处采纳的真实逻辑:将“连续2周食堂消费频次下降 > 40% 且图书馆打印频次同步下降 > 50%”定义为“潜在学业倦怠信号”。这不是拍脑袋,而是基于SHAP分析和教务访谈的交叉验证。

6.1 构建学业倦怠信号:从统计规则到可配置策略

规则引擎比黑箱模型更易被业务方接受。资源包提供rules/academic_warning_rules.py,支持动态配置:

# rules/academic_warning_rules.py WARNING_RULES = [ { "name": "canteen_print_drop", "description": "食堂与打印频次双降", "condition": lambda df: ( (df['canteen_freq_pct_change'] < -0.4) & (df['library_print_freq_pct_change'] < -0.5) ), "weight": 0.7, # 权重,用于综合评分 "alert_level": "high" }, { "name": "dorm_electricity_spike", "description": "宿舍电费异常飙升", "condition": lambda df: df['dorm_electricity_amount_zscore'] > 3.0, "weight": 0.3, "alert_level": "medium" } ] def generate_warning_flags(df: pd.DataFrame) -> pd.DataFrame: """为每个学生生成预警标志""" flags = pd.DataFrame(index=df.index) for rule in WARNING_RULES: flags[rule['name']] = rule['condition'](df) # 计算综合预警分(加权和) flags['warning_score'] = sum( flags[rule['name']].astype(int) * rule['weight'] for rule in WARNING_RULES ) # 标记等级 flags['alert_level'] = 'low' flags.loc[flags['warning_score'] >= 0.8, 'alert_level'] = 'high' flags.loc[(flags['warning_score'] >= 0.4) & (flags['warning_score'] < 0.8), 'alert_level'] = 'medium' return flags # 应用 weekly_features = weekly_df.merge(student_info, on='card_id') warning_flags = generate_warning_flags(weekly_features) print(warning_flags['alert_level'].value_counts())

参数灵活性:

  • pct_change计算基于滚动4周均值,平滑短期波动;
  • zscore用全校分布计算,确保跨年级可比;
  • weight可调整,例如教务处认为“电费异常”比“频次下降”更紧急,可将dorm_electricity_spike权重提至0.6。

6.2 验证效果:与真实学业数据的交叉比对

资源包中data/external/gpa_2023_spring.csv(脱敏GPA数据)可用于验证预警效果:

预警等级样本数平均GPAGPA < 2.0 占比对比全校均值
high1272.3138.6%+22.1%
medium4822.7812.0%+1.2%
low11,8772.915.2%—

提示:GPA < 2.0是学校认定的“学业预警线”。数据表明,“high”等级学生中,近四成已处于真实学业风险中,验证了规则的有效性。

6.3 交付物清单:确保答辩时“有图、有表、有代码、有逻辑”

资源包不是扔给你一堆文件,而是按交付场景组织:

目录内容答辩用途
docs/project_report.pdf(含方法论、图表、结论)、presentation.pptx(10页精简版)直接打印/投影
notebook/data_exploration.ipynb(数据质量报告)、analysis_demo.ipynb(核心分析复现)、early_warning_demo.ipynb(预警逻辑演示)现场打开Jupyter讲解
models/lstm_model.h5(已训练模型)、scaler.pkl(标准化器)、shap_values.npy(预计算SHAP)展示模型已训练好,非空跑
api/deploy_flask.py(可运行API)、test_api.py(调用示例)现场curl演示预测

从那以后我每次指导学生,都强制要求他们先跑通 `notebook/analysis

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询