资讯动态

航空公司客户价值分析:Python实现LRFMC模型与K-Means聚类实战

发布时间:2026/10/13 21:07:17 来源:尧图企业网站定制
简介面向航空公司客户价值分析场景的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, encodinggbk) print(air_data.shape) # (62988, 44) print(air_data.columns) # 44 个字段名这段代码里值得注意的就两个点encoding指定了gbk因为这份源数据用默认的utf-8去读会直接报UnicodeDecodeErrorsep没写因为 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, indexFalse, encodinggbk)这段逻辑看起来简单但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], axis1) 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, columnsLRFMC.columns) # 落盘这就是包里的 zscoredata.xls zscored.to_excel(zscoredata.xls, indexFalse) 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_clustersk, n_init10, max_iter500, random_state42) model.fit(zscored) # 每个样本的聚类标签 cluster_labels model.labels_ # 把标签合并回五维表方便后面分析 LRFMC[cluster] cluster_labelsn_clusters5是聚类数n_init10表示 K-Means 会随机初始化 10 次从中选最优解这个参数在 sklearn 新版本里尤其重要它直接决定聚类结果稳不稳定max_iter500是单次迭代的上限防止算法不收敛时无限跑下去random_state42固定随机种子保证每次运行得到相同的聚类结果——这个参数对复现实验至关重要不固定的话同一份数据每次跑出来的客户群标签都会不一样后面的业务解读就没法复现。跑完之后model.labels_就是每一条会员记录所属的客户群编号0 到 4把它合并回LRFMC表后面就能按客户群分组看特征差异了。3.3 聚类结果解读五类客户分别是谁聚类出来的编号本身没有业务含义必须结合每个客户群在 LRFMC 五个维度上的均值来解读。源码的做法是按聚类标签分组算各组均值再排序观察。# 按聚类标签分组计算五维均值 cluster_mean LRFMC.groupby(cluster)[[L, R, F, M, C]].mean() print(cluster_mean.round(2))看这张均值表的时候重点看每个客户群在哪个维度显著偏高、在哪个维度显著偏低。以常见的聚类结果为例客户群 0L 高、R 低、F 高、M 高、C 高——入会时间长、最近坐过、飞得多、里程高、折扣低愿意全价买这是价值最高的群体对应「重要保持客户」。客户群 1L 低、R 低、F 高、M 高、C 低——入会时间短但飞得很猛买的大多是折扣票对应「重要发展客户」有潜力但还没完全释放价值。客户群 2L 高、R 高、F 中、M 中、C 中——入会时间长但最近很久没坐了历史飞行中等对应「重要挽留客户」在流失边缘。客户群 3L 低、R 中、F 低、M 低、C 低——各项都低对应「一般客户」价值贡献有限。客户群 4L 中、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 折扣系数低说明飞得多但买的都是折扣票价值还没完全变现。再比如群 2L 高说明是个老会员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), endpointFalse).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, labelf客户群{clu}) plt.legend(locupper 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 编码的中文字段就崩了。解决读取时显式指定encodinggbk如果还是报错改成encodinggb18030这是 GBK 的超集兼容性更好。写回文件时也别忘了一致to_csv(data_cleaned.csv, encodinggbk)否则下次再读又得改编码。包里那份字段说明表也是同样的编码问题用 Excel 打开乱码的话先把它另存成 UTF-8 再打开。5.2 K-Means 聚类结果每次跑都不一样现象同一份zscoredata.xls今天跑聚类明天再跑一遍客户群的编号和人数对不上。原因K-Means 的初始质心是随机生成的不同初始质心会收敛到不同局部最优解这是算法本身的特性不是 bug。解决固定随机种子random_state42让每次运行的随机过程可复现。同时把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 上一般用SimHeiLinux 服务器上如果没装中文字体需要先装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_clustersk, n_init10, random_state42) labels model.fit_predict(zscored) # 轮廓系数 s silhouette_score(zscored, labels) scores.append(s) print(fK{k}, 轮廓系数{s:.4f}) # SSE肘部法则 sse.append(model.inertia_)跑完之后看两个输出轮廓系数序列里值最大的 K 优先选如果轮廓系数差异不大再配合 SSE 曲线找拐点。这两个指标结合起来能给你一个「K 取 5 是有依据的」量化结论而不是业务方一问就哑口无言。6.2 从指标回到业务验证聚类结果是否可行指标只是参考最终还要回到业务校验。我会把聚类结果的均值表拿给业务同事看问三个问题每个客户群能不能用一句话说清特征客户群之间的差异是否符合业务直觉有没有哪个客户群的特征组合在业务上解释不通如果三个问题都能答上这个 K 值才真正立得住。轮廓系数最高的 K 不一定是最优的因为指标只衡量统计意义上的聚类质量不衡量业务意义——这个取舍是做用户分群的人最容易忽略的。从那以后我每次做客户分群不管数据多干净不管业务方多急着要结论都强制自己先跑一遍轮廓系数和 SSE 曲线再拿聚类结果回去找业务核对一遍。这套动作加起来花不了五分钟但能避免把 K 值定错、把客户群解读跑偏省下的返工时间远不止五分钟。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑