@@ -142,7 +142,9 @@ def __init__(
142
142
if smoother_name == "savgol" :
143
143
# The polynomial fitting is done on a past window of size window_length
144
144
# including the current day value.
145
- self .coeffs = self .savgol_coeffs (- self .window_length + 1 , 0 )
145
+ self .coeffs = self .savgol_coeffs (
146
+ - self .window_length + 1 , 0 , self .poly_fit_degree
147
+ )
146
148
else :
147
149
self .coeffs = None
148
150
@@ -279,30 +281,35 @@ def left_gauss_linear_smoother(self, signal):
279
281
signal_smoothed [signal_smoothed <= self .minval ] = self .minval
280
282
return signal_smoothed
281
283
282
- def savgol_predict (self , signal ):
284
+ def savgol_predict (self , signal , poly_fit_degree , nr ):
283
285
"""Predict a single value using the savgol method.
284
286
285
287
Fits a polynomial through the values given by the signal and returns the value
286
- of the polynomial at the right-most signal-value. More precisely, fits a polynomial
287
- f(t) of degree poly_fit_degree through the points signal[-n], signal[-n+1] ..., signal[-1],
288
- and returns the evaluation of the polynomial at the location of signal[0].
288
+ of the polynomial at the right-most signal-value. More precisely, for a signal of length
289
+ n, fits a poly_fit_degree polynomial through the points signal[-n+1+nr], signal[-n+2+nr],
290
+ ..., signal[nr], and returns the evaluation of the polynomial at signal[0]. Hence, if
291
+ nr=0, then the last value of the signal is smoothed, and if nr=-1, then the value after
292
+ the last signal value is anticipated.
289
293
290
294
Parameters
291
295
----------
292
296
signal: np.ndarray
293
297
A 1D signal to smooth.
298
+ poly_fit_degree: int
299
+ The degree of the polynomial fit.
300
+ nr: int
301
+ An integer that determines the position of the predicted value relative to the signal.
294
302
295
303
Returns
296
304
----------
297
305
predicted_value: float
298
306
The anticipated value that comes after the end of the signal based on a polynomial fit.
299
307
"""
300
- # Add one
301
- coeffs = self .savgol_coeffs (- len (signal ) + 1 , 0 )
308
+ coeffs = self .savgol_coeffs (- len (signal ) + 1 + nr , nr , poly_fit_degree )
302
309
predicted_value = signal @ coeffs
303
310
return predicted_value
304
311
305
- def savgol_coeffs (self , nl , nr ):
312
+ def savgol_coeffs (self , nl , nr , poly_fit_degree ):
306
313
"""Solve for the Savitzky-Golay coefficients.
307
314
308
315
The coefficients c_i give a filter so that
@@ -322,23 +329,20 @@ def savgol_coeffs(self, nl, nr):
322
329
The right window bound for the polynomial fit, inclusive.
323
330
poly_fit_degree: int
324
331
The degree of the polynomial to be fit.
325
- gaussian_bandwidth: float or None
326
- If float, performs regression with Gaussian weights whose variance is
327
- the gaussian_bandwidth. If None, performs unweighted regression.
328
332
329
333
Returns
330
334
----------
331
335
coeffs: np.ndarray
332
- A vector of coefficients of length nl that determines the savgol
336
+ A vector of coefficients of length nr - nl + 1 that determines the savgol
333
337
convolution filter.
334
338
"""
335
339
if nl >= nr :
336
340
raise ValueError ("The left window bound should be less than the right." )
337
341
if nr > 0 :
338
- raise warnings .warn ("The filter is no longer causal." )
342
+ warnings .warn ("The filter is no longer causal." )
339
343
340
- A = np .vstack ( # pylint: disable=invalid-name
341
- [np .arange (nl , nr + 1 ) ** j for j in range (self . poly_fit_degree + 1 )]
344
+ A = np .vstack (
345
+ [np .arange (nl , nr + 1 ) ** j for j in range (poly_fit_degree + 1 )]
342
346
).T
343
347
344
348
if self .gaussian_bandwidth is None :
@@ -389,8 +393,10 @@ def savgol_smoother(self, signal):
389
393
# At the very edge, the design matrix is often singular, in which case
390
394
# we just fall back to the raw signal
391
395
try :
392
- signal_smoothed [ix ] = self .savgol_predict (signal [:ix + 1 ])
393
- except np .linalg .LinAlgError :
396
+ signal_smoothed [ix ] = self .savgol_predict (
397
+ signal [: ix + 1 ], self .poly_fit_degree , 0
398
+ )
399
+ except np .linalg .LinAlgError : # for small ix, the design matrix is singular
394
400
signal_smoothed [ix ] = signal [ix ]
395
401
return signal_smoothed
396
402
elif self .boundary_method == "identity" :
@@ -403,19 +409,13 @@ def savgol_smoother(self, signal):
403
409
def savgol_impute (self , signal ):
404
410
"""Impute the nan values in signal using savgol.
405
411
406
- This method fills the nan values in the signal with an M-degree polynomial fit
412
+ This method fills the nan values in the signal with a quadratic polynomial fit
407
413
on a rolling window of the immediate past up to window_length data points.
408
414
409
- In the case of a single data point in the past, the single data point is
410
- continued. In the case of no data points in the past (i.e. the signal starts
411
- with nan), an error is raised.
415
+ A number of boundary cases are handled involving nan filling close to the boundary.
412
416
413
417
Note that in the case of many adjacent nans, the method will use previously
414
- imputed values to do the fitting for later values. E.g. for
415
- >>> x = np.array([1.0, 2.0, np.nan, 1.0, np.nan])
416
- the last np.nan will be fit on np.array([1.0, 2.0, *, 1.0]), where * is the
417
- result of imputing based on np.array([1.0, 2.0]) (depends on the savgol
418
- settings).
418
+ imputed values to do the fitting for later values.
419
419
420
420
Parameters
421
421
----------
@@ -428,20 +428,27 @@ def savgol_impute(self, signal):
428
428
An imputed 1D signal.
429
429
"""
430
430
signal_imputed = np .copy (signal )
431
- for ix in np .where (np .isnan (signal ))[0 ]:
431
+ for ix in np .where (np .isnan (signal_imputed ))[0 ]:
432
432
# Boundary cases
433
433
if ix < self .window_length :
434
- # A nan following a single value should just be extended
434
+ # At the boundary, a single value should just be extended
435
435
if ix == 1 :
436
- signal_imputed [ix ] = signal_imputed [0 ]
437
- # Otherwise, use savgol fitting
436
+ signal_imputed [ix ] = signal_imputed [ix - 1 ]
437
+ # Reduce the polynomial degree if needed
438
+ elif ix == 2 :
439
+ signal_imputed [ix ] = self .savgol_predict (
440
+ signal_imputed [:ix ], 1 , - 1
441
+ )
442
+ # Otherwise, use savgol fitting on the largest window prior
438
443
else :
439
- coeffs = self .savgol_coeffs (- ix , - 1 )
440
- signal_imputed [ix ] = signal_imputed [:ix ] @ coeffs
441
- # Use a polynomial fit on the past window length to impute
444
+ signal_imputed [ix ] = self .savgol_predict (
445
+ signal_imputed [:ix ], self .poly_fit_degree , - 1
446
+ )
447
+ # Away from the boundary, use savgol fitting on a fixed window
442
448
else :
443
- coeffs = self .savgol_coeffs (- self .window_length , - 1 )
444
- signal_imputed [ix ] = (
445
- signal_imputed [ix - self .window_length : ix ] @ coeffs
449
+ signal_imputed [ix ] = self .savgol_predict (
450
+ signal_imputed [ix - self .window_length : ix ],
451
+ self .poly_fit_degree ,
452
+ - 1 ,
446
453
)
447
454
return signal_imputed
0 commit comments