Pandas: зигзагообразная сегментация данных на основе локальных минимумов-максимумов

У меня есть данные временного ряда. Генерация данных

date_rng = pd.date_range('2019-01-01', freq='s', periods=400)
df = pd.DataFrame(np.random.lognormal(.005, .5,size=(len(date_rng), 3)),
                  columns=['data1', 'data2', 'data3'],
                  index= date_rng)
s = df['data1']

Я хочу создать зигзагообразную линию, соединяющую локальные максимумы и локальные минимумы, которая удовлетворяет условию, согласно которому по оси Y |highest - lowest value| каждой зигзагообразной линии должно превышать процент (скажем, 20%) от расстояния предыдущая зигзагообразная линия И заданное значение k (скажем, 1,2)

Я могу найти локальные экстремумы, используя этот код:

# Find peaks(max).
peak_indexes = signal.argrelextrema(s.values, np.greater)
peak_indexes = peak_indexes[0]

# Find valleys(min).
valley_indexes = signal.argrelextrema(s.values, np.less)
valley_indexes = valley_indexes[0]
# Merge peaks and valleys data points using pandas.
df_peaks = pd.DataFrame({'date': s.index[peak_indexes], 'zigzag_y': s[peak_indexes]})
df_valleys = pd.DataFrame({'date': s.index[valley_indexes], 'zigzag_y': s[valley_indexes]})
df_peaks_valleys = pd.concat([df_peaks, df_valleys], axis=0, ignore_index=True, sort=True)

# Sort peak and valley datapoints by date.
df_peaks_valleys = df_peaks_valleys.sort_values(by=['date'])

но я не знаю, как применить к нему пороговое условие. Посоветуйте, пожалуйста, как применить такое условие.

Поскольку данные могут содержать миллионы меток времени, настоятельно рекомендуется эффективный расчет.

Более четкое описание: введите здесь описание изображения

Пример вывода из моих данных:

 # Instantiate axes.
(fig, ax) = plt.subplots()
# Plot zigzag trendline.
ax.plot(df_peaks_valleys['date'].values, df_peaks_valleys['zigzag_y'].values, 
                                                        color='red', label="Zigzag")

# Plot original line.
ax.plot(s.index, s, linestyle='dashed', color='black', label="Org. line", linewidth=1)

# Format time.
ax.xaxis_date()
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d"))

plt.gcf().autofmt_xdate()   # Beautify the x-labels
plt.autoscale(tight=True)

plt.legend(loc='best')
plt.grid(True, linestyle='dashed')

введите описание изображения здесь

Мой желаемый результат (что-то похожее на это, зигзаг соединяет только важные сегменты) введите здесь описание изображения


person Thanh Nguyen    schedule 02.01.2020    source источник


Ответы (2)


Я ответил на мое лучшее понимание вопроса. Пока не ясно, как переменная K влияет на фильтр.

Вы хотите отфильтровать экстремумы на основе текущего условия. Я предполагаю, что вы хотите пометить все экстремумы, относительное расстояние которых до последнего отмеченного экстремума больше p%. Я также предполагаю, что вы всегда считаете первый элемент временного ряда допустимой/актуальной точкой.

Я реализовал это с помощью следующей функции фильтра:

def filter(values, percentage):
    previous = values[0] 
    mask = [True]
    for value in values[1:]: 
        relative_difference = np.abs(value - previous)/previous
        if relative_difference > percentage:
            previous = value
            mask.append(True)
        else:
            mask.append(False)
    return mask

Чтобы запустить ваш код, я сначала импортирую зависимости:

from scipy import signal
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates

Чтобы сделать код воспроизводимым, я исправляю случайное начальное число:

np.random.seed(0)

Остальное отсюда - копипаста. Обратите внимание, что я уменьшил количество выборки, чтобы сделать результат более понятным.

date_rng = pd.date_range('2019-01-01', freq='s', periods=30)
df = pd.DataFrame(np.random.lognormal(.005, .5,size=(len(date_rng), 3)),
                  columns=['data1', 'data2', 'data3'],
                  index= date_rng)
s = df['data1']
# Find peaks(max).
peak_indexes = signal.argrelextrema(s.values, np.greater)
peak_indexes = peak_indexes[0]
# Find valleys(min).
valley_indexes = signal.argrelextrema(s.values, np.less)
valley_indexes = valley_indexes[0]
# Merge peaks and valleys data points using pandas.
df_peaks = pd.DataFrame({'date': s.index[peak_indexes], 'zigzag_y': s[peak_indexes]})
df_valleys = pd.DataFrame({'date': s.index[valley_indexes], 'zigzag_y': s[valley_indexes]})
df_peaks_valleys = pd.concat([df_peaks, df_valleys], axis=0, ignore_index=True, sort=True)
# Sort peak and valley datapoints by date.
df_peaks_valleys = df_peaks_valleys.sort_values(by=['date'])

Затем мы используем функцию фильтра:

p = 0.2 # 20% 
filter_mask = filter(df_peaks_valleys.zigzag_y, p)
filtered = df_peaks_valleys[filter_mask]

И постройте, как вы сделали как свой предыдущий график, так и недавно отфильтрованные экстремумы:

 # Instantiate axes.
(fig, ax) = plt.subplots(figsize=(10,10))
# Plot zigzag trendline.
ax.plot(df_peaks_valleys['date'].values, df_peaks_valleys['zigzag_y'].values, 
                                                        color='red', label="Extrema")
# Plot zigzag trendline.
ax.plot(filtered['date'].values, filtered['zigzag_y'].values, 
                                                        color='blue', label="ZigZag")

# Plot original line.
ax.plot(s.index, s, linestyle='dashed', color='black', label="Org. line", linewidth=1)

# Format time.
ax.xaxis_date()
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d"))

plt.gcf().autofmt_xdate()   # Beautify the x-labels
plt.autoscale(tight=True)

plt.legend(loc='best')
plt.grid(True, linestyle='dashed')

введите здесь описание изображения

ИЗМЕНИТЬ:

Если вы хотите, чтобы как первая, так и последняя точка считались действительными, вы можете адаптировать функцию фильтра следующим образом:

def filter(values, percentage):
    # the first value is always valid
    previous = values[0] 
    mask = [True]
    # evaluate all points from the second to (n-1)th
    for value in values[1:-1]: 
        relative_difference = np.abs(value - previous)/previous
        if relative_difference > percentage:
            previous = value
            mask.append(True)
        else:
            mask.append(False)
    # the last value is always valid
    mask.append(True)
    return mask
person Nikolas Rieble    schedule 07.01.2020
comment
привет, спасибо за отличный ответ. Да, ваше предположение верно, отметьте все экстремумы, относительное расстояние которых до последнего отмеченного экстремума больше, чем p%. И всегда следует учитывать как первую, так и последнюю точку. Я проверил ваш ответ, иногда он пропустил последний пункт, не могли бы вы мне помочь? - person Thanh Nguyen; 08.01.2020

Вы можете использовать функцию прокрутки Pandas для создания локальных экстремумов. Это немного упрощает код по сравнению с вашим подходом Scipy.

Функции поиска экстремумов:

def islocalmax(x):
    """Both neighbors are lower,
    assumes a centered window of size 3"""
    return (x[0] < x[1]) & (x[2] < x[1])

def islocalmin(x):
    """Both neighbors are higher,
    assumes a centered window of size 3"""
    return (x[0] > x[1]) & (x[2] > x[1])

def isextrema(x):
    return islocalmax(x) or islocalmin(x)

Функция для создания зигзага может быть применена к Dataframe сразу (по каждому столбцу), но это приведет к появлению NaN, поскольку возвращаемые временные метки будут разными для каждого столбца. Вы можете легко удалить их позже, как показано в примере ниже, или просто применить функцию к одному столбцу в вашем кадре данных.

Обратите внимание, что я раскомментировал тест для порога k, я не уверен, что правильно понял эту часть. Вы можете включить его, если абсолютная разница между предыдущим и текущим экстремумом должна быть больше, чем k: & (ext_val.diff().abs() > k)

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

def create_zigzag(col, p=0.2, k=1.2):

    # Find the local min/max
    # converting to bool converts NaN to True, which makes it include the endpoints    
    ext_loc = col.rolling(3, center=True).apply(isextrema, raw=False).astype(np.bool_)

    # extract values at local min/max
    ext_val = col[ext_loc]

    # filter locations based on threshold
    thres_ext_loc = (ext_val.diff().abs() > (ext_val.shift(-1).abs() * p)) #& (ext_val.diff().abs() > k)

    # Keep the endpoints
    thres_ext_loc.iloc[0] = True
    thres_ext_loc.iloc[-1] = True

    thres_ext_loc = thres_ext_loc[thres_ext_loc]

    # extract values at filtered locations 
    thres_ext_val = col.loc[thres_ext_loc.index]

    # again search the extrema to force the zigzag to always go from high > low or vice versa,
    # never low > low, or high > high
    ext_loc = thres_ext_val.rolling(3, center=True).apply(isextrema, raw=False).astype(np.bool_)
    thres_ext_val  =thres_ext_val[ext_loc]

    return thres_ext_val

Сгенерируйте некоторые образцы данных:

date_rng = pd.date_range('2019-01-01', freq='s', periods=35)

df = pd.DataFrame(np.random.randn(len(date_rng), 3),
                  columns=['data1', 'data2', 'data3'],
                  index= date_rng)

df = df.cumsum()

Примените функцию и извлеките результат для столбца data1:

dfzigzag = df.apply(create_zigzag)
data1_zigzag = dfzigzag['data1'].dropna()

Визуализируйте результат:

fig, axs = plt.subplots(figsize=(10, 3))

axs.plot(df.data1, 'ko-', ms=4, label='original')
axs.plot(data1_zigzag, 'ro-', ms=4, label='zigzag')
axs.legend()

введите здесь описание изображения

person Rutger Kassies    schedule 08.01.2020
comment
спасибо за Ваш ответ. Я хочу спросить об этой линии (ext_val.diff().abs() > (ext_val.shift(-1).abs() * p)), как я понимаю, вы сравниваете расстояние между двумя точками с p% последней точки, я прав? Потому что я хочу сравнить каждый сегмент зигзага с предыдущим сегментом и повторять до тех пор, пока не будет выполнено условие. - person Thanh Nguyen; 10.01.2020