0
  • Edit: I have tried to incoroporate @dankal444 script. Even though it does indeed remove the high frequencies as shown in the second figure, I still get a flatter spectrum at high frequencies in the downsampled timeseries.

I have been trying to estimate the power spectral density of a timeseries using fourier transform. I have a high resolution dataset and downsample it with linear interpolation to a lower cadence. However, this results in an artificial flattening of the power spectrum. Any idea why this happens? Should I first apply low pass filter on the timeseries before downsampling?If so which one would be appropriate? Here is an example:

import datetime

# Ypou will need to ->  pip install fbm
from fbm import fbm

import numpy as np
from scipy.signal import butter, filtfilt


# Define functions
def TracePSD_2nd(x, dt):
    """ 
    Estimate Power spectral density:
    Inputs:
    u : timeseries, np.array
    dt: 1/sampling frequency
    """
    N = len(x)
    yf = np.fft.rfft(x)

    
    B_pow = abs(yf) ** 2 / N * dt

    freqs = np.fft.fftfreq(len(x), dt)
    freqs = freqs[freqs>0]
    idx   = np.argsort(freqs)
    
    return freqs[idx], B_pow[idx]

def butter_lowpass(f_cutoff, fs, order=5):
    return butter(order, f_cutoff, fs=fs, btype='low', analog=False)

def butter_lowpass_filter(data, dt, order=5):
    fs         = 1/dt
    f_nyquist  = fs / 2
    f_cutoff   = 0.999* f_nyquist
    b, a = butter_lowpass(f_cutoff, fs, order=order)
    if len(np.shape(data))>1:
        x   = filtfilt(b, a, data.T[0])
        y   = filtfilt(b, a, data.T[1])
        z   = filtfilt(b, a, data.T[2])
        res =  np.transpose(np.vstack((x,y,z)))
    else:
        res = filtfilt(b, a, data)
    return res

# User defined parameters

resolution      = 1000  # in miliseconds
init_resolution = 10    # in miliseconds

# create a sythetic timeseries using a fractional brownian motion !( In case you don't have fbm-> pip install fbm)
start_time = datetime.datetime.now()

# Create index for timeseries
end_time  = datetime.datetime.now()+ pd.Timedelta('1H')
freq      = str(init_reolution)+'ms'
index = pd.date_range(
    start = start_time, 
    end = end_time, 
    freq = freq
)

# Generate a fBm realization
fbm_sample = fbm(n=len(index), hurst=0.75, length=1, method='daviesharte')


# Create a dataframe to resample the timeseries.
df_b = pd.DataFrame({'DateTime': index, 'Br':fbm_sample[:-1]}).set_index('DateTime')



#Original version of timeseries
y = df_b.Br

# Apply filter
res = butter_lowpass_filter(y.values, dt=resolution*1e-3)

# Recreate dataframe after timeseries has been fitelered
df_b['Br'] = res  


# Resample the synthetic timeseries
x = df_b.Br.resample(str(int(resolution))+"ms").mean()


# Estimate the sampling rate
dtx  = (x.dropna().index.to_series().diff()/np.timedelta64(1, 's')).median()
dty  = (y.dropna().index.to_series().diff()/np.timedelta64(1, 's')).median()

# Estimate PSD using second method
resya = TracePSD_2nd(y, dty)
resxa = TracePSD_2nd(x, dtx)


plt.loglog(resya[0], resya[1], label ='Original timeseries')

plt.loglog(resxa[0], resxa[1], label ='Downsampled timeseries+ filter')


plt.legend()

enter image description here

enter image description here

Jokerp
  • 213
  • 1
  • 9
  • 1
    Could you remove the parts of code that aren't necessary for the question ? we only see ax[1], therefore only the function and resya and resxa is needed as far as I can tell. For these two calculations it would be helpful if you could provide some sample of your data that reproduces the problem. – Rabinzel Oct 05 '22 at 07:00
  • Ok just made the changes you suggested! Thanks – Jokerp Oct 05 '22 at 07:09
  • What do you mean by "artificial flattening of the power spectrum"? Green line is not more flat than the blue line – dankal444 Oct 05 '22 at 12:23
  • It is flatter at the highest frequencies right? At the same part of the spectrum the blue line is steeper. I think that this is caused by aliasing effect that I wanted to remove by applying some kind of low pass-filter but I am not sure how to implement – Jokerp Oct 05 '22 at 13:00

1 Answers1

1

I have been trying to estimate the power spectral density of a timeseries using fourier transform. I have a high resolution dataset and downsample it with linear interpolation to a lower cadence. However, this results in an artificial flattening of the power spectrum. Any idea why this happens?

This extra energy in high energy part is due to aliasing effect.

Should I first apply low pass filter on the timeseries before downsampling?

Almost always. If you have any energy above new Nyquist frequency you need to do low-pass filtering or you will have aliasing effect. Only if there is no energy above Nyquist freq, there is no need to do filtering - there is nothing to filter out :)

If so which one would be appropriate?

Generally, it is up to you how precise filtering you want to use. I would do with simple Butterworth, take a look here for an example.

EDIT: requested example of filtering:

# assuming you have some:
# - signal
# - cutoff (frequency)
# - fs (sampling frequency)

import numpy as np
from scipy.signal import butter, filtfilt

def butter_lowpass(cutoff, fs, order=5):
    return butter(order, cutoff, fs=fs, btype='low', analog=False)

def butter_lowpass_filter(data, cutoff, fs, order=5):
    b, a = butter_lowpass(cutoff, fs, order=order)
    y = filtfilt(b, a, data)
    return y

filtered_signal = butter_lowpass_filter(signal, cutoff, fs)
dankal444
  • 3,172
  • 1
  • 23
  • 35
  • Thank you so much for taking the time to reply. I am not familiar with this kind od filtering technique though. Does it introduce a phase shift in the data? It is important for me that the method used does not. Could you please incorporate an exampe to the code? – Jokerp Oct 05 '22 at 14:51
  • @Jokerp This method I linked will introduce phase shift. I edited it - changed it a bit - to remove the phase shift (by using `filtfilt` instead of `lfilter`) – dankal444 Oct 05 '22 at 20:53
  • Thank you so much! Is there a rule of thumb for the cutoff frequency? given for example the resolution of the downsampled timeseries? – Jokerp Oct 05 '22 at 20:56
  • It can't be bigger than Nyquist frequency (=half of sampling rate). It is good though to make it a bit smaller than Nyquist frequency (95-98%), so you do not have to use high order, very steep filter. – dankal444 Oct 05 '22 at 20:59
  • thanks and sorry for asking again but you mean this? So if dt is the samling rate of the downampled timeseries the cutoff freq should be ~0.95*1/(2*dt)? – Jokerp Oct 06 '22 at 07:24
  • 1
    yes, but to be more elegant, I would do `fs = 1/dt`, then `f_cutoff = 0.95 * fs / 2` or even add `f_nyquist = fs / 2` inbetween – dankal444 Oct 06 '22 at 07:58
  • Sorry to bother you again. I have modified the question because I still don't get the desired result. Would you mind taking a look? Thanks! – Jokerp Oct 06 '22 at 09:13
  • Hmm maybe thats due to the fact that beginning of the time signal does not match the end of it. Fourier Transform "assumes" you provide a period of infinite signal - if it is not the case you need to either use [windowing](https://www.egr.msu.edu/classes/me451/me451_labs/Fall_2013/Understanding_FFT_Windows.pdf) or disregard those high frequencies (not analyse them or zero them) – dankal444 Oct 06 '22 at 10:08
  • putting the cutoff at 95% of Nyquist requires an aggressive filter, and is probably too close for this application. 85% or 90% would be more reasonable. You should NOT expect a straight line down to the Nyquist frequency in the results. You should be able to see the filter cutoff before *and after* downsampling. – Matt Timmermans Oct 08 '22 at 13:37