news 2026/10/19 10:24:47

Pandas get_dummies 独热编码:新列生成规则与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Pandas get_dummies 独热编码:新列生成规则与避坑指南

在实际项目里第一次跑pd.get_dummies()的时候,大概谁都有过这种瞬间:明明只调用了一行代码,DataFrame 突然多出十几列,列名歪歪扭扭写着city_北京、level_A这种组合,每一行全是 0 和 1。更奇怪的是,有时候列名只有"北京"、"上海",前面没有任何前缀;有时候有些值明明存在,结果里却少了一列;有时候空值还会变成一个单独的 nan 列。这些现象都不是 bug,而是get_dummies()的设计逻辑在起作用。

这行函数做的事情,本质上就是把一个分类列里每一个取值拆成"是否为该值"的 0/1 列。比如城市列有北京、上海、广州三个值,它会生成三列,每一行只有对应城市那一列是 1,其他都是 0。数学上叫独热编码,pandas 里给这个操作起了个形象的名字get_dummies。

但"到底会生成哪些新列",这个问题远没有看上去那么简单。它受到数据结构、列类型、前缀参数、是否删除首列、是否处理缺失值等一堆因素影响。同一份数据,参数稍微换一换,输出的列就完全不同。这篇文章就把get_dummies()生成新列的每一个规则拆开来说清楚,包括那些大部分教程没有写的边角情况。以下示例以 pandas 2.x 版本为准,个别细节如果你还在用 1.x,需要稍微对照一下版本差异。

1. get_dummies 生成新列的底层规则:列名到底怎么来的

1.1 单列 Series 场景:没有前缀的"裸"列名

先看最简单的情况:只对一个 Series 做编码。

import pandas as pd s = pd.Series(['北京', '上海', '广州', '北京']) encoded = pd.get_dummies(s) print(encoded)

输出:

北京 上海 广州 0 1 0 0 1 0 1 0 2 0 0 1 3 1 0 0

这里的新列名直接就是原始取值本身:北京、上海、广州,前面没有任何前缀。原因是输入是一个没有名字的 Series,pandas 不知道该怎么给列加前缀,为了不让结果列名空着,就直接拿值本身当列名。

如果这个 Series 有名字,比如s.name = 'city',输出列名会变吗?答案是不会。只要你传入的是 Series,get_dummies()就不会自动套用 Series 的名字作为前缀,除非你手动指定prefix。这一点和 DataFrame 场景的区别非常容易让人踩坑,后面会专门说。

1.2 DataFrame 多列场景:默认按原始列名加前缀

换成 DataFrame 传入,输出就完全不一样了。

df = pd.DataFrame({ 'city': ['北京', '上海', '广州'], 'level': ['A', 'B', 'A'] }) print(pd.get_dummies(df))

输出:

city_北京 city_上海 city_广州 level_A level_B 0 1 0 0 1 0 1 0 1 0 0 1 2 0 0 1 1 0

看到没?列名变成了原始列名_取值的形式。city列里每个值都变成一列,用city_北京这样的命名;level列里的取值变成level_A、level_B。这就是 pandas 为了避免多列之间产生列名冲突做的自动处理:每个编码组都用它在 DataFrame 里的列名做前缀,再用默认的下划线_连接。

如果你在 DataFrame 上只传入某一列,比如pd.get_dummies(df['city']),它返回的是裸列名(北京、上海、广州),和传入整个 DataFrame 的结果完全不同。记住这条规律:同样是"城市"这一列,在 DataFrame 场景下默认前缀是列名,在 Series 场景下默认没有前缀,除非显式传prefix。

1.3 新列的顺序和数量由什么决定

新列的数量,默认等于每个被编码列的取值数量之和。比如刚才city有 3 个取值,level有 2 个取值,总共就是 5 列。

列的顺序也有规律:先按原始 DataFrame 里的列顺序从左往右处理,每一列内部再按取值在原始数据中出现的先后顺序排列。拿city列来说,第一行是北京,第二行是上海,第三行是广州,所以生成的列顺序就是city_北京、city_上海、city_广州,而不是字典序的"北京、广州、上海"。这一点和很多人的直觉不一样,也直接影响drop_first删掉的是哪一列。

2. 控制新列数量和名字的四个关键参数

2.1 prefix 和 prefix_sep:给新列起名的开关

prefix是控制列名前缀最直接的手段,但它有三种常见用法,效果差别很大。

第一种,传入单个字符串。强制把所有被编码列的前缀都设置成同一个值:

print(pd.get_dummies(df, prefix='dummy'))

输出列名变成了:

dummy_北京 dummy_上海 dummy_广州 dummy_A dummy_B

这样会把city和level两组原本不同前缀的列,合并到同一套命名下。表面看挺省事,但实际上非常危险:如果city和level里有相同的取值(比如都出现过"上海"和"A"),生成的列名会重复。比如city既有"A"又有"B",level也有"A"和"B",那么dummy_A这个列名会出现两次,最后得到一个带重复列名的 DataFrame。我在项目里见过这种操作引发的诡异报错,后来基本只用字符串前缀去处理单列编码,多列一律不用。

第二种,传入列表。列表长度必须和实际被编码的列数一致,pandas 会按顺序逐个匹配:

print(pd.get_dummies(df, prefix=['city', 'level']))

第三种,传入字典。用字典把原始列名映射到自定义前缀,最灵活也最不容易出错:

print(pd.get_dummies(df, prefix={'city': 'c', 'level': 'lv'}))

输出列名是c_北京、c_上海、c_广州、lv_A、lv_B。

prefix_sep则是连接前缀和取值的分隔符,默认是下划线_。改成其他符号,列名会连带变化:

print(pd.get_dummies(df, prefix_sep='-'))

此时列名为city-北京、city-上海。这里有个使用细节:带横杠、空格这类特殊字符的列名,用df['city-北京']才能取到,用df.city-北京会被 Python 解析成减法表达式直接报错。

2.2 columns 参数:手动圈定编码范围

默认情况下,pd.get_dummies(df)会自动编码所有 object 类型、category 类型和 bool 类型的列。但实际业务里,一个 DataFrame 里往往既有分类列又有数值列,我不想动数值列,只想编码其中某一两列,这时候columns就派上用场了。

df2 = pd.DataFrame({ 'city': ['北京', '上海'], 'level': ['A', 'B'], 'score': [88, 95], 'age': [23, 31] }) print(pd.get_dummies(df2, columns=['city']))

输出里level、score、age都原样保留,只有city被拆成了city_北京、city_上海。这个参数还有个重要用途:强制编码数值列。默认规则里数值列会被直接跳过,但如果你在columns里显式指定某个数值列,pandas 会忽略"是不是数值"这层检查,照样给它生成独热列。比如pd.get_dummies(df2, columns=['score'])就会把score里的每个值都变成一列。

2.3 drop_first:从 k 列变成 k-1 列

线性回归这类模型里,如果一组独热编码把 k 个类别全部保留,会产生完全共线性,模型没法正常求解。drop_first=True就是为了解决这个问题:每个被编码列的第一个类别会被删掉,只保留剩下的 k-1 列。

print(pd.get_dummies(df['city'], drop_first=True))

df['city']的取值是北京、上海、广州,出现顺序也就是这个顺序。第一个是北京,所以输出只有两列:

上海 广州 0 0 0 1 1 0 2 0 1

注意:pandas 删掉的"第一个",不是按字典序,而是按列在数据里的出现顺序;如果原始列是 category 类型,则按分类顺序决定。这一点在 3.3 节会细说。

drop_first可以节省列数、避免共线性,但也带来一个副作用:如果你之后用模型导出特征重要性、看系数,被删掉的那个类别没有自己的系数,解释模型时需要把它当成"基准组"来理解。

2.4 dummy_na:要不要给 NaN 单独开一列

默认情况下,get_dummies()遇到 NaN 不会单独生成列。原始数据里有 NaN 的那一行,在所有新生成的 0/1 列里都是 0。比如:

s2 = pd.Series(['北京', None, '上海', '北京']) print(pd.get_dummies(s2))

输出:

北京 上海 0 1 0 1 0 0 2 0 1 3 1 0

索引 1 那行全是 0,代表"城市未知"。这在某些模型里没问题,树模型能处理全 0 行作为缺失,但线性模型会把全 0 行当成一个特殊类别,效果可能和你预期的不一样。如果明确希望把"缺失"也当成一个独立类别,就设置dummy_na=True:

print(pd.get_dummies(s2, dummy_na=True))

输出:

北京 上海 nan 0 1 0 0 1 0 0 1 2 0 1 0 3 1 0 0

新列名统一叫nan,放在所有取值列的最后面。这里有个小坑:如果原数据里本身就有字符串 "nan",它和真正的缺失值在结果里会共用同一列,直接合并成分不清的状态。真实项目中建议先把字符串 nan 替换成别的占位符,再考虑开不开启dummy_na。

2.5 dtype 参数:控制 0/1 列的数据类型

新列默认类型是uint8,就是无符号 8 位整数。绝大多数场景足够用了,内存也很小。有些库或者自己写的手工特征工程,可能希望输出是浮点类型或布尔类型,可以用dtype指定:

pd.get_dummies(df['city'], dtype=float)

如果你用dtype=bool,每一列会变成 True/False,对某些模型或者数据导出场景反而更直观。但要注意,布尔列的 True/False 在后续数值计算中表现和 0/1 有细微差别,比如做协方差、相关系数时,Pandas 的布尔列升级规则和 uint8 列并不完全一样,能不用就不用。

3. 那些容易踩坑的边界情况

3.1 为什么数值列会被"无视"

pd.get_dummies(df)在处理 DataFrame 时,默认只会编码 object、category、bool 这三种类型的列。纯数值列,比如整数的年龄、浮点数的收入,如果不在columns参数里显式指定,就一个字都不会动。

很多人刚接触时会遇到这种状况:DataFrame 里明明有十几列,每列都是 0 和 1 组成的分类编码,结果调用get_dummies()之后,输出居然是空 DataFrame,一行代码跑完毛都没变。原因很简单:这些列全是int64或float64,get_dummies()认为它们是连续变量,不该做独热。

这个设计是有道理的。数值列通常自带大小关系和运算含义,年龄 30 明显比 20 大,收入 10000 比 5000 高一倍,强行把它们拆成 0/1 会丢信息。pandas 默认保护这种连续性,是一种良性的防御机制。如果你确实想把一个纯数值列当分类处理,两个办法都行:

方法一,用columns参数强制指定:

pd.get_dummies(df2, columns=['age'])

方法二,先转成 category 类型:

df2['age_cat'] = df2['age'].astype('category') pd.get_dummies(df2)

整天和数据打交道的人应该都经历过这个:同一个值,在 Series 里传进去能生成列,在 DataFrame 里传进去就消失。原因就在这节说的规则。

3.2 用 category dtype 固定列集合,解决最头疼的列不一致

真实业务中最难受的场景不是列名难看,而是训练集和测试集的列对不上。比如训练数据里city有三个取值,测试数据里只有两个;或者反过来,测试集里出现了一个训练集从没见过的城市。

如果分别在训练集和测试集上调用pd.get_dummies(),两边生成的新列数量、列名都会不同。最终你拿着两个宽度不一致的 DataFrame 去喂模型,sklearn 直接报维度错误,或者更隐蔽地,因为列顺序对不上而得到错误结果。

解决这个问题的标准做法,是在拆分训练集和测试集之前,先确定完整的类别集合,再把列统一转成 category 类型并指定 categories:

known_city = ['北京', '上海', '广州'] train = pd.DataFrame({'city': ['北京', '上海']}) test = pd.DataFrame({'city': ['上海', '广州']}) train['city'] = pd.Categorical(train['city'], categories=known_city) test['city'] = pd.Categorical(test['city'], categories=known_city) X_train = pd.get_dummies(train['city']) X_test = pd.get_dummies(test['city']) print(X_train.shape) # (2, 3) print(X_test.shape) # (2, 3)

只要 category 里的 categories 一样,get_dummies()永远会按照这个固定的类别列表生成列,哪怕某个 DataFrame 里压根没出现这个类别,也会生成一列全 0。这是确保训练集和测试集列对齐最省心的办法。

这个用法比事后reindex更安全。reindex做不到的事情是:如果测试集里出现了一个训练集没有的新值,它无法凭空补出这一列。如果真遇到这种分布漂移,必须先决定策略:新增类别是当成未知处理,还是归到"其他"类。一般建议在建模前就把低频类别合并成一个"other",再进 category 固定列表。

3.3 列顺序与 drop_first 删掉的到底是哪一列

前文提过,drop_first删的是"第一个"类别。但这个"第一个"到底是谁,取决于列的 dtype。

对于 object 类型列,顺序是取值在数据中第一次出现的顺序。比如下面这一列:

s3 = pd.Series(['上海', '北京', '广州', '上海'])

取值出现顺序是上海、北京、广州,drop_first=True会删掉上海。但如果你把这一列转成 category 类型,并且显式指定了 categories:

s3_cat = pd.Categorical(s3, categories=['北京', '上海', '广州'])

那么顺序变成北京、上海、广州,drop_first=True会删掉北京。同一份数据,因为 dtype 不同,删掉的列完全不一样。模型特征少了哪一列,基准组就是哪个类别,这直接影响后续系数解释。

所以我在项目里都会养成一个习惯:先把所有要编码的分类列统一成 category 类型并指定好 categories 顺序,再统一调用get_dummies()。这样一来,列的顺序、drop_first 删哪列、训练测试列是否对齐,全都在掌控范围内。

4. 真实项目里的建模链路:训练集与测试集列不一致怎么办

4.1 训练集和测试集列数不一致的"灵异事件"

发生在我自己项目里最典型的一次:给一个分类模型准备特征,数据是按月份拆开的训练集和验证集,当时图省事,直接在两个 DataFrame 上分别调了一次pd.get_dummies()。训练集有 47 个城市,验证集有 43 个城市,两边生成的新列数正好差了 4 列,一跑模型,sklearn 报ValueError: X has 43 features, but LogisticRegression is expecting 47 features。

更迷惑的是,如果你把两个编码后的 DataFrame 直接pd.concat起来,列会变成 47 + 43 共 90 列,而且同名列错位,整个特征矩阵是歪的。这种问题如果埋得深一点,模型不会报错,但结果却用的是错位数据,谁看谁头大。

4.2 建模前的标准流水线写法

吃过亏以后,我总结了一套固定流程,每次做分类建模都照这个来:

第一步,先在全量数据上收集类别。一般用pd.concat([train, test], axis=0)全量观察,或者结合业务预设类别,绝不只用训练集。

第二步,把要编码的列统一转成 category 类型,并指定 categories。多列一起处理时,可以用字典把列名映射到类别列表:

cat_map = { 'city': ['北京', '上海', '广州'], 'level': ['A', 'B', 'C'] } for col, cats in cat_map.items(): df[col] = pd.Categorical(df[col], categories=cats)

第三步,统一做 get_dummies。注意传入 DataFrame 多个列时,默认前缀就是列名,结果区隔清晰。如果想把编码后的列拼到原来的数值特征里,直接pd.concat按列拼接即可。标准流程大概是:

X = pd.get_dummies(df[['city', 'level']], drop_first=True) X = pd.concat([df[['score', 'age']].reset_index(drop=True), X.reset_index(drop=True)], axis=1)

这里特别提一下索引问题:如果原始 DataFrame 的索引经过筛选后变得不连续,而get_dummies()返回的 DataFrame 索引默认和原表一致,直接pd.concat会产生大量 NaN。最简单的处理是在拼接前统一reset_index(drop=True),把两边索引都归零重排。

4.3 什么时候真的别用 get_dummies

get_dummies()用起来很爽,但它不是万能的。我用过的实际项目里,下面这几种情况都建议换工具:

首先是基数过高。某一列有几百上千个唯一值,比如用户 ID、订单号、精确地址,做独热编码会得到几千列,内存直接爆炸,而且绝大多数列稀疏到 99.9% 都是 0,模型效果大概率更差。这种场景改用目标编码、频次编码,或者直接用 embedding 技术处理。

其次是树模型。随机森林、梯度提升树这类模型本身能处理类别切分,强行独热编码反而增加分裂时间、降低可解释性,还容易过拟合。对这类模型,直接用标签编码把类别映射成整数反而更合适。pandas 里可以用pd.factorize():

codes, uniques = pd.factorize(df['city'])

返回的codes就是每个取值对应的整数编号,树模型用这个完全没问题。

再次是当你需要反复对同一批类别做转换时。get_dummies()是一个"是有多少取值就生成多少列"的无状态函数,它不记忆之前的类别集合。写多个模块代码的时候,最好用 sklearn 的OneHotEncoder,它 fit 之后可以反复 transform,天然保证训练集和测试集列一致。pandas 2.x 里也可以用Categorical类型配合 categories 来达到类似效果,但需要写更多样板代码。

5. 常见问题速查与长期使用心得

5.1 常见问题速查表

现象原因解决办法
输出列名没前缀,只有北京这样的值传入的是 Series,默认没有前缀指定prefix='city'
DataFrame 编码后数值列原地不动默认只处理 object/category/bool 列用columns参数强制指定数值列
结果列数和 expected 的 unique 数量不一致可能设了drop_first=True,删掉了第一个类别确认去掉drop_first,或调整 categories 顺序
结果里出现一列nandummy_na=True生效了确认是否真要保留,必要时把缺失单独处理
训练集和测试集列数不一样两边各自调get_dummies(),且类别集合不同先统一 category 的 categories,再编码
拼接编码后的 DataFrame 出现大量 NaN原表索引不连续,concat索引错位编码前后统一reset_index(drop=True)
同一列里有真正字符串 "nan" 和缺失值字符串 nan 和缺失值都会进入nan列先把字符串 nan 替换成其他占位值

5.2 根据长期使用的经验,最后再分享三点

第一点,新列命名尽量保持"前缀 + 取值"的稳定格式。get_dummies()的输出列名不只是给人看的,后续你很可能要根据列名反推特征来源、做特征的增删,或者用df.filter(regex='city')筛选某一组特征。如果前缀乱设、分隔符频繁更换,这些操作会变得很难维护。

第二点,get_dummies()返回的列默认是 pandas 的普通列,底层是 dense 的 0/1 矩阵。如果数据量大、类别多但每行实际命中的类别极少,dense 存储会浪费大量内存。这时要么用 sklearn 的稀疏输出,要么手动构建稀疏矩阵。一个项目中我用过几百列独热特征配合 LogisticRegression,内存从 3 GB 降到了 200 MB 以下,方法是先把数据转成scipy.sparse.csr_matrix再交给模型。

第三点,永远不要在拆分训练集和测试集之后才去收集类别。需要做独热编码的列,最好在数据清洗阶段就把所有可能的类别收集好,转成 category 类型固定下来。这样后面每次跑训练、评估、上线预测,特征列永远是一致的。用get_dummies()最大的风险不是它本身,而是随数据分布变化的列集合;一旦把列集合锁死,这个函数就是一个稳定可靠的生产工具。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 10:24:12

内嵌式模组如何重塑精密传动系统的刚度与空间效率

1. 内嵌式模组在精密传动里到底指什么——先把边界划清楚这几年做精密传动项目,最明显的感觉是:设备对“空间利用率”的要求越来越高,很多机构不再愿意像以前那样,把电机、减速器、联轴器、编码器一个个串在一条线上,再…

作者头像 李华
网站建设 2026/10/19 10:23:44

合并K个升序链表:从最小堆到分治,吃透多路归并

合并 K 个升序链表,这道题几乎每个刷算法的人都会碰到。它题目很短,但考点密度相当高:链表遍历、指针修改、堆的使用、分治思想、复杂度推导,甚至还能延展到外部排序。如果你只是背一个 PriorityQueue 版本,面试时被追…

作者头像 李华
网站建设 2026/10/19 10:23:33

ArtCraft:用可复现工艺承载不可复制的创意表达

1. 从“artcraft”这个名字说起:它到底想解决什么问题 第一次看到“artcraft”这个标题,我脑子里蹦出来的不是某个具体工具,而是一种很朴素的冲动——把“art”和“craft”这两个词捏在一起。art是艺术、是审美、是那种说不清道不明的“感觉对…

作者头像 李华
网站建设 2026/10/19 10:22:29

context-mode:多任务场景下的上下文切换与AI协作管理

最近大半年,我一直在折腾一个叫 context-mode 的小工具。起因很简单:我手上同时维护着一个工具链项目、几个内容脚本和一个半死不活的小网站,每次从一套逻辑切到另一套逻辑,都要重新找对应的提示词、重新核对文件路径、重新回忆当…

作者头像 李华
网站建设 2026/10/19 10:21:53

AI生成测试数据的三类过拟合陷阱与反过拟合方案

前阵子收到一个很典型的咨询:某团队用大模型生成了几万条测试数据,模型在合成测试集上准确率到了98%,结果一上生产环境立刻掉到80%以下。对方问我到底是数据生成的质量不行,还是模型本身就不行。我说,大概率是你掉进了…

作者头像 李华
网站建设 2026/10/19 10:21:27

Python医疗知识图谱问答系统源码部署:从环境配置到完整问答链路跑通

简介:本资源为基于Python的医疗知识图谱问答系统完整源码,面向计算机相关专业学生及需要完成毕业设计、期末大作业或课程设计的开发者。项目围绕医疗领域知识图谱构建与智能问答展开,涵盖意图识别、命名实体识别、知识抽取与图谱构建等核心模…

作者头像 李华