简介:这份资源是面向数据分析与机器学习入门者的航空公司客户价值分析实战源码包,围绕客户分群、客户生命周期价值预测等典型业务问题展开,适合希望把Python数据分析技能落到真实场景的学习者。压缩包共10个文件,以xls与csv数据表为主,另含1个py脚本和1份模块说明txt,整体约17.13MB,数据文件可用于清洗、标准化与聚类建模,脚本则串联起完整分析流程。目前已有891人学习下载,说明该案例在同类练习中具有一定参考热度。读者可从中获得一套可运行的客户价值分析实现思路,涵盖数据预处理、特征工程、K-means客户分群以及价值评估等环节,并借助说明文档理解各模块职责与调用关系,便于在此基础上替换数据、调整参数或迁移到其他行业的客户分析任务中。
1. 从一份「航空公司客户价值分析Python源码.rar」说起:它到底能算出什么
拿到这个压缩包标题的人,十有八九是两种情况:一是做数据分析课程设计,导师丢过来一句「做个客户价值分析」;二是真在航司或OTA做会员运营,想用Python把手里那堆飞行记录、里程、票价数据盘活。不管哪种,核心诉求都一样——把「谁值得重点维护、谁快流失了、谁该被唤醒」这件事,用数据算出来,而不是拍脑袋。
航空公司的客户价值分析和电商、零售有本质区别:它的消费行为高度稀疏(一年飞几次很正常)、金额跨度极大(经济舱和头等舱差几十倍)、而且强绑定时间窗(淡旺季、常旅客等级周期)。所以直接套用RFM(最近一次消费、消费频率、消费金额)往往翻车,行业里更常用的是LRFMC模型——L是入会时长(Length),R是最近一次乘机间隔(Recency),F是飞行次数(Frequency),M是飞行总里程(Mileage),C是平均折扣系数(Coefficient)。这套模型是航司客户价值分析里最稳的骨架,源码包大概率也是围绕它展开。
这篇文章不假装我拆过你手里那个rar,而是把「航空公司客户价值分析」这件事从数据长什么样、指标怎么算、聚类怎么调、结果怎么读,一路讲到落地时最容易踩的坑。适合会一点pandas、但没做过完整客户价值项目的人照着复现,也适合做过RFM但被航司数据折磨过的老手对照参数边界。
2. LRFMC五个指标怎么从原始乘机流水里算出来
2.1 先搞清楚航司数据的三个典型表结构
真实航司或公开竞赛数据集(比如常见的航空客户价值数据集)通常长这样:一张客户信息表(会员卡号、入会时间、性别、年龄、工作地),一张乘机记录表(会员卡号、航班号、乘机日期、舱位、飞行里程、折扣率),有时还有一张积分兑换表。三张表靠会员卡号关联。
关键点在于:乘机记录表是一行一次飞行,同一个人有多行。所以LRFMC不是直接读出来的,而是分组聚合出来的。很多人第一步就错在把乘机记录当成客户表直接跑聚类,结果每个样本是一次飞行而不是一个人,聚类出来的「客户群」毫无意义。
先做一次合并和去重检查,这是所有后续计算的地基:
import pandas as pd import numpy as np # 读取三张表,注意编码,航司老系统导出常见gbk customer = pd.read_csv('customer_info.csv', encoding='gbk') flight = pd.read_csv('flight_record.csv', encoding='gbk') # 检查会员卡号是否有重复、空值 print(customer['会员卡号'].duplicated().sum()) print(flight['会员卡号'].isnull().sum()) # 统一卡号为字符串,避免前导零丢失 customer['会员卡号'] = customer['会员卡号'].astype(str) flight['会员卡号'] = flight['会员卡号'].astype(str) # 乘机日期转datetime,后续算R要用 flight['乘机日期'] = pd.to_datetime(flight['乘机日期'])逻辑说明:卡号转字符串这一步看着废话,但航司系统导出的卡号经常带前导零,pandas默认读成int会把00123变成123,合并时直接对不上,这是血泪经验。日期转datetime是为了后面做时间差,字符串相减会报错。
参数说明:encoding='gbk'是航司老系统的常见编码,如果报UnicodeDecodeError就换utf-8或gb18030。duplicated().sum()返回重复行数,正常应该是0,不是0说明客户表有脏数据,得先去重。
2.2 L、R、F、M、C五个指标的精确口径
这五个指标每一个都有「看起来对但实际错」的写法,逐个说清楚。
L(入会时长):用观测窗口的结束日期减去入会日期,单位是天或月。注意观测窗口要统一,比如统一取数据里最大乘机日期作为窗口终点,而不是用today(),否则每次跑结果都不一样。
R(最近一次乘机间隔):窗口终点减去该客户最后一次乘机日期。R越小说明越近,价值越高。
F(飞行次数):直接对乘机记录按卡号计数。但要注意往返算两次还是两次航班,一般按航班段算,一次中转算两段。
M(飞行总里程):按卡号对里程求和。这里有个坑——里程字段如果有缺失,直接sum()会跳过,但如果你想要「缺失当0」,得先fillna(0)。
C(平均折扣系数):这是航司特有的,等于平均折扣率。折扣率越低(比如0.3)说明买的是低价票,价值相对低;接近1说明全价或头等舱。计算时用mean()而不是sum()。
# 确定观测窗口终点 snapshot_date = flight['乘机日期'].max() + pd.Timedelta(days=1) # 按卡号聚合出R、F、M、C agg = flight.groupby('会员卡号').agg( R=('乘机日期', lambda x: (snapshot_date - x.max()).days), F=('航班号', 'count'), M=('飞行里程', 'sum'), C=('折扣率', 'mean') ).reset_index() # 合并客户信息,算L df = pd.merge(customer, agg, on='会员卡号', how='left') df['入会时间'] = pd.to_datetime(df['入会时间']) df['L'] = (snapshot_date - df['入会时间']).dt.days # 没飞过的客户,R/F/M/C补0或特定值 df[['R','F','M','C']] = df[['R','F','M','C']].fillna(0)逻辑说明:snapshot_date加一天是为了让「当天乘机」的R至少为1而不是0,避免出现0值干扰后续log变换。how='left'保证所有会员都保留,没乘机记录的会员R/F/M/C为NaN,这里填0——但要注意,R填0其实语义是「最近」,对没飞过的人是错的,更严谨的做法是给一个很大的R值或单独标记。这是源码包里经常处理得含糊的地方。
参数说明:lambda x: (snapshot_date - x.max()).days里x.max()是该客户最后一次乘机日期。F用航班号计数,如果航班号有缺失,改用乘机日期计数更稳。
2.3 标准化和log变换:为什么不能直接丢进KMeans
LRFMC五个指标量纲完全不同:L可能几千天,M可能几十万里程,C在0到1之间。KMeans是基于欧氏距离的,量纲大的指标会主导距离计算,结果就是聚类几乎只按M分。所以必须标准化。
但航司数据还有个特点:M和F是右偏的(少数人飞得极多),直接标准化后极端值仍然拉偏均值。常见做法是先做log变换再标准化:
from sklearn.preprocessing import StandardScaler # 对右偏的L、F、M做log1p,避免log(0) df['L_log'] = np.log1p(df['L']) df['F_log'] = np.log1p(df['F']) df['M_log'] = np.log1p(df['M']) features = df[['L_log','R','F_log','M_log','C']].copy() scaler = StandardScaler() features_scaled = scaler.fit_transform(features)逻辑说明:log1p等于log(1+x),专门处理含0的数据。R和C本身分布相对均匀,可以不做log。标准化用StandardScaler把每个指标变成均值0方差1,这样五个指标在距离计算里权重才公平。
参数说明:如果你想让某个指标权重更高(比如业务上更看重M),可以在标准化后乘以权重系数,但一般先跑默认权重看结果再调。
3. KMeans聚类跑通之后,怎么把簇翻译成业务语言
3.1 聚类数K到底选几个:肘部法和轮廓系数一起看
KMeans最大的玄学就是K选几。教科书说肘部法,但航司数据经常肘部不明显。实操里我一般同时看肘部法(inertia)和轮廓系数(silhouette),再结合业务能不能解释。
from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score import matplotlib.pyplot as plt inertias, silhouettes = [], [] K_range = range(2, 9) for k in K_range: km = KMeans(n_clusters=k, random_state=42, n_init=10) labels = km.fit_predict(features_scaled) inertias.append(km.inertia_) silhouettes.append(silhouette_score(features_scaled, labels)) for k, i, s in zip(K_range, inertias, silhouettes): print(f'K={k}, inertia={i:.1f}, silhouette={s:.3f}')逻辑说明:inertia_是簇内平方和,越小越紧凑,但K越大必然越小,所以看下降速度拐点。silhouette_score越接近1越好,但计算量大,K范围别开太大。random_state=42固定随机种子,保证每次结果一致,否则你调完参数发现结果变了会怀疑人生。n_init=10是跑10次不同初始化取最优,新版sklearn默认就是10,显式写出来更清楚。
参数说明:航司客户价值分析里,K一般取4到6。K=5经常对应「高价值常旅客、潜力客户、一般客户、流失客户、低价值客户」这种经典分群,业务上好解释。如果轮廓系数在K=5和K=6差不多,选5,因为少一个群运营成本低。
3.2 给每个簇打业务标签:看质心而不是看名字
聚类出来的0、1、2、3只是编号,没有含义。要把它翻译成业务语言,得看每个簇在原始指标上的均值(质心反标准化后)。
# 把聚类标签贴回原数据 df['cluster'] = labels # 按簇看原始LRFMC均值 profile = df.groupby('cluster')[['L','R','F','M','C']].mean().round(2) print(profile) # 看每个簇的人数 print(df['cluster'].value_counts())逻辑说明:groupby('cluster').mean()得到每个簇的画像。比如某个簇R很小、F和M很大、C接近1,那就是高价值客户;某个簇R很大、F很小,那就是流失客户。人数分布也要看,如果一个簇只有几个人,可能是异常值,考虑合并或剔除。
参数说明:round(2)只是显示好看,不影响计算。如果某簇人数占比低于5%,业务上通常不值得单独做运营策略,可以并到最近的簇。
3.3 用雷达图把五个簇的差异讲给业务方听
技术人自己看表格能懂,但给运营、市场同事汇报,一张雷达图胜过十行数字。把质心标准化后画雷达图,每个簇一条线,五个轴是LRFMC。
import matplotlib.pyplot as plt # 中文显示 plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False # 质心标准化 centers = df.groupby('cluster')[['L','R','F','M','C']].mean() centers_norm = (centers - centers.min()) / (centers.max() - centers.min()) angles = np.linspace(0, 2*np.pi, 5, endpoint=False).tolist() angles += angles[:1] # 闭合 fig, ax = plt.subplots(figsize=(6,6), subplot_kw=dict(polar=True)) for idx, row in centers_norm.iterrows(): values = row.tolist() + [row.tolist()[0]] ax.plot(angles, values, label=f'簇{idx}') ax.fill(angles, values, alpha=0.1) ax.set_xticks(angles[:-1]) ax.set_xticklabels(['L','R','F','M','C']) ax.legend(loc='upper right') plt.show()逻辑说明:雷达图前先把质心做min-max归一化到0-1,否则量纲差异会让图完全没法看。angles首尾闭合是matplotlib雷达图的标准写法。fill加透明度让重叠区域可读。
参数说明:SimHei是黑体,Linux服务器上可能没有,换成WenQuanYi Micro Hei或提前装字体。如果报字体警告但图能出,可以忽略。
4. 避坑与排查:航司客户价值分析里最容易翻车的五件事
4.1 现象:聚类结果每次跑都不一样
原因:KMeans初始化是随机的,没固定random_state,或者n_init太小导致陷入局部最优。
解决:KMeans(n_clusters=k, random_state=42, n_init=10),两个参数都写上。如果换了sklearn版本结果还是飘,检查是不是标准化那步用了会变的统计量。
4.2 现象:某个簇人数特别少,只有个位数
原因:数据里有极端值,比如某个客户里程几百万,KMeans为了它单独分一个簇。
解决:先看这个簇的原始指标,如果是异常值(比如测试账号、内部账号),直接剔除;如果是真实高价值客户,考虑用RobustScaler替代StandardScaler,或者对M做分位数截断(比如99%分位封顶)。
4.3 现象:R指标算出来是负数
原因:snapshot_date用了today(),而数据里有未来日期的乘机记录(数据录入错误或测试数据)。
解决:snapshot_date统一用flight['乘机日期'].max(),并且检查有没有大于当前日期的记录,有就过滤掉。
4.4 现象:合并后客户数变多了
原因:乘机记录表里有客户信息表里没有的卡号,how='left'不会增加行,但如果用了how='outer'就会。或者客户表本身有重复卡号。
解决:合并前对客户表按卡号去重,合并后用df.shape[0]和客户表行数对比,不一致就查merge的indicator参数。
4.5 现象:C指标出现大于1的值
原因:折扣率字段口径不统一,有的系统存的是百分比(如85),有的是小数(0.85)。
解决:先df['折扣率'].describe()看分布,如果最大值是100左右,统一除以100。这个坑在跨系统取数时特别常见。
5. 把分析结果变成可复用的打分卡:一个不用聚类的轻量技巧
聚类适合探索,但生产环境里每次新数据都重跑KMeans、簇编号还可能变,运营侧没法用。我一般会再补一步:把聚类结果沉淀成一张LRFMC打分卡,用规则给每个客户打分,新数据来了直接查表。
具体做法是:对每个指标按分位数切成5档,从高价值到低价值分别给5到1分,然后加权求和。权重根据业务定,比如M和F各0.3,R和C各0.15,L占0.1。这样每个客户得到一个0到5之间的价值分,直接排序就能用。
# 用分位数给每个指标打分,R是越小越好,所以反向 df['L_score'] = pd.qcut(df['L'], 5, labels=[1,2,3,4,5]).astype(int) df['R_score'] = pd.qcut(df['R'], 5, labels=[5,4,3,2,1]).astype(int) df['F_score'] = pd.qcut(df['F'].rank(method='first'), 5, labels=[1,2,3,4,5]).astype(int) df['M_score'] = pd.qcut(df['M'].rank(method='first'), 5, labels=[1,2,3,4,5]).astype(int) df['C_score'] = pd.qcut(df['C'], 5, labels=[1,2,3,4,5]).astype(int) # 加权总分 weights = {'L_score':0.1, 'R_score':0.15, 'F_score':0.3, 'M_score':0.3, 'C_score':0.15} df['value_score'] = sum(df[col]*w for col, w in weights.items()) # 按分数分档 df['value_tier'] = pd.cut(df['value_score'], bins=[0,2,3,4,5], labels=['低价值','一般','潜力','高价值']) print(df['value_tier'].value_counts())逻辑说明:qcut按分位数切5档,保证每档人数大致相等。R是反向指标,所以标签顺序反过来。rank(method='first')是为了处理大量重复值(比如很多客户F=1),直接qcut会因为边界重复报错。加权求和后cut分档,运营直接按value_tier筛选人群。
参数说明:权重是业务可调的,如果航司更看重里程,把M_score权重提到0.4。分档边界[0,2,3,4,5]根据实际分数分布调,别硬套。
这套打分卡的好处是可解释、可复现、可增量,新客户进来算一次分数就行,不用重跑聚类。我自己的习惯是:聚类用来发现「原来客户能分成这几种」,打分卡用来做「每天给运营一张可执行名单」。两者配合,比只交一个聚类模型实用得多。希望帮到你。
本文还有配套的精品资源,点击获取