Панди: зигзагоподібна сегментація даних на основі локальних мінімумів-максимумів


10

У мене є дані про часові видання. Генерування даних

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']

Я хочу створити зигзагоподібну лінію, що з'єднує між локальними максимумами та локальними мінімумами, що задовольняє умові, що на осі |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')

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

Мій бажаний вихід (щось подібне до цього, зигзаг з'єднує лише значні сегменти) введіть тут опис зображення

Відповіді:


3

Я відповів на найкраще розуміння питання. Однак не зрозуміло, як змінна 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)

Решта звідси - це copypasta. Зауважте, що я зменшив кількість проби, щоб зрозуміти результат.

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

привіт, дякую за чудову відповідь. Так, ваше припущення є правильним: "позначте всю екстремуму, відносна відстань якої до останнього позначеного екстремуму перевищує p%", і завжди слід враховувати і першу, і останню точку. Я перевірив вашу відповідь, іноді пропущений останній пункт, ви могли б мені допомогти в цьому?
Тхань Нгуен

3

Ви можете використовувати функцію прокатки 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, оскільки повернені часові позначки будуть різними для кожного стовпця. Ви можете їх легко скинути пізніше, як показано в прикладі нижче, або просто застосувати функцію до одного стовпця у вашому Dataframe.

Зауважте, що я коментував тест із пороговим рівнем 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()

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


Дякую за вашу відповідь. Я хочу запитати про цю пряму (ext_val.diff().abs() > (ext_val.shift(-1).abs() * p)), наскільки я розумію, ви порівнюєте відстань між двома точками з p%останньою точкою, я прав? Тому що я хочу порівняти кожен сегмент зигзагу з попереднім сегментом і повторювати, поки умова не буде виконана.
Тхань Нгуен
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.