类别变量的类型
类别变量分为两种类型,处理方式完全不同:
- 名义变量(Nominal):没有顺序关系的类别,如性别、颜色、城市
- 有序变量(Ordinal):有顺序关系的类别,如学历(小学<初中<高中<大学)、评分(1-5星)
类别变量处理方法
1. Label Encoding(标签编码)
将类别映射为整数,适用于有序变量。
from sklearn.preprocessing import LabelEncoder
df = pd.DataFrame({'education': ['小学', '初中', '高中', '大学', '研究生']})
le = LabelEncoder()
df['education_encoded'] = le.fit_transform(df['education'])
print(df)
# education education_encoded
# 0 小学 1
# 1 初中 2
# 2 高中 3
# 3 大学 0
# 4 研究生 4
⚠️ 警告:Label Encoding会引入数值大小关系,比如"大学"=0,"研究生"=4,模型会认为研究生>大学,但对于名义变量这是不合理的。
2. One-Hot Encoding(独热编码)
为每个类别创建二进制列,适用于名义变量和类别数量较少的情况。
from sklearn.preprocessing import OneHotEncoder
df = pd.DataFrame({'color': ['红', '绿', '蓝', '红', '绿']})
ohe = OneHotEncoder(sparse_output=False, drop='first')
encoded = ohe.fit_transform(df)
encoded_df = pd.DataFrame(encoded, columns=ohe.get_feature_names_out())
print(encoded_df)
# color_绿 color_蓝
# 0 0.0 0.0
# 1 1.0 0.0
# 2 0.0 1.0
# 3 0.0 0.0
# 4 1.0 0.0
✅ drop='first'的作用:删除第一列,避免多重共线性问题。比如颜色只有红、绿、蓝三种,如果知道不是绿也不是蓝,那一定是红。
3. Binary Encoding(二进制编码)
将类别编码为二进制数,适合类别数量较多的情况。
import category_encoders as ce
df = pd.DataFrame({'category': ['A', 'B', 'C', 'D', 'E']})
encoder = ce.BinaryEncoder()
encoded = encoder.fit_transform(df)
print(encoded)
# category_0 category_1 category_2
# 0 0 0 1
# 1 0 1 0
# 2 0 1 1
# 3 1 0 0
# 4 1 0 1
✅ 何时使用Binary Encoding?当类别数量较多(>10)但又不想使用Embedding时,Binary Encoding是一个好选择,它产生的特征维度是log2(n)。
4. Target Encoding(目标编码)
用目标变量的统计值编码类别,适合高基数类别变量。
import category_encoders as ce
df = pd.DataFrame({
'city': ['北京', '上海', '北京', '广州', '上海', '深圳'],
'price': [100, 120, 95, 80, 115, 90]
})
encoder = ce.TargetEncoder(cols=['city'])
df['city_encoded'] = encoder.fit_transform(df['city'], df['price'])
print(df)
# city price city_encoded
# 0 北京 100 97.500000
# 1 上海 120 117.500000
# 2 北京 95 97.500000
# 3 广州 80 80.000000
# 4 上海 115 117.500000
# 5 深圳 90 90.000000
⚠️ 目标编码的陷阱:直接在整个数据集上使用目标编码会导致数据泄露。必须在训练集上拟合,然后在测试集上转换。
encoder = ce.TargetEncoder(cols=['city'])
X_train['city_encoded'] = encoder.fit_transform(X_train['city'], y_train)
X_test['city_encoded'] = encoder.transform(X_test['city'])
5. Frequency Encoding(频率编码)
用类别出现的频率编码,适合高基数类别变量。
df = pd.DataFrame({'product': ['A', 'B', 'A', 'C', 'A', 'B']})
frequency = df['product'].value_counts(normalize=True)
df['product_freq'] = df['product'].map(frequency)
print(df)
# product product_freq
# 0 A 0.500000
# 1 B 0.333333
# 2 A 0.500000
# 3 C 0.166667
# 4 A 0.500000
# 5 B 0.333333
类别变量处理方法对比
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| Label Encoding | 有序变量 | 简单、保留顺序信息 | 引入虚假数值关系 |
| One-Hot Encoding | 低基数名义变量 | 无虚假关系、适用于大多数模型 | 维度爆炸(高基数时) |
| Binary Encoding | 中高基数名义变量 | 维度适中、保留类别信息 | 需要额外库、引入数值关系 |
| Target Encoding | 高基数类别变量 | 利用目标信息、维度低 | 容易过拟合、数据泄露风险 |
| Frequency Encoding | 高基数类别变量 | 简单、维度低、无数据泄露 | 不利用目标信息 |
特征选择方法
1. 过滤法(Filter Methods)
基于统计指标筛选特征,不依赖具体模型。
方差过滤
from sklearn.feature_selection import VarianceThreshold
selector = VarianceThreshold(threshold=0.1)
X_selected = selector.fit_transform(X)
print(f'原始特征数: {X.shape[1]}')
print(f'筛选后特征数: {X_selected.shape[1]}')
互信息(Mutual Information)
from sklearn.feature_selection import mutual_info_classif
mi_scores = mutual_info_classif(X, y)
mi_df = pd.DataFrame({
'feature': X.columns,
'mi_score': mi_scores
}).sort_values('mi_score', ascending=False)
print(mi_df)
卡方检验(Chi-squared)
from sklearn.feature_selection import chi2, SelectKBest
selector = SelectKBest(chi2, k=10)
X_selected = selector.fit_transform(X, y)
selected_features = X.columns[selector.get_support()]
print(f'选中的特征: {selected_features}')
2. 包装法(Wrapper Methods)
用模型性能评估特征子集,计算量大但效果好。
递归特征消除(RFE)
from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
estimator = LogisticRegression()
selector = RFE(estimator, n_features_to_select=10, step=1)
selector = selector.fit(X, y)
print(f'选中的特征: {X.columns[selector.support_]}')
print(f'特征排名: {selector.ranking_}')
逐步特征选择
from mlxtend.feature_selection import SequentialFeatureSelector
sfs = SequentialFeatureSelector(
LogisticRegression(),
k_features=10,
forward=True,
floating=False,
scoring='accuracy',
cv=5,
n_jobs=-1
)
sfs = sfs.fit(X, y)
print(f'选中的特征: {list(sfs.k_feature_names_)}')
3. 嵌入法(Embedded Methods)
特征选择作为模型训练的一部分,同时进行。
L1正则化(Lasso)
from sklearn.linear_model import Lasso
lasso = Lasso(alpha=0.1)
lasso.fit(X, y)
coef_df = pd.DataFrame({
'feature': X.columns,
'coefficient': lasso.coef_
}).sort_values('coefficient', key=abs, ascending=False)
print(coef_df[coef_df['coefficient'] != 0])
树模型特征重要性
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X, y)
importances = pd.DataFrame({
'feature': X.columns,
'importance': rf.feature_importances_
}).sort_values('importance', ascending=False)
print(importances.head(10))
✅ 特征选择方法选择策略:
- 快速筛选:方差过滤 + 互信息
- 小数据集:RFE或逐步特征选择
- 大数据集:树模型特征重要性或L1正则化
- 最终验证:用Pipeline将特征选择和模型训练结合,避免数据泄露
- 快速筛选:方差过滤 + 互信息
- 小数据集:RFE或逐步特征选择
- 大数据集:树模型特征重要性或L1正则化
- 最终验证:用Pipeline将特征选择和模型训练结合,避免数据泄露
特征选择陷阱
陷阱一:数据泄露
# ❌ 错误:在整个数据集上进行特征选择
selector = SelectKBest(chi2, k=10)
X_selected = selector.fit_transform(X, y)
X_train, X_test, y_train, y_test = train_test_split(X_selected, y)
# ✅ 正确:只在训练集上拟合,然后在测试集上转换
X_train, X_test, y_train, y_test = train_test_split(X, y)
selector = SelectKBest(chi2, k=10)
X_train_selected = selector.fit_transform(X_train, y_train)
X_test_selected = selector.transform(X_test)
陷阱二:忽略特征之间的相关性
# ✅ 计算特征相关性矩阵
corr_matrix = X.corr()
# 找到高度相关的特征对
high_corr_pairs = []
for i in range(len(corr_matrix.columns)):
for j in range(i):
if abs(corr_matrix.iloc[i, j]) > 0.8:
high_corr_pairs.append((corr_matrix.columns[i], corr_matrix.columns[j]))
print(f'高度相关的特征对: {high_corr_pairs}')
陷阱三:过度选择特征
特征越多不一定越好,过多特征会导致模型复杂度过高、训练时间长、泛化能力差。
# ✅ 使用交叉验证选择合适的特征数量
for k in [5, 10, 15, 20, 30]:
selector = SelectKBest(chi2, k=k)
X_train_selected = selector.fit_transform(X_train, y_train)
X_test_selected = selector.transform(X_test)
model = LogisticRegression()
model.fit(X_train_selected, y_train)
score = model.score(X_test_selected, y_test)
print(f'特征数={k}, 准确率={score:.4f}')
实用技巧
1. 处理高基数类别变量
df = pd.DataFrame({'city': ['北京'] * 100 + ['上海'] * 80 + ['广州'] * 50 + ['深圳'] * 30 + ['其他'] * 200})
# ✅ 将低频类别合并为"其他"
frequency = df['city'].value_counts(normalize=True)
threshold = 0.05
high_freq_cities = frequency[frequency >= threshold].index.tolist()
df['city_grouped'] = df['city'].apply(lambda x: x if x in high_freq_cities else '其他')
print(df['city_grouped'].value_counts())
2. 特征交叉
df = pd.DataFrame({
'age_group': ['青年', '中年', '老年'],
'income_group': ['低', '中', '高']
})
# ✅ 创建特征交叉
df['age_income'] = df['age_group'] + '_' + df['income_group']
print(df['age_income'].unique())
# ['青年_低' '中年_中' '老年_高']
3. 使用Pipeline整合所有步骤
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.compose import ColumnTransformer
numeric_features = ['age', 'income']
categorical_features = ['city', 'education']
preprocessor = ColumnTransformer([
('num', StandardScaler(), numeric_features),
('cat', OneHotEncoder(drop='first'), categorical_features)
])
pipeline = Pipeline([
('preprocessor', preprocessor),
('selector', SelectKBest(chi2, k=10)),
('model', LogisticRegression())
])
pipeline.fit(X_train, y_train)
score = pipeline.score(X_test, y_test)
总结
特征工程是机器学习的核心,好的特征比好的模型更重要。关键要点:
- 区分类别变量类型:有序变量用Label Encoding,名义变量用One-Hot/Binary Encoding
- 处理高基数类别:使用Target Encoding、Frequency Encoding或合并低频类别
- 避免数据泄露:始终在训练集上拟合预处理和特征选择,然后在测试集上转换
- 使用Pipeline:将预处理、特征选择和模型训练串联起来,确保流程正确
- 交叉验证特征选择:用交叉验证选择合适的特征数量,避免过拟合
记住:特征工程是迭代过程。先从简单的方法开始(One-Hot + 方差过滤),建立基准,然后逐步尝试更复杂的方法(Target Encoding + RFE),直到达到满意的效果。