Python学生校园消费行为分析:从数据清洗到聚类结果集
2026/9/15 4:47:33 网站建设 项目流程

简介:一套基于Python的学生校园消费行为分析完整方案,面向数据分析初学者、高校相关课程设计及“智慧校园”应用研究者。围绕校园一卡通消费记录,涵盖数据清洗、特征工程、食堂消费时段与占比分析、性别差异对比等任务,可复现类似“暖心饭卡”项目的贫困生消费画像思路。压缩包共22个文件,以ipynb分析代码、py脚本为主,辅以结果图表(jpg)、说明文档(docx/md)及原始数据包(zip),整体约19.08MB。代码按任务拆分,便于按步骤学习与二次扩展。已有143人学习浏览。通过源码+数据+结果集的组合,读者可快速跑通完整分析流程,掌握pandas可视化、餐饮消费规律挖掘等实用技能,并直接参考报告撰写范式。

1. 学生校园消费行为分析:从一卡通流水到可复现的结果集

一份校园一卡通流水,少则几万条、多则上百万条,直接扔进Excel只能看到一堆数字,看不出规律。学生校园消费行为分析要解决的核心问题,是从这些流水里提取可解释结论:谁在正常三餐、谁长期不吃早餐、谁消费频次异常、哪些学生需要重点关注。常规落地路径是——用Python读取原始消费数据,清洗掉充值、退款和重复流水,按时间、餐次、商户三个维度构造特征,再用聚类和异常检测生成消费画像,最后导出结构化的结果集。这套分析源码加结果集的组合本身就是可复用的数据产物,换一批流水文件,改改路径和阈值就能重跑。整个过程不依赖大数据组件,pandas 和 scikit-learn 足够覆盖百万行以内的校园场景。

2. 消费行为分析的数据建模:流水字段、脏数据与特征工程

分析开始之前,先把数据模型定清楚。一卡通系统导出的文件通常是一行一条流水,字段各校略有差异,但必含学生标识、交易时间、金额、商户编号。这个阶段的目标不是写算法,而是把原始流水变成每行一个学生的特征宽表,后面聚类和异常检测都在这张表上做。

2.1 一卡通流水的最小字段集与消费口径

建表之前先确认拿到的最小字段集,少了任何一项,后面的特征都算不出来。

字段示例值作用
student_id20230012学生唯一标识,用于分组和后续关联学籍
tx_time2024-11-05 12:03:44交易时间,必须解析成 datetime 类型
amount8.50交易金额,消费为正数
merchant_idC01商户编号,对应食堂窗口、超市、开水房
balance42.10交易后余额,用于一致性校验

tx_time 不转成 datetime,后面按小时划分餐次、按天聚合都会出错,这是新手最容易忽略的一步。balance 字段的价值在一段话里就能说清:同一张卡相邻两笔消费之间,余额应当等于上一笔余额减去当前金额,误差不超过一分钱。对不上的行,要么是系统补录导致顺序错乱,要么是换卡后旧卡数据混入,需要标记出来单独核查。

分析口径上,只保留消费类型。充值、退款、补助发放、挂失补卡都属于总流水,直接进入特征计算会把日均消费拉高,聚类结果完全失真。有的系统没有交易类型字段,就用金额正负和商户编号过滤:正数且金额落在食堂合理区间内的才算消费行。

2.2 时间、餐次、结构三个维度的特征设计

特征设计围绕三个维度展开,每个维度回答一类业务问题,组合起来才能完整描述一个学生的消费画像。

维度代表特征计算口径
时间规律首笔时间、末笔时间、首笔时间标准差每天首末笔消费时刻,跨天求均值与标准差
餐次窗口早餐次数、午餐次数、晚餐次数、夜宵次数按小时区间打标后统计,再按在校天数均值化
消费结构日均金额、单笔均值、商户数、充值笔数金额和商户编号的聚合,必要时关联学籍表

时间维度反映作息规律性,标准差小说明每天吃饭时辰固定;餐次窗口反映三餐覆盖度,连续多天只有午晚两餐,可能是生活习惯,也可能与预算约束有关;消费结构反映消费水平和选择面,单笔均值低但商户数多,通常是有意识控制开销。三个维度合成一张宽表,每行一个学生,这就是后续聚类和异常检测的输入。

选择特征时要注意边界:充值金额不要进特征,它是补给行为,不是消费行为;商户编号只保留聚合后的商户数,不要把每个窗口的商户ID铺开成一堆哑变量,那会让特征维度膨胀到几百列,聚类距离被稀疏维度带偏。

2.3 Python数据清洗与特征聚合的落地代码

import pandas as pd df = pd.read_csv('consume_raw.csv', parse_dates=['tx_time']) df = df[df['tx_type'] == 'consume'] df = df.drop_duplicates(subset=['student_id', 'tx_time', 'amount']) df = df[(df['amount'] > 0) & (df['amount'] <= 200)] df = df[df['tx_time'].dt.hour.between(6, 23)]

第一行把交易时间解析成 datetime;第二行过滤交易类型,只留下消费流水;第三行按学生、时间、金额三列去重,三列完全一样视为同一条流水被记录了两次;第四行剔除金额异常行,上限 200 元按普通食堂消费习惯设定,如果学校有超市档口或水果店,上调到 500 更合理;第五行去掉凌晨 0 点到 6 点的零星流水,这类记录在校园场景里数量极少,却会干扰餐次窗口统计。

提示:amount 下限用大于 0 而不是大于等于 0,防止把退款记录漏进来。上限阈值要结合商户清单复核,一刀切 200 会把水果店的大额订单误删。

df['biz_date'] = (df['tx_time'] - pd.Timedelta(hours=6)).dt.date df['hour'] = df['tx_time'].dt.hour df['meal'] = pd.cut(df['hour'], bins=[5, 9, 13, 17, 20, 24], labels=['breakfast', 'lunch', 'dinner', 'night', 'other']) feature = df.groupby('student_id').agg( total_spend=('amount', 'sum'), tx_count=('amount', 'count'), avg_spend=('amount', 'mean'), merchant_nunique=('merchant_id', 'nunique'), active_days=('biz_date', 'nunique'), ) feature['avg_daily'] = feature['total_spend'] / feature['active_days']

biz_date 把时间后移 6 小时再取日期,凌晨消费归属到前一天,后面聚类就不会出现某学生夜间吃了顿夜宵、导致早餐覆盖天数异常的假象。pd.cut 的边界按校园作息设定:5 到 9 点是早餐,9 到 13 点是午餐,13 到 17 点是下午加餐,17 到 20 点是晚餐,20 点后是夜宵。午餐区间拉到 9 点起算,是为了覆盖上午没课、十一点半才去吃午饭的那批学生。

聚合时 active_days 用的是实际出现消费的天数,而不是日历天数。寒暑假、实习周学生在校天数不同,直接除以总天数会把离校学生误判成低消费群体。avg_daily 才是口径统一的日均消费,后面异常检测和高低消费分组都以它为准。

3. 用Python跑通消费行为分析主流程:聚类、异常检测与结果集输出

特征宽表就绪后进入主流程。环境用 Python 3.8 以上,装 pandas、scikit-learn、matplotlib 三件套就够,不需要引入大数据组件。百万行以内的消费数据,单机内存能轻松装下,KMeans 跑完全量只需秒级;真要到了千万行,才需要考虑按时间切片或者换分布式框架。

3.1 KMeans聚类参数怎么定:n_clusters、n_init与标准化

from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score nums = feature.select_dtypes('number').drop(columns=['cluster']) X = StandardScaler().fit_transform(nums) for k in range(2, 8): km = KMeans(n_clusters=k, n_init=10, random_state=42) labels = km.fit_predict(X) print(k, round(silhouette_score(X, labels), 4))

标准化这一步直接决定聚类结果质量。日均金额是几十的量级,早餐覆盖率是 0 到 1 的小数,商户数是个位数,量纲差几十倍,不缩放的话 KMeans 的欧氏距离完全被金额主导,其他维度等于没参与。n_init=10 表示每个 k 值用 10 组不同的初始中心各跑一遍,取目标函数最小的一次,防止随机初始化把结果带进局部最优。random_state=42 固定随机种子,保证同一份数据两次运行得到完全一致的标签,结果要写进报告或交给别的部门复核时,这一点是硬要求。

轮廓系数取 0 到 1,校园消费场景 0.2 到 0.4 属正常。消费行为本身就是连续渐变,不存在天然清晰的群体边界,硬要追求 0.6 以上,通常说明特征维度太少或样本群体极度割裂,反而要回去检查清洗逻辑。选 k 时除了看分数,还要看每个簇的人数占比,出现一个簇占 80% 以上的情况,说明 k 取小了或者特征区分度不够,要降维到两个主成分画散点图看重叠程度。

3.2 异常消费检测:单维用IQR,组合特征用DBSCAN

q1 = feature['avg_daily'].quantile(0.25) q3 = feature['avg_daily'].quantile(0.75) upper = q3 + 1.5 * (q3 - q1) amount_outliers = feature[feature['avg_daily'] > upper] print(amount_outliers[['avg_daily', 'tx_count']].head())

单维金额异常用 IQR 最直接,超过上界的学生要么消费习惯特殊,要么流水中混入了代刷场景。校园里一个学生帮室友带饭、一周刷掉上千块的情况并不罕见,单看金额只能筛出候选名单,不能直接定性。要判断是不是真的偏离群体,还需要把日均金额、餐次覆盖、商户数组合起来看,这时用 DBSCAN。

DBSCAN 的两个参数要按样本量调:几万学生时 eps 从 0.5 试到 1.5,min_samples 取 10 左右。先看聚类结果里簇的数量,如果只有一个大簇加一堆散点,说明 eps 太小,放宽再跑;如果散点占了三分之一,说明 eps 太大,正常学生都被划成离群点。DBSCAN 不需要预设簇数,正好适合在 KMeans 之外做一个交叉验证:两种算法都标出来的离群学生,优先级最高。

3.3 结果集的字段设计与Excel兼容输出

result = feature.copy() result['cluster'] = labels result['is_amount_outlier'] = result.index.isin(amount_outliers.index) result.to_csv('result_set.csv', encoding='utf-8-sig', index=True)

结果集要让人能复核,不能只给一个 cluster 编号。cluster 后面要跟每个特征的取值,is_amount_outlier 单独成列,这样业务方拿到表,能直接看到"第 3 号聚类的人日均 12 元、早餐覆盖率 40%"这种可解释信息。to_csv 的 encoding 用 utf-8-sig 而不是 utf-8,Excel 直接打开才不会乱码;打开后如果复制粘贴提示与数据验证限制不匹配,通常是目标表设置了数据验证规则,跟导出格式无关,换一列粘贴即可。

提示:常见做法是同时输出三个文件——result_set.csv 给学生管理部门,feature.csv 留作二次建模,cluster_summary.csv 给管理层看群体画像。三个文件的字段口径要保持一致,避免各算各的。

4. 消费数据不一致与结果解读:三个最容易翻车的地方

脚本能跑通只是开始,结果能不能用,取决于数据一致性处理和业务解读。这三个问题几乎在每个校园数据集里都会出现,提前处理能省下大量返工时间。

4.1 数据不一致:重复流水与余额连续性校验

数据不一致最常见的形态是同一笔消费被记了两次。来源有三个:换卡后旧卡流水补录、夜间增量同步任务重跑、人工补录没做幂等。直接按 student_id 去重会把真正的大额消费误删,正确做法是先按"学生+交易时间+金额"找完全重复的行,再对剩余流水做余额连续性校验。

df = df.sort_values(['student_id', 'tx_time']) df['prev_balance'] = df.groupby('student_id')['balance'].shift(1) df['expected_balance'] = df['prev_balance'] - df['amount'] df['balance_diff'] = (df['expected_balance'] - df['balance']).abs() bad_rows = df[df['prev_balance'].notna() & (df['balance_diff'] > 0.01)]

按学生分组排序后,shift(1) 取上一笔余额,期望余额是上笔余额减当前金额。balance_diff 超过 0.01 元,说明中间缺了一笔流水或余额字段没更新。这批行建议标记而不是删除,删掉会进一步破坏余额链条,后面财务对账时也更难定位。处理完的流水再进特征工程,日均金额才有底气。

4.2 餐次窗口边界:凌晨消费归属与教学周口径

餐次划分直接用交易小时判断有个死角:凌晨 0 点到 1 点的消费,日期上属于新的一天,行为上却是前一天晚上的延续。归到当天的早餐明显错误,当作 other 丢弃又会丢夜宵行为。常见做法是把时间整体后移 6 小时,再取日期和小时,凌晨消费自然并入前一个自然日。

shifted = df['tx_time'] - pd.Timedelta(hours=6) df['biz_date'] = shifted.dt.date df['biz_hour'] = shifted.dt.hour

参数说明:偏移 6 小时是把凌晨 0 点到 6 点并入前一日,零售和餐饮行业处理跨天营业的通用口径。另外,寒暑假、考试周、实习周的消费结构差异极大。特征重算前先按 biz_date 排除假期,或者单独出一版只含教学周的结果集,否则长假拉低的均值会掩盖正常学期的消费水平,聚类分组也会偏向假期维度。

4.3 聚类编号不能当标签:cluster画像与层次聚类复核

KMeans 给出的 cluster 编号是算法对特征空间的划分,不是业务标签。编号 0 的学生可能消费中等,换个数据集编号 0 又成了最低群体。解读前必须输出每个簇的特征均值表,把"cluster 0"翻译成"日均消费低、餐次覆盖少"这种业务可读的描述。

cluster人数日均金额早餐覆盖率商户数业务解读
012409.842%3.2规律性偏弱的低消费群体
1273018.688%5.1三餐规律的主流群体
218042.376%8.9高消费且选择面广的少数群体

如果 KMeans 每个 k 的轮廓系数都不理想,或者某个簇人数超过七成失去区分度,可以换层次聚类复核。scipy 的 linkage 函数能输出树状图,直接看分割层数,不需要预设 k。但层次聚类的复杂度是样本数的平方,学生量超过两万时内存和耗时都会明显上升。常见做法是先 KMeans 粗分到 20 个簇,再对这 20 个中心点做层次聚类,既保留树状图的可解释性,又不爆内存。

5. 把消费分析结果集做成可增量更新的数据快照

每周有新流水进来,结果集还要可追溯,这里有一个实用的增量做法。全量重跑在几十万行时还能接受,但特征工程一旦引入滚动窗口,重算时间会非线性上涨。常见做法是"增量清洗 + 窗口重算 + 快照存储"三步。

import json from pathlib import Path state = json.loads(Path('state.json').read_text()) last_ts = pd.Timestamp(state['last_ts']) new_rows = pd.read_csv('consume_daily.csv', parse_dates=['tx_time']) new_rows = new_rows[new_rows['tx_time'] > last_ts] if not new_rows.empty: clean_new = clean_flow(new_rows) full = pd.concat([snapshot, clean_new]).drop_duplicates( subset=['student_id', 'tx_time', 'amount']) feature = build_features(full) result = run_clustering(feature) result.to_csv(f"result_set_{full['biz_date'].max()}.csv", encoding='utf-8-sig') state['last_ts'] = str(new_rows['tx_time'].max()) Path('state.json').write_text(json.dumps(state))

state.json 记录上次导入数据的最大时间戳,作为增量边界。合并到快照后仍要 drop_duplicates,因为增量文件边界处可能与上一批重叠。特征必须基于合并后的全量快照重算,不能只算新增行,餐次覆盖天数和跨天归属都依赖历史窗口。

验证技巧:每两周跑一次全量重算,对比增量版的 cluster 标签一致率,低于 95% 就去查 state.json 的时间戳是否被手工改过。结果集文件名带日期后缀,保留上一版,回溯问题时直接对比两个 CSV 的行数、总金额和每个簇的人数,这三个指标能快速定位是数据缺失还是特征口径变了。增量产生的中间文件按日期归入当日目录,配合每日数据备份,结果集整体就是一份可回滚的分析资产。

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

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

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

立即咨询