【问题标题】:Finding similar sub-sequences in a time series?在时间序列中找到相似的子序列?
【发布时间】:2019-10-14 23:54:12
【问题描述】:

我有数千个时间序列(24 维数据——一天中的每个小时都有 1 个维度)。在这些时间序列中,我对如下所示的特定子序列或模式感兴趣:

我对类似于突出显示部分的整体形状的子序列感兴趣 - 即,具有急剧负斜率的子序列,然后是几个小时的时间段,其中斜率相对平坦,然后才最终以急剧的正斜率结束。我知道我感兴趣的子序列不会完全匹配,并且很可能会及时移动,缩放不同,有更长/更短的斜率相对平坦的周期等,但我想找到一种检测它们的方法。

为此,我开发了一个简单的启发式(基于我对突出显示部分的定义)来快速找到一些感兴趣的子序列。但是,我想知道是否有一种更优雅的方式(在 Python 中)来搜索我感兴趣的子序列的数千个时间序列(同时考虑到上面提到的事情——时间、规模等方面的差异)。 )?

【问题讨论】:

  • 可以做到,但如果您更清楚地说明问题定义会有所帮助:(1)什么是“尖锐”? (2) 它们是否“同样”尖锐(相同的斜率幅度)? (3)最小的扁平线长度,还是没关系? (4) 每个斜率的最小上升/下降时间,还是无关紧要?
  • @OverLordGoldDragon 会的。 (1) 我很紧张在“尖锐”这个词上加上绝对数字,但我们只能说这是从峰值到某个最小值的重大变化(1 小时内 +/- 3 个垂直单位的变化——我的数据从 0 标准化到 1)(2)同样尖锐并不重要,只需要满足在步骤(1)中设置的最小阈值(3)最小平线长度为 4 小时,最长为 6 小时(4)每个斜坡的最小上升/下降时间 - 没关系。希望这有助于澄清一些事情。
  • 很好的说明;一切都是公平的,但需要更多关于 (4) 的内容:确切的应用是什么?对于类似信号的时间序列,将 (4) 设置为 free 可能会产生大量误报 - 但由于您的时间范围以小时为单位,因此可能没问题。
  • 每天是一个单独的系列,还是将它们连接成几周、几个月等。它们是列表、文件中的一行、许多文件等?您是否要使用不同的斜率、最小/最大平面等进行多次分析?一些示例数据可能会有所帮助。
  • @OverLordGoldDragon 我认为对于我的应用程序,我可以得到一些误报。上升/下降时间可以是 1 小时(如上图)或更长(2-3 小时)。

标签: python time-series sequence heuristics


【解决方案1】:

编辑:一年后,我无法相信我将平线和斜率检测变得多么复杂;偶然发现同样的问题,我意识到它就像

idxs = np.where(x[1:] - x[:-1] == 0)
idxs = [i for idx in idxs for i in (idx, idx + 1)]

第一行通过np.diff(x)高效实现;进一步,例如检测斜率 > 5,使用np.diff(x) > 5。第二行是因为差分抛出了正确的端点(例如diff([5,6,6,6,7]) = [1,0,0,1] -> idxs=[1,2],不包括3,


下面的功能应该做;用直观的变量和方法名称编写的代码,并且应该通过一些阅读来不言自明。代码高效且可扩展。


功能

  • 指定最小和最大扁平线长度
  • 指定左右尾部的最小和最大坡度
  • 指定多个区间内左右尾部的最小和最大平均斜率

示例

import numpy as np
import matplotlib.pyplot as plt

# Toy data
t = np.array([[ 5,  3,  3,  5,  3,  3,  3,  3,  3,  5,  5,  3,  3,  0,  4,  
                1,  1, -1, -1,  1,  1,  1,  1, -1,  1,  1, -1,  0,  3,  3,  
                5,  5,  3,  3,  3,  3,  3,  5,  7,  3,  3,  5]]).T
plt.plot(t)
plt.show()

# Get flatline indices
indices = get_flatline_indices(t, min_len=4, max_len=5)
plt.plot(t)
for idx in indices:
    plt.plot(idx, t[idx], marker='o', color='r')
plt.show()

# Filter by edge slopes
lims_left  = (-10, -2)
lims_right = (2,  10)
averaging_intervals = [1, 2, 3]
indices_filtered = filter_by_tail_slopes(indices, t, lims_left, lims_right,
                                         averaging_intervals)
plt.plot(t)
for idx in indices_filtered:
    plt.plot(idx, t[idx], marker='o', color='r')
plt.show()

def get_flatline_indices(sequence, min_len=2, max_len=6):
    indices=[]
    elem_idx = 0
    max_elem_idx = len(sequence) - min_len
        
    while elem_idx < max_elem_idx:
        current_elem = sequence[elem_idx]
        next_elem    = sequence[elem_idx+1]
        flatline_len = 0

        if current_elem == next_elem:
            while current_elem == next_elem:
                flatline_len += 1
                next_elem = sequence[elem_idx + flatline_len]
                
            if flatline_len >= min_len:
                if flatline_len > max_len:
                    flatline_len = max_len
    
                trim_start = elem_idx
                trim_end   = trim_start + flatline_len
                indices_to_append = [index for index in range(trim_start, trim_end)]
                indices += indices_to_append

            elem_idx += flatline_len
            flatline_len = 0
        else:
            elem_idx += 1
    return indices if not all([(entry == []) for entry in indices]) else []
def filter_by_tail_slopes(indices, data, lims_left, lims_right, averaging_intervals=1):
    indices_filtered = []
    indices_temp, tails_temp = [], []
    got_left, got_right = False, False
    
    for idx in indices:
        slopes_left, slopes_right = _get_slopes(data, idx, averaging_intervals)
        
        for tail_left, slope_left in enumerate(slopes_left):
            if _valid_slope(slope_left, lims_left):
                if got_left:
                    indices_temp = []  # discard prev if twice in a row
                    tails_temp = []
                indices_temp.append(idx)
                tails_temp.append(tail_left + 1)
                got_left = True
        if got_left:
            for edge_right, slope_right in enumerate(slopes_right):
                if _valid_slope(slope_right, lims_right):
                    if got_right:
                        indices_temp.pop(-1)
                        tails_temp.pop(-1)
                    indices_temp.append(idx)
                    tails_temp.append(edge_right + 1)
                    got_right = True

        if got_left and got_right:
            left_append  = indices_temp[0] - tails_temp[0]
            right_append = indices_temp[1] + tails_temp[1]
            indices_filtered.append(_fill_range(left_append, right_append))
            indices_temp = []
            tails_temp = []
            got_left, got_right = False, False
    return indices_filtered
def _get_slopes(data, idx, averaging_intervals):
    if type(averaging_intervals) == int:
        averaging_intervals = [averaging_intervals]

    slopes_left, slopes_right = [], []
    for interval in averaging_intervals:
        slopes_left  += [(data[idx] - data[idx-interval]) / interval]
        slopes_right += [(data[idx+interval] - data[idx]) / interval]
    return slopes_left, slopes_right

def _valid_slope(slope, lims):
    min_slope, max_slope = lims
    return (slope  >= min_slope) and (slope <= max_slope)

def _fill_range(_min, _max):
    return [i for i in range(_min, _max + 1)]

【讨论】:

  • 我花了很多时间阅读代码,调整了一些参数,并确保我理解你在做什么。在此过程中我注意到的一件事是,_iterative_average 被定义为一个函数,但从未使用过。除此之外,我真的很感谢你投入的时间。对我来说绝对有用。
  • 哦,对了,我想我最后只是将它集成到 _get_slopes 中,并没有注意到(最后有点累)——虽然不确定,我会仔细检查。很高兴你通读了它,有些人往往不会打扰 - 不客气。
  • @ultimate8 你说得对,不需要 - 答案已更新。如果问题已解决,请考虑投票并接受答案。
猜你喜欢
  • 1970-01-01
  • 2021-02-14
  • 2020-02-09
  • 1970-01-01
  • 2018-05-26
  • 1970-01-01
  • 2012-11-29
  • 2020-06-23
  • 1970-01-01
相关资源
最近更新 更多