pandas Nullable Integer 数据类型完整指南:用 Int64 与 pandas.NA 告别"整数变浮点"的精度陷阱
【免费下载链接】pandasFlexible and powerful data analysis / manipulation library for Python, providing labeled data structures similar to R data.frame objects, statistical functions, and much more项目地址: https://gitcode.com/gh_mirrors/pa/pandas
本指南基于 pandas 官方用户手册(integer_na.rst)编写,系统讲解 pandas 的可空整数(nullable integer)扩展类型:为什么整数列一旦含缺失值就会被迫变成浮点数、IntegerArray如何在保持整数语义的同时使用pandas.NA表示缺失,以及从构造、运算到归约的全套实战用法。读完本文,你将掌握pd.array([...], dtype="Int64")的正确打开方式,理解pandas.array与Series在 dtype 推断上的差异,并能安全地在含缺失值的整数列上执行算术、比较、分组与求和操作而不丢失精度。
为什么需要"可空整数":NaN 把整数变成了浮点
在 pandas 的 缺失数据处理 章节中可以看到,pandas 主要使用NaN表示缺失值。由于NaN在 NumPy 中是浮点数,任何含缺失值的整数数组都会被强制提升为浮点类型。对大多数场景这无伤大雅,但如果你的整数列是标识符(identifier)——例如用户 ID、订单号、外键——情况就变得棘手:
- 语义混乱:将 ID 从整数转成浮点,类型信息被破坏,序列化、对接数据库或下游校验时容易出错;
- 精度丢失:部分大整数无法被浮点数精确表示(IEEE 754 双精度浮点只有 52 位尾数,超过
2^53的整数会丢失精度),例如9007199254740993这样的 ID 在 float64 中会被舍入。
pandas 给出的答案是可空整数扩展类型:arrays.IntegerArray使用pandas.NA作为缺失值(而不是NaN),让缺失值不再"污染"整数 dtype。
构造 IntegerArray:pd.array + 大写的 "Int64"
IntegerArray是 pandas 内部实现的一种扩展类型,可以通过pd.array配合整数 dtype 构造。注意官方强烈推荐显式指定 dtype,避免依赖推断规则。
import pandas as pd import numpy as np # 方式一:显式 dtype 对象 arr = pd.array([1, 2, None], dtype=pd.Int64Dtype()) arr # <IntegerArray> # [1, 2, <NA>] # Length: 3, dtype: Int64字符串别名:"Int64"(大写 I)与 NumPy 的 'int64' 不同
"Int64"(注意大写的I)是字符串别名,用于和 NumPy 的'int64'dtype 区分:
pd.array([1, 2, np.nan], dtype="Int64") # <IntegerArray> # [1, 2, <NA>] # Length: 3, dtype: Int64所有 NA 类值统一归一化为 pandas.NA
构造时,各种缺失值记号(np.nan、None、pd.NA)都会被统一替换为pandas.NA:
pd.array([1, 2, np.nan, None, pd.NA], dtype="Int64") # <IntegerArray> # [1, 2, <NA>, <NA>, <NA>] # Length: 5, dtype: Int64存入 Series / DataFrame
构造好的数组可以像普通 NumPy 数组一样存入Series:
pd.Series(arr) # 0 1 # 1 2 # 2 <NA> # dtype: Int64也可以直接把列表对象连同 dtype 一起传给Series构造器:
pd.Series([1, 2, None], dtype="Int64")从源码看,pd.array([1, None])会走_coerce_to_data_and_mask的推断路径(numeric.py),默认采用np.int64作为底层存储 dtype(对应IntegerDtype._default_np_dtype,见 integer.py)。
⚠️ 关键陷阱:pandas.array 与 Series 的 dtype 推断规则不同
官方文档明确警告:当前pandas.array与pandas.Series使用不同的 dtype 推断规则:
# pandas.array 会推断出可空整数 dtype pd.array([1, None]) # <IntegerArray> # [1, <NA>] # Length: 2, dtype: Int64 pd.array([1, 2]) # <IntegerArray> # [1, 2] # Length: 2, dtype: Int64# 出于向后兼容,Series 将其推断为普通整数或浮点 dtype pd.Series([1, None]) # 0 1.0 # 1 NaN # dtype: float64 pd.Series([1, 2]) # 0 1 # 1 2 # dtype: int64看到差异了吗?pd.Series([1, None])得到的是float64(含有NaN的浮点列),而不是可空整数。为避免混淆,官方建议始终显式提供 dtype:
pd.array([1, None], dtype="Int64") pd.Series([1, None], dtype="Int64")文档同时说明:未来可能会为Series提供推断可空整数 dtype 的选项。
预占位列的最佳实践:用 pd.Series(pd.NA, dtype="Int64") 而非直接赋 pd.NA
如果先创建一个全NA的新列、之后再用真实数据填充(例如df['new_col'] = pd.NA),该列的 dtype 会被设为object,后续性能明显劣于合适的类型。更优做法是:
df = pd.DataFrame() df['objects'] = pd.Series(pd.NA, dtype="Int64") df.dtypes # objects Int64 # dtype: object(或其他支持NA的 dtype,如"Float64"、"string")。注意直接df['new_col'] = pd.NA会得到objectdtype:
df = pd.DataFrame() df['objects'] = pd.NA df.dtypes # objects object # dtype: object底层实现:两个 NumPy 数组(data + mask)
要理解可空整数的行为,值得看一眼它的内部表示。IntegerArray继承自NumericArray(numeric.py),后者又继承自BaseMaskedArray(masked.py)。其 docstring 明确描述了内部结构(integer.py):
- data:一个 dtype 合适的 NumPy 整数数组,存放实际数值;
- mask:一个布尔数组,标记缺失位置(
True表示缺失)。
也就是说,缺失信息与数值本身分离存储,数值部分始终保持整数 dtype,因此无需像传统方案那样把整数提升成浮点。这正好解释了为什么IntegerArray能"鱼与熊掌兼得":既有原生的整数表示,又能表达缺失。
IntegerArray的构造参数为:
| 参数 | 类型 | 说明 |
|---|---|---|
values | numpy.ndarray | 1 维整数 dtype 数组 |
mask | numpy.ndarray | 1 维布尔 dtype 数组,标记缺失位置 |
copy | bool, default False | 是否复制values与mask |
IntegerDtype在底层存储上使用_internal_fill_value = 1填充掩码位置(避免向上转型),并以np.int64作为默认存储 dtype。构造时若掩码存在,values[mask]处会被填充该值(见 numeric.py)。
可用的 dtype 全家桶
从 integer.py 可以看到,IntegerDtype派生出一系列注册好的具体 dtype,覆盖有符号与无符号整数:
- 有符号:
Int8Dtype、Int16Dtype、Int32Dtype、Int64Dtype,对应"Int8"~"Int64"; - 无符号:
UInt8Dtype、UInt16Dtype、UInt32Dtype、UInt64Dtype,对应"UInt8"~"UInt64"。
它们之间的映射定义在模块末尾的NUMPY_INT_TO_DTYPE字典中(integer.py),例如np.dtype(np.int8)对应Int8Dtype()。构造示例:
pd.array([1, None, 3], dtype=pd.Int32Dtype()) pd.array([1, None, 3], dtype="UInt16")运算行为:向 NumPy 语义看齐,缺失值自动传播
涉及可空整数数组的运算与 NumPy 数组行为类似:缺失值会传播,需要时数据会强制转换为其他 dtype。
s = pd.Series([1, 2, None], dtype="Int64") # 算术:缺失值传播 s + 1 # 0 2 # 1 3 # 2 <NA> # dtype: Int64 # 比较:缺失值传播,结果中缺失仍为 <NA> s == 1 # 0 True # 1 False # 2 <NA> # dtype: boolean # 切片操作 s.iloc[1:3] # 1 2 # 2 <NA> # dtype: Int64 # 与其他 dtype 运算:自动对齐类型,结果按需提升 s + s.iloc[1:3].astype("Int8") # 0 <NA> # 1 4 # 2 <NA> # dtype: Int64 # 需要时强制转换:加上浮点后结果变为可空浮点 s + 0.01 # 0 1.01 # 1 2.01 # 2 <NA> # dtype: Float64注意s + s.iloc[1:3]的结果在索引 0 处为<NA>,这展示了可空整数在对齐运算(类似 join 的索引对齐)时缺失值的传播语义。
在 DataFrame 中协同工作
这些 dtype 可以作为DataFrame的列类型参与运算:
df = pd.DataFrame({"A": s, "B": [1, 1, 3], "C": list("aab")}) df # A B C # 0 1 1 a # 1 2 1 a # 2 <NA> 3 b df.dtypes # A Int64 # B int64 # C object # dtype: objectdf中混合了可空整数列A、普通整数列B和对象列C,各列保持各自的 dtype。
合并、重塑与类型转换
# concat 合并后各列 dtype 保持 pd.concat([df[["A"]], df[["B", "C"]]], axis=1).dtypes # A Int64 # B int64 # C object # dtype: object # 可空整数可转回普通浮点 df["A"].astype(float) # 0 1.0 # 1 2.0 # 2 NaN # dtype: float64astype(float)时,<NA>会被转换为NaN,这正是"有缺失的整数被迫变浮点"的经典场景——当你确实需要浮点结果时,可以显式完成这一转换。
归约与 groupby 操作
sum等归约操作以及groupby聚合同样开箱即用:
df.sum(numeric_only=True) # A 3.0 # B 5.0 # dtype: float64 df.sum() # A 3 # B 5 # C aab # dtype: object df.groupby("B").A.sum() # B # 1 3 # 3 0 # Name: A, dtype: Int64注意三处细节:
df.sum(numeric_only=True)的结果是float64(A列的和从Int64转为浮点);- 全列
df.sum()时C列按字符串拼接; df.groupby("B").A.sum()的返回列保持Int64dtype——分组求和后的结果仍然是可空整数,缺失值语义得以保留。
标量缺失值:pandas.NA
IntegerArray使用pandas.NA作为标量缺失值。对单个缺失元素做切片会返回pandas.NA:
a = pd.array([1, None], dtype="Int64") a[1] # <NA>这一点与np.nan有本质区别:pd.NA是 pandas 统一的缺失值标记,属于pandas自己的 NA 语义体系(pandas.NA实例),其参与运算时遵循"缺失传播 + 结果类型提升"的规则,不会像np.nan那样把整数 dtype 直接拉成浮点。
小结与推荐用法
| 场景 | 推荐写法 |
|---|---|
| 创建可空整数数组 | pd.array([1, 2, None], dtype="Int64") |
| 创建含缺失的整数 Series | pd.Series([1, 2, None], dtype="Int64") |
| 给 DataFrame 预填 NA 列 | df['col'] = pd.Series(pd.NA, dtype="Int64") |
| 无符号变体 | "UInt8"~"UInt64" |
| 转回浮点 | s.astype(float)(<NA>变NaN) |
核心结论:凡是对"整数 ID 不得变浮点"有硬性要求的场景(标识符列、精确大整数、下游强类型约束),都应使用可空整数 dtype;对普通分析场景,float64+NaN仍是轻量默认选择。构造时始终显式传入 dtype,是规避pandas.array与Series推断差异的最稳妥做法。想深入了解扩展类型的通用机制,可继续阅读 扩展类型开发指南 与 缺失数据处理;想看源码级实现,可直接研读 pandas/core/arrays/integer.py 及其基类 pandas/core/arrays/numeric.py、pandas/core/arrays/masked.py。
【免费下载链接】pandasFlexible and powerful data analysis / manipulation library for Python, providing labeled data structures similar to R data.frame objects, statistical functions, and much more项目地址: https://gitcode.com/gh_mirrors/pa/pandas
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考