简介:这份航空公司客户价值分析Python源码包,面向数据分析、机器学习入门者及航空业务运营人员,目标是帮助读者从原始订票记录中挖掘客户价值并制定差异化运营策略。项目完整演示了数据清洗、缺失值填充、标准化、特征构造、K-means客户分群、聚类结果可视化以及基于RFM改进模型的客户生命周期价值计算流程,还包含对模型结果做简单评估与业务解读的思路,适合课程设计、毕业设计或企业实战参考。压缩包采用RAR格式,共10个文件,其中5个xls表格用于存放原始或预处理数据,3个csv数据集供程序读取,另有1个Python主脚本和1个txt说明文档,包体大小17.13MB,代码、数据与文档配套完整。目前已有891人学习下载,资源热度较高。解压后可以直接运行主脚本复现分析过程,借助说明文档快速定位模块作用,并根据不同业务场景调整聚类K值与特征权重,是一份能上手实操的客户价值分析参考模板。
1. 航空公司客户价值分析:为什么航司不能用零售业那套RFM
先抛一个反直觉的结论:在航空公司的会员数据里,累计消费最高的那批人,往往不是最值得挽留的那批人。很多常旅客的消费是靠高舱位、协议票和积分兑换堆出来的,他们一个月飞不了一次,折扣系数却高得惊人,一旦公司差旅政策调整或航线收缩,价值立刻归零。这份“航空公司客户价值分析Python源码”要解决的,正是这类问题:把会员的入会时长、最近乘机时间、飞行频率、累计里程、平均折扣系数组合成 LRFMC 五维指标,替代零售业常用的 RFM,再用 KMeans 聚类把几万名会员分成高价值、潜力、一般、低价值几个群体,指导里程赠送、升舱和防流失策略。适合正在做数据分析、BI 报表,或者想用 Python 练手聚类建模的从业者。它的核心不是跑一个花哨模型,而是先整理出一张能用的宽表,再让聚类结果能解释、能落到营销动作上。
2. 数据清洗与LRFMC五维指标:先用Pandas把会员数据变成一张能聚类的宽表
2.1 一张典型的航空会员宽表长什么样:字段取舍是建模的地基
拿到一份会员数据源码时,第一步永远不是写聚类代码,而是先搞清楚表里有哪些字段、哪些是行为字段、哪些是结果字段。我一般会先把数据字典打出来,逐列确认字段含义。常见做法是直接用会员卡号做主键,围绕它展开入会时间、观测窗口结束时间、最近乘机时间、飞行次数、总飞行公里数和平均折扣系数。
这张表里常见的字段和用途大致如下:
| 字段名 | 含义 | 建模时的用途 |
|---|---|---|
| MEMBER_NO | 会员卡号 | 主键,做标签回填用 |
| FFP_DATE | 入会时间 | 计算 L(入会时长) |
| LOAD_TIME | 观测窗口结束时间 | 所有时间指标的计算终点 |
| LAST_TO_END_DATE | 最近乘机到窗口结束的时间 | 计算 R(最近乘机间隔) |
| FLIGHT_COUNT | 观测窗口内飞行次数 | 直接作为 F |
| SEG_KM_SUM | 观测窗口内总飞行公里数 | 直接作为 M |
| AVG_DISCOUNT | 平均折扣系数 | 直接作为 C |
| SUM_YR_1 / SUM_YR_2 | 两年票价收入 | 只用于清洗,不直接进特征 |
这里最容易踩的坑是把 SUM_YR_1、SUM_YR_2 直接当作价值特征。这两个字段是票面收入,受舱位、航线距离、购票渠道影响很大,同一名旅客飞一趟京沪线可能比飞十趟省内支线收入还高,混进聚类会严重干扰分群。更稳妥的做法是只把票价收入字段用于异常数据过滤,特征矩阵里只保留 L、R、F、M、C 五个行为维度。
2.2 计算L、R、F、M、C五个指标:核心的Pandas代码与参数说明
五个指标的物理意义要先记牢:L 是入会到观测窗口结束的月数,R 是最近一次乘机到观测窗口结束的月数,F 是观测窗口内乘机次数,M 是观测窗口内总飞行公里数,C 是平均折扣系数。其中 R 越小说明最近刚飞过,L 越大说明入会时间久,C 越大说明旅客越少买折扣票,单位里程的实际贡献越高。用 Pandas 计算的核心代码是这样:
import pandas as pd # 读取会员宽表,注意csv编码按实际文件调整 df = pd.read_csv('air_data.csv', encoding='utf-8-sig') # 时间字段统一成datetime类型 df['FFP_DATE'] = pd.to_datetime(df['FFP_DATE']) df['LOAD_TIME'] = pd.to_datetime(df['LOAD_TIME']) df['LAST_TO_END_DATE'] = pd.to_datetime(df['LAST_TO_END_DATE']) # L:入会时间距离观测窗口结束的月份数,天数除以30近似月 df['L'] = (df['LOAD_TIME'] - df['FFP_DATE']).dt.days / 30 # R:最近一次乘机距离观测窗口结束的月份数,同样以30天为一月 df['R'] = (df['LOAD_TIME'] - df['LAST_TO_END_DATE']).dt.days / 30 # F、M、C 直接从原始字段取值 df['F'] = df['FLIGHT_COUNT'] df['M'] = df['SEG_KM_SUM'] df['C'] = df['AVG_DISCOUNT'] features = ['L', 'R', 'F', 'M', 'C'] print(df[features].describe())这段代码里把月数统一用 30 天折算,而不是用 calendar.month 做自然月差,原因很简单:让五个维度的数值量级尽量接近,F 只有几十、M 上万,如果 L 和 R 再出现几百天的大数,欧氏距离基本会被里程和时长吃掉。打印 describe 是建模前必做的一步,重点看均值、最小值和最大值,确认没有字段出现负数或异常量级。
2.3 脏数据一定要先清理:三个过滤条件背后的业务原因
公开的航空会员数据里经常存在票价为 0 但折扣系数不为 0 的记录,这类数据常见于免票、积分兑换和系统异常,乘机行为真实存在但收入字段没有意义,直接放进聚类会拉低整个簇的收入特征。还有一类是票价收入为 0 且飞行公里数也为 0,连飞行记录都没有,对分析毫无贡献。我的清洗逻辑一般长这样:
# 过滤规则一:票价为0但折扣不为0,属于免票或异常订单,删除 df = df[~((df['SUM_YR_1'] == 0) & (df['AVG_DISCOUNT'] != 0))] # 过滤规则二:票价为0且飞行公里数也为0,无实际飞行记录,删除 df = df[~((df['SUM_YR_1'] == 0) & (df['SEG_KM_SUM'] == 0))] # 过滤规则三:关键特征为空或出现异常值,先看比例再决定删除还是填充 df = df.dropna(subset=features) df = df[df['F'] > 0]这三条规则背后的业务逻辑要清楚:第一条删的是价格体系异常的数据,第二条删的是没有飞行行为的僵尸记录,第三条是保证进入聚类的样本都真正飞过。不要拿到数据就无脑删空值,先看缺失比例,如果 C 字段缺失超过三成,说明数据采集本身有问题,这时候应该回头找业务确认口径,而不是用均值填一个假折扣系数进去。清洗完成后记得重置索引,避免后面合并标签时行号错位。
3. KMeans聚类建模:从标准化到五类客户分群,完整跑通一个流程
3.1 为什么聚类前必须先做Z-Score标准化:量纲问题的具体教训
不标准化的聚类结果是可以预见的:M 字段动辄几万公里,C 字段只有 0 到 1,F 字段可能只有两位数,欧氏距离基本被 M 一个维度主宰,其他四个维度全变成噪声。这个教训我在早期做过一次,聚类出来五个簇,簇中心只有里程数值在变化,其他维度几乎相同,业务方看了一眼就否掉了。所以标准化不是可选项,是必选项。
from sklearn.preprocessing import StandardScaler X = df[features].copy() scaler = StandardScaler() X_std = scaler.fit_transform(X)这里有个很容易被忽略的细节:fit_transform 是对训练数据计算均值和标准差,并把计算结果存进 scaler 对象。之后只要再来新数据,一律调用 scaler.transform,不能用新数据重新 fit。否则新数据的均值和标准差异变了,同一个客户在不同批次里会被分到不同簇,这在后面做线上分群时是致命问题。StandardScaler 的公式就是 (x - mean) / std,经过变换后每个维度均值接近 0,标准差接近 1,五个特征在距离计算时地位才平等。
3.2 K值怎么定:肘部法则和轮廓系数的组合判断
KMeans 最大的参数就是 K,也就是到底分成几类。定 K 的标准做法是先看肘部曲线,再结合业务解释力决定,不要只盯轮廓系数这个数字。我的判断流程是先跑一个循环,把 K 从 2 试到 8,记录每个 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: km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42) km.fit(X_std) sse.append(km.inertia_) sil.append(silhouette_score(X_std, km.labels_)) for k, s, si in zip(k_range, sse, sil): print(f'K={k}, SSE={s:.2f}, Silhouette={si:.4f}')看这个输出时要把握两个点:SSE 曲线会在某个 K 值后下降明显变缓,这个拐点就是肘部;轮廓系数在 0.3 到 0.5 之间算可接受,超过 0.5 要怀疑特征是否太简单,低于 0.2 说明数据基本分不开。航空客户价值分析这个场景,K 落在 4 到 6 都比较常见,因为五维特征能支撑的业务角色也就四五类,分太多会出现两个簇的业务画像几乎一样的情况,给营销部门解释起来很吃力。
3.3 用sklearn跑通KMeans:最小可复现代码与关键参数
定好 K 值之后,正式建模代码非常短。我用 n_clusters=5 作为示例,实际跑的时候以你的肘部图为准。关键是把随机种子固定下来,让结果可复现。
model = KMeans(n_clusters=5, init='k-means++', n_init=10, max_iter=300, random_state=42) model.fit(X_std) # 把聚类标签贴回原数据 df['cluster'] = model.labels_ # 打印每个簇的样本量和簇中心 print(df['cluster'].value_counts().sort_index()) center_df = pd.DataFrame(model.cluster_centers_, columns=features) print(center_df)参数解释:init='k-means++' 表示用改进后的质心初始化方法,避免随机初始质心聚到局部最优;n_init=10 表示用 10 组不同的初始质心分别迭代,最终返回组内误差最小的那组结果;max_iter=300 是单轮迭代上限,一般数据量几万条时到不了这个上限就收敛;random_state=42 让整个过程可复现。跑完先看 value_counts,如果某个簇只有几十个人,另一个簇有几万人,先别急着调参,去回看是否有异常值没清洗干净,这个现象在数据没清理时经常出现。
3.4 聚类结果不能只给一张图:把分群写回宽表再落库
建模跑完之后,业务部门真正要的不是聚类图,而是一张带分群标签的会员表。所以我会把 cluster 字段合并回原表,连同会员卡号和五个特征一起导出,供后续 BI 报表和营销系统使用。
result = df[['MEMBER_NO', 'L', 'R', 'F', 'M', 'C', 'cluster']].copy() result.to_csv('member_cluster_result.csv', index=False, encoding='utf-8-sig')导出的文件一般会按 cluster 加一个中文业务命名列,比如“高价值保持客户”“高价值流失预警”“潜力客户”“一般客户”“低价值客户”,但这步不是建模里做的,而是在业务会议上和运营同事讨论确认的。落库之后建议顺手算一下各簇五个维度的均值,和全局均值做对比,哪个维度显著高于或低于整体,这个簇的画像就出来了。这一步做得越早,后面调优时越有方向,因为聚类本身不会告诉你哪个簇是高价值,只有和业务常识对上号才有意义。
4. 调优与可视化:聚类效果不好时,从权重、种子、降维三个维度查
4.1 特征权重调整:为什么C和L在这套案例里值得往上提
LRFMC 五个维度在业务意义上的重要性并不完全相等。C(平均折扣系数)直接反映单位里程的实际收益,L(入会时长)反映客户关系的沉淀深度,这两者在航司价值判断里理应有更高权重。一种常见做法是在标准化之后给特征乘一个权重向量,再喂给 KMeans,本质上是在调整距离公式里各维度的贡献比例。
import numpy as np # 权重顺序必须与features列表一致:L, R, F, M, C weights = np.array([1.2, 1.0, 1.5, 1.2, 2.0]) X_weighted = X_std * weights这里的权重不是拍脑袋定的,要有业务解释:C 权重给到 2.0,是因为折扣系数直接和利润相关,同样飞一万公里的旅客,全价票和两折票的价值差好几倍;F 给到 1.5,是因为高频飞行意味着旅客把航司当成出行习惯,粘性是未来收入的基础。权重一改,聚类结果和簇中心全部会变,所以每次调权重都要记录一版结果,方便复盘。建议不要一次调五个维度,逐次改一两个维度并观察分群是否更符合业务直觉,这种调参方式虽然慢,但至少每步都能解释。
4.2 init、n_init和random_state:聚类随机性的来源和后果
KMeans 的初始质心是随机的,同一个数据集不固定随机种子,两次跑出来的分群结果可能不同。业务部门对这种现象容忍度极低,你要是在周会上说“结果每次跑都略微不一样”,对方很容易对整个分析失去信任。所以从一开始就把随机种子固定下来,并理解这三个参数的配合关系。
km_a = KMeans(n_clusters=5, init='random', n_init=1, random_state=1).fit(X_std) km_b = KMeans(n_clusters=5, init='random', n_init=1, random_state=2).fit(X_std) # 对比两次分群差异 print(pd.crosstab(km_a.labels_, km_b.labels_))init='random' 是最容易暴露随机性的配置,实际生产中不要用这个,默认或显式指定 k-means++ 就能大幅减少初始值影响。n_init 越大越能避开局部最优,但耗时也会线性增加,几万条样本用 n_init=10 是性能和稳定的折中。random_state 的唯一作用就是让随机过程可复现,任何一次建模都必须注明种子值,这比模型本身更重要,因为别人拿你的代码跑不出同一份结果,等于没有交付。
4.3 用PCA降维到二维做散点图:判断分群是否真的分开了
五个维度的聚类没办法直接画图观察,常规做法是用 PCA 把高维数据压到二维,再用散点图把簇的分布画出来。注意 PCA 只用于可视化验证,不能用 PCA 的二维结果重新聚类,那会丢掉太多方差信息。
from sklearn.decomposition import PCA import matplotlib.pyplot as plt pca = PCA(n_components=2, random_state=42) X_pca = pca.fit_transform(X_std) plt.figure(figsize=(9, 6)) scatter = plt.scatter(X_pca[:, 0], X_pca[:, 1], c=df['cluster'], cmap='tab10', s=6, alpha=0.7) plt.colorbar(scatter) plt.title('KMeans Clustering Result (PCA)') plt.tight_layout() plt.savefig('cluster_pca_scatter.png', dpi=150)看这张图时重点观察簇与簇之间是否有明显交叠。如果两个簇的散点完全混合在一起,说明这两个簇在原始空间里本来就没分开,这时候要考虑的不是调可视化参数,而是回去检查特征选择或 K 值是否需要调整。如果不同簇呈现出明显分界,哪怕边界不是那么干净,聚类效果从工程角度就可以接受,因为用户行为本身就是连续分布的,硬切边界必然存在一些边缘样本。
4.4 用雷达图给五类客户画像:一个能直接进汇报的图表
散点图给自己看,雷达图给业务看。雷达图的优势是把五个维度的均值差异直观呈现出来,一眼就能看出每个簇“什么高、什么低”。我给每个簇画一张雷达图,五张图横向对比,汇报时比贴一张簇中心表格有效得多。
import numpy as np labels = ['L', 'R', 'F', 'M', 'C'] # 顺序与特征一致 center_values = model.cluster_centers_ # 标准化后的簇中心 # 画第0个簇的雷达图 values = center_values[0].copy() values = np.append(values, values[0]) # 首尾闭合 angles = np.linspace(0, 2 * np.pi, len(labels), endpoint=False).tolist() angles += angles[:1] fig, ax = plt.subplots(figsize=(6, 6), subplot_kw=dict(polar=True)) ax.plot(angles, values, 'o-', linewidth=2) ax.fill(angles, values, alpha=0.25) ax.set_xticks(angles[:-1]) ax.set_xticklabels(labels) plt.title('Cluster 0 Profile') plt.tight_layout() plt.savefig('cluster_0_radar.png', dpi=150)这里直接使用标准化后的簇中心,优点是不同簇之间可以相互比较,缺点是标准化后的数值没有业务含义,比如某个特征值为 1.5 并不代表具体多少公里。如果想让雷达图直接展示业务指标,就把标准化后的簇中心按 scaler.inverse_transform 逆变换回原始值再画,轴标签改成实际数值,业务方更好接受。一张图里五条折线叠在一起会太乱,分开五张图,每张图标上簇编号和样本占比,汇报效果最好。
5. 避坑与排查:客户价值分析最容易翻车的五个地方
5.1 数据泄漏:把已经标注好的“客户等级”当特征,分数好看但全盘作废
现象:聚类轮廓系数很高,分群结果和原有客户等级几乎一一对应,看起来像模型自己学出了规律,实际没有产出任何增量价值。
原因:原始表里如果带了“客户等级”“价值分层”这类人工标签字段,把它们混进特征矩阵,KMeans 当然会依据这些标签把人群切得很干净。这不是模型找到了规律,而是数据泄漏。
解决:建模前明确区分行为字段和结果字段。会员卡号、入会时间、乘机时间、飞行次数、里程、折扣系数属于行为字段,可以进特征;客户等级、上季度消费分箱、是否流失标记属于结果字段,全部排除。我一般会在特征选择清单里单独列一列“是否结果字段”,给每列打钩确认后再建模。
5.2 标准化时误用两次fit_transform:新数据和旧数据的均值得不到同一套
现象:训练时对同一批数据调用了两次 scaler.fit_transform,第二次调用后之前的均值和标准差全被覆盖,新旧数据跑出来的分群结果对不上。
原因:fit_transform 等于先算均值和标准差,再用这套参数做变换,第二次调用意味着重新计算参数,原来那套参数丢了。
解决:训练阶段只调用一次 fit 或 fit_transform,之后统一走 transform。严格写出两个阶段的代码:
# 训练阶段:学参数并变换 scaler.fit(X_train) X_train_std = scaler.transform(X_train) # 上线阶段:只变换,不重新学参数 X_new_std = scaler.transform(X_new)5.3 random_state不固定:同一份数据两次聚类结果差异很大
现象:代码一行没改,昨天跑出来簇 0 有 8000 人,今天跑出来簇 2 有 8000 人,业务方直接质疑分析可靠性。
原因:KMeans 初始质心随机,没有固定随机种子,每次运行都可能收敛到不同的局部最优解。
解决:显式设置 model = KMeans(..., random_state=42),并把 random_state 作为参数写进配置脚本里。如果团队有严格的实验管理习惯,可以再把数据版本、特征版本、权重版本一并记录,确保任何一次结果都能被其他人复现。固定随机种子之后,KMeans 的“玄学”部分基本就消失了,剩下的是可以稳定讨论的参数问题。
5.4 读CSV时中文乱码、字段类型异常让代码直接崩
现象:pd.read_csv 报 UnicodeDecodeError,或者 FLIGHT_COUNT 显示为 object 类型,后面做减法或者聚类直接报 TypeError。
原因:飞书/Excel 导出的 CSV 常见编码是 gbk 或 utf-8-sig,和 Pandas 默认的 utf-8 不一致;字段里混入空格、引号或空值,也会被识别成字符串类型。
解决:读取时先试两种编码,乱码就用 encoding='gbk' 或 encoding='utf-8-sig'。字段类型用 pd.to_numeric 强制转换,再检查转换前后的 NaN 数量。
df = pd.read_csv('air_data.csv', encoding='gbk') df['FLIGHT_COUNT'] = pd.to_numeric(df['FLIGHT_COUNT'], errors='coerce') print(df['FLIGHT_COUNT'].isna().sum())5.5 聚类指标好看不等于业务可用:上线前必须验证两件事
现象:SSE 很小,轮廓系数也达标,但营销部门按簇投放之后响应率没有显著差异,或者各簇之间的客单价、退票率几乎相同。
原因:聚类只能保证“在特征空间里距离近的样本分到一起”,不能保证“分出来的这群人有不同的商业行为”。特征里如果缺少能区分业务结果的维度,分群自然没有业务差异。
解决:聚类上线前至少做两项验证。第一,把各簇的留存率、季度消费变化、退票率等业务指标拉出来对比,至少有两个簇在关键指标上有显著差异;第二,做一次小流量策略测试,比如对高价值簇发放里程券、对照簇不发放,观察后续消费变化。聚类标签本身不是终点,它只是把营销资源分配这件事从“全体人群”细化成“若干个可验证的人群”,验证不过关就回炉调特征,而不是硬着头皮上线。
6. 再进一步:把聚类标签变成自动分群策略,用规则引擎跑进实际业务
做完一次离线聚类只是第一步,真正能长期用的方式是把模型保存下来,对每天新增的会员做自动分群。常见做法是把 StandardScaler 和 KMeans 模型分别用 joblib 序列化保存,新数据跑同一套清洗逻辑,调用同一个 scaler.transform,再 predict 出簇标签。这样每一天的会员宽表进入系统时,都能自动算出它属于哪个价值群体,不需要重新训练模型。
import joblib # 训练完成后保存 joblib.dump(scaler, 'scaler.pkl') joblib.dump(model, 'kmeans_model.pkl') # 新一批会员进入时 scaler = joblib.load('scaler.pkl') model = joblib.load('kmeans_model.pkl') new_member = pd.DataFrame([...]) # 经过与训练时完全相同的清洗逻辑 new_std = scaler.transform(new_member[['L', 'R', 'F', 'M', 'C']]) cluster = model.predict(new_std)[0]得到 cluster 之后,再配一张策略映射表,把簇编号翻译成具体动作。比如簇 0 可能是高价值保持客户,动作是里程翻倍和优先升舱;簇 1 可能是高价值流失预警,动作是专属客服和唤回券;簇 4 是低价值客户,动作是标准邮件触达。这套映射逻辑用 Python dict 就能实现,也是一个最轻量的规则引擎。不要嫌它简单,多数公司的营销系统缺的不是复杂模型,而是把标签稳定地送到投放接口的过程。
生产环境跑这个方案时,我一般会再加一道定期重训的作业,比如每月用最近三个月的数据重新训练一次模型,避免因为航线调整或季节变化导致客户行为偏移后,分群标签失真。重训的时机判断不靠感觉,而是对比模型上线前后的簇中心和各簇样本占比,变化超过两成就要考虑重训。
最后说一个我养成的习惯:拿到任何数据分析项目,第一件事永远是先问数据字典里每个字段是行为还是结果、口径怎么定义。模型可以重跑,代码可以重构,但口径错了,后面所有结果都没有后悔药。希望以上的流程和坑能给你省下几天的排查时间,帮你在做客户价值分析时按图索骥,跑出一份业务方愿意接住的结果。
本文还有配套的精品资源,点击获取