刚入行那会儿,我也以为机器学习就是选个算法调调参。直到在真实项目中碰得头破血流才明白:模型表现80%取决于数据质量,而算法选择只占20%。就像米其林大厨再厉害,用烂菜叶子也做不出美味。
数据预处理就像给食材做初加工——清洗、切配、腌制。特征工程则是更精细的刀工处理和调味搭配。我见过太多团队把时间全花在模型调优上,结果发现瓶颈其实在数据层。去年我们优化一个推荐系统,仅通过改进用户行为数据的清洗逻辑,就让A/B测试指标提升了37%,这比换任何算法都立竿见影。
缺失值处理:
最近帮某银行做风控模型时,发现30%的用户职业字段为空。直接删除会损失大量样本,我们最终采用分层填充:
异常值检测:
在电商价格数据中,我用三种方法交叉验证:
python复制# IQR方法
Q1 = df['price'].quantile(0.25)
Q3 = df['price'].quantile(0.75)
IQR = Q3 - Q1
outliers = df[(df['price'] < (Q1 - 1.5*IQR)) | (df['price'] > (Q3 + 1.5*IQR))]
# 3σ原则
mean = df['price'].mean()
std = df['price'].std()
outliers = df[np.abs(df['price']-mean) > 3*std]
# 业务规则(如价格>100万视为异常)
注意:异常值不一定要删除!在反欺诈场景中,异常值可能就是关键信号。
非正态分布处理:
遇到右偏的收入数据时,Box-Cox变换比简单取对数效果更好:
python复制from scipy.stats import boxcox
df['income'], _ = boxcox(df['income'] + 1) # +1避免0值
时间特征分解:
处理销售预测数据时,除了提取年月日,还要分解出:
python复制df['is_weekend'] = df['date'].dt.dayofweek >= 5
df['is_month_end'] = df['date'].dt.is_month_end
df['quarter'] = df['date'].dt.quarter
在用户画像项目中,我们通过组合原始字段创造出高价值特征:
用ElasticNet做特征筛选时,我发现调整alpha和l1_ratio的组合能获得不同稀疏度:
python复制from sklearn.linear_model import ElasticNetCV
en = ElasticNetCV(l1_ratio=[.1, .5, .7, .9, .95, .99],
n_alphas=100, cv=5)
en.fit(X, y)
print(f"最佳alpha: {en.alpha_}, 最佳l1_ratio: {en.l1_ratio_}")
selected = [f for f, coef in zip(features, en.coef_) if abs(coef) > 0]
通过featuretools库自动生成特征交互:
python复制import featuretools as ft
es = ft.EntitySet()
es = es.entity_from_dataframe(entity_id='data',
dataframe=df,
index='user_id')
features, defs = ft.dfs(entityset=es,
target_entity='data',
max_depth=2) # 控制交互深度
做新闻分类时,传统TF-IDF遇到维度爆炸问题。我们的解决方案:
python复制from sentence_transformers import SentenceTransformer
model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
embeddings = model.encode(texts)
处理医学影像时,发现直接使用预训练CNN特征效果不佳。改进方案:
python复制import torch
model = torch.hub.load('pytorch/vision', 'resnet50', pretrained=True)
modules = list(model.children())[:-1] # 去掉最后一层
feature_extractor = torch.nn.Sequential(*modules)
features = feature_extractor(images)
处理千万级数据时,内存经常爆掉。我们总结的优化方案:
python复制df['city'] = df['city'].astype('category') # 内存减少90%
数据泄露:
在时间序列中,绝对不能用未来数据做归一化!应该:
python复制scaler = StandardScaler()
train_scaled = scaler.fit_transform(train)
test_scaled = scaler.transform(test) # 不能用fit_transform!
维度诅咒:
当特征数>样本数时,一定要先降维再训练。我们常用谱聚类降维:
python复制from sklearn.decomposition import TruncatedSVD
svd = TruncatedSVD(n_components=100)
X_reduced = svd.fit_transform(X)
用sklearn Pipeline封装预处理流程:
python复制from sklearn.pipeline import make_pipeline
from sklearn.compose import ColumnTransformer
numeric_transformer = make_pipeline(
SimpleImputer(strategy='median'),
StandardScaler())
categorical_transformer = make_pipeline(
SimpleImputer(strategy='constant'),
OneHotEncoder(handle_unknown='ignore'))
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)])
full_pipeline = make_pipeline(
preprocessor,
SelectKBest(score_func=f_classif, k=100),
RandomForestClassifier())
我们采用的层次化特征存储架构:
python复制# 使用Feast特征库
from feast import FeatureStore
store = FeatureStore(repo_path=".")
features = store.get_online_features(
features=[
'user_stats:credit_score',
'user_stats:avg_order_value'
],
entity_rows=[{"user_id": 123}]
).to_dict()
除了常规的permutation importance,我们还用SHAP值解释特征:
python复制import shap
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X)
shap.summary_plot(shap_values, X, plot_type="bar")
在特征服务中埋入数据质量检查点:
python复制from alibi_detect import KSDrift
drift_detector = KSDrift(X_ref, p_val=0.05)
preds = drift_detector.predict(X_new)
经过多个项目实战,我总结出一个黄金法则:在特征工程上多花1小时,能在模型调优上节省10小时。最近我们团队甚至把特征工程环节独立出来,成立了专门的特征平台组,用工程化的方法管理整个特征生命周期。