简介:面向高校学生与编程初学者的校园消费行为分析项目,紧密贴合期末大作业与课程设计场景。项目围绕学生校园消费数据展开,涵盖数据预处理、特征提取、行为分析、模型构建与可视化等完整流程;多个脚本按任务拆分,自带的项目数据文件可保证开箱即用,无需修改即可运行,适合快速理解并完成建模作业。压缩包共六个文件,以五个脚本和一个数据压缩包为主,整体大小约四点八九兆,结构紧凑,便于按模块阅读与试验。目前已有六百一十八人学习下载,获得一定关注。通过该项目,使用者不仅能获得可直接运行的完整建模方案,还能学习任务拆解、脚本设计、数据分析和结果呈现的具体方法,为后续课题设计或竞赛实践提供有力参考。
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', encoding='gbk') print(df.shape) print(df.dtypes) print(df.head(10))逻辑说明:先确认数据量级和列类型,如果交易时间列被读成了object而非datetime,后面所有时间维度分析都会出问题。encoding先试gbk,因为校园一卡通系统导出的数据十有八九是GBK编码,直接utf-8读会报错或者出现乱码列名。
参数说明:如果读入时encoding报错,就依次尝试encoding='gb18030'、encoding='utf-8'。另外parse_dates=['交易时间']可以在read_csv阶段直接把时间列转成datetime类型,省得后面再pd.to_datetime()一步。
2.2 时间字段解析与消费金额异常值处理
一卡通流水的脏数据集中在两个地方:时间字段格式不统一、消费金额出现负数或极端值。负数通常是退款记录,不能直接删,要单独标记;极端值比如单笔超过500元的食堂消费,大概率是批量充值或者机器误刷。
df['交易时间'] = pd.to_datetime(df['交易时间'], format='%Y-%m-%d %H:%M:%S', errors='coerce') df['消费金额'] = pd.to_numeric(df['消费金额'], errors='coerce') # 退款记录单独标记,不直接删 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)}")逻辑说明:errors='coerce'会把解析失败的时间转成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['消费时段'], values=df['消费金额'], aggfunc='sum').fillna(0) pivot_time['总消费'] = pivot_time.sum(axis=1) 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', index=False, encoding='utf-8-sig')逻辑说明:agg字典里可以写多种聚合函数,这里统计了总量、均值、波动三个维度。时段占比特征是把流水升到用户级的核心技巧——它描述的是"行为偏好"而非"消费多少",后续聚类能分出不同类型的人。
参数说明:encoding='utf-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_clusters=k, random_state=42, n_init=10) labels = km.fit_predict(X_scaled) sil = silhouette_score(X_scaled, labels) silhouette_scores.append(sil) print(f"k={k}, silhouette={sil:.4f}") best_k = np.argmax(silhouette_scores) + 2 print(f"最优聚类数: {best_k}")逻辑说明:轮廓系数是选K的常用手段,但要注意它只衡量紧密度和分离度,不代表业务可解释性。有些K值轮廓系数不高但聚类结果业务上很好讲,所以大作业里通常选"轮廓系数次优但能讲出各群特征"的K。
参数说明:random_state=42固定随机种子保证可复现,n_init=10是KMeans里避免局部最优的常用设置。如果跑出来的轮廓系数整体都低于0.3,先回去检查特征是不是有大量冗余——比如消费次数和消费天数是高度相关的。
4. 聚类建模与结果可视化:把学生分成几类人,才算把作业做深
4.1 KMeans聚类与聚类中心解读
选中K值后,正式建模并输出每个群体的特征画像。聚类中心是业务解释的钥匙——每一行的数值代表该类学生在各个特征维度上的平均水平。
final_k = best_k if best_k > 2 else 3 km = KMeans(n_clusters=final_k, random_state=42, n_init=10) user_features['聚类标签'] = km.fit_predict(X_scaled) # 聚类中心转为原始尺度 centers = scaler.inverse_transform(km.cluster_centers_) centers_df = pd.DataFrame(centers, columns=feature_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_init=20跑更充分。聚类中心还原后如果出现负的金额占比,检查是否用了带负值的特征标准化方式,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( r=norm_data.loc[label].values, theta=categories, fill='toself', name=f'群体{label}' )) fig.update_layout( polar=dict(radialaxis=dict(visible=True, range=[0, 1])), showlegend=True, title='不同学生群体的消费行为特征对比' ) fig.write_html('聚类结果雷达图.html')逻辑说明:这个图解决的核心问题是"聚类结果怎么向老师解释"。四个时段占比加两个消费强度维度,能一眼看出群体差异——比如群体0可能早餐占比突出、总消费低,可以解释为"规律作息的省钱党";群体1夜宵占比高、消费波动大,是"夜猫子型"。
参数说明:fill='toself'是雷达图填充半透明色的设置,多个群体叠加时建议开启。归一化只对量纲差异大的数值特征做,占比类本身已经是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', encoding='utf-8-sig')逻辑说明:画像表的业务命名需要结合原始数据做交叉验证——比如看群体1里是不是有较多的高年级学生、群体3是不是女生偏多。这种交叉分析可以进一步挖掘,但在大作业阶段,基于消费特征本身的命名已经足够支撑结论。
参数说明:round(2)保留两位小数,报告里直接贴CSV转Excel的表即可。画像表字段不要贪多,选最能区分群体的5到6个特征就够了。
4.4 避坑与排查:跑这个项目最容易翻车的5个问题
坑1:read_csv读入时GBK编码报错。现象是UnicodeDecodeError: 'gbk' codec can't decode byte。原因是一卡通导出的CSV混合了UTF-8编码的列名和GBK编码的内容,单用一种编码解不开。解决是用encoding='gb18030'替代gbk,它是GBK超集,兼容性更好;还不行就errors='replace'先把文件读进来,后面再清洗乱码字段。
坑2:时间字段解析后出现大量NaT。现象是pd.to_datetime之后isna()比例超过5%。原因是部分记录的格式是2024/06/01 12:30而另一部分是2024-06-01 12:30:00,混合格式导致解析失败。解决是先df['交易时间'].str.upper()看格式分布,再用format='mixed'参数让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='学号', how='left') user_features = user_features.merge(weekend_avg, on='学号', how='left') 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}")这三个改动加进去,报告的分析深度立刻不一样。我自己做这类项目时踩过的最大教训是:聚类结果出来不急着写结论,先做交叉验证——用你已经知道的属性(性别、年级、专业)去核对聚类有没有分出符合直觉的群体,如果完全对不上,先回去查特征有没有算错,而不是硬编故事。从那以后我每次跑聚类都强制走一遍"聚类 → 分组统计 → 业务解释 → 已知标签交叉验证"的流程,保证结果不是黑匣子。希望这份拆解能帮你的大作业顺利落地,少熬几个夜。
本文还有配套的精品资源,点击获取