又是这个报错。相信很多做数据分析的同学都撞见过:明明只是想把一列浮点数字转成整数,结果astype(int)一执行,pandas 直接甩出一句IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer。遇到这个报错的第一反应往往是“我的数据哪脏了”,但真正的问题是:整数类型和 NaN、inf 之间的关系,远比你想象的更严格。
这个报错本质上是 pandas 在保护你——它拒绝把一个没法用整数表示的值硬塞进整数类型里。我最早在 pandas 0.24 上跑旧代码时,这个操作还只是给个 FutureWarning,升级到 pandas 1.0 之后直接变成硬报错,当时确实让不少线上管道直接崩了。这篇文章我会从根因、触发场景、五种解法到生产环境工程化习惯,把这条报错彻底拆干净。不管你是刚接触 pandas 的新手,还是被线上任务搞到头大的老手,都有可以直接抄作业的代码。
1. 报错现场与根因拆解:为什么int装不下NaN和inf
1.1 三步复现IntCastingNaNError的典型代码
这个报错最经典的触发方式就是把含“洞”的浮点列直接转整数。写个最小复现:
import pandas as pd import numpy as np df = pd.DataFrame({ "price": [19.9, 20.0, float("nan"), 21.5] }) df["price"].astype(int)在 pandas 1.0 及以上的版本里,最后一行会直接抛出:
IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer注意这里的报错信息把两类值并列在了一一起:NA和inf。也就是说,不仅仅是空值,哪怕是float('inf')或float('-inf')这种无穷大值,同样会被拦截。你可以在同一列里混入几个无穷大试试:
df2 = pd.DataFrame({"v": [1.0, np.inf, -np.inf, 2.0]}) df2["v"].astype(int)结果一模一样,照样炸。理解了这一点,排查方向就不是“某一列有 NaN”,而是“这一列里有没有任何非有限值”。
1.2 整数类型为什么天生拒绝“缺失值”和“无穷大”
这里要稍微讲点底层原理,理解之后你会觉得这个报错其实特别合理。
整数类型在计算机里是固定位数的,比如int64就是 64 个比特位,每个位模式都对应一个整数。它没有为“缺失”“无穷大”这样的状态预留任何位模式。而 NaN 和 inf 都是浮点数规范(IEEE 754)里的特殊值,用某几组特定位表示,只有浮点类型能承载它们。
打个比方:int 就像一列编好号的储物柜,每个柜子必须放进一个具体物品,你不能在一个柜子里贴个“这里什么也没有”;inf 相当于一个柜门永远关不上、无限大的空间,同样没法放进去。所以 pandas 不是不让你转,而是它真的不知道该怎么把一个“不存在”或“无限大”的东西映射成一个整数。
如果你非要用旧版行为,在 pandas 0.24 以前,astype(int)遇到 NaN 时可能会得到非常离谱的未定义值(旧版本里甚至会出现负数、随机大数),这种静默错误比报错更危险。所以从 pandas 1.0 开始,社区索性把这个默认行为改成显式报错,逼你先处理非有限值——长期来看这是帮你,不是坑你。
2. 先别急着修,定位NA与inf是被谁带进来的
2.1 文件导入时漏网的缺失值
绝大多数时候,NaN 不是你手动制造的,而是数据文件本身就有“洞”。最常见的是read_csv时遇到空字段、NA、null、N/A这类标记,pandas 会默认把它们解析成NaN。
df = pd.read_csv("sales.csv") print(df.dtypes)你看到price列是float64,很可能就是因为里面有缺失值。如果原来文件里的价格都是整数格式,pandas 碰到缺失值后只能把整列抬升成浮点类型,这本身就是一条线索。
此时先别急着转换,先看缺失到底有多严重:
df["price"].isna().sum() df["price"].isna().mean()缺失占比超过 50% 的列,后续转换策略和缺失占比不到 1% 的列,完全不是一个处理思路。
2.2 merge、groupby、算术运算中“凭空出现”的inf
第二种坑更隐蔽——原始数据里根本没有 NaN,是你在处理过程中制造出来的。举几个常遇到的场景:
- 用
df1.merge(df2, on="id", how="left")时,右表没有对应键,结果列就变成 NaN。 - 计算转化率、点击率这类比率时,分母为 0,
click / view直接得到inf。 - 对数据做
np.log(0)、np.sqrt(-1),前者产生-inf,后者产生 NaN。 - groupby 聚合后,某些分组没有数据,聚合列也可能是 NaN。
我踩过印象最深的一次是:两张表按用户 ID 左连接,因为 ID 在右表里重复了多次,产生了多对多膨胀,空值区域一下子多出一大片。当时我没查 merge 后的行数,直接去转 int,被IntCastingNaNError卡住后才发现源头的 join 逻辑就有问题。
2.3 三行代码快速定位异常行
不管数据是怎么脏的,修复前先准确查出异常行,我通常用下面这三行:
num_col = pd.to_numeric(df["target"], errors="coerce") bad_mask = num_col.isna() | num_col.isin([np.inf, -np.inf]) df.loc[bad_mask]如果列里本身是数字类型,直接:
bad_mask = df["target"].isna() | df["target"].isin([np.inf, -np.inf]) df.loc[bad_mask]然后把bad_mask导出到 CSV 或直接打印前几十行,肉眼看看到底是文件里的空值、除零产生的 inf,还是某些异常文本变成的 NaN。这个定位过程一定要做,因为不同的产生路径对应完全不同的修复方案,直接在数据末尾 fillna 只是治标。
我用一个速查表总结异常值来源与特点,排查时照着看:
| 异常来源 | 典型值 | 出现场景 | 特征 |
|---|---|---|---|
| 文件导入 | NaN | CSV/Excel 空单元格、NA 标记 | 集中在导入列,数量与原始文件一致 |
| join / merge | NaN | 左连接或右连接无匹配 | 新增列出现整块缺失 |
| 除法、对数 | inf / -inf | a / b时 b=0、log(0) | 数值明显异常,分布可能有规律 |
| 字符串转换 | NaN | to_numeric(errors="coerce") | 原本看起来像数字的列,部分行无法解析 |
3. 五套方案实操选型:从fillna到可空Int64
3.1 方案A:fillna填充后转int,适合缺失占比低
最直觉的解决方案是把缺失值填成一个具体的整数,再执行转换。
df["price"] = df["price"].fillna(0).astype(int)这段代码能跑通,但隐藏风险非常大——如果业务上“0”和“缺失”含义不同(比如价格缺失不等于免费),你用 0 一填充,后续所有按价格聚合的统计都会被带偏。所以填什么值必须由业务语义决定,不能图省事统一填 0。
常见填法参考:
- 计数字段:填 0 往往合理,比如“购买次数”缺失可以理解为 0 次。
- ID、业务编码:填 0 或 -1 都行,只要确保和真实 ID 不冲突。
- 连续型特征:填中位数或均值,适合后续喂给机器学习模型。
- 时间序列字段:用
method="ffill"前向填充,保留趋势信息。
一个我常用的组合是先替换 inf 再 fillna:
df["price"] = ( df["price"] .replace([np.inf, -np.inf], np.nan) .fillna(0) .astype(int) )这样一次性把非有限值和缺失值统一处理掉,不会遗漏 inf。
3.2 方案B:dropna直接删行,简单但有代价
如果缺失值占比很低,比如不到 1%,而且你并不需要保留那些行,直接删掉是最干净的办法。
df = df.dropna(subset=["price"]).copy() df["price"] = df["price"].astype(int)需要注意的是dropna之后一定要reset_index(drop=True),否则行索引还带着原来的数值。另外,如果这一列是下游表的主键或者关联键,删行可能导致其他表的数据对不上,这个方案就行不通。
我一般只在两种场景下用 dropna:一是临时探索数据,追求快速;二是缺失行数极少,并且确认这些行对最终统计没有任何影响。
3.3 方案C:astype('Int64')保留NA,pandas的“可空整数”
如果你既想要整数类型,又舍不得丢缺失值,pandas 很早就提供了可空整数类型Int64。注意这里是大写的I,和 numpy 的int64(小写)不是同一个东西。
df["price"] = df["price"].astype("Int64") print(df["price"].dtype) # Int64转换后,原本的np.nan会变成pd.NA,列类型是Int64,你在数据里仍然能看到缺失值,但类型已经是整数语义了。后续做sum()、mean()、value_counts()等操作时,pandas 会默认跳过pd.NA,不会像普通的float64那样把缺失值带进计算。
如果你的数据是从各种来源拼接起来的,可以先让 pandas 自动推断可空类型,再根据需要微调:
df = df.convert_dtypes() print(df.dtypes)convert_dtypes()会把合适的浮点列自动转成Int64,把字符串列推断为string类型,很多IntCastingNaNError问题在源头就被消除了。我个人在 pandas 1.x 时代就养成了 pipeline 开头先跑一次convert_dtypes()的习惯。
3.4 方案D:先清理inf再走方案A/B
很多情况下数据的 NaN 其实并不多,问题恰恰出在inf上。特别是统计完比率之后,分母为零的行会顶着一个巨大的无穷大,这时候直接fillna是不生效的,因为inf不是空值。
s = pd.Series([1.0, np.inf, 2.0, np.nan]) s.fillna(0).astype(int) # IntCastingNaNError:inf 还在正确姿势是先把 inf 替换成 NaN(或直接替换成具体值):
clean_s = s.replace([np.inf, -np.inf], np.nan) clean_s.fillna(0).astype(int)还有一种更省事的方式是按“有穷数”来筛选:
mask = np.isfinite(s) s.loc[mask].astype(int)np.isfinite会同时筛掉 NaN 和 inf,语义非常清晰。我习惯在排查阶段用这个函数,因为它能一步定位出“所有非有限值”,不用分别判断 isna 和 isinf。
3.5 方案E:round/ceil/floor后转int,浮点精度陷阱
如果列里的数据本身都是整数,只是因为浮点运算带上了小数点,比如 49.99999999、3.00000001,你可能会想先 round 再转。这个思路没错,但有几个细节容易翻车。
s = pd.Series([49.999999, 3.0000001, 5.5]) s.round().astype(int)round()返回的还是浮点类型,但只要结果都是有限值,astype(int)就不会报错。这里要注意的是 Python 的银行家舍入:round(5.5)结果是 6,round(4.5)结果是 4,和很多人以为的“四舍五入”不太一样。如果你需要严格四舍五入,用:
import numpy as np s2 = np.floor(s + 0.5).astype(int)另外,如果列里存在 NaN 或 inf,round()并不会把它们去掉,所以方案 E 通常要配合方案 A/B/C 一起使用。
3.6 一张表搞定方案对比与选型
五种方案放到一起对比,选型的时候直接看业务需求:
| 方案 | 保留缺失值 | 适用场景 | 主要风险 |
|---|---|---|---|
| fillna + astype(int) | 否 | 缺失有明确业务含义,能填具体值 | 填错值会污染统计结果 |
| dropna + astype(int) | 否 | 缺失占比低,删行不影响整体 | 影响行数,主键关联场景慎用 |
| astype('Int64') | 是 | 不想丢缺失,又要整数语义 | 老版本pandas不支持 |
| 清理inf后fillna/dropna | 视后续处理 | 数据里有除零、对数产生的inf | 掩盖源头问题 |
| round/ceil后astype(int) | 否 | 浮点精度噪声,需要取整 | 舍入规则与预期不一致 |
方案没有绝对的优劣,关键看你的下游是统计报表还是机器学习模型。如果是统计报表,缺失值往往需要保留并在报表里体现,优先用Int64;如果是建模特征,则需要填充或删除,避免模型输入里有空值。
4. 边界情况与坑位合集:字符串、时间戳、超大值
4.1 字符串列里的'3.0'和'1,000':astype(int)救不了
还有一个常见的误解是:只要是看起来像数字的列,就能直接astype(int)。但字符串列里的"3.0"、"1,000"、"12a",直接转 int 会得到 ValueError,而不是IntCastingNaNError。
s = pd.Series(["3.0", "1,000", "12a"]) s.astype(int) # ValueError: invalid literal for int() with base 10: '3.0'正确做法是先用pd.to_numeric统一清洗:
num = pd.to_numeric(s.str.replace(",", ""), errors="coerce") print(num) # 0 3.0 # 1 1000.0 # 2 NaN得到的结果里可能又出现 NaN,接下来再去走方案 A/B/C。这类问题最让人头疼的地方就是:同一个字段在不同批次的数据里格式不一致,比如这个月导出是1000,下个月就变成1,000了。所以清理时一定要把replace(",", "")这类操作写进 pipeline,而不是只修一次。
4.2 时间戳转int时NaT的污染
处理日期列时,NaT是“Not a Time”的缺失标记。如果你想把日期列转成整数时间戳(比如从 1970 年开始的纳秒数),格式类似:
ts = pd.to_datetime(pd.Series(["2023-06-01", None])) ts.astype("int64") # 可能得到异常极值或直接报错这里有个很麻烦的点:NaT在底层是用int64的最小值-9223372036854775808来存储的,你转成整数后如果没检查,就会有一个巨大负数潜伏在数据里。后续做时间差计算、排序、画图,都会被这个极值污染。
我的处理习惯是先把时空值剔掉,再转时间戳:
ts = ts.dropna() timestamps = ts.astype("int64") // 10**9 # 转成秒级时间戳如果一定要保留缺失行,就把时间戳列和缺失标记列分开存,比如加入一列is_missing,不要把NaT硬塞进整数列。
4.3 浮点49.999999:round也可能骗你
浮点数转整数时,截断行为是很多人踩坑的重灾区。astype(int)对 3.99 的处理是直接截断成 3,不是四舍五入成 4。而由于浮点表示误差,你可能看到一个数“应该是 50”,但实际上底层是 49.999999999。
x = 50.0 y = 0.1 * 500 # 理论是50.0,实际可能是50.0,但也可能是49.999...如果你用int(x)去转,很可能得到 49。所以需要取整的列,我一般先用round()或np.rint()明确舍入意图,再转整数:
df["count"] = df["count"].round().astype(int)注意astype(int)本身不会帮你舍入,它永远只做截断。这个细节在金融场景里尤其致命,一分钱误差可能造成后续对账不平。
4.4 int64溢出边界:别把时间戳塞进32位
最后一个边界问题是数值范围。np.int32能表示的范围是 -2147483648 到 2147483647,如果你把时间戳(纳秒级)塞进去,直接溢出;即使没有 NaN 和 inf,也可能得到完全错误的值。pandas 默认给astype(int)用的是平台相关的int64,大多数场景够用,但如果你显式指定np.int32,就要多留个心眼。
一个真实案例:把毫秒级时间戳转np.int32,结果出现了负数,因为超出了表示范围。后来统一改成np.int64才正常。排查这类问题,转完后看基本统计量是个好习惯:
df["ts"].astype("int64").describe()如果 min 或 max 出现明显不符合业务常识的极值,先怀疑溢出或 NaT 残留。
5. 生产环境数据管道的工程化解法
5.1 给每一列定“类型契约”,让错误早点暴露
临时脚本里报错,修一下就行。但在生产管道里,我建议不要等到astype(int)这一步才炸,而是从入口就建立“类型契约”:明确每一列的预期 dtype、是否允许缺失、取值边界,在数据加载后立刻校验。
最轻量的做法是自定义一个断言函数:
def assert_schema(df, col, allow_nan=False, allow_inf=False): values = df[col] if not allow_nan: assert not values.isna().any(), f"{col} contains NaN" if not allow_inf: assert np.isfinite(values.dropna()).all(), f"{col} contains inf"管道开头跑一遍断言,哪个上游表出了问题,立刻就能定位到具体列,而不是等下游转 int 时才收到IntCastingNaNError。这比事后排查节省大量时间。想在更大规模的项目里做这件事,pandera 这类库会更正式,但小团队从函数断言开始就够了。
5.2 内置一个safe_int_cast兜底函数
我自己的项目里长期维护着一个万能转换函数,虽然看起来简单,但用它能统一处理绝大多数转 int 的异常情况,代码里到处抄来抄去反而容易出错:
import pandas as pd import numpy as np def safe_int_cast(series: pd.Series, fill_value=None) -> pd.Series: # 第一步:强制转数值,非数值变成NaN numeric = pd.to_numeric(series, errors="coerce") # 第二步:统一把inf替换为NaN,因为inf无法转整数 numeric = numeric.replace([np.inf, -np.inf], np.nan) if fill_value is not None: # 有具体填充值时,转成普通int64 return numeric.fillna(fill_value).astype("int64") # 不填充时,保留缺失语义,使用可空Int64 return numeric.astype("Int64")调用方式很灵活:
df["price"] = safe_int_cast(df["price"], fill_value=0) df["user_id"] = safe_int_cast(df["user_id"]) # 保留NA,转Int64这个函数融合了方案 A/C/D,覆盖了字符串、NaN、inf 三类常见问题。需要注意:如果series原本就是 datetime64 类型,pd.to_numeric可能会报错,所以日期列请单独处理。
5.3 新旧pandas版本兼容的几个注意点
最后聊一下版本兼容。我在生产环境见过不少老项目跑在 pandas 0.25 上,代码写的是astype(int),升级到 pandas 1.0 之后立刻被IntCastingNaNError打爆。这其实是个好事,因为原本的静默错误终于暴露了。
如果你需要同时兼容老版本和新版本,建议统一改用astype("Int64")和pd.to_numeric,这两种写法在 pandas 0.25 里也能正常工作。另外注意:
convert_dtypes()在 pandas 1.0 引入,2.x 更稳定,老版本没有。pd.NA在 1.0 引入,0.25 里没有,用了会报错。DataFrame.replace([np.inf, -np.inf], np.nan)是老牌写法,各版本都支持。
最好在 requirements 里固定 pandas 版本,并在 CI 里跑一个简单的 dtype 测试,确保升级时不踩雷。类型转换这种事,看起来是小问题,但一旦进入生产环境,一个非有限值可能让整张报表数据全部归零或偏移。
回到最初的IntCastingNaNError。我个人用了很多年 pandas,现在看到这个报错反而不慌了,因为它横竖在告诉我:数据里存在不该存在的非有限值。比起默默转错,它选择大声喊出来,这是 pandas 设计上的进步。每次遇到它,我会先查数据来路,再选转换方案,最后回到源头把异常值规范掉。你要是在自己的数据管道里也撞上了,按这篇文章的顺序排查一遍,基本不会再被它卡住。