简介:基于Python的学生校园消费行为分析项目,围绕校园一卡通消费记录展开,通过数据清洗、特征工程与可视化等手段,从食堂、时段、性别等维度刻画学生消费规律,可服务于食堂运营优化及贫困生精准援助等场景。资源面向数据分析与Python学习者,覆盖完整源码、实验数据与分析结果集。包内共22个文件,包含7个Jupyter Notebook(承担任务拆解与建模过程)、3个Python脚本、8张结果展示图(就餐峰值、食堂各时段占比、性别消费对比等),另附分析报告与说明文档,压缩包整体约19.08MB,目录结构清晰。目前已有143人学习下载。对相关领域初学者或竞赛选手而言,资源打通了从一卡通原始数据到分析结论的完整闭环,Notebook分步呈现数据处理与建模思路,图表与报告可直接复用,便于快速掌握学生消费行为分析的常见方法,也可在此基础上扩展个性化研究。
1. 基于Python的学生校园消费行为分析:源码、数据与结果集到底能做出什么
每年毕业季和课程设计季,都会有一大批人搜“学生校园消费行为分析Python源码”,这个词的热度常年不减。但真拿到源码包、数据集和结果集之后,最常见的翻车点其实是——代码能跑,但不知道每一步在干什么,也不知道结果怎么解释。学生校园消费行为分析这个方向的核心价值在于:把一卡通流水这种高维、稀疏、带时间戳的原始数据,转化成可解释的用户画像、消费规律和异常行为信号。它适合课程设计、毕业设计,也适合做数据分析和机器学习方向的练手项目。
这一篇我不会假装有某个现成源码包在手,只从一线工程视角,把这类项目最常见的实现路径、参数设置、结果集结构和踩坑点逐一拆开。你用自己学校的一卡通数据、爬虫抓的公开数据或模拟数据都能照做,关键是理解口径和边界。先说明白一件事:这类项目难的不是聚类或可视化,而是“消费行为”这个词到底怎么定义成特征。
2. 从流水到特征:学生消费行为分析的数据口径与预处理方案
2.1 一卡通消费原始数据长什么样:字段、粒度与脏数据
学生校园消费行为分析的第一步从来不是写聚类代码,而是先把流水数据摸清楚。绝大多数高校的一卡通流水会包含这几类核心字段:学号(或卡号)、交易时间、消费金额、商户名称、消费类型(餐费、超市、洗浴、饮水、门禁扣费等)、余额。字段命名各校不同,但信息含量基本一致。我见过最差的数据集只有四个字段:卡号、时间、金额、地点名称,连学号都没有,这种情况下做个人画像会非常吃力。
拿到数据先做三件事:看时间跨度、看记录条数、看字段缺失比例。时间跨度至少覆盖一个完整学期才有意义,因为消费行为天然有周周期和月度周期。记录条数决定你能不能用个体级别的分析方法——如果只有几千条流水,那只能做群体分析,做不了个体聚类。缺失比例高的时候,首先确认是采集问题还是导出问题,不要直接填充。
原始数据里最常见的脏数据有:测试卡(金额为0或负数)、退款流水(负金额但商户名带“退”字)、跨校区流水(同一张卡多条不同校区记录)、凌晨异常消费、余额字段与消费金额不匹配。这些脏数据直接用阈值过滤经常会误伤,更稳妥的做法是先按卡号和交易时间排序,人工抽看100条流水,把业务上的异常模式记下来,再写过滤规则。
2.2 用Python做消费流水的清洗与聚合:最小代码实现
下面是一段可以直接跑通的清洗代码,适用于大多数“卡号、时间、金额、商户”四字段格式,其他字段自行映射即可。
import pandas as pd import numpy as np from datetime import datetime # 加载原始流水,注意encoding按实际情况调整 df = pd.read_csv('consume_flow.csv', encoding='utf-8-sig') df.columns = ['card_no', 'trade_time', 'amount', 'merchant'] # 时间字段转标准格式 df['trade_time'] = pd.to_datetime(df['trade_time']) # 过滤明显异常:金额为0、负金额且非退款标记、单笔超过200的测试交易 # 阈值设置依据:普通校园消费单笔极少超过100,超过200基本是充值或测试 df = df[df['amount'] > 0] df = df[df['amount'] < 200] # 剔除夜间测试消费:凌晨2点到5点基本无真实消费场景 df = df[~df['trade_time'].dt.hour.isin([2,3,4])] # 按卡号和商户名去除完全重复的流水(同一秒同一商户同金额视为重复) df = df.drop_duplicates(subset=['card_no', 'trade_time', 'merchant', 'amount']) # 衍生基础字段:日期、星期、小时、是否工作日 df['date'] = df['trade_time'].dt.date df['weekday'] = df['trade_time'].dt.weekday df['hour'] = df['trade_time'].dt.hour df['is_weekend'] = df['weekday'].isin([5,6]).astype(int) print(f"清洗后剩余记录数: {len(df)}") print(df[['card_no', 'trade_time', 'amount', 'merchant']].head())这段代码的逻辑是:先做格式统一,再做业务规则过滤,最后做衍生字段。注意几个参数的设置逻辑:200元的单笔阈值不是拍脑袋,而是先看金额分布的95分位数再定;凌晨2点到5点的剔除规则也要先统计各小时交易量再确认——有些学校有24小时便利店,这部分需要保留,建议先用df.groupby('hour').size()看一眼分布再决定几点到几点可以安全剔除。
清洗之后的数据集才是分析的基础。如果直接拿原始流水去做聚类或可视化,结果集里会出现大量“余额充值”“食堂补卡”这类非消费记录,导致聚类中心被拉偏。
2.3 打标签的隐蔽细节:消费行为≠消费金额
这里有个关键认知:消费行为分析的重心不是“花了多少钱”,而是“怎么花钱”。两个学生月消费额完全相同,但一个是每天规律三顿饭、周末宅宿舍,另一个是周一到周五几乎不消费、周末集中大额购物——这两类人的消费行为画像完全不同。所以在做特征工程时,要围绕消费频次、消费时段分布、消费地点多样性、消费金额稳定性这四个维度展开,而不是只盯着月总消费额。
我见过很多源码包只做“月均消费”“日均消费”“总消费”三个特征就上KMeans,做出来的聚类结果基本就是按穷富分层,完全体现不出“行为”两个字。正确做法是至少构造以下特征组:消费频次(日均笔数、每周活跃天数)、时段偏好(早餐时段占比、午餐时段占比、晚餐时段占比、夜宵时段占比)、地点结构(常去商户数、单一商户依赖度、食堂消费占比)、金额画像(金额均值、金额标准差、金额中位数、大额消费次数)。这些特征构造完,聚类结果才有解读空间。
3. 消费画像怎么做:RFM变体与KMeans聚类参数选择
3.1 把RFM模型改造成校园消费版:R、F、M的定义差异
RFM模型是消费分析的老祖宗,但电商领域的RFM不能直接用在校园场景。电商的R是最近一次购买时间距今天数,校园场景里学期末和学期初差距很大,直接用自然日计算最近消费间隔会被假期严重干扰。我做校园项目时通常这样改造:
- R值:取数据时间范围内最后消费日期距数据截止日期的天数,但只统计在活跃周内的记录,排除假期前后连续不消费的自然中断。
- F值:不是总消费次数,而是“周均活跃消费天数”。因为有些学生一天刷10次(每顿饭刷一次),有些学生一天刷3次,用总次数会膨胀了吃饭次数而压缩行为差异。
- M值:保留总消费额和月均消费额两个版本,看业务需要。M值在校园场景下代表消费能力,但也要结合家庭经济水平做归一化——否则聚类出来的本质是家庭收入分层。
这套改造做下来你会发现,同一个RFM三元组,校园场景和电商场景的解释完全反向:电商R值小、F值高是优质用户,但校园里R值小、F值高的学生可能就是天天宅在宿舍周边消费,消费地点多样性未必高。所以特征里一定要加一个“地点多样性”维度,否则聚类结果很难写成报告。
3.2 KMeans聚类:归一化、K值选择与轮廓系数
KMeans是这个项目里最常见的聚类算法,选它不是因为效果最好,而是因为结果好解释、写报告方便。但KMeans有个致命前提:必须做标准化。消费金额的量纲是元,消费频次的量纲是次/周,消费时段的量纲是百分比,如果不标准化,KMeans的欧氏距离会被金额特征完全主导。
from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score import pandas as pd # 假设df_features是特征表,索引是card_no,列是特征名 feature_cols = ['weekly_freq', 'avg_amount', 'std_amount', 'breakfast_ratio', 'lunch_ratio', 'dinner_ratio', 'merchant_diversity', 'weekend_ratio'] X = df_features[feature_cols].values # 标准化:这一步不做,聚类结果基本是按金额一维排序 scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # 用轮廓系数选K:遍历2到8,选轮廓系数下降拐点 # 注意:轮廓系数不是越大越好,要结合业务可解释性 sil_scores = {} for k in range(2, 9): km = KMeans(n_clusters=k, n_init=10, random_state=42) labels = km.fit_predict(X_scaled) sil_scores[k] = silhouette_score(X_scaled, labels) print("不同K值的轮廓系数:", sil_scores) # 真实项目中我常用K=4,原因在后文说明 best_k = 4 km = KMeans(n_clusters=best_k, n_init=10, random_state=42) cluster_labels = km.fit_predict(X_scaled) # 把聚类结果写回特征表 df_features['cluster'] = cluster_labels df_features.to_csv('consumer_clusters.csv', encoding='utf-8-sig')KMeans里有三个参数值得单独说。n_init默认是10,表示用不同随机中心跑10次取最优,这个参数在样本量小时效果不明显,但样本量上万后建议调大到20,否则每次运行结果都可能不一样。random_state必须固定,否则你跑出来的聚类结果和源码包里给的结果集对不上,写报告时图表数字会变。max_iter遇到不收敛警告时再调,默认300够用,调大意义不大。
选K值时轮廓系数只是个参考,不要机械地选最大值。比如轮廓系数在K=2时最高,但K=2把学生分成“爱花钱”和“不爱花钱”两类,报告毫无深度。常见做法是跑完2到8后,每个K值都打印每个簇的特征均值,看哪一个K的簇间差异在业务上最好解释——我一般选4或5,因为“规律就餐型”“夜猫子型”“高消费型”“极简型”这四类已经能覆盖大多数校园场景,分太多类反而写不出差异。
3.3 聚类结果怎么验证:别只看轮廓系数
轮廓系数只能说明簇的分离度,不能说明簇的业务含义。验证聚类结果是否靠谱,标准做法是回看每个簇的特征均值表,逐项检查是否符合直觉。比如“高消费型”簇的avg_amount应该显著高于整体均值,merchant_diversity也应该高;“规律就餐型”簇的breakfast_ratio和lunch_ratio应该显著高于其他簇。如果某个簇的特征均值和另一个簇几乎相同,那就是K选大了,或者特征里有冗余项。
另一个验证思路是用时间序列做外部校验。把聚类标签贴回原始流水,按周统计每个簇的消费总额趋势,看是否呈现有规律的周期波动。如果某个簇的周消费趋势是一条直线或者白噪音,说明这个簇可能没分好。这个验证过程在写报告时非常好用,因为它能给出“簇间差异显著”的量化证据。
4. 消费时段与行为模式识别:从时间特征到可解释标签
4.1 早中晚时段划分:一个影响全局的参数
消费时段划分是整个项目里最容易被低估的参数。很多源码直接用hour < 10当早餐、10 <= hour < 15当午餐、15 <= hour < 21当晚餐,这种粗暴划分在真实校园数据里会产生严重偏差。比如某高校上午第一大节11:30下课,食堂高峰在11:30到12:30,早餐截止在9:30之后基本没人;又比如部分高校有夜宵档口营业到22:30,如果晚餐截止到21点,夜宵消费会被算成明天的早餐。
正确做法是先用时间段聚合出的“每小时消费笔数”曲线,找到真实波峰。下面这段代码就是干这个的:
# 统计全天24小时的消费笔数分布,用于确定时段切分点 hourly_counts = df.groupby('hour').size() hourly_pct = hourly_counts / hourly_counts.sum() * 100 print(hourly_pct) # 根据波峰位置手动设置时段划分 # 示例:峰值在8-9点为早餐,11-13点为午餐,17-19点为晚餐 def assign_meal_period(hour): if 6 <= hour <= 9: return 'breakfast' elif 11 <= hour <= 13: return 'lunch' elif 17 <= hour <= 19: return 'dinner' elif 20 <= hour <= 23: return 'night_snack' else: return 'other' df['meal_period'] = df['hour'].apply(assign_meal_period) # 计算每个学生各时段消费笔数占比作为特征 meal_pivot = df.groupby(['card_no', 'meal_period']).size().unstack(fill_value=0) meal_ratio = meal_pivot.div(meal_pivot.sum(axis=1), axis=0) meal_ratio.columns = [f'{col}_ratio' for col in meal_ratio.columns]这段代码的关键在于时段边界值是自己根据数据分布定的,不是抄通用公式。不同学校的作息差异很大,有的学校上午第一大节8点开始,早餐高峰在7:30左右;有的学校上午没课,早餐高峰能拖到10点。做这一步时不要嫌麻烦,把每个小时的消费笔数打印出来看一眼,再定边界。这个参数直接影响后面所有时段相关特征和聚类结果。
4.2 消费地点多样性:被多数源码忽略但最能区分行为模式的特征
消费地点多样性这个特征在很多课程设计源码里根本不存在,但它恰恰是区分“规律食堂党”和“校园漫游型消费”的核心维度。构造方式很简单:每个学生出现过的商户名称去重计数,再和周均消费笔数做比值,得到“平均每笔消费涉及多少不同商户”的指标。
这个特征在聚类里能有效把“两点一线型”和“高流动型”学生分开,但要注意一个坑:商户名称在不同校区可能重名,比如两个校区都有“一食堂”,如果数据没带校区ID,地名多样性会被低估。如果原始流水有校区字段,最好先把商户名拼上校区前缀再算多样性。有的数据集里商户名还会出现“临时窗口1”“临时窗口2”这类不可复用的名称,这种要做归一化映射,把临时窗口全部映射成“临时档口”一个类别,否则同一个档口换名字会被算成两个地点。
4.3 周末消费占比与作息规律指数:行为标签的扩展维度
周末消费占比这个特征非常便宜但很有解释力。它的定义是周末消费笔数除以总消费笔数,能反映学生是“周末宅校型”还是“周末离校型”。配合夜间消费占比(晚22点后的消费笔数占比),可以进一步识别“夜猫子型”消费行为。这两个特征加进聚类后,簇的边界会更清晰。
作息规律指数更简单——直接统计每个学生一周7天里“有消费记录的天数”,但这个指标在假期周会失真。我的处理方式是只取学期内完整周的数据,去掉放假前后两周,再计算平均每周活跃天数。这样做出来的规律指数更接近真实生活作息。这些特征结合起来,才算把“消费行为”做成了能解释的标签体系,而不是一堆堆在表格里没人看的数字。
5. 避坑指南:学生消费行为分析的常见翻车点与排查方法
5.1 流水数据里的“假消费”记录:现象、原因与解决
现象:聚类结果里出现一个簇,消费笔数极高但金额极低,日均消费10次以上、单笔均额不到1元。
原因:这类数据多半是饮水机的流水。很多学校的饮水机每次刷卡0.1到0.5元,一天接10次水很正常。饮水消费混在餐费里面做聚类,会把“接水频繁”错当成“消费行为活跃”,完全扭曲画像。
解决:找到饮水机对应的商户名关键词(“饮水”“开水”“直饮水”),单独把这类流水剔除或单独建特征。如果源码包里没做这一步,你可以自己加过滤规则:df = df[~df['merchant'].str.contains('饮水|开水|直饮')]。这个字段一定要问清楚,否则聚类结果直接翻车。
5.2 跨学期数据直接拼接:现象、原因与解决
现象:按月份画的消费趋势图在9月和3月出现断崖式下跌。
原因:数据跨越了两个学期,中间有寒假或暑假。假期学生基本不消费,直接把整个自然年的流水拼在一起,假期那几个月都是零消费或极少消费,趋势图没法看,聚类特征里的“周均活跃天数”也会被假期拉低。
解决:做两类处理——如果做个体画像,只取学期内数据;如果做趋势分析,把假期月份单独标注出来,不要把假期当成普通停工周。具体做法是先用df['month'] = df['trade_time'].dt.month标记月份,然后df = df[~df['month'].isin([1,2,7,8])]剔除寒假暑假月份。非要保留假期数据的话,把“是否假期”作为单独特征塞进模型,而不是混在时间序列里直接画图。
5.3 KMeans聚类结果每次跑都不一样:现象、原因与解决
现象:同一份数据、同一个K值,连续跑两次聚类,结果集里的簇编号对不上,甚至簇中心都不同。
原因:KMeans用随机初始化中心点,random_state没固定或者n_init太小,聚类结果会出现波动。这在课程设计答辩时很尴尬——导师让你现场重跑一遍,结果图跟报告里对不上。
解决:设置random_state=42固定随机种子,n_init调大到20。如果换了机器跑还是对不上,检查sklearn版本——旧版本和新版本的KMeans在初始化策略上有微调,结果会有细微差异。把跑出结果集的Python版本和sklearn版本写进实验环境说明里,这招在写报告时很好用。
5.4 特征里混入充值记录:现象、原因与解决
现象:聚类结果里出现一个簇,总额极高、笔数极少、金额方差极大。
原因:一卡通系统里“充值”也是交易流水,金额通常是100、200、500这类整数。如果清洗代码里没有排除充值类型,充值记录会被当成消费行为,一个人充500块会被误判为“大额消费型”。
解决:在清洗阶段看商户名或交易类型字段,把包含“充值”“圈存”“转账”的记录直接剔除。如果数据没有交易类型字段,就用金额特征做辅助判断——正常校园消费几乎不会出现大量整百金额,按金额尾数过滤也能去掉大部分充值记录。但这个方法不完美,因为食堂套餐可能有整10元定价,优先级最高的还是找到交易类型字段。
5.5 商户名编码混乱导致地点特征失效:现象、原因与解决
现象:算出来的merchant_diversity异常偏高,平均每个学生去过几十个商户,明显不符合直觉。
原因:同一家商户在不同时期有不同的系统编码,比如“一食堂一楼”“一食堂-1F”“第一食堂一层”三个名字其实是同一个地方,但没有归一化,被当成三个不同地点。
解决:用字符串模糊匹配或人工映射表做商户名归一化。常见做法是提取关键词(“一食堂”“清真”“超市”“便利店”)做类别映射,而不是直接用原始商户名。这个预处理步骤做得好不好,直接决定地点相关特征是否可信。
6. 结果集怎么用:消费行为分析报告的输出与验证
6.1 结果集的标准构成:聚类表、特征统计、可视化图
一个完整的结果集不只是一张聚类标签表,通常包含四类产物:带聚类标签的学生特征表(CSV格式,每一行是一个学生,列是特征值和标签);每个簇的特征均值对比表(用于报告分析和簇解释);可视化图(特征分布图、聚类散点图、消费时段热力图);核心分析结论文本(每个簇的行为描述和占比)。源码包里如果只有聚类代码没有结果说明,报告写作时会很痛苦。
以聚类表为例,每行记录格式建议包含:card_no、cluster、weekly_freq、avg_amount、merchant_diversity、weekend_ratio等。这张表是后续所有分析的主表,不要只保留聚类标签而丢了原始特征。簇特征均值表则是用来解释簇含义的——比如报告里写“第0簇学生日均消费2次,主要集中在食堂,周末消费占比仅15%,属于规律就餐型”,这些数字全部来自均值对比表。
6.2 用Excel报告模板输出:参数与格式
结果集交付时最常用的是Excel格式,因为写报告和答辩展示都方便。用pandas.ExcelWriter可以把多个DataFrame写进同一个工作簿的不同sheet,比输出多个CSV文件更整洁。
with pd.ExcelWriter('消费行为分析结果.xlsx', engine='openpyxl') as writer: df_features.to_excel(writer, sheet_name='学生消费特征表', index=False) cluster_summary.to_excel(writer, sheet_name='簇特征均值对比', index=False) # 每个簇的样本量占比 cluster_size = df_features['cluster'].value_counts() cluster_pct = cluster_size / cluster_size.sum() * 100 cluster_pct.to_frame(name='占比(%)').to_excel(writer, sheet_name='簇分布')这个写法的好处是结构清晰:一个Excel文件包含个体数据、聚合数据和分布统计,导师或甲方打开一个文件就能看到全部结果。注意openpyxl引擎要预先安装,pip install openpyxl,否则写入Excel会报错。
6.3 行为标签落库与后续扩展:从聚类结果到持续监控
如果这个项目不止用于课程设计,而是要做成可运行的系统,聚类结果不应该只停留在CSV和Excel里。常见做法是建一张MySQL表,字段包括学号、所属簇、聚类日期、特征快照,每周跑一次清洗和聚类脚本,对比簇成员的变化。有学生的簇归属发生变化,说明他的消费行为模式发生了转移——比如从“规律就餐型”变成了“外卖型”,这可能是作息变化或生活状态变化的信号。这个扩展方向是校园消费行为分析真正有应用价值的地方,但需要先跑通基础版本的清洗、聚类和结果输出,否则后面全是空中楼阁。
6.4 一个验证习惯:每次跑完聚类先抽10个学生回看原始流水
这是我做这类项目最习惯的动作——聚类跑完不要急着生成报告,随机抽每个簇5到10个学生,回到原始流水一条一条看他们的消费记录。如果“高消费型”簇里抽出来的学生确实每周都在校外餐厅和超市大额消费,说明聚类可信;“规律就餐型”簇里如果抽出来的学生有一半根本不在食堂吃饭,那特征构造或者清洗环节一定有问题,回去查。
这个习惯救过我很多次,因为特征工程和聚类都是黑匣子,只有回到最原始的流水才能确认中间环节没把数据搞坏。写报告时也可以把抽检结果截图放进去,作为“结果可信度验证”的一部分,比单贴一个轮廓系数有说服力得多。
希望这篇基于Python的学生校园消费行为分析落地笔记能帮到你,至少让你少走我当年走过的弯路。
本文还有配套的精品资源,点击获取