1. 空气质量数据预处理,为什么总是绕不开异常值
城市空气质量数据大概是环境数据里最"闹腾"的一种。采集设备分布在城市各个监测站点,受传感器漂移、通信故障、扬尘干扰甚至人为维护影响,拿到的原始数据几乎不可能是一张干干净净的表。我在处理某市环境监测中心的模拟项目X时,遇到的典型情况是:一列PM2.5浓度序列里,小部分时间点出现负值,个别时段突然跳到正常值几十倍,还有整段整段的缺失——这还没算时间戳错位、单位不一致这类"隐形地雷"。
做数据分析的人容易犯一个毛病:拿到数据先画图、先建模,发现结果不对才回头排查数据质量。正确的顺序恰恰相反,数据清洗应当占整个分析流程相当高的比重。空气质量数据的异常值处理尤其关键,因为后续无论是算日均值、做趋势分析,还是构建预测模型,异常值都会被算法"认真对待"。比如算一个月的平均浓度,只要混进去几个离谱的高值,均值可能被抬高到完全失真,而用中位数或分位数又可能把真实的污染过程也过滤掉——所以处理异常值不能只靠一招,得结合统计方法和业务判断。
这篇文章不打算推一套"万能清洗流水线",而是聚焦两件事:怎么用合理的方法识别空气质量数据中的异常值,以及为什么interpolate()线性插值在填充缺失和修正异常时表现得比均值填充、前向填充更稳妥。内容基于我在模拟项目X里处理某城市逐小时空气污染物浓度序列(包含PM2.5、PM10、SO₂、NO₂、CO和O₃六项指标)的实际过程,代码全部用Python和Pandas实现,没涉及什么高端框架,但每一步的取舍逻辑我会尽量讲透。
2. 先搞清楚异常值长什么样,再决定怎么处理
2.1 空气质量数据的四类"脏数据"
处理异常值之前,必须先把异常值的类型摸清楚。不同成因的异常值,处理策略完全不一样。我在项目X中归纳了四类:
- 物理性异常:浓度出现负值,或者超过该污染物的理论极值。比如PM2.5浓度-5µg/m³,SO₂浓度12000µg/m³,这类数据不用犹豫,直接判定为异常。负值通常来自仪器零点漂移或校准期间的错误测量;超大值则可能是传感器故障、扬尘干扰或数据录入单位错误(比如把mg/m³当成µg/m³)。
- 逻辑性异常:不符合物理约束或化学反应规律的组合。例如NO₂浓度很高而O₃浓度同时很高,在常规光化学反应机制下这种组合就值得警惕(尽管并非绝对不可能);再比如PM2.5浓度大于PM10浓度,这违反了粒径分布的物理逻辑,因为PM10包含了PM2.5。
- 统计性异常:数值在统计分布上表现得离谱。常见的是超出均值±3倍标准差的范围,或用箱线图法判断的离群点。这类异常可能是真实的强污染事件(如沙尘暴导致PM10骤增),也可能是传感器瞬时故障,区分它们需要结合时间上下文。
- 结构性缺失:严格来说缺失不是异常值,但处理方式与异常值强相关。表现为某个站点整点数据连续缺失、传感器维护期间的数据凹洞,或时间戳跳变导致的"幽灵间隙"。
识别阶段用Python做基础探索很方便,一张describe()加上分组统计就能看出端倪:
import pandas as pd import numpy as np # 读取模拟数据,time为索引 df = pd.read_csv('air_quality_hourly.csv', parse_dates=['time'], index_col='time') df.describe(percentiles=[0.01, 0.05, 0.25, 0.5, 0.75, 0.95, 0.99])describe()的分位数输出能快速暴露问题。如果P99和P50之间的差距大到不合理,比如PM2.5的P50是35,P99却到了800,那大概率存在高值异常;如果min出现负数,说明存在物理性异常。我在项目里还会顺手看各列缺失占比:
missing_ratio = df.isnull().mean() * 100 print(missing_ratio)PM2.5缺失3%、NO₂缺失1.5%、O₃缺失2.8%,这个水平在真实监测数据里算很温和了。真实项目中缺失率超过10%也不稀奇,关键看缺失的模式——随机缺失和连续缺失要区别对待,后者对插值的算法选择有直接影响。
2.2 三种常用的异常值识别方法
识别异常值的方法很多,但在空气质量场景下常用的就三招:阈值判断(业务硬规则)、3σ原则(统计硬规则)、IQR(箱线图法)。三者的定位完全不同:
阈值判断适合处理物理性异常,依据是《环境空气质量标准》里各污染物的浓度限值范围。比如PM2.5的24小时均值限值是75µg/m³,但单小时值偶尔冲高到几百也并非不可能(重污染过程期间PM2.5小时值可超过500µg/m³),所以不能拿日均限值当小时数据的硬上限。更稳妥的做法是用"仪器量程"作为判断依据,超出量程的才判死。项目X里我对PM2.5设置的硬阈值是0~1000µg/m³,超出范围的直接标记为NaN。
3σ原则的逻辑是假设数据近似正态分布,超过均值±3σ的点视为异常。但对空气质量数据要小心:PM2.5、PM10这类污染物呈明显的右偏分布(少数极端高值把均值拉高),直接用3σ会把本来正常的峰值污染过程误杀。我一般先对浓度取对数或者用中位数MAD(绝对中位差)的方式替代σ:
from scipy.stats import median_abs_deviation def mad_outlier_mask(s, thresh=5): med = s.median() mad = median_abs_deviation(s.dropna()) if mad == 0: return pd.Series(False, index=s.index) return (s - med).abs() > thresh * mad用MAD比标准差要稳健得多,不受极端值本身的影响,所以识别出来的才更接近"真异常"。
IQR法用Q1和Q3的间距判定离群点,上界是Q3+1.5IQR,下界是Q1-1.5IQR。这个方法对偏态分布也适用,对分布形态的假设比3σ弱得多。缺点是1.5这个系数比较敏感——对空气质量小时数据,真实高污染过程很容易被标成离群点。实战中我会把系数放宽到3,宁可多留一些可疑数据,也不要把真实事件误杀。
这三种方法各有侧重,实际处理时我秉持的原则是:物理性异常直接毙掉,统计性异常标记后结合时间连续性判断。比如某个时刻PM2.5突增到300,如果前后一两个小时也是200多,那说明是真实的污染累积过程,不是异常;如果前后都是30多突然跳到300然后跳回30,高度怀疑传感器故障或扬尘干扰,这时候再考虑修正或删除。这就是为什么异常值处理不能单靠一列数据,必须看时间上下文。
3. 为什么我优先用interpolate()线性插值,而不是均值填充或前向填充
3.1 均值填充、前向填充各自的问题
缺失值和确认的异常值,最终都要变成实际可用的数值。最朴素的做法是均值填充:用整列的平均值填进空缺。但空气质量数据是强时间序列,均值填充相当于把"时间"这个最关键的维度扔掉了。举个直观的例子:某天凌晨2点PM2.5缺失,如果用全天均值填进去,白天和夜间的浓度差异就被抹平了,后续做日变化分析时峰值、谷值都会被拉平。
前向填充(ffill)用上一个有效值填充空缺,看似保留了时间连续性,问题也明显:缺失段比较长时,填充出来的是一段水平线,浓度不随时间变化,违背了污染物扩散的基本规律。后向填充逻辑类似,不再赘述。
插值的思路完全不同:它利用缺失点前后两个有效值,通过某种函数关系估算中间点的值。线性插值假设这段区间内浓度随时间线性变化。这个假设在空气质量数据里多数情况是合理的——一两个小时的浓度变化通常不会剧烈到完全非线性;但遇到沙尘暴过境、逆温层破裂这类突变过程,线性假设也会失真,所以要控制插值的跨度。
3.2 线性插值的数学原理与Pandas实现
Pandas里interpolate()的默认方法就是linear,核心思想很简单:已知两点(x0, y0)和(x1, y1),在区间内任意点x处的插值估计为:
y = y0 + (y1 - y0) * (x - x0) / (x1 - x0)其中x值在index为DatetimeIndex时对应时间戳的数值表示。Pandas会自动把时间索引转换为内部数值参与计算,所以直接写df.interpolate()就会按时间顺序在相邻有效值之间做线性插值。
实际代码长这样:
# 先做异常值标记,统一替换成NaN df_clean = df.copy() for col in ['PM2.5', 'PM10', 'SO2', 'NO2', 'CO', 'O3']: # 硬阈值 df_clean.loc[df[col] < 0, col] = np.nan df_clean.loc[df[col] > upper_limit[col], col] = np.nan # MAD标异常 mask = mad_outlier_mask(df_clean[col], thresh=5) df_clean.loc[mask, col] = np.nan # 线性插值 df_interp = df_clean.interpolate(method='linear', limit=6, limit_direction='both')这里limit=6表示连续缺失超过6个点就不插值,避免长段缺失被强行"臆造"出来。limit_direction='both'允许在序列开头和结尾也做插值。开头结尾的数据如果缺失,默认linear插值不会处理,因为需要两个端点——但limit_direction='both'不是万能的,若序列最开头就缺失,依然没有有效值可参考,此时需要配合method='nearest'或直接删除。
3.3 为什么线性插值比均值填充"可信"
从信息利用的角度看,均值填充用的是全局统计量,把整列数据的分布特征压缩成一个常数;线性插值用的是局部邻域信息,利用的是时间上相邻两个观测点的递变趋势。空气质量数据天然存在时间自相关——t时刻的浓度与t-1小时、t-2小时显著相关,线性插值正是利用了这种自相关性,所以填充结果更接近真实值。
另一个优势是不引入分布偏差。均值填充会拉低方差(数据整体向均值收缩),后续做异常检测时,真实的高值更容易被误判为异常。而线性插值生成的中间点被夹在两个实际观测值之间,方差影响很小。
在模拟项目X里,我做过一个小验证:把某一段PM2.5数据人为挖掉20%的点,分别用均值填充和线性插值补回,对比填充值与真实值的MAE(平均绝对误差)。结果显示线性插值的MAE大约是均值填充的三分之一到二分之一。这个结果不意外,但也说明了一个道理——不要迷信某个方法的"标准答案",用数据本身去验证方法的适用性,才是可靠的做法。
4. 完整演练:从原始表到干净数据序列的处理流程
4.1 数据读入与时间索引处理
处理空气质量数据的第一个大坑,往往不在清洗本身,而在时间轴。我接到模拟项目X的数据时,第一版代码直接用默认的整数索引跑插值,结果interpolate()根本不按时间等距处理——因为Pandas不知道你的数据是等间隔采样的。
正确的做法是先读入时间列,设为索引,再检查时间间隔是否均匀:
df_raw = pd.read_csv('city_air_quality.csv') df_raw['time'] = pd.to_datetime(df_raw['time'], format='%Y-%m-%d %H:%M:%S') df_raw = df_raw.set_index('time').sort_index() # 检查时间间隔 diff = df_raw.index.to_series().diff().dropna() print(diff.value_counts())如果value_counts()里绝大多数是1小时,说明采样基本规律;如果出现2小时、23小时之类的间隔,就要警惕有整段缺失或者时间戳错乱。项目X里还发现过重复时间戳——同一小时出现两行记录,这种情况要先drop_duplicates()再处理。
4.2 数据裁剪与逐列异常标记
处理大表时,我喜欢分三步走:先归档原始数据,再生成标记列,最后统一替换。这样做的好处是可回溯——每一条被修改的数据都有据可查。
df_flag = pd.DataFrame(index=df_raw.index) for col in ['PM2.5', 'PM10', 'SO2', 'NO2', 'CO', 'O3']: flag = pd.Series('normal', index=df_raw.index, dtype='object') # 物理性异常 flag[df_raw[col] < 0] = 'neg_phys' flag[df_raw[col] > upper_limit[col]] = 'high_phys' # 统计性异常(MAD) mad_mask = mad_outlier_mask(df_raw[col], thresh=5) flag[mad_mask] = 'mad_outlier' df_flag[col + '_flag'] = flag # 替换异常值为NaN,保留原始列用于对比 df_work = df_raw.copy() for col in ['PM2.5', 'PM10', 'SO2', 'NO2', 'CO', 'O3']: mask = df_flag[col + '_flag'] != 'normal' df_work.loc[mask, col] = np.nan这一步的设计逻辑:物理性异常直接判死,统计性异常只做标记和替换,但还是会把完整的历史记录留在df_raw里。万一后续业务方觉得某个"异常"其实是真实污染事件,还可以恢复核对。
4.3 分列插值、连续缺失阈值判断与汇总输出
插值不是无脑一行interpolate(),有些细节不处理会埋雷。
连续缺失判断:limit参数可以控制连续缺失的最长插值长度,但它只作用于每个连续NaN块。我先统计缺失块长度:
for col in df_work.columns: null_run = df_work[col].isnull().astype(int).groupby( (df_work[col].isnull() != df_work[col].isnull().shift()).cumsum() ).sum() long_run = null_run[null_run > 6] if len(long_run) > 0: print(f'{col} 存在长度超过6的连续缺失块:{long_run.tolist()}')对长缺失块,limit封顶后插值不会强行填充,最终检测到的长缺失段只能删除或标记为无效。空气质量分析中,时间序列上出现3小时以上的连缺,插值结果已经不可靠,与其硬填不如跳过。
线性插值执行:
df_final = df_work.interpolate(method='linear', limit=6, limit_direction='both')插值后检查:
remaining_na = df_final.isnull().sum() print(remaining_na)汇总输出:最后我会生成一张处理报告,记录每列异常数、填充数、剩余缺失数和处理前后的基础统计量对比:
report = pd.DataFrame({ 'raw_na': df_raw.isnull().sum(), 'flag_outliers': (df_flag != 'normal').sum(), 'filled_by_interp': (df_work.isnull().sum() - df_final.isnull().sum()), 'remaining_na': df_final.isnull().sum(), 'raw_mean': df_raw.mean(), 'clean_mean': df_final.mean() }) report.to_excel('cleaning_report.xlsx')这个报告在项目交付时特别有用。业务同事看到"异常值占总数据0.8%、插值填充占0.5%"这类数字,比我解释一百遍"数据很干净"都管用。
4.4 效果怎么看:一张图说明问题
清洗完成后,强烈建议画一张处理前后的对比图。不画图你很难感知插值到底把数据改成了什么样:
import matplotlib.pyplot as plt plt.figure(figsize=(14, 5)) plt.plot(df_raw.index, df_raw['PM2.5'], label='raw', alpha=0.5, linewidth=0.8) plt.plot(df_final.index, df_final['PM2.5'], label='after interp', alpha=0.8, linewidth=0.8) plt.legend() plt.ylabel('PM2.5 (µg/m³)') plt.show()如果处理后曲线在多处出现"直上直下"的折线,说明插值跨度太大,需要缩小limit;如果曲线几乎完全重合,说明异常值本身很少;如果处理后曲线比原始曲线更平滑又没有明显的突变阶梯,说明清洗结果比较理想。
5. 时间序列插值中容易忽略的坑
5.1 索引必须是有序且等间距的DatetimeIndex
interpolate()在处理无序索引时行为很隐晦——它不会报错,但会按照当前索引顺序插值,结果可能完全不对。sort_index()之后还要确认索引没有重复项和时间间隔的一致性。模拟项目X里出现过"夏令时切换导致某天只有23小时"的情况,插值本身不受影响,但后续做日均值会偏移,记得先归一化到标准时间轴。
5.2 method='time'与method='linear'的差别
很多人不知道,Pandas里还有method='time'选项。它也是线性插值,但它会考虑时间间隔的真实长度。如果索引不是严格等间隔(比如中间有缺失导致两个有效值之间差了3小时而不是1小时),method='linear'会默认按等距处理,method='time'会把时间间隔计入权重。对空气质量数据来说,如果确认数据基本等间隔,linear足够;如果时间轴有缺失或不均匀,用method='time'更准确:
df_interp_time = df_work.interpolate(method='time', limit=6)5.3 异常值检测阈值不要一刀切
不同污染物浓度水平差异巨大。CO浓度通常0.x~几个mg/m³,而PM2.5的µg/m³数值是三位数级别。把同一套MAD阈值套用到所有列,结果必然失衡。我在项目X里针对每列单独调参:CO用MAD倍数4,PM2.5用5,O₃用6——因为O₃的日变化幅度本来就很剧烈,阈值太紧会误伤正常波动。
5.4 插值之后的"二次清洗"
插值产生的值毕竟是估算,如果原数据的异常率很高(超过5%),插值结果的可信度就要打折扣。这种情况下我先做一次插值,再把插值后依然明显不合理的数据做二次检查。项目X的CO列有个极端情况:原始数据里近8%的值异常,插值后虽然看起来连续,但多个插值点拼出了一段"人为的平缓曲线",和真实浓度过程差距很大。后来我把该列的异常判据改用滑动窗口识别:
def rolling_outlier_mask(s, window=6, n_sigma=4): rolling_mean = s.rolling(window, center=True, min_periods=3).mean() rolling_std = s.rolling(window, center=True, min_periods=3).std() return (s - rolling_mean).abs() > n_sigma * rolling_std滑动窗口的思路更贴合空气质量数据的特征:局部平稳、全局波动。比全局3σ更能保住真实的污染过程。
6. 一个小场景:traffic流量序列对插值的启发
项目X处理完空气质量数据后,我顺手用同一套逻辑处理了一份某路段的交通流量时序数据(模拟数据)。本来想看看通用性怎么样,结果发现了一个有趣的对比。
交通流量数据比空气质量数据的周期性更强——早晚高峰规律极其明显,工作日和周末形态完全不同。用普通的线性插值填充缺失值,在工作日早上7点到9点之间的缺失段,插值结果把高峰"填平"了一部分,因为线性假设在流量陡升阶段失效。后来我把插值方法换成了method='quadratic'(二次插值),效果好了不少,因为二次曲线能捕捉到一定的曲率变化。
对比之下,空气质量数据的日变化虽然也有规律(比如O₃白天高夜间低、PM2.5早晚双峰),但总体上没有交通流量那么"锋利",线性插值在大部分场景下已经够用。这个对比给我的启示是:插值方法没有绝对的最优,要做决定先看数据形态。对季节性强的数据,可以试试spline(样条插值),但要注意过拟合问题,样条阶数太高容易在缺失段产生震荡。
# 交通流量的试验代码,仅示意 traffic_filled = traffic_df.interpolate(method='quadratic', limit=4)quadratic有个小坑:如果连续缺失段两侧的数值差异太大,二次插值可能在中间产生超出两侧端点范围的overshoot。实际使用时要留意插值结果有没有出现离谱的峰值,一旦出现就要降低阶数或者改回线性。
7. 用仿真数据验证插值效果的实验设计
前面提到过,判断一种填充方法行不行,最有力的证据是仿真实验。这个方法我在项目X中用得很频繁:拿一段完整的真实数据,人工制造缺失,再对比不同填充方法与真实值的偏差。
具体做法分四步。
第一步:准备"干净"样本。从df_final中挑出一段连续无缺失、且无明显异常波动的序列,比如连续7天的PM10小时数据,共168个点。用这段数据当"真值"。
第二步:人工植入缺失模式。随机选30个位置设为NaN,或者模拟"连续缺失4小时"的块状缺失。为了排除偶然性,随机缺失重复做10次,每次重新采样。
第三步:应用不同填充方法。至少对比三种:线性插值(interpolate(method='linear'))、均值填充、前向填充。想更全面再加method='time'和quadratic。
第四步:计算误差指标。对每个缺失点计算填充值与真实值的绝对误差,汇总为MAE和RMSE。10次模拟取平均,结果更稳健:
from sklearn.metrics import mean_absolute_error, mean_squared_error def simulate_interp(s, missing_ratio=0.15, method='linear'): errors = [] for seed in range(10): rng = np.random.default_rng(seed) s_missing = s.copy() n_missing = int(len(s) * missing_ratio) idx_missing = rng.choice(s.index, size=n_missing, replace=False) s_missing.loc[idx_missing] = np.nan filled = s_missing.interpolate(method=method, limit=6) true_vals = s.loc[idx_missing] pred_vals = filled.loc[idx_missing] errors.append(mean_absolute_error(true_vals, pred_vals)) return np.mean(errors) for method in ['linear', 'time', 'quadratic', 'nearest']: mae = simulate_interp(df_final['PM10'], method=method) print(f'{method}: MAE = {mae:.2f} µg/m³')我在项目X里的实测结果是:linear的MAE约12.4µg/m³,quadratic约11.8µg/m³,nearest约18.1µg/m³,均值填充约30+µg/m³。差异非常明显。这类实验做完,你再面对"该用哪种插值"这个问题时,就不再是拍脑袋,而是有一套可复现的判断依据。
8. 多时间尺度与多站点数据下的处理策略调整
8.1 日均值与小时值的插值策略差异
如果数据不是逐小时而是逐日,插值策略要单独考量。日均值序列的波动比小时值平滑得多,线性插值的适用性更强,但日均值数据里一个缺失点代表的是一整天的信息,简单的线性插值会把工作日/周末差异平滑掉。对日均数据,我会考虑加入星期特征做分层插值——工作日和周末分别插值。Pandas没有直接支持这个,需要手动拆分:
df_daily['is_weekend'] = df_daily.index.dayofweek >= 5 df_daily['filled'] = np.nan for is_weekend, grp in df_daily.groupby('is_weekend'): grp_work = grp['value'].copy() df_daily.loc[grp.index, 'filled'] = grp_work.interpolate(method='linear', limit=2)这个细节很小,但能显著减少在节假日前后出现的插值偏差。当然前提是历史数据量足够,能分出工作日和周末的形态。
8.2 多个监测站点的协同插值
一个城市往往有十几个国控站点。单站插值只用了本站的信息,但如果相邻站点的浓度趋势相近,可以用多站信息辅助填充。最简单的做法是:A站缺失时,用B站同一时刻的浓度变化比例来估算。这个方法叫比例插值(ratio-based interpolation),逻辑是:如果B站在t时刻浓度比t-1时刻涨了10%,假设A站也涨了10%(同时段污染过程空间相关),那么A站缺失值≈A站t-1值×1.1。
实际代码实现要复杂一些,得先做站点间相关性分析,只挑相关系数超过0.8的站点做辅助。这个思路扩展性强,对区域污染过程的数据修复尤其有效。但如果缺失面积太大(整个城市所有站点同时缺),协同插值也无能为力,这时只能考虑用气象数据(风场、边界层高度)做机理模型估算,那已经超出Pandas能解决的范围了。
8.3 数据量不足时的"宁缺毋填"原则
当缺失数据比例过高时,插值出来的数据本身可能引入系统性偏差。我在模拟项目X中设定了一个底线:单列缺失率超过20%,放弃插值,直接对该列做剔除或单独标记。因为缺失率这么高的情况下,插值填出来的值会让模型误以为数据完整,实际却是估算值在主导统计结果,这比明确标记"此处缺失"更危险。
特别是做后续的机器学习建模时,如果训练集里大部分"真实值"其实是插值来的,模型的泛化能力会被严重高估。更好的做法是:把这些高缺失列作为特征保留,但给每个样本加一个"是否插值"的权重列,在模型训练时降权处理。或者在特征工程时直接把缺失率纳为特征,缺失严重的时间段本身就值得关注——在空气质量分析里,"数据缺失"有时恰恰是设备故障或极端天气的线索,这里的信息不比浓度值少。
9. 对interpolate()整套流程的实操总结
做完整套数据清洗流程下来,我个人最想强调的不是代码本身,而是把每一步决策都记录下来。空气质量数据的预处理往往要反复迭代,今天我用了MAD+3σ的组合,明天业务方可能反馈"某次沙尘暴数据不该被当成异常",那我就要调整阈值重新跑一遍。没有记录的话,过一个月想复现当时的处理逻辑,几乎是不现实的。
几个实操心得供参考:
第一,异常值处理宁可保守,不要激进。把真实污染事件误删,比留几个可疑点对分析结果的影响更大。大不了在后续建模时降低异常点权重,但删了就永远找不回来。所以统计性异常先用MAD标记,再用时间上下文二次确认,不到万不得已不直接删除整行。
第二,插值参数(limit)的设置要结合业务。对逐小时空气质量数据,limit默认我设为6小时(6个点),因为气象条件在6小时内通常相对稳定;超过6小时的缺口,线性插值就开始变得不可靠了,不如留白。如果分析对时间连续性要求极高,再考虑把limit放宽到12,但要在报告里标注这些数据为"低置信度插值"。
第三,千万别忽视时间索引的清洗。项目的真实经历是:因为时间戳格式不一致(有的行是2024-05-01 00:00:00,有的行是2024/5/1 0:00),导致pd.to_datetime部分解析失败,那列的缺失值瞬间多了十几条,差点被当成正常缺失处理掉。检查时间索引永远放在数据清洗的第一步。
第四,插值不是终点。插值完成后,把填充过的位置打上标记,后续在统计分析、建模甚至出图时,需要的话可以对填充数据做敏感性分析——比如分别用"含填充数据"和"仅原始数据"跑一遍模型,看看结论是否一致。如果结论差异巨大,那说明分析对填充值的依赖过强,数据质量本身才是应该优先解决的问题。
回到标题里的那句"interpolate()线性插值",其实它只是整个空气质量数据预处理链条中的一个环节。真正让数据变干净的是它背后的一整套思路:识别异常要有业务和统计双重依据,填充缺失要优先利用时间自相关的信息,验证效果要回落到可量化的误差指标上。把这些想清楚了,代码写起来反而很简单——一页多一点的Pandas操作就能跑完。
最后分享一个我自己反复用的技巧:处理任何一批数据,我都会在项目的根目录创建一个preprocessing_log.md,记下每次清洗用了什么方法、改了哪些数据量、为什么这样改。这个习惯看起来不起眼,但当我三个月后再拿到同一批数据的不同版本,或者同事跑来问"PM2.5那列哪些点是后来填的"时,我能直接翻记录回答,不用对着代码猜半天。数据清洗这件事,一半靠方法,一半靠可追溯性——方法决定你处理得好不好,记录决定你能不能让这些处理经得起推敲。