Smoothing¶
Linear and non-linear smoothing filters with explicit boundary handling.
See the smoothing guide for filter selection and padding trade-offs.
smoothing
¶
Smoothing filters for one-dimensional series.
Three filters, all pure functions that return a new list and never touch their input:
| Filter | Kind | Good at | Bad at |
|---|---|---|---|
moving_average |
linear, finite | white noise | preserving edges |
exponential_moving_average |
linear, infinite | streaming | symmetric response |
median_filter |
non-linear, finite | isolated spikes | smooth gradients |
Finite-support filters need values beyond the ends of the series. The
Padding strategy decides what those values are.
Padding
¶
Bases: StrEnum
How a finite-support filter extends a series past its boundaries.
Being a StrEnum, every member compares equal to its own name, so callers
can pass the plain string "edge" instead of importing the enum.
Attributes:
| Name | Type | Description |
|---|---|---|
EDGE |
Repeat the first and last value. Keeps the output length equal to the input length and does not pull the ends toward zero. |
|
ZERO |
Pad with |
|
REFLECT |
Mirror the series about its endpoints without repeating them.
Preserves local slope better than |
|
NONE |
Do not pad. The output is shorter than the input by
|
Examples:
>>> Padding("edge") is Padding.EDGE
True
>>> moving_average([0.0, 0.0, 9.0], window=3, padding="zero")
[0.0, 3.0, 3.0]
moving_average
¶
Smooth series with a centred, equally weighted moving average.
With w the window length and l = ⌊w/2⌋,
yᵢ = (1/w) ∑ₖ xᵢ₊ₖ, for k = −l, …, w − 1 − l.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
series
|
Series
|
The observations to smooth. |
required |
window
|
int
|
Number of samples averaged per output point. Must be positive and no larger than the series. |
required |
padding
|
Padding | str
|
Boundary strategy, see |
EDGE
|
Returns:
| Type | Description |
|---|---|
list[float]
|
A new list of smoothed values. |
Raises:
| Type | Description |
|---|---|
EmptySeriesError
|
If |
ValidationError
|
If |
Examples:
>>> moving_average([1.0, 2.0, 3.0, 4.0, 5.0], window=3)
[1.3333333333333333, 2.0, 3.0, 4.0, 4.666666666666667]
A window of 1 is the identity:
Dropping the padded ends shortens the output instead:
Source code in src/mypackage/smoothing.py
exponential_moving_average
¶
exponential_moving_average(series: Series, alpha: float) -> list[float]
Smooth series with a causal exponentially weighted moving average.
y₀ = x₀, yᵢ = α xᵢ + (1 − α) yᵢ₋₁,
with α the smoothing factor alpha.
The filter is causal: y[i] depends only on samples up to i, so it
can run on a live stream. The cost is a phase lag of roughly
(1 − α)/α samples.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
series
|
Series
|
The observations to smooth. |
required |
alpha
|
float
|
Smoothing factor in |
required |
Returns:
| Type | Description |
|---|---|
list[float]
|
A new list of smoothed values, the same length as |
Raises:
| Type | Description |
|---|---|
EmptySeriesError
|
If |
ValidationError
|
If |
Examples:
>>> exponential_moving_average([1.0, 2.0, 3.0], alpha=0.5)
[1.0, 1.5, 2.25]
>>> exponential_moving_average([1.0, 2.0, 3.0], alpha=1.0)
[1.0, 2.0, 3.0]
Source code in src/mypackage/smoothing.py
median_filter
¶
Smooth series with a centred running median.
Unlike the moving average, the median is a non-linear statistic with a breakdown point of 50%: up to half the samples in a window can be arbitrarily corrupted without moving the output. That is what makes it the right tool for isolated spikes, which a mean filter merely smears.
Uses a Window internally so the streaming and batch
code paths share one buffer implementation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
series
|
Series
|
The observations to smooth. |
required |
window
|
int
|
Number of samples per running median. |
required |
padding
|
Padding | str
|
Boundary strategy, see |
EDGE
|
Returns:
| Type | Description |
|---|---|
list[float]
|
A new list of filtered values. |
Raises:
| Type | Description |
|---|---|
EmptySeriesError
|
If |
ValidationError
|
If |
Examples:
A single spike is removed outright, not smeared across neighbours:
The moving average, by contrast, spreads the spike over three samples:
>>> [round(v, 2) for v in moving_average([1.0, 1.0, 99.0, 1.0, 1.0], 3)]
[1.0, 33.67, 33.67, 33.67, 1.0]