Обнаружение паттернов из двух массивов данных в Python - PullRequest
0 голосов
/ 12 июля 2020

Я пытаюсь обнаружить закономерности из данных открытие-максимум-минимум-закрытие (OHL C) , поэтому вот что я сделал:

  1. Найти локальный минимумы и максимумы в наборе данных
  2. Нормализовать мои данные, преобразовав массив локальных минимумов и максимумов в массив чисел, где каждое число является отклонением от предыдущей точки.

До сих пор все работает, но я застрял на следующей части. Я определил массив данных, который представляет собой образец , который при нанесении на график будет иметь определенную форму. Теперь я пытаюсь найти в других наборах данных фигуры, похожие на указанный мной шаблон.

Вот шаблон, указанный мной:

Pattern = [7.602339181286544, 3.5054347826086927, -5.198214754528746, 4.7078371642204315, -2.9357312880190425, 2.098092643051778, -0.5337603416066172]

А вот образец набор данных:

SampleTarget = [-2.2538552787663173, -3.00364077669902, 2.533625273694082, -2.2574740695546116, 3.027465667915112, 6.4222962738564, -2.647309991460278, 7.602339181286544, 3.5054347826086927, -5.198214754528746, 4.7078371642204315, -2.9357312880190425, 2.098092643051778, -0.5337603416066172, 4.212503353903944, -2.600411946446969, 8.511763150938416, -3.775883069427527, 1.8227848101265856, 3.6300348085529524, -1.4635316698656395, 5.527148770392016, -1.476695892939546, 12.248243559718961, -4.443980805341117, 1.9213973799126631, -9.061696658097686, 5.347467608951697, -2.8622540250447197, 2.6012891344383067]

Я ищу способ определить, когда в определенный момент на SampleTarget обнаруживается серия значений, аналогичных Pattern.

В этом случае, например, мне нужно каким-то образом определить, что есть часть SampleTarget, где значения аналогичны Pattern, поскольку это тот же набор данных, из которого я извлек Pattern.

Что я пробовал:

Мне предложили использовать numpy.correlate, python-dtw (Dynami c time warping) или stumpy , но проблема, с которой я столкнулся вместе с тем отсутствие практических примеров по этому конкретному вопросу.

Ответы [ 4 ]

1 голос
/ 12 августа 2020

Чтобы найти известный шаблон, Q, из независимого временного ряда, T, с помощью пакета STUMPY Python вам нужно будет сделать что-то вроде этого:

from stumpy.core import mass
import numpy as np

Pattern = np.array([7.602339181286544, 3.5054347826086927, -5.198214754528746, 4.7078371642204315, -2.9357312880190425, 2.098092643051778, -0.5337603416066172])

SampleTarget = np.array([-2.2538552787663173, -3.00364077669902, 2.533625273694082, -2.2574740695546116, 3.027465667915112, 6.4222962738564, -2.647309991460278, 7.602339181286544, 3.5054347826086927, -5.198214754528746, 4.7078371642204315, -2.9357312880190425, 2.098092643051778, -0.5337603416066172, 4.212503353903944, -2.600411946446969, 8.511763150938416, -3.775883069427527, 1.8227848101265856, 3.6300348085529524, -1.4635316698656395, 5.527148770392016, -1.476695892939546, 12.248243559718961, -4.443980805341117, 1.9213973799126631, -9.061696658097686, 5.347467608951697, -2.8622540250447197, 2.6012891344383067])

distance_profile = mass(Pattern, SampleTarget)

# Output of `distance_profile`
array([4.55219811, 4.21544139, 3.29336127, 4.72614564, 2.94202855,
       3.33790488, 4.62672866, 0.        , 4.51937582, 3.47144433,
       4.17966567, 3.26871969, 4.72146046, 2.53070957, 4.46398626,
       3.64503919, 2.64282983, 4.81577841, 2.69799924, 4.64286098,
       2.67446216, 4.52739326, 2.54663088, 3.79885921])

По сути, функция mass вычисляет distance_profile, беря ваш Pattern и сдвигая окно (той же длины, что и ваше Pattern) вдоль SampleTarget и вычисляя z-нормализованное Евклидово расстояние. Каждое «окно is referred to as a subsequence and each element of the distance_profile corresponds to the distance between one subsequence and your Pattern».

Так, например, расстояние между вашим Pattern и первой подпоследовательностью SampleTarget[0:0+len(Pattern)] составляет distance_profile[0] = 4.55219811.

Точно так же расстояние между вашей Pattern и первой подпоследовательностью SampleTarget[1:1+len(Pattern)] составляет distance_profile[1] = 4.21544139.

И, как правило, расстояние между вашей Pattern и подпоследовательностью ith, SampleTarget[i:i+len(Pattern)], равно distance_profile[i].

Теперь, чтобы найти части SampleTarget, которые "наиболее близки" к Pattern, вы можете найти наименьшие значения в своем distance_profile, а затем использовать соответствующие index из вашего distance_profile, чтобы перекрестно ссылаться на индекс из вашего SampleTarget.

Более конкретно, используя наш пример выше, наименьшее значение, найденное в distance_profile, составляет 0 (идеальное совпадение) и это находится по индексу i = 7. Итак, теперь вы должны обнаружить, что SampleTarget[7:7+len(Pattern)] должен быть идентичен Pattern. Обратите внимание, что STUMPY (и mass) не заботится о том, существует ли идентичное совпадение. Что вы Скорее всего, я захочу выбрать разумный порог / порог расстояния и изучить все «совпадения», которые попадают ниже этого порогового значения расстояния. Случайно / статично, я рекомендую выбрать порог ниже np.mean(distance_profile) - 2 * np.std(distance_profile) в качестве разумно информированной отправной точки.

Наконец, последнее замечание о том, что функция mass вычисляет расстояния скользящего окна в O(nlogn) ( log - это база 2), в то время как простое скользящее окно вычисляет профиль расстояния в O(nm) (где m - длина вашего шаблона). Таким образом, для m > 20, mass всегда будет быстрее, но разница в производительности практически незаметна для более коротких шаблонов. И в случае, если кто-то хочет обсудить это, имейте в виду, что mass скомпилирован JIT, и поэтому при первом вызове функции она будет "медленной" из-за того, что функция должна быть скомпилирована, но должна после этого будьте очень быстры.

1 голос
/ 12 июля 2020

Вот трюк, чтобы сделать это:

import numpy as np
pat = np.array(Pattern)
data = np.array(SampleTarget)
n = len(data)
m = len(pat)
k = data.strides[0] # typically 8 for float64

# data2d is a view to the original data,
# with data_2d[:-m, 6] == data_2d[1:1-m, 5] == ... == data_2d[6:, 0]
data_2d = np.lib.stride_tricks.as_strided(data, shape=(n-m+1, m), strides=(k, k))

# So you can check for matches on data[i, :] for all i
print(np.all(np.isclose(data_2d, pat), axis=1))

Вывод:

array([False, False, False, False, False, False, False,  True, False,
       False, False, False, False, False, False, False, False, False,
       False, False, False, False, False])

Вы можете использовать np.where или np.argwhere, чтобы получить индекс совпадения (например, ). Вы можете настроить параметры atol и rtol для np.isclose, чтобы установить порог для приблизительного совпадения.

Пояснение: если вы проделаете трюк as_strided с data=np.arange(30), тогда data2d будет:

array([[ 0,  1,  2,  3,  4,  5,  6],
       [ 1,  2,  3,  4,  5,  6,  7],
       [ 2,  3,  4,  5,  6,  7,  8],
       ...
       [21, 22, 23, 24, 25, 26, 27],
       [22, 23, 24, 25, 26, 27, 28],
       [23, 24, 25, 26, 27, 28, 29]])

EDIT: это эффективный способ создать представление тех же данных с помощью скользящего windows, не требуя дополнительной памяти. Поиск в массиве numpy a[i, j] находит адрес памяти как start_address + a.strides[0]*i + a.strides[1]*j; установив для шагов значение (8, 8), где 8 - это размер значения с плавающей запятой, вы получите эффект скользящего окна. Поскольку разные элементы массива относятся к одной и той же памяти, лучше всего рассматривать массив, созданный таким образом, как доступный только для чтения.

РЕДАКТИРОВАТЬ: если вы хотите иметь метрику «оценки» c для качества совпадение, вы можете, например, сделать это:

>>> np.linalg.norm(data_2d - pat, axis=1) 

array([17.5, 17.4, 13.3, 20.5, 12.9, 14.9, 19.7,  0. , 17.4, 13.8, 16.9,
       13.7, 19. , 10.3, 18.3, 15.2, 10.9, 22.3, 13. , 21.8, 15.2, 24.5,
       14.9, 20.7])
# (numbers rounded to reduce clutter)

ближе к нулю означает лучшее совпадение. Здесь norm принимает длину вектора разности d=data-pat, т.е. sqrt(d[0]**2 + ... + d[m-1]**2).

EDIT: если вас интересуют узоры, которые имеют такую ​​же форму, но увеличены или уменьшены значение, вы можете сделать это:

# New dataset with two occurrences of the pattern: one scaled by a factor 1.1,
# one scaled 0.5 with a bit of noise added
data_mod = data*1.1
np.random.seed(1)
data_mod[16:16+m] = pat*0.5 + np.random.uniform(-0.5, 0.5, size=m)
data_2d_mod = np.lib.stride_tricks.as_strided(
    data_mod, shape=(n-m+1, m), strides=(k, k))

# pat_inv: pseudoinverse of pat vector
pat_inv = 1/(pat @ pat) * pat 

# cofs: fit coefficients, shape (n1,)
cofs = data_2d_mod @ pat_inv # fit coefficients, shape (n1,)

# sum of squared residuals, shape (n1,) - zero means perfect fit
ssqr = ((data_2d_mod - cofs.reshape(-1, 1) * pat)**2).sum(axis=1)

print(f'cofs:\n{np.around(cofs, 2)}')
print(f'ssqr:\n{np.around(ssqr, 1)}')

Результат:

cofs:
[-0.38 -0.14  0.4  -0.54  0.59  0.36 -0.48  1.1  -0.33  0.12 -0.06  0.18
 -0.21  0.23  0.22 -0.33  0.52 -0.2   0.22 -0.35  0.6  -0.91  0.92  0.01]
ssqr:
[ 81.6 161.8 147.4 155.1 167.3 196.1 138.6   0.   97.8 103.5  85.9  59.3
  57.1  54.9  58.3  29.2   0.7 198.7 217.4 201.9 266.3 235.1 242.8 361.9]

Вы видите, что cofs[7] == 1.1, что означает, что шаблон нужно было масштабировать с коэффициентом 1,1 для соответствующих данных окно для наилучшего вписывания. Подгонка была идеальной, что видно по ssqr[7] == 0. Он также находит другой, с cofs[16] == 0.52 (близко к ожидаемому значению 0,5) и ssqr[16] == 0.7.

Другой пример: cofs[21]==-0.91 и ssqr[12]==235.1. Это означает, что data_mod[12:19] чем-то напоминает паттерн, но в инвертированном виде (поменяны местами положительные и отрицательные). Это зависит от того, что вы хотите делать с данными; скорее всего, вы захотите посмотреть на cofs значения в диапазоне от 0,5 до 2: ваш шаблон поиска может встречаться в данных в 2 раза больше или меньше. Это должно сочетаться с достаточно маленькими ssqr значениями.

Здесь вы видите три возможных совпадения на графике:

данные с совпадениями с образцом

Если вы используете ssqr как метрику c, имейте в виду, что серия нулей во входных данных приведет к cofs=0 и ssqr=0.

Рассмотрите возможность использования np.sqrt(ssqr/m)/np.abs(cofs) в качестве метри c вместо этого по двум причинам. (1) он будет соответствовать в соответствии с относительной ошибкой и приведет к NaN значениям в случае нулевого ввода. (2) это более интуитивно понятно; если значение равно 0,5, это означает, что точки данных отклоняются примерно на 0,5 от значений шаблона. Вот значения для этого метри c с использованием тех же данных примера:

[ 9.1  35.3  11.6  8.8   8.3  14.8   9.4   0.  11.4  33.3 55.9  16.4
 13.9  12.1  12.9  6.2   0.6  27.2  25.4 15.2  10.4  6.4   6.4 482.5]

Для совпадения в data_mod[21:28] разница c составляет 6,4, что примерно соответствует различиям. как видно на сюжете.

1 голос
/ 15 июля 2020

Проблема, которую вы пытаетесь решить, представляет собой примерную проблему сопоставления подпоследовательностей (или сопоставление нечетких многоугольников).

Эта проблема может быть решена с помощью расстояния Левенштейна. Предположим, -

Pattern = [7.602339181286544, 3.5054347826086927, -5.198214754528746, 4.7078371642204315, -2.9357312880190425, 2.098092643051778, -0.5337603416066172]
SampleTarget = [-2.2538552787663173, -3.00364077669902, 2.533625273694082, -2.2574740695546116, 3.027465667915112, 6.4222962738564, -2.647309991460278, 7.602339181286544, 3.5054347826086927, -5.198214754528746, 4.7078371642204315, -2.9357312880190425, 2.098092643051778, -0.5337603416066172, 4.212503353903944, -2.600411946446969, 8.511763150938416, -3.775883069427527, 1.8227848101265856, 3.6300348085529524, -1.4635316698656395, 5.527148770392016, -1.476695892939546, 12.248243559718961, -4.443980805341117, 1.9213973799126631, -9.061696658097686, 5.347467608951697, -2.8622540250447197, 2.6012891344383067]
x0 = np.arange(len(SampleTarget))
x1 = np.arange(len(Pattern))
plt.plot(x0,SampleTarget)
plt.plot(x1,Pattern)

Вы пытаетесь сопоставить Pattern с SampleTarget, «перекатывая» его по оси. В основном вам нужно найти оценку, которая покажет вам, насколько «далека» форма паттерна от паттерна к окну SampleTarget, который он покрывает. Это можно сделать с помощью EDIT DISTANCE или LEVENSTEIN DISTANCE. Что интуитивно понятно -

Какое количество правок мне нужно, чтобы изменить конкретную c последовательность на другую.

enter image description here

#!pip install Distance
import distance

score = []
for i in range(len(SampleTarget)):
    SampleTarget_sub = SampleTarget[i:i+len(Pattern)] #rolling the Pattern over windows of SampleTarget
    score.append(distance.levenshtein(Pattern, SampleTarget_sub))
    
print(score)
[7, 7, 7, 7, 6, 4, 2, 0, 2, 4, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]

This tells you that at 0th window position you need 7 edits to change the Pattern into the subsequence of SampleTarget and at the 7th position, the distance between Pattern and SampleTarget subsequence is 0, meaning it needs 0 edits to change Pattern to the SampleTarget subsequence at the 7th position, meaning exact match.

x2 = np.arange(start = np.argmin(score),stop= np.argmin(score)+len(Pattern))
plt.plot(x0,SampleTarget)
plt.plot(x2,Pattern)

enter image description here

Now let's say that the patterns are NOT the exact match and have some points in the middle that don't actually match correctly.

#modified a value in pattern
Pattern = [7.602339181286544, 3.5054347826086927, -5.198214754528746, 4.7078371642204315, -2.9357312880190425, 4.098092643051778, -0.5337603416066172]
SampleTarget = [-2.2538552787663173, -3.00364077669902, 2.533625273694082, -2.2574740695546116, 3.027465667915112, 6.4222962738564, -2.647309991460278, 7.602339181286544, 3.5054347826086927, -5.198214754528746, 4.7078371642204315, -2.9357312880190425, 2.098092643051778, -0.5337603416066172, 4.212503353903944, -2.600411946446969, 8.511763150938416, -3.775883069427527, 1.8227848101265856, 3.6300348085529524, -1.4635316698656395, 5.527148770392016, -1.476695892939546, 12.248243559718961, -4.443980805341117, 1.9213973799126631, -9.061696658097686, 5.347467608951697, -2.8622540250447197, 2.6012891344383067]

Running the code again, the scores I get are -

[7, 7, 7, 7, 6, 4, 3, 1, 3, 5, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]

This still corresponds to moving the sequence to the 7th as its the minimum distance from the original Pattern

enter image description here

If you have too much jitteriness in the sequence, i would recommend simplifying your sequences using a polygon approximation algorithm such as Алгоритм Рамера – Дугласа – Пекера (RDP) . Это приведет к лучшим результатам при применении расстояний Левенштейна. Для него также есть реализация python !

Надеюсь, это решит вашу проблему!

1 голос
/ 12 июля 2020

Вот довольно импровизированное решение, которое предполагает, что вы ищете совпадение exact, это просто проверка совпадений методом перебора путем перебора всего списка, если он находит совпадение, он проверяет следующую позицию и т. Д. И т. Д. . Он также предполагает, что шаблон [0] не повторяется в списке шаблонов, однако его можно легко закодировать с помощью немного более поразительного

for i in range(len(SampleTarget)):
    # Iterate over the list and check if the number matchs the first
    # one we are checking agaisnt for our pattern
    if SampleTarget[i] == Pattern[0]:
        # Hey this index might be the start of our pattern,
        # lets check to see if the following items are our pattern
        startIndex = i
        for x in range(len(Pattern)):
            curCheck = startIndex + x # Get current place to check agaisnt

            if SampleTarget[curCheck] != Pattern[x]:
                # Disregard the loop, this isnt it
                break

        # Hey, we made it to the end of the break, so it matches
        # Lets print the index where we found the match
        print(f"Found a pattern match in the sample!\nStart Index: {startIndex}\nEnd Index: {curCheck}")

Вот мой вариант, который соответствует неточным значениям в пределах заданного допуска. Не стесняйтесь изменять его по своему усмотрению, но в настоящее время он составляет 0,005, и вы читаете об этом здесь

import math

for i in range(len(SampleTarget)):
    if math.isclose(SampleTarget[i], Pattern[0], abs_tol=0.005):
        startIndex = i
        for x in range(len(Pattern)):
            curCheck = startIndex + x

            if not math.isclose(SampleTarget[curCheck], Pattern[x], abs_tol=0.005):
                break

        print(f"Found a pattern match in the sample!\nStart Index: {startIndex}\nEnd Index: {curCheck}")

И оба будут выводить одно и то же, только второй не проверяет равенство и скорее проверяет на аналогичной основе, а не на абсолютной.

Надеюсь, это поможет! Несмотря на то, что вы упомянули вещи, а затем я вместо этого вытащил петли хахаха

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...