关于AI监督学习的几点记录

他们有以下问题:

  • 客户流失率约为 15%,在行业里偏高

  • 每月流失的客户中,有 30% 本来可以通过挽留措施挽回

  • 没有系统化的流失预警机制,基本上是客户通知要取消时才知道

  • 销售团队时间和资源有限,需要优先关注高风险客户

  • 预测准确性:能够准确识别未来一个月内可能流失的客户

  • 可解释性:需要知道客户流失的主要原因,便于制定针对性措施

  • 实用性:模型要能部署到生产环境,每月自动运行

  • 及时性:提前一周给出预警,给销售团队足够的响应时间

为什么写这篇文章

最近在帮一个朋友做客户流失预测,他说:“我有两年的客户数据,想预测哪些客户下个月会流失。“我一开始觉得这不就是分类问题吗,用逻辑回归应该能搞定。结果一上手才发现,从数据到预测,中间有很多坑:数据质量、特征工程、模型选择、过拟合…

市面上讲监督学习的文章很多,但大部分都是教科书式的讲解,缺少实际的踩坑经验。这篇文章我想以客户流失预测为例,把监督学习从理论到实践的整个过程真实地写一遍,包括那些被坑的经历。

背景和需求

具体场景

朋友的公司是一家 SaaS 服务商,主要提供企业协作工具。他们有以下问题:

  • 客户流失率约为 15%,在行业里偏高
  • 每月流失的客户中,有 30% 本来可以通过挽留措施挽回
  • 没有系统化的流失预警机制,基本上是客户通知要取消时才知道
  • 销售团队时间和资源有限,需要优先关注高风险客户

核心需求

  • 预测准确性:能够准确识别未来一个月内可能流失的客户
  • 可解释性:需要知道客户流失的主要原因,便于制定针对性措施
  • 实用性:模型要能部署到生产环境,每月自动运行
  • 及时性:提前一周给出预警,给销售团队足够的响应时间

数据情况

现有的数据资源:

  • 用户基本信息(注册时间、公司规模、行业等)
  • 使用行为数据(登录频率、功能使用情况、活跃时长)
  • 订阅信息(套餐类型、付款历史、续费次数)
  • 交互记录(客服工单、反馈次数、产品迭代参与度)
  • 过去 24 个月的流失标签

理论基础:监督学习是什么

核心概念

监督学习(Supervised Learning)是最常见的机器学习类型,简单说就是:用有标签的数据训练模型,然后对无标签的数据进行预测

打个比方:就像教小孩认水果。你给他看苹果说"这是苹果”,看香蕉说"这是香蕉”(这就是训练数据,带标签)。然后给他一个新的水果,他能认出来是苹果还是香蕉(这就是预测)。

两种主要类型

监督学习主要分为两类:

1. 分类(Classification)

预测离散的类别标签。在我们的场景中:

  • 目标变量:客户是否会流失(流失 / 不流失)
  • 常见算法:逻辑回归、决策树、随机森林、XGBoost、支持向量机
  • 评估指标:准确率、精确率、召回率、F1 分数、ROC-AUC

2. 回归(Regression)

预测连续的数值。在我们的场景中:

  • 目标变量:客户续费金额 / 客户生命周期价值
  • 常见算法:线性回归、岭回归、Lasso 回归、决策树回归、梯度提升回归
  • 评估指标:均方误差(MSE)、均方根误差(RMSE)、R²

工作流程

监督学习的标准流程:

graph LR A[数据收集] --> B[数据清洗] B --> C[特征工程] C --> D[模型选择] D --> E[模型训练] E --> F[模型评估] F --> G{模型效果如何?} G -->|不满足| H[调参优化] H --> E G -->|满足| I[模型部署] I --> J[监控与维护]

实现过程

第一步:数据收集和探索

首先我拿到了朋友的数据库访问权限,开始探索数据:

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%,模型倾向于预测多数类。

解决

  1. 使用重采样技术(SMOTE)
  2. 调整类别权重
  3. 使用合适的评估指标(关注 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:过拟合

现象:训练集表现很好,但测试集表现差,且模型对新数据泛化能力差。

原因:模型太复杂,学习到了训练数据的噪声。

解决

  1. 增加训练数据
  2. 减少模型复杂度(限制树的深度)
  3. 使用正则化
  4. 交叉验证
# 限制模型复杂度
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%,可以专注于真正需要关注的客户

特征重要性

最终模型中最重要的特征:

  1. 使用趋势(0.28):最近使用时长的变化趋势
  2. 客服工单数(0.19):过去 3 个月的客服工单数量
  3. 续费次数(0.15):历史续费次数
  4. 功能使用深度(0.12):已使用功能 / 可用功能
  5. 团队协作活跃度(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}")

总结和反思

经过这个项目,我对监督学习有了更深入的理解:

关键收获

  1. 数据质量比算法选择更重要:花 80% 的时间在数据准备上,可能比尝试 10 个不同算法效果更好。

  2. 特征工程是核心竞争力:对业务的深入理解比掌握更多算法更关键。创造有意义的特征需要业务知识。

  3. 评估指标要匹配业务目标:不要只看准确率,要根据业务需求选择合适的指标(在这个项目中,召回率比精确率更重要)。

  4. 模型只是工具,解决业务问题才是目标:最终的价值不是模型有多复杂,而是能否真正帮助业务提升。

经验教训

  • 不要一开始就追求复杂模型:简单的基线模型可以帮你快速验证思路。
  • 要持续监控模型性能:模型不是一次性部署就完事了,需要持续维护和更新。
  • 要与业务方紧密合作:技术方案的成败往往取决于对业务的理解程度。
  • 要有容错机制:模型预测不是 100% 准确的,要设计好兜底方案。

适用场景

监督学习特别适合以下场景:

  • 有大量历史标签数据
  • 预测目标相对明确
  • 特征与目标之间有可学习的关系
  • 对预测的准确性有一定要求

不适用场景

  • 缺少标签数据(这时考虑无监督学习)
  • 目标变量定义模糊
  • 环境变化太快,历史规律不适用
  • 对可解释性要求极高(这时考虑规则系统)

结语

监督学习看似简单,但要真正做好并不容易。这个项目从开始到上线花了将近三个月时间,中间经历了多次失败和重来。但最终看到销售团队根据我们的预测成功挽留客户时,那种成就感是无可替代的。

机器学习不是魔法,它只是帮助我们更好地理解数据、发现规律的工具。真正有价值的是我们如何将技术洞察转化为业务行动。

希望这篇文章能给你一些启发,如果你也在做类似的监督学习项目,欢迎交流经验。记住:最好的算法是最能解决实际问题的算法,而不是最复杂的算法。

可用性说明:本文发布于 2020 年 3 月,距今已超过五年。文中涉及的软件版本、接口、下载地址、命令参数和操作界面可能已经发生变化,部分方案在当前环境下可能失效。请结合官方最新文档核对后再操作,生产环境使用前务必先行验证。

版权声明: 本文首发于 指尖魔法屋-关于AI监督学习的几点记录https://blog.thinkmoon.cn/post/310-ai-supervised-learning-label-prediction-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!