3、数据预处理:缺失值处理(向前填充、插值)、异常值检测与处理、时间序列重采样

大家好,我是你们的老朋友。今天咱们聊聊数据预处理。

说实话,做时间序列分析,百分之六十的精力都花在数据清洗上。这不是夸张。我见过太多人,模型调得飞起,结果数据本身就有问题,最后全白干。所以,这一章咱们把地基打牢。

3.1 缺失值处理:别让空值毁了你的模型

时间序列数据里,缺失值太常见了。传感器坏了、网络断了、人为漏记……原因五花八门。但处理方式,就那么几种。

3.1.1 向前填充(Forward Fill)

我个人习惯,在金融数据里用得最多。比如股票价格,某一天停牌了,用前一天的收盘价填充,逻辑上说得通。

说白了,就是用上一个有效值,填补后面的空缺。

import pandas as pd

# 示例数据
data = {'date': pd.date_range('2024-01-01', periods=5, freq='D'),
        'value': [100, None, None, 103, 105]}
df = pd.DataFrame(data)
df.set_index('date', inplace=True)

# 向前填充
df['value_ffill'] = df['value'].ffill()
print(df)
小提示:向前填充适合数据变化缓慢的场景。如果数据波动大,慎用。

3.1.2 插值法(Interpolation)

插值就灵活多了。它假设数据点之间存在某种数学关系,然后估算出缺失值。

我在项目中遇到过,温度传感器每隔10分钟采集一次数据,中间偶尔丢几个点。用线性插值,效果就很好。

# 线性插值
df['value_interp'] = df['value'].interpolate(method='linear')
print(df)

除了线性插值,还有多项式插值、样条插值。你想想看,如果数据有明显的周期性,用时间插值(method='time')会更合理。

注意:插值不是万能的。如果连续缺失太多,插出来的值可能完全失真。我曾经吃过这个亏,后来学乖了——连续缺失超过3个点,我就改用其他方法。

3.2 异常值检测与处理:揪出那些“捣蛋鬼”

异常值,就是那些明显偏离正常范围的数据点。比如,某天的销售额突然暴增100倍,结果发现是系统bug。

怎么揪出它们?我常用的方法有几种。

3.2.1 基于统计的方法

最简单粗暴的,就是3σ原则。数据服从正态分布的话,超过均值±3倍标准差的值,就是异常。

import numpy as np

# 生成数据
np.random.seed(42)
data = np.random.normal(100, 10, 100)
data[50] = 200  # 人为加入异常值

# 3σ检测
mean = np.mean(data)
std = np.std(data)
threshold = 3 * std
outliers = data[np.abs(data - mean) > threshold]
print(f"检测到 {len(outliers)} 个异常值")

但说实话,这个方法对非正态分布的数据不太友好。我建议用四分位距法(IQR),更稳健。

3.2.2 基于移动窗口的方法

时间序列有连续性。如果某个点的值,跟它前后几个点的平均值差太多,那大概率是异常。

# 移动窗口检测
window = 5
rolling_mean = df['value'].rolling(window=window).mean()
rolling_std = df['value'].rolling(window=window).std()
df['is_outlier'] = np.abs(df['value'] - rolling_mean) > 2 * rolling_std
核心思路:异常值检测没有银弹。多试几种方法,交叉验证,才能找到真正的“捣蛋鬼”。

3.2.3 异常值处理

找到异常值之后,怎么处理?

  • 删除:如果异常值很少,直接删掉。但要注意,别把有效信息也删了。
  • 替换:用前后值的均值、中位数替换。我一般用中位数,因为它对极端值不敏感。
  • 插值:把异常值当成缺失值,用插值法处理。

我曾经在一个电商项目中,发现某天的订单量异常高。查了半天,原来是促销活动导致的。这种“异常”其实是有效信息,不能随便处理。嗯,这里要注意,业务理解比技术更重要。

3.3 时间序列重采样:改变数据的时间粒度

重采样,就是把数据从一个时间频率,转换到另一个频率。比如,把分钟级数据聚合成小时级数据。

3.3.1 降采样(Downsampling)

从高频到低频。比如,把每天的销售数据,汇总成每周的。

# 创建日频数据
date_rng = pd.date_range(start='2024-01-01', end='2024-01-31', freq='D')
df_daily = pd.DataFrame(date_rng, columns=['date'])
df_daily['sales'] = np.random.randint(100, 200, size=(len(date_rng)))
df_daily.set_index('date', inplace=True)

# 降采样为周频,取平均值
weekly = df_daily.resample('W').mean()
print(weekly.head())

降采样时,聚合函数很关键。求和、平均、最大值……选哪个?看你的业务需求。我习惯先画个图,看看数据分布,再决定。

3.3.2 升采样(Upsampling)

从低频到高频。比如,把季度数据转换成月度数据。这就会引入缺失值,需要配合填充方法。

# 升采样为日频,向前填充
daily_upsampled = weekly.resample('D').ffill()
print(daily_upsampled.head())
经验之谈:升采样会“创造”数据,一定要谨慎。我一般只用它做可视化,不会用它做模型训练。

3.3.3 重采样的坑

重采样时,时间索引必须是datetime类型。另外,注意时区问题。我有个朋友,因为没处理时区,结果数据对不上,排查了一整天。

频率代码 含义 示例
D 日历日 每日数据
W 每周 每周一的数据
M 每月最后一天 月度汇总
Q 每季度最后一天 季度数据
H 每小时 小时级数据

好了,这一章的内容就到这里。数据预处理是脏活累活,但也是最有价值的一步。你想想看,数据干净了,后面的分析才能顺风顺水。

下一章,咱们聊聊时间序列的平稳性检验。到时候见。

课后思考:如果数据中存在周期性缺失(比如每周六都不采集数据),向前填充还合适吗?为什么?

公众号:蓝海资料掘金营,微信deep3321