文献参考:An Online Algorithm for Segmenting Time Series
一、时间序列分段优点:
时间序列分段是指将长度为n的时间序列T用K条直线来拟合。因为K通常比n小得多,这种表示方式使得数据的存储、传输和计算更加高效。具体来说,在数据挖掘中,分段算法可以:
- 支持快速精确类似搜索;
- 支持新的距离度量,包括模糊查询,加权查询,多分辨率查询,动态时间扭曲和相关性反馈等;
- 支持并行挖掘文本和时间序列;
- 支持新的聚类和分类算法; 支持改变点检测
二、分段算法总体思路
- 给定一个时间序列T,仅用K个片段产生最佳表示。
- 给定一个时间序列T,生成最佳表示,使任何段的最大误差不超过某个用户指定的阈值max_error
- 给定一个时间序列T,生成最好的表示,使所有片段的综合误差小于某个用户指定的阈值total_max_error。
并非所有算法都满足以上条件。
三、时序分割的三种主要方法
- 滑动窗口(Sliding Windows):滑动窗口算法的工作原理是在时间序列的第一个数据点上锚定一个潜在段的左点,然后试图通过增加更长的段来接近数据的右点。在某个点i,如果潜在段的误差大于用户指定的阈值,则将锚点到i-1的子序列转换为段。然后将锚点移动到位置i,重复这个过程,直到整个时间序列转化为分段。
伪代码如下:
python实现:
def Sliding_Window(T, max_error, seq_range=None):
if not seq_range:
seq_range = (0, len(T) - 1)
start = seq_range[0]
end = start
result = T
while end < seq_range[1]:
end += 1
if calculate_error(T, (start, end)) <= max_error:
result = T[start:end + 1]
else:
break
if end == seq_range[1]:
return [result]
else:
return [result] + Sliding_Window(T, max_error, (end, seq_range[1]))
这是最简单的滑动窗口方法,实际应用中可以调整窗口大小,增加变量i的“跳跃长度为k”而不是1。对于k = 15 ,算法速度快15倍,对有些数据输出影响很小。并且由于残差是累积增长的,随着数据点的增加而不减少,因此不需要每一次都逐步计算i从2到最后选择点的残差,可以一开始设定 i = s,如果计算出来的误差小于 max_error,再考虑增加 i。否则,就开始减少i,直到测量的误差小于max_error。如果段的平均长度相对于其长度的标准偏差较大,这个优化可以大大提高效率。残差的单调非减少性质也允许针对段的长度进行二分搜索。
滑动窗口在有噪声的情况下表现较好,但有些时候表现较差。
Park等人(2001)建议修改算法,创建“单调变化”的分段。也就是说每个片段的点都由
t
1
⩽
t
2
⩽
.
.
.
⩽
t
n
t_{1}\leqslant t_{2}\leqslant ...\leqslant t_{n}
t1⩽t2⩽...⩽tn或者
t
1
≥
t
2
≥
.
.
.
⩾
t
n
t_{1}\geq t_{2}\geq ...\geqslant t_{n}
t1≥t2≥...⩾tn的顺序组成。该方法在光滑的合成数据集上取得了良好的效果。但是在真实世界的有噪音数据集上,拟合结果是严重的过度碎片。
- 自上而下(Top-Down):对时间序列进行递归分割,直到满足一定的停止条件。自顶向下的算法通过考虑时间序列的每一种可能的划分,并将其分割到最佳位置。然后测试这两个子节,看它们的近似误差是否低于某个用户指定的阈值。如果没有,算法递归地继续分割子序列,直到所有分段的逼近误差都低于阈值。
伪代码如下:
python实现:
def improvement_splitting_here(T, i, seq_range):
return calculate_error(T, (seq_range[0], i)) + calculate_error(T, (i + 1, seq_range[1]))
def Top_Down(T, max_error, seq_range=None):
if not seq_range:
seq_range = (0, len(T) - 1)
best_so_far = float('inf')
break_point = float('inf')
for i in range(seq_range[0] + 1, seq_range[1]):
improvement_in_approximation = improvement_splitting_here(T, i, seq_range)
if improvement_in_approximation < best_so_far:
break_point = i
best_so_far = improvement_in_approximation
left_error = calculate_error(T, (seq_range[0], break_point))
left_seg = T[seq_range[0]:break_point + 1]
right_error = calculate_error(T, (break_point+1, seq_range[1]))
right_seg = T[break_point+1:seq_range[1]+1]
if left_error > max_error:
segleft = Top_Down(T, max_error, (seq_range[0], break_point))
else:
segleft = [left_seg]
if right_error > max_error:
segright = Top_Down(T, max_error, (break_point, seq_range[1]))
else:
segright = [right_seg]
return segleft + segright
自上而下算法应用很广。Park等人对它进行了改进,首先扫描整个数据集,标记出波峰和波谷,用这些极值点创建初步的分段,然后对每一段使用自上而下算法,以防个别段上的误差极高。这种方法在平滑数据集上表现极好,但是在现实数据中容易出现过于分散的拟合情况。
Lavrenko等人使用自顶向下的算法来支持文本和时间序列的并发挖掘。他们试图发现新闻故事对金融市场的影响。他们的算法包含了一些有趣的修改,包括一个基于t检验的新奇的停止标准。
Smyth and Ge重新表示该算法以支持基于隐马尔科夫模型的变化点检测方法和模式匹配方法。
- 自下而上( Bottom-Up:):首先创建时间序列的最佳可能拟合直线,即用n/2个片段来拟合长度为n的时间序列。然后计算合并每队相邻片段的代价,然后迭代合并代价最低的片段,直到满足停止条件。
算法伪代码如下:
python代码:
def Bottom_Up(T, max_error):
Seg_TS = []
seg = []
merge_cost = []
for i in range(0, len(T), 2):
Seg_TS += [T[i:i + 2]]
seg.append((i, i + 1))
for i in range(0, len(Seg_TS) - 1):
merge_cost.insert(i, calculate_error(T, (seg[i][0], seg[i + 1][1])))
while min(merge_cost) < max_error:
index = merge_cost.index(min(merge_cost))
Seg_TS[index] = Seg_TS[index] + Seg_TS[index + 1]
seg[index] = (seg[index][0], seg[index + 1][1])
del Seg_TS[index + 1]
del seg[index + 1]
if index > 1:
merge_cost[index - 1] = calculate_error(T, (seg[index - 1][0], seg[index][1]))
if index + 1 < len(merge_cost):
merge_cost[index] = calculate_error(T, (seg[index][0], seg[index + 1][1]))
del merge_cost[index + 1]
else:
del merge_cost[index]
return Seg_TS
一般有两种方法得到近似拟合直线——线性插值和线性回归。这两种技术如图所示。线性插值趋向于紧密地对齐连续段的端点,给分段逼近一个“平滑”的外观。相反,线性回归在一些数据集上可能产生非常脱节的外观。线性插值的美学优势和较低的计算复杂度使其成为计算机图形应用[9]的首选技术。但是,就欧氏距离而言,拟合直线的质量通常不如回归方法。我在写代码的时候采用的最小二乘法拟合直线,误差采用平方和,但在实际应用过程中,拟合方法和误差都是可以灵活调整的。
def calculate_error(st, seq_range):
x = arange(seq_range[0], seq_range[1] + 1)
y = array(st[seq_range[0]:seq_range[1] + 1])
A = ones((len(x), 2), float)
A[:, 0] = x
# 返回回归系数、残差平方和、自变量X的秩、X的奇异值
(p, residuals, ranks, s) = lstsq(A, y, rcond=None)
try:
error = residuals[0]
except IndexError:
error = 0.0
return error
四、主要分段算法特征比较
滑动窗口算法的主要问题是它不能向前看,缺少离线(批处理)算法的全局视图。自下而上的和自顶向下的方法可以产生更好的结果,但是这种方法是离线的,并且需要扫描整个数据集。这在数据挖掘上下文中是不切实际的,甚至是不可行的,因为数据的顺序是兆兆字节,或者以连续的流到达。因此,我们引入了一种新颖的方法,在此方法中,我们捕获了滑动窗口的在线特性,同时保留了自底向上的优势。我们称我们的新算法SWAB(滑动窗口和自下而上)。
SWAB算法保持一个大小为W的缓存区。初始化缓存区使得有足够的数据来创建5~6个分段。对缓存区的数据应用Bottom_Up算法,并报告最左边的段。从缓存区中删除与报告段对应的数据,然后读取更多的数据。读入数据点的数量取决于传入数据的结构。这个过程由Best_Line函数完成,该函数就是经典的Sliding_Window算法。这些点被放入缓存区,然后再次应用Bottom_Up算法。只要数据到达(可能是永远的),就会重复对缓冲区应用自底向上的过程,报告最左边的段,并读取下一个“最适合”的子序列。
Best_Line函数使用(相对较差的)滑动窗口查找与单个段对应的数据,并将其提供给缓冲区。当数据在缓冲区中移动时,(相对较好的)自底向上算法有机会细化分割,因为它有数据的“半全局”视图。当数据从缓冲区中弹出时,分割断点通常与自底向上的批处理版本所选择的断点相同。
使用缓冲区允许我们获得自底向上的数据集的“半全局”视图。但是,重要的是要在窗口大小上设置上限和下限。缓存区允许任意增长,使我们的算法可以回归到纯Bottom_Up,但是缓存区较小将使它恶化为Sliding_Window,可能出现过多的碎片。在我们的算法中,我们使用了初始缓冲区的一半和两倍作为上界和下界。
我们的算法可以看作是出于极端Sliding_Window和Bottom_Up算法之间的综合体。令人惊讶的结果(如下所示)是,通过允许缓冲区只包含通常一个分段的5或6倍的数据,该算法产生的结果与自底向上基本相同,但能够处理永不终止的数据流。我们的新算法只需要一个小的,恒定的内存量,时间复杂度是一个小的常数因子比标准的自底向上算法。