state_space 0.1.0 copy "state_space: ^0.1.0" to clipboard
state_space: ^0.1.0 copied to clipboard

Gaussian process regression for irregular time series: smoothed trends with uncertainty bands, forecasts and learned hyperparameters, in linear time. Pure Dart.

Smooth trends from noisy, irregular time series #

Social preview

state_space estimates smoothed trends from noisy readings taken at irregular times. It returns the trend, its slope and a credible band, across gaps and into the future. It was built for the body-weight diary trale, where days are skipped and readings are sporadic, but works on any time series. Written in pure Dart, it runs on every Flutter platform.

Under the hood it is Gaussian process regression, computed exactly in linear time with a Kalman filter and RTS smoother. The hyperparameters are fitted by maximising the marginal likelihood.

Readings with a gap, the smoothed trend with its 95% credible band, and a forecast

Install #

dependencies:
  state_space: ^0.1.0

The API is pre-1.0 and may still change.

Usage #

import 'package:state_space/state_space.dart';

final data = [
  Observation(0, 81.3),
  Observation(2, 81.0),   // a gap is not a special case
  Observation(3, 80.5),
  Observation(7, 80.0),
  Observation(8, 80.0),
  Observation(8, 79.8),   // neither are two readings at the same instant
  // ... four weeks of readings in all
];

// fit estimates the variances; the value passed here is only a placeholder
final fitted = fit(StructuralModel.localLinearTrend(processVariance: 1), data);
fitted.warnings;                 // empty here

final everyDay = [for (var d = 0; d <= 30; d++) d.toDouble()];
final trend = fitted.model.smooth(data, grid: everyDay);

trend.mean;                      // the smoothed curve
trend.trendSlope;                // its rate of change
trend.credibleBand();            // lower and upper band, as two arrays

final ahead = [for (var d = 31; d <= 60; d++) d.toDouble()];
fitted.model.forecast(data, ahead);

There is no interpolation or resampling. A missing day is a step without an update, an irregular gap is a different dt, and a repeated timestamp is dt = 0.

Time is a plain double with no units, calendars or locales, and the process variances are per its unit. The default search brackets suit days, and TimeAxis turns calendar dates into days.

The fit is Gaussian, so one mistyped reading distorts it. FitResult.warnings names such a reading.

Getting started has the full series, TimeAxis, and how to set a bad reading aside.

Components #

component use it for states
LocalLinearTrend a smooth trend that keeps its direction; the default 2
LocalLevel a level with no persistent direction 1
TrigonometricSeasonal a pattern with a known period, such as a week or a year 2 per harmonic
RegressionComponent dated events and known covariates 1 per column
Matern short-lived correlated deviations beside a trend 1, 2 or 3
StochasticCycle an approximate rhythm whose period is estimated 2

Each is a Gaussian process kernel with an exact state-space form. The default trend's posterior mean is a natural cubic smoothing spline. Components has the kernels, and how to write your own.

Combining components #

Components are added together. One-off events, such as a fortnight over Christmas or a course of medication, go in as indicator columns. Their coefficients are states rather than parameters, so they add no dimension to the fit:

final diary = ...;               // a year of daily readings

final model = StructuralModel([
  LocalLinearTrend(processVariance: 1e-4),
  TrigonometricSeasonal(period: 7, harmonics: 2, processVariance: 1e-3),
  RegressionComponent([
    IndicatorRegressor('christmas', [(from: 350, to: 364)]),
  ]),
]);   // two parameters to fit

final fitted = fit(model, diary);
final posterior = fitted.model.smooth(diary);
posterior.componentMean(0);      // the trend
posterior.componentMean(1);      // the weekly pattern, separately
posterior.coefficients.first;    // the Christmas effect, with its error

fitted.model                     // residual checks
    .diagnose(diary)
    .ljungBox(lags: 14, fittedParameters: model.parameterCount);

example/events_example.dart fits a trend and two such indicators to a simulated year and prints the recovered effects beside the truth they were generated from.

Performance #

A full smooth with a trend takes about 170–190 ns per observation, from a hundred readings to a hundred thousand, so ten years of daily readings take under a millisecond. The tables are in Validation.

Results are plain data (Float64Lists, doubles and small records), so a model and its posterior can be sent between isolates.

Documentation #

Getting started dates, grids, slopes, forecasts, unequal and bad readings, noise floors, errors, isolates
Choosing a model which components, and how to compare them
Components what each one is, which kernels are reachable, and writing your own
How it works the Gaussian process, the SDE, the filter, and the numerical choices
Validation what is checked, against what, and how closely, plus benchmarks
Calibration how the models behave on weight diaries
Roadmap what might come next, and the references
API reference every class and method
Changelog what changed in each release
Contributing the checks CI runs, and regenerating fixtures, tables and figures

Credits #

Developed with Claude Code as a pair programmer.

Licence #

MIT.

1
likes
160
points
--
downloads
screenshot

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Gaussian process regression for irregular time series: smoothed trends with uncertainty bands, forecasts and learned hyperparameters, in linear time. Pure Dart.

Repository (GitHub)
View/report issues
Contributing

Topics

#machine-learning #gaussian-process #time-series #statistics #kalman-filter

License

MIT (license)

More

Packages that depend on state_space