跳转至

项目1: 回归分析实战

难度: ⭐⭐ 中等 时间: 8-12小时 涉及知识: 线性回归、多项式回归、正则化、模型评估


📖 项目概述

项目背景

回归分析是机器学习中最基础也是最重要的任务之一,用于预测连续值。广泛应用于房价预测、股票价格预测、销量预测等场景。本项目将带你从零开始构建一个完整的回归分析系统。

项目目标

构建一个完整的回归分析系统,能够: - 处理和分析数据 - 实现多种回归算法 - 进行特征工程 - 评估模型性能 - 可视化结果 - 部署模型为Web服务

技术栈

  • 机器学习框架: scikit-learn
  • 数据处理: pandas, numpy
  • 可视化: matplotlib, seaborn
  • Web框架: Streamlit

🏗️ 项目结构

Text Only
regression-analysis/
├── data/                      # 数据目录
│   ├── raw/                  # 原始数据
│   ├── processed/            # 处理后数据
│   └── features/            # 特征数据
├── models/                   # 模型目录
│   ├── __init__.py
│   ├── linear.py            # 线性回归
│   ├── polynomial.py        # 多项式回归
│   └── regularized.py      # 正则化回归
├── utils/                    # 工具函数
│   ├── __init__.py
│   ├── data_preprocessing.py # 数据预处理
│   ├── feature_engineering.py # 特征工程
│   ├── model_evaluation.py  # 模型评估
│   └── visualization.py    # 可视化
├── train.py                  # 训练脚本
├── evaluate.py               # 评估脚本
├── app.py                    # Web应用
├── config.py                 # 配置文件
└── requirements.txt          # 依赖文件

🎯 核心功能

1. 数据预处理

  • 数据加载: 加载CSV/Excel数据
  • 缺失值处理: 填充或删除缺失值
  • 异常值检测: 识别和处理异常值
  • 数据标准化: 标准化/归一化

2. 特征工程

  • 特征选择: 选择重要特征
  • 特征变换: 对数变换、Box-Cox变换
  • 多项式特征: 生成交互项
  • 特征编码: 独热编码、标签编码

3. 回归模型

  • 线性回归: 基础线性回归
  • 多项式回归: 非线性关系建模
  • Ridge回归: L2正则化
  • Lasso回归: L1正则化
  • ElasticNet: L1+L2正则化

4. 模型评估

  • MSE: 均方误差
  • RMSE: 均方根误差
  • MAE: 平均绝对误差
  • : 决定系数
  • 交叉验证: K折交叉验证

5. 结果可视化

  • 散点图: 实际值vs预测值
  • 残差图: 残差分析
  • 学习曲线: 训练过程可视化
  • 特征重要性: 特征重要性可视化

💻 代码实现

1. 配置文件 (config.py)

Python
"""
回归分析配置文件
"""
from dataclasses import dataclass

@dataclass  # @dataclass自动生成__init__等方法
class Config:
    """配置类"""

    # 数据配置
    data_dir: str = "./data"
    train_size: float = 0.8
    random_state: int = 42

    # 特征工程配置
    polynomial_degree: int = 2
    feature_selection_method: str = "all"  # all, k_best, rfe
    n_features: int = 10

    # 模型配置
    model_type: str = "linear"  # linear, ridge, lasso, elasticnet
    alpha: float = 1.0  # 正则化系数
    l1_ratio: float = 0.5  # ElasticNet的L1比例

    # 训练配置
    cv_folds: int = 5
    scoring: str = "neg_mean_squared_error"

    # 可视化配置
    figsize: tuple[int, int] = (12, 8)
    style: str = "seaborn-v0_8"  # matplotlib 3.6+ 中 "seaborn" 已弃用

    # 保存配置
    model_dir: str = "./models"
    plot_dir: str = "./plots"

config = Config()

2. 数据预处理 (utils/data_preprocessing.py)

Python
"""
数据预处理
"""
from typing import Tuple
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from sklearn.impute import SimpleImputer

class DataPreprocessor:
    """数据预处理器"""

    def __init__(self, config):  # __init__构造方法,创建对象时自动调用
        """初始化预处理器"""
        self.config = config
        self.scaler = None
        self.imputer = None

    def load_data(self, filepath) -> pd.DataFrame:
        """
        加载数据(支持文件路径或Streamlit UploadedFile对象)

        Args:
            filepath: 文件路径(str)或Streamlit UploadedFile对象

        Returns:
            数据框
        """
        # 兼容Streamlit的UploadedFile对象(使用.name属性获取文件名)
        filename = filepath.name if hasattr(filepath, 'name') else filepath
        if filename.endswith('.csv'):
            return pd.read_csv(filepath)
        elif filename.endswith('.xlsx'):
            return pd.read_excel(filepath)
        else:
            raise ValueError("不支持的文件格式")

    def handle_missing_values(
        self,
        df: pd.DataFrame,
        strategy: str = "mean",
    ) -> pd.DataFrame:
        """
        处理缺失值

        Args:
            df: 数据框
            strategy: 填充策略 (mean, median, most_frequent)

        Returns:
            处理后的数据框
        """
        self.imputer = SimpleImputer(strategy=strategy)
        df_imputed = pd.DataFrame(
            self.imputer.fit_transform(df),
            columns=df.columns,
            index=df.index,
        )
        return df_imputed

    def detect_outliers(
        self,
        df: pd.DataFrame,
        column: str,
        method: str = "iqr",
        threshold: float = 1.5,
    ) -> pd.Series:
        """
        检测异常值

        Args:
            df: 数据框
            column: 列名
            method: 检测方法 (iqr, zscore)
            threshold: 阈值

        Returns:
            异常值布尔序列
        """
        if method == "iqr":
            Q1 = df[column].quantile(0.25)
            Q3 = df[column].quantile(0.75)
            IQR = Q3 - Q1

            lower_bound = Q1 - threshold * IQR
            upper_bound = Q3 + threshold * IQR

            return (df[column] < lower_bound) | (df[column] > upper_bound)

        elif method == "zscore":
            z_scores = np.abs((df[column] - df[column].mean()) / df[column].std())
            return z_scores > threshold

    def remove_outliers(
        self,
        df: pd.DataFrame,
        column: str,
        method: str = "iqr",
        threshold: float = 1.5,
    ) -> pd.DataFrame:
        """
        移除异常值

        Args:
            df: 数据框
            column: 列名
            method: 检测方法
            threshold: 阈值

        Returns:
            处理后的数据框
        """
        outliers = self.detect_outliers(df, column, method, threshold)
        return df[~outliers]

    def normalize_data(
        self,
        df: pd.DataFrame,
        method: str = "standard",
    ) -> pd.DataFrame:
        """
        标准化数据

        Args:
            df: 数据框
            method: 标准化方法 (standard, minmax)

        Returns:
            标准化后的数据框
        """
        if method == "standard":
            self.scaler = StandardScaler()
        elif method == "minmax":
            self.scaler = MinMaxScaler()
        else:
            raise ValueError("不支持的标准化方法")

        df_normalized = pd.DataFrame(
            self.scaler.fit_transform(df),
            columns=df.columns,
            index=df.index,
        )
        return df_normalized

    def split_data(
        self,
        X: pd.DataFrame,
        y: pd.Series,
    ) -> Tuple:
        """
        划分数据集

        Args:
            X: 特征
            y: 目标

        Returns:
            训练集和测试集
        """
        return train_test_split(
            X, y,
            train_size=self.config.train_size,
            random_state=self.config.random_state,
        )

3. 线性回归模型 (models/linear.py)

Python
"""
线性回归模型
"""
import numpy as np
from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score
from sklearn.model_selection import cross_val_score

class LinearRegressionModel:
    """线性回归模型"""

    def __init__(self, model_type="linear", **kwargs):  # *args接收任意位置参数,**kwargs接收任意关键字参数
        """
        初始化模型

        Args:
            model_type: 模型类型
            **kwargs: 模型参数
        """
        self.model_type = model_type

        if model_type == "linear":
            self.model = LinearRegression(**kwargs)
        elif model_type == "ridge":
            self.model = Ridge(**kwargs)
        elif model_type == "lasso":
            self.model = Lasso(**kwargs)
        elif model_type == "elasticnet":
            self.model = ElasticNet(**kwargs)
        else:
            raise ValueError(f"不支持的模型类型: {model_type}")

    def fit(self, X_train, y_train):
        """
        训练模型

        Args:
            X_train: 训练特征
            y_train: 训练目标
        """
        self.model.fit(X_train, y_train)

    def predict(self, X):
        """
        预测

        Args:
            X: 特征

        Returns:
            预测值
        """
        return self.model.predict(X)

    def evaluate(self, X_test, y_test):
        """
        评估模型

        Args:
            X_test: 测试特征
            y_test: 测试目标

        Returns:
            评估指标字典
        """
        y_pred = self.predict(X_test)

        metrics = {
            "mse": mean_squared_error(y_test, y_pred),
            "rmse": np.sqrt(mean_squared_error(y_test, y_pred)),
            "mae": mean_absolute_error(y_test, y_pred),
            "r2": r2_score(y_test, y_pred),
        }

        return metrics

    def cross_validate(self, X, y, cv=5):
        """
        交叉验证

        Args:
            X: 特征
            y: 目标
            cv: 折数

        Returns:
            交叉验证分数
        """
        scores = cross_val_score(
            self.model, X, y,
            cv=cv,
            scoring="neg_mean_squared_error",
        )
        return -scores

    def get_coefficients(self):
        """
        获取系数

        Returns:
            系数数组
        """
        if hasattr(self.model, 'coef_'):  # hasattr检查对象是否有指定属性
            return self.model.coef_
        else:
            return None

    def get_intercept(self):
        """
        获取截距

        Returns:
            截距值
        """
        if hasattr(self.model, 'intercept_'):
            return self.model.intercept_
        else:
            return None

4. 多项式回归 (models/polynomial.py)

Python
"""
多项式回归
"""
import numpy as np
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline

class PolynomialRegression:
    """多项式回归"""

    def __init__(self, degree=2):
        """
        初始化模型

        Args:
            degree: 多项式次数
        """
        self.degree = degree
        self.model = Pipeline([
            ('poly', PolynomialFeatures(degree=degree)),
            ('linear', LinearRegression()),
        ])

    def fit(self, X_train, y_train):
        """
        训练模型

        Args:
            X_train: 训练特征
            y_train: 训练目标
        """
        self.model.fit(X_train, y_train)

    def predict(self, X):
        """
        预测

        Args:
            X: 特征

        Returns:
            预测值
        """
        return self.model.predict(X)

    def evaluate(self, X_test, y_test):
        """
        评估模型

        Args:
            X_test: 测试特征
            y_test: 测试目标

        Returns:
            评估指标字典
        """
        y_pred = self.predict(X_test)

        from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score

        metrics = {
            "mse": mean_squared_error(y_test, y_pred),
            "rmse": np.sqrt(mean_squared_error(y_test, y_pred)),
            "mae": mean_absolute_error(y_test, y_pred),
            "r2": r2_score(y_test, y_pred),
        }

        return metrics

5. 可视化工具 (utils/visualization.py)

Python
"""
可视化工具
"""
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np

def plot_actual_vs_predicted(
    y_true: np.ndarray,
    y_pred: np.ndarray,
    figsize: tuple[int, int] = (10, 6),
):
    """
    绘制实际值vs预测值散点图

    Args:
        y_true: 实际值
        y_pred: 预测值
        figsize: 图形大小
    """
    plt.figure(figsize=figsize)

    # 绘制散点图
    plt.scatter(y_true, y_pred, alpha=0.5)

    # 绘制对角线
    min_val = min(y_true.min(), y_pred.min())
    max_val = max(y_true.max(), y_pred.max())
    plt.plot([min_val, max_val], [min_val, max_val], 'r--', lw=2)

    plt.xlabel('实际值', fontsize=12)
    plt.ylabel('预测值', fontsize=12)
    plt.title('实际值 vs 预测值', fontsize=14)
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.show()

def plot_residuals(
    y_true: np.ndarray,
    y_pred: np.ndarray,
    figsize: tuple[int, int] = (10, 6),
):
    """
    绘制残差图

    Args:
        y_true: 实际值
        y_pred: 预测值
        figsize: 图形大小
    """
    residuals = y_true - y_pred

    plt.figure(figsize=figsize)

    plt.subplot(1, 2, 1)
    plt.scatter(y_pred, residuals, alpha=0.5)
    plt.axhline(y=0, color='r', linestyle='--')
    plt.xlabel('预测值', fontsize=12)
    plt.ylabel('残差', fontsize=12)
    plt.title('残差 vs 预测值', fontsize=14)
    plt.grid(True, alpha=0.3)

    plt.subplot(1, 2, 2)
    plt.hist(residuals, bins=30, edgecolor='black', alpha=0.7)
    plt.xlabel('残差', fontsize=12)
    plt.ylabel('频数', fontsize=12)
    plt.title('残差分布', fontsize=14)
    plt.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.show()

def plot_feature_importance(
    feature_names: list,
    importance: np.ndarray,
    top_n: int = 10,
    figsize: tuple[int, int] = (10, 6),
):
    """
    绘制特征重要性

    Args:
        feature_names: 特征名称列表
        importance: 重要性数组
        top_n: 显示前N个特征
        figsize: 图形大小
    """
    # 排序
    indices = np.argsort(importance)[::-1][:top_n]

    plt.figure(figsize=figsize)
    plt.bar(range(len(indices)), importance[indices])
    plt.xticks(range(len(indices)), [feature_names[i] for i in indices], rotation=45)  # 列表推导式,简洁创建列表
    plt.xlabel('特征', fontsize=12)
    plt.ylabel('重要性', fontsize=12)
    plt.title(f'前{top_n}个重要特征', fontsize=14)
    plt.tight_layout()
    plt.show()

def plot_learning_curve(
    train_scores: np.ndarray,
    val_scores: np.ndarray,
    figsize: tuple[int, int] = (10, 6),
):
    """
    绘制学习曲线

    Args:
        train_scores: 训练分数
        val_scores: 验证分数
        figsize: 图形大小
    """
    plt.figure(figsize=figsize)

    epochs = range(1, len(train_scores) + 1)

    plt.plot(epochs, train_scores, 'b-', label='训练集')
    plt.plot(epochs, val_scores, 'r-', label='验证集')

    plt.xlabel('Epoch', fontsize=12)
    plt.ylabel('分数', fontsize=12)
    plt.title('学习曲线', fontsize=14)
    plt.legend(fontsize=12)
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.show()

6. Streamlit应用 (app.py)

Python
"""
回归分析Web应用
"""
import streamlit as st
import pandas as pd
import numpy as np
import joblib
import os

from config import config
from utils.data_preprocessing import DataPreprocessor
from models.linear import LinearRegressionModel
from utils.visualization import plot_actual_vs_predicted, plot_residuals

# 页面配置
st.set_page_config(
    page_title="回归分析系统",
    page_icon="📊",
    layout="wide"
)

# 标题
st.title("📊 回归分析系统")
st.markdown("---")

# 侧边栏
st.sidebar.header("模型设置")

# 模型选择
model_type = st.sidebar.selectbox(
    "选择模型",
    ["linear", "ridge", "lasso", "elasticnet"],
    format_func=lambda x: {  # lambda匿名函数,简洁定义单行函数
        "linear": "线性回归",
        "ridge": "Ridge回归",
        "lasso": "Lasso回归",
        "elasticnet": "ElasticNet",
    }[x],
)

# 正则化系数
if model_type in ["ridge", "lasso", "elasticnet"]:
    alpha = st.sidebar.slider(
        "正则化系数 (α)",
        min_value=0.01,
        max_value=10.0,
        value=1.0,
        step=0.1,
    )
else:
    alpha = 1.0

# ElasticNet的L1比例
if model_type == "elasticnet":
    l1_ratio = st.sidebar.slider(
        "L1比例",
        min_value=0.0,
        max_value=1.0,
        value=0.5,
        step=0.1,
    )
else:
    l1_ratio = 0.5

# 主界面
col1, col2 = st.columns(2)

with col1:
    st.subheader("数据上传")

    # 上传数据
    uploaded_file = st.file_uploader(
        "上传CSV或Excel文件",
        type=['csv', 'xlsx'],
    )

    if uploaded_file is not None:
        # 加载数据
        preprocessor = DataPreprocessor(config)
        df = preprocessor.load_data(uploaded_file)

        st.write("数据预览:")
        st.dataframe(df.head())

        # 选择目标列
        target_column = st.selectbox(
            "选择目标列",
            df.columns,
        )

        # 选择特征列
        feature_columns = st.multiselect(
            "选择特征列",
            [col for col in df.columns if col != target_column],
            default=[col for col in df.columns if col != target_column][:5],
        )

        if st.button("训练模型", type="primary"):
            if not feature_columns:
                st.warning("请至少选择一个特征列")
            else:
                # 准备数据
                X = df[feature_columns]
                y = df[target_column]

                # 划分数据
                X_train, X_test, y_train, y_test = preprocessor.split_data(X, y)

                # 训练模型
                with st.spinner("正在训练模型..."):
                    if model_type == "elasticnet":
                        model = LinearRegressionModel(
                            model_type=model_type,
                            alpha=alpha,
                            l1_ratio=l1_ratio,
                        )
                    else:
                        model = LinearRegressionModel(
                            model_type=model_type,
                            alpha=alpha,
                        )

                    model.fit(X_train, y_train)

                    # 评估模型
                    metrics = model.evaluate(X_test, y_test)

                    # 显示结果
                    with col2:
                        st.subheader("模型评估")

                        # 显示指标
                        st.metric("MSE", f"{metrics['mse']:.4f}")
                        st.metric("RMSE", f"{metrics['rmse']:.4f}")
                        st.metric("MAE", f"{metrics['mae']:.4f}")
                        st.metric("R²", f"{metrics['r2']:.4f}")

                        # 预测
                        y_pred = model.predict(X_test)

                        # 可视化
                        st.subheader("结果可视化")

                        # 实际值vs预测值
                        plot_actual_vs_predicted(y_test.values, y_pred)

                        # 残差图
                        plot_residuals(y_test.values, y_pred)

                        # 特征重要性
                        if model.get_coefficients() is not None:
                            st.subheader("特征重要性")

                            importance = np.abs(model.get_coefficients())
                            plot_feature_importance(
                                feature_columns,
                                importance,
                                top_n=min(10, len(feature_columns)),
                            )

🧪 测试方法

1. 单元测试

Python
"""
单元测试示例
"""
import pytest
import numpy as np
from models.linear import LinearRegressionModel

def test_linear_regression():
    """测试线性回归"""
    # 创建模型
    model = LinearRegressionModel(model_type="linear")

    # 创建测试数据
    X_train = np.random.randn(100, 5)
    y_train = X_train @ np.array([1, 2, 3, 4, 5]) + np.random.randn(100) * 0.1  # np.array创建NumPy数组

    X_test = np.random.randn(20, 5)
    y_test = X_test @ np.array([1, 2, 3, 4, 5]) + np.random.randn(20) * 0.1

    # 训练
    model.fit(X_train, y_train)

    # 预测
    y_pred = model.predict(X_test)

    # 验证
    assert y_pred.shape == y_test.shape
    metrics = model.evaluate(X_test, y_test)
    assert metrics['r2'] > 0.5  # R²应该大于0.5

    print("✓ 线性回归测试通过")

2. 集成测试

Python
"""
集成测试示例
"""
def test_regression_pipeline():
    """测试回归流程"""
    from config import config
    from utils.data_preprocessing import DataPreprocessor

    # 创建预处理器
    preprocessor = DataPreprocessor(config)

    # 创建测试数据
    df = pd.DataFrame({
        'feature1': np.random.randn(100),
        'feature2': np.random.randn(100),
        'feature3': np.random.randn(100),
        'target': np.random.randn(100),
    })

    # 处理数据
    X = df[['feature1', 'feature2', 'feature3']]
    y = df['target']

    X_train, X_test, y_train, y_test = preprocessor.split_data(X, y)

    # 训练模型
    model = LinearRegressionModel(model_type="linear")
    model.fit(X_train, y_train)

    # 评估
    metrics = model.evaluate(X_test, y_test)

    # 验证
    assert 'mse' in metrics
    assert 'rmse' in metrics
    assert 'mae' in metrics
    assert 'r2' in metrics

    print("✓ 回归流程测试通过")

📊 扩展建议

1. 功能扩展

  • 时间序列: ARIMA, Prophet
  • 集成方法: 随机森林, XGBoost
  • 贝叶斯回归: 贝叶斯线性回归
  • Gaussian Process: 高斯过程回归

2. 性能优化

  • 特征选择: 递归特征消除
  • 超参数调优: GridSearch, RandomSearch
  • 模型融合: Stacking, Blending
  • 自动ML: Auto-sklearn

3. 部署优化

  • 模型导出: ONNX格式
  • API服务: FastAPI
  • 批量预测: 批量处理
  • 缓存机制: 结果缓存

📚 学习收获

完成本项目后,你将掌握:

  • ✅ 回归分析原理和算法
  • ✅ 特征工程技术
  • ✅ 正则化方法
  • ✅ 模型评估指标
  • ✅ 可视化技巧
  • ✅ Streamlit Web应用开发
  • ✅ 完整的回归分析项目开发

🔗 参考资源


项目完成时间: 8-12小时 难度等级: ⭐⭐ 中等 推荐指数: ⭐⭐⭐⭐⭐