特征工程决定了模型效果的上限,但它恰恰是整个建模流程中最琐碎的部分。清洗缺失值、衍生新特征、编码类别变量,这些工作往往占据了数据科学家一半以上的时间。随着AI编程助手能力不断增强,用自然语言描述需求就能得到一段可运行的特征工程代码,不过要让生成的代码真正可用、可复用、可维护,还得把它规范地组织到Pandas管道和Scikit-learn转换器的体系里。本文围绕这条思路展开,介绍如何借助AI高效产出高质量的特征工程代码。

为什么特征工程代码需要管道化
先看一段常见的写法。很多入门者在处理数据时,会写出一大段平铺直叙的代码:先fillna填补缺失值,接着map做编码,然后手动拼接新列。这种代码跑一次没问题,但一旦换数据集、换项目,就得整段复制粘贴再改,训练和推理阶段的逻辑还容易不一致——训练时做了对数变换,预测时忘了做,模型效果直接崩掉。
管道化的核心价值在于两点。第一是复用:把每一步特征处理封装成独立函数或类,任何数据集都能直接套用。第二是防泄漏:Scikit-learn的Pipeline机制会保证fit阶段学到的统计量(比如均值、众数、分箱边界)只在训练集上计算,transform阶段统一应用到训练集和测试集,从机制上杜绝数据泄漏。这一点在交叉验证里尤其重要,手写的预处理代码很难保证每个fold都正确地重新拟合。
用AI生成代码时,管道化还有个隐性好处:结构化的目标让提示词更明确。与其让AI生成一坨处理脚本,不如明确要求它输出一个继承BaseEstimator和TransformerMixin的类,生成结果的质量会明显提升。
用Pandas的pipe方法组织特征处理链
Pandas从0.16版开始提供了pipe方法,可以把DataFrame依次传入一系列自定义函数,形成链式调用。它的签名是df.pipe(func, *args, **kwargs),func的第一个参数必须是DataFrame本身。这种方式把特征处理逻辑拆成一个个小函数,读起来就像流水线工序表。
import pandas as pd
import numpy as np
def fill_missing(df, num_strategy='median'):
"""填补缺失值:数值列用中位数,类别列用众数"""
for col in df.columns:
if df[col].dtype in (np.int64, np.float64):
df[col] = df[col].fillna(df[col].median() if num_strategy == 'median' else df[col].mean())
else:
df[col] = df[col].fillna(df[col].mode()[0])
return df
def extract_time_features(df, time_col):
"""从时间列衍生出年、月、小时特征"""
dt = pd.to_datetime(df[time_col])
df['year'] = dt.dt.year
df['month'] = dt.dt.month
df['hour'] = dt.dt.hour
return df.drop(columns=[time_col])
def clip_outliers(df, col, lower=0.01, upper=0.99):
"""按分位数截断极端值"""
lo, hi = df[col].quantile([lower, upper])
df[col] = df[col].clip(lo, hi)
return df
# 链式调用,处理顺序一目了然
result = (df
.pipe(fill_missing, num_strategy='mean')
.pipe(extract_time_features, time_col='signup_time')
.pipe(clip_outliers, col='amount'))这段代码结构清晰,每个函数职责单一,单独测试也方便。让AI生成这类代码时,可以在提示词中写明:请生成三个独立的特征处理函数,每个函数接收DataFrame并返回DataFrame,最后用pipe方法串联。给出函数签名约束后,AI输出的代码基本不需要二次调整。
需要注意的是,Pandas的pipe适合无状态的转换,也就是不依赖训练集统计量的操作,比如字符串清洗、时间特征提取。一旦处理逻辑需要在训练集上拟合参数(标准化、目标编码),就应该交给下一节介绍的Scikit-learn转换器,否则统计量会在全量数据上计算,造成信息泄漏。
编写规范的Scikit-learn自定义转换器
Scikit-learn的Transformer遵循一套固定协议:fit方法负责从训练数据学习参数并返回self,transform方法负责应用转换。只要继承BaseEstimator和TransformerMixin,并实现这两个方法,自定义的类就能无缝嵌入Pipeline和ColumnTransformer,还能参与网格搜索调参。
from sklearn.base import BaseEstimator, TransformerMixin
class RareCategoryGrouper(BaseEstimator, TransformerMixin):
"""把出现频率低于阈值的类别归为'other',阈值在fit阶段学习"""
def __init__(self, threshold=0.01):
self.threshold = threshold
def fit(self, X, y=None):
counts = X.iloc[:, 0].value_counts(normalize=True)
self.keep_categories_ = counts[counts >= self.threshold].index.tolist()
return self
def transform(self, X):
X_out = X.copy()
X_out.iloc[:, 0] = X_out.iloc[:, 0].where(
X_out.iloc[:, 0].isin(self.keep_categories_), 'other')
return X_out
# 嵌入完整管道
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import LogisticRegression
preprocess = ColumnTransformer([
('num', StandardScaler(), ['age', 'amount']),
('cat', Pipeline([
('rare', RareCategoryGrouper(threshold=0.02)),
('onehot', OneHotEncoder(handle_unknown='ignore'))
]), ['city'])
])
model = Pipeline([('prep', preprocess), ('clf', LogisticRegression(max_iter=1000))])
model.fit(X_train, y_train)上面这个RareCategoryGrouper是典型例子:低频类别归并必须基于训练集的频率统计,放进Transformer里才能保证测试时沿用训练阶段学到的类别集合。注意几个规范细节:__init__中只做参数赋值不做计算,拟合结果保存在以下划线结尾的属性里,transform中绝不修改原始数据。这些约定AI有时会违反,生成的代码在fit里就做转换或者参数计算写在__init__里,导致交叉验证时无法正确刷新状态,验收时必须重点检查。
让AI写这类转换器时,建议在提示词里直接给出类骨架,明确要求继承BaseEstimator和TransformerMixin,说明输入是DataFrame还是ndarray、输出的形状是什么。约束越具体,AI越不容易在fit_transform的调用链上犯错。
如何向AI精准描述特征需求并验证结果
AI生成的代码不能拿来就用,关键在于会写提示词、会验证。写提示词时推荐遵循三要素:数据结构、转换逻辑、输出约束。例如这样描述:输入是包含user_id(类别)、amount(浮点,有缺失)、ts(时间戳字符串)三列的DataFrame,请生成一个Scikit-learn转换器,对amount做中位数填补加对数变换,从ts提取小时和星期几两个特征并做独热编码,最终输出ndarray,要求兼容Pipeline。这样的描述足够具体,AI基本能一次生成可用代码。
验证环节有三个必做检查。第一,单元测试:用几行手工构造的迷你数据跑一遍,核对输出值是否符合预期。第二,防泄漏检查:在Pipeline上做cross_val_score,对比手写预处理版本的分数,如果分数异常偏高,大概率是统计量在全量数据上拟合了。第三,接口检查:调用get_params和set_params确认参数可以被外部修改,能否正常参与GridSearchCV调参,这是检验转换器是否符合Scikit-learn规范的硬指标。
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import Pipeline
# 用交叉验证检查是否有信息泄漏
scores = cross_val_score(model, X, y, cv=5, scoring='roc_auc')
print('AUC均值: %.4f, 标准差: %.4f' % (scores.mean(), scores.std()))
# 用迷你数据核对转换逻辑
mini = pd.DataFrame({'city': ['北京', '上海', '小城A', '小城B'] * 10})
grouper = RareCategoryGrouper(threshold=0.05).fit(mini)
print(grouper.transform(mini)['city'].unique())
# 期望输出:['北京' '上海' 'other'],小城频率低于阈值被归并实际工作流可以这样安排:先用AI快速生成转换器的初版代码,人工审查fit和transform的职责划分与参数命名,再补上单元测试,最后整合进Pipeline跑交叉验证。整个过程比从零手写快很多,同时质量又有保障。
常见场景的组合套路
数值特征、类别特征、时间特征、文本特征各有惯用组合。数值列常用中位数填补加StandardScaler或QuantileTransformer;类别列低频归并加OneHotEncoder,高基数场景改用目标编码但必须配合交叉验证防止泄漏;时间戳列先拆解出周期性特征(小时、星期、是否周末),再视情况做正弦余弦编码保留周期性;文本列用TfidfVectorizer接TruncatedSVD降维。
这些套路完全可以整理成一份提示词模板库,遇到新项目时替换列名和参数即可让AI批量生成。比如固定句式:请针对以下列生成ColumnTransformer配置,数值列[列名列表]用某某处理,类别列[列名列表]用某某处理,要求返回可直接放入Pipeline的对象。积累几轮之后,特征工程的开发效率会有质的提升,AI负责体力活,人负责设计与把关,这才是合理的分工方式。
特征工程Pandas管道Scikit-learn转换器修改时间:2026-09-12 10:24:43