ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

分类数据与顺序数据:编码方案选择与模型适配实战指南

分类数据与顺序数据:编码方案选择与模型适配实战指南 1. 分类数据与顺序数据为什么你的数据预处理总在第一步就翻车做数据这行的朋友十个有八个在建模之前都栽在同一个坑里拿到一份数据看着都是数字或者文字直接往模型里灌结果要么报错要么跑出来的结果完全没法解释。我见过太多人把“性别”这一列用0和1编码之后模型效果反而变差了也见过有人把“满意度低/中/高”直接映射成1/2/3然后一本正经地解释回归系数。问题出在哪就出在没搞清楚手里这列数据到底是分类数据还是顺序数据。这两个概念听起来像是统计学课本里的老古董但实际工作中它们决定了你该用什么编码方式、该选什么模型、该怎么解释结果。分类数据和顺序数据都属于定性数据也就是描述性质、类别而非数量的数据。区别在于分类数据的各个类别之间没有内在顺序比如血型、城市、产品颜色而顺序数据的类别之间有明确的等级或顺序关系比如教育程度、满意度评级、疼痛等级。这个区别看似微小但它直接影响到你后续所有的处理决策。这篇文章适合谁看如果你是从业一两年的数据分析师、算法工程师或者正在转行做数据科学的学习者只要你在实际项目中需要处理表格数据、做特征工程、跑分类或回归模型那这些内容就是绕不开的基本功。我会从底层逻辑讲起把编码方案的选择、模型适配、常见踩坑点全部拆开揉碎配上可以直接复现的代码和参数说明。读完你至少能做到一件事拿到一列数据三秒钟判断它是什么类型然后知道下一步该干什么。2. 先把概念钉死分类数据和顺序数据的本质区别2.1 从测量尺度说起理解数据的“自由度”要真正搞懂分类数据和顺序数据得先回到统计学里测量尺度这个概念。测量尺度分四种定类、定序、定距、定比。分类数据对应的是定类尺度顺序数据对应的是定序尺度。定类尺度是测量尺度里最弱的一种它只能区分“相同”或“不同”不能做任何大小比较。比如“血型”这个变量A型、B型、O型、AB型之间你不能说A型比B型大也不能说O型排在AB型前面它们就是四个平行的类别。定序尺度比定类尺度强一点它不仅能区分不同还能排出顺序。比如“教育程度”这个变量小学、初中、高中、本科、硕士、博士这个顺序是有意义的硕士确实比本科高一级。但定序尺度的问题在于它虽然能排序但相邻类别之间的差距不一定相等。小学到初中的差距和硕士到博士的差距显然不是一回事。这一点非常关键后面讲编码的时候会反复用到。我习惯用一个简单的判断方法拿到一列数据先问自己两个问题。第一这些类别之间有没有大小、高低、优劣之分如果没有那就是分类数据。第二如果有顺序那相邻两个类别之间的差距是不是等距的如果不是等距的那就是顺序数据如果是等距的那它其实已经接近定距尺度了比如“温度”这种虽然看起来像顺序但差距是等距的处理方式又不一样。2.2 分类数据的典型特征与识别方法分类数据有几个非常明显的特征。第一类别之间互斥且穷尽。互斥意味着一个样本只能属于一个类别不能同时是A又是B穷尽意味着所有可能的类别都被覆盖了不会出现一个样本不属于任何类别的情况。第二类别之间没有顺序。第三类别数量通常是有限且可枚举的。常见的分类数据包括性别、婚姻状况、职业类型、产品类别、城市、支付方式、设备类型等等。识别分类数据有一个很实用的技巧你试着给这些类别排个序如果排出来的顺序让你觉得别扭或者有争议那它就是分类数据。比如“颜色”这个变量红橙黄绿蓝靛紫有人按光谱排有人按喜好排没有统一标准所以它是分类数据。再比如“职业”你说工程师和教师谁排前面没法排所以也是分类数据。在实际项目中分类数据还有一个变种需要特别注意高基数分类数据。也就是类别数量特别多的分类变量比如用户ID、商品SKU、邮政编码。这类数据如果直接用独热编码维度会爆炸必须用其他方法处理比如目标编码、频率编码或者嵌入表示。这个后面会专门讲。2.3 顺序数据的典型特征与识别方法顺序数据的核心特征是类别之间有明确的、被广泛认可的顺序。注意“被广泛认可”这个限定词因为顺序这个东西有时候是主观的但在业务场景中通常有共识。比如“满意度”这个变量非常不满意、不满意、一般、满意、非常满意这个顺序在绝大多数业务场景下都是被认可的。再比如“会员等级”普通会员、银卡、金卡、钻石卡顺序也很明确。顺序数据还有一个重要特征类别数量通常不会太多一般在3到7个之间。如果超过7个要么是量表设计有问题要么它可能更接近定距数据了。比如“评分”这个变量1到10分虽然看起来是顺序的但很多人会把它当定距数据处理因为10个等级之间的差距在心理感知上可能近似等距。识别顺序数据的方法也很简单你给这些类别排个序如果排出来的顺序在业务语境下是合理的、没有争议的那它就是顺序数据。比如“年龄段”18-25、26-35、36-45、46-55、55以上这个顺序是明确的而且相邻年龄段之间的差距是等距的都是10年所以它其实已经接近定距数据了。但“教育程度”就不一样小学到初中是9年初中到高中是3年高中到本科是4年本科到硕士是2-3年差距不等所以它是典型的顺序数据。2.4 一张表看清两者的核心差异对比维度分类数据顺序数据测量尺度定类尺度定序尺度类别间关系仅能区分相同/不同可区分相同/不同且可排序类别间差距无差距概念有顺序但差距不一定等距典型例子性别、血型、城市、颜色教育程度、满意度、会员等级可用运算等于/不等于等于/不等于、大于/小于常用编码独热编码、目标编码序数编码、独热编码适用模型树模型天然支持线性模型需编码树模型天然支持线性模型需谨慎编码统计描述频数、众数频数、众数、中位数、百分位数这张表建议你存下来每次拿到新数据的时候对照着看一眼。我自己的习惯是在Jupyter Notebook里开一个Markdown单元格把这张表贴进去边做边对照能省掉很多返工的时间。3. 编码方案怎么选不同场景下的最优解3.1 独热编码分类数据的默认选择但别无脑用独热编码是处理分类数据最常用的方法原理很简单一个有K个类别的分类变量编码成K个二进制列每个样本在对应类别的那一列上取1其余取0。比如“颜色”有红、绿、蓝三个类别独热编码后就变成三列是否红色、是否绿色、是否蓝色。独热编码最大的优点是不引入虚假的顺序关系。如果你把红绿蓝编码成1、2、3模型会误以为蓝色比红色大这显然不对。独热编码避免了这个问题每个类别都是独立的维度。但独热编码有两个明显的坑。第一个坑是维度爆炸。如果一个分类变量有1000个类别独热编码后就是1000列如果数据里有好几个这样的变量特征维度直接上万训练速度慢不说还容易过拟合。第二个坑是多重共线性。K个类别编码成K列这K列之和恒等于1存在完全共线性。对于线性模型来说这会导致系数估计不稳定。解决办法是去掉一列也就是K个类别编码成K-1列这叫虚拟编码。树模型对共线性不敏感可以保留全部K列。import pandas as pd # 示例数据 df pd.DataFrame({ color: [red, green, blue, red, green] }) # 独热编码保留全部类别 df_onehot pd.get_dummies(df, columns[color], prefixcolor) print(df_onehot) # 虚拟编码去掉第一列 df_dummy pd.get_dummies(df, columns[color], prefixcolor, drop_firstTrue) print(df_dummy)注意pd.get_dummies默认返回布尔值如果后续模型需要数值型记得用.astype(int)转换。另外训练集和测试集必须用同一套编码规则否则会出现列对不齐的问题。生产环境中建议用sklearn的OneHotEncoder它可以保存编码规则并应用到新数据上。3.2 序数编码顺序数据的正确打开方式顺序数据如果也用独热编码会丢失顺序信息。比如“满意度”有五个等级独热编码后模型只知道有五个不同的类别不知道它们之间有高低之分。这时候应该用序数编码也就是按照顺序映射成整数。比如非常不满意1不满意2一般3满意4非常满意5。序数编码保留了顺序信息模型可以利用“大于”“小于”这样的关系。但这里有一个非常关键的注意事项序数编码假设相邻类别之间的差距是等距的。如果你用线性回归或者逻辑回归模型会把这个编码当成连续的数值来处理计算系数的时候会认为从1到2的差距和从4到5的差距是一样的。如果实际差距不等距系数解释就会有问题。那怎么办两个方案。方案一如果差距确实不等距但又想保留顺序信息可以用独热编码加单调性约束不过这需要模型支持实现起来比较复杂。方案二更实用的做法是对于树模型序数编码完全没问题因为树模型只关心分割点不关心具体数值的差距。对于线性模型如果差距不等距可以考虑用样条函数或者分段编码来处理。from sklearn.preprocessing import OrdinalEncoder # 示例数据 df pd.DataFrame({ satisfaction: [very_low, low, medium, high, very_high] }) # 指定顺序 categories [[very_low, low, medium, high, very_high]] encoder OrdinalEncoder(categoriescategories) df[satisfaction_encoded] encoder.fit_transform(df[[satisfaction]]) print(df)提示OrdinalEncoder的categories参数必须显式指定否则它会按字母顺序编码那就乱套了。我见过有人忘了指定顺序结果“high”被编码成1“low”被编码成3模型跑出来完全反了。3.3 目标编码高基数分类数据的救星当分类变量的类别数量很多时独热编码就不适用了。比如“城市”这个变量全国有几百个城市独热编码后几百列模型根本吃不消。这时候可以用目标编码也叫均值编码。原理是用目标变量的均值来替换类别值。比如预测房价“北京”这个类别对应的目标编码就是北京所有样本的房价均值。目标编码的优点是大幅降低维度一个分类变量编码后只有一列。但它有一个致命问题数据泄露。如果直接用全量数据计算均值训练集的信息就泄露到了编码里模型在训练集上表现很好在测试集上直接崩掉。解决办法是用交叉验证的方式计算编码也就是把数据分成K折每一折的编码用其他K-1折的数据计算。from sklearn.model_selection import KFold import numpy as np def target_encode(train_df, test_df, col, target, n_splits5, smoothing10): 目标编码带平滑和交叉验证 smoothing: 平滑参数防止类别样本太少时编码不稳定 global_mean train_df[target].mean() # 计算每个类别的均值和样本数 agg train_df.groupby(col)[target].agg([mean, count]) # 平滑编码 (类别均值 * 样本数 全局均值 * 平滑参数) / (样本数 平滑参数) agg[encoded] (agg[mean] * agg[count] global_mean * smoothing) / (agg[count] smoothing) # 应用到训练集和测试集 train_encoded train_df[col].map(agg[encoded]).fillna(global_mean) test_encoded test_df[col].map(agg[encoded]).fillna(global_mean) return train_encoded, test_encoded注意目标编码只适用于有监督任务而且必须严格在交叉验证框架内进行。我个人的经验是平滑参数smoothing取10到100之间比较稳妥具体看类别样本数量。如果某个类别的样本数只有个位数平滑参数要设大一点否则编码值波动太大。3.4 频率编码与嵌入表示备选方案频率编码是用类别出现的频率来替换类别值。比如“北京”出现了1000次总样本10000条那编码值就是0.1。频率编码不依赖目标变量所以没有数据泄露的问题但它丢失了类别本身的语义信息。适合用在不关心类别具体含义、只关心其分布的场景。嵌入表示是深度学习中处理高基数分类数据的主流方法。原理是给每个类别学习一个低维稠密向量比如1000个类别映射到10维空间。嵌入表示能捕捉类别之间的相似性但需要足够的训练数据和计算资源。对于传统机器学习任务目标编码和频率编码通常就够了。4. 模型适配不同模型对数据类型的敏感度差异4.1 树模型对分类和顺序数据天然友好决策树、随机森林、梯度提升树这类树模型对分类数据和顺序数据的处理是最省心的。树模型的分割逻辑是找到一个特征和一个阈值把数据分成两部分。对于分类数据树模型可以直接处理不需要独热编码。比如“颜色”这个变量树模型会尝试“颜色红色”和“颜色≠红色”这样的分割。对于顺序数据树模型也能直接处理而且因为序数编码保留了顺序树模型可以找到更合理的分割点。但树模型也不是完全没有坑。第一个坑是高基数分类数据。如果一个分类变量有几百个类别树模型在每次分割时都要尝试所有可能的类别组合计算量巨大而且容易过拟合。这时候还是得用目标编码降维。第二个坑是类别不平衡。如果某个类别的样本极少树模型可能直接忽略这个类别导致预测偏差。from sklearn.ensemble import RandomForestClassifier from sklearn.preprocessing import OrdinalEncoder # 树模型可以直接处理序数编码后的顺序数据 # 分类数据也可以用序数编码树模型不会引入虚假顺序 encoder OrdinalEncoder() X_train_encoded encoder.fit_transform(X_train[[color, satisfaction]]) rf RandomForestClassifier(n_estimators100, random_state42) rf.fit(X_train_encoded, y_train)实操心得用树模型的时候我习惯把所有分类变量都用序数编码转成整数然后直接喂给模型。这样代码简单效果也不差。但要注意如果分类变量是高基数的还是得先做目标编码或者频率编码。4.2 线性模型编码方式直接决定结果好坏线性回归、逻辑回归、支持向量机这类线性模型对编码方式非常敏感。分类数据必须用独热编码不能用序数编码否则模型会引入虚假的顺序关系。顺序数据可以用序数编码但要注意等距假设的问题。如果相邻类别差距不等距序数编码会导致系数解释困难。线性模型还有一个问题多重共线性。独热编码后如果保留全部K列K列之和恒等于1存在完全共线性导致系数估计不稳定。解决办法是去掉一列或者用正则化L1/L2来缓解。from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline # 分类变量用独热编码顺序变量用序数编码 preprocessor ColumnTransformer( transformers[ (cat, OneHotEncoder(dropfirst), [color, city]), (ord, OrdinalEncoder(categories[[low, medium, high]]), [satisfaction]) ]) pipeline Pipeline([ (preprocessor, preprocessor), (classifier, LogisticRegression(max_iter1000)) ]) pipeline.fit(X_train, y_train)注意OneHotEncoder的dropfirst参数就是去掉第一列避免共线性。如果用的是pd.get_dummies记得加drop_firstTrue。另外逻辑回归的max_iter默认是100数据量大或者特征多的时候容易不收敛建议调到1000以上。4.3 神经网络嵌入层是标配神经网络处理分类数据标准做法是用嵌入层。嵌入层把每个类别映射成一个低维稠密向量这些向量在训练过程中学习得到。嵌入层的好处是能捕捉类别之间的相似性而且维度可控。比如1000个类别映射到10维参数量只有10000个比独热编码的1000维少得多。顺序数据在神经网络里可以用序数编码也可以先独热编码再嵌入。如果顺序信息很重要可以在嵌入层之后加一个单调性约束不过这需要自定义层实现起来比较复杂。实际项目中大多数情况下直接用序数编码就够了。import tensorflow as tf from tensorflow.keras import layers, Model def build_model(num_categories, embedding_dim10): cat_input layers.Input(shape(1,), namecategory) embedding layers.Embedding(num_categories, embedding_dim)(cat_input) embedding layers.Flatten()(embedding) # 其他特征 other_input layers.Input(shape(5,), nameother_features) concat layers.Concatenate()([embedding, other_input]) x layers.Dense(64, activationrelu)(concat) x layers.Dense(32, activationrelu)(x) output layers.Dense(1, activationsigmoid)(x) model Model(inputs[cat_input, other_input], outputsoutput) model.compile(optimizeradam, lossbinary_crossentropy, metrics[auc]) return model提示嵌入维度一般取min(50, (num_categories 1) // 2)这是FastAI推荐的经验公式。类别越多嵌入维度可以适当增大但一般不超过50。5. 实操全流程从数据探查到模型上线5.1 第一步数据探查与类型判定拿到一份数据第一件事不是急着建模而是先搞清楚每一列是什么类型。我习惯用pandas的info()和describe()先看个大概然后对每一列做针对性检查。import pandas as pd df pd.read_csv(data.csv) # 基本信息 print(df.info()) print(df.describe()) # 对每一列判断类型 for col in df.columns: nunique df[col].nunique() dtype df[col].dtype print(f{col}: dtype{dtype}, nunique{nunique}) # 如果类别数少于20打印所有类别 if nunique 20: print(f 类别: {df[col].unique()})判断逻辑是这样的如果一列是数值型但类别数很少比如小于20它可能是分类数据或者顺序数据需要进一步确认。如果一列是字符串型那基本就是分类数据或者顺序数据。然后根据业务含义判断有没有顺序。实操心得我见过很多人拿到数据直接pd.get_dummies一把梭结果把“年龄”这种连续变量也独热编码了维度爆炸不说还丢失了年龄的连续信息。记住独热编码只适用于分类数据和顺序数据连续数据不要用。5.2 第二步缺失值处理与异常类别合并分类数据和顺序数据经常有缺失值处理方式跟连续变量不一样。连续变量可以用均值、中位数填充分类变量通常用众数填充或者单独作为一个类别“未知”。如果缺失比例很高比如超过30%可以考虑直接删掉这一列。异常类别合并也是常见操作。比如“城市”这个变量有几百个类别其中很多类别只有一两个样本。这些稀有类别可以合并成“其他”类别减少维度。合并的阈值一般是样本数少于总样本的1%或者5%。# 缺失值用众数填充 df[color].fillna(df[color].mode()[0], inplaceTrue) # 稀有类别合并 threshold len(df) * 0.01 counts df[city].value_counts() rare_categories counts[counts threshold].index df[city] df[city].replace(rare_categories, other)注意稀有类别合并的阈值不要设得太高否则会把有意义的类别也合并掉。我一般先用1%试一下看看合并后还剩多少类别如果还是太多再逐步提高阈值。5.3 第三步编码方案落地与特征工程编码方案的选择取决于数据类型和模型类型。我整理了一个决策流程先判断数据类型分类数据用独热编码低基数或目标编码高基数顺序数据用序数编码树模型或独热编码线性模型。然后看模型类型树模型对编码不敏感线性模型和神经网络对编码敏感。from sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder, StandardScaler from sklearn.pipeline import Pipeline from sklearn.impute import SimpleImputer # 定义列类型 cat_cols [color, city, payment_method] ord_cols [satisfaction, education_level] num_cols [age, income] # 分类数据众数填充 独热编码 cat_transformer Pipeline([ (imputer, SimpleImputer(strategymost_frequent)), (onehot, OneHotEncoder(handle_unknownignore, dropfirst)) ]) # 顺序数据众数填充 序数编码 ord_transformer Pipeline([ (imputer, SimpleImputer(strategymost_frequent)), (ordinal, OrdinalEncoder(categories[ [very_low, low, medium, high, very_high], [primary, secondary, bachelor, master, phd] ])) ]) # 连续数据均值填充 标准化 num_transformer Pipeline([ (imputer, SimpleImputer(strategymean)), (scaler, StandardScaler()) ]) # 组合 preprocessor ColumnTransformer([ (cat, cat_transformer, cat_cols), (ord, ord_transformer, ord_cols), (num, num_transformer, num_cols) ])提示OneHotEncoder的handle_unknownignore参数很重要它保证测试集出现训练集没见过的类别时不会报错而是全部编码为0。生产环境中这个参数必加。5.4 第四步模型训练与编码效果验证编码方案落地后需要验证编码是否有效。我通常用交叉验证来评估模型效果同时对比不同编码方案的效果差异。比如分类数据用独热编码 vs 目标编码顺序数据用序数编码 vs 独热编码看看哪种方案在验证集上表现更好。from sklearn.model_selection import cross_val_score from sklearn.ensemble import GradientBoostingClassifier # 完整流程 pipeline Pipeline([ (preprocessor, preprocessor), (classifier, GradientBoostingClassifier(n_estimators100, random_state42)) ]) # 交叉验证 scores cross_val_score(pipeline, X_train, y_train, cv5, scoringroc_auc) print(fAUC: {scores.mean():.4f} (/- {scores.std():.4f}))实操心得我一般会跑三组对比实验。第一组分类数据用独热编码顺序数据用序数编码第二组全部用独热编码第三组分类数据用目标编码顺序数据用序数编码。然后看哪组AUC最高。大多数情况下第一组和第三组效果差不多但第三组训练速度快很多。6. 常见问题与排查技巧实录6.1 编码后模型效果反而变差怎么回事这是最常见的问题。原因通常有三个。第一顺序数据用了独热编码丢失了顺序信息。比如“满意度”独热编码后模型不知道“非常满意”比“满意”更好。解决办法是改用序数编码。第二分类数据用了序数编码引入了虚假顺序。比如“颜色”编码成1、2、3模型误以为3比1大。解决办法是改用独热编码。第三高基数分类数据用了独热编码维度爆炸导致过拟合。解决办法是改用目标编码或频率编码。排查方法很简单把编码后的特征列打印出来看看有没有明显不合理的编码。比如“颜色”编码后出现了大小关系那肯定有问题。6.2 训练集和测试集编码不一致怎么破这个问题通常出现在手动编码的场景。比如训练集用pd.get_dummies编码测试集也用pd.get_dummies编码但测试集少了一个类别导致列数对不上。解决办法是用sklearn的OneHotEncoder它可以保存编码规则然后应用到测试集上。或者手动对齐列测试集缺少的列补0。# 错误做法分别编码 train_encoded pd.get_dummies(train_df) test_encoded pd.get_dummies(test_df) # 列数可能不一致 # 正确做法用OneHotEncoder from sklearn.preprocessing import OneHotEncoder encoder OneHotEncoder(handle_unknownignore) train_encoded encoder.fit_transform(train_df[cat_cols]) test_encoded encoder.transform(test_df[cat_cols]) # 列数一定一致注意handle_unknownignore保证测试集出现新类别时不会报错而是编码为全0。如果测试集出现新类别模型会把它当成“未知”处理这是合理的。6.3 目标编码的数据泄露怎么彻底避免目标编码的数据泄露是新手最容易踩的坑。避免方法只有一个严格在交叉验证框架内计算编码。具体来说把训练集分成K折每一折的编码用其他K-1折的数据计算。测试集的编码用全量训练集计算。from sklearn.model_selection import KFold def target_encode_cv(train_df, test_df, col, target, n_splits5, smoothing10): global_mean train_df[target].mean() train_encoded pd.Series(indextrain_df.index, dtypefloat) kf KFold(n_splitsn_splits, shuffleTrue, random_state42) for train_idx, val_idx in kf.split(train_df): # 用训练折计算编码 agg train_df.iloc[train_idx].groupby(col)[target].agg([mean, count]) agg[encoded] (agg[mean] * agg[count] global_mean * smoothing) / (agg[count] smoothing) # 应用到验证折 train_encoded.iloc[val_idx] train_df.iloc[val_idx][col].map(agg[encoded]).fillna(global_mean) # 测试集用全量训练集计算 agg_full train_df.groupby(col)[target].agg([mean, count]) agg_full[encoded] (agg_full[mean] * agg_full[count] global_mean * smoothing) / (agg_full[count] smoothing) test_encoded test_df[col].map(agg_full[encoded]).fillna(global_mean) return train_encoded, test_encoded实操心得目标编码的平滑参数smoothing非常关键。如果某个类别的样本数很少编码值会非常不稳定。平滑参数的作用是把类别均值往全局均值拉样本数越少拉得越狠。我一般设smoothing10起步如果类别样本数普遍很少调到50甚至100。6.4 常见问题速查表问题现象可能原因排查方法解决方案模型效果差AUC低于0.6顺序数据用了独热编码检查顺序变量编码方式改用序数编码模型效果差AUC低于0.6分类数据用了序数编码检查分类变量编码方式改用独热编码训练集AUC高测试集AUC低高基数分类数据独热编码检查分类变量类别数改用目标编码或频率编码训练集AUC高测试集AUC低目标编码数据泄露检查目标编码计算方式改用交叉验证计算编码模型不收敛独热编码多重共线性检查独热编码列数去掉一列或用正则化测试集报错“未知类别”编码器未处理未知类别检查编码器参数设置handle_unknownignore特征维度爆炸高基数分类数据独热编码检查特征维度改用目标编码或嵌入表示7. 几个我踩过的坑和私藏技巧7.1 顺序数据的“伪等距”陷阱顺序数据最容易被忽略的问题是等距假设。序数编码把类别映射成1、2、3、4、5线性模型会认为这些数字之间的差距是等距的。但实际业务中很多顺序数据的差距并不等距。比如“满意度”从“非常不满意”到“不满意”的差距和从“满意”到“非常满意”的差距在心理感知上可能完全不同。我遇到过一个案例用户满意度有五个等级用序数编码后跑逻辑回归系数是正的看起来合理。但仔细分析发现模型把“非常满意”和“满意”的差距当成了和“不满意”到“一般”一样的差距导致对高满意度用户的预测偏差很大。后来改用独热编码加单调性约束效果好了很多。实操心得如果顺序数据的差距明显不等距而且用的是线性模型建议用独热编码然后手动加单调性约束。如果嫌麻烦直接用树模型树模型对等距假设不敏感。7.2 分类数据的“稀有类别”处理分类数据经常有稀有类别也就是样本数极少的类别。这些稀有类别如果直接独热编码会导致过拟合。我一般会先统计每个类别的样本数然后把样本数少于总样本1%的类别合并成“其他”。但合并的时候要注意有些稀有类别可能是有业务意义的。比如“支付方式”里有一种“数字货币”支付虽然样本少但可能代表一种趋势。这种情况下我倾向于保留这个类别但用目标编码而不是独热编码减少维度。7.3 编码方案不是一劳永逸的最后一个坑是编码方案不是选好了就一劳永逸的。随着数据更新类别分布会变化编码方案可能需要调整。比如“城市”这个变量一开始只有100个城市后来业务扩展到全国变成300个城市独热编码就不适用了得改成目标编码。我的做法是在模型上线后定期监控编码后的特征分布如果发现某个类别的样本数突然暴增或暴减就重新评估编码方案。另外编码器要保存下来新数据来的时候用同一个编码器转换不能重新训练编码器。import joblib # 保存编码器 joblib.dump(encoder, encoder.pkl) # 加载编码器 encoder joblib.load(encoder.pkl) new_data_encoded encoder.transform(new_data[cat_cols])提示joblib比pickle更适合保存sklearn对象尤其是包含大量numpy数组的对象。保存的时候记得把编码器和模型一起保存方便后续维护。7.4 一个容易被忽略的细节编码后的特征重要性编码后的特征重要性分析也很重要。独热编码后每个类别变成一个独立特征特征重要性可以告诉你哪些类别对预测最重要。但如果用目标编码所有类别变成一个特征就看不到单个类别的重要性了。这时候可以用SHAP值来分析SHAP值可以给出每个样本每个特征的贡献度即使特征经过了目标编码也能追溯到原始类别。import shap # 训练模型 model.fit(X_train_encoded, y_train) # 计算SHAP值 explainer shap.TreeExplainer(model) shap_values explainer.shap_values(X_test_encoded) # 可视化 shap.summary_plot(shap_values, X_test_encoded)实操心得SHAP值计算量比较大数据量大或者模型复杂的时候会很慢。我一般先采样一部分数据计算SHAP值看看整体趋势不用全量计算。8. 写在最后一些个人体会做了这么多年数据我越来越觉得数据预处理才是决定模型效果的天花板。模型选得再好超参数调得再精细如果数据编码这一步做错了后面全是白费功夫。分类数据和顺序数据的区分看起来是统计学的基础知识但实际项目中我见过太多人栽在这上面。我自己的习惯是拿到任何一份数据先花20%的时间做数据探查和类型判定把每一列都搞清楚是什么类型、该怎么编码。这个时间花得值后面能省掉大量返工的时间。另外编码方案一定要做对比实验不要凭感觉选。我一般会跑三到五组对比实验用交叉验证的AUC或者F1来选最优方案。最后分享一个小技巧如果你不确定一列数据是分类还是顺序可以试着用两种编码方式各跑一遍模型看哪种效果更好。如果序数编码效果明显更好那它很可能是顺序数据如果独热编码效果更好那它很可能是分类数据。这个方法虽然有点“暴力”但在实际项目中非常管用。
返回列表