Pandas: การแบ่งส่วนข้อมูลเป็นซิกแซกตามท้องถิ่น minima-maxima


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

ฉันต้องการสร้างเส้นซิกแซกเชื่อมต่อระหว่าง maxima ท้องถิ่นและ minima ท้องถิ่นที่ตรงตามเงื่อนไขที่บนแกน y |highest - lowest value|ของแต่ละบรรทัดซิกแซกต้องเกินเปอร์เซ็นต์ (พูด 20%) ของระยะก่อนหน้า เส้นซิกแซกและค่าที่ระบุไว้ล่วงหน้า k (พูด 1.2)

ฉันสามารถหา extrema ท้องถิ่นโดยใช้รหัสนี้:

# 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 มีอิทธิพลต่อตัวกรองอย่างไร

คุณต้องการกรอง extrema ตามเงื่อนไขการทำงาน ฉันคิดว่าคุณต้องการทำเครื่องหมาย extrema ทั้งหมดที่มีระยะห่างสัมพัทธ์กับextremum ที่ทำเครื่องหมายล่าสุดนั้นใหญ่กว่า 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]

และพล็อตตามที่คุณทำทั้งพล็อตก่อนหน้าของคุณรวมถึง extrema ที่กรองใหม่:

 # 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

สวัสดีขอบคุณสำหรับคำตอบที่ดี ใช่ข้อสันนิษฐานของคุณถูกต้อง "ทำเครื่องหมาย extrema ทั้งหมดที่มีระยะทางสัมพัทธ์กับ extremum ที่ทำเครื่องหมายล่าสุดนั้นใหญ่กว่า p%" และควรพิจารณาทั้งจุดแรกและจุดสุดท้ายเสมอ ฉันได้ตรวจสอบคำตอบของคุณแล้วบางครั้งก็พลาดจุดสุดท้ายคุณช่วยฉันได้ไหม
Thanh Nguyen

3

คุณสามารถใช้ฟังก์ชั่นการรีดแพนด้าของการสร้าง extrema ท้องถิ่น ซึ่งจะทำให้รหัสง่ายขึ้นเล็กน้อยเมื่อเทียบกับวิธีการของ Scipy

ฟังก์ชั่นเพื่อค้นหา extrema:

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's เนื่องจากการประทับเวลาที่ส่งคืนจะแตกต่างกันสำหรับแต่ละคอลัมน์ คุณสามารถวางสิ่งเหล่านี้ในภายหลังได้อย่างง่ายดายดังแสดงในตัวอย่างด้านล่างหรือเพียงแค่ใช้ฟังก์ชั่นกับคอลัมน์เดียวใน 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%จุดสุดท้ายใช่ไหม? เนื่องจากฉันต้องการเปรียบเทียบแต่ละส่วนที่คดเคี้ยวไปมากับส่วนก่อนหน้าและทำซ้ำจนกว่าจะพอใจเงื่อนไข
Thanh Nguyen
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.