基于LRFMC的航空公司客户价值分析Python实战指南
2026/9/15 17:15:46 网站建设 项目流程

简介:航空公司客户价值分析Python源码与配套数据文件构成一套完整的数据挖掘实践项目,面向具备一定Python基础、希望掌握商业分析或机器学习流程的学习者,目标是基于航空公司的会员信息与飞行记录,通过特征工程和聚类方法识别不同类型的客户价值。压缩包共10个文件,包括5个Excel数据表(存储原始数据与中间结果)、3个CSV数据文件、1个Python主程序及1个导入模块说明文档,总体积17.13MB,文件覆盖数据清洗、z-score标准化、特征构造、聚类建模与结果导出等关键环节。目前已有891人学习下载,读者可通过源码完整走通从原始数据探索、缺失值处理、RFM指标计算,到K-means客户分群及结果可视化的全流程,并结合案例理解特征选择、模型评估及业务应用思路。代码结构清晰、注释完整,覆盖常见数据挖掘竞赛或实训中客户价值分析的核心步骤,适合作为课程设计、毕业设计或企业客户分析项目的参考实现,便于二次开发与扩展。

1. 把客户价值分析从零售 RFM 平移过来,哪里会先翻车

拿到 air_data.csv 做航空公司客户价值分析,最常见的错误是直接照搬零售场景的 RFM 模型,用消费间隔、消费频率、消费金额去套 K-means,结果把买全价票的商务客和刷折扣票的里程客归成同一类。LRFMC 是航空场景的替代方案,用入会时长 L、最近乘机间隔 R、飞行次数 F、总里程 M、平均折扣系数 C 描述会员价值。这套 Python 源码从 air_data.csv 一路处理到 zscoreddata.xls,中间经过 data_cleaned.csv 的清洗和 z-score 标准化,再交给 K-means 分群,全程只用 pandas 和 sklearn,不依赖任何深度学习框架。适合有半年以上 Python 基础、想完整复现客户价值分析链路的数据分析师。下载包里的 explore.xls 和属性说明文件,可以直接作为探索阶段的输出参考。

2. 数据读取与探索:先摸清 air_data.csv 有哪些列可用

2.1 导入模块与运行前的环境检查

导入模块说明.txt 里按顺序列了 pandas、numpy、matplotlib、seaborn 和 sklearn 的 preprocessing 模块。聚类只用到 sklearn.cluster.KMeans,标准化用到 preprocessing.StandardScaler,整个项目不依赖深度学习框架,几个类就能完成。跑之前先确认环境里装了 openpyxl,因为 to_excel 写 .xls 文件时默认走 openpyxl 引擎,缺了它会在落盘那一步报 ImportError。

pip install pandas numpy matplotlib seaborn scikit-learn openpyxl

装完依赖后开始读数据:

import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from sklearn.preprocessing import StandardScaler air_data = pd.read_csv('air_data.csv', encoding='gb18030') print(air_data.shape) print(air_data.dtypes) print(air_data.isnull().sum())

逻辑说明:read_csv 的 encoding 参数是第一道坎。航司业务系统导出的 CSV 大多是 GB18030 编码,直接用默认 utf-8 打开会在第一行就抛 UnicodeDecodeError。air_data.shape 输出类似 (62892, 44) 的行列数,44 列里真正参与 LRFMC 建模的只有五个字段,其余像 FFP_TIER、WORK_CITY 这些保留到探索阶段做参考,特征工程阶段再剔除。isnull().sum() 按列统计缺失值,如果某列缺失数超过行数的一半,这列基本可以直接丢,不需要再花时间填充。

参数说明:encoding 参数只在读取时生效。如果列名能读出来但内容乱码,八成是文件实际编码和指定编码不一致,可以依次试 gb18030、gbk、utf-8-sig 这几种常见编码。读进来的 DataFrame 先不要急着改列名,等确认编码正确后再处理,否则排查问题时会混入两个变量。

2.2 explore.xls 里应该看到什么

explore.xls 是第一步探索性分析的落盘产物,里面装的是描述统计表和分布直方图。它回答三个问题:哪些字段有缺失、哪些字段有业务异常值、哪些字段分布是长尾。

explore_cols = ['FFP_DATE', 'LAST_TO_END', 'FLIGHT_COUNT', 'SEG_KM_SUM', 'avg_discount', 'SUM_YR_1', 'SUM_YR_2'] explore_data = air_data[explore_cols].describe().T explore_data.to_excel('explore.xls')

参数说明:describe() 只统计数值列,FFP_DATE 是 datetime 类型会被跳过,想看日期分布需要单独再画直方图。转置后的表每行一个字段,列依次是 count、mean、std、min、25%、50%、75%、max。对比 mean 和 50% 就能看出长尾程度:SEG_KM_SUM 的均值如果远大于中位数,说明少数高频会员贡献了大部分里程,这类分布直接喂给 K-means 会让普通会员被挤成一团,所以标准化环节不能省略。

提示:explore.xls 和数据清洗文件是两条线的产物。不要把 explore.xls 当成清洗后的数据用,它只是中间检查点,真正的建模输入是后面生成的标准文件。

2.3 核心字段的数据质量检查表

在写清洗代码之前,我习惯先画一张字段处理决策表,把哪些字段留、哪些删、哪些要特殊处理写清楚,避免在代码里随手 dropna:

字段业务含义常见问题处理策略
FFP_DATE入会日期部分缺失转 datetime,缺失则剔除
LAST_TO_END最近乘机距今天数少量缺失中位数回填
FLIGHT_COUNT乘机次数出现 0 值保留,0 是有效业务值
SEG_KM_SUM飞行总里程缺失、为 0缺失剔除,0 保留
avg_discount平均折扣系数为 0 表示免票过滤为 0 的行
SUM_YR_1/SUM_YR_2两年累计票价同时为空直接剔除

代码层面把这几个规则合并成布尔索引:

filter_rule = ( air_data['SEG_KM_SUM'].notnull() & air_data['SUM_YR_1'].notnull() & air_data['avg_discount'].notnull() & (air_data['avg_discount'] != 0) ) raw_valid = air_data[filter_rule] print(raw_valid.shape)

代码说明:filter_rule 用括号包住每个条件再执行与运算,pandas 里不加括号会抛“DataFrame 真值不明确”的异常。notnull 判断先于数值判断执行,避免 NaN 参与比较产生意外结果。raw_valid 是初步过滤后的数据,后面清洗还会做第二次取舍。这张表也是交付给同事时最好的注释,比代码开头的 docstring 直观得多。

3. 从 data_cleaned 到 zscoreddata:清洗与标准化链路

3.1 两个 data_cleaned 文件的分工

压缩包里同时有 data_cleaned.csv 和 data_cleaned1.csv,名字很接近但不是重复文件。前者是清洗后的原始字段快照,后者是构造完 LRFMC 特征后的样本,专门用来做标准化和聚类。四份数据的分工可以这样看:

文件内容下游用途
data_cleaned.csv过滤异常后的全部字段特征工程输入
data_cleaned1.csv清洗后加 LRFMC 新列标准化的输入
zscoreddata.xls五维标准化结果K-means 实际输入
explore.xls描述统计与分布探索性分析报告

清洗代码按函数组织,便于换数据源时复用:

def clean_airline_data(df): df = df.copy() df = df[df['SUM_YR_1'].notnull()] df = df[df['SUM_YR_1'] != 0] df = df[df['avg_discount'] != 0] return df data_cleaned = clean_airline_data(air_data) data_cleaned.to_csv('data_cleaned.csv', index=False, encoding='utf-8-sig')

逻辑说明:函数内先 copy,避免 pandas 的 SettingWithCopyWarning 污染外部变量。SUM_YR_1 先判非空再判非 0,空值和 0 值是两个不同的脏数据类型:空值说明记录不完整,0 值可能是免票或未出票。avg_discount 为 0 的免票记录虽然真实存在,但放进聚类会把折扣维度拖出一整个异常簇,分析价值很低。

参数说明:to_csv 的 encoding='utf-8-sig' 是给 Excel 用户准备的。不带 BOM 的 UTF-8 在 Excel 里打开,中文列名会乱码,加上 sig 后 Excel 能正确识别。如果你平时用 pandas 处理完数据直接丢给别人,这个参数值得养成习惯。

3.2 StandardScaler 为什么在这里是标配

航空客户价值分析的五个维度量纲差异非常大:L 是几百到几千天,F 是个位数到几百次,M 是几千到几十万公里,C 是 0 到 1.5 的小数。K-means 基于欧氏距离,量纲大的维度会主导距离计算,不标准化的结果就是聚类主要由 L 和 M 决定,C 和 R 几乎不起作用。

feature_cols = ['L', 'R', 'F', 'M', 'C'] scaler = StandardScaler() zscored_array = scaler.fit_transform(data_cleaned1[feature_cols]) zscored_df = pd.DataFrame(zscored_array, columns=feature_cols) zscored_df.to_excel('zscoreddata.xls', index=False)

逻辑说明:StandardScaler 对每个维度独立计算均值和标准差,输出 (x - mean) / std,使五个维度都落在均值 0、标准差 1 的统一尺度上。zscoreddata.xls 里每列的中心都在 0 附近,K-means 的欧氏距离不再被某个字段的绝对值带偏。

参数说明:fit_transform 在同一份数据上同时学习参数并转换,只能用于训练阶段。如果后续有新客户数据要做预测,必须用同一个 scaler 的 transform 方法,不能重新 fit_transform,否则新数据的均值和标准差变了,老模型和新数据不在同一空间。我一般会把 scaler 用 joblib 保存到 tmp 目录,下次直接加载。

3.3 标准化前先做断言,行数和缺失值都要查

进入 K-means 之前,标准化这块最好做两道门卫检查,避免聚类跑出一个莫名其妙的结果、debug 半天才发现是前面某一步丢了数据:

assert zscored_df.isnull().sum().sum() == 0, 'zscored data contains NaN' assert zscored_df.shape[0] == data_cleaned1.shape[0], 'row count changed after scaling'

代码说明:assert 在条件为假时抛 AssertionError 并打印后面的字符串。第一行把整个 DataFrame 的缺失值加起来判断是否为 0,第二个 assert 判断标准化过程有没有改变行数。这两道门卫平时是沉默的,一旦触发就知道问题出在清洗和标准化之间,而不是聚类参数本身。

import joblib joblib.dump(scaler, 'tmp/scaler.pkl')

顺便说一句,tmp 目录在源码里不是临时垃圾区。这里通常会放 scaler.pkl、聚类模型或 np.save 出来的簇中心,目的是让预测脚本不用重新训练。只要数据分布没有大偏移,线上打分直接用这一套保存物就够了。

4. LRFMC 特征构造与 K-means 聚类实现

4.1 五维特征里最容易被算错的 L 和 R

LRFMC 特征从原始字段构造的核心就是把日期转成天数差。

data_cleaned['FFP_DATE'] = pd.to_datetime(data_cleaned['FFP_DATE']) anchor_date = data_cleaned['FFP_DATE'].max() + pd.Timedelta(days=1) data_cleaned['L'] = (anchor_date - data_cleaned['FFP_DATE']).dt.days data_cleaned['R'] = data_cleaned['LAST_TO_END'] data_cleaned['F'] = data_cleaned['FLIGHT_COUNT'] data_cleaned['M'] = data_cleaned['SEG_KM_SUM'] data_cleaned['C'] = data_cleaned['avg_discount'] data_cleaned1 = data_cleaned[['FFP_DATE', 'L', 'R', 'F', 'M', 'C']].copy() data_cleaned1.to_csv('data_cleaned1.csv', index=False, encoding='utf-8-sig')

代码说明:anchor_date 取数据集中最大入会日期的下一天,这样所有会员的 L 最小是 1,不会出现 0 天会员。之所以不用系统当前日期做锚点,是因为同一份 CSV 在 3 月跑和 10 月跑,L 会差出七个月,分析结果不可复现。R 直接取 LAST_TO_END,这个字段在源表里已经是“最近一次乘机距今的天数”,不需要再转换。F 和 M 看着简单,但要注意 FLIGHT_COUNT 为 0 的会员是否应该保留——如果一张票都没飞但里程非 0,多半是里程兑换活动,这类样本在聚类里会成为噪声。

参数说明:pd.to_datetime 要放在构造 L 之前,否则字符串相减直接报 TypeError。copy() 是为了避免 data_cleaned1 和 data_cleaned 共享同一块内存,后面给 data_cleaned1 加 cluster 列时不会污染前面的表。

4.2 用肘部法则配合 n_init 选 K 值

K-means 的 K 值选择在航空客户场景里通常落在 4 到 6 之间。代码上先把肘部曲线画出来:

from sklearn.cluster import KMeans import matplotlib.pyplot as plt sse = [] for k in range(2, 9): km = KMeans(n_clusters=k, n_init=10, max_iter=300, random_state=42) km.fit(zscored_df) sse.append(km.inertia_) plt.plot(range(2, 9), sse, marker='o') plt.xlabel('k') plt.ylabel('SSE') plt.savefig('elbow.png', dpi=120)

逻辑说明:inertia_ 是簇内误差平方和,K 越大该值必然越小,但下降速度会变缓,曲线出现肘形拐点的位置就是收益递减的分界。航空客户价值分析一般选 K=5,对应高价值核心、高潜成长、普通休闲、沉睡新客、流失风险五类。如果业务要求更细,可以试 K=6,但超过 7 个簇之后群体边界很难向管理层解释。样本量小的时候也可以试层次聚类的 dendrogram 做交叉判断,但几万条会员记录下 linkage 矩阵的内存压力太大,工程上还是 K-means 更实用。

参数说明:n_init=10 指定 K-means 从 10 组不同初始中心出发,选择 inertia 最小的结果作为最终模型。sklearn 1.4 之后 n_init 默认值从 10 改成了 auto,不显式指定的话,不同 scikit-learn 版本跑出来的结果可能在细节上对不上。random_state=42 固定随机种子,保证每次复现结果一致。max_iter=300 是单个初始中心的迭代上限,几万条样本一般几十轮就收敛,如果出现不收敛警告再调大。

K 值也可以用轮廓系数做交叉验证:

from sklearn.metrics import silhouette_score score = silhouette_score(zscored_df, km.labels_) print('silhouette score:', score)

轮廓系数大于 0.5 表示聚类结构明显,0.3 到 0.5 之间属于可接受范围。航空公司会员数据长尾严重,一般不会出现特别高的分值,0.35 左右就可以进入业务解读,不要为了凑一个高分把用户群拆成碎片。

4.3 聚类画像表与业务命名

聚类跑完,标签本身没有任何业务含义,必须按维度均值给每个簇做画像。

data_cleaned1['cluster'] = km.labels_ profile = data_cleaned1.groupby('cluster')[['L', 'R', 'F', 'M', 'C']].mean().round(2) profile.to_excel('cluster_profile.xls')

groupby 按 cluster 分组求五维均值,输出的 5 行 5 列表就是分群画像。解读时要记住四个方向:R 越小表示最近刚飞过,活跃度高;L 长表示入会久;F 高表示频率高;M 大表示里程贡献大;C 高表示常买全价票,价格不敏感。

常见的画像分布可以参考下表,但数字一定以你自己跑出来的数据为准:

clusterLRFMC业务命名
0高价值核心客群
1普通经济舱客群
2新入会沉默客群
3历史高频流失风险
4里程多但折扣低客群

第 4 簇是最典型的“里程大户但贡献不匹配”群体,飞得多、里程大,但平均折扣系数低,说明大量购买特价舱位。这类客户适合用里程促销刺激消费频次,不适合给最高等级会员权益。给运营同事输出画像时,用业务命名而不是簇编号,因为 cluster 编号每次重跑都可能改变,业务名却是稳定的。

5. 模型上线前要补上的落盘与稳定性校验

5.1 把标准化参数和聚类模型一起持久化

线下跑通聚类只是第一步,真正要每月给运营输出客户群,需要把训练好的标准化容器和聚类模型保存下来,而不是每次都重新训练。

import joblib joblib.dump(scaler, 'tmp/scaler.pkl') joblib.dump(km, 'tmp/km.pkl') new_z = scaler.transform(new_data[feature_cols]) new_cluster = km.predict(new_z)

predict 返回每个新样本最近的簇中心索引。注意 new_data 的字段顺序必须和 feature_cols 一致,这是容易踩的隐性坑——列名相同但顺序不同,predict 的结果会完全错位。建议在预测脚本里对新数据做一次 reindex,确保顺序一致后再进模型。

5.2 用簇占比波动检查聚类稳定性

每次重新聚类后,检查每个簇的样本占比。这个占比直接对应运营预算分配比例,如果某次跑出来高价值客群从 12% 跳到了 25%,大概率不是客户突然变优质了,而是数据清洗条件发生了变化。

cluster_share = data_cleaned1['cluster'].value_counts(normalize=True).sort_index() print(cluster_share.round(4))

value_counts(normalize=True) 输出每个簇的样本占比,sort_index 保证显示顺序按簇编号排列而不是按占比降序。对比历史两次运行的占比,如果某个簇偏移超过两个百分点,优先检查 SUM_YR_1、avg_discount 的过滤条件有没有变动,再看是否新增了采购渠道或其他外部数据源。

最终落到运营侧的动作,可以参照这个映射:

客户群运营动作
高价值核心客群专属客户经理、优先升舱、会员等级保护
普通经济舱客群维持现状,不做高成本营销
新入会沉默客群首飞券、会员权益提醒、唤醒邮件
历史高频流失风险里程到期提醒、保级进度提示
里程多但折扣低客群定向里程促销,控制成本型权益

输出这张表时,把 cluster_profile.xls 里的均值画像一起带上,不要只发簇编号。运营侧看到的是“高价值核心客群 R 均值 15 天、F 均值 24 次”,才知道这批人最近一次乘机是半个月前,决策才有依据。每次新一期数据跑完后,把 cluster_share 和 cluster_profile 与上一期对比,任一簇占比变动超过两个百分点时,先检查 scaler.pkl 和清洗条件是否与上期一致,再谈业务变化。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询