Pandas: Segmentasi data Zigzag berdasarkan minima-maxima lokal

10

Saya punya data deret waktu. Menghasilkan data

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

Saya ingin membuat garis zig-zag yang menghubungkan antara maxima lokal dan minimum lokal, yang memenuhi syarat bahwa pada sumbu y, |highest - lowest value|setiap garis zig-zag harus melebihi persentase (katakanlah 20%) dari jarak sebelumnya garis zig-zag, DAN nilai yang dinyatakan sebelumnya k (katakanlah 1.2)

Saya dapat menemukan ekstrema lokal menggunakan kode ini:

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

tapi saya tidak tahu bagaimana menerapkan kondisi ambang itu. Tolong beri tahu saya bagaimana menerapkan kondisi seperti itu.

Karena data dapat berisi jutaan cap waktu, perhitungan yang efisien sangat disarankan

Untuk deskripsi yang lebih jelas: masukkan deskripsi gambar di sini

Contoh output, dari data saya:

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

masukkan deskripsi gambar di sini

Output yang saya inginkan (sesuatu yang mirip dengan ini, zigzag hanya menghubungkan segmen signifikan) masukkan deskripsi gambar di sini

Thanh Nguyen
sumber

Jawaban:

3

Saya telah menjawab pemahaman terbaik saya atas pertanyaan itu. Namun tidak jelas bagaimana variabel K mempengaruhi filter.

Anda ingin memfilter ekstrema berdasarkan kondisi berlari. Saya berasumsi bahwa Anda ingin menandai semua ekstrema yang jarak relatifnya ke ekstrem yang ditandai terakhir lebih besar dari p%. Saya selanjutnya berasumsi bahwa Anda selalu menganggap elemen pertama dari deret waktu sebagai poin yang valid / relevan.

Saya menerapkan ini dengan fungsi filter berikut:

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

Untuk menjalankan kode Anda, saya pertama-tama mengimpor dependensi:

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

Untuk membuat kode dapat direproduksi, saya memperbaiki seed acak:

np.random.seed(0)

Sisanya dari sini adalah copypasta. Perhatikan bahwa saya mengurangi jumlah sampel untuk memperjelas hasilnya.

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

Kemudian kami menggunakan fungsi filter:

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

Dan plot seperti yang Anda lakukan pada plot sebelumnya dan juga ekstrema yang baru saja difilter:

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

masukkan deskripsi gambar di sini

EDIT :

Jika ingin keduanya mempertimbangkan yang pertama dan yang terakhir sebagai valid, maka Anda dapat menyesuaikan fungsi filter sebagai berikut:

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
sumber
hai, terima kasih atas jawaban yang bagus. Ya anggapan Anda benar "tandai semua ekstrema yang jarak relatifnya ke ekstrem yang ditandai terakhir lebih besar dari p%.", Dan titik pertama dan terakhir harus selalu dipertimbangkan. Saya telah memeriksa jawaban Anda, kadang-kadang tidak ada pada poin terakhir, dapatkah Anda membantu saya mengenai hal itu?
Thanh Nguyen
3

Anda dapat menggunakan fungsi rolling panda untuk membuat ekstrema lokal. Itu menyederhanakan kode sedikit dibandingkan dengan pendekatan Scipy Anda.

Fungsi untuk menemukan ekstrema:

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)

Fungsi untuk membuat zigzag, dapat diterapkan pada Dataframe sekaligus (di atas setiap kolom), tetapi ini akan memperkenalkan NaN's karena cap waktu yang dikembalikan akan berbeda untuk setiap kolom. Anda dapat dengan mudah menjatuhkan ini nanti seperti yang ditunjukkan pada contoh di bawah ini, atau cukup menerapkan fungsi pada satu kolom di Dataframe Anda.

Perhatikan bahwa saya membatalkan pengujian terhadap ambang batas k, saya tidak yakin apakah sepenuhnya memahami bagian itu dengan benar. Anda dapat memasukkannya jika perbedaan absolut antara ekstrim sebelumnya dan saat ini harus lebih besar dari k:& (ext_val.diff().abs() > k)

Saya juga tidak yakin apakah zigzag akhir harus selalu bergerak dari tinggi asli ke rendah atau sebaliknya. Saya berasumsi itu seharusnya, jika tidak, Anda dapat menghapus pencarian kedua untuk ekstrim di akhir fungsi.

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

Hasilkan beberapa data sampel:

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

Terapkan fungsi dan ekstrak hasilnya untuk kolom 'data1':

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

Visualisasikan hasilnya:

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

masukkan deskripsi gambar di sini

Kassi Rutger
sumber
Terima kasih atas jawaban anda. Saya ingin bertanya tentang garis ini (ext_val.diff().abs() > (ext_val.shift(-1).abs() * p)), seperti yang saya mengerti, Anda membandingkan jarak antara dua titik dengan p%titik terakhir, apakah saya benar? Karena saya ingin membandingkan setiap segmen zigzag dengan segmen sebelumnya, dan ulangi sampai kondisinya terpenuhi.
Thanh Nguyen