ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于一卡通数据的校园消费行为分析与学生经济评估方案

基于一卡通数据的校园消费行为分析与学生经济评估方案 简介面向校园消费行为分析与学生经济评估场景这份基于 Python 的完整项目资源提供了从数据加载、预处理到建模与可视化的全链路实现。资源包含源码、样例数据、运行说明与详细报告适合学习数据分析、机器学习或进行课设/毕设参考的开发者。包体共 24 个文件以 Python 脚本(.py)与编译缓存(.pyc)为主另含 CSV 数据文件、Markdown 使用文档、PDF/Word 说明报告及启动脚本整体约 13.06MB结构清晰便于按模块查找。项目内部按 data_loader、preprocessor、model、visualizer、app 模块化组织分别对应数据读取、预处理、KMeans 聚类、Plotly 可视化与 Streamlit 界面展示便于二次开发。预处理环节涵盖异常消费过滤、时间特征提取与特征标准化主应用提供实时比对、群体画像等三种分析模式。覆盖消费时段分布、食堂消费占比及三维聚类特征等分析维度说明报告与运行文档有助于快速上手已有 142 人学习下载适合想掌握数据挖掘完整流程的读者。1. 校园消费行为分析与学生经济评估从一个数据集到一张评估表“校园消费行为分析与学生经济评估”这类项目资源拿到的通常是一整套可运行交付物源码、脱敏后的校园消费数据集、运行文档教程和说明报告。它解决的问题很具体把每天几十万行一卡通刷卡流水变成每个学生每月的消费画像再结合消费结构给出一个消费水平指数供学工部门做经济困难认定初筛、助学金评定参考。适合毕设、课设也适合高校信息化里“数据中台建好了但指标没人算”的落地场景。这里有一个容易误判的点月均消费金额低不直接等于经济困难因为食堂、超市、医务室、图书馆自助设备的消费性质完全不同。一个只刷早晚餐的学生和一个月刷几次超市的学生同样花 600 元含义不一样。所以第一步不是建模而是把流水按食堂与非食堂、工作日与周末、白天与夜间拆开先把口径定清楚。下面的方案就是一套能照做的分析流程从原始流水到最终报告每一步都给出命令和参数。2. 从一卡通流水到学生消费特征清洗口径与 pandas / SQL 实现2.1 原始数据集的三个表结构与字段约定校园消费行为分析属于典型的大数据量但小体量任务行数多字段少。常见交付的数据集是一个 data 目录至少包含三张表trade_data.csv 保存每一笔刷卡流水shop_info.csv 保存商户类型student_info.csv 保存学生基本属性。字段可以按下面的约定设计这套字段也是高校一卡通系统导出时的常见形态。表关键字段备注trade_datatrade_id, student_id, shop_id, trade_time, amount, pay_type一行为一笔消费shop_infoshop_id, shop_name, categorycategory 取值食堂、超市、医务室、洗浴、图书student_infostudent_id, college, grade, dorm_area, scholarship_flag关联用注意脱敏第一步先把 trade_data 按时间排序检查退款和负数记录。一卡通系统中退款通常以负数出现也有部分实现用 pay_typerefund 标记各校不统一。遇到这种情况按 amount 0 过滤最稳妥同时要保留退款笔数统计进说明报告让清洗比例可追溯。为什么用月度而不是日度特征日度粒度噪音太大考试周、节假日停课都会让单日流水突变年度粒度又太粗覆盖不了学生用卡习惯的学期内变化。月度特征在稳定性与时效性之间最平衡也方便后续按月重算、输出月度报表。2.2 数据清洗三种必须处理的坏数据我一般会先查三类问题这也是项目里数据不一致的原因最常见所在。第一类是同一学生换卡student_id 没变但卡片物理号变了流水里会出现同一学生在同一秒产生两条相同金额的记录这是系统重传第二类是测试账号student_id 以 TEST 开头或者学号长度不等于正常位数这类要直接剔除第三类是“幽灵扣费”金额等于 0 或单笔超过 100 元通常由刷卡设备故障产生。数据不一致的原因大多是这三类叠加而不是字段本身错位。清洗代码可以写成独立脚本保证重复运行时口径一致import pandas as pd df pd.read_csv(trade_data.csv, parse_dates[trade_time]) df df[df[amount] 0] df df[~df[student_id].str.upper().str.startswith(TEST)] df df[(df[amount] 0.5) (df[amount] 100)] # 同学生同一秒同金额视为重复流水去重 df df.drop_duplicates(subset[student_id, trade_time, amount])参数说明0.5 元以下多为设备零扣费测试100 元作为单笔金额上限是兼顾食堂和超市整单消费的常用选择如果你想收紧可以先看金额分布的 99.5 分位再定。drop_duplicates 的 subset 三个字段同时命中才删除避免把真实消费误删。清洗后要输出一个剔除比例这个数字会写进说明报告让评估结论可追溯。2.3 用 pandas 抽取月度消费特征清洗完成后进入特征抽取。特征是模型的地基我按三个维度构造金额维度看月总消费和月度标准差频次维度看消费天数和消费笔数结构维度看夜间消费占比和超市非必需消费占比。下面的代码把每一笔流水归到“学生 年 月”的粒度上df[y] df[trade_time].dt.year df[m] df[trade_time].dt.month df[day] df[trade_time].dt.date df[hour] df[trade_time].dt.hour df[is_night] ((df[hour] 21) | (df[hour] 6)).astype(int) df[night_amt] df[amount] * df[is_night] df[market_amt] df[amount] * df[category].eq(超市).astype(int) feat df.groupby([student_id, y, m]).agg( month_amt(amount, sum), month_cnt(amount, size), active_days(day, nunique), monthly_std(amount, std), night_amt(night_amt, sum), market_amt(market_amt, sum), )逻辑说明is_night 把晚上 9 点到次日早上 6 点标记为夜间覆盖夜宵和便利店场景market_amt 统计超市消费金额作为“非必需消费”备选特征因为在不少学校超市消费与家庭经济水平的相关性比食堂消费更强。monthly_std 表示单笔金额波动波动大说明该学生既有食堂小额消费又有超市整单消费能帮聚类区分“稳定食堂人群”和“混合消费人群”。2.4 流水在 MySQL 时用等价 SQL 聚合如果数据不是 CSV 而是落在 MySQL 关系库里特征是同一个口径直接用 SQL 聚合即可这也是运行文档教程里要提供的等价实现。把 trade_data 换成实际库表名DATE_FORMAT 在 MySQL 8.0 和 5.7 都可用SELECT student_id, DATE_FORMAT(trade_time, %Y-%m) AS ym, SUM(amount) AS month_amt, COUNT(*) AS month_cnt, COUNT(DISTINCT DATE(trade_time)) AS active_days, STDDEV(amount) AS monthly_std, SUM(CASE WHEN HOUR(trade_time) 21 OR HOUR(trade_time) 6 THEN amount ELSE 0 END) AS night_amt FROM trade_data WHERE amount 0 AND student_id NOT LIKE TEST% GROUP BY student_id, ym HAVING month_cnt 5参数说明HAVING month_cnt 5 把当月仅有零星几笔记录的学生剔除这类学生大概率近期未在校使用一卡通纳入建模会污染分层STDDEV 计算的是样本标准差数据量足够时与 pandas 默认的 ddof1 结果一致。SQL 和 pandas 两个版本必须输出相同行数这是运行文档里自检命令的一部分。3. 学生经济评估的消费分层指数KMeans 聚类与阈值界定3.1 为什么先做消费分层而不是直接算“贫富”学生经济评估在项目里是辅助决策任务不是分类任务。交付数据里没有“经济困难”和“非困难”的真实标签只有少量勤工俭学、助学金发放记录可以做交叉验证。所以整个路径是先用无监督方法把全体学生按消费行为切成几层输出可排序的消费指数再根据这个指数生成候选名单。KMeans 是这个场景最常见的起始模型原因很实际计算快结果可解释聚类中心能翻译成“低月均消费、低超市占比”这类人话。但它的前提假设是各簇形状接近球形对极端值和偏态分布敏感。校园消费数据里 month_amt 是典型的右偏分布一小部分高消费学生会把均值拉高。所以先标准化还不够我一般会先看分布如果偏度大于 1先做 log1p 变换再标准化。3.2 特征标准化与确定 k 值的完整代码先拿近 3 个月的月度特征合并成每个学生一行。合并时连续 3 个月有记录的学生取均值不满 3 个月的用已有月度数据补均值并在特征里加一个 month_count 表示覆盖月份数后续低覆盖学生单独处理不混进主聚类。import numpy as np from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score cols [month_amt, month_cnt, active_days, night_amt, market_amt] feat_all feat3m[cols].fillna(0) feat_all[month_amt_log] np.log1p(feat_all[month_amt]) cols[0] month_amt_log scaler StandardScaler() X scaler.fit_transform(feat_all[cols]) k_scores [] sil_scores [] for k in range(2, 7): model KMeans(n_clustersk, n_init10, random_state42).fit(X) k_scores.append(model.inertia_) sil_scores.append(silhouette_score(X, model.labels_))逻辑说明inertia 是簇内平方和曲线拐点对应手肘位置silhouette_score 范围是 -1 到 1越接近 1 说明簇越紧凑。校园消费数据通常 k3 或 k4 出现明显拐点k4 最常用因为能解释成低、中、较高、高四个消费层级。跑完后把两个指标打印出来对比不要只看 inertia 一个值。3.3 KMeans 分层结果转换与消费指数计算公式k 确定后重新训练模型并把聚类中心反标准化后排序最低的类别是 L1。这里有一个常见误区KMeans 的 label 编号是随机的必须先按聚类中心的 month_amt 排序重新编号再把编号映射回原数据。k 4 model KMeans(n_clustersk, n_init10, random_state42) feat_all[cluster] model.fit_predict(X) center pd.DataFrame( scaler.inverse_transform(model.cluster_centers_), columnscols ) center[cluster] range(k) center center.sort_values(month_amt_log).reset_index(dropTrue) rank_map {cluster: fL{idx1} for idx, cluster in enumerate(center[cluster])} feat_all[level] feat_all[cluster].map(rank_map)参数说明n_init10 表示每个 k 值用 10 个不同的初始中心跑取最优解防止局部最优random_state42 固定随机种子保证每次运行结果一致这在生成说明报告时很重要。注意 center 排序用的列是 month_amt_log因为聚类输入是对数变换后的值。层级映射后再生成一个 0 到 100 的消费水平指数。公式采用 min-max 归一化把 month_amt 映射过去。相比直接用金额指数在做跨学期比较时不受整体物价波动影响语义更好解释。def to_index(s): return ((s - s.min()) / (s.max() - s.min()) * 100).round(1) feat_all[consume_index] to_index(feat_all[month_amt])注意这是消费水平指数不是“困难度指数”。经济困难是结果的一层解释不在这一步输出。提示KMeans 的 label 不要直接入库必须先经过中心排序映射成 L1-L4。很多后续报表引用旧 label 导致层级含义颠倒排错一般就卡在这。3.4 分层结果表与交叉验证口径分层后每一层的中心值整理成结果表写进说明报告层级月均消费参考区间月均消费天数夜间消费表现建议动作L1全校最低通常处于 P10-P20低于全体中位数占比低与资助记录交叉验证L2低于全校中位数中位附近波动大供辅导员人工复核L3中位附近稳定正常继续观察L4明显高于中位高频明显偏高不进入初筛这里的建议动作是常见做法L1 层并不直接等于“应资助名单”而是“需关注候选”。把 L1 名单与学工系统的助学金发放记录、勤工俭学记录做交集算两个指标命中率建档名单里有多少落在 L1/L2和覆盖率L1/L2 里有多少在历史资助名单中。这一步能定量回答“分层结果靠不靠谱”也是说明报告里最有说服力的一页。4. 项目源码、运行文档与说明报告交付物的组织与复现步骤4.1 资源包的目录结构与依赖管理拿到这样的资源包最先看的不是代码而是 README 和 requirements.txt。运行文档教程通常按“环境安装、数据放置、脚本顺序、报告生成”四个步骤组织。下面是一个可复现的目录结构按这个结构组织能避免“脚本在这个机器能跑、换个机器就崩”的经典问题campus_consumption_analysis/ ├── data/ │ ├── raw/ # 原始流水、商户表、学生表只读 │ ├── processed/ # 清洗后的特征表 │ └── backup/ # 备份目录 ├── src/ │ ├── build_features.py │ ├── cluster_eval.py │ └── report_gen.py ├── notebooks/ │ └── 01_explore.ipynb ├── docs/ │ └── 运行文档.md ├── report/ │ └── 说明报告.html ├── requirements.txt └── README.mdrequirements.txt 建议锁定主库不锁传递依赖pandas、numpy、scikit-learn、jinja2、matplotlib。Python 版本要求写在 README 里常见做法是 3.10 及以上。老版本 pandas 对 groupbyagg 的字符串写法支持有差异运行文档教程里要写清楚。4.2 从零复现的完整命令序列运行文档教程里真正值钱的是命令执行顺序先安装依赖再把原始数据放进 data/raw/然后依次执行三个脚本。pip install -r requirements.txt python src/build_features.py \ --input data/raw/trade_data.csv \ --merchant data/raw/shop_info.csv \ --output data/processed/student_month_feat.csv python src/cluster_eval.py \ --feature data/processed/student_month_feat.csv \ --k 4 \ --out report/level_result.csv python src/report_gen.py \ --result report/level_result.csv \ --template templates/dashboard.html \ --out report/说明报告.html逻辑说明build_features.py 负责清洗和特征抽取输出中间产物cluster_eval.py 读中间产物做标准化、聚类和指数计算report_gen.py 用 Jinja2 模板把结果表、图表和说明文字拼成最终 HTML。这样拆的好处是任意一步挂了只需要重跑那一步不用重新清洗流水。运行完检查三点每个脚本退出码为 0level_result.csv 行数等于学生总数报告里 L1 到 L4 人数占比在可解释范围内。如果 L1 超过 30%大概率是数据覆盖不全而不是学生真的困难回去查 active_days 分布。4.3 说明报告应包含的四块内容说明报告不是把代码注释抄一遍而是要回答“这个评估为什么不直接用月均消费排序”。常见的说明报告包括四块数据来源与脱敏说明、清洗口径与剔除比例、模型选择与 k 值确定依据、结论与局限性。其中局限性要写清楚两点一是消费数据只能反映校内一卡通场景外卖、网购、走读生完全不覆盖二是聚类结果受学期结构影响考试月和寒暑假前一个月消费模式会突变。报告生成用 Jinja2 模板比直接写 HTML 高效。模板里放 {{ percent_l1 }}、{{ chart_l1 }} 这类占位符report_gen.py 负责把图表 base64 编码后填入。这样每次跑月度数据报告样式不会漂移。4.4 数据备份与恢复的落地命令数据备份与恢复在这个项目里容易被忽略原因是一份 CSV 不大删了也无所谓。但 processed 特征表是中间产物一旦丢失重新生成要跑完整条链路还可能因为源数据变化导致结果漂移。我一般会在运行前做一次快照cp -r data/raw data/backup/raw_$(date %F) cp data/processed/student_month_feat.csv data/backup/feat_$(date %F).csv参数说明备份带日期后缀是为了和上次运行结果做差异对比确认源数据没有变化后再继续。如果流水表在 MySQL 里运行文档里补充 mysqldump 单表导出命令备份到一个表就够了。5. 评估结果验证数据一致性检查与聚类稳定性校验到了最后一步结果不是跑完脚本就收工还要验证“换个随机种子结果变不变”。这个项目的数据量通常在几万人量级KMeans 的初始中心随机性会导致标签抖动同一个学生这次跑是 L1下次变成 L2说明分层边界不可靠。可以用 adjusted_rand_score 做稳定性校验from sklearn.metrics import adjusted_rand_score from sklearn.cluster import KMeans def stability_check(X, k4, runs20): labels [] for seed in range(runs): model KMeans(n_clustersk, n_init10, random_stateseed) labels.append(model.fit_predict(X)) scores [adjusted_rand_score(labels[0], label) for label in labels[1:]] return float(np.min(scores)), float(np.mean(scores))逻辑说明ARI 是衡量两次聚类标签一致性的指标取 1 表示完全一致取 0 表示随机一致。对同一份特征矩阵跑 20 个不同 random_state计算第一组标签与其余 19 组的 ARI最低值低于 0.9 就说明边界不稳定。此时优先检查是不是特征里有极端离群值而不是急着调 k。把标准化前后的分布打出来month_amt 的右偏是导致不稳定的第一嫌疑。数据一致性检查放在稳定性检查之前还是之后我习惯先做数据再做模型稳定性。把这两段检查写进一个 verify.py配合上一章提到的带日期备份每个月跑一次月度数据时就能自动对比。如果你们学校的流水是从两个校区合并来的把按校区的 active_days 透视也加进同一份巡检脚本哪个校区某天断传、哪批学生一卡通换号都能在分层之前暴露出来。本文还有配套的精品资源点击获取
返回列表