关于AI监督学习的几点记录
他们有以下问题:
客户流失率约为 15%,在行业里偏高
每月流失的客户中,有 30% 本来可以通过挽留措施挽回
没有系统化的流失预警机制,基本上是客户通知要取消时才知道
销售团队时间和资源有限,需要优先关注高风险客户
预测准确性:能够准确识别未来一个月内可能流失的客户
可解释性:需要知道客户流失的主要原因,便于制定针对性措施
实用性:模型要能部署到生产环境,每月自动运行
及时性:提前一周给出预警,给销售团队足够的响应时间
为什么写这篇文章
最近在帮一个朋友做客户流失预测,他说:“我有两年的客户数据,想预测哪些客户下个月会流失。“我一开始觉得这不就是分类问题吗,用逻辑回归应该能搞定。结果一上手才发现,从数据到预测,中间有很多坑:数据质量、特征工程、模型选择、过拟合…
市面上讲监督学习的文章很多,但大部分都是教科书式的讲解,缺少实际的踩坑经验。这篇文章我想以客户流失预测为例,把监督学习从理论到实践的整个过程真实地写一遍,包括那些被坑的经历。
背景和需求
具体场景
朋友的公司是一家 SaaS 服务商,主要提供企业协作工具。他们有以下问题:
- 客户流失率约为 15%,在行业里偏高
- 每月流失的客户中,有 30% 本来可以通过挽留措施挽回
- 没有系统化的流失预警机制,基本上是客户通知要取消时才知道
- 销售团队时间和资源有限,需要优先关注高风险客户
核心需求
- 预测准确性:能够准确识别未来一个月内可能流失的客户
- 可解释性:需要知道客户流失的主要原因,便于制定针对性措施
- 实用性:模型要能部署到生产环境,每月自动运行
- 及时性:提前一周给出预警,给销售团队足够的响应时间
数据情况
现有的数据资源:
- 用户基本信息(注册时间、公司规模、行业等)
- 使用行为数据(登录频率、功能使用情况、活跃时长)
- 订阅信息(套餐类型、付款历史、续费次数)
- 交互记录(客服工单、反馈次数、产品迭代参与度)
- 过去 24 个月的流失标签
理论基础:监督学习是什么
核心概念
监督学习(Supervised Learning)是最常见的机器学习类型,简单说就是:用有标签的数据训练模型,然后对无标签的数据进行预测。
打个比方:就像教小孩认水果。你给他看苹果说"这是苹果”,看香蕉说"这是香蕉”(这就是训练数据,带标签)。然后给他一个新的水果,他能认出来是苹果还是香蕉(这就是预测)。
两种主要类型
监督学习主要分为两类:
1. 分类(Classification)
预测离散的类别标签。在我们的场景中:
- 目标变量:客户是否会流失(流失 / 不流失)
- 常见算法:逻辑回归、决策树、随机森林、XGBoost、支持向量机
- 评估指标:准确率、精确率、召回率、F1 分数、ROC-AUC
2. 回归(Regression)
预测连续的数值。在我们的场景中:
- 目标变量:客户续费金额 / 客户生命周期价值
- 常见算法:线性回归、岭回归、Lasso 回归、决策树回归、梯度提升回归
- 评估指标:均方误差(MSE)、均方根误差(RMSE)、R²
工作流程
监督学习的标准流程:
实现过程
第一步:数据收集和探索
首先我拿到了朋友的数据库访问权限,开始探索数据:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# 读取数据
customers = pd.read_csv('customer_data.csv')
usage_data = pd.read_csv('usage_data.csv')
churn_data = pd.read_csv('churn_labels.csv')
# 查看数据概况
print(customers.info())
print(customers.describe())
# 检查缺失值
print(customers.isnull().sum())
# 查看流失率分布
print(churn_data['churn_status'].value_counts())
print(f"流失率: {churn_data['churn_status'].mean():.2%}")
发现的第一个问题:数据质量很差
- 20% 的客户缺少公司规模信息
- 使用行为数据有很多异常值(比如连续登录 48 小时不间断)
- 流失标签的定义不统一(有的按取消日期,有的按最后使用日期)
- 时间戳格式不统一
这里花了我整整两天时间处理这些问题。先把数据清洗干净,是后续一切工作的基础。
第二步:数据清洗
def clean_data(customers, usage_data, churn_data):
# 统一时间格式
customers['registration_date'] = pd.to_datetime(customers['registration_date'])
usage_data['activity_date'] = pd.to_datetime(usage_data['activity_date'])
churn_data['churn_date'] = pd.to_datetime(churn_data['churn_date'])
# 处理缺失值
customers['company_size'].fillna(customers['company_size'].median(), inplace=True)
customers['industry'].fillna('Unknown', inplace=True)
# 处理异常值 - 比如登录时长超过 12 小时的设为 12 小时
usage_data['login_hours'] = usage_data['login_hours'].clip(upper=12)
# 统一流失标签定义:以最后使用日期为准
churn_data['churn_status'] = (
(pd.to_datetime('2024-01-01') - churn_data['last_activity_date']).dt.days > 30
).astype(int)
return customers, usage_data, churn_data
customers_clean, usage_clean, churn_clean = clean_data(customers, usage_data, churn_data)
踩坑记录:一开始我想直接用公司规模的均值填充缺失值,结果发现这个数据分布极不均匀(大部分是 1-10 人的小公司,少数是 1000+ 人的大公司),用均值会把中等公司的信息扭曲。后来改用中位数,结果好很多。
第三步:特征工程
这是监督学习中最关键、也最耗时的步骤。
3.1 基础特征
def create_basic_features(customers, churn_data):
features = pd.DataFrame()
# 基础信息
features['customer_id'] = customers['customer_id']
features['company_size'] = customers['company_size']
features['industry'] = customers['industry']
features['plan_type'] = customers['plan_type']
# 客户年龄(月)
features['customer_age_months'] = (
(pd.to_datetime('2024-01-01') - customers['registration_date']).dt.days / 30
).round(1)
# 历史续费次数
features['renewal_count'] = customers['renewal_count']
# 流失标签
features['churn_status'] = churn_data['churn_status']
return features
basic_features = create_basic_features(customers_clean, churn_clean)
3.2 使用行为特征
def create_usage_features(usage_data, churn_data):
# 按客户聚合使用数据
usage_agg = usage_data.groupby('customer_id').agg({
'login_hours': ['mean', 'std', 'max', 'min'],
'login_count': ['sum', 'mean'],
'features_used': ['sum', 'nunique'],
'activity_date': ['max', 'min']
}).reset_index()
# 扁平化列名
usage_agg.columns = ['_'.join(col).strip() for col in usage_agg.columns.values]
usage_agg = usage_agg.rename(columns={'customer_id_': 'customer_id'})
# 计算活跃度变化趋势
usage_monthly = usage_data.groupby([
'customer_id',
usage_data['activity_date'].dt.to_period('M')
])['login_hours'].sum().reset_index()
# 最近 3 个月的使用率变化
monthly_pivot = usage_monthly.pivot(
index='customer_id',
columns='activity_date',
values='login_hours'
).fillna(0)
if len(monthly_pivot.columns) >= 3:
monthly_pivot['usage_trend'] = (
monthly_pivot.iloc[:, -1] - monthly_pivot.iloc[:, -3]
) / (monthly_pivot.iloc[:, -3] + 1e-6)
return usage_agg, monthly_pivot
usage_features, monthly_usage = create_usage_features(usage_clean, churn_clean)
3.3 特征选择和重要性分析
def select_features(features, target):
# 相关性分析
correlation = features.corrwith(target).sort_values(ascending=False)
print("特征与目标变量的相关性:")
print(correlation)
# 特征重要性(使用随机森林)
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(features, target)
importance = pd.DataFrame({
'feature': features.columns,
'importance': rf.feature_importances_
}).sort_values('importance', ascending=False)
return importance
# 合并所有特征
all_features = pd.merge(basic_features, usage_features, on='customer_id', how='left')
# 只保留数值型特征
numeric_features = all_features.select_dtypes(include=[np.number])
feature_importance = select_features(
numeric_features.drop(['churn_status', 'customer_id'], axis=1),
all_features['churn_status']
)
关键发现:
- 最重要的特征是"最近一个月使用时长下降比例"
- 其次是"客服工单数量"和"续费次数"
- 一些看似重要的特征(如公司规模)其实影响不大
第四步:模型选择和训练
我尝试了多个模型:
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.svm import SVC
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score
# 准备数据
X = numeric_features.drop(['churn_status', 'customer_id'], axis=1)
y = numeric_features['churn_status']
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# 标准化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 定义模型
models = {
'Logistic Regression': LogisticRegression(random_state=42),
'Random Forest': RandomForestClassifier(n_estimators=100, random_state=42),
'Gradient Boosting': GradientBoostingClassifier(random_state=42),
'SVM': SVC(probability=True, random_state=42)
}
# 训练和评估
results = {}
for name, model in models.items():
if name == 'SVM':
model.fit(X_train_scaled, y_train)
y_pred = model.predict(X_test_scaled)
y_prob = model.predict_proba(X_test_scaled)[:, 1]
else:
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
y_prob = model.predict_proba(X_test)[:, 1]
results[name] = {
'accuracy': accuracy_score(y_test, y_pred),
'precision': precision_score(y_test, y_pred),
'recall': recall_score(y_test, y_pred),
'f1': f1_score(y_test, y_pred),
'roc_auc': roc_auc_score(y_test, y_prob),
'model': model
}
第五步:模型评估和选择
# 比较模型性能
results_df = pd.DataFrame(results).T
print(results_df)
# 可视化结果
plt.figure(figsize=(12, 6))
results_df[['accuracy', 'precision', 'recall', 'f1', 'roc_auc']].plot(kind='bar')
plt.title('模型性能比较')
plt.ylabel('分数')
plt.xticks(rotation=45)
plt.legend(bbox_to_anchor=(1.05, 1))
plt.tight_layout()
plt.show()
最终选择:Gradient Boosting(梯度提升树)
原因:
- ROC-AUC 分数最高(0.87)
- 在召回率和精确率之间有较好的平衡
- 提供了特征重要性,便于解释
第六步:超参数调优
from sklearn.model_selection import GridSearchCV
# 参数网格
param_grid = {
'n_estimators': [100, 200, 300],
'learning_rate': [0.01, 0.1, 0.2],
'max_depth': [3, 5, 7],
'min_samples_split': [2, 5, 10]
}
# 网格搜索
grid_search = GridSearchCV(
GradientBoostingClassifier(random_state=42),
param_grid,
cv=5,
scoring='roc_auc',
n_jobs=-1
)
grid_search.fit(X_train, y_train)
print(f"最佳参数: {grid_search.best_params_}")
print(f"最佳分数: {grid_search.best_score_:.4f}")
# 使用最佳模型
best_model = grid_search.best_estimator_
踩坑记录
坑 1:数据泄露
现象:模型在训练集上表现完美(准确率 99%),但在测试集上表现很差(准确率 60%)。
原因:我在特征工程时,不小心把一些未来信息混进去了。比如计算"平均使用时长"时,包含了预测当月的数据。
解决:严格按照时间切分数据,确保只用历史数据预测未来。
# 正确的做法:按时间划分训练集和测试集
train_data = all_features[all_features['customer_age_months'] <= 18]
test_data = all_features[all_features['customer_age_months'] > 18]
坑 2:类别不平衡
现象:模型总是预测"不流失",准确率看着不错(85%),但召回率很低(10%)。
原因:流失客户只占 15%,模型倾向于预测多数类。
解决:
- 使用重采样技术(SMOTE)
- 调整类别权重
- 使用合适的评估指标(关注 F1 和 AUC,而不是准确率)
from imblearn.over_sampling import SMOTE
smote = SMOTE(random_state=42)
X_train_balanced, y_train_balanced = smote.fit_resample(X_train, y_train)
# 或者使用类别权重
model = GradientBoostingClassifier(
class_weight={0: 1, 1: 3}, # 给流失类更高权重
random_state=42
)
坑 3:过拟合
现象:训练集表现很好,但测试集表现差,且模型对新数据泛化能力差。
原因:模型太复杂,学习到了训练数据的噪声。
解决:
- 增加训练数据
- 减少模型复杂度(限制树的深度)
- 使用正则化
- 交叉验证
# 限制模型复杂度
model = GradientBoostingClassifier(
max_depth=3, # 限制树深度
min_samples_split=10, # 增加分裂最小样本数
learning_rate=0.1, # 降低学习率
n_estimators=100,
random_state=42
)
坑 4:特征工程不足
现象:无论怎么调参,模型性能都上不去,卡在 AUC 0.75 左右。
原因:原始特征不够,缺少关键的业务特征。
解决:深入了解业务,创造更多有意义的特征。
# 添加业务特征
def create_business_features(usage_data, customer_data):
features = pd.DataFrame()
# 功能使用深度
features['feature_adoption_rate'] = (
usage_data['features_used'] / usage_data['available_features']
)
# 团队协作活跃度
features['team_collaboration_score'] = (
usage_data['shared_documents'] + usage_data['joint_sessions']
)
# 客服支持依赖度
features['support_dependency'] = (
customer_data['support_tickets'] / customer_data['usage_months']
)
return features
最终结果
模型性能
经过多轮优化,最终模型的性能:
- 准确率:82%
- 精确率:76%
- 召回率:68%
- F1 分数:0.72
- ROC-AUC:0.87
业务价值
模型上线后的实际效果:
- 识别准确率:在预测为高风险的客户中,85% 确实在一个月内流失
- 挽留成功率:对预测高风险的客户进行干预,挽留成功率提升到 40%
- 收入影响:每月减少约 15% 的流失损失
- 效率提升:销售团队的工作效率提升 30%,可以专注于真正需要关注的客户
特征重要性
最终模型中最重要的特征:
- 使用趋势(0.28):最近使用时长的变化趋势
- 客服工单数(0.19):过去 3 个月的客服工单数量
- 续费次数(0.15):历史续费次数
- 功能使用深度(0.12):已使用功能 / 可用功能
- 团队协作活跃度(0.10):团队共享和协作的活跃程度
部署和监控
部署方案
模型部署采用了简单的批量预测方式:
import joblib
import schedule
import time
# 保存模型
joblib.dump(best_model, 'churn_prediction_model.pkl')
joblib.dump(scaler, 'feature_scaler.pkl')
# 定时任务
def predict_churn():
# 读取最新数据
latest_data = get_latest_customer_data()
# 预处理
features = preprocess_data(latest_data)
# 预测
predictions = best_model.predict_proba(features)[:, 1]
# 生成高风险客户列表
high_risk_customers = latest_data[predictions > 0.7]
# 发送通知
send_alert_to_sales_team(high_risk_customers)
# 每周一早上运行
schedule.every().monday.at("09:00").do(predict_churn)
while True:
schedule.run_pending()
time.sleep(3600) # 每小时检查一次
监控指标
建立了以下监控机制:
- 模型性能监控:每月计算一次预测准确率
- 数据漂移监控:监控特征分布的变化
- 业务指标监控:跟踪实际流失率和挽留成功率
- 告警机制:当性能下降超过 5% 时触发告警
def monitor_model_performance():
# 计算本周预测准确率
recent_predictions = get_recent_predictions()
actual_outcomes = get_actual_outcomes()
accuracy = calculate_accuracy(recent_predictions, actual_outcomes)
# 与基线比较
baseline_accuracy = 0.82
if accuracy < baseline_accuracy * 0.95:
send_alert(f"模型性能下降!当前准确率: {accuracy:.2%}")
# 检查数据漂移
current_feature_dist = get_current_feature_distribution()
baseline_feature_dist = load_baseline_distribution()
drift_score = calculate_drift(current_feature_dist, baseline_feature_dist)
if drift_score > 0.1:
send_alert(f"检测到数据漂移!漂移分数: {drift_score:.3f}")
总结和反思
经过这个项目,我对监督学习有了更深入的理解:
关键收获
数据质量比算法选择更重要:花 80% 的时间在数据准备上,可能比尝试 10 个不同算法效果更好。
特征工程是核心竞争力:对业务的深入理解比掌握更多算法更关键。创造有意义的特征需要业务知识。
评估指标要匹配业务目标:不要只看准确率,要根据业务需求选择合适的指标(在这个项目中,召回率比精确率更重要)。
模型只是工具,解决业务问题才是目标:最终的价值不是模型有多复杂,而是能否真正帮助业务提升。
经验教训
- 不要一开始就追求复杂模型:简单的基线模型可以帮你快速验证思路。
- 要持续监控模型性能:模型不是一次性部署就完事了,需要持续维护和更新。
- 要与业务方紧密合作:技术方案的成败往往取决于对业务的理解程度。
- 要有容错机制:模型预测不是 100% 准确的,要设计好兜底方案。
适用场景
监督学习特别适合以下场景:
- 有大量历史标签数据
- 预测目标相对明确
- 特征与目标之间有可学习的关系
- 对预测的准确性有一定要求
不适用场景
- 缺少标签数据(这时考虑无监督学习)
- 目标变量定义模糊
- 环境变化太快,历史规律不适用
- 对可解释性要求极高(这时考虑规则系统)
结语
监督学习看似简单,但要真正做好并不容易。这个项目从开始到上线花了将近三个月时间,中间经历了多次失败和重来。但最终看到销售团队根据我们的预测成功挽留客户时,那种成就感是无可替代的。
机器学习不是魔法,它只是帮助我们更好地理解数据、发现规律的工具。真正有价值的是我们如何将技术洞察转化为业务行动。
希望这篇文章能给你一些启发,如果你也在做类似的监督学习项目,欢迎交流经验。记住:最好的算法是最能解决实际问题的算法,而不是最复杂的算法。
可用性说明:本文发布于 2020 年 3 月,距今已超过五年。文中涉及的软件版本、接口、下载地址、命令参数和操作界面可能已经发生变化,部分方案在当前环境下可能失效。请结合官方最新文档核对后再操作,生产环境使用前务必先行验证。
版权声明: 本文首发于 指尖魔法屋-关于AI监督学习的几点记录(https://blog.thinkmoon.cn/post/310-ai-supervised-learning-label-prediction-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。