数据清理与特征工程完整指南

数据清理与特征工程完整指南


目录

  1. 背景与动机
  2. [数据清理(Data Cleaning)](#数据清理(Data Cleaning))
  3. 特征工程基础
  4. 特征预处理
  5. 特征构造与提取
  6. 特征选择
  7. 深度学习特有的预处理
  8. 实战流程与最佳实践
  9. 常见误区与陷阱
  10. 扩展阅读与进阶方向

1. 背景与动机

1.1 为什么需要数据预处理?

在机器学习和深度学习项目中,数据预处理通常占据 60-80% 的时间,这是因为:

  1. 原始数据往往不可直接使用

    • 缺失值、异常值、不一致格式
    • 数据规模不统一、分布不均衡
    • 噪声数据影响模型学习
  2. 模型性能高度依赖数据质量

    • "Garbage In, Garbage Out"(垃圾进,垃圾出)
    • 高质量特征 > 复杂模型(很多场景下)
  3. 不同模型对数据有不同要求

    • 线性模型需要特征缩放
    • 树模型对缺失值更宽容
    • 深度学习需要大量数据和标准化输入

1.2 预处理在ML/DL流程中的位置

复制代码
原始数据 → 数据清理 → 特征工程 → 模型训练 → 模型评估 → 部署
         ↑__________________|
              (反馈循环:根据模型表现调整特征)

1.3 核心原则

  • 理解数据优先:在处理前必须进行探索性数据分析(EDA)
  • 不要数据泄露:测试集的预处理参数必须来自训练集
  • 保留原始数据:预处理应在副本上进行
  • 文档化每一步:确保流程可复现

2. 数据清理(Data Cleaning)

2.1 缺失值处理

2.1.1 缺失值的类型
类型 英文缩写 特点 处理策略
完全随机缺失 MCAR 缺失与任何变量无关 删除或填充影响较小
随机缺失 MAR 缺失与其他已观测变量有关 使用其他变量预测填充
非随机缺失 MNAR 缺失与未观测值本身有关 需要领域知识,可能需建模
2.1.2 检测缺失值
python 复制代码
import pandas as pd
import numpy as np

# 加载数据
df = pd.read_csv('data.csv')

# 检查缺失值
print(df.isnull().sum())  # 每列缺失数量
print(df.isnull().mean())  # 每列缺失比例

# 可视化缺失值模式
import missingno as msno
msno.matrix(df)  # 缺失值矩阵图
msno.heatmap(df)  # 缺失值相关性热图
2.1.3 处理策略

方法1:删除

python 复制代码
# 删除含有缺失值的行
df_cleaned = df.dropna()

# 删除缺失值超过50%的列
threshold = len(df) * 0.5
df_cleaned = df.dropna(thresh=threshold, axis=1)

方法2:简单填充

python 复制代码
# 均值/中位数填充(数值型)
df['age'].fillna(df['age'].mean(), inplace=True)
df['income'].fillna(df['income'].median(), inplace=True)

# 众数填充(类别型)
df['category'].fillna(df['category'].mode()[0], inplace=True)

# 前向/后向填充(时间序列)
df['temperature'].fillna(method='ffill', inplace=True)  # 用前一个值
df['temperature'].fillna(method='bfill', inplace=True)  # 用后一个值

方法3:高级填充

python 复制代码
from sklearn.impute import KNNImputer, IterativeImputer

# KNN填充:使用最近邻的平均值
imputer = KNNImputer(n_neighbors=5)
df_filled = pd.DataFrame(imputer.fit_transform(df), columns=df.columns)

# 迭代填充(MICE算法):使用其他特征预测缺失值
imputer = IterativeImputer(max_iter=10, random_state=42)
df_filled = pd.DataFrame(imputer.fit_transform(df), columns=df.columns)

方法4:标记缺失

python 复制代码
# 创建缺失指示器
df['age_missing'] = df['age'].isnull().astype(int)
df['age'].fillna(df['age'].median(), inplace=True)
2.1.4 选择策略的建议
  • 缺失率 < 5%:删除或简单填充
  • 缺失率 5-20%:高级填充 + 标记缺失
  • 缺失率 > 20%:考虑删除该特征,或使用专门模型

2.2 异常值检测与处理

2.2.1 异常值检测方法

方法1:统计方法

python 复制代码
# 3σ原则(假设正态分布)
mean = df['price'].mean()
std = df['price'].std()
outliers = df[(df['price'] < mean - 3*std) | (df['price'] > mean + 3*std)]

# IQR方法(四分位距)
Q1 = df['price'].quantile(0.25)
Q3 = df['price'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = df[(df['price'] < lower_bound) | (df['price'] > upper_bound)]

方法2:基于模型

python 复制代码
from sklearn.ensemble import IsolationForest
from sklearn.covariance import EllipticEnvelope

# 孤立森林
iso_forest = IsolationForest(contamination=0.1, random_state=42)
outlier_labels = iso_forest.fit_predict(df[numerical_cols])
df['is_outlier'] = outlier_labels

# 椭圆包络
envelope = EllipticEnvelope(contamination=0.1)
outlier_labels = envelope.fit_predict(df[numerical_cols])

方法3:可视化检测

python 复制代码
import matplotlib.pyplot as plt
import seaborn as sns

# 箱线图
plt.figure(figsize=(10, 6))
sns.boxplot(data=df[numerical_cols])

# 散点图
sns.scatterplot(x='feature1', y='feature2', data=df)
2.2.2 处理策略
python 复制代码
# 1. 删除异常值(谨慎使用)
df_cleaned = df[(df['price'] >= lower_bound) & (df['price'] <= upper_bound)]

# 2. 截断(Capping/Winsorization)
df['price'] = df['price'].clip(lower=lower_bound, upper=upper_bound)

# 3. 转换(对数变换减少偏度)
df['price_log'] = np.log1p(df['price'])  # log1p避免log(0)

# 4. 分箱(将连续值离散化)
df['price_bin'] = pd.cut(df['price'], bins=5, labels=['very_low', 'low', 'medium', 'high', 'very_high'])

2.3 重复数据处理

python 复制代码
# 检查重复行
duplicates = df.duplicated()
print(f"重复行数:{duplicates.sum()}")

# 查看重复行
print(df[duplicates])

# 删除重复行(保留第一次出现)
df_cleaned = df.drop_duplicates()

# 基于特定列去重
df_cleaned = df.drop_duplicates(subset=['user_id', 'timestamp'], keep='first')

# 检查近似重复(基于相似度)
from fuzzywuzzy import fuzz

def find_similar_strings(col, threshold=90):
    similar_pairs = []
    unique_values = col.unique()
    for i, val1 in enumerate(unique_values):
        for val2 in unique_values[i+1:]:
            if fuzz.ratio(str(val1), str(val2)) > threshold:
                similar_pairs.append((val1, val2))
    return similar_pairs

2.4 数据类型转换

python 复制代码
# 查看数据类型
print(df.dtypes)

# 转换为正确类型
df['date'] = pd.to_datetime(df['date'])
df['price'] = pd.to_numeric(df['price'], errors='coerce')  # 无法转换的变为NaN
df['category'] = df['category'].astype('category')  # 节省内存

# 优化内存使用
def reduce_mem_usage(df):
    for col in df.columns:
        col_type = df[col].dtype
        if col_type != object:
            c_min = df[col].min()
            c_max = df[col].max()
            if str(col_type)[:3] == 'int':
                if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
                    df[col] = df[col].astype(np.int8)
                elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
                    df[col] = df[col].astype(np.int16)
                # ... 类似逻辑处理int32, int64
    return df

2.5 数据一致性检查

python 复制代码
# 检查逻辑一致性
assert (df['age'] >= 0).all(), "年龄不能为负"
assert (df['end_date'] >= df['start_date']).all(), "结束日期必须晚于开始日期"

# 检查值域
valid_categories = ['A', 'B', 'C']
invalid = df[~df['category'].isin(valid_categories)]
print(f"无效类别:{invalid}")

# 标准化文本
df['country'] = df['country'].str.lower().str.strip()
df['country'] = df['country'].replace({'usa': 'united states', 'u.s.': 'united states'})

# 统一日期格式
df['date'] = pd.to_datetime(df['date'], format='%Y-%m-%d', errors='coerce')

3. 特征工程基础

3.1 特征工程的定义与重要性

特征工程是利用领域知识和数据处理技术,从原始数据中提取或构造有用特征的过程。

Andrew Ng:"应用机器学习基本上就是特征工程。"

特征工程的价值

  • 简单模型 + 好特征 > 复杂模型 + 差特征
  • 提高模型准确性、降低过拟合、加速训练

3.2 特征类型划分

特征类型 描述 示例
数值特征 连续或离散的数值 年龄、价格、温度
类别特征 有限的离散类别 性别、城市、产品类型
有序类别 有顺序关系的类别 教育程度、满意度评级
时间特征 日期、时间戳 注册日期、交易时间
文本特征 自然语言文本 商品描述、用户评论
图像特征 像素数据或提取的特征 照片、扫描件

3.3 特征工程的基本流程

复制代码
原始特征 → 特征预处理 → 特征构造 → 特征选择 → 最终特征集
    ↓           ↓            ↓            ↓
  EDA        缩放/编码     交叉/变换    降维/筛选

4. 特征预处理

4.1 数值特征处理

4.1.1 标准化(Standardization)

目的:将特征转换为均值为0,标准差为1的分布

何时使用

  • 特征服从或近似正态分布
  • 模型对特征尺度敏感(如SVM、逻辑回归、神经网络)
python 复制代码
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
df[['age', 'income']] = scaler.fit_transform(df[['age', 'income']])

# 公式:z = (x - μ) / σ
4.1.2 归一化(Normalization)

目的:将特征缩放到[0, 1]或[-1, 1]区间

何时使用

  • 特征分布不明确或有明显边界
  • 深度学习(加速收敛)
  • 基于距离的算法(KNN、K-Means)
python 复制代码
from sklearn.preprocessing import MinMaxScaler

# Min-Max缩放到[0, 1]
scaler = MinMaxScaler()
df[['feature1', 'feature2']] = scaler.fit_transform(df[['feature1', 'feature2']])

# 公式:x_scaled = (x - x_min) / (x_max - x_min)

# 缩放到[-1, 1]
scaler = MinMaxScaler(feature_range=(-1, 1))
4.1.3 鲁棒缩放(Robust Scaling)

目的:对异常值不敏感的缩放

python 复制代码
from sklearn.preprocessing import RobustScaler

scaler = RobustScaler()
df[['price']] = scaler.fit_transform(df[['price']])

# 使用中位数和IQR:x_scaled = (x - median) / IQR
4.1.4 对数变换

目的:处理偏态分布,使其更接近正态

python 复制代码
# 右偏分布(长尾)
df['income_log'] = np.log1p(df['income'])  # log(1+x) 避免log(0)

# Box-Cox变换(需要数据 > 0)
from scipy.stats import boxcox
df['price_boxcox'], lambda_param = boxcox(df['price'])

# Yeo-Johnson变换(可处理负值)
from sklearn.preprocessing import PowerTransformer
pt = PowerTransformer(method='yeo-johnson')
df['feature_transformed'] = pt.fit_transform(df[['feature']])
4.1.5 分箱(Binning)
python 复制代码
# 等宽分箱
df['age_bin'] = pd.cut(df['age'], bins=5)

# 等频分箱(每个箱内样本数相近)
df['income_bin'] = pd.qcut(df['income'], q=4, labels=['Q1', 'Q2', 'Q3', 'Q4'])

# 自定义边界
df['score_level'] = pd.cut(df['score'], 
                            bins=[0, 60, 80, 100], 
                            labels=['不及格', '良好', '优秀'])

4.2 类别特征编码

4.2.1 标签编码(Label Encoding)

适用:有序类别特征

python 复制代码
from sklearn.preprocessing import LabelEncoder

le = LabelEncoder()
df['education_encoded'] = le.fit_transform(df['education'])

# 示例:['小学', '初中', '高中', '本科'] → [0, 1, 2, 3]

⚠️ 警告:不要对无序类别使用标签编码(模型会误认为有大小关系)

4.2.2 独热编码(One-Hot Encoding)

适用:无序类别特征(类别数较少)

python 复制代码
# 使用Pandas
df_encoded = pd.get_dummies(df, columns=['city', 'color'], drop_first=True)

# 使用Scikit-learn
from sklearn.preprocessing import OneHotEncoder

encoder = OneHotEncoder(sparse_output=False, drop='first')
encoded = encoder.fit_transform(df[['category']])
encoded_df = pd.DataFrame(encoded, columns=encoder.get_feature_names_out())

优点 :不引入虚假顺序
缺点:高基数类别会导致维度爆炸(如城市有1000个)

4.2.3 目标编码(Target Encoding)

适用:高基数类别 + 监督学习

python 复制代码
# 用该类别的目标变量均值替换
category_means = df.groupby('category')['target'].mean()
df['category_encoded'] = df['category'].map(category_means)

# 使用category_encoders库(带平滑处理)
from category_encoders import TargetEncoder

encoder = TargetEncoder(cols=['high_cardinality_col'])
df_encoded = encoder.fit_transform(df['high_cardinality_col'], df['target'])

⚠️ 警告:容易过拟合,需要使用交叉验证或平滑技术

4.2.4 频数编码(Count Encoding)
python 复制代码
# 用该类别出现的次数替换
category_counts = df['category'].value_counts()
df['category_count'] = df['category'].map(category_counts)
4.2.5 嵌入编码(Embedding)

适用:深度学习 + 高基数类别

python 复制代码
import torch
import torch.nn as nn

# 示例:用户ID有10000个,嵌入到32维空间
embedding = nn.Embedding(num_embeddings=10000, embedding_dim=32)
user_ids = torch.LongTensor([1, 55, 999])
embedded = embedding(user_ids)  # shape: (3, 32)

4.3 时间特征处理

python 复制代码
# 提取时间组件
df['date'] = pd.to_datetime(df['date'])
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['day'] = df['date'].dt.day
df['dayofweek'] = df['date'].dt.dayofweek  # 0=Monday
df['quarter'] = df['date'].dt.quarter
df['is_weekend'] = df['dayofweek'].isin([5, 6]).astype(int)
df['is_month_start'] = df['date'].dt.is_month_start.astype(int)

# 计算时间差
df['days_since_registration'] = (pd.Timestamp.now() - df['registration_date']).dt.days

# 周期性编码(避免12月和1月距离过大)
df['month_sin'] = np.sin(2 * np.pi * df['month'] / 12)
df['month_cos'] = np.cos(2 * np.pi * df['month'] / 12)

# 时间窗口特征(滑动统计)
df['sales_7day_avg'] = df['sales'].rolling(window=7).mean()
df['price_30day_std'] = df['price'].rolling(window=30).std()

4.4 文本特征处理

4.4.1 基础文本清洗
python 复制代码
import re
from nltk.corpus import stopwords
from nltk.stem import PorterStemmer, WordNetLemmatizer

def clean_text(text):
    # 转小写
    text = text.lower()
    # 去除URL
    text = re.sub(r'http\S+', '', text)
    # 去除特殊字符
    text = re.sub(r'[^a-zA-Z\s]', '', text)
    # 去除多余空格
    text = ' '.join(text.split())
    return text

df['text_clean'] = df['text'].apply(clean_text)

# 去除停用词
stop_words = set(stopwords.words('english'))
df['text_no_stopwords'] = df['text_clean'].apply(
    lambda x: ' '.join([word for word in x.split() if word not in stop_words])
)

# 词干提取/词形还原
stemmer = PorterStemmer()
lemmatizer = WordNetLemmatizer()
df['text_stemmed'] = df['text_clean'].apply(
    lambda x: ' '.join([stemmer.stem(word) for word in x.split()])
)
4.4.2 特征提取

方法1:词袋模型(Bag of Words)

python 复制代码
from sklearn.feature_extraction.text import CountVectorizer

vectorizer = CountVectorizer(max_features=1000)
bow_features = vectorizer.fit_transform(df['text_clean'])

方法2:TF-IDF

python 复制代码
from sklearn.feature_extraction.text import TfidfVectorizer

tfidf = TfidfVectorizer(max_features=1000, ngram_range=(1, 2))
tfidf_features = tfidf.fit_transform(df['text_clean'])

方法3:词嵌入(Word2Vec/GloVe/FastText)

python 复制代码
from gensim.models import Word2Vec

sentences = [text.split() for text in df['text_clean']]
model = Word2Vec(sentences, vector_size=100, window=5, min_count=2, workers=4)

# 文档向量:所有词向量的平均
def get_doc_vector(tokens, model):
    vectors = [model.wv[word] for word in tokens if word in model.wv]
    return np.mean(vectors, axis=0) if vectors else np.zeros(model.vector_size)

df['text_vector'] = df['text_clean'].apply(
    lambda x: get_doc_vector(x.split(), model)
)

方法4:预训练模型(BERT/GPT)

python 复制代码
from transformers import BertTokenizer, BertModel
import torch

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertModel.from_pretrained('bert-base-uncased')

def get_bert_embedding(text):
    inputs = tokenizer(text, return_tensors='pt', truncation=True, max_length=512)
    with torch.no_grad():
        outputs = model(**inputs)
    return outputs.last_hidden_state.mean(dim=1).squeeze().numpy()

4.5 图像特征预处理

4.5.1 基础预处理
python 复制代码
from PIL import Image
import torchvision.transforms as transforms

# 定义预处理流程
transform = transforms.Compose([
    transforms.Resize((224, 224)),           # 调整大小
    transforms.CenterCrop(224),              # 中心裁剪
    transforms.ToTensor(),                   # 转为Tensor [0, 1]
    transforms.Normalize(                    # 标准化
        mean=[0.485, 0.456, 0.406],         # ImageNet均值
        std=[0.229, 0.224, 0.225]           # ImageNet标准差
    )
])

# 应用
image = Image.open('image.jpg')
image_tensor = transform(image)
4.5.2 数据增强(见第7章)

5. 特征构造与提取

5.1 特征交叉与组合

特征交叉捕获特征间的交互效应

python 复制代码
# 数值特征组合
df['price_per_sqft'] = df['price'] / df['square_feet']
df['age_income_ratio'] = df['age'] / (df['income'] + 1)

# 类别特征交叉
df['city_category'] = df['city'] + '_' + df['product_category']

# 多项式特征(自动生成交叉项)
from sklearn.preprocessing import PolynomialFeatures

poly = PolynomialFeatures(degree=2, include_bias=False)
poly_features = poly.fit_transform(df[['feature1', 'feature2']])

# 示例:[a, b] → [a, b, a², ab, b²]

5.2 聚合特征

python 复制代码
# 分组统计
user_stats = df.groupby('user_id').agg({
    'transaction_amount': ['mean', 'sum', 'std', 'count'],
    'transaction_date': lambda x: (x.max() - x.min()).days
}).reset_index()

user_stats.columns = ['user_id', 'avg_amount', 'total_amount', 
                      'std_amount', 'transaction_count', 'active_days']

df = df.merge(user_stats, on='user_id', how='left')

5.3 领域知识特征

示例:电商领域

python 复制代码
# 用户购买力
df['purchasing_power'] = df['total_spent'] / df['days_since_registration']

# 商品流行度
product_popularity = df.groupby('product_id').size()
df['product_popularity'] = df['product_id'].map(product_popularity)

# 时间相关
df['is_holiday_season'] = df['month'].isin([11, 12]).astype(int)
df['is_payday'] = df['day'].isin([1, 15]).astype(int)

5.4 自动特征生成

python 复制代码
# 使用Featuretools
import featuretools as ft

# 定义实体集
es = ft.EntitySet(id='transactions')
es = es.add_dataframe(
    dataframe_name='transactions',
    dataframe=df,
    index='transaction_id',
    time_index='timestamp'
)

# 自动生成特征
feature_matrix, feature_defs = ft.dfs(
    entityset=es,
    target_dataframe_name='transactions',
    max_depth=2,
    verbose=True
)

6. 特征选择

6.1 为什么需要特征选择?

  • 降低维度:减少训练时间和内存
  • 减少过拟合:去除冗余和噪声特征
  • 提高可解释性:简化模型

6.2 过滤法(Filter Methods)

基于统计指标,与模型无关

6.2.1 方差阈值
python 复制代码
from sklearn.feature_selection import VarianceThreshold

# 删除方差低于阈值的特征
selector = VarianceThreshold(threshold=0.1)
df_selected = selector.fit_transform(df)
6.2.2 相关性分析
python 复制代码
# 计算相关系数
correlation_matrix = df.corr()

# 删除高度相关的特征(保留其一)
def remove_correlated_features(df, threshold=0.95):
    corr_matrix = df.corr().abs()
    upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool))
    to_drop = [column for column in upper.columns if any(upper[column] > threshold)]
    return df.drop(columns=to_drop)

df_reduced = remove_correlated_features(df, threshold=0.95)
6.2.3 卡方检验(类别特征 + 分类任务)
python 复制代码
from sklearn.feature_selection import SelectKBest, chi2

selector = SelectKBest(chi2, k=10)  # 选择最好的10个特征
X_new = selector.fit_transform(X, y)
selected_features = X.columns[selector.get_support()]
6.2.4 互信息
python 复制代码
from sklearn.feature_selection import mutual_info_classif

mi_scores = mutual_info_classif(X, y)
mi_scores = pd.Series(mi_scores, index=X.columns).sort_values(ascending=False)
top_features = mi_scores.head(20).index

6.3 包装法(Wrapper Methods)

基于模型性能迭代选择特征

6.3.1 递归特征消除(RFE)
python 复制代码
from sklearn.feature_selection import RFE
from sklearn.ensemble import RandomForestClassifier

model = RandomForestClassifier()
rfe = RFE(estimator=model, n_features_to_select=10)
rfe.fit(X, y)

selected_features = X.columns[rfe.support_]
6.3.2 前向/后向选择
python 复制代码
from mlxtend.feature_selection import SequentialFeatureSelector

sfs = SequentialFeatureSelector(
    estimator=RandomForestClassifier(),
    k_features=10,
    forward=True,  # True=前向, False=后向
    scoring='accuracy',
    cv=5
)
sfs.fit(X, y)
selected_features = list(sfs.k_feature_names_)

6.4 嵌入法(Embedded Methods)

模型训练过程中自动选择特征

6.4.1 L1正则化(Lasso)
python 复制代码
from sklearn.linear_model import LassoCV

lasso = LassoCV(cv=5, random_state=42)
lasso.fit(X, y)

# 系数为0的特征被自动排除
selected_features = X.columns[lasso.coef_ != 0]
6.4.2 树模型特征重要性
python 复制代码
from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X, y)

# 特征重要性排序
feature_importance = pd.Series(rf.feature_importances_, index=X.columns)
feature_importance = feature_importance.sort_values(ascending=False)

# 选择top-k特征
top_features = feature_importance.head(20).index
6.4.3 SHAP值
python 复制代码
import shap

explainer = shap.TreeExplainer(rf)
shap_values = explainer.shap_values(X)

# 全局特征重要性
shap.summary_plot(shap_values, X, plot_type="bar")

6.5 降维方法

6.5.1 主成分分析(PCA)
python 复制代码
from sklearn.decomposition import PCA

pca = PCA(n_components=0.95)  # 保留95%的方差
X_pca = pca.fit_transform(X)

print(f"降维后特征数:{pca.n_components_}")
6.5.2 t-SNE / UMAP(可视化)
python 复制代码
from sklearn.manifold import TSNE
import umap

# t-SNE(适合可视化,不适合预处理)
tsne = TSNE(n_components=2, random_state=42)
X_tsne = tsne.fit_transform(X)

# UMAP(更快,保留更多全局结构)
reducer = umap.UMAP(n_components=2, random_state=42)
X_umap = reducer.fit_transform(X)

7. 深度学习特有的预处理

7.1 数据增强(Data Augmentation)

7.1.1 图像数据增强
python 复制代码
import torchvision.transforms as transforms

train_transform = transforms.Compose([
    transforms.RandomResizedCrop(224),       # 随机裁剪并调整大小
    transforms.RandomHorizontalFlip(p=0.5),  # 随机水平翻转
    transforms.RandomRotation(15),           # 随机旋转±15度
    transforms.ColorJitter(                  # 颜色抖动
        brightness=0.2,
        contrast=0.2,
        saturation=0.2,
        hue=0.1
    ),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], 
                        std=[0.229, 0.224, 0.225])
])

# 高级增强(Albumentations库)
import albumentations as A

transform = A.Compose([
    A.HorizontalFlip(p=0.5),
    A.ShiftScaleRotate(p=0.5),
    A.RandomBrightnessContrast(p=0.3),
    A.GaussianBlur(p=0.2),
    A.CoarseDropout(max_holes=8, max_height=8, max_width=8, p=0.3)
])
7.1.2 文本数据增强
python 复制代码
import nlpaug.augmenter.word as naw

# 同义词替换
aug = naw.SynonymAug(aug_src='wordnet')
augmented_text = aug.augment(original_text)

# 回译(英文→其他语言→英文)
from googletrans import Translator

translator = Translator()
def back_translate(text, intermediate_lang='fr'):
    translated = translator.translate(text, dest=intermediate_lang).text
    back = translator.translate(translated, dest='en').text
    return back

# 上下文词嵌入替换(BERT-based)
aug = naw.ContextualWordEmbsAug(
    model_path='bert-base-uncased',
    action="substitute"
)
7.1.3 时间序列增强
python 复制代码
# 时间扭曲
def time_warp(x, sigma=0.2):
    return x * (1 + np.random.normal(0, sigma, x.shape))

# 幅度缩放
def magnitude_scale(x, sigma=0.1):
    scale = np.random.normal(1, sigma)
    return x * scale

# 添加噪声
def add_noise(x, sigma=0.05):
    noise = np.random.normal(0, sigma, x.shape)
    return x + noise

7.2 批标准化的考虑

python 复制代码
import torch.nn as nn

# 在模型中使用BatchNorm时,输入不需要提前标准化
# BatchNorm会在训练过程中学习归一化参数

class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        self.conv1 = nn.Conv2d(3, 64, 3)
        self.bn1 = nn.BatchNorm2d(64)  # 自动归一化
        self.relu = nn.ReLU()
        
    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)  # 归一化激活值
        x = self.relu(x)
        return x

注意

  • 训练时使用批次统计量
  • 推理时使用训练时累积的全局统计量
  • 小批量(batch_size < 32)时效果可能不佳,考虑GroupNorm或LayerNorm

7.3 序列数据的填充与截断

python 复制代码
from tensorflow.keras.preprocessing.sequence import pad_sequences

# 文本序列
sequences = [[1, 2, 3], [4, 5], [6, 7, 8, 9, 10]]

# 填充到相同长度
padded = pad_sequences(sequences, maxlen=5, padding='post', truncating='post')
# 结果:
# [[1, 2, 3, 0, 0],
#  [4, 5, 0, 0, 0],
#  [6, 7, 8, 9, 10]]

# PyTorch版本
from torch.nn.utils.rnn import pad_sequence
import torch

sequences = [torch.tensor(seq) for seq in sequences]
padded = pad_sequence(sequences, batch_first=True, padding_value=0)

7.4 预训练模型的输入要求

python 复制代码
# 使用预训练模型时,必须匹配其预处理方式

# ResNet (ImageNet预训练)
from torchvision.models import resnet50, ResNet50_Weights

weights = ResNet50_Weights.DEFAULT
preprocess = weights.transforms()  # 自动获取正确的预处理

# BERT (文本)
from transformers import BertTokenizer

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
inputs = tokenizer(
    text,
    padding='max_length',
    truncation=True,
    max_length=512,
    return_tensors='pt'
)

8. 实战流程与最佳实践

8.1 完整的预处理Pipeline

python 复制代码
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer

# 定义特征类型
numerical_features = ['age', 'income', 'credit_score']
categorical_features = ['city', 'occupation', 'education']

# 数值特征管道
numerical_transformer = Pipeline(steps=[
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())
])

# 类别特征管道
categorical_transformer = Pipeline(steps=[
    ('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
    ('onehot', OneHotEncoder(handle_unknown='ignore', sparse_output=False))
])

# 组合
preprocessor = ColumnTransformer(
    transformers=[
        ('num', numerical_transformer, numerical_features),
        ('cat', categorical_transformer, categorical_features)
    ])

# 使用
X_train_processed = preprocessor.fit_transform(X_train)
X_test_processed = preprocessor.transform(X_test)  # ⚠️ 只transform,不fit

8.2 训练集/验证集/测试集的处理原则

8.2.1 核心原则
  1. 只在训练集上fit预处理器
  2. 在验证集和测试集上只transform
  3. 时间序列必须按时间划分
python 复制代码
from sklearn.model_selection import train_test_split

# 划分数据集
X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.3, random_state=42)
X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42)

# ✅ 正确做法
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)      # fit + transform
X_val_scaled = scaler.transform(X_val)              # 只transform
X_test_scaled = scaler.transform(X_test)            # 只transform

# ❌ 错误做法(会导致数据泄露)
X_train_scaled = StandardScaler().fit_transform(X_train)
X_val_scaled = StandardScaler().fit_transform(X_val)  # 不应该重新fit!
8.2.2 时间序列特殊处理
python 复制代码
# ❌ 错误:随机划分会导致未来信息泄露
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

# ✅ 正确:按时间顺序划分
split_point = int(len(X) * 0.8)
X_train, X_test = X[:split_point], X[split_point:]
y_train, y_test = y[:split_point], y[split_point:]

# 或使用TimeSeriesSplit
from sklearn.model_selection import TimeSeriesSplit

tscv = TimeSeriesSplit(n_splits=5)
for train_index, test_index in tscv.split(X):
    X_train, X_test = X[train_index], X[test_index]
    y_train, y_test = y[train_index], y[test_index]

8.3 数据泄露的预防

8.3.1 常见数据泄露场景
  1. 在全数据集上fit预处理器
python 复制代码
# ❌ 数据泄露
scaler = StandardScaler().fit(X)  # 包含了测试集信息
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
  1. 使用未来信息
python 复制代码
# ❌ 数据泄露:用整个序列的统计量填充缺失值
df['price'].fillna(df['price'].mean())  # mean包含了未来数据

# ✅ 正确:用历史均值填充
df['price'] = df['price'].fillna(method='ffill')
  1. 特征选择时使用全数据
python 复制代码
# ❌ 数据泄露
selector = SelectKBest(k=10)
X_selected = selector.fit_transform(X, y)  # 包含测试集
X_train, X_test = train_test_split(X_selected)

# ✅ 正确
X_train, X_test, y_train, y_test = train_test_split(X, y)
selector = SelectKBest(k=10).fit(X_train, y_train)
X_train_selected = selector.transform(X_train)
X_test_selected = selector.transform(X_test)
  1. 目标编码时的泄露
python 复制代码
# ❌ 数据泄露
category_means = df.groupby('category')['target'].mean()
df['category_encoded'] = df['category'].map(category_means)

# ✅ 正确:使用交叉验证
from category_encoders import TargetEncoder
encoder = TargetEncoder()
# 在Pipeline中使用,自动处理

8.4 常用工具库介绍

工具库 用途 安装
Pandas 数据处理基础 pip install pandas
NumPy 数值计算 pip install numpy
Scikit-learn 经典ML预处理 pip install scikit-learn
Featuretools 自动特征工程 pip install featuretools
Category Encoders 高级类别编码 pip install category-encoders
Imbalanced-learn 不平衡数据处理 pip install imbalanced-learn
Missingno 缺失值可视化 pip install missingno
Optuna 超参数优化 pip install optuna
Great Expectations 数据质量验证 pip install great-expectations

9. 常见误区与陷阱

❌ 误区1:忽略数据探索(EDA)

错误做法 :拿到数据直接建模
正确做法:先进行充分的探索性分析

python 复制代码
# 必做的EDA步骤
df.head()
df.info()
df.describe()
df.isnull().sum()
df.corr()
sns.pairplot(df)

❌ 误区2:盲目删除缺失值

错误做法df.dropna() 一删了之
后果

  • 损失大量信息
  • 引入样本偏差
  • 缺失本身可能是重要特征

正确做法

  1. 分析缺失模式(MCAR/MAR/MNAR)
  2. 考虑填充方法
  3. 创建缺失指示器

❌ 误区3:对测试集fit预处理器

详见8.2节


❌ 误区4:对树模型过度预处理

事实

  • 决策树/随机森林/XGBoost对特征尺度不敏感
  • 不需要标准化/归一化
  • 可以直接处理缺失值(XGBoost/LightGBM)

建议

  • 树模型:最小化预处理
  • 线性模型/神经网络:需要完整预处理

❌ 误区5:过度特征工程

表现

  • 创建数百个特征
  • 大量特征交叉
  • 复杂的手工特征

后果

  • 过拟合
  • 训练慢
  • 难以解释

建议

  • 从简单开始,逐步增加
  • 用特征选择控制复杂度
  • 监控验证集性能

❌ 误区6:忽略类别不平衡

python 复制代码
# 检查类别分布
print(y.value_counts())

# 处理方法
from imblearn.over_sampling import SMOTE
from imblearn.under_sampling import RandomUnderSampler

# 过采样
smote = SMOTE(random_state=42)
X_resampled, y_resampled = smote.fit_resample(X, y)

# 类权重
from sklearn.ensemble import RandomForestClassifier
model = RandomForestClassifier(class_weight='balanced')

❌ 误区7:归一化/标准化混淆

场景 使用
特征服从正态分布 StandardScaler
特征有明确边界 MinMaxScaler
有异常值 RobustScaler
神经网络(通用) MinMaxScaler或StandardScaler
树模型 不需要

10. 扩展阅读与进阶方向

10.1 推荐书籍

  1. 《Feature Engineering for Machine Learning》 - Alice Zheng & Amanda Casari
  2. 《Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow》 - Aurélien Géron
  3. 《Python for Data Analysis》 - Wes McKinney(Pandas作者)

10.2 进阶主题

10.2.1 自动机器学习(AutoML)
python 复制代码
# TPOT:自动化特征工程+模型选择
from tpot import TPOTClassifier

tpot = TPOTClassifier(generations=5, population_size=20, verbosity=2)
tpot.fit(X_train, y_train)
print(tpot.score(X_test, y_test))
tpot.export('best_pipeline.py')

# H2O AutoML
import h2o
from h2o.automl import H2OAutoML

h2o.init()
aml = H2OAutoML(max_models=20, seed=1)
aml.train(x=x, y=y, training_frame=train)
10.2.2 特征存储(Feature Store)

目的:集中管理、共享、版本化特征

工具

  • Feast
  • Tecton
  • Hopsworks
10.2.3 流式数据预处理
python 复制代码
# 使用River(前身:creme)
from river import preprocessing, stream

# 在线标准化
scaler = preprocessing.StandardScaler()

for x, y in stream.iter_csv('data.csv'):
    scaler.learn_one(x)
    x_scaled = scaler.transform_one(x)
10.2.4 大规模数据处理
python 复制代码
# Dask:并行化Pandas操作
import dask.dataframe as dd

ddf = dd.read_csv('large_file.csv')
result = ddf.groupby('category').mean().compute()

# PySpark
from pyspark.sql import SparkSession
from pyspark.ml.feature import VectorAssembler, StandardScaler

spark = SparkSession.builder.appName("FeatureEngineering").getOrCreate()
df = spark.read.csv('data.csv', header=True, inferSchema=True)

assembler = VectorAssembler(inputCols=['feature1', 'feature2'], outputCol='features')
scaler = StandardScaler(inputCol='features', outputCol='scaled_features')

10.3 在线资源

10.4 竞赛学习

推荐平台

  • Kaggle
    -天池(阿里云)
  • DataFountain

学习路径

  1. 从简单竞赛开始
  2. 研究Top解决方案
  3. 复现他人代码
  4. 总结特征工程模式

附录:完整代码示例

A.1 端到端示例:泰坦尼克号生存预测

python 复制代码
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report

# 1. 加载数据
df = pd.read_csv('titanic.csv')

# 2. 探索性数据分析(简化)
print(df.head())
print(df.info())
print(df.isnull().sum())

# 3. 特征工程
# 3.1 处理缺失值
df['Age'].fillna(df['Age'].median(), inplace=True)
df['Embarked'].fillna(df['Embarked'].mode()[0], inplace=True)
df.drop('Cabin', axis=1, inplace=True)  # 缺失过多

# 3.2 特征构造
df['FamilySize'] = df['SibSp'] + df['Parch'] + 1
df['IsAlone'] = (df['FamilySize'] == 1).astype(int)
df['Title'] = df['Name'].str.extract(' ([A-Za-z]+)\.', expand=False)
df['Title'] = df['Title'].replace(['Lady', 'Countess', 'Capt', 'Col', 'Don', 
                                     'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona'], 'Rare')
df['Title'] = df['Title'].replace('Mlle', 'Miss')
df['Title'] = df['Title'].replace('Ms', 'Miss')
df['Title'] = df['Title'].replace('Mme', 'Mrs')

# 3.3 编码类别特征
label_encoders = {}
for col in ['Sex', 'Embarked', 'Title']:
    le = LabelEncoder()
    df[col] = le.fit_transform(df[col])
    label_encoders[col] = le

# 3.4 选择特征
features = ['Pclass', 'Sex', 'Age', 'Fare', 'Embarked', 'FamilySize', 'IsAlone', 'Title']
X = df[features]
y = df['Survived']

# 4. 划分数据集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 5. 标准化(可选,随机森林不需要)
# scaler = StandardScaler()
# X_train_scaled = scaler.fit_transform(X_train)
# X_test_scaled = scaler.transform(X_test)

# 6. 训练模型
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)

# 7. 评估
y_pred = model.predict(X_test)
print(f"准确率: {accuracy_score(y_test, y_pred):.4f}")
print(classification_report(y_test, y_pred))

# 8. 特征重要性
feature_importance = pd.Series(model.feature_importances_, index=features).sort_values(ascending=False)
print("\n特征重要性:")
print(feature_importance)

总结

数据清理和特征工程是机器学习成功的关键。本笔记涵盖了:

数据清理 :缺失值、异常值、重复数据、类型转换

特征预处理 :数值/类别/时间/文本/图像特征处理

特征构造 :交叉、聚合、领域知识

特征选择 :过滤/包装/嵌入法、降维

深度学习 :数据增强、批标准化、序列处理

最佳实践:Pipeline、数据泄露预防、工具库

关键要点

  1. 先理解数据:EDA是第一步
  2. 防止数据泄露:测试集绝不参与fit
  3. 根据模型选择方法:树模型vs线性模型vs深度学习
  4. 迭代优化:从简单开始,监控验证集性能
  5. 保持可复现性:Pipeline + 版本控制
相关推荐
老王熬夜敲代码2 小时前
计算机网络--IP概念
linux·网络·笔记
LDG_AGI2 小时前
【推荐系统】深度学习训练框架(十九):TorchRec之DistributedModelParallel
人工智能·深度学习·机器学习·推荐算法
小陈phd2 小时前
大语言模型实战(二)——Transformer网络架构解读
人工智能·深度学习·transformer
悠哉悠哉愿意3 小时前
【EDA学习笔记】电子技术基础知识:元件数据手册
笔记·单片机·嵌入式硬件·学习·eda
德福危险3 小时前
C语言数据类型与变量 系统总结笔记
c语言·笔记·算法
LDG_AGI3 小时前
【推荐系统】深度学习训练框架(十七):TorchRec之KeyedJaggedTensor
人工智能·pytorch·深度学习·机器学习·数据挖掘·embedding
CoovallyAIHub3 小时前
从电影特效到体育科学,运动追踪只能靠“人眼”吗?
深度学习·算法·计算机视觉
paopao_wu3 小时前
深度学习3:理解神经网络
人工智能·深度学习·神经网络
EXtreme353 小时前
【DL】从零构建智能:神经网络前向传播、反向传播与激活函数深度解密
人工智能·深度学习·神经网络·梯度下降·反向传播·链式法则