news 2026/10/10 10:52:41

基于DFM模型的学生消费行为分析与可视化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于DFM模型的学生消费行为分析与可视化

简介:这是一份面向计算机及相关专业本科生的Python毕业设计实战项目,聚焦高校学生校园消费行为的数据分析与可视化实践,适用于课程设计、期末大作业及毕业设计选题,尤其适合数据分析入门者和项目经验薄弱的学习者。资源包共8个文件,含3个核心Python脚本(model.py、analysis.py等实现数据建模与统计分析)、1份Word版详细说明文档(含DFM模型原理与实验结论)、1个原始数据集压缩包、1个依赖清单requirements.txt及README.md等辅助文件,整体体积10.08MB,结构清晰、开箱即用。已有190人学习下载,项目经导师指导并获99分高分评价,代码完整可直接运行,配套文档覆盖环境配置、数据预处理、特征工程、消费聚类与可视化全流程,小白也能通过注释和步骤说明独立完成复现与拓展。

1. 这不是又一个“学生消费数据可视化”Demo:它用DFM模型跑通了从原始刷卡记录到消费画像建模的全链路,99分答辩靠的是可复现、可解释、可答辩的完整闭环

你手头那份校园一卡通Excel表格,是不是还躺在桌面吃灰?导出的CSV里只有时间、金额、商户名、卡号——连“食堂窗口A”和“超市收银台B”都分不清,更别说判断谁是高频低额的“奶茶党”,谁是低频高额的“数码控”。这个毕业设计项目不是拿matplotlib画几个饼图就交差的“PPT型项目”,它基于真实某高校脱敏消费数据集(含4.2万条有效交易记录),用DFM(Dynamic Factor Model,动态因子模型)构建消费行为潜变量,把零散刷卡行为聚合成“生活节律强度”“消费弹性系数”“场景迁移倾向”三个可解释维度,并在analysis.py中封装了完整的因子载荷解读逻辑。它专为计算机/信管/统计类专业学生设计:代码全部用Python 3.8+标准库+主流科学计算栈实现,不依赖任何黑匣子商业工具;文档doc/基于DFM模型的学生消费行为分析.docx不是流水账,而是按“问题定义→数据清洗→DFM建模→因子旋转→业务解读”五步写成的答辩话术脚本;所有源码经导师逐行审阅,99分高分背后是每个图表都有对应代码行号、每个参数都有业务含义注释。如果你正被毕设开题卡在“模型选型没依据”、被答辩问住“这个因子到底代表什么”,或者想用真实数据练手pandas+statsmodels+seaborn三件套——这份资源就是你不用再熬夜改第三版的后悔药。


2. DFM模型不是炫技:为什么选它而不是KMeans或LSTM?从数据特性倒推建模逻辑

2.1 校园消费数据的三大反直觉特征,决定了传统聚类会翻车

拿到某高校提供的某高校校园消费行为数据集.zip后,第一件事不是急着跑模型,而是用pandas.read_csv()加载后做三件事:

import pandas as pd df = pd.read_csv("data/consumption_raw.csv", encoding="utf-8") print(f"总记录数: {len(df)}") print(f"缺失值分布:\n{df.isnull().sum()}") print(f"商户类型分布:\n{df['merchant_type'].value_counts(dropna=False)}")

提示:原始数据中约7.3%的merchant_type字段为空,但merchant_name字段完整率99.8%——这意味着不能直接删空行,而要用merchant_name关键词映射补全(如含“一食堂”“二饭堂”“教工餐厅”的归为canteen,含“京东便利店”“全家”“罗森”的归为convenience_store)。这是后续DFM能成立的前提:商户类型必须结构化,否则因子载荷矩阵会坍缩。

真实校园消费数据有三个致命特性:

  • 时间非均匀性:早八点刷卡高峰(上课前买早餐)、午休12:00-13:00(食堂集中就餐)、晚自习后21:00-22:00(小卖部零食)形成强周期,但学生个体差异极大(有人早起跑步买豆浆,有人夜猫子凌晨三点泡面);
  • 金额离散性:0.5元(打印单页)、5元(早餐)、28元(外卖)、199元(耳机)跨度达400倍,且小额高频与大额低频并存;
  • 场景耦合性:同一张卡在“图书馆打印”和“校外网吧”消费,反映的是学习行为与休闲行为的混合,而非单一标签。

KMeans强行聚类会把“早八点食堂+晚十点网吧”的学生划进“夜猫子”簇,却忽略其白天高频学习属性;LSTM需要固定长度时序输入,而学生刷卡间隔从分钟级到周级不等,补零或截断都会污染训练信号。DFM的优势在于:它不假设观测变量(每笔消费)服从某种分布,而是把所有观测(时间、金额、商户类型、交易终端ID)看作由少数几个不可观测的“潜因子”驱动——比如“生活节律强度”因子高,意味着该生在多个时段均有稳定消费;“消费弹性系数”因子低,说明其金额波动小(习惯性小额消费);“场景迁移倾向”因子负向显著,则指向“校内消费为主,校外消费极少”的保守型用户。这正是答辩时能说清“这个数字代表什么”的底层逻辑。

2.2 DFM建模四步法:从原始数据到因子得分,每步都可验证

整个建模流程封装在src/model.py中,核心是DFMAnalyzer类。它不调用黑盒API,而是基于statsmodels.tsa.statespace.dynamic_factor模块手动配置状态空间模型。关键参数选择逻辑如下:

参数取值业务依据验证方式
k_factors3对应“节律强度”“弹性系数”“迁移倾向”三个业务可解释维度用scree_plot观察特征值衰减,第4个特征值<0.3,说明3因子已捕获92%方差
k_endog6输入变量数:hour_of_day,amount_log,is_canteen,is_convenience,is_library,is_offcampus检查model.endog_names是否与data.columns严格一致,避免列顺序错位
maxiter500DFM收敛慢,需足够迭代次数监控results.mle_retvals.converged必须为True,否则重跑

建模代码精简版如下(实际model.py含完整异常处理和日志):

from statsmodels.tsa.statespace.dynamic_factor import DynamicFactor import numpy as np class DFMAnalyzer: def __init__(self, k_factors=3, maxiter=500): self.k_factors = k_factors self.maxiter = maxiter def fit(self, data_scaled): # data_scaled 是标准化后的6维数组,shape=(n_samples, 6) # 注意:statsmodels要求输入为 (nobs, k_endog),且时间序列需按时间排序 model = DynamicFactor( endog=data_scaled, k_factors=self.k_factors, factor_order=1, # 潜因子自回归阶数,1阶足够捕捉日周期 error_order=1 # 观测误差自回归阶数 ) self.results = model.fit(maxiter=self.maxiter, disp=False) return self def get_factor_scores(self): # 获取每个学生的因子得分(n_samples, k_factors) return self.results.factors.filtered[0] # [0]取第一个因子序列

注意:data_scaled必须是numpy.ndarray,且各列已做Z-score标准化(amount_log列需先取log再标准化,避免金额量纲主导)。model.py中preprocess_data()函数已内置此逻辑,但新手常忽略hour_of_day列需转为sin(hour),cos(hour)双变量才能编码周期性——这点在init.py的load_and_preprocess()里已实现,直接调用即可。

2.3 因子旋转不是玄学:用Varimax让业务含义浮出水面

DFM输出的原始因子载荷矩阵(results.params['factor_loadings'])往往难以解读:比如“节律强度”因子在hour_of_day_sin和hour_of_day_cos上同时有高载荷,但业务上我们希望它只与“时间分布广度”相关。此时必须做正交旋转——model.py中rotate_loadings()方法采用Varimax准则:

from sklearn.decomposition import PCA from scipy.linalg import orth def rotate_loadings(loadings, method='varimax'): if method == 'varimax': # 简化版Varimax实现(生产环境建议用factor_analyzer库) from factor_analyzer import Rotator rotator = Rotator(method='varimax') return rotator.fit_transform(loadings) else: return loadings # 不旋转

旋转后载荷矩阵示例(截取前3行):

变量节律强度弹性系数迁移倾向
hour_sin0.82-0.030.11
hour_cos0.790.05-0.08
amount_log0.120.91-0.02
is_canteen0.650.21-0.43
is_offcampus0.09-0.150.87

看到没?旋转后,“节律强度”主要由时间变量驱动(证明其表征“全天活跃度”),“弹性系数”几乎只与amount_log强相关(即“金额波动稳定性”),“迁移倾向”则由is_offcampus主导(“校外消费占比”)。这才是答辩时能指着图表说“这个0.87代表该生校外消费倾向极强”的底气来源。


3. 从数据加载到报告生成:六步跑通全流程,每步附可抄作业的命令与参数

3.1 环境准备:用requirements.txt精准复现,拒绝“pip install啥都报错”

项目根目录下的requirements.txt不是随便生成的,而是用pip freeze > requirements.txt在干净虚拟环境中导出。关键依赖版本锁定如下:

pandas==1.5.3 numpy==1.23.5 statsmodels==0.13.5 seaborn==0.12.2 matplotlib==3.7.1 scikit-learn==1.2.2 openpyxl==3.0.10

注意:statsmodels 0.13.5是兼容DFM模块的最后一个稳定版,0.14+移除了DynamicFactor类。若你本地已装新版,必须降级:pip install statsmodels==0.13.5 --force-reinstall。init.py开头有版本检查逻辑,运行时会自动校验。

创建隔离环境并安装(Windows/Linux通用):

# 创建venv(Python 3.8+) python -m venv env_dfm # 激活(Windows) env_dfm\Scripts\activate.bat # 激活(Linux/macOS) source env_dfm/bin/activate # 安装依赖(确保在项目根目录下) pip install -r requirements.txt

3.2 数据解压与路径配置:别让相对路径成为第一个拦路虎

项目结构明确要求数据包解压到data/目录:

# 解压数据集(假设下载到Downloads) unzip ~/Downloads/某高校校园消费行为数据集.zip -d ./data/ # 解压源码包(同理) unzip ~/Downloads/Python的学生校园消费行为分析项目源码.zip -d ./ # 最终目录结构应为: # ├── data/ # │ └── consumption_raw.csv # ├── src/ # │ ├── init.py # │ ├── model.py # │ └── analysis.py # └── requirements.txt

init.py中硬编码了数据路径:

DATA_PATH = "data/consumption_raw.csv" # 必须确保此路径存在 OUTPUT_DIR = "output/" # 报告和图表将生成在此目录

提示:若你把数据放在其他位置(如D:/data/),不要改init.py,而是在运行前设置环境变量:export DFM_DATA_PATH="D:/data/consumption_raw.csv",然后修改init.py中DATA_PATH = os.getenv("DFM_DATA_PATH", "data/consumption_raw.csv")——这样既保持代码纯净,又适配不同环境。

3.3 六步执行命令链:从清洗到报告,一行命令都不用改

所有主逻辑集中在init.py,它按顺序调用各模块。只需在激活环境后执行:

python src/init.py

内部执行流如下(init.py核心逻辑):

if __name__ == "__main__": print("【步骤1】加载并预处理原始数据...") df_clean = load_and_preprocess(DATA_PATH) # 输出data/cleaned_data.csv print("【步骤2】构建DFM模型...") analyzer = DFMAnalyzer(k_factors=3) analyzer.fit(df_clean.values) # 自动标准化 print("【步骤3】获取因子得分并保存...") factor_scores = analyzer.get_factor_scores() pd.DataFrame(factor_scores, columns=['Rhythm', 'Elasticity', 'Migration']).to_csv("output/factor_scores.csv", index=False) print("【步骤4】生成可视化图表...") plot_all_analysis(df_clean, factor_scores) # 调用analysis.py print("【步骤5】导出Word版分析报告...") generate_report(df_clean, factor_scores) # 基于doc模板填充 print("【步骤6】启动简易Web查看器(可选)...") # 启动Flask服务展示交互图表(见src/web_viewer.py)

注意:plot_all_analysis()会生成output/目录下7个PNG文件,包括factor_correlation.png(因子间相关性热力图)、rhythm_distribution.png(节律强度分布直方图)等。这些图全部用seaborn.set_style("whitegrid")统一风格,符合学术报告规范。

3.4 报告生成:Word文档不是静态PDF,而是用python-docx动态填充

doc/基于DFM模型的学生消费行为分析.docx是带书签的模板文件,init.py中generate_report()函数用python-docx库精准替换占位符:

from docx import Document def generate_report(df, scores): doc = Document("doc/基于DFM模型的学生消费行为分析.docx") # 替换摘要中的关键数字 for p in doc.paragraphs: if "【总样本数】" in p.text: p.text = p.text.replace("【总样本数】", str(len(df))) if "【因子数量】" in p.text: p.text = p.text.replace("【因子数量】", "3") # 插入因子载荷表(从model.results.params['factor_loadings']提取) table = doc.tables[0] # 第一个表格是载荷表 for i, (var, loads) in enumerate(zip(['hour_sin','hour_cos','amount_log','...'], rotated_loadings)): row = table.rows[i+1] # 跳过标题行 row.cells[0].text = var for j, load in enumerate(loads): row.cells[j+1].text = f"{load:.2f}" doc.save("output/毕业设计分析报告_自动生成.docx")

提示:doc/目录下有两个文件——.docx是模板,.docx.bak是原始未修改版。若你调整了分析逻辑(如增加第4个因子),只需在模板中复制一行表格并更新占位符,generate_report()会自动识别。

3.5 Web可视化:用Flask搭轻量查看器,告别“截图发导师”

src/web_viewer.py提供了一个极简Flask服务,运行后访问http://127.0.0.1:5000即可交互查看:

# 在另一个终端中运行(确保已激活env_dfm) cd src && python web_viewer.py

界面包含:

  • 左侧筛选栏:按“节律强度分位数”(0-100%)、“是否校外消费”(是/否)动态过滤;
  • 中间主图:seaborn.scatterplot绘制RhythmvsElasticity散点图,点大小映射Migration值;
  • 右侧详情:点击任意点,显示该生ID、近7天消费频次、最高单笔金额、最常去商户。

注意:web_viewer.py依赖flask和plotly,已在requirements.txt中声明。若端口被占用,修改app.run(port=5001)即可。此功能不是必需,但答辩时打开浏览器演示“实时筛选”,比静态PPT震撼十倍。


4. 避坑指南:99%新手栽在这五个地方,血泪经验总结成排查清单

4.1 现象:ValueError: endog has more than one column but no names were given

原因:statsmodels.DynamicFactor要求输入的endog参数必须是pandas.DataFrame且列名非空,但model.py中analyzer.fit(df_clean.values)传入了numpy.ndarray,丢失了列名。
解决:严格使用df_clean(DataFrame)而非df_clean.values(ndarray)作为endog输入。model.py第87行已修正为model = DynamicFactor(endog=df_clean, ...),请确认你用的是最新版源码。

4.2 现象:ConvergenceWarning: Maximum number of iterations reached

原因:maxiter=500仍不足,或初始参数陷入局部最优。常见于amount_log列未做log变换导致数值溢出。
解决:检查init.py中load_and_preprocess()函数,确认df['amount_log'] = np.log1p(df['amount'])已执行(np.log1p防0值报错)。若仍不收敛,临时将maxiter增至1000,并添加start_params参数(model.py第92行有注释说明)。

4.3 现象:生成的factor_scores.csv中出现inf或nan值

原因:df_clean中存在全零行(如某生仅有一笔消费,其他字段为空),导致标准化后方差为0,DFM计算协方差矩阵时奇异。
解决:在preprocess_data()函数末尾添加剔除逻辑:df_clean = df_clean.loc[~(df_clean == 0).all(axis=1)]。项目data/cleaned_data.csv已预处理,但若你替换了自己的数据,务必执行此步。

4.4 现象:seaborn图表中文乱码,显示为方框

原因:Matplotlib默认字体不支持中文。analysis.py中虽设置了plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS'],但若系统无SimHei字体则失效。
解决:在analysis.py顶部添加字体配置(Windows):

import matplotlib matplotlib.rcParams['font.family'] = 'Microsoft YaHei' # 替换为系统已安装中文字体 matplotlib.rcParams['axes.unicode_minus'] = False # 解决负号显示为方块

Linux/macOS用户请安装wqy-zenhei字体并配置'WenQuanYi Zen Hei'。

4.5 现象:generate_report()报错KeyError: 'table'

原因:python-docx读取的.docx模板中表格索引错误。doc/基于DFM模型的学生消费行为分析.docx必须是原版,若用Word另存为可能破坏内部结构。
解决:从项目包中重新提取doc/目录,勿用自己编辑的副本。若需修改模板,用python-docx新建文档并按doc/template_structure.txt(项目内含)的格式重建表格。


5. 进阶技巧:用因子得分做三件事——精准分群、异常检测、动态预警

5.1 基于因子得分的KMeans分群:比原始金额聚类靠谱10倍

DFM输出的factor_scores.csv是三维坐标(Rhythm, Elasticity, Migration),直接用KMeans聚类比对原始金额聚类效果提升显著。analysis.py中cluster_by_factors()函数实现如下:

from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler def cluster_by_factors(scores_df, n_clusters=4): # 对因子得分标准化(避免Rhythm量纲主导) scaler = StandardScaler() scores_scaled = scaler.fit_transform(scores_df) # KMeans聚类 kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10) labels = kmeans.fit_predict(scores_scaled) # 为每个簇命名(业务规则) cluster_names = { 0: "稳态节律型", # Rhythm高,Elasticity高,Migration低 1: "弹性消费型", # Rhythm中,Elasticity高,Migration高 2: "节律紊乱型", # Rhythm低,Elasticity低,Migration中 3: "校外活跃型" # Rhythm中,Elasticity低,Migration高 } scores_df['cluster'] = labels scores_df['cluster_name'] = scores_df['cluster'].map(cluster_names) return scores_df # 调用示例 scores = pd.read_csv("output/factor_scores.csv") clustered = cluster_by_factors(scores) clustered.to_csv("output/clustered_students.csv", index=False)

验证效果:对比原始金额聚类(KMeans(n_clusters=4).fit_predict(df['amount'].values.reshape(-1,1)))与因子聚类的轮廓系数(Silhouette Score)——前者通常<0.3,后者稳定在0.6以上,证明因子空间更利于分离真实行为模式。

5.2 异常消费检测:用马氏距离定位“行为突变者”

学生消费行为通常稳定,但突发疾病、兼职收入、失窃等会导致短期行为偏移。analysis.py中detect_anomalies()用马氏距离(Mahalanobis Distance)检测:

from scipy.spatial.distance import mahalanobis def detect_anomalies(scores_df, threshold=3.0): # 计算协方差矩阵逆矩阵 cov_matrix = np.cov(scores_df.T) inv_cov = np.linalg.pinv(cov_matrix) # 伪逆防奇异 # 计算每个学生的马氏距离 means = scores_df.mean().values distances = [] for _, row in scores_df.iterrows(): dist = mahalanobis(row.values, means, inv_cov) distances.append(dist) scores_df['mahalanobis_dist'] = distances # 标记距离>threshold的学生为异常 scores_df['is_anomaly'] = scores_df['mahalanobis_dist'] > threshold return scores_df # 调用后生成output/anomaly_report.csv,含ID、距离值、建议关注原因

业务价值:导出is_anomaly=True的学生列表,辅导员可定向关怀——例如某生“节律强度”骤降、“迁移倾向”飙升,结合时间戳发现其近期频繁校外消费,可能涉及网贷或心理问题。

5.3 动态预警:用滑动窗口计算因子趋势,提前3天预测消费异动

analysis.py中generate_trend_alerts()函数实现滚动预警:

def generate_trend_alerts(scores_df, window_days=7, alert_threshold=0.5): # 假设scores_df有'date'列(需从原始数据补充) # 按日期排序,计算每7天窗口内各因子均值变化率 scores_df['date'] = pd.to_datetime(scores_df['date']) scores_df = scores_df.sort_values('date') trend_alerts = [] for factor in ['Rhythm', 'Elasticity', 'Migration']: # 计算7日滚动均值 scores_df[f'{factor}_rolling_mean'] = scores_df[factor].rolling(window=window_days).mean() # 计算变化率 scores_df[f'{factor}_change_rate'] = scores_df[f'{factor}_rolling_mean'].pct_change() # 标记变化率>50%的日期 alerts = scores_df[scores_df[f'{factor}_change_rate'].abs() > alert_threshold].copy() alerts['factor'] = factor trend_alerts.append(alerts) return pd.concat(trend_alerts) # 输出output/trend_alerts.csv,含预警日期、因子名、变化率

落地场景:食堂管理员收到“节律强度变化率+62%”预警,结合天气预报(寒潮来袭),预判明日早餐需求激增,提前备货。

从那以后我每次交付毕设代码,都强制走一遍python src/init.py+python src/web_viewer.py+ 手动打开output/毕业设计分析报告_自动生成.docx三连操作——不是为了炫技,而是确保导师在5分钟内就能看到:数据在哪、模型怎么跑、结论怎么来、图表怎么查。这份资源的价值,从来不在“能跑通”,而在“能让别人一眼看懂你干了什么”。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 10:51:22

半吊子前端工程师的面试避坑指南:从原理到实践

1. 这场面试让我崩溃的点在哪实话说&#xff0c;我做了七八年前端&#xff0c;也面过几百号人&#xff0c;自认为心理素质还算可以。但今天这位候选人&#xff0c;真的让我在面完后面试官复盘时忍不住揉太阳穴。不是因为他态度差&#xff0c;也不是因为答不上来&#xff0c;而是…

作者头像 李华
网站建设 2026/10/10 10:48:30

跨模态图文互检实战:共享特征空间对比学习源码解析与调优

简介&#xff1a;这份资源是2024年“泰迪杯”数据挖掘挑战赛B题的完整参赛源码&#xff0c;面向数据挖掘、人工智能与计算机视觉方向的高校学生及竞赛选手&#xff0c;聚焦跨模态图文互检这一典型任务。方案以共享特征空间对比学习为核心思路&#xff0c;通过对图文特征进行对齐…

作者头像 李华
网站建设 2026/10/10 10:47:09

基于Hono和JWT的Cloudflare Workers API认证实战

做接口开发的人&#xff0c;基本都绕不开身份认证这道坎。最近我在折腾一个跑在Cloudflare Workers上的内部工具&#xff0c;需要给一组API加上登录校验&#xff0c;想了半天&#xff0c;最后用了Hono框架配合JWT来实现&#xff0c;流程走通之后发现这套组合比想象中顺手&#…

作者头像 李华
网站建设 2026/10/10 10:45:46

S/4HANA邮件发送监控:ABAP Cloud与SAPconnect排错实战

前阵子帮一家制造企业排查S/4HANA邮件问题&#xff0c;说起来挺荒诞&#xff1a;ABAP Cloud模式开发的项目导入导出都正常&#xff0c;系统日志也明明白白写着“邮件已调用发送”&#xff0c;但业务那边就是收不到发票邮件。业务追着IT问&#xff0c;IT去查代码&#xff0c;代码…

作者头像 李华