数据清理与特征工程完整指南
目录
- 背景与动机
- [数据清理(Data Cleaning)](#数据清理(Data Cleaning))
- 特征工程基础
- 特征预处理
- 特征构造与提取
- 特征选择
- 深度学习特有的预处理
- 实战流程与最佳实践
- 常见误区与陷阱
- 扩展阅读与进阶方向
1. 背景与动机
1.1 为什么需要数据预处理?
在机器学习和深度学习项目中,数据预处理通常占据 60-80% 的时间,这是因为:
-
原始数据往往不可直接使用
- 缺失值、异常值、不一致格式
- 数据规模不统一、分布不均衡
- 噪声数据影响模型学习
-
模型性能高度依赖数据质量
- "Garbage In, Garbage Out"(垃圾进,垃圾出)
- 高质量特征 > 复杂模型(很多场景下)
-
不同模型对数据有不同要求
- 线性模型需要特征缩放
- 树模型对缺失值更宽容
- 深度学习需要大量数据和标准化输入
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 核心原则
- 只在训练集上fit预处理器
- 在验证集和测试集上只transform
- 时间序列必须按时间划分
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 常见数据泄露场景
- 在全数据集上fit预处理器
python
# ❌ 数据泄露
scaler = StandardScaler().fit(X) # 包含了测试集信息
X_train_scaled = scaler.transform(X_train)
X_test_scaled = scaler.transform(X_test)
- 使用未来信息
python
# ❌ 数据泄露:用整个序列的统计量填充缺失值
df['price'].fillna(df['price'].mean()) # mean包含了未来数据
# ✅ 正确:用历史均值填充
df['price'] = df['price'].fillna(method='ffill')
- 特征选择时使用全数据
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)
- 目标编码时的泄露
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() 一删了之
后果:
- 损失大量信息
- 引入样本偏差
- 缺失本身可能是重要特征
正确做法:
- 分析缺失模式(MCAR/MAR/MNAR)
- 考虑填充方法
- 创建缺失指示器
❌ 误区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 推荐书籍
- 《Feature Engineering for Machine Learning》 - Alice Zheng & Amanda Casari
- 《Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow》 - Aurélien Géron
- 《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 在线资源
- Kaggle Learn:https://www.kaggle.com/learn
- Fast.ai Practical Deep Learning:https://course.fast.ai/
- Scikit-learn用户指南:https://scikit-learn.org/stable/user_guide.html
- Papers with Code:https://paperswithcode.com/
10.4 竞赛学习
推荐平台:
- Kaggle
-天池(阿里云) - DataFountain
学习路径:
- 从简单竞赛开始
- 研究Top解决方案
- 复现他人代码
- 总结特征工程模式
附录:完整代码示例
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、数据泄露预防、工具库
关键要点:
- 先理解数据:EDA是第一步
- 防止数据泄露:测试集绝不参与fit
- 根据模型选择方法:树模型vs线性模型vs深度学习
- 迭代优化:从简单开始,监控验证集性能
- 保持可复现性:Pipeline + 版本控制