一、前言
在电信、互联网订阅等行业,获取一个新客户的成本往往远高于留住一个老客户。因此,客户流失预测成为了运营团队的核心课题之一------如果能提前识别出高流失风险的客户,就可以通过定向优惠、专属客服等手段进行干预,有效降低流失率。
本文将基于经典的 Telco Customer Churn 数据集,从零到一完整走一遍客户流失预测的建模流程:
- 如何对包含大量类别特征的电信数据做清洗与特征工程
- 为什么 TotalCharges 字段需要特殊处理,以及如何构造衍生特征
- 使用 RFE 递归特征消除筛选关键特征
- 逻辑回归、随机森林、XGBoost 四种模型的横向对比
- PR 曲线与阈值调优在不平衡分类中的应用
- SMOTE 过采样与网格搜索的实战效果
- 如何将模型输出转化为业务可用的"客户风险分层"和线上推理函数
数据集包含 7043 条客户记录,流失客户占比约 26.54%,属于轻度不平衡的二分类问题。
二、数据探索:先搞清楚数据长什么样
2.1 导入数据与基本信息
python
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, recall_score, precision_score, f1_score, roc_auc_score, auc, precision_recall_curve
from sklearn.ensemble import RandomForestClassifier
from xgboost import XGBClassifier
from sklearn.feature_selection import RFE
from sklearn.model_selection import GridSearchCV
from imblearn.over_sampling import SMOTE
# 设置 pandas 显示优化
pd.set_option('display.max_columns', None)
pd.set_option('display.width', 2000)
pd.set_option('display.max_colwidth', 35)
# 设置显示中文
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 读取数据
df = pd.read_csv("5.Telco-Customer-Churn.csv")
eda_df = df.copy()
# 查看数据基本信息
print(f"数据形状:{df.shape}")
print(f"\n前五行数据:\n{df.head()}")
print(f"\n数据类型与缺失值情况:")
print(df.info())
print(df.describe())
txt
数据形状:(7043, 21)
前五行数据:
customerID gender SeniorCitizen Partner Dependents tenure PhoneService MultipleLines InternetService OnlineSecurity OnlineBackup DeviceProtection TechSupport StreamingTV StreamingMovies Contract PaperlessBilling PaymentMethod MonthlyCharges TotalCharges Churn
0 7590-VHVEG Female 0 Yes No 1 No No phone service DSL No Yes No No No No Month-to-month Yes Electronic check 29.85 29.85 No
1 5575-GNVDE Male 0 No No 34 Yes No DSL Yes No Yes No No No One year No Mailed check 56.95 1889.5 No
2 3668-QPYBK Male 0 No No 2 Yes No DSL Yes Yes No No No No Month-to-month Yes Mailed check 53.85 108.15 Yes
3 7795-CFOCW Male 0 No No 45 No No phone service DSL Yes No Yes Yes No No One year No Bank transfer (automatic) 42.30 1840.75 No
4 9237-HQITU Female 0 No No 2 Yes No Fiber optic No No No No No No Month-to-month Yes Electronic check 70.70 151.65 Yes
数据类型与缺失值情况:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 21 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 customerID 7043 non-null object
1 gender 7043 non-null object
2 SeniorCitizen 7043 non-null int64
3 Partner 7043 non-null object
4 Dependents 7043 non-null object
5 tenure 7043 non-null int64
6 PhoneService 7043 non-null object
7 MultipleLines 7043 non-null object
8 InternetService 7043 non-null object
9 OnlineSecurity 7043 non-null object
10 OnlineBackup 7043 non-null object
11 DeviceProtection 7043 non-null object
12 TechSupport 7043 non-null object
13 StreamingTV 7043 non-null object
14 StreamingMovies 7043 non-null object
15 Contract 7043 non-null object
16 PaperlessBilling 7043 non-null object
17 PaymentMethod 7043 non-null object
18 MonthlyCharges 7043 non-null float64
19 TotalCharges 7043 non-null object
20 Churn 7043 non-null object
dtypes: float64(1), int64(2), object(18)
memory usage: 1.1+ MB
None
SeniorCitizen tenure MonthlyCharges
count 7043.000000 7043.000000 7043.000000
mean 0.162147 32.371149 64.761692
std 0.368612 24.559481 30.090047
min 0.000000 0.000000 18.250000
25% 0.000000 9.000000 35.500000
50% 0.000000 29.000000 70.350000
75% 0.000000 55.000000 89.850000
max 1.000000 72.000000 118.750000
2.2 字段说明
数据共有 7043 行、21 列,没有显式缺失值。各字段含义如下:
| 字段名 | 中文翻译 | 说明 |
|---|---|---|
| customerID | 客户编号 | 唯一用户 ID |
| gender | 性别 | |
| SeniorCitizen | 是否老年用户 | 1 = 老年人,0 = 非老年 |
| Partner | 是否有伴侣 | |
| Dependents | 是否有家属/受抚养人 | 子女等家属 |
| tenure | 在网时长 | 客户使用服务月数 |
| PhoneService | 是否开通电话业务 | |
| MultipleLines | 是否多条电话线路 | |
| InternetService | 互联网服务类型 | 无 / 光纤 / DSL |
| OnlineSecurity | 在线安全服务 | 网络安全增值业务 |
| OnlineBackup | 在线备份服务 | |
| DeviceProtection | 设备保护服务 | |
| TechSupport | 技术支持服务 | |
| StreamingTV | 流媒体电视服务 | 网络电视 |
| StreamingMovies | 流媒体电影服务 | |
| Contract | 合约类型 | 月付 / 一年合约 / 两年合约 |
| PaperlessBilling | 是否无纸化账单 | |
| PaymentMethod | 支付方式 | |
| MonthlyCharges | 月费用 | 每月消费金额 |
| TotalCharges | 总费用 | 累计消费总额 |
| Churn | 是否流失 | Yes = 客户销户流失,No = 留存 |
注意:
TotalCharges的类型是object而不是数值型,这是一个需要处理的坑,后面特征工程阶段会专门处理。
2.3 标签分布
python
# 统计标签分布
label_cnt = df["Churn"].value_counts()
print("类别分布:")
print(label_cnt)
print(f"留存占比:{label_cnt['No'] / len(df) * 100:.2f}%")
print(f"流失占比:{label_cnt['Yes'] / len(df) * 100:.2f}%")
txt
类别分布:
Churn
No 5174
Yes 1869
Name: count, dtype: int64
留存占比:73.46%
流失占比:26.54%
流失客户占比 26.54%,属于轻度不平衡。虽然不像信用卡欺诈那样极端(0.17%),但准确率依然不能作为唯一评价指标,需要同时关注召回率、精确率和 F1。
2.4 EDA:无纸化账单对流失率的影响
在正式建模前,先做一个简单的探索性分析。主观上觉得"是否无纸化账单"对流失影响不大,但数据说话更可靠。
python
""" 看看账单类型的影响 """
''' 增加这一步是因为我主观觉得影响不大,但是又不确定 '''
eda_df["Churn_num"] = eda_df["Churn"].map({"Yes": 1, "No": 0})
plt.figure(figsize=(6, 4))
sns.barplot(x="PaperlessBilling", y="Churn_num", data=eda_df)
plt.title("有无无纸化账单-客户流失率")
plt.ylabel("平均流失概率")
plt.show()
# 数值统计
print(eda_df.groupby("PaperlessBilling")["Churn_num"].mean())

txt
PaperlessBilling
No 0.163301
Yes 0.335651
Name: Churn_num, dtype: float64
结果出乎意料:使用无纸化账单的客户流失率(33.57%)几乎是不使用者(16.33%)的两倍。这个特征在后续建模中被保留了下来。
三、特征工程与数据划分
3.1 完整特征处理流程
这一步包含多个操作:丢弃无用特征、修复 TotalCharges、构造衍生特征、独热编码、标签映射、分层划分、标准化。
python
# 数据集划分 + 特征工程
""" 凭借个人经验, 初步判断出没用的特征: customerID, 将它丢弃 """
drop_cols = ["customerID"]
df = df.drop(columns=drop_cols)
""" 处理 TotalCharges """
df["TotalCharges"] = df["TotalCharges"].str.strip()
df["TotalCharges"] = df["TotalCharges"].replace("", "0")
df["TotalCharges"] = pd.to_numeric(df["TotalCharges"], errors="coerce")
""" 新增几个特征 """
df["AvgMonthlyCharge"] = df["TotalCharges"].astype(float) / (df["tenure"] + 1) # 1. 月均消费:总消费 / 在网月数 (+1 防止除以 0)
df["ShortMonthRisk"] = ((df["Contract"] == "Month-to-month") & (df["tenure"] <= 12)).astype(int) # 2. 短期月付高风险:月付合约 + 在网 ≤12 个月
df["FiberShortRisk"] = ((df["InternetService"] == "Fiber optic") & (df["tenure"] <= 12)).astype(int) # 3. 光纤短期高风险:光纤业务 + 在网 ≤12 个月
""" 因为大部分特征的类型都是 object, 所以需要进行特征编码 """
df = pd.get_dummies(df, columns=[
"gender", "Partner", "Dependents", "PhoneService", "MultipleLines", "InternetService", "OnlineSecurity", "PaperlessBilling",
"OnlineBackup", "DeviceProtection", "TechSupport", "StreamingTV", "StreamingMovies", "Contract", "PaymentMethod"
],
drop_first=True
)
""" 将 Churn 从 Yes, No ==> 1, 0 """
df["Churn"] = df["Churn"].map({"Yes": 1, "No": 0})
""" 划分数据集 """
X = df.drop("Churn", axis=1)
y = df["Churn"]
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42
)
print(f"训练集大小:{X_train.shape}")
print(f"测试集大小:{X_test.shape}")
print(f"\n训练集标签分布:\n{y_train.value_counts()}")
print(f"\n测试集标签分布:\n{y_test.value_counts()}")
""" 标准化数值特征 """
scaler = StandardScaler()
num_cols = ["tenure", "MonthlyCharges", "TotalCharges", "AvgMonthlyCharge"]
X_train[num_cols] = scaler.fit_transform(X_train[num_cols])
X_test[num_cols] = scaler.transform(X_test[num_cols])
txt
训练集大小:(5634, 33)
测试集大小:(1409, 33)
训练集标签分布:
Churn
0 4139
1 1495
Name: count, dtype: int64
测试集标签分布:
Churn
0 1035
1 374
Name: count, dtype: int64
几个关键处理点说明:
TotalCharges处理:原字段是字符串类型,且存在空字符串(新客户 tenure=0 时总费用为空),需要先 strip 再替换为空值为 0,最后转数值型。- 衍生特征 :构造了
AvgMonthlyCharge(月均消费)、ShortMonthRisk(短期月付风险标记)、FiberShortRisk(光纤短期风险标记)三个业务特征。 - 独热编码 :15 个类别特征全部
get_dummies,drop_first=True避免多重共线性。 - 分层抽样 :
stratify=y保证训练集和测试集流失占比一致。 - 标准化:只对 4 个数值型特征做标准化,独热编码后的 0/1 特征不需要处理。
四、RFE 递归特征消除
独热编码后特征维度达到 33 维,其中可能存在冗余或弱特征。使用 RFE(Recursive Feature Elimination)以逻辑回归为基学习器,递归剔除弱特征,保留 20 个最优特征。
python
# 基学习器
estimator = LogisticRegression(class_weight="balanced", max_iter=300)
# 保留 20 个最优特征,自动递归剔除弱特征
rfe = RFE(estimator, n_features_to_select=20, step=1)
X_train = rfe.fit_transform(X_train, y_train)
X_test = rfe.transform(X_test)
# 输出保留/剔除的特征
keep_fea = X.columns[rfe.support_]
drop_fea = X.columns[~rfe.support_]
print("RFE保留特征: \n", list(keep_fea))
print("RFE剔除特征: \n", list(drop_fea))
print(f"X_train: {X_train.shape}")
print(f"X_test: {X_test.shape}")
txt
RFE保留特征:
['tenure', 'TotalCharges', 'AvgMonthlyCharge', 'ShortMonthRisk', 'PhoneService_Yes', 'MultipleLines_No phone service', 'MultipleLines_Yes', 'InternetService_Fiber optic', 'OnlineSecurity_Yes', 'PaperlessBilling_Yes', 'OnlineBackup_No internet service', 'DeviceProtection_No internet service', 'TechSupport_No internet service', 'StreamingTV_No internet service', 'StreamingTV_Yes', 'StreamingMovies_No internet service', 'StreamingMovies_Yes', 'Contract_One year', 'Contract_Two year', 'PaymentMethod_Electronic check']
RFE剔除特征:
['SeniorCitizen', 'MonthlyCharges', 'FiberShortRisk', 'gender_Male', 'Partner_Yes', 'Dependents_Yes', 'InternetService_No', 'OnlineSecurity_No internet service', 'OnlineBackup_Yes', 'DeviceProtection_Yes', 'TechSupport_Yes', 'PaymentMethod_Credit card (automatic)', 'PaymentMethod_Mailed check']
X_train: (5634, 20)
X_test: (1409, 20)
可以看到,RFE 保留了在网时长、总费用、月均消费、合约类型、光纤服务、电子支票支付等关键特征,剔除了性别、是否有伴侣、老年用户等对流失预测贡献较小的特征。
五、多模型横向对比
5.1 统一评估函数
先封装一个通用的模型评估函数,避免重复代码。
python
# 统一模型评估函数
def eval_model(model, X_tr, X_te, y_tr, y_te, model_name):
model.fit(X_tr, y_tr)
y_pred = model.predict(X_te)
y_proba = model.predict_proba(X_te)[:, 1]
res_dict = {
"模型名称": model_name,
"Accuracy": round(accuracy_score(y_te, y_pred), 4),
"Precision": round(precision_score(y_te, y_pred), 4),
"Recall": round(recall_score(y_te, y_pred), 4),
"F1": round(f1_score(y_te, y_pred), 4),
"AUC": round(roc_auc_score(y_te, y_proba), 4)
}
return res_dict, y_proba
5.2 四个模型训练与指标汇总
同时训练基础逻辑回归、平衡逻辑回归、平衡随机森林和 XGBoost 四个模型。XGBoost 使用 scale_pos_weight 处理类别不平衡。
python
# 1. 基础逻辑回归
logicR = LogisticRegression(max_iter=300, random_state=42)
res_lr_base, proba_lr = eval_model(logicR, X_train, X_test, y_train, y_test, "基础逻辑回归")
# 2. 平衡逻辑回归
logicR_balanced = LogisticRegression(max_iter=300, random_state=42, class_weight="balanced")
res_lr_bal, proba_lr_bal = eval_model(logicR_balanced, X_train, X_test, y_train, y_test, "平衡逻辑回归")
# 3. 平衡随机森林
rf = RandomForestClassifier(n_estimators=300, class_weight="balanced", random_state=42)
res_rf, proba_rf = eval_model(rf, X_train, X_test, y_train, y_test, "平衡随机森林")
# 4. XGBoost
neg, pos = (y_train == 0).sum(), (y_train == 1).sum()
scale_pos_weight = neg / pos
xgb = XGBClassifier(
n_estimators=300,
max_depth=6,
learning_rate=0.1,
subsample=0.8,
colsample_bytree=0.8,
scale_pos_weight=scale_pos_weight,
random_state=42,
eval_metric="logloss"
)
res_xgb, proba_xgb = eval_model(xgb, X_train, X_test, y_train, y_test, "XGBoost")
# 汇总所有模型指标
all_res = [res_lr_base, res_lr_bal, res_rf, res_xgb]
print("================== 全模型指标汇总对比表 =================")
pd.DataFrame(all_res)
================== 全模型指标汇总对比表 =================
| 模型名称 | Accuracy | Precision | Recall | F1 | AUC |
|---|---|---|---|---|---|
| 0 | 基础逻辑回归 | 0.7991 | 0.6574 | 0.5080 | 0.5732 |
| 1 | 平衡逻辑回归 | 0.7395 | 0.5060 | 0.7861 | 0.6157 |
| 2 | 平衡随机森林 | 0.7566 | 0.5348 | 0.6364 | 0.5812 |
| 3 | XGBoost | 0.7601 | 0.5370 | 0.6979 | 0.6070 |
初步观察:
- 基础逻辑回归:精确率最高(65.74%),但召回率最低(50.80%),很多流失客户没被识别出来。
- 平衡逻辑回归:召回率飙升到 78.61%,F1 最高(61.57%),AUC 也最高(0.8453),是当前综合表现最好的模型。
- XGBoost:召回率 69.79%,F1 60.70%,表现中规中矩。
- 随机森林:各项指标都不突出,AUC 最低。
5.3 PR 曲线对比
画出四个模型的 PR 曲线,直观对比在不同召回率下的精确率表现。
python
all_precisions = []
all_recalls = []
all_thresholds = []
all_pr_aucs = []
def f(proba):
precision, recall, thresholds = precision_recall_curve(y_test, proba)
pr_auc = auc(recall, precision)
all_precisions.append(precision)
all_recalls.append(recall)
all_thresholds.append(thresholds)
all_pr_aucs.append(pr_auc)
probas = [proba_lr, proba_lr_bal, proba_rf, proba_xgb]
for proba in probas:
f(proba)
model_names = ["基础逻辑回归", "平衡逻辑回归", "平衡随机森林", "XGBoost"]
colors = ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728"]
plt.figure(figsize=(8, 6))
for i in range(len(model_names)):
plt.plot(
all_recalls[i], # x 轴
all_precisions[i], # y 轴
color=colors[i],
linewidth=2,
label=f"{model_names[i]} (AUC={all_pr_aucs[i]:.4f})"
)
pos_ratio = y_test.mean()
plt.axhline(y=pos_ratio, color="gray", linestyle='--', linewidth=1.5, label=f"随机基线(Precision={pos_ratio:.4f})")
plt.title("四个模型的 PR 曲线对比", fontsize=14)
plt.xlabel("Recall", fontsize=12)
plt.ylabel("Precision", fontsize=12)
plt.legend(loc="lower left", fontsize=10)
plt.grid(True, linestyle="--", alpha=0.6)
plt.tight_layout()
plt.show()

5.4 阈值调优:寻找各模型最优 F1
默认阈值 0.5 不一定是最优的。遍历 0.05~0.95 之间的 50 个候选阈值,找到每个模型 F1 最高的工作点。
python
# 遍历阈值,寻找最优 F1
""" 封装函数 """
def find_best_threshold(proba, t_min=0.05, t_max=0.95, n_steps=50):
best_f1, best_threshold = 0.0, 0.0
for t in np.linspace(t_min, t_max, n_steps):
y_cut = (proba >= t).astype(int)
f1 = f1_score(y_test, y_cut, zero_division=0)
if f1 > best_f1:
best_f1, best_threshold = f1, t
return best_f1, best_threshold
best_results = []
for proba, model_name in zip(probas, model_names):
best_f1, best_threshold = find_best_threshold(proba)
best_results.append({
"模型名称": model_name,
"最优分类阈值": round(best_threshold, 4),
"最优 F1 分数": round(best_f1, 4)
})
df_best = pd.DataFrame(best_results)
print("各模型【最优F1分类阈值】对比")
df_best
txt
各模型【最优F1分类阈值】对比
模型名称 最优分类阈值 最优 F1 分数
0 基础逻辑回归 0.3255 0.6301
1 平衡逻辑回归 0.5643 0.6335
2 平衡随机森林 0.3255 0.6030
3 XGBoost 0.3990 0.6188
调优阈值后,平衡逻辑回归以 F1=0.6335 依然领先,基础逻辑回归通过降低阈值(0.3255)也追到了 0.6301。
六、SMOTE 过采样实验
尝试另一种不平衡处理方案:SMOTE 过采样。在训练集上合成新的流失样本,使正负样本 1:1 平衡,测试集保持不变。
python
smote = SMOTE(random_state=42)
X_train_smote, y_train_smote = smote.fit_resample(X_train, y_train)
print("SMOTE 重采样后训练集分布: ")
print(pd.Series(y_train_smote).value_counts())
""" 逻辑回归 + SMOTE """
logicR_smote = LogisticRegression(
max_iter=300,
random_state=42
)
res_logicR_smote, _ = eval_model(
logicR_smote,
X_train_smote,
X_test,
y_train_smote,
y_test,
"LR + SMOTE 采样"
)
""" XGB + SMOTE """
xgb_smote = XGBClassifier(
n_estimators=300,
max_depth=6,
random_state=42,
eval_metric="logloss"
)
res_xgb_smote, _ = eval_model(
xgb_smote,
X_train_smote,
X_test,
y_train_smote,
y_test,
"XGB + SMOTE 采样"
)
all_res.extend([res_logicR_smote, res_xgb_smote])
print("\n================== 新增SMOTE后的完整指标汇总 ==================")
pd.DataFrame(all_res)
txt
SMOTE 重采样后训练集分布:
Churn
0 4139
1 4139
Name: count, dtype: int64
================== 新增SMOTE后的完整指标汇总 ==================
| 模型名称 | Accuracy | Precision | Recall | F1 | AUC |
|---|---|---|---|---|---|
| 0 | 基础逻辑回归 | 0.7991 | 0.6574 | 0.5080 | 0.5732 |
| 1 | 平衡逻辑回归 | 0.7395 | 0.5060 | 0.7861 | 0.6157 |
| 2 | 平衡随机森林 | 0.7566 | 0.5348 | 0.6364 | 0.5812 |
| 3 | XGBoost | 0.7601 | 0.5370 | 0.6979 | 0.6070 |
| 4 | LR + SMOTE 采样 | 0.7367 | 0.5026 | 0.7888 | 0.6139 |
| 5 | XGB + SMOTE 采样 | 0.7708 | 0.5627 | 0.6123 | 0.5864 |
结果分析:
- LR + SMOTE :效果和
class_weight="balanced"几乎一致(F1 0.6139 vs 0.6157),召回率甚至略高一点(78.88%),说明在这个数据集上,加权损失和过采样的效果相当。 - XGB + SMOTE:相比不加采样的 XGBoost,精确率提升了(56.27% vs 53.70%),但召回率下降了(61.23% vs 69.79%),F1 反而略低。
综合来看,class_weight="balanced" 的逻辑回归仍然是性价比最高的选择。
七、网格搜索调优
对表现最好的平衡逻辑回归做网格搜索,调优正则化强度参数 C。
python
# 网格搜索
param_grid_logicR = {
"C": [0.01, 0.05, 0.1, 0.5, 1, 5, 10, 20, 50, 100] # C 是正则化强度的倒数,C 越大正则化越弱。
}
grid = GridSearchCV(
estimator=LogisticRegression(class_weight="balanced", max_iter=500, random_state=42),
param_grid=param_grid_logicR,
cv=5,
scoring="f1",
n_jobs=-1
)
grid.fit(X_train, y_train)
print(f"平衡逻辑回归网格搜索最优参数: {grid.best_params_}")
print(f"5 折交叉验证最优 F1: {grid.best_score_:.4f}")
best_lr = grid.best_estimator_
res_best_lr, best_proba = eval_model(best_lr, X_train, X_test, y_train, y_test, "网格调优平衡逻辑回归")
pd.DataFrame([res_best_lr])
txt
平衡逻辑回归网格搜索最优参数: {'C': 20}
5 折交叉验证最优 F1: 0.6305
模型名称 Accuracy Precision Recall F1 AUC
0 网格调优平衡逻辑回归 0.7381 0.5043 0.7834 0.6136 0.8452
最优参数 C=20(较弱的正则化),测试集 F1 为 0.6136,和默认参数相比变化不大,说明逻辑回归在这个数据集上对正则化强度不敏感,模型已经比较稳定。
八、客户风险分层:把模型输出变成业务语言
模型输出的 0~1 概率对业务人员不够直观。将流失概率划分为高、中、低三个风险等级,方便运营团队制定差异化的挽留策略。
python
risk_df = pd.DataFrame({
"流失概率": best_proba, # 模型预测的每个客户的流失概率
"真实标签": y_test.values # 测试集的真实流失标签
})
# 自定义三层风险划分
def risk_tag(p):
if p >= 0.7:
return "高风险"
elif p >= 0.3:
return "中风险"
else:
return "低风险"
risk_df["风险等级"] = risk_df["流失概率"].apply(risk_tag)
# 分层统计数量 & 真实流失率
stat = risk_df.groupby("风险等级").agg(
客户数量=("流失概率", "count"),
实际流失占比=("真实标签", "mean")
).round(4)
risk_order = ["高风险", "中风险", "低风险"]
stat = stat.reindex(risk_order)
print("客户风险分层统计:\n", stat)
txt
客户风险分层统计:
客户数量 实际流失占比
风险等级
高风险 347 0.6254
中风险 465 0.2839
低风险 597 0.0419
分层效果解读:
| 风险等级 | 客户数量 | 实际流失占比 | 业务建议 |
|---|---|---|---|
| 高风险(≥0.7) | 347 | 62.54% | 重点挽留,人工外呼 + 大额优惠 |
| 中风险(0.3~0.7) | 465 | 28.39% | 自动化触达,短信/APP 推送优惠券 |
| 低风险(<0.3) | 597 | 4.19% | 维持现状,常规运营即可 |
高风险群体的实际流失率是低风险群体的近 15 倍,分层效果非常显著。
九、线上推理函数
最后,将整个预测流程封装成一个可直接调用的函数,输入原始客户信息,输出流失概率和风险等级。
python
def predict_churn(raw_customer_info):
"""
电信客户流失线上推理函数
参数:
raw_customer_info: dict, 原始客户字段
返回:
churn_prob: float 客户流失概率(0~1)
risk_level: str 流失风险等级
"""
# 1. 转为单行 DataFrame
full_cols = list(X.columns)
input_df = pd.DataFrame([raw_customer_info], columns=full_cols)
# 2. 统一处理数值字段标准化
num_cols = ["tenure", "MonthlyCharges", "TotalCharges", "AvgMonthlyCharge"]
input_df[num_cols] = scaler.transform(input_df[num_cols])
# 3. RFE 特征筛选,转为保留的 20 维特征
input_rfe = rfe.transform(input_df)
# 4. 使用网格调优平衡逻辑回归预测流失概率
churn_prob = (best_lr.predict_proba(input_rfe)[0, 1])
churn_prob = round(churn_prob, 4)
# 5. 根据概率划分风险
risk_level = risk_tag(churn_prob)
return churn_prob, risk_level
模拟一条客户数据进行测试:
python
# 模拟一条高流失风险客户原始数据
test_customer = {
"tenure": 5,
"MonthlyCharges": 74.5,
"TotalCharges": 372.5,
"AvgMonthlyCharge": 74.5,
"ShortMonthRisk": 1,
"FiberShortRisk": 1,
"gender_Male": 1,
"SeniorCitizen": 0,
"Partner_Yes": 0,
"Dependents_Yes": 0,
"PhoneService_Yes": 1,
"MultipleLines_No phone service": 0,
"MultipleLines_Yes": 0,
"InternetService_Fiber optic": 1,
"InternetService_No": 0,
"OnlineSecurity_Yes": 0,
"OnlineSecurity_No internet service": 0,
"OnlineBackup_Yes": 0,
"OnlineBackup_No internet service": 1,
"DeviceProtection_Yes": 0,
"DeviceProtection_No internet service": 1,
"TechSupport_Yes": 0,
"TechSupport_No internet service": 1,
"StreamingTV_Yes": 1,
"StreamingTV_No internet service": 0,
"StreamingMovies_Yes": 1,
"StreamingMovies_No internet service": 0,
"Contract_One year": 0,
"Contract_Two year": 0,
"PaymentMethod_Electronic check": 1,
"PaymentMethod_Credit card (automatic)": 0,
"PaymentMethod_Mailed check": 0,
"PaperlessBilling_Yes": 1
}
# 推理调用
prob, risk = predict_churn(test_customer)
print(f"客户流失概率:{prob*100:.2f}% ({risk})")
txt
客户流失概率:81.76% (高风险)
这位在网仅 5 个月、月付合约、使用光纤和电子支票支付、开通无纸化账单的客户,模型预测流失概率 81.76%,判定为高风险------符合业务直觉。
十、总结
本次项目是我按照自己的思路做的,所以有很多不足之处。中间的四类模型真的要把我折磨死了,F1, Recall 等结果一直在徘徊,提不上去,希望看到的各位大佬能够支个招🫶
- 项目代码文件 :客户流失预测.ipynb
- 项目数据下载 :Telco-Customer-Churn.csv