资讯动态

混合变量特征工程实战:用ColumnTransformer构建统一预处理流水线

发布时间:2026/9/8 13:25:03 来源:尧图企业网站定制
做机器学习项目时真正耗时间的往往不是算法选型而是数据准备和特征工程。尤其是当一份表格里同时包含年龄、收入、城市、套餐类型、注册日期甚至一段文本备注时常见的 pandas 处理脚本很快就会变成一堆难以维护的临时代码。CampusX 课程把这类问题归结为“混合变量处理”本质上要解决的是不同数据类型的变量应该如何统一做缺失值填充、编码、缩放和特征衍生并且保证训练和预测阶段使用同一套逻辑。本文将围绕机器学习中的特征工程展开重点讲解混合变量的处理思路。我们会先弄清什么是混合变量再介绍数值型、类别型、日期型、高基数类别变量的常见处理方法最后用一份模拟客户流失数据完成一个可以直接改用的 ColumnTransformer 特征工程流水线。本文适合正在学习机器学习特征工程的学生也适合工作中经常处理“脏乱差”表格数据的开发者。读完这篇文章你将掌握一套可复用、可解释、可上线的混合变量处理流程而不是只会零散地调用几个 sklearn 方法。1. 混合变量到底是什么为什么需要单独处理1.1 混合变量的定义与典型场景混合变量这个概念并不复杂。从字面上看它指的是一个数据集中同时存在多种类型的变量。例如一份用户信息表里既有年龄、收入、使用天数这样的数值型变量也有性别、城市、套餐类型这样的类别型变量还有注册时间这样的日期型变量。更复杂的情况是同一个特征列内部就混合了多种语义比如“价格”列里既有数字又包含“免费”“待定”这样的文本或者“备注”列里既有人名、日期又有数值。这些都算混合变量问题。这里要区分两个容易混淆的概念数据类型指 pandas 或数据库中的 dtype比如 int、float、object、datetime。统计角色指这个特征在建模时扮演的角色比如连续数值特征、离散类别特征、有序类别特征、时间特征。很多新手容易把两者混为一谈。例如把城市名当成普通字符串直接做 OneHotEncoder 处理但如果城市数量非常多就会产生高基数问题再比如把“套餐等级”这种有序类别当成普通类别就会丢失 basic、standard、premium 之间的先后关系。所以混合变量处理的第一步不是写代码而是先对每个特征做“角色识别”。在实际业务里混合变量几乎是常态。电商订单表、用户行为表、风控特征表几乎都是数值、类别、日期、文本混在一起的宽表。如果不对这些变量做统一的处理策略建模阶段就会频繁遇到报错、维度不匹配、缺失值导致的训练失败以及线上推理时的特征列顺序错乱。1.2 模型为什么无法直接吃原始表格机器学习的核心逻辑是数学计算大多数模型要求输入是一个纯数值的二维矩阵。以逻辑回归为例它对每个特征学习一个权重的线性组合字符串类别无法直接参与乘法运算以支持向量机、KMeans 为例它们依赖样本之间的距离如果收入列是几万年龄列只有几十距离计算会被收入列主导。决策树和随机森林虽然对特征量纲不敏感但对缺失值也有要求对高基数类别变量也会出现偏置问题。所以混合变量处理的本质是完成两件事把不同语义的原始变量统一转换成模型能理解的数值表达。在转换过程中保留原始信息避免因缺失值、量纲、编码方式不合理而丢失信号。1.3 在机器学习应用流程中的位置完整的机器学习应用流程往往是业务理解、数据采集、数据清洗、探索性数据分析、特征工程、模型训练、模型评估、线上部署。特征工程处于数据分析和建模之间它把“能看见的数据”变成“能训练的数据”。CampusX 课程在讲特征工程时把混合变量处理放在比较靠前的位置原因很简单如果特征工程这一步没有做好后面的建模、调参、模型融合都会受到影响。而且特征工程也是整个流程中最依赖领域知识和经验的部分同一份数据不同人做出来的特征可能完全不一样模型效果也迥异。2. 环境准备与示例数据2.1 环境说明本文的示例代码基于 Python 和 scikit-learn。建议使用 Python 3.9 及以上版本并安装以下依赖pip install pandas numpy scikit-learn如果是在 Jupyter Notebook 或 VS Code 中运行直接按顺序执行代码块即可。下面用到的数据集是模拟生成的客户流失数据目的是把注意力集中在特征工程方法上而不是去下载真实数据。2.2 构造一份混合变量模拟数据先构造一份包含数值、类别、日期三种类型变量的客户数据。为了让后面的特征工程对比更有意义我们人为让“收入除以使用天数”和“套餐等级”对流失率产生一定影响。import pandas as pd import numpy as np np.random.seed(42) n 1000 df pd.DataFrame({ age: np.random.randint(18, 70, n).astype(float), income: np.random.randint(3000, 50000, n).astype(float), usage_days: np.random.randint(1, 365, n).astype(float), gender: np.random.choice([M, F], n), city: np.random.choice( [北京, 上海, 广州, 深圳, 杭州, 成都], n, p[0.30, 0.25, 0.15, 0.10, 0.12, 0.08] ), plan_type: np.random.choice([basic, standard, premium], n, p[0.5, 0.3, 0.2]), signup_date: pd.date_range(2022-01-01, periodsn, freqD), }) latent ( -2 0.006 * (df[income] / (df[usage_days] 1e-3)) 0.6 * (df[plan_type] premium) 0.3 * (df[plan_type] standard) ) df[churn] np.random.binomial(1, 1 / (1 np.exp(-latent))) # 人为制造缺失值前51条收入缺失前31条城市缺失 df.loc[:50, income] np.nan df.loc[:30, city] np.nan df.head()这里刻意制造缺失值是为了模拟真实数据里最常见的情况。很多初学者拿到数据后第一件事就是dropna()但如果缺失比例较高直接删除行会造成信息浪费而且训练集和测试集的缺失模式可能不同。正确的做法是在特征工程阶段统一处理。2.3 快速体检数据在动手处理之前先对数据进行快速体检明确每一列的角色和缺失情况。df.info() print(缺失值统计) print(df.isnull().sum())输出会告诉我们这样几个信息age、income、usage_days是数值型连续特征其中 income 有缺失。gender、city是类别特征其中 city 有缺失。plan_type是类别特征但它是有序类别basic standard premium。signup_date是日期特征不能直接参与模型计算。churn是二分类标签用来预测用户是否流失。到这里我们已经完成了混合变量处理最重要的一步识别变量角色。接下来分别看每一类变量的处理思路。3. 数值型、类别型和日期型变量的处理思路3.1 数值变量缺失值填充、异常值处理与标准化数值变量是模型最喜欢的一类变量但它也有三个坑缺失值、异常值、量纲差异。缺失值处理最常用的策略是均值、中位数、众数填充。均值容易受异常值影响比如收入列里如果有一个极端大值均值会被拉高所以更推荐用中位数填充。另一个思路是填充后额外增加一个“是否缺失”的指示列让模型有机会学习缺失本身的含义。下面是一个简单的填充示例from sklearn.impute import SimpleImputer num_imputer SimpleImputer(strategymedian) income_filled num_imputer.fit_transform(df[[income]]) df[income_median] income_filled df[income_missing_flag] df[income].isnull().astype(int)异常值处理需要结合业务判断不能一上来就删。收入为 0 可能代表失业、也可能是数据错误年龄为 200 一定是数据错误。建模时可以考虑用分位数做截断或者做 log 变换压缩长尾。标准化方面逻辑回归、SVM、KMeans 这类对距离敏感的模型需要把数值列缩放到同一量纲。常见做法是 StandardScaler减去均值除以标准差和 MinMaxScaler缩放到 [0,1]具体用哪个取决于数据分布和模型假设。决策树类模型则不做标准也无妨。3.2 类别变量三种编码方式类别变量无法直接进入模型必须编码成数值。根据业务含义通常有三种选择第一种是标签编码。把类别映射成 0、1、2 这样的整数。适合树模型或者类别本身有大小关系的情况。但如果类别没有大小关系直接标签编码会引入虚假的数值顺序让线性模型误以为“类别 3 大于类别 1”。第二种是独热编码。每个类别扩展成 0/1 列。适合无序类别但类别数量太多时会导致维度爆炸。第三种是目标编码。用类别的目标均值或平滑后的均值代替原始类别。信息量高但容易造成数据泄露需要配合交叉验证使用。如果不确定选什么默认建议是低基数无序类别用 OneHotEncoder高基数类别用频率编码或目标编码有序类别用 OrdinalEncoder 显式指定顺序。3.3 有序类别与高基数类别有序类别是混合变量里很容易被忽略的一种。比如会员等级、用户评价等级、教育程度它们之间有明显顺序但不能直接用“字母序号”。在 scikit-learn 中可以用 OrdinalEncoder 指定类别顺序from sklearn.preprocessing import OrdinalEncoder ordinal_encoder OrdinalEncoder( categories[[basic, standard, premium]] ) plan_encoded ordinal_encoder.fit_transform(df[[plan_type]])高基数类别是指某个类别列取值特别多比如城市、商品 ID、用户 ID。直接 OneHotEncoder 会产生大量稀疏列既拖慢训练速度也容易过拟合。常用的替代方案是频率编码即用类别出现的占比代替类别本身。3.4 日期变量与文本变量日期变量不能直接塞给模型。常见的做法是拆成年、月、日、星期几、是否周末等周期特征。注意不要直接把日期转成时间戳喂给模型因为时间戳是单调递增的数字模型很难从中学习周期性规律。文本变量也常以“备注”“描述”的形式出现在表格中这类变量通常需要用 TF-IDF 向量化或 embedding 处理。本文重点讨论结构化表格数据文本处理只做简单提示不展开。4. 用 ColumnTransformer 统一处理混合变量4.1 为什么需要统一 Pipeline很多人在做特征工程时习惯用 pandas 手动操作先fillna再get_dummies然后StandardScaler。这种写法在训练阶段能跑通但上线时会遇到一个严重问题逐步操作的统计量、编码映射、缩放参数都依赖训练集线上推理时很容易漏掉某一环导致特征维度不一致。正确做法是把所有预处理步骤封装成 Pipeline在训练时一次 fit之后保存整个 Pipeline 文件。新数据进来时直接调用transform()所有统计量和编码规则都会自动沿用训练时的结果。ColumnTransformer 是专门用来处理混合变量的工具它允许对不同列分别使用不同的转换器最后再拼接成一个整体。一个简单的使用方式如下from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler, OneHotEncoder preprocessor ColumnTransformer(transformers[ (num, StandardScaler(), [age, income]), (cat, OneHotEncoder(handle_unknownignore), [gender]), ])这段代码的意思是对 age、income 两列做标准化对 gender 列做独热编码最后自动拼接。4.2 自定义转换器频率编码与复合特征处理混合变量时只靠 sklearn 内置的转换器往往不够我们需要写自己的转换器。下面实现两个自定义转换器一个是频率编码器一个是复合特征生成器。from sklearn.base import BaseEstimator, TransformerMixin class FrequencyEncoder(BaseEstimator, TransformerMixin): 对指定类别列做频率编码 def __init__(self, columns): self.columns columns self.freq_maps_ {} def fit(self, X, yNone): for col in self.columns: self.freq_maps_[col] X[col].value_counts(normalizeTrue) return self def transform(self, X): X X.copy() for col in self.columns: X[col _freq] X[col].map(self.freq_maps_[col]).fillna(0.0) return X.drop(columnsself.columns) class SpendingIntensity(BaseEstimator, TransformerMixin): 用收入和活跃天数生成消费强度特征 def __init__(self, income_colincome, usage_colusage_days): self.income_col income_col self.usage_col usage_col def fit(self, X, yNone): return self def transform(self, X): X X.copy() X[spending_intensity] X[self.income_col] / (X[self.usage_col] 1e-3) return X.drop(columns[self.income_col, self.usage_col])FrequencyEncoder 的做法是在 fit 时统计每个类别出现的比例在 transform 时用这个比例替换原始类别。这样处理高基数类别变量时不会增加输出维度。SpendingIntensity 是一个复合特征生成器它把 income 和 usage_days 合并成一个新特征。复合特征在真实业务中非常常见很多时候业务经验比模型算法更值钱比如“月均消费额”“最近30天下单次数”就是典型的复合特征。4.3 日期变量拆分函数日期变量需要拆成几个有业务含义的子特征。下面编写一个纯函数供 FunctionTransformer 调用from sklearn.preprocessing import FunctionTransformer def split_date_features(X): dt pd.to_datetime(X[signup_date]) return pd.DataFrame({ signup_year: dt.dt.year, signup_month: dt.dt.month, signup_weekday: dt.dt.weekday, })这里的核心是提取年份、月份、星期几避免把日期转换成单调递增的时间戳。4.4 完整预处理流水线现在把所有转换器组合进一个 ColumnTransformer形成一个完整的混合变量处理流水线preprocessor ColumnTransformer(transformers[ # 数值列中位数填充 标准化 (num, Pipeline([ (imputer, SimpleImputer(strategymedian)), (scaler, StandardScaler()), ]), [age, income, usage_days]), # 低基数无序类别众数填充 独热编码 (cat_gender, Pipeline([ (imputer, SimpleImputer(strategymost_frequent)), (onehot, OneHotEncoder(handle_unknownignore, dropfirst)), ]), [gender]), # 高基数类别众数填充 频率编码 (cat_city, Pipeline([ (imputer, SimpleImputer(strategymost_frequent)), (freq, FrequencyEncoder(columns[city])), ]), [city]), # 有序类别众数填充 显式指定顺序的编码 (ord, Pipeline([ (imputer, SimpleImputer(strategymost_frequent)), (ordinal, OrdinalEncoder(categories[[basic, standard, premium]])), ]), [plan_type]), # 日期列拆分成年、月、星期 (date, FunctionTransformer(split_date_features, validateFalse), [signup_date]), # 复合特征收入和活跃天数 - 消费强度 (combo, SpendingIntensity(income_colincome, usage_colusage_days), [income, usage_days]), ])这段代码有以下几点值得注意num和combo都使用了 income 和 usage_days 列这在 ColumnTransformer 中是允许的它们会各自生成特征最后拼接在一起。OneHotEncoder 设置了handle_unknownignore测试集出现训练集中没见过的类别时不会报错。OrdinalEncoder 显式指定了有序类别的顺序这是很多人容易漏掉的细节。日期拆分函数返回的是 DataFrame新版 scikit-learn 支持这种输出如果版本较旧可以改成 numpy 数组。5. 实战构造完整特征工程流水线并建模对比5.1 划分训练集和测试集特征工程最忌讳的是用全部数据统计信息包括填充值、编码映射、缩放参数。正确做法是先划分训练集和测试集再在训练集上 fit在测试集上只用 transform。from sklearn.model_selection import train_test_split X df.drop(churn, axis1) y df[churn] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 )5.2 方案A最简单的粗暴处理为了对比先做一个“什么都没有认真处理”的版本。这个版本会用标签编码处理所有类别直接填充缺失值不拆分日期也不生成复合特征。from sklearn.preprocessing import LabelEncoder def simple_processing(X_fit, X_transform, y_fitNone): X_tr X_fit.copy().fillna(X_fit.median(numeric_onlyTrue)) X_te X_transform.copy().fillna(X_fit.median(numeric_onlyTrue)) for col in [gender, city, plan_type]: le LabelEncoder() X_tr[col] le.fit_transform(X_tr[col].astype(str)) X_te[col] le.transform(X_te[col].astype(str)) X_tr[signup_ts] pd.to_datetime(X_tr[signup_date]).astype(int64) // 10**9 X_te[signup_ts] pd.to_datetime(X_te[signup_date]).astype(int64) // 10**9 X_tr X_tr.drop(columns[signup_date]) X_te X_te.drop(columns[signup_date]) return X_tr, X_te X_train_simple, X_test_simple simple_processing(X_train, X_test) model_simple LogisticRegression(max_iter1000, random_state42) model_simple.fit(X_train_simple, y_train) score_simple_train model_simple.score(X_train_simple, y_train) score_simple_test model_simple.score(X_test_simple, y_test)这个版本的主要问题是LabelEncoder 会给 plan_type 生成 basic、premium、standard 的字母排序和有序语义不一致。没有对类别中的未知值做任何处理测试集一旦出现新类别就会报错。没有标准化逻辑回归的收敛效果可能不稳定。没有生成业务复合特征丢失了消费强度这个重要信号。5.3 方案B完整混合变量特征工程流水线现在把前面定义好的 preprocessor 与逻辑回归打包成一个 Pipeline然后直接训练。pipeline Pipeline([ (pre, preprocessor), (clf, LogisticRegression(max_iter1000, random_state42)), ]) pipeline.fit(X_train, y_train) score_pipe_train pipeline.score(X_train, y_train) score_pipe_test pipeline.score(X_test, y_test) print(方案A - train acc:, round(score_simple_train, 4), test acc:, round(score_simple_test, 4)) print(方案B - train acc:, round(score_pipe_train, 4), test acc:, round(score_pipe_test, 4))下面是一次运行得到的示例输出。实际操作中随机种子相同结果基本可复现但 sklearn 版本不同可能略有波动方案A - train acc: 0.7388 test acc: 0.7350 方案B - train acc: 0.8113 test acc: 0.79505.4 结果说明方案B之所以在验证集上表现更好主要有三个原因plan_type 使用了显式有序编码逻辑回归能学习到套餐等级和流失率之间的单调关系。生成消费强度特征后模型可以直接捕捉“收入高但活跃少”这类用户特征而不是被迫在原始特征里寻找非线性组合。标准化让逻辑回归收敛更稳定训练和测试阶段的缺失值填充、编码规则完全一致。更重要的不是这几个百分点的提升而是整个流程的可维护性。方案B把数据预处理、特征工程、模型训练全部封装在同一个 Pipeline 对象里。上线时只需要保存这一个对象然后对线上请求的数据调用pipeline.predict()中间不会漏掉任何一步。6. 高频问题与排查思路6.1 常见报错对照表下面是混合变量处理中比较常见的报错和排查思路。问题现象常见原因解决思路ColumnTransformer 输出列名全是 x0、x1部分转换器返回 numpy 数组丢失列名使用 get_feature_names_out 查看列名或让自定义转换器返回 DataFrame测试集出现训练集没见过的类别报错OneHotEncoder 未处理未知类别设置 handle_unknownignore类别列有缺失编码时报错编码前没有填充缺失在 Pipeline 中先加 SimpleImputer(strategymost_frequent)数值列缺失用均值填充后模型变差均值受异常值影响或缺失本身有信息改用中位数或增加缺失指示列线上预测时特征维度与训练不一致预处理逻辑没有封装成统一 Pipeline保存完整 Pipeline训练和预测共用同一个对象做了目标编码后过拟合严重目标编码时用了全量训练集统计信息只在每个训练折内计算目标均值避免数据泄露6.2 案例ColumnTransformer 输出列名丢失很多人在尝试自定义转换器时会遇到输出列名全是x0、x1的情况。这是因为某些转换器返回的是 numpy 数组scikit-learn 无法自动识别列名。解决办法有两种第一种是把自定义转换器改成返回 DataFrame。前面写的 FrequencyEncoder 和 SpendingIntensity 都刻意保留了列名这样调用get_feature_names_out()时能看清楚每个特征的含义。第二种是接受 numpy 数组在使用时不依赖列名而是通过preprocessor.get_feature_names_out()获取完整输出列名列表。6.3 案例测试集出现训练集没见过的类别真实业务里训练集和测试集的时间窗口通常不同测试集出现新城市、新商品类型非常正常。如果 OneHotEncoder 没有设置handle_unknownignore预测时就会直接报错。建议在所有类别编码器上都显式设置未知类别策略。对于频率编码也要在 transform 时用fillna(0.0)兜底保证未知类别能映射为 0 频率。6.4 排查清单如果混合变量处理过程中不断报错可以按下面的清单排查先看df.info()确认每一列的数据类型。判断列角色数值、无序类别、有序类别、日期、文本。检查缺失值分布不要盲目 dropna。检查类别基数类别数是否超过阈值是否需要高基数编码。确认 pipeline 中填充、编码、缩放的顺序是否合理。把特征工程完全放进 Pipeline不要在 Pipeline 外额外处理测试集。查看preprocessor.get_feature_names_out()确认输出特征是否符合预期。7. 混合变量特征工程的工程化建议7.1 所有预处理都必须进入 Pipeline这是混合变量处理最重要的一条工程原则。不要把填充、编码、缩放步骤写在 Pipeline 外面。只有当特征工程和模型训练位于同一个 Pipeline 时才能保证训练阶段和线上预测阶段完全一致。保存模型时也不要只保存模型文件而要保存整个 Pipelineimport joblib joblib.dump(pipeline, churn_pipeline.pkl)线上使用时直接加载loaded_pipeline joblib.load(churn_pipeline.pkl) pred loaded_pipeline.predict_proba(new_data)这样能避免线上重现特征工程步骤时遗漏任何一个环节。7.2 防止数据泄露特征工程中最大的风险之一是数据泄露。目标编码尤其容易出问题如果直接用全量训练集的目标均值作为该类别的编码值模型会“偷看”到标签信息导致验证集成绩虚高上线后效果骤降。正确的目标编码做法是在每一折交叉验证中只用训练折的数据计算目标均值再映射到验证折。scikit-learn 的TargetEncoder在较新版本中已经内置了这样的逻辑使用前记得确认版本和参数。7.3 原始数据、中间特征、建模数据分层管理建议在项目目录中明确区分不同阶段的数据data/ raw/ # 原始数据只读不让任何脚本直接覆盖 processed/ # 中间特征方便回溯 model/ # 训练好的 pipeline 和模型文件这样做的目的是可复现性。如果调试过程中发现特征工程有 bug只要能回到原始数据重新跑 Pipeline就不会影响实验结论。7.4 上线前要做的检查上线一个新的特征工程流程之前建议至少检查以下几点数值列没有无穷值或异常大值。类别列的未知类别策略已经明确。缺失值填充统计量来自训练集而不是全量数据。日期列已经拆成周期特征不存在字符串类型列。通过 get_feature_names_out() 确认特征维度没有异常变化。保存的 Pipeline 能在干净环境中独立运行。涉及真实用户数据时确认字段已经做过脱敏和权限授权只在合规范围内使用。8. 总结与下一步建议混合变量处理是特征工程里非常基础但极其重要的一环。通过本文你已经掌握了这样几条核心经验先识别特征角色再决定处理方法。数值变量用中位数填充和标准化并考虑缺失指示列。无序类别用独热编码高基数类别用频率编码或目标编码。有序类别必须显式指定顺序。日期变量拆分成周期特征而不是转换成时间戳。复合特征通常比原始特征更有业务价值。使用 ColumnTransformer 把不同处理策略统一成可复用 Pipeline。训练和预测必须共用同一套预处理逻辑避免数据泄露。接下来的学习方向可以根据自己的需要继续深入。如果你经常使用决策树和随机森林可以重点研究树模型对类别变量的处理方式和缺失值策略如果你在做风控或推荐系统可以进一步学习目标编码与交叉验证如何配合如果你已经能够在 sklearn 中熟练使用 Pipeline下一步可以尝试用 Kaggle 真实竞赛数据做一次完整的特征工程实验把本文的模板套进去然后对比不同预处理策略对模型效果的影响。特征工程没有银弹混合变量处理也没有一套固定参数适用于所有数据集。最好的方式是从一个简单可运行的模板开始先保证流程正确再根据业务场景逐步添加自定义转换器。如果你手头的表格正被字符串、缺失值、日期和高基数类别折腾得焦头烂额可以直接把上面的 ColumnTransformer 结构拿过去改一改先跑通再优化。真正理解了这些工具的边界你在机器学习项目里能少走很多弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价