Python校园一卡通消费分析:从数据清洗到可视化实战
2026/9/16 16:46:22 网站建设 项目流程

简介:面向毕业设计、期末大作业及课程设计场景,Python学生校园消费行为分析项目包含数据与完整源码,适合Python初学者至进阶学习者参考。项目代码注释详实,整体结构清晰,覆盖数据清洗、任务分解、可视化分析等环节,并配套多张图表结果与分析报告,可帮助理解校园消费数据的处理流程。压缩包内共有21个文件,以7个ipynb任务脚本和7个jpg结果图为主,另含3个py脚本、1份docx分析报告、1个md说明、1个txt备注及1个zip附件,包体大小约18.93MB,下载后简单部署即可运行。目前已有329人学习下载。资源来自个人手打高分项目,包含食堂早中晚餐占比、就餐峰值、不同性别消费对比等典型分析维度,可直接迁移用于相似选题的演示或二次开发。

1. 从一卡通流水到消费行为画像:Python分析项目的设计起点

当你拿到几千条校园卡消费记录时,最想做的往往不是算总额,而是从时间戳和金额里读出学生的生活节奏:几点吃早餐、常去哪个食堂、周末和平时消费差多少。这套基于Python的学生校园消费行为分析项目,把这件事做成了一个完整可复现的流程,原始数据、带注释的源码、跑完生成的结果图表都放在一起。对毕业设计、课程设计或者期末大作业来说,它的价值在于给出了从数据导入、清洗、可视化到分组对比的完整路径。项目本身不依赖复杂的展示层,分析链路才是重点,换一套数据也能复用大部分逻辑。

2. 数据清洗与特征工程:把流水变成可分析的结构化数据

2.1 一卡通流水的原始形态

动手前要先确认数据长什么样。从项目的命名和结果图来看,原始数据是典型的校园一卡通消费流水,至少包含消费时间、交易金额、消费窗口、用户性别、专业班级等字段。部分维度可能不在同一张表里,需要通过学号关联。task1_1.ipynb 和 task1_2.ipynb 负责的就是数据导入和初步清洗。

通常拿到手的是 Excel 或 CSV,用 pandas 读取后,第一步是检查字段类型、缺失值和重复记录。一个常见陷阱是金额字段被读成字符串,尤其当原始表里含有“¥”之类的符号时。下面的代码演示了标准处理流程,这套项目的源代码里就采用了同样的思路。

import pandas as pd # 读取原始流水,先不做处理,方便排查格式问题 df = pd.read_csv("campus_consume.csv", encoding="utf-8-sig") # 查看字段名和前几行,确认哪些列有缺失 print(df.head(3)) print(df.info())

这里的encoding="utf-8-sig"是为了兼容 Excel 另存的 CSV 文件开头产生的 BOM 头。df.info()能直接输出每列的非空数量、类型和内存占用,是判断数据质量最快的入口。如果看到金额列是 object 类型,不要急着 to_numeric,先检查是不是有逗号分隔或者货币符号,以免误删记录。

2.2 时间字段解析与消费时段划分

消费行为分析离不开时间。原始数据里的时间字段可能是字符串,需要先统一转成datetime64,然后从中提取小时、星期、日期等维度。这个操作是后面做就餐峰值和周末对比的基础。同时,将时间划分成早餐、午餐、晚餐等业务时段,才能回答“哪一餐消费最高”这类问题。

# 假设原始时间列名为 consume_time df["consume_time"] = pd.to_datetime(df["consume_time"], format="%Y-%m-%d %H:%M:%S") # 提取时间特征 df["hour"] = df["consume_time"].dt.hour df["weekday"] = df["consume_time"].dt.dayofweek # 0=周一,6=周日 df["date"] = df["consume_time"].dt.normalize() # 去掉时分秒,保留日期 # 按业务习惯划分就餐时段,注意边界值要覆盖到整点 def meal_period(hour): if 6 <= hour < 10: return "早餐" elif 10 <= hour < 15: return "午餐" elif 15 <= hour < 21: return "晚餐" else: return "夜宵/其他" df["period"] = df["hour"].apply(meal_period)

format参数显式指定时间格式,比让 pandas 自动推断快且不容易出错。dayofweek返回 0 到 6,后续做周内和周末对比时需要映射成中文。划分时段时用左闭右开区间,避免同一分钟被算作两个时段。时段边界可以根据学校食堂实际开放时间调整,比如有的学校早餐持续到 10:30,那就要把10改成11,才能和后续交叉统计保持一致。

2.3 缺失值、重复值与极端金额处理

流水中经常有退款记录或重复刷卡记录。退款金额是负数,重复记录则是同一时间同一窗口同一金额连续出现两次。这里要区分业务规则,不能直接删掉所有负数。比如校园卡充值消费场景里,负数可能是退餐,其绝对值才有统计意义。

# 去重:同一时间、同一学号、同一金额、同一窗口才算真正重复 df = df.drop_duplicates(subset=["student_id", "consume_time", "amount", "window"], keep="first") # 处理退款:单独标记,不参与正餐消费统计 df["is_refund"] = df["amount"] < 0 df["abs_amount"] = df["amount"].abs() # 筛选明显异常的大额消费,比如超过单笔200元,结合校园消费场景判断 outlier_mask = df["abs_amount"] > 200 print(f"异常大额记录数: {outlier_mask.sum()}")

subset指定去重依据,避免把同一天正常的两笔相同金额消费误删。退款记录应该在“实付金额”维度上特殊处理,如果项目整体只关心消费频次和消费强度,可以单独统计,不建议直接删除后影响均值。极端金额的阈值不要定死,先看df["abs_amount"].describe()的分位数,再决定 200 还是 300。

提示:退款记录先打上is_refund标记再决定是否过滤,直接删掉会让总消费金额偏低。

2.4 构建分析用的特征表

单笔流水粒度太细,分组对比时需要按“人”聚合。这里构建一个以学生 ID 为索引的消费特征表,包含总消费、总次数、日均消费、就餐时段偏好等字段。task3 系列会用到这层数据,不能把所有分析都压在原始流水上,否则透视表计算会随着记录数增长而变慢。

# 按学生聚合 feature = df.groupby("student_id").agg( total_amount=("abs_amount", "sum"), # 总消费金额 total_count=("abs_amount", "size"), # 消费次数 avg_amount=("abs_amount", "mean"), # 单笔平均金额 max_amount=("abs_amount", "max"), # 单笔最大金额 ).reset_index() # 合并性别、专业等静态维度(从另外的信息表读入) stu_info = pd.read_csv("student_info.csv", encoding="utf-8-sig") feature = feature.merge(stu_info, on="student_id", how="left") # 补充时段偏好:计算每个学生各时段的消费占比 period_cross = pd.crosstab(df["student_id"], df["period"], normalize="index") feature = feature.merge(period_cross, on="student_id", how="left") print(feature.head())

groupby().agg()用一个字典一次算出多个统计量,避免多次 groupby 带来的额外开销。crosstab(normalize='index')把每个学生的时段消费次数归一化成比例,保留“这个人更偏哪一餐”的信息。这段代码在源码中以类似形态出现在 task3_1.ipynb 和 task3_2.ipynb 里。how="left"会让没有匹配到信息表的学生变成 NaN,后续分组计算时要dropna或用fillna补成“未知”。

建好的特征表大致是下面的结构,也是后面做占比和显著性分析的基础:

字段名类型计算方式用途
student_idobject原始流水学号关联主键
total_amountfloat逐笔金额绝对值求和消费强度
total_countint流水条数消费频次
avg_amountfloat金额均值单次消费水平
breakfast_ratiofloat早餐次数 / 总次数时段偏好
genderobject学生信息表合并分组变量
majorobject学生信息表合并分组变量

表格里的breakfast_ratio在后续分析里可以和性别做交叉,看看是不是某一类学生更少去食堂吃早餐。

3. 食堂消费占比与就餐峰值:用可视化把规律说出来

3.1 整体占比与早中晚餐分布

项目里已经生成了“食堂占比.jpg”和“食堂午餐占比.jpg”等图片,说明分析过程中是把食堂按窗口或商户聚合,看不同食堂和不同时段的占比差异。可视化之前,先做数据透视,再用 matplotlib 绘制。这里的逻辑是:先算占比,再用图突出重点。饼图适合展示占比结构,但当食堂超过5家时,建议改成横向柱状图,因为饼图的标签会互相遮挡。

import matplotlib.pyplot as plt # 按食堂名称聚合订单笔数 place_count = df.groupby("place").size().sort_values(ascending=False) # 绘制占比饼图 plt.figure(figsize=(8, 6)) plt.pie(place_count.values, labels=place_count.index, autopct="%.1f%%") plt.title("各食堂消费占比") plt.tight_layout() plt.savefig("食堂占比.jpg", dpi=150) plt.show()

autopct控制饼图上的百分比格式,dpi=150保证存下来的图片在论文里足够清晰。注意sort_values(ascending=False)之后,排序靠前的食堂会从12点方向开始依次排列,颜色区分也符合阅读直觉。如果某个食堂占比低于 2%,在饼图上几乎看不见,可以先合并成一个“其他”类别。

3.2 早中晚餐占比对比

如果把时段和食堂两个维度叠在一起,就能看出“哪家食堂承包了早餐”。实现上可以用pivot_tablecrosstab。crosstab 的normalize参数在这里是关键,按列归一化和按行归一化会得到完全不同的结论。

# 时段×食堂 的交叉表 meal_place = pd.crosstab(df["place"], df["period"], normalize="columns") # 只看早餐时段,按占比排序 breakfast_df = meal_place["早餐"].sort_values(ascending=False).head(5) # 绘制横向柱状图 plt.figure(figsize=(8, 4)) breakfast_df.plot(kind="barh") plt.xlabel("早餐时段消费占比") plt.title("早餐主要消费食堂 Top5") plt.tight_layout() plt.savefig("食堂早餐占比.jpg", dpi=150)

normalize="columns"让每列和为 1,也就是同一个时段内各食堂的分配比例。这样保存的图片与项目中的“食堂早餐占比.jpg”对应,含义是“早餐时大家更常去哪家食堂”。如果改成normalize="index",则变成每个食堂内部三个时段的分配,适合回答“某食堂的生意集中在哪一餐”。两个维度并不冲突,可以都画出来互相印证。

3.3 就餐峰值识别

就餐峰值比占比更直观,按小时统计订单量即可。需要注意不同日期类型的峰值可能不同,工作日和周末的食堂开放时间也不一样,所以先拆开看,不要混在一起画一条曲线。峰值识别的结果可以指导食堂窗口排班,这也是这类项目在答辩时容易被追问的落地场景。

# 添加星期类型 df["date_type"] = df["weekday"].apply(lambda x: "周末" if x >= 5 else "工作日") # 按小时和星期类型统计订单量 hourly = df.groupby(["date_type", "hour"]).size().reset_index(name="order_cnt") # 分别绘制两条曲线 for dtype in ["工作日", "周末"]: sub = hourly[hourly["date_type"] == dtype] plt.plot(sub["hour"], sub["order_cnt"], label=dtype, marker="o") plt.xlabel("小时") plt.ylabel("订单量") plt.title("工作日与周末就餐峰值对比") plt.legend() plt.tight_layout() plt.savefig("就餐峰值.jpg", dpi=150)

marker="o"让散点出现在每个整点上,方便读取具体峰值时段。从这样的图能直接看出午餐峰值是否集中在 12 点、晚餐是否被拉长到 19-20 点。如果两条曲线都在某个小时出现尖峰,说明那个时段是全周性刚需,可以进一步按食堂拆分看是哪一个食堂贡献了主峰。

从源码文件的组织看,task2_1.ipynb、task2_2.ipynb 与上面的分析是逐段对应的,推荐按“先占比、后峰值、再交叉”的顺序执行,这样产出的图片自然构成了毕设中的图表章节。对应的输出结构见下表:

分析主题相关源码输入数据输出图表
各食堂整体占比task1_2.ipynb清洗后的流水表食堂占比.jpg
早/午/晚餐时段占比task2_1.ipynb含 period 字段的流水表食堂早餐/午餐/晚餐占比.jpg
工作日与周末就餐峰值task2_2.ipynb含 hour、date_type 字段就餐峰值.jpg

这张表在写报告时可以直接用作“图表索引”,说明每个图是用哪一段代码、从哪一层数据里算出来的。

4. 性别与专业维度的消费差异:从分组统计到显著性验证

4.1 分组聚合看消费强度差异

单看总量没意义,因为男女样本量可能不同。需要看人均和单笔平均。这里用 groupby 按性别分组,计算多指标。注意这里用到的是第2章构建的feature表,不是原始流水。

gender_stat = feature.groupby("gender").agg( student_count=("student_id", "nunique"), avg_total=("total_amount", "mean"), avg_per_order=("avg_amount", "mean"), avg_count=("total_count", "mean") ).round(2) print(gender_stat)

nunique统计的是去重后的学生人数,而不是记录数,避免一个人刷几十次卡把人数带偏。round(2)控制输出小数位。avg_per_orderavg_total的区别是:前者是“每顿平均花多少”,后者是“一个阶段总共花多少”。这两个指标往往有相反的趋势,比如某群体单次消费不高但总消费多,说明它消费频次更高,这两者要一起解读才会更有深度。

4.2 用箱线图检查分布情况

均值会被极端值拉偏,箱线图能展示中位数、四分位数和离群点。对毕业设计来说,一张箱线图加上一个 p 值,比单纯堆数字更有说服力。画箱线图之前先确认分组数据量,如果某一组人数少于 30,箱线图的四分位数就不够稳定,最好在前面加print(feature["gender"].value_counts())看一眼样本量。

import matplotlib.pyplot as plt # 只画男生和女生的日均消费分布 data_by_gender = [feature[feature["gender"] == g]["avg_amount"] for g in ["男", "女"]] plt.figure(figsize=(6, 5)) plt.boxplot(data_by_gender, labels=["男", "女"], showfliers=False) plt.ylabel("单笔平均消费金额") plt.title("不同性别单笔消费分布对比") plt.tight_layout() plt.savefig("性别消费对比.jpg", dpi=150)

showfliers=False不显示离群点,让箱体更紧凑。如果离群点过多,可以先通过分位数裁剪数据,否则箱线图会被压成一条线。在项目里,性别字段可能是“男”“女”也可能是“M”“F”,先用value_counts()确认取值,再做feature["gender"].replace({"M": "男", "F": "女"}, inplace=True),这一步不能省。

4.3 用scipy做独立样本t检验

图形只能“看”,统计检验才能回答“差异是否显著”。这里用scipy.stats.ttest_ind比较男女单笔消费均值。前提是两组样本量不要悬殊,并且大致服从正态分布;校园消费数据通常近似正态,可以直接用这个函数。如果数据明显偏态,改用mannwhitneyu做非参数检验更稳妥。

from scipy.stats import ttest_ind male_vals = feature[feature["gender"] == "男"]["avg_amount"] female_vals = feature[feature["gender"] == "女"]["avg_amount"] t_stat, p_value = ttest_ind(male_vals, female_vals, equal_var=False) print(f"t统计量: {t_stat:.4f}") print(f"p值: {p_value:.4f}") if p_value < 0.05: print("结论:在0.05显著性水平下,性别间消费差异显著") else: print("结论:差异不显著")

equal_var=False表示采用 Welch t 检验,不假设两样本方差相等,适合样本量不一致的情况。p 值只能回答“有没有差异”,不能说“男生就是比女生花得多”,还需要结合上一节的均值方向。在撰写毕业设计报告时,把这段输出直接转述成“某类人群的日均消费显著更高(t=...,p<0.05)”就是一个标准的统计结论。

注意:t检验只能证明差异存在,无法说明差异有多大,报告里最好同时给出均值和标准差。

4.4 专业与性别的交叉对比

项目里有一张“18连锁经营专业不同性别消费对比图”,说明分析维度还包含专业。这里演示如何做二维交叉分析。交叉表的价值在于同时揭示两个因素:专业内部性别差异,以及同性别在不同专业间的差异。但要注意,当某个分组样本量过小时,结果不具备可解释性。

# 专业 × 性别 的消费均值透视表 major_gender = feature.pivot_table( index="major", columns="gender", values="total_amount", aggfunc="mean", observed=True ).round(2) # 保存到csv,方便写报告时引用 major_gender.to_csv("major_gender_consumption.csv", encoding="utf-8-sig")

observed=True是在 pandas 2.x 里处理分类变量时避免告警的常用参数。透视表行列分别是专业和性别,值是总消费均值。save 成 CSV 的时候注意encoding="utf-8-sig",否则用 Excel 打开中文会乱码。如果某个专业只有 1 个学生,aggfunc="mean"得到的值不具备代表性,需要同时输出人数并加过滤条件。这是拿到源码后直接跑最容易忽略的地方。

可以在报告里用下面的表格模板记录结果,把实际数据填入对应单元格即可:

专业男生人数女生人数男生均值女生均值显著性
18连锁经营122815.2012.45p=0.032
19会计83513.1011.02p=0.104

表中数字是示意,实际要跑完项目代码后替换。表格结构本身表达的是:先报告样本量,再报告均值和 p 值,这样的排序能让读者一眼判断结论可信度。

5. 从ipynb到脚本化:复现项目与参数化改造

5.1 把多个notebook串成单入口

项目拆成了多个 ipynb 文件,复现时还要手动按顺序执行。如果想作为毕设演示或交付,建议整理成一个main.py,用函数把清洗、特征工程、可视化和统计检验封装起来。这样导师或答辩评委拿到手后,不需要打开 notebook 就能重跑。

import pandas as pd def load_data(csv_path): df = pd.read_csv(csv_path, encoding="utf-8-sig") # 这里补上清洗逻辑 return df def build_features(df): # 这里补上特征构建逻辑 return feature if __name__ == "__main__": df = load_data("data/campus_consume.csv") feature = build_features(df) # 然后调用生成图表的函数

if __name__ == "__main__":作为程序入口,避免被 import 时直接执行。函数拆分的粒度以“一个函数对应一个任务”为准,比如plot_meal_share(df)对应占比分析,plot_peak(df)对应峰值分析。如果你觉得保留 notebook 更符合课程要求,也可以保留 ipynb,但把公共函数提取到一个utils.py里,notebook 只负责调用和展示。

5.2 用参数控制输入输出路径

源码里直接写好相对路径没有问题,但换机器、换数据集时容易报错。更稳的写法是使用argparse接收路径参数。这样也能把这份源码改成你的毕设题目,比如换成“某高校图书馆消费分析”,只需要在命令行传入新的数据路径。

import argparse import os parser = argparse.ArgumentParser(description="校园消费行为分析") parser.add_argument("--input", default="data/campus_consume.csv", help="消费流水CSV路径") parser.add_argument("--output", default="./result", help="图表输出目录") args = parser.parse_args() os.makedirs(args.output, exist_ok=True)

--output参数配合os.makedirs(exist_ok=True),可以避免每次手动建目录。运行方式变成python main.py --input data/xxx.csv --output ./result,方便反复调参。如果你在 Windows 上跑,注意路径分隔符用/而不是\\,否则在 macOS 或 Linux 上切换会报找不到文件。

5.3 把结论汇总到一份Excel

图片放在文件夹里不直观,建议用pandas.ExcelWriter把统计表和图片说明汇总到一个 Excel 里,方便在毕设文档中引用。导出时最常见的坑是索引没有 reset,导致第一列变成空值。

with pd.ExcelWriter("结论汇总.xlsx") as writer: gender_stat.to_excel(writer, sheet_name="性别对比") major_gender.to_excel(writer, sheet_name="专业性别交叉") hourly.to_excel(writer, sheet_name="峰值统计")

ExcelWriter配合with语句可以在一个文件里写多个 sheet,避免多次打开文件出现权限错误。注意分组聚合后的结果如果带 MultiIndex,需要先reset_index()再输出,否则导出的 Excel 里索引会变成空列。另外,to_excel默认会把 DataFrame 的 index 也写进去,如果 index 是学号这种业务键,保留是有意义的;如果是 range 索引,就在to_excel里加index=False

如果你拿到的数据有更细的商户类别字段,可以把这里的“食堂占比”替换成“窗口菜品类型占比”,透视表和饼图逻辑不用改动。唯一要注意的是窗口字段的编码一致性,有的数据用中文名,有的用编号,先建立映射字典统一成一套标签再进groupby,不然一个窗口会被拆成好几段,占比图会失真。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询