数据预处理:给时序数据“洗个澡”

做安全分析这么多年,我有个很深的体会:数据预处理往往比模型本身更磨人。说句实在话,原始的时间序列就像刚挖出来的矿石,杂质多得吓人。你要是不花点功夫清理,后面再好的算法也白搭。

今天咱们就聊聊这块 - 如何给Secrets的访问时序数据做预处理。说白了,就是从脏数据里提炼出有价值的信息。

核心要点速览:
  • 缺失值处理:不能简单删除,需要根据场景选择填充策略
  • 异常点筛除:区分噪声和真实的异常访问事件
  • 重采样:统一时间粒度,让数据可比较
  • 插值方法:补全数据但不引入过多偏差

缺失值处理 - 别让空值带偏了你

我记得第一次实战分析时,直接跳过了缺失值检查。结果模型训练出来,预测全在凌晨三四点跳变。查了半天才发现,夜间的数据本来就有大量缺失。

缺失值是怎么产生的?我总结了几种常见原因:

  • 服务暂停:Secrets管理服务重启或维护,期间没有日志产生
  • 网络问题:日志传输丢包,导致部分时间点数据丢失
  • 采样策略:某些系统只在状态变化时记录,空闲时段就是空值

处理策略上,我个人习惯这么选:

场景 推荐方法 说明
短缺口(1-3个点) 线性插值 简单有效,不会剧烈改变趋势
长缺口(>10个点) 前向填充/后向填充 用最近的正常值填补,不会引入虚假波动
周期性缺失 季节分解插值 利用历史同时间段的值做加权平均
随机缺失 KNN插值 找相似时间点的值做平均,效果好但计算量大
我的避坑指南:不要贸然删除带缺失值的样本。我曾经这么干过,结果把凌晨3点的异常扫描事件全删了。为什么?因为攻击者最喜欢挑监控薄弱的时段下手。缺失本身就是信息。

异常点筛除 - 小心别把宝贝当垃圾扔了

异常点筛除是最容易翻车的地方。你想想看,我们的目标就是检测异常访问行为,结果你先把异常点筛掉了,那还检测什么?

所以这里要分清楚两类“异常”:

  • 噪声异常:传感器故障、日志错乱导致的数据毛刺。这类要剔除。
  • 事件异常:真实的异常访问行为。这类我们要保留,甚至需要标注出来。

我常用的筛除方法是3σ原则和四分位距法。但光靠统计不行,得结合领域知识。举个例子:

import numpy as np
import pandas as pd

def clean_noise(series, method='iqr', threshold=1.5):
    """
    筛除噪声异常,保留事件异常
    
    个人经验:阈值设1.5倍IQR通常够用了
    如果数据本身波动较大,可以放宽到3倍
    """
    if method == 'iqr':
        Q1 = series.quantile(0.25)
        Q3 = series.quantile(0.75)
        IQR = Q3 - Q1
        lower = Q1 - threshold * IQR
        upper = Q3 + threshold * IQR
        
        # 标记哪些是噪声(不要直接删除,先标记)
        noise_mask = (series < lower) | (series > upper)
        return series.mask(noise_mask)  # 异常点置为NaN,后续插值处理
    else:
        mean = series.mean()
        std = series.std()
        z_scores = np.abs((series - mean) / std)
        noise_mask = z_scores > 3
        return series.mask(noise_mask)
注意:千万别在这步就把真实异常给抹掉了。我建议的做法是:先做一次轻度的噪声过滤,剩下的“异常”保留给后续分析。你永远不知道,那个被筛掉的点是不是某次入侵的关键线索。

重采样与插值 - 把时间对齐了再说

实际生产环境里,不同服务的时间粒度差别很大。有的每秒记录一次,有的十分钟才记一笔。如果不做重采样,你根本没法做关联分析。

选择重采样的频率,我一般遵循这个原则:

  • 以数据最稀疏的那个服务为基准
  • 至少保留统计意义上的有效样本数
  • 考虑业务峰值时段的细粒度需求

常用方法对比:

方法 适用场景 缺点
降采样+聚合 高频数据转低频 可能丢失瞬间波动
升采样+插值 低频数据补全为高频 引入人为构造的数据点
对齐重采样 多源数据时间轴对齐 需要选择合适的基准频率

插值方法这块,我踩过不少坑。一开始图省事用线性插值,结果在拐点处误差特别大。后来改用三次样条插值,效果好很多,但计算量也上来了。

def smart_resample(df, freq='5min', method='linear'):
    """
    智能重采样,自动处理缺失时段
    
    :param df: DataFrame, index必须是DateTimeIndex
    :param freq: 目标频率, 如'5min', '1H'
    :param method: 插值方法: 'linear','spline','polynomial'
    """
    # 先重采样(聚合)
    resampled = df.resample(freq).agg({
        'access_count': 'sum',
        'unique_users': 'nunique',
        'error_count': 'mean'
    })
    
    # 再插值(填充空值)
    if method == 'linear':
        return resampled.interpolate(method='linear')
    elif method == 'spline':
        return resampled.interpolate(method='spline', order=3)
    elif method == 'polynomial':
        return resampled.interpolate(method='polynomial', order=5)
小窍门:我一般先做可视化,看数据分布再做决定。如果数据趋势平缓,线性插值足够了。如果波动剧烈,考虑用向前填充,至少不会引入虚假的尖峰。

完整的预处理流程

说了这么多,我画了张流程图把整个过程串起来。这张图是我自己项目中一直用的框架,经过不少次迭代才打磨成现在这样。

时序数据预处理流程 时序数据预处理完整流程 原始时序数据 步骤1:缺失值处理 检测 → 分类 → 选择填充策略 步骤2:异常点筛除 区分噪声 vs 真实异常事件 步骤3:重采样 统一时间粒度对齐多源数据 步骤4:插值补全 线性/样条/前向填充选择 清洗后时序数据 关键决策点: • 短缺口 → 线性插值 • 长缺口 → 前向填充 • 周期性缺失 → 分解插值 • 随机缺失 → KNN插值 异常点分类: • 噪声异常 → 筛除 • 事件异常 → 保留标注 • 判断依据:领域知识+统计 插值方法选择: • 线性: 平滑趋势 • 样条: 波动数据 • 前向: 保守填充
总结一下:数据预处理没有万金油方案。我把这个框架当作起点,每次实战都会根据数据特点做调整。核心思路就是:别丢信息,别引入噪音,保留真实异常。后面几章我们会用这些清洗过的数据做真正的异常检测分析。