学习笔记
2026-07-11 · 约9分钟阅读

特征工程笔记:类别变量处理与特征选择

机器学习 特征工程 数据预处理

类别变量的类型

类别变量分为两种类型,处理方式完全不同:

类别变量处理方法

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将特征选择和模型训练结合,避免数据泄露

特征选择陷阱

陷阱一:数据泄露

# ❌ 错误:在整个数据集上进行特征选择
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)

总结

特征工程是机器学习的核心,好的特征比好的模型更重要。关键要点:

  1. 区分类别变量类型:有序变量用Label Encoding,名义变量用One-Hot/Binary Encoding
  2. 处理高基数类别:使用Target Encoding、Frequency Encoding或合并低频类别
  3. 避免数据泄露:始终在训练集上拟合预处理和特征选择,然后在测试集上转换
  4. 使用Pipeline:将预处理、特征选择和模型训练串联起来,确保流程正确
  5. 交叉验证特征选择:用交叉验证选择合适的特征数量,避免过拟合

记住:特征工程是迭代过程。先从简单的方法开始(One-Hot + 方差过滤),建立基准,然后逐步尝试更复杂的方法(Target Encoding + RFE),直到达到满意的效果。

🤖
AI助手
ONLINE