尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python校园卡消费行为分析:从数据清洗到KMeans用户分群实战

Python校园卡消费行为分析:从数据清洗到KMeans用户分群实战 简介一套基于Python的学生校园消费行为分析项目内含完整源码、配套数据集与项目报告主要面向计算机相关专业正在完成课程设计或期末大作业的学生也适合需要数据分析实战练习的进阶学习者。项目本身为成熟大作业方案曾取得98分高分结构完整、可直接运行便于在此基础上进行功能扩展或报告改写。资源包为zip格式约10.16MB核心内容包括Python数据分析脚本、校园消费记录数据集以及详细介绍设计思路与实现步骤的文档可支撑从数据读取、清洗转换到消费特征分析与可视化展示的完整流程。压缩包内部目录清晰按源码、数据、报告等模块划分使用者可以快速定位所需内容。目前已有118人学习对于需要一套可落地参考项目、又想节省时间完成高质量大作业的同学而言具备较高的参考价值。1. 别把校园消费分析做成图表展示关键是行为分群假设你拿到一份校园卡流水数据集几十万行交易、几千个学生、几十个商户。如果只画“食堂营业额趋势”“超市品类占比”那多半停留在描述性统计离分析还差一步。真正能拿高分的 python 学生校园消费行为分析项目源码一定会把“消费行为”落成一个可解释的用户分群问题谁在月初透支、谁长期夜宵、谁只去食堂不去超市这些才是有业务含义的产出。这个方向也适合作为课程设计和期末大作业数据量适中、特征清晰、模型不需要太重Pandas做清洗、KMeans做分群、可视化做论证就能构成一条完整链路。下文按一套成熟项目的代码顺序拆解覆盖数据集字段处理、特征工程、聚类参数和报告写法新手可以直接跟着步骤复现熟练工可以重点看坑点和分析深度。2. 先处理校园卡流水数据集字段与清洗决定了后面能不能用2.1 原始流水表先认识字段再谈分析不同学校导出的校园卡字段名有差异但核心结构基本一致。下表列出这份 python 数据集里最常用到的字段字段名示例值分析中的用途student_id2021010101用户粒度所有行为特征都按它聚合trade_time2024-05-10 12:03:11餐段、周内日、月末效应的基础merchant第一食堂商户偏好category餐饮/超市/洗浴/打印消费结构做分群的强特征amount12.50金额注意正负号与是否真实扣款balance88.30可用余额可以反映充值习惯这个项目能拿98分很关键的一点是数据清洗没有糊弄。常见的错误是一拿到csv就groupby(student_id).sum()结果把退款、挂账和异常数据全算进去聚类结果自然没有逻辑。我更倾向于先写一个clean_card_data函数把所有规则固化答辩时可以直接说“数据清洗占整个项目30%的工作量”。2.2 Pandas清洗函数空值、重复值、金额正负一起处理下面这段代码基本可以套到大多数校园消费分析项目源码里import pandas as pd def clean_card_data(df: pd.DataFrame) - pd.DataFrame: # 统一列名便于后续代码复用 df df.rename(columns{ 学号: student_id, 交易时间: trade_time, 商户: merchant, 消费金额: amount, 余额: balance }) # 1. 时间解析errorscoerce 避免脏数据直接中断 df[trade_time] pd.to_datetime(df[trade_time], errorscoerce) # 2. 删除关键字段为空的行 df df.dropna(subset[student_id, trade_time, amount]) # 3. 金额为0的流水无分析意义直接去掉 df df[df[amount] ! 0] # 4. 重复流水同一学生同一商户同一时间同一金额视为重复 df df.drop_duplicates( subset[student_id, trade_time, merchant, amount], keepfirst ) # 5. 只保留有效业务时段比如6点到23点 hour df[trade_time].dt.hour df df[(hour 6) (hour 23)] return df.sort_values([student_id, trade_time]).reset_index(dropTrue)逻辑说明1-4步解决数据质量问题第5步过滤掉深夜的无效交易避免“凌晨3点打水”这类低频异常拉高某些学生的活跃度。errorscoerce用来把解析不了的时间转成NaT后面统一删掉不要用 try-except 逐行处理。参数说明drop_duplicates的subset决定重复口径如果某个食堂不同窗口在同一秒各自扣费可能被误删所以使用时可以把 merchant 换成 category按你的实际数据调整。时段过滤也可以放宽到 0-24但做餐段分析时午夜交易对“早餐/午餐/晚餐”分类没有帮助先过滤反而能减少噪声。2.3 按学生聚合把流水变成行为特征清洗之后的流水仍然是明细表不能直接进入聚类。建议先按时间桶聚合再合并到每个学生一行。常见做法是构造下面这些基础特征daily df.groupby([student_id, df[trade_time].dt.date]).agg( day_txn(amount, count), day_amount(amount, sum), day_max(amount, max), ).reset_index() stu_feat daily.groupby(student_id).agg( active_days(day_txn, size), # 活跃天数 total_txn(day_txn, sum), # 总消费次数 total_amount(day_amount, sum), # 总消费金额 avg_daily_txn(day_txn, mean), # 日均次数 avg_daily_amount(day_amount, mean), # 日均金额 max_single(day_max, max), # 单笔最大金额 ).reset_index()聚合顺序是先按天归约再按学生归约而不是直接对学生做 count 和 sum。原因很简单直接 sum 会把一天内连续刷10次的小额消费放大而先算日均之后特征更接近真实行为。比如“早餐固定刷2次”的学生活跃天数一样但日均次数不同聚类时会自然分成两类。max_single是很值得留的特征它能在不依赖商户信息的情况下识别出“偶尔买大件”的学生。如果这个值明显高于大学校园卡常规消费还能在报告里引出“校园贷或代购风险”的讨论前提是数据字段支持。2.4 时间特征餐段和周内偏好是最容易出图的两类特征继续用 pivot_table 生成时段占比。因为最后聚类需要每个学生一行特征列可以是“早餐次数占比、午餐次数占比、晚餐次数占比、夜宵次数占比”。def meal_period(hour: int) - str: if 6 hour 9: return breakfast elif 10 hour 14: return lunch elif 16 hour 20: return dinner elif 21 hour 23: return late_night return other df[period] df[trade_time].dt.hour.map(meal_period) period_ratio ( df.groupby([student_id, period]) .size() .unstack(fill_value0) ) period_ratio period_ratio.div(period_ratio.sum(axis1), axis0) period_ratio period_ratio.add_prefix(meal_ratio_)逻辑说明div(..., axis0)会把每个学生的各餐段次数转成占比这样饭量大的学生不会被误判为更喜欢早餐。unstack(fill_value0)保证某学生完全不在某个时段出现时补0不产生NaN。参数说明餐段边界不是死的有些学校午餐从11点开始有些下午课多会拖到13点以后建议先用df[trade_time].dt.hour.value_counts()看一眼分布再调整字典。这个步骤属于特征工程里最便于解释的部分写报告时可以配一张“各时段消费次数堆积图”比只放相关系数更像数据分析。注意meal_period里包含other如果保留要保证它不是聚类时的主要权重通常删掉meal_ratio_other因为占比可能低且不稳定。特征列之间强相关也会干扰KMeans的球形假设下一章会处理。3. 用KMeans做消费行为分群不要把聚类当成黑盒3.1 选择KMeans的核心原因解释成本低替换成本也低在学生校园消费行为分析项目代码里不一定非用聚类不可但KMeans是最容易解释的。它本质上是将样本划分成k个簇每个簇的中心就是一个平均值向量。落到报告里你可以写“第2类学生的早餐占比均值是0.41午餐占比是0.30日均消费次数2.8次”这类描述对任课老师来说非常友好。它不是没有缺点。KMeans对异常值敏感所以我在第2章清洗时去掉金额为0的行也假设簇是凸的所以量纲不同的特征必须标准化。如果数据中存在严重的“一元聚类”或大量重复学生可以先跑一遍轮廓系数验证再决定是否换用 DBSCAN。对校园卡数据来说KMeans是性价比最高的起点。3.2 标准化和特征压缩先做StandardScaler再决定要不要PCA聚类前构造出的特征包括 total_amount、avg_daily_txn、meal_ratio_* 等单位不同直接跑KMeans会让总金额主导距离。标准做法是from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA feature_cols [ total_amount, avg_daily_txn, avg_daily_amount, max_single, meal_ratio_breakfast, meal_ratio_lunch, meal_ratio_dinner, meal_ratio_late_night ] X stu_feat[feature_cols].fillna(0) X_scaled StandardScaler().fit_transform(X)逻辑说明先fillna(0)是方便处理没有晚市消费的学生但在报告中要说明“零填充只用于特征矩阵不用于原始数据”。StandardScaler把每个特征变成均值0、方差1确保聚类时各维度权重一致。不要用 MinMaxScaler因为校园卡数据里个别高消费学生的离群值会把大部分样本压缩到很小的区间反而削弱差异。如果你希望可视化时在二维平面解释人群边界可以再接PCA降到2维但要注意降维会损失可解释性。一种折中是保留完整特征用于聚类最后只把聚类结果和PCA前两主成分一起画图做展示而不是把降维结果直接拿去聚类。pca PCA(n_components2, random_state42) coords pca.fit_transform(X_scaled) stu_feat[x] coords[:, 0] stu_feat[y] coords[:, 1] print(pca.explained_variance_ratio_)参数说明PCA会在数据标准化之后再做因为标准化之前协方差矩阵会被金额特征主导。random_state固定是为了答辩时重新运行能复现同一张图。如果前两维累计解释率低于0.5说明原始数据的信息不容易在二维展示不要硬说“二维分群清晰”可以改用三维图或直接看聚类中心表。3.3 确定k值同时看肘部曲线和轮廓系数很多课程设计代码把k写死成3这是最容易被问倒的地方。更稳妥的办法是把2到8的聚类结果都算一遍同时输出惯量和轮廓系数from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score import numpy as np inertia_list, sil_list [], [] k_range range(2, 9) for k in k_range: km KMeans(n_clustersk, random_state42, n_init10) labels km.fit_predict(X_scaled) inertia_list.append(km.inertia_) sil_list.append(silhouette_score(X_scaled, labels)) best_k np.argmax(sil_list) 2 print(best k:, best_k)逻辑说明inertia是样本到簇中心的平方距离和曲线出现拐弯的位置对应一个合理的k。silhouette_score衡量簇内紧密和簇间分离程度取值越高越好。两个指标不一定指向同一个k建议以业务解释作为最终标准。比如k4的轮廓系数略低于k3但k4能拆出一类“夜间活跃型”那k4更值得采用。参数说明n_init10表示每个k重新初始化10次选最优结果避免KMeans落入局部最优。对这份数据集规模10次足够不需要把n_init调得太大数据量超过10万行时再考虑增加到20。还有一点容易被忽略k_range不要从1开始因为k1的轮廓系数没有定义写了反而会被追问。3.4 聚类中心表和人群命名给每个簇命名是区分“会做分析”和“只会跑代码”的重要分水岭。先看聚类中心再结合业务习惯命名km KMeans(n_clustersbest_k, random_state42, n_init10) stu_feat[cluster] km.fit_predict(X_scaled) cluster_profile stu_feat.groupby(cluster)[feature_cols].mean() cluster_profile cluster_profile.round(3) print(cluster_profile.to_string())to_string()是为了在Jupyter Notebook里输出整齐的表格如果要在报告里用直接cluster_profile.to_csv(cluster_profile.csv)。最终得到的聚类中心表大概长这样clustertotal_amountavg_daily_txnmeal_ratio_breakfastmeal_ratio_lunchmeal_ratio_dinnermeal_ratio_late_night0中等高0.250.370.300.061高中0.180.280.260.212低低0.080.410.420.033极高中0.200.330.290.12根据均值特征可以命名0是“高频均衡型”1是“夜间消费型”2是“食堂正餐型”3是“高客单型”。命名要简短且能从表里被验证不要用“类型A”这种占位词。4. 完整跑通一份 python学生校园消费行为分析项目代码从数据清洗到可视化4.1 把流程组织成可复现的notebook或脚本一套能拿高分的学生校园消费行为分析项目代码不能只有零散单元格。我建议按下面顺序组织代码文件data/放原始csv和清洗后的parquetsrc/clean.py数据清洗src/features.py特征工程src/cluster.py聚类与评估src/report_charts.py图表输出这样一个项目结构既照顾了大作业要求也方便后续修改。以下是一个主流程示例把这些函数串起来import logging import pandas as pd from src.clean import clean_card_data from src.features import build_student_features, build_meal_ratio from src.cluster import select_k, run_kmeans logging.basicConfig(levellogging.INFO) raw pd.read_csv(data/campus_card.csv) clean_df clean_card_data(raw) logging.info(fraw{len(raw)} cleaned{len(clean_df)}) stu_feat build_student_features(clean_df) stu_feat stu_feat.merge(build_meal_ratio(clean_df), onstudent_id, howleft) best_k, sil_scores select_k(stu_feat, feature_cols, k_rangerange(2, 9)) logging.info(fbest_k{best_k}, silhouette{sil_scores[best_k-2]:.3f}) stu_feat run_kmeans(stu_feat, feature_cols, n_clustersbest_k)逻辑说明logging.basicConfig的levellogging.INFO是为了让答辩现场能看到每一步跑到了哪里清洗后样本量的变化会被记录在log里这也是一份数据质量报告。merge(..., howleft)保证即使某个学生没有任何餐段记录也不会被拼接丢掉。feature_cols建议放在features.py的全局变量里脚本之间统一引用。如果你用的是notebook就把每个代码块写清楚cell标题不要一个100行的大cell否则答辩演示时很难分步骤讲。4.2 可视化图表怎么选趋势、结构、分群三张图就够一个常见的误区是把 matplotlib 能画的所有图表都塞进报告。真正有说服力的图往往只有三类。第一类是消费趋势图用来回答问题“什么时段校园消费最集中”。可以用df.set_index(trade_time)[amount].resample(W).sum().plot()看周度变化或者画24小时消费量分布。第二类是商户结构图用柱状图看各category的总消费额比如食堂、超市、水果店、开水房。第三类是分群效果图把PCA投影后的点按cluster着色import matplotlib.pyplot as plt plt.figure(figsize(10, 7)) scatter plt.scatter( stu_feat[x], stu_feat[y], cstu_feat[cluster], cmapviridis, s18, alpha0.7 ) plt.colorbar(scatter, labelcluster) plt.xlabel(PCA component 1) plt.ylabel(PCA component 2) plt.title(Student segments by consumption behavior) plt.savefig(output/clusters_pca.png, dpi150, bbox_inchestight) plt.show()逻辑说明散点图只用来展示聚类的边界感不代表真实的原始坐标所以x轴和y轴写 PCA component 1/2不能写“消费金额”和“消费次数”。s18控制点大小校园卡数据学生数通常在数千人点太大容易重叠alpha0.7处理重叠点的透明度如果人数过万可以把alpha降到0.4。bbox_inchestight防止保存时坐标轴标签被截断。在一些高分报告里还会配一张聚类中心热力图直接反映不同人群的特征差异。热力图用颜色代表标准化后的均值非常直观。但要注意热力图展示的应该是标准化值或原始均值不要直接放X_scaled否则老师不知道单位是什么。4.3 交叉表验证人群差异聚类要有业务旁证聚类之后如果只看聚类中心表仍然可能被质疑“是不是硬分的”。可以用一个聚类外变量做交叉验证。比如把商户大类category与cluster做交叉表ct pd.crosstab(df_final[cluster], df_final[category], normalizeindex) print((ct * 100).round(1))这里df_final应该是每个学生的消费明细按cluster回填之后的表做法是先给学生表打上cluster再回到流水分组表去映射。normalizeindex是把每一行转成百分比适合比较多个人群内部的商户偏好。比如“夜间消费型”在“超市”类目的占比明显高于其他人群那说明该人群不只是睡得晚而是有固定的零食采购习惯这就是可以写进报告的业务旁证。聚类结果如果无法用任何外部字段验证报告深度就会停留在“机器分出了三类”这种结论上。4.4 把中间结果缓存下来答辩时能快速回溯课程设计答辩最怕的问题之一是“你这个图是怎么来的”。如果每次重新运行从csv到聚类要跑2分钟临时回答会很狼狈。建议把所有关键中间结果存成文件stu_feat.to_pickle(output/stu_feat_with_cluster.pkl) cluster_profile.to_csv(output/cluster_profile.csv) sil_scores.to_csv(output/silhouette_scores.csv, indexFalse)逻辑说明to_pickle比csv更适合保存DataFrame的dtype召回时不会把全角数字读错。如果报告里的图表是你在一台机器上生成的答辩换电脑时只需读取pickle重新画图不需要重新执行清洗和聚类。sil_scores存下来之后可以在论文里写成“k从2到8的轮廓系数见附录”。参数说明保存路径统一放在output/不要把中间结果和原始数据混在data/里。项目源码交付时通常只保留原始数据集和一个output/空目录中间产物由用户运行生成这样既保证数据一致性也避免提交文件过大。5. 把聚类结果落进报告两个能被追问的具体技巧5.1 用“餐段-商户”交叉表代替“建议多开窗口”很多项目在报告结尾写“增加食堂窗口、延长营业时间”这是正确的废话。一个可执行的做法是找出各个餐段中消费频次最高的前几个商户然后量化高峰错峰窗口。以下代码把明细数据转成按餐段和商户分组的表并输出高峰时段peak ( clean_df.groupby([period, merchant]) .size() .reset_index(nametxn_cnt) .sort_values([txn_cnt], ascendingFalse) ) print(peak.head(10))再进一步算出单个商户在对应餐段中的占比就能看出某个窗口是否承担了过高的压力peak[share] peak[txn_cnt] / peak.groupby(period)[txn_cnt].transform(sum)然后在报告里写“午餐时段一食堂某个窗口占时段总笔数的18%建议高峰期增加一个取餐口”。这就是从行为分析到管理建议的落点。可以放一张表格式类似periodmerchanttxn_cntsharelunch一食堂2号窗口12000.18dinner一食堂5号窗口9800.15late_night超市收银台7600.225.2 异常消费识别用余额序列找“刷空卡”事件这个技巧在报告里可以作为“学生消费风险画像”的一部分非常容易加分。先按学生和交易时间排序然后计算每次交易后的余额如果余额低于一元且前一次余额不低于3元则标记为一次“刷空卡事件”clean_df clean_df.sort_values([student_id, trade_time]) clean_df[prev_balance] clean_df.groupby(student_id)[balance].shift(1) clean_df[drop] clean_df[prev_balance] 3 # 前一次余额不低于3元 clean_df[now_low] clean_df[balance] 1 # 本次交易后不足1元 empty_card clean_df[clean_df[drop] clean_df[now_low]] low_stu empty_card.groupby(student_id).size().sort_values(ascendingFalse)参数说明阈值3元和1元要按菜品最低价调整如果学校食堂最低消费5元可以改成5和2。shift(1)是组内上一行的balance偏移因为之前已经按学生排序偏移后能判断这次消费是不是把余额刷到了临界点。这个分析不需要额外收集数据答辩时还能带出“学生临时充值提醒”的应用场景。这个阈值在几万条流水上跑完不到一秒比较适合放在报告附录里作为模型之外的补充验证。本文还有配套的精品资源点击获取
返回列表