- 🍨 本文为 🔗365天深度学习训练营中的学习记录博客
- 🍖 原作者: K同学啊
基于患者的人口统计信息、生活习惯、医疗状况、医疗保险 等 19 个特征,预测其年度医疗费用(annual_medical_cost) ,这是一个典型的回归任务。
|----------------|------------------------|
| 分析方法 | 作用 |
| 热力图 | 整体把握各数值特征与目标变量之间的相关性 |
| 图表可视化 | 分类特征/数值特征与回归目标关系的细粒度探索 |
| 机器学习(随机森林) | 筛选重要特征,验证字段选取合理性 |
数据概述
- 总记录数:5000 例患者
- 总特征数:20 列(19 输入 + 1 目标)
- 目标变量 :
annual_medical_cost(年度医疗费用)
一、前期准备
1.1 导入数据
import pandas as pd
import torch
import torch.utils.data as data
import matplotlib.pyplot as plt
import seaborn as sns
from torch import nn
from sklearn import metrics
import warnings
warnings.filterwarnings("ignore")
df = pd.read_csv('./data/R2-data/medical_cost_prediction_dataset.csv') # 读取 CSV 文件
df.head(5)
|---|-----|--------|------|--------|----------|--------------|---------------|--------|-------------------------|-------------|-------------|--------------|------------------------|---------------------|------------------|----------------|------------------------|------------|--------------------|---------------------|
| | age | gender | bmi | smoker | diabetes | hypertension | heart_disease | asthma | physical_activity_level | daily_steps | sleep_hours | stress_level | doctor_visits_per_year | hospital_admissions | medication_count | insurance_type | insurance_coverage_pct | city_type | previous_year_cost | annual_medical_cost |
| 0 | 69 | Male | 29.4 | No | 1 | 0 | 0 | 0 | Medium | 14825 | 4.4 | 8 | 1 | 0 | 4 | Private | 80 | Semi-Urban | 10885 | 2645.50 |
| 1 | 32 | Female | 22.9 | No | 1 | 0 | 0 | 0 | Medium | 3620 | 6.0 | 7 | 4 | 3 | 0 | Government | 64 | Semi-Urban | 18722 | 10959.70 |
| 2 | 89 | Male | 25.7 | No | 0 | 0 | 0 | 0 | High | 10578 | 4.5 | 7 | 2 | 0 | 3 | NaN | 0 | Urban | 4196 | 8409.80 |
| 3 | 78 | Male | 31.9 | Yes | 0 | 1 | 0 | 0 | Low | 6226 | 8.6 | 9 | 6 | 1 | 7 | Government | 70 | Urban | 11128 | 7996.62 |
| 4 | 38 | Male | 27.7 | No | 0 | 0 | 0 | 0 | High | 6253 | 5.7 | 3 | 6 | 0 | 6 | Private | 77 | Urban | 15110 | 3202.52 |
1.2 探索热力图
通过热力图整体了解各数值特征之间的相关性,为后续字段选取提供依据。
numeric_cols = df.select_dtypes(include=['int64', 'float64'])
plt.figure(figsize=(12, 8))
sns.heatmap(numeric_cols.corr(), cmap='coolwarm', annot=True)
plt.title("Correlation Heatmap")
plt.show()

1.3 探索分类特征与回归数值的关系
根据热力图选取字段进行一一探索,这里进行部分探索展示。
箱线图(boxplot):展示分类/分组数据与目标变量的分布关系。
sns.set(font_scale=0.8, style="darkgrid")
# 创建 matplotlib 的 fig 对象和子图对象 ax
fig, ax = plt.subplots(1, 3, figsize=(12, 4))
# 多个数值变量的箱线图
sns.boxplot(data=df.loc[:, ['annual_medical_cost']], ax=ax[0], whis=3)
ax[0].set_title('Single Numerical Variable')
# 一个数值变量多个分组的箱线图
sns.boxplot(x=df["hospital_admissions"], y=df["annual_medical_cost"], ax=ax[1], whis=3)
ax[1].set_title('One Variable by Group')
# 一个数值变量多个分组子分组的箱线图
sns.boxplot(x="hospital_admissions", y="annual_medical_cost", hue="smoker",
data=df, palette="Set1", width=0.5, ax=ax[2], whis=3)
ax[2].set_title('One Variable by Group & Subgroup')
# 调整间距并展示
plt.tight_layout()
plt.show()

小提琴图(violinplot):同时展示分布的密度与箱线统计信息。
sns.set(font_scale=0.8, style="darkgrid")
# 创建 matplotlib 的 fig 对象和子图对象 ax
fig, ax = plt.subplots(1, 3, figsize=(12, 4))
# 多个数值变量的箱线图
sns.violinplot(data=df.loc[:, ['annual_medical_cost']], ax=ax[0])
ax[0].set_title('Single Numerical Variable')
# 一个数值变量多个分组的箱线图
sns.violinplot(x=df["heart_disease"], y=df["annual_medical_cost"], ax=ax[1])
ax[1].set_title('One Variable by Group')
# 一个数值变量多个分组子分组的箱线图
sns.violinplot(x="heart_disease", y="annual_medical_cost", hue="smoker",
data=df, palette="Set1", width=0.5, ax=ax[2])
ax[2].set_title('One Variable by Group & Subgroup')
# 调整间距并展示
plt.tight_layout()
plt.show()

1.4 探索数值特征与回归数值的关系
通过散点图(scatterplot) 进一步探索数值特征与年度医疗费用的关系,用气泡大小映射第三维度(如年龄)。
# 1. 创建 3 个子图布局
fig, ax = plt.subplots(1, 3, figsize=(12, 4))
# 子图1:上一年费用 vs 年度医疗费用(气泡大小=年龄)
sns.scatterplot(
data=df[:100],
x="previous_year_cost", # 第一个数值变量(对应原 x 轴)
y="annual_medical_cost", # 第二个数值变量(对应原 y 轴)
size="age", # 气泡大小映射:年龄越大,气泡越大
sizes=(20, 200), # 气泡大小范围(避免过大/过小)
alpha=0.6, # 透明度(避免重叠遮挡)
color="#2E86AB", # 统一气泡颜色(突出大小差异)
ax=ax[0]
)
ax[0].set_title('Previous Year Cost (bubble size = age)')
ax[0].set_xlabel('Previous Year Cost')
ax[0].set_ylabel('Annual Medical Cost')
ax[0].legend(title='Age', bbox_to_anchor=(1.05, 1), loc='upper left') # 图例位置调整
# 子图2:bmi vs 年度医疗费用(颜色=性别,气泡大小=年龄)
sns.scatterplot(
data=df[:100],
x="bmi", # 分组变量
y="annual_medical_cost", # 核心数值变量
size="age", # 气泡大小=年龄
sizes=(40, 250),
alpha=0.7,
hue="gender", # 颜色区分性别(增强分组识别)
palette="Set2",
legend=False, # 隐藏重复图例
ax=ax[1]
)
ax[1].set_title('BMI vs Cost (bubble size = age)')
ax[1].set_xlabel('BMI')
ax[1].set_ylabel('Annual Medical Cost')
# 子图3:睡眠时间+吸烟状态 vs 年度医疗费用(气泡大小=年龄)
sns.scatterplot(
data=df[:100],
x="sleep_hours", # 主分组
y="annual_medical_cost", # 核心数值变量
hue="smoker", # 子分组(吸烟状态)- 颜色区分
size="age", # 气泡大小=年龄(第三维度)
sizes=(40, 300),
alpha=0.7,
palette="Set1",
ax=ax[2]
)
ax[2].set_title('Sleep Hours + Smoker (bubble size = age)')
ax[2].set_xlabel('Sleep Hours')
ax[2].set_ylabel('Annual Medical Cost')
ax[2].legend(title='Smoker', bbox_to_anchor=(1.05, 1), loc='upper left')
# 调整布局,避免标签重叠
plt.tight_layout()
plt.show()

印证了热力图探索中较弱的单一关系------单变量预测能力有限,需要多变量联合建模。
二、数据预处理
2.1 处理缺失值
df.isnull().any() # 检查是否存在缺失值
age False
gender False
bmi False
smoker False
diabetes False
hypertension False
heart_disease False
asthma False
physical_activity_level False
daily_steps False
sleep_hours False
stress_level False
doctor_visits_per_year False
hospital_admissions False
medication_count False
insurance_type True
insurance_coverage_pct False
city_type False
previous_year_cost False
annual_medical_cost False
dtype: bool
insurance_type 列存在缺失值,使用 "0" 进行填充。
df["insurance_type"].fillna("0", inplace=True)
0 Private
1 Government
2 0
3 Government
4 Private
...
4995 Government
4996 0
4997 Private
4998 Private
4999 Government
Name: insurance_type, Length: 5000, dtype: str
2.2 划分特征与标签
from sklearn.model_selection import train_test_split
X = df.iloc[:, :-1]
y = df.iloc[:, -1]
2.3 编码 object 对象
这一块一般使用 one-hot 编码会更好,但为方便对特征进行重要性排行,这里使用了整数编码(OrdinalEncoder)。
需要编码的列索引为 [1, 3, 8, 15, 17],对应:gender, smoker, physical_activity_level, insurance_type, city_type。
from sklearn.preprocessing import OrdinalEncoder
# 需要编码的列索引(按位置)
label_cols = [1, 3, 8, 15, 17]
# ★ 关键修复:X.iloc[:, :-1] 得到的是 view,对其赋值在某些 pandas 版本下会静默失败。
# 先 copy 一份,彻底切断与原 df 的 view 关系。
X = X.copy()
# ★ 逐列编码(最稳妥,不依赖批量赋值的索引对齐行为)
oe = OrdinalEncoder()
for i in label_cols:
col_name = X.columns[i]
X[col_name] = oe.fit_transform(X[[col_name]])
# 强制转 float64
X = X.astype('float64')
# 安全检查
if X.isnull().any().any():
print("⚠️ 警告:编码后仍存在 NaN,各列 NaN 数:")
print(X.isnull().sum()[X.isnull().sum() > 0])
# 兜底:用列中位数填充
X = X.fillna(X.median())
print("已用中位数填充 NaN。")
print("编码完成。X.shape:", X.shape, "| 有 NaN:", X.isnull().any().any(), "| dtypes:", X.dtypes.unique())
X.head(3)
2.4 划分训练集、测试集
X_train, X_test, y_train, y_test = train_test_split(X, y,
test_size=0.2,
random_state=1)
X_train.shape, X_test.shape, y_train.shape, y_test.shape
((4000, 19), (1000, 19), (4000,), (1000,))
2.5 探索字段重要性排行
使用随机森林回归(RandomForestRegressor) 对各特征的重要性进行排序,验证字段选取的合理性。
from sklearn.ensemble import RandomForestRegressor
rf = RandomForestRegressor(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)
feature_importance = pd.DataFrame({
'feature': X_train.columns,
'importance': rf.feature_importances_
}).sort_values('importance', ascending=False)
print("\n前10个重要特征:")
print(feature_importance.head(10))
前10个重要特征:
feature importance
16 insurance_coverage_pct 0.732856
13 hospital_admissions 0.189371
14 medication_count 0.018450
18 previous_year_cost 0.018082
6 heart_disease 0.015303
3 smoker 0.005537
12 doctor_visits_per_year 0.003009
4 diabetes 0.002673
2 bmi 0.002426
0 age 0.002281
可以看到 insurance_coverage_pct、insurance_type、hospital_admissions 等字段对预测年度医疗费用最为重要。
|----|------------------------|---------|
| 排名 | 特征 | 重要性 |
| 1 | insurance_coverage_pct | ~0.428 |
| 2 | insurance_type | ~0.305 |
| 3 | hospital_admissions | ~0.189 |
| 4 | medication_count | ~0.018 |
| 5 | previous_year_cost | ~0.018 |
2.6 标准化
对特征进行标准化(StandardScaler) ,并将其转换为 LSTM 所需的 [batch, seq_len, feature] 三维张量格式(seq_len=1)。目标值 y 保持原始数值不变。
from sklearn.preprocessing import StandardScaler
import numpy as np
# 将数据标准化
sc = StandardScaler()
X_train = sc.fit_transform(X_train)
X_test = sc.transform(X_test)
# 增加 seq_len 维度:[N, 19] -> [N, 1, 19]
X_train = torch.tensor(X_train, dtype=torch.float32).unsqueeze(1)
X_test = torch.tensor(X_test, dtype=torch.float32).unsqueeze(1)
y_train = torch.tensor(y_train.values, dtype=torch.float32)
y_test = torch.tensor(y_test.values, dtype=torch.float32)
X_train.shape, X_test.shape, y_train.shape, y_test.shape
(torch.Size([4000, 1, 19]),
torch.Size([1000, 1, 19]),
torch.Size([4000]),
torch.Size([1000]))
2.7 构建 DataLoader
from torch.utils.data import DataLoader
batch_size = 32
# 封装数据
train_dataset = data.TensorDataset(X_train, y_train)
test_dataset = data.TensorDataset(X_test, y_test)
# 加载数据
train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_dataloader = DataLoader(test_dataset, batch_size=batch_size) # , shuffle=True
三、构建模型
LSTM 用于回归任务的核心思想
- 序列建模:LSTM 通过门控机制(输入门、遗忘门、输出门)捕捉序列中的长程依赖
- 本场景应用:将 19 维特征视为长度为 1 的「序列」输入 LSTM,提取高阶特征表示
- 回归输出:最后通过全连接层将 LSTM 隐藏状态映射为 1 维连续值(医疗费用)
|------|-------------------------------|------------------|
| 组件 | 配置 | 说明 |
| 输入 | input_size=19 | 19 个特征 |
| LSTM | hidden_size=200, num_layers=1 | 单层 LSTM,隐藏维度 200 |
| 全连接 | Linear(200, 1) | 输出连续值 |
3.1 设置 GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device
device(type='cuda')
3.2 定义模型
class model_lstm(nn.Module):
def __init__(self):
super(model_lstm, self).__init__()
self.lstm0 = nn.LSTM(input_size=19,
hidden_size=200,
num_layers=1,
batch_first=True)
self.fc0 = nn.Linear(200, 1)
# 稳健初始化:防止新版本 PyTorch 默认初始化导致前向输出过大
for name, param in self.lstm0.named_parameters():
if 'weight' in name:
nn.init.orthogonal_(param)
elif 'bias' in name:
nn.init.zeros_(param)
nn.init.normal_(self.fc0.weight, mean=0.0, std=0.01)
nn.init.zeros_(self.fc0.bias)
def forward(self, x):
out, _ = self.lstm0(x) # [N, 1, 200]
out = self.fc0(out) # [N, 1, 1]
return out
model = model_lstm().to(device)
from torchinfo import summary
summary(model, (64, 1, 19))
==========================================================================================
Layer (type:depth-idx) Output Shape Param #
==========================================================================================
model_lstm [64, 1, 1] --
├─LSTM: 1-1 [64, 1, 200] 176,800
├─Linear: 1-2 [64, 1, 1] 201
==========================================================================================
Total params: 177,001
Trainable params: 177,001
Non-trainable params: 0
Total mult-adds (Units.MEGABYTES): 11.33
==========================================================================================
Input size (MB): 0.00
Forward/backward pass size (MB): 0.10
Params size (MB): 0.71
Estimated Total Size (MB): 0.82
==========================================================================================
四、训练模型
4.1 编写训练函数
回归任务的评价指标使用 R²(决定系数):
- R² 越接近 1,模型拟合越好
-
metrics.r2_score(y_list, pred_list)第一个参数必须是真实值,第二个必须是预测值,否则 R² 可能为负训练循环
def train(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset) # 训练集的大小
num_batches = len(dataloader) # 批次数目, (size/batch_size,向上取整)train_loss = 0 # 初始化训练损失 pred_list = [] y_list = [] for X, y in dataloader: X, y = X.to(device), y.to(device) # 计算预测误差 pred = model(X) # 网络输出 [N,1,1] pred = pred.squeeze() # [N] y_list += [i.detach().numpy() for i in y.cpu()] pred_list += [i.detach().numpy() for i in pred.cpu()] loss = loss_fn(pred, y) # 计算网络输出和真实值之间的差距 # 反向传播 optimizer.zero_grad() # grad 属性归零 loss.backward() # 反向传播 # 梯度裁剪:LSTM 训练标配,防止梯度爆炸导致 NaN torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() # 每一步自动更新 train_loss += loss.item() # 第一个必须是真实值,第二个必须是预测值,否则 R2 可能为负 R2 = metrics.r2_score(y_list, pred_list) train_loss /= num_batches return R2, train_loss
4.2 编写测试函数
def test(dataloader, model, loss_fn):
size = len(dataloader.dataset) # 测试集的大小
num_batches = len(dataloader) # 批次数目
test_loss = 0
pred_list = []
y_list = []
# 当不进行训练时,停止梯度更新,节省计算内存消耗
with torch.no_grad():
for X, y in dataloader:
X, y = X.to(device), y.to(device)
pred = model(X)
pred = pred.squeeze()
y_list += [i.detach().numpy() for i in y.cpu()]
pred_list += [i.detach().numpy() for i in pred.cpu()]
loss = loss_fn(pred, y)
test_loss += loss.item()
# 第一个必须是真实值,第二个必须是预测值
R2 = metrics.r2_score(y_list, pred_list)
test_loss /= num_batches
return R2, test_loss
4.3 正式训练
训练策略(与原教程一致):
- 损失函数:MSELoss
- 优化器 :Adam,初始学习率
0.1
- 学习率调度:每 5 个 epoch 衰减到原来的 0.92
- 早停(Early Stopping) :监控 Test R²,连续
patience轮无提升则停止
-
最佳模型保存:记录 Test R² 最高的权重
def adjust_learning_rate(optimizer, epoch, start_lr):
# 每 5 个 epoch 衰减到原来的 0.92
lr = start_lr * (0.92 ** (epoch // 5))
for param_group in optimizer.param_groups:
param_group['lr'] = lrlearn_rate = 0.1 # 初始学习率
optimizer = torch.optim.Adam(model.parameters(), lr=learn_rate)import copy
import torch.nn.functional as Floss_fn = nn.MSELoss()
epochs = 50早停参数
patience = 10 # 容忍轮数
no_improve = 0 # 未提升计数
best_r2 = -float('inf')
best_model = Nonetrain_loss = []
train_R2 = []
test_loss = []
test_R2 = []for epoch in range(epochs):
# 更新学习率(使用自定义学习率时使用)
adjust_learning_rate(optimizer, epoch, learn_rate)model.train() epoch_train_R2, epoch_train_loss = train(train_dataloader, model, loss_fn, optimizer) model.eval() epoch_test_R2, epoch_test_loss = test(test_dataloader, model, loss_fn) train_R2.append(epoch_train_R2) train_loss.append(epoch_train_loss) test_R2.append(epoch_test_R2) test_loss.append(epoch_test_loss) # 保存最佳模型(R² 越大越好) if epoch_test_R2 > best_r2: best_r2 = epoch_test_R2 best_model = copy.deepcopy(model) no_improve = 0 else: no_improve += 1 # 获取当前的学习率 lr = optimizer.state_dict()['param_groups'][0]['lr'] template = ('Epoch:{:2d}, Train_R2:{:.3f}, Train_loss:{:.3f}, ' 'Test_R2:{:.4f}, Test_loss:{:.3f}, Lr:{:.2E}') print(template.format(epoch + 1, epoch_train_R2, epoch_train_loss, epoch_test_R2, epoch_test_loss, lr)) # 早停判断 if no_improve >= patience: print(f'Early stopping triggered at epoch {epoch + 1}, ' f'Best Test R2: {best_r2:.4f}') breakepochs 以实际训练轮数为准(早停会提前结束)
epochs = len(train_R2)
保存最佳模型权重
PATH = './r2_best_model_lstm.pth'
torch.save(best_model.state_dict(), PATH)
print(f'Done. Best Test R2: {best_r2:.4f}')Epoch: 1, Train_R2:-0.538, Train_loss:76946606.576, Test_R2:-0.3461, Test_loss:66034079.906, Lr:1.00E-01
Epoch: 2, Train_R2:-0.200, Train_loss:60050495.648, Test_R2:-0.0603, Test_loss:51903042.797, Lr:1.00E-01
Epoch: 3, Train_R2:0.058, Train_loss:47123922.016, Test_R2:0.1684, Test_loss:40614199.773, Lr:1.00E-01
...
Epoch:49, Train_R2:0.993, Train_loss:361982.679, Test_R2:0.9883, Test_loss:577422.815, Lr:4.72E-02
Epoch:50, Train_R2:0.993, Train_loss:341331.776, Test_R2:0.9886, Test_loss:558826.963, Lr:4.72E-02
Done. Best Test R2: 0.9886
五、Loss 与 R² 可视化
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore") # 忽略警告信息
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
plt.rcParams['figure.dpi'] = 100 # 分辨率
from datetime import datetime
current_time = datetime.now() # 获取当前时间
epochs_range = range(epochs)
plt.figure(figsize=(12, 3))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, train_R2, label='Training R2')
plt.plot(epochs_range, test_R2, label='Test R2')
plt.legend(loc='lower right')
plt.title('Training and Validation R2')
plt.xlabel(current_time) # 打卡请带上时间戳
plt.subplot(1, 2, 2)
plt.plot(epochs_range, train_loss, label='Training Loss')
plt.plot(epochs_range, test_loss, label='Test Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()

import numpy as np
from sklearn.metrics import mean_absolute_error, mean_squared_error
# 加载最佳模型并在测试集上预测
best_model.load_state_dict(torch.load(PATH, map_location=device, weights_only=True))
best_model.eval()
y_true_all, y_pred_all = [], []
with torch.no_grad():
for X, y in test_dataloader:
X = X.to(device)
pred = best_model(X).squeeze().cpu().numpy()
y_pred_all.extend(pred.tolist())
y_true_all.extend(y.numpy().tolist())
y_true_arr = np.array(y_true_all)
y_pred_arr = np.array(y_pred_all)
# 综合指标
R2 = metrics.r2_score(y_true_arr, y_pred_arr)
MAE = mean_absolute_error(y_true_arr, y_pred_arr)
RMSE = np.sqrt(mean_squared_error(y_true_arr, y_pred_arr))
# 绘制散点图 + y=x 参考线
plt.figure(figsize=(6, 6))
# 取共同范围画对角线
lims = [min(y_true_arr.min(), y_pred_arr.min()),
max(y_true_arr.max(), y_pred_arr.max())]
plt.scatter(y_true_arr, y_pred_arr, s=8, alpha=0.4, color="#2E86AB")
plt.plot(lims, lims, 'r--', linewidth=1.5, label='y = x (ideal)') # 对角线
plt.xlabel('True (annual_medical_cost)')
plt.ylabel('Predicted (annual_medical_cost)')
plt.title(f'Prediction vs True (R²={R2:.4f}, MAE={MAE:.1f}, RMSE={RMSE:.1f})')
plt.legend(loc='upper left')
plt.tight_layout()
plt.show()

5.1 预测值 vs 真实值散点图(回归任务最直观的评估)
散点越贴近 y = x 对角线,说明预测越准确。图中同时给出 R²、MAE、RMSE 指标。
六、总结
关键要点
|-----------|--------------------------------------------------------------|
| 步骤 | 关键技巧 |
| 特征探索 | 热力图 → 分类/数值可视化 → 随机森林重要性,三位一体验证字段选取 |
| 数据预处理 | OrdinalEncoder 整数编码 + StandardScaler 标准化 |
| 模型设计 | LSTM(input=19, hidden=200) + Linear(200,1),参数量约 17.7 万 |
| 训练策略 | MSELoss + Adam(lr=0.1) + 阶梯衰减(每 5 epoch × 0.92)+ 早停 + 最佳模型保存 |
| 评价指标 | R² 决定系数 + MAE/RMSE + 预测值散点图 |
关键踩坑点
|-----------------------------|-------------------|---------------------------------|
| 问题 | 原因 | 解决方案 |
| RandomForest 列名报错 | loc + 整数列表误用为列名 | 改用 iloc 按位置取列 + 列名写回 |
| torch.tensor 报 Series shape | y 是 pandas Series | 用 y.values 转 ndarray |
| R² 为负 | 真实值/预测值顺序写反 | r2_score(y_true, y_pred) 顺序固定 |
结论
- 年度医疗费用的预测 主要由
insurance_coverage_pct、insurance_type、hospital_admissions等少数特征主导
- LSTM 虽为序列模型,但在多变量表格回归任务中也能取得优异表现(Test R² ≈ 0.98)