ARTICLE DETAIL

资讯详情

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

基于Python的学生校园消费行为分析与聚类建模实战

基于Python的学生校园消费行为分析与聚类建模实战 简介面向高校学生与编程初学者的校园消费行为分析项目紧密贴合期末大作业与课程设计场景。项目围绕学生校园消费数据展开涵盖数据预处理、特征提取、行为分析、模型构建与可视化等完整流程多个脚本按任务拆分自带的项目数据文件可保证开箱即用无需修改即可运行适合快速理解并完成建模作业。压缩包共六个文件以五个脚本和一个数据压缩包为主整体大小约四点八九兆结构紧凑便于按模块阅读与试验。目前已有六百一十八人学习下载获得一定关注。通过该项目使用者不仅能获得可直接运行的完整建模方案还能学习任务拆解、脚本设计、数据分析和结果呈现的具体方法为后续课题设计或竞赛实践提供有力参考。1. 基于Python的学生校园消费行为分析这份期末大作业到底藏了多少东西一卡通流水数据是校园里最容易拿到、也最容易被做浅的行为数据。多数人交上去的作业止步于统计了食堂人均消费、画了张条形图而这份基于Python的学生校园消费行为分析期末大作业把整个分析链路做完整了从原始流水清洗、特征工程到聚类建模和可视化解释是典型的数据分析课程设计完整范式。压缩包里是五个按任务拆分的脚本——task1_X.py、task2_X1.py、task2_X2.py、task3_X_1.py、task3_X_2.py外加一份项目数据.zip结构上把数据探索 → 特征工程 → 建模三个阶段分得清清楚楚。适合正在做期末大作业、需要参考完整代码框架和数据预处理思路的同学如果你只想抄一个能跑通的脚本直接跑task1到task3也能拿到完整输出结果。这套资源的价值不在于算法多前沿而在于流程完整、注释规范、每个task的结果都能对应上作业要求里的得分点。下面我按实际拆解的顺序把每个脚本做了什么、怎么改参数、有哪些坑一次说清楚。2. 数据预览与预处理先搞清一卡通流水里有什么脏东西2.1 数据集字段结构与读取方式拿到项目数据.zip解压后里面是典型的校园一卡通消费流水表核心字段基本逃不出这几个学号、交易时间、消费金额、消费窗口/商户名称、消费类型。用pandas读进来之后第一步不是急着算均值而是先看shape和dtypes——这一步能筛掉一半的坑。import pandas as pd import numpy as np df pd.read_csv(consumption.csv, encodinggbk) print(df.shape) print(df.dtypes) print(df.head(10))逻辑说明先确认数据量级和列类型如果交易时间列被读成了object而非datetime后面所有时间维度分析都会出问题。encoding先试gbk因为校园一卡通系统导出的数据十有八九是GBK编码直接utf-8读会报错或者出现乱码列名。参数说明如果读入时encoding报错就依次尝试encodinggb18030、encodingutf-8。另外parse_dates[交易时间]可以在read_csv阶段直接把时间列转成datetime类型省得后面再pd.to_datetime()一步。2.2 时间字段解析与消费金额异常值处理一卡通流水的脏数据集中在两个地方时间字段格式不统一、消费金额出现负数或极端值。负数通常是退款记录不能直接删要单独标记极端值比如单笔超过500元的食堂消费大概率是批量充值或者机器误刷。df[交易时间] pd.to_datetime(df[交易时间], format%Y-%m-%d %H:%M:%S, errorscoerce) df[消费金额] pd.to_numeric(df[消费金额], errorscoerce) # 退款记录单独标记不直接删 df[is_refund] df[消费金额] 0 df.loc[df[is_refund], 消费金额] df.loc[df[is_refund], 消费金额].abs() # 金额分位数查看判断极端值 print(df[消费金额].describe()) q99 df[消费金额].quantile(0.99) df_outlier df[df[消费金额] q99] print(f超过99分位数的记录数: {len(df_outlier)})逻辑说明errorscoerce会把解析失败的时间转成NaT后续用dropna(subset[交易时间])处理退款转成绝对值是为了后续聚合总消费时不出负数抵消。分位数描述能快速看到金额分布是否有长尾。参数说明quantile(0.99)这个阈值不是拍脑袋定的一卡通食堂消费的正常区间在0.5到50元之间如果99分位数超过100说明数据里混入了非餐饮消费需要结合商户名称字段做进一步过滤。2.3 消费时段衍生早中晚餐时段切分预处理阶段最有价值的步骤是给每条流水打上时段标签。校园消费行为分析的核心维度就是时段——早餐集中在7点到9点、午餐11点到13点、晚餐17点到19点夜宵另算。这个特征直接影响后续聚类效果。def get_time_period(hour): if 6 hour 10: return 早餐 elif 10 hour 15: return 午餐 elif 15 hour 20: return 晚餐 elif 20 hour 24: return 夜宵 else: return 深夜 df[消费时段] df[交易时间].dt.hour.apply(get_time_period) df[消费日期] df[交易时间].dt.date df[星期] df[交易时间].dt.dayofweek # 周一0 print(df[消费时段].value_counts())逻辑说明时段切分边界不是固定的如果数据里食堂营业时间有差异需要先画个消费小时分布直方图再定边界。这里6-10点算早餐是因为部分食堂6点半开门10点切午餐是考虑到上午下课早的院系。参数说明dt.dayofweek返回0到6的整数后续可以按工作日/周末分组对比这是大作业里一个稳稳的得分点。消费日期字段用于计算每人每天消费次数和日消费金额。3. 特征工程与用户画像从原始流水到能用聚类喂进去的用户特征表3.1 用户级特征聚合消费频次、金额、时段偏好task2_X1.py和task2_X2.py的核心工作就是把流水表聚合成每个学生一行的用户特征表。这一步直接决定建模质量也是区分大作业优劣的分水岭——只交流水统计的拿及格分做了用户画像的拿优秀分。user_features df.groupby(学号).agg( 总消费金额(消费金额, sum), 消费次数(消费金额, count), 日均消费(消费金额, mean), 消费天数(消费日期, nunique), 平均每笔金额(消费金额, mean), 金额标准差(消费金额, std) ).reset_index() # 时段偏好每人早餐消费占本人总消费的比例 pivot_time pd.crosstab(df[学号], df[消费时段], valuesdf[消费金额], aggfuncsum).fillna(0) pivot_time[总消费] pivot_time.sum(axis1) for col in [早餐, 午餐, 晚餐, 夜宵]: pivot_time[f{col}占比] pivot_time[col] / pivot_time[总消费] user_features user_features.merge(pivot_time[[早餐占比, 午餐占比, 晚餐占比, 夜宵占比]], on学号) user_features.to_csv(user_features.csv, indexFalse, encodingutf-8-sig)逻辑说明agg字典里可以写多种聚合函数这里统计了总量、均值、波动三个维度。时段占比特征是把流水升到用户级的核心技巧——它描述的是行为偏好而非消费多少后续聚类能分出不同类型的人。参数说明encodingutf-8-sig是为了让Excel打开CSV不乱码这个细节在交报告时很加分。fillna(0)处理的是某学生从未在夜宵时段消费的情况不填充的话后面算占比会出现NaN。3.2 消费稳定性与异常消费识别除了基础的频次金额task2_X2.py里还加了消费稳定性和异常行为的刻画。一卡通数据里最容易识别出的异常行为是短期高频消费——比如半小时内在多个窗口连续刷卡可能是代刷行为。# 每笔消费与上一笔的时间差按学号分组计算 df df.sort_values([学号, 交易时间]) df[上一笔时间] df.groupby(学号)[交易时间].shift(1) df[时间间隔] (df[交易时间] - df[上一笔时间]).dt.total_seconds() / 60 # 短时间跨窗口消费间隔小于5分钟且窗口不同 df[is_short_interval] (df[时间间隔] 5) (df[时间间隔].notna()) # 用户级特征短间隔消费占比 user_short df.groupby(学号)[is_short_interval].mean().rename(短间隔占比) user_features user_features.merge(user_short, on学号) # 日消费波动每人每天消费金额的标准差均值 daily_std df.groupby([学号, 消费日期])[消费金额].std().groupby(学号).mean().rename(日消费波动) user_features user_features.merge(daily_std, on学号)逻辑说明shift(1)是拿到上一行的交易时间配合sort_values实现组内时间差计算。短间隔跨窗口消费占比高的人可能是代刷或集中采购这个特征在聚类里能把代购党和普通学生分开。参数说明5分钟这个阈值是经验值实际项目中可以先画个时间间隔的分布直方图看波谷在哪里再定。dropna要处理第一笔交易没有上一笔时间的情况这里用notna()规避了。3.3 特征标准化与聚类输入准备特征表建好后不能直接喂给聚类算法。消费金额和占比类特征的量纲差了几个数量级不标准化的话聚类结果完全被金额主导。task3_X_1.py里用的是StandardScaler但我自己跑这类数据时更倾向于RobustScaler——一卡通数据里总有充值退款造成的极端值RobustScaler对中位数和四分位数的依赖让结果更稳。from sklearn.preprocessing import StandardScaler, RobustScaler from sklearn.cluster import KMeans feature_cols [总消费金额, 消费次数, 日均消费, 消费天数, 平均每笔金额, 金额标准差, 早餐占比, 午餐占比, 晚餐占比, 夜宵占比, 短间隔占比, 日消费波动] # 标准化RobustScaler对异常值更稳健 scaler RobustScaler() X_scaled scaler.fit_transform(user_features[feature_cols]) # 轮廓系数选K from sklearn.metrics import silhouette_score silhouette_scores [] for k in range(2, 9): km KMeans(n_clustersk, random_state42, n_init10) labels km.fit_predict(X_scaled) sil silhouette_score(X_scaled, labels) silhouette_scores.append(sil) print(fk{k}, silhouette{sil:.4f}) best_k np.argmax(silhouette_scores) 2 print(f最优聚类数: {best_k})逻辑说明轮廓系数是选K的常用手段但要注意它只衡量紧密度和分离度不代表业务可解释性。有些K值轮廓系数不高但聚类结果业务上很好讲所以大作业里通常选轮廓系数次优但能讲出各群特征的K。参数说明random_state42固定随机种子保证可复现n_init10是KMeans里避免局部最优的常用设置。如果跑出来的轮廓系数整体都低于0.3先回去检查特征是不是有大量冗余——比如消费次数和消费天数是高度相关的。4. 聚类建模与结果可视化把学生分成几类人才算把作业做深4.1 KMeans聚类与聚类中心解读选中K值后正式建模并输出每个群体的特征画像。聚类中心是业务解释的钥匙——每一行的数值代表该类学生在各个特征维度上的平均水平。final_k best_k if best_k 2 else 3 km KMeans(n_clustersfinal_k, random_state42, n_init10) user_features[聚类标签] km.fit_predict(X_scaled) # 聚类中心转为原始尺度 centers scaler.inverse_transform(km.cluster_centers_) centers_df pd.DataFrame(centers, columnsfeature_cols) centers_df[人数] user_features[聚类标签].value_counts().sort_index().values centers_df[占比] centers_df[人数] / len(user_features) * 100 print(centers_df.round(2).T)逻辑说明inverse_transform把标准化后的聚类中心还原成原始量纲这样写报告时能直接说第0类学生月均消费XXX元、早餐占比只有5%而不是输出一堆-1.2、0.8这种评委看不懂的标准化数值。参数说明小规模数据几百行时KMeans没问题如果数据量过万可以加n_init20跑更充分。聚类中心还原后如果出现负的金额占比检查是否用了带负值的特征标准化方式RobustScaler的inverse_transform不会出现这种情况。4.2 雷达图与分群对比可视化表达聚类的业务含义task3_X_2.py里的核心是出图——聚类结果没有可视化等于白做。雷达图是展示多维度群体差异的最佳选择。用plotly画交互式雷达图一个类一个trace对比直观且能直接截图放报告。import plotly.graph_objects as go # 雷达图每类学生的时段占比对比 categories [早餐占比, 午餐占比, 晚餐占比, 夜宵占比, 日均消费, 消费次数] norm_data user_features.groupby(聚类标签)[categories].mean() # 为了雷达图比例协调日均消费和消费次数做min-max归一 for col in [日均消费, 消费次数]: norm_data[col] (norm_data[col] - norm_data[col].min()) / (norm_data[col].max() - norm_data[col].min()) fig go.Figure() for label in norm_data.index: fig.add_trace(go.Scatterpolar( rnorm_data.loc[label].values, thetacategories, filltoself, namef群体{label} )) fig.update_layout( polardict(radialaxisdict(visibleTrue, range[0, 1])), showlegendTrue, title不同学生群体的消费行为特征对比 ) fig.write_html(聚类结果雷达图.html)逻辑说明这个图解决的核心问题是聚类结果怎么向老师解释。四个时段占比加两个消费强度维度能一眼看出群体差异——比如群体0可能早餐占比突出、总消费低可以解释为规律作息的省钱党群体1夜宵占比高、消费波动大是夜猫子型。参数说明filltoself是雷达图填充半透明色的设置多个群体叠加时建议开启。归一化只对量纲差异大的数值特征做占比类本身已经是0到1不需要处理。4.3 不同聚类群体的画像解读给每类人起个名字聚类结果落地到大作业报告里需要给每个群体起业务标签。这一步是将算法结果转化为可读结论的关键——评委不关心你用了什么库关心的是你能不能从数据里看出一类人。以实际跑出来的四类结果为例典型的聚类画像如下。群体0消费笔数多、单笔金额低、午餐和晚餐占比均衡定义平价规律型群体1总消费金额高、各时段占比分散、消费天数多定义高活跃重度消费者群体2夜宵占比显著高于均值、金额标准差大定义夜宵型/不规律型群体3早餐占比高、日均消费低、消费天数少定义经济自律型。# 把画像结果输出成报告表 profile_summary centers_df[[总消费金额, 消费次数, 日均消费, 早餐占比, 晚餐占比, 夜宵占比]].round(2) profile_summary[人数占比] centers_df[占比].round(1) profile_summary.columns [月均消费(元), 消费次数, 日均消费(元), 早餐占比, 晚餐占比, 夜宵占比, 人数占比] profile_summary.to_csv(聚类画像表.csv, encodingutf-8-sig)逻辑说明画像表的业务命名需要结合原始数据做交叉验证——比如看群体1里是不是有较多的高年级学生、群体3是不是女生偏多。这种交叉分析可以进一步挖掘但在大作业阶段基于消费特征本身的命名已经足够支撑结论。参数说明round(2)保留两位小数报告里直接贴CSV转Excel的表即可。画像表字段不要贪多选最能区分群体的5到6个特征就够了。4.4 避坑与排查跑这个项目最容易翻车的5个问题坑1read_csv读入时GBK编码报错。现象是UnicodeDecodeError: gbk codec cant decode byte。原因是一卡通导出的CSV混合了UTF-8编码的列名和GBK编码的内容单用一种编码解不开。解决是用encodinggb18030替代gbk它是GBK超集兼容性更好还不行就errorsreplace先把文件读进来后面再清洗乱码字段。坑2时间字段解析后出现大量NaT。现象是pd.to_datetime之后isna()比例超过5%。原因是部分记录的格式是2024/06/01 12:30而另一部分是2024-06-01 12:30:00混合格式导致解析失败。解决是先df[交易时间].str.upper()看格式分布再用formatmixed参数让pandas自动推断或者分两次解析后合并。坑3聚类轮廓系数非常低低于0.2。现象是K从2到8全部低于0.2选不出K。原因是特征里堆了大量相关性高的变量消费次数和消费天数、总消费和日均消费之间高度共线等于给KMeans喂了重复信息。解决是先算相关矩阵删掉相关系数超过0.8的特征保留业务解释性最强的那个。坑4聚类群里出现空类。现象是value_counts()里有某个类只有个位数样本。原因是K选大了或者数据本身分布偏斜。解决是先用降维可视化看数据分布如果样本集中在某几个区域就把K降到3或4而不是硬选轮廓系数最大的K。坑5雷达图数值看上去全是0。现象是plotly雷达图里多个群体的形状几乎没有差异。原因是标准化后的数据没有转回原始尺度或者归一化时除以了包含0的标准差。解决是检查归一化代码是否对每列做了(x - min) / (max - min)并在画图前print(norm_data.head())确认数据有区分度。5. 作业加分技巧怎么把这份代码改出你自己的分析深度如果你只是把代码跑通交上去起点分有了但拿不到优秀。我自己带过不少类似项目三个改动方向可以显著提高作业质量且不需要额外学新模型。第一个改动是加一个维度交叉分析比如把聚类标签和是否周末消费交叉看每类群体在工作日和周末的消费差异。代码层面只需在特征表里多算两个列——工作日日均消费和周末日均消费——然后重新聚类或者直接用已有的聚类标签分组做对比表。这个改动的价值在于它展示了你不只会跑KMeans还会设计分析维度。# 工作日与周末分消费对比 df[is_weekend] df[星期].isin([5, 6]) weekday_avg df[~df[is_weekend]].groupby(学号)[消费金额].mean().rename(工作日日均) weekend_avg df[df[is_weekend]].groupby(学号)[消费金额].mean().rename(周末日均) user_features user_features.merge(weekday_avg, on学号, howleft) user_features user_features.merge(weekend_avg, on学号, howleft) user_features[[工作日日均, 周末日均]] user_features[[工作日日均, 周末日均]].fillna(0)第二个改动是聚类之前加一个PCA或者UMAP可视化让报告里有一张二维散点图不同颜色代表不同群体的图。这个图比雷达图更直观评委一眼就能看出确实分了四类。代码就是把X_scaled降到二维再画scatter。第三个改动是效果验证对聚类结果做简单统计检验比如用卡方检验验证不同类别的性别/年级构成是否有显著差异。这一步能体现你的数据分析素养且代码量极短——几行scipy.stats.chi2_contingency就够了。from scipy.stats import chi2_contingency # 假设有一个性别列与聚类标签做交叉表 cross_tab pd.crosstab(user_features[性别], user_features[聚类标签]) chi2, p_value, dof, expected chi2_contingency(cross_tab) print(f卡方值: {chi2:.2f}, p值: {p_value:.4f})这三个改动加进去报告的分析深度立刻不一样。我自己做这类项目时踩过的最大教训是聚类结果出来不急着写结论先做交叉验证——用你已经知道的属性性别、年级、专业去核对聚类有没有分出符合直觉的群体如果完全对不上先回去查特征有没有算错而不是硬编故事。从那以后我每次跑聚类都强制走一遍聚类 → 分组统计 → 业务解释 → 已知标签交叉验证的流程保证结果不是黑匣子。希望这份拆解能帮你的大作业顺利落地少熬几个夜。本文还有配套的精品资源点击获取
返回列表