资讯动态

Python校园消费行为分析:从流水清洗到K-Means聚类

发布时间:2026/10/19 10:24:38 来源:尧图企业网站定制
简介数据分析是挖掘业务规律的基础工具而聚类分析则是将海量行为数据转化为人群标签的常用方法。无论是电商交易、校园一卡通流水还是其他行为日志数据分析流程都遵循清洗、聚合、可视化、建模的通用路径。本文从数据清洗与字段对齐讲起聚焦pandas处理重复记录与异常值、matplotlib绘制消费时段与金额分布图再引入K-Means聚类构建学生消费画像并回答“哪类学生是消费主力”等业务问题。该方法不依赖复杂分布式环境仅用Python基础库即可完成统计分析与人群分层适用于课程设计、毕业设计或快速产出分析报告等场景。最终通过轮廓系数选择K值、特征标准化与PCA降维验证将聚类编号翻译为可解释的消费群体标签让数据分析真正闭环到决策建议。1. 一份学生消费流水的毕业设计难点不在建模而在还原业务很多同学拿到“学生校园消费行为分析”这个题目第一反应是找现成的开源数据集或者直接往机器学习模型上靠。实际上手才发现校园一卡通流水更像一个黑匣子每一条记录只有卡号、时间、金额、商户名称没有性别、年级、专业这些维度就算想分人群也无从下手。这套基于Python的学生校园消费行为分析方案解决的是从原始流水到结论落地的完整链路数据清洗和字段对齐怎么做统计图表怎么和业务解释对上聚类结果怎么翻译成“哪类学生是消费主力”这类可答辩的结论。它适合正在做数据分析方向课设、毕设的学生也适合想快速把一卡通数据变成一份完整分析报告的新手。整份资源包含两个数据表、三组Python脚本和一批可视化的结果图按任务拆开看每一步都有明确的输入与输出。2. 方案总体设计两份数据表、三组脚本与全链路交付物拿到压缩包之后第一步不是看代码而是把文件清单和任务线对应起来。该资源的文件结构其实非常清晰data1.csv与data2.csv是输入数据task1_*.py负责数据清洗task2_*.py负责消费统计与可视化task3_*.py负责特征工程与聚类画像最后result目录里存放全部图表和中间结果。下面按模块说明。2.1 数据集语义还原data1.csv 与 data2.csv 的字段对应关系这份资源没有提供字段字典但按校园消费场景的惯例和脚本中对列的引用方式可以还原两个表的组织方式。data1.csv是一卡通消费流水明细通常包含学生编号、消费时间、消费金额、商户类型或窗口编号data2.csv是学生基础信息表包含学生编号、性别、年级、院系、宿舍楼区域等属性。两份表通过学生编号字段关联属于典型的事实表加维度表结构。用pandas做关联前的第一步一定是检查两个表的键是否唯一。有的校园卡系统里同一学生编号对应多张卡有的基础信息表存在一人多条记录的情况直接merge会让流水行数膨胀后面的聚合统计全部失真。建议先对基础信息表按学生编号去重保留一条最新记录再做连接。另一个容易忽略的点是编码格式一卡通系统导出的 CSV 经常是 GBK 或 GB2312 编码直接pd.read_csv()读进来全是乱码一般用encodinggbk或encodingutf-8-sig来回试。2.2 三个任务的职责划分从清洗到画像的递进关系任务一对应task1_X1.py、task1_X2.py产出task1_X.csv与task1_1_X.csv。前者是清洗后的流水明细表后者是学生维度的聚合表这一步解决“数据能不能直接用来分析”的问题。常见的清洗动作包括删除同一学生同一分钟同一金额的重复消费记录、过滤金额为负数的退款和充值记录、将时间字段统一为 datetime 类型、处理缺失的商户类型。任务二对应task2_X1.py产出task2_X1.png至task2_X5.png五张图分别覆盖消费时段分布、金额分布、商户类型消费笔数排行、每月消费总额趋势、人均消费金额分布。这五张图对应答辩时最常被问的“你发现了什么规律”每一张图都要能讲出一个业务结论例如“午餐时段消费笔数明显高于早餐和晚餐”而不是单纯堆图。我在拆这个任务时发现画图本身不难难的是让图表的纵轴、图例、标题能直接回答一个问题所以每张图我都固定好输出文件名方便对照结果集核对。任务三对应task3_X1.py至task3_X3.py产出task3_X1.png至task3_X4.png四张图是整份方案的加分项。这里用到 K-Means 聚类先把流水聚合为每名学生的特征向量再做标准化与聚类最后把聚类结果投射到二维平面并给每一类人群做画像。输出结果从“统计规律”升级到“人群分层”是区分高分毕业设计和普通作业的关键。2.3 技术栈选型为什么用 pandas matplotlib scikit-learn 就足够整份资源基于 Python 完成核心依赖只有pandas、numpy、matplotlib、scikit-learn四个库。相比直接上 PySpark 或深度模型这套组合在这个场景下更合理数据量级是单校区一学期的一卡通流水顶多几万到几十万行单机 pandas 完全跑得动算法层面只需要清洗、聚合、聚类和可视化没有复杂的分布式需求答辩现场演示时依赖越少越不容易翻车。另一个现实因素是很多课程环境只预装 Anaconda这四个库是基础包不需要额外配环境。如果样本量超过 50 万行groupby和merge的速度会下降通常的做法是用category类型优化商户字段或者先用pyarrow引擎读 CSV能显著减少内存占用。但这份数据集的规模远没到需要优化的程度所以我更建议把精力放在字段语义对齐和业务结论上而不是过度工程。3. 任务一与任务二实现数据清洗、统计口径与图表快速产出这一章是整套代码里最容易被复制和改造的部分。任务一的代码解决“拿到手的数据怎么变成能用的表”任务二的代码解决“怎么用图表把规律讲清楚”。两者连在一起就是一份完整的探索性数据分析流程。3.1 数据清洗代码去重、时间解析与异常金额过滤import pandas as pd import numpy as np # 读取流水明细与基础信息两个文件都按 utf-8-sig 尝试乱码时换 gbk df_log pd.read_csv(data1.csv, encodingutf-8-sig) df_info pd.read_csv(data2.csv, encodingutf-8-sig) # 基础信息表按学生编号去重防止 merge 后流水行数膨胀 df_info df_info.drop_duplicates(subset[student_id]) # 删除同一学生同一分钟同一金额的重复记录一卡通重复扣款场景 df_log df_log.drop_duplicates( subset[student_id, consume_time, amount] ) # 时间列统一转为 datetime解析失败的行会被置为 NaT df_log[consume_time] pd.to_datetime( df_log[consume_time], errorscoerce ) # 过滤无效时间与异常金额校园消费通常在 0.5~100 元内 df_log df_log.dropna(subset[consume_time]) df_log df_log[(df_log[amount] 0.5) (df_log[amount] 100)] # 合并基础信息用于后续按人群分组分析 df pd.merge(df_log, df_info, onstudent_id, howleft) df.to_csv(task1_X.csv, indexFalse, encodingutf-8-sig)这里几个参数需要重点说明。subset参数指定去重时参考的列组合只对卡号去重太粗暴会误删同一学生一天内的多笔真实消费组合键能兼顾“重复扣款”和“真实多次消费”的区别。errorscoerce让无法解析的时间变成NaT而不是抛异常中断脚本之后再统一丢弃这样单条脏数据不会拖垮整批处理。金额阈值 0.5 到 100 元不是拍脑袋定的校园一卡通早餐一般 3 到 8 元午餐晚餐 8 到 20 元超市购物二三十元超过 100 元的多为代缴或批量充值不同学校差异较大建议先用df_log[amount].describe()看分位数再定上下限。3.2 学生维度聚合生成 task1_1_X.csv 的字段设计task1_X.csv是清洗后的流水明细行数仍然很大直接做聚类不合适。task1_1_X.csv是把流水压缩到学生维度的聚合表每一行代表一名学生在一个统计周期内的消费概况。核心聚合字段包括消费总次数、消费总金额、单笔平均金额、消费活跃天数这些字段是后续所有分析的基石。# 学生维度聚合次数、总额、均值、活跃天数 stu df.groupby(student_id).agg( total_count(amount, count), # 总消费笔数 total_amount(amount, sum), # 总消费金额 avg_amount(amount, mean), # 单笔平均金额 active_days(consume_time, lambda s: s.dt.date.nunique()) # 活跃天数 ).reset_index() # 衍生字段日均消费次数、日均消费金额、早餐/午餐/晚餐占比 stu[daily_count] stu[total_count] / stu[active_days] stu[daily_amount] stu[total_amount] / stu[active_days] stu.to_csv(task1_1_X.csv, indexFalse, encodingutf-8-sig)活跃天数这一列是整个聚合的隐藏重点。如果只统计总次数和总额一名每天都来食堂吃饭的学生和一名只在月初月末集中充值消费的学生可能总金额接近但行为模式完全不同。active_days参与衍生字段计算后daily_count和daily_amount才能真实反映消费频率这也是后续聚类特征里区分度最高的两个变量。输出 CSV 时统一用utf-8-sig编码这样 Excel 直接打开不会乱码给导师看中间结果也省心。3.3 消费时段分布图从数据到业务解释的完整链路时段分布是答辩时最好讲的一张图它直接对应“学生作息规律”这个业务问题。实现上把consume_time抽取小时按小时分组统计笔数和金额再画成柱状图。task2_X1.png就是这张时段分布图我按小时聚合后通常会叠加一条金额折线让“笔数高峰”和“金额高峰”在同一个坐标系里对比。import matplotlib.pyplot as plt # 中文字体与负号显示设置必须放在画图之前 plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False df[hour] df[consume_time].dt.hour hour_stat df.groupby(hour)[amount].agg([count, sum]).reset_index() fig, ax1 plt.subplots(figsize(10, 4)) ax1.bar(hour_stat[hour], hour_stat[count], color#4C72B0, label消费笔数) ax2 ax1.twinx() ax2.plot(hour_stat[hour], hour_stat[sum], color#C44E52, markero, label消费金额) ax1.set_xlabel(消费时段小时) ax1.set_ylabel(交易笔数) ax2.set_ylabel(消费金额元) plt.title(校园消费时段分布笔数与金额双轴对比) plt.xticks(range(0, 24)) plt.tight_layout() plt.savefig(task2_X1.png, dpi150)双轴图容易踩的坑是左右刻度量级差太多导致其中一条线被压平。我的习惯是先单独看两列数据的最大最小值如果金额最大值是笔数最大值的十倍以上就说明两个指标不在一个量级需要考虑对金额列做对数变换或者改用归一化后的双轴。另外plt.xticks(range(0, 24))保证横轴完整显示 0 到 23 点不会因为某些小时没有消费数据就缺刻度。task2_X2.png到task2_X5.png的实现思路类似金额分布画直方图加核密度曲线商户类型排行画水平柱状图方便读标签月度趋势画折线图观察学期内变化人均消费金额画箱线图看离群点。每一张图都要在答辩时对应一句结论画完图后我一般会随手在代码注释里写上该图能支撑的业务结论避免分析做完就忘。4. 任务三实现特征工程、K-Means 聚类与学生人群画像任务三是整份方案中技术含量最高的环节也是评审老师判定“有没有真正掌握数据分析方法”的分水岭。它输入的是任务一产出的学生维度聚合表输出的是四张图和对应的人群画像结论。这一章重点讲特征怎么选、K 值怎么定、聚类结果怎么解释。4.1 聚类特征构建不是所有聚合字段都适合直接进模型从task1_1_X.csv中能拿到总笔数、总金额、单笔均值、活跃天数、日均笔数、日均金额等字段但不能全扔进 K-Means。总笔数和总金额高度相关属于共线性特征会让聚类结果过度偏向金额维度活跃天数和日均笔数信息重复两者保留一个即可。我最终保留三个特征日均消费次数、日均消费金额、单笔平均金额。这三个特征分别刻画消费频率、消费总量和消费档次业务含义互不重叠。特征选定后标准化是必须做的一步。K-Means 基于欧氏距离计算样本相似度日均金额的数值范围是几十单笔均值的范围是个位数到二十几如果不标准化聚类结果几乎完全由金额字段主导消费次数的区分度会被淹没。用StandardScaler做 Z 分数标准化后三个特征均值为 0、方差为 1权重才平等。4.2 K 值选择与聚类实现轮廓系数曲线匹配 task3_X1.pngK-Means 最被人诟病的地方是 K 值需要人工指定。这里不靠玄学用轮廓系数曲线来选。轮廓系数衡量样本与自身簇的紧密度和与相邻簇的分离度取值范围 -1 到 1越接近 1 说明聚类效果越好。通常遍历 K 从 2 到 6画出每个 K 对应的平均轮廓系数取峰值或肘部对应的 K 值。from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score features stu[[daily_count, daily_amount, avg_amount]].copy() scaler StandardScaler() X scaler.fit_transform(features) # 遍历 K 值计算轮廓系数 sil_scores [] for k in range(2, 7): model KMeans(n_clustersk, n_init10, random_state42) labels model.fit_predict(X) score silhouette_score(X, labels) sil_scores.append(score) print(fK{k}, silhouette_score{score:.4f}) # 选出轮廓系数最大的 K 值作为最终聚类数 best_k range(2, 7)[sil_scores.index(max(sil_scores))] final_model KMeans(n_clustersbest_k, n_init10, random_state42) stu[cluster] final_model.fit_predict(X)n_init10这个参数要特别说明。K-Means 的初始质心是随机的单次运行可能陷入局部最优n_init表示用不同随机种子跑 10 次取最优结果能显著降低结果的随机性。random_state42固定随机种子保证每次运行结果一致答辩现场重新跑一遍也不会和报告里的数字对不上。轮廓系数对 K 的选择在这个数据集上通常指向 K3即三类人群高频刚需型、中频均衡型、低频随机型。需要注意的是轮廓系数不是越高越好。K 越大每个簇越紧致轮廓系数往往越高但过细的分群失去业务解释力。如果 K5 的轮廓系数只比 K3 高一点点而 K5 的群体画像很难用一句话概括我一般会选 K3因为答辩时能给每个群体起名字、讲特征这才是聚类的目的所在。4.3 画像解释与结果图映射把聚类编号翻译成业务语言聚类完成后输出task3_X1.png到task3_X4.png四张图。task3_X1.png是轮廓系数随 K 值变化的曲线展示选 K 的过程依据task3_X2.png是用 PCA 降维后把样本点投射到二维平面的散点图不同聚类不同颜色让评委直观看到分群效果task3_X3.png是三个类别的特征均值对比柱状图可视化每类人群在日均笔数、日均金额、单笔均值上的差异task3_X4.png是三类人群占比饼图配合画像结论使用。把聚类编号翻译成业务语言时我一般会给每类人群定义可读的名字和描述。第一类高频刚需型日均消费次数在 3 次以上日均金额高单笔均值稳定在 10 到 15 元这类学生几乎三餐都在食堂解决消费习惯规律是食堂最核心的客群第二类中频均衡型日均 1 到 2 次金额中等偶尔去超市和水果店行为模式贴合大多数在校生第三类低频随机型日均不足 1 次但单笔均值很高可能习惯囤积食品或经常在校外就餐只在特定场景使用一卡通。这三类画像直接支撑答辩时“学校可以针对不同人群制定差异化营销策略”这类建议。聚类结果的验证也不能跳过。我会用原始流水回查每个聚类群体的月度消费趋势确认某类人群不是某一个月异常波动造成的误分。如果某类学生的画像在学期中段突然改变还会检查是否因为一卡通系统升级导致商户类型字段变化。这种交叉验证过程写进论文比单纯贴聚类散点图更有说服力。5. 常见问题与避坑指南这几处不处理复现结果会直接翻车整套源码经过调试可以运行但如果换一台电脑、换一个数据集、改一个字段名最容易出问题的就是下面五个位置。按“现象到原因再到解决”的顺序梳理是为了让读者不只在原数据上跑通还能迁移到自己的数据上也能跑。5.1 CSV 编码格式导致中文乱码现象pd.read_csv(data1.csv)读出来列名全是乱码或者打印数据框时中文显示成菱形符号。原因一卡通系统导出的文件多半是 GBK 编码而 pandas 默认用 UTF-8 解码。解决读取时显式指定encodinggbk或encodingutf-8-sig两种都试一下就能确定实际编码。输出文件时统一用encodingutf-8-sig这样 Excel 打开不会乱码也用 BOM 头规避了再次读取时编码误判。这个坑在 Windows 环境尤其常见matplotlib 画图时中文字体缺失的表现完全不同需要单独设置字体。5.2 时间字段解析失败导致整行数据被丢弃现象pd.to_datetime报错或者转换后大量时间变成NaT聚合结果行数明显偏少。原因原始数据里混入了2024/5/1 12:30、202405011230、2024-05-01 12:30:00等多种格式部分行时间格式不统一。解决先df[consume_time].head(20)人工确认主流格式然后用format参数强制指定解析失败的行设置errorscoerce置为NaT再统一丢弃而不是让脚本中断。经验是任何导出的流水表里都会有少量脏时间清洗时保留这些行会让后续按小时聚合出现幽灵数据。5.3 聚类前忘记标准化导致分群无效现象聚类结果里某一类几乎占据 95% 的样本另外两类各占极小比例PCA 降维图看不出清晰边界。原因没有做标准化金额特征的数值范围远大于次数特征欧氏距离被金额主导。解决在fit前必须用StandardScaler或MinMaxScaler处理所有特征列。检查方式也很直接标准化后打印每个特征的均值和方差确认均值接近 0、方差接近 1 再进入聚类。如果你发现聚类结果和业务直觉对不上九成是这一步漏了。5.4 去重键设置不当导致样本量失真现象drop_duplicates后数据量从 10 万行骤减到 3 万行或者反过来重复记录没有去掉。原因只用student_id去重会误删同一学生的多次真实消费只按全部字段去重又无法命中同一秒重复扣款的情况。解决组合键[student_id, consume_time, amount]是折中方案既能过滤“一卡通终端重复扣款”产生的完全一致记录又不会误删同一天内不同时段的正常消费。如果原始数据里有交易流水号用流水号去重是最准确的但这份资源的数据集没有流水号字段组合键是最稳妥的替代方案。5.5 matplotlib 保存图片为空白或纯色块现象plt.show()能看到图但plt.savefig()出来的 PNG 是空白或全部是白底黑字的残缺图。原因savefig默认使用后端可能与当前环境冲突或者tight_layout没有调用导致坐标轴标签被裁剪。解决savefig前先调用plt.tight_layout()自动调整布局再显式指定dpi150。如果保存的图还是空白在脚本开头加一行import matplotlib; matplotlib.use(Agg)强制使用无界面后端在服务器和本地都能稳定出图。这个坑在远程调试或 Jupyter 环境切换时最常见。6. 验收与答辩用数据核验表和一张主线图撑住全场方案做完不等于可以交付。我一般会按三个层次做验收第一层是数据层确认清洗前后行数变化、金额范围、时间跨度都有记录第二层是代码层保证脚本换一台机器从零跑一遍能产出完全一致的结果第三层是业务层每一张图都要能说出一句对应结论。三层都过了再进入答辩准备。数据层验收建议做一张简单的核验表。清洗前流水总数、清洗后流水总数、去重删除行数、异常金额行数、时间覆盖范围、有效学生人数这六个数字写进论文的预处理章节既展示工作量又方便答辩时被人追问。代码层验收最有效的方式是从原始 ZIP 解压开始不打开任何中间 CSV直接按顺序跑完task1、task2、task3三组脚本比对输出的 PNG 和 CSV 与源结果集是否一致。这一步选出当时源码“经过严格调试、确保可以运行”的说法也让你自己熟悉整个执行流程。答辩时不要从第一行代码开始讲评委没有耐心听你念代码。我采用的策略是准备一张主线图左边是输入数据中间是三个处理步骤右边是输出结论。每一层用一句话解释例如“第一步清洗去掉了 1200 条重复扣款和 38 条异常金额第二步做时段分析和商户排行发现午餐时段消费占比超过 35%第三步用 K-Means 把学生分成三类其中高频刚需型只占 18% 却贡献了 31% 的消费总额”。这段话把需求、方法、结果串成一条线两分钟内讲完后续评委的问题基本围绕细节展开不会出现不知道你做了什么的局面。有一次模拟答辩同学把聚类代码的每个循环都翻出来讲结果被评委反问“你觉得第 30 行为什么要加那个参数”时卡住了。从那以后我再讲任何分析项目都强制自己先总结三个数字跑通代码要多久、清洗掉多少脏数据、聚类分出几类人。能讲清楚这三个数字方案就立住了。希望这份从清洗到聚类的完整拆解能帮你在自己的数据集上少走几步弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑