简介:面向航空公司客户价值分析场景的Python源码压缩包,适合数据挖掘、机器学习初学者和数据分析从业者。项目围绕客户分群与价值预测展开,涵盖数据清洗、特征工程、可视化及建模评估等环节,可帮助读者掌握用Python处理真实业务数据的完整思路。压缩包共10个文件,以xls和csv数据文件为主,另有py源码脚本和txt模块说明,数据与代码分离,便于对照学习;整体大小17.13MB,属于轻量实用的小型数据资源。目前已有891人学习下载。通过源码可学习K-means聚类、客户生命周期价值估算等关键方法,结合自带数据文件能完整复现分析流程。对于希望提升数据分析和机器学习实战能力的学习者,这是一份可直接运行、快速入手的参考资料。
1. 航空公司客户价值分析:这份 Python 源码拆开后在算什么
做用户分群的人都知道,零售行业惯用的 RFM 模型搬到航空场景经常会失灵。机票价格受航线长短、淡旺季影响太大,一个常年飞国际航线的乘客,消费金额可能比飞国内短途的高价值乘客还高,直接拿消费金额衡量客户价值,结论会偏得离谱。这份《航空公司客户价值分析》Python 源码要解决的正是这个问题:把客户价值从消费金额里剥出来,改用 LRFMC 五个维度——入会时长、最近乘机间隔、飞行次数、飞行总里程、平均折扣系数——去刻画每一个会员。
包里的东西很完整:air_data.csv是原始会员数据,data_cleaned.csv是清洗后的中间结果,zscoredata.xls是标准化之后的建模输入,code.py把读取、清洗、标准化、K-Means 聚类、结果解读整条链路都串完了,tmp目录里还能看到explore.xls这类探查中间件,甚至字段说明表也在里面。适合两类人:一类是在学数据挖掘、想把聚类实战跑通的学生;另一类是手里攒着用户数据、想套一套客户分层逻辑的分析岗。下面我严格按这份源码的执行顺序,把每一步为什么这么做、参数怎么调、坑在哪,一层层拆开讲。
2. 数据清洗与特征工程:从 air_data.csv 到 LRFMC 五维表
2.1 读取原始数据:先摸清字段,再谈分析
这份源码的第一步不是直接建模,而是先读数据、做探查。air_data.csv是从航空公司业务系统导出的会员信息表,列名是中文,字符串字段也带中文,读取时最容易翻车的就是编码问题。
import pandas as pd import numpy as np # 读取原始数据,航空公司的导出文件常见 GBK 编码 air_data = pd.read_csv('air_data.csv', encoding='gbk') print(air_data.shape) # (62988, 44) print(air_data.columns) # 44 个字段名这段代码里值得注意的就两个点:encoding指定了gbk,因为这份源数据用默认的utf-8去读会直接报UnicodeDecodeError;sep没写,因为 csv 默认就是逗号分隔,不需要额外指定。读完先别急着做任何处理,看一眼shape确认样本量和字段数,再看columns把 44 个字段过一遍。这一步的价值在于:你还没动手清洗,但已经知道有哪些字段可能用得上——票价、飞行次数、里程、折扣、入会时间、最近飞行时间,基本都在里面。
接着做数值字段的分布探查,这是决定后面清洗边界的关键依据。
# 探查数值字段的分布,确定清洗边界 summary = air_data[['SUM_YR_1', 'SUM_YR_2', 'FLIGHT_COUNT', 'SEG_KM_SUM', 'avg_discount']].describe() print(summary) # 缺失值分布 print(air_data.isnull().sum())describe()输出的四分位数和极值,直接告诉你哪些字段存在离谱的取值。比如SUM_YR_1、SUM_YR_2是两年内的票价合计,如果大量记录是 0,就要怀疑是数据没录全还是真的有 0 元票;avg_discount是平均折扣系数,正常范围应该在 0 到 1 之间,跑出负数肯定是脏数据。我一般还会顺手跑一个isnull().sum(),把缺失字段和缺失量记录下来——这份源码里票价字段就是有空值的,下一节的清洗规则就是围绕它定的。
2.2 清洗逻辑:票价为零、票价为空的记录怎么处理
看原始数据的缺失值分布会发现,SUM_YR_1和SUM_YR_2(两个观测年度的票价合计)存在空值。与此同时还有一类很隐蔽的脏数据:票价字段是 0,但飞行里程和折扣系数又不是 0——这明显是录入异常,一个人不可能飞了几万公里却一分钱没花。源码里对这两类问题各有一套处理规则:
# 第一步:票价为空的记录直接删除 airline = air_data[air_data['SUM_YR_1'].notnull() & air_data['SUM_YR_2'].notnull()] print('删除空票价后:', airline.shape) # 第二步:票价为 0 但飞行里程或折扣不为 0 的记录删除 index1 = airline['SUM_YR_1'] != 0 index2 = airline['SUM_YR_2'] != 0 index3 = (airline['SEG_KM_SUM'] == 0) & (airline['avg_discount'] == 0) airline = airline[index1 | index2 | index3] print('删除矛盾记录后:', airline.shape) # 清洗结果落盘 airline.to_csv('data_cleaned.csv', index=False, encoding='gbk')这段逻辑看起来简单,但index3是很多人会忽略的细节。index1 | index2 | index3的意思是:只要两年票价里有任意一年不为 0,就保留;或者票价全为 0,但里程和折扣也是 0,也保留。第二种情况对应的是极少数特殊票——0 元里程票、积分兑换票,它们虽然票价为 0,但里程和折扣都真实记录了客户行为,这类数据对价值分析仍然有意义。真正要删的,是那些票价与飞行行为互相矛盾的坏记录。
删完以后样本量会少几十到上百条,具体数量取决于原始数据的脏度。这一步结束后把结果存成data_cleaned.csv,就是你在包里看到的那份中间产物。我自己的习惯是再另存一份data_cleaned1.csv当备份,防止后面特征构造时改坏了数据还要从头再来。
2.3 特征构造:为什么航空场景不用 RFM 而用 LRFMC
数据洗干净之后,源码没有直接拿原始字段建模,而是构造了五个新特征。这一点是整份代码的题眼:零售行业经典的 RFM 模型(最近消费时间、消费频率、消费金额)到航空场景要做一次替换——把「消费金额」换成「飞行里程 + 折扣系数」,再加一个「入会时长」。原因很直接:机票价格随航线浮动,同样飞 5000 公里,商务舱和经济舱的消费金额能差出好几倍,金额不能公平反映客户对航空公司的价值;而飞行里程代表客户对航线网络的依赖程度,折扣系数代表客户对价格的敏感度,这两个维度结合起来,才真正刻画了客户的消费能力和忠诚度。
import datetime # L:入会时长,单位换算成月 ffp_date = pd.to_datetime(airline['FFP_DATE']) load_date = pd.to_datetime(airline['LOAD_TIME']) L = (load_date - ffp_date).dt.days / 30 # R:最近一次乘机距离观测窗口结束的时间间隔,单位天 R = airline['LAST_TO_END'] # F:飞行次数 F = airline['FLIGHT_COUNT'] # M:总飞行里程 M = airline['SEG_KM_SUM'] # C:平均折扣系数 C = airline['avg_discount'] # 拼成五维表 LRFMC = pd.concat([L, R, F, M, C], axis=1) LRFMC.columns = ['L', 'R', 'F', 'M', 'C'] print(LRFMC.head())构造L时注意FFP_DATE是入会日期、LOAD_TIME是观测窗口截止日期,两者相减再除以 30 得到以月为单位的入会时长——为什么不直接除以 365?因为后面聚类时这个量纲要和 R(天)、F(次)、M(公里)放在一起计算距离,用月能让数值落在和 F 差不多的量级,降低极端值对距离计算的干扰。
R直接用LAST_TO_END字段,它是数据里已经算好的天数,不需要自己再减一遍日期。C用avg_discount而不是消费金额,正是前文说的替换逻辑——折扣系数低说明客户习惯买折扣票,价格敏感;折扣系数高说明客户愿意为灵活性买单,这类客户的价值弹性更大。到这里,建模输入LRFMC五维表就构造完成了,下一步进入标准化和聚类。
3. K-Means 聚类实战:标准化、聚类与客户群画像
3.1 数据标准化:zscoredata.xls 是怎么来的
拿到 LRFMC 五维表之后,源码做的第一件事不是直接扔进 K-Means,而是先标准化。K-Means 本质上是基于欧氏距离的聚类算法,它计算的是样本点之间的空间距离,而 LRFMC 五个维度量纲完全不同:L 是几十个月,R 是几百天,F 是个位数,M 是几万公里,C 是零点几的折扣系数。如果不做处理,M(总里程)的数值范围会直接把其他四个维度的差异淹没掉,聚类结果几乎等于只按里程一个维度切分,这显然不是我们想要的。
from sklearn.preprocessing import StandardScaler # 标准化:让每个维度的均值为 0,标准差为 1 scaler = StandardScaler() zscored = scaler.fit_transform(LRFMC) # 转回 DataFrame 便于查看和保存 zscored = pd.DataFrame(zscored, columns=LRFMC.columns) # 落盘,这就是包里的 zscoredata.xls zscored.to_excel('zscoredata.xls', index=False) print(zscored.describe())StandardScaler做的事情是(x - mean) / std,标准化之后每个维度的均值变成 0、标准差变成 1。这一步做完,五个维度才真正处于同一量级,聚类时每个特征对距离的贡献是平等的。
这里有个常见的误解:有的人用MinMaxScaler做归一化,把数据压到 0-1 区间。两种方法对 K-Means 都可行,但这套源码选的是标准化,原因是 LRFMC 五个维度的分布偏态差异较大,标准化对异常值的容忍度比归一化高一些,聚类边界更稳定。你在包里看到的zscoredata.xls,就是这一步的输出——它也是后面 K-Means 的实际输入。
3.2 K-Means 聚类代码与参数设置
标准化完成后进入核心环节:K-Means 聚类。源码把 K 值定为 5,对应的业务逻辑是航空公司通常把客户分成五类:重要保持、重要发展、重要挽留、一般、低价值。这个 K 值不是拍脑袋定的,原案例里是结合业务经验先定 5,再用轮廓系数验证,这一步我在第 6 章会展开。
from sklearn.cluster import KMeans # K 值按业务经验定为 5 k = 5 model = KMeans(n_clusters=k, n_init=10, max_iter=500, random_state=42) model.fit(zscored) # 每个样本的聚类标签 cluster_labels = model.labels_ # 把标签合并回五维表,方便后面分析 LRFMC['cluster'] = cluster_labelsn_clusters=5是聚类数;n_init=10表示 K-Means 会随机初始化 10 次,从中选最优解,这个参数在 sklearn 新版本里尤其重要,它直接决定聚类结果稳不稳定;max_iter=500是单次迭代的上限,防止算法不收敛时无限跑下去;random_state=42固定随机种子,保证每次运行得到相同的聚类结果——这个参数对复现实验至关重要,不固定的话,同一份数据每次跑出来的客户群标签都会不一样,后面的业务解读就没法复现。
跑完之后model.labels_就是每一条会员记录所属的客户群编号(0 到 4),把它合并回LRFMC表,后面就能按客户群分组看特征差异了。
3.3 聚类结果解读:五类客户分别是谁
聚类出来的编号本身没有业务含义,必须结合每个客户群在 LRFMC 五个维度上的均值来解读。源码的做法是按聚类标签分组,算各组均值,再排序观察。
# 按聚类标签分组,计算五维均值 cluster_mean = LRFMC.groupby('cluster')[['L', 'R', 'F', 'M', 'C']].mean() print(cluster_mean.round(2))看这张均值表的时候,重点看每个客户群在哪个维度显著偏高、在哪个维度显著偏低。以常见的聚类结果为例:
- 客户群 0:L 高、R 低、F 高、M 高、C 高——入会时间长、最近坐过、飞得多、里程高、折扣低(愿意全价买),这是价值最高的群体,对应「重要保持客户」。
- 客户群 1:L 低、R 低、F 高、M 高、C 低——入会时间短但飞得很猛,买的大多是折扣票,对应「重要发展客户」,有潜力但还没完全释放价值。
- 客户群 2:L 高、R 高、F 中、M 中、C 中——入会时间长,但最近很久没坐了,历史飞行中等,对应「重要挽留客户」,在流失边缘。
- 客户群 3:L 低、R 中、F 低、M 低、C 低——各项都低,对应「一般客户」,价值贡献有限。
- 客户群 4:L 中、R 高、F 低、M 低、C 低——入会时间不短但已经不怎么飞了,对应「低价值客户」。
注意输出里群的编号顺序每次跑可能不同,因为 K-Means 初始质心随机,但把均值表打出来之后,五类客户的相对高低关系是稳定的。这一步做完,客户分群已经从「数据」变成了「业务上有解释力的人群标签」。
4. 客户价值评估:从聚类标签到可落地的运营策略
4.1 五类客户的 LRFMC 特征差异
聚类标签合并回原始表之后,下一步是让每个客户群的画像变得可读。直接用一张相对高低的对比表,比看一堆均值数字直观得多。以一次典型运行为例,五类客户在 LRFMC 上的特征可以归纳成下面这张表:
| 客户群 | L 入会时长 | R 最近乘机间隔 | F 飞行次数 | M 总里程 | C 折扣系数 | 业务判定 |
|---|---|---|---|---|---|---|
| 群 0 | 高 | 低 | 高 | 高 | 高 | 重要保持客户 |
| 群 1 | 低 | 低 | 高 | 高 | 低 | 重要发展客户 |
| 群 2 | 高 | 高 | 中 | 中 | 中 | 重要挽留客户 |
| 群 3 | 低 | 中 | 低 | 低 | 低 | 一般客户 |
| 群 4 | 中 | 高 | 低 | 低 | 低 | 低价值客户 |
这张表的读法不是看单个数字,而是看组合。比如群 0 和群 1 的 F、M 都很高,区别在 C:群 0 折扣系数高,说明愿意买全价票,实际贡献大;群 1 折扣系数低,说明飞得多但买的都是折扣票,价值还没完全变现。再比如群 2,L 高说明是个老会员,R 高说明最近不飞了,F、M 中等说明历史上有过贡献——这类客户最可惜,是典型的流失预警对象。
4.2 价值排序与策略映射
聚类均值表跑出来以后,客户群的优先级要从业务贡献度来排:重要保持客户 > 重要发展客户 > 重要挽留客户 > 一般客户 > 低价值客户。这个排序决定了运营资源的投放方向。
- 重要保持客户:占比小但贡献大,应该提供一对一服务、优先升舱、生日关怀、专属客服,目标是维持粘性,防止被竞品挖走。
- 重要发展客户:坐得多但买折扣票,策略是推动消费升级——用里程累积加倍、舱位升级券这类手段,引导他们尝试高折扣系数产品。
- 重要挽留客户:曾经有价值但现在在流失,策略是主动唤醒——定向推送优惠券、会员积分过期提醒、专属回馈活动,把 R 值拉低。
- 一般客户和低价值客户:保持标准化服务即可,低成本触达,不建议投入高成本运营资源。
这一步是整份源码最接近业务价值的环节。聚类本身只是把客户分了组,但分组之后怎么定义优先级、怎么分配营销预算,才是「客户价值分析」四个字的落点。源码里code.py到这一步会输出客户群的占比和特征均值,你拿到这个结果就可以直接映射到上面的策略框架里。
4.3 可视化输出:雷达图看客户群差异
要让分析结果能直接拿去汇报,光有数字表格不够,源码里用雷达图把五类客户的 LRFMC 特征画出来,是效果最好的一种呈现方式。因为五个维度在标准化之后量纲一致,可以直接画在同一张雷达图上对比。
import matplotlib.pyplot as plt import numpy as np # 中文字体设置,防止标签变方块 plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False # 雷达图需要把首尾闭合 labels = ['L', 'R', 'F', 'M', 'C'] angles = np.linspace(0, 2 * np.pi, len(labels), endpoint=False).tolist() angles += angles[:1] for clu in cluster_mean.index: values = cluster_mean.loc[clu, labels].values values = np.append(values, values[0]) # 首尾闭合 plt.polar(angles, values, label=f'客户群{clu}') plt.legend(loc='upper right') plt.show()这段代码的关键是雷达图必须把序列首尾相接,否则画出来的图是断开的。np.append(values, values[0])这行干的就是这件事。plt.rcParams两行是中文显示的关键,不设置的话图上的客户群名称会变成一个个方块。画完之后,五条多边形叠加在一张图里,哪个客户群五个维度是饱满的、哪个是扁平的,一眼就能看出来,汇报时比贴一张均值表有说服力得多。
5. 实战避坑:这份源码里最容易翻车的五个细节
5.1 读 CSV 时中文列名乱码或直接报错
现象:pd.read_csv('air_data.csv')跑完报UnicodeDecodeError,或者能读出来但列名是一堆乱码。
原因:这份数据是中文业务系统导出的,编码是 GBK 而不是默认的 UTF-8。Python 的read_csv默认用 UTF-8 解码,遇到 GBK 编码的中文字段就崩了。
解决:读取时显式指定encoding='gbk';如果还是报错,改成encoding='gb18030',这是 GBK 的超集,兼容性更好。写回文件时也别忘了一致,to_csv('data_cleaned.csv', encoding='gbk'),否则下次再读又得改编码。包里那份字段说明表也是同样的编码问题,用 Excel 打开乱码的话,先把它另存成 UTF-8 再打开。
5.2 K-Means 聚类结果每次跑都不一样
现象:同一份zscoredata.xls,今天跑聚类,明天再跑一遍,客户群的编号和人数对不上。
原因:K-Means 的初始质心是随机生成的,不同初始质心会收敛到不同局部最优解,这是算法本身的特性,不是 bug。
解决:固定随机种子,random_state=42,让每次运行的随机过程可复现。同时把n_init从默认值调大到 10,让算法从 10 个不同初始点里选最优的,降低落到差解的几率。血泪经验是:跑任何聚类实验,random_state一定要写,不然你回头想复盘的时候根本没法确定结果是不是同一套数据跑出来的。
5.3 把票价为零的记录一刀切删掉
现象:清洗完发现样本量少了一大截,而且后面聚类结果里多了一个全是 0 元票客户的异常群。
原因:把「SUM_YR_1 == 0」当成脏数据直接删,误伤了正常的 0 元里程票、积分兑换票。这类客户虽然没付现金,但飞行行为是真实的,对价值分析有意义。
解决:按源码里的三重条件过滤,而不是单条件删除。保留「任意一年票价不为 0」的记录,同时保留「票价全为 0 但里程和折扣也为 0」的记录;只删除「票价全为 0 但里程或折扣不为 0」的矛盾记录。判断脏数据要看你删的这条记录是否自相矛盾,而不是看它是不是 0。
5.4 K 值拍脑袋定 5,没验证
现象:聚类做完了,但客户群之间的特征差异不明显,两张群的均值表看起来差不多,业务方问一句「为什么是 5 类」就答不上来。
原因:K 值是 K-Means 里最敏感的参数,K 太小会把不同价值的客户混在一起,K 太大又切得太碎。直接拍一个数字,缺少量化依据。
解决:用轮廓系数和 SSE 肘部曲线做验证,具体代码在第 6 章。跑一遍 K 从 2 到 8 的轮廓系数,挑系数最高的那个 K;如果轮廓系数差异不大,再结合业务经验取舍。从那以后我做分群,K 值的确定必须过一遍这个验证,不然宁可先不定。
5.5 Matplotlib 中文标签显示成方块
现象:雷达图、柱状图上的客户群名称全是空心方块,中文全乱。
原因:Matplotlib 默认字体是 DejaVu Sans,不支持中文字符。
解决:绘图前加两行设置,指定中文字体,关闭 Unicode 负号显示。Windows 上一般用SimHei,Linux 服务器上如果没装中文字体,需要先装fonts-noto-cjk再用Noto Sans CJK SC。这里还有一个隐性的坑:负号。设置完中文字体后如果还看到负号变方块,就是axes.unicode_minus没设为False。标准化的数据里有负数,雷达图又必须画这些值,这两行配置缺一不可。
6. 进阶用法:用轮廓系数验证 K 值,让分群不再靠拍脑袋
6.1 轮廓系数与 SSE 曲线
第 5 章提到 K 值不能拍脑袋,这里给出两份源码之外的验证代码。轮廓系数衡量的是每个样本与其所在簇的紧密程度、以及与其他簇的分离程度,取值在 -1 到 1 之间,越接近 1 说明聚类越合理。SSE(簇内误差平方和)则看的是随着 K 增大,误差下降是否放缓,寻找「肘部」位置。
from sklearn.metrics import silhouette_score from sklearn.cluster import KMeans K_range = range(2, 8) scores = [] sse = [] for k in K_range: model = KMeans(n_clusters=k, n_init=10, random_state=42) labels = model.fit_predict(zscored) # 轮廓系数 s = silhouette_score(zscored, labels) scores.append(s) print(f'K={k}, 轮廓系数={s:.4f}') # SSE(肘部法则) sse.append(model.inertia_)跑完之后看两个输出:轮廓系数序列里值最大的 K 优先选;如果轮廓系数差异不大,再配合 SSE 曲线找拐点。这两个指标结合起来,能给你一个「K 取 5 是有依据的」量化结论,而不是业务方一问就哑口无言。
6.2 从指标回到业务:验证聚类结果是否可行
指标只是参考,最终还要回到业务校验。我会把聚类结果的均值表拿给业务同事看,问三个问题:每个客户群能不能用一句话说清特征?客户群之间的差异是否符合业务直觉?有没有哪个客户群的特征组合在业务上解释不通?如果三个问题都能答上,这个 K 值才真正立得住。轮廓系数最高的 K 不一定是最优的,因为指标只衡量统计意义上的聚类质量,不衡量业务意义——这个取舍,是做用户分群的人最容易忽略的。
从那以后我每次做客户分群,不管数据多干净,不管业务方多急着要结论,都强制自己先跑一遍轮廓系数和 SSE 曲线,再拿聚类结果回去找业务核对一遍。这套动作加起来花不了五分钟,但能避免把 K 值定错、把客户群解读跑偏,省下的返工时间远不止五分钟。希望帮到你。
本文还有配套的精品资源,点击获取