ARTICLE DETAIL

资讯详情

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

基于Python的学生校园消费行为分析:从数据清洗到RFM聚类建模

基于Python的学生校园消费行为分析:从数据清洗到RFM聚类建模 简介这份资源是面向高校学生的Python数据分析期末大作业完整项目包围绕校园消费行为建模展开适合正在准备课程设计、需要高分通过考核的本科生使用。压缩包共6个文件以5个Python源码脚本为主另含1个数据压缩包整体约4.89MB脚本按任务模块拆分分别对应数据读取清洗、消费特征统计、行为聚类与可视化建模等环节结构清晰便于逐项理解与复现。项目已包含可直接运行的数据与代码无需额外修改即可跑通完整分析流程读者可据此掌握从原始消费记录到分组画像、消费规律挖掘的建模思路并参考其任务划分方式整理自己的报告框架。目前已有618人学习下载适合作为课程设计参考或数据分析入门练手素材。1. 从一份校园消费流水说起学生消费行为分析到底在分析什么每年期末总有一批同学被基于Python的学生校园消费行为分析这类大作业卡住。题目看着简单——不就是拿一卡通流水算算谁花得多、谁花得少吗真上手才发现原始数据里全是脏东西时间戳格式不统一、金额有负数退款、窗口名称五花八门、还有大量只刷了一两次的僵尸卡。更麻烦的是做完清洗之后不知道该分析什么最后交上去一堆柱状图被老师一句没有建模深度打回来。这篇笔记就围绕这个标题把从原始流水到可解释模型的全流程拆开讲。核心目标有三个第一把消费数据清洗成能建模的结构化数据第二用RFM和聚类把学生分成可解释的群体第三给出一个能写进论文的建模框架而不是只停留在描述统计。适合正在做期末大作业的本科生、刚接触Python数据分析的入门者以及需要快速搭出一套可复现分析流程的人。全程用pandas、scikit-learn、matplotlib这套最常见的组合不依赖任何冷门库vscode python环境配置好就能跑。2. 数据从哪来、长什么样校园一卡通流水的字段拆解与清洗2.1 一卡通流水的典型字段与脏数据分布真实的一卡通消费流水字段通常比想象中少但脏得比想象中狠。常见字段包括卡号学号脱敏后的编号、交易时间精确到秒、交易金额有正有负、交易类型消费/充值/退款、终端编号对应食堂窗口或超市POS机、余额。有些学校还会带商户名称和楼栋号。脏数据主要集中在四个地方。一是时间字段有的系统导出是2024/3/5 12:03:45有的是2024-03-05 12:03还有的是Excel序列号比如45356.5。二是金额退款记录是负数充值记录是正数但金额偏大如果不区分交易类型直接求和会把充值当成消费。三是终端编号同一个食堂窗口可能因为换POS机导致编号变化需要做映射归并。四是重复记录系统补单或网络重试会产生完全相同的行。我一般拿到数据先做三件事看行数和列数、看每列的空值率和唯一值数量、看金额的分布尤其是负值和极端大值。这三步能在五分钟内判断出这份数据需要多重的清洗。import pandas as pd import numpy as np # 读取原始流水注意编码校园系统导出常见gbk df pd.read_csv(card_transactions.csv, encodinggbk) # 基础体检行数、列名、空值、金额分布 print(shape:, df.shape) print(columns:, df.columns.tolist()) print(null rate:\n, df.isnull().mean()) print(amount describe:\n, df[交易金额].describe()) # 看交易类型的分布区分消费和充值 print(df[交易类型].value_counts())这段代码的逻辑是先建立对数据的整体认知再决定清洗策略。encodinggbk是因为很多校园系统导出默认用GBK用UTF-8会直接报错。isnull().mean()比isnull().sum()更直观能直接看到每列缺失比例。describe()重点看min和max如果min是很大的负数说明退款记录不少如果max是几千大概率混入了充值。2.2 清洗流水时间解析、金额过滤与重复行处理清洗的核心原则是只保留真实消费行为把充值、退款、异常大额全部隔离出去单独看。时间字段统一转成datetime金额只保留正数且在一定范围内比如0.5到200元之间超出这个范围的要么是充值要么是异常。重复行用全字段去重。# 1. 时间字段统一解析兼容多种格式 def parse_time(x): try: return pd.to_datetime(x) except: # Excel序列号转日期 return pd.to_datetime(1899-12-30) pd.to_timedelta(float(x), unitD) df[交易时间] df[交易时间].apply(parse_time) # 2. 只保留消费类型排除充值和退款 df df[df[交易类型] 消费].copy() # 3. 金额过滤剔除0.5元以下和200元以上 df df[(df[交易金额] 0.5) (df[交易金额] 200)] # 4. 全字段去重 df df.drop_duplicates() # 5. 提取时间特征 df[日期] df[交易时间].dt.date df[小时] df[交易时间].dt.hour df[星期] df[交易时间].dt.dayofweek print(清洗后行数:, len(df)) print(涉及卡号数:, df[卡号].nunique())parse_time函数用try-except兜底是因为真实数据里经常混着两种格式硬转会直接抛异常中断。金额上下限0.5和200不是拍脑袋定的0.5以下多是打印或查询扣费200以上在校园场景基本是充值或异常。drop_duplicates()不加参数表示全字段比对比只按卡号和时间去重更安全因为同一秒可能真有两次不同窗口的消费。清洗完之后一定要打印清洗前后的行数对比和卡号数量。如果清洗后卡号数骤降说明过滤条件太狠可能把正常消费也误杀了。我见过有同学把金额上限设成50结果把聚餐消费全砍了聚类出来全是低消费群体结论完全失真。3. 从流水到特征RFM建模与消费行为画像构建3.1 RFM三个维度的校园场景改造RFM是消费分析的经典框架Recency最近一次消费距今天数、Frequency消费频次、Monetary消费金额。但直接套用电商那套参数在校园场景会翻车。电商的R通常是30天、90天校园一学期也就四五个月R用7天或14天更合理。F不能只算总次数要区分食堂高频和超市低频因为一天刷三次食堂和一周去一次超市行为含义完全不同。M也不能只看总额要看单次均值否则一个经常帮全宿舍带饭的同学会被误判成高消费。我的做法是R用最近7天是否有消费二值化F用日均消费次数M用单次消费均值。这样三个维度都在同一量级附近聚类时不会因为金额数值大而主导距离计算。# 以某个参考日期为基准计算RFM ref_date df[交易时间].max() pd.Timedelta(days1) rfm df.groupby(卡号).agg( last_time(交易时间, max), freq(交易时间, count), total_amount(交易金额, sum), avg_amount(交易金额, mean), active_days(日期, nunique) ).reset_index() # R: 最近一次消费距参考日期的天数 rfm[R] (ref_date - rfm[last_time]).dt.days # F: 日均消费次数避免活跃天数不同带来的偏差 rfm[F] rfm[freq] / rfm[active_days] # M: 单次消费均值 rfm[M] rfm[avg_amount] print(rfm[[R, F, M]].describe())这里的关键是active_days这个中间变量。直接用总次数做F一个只活跃3天但每天刷10次的卡和一个活跃30天每天刷1次的卡总次数可能差不多但行为模式天差地别。用日均次数就把集中爆发型和细水长流型区分开了。ref_date取最大时间加一天是为了避免R出现0或负数。3.2 标准化与KMeans聚类把学生分成几类才合理RFM三个维度量纲不同R是0到几十天F是0到几次M是几块到几十块。直接丢进KMeans金额会主导距离。所以必须先标准化我一般用StandardScaler做Z-score标准化让每个维度均值0方差1。聚类数K怎么定不要拍脑袋选3或5。用肘部法inertia和轮廓系数silhouette_score一起看。校园消费数据通常3到5类比较合理太少区分度不够太多每类样本太少没有统计意义。from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score X rfm[[R, F, M]].values X_scaled StandardScaler().fit_transform(X) # 遍历K2到8看inertia和轮廓系数 for k in range(2, 9): km KMeans(n_clustersk, random_state42, n_init10) labels km.fit_predict(X_scaled) print(fK{k}, inertia{km.inertia_:.1f}, silhouette{silhouette_score(X_scaled, labels):.3f})random_state42保证每次跑结果一致n_init10表示跑10次不同初始化取最优避免陷入局部最优。看结果时inertia会随K增大一直下降重点看下降速度明显变缓的拐点轮廓系数越接近1越好通常在拐点附近取最大值。两个指标指向同一个K时就选它。选好K之后把聚类标签贴回原数据算每类的RFM均值给每类起个业务名字。比如R小F高M低是食堂高频型R大F低M高是低频高客单型R小F高M高是活跃消费型R大F低M低是边缘沉默型。这些名字要能直接写进论文的分析章节比类别0、类别1有说服力得多。4. 避坑与排查校园消费分析里最容易翻车的五个地方4.1 把充值当成消费总额虚高好几倍现象算出来的学生月均消费两三千明显不符合校园实际。原因原始数据里充值和消费混在一起充值金额通常是几百直接把总额拉高了。解决清洗第一步就按交易类型过滤只保留消费并且把充值记录单独存一份用于分析消费习惯比如月初集中充值说明生活费按月发放。4.2 时间字段解析失败导致整列变成NaT现象pd.to_datetime之后大量NaT后续按日期分组全空。原因数据里混了Excel序列号、中文日期、带时区的字符串等多种格式。解决用try-except逐个解析Excel序列号用pd.to_datetime(1899-12-30) pd.to_timedelta(x, unitD)转换。转换后统计NaT比例超过5%就要回头检查原始数据。4.3 聚类前没标准化金额主导了所有距离现象聚类结果每一类的R和F都差不多只有M差异明显。原因金额数值范围远大于天数和次数欧氏距离被金额主导。解决聚类前必须用StandardScaler或MinMaxScaler标准化。我一般用Z-score因为对极端值没那么敏感。标准化之后可以打印每列的均值和方差验证。4.4 用总消费额排序代替行为分析现象论文里只有消费前10名的表格没有群体特征。原因把分析做成了排名没有做分群。解决RFM聚类的价值在于把几百上千个学生压缩成4到5个可解释的群体每个群体有明确的行为标签和人数占比。排名只能看个体分群才能看结构。4.5 忽略消费时间分布错过最有价值的特征现象只分析了金额和频次没分析消费时段。原因时间字段清洗完就只用来算R没有提取小时和星期特征。解决把小时和星期做成交叉表能看出早餐型夜宵型周末宅等模式。这些特征加进聚类或者单独做时段热力图论文的分析深度立刻上一个台阶。5. 让分析更值钱时段特征、可视化与模型验证的进阶做法5.1 把消费时段做成特征聚类效果明显提升前面RFM只用了三个维度信息量有限。把消费时段加进去能区分出早八人和夜猫子这两类人的消费行为差异很大。具体做法是把一天分成几个时段早餐6-9点、午餐11-13点、晚餐17-19点、夜宵21-23点、其他。然后统计每个卡号在各时段的消费次数占比作为新特征拼到RFM后面。def time_period(h): if 6 h 9: return 早餐 elif 11 h 13: return 午餐 elif 17 h 19: return 晚餐 elif 21 h 23: return 夜宵 else: return 其他 df[时段] df[小时].apply(time_period) # 每个卡号在各时段的消费占比 period_pivot df.pivot_table( index卡号, columns时段, values交易金额, aggfunccount, fill_value0 ) period_ratio period_pivot.div(period_pivot.sum(axis1), axis0) period_ratio.columns [f时段_{c} for c in period_ratio.columns] # 拼到RFM表 rfm rfm.merge(period_ratio, on卡号, howleft).fillna(0)pivot_table用count统计各时段消费次数div按行归一化得到占比。这样每个卡号多出5个特征加起来一共8个维度。重新做标准化和聚类你会发现原来混在一起的高频低额群体现在能拆成早餐高频和夜宵高频两类业务解释性更强。5.2 可视化三张图讲清楚消费结构论文里的图不在多在于每张都能说明一个问题。我一般放三张第一张是消费金额的分布直方图说明整体消费水平第二张是各时段消费次数的柱状图说明消费时间结构第三张是聚类结果的散点图用PCA降到二维说明群体分离度。import matplotlib.pyplot as plt from sklearn.decomposition import PCA # 图1消费金额分布 plt.figure(figsize(8, 4)) plt.hist(df[交易金额], bins50, edgecolorblack) plt.xlabel(单次消费金额) plt.ylabel(频次) plt.title(校园消费金额分布) plt.savefig(amount_dist.png, dpi150, bbox_inchestight) # 图2聚类结果PCA散点图 pca PCA(n_components2) X_pca pca.fit_transform(X_scaled) plt.figure(figsize(8, 6)) plt.scatter(X_pca[:, 0], X_pca[:, 1], clabels, cmapviridis, alpha0.6) plt.xlabel(PC1) plt.ylabel(PC2) plt.title(学生消费群体聚类分布) plt.colorbar(label群体编号) plt.savefig(cluster_pca.png, dpi150, bbox_inchestight)dpi150保证论文打印清晰bbox_inchestight去掉多余白边。PCA散点图里如果各类分得开说明聚类有效如果混成一团要么K选得不对要么特征区分度不够需要回头调整。5.3 验证聚类稳定性换个随机种子再跑一遍聚类结果最怕的是换一次随机种子就变个样。验证方法很简单用不同的random_state跑5次看每次的轮廓系数和各类人数占比是否稳定。如果波动很大说明K选得不好或者特征里有噪声。验证项稳定标准不稳定时的处理轮廓系数5次波动小于0.05重新选K或删掉低区分度特征各类人数占比波动小于5%检查是否有极端值未处理类中心RFM均值排序一致增加n_init次数或换初始化方式我自己的习惯是跑完聚类先不急着写结论把random_state从42换到0、1、7各跑一遍三次结果一致才敢往论文里写。有一次帮同学看代码发现他K4时两类人数分别是2和3这种样本量做不了任何统计推断纯粹是噪声被硬分出来了。后来把K降到3每类都有几十人结论才站得住。做这类期末大作业最值钱的不是代码多复杂而是每一步都有验证、每个结论都有数据支撑。我踩过最大的坑就是急着出图清洗没做干净就聚类结果被老师问你这负消费是怎么回事当场卡住。后来养成习惯每清洗一步就打印一次统计摘要确认无误再往下走。希望帮到你。本文还有配套的精品资源点击获取
返回列表