在实际项目里第一次跑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这里的新列名直接就是原始取值本身北京、上海、广州前面没有任何前缀。原因是输入是一个没有名字的 Seriespandas 不知道该怎么给列加前缀为了不让结果列名空着就直接拿值本身当列名。如果这个 Series 有名字比如s.name city输出列名会变吗答案是不会。只要你传入的是 Seriesget_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, prefixdummy))输出列名变成了dummy_北京 dummy_上海 dummy_广州 dummy_A dummy_B这样会把city和level两组原本不同前缀的列合并到同一套命名下。表面看挺省事但实际上非常危险如果city和level里有相同的取值比如都出现过上海和A生成的列名会重复。比如city既有A又有Blevel也有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_firstTrue就是为了解决这个问题每个被编码列的第一个类别会被删掉只保留剩下的 k-1 列。print(pd.get_dummies(df[city], drop_firstTrue))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_naTrueprint(pd.get_dummies(s2, dummy_naTrue))输出北京 上海 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], dtypefloat)如果你用dtypebool每一列会变成 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或float64get_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 类型并指定 categoriesknown_city [北京, 上海, 广州] train pd.DataFrame({city: [北京, 上海]}) test pd.DataFrame({city: [上海, 广州]}) train[city] pd.Categorical(train[city], categoriesknown_city) test[city] pd.Categorical(test[city], categoriesknown_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_firstTrue会删掉上海。但如果你把这一列转成 category 类型并且显式指定了 categoriess3_cat pd.Categorical(s3, categories[北京, 上海, 广州])那么顺序变成北京、上海、广州drop_firstTrue会删掉北京。同一份数据因为 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], axis0)全量观察或者结合业务预设类别绝不只用训练集。第二步把要编码的列统一转成 category 类型并指定 categories。多列一起处理时可以用字典把列名映射到类别列表cat_map { city: [北京, 上海, 广州], level: [A, B, C] } for col, cats in cat_map.items(): df[col] pd.Categorical(df[col], categoriescats)第三步统一做 get_dummies。注意传入 DataFrame 多个列时默认前缀就是列名结果区隔清晰。如果想把编码后的列拼到原来的数值特征里直接pd.concat按列拼接即可。标准流程大概是X pd.get_dummies(df[[city, level]], drop_firstTrue) X pd.concat([df[[score, age]].reset_index(dropTrue), X.reset_index(dropTrue)], axis1)这里特别提一下索引问题如果原始 DataFrame 的索引经过筛选后变得不连续而get_dummies()返回的 DataFrame 索引默认和原表一致直接pd.concat会产生大量 NaN。最简单的处理是在拼接前统一reset_index(dropTrue)把两边索引都归零重排。4.3 什么时候真的别用 get_dummiesget_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默认没有前缀指定prefixcityDataFrame 编码后数值列原地不动默认只处理 object/category/bool 列用columns参数强制指定数值列结果列数和 expected 的 unique 数量不一致可能设了drop_firstTrue删掉了第一个类别确认去掉drop_first或调整 categories 顺序结果里出现一列nandummy_naTrue生效了确认是否真要保留必要时把缺失单独处理训练集和测试集列数不一样两边各自调get_dummies()且类别集合不同先统一 category 的 categories再编码拼接编码后的 DataFrame 出现大量 NaN原表索引不连续concat索引错位编码前后统一reset_index(dropTrue)同一列里有真正字符串 nan 和缺失值字符串 nan 和缺失值都会进入nan列先把字符串 nan 替换成其他占位值5.2 根据长期使用的经验最后再分享三点第一点新列命名尽量保持前缀 取值的稳定格式。get_dummies()的输出列名不只是给人看的后续你很可能要根据列名反推特征来源、做特征的增删或者用df.filter(regexcity)筛选某一组特征。如果前缀乱设、分隔符频繁更换这些操作会变得很难维护。第二点get_dummies()返回的列默认是 pandas 的普通列底层是 dense 的 0/1 矩阵。如果数据量大、类别多但每行实际命中的类别极少dense 存储会浪费大量内存。这时要么用 sklearn 的稀疏输出要么手动构建稀疏矩阵。一个项目中我用过几百列独热特征配合 LogisticRegression内存从 3 GB 降到了 200 MB 以下方法是先把数据转成scipy.sparse.csr_matrix再交给模型。第三点永远不要在拆分训练集和测试集之后才去收集类别。需要做独热编码的列最好在数据清洗阶段就把所有可能的类别收集好转成 category 类型固定下来。这样后面每次跑训练、评估、上线预测特征列永远是一致的。用get_dummies()最大的风险不是它本身而是随数据分布变化的列集合一旦把列集合锁死这个函数就是一个稳定可靠的生产工具。
阅读完成 · 觉得有帮助?