数据准备与预处理实战:从原始数据到模型输入的完整流程

0 阅读1分钟

在前面的课程中,我们学习了Python基础、AI发展史和数学基础。今天,我们将深入学习数据准备与预处理,这是AI项目成功的关键第一步。数据质量直接影响模型性能,掌握数据预处理技能是每个AI工程师的必备能力。

为什么数据预处理如此重要?

在AI项目中,数据预处理通常占据整个项目时间的60-80%。高质量的数据预处理可以:

  1. 提升模型性能:清洗后的数据能让模型学习到更准确的模式
  2. 加快训练速度:规范化的数据能提高训练效率
  3. 避免常见错误:处理缺失值、异常值等能防止模型崩溃
  4. 提高可解释性:结构化的数据更易于分析和理解
graph TD
    A[原始数据] --> B[数据收集]
    B --> C[数据清洗]
    C --> D[特征工程]
    D --> E[数据转换]
    E --> F[数据分割]
    F --> G[模型输入]
    
    style A fill:#ff6b6b
    style G fill:#51cf66

数据收集与探索

常见数据集介绍

在开始之前,让我们了解一些常用的公开数据集:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import load_iris, load_boston, fetch_california_housing
import warnings
warnings.filterwarnings('ignore')

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

print("=" * 60)
print("常用数据集介绍")
print("=" * 60)

# 1. 鸢尾花数据集(分类任务)
iris = load_iris()
print("\n1. 鸢尾花数据集(Iris)")
print(f"   样本数: {iris.data.shape[0]}")
print(f"   特征数: {iris.data.shape[1]}")
print(f"   类别数: {len(iris.target_names)}")
print(f"   类别名称: {iris.target_names}")

# 2. 波士顿房价数据集(回归任务)
try:
    boston = load_boston()
    print("\n2. 波士顿房价数据集(Boston Housing)")
    print(f"   样本数: {boston.data.shape[0]}")
    print(f"   特征数: {boston.data.shape[1]}")
except:
    print("\n2. 波士顿房价数据集已弃用,使用加州房价数据集")
    california = fetch_california_housing()
    print(f"   样本数: {california.data.shape[0]}")
    print(f"   特征数: {california.data.shape[1]}")

# 3. 创建示例数据集
print("\n3. 自定义数据集示例")
np.random.seed(42)
custom_data = {
    '年龄': np.random.randint(18, 65, 100),
    '收入': np.random.normal(50000, 15000, 100),
    '教育年限': np.random.randint(12, 20, 100),
    '是否购买': np.random.choice([0, 1], 100, p=[0.6, 0.4])
}
df_custom = pd.DataFrame(custom_data)
print(f"   样本数: {len(df_custom)}")
print(f"   特征数: {len(df_custom.columns)}")
print("\n前5行数据:")
print(df_custom.head())

数据探索性分析(EDA)

数据探索是理解数据的第一步,包括统计描述、可视化等:

def comprehensive_eda(df, target_col=None):
    """全面的探索性数据分析"""
    
    print("=" * 60)
    print("数据基本信息")
    print("=" * 60)
    print(f"数据形状: {df.shape}")
    print(f"列名: {list(df.columns)}")
    print(f"\n数据类型:")
    print(df.dtypes)
    
    print("\n" + "=" * 60)
    print("数据统计描述")
    print("=" * 60)
    print(df.describe())
    
    print("\n" + "=" * 60)
    print("缺失值统计")
    print("=" * 60)
    missing = df.isnull().sum()
    missing_percent = 100 * missing / len(df)
    missing_df = pd.DataFrame({
        '缺失数量': missing,
        '缺失比例(%)': missing_percent
    })
    print(missing_df[missing_df['缺失数量'] > 0])
    
    print("\n" + "=" * 60)
    print("重复值统计")
    print("=" * 60)
    duplicates = df.duplicated().sum()
    print(f"重复行数: {duplicates}")
    
    # 可视化
    fig, axes = plt.subplots(2, 2, figsize=(15, 12))
    
    # 1. 数值特征分布
    numeric_cols = df.select_dtypes(include=[np.number]).columns
    if len(numeric_cols) > 0:
        df[numeric_cols[:4]].hist(ax=axes[0, 0], bins=20, edgecolor='black')
        axes[0, 0].set_title('数值特征分布')
    
    # 2. 缺失值热力图
    if df.isnull().sum().sum() > 0:
        sns.heatmap(df.isnull(), ax=axes[0, 1], cbar=True, yticklabels=False)
        axes[0, 1].set_title('缺失值热力图')
    else:
        axes[0, 1].text(0.5, 0.5, '无缺失值', ha='center', va='center')
        axes[0, 1].set_title('缺失值热力图')
    
    # 3. 相关性矩阵
    if len(numeric_cols) > 1:
        corr_matrix = df[numeric_cols].corr()
        sns.heatmap(corr_matrix, annot=True, fmt='.2f', ax=axes[1, 0], cmap='coolwarm', center=0)
        axes[1, 0].set_title('特征相关性矩阵')
    
    # 4. 箱线图(异常值检测)
    if len(numeric_cols) > 0:
        df[numeric_cols[:4]].boxplot(ax=axes[1, 1])
        axes[1, 1].set_title('数值特征箱线图(异常值检测)')
        axes[1, 1].tick_params(axis='x', rotation=45)
    
    plt.tight_layout()
    plt.show()
    
    return df

# 使用鸢尾花数据集进行EDA
iris_df = pd.DataFrame(iris.data, columns=iris.feature_names)
iris_df['target'] = iris.target
iris_df['species'] = iris_df['target'].map({0: 'setosa', 1: 'versicolor', 2: 'virginica'})

comprehensive_eda(iris_df)

数据清洗

数据清洗是预处理的核心环节,包括处理缺失值、异常值、重复值等。

处理缺失值

class MissingValueHandler:
    """缺失值处理类"""
    
    def __init__(self, strategy='mean'):
        """
        参数:
        strategy: 处理策略 ('mean', 'median', 'mode', 'drop', 'forward_fill', 'backward_fill')
        """
        self.strategy = strategy
        self.fill_values = {}
    
    def fit(self, df):
        """学习填充值"""
        numeric_cols = df.select_dtypes(include=[np.number]).columns
        categorical_cols = df.select_dtypes(include=['object', 'category']).columns
        
        for col in numeric_cols:
            if self.strategy == 'mean':
                self.fill_values[col] = df[col].mean()
            elif self.strategy == 'median':
                self.fill_values[col] = df[col].median()
            elif self.strategy == 'mode':
                self.fill_values[col] = df[col].mode()[0] if len(df[col].mode()) > 0 else 0
        
        for col in categorical_cols:
            self.fill_values[col] = df[col].mode()[0] if len(df[col].mode()) > 0 else 'Unknown'
        
        return self
    
    def transform(self, df):
        """应用填充"""
        df_filled = df.copy()
        
        for col, value in self.fill_values.items():
            if col in df_filled.columns:
                if self.strategy in ['forward_fill', 'ffill']:
                    df_filled[col] = df_filled[col].fillna(method='ffill')
                elif self.strategy in ['backward_fill', 'bfill']:
                    df_filled[col] = df_filled[col].fillna(method='bfill')
                else:
                    df_filled[col] = df_filled[col].fillna(value)
        
        return df_filled
    
    def fit_transform(self, df):
        """同时进行学习和转换"""
        return self.fit(df).transform(df)

# 创建包含缺失值的数据集
np.random.seed(42)
data_with_missing = {
    '特征1': np.random.normal(100, 15, 100),
    '特征2': np.random.normal(50, 10, 100),
    '特征3': np.random.choice(['A', 'B', 'C'], 100),
    '目标值': np.random.normal(200, 30, 100)
}
df_missing = pd.DataFrame(data_with_missing)

# 随机引入缺失值
missing_indices_1 = np.random.choice(df_missing.index, size=10, replace=False)
missing_indices_2 = np.random.choice(df_missing.index, size=8, replace=False)
df_missing.loc[missing_indices_1, '特征1'] = np.nan
df_missing.loc[missing_indices_2, '特征2'] = np.nan

print("原始数据缺失情况:")
print(df_missing.isnull().sum())

# 使用不同策略处理缺失值
strategies = ['mean', 'median', 'forward_fill']
results = {}

for strategy in strategies:
    handler = MissingValueHandler(strategy=strategy)
    df_processed = handler.fit_transform(df_missing)
    results[strategy] = df_processed
    print(f"\n使用 {strategy} 策略处理后:")
    print(f"剩余缺失值: {df_processed.isnull().sum().sum()}")

# 可视化对比
fig, axes = plt.subplots(2, 2, figsize=(15, 10))

# 原始数据
axes[0, 0].bar(['特征1', '特征2'], [df_missing['特征1'].isnull().sum(), 
                                    df_missing['特征2'].isnull().sum()])
axes[0, 0].set_title('原始数据缺失值')
axes[0, 0].set_ylabel('缺失数量')

# 处理后数据
for i, (strategy, df_proc) in enumerate(results.items()):
    axes[0, 1].bar([f'特征1\n({strategy})', f'特征2\n({strategy})'], 
                   [df_proc['特征1'].isnull().sum(), df_proc['特征2'].isnull().sum()],
                   alpha=0.7, label=strategy)
axes[0, 1].set_title('处理后缺失值')
axes[0, 1].set_ylabel('缺失数量')
axes[0, 1].legend()

# 特征分布对比
axes[1, 0].hist(df_missing['特征1'].dropna(), bins=20, alpha=0.5, label='原始', edgecolor='black')
axes[1, 0].hist(results['mean']['特征1'], bins=20, alpha=0.5, label='填充后', edgecolor='black')
axes[1, 0].set_title('特征1分布对比')
axes[1, 0].legend()

axes[1, 1].hist(df_missing['特征2'].dropna(), bins=20, alpha=0.5, label='原始', edgecolor='black')
axes[1, 1].hist(results['median']['特征2'], bins=20, alpha=0.5, label='填充后', edgecolor='black')
axes[1, 1].set_title('特征2分布对比')
axes[1, 1].legend()

plt.tight_layout()
plt.show()

处理异常值

class OutlierHandler:
    """异常值处理类"""
    
    def __init__(self, method='iqr', threshold=1.5):
        """
        参数:
        method: 检测方法 ('iqr', 'zscore', 'isolation')
        threshold: 阈值
        """
        self.method = method
        self.threshold = threshold
        self.bounds = {}
    
    def detect_outliers(self, df):
        """检测异常值"""
        outliers_mask = pd.Series([False] * len(df), index=df.index)
        numeric_cols = df.select_dtypes(include=[np.number]).columns
        
        for col in numeric_cols:
            if self.method == 'iqr':
                Q1 = df[col].quantile(0.25)
                Q3 = df[col].quantile(0.75)
                IQR = Q3 - Q1
                lower_bound = Q1 - self.threshold * IQR
                upper_bound = Q3 + self.threshold * IQR
                self.bounds[col] = (lower_bound, upper_bound)
                outliers_mask |= (df[col] < lower_bound) | (df[col] > upper_bound)
            
            elif self.method == 'zscore':
                z_scores = np.abs((df[col] - df[col].mean()) / df[col].std())
                outliers_mask |= z_scores > self.threshold
                self.bounds[col] = (df[col].mean() - self.threshold * df[col].std(),
                                   df[col].mean() + self.threshold * df[col].std())
        
        return outliers_mask
    
    def remove_outliers(self, df):
        """移除异常值"""
        outliers_mask = self.detect_outliers(df)
        return df[~outliers_mask]
    
    def cap_outliers(self, df):
        """截断异常值"""
        df_capped = df.copy()
        numeric_cols = df.select_dtypes(include=[np.number]).columns
        
        for col in numeric_cols:
            if col in self.bounds:
                lower, upper = self.bounds[col]
                df_capped[col] = df_capped[col].clip(lower=lower, upper=upper)
        
        return df_capped

# 创建包含异常值的数据
np.random.seed(42)
normal_data = np.random.normal(100, 15, 95)
outlier_data = np.random.normal(200, 10, 5)  # 异常值
data_with_outliers = np.concatenate([normal_data, outlier_data])
df_outliers = pd.DataFrame({'特征': data_with_outliers})

print("异常值检测与处理")
print("=" * 60)

# 检测异常值
handler = OutlierHandler(method='iqr', threshold=1.5)
outliers_mask = handler.detect_outliers(df_outliers)
print(f"检测到异常值数量: {outliers_mask.sum()}")

# 可视化
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# 原始数据
axes[0].scatter(range(len(df_outliers)), df_outliers['特征'], alpha=0.6)
axes[0].axhline(y=handler.bounds['特征'][0], color='r', linestyle='--', label='下界')
axes[0].axhline(y=handler.bounds['特征'][1], color='r', linestyle='--', label='上界')
axes[0].scatter(df_outliers[outliers_mask].index, 
                df_outliers[outliers_mask]['特征'], 
                color='red', s=100, label='异常值', zorder=5)
axes[0].set_title('原始数据(标注异常值)')
axes[0].set_xlabel('样本索引')
axes[0].set_ylabel('特征值')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# 移除异常值
df_removed = handler.remove_outliers(df_outliers)
axes[1].scatter(range(len(df_removed)), df_removed['特征'], alpha=0.6)
axes[1].set_title(f'移除异常值后(剩余{len(df_removed)}个样本)')
axes[1].set_xlabel('样本索引')
axes[1].set_ylabel('特征值')
axes[1].grid(True, alpha=0.3)

# 截断异常值
df_capped = handler.cap_outliers(df_outliers)
axes[2].scatter(range(len(df_capped)), df_capped['特征'], alpha=0.6)
axes[2].axhline(y=handler.bounds['特征'][0], color='r', linestyle='--', label='下界')
axes[2].axhline(y=handler.bounds['特征'][1], color='r', linestyle='--', label='上界')
axes[2].set_title('截断异常值后')
axes[2].set_xlabel('样本索引')
axes[2].set_ylabel('特征值')
axes[2].legend()
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

特征工程

特征工程是提升模型性能的关键步骤,包括特征编码、特征缩放、特征选择等。

特征编码

class FeatureEncoder:
    """特征编码类"""
    
    @staticmethod
    def one_hot_encode(df, columns):
        """独热编码"""
        return pd.get_dummies(df, columns=columns, prefix=columns)
    
    @staticmethod
    def label_encode(df, columns):
        """标签编码"""
        df_encoded = df.copy()
        label_mapping = {}
        
        for col in columns:
            unique_values = df[col].unique()
            label_mapping[col] = {val: idx for idx, val in enumerate(unique_values)}
            df_encoded[col] = df[col].map(label_mapping[col])
        
        return df_encoded, label_mapping
    
    @staticmethod
    def target_encode(df, categorical_col, target_col):
        """目标编码(均值编码)"""
        target_mean = df.groupby(categorical_col)[target_col].mean()
        df_encoded = df.copy()
        df_encoded[f'{categorical_col}_target_encoded'] = df[categorical_col].map(target_mean)
        return df_encoded

# 示例:处理分类特征
categorical_data = {
    '城市': ['北京', '上海', '广州', '北京', '上海', '深圳'] * 20,
    '类别': ['A', 'B', 'C', 'A', 'B', 'C'] * 20,
    '数值': np.random.normal(100, 15, 120)
}
df_cat = pd.DataFrame(categorical_data)

print("特征编码示例")
print("=" * 60)

# 独热编码
df_onehot = FeatureEncoder.one_hot_encode(df_cat, ['城市', '类别'])
print("\n独热编码后:")
print(f"原始特征数: {len(df_cat.columns)}")
print(f"编码后特征数: {len(df_onehot.columns)}")
print(df_onehot.head())

# 标签编码
df_label, mapping = FeatureEncoder.label_encode(df_cat, ['城市', '类别'])
print("\n标签编码后:")
print(f"编码映射: {mapping}")
print(df_label.head())

特征缩放

from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler

class FeatureScaler:
    """特征缩放类"""
    
    def __init__(self, method='standard'):
        """
        参数:
        method: 缩放方法 ('standard', 'minmax', 'robust')
        """
        self.method = method
        if method == 'standard':
            self.scaler = StandardScaler()
        elif method == 'minmax':
            self.scaler = MinMaxScaler()
        elif method == 'robust':
            self.scaler = RobustScaler()
    
    def fit_transform(self, X):
        """拟合并转换"""
        return self.scaler.fit_transform(X)
    
    def transform(self, X):
        """转换"""
        return self.scaler.transform(X)

# 创建不同尺度的特征
np.random.seed(42)
data_scaling = {
    '特征1': np.random.normal(100, 10, 100),      # 均值100,标准差10
    '特征2': np.random.normal(0.5, 0.1, 100),     # 均值0.5,标准差0.1
    '特征3': np.random.normal(10000, 1000, 100),  # 均值10000,标准差1000
}
df_scaling = pd.DataFrame(data_scaling)

print("特征缩放对比")
print("=" * 60)

# 原始数据统计
print("\n原始数据统计:")
print(df_scaling.describe())

# 不同缩放方法
scalers = {
    'StandardScaler': FeatureScaler('standard'),
    'MinMaxScaler': FeatureScaler('minmax'),
    'RobustScaler': FeatureScaler('robust')
}

scaled_data = {}
for name, scaler in scalers.items():
    scaled_data[name] = scaler.fit_transform(df_scaling)
    print(f"\n{name} 缩放后统计:")
    print(pd.DataFrame(scaled_data[name], columns=df_scaling.columns).describe())

# 可视化对比
fig, axes = plt.subplots(2, 3, figsize=(18, 10))

for i, (name, data) in enumerate(scaled_data.items()):
    axes[0, i].boxplot(data, labels=df_scaling.columns)
    axes[0, i].set_title(f'{name} - 箱线图')
    axes[0, i].tick_params(axis='x', rotation=45)
    
    axes[1, i].hist(data[:, 0], bins=20, alpha=0.7, label='特征1', edgecolor='black')
    axes[1, i].hist(data[:, 1], bins=20, alpha=0.7, label='特征2', edgecolor='black')
    axes[1, i].hist(data[:, 2], bins=20, alpha=0.7, label='特征3', edgecolor='black')
    axes[1, i].set_title(f'{name} - 分布对比')
    axes[1, i].legend()
    axes[1, i].set_xlabel('缩放后的值')

plt.tight_layout()
plt.show()

数据分割

将数据分为训练集、验证集和测试集是模型评估的关键:

from sklearn.model_selection import train_test_split

def split_data(X, y, test_size=0.2, val_size=0.1, random_state=42):
    """数据分割"""
    # 首先分出测试集
    X_temp, X_test, y_temp, y_test = train_test_split(
        X, y, test_size=test_size, random_state=random_state, stratify=y if len(np.unique(y)) < 10 else None
    )
    
    # 再从剩余数据中分出验证集
    val_size_adjusted = val_size / (1 - test_size)
    X_train, X_val, y_train, y_val = train_test_split(
        X_temp, y_temp, test_size=val_size_adjusted, random_state=random_state,
        stratify=y_temp if len(np.unique(y_temp)) < 10 else None
    )
    
    return X_train, X_val, X_test, y_train, y_val, y_test

# 使用鸢尾花数据集演示
X_iris = iris.data
y_iris = iris.target

X_train, X_val, X_test, y_train, y_val, y_test = split_data(
    X_iris, y_iris, test_size=0.2, val_size=0.1
)

print("数据分割结果")
print("=" * 60)
print(f"训练集: {X_train.shape[0]} 样本 ({X_train.shape[0]/len(X_iris)*100:.1f}%)")
print(f"验证集: {X_val.shape[0]} 样本 ({X_val.shape[0]/len(X_iris)*100:.1f}%)")
print(f"测试集: {X_test.shape[0]} 样本 ({X_test.shape[0]/len(X_iris)*100:.1f}%)")

# 可视化数据分割
fig, axes = plt.subplots(1, 3, figsize=(18, 5))

for i, (data, labels, title) in enumerate([
    (X_train, y_train, '训练集'),
    (X_val, y_val, '验证集'),
    (X_test, y_test, '测试集')
]):
    scatter = axes[i].scatter(data[:, 0], data[:, 1], c=labels, cmap='viridis', alpha=0.7)
    axes[i].set_title(title)
    axes[i].set_xlabel('特征1')
    axes[i].set_ylabel('特征2')
    plt.colorbar(scatter, ax=axes[i])

plt.tight_layout()
plt.show()

完整的数据预处理流程

让我们将以上所有步骤整合成一个完整的流程:

class DataPreprocessor:
    """完整的数据预处理器"""
    
    def __init__(self):
        self.missing_handler = None
        self.outlier_handler = None
        self.scaler = None
        self.encoder = None
    
    def fit(self, df, target_col=None, categorical_cols=None, 
            missing_strategy='mean', scaling_method='standard'):
        """拟合预处理器"""
        df_processed = df.copy()
        
        # 1. 处理缺失值
        self.missing_handler = MissingValueHandler(strategy=missing_strategy)
        df_processed = self.missing_handler.fit_transform(df_processed)
        
        # 2. 处理异常值
        self.outlier_handler = OutlierHandler(method='iqr')
        self.outlier_handler.detect_outliers(df_processed)
        df_processed = self.outlier_handler.cap_outliers(df_processed)
        
        # 3. 特征编码(如果有分类特征)
        if categorical_cols:
            df_processed, _ = FeatureEncoder.label_encode(df_processed, categorical_cols)
        
        # 4. 特征缩放
        numeric_cols = df_processed.select_dtypes(include=[np.number]).columns
        if target_col and target_col in numeric_cols:
            numeric_cols = numeric_cols.drop(target_col)
        
        if len(numeric_cols) > 0:
            self.scaler = FeatureScaler(method=scaling_method)
            df_processed[numeric_cols] = self.scaler.fit_transform(df_processed[numeric_cols])
        
        return self
    
    def transform(self, df):
        """转换数据"""
        df_processed = df.copy()
        
        # 应用所有转换
        if self.missing_handler:
            df_processed = self.missing_handler.transform(df_processed)
        
        if self.outlier_handler:
            df_processed = self.outlier_handler.cap_outliers(df_processed)
        
        if self.scaler:
            numeric_cols = df_processed.select_dtypes(include=[np.number]).columns
            df_processed[numeric_cols] = self.scaler.transform(df_processed[numeric_cols])
        
        return df_processed

# 完整流程演示
print("\n" + "=" * 60)
print("完整数据预处理流程演示")
print("=" * 60)

# 创建综合数据集
np.random.seed(42)
comprehensive_data = {
    '年龄': np.random.randint(18, 65, 200),
    '收入': np.random.normal(50000, 15000, 200),
    '城市': np.random.choice(['北京', '上海', '广州', '深圳'], 200),
    '教育': np.random.choice(['本科', '硕士', '博士'], 200),
    '购买金额': np.random.normal(1000, 300, 200)
}
df_comprehensive = pd.DataFrame(comprehensive_data)

# 引入一些数据质量问题
missing_indices = np.random.choice(df_comprehensive.index, size=15, replace=False)
df_comprehensive.loc[missing_indices, '收入'] = np.nan

outlier_indices = np.random.choice(df_comprehensive.index, size=5, replace=False)
df_comprehensive.loc[outlier_indices, '购买金额'] = np.random.normal(5000, 500, 5)

print("\n原始数据:")
print(df_comprehensive.head())
print(f"\n缺失值: {df_comprehensive.isnull().sum().sum()}")
print(f"数据形状: {df_comprehensive.shape}")

# 应用预处理
preprocessor = DataPreprocessor()
df_processed = preprocessor.fit(
    df_comprehensive, 
    target_col='购买金额',
    categorical_cols=['城市', '教育'],
    missing_strategy='mean',
    scaling_method='standard'
)

print("\n预处理后数据:")
print(df_processed.head())
print(f"\n缺失值: {df_processed.isnull().sum().sum()}")
print(f"数据形状: {df_processed.shape}")

# 可视化对比
fig, axes = plt.subplots(2, 2, figsize=(15, 10))

# 收入分布对比
axes[0, 0].hist(df_comprehensive['收入'].dropna(), bins=20, alpha=0.7, 
                label='原始', edgecolor='black')
axes[0, 0].hist(df_processed['收入'], bins=20, alpha=0.7, 
                label='处理后', edgecolor='black')
axes[0, 0].set_title('收入分布对比')
axes[0, 0].legend()

# 购买金额分布对比
axes[0, 1].hist(df_comprehensive['购买金额'], bins=20, alpha=0.7, 
                label='原始', edgecolor='black')
axes[0, 1].hist(df_processed['购买金额'], bins=20, alpha=0.7, 
                label='处理后', edgecolor='black')
axes[0, 1].set_title('购买金额分布对比')
axes[0, 1].legend()

# 城市分布
city_counts_orig = df_comprehensive['城市'].value_counts()
city_counts_proc = df_processed['城市'].value_counts()
x = np.arange(len(city_counts_orig))
width = 0.35
axes[1, 0].bar(x - width/2, city_counts_orig.values, width, label='原始', alpha=0.7)
axes[1, 0].bar(x + width/2, city_counts_proc.values, width, label='处理后', alpha=0.7)
axes[1, 0].set_xticks(x)
axes[1, 0].set_xticklabels(city_counts_orig.index)
axes[1, 0].set_title('城市分布对比')
axes[1, 0].legend()

# 相关性对比
numeric_cols = ['年龄', '收入', '购买金额']
corr_orig = df_comprehensive[numeric_cols].corr()
corr_proc = df_processed[numeric_cols].corr()

im1 = axes[1, 1].imshow(corr_proc, cmap='coolwarm', aspect='auto', vmin=-1, vmax=1)
axes[1, 1].set_xticks(range(len(numeric_cols)))
axes[1, 1].set_yticks(range(len(numeric_cols)))
axes[1, 1].set_xticklabels(numeric_cols, rotation=45)
axes[1, 1].set_yticklabels(numeric_cols)
axes[1, 1].set_title('处理后特征相关性')
for i in range(len(numeric_cols)):
    for j in range(len(numeric_cols)):
        axes[1, 1].text(j, i, f'{corr_proc.iloc[i, j]:.2f}', 
                       ha='center', va='center', color='white' if abs(corr_proc.iloc[i, j]) > 0.5 else 'black')
plt.colorbar(im1, ax=axes[1, 1])

plt.tight_layout()
plt.show()

实战案例:MNIST数据预处理

让我们以MNIST手写数字数据集为例,展示完整的数据预处理流程:

from sklearn.datasets import fetch_openml
from sklearn.preprocessing import StandardScaler

# 加载MNIST数据集(使用较小的子集以加快速度)
print("加载MNIST数据集...")
try:
    # 尝试加载完整数据集
    mnist = fetch_openml('mnist_784', version=1, as_frame=False, parser='auto')
    X_mnist, y_mnist = mnist.data, mnist.target.astype(int)
    
    # 为了演示,只使用前10000个样本
    X_mnist = X_mnist[:10000]
    y_mnist = y_mnist[:10000]
    
    print(f"MNIST数据集形状: {X_mnist.shape}")
    print(f"标签范围: {y_mnist.min()} - {y_mnist.max()}")
    
    # 数据分割
    X_train_mnist, X_val_mnist, X_test_mnist, y_train_mnist, y_val_mnist, y_test_mnist = split_data(
        X_mnist, y_mnist, test_size=0.2, val_size=0.1
    )
    
    # 特征缩放
    scaler_mnist = StandardScaler()
    X_train_mnist_scaled = scaler_mnist.fit_transform(X_train_mnist)
    X_val_mnist_scaled = scaler_mnist.transform(X_val_mnist)
    X_test_mnist_scaled = scaler_mnist.transform(X_test_mnist)
    
    print("\nMNIST数据预处理完成!")
    print(f"训练集: {X_train_mnist_scaled.shape}")
    print(f"验证集: {X_val_mnist_scaled.shape}")
    print(f"测试集: {X_test_mnist_scaled.shape}")
    
    # 可视化一些样本
    fig, axes = plt.subplots(2, 5, figsize=(15, 6))
    for i in range(10):
        row = i // 5
        col = i % 5
        img = X_train_mnist[i].reshape(28, 28)
        axes[row, col].imshow(img, cmap='gray')
        axes[row, col].set_title(f'标签: {y_train_mnist[i]}')
        axes[row, col].axis('off')
    plt.suptitle('MNIST数据集样本示例', fontsize=16)
    plt.tight_layout()
    plt.show()
    
except Exception as e:
    print(f"加载MNIST数据集时出错: {e}")
    print("这可能是网络问题,可以稍后重试或使用本地数据集")

数据预处理最佳实践

graph TD
    A[开始数据预处理] --> B[数据探索EDA]
    B --> C{发现数据问题?}
    C -->|有缺失值| D[处理缺失值]
    C -->|有异常值| E[处理异常值]
    C -->|有重复值| F[去除重复值]
    D --> G[特征工程]
    E --> G
    F --> G
    G --> H[特征编码]
    G --> I[特征缩放]
    G --> J[特征选择]
    H --> K[数据分割]
    I --> K
    J --> K
    K --> L[训练集/验证集/测试集]
    L --> M[模型训练]
    
    style A fill:#ff6b6b
    style M fill:#51cf66

关键要点总结

  1. 数据探索优先:在开始处理前,充分了解数据特征
  2. 保留原始数据:始终保留原始数据的副本
  3. 处理顺序重要:先处理缺失值,再处理异常值,最后进行特征工程
  4. 验证集独立:确保验证集和测试集不参与任何预处理参数的拟合
  5. 文档记录:记录所有预处理步骤和参数,便于复现

课后练习

  1. 实践任务

    • 选择一个公开数据集(如UCI Machine Learning Repository)
    • 完成完整的数据探索性分析
    • 实现数据清洗和特征工程流程
    • 将数据分割为训练集、验证集和测试集
  2. 思考题

    • 什么情况下应该删除缺失值,什么情况下应该填充?
    • 异常值处理的不同方法各适用于什么场景?
    • 为什么需要对特征进行缩放?不同缩放方法的优缺点是什么?
  3. 扩展练习

    • 实现一个自动化的数据预处理管道
    • 尝试使用Pipeline将预处理步骤串联起来
    • 对比不同预处理策略对模型性能的影响

总结

本节我们深入学习了数据准备与预处理的完整流程:

  1. 数据探索:通过EDA了解数据特征和问题
  2. 数据清洗:处理缺失值、异常值和重复值
  3. 特征工程:编码、缩放和选择特征
  4. 数据分割:合理划分训练、验证和测试集

掌握这些技能是成为优秀AI工程师的基础。在下一节中,我们将学习如何将这些预处理后的数据用于模型训练。


数据预处理是AI项目的基石,投入时间做好数据预处理,往往能获得比复杂模型更好的效果提升。