Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Guided tour — every feature on one dataset

Open In Colab

mypackage is the worked example shipped with this project template: a dependency-free library for descriptive statistics, smoothing, and composable transforms over one-dimensional numeric series. This notebook runs through the whole public API on a single synthetic dataset, so you can see how the three layers fit together.

What you’ll learn:

  1. Batch statistics with summarize, and streaming statistics with RunningStats.

  2. The three smoothing filters and how their failure modes differ.

  3. The fit / apply lifecycle, and why Pipeline fits sequentially.

  4. Writing a transform of your own that composes with the built-ins.

  5. The typed exception hierarchy.

Background

The library is organised in three layers, each usable on its own:

LayerStyleEntry points
Statisticspure functions + one accumulatorsummarize, quantile, zscores, RunningStats
Smoothingpure functionsmoving_average, exponential_moving_average, median_filter
Transformsobjects with fit / applyStandardize, MinMaxScale, Clip, MovingAverage, Pipeline

Everything operates on any Sequence[float] — a list, a tuple, a range — and every function returns a new list. Nothing is ever mutated in place, which is what makes the layers safe to compose in any order.

Setup

Detect Colab and install the package only when running there. Local and CI users with the environment already set up skip the install and go straight to the imports.

Print an explicit version and platform readout so reproducing this notebook later is unambiguous.

Python implementation: CPython
Python version       : 3.12.3
IPython version      : 9.10.0

numpy     : 2.4.4
matplotlib: 3.11.0
mypackage : 0.1.6

Compiler    : GCC 13.3.0
OS          : Linux
Release     : 6.18.44-fc-v37
Machine     : x86_64
Processor   : x86_64
CPU cores   : 4
Architecture: 64bit

A dataset with every pathology

We build a signal that exercises each part of the API: a slow trend, additive Gaussian noise, and three isolated spikes. NumPy is used here only to generate the data — mypackage itself never sees an array and has no NumPy dependency.

<Figure size 1200x500 with 1 Axes>

Layer 1 — descriptive statistics

summarize makes one pass and returns a frozen Summary dataclass. Being frozen means it is immutable, hashable, and comparable, so it is safe to cache or use as a test fixture.

count    240
mean     1.6543
median   1.5670
std      1.3264
min/max  -0.4215 / 10.8663
spread   11.2878

Notice how far apart the mean and the median are: that gap is the three spikes pulling the mean upward while leaving the median essentially untouched. This is the same robustness argument that makes the median filter useful later on.

to_dict adds the derived properties so the summary is directly serialisable or renderable as a table.

count         240.0000
ddof            1.0000
maximum        10.8663
mean            1.6543
median          1.5670
minimum        -0.4215
spread         11.2878
std             1.3264
variance        1.7595

Quantiles use linear interpolation between order statistics, matching NumPy’s default method="linear".

     q     mypackage         numpy      |diff|
----------------------------------------------
  0.00     -0.421515     -0.421515    0.00e+00
  0.05      0.016602      0.016602    3.47e-18
  0.25      0.820235      0.820235    1.11e-16
  0.50      1.567024      1.567024    0.00e+00
  0.75      2.403014      2.403014    0.00e+00
  0.95      3.150650      3.150650    0.00e+00
  1.00     10.866258     10.866258    0.00e+00

Streaming statistics

When the data arrives incrementally — or does not fit in memory — RunningStats folds one observation at a time into a running mean and sum of squared deviations using Welford’s algorithm, in constant space.

streaming mean     1.654343193212
batch mean         1.654343193212
streaming variance 1.759464826283
batch variance     1.759464826283
<Figure size 1200x500 with 2 Axes>

The three visible jumps are the spikes entering the accumulator.

Why Welford rather than the textbook formula

The naive identity s2=(∑xi2−(∑xi)2/N)/(N−1)s^2 = \left(\sum x_i^2 - (\sum x_i)^2 / N\right) / (N-1) subtracts two nearly equal large numbers. Offset a tiny series by 109 and it loses every significant digit — and returns a negative variance, which is not a number a variance can be.

exact answer        1.666666666667
mypackage.summarize 1.666666666667
RunningStats        1.666666666667
naive formula       0.000000000000   <-- wrong sign, wrong magnitude

Layer 2 — smoothing

Three filters, each with a different failure mode. The deep-dive notebook explores them properly; here we just see all three side by side.

<Figure size 1800x500 with 3 Axes>

The mean filter spreads each spike over nine samples; the median filter deletes them outright; the exponential filter attenuates them but lags the trend. Quantifying that:

filter                    RMSE vs trend
---------------------------------------
raw                              0.9368
moving_average(9)                0.3160
median_filter(9)                 0.1219
ewma(0.15)                       0.3487

Layer 3 — transforms

A transform is any object with an apply method. Stateful ones — Standardize, MinMaxScale — learn from data in fit and raise NotFittedError if used before that.

NotFittedError: Standardize is not fitted; call .fit(series) first
learned mean = 1.6543, learned std = 1.3264

Keeping fit and apply separate is what prevents leakage: the statistics are learned once, on a reference split, and reused everywhere else.

fitted on train only: mean=1.3028, std=1.3319
if refitted on test:  mean=2.3575, std=1.0009

Those differ — which is exactly the information that would leak.

inverse round-trips back to the original scale.

largest round-trip error: 4.44e-16

Pipelines

Pipeline applies steps left to right, and fits each step on the output of the previous one — exactly the data that step will see at apply time.

steps: ['Clip', 'MovingAverage', 'Standardize']
output mean 1.42e-16, std 1.0000
<Figure size 1200x700 with 2 Axes>

Order matters, and the sequential fit is why. If Standardize were fitted on the raw series instead of on the clipped, smoothed one, its learned standard deviation would be inflated by the very spikes the earlier steps exist to remove.

std learned after Clip + MovingAverage : 0.9359
std learned on the raw series          : 1.3264

Pipelines compose with |, and neither operand is mutated — you always get a new pipeline.

original: 3 steps, extended: 4 steps

Writing your own transform

The Transform protocol is structural, so a user-defined class satisfies it without inheriting from anything or registering anywhere.

isinstance(Detrend(31), mp.Transform) -> True
<Figure size 1200x500 with 1 Axes>
residual mean 0.0541
residual std  0.5766

Error handling

Every failure raises a typed exception. All of them derive from MypackageError, and each also derives from the closest standard-library exception, so code written against the stdlib keeps working.

empty series         EmptySeriesError     'series' must contain at least one value, got 0
non-numeric input    ValidationError      'series' must contain only numbers, got 'two' at index 1
window too large     ValidationError      'window' must not exceed the series length, got window=99 for 2 value(s)
alpha out of range   ValidationError      'alpha' must lie in (0, 1], got 2.0
constant series      ValidationError      cannot standardise a constant series (std == 0)
unfitted transform   NotFittedError       Standardize is not fitted; call .fit(series) first
bad pipeline step    ValidationError      pipeline step 0 does not implement Transform.apply, got 'not a transform'
EmptySeriesError is a ValueError: True
NotFittedError is a RuntimeError: True

Utilities

Window[T] is a generic fixed-capacity ring buffer — the same one median_filter uses internally — and timer() is a context manager for quick wall-clock measurements.

Window(capacity=3, items=[3, 4, 5])  is_full=True
200 x moving_average(240 samples, window=9): 16.5 ms

Summary

  1. Statistics — summarize returns a frozen Summary; RunningStats gives the same answers in constant memory, and both are numerically stable where the textbook formula is not.

  2. Smoothing — the mean filter smears spikes, the median filter deletes them, the exponential filter lags; picking one is a choice about what to preserve.

  3. Transforms — fit and apply are separate so statistics can be learned once and reused; Pipeline fits sequentially so each step sees the data it will actually be given.

  4. Extensibility — Transform is a structural protocol, so your own classes compose with the built-ins without inheriting from anything.

  5. Errors — one hierarchy rooted at MypackageError, with each member also subclassing the nearest stdlib exception.

Next: the smoothing deep dive works through filter selection, boundary handling, and window tuning quantitatively.