I have quite a noisy data, and I am trying to work out a high and low envelope to the signal. It is kinda of like this example in MATLAB:
http://uk.mathworks.com/hel
First attempt was to make use of scipy Hilbert transform to determine the amplitude envelope but this didn't work as expected in many cases, mainly reason because, citing from this digital signal processing answer:
Hilbert envelope, also called Energy-Time Curve (ETC), only works well for narrow-band fluctuations. Producing an analytic signal, of which you later take the absolute value, is a linear operation, so it treats all frequencies of your signal equally. If you give it a pure sine wave, it will indeed return to you a straight line. When you give it white noise however, you will likely get noise back.
From then, since the other answers were using cubic spline interpolation and did tend to become cumbersome, a bit unstable (spurious oscillations) and time consuming for very long and noisy data arrays, I will contribute here with a simple and numpy efficient version that seems to work pretty fine:
import numpy as np
from matplotlib import pyplot as plt
def hl_envelopes_idx(s,dmin=1,dmax=1):
"""
Input :
s : 1d-array, data signal from which to extract high and low envelopes
dmin, dmax : int, size of chunks, use this if size of data is too big
Output :
lmin,lmax : high/low enveloppe idx of signal s
"""
# locals min
lmin = (np.diff(np.sign(np.diff(s))) > 0).nonzero()[0] + 1
# locals max
lmax = (np.diff(np.sign(np.diff(s))) < 0).nonzero()[0] + 1
"""
# using the following might help in some case by cutting the signal in "half" along y-axis
s_mid = np.mean(s) (0 if s centered around x-axis or more generally mean of signal)
# pre-sorting of locals min based on sign
lmin = lmin[s[lmin]s_mid]
"""
# global max of dmax-chunks of locals max
lmin = lmin[[i+np.argmin(s[lmin[i:i+dmin]]) for i in range(0,len(lmin),dmin)]]
# global min of dmin-chunks of locals min
lmax = lmax[[i+np.argmax(s[lmax[i:i+dmax]]) for i in range(0,len(lmax),dmax)]]
return lmin,lmax
Example 1: quasi-periodic vibration
t = np.linspace(0,8*np.pi,5000)
s = 0.8*np.cos(t)**3 + 0.5*np.sin(np.exp(1)*t)
high_idx, low_idx = hl_envelopes_idx(s)
# plot
plt.plot(t,s,label='signal')
plt.plot(t[high_idx], s[high_idx], 'r', label='low')
plt.plot(t[low_idx], s[low_idx], 'g', label='high')
Example 2: noisy decaying signal
t = np.linspace(0,2*np.pi,5000)
s = 5*np.cos(5*t)*np.exp(-t) + np.random.rand(len(t))
high_idx, low_idx = hl_envelopes_idx(s,dmin=15,dmax=15)
# plot
plt.plot(t,s,label='signal')
plt.plot(t[high_idx], s[high_idx], 'r', label='low')
plt.plot(t[low_idx], s[low_idx], 'g', label='high')
Example 3: nonsymmetric modulated chirp
A much more complex signal of 18867925
samples (which isn't included here):