☰
pandas select_dtypes 详解:按数据类型高效筛选DataFrame列
2026/10/15 10:44:19 网站建设 项目流程

1. 为什么需要按 dtype 选列:从一个数据处理场景说起

先聊个实际场景。我经常处理那种几百列、几千行的宽表数据,来源往往是业务系统导出的 Excel 或者多张表 join 之后的结果。这种表的一个典型特点是:列很多,但你真正关心的类型很集中。比如有一堆数值型的指标列,有一堆时间列,还有一堆分类代码列,混在一起。你想做的是把数值列挑出来做统计、把时间列挑出来做周期分析、把分类列挑出来做编码处理——这时候如果还在靠肉眼一列一列看,就太没效率了。

用 pandas 里一个很基础但非常实用的功能就能解决:DataFrame.select_dtypes()。这个方法是专门用来按 dtype 提取列的,你告诉它你要哪些类型,它就把匹配的列挑出来;你也可以告诉它你要排除哪些类型,让它把不想要的过滤掉。配合include、exclude两个参数,能在几秒钟内完成对宽表的“类型分流”。

这篇文章就围绕select_dtypes这个核心,讲清楚它的用法、原理、常见坑,以及我实际拿它处理数据时的一些心得。适合所有用 pandas 做数据清洗、特征工程、报表分析的读者,不管你是刚接触 DataFrame 的新手,还是已经被各种 dtype 坑过几次的进阶用户,看完应该都能把它用得顺手。

先说结论:select_dtypes解决的是“按列的数据类型来筛选”这个需求,而它背后真正值得理解的是 pandas 的 dtype 体系和类型判断逻辑。搞清楚这两件事,你才能在面对 object、category、datetime64 这些类型时不再迷糊。

2. 先摸清 dtype 的家底:pandas 里常见的列类型

2.1 从一张 DataFrame 的 dtype 分布说起

当你拿到一个 DataFrame,第一件事应该是看一下每一列的类型。用df.dtypes就能看到全部列的类型,返回的是一个 Series,索引是列名,值是 dtype。实际操作中,我还会配合df.dtypes.value_counts()看一眼整个表的类型分布,这样心里就有数了。

import pandas as pd df = pd.DataFrame({ "姓名": ["张三", "李四", "王五"], "年龄": [25, 30, 35], "身高": [172.5, 168.3, 175.1], "入职日期": ["2023-01-15", "2022-08-20", "2021-11-03"], "是否在职": [True, False, True], "备注": [None, "优秀员工", "需观察"] }) print(df.dtypes)

输出结果类似这样:

姓名 object 年龄 int64 身高 float64 入职日期 object 是否在职 bool 备注 object dtype: object

这里有个很典型的坑:日期列显示的是 object,而不是 datetime64。这是 pandas 里最常见的类型问题之一,后面会专门讲。

2.2 几类核心 dtype 速览

pandas 的 dtype 可以分成几个大类,理解这些分类对后续筛选至关重要:

  • 数值型:int64、float64,以及平台相关的int32、float32等。在 pandas 2.0 之后,还多了int64[pyarrow]这类可空数值类型。
  • 布尔型:bool,用来表示真/假。
  • 时间类型:datetime64[ns]、timedelta64[ns],以及带时区的datetime64[ns, tz]。
  • 字符串/对象类型:object是最常见的,存储混合内容或者字符串;string是 pandas 的专用字符串类型,在 pandas 1.0 之后可用。
  • 分类类型:category,适合存储有限取值的离散数据,例如性别、城市等级。
  • 其他:complex、uint8等,实际用得少,但select_dtypes也能处理。

搞清这些类型后,你才能明白select_dtypes为什么能按关键字匹配,比如'number'能同时匹配int64和float64,'datetime64'能匹配所有datetime64子类型。

2.3 nullable 类型与 pyarrow 后端的取舍

最近在 pandas 2.x 版本里,越来越多人在尝试pd.array支持的Int64(注意大写的 I)和 pyarrow 后端的int64[pyarrow]。这类类型跟经典 numpy 类型有一个显著区别:它们允许缺失值pd.NA存在。

这意味着,如果你用select_dtypes(include=['int64']),不一定能选中Int64列,因为符号匹配上有差异。处理这类情况时,通常要明确写出类型关键字,比如:

df.select_dtypes(include=['Int64']) # 选中 nullable Int64 列 df.select_dtypes(include=['int64']) # 选中经典 numpy 的 int64 列

不过,日常使用中,很多数据源还是以经典 numpy 类型为主,所以我后面讲的操作,也以这些经典类型为主,但会特别标注 nullable 类型的差异。

3. select_dtypes 的完整用法拆解

3.1 参数说明与基础调用方式

select_dtypes是 DataFrame 和 Series 都有的方法。对 DataFrame 调用,返回的是一个新的 DataFrame,包含符合条件的列。核心参数只有两个:

  • include:指定要保留的 dtype,可以是单个类型字符串,也可以是列表。
  • exclude:指定要排除的 dtype,同样可以是单个或列表。

二者可以单独用,也可以同时用。同时用时,逻辑是“先按 include 筛选,再在结果中去掉 exclude 命中的列”。

几个常用例子:

# 只保留数值列 df_numeric = df.select_dtypes(include=['number']) # 只保留整数列 df_int = df.select_dtypes(include=['int64']) # 排除 object 列 df_no_object = df.select_dtypes(exclude=['object']) # 同时筛选:保留数值和布尔,但排除浮点 df_mixed = df.select_dtypes(include=['number', 'bool'], exclude=['float64'])

注意include和exclude不能同时为空,否则会报错。如果你只是想复制一个 DataFrame,直接用df.copy()就行,没必要用select_dtypes()。

3.2 字符串关键字与类型名的匹配规则

select_dtypes的匹配并不是简单的“相等比较”,而是基于字符串包含的关系。比如include='number'会匹配所有类型名中包含number的 dtype,包括int64、float64、complex128等。同理,include='datetime'会匹配datetime64[ns]和datetime64[ns, UTC]。

但是要注意:'int'也能匹配到很多类型,比如int64、int32,如果表里有uint8这种无符号整数呢?'int'匹配不到uint8,因为uint8的类型名是uint8,字符串里包含的是uint,不是int。这个细微差别,容易让人以为选全了整数,结果漏掉无符号列。

为了减少这种歧义,我建议在实际项目中优先用完整的类型名,或者用 pandas 提供的几个特殊关键字:

  • 'number':所有数值类型。
  • 'datetime':所有 datetime64 相关类型。
  • 'timedelta':所有 timedelta64 相关类型。
  • 'datetimetz':带时区的 datetime 类型。
  • 'bool':布尔类型。
  • 'object':object 类型。
  • 'category':category 类型。
  • 'string':pandas 的 StringDtype。

这些关键字是官方文档明确支持的,比手写具体类型更稳。

3.3 include 与 exclude 同时使用的优先级逻辑

刚才提到过,同时使用时会先算 include,再剔除 exclude。举个实际例子:

df_result = df.select_dtypes(include=['number', 'object'], exclude=['float64'])

这个操作的结果是:先选中所有数值列和 object 列,再把 float64 列删掉。所以如果表里有一列float32,它不会被 exclude 误伤,只有float64会被排除。

有时候你会故意这样组合来“减掉某个具体类型”,但在大多数场景下,我建议不要过度依赖这种组合方式。因为一旦列多、类型杂,这样的筛选逻辑容易让人读代码时困惑。更清晰的做法是:先用include定位大类,再用单独的一行做类型清洗(比如.astype()),让代码的意图一步到位。

4. 实操案例:从宽表里按类型分流数据

4.1 构造一个模拟样本数据

为了演示,我构造一份模拟业务数据,里面有数值、分类、日期、备注等不同类型,贴近真实场景。

import pandas as pd import numpy as np df = pd.DataFrame({ "store_id": [1001, 1002, 1003, 1004], "city": ["北京", "上海", "广州", "深圳"], "sales_amount": [12800.5, 23600.3, 9800.0, 15600.8], "order_cnt": [45, 62, 38, 57], "avg_price": [284.4, 380.6, 257.9, 273.7], "is_active": [True, True, False, True], "open_date": ["2021-05-01", "2020-12-15", "2022-03-10", "2019-07-22"], "close_date": ["2023-12-31", "2024-06-30", None, "2022-11-30"], "remark": ["重点门店", None, "待评估", "正常运营"], }) print(df.dtypes)

这里open_date、close_date目前仍然是 object 类型,需要转换后才能被select_dtypes(include=['datetime'])选中。

4.2 转换日期列并完成四大类提取

先把日期列转成真正的datetime64[ns]:

df["open_date"] = pd.to_datetime(df["open_date"]) df["close_date"] = pd.to_datetime(df["close_date"])

然后分组提取:

# 数值列 numeric_cols = df.select_dtypes(include=['number']).columns.tolist() # 布尔列 bool_cols = df.select_dtypes(include=['bool']).columns.tolist() # 日期列 date_cols = df.select_dtypes(include=['datetime']).columns.tolist() # 文本/对象列 object_cols = df.select_dtypes(include=['object']).columns.tolist()

打印出来:

数值列: ['sales_amount', 'order_cnt', 'avg_price'] 布尔列: ['is_active'] 日期列: ['open_date', 'close_date'] 文本列: ['store_id', 'city', 'remark']

注意store_id在这里是 int64 类型,所以不会出现在文本列里。如果它本来就被读成了 object(比如 Excel 里存的是超长数字),那就会落到 object 列里——这是实际工作中很常见的坑。

4.3 实战组合:数值列统计与日期列周期分析

提取只是第一步,提取之后通常要做进一步分析。比如对数值列做汇总:

numeric_df = df.select_dtypes(include=['number']) print(numeric_df.describe())

describe()能快速输出均值、标准差、最小值、四分位数等统计量,省掉自己一个个算的麻烦。

再比如对日期列做周期统计:

date_df = df.select_dtypes(include=['datetime']) for col in date_df.columns: df["year_" + col] = df[col].dt.year

这样就生成了两个年份列,方便后续做同比分析。整体流程是先select_dtypes精准锁定目标列,再批量进行后续操作,避免了手工维护列名列表的麻烦。

这里有个经验:当你需要“批量处理某一类列”时,先通过select_dtypes拿到列名列表,再循环处理,远比手动写死列名更可靠。因为数据源的列顺序可能会变,列名也可能增减,但类型规律相对稳定。

5. 不要在这几个地方踩坑:我对 select_dtypes 的实际教训

5.1 日期列被误认为 object 导致筛选失败

这是我在实际项目中遇到频率最高的坑。Excel 或 CSV 导入的日期列,默认往往就是 object,并不是真正的 datetime64。所以直接select_dtypes(include=['datetime'])会返回空结果,这让很多人第一反应是代码有问题,但实际上类型没转过来。

解决办法是导入后先做类型校正:

df["open_date"] = pd.to_datetime(df["open_date"], errors="coerce")

errors="coerce"表示解析失败的会变成NaT,而不是直接报错。这样至少能保证类型被转换成功,坏数据的处理放到下一步再做。

5.2 布尔类型并不是 number 的子集

bool在 pandas 里虽然可以在很多场景下当作 0/1 来参与计算,但它不属于'number'的匹配范围。所以如果你用select_dtypes(include=['number']),布尔列是选不出来的。

如果你确实想把布尔列当成数值列处理,可以:

df["is_active"].astype(int)

或者干脆用select_dtypes(include=['number', 'bool'])把两类都选中。这一点在特征工程里很重要,因为很多机器学习模型要求输入全是数值,布尔列需要手动转换成 0/1。

5.3 include 和 exclude 传空字符串或同时为空

select_dtypes要求 include 和 exclude 至少传一个,否则会抛异常。比如:

df.select_dtypes(include=[], exclude=[])

会直接报错。实际代码中,如果include是变量,并且可能为空列表,最好加一层判断。

这里还有一个容易绕的细节:如果include传了'number',但 DataFrame 里没有任何数值列,select_dtypes不会报错,只会返回一个没有列的空 DataFrame。后续操作时要留意这种边界情况,否则循环里.columns.tolist()取出来是空列表,逻辑上虽然没异常,但可能埋下隐患。

提示:拿到select_dtypes的结果后,先看一眼.shape或者.columns.tolist(),确认不是空表,再继续做下一步。

5.4 类型关键字输入错误时不一定报错

pandas 对 dtype 关键字的校验并不严格。如果你传了一个不存在的关键字,比如include=['numberr'],它不会立刻报错,而是返回空结果。这种静默失败很坑,一旦代码里没检查行数,后续逻辑就可能莫名出错。

我的建议是写一个简单的断言:

selected = df.select_dtypes(include=['numberr']) assert selected.shape[1] > 0, "未匹配到任何列,请检查 dtype 关键字"

这样至少能在开发阶段快速发现问题,而不是等到线上跑挂了才回头排查。

5.5 不要忽略 MultiIndex 列名的情况

如果你的 DataFrame 有 MultiIndex 列,select_dtypes依然可以按类型筛选,返回结果的列名仍是原来的 MultiIndex 结构,不需要额外处理。这个特性在透视表生成的宽表里很有用,因为透视表的列往往是一个多级索引,手动筛选列名很麻烦,按 dtype 筛选反而是最省力的方式。

6. 进阶用法:在特征工程和自动化流程中的应用

6.1 批量处理数值列的空值填充

拿到的原始数据通常有缺失值。把数值列选出来后,根据列的业务含义决定填充策略,这个过程可以用select_dtypes自动化:

num_cols = df.select_dtypes(include=['number']).columns.tolist() for col in num_cols: if df[col].dtype == "float64": df[col] = df[col].fillna(df[col].median()) else: df[col] = df[col].fillna(0)

这样就能避免手写一堆列名的重复代码,而且如果数据源新增了数值列,这段逻辑依然生效。

我建议在实际项目里,把这一类“按类型批量处理”的操作封装成函数,例如:

def fill_missing_by_type(frame): numeric_cols = frame.select_dtypes(include=["number"]).columns object_cols = frame.select_dtypes(include=["object", "string"]).columns bool_cols = frame.select_dtypes(include=["bool"]).columns frame[numeric_cols] = frame[numeric_cols].fillna(0) frame[object_cols] = frame[object_cols].fillna("未知") frame[bool_cols] = frame[bool_cols].fillna(False) return frame

这样用起来清爽很多,效果也更稳定。

6.2 自动识别特征列和目标列

在做机器学习建模时,数据集中往往有 ID 列、时间列、文本备注列等等,这些列不适合直接进模型。用select_dtypes可以先粗筛一遍,再结合列名过滤,把“该留着做特征”的列和“该剔除”的列分开。

例如:

# 选出数值和布尔列,作为候选特征 candidate_features = df.select_dtypes(include=['number', 'bool']).columns.tolist() # 排除掉明显不是特征的 ID 列(按列名排除) drop_id_cols = [col for col in candidate_features if col.endswith("_id")] feature_cols = [col for col in candidate_features if col not in drop_id_cols]

这种做法在特征数量多的时候特别省心,也避免了手动维护一份长长的特征清单。

6.3 配合 info() 与 memory_usage 做内存优化

DataFrame 列数量大、行数多时,内存占用是个不能忽视的问题。dtypes配合memory_usage(deep=True),可以精确看到每一列消耗了多少内存。常见的优化手段是:

  • 把不需要精度的数值列从float64降为float32。
  • 把取值范围小的整数列从int64降为int32。
  • 把低基数的 object 列转为category。

select_dtypes在这里同样能派上用场:

float_cols = df.select_dtypes(include=['float64']).columns.tolist() df[float_cols] = df[float_cols].astype('float32')

降类型后,数据统计结果几乎没变化,但内存占用能下降不少。对于千万行的数据,这个优化非常明显。

我个人实测过一个十几列、两千万行的数据集,把所有float64降成float32、把int64降成int32,内存从接近 2GB 降到了 1.2GB 左右,效果立竿见影。

7. 排查技巧速查:select_dtypes 出问题时怎么定位

7.1 第一步:看 dtypes,别猜

遇到筛选结果不对,第一件事永远是打印df.dtypes。这个习惯能帮你排除掉“类型本身就不是你以为的那个类型”的问题。我见过很多次“为什么 exclude 了 object 还留下这么多列”,结果一看,里面有string类型,没被object匹配到。

7.2 第二步:验证挑选结果

选出来之后,立刻检查:

selected = df.select_dtypes(include=['number']) print(selected.columns.tolist()) print(selected.dtypes)

如果列数符合预期,再做后续。如果一开始就错了,后面全白做。

7.3 第三步:区分 pandas 版本差异

pandas 老版本(1.x 之前)和 2.x 在 dtype 系统上有一些变化,比如string类型默认不生效、Int64与int64的区别等。如果你在某台机器上代码正常,换到另一台机器上结果不同,先对比两边的 pandas 版本,再看是不是类型匹配关键字的问题。

print(pd.__version__)

这个信息能帮你快速定位问题。我通常会在工程初始化时,把 pandas 版本固定到一个大版本上,比如pandas>=2.0,<3.0,避免这种跨版本的不确定性。

7.4 附:常见问题与解决方案速查表

现象可能原因解决方法
select_dtypes(include=['datetime'])返回空日期列是 object 类型先用pd.to_datetime转换
数值筛选没包含布尔列bool 不属于 number 匹配范围include 加'bool'或手动 astype(int)
include和exclude同时为空报错参数传递错误至少传递一个参数
筛选结果为空但不报错类型关键字拼写错误认真核对关键字;用df.dtypes.value_counts()复查
数值列选出来有缺失值原始数据本身有缺失选出来后统一做 fillna

8. 我对 select_dtypes 的实操体会

用了这么多年 pandas,select_dtypes是为数不多的“看起来简单、用起来省心、上限也高”的方法。它在日常数据分析里的价值,不只是省几行代码,而是提供了一种“按类型批量操作”的思维模式。一旦你习惯了这种模式,处理宽表、处理动态变化的表结构、写可复用的数据处理函数时,都会顺手很多。

最后分享一个小技巧:如果你的数据列在不断的迭代中会新增字段,直接把select_dtypes(include=[...])写进你的 ETL 流程里,比每次手动维护列名列表要可靠得多。因为列名会变,但类型规律通常更稳定。

我个人现在写数据处理脚本时,已经很少直接写死列名了。大部分情况下,先用select_dtypes圈定类型范围,再配合列名前缀后缀做二次过滤,基本能覆盖绝大多数业务需求。这个思路一旦用顺,你会发现自己处理杂表的速度能提升不少。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询