3-1 缺失值的化验单:探测与处置 本节摘要:isna族负责探测缺失,dropna负责丢弃,fillna与interpolate负责补值。本节的核心不是函数参数,而是先判语义再选策略:真缺失、未采集、不适用三种缺失对应三种完全不同的医嘱。 承接第02章结尾的伏笔:布尔条件静默丢弃缺失行。内科第一步是把这些躲过筛选的缺失行正式揪出来,开一张化验单。 先出化验单 三张化验单各有用途:逐格探测用于定位,列占比用于决策(占比超三成的列补值意义已存疑),info用于总览。 NaN的血型知识 💡 关键直觉:判断缺失只有isna一个入口。所有拿NaN去比较的写法都是静默的空结果,不报错、不出数,是最阴险的一类bug。
本节摘要:isna族负责探测缺失,dropna负责丢弃,fillna与interpolate负责补值。本节的核心不是函数参数,而是先判语义再选策略:真缺失、未采集、不适用三种缺失对应三种完全不同的医嘱。
承接第02章结尾的伏笔:布尔条件静默丢弃缺失行。内科第一步是把这些躲过筛选的缺失行正式揪出来,开一张化验单。
import pandas as pd import numpy as np df = pd.DataFrame({ '门店': ['A', 'B', 'A', 'C', 'A'], '金额': [1280, np.nan, 1732, np.nan, 880], '会员': ['M23', None, 'M08', 'M23', None], }) # 口径一:逐格探测 print(df.isna()) # 门店 金额 会员 # 0 False False False # 1 False True True # 2 False False False # 3 False True False # 4 False False True # 口径二:每列缺失数与占比 print(df.isna().sum()) # 门店 0 # 金额 2 # 会员 2 print((df.isna().mean() * 100).round(1)) # 门店 0.0 # 金额 40.0 # 会员 40.0 # 口径三:info一把梭,规模与稀疏度同看 df.info() # <class 'pandas.core.frame.DataFrame'> # RangeIndex: 5 entries, 0 to 4 # Data columns (total 3 columns): # # Column Non-Null Count Dtype # 0 门店 5 non-null object # 1 金额 3 non-null float64 # 2 会员 3 non-null object
三张化验单各有用途:逐格探测用于定位,列占比用于决策(占比超三成的列补值意义已存疑),info用于总览。
print(np.nan == np.nan) # False!NaN不等于任何值包括自己 print(pd.isna(None)) # True:None在Pandas里被认作缺失 print(pd.Series([1, None]).dtype) # float64,整数列沾缺失即升格浮点 # 由此推论:== np.nan 永远筛不出缺失,必须用 isna # df[df['金额'] == np.nan] 永远是空表 # df[df['金额'].isna()] 才是对的
💡 关键直觉:判断缺失只有isna一个入口。所有拿NaN去比较的写法都是静默的空结果,不报错、不出数,是最阴险的一类bug。
# 旋钮一 axis:按行丢还是按列丢 print(df.dropna(axis=0).shape) # 缺失行全丢 (1, 3) print(df.dropna(axis=1).shape) # 含缺失的列全丢 (5, 1) # 旋钮二 how:any有一颗就丢,all整行烂透才丢 print(df.dropna(how='all').shape) # (5, 3) 没有全空行 print(df.dropna(subset=['金额']).shape) # (3, 3) 只盯金额列 # 旋钮三 thresh:至少保留几个有效值 print(df.dropna(thresh=2).shape) # (3, 3) 至少2列有效的行留下
subset是最常用也最容易被忽略的旋钮:会员缺失很正常(散客),金额缺失才致命。判重的标准应该盯住关键列,而不是整行。
# 策略一:固定值与统计量 filled = df['金额'].fillna(df['金额'].median()) print(filled) # 0 1280.0 # 1 1080.0 # 2 1732.0 # 3 1080.0 # 4 880.0 # 策略二:前后值填充,适合有序数据(时间序列详见第07章) ts = pd.Series([100, None, None, 130], index=['1月', '2月', '3月', '4月']) print(ts.ffill()) # 1月 100.0 # 2月 100.0 # 3月 100.0 # 4月 130.0 # 策略三:插值,按位置线性估计 print(ts.interpolate()) # 1月 100.0 # 2月 110.0 # 3月 120.0 # 4月 130.0
选哪种?我的经验法则:类别列用众数或"未知"占位,数值列看有没有顺序——有顺序用插值或ffill,没顺序用中位数;均值要慎用,离群值会把均值带偏(离群值的判定是4-2的事)。

补值策略最容易摇摆的就是这两个。判断依据是数据的"顺序里有没有信息":每日销量这种均匀时间序列,中间缺一天,插值按两侧水平线性估计是合理的;累计指标(库存余额、会员总数)只会继承不会凭空算,ffill才是对的;而散客的交易记录之间没有顺序含义,任何补值都是编造,保留NaN让聚合自动跳过才是诚实的做法。再补一个实战细节:插值默认只对"中间的洞"生效,序列开头的缺失插不出来(前面没有参照),那类洞要么回源补采,要么明确标记后保留。
ts = pd.Series([None, 110, None, 130, None], index=range(5)) print(ts.interpolate(limit_area='inside')) # 0 NaN 开头的洞插不了 # 1 110.0 # 2 120.0 # 3 130.0 # 4 NaN
把本节器械串成流程:探测、定语义、选策略、验结果。
report = pd.DataFrame({ '门店': ['A', 'B', 'A', 'C'], '库存': [120, None, 135, None], # 存量指标 '销量': [30, 25, None, 40], # 流量指标 }) report['库存'] = report['库存'].ffill() # 存量:前值继承 report['销量'] = report['销量'].fillna(0) # 流量:当天无单记零 print(report.isna().sum().sum()) # 0,处置闭环
同一家店的两列用了两种策略——策略跟着指标语义走,不跟着列走。这张处置单的思路可以原样搬进任何真实业务表。
下一节处理内科的第二类基础病:重复记录。