拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

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

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_idstudent_info.xlsx学生基础档案student_id,gender,major,grade,classmerchant_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:15ISO格式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_formatTrue更稳定——后者在混合格式下会静默失败。第二步卡号去重与身份绑定原始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_oncard_no_clean, right_onstudent_id, howleft, suffixes(_trans, _info)) # 对仍为空的记录用device_idtrans_time近似匹配仅限食堂设备 merged.loc[merged[gender].isna() merged[device_id].str.contains(canteen), gender] 未知逻辑说明howleft确保交易流水不丢失suffixes避免列名冲突device_id回填是兜底策略——实际项目中我们发现食堂POS机device_id前缀固定为CANT-可辅助识别。第三步商户类型映射与餐段判定# task1_1.ipynb 第45行基于merchant_mapping的精准归类 merchant_map pd.read_csv(code/data_raw/merchant_mapping.csv, encodingutf-8) df_mapped df.merge(merchant_map, onmerchant_name, howleft) # 餐段判定逻辑非简单按时间切片 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_value0) meal_daily_ratio meal_counts.div(meal_counts.sum(axis0), axis1) * 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聚类肘部法确定k3 from sklearn.cluster import KMeans inertias [] for k in range(2, 7): kmeans KMeans(n_clustersk, random_state42, n_init10) kmeans.fit(features_scaled) inertias.append(kmeans.inertia_) # 选择k3肘部点重新训练并保存标签 kmeans_final KMeans(n_clusters3, random_state42, n_init10) 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(..., ongender)解决打开student_info.xlsx将第一行表头改为gender或修改task2_2.ipynb第8行student_info.rename(columns{sex:gender}, inplaceTrue)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_init10虽提升稳定性但标签编号仍不固定解决按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, indexFalse) 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, binsrange(25), alpha0.7, colorsteelblue) # 标注食堂开放时间非理论时间 plt.axvline(x6.5, colorred, linestyle--, label早餐开放6:30) plt.axvline(x12.5, colororange, linestyle--, label午餐开放12:30) plt.axvline(x17.5, colorgreen, linestyle--, label晚餐开放17:30) # 标注课表强相关峰基于教务处课表数据 plt.axvspan(10.25, 10.75, alpha0.2, coloryellow, label第3-4节下课10:15-10:30) plt.axvspan(12.25, 12.75, alpha0.2, colorpink, 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(datachain_data, xgender, ydaily_avg, width0.5, # 箱体宽度 fliersize3, # 离群点大小 linewidth1.2) # 边框粗细 # 添加均值点弥补箱线图不显示均值的缺陷 for i, gender in enumerate([男, 女]): mean_val chain_data[chain_data[gender]gender][daily_avg].mean() ax.plot(i, mean_val, D, colorblack, markersize5, labelf{gender}均值) ax.legend()参数价值fliersize3让离群点不喧宾夺主linewidth1.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], cfeatures[cluster_relabel], cmapviridis, alpha0.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^, s150, cred, edgecolorsblack, linewidth1.5) # 动态生成标注文本含业务解读 if i 0: label_text fCluster {i}\n总金额低\n均值稳定\n节俭规律型 elif i 1: label_text fCluster {i}\n总金额中\n均值偏高\n品质偏好型 else: label_text fCluster {i}\n总金额高\n均值波动\n偶发大额型 plt.text(center[0]5, center[1]0.5, label_text, fontsize9, bboxdict(boxstyleround,pad0.3, facecolorwhite, alpha0.8)) plt.xlabel(总消费金额元) plt.ylabel(单次均值元) plt.title(学生消费行为三维聚类按总金额与单次均值)玄学细节bboxdict(...)给文本加白底半透明框避免被散点遮挡center[0]5是手动微调位置确保文字不压线——我试过用annotate自动定位但答辩PPT放大后文字常错位手调反而最稳。从那以后我每次做毕设可视化都强制走一遍“标注业务节点→配色校验→文字防遮挡”三步检查。哪怕多花15分钟答辩时导师指着图说“这个标注很到位”比你讲十分钟方法论都有力。希望帮到你。本文还有配套的精品资源点击获取
返回列表