基于Python的学生校园消费行为分析:从数据清洗到K-Means聚类建模全流程
简介这份资源是面向高校学生的Python数据分析期末大作业完整项目包主题为学生校园消费行为分析适合正在准备课程设计、建模作业或需要数据分析实战案例的初学者与进阶学习者。压缩包共6个文件以5个Python源码脚本为主另含1个数据压缩包整体约4.89MB脚本按任务模块拆分便于理解建模流程与代码组织方式。项目围绕校园消费数据展开涵盖数据读取、清洗、统计与可视化等环节可直接运行、无需修改能帮助读者快速搭建分析框架、对照复现结果并迁移到类似消费行为课题。目前已有618人学习下载可作为期末大作业的参考模板也可用于练习Python数据处理与建模思路节省从零搭建的时间成本。1. 从一份期末大作业说起学生消费数据里到底能挖出什么每年期末总有一批同学被“基于Python的学生校园消费行为分析”这个大作业卡住。题目看起来不难——不就是拿一卡通流水跑几个图吗但真动手才发现数据字段看不懂、消费类别对不上、时间戳格式五花八门更别提还要“建模”。我带过几届本科毕设也帮研究生调过数学建模竞赛的代码最常见的翻车现场不是算法写错而是数据清洗阶段就崩了。这篇笔记就按一线实操的路径把从原始流水到可解释模型的全流程拆开讲清楚。适合两类人一是正在做期末大作业、需要一份能跑通的代码框架的同学二是想用校园消费数据做用户画像、异常检测的初级数据分析师。读完你能拿到一套可复现的清洗脚本、三个必调的建模参数以及一份避坑清单。2. 数据到手先别急着画图字段清洗与消费类别重构2.1 校园一卡通流水的典型字段与脏数据形态常见的一卡通导出数据一般包含这些列学号、姓名、交易时间、交易金额、交易类型、商户名称、终端号。不同学校导出格式有差异但脏数据套路高度一致。我见过最离谱的一份数据交易时间列里混着2024/3/5 12:30、2024-03-05 12:30:00和05-03-2024 12:30三种格式直接pd.to_datetime会报错。金额列里还有-和空字符串商户名称里同一个食堂写成“一食堂”“第一食堂”“一餐”三种。这些不处理后面按商户聚合全是坑。清洗的第一步不是写代码是先做字段审计。用下面这段脚本快速摸清每列的缺失率、唯一值和样本import pandas as pd df pd.read_csv(campus_card.csv, encodinggbk) # 字段审计缺失率、类型、唯一值数量 audit pd.DataFrame({ dtype: df.dtypes, missing_rate: df.isnull().mean().round(4), nunique: df.nunique(), sample: [df[c].dropna().iloc[0] if df[c].notna().any() else None for c in df.columns] }) print(audit)逻辑说明encodinggbk是因为很多学校导出的是 GBK 编码用 UTF-8 读会乱码。missing_rate超过 0.3 的列基本可以放弃nunique等于 1 的列没有分析价值。参数上如果交易时间列缺失率超过 5%建议直接找数据源补导不要强行填充。2.2 消费类别重构从商户名称到可分析标签原始商户名称太细直接聚合会得到几百个类别没法建模。我一般会做两级映射先按关键词粗分再人工校验。比如包含“食堂”“餐厅”“餐饮”的归为“餐饮”包含“超市”“便利店”的归为“购物”包含“洗澡”“热水”的归为“生活服务”。下面是一个可复用的映射函数import re def map_category(merchant): if pd.isna(merchant): return 未知 merchant str(merchant) rules [ (r食堂|餐厅|餐饮|面馆|快餐, 餐饮), (r超市|便利店|小卖部|水果, 购物), (r洗澡|热水|洗衣|饮水, 生活服务), (r图书馆|打印|复印, 学习支出), (r医院|药房|医务, 医疗), ] for pattern, label in rules: if re.search(pattern, merchant): return label return 其他 df[消费类别] df[商户名称].apply(map_category) print(df[消费类别].value_counts())逻辑说明re.search比str.contains更灵活能处理部分匹配。参数上rules列表的顺序很重要——先匹配“食堂”再匹配“超市”避免“食堂超市”被误分。跑完后一定要value_counts()看分布如果“其他”占比超过 20%说明规则覆盖不够需要补充关键词。这一步的产出是后续所有分析的基础类别错了后面建模全是玄学。2.3 时间字段解析与异常交易剔除时间字段处理有两个目标统一格式、提取可分析维度小时、星期、是否节假日。统一格式用pd.to_datetime配合errorscoerce把解析失败的置为 NaT然后统计失败率。失败率超过 1% 就要回头检查原始数据。异常交易剔除主要看金额单笔超过 500 元的餐饮消费、负数金额、金额为 0 的记录大概率是系统冲正或测试数据直接删掉。df[交易时间] pd.to_datetime(df[交易时间], errorscoerce) print(f时间解析失败率: {df[交易时间].isna().mean():.2%}) # 剔除异常金额 df df[(df[交易金额] 0) (df[交易金额] 500)] df[小时] df[交易时间].dt.hour df[星期] df[交易时间].dt.dayofweek df[是否周末] df[星期].isin([5, 6]).astype(int)逻辑说明errorscoerce把无法解析的值变成 NaT方便统计失败率。金额上限设 500 元是经验值大部分校园单笔消费不会超过这个数超过的可能是学费代扣或设备采购不属于日常消费行为。dt.dayofweek返回 0-65 和 6 是周六周日。这一步做完数据才真正可用。3. 行为特征工程把流水变成可建模的样本3.1 按学生聚合的核心特征设计原始流水是“交易级”数据建模需要“学生级”样本。我一般会构造这几类特征消费频次总交易次数、日均次数、消费金额总金额、日均金额、单笔均值、消费结构各类别占比、时间偏好早餐次数、夜宵次数、周末消费占比。这些特征能覆盖大部分行为分析场景。下面是一个聚合脚本# 按学号聚合 stu df.groupby(学号).agg( 总消费次数(交易金额, count), 总消费金额(交易金额, sum), 单笔均值(交易金额, mean), 单笔最大值(交易金额, max), 消费天数(交易时间, lambda x: x.dt.date.nunique()) ).reset_index() # 类别占比 cat_pivot df.pivot_table(index学号, columns消费类别, values交易金额, aggfuncsum, fill_value0) cat_ratio cat_pivot.div(cat_pivot.sum(axis1), axis0).add_prefix(占比_) stu stu.merge(cat_ratio, on学号, howleft) # 时间偏好 stu[早餐次数] df[df[小时].between(6, 9)].groupby(学号).size().reindex(stu[学号]).fillna(0).values stu[夜宵次数] df[df[小时].between(21, 23)].groupby(学号).size().reindex(stu[学号]).fillna(0).values逻辑说明消费天数用nunique统计去重日期数比直接算时间跨度更准。pivot_table做类别金额透视div算占比。reindex保证没有消费记录的学生补 0。参数上早餐时段设 6-9 点、夜宵设 21-23 点是通用做法如果学校作息特殊可以调整。这一步产出的stu表就是建模的输入。3.2 消费规律特征间隔、波动与集中度除了基础统计量消费行为的“节奏”也很重要。我常用三个特征消费间隔的变异系数衡量规律性、日消费金额的标准差衡量波动、恩格尔系数餐饮占比。变异系数越小说明消费越规律标准差越大说明消费越不稳定。这些特征在异常检测和贫困生识别里很管用。import numpy as np # 消费间隔变异系数 def interval_cv(group): times group[交易时间].sort_values() if len(times) 3: return np.nan intervals times.diff().dt.total_seconds().dropna() return intervals.std() / intervals.mean() if intervals.mean() 0 else np.nan stu[消费间隔CV] df.groupby(学号).apply(interval_cv).values # 日消费波动 daily df.groupby([学号, df[交易时间].dt.date])[交易金额].sum().reset_index() stu[日消费标准差] daily.groupby(学号)[交易金额].std().reindex(stu[学号]).values stu[恩格尔系数] stu[占比_餐饮] if 占比_餐饮 in stu.columns else 0逻辑说明interval_cv先按时间排序算相邻交易的时间差再求变异系数。交易少于 3 次的返回 NaN避免除零。日消费标准差先按天聚合再算标准差反映消费的日间波动。恩格尔系数直接用餐饮占比。参数上变异系数超过 2 的学生消费行为高度不规律可能是数据异常或特殊群体建模时要单独看。3.3 特征筛选相关性分析与共线性处理特征不是越多越好。我见过一个同学堆了 40 多个特征结果逻辑回归跑出来系数全乱。一般先用相关系数矩阵看共线性把相关系数超过 0.85 的特征对删掉一个。再用方差膨胀因子VIF做二次确认。下面是一个快速筛选脚本import seaborn as sns import matplotlib.pyplot as plt # 只保留数值列 num_cols stu.select_dtypes(include[np.number]).columns.tolist() num_cols [c for c in num_cols if c ! 学号] # 相关系数矩阵 corr stu[num_cols].corr() plt.figure(figsize(12, 10)) sns.heatmap(corr, cmapcoolwarm, center0, annotFalse) plt.title(特征相关性热力图) plt.show() # 找出高相关对 high_corr [(c1, c2, corr.loc[c1, c2]) for i, c1 in enumerate(num_cols) for c2 in num_cols[i1:] if abs(corr.loc[c1, c2]) 0.85] print(高相关特征对:, high_corr)逻辑说明热力图用来直观判断high_corr列表给出具体的高相关对。参数上阈值 0.85 是经验值可以放宽到 0.9。对于高相关对保留业务含义更清晰的那个。比如“总消费金额”和“日均金额”高度相关如果分析重点是消费水平保留“日均金额”如果重点是总支出保留“总消费金额”。这一步做完特征集基本稳定。4. 建模与验证聚类、分类还是回归4.1 选型理由不同业务目标对应不同模型“学生消费行为分析”这个标题下的建模常见目标有三类一是群体划分聚类把学生分成高消费、低消费、规律型、冲动型二是异常检测分类或孤立森林找出消费异常的学生三是预测回归比如预测下月消费金额。选型取决于你的业务问题。期末大作业一般要求“建模”我建议优先做聚类因为无监督方法不需要标签结果也容易解释。如果数据里有贫困生标签可以做分类但要注意样本不平衡。4.2 K-Means聚类的三个必调参数K-Means是最常用的聚类算法但三个参数不调好结果没法看。第一个是聚类数 K用肘部法或轮廓系数确定第二个是初始化次数n_init默认 10 次建议调到 20 次避免局部最优第三个是标准化消费金额和消费次数量纲差很多必须做 Z-score 标准化。下面是一个完整流程from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score # 选择建模特征 features [总消费金额, 总消费次数, 单笔均值, 消费间隔CV, 恩格尔系数] X stu[features].fillna(0) # 标准化 scaler StandardScaler() X_scaled scaler.fit_transform(X) # 肘部法找 K inertias [] sil_scores [] for k in range(2, 9): km KMeans(n_clustersk, n_init20, random_state42) labels km.fit_predict(X_scaled) inertias.append(km.inertia_) sil_scores.append(silhouette_score(X_scaled, labels)) # 画肘部图和轮廓系数 fig, ax1 plt.subplots() ax1.plot(range(2, 9), inertias, bo-) ax1.set_xlabel(K) ax1.set_ylabel(Inertia) ax2 ax1.twinx() ax2.plot(range(2, 9), sil_scores, ro-) ax2.set_ylabel(Silhouette) plt.title(肘部法与轮廓系数) plt.show()逻辑说明StandardScaler做 Z-score 标准化把每个特征变成均值 0、方差 1。n_init20让算法跑 20 次不同初始化取最优结果。random_state42保证可复现。肘部图看 Inertia 下降变缓的点轮廓系数看峰值。两个指标结合确定 K。参数上如果轮廓系数最高点对应的 K 和肘部点不一致优先选轮廓系数高的因为轮廓系数直接衡量聚类质量。4.3 聚类结果解读与业务标签映射跑完 K-Means 后km.labels_是每个学生的簇标签。但标签是数字没有业务含义。需要看每个簇的特征均值手动映射成“高消费低频”“低消费高频”“规律型”“冲动型”等标签。下面是一个解读脚本# 把标签写回原表 stu[簇] km.labels_ # 看每个簇的特征均值 cluster_profile stu.groupby(簇)[features].mean().round(2) print(cluster_profile) # 看每个簇的人数 print(stu[簇].value_counts()) # 映射业务标签根据实际均值调整 label_map {} for c in cluster_profile.index: row cluster_profile.loc[c] if row[总消费金额] cluster_profile[总消费金额].median(): label_map[c] 高消费 else: label_map[c] 低消费 stu[消费群体] stu[簇].map(label_map)逻辑说明cluster_profile展示每个簇的特征均值是解读的核心依据。label_map根据均值中位数做简单二分实际项目中可以更细。参数上如果某个簇人数少于总人数的 5%可能是异常簇需要检查是否是数据问题。这一步的产出是每个学生的群体标签可以直接用于后续分析或可视化。5. 避坑与排查那些让我熬夜的翻车现场5.1 时间解析失败导致特征全空现象跑完聚合脚本发现早餐次数全是 0但原始数据里明明有早餐交易。原因时间字段解析失败dt.hour返回 NaNbetween(6, 9)全为 False。解决在解析后加一行print(df[交易时间].isna().sum())确认失败数量。如果失败率高用pd.to_datetime(df[交易时间], formatmixed, errorscoerce)让 pandas 自动推断格式或者手动写多个格式依次尝试。5.2 商户名称映射遗漏导致“其他”占比过高现象消费类别分布里“其他”占了 40%但原始商户名称看起来都很正常。原因映射规则的关键词没覆盖全比如“美食广场”“风味餐厅”没被“食堂|餐厅”匹配到。解决先df[商户名称].value_counts().head(50)看高频商户逐个补充关键词。或者用str.contains加caseFalse忽略大小写。我一般会迭代两轮直到“其他”占比降到 10% 以下。5.3 标准化遗漏导致聚类结果被金额主导现象K-Means 跑出来一个簇全是高消费其他簇混在一起轮廓系数很低。原因没做标准化消费金额的方差远大于消费次数距离计算被金额主导。解决建模前必须StandardScaler。检查方法是看X_scaled的均值和方差应该接近 0 和 1。如果某个特征标准化后仍有极端值考虑先做对数变换。5.4 样本不平衡导致分类模型失效现象做贫困生分类正样本只占 3%模型准确率 97% 但召回率为 0。原因样本极度不平衡模型全预测为负类也能拿高准确率。解决用class_weightbalanced让模型关注少数类或者用 SMOTE 过采样。评估指标改用 F1-score 和 AUC不要看准确率。如果正样本太少考虑用异常检测方法孤立森林代替分类。5.5 数据泄露导致验证分数虚高现象交叉验证分数 0.95但测试集只有 0.6。原因特征工程时用了未来信息比如用全量数据算的均值填充缺失值或者聚类时把测试集也放进去了。解决所有统计量均值、标准差只能在训练集上算再应用到测试集。聚类是无监督的可以全量跑但分类任务必须严格划分训练测试集。检查方法是看特征里有没有包含“未来”信息的列。6. 从聚类到预警一个可落地的消费异常检测技巧聚类做完群体标签有了但很多同学还想做“预警”——找出消费突然异常的学生。这里分享一个我在实际项目里用过的技巧基于滑动窗口的消费突变检测。思路很简单对每个学生按周计算消费金额然后用前 4 周的均值和标准差判断第 5 周是否超过均值加 2 倍标准差。超过就标记为异常周。这个方法不需要标签计算量小解释性强。# 按周聚合 df[周] df[交易时间].dt.isocalendar().week weekly df.groupby([学号, 周])[交易金额].sum().reset_index() # 滑动窗口检测 def detect_anomaly(group): group group.sort_values(周) group[前4周均值] group[交易金额].rolling(4, min_periods2).mean().shift(1) group[前4周标准差] group[交易金额].rolling(4, min_periods2).std().shift(1) group[异常] (group[交易金额] group[前4周均值] 2 * group[前4周标准差]).astype(int) return group weekly weekly.groupby(学号).apply(detect_anomaly).reset_index(dropTrue) anomalies weekly[weekly[异常] 1] print(f检测到 {len(anomalies)} 个异常周) print(anomalies[[学号, 周, 交易金额, 前4周均值]].head(10))逻辑说明rolling(4, min_periods2)计算前 4 周的滚动统计量shift(1)确保只用历史数据避免数据泄露。min_periods2表示至少要有 2 周数据才计算否则返回 NaN。异常判断用均值加 2 倍标准差这是经验阈值可以根据实际数据调整到 1.5 或 3。参数上如果某个学生连续 3 周被标记异常建议人工介入查看可能是经济困难或消费习惯突变。这个方法的局限是只能检测“突增”对“突降”需要把判断条件反过来。另外寒暑假期间消费模式变化大建议把假期周排除。我一般会在检测后加一步人工复核看异常周的商户明细确认是真实消费还是数据问题。这套逻辑跑通后可以封装成定时任务每周自动跑一次输出异常名单给辅导员参考。做这类分析最大的教训是不要一上来就追求复杂模型。我见过太多同学花两周调 XGBoost 参数结果数据清洗没做好模型分数全是虚高。先把字段审计、类别映射、时间解析这三步做扎实再跑一个 K-Means 看看群体分布往往就能发现很多有意思的规律。建模是最后一步不是第一步。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →