拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于Python的校园消费行为分析与经济评估系统实战解析

简介面向高校数据分析学习者、管理决策者及相关课题开发者这份基于Python的校园消费行为分析与经济评估系统提供完整可运行的源码与数据。系统依托校园智能卡消费记录运用pandas、numpy进行数据处理借助seaborn、plotly等完成可视化并通过scikit-learn构建评估模型整体串联数据预处理、特征工程、可视化展示和模型评估四大模块能识别食堂、超市、图书馆等场景下的消费行为特征构建个人经济画像为助学金分配、商业布局优化提供科学依据。资源包共20个文件包含Python脚本、pyc预处理模块、csv样本数据、项目说明报告pdf/docx及配置备份zbak/zip整体约15.08MB目录结构清晰便于按需查阅。已有74人学习下载适合需要快速理解端到端数据分析流程、并希望基于实际校园数据开展实践的研究者与开发者随附的说明文档与备份文件还可辅助二次开发与方案复盘。1. 从一张校园卡流水说起校园消费行为分析到底在解决什么问题在高校里食堂、超市、图书馆的每一笔刷卡记录其实都在回答一个问题这个学生的月度消费盘子有多大、花在哪、稳不稳定。比起问卷访谈和辅导员主观判断一卡通流水是少有的、几乎零成本的客观数据源。这套基于Python的校园消费行为分析与经济评估系统做的就是把几万行刷卡流水清洗成特征表再聚类分群、给经济状况打分最终在Streamlit界面上直接看到结果。适合三类人做课设的学生需要一套能跑通、能答辩的完整链路高校信息化或学工部门想用数据辅助助学金评定还有刚接触pandas和scikit-learn的人想知道一条真实数据管线是怎么从CSV走到可视化报表的。它能帮你省掉从零搭框架的时间也能让你看清模型结论哪些可信、哪些要人工复核。2. 数据入口先搞清两份 CSV 的字段与读取方式拿到压缩包先别急着streamlit run app.py我一般先把目录结构过一遍。这套项目的核心文件很清晰app.py是应用入口data/下放着data1.csv和data2.csv两份消费明细core/里四个模块各管一段——data_loader.py负责读数据preprocessor.py负责清洗和特征工程model.py跑经济评估visualizer.py生成图表。另外还有一堆settings.py.zbak、*.pyc.zbak、备份文件.zip这类打包时混进来的冗余文件不影响运行但建议直接忽略或删除后面第 5 章会专门说它们的坑。2.1 目录与文件职责运行文件、数据文件和打包残留怎么区分先看根目录下的README.md和学生校园消费行为分析项目说明报告.pdf这两份是项目的说明文档环境要求、模块设计、运行方式都写在里面。真正的可执行代码只有app.py和core/下的四个.py文件settings.py是配置文件通常放数据路径、图表样式、特征列名这类常量。数据文件方面data1.csv和data2.csv的分工在项目里比较常见一份更接近原始刷卡流水一份是已经做过部分聚合的中间表或者按时间切分的另一段数据。我没有办法替你确认这两份文件的准确差异但按这套系统的定位它们合并起来才构成完整的分析基础。遇到这类情况我建议你解压后先做一个动作head -5 data/data1.csv head -5 data/data2.csv ls -lh data/*.csvhead看前五行目的是确认列名和样例值ls -lh看文件大小判断哪份是主数据、哪份是辅助数据。校园一卡通数据的字段设计大同小异核心字段大概率落在这些列上学号、消费时间、消费金额、消费场所食堂窗口/超市/开水房、交易类型消费/退款/充值、卡内余额。你需要做的是实际看一次文件头把列名记下来后面settings.py里的字段映射全部要跟它对齐。2.2 data_loader 的读取策略编码、列类型与两份数据合并core/data_loader.py这个模块做的事情不复杂把 CSV 读进来处理编码和列类型然后合并成一份干净的 DataFrame。校园数据的 CSV 常见两处坑就在这个阶段一是编码很多从教务系统导出的文件是 GBK 或 GB2312直接用 pandas 默认的 UTF-8 读会抛UnicodeDecodeError二是时间列如果读进来是字符串后面所有按时段聚合的活都做不了。这个项目的data_loader.py核心逻辑大概是这样的模式import pandas as pd def load_consumption_data(path, encodingutf-8, parse_time_col交易时间): 读取校园卡消费流水返回统一结构的 DataFrame。 两份 CSV 字段可能不完全一致这里统一重命名。 df pd.read_csv(path, encodingencoding) # 常见别名归一不同导出系统列名不同 rename_map { 学号: student_id, 卡号: student_id, 消费时间: trade_time, 交易时间: trade_time, 消费金额: amount, 交易金额: amount, 消费场所: location, 商户名称: location, } df df.rename(columnsrename_map) # 只保留真正需要的列避免未知列干扰后续特征 keep_cols [student_id, trade_time, amount, location] for col in keep_cols: if col not in df.columns: df[col] None df df[keep_cols] # 时间列转 datetimeerrorscoerce 让脏数据变成 NaT df[trade_time] pd.to_datetime(df[trade_time], errorscoerce) df[amount] pd.to_numeric(df[amount], errorscoerce) return df.dropna(subset[trade_time, amount])参数说明encoding默认给 UTF-8但你在 Windows 上跑大概率要改成gbk或者gb18030后面避坑章会讲怎么快速判断parse_time_col指定时间列名不同学校导出系统的命名习惯不一样这个参数就是给你做适配用的。errorscoerce是把脏数据置为缺失值而不是直接报错属于读数据阶段比较稳妥的做法。两份文件都加载后用pd.concat(ignore_indexTrue)纵向合并就行。我要提醒一句如果合并后行数暴增到几百万先不要急着全量跑在第 3 章的特征聚合阶段会做一次按学号和日期分组的压缩。2.3 启动前的环境准备这些库一个都不能少项目摘要里给了明确的依赖清单数据处理是 pandas 和 numpy可视化是 matplotlib、seaborn、plotly机器学习是 scikit-learn交互界面是 streamlit。安装命令在摘要里已经是现成的pip install pandas numpy matplotlib seaborn scikit-learn streamlit plotly我实际装的时候发现几个注意点。第一如果本机有多个 Python 环境比如 conda 和系统 Python 共存先确认pip指向的是你要用的那个解释器最稳的办法是python -m pip install ...第二streamlit和plotly版本有时候会和旧版 pandas 打架建议在干净环境里一次性装最新版第三装完之后用下面这条命令验证python -c import pandas, numpy, matplotlib, seaborn, sklearn, streamlit, plotly; print(all ok)任何一行报ModuleNotFoundError就说明对应包没装进当前环境逐个补装即可。这套验证命令我建议你养成习惯——不是每个项目都会像这份代码一样把所有依赖写在 README 里自己动手确认环境是最可靠的。3. 预处理与特征工程把刷卡流水变成可训练的特征表原始流水长什么样跑过一卡通数据的人都懂一个人一天可能刷十几次食堂两顿、超市一瓶水、图书馆打印两次金额从几毛到几十都有。如果直接把这种粒度喂给模型意义不大。经济评估关心的不是某一笔消费而是一个学生在一段时间内的稳定消费模式。所以preprocessor.py的价值就是把细碎的流水压缩成「每个学生一行特征」的宽表。3.1 数据清洗重复记录、负金额和异常时段怎么处理清洗顺序我一般是固定的先去重再处理缺失值最后过滤异常值顺序不能反。如果先过滤再取重可能把本来应该保留的有效退款记录误伤掉。去重有一个真实的坑同一秒、同一学号、同一金额、同一窗口的记录有可能是系统重发产生的重复但也可能是两笔真实消费前后脚发生。常见做法是加一列「时间差」只有完全相同的记录才删掉时间差在几秒内但属于不同交易编号的保持原样。你可以在preprocessor.py里看到类似这样的逻辑def deduplicate(df): # 完全相同的流水视为系统重复保留第一条 dedup_cols [student_id, trade_time, amount, location] df df.drop_duplicates(subsetdedup_cols, keepfirst) return df负金额的问题更隐蔽。刷卡流水里出现负数大部分是退款或退卡余额少数是冲正操作。要不要保留我建议是保留下来单独打一列is_refund标志而不是直接删除。因为一个学生短期内频繁退款本身就是一个行为特征——可能是刷错了反复退也可能是代刷后转账结算。直接删掉会丢失这部分语义。异常时段处理主要看食堂数据。正常消费时间落在 6:00 到 23:00 之间凌晨两三点的刷卡记录多半是宿舍门禁或便利店夜间消费混进来了。如果你的分析对象限定在食堂消费就把时间过滤条件写成(df[trade_time].dt.hour 6) (df[trade_time].dt.hour 23)如果连超市、图书馆一起分析这个条件就不要加否则会自动丢掉图书馆的夜间开放记录。3.2 特征抽取月度消费、日均水平、离散度和食堂外占比清洗完的流水要聚合到学生维度。下面的特征我把它们按用途分成三类金额水平类、稳定性类、结构类。金额水平反映整体开销稳定性反映消费习惯是否规律结构类反映钱花在哪些场景。这份代码基本可以照抄到你自己的项目里import pandas as pd import numpy as np def build_student_features(df): # 先造一个月份列方便按自然月聚合 df[month] df[trade_time].dt.to_period(M) # 金额中位数比均值更抗异常值干扰 agg df.groupby(student_id).agg( total_amount(amount, sum), avg_amount(amount, mean), median_amount(amount, median), amount_std(amount, std), trade_count(amount, count), active_days(trade_time, lambda s: s.dt.date.nunique()), unique_locations(location, nunique), ).reset_index() # 日均消费总金额 / 活跃天数比总额更能排除离校天数影响 agg[daily_avg] agg[total_amount] / agg[active_days].replace(0, np.nan) # 离散系数波动大小除以平均水平消除绝对金额差异 agg[cv] agg[amount_std] / agg[avg_amount].replace(0, np.nan) return agg参数说明to_period(M)是 pandas 里把时间戳归到月份的标准做法后面做月度同环比会用到active_days用了nunique统计不重复日期数量这是一个容易被忽略但很重要的特征——两个学生总金额一样一个每天雷打不动三顿饭另一个只在校十天但每次点外卖大额消费经济评估结论应该完全不同cv是变异系数标准差除以均值消除金额绝对值差异后再比较波动性。结构类特征还要看消费场所分布。我的做法是计算食堂消费金额占比以及食堂之外超市、奶茶、快递站的占比。食堂占比高通常说明消费结构偏刚需超市和校外消费占比高可选消费空间更大。预处理阶段把location用str.contains匹配关键词比如包含「食堂」「餐厅」「一楼」的打上is_canteen标签再按student_id聚合出占比列。3.3 聚合粒度与时间窗口的选择全学期聚合还是按月滚动特征工程里逃不掉的一个问题是聚合窗口选多长。全学期聚合会得到一个总画像信息密度高但时间细节全丢按月滚动可以看到趋势但每个月都可能遇到数据缺失——某个月学生可能实习离校、可能放假月消费直接跳水不是经济紧张而是人不在学校。我建议的折中方案是主特征表用最近一个完整月的窗口计算附加上一个月的特征做差值比率。比如「本月日均消费 / 上月日均消费 - 1」这个增长率特征能反映消费水平的变化方向对经济状况突变有很强的提示作用。放暑假的那个月直接剔除不参与建模。这个窗口策略你在跑这份数据的时候可以通过改settings.py里的WINDOW_MONTHS常量来调节代码里预留了配置口子比我写死在函数里要实用得多。4. 经济评估模型聚类分群与评分规则怎么落地特征表建好之后核心问题变成怎么把「月均消费 400 元、食堂占比 85%、波动小」这类特征组合转换成一个可解释的经济状况结论。这里我不会直接建议你上回归预测或者神经网络——因为校园消费数据没有标准的标签你很难拿到每个学生的真实家庭收入做监督训练所以更可行的路线是无监督聚类再加一套规则评分卡。这个项目落在model.py里的也是这个思路。4.1 为什么选聚类而不是直接给阈值没有真实标签时的稳妥路线先回答一个必须想清楚的问题为什么不用阈值比如「月消费低于 800 元就是经济困难」。原因是消费金额绝对值受地区物价、食堂定价、学生个人饮食习惯影响太大同一所学校里男生女生平均消费能差出 30% 以上。用统一阈值切必然误伤一部分小饭量或减肥人群。聚类的好处是让数据自己说话。特征经过标准化之后算法按消费模式把学生分成几类每一类内部的消费特征相对一致类与类之间有可解释的差异。这份系统里用的是 scikit-learn 的KMeans聚类数一般取 3 到 4 类——太多了不好解释太少了分不开。判断聚类数的肘部法则可以用但实际我更看重每类在「日均消费」和「食堂占比」两个核心维度上是否有业务可解释的差异。4.2 聚类落代码标准化、分群、给每个群体贴上业务标签下面是标准化的建模流程。注意StandardScaler必须用训练集拟合后用同一个对象转换测试数据不能拆开各 fit 一次这个细节写代码时容易忽略但后果很严重from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler import numpy as np def assess_economic_status(feature_df, n_clusters3): # 挑选参与聚类的特征列量纲差异大必须先标准化 feature_cols [daily_avg, cv, canteen_ratio, active_days] X feature_df[feature_cols].fillna(0).values scaler StandardScaler() X_scaled scaler.fit_transform(X) kmeans KMeans(n_clustersn_clusters, random_state42, n_init10) labels kmeans.fit_predict(X_scaled) # 把聚类标签映射为业务标签按各类日均消费均值从低到高排序 result feature_df.copy() result[cluster] labels group_avg result.groupby(cluster)[daily_avg].mean() # 最低消费群组标记为 0 号顺序可能每次不同 rank_map {cluster: rank for rank, cluster in enumerate(group_avg.sort_values().index)} result[economic_level] result[cluster].map(rank_map) return result参数说明n_init10是让 KMeans 用 10 个不同初始中心跑 10 次取最优避免随机初始化导致聚类结果不稳定这个参数在 scikit-learn 1.4 之后默认值就是 10但老版本默认是 10 也建议显式写出来random_state42保证可复现。最核心的rank_map那段逻辑是把算法输出的无意义簇编号按日均消费从低到高重排成 0、1、2——0 号代表消费水平最低的群体后续评分、报告展示都基于这个重排后的编号。4.3 从分群到评分用分位数把聚类结果压成 0100 的经济分聚类只能给出「你是哪一类」但管理场景往往需要更精细的数值。比如助学金评定老师希望看到一个排序而不是「你是第 0 类」。这时候在聚类基础上再加一层评分卡对每个特征计算分位数得分再按权重合成。def economic_score(df, featuredaily_avg, reverseTrue): 计算经济特征的分位数得分。 reverseTrue 表示消费金额越低得分越高代表经济压力更大。 q df[feature].rank(pctTrue) if reverse: score 100 - q * 100 else: score q * 100 return np.round(score, 1)逻辑说明rank(pctTrue)算出每个学生在该特征上的百分位排名值域 0 到 1。reverseTrue时日均消费最低的人拿到接近 100 分代表经济压力更大这样「分数越高越需要关注」的语义统一方便字段排序。实际项目里一般会对每日消费、食堂占比、波动性三个特征分别打分再按 6:3:1 加权合成总分。权重放在settings.py里方便学工处调整——有的学校更看重月消费绝对额有的更看重食堂依赖度。聚类和打分的关系要说明白聚类负责把人群粗分成几档评分负责在档内细分排序。两者结合比单纯聚类更能落到具体决策上。5. 避坑与排查五条可复现的现场记录这套系统跑起来不难但我在实际环境和不同机器上试过之后总结出五条真实踩过的坑。每一条都是「现象、原因、解决」的结构建议你把这一章当作排错手册遇到问题按图索骥。5.1 启动报错ModuleNotFoundError: plotly依赖装了一半现象streamlit run app.py跑起来界面刚渲染到图表区域直接抛ModuleNotFoundError: No module named plotly。原因摘要里给的 pip 安装命令是一整条但有人会手快只复制了前几个包或者本机之前装过旧版 streamlit把plotly这个可选依赖漏掉了。解决不要只装 plotly最好把整条依赖用下面命令全量重装一遍避免版本不匹配python -m pip install --upgrade pandas numpy matplotlib seaborn scikit-learn streamlit plotly装完后重启 streamlit 进程。我个人的习惯是只相信python -m pip因为直接输pip可能指向别的 Python 环境。5.2 CSV 中文乱码pandas 默认读不了 GBK 文件现象data_loader.py读 CSV 报错或者读进来了但没有报错打印表头发现全是乱码。原因学校教务系统导出的 CSV 很多是 GBK 或 GB2312 编码pandas 默认用 UTF-8 解码遇到 GBK 字符直接变成乱码或抛UnicodeDecodeError。解决先确认文件编码再改加载参数with open(data/data1.csv, rb) as f: raw f.read(100) print(raw) # 如果看到 \xd1\xa7\xc9\xfa 这类字节就是 GBK 编码确认之后把load_consumption_data(/path/to.csv, encodinggbk)。建议直接在settings.py里把CSV_ENCODING utf-8改成gbk全局生效。5.3 聚类结果几乎全都挤在同一类没做标准化的典型症状现象model.py跑出来的economic_level分布严重失衡90% 以上学生被分到同一类另外几类只有零星几个人。原因这是最典型的「特征没标准化」症状。daily_avg的数量级在几百canteen_ratio是 0 到 1 的小数KMeans 用欧氏距离算相似度金额特征的绝对值把占比特征完全压制了聚类结果等于只按消费金额单维度切分。解决确认在fit_transform之前使用了StandardScaler。如果已经用了再检查是不是在fillna(0)之后没重新赋值——fillna返回新对象原数据没变X里还是有 NaNKMeans 对 NaN 的处理就是把该样本归到距离最近的簇一样会失衡。5.4 时间序列图的时间轴是乱的时间列格式不统一现象visualizer.py画出月度趋势图横轴异常跳跃或者每月数据点忽多忽少。原因数据里时间格式不统一比如2024/09/01和2024-09-01 12:30混在一起pd.to_datetime正常解析了大部分但混入的 Excel 序列号或者「2024.9.1」这种格式变成了NaT或解析到错误日期。解决用errorscoerce先把解析失败的变成NaT打印出这些行看看长什么样bad df[pd.isna(df[trade_time])] print(bad.head(20))看是哪种格式没被识别然后针对性写一个预处理函数手动规范化后再转时间类型。不要指望 pandas 自动猜对所有格式。5.5 备份文件混进目录导致 Python 导入了旧代码现象改了core/preprocessor.py里的逻辑但运行结果和改之前一模一样甚至报错信息里的行号对不上当前文件。原因压缩包里带着data_loader.py.zbak、settings.cpython-39.pyc.zbak这些备份和缓存文件。Python 的 import 机制在开发目录下可能把旧的.pyc或者同名的.zbak文件当成了模块缓存导致实际执行的不是你编辑的文件。解决清理目录只保留运行必需的文件find . -name *.zbak -delete find . -name __pycache__ -type d -exec rm -rf {} find . -name *.pyc -deletefind参数说明-name按文件名匹配-type d只匹配目录-exec rm -rf {} 对匹配结果依次执行删除。清理完之后再跑一次基本都能恢复正常。这也是为什么我拿到任何源码包的第一件事是把所有.zbak、.pyc、__pycache__清干净再开始改代码。6. 可视化与结果验证上线前不只看图还要看特征分组差异visualizer.py和app.py做的是把前几步的中间结果变成人类能看懂的东西。但我要提醒一个更重要的点图表不只是拿来展示的更是拿来验证模型结果的。可视化做到最后我关注的是「聚类结果是否合理」这件事能不能直接体现在图里。6.1 Streamlit 界面与图表联动核心代码怎么写app.py的整体结构是读取数据、调用预处理和模型、把结果渲染成多个页面区块。Streamlit 的写法非常适合这种数据管线因为它天然按从上到下的顺序执行和你的分析流程一致import streamlit as st import matplotlib.pyplot as plt import seaborn as sns from core.data_loader import load_consumption_data from core.preprocessor import build_student_features from core.model import assess_economic_status st.set_page_config(page_title校园消费分析, layoutwide) st.cache_data def load_all(): df1 load_consumption_data(data/data1.csv) df2 load_consumption_data(data/data2.csv) df pd.concat([df1, df2], ignore_indexTrue) return df df load_all() features build_student_features(df) result assess_economic_status(features) st.title(校园消费行为与经济评估) col_left, col_right st.columns(2) with col_left: st.subheader(月均消费分布) fig, ax plt.subplots(figsize(8, 4)) sns.histplot(result[daily_avg], bins30, kdeTrue, axax) st.pyplot(fig) with col_right: st.subheader(分群人数占比) # 用 value_counts 归一化成百分比直接展示 share result[economic_level].value_counts(normalizeTrue).sort_index() st.bar_chart(share)代码里的st.cache_data是 Streamlit 的缓存装饰器作用是让数据加载和预处理只在第一次运行时执行后面每次交互操作都会复用缓存结果否则每次滑块变动都要重跑一遍全流程几万行数据会明显卡顿。要注意的是带缓存的函数参数必须是可哈希的传文件路径字符串没问题传 DataFrame 进去会报错。6.2 验证聚类结果是否可信一套必须走完的三步检查图表渲染出来不代表模型是对的。我拿到聚类结果先不做报告而是先做三步验证。第一步打印每个聚类的特征均值表确认群组之间在日均消费、食堂占比上有梯度差异并且梯度方向符合业务直觉。第二步从每个聚类里随机抽 5 个学生回到原始流水里人工翻看他们的消费记录确认标签和实际行为能对上——这一步看起来原始但它能抓住聚类分错的大部分情况比如某个「低消费」组里混进了大量当月只在校几天的实习生。第三步检查聚类结果随时间的稳定性用上个月的数据训练聚类用这个月的数据转换预测看同一个学生的分群是否发生大规模跳变。如果 30% 以上学生换了群组说明特征选得有问题或者时间窗口太短结论需要谨慎使用。这三步走完我对聚类结果才有基本的信任。从那以后我每次处理一卡通数据都强制在建模后走一遍这三步检查再进入报告撰写环节。做多了你会发现真正有价值的往往不是那个最终分数而是验证过程中发现的反常样本——它们才是校园管理里最需要关注的具体的人。希望这份拆解对你有帮助照着跑一遍你会比自己想象的更快理解这条从流水到决策的完整链路。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门