🧑 博主简介:曾任某智慧城市类企业算法总监,目前在美国市场的物流公司从事高级算法工程师一职,深耕人工智能领域,精通python数据挖掘、可视化、机器学习等,发表过AI相关的专利并多次在AI类比赛中获奖。CSDN人工智能领域的优质创作者,提供AI相关的技术咨询、项目开发和个性化解决方案等服务,如有需要请站内私信或者联系任意文章底部的的VX名片(ID:xf982831907)

💬 博主粉丝群介绍:① 群内初中生、高中生、本科生、研究生、博士生遍布,可互相学习,交流困惑。② 热榜top10的常客也在群里,也有数不清的万粉大佬,可以交流写作技巧,上榜经验,涨粉秘籍。③ 群内也有职场精英,大厂大佬,可交流技术、面试、找工作的经验。④ 进群免费赠送写作秘籍一份,助你由写作小白晋升为创作大佬。⑤ 进群赠送CSDN评论防封脚本,送真活跃粉丝,助你提升文章热度。有兴趣的加文末联系方式,备注自己的CSDN昵称,拉你进群,互相学习共同进步。

在这里插入图片描述


一、项目背景

本文基于保险客户数据集,完整拆解机器学习回归任务的全流程——从数据探索、特征工程到模型构建与部署,包含可直接运行的Python代码,结合行业背景与技术细节双视角,适合机器学习初学者夯实基础,也能为进阶者提供场景化实战参考。

二、数据集核心特征解读

数据集包含1338条保险客户记录,特征可分为三类,各特征对保费的影响逻辑如下:

特征类型字段名取值说明业务影响逻辑
人口特征age整数(18-64)年龄越大,健康风险越高,保费通常越高
人口特征sexmale/female生理差异导致健康风险不同(但实际影响较弱)
人口特征children整数(0-5)子女数量影响家庭医疗支出需求
健康特征bmi浮点数(15-54)身体质量指数,超标(≥30)易引发慢性病,推高保费
健康特征smokeryes/no吸烟是健康高风险核心指标,保费差异最显著
地域特征region四大区域不同地区医疗成本、气候环境差异影响保费
目标变量charges浮点数(~1k-63k美元)客户年度医疗保险费用

三、加载数据集

# 导入必要的库
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import FunctionTransformer, LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.tree import DecisionTreeRegressor
from sklearn.metrics import mean_squared_error, r2_score
import scipy.stats as stats
import warnings
warnings.filterwarnings('ignore')

# 设置中文字体(解决绘图中文乱码)
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False

# 加载数据(确保insurance.csv文件路径与代码同级)
df = pd.read_csv("insurance.csv")
print("数据集形状:", df.shape)
print("\n数据集前5行(快速预览数据结构):")
print(df.head())
print("\n数据集基本信息(数据类型/非空值检查):")
print(df.info())

四、探索性数据分析(EDA):从数据中挖掘业务规律

EDA的核心目标是发现特征与目标变量的关联、识别数据异常、验证业务直觉,而非单纯绘图。以下从「单特征分布」「特征间关联」两个维度展开。

4.1 数据分布可视化:直观发现关键规律

通过多维度图表组合,一次性呈现核心分布特征,重点关注「目标变量分布」「核心特征与保费的关系」:

# 创建综合EDA可视化图表
fig = plt.figure(figsize=(18, 12))

# 1. 保险费用分布(核心:识别分布偏态)
ax1 = plt.subplot(2, 3, 1)
sns.histplot(data=df, x='charges', kde=True, bins=30, color='skyblue')
ax1.set_title('保险费用分布', fontsize=14, fontweight='bold')
ax1.set_xlabel('保险费用(美元)', fontsize=12)
ax1.set_ylabel('频数', fontsize=12)
ax1.grid(alpha=0.3)

# 2. 性别与保险费用关系(验证:性别影响是否显著)
ax2 = plt.subplot(2, 3, 2)
sns.boxplot(data=df, x='sex', y='charges', palette='Set2')
ax2.set_title('性别 vs 保险费用', fontsize=14, fontweight='bold')
ax2.set_xlabel('性别', fontsize=12)
ax2.set_ylabel('保险费用(美元)', fontsize=12)
ax2.set_xticklabels(['女性', '男性'])
ax2.grid(alpha=0.3)

# 3. 吸烟状况与保险费用关系(核心:验证高风险特征)
ax3 = plt.subplot(2, 3, 3)
sns.boxplot(data=df, x='smoker', y='charges', palette='Set3')
ax3.set_title('吸烟状况 vs 保险费用', fontsize=14, fontweight='bold')
ax3.set_xlabel('是否吸烟', fontsize=12)
ax3.set_ylabel('保险费用(美元)', fontsize=12)
ax3.set_xticklabels(['不吸烟', '吸烟'])
ax3.grid(alpha=0.3)

# 4. 年龄与保险费用关系(交互:年龄+吸烟的组合影响)
ax4 = plt.subplot(2, 3, 4)
sns.scatterplot(data=df, x='age', y='charges', hue='smoker', 
                palette={0: 'green', 1: 'red'}, alpha=0.6)
ax4.set_title('年龄 vs 保险费用', fontsize=14, fontweight='bold')
ax4.set_xlabel('年龄', fontsize=12)
ax4.set_ylabel('保险费用(美元)', fontsize=12)
ax4.legend(title='是否吸烟', labels=['不吸烟', '吸烟'])
ax4.grid(alpha=0.3)

# 5. BMI与保险费用关系(交互:BMI+吸烟的健康风险)
ax5 = plt.subplot(2, 3, 5)
sns.scatterplot(data=df, x='bmi', y='charges', hue='smoker',
                palette={0: 'blue', 1: 'orange'}, alpha=0.6)
ax5.set_title('BMI vs 保险费用', fontsize=14, fontweight='bold')
ax5.set_xlabel('BMI(身体质量指数)', fontsize=12)
ax5.set_ylabel('保险费用(美元)', fontsize=12)
ax5.legend(title='是否吸烟', labels=['不吸烟', '吸烟'])
ax5.grid(alpha=0.3)

# 6. 地区分布(基础:客户地域分布均匀性)
ax6 = plt.subplot(2, 3, 6)
region_counts = df['region'].value_counts()
colors = ['#FF9999', '#66B2FF', '#99FF99', '#FFCC99']
wedges, texts, autotexts = ax6.pie(region_counts.values, labels=region_counts.index,
                                   autopct='%1.1f%%', colors=colors, startangle=90)
ax6.set_title('客户地区分布', fontsize=14, fontweight='bold')
plt.setp(autotexts, size=10, weight="bold")

plt.tight_layout()
plt.show()

# 数值特征描述性统计(量化数据特征)
print("\n数值特征描述性统计(核心指标):")
print(df.describe().round(2))

# 分类特征分布(统计样本占比)
print("\n分类特征分布(样本占比):")
for col in ['sex', 'smoker', 'region']:
    print(f"\n{col}分布:")
    print(df[col].value_counts(normalize=True).round(3) * 100, "%")

  1. 图1关键发现:保费呈右偏分布,多数客户保费较低,少数客户保费极高
  2. 图2关键发现:性别对保费影响极小,箱线图分布几乎重合
  3. 图3关键发现:吸烟客户保费中位数是不吸烟客户的3倍以上,差异极显著
  4. 图4关键发现:不吸烟客户保费随年龄线性增长,吸烟客户保费整体偏高且增长更快
  5. 图5关键发现:BMI≥30的吸烟客户保费最高,肥胖+吸烟是高风险组合
  6. 图6关键发现:地区分布较均匀
  7. 其它关键发现:
  • age:均值39.2岁,标准差14.0,覆盖全年龄段
  • bmi:均值30.6,标准差6.1,整体偏胖(正常范围18.5-24.9)
  • charges:均值13270美元,标准差12110,离散度极高(右偏分布)
  • sex:男女比例接近1:1(50.5% vs 49.5%)
  • smoker:吸烟者占比20.5%,非吸烟者占79.5%
  • region:四大地区占比均在24%-27%之间

4.2 相关性分析:量化特征与保费的关联强度

EDA的可视化结论需要量化验证,通过相关性分析明确特征重要性排序,为后续特征工程和模型构建指明方向:

# 数据预处理:编码分类变量(将文本特征转为数值,才能计算相关性)
df_encoded = df.copy()
le = LabelEncoder()
df_encoded['smoker'] = le.fit_transform(df_encoded['smoker'])  # 1=吸烟,0=不吸烟
df_encoded['sex'] = le.fit_transform(df_encoded['sex'])        # 1=男性,0=女性
df_encoded = pd.get_dummies(df_encoded, columns=['region'], drop_first=True)  # 地区独热编码

# 绘制相关性热图(聚焦核心关联)
plt.figure(figsize=(12, 8))
correlation_matrix = df_encoded.corr(method='pearson')
# 屏蔽上三角矩阵(避免重复信息)
mask = np.triu(np.ones_like(correlation_matrix, dtype=bool))
sns.heatmap(correlation_matrix, mask=mask, annot=True, fmt='.2f', 
            cmap='coolwarm', center=0, square=True,
            cbar_kws={"shrink": 0.8})
plt.title('特征相关性热图', fontsize=16, fontweight='bold', pad=20)
plt.tight_layout()
plt.show()

# 目标变量与各特征的相关性排序(核心结论)
print("\n保险费用与各特征的相关性(从高到低):")
correlation_with_target = df_encoded.corr()['charges'].sort_values(ascending=False)
print(correlation_with_target)

  1. 相关性核心解读:
    1. smoker(吸烟):0.79 → 极强正相关,是影响保费的第一因素
    1. age(年龄):0.30 → 中等正相关,第二重要因素
    1. bmi(BMI):0.20 → 弱正相关,第三重要因素
    1. sex/children/region:相关系数<0.1 → 几乎无线性关联

五、特征工程:从原始数据到有效特征

特征工程是提升模型性能的核心环节,本项目针对保险数据的特点,重点解决「异常值干扰」「特征分布偏态」「隐藏特征交互」三大问题:

5.1 数据预处理:标准化特征格式

核心目标是将「非数值特征转为数值」「检测异常值」,为后续特征优化打基础:

def preprocess_data(df):
    """
    数据预处理函数(可复用)
    功能:统一特征格式,编码分类变量
    输入:原始DataFrame
    输出:预处理后的DataFrame
    """
    df = df.copy()
    
    # 1. 标签编码:二分类变量(smoker/sex)转为0/1
    le = LabelEncoder()
    df['smoker'] = le.fit_transform(df['smoker'])
    df['sex'] = le.fit_transform(df['sex'])
    
    # 2. 独热编码:多分类变量(region),drop_first避免多重共线性
    df = pd.get_dummies(df, columns=['region'], drop_first=True, dtype=int)
    
    return df

# 应用预处理
df_processed = preprocess_data(df)
print("预处理后的数据集形状:", df_processed.shape)
print("\n预处理后的列名(确认特征编码完成):")
print(df_processed.columns.tolist())

def detect_outliers(df, column):
    """
    异常值检测函数(IQR准则)
    原理:四分位距±1.5倍IQR外的数值为异常值,适合非正态分布数据
    输入:DataFrame、待检测列名
    输出:异常值下界、上界、异常值占比
    """
    Q1 = df[column].quantile(0.25)  # 下四分位数(25%分位)
    Q3 = df[column].quantile(0.75)  # 上四分位数(75%分位)
    IQR = Q3 - Q1                   # 四分位距
    lower_bound = Q1 - 1.5 * IQR    # 异常值下界
    upper_bound = Q3 + 1.5 * IQR    # 异常值上界
    
    outliers = df[(df[column] < lower_bound) | (df[column] > upper_bound)]
    outlier_percentage = len(outliers) / len(df) * 100
    
    return lower_bound, upper_bound, outlier_percentage

# 检查关键特征的异常值(BMI和保费是核心数值特征)
print("\n异常值检测结果(核心特征):")
for col in ['bmi', 'charges']:
    lower_bound, upper_bound, outlier_percentage = detect_outliers(df_processed, col)
    print(f"\n{col}:")
    print(f"  异常值下界: {lower_bound:.2f}")
    print(f"  异常值上界: {upper_bound:.2f}")
    print(f"  异常值比例: {outlier_percentage:.2f}%")

# 异常值解读:
# - bmi:异常值占比~1.2%,比例低,无需删除,缩尾处理即可
# - charges:异常值占比~10%,比例较高,删除会丢失大量信息,缩尾更合适

  1. 相关性核心解读:
  • bmi:异常值占比~1.2%,比例低,无需删除,缩尾处理即可
  • charges:异常值占比~10%,比例较高,删除会丢失大量信息,缩尾更合适

5.2 特征转换与创建:挖掘隐藏价值

针对数据特点,通过「异常值缩尾」「分布变换」「交互特征创建」提升特征表达能力:

def feature_engineering(df):
    """
    特征工程核心函数
    核心优化点:
    1. 异常值缩尾:保留数据,避免极端值干扰
    2. 对数变换:改善保费右偏分布,提升模型拟合效果
    3. 交互特征:挖掘年龄/BMI/吸烟的组合影响
    4. 分类特征:将连续特征(BMI/年龄)分组,捕捉非线性关系
    """
    df = df.copy()
    
    # 1. 异常值处理(缩尾:将异常值替换为边界值,而非删除)
    # BMI异常值缩尾
    bmi_q1 = df['bmi'].quantile(0.25)
    bmi_q3 = df['bmi'].quantile(0.75)
    bmi_iqr = bmi_q3 - bmi_q1
    bmi_lower = bmi_q1 - 1.5 * bmi_iqr
    bmi_upper = bmi_q3 + 1.5 * bmi_iqr
    df['bmi'] = np.where(df['bmi'] > bmi_upper, bmi_upper,
                         np.where(df['bmi'] < bmi_lower, bmi_lower, df['bmi']))
    
    # 保费异常值缩尾
    charges_q1 = df['charges'].quantile(0.25)
    charges_q3 = df['charges'].quantile(0.75)
    charges_iqr = charges_q3 - charges_q1
    charges_lower = charges_q1 - 1.5 * charges_iqr
    charges_upper = charges_q3 + 1.5 * charges_iqr
    df['charges'] = np.where(df['charges'] > charges_upper, charges_upper,
                             np.where(df['charges'] < charges_lower, charges_lower, df['charges']))
    
    # 2. 目标变量对数变换(log1p = log(1+x),避免0值问题)
    # 作用:将右偏分布转为近似正态分布,提升线性模型拟合效果
    trf = FunctionTransformer(np.log1p)
    df['charges_log'] = trf.fit_transform(df[['charges']])
    
    # 3. 创建交互特征(核心:挖掘特征间的组合影响)
    df['age_bmi_interaction'] = df['age'] * df['bmi'] / 100  # 年龄×BMI(归一化)
    df['bmi_smoker_interaction'] = df['bmi'] * df['smoker']  # BMI×吸烟(健康风险)
    df['age_smoker_interaction'] = df['age'] * df['smoker']  # 年龄×吸烟(年龄+吸烟风险)
    
    # 4. BMI分类特征(按医学标准分组)
    df['bmi_category'] = pd.cut(df['bmi'], 
                                 bins=[0, 18.5, 25, 30, 40, 100],
                                 labels=['偏瘦', '正常', '超重', '肥胖', '重度肥胖'])
    # 编码BMI分类(适配模型输入)
    bmi_cat_mapping = {'偏瘦': 0, '正常': 1, '超重': 2, '肥胖': 3, '重度肥胖': 4}
    df['bmi_category_encoded'] = df['bmi_category'].map(bmi_cat_mapping)
    
    # 5. 年龄分组特征(按生命周期分组)
    df['age_group'] = pd.cut(df['age'], 
                              bins=[0, 25, 35, 45, 55, 100],
                              labels=['青年', '中青年', '中年', '中老年', '老年'])
    # 编码年龄分组
    age_group_mapping = {'青年': 0, '中青年': 1, '中年': 2, '中老年': 3, '老年': 4}
    df['age_group_encoded'] = df['age_group'].map(age_group_mapping)
    
    return df

# 应用特征工程
df_final = feature_engineering(df_processed)
print("\n特征工程后数据集形状:", df_final.shape)
print("\n新增特征(核心价值特征):")
new_features = set(df_final.columns) - set(df_processed.columns)
print(list(new_features))

# 可视化特征工程效果(验证优化是否有效)
fig = plt.figure(figsize=(15, 10))

###原始保费 vs 对数变换后保费(核心:分布改善)
# 1. 原始保费 
ax1 = plt.subplot(3, 3, 1)
sns.histplot(df_final['charges'], kde=True, color='blue', alpha=0.5, label='原始保费')
ax1.set_title('原始保费分布', fontsize=14, fontweight='bold')
ax1.set_xlabel('值', fontsize=12)
ax1.set_ylabel('密度', fontsize=12)
ax1.legend()
ax1.grid(alpha=0.3)

# 2. 对数变换后保费
ax1 = plt.subplot(3, 3, 2)
sns.histplot(df_final['charges_log'], kde=True, color='red', alpha=0.5, label='对数变换后')
ax1.set_title('对数变换后保费', fontsize=14, fontweight='bold')
ax1.set_xlabel('值', fontsize=12)
ax1.set_ylabel('密度', fontsize=12)
ax1.legend()
ax1.grid(alpha=0.3)
# 效果:对数变换后保费分布更接近正态,模型拟合更友好

# 3. 处理后的BMI分布(验证:异常值缩尾效果)
ax2 = plt.subplot(3, 3, 3)
sns.histplot(df_final['bmi'], kde=True, color='green')
ax2.set_title('处理后的BMI分布', fontsize=14, fontweight='bold')
ax2.set_xlabel('BMI', fontsize=12)
ax2.set_ylabel('密度', fontsize=12)
ax2.grid(alpha=0.3)

# 4. BMI类别分布(业务:健康风险分组)
ax3 = plt.subplot(3, 3, 4)
bmi_cat_counts = df_final['bmi_category'].value_counts()
bars = ax3.bar(range(len(bmi_cat_counts)), bmi_cat_counts.values, color='orange')
ax3.set_title('BMI类别分布', fontsize=14, fontweight='bold')
ax3.set_xlabel('BMI类别', fontsize=12)
ax3.set_ylabel('客户数量', fontsize=12)
ax3.set_xticks(range(len(bmi_cat_counts)))
ax3.set_xticklabels(bmi_cat_counts.index, rotation=45)
ax3.grid(axis='y', alpha=0.3)
# 业务解读:超重/肥胖客户占比超60%,是主要客群

# 5. 年龄组分布(业务:客户年龄结构)
ax4 = plt.subplot(3, 3, 5)
age_group_counts = df_final['age_group'].value_counts()
bars = ax4.bar(range(len(age_group_counts)), age_group_counts.values, color='purple')
ax4.set_title('客户年龄组分布', fontsize=14, fontweight='bold')
ax4.set_xlabel('年龄组', fontsize=12)
ax4.set_ylabel('客户数量', fontsize=12)
ax4.set_xticks(range(len(age_group_counts)))
ax4.set_xticklabels(age_group_counts.index, rotation=45)
ax4.grid(axis='y', alpha=0.3)

# 6. 交互特征 vs 保费(核心:验证交互价值)
ax5 = plt.subplot(3, 3, 6)
sns.scatterplot(data=df_final, x='age_bmi_interaction', y='charges_log', 
                hue='smoker', palette='viridis', alpha=0.6)
ax5.set_title('年龄×BMI 交互 vs 保费', fontsize=14, fontweight='bold')
ax5.set_xlabel('年龄×BMI/100', fontsize=12)
ax5.set_ylabel('保费(对数)', fontsize=12)
ax5.legend(title='是否吸烟', labels=['不吸烟', '吸烟'])
ax5.grid(alpha=0.3)
# 效果:交互特征能有效区分不同风险客户的保费差异

# 7. 正态性检验(验证:对数变换效果)
ax6 = plt.subplot(3, 3, 8)
stats.probplot(df_final['charges_log'], dist="norm", plot=plt)
ax6.set_title('保费对数变换正态性检验', fontsize=14, fontweight='bold')
ax6.grid(alpha=0.3)
# 效果:点基本沿直线分布,说明变换后接近正态分布

plt.tight_layout()
plt.show()

六、模型训练与评估:从基线到最优模型

本项目选择4类经典回归模型,通过「多模型对比」「可视化评估」「特征重要性分析」找到最优解:

6.1 训练数据准备:划分与验证

# 准备特征和目标变量
# 特征:排除目标变量、非数值分类特征(已编码)
X = df_final.drop(['charges', 'charges_log', 'bmi_category', 'age_group'], axis=1)
# 目标变量:使用对数变换后的保费(分布更优)
y = df_final['charges_log']  

# 划分训练集(70%)和测试集(30%),random_state保证结果可复现
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

print(f"训练集形状: X_train={X_train.shape}, y_train={y_train.shape}")
print(f"测试集形状: X_test={X_test.shape}, y_test={y_test.shape}")
print(f"\n特征数量: {X_train.shape[1]}")
print(f"最终特征列表: {X.columns.tolist()}")

6.2 多模型训练与评估:统一评估标准

def train_and_evaluate_model(model, model_name, X_train, X_test, y_train, y_test):
    """
    模型训练与评估通用函数(可复用)
    核心评估指标:
    1. R²:决定系数,越接近1说明模型解释力越强
    2. RMSE:均方根误差,反映预测值与实际值的平均偏差(原始尺度)
    3. MAE:平均绝对误差,更稳健的误差指标
    """
    print(f"\n{'='*60}")
    print(f"训练模型: {model_name}")
    print('='*60)
    
    # 训练模型
    model.fit(X_train, y_train)
    
    # 预测(对数尺度)
    y_train_pred = model.predict(X_train)
    y_test_pred = model.predict(X_test)
    
    # 将预测值转换回原始保费尺度(expm1 = exp(x)-1,逆变换)
    y_train_original = np.expm1(y_train)
    y_test_original = np.expm1(y_test)
    y_train_pred_original = np.expm1(y_train_pred)
    y_test_pred_original = np.expm1(y_test_pred)
    
    # 计算评估指标
    train_r2 = r2_score(y_train, y_train_pred)  # 对数尺度R²
    test_r2 = r2_score(y_test, y_test_pred)
    train_rmse = np.sqrt(mean_squared_error(y_train_original, y_train_pred_original))  # 原始尺度RMSE
    test_rmse = np.sqrt(mean_squared_error(y_test_original, y_test_pred_original))
    train_mae = np.mean(np.abs(y_train_original - y_train_pred_original))  # 原始尺度MAE
    test_mae = np.mean(np.abs(y_test_original - y_test_pred_original))
    
    # 打印结果
    print(f"训练集 R² 分数: {train_r2:.4f}")
    print(f"测试集 R² 分数: {test_r2:.4f}")
    print(f"训练集 RMSE: ${train_rmse:.2f}")
    print(f"测试集 RMSE: ${test_rmse:.2f}")
    print(f"训练集 MAE: ${train_mae:.2f}")
    print(f"测试集 MAE: ${test_mae:.2f}")
    
    return {
        'model': model,
        'y_train_pred': y_train_pred,
        'y_test_pred': y_test_pred,
        'y_train_pred_original': y_train_pred_original,
        'y_test_pred_original': y_test_pred_original,
        'metrics': {
            'train_r2': train_r2,
            'test_r2': test_r2,
            'train_rmse': train_rmse,
            'test_rmse': test_rmse,
            'train_mae': train_mae,
            'test_mae': test_mae
        }
    }

# 定义待训练的模型(覆盖线性/树模型/集成模型)
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor

models = {
    '线性回归': LinearRegression(),  # 基线模型,简单易解释
    '决策树回归': DecisionTreeRegressor(max_depth=5, random_state=42),  # 捕捉非线性
    '随机森林回归': RandomForestRegressor(n_estimators=100, max_depth=10, random_state=42),  # 集成树模型
    '梯度提升回归': GradientBoostingRegressor(n_estimators=100, learning_rate=0.1, random_state=42)  # 梯度提升(高性能)
}

# 训练所有模型并保存结果
results = {}
for model_name, model in models.items():
    results[model_name] = train_and_evaluate_model(model, model_name, X_train, X_test, y_train, y_test)

6.3 模型性能可视化:直观对比与诊断

# 创建模型性能综合对比图
fig = plt.figure(figsize=(18, 12))

# 1. R²分数对比(核心:模型解释力)
ax1 = plt.subplot(2, 3, 1)
model_names = list(results.keys())
train_r2_scores = [results[m]['metrics']['train_r2'] for m in model_names]
test_r2_scores = [results[m]['metrics']['test_r2'] for m in model_names]

x = np.arange(len(model_names))
width = 0.35

bars1 = ax1.bar(x - width/2, train_r2_scores, width, label='训练集', color='skyblue')
bars2 = ax1.bar(x + width/2, test_r2_scores, width, label='测试集', color='lightcoral')

ax1.set_title('模型R²分数对比', fontsize=14, fontweight='bold')
ax1.set_xlabel('模型', fontsize=12)
ax1.set_ylabel('R²分数(越高越好)', fontsize=12)
ax1.set_xticks(x)
ax1.set_xticklabels(model_names, rotation=45)
ax1.legend()
ax1.grid(axis='y', alpha=0.3)

# 添加数值标签(直观对比)
for bars in [bars1, bars2]:
    for bar in bars:
        height = bar.get_height()
        ax1.text(bar.get_x() + bar.get_width()/2., height + 0.01,
                f'{height:.3f}', ha='center', va='bottom', fontsize=9)

# 2. RMSE对比(核心:预测误差)
ax2 = plt.subplot(2, 3, 2)
train_rmse_scores = [results[m]['metrics']['train_rmse'] for m in model_names]
test_rmse_scores = [results[m]['metrics']['test_rmse'] for m in model_names]

bars1 = ax2.bar(x - width/2, train_rmse_scores, width, label='训练集', color='lightgreen')
bars2 = ax2.bar(x + width/2, test_rmse_scores, width, label='测试集', color='gold')

ax2.set_title('模型RMSE对比', fontsize=14, fontweight='bold')
ax2.set_xlabel('模型', fontsize=12)
ax2.set_ylabel('RMSE(越低越好,美元)', fontsize=12)
ax2.set_xticks(x)
ax2.set_xticklabels(model_names, rotation=45)
ax2.legend()
ax2.grid(axis='y', alpha=0.3)

# 3. 最佳模型:预测值 vs 实际值(诊断:拟合效果)
best_model_name = max(results.items(), key=lambda x: x[1]['metrics']['test_r2'])[0]
best_result = results[best_model_name]

ax3 = plt.subplot(2, 3, 3)
ax3.scatter(y_test, best_result['y_test_pred'], alpha=0.6, color='blue')
# 理想线:预测值=实际值
ax3.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 
         'r--', lw=2, label='理想预测线')
ax3.set_title(f'{best_model_name}: 预测值 vs 实际值', fontsize=14, fontweight='bold')
ax3.set_xlabel('实际值(对数尺度)', fontsize=12)
ax3.set_ylabel('预测值(对数尺度)', fontsize=12)
ax3.legend()
ax3.grid(alpha=0.3)
# 诊断:点越靠近理想线,模型拟合效果越好

# 4. 最佳模型:残差图(诊断:误差分布)
ax4 = plt.subplot(2, 3, 4)
residuals = y_test - best_result['y_test_pred']  # 残差=实际值-预测值
ax4.scatter(best_result['y_test_pred'], residuals, alpha=0.6, color='green')
ax4.axhline(y=0, color='r', linestyle='--')  # 残差为0的基准线
ax4.set_title(f'{best_model_name}: 残差图', fontsize=14, fontweight='bold')
ax4.set_xlabel('预测值(对数尺度)', fontsize=12)
ax4.set_ylabel('残差', fontsize=12)
ax4.grid(alpha=0.3)
# 诊断:残差随机分布在0线附近,无明显规律 → 模型无系统性偏差

# 5. 最佳模型:特征重要性(业务:关键影响因素)
ax5 = plt.subplot(2, 3, 5)
if hasattr(best_result['model'], 'feature_importances_'):  # 仅树模型有特征重要性
    feature_importance = best_result['model'].feature_importances_
    feature_names = X.columns
    
    # 取前10个重要特征
    importance_df = pd.DataFrame({
        'feature': feature_names,
        'importance': feature_importance
    }).sort_values('importance', ascending=True).tail(10)
    
    bars = ax5.barh(range(len(importance_df)), importance_df['importance'], color='orange')
    ax5.set_yticks(range(len(importance_df)))
    ax5.set_yticklabels(importance_df['feature'])
    ax5.set_title(f'{best_model_name}: 特征重要性(前10)', fontsize=14, fontweight='bold')
    ax5.set_xlabel('重要性(越高越关键)', fontsize=12)
    ax5.grid(axis='x', alpha=0.3)
# 业务价值:明确哪些特征对保费预测最关键,指导定价策略

# 6. 最佳模型:预测误差分布(诊断:误差正态性)
ax6 = plt.subplot(2, 3, 6)
errors = y_test_original - best_result['y_test_pred_original']  # 原始尺度误差
sns.histplot(errors, kde=True, color='purple', ax=ax6)
ax6.axvline(x=0, color='r', linestyle='--')  # 误差为0的基准线
ax6.set_title(f'{best_model_name}: 预测误差分布', fontsize=14, fontweight='bold')
ax6.set_xlabel('预测误差(美元)', fontsize=12)
ax6.set_ylabel('密度', fontsize=12)
ax6.grid(alpha=0.3)
# 诊断:误差接近正态分布,均值接近0 → 模型预测误差无偏

plt.tight_layout()
plt.show()

6.4 详细性能报告:量化对比与结论

# 生成模型性能详细报告
print("\n" + "="*60)
print("模型性能详细报告(核心结论)")
print("="*60)

# 创建性能对比表格(直观对比)
performance_data = []
for model_name, result in results.items():
    metrics = result['metrics']
    performance_data.append({
        '模型': model_name,
        '训练集R²': f"{metrics['train_r2']:.4f}",
        '测试集R²': f"{metrics['test_r2']:.4f}",
        '训练集RMSE(美元)': f"{metrics['train_rmse']:.2f}",
        '测试集RMSE(美元)': f"{metrics['test_rmse']:.2f}",
        '训练集MAE(美元)': f"{metrics['train_mae']:.2f}",
        '测试集MAE(美元)': f"{metrics['test_mae']:.2f}"
    })

performance_df = pd.DataFrame(performance_data)
print("\n模型性能对比表:")
print(performance_df.to_string(index=False))

# 找出最佳模型(测试集R²最高)
best_model_name = max(results.items(), key=lambda x: x[1]['metrics']['test_r2'])[0]
best_metrics = results[best_model_name]['metrics']
print(f"\n【最佳模型】: {best_model_name}")
print(f"  测试集R²分数: {best_metrics['test_r2']:.4f} → 模型能解释{best_metrics['test_r2']*100:.1f}%的保费变异")
print(f"  测试集RMSE: ${best_metrics['test_rmse']:.2f} → 平均预测误差约{best_metrics['test_rmse']:.2f}美元")
print(f"  测试集MAE: ${best_metrics['test_mae']:.2f} → 平均绝对误差约{best_metrics['test_mae']:.2f}美元")

# 特征重要性分析(业务解读)
if hasattr(results[best_model_name]['model'], 'feature_importances_'):
    print(f"\n【{best_model_name} 特征重要性TOP5】:")
    feature_importance = results[best_model_name]['model'].feature_importances_
    importance_df = pd.DataFrame({
        '特征': X.columns,
        '重要性': feature_importance
    }).sort_values('重要性', ascending=False).head(5)
    print(importance_df.to_string(index=False))
    # 业务解读:吸烟状态、年龄×吸烟交互、BMI×吸烟交互是核心特征,符合保险定价逻辑

七、模型部署与预测:从模型到业务应用

将训练好的最优模型封装为预测函数,适配新客户数据,实现实际业务场景的保费预测:

# 封装预测函数(适配新数据)
def predict_insurance_charges(model, input_data):
    """
    保险费用预测函数(业务落地核心)
    输入:
    - model:训练好的最优模型
    - input_data:新客户数据(字典格式)
    输出:
    - prediction_original:预测的保险费用(原始美元尺度)
    """
    # 1. 将输入数据转为DataFrame(适配预处理函数)
    input_df = pd.DataFrame([input_data])
    
    # 2. 应用与训练数据一致的预处理
    input_df = preprocess_data(input_df)
    
    # 3. 应用与训练数据一致的特征工程(核心:保证特征匹配)
    # 添加交互特征
    input_df['age_bmi_interaction'] = input_df['age'] * input_df['bmi'] / 100
    input_df['bmi_smoker_interaction'] = input_df['bmi'] * input_df['smoker']
    input_df['age_smoker_interaction'] = input_df['age'] * input_df['smoker']
    
    # 补充缺失特征(避免模型输入维度不匹配)
    for col in X.columns:
        if col not in input_df.columns:
            input_df[col] = 0
    
    # 确保特征顺序与训练集一致(关键:模型输入维度顺序必须匹配)
    input_df = input_df[X.columns]
    
    # 4. 预测(对数尺度)
    prediction_log = model.predict(input_df)[0]
    # 5. 转换回原始美元尺度(逆对数变换)
    prediction_original = np.expm1(prediction_log)
    
    return prediction_original

# 示例预测(模拟实际业务场景)
print("\n" + "="*60)
print("保险费用预测示例(业务落地)")
print("="*60)

# 加载最优模型
best_model = results[best_model_name]['model']

# 模拟3类典型客户(覆盖低/中/高风险)
example_customers = [
    # 低风险客户:年轻、不吸烟、BMI正常
    {
        'age': 25,
        'sex': 'female',
        'bmi': 22.5,
        'children': 0,
        'smoker': 'no',
        'region': 'southwest'
    },
    # 高风险客户:中年、吸烟、BMI肥胖
    {
        'age': 45,
        'sex': 'male',
        'bmi': 30.5,
        'children': 2,
        'smoker': 'yes',
        'region': 'northeast'
    },
    # 中风险客户:老年、不吸烟、BMI超重
    {
        'age': 60,
        'sex': 'male',
        'bmi': 28.0,
        'children': 3,
        'smoker': 'no',
        'region': 'southeast'
    }
]

# 批量预测并输出结果
print("\n预测结果(业务解读):")
for i, customer in enumerate(example_customers, 1):
    predicted_charges = predict_insurance_charges(best_model, customer)
    print(f"\n【客户 {i}】")
    print(f"  基本信息:{customer['age']}岁 | {customer['sex']} | BMI{customer['bmi']:.1f} | 子女{customer['children']}人 | {'吸烟' if customer['smoker']=='yes' else '不吸烟'} | {customer['region']}")
    print(f"  预测年度保费:${predicted_charges:,.2f}")
    # 业务解读
    if predicted_charges < 5000:
        risk_level = "低风险"
    elif predicted_charges < 20000:
        risk_level = "中风险"
    else:
        risk_level = "高风险"
    print(f"  风险等级:{risk_level}")

八、项目总结与进阶方向

8.1 核心结论(业务+技术)

# 项目核心总结
print("\n" + "="*60)
print("项目总结与改进建议")
print("="*60)

# 业务结论
summary_points = [
    "1. 核心影响因素:吸烟是保费的第一决定因素(相关性0.79),其次是年龄和BMI",
    "2. 风险组合效应:吸烟+肥胖+高龄的客户保费最高,是保险公司高风险客群",
    "3. 模型性能:梯度提升回归最优,测试集R²达0.87,平均预测误差约3000美元",
    "4. 特征价值:交互特征(如年龄×吸烟)能显著提升模型预测能力",
    "5. 数据规律:保费呈右偏分布,对数变换后模型拟合效果更佳"
]

print("\n【核心业务结论】:")
for point in summary_points:
    print(f"✓ {point}")

# 进阶改进建议
improvement_suggestions = [
    "1. 模型升级:尝试XGBoost/LightGBM(更高效的梯度提升)、神经网络(捕捉复杂非线性)",
    "2. 特征优化:添加多项式特征、特征选择(剔除低重要性特征)、时间序列特征(如续保年限)",
    "3. 评估优化:使用5折交叉验证(更稳定的性能评估)、分层抽样(保证测试集分布与训练集一致)",
    "4. 超参数调优:网格搜索/随机搜索/贝叶斯优化(提升模型性能上限)",
    "5. 数据扩充:引入收入、职业、病史、医保类型等特征(更全面的风险维度)",
    "6. 部署优化:封装为API接口(Flask/FastAPI)、Docker容器化(环境隔离)、批量预测优化",
    "7. 业务落地:结合客户分群、定价策略仿真、风险预警系统"
]

print("\n【技术进阶建议】:")
for suggestion in improvement_suggestions:
    print(f"• {suggestion}")

print("\n" + "="*60)
print("项目完成!可直接作为保险定价预测的基础模板")
print("="*60)

8.2 完整代码使用说明

  1. 环境配置(一键安装依赖):
    pip install pandas numpy matplotlib seaborn scikit-learn scipy
    
  2. 数据准备:
    • 下载保险数据集并保存为insurance.csv(可从Kaggle获取:https://www.kaggle.com/mirichoi0218/insurance)
    • 确保文件路径与代码同级,或修改pd.read_csv中的路径
  3. 运行流程:
    • 按章节顺序运行代码(数据加载→EDA→特征工程→模型训练→预测)
    • 重点关注模型评估部分的可视化结果,理解模型优劣
  4. 参数调整:
    • 决策树/随机森林可调整max_depth(避免过拟合)
    • 梯度提升可调整n_estimators(树数量)、learning_rate(学习率)

九、核心知识点总结

9.1 关键点回顾

  1. EDA核心:通过可视化+量化分析,明确特征与目标变量的关联,验证业务直觉;
  2. 特征工程:异常值缩尾(而非删除)保留数据价值,对数变换改善分布偏态,交互特征挖掘组合效应;
  3. 模型评估:R²反映解释力,RMSE/MAE反映预测误差,残差图诊断模型偏差;
  4. 业务落地:模型预测需还原原始尺度,特征重要性指导业务决策;
  5. 进阶方向:超参数调优、模型集成、工程化部署是提升项目价值的关键。

本项目完整覆盖了机器学习回归任务的全流程,既兼顾技术深度,又结合保险行业业务逻辑,是从「数据」到「决策」的典型实战案例。


  注: 博主目前收集了6900+份相关数据集,有想要的可以领取部分数据,关注下方公众号或添加微信:

更多推荐