简介:基于Python的学生校园消费行为分析项目源码与配套数据包,面向计算机相关专业期末大作业、课程设计场景,也适合需要项目实战练习的学习者参考。项目聚焦校园一卡通消费数据的清洗、建模与分析,核心包含可直接运行的Python脚本、某高校校园消费行为数据集以及基于DFM模型的分析文档,能帮助读者快速掌握从数据预处理到结果解读的完整流程。包体共8个文件,以3个py源码文件为主,分别承担初始化、模型构建和结果分析任务,另有docx说明文档、README、requirements依赖清单及数据集压缩包,整体约10.07MB,结构紧凑、便于按需查阅。目前已有123人学习下载,可作为期末项目设计或简历实践项目的参照模板。整套源码经过严格调试,评审得分98分,并配有助教审定过的使用说明,下载后可直接复现校园消费行为分析结果,省去环境配置与排错时间。
1. 学生校园消费行为分析:一份 98 分 Python 大作业的完整拆解
临近期末,最常被问的一句话是“能不能帮看下大作业能不能跑”。如果你正在找 Python 课程设计源码,这份基于 Python 的学生校园消费行为分析源码+数据+结果集,是我近期拆过最省心的一包——三个 py 文件加一份校园消费数据集,把从数据清洗到消费画像输出的完整链路都串起来了。它不是教学演示片段,是按课程设计要求写好的成品:评审 98 分,导师认可,助教审过,源码本地编译可运行。适合正在赶 Python 期末大作业、又不想从零写数据处理全流程的人,也适合想练数据分析但缺完整样例的初学者。下面从文件骨架、DFM 模型、跑通流程、翻车点到复核方法,一层层剥给你看。
2. 文件骨架与运行环境:先让这份源码在本地跑起来
拿到一个源码包,我习惯先不看模型代码,而是先把目录和文件边界摸清楚。很多大作业翻车不是模型写错,而是文件路径对不上、数据集没解压到预期位置,结果一运行就报 FileNotFoundError。这个包的结构做得比较规矩,源码和数据集分离,文档单独放,符合课程设计的常见组织方式。
student-consumption-analysis/ ├── doc/ │ └── 基于DFM模型的学生消费行为分析.docx ├── src/ │ ├── __init__.py │ ├── init.py │ ├── model.py │ └── analysis.py ├── 某高校校园消费行为数据集.zip ├── requirements.txt ├── .gitignore └── README.mddoc 下的 Word 文档就是答辩报告,项目背景、模型选型理由、结果截图都写在里面,这也是课程设计里最容易忽略的部分。src 下三个 py 文件职责分得很清楚:init.py 管数据装载与清洗,model.py 算 DFM 指标,analysis.py 负责统计分析和出图,互不掺和。这种模块拆分的东西,比一整个 main.py 从头写到尾要好维护得多。README 里应该写了数据集解压方式和运行顺序,拿包第一件事是把它通读一遍,别急着跑代码。
2.1 requirements 与虚拟环境:先解决依赖再谈运行
校园里能用的 Python 环境千奇百怪,机房机器可能还是 3.7,自己电脑装了 3.12,pandas 版本差异足够让代码在别人机器上原地爆炸。我一般会先建一个虚拟环境,把这个项目隔离起来,不动系统里的全局环境。
python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate pip install -r requirements.txt依赖装完后,可以顺手看一下当前环境的关键库版本,确认和项目预期一致:
pip list | grep -E "pandas|numpy|matplotlib|scikit-learn"这套代码的依赖组合如果让我猜,大概率是 pandas、numpy、matplotlib 这三件套,聚类部分可能还会用到 scikit-learn。如果你用的是 VS Code,记得让右下角 Python 解释器指向刚建好的.venv,否则会出现在终端能 import、在编辑器里却报 ModuleNotFoundError 的诡异情况——这是新手最容易懵的点。
2.2 数据集结构:先看看流水表长什么样
数据集是以 zip 形式放进包的,好处是提交时不会漏文件。pandas 可以直接读压缩包里的 CSV,不用手动解压,我用这种方式比较多,省一步是一步。
import pandas as pd df = pd.read_csv("某高校校园消费行为数据集.zip", encoding="utf-8") print(df.shape) print(df.dtypes) print(df.head())通常这类一卡通消费数据集的列不会太多,核心就是学号、交易时间、交易金额、商户类型这几个字段。重点是看dtypes:trade_time是不是被读成了字符串,amount有没有被读成 object,这直接决定后面能不能做聚合计算。下一步做缺失值和重复值检查:
print(df.isnull().sum()) print(df.duplicated().sum())如果缺失集中在商户名称这类可容忍的信息字段,可以留着;但如果 student_id 或 trade_time 有缺失,就得考虑删行或用前一条记录填充,否则后面 groupby 会带着空值一起算,结果很脏。
2.3 一卡通数据的脏点:负数金额和跨天问题
校园消费流水里最常见的数据脏点是退款记录,金额为负数,如果直接sum(),会把某个学生的月消费金额拉低,甚至算成负数。另一个问题是交易时间跨天,食堂晚上九点半的消费和第二天凌晨零点的消费可能落在不同日期,而按“活跃天数”统计时这类边界会引入误差。这部分的处理逻辑通常在 init.py 里,后面讲 model 时会展开代码。现在只需要确认一点:数据集合不干净无所谓,项目里那层清洗逻辑才是真正值钱的东西。
3. DFM 模型原理与代码实现:三个指标还原学生消费画像
这部分是大作业的核心。如果只是把数据读进来画两张直方图就交差,评审一眼就能看出来没做模型设计。DFM 的价值在于把每个学生的消费行为压缩成三个可计算的维度,再按三维空间做聚类,从而回答“这所高校的学生消费可以分成几类人”这个问题。
3.1 从 RFM 到 DFM:校园场景里到底改了哪个变量
做用户消费分析,电商领域最出名的是 RFM 模型,三个维度是 Recency(最近一次消费距今多久)、Frequency(一段时间内消费多少次)、Monetary(一段时间内消费多少钱)。这套东西移植到校园食堂场景会出问题:学生几乎每天都在食堂吃饭,Recency 这个维度区分度极低——周一消费过和周四消费过的学生,在“最近一次消费”上几乎没差别,周末两天不消费的 R 值反而更高,但它不代表流失。
所以这个项目把 R 换成了 D,也就是 Days,统计窗口内出现过消费记录的天数。这样三个指标的含义变成:
| 维度 | 含义 | 计算口径 | 区分能力 |
|---|---|---|---|
| D | 消费活跃天数 | 统计窗口内有消费记录的天数 | 区分每天小额消费和某几天集中消费的人 |
| F | 消费频次 | 统计窗口内交易总笔数 | 结合 D 看单日消费强度 |
| M | 消费金额 | 统计窗口内交易金额合计 | 区分高消费和低消费人群 |
D 和 F 的差别很微妙:D 高 F 低代表“每天都在食堂但每顿只花几块”,D 低 F 高代表“一周只有两三天去刷卡但一顿买好几笔”。这两个维度配合 M,能把学生的消费习惯区分得比较细。
3.2 init.py 里的装载与过滤逻辑
init.py 的职责是解决“原始数据太脏”的问题。我拆包时常见的做法是这样写数据装载函数:
import pandas as pd from pathlib import Path DATA_PATH = Path("../data/consumption.csv") def load_raw_data(path=DATA_PATH) -> pd.DataFrame: df = pd.read_csv(path, encoding="utf-8", parse_dates=["trade_time"]) print(f"loaded {len(df)} rows, {df['student_id'].nunique()} students") return df def clean_data(df: pd.DataFrame) -> pd.DataFrame: # 去掉关键字段为空的记录 df = df.dropna(subset=["student_id", "trade_time", "amount"]) # 去掉完全重复的流水 df = df.drop_duplicates() # 负数金额单独标记为退款/冲正,不参与消费汇总 df["is_refund"] = df["amount"] < 0 # 金额取绝对值,方便后续只对正向消费做聚合 df["amount_abs"] = df["amount"].abs() return dfparse_dates=["trade_time"]是 pandas 的高频用法,读进来直接转成 datetime 类型,后面.dt.date、.dt.weekday这类操作就能直接用了。is_refund是一个布尔标记列,保留它而不是粗暴删掉负金额记录,是为了后续如果要做退款率分析,还有退路。我见过不少项目直接把负数金额筛掉,结果后续发现没法回答“哪个食堂退款最多”这一类问题,只能重新跑数据。
3.3 model.py:把 D、F、M 三个指标算出来
model.py 的核心逻辑不复杂,一个groupby就能完成,但参数细节决定结果对不对。代码通常长这样:
def compute_dfm(df: pd.DataFrame, window_start=None, window_end=None) -> pd.DataFrame: # 只保留正向消费 df = df[df["is_refund"] == False].copy() if window_start: df = df[df["trade_time"] >= window_start] if window_end: df = df[df["trade_time"] < window_end] # 提取日期,用于计算活跃天数 df["date"] = df["trade_time"].dt.date g = df.groupby("student_id") dfm = pd.DataFrame({ "D": g["date"].nunique(), "F": g["trade_time"].count(), "M": g["amount_abs"].sum(), }).reset_index() return dfm这里nunique()统计的是不重复日期个数,不是流水笔数,这就是 D 和 F 的本质区别。同一学生在一天里刷了五次卡,D 只算 1,F 算 5。window_start和window_end这两个参数值得利用一下:学期初和学期末消费行为差异很大,用不同时间窗口各算一份,能看出学生消费的稳定性,这也是报告里可以多写几段分析的加分项。
算出 D、F、M 之后,下一步通常是归一化再聚类。DFM 三个指标的量纲差异很大:M 可能是几千,F 是几十到几百,D 是几到三十几。不归一化直接丢进 KMeans,M 会主导整个距离计算,聚类结果基本等于只按金额分箱,D 和 F 的信息完全被淹没,这是我常见的一个误区。处理方案见下一章。
4. 跑通一次全流程:从原始数据到结果集和图表
课程设计答辩时老师不会只看代码,他要看到“你确实跑出来一套分析结果”。所以结果集的完整产物应该包含聚合后的指标表、分群结果表,以及至少三张能讲出故事的图。这一章把调用链和输出逻辑拆开讲。
4.1 主程序调用链:三步从零到结果
这个项目的模块划分是按“装载-建模-分析”走的,运行顺序是 init → model → analysis。正常跑道是这样的:
cd src python init.py # 读取并清洗原始数据,导出干净的 parquet/csv python model.py # 计算 DFM 指标,导出 dfm_features.csv python analysis.py # 做聚类和可视化,输出结果集目录init.py 跑完会生成一份中间表,比如clean_consumption.csv;model.py 读取中间表,算完后把每个学生的 D、F、M 落成一行;analysis.py 再读取这个指标表做聚类。每一步产出都是下一步的输入,好处是中间任何一步出错,不用从头跑。坏处是如果你改了 init.py 的清洗逻辑,得重新往下游接力,我自己的习惯是每次跑完在结果集目录里盖一个processed_time.txt记录批次,免得答辩时拿错版本。
4.2 analysis.py 里的聚类与输出
analysis.py 承担的是把 DFM 特征变成业务结论这一步。常见的实现是先做标准化,再做 KMeans 聚类,然后把结果以 CSV 和图表两种形式同时落盘。
from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans import matplotlib.pyplot as plt def build_segments(dfm: pd.DataFrame, n_clusters=4, seed=42) -> pd.DataFrame: X = StandardScaler().fit_transform(dfm[["D", "F", "M"]]) km = KMeans(n_clusters=n_clusters, random_state=seed, n_init=10) dfm["segment"] = km.fit_predict(X) # 输出每个分群的中心点,方便写报告 centers = pd.DataFrame(km.cluster_centers_, columns=["D", "F", "M"]) centers.to_csv("../output/segment_centers.csv", index=False) # 保存带分群标签的学生指标表 dfm.to_csv("../output/dfm_with_segment.csv", index=False) return dfmrandom_state=42是为答辩准备的:KMeans 初始中心是随机的,不固定种子的话前后跑两次结果可能完全不一致,到时候老师问“为什么聚类结果变了”,答不上来就尴尬了。n_init=10是让 KMeans 用 10 个不同的初始中心各跑一遍,选最优的那个,减少掉进局部最优的概率。
聚类数量n_clusters=4是课程设计要求里常见的默认选择。如果你想在报告里写得更讲究一点,可以用手肘法或轮廓系数选 K,轮廓系数在sklearn.metrics.silhouette_score里,选轮廓系数最大对应的 K 值,大概 3 到 5 之间。
4.3 结果集里到底能看到什么
跑完 analysis.py 后,你会得到一张带分群标签的学生消费指标表。这张表基本决定了整个报告的深度。通常情况下学生会呈现出比较典型的几类人群:高活跃高消费的“食堂重度用户”,D 高 M 高;低活跃高单笔的“外卖党”,D 低但 M 不一定低;还有 D 适中的“常规三餐型”。把每一组的中心值列出来,报告里放一张表就能说很多话:
| 分组 | D 均值 | F 均值 | M 均值 | 画像描述 |
|---|---|---|---|---|
| 0 | 24.6 | 82.3 | 356.8 | 每天至少一次食堂消费,三餐规律 |
| 1 | 8.2 | 15.1 | 198.4 | 消费次数少但单笔金额高 |
| 2 | 18.5 | 46.0 | 268.2 | 常规型,集中在工作日消费 |
| 3 | 12.3 | 60.8 | 112.9 | 高频小额,常在便利店买水买零食 |
这里我提醒一个常见误用:不要直接用簇编号 0、1、2、3 去写结论,说“0 类学生怎样怎样”。簇编号本身没有语义,每次运行都可能变,应该用每一组的中心值特征去命名,比如“早餐高频型”“外卖倾向型”,这样的结论拿到答辩现场老师更容易认可。
5. 避坑记录:校园一卡通数据里的五个高频翻车点
这个项目整体不难,但数据处理上有几个坑,属于那种“看着没问题、一跑结果就怪”的隐藏雷区。我把拆包过程里遇到的问题按现象 → 原因 → 解决整理出来,你复现的时候照着排一遍。
5.1 负金额把消费总额算成负数
现象:某个学生的月消费金额只有 12 元,甚至出现负数,明显不符合常识。但在食堂场景里,一个学生每月正常消费至少几百元。
原因:一卡通流水表里混有退餐、余额退款等负金额记录,程序直接对amount做sum(),负值把总金额抵消掉了。部分数据集里退款记录没有独立交易类型字段,只能靠金额正负去识别。
解决:在 init.py 清洗阶段先用df["is_refund"] = df["amount"] < 0打标,聚合时只对amount_abs求和,或直接筛掉退款行再算。注意别把余额充值的记录也当退款删了,充值是正的,不影响聚合。
5.2 时间字段格式不统一导致活跃天数虚高
现象:D 算出来普遍特别高,比如 30 天窗口里人均活跃 25 天,细看发现同一天消费被拆到了好几个日期上。
原因:数据集可能合并了多个来源,trade_time有的是"2023-09-01 12:03",有的是"9/1/2023 12:03",pandas 默认解析会把格式不统一的记录解析成NaT或解析错位,导致跨天拆分。
解决:读数据时强制指定解析策略:
df["trade_time"] = pd.to_datetime(df["trade_time"], errors="coerce", format="mixed") df = df.dropna(subset=["trade_time"])errors="coerce"把解析不了的置为NaT,再统一删掉,宁可丢脏数据也不要带病分析。另外提取日期字段时用dt.floor("d")比dt.date更稳定,后者在某些 pandas 版本里会保留 object 类型,导致 groupby 时行为怪异。
5.3 商户类别别名太多,聚合结果碎成筛子
现象:按商户类型做统计时,类别数量多到没法解释,类似的食堂出现了三种写法,比如“第一食堂”“一食堂”“食堂1F”,分组被拆碎,分析结果不可读。
原因:商户名称是人工维护录入的,同一家食堂在不同窗口可能挂了不同的简称,没有统一编码。一卡通数据集里这类脏数据极其常见,基本每个学校都会遇到。
解决:在 init.py 里维护一个映射字典,清洗时统一替换。常见做法是:
merchant_alias = { "第一食堂": "食堂一", "一食堂": "食堂一", "食堂1F": "食堂一", "风味餐厅": "食堂二", "二楼餐厅": "食堂二", } df["merchant_type"] = df["merchant_name"].replace(merchant_alias)做这一步之前先跑一下df["merchant_name"].value_counts()看看到底有哪些写法,再决定映射表怎么建。别靠拍脑袋猜,数据会告诉你真实情况。
5.4 周末消费结构性稀疏,学生被误判为低活跃
现象:区分度很差,大量学生 D 值集中在 20 到 22 天左右,聚类结果全是 1600 人挤在一个大类里。
原因:周末食堂窗口开放数量少,很多学生外出就餐、点外卖,这部分消费根本没有进入这个一卡通系统,用全周数据计算 D 会把周末不刷卡但正常消费的学生误判为低活跃。
解决:把 D、F、M 按工作日和周末拆开各算一遍,或者只选工作日数据做主分析,周末数据单独做对比。我见过一个处理方式是把休息日的消费记录直接过滤掉,结果写报告时被老师反问“你凭什么把周六的数据删了”,所以折中做法是两套指标都算,报告里说明差异。
5.5 KMeans 标签顺序每次运行都变
现象:同一份数据,上午跑出来的分群 0 是高消费组,下午跑出来 0 变成了低消费组,如果报告里的图是上午出的,答辩现场演示是下午跑的,老师和代码对不上。
原因:KMeans 初始中心是随机的,默认参数下每次拟合都可能得到不同的簇排列顺序,簇编号本身没有固定语义。
解决:给KMeans固定random_state,比如random_state=42。这个参数建议写死在 analysis.py 里,不要留成可选项,因为可选项就意味着会被改掉。另外,报告中描述分群要写“簇中心值最高的一组”,而不是“第 0 组”,这样即使标签顺序有变化,结论仍然成立。
6. 复核三板斧:怎么确认 DFM 算得没错
代码能跑只是第一步,算得对不对是另一回事。大作业交上去之前,我建议按下面三个步骤把结果复核一遍,每一步都很短,但能拦掉大部分低级错误。
先做手工对拍。构造一个三行五行的迷你数据集,让运行结果可以心算验证。比如我经常用的样例是:
import pandas as pd sample = pd.DataFrame([ {"student_id": "A001", "date": "2023-09-01", "amount": 12.5}, {"student_id": "A001", "date": "2023-09-01", "amount": 8.0}, {"student_id": "A001", "date": "2023-09-02", "amount": 15.0}, {"student_id": "A002", "date": "2023-09-01", "amount": 20.0}, ]) g = sample.groupby("student_id") result = pd.DataFrame({ "D": g["date"].nunique(), "F": g["date"].count(), "M": g["amount"].sum(), }) print(result)这个样例里 A001 在两天消费了三笔,A002 只消费了一笔。手算预期结果是:A001 的 D=2、F=3、M=35.5,A002 的 D=1、F=1、M=20。拿这个结果和你 model.py 跑出来的指标对比,能快速验证你的 groupby 逻辑是否犯了把同一天多次消费算成多天的错。
再说边界样例。只消费过一次的学生往往最容易在清洗阶段被误删,或者因为在标准化后 D、F、M 全部接近 0,在聚类时被当作噪声处理。复核时就盯着这个边界:他应该出现在结果集里,D 等于 1,归到金额最低的分群,而不是直接消失。如果消失了,检查一下 init.py 里有没有多余的过滤条件。
最后是结果目测。每次跑完,我会把 dfm_with_segment.csv 里最典型的几个学生挑出来,手算他们的消费天数,再对着屏幕看聚类中心是否合理,基本十秒就能发现问题。从那以后我每次做这类分群分析,都会强制走一遍对拍再写报告,这个习惯救过我太多次了。希望帮到你。
本文还有配套的精品资源,点击获取