5.20 打卡

DAY 31 文件的规范拆分和写法

知识点回顾

  1. 规范的文件命名
  2. 规范的文件夹管理
  3. 机器学习项目的拆分
  4. 编码格式和类型注解

作业:尝试针对之前的心脏病项目,准备拆分的项目文件,思考下哪些部分可以未来复用。

heart_disease_prediction/

├── data/                   # 数据文件夹
│   ├── raw/               # 原始数据
│   │   └── heart.csv      # <-- 你的原始 heart.csv 文件应该放在这里
│   └── processed/         # 处理后的数据或中间结果 (可选)

├── src/                   # 项目源代码目录
│   ├── __init__.py        # 使 src 成为 Python 包
│   ├── config.py          # 项目配置:路径、参数、特征列表等
│   ├── utils.py           # 通用工具函数:如对象保存/加载
│   │
│   ├── data/             # 数据处理相关模块
│   │   ├── __init__.py
│   │   ├── loading.py     # 数据加载和分割
│   │   ├── preprocessing.py # 数据清洗、缺失值处理、编码、缩放
│   │   └── feature_engineering.py # 特征工程 (可能很简单或不需要,但保留结构)
│   │
│   ├── models/           # 模型相关模块
│   │   ├── __init__.py
│   │   ├── training.py    # 模型选择和训练
│   │   └── evaluation.py  # 模型评估
│   │
│   └── visualization/    # 可视化相关模块
│       ├── __init__.py
│       └── plots.py       # 绘制图表函数

├── models/                 # 保存训练好的模型和预处理器
│   └── best_model.pkl     # 训练好的模型
│   └── preprocessor.pkl   # 拟合好的数据预处理器 (重要!)

├── notebooks/            # Jupyter/IPython Notebooks 用于探索性分析 (EDA) 或实验
│   └── heart_eda_modeling_exploration.ipynb

├── main.py                 # 项目主入口:运行完整的训练和评估流程
├── predict.py              # 独立的预测脚本:加载模型对新数据进行预测
├── requirements.txt      # 项目所需的 Python 库列表
└── README.md            # 项目说明、设置和使用指南
1 heart_disease_prediction/src/config.py

# -*- coding: utf-8 -*-

"""
心脏病预测项目配置中心。
存储文件路径、参数、特征列表等。
"""

import os
from typing import List, Dict, Any

# 定义项目根目录,通过当前文件路径向上追溯两级
BASE_DIR: str = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# --- 数据相关配置 ---
RAW_DATA_DIR: str = os.path.join(BASE_DIR, 'data', 'raw')
# 原始数据文件名,请确保你的 heart.csv 放在 data/raw/ 目录下
RAW_DATA_FILE: str = os.path.join(RAW_DATA_DIR, 'heart.csv')
PROCESSED_DATA_DIR: str = os.path.join(BASE_DIR, 'data', 'processed') # 处理后数据保存路径 (可选)

# --- 模型保存相关配置 ---
MODELS_DIR: str = os.path.join(BASE_DIR, 'models')
# 训练好的模型保存路径
TRAINED_MODEL_PATH: str = os.path.join(MODELS_DIR, 'best_model.pkl')
# 拟合好的数据预处理器保存路径 (非常重要,预测时需要用同一个预处理器)
PREPROCESSOR_PATH: str = os.path.join(MODELS_DIR, 'preprocessor.pkl')

# --- 数据处理参数 ---
TARGET_COLUMN: str = 'target' # 目标变量的列名,heart.csv 通常是 'target'
TEST_SIZE: float = 0.2       # 测试集占总数据的比例
RANDOM_STATE: int = 42       # 随机种子,用于保证数据分割、模型初始化等的可复现性

# heart.csv 数据集的特征列表 (请根据你的实际文件核对并修改)
# 数值特征
NUMERICAL_FEATURES: List[str] = [
    'age',        # 年龄
    'trestbps',   # 静息血压
    'chol',       # 血清胆固醇
    'thalach',    # 最大心率
    'oldpeak'     # 运动引起的 ST 段压低
    # 如果有其他数值特征请添加
]

# 类别特征
CATEGORICAL_FEATURES: List[str] = [
    'sex',        # 性别 (0/1)
    'cp',         # 胸痛类型 (1-4)
    'fbs',        # 空腹血糖 > 120 mg/dl (0/1)
    'restecg',    # 静息心电图结果 (0/1/2)
    'exang',      # 运动诱发的心绞痛 (0/1)
    'slope',      # 运动最高峰时段 ST 段的坡度 (0/1/2)
    'ca',         # 主要血管的数量 (0-3)
    'thal'        # 地中海贫血症 (3=正常, 6=固定缺陷, 7=可逆缺陷)
    # 'ca' 和 'thal' 在某些版本的 heart.csv 中可能包含非数字值 ('?'), 需要在预处理中特别处理或清理
    # 如果有其他类别特征请添加
]

# 预处理参数:缺失值填充策略
# heart.csv 通常没有缺失值,但保留这个配置以备用或用于其他数据集
IMPUTATION_STRATEGY_NUM: str = 'median'        # 数值特征缺失值填充策略
IMPUTATION_STRATEGY_CAT: str = 'most_frequent' # 类别特征缺失值填充策略,或使用 'constant' 填充为 'missing'

# --- 模型配置 ---
# 选择要使用的模型名称,需要在 src/models/training.py 中实现对应的获取逻辑
SELECTED_MODEL: str = 'logistic_regression' # 可选 'random_forest', 'svm', 'knn' 等

# 各模型的超参数字典 (如果选择了其他模型,请在此处添加其参数)
LOGISTIC_REGRESSION_PARAMS: Dict[str, Any] = {
    'C': 1.0,
    'solver': 'liblinear', # 适用于小型数据集和二分类
    'random_state': RANDOM_STATE,
    'class_weight': 'balanced' # 对于目标类别不平衡的数据集很有用
}

RANDOM_FOREST_PARAMS: Dict[str, Any] = {
    'n_estimators': 200,      # 森林中的树数量
    'max_depth': 8,           # 树的最大深度,限制模型复杂度
    'random_state': RANDOM_STATE,
    'class_weight': 'balanced' # 对于目标类别不平衡的数据集很有用
}

# --- 评估配置 ---
# 需要计算和报告的评估指标列表
METRICS: List[str] = ['accuracy', 'precision', 'recall', 'f1', 'roc_auc']

# 其他可能的配置项:
# - 特征工程参数
# - 模型超参数调优的参数 (如交叉验证折数)
# - 日志文件路径等

heart_disease_prediction/src/utils.py

# -*- coding: utf-8 -*-

"""
通用工具函数:保存和加载 Python 对象、日志记录等。
"""

import joblib
import os
from typing import Any

def save_object(obj: Any, filepath: str) -> None:
    """
    使用 joblib 将 Python 对象 (如模型、预处理器) 保存到文件。

    Args:
        obj: 要保存的 Python 对象。
        filepath: 保存文件的完整路径。
    """
    # 确保目标目录存在,如果不存在则创建
    os.makedirs(os.path.dirname(filepath), exist_ok=True)
    joblib.dump(obj, filepath)
    print(f"对象已成功保存到 {filepath}")

def load_object(filepath: str) -> Any:
    """
    使用 joblib 从文件加载 Python 对象。

    Args:
        filepath: 要加载的文件的完整路径。

    Returns:
        从文件加载的 Python 对象。

    Raises:
        FileNotFoundError: 如果指定路径的文件不存在。
    """
    if not os.path.exists(filepath):
        raise FileNotFoundError(f"错误:文件未找到:{filepath}")
    obj = joblib.load(filepath)
    print(f"对象已成功从 {filepath} 加载")
    return obj

# 根据需要添加其他通用工具函数,例如:
# def setup_logging(log_filepath: str): ...
# def report_metrics(metrics: Dict[str, float]): ...

heart_disease_prediction/src/data/loading.py

# -*- coding: utf-8 -*-

"""
负责数据加载和初始分割(特征/目标、训练集/测试集)。
"""

import pandas as pd
from sklearn.model_selection import train_test_split
from typing import Tuple

# 从 src.config 导入配置
from src.config import TARGET_COLUMN, TEST_SIZE, RANDOM_STATE, RAW_DATA_FILE

def load_data(filepath: str = RAW_DATA_FILE) -> pd.DataFrame:
    """
    从 CSV 文件加载数据集。

    Args:
        filepath: CSV 数据文件的路径。

    Returns:
        包含数据的 pandas DataFrame。

    Raises:
        FileNotFoundError: 如果数据文件未找到。
        Exception: 读取 CSV 时发生的其他错误。
    """
    print(f"尝试从 {filepath} 加载数据...")
    try:
        df = pd.read_csv(filepath)
        print(f"成功加载数据。数据形状: {df.shape}")
        return df
    except FileNotFoundError:
        print(f"错误:数据文件未找到:{filepath}")
        raise # 重新抛出异常
    except Exception as e:
        print(f"加载数据时发生错误:{e}")
        raise # 重新抛出异常


def split_features_target(df: pd.DataFrame, target_column: str = TARGET_COLUMN) -> Tuple[pd.DataFrame, pd.Series]:
    """
    将 DataFrame 分割为特征 (X) 和目标 (y)。

    Args:
        df: 输入 DataFrame。
        target_column: 目标列的名称。

    Returns:
        一个元组,包含特征 DataFrame (X) 和目标 Series (y)。

    Raises:
        ValueError: 如果目标列未在 DataFrame 中找到。
    """
    print(f"分割特征和目标变量 (目标列: '{target_column}')...")
    if target_column not in df.columns:
        raise ValueError(f"错误:目标列 '{target_column}' 未在 DataFrame 的列中找到。")
    X = df.drop(columns=[target_column])
    y = df[target_column]
    print(f"分割完成。特征形状: {X.shape}, 目标形状: {y.shape}")
    return X, y

def split_train_test(X: pd.DataFrame, y: pd.Series, test_size: float = TEST_SIZE, random_state: int = RANDOM_STATE) -> Tuple[pd.DataFrame, pd.DataFrame, pd.Series, pd.Series]:
    """
    将数据分割为训练集和测试集。

    Args:
        X: 特征 DataFrame。
        y: 目标 Series。
        test_size: 测试集占总数据的比例。
        random_state: 随机种子。

    Returns:
        一个元组,包含 X_train, X_test, y_train, y_test。
    """
    print(f"分割数据为训练集和测试集 (测试集比例: {test_size}, 随机种子: {random_state})...")
    X_train, X_test, y_train, y_test = train_test_split(
        X, y, test_size=test_size, random_state=random_state, stratify=y # 分类问题使用 stratify 保证训练集和测试集中目标类别的比例相似
    )
    print(f"分割完成。训练集样本数: {len(X_train)}, 测试集样本数: {len(X_test)}")
    return X_train, X_test, y_train, y_test

heart_disease_prediction/src/data/preprocessing.py

# -*- coding: utf-8 -*-

"""
负责数据预处理(处理缺失值、编码、缩放)的函数。
使用 scikit-learn 的 Pipeline 和 ColumnTransformer 构建预处理流程。
"""

import pandas as pd
import numpy as np  # 预处理器输出通常是 numpy 数组
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from typing import List, Tuple

# 从 src.config 导入配置
from src.config import NUMERICAL_FEATURES, CATEGORICAL_FEATURES, \
                       IMPUTATION_STRATEGY_NUM, IMPUTATION_STRATEGY_CAT

def create_preprocessor(
    numerical_features: List[str] = NUMERICAL_FEATURES,
    categorical_features: List[str] = CATEGORICAL_FEATURES,
    num_imputer_strategy: str = IMPUTATION_STRATEGY_NUM,
    cat_imputer_strategy: str = IMPUTATION_STRATEGY_CAT
) -> ColumnTransformer:
    """
    创建一个 ColumnTransformer,用于对数值和类别特征应用不同的预处理步骤。

    Args:
        numerical_features: 数值特征列名列表。
        categorical_features: 类别特征列名列表。
        num_imputer_strategy: 数值特征缺失值填充策略。
        cat_imputer_strategy: 类别特征缺失值填充策略。

    Returns:
        一个未拟合的 scikit-learn ColumnTransformer 对象。
    """
    print("创建数据预处理器...")

    # 1. 数值特征处理管道:缺失值填充 -> 标准化
    numerical_pipeline = Pipeline(steps=[
        ('imputer', SimpleImputer(strategy=num_imputer_strategy)), # 填充策略 (如 median, mean)
        ('scaler', StandardScaler())                               # 标准化 (均值为0,方差为1)
    ])

    # 2. 类别特征处理管道:缺失值填充 -> One-Hot 编码
    # 注意:如果 heart.csv 的 'ca'/'thal' 确实有 '?',
    # SimpleImputer 需要设置 missing_values='?'
    categorical_pipeline = Pipeline(steps=[
        ('imputer', SimpleImputer(strategy=cat_imputer_strategy, fill_value='missing')), # 填充策略 (如 most_frequent, constant)
        ('onehot', OneHotEncoder(handle_unknown='ignore')) # One-Hot 编码。handle_unknown='ignore' 在预测时遇到训练集未见过的类别时忽略,而不是报错
    ])

    # 3. 使用 ColumnTransformer 组合不同类型的特征处理管道
    preprocessor = ColumnTransformer(
        transformers=[
            ('num', numerical_pipeline, numerical_features),
            ('cat', categorical_pipeline, categorical_features)
        ],
        remainder='passthrough' # 对于不在 numerical_features 和 categorical_features 列表中的列,不做任何处理直接通过
        # 或者设置为 'drop' 来丢弃未指定的列
    )

    print("数据预处理器创建完成。")
    return preprocessor

def apply_preprocessing(X: pd.DataFrame, preprocessor: ColumnTransformer) -> np.ndarray:
     """
     将拟合好的预处理器应用于特征 DataFrame。

     Args:
         X: 输入特征 DataFrame (例如 X_train, X_test 或新数据)。
         preprocessor: 拟合好的 scikit-learn ColumnTransformer 对象。

     Returns:
         经过预处理的特征的 NumPy 数组。
         (ColumnTransformer 默认输出 NumPy 数组)
     """
     print("应用数据预处理...")
     # 注意:这里只调用 transform,fit 应该只在训练数据上调用一次
     X_processed = preprocessor.transform(X)
     print(f"数据预处理应用完成。输出形状: {X_processed.shape}")
     return X_processed

# 在 main.py 中,你会先调用 preprocessor.fit_transform(X_train) 来拟合并转换训练数据
# 然后调用 preprocessor.transform(X_test) 来转换测试数据 (只转换,不拟合)

heart_disease_prediction/src/data/feature_engineering.py

# -*- coding: utf-8 -*-

"""
负责创建新特征的函数。
对于 heart.csv 数据集,通常特征已经比较精简,可能不需要复杂的特征工程。
保留这个文件结构,以便未来扩展或用于其他数据集。
"""

import pandas as pd
from typing import List

# 可以从 config 导入特征列表,虽然在这个简单示例中可能不需要
# from src.config import NUMERICAL_FEATURES, CATEGORICAL_FEATURES

def create_features(df: pd.DataFrame) -> pd.DataFrame:
    """
    基于现有特征创建新特征。

    Args:
        df: 输入 DataFrame (训练集或测试集,包含所有原始列)。

    Returns:
        添加了新特征(或原始)的 DataFrame。
    """
    print("进行特征工程 (如果需要)...")

    # 在这里添加你的特征工程逻辑。例如:
    # - 创建交互特征 (如 age 和 chol 的乘积)
    # - 对某些特征进行分箱 (如 age 分为老年/中年/青年)
    # - 处理 'ca' 和 'thal' 中的特殊字符串/缺失值(如果 load_data 或 preprocessing 没有处理)

    # 示例:创建一个简单的交互特征 (假设 age 和 chol 列存在)
    # if 'age' in df.columns and 'chol' in df.columns:
    #     df['age_x_chol'] = df['age'] * df['chol']
    #     print("创建了特征 'age_x_chol'")

    # 对于标准的 heart.csv 数据集,通常不需要复杂的特征工程,原始特征效果就不错。
    # 此处保留函数框架,但默认不进行任何特征创建,直接返回原始 DataFrame。
    # 如果你的具体 heart.csv 版本需要特征工程,请在此处添加代码。

    print(f"特征工程完成 (可能未创建新特征)。当前形状: {df.shape}")

    # 如果创建了新特征,**非常重要**:
    # 1. 确定新特征的类型 (数值或类别)。
    # 2. **更新 src/config.py 中的 NUMERICAL_FEATURES 或 CATEGORICAL_FEATURES 列表**,以便预处理器能够处理这些新特征。
    #    或者,设计 preprocessor 更灵活地识别所有数值/类别列。

    return df

# 可以添加特征选择、降维等其他与特征相关的函数
# def select_features(X: pd.DataFrame) -> pd.DataFrame: ...

heart_disease_prediction/src/models/training.py

# -*- coding: utf-8 -*-

"""
负责模型选择和训练的函数。
"""

import pandas as pd # 目标变量 y 通常是 pandas Series
import numpy as np  # 特征 X 经过预处理后通常是 numpy 数组
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
# 根据 config.py 中 SELECTED_MODEL 的设置,导入相应的模型类
from sklearn.base import BaseEstimator # 用于类型提示,表示一个 scikit-learn 估计器

# 从 src.config 导入配置
from src.config import SELECTED_MODEL, LOGISTIC_REGRESSION_PARAMS, RANDOM_FOREST_PARAMS, RANDOM_STATE

def get_model(model_name: str = SELECTED_MODEL) -> BaseEstimator:
    """
    根据模型名称获取指定的模型实例,并设置预定义参数。

    Args:
        model_name: 要实例化模型名称 ('logistic_regression', 'random_forest' 等)。
                    名称应与 config.py 中 SELECTED_MODEL 保持一致,并在本函数中有对应的实现。

    Returns:
        一个未拟合的 scikit-learn 模型实例。

    Raises:
        ValueError: 如果模型名称不受支持。
    """
    print(f"获取模型:{model_name}...")
    if model_name == 'logistic_regression':
        model = LogisticRegression(**LOGISTIC_REGRESSION_PARAMS)
    elif model_name == 'random_forest':
        model = RandomForestClassifier(**RANDOM_FOREST_PARAMS)
    # TODO: 在这里添加对其他模型名称的支持及其参数
    # elif model_name == 'svm':
    #    from sklearn.svm import SVC
    #    model = SVC(**SVM_PARAMS) # 需要在 config.py 中定义 SVM_PARAMS
    # elif model_name == 'knn':
    #    from sklearn.neighbors import KNeighborsClassifier
    #    model = KNeighborsClassifier(**KNN_PARAMS) # 需要在 config.py 中定义 KNN_PARAMS
    else:
        raise ValueError(f"错误:模型 '{model_name}' 不受支持。请检查 config.py 或在 get_model 函数中添加其实现。")

    print(f"模型 {type(model).__name__} 已创建。")
    return model

def train_model(model: BaseEstimator, X_train: np.ndarray, y_train: pd.Series) -> BaseEstimator:
    """
    使用训练数据拟合模型。

    Args:
        model: 未拟合的 scikit-learn 模型实例。
        X_train: 训练集的特征(经过预处理的 NumPy 数组)。
        y_train: 训练集的目标变量(pandas Series)。

    Returns:
        拟合好的 scikit-learn 模型实例。
    """
    print(f"训练模型 {type(model).__name__}...")
    model.fit(X_train, y_train)
    print("模型训练完成。")
    return model

# 可以添加超参数调优、交叉验证等相关的函数
# def tune_hyperparameters(model: BaseEstimator, X_train, y_train): ...

heart_disease_prediction/src/models/evaluation.py

# -*- coding: utf-8 -*-

"""
负责模型性能评估的函数。
"""

import pandas as pd
import numpy as np
from sklearn.metrics import (
    accuracy_score,
    precision_score,
    recall_score,
    f1_score,
    roc_auc_score,
    confusion_matrix, # 可选用于混淆矩阵
    roc_curve         # 可选用于 ROC 曲线数据
)
from typing import Dict, List, Union

# 从 src.config 导入需要报告的评估指标列表
from src.config import METRICS

def evaluate_model(
    y_true: pd.Series,        # 真实标签
    y_pred: np.ndarray,       # 预测标签
    y_proba: np.ndarray,      # 预测概率 (通常是 (n_samples, n_classes) 形状)
    metrics_list: List[str] = METRICS
) -> Dict[str, float]:
    """
    使用指定的指标列表评估模型性能。

    Args:
        y_true: 真实的标签值 (pandas Series)。
        y_pred: 预测的标签值 (NumPy 数组)。
        y_proba: 预测的概率分数 (NumPy 数组)。对于二分类,通常是 (n_samples, 2) 形状。
                 第一个维度是每个样本,第二个维度是属于每个类别的概率 [P(class 0), P(class 1)]。
        metrics_list: 要计算的指标名称列表 ('accuracy', 'precision', 'recall', 'f1', 'roc_auc' 等)。

    Returns:
        一个字典,键是指标名称,值是计算出的分数。如果计算失败,值为 float('nan')。
    """
    print("评估模型性能...")
    metrics: Dict[str, float] = {}

    # 提取正类的概率 (假设正类是 1)
    # y_proba 形状通常是 (n_samples, n_classes)。对于二分类,取列索引 1 的概率。
    y_proba_positive_class = None
    if y_proba.ndim == 2 and y_proba.shape[1] == 2:
         y_proba_positive_class = y_proba[:, 1]
    elif y_proba.ndim == 1 and 'roc_auc' in metrics_list:
         # 如果只请求了 roc_auc 并且 y_proba 是一维的,假设它已经是正类概率
         y_proba_positive_class = y_proba
         print("警告:y_proba 是一维数组,假设其为正类概率,用于 ROC AUC 计算。")
    elif 'roc_auc' in metrics_list:
         print(f"警告:y_proba 形状 {y_proba.shape} 异常,无法计算 ROC AUC。")


    for metric_name in metrics_list:
        try:
            if metric_name == 'accuracy':
                score = accuracy_score(y_true, y_pred)
            elif metric_name == 'precision':
                # 对于二分类,默认计算正类 (pos_label=1) 的精度
                score = precision_score(y_true, y_pred, zero_division=0) # zero_division=0 在没有预测出正类时返回 0
            elif metric_name == 'recall':
                 # 对于二分类,默认计算正类 (pos_label=1) 的召回率
                score = recall_score(y_true, y_pred, zero_division=0) # zero_division=0 在真实正类数为 0 时返回 0
            elif metric_name == 'f1':
                 # 对于二分类,默认计算正类 (pos_label=1) 的 F1 分数
                score = f1_score(y_true, y_pred, zero_division=0) # zero_division=0 在没有真实正类或没有预测出正类时返回 0
            elif metric_name == 'roc_auc':
                if y_proba_positive_class is not None:
                    score = roc_auc_score(y_true, y_proba_positive_class)
                else:
                    score = float('nan') # 无法计算 ROC AUC
            # TODO: 在这里添加对其他指标的支持
            # elif metric_name == 'log_loss':
            #     if y_proba.ndim == 2:
            #          from sklearn.metrics import log_loss
            #          score = log_loss(y_true, y_proba)
            #     else:
            #          score = float('nan')
            else:
                print(f"警告:不支持的评估指标 '{metric_name}'。")
                metrics[metric_name] = float('nan') # 标记为 NaN 或跳过
                continue

            metrics[metric_name] = score

        except Exception as e:
            print(f"计算指标 '{metric_name}' 时发生错误: {e}")
            metrics[metric_name] = float('nan') # 标记计算失败

    print("评估完成。指标结果:")
    # 打印每个指标的结果
    for metric_name, score in metrics.items():
         print(f"- {metric_name}: {score:.4f}" if not np.isnan(score) else f"- {metric_name}: 计算失败")

    return metrics

# 可以添加其他评估相关的函数,例如交叉验证分数计算等

heart_disease_prediction/src/visualization/plots.py

# -*- coding: utf-8 -*-

"""
负责生成各种可视化图表的函数。
"""

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
from sklearn.metrics import roc_curve, confusion_matrix, RocCurveDisplay # 导入 RocCurveDisplay 更方便绘制 ROC

# 从 src.config 导入目标列名称
from src.config import TARGET_COLUMN

def plot_feature_distribution(df: pd.DataFrame, feature: str, target: str = TARGET_COLUMN) -> None:
    """
    绘制单个特征的分布图,可选按目标变量分组。

    Args:
        df: 输入 DataFrame。
        feature: 要绘制分布的特征名称。
        target: 目标列名称 (用于 hue)。
    """
    print(f"绘制特征 '{feature}' 的分布图...")
    plt.figure(figsize=(10, 6))
    # 判断特征类型,选择合适的绘图方式
    if df[feature].dtype in ['int64', 'float64']:
        # 数值特征使用直方图和 KDE (核密度估计)
        sns.histplot(data=df, x=feature, hue=target, kde=True, stat='density', common_norm=False)
        # common_norm=False 使每个类别(心脏病 vs 无心脏病)的 KDE 曲线独立标准化,更易比较分布形状
    else:
        # 类别特征使用计数图
        # order=df[feature].value_counts().index 按照类别数量排序绘制
        sns.countplot(data=df, x=feature, hue=target, order=df[feature].value_counts().index)
        plt.xticks(rotation=45, ha='right') # 旋转 x 轴标签避免重叠

    plt.title(f'特征 "{feature}" 按 "{target}" 分组的分布')
    plt.xlabel(feature)
    plt.ylabel('密度' if df[feature].dtype in ['int64', 'float64'] else '数量')
    plt.tight_layout() # 调整布局,防止元素重叠
    plt.show()

def plot_roc_curve(y_true: pd.Series, y_proba: np.ndarray, model_name: str = "模型") -> None:
    """
    绘制 ROC 曲线。

    Args:
        y_true: 真实的标签值 (pandas Series)。
        y_proba: 预测的概率分数 (NumPy 数组)。对于二分类,应包含正类 (1) 的概率。
        model_name: 模型名称,用于图表标题/图例。
    """
    print(f"绘制模型 '{model_name}' 的 ROC 曲线...")
    # 确保 y_proba 是针对正类 (标签为 1) 的概率
    y_proba_positive_class = None
    if y_proba.ndim == 2 and y_proba.shape[1] == 2:
         y_proba_positive_class = y_proba[:, 1] # 取第二列作为正类概率 (通常标签 1 是正类)
    elif y_proba.ndim == 1:
         y_proba_positive_class = y_proba # 假设输入的一维数组就是正类概率
    else:
         print("错误:y_proba 形状异常,无法提取正类概率绘制 ROC 曲线。")
         return # 无法绘制,直接返回

    # 使用 RocCurveDisplay 可以简化绘制过程并自动计算 AUC
    # from_predictions 需要真实标签和预测概率
    try:
        roc_display = RocCurveDisplay.from_predictions(y_true, y_proba_positive_class, name=model_name)
        plt.figure(figsize=(8, 8))
        roc_display.plot(ax=plt.gca()) # 将绘制结果添加到当前 matplotlib 轴上

        plt.plot([0, 1], [0, 1], 'k--', label='随机猜测') # 绘制对角线作为随机猜测的参照
        plt.xlim([0.0, 1.0])
        plt.ylim([0.0, 1.05])
        plt.xlabel('假阳性率 (False Positive Rate)')
        plt.ylabel('真阳性率 (True Positive Rate)')
        plt.title(f'接收者操作特征 (ROC) 曲线 - {model_name}')
        plt.legend(loc="lower right")
        plt.grid(True)
        plt.show()
    except Exception as e:
        print(f"绘制 ROC 曲线时发生错误: {e}")


def plot_confusion_matrix(y_true: pd.Series, y_pred: np.ndarray, classes: List[str] = ['无心脏病', '有心脏病']) -> None:
    """
    绘制混淆矩阵。

    Args:
        y_true: 真实的标签值 (pandas Series)。
        y_pred: 预测的标签值 (NumPy 数组)。
        classes: 类别名称列表,用于混淆矩阵的行和列标签。
                 例如对于 heart.csv,[0, 1] 对应 ['无心脏病', '有心脏病']。
    """
    print("绘制混淆矩阵...")
    cm = confusion_matrix(y_true, y_pred)
    plt.figure(figsize=(8, 6))
    # 使用 seaborn 的 heatmap 绘制混淆矩阵热力图
    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=classes, yticklabels=classes)
    plt.title('混淆矩阵')
    plt.xlabel('预测标签')
    plt.ylabel('真实标签')
    plt.show()

# 根据需要添加其他绘图函数(如特征重要性图、校准曲线、数据分布图等)

heart_disease_prediction/main.py

# -*- coding: utf-8 -*-

"""
主脚本:协调心脏病预测项目的整个训练和评估流程。
依次执行:加载数据 -> 特征工程 -> 分割数据 -> 预处理 -> 模型训练 -> 模型评估 -> 保存结果。
"""

import pandas as pd
import numpy as np
import sys
import os

# 将项目根目录下的 src 目录添加到 Python 解释器的路径中,
# 这样就可以使用 from src.module import ... 方式导入
# os.path.dirname(__file__) 获取当前文件 (main.py) 的目录
# os.path.abspath(...) 获取绝对路径
# os.path.join(...) 拼接路径
# os.path.dirname(...) 获取父目录
# 所以 os.path.abspath(os.path.join(os.path.dirname(__file__), 'src')) 就是 src 目录的绝对路径
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'src')))

# 从 src 模块导入所需的函数和配置
from src.config import (
    RAW_DATA_FILE,        # 原始数据文件路径
    TRAINED_MODEL_PATH,   # 模型保存路径
    PREPROCESSOR_PATH,    # 预处理器保存路径
    SELECTED_MODEL,       # 选择的模型名称
    TARGET_COLUMN,        # 目标列名称 (在 split_features_target 内部使用,但导入一下,方便检查)
    METRICS               # 评估指标列表 (在 evaluate_model 内部使用,但导入一下,方便检查)
)
# 从 src.data 子包导入数据处理相关函数
from src.data.loading import load_data, split_features_target, split_train_test
from src.data.preprocessing import create_preprocessor, apply_preprocessing
from src.data.feature_engineering import create_features # 导入特征工程函数
# 从 src.models 子包导入模型相关函数
from src.models.training import get_model, train_model
from src.models.evaluation import evaluate_model
# 从 src.visualization 子包导入绘图函数
from src.visualization.plots import plot_roc_curve, plot_confusion_matrix
# 从 src.utils 导入工具函数
from src.utils import save_object # 导入保存对象的函数

def main():
    print("--- 启动心脏病预测训练流程 ---")

    # --- 1. 加载数据 ---
    try:
        df = load_data(RAW_DATA_FILE)
    except FileNotFoundError:
        print("数据加载失败,请检查 config.py 中的 RAW_DATA_FILE 路径和文件是否存在。退出流程。")
        sys.exit(1) # 数据文件未找到时立即退出脚本
    except Exception as e:
        print(f"数据加载时发生意外错误:{e}。退出流程。")
        sys.exit(1)

    # --- 2. 特征工程 ---
    # 注意:通常特征工程在分割之前应用到整个数据集,以保持一致性。
    # 如果你的特征工程依赖时间顺序或其他只应在训练集上学习的方面,则需要在分割后分别处理训练集和测试集。
    df_engineered = create_features(df.copy()) # 对数据副本进行操作,避免修改原始 DataFrame

    # --- 3. 分割特征和目标变量 ---
    try:
        X, y = split_features_target(df_engineered)
    except ValueError as e:
        print(f"特征/目标分割失败:{e}。请检查 config.py 中的 TARGET_COLUMN。退出流程。")
        sys.exit(1)

    # --- 4. 分割训练集和测试集 ---
    X_train, X_test, y_train, y_test = split_train_test(X, y)

    # --- 5. 创建并拟合预处理器 ---
    # 创建预处理器对象
    preprocessor = create_preprocessor()
    # **在训练集上**拟合预处理器,并转换训练集
    X_train_processed = preprocessor.fit_transform(X_train)
    # 使用**已经拟合好的**预处理器转换测试集 (只调用 transform)
    X_test_processed = preprocessor.transform(X_test)

    print("预处理完成。")

    # --- 6. 获取并训练模型 ---
    try:
        model = get_model(SELECTED_MODEL)
    except ValueError as e:
        print(f"模型获取失败:{e}。请检查 config.py 中的 SELECTED_MODEL。退出流程。")
        sys.exit(1)

    trained_model = train_model(model, X_train_processed, y_train)

    # --- 7. 评估模型 ---
    print("\n--- 在测试集上进行模型评估 ---")
    # 使用训练好的模型对测试集进行预测
    y_pred = trained_model.predict(X_test_processed)
    # 获取预测概率,用于计算 ROC AUC 等指标
    # predict_proba 方法返回一个形状为 (n_samples, n_classes) 的数组
    y_proba = trained_model.predict_proba(X_test_processed)

    # 调用评估函数计算指标并打印结果
    evaluation_metrics = evaluate_model(y_test, y_pred, y_proba, METRICS)

    # --- 8. 可选:绘制评估图表 ---
    print("\n--- 绘制评估图表 ---")
    # 绘制混淆矩阵
    plot_confusion_matrix(y_test, y_pred, classes=['无心脏病', '有心脏病'])
    # 绘制 ROC 曲线,确保传递正类概率 (通常是 y_proba[:, 1])
    plot_roc_curve(y_test, y_proba, model_name=SELECTED_MODEL)

    # --- 9. 保存训练好的模型和预处理器 ---
    print("\n--- 保存模型和预处理器 ---")
    try:
        save_object(trained_model, TRAINED_MODEL_PATH)
        save_object(preprocessor, PREPROCESSOR_PATH) # 保存拟合好的预处理器!
    except Exception as e:
        print(f"保存模型或预处理器时发生错误:{e}")


    print("\n--- 心脏病预测训练流程结束 ---")
    # 最终指标已在 evaluate_model 中打印,这里可以根据需要再次总结或写入日志文件。
    # print("测试集最终评估指标:", evaluation_metrics)


if __name__ == "__main__":
    # 当 main.py 脚本直接运行时,执行 main() 函数
    main()

10 heart_disease_prediction/predict.py

# -*- coding: utf-8 -*-

"""
独立的预测脚本:
加载之前训练好的模型和预处理器,对新的心脏病数据进行预测。
这个脚本模拟了将模型部署到生产环境进行推理的过程。
"""

import pandas as pd
import numpy as np
import sys
import os
from typing import Any # 用于类型提示加载的对象

# 将项目根目录下的 src 目录添加到 Python 解释器的路径中
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'src')))

# 导入所需的组件和配置
from src.config import (
    TRAINED_MODEL_PATH,   # 保存的模型路径
    PREPROCESSOR_PATH,    # 保存的预处理器路径
    TARGET_COLUMN         # 目标列名称 (用于在新数据中检查并移除)
)
from src.data.feature_engineering import create_features # 预测时也需要应用相同的特征工程
from src.utils import load_object # 导入加载对象的函数
# from src.models.evaluation import evaluate_model # 如果新数据包含标签,可以导入用于评估

def load_new_data(filepath: str) -> pd.DataFrame:
    """
    加载用于预测的新数据。

    Args:
        filepath: 新数据 CSV 文件的路径。

    Returns:
        包含新数据的 pandas DataFrame。

    Raises:
        FileNotFoundError: 如果数据文件未找到。
        Exception: 读取 CSV 时发生的其他错误。
    """
    print(f"尝试从 {filepath} 加载新数据用于预测...")
    try:
        df = pd.read_csv(filepath)
        print(f"成功加载新数据。数据形状: {df.shape}")
        return df
    except FileNotFoundError:
        print(f"错误:新数据文件未找到:{filepath}")
        raise # 重新抛出异常
    except Exception as e:
        print(f"加载新数据时发生错误:{e}")
        raise # 重新抛出异常


def make_prediction(
    data: pd.DataFrame,        # 包含新数据的 DataFrame
    model: Any,                # 加载的、已训练好的模型对象
    preprocessor: Any          # 加载的、已拟合好的预处理器对象
) -> np.ndarray:
    """
    对新数据应用与训练时相同的预处理步骤,并使用加载的模型进行预测。

    Args:
        data: 输入的、包含新数据特征的 DataFrame。
        model: 加载的、已训练好的模型对象 (实现了 .predict() 方法)。
        preprocessor: 加载的、已拟合好的数据预处理器对象 (实现了 .transform() 方法)。

    Returns:
        一个 NumPy 数组,包含每个样本的预测类别标签 (通常是 0 或 1)。
    """
    print("启动预测过程...")

    # --- 1. 应用与训练时相同的特征工程 ---
    # 对输入数据创建一个副本,避免修改原始 DataFrame
    data_for_prediction = create_features(data.copy())

    # --- 2. 确保目标列不存在 ---
    # 如果新数据意外地包含了目标列,需要先移除它,因为模型只接受特征作为输入
    if TARGET_COLUMN in data_for_prediction.columns:
        print(f"警告:预测数据中检测到目标列 '{TARGET_COLUMN}'。正在移除。")
        data_for_prediction = data_for_prediction.drop(columns=[TARGET_COLUMN])

    # --- 3. 应用加载的预处理器 ---
    # **重要:** 在预测时,只调用 preprocessor 的 `transform()` 方法。
    # 绝对不要在这里调用 `fit()` 或 `fit_transform()`,因为预处理器必须使用训练集的数据分布进行拟合。
    print("应用加载的预处理器对新数据进行转换...")
    try:
        data_processed = preprocessor.transform(data_for_prediction)
        print(f"数据预处理完成。转换后形状: {data_processed.shape}")
    except Exception as e:
         print(f"应用预处理器时发生错误:{e}。请检查新数据的列是否与训练数据兼容。")
         raise # 无法继续预测,抛出异常

    # --- 4. 使用加载的模型进行预测 ---
    print("使用训练好的模型进行预测...")
    try:
        predictions = model.predict(data_processed)
        print("预测完成。")
    except Exception as e:
         print(f"使用模型进行预测时发生错误:{e}。")
         raise # 预测失败,抛出异常


    return predictions

def main():
    # 定义新数据文件的路径 (请替换为你的实际新数据文件路径)
    # 假设你的新数据文件名为 new_heart_data.csv,放在 data/raw/ 目录下
    NEW_DATA_FILE = os.path.join(os.path.dirname(__file__), 'data', 'raw', 'new_heart_data.csv')

    # --- 1. 加载用于预测的新数据 ---
    try:
        new_data_df = load_new_data(NEW_DATA_FILE)
    except FileNotFoundError:
        print("加载新数据失败,请检查文件路径。退出预测。")
        sys.exit(1)
    except Exception as e:
        print(f"加载新数据时发生错误:{e}。退出预测。")
        sys.exit(1)


    # --- 2. 加载之前训练好的模型和预处理器 ---
    print("\n--- 加载训练好的模型和预处理器 ---")
    try:
        # 从保存的路径加载模型和预处理器对象
        trained_model = load_object(TRAINED_MODEL_PATH)
        fitted_preprocessor = load_object(PREPROCESSOR_PATH)
    except FileNotFoundError:
        print("错误:未找到保存的模型或预处理器文件。请先运行 main.py 进行训练并保存模型。退出预测。")
        sys.exit(1)
    except Exception as e:
        print(f"加载模型或预处理器时发生错误:{e}。退出预测。")
        sys.exit(1)

    # --- 3. 对新数据进行预测 ---
    try:
        predictions = make_prediction(new_data_df, trained_model, fitted_preprocessor)
    except Exception as e:
        print(f"进行预测时发生错误:{e}。退出预测。")
        sys.exit(1)


    # --- 4. 输出或处理预测结果 ---
    print("\n--- 预测结果 ---")
    # predictions 是一个 numpy 数组,包含了对应于 new_data_df 中每一行的预测结果
    # 你可以将预测结果添加到原始新数据 DataFrame 中,或者保存到新的 CSV 文件
    # 为了演示,我们只打印前10个预测结果和总预测数
    print("前 10 个预测结果 (0: 无心脏病, 1: 有心脏病):", predictions[:10])
    print(f"总共为 {len(predictions)} 个样本进行了预测。")

    # 示例:将预测结果添加到原始新数据 DataFrame 并打印前几行
    # 如果你想保留原始列,可以这样做
    new_data_df_with_predictions = new_data_df.copy()
    new_data_df_with_predictions['predicted_target'] = predictions
    print("\n带预测结果的新数据(前 5 行):")
    print(new_data_df_with_predictions.head())

    # 示例:将带有预测结果的新数据保存到文件
    # output_filepath = os.path.join(os.path.dirname(__file__), 'data', 'processed', 'heart_predictions.csv')
    # try:
    #     new_data_df_with_predictions.to_csv(output_filepath, index=False)
    #     print(f"\n带有预测结果的新数据已保存到 {output_filepath}")
    # except Exception as e:
    #     print(f"保存预测结果到文件时发生错误:{e}")

    # --- 可选:如果新数据包含真实标签,可以评估预测性能 ---
    # (这在实际生产环境中通常没有,但如果你有带标签的新的测试集可以用来验证)
    # if TARGET_COLUMN in new_data_df.columns:
    #      print("\n--- 在新数据上评估预测性能 ---")
    #      y_true_new = new_data_df[TARGET_COLUMN]
    #      # 需要获取新数据对应的预测概率来计算 AUC 等指标
    #      # 注意:这里的 input data 必须是没有目标列、经过特征工程、经过预处理的
    #      new_data_for_proba = create_features(new_data_df.drop(columns=[TARGET_COLUMN])) # 移除目标列,进行特征工程
    #      new_data_for_proba_processed = fitted_preprocessor.transform(new_data_for_proba) # 应用预处理器
    #      y_proba_new = trained_model.predict_proba(new_data_for_proba_processed)
    #      # 导入 evaluate_model 并调用
    #      from src.models.evaluation import evaluate_model
    #      print("评估指标:")
    #      evaluate_model(y_true_new, predictions, y_proba_new)


if __name__ == "__main__":
    # 当 predict.py 脚本直接运行时,执行 main() 函数
    main()

你可能感兴趣的:(人工智能)