如果你用过Pandas做数据分析,pd.get_dummies()几乎不可能绕开。网上很多教程一句话带过:“把类别变量变成0/1”。但我见过太多人真的只用这一句理解它,结果跑完一看DataFrame,发现多出来几十列,而且列名完全对不上预期,更不知道哪些是原来的列、哪些是新增列。这篇文章就围绕一个很具体的问题展开:pd.get_dummies()到底会生成哪些独热的新列,背后的列名规则是什么,有哪些参数会影响新列的数量和名字,以及在真实项目里怎么保证训练集和测试集不会因为独热编码而“列数对不上”。适合正在做特征工程、准备建模,或者被一堆自动生成的列名搞到头大的同学;哪怕你刚接触Pandas,直接照着代码跑一遍也能明白。
1. get_dummies到底在做什么:从一列到N列的拆分逻辑
1.1 它不是“把一列变成两列”,而是“每个类别单独占一列”
很多模型不认识“红”“绿”“蓝”这样的文本,只能吃数值。独热编码的核心思想是:把一个类别列里的每一个不同取值,拆成一个独立的二元列,样本属于哪个类别,对应列就置1,其他列置0。所以真正的问题不是“一列变几列”,而是“这一列里到底有多少个不同类别”。有多少个类别,就会生成多少个新列。
这里有个很容易忽略的点:pd.get_dummies()默认返回的是一个新的DataFrame,而不是在原表上原地修改。它会把被处理的文本列“消化”掉,转成一组新列,同时保留那些没有被处理的数值列。如果你直接把它赋值给原变量,原来的文本列就没了,只剩下拆分后的新列。
import pandas as pd df = pd.DataFrame({ "颜色": ["红", "绿", "蓝", "红"], "销量": [100, 120, 90, 110] }) df_dummies = pd.get_dummies(df) print(df_dummies.columns.tolist())输出大致是:
['销量', '颜色_红', '颜色_绿', '颜色_蓝']去掉销量这个数值列不看,颜色这一列变成了三个新列:颜色_红、颜色_绿、颜色_蓝。列名的基本规则在这里已经很明显了:默认前缀是原列名,分隔符是下划线,后缀是类别值本身。
1.2 多个分类列时,新列名按“原列名 + 类别值”各自独立生成
如果数据里同时有好几个类别列,比如城市、等级、渠道,get_dummies()会分别对每一列做独热,不会把不同列的值混在一起。每一列的新列名都遵循同样的拼装规则。
df2 = pd.DataFrame({ "城市": ["北京", "上海", "广州"], "等级": ["A", "B", "C"], "金额": [1, 2, 3] }) df2_dummies = pd.get_dummies(df2) print(df2_dummies.columns.tolist())结果大致是:
['金额', '城市_北京', '城市_上海', '城市_广州', '等级_A', '等级_B', '等级_C']所以,如果你看到一张表里多了城市_北京、等级_A这样的列,不要去类别值里找规律,规律全在列名拼接规则里:被处理列的名字 + 下划线 + 该列里出现的类别值。只要记住这条公式,绝大多数情况下你都能反推出一个新列到底是从哪一列来的、代表的是哪个类别。
1.3 新列的顺序和排序规则,不同数据版本会有差异
我实际用过不同版本的Pandas,get_dummies()生成新列的顺序并不可靠。大多数情况下,它会按类别在数据里第一次出现的先后顺序来排。但如果原列本身是category类型,它就会按categories属性里定义的顺序来生成。同一种数据,换成category类型后,输出列的顺序很可能是另一副面孔。
这个细节在小白阶段可能无所谓,但在做特征对齐的时候容易坑人。比如你要把一个独热编码后的结果喂给某个模型,而模型内部要求特征列顺序固定,那你就不能依赖“顺序恰好一致”。最稳的做法是生成之后立刻打印columns.tolist()检查,或者给所有新列做一次排序,让它可预测。
注意:绝大多数情况下,独热编码生成的列名里的“类别值”是原始字符串。如果原字符串带空格、带符号、是中文,这些内容都会原封不动出现在列名里。列名里出现空格和括号,后续访问列、存文件、接SQL都会很麻烦。
2. 新列是怎么被“改名”的:六个关键参数影响列名和列数量
2.1 prefix:默认前缀就是原列名,但你可以彻底改掉它
如果你什么都不传,prefix=None,那么前缀就是原列名。这是最常见的情况。但某些场景下,你不想让新列名跟原列名长得一样,比如想把多个不同数据源的表统一命名风格,或者列名太长想缩短,就可以用prefix参数。
prefix可以传字符串、字符串列表,也可以传字典。传字符串时,所有被处理的列都会共用这个前缀;传列表时,列表长度必须跟columns参数的长度对齐;传字典时,可以给每一列单独指定一个前缀,最直观。
pd.get_dummies(df, prefix={"颜色": "color"})这样生成的新列名就变成color_红、color_绿、color_蓝。注意,前缀变了之后,你依然能从后缀看出原来的类别值,但列名和原列名的关系就没那么直白了。建议在代码里留好注释,免得一个月后自己都忘了color_xxx是从哪来的。
2.2 prefix_sep:分隔符看着不起眼,但影响后续解析
prefix_sep默认是下划线_。你可以改成点.、横杠-甚至空字符串。改成空字符串后,新列名会变成颜色红这样的形式。手写几个字符串可能没什么感觉,但放到几百列的宽表里,这种列名会让字段含义变得非常模糊。
我个人的建议是:不要用横杠,因为很多建模库、SQL引擎、特征存储系统对字段名里的减号很敏感,解析时会把它当成减法运算。点号虽然看着干净,但在有些工具里也有特殊含义。最稳妥的就是下划线。如果嫌列名太长,优先考虑缩短前缀,而不是换分隔符。
pd.get_dummies(df, prefix="c", prefix_sep="_")生成的是c_红、c_绿、c_蓝,干净、好读、兼容性好。
2.3 columns:控制处理哪些列,这是第一道闸门
如果不指定columns,get_dummies()会把DataFrame里所有object类型和category类型的列都处理掉。这个问题在真实数据里非常致命。一个表里可能同时存在备注、用户ID、渠道名称、地址等一堆字符串列,你本来只想处理渠道名称,结果备注也被拆成了几百列,表直接宽到没法看。
所以,只要DataFrame不是只有一两个类别列,我都建议显式传columns。
df_dummies = pd.get_dummies(df, columns=["颜色", "等级"])传了columns之后,只有这两列被拆分成新列,其他列保持原样。这一步是控制“新列集合”最有效的手段。尤其是当你的数据里有高基数字符串列时,漏掉这个参数几乎等于给自己埋雷。
2.4 dummy_na:缺失值到底怎么编码,是隐性信息
默认情况下,dummy_na=False。如果某个样本在类别列上是缺失值,它不会单独生成一个新列,而是所有类别列都填0。这在语义上等于“这个样本不属于任何类别”。
但缺失本身往往是有意义的,比如“颜色未知”和“颜色是红”是两种状态。这时候可以用dummy_na=True,Pandas会额外生成一列,名字一般是原列名_nan。注意,这个nan的外形会因为缺失值类型不同而略有差异:如果缺失值是np.nan,生成的列名后缀通常是小写nan;如果是字符串"nan",那它本来就是一个正常类别,不需要dummy_na单独处理。
pd.get_dummies(df, columns=["颜色"], dummy_na=True)这样新列集合从三个变成四个:红、绿、蓝、nan。在建模时,这四列能完整表达原始信息,不会把缺失样本和普通类别样本混淆。
2.5 drop_first:从K列变成K-1列,减少的到底是哪一列
默认drop_first=False,一个类别列如果有K个不同值,就生成K列。但在线性回归、逻辑回归这类模型里,K列存在完全共线性问题:知道了前K-1列,第K列的值一定能推出来。模型会因此不稳定,甚至直接报奇异矩阵错误。
drop_first=True会把第一个出现的类别作为基准,只生成K-1列。比如颜色列有红、绿、蓝,那么会生成颜色_绿、颜色_蓝两列;样本颜色为红时,这两列都是0,相当于默认基准。
pd.get_dummies(df["颜色"], drop_first=True)这个参数直接影响新列数量。树模型不太在意共线性,所以要不要用drop_first完全看你后续接什么模型。如果你不确定,稳妥做法是默认False,让信息完整保留。
2.6 dtype:生成的新列到底是什么类型,直接影响内存
get_dummies()生成的新列默认类型在不同Pandas版本里不太一样,早期版本常见uint8,后来版本也可能默认返回bool。与其依赖默认值,不如显式指定dtype="uint8",让结果可控。
这不仅仅是类型洁癖问题。百万行数据、几十个类别列的场景下,uint8每个元素只占1字节,而int64每个元素占8字节,内存差距接近8倍。如果你用的是老版本,或者团队代码里混用了不同Pandas版本,显式传dtype可以避免一上线就内存溢出的尴尬。
pd.get_dummies(df, columns=["颜色"], dtype="uint8")另外,老版本里的sparse参数现在基本被废弃了,不建议继续用。想要稀疏矩阵,直接考虑用下面的OneHotEncoder,生态支持更完整。
3. 真正会翻车的三个场景:列数对不上、列名冲突、漏掉类别
3.1 训练集和测试集分开做独热,模型必炸
这是我在真实项目里见过最多、也最典型的错误。很多人会把训练集和测试集分开放好,然后各自调用一次get_dummies()。表面上看没什么问题,但独热编码生成的列集合取决于“当前数据里实际出现了哪些类别”。
如果某个类别只在训练集里出现,测试集里没有,那么测试集的列数就比训练集少一列;反过来说,如果测试集里冒出一个训练集从未见过的新类别,测试集的列数又会比训练集多一列。模型训练时用的特征矩阵是K列,预测时测试集却是K+1列,框架直接报特征数量不一致。
解决办法有两个。第一个是先拼接再拆分:
df_all = pd.concat([df_train, df_test], axis=0) df_all_dummies = pd.get_dummies(df_all, columns=["颜色"]) df_train_dummies = df_all_dummies.iloc[:len(df_train)] df_test_dummies = df_all_dummies.iloc[len(df_train):]第二个是先对训练集做独热,保存训练集的列名,再用reindex对齐测试集:
train_columns = df_train_dummies.columns df_test_dummies = pd.get_dummies(df_test, columns=["颜色"]) df_test_dummies = df_test_dummies.reindex(columns=train_columns, fill_value=0)第二种方式更常用,因为测试集可能不是一次性读进来的,真实场景里还要处理线上预测。这一步的关键在于:永远以训练集的列集合为准,不要用测试集反过来对齐训练集。
3.2 类别值里的特殊字符会把列名搞乱
独热编码的新列名等于“前缀 + 分隔符 + 原始类别值”。一旦原始类别值里带空格、斜杠、括号、中文标点,新列名就会变得非常难用。
比如城市列里有New York,生成的列名是城市_New York。这个列名用df["城市_New York"]访问没问题,但你要是存成CSV再读回来,或者接SQL、接某些特征平台,空格就可能成为安全隐患。中文列名虽然Pandas支持,但在一些建模框架里会乱码或无法解析。
我踩过几次坑之后,养成了一个习惯:在get_dummies()之前先对类别值做一次清洗,把空格统一替换成下划线,大小写统一,再去掉特殊符号。这一步不改变类别语义,但能让生成的列名干净很多。
df["城市"] = df["城市"].str.strip().str.replace(" ", "_").str.lower()当然,如果你必须保留原始类别值用于业务解释,那就不要清洗,而是在独热之后对列名再统一做一次rename。无论哪种方式,都要有意识地处理,而不是让列名“随机生成”。
3.3 数字型分类变量默认不生成新列,很多人都会懵
get_dummies()的默认行为是只处理object、string、category类型的列。如果某一列是整数编码的地区码,比如1、2、3,就算它本质上是分类变量,Pandas也不会理它。为什么?因为Pandas默认把数值列当作连续变量,独热一个连续变量没有意义。
解决办法是先把这一列转成category类型,再交给get_dummies()。
df["地区码"] = df["地区码"].astype("category") pd.get_dummies(df, columns=["地区码"])这样生成的新列是地区码_1、地区码_2、地区码_3。这个细节很重要,因为很多从数据库里导出的数据,分类变量经常被存成整数。如果没转类型就调用函数,你可能以为自己在做独热编码,实际上这一列压根没被处理,最后模型结果差得莫名其妙。
3.4 布尔列和category类型的“未出现类别”会带来隐性差异
如果某一列本来就是布尔值True/False,get_dummies()也会生成两个新列,列名是列名_True、列名_False。这本身没问题,但要注意列名里的True/False只是字符串,别在代码里把它们和Python布尔值搞混。
另一个更容易被忽略的是category类型的“未出现类别”。如果某一列的categories里定义了A、B、C,但当前数据里只出现了A、B,get_dummies()依然会生成A、B、C三列,因为C已经在分类定义里。而普通object类型只会按实际出现的值生成两列。这两种行为没有绝对的对错,但会影响你“新列数量”的预期。想要稳定可控,建议对分类列统一使用category类型,并主动检查cat.categories。
4. 项目实战中的排查技巧,以及和OneHotEncoder怎么选
4.1 每次生成之后,先做这三件事
很多新手生成完独热列就急着建模,结果列名、类型、缺失值哪里出问题都不知道。我的习惯是生成之后固定做三件事。
第一,看新列名。不能只看一眼,要用一个列表把新增列全部列出来:
original_cols = set(df.columns) new_cols = [c for c in df_dummies.columns if c not in original_cols] print(new_cols)注意,被处理的原始列在df_dummies里已经不在了,所以这个集合差能看到所有新列。如果新列列表里混进了颜色这种看起来像原列名的东西,多半是你忘了传columns,导致某些列被重复处理。
第二,看类型。检查是不是目标类型:
print(df_dummies.dtypes)如果新列是uint8或者bool,内存基本可控;如果全是int64,建议回头把dtype参数加上。
第三,看缺失。独热编码后的结果里理论上不应该有NaN:
print(df_dummies.isna().sum().sum())如果这个数大于0,说明数据在独热之前就已经存在缺失,并且dummy_na=False。要不要处理缺失,取决于业务判断,但你不能连它存在都不知道。
4.2 get_dummies和OneHotEncoder,到底用哪个
很多人会在pd.get_dummies()和sklearn.preprocessing.OneHotEncoder之间纠结。我的判断标准看使用场景。
| 维度 | pd.get_dummies | OneHotEncoder |
|---|---|---|
| 返回结果 | DataFrame,和Pandas无缝衔接 | ndarray或稀疏矩阵,需要自己拼回DataFrame |
| 训练/测试对齐 | 需要手动reindex,容易出错 | 先fit训练集,再用同一个encoder transform测试集,天然对齐 |
| 未知类别处理 | 会直接多生成新列 | handle_unknown='ignore'可以忽略未知类别 |
| 内存表现 | 默认稠密矩阵,高基数时容易爆 | 支持sparse_output=True,内存友好很多 |
| Pipeline集成 | 不适直接进sklearn管道 | 可以作为管道里的一个转换器 |
简单探索、快速验证、写一次性脚本,用get_dummies()很顺手。正式建模、有严格的训练测试流程、需要做交叉验证,我更推荐用OneHotEncoder,因为它能保证训练集和测试集使用同一套类别字典,不会在特征对齐上出幺蛾子。
4.3 高基数列失控之前,先处理类别数量
如果一个类别列有几千个不同取值,直接独热会生成几千列。这个操作在百万行数据上基本就是灾难,光是存储和计算开销就能把内存吃满。
我的做法是先把基数降下来。最常用的是频数编码:保留出现次数最多的Top N个类别,其余归为“其他”。这样既保留了主要信息,又把列数控制在一个合理的范围内。
top_cities = df["城市"].value_counts().head(20).index df["城市_压缩"] = df["城市"].where(df["城市"].isin(top_cities), "其他")然后再对压缩后的列做独热。这一步在真实数据里非常实用。不要看到列数爆炸才想起来优化,而是在对表结构还不熟悉的时候,就要提前看每个列的nunique。
print(df.select_dtypes("object").nunique().sort_values())在动手独热之前多花一分钟跑这个,能帮你判断哪些列适合独热,哪些列需要先做降基处理。
4.4 我的个人排坑流程,最后分享一个小技巧
我处理这种特征工程任务时,习惯先把所有object列和category列的nunique打印出来,只挑基数低于某个阈值的列做独热,剩下的列单独走频数编码或嵌入编码。列名统一采用小写加下划线,生成后用reindex(columns=train_columns, fill_value=0)对齐测试集,最后再用memory_usage检查一次内存占用。这套流程虽然多几步,但能省掉后面调试特征对齐和内存溢出的大量时间。
最后说一个很多文档不会写的小技巧:如果你只是想知道某一列经get_dummies()后会生成哪些新列,不必真的跑一整个模型,直接看df[col].unique()然后按前缀_类别值手工拼出来就八九不离十。但在真实项目里,类别值可能带着空格和特殊符号,拼接结果不一定完全符合预期,所以最终还是要以实际生成后的columns.tolist()为准。毕竟,独热编码的规律可以背,数据里的脏值却永远需要亲眼确认。