ARTICLE DETAIL

资讯详情

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

Python校园消费行为分析:从数据清洗到聚类诊断的完整实践

Python校园消费行为分析:从数据清洗到聚类诊断的完整实践 简介本资源是一份面向高校计算机与数据科学专业学生的Python数据分析实战项目聚焦校园消费行为建模与可视化分析适用于课程设计、期末大作业及入门级数据分析实践。项目完整包含可直接运行的源码、清洗后的结构化数据集CSV格式及多维度分析结果图表PNG/JPG覆盖数据预处理、用户分群、消费趋势挖掘与可视化全流程难度适中且经助教审定评审得分达95分以上。压缩包共19个文件含4个核心Python脚本实现不同分析任务、4个CSV数据文件含原始与中间处理数据、9张PNG及2张JPG结果图直观呈现聚类、时序、分布等分析结论整体大小20.22MB结构清晰、模块分离明确。目前已有81人学习下载读者可直接复现高分课程设计成果获取调试通过的代码逻辑、典型分析思路、图表生成方法及结果解读范例快速提升Python数据处理与业务分析综合能力。1. 为什么用 Python 做学生校园消费行为分析不是“跑个图表就交差”而是要真正挖出食堂刷卡背后的规律这不是一个“用 pandas 读 Excel、画几个柱状图”的课程设计作业——高分的关键在于你能否从真实校园一卡通流水数据里识别出消费频次异常的学生群体、发现不同年级在餐饮 vs 超市 vs 打印场景下的决策差异、验证“月末消费激增”是否真存在、甚至定位出潜在的经济困难预警信号。我带过三届课程设计90% 的同学卡在“数据清洗不干净导致聚类全乱”“时间序列没对齐导致趋势图失真”“特征工程拍脑袋选了‘总金额’却漏掉‘单次均值间隔标准差’这种关键组合”。这个源码包的价值不在于它自带 2376 条脱敏消费记录含时间戳、商户类型、金额、卡号而在于它把从原始 CSV 到可解释性报告的完整链路拆解成 7 个可调试模块数据加载校验 → 时间切片对齐 → 商户类型标准化 → 消费行为编码非简单求和→ 多维度分组聚合 → KMeansDBSCAN 双聚类对比 → 可视化结论反哺业务假设。适合两类人一是需要交高分作业但不想抄模板的本科生二是想快速复现校园行为分析 baseline 的新入职数据分析岗工程师——你不需要懂机器学习原理但必须理解“为什么要把 2023-09-01 08:23:17 转成 weekdayhour_binis_weekend_three_day_before”。2. 用 Python 在本地跑通学生消费行为分析从解压到生成第一份热力图的最小闭环2.1 解压后立刻验证数据完整性三个文件缺一不可源码包解压后得到data/、src/、results/三个目录。不要跳过这一步验证——我见过 17 个同学因data/raw_transaction.csv编码是 GBK 而不是 UTF-8直接报UnicodeDecodeError卡死在第一步。执行以下命令确认# 进入解压目录 cd student-consumption-analysis # 检查核心数据文件编码与行数Linux/macOS file -i data/raw_transaction.csv wc -l data/raw_transaction.csv # Windows 用户用 PowerShell 替代cmd 不可靠 # Get-Content data\raw_transaction.csv -Encoding Byte | Measure-Object -Line提示file -i输出应为charsetutf-8若显示charsetgbk需用iconv -f gbk -t utf-8 data/raw_transaction.csv data/raw_transaction_utf8.csv转换。原始文件行数应为 2376 行含表头少于该数说明下载损坏。2.2 安装依赖时避开 pip 版本陷阱requirements.txt 的隐藏约束src/requirements.txt明面只写pandas1.5.3但实际运行会触发statsmodels对numpy1.24的强依赖。直接pip install -r requirements.txt在 Python 3.11 环境下必失败。正确做法是分步安装# 先创建隔离环境强烈推荐避免污染主环境 python -m venv venv source venv/bin/activate # Linux/macOS # venv\Scripts\activate.bat # Windows # 安装 numpy 特定版本解决 statsmodels 冲突 pip install numpy1.23.5 # 再装其他依赖此时 pandas 1.5.3 会自动适配 numpy 1.23.5 pip install -r src/requirements.txt # 验证关键库版本输出应匹配 python -c import pandas as pd; print(pd.__version__) # 输出1.5.3 python -c import seaborn as sns; print(sns.__version__) # 输出0.12.2参数说明numpy1.23.5是经过实测的兼容版本pandas1.5.3因内置datetime解析器更稳定比 2.x 版本更适合处理校园卡系统常见的不规范时间格式如2023/09/01 8:23缺少前导零。2.3 运行主流程main.py的四个必调参数与默认行为进入src/目录后执行python main.py默认只生成基础统计报表。要产出高分所需的热力图与聚类结果必须传参# 最小可行命令生成所有图表含热力图 聚类报告 python main.py --output_dir ../results --cluster_method kmeans --plot_all True # 关键参数含义 # --output_dir: 结果保存路径必须是相对路径../results 指向根目录 results/ # --cluster_method: 可选 kmeans 或 dbscandbscan 对异常消费点更敏感 # --plot_all: True 时生成 12 张图含 hourly_heatmap.png, cluster_scatter.png # --min_cluster_size: 仅 dbscan 模式生效默认 5低于此数的消费群组不视为有效簇运行后检查results/目录是否生成hourly_heatmap.png✅ 正确现象横轴为小时0-23纵轴为星期Mon-Sun颜色深浅表示该时段该日的平均消费人次❌ 错误现象图像全白或报错ValueError: x and y must be the same length→ 说明时间字段未成功解析为datetime类型见 3.2 节3. 数据清洗的三大生死线时间解析、商户归类、金额异常值剔除3.1 时间字段解析为什么pd.to_datetime()必须加infer_datetime_formatFalse原始数据中时间列名为transaction_time样例值2023/09/01 8:23、2023-09-15 12:05:33、2023.09.22 18:47。若直接pd.to_datetime(df[transaction_time])pandas 会因格式混杂而报ParserError或错误推断为2023-01-09把/当月/日。必须显式指定格式并关闭自动推断# src/data_loader.py 中关键代码段 def load_and_parse_data(file_path): df pd.read_csv(file_path, encodingutf-8) # ❌ 错误写法自动推断失败率80% # df[transaction_time] pd.to_datetime(df[transaction_time]) # ✅ 正确写法用正则预处理 多格式尝试 df[transaction_time] df[transaction_time].str.replace(r[/\.], -, regexTrue) df[transaction_time] pd.to_datetime( df[transaction_time], formatmixed, # pandas 1.5.3 新增支持混合格式 errorscoerce # 无法解析的转为 NaT后续过滤 ) # 删除 NaT 行原始数据中约 0.3% 时间字段损坏 df df.dropna(subset[transaction_time]) return df逻辑说明formatmixed让 pandas 对每行单独尝试常见格式%Y-%m-%d %H:%M:%S、%Y/%m/%d %H:%M等比infer_datetime_formatTrue更鲁棒errorscoerce避免中断整个解析流程后续用dropna清洗。3.2 商户类型标准化用映射字典而非模糊匹配原始商户名如一食堂二楼窗口3、图书馆打印中心、菜鸟驿站东区若用str.contains(食堂)匹配会漏掉教工餐厅、清真食堂。项目采用预定义映射字典src/config/merchant_mapping.json{ canteen: [食堂, 餐厅, 饭堂, 清真, 教工餐厅, 风味小吃], supermarket: [超市, 便利, 小卖部, 全家, 罗森], printing: [打印, 复印, 图文, 文印], transport: [公交, 地铁, 校园巴士], other: [自动贩卖机, ATM, 缴费] }# src/feature_engineering.py 中商户分类逻辑 def categorize_merchant(merchant_name): merchant_name str(merchant_name).lower() for category, keywords in MERCHANT_MAPPING.items(): if any(kw in merchant_name for kw in keywords): return category return other df[merchant_category] df[merchant_name].apply(categorize_merchant)参数说明MERCHANT_MAPPING从 JSON 加载支持热更新str.lower()统一大小写避免ATM和atm匹配失败any()短路逻辑提升性能2376 行数据耗时 0.2s。3.3 金额异常值剔除用 IQR 法而非固定阈值有同学设amount 500为异常结果剔除了 3 笔真实的“校园卡充值”金额 1000 元。正确做法是按商户类型分组计算 IQR# src/preprocessing.py 中异常值处理 def remove_outliers_by_category(df): # 对每个商户类型分别计算 IQR outlier_indices [] for category in df[merchant_category].unique(): cat_df df[df[merchant_category] category] Q1 cat_df[amount].quantile(0.25) Q3 cat_df[amount].quantile(0.75) IQR Q3 - Q1 lower_bound Q1 - 1.5 * IQR upper_bound Q3 1.5 * IQR # 标记该类别下的异常索引 cat_outliers cat_df[(cat_df[amount] lower_bound) | (cat_df[amount] upper_bound)].index outlier_indices.extend(cat_outliers.tolist()) # 返回剔除异常值后的 DataFrame return df.drop(indexoutlier_indices) df_clean remove_outliers_by_category(df)血泪经验printing类别的 IQR 范围是 [0.5, 15] 元canteen是 [2.5, 28] 元supermarket是 [5, 120] 元——固定阈值会误杀supermarket的正常购物而 IQR 法保留了 99.2% 的有效交易。4. 行为特征工程为什么“消费次数”不如“周内消费熵值”有区分度4.1 构建时间维度特征从 timestamp 到可聚类的数值向量单纯用df[transaction_time].dt.hour得到 0-23 的整数但 23 点和 0 点在周期上相邻直接聚类会割裂夜宵群体。必须转换为圆周坐标# src/features/time_features.py import numpy as np def add_circular_time_features(df): df[hour_sin] np.sin(2 * np.pi * df[transaction_time].dt.hour / 24) df[hour_cos] np.cos(2 * np.pi * df[transaction_time].dt.hour / 24) df[day_sin] np.sin(2 * np.pi * df[transaction_time].dt.dayofweek / 7) df[day_cos] np.cos(2 * np.pi * df[transaction_time].dt.dayofweek / 7) return df # 应用后23点 → (sin≈-0.26, cos≈-0.97)0点 → (sin≈0, cos≈1)距离计算更合理逻辑说明sin/cos编码将线性时间映射到单位圆上欧氏距离能反映周期相似性如周一早8点与周五早8点距离远但周一早8点与周一晚8点距离近dayofweek从 0周一到 6周日/7归一化到[0,1)区间。4.2 设计消费行为编码用“频次金额时间离散度”三维刻画课程设计常犯错误用groupby(student_id)[amount].sum()作为唯一特征。这无法区分“每月充 300 元、每天花 10 元”和“每月充 300 元、月底突击花完”的学生。本项目定义 7 个核心行为指标特征名计算逻辑业务意义freq_weeklycount() / weeks_span每周平均消费次数反映活跃度amount_meanmean()单次平均消费反映消费能力amount_stdstd()单次金额波动反映消费稳定性interval_mean_hoursdiff().dt.total_seconds().mean() / 3600两次消费平均间隔反映规律性entropy_weekday-sum(p_i * log2(p_i))消费分布于周一至周日的熵值值越低越固定如只在周一吃饭peak_hour_ratiocount at max hour / total count最高频次小时占比反映作息集中度canteen_ratiocanteen_count / total_count食堂消费占比反映饮食习惯# src/features/behavior_encoding.py def calculate_student_behavior(df): # 按学生 ID 分组 grouped df.groupby(student_id) # 初始化结果字典 behavior_data {} for stu_id, group in grouped: # 时间跨度周 weeks_span (group[transaction_time].max() - group[transaction_time].min()).days / 7 # 计算各指标 freq_weekly len(group) / max(weeks_span, 1) # 防止除零 amount_mean group[amount].mean() amount_std group[amount].std(ddof0) or 0 # 时间间隔需排序 sorted_time group[transaction_time].sort_values() intervals sorted_time.diff().dt.total_seconds().dropna() / 3600 interval_mean_hours intervals.mean() if len(intervals) 0 else 0 # 周几熵值 weekday_counts group[transaction_time].dt.dayofweek.value_counts(normalizeTrue) entropy_weekday -sum(p * np.log2(p) for p in weekday_counts if p 0) # 高峰小时占比 hour_counts group[transaction_time].dt.hour.value_counts(normalizeTrue) peak_hour_ratio hour_counts.max() # 食堂占比 canteen_ratio (group[merchant_category] canteen).mean() behavior_data[stu_id] [ freq_weekly, amount_mean, amount_std, interval_mean_hours, entropy_weekday, peak_hour_ratio, canteen_ratio ] # 转为 DataFrame feature_names [freq_weekly,amount_mean,amount_std,interval_mean_hours, entropy_weekday,peak_hour_ratio,canteen_ratio] return pd.DataFrame.from_dict(behavior_data, orientindex, columnsfeature_names)玄学提醒entropy_weekday是区分“规律型”与“随机型”消费的关键——值 1.5 的学生多为课表固定、按时就餐值 2.3 的学生可能实习/兼职消费时间碎片化。这个指标在后续聚类中权重最高。5. 避坑学生消费分析中 4 个高频翻车点与硬核解法5.1 现象KMeans 聚类结果全是单点簇DBSCAN 报ValueError: Found array with 0 sample(s)原因特征未标准化。freq_weekly范围是 0-15amount_std范围是 0-50entropy_weekday范围是 0-2.5KMeans 欧氏距离被大数值特征主导导致所有点被拉向amount_std高值区。解决必须用StandardScaler非 MinMaxScaler——因为amount_std存在长尾MinMax 会压缩有效区分度from sklearn.preprocessing import StandardScaler # src/clustering.py 中正确做法 scaler StandardScaler() X_scaled scaler.fit_transform(X_features) # X_features 是 7 列行为特征 # 验证标准化效果 print(标准化后各特征 std ≈, X_scaled.std(axis0)) # 输出应接近 [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]注意StandardScaler的fit_transform必须在训练集上执行且测试时要用同一scaler的transform——本项目无测试集但养成习惯。5.2 现象热力图hourly_heatmap.png纵轴星期顺序错乱Sun-Mon-Tue...原因seaborn.heatmap()默认按字母序排列dayofweek0Mon, 6Sun但字符串Monday排在Sunday前导致纵轴为 Mon, Sun, Tue...解决显式指定category类型并设置顺序# src/visualization.py 中修复代码 def plot_hourly_heatmap(df): # 创建星期名称列确保顺序 df[weekday_name] df[transaction_time].dt.day_name() # 强制类别顺序 weekday_order [Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday] df[weekday_name] pd.Categorical(df[weekday_name], categoriesweekday_order, orderedTrue) # 透视表hour 为列weekday_name 为行 pivot_df df.pivot_table( indexweekday_name, columnshour, valuesstudent_id, aggfunccount, fill_value0 ) plt.figure(figsize(10, 6)) sns.heatmap(pivot_df, cmapYlOrRd, cbar_kws{label: Transaction Count}) plt.title(Hourly Transaction Heatmap by Weekday) plt.savefig(../results/hourly_heatmap.png, dpi300, bbox_inchestight)5.3 现象cluster_scatter.png中不同簇颜色重叠看不出分组原因plt.scatter()未设置alpha透明度且点数过多2376 点导致完全糊成一片。解决用alpha0.6s15控制点大小并添加cmap显式映射# src/visualization.py 中散点图代码 def plot_cluster_scatter(X, labels, titleCluster Distribution): plt.figure(figsize(8, 6)) scatter plt.scatter( X[:, 0], X[:, 1], # 取前两个主成分或 freq_weekly vs amount_mean clabels, cmaptab10, alpha0.6, s15, edgecolorsnone ) plt.colorbar(scatter, labelCluster ID) plt.xlabel(Frequency (Weekly)) plt.ylabel(Amount Mean (¥)) plt.title(title) plt.savefig(../results/cluster_scatter.png, dpi300, bbox_inchestight)5.4 现象results/cluster_report.txt中 “食堂偏好簇” 描述为 “canteen_ratio: 0.82”但实际查看该簇学生消费记录发现 30% 是超市购物原因canteen_ratio是均值但该簇内部分学生canteen_ratio0.95部分canteen_ratio0.65均值 0.82 掩盖了内部异质性。解决报告中必须补充标准差和分布直方图# src/reporting.py 中增强版报告 def generate_cluster_report(df, labels): df[cluster] labels report_lines [] for cluster_id in sorted(df[cluster].unique()): cluster_df df[df[cluster] cluster_id] canteen_ratio_mean cluster_df[canteen_ratio].mean() canteen_ratio_std cluster_df[canteen_ratio].std() report_lines.append(f\nCluster {cluster_id}:) report_lines.append(f Size: {len(cluster_df)} students) report_lines.append(f Canteen ratio: {canteen_ratio_mean:.2f} ± {canteen_ratio_std:.2f}) # 添加直方图存为 png plt.figure(figsize(6, 4)) plt.hist(cluster_df[canteen_ratio], bins10, alpha0.7, colorskyblue) plt.xlabel(Canteen Ratio) plt.ylabel(Student Count) plt.title(fCluster {cluster_id} Canteen Ratio Distribution) plt.savefig(f../results/cluster_{cluster_id}_canteen_dist.png, dpi300, bbox_inchestight) with open(../results/cluster_report.txt, w) as f: f.write(\n.join(report_lines))后悔药如果已生成报告才发现问题直接打开results/cluster_0_canteen_dist.png查看分布形态——单峰尖锐说明同质双峰说明存在子群体需进一步细分。6. 高分技巧用聚类结果反向验证业务假设让报告从“描述”升级为“诊断”6.1 业务假设驱动的验证框架三步法锁定高价值洞察高分课程设计与普通作业的本质区别在于是否用数据证伪或证实真实业务问题。例如教务处提出假设“大四学生因实习减少校内消费”。不能只写“大四学生消费频次较低”而要构建验证链定义对照组从聚类结果中提取freq_weekly 2的低频消费簇记为 Cluster A交叉验证查询 Cluster A 中学生年级分布data/student_info.csv提供年级字段归因分析若 Cluster A 中大四占比达 78%再检查其interval_mean_hours是否显著高于其他年级 48 小时佐证“非日常消费”特性# src/validation/business_hypothesis.py def validate_graduate_hypothesis(cluster_df, student_info_df): # 获取 Cluster A 的学生 ID cluster_a_ids cluster_df[cluster_df[cluster] 0][student_id].tolist() # 关联年级信息 grad_students student_info_df[ student_info_df[student_id].isin(cluster_a_ids) (student_info_df[grade] 2019) # 大四对应 2019 级 ] # 计算大四占比 grad_ratio len(grad_students) / len(cluster_a_ids) if cluster_a_ids else 0 # 检查时间间隔 avg_interval cluster_df[cluster_df[cluster] 0][interval_mean_hours].mean() return { grad_ratio: grad_ratio, avg_interval_hours: avg_interval, support_evidence: grad_ratio 0.7 and avg_interval 48 } # 调用示例 hypothesis_result validate_graduate_hypothesis(clustered_df, student_info) print(f大四学生占比: {hypothesis_result[grad_ratio]:.1%}) print(f平均消费间隔: {hypothesis_result[avg_interval_hours]:.1f} 小时) print(f假设成立: {hypothesis_result[support_evidence]})参数说明student_info.csv是配套数据集中的学生属性表含student_id,grade,major,gender必须与消费表通过student_id关联grade字段为入学年份2019大四避免用year字段易与消费年份混淆。6.2 生成可交付的诊断建议从“是什么”到“怎么办”高分报告最后一节必须给出可落地的行动项而非“建议加强管理”。例如针对entropy_weekday 1.2 的“规律型”学生簇共 412 人可输出问题定位数据证据建议动作责任部门早餐覆盖不足该簇 83% 消费集中在 11:00-13:00但 7:00-9:00 仅占 2.1%在教学楼一楼增设移动早餐车试点 3 个点位后勤集团打印服务错峰难peak_hour_ratio0.42集中在 14:00-16:00课后开放夜间打印权限20:00-22:00 免费图书馆超市消费粘性弱canteen_ratio0.89超市消费仅占 4.3%且多为应急购买推出“超市-食堂联名套餐券”满 20 减 5商户运营部# src/reporting/diagnostic_recommendation.py def generate_diagnostic_report(cluster_summary): recommendations [] # 规律型簇entropy_weekday 1.2 regular_cluster cluster_summary[cluster_summary[entropy_weekday] 1.2] if not regular_cluster.empty: # 早餐缺口计算 morning_ratio regular_cluster[hour_7_to9_ratio].mean() if morning_ratio 0.05: recommendations.append({ issue: 早餐覆盖不足, evidence: f7-9点消费占比仅{morning_ratio:.1%}, action: 教学楼一楼增设移动早餐车, dept: 后勤集团 }) # 生成 Markdown 报告 md_lines [# 校园消费行为诊断建议] for rec in recommendations: md_lines.append(f## {rec[issue]}) md_lines.append(f- **数据证据**{rec[evidence]}) md_lines.append(f- **建议动作**{rec[action]}) md_lines.append(f- **责任部门**{rec[dept]}) md_lines.append() with open(../results/diagnostic_recommendation.md, w) as f: f.write(\n.join(md_lines))实战经验教务处评审最看重“建议动作”是否具体含地点、时间、资源、是否可衡量如“增设3个点位”而非“优化服务”。我在去年指导的作业中有同学凭此诊断建议获得后勤集团书面采纳函直接加分 15%。希望帮到你。最后说一句血泪教训别在答辩前一晚改main.py——我曾因临时加一行plt.tight_layout()导致所有图片空白重跑 3 小时才救回来。现在我的习惯是代码冻结后先python main.py --plot_all False生成纯文本报告确认逻辑无误再开图。本文还有配套的精品资源点击获取
返回列表