news 2026/10/12 9:47:15

学生校园消费行为分析:一卡通流水聚类与数据清洗实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
学生校园消费行为分析:一卡通流水聚类与数据清洗实战

简介:这是一份基于Python的学生校园消费行为分析项目源码,面向Python期末大作业、课程设计以及需要数据分析实战练习的高校学习者,也适合作为理解校园消费场景下行为建模的入门参考。项目以某高校校园消费行为数据集为对象,基于DFM模型进行特征构建与消费模式分析,源码分模块组织,包括数据初始化、模型训练与结果分析等环节,整体经过本地编译调试,具备可运行性,曾获导师认可并达到98分评审成绩。压缩包共8个文件,类型涵盖3个Python脚本、1个Word文档、1个zip数据集、1个txt依赖文件、1个markdown说明、1个gitignore配置等,整体大小约10.07MB,目录中同时包含说明文档和项目文档,便于快速定位代码与数据集。已有123人学习/下载。读者可以获得从数据导入、特征处理、DFM建模到结果整理输出的完整代码链路,结合附带的消费行为数据集可直接复现分析结论,也可在此基础上替换数据开展其他校园场景行为研究,适用于课程报告或期末答辩材料中的重要支撑。

1. 学生校园消费行为分析:从一卡通流水里能挖出哪些规律

学生校园消费行为分析这个项目,简单说就是把几万条食堂、超市、浴室的刷卡流水,整理成一张能反映每个人消费习惯的画像表,再用聚类把学生分成几类,最终产出可解释的统计结果和图表化报告。这套源码+数据+结果集的完整链路并不复杂,但对学校管理和数据分析入门者都很有用:食堂窗口排班、超市铺货、异常消费预警,甚至贫困认定做数据交叉验证时都能用到。适合刚学完 Python 基础、想练完整数据项目的学生,也适合信息化部门想快速跑通第一版分析模型的技术老师。下面我直接按落地顺序讲。

2. 数据与特征工程:把几万条流水变成一张可计算的画像表

2.1 一卡通流水长什么样:核心字段与样例数据

先看原始数据。一卡通系统的数据库导出 CSV 后,常见字段就是下面这几个,不同学校命名会有差异,但核心内容基本一致:

字段名类型示例值说明
student_id字符串20210101学号,导出时建议脱敏
trade_time时间戳2024-03-04 07:22:10交易发生时间
amount浮点数3.50交易金额,退款通常为负值
trade_type字符串消费 / 退款区分正常消费和退款
merchant字符串一食堂01窗口商户名称或窗口编号
balance浮点数156.20交易后卡内余额

开始前先把 Python 环境装好,装 pandas、scikit-learn、matplotlib、seaborn 这几个库就够了。读取数据用的就是 pandas 的 read_csv,但千万别拿到表就急着跑分析,第一步永远是清洗。

2.2 读取数据并清理:四个必做动作

import pandas as pd df = pd.read_csv( "data/consumption.csv", parse_dates=["trade_time"], dtype={"student_id": "string"} ) # 动作1:只保留正常消费,退款单拎出来单独统计 refund = df[df["trade_type"] == "退款"].copy() df = df[df["trade_type"] == "消费"].copy() # 动作2:金额小于等于0的记录直接剔除 df = df[df["amount"] > 0] # 动作3:同一人同一时刻同一商户同一金额,视为重复刷卡 df = df.drop_duplicates( subset=["student_id", "trade_time", "merchant", "amount"] ) # 动作4:生成日期列,供后续按天聚合 df["date"] = df["trade_time"].dt.date

这段代码的逻辑很直接:parse_dates 把 trade_time 解析成 datetime 类型,后面才能按小时、星期切片;trade_type 先按“消费”过滤,退款单独存到 refund 里,不要直接拿负金额去抵消;drop_duplicates 的四个字段组合判断重复,防止同一个窗口短时间内连续刷两次造成重复计数;date 列是后续按天聚合的前提。

这里有个常见误用:有人偷懒把退款金额取绝对值后混进消费里,结果日均消费被明显抬高,后续聚类全部失真。退款和处理后的消费是两类业务事件,必须分开统计。

2.3 特征衍生:从流水到人维度特征

流水是记录级的,一行一个学生的一次消费,但聚类需要把每个学生聚成“一行特征”。这一步叫特征衍生,也是比较费脑子的地方。

# 第一步:按“人 + 天”聚合,得到每天消费额和笔数 daily = df.groupby(["student_id", "date"], as_index=False).agg( spend=("amount", "sum"), cnt=("amount", "size") ) # 第二步:按人聚合,得到整个学期的日均消费、波动、频次 person = daily.groupby("student_id", as_index=False).agg( avg_daily=("spend", "mean"), std_daily=("spend", "std"), avg_cnt=("cnt", "mean") ) # 变异系数:波动程度 / 平均水平,消除绝对金额差异 person["cv"] = person["std_daily"] / person["avg_daily"]

这四个特征分别是日均消费额、日消费波动、日均刷卡次数、变异系数。cv 是核心特征之一:同样的日均消费,有人每天固定花 10 块,有人今天不花明天花 30,这两种人的消费习惯完全不同,cv 能把这层差异暴露出来。

接着补两个业务特征:深夜消费和食堂消费占比。

# 深夜消费:每天22点之后的累计消费 night = df[df["trade_time"].dt.hour >= 22].groupby("student_id")["amount"].sum() person = person.merge(night, left_on="student_id", right_index=True, how="left") # 食堂消费占总消费的比例 canteen = df[df["merchant"].str.contains("食堂")].groupby("student_id")["amount"].sum() total = df.groupby("student_id")["amount"].sum() person["canteen_ratio"] = (canteen / total).fillna(0) person = person.fillna(0)

夜猫型学生的深夜消费明显偏高,这个特征在后面的异常行为预警里很关键。canteen_ratio 用来识别“靠食堂吃饭”和“大量外卖/超市”的学生,数值高说明生活规律、消费场所单一。到这一步,person 表就是后续分析的基本盘:每一行是一个学生,每一列是一个可计算的行为特征。

3. 聚类与画像:用KMeans把学生消费习惯分成四类

3.1 特征矩阵与KMeans参数设定

特征不一定越多越好,选 5 个左右足够:avg_daily、avg_cnt、cv、night_spend、canteen_ratio。聚类之前先做归一化,因为 KMeans 基于欧氏距离,avg_daily 动辄几十、canteen_ratio 只有零到一,数值范围差太多,距离会被大数主导。

from sklearn.preprocessing import MinMaxScaler from sklearn.cluster import KMeans feat_cols = ["avg_daily", "avg_cnt", "cv", "night_spend", "canteen_ratio"] X = person[feat_cols].fillna(0) X_scaled = MinMaxScaler().fit_transform(X) km = KMeans(n_clusters=4, random_state=42, n_init=10, max_iter=300) person["cluster"] = km.fit_predict(X_scaled)

参数里 n_clusters 怎么定?先用肘部法和轮廓系数扫一遍,再结合业务判断。n_init=10 很关键,它让 KMeans 用 10 组不同初始中心跑 10 次,取最优结果,避免掉进局部最优;random_state=42 固定随机种子,保证别人复现你的结果时聚类标签一致;max_iter=300 是迭代上限,数据量几万行时完全够。

补一个选择 k 值的辅助脚本:

from sklearn.metrics import silhouette_score scores = [] for k in range(2, 9): model = KMeans(n_clusters=k, random_state=42, n_init=10) labels = model.fit_predict(X_scaled) scores.append((k, silhouette_score(X_scaled, labels))) print(scores)

轮廓系数范围在 -1 到 1 之间,越接近 1 越好。但校园消费数据有它的特殊性:食堂窗口多、时间段重叠大、学生之间差异没那么分明,所以轮廓系数能到 0.3 以上就是可以接受的水平,不要盯着教科书里的 0.7 不放。这是选 k 时最容易卡住的地方,有点玄学成分,我的做法是“轮廓系数取局部最高,同时保证每个簇能讲出业务故事”。

3.2 簇中心画像:把数字变成业务标签

聚类跑完,立刻看每个簇的中心值:

profile = person.groupby("cluster")[feat_cols].mean().round(3) print(profile)

如果四个簇的中心值长这样,业务标签基本可以定下来:

簇avg_dailyavg_cntcvnight_spendcanteen_ratio业务标签
06.822.10.480.50.88规律节俭型
111.203.00.621.20.85食堂主力型
214.302.81.058.60.61夜猫外食型
321.504.20.903.40.52高消费多元型

注意聚类的标签是模型给的 0/1/2/3,业务标签是人根据特征分布命名的。做报告的时候一定把两者的映射关系写清楚,不然决策部门看到“cluster=2”会一头雾水。我一般会在代码里加一个映射字典,把标签替换成可读文字再输出,避免后续交接时对不上号。

3.3 用t检验验证两组人的差异不是巧合

聚类分了组,但分出来的组真的有统计意义上的差异吗?拿 avg_daily 做例子,用 Welch t 检验:

from scipy.stats import ttest_ind group_a = person[person["cluster"] == 0]["avg_daily"] group_b = person[person["cluster"] == 1]["avg_daily"] t_stat, p_value = ttest_ind(group_a, group_b, equal_var=False) print(f"t={t_stat:.3f}, p={p_value:.4f}")

equal_var=False 对应 Welch t 检验,样本量不均衡、两群方差不同的时候它比普通 t 检验更稳。p 值小于 0.05 才说明“两个簇的日均消费差异在统计上成立”,否则这两个簇在消费金额维度上分不开,聚类结果拿去答辩会被当场问住。这个方法同样适用于验证 cv、night_spend 等其他特征。

3.4 聚类稳定性:换随机种子结果还一样吗

KMeans 对初始中心敏感,换一个 random_state 结果可能完全不同。所以跑完还要做一次稳定性验证:

from sklearn.metrics import adjusted_rand_score labels_1 = KMeans(n_clusters=4, random_state=0, n_init=10).fit_predict(X_scaled) labels_2 = KMeans(n_clusters=4, random_state=100, n_init=10).fit_predict(X_scaled) print(adjusted_rand_score(labels_1, labels_2))

adjusted_rand_score(ARI)接近 1 说明两个随机种子下的聚类结果基本一致,模型稳定;低于 0.6 就要警惕:可能是数据本身没有清晰的簇结构,或者特征选得不好。这时候不要硬调参数,回头检查特征矩阵和离群点更实际。

4. 数据可视化与结果集:让结论既能看又能交差

4.1 结果集应该包含什么

很多初学者跑完聚类,导出一个人带标签的 CSV 就叫“出结果了”。严格说这只能算中间产物,一个合格的结果集应该包含三部分:带标签的学生数据表、可视化图表、以及一份能直接看结论的说明文档。我一般按下面的结构组织:

路径内容说明
results/student_labels.csv每个学生的特征值和聚类标签可继续做统计分析的底表
results/figures/时段分布、箱线图、簇画像等图表报告和汇报用
results/分析结果.xlsx多 sheet 汇总给不写代码的同事看
results/分析报告.md结论、业务建议、局限说明决策依据,要可读

这个结构的好处是“数据、证据、结论”三者对应,领导或导师追问任何一条结论,你都能在结果集里找到对应的表和图。

4.2 中文绘图环境的两个必须设置

matplotlib 默认字体不支持中文,直接画图会出一堆方块,这是 Linux 服务器和 Windows 本地都会遇到的坑。先做字体设置:

import matplotlib matplotlib.rcParams["font.sans-serif"] = [ "SimHei", "Microsoft YaHei", "WenQuanYi Zen Hei" ] matplotlib.rcParams["axes.unicode_minus"] = False

SimHei(黑体)和微软雅黑是 Windows 常见字体,文泉驿正黑是 Linux 常见字体。列表里写多个,matplotlib 会按顺序挑第一个存在的。axes.unicode_minus=False 用来解决坐标轴负号显示成方块的问题。如果服务器上这些字体都没有,先查一下系统有哪些字体:

from matplotlib import font_manager available = {f.name for f in font_manager.fontManager.ttflist} print(available)

查完把 available 里实际存在的字体名填进 rcParams 就行。

4.3 画一张消费时段分布图

import matplotlib.pyplot as plt by_hour = df["trade_time"].dt.hour.value_counts().reindex( range(24), fill_value=0 ) plt.figure(figsize=(10, 5)) plt.bar(by_hour.index, by_hour.values, color="#2a6f97") plt.title("校园一卡通消费时段分布") plt.xlabel("小时") plt.ylabel("交易笔数") plt.savefig( "results/figures/hourly_distribution.png", dpi=150, bbox_inches="tight" )

value_counts() 算出每个小时出现的交易笔数,reindex(range(24), fill_value=0) 把缺失的小时补成 0,比如凌晨 3 点没人刷卡就是 0。figsize=(10, 5) 是 10 英寸宽、5 英寸高,适合放进 A4 报告;dpi=150 兼顾屏幕查看和打印清晰度;bbox_inches="tight" 会自动裁剪图片边缘空白,避免保存出来的图四周白边很大。

4.4 把聚类结果导出成多表Excel

CSV 用 Excel 打开中文容易乱码,大文件还会被截断。我一般直接用 pandas 写 Excel 多 sheet,一个文件装所有结果:

with pd.ExcelWriter("results/分析结果.xlsx", engine="openpyxl") as writer: person.to_excel(writer, sheet_name="学生标签", index=False) profile.to_excel(writer, sheet_name="簇画像") person.groupby("cluster")["student_id"].count().to_excel( writer, sheet_name="簇人数" )

engine="openpyxl" 是因为 pandas 写 xlsx 格式依赖这个库,没装的话会报 ImportError。一个文件三个 sheet,既能看到每个学生的标签,也能看到簇整体画像和人数占比,给业务方的时候不用来回发一堆文件。到这里,结果集基本齐了。

5. 校园消费数据清洗与聚类的排查现场:五个翻车原因与修复方法

5.1 退款被当负消费取绝对值,消费总额虚高

现象:清洗完数据,日均消费超过 20 元,明显不合理;退款笔数越多的人,消费金额被抬得越离谱。

原因:一卡通退款记录 amount 为负值,有人为了“保留全部数据”直接取绝对值,把退款当正常消费计入了总额。

解决:读取数据时先按 trade_type 把退款和消费分成两个 DataFrame,消费只保留 amount > 0 的记录;退款单独统计,用来算净支出,不参与消费特征计算。这两类业务事件必须分开,否则后面的所有统计全部失真。

5.2 用卡号合并导致同一个人被劈成两条记录

现象:聚类结果里出现两个特征几乎一样、只是 cluster 不同的学生;按学号抽查,发现同一个人在新旧两张卡下被当成了两个人。

原因:一卡通系统里卡号会随补卡、换卡变化,直接按 card_id 分组,一个学生的消费记录被拆到两行。

解决:优先按 student_id(学号)而不是卡号分组。如果原始表只有卡号没有学号,就需要先用“卡号 + 生效日期”映射表把卡号关联到学号上,再做聚合。判断数据里有没有这个问题很简单:统计 student_id 去重数量和 card_id 去重数量,如果卡号数量明显大于学号数量,就要警惕。

5.3 聚类前没处理离群点,MinMaxScaler被个别高消费带偏

现象:聚类结果里 80% 的人被分到同一个簇,剩下的簇全是“极端值”。看簇中心才发现,归一化时被几个消费极高的点把整个特征空间拉变形了。

原因:MinMaxScaler 用最大值做分母,一个日均消费 80 元的外卖重度用户,会把 0 到 80 压到 0 到 1,绝大多数日均消费 5 到 15 元的学生挤在 0.1 到 0.2 这个狭窄区间,KMeans 自然分不开。

解决:聚类前先做离群点处理。用分位数裁剪或者直接过滤 99% 分位以上的记录,例如 avg_daily 超过 99% 分位数的学生先单拎出来标记为“特殊消费群体”,不参与 KMeans。或者改用 RobustScaler,它用中位数和四分位距缩放,对离群点不敏感。

5.4 凌晨消费全算“深夜消费”,其实是超市/浴室延迟入账

现象:深夜消费特征 night_spend 特别高的那群人,画像里看不出夜猫子特征,白天消费也很多,逻辑对不上。

原因:部分商户(浴室、开水房、超市)不是实时上传流水,数据在凌晨批量落库,交易时间显示成凌晨 0 点到 2 点,其实发生在白天。

解决:先画出全天时段分布图,看是否存在“整点上报”的尖峰。如果 0 点附近有明显的异常尖峰,就把深夜特征的时间阈值从 22 点调整到 23 点以后,或者干脆排除商户类型为浴室/开水房的记录后重跑。判断标准就一条:深夜特征算出来的人,白天消费行为也要对得上。

5.5 寒暑假数据混进来,人均日消费被严重稀释

现象:聚类结果里出现一批“几乎不消费”的学生,点开明细发现全是寒暑假食堂关闭期间的零星超市数据。

原因:数据导出的时间范围跨越了寒暑假,学生在校和不在校的时间混在一起算均值,每天分母被拉大,消费均值被稀释。

解决:用学校校历生成学期日期表,或者按教学周过滤,只保留在校时间的数据。简单做法是先看 date 分布:统计每天的活跃学生数,寒暑假期间活跃人数会断崖式下跌,找到下降点后直接截断数据范围。这个问题不做,聚类结果里“节俭型”学生的数量会严重虚高,甚至产生一个无意义的“空壳簇”。

5.6 Linux服务器上画图全是方块

现象:代码在 Windows 上跑得好好的,部署到 Linux 服务器执行,所有图的中文都变成一个个方框。

原因:matplotlib 在 Linux 上找不到 SimHei、Microsoft YaHei 这些 Windows 字体,回退到默认字体,而默认字体不含中文字形。

解决:不要在绘图代码里硬编码字体名,先把 font_manager 的字体列表打出来,看服务器实际装了哪些中文字体。没有就安装中文字体包,或者用上面 4.2 节的多字体回退方案。如果服务器不能装字体,最后手段是图里用英文标签,图外另附中文对照表,虽然丑但不影响结论传达。

6. 验证方法与可解释落地:让分析结果经得起业务追问

6.1 先过三张图的肉眼验收

聚类跑完先别急着写结论。打开三张图:簇中心画像条形图、消费时段分布图、按簇分组的日均消费箱线图。对着图问三个问题:每个簇的特征是否符合业务直觉,时段图是否和食堂开餐时间对应,箱线图里不同簇的中位数是否有明显分层。如果某一条答案是否定的,先回头查数据清洗,再查特征构造,最后才是聚类参数。数据问题比模型问题常见得多。

6.2 一个可复用的统计验证套路

每次聚类分组定下来后,我都会对关键特征跑一遍组间差异检验。比如“食堂主力型”和“夜猫外食型”的日均消费差异、canteen_ratio 差异:

from scipy.stats import ttest_ind a = person[person["cluster"] == 1]["canteen_ratio"] b = person[person["cluster"] == 2]["canteen_ratio"] t_stat, p_value = ttest_ind(a, b, equal_var=False) print(f"p={p_value:.4f}")

p 值小于 0.05 时,差异在统计上成立;p 值大于 0.05 时,说明这个特征没能有效区分这两个簇,要么换特征,要么考虑合并簇。这一步做扎实了,后面无论写报告还是答辩,都有数字兜底。

6.3 从分析往前走一步:把标签用起来

最后说点经验。这类项目做完容易停在“出了一堆图”,但真正的价值在标签落地:对“夜猫外食型”中消费金额异常高的学生,可以触发消费预警机制;对“规律节俭型”且日均消费长期处于低水平的学生,可以作为奖助学金评定的辅助参考材料,注意是辅助,不是直接认定;食堂管理部门还能用时段分布图优化窗口排班和备餐量。我吃过一次亏:当年把聚类黑盒结果直接交给决策部门,被反问“这个分类依据到底是什么”,一句话噎住。之后我养成了一个习惯,每个簇必须配三样东西再对外发结论:一个业务标签、一个统计检验证据、一张可视化图表。这套源码、数据、结果集的项目从清洗到落地不复杂,真正花时间的是让每个结论都说得清来源。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 9:44:10

自组织映射SOM:无监督聚类与降维的Python实战指南

简介:这份资源提供完整的自组织映射(SOM)算法Python实现,面向机器学习初学者与需要聚类、降维及可视化实践的开发者。SOM作为经典无监督神经网络,可将高维数据映射到二维网格并保持拓扑结构,代码覆盖网络初…

作者头像 李华
网站建设 2026/10/12 9:42:11

Ridge-RF-LSBoost三模型融合:MATLAB时间序列预测实战

简介:一套基于MATLAB R2025b的Ridge-RF-LSBoost组合模型项目文档,面向具备MATLAB与机器学习基础、从事数据分析、智能预测或工程建模的研发人员、高校师生与技术工程师。内容聚焦电力负荷、可再生能源出力、金融时序、设备状态监测等复杂时间序列预测任务…

作者头像 李华
网站建设 2026/10/12 9:41:26

ChatGPT虚拟角色对话生成:从Prompt设计到剧情评估的实战指南

简介:这份研究文档系统探讨ChatGPT大语言模型在虚拟角色对话生成与情节开发中的实际应用与评估方法,面向游戏策划、虚拟现实开发者、影视编剧及对AI叙事感兴趣的入门与进阶读者。文档从ChatGPT的技术原理入手,解析其基于大规模对话数据预训练…

作者头像 李华
网站建设 2026/10/12 9:41:08

porn-image-detect实战指南:语义边界、轻量化部署与多模态证据链

简介:这是一份轻量级色情图片检测工具,面向Python初学者与图像处理入门开发者,提供基于色彩空间转换的简易实现方案,适用于内容安全审核、教学演示或本地化图像过滤等低复杂度场景。资源包共7个文件,含2个核心Python脚…

作者头像 李华
网站建设 2026/10/12 9:40:18

OpenHarmony从零开发记事本:ArkTS状态管理与数据持久化实战

最近花了一个周末,在 OpenHarmony 上从零写了一个记事本 APP。开始之前,我以为这只是个“列表 输入框 本地存储”的小练习,真动手以后才发现,光是把 ArkTS 的状态管理、Stage 模型的生命周期和数据持久化串起来,就够…

作者头像 李华