[英]Pandas: Zigzag segmentation of data based on local minima-maxima
我有一個時間序列數據。 生成數據
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')
我已經回答了我對這個問題的最佳理解。 然而,尚不清楚變量 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
您可以使用 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)
創建之字形的函數,它可以一次應用於數據幀(在每一列上),但這將引入 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()
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.