ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

航空公司客户分群实战:K-means聚类与LRFMC特征工程全解析

航空公司客户分群实战:K-means聚类与LRFMC特征工程全解析 简介航空公司客户价值分析是面向数据分析初学者与运营人员的实战项目基于真实航班数据结合Python与K-means聚类帮助读者掌握客户分群与价值识别的完整思路。资源共4个文件包含2个Python脚本核心算法与可视化展示、1个CSV原始数据历年客户飞行记录和1个PPT演示文稿分析结论与策略建议压缩包总大小约6.87MB。目前已有2870人学习下载。压缩包内含的四个文件可完整走通数据加载、缺失值处理、标准化、K-means聚类建模与结果可视化流程其中main.py实现核心聚类逻辑display.py绘制客户分布图air_data.csv提供可复现的实验数据PPT系统梳理了从业务理解到策略落地的分析框架。分析涉及飞行里程、乘机次数、平均折扣等关键特征可帮助识别高、中、低价值客户群体并为差异化营销与航线优化提供数据支撑。1. 航空公司客户分群为什么绕不开K-means这道坎做过用户运营的人大概都遇到过这个场景数据库中躺着几十万条航班记录销售团队要你给出哪些人值得送升舱券而传统RFM模型在航空业里却总差一口气——因为里程累积和会员等级的权重远高于消费频次单纯用最近消费时间、频率、金额三个轴去切分很容易把一年飞两次两仓的商务客和每月飞一次的折扣旅客混为一谈。这个项目给出的解法是把RFM改造成LRFMC五维特征再用K-means做无监督聚类把客户硬生生分成五群每一群对应一套营销策略。整个分析链路从air_data.csv出发经过Pandas清洗、构建五个衍生特征、标准化再到Scikit-learn里的KMeans迭代收敛最后落到PPT里的特征雷达图。实操中你会发现K-means本身不复杂真正的坑全在数据清洗和K值选定上——这也是这篇文章想重点拆开的部分。项目适合刚接触聚类分析的数据分析师、Python学习者也适合想用现成代码快速跑通客户分群流程的运营开发。2. 源数据清洗与特征工程从air_data.csv到LRFMC特征矩阵2.1 原始数据的基本构成与清洗目标air_data.csv是典型的航空公司销售数据快照每条记录对应一个会员在一段时间内的乘机汇总常见字段包括会员卡号、入会时间、首次乘机日期、末次乘机日期、航班数、总里程、票价收入、折扣系数等。但真实数据往往存在三类问题票价为空的记录、折扣系数为0但里程不为0的异常行、以及重复的会员卡号。import pandas as pd import numpy as np df pd.read_csv(air_data.csv, encodinggbk, enginepython) print(df.shape) print(df.isnull().sum()) # 1) 丢弃票价为空的记录 df df.dropna(subset[SUM_YR_1, SUM_YR_2]) # 2) 丢弃票价为0但飞行里程0的异常记录 df df.drop(df[(df[SUM_YR_1] 0) (df[avg_discount] ! 0)].index) df df.drop(df[(df[SUM_YR_2] 0) (df[avg_discount] ! 0)].index) # 3) 按会员卡号去重 df df.drop_duplicates(subsetMEMBER_NO, keepfirst) print(df.shape)这段代码做了三件事dropna删除两个年度票价字段为空的记录避免后续计算LRFMC时出现NaN第二、三步分别过滤票价为0但折扣系数不为0的冲突数据以及重复会员卡。实际跑数时我一般还会加一条df[avg_discount] 1的判断因为折扣系数超过1说明票价原价上浮如果业务上不存在这种场景就要剔除。领到一份新数据先别急着聚类花15分钟做描述性统计比写模型更重要print(df[[FFP_DATE, LAST_TO_END, FLIGHT_COUNT, SEG_KM_SUM, avg_discount]].describe())注意LAST_TO_END在源文件里往往是观测窗口结束日期到末次乘机日期的间隔天数已经是距今多久没飞的语义不需要再手工换算。窗口结束日期一般取所有记录中的最大日期项目代码里通常硬编码为2014-03-31前后,你换成自己数据集的最大日期即可。2.2 LRFMC五维特征是怎么构建出来的传统RFM不适合航空业因为普通旅客可能一年飞20次短途总消费却比不上一次洲际两仓。这个项目改用五个维度。特征对应字段业务含义计算方式L入会时长FFP_DATE 到窗口结束日期会员成熟度结束日期 − FFP_DATE取月份数R最近乘机间隔LAST_TO_END流失风险原始字段直接取值越小越危险F乘机次数FLIGHT_COUNT活跃度原始字段直接取值M总里程SEG_KM_SUM出行总量原始字段直接取值C折扣系数均值avg_discount舱位等级敏感度原始字段直接取值越低越折扣敏感核心代码在main.py里体现为构造特征数据文件中的原始记录并不直接给出这五列需要自己组合# 五维特征构建 df[L] (pd.to_datetime(2014-03-31) - pd.to_datetime(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 df[[L, R, F, M, C]].copy()关于L的计算用dt.days // 30把天数折算成月而不是直接用pd.DateOffset(months1)做减法是因为样本量大时后者逐行计算效率低。如果你想更严谨一点可以用(结束日期 - FFP_DATE).dt.days / 30.0保留小数但K-means基于欧氏距离微小差别不影响聚类边界。R越小代表最近刚飞过流失概率低F和M天然正相关但M包含了航距权重能区分高频短途和低频长途用户。2.3 Z-score标准化为什么比Min-Max更稳LRFMC五个字段量纲差异巨大L可能从1到120M则从0到几十万直接丢进KMeans会导致M一维主导整个距离计算。项目标准做法是Z-score标准化即(x - mean) / std它不像Min-Max那样对离群值敏感——航空数据里偶尔出现的百万里程会员会把Min-Max的缩放区间拉得很宽导致普通用户的特征值全部挤到0附近。from sklearn.preprocessing import StandardScaler scaler StandardScaler() features_scaled scaler.fit_transform(features) # 关键保存scaler用于后续新数据映射 import joblib joblib.dump(scaler, scaler.pkl)fit_transform在训练集上计算均值和标准差并完成转换后续如果来了一条新客户数据做预测必须用同一个scaler.transform()重新fit会导致分布偏移。我踩过这个坑分析阶段全部重跑没问题但落到生产API时每次请求都重新fit聚类边界完全漂移。记得把scaler和训练好的模型一起打包。标准化后每个特征均值约0、方差约1这时再做K-means五个维度才真正等权参与距离计算。3. K-means聚类实现K值确定与Scikit-learn落地3.1 先理解K-means在这类数据上的行为边界K-means是最经典的原型聚类算法目标是把N个样本划分到K个簇中使每个样本到其所属簇中心的距离平方和最小。算法流程四步随机初始化K个质心计算每个样本到所有质心的距离归属最近的质心重新计算每簇的均值作为新质心重复直到质心变化小于阈值或达到最大迭代次数。因为初始质心是随机的random_state参数直接决定结果可复现性项目中用random_state42锁定。但K-means默认用欧氏距离对球形分布的数据效果好而客户价值数据通常不是标准球形。所以实操中一般先用PCA降维看看数据的松散程度如果形状诡异要考虑先做对数变换再标准化或者改用GMM。这个项目的数据分布相对集中直接标准化后聚类即可。from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score K_range range(2, 9) sil_scores [] inertia [] for k in K_range: model KMeans(n_clustersk, random_state42, n_init10, max_iter300) labels model.fit_predict(features_scaled) sil_scores.append(silhouette_score(features_scaled, labels)) inertia.append(model.inertia_) print(fk{k}, silhouette{silhouette_score(features_scaled, labels):.4f}, inertia{model.inertia_:.1f})n_init10代表用10组不同初始质心跑完取最优max_iter300限定了单次迭代上限。轮廓系数取值范围[-1, 1]越大说明簇内越紧凑、簇间越分离一般0.25以上就算可接受的聚类结构。model.inertia_是簇内误差平方和画出来就是肘部图。3.2 肘部法则加上轮廓系数双指标定K定K值是K-means实操里争议最大的环节。只靠肘部法则经常看到曲线平滑下降没有明显拐点我一般用两个指标合力判断。import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False fig, ax1 plt.subplots() ax2 ax1.twinx() ax1.plot(K_range, inertia, bo-, labelinertia) ax2.plot(K_range, sil_scores, rs--, labelsilhouette) ax1.set_xlabel(K) ax1.set_ylabel(inertia) ax2.set_ylabel(silhouette score) plt.savefig(k_selection.png, dpi150)运行后观察inertia曲线在k5之后下降明显变缓形成肘部轮廓系数在k5附近取得局部峰值说明5类在这个数据集里最合适。如果两个指标冲突优先相信轮廓系数——它直接衡量结构质量而inertia单调递减是数学必然拐点判定主观性强。3.3 最终聚类模型的完整训练流程final_model KMeans(n_clusters5, random_state42, n_init20, max_iter500) cluster_labels final_model.fit_predict(features_scaled) df[CLUSTER] cluster_labels print(df.groupby(CLUSTER)[[L, R, F, M, C]].mean())输出每个簇的特征均值表是PPT里雷达图的底层数据。n_init从10提到20是为了让k5的每一个初始配置都充分跑避免局部最优——数据量一万以内看不出差距但十万元以上时靠n_init增加计算量换来稳定性是值得的。random_state固定后重跑代码得到的簇编号顺序完全一致但要注意簇编号本身没有业务含义0号不一定是低价值客户必须根据均值表逐簇解读。4. 客户分群结果解读从聚类编号到业务价值标签4.1 先看五个簇的特征均值表聚类完成后核心工作变成给每个簇起一个业务名字。假设输出如下各维度均为标准化后的均值正负代表高于或低于全量平均水平簇LRFMC业务解读0高低高高高高价值忠诚客户1中高低低低流失边缘折扣客户2低中低低中新入会潜在客户3高低高中低高频折扣里程客4中高低中高沉默高舱客户注意聚类编号的顺序取决于K-means初始化在你自己的环境里可能完全不同判断依据永远是均值表的相对大小而不是编号。建议按R值从低到高排序R低的簇群体量通常最小却是营销价值最集中的一群。4.2 用雷达图把五个簇画成一张业务图display.py的价值在这里体现把标准化均值映射到五轴雷达图上业务方一眼能看出每类客户的长短板。import matplotlib.pyplot as plt import numpy as np features_mean df.groupby(CLUSTER)[[L, R, F, M, C]].mean() labels [L, R, F, M, C] fig, axes plt.subplots(1, 5, figsize(20, 4), subplot_kwdict(polarTrue)) for i, (idx, row) in enumerate(features_mean.iterrows()): vals row[labels].values vals np.concatenate([vals, [vals[0]]]) angles np.linspace(0, 2 * np.pi, len(labels), endpointFalse).tolist() angles angles[:1] axes[i].plot(angles, vals, o-, linewidth1) axes[i].fill(angles, vals, alpha0.25) axes[i].set_xticks(angles[:-1]) axes[i].set_xticklabels(labels) axes[i].set_title(fCluster {idx}) plt.tight_layout() plt.savefig(radar_clusters.png, dpi200)雷达图适合5个维度以内的对比维度再多就会变成蜘蛛网看不清。注意我这里使用np.concatenate把第一个值接到列表末尾目的是让图形闭合否则雷达图的边会缺一条。每个簇用一个子图画出来横向排列方便对比五张图放在PPT同一页效果最好。4.3 从业务角度审视聚类的合理性分群不是算完就结束。拿到五个簇后我会做两个验证一是算每个簇的样本占比占比低于5%的簇要警惕是否是离群值构成的孤岛二是算每个簇的SUM_YR_1 SUM_YR_2均值验证高价值簇的贡献是否真的最高。df[TOTAL_REVENUE] df[SUM_YR_1] df[SUM_YR_2] revenue_by_cluster df.groupby(CLUSTER)[TOTAL_REVENUE].mean() print(df.groupby(CLUSTER).size() / len(df) * 100) print(revenue_by_cluster.sort_values(ascendingFalse))这段代码输出两个表各簇人数占比、各簇平均收入排序。如果某个簇人数占比只有2%但平均收入非常高那它不是独立业务群而是少量头部客户营销策略要单独设计不能和其他簇共享活动方案。一个常见误用是直接对原始特征做K-means而不检查特征相关性。LRFMC里F和M高度正相关Z-score没有消除相关性聚类时会变相放大这两个维度的权重导致簇边界偏向里程维度。稳妥做法是先用PCA白化再聚类或者保持五个维度并用但在业务解释时把F和M合并看。我建议优先保持五个维度因为PCA后维度失去业务可解释性PPT汇报时会更难讲。5. 一个稳定性的验证技巧用轮廓系数和簇内距离把聚类结果兜住最后的技巧是关于验证的因为聚类模型不像分类模型有标签可以计算准确率很容易出现代码跑了、图出了、但结果根本不稳定的情况。你在PPT里展示五个簇之前先花五分钟做两个检验。from sklearn.metrics import silhouette_samples import numpy as np sample_sil silhouette_samples(features_scaled, cluster_labels) df[SIL] sample_sil # 检查每个簇的轮廓系数分布 sil_stats df.groupby(CLUSTER)[SIL].agg([mean, std, count]) print(sil_stats) # 筛选出轮廓系数为负的样本看看是不是离群值 bad_samples df[df[SIL] 0] print(bad_samples.shape) print(df[df[SIL] 0][CLUSTER].value_counts())轮廓系数是按样本计算的整体均值只是汇总。逐簇看时你会发现某些簇的均值很高但标准差很大说明簇内既有非常紧凑的核心成员也有不少边界样本被硬塞进来。轮廓系数为负的样本代表它距离相邻簇中心更近这类样本如果占比超过10%说明K值偏小应该尝试k6或者k7。这段代码输出bad_samples的分布如果负值样本集中出现在某一个簇下一轮迭代建议把那个簇拆开再聚一次。第二个验证技巧是重采样稳定性检验。K-means对初始化敏感即使random_state固定也只能保证一份代码可复现不能保证业务上稳定。我的做法是多次改变random_state跑同样K值然后计算两两结果的调整兰德指数ARI看聚类结构是否一致from sklearn.metrics import adjusted_rand_score labels_list [] for seed in [0, 42, 2024, 999, 12345]: km KMeans(n_clusters5, random_stateseed, n_init20, max_iter500) labels_list.append(km.fit_predict(features_scaled)) for i in range(len(labels_list)): for j in range(i 1, len(labels_list)): print(fseed pair [{i},{j}] ARI {adjusted_rand_score(labels_list[i], labels_list[j]):.4f})ARI取值范围[-1, 1]1代表两个聚类结果完全一致。如果不同随机种子得到的ARI低于0.8说明数据本身的簇结构比较模糊或者K值选偏了。这时候回到第3章重新画肘部图把k换掉再跑一轮不要强行用k5的结果。最后把验证结论写进PPT汇报里不只放雷达图和分布饼图再加上一页轮廓系数分布图和ARI对比表业务方对聚类结果的信任度会高很多。毕竟无监督分析最大的质疑就是你凭什么说客户分五类验证指标能有效回答这个问题。如果验证通过再去做升舱券投放、积分优惠和航线调整才会落到可靠的客群基础上。本文还有配套的精品资源点击获取
返回列表