Python校园消费行为分析:从数据清洗到KMeans聚类实战

发布时间:2026/8/27 1:55:07
Python校园消费行为分析:从数据清洗到KMeans聚类实战 简介数据分析和数据挖掘是当前数字化校园建设中的高频技术需求校园一卡通产生的消费流水为研究学生行为提供了宝贵的数据基础。围绕消费记录我们可以通过数据预处理、特征工程、可视化分析和聚类建模等完整链路提取出具有解释力的行为规律。Python凭借pandas、matplotlib、scikit-learn等生态组件成为此类分析任务的天然选择。本文从数据清洗、特征提取等基础环节入手介绍如何构建学生消费特征矩阵并结合KMeans聚类算法实现学生分群辅助校园管理决策。通过一个可复现的课程设计项目展示了从原始消费流水到聚类画像的完整技术路径适用于校园数据分析、用户行为研究、数据挖掘实践等典型应用场景。1. 项目整体设计思路与技术选型1.1 为什么用 Python 做校园消费行为分析拿到“学生校园消费行为分析”这个题目很多人的第一反应是“这不就是统计一下食堂刷了多少钱”。但如果只是做到这个程度评分老师大概率只会给个及格分。真正高分的设计核心在于能不能把“消费流水”这种最普通的数据挖掘出行为层面的规律再给出有一定解释力的结论。Python 在这个题目里几乎是唯一合理的选择。原因有三点。第一整个分析链路完整从数据读取、清洗、聚合统计、可视化到最后的聚类建模pandas、matplotlib、seaborn、scikit-learn 这几个库可以一条龙跑完不需要在多个工具之间来回切换。第二课程设计答辩环节通常要求现场演示代码逻辑Python 的代码可读性天然占优势每一步做什么非常直观。第三校园一卡通消费数据通常是 CSV 或者 Excel 导出字段比较规整恰好是 pandas 最擅长的处理场景。这个项目的源码结构我建议按照“数据层—分析层—展示层”三层来组织。数据层负责读入原始流水、做基础清洗输出干净的标准表分析层负责各类统计指标的计算、行为特征的提取展示层负责生成图表和最终的结果汇总文件。三层分离的好处是如果老师要求换一批数据重新跑一遍你只需要替换数据层的输入文件后面的代码完全不用动这个加分项在答辩时很值得提。1.2 数据流与最终交付物设计课程设计的交付物不只是“能跑出几张图”就完了。优秀的交付应该包含三块原始数据或模拟数据、可运行的源码、结果结果集包括统计表、图表、核心结论。这三块分别对应你提交材料里的“数据集”“源码”“结果集”缺一不可。在实际操作中常见的问题是很多同学只提交代码和一份实验报告结果老师想验证代码能不能跑通时手里没有数据。所以我在项目里专门生成了模拟数据集字段完全参考真实一卡通流水设计包括学号、消费时间、商户名称、消费金额、消费类型等。这样做的另一个好处是模拟数据可以先按已知规律生成再用代码反推验证如果反推结果和设定规律对得上说明整个分析流程没有问题这个思路在课程设计答辩里非常加分。最终结果集的输出格式建议统一用 CSV PNG 图片的组合。CSV 方便老师查看具体的统计数值PNG 图片方便直接贴到实验报告里。数值类结果和图表类结果分开存放目录层级如下result/ ├── statistics/ # 统计结果 CSV │ ├── daily_consume.csv │ ├── period_consume.csv │ ├── merchant_rank.csv │ └── student_features.csv └── figures/ # 可视化图表 ├── daily_trend.png ├── period_distribution.png ├── merchant_top10.png ├── amount_distribution.png ├── cluster_scatter.png └── radar_chart.png目录结构越清晰老师一眼就能看出你的“结果集”是成体系的而不是随手堆了几张图。2. 数据集的字段设计与模拟数据生成2.1 一卡通消费流水应该包含哪些字段分析结果的可靠程度完全取决于原始数据的质量。一份可用的校园一卡通消费流水至少要包含以下字段字段名类型说明student_id字符串学号用于标识学生个体trade_time时间戳消费发生的具体时间merchant字符串商户名称如“第一食堂”“超市”amount浮点数消费金额单位元trade_type字符串消费类型如“餐饮”“购物”“充值”等需要注意的是真实的校园卡系统里一份流水表往往还会包含卡号、设备编号、校区编号等字段。对于课程设计来说学号、时间、商户、金额这四列是分析的核心其他字段属于附加信息有则更好没有也不影响主要结论。生成模拟数据时有一个细节值得注意学号不要用连续的整数否则聚类时容易出现“学号大小”被误当成特征的干扰项。我习惯把学号构造成类似“202401011”这种带点位数差异的编号模拟真实学号的复杂度。2.2 模拟数据的生成要“有规律”才真实模拟数据最忌讳的是纯随机。如果每个人每天的消费次数、金额完全是均匀随机分布那分析出来的结果必然是“一锅粥”没有任何规律可挖。真实的学生消费行为是有明显结构的生成模拟数据时必须把这些结构埋进去。我在生成数据时埋了这几类规律第一消费次数服从“早晚高峰”分布。早餐集中在 7:00-8:30午餐集中在 11:00-13:00晚餐集中在 17:00-18:30夜宵集中在 21:00 之后。我按时间段设置了不同的概率权重让生成的流水在时间维度上天然呈现“三峰”形态。第二不同学生的消费水平有差异。我给每个学生随机分配一个“消费等级”比如月均消费 800 元、1200 元、1800 元三档再在这个基准上叠加随机波动。这样生成出来的数据里学生与学生之间就存在可被聚类算法识别的区分度。第三商户类型和消费时段有相关性。食堂在正餐时段是绝对主力超市的消费则更多地分布在下午和晚上饮料店的消费高峰在午后和晚间。这个规律如果不埋进去后面的“时段—商户”交叉分析就做不出好看的图表。生成模拟数据的代码片段如下import pandas as pd import numpy as np from datetime import datetime, timedelta np.random.seed(42) def gen_student_base(num_students300): levels np.random.choice([800, 1200, 1800], sizenum_students, p[0.4, 0.4, 0.2]) student_ids [f2024{i:05d} for i in range(1, num_students 1)] return pd.DataFrame({student_id: student_ids, consume_level: levels}) def gen_consume_records(students, days30): records [] merchants [ (第一食堂, 餐饮, 5, 25), (第二食堂, 餐饮, 5, 30), (超市, 购物, 2, 60), (面包房, 餐饮, 3, 20), (饮料店, 饮品, 3, 15), ] for _, row in students.iterrows(): sid row[student_id] base_level row[consume_level] daily_times max(1, int(np.random.normal(base_level / 60, 0.5))) for day_offset in range(days): daily_count np.random.poisson(daily_times) for _ in range(daily_count): # 按时间段权重生成消费时间 period np.random.choice( [breakfast, lunch, dinner, night], p[0.15, 0.35, 0.35, 0.15] ) hour_range { breakfast: (7, 9), lunch: (11, 13), dinner: (17, 19), night: (21, 23), }[period] hour np.random.randint(hour_range[0], hour_range[1] 1) minute np.random.randint(0, 60) trade_time datetime(2024, 5, 1) timedelta(daysday_offset, hourshour - 8, minutesminute) merchant np.random.choice([m[0] for m in merchants]) amount np.round( np.random.uniform(3, 30) * (base_level / 1200), 2 ) records.append([sid, trade_time, merchant, amount]) return pd.DataFrame(records, columns[student_id, trade_time, merchant, amount])这段代码有两个关键点。一是用np.random.seed(42)固定随机种子保证每次运行生成的模拟数据完全一致这是课程设计里非常重要的复现性要求答辩时老师可能会要求重新运行数据一致性会让演示过程顺利很多。二是生成时间时用了datetime计算保证时间戳字段是标准的时间格式后面做时段提取时可以直接用.dt.hour属性不需要额外解析。3. 数据清洗与核心分析实现3.1 数据清洗不是简单地删掉空值拿到原始流水后第一步不是做统计而是清洗。真实场景下的一卡通流水非常脏常见问题包括学号缺失、时间格式不统一、金额为负数可能是退款记录、重复记录、超出合理范围的异常金额等。清洗环节我按四个步骤处理空值处理检查关键字段是否为空学号和金额为空的行直接删除因为这两个字段是所有分析的基础。退款记录识别校园卡系统里消费金额为负数的记录通常是退款不是消费行为。分析消费行为时必须把这些记录剔除或者单独归类否则金额统计会失真。金额合理性校验单笔消费金额如果低于 0.5 元或高于 200 元需要重点关注。低于 0.5 元可能是饮水机、开水房的费用高于 200 元可能是超市大额购物或者设备异常这些记录是否保留要看分析目的。如果做的是“餐饮消费分析”我会把高于 100 元的记录单独标记出来而不是直接删除。重复记录去重检查是否存在完全相同的流水记录如果存在保留第一条即可。清洗代码参考def clean_data(df): df df.copy() # 统一时间格式 df[trade_time] pd.to_datetime(df[trade_time]) # 删除空值 df df.dropna(subset[student_id, amount]) # 剔除退款记录负数金额 df df[df[amount] 0] # 金额异常标记 df[is_abnormal] df[amount].apply(lambda x: x 100) # 去重 df df.drop_duplicates() return df清洗完成后建议把清洗前后的记录数做一个对比输出到结果集里。这个对比在课程设计报告的“数据预处理”章节里是很好的素材能直观体现你对数据质量的把控意识。3.2 消费行为特征提取从流水到特征矩阵分析的第二步是把逐条的流水记录转换成“每个学生一行”的特征矩阵。这个矩阵是后续统计分析和聚类建模的基础。我建议提取以下几个核心特征特征名含义计算方式total_amount总消费金额该学生所有有效消费金额之和total_count总消费次数该学生所有有效消费记录条数avg_amount平均每笔金额总金额 / 总次数active_days活跃天数有消费记录的天数daily_avg日均消费总金额 / 活跃天数breakfast_ratio早餐占比早餐时段消费次数 / 总次数night_ratio晚间消费占比晚间时段消费次数 / 总次数这些特征不是随便选的。总金额反映消费水平消费次数反映活跃度平均每笔金额反映消费习惯比如喜欢小额多次还是大额少次早餐占比较高的学生通常生活规律更好晚间消费占比较高的学生可能有熬夜习惯。这些特征组合起来能够刻画一个学生的消费画像。特征提取代码def extract_features(df): # 提取消费时段 df[hour] df[trade_time].dt.hour def get_period(h): if 7 h 9: return breakfast elif 11 h 13: return lunch elif 17 h 19: return dinner else: return night df[period] df[hour].apply(get_period) grouped df.groupby(student_id) features pd.DataFrame({ total_amount: grouped[amount].sum(), total_count: grouped[amount].count(), avg_amount: grouped[amount].mean(), active_days: grouped[trade_time].apply(lambda x: x.dt.date.nunique()), daily_avg: grouped[amount].sum() / grouped[trade_time].apply(lambda x: x.dt.date.nunique()), }) # 时段占比 period_counts df.groupby([student_id, period]).size().unstack(fill_value0) for p in [breakfast, lunch, dinner, night]: if p not in period_counts.columns: period_counts[p] 0 features[breakfast_ratio] period_counts[breakfast] / features[total_count] features[night_ratio] period_counts[night] / features[total_count] return features.reset_index()这里有个细节值得注意active_days的计算用的是trade_time.dt.date.nunique()而不是简单地对天数做nunique()。原因是如果把时间戳直接nunique()同一学生同一天不同时段的消费会被算成多个活跃天导致活跃天数虚高。这个坑我一开始踩过后来对比了真实数据才意识到问题所在同学们写代码时一定要留意这种“看似对、实际错”的边界情况。3.3 消费时段与商户偏好分析学生消费行为分析不能只停留在“每个人花多少钱”这个层面还需要从整体视角看“什么时间在消费”“在哪些商户消费”。时段分析的核心是绘制“24小时消费订单量分布图”。将一天划分为 24 个区间统计每个时间段的订单总数画成柱状图或折线图。正常的校园消费数据会呈现明显的三峰形态早餐高峰、午餐高峰、晚餐高峰。如果某个食堂的数据出现了“夜宵高峰比午餐还高”的奇特形态那很可能说明这所学校的学生普遍熬夜这个结论在报告里非常出彩。商户偏好分析则可以从两个角度展开一是整体消费金额排名找出 TOP10 热门商户二是分时段看商户热度比如早餐时段哪个食堂最受欢迎晚间时段超市的销量是否明显上升。这两个角度合在一起可以回答“学生钱花在哪了”和“学生在什么时间花钱”这两个核心问题。实现商户 TOP10 统计的代码merchant_rank ( df.groupby(merchant)[amount] .agg([sum, count]) .sort_values(sum, ascendingFalse) .reset_index() ) merchant_rank.columns [merchant, total_amount, total_count] merchant_rank[avg_amount] merchant_rank[total_amount] / merchant_rank[total_count] merchant_rank.to_csv(result/statistics/merchant_rank.csv, indexFalse, encodingutf-8-sig)我特别强调encodingutf-8-sig这个参数。如果不加这个参数直接用utf-8编码写入 CSV文件在 Windows 系统的 Excel 里打开时中文列名会变成乱码。加了-sig后缀会在文件开头写入 BOM 头Excel 就能正确识别编码。这个细节在很多课程设计项目里都会被忽略却直接影响老师的体验。4. 学生消费行为分层聚类与可视化4.1 KMeans 聚类的参数选择与特征标准化做完基础统计之后就可以进入更高阶的环节学生分群。这个环节是课程设计从“数据分析”升级为“数据挖掘”的关键。最常用的算法是 KMeans 聚类。但 KMeans 有一个前提特征必须做标准化StandardScaler否则金额类的特征数值大会完全压制占比类的特征数值小聚类结果基本等于只按“谁有钱”在分群。标准化处理之后另一个关键问题是 K 值的确定。我建议用“肘部法则”辅助判断对 K2 到 K8 分别计算聚类误差平方和SSE绘制折线图找到曲线拐点位置。拐点通常意味着继续增加簇数带来的误差下降幅度明显变缓这个位置对应的 K 就是相对合理的簇数。代码实现from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler feature_cols [total_amount, total_count, avg_amount, daily_avg, breakfast_ratio, night_ratio] X features[feature_cols].values scaler StandardScaler() X_scaled scaler.fit_transform(X) # 肘部法则确定 K 值 sse [] for k in range(2, 9): km KMeans(n_clustersk, random_state42, n_init10) km.fit(X_scaled) sse.append(km.inertia_) # 根据肘部图选取 K3 或 K4这里以 K3 为例 km KMeans(n_clusters3, random_state42, n_init10) labels km.fit_predict(X_scaled) features[cluster] labels需要提醒的是n_init参数在新版 scikit-learn 中默认为 10但显式写出来能让代码的意图更清晰避免不同版本间行为差异影响结果。另外random_state必须固定否则每次运行的聚类结果可能不同这会直接影响结果集的可复现性。4.2 聚类结果解读每类学生的消费画像聚类的目标是找到行为模式不同的学生群体而不是单纯地把数据分成几堆。聚类完成后需要对每个簇进行画像描述。以 K3 为例实际项目中常见的分群结果大致如下簇 0规律型日均消费 30-50 元消费次数适中早餐占比高晚间消费占比低。这类学生作息规律消费以食堂正餐为主。簇 1高消费型总金额和日均消费明显高于平均平均每笔金额大商户偏好中超市和饮料店占比上升。这类学生消费能力较强不仅在食堂消费在商业店铺也有较多支出。簇 2节约型总金额和日均消费偏低消费次数少平均每笔金额低。这类学生消费较为节俭可能更倾向于基础餐饮。输出每个簇的画像摘要可以用groupby加均值统计cluster_profile ( features.groupby(cluster)[feature_cols] .mean() .round(2) ) cluster_profile.to_csv(result/statistics/cluster_profile.csv, encodingutf-8-sig)这个结果表是整个项目最有“分析价值”的部分在课程设计报告中应当重点呈现。老师看到的不再是“数据统计”而是“从数据中发现了三类不同的学生消费模式”这个层次感是高分的关键。4.3 可视化设计一张图讲清一个结论可视化不是随便画几张图凑数每张图都应该对应报告中的一个具体结论。我建议至少画齐这六张图每日消费总额折线图观察 30 天内的消费波动能清楚看到周末和工作日的差异。24 小时订单量分布图展示早中晚三个就餐高峰。商户消费金额 TOP10 柱状图回答“钱花在哪”的问题。单笔消费金额分布直方图观察金额的集中区间通常集中在 10-15 元。消费金额与消费次数的聚类散点图用不同颜色标记不同簇的学生直观展示分群效果。各簇特征的雷达图对比不同簇在金额、次数、占比等多维度的差异。绘图时有一组非常实用的中文字体设置直接抄作业import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei, Microsoft YaHei] plt.rcParams[axes.unicode_minus] Falseaxes.unicode_minus必须设置为False否则坐标轴上的负号会显示成方块。这两行配置能省去大部分中文乱码问题。雷达图是展示聚类特征差异的利器贴一下核心代码import numpy as np from matplotlib.patches import Circle import matplotlib.pyplot as plt def plot_radar(data, labels, save_path): n_clusters data.shape[0] n_features data.shape[1] angles np.linspace(0, 2 * np.pi, n_features, endpointFalse).tolist() angles angles[:1] fig, ax plt.subplots(figsize(8, 8), subplot_kwdict(polarTrue)) for i in range(n_clusters): values data.iloc[i].tolist() values values[:1] ax.plot(angles, values, labelfCluster {i}) ax.fill(angles, values, alpha0.15) ax.set_xticks(angles[:-1]) ax.set_xticklabels(data.columns) ax.legend(locupper right, bbox_to_anchor(1.2, 1.1)) plt.tight_layout() plt.savefig(save_path, dpi200, bbox_inchestight) plt.close()注意画雷达图时各特征的数据范围完全不同如果不做归一化雷达图会变成一坨没法看的几何图形。我这里的建议是先把特征做 MinMax 归一化到 0-1 区间再传入雷达图绘制函数。5. 常见问题与排错实录5.1 “file is not a zip file” 与解压失败问题拿到项目压缩包时最容易遇到的就是解压失败。标题里的.zip后缀很容易让人忽略一个细节Windows 系统默认的“资源管理器解压”对中文文件名支持不好如果压缩包内含中文字符文件经常会出现解压中途报错、文件丢失的情况。我建议课程设计这个场景下统一用 Python 脚本或者命令行工具解压。命令行方式最稳妥unzip -O gbk 项目文件.zip -d output_folder-O gbk参数强制以 GBK 编码解压能有效解决中文乱码问题。如果是在 Windows 上也可以用 Python 的zipfile模块解压时手动处理文件名编码import zipfile with zipfile.ZipFile(项目文件.zip, r) as zf: for info in zf.infolist(): # 修正中文文件名编码 try: filename info.filename.encode(cp437).decode(gbk) except UnicodeDecodeError: filename info.filename zf.extract(info, output_folder)这个技巧在拿到任何中文 zip 包时都适用。另外提一个常见误区不要直接用 macOS 自带的“归档实用工具”解压中文 zip它的编码处理策略和 Windows 不一样解压出来的文件名经常是乱码。5.2 中文乱码问题CSV 打开乱码与图表乱码这个问题几乎每个做数据可视化项目的同学都会遇到根源在于编码体系不一致。解决方式分两类CSV 文件 Excel 打开乱码写入时指定encodingutf-8-sig。matplotlib 图表中文显示为方块设置字体参数。如果设置了SimHei后仍然显示方块大概率是运行环境中没有安装对应的中文字体。可以用下面的代码检查当前环境的可用字体import matplotlib.font_manager as fm fonts [f.name for f in fm.fontManager.ttflist] print(SimHei in fonts, Microsoft YaHei in fonts)如果返回全是False说明环境缺少中文字体。在 Linux 服务器上运行时需要先安装字体包如果只是在本地跑切换 Python 解释器环境往往就能解决。5.3 聚类结果不稳定与 K 值选择问题同一个数据集两次运行的聚类结果不同这个问题在课程设计里非常扣分。主要原因是没有固定随机种子。修复方式就是在创建 KMeans 实例时显式指定random_statekm KMeans(n_clusters3, random_state42, n_init10)另一个常见问题是肘部法则画出来的曲线没有明显的“肘部”K 值很难判断。这种情况在真实数据里经常出现因为真实数据往往不像教科书里那么理想。我的经验是不纠结于“最优 K 值”而是从业务可解释性出发。K3 或者 K4 通常就能给出足够清晰的分群结果如果分出来的簇在业务上无法解释再调整 K 值重跑而不是死盯肘部图。5.4 代码运行环境与依赖版本这个项目依赖 pandas、numpy、matplotlib、seaborn、scikit-learn。安装时不要用pip install pandas一个一个装直接一次性装完pip install pandas numpy matplotlib seaborn scikit-learn如果是在 Anaconda 环境下这些库通常已经预装不需要额外安装。需要提醒的是scikit-learn 在 1.2 版本之后对部分 API 做了调整如果运行时报n_init相关错误建议把 scikit-learn 升级到 1.3 以上pip install --upgrade scikit-learn6. 课程设计报告的组织与答辩要点6.1 报告结构建议让老师 5 分钟看懂全文课程设计报告不需要像毕业论文那样冗长但结构必须完整。我的经验是控制在 15-20 页重点突出“分析方法”和“分析结论”。推荐的报告结构项目背景与目标1-2 页说明为什么要做校园消费行为分析分析目标是什么。数据说明1-2 页数据集来源、字段含义、数据量、清洗前后的对比。分析方法3-4 页特征提取方法、统计分析方法、聚类算法原理简述。分析结果4-6 页图表 结论这是报告的主体。总结与改进方向1-2 页简述项目收获说明还有哪些可以优化的方向。这里有一个很实用的技巧每一章开头用一句加粗的话概括本章结论然后把图表和详细分析放在后面。老师翻阅报告时只需要看加粗的标题句5 分钟内就能掌握全文核心内容。这个写作习惯在答辩时非常讨喜。6.2 答辩高频问题与应答思路答辩环节老师最爱问的问题我整理成一张速查表高频问题应答思路为什么选择 KMeans 而不是其他聚类算法KMeans 对大规模数据计算效率高参数简单适合行为分群缺点是需预设 K 值对异常值敏感因此做了标准化和异常值处理数据集是怎么来的如果是模拟数据如实说明模拟数据的生成逻辑如果用了真实数据说明脱敏处理方式这些结论有什么实际价值可以用于学校食堂档口优化、贫困生补贴精准发放、校园商业业态规划等场景如果数据量扩大 100 倍代码能跑吗正常回答当前代码用 pandas 处理百万级数据没问题再大的数据量可以通过分块读取或引入 Spark 优化第 4 个问题容易被忽略但很多老师喜欢问。如果代码里大量使用groupby和apply百万级数据是能跑的但千万级以上会变慢。你可以提一句“通过分块读取pd.read_csv(..., chunksize10000)可以缓解内存压力”这个回答会让老师觉得你考虑过性能问题。6.3 结果集的“加分项”技巧最后分享一个让结果集更出彩的小技巧。除了常规的统计表和图表外我额外生成了一个“分析结论汇总”目录里面存放一张conclusion.md文件用文字描述每个分析结论# 分析结论汇总 ## 整体消费规律 - 30 天内日均消费记录约 XXXX 条日均消费金额约 XXXX 元。 - 消费高峰出现在 11:00-13:00 与 17:00-19:00 两个时段。 ## 学生分群结果 - 规律型学生占比约 40%日均消费 45 元作息规律。 - 高消费型学生占比约 25%日均消费 80 元商业店铺消费占比高。 - 节约型学生占比约 35%日均消费 30 元消费集中在基础餐饮。 ## 建议 - 依据分群结果对高消费型学生聚集的区域增设商业业态。 - 对规律型学生为主的教学区食堂优化午餐高峰期的出餐效率。这份conclusion.md文件的作用是“一键生成报告结论”。老师在查看结果集时不需要自己去翻图表、猜结论直接读这个文件就能理解项目的价值。这种“结果集里有结论”的设计是我做数据类课程设计最推荐的加分项比任何花哨的界面都管用。本文还有配套的精品资源点击获取