ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python学生校园消费行为分析:从流水预处理到KMeans聚类结果集

Python学生校园消费行为分析:从流水预处理到KMeans聚类结果集 简介面向Python课程设计与期末大作业的校园消费行为分析项目提供完整可运行的源码、真实校园消费数据集及分析结果。项目包含数据预处理、特征分析、模型构建与结果解释等模块并配套基于DFM模型的学生消费行为分析文档适合计算机相关专业学生作为高分课设参考或Python数据分析实战练习。压缩包共8个文件以3个Python脚本为核心覆盖初始化、建模与分析流程辅以数据集压缩包、依赖清单、项目说明、分析文档及配置文件整体大小10.07MB目录结构清晰便于直接运行与二次修改。已有123人学习下载所有源码均经过本地编译调试评审分达98分可帮助学习者快速跑通校园消费行为分析全流程减少环境配置与排错成本也能为课程设计答辩提供完整思路与可视化结果支撑。1. 学生校园消费行为分析先把流水定义清楚再谈结果集校园一卡通流水经常欺骗人。拿到几万行消费记录第一反应往往是直接做聚类看画像但真正把基于Python的学生校园消费行为分析跑出可用结果的人都知道成败不在算法而在源头字段。这套分析闭环通常由三部分组成可回放的数据预处理源码、干净且带业务口径的校园消费数据、以及最终导出的带标签结果集三者缺一不可。本文按一个可落地的实现路径展开——先定字段和校验规则再讲特征工程最后落到KMeans聚类与结果集导出。适合两类读者处理一卡通平台数据的校园信息化工程师以及想用真实场景练手的数据分析从业者。需要提醒的是这个场景最大的难点是隐性数据污染充值退款混在消费里、商户名称不统一、时间跨学期漂移每一条都会让聚类结果失真。2. 流水字段设计把学生消费分析的数据源先定死2.1 一张流水表里必须保住的六个字段做学生校园消费行为分析时我一般不会直接接管一卡通原库而是先导出一张宽表。这张表不需要复杂血缘六个字段就够支撑后续全部特征和结果集流水号、学号、交易时间、商户编码、商户名称、扣款金额。字段太杂反而容易把脏数据带进来。字段名类型业务含义最容易踩的坑flow_idstring一卡通流水唯一标识重复流水导致消费频次虚高student_idstring学号或脱敏后的学生ID用数值类型读入会丢失前导零trans_timedatetime交易发生的本地时间时区不统一导致时段特征偏移merchant_idstring商户编码常含校区号编码断码或编码规则中途变更merchant_namestring商户显示名称同一食堂有多个别名无法直接聚合amountfloat实际扣款金额单位元充值与退款是负数不能算消费这里有一条容易被忽略的口径学生校园消费行为分析只统计「扣款」不统计充值、补贴和退款。很多原始流水里充值记录和消费记录在同一个科目下金额有正有负如果不过滤日均消费和月均消费都会被严重拉偏。2.2 数据一致性校验先杀掉三类「假消费记录」数据不一致是消费分析里最隐蔽的问题。上游结算系统可能延迟一天才把流水推入主库部分自助终端的时间没有做NTP同步还有个别网点会把补扣款以冲正形式写入。所以拿到数据后我不会直接进入特征工程而是先跑一段审计脚本import pandas as pd def audit_flow(df: pd.DataFrame) - pd.DataFrame: # 时间字段必须统一转为 pandas 时间戳否则无法做滑窗和跨天判断 df[ts] pd.to_datetime(df[ts], errorscoerce, utcTrue) # 1) 唯一性校验流水号重复会让同一笔消费被统计两次 dup_mask df.duplicated(subset[flow_id], keepFalse) print(f重复流水号条数: {int(dup_mask.sum())}) # 2) 金额口径负数通常是退款或补贴不能参与消费聚合 refund df[amount] 0 print(f退款/补贴条数: {int(refund.sum())}) # 3) 时间漂移与当前时间相差超过24小时视为异常前移/后滞数据 drift (df[ts] - pd.Timestamp.now(tzUTC)).abs() pd.Timedelta(24h) print(f时间漂移条数: {int(drift.sum())}) # 保留有效消费流水 return df.loc[~dup_mask ~refund ~drift].copy()这段代码把审计和清洗放在同一函数里执行逻辑上分为三关重复流水、负数金额、时间漂移。errorscoerce的作用是把无法解析的时间字符串转成NaT之后再用notna()过滤而不是让整个脚本在类型转换处报错中断。三个布尔掩码最后合并成一份过滤条件保留干净的消费流水。有一点要特别说明审计出来的异常不要直接丢弃最好单独导出audit_anomaly.csv。数据不一致的原因需要追溯到具体的商户终端和结算批次如果审计后直接扔掉原始记录后续排查时就没有依据了。提示审计的阈值要看业务形态。24小时漂移阈值对延迟入账有效如果上游有批量补录场景建议放宽到7天再用「同 flow_id 是否已经存在」做二次去重。2.3 商户编码与名称的映射让聚合有统一粒度流水表里的商户名称通常是中文别名同一家风味食堂可能有「一食堂风味窗口」「风味餐厅」「一食1F-3号」等多种叫法。直接用商户名做分类会让特征空间爆炸。常见做法是维护一张正则映射表用关键词匹配给每条流水打上商户大类标签CATEGORY_RULES [ (食堂, r(食堂|餐厅|风味|快餐|包子|面馆)), (超市, r(超市|便利店|小卖部|天猫校园)), (洗浴, r(浴室|澡堂|热水房)), (饮水, r(开水|直饮|饮水机)), (打印, r(打印|文印|自助打印机)), (校车, r(校车|班车)), ] def map_merchant(name: str) - str: for category, pattern in CATEGORY_RULES: if re.search(pattern, name): return category return 其他映射表的匹配顺序本身有意义比如「风味餐厅」同时包含「食堂」和「餐厅」两个关键字所以要把更精确的规则放在前面。这样设计之后后面所有的聚合都基于merchant_category这一列不再直接使用原始名称也避免了大语言模型或关键词统计带来的分类抖动。3. 特征工程从流水到学生行为指标的Python实现3.1 类型转换与金额口径预处理里最常见的翻车点预处理的顺序很重要。先处理类型转换再做金额校验最后才做商户映射。顺序反了会在数据治理阶段浪费大量时间。学生学号必须保留为字符串否则student_id: 2021031801会被 Excel 和部分数据库读成浮点数后边 join 特征矩阵时对不上号。def load_flow(path: str) - pd.DataFrame: df pd.read_csv(path, dtype{student_id: str, flow_id: str}) df[amount] pd.to_numeric(df[amount], errorscoerce) df[ts] pd.to_datetime(df[ts], errorscoerce, utcTrue) # 消费金额必须为正数零元流水多数是设备自检产生的哑数据 df df[(df[amount] 0) df[ts].notna()].copy() return dfdtype在读取时指定比读取后再做 python 类型转换更稳妥。pd.to_numeric配合errorscoerce会把脏字符转成NaN便于在下一行统一过滤。零元流水建议保留在审计结果里但不进入特征计算因为自助终端开机自检偶尔会产生交易时间正常、金额为 0 的哑记录。3.2 特征字典把一次消费翻译成行为特征特征工程的目标是把每个人的流水压缩成一行行为画像。核心思路是「先日聚合再跨日汇总」这样能避免单日多笔就餐被放大成高频消费。我常用的学生维度特征如下特征名计算口径业务含义异常示例avg_daily_amount总消费金额 / 实际消费天数日均消费水平刚开学一周金额虚高avg_daily_cnt总消费笔数 / 实际消费天数日均消费频次单日10笔以上可能是替他人刷卡meal_regular三餐时段消费金额占比就餐规律程度全天无规律时段打散market_ratio超市类金额 / 总金额生活物资消费倾向军训期间占比上升night_ratio21点后消费金额占比夜宵/加班学习倾向期末周明显抬升consume_cv每日消费金额的标准差 / 均值消费波动性充值后一次大额集中消费Python 侧实现时可以这样构造特征矩阵import numpy as np def build_student_features(flow: pd.DataFrame) - pd.DataFrame: df flow.copy() df[hour] df[ts].dt.hour df[ts].dt.minute / 60.0 df[date] df[ts].dt.date # 时段切分早餐 6:30-9:30午餐 11:00-14:00晚餐 17:00-21:00 bins [-np.inf, 6.5, 9.5, 11.0, 14.0, 17.0, 21.0, np.inf] labels [pre_dawn, breakfast, brunch, lunch, afternoon, dinner, night] df[period] pd.cut(df[hour], binsbins, labelslabels, rightFalse) # 日粒度聚合同一学生同一天的多笔同类消费先合并 daily df.groupby([student_id, date, category], as_indexFalse).agg( amount(amount, sum), cnt(flow_id, count), ) # 跨日聚合得到学生级特征 student df.groupby(student_id, as_indexFalse).agg( total_amount(amount, sum), total_cnt(flow_id, count), days(date, nunique), ) student[avg_daily_amount] student[total_amount] / student[days] student[avg_daily_cnt] student[total_cnt] / student[days] return student这段代码的关键是pd.cut的区间定义rightFalse表示左闭右开避免 6:30 和 9:30 这类边界时间同时落进两个时段。daily先把同学生同日期同商户类别的多笔消费合并消除窗口排队多次扣款造成的频次噪声。需要注意的是极端大额消费不应该被直接替换掉更好的做法是保留到单独的异常标签列避免特征被单次充值后的大额集中消费污染。3.3 标准化选型KMeans 面前的特征预处理KMeans 基于欧氏距离特征量纲不一致时高量纲变量会主导聚类结果。消费行为数据里金额特征普遍右偏少数学生一个月花三千多数人只花八百直接用 StandardScaler 会被极值拉平中位数附近的差异。更稳的做法是先对偏态特征做对数压缩再标准化或者直接用 RobustScaler。from sklearn.preprocessing import RobustScaler FEAT_COLS [avg_daily_amount, avg_daily_cnt, meal_regular, market_ratio, night_ratio, consume_cv] # log1p 压缩长尾之后用中位数和 IQR 做鲁棒缩放 X np.log1p(student[FEAT_COLS].astype(float)) scaler RobustScaler(quantile_range(25.0, 75.0)) X_scaled scaler.fit_transform(X)np.log1p对 0 值友好不会因为数据里有 0 而报错同时把右偏分布拉回接近对称形态。RobustScaler 用中位数和四分位距做缩放比均值方差对离群值更钝感。这一步做完后续聚类输入的是无量纲的特征矩阵而不是原始金额和笔数混在一起的数据。4. 用 KMeans 聚类生成消费画像并导出结果集4.1 聚类数怎么定肘部法判据与业务解释力的权衡学生校园消费行为分析里聚类数 k 的选择需要同时看统计指标和业务可解释性。轮廓系数衡量样本与自身簇内紧密程度肘部法则看 SSE 拐点。我一般遍历 k2 到 k8同时打印两类指标from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score for k in range(2, 9): model KMeans(n_clustersk, n_init10, random_state42) labels model.fit_predict(X_scaled) sil silhouette_score(X_scaled, labels) print(fk{k}, inertia{model.inertia_:.1f}, silhouette{sil:.4f})n_init10表示每个 k 值跑 10 次随机初始化再取最优避免因单次初始化陷入局部最优。轮廓系数不是越大越好实际项目里 k2 时轮廓系数往往最高但两类画像太粗糙无法解释食堂运营需要的多级差异。根据我的经验校园消费数据里 k3 到 k5 是业务上最好解释的范围。4.2 画像回写与簇命名让业务方读得懂结果集聚类完成后每个簇只是一个数字编号业务方没法直接用。需要给每个簇计算特征中位数并根据中位数表现命名画像。低消费组的命名要中性推荐「节俭型」「低频消费型」避免出现经济状况相关的直接推断。student[cluster] model.labels_ profile student.groupby(cluster)[FEAT_COLS].median().round(3) # 人工赋予可解释画像名称 profile[profile_name] [规律三餐型, 高消费多元型, 节俭低频型, 超市偏好型]这里用中位数而不是均值做画像描述是因为消费数据偏态明显少数高消费学生会把集群均值拉高中位数更接近该簇普通学生的真实水平。命名逻辑可以直接写进导出脚本让最终结果集里每个学生都带有可读的profile_name字段。4.3 结果集的结构设计与文件输出基于Python的学生校园消费行为分析源码通常要产出四类结果集带标签的学生特征矩阵、聚类中心描述、画像分布统计、以及可视化图表。文件结构如下output/ ├── student_features_labeled.csv # 每个学生一行含聚类标签 ├── cluster_profile_summary.csv # 每个簇的特征中位数与画像名 ├── cluster_distribution.json # 各簇人数与占比供前端看板使用 └── cluster_scatter_pca.png # PCA降维后的二维散点图导出代码不能只在控制台打印结果应写成可重复执行的脚本把计算结果落盘import json from sklearn.decomposition import PCA import matplotlib matplotlib.use(Agg) import matplotlib.pyplot as plt # 带标签的学生特征写入 CSVstudent_id 保持字符串类型 student.to_csv(output/student_features_labeled.csv, indexFalse, encodingutf-8-sig) # 聚类分布写成 JSON方便触屏看板和报表系统直接消费 dist student[profile_name].value_counts().to_dict() with open(output/cluster_distribution.json, w, encodingutf-8) as f: json.dump(dist, f, ensure_asciiFalse, indent2) # PCA 降到二维只用于可视化不参与聚类训练 pca PCA(n_components2, random_state42) coords pca.fit_transform(X_scaled) plt.scatter(coords[:, 0], coords[:, 1], cstudent[cluster], s6, cmapviridis) plt.savefig(output/cluster_scatter_pca.png, dpi150, bbox_inchestight)这里有一点必须强调PCA 降维只能用于结果展示不能把 PCA 之后的数据作为 KMeans 的输入。PCA 会改变样本间的距离结构在原始特征空间聚类之后再降维可视化才是正确的顺序。utf-8-sig编码导出的 CSV 在 Excel 中能直接看到中文不会出现乱码这是交付结果集时容易被忽略的细节。结果集文件内容定位主要使用方student_features_labeled.csv每人一行的特征和标签明细数据分析人员cluster_profile_summary.csv各簇中位数、画像名、人数占比食堂运营与校园管理人员cluster_distribution.json画像占比轻量级数据交换前端看板cluster_scatter_pca.png聚类分布的直观散点图汇报PPT5. 结果集验证方法与三类高发坑的绕法5.1 换随机种子再跑一遍聚类稳定性的硬指标KMeans 的结果依赖随机初始化同一个数据集换一个random_state聚类结果可以完全不同。所以结果集交付前需要做稳定性验证多次换随机种子跑聚类用调整兰德指数衡量两次标签的一致程度。平均 ARI 高于 0.8 时聚类结果才具备可交付的可靠性。from sklearn.metrics import adjusted_rand_score ari_scores [] for seed in range(10): model KMeans(n_clusters4, n_init10, random_stateseed) labels_tmp model.fit_predict(X_scaled) ari adjusted_rand_score(labels_ref, labels_tmp) ari_scores.append(ari) print(f平均ARI: {np.mean(ari_scores):.4f}, 最低ARI: {np.min(ari_scores):.4f})如果最低 ARI 低于 0.6说明数据里存在跨簇的临界样本这类样本大概率落在两个簇的交界区域。常见做法是保留原始聚类在结果集里额外增加一列cluster_boundary标记这些置信度不足的样本让业务方在使用时避免对临界样本做硬性判断。5.2 结果集交付的格式坑长学号与 Excel 数据验证限制用 Excel 打开结果集时「学号变科学计数法」是最常见的问题。解决方法是导出前把学号列用astype(str)固定并在 CSV 前加utf-8-sigBOM。已经导出的 CSV 可以在 Excel 里用分列功能强行转回文本但更推荐在 Python 侧一步到位。另一个隐蔽问题是向 Excel 写入时触发「此值与此单元格定义的数据验证限制不匹配」。这是因为很多运营同学会在模板表里预置下拉列表和数据验证规则程序直接往这些单元格写值时Excel 会拦截。绕法有两条要么导出纯 CSV 不经过模板要么用 openpyxl 打开模板后先清除数据验证再写值。from openpyxl import load_workbook wb load_workbook(template.xlsx) ws wb.active # 清除整表的数据验证规则避免程序写入被 Excel 拦截 ws.data_validations.dataValidation [] wb.save(output_labeled.xlsx)5.3 跨学期数据周期与冷启动处理校园消费数据天然分学期9 月开学季、期末周、寒暑假前后行为差异巨大。把一整年的流水混在一起聚类会把「学期阶段」当成消费画像的主要区分维度。实际处理时我会加一个week_of_term字段先按学期切分再在每个学期内分别做特征和聚类最后用学号把跨学期的画像串起来。冷启动场景里新学期前两周数据量少学生画像不稳定。推荐的做法是先用上一学期的簇中心做预测而不是重新聚类。把新流水映射到旧特征空间后用KMeans.fit_predict之前保存的模型直接标注等第三周数据量充足后再重算并覆盖结果集。这样整套基于Python的学生校园消费行为分析源码就形成了一次可迭代、可回溯的闭环后续只需要把学期起始日期参数化成配置项结果集就能自动跟随数据周期重算。本文还有配套的精品资源点击获取
返回列表