
简介航空公司客户价值分析Python源码包是一套围绕客户分群与价值评估的实战代码资源面向正在学习数据挖掘、Python 数据分析及机器学习应用的开发者与研究者可服务于毕业设计、课程实验或行业实践参考。压缩包共收录10个文件其中5个xls表格和3个csv文件用于存放原始客户数据与中间结果1个py脚本实现了从数据清洗到模型构建的主要流程1个txt说明整理导入方式与模块作用整体大小17.13MB目录结构清晰、便于对应学习。代码覆盖数据预处理、特征构造、z-score标准化、K-means客户聚类和画像分析等关键环节依赖Pandas、Matplotlib、Scikit-learn等常见库稍加修改即可适配自有数据。已有891人学习浏览对于希望快速梳理航空公司客户价值分析完整路径、并得到可复现代码与数据样本的读者这是一份能明显节省踩坑时间、具备较强参考价值的实用源码包。1. 航空客户价值分析的真正难点不是算金额而是给客户分层看到“航空公司客户价值分析”这个标题很多人的第一反应是拉一张消费金额排行榜。但实际业务里真正的难点不是谁花钱多而是从一份会员飞行记录里判断哪些客户正在流失哪些客户只买最低折扣票哪些客户值得你主动维护。市面上这类 Python 源码包的核心思路几乎一致——把会员数据和航班数据整理成 LRFMC 五维特征再用 KMeans 把客户分成高价值、潜在价值、流失预警等几类。这条链路很适合数据分析学习者因为它把特征工程、无监督学习、业务解读一次串完。下面我把从模型选型到最终落地的完整做法拆开讲参数和坑都放在对应位置。2. 为什么航空客户价值分析要把 RFM 改成 LRFMC模型选择的业务逻辑2.1 标准 RFM 在航空场景的两个不适配点传统客户价值分析最常用的是 RFM 模型三个维度分别是 R最近一次消费时间、F消费频率、M消费金额。这个模型在电商、零售场景里好用因为消费金额能直接反映客户贡献消费频率也相对稳定。但把 RFM 直接套到航空业马上会碰壁。第一个问题是 M 维度的口径。机票金额受航线距离、淡旺季、提前购票天数影响极大同样飞十次有人飞京沪短途有人飞洲际长途金额能差出十几倍。而且票面金额里还混杂机场建设费、税费这些与客户价值无关的部分。第二个问题是 F 维度失真。飞十次短途的客户飞行成本可能还不如飞两次长途的客户高但 F 值会把前者判成更高频客户。换句话说标准 RFM 能回答“谁最近来过、谁来得勤、谁花得多”却回答不了航空业最关心的两个问题这个客户是忠诚老会员还是新会员他习惯买全价票还是打折票。常见做法是用两个新维度补足入会时长 L 和平均折扣系数 C。再加上针对航空的消费口径调整——用累计飞行里程代替消费金额。这样改造后的模型就是 LRFMC你可以把它理解成“航空版的 RFM 升级方案”。2.2 LRFMC 五维特征的业务含义与参数定义下表是航空客户价值分析里最常见的五维定义。各源码包里的列名可能不同但含义基本一致。维度常见计算口径业务含义价值方向L观察期结束日期减入会日期按月计客户与本航司保持关系的时间长度越长越有忠诚基础R观察期结束日期减最后飞行日期按天计距离上次飞行的时间间隔越大越可能流失F观察期内累计飞行次数飞行频次反映活跃度越大黏性越高M观察期内累计飞行里程总体里程贡献代替金额越大贡献越高C观察期内平均折扣系数买票时平均享受的折扣水平越小越接近全价票价值越高这里要特别小心 C 的方向。很多教程里默认avg_discount0表示全价票avg_discount0.5表示五折票所以 C 值越小说明客户越不依赖折扣商业价值越高。如果你拿到的数据里avg_discount0.8表示八折那方向就反了。拿到源码包第一步不是跑模型而是确认字段字典看 C 到底是“折扣系数”还是“支付比例”这决定后面画像解读时的正负号。2.3 观察窗口与锚点时间全项目最容易被忽略的设定LRFMC 里的每一项计算都依赖一个统一时间基准。很多新手直接拿“每条记录自己的最后飞行时间”去减算出来的 R 全都等于 0导致聚类结果彻底失去意义。正确做法是先确定一个观察期结束时间代码里通常叫LOAD_TIME或obs_end所有客户的 L 和 R 都用同一个锚点计算。import pandas as pd # 观察期结束时间一般取数据导入时间或所有航班记录的最大日期 obs_end pd.Timestamp(2023-12-31) # R最近一次飞行距今多少天锚点统一客户之间才可比 df_feat[R] (obs_end - pd.to_datetime(df_feat[LAST_FLIGHT_DATE])).dt.days # L入会时长换算成月份天数除以 30 只是近似够用 df_feat[L] (obs_end - pd.to_datetime(df_feat[FFP_DATE])).dt.days // 30这段代码里obs_end是一个固定值不是每行各自的最大日期。这样做目的是让所有会员站在同一条时间线上比较A 客户上次飞行是 30 天前B 客户是 300 天前R 值差异才有业务含义。观察窗口长度一般选 12 到 24 个月太短会漏掉低频客户太长会把多年前的飞行也计入当期价值导致数据失真。如果你发现源码里的LOAD_TIME字段和你最后的航班记录日期差很多先检查是不是数据本身不完整再去动锚点。3. pandas 数据清洗与 LRFMC 特征计算源码里最容易被跳过的部分3.1 原始表结构与字段映射先统一列名再谈建模航空客户价值分析的源码包通常带一到两个 CSV 数据文件常见结构是会员信息表加航班明细表。会员表里有会员编号、入会日期航班明细表里有每次飞行的日期、航班状态、里程数、折扣。因为数据来源不同列名可能是中文也可能是英文缩写先把字段统一成一套规范名后面所有代码才不会反复改。import pandas as pd # 会员表一个会员一行 member pd.read_csv(member_info.csv, encodinggbk) # 航班明细表一个会员可能有多条记录 flights pd.read_csv(flight_detail.csv, encodinggbk) # 按会员号左连接得到每一笔航班对应的会员资料 df member.merge(flights, onMEMBER_NO, howleft) df.head()读文件时最容易踩的坑是编码。很多业务导出的 CSV 是 GBK 编码直接pd.read_csv(xxx.csv)会报错或者出现乱码。encodinggbk不行就换encodinggb18030后者兼容性更好。列名规范方面我习惯把最终要用的字段固定为MEMBER_NO、FFP_DATE、LAST_FLIGHT_DATE、FLIGHT_COUNT、SEG_KM_SUM、AVG_DISCOUNT这样后面特征计算代码可以原样复用不容易被原始列名的各种变体绕晕。3.2 剔除无效飞行记录与从未飞行的会员拿到合并后的表先做两件事过滤无效航班状态剔除从未实际飞行的会员。航班状态字段在源码包里的值通常有OK正常、C取消、R退票等。取消和退票的记录不能计入有效里程和次数否则一个频繁退票的客户会被误判成高 F 值客户。# 航班状态为取消或退票的记录不计入客户价值 df df[~df[TICKET_STATUS].isin([C, R])].copy() # 累计飞行次数或累计里程为 0 的会员属于只注册未飞行的“僵尸会员” df df[(df[FLIGHT_COUNT] 0) (df[SEG_KM_SUM] 0)].copy()这两行过滤看似简单却是聚类质量的分水岭。如果保留未飞行会员他们的 L、R、F、M、C 会集中在一个特殊位置KMeans 很容易单独分出一个“全零簇”看起来像发现了一类新客群实际只是数据没洗干净。如果航班明细表和会员表是分开的先合并再过滤如果是已经聚合好的宽表只需要第二行过滤。判断依据是SEG_KM_SUM和FLIGHT_COUNT这两列的值是否都为 0。3.3 五维特征计算从明细数据到宽表的完整代码清洗完成后把数据按会员编号聚合生成每个会员一行、带五个特征的宽表。聚合时注意各字段的取法不同入会日期取最早最后飞行日期取最晚飞行次数求和里程求和折扣取平均。# 按会员聚合同一会员多条记录压成一行 df_agg df.groupby(MEMBER_NO).agg( FFP_DATE(FFP_DATE, min), LAST_FLIGHT_DATE(LAST_FLIGHT_DATE, max), FLIGHT_COUNT(FLIGHT_COUNT, sum), SEG_KM_SUM(SEG_KM_SUM, sum), AVG_DISCOUNT(AVG_DISCOUNT, mean) ).reset_index() # 观察期锚点源码包一般以数据导入时间或最大航班日期为准 obs_end pd.Timestamp(2023-12-31) # L入会月份数统一向下取整 df_agg[L] ((obs_end - pd.to_datetime(df_agg[FFP_DATE])).dt.days // 30).clip(lower0) # R最后一次飞行距今的天数 df_agg[R] (obs_end - pd.to_datetime(df_agg[LAST_FLIGHT_DATE])).dt.days # F、M、C直接取聚合结果并保证数值类型 df_agg[F] df_agg[FLIGHT_COUNT].astype(int) df_agg[M] df_agg[SEG_KM_SUM].astype(float) df_agg[C] df_agg[AVG_DISCOUNT].astype(float) # 打印分布检查是否还有异常极值 feat_cols [L, R, F, M, C] print(df_agg[feat_cols].describe())逻辑说明clip(lower0)是防御性写法防止个别脏数据里入会日期晚于观察期导致 L 为负数。R理论上不会为负但如果你发现大量客户的 R 小于 0说明原始数据里混入了晚于锚点的记录这时要回头检查数据日期范围而不是硬算。F用astype(int)是因为飞行次数不应该是小数M和C保留浮点数。描述统计里重点看两处M的最大值是否远大于 75 分位数如果出现“一个客户里程是别人一百倍”的情况考虑对M做 99 分位截断。另一种处理是保留长尾让 KMeans 自行区分超级客户但这会让聚类结果被少数极端值牵制我一般倾向截断。# 对 M 做 99% 分位截断避免超高频客户过度主导聚类中心 upper_m df_agg[M].quantile(0.99) df_agg[M] df_agg[M].clip(upperupper_m)截断不是删除而是把极端值压到 99 分位的位置保留该客户“很高”的相对位置但不再让单个客户把整个聚类中心拉走。处理完后把特征宽表存一份中间结果后面聚类和存档都用这份数据不用每次重新跑清洗。df_agg.to_csv(lr_fmc_features.csv, indexFalse, encodingutf-8-sig)保存用utf-8-sig是为了让 Excel 打开 CSV 时中文列名不乱码。到这一步建模前的数据准备工作就结束了。4. KMeans 聚类与五类客户分群训练、评估与打标签的完整代码4.1 标准化为什么这里必须用 z-score 而不是 MinMaxLRFMC 五个特征的单位差异非常大L 是几十个月R 是几百天M 是几万公里。如果不做标准化直接丢进 KMeans欧氏距离会被 M 一个维度主导其他四个特征形同虚设。常见的两种标准化是 MinMax 和 z-score这个项目里我选 z-score。from sklearn.preprocessing import StandardScaler feat_cols [L, R, F, M, C] scaler StandardScaler() X_scaled scaler.fit_transform(df_agg[feat_cols])MinMax 的问题在于它对离群点太敏感而且把数据压缩到 0 到 1 区间后聚类中心的数值解释要靠还原多一层转换反而麻烦。z-score 把每个特征变成均值为 0、方差为 1 的分布聚类中心数值的正负可以直接理解为“高于平均水平”或“低于平均水平”后面打标签时非常直观。fit_transform是在训练集上拟合并转换如果你后面要预测新客户必须用同一个scaler去transform不能重新 fit否则特征分布会被新数据改变。4.2 聚类数 k 的选择肘部图、轮廓系数和业务可解释性KMeans 需要手动指定聚类数。有些人只看肘部图有些人只看轮廓系数我的习惯是两者都跑但最终拍板看业务含义。下面是同时绘制肘部图和轮廓系数的做法。import matplotlib.pyplot as plt from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score k_range range(2, 9) inertia [] silhouettes [] for k in k_range: km KMeans(n_clustersk, random_state42, n_init10) labels km.fit_predict(X_scaled) inertia.append(km.inertia_) silhouettes.append(silhouette_score(X_scaled, labels)) # 画肘部图找拐点 plt.plot(list(k_range), inertia, markero) plt.title(KMeans Elbow) plt.show() # 画轮廓系数找峰值 plt.plot(list(k_range), silhouettes, markero) plt.title(Silhouette Score) plt.show()inertia_是簇内误差平方和值越小说明样本离簇中心越近但它会随 k 增大单调下降真正的拐点才是合适的 k。silhouette_score取值范围从 -1 到 1越大说明簇内紧凑、簇间分离。实际跑这个数据轮廓系数往往在 k2 或 k3 时最高但这不代表业务上要选 2。把客户只分两类业务方会觉得“仿佛说了又仿佛没说”。航空客户价值分析的惯例是分五类对应高价值忠诚、潜在价值、流失预警、普通、低价值五档k5 在肘部图上通常接近拐点位置又足够细还能讲出故事。代码里random_state42和n_init10是固定项后面避坑章会说为什么这两个参数必须写。4.3 训练 KMeans 与聚类中心解读给数字簇起业务名字选定 k5 后重新训练然后把每个簇的特征均值打印出来。这里有个关键操作我们看的是还原回原始单位的均值而不是 z-score 后的均值否则业务方看不懂“-1.2”是什么意思。# 最终模型 final_model KMeans(n_clusters5, random_state42, n_init10) df_agg[Cluster] final_model.fit_predict(X_scaled) # 各簇特征均值标准化后的数值 profile_z df_agg.groupby(Cluster)[feat_cols].mean() # 还原成原始单位再解读 profile_raw pd.DataFrame( scaler.inverse_transform(profile_z), columnsfeat_cols ).round(2) print(profile_raw)scaler.inverse_transform是把标准化后的簇中心还原回原始尺度。比如标准化后的 R 均值是 -0.8还原后可能是 45表示这个簇的客户平均 45 天没飞了。对照这些数值可以给每个簇打业务标签下面是一个典型的判断思路。# 说明标签映射必须根据上一步打印的 profile_raw 调整顺序 cluster_labels { 0: 高价值忠诚客户, # L 高、R 低、F 高、M 高、C 低 1: 流失预警客户, # R 高、F 和 M 中等偏低 2: 潜在价值客户, # L 中等、R 中等、F 和 M 中上 3: 普通客户, # 五个特征都在均值附近 4: 低价值折扣客户 # C 高、M 低、F 低 } df_agg[Customer_Type] df_agg[Cluster].map(cluster_labels)这里要特别提醒KMeans 的簇编号是随机的0不一定是高价值1也不一定是流失预警。上面代码里的映射是我为“某一种典型聚类结果”写的示例你跑出来的编号顺序大概率不同。正确流程是先看profile_raw的每一行数值再决定哪个簇叫什么名字。比如某簇 R 特别大、L 偏短、F 刚过均值就优先考虑流失预警某簇 C 特别小、M 很大就是高价值。输出一张可以直接交给业务方的表是这章的落点result df_agg[[MEMBER_NO, L, R, F, M, C, Cluster, Customer_Type]] result.to_csv(customer_segmentation_result.csv, indexFalse, encodingutf-8-sig)这张表就是整个价值分析项目的核心产出每个会员一行带五个特征、簇编号和业务标签。后续做短信召回、里程营销、高价值客户维护都以这张表为起点。5. 航空客户价值分析源码运行时的 6 个常见坑与排查手册5.1 R 与 L 的时间单位不统一导致聚类第一特征变成玄学现象聚类结果里有一个簇的 R 均值小到 0 附近另一个簇 L 均值却大得离谱怎么看都不符合业务直觉。原因L 按天算没有除以 30R 按月份算没乘回天数五个特征的量纲混乱标准化后也难以纠正。解决L 统一用“天除以 30 取整”R 统一用“天”保证时间维度单位一致。df_agg[L] ((obs_end - df_agg[FFP_DATE]).dt.days // 30).clip(lower0) df_agg[R] (obs_end - df_agg[LAST_FLIGHT_DATE]).dt.days5.2 没有剔除从未飞行的会员聚类产生“僵尸客户簇”现象聚类结果里总有一个簇F 和 M 全是 0L 却很大它看起来像一类“长期注册但从不飞行”的客群然后你还会认真分析怎么挽回他们。原因清洗阶段只过滤了航班状态没有过滤总里程为 0 的会员。解决在聚合前或聚合后把FLIGHT_COUNT 0或SEG_KM_SUM 0的会员剔除。这类客户真实存在但他们不该进入价值分群模型应该单独放进“沉睡会员召回清单”处理。5.3 KMeans 每次跑出来的簇编号和簇中心都不一样现象同一份数据第一次跑簇 0 是高价值客户第二次跑簇 0 变成低价值客户第三次又变了业务方看到前后不一致直接不信任结果。原因KMeans 初始质心随机n_init和random_state没固定。解决训练时固定random_state42, n_init10。另外固定随机种子只保证你的代码稳定不保证不同版本 sklearn 之间稳定所以还要在训练后对簇中心做一次排序重编号。# 按 M 均值降序重排簇编号让编号顺序相对稳定 centers pd.DataFrame(final_model.cluster_centers_, columnsfeat_cols) centers[new_label] range(len(centers)) # 默认按当前顺序编号 order centers.sort_values(M, ascendingFalse).index label_map {} for new_idx, old_idx in enumerate(order): label_map[old_idx] new_idx df_agg[Cluster] df_agg[Cluster].map(label_map)这段代码把 M 均值最高的簇重编号为 0次高为 1以后再运行只要聚类边界没大变编号顺序就能保持一致。5.4 空值处理不当把缺失值变成“假零簇”现象模型训练报错或者聚类结果里出现一个所有特征都接近 0 的簇。原因直接用dropna()会把样本删光直接fillna(0)又把所有缺失值拉到同一个位置让 KMeans 认为它们是一类人。解决区分缺失场景。R 为空代表没有飞行日期这类样本应在清洗阶段删除M 为空需要用中位数填充C 为空可以按 0 或 1 填充取决于字段定义。df_agg[M] df_agg[M].fillna(df_agg[M].median()) df_agg[C] df_agg[C].fillna(0)逻辑说明M 是连续分布用中位数不容易引入偏态C 是折扣系数空值填 0 在“0 代表全价”的语义下是保守做法如果你确认字段是“0.8 代表八折”那就要改成填 1。5.5 观察期锚点不统一把新会员误判成流失客户现象R 值整体偏大大量客户被标成“流失预警”但这个结果和市场部实际感知不符。原因有些人用每个客户自己的最大航班日期当锚点导致老客户 R 很小、新客户 R 很大彼此不可比。解决锚点必须全局统一一般取源码包数据附带的LOAD_TIME没有就取所有记录的最大日期。如果新会员入会时间距离锚点只有两个月R 天然就小不要拿他和入会十年的老会员比绝对值聚类会自动把“短期高活跃”和“长期高活跃”分开。5.6 不同 sklearn 版本的 KMeans 接口差异导致报错现象源码在 Python 3.7 能跑换到 Python 3.11 报n_init类型错误或者提示random_state参数不存在。原因sklearn 1.2 前后 KMeans 参数默认值和校验规则有变化。解决显式传入random_state42, n_init10不要依赖默认值如果遇到老代码里写n_initauto改成整数。聚类完成后顺带用joblib.dump把模型和标准化器存下来方便下次直接预测。import joblib joblib.dump(final_model, kmeans_model.pkl) joblib.dump(scaler, scaler.pkl)注意保存模型和保存数据表同样重要。否则下次跑出新标签老表和新表对不上你会陷入“到底该信哪一版结果”的困境。6. 让价值分析真正落地把聚类标签做成季度客户价值监控表聚类结果如果只输出一次本质上是一张静态画像三个月后再看可能已经失效。我习惯把它改造成可滚动更新的监控表模型和标准化器保存下来每个月只对新增飞行记录做特征更新再用旧模型预测新客群。import joblib import pandas as pd model joblib.load(kmeans_model.pkl) scaler joblib.load(scaler.pkl) # 新客户特征宽表列名必须和训练时一致 new_feat pd.read_csv(new_customer_features.csv) new_feat[[L, R, F, M, C]] scaler.transform(new_feat[[L, R, F, M, C]]) # 预测簇编号并读取业务标签映射 new_feat[Cluster] model.predict(new_feat[[L, R, F, M, C]]) new_feat[Customer_Type] new_feat[Cluster].map(cluster_labels)监控表里除了基础特征和类型标签我还会叠加两张列当前 R 跟上期 R 的差值以及 F 的趋势方向。差值大于 30 天的客户进“沉睡预警”90 天以上进“流失预警”。如果 L 大于 24 且 C 小于 0.25这类人即使 R 还没变大也要放到高价值保持清单里优先维护。这比单看一个簇标签更能反映动态变化。验证方法上不要只看轮廓系数真正有效的验证是人工抽查。我从聚类结果里随机抽 20 个高价值客户和 20 个低价值客户让业务侧同事对照近半年的订票记录判断标签是否靠谱。如果高价值客户里有三成已经大半年没消费说明建模时 R 的特征权重被其他维度盖住了需要回头检查标准化和聚类数。我第一次跑完这个项目时只交了一张聚类结果表和一张轮廓系数图被业务方问“所以呢”问住了。后来我养成的习惯是先固定随机种子再固定簇编号顺序最后把输出表做成按月的更新任务这套价值分析才算真正从代码变成了能指导运营动作的工具。希望帮到你。本文还有配套的精品资源点击获取