4. 缺失值处理策略与实战
农业监测数据在采集、传输、入库的每个环节都可能产生缺失:传感器断电导致整段空白、无线网关丢包造成零星缺测、人工巡检漏记形成不规则空洞,甚至设备故障后连续多日无数据。处理缺失值的第一步不是急着删或填,而是先判断“缺在哪里、为什么缺、缺了多少”。不同的缺失模式对应完全不同的处理策略,选错方法比不处理更危险。
一、先识别缺失模式,再决定处理方式
从业务角度看,农业监测数据的缺失大致分为三类。第一类是随机点缺失,例如某块田的土壤湿度传感器偶尔丢一两个时间点,缺失与数值本身无关,删除或简单插值都不会引入明显偏差。第二类是连续段缺失,如暴雨导致网关离线三天,这段时间的温湿度全部为空,若用前后向填充会把三天的变化压成一条水平线,严重扭曲真实过程。第三类是系统性缺失,例如某型号传感器在低温下集体失效,缺失本身携带了“低温”这一信息,直接删除会丢掉重要线索。
在 Pandas 中,先用 isna() 配合聚合快速摸清缺失分布,而不是只看总体缺失率:
import pandas as pd
df = pd.read_csv("field_sensors.csv", parse_dates=["timestamp"])
df = df.set_index("timestamp")
# 每个字段的缺失数量与比例
missing = df.isna().sum().to_frame("缺失数")
missing["缺失率"] = (missing["缺失数"] / len(df)).round(3)
# 按天统计缺失,识别连续段
daily_missing = df["soil_moisture"].isna().resample("D").sum()
如果某字段缺失率超过 40%,且集中在特定时段,就要优先怀疑设备或传输问题,而不是急于填充。此时正确的动作是回到数据源头核查,填充只是补救手段。
二、删除策略:什么时候删、删多少
删除分为按行删除和按列删除。dropna() 的 how 与 thresh 参数决定了删除的严格程度。对于多字段监测表,若某行只有空气温度为空而其他字段完整,直接整行删除会浪费大量有效观测。更合理的做法是对关键字段设置阈值,例如要求一条记录至少包含温度、湿度、光照三项中的两项:
# 保留至少 2 个非空观测的行
df_kept = df.dropna(thresh=2)
# 仅删除关键字段缺失的行
df_kept = df.dropna(subset=["air_temp", "soil_moisture"])
按列删除则适用于长期大面积缺失的字段。判断依据不是单一比例,而是“该字段是否仍能支撑后续分析”。若某字段缺失率 60% 但缺失集中在冬季非生长季,生长季数据完整,就不应删除整列,而应在分析时按季节切片。删除的本质是牺牲样本量换取可靠性,必须结合业务时间窗口来判断。
三、填充方法:从统计量到时间序列
填充方法的选择取决于数据的时序特性和业务含义。均值填充适合分布近似对称、无明显趋势的指标,计算简单但会压缩方差;中位数填充对异常值更稳健,适合受极端天气影响的降雨量、风速等偏态数据。两者都属于“全局统计量填充”,不区分时间位置,容易在连续缺失段制造虚假的平稳。
前后向填充利用时间相邻性,ffill() 用前一个有效值向后传播,bfill() 反之。它适合短时间缺测(如单点丢包),但对连续段缺失会产生“阶梯状”假数据。实际使用中应限制填充跨度,避免无限传播:
# 仅向前填充最多 2 个时间步,超出仍保留 NaN
df["air_temp_ffill"] = df["air_temp"].ffill(limit=2)
# 线性插值,按时间索引等距插补
df["soil_moisture_interp"] = df["soil_moisture"].interpolate(method="time")
插值法比前后向填充更平滑,method="time" 会依据时间戳的实际间隔加权,适合采样间隔不完全规则的数据。对于温度这类连续变化、惯性较强的变量,线性插值在短缺口内表现良好;但对于土壤湿度在灌溉前后的突变,线性插值会抹平跳变,此时应结合灌溉记录做事件驱动填充,而非纯数学插补。
四、基于业务规则的填充案例
纯统计填充的最大问题是“数学上合理、农学上错误”。下面以一个温室监测场景为例:某温室记录空气温度、土壤湿度、灌溉事件三个字段,其中土壤湿度在灌溉后应明显上升。若某段土壤湿度缺失恰好跨越一次灌溉,用均值或线性插值都会得到“灌溉后湿度不变”的错误结论。
合理的做法是把业务规则编码进填充逻辑。例如:若缺失时段内存在灌溉事件,则用灌溉前最后一次有效值加上经验增幅(示例假设为 8 个百分点)作为填充值;若无灌溉事件,则用前后有效值的线性插值。示例代码如下,参数为教学假设,实际应依据本地标定调整:
# 标记灌溉事件(示例:irrigation_flag 为 1 表示当日灌溉)
df["irrigated"] = df["irrigation_flag"].fillna(0).astype(bool)
# 先做线性插值作为基础填充
df["sm_filled"] = df["soil_moisture"].interpolate(method="time", limit=6)
# 对跨越灌溉日的缺失,用灌溉前值 + 经验增幅修正
irrigation_days = df.index[df["irrigated"]]
for day in irrigation_days:
window = df.loc[day:day + pd.Timedelta(days=1)]
mask = window["soil_moisture"].isna()
if mask.any():
prev_valid = df.loc[:day, "soil_moisture"].dropna()
if not prev_valid.empty:
base = prev_valid.iloc[-1]
df.loc[window.index[mask], "sm_filled"] = base + 8
填充完成后必须做结果校验,而不是直接进入下游分析。校验至少包括三点:填充值是否落在物理合理区间(如土壤湿度 0~100%)、填充段与相邻有效值的跳变是否过大、填充比例是否在可接受范围内。建议新增一列 sm_source 标记数据来源(原始/插值/规则填充),让后续分析能够区分真实观测与补全值,避免把填充数据当作实测数据使用。
总结来看,缺失值处理不是一套固定公式,而是“模式识别—策略选择—业务校验”的闭环。随机点缺失可以放心插值,连续段缺失要警惕虚假平滑,系统性缺失则应保留其信息价值。把业务规则写进填充逻辑,并始终保留来源标记,才能让清洗后的农业监测数据既完整又可信。