A robust approach for post process filtering without edge effects (or minimum edge effects) is to combine forward-backward filtering with mirror padding. The function filtfilt available in MATLAB, Octave and Python implements a zero phase "forward backward" filter, resulting in no time offsets while using the prescribed filter twice (once in the forward direction and once in the reverse direction and thus cancelling the filters own delay).
However filtfilt used on its own will have edge effects due to Gibbs-like ringing near the edges due to the abupt change in the samples there. This is reduced by appending additional samples to the front and end prior to doing the filtering. Mirror padding reflects the existing data without duplicating the edge values, and is simple and robust to use, but linear extrapolation can also be used as an alternative solution (better when signals tend to be linear at the edges such as the OP's case here).
Either way, below shows an example filtering implementation in Python which should provide an excellent result for this application (choose the cutoff frequency based on smoothing desired while not losing the true curvature of the waveform):
nyq = 0.5 * fs # fs is sampling rate in Hz normalized_cutoff = cutoff / nyq # choose a cutoff freq in Hz # filter coefficients, Butterworth in honor of my friend Hilmar b, a = butter(order, norm_cutoff, btype='low', analog=False # mirroring at edges (or do linear extrapolation: try both and compare!) pad_length = 5 * max(len(a), len(b)) data_padded = np.pad(data, pad_width=pad_length, mode='reflect') filtered = filtfilt(b, a, data_padded) # remove padding filtered = filtered[pad_length: -pad_length]
As mentioned, a linear extension would provide even better results for padding the signal such that both the value and first derivative are continuous. Below shows the code that could be used in place of the one np.pad() line above:
left_slope = data[1] - data[0] right_slope = data[-1] - data[-2] left_pad = data[0] - left_slope * np.arange(pad_width, 0, -1) right_pad = data[-1] + right_slope * np.arange(1, pad_width + 1) data_padded = np.concatenate([left_pad, data, right_pad])
NOTE: The Savitksy-Golay filter provided by Python (scipy.signal.savgol_filter) provides mirror padding as a direct option in its impelmentation (see doc here for parameter mode: https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.savgol_filter.html ) and for the same reasons given above can be instead combined with the custom derivative-matched padding for explicit control over the edge behavior:
window_length = 2 * pad_width # enter order desired for filter smoothed = scipy.signal.savgol_filter(data_padded, window_length, order) smoothed = smoothed[pad_width:-pad_width]