简介:面向数据挖掘课程设计的学生与从业者,这份资料以航空公司客户价值分析为完整案例,系统演示数据预处理、探索性分析、客户细分、预测建模与评估优化全流程。项目基于多维客户数据,包含数据清洗、标准化、KMeans聚类、逻辑回归与随机森林等典型方法,并注重数据隐私处理与结果业务化解读。压缩包共23个文件,以Python脚本、Excel数据表、CSV数据集为主,辅以XML配置和Word文档,整体约20.78MB。脚本涵盖数据探索、清洗、尺度变换及聚类实现,数据文件包含客户属性、飞行记录、消费金额等维度,文档则记录课程设计的方法选择、评分标准与实验结论,可帮助读者理解从原始数据到业务建议的完整路径。目前已有1254人学习下载,适合需要完成课程设计、备赛或入门客户价值分析的读者。
1. 从一份课程设计 zip 说起:航空公司客户价值分析到底在分析什么
如果你正在做数据挖掘课设或毕设,搜“航空公司客户价值分析数据挖掘设计源代码”时刷到这份 zip,大概率是被“源代码”三个字吸引进来的。这个题目的经典程度和它的名字一样老套,但每年仍然有大量学生卡在同一个地方:数据拿到了、代码能跑,可老师一问“为什么用这个模型、聚类结果怎么解释”就答不上来。这份数据挖掘设计.zip 本质上是一个完整复现套件,装的是数据、代码、报告和演示文件,解决的是从原始客户数据到聚类分群再到业务解读的整条链路。我的建议是不要把它当成“交作业即用”的素材,而是把它当一个可运行的样例工程来拆,适合正在做航空客户价值分析、会员分群、RFM 变体落地,或者想复现聚类算法完整流程的人。
2. 源码与数据集结构:先理清 zip 里的四种文件
2.1 解压后的文件清单与各自用途
拿到 zip 先别急着跑代码,我习惯第一步把解压目录铺开看一遍。这种课设包的标准结构基本是四类东西:数据集文件(通常是 csv 或 xlsx)、预处理和聚类的 Python 脚本(或 Jupyter Notebook)、实验报告文档、答辩 PPT。文件不多,但每类文件的角色天差地别。
| 文件/目录 | 典型内容 | 在复制时重点关注 |
|---|---|---|
| air_data.csv | 航空客户原始信息,每行一个会员,多条属性 | 字段是否被改动过、数据量与你预期是否一致 |
| 数据预处理.ipynb / preprocess.py | 缺失值清洗、剔除异常记录、LRFMC 指标计算 | 观测窗口截止时间怎么取、标准化用哪种方式 |
| 聚类分析.ipynb / cluster.py | K-Means 聚类、K 值选取、结果画像 | 是否固定了随机种子、聚类中心怎么输出 |
| 实验报告.doc/pdf | 背景、方法、结果与营销建议 | 是否与附带代码跑出的图、表对应 |
这里有个最常见的坑:报告里的截图和代码跑出来的结果对不上。原因多半是作者改过代码参数但没更新截图,或者你本地 pandas、sklearn 版本不同导致随机结果差异。拿到资源后第一件事是完整跑一遍,再用自己的结果替换报告里的旧图。
2.2 读懂字段字典:17 个字段里哪些能用、哪些是坑
航空客户数据的字段设计是有讲究的,不是每列都能直接塞进聚类。这份资源里的原始表通常包含会员卡号、入会时间、首次乘机时间、最后一次乘机时间、飞行次数、飞行总里程、累计票价、平均折扣率、性别、年龄、工作城市等十几列。我一般拿到数据后先做个快速体检。
import pandas as pd df = pd.read_csv("air_data.csv", encoding="gbk") print(df.shape) print(df.dtypes) print(df.isnull().sum()) # 看离散字段的取值分布,例如性别、会员等级 for col in ["GENDER", "FFP_TIER"]: if col in df.columns: print(df[col].value_counts())逻辑说明:df.shape确认行列数,df.dtypes看每个字段类型,isnull().sum()统计缺失值,这一套下来你就能判断数据质量。舱位等级、会员卡等级这类离散字段虽然可以用来讲故事,但在 LRFMC 模型里通常不直接参与聚类,因为它们的取值范围和含义跟 L、R、F、M、C 不在一个量纲上。
参数说明:编码方式用gbk是因为这类课程设计数据多半导出国产物料,用utf-8读会直接报 UnicodeDecodeError。如果你的环境读不了,改成encoding="utf-8"或encoding="gb18030"试试。性别、城市、等级这类字段先留着,聚类时不进特征矩阵,但后面做用户画像要用。
3. 构建 LRFMC 指标:预处理才是课设里最容易扣分的地方
3.1 从 RFM 到 LRFMC:为什么要多一个 L
教科书上对客户价值分析的标准套路是 RFM:最近一次消费时间(Recency)、消费频率(Frequency)、消费金额(Monetary)。这个模型放在零售、电商场景很好用,但做航空公司客户分析时有个明显问题:机票价格受季节、航线、提前购票时间影响太大,直接用消费金额衡量客户价值会误伤低票价航线的常旅客。而且航空会员里有一类重要客户,他们入会时间特别长、飞行频次高,但最近半年飞得少,RFM 会把这类人归为流失客户,这显然不合理。
所以航空场景的标准做法是把 RFM 改造成 LRFMC 五维指标:
| 指标 | 含义 | 对应原始字段的驱动方式 |
|---|---|---|
| L | 入会时间长度 | 观测窗口结束时间减去入会时间 |
| R | 最近一次乘机时间间隔 | 最后一次乘机日期距今的月数 |
| F | 飞行频率 | 累计飞行次数 |
| M | 飞行总里程 | 累计飞行公里数 |
| C | 平均折扣系数 | 平均折扣率,反映舱位等级偏好 |
多出来的 L 用来识别“老会员”的价值,C 用来识别“高舱位客户”。这两个维度是航空场景区别于普通电商 RFM 的核心,也是答辩时最容易讲出彩的地方。把 RFM 到 LRFMC 的推导逻辑写在报告里,评分通常会高于直接套库。
3.2 用 pandas 完成清洗与 L/R/F/M/C 计算
这一节是整套代码里最关键的一步。很多新手直接拿原始表跑聚类,结果聚类中心解读出来完全没意义,原因就是字段没处理成指标。下面代码是课设里最常用的实现顺序。
import pandas as pd import numpy as np df = pd.read_csv("air_data.csv", encoding="gbk") # 1. 去掉完全重复的行 df = df.drop_duplicates() # 2. 剔除异常记录:票价为 0 或者折扣率为 0 df = df[(df["SUM_YR_1"] > 0) & (df["SUM_YR_2"] > 0)] df = df[(df["avg_discount"] > 0) & (df["avg_discount"] <= 1)] # 3. 日期字段转成 datetime,并构造观测窗口 df["FFP_DATE"] = pd.to_datetime(df["FFP_DATE"]) df["LOAD_TIME"] = pd.to_datetime(df["LOAD_TIME"]) end_date = df["LOAD_TIME"].max() # 4. 构造 LRFMC 五维特征 df["L"] = (end_date - df["FFP_DATE"]).dt.days / 30 df["R"] = df["LAST_TO_END"] df["F"] = df["FLIGHT_COUNT"] df["M"] = df["SEG_KM_SUM"] df["C"] = df["avg_discount"] features = ["L", "R", "F", "M", "C"]逻辑说明:drop_duplicates()去重是为了防止同一会员有多个冗余行;剔除票价为 0 和折扣率为 0 的记录,是因为这些数据在真实业务里通常是赠票、员工票或录入脏数据,留着会严重干扰聚类。end_date取数据里最大的LOAD_TIME,也就是把所有客户放在同一个时间窗口内比较,这是 L 和 R 计算公平性的前提。L 的单位是月,所以用days/30。
参数说明:LAST_TO_END这个字段在经典案例里本身就是“最后一次乘机时间距观测窗口结束的月数”,所以直接用,不需要再减当前时间。如果你的数据里没有这个字段,而是给了LAST_FLIGHT_DATE,结算方式就变成(end_date - df["LAST_FLIGHT_DATE"]).dt.days / 30。这套课设的评分重点之一就是你把原始字段转化为指标的过程是否讲得通,所以清洗和构造逻辑一定要在报告里写清楚。
3.3 为什么要用标准差标准化而不是归一化
特征构造完之后,直接聚类是有问题的。L 的量级可能是一两百个月,R 可能是几个月,F 是几十次,M 是几万公里,C 在 0 到 1 之间。量纲不一致会导致欧氏距离完全被 M 和 F 主导,聚类结果基本等于只按“飞得多不多”分堆。
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X = scaler.fit_transform(df[features]) # 如果需要把标准化后的数据写回表,用下面的方式 df_scaled = pd.DataFrame(X, columns=features, index=df.index)逻辑说明:StandardScaler做的是 Z-score 标准化,每个特征减去均值、除以标准差,变换后均值为 0、方差为 1。它比 MinMax 归一化更适合 K-Means,原因是 K-Means 依赖欧氏距离,而 Z-score 不会把数据压到固定区间导致离群点信息被削弱,且对后续计算协方差、轮廓系数更友好。
参数说明:fit_transform只用一次,把学到的均值和方差存进scaler对象里。后面如果有新客户数据要做预测,必须用同一个scaler.transform(),不能重新 fit,否则训练和预测时的分布不一致,聚类结果没法解释。
4. K-Means 聚类的落地细节:K 值怎么选、中心点怎么解读
4.1 聚类的输入矩阵怎么组织
标准化的五列特征拼成一个矩阵 X,每一行是一个客户,每一列是一个指标,这是 K-Means 直接吃的输入格式。要注意的事情有两件:一是特征顺序固定为 L、R、F、M、C,后续聚类中心解读和画雷达图都依赖这个顺序;二是索引要对齐,如果你中间做过dropna()或筛选,一定要用reset_index(drop=True),否则后面把聚类标签并回原表时会出现错位。
X = df_scaled[features].values print(X.shape)逻辑说明:df_scaled[features].values把 DataFrame 转成 numpy 二维数组,sklearn 的KMeans只接受这种数值矩阵输入,不接受带列名的 DataFrame。X.shape看一眼是 (行数, 5),确认没有把其他字段混进去。
4.2 用 SSE 肘部法和轮廓系数选定 K 值
K-Means 的 K 值没有公式可算,课设里最稳妥的选法是先画 SSE 肘部图,再用轮廓系数辅助验证。这两个指标在答辩时都要能讲出含义。
from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score sse = [] sil = [] k_range = range(2, 9) for k in k_range: model = KMeans(n_clusters=k, n_init=10, random_state=0) labels = model.fit_predict(X) sse.append(model.inertia_) sil.append(silhouette_score(X, labels)) # 查看每个 k 对应的指标 for k, s, si in zip(k_range, sse, sil): print(f"k={k}, SSE={s:.2f}, Silhouette={si:.4f}")逻辑说明:SSE 是样本到其所属簇中心的距离平方和,K 增大时 SSE 必然下降,但下降速度会有一个明显的拐点,拐点附近就是比较合理的 K 值。轮廓系数范围在 -1 到 1 之间,越接近 1 说明簇内紧密、簇间分离越好,0.5 以上都是可接受的结果。经典案例中 5 类客户通常对应 k=5,但我不建议直接写死,先看你的数据拐点。
参数说明:n_init=10表示 K-Means 用 10 个不同的初始质心各跑一遍,取最优结果,这是解决局部最优的标准参数,新手最容易漏。random_state=0固定随机种子,保证每次跑出来的结果一致,否则你换台电脑结果就变了,报告里没法沉淀。
4.3 用固定种子做最终聚类并读出中心点
选定 K 值之后就是正式的最终聚类,这一步和 4.2 的区别是把 k 固定下来,并且把标签写回原始 DataFrame,方便后续做用户画像。
model = KMeans(n_clusters=5, n_init=10, random_state=42) df["cluster"] = model.fit_predict(X) # 查看聚类中心(标准化后的均值) centers = pd.DataFrame(model.cluster_centers_, columns=features) print(centers) # 每个簇的样本数 print(df["cluster"].value_counts().sort_index())逻辑说明:model.cluster_centers_返回的是每个特征在标准化空间里的中心坐标,因为输入是标准化后的 X,所以这里的数值是 Z-score 的含义,正数表示高于全体均值、负数表示低于全体均值。value_counts()看每个簇的人数占比,如果某个簇人数特别少,很可能是离群点问题,回退到第 5 章的截断处理。
random_state=42是工程里常用的固定种子,换成任意整数都行,目的是结果可复现。需要注意的是聚类标签0-4本身没有业务含义,我们是在解读完中心点之后,才给每个簇赋予“重要保持客户”“一般客户”这类名字。
4.4 五种客户画像怎么读:从聚类中心到营销建议
聚类中心读出的是标准化数值,要把它翻译成业务语言才能写进报告。常见做法是把每个簇的 L、R、F、M、C 原值均值列出来,和全体均值做对比,然后给簇命名。
| 簇 | L | R | F | M | C | 客户类型判断 | 建议策略 |
|---|---|---|---|---|---|---|---|
| 0 | 高 | 低 | 高 | 高 | 高 | 重要保持客户 | 优先保障,提供会员权益和专属服务 |
| 1 | 中 | 低 | 中 | 中 | 低 | 重要发展客户 | 推动升舱和联名合作,提升折扣系数 |
| 2 | 高 | 高 | 中 | 中 | 高 | 重要挽留客户 | 定向召回,发放优惠券唤醒 |
| 3 | 低 | 中 | 低 | 低 | 低 | 一般客户 | 维持基本服务,低成本触达 |
| 4 | 低 | 高 | 低 | 低 | 低 | 低价值客户 | 不做额外投入 |
这里有个很容易翻车的细节:R 值越高代表“越久没坐飞机”,所以解读时 R 的方向和其他四个指标相反。很多学生的报告把“R 高”解读成“最近刚飞完”,直接被老师看出来没理解指标含义。写报告时图表和结论都建议围绕这张表,把中心点数值还原成业务动作。
5. 避坑与排错:五个让新手翻车的常见问题
5.1 问题一:每次跑出来的聚类结果都不一样
现象:同一个代码跑两次,value_counts()的分布对不上,报告截图和实际结果不一致。原因:K-Means 初始质心是随机选择的,不同初始点会收敛到不同的局部最优,你重跑一次相当于换了初始点。解决:聚类和 K 值选择阶段统一固定random_state,并在报告中写明参数值,例如KMeans(n_clusters=5, n_init=10, random_state=42)。n_init=10会尝试 10 组初始点并保留最佳那个,基本能消除随机性影响。从那以后我每次跑聚类都会把随机种子写在代码注释里。
5.2 问题二:M 字段里有超长尾巴,聚类结果变成“一拖四”
现象:五个簇里有四个都长得很像,剩下一个是极少数高里程用户,中心点解读完全没有业务区分度。原因:飞行里程 M 的分布极度右偏,少数洲际航线用户把均值拉得很高,标准化后这些点成为离群点,K-Means 会专门为它们开辟一个簇。解决:在标准化之前对异常值做分位数截断。这里有个常见误用,很多人直接删掉“看起来不正常”的行,但那是真实客户,正确做法是压缩而不是删除。代码逻辑是在标准化前用clip()处理极值,之后再重新做标准化和聚类。
for col in features: q99 = df[col].quantile(0.99) df[col] = df[col].clip(upper=q99) X = scaler.fit_transform(df[features])逻辑说明:quantile(0.99)算出第 99 分位数,clip(upper=...)把所有超过该值的样本压到这个分位数上,保留样本数量但削减极值影响。这样既保住高价值客户还能避免聚类被极端值带走。
5.3 问题三:新数据导入后 predict 报错或结果全乱
现象:模型训练完了,拿新一批客户数据进来跑model.predict(),要么报特征数量不一致,要么聚类结果全部归入同一类。原因:训练时用到的是整个原始表的 17 列,predict 时你只给了 5 列,sklearn 按位置取特征,列索引错位之后数据全乱;或者新数据没有经过同一套清洗和标准化。解决:把所有预处理逻辑封装成一个函数,训练和预测时调用同一份代码,避免两边逻辑漂移。
def build_features(raw_df, end_date=None): df = raw_df.copy() if end_date is None: end_date = pd.to_datetime(df["LOAD_TIME"]).max() df["FFP_DATE"] = pd.to_datetime(df["FFP_DATE"]) df["L"] = (end_date - df["FFP_DATE"]).dt.days / 30 df["R"] = df["LAST_TO_END"] df["F"] = df["FLIGHT_COUNT"] df["M"] = df["SEG_KM_SUM"] df["C"] = df["avg_discount"] return df[["L", "R", "F", "M", "C"]]逻辑说明:这个函数接收原始 DataFrame 返回五列特征,训练时先把原始表传进去拿特征,再标准化和 fit;预测时同样把新表传进去,然后用保存好的scaler.transform()转成同分布数据段,才能交给model.predict()。
5.4 问题四:报告中的雷达图坐标轴错乱
现象:客户画像用雷达图展示,五个维度是 L、R、F、M、C,但图里某个维度的数值明显不对,整个图形被压扁。原因:雷达图的每个维度坐标范围不一致,R 和 C 范围小、M 范围大,没有对展示数据做归一化,图就被 M 拉变形,或者横纵坐标写反导致五维变量显示不全。解决:画雷达图时先用 MinMax 把五列缩放到 0-1 区间,并手动固定每个维度坐标起始都在 0。这属于画图技巧上的坑,写在实验报告里也能体现细节。
5.5 问题五:中文标签显示成方块
现象:seaborn 或 matplotlib 画图,标题、图例里的中文全部变方块,报告没法看。原因:matplotlib 默认字体不包含中文字符。解决:代码开头配置字体,常见做法是选系统中的黑体或微软雅黑。
import matplotlib.pyplot as plt plt.rcParams["font.sans-serif"] = ["SimHei"] # Windows 常见 plt.rcParams["axes.unicode_minus"] = False参数说明:font.sans-serif设置无衬线字体,SimHei 是 Windows 自带黑体,macOS 可以改成["Arial Unicode MS"]。axes.unicode_minus要设为 False,否则显示负号时会变方块。这个坑不大,但每年都有人踩,先写上能省十分钟调试时间。
6. 进阶:用客户价值得分验证聚类合理性
聚类做完直接把结论写进报告,逻辑上不够闭环,因为缺少一个验证工作。我们的做法是用一个可解释的客户价值得分公式把五个簇排序,看排序结果与聚类画像是否一致。这算是一个不错的加分项,也是能体现梯度调优能力的地方。
先定义权重:L 波动小不作为强权重,重点放在 R、F、M、C 四个维度。参考常见方案,可定义value_score = 0.2*L + 0.15*R + 0.3*F + 0.25*M + 0.1*C,再用 MinMax 归一化到 0-100 分。
from sklearn.preprocessing import MinMaxScaler # 每个簇内求指标均值,做一次 0-1 归一化 cluster_profile = df.groupby("cluster")[features].mean() mm = MinMaxScaler() profile_norm = pd.DataFrame( mm.fit_transform(cluster_profile), columns=features, index=cluster_profile.index ) # 计算每个簇的价值得分 weights = {"L": 0.2, "R": 0.15, "F": 0.3, "M": 0.25, "C": 0.1} profile_norm["score"] = sum( profile_norm[col] * w for col, w in weights.items() ) print(profile_norm.sort_values("score", ascending=False))逻辑说明:groupby("cluster")[features].mean()得到聚类中心,MinMaxScaler把各维度缩到 0-1 后再做加权求和,这样得分就聚成 0-1 的分数。如果排序结果里得分最高的簇确实对应 L 高、R 低、F 高、M 高、C 高的那类客户,说明聚类分群与业务逻辑互相验证了。课堂上讲这一步时,老师通常会认为你已经把聚类结果当作业务决策依据来用,而不是停留在“聚类完了画个图”的阶段。
这个小验证让答案更有说服力,同时也帮你把整套流程从“算法跑通”推进到“业务能用”。稍微调整权重,就可以对应不同公司对不同类型客户的侧重,这也是课设答辩时最容易被提问的地方。权重取多少不是玄学,而是要能解释清楚:你所在公司更看重频繁出行还是高舱位消费。
从那以后,我做这类客户分群项目,都会在聚类代码后面挂一个得分排序表,再迭代一轮。拿到一份数据先跑一遍全流程,再回头比对各簇得分与业务假设是否矛盾,一旦矛盾就去检查预处理阶段是不是有字段理解偏差。这个流程走熟了,你的课设或者毕设基本就不会被问倒。做聚类课设最怕的就是“代码跑通了但解释不了为什么分出来这些类”,把得分验证写进报告,等于给自己留了张底牌。希望这篇笔记能帮到你,至少让你少踩几个我已经替你踩过的坑。
本文还有配套的精品资源,点击获取