# 毕业设计:基于随机森林、XGBoost 和 LightGBM 的心脏病症状预测数据分析系统
---
## 一、项目背景与意义
心血管疾病是全球死亡率最高的疾病之一,早期发现和干预至关重要。通过机器学习模型对患者临床数据进行分析,可以有效预测其是否患有心脏病,辅助医生做出诊断决策。
本毕业设计旨在构建一个**基于随机森林(Random Forest)、XGBoost 和 LightGBM 的多模型对比预测系统**,使用公开的心脏病数据集(如 UCI Heart Disease Dataset),实现数据预处理、特征工程、模型训练、评估与可视化,并开发一个简易 Web 系统供用户输入信息后获得预测结果。
---
## 二、技术栈
- **编程语言**:Python
- **机器学习库**:
- `scikit-learn`(随机森林)
- `xgboost`
- `lightgbm`
- **数据处理**:`pandas`, `numpy`
- **可视化**:`matplotlib`, `seaborn`
- **Web 后端框架**:Flask
- **前端界面**:HTML/CSS/JavaScript(简单表单)
- **部署方式**:本地运行或轻量级服务器
---
## 三、数据集介绍
我们使用 [UCI Machine Learning Repository - Heart Disease Dataset](https://archive.ics.uci.edu/ml/datasets/Heart+Disease)
### 数据来源:
> https://archive.ics.uci.edu/ml/machine-learning-databases/heart-disease/
### 字段说明(共14个字段):
| 特征 | 描述 |
|------|------|
| age | 年龄 |
| sex | 性别 (0=女性, 1=男性) |
| cp | 胸痛类型 (0-3) |
| trestbps | 静息血压 (mm Hg) |
| chol | 血清胆固醇 (mg/dl) |
| fbs | 空腹血糖 > 120 mg/dl (0=否, 1=是) |
| restecg | 静息心电图 (0-2) |
| thalach | 最大心率 |
| exang | 运动诱发心绞痛 (0=无, 1=有) |
| oldpeak | 相对于休息的 ST 抑制程度 |
| slope | ST 段斜率 (0-2) |
| ca | 主要血管数量(荧光染色)(0-3) |
| thal | 地中海贫血(3=正常; 6=固定缺陷; 7=可逆缺陷) |
| target | 是否患心脏病 (0=否, 1=是) |
> 注:原始数据来自四个数据库(Cleveland, Hungary, Switzerland, Long Beach VA),通常只用 Cleveland 子集(303 条记录,14 字段)
---
## 四、完整代码实现
我们将分模块编写代码:
```
project/
│
├── data/
│ └── heart.csv
├── models/
│ ├── rf_model.pkl
│ ├── xgb_model.pkl
│ └── lgb_model.pkl
├── app.py # Flask 主程序
├── train.py # 模型训练脚本
├── utils.py # 工具函数(绘图等)
└── templates/
└── index.html # 前端页面
```
---
### ✅ 第一步:安装依赖
```bash
pip install flask pandas numpy scikit-learn xgboost lightgbm matplotlib seaborn joblib
```
---
### ✅ 第二步:`train.py` —— 数据加载 + 模型训练 + 保存
```python
# train.py
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
from sklearn.preprocessing import StandardScaler
import joblib
import os
# 创建目录
os.makedirs("models", exist_ok=True)
# 加载数据(假设已下载为 heart.csv)
df = pd.read_csv("data/heart.csv")
# 查看基本信息
print("数据形状:", df.shape)
print(df.head())
print("缺失值:\n", df.isnull().sum())
# 分离特征与标签
X = df.drop(columns=['target'])
y = df['target']
# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
# 标准化数值型特征(可选,对树模型影响较小但有助于比较)
scaler = StandardScaler()
num_cols = ['age', 'trestbps', 'chol', 'thalach', 'oldpeak']
X_train[num_cols] = scaler.fit_transform(X_train[num_cols])
X_test[num_cols] = scaler.transform(X_test[num_cols])
# 保存标准化器
joblib.dump(scaler, "models/scaler.pkl")
# 初始化三个模型
rf_model = RandomForestClassifier(n_estimators=100, random_state=42)
xgb_model = XGBClassifier(use_label_encoder=False, eval_metric='logloss', random_state=42)
lgb_model = LGBMClassifier(random_state=42)
# 训练模型
print("\n开始训练模型...")
rf_model.fit(X_train, y_train)
xgb_model.fit(X_train, y_train)
lgb_model.fit(X_train, y_train)
# 测试集预测
rf_pred = rf_model.predict(X_test)
xgb_pred = xgb_model.predict(X_test)
lgb_pred = lgb_model.predict(X_test)
# 输出性能指标
def evaluate_model(name, y_true, y_pred):
acc = accuracy_score(y_true, y_pred)
print(f"\n{name} 准确率: {acc:.4f}")
print(f"{name} 分类报告:")
print(classification_report(y_true, y_pred))
return acc
acc_rf = evaluate_model("随机森林", y_test, rf_pred)
acc_xgb = evaluate_model("XGBoost", y_test, xgb_pred)
acc_lgb = evaluate_model("LightGBM", y_test, lgb_pred)
# 选择最佳模型保存为主模型(这里以准确率为标准)
best_acc = max(acc_rf, acc_xgb, acc_lgb)
if best_acc == acc_rf:
best_model = rf_model
best_name = "RandomForest"
elif best_acc == acc_xgb:
best_model = xgb_model
best_name = "XGBoost"
else:
best_model = lgb_model
best_name = "LightGBM"
print(f"\n✅ 最佳模型: {best_name}, 准确率: {best_acc:.4f}")
# 保存所有模型
joblib.dump(rf_model, "models/rf_model.pkl")
joblib.dump(xgb_model, "models/xgb_model.pkl")
joblib.dump(lgb_model, "models/lgb_model.pkl")
joblib.dump(best_model, "models/best_model.pkl")
# 特征重要性可视化准备
import matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(10,6))
feat_importance = pd.Series(best_model.feature_importances_, index=X.columns).sort_values(ascending=False)
sns.barplot(x=feat_importance.values, y=feat_importance.index)
plt.title("特征重要性排序(基于最佳模型)")
plt.xlabel("重要性")
plt.tight_layout()
plt.savefig("static/feature_importance.png")
plt.close()
print("✅ 所有模型训练完成并保存!")
```
#### 🔍 代码解释:
- 使用 `pandas` 加载 CSV 数据。
- 自动识别缺失值并划分训练/测试集。
- 对连续变量进行标准化(虽然树模型不敏感,但保持一致性)。
- 训练三种模型并输出分类报告。
- 将模型使用 `joblib` 保存到磁盘以便后续调用。
- 绘制特征重要性图用于展示。
---
### ✅ 第三步:`utils.py` —— 可视化工具
```python
# utils.py
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
from sklearn.metrics import confusion_matrix
import os
def plot_confusion_matrices(y_test, rf_pred, xgb_pred, lgb_pred):
models = ["Random Forest", "XGBoost", "LightGBM"]
preds = [rf_pred, xgb_pred, lgb_pred]
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
for i, (name, pred) in enumerate(zip(models, preds)):
cm = confusion_matrix(y_test, pred)
sns.heatmap(cm, annot=True, fmt="d", ax=axes[i], cmap="Blues")
axes[i].set_title(f"{name} 混淆矩阵")
axes[i].set_ylabel("真实")
axes[i].set_xlabel("预测")
plt.tight_layout()
os.makedirs("static", exist_ok=True)
plt.savefig("static/confusion_matrices.png")
plt.close()
def plot_accuracy_comparison(acc_rf, acc_xgb, acc_lgb):
df_acc = pd.DataFrame({
'Model': ['Random Forest', 'XGBoost', 'LightGBM'],
'Accuracy': [acc_rf, acc_xgb, acc_lgb]
})
plt.figure(figsize=(8, 5))
sns.barplot(data=df_acc, x='Model', y='Accuracy', palette='viridis')
plt.title('各模型准确率对比')
plt.ylim(0.7, 1.0)
for idx, row in df_acc.iterrows():
plt.text(idx, row.Accuracy + 0.005, f"{row.Accuracy:.3f}", ha='center')
plt.tight_layout()
plt.savefig("static/accuracy_comparison.png")
plt.close()
```
> 在 `train.py` 中最后添加以下内容来生成图表:
```python
# 添加在 train.py 末尾
from utils import plot_confusion_matrices, plot_accuracy_comparison
plot_confusion_matrices(y_test, rf_pred, xgb_pred, lgb_pred)
plot_accuracy_comparison(acc_rf, acc_xgb, acc_lgb)
```
---
### ✅ 第四步:`app.py` —— Flask Web 接口
```python
# app.py
from flask import Flask, request, render_template, jsonify
import joblib
import numpy as np
import pandas as pd
import os
app = Flask(__name__)
app.config['SECRET_KEY'] = 'heart_disease_2025'
# 全局变量存储模型和 scaler
model_rf = None
model_xgb = None
model_lgb = None
best_model = None
scaler = None
model_names = {
'rf': '随机森林',
'xgb': 'XGBoost',
'lgb': 'LightGBM'
}
@app.before_first_request
def load_models():
global model_rf, model_xgb, model_lgb, best_model, scaler
try:
model_rf = joblib.load("models/rf_model.pkl")
model_xgb = joblib.load("models/xgb_model.pkl")
model_lgb = joblib.load("models/lgb_model.pkl")
best_model = joblib.load("models/best_model.pkl")
scaler = joblib.load("models/scaler.pkl")
print("✅ 模型加载成功")
except Exception as e:
print("❌ 模型加载失败:", str(e))
@app.route('/')
def index():
return render_template('index.html')
@app.route('/predict', methods=['POST'])
def predict():
try:
# 获取前端传来的 JSON 数据
data = request.get_json()
# 构造输入 DataFrame
input_data = pd.DataFrame([{
'age': float(data['age']),
'sex': int(data['sex']),
'cp': int(data['cp']),
'trestbps': float(data['trestbps']),
'chol': float(data['chol']),
'fbs': int(data['fbs']),
'restecg': int(data['restecg']),
'thalach': float(data['thalach']),
'exang': int(data['exang']),
'oldpeak': float(data['oldpeak']),
'slope': int(data['slope']),
'ca': int(data['ca']),
'thal': int(data['thal'])
}])
# 标准化数值列
num_cols = ['age', 'trestbps', 'chol', 'thalach', 'oldpeak']
input_data[num_cols] = scaler.transform(input_data[num_cols])
# 多模型预测
pred_rf = model_rf.predict(input_data)[0]
prob_rf = model_rf.predict_proba(input_data)[0].tolist()
pred_xgb = model_xgb.predict(input_data)[0]
prob_xgb = model_xgb.predict_proba(input_data)[0].tolist()
pred_lgb = model_lgb.predict(input_data)[0]
prob_lgb = model_lgb.predict_proba(input_data)[0].tolist()
result = {
'success': True,
'predictions': [
{
'model': '随机森林',
'prediction': int(pred_rf),
'probability': [round(p, 3) for p in prob_rf]
},
{
'model': 'XGBoost',
'prediction': int(pred_xgb),
'probability': [round(p, 3) for p in prob_xgb]
},
{
'model': 'LightGBM',
'prediction': int(pred_lgb),
'probability': [round(p, 3) for p in prob_lgb]
}
],
'final_decision': int(best_model.predict(input_data)[0]),
'final_prob': best_model.predict_proba(input_data)[0].max().round(3)
}
return jsonify(result)
except Exception as e:
return jsonify({'success': False, 'error': str(e)})
if __name__ == '__main__':
if not os.path.exists("models/best_model.pkl"):
print("⚠️ 请先运行 train.py 训练模型!")
else:
app.run(debug=True)
```
#### 🔍 解释:
- 使用 `@before_first_request` 加载所有模型一次。
- `/predict` 接收 JSON 请求,执行三个模型预测并返回概率。
- 返回最终决策基于“最佳模型”的预测结果。
- 支持错误捕获。
---
### ✅ 第五步:`templates/index.html` —— 用户交互界面
```html
<!-- templates/index.html -->
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>心脏病预测系统</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<style>
body { font-family: Arial, sans-serif; margin: 40px; background: #f4f6f9; }
.container { max-width: 800px; margin: auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
h1 { text-align: center; color: #d9534f; }
label { display: block; margin-top: 15px; font-weight: bold; }
input, select { width: 100%; padding: 8px; margin-top: 5px; border: 1px solid #ccc; border-radius: 4px; }
button { margin-top: 20px; padding: 10px 20px; background: #0275d8; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:hover { background: #014a8f; }
.result { margin-top: 20px; padding: 15px; border: 1px solid #ddd; border-radius: 5px; background: #f9f9f9; }
.danger { color: #d9534f; font-weight: bold; }
.safe { color: #5cb85c; font-weight: bold; }
</style>
</head>
<body>
<div id="app" class="container">
<h1>❤️ 心脏病风险预测系统</h1>
<form @submit.prevent="predict">
<label>年龄:</label>
<input v-model.number="age" type="number" required />
<label>性别 (0=女, 1=男):</label>
<select v-model.number="sex" required>
<option :value="0">女</option>
<option :value="1">男</option>
</select>
<label>胸痛类型 (0-3):</label>
<input v-model.number="cp" type="number" min="0" max="3" required />
<label>静息血压 (mm Hg):</label>
<input v-model.number="trestbps" type="number" required />
<label>血清胆固醇 (mg/dl):</label>
<input v-model.number="chol" type="number" required />
<label>空腹血糖 >120mg/dl (0=否, 1=是):</label>
<select v-model.number="fbs">
<option :value="0">否</option>
<option :value="1">是</option>
</select>
<label>静息心电图 (0-2):</label>
<input v-model.number="restecg" type="number" min="0" max="2" required />
<label>最大心率:</label>
<input v-model.number="thalach" type="number" required />
<label>运动诱发心绞痛 (0=无, 1=有):</label>
<select v-model.number="exang">
<option :value="0">无</option>
<option :value="1">有</option>
</select>
<label>ST 抑制程度 (oldpeak):</label>
<input v-model.number="oldpeak" type="number" step="0.1" required />
<label>ST 斜率 (0-2):</label>
<input v-model.number="slope" type="number" min="0" max="2" required />
<label>主要血管数 (0-3):</label>
<input v-model.number="ca" type="number" min="0" max="3" required />
<label>地中海贫血 (3=正常, 6=固定, 7=可逆):</label>
<input v-model.number="thal" type="number" required />
<button type="submit">预测心脏病风险</button>
</form>
<div v-if="result" class="result">
<h3>📊 预测结果</h3>
<p><strong>最终判断:</strong>
<span v-if="result.final_decision" class="danger">高风险 ⚠️</span>
<span v-else class="safe">低风险 ✅</span>
</p>
<p>患病概率: {{ result.final_prob * 100 }}%</p>
<table border="1" cellpadding="8" style="width:100%; margin-top:10px;">
<tr><th>模型</th><th>预测</th><th>健康概率</th><th>患病概率</th></tr>
<tr v-for="p in result.predictions">
<td>{{ p.model }}</td>
<td>{{ p.prediction ? '高风险' : '低风险' }}</td>
<td>{{ p.probability[0].toFixed(3) }}</td>
<td>{{ p.probability[1].toFixed(3) }}</td>
</tr>
</table>
</div>
</div>
<script>
new Vue({
el: '#app',
data() {
return {
age: 60,
sex: 1,
cp: 1,
trestbps: 140,
chol: 250,
fbs: 0,
restecg: 1,
thalach: 160,
exang: 0,
oldpeak: 1.5,
slope: 1,
ca: 1,
thal: 3,
result: null
}
},
methods: {
predict() {
fetch('/predict', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.$data)
})
.then(res => res.json())
.then(data => {
if (data.success) {
this.result = data;
} else {
alert("预测失败:" + data.error);
}
})
.catch(err => {
alert("请求出错:" + err.message);
});
}
}
});
</script>
</body>
</html>
```
#### 🔍 功能特点:
- 使用 Vue.js 实现动态表单绑定。
- 提交后发送 AJAX 请求到 `/predict`。
- 显示每个模型的预测结果及概率。
- 最终结论由最优模型决定。
---
## 五、运行步骤
1. 下载数据集并放入 `data/heart.csv`
- 可从 [Kaggle Heart Disease Dataset](https://www.kaggle.com/datasets/johnsmith88/heart-disease-dataset) 获取清理好的版本
2. 安装依赖
3. 运行训练:
```bash
python train.py
```
4. 启动 Web 服务:
```bash
python app.py
```
5. 浏览器访问:`http://localhost:5000`
---
## 六、预期成果截图(描述)
- **首页**:美观的表单界面
- **预测结果页**:显示三大模型预测结果表格 + 最终建议
- **后台图表**:
- 特征重要性图(thalach、ca、oldpeak 最重要)
- 准确率对比柱状图
- 混淆矩阵热力图
---
## 七、创新点与扩展建议
### ✅ 创新点:
- 多模型集成对比 + 可视化分析
- 支持实时在线预测
- 使用应用工厂思想便于部署
- 提供概率输出而非单一判断
### 🔧 扩展方向:
- 添加模型解释(SHAP 值分析)
- 支持 CSV 批量上传预测
- 增加用户注册/历史记录功能
- 部署为 Docker 容器 + Nginx + Gunicorn
- 移动端适配(响应式设计)
---