Kalman filters are very interesting as it fits any kind of curve and you can fine-tune them to adjust the right noise reduction.
import numpy as np
import pylab as pl
from pykalman import KalmanFilter
#Some data with noise
rnd = np.random.RandomState(0)
n_timesteps = 100
x = np.linspace(0, 3 * np.pi, n_timesteps)
y = 20 * (np.sin(x) + 0.2 * rnd.randn(n_timesteps))
pl.figure(figsize=(16, 6))
pl.scatter(x,y, marker='x', color='b', label='observations')
pl.xlabel('time')
pl.ylabel('Position')
pl.show()

The noise reduction is defined by the covariance and the transition matrix. A 1D matrix would be a linear simplification and would fit for simple shapes, whereas a 2D matrix would be for non linear simplifaction for more complex shapes.
#Here you define the smoothing strength
#High smoothing in green
kf_high = KalmanFilter(transition_matrices=np.array([1]),
transition_covariance=0.06)
#Low smoothing in blue
kf_low = KalmanFilter(transition_matrices=np.array([[1, 1], [0, 1]]),
transition_covariance=0.02 * np.eye(2))
states_pred_high = kf_high.em(y).smooth(y)[0]
states_pred_low = kf_low.em(y).smooth(y)[0]
pl.figure(figsize=(16, 6))
position_line = pl.plot(x, states_pred_high[:, 0],markersize=2,
linestyle='-', marker='o', color='b',
label='position est.')
obs_scatter = pl.scatter(x, y, marker='x', color='b',
label='observations')
position_line = pl.plot(x, states_pred_low[:, 0],markersize=2,
linestyle='-', marker='o', color='g',
label='position est.')
pl.legend(loc='lower left',fontsize=14)
pl.xlim(xmin=0, xmax=x.max())
pl.xlabel('time')
pl.show()

See: https://medium.com/dataman-in-ai/kalman-filter-explained-4d65b47916bf