Python校园一卡通消费行为分析:从数据清洗到可视化全流程实战
简介面向数据分析与Python学习者这套资源以校园一卡通消费记录为分析对象围绕学生消费行为展开完整项目实践覆盖数据预处理、特征探索、可视化分析与结论撰写适合课程设计、毕业设计或数据挖掘入门进阶。包内共22个文件、约19.08MB以7个ipynb交互式分析脚本和3个py辅助代码为核心配套8张结果图表、一份docx分析报告及说明文档并附带retail-master原始数据包结构与用途一目了然。已有143人学习下载。资源完整复现“暖心饭卡”公益项目背景下的数据分析过程利用真实刷卡记录还原食堂分时段就餐占比、三餐峰值、不同性别消费差异等核心分析运行notebook即可逐步生成图表并核对结果集便于理解从数据到结论的完整技术路线。1. 校园一卡通消费数据不只会查流水还能画出学生的消费行为全貌南理工用刷卡记录做“暖心饭卡”精准援助的新闻本质上是把校园一卡通流水当成了分析对象——食堂什么时候是高峰、学生平均每顿花多少、哪些人消费长期偏低这些结论都藏在原始流水里。这份基于Python的学生校园消费行为分析源码数据结果集就是把这条链路完整跑通的示例工程从原始刷卡记录开始到清洗、聚合、可视化最后输出能直接放进报告里的图表和结论。它不是那种给你看个demo就完事的资源而是真正可以改路径、调参数、复现结果的完整代码包。适合正在做课程设计、毕业设计或者想快速入门数据分析实战的从业者。2. 数据准备与预处理从一卡通流水到可分析的DataFrame2.1 数据集的目录结构与核心字段说明拿到retail-master.zip解压之后第一件事不是急着跑代码而是先弄清楚目录里到底有什么。这个工程的文件组织是围绕任务拆分的task1_1.ipynb、task1_2.ipynb、task2_1.ipynb、task2_2.ipynb、task3_1.ipynb一直到task3_3.ipynb对应的是一个完整分析流程的各个阶段。外加data目录存放原始数据code目录放的是v2.1版本的整合代码图片结果集直接以jpg形式放在根目录下方便做报告时直接引用。原始数据是典型的校园一卡通流水我拆过的消费数据分析项目里这种数据结构基本都长这样学号、交易时间、消费金额、消费窗口、消费类型这几个字段是标配。其中最容易出问题的字段是交易时间——有的导出格式是字符串有的混入了空值还有的会把日期和时间拆成两列。拿到数据第一步是统一schema我一般先跑一遍df.info()看字段类型再用pd.to_datetime()把时间列统一成datetime类型。import pandas as pd # 读取原始流水encoding按导出格式选常见是gbk或utf-8 df pd.read_csv(data/consumption.csv, encodinggbk, parse_dates[交易时间]) # 统一字段名为小写英文方便后面写代码 df.columns [student_id, trade_time, amount, window, trade_type] # 检查每个字段的缺失情况和类型 print(df.info()) print(df.isnull().sum())这段代码的作用是先把字段名统一再用parse_dates让pandas在读取阶段就把时间列解析成datetime对象。encodinggbk是很多校园系统导出CSV的默认编码如果你的数据是UTF-8这行会直接报错改成utf-8即可。字段名统一成英文小写不是必须的但后面要写几十行聚合代码时全英文小写能省掉很多次Tab键补全时的纠结。2.2 用pandas完成清洗缺失值、异常时间戳与数据类型统一清洗这一步是整个分析里最不性感但最重要的环节。校园一卡通流水里有几类典型脏数据我逐个说第一类是缺失值。部分刷卡记录可能没有消费窗口或者金额字段为NaN。处理思路很简单金额缺失直接删掉因为后续所有统计都依赖金额窗口缺失可以保留因为按时间维度聚合时用不到这个字段。第二类是时间戳异常比如有些记录的时间是1900-01-01这是系统初始化产生的脏数据直接过滤掉即可。第三类是金额为0或负数的情况——负数是退款0元记录可能是查询余额操作这两种都不属于真正的消费行为。# 清洗规则金额缺失删除、时间异常过滤、退款与0元记录剔除 df df.dropna(subset[amount]) df df[(df[trade_time] 2015-01-01) (df[trade_time] 2025-01-01)] # 只保留正常消费记录退款负数和0元操作不参与消费行为分析 df df[(df[amount] 0) (df[amount] 100)] print(f清洗后剩余记录数: {len(df)})逻辑说明先删金额缺失的行因为后续均值、分位数计算遇到NaN会直接得到空结果然后按时间范围过滤掉系统初始化产生的脏数据最后金额上界取100是经验值——校园食堂单笔消费极少超过100元超过这个值大概率是批量充值和异常交易。这三个条件合起来基本能把一卡通流水里的噪声清理掉。参数可以根据你自己的数据分布调整比如有些学校食堂有小卖部单笔金额到200也正常那就把上界调高关键看业务场景。3. 时间维度消费画像早餐/午餐/晚餐占比与就餐峰值的计算3.1 时段划分为什么用固定时间窗而不是机器学习聚类消费行为分析里最直观的维度是时间。食堂什么时候排队最长、早餐到底有多少人吃、晚餐是不是比午餐更分散这些结论都从时间聚合来。这个工程里做了食堂早餐占比、午餐占比、晚餐占比和就餐峰值四张图本质上就是把交易时间映射到三个时段然后统计每个时段的消费频次或金额占全天比例。时段划分看起来是个简单问题其实有讲究。最简单粗暴的做法是写死时间段早餐6:00-9:00午餐11:00-13:00晚餐17:00-19:00。这个方案我用过优点是解释成本低别人看图表能直接理解缺点是某些学生9:30才去吃早餐会被划到午餐时段造成边界误差。更精细的做法是画出全天交易量曲线找到曲线低谷作为分割点——这本质上是人工找拐点比机器学习聚类可控得多也更容易向非技术背景的老师和同学解释。校园消费分析的数据量不大先画分布曲线再定边界是我常用的方法。import matplotlib.pyplot as plt # 提取小时字段 df[hour] df[trade_time].dt.hour # 绘制全天交易频次分布 hourly_counts df.groupby(hour).size() plt.figure(figsize(12, 5)) hourly_counts.plot(kindline, markero) plt.title(全天交易频次分布) plt.xlabel(小时) plt.ylabel(交易笔数) plt.grid(True, alpha0.3) plt.show()逻辑说明先提取小时字段然后按小时分组统计交易笔数最后画折线图。通过图形观察通常能明显看到三个峰——早餐高峰集中在7-8点午餐高峰在11-12点晚餐高峰在17-18点。曲线低谷出现在10点、14点和16点左右这些低谷就是天然的分割边界。参数注释figsize控制图像宽高markero让每个数据点更清晰grid加网格方便读数。3.2 分组聚合与可视化占比图如何从groupby到jpg时段边界定好之后下一步就是计算三个时段的消费占比。这个工程的task2_1.ipynb和task2_2.ipynb主要干的就是这个活。占比的定义有两种口径按笔数算占比反映的是食堂人流分布按金额算占比反映的是食堂收入结构。如果分析目标是了解学生就餐习惯用笔数如果分析目标是食堂经营状况用金额。两份都有参考价值我习惯同时算出来对比看看。# 定义时段划分函数 def meal_period(hour): if 6 hour 10: return 早餐 elif 10 hour 15: return 午餐 elif 15 hour 20: return 晚餐 else: return 其他 # 应用时段标记 df[meal] df[hour].apply(meal_period) # 按时段统计笔数占比 meal_stats df.groupby(meal).size().reset_index(namecount) meal_stats[percentage] meal_stats[count] / meal_stats[count].sum() * 100 # 绘制占比饼图 plt.figure(figsize(8, 8)) plt.pie(meal_stats[percentage], labelsmeal_stats[meal], autopct%.1f%%) plt.title(食堂消费时段占比) plt.savefig(食堂占比.jpg, dpi300, bbox_inchestight) plt.show()逻辑说明meal_period函数接收小时数返回对应的餐段名称这一步把连续的时间映射成离散的分类标签。groupby(meal).size()统计每个时段的记录数percentage计算占比。饼图的autopct%.1f%%控制扇形标签显示格式保留一位小数。savefig里的dpi300是出版级清晰度bbox_inchestight会自动裁剪多余白边这两个参数是让图表能直接放进论文或报告的关键。要注意的是meal_period里我把早餐区间设成了6到10点比前面提到的6-9点多留了一小时缓冲这是根据实际数据分布微调后的结果说明时段边界应该跟着数据走不是越精确越好而是越符合真实行为越好。4. 性别与专业维度的交叉分析18连锁经营专业的消费差异4.1 按性别分组均值、分位数与箱线图的选择时间维度解决的是“什么时候吃”性别和专业维度解决的是“谁在吃、吃多少”。这个工程里有一个专门的输出图叫18连锁经营专业不同性别消费对比图.jpg说明分析对象聚焦在特定专业按性别拆开看消费差异。这种分析在校园场景里很常见——不同专业可能有不同的课表节奏不同性别在食堂消费金额上通常也有稳定差异。性别消费对比的第一步是按性别分组计算描述性统计量。核心指标有两个人均每笔消费金额和月度总消费金额。前者反映单次消费水平后者反映整体消费投入。箱线图是展示这两个维度差异的最好工具——它同时呈现中位数、四分位距和离群点比单纯比较均值更能说明分布形态。# 假设数据里有gender字段和major字段 df[gender] df[student_id].map(student_info[gender]) df[major] df[student_id].map(student_info[major]) # 筛选18连锁经营专业 major_data df[df[major] 18连锁经营] # 按性别分组统计每笔消费金额 gender_groups major_data.groupby(gender)[amount] # 输出均值、中位数和样本量 print(gender_groups.agg([mean, median, count]))逻辑说明student_info是一个独立的DataFrame存放学号和性别、专业的映射关系通过map方法关联到消费流水上。groupby(gender)[amount]把同一性别的所有消费金额归组agg里的列表参数同时计算均值、中位数和样本量。这三个统计量各有用途均值看整体水平中位数看典型水平count看样本量是否足够下结论——如果某个性别只有几十条记录那后面的图就没有统计意义。4.2 交叉维度输出从DataFrame透视到保存对比图有了分组统计的基础下一步是画图。性别对比图可以画箱线图也可以画分面直方图。箱线图的优点是把离群点直接暴露出来——比如男生群体里可能有个别人每顿吃30元以上这在女生群体里几乎不存在。直方图则更适合展示分布的形态差异比如女生群体的消费金额可能集中在8-15元男生群体则更分散。import seaborn as sns # 设置绘图风格 sns.set_style(whitegrid) plt.figure(figsize(10, 6)) sns.boxplot(datamajor_data, xgender, yamount) # 保存图片dpi300保证印刷质量 plt.title(18连锁经营专业不同性别消费对比图) plt.xlabel(性别) plt.ylabel(单笔消费金额元) plt.savefig(18连锁经营专业不同性别消费对比图.jpg, dpi300, bbox_inchestight) plt.show()逻辑说明sns.boxplot接收两个核心参数——x轴是性别字段y轴是金额字段。箱线图中间的线代表中位数箱子范围是四分位距箱外的点是离群值。如果男生组箱体明显高于女生组说明男生在食堂的单笔消费整体偏高。参数说明sns.set_style(whitegrid)设置白色网格背景savefig的文件名和之前类似用清晰的中文名方便直接插入报告文档。5. 常见问题与避坑消费行为分析里容易翻车的几个细节5.1 编码不一致导致数据读取就报错现象pd.read_csv(消费记录.csv)直接抛UnicodeDecodeError或者读出来是乱码。原因校园一卡通系统导出的CSV文件有的用GBK编码有的用GB2312还有少数用UTF-8-BOM。pandas默认用UTF-8解码遇到GBK编码的中文字段就崩了。解决用encodinggbk和encodingutf-8各自试一遍仍然报错就用encodinggb18030这个是GBK的超集兼容性更强。我习惯在读取前先用记事本打开CSV看一眼右下角的状态栏能直接看到编码格式。更稳的做法是读取时加上errorsignore参数至少先把数据读进来再说后续再处理乱码字段。5.2 退款记录混进正常消费导致金额异常偏低现象画出来的消费金额分布图在0元附近有一个突兀的高峰均值被拉得很低。原因一卡通流水里包含退款记录金额是负数还有余额查询操作金额是0。这些记录不是消费行为但在分组统计时会被算进去拉低均值和中位数。解决清洗阶段统一过滤amount 0的记录退款和0元操作不进分析集。我在前面预处理代码里写的就是这个逻辑。需要额外注意的一点是有些系统的退款金额是正数但交易类型字段标着“退款”只过滤负数会漏掉这部分所以要同时检查金额和交易类型两个字段。5.3 时间字段类型不是datetime导致按小时聚合失效现象df.groupby(df[trade_time].dt.hour)报错AttributeError: Can only use .dt accessor with datetimelike values。原因读取CSV时没有指定parse_dates时间字段被pandas自动识别为字符串对象字符串没有.dt访问器。解决读取时加parse_dates[交易时间]或者读取后用pd.to_datetime(df[交易时间])手动转换。我还遇到过更隐蔽的情况——数据里混入了几条空时间戳to_datetime会把空值变成NaT后续聚合计算会出现NaN所以转换前先处理缺失值顺序别反了。5.4 时段边界拍脑袋导致三餐占比失真现象早餐占比远低于常识——早上9点到10点的记录被归到午餐时段午餐占比虚高。原因写死时段边界时没有先看数据分布。有的学生上午第一节没课9点半才去吃早餐有的学校食堂10点半就开始供应午餐。固定的6-9、11-13、17-19三段不能适配所有校园。解决先画全天频次分布曲线找到每个峰谷的实际边界再回头调整meal_period函数的阈值。这个过程不复杂但一定要做——我在前面第3章里展示的就是这个思路。数据驱动的边界设定比拍脑袋靠谱得多。5.5 学号关联消费者信息时key不匹配导致全表变空现象合并后行数骤减甚至全部变成NaN排查了半天发现是分组时所有学生都归到了“未知”。原因消费流水里的学号是字符串学生信息表里的学号可能是数字格式直接关联时类型不一致导致匹配失败或者是学号前后带了不可见空格。解决关联前先统一类型——df[student_id] df[student_id].astype(str).str.strip()两边都做这个操作再合并。用.map()做关联前先print(set(student_info[student_id]) - set(df[student_id]))看看有多少学号匹配不上如果差集很大优先排查类型和空格问题。6. 把分析链路打包成可复用脚本从图表反推筛选逻辑6.1 从结果集反向理解筛选维度这个工程输出了一张叫money.jpg的图一看就是消费金额的分布直方图。这类图的用途有两个一是直观展示全校消费金额的分布形态二是作为“制定补助标准”的依据——如果分布呈现右偏说明多数人消费集中在某个区间少数高消费拖出了长尾。从图反推代码逻辑的关键是看x轴和y轴的标签确认统计口径。实操上money.jpg的生成逻辑大概率是df[amount].hist(bins50)这类代码。我一般会把直方图和KDE曲线叠加在一起看KDE曲线能更平滑地展示分布形状避免直方图bin宽度选择引起的视觉偏差。import numpy as np # 单笔消费金额分布加KDE密度曲线叠加 plt.figure(figsize(10, 6)) sns.histplot(df[amount], bins50, kdeTrue) # 标注均值和中位数位置 mean_val df[amount].mean() median_val df[amount].median() plt.axvline(mean_val, colorred, linestyle--, labelf均值: {mean_val:.2f}) plt.axvline(median_val, colorgreen, linestyle--, labelf中位数: {median_val:.2f}) plt.title(学生单笔消费金额分布) plt.xlabel(消费金额元) plt.ylabel(频次) plt.legend() plt.savefig(money_distribution.jpg, dpi300, bbox_inchestight) plt.show()这段代码补充了均值和中位数的参考线——如果两条线相距很远说明分布偏斜程度大这时候用中位数代表“大多数人的消费水平”比均值更合理。bins50是直方图的箱数数据量大的时候可以调到100让曲线更细腻数据量小就调低到20-30避免直方图太碎看不出形状。6.2 把分析流程封装成可复用模块完成单个任务的分析之后下一步是把整个流程整理成一份可直接复用的脚本。这个工程里的数据及代码v2.1/code目录就是干这个事的。我自己的习惯是把清洗、时段划分、分组统计、画图四个阶段分别封装成函数然后用一个main()流程串起来这样换一份新数据时只需要改文件路径不用重写逻辑。def analyze_consumption(filepath: str, output_dir: str ./output): 一卡通消费行为分析主流程 :param filepath: 原始流水文件路径 :param output_dir: 图表输出目录 import os os.makedirs(output_dir, exist_okTrue) # 读取与清洗 df pd.read_csv(filepath, encodinggbk, parse_dates[交易时间]) df.columns [student_id, trade_time, amount, window, trade_type] df df.dropna(subset[amount]) df df[(df[amount] 0) (df[amount] 100)] # 时间维度分析 df[hour] df[trade_time].dt.hour df[meal] df[hour].apply(meal_period) meal_stats df.groupby(meal).size().reset_index(namecount) meal_stats[percentage] meal_stats[count] / meal_stats[count].sum() * 100 # 输出结果 meal_stats.to_csv(os.path.join(output_dir, meal_stats.csv), indexFalse) return df这段封装代码把前面所有步骤压缩成一个函数。filepath和output_dir是唯二需要外部传入的参数函数内部流程固定。返回的df保留了清洗后的完整DataFrame方便在Jupyter Notebook里继续做探索性分析。参数说明output_dir默认是当前目录下的./output运行前会自动创建返回值是清洗后的DataFrame这是为了方便在后续任务里复用中间结果。从那以后我每次拿到一份新的消费数据都强制先走一遍这个函数——数据没跑通之前不做任何图形分析。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →