资讯详情 Python校园一卡通消费行为分析:从模拟数据到K-means聚类
📅 2026/10/11 16:57:12
简介面向高校计算机、数据分析方向毕业设计及课设场景这份资源以Python为工具完整实现学生校园消费行为分析流程。内容覆盖数据清洗、学生信息与消费记录关联、食堂早中晚就餐人数与工作日/非工作日就餐时间分析以及不同性别、专业学生群体的消费特点、人均消费与刷卡频次对比贴合校园一卡通数据的实际分析场景。压缩包共30个文件约21.28MB主要包含5个Python脚本、8个CSV数据、9个PNG可视化结果图、1份DOCX分析报告并辅以XML等工程配置文件脚本与数据分层存放便于按任务模块对照复现。目前已有1067人学习或下载适合需要快速搭建毕业设计代码框架、理清数据分析流程并产出图文报告的学生参考。从中可掌握数据清洗与字段关联、pandas分组聚合、可视化出图及分析报告撰写等关键技能同时获得可运行的完整示例代码、中间结果与结论图表能显著缩短从数据处理到最终展示的开发周期。1. 校园消费行为分析不是写个爬虫Python毕业设计怎么选题才算完整拿到“Python-学生校园消费行为-毕业设计”这个题目时很多人的第一反应是“这不就是个数据分析题吗”然后交上去一个塞满图表的 Jupyter Notebook答辩时被问“你的系统架构是什么”“数据从哪来”“这个结果对学校有什么用”就问懵了。实际上这是一个从数据采集、清洗、特征工程、聚类分析到 Web 可视化的小型信息系统题也是计算机、软件工程、大数据方向最稳妥的基于 Python 的毕业设计选题之一。整篇可以按“模拟数据 Pandas 预处理 K-means 聚类 Streamlit 展示”的主线走不需要爬虫、不需要真实校园数据库也能把每个环节讲得有据可查。这篇笔记按我当时带过的落地路径写从造数据开始到能演示、能答辩、能继续深化为止。2. 数据准备与清洗模拟一套可信的校园一卡通流水2.1 字段设计先想清楚要回答什么问题别急着写代码先回答三个问题你想发现什么规律学校的运营方会关心什么你的算法需要哪些字段支撑学生校园消费行为最常见的研究问题就四类一是分群把学生按消费习惯分成“规律食堂型”“超市囤货型”“作息混乱型”等二是异常识别比如某个账户消费频率突增或长期空置三是时间规律比如早中晚餐峰值、周末和节假日的消费波动四是简单的消费预测比如预测明天的食堂营业额。毕设你只需要做透第一类加第三类异常识别作为进阶加分项预测放最后。对应地原始流水表只需要五个字段就够字段类型说明card_idstr学号或卡号建模时只当标识不参与聚类trade_timedatetime消费时间精确到分钟store_namestr消费地点如“一食堂”“超市”“浴室”amountfloat消费金额单位元pay_typestr支付方式刷脸/刷卡/扫码可留作备用维度很多同学一上来就想着做关联规则想把“买了泡面的人还买了什么”做进系统里。有这个规划没问题但字段里必须能区分商品而校园一卡通流水通常只到商户粒度没有商品明细。这就是为什么我建议先按上述字段把基础设施搭好后面真要扩展再考虑模拟一套虚拟的小卖部订单明细而不是反过来为了凑关联规则硬改数据结构。2.2 用NumPy生成符合作息的消费记录没有真实校园一卡通数据的情况下模拟数据是毕设非常正常的做法关键在于把生成逻辑做好让数据带有真实的作息规律而不是均匀随机分布。你需要在答辩时说清楚“数据是按什么规律生成的噪声是怎么加的”这比支支吾吾说“从网上找的”可信得多。我一般用 NumPy 直接生成不依赖 Faker减少装包环节。生成前设定好三条规则每天消费频次落在 1~4 次之间金额按时段分布早餐 2~8 元午晚餐 8~18 元超市 1~50 元每个学生对不同商户有稳定偏好同时叠加随机波动。import numpy as np import pandas as pd from datetime import datetime, timedelta np.random.seed(42) n_students 500 # 模拟 500 个学生 n_days 120 # 模拟 120 天约一个学期 base_date datetime(2024, 3, 1) students [f2023{str(i).zfill(3)} for i in range(1, n_students 1)] stores { 一食堂: 0.38, 二食堂: 0.24, 超市: 0.20, 浴室: 0.12, 医务室: 0.06, } store_names list(stores.keys()) store_probs list(stores.values()) rows [] for s in students: # 每个学生对商户有偏好权重基础概率上叠加个体偏移 pref np.random.dirichlet(np.ones(len(store_names)) * 1.2 0.1) pref pref / pref.sum() # 生成 120 天内的消费记录 for d in range(n_days): cur_date base_date timedelta(daysd) day_count np.random.poisson(2.3) # 日均 2~3 笔 day_count max(1, min(day_count, 4)) for _ in range(day_count): # 40% 概率用个人偏好60% 概率用全校分布 if np.random.rand() 0.4: store np.random.choice(store_names, ppref) else: store np.random.choice(store_names, pstore_probs) hour np.random.choice([7, 8, 11, 12, 17, 18, 21, 22], p[0.05, 0.20, 0.15, 0.25, 0.15, 0.10, 0.05, 0.05]) minute np.random.randint(0, 60) trade_time cur_date.replace(hourhour, minuteminute) if 食堂 in store: if hour 10: amount round(np.random.normal(5.5, 1.8), 2) elif 10 hour 15: amount round(np.random.normal(13.0, 3.5), 2) else: amount round(np.random.normal(14.0, 4.0), 2) elif store 超市: amount round(np.random.normal(18.0, 12.0), 2) elif store 浴室: amount round(np.random.normal(2.0, 0.6), 2) else: amount round(np.random.normal(30.0, 20.0), 2) amount abs(round(amount, 2)) if hour 7 or hour 22: amount amount # 保留深夜消费后续有用途 rows.append([s, trade_time, store, amount, np.random.choice([刷卡, 扫码])]) df pd.DataFrame(rows, columns[card_id, trade_time, store_name, amount, pay_type]) print(df.shape) print(df.head())这段代码的几个关键参数值得说清楚np.random.seed(42)保证每次生成的数据一致答辩时重新运行不会变lambda2.3的泊松分布控制每天消费笔数均值略大于两笔贴合三餐加零食的真实场景时段概率p把上午 8 点、中午 12 点两个高峰体现出来这是后续“消费时段热力分析”的基础对每个学生用dirichlet生成独立偏好向量避免所有人都按全校平均比例消费否则聚类会非常困难。生成后用abs()兜底金额为负的情况并且保留凌晨消费很多校园浴室深夜是有热水刷卡的这一部分数据后面有分析价值。2.3 清洗与异常值过滤夜宵和深夜热水别一刀切拿到模拟数据后第一反应是“有没有脏数据”比“数据是否真实”更重要。清洗的目标是去掉会影响统计结果的异常值而不是把看起来奇怪的记录全删掉。df df[df[amount] 0.1] # 过滤金额小于一毛的记录 df df[df[amount] 200] # 正常校园单笔消费极少超过 200 df df[df[trade_time] 2024-03-01] df df[df[card_id].str.match(r^\d{7}$)] # 卡号必须 7 位纯数字 # 只删除 1:00~4:30 之间金额异常的幽灵消费 night_suspect (df[trade_time].dt.hour.isin([1, 2, 3, 4])) (df[amount] 50) df df[~night_suspect] print(清洗后记录数:, len(df)) print(每日平均消费笔数:, len(df) / n_days)清洗里面最容易踩的坑就是“把凌晨数据全删掉”。凌晨一两点出现的浴室刷卡或者超市泡面消费在分析夜猫子型学生时恰恰是最关键的特征。我这边只删除了凌晨时段金额大于 50 元的极端记录其他凌晨数据全部保留。金额过滤阈值设成 200 也是经验值食堂一笔打到 200 属于替多人代刷单次分析价值不大留作离群点检测话题更好。卡号过滤则是模拟真实一卡通系统里可能混入的测试卡和临时卡。清洗之后建议顺手生成一份“清洗报告”记录原始行数、删除行数和各条过滤规则的命中数量。这个报告放到答辩 PPT 里比任何说明都直观。到这里你已经拥有了一份 120 天、几百名学生、数万行消费流水的干净数据集撑起一个毕设的数据量完全足够。3. 消费特征工程与K-means聚类把数万条流水变成三类人3.1 用户级特征RFM模型在校园场景的改法原始流水是一卡一天多条记录而聚类需要把每个学生聚成一类所以必须先做用户级聚合把每个学生的消费行为压缩成几个特征。电商常用的 RFM 模型在这里要改校园消费的频率 F 区分度很高最近一次消费时间 R 在封闭校园里意义不大消费金额 M 由于食堂单价低不能直接照搬要换成“月均消费额”和“单笔波动”。我把特征定在六个维度覆盖消费水平、频次、习惯、作息四个方向特征名计算方式作用total_amount120 天总消费额区分消费水平trade_count总消费笔数区分活跃度daily_avg日均消费笔数活跃度的另一视角breakfast_rate早餐笔数 / 总笔数识别规律作息型night_rate21 点后消费占比识别夜猫子型amount_std单笔金额标准差区分平稳消费和波动消费user df.groupby(card_id).agg( total_amount(amount, sum), trade_count(amount, count), daily_avg(amount, count), amount_std(amount, std), ) # 早餐6:00~9:00 之间的食堂消费夜消费21:00 之后所有消费 df[is_breakfast] df[trade_time].dt.hour.between(6, 9) df[is_night] df[trade_time].dt.hour 21 night_ratio df.groupby(card_id)[is_night].mean() breakfast_ratio df.groupby(card_id)[is_breakfast].mean() user[breakfast_rate] breakfast_ratio user[night_rate] night_ratio user[daily_avg] user[trade_count] / n_days user user.fillna(0)这里有个容易忽略的细节groupby(card_id).agg()里对同一列只能用一个聚合函数所以total_amount和trade_count可以同时算但如果想算“午餐平均金额”这类特征就得先构造一个子集再 groupby然后join回来。daily_avg用总笔数除以模拟天数避免不同学生激活时间不同导致的偏差这个细节在学生缺卡场景下特别重要真实数据里经常有学生开学三周才办卡。早餐和夜间占比用groupby mean直接得到比例比手动计数再相除更干净。3.2 聚类参数标准化、手肘法与轮廓系数怎么选KK-means 是这类题目的首选聚类算法解释成本低答辩时老师也容易理解。但 K-means 对特征尺度极其敏感total_amount动辄几千breakfast_rate只有 0~1如果不做标准化聚类结果会完全由金额决定。from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score import matplotlib.pyplot as plt import numpy as np feat_cols [ total_amount, trade_count, daily_avg, breakfast_rate, night_rate, amount_std, ] scaler StandardScaler() X_scaled scaler.fit_transform(user[feat_cols]) # 手肘法与轮廓系数结合选 K scores [] silhouettes [] K_range range(2, 9) for k in K_range: km KMeans(n_clustersk, random_state42, n_init10, max_iter500) labels km.fit_predict(X_scaled) scores.append(km.inertia_) silhouettes.append(silhouette_score(X_scaled, labels)) fig, ax1 plt.subplots() ax1.plot(K_range, scores, o-, labelinertia) ax2 ax1.twinx() ax2.plot(K_range, silhouettes, s--, colororange, labelsilhouette) plt.show()为什么不直接用轮廓系数最大值的 K而是“手肘 轮廓”结合看因为校园消费数据聚类时轮廓系数往往在 K2 时最高但那只是把学生分成“花得多”和“花得少”提供不了运营价值。我一般先看手肘图拐点再参考轮廓系数最后人工看每个簇的画像是否可解释。这里n_init10是让 K-means 用 10 组不同初始质心取最优random_state42保证结果可复现。3.3 时间维度特征早餐率、夜宵率与周末波动六个维度里最容易被老师追问的是“为什么选早餐率和夜宵率”。这两个特征是从学生管理角度设计的早餐规律性反映学生的作息健康程度夜宵占比反映宿舍区消费活跃时段这在后勤管理里是真实存在的关注点。你在答辩时能说出“这两个特征可以用来分析不同学生群体的作息规律进而指导食堂的备餐时段”比单纯说“我挑了六个特征”有说服力得多。聚类完后用groupby看每个簇的特征均值给簇起名字。以我常用的经验最终会得到三类可解释的结果第一类早餐率高、单笔金额稳定、以食堂为主命名“规律堂食型”第二类夜间消费占比高、金额波动大、超市占比高命名“夜间囤货型”第三类日均消费笔数少、月均消费额低、偶尔出现大额消费命名“低频偶发型”。如果发现某类早餐率极低、总消费却很高那就是“外卖替代型”属于值得运营方重点关注的人群。user[cluster] km.labels_ cluster_profile user.groupby(cluster)[feat_cols].mean().round(2) print(cluster_profile)这里的km.labels_是对全部样本的聚类结果groupby后打印的每个簇的均值表就是答辩 PPT 里最重要的一张表。命名时不要用“类0、类1”而是先看每个簇在六个特征上的相对高低再用“规律堂食型”这种能说明行为的名字。注意聚类得到的簇并不天然代表某个学生“好”或“坏”描述时用“作息规律性强”“夜间消费占比高”这类中性表达避免用“懒散型”这种标签被老师追问时容易站不住。4. 可视化与交付用Streamlit把聚类结果做成答辩系统4.1 选型对比Streamlit vs FlaskECharts到这一章你手里已经有一份分析结果。这时候的关键决策是做一个能现场演示的 Web 系统还是只交一份 Notebook如果你的毕设题目是软件工程方向的需要一个“系统”做交付物如果是数据分析方向的做一个轻量展示页也能极大拉高答辩印象分。在这两个方向里我推荐 Streamlit它上手成本低、演示不容易翻车。对比一下两个方案的适用场景Streamlit 用 Python 直接写界面图表用 Plotly 或自带组件一个文件就能跑起完整系统适合三天出活、重点是“分析过程完整”的场景Flask ECharts 前端后端分离更贴近传统软件工程毕设的结构但需要写 HTML、JavaScript、Ajax 接口调试成本明显更高。如果导师要求“软件工程毕业设计必须有完整前后端”选 Flask否则优先 Streamlit。这条路线省下来的时间足够你把聚类验证和文档写得更扎实。4.2 核心页面实现上传、聚类、雷达图Streamlit 的典型页面结构侧边栏放参数设置主区域放数据预览、簇画像表和雷达图。雷达图用来对比不同类型的用户在六个特征上的差异比柱状图直观得多。import streamlit as st import pandas as pd import plotly.express as px from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans st.set_page_config(page_title校园消费行为分析, layoutwide) st.title(学生校园消费行为分析系统) uploaded st.file_uploader(上传一卡通流水 CSV, type[csv]) k st.sidebar.slider(聚类数 K, min_value2, max_value8, value3) if uploaded is not None: df pd.read_csv(uploaded) st.dataframe(df.head(100)) # 这里复用第三章的特征工程逻辑封装成函数更合适 user df.groupby(card_id).agg( total_amount(amount, sum), trade_count(amount, count), ) scaler StandardScaler() X_scaled scaler.fit_transform(user) km KMeans(n_clustersk, random_state42, n_init10) user[cluster] km.fit_predict(X_scaled) # 用特征均值表画雷达图前的归一化 profile user.groupby(cluster).mean().reset_index() fig px.line_polar( profile, rtotal_amount, thetatrade_count, line_closeTrue ) st.plotly_chart(fig)这段代码里有三个地方要特别注意。st.sidebar.slider把 K 值暴露成交互参数答辩时可以现场从 3 调到 5展示聚类结果随 K 变化的敏感性这是个加分操作。雷达图的line_polar需要特征值量纲一致所以实际使用时我会对profile再做一个 Min-Max 归一化否则total_amount会把breakfast_rate压成一条直线我上面为了示例清晰省略了这步你自己复现时不要省。第三个注意点是把特征工程逻辑抽出成函数不要在页面回调里重复写否则上传两次文件后状态容易乱。4.3 答辩演示顺序怎么安排不容易翻车系统做完不等于答辩稳了。我见过太多人演示时先点上传、再加载数据卡在等待画面十秒钟现场气氛立刻尴尬。演示顺序应该是先开系统主页、再上传数据、等聚类完成后先讲特征表和轮廓系数图再切到雷达图和簇画像页最后用时段热力图收尾。把最耗时的计算提前在答辩前运行好如果答辩机器上没有环境录一段三分钟的演示视频作为备手这比现场装包靠谱得多。这个阶段还要做一件容易被忽略的事把聚类到每个学生的结果导出成 CSV。答辩时老师可能随机问“那个 2023018 号同学属于哪个簇”如果你只能现场重新跑一遍等于把翻车机会交给随机数。提前导出预测结果老师问起时直接查表回答这是所有演示中最保险的一招。5. 常见问题与避坑聚类翻车、数据太假与GitHub抄来的系统5.1 现象聚类结果被“消费总额”一个特征主导分出来的三类只是“花得多、花得少、中间”这个现象几乎是每个没做标准化的同学都会遇到的翻车现场。用了原始特征直接喂给 K-means 后六个维度里total_amount的数量级是几千breakfast_rate是零点几欧氏距离计算时其他五个维度形同虚设。原因就是量纲差异K-means 基于欧氏距离大数值特征会主导距离计算。解决方法是先做标准化再聚类。还有个进阶确认方法聚类后输出每个特征对簇间差异的贡献我一般用cluster_profile的各列极差除以该列标准差来判断如果只有total_amount差异显著说明标准化或特征构造出了问题。这不是算法玄学而是 K-means 的基本性质决定的标准化的选择直接影响结论答辩时主动说出这一点反而是加分项。5.2 现象模拟数据过于完美答辩被质疑“真实校园数据哪有这么均匀”用正态分布生成的金额非常光滑每个食堂的消费笔数比例也稳定答辩老师看一眼直方图就问“你这数据哪来的”。这是模拟数据做毕设最尴尬的时刻。原因是你只按分布抽了随机数没有注入真实系统里的噪声和缺失。解决的方法是加四类“脏数据”第一前 30 天有部分学生没开卡模拟激活日期第二每周五晚到周六上午食堂消费下降超市上升模拟学生外出和囤货第三随机加入 0.1% 的卡号错误和金额为负的退款记录第四让少数学生对某个窗口有极高复购率人为制造离群点。这些规则写进生成代码后在答辩文档里说明“数据生成时考虑了激活期、周末效应和异常注入”可信度会高一个档次。5.3 现象用线性回归预测“明天食堂收入”周末的预测值严重偏高很多同学会把预测塞进这个题目的加分项最常选的就是按日期预测总消费额把手里的 120 天数据切出训练集和测试集sklearn 一套线性回归跑出来平均误差不小但能看。结果一到周末预测值直接高出真实值两倍。原因是校园消费有强烈的每周周期性周一到周四人流稳定周五下午开始骤降周六只有食堂二楼的清真窗口和小卖部营业线性回归这种单一时间趋势模型捕捉不到这个周期。解决方法是至少要加“星期几”的多个虚拟变量或者用 Facebook Prophet 这类带节假日的模型更简单地把特征里加入is_weekend、day_of_week等列。省事的做法是干脆不做日粒度预测改成“工作日/周末”两类分别做均值估计解释起来比一个误差很大的回归模型更有逻辑。5.4 现象关联规则跑出一堆“泡面—火腿肠”式的废话规则时间序列做完有人还想做关联规则提升技术含量用 mlxtend 跑购物篮分析。由于一卡通流水只到商户粒度你拿到的“一食堂”“二食堂”之间根本没有有意义的关联跑出来全是“在一食堂消费的同学大概率去超市买饮料”这类逻辑上就成立、运营上没用的话。原因不是代码错了而是数据粒度和算法不匹配。关联规则要求事务里有商品明细校园一卡通消费流水只有地点和金额。解决方式有三种一是干脆不做关联规则把精力放在聚类解释上二是自己模拟一个“校园超市购物明细”数据集商品粒度做规则才有意义三是在论文里写明“流水系统未记录商品信息因此本设计不做购物篮分析仅做消费时段关联”这句话体现的是你的思考深度比硬凑结果更赚印象分。5.5 现象从GitHub抄来的系统答辩提问环节一问三不知网上搜“基于 Python 的学生消费分析系统”能找到一个结果很棒、长得很好看的 GitHub 项目下载回来改个名字就交。准备答辩时才发现打开项目看不懂settings.py里的配置不知道那个漂亮的仪表盘数据是从 MySQL 还是 SQLite 读的更说不清其中某个聚类算法的参数为什么那样设。这一类现象在每年的毕业设计中从不缺席也是最容易被答辩组一眼看穿的。原因很简单项目的实现方式、选型理由、踩坑过程一个月速成根本补不回来。解决的话我的建议是从 GitHub 上学习的应该是“这个功能用什么组件实现”“这个图表用哪个库画”的知识点然后自己一个问题一个问题复现。哪怕你的界面没他好看但每行代码都能解释的成果在答辩老师眼里远比一个精美黑匣子可信。你哪怕只做了数据清洗和聚类两个模块只要讲得透就足以拿到一个踏实的成绩。6. 可验证的聚类交付一份验证脚本与两个进阶方向6.1 最快验证聚类效果的脚本轮廓系数降维散点聚完类别急着下结论写一个验证脚本把“这个聚类到底靠不靠谱”量化出来。除了前面手肘图中的silhouette_score我习惯再输出每个簇的样本数和一个二维降维散点图用来确认没有出现一个簇只有一个极端离群点的情况。from sklearn.manifold import TSNE import matplotlib.pyplot as plt best_k 3 km KMeans(n_clustersbest_k, random_state42, n_init10) user[cluster] km.fit_predict(X_scaled) sil silhouette_score(X_scaled, user[cluster]) print(f轮廓系数: {sil:.3f}) # t-SNE 降到二维按簇着色 tsne TSNE(n_components2, random_state42, perplexity30) reduced tsne.fit_transform(X_scaled) plt.scatter(reduced[:, 0], reduced[:, 1], cuser[cluster], cmapviridis, s10) plt.show()轮廓系数取值在 -1 到 1 之间0.3~0.5 对校园消费数据意味着“簇内紧密、簇间有区分”已经够用低于 0.2 就要回头检查特征和 K 值。tsne 的perplexity30是二维投影的经验默认值样本量只有几百时调成 15 会更稳定。这两个数字写在论文实验章节里老师会觉得你确实做了模型验证。我每次做完聚类都会跑这个脚本超过 0.4 才敢把聚类结果写进文档这是这些年养成的习惯。6.2 进阶方向异常消费预警与消费时段画像聚类只是第一步。如果想让这个毕设有延伸价值往“异常消费预警”方向走是性价比最高的。用IsolationForest对每个学生的周消费笔数和金额做无监督异常检测识别出消费骤降或骤增的账户这在真实校园场景里能关联到补助发放、困难学生识别等应用方向纯技术角度也立得住。我做这类题目的习惯始终是一条系统可以简单但验证不能省。先把聚类结果验证脚本跑通再考虑更多算法否则再好看的界面也只是空中楼阁。最后希望这个方向的思路帮你少走几个弯路踏踏实实做完答辩顺顺利利希望帮到你。本文还有配套的精品资源点击获取