在前面的课程中,我们学习了Python基础、AI发展史和数学基础。今天,我们将深入学习数据准备与预处理,这是AI项目成功的关键第一步。数据质量直接影响模型性能,掌握数据预处理技能是每个AI工程师的必备能力。
为什么数据预处理如此重要?
在AI项目中,数据预处理通常占据整个项目时间的60-80%。高质量的数据预处理可以:
- 提升模型性能:清洗后的数据能让模型学习到更准确的模式
- 加快训练速度:规范化的数据能提高训练效率
- 避免常见错误:处理缺失值、异常值等能防止模型崩溃
- 提高可解释性:结构化的数据更易于分析和理解
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
关键要点总结
- 数据探索优先:在开始处理前,充分了解数据特征
- 保留原始数据:始终保留原始数据的副本
- 处理顺序重要:先处理缺失值,再处理异常值,最后进行特征工程
- 验证集独立:确保验证集和测试集不参与任何预处理参数的拟合
- 文档记录:记录所有预处理步骤和参数,便于复现
课后练习
-
实践任务:
- 选择一个公开数据集(如UCI Machine Learning Repository)
- 完成完整的数据探索性分析
- 实现数据清洗和特征工程流程
- 将数据分割为训练集、验证集和测试集
-
思考题:
- 什么情况下应该删除缺失值,什么情况下应该填充?
- 异常值处理的不同方法各适用于什么场景?
- 为什么需要对特征进行缩放?不同缩放方法的优缺点是什么?
-
扩展练习:
- 实现一个自动化的数据预处理管道
- 尝试使用Pipeline将预处理步骤串联起来
- 对比不同预处理策略对模型性能的影响
总结
本节我们深入学习了数据准备与预处理的完整流程:
- 数据探索:通过EDA了解数据特征和问题
- 数据清洗:处理缺失值、异常值和重复值
- 特征工程:编码、缩放和选择特征
- 数据分割:合理划分训练、验证和测试集
掌握这些技能是成为优秀AI工程师的基础。在下一节中,我们将学习如何将这些预处理后的数据用于模型训练。
数据预处理是AI项目的基石,投入时间做好数据预处理,往往能获得比复杂模型更好的效果提升。