# Source code for heartpy.filtering

```'''
'''

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

__all__ = ['filter_signal',
'hampel_filter',
'hampel_correcter',
'smooth_signal']

def butter_lowpass(cutoff, sample_rate, order=2):
'''standard lowpass filter.

Function that defines standard Butterworth lowpass filter

Parameters
----------
cutoff : int or float
frequency in Hz that acts as cutoff for filter.
All frequencies above cutoff are filtered out.

sample_rate : int or float
sample rate of the supplied signal

order : int
filter order, defines the strength of the roll-off
around the cutoff frequency. Typically orders above 6
are not used frequently.
default: 2

Returns
-------
out : tuple
numerator and denominator (b, a) polynomials
of the defined Butterworth IIR filter.

Examples
--------
>>> b, a = butter_lowpass(cutoff = 2, sample_rate = 100, order = 2)
>>> b, a = butter_lowpass(cutoff = 4.5, sample_rate = 12.5, order = 5)
'''
nyq = 0.5 * sample_rate
normal_cutoff = cutoff / nyq
b, a = butter(order, normal_cutoff, btype='low', analog=False)
return b, a

def butter_highpass(cutoff, sample_rate, order=2):
'''standard highpass filter.

Function that defines standard Butterworth highpass filter

Parameters
----------
cutoff : int or float
frequency in Hz that acts as cutoff for filter.
All frequencies below cutoff are filtered out.

sample_rate : int or float
sample rate of the supplied signal

order : int
filter order, defines the strength of the roll-off
around the cutoff frequency. Typically orders above 6
are not used frequently.
default : 2

Returns
-------
out : tuple
numerator and denominator (b, a) polynomials
of the defined Butterworth IIR filter.

Examples
--------
we can specify the cutoff and sample_rate as ints or floats.

>>> b, a = butter_highpass(cutoff = 2, sample_rate = 100, order = 2)
>>> b, a = butter_highpass(cutoff = 4.5, sample_rate = 12.5, order = 5)
'''
nyq = 0.5 * sample_rate
normal_cutoff = cutoff / nyq
b, a = butter(order, normal_cutoff, btype='high', analog=False)
return b, a

def butter_bandpass(lowcut, highcut, sample_rate, order=2):
'''standard bandpass filter.
Function that defines standard Butterworth bandpass filter.
Filters out frequencies outside the frequency range
defined by [lowcut, highcut].

Parameters
----------
lowcut : int or float
Lower frequency bound of the filter in Hz

highcut : int or float
Upper frequency bound of the filter in Hz

sample_rate : int or float
sample rate of the supplied signal

order : int
filter order, defines the strength of the roll-off
around the cutoff frequency. Typically orders above 6
are not used frequently.
default : 2

Returns
-------
out : tuple
numerator and denominator (b, a) polynomials
of the defined Butterworth IIR filter.

Examples
--------
we can specify lowcut, highcut and sample_rate as ints or floats.

>>> b, a = butter_bandpass(lowcut = 1, highcut = 6, sample_rate = 100, order = 2)
>>> b, a = butter_bandpass(lowcut = 0.4, highcut = 3.7, sample_rate = 72.6, order = 2)
'''
nyq = 0.5 * sample_rate
low = lowcut / nyq
high = highcut / nyq
b, a = butter(order, [low, high], btype='band')
return b, a

[docs]def filter_signal(data, cutoff, sample_rate, order=2, filtertype='lowpass',
return_top = False):
'''Apply the specified filter

Function that applies the specified lowpass, highpass or bandpass filter to
the provided dataset.

Parameters
----------
data : 1-dimensional numpy array or list
Sequence containing the to be filtered data

cutoff : int, float or tuple
the cutoff frequency of the filter. Expects float for low and high types
and for bandpass filter expects list or array of format [lower_bound, higher_bound]

sample_rate : int or float
the sample rate with which the passed data sequence was sampled

order : int
the filter order
default : 2

filtertype : str
The type of filter to use. Available:
- lowpass : a lowpass butterworth filter
- highpass : a highpass butterworth filter
- bandpass : a bandpass butterworth filter
- notch : a notch filter around specified frequency range
both the highpass and notch filter are useful for removing baseline wander. The notch
filter is especially useful for removing baseling wander in ECG signals.

Returns
-------
out : 1d array
1d array containing the filtered data

Examples
--------
>>> import numpy as np
>>> import heartpy as hp

Using standard data provided

We can filter the signal, for example with a lowpass cutting out all frequencies
of 5Hz and greater (with a sloping frequency cutoff)

>>> filtered = filter_signal(data, cutoff = 5, sample_rate = 100.0, order = 3, filtertype='lowpass')
>>> print(np.around(filtered[0:6], 3))
[530.175 517.893 505.768 494.002 482.789 472.315]

Or we can cut out all frequencies below 0.75Hz with a highpass filter:

>>> filtered = filter_signal(data, cutoff = 0.75, sample_rate = 100.0, order = 3, filtertype='highpass')
>>> print(np.around(filtered[0:6], 3))
[-17.975 -28.271 -38.609 -48.992 -58.422 -67.902]

Or specify a range (here: 0.75 - 3.5Hz), outside of which all frequencies
are cut out.

>>> filtered = filter_signal(data, cutoff = [0.75, 3.5], sample_rate = 100.0,
... order = 3, filtertype='bandpass')
>>> print(np.around(filtered[0:6], 3))
[-12.012 -23.159 -34.261 -45.12  -55.541 -65.336]

A 'Notch' filtertype is also available (see remove_baseline_wander).

>>> filtered = filter_signal(data, cutoff = 0.05, sample_rate = 100.0, filtertype='notch')

Finally we can use the return_top flag to only return the filter response that
has amplitute above zero. We're only interested in the peaks, and sometimes
this can improve peak prediction:

>>> filtered = filter_signal(data, cutoff = [0.75, 3.5], sample_rate = 100.0,
... order = 3, filtertype='bandpass', return_top = True)
>>> print(np.around(filtered[48:53], 3))
[ 0.     0.     0.409 17.088 35.673]
'''
if filtertype.lower() == 'lowpass':
b, a = butter_lowpass(cutoff, sample_rate, order=order)
elif filtertype.lower() == 'highpass':
b, a = butter_highpass(cutoff, sample_rate, order=order)
elif filtertype.lower() == 'bandpass':
assert type(cutoff) == tuple or list or np.array, 'if bandpass filter is specified, \
cutoff needs to be array or tuple specifying lower and upper bound: [lower, upper].'
b, a = butter_bandpass(cutoff, cutoff, sample_rate, order=order)
elif filtertype.lower() == 'notch':
b, a = iirnotch(cutoff, Q = 0.005, fs = sample_rate)
else:
raise ValueError('filtertype: %s is unknown, available are: \
lowpass, highpass, bandpass, and notch' %filtertype)

filtered_data = filtfilt(b, a, data)

if return_top:
return np.clip(filtered_data, a_min = 0, a_max = None)
else:
return filtered_data

[docs]def remove_baseline_wander(data, sample_rate, cutoff=0.05):
'''removes baseline wander

Function that uses a Notch filter to remove baseline
wander from (especially) ECG signals

Parameters
----------
data : 1-dimensional numpy array or list
Sequence containing the to be filtered data

sample_rate : int or float
the sample rate with which the passed data sequence was sampled

cutoff : int, float
the cutoff frequency of the Notch filter. We recommend 0.05Hz.
default : 0.05

Returns
-------
out : 1d array
1d array containing the filtered data

Examples
--------
>>> import heartpy as hp

baseline wander is removed by calling the function and specifying
the data and sample rate.

>>> filtered = remove_baseline_wander(data, 100.0)
'''

return filter_signal(data = data, cutoff = cutoff, sample_rate = sample_rate,
filtertype='notch')

[docs]def hampel_filter(data, filtsize=6):
'''Detect outliers based on hampel filter

Funcion that detects outliers based on a hampel filter.
The filter takes datapoint and six surrounding samples.
Detect outliers based on being more than 3std from window mean.
See:
https://www.mathworks.com/help/signal/ref/hampel.html

Parameters
----------
data : 1d list or array
list or array containing the data to be filtered

filtsize : int
the filter size expressed the number of datapoints
taken surrounding the analysed datapoint. a filtsize
of 6 means three datapoints on each side are taken.
total filtersize is thus filtsize + 1 (datapoint evaluated)

Returns
-------
out :  array containing filtered data

Examples
--------
>>> from .datautils import get_data, load_exampledata
>>> filtered = hampel_filter(data, filtsize = 6)
>>> print('%i, %i' %(data, filtered))
497, 496
'''

#generate second list to prevent overwriting first
#cast as array to be sure, in case list is passed
output = np.copy(np.asarray(data))
onesided_filt = filtsize // 2
for i in range(onesided_filt, len(data) - onesided_filt - 1):
dataslice = output[i - onesided_filt : i + onesided_filt]
median = np.median(dataslice)
if output[i] > median + (3 * mad):
output[i] = median
return output

[docs]def hampel_correcter(data, sample_rate):
'''apply altered version of hampel filter to suppress noise.

Function that returns te difference between data and 1-second
windowed hampel median filter. Results in strong noise suppression
characteristics, but relatively expensive to compute.

Result on output measures is present but generally not large. However,
use sparingly, and only when other means have been exhausted.

Parameters
----------
data : 1d numpy array
array containing the data to be filtered

sample_rate : int or float
sample rate with which data was recorded

Returns
-------
out : 1d numpy array
array containing filtered data

Examples
--------
>>> from .datautils import get_data, load_exampledata
>>> filtered = hampel_correcter(data, sample_rate = 116.995)

'''

return data - hampel_filter(data, filtsize=int(sample_rate))

def quotient_filter(RR_list, RR_list_mask = [], iterations=2):
'''applies a quotient filter

Function that applies a quotient filter as described in
"Piskorki, J., Guzik, P. (2005), Filtering Poincare plots"

Parameters
----------
RR_list - 1d array or list
array or list of peak-peak intervals to be filtered

RR_list_mask - 1d array or list
array or list containing the mask for which intervals are
rejected. If not supplied, it will be generated. Mask is
zero for accepted intervals, one for rejected intervals.

iterations - int
how many times to apply the quotient filter. Multipled
iterations have a stronger filtering effect
default : 2

Returns
-------
mask for RR_list, 1 where intervals are rejected, 0 where
intervals are accepted.

Examples
--------
Given some example data let's generate an RR-list first
>>> import heartpy as hp
>>> sample_rate = hp.get_samplerate_mstimer(timer)
>>> wd, m = hp.process(data, sample_rate)
>>> rr = wd['RR_list']

Given this data we can use this function to further clean the data:

Although specifying the mask is optional, as you may not always have a

'''

else:
'error: RR_list and RR_list_mask should be same length if RR_list_mask is specified'

for iteration in range(iterations):
for i in range(len(RR_list) - 1):
pass #skip if one of both intervals is already rejected
elif 0.8 <= RR_list[i] / RR_list[i + 1] <= 1.2:
pass #if R-R pair seems ok, do noting

[docs]def smooth_signal(data, sample_rate, window_length=None, polyorder=3):
'''smooths given signal using savitzky-golay filter

Function that smooths data using savitzky-golay filter using default settings.

Functionality requested by Eirik Svendsen. Added since 1.2.4

Parameters
----------
data : 1d array or list
array or list containing the data to be filtered

sample_rate : int or float
the sample rate with which data is sampled

window_length : int or None
window length parameter for savitzky-golay filter, see Scipy.signal.savgol_filter docs.
Must be odd, if an even int is given, one will be added to make it uneven.
default : 0.1  * sample_rate

polyorder : int
the order of the polynomial fitted to the signal. See scipy.signal.savgol_filter docs.
default : 3

Returns
-------
smoothed : 1d array
array containing the smoothed data

Examples
--------
Given a fictional signal, a smoothed signal can be obtained by smooth_signal():

>>> x = [1, 3, 4, 5, 6, 7, 5, 3, 1, 1]
>>> smoothed = smooth_signal(x, sample_rate = 2, window_length=4, polyorder=2)
>>> np.around(smoothed[0:4], 3)
array([1.114, 2.743, 4.086, 5.   ])

If you don't specify the window_length, it is computed to be 10% of the
sample rate (+1 if needed to make odd)
>>> import heartpy as hp
>>> smoothed = smooth_signal(data, sample_rate = 100)

'''

if window_length == None:
window_length = sample_rate // 10

if window_length % 2 == 0 or window_length == 0: window_length += 1

smoothed = savgol_filter(data, window_length = window_length,
polyorder = polyorder)

return smoothed
```