ARTICLE DETAIL

资讯详情

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

高校学生消费行为分析:Python时间序列聚类实战

高校学生消费行为分析:Python时间序列聚类实战 简介本资源是一套面向高校学生与数据分析初学者的Python校园消费行为分析实战项目适用于期末大作业、课程设计及毕业设计场景聚焦学生消费偏好、时段规律与食堂就餐结构等实际问题助力掌握数据清洗、统计分析与可视化全流程技能。压缩包共21个文件含7个Jupyter Notebook含task1_1至task3_3等核心分析脚本、7张结果图如食堂早/中/晚餐占比、就餐峰值、性别消费对比等、3个Python工具脚本、1份Word版完整分析报告、1份README说明文档及1个附录ZIP总大小18.93MB结构清晰、注释详尽新手可直接部署运行。已有334人学习下载项目为作者手打高分作品98分获导师高度认可读者可即刻获取从原始数据到多维度图表、聚类分析思路、消费趋势解读及报告撰写的全链路交付物兼具教学性与工程参考价值。1. 为什么学生食堂刷卡记录比成绩单更能暴露真实生活节奏这不是一个「用Python画几个柱状图」的课设作业而是一次对校园消费数据的穿透式解剖从食堂窗口刷卡时间戳、超市小票金额、图书馆门禁进出频次到晚归宿舍楼登记记录——这些散落在后勤、教务、一卡通系统里的碎片拼起来就是学生真实的生活节律、经济能力边界与行为惯性。我去年帮三所高校信息中心做消费行为建模时发现单纯看「月均消费额」会漏掉关键信号比如某学生月均320元但92%集中在周一至周三中午11:45–12:15这半小时刷完再比如另一名学生周末消费占比达78%且单笔超50元的交易全发生在周五晚21:00后。这类模式无法靠Excel求平均值捕捉必须用Python构建时间序列切片聚类异常检测的闭环分析链。本文不讲理论推导只拆解一套可直接部署的最小可行方案含清洗脚本处理缺失值/时间格式错乱/商户编码映射、特征工程模板时段权重、消费熵、跨场景关联度、聚类验证方法轮廓系数业务可解释性双校验以及最关键的——如何把结果集反向喂回宿管系统做精准关怀预警。适合学工处老师、高校信息化工程师、或正在写毕业设计的数据科学方向本科生。2. 用PandasNumPy在本地跑通消费行为分析的最小命令集2.1 数据加载与原始结构校验先看清脏在哪再动手洗校园消费数据通常以CSV或Excel形式交付但实际拿到手往往是「三无状态」无统一时间格式有的存为2023-09-01 08:23:17有的是2023/09/01 8:23还有20230901082317这种纯数字串、无商户标准化编码同一食堂窗口可能被记为A01/食堂一楼东区/一卡通-001、无学生身份唯一标识学号、身份证号、一卡通ID混用。第一步不是写算法而是用5行代码建立数据健康快照import pandas as pd import numpy as np # 加载原始数据假设文件名为 campus_consumption.csv df pd.read_csv(campus_consumption.csv, encodingutf-8) # 输出基础诊断报告 print(f总记录数{len(df)}) print(f字段列表{list(df.columns)}) print(f时间字段示例取前3行\n{df[transaction_time].head(3).tolist()}) print(f商户名称去重数{df[merchant_name].nunique()}样例{df[merchant_name].dropna().unique()[:5]}) print(f学生ID缺失率{df[student_id].isnull().mean():.2%})提示这段代码的价值不在运行结果而在暴露问题类型。比如若merchant_name去重数达127个但实际只有8个食堂档口说明商户命名极度混乱若student_id缺失率超15%则需立即启动ID补全策略如用手机号姓名模糊匹配教务库而非强行删行。2.2 时间字段标准化用正则parse_date双保险解决格式战争校园系统导出的时间字段常有6种以上格式硬编码pd.to_datetime()会报错。我的做法是先用正则提取所有可能的时间片段再用dateutil.parser.parse兜底from dateutil import parser import re def robust_time_parse(time_str): if pd.isna(time_str): return pd.NaT # 预处理移除空格、中文字符、多余符号 clean_str re.sub(r[^\d/:.\-\s], , str(time_str)) # 尝试常见格式按成功率降序 formats [ %Y-%m-%d %H:%M:%S, %Y/%m/%d %H:%M:%S, %Y%m%d%H%M%S, %Y-%m-%d %H:%M, %Y/%m/%d %H:%M ] for fmt in formats: try: return pd.to_datetime(clean_str, formatfmt) except (ValueError, TypeError): continue # 兜底用dateutil自动识别慢但稳 try: return parser.parse(clean_str) except (ValueError, TypeError): return pd.NaT # 应用到数据集 df[parsed_time] df[transaction_time].apply(robust_time_parse) print(f时间解析失败率{(df[parsed_time].isna()).mean():.2%})参数说明re.sub(r[^\d/:.\-\s], , str(time_str))这行是关键预处理它把2023年09月01日 08:23:17变成20230901 08:2317避免parser.parse因中文字符卡死formats列表按实际项目中出现频率排序把%Y-%m-%d %H:%M:%S放首位是因为约63%的系统默认用此格式parser.parse作为最后防线虽慢但能处理Sep 1, 2023 8:23 AM这类英文格式——我们曾遇到国际学院数据混入此类记录。2.3 商户编码映射表用字典正则构建可维护的标准化层面对食堂一楼西区/A03/一卡通-003都指向同一档口的情况硬编码replace()会随新商户上线而失效。我采用「主键字典模糊匹配」双层机制# 定义商户主键映射业务方确认的权威编码 merchant_mapping { 食堂: [食堂, 一卡通-食堂, A0[1-9], 东区食堂, 西区档口], 超市: [超市, 便利, 小卖部, B0[1-9]], 打印店: [打印, 复印, 图文, C0[1-9]], 图书馆: [图书馆, 借阅, 阅览室, D0[1-9]] } # 构建标准化函数 def map_merchant(name): if pd.isna(name): return UNKNOWN name_lower str(name).lower() for standard_name, patterns in merchant_mapping.items(): for pattern in patterns: if isinstance(pattern, str): if pattern.lower() in name_lower: return standard_name else: # 正则表达式 if re.search(pattern, name_lower): return standard_name return OTHER df[merchant_std] df[merchant_name].apply(map_merchant) print(f商户标准化后分布\n{df[merchant_std].value_counts()})逻辑说明字典merchant_mapping由后勤处提供并签字确认每次新增商户只需在此增条目无需改代码A0[1-9]这类正则能同时匹配A01/A07避免逐个写死name_lower统一转小写规避食堂和食堂大小写不一致问题返回UNKNOWN而非np.nan因为后续聚类时缺失类别会导致样本被丢弃而UNKNOWN可作为独立分析维度。3. 从原始流水到行为特征7个必算指标及其业务含义3.1 时段消费强度用滑动窗口量化「生活节律」学生是否早起是否熬夜仅看「最早一笔消费时间」会受偶然因素干扰如某天赶早课买早餐。更可靠的是计算每小时消费频次的滑动窗口均值# 按小时聚合交易次数 df_hourly df.set_index(parsed_time).resample(1H).size().reset_index(namecount) # 计算24小时滑动窗口模拟人体生物钟周期 df_hourly[hour] df_hourly[parsed_time].dt.hour # 按小时分组求均值得到基准强度 baseline df_hourly.groupby(hour)[count].mean() # 计算每个学生的时段强度需先按student_id分组 def calc_hourly_intensity(group): if len(group) 5: # 少于5笔交易的学生不参与强度计算 return pd.Series([0]*24, indexrange(24)) hourly_count group.set_index(parsed_time).resample(1H).size() hourly_count hourly_count.reindex(pd.date_range( starthourly_count.index.min(), endhourly_count.index.max(), freq1H ), fill_value0) # 计算该生每小时强度相对于全校均值的倍数 intensity [] for h in range(24): hour_data hourly_count[hourly_count.index.hour h] if len(hour_data) 0: intensity.append(hour_data.mean() / (baseline[h] 1e-6)) else: intensity.append(0) return pd.Series(intensity, indexrange(24)) # 应用到全量数据 student_intensity df.groupby(student_id).apply(calc_hourly_intensity)业务含义若某学生hour7早7点强度值为3.2表示其早7点消费频次是全校同时间段均值的3.2倍大概率是规律晨读党hour23强度值为0.1说明极少深夜消费配合门禁数据可判断是否按时归寝分母加1e-6防除零这是血泪经验——某次测试因全校凌晨2点无交易导致baseline[2]为0整列计算崩溃。3.2 消费熵值用Shannon熵衡量消费多样性高消费≠高多样性。一个每月花2000元但95%在奶茶店的学生与每月花800元却分散在食堂、超市、打印店、图书馆的学生行为模式截然不同。用Shannon熵量化from scipy.stats import entropy def calc_entropy(group): # 统计该生各商户类型消费次数 merchant_dist group[merchant_std].value_counts(normalizeTrue) # 计算Shannon熵单位比特 ent entropy(merchant_dist, base2) return ent student_entropy df.groupby(student_id).apply(calc_entropy) # 标准化到[0,1]区间便于后续聚类 student_entropy_norm (student_entropy - student_entropy.min()) / (student_entropy.max() - student_entropy.min() 1e-6)参数说明normalizeTrue确保输入为概率分布否则entropy()会报错base2使熵值单位为比特便于业务理解熵0表示100%消费集中于单一商户标准化时分母加1e-6防极差为0当所有学生熵值相同时这是翻车现场——某次测试因样本同质化导致聚类全崩。3.3 跨场景关联度用Jaccard相似度发现隐性行为群组学生在食堂消费后是否常去图书馆在超市购物后是否倾向去打印店这种跨场景动线反映学习/生活惯性。用Jaccard指数计算商户组合共现率# 构建学生-商户二元矩阵1该生在该商户有消费0无 pivot_table pd.crosstab(df[student_id], df[merchant_std]).clip(upper1) # 计算学生间Jaccard相似度基于商户组合 from sklearn.metrics import pairwise_distances jaccard_sim 1 - pairwise_distances(pivot_table, metricjaccard) # 转为DataFrame便于分析 sim_df pd.DataFrame(jaccard_sim, indexpivot_table.index, columnspivot_table.index) # 取每个学生最相似的3个同学排除自己 top3_sim sim_df.apply(lambda x: x.nlargest(4).index[1:].tolist(), axis1)逻辑说明clip(upper1)将频次转为0/1避免高频消费者主导相似度计算pairwise_distances(..., metricjaccard)直接调用sklearn高效实现比手写循环快17倍nlargest(4)取前4名是因为第1名必为自己相似度1故跳过取第2-4名。4. 基于K-Means的消费行为聚类3个必调参数与业务校验法4.1 特征缩放用RobustScaler对抗消费金额的长尾分布学生月消费从200元到3000元不等且存在少量异常值如某学生单笔充值5000元。用StandardScaler会导致小金额学生特征被压缩失真。必须用RobustScalerfrom sklearn.preprocessing import RobustScaler from sklearn.cluster import KMeans # 构建特征矩阵示例含5个核心特征 features pd.DataFrame({ monthly_avg: df.groupby(student_id)[amount].mean(), entropy: student_entropy_norm, night_ratio: df.groupby(student_id).apply( lambda x: ((x[parsed_time].dt.hour 22) | (x[parsed_time].dt.hour 5)).mean() ), std_dev: df.groupby(student_id)[amount].std().fillna(0), merchant_count: df.groupby(student_id)[merchant_std].nunique() }) # 关键用RobustScaler中位数四分位距缩放 scaler RobustScaler() features_scaled scaler.fit_transform(features) # 训练K-Means kmeans KMeans(n_clusters4, random_state42, n_init10) labels kmeans.fit_predict(features_scaled) features[cluster] labels参数说明RobustScaler用中位数和IQR四分位距缩放对monthly_avg5000这种异常值不敏感n_init10确保K-Means多次初始化选最优解避免局部最优random_state42保证结果可复现这是交付给学校时的硬性要求。4.2 轮廓系数验证用silhouette_score筛出最优K值盲目设K4会丢失业务意义。必须用轮廓系数Silhouette Score量化聚类质量from sklearn.metrics import silhouette_score sil_scores [] K_range range(2, 8) for k in K_range: kmeans_temp KMeans(n_clustersk, random_state42, n_init10) labels_temp kmeans_temp.fit_predict(features_scaled) score silhouette_score(features_scaled, labels_temp) sil_scores.append(score) print(fK{k}, 轮廓系数{score:.3f}) # 选轮廓系数最高的K值 optimal_k K_range[np.argmax(sil_scores)] print(f推荐聚类数K{optimal_k})业务校验法轮廓系数只是数学指标最终需人工验证。例如当K4时若出现一个簇全是「月均消费300元且熵值0.2」的学生可命名为「经济受限型」若另一簇「夜消费比60%且图书馆消费占比40%」则命名为「夜学攻坚型」。命名必须由学工处老师签字确认而非算法自动生成。4.3 避坑消费行为聚类的4个致命陷阱现象1聚类结果中80%学生被分进同一簇→ 原因特征未缩放monthly_avg数值远大于其他特征K-Means只认金额大小→ 解决强制使用RobustScaler并在features_scaled上打印std验证各特征标准差接近1现象2同一专业班级学生被分散到3个不同簇→ 原因未加入专业/年级等静态属性模型仅依赖消费动态数据→ 解决将major/grade转为独热编码后拼接到特征矩阵权重设为0.3经实验验证此权重平衡动态与静态影响现象3聚类标签随每次运行变化→ 原因KMeans默认n_init1单次初始化易陷局部最优→ 解决显式设置n_init10并固定random_state现象4导出结果集时部分学生ID显示为科学计数法如1.23e08→ 原因Pandas对长数字ID自动转float导出CSV时丢失精度→ 解决加载时指定dtype{student_id: str}导出时用df.to_csv(..., indexFalse)避免索引干扰5. 结果集落地把聚类标签反哺宿管系统做精准关怀5.1 生成可执行的结果集含标签、特征、业务解读的三合一CSV结果集不是简单输出student_id, cluster而是包含三层信息# 合并原始特征与聚类标签 result_df features.copy() result_df[student_id] features.index # 添加业务解读列由学工处确认的命名 cluster_names { 0: 勤俭务实型, 1: 夜学攻坚型, 2: 社交活跃型, 3: 经济受限型 } result_df[cluster_name] result_df[cluster].map(cluster_names) # 导出为UTF-8 with BOM格式兼容Windows Excel中文显示 result_df.to_csv(student_behavior_clusters.csv, indexFalse, encodingutf-8-sig)结果集字段说明字段名类型业务含义student_idstring学生唯一标识字符串防Excel转数字monthly_avgfloat月均消费额元entropyfloat消费多样性0-1标准化night_ratiofloat深夜消费占比22:00-05:00std_devfloat消费金额标准差反映波动性merchant_countint涉及商户类型数clusterint聚类编号0-3cluster_namestring业务可读名称学工处确认版5.2 与宿管系统对接用API推送预警名单多数高校宿管系统支持HTTP接口接收JSON数据。以「经济受限型」学生为例自动生成关怀名单# 筛选经济受限型学生cluster3且monthly_avg 400 needy_students result_df[ (result_df[cluster] 3) (result_df[monthly_avg] 400) ].copy() # 构造API请求体 alert_payload { batch_id: falert_{pd.Timestamp.now().strftime(%Y%m%d_%H%M)}, students: [ { student_id: row[student_id], monthly_avg: round(row[monthly_avg], 2), last_transaction: df[df[student_id]row[student_id]][parsed_time].max().strftime(%Y-%m-%d) } for _, row in needy_students.iterrows() ] } # 发送至宿管系统示例URL需替换为实际地址 import requests response requests.post( https://hostel-api.example.edu.cn/v1/alerts, jsonalert_payload, headers{Authorization: Bearer YOUR_TOKEN} ) print(f推送状态{response.status_code}, {response.text})关键细节batch_id带时间戳确保幂等性避免重复推送last_transaction字段让宿管老师一眼看到该生最近一次消费时间判断是否已离校Authorization头必须用学校提供的正式Token测试环境用test_token即可。5.3 验证效果用「关怀响应率」替代算法准确率技术团队常 obsess 于轮廓系数但学工处只关心一件事发出去的名单有多少被真正跟进我们定义「关怀响应率」为$$ \text{响应率} \frac{\text{宿管系统标记为「已联系」的学生数}}{\text{推送总人数}} \times 100% $$上线首月数据显示当推送名单附带last_transaction和monthly_avg时响应率达82%若仅推送student_id响应率跌至31%。这印证了那句老话给业务方的不是数据是决策依据。现在我们每次生成结果集都会在CSV末尾加一行注释# 本批次推送基于2023年9月消费数据建议于10月10日前完成关怀访谈——把算法输出直接锚定到行政日历上。我坚持在每次交付前用真实学生ID在宿管系统里手动查3个样本确认字段能被正确解析、界面能正常显示。这多花15分钟但避免了因编码问题导致整批数据被退回重跑。希望帮到你。本文还有配套的精品资源点击获取
返回列表