ARTICLE DETAIL

资讯详情

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

Python校园消费行为分析:pandas+sklearn+Streamlit实战指南

Python校园消费行为分析:pandas+sklearn+Streamlit实战指南 简介面向Python数据分析与机器学习方向的毕业设计人群这份资源以校园智能卡消费数据为切入点完整覆盖“数据加载→预处理→特征建模→可视化展示”的经典项目流程。压缩包共含17个文件其中6个Python源文件分别承担数据读取、预处理、模型训练、结果可视化等核心功能并附带编译产生的pyc缓存文件2份CSV文件提供可复用的原始实验数据另有PDF与Word版本的详细项目说明报告、演示截图以及README说明文档整体压缩包大小为13.05MB目录按功能模块划分清晰。项目还提供了基于Streamlit的交互式界面入口运行app.py即可在浏览器中查看消费行为分布、异常消费识别和学生经济评估结果降低复现门槛。目前已有104人学习下载适合需要参考完整毕业设计源码、学习校园数据挖掘套路或快速搭建同类分析系统的Python开发者。1. 学生校园消费行为分析这个毕业设计项目到底解决了什么问题学生校园消费行为分析说白了就是把校园一卡通的流水数据变成能写进论文里的结论。这个项目拿到的原材料不是问卷而是学生每天在食堂、超市、文印店、开水房刷卡产生的真实消费流水几万行记录摆在面前人眼看不完拍脑袋下结论又不作数。项目用 pandas 做清洗和聚合用 scikit-learn 做聚类把学生区分成不同消费层级再用 Streamlit 把结果做成可交互的面板最后输出一份能支撑毕业设计文档的分析结论。它解决的是“拿到一张流水表怎么下手”的问题也顺带把数据分析的完整链路走了一遍。适合正在选 Python 毕业设计题目的同学也适合想快速上手 pandas sklearn Streamlit 三条主线的人。2. 从智能卡流水到行为特征数据加载与预处理的全过程2.1 先摸清楚两份 CSV 的字段结构拿到资源第一件事不是急着跑 app.py而是先打开 data/data1.csv 和 data/data2.csv 看字段。校园卡消费数据的字段一般逃不出这么几类学号、消费时间、消费金额、消费地点、商户类型以及可能存在的校区或卡类型字段。data1 和 data2 大概率是两个时间窗口或两个批次的流水合并之后样本量更足聚类结果也更稳。先写一个临时探查脚本不急着进入项目代码import pandas as pd df1 pd.read_csv(data/data1.csv, encodingutf-8) df2 pd.read_csv(data/data2.csv, encodingutf-8) print(data1 shape:, df1.shape) print(data2 shape:, df2.shape) print(df1.columns.tolist()) print(df1.head())拿到结果后重点看三件事一是 student_id 是否唯一标识一个学生二是 consume_time 是字符串还是已解析成时间类型三是 consume_amount 是否存在 0 值或负值这些直接决定后面清洗逻辑怎么写。常见做法是让两批数据用pd.concat纵向拼接但拼接前必须确认两边字段名完全一致拼接后要重新检查去重和排序否则后面 groupby 时会出现莫名其妙的多算或少算。2.2 data_loader.py读取和解析的标准姿势项目里的 core/data_loader.py 承担的是数据入口职责。它把读取、编码处理、时间解析集中在一个函数里后续 app.py 和 model.py 都从它这里拿数据避免每个模块各写一遍 read_csv也方便统一改路径和参数。# core/data_loader.py import pandas as pd def load_data(path: str data/data1.csv, encoding: str utf-8) - pd.DataFrame: df pd.read_csv(path, encodingencoding) if consume_time in df.columns: df[consume_time] pd.to_datetime(df[consume_time], errorscoerce) return df这个函数有两个参数值得注意。path 建议统一用相对路径因为答辩时项目可能被拷到任何一台机器上绝对路径一换电脑就崩。encoding 默认 utf-8但有些旧版导出的 CSV 是 gbk 编码如果读出来全是乱码把 encoding 改成 gbk 重试即可。pd.to_datetime的errorscoerce很关键它会把解析失败的脏数据转为 NaT而不是直接抛异常中断程序后面清洗时统一 dropna 就能把脏行处理掉。2.3 preprocessor.py 的清洗逻辑缺、重、脏三关预处理是整个项目里最拉分的地方。core/preprocessor.py 里做的事情归纳起来就是过三关缺失值关、重复值关、脏值关。# core/preprocessor.py import pandas as pd def clean_data(df: pd.DataFrame) - pd.DataFrame: # 第一关时间或金额为空的行直接丢掉 df df.dropna(subset[consume_time, consume_amount]) # 第二关金额必须大于00元转账和负数退款不属于消费 df df[df[consume_amount] 0] # 第三关同一学生同一秒同一金额视为重复刷卡记录 df df.drop_duplicates(subset[student_id, consume_time, consume_amount]) return df这三个步骤各有讲究。dropna 看起来简单其实要先想清楚是删行还是填充——消费流水这种场景下时间或金额缺失的行没有保留价值直接删但如果是分析就餐率缺失日期反而要单独统计不能一刀切。金额过滤我一般保留一个上限阈值单笔超过 500 元的记录先不直接删而是单独打标因为这些可能是课本费、充值这类非食堂消费直接删会导致总消费金额偏低。drop_duplicates 的三列组合覆盖了“学生、时间、金额都相同”的重复刷卡这是校园卡系统里最常见的重复来源排查时优先查这一条。2.4 特征构造把流水表变成按学生分组的宽表清洗完之后原始流水还是长表一行是一笔消费。建模需要的是宽表一行是一个学生的聚合特征。build_features 函数就是把长表转宽表的这一步。def build_features(df: pd.DataFrame) - pd.DataFrame: # 按天聚合得到每个学生每天的消费总额 daily df.groupby([student_id, df[consume_time].dt.date])[consume_amount].sum().reset_index() daily.columns [student_id, consume_date, daily_amount] # 再按学生聚合得到统计特征 stats daily.groupby(student_id)[daily_amount].agg( avg_dailymean, std_dailystd, max_dailymax, active_dayscount ).reset_index() # 计算食堂消费占比 canteen_set [第一食堂, 第二食堂, 清真食堂, 风味食堂] df_canteen df[df[merchant_type].isin(canteen_set)] canteen_sum df_canteen.groupby(student_id)[consume_amount].sum() total_sum df.groupby(student_id)[consume_amount].sum() stats[canteen_ratio] (canteen_sum / total_sum).fillna(0).values return stats这里最隐蔽的坑是dt.date的使用groupby 的键必须是df[consume_time].dt.date而不是 consume_time 本身否则同一学生同一时刻的多次消费会被拆成不同行。食堂占比这一步如果某个学生在数据窗口内一次食堂都没刷total_sum 里有记录但 canteen_sum 里没有直接相除是 NaNfillna(0) 是必须的。我一般还会加一个 avg_per_meal 之类的特征表示每顿的平均花费它对区分“吃食堂为主”和“顿顿点外卖”的学生很有用只是这一步需要先按餐次划分时间窗口工作量会相应增加。2.5 预处理完怎么验证没出错特征表构造出来后不要急着去聚类先验证一遍。最直接的口径是stats 的行数应该等于 df 里唯一的 student_id 数量任何一行的 avg_daily 乘以 active_days 应该近似等于该学生的总消费除以记录天数如果差太多说明聚合键有问题。另一个习惯是画出消费金额分布直方图和每日活跃消费人数曲线用肉眼看是否有异常尖峰。比如某天消费总额突然变成 0那多半是数据缺失而不是学生集体不吃饭。3. 学生经济评估模型用聚类而不是靠拍脑袋定阈值3.1 为什么不直接按月消费金额划分贫富很多第一次做这个题目的同学会直接写月消费低于 500 算低消费高于 1500 算高消费。这样做的最大问题是阈值是拍脑袋定的换个学校、换个月份就不成立。更重要的是经济状况不能只看消费总额一个每天只吃食堂、月消费 800 的学生和一个外卖不断、月消费 800 的学生生活质量完全不同。所以项目的正确做法是拿多维特征去聚类让数据自己决定分层而不是先入为主定死一条线。3.2 特征标准化不标准化聚类结果会被单个特征绑架聚类之前必须先对特征做标准化。因为 avg_daily 的量纲是元可能是几十到上百而 canteen_ratio 是 0 到 1 的小数如果不标准化KMeans 的距离计算会被金额特征完全主导canteen_ratio 等于白做。项目里用 sklearn 的 StandardScaler 做这件事把它变成均值为 0、标准差为 1 的分布让每个特征在距离计算里都能说上话。3.3 model.py 里的聚类实现与参数细节# core/model.py import pandas as pd from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans def assess_economic(features: pd.DataFrame, n_clusters: int 5) - pd.DataFrame: feature_cols [avg_daily, std_daily, max_daily, active_days, canteen_ratio] X features[feature_cols].fillna(0) scaler StandardScaler() X_scaled scaler.fit_transform(X) model KMeans(n_clustersn_clusters, random_state42, n_init10) features[cluster] model.fit_predict(X_scaled) return features, modeln_clusters 的取值不是越大越好。项目默认 5 类对应常见的五档非常紧张、偏紧张、中等、较宽裕、宽裕。random_state 固定成 42 是为了让每次运行结果可复现答辩时老师问“为什么两次跑结果不一样”是非常常见的翻车场景。n_init10 是让 KMeans 从 10 个不同的初始中心点开始跑取最优结果避免掉进局部最优。参数默认值说明n_clusters5对应五档经济层级可调 3-8random_state42固定随机种子保证可复现n_init10多初始中心择优避免局部最优StandardScaler均值0方差1消除特征量纲差异3.4 聚类中心到经济等级的映射聚类跑完之后做的事是“给标签起名字”。把model.cluster_centers_按 avg_daily 排序最低一组对应经济最紧张。但不要只看均值还要结合 canteen_ratio 和 max_daily 来解释如果某组 avg_daily 低但 max_daily 很高说明平时省、偶尔大额消费可能是特殊情况导致的不能简单定义为贫困。# 按聚类中心的avg_daily排序 centers pd.DataFrame(model.cluster_centers_, columnsfeature_cols) centers_sorted centers.sort_values(avg_daily, ascendingFalse) print(centers_sorted)输出之后把 cluster 原始标签重新映射成 1-5 档这一步一定要在文档里写清楚映射规则否则答辩时评委对照聚类散点图和标签说明会对不上号。4. 用 Streamlit 搭交互式分析面板app.py 的工程化写法4.1 页面骨架侧边栏控制、主区域展示app.py 是项目入口运行命令是streamlit run app.py。Streamlit 的写法核心是自上而下执行脚本每次交互都会重新跑全脚本所以数据加载和预处理要加缓存装饰器否则拖一次滑块就卡顿几秒。# app.py import pandas as pd import streamlit as st from core.data_loader import load_data from core.preprocessor import clean_data, build_features from core.model import assess_economic from core.visualizer import plot_daily_trend, plot_cluster_scatter st.cache_data def load_and_prepare(): df1 load_data(data/data1.csv) df2 load_data(data/data2.csv) df pd.concat([df1, df2], ignore_indexTrue) df clean_data(df) return build_features(df), df st.set_page_config(page_title校园消费行为分析, layoutwide) st.title(校园消费行为分析与学生经济评估) features, raw_df load_and_prepare()st.cache_data是 Streamlit 1.18 之后推荐的缓存 API老项目里可能见到st.cache如果跑的时候报错说 cache 已废弃把这行改成 cache_data 即可。layoutwide 是页面宽度设置默认是窄屏图表多了放不下务必改成 wide。4.2 plotly 图怎么嵌进 Streamlitcore/visualizer.py 里封装的图表函数返回 plotly figure在 app.py 里用st.plotly_chart渲染。plotly 的好处是缩放、悬停、筛选都是浏览器原生交互答辩演示时鼠标划过散点能看到具体学生编号和消费金额观感比 matplotlib 强太多。# core/visualizer.py import plotly.express as px def plot_cluster_scatter(features: pd.DataFrame): fig px.scatter( features, xavg_daily, ycanteen_ratio, colorcluster, hover_data[student_id, max_daily], title消费层级聚类散点图 ) return fig在 app.py 里调用时st.plotly_chart(fig, use_container_widthTrue)这里的 use_container_width 很关键不写的话图表默认宽度只有约 800 像素在 wide 布局里会显得特别小评委在投影上看不清散点分布。4.3 侧边栏控件让答辩现场能交互式调参Streamlit 的 st.sidebar 可以放滑块、下拉框、多选框。这个项目里最有价值的交互是让老师现场选择聚成几类或者筛掉消费异常的学生再看散点。st.sidebar.header(参数调节) n_clusters st.sidebar.slider(聚类档位数, min_value3, max_value8, value5) filter_ratio st.sidebar.slider(食堂消费占比下限, 0.0, 1.0, 0.0) features_subset features[features[canteen_ratio] filter_ratio] features_subset, model assess_economic(features_subset, n_clustersn_clusters) fig plot_cluster_scatter(features_subset) st.plotly_chart(fig, use_container_widthTrue)这里注意如果预留了聚类档位数滑块聚类中心到经济等级的映射也要相应动态调整否则滑块拖到 6 类时等级映射逻辑还是 5 类的展示就会乱掉。一个简单做法是把排名映射写在函数里用pd.cutrank实现动态分档。4.4 本地运行的正确姿势命令是streamlit run app.py第一次运行会弹浏览器默认端口 8501。如果要换端口加--server.port 8888。如果跑起来发现页面下方提示找不到入口脚本多半是当前目录不对需要在项目根目录执行命令而不是在别的目录下指路径。另外 Streamlit 每次保存代码会自动热重载但它只能识别入口脚本 app.py 的变化core 目录下的改动必须手动刷新浏览器才会生效这个细节容易让人误以为代码没保存。5. 避坑与排查五个真实翻车点与解决方式5.1 现象streamlit run app.py 报 ModuleNotFoundError报错信息是 No module named sklearn 或 No module named plotly但明明装过这些库。原因基本是环境混用了比如 Linux 上 pip 和 pip3 指向不同版本或者系统里有多个 Python 解释器pip install 装到了旧环境里。解决是直接在项目根目录一次性装齐六件套再用 python -c 验证pip install pandas numpy matplotlib seaborn scikit-learn streamlit plotly python -c import sklearn; print(sklearn.__version__)如果是 conda 环境先 conda activate 再执行命令。这个坑 90% 出在多个 Python 环境并存的机器上血泪经验是装完依赖后不要急着跑项目先验证每依赖能 import 成功。5.2 现象图表标题和坐标轴标签全是方块乱码中文显示成方块这是 matplotlib 和部分 plotly 渲染环境缺少中文字体导致的。Windows 上常见的是 SimHei 字体缺失或未生效。解决是在 visualizer.py 或入口脚本里显式指定中文字体import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False第二个 rcParams 是处理负号显示异常的很多同学只配字体忘记配负号坐标轴上出现方块负号整个图就毁了。在 Linux 服务器上跑的话还要先安装中文字体包否则 SimHei 本身就不存在。5.3 现象FileNotFoundError 读不到 data1.csv报错显示 No such file or directory: data/data1.csv。原因是在项目根目录之外的路径执行了 python 或 streamlit相对路径解析不到 data 目录。解决是先进项目根目录再启动。更推荐的方式是在 README 里写死“必须在项目根目录运行”而不是在代码里拼绝对路径因为换机器后绝对路径也会失效。如果实在要改可以把路径配置下沉到 configs/settings.py 里统一管理至少换环境时只改一个文件。5.4 现象聚类结果和文档里的截图对不上文档截图是作者用某个版本的 data2 跑出来的或者聚类参数不同导致演示时散点分布和论文里的图不一致。解决是固定随机种子并核对样本量。如果数据清洗后样本量和文档对不上优先检查是不是把 data1 和 data2 都拼进去了而文档只跑了其中一批。答辩遇到这个问题时直接说“文档基于批次一演示基于全量数据”一般能圆过去但最好提前统一口径把文档也更新成全量数据的结果。5.5 现象页面一加载就转圈拖滑块几秒才响应每次交互触发全量重跑数据清洗和特征构造没走缓存是最大原因。另外把原始长表直接 st.dataframe 渲染几万行数据也会拖垮浏览器。解决是给load_and_prepare包上st.cache_data并且只展示聚合特征表的前 100 行。数据量实在大时可以在预处理阶段把原始表按学生抽样后再渲染反正答辩展示的是形态不是每条明细。6. 进阶把分析结果变成论文插图和答辩演示的三个技巧6.1 plotly 导出高清静态图答辩文档需要静态图不能截图。截图分辨率取决于屏幕缩进论文里很容易发虚。plotly figure 可以一键导出高清 PNGfig plot_cluster_scatter(features) fig.write_image(fig_cluster.png, scale2, width1400, height900)write_image 需要安装 kaleido 包pip install kaleido即可。这个是 plotly 的静态渲染器装不上时会有提示装完重启内核生效。scale2 是两倍分辨率插入论文 300dpi 也不糊。6.2 聚类结果落表让论文有据可查把每个学生的消费特征和经济等级导出成 CSV可以放进论文附录也可以作为原始数据复核的依据。features.to_csv(student_economic_assessment.csv, indexFalse, encodingutf-8-sig)utf-8-sig 是为了让 Excel 直接打开 CSV 不乱码。论文里可以作为“实证数据”展示评委问起来也拿得出可追溯的明细比纯口头描述有说服力。6.3 给答辩留一个可解释口径答辩时最怕被问“你怎么证明聚类结果是正确的”。我的习惯做法是在文档里增加一张“各层级消费特征均值表”把每一档的平均消费、食堂占比、活跃天数列出来从业务角度解读每个档位的行为特征。比如“宽裕档日均消费高但食堂占比低说明外卖和超市消费占大头”这个解释比单说“SSE 下降了 20%”更有说服力因为评委更在意业务逻辑闭环。这个项目我拆的时候最有感触的就是数据清洗和特征构造占了整个工作量的一大半模型反而最简单。从那以后我每次拿校园卡类数据集都强制自己先把字段类型、缺失值分布、重复率这三项打印一遍再动模型。整个过程虽琐碎但确实是整个项目让人信服的地基。希望帮到你。本文还有配套的精品资源点击获取
返回列表