
简介面向校园消费数据分析场景的Python设计与分析项目围绕学生消费记录与个人信息表完成数据清洗、信息关联、食堂就餐情况分析以及学生消费行为分析等完整流程适合数据分析初学者、课程设计或相关课题参考。资源包共30个文件以5个Python脚本为核心配合8个CSV数据表、9张结果图片、1份Word分析报告及工程配置文件压缩包大小118.44MB。目前已有739人学习浏览内容覆盖缺失值与异常数据清洗、多表关联、早中晚各食堂就餐人数占比、工作日与非工作日就餐时间、不同性别及各专业消费对比等关键方法并附有可视化结果图与分析报告。整体目录结构清晰从原始数据到最终分析结论均有对应脚本和输出便于读者按步骤理解与复用。1. 校园消费行为分析的设计起点从一卡通流水到行为指标在校园信息化场景里学生一卡通每天产生大量交易流水食堂、超市、开水房的记录不断堆积。真正动手分析这类数据时最先遇到的瓶颈往往不是模型不会跑而是设计问题按什么粒度定义消费、用哪些指标衡量行为异动、如何区分“消费高”和“活跃度高”。这个项目的核心是把一卡通流水做清洗、聚合和特征设计再用 Python 完成可视化与聚类分析输出一张可直接解释的学生消费行为表。适合需要做校园数据分析、学工研判或 Python 数据项目练手的开发者阅读用过 pandas 但未系统性做过行为分析的人收获最大。2. 用 Python 搭建校园消费行为数据集清洗、聚合与特征设计一卡通原始流水通常以 CSV 格式从卡务系统导出常见字段包括学号、交易时间、商户名称、交易类型、交易金额、卡内余额。字段口径在不同学校之间差异不小有的系统把退款记为负数有的把补助金记为充值。设计分析的第一步是把这些原始字段转成稳定、可复用的行为变量。2.1 定义数据字典与初始加载方式先确认数据字典再写代码顺序不能反过来。常见做法是向卡务系统要一份字段说明要不到的时候就用df.head(20)抽样打印人工推断字段含义。一个典型的流水样例结构如下字段示例说明student_id2023081101学生学号统一按字符串处理trade_time2024-03-04 11:32:05交易发生时间merchant食堂-第一餐厅消费场所trade_type消费消费/充值/退款/补助amount8.50交易金额正数表示进账balance12.30交易后卡内余额加载代码import pandas as pd df pd.read_csv( card_trade.csv, parse_dates[trade_time], encodingutf-8 ) df[student_id] df[student_id].astype(str) print(df.info())逻辑说明parse_dates让trade_time直接变成 datetime 类型后续提取小时、星期都不用手动转换student_id转成字符串是因为学号不参与数值运算且部分学号以 0 开头按数值读入会丢失前导零。encoding参数用来规避 Windows 下从 Excel 导数据常见的中文乱码如果报UnicodeDecodeError把utf-8改成gbk再试。读取时建议用usecols只选需要的列例如usecols[student_id, trade_time, merchant, amount]可以把几万行的读入时间缩短到秒级。balance这个字段如果没有做余额序列分析的需求可以先不加载减少内存占用。2.2 时间字段与金额字段的清洗规则清洗集中在时间和金额两条线上。金额字段的常见异常包括退款产生的负数、单笔金额过高、以及充值与消费记录混在同一张表里时间字段则需要从trade_time中拆出hour、weekday、month供后续的时段分析和周规律分析使用。我一般先按trade_type过滤消费记录再对金额做区间限制consum df[df[trade_type] 消费].copy() consum consum[consum[amount] 0] consum consum[(consum[amount] 0.1) (consum[amount] 50)] consum[hour] consum[trade_time].dt.hour consum[weekday] consum[trade_time].dt.dayofweek consum[month] consum[trade_time].dt.month逻辑说明amount 0排除退款和金额异常为负的记录0.1元的下限用来剔除系统产生的 0 元流水50元上限是经验值覆盖绝大多数食堂档口和超市订单。如果分析对象包含健身房或培训缴费这类大额消费应单独建表而不是与日常餐饮混在一起。hour、weekday、month三列生成之后后面的聚合和可视化都直接引用这三列无需反复解析时间。这里有个细节过滤条件建议一次性写成组合布尔数组避免在多步链式赋值时触发SettingWithCopyWarning。上面的写法里copy()隔离了df与consum后续新增列不会影响原始加载帧也不会出现赋值不生效的隐性问题。如果原始数据覆盖了寒暑假建议先把假期记录剔掉。常见做法是把固定的日期区间维护成一个节假日配置表例如寒假 1 月 15 日到 2 月 20 日、暑假 6 月 25 日到 8 月 30 日每年单独调整否则假期内的极低消费会把整体日均拉低直接影响后续聚类特征。2.3 面向行为分析的特征设计聚合窗口与口径特征设计的核心是决定“以谁为粒度、以什么窗口聚合”。通常以学生为粒度把每个学生的消费行为压缩成一行特征记录。特征分成三类总体消费水平、时段偏好、场所偏好。总消费金额一样的学生可能是每天规律吃饭的“稳定型”也可能是集中几天高额消费的“波动型”只有同时保留金额与活跃天数才能区分这两种情况。daily consum.groupby([student_id]).agg( total_amount(amount, sum), total_count(amount, count), avg_amount(amount, mean), ) daily[days_active] consum.groupby(student_id)[trade_time].nunique() daily[avg_daily] daily[total_amount] / daily[days_active] df_breakfast consum[consum[hour].between(6, 9)].groupby(student_id).size() daily[breakfast_ratio] daily.index.map(df_breakfast).fillna(0) / daily[total_count]逻辑说明days_active是活跃天数来自trade_time的nunique避免把同一时间点的两条记录重复计算avg_daily用总金额除以活跃天数比直接除以 30 天更能反映在校期间的日均水平。breakfast_ratio用 6 点到 9 点之间的消费次数除以总消费次数衡量早餐习惯。早餐窗口可以按学校作息调整为 7 点到 9 点参数影响的是特征的业务含义需要与使用方对齐后再固定不要中途更换。注意聚合窗口的口径要在项目开始时写进数据字典。比如“日均消费”是除以自然天还是活跃天数不同口径算出的排名可能相差 20% 以上建模之前不要再切换。3. 校园消费行为的可视化探索消费时段与金额分布的 Python 分析数据清洗完成之后建议先把关键维度可视化再进入建模环节。可视化的价值不只是出图而是快速检验前面设计的指标是否符合校园生活的实际规律比如饭点高峰是否存在、周末消费是否出现时段后移、各年级之间是否有肉眼可见的差异。3.1 用热力图观察一周消费时段分布把交易记录按“小时”和“星期”交叉聚合生成一张消费量热力图。这张图能同时回答三个问题一天内的消费高峰在哪里、工作日和周末的形态差异有多大、是否存在夜间消费人群。环境里如果还没装 seaborn先执行pip install seaborn matplotlib一行即可。import seaborn as sns import matplotlib.pyplot as plt heat_data consum.pivot_table( indexhour, columnsweekday, valuesamount, aggfunccount, fill_value0, ) heat_data heat_data.reindex(range(24), fill_value0) sns.heatmap(heat_data, cmapYlOrRd, annotFalse, linewidths0.5) plt.yticks(range(0, 24), [f{h}:00 for h in range(0, 24)]) plt.show()逻辑说明index为hour表示每一行对应一天中的某个小时columns为weekday表示周一到周日values用amount但aggfunc设为count实际统计的是交易笔数。fill_value0让没有交易的格子显示为 0 而不是 NaN。reindex(range(24), fill_value0)这一步不能省因为原始数据里某些凌晨时段可能一行记录都没有不补行的话热力图会缺行图像结构直接错位。annotFalse适合全量数据交易笔数上万时格子里写数字会糊成一片如果只统计单个餐厅几百条记录可以改成annotTrue并设置fmtd显示整数。cmap选择YlOrRd是因为深色代表高交易量颜色语义直观。看图的重点是找“深色块”正常校园数据会在 7 到 8 点、11 到 13 点、17 到 19 点出现三条纵向色带周末这三条带会整体后移一小时以上这是判断校园作息节奏最直接的证据。3.2 金额分布与箱线图定位高消费与离群区间单笔消费金额分布能反映食堂起步价和校园物价水平。食堂订单通常集中在几元到十几元开水房订单只有几角钱两种场所混在一起时不管直方图还是箱线图都需要限制坐标范围才能看清主体分布。plt.figure(figsize(10, 4)) plt.subplot(1, 2, 1) consum[amount].hist(bins30, range(0, 50), edgecolorwhite) plt.xlabel(amount/yuan) plt.subplot(1, 2, 2) sns.boxplot(dataconsum, xamount, showfliersFalse) plt.xlim(0, 30) plt.show()逻辑说明直方图的bins30与range(0, 50)把 0 到 50 元切成 30 个桶既能看到主峰位置也能看出长尾不设置range的话个别大额记录会把横轴拉伸到几百元主体分布会被压成一条线。箱线图单独使用时会画出大量离群点showfliersFalse先隐藏它们再通过xlim(0, 30)聚焦分布主体。注意箱线图隐藏离群点只是为了看清 25% 分位到 75% 分位的盒子不代表数据被删除。落在 50 元以上的离群记录要回到原始数据逐条核对通常需要确认是不是多人拼单、自助餐计费或数据录入错误。如果把 50 元上限临时放宽到 200 元能看到一条很稀疏的长尾。真正的离群区间要交给业务方确认例如某月出现连续多笔 80 元的消费学生处会反馈是否来自校外机构在食堂包场单纯从统计上把这些记录删掉属于数据分析里常见的“技术正确、业务失真”。3.3 不同学生群体的消费画像对比特征表daily与学工系统的基础信息表关联后可以做最基础的群体对比。学生信息表通常包含student_id、grade、college等字段关联前先对student_id去重防止一个学生多行导致人数虚高。student_info pd.read_csv(student_info.csv, dtype{student_id: str}) profile daily.reset_index().merge( student_info[[student_id, grade]], onstudent_id, howleft ) grade_group profile.groupby(grade).agg( avg_daily_mean(avg_daily, mean), breakfast_ratio_mean(breakfast_ratio, mean), student_count(student_id, count), ) print(grade_group.round(3))逻辑说明reset_index先把daily的索引student_id还原成普通列再与student_info按学号左连接这样没有基础信息的学生也能保留在结果里。groupby(grade)后分别取avg_daily与breakfast_ratio的均值得到每个年级的平均日消费和早餐比例student_count用于确认每个年级的样本量避免个别只有几个人的年级干扰判断。一次典型的输出长这样年级人数日均消费均值(元)早餐比例均值2021级15618.350.192022级14921.420.242023级16122.800.27如果 2021 级人数明显偏少不能直接下结论说高年级消费下降而要检查是不是毕业年级离校导致活跃天数偏少。把grade字段换成college或宿舍区同样的代码可以快速产出不同维度的对比表适合做成固定周报。4. 校园消费行为的群体分析Python 下的 RFM 设计与 KMeans 聚类可视化确认整体规律后进入项目核心环节群体分析。这里的思路是先通过 RFM 框架把多维行为压缩成三个核心指标再做聚类让数据自己说出存在哪几类消费人群而不是预设标签。4.1 校园场景下的 RFM 指标设计RFM 是零售分析里的经典框架R 表示最近一次消费时间F 表示消费频率M 表示消费金额。校园场景不能直接照搬电商口径因为校内消费是刚需高频行为最近一次消费距离都集中在几天内直接用原始值区分度很差。指标电商口径校园消费口径R最近一次购买距今的天数最近一次校内消费距今的天数F购买次数按订单30 天内的交易次数M购买总金额30 天内的消费总金额计算时先确定观察窗口一般取数据截至日往前推 30 天end_date consum[trade_time].max() rdf consum.groupby(student_id)[trade_time].max().reset_index() rdf[recency] (end_date - rdf[trade_time]).dt.days window_start end_date - pd.Timedelta(days30) fdf ( consum[consum[trade_time] window_start] .groupby(student_id)[amount] .count() .rename(frequency) .reset_index() ) mdf ( consum[consum[trade_time] window_start] .groupby(student_id)[amount] .sum() .rename(monetary) .reset_index() )逻辑说明end_date用全量数据的最大时间戳保证所有学生的 R 值都是相对同一个截止点计算的。recency是最后一次消费距end_date的天数数值越小代表越活跃。F 和 M 都限制在最近 30 天窗口内是为了让不同入学时间、不同离校状态的学生处于可比状态。30 天窗口不是唯一选择。按自然月滚动计算适合做月度报表学期内窗口能覆盖整学期的消费积累。窗口越长高频学生的 F 值方差越大窗口越短对节假日越敏感。刚开始跑通流程时先用 30 天后续再和业务方确定固定的“月度窗口”定义。4.2 数据标准化与 KMeans 聚类的参数选择把三个指标合并成 RFM 表后不能直接喂给 KMeans。monetary的数值范围可能是recency的几十倍距离计算会被金额主导最常用的处理是用StandardScaler做 z-score 标准化。from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler rfm rdf.merge(fdf, onstudent_id).merge(mdf, onstudent_id) features rfm[[recency, frequency, monetary]].copy() scaler StandardScaler() X scaler.fit_transform(features) for k in range(2, 7): model KMeans(n_clustersk, random_state42, n_init10) model.fit(X) print(k, round(model.inertia_, 2)) best_k 3 model KMeans(n_clustersbest_k, random_state42, n_init10) model.fit(X)逻辑说明StandardScaler把每个特征变换成均值 0、方差 1 的分布消除量纲差异。循环从k2试到k6并打印每个 k 对应的inertia也就是样本到所属簇中心距离的平方和。k 增大时inertia必然下降下降幅度明显变缓的位置就是候选 k例如 2 到 3 降了 30%3 到 4 只降 5%那么best_k3是合适取值。random_state42固定随机种子保证可复现n_init10让算法用 10 组不同初始中心各跑一次取最优。如果样本量超过五万可以把n_init降到 4 或 5 换取时间。聚类完成后不要只看inertia还可以配合silhouette_score辅助验证但校园项目里最重要的标准是簇画像能不能在业务上解释清楚。4.3 聚类结果的可解释性画像输出与交叉验证聚类完成后把每个簇的特征均值还原成业务语言。最直接的方式是输出分簇统计表再算每个簇的人数占比。rfm[cluster] model.labels_ cluster_profile rfm.groupby(cluster).agg( avg_recency(recency, mean), avg_frequency(frequency, mean), avg_monetary(monetary, mean), size(student_id, count), ) cluster_profile[share] ( cluster_profile[size] / cluster_profile[size].sum() ) print(cluster_profile.round(2))逻辑说明model.labels_是每个学生对应的簇编号赋值给rfm后就能按簇聚合。avg_recency小、avg_frequency和avg_monetary高的一类可命名为“高频高消费”avg_recency大、avg_frequency低的类别属于“活跃度下降型”。每个簇的命名不需要自动生成靠人根据均值特征来归纳结论更可信。仅看均值不够建议补充打印每个簇在三个特征上的最小值与最大值避免个别极端学生把簇均值拉偏。人数占比上如果某个簇占比低于 5%就要考虑它到底是独立群体还是离群记录聚成的噪声簇噪声簇不参与后续名单输出直接标记为“样本量过小暂不研判”。5. 进阶异动识别与名单输出的 Python 落地技巧建模之后项目真正交付的不只是数字而是能进入工作流的成果。这章把聚类结果落到两个具体动作上识别消费异动学生、输出可读名单。5.1 用双窗口对比计算消费异动因子相比直接对比两个时间段的总额用日均更公平。这里取最近 7 天作为观察窗口往前推 23 天作为基线窗口两个窗口长度不一致因此先转换成日均再计算变化比例。end_date consum[trade_time].max() recent_mask consum[trade_time] end_date - pd.Timedelta(days7) baseline_mask ( (consum[trade_time] end_date - pd.Timedelta(days30)) (consum[trade_time] end_date - pd.Timedelta(days7)) ) recent_daily ( consum[recent_mask].groupby(student_id)[amount].sum() / 7 ).rename(recent_daily) baseline_daily ( consum[baseline_mask].groupby(student_id)[amount].sum() / 23 ).rename(baseline_daily) anomaly recent_daily.to_frame().join(baseline_daily, howleft) anomaly[change_ratio] anomaly[recent_daily] / anomaly[baseline_daily] anomaly anomaly[anomaly[change_ratio] 1.5].dropna()逻辑说明recent_daily是最近 7 天的日均消费baseline_daily是此前 23 天的日均消费change_ratio大于 1.5 表示近一周消费水平比基线高出 50% 以上。阈值 1.5 是可调参数学工场景下先按 top 5% 学生数反推阈值比硬编码合理。dropna剔除基线窗口无消费记录的样本这类样本无法计算变化比例需要单独归入“新入校”名单。如果某个学生 7 天内完全没有消费记录也要单独输出一个列表这通常是离校、请假或长期未使用校园卡的信号与“消费上升”是两类完全不同的关注方向。5.2 名单输出与隐私处理的细节最后把聚类标签和异动因子合并到一张表输出给使用方。使用方只应该看到学号、类别标签和异动方向不建议在 CSV 里附带消费金额明细。cluster_name {0: 稳定型, 1: 波动型, 2: 高活跃型} report rfm[[student_id, cluster]].merge( anomaly[[change_ratio]], onstudent_id, howleft ) report[cluster_label] report[cluster].map(cluster_name) report[change_ratio] report[change_ratio].fillna(0).round(2) report.to_csv(consumption_analysis_result.csv, indexFalse, encodingutf-8-sig)输出文件的核心字段如下字段说明student_id学生学号cluster_label群体标签如“稳定型”“波动型”change_ratio近 7 天日均相对基线的变化倍数逻辑说明encodingutf-8-sig会让生成的 CSV 在 Excel 中直接打开不乱码这是 Python 写中文数据文件时最常被忽略的参数。cluster_name由 4.3 的簇画像人工映射这里假设聚类数为 3字段只保留标签不保留消费明细降低数据泄露风险。提示名单使用方如果需要核查明细应走原有学工系统查询流程不要在 CSV 里附带卡内余额、具体商户等敏感字段。拿到异动名单后按学院拆分再和课程表、节假日等日历数据做交叉比对就能区分出正常波动还是需要关注的情况。阈值和窗口参数直接放在脚本顶部换学期时只需调整两个数字即可重新出表。本文还有配套的精品资源点击获取