简介:面向高校数据分析学习者、管理决策者及相关课题开发者,这份基于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, encoding="utf-8", parse_time_col="交易时间"): """ 读取校园卡消费流水,返回统一结构的 DataFrame。 两份 CSV 字段可能不完全一致,这里统一重命名。 """ df = pd.read_csv(path, encoding=encoding) # 常见别名归一:不同导出系统列名不同 rename_map = { "学号": "student_id", "卡号": "student_id", "消费时间": "trade_time", "交易时间": "trade_time", "消费金额": "amount", "交易金额": "amount", "消费场所": "location", "商户名称": "location", } df = df.rename(columns=rename_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] # 时间列转 datetime,errors="coerce" 让脏数据变成 NaT df["trade_time"] = pd.to_datetime(df["trade_time"], errors="coerce") df["amount"] = pd.to_numeric(df["amount"], errors="coerce") return df.dropna(subset=["trade_time", "amount"])参数说明:encoding默认给 UTF-8,但你在 Windows 上跑大概率要改成"gbk"或者"gb18030",后面避坑章会讲怎么快速判断;parse_time_col指定时间列名,不同学校导出系统的命名习惯不一样,这个参数就是给你做适配用的。errors="coerce"是把脏数据置为缺失值而不是直接报错,属于读数据阶段比较稳妥的做法。两份文件都加载后用pd.concat(ignore_index=True)纵向合并就行。我要提醒一句:如果合并后行数暴增到几百万,先不要急着全量跑,在第 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(subset=dedup_cols, keep="first") 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_clusters=3): # 挑选参与聚类的特征列,量纲差异大必须先标准化 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_clusters=n_clusters, random_state=42, n_init=10) 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_init=10是让 KMeans 用 10 个不同初始中心跑 10 次取最优,避免随机初始化导致聚类结果不稳定,这个参数在 scikit-learn 1.4 之后默认值就是 10,但老版本默认是 10 也建议显式写出来;random_state=42保证可复现。最核心的rank_map那段逻辑,是把算法输出的无意义簇编号按日均消费从低到高重排成 0、1、2——0 号代表消费水平最低的群体,后续评分、报告展示都基于这个重排后的编号。
4.3 从分群到评分:用分位数把聚类结果压成 0~100 的经济分
聚类只能给出「你是哪一类」,但管理场景往往需要更精细的数值。比如助学金评定,老师希望看到一个排序,而不是「你是第 0 类」。这时候在聚类基础上再加一层评分卡:对每个特征计算分位数得分,再按权重合成。
def economic_score(df, feature="daily_avg", reverse=True): """ 计算经济特征的分位数得分。 reverse=True 表示消费金额越低,得分越高(代表经济压力更大)。 """ q = df[feature].rank(pct=True) if reverse: score = 100 - q * 100 else: score = q * 100 return np.round(score, 1)逻辑说明:rank(pct=True)算出每个学生在该特征上的百分位排名,值域 0 到 1。reverse=True时,日均消费最低的人拿到接近 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", encoding="gbk")。建议直接在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里还是有 NaN,KMeans 对 NaN 的处理就是把该样本归到距离最近的簇,一样会失衡。
5.4 时间序列图的时间轴是乱的:时间列格式不统一
现象:visualizer.py画出月度趋势图,横轴异常跳跃,或者每月数据点忽多忽少。
原因:数据里时间格式不统一,比如2024/09/01和2024-09-01 12:30混在一起,pd.to_datetime正常解析了大部分,但混入的 Excel 序列号或者「2024.9.1」这种格式变成了NaT或解析到错误日期。
解决:用errors="coerce"先把解析失败的变成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="校园消费分析", layout="wide") @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_index=True) 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"], bins=30, kde=True, ax=ax) st.pyplot(fig) with col_right: st.subheader("分群人数占比") # 用 value_counts 归一化成百分比直接展示 share = result["economic_level"].value_counts(normalize=True).sort_index() st.bar_chart(share)代码里的@st.cache_data是 Streamlit 的缓存装饰器,作用是让数据加载和预处理只在第一次运行时执行,后面每次交互操作都会复用缓存结果,否则每次滑块变动都要重跑一遍全流程,几万行数据会明显卡顿。要注意的是带缓存的函数参数必须是可哈希的,传文件路径字符串没问题,传 DataFrame 进去会报错。
6.2 验证聚类结果是否可信:一套必须走完的三步检查
图表渲染出来不代表模型是对的。我拿到聚类结果先不做报告,而是先做三步验证。第一步,打印每个聚类的特征均值表,确认群组之间在日均消费、食堂占比上有梯度差异,并且梯度方向符合业务直觉。第二步,从每个聚类里随机抽 5 个学生,回到原始流水里人工翻看他们的消费记录,确认标签和实际行为能对上——这一步看起来原始,但它能抓住聚类分错的大部分情况,比如某个「低消费」组里混进了大量当月只在校几天的实习生。第三步,检查聚类结果随时间的稳定性:用上个月的数据训练聚类,用这个月的数据转换预测,看同一个学生的分群是否发生大规模跳变。如果 30% 以上学生换了群组,说明特征选得有问题或者时间窗口太短,结论需要谨慎使用。
这三步走完,我对聚类结果才有基本的信任。从那以后我每次处理一卡通数据,都强制在建模后走一遍这三步检查再进入报告撰写环节。做多了你会发现,真正有价值的往往不是那个最终分数,而是验证过程中发现的反常样本——它们才是校园管理里最需要关注的具体的人。希望这份拆解对你有帮助,照着跑一遍,你会比自己想象的更快理解这条从流水到决策的完整链路。
本文还有配套的精品资源,点击获取