简介:这是一套围绕学生校园消费行为分析题的Python建模项目,专为高校期末大作业、课程设计等场景打造。资源包共6个文件,包含5个Python脚本和1个原始数据压缩包,脚本按照不同任务模块编写,如基本数据探查、消费特征提取、关联分析或预测建模等,下载解压后无需额外修改即可运行,并可直接作为答辩材料的基础工程。整个zip体积仅4.89MB,目录层级简洁,便于按任务顺序理解代码逻辑。目前已有618人学习下载,是同类作业中热度较高的参考模板。读者可获得一整套可复用的分析流程框架、数据预处理与建模代码、任务拆解思路及结果展示方式,尤其适合希望快速搭建项目骨架、降低排错成本的同学。
1. 校园消费行为分析到底在做什么?先把期末大作业的验收点摆出来
晚上十点半的食堂刷卡机前,排着刚下晚课的学生,一张一卡通流水背后是几十万条可以建模的数据。这份期末大作业最容易踩的误区是:以为把数据丢给 sklearn,跑出一个聚类结果就完事。真实的验收点有三个——能不能把原始流水清洗成一张可分析的特征表,能不能用合适的建模方法分出有业务含义的人群,以及能不能在答辩现场十分钟内把「数据—特征—模型—结论」这条链路讲清楚。这里的 Python 建模落点不是比拼准确率,而是比拼可解释性,适合正在做数据分析课设、数学建模入门、或者想认真完成期末大作业的同学。
2. 数据清洗与特征工程:把一卡通流水变成能建模的二维表
2.1 拿到原始流水后先别急着建模,先检查字段和样本
校园一卡通消费流水在不同学校的导出格式略有差异,但核心字段基本稳定:卡号或学号、交易时间、交易金额、商户名称、商户类型、收支标志。最好的习惯是先用 Excel 或 pandas 打开前 100 行,肉眼看一遍数据长得什么样,而不是直接开始写聚合代码。曾经有人拿到数据后直接按卡号分组求均值,结果发现「人均消费」只有 0.15 元,一查才知道有大量刷卡充值记录混在消费记录里,充值金额都是整百,把均值拉低了。
我一般会先写一段极简的检查代码,输出数据的行数、唯一卡数、时间范围和金额分布,这几个指标能快速暴露数据集的真实规模和分析难度。重点是看三段信息:交易时间是不是统一格式、金额列有没有负数、同一张卡同一秒同一商户是否存在重复记录。这三类问题如果不处理,后续所有统计口径都会偏,而且偏得悄无声息。
import pandas as pd df = pd.read_csv('card_flow.csv', parse_dates=['trade_time']) print('总记录数:', len(df)) print('唯一卡号数:', df['card_no'].nunique()) print('时间范围:', df['trade_time'].min(), '->', df['trade_time'].max()) print('金额描述:') print(df['amount'].describe()) print('负数金额条数:', (df['amount'] < 0).sum()) print('重复记录条数:', df.duplicated().sum())这段代码先读数据,再把交易时间解析为 datetime 类型。nunique 用于数出去重后的卡号数量,duplicated 用来判断全字段重复的行有多少。输出结果之后,你会对本次建模的数据形态有个整体印象:数据量是几万还是几十万,时间跨度是三个月还是一整年,负数是偶尔几笔还是大面积存在。先看全貌再动手,避免后续在错误假设上堆代码。
2.2 处理退款负数、充值记录和重复刷卡
常见的坑有两个方向。第一个方向是负金额:食堂窗口操作失误后的退款、超市退货、洗衣房余额退回,都会造成负数交易。如果分析目标是「消费行为」,我建议把负数交易单独拎出来,只在退款分析里用,不混进消费频率和消费金额的计算。第二个方向是充值记录:一卡通充值本质是转账,不是消费,通常交易类型字段里会有「充值」「消费」的标记,没有标记的话可以用商户类型或者金额规律来过滤。
删除或标记这些记录后,再处理重复刷卡。有些食堂闸机数据会同时写入两条相同记录,时间戳精确到秒且金额一样,这种情况直接用 drop_duplicates 删掉。但要注意,如果刷完卡发现有退款,退款记录和原消费记录成对出现,此时不能简单按全字段去重,否则会把正常的一进一出也删掉。处理时我通常只对「卡号 + 商户 + 时间 + 金额」四字段完全相同的行去重,保留第一次出现。
df = df[df['amount'] > 0].copy() # 先排除退款和充值,只看正向消费 df_clean = df.drop_duplicates( subset=['card_no', 'merchant', 'trade_time', 'amount'], keep='first' ) print('清洗后记录数:', len(df_clean))这里的逻辑是先假设金额大于 0 都是消费,需要你确认自己的数据里有没有负的消费、有没有正数充值。drop_duplicates 的 subset 参数非常关键,它指定了「按哪些列判断重复」,避免把同一时间在同一个商户买两笔相同金额商品的正常行为误删。keep 参数用 first 保留第一条,在时间序列场景下等于默认选择先发生的记录。
2.3 时间特征拆分:星期、小时、早晚高峰
一卡通流水里的交易时间是最有建模价值的字段,它决定了你能不能在报告里画「一天消费曲线」和「一周消费热力图」。清洗完成后,我会把时间拆成三个新列:小时、星期、是否为工作日。小时用来判断早餐、午餐、晚餐、夜宵的高峰分布,星期用来分析周末和上学日的差异,工作日标记则帮助后续分群时识别作息规律。
dt = df_clean['trade_time'] df_clean['hour'] = dt.dt.hour df_clean['weekday'] = dt.dt.weekday # 0=周一, 6=周日 df_clean['is_workday'] = df_clean['weekday'].apply(lambda x: 1 if x < 5 else 0)dt.hour 和 dt.weekday 是 pandas 的 datetime 访问器,效率高且不容易写错。weekday 范围是 0 到 6,0 代表周一,6 代表周日。is_workday 是自定义规则,默认周一至周五算工作日。如果你的学校周六还有课,或者周五下午就放假,这个标记要按自己学校的校历调整,不能照抄默认值。
2.4 构建「以人为单位」的消费特征表
建模时最常用的主体是「人」,也就是每张卡对应一行特征。把流水按卡号分组,统计消费总金额、总次数、平均金额、消费活跃天数、高频时段等字段,最终得到一张宽表。这张表就是后续 KMeans、DBSCAN 的输入。
daily = df_clean.groupby(['card_no']).agg( total_amount=('amount', 'sum'), total_count=('amount', 'count'), avg_amount=('amount', 'mean'), active_days=('trade_time', 'nunique'), avg_hour=('hour', 'mean'), ) daily['count_per_active_day'] = daily['total_count'] / daily['active_days'] print(daily.head())groupby 后面的 agg 是 pandas 推荐的聚合写法,每一行指定新列名、原始列和聚合函数。total_amount 是总消费额,total_count 是总刷卡次数,active_days 是去重后的活跃天数,用 nunique 统计。avg_hour 是消费时间的均值,它有一个隐患:跨午夜的作息会被平均到奇怪的位置,比如晚上 23 点和凌晨 1 点的平均会得到 12 点。这种情况我用「圆形均值」处理,或者干脆把它换成前三个固定时段的消费占比。
2.5 衍生特征:三餐占比、夜间消费比例、消费熵
基础特征只能描述消费多少,不能描述消费习惯。期末答辩时最能打动老师的是「这个簇的人早餐比例明显高」,而不是「这个簇平均消费 12.3 元」。因此我额外构建三类衍生特征。
第一类是三餐和夜宵占比,用小时区间切分。第二类是夜间消费比例,把 23 点到次日 5 点定义为夜宵时段,统计夜宵金额占总金额的比例。第三类是消费熵,衡量一个人刷卡金额的分散程度——如果一个人每顿都是 10 元整,熵很低;如果忽而 3 元忽而 40 元,熵很高,说明饮食结构波动大。
def meal_type(hour): if 6 <= hour < 10: return 'breakfast' elif 10 <= hour < 15: return 'lunch' elif 15 <= hour < 20: return 'dinner' elif 20 <= hour < 24 or hour < 5: return 'night' else: return 'other' df_clean['meal'] = df_clean['hour'].apply(meal_type) meal_cross = pd.crosstab(df_clean['card_no'], df_clean['meal'], normalize='index') def entropy(series): p = series[series > 0] return -(p * np.log(p)).sum()import numpy as np from scipy.stats import entropy as scipy_entropy def cal_entropy_by_group(group): val_counts = group['amount'].value_counts(normalize=True) return scipy_entropy(val_counts)第一个函数按小时区间把每笔交易归入早餐、午餐、晚餐或夜宵。pd.crosstab 配合 normalize='index' 得到每张卡在不同餐段的消费次数占比。第二个函数计算金额分布的熵,我直接调用 scipy.stats.entropy,它接受概率分布数组并返回非负熵值。熵越大代表不同金额的分布越分散,越小代表消费金额集中在少数几个固定档位,比如总是刷 8 元和 12 元套餐。
2.6 标准化:为什么用 StandardScaler,不用 MinMaxScaler
特征表建好后,量纲差异非常明显:消费总金额可能是几千,早餐占比只有 0.1。如果直接把原始值喂给聚类算法,金额会主导距离计算。常见做法是做标准化。StandardScaler 会把每个特征变成均值为 0、方差为 1 的分布,适合 KMeans 和 DBSCAN 这种基于距离的模型。MinMaxScaler 会把数据压缩到 0~1 区间,问题在于它很容易被极端值拉偏,而校园消费里炒饭、夜宵或校外大额支出很容易产生极端值。
from sklearn.preprocessing import StandardScaler feature_cols = ['total_amount', 'total_count', 'avg_amount', 'active_days', 'count_per_active_day', 'breakfast_ratio', 'dinner_ratio', 'night_ratio', 'entropy'] X = daily[feature_cols].fillna(0) scaler = StandardScaler() X_scaled = scaler.fit_transform(X)fillna(0) 是因为有些卡可能没有夜宵记录,占比为空。fit_transform 在这段代码里直接完成了两步操作:fit 计算均值和标准差,transform 应用标准化。等到建模阶段,你只需把 X_scaled 传入聚类算法。注意保留原始特征表 daily,后面画图、打标签、写报告都需要它,而不是直接使用标准化后的矩阵。
3. 建模方案:KMeans、DBSCAN、孤立森林怎么选
3.1 先定目标,再选模型,不要先跑代码
很多期末作业翻车的原因是拿到数据就开始找聚类代码,跑完却讲不清每一类代表什么。建模之前必须回答一个问题:这次分析到底要得出什么结论?如果你的题目是「分析学生消费行为」,最合理的目标是分群画像,把学生分成规律干饭型、高消费夜宵型、低消费节俭型等若干类。如果你的题目是「识别异常消费」,那应该用异常检测模型,而不是聚类。如果你的题目是「根据前两个月消费预测后一个月」,那就进入回归或时间序列的范畴,不是同一套流程。
在这份校园消费分析作业里,最常见也最容易讲清楚的是「聚类 + 画像」。KMeans 能分出消费水平不同的群体,每个群体的特征中心就是画像。DBSCAN 则适合发现不规则形状的群体,还有机会把「每天只刷一顿饭」的极端群体标记为噪声点。孤立森林不是聚类,它的用途是识别偏离正常消费模式的人。建议把它们放在同一份作业里,分别回答「共性分群」和「异常个体」两个问题,报告结构会很完整。
3.2 聚类数怎么定:肘部法则和轮廓系数
KMeans 需要手动指定簇数 k,这个 k 不能拍脑袋。业界用的两个指标分别是 SSE 肘部法则和轮廓系数。SSE 是簇内样本到中心点的距离平方和,k 越大 SSE 越小,但下降到某个点后收益骤减,那个拐点就是肘部。轮廓系数则衡量每个样本和自身簇的相似度,同类别紧凑、不同类别分离时系数高,范围是 -1 到 1,越接近 1 越好。
from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score sse = [] sil = [] k_range = range(2, 9) for k in k_range: km = KMeans(n_clusters=k, random_state=42, n_init=10) labels = km.fit_predict(X_scaled) sse.append(km.inertia_) sil.append(silhouette_score(X_scaled, labels)) for k, s, si in zip(k_range, sse, sil): print(f'k={k}, SSE={s:.1f}, silhouette={si:.4f}')KMeans 的计算受初始中心点影响,所以每一次迭代都指定 random_state=42,保证结果可复现。n_init=10 表示用 10 组不同的初始中心运行算法,最后返回 SSE 最小的一组,避免随机性带来翻车。silhouette_score 在 X_scaled 上计算,取值越大说明簇与簇之间分离越好。我的习惯是优先看肘部拐点,再看 silhouette 是否超过 0.25,如果超过 0.5 反而要警惕特征是否过于简单,比如只靠金额一列劈成了两半。
3.3 用 KMeans 跑通最小建模流程并打上人群标签
确定 k 之后,就可以跑最终的聚类。以 k=4 为例,把聚类结果合并回原始特征表,就能看到每一类人的消费规律。这里还要注意一点:KMeans 聚类是按距离切分的,它不知道食堂、超市、奶茶店的业务含义,打完标签后必须回看特征均值,给每个簇起一个人类看得懂的名字。
k = 4 km = KMeans(n_clusters=k, random_state=42, n_init=10) cluster_labels = km.fit_predict(X_scaled) daily['cluster'] = cluster_labels cluster_profile = daily.groupby('cluster')[feature_cols].mean() print(cluster_profile.round(2))fit_predict 一次性完成训练和预测,返回每个样本的簇编号。groupby 后取均值,得到每类人的平均消费金额、平均活跃天数、早餐占比等。看到数据后再命名:cluster 0 如果平均金额高、活跃天数多、夜宵占比高,就命名为「活跃夜宵型」;cluster 1 如果总金额低、早餐占比高、频次少,就命名为「规律早餐型」。命名是报告的点睛之笔,不要直接写「群组 0」。
3.4 DBSCAN 的取舍:噪点本身就是一种结果
KMeans 会把每个样本都分到一个簇,哪怕这个样本和其他任何样本都不像。DBSCAN 则会把这些样本直接标记为噪点,也就是标签 -1。在校园消费场景里这很有价值:低活跃、偶发消费、作息不规律的学生会在 DBSCAN 中被暴露出来。不过 DBSCAN 对参数非常敏感,两个参数 eps 描述邻域半径,min_samples 描述成为核心对象所需的最少样本数。
from sklearn.cluster import DBSCAN db = DBSCAN(eps=0.8, min_samples=10) db_labels = db.fit_predict(X_scaled) n_clusters_db = len(set(db_labels)) - (1 if -1 in db_labels else 0) print('DBSCAN 发现簇数:', n_clusters_db) print('噪点数量:', (db_labels == -1).sum()) daily['dbscan_label'] = db_labelseps 表示如果某个样本的邻域内至少有 min_samples 个样本,它才会被当作核心点。eps 设太小会把群体拆得零碎,设太大又把所有人都划进一个大簇。对于标准化后的数据,我一般把 eps 设在 0.5 到 1.2 之间,min_samples 设在 10 到 20。这里的技巧是先跑一次,看噪点比例是否在 5%~15%,噪音比例太高说明参数过紧,可以立刻调整。
3.5 用孤立森林找异常消费,不需要任何标签
孤立森林是「无监督异常检测」算法,它不依赖标签,而是随机切分特征空间,异常点因为分布稀疏,只需要很少的切分次数就能被孤立出来。校园消费场景中,它可以识别刷卡金额异常大、消费频率异常低或夜间消费异常多的人。这里要特别强调:异常不等于坏人,可能是校外人员借卡、访客临时消费,也可能是有学生因病长时间不在校。
from sklearn.ensemble import IsolationForest iso = IsolationForest(contamination=0.02, random_state=42) anomaly_labels = iso.fit_predict(X_scaled) daily['anomaly'] = anomaly_labels print(daily['anomaly'].value_counts())contamination 参数表示你预估的数据集中异常值比例,这里设 0.02 也就是假设 2% 的人消费模式异常。fit_predict 返回 1 表示正常,-1 表示异常,注意方向和聚类标签正好相反,写报告前容易看反。IsolationForest 的核心优点是快,几十万行数据也只需要几秒钟。缺点是结果不好解释,它不会告诉你为什么异常,需要回到原始特征表逐个看异常样本的数值。
3.6 验证聚类不是「随机碰运气」
聚类模型的验证是期末答辩时最容易被人追问的环节。老师说「你这个聚类结果会不会换一组初始值就完全变了」,如果你答不上来,前后就会陷入被动。我通常做两个验证:第一个是稳定性验证,把 random_state 改成其他值,多次跑 KMeans,比较每个簇的人数比例是否保持稳定;第二个是样本验证,随机抽取 80% 的样本重新聚类,再把剩余 20% 预测到最近的簇中心,观察整体分布是否一致。
from sklearn.model_selection import train_test_split X_train, X_test = train_test_split(X_scaled, test_size=0.2, random_state=7) km_temp = KMeans(n_clusters=4, random_state=0, n_init=10).fit(X_train) train_labels = km_temp.predict(X_train) test_labels = km_temp.predict(X_test) print(pd.Series(train_labels).value_counts(normalize=True).sort_index()) print(pd.Series(test_labels).value_counts(normalize=True).sort_index())这里用 train_test_split 把样本切分成 80% 训练、20% 测试。predict 方法会把新样本分配到距离最近的中心。如果两次的人群占比差距在 3% 以内,说明结果不是偶然。如果差距很大,优先检查特征表是否被少量极端值主导。
4. 期末大作业避坑指南:5 条踩过的数据与建模坑
4.1 时间字段解析失败,按天聚合结果全是 NaN
现象:用 pd.to_datetime 转换交易时间后,发现小时列全部变成 NaN,按天聚合后图像一片空白。 原因:原始数据里时间格式不统一,有的行是「2025-03-12 12:30:45」,有的行是「2025/03/12 12:30」,甚至还有「2025年3月12日」。to_datetime 无法自动识别所有格式,解析失败的行变成了空值。 解决:先用 errors='coerce' 强制转换,标记解析失败的行;再检查失败样本的时间格式,写一个自定义解析函数补齐。最简单的方式是让 to_datetime 的 format 参数明确指定格式。不要相信 Excel 导出的时间是「标准格式」,必须有这一层检查。
4.2 退款负数拉低均值,消费金额变成负的
现象:早餐平均消费是 -3.2 元,食堂抢了学生的钱。 原因:退款记录被当作消费纳入聚合,负金额抵消了正金额。 解决:把金额列拆成正负两套逻辑。如果只做消费分析,直接保留 amount > 0;需要研究退款时,把负数的绝对值拿出来单独分析。注意充值记录通常是正数,如果不按交易类型区分,也会混进消费金额。建议先看交易类型字段的值分布,再决定过滤条件。
4.3 去重把正常购买记录误删了
现象:同一张卡在同一家店买了两次相同金额的饮料,时间相差 1 秒,结果被全字段去重删除。 原因:没有指定去重列,直接用 drop_duplicates() 把「卡号 + 商户 + 时间 + 金额」四列完全相同的记录当成了重复数据。其实一秒钟刷两次卡是合理的人类行为,比如一次刷卡本人消费,一次帮同学带饭。 解决:只对高频重复类型做去重,判断标准是「同一秒内同一商户同一金额且同一卡号」。如果数据里有交易流水号字段,优先用流水号去重。没有流水号时,subset 要加上三个以上维度,避免误删。
4.4 卡号字符串混进特征,聚类结果被「身份证号」支配
现象:聚类出的四个簇分别对应卡号开头四个数字段,业务上毫无意义。 原因:把 card_no 或学号列当成了数值特征,标准化后进入聚类模型。KMeans 基于欧氏距离,卡号中任何一点数字差异都会被当成距离,导致聚类完全偏离消费行为。 解决:第一步是删除卡号、学号、姓名等身份标识列;第二步检查特征表里是否有其他高基数列,比如商户名称。需要用独热编码处理商户偏好字段,或者直接不把商户名称作为聚类输入,改用它构造「食堂消费占比」这类聚合特征。
4.5 轮廓系数很高,但人群画像完全解释不通
现象:k=2 时轮廓系数高达 0.6,看起来聚类效果很好,但打开画像发现一群人全是总消费金额极高的夜宵大户,另一群人全是几乎不刷卡的低频用户,中间人群被强行切成了两半。 原因:特征中存在极端值,KMeans 对这种尖峰分布极其敏感,少数高消费样本把整个簇中心拉走了。轮廓系数衡量的是几何分离程度,不判断业务合理性。 解决:先对特征做裁剪,比如 95% 分位数截断极端消费金额;或者使用分位数变换替代标准化。更重要的是必须回看每个簇的人数比例。如果人数最少那一类只占 2%,它更可能是噪声而不是人群,此时通过 W 检验调整 k 或改用 DBSCAN 处理离群点。
5. 可视化与结果报告:让期末答辩在 10 分钟内有底气
5.1 三种能在一页 PPT 上讲清楚的图
数据建模的成果需要用图形呈现,评卷老师没有耐心一行行看 DataFrame。我通常画三张图:第一张是所有学生的分时段消费折线图,展示早中晚高峰;第二张是周消费热力图,横轴星期、纵轴小时,用颜色深浅表示消费次数;第三张是每个聚类小组的平均日消费金额分布图,用来突出分群差异。
import matplotlib.pyplot as plt import seaborn as sns plt.rcParams['font.sans-serif'] = ['SimHei', 'PingFang SC', 'Microsoft YaHei'] plt.rcParams['axes.unicode_minus'] = False fig, axs = plt.subplots(1, 3, figsize=(16, 4)) hourly_count = df_clean.groupby('hour').size() axs[0].plot(hourly_count.index, hourly_count.values, marker='o') axs[0].set_title('分时段消费次数') pivot = df_clean.pivot_table(index='hour', columns='weekday', values='amount', aggfunc='count').fillna(0) sns.heatmap(pivot, ax=axs[1], cmap='YlOrRd') axs[1].set_title('每周消费规律热力图') daily_group = daily.groupby('cluster')['total_count'].mean() daily_group.plot(kind='bar', ax=axs[2], color=['#4C72B0', '#55A868', '#C44E52', '#8172B2']) axs[2].set_title('各簇平均消费次数') plt.tight_layout()先设置中文字体,否则图表里的中文会变成方块,这是 matplotlib 默认字体导致的。分时段曲线把「早中晚三个高峰」一句话讲完。热力图的 x 轴是周一到周日,y 轴是 0 到 23 点,能直观看到周末上午消费明显后移。第三张柱状图用于展示各簇画像差异,柱子的高度对比就能支撑「人群可分」的结论。
5.2 用表格把人群画像输出成报告素材
图像适合口头表达,书面报告里还需要一张精确的表格。我一般会把每个簇的核心特征整理成这种格式:簇标签、自定义人群名、人数占比、平均日消费金额、平均日活跃次数、早餐占比、夜宵占比。这张表可以直接粘贴到期末报告的模型结果部分,也能在答辩被追问时快速定位到对应数据。
| 簇 | 人群标签 | 人数占比 | 日均消费(元) | 日均次数 | 早餐占比 | 夜宵占比 |
|---|---|---|---|---|---|---|
| 0 | 规律早餐型 | 28% | 9.6 | 2.1 | 42% | 5% |
| 1 | 活跃夜宵型 | 19% | 21.3 | 3.4 | 9% | 38% |
| 2 | 节制消费型 | 36% | 6.8 | 1.2 | 31% | 11% |
| 3 | 周末集中型 | 17% | 13.5 | 1.6 | 14% | 20% |
表格里的值来自 cluster_profile 的聚合结果,不要手工填数,也不要先把报告写好了再倒推数据。如果发现某种类型的占比太小,比如只有 2%,说明这个簇可能是异常值簇,应该回去调整模型参数而不是硬搬进报告。
5.3 报告结构:抽掉代码也能自洽
期末报告吓退同学的往往不是分析,而是不知道按什么顺序写。我建议直接按项目流程组织:第一部分写背景与目标,说明为什么用校园消费数据做建模,想解决什么问题;第二部分写数据来源与预处理,包括字段说明和清洗流程;第三部分写特征工程,解释你选择了哪几个特征、为什么没有选别的;第四部分写模型选择与参数确定;第五部分写分群结果与人群画像;最后一段写对学生消费管理和个性化服务的建议。
其中第三部分最容易被一笔带过。实际上老师最看重的是特征构造思路。你可以写「用 pandas 从原始流水中拆出小时、星期、餐段三类时间特征」,也可以展开说明「消费熵衡量了消费习惯的稳定性」,后者更能体现建模思考。但如果你的作业要求里没有要求写建议,那最后一段不写也没问题,不用为了凑篇幅硬编对策。
5.4 被老师问「为什么选 KMeans」时,这样回答不慌张
答这种问题讲究「性能 + 业务」双线。先说业务:这次分析的目的是给全校学生做消费人群分层,KMeans 的每个簇可以用中心点描述,天然适合输出画像。再说性能:KMeans 在大样本下速度快,算法复杂度接近线性,咱们学校这条数据集有二十多万条流水,跑一遍只需要几秒钟。更重要的是它只有 k 一个超参数,调参路径清晰,不像 DBSCAN 的 eps 和 min_samples 需要反复试。
如果老师追问为什么不用 GMM 或层次聚类,可以说 GMM 假设每个簇服从高斯分布,校园消费金额分布实际上偏态明显,这个假设不一定成立;层次聚类的计算量随样本数增长较快,面对几万张卡的特征表不划算。回答时不需要推倒重来,只要把 KMeans 的优势和你对数据分布的观察讲清楚即可。
6. 收尾一步:用一个月做稳定性验证,让报告再上一个等级
期末作业交完后,真正拉开差距的是验证部分。我最后一件事永远是「留一个月数据当验证集,用前几个月的参数复跑一次」。具体操作是把整个数据集按月份切分,比如用 3—5 月数据建模,再用 6 月数据计算每个样本到已得簇中心的距离,重新分配簇标签,最后对比两轮聚类人数比例。如果一个簇的人数比例从 28% 跳到 64%,说明建模只抓住了月份效应而不是消费习惯,需要回到特征设计去加入「学期前半段 / 后半段」标记。
另一个值得做的扩展是特征重要性验证。把聚类标签作为目标变量,用随机森林训练一个分类器,看哪些特征贡献度最高。如果早餐占比排第一,说明你的分群主要靠早餐习惯切分,报告里就要突出描述这个特征;如果消费总金额排第一,则要承认这是个「消费水平分层」模型,而非「作息规律分层」模型。这一步不复杂,却能让报告里所有结论都有依据。
我当年做这份作业时,最深的教训是急着跑结果,跳过验证直接写结论。答辩时老师对着投影仪说「你把 5 月的数据抽出来跑一遍」,结果簇分布完全变了,当场冷场。后来我养成了先切一个月数据做稳定性验证的习惯,再小的分析也按这个流程走。希望帮到你,这 10 分钟会让你在讲台上的语气跟别人不一样。
本文还有配套的精品资源,点击获取