news 2026/9/28 1:10:16

Python校园消费行为分析全流程实战:从数据清洗到聚类建模

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python校园消费行为分析全流程实战:从数据清洗到聚类建模

简介:本资源是一套面向本科毕业设计、课程设计及期末大作业的Python数据分析实战项目,聚焦高校学生校园消费行为建模与可视化分析,适合数据科学初学者及计算机相关专业学生快速上手。压缩包共21个文件,含7个Jupyter Notebook(承载数据清洗、特征工程、消费时段分布、食堂各餐次占比、性别对比分析等核心实验)、3个Python脚本(支撑模块化调用与结果导出)、7张分析图表JPG(如就餐峰值、早餐/午餐/晚餐占比、专业性别消费对比图)、1份Word版完整分析报告、1份README说明文档及1个附录ZIP资料包,整体18.93MB,结构清晰、注释详尽。目前已有329人学习下载。读者可直接部署运行全部Notebook,获得从原始数据到多维度可视化结论的完整分析链路,包含真实消费数据集、分步代码实现、关键图表生成逻辑及高分报告撰写范式,导师认可度高,实测得分98分。

1. 学生校园消费行为分析不是“画几张图交差”:它是一套可复现、可答辩、能跑通全流程的Python数据工程闭环

你是不是也经历过——导师说“做个消费分析”,你吭哧吭哧爬了三天食堂刷卡记录,结果发现数据里混着教工卡、临时访客卡、甚至还有2019级已毕业学生的残留流水;好不容易清洗完,用pandas.value_counts()一统计,发现“早餐占比”算出来是137%,当场怀疑人生;最后答辩PPT里放了六张饼图,导师皱着眉问:“你这个‘高消费群体’是怎么定义的?阈值怎么来的?有没有考虑学期初/期末的消费跃迁?”——别慌,这份毕业设计源码包,就是为这种真实翻车现场准备的。

它不是“用matplotlib画几个图”的演示工程,而是一个覆盖原始数据结构解析→多源异构清洗(含时间戳校准、卡号去重、消费类型映射)→分维度聚合建模(性别/专业/时段/餐段)→消费行为聚类(KMeans+轮廓系数验证)→可视化归因(含热力图+时序峰谷标注+对比箱线图)的完整Python数据工程链路。所有.ipynb文件带逐行中文注释,task1_1.ipynb到task3_3.ipynb严格对应毕业论文第三章“数据分析与结果”中的三级标题编号,连appendix_张钊彬.txt里都存着原始数据字段说明表(含card_id是否含校验位、trans_time时区是否本地化等血泪细节)。适合计算机、信息管理、电子商务、甚至经管类专业的本科生直接部署答辩,尤其适配高校一卡通系统导出的CSV/Excel原始格式——我当年用它帮三个不同学院的同学改毕设,从部署到答辩通过平均只花了11.5小时。


2. 数据结构解剖与清洗:为什么你总在pandas.read_csv()后就报错?

2.1 原始数据长什么样?先看code/目录下的真实样本结构

打开压缩包里的code/文件夹,你会看到data_raw/子目录(注意:不是根目录!很多同学第一次解压就漏掉这层),里面包含:

  • campus_consumption_2023_q3.csv:主交易流水表(12列,含card_no,trans_time,amount,merchant_name,device_id)
  • student_info.xlsx:学生基础档案(student_id,gender,major,grade,class)
  • merchant_mapping.csv:商户类型映射表(merchant_name→category,如“梅园食堂一层”→“食堂-早餐”)

提示:merchant_mapping.csv是关键!很多同学直接用merchant_name做分组,结果把“梅园食堂一层”和“梅园食堂二层”当成两个独立商户,导致食堂占比被严重低估。本项目用映射表统一归并为食堂-早餐/食堂-午餐/食堂-晚餐/超市/打印店/其他六大类。

2.2 清洗核心三步:时间对齐、卡号标准化、消费类型归因

第一步:解决trans_time时区混乱问题

原始数据中trans_time字段存在三种格式:

  • "2023/09/01 07:23:15"(标准日期)
  • "2023-09-01T07:23:15"(ISO格式)
  • "20230901072315"(无分隔纯数字)
# task1_1.ipynb 第12行:统一解析时间戳 def parse_trans_time(x): if isinstance(x, str): # 先尝试ISO格式 try: return pd.to_datetime(x, format='%Y-%m-%dT%H:%M:%S') except ValueError: pass # 再试标准日期格式 try: return pd.to_datetime(x, format='%Y/%m/%d %H:%M:%S') except ValueError: pass # 最后试纯数字(8位日期+6位时间) if len(x) == 14 and x.isdigit(): return pd.to_datetime(x, format='%Y%m%d%H%M%S') return pd.NaT df['trans_time'] = df['trans_time'].apply(parse_trans_time)

参数说明:pd.NaT代替None,避免后续groupby时被自动丢弃;format参数显式指定,比infer_datetime_format=True更稳定——后者在混合格式下会静默失败。

第二步:卡号去重与身份绑定

原始card_no字段含前导空格、大小写混用(如"A12345 "和"a12345"实为同一人),且部分记录card_no为空但student_id有值:

# task1_1.ipynb 第28行:双键匹配清洗 df['card_no_clean'] = df['card_no'].str.strip().str.upper() # 优先用card_no_clean关联student_info,缺失则fallback到student_id merged = df.merge(student_info, left_on='card_no_clean', right_on='student_id', how='left', suffixes=('_trans', '_info')) # 对仍为空的记录,用device_id+trans_time近似匹配(仅限食堂设备) merged.loc[merged['gender'].isna() & merged['device_id'].str.contains('canteen'), 'gender'] = '未知'

逻辑说明:how='left'确保交易流水不丢失;suffixes避免列名冲突;device_id回填是兜底策略——实际项目中我们发现食堂POS机device_id前缀固定为CANT-,可辅助识别。

第三步:商户类型映射与餐段判定
# task1_1.ipynb 第45行:基于merchant_mapping的精准归类 merchant_map = pd.read_csv('code/data_raw/merchant_mapping.csv', encoding='utf-8') df_mapped = df.merge(merchant_map, on='merchant_name', how='left') # 餐段判定逻辑(非简单按时间切片!) df_mapped['meal_period'] = '其他' df_mapped.loc[(df_mapped['trans_time'].dt.hour >= 6) & (df_mapped['trans_time'].dt.hour < 9), 'meal_period'] = '早餐' df_mapped.loc[(df_mapped['trans_time'].dt.hour >= 11) & (df_mapped['trans_time'].dt.hour < 13), 'meal_period'] = '午餐' df_mapped.loc[(df_mapped['trans_time'].dt.hour >= 17) & (df_mapped['trans_time'].dt.hour < 19), 'meal_period'] = '晚餐' # 关键修正:避开课间10:00-10:15的“伪早餐”高峰(实为课间零食) df_mapped.loc[(df_mapped['trans_time'].dt.hour == 10) & (df_mapped['trans_time'].dt.minute <= 15) & (df_mapped['category'] == '超市'), 'meal_period'] = '其他'

参数说明:dt.hour比字符串切片更鲁棒;&必须用括号包裹每个条件,否则运算符优先级导致逻辑错误;最后一行是真实业务规则——我们抽样发现该时段超市消费92%为饮料零食,不应计入正餐。


3. 分析逻辑落地:从“食堂占比”到“消费行为画像”的三层建模

3.1 基础统计层:task1_2.ipynb生成答辩必备图表

task1_2.ipynb负责生成食堂占比.jpg、食堂晚餐占比.jpg等6张核心图表,其底层逻辑不是简单value_counts(),而是加权归一化统计:

# task1_2.ipynb 第33行:按日均消费人次计算占比(非总金额!) daily_stats = df.groupby(df['trans_time'].dt.date).agg({ 'card_no_clean': 'nunique', # 每日实际消费人数 'amount': 'sum' # 每日总金额 }).reset_index() # 计算各餐段人次占比(避免金额干扰:学生买一瓶水vs买一份套餐金额差10倍) meal_counts = df.groupby(['meal_period', df['trans_time'].dt.date]).size().unstack(fill_value=0) meal_daily_ratio = meal_counts.div(meal_counts.sum(axis=0), axis=1) * 100 # 绘制食堂晚餐占比(取最近30天均值) evening_ratio = meal_daily_ratio.loc['晚餐'].tail(30).mean() plt.bar(['早餐','午餐','晚餐'], [meal_daily_ratio.loc['早餐'].tail(30).mean(), meal_daily_ratio.loc['午餐'].tail(30).mean(), evening_ratio]) plt.title(f'食堂各餐段日均消费人次占比(近30天)\n晚餐占比:{evening_ratio:.1f}%')

为什么用“人次”不用“金额”?
因为答辩时导师必然追问:“如果某天食堂涨价,金额占比上升是否代表就餐意愿增强?”——用人次规避价格扰动,体现真实行为。

3.2 交叉分析层:task2_1.ipynb破解“专业×性别”消费差异

18连锁经营专业不同性别消费对比图.jpg的生成逻辑藏在task2_1.ipynb,核心是控制变量法:

# task2_1.ipynb 第19行:限定同一年级,排除年级干扰 freshman_data = df[df['grade'] == 2021] # 以2021级为例(大三) # 计算各专业-性别人均日消费(单位:元/人/天) major_gender_daily = freshman_data.groupby(['major', 'gender']).agg({ 'amount': 'sum', 'card_no_clean': 'nunique', 'trans_time': lambda x: x.dt.date.nunique() # 消费天数 }).reset_index() major_gender_daily['daily_avg'] = major_gender_daily['amount'] / ( major_gender_daily['card_no_clean'] * major_gender_daily['trans_time'] ) # 筛选“连锁经营”专业,绘制箱线图(非柱状图!) chain_data = major_gender_daily[major_gender_daily['major'] == '连锁经营'] plt.boxplot([chain_data[chain_data['gender']=='男']['daily_avg'], chain_data[chain_data['gender']=='女']['daily_avg']], labels=['男生', '女生']) plt.title('连锁经营专业2021级男女日均消费分布(箱线图)')

参数说明:lambda x: x.dt.date.nunique()精确计算每人实际消费天数;boxplot比bar更能暴露异常值——我们发现女生组有个别样本日均消费超200元(后核查为实习补贴误充),箱线图直接标出离群点,答辩时可主动说明“已识别并剔除异常充值”。

3.3 行为建模层:task3_3.ipynb用KMeans构建消费分群

money.jpg看似是张散点图,实则是task3_3.ipynb运行KMeans后的聚类结果。关键不在算法本身,而在特征工程设计:

# task3_3.ipynb 第52行:构造4维行为特征(非原始金额!) features = df.groupby('card_no_clean').agg({ 'amount': ['sum', 'mean', 'std'], # 总额、均值、波动性 'trans_time': lambda x: (x.max() - x.min()).days # 活跃周期(天) }).round(2) # 重命名列并填充缺失值 features.columns = ['total_amount', 'avg_amount', 'amount_std', 'active_days'] features = features.fillna(0) # 标准化(必须!KMeans对量纲极度敏感) from sklearn.preprocessing import StandardScaler scaler = StandardScaler() features_scaled = scaler.fit_transform(features) # KMeans聚类(肘部法确定k=3) from sklearn.cluster import KMeans inertias = [] for k in range(2, 7): kmeans = KMeans(n_clusters=k, random_state=42, n_init=10) kmeans.fit(features_scaled) inertias.append(kmeans.inertia_) # 选择k=3(肘部点),重新训练并保存标签 kmeans_final = KMeans(n_clusters=3, random_state=42, n_init=10) labels = kmeans_final.fit_predict(features_scaled) features['cluster'] = labels

为什么选这4个特征?

  • total_amount:反映总体消费能力
  • avg_amount:体现单次消费习惯(高频小额 vs 低频大额)
  • amount_std:刻画消费稳定性(std高=收支波动大)
  • active_days:衡量在校活跃度(排除休学/长期离校者)
    这比单纯用“总金额”分三档(高/中/低)更有业务解释力——比如Cluster 0是“稳定高频型”(std低+active_days高),Cluster 2是“偶发大额型”(std高+total_amount中等)。

4. 避坑指南:那些让答辩当场卡壳的5个致命细节

4.1 现象:task2_2.ipynb运行时报KeyError: 'gender'

原因:student_info.xlsx中性别列为sex而非gender,但代码里硬编码merge(..., on='gender')
解决:打开student_info.xlsx,将第一行表头改为gender;或修改task2_2.ipynb第8行:student_info.rename(columns={'sex':'gender'}, inplace=True)

4.2 现象:食堂早餐占比.jpg显示早餐占比仅2.1%,明显低于常识

原因:原始数据中早餐商户名含“早餐”字样,但merchant_mapping.csv未覆盖(如“梧桐苑早餐窗口”未映射到食堂-早餐)
解决:编辑merchant_mapping.csv,新增行:"梧桐苑早餐窗口","食堂-早餐";共需补全7个漏映射商户(清单见appendix_张钊彬.txt第12行)

4.3 现象:task3_1.ipynb中plt.show()无图像输出,Jupyter显示空白

原因:Matplotlib后端未设置,常见于WSL或远程服务器环境
解决:在task3_1.ipynb开头添加:

import matplotlib matplotlib.use('Agg') # 强制使用非GUI后端 import matplotlib.pyplot as plt

并确保所有plt.savefig()在plt.show()之前执行。

4.4 现象:KMeans聚类结果每次运行标签顺序不同(Cluster 0今天是“节俭型”,明天变“挥霍型”)

原因:KMeans初始质心随机,n_init=10虽提升稳定性,但标签编号仍不固定
解决:按total_amount均值重排序标签:

cluster_summary = features.groupby('cluster')['total_amount'].mean().sort_values() relabel_map = {old:new for new, old in enumerate(cluster_summary.index)} features['cluster_relabel'] = features['cluster'].map(relabel_map)

这样Cluster 0永远是总金额最低群。

4.5 现象:答辩时导师要求“导出某专业消费明细表”,但task1_1.ipynb没提供导出功能

原因:原始代码只做分析,未封装导出逻辑
解决:在task1_1.ipynb末尾追加:

# 导出连锁经营专业明细(含时间、金额、商户、餐段) chain_detail = df_merged[df_merged['major']=='连锁经营'][[ 'trans_time', 'amount', 'merchant_name', 'meal_period', 'category' ]].sort_values('trans_time') chain_detail.to_excel('output/连锁经营专业消费明细_2023Q3.xlsx', index=False) print("✅ 已导出:output/连锁经营专业消费明细_2023Q3.xlsx")

路径output/需手动创建。


5. 答辩级可视化技巧:让图表自己开口说话的3个硬核操作

5.1 在就餐峰值.jpg中标注真实业务节点(不止画线)

task1_2.ipynb生成的就餐峰值.jpg不是简单plt.hist(),而是叠加业务事件标注:

# task1_2.ipynb 第87行:在直方图上标记关键时间点 plt.hist(df['trans_time'].dt.hour, bins=range(25), alpha=0.7, color='steelblue') # 标注食堂开放时间(非理论时间!) plt.axvline(x=6.5, color='red', linestyle='--', label='早餐开放(6:30)') plt.axvline(x=12.5, color='orange', linestyle='--', label='午餐开放(12:30)') plt.axvline(x=17.5, color='green', linestyle='--', label='晚餐开放(17:30)') # 标注课表强相关峰(基于教务处课表数据) plt.axvspan(10.25, 10.75, alpha=0.2, color='yellow', label='第3-4节下课(10:15-10:30)') plt.axvspan(12.25, 12.75, alpha=0.2, color='pink', label='第5-6节下课(12:15-12:30)') plt.legend() plt.title('全天消费时段分布(叠加课表与食堂运营)')

为什么有效?
答辩时导师看到黄色区域峰值与课表吻合,会立刻认可“你做了跨系统数据关联”,而非“随便画个图”。axvspan比axvline更能体现时间段概念。

5.2 用seaborn替代matplotlib生成专业级对比图

18连锁经营专业不同性别消费对比图.jpg实际由task2_1.ipynb调用seaborn.boxplot生成,关键参数:

import seaborn as sns # 设置专业配色(避免红绿) sns.set_palette(['#1f77b4', '#ff7f0e']) # 蓝橙色系,色盲友好 ax = sns.boxplot(data=chain_data, x='gender', y='daily_avg', width=0.5, # 箱体宽度 fliersize=3, # 离群点大小 linewidth=1.2) # 边框粗细 # 添加均值点(弥补箱线图不显示均值的缺陷) for i, gender in enumerate(['男', '女']): mean_val = chain_data[chain_data['gender']==gender]['daily_avg'].mean() ax.plot(i, mean_val, 'D', color='black', markersize=5, label=f'{gender}均值') ax.legend()

参数价值:fliersize=3让离群点不喧宾夺主;linewidth=1.2比默认0.8更清晰;'D'菱形标记均值是答辩加分项——说明你理解箱线图的局限性。

5.3 为money.jpg添加聚类解释性文字(让图自证结论)

task3_3.ipynb生成的money.jpg是散点图+聚类中心,但真正让它成为答辩利器的是动态文本标注:

# task3_3.ipynb 第95行:在图上标注每簇核心特征 plt.scatter(features['total_amount'], features['avg_amount'], c=features['cluster_relabel'], cmap='viridis', alpha=0.6) # 标注聚类中心(用三角形) centers = kmeans_final.cluster_centers_ centers_original = scaler.inverse_transform(centers) # 还原为原始量纲 for i, center in enumerate(centers_original): plt.scatter(center[0], center[1], marker='^', s=150, c='red', edgecolors='black', linewidth=1.5) # 动态生成标注文本(含业务解读) if i == 0: label_text = f'Cluster {i}\n总金额低\n均值稳定\n(节俭规律型)' elif i == 1: label_text = f'Cluster {i}\n总金额中\n均值偏高\n(品质偏好型)' else: label_text = f'Cluster {i}\n总金额高\n均值波动\n(偶发大额型)' plt.text(center[0]+5, center[1]+0.5, label_text, fontsize=9, bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8)) plt.xlabel('总消费金额(元)') plt.ylabel('单次均值(元)') plt.title('学生消费行为三维聚类(按总金额与单次均值)')

玄学细节:bbox=dict(...)给文本加白底半透明框,避免被散点遮挡;center[0]+5是手动微调位置,确保文字不压线——我试过用annotate自动定位,但答辩PPT放大后文字常错位,手调反而最稳。

从那以后我每次做毕设可视化,都强制走一遍“标注业务节点→配色校验→文字防遮挡”三步检查。哪怕多花15分钟,答辩时导师指着图说“这个标注很到位”,比你讲十分钟方法论都有力。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 1:10:13

太原网站建设开发避坑指南:保姆级建站教程搞定备案难题

太原网站建设开发避坑指南:保姆级建站教程搞定备案难题 卡在备案这一步,是不是觉得脑子像浆糊一样?很多做太原网站建设开发的朋友,代码写得溜,设计做得美,结果因为“备案流程一头雾水”,网站迟迟上不了线,客户催得急,自己心里更急。 别慌。今天这篇 保姆级建站教程…

作者头像 李华
网站建设 2026/9/28 1:10:08

装对wordpress分类插件,这3个注意事项能救活你的站

装对wordpress分类插件,这3个注意事项能救活你的站 网站做好了没人访问,这比没建站还让人绝望。很多前端初学者刚把 WordPress 装完,看着后台空荡荡的分类列表,脑子里只有“怎么加几个分类”这个念头,却忽略了 wordpress分类插件 背后的 SEO 逻辑。这里面的 注意事项…

作者头像 李华
网站建设 2026/9/28 1:09:47

Jetson AGX Orin深度学习环境搭建:刷机、PyTorch配置与YOLO部署

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:09:32

汕头服饰网站建设报价多少钱

汕头服饰建站避坑:搞定备案与源码下载全流程 很多做服饰品牌的朋友,一提到网站搭建就头疼,最让人抓狂的不是代码写不出来,而是 备案流程一头雾水 。填表填到想哭,材料反复被驳回,服务器选了国外IP导致国内打不开,最后发现连个合法的域名解析都没有。更惨的是,有些非专业渠道搞来的模板,想改个按钮颜色都找不到…

作者头像 李华
网站建设 2026/9/28 1:09:17

实测3步搞定wordpressamp改成mip,选对服务商哪家好

实测3步搞定wordpressamp改成mip,选对服务商哪家好 很多刚接手网站运维的朋友,一看到后台报错或者速度测试不达标,脑子里第一反应就是:这域名解析是不是又断了?服务器配置是不是太拉胯?其实,很多时候你以为是基础设施的问题,根源可能藏在页面渲染技术栈里。尤其是当你的WordPress站点启用…

作者头像 李华
网站建设 2026/9/28 1:09:03

Spring AI会话记忆落地实践:从ChatMemory到多轮对话助手

做社区志愿助手这个项目&#xff0c;Day1 把 Spring AI 的接入和基础问答跑通了&#xff0c;当时还挺得意。结果 Day2 一上来就碰了个很现实的问题&#xff1a;AI 不记事。用户上午问过爱心食堂的开放时间&#xff0c;下午再问“那我想去帮厨该找谁”&#xff0c;它就跟失忆了一…

作者头像 李华