Pandas: segmentación en zigzag de datos basada en mínimos máximos locales

10

Tengo datos de una serie temporal. Generando datos

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

Quiero crear una línea en zig-zag que conecte entre los máximos locales y los mínimos locales, que satisfaga la condición de que en el eje y, |highest - lowest value|de cada línea en zig-zag debe exceder un porcentaje (digamos 20%) de la distancia de la anterior línea en zig-zag, Y un valor preestablecido k (digamos 1.2)

Puedo encontrar los extremos locales usando este código:

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

pero no sé cómo aplicarle la condición de umbral. Por favor, avísenme sobre cómo aplicar tal condición.

Como los datos pueden contener millones de marcas de tiempo, se recomienda un cálculo eficiente

Para una descripción más clara: ingrese la descripción de la imagen aquí

Ejemplo de salida, de mis datos:

 # 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')

ingrese la descripción de la imagen aquí

Mi salida deseada (algo similar a esto, el zigzag solo conecta los segmentos significativos) ingrese la descripción de la imagen aquí

Thanh Nguyen
fuente

Respuestas:

3

He respondido a mi mejor comprensión de la pregunta. Sin embargo, no está claro cómo la variable K influye en el filtro.

Desea filtrar los extremos en función de una condición de ejecución. Supongo que desea marcar todos los extremos cuya distancia relativa al último extremo marcado es mayor que p%. Además, supongo que siempre considera que el primer elemento de la serie de tiempo es un punto válido / relevante.

Implementé esto con la siguiente función de filtro:

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

Para ejecutar su código, primero importo dependencias:

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

Para hacer que el código sea reproducible, arreglo la semilla aleatoria:

np.random.seed(0)

El resto de aquí es copypasta. Tenga en cuenta que disminuí la cantidad de muestra para aclarar el resultado.

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

Luego usamos la función de filtro:

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

Y traza como hiciste tanto tu trama anterior como los extremos recién filtrados:

 # 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')

ingrese la descripción de la imagen aquí

EDITAR :

Si desea que ambos consideren válido tanto el primer como el último punto, puede adaptar la función de filtro de la siguiente manera:

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
Nikolas Rieble
fuente
Hola, gracias por una gran respuesta. Sí, su suposición es correcta "marque todos los extremos cuya distancia relativa al último extremo marcado sea mayor que p%", y tanto el primer como el último punto siempre deben considerarse. Verifiqué tu respuesta, a veces se pierde el último punto, ¿podrías ayudarme en eso?
Thanh Nguyen
3

Puede usar la funcionalidad de balanceo de Pandas para crear los extremos locales. Eso simplifica un poco el código en comparación con su enfoque Scipy.

Funciones para encontrar los extremos:

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)

La función para crear el zigzag, se puede aplicar en el Marco de datos a la vez (sobre cada columna), pero esto introducirá NaN ya que las marcas de tiempo devueltas serán diferentes para cada columna. Puede soltarlos fácilmente más tarde como se muestra en el siguiente ejemplo, o simplemente aplicar la función en una sola columna en su Marco de datos.

Tenga en cuenta que descomenté la prueba contra un umbral k, no estoy seguro si entiendo completamente esa parte correctamente. Puede incluirlo si la diferencia absoluta entre el extremo anterior y el actual necesita ser mayor que k:& (ext_val.diff().abs() > k)

Tampoco estoy seguro de si el zigzag final siempre debe moverse de un máximo original a un mínimo o viceversa. Supuse que debería, de lo contrario, puede eliminar la segunda búsqueda de extrema al final de la función.

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

Genere algunos datos de muestra:

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()

Aplique la función y extraiga el resultado para la columna 'data1':

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

Visualiza el resultado:

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()

ingrese la descripción de la imagen aquí

Rutger Kassies
fuente
gracias por tu respuesta. Quiero preguntar sobre esta línea (ext_val.diff().abs() > (ext_val.shift(-1).abs() * p)), según tengo entendido, está comparando la distancia entre dos puntos con p%el último punto, ¿estoy en lo cierto? Porque quiero comparar cada segmento en zigzag con el segmento anterior y repetir hasta que se cumpla la condición.
Thanh Nguyen