From a31a8ccb295af8e0ef1cca942dbb6395485e600e Mon Sep 17 00:00:00 2001 From: kleyt0n Date: Tue, 24 Mar 2026 23:41:16 +0000 Subject: [PATCH] feat: enhance the documentation and change for furo template --- .readthedocs.yaml | 13 +- README.md | 51 +++----- docs/_static/custom.css | 3 + docs/_static/logo.png | Bin 0 -> 64084 bytes docs/api/components.rst | 15 ++- docs/api/core.rst | 22 +++- docs/api/datasets.rst | 18 +++ docs/api/dlm.rst | 11 +- docs/api/estimation.rst | 15 ++- docs/api/filters.rst | 11 +- docs/api/forecast.rst | 14 +- docs/api/index.rst | 26 ++++ docs/api/plotting.rst | 10 +- docs/api/smoothers.rst | 10 +- docs/conf.py | 17 ++- docs/examples/index.rst | 55 ++++++++ docs/getting-started/concepts.rst | 100 ++++++++++++++ docs/getting-started/installation.rst | 48 +++++++ docs/getting-started/quickstart.rst | 104 +++++++++++++++ docs/index.rst | 98 ++++++++++++-- docs/math.rst | 2 + docs/quickstart.rst | 180 -------------------------- docs/requirements.txt | 3 + docs/user-guide/batch-processing.rst | 50 +++++++ docs/user-guide/components.rst | 161 +++++++++++++++++++++++ docs/user-guide/datasets.rst | 40 ++++++ docs/user-guide/estimation.rst | 118 +++++++++++++++++ docs/user-guide/forecasting.rst | 78 +++++++++++ docs/user-guide/index.rst | 15 +++ docs/user-guide/plotting.rst | 99 ++++++++++++++ pyproject.toml | 3 +- uv.lock | 82 +++++++++++- 32 files changed, 1216 insertions(+), 256 deletions(-) create mode 100644 docs/_static/custom.css create mode 100644 docs/_static/logo.png create mode 100644 docs/api/datasets.rst create mode 100644 docs/examples/index.rst create mode 100644 docs/getting-started/concepts.rst create mode 100644 docs/getting-started/installation.rst create mode 100644 docs/getting-started/quickstart.rst delete mode 100644 docs/quickstart.rst create mode 100644 docs/requirements.txt create mode 100644 docs/user-guide/batch-processing.rst create mode 100644 docs/user-guide/components.rst create mode 100644 docs/user-guide/datasets.rst create mode 100644 docs/user-guide/estimation.rst create mode 100644 docs/user-guide/forecasting.rst create mode 100644 docs/user-guide/index.rst create mode 100644 docs/user-guide/plotting.rst diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c4b4ce2..b136f64 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,23 +1,16 @@ # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details -# Required version: 2 -# Set the OS, Python version, and other tools you might need build: os: ubuntu-24.04 tools: python: "3.13" -# Build documentation in the "docs/" directory with Sphinx sphinx: configuration: docs/conf.py -# Optionally, but recommended, -# declare the Python requirements required to build your documentation -# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -# python: -# install: -# - requirements: docs/requirements.txt - \ No newline at end of file +python: + install: + - requirements: docs/requirements.txt diff --git a/README.md b/README.md index cc40109..605fd82 100644 --- a/README.md +++ b/README.md @@ -11,30 +11,31 @@ ## Installation ```bash +pip install dynaris +# or uv add dynaris ``` +## Documentation + +Full documentation is available at [dynaris.readthedocs.io](https://dynaris.readthedocs.io). + ## Quickstart ```python -from dynaris import LocalLevel, Seasonal, DLM +from dynaris import LocalLevel, DLM from dynaris.datasets import load_nile # Load data y = load_nile() -# Build a model by composing components -model = LocalLevel(sigma_level=38.0, sigma_obs=123.0) + Seasonal(period=12) - -# Fit, smooth, forecast -dlm = DLM(model) +# Build a local-level model and fit +dlm = DLM(LocalLevel(sigma_level=38.0, sigma_obs=123.0)) dlm.fit(y).smooth() -fc = dlm.forecast(steps=12) -# Print summary +# Forecast and plot +fc = dlm.forecast(steps=10) print(dlm.summary()) - -# Single-figure overview dlm.plot(kind="panel") ``` @@ -43,7 +44,7 @@ dlm.plot(kind="panel") Build models by combining components with `+`: ```python -from dynaris import LocalLinearTrend, Seasonal, Cycle, Autoregressive, Regression +from dynaris import LocalLinearTrend, Seasonal, Cycle model = ( LocalLinearTrend(sigma_level=1.0, sigma_slope=0.1) @@ -80,26 +81,14 @@ print(f"Log-likelihood: {result.log_likelihood:.2f}") ## Datasets -```python -from dynaris.datasets import load_nile, load_airline, load_lynx, load_sunspots, load_temperature, load_gdp - -y = load_airline() # 144 monthly obs, 1949-1960 -y = load_lynx() # 114 annual obs, 1821-1934 (~10-year cycle) -y = load_sunspots() # 288 annual obs, 1700-1987 (~11-year cycle) -y = load_temperature() # 144 annual obs, 1880-2023 (warming trend) -y = load_gdp() # 319 quarterly obs, 1947-2026 (business cycle) -``` - -## Notation - -Dynaris follows the West & Harrison (1997) notation: - -| Symbol | Code | Meaning | -|--------|------|---------| -| **G** | `model.G` / `system_matrix` | System (evolution) matrix | -| **F** | `model.F` / `observation_matrix` | Observation (regression) matrix | -| **W** | `model.W` / `evolution_cov` | Evolution covariance | -| **V** | `model.V` / `obs_cov` | Observational variance | +| Dataset | Loader | N | Frequency | Domain | +|---------|--------|---|-----------|--------| +| Nile river flow | `load_nile()` | 100 | Annual | Hydrology | +| Airline passengers | `load_airline()` | 144 | Monthly | Transportation | +| Lynx population | `load_lynx()` | 114 | Annual | Ecology | +| Sunspot numbers | `load_sunspots()` | 288 | Annual | Astronomy | +| Global temperature | `load_temperature()` | 144 | Annual | Climate | +| US GDP growth | `load_gdp()` | 319 | Quarterly | Economics | ## License diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 0000000..23f074f --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,3 @@ +.sidebar-logo img { + max-width: 80px; +} diff --git a/docs/_static/logo.png b/docs/_static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9efcc20642f811b5b960d875652027834c69a98c GIT binary patch literal 64084 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 Y)cs})Y`Rf8Dpz+b;cqa=UkCvYv$paewV1u!Cu zSR-hAeAzl1ld8)OwrvrQrEMDqPEL^J+u4$Qr37**{$)oUPlZn`=vl0e4|aO#j&$xNfjfucHOgb*5&NAxutP&({z43R^w8Aa!8w7UtfQiF|#clb(%IY?h;3xj+SE7 z1aq~jP4z^tcd>Va*Sv4ddDn9!z3#9@ur>5S>{`zPYU!U2#6}T%U6DeKq4yf&*w$1` zV2YQYv>I!w4ncuml;i3?k;>7NWj}F=DUTlxzkXMjmj1Fr?S1H7N#4+m>%2Ynws{fX zZZEHG1sW#A=O;D*X>6%Ki;abylk?yps_)z%oe*tvWB7y45NAG2{cAiMfylBPlg=1H z$(oq&>Ow#TueH1)odA%~WPb-)ZS^>gU?`vS$*!u+g9W|Y&8zx{VCX=s{Y-df&U-331uoh*#ide!5HFB*=xg7Qe!pXtswW^X(`<@e9%N`wpX zY6;&R52TaB;M$EO=lUTjiOFT1&u(ckjXyNC1Oa@|J$GEWm;-!UTi?E&IEfAAIrM=t z7aXhAG=Sh`d-(I?C&%s1^7h}m|BTDb2La?N!HE+1UyIkNxP5cC}6te#}m|43kT8Mj9D53)Ml* z4PfhS{MA^r5-@C)aQQZ$KUoD7tqX-*QD1-Mp%m@VZrv#8=l^pzR_!+Wh7ki8InRa6iyqfLL zT(8n$N+!y2cOqiz&ctrSI3VEtlhTlgd}_#2?%$3aj;vr{XFl6 zT56(Mr3=u7zeYtxMUb^f3R+oByKp@Ko32ik?dD{Ep7q%A?qctyhCgap`-chUXZplE zo_qK1L2Y>M0dru&iIO=C?a>Dm!b9)h~XrE#~>Y*Cjte;>8ixqB#QiiDKpTKTTO zs;X0MLqP1U&$&4{En&9C=TK2XM29==@CWwNe4h8D!%XGd8j~RuW=2mc!GDbX7V*eh zA~E^?=-(qMppJO|{(Y;}BaQ%4Tr!TUJ!Ae+>^jhxzALn4aai|A1vrW7uU-A4b<%_; zaL8H*+4Qd!t|ncl1B4Z1gRkfG>@4G(tcQRGFgac3$d*51CwA@@>VbF6z-7eRz8x8K z1B^3)=8)@m4MzH)APe9X*CHdyOF1Y=2Yn)6%#N4dM6alDG=WKZTdngE z=*8AzW2ci0_0B6H219uc16?-4e@?LUssJ7Dr;qc*iJvfqO5Vdxzs9)uc%a!S$j}jd zSiJ+JiDM5Lpz)6DIvgIM+?VfygRroI148T&s7w9oCnij64wRi8XOWWiQ|qzw1KD#& z7kt`?dAXbRhm1FElyy8#0l#wPLZxLzK?T zoxa8;6n)Q8C$d22X*)Uj_Cy;|^d6JaZm)&4oG7K+YHg6sx@{Q}5_O9e^E->Z`lcIM z0E|RC2lh-lnWHV_ER>}Gh#DJ2op|NO`bYxO_IH2#o6>_B+4ke5R`Kx9$B8%%ZEfEk zIiL97d>R{ZI_bky=SMCrJ@w#^nz*&)!F4rwb5rswA=5XD9k!L@jQE*~yU3uBmC>Eb z!?p6gE}z4t#YsFS675LG-z{&D7|07>gc$O|LOu$7M(*c2rO`{?`o)iI~j%}nJH923bS#n!b< zf1dBi7nsS&FNQ-B+#5AV>MMyEk~}*-TWyD(is56)DXG4y8P!^E-zqy-PY)TNKHc`q zyL;yj@FA%1mwrF#wRWT z)WW&D)sVuuF3Gzw?uxsdp5kMh45(kK%2or=Q88$la_s;0D|(-@{Yvqgk+1E2I0WJ| zK9+T~Ygd;YWBKU^t5SxoT&OOpr?lS=@T3GLTR)Ld$b?;HD$?od?b}`%N=HY^fl0>k zcC(%`r0(_~)mncSG@` z`xrod84K$&)@3dexSXBzGPC-Xe2|jWvN`H&xh@=W^!w6Xg6r3@DOIE%YSf6_jkNe=zgB1ug5`Gl8t9wAvBY+w=VFvTIl@l>^O>RmGq`#l>T z?(%o9c;oR3geL5ntN)AsO_q+Nt?hd#5Cmybz)asGd2H5mU9*mWknoPpnw+X*E)+pe z*7|_|b*+R1_If`RSVjKa?E^p=7Md;lIh#2Fc%~8&tP((}(&hpDkL3rV&_}IgjnWNI){&-{=Q$n!5GM@}3)8S+L0y5M$OO*$Sis#C zqJ9P!5UD8D57D^K7{w<_97xsIcO+=+FgeT?yLkBJ_y=IL=2adltjR9 zB_*gomm^9bJq+CCa&n|t?#WL9YZGd@fP9iXIeD-+AE=GH-l8m61ShULIR3EJ@SAm@ zE*YjBmlVKEz{Y>&i0F@<)1Qg@vx3TbNo1M+6M&+kwkv1v|1)xc$pE(Hnauw;#@Lzc z|ND7;CiDMZ=Kt#MGui+5vOkmge=qa1@qQ-r{|$TM-*NX}zn|%tv+;g5-v4L(v;TT7 z|KEPTGhOrF>zXr}|MxQgcOIU}{=b*~nauxtnV*gKGnt>+kY_gJe=!Em^!tCWYtCf; z-^=`LO+AzOf1ywR9cTZyyr1cs|9;$`$^5^U`M>k+O!oi1?9XI=CiDM4t*vL{{cOCS zjdzg1SuN`SSS{|Ej`{C(%$W`Ozbx~Wv)b$b4V`+9ld5>~d$-6AN~4PiNeHG1sJZ+< DKD_8v literal 0 HcmV?d00001 diff --git a/docs/api/components.rst b/docs/api/components.rst index bfa239a..cecc63e 100644 --- a/docs/api/components.rst +++ b/docs/api/components.rst @@ -3,16 +3,25 @@ DLM Components Composable building blocks for Dynamic Linear Models. Each function returns a :class:`~dynaris.core.state_space.StateSpaceModel` that can be composed -via the ``+`` operator. +via the ``+`` operator. See :doc:`/user-guide/components` for usage guidance. + +Trend +----- .. autofunction:: dynaris.dlm.components.LocalLevel .. autofunction:: dynaris.dlm.components.LocalLinearTrend +Periodic +-------- + .. autofunction:: dynaris.dlm.components.Seasonal +.. autofunction:: dynaris.dlm.components.Cycle + +Other +----- + .. autofunction:: dynaris.dlm.components.Regression .. autofunction:: dynaris.dlm.components.Autoregressive - -.. autofunction:: dynaris.dlm.components.Cycle diff --git a/docs/api/core.rst b/docs/api/core.rst index ad5a522..5401284 100644 --- a/docs/api/core.rst +++ b/docs/api/core.rst @@ -1,12 +1,16 @@ Core Types ========== -Fundamental data structures: state-space model, Gaussian state, result -containers, and filter/smoother protocols. +Fundamental data structures used throughout dynaris. These are the building +blocks that filters, smoothers, and the DLM class operate on. StateSpaceModel --------------- +The central model representation. Holds the four system matrices (F, G, V, W) +following West and Harrison (1997) notation. Returned by all component +functions and composed via ``+``. + .. autoclass:: dynaris.core.state_space.StateSpaceModel :members: :show-inheritance: @@ -14,6 +18,9 @@ StateSpaceModel GaussianState ------------- +Represents a Gaussian belief about the state: a mean vector and covariance +matrix. Used internally by the Kalman filter and smoother at each time step. + .. autoclass:: dynaris.core.types.GaussianState :members: :show-inheritance: @@ -21,20 +28,31 @@ GaussianState FilterResult ------------ +Container returned by the Kalman filter. Holds filtered state means, +covariances, log-likelihood, and forecast errors for all time steps. + .. autoclass:: dynaris.core.results.FilterResult :members: :show-inheritance: + :no-index: SmootherResult -------------- +Container returned by the RTS smoother. Holds smoothed state means and +covariances for all time steps. + .. autoclass:: dynaris.core.results.SmootherResult :members: :show-inheritance: + :no-index: Protocols --------- +Interfaces that filter and smoother implementations must satisfy. Useful +for type checking and extending dynaris with custom algorithms. + .. autoclass:: dynaris.core.protocols.FilterProtocol :members: diff --git a/docs/api/datasets.rst b/docs/api/datasets.rst new file mode 100644 index 0000000..73fcd17 --- /dev/null +++ b/docs/api/datasets.rst @@ -0,0 +1,18 @@ +Datasets +======== + +Built-in dataset loaders for examples and testing. Each function returns a +pandas ``Series`` with an appropriate index. See :doc:`/user-guide/datasets` +for a summary table. + +.. autofunction:: dynaris.datasets.data.load_nile + +.. autofunction:: dynaris.datasets.data.load_airline + +.. autofunction:: dynaris.datasets.data.load_lynx + +.. autofunction:: dynaris.datasets.data.load_sunspots + +.. autofunction:: dynaris.datasets.data.load_temperature + +.. autofunction:: dynaris.datasets.data.load_gdp diff --git a/docs/api/dlm.rst b/docs/api/dlm.rst index 7da2f3d..64a7160 100644 --- a/docs/api/dlm.rst +++ b/docs/api/dlm.rst @@ -3,7 +3,16 @@ DLM --- High-Level Interface The :class:`~dynaris.dlm.api.DLM` class is the primary user-facing interface. It wraps a :class:`~dynaris.core.state_space.StateSpaceModel` with convenient -``fit``, ``forecast``, ``smooth``, ``plot``, and ``summary`` methods. +methods for the full modeling workflow: + +1. ``fit(y)`` --- run the Kalman filter +2. ``smooth()`` --- run the RTS smoother +3. ``forecast(steps)`` --- multi-step-ahead predictions +4. ``plot(kind)`` --- visualize results +5. ``summary()`` --- print model and fit information + +Most users only need this class. The lower-level filter, smoother, and +forecast functions are available for advanced use cases. .. autoclass:: dynaris.dlm.api.DLM :members: diff --git a/docs/api/estimation.rst b/docs/api/estimation.rst index 4c9e86f..3cbfda1 100644 --- a/docs/api/estimation.rst +++ b/docs/api/estimation.rst @@ -1,11 +1,17 @@ Parameter Estimation ==================== -Maximum likelihood estimation, EM algorithm, and model diagnostics. +Maximum likelihood estimation, EM algorithm, residual diagnostics, and +parameter transforms. See :doc:`/user-guide/estimation` for a guide on +choosing between MLE and EM. MLE --- +Gradient-based optimization of the log-likelihood using JAX autodiff. +Flexible: supports any differentiable parameterization via a user-defined +``model_fn``. + .. autofunction:: dynaris.estimation.mle.fit_mle .. autoclass:: dynaris.estimation.mle.MLEResult @@ -14,6 +20,9 @@ MLE EM Algorithm ------------ +Iterative variance estimation with guaranteed non-decreasing log-likelihood. +Simpler setup than MLE --- just pass an initial model. + .. autofunction:: dynaris.estimation.em.fit_em .. autoclass:: dynaris.estimation.em.EMResult @@ -22,6 +31,8 @@ EM Algorithm Diagnostics ----------- +Tools for checking model adequacy after fitting. + .. autofunction:: dynaris.estimation.diagnostics.standardized_residuals .. autofunction:: dynaris.estimation.diagnostics.acf @@ -33,6 +44,8 @@ Diagnostics Transforms ---------- +Map unconstrained parameters to positive values for variance estimation. + .. autofunction:: dynaris.estimation.transforms.softplus .. autofunction:: dynaris.estimation.transforms.inverse_softplus diff --git a/docs/api/filters.rst b/docs/api/filters.rst index 9f71e31..ed7bccf 100644 --- a/docs/api/filters.rst +++ b/docs/api/filters.rst @@ -1,7 +1,16 @@ Kalman Filter ============= -Forward filtering for linear-Gaussian state-space models. +Forward filtering for linear-Gaussian state-space models. The Kalman filter +processes observations sequentially, computing the posterior state distribution +at each time step. + +.. note:: + + Most users do not need to call these functions directly --- + :meth:`DLM.fit() ` wraps the Kalman filter + internally. These are available for advanced use cases requiring direct + access to intermediate filter quantities. .. autoclass:: dynaris.filters.kalman.KalmanFilter :members: diff --git a/docs/api/forecast.rst b/docs/api/forecast.rst index 41fb6b8..ab10f18 100644 --- a/docs/api/forecast.rst +++ b/docs/api/forecast.rst @@ -1,8 +1,17 @@ Forecasting =========== -Multi-step-ahead predictions with uncertainty quantification and -batch processing. +Multi-step-ahead predictions with uncertainty quantification and batch +processing. Forecasts can be initialized from either the filtered or +smoothed terminal state: + +- **From filtered state** (``forecast_from_filter``): uses observations up to + time :math:`T` only +- **From smoothed state** (``forecast_from_smoother``): uses the full dataset + for a more refined starting point + +The high-level ``forecast`` function is called internally by +:meth:`DLM.forecast() `. .. autofunction:: dynaris.forecast.forecast.forecast @@ -18,3 +27,4 @@ batch processing. .. autoclass:: dynaris.forecast.forecast.ForecastResult :members: + :no-index: diff --git a/docs/api/index.rst b/docs/api/index.rst index 550f275..1e616e4 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -1,8 +1,33 @@ API Reference ============= +Complete reference for all public classes and functions in dynaris. + ++------------------+-------------------------------------------------------------+ +| Module | Description | ++==================+=============================================================+ +| :doc:`dlm` | High-level ``DLM`` class (fit, smooth, forecast, plot) | ++------------------+-------------------------------------------------------------+ +| :doc:`components` | Six composable building blocks (``LocalLevel``, etc.) | ++------------------+-------------------------------------------------------------+ +| :doc:`core` | ``StateSpaceModel``, ``GaussianState``, result containers | ++------------------+-------------------------------------------------------------+ +| :doc:`filters` | Kalman filter (predict, update, full forward pass) | ++------------------+-------------------------------------------------------------+ +| :doc:`smoothers` | Rauch-Tung-Striebel backward smoother | ++------------------+-------------------------------------------------------------+ +| :doc:`estimation` | MLE, EM algorithm, diagnostics, transforms | ++------------------+-------------------------------------------------------------+ +| :doc:`forecast` | Multi-step forecasting and batch processing | ++------------------+-------------------------------------------------------------+ +| :doc:`plotting` | Visualization functions for all plot kinds | ++------------------+-------------------------------------------------------------+ +| :doc:`datasets` | Built-in dataset loaders | ++------------------+-------------------------------------------------------------+ + .. toctree:: :maxdepth: 2 + :hidden: dlm components @@ -12,3 +37,4 @@ API Reference estimation forecast plotting + datasets diff --git a/docs/api/plotting.rst b/docs/api/plotting.rst index 3cc3382..e9bd5c8 100644 --- a/docs/api/plotting.rst +++ b/docs/api/plotting.rst @@ -1,7 +1,15 @@ Plotting ======== -Minimalist visualization functions using the cividis colormap. +Visualization functions with a clean, minimalist style. All functions are +called internally by :meth:`DLM.plot() ` but can +also be used standalone. + +- ``plot_filtered`` --- observed data with filtered state overlay +- ``plot_smoothed`` --- smoothed state estimates with confidence bands +- ``plot_components`` --- decomposition into individual state components +- ``plot_forecast`` --- forecast fan chart with historical context +- ``plot_diagnostics`` --- QQ-plot, ACF, and histogram of residuals .. autofunction:: dynaris.plotting.plots.plot_filtered diff --git a/docs/api/smoothers.rst b/docs/api/smoothers.rst index 9e1006d..b37e47e 100644 --- a/docs/api/smoothers.rst +++ b/docs/api/smoothers.rst @@ -1,7 +1,15 @@ RTS Smoother ============ -Rauch--Tung--Striebel backward smoother for linear-Gaussian models. +Rauch-Tung-Striebel backward smoother for linear-Gaussian models. Uses the +full dataset to refine state estimates, producing lower-variance posteriors +than the forward-only Kalman filter. + +.. note:: + + Most users do not need to call these functions directly --- + :meth:`DLM.smooth() ` wraps the RTS smoother + internally. .. autoclass:: dynaris.smoothers.rts.RTSSmoother :members: diff --git a/docs/conf.py b/docs/conf.py index 131c605..be1d57f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,6 +22,7 @@ "sphinx.ext.mathjax", "sphinx.ext.viewcode", "sphinx.ext.intersphinx", + "sphinx_copybutton", ] templates_path = ["_templates"] @@ -29,18 +30,16 @@ # -- Options for HTML output ------------------------------------------------- -html_theme = "alabaster" +html_theme = "furo" +html_title = "dynaris" +html_logo = "_static/logo.png" html_theme_options = { - "description": "JAX-powered Dynamic Linear Models", - "github_user": "quant-sci", - "github_repo": "dynaris", - "github_button": True, - "fixed_sidebar": True, - "sidebar_collapse": True, - "page_width": "940px", - "sidebar_width": "220px", + "source_repository": "https://github.com/quant-sci/dynaris", + "source_branch": "main", + "source_directory": "docs/", } html_static_path = ["_static"] +html_css_files = ["custom.css"] # -- Extension configuration ------------------------------------------------- diff --git a/docs/examples/index.rst b/docs/examples/index.rst new file mode 100644 index 0000000..2adea4e --- /dev/null +++ b/docs/examples/index.rst @@ -0,0 +1,55 @@ +Examples +======== + +Ready-to-run scripts demonstrating dynaris on real-world datasets. Each +example lives in the ``examples/`` directory of the +`repository `_. + +Classic / Introductory +---------------------- + +**Nile River Flow** --- ``nile_river.py`` + Local level model on annual Nile discharge (1871--1970). The simplest + possible DLM: a random walk observed with noise. Demonstrates fitting, + smoothing, and forecasting. + +**Trend + Seasonality** --- ``trend_seasonal.py`` + Simulated sales data with a local linear trend and monthly seasonality. + Shows how to compose components and extract individual contributions. + +Economics & Finance +------------------- + +**Airline Passengers** --- ``airline_passengers.py`` + Box-Jenkins airline data (1949--1960). Trend plus seasonal decomposition, + forecasting, and component visualization. + +**GDP Business Cycle** --- ``gdp_business_cycle.py`` + US quarterly GDP growth. Local level plus AR(2) to capture business + cycle dynamics. + +Natural Sciences +---------------- + +**Lynx Population** --- ``lynx_population.py`` + Canadian lynx trappings (1821--1934). Uses an autoregressive component to + model the ~10-year population cycle. + +**Sunspot Cycles** --- ``sunspot_cycles.py`` + Annual sunspot numbers (1700--1987). Level plus cycle component to detect + the ~11-year solar cycle. + +**Global Temperature** --- ``global_temperature.py`` + Annual temperature anomaly (1880--2023). Linear trend detection in climate + data. + +Advanced +-------- + +**Dynamic Regression** --- ``dynamic_regression.py`` + Time-varying regression coefficients. Shows how regression weights evolve + over time when ``sigma_reg > 0``. + +**Panel Overview** --- ``panel_overview.py`` + Demonstrates the multi-panel plot combining filtered, smoothed, forecast, + and diagnostic views in a single figure. diff --git a/docs/getting-started/concepts.rst b/docs/getting-started/concepts.rst new file mode 100644 index 0000000..6b14c12 --- /dev/null +++ b/docs/getting-started/concepts.rst @@ -0,0 +1,100 @@ +Key Concepts +============ + +What is a Dynamic Linear Model? +-------------------------------- + +A Dynamic Linear Model (DLM) is a state-space model where both the state +evolution and observation equations are linear and driven by Gaussian noise. +DLMs are a flexible framework for decomposing a time series into interpretable +components --- trend, seasonality, regression effects, cycles --- and +forecasting with uncertainty. + +At each time step :math:`t`, a DLM is defined by: + +- A **state** :math:`\boldsymbol{\theta}_t` that evolves over time (e.g., the + current level, slope, and seasonal effects) +- An **observation** :math:`Y_t` that is a noisy linear function of the state + +The Kalman filter estimates the state given observations, and the RTS smoother +refines those estimates using the full dataset. + +Components and composition +-------------------------- + +dynaris provides six building blocks, each defining a specific structure +for the state-space matrices: + ++-------------------------+------------+--------------------------------------------+ +| Component | State dim | Description | ++=========================+============+============================================+ +| ``LocalLevel`` | 1 | Random walk plus noise | ++-------------------------+------------+--------------------------------------------+ +| ``LocalLinearTrend`` | 2 | Level plus slope | ++-------------------------+------------+--------------------------------------------+ +| ``Seasonal`` | period - 1 | Dummy or Fourier seasonal effects | ++-------------------------+------------+--------------------------------------------+ +| ``Regression`` | n_regressors | Dynamic or static coefficients | ++-------------------------+------------+--------------------------------------------+ +| ``Autoregressive`` | order | AR(p) in companion form | ++-------------------------+------------+--------------------------------------------+ +| ``Cycle`` | 2 | Damped stochastic sinusoid | ++-------------------------+------------+--------------------------------------------+ + +Combine any of these with the ``+`` operator: + +.. code-block:: python + + from dynaris import LocalLinearTrend, Seasonal, Cycle + + model = LocalLinearTrend() + Seasonal(period=12) + Cycle(period=40, damping=0.95) + # Combined state_dim = 2 + 11 + 2 = 15 + +Under the hood, this builds a single ``StateSpaceModel`` with block-diagonal +system matrices (the **superposition principle** from West and Harrison, 1997). + +The workflow +------------ + +A typical dynaris session follows four steps: + +1. **Build** --- compose components into a model +2. **Fit** --- run the Kalman filter on observed data +3. **Smooth** --- run the RTS smoother for refined estimates +4. **Forecast** --- project forward with uncertainty + +.. code-block:: python + + from dynaris import LocalLevel, DLM + + model = LocalLevel() + dlm = DLM(model) + dlm.fit(y) # Kalman filter + dlm.smooth() # RTS smoother + fc = dlm.forecast(steps=12) + dlm.plot(kind="panel") + +Notation +-------- + +dynaris follows the West and Harrison (1997) notation throughout: + ++----------+-------------------------------+------------------------------------------+ +| Symbol | Code | Meaning | ++==========+===============================+==========================================+ +| **F** | ``model.F`` / ``observation_matrix`` | Observation (regression) vector | ++----------+-------------------------------+------------------------------------------+ +| **G** | ``model.G`` / ``system_matrix`` | System (evolution) matrix | ++----------+-------------------------------+------------------------------------------+ +| **V** | ``model.V`` / ``obs_cov`` | Observational variance | ++----------+-------------------------------+------------------------------------------+ +| **W** | ``model.W`` / ``evolution_cov`` | Evolution covariance | ++----------+-------------------------------+------------------------------------------+ + +For the full mathematical treatment, see :doc:`/math`. + +References +---------- + +- West, M. and Harrison, J. (1997). *Bayesian Forecasting and Dynamic Models*, + 2nd edition. Springer. diff --git a/docs/getting-started/installation.rst b/docs/getting-started/installation.rst new file mode 100644 index 0000000..b3179ca --- /dev/null +++ b/docs/getting-started/installation.rst @@ -0,0 +1,48 @@ +Installation +============ + +From PyPI +--------- + +.. code-block:: bash + + pip install dynaris + +Or with `uv `_: + +.. code-block:: bash + + uv add dynaris + +From source +----------- + +.. code-block:: bash + + git clone https://github.com/quant-sci/dynaris.git + cd dynaris + pip install -e . + +Verify the installation +----------------------- + +.. code-block:: python + + import dynaris + print(dynaris.__version__) + +Dependencies +------------ + +dynaris requires Python 3.12+ and depends on: + +- `JAX `_ (automatic differentiation and JIT) +- NumPy +- pandas +- SciPy +- Matplotlib + +.. note:: + + By default, JAX installs with CPU support. For GPU acceleration, see the + `JAX installation guide `_. diff --git a/docs/getting-started/quickstart.rst b/docs/getting-started/quickstart.rst new file mode 100644 index 0000000..5f298da --- /dev/null +++ b/docs/getting-started/quickstart.rst @@ -0,0 +1,104 @@ +Quickstart +========== + +This page walks through the core dynaris workflow: build a model, fit it to +data, smooth, forecast, and plot. + +Your first DLM +-------------- + +The simplest DLM is a **local level** model --- a random walk observed with +noise. Let's fit one to the classic Nile river dataset: + +.. code-block:: python + + from dynaris import LocalLevel, DLM + from dynaris.datasets import load_nile + + # 1. Load data (100 annual observations) + y = load_nile() + + # 2. Define a local-level model + model = LocalLevel(sigma_level=38.0, sigma_obs=123.0) + + # 3. Wrap in DLM and fit + dlm = DLM(model) + dlm.fit(y) + + # 4. Inspect results + print(dlm.summary()) + +The ``fit`` method runs the Kalman filter forward through the observations, +computing filtered state estimates and the log-likelihood. + +Composing components +-------------------- + +The real power of dynaris is composition. Combine a trend with seasonality +in a single line: + +.. code-block:: python + + from dynaris import LocalLinearTrend, Seasonal, DLM + from dynaris.datasets import load_airline + + y = load_airline() # 144 monthly observations + + model = LocalLinearTrend(sigma_level=2.0, sigma_slope=0.1) + Seasonal(period=12) + dlm = DLM(model) + dlm.fit(y) + +This produces a single state-space model with block-diagonal system matrices. +See :doc:`/user-guide/components` for all six available components. + +Smoothing +--------- + +The Kalman filter processes observations forward in time. The RTS smoother +uses future observations to refine past estimates: + +.. code-block:: python + + dlm.fit(y).smooth() + + # Smoothed states have lower variance than filtered states + df = dlm.smoothed_states_df() + +Forecasting +----------- + +Generate multi-step-ahead forecasts with uncertainty intervals: + +.. code-block:: python + + forecast_df = dlm.forecast(steps=24) + print(forecast_df) + +If you fit with a pandas Series that has a ``DatetimeIndex``, the forecast +DataFrame continues the date index automatically. + +See :doc:`/user-guide/forecasting` for advanced options. + +Plotting +-------- + +View results with a single call: + +.. code-block:: python + + # Single-figure overview (filtered, smoothed, forecast, diagnostics) + dlm.plot(kind="panel") + + # Or individual plot types + dlm.plot(kind="filtered") + dlm.plot(kind="forecast", n_history=36) + +See :doc:`/user-guide/plotting` for all available plot kinds. + +Next steps +---------- + +- :doc:`concepts` --- understand the DLM framework and notation +- :doc:`/user-guide/components` --- explore all six building blocks +- :doc:`/user-guide/estimation` --- learn about parameter estimation (MLE and EM) +- :doc:`/math` --- full mathematical foundations diff --git a/docs/index.rst b/docs/index.rst index 27db1cd..9c8cbbc 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,23 +1,101 @@ dynaris ======= -A fast, composable, JAX-native DLM library with autodiff -parameter estimation, multi-step forecasting, and clean visualization. +**dynaris** is a JAX-powered Python library for Dynamic Linear Models (DLMs). +Build composable state-space models, estimate parameters with automatic +differentiation, and produce forecasts with uncertainty --- all in a few lines +of code. + +.. code-block:: bash + + pip install dynaris .. code-block:: python - from dynaris import LocalLevel, Seasonal, DLM + from dynaris import LocalLevel, DLM + from dynaris.datasets import load_nile + + y = load_nile() + dlm = DLM(LocalLevel(sigma_level=38.0, sigma_obs=123.0)) + dlm.fit(y).smooth() + dlm.forecast(steps=10) + dlm.plot(kind="panel") + +Why State-Space Models? +----------------------- + +State-space models are a unifying framework for time series analysis. Rather +than fitting a single equation to observed data, they maintain a latent +**state** --- the true level, trend, seasonal pattern, or regression +coefficient --- and update it recursively as new observations arrive. This +separation of *what we observe* from *what drives the process* gives +state-space models several structural advantages: + +- **Decomposition.** A complex series is expressed as the sum of interpretable + components (trend, seasonality, cycles, regression effects), each evolving + according to its own dynamics. The components are estimated jointly, not + sequentially stripped away. +- **Exact uncertainty quantification.** The Kalman filter propagates a full + Gaussian posterior at every time step, so filtered estimates, smoothed + retrospectives, and multi-step forecasts all carry principled confidence + intervals --- no bootstrap or asymptotic approximation required. +- **Missing data and irregular spacing.** When an observation is absent, the + filter simply skips the update step and lets the prior covariance grow. + No imputation heuristics are needed. +- **Online and retrospective inference.** The same model supports real-time + filtering (estimate the present), smoothing (revise the past given the + future), and forecasting (project forward), all from a single set of + sufficient statistics. - model = LocalLevel() + Seasonal(period=12) - dlm = DLM(model) - dlm.fit(y) - dlm.forecast(steps=12) - dlm.plot() +Dynamic Linear Models (DLMs) are the linear-Gaussian specialization of this +framework, where closed-form Kalman recursions replace approximate inference. +They are the workhorse behind structural time series decomposition, adaptive +forecasting, and dynamic regression in fields from econometrics and signal +processing to environmental monitoring. + +About dynaris +------------- + +dynaris implements the full DLM inference pipeline in JAX. Models are built by +composing six structural components --- ``LocalLevel``, ``LocalLinearTrend``, +``Seasonal``, ``Cycle``, ``Regression``, and ``Autoregressive`` --- using the +``+`` operator, which constructs block-diagonal state-space matrices via the +superposition principle of West and Harrison (1997). + +Filtering and smoothing run inside ``jax.lax.scan``, making them JIT-compilable +and end-to-end differentiable. This means the full Kalman log-likelihood can +be optimized with gradient-based methods (``fit_mle``) or the EM algorithm +(``fit_em``), and batch inference over many series parallelizes naturally +through ``jax.vmap``. .. toctree:: :maxdepth: 2 - :caption: Contents + :caption: Getting Started + + getting-started/installation + getting-started/quickstart + getting-started/concepts + +.. toctree:: + :maxdepth: 2 + :caption: User Guide + + user-guide/index + +.. toctree:: + :maxdepth: 1 + :caption: Theory - quickstart math + +.. toctree:: + :maxdepth: 1 + :caption: Examples + + examples/index + +.. toctree:: + :maxdepth: 2 + :caption: API Reference + api/index diff --git a/docs/math.rst b/docs/math.rst index cca5854..de9eb65 100644 --- a/docs/math.rst +++ b/docs/math.rst @@ -5,6 +5,8 @@ This section presents the mathematical foundations of Dynamic Linear Models following the notation and framework of West and Harrison (1997), *Bayesian Forecasting and Dynamic Models*. +For a plain-language introduction, see :doc:`getting-started/concepts`. + The DLM Quadruple ----------------- diff --git a/docs/quickstart.rst b/docs/quickstart.rst deleted file mode 100644 index 19f0689..0000000 --- a/docs/quickstart.rst +++ /dev/null @@ -1,180 +0,0 @@ -Quickstart -========== - -Installation ------------- - -.. code-block:: bash - - pip install dynaris - -Your first DLM --------------- - -Dynaris models are built by composing components with the ``+`` operator, -then wrapped in a :class:`~dynaris.dlm.api.DLM` for a high-level interface. - -.. code-block:: python - - from dynaris import LocalLevel, DLM - - # 1. Define a local-level model (random walk + noise) - model = LocalLevel(sigma_level=38.0, sigma_obs=123.0) - - # 2. Wrap in DLM and fit - dlm = DLM(model) - dlm.fit(y) # y can be a numpy array, JAX array, or pandas Series - - # 3. Inspect results - print(dlm.summary()) - -Composing components --------------------- - -The real power of dynaris is composition. Combine a trend with seasonality -in a single line: - -.. code-block:: python - - from dynaris import LocalLinearTrend, Seasonal, DLM - - model = LocalLinearTrend(sigma_level=2.0, sigma_slope=0.1) + Seasonal(period=12) - dlm = DLM(model) - dlm.fit(y) - -This produces a single state-space model with block-diagonal system matrices. -The state vector concatenates the trend states (level, slope) with the -seasonal states (period - 1 dimensions). - -Smoothing ---------- - -The Kalman filter processes observations forward in time. The RTS smoother -uses future observations to refine past estimates: - -.. code-block:: python - - dlm.fit(y).smooth() - - # Smoothed states have lower variance than filtered states - df = dlm.smoothed_states_df() - -Forecasting ------------ - -Generate multi-step-ahead forecasts with uncertainty intervals: - -.. code-block:: python - - forecast_df = dlm.forecast(steps=24) - print(forecast_df) - # mean lower_95 upper_95 - # 0 850.123 612.456 1087.790 - # 1 850.123 598.234 1102.012 - # ... - -If you fit with a pandas Series that has a ``DatetimeIndex``, the forecast -DataFrame continues the date index automatically. - -Plotting --------- - -All plots use a clean, minimalist style with the cividis colormap: - -.. code-block:: python - - # Filtered vs observed - dlm.plot(kind="filtered") - - # Smoothed states - dlm.plot(kind="smoothed") - - # Forecast fan chart - dlm.forecast(steps=24) - dlm.plot(kind="forecast", n_history=36) - - # Residual diagnostics (QQ-plot, ACF, histogram) - dlm.plot(kind="diagnostics") - - # Component decomposition (requires smooth first) - dlm.smooth() - dlm.plot(kind="components", component_dims={ - "Level": 0, - "Slope": 1, - "Seasonal": 2, - }) - -Parameter estimation --------------------- - -Estimate unknown variance parameters via maximum likelihood: - -.. code-block:: python - - import jax.numpy as jnp - from dynaris import LocalLevel - from dynaris.estimation import fit_mle - - def model_fn(params): - return LocalLevel( - sigma_level=jnp.exp(params[0]), - sigma_obs=jnp.exp(params[1]), - ) - - result = fit_mle(model_fn, y, init_params=jnp.zeros(2)) - print(f"Optimal log-likelihood: {result.log_likelihood:.2f}") - fitted_model = result.model - -Or use the EM algorithm: - -.. code-block:: python - - from dynaris.estimation import fit_em - - result = fit_em(y, initial_model, max_iter=100) - print(f"Converged: {result.converged}") - -Batch processing ----------------- - -Fit or forecast multiple series in parallel with ``jax.vmap``: - -.. code-block:: python - - import jax.numpy as jnp - - # y_batch: shape (n_series, T, obs_dim) - batch_result = dlm.fit_batch(y_batch) - print(batch_result.log_likelihood) # shape (n_series,) - -All components --------------- - -Dynaris provides six composable DLM building blocks: - -+-------------------------+------------+--------------------------------------------+ -| Component | State dim | Description | -+=========================+============+============================================+ -| ``LocalLevel`` | 1 | Random walk + noise | -+-------------------------+------------+--------------------------------------------+ -| ``LocalLinearTrend`` | 2 | Level + slope | -+-------------------------+------------+--------------------------------------------+ -| ``Seasonal`` | period - 1 | Dummy or Fourier form | -+-------------------------+------------+--------------------------------------------+ -| ``Regression`` | n_regressors | Dynamic/static coefficients | -+-------------------------+------------+--------------------------------------------+ -| ``Autoregressive`` | order | AR(p) in companion form | -+-------------------------+------------+--------------------------------------------+ -| ``Cycle`` | 2 | Damped stochastic sinusoid | -+-------------------------+------------+--------------------------------------------+ - -Combine any of these with ``+``: - -.. code-block:: python - - model = ( - LocalLinearTrend() - + Seasonal(period=12) - + Cycle(period=40, damping=0.95) - ) - # state_dim = 2 + 11 + 2 = 15 diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..f442e2a --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +sphinx>=9.1.0 +furo>=2024.8.6 +sphinx-copybutton>=0.5 diff --git a/docs/user-guide/batch-processing.rst b/docs/user-guide/batch-processing.rst new file mode 100644 index 0000000..dc2a93f --- /dev/null +++ b/docs/user-guide/batch-processing.rst @@ -0,0 +1,50 @@ +Batch Processing +================ + +dynaris supports fitting and forecasting multiple time series in parallel +using ``jax.vmap``. + +Batch fitting +------------- + +Pass a batch of series as a 3D array with shape ``(n_series, T, obs_dim)``: + +.. code-block:: python + + import jax.numpy as jnp + from dynaris import LocalLevel, DLM + + model = LocalLevel() + dlm = DLM(model) + + # y_batch: (n_series, T, 1) + batch_result = dlm.fit_batch(y_batch) + print(batch_result.log_likelihood) # shape (n_series,) + +Each series is filtered independently, but all series run in parallel on the +same hardware (CPU cores or GPU). + +Batch forecasting +----------------- + +After batch fitting, generate forecasts for all series at once: + +.. code-block:: python + + from dynaris.forecast import forecast_batch + + fc = forecast_batch(batch_result, model, steps=12) + +Low-level API +------------- + +The batch functions wrap ``jax.vmap`` over the single-series equivalents: + +.. code-block:: python + + from dynaris.forecast import fit_batch, forecast_batch + + batch_filter = fit_batch(model, y_batch) + batch_fc = forecast_batch(batch_filter, model, steps=12) + +See :doc:`/api/forecast` for the full API. diff --git a/docs/user-guide/components.rst b/docs/user-guide/components.rst new file mode 100644 index 0000000..af56e54 --- /dev/null +++ b/docs/user-guide/components.rst @@ -0,0 +1,161 @@ +Components +========== + +dynaris provides six composable DLM building blocks. Each function returns a +:class:`~dynaris.core.state_space.StateSpaceModel` that can be combined with +others using the ``+`` operator. + ++-------------------------+------------+--------------------------------------------+ +| Component | State dim | Description | ++=========================+============+============================================+ +| ``LocalLevel`` | 1 | Random walk plus noise | ++-------------------------+------------+--------------------------------------------+ +| ``LocalLinearTrend`` | 2 | Level plus slope | ++-------------------------+------------+--------------------------------------------+ +| ``Seasonal`` | period - 1 | Dummy or Fourier seasonal effects | ++-------------------------+------------+--------------------------------------------+ +| ``Regression`` | n_regressors | Dynamic or static coefficients | ++-------------------------+------------+--------------------------------------------+ +| ``Autoregressive`` | order | AR(p) in companion form | ++-------------------------+------------+--------------------------------------------+ +| ``Cycle`` | 2 | Damped stochastic sinusoid | ++-------------------------+------------+--------------------------------------------+ + +Trend components +---------------- + +LocalLevel +~~~~~~~~~~ + +The simplest DLM: a random walk observed with noise. + +.. math:: + + \mu_t = \mu_{t-1} + \omega_t, \quad + Y_t = \mu_t + \nu_t + +.. code-block:: python + + from dynaris import LocalLevel + + model = LocalLevel(sigma_level=1.0, sigma_obs=1.0) + +**When to use:** stationary or slowly changing series without a clear trend +or seasonality. Classic example: Nile river annual flow. + +LocalLinearTrend +~~~~~~~~~~~~~~~~ + +Extends ``LocalLevel`` with a slope (growth rate) component. + +.. math:: + + \mu_t = \mu_{t-1} + \beta_{t-1} + \omega_{\mu,t}, \quad + \beta_t = \beta_{t-1} + \omega_{\beta,t} + +.. code-block:: python + + from dynaris import LocalLinearTrend + + model = LocalLinearTrend(sigma_level=2.0, sigma_slope=0.1, sigma_obs=1.0) + +**When to use:** series with a changing trend direction, such as GDP growth +or temperature anomalies. + +Periodic components +------------------- + +Seasonal +~~~~~~~~ + +Models repeating patterns at a fixed period. Supports both **dummy** (default) +and **Fourier** forms. + +.. code-block:: python + + from dynaris import Seasonal + + # Dummy-form seasonal (default) + model = Seasonal(period=12, sigma_seasonal=0.5) + + # Fourier-form seasonal + model = Seasonal(period=12, sigma_seasonal=0.5, form="fourier") + +**When to use:** monthly, quarterly, or weekly data with repeating patterns. +State dimension is ``period - 1``. + +See :doc:`/math` for the mathematical formulation. + +Cycle +~~~~~ + +A damped stochastic sinusoid for quasi-periodic behavior. + +.. code-block:: python + + from dynaris import Cycle + + model = Cycle(period=40, damping=0.95, sigma_cycle=1.0) + +**When to use:** series with approximate cycles of known period, such as +sunspot activity (~11 years) or business cycles. Setting ``damping=1.0`` +gives an undamped cycle; values below 1.0 let the cycle decay. + +Other components +---------------- + +Regression +~~~~~~~~~~ + +Dynamic (time-varying) or static regression coefficients. + +.. code-block:: python + + from dynaris import Regression + + # Dynamic coefficients (random walk) + model = Regression(n_regressors=2, sigma_reg=0.1) + + # Static coefficients (set sigma_reg=0) + model = Regression(n_regressors=2, sigma_reg=0.0) + +**When to use:** when external predictors (regressors) influence the series. +With ``sigma_reg > 0``, coefficients evolve over time; with ``sigma_reg = 0``, +they are constant. + +Autoregressive +~~~~~~~~~~~~~~ + +An AR(p) process in companion-form state space. + +.. code-block:: python + + from dynaris import Autoregressive + + model = Autoregressive(coefficients=[0.5, -0.3], sigma_ar=1.0) + +**When to use:** series with serial correlation not captured by trend or +seasonal components. Common in population dynamics (e.g., lynx data). + +Composing components +-------------------- + +Combine any components with ``+`` to build richer models: + +.. code-block:: python + + from dynaris import LocalLinearTrend, Seasonal, Cycle, DLM + + model = ( + LocalLinearTrend(sigma_level=2.0, sigma_slope=0.1) + + Seasonal(period=12, sigma_seasonal=0.5) + + Cycle(period=40, damping=0.95) + ) + # state_dim = 2 + 11 + 2 = 15 + + dlm = DLM(model) + dlm.fit(y) + +This uses the DLM **superposition principle**: system matrices become +block-diagonal, and observation noise adds across components. See +:doc:`/math` for details. diff --git a/docs/user-guide/datasets.rst b/docs/user-guide/datasets.rst new file mode 100644 index 0000000..84952de --- /dev/null +++ b/docs/user-guide/datasets.rst @@ -0,0 +1,40 @@ +Built-in Datasets +================= + +dynaris ships with six classic time series datasets for examples and testing. +Each loader returns a pandas ``Series`` with an appropriate index. + ++------------------+--------------------+------+-----------+-----------------+ +| Dataset | Loader | N | Frequency | Domain | ++==================+====================+======+===========+=================+ +| Nile river flow | ``load_nile()`` | 100 | Annual | Hydrology | ++------------------+--------------------+------+-----------+-----------------+ +| Airline | ``load_airline()`` | 144 | Monthly | Transportation | +| passengers | | | | | ++------------------+--------------------+------+-----------+-----------------+ +| Lynx population | ``load_lynx()`` | 114 | Annual | Ecology | ++------------------+--------------------+------+-----------+-----------------+ +| Sunspot numbers | ``load_sunspots()``| 288 | Annual | Astronomy | ++------------------+--------------------+------+-----------+-----------------+ +| Global | ``load_temperature | 144 | Annual | Climate | +| temperature | ()`` | | | | ++------------------+--------------------+------+-----------+-----------------+ +| US GDP growth | ``load_gdp()`` | 319 | Quarterly | Economics | ++------------------+--------------------+------+-----------+-----------------+ + +Usage +----- + +.. code-block:: python + + from dynaris.datasets import load_airline + + y = load_airline() + print(y.head()) + print(f"Shape: {y.shape}, Index: {y.index[0]} to {y.index[-1]}") + +All loaders accept no arguments and return a pandas ``Series``. The index +is a ``DatetimeIndex`` for monthly/quarterly data or an integer index for +annual data. + +See :doc:`/api/datasets` for the full API reference. diff --git a/docs/user-guide/estimation.rst b/docs/user-guide/estimation.rst new file mode 100644 index 0000000..487bbd9 --- /dev/null +++ b/docs/user-guide/estimation.rst @@ -0,0 +1,118 @@ +Parameter Estimation +==================== + +dynaris provides two approaches for estimating unknown variance parameters: +**maximum likelihood estimation (MLE)** via automatic differentiation and the +**EM algorithm**. + +Maximum Likelihood (MLE) +------------------------ + +The log-likelihood is computed by the Kalman filter's prediction error +decomposition. Since the entire computation runs in JAX, gradients are +obtained via ``jax.grad`` and passed to a gradient-based optimizer. + +.. code-block:: python + + import jax.numpy as jnp + from dynaris import LocalLevel + from dynaris.estimation import fit_mle + + def model_fn(params): + return LocalLevel( + sigma_level=jnp.exp(params[0]), + sigma_obs=jnp.exp(params[1]), + ) + + result = fit_mle(model_fn, y, init_params=jnp.zeros(2)) + print(f"Log-likelihood: {result.log_likelihood:.2f}") + fitted_model = result.model + +The ``model_fn`` maps unconstrained parameters to a ``StateSpaceModel``. +Use ``jnp.exp`` (log transform) or ``softplus`` to ensure variance parameters +stay positive. + +The result is an :class:`~dynaris.estimation.mle.MLEResult` containing the +optimized model, final parameters, and log-likelihood. + +EM Algorithm +------------ + +The Expectation-Maximization algorithm alternates between running the Kalman +smoother (E-step) and updating variance estimates (M-step): + +.. code-block:: python + + from dynaris.estimation import fit_em + + result = fit_em(y, initial_model, max_iter=100) + print(f"Converged: {result.converged}") + print(f"Iterations: {result.n_iter}") + fitted_model = result.model + +The result is an :class:`~dynaris.estimation.em.EMResult` with the fitted +model, convergence status, and iteration count. + +MLE vs EM +--------- + ++-------------------+----------------------------------+----------------------------------+ +| Criterion | MLE | EM | ++===================+==================================+==================================+ +| Speed | Fewer iterations (gradient info) | More iterations, but each is | +| | | cheap and closed-form | ++-------------------+----------------------------------+----------------------------------+ +| Flexibility | Any differentiable | Variance parameters only | +| | parameterization | | ++-------------------+----------------------------------+----------------------------------+ +| Convergence | Can find local minima | Guaranteed non-decreasing | +| | | log-likelihood | ++-------------------+----------------------------------+----------------------------------+ +| Setup | Requires writing ``model_fn`` | Just pass the initial model | ++-------------------+----------------------------------+----------------------------------+ + +**Rule of thumb:** use MLE for complex parameterizations; use EM when you only +need variance estimates and want a simple interface. + +Parameter transforms +-------------------- + +Variance parameters must be positive. dynaris provides two transforms for +mapping unconstrained parameters to valid ranges: + +- **Log transform:** :math:`\sigma^2 = \exp(\psi)` --- simple, widely used +- **Softplus transform:** :math:`\sigma^2 = \log(1 + \exp(\psi))` --- smoother + gradient near zero + +.. code-block:: python + + from dynaris.estimation import softplus, inverse_softplus + + # Map unconstrained -> positive + sigma_sq = softplus(raw_param) + + # Map positive -> unconstrained + raw_param = inverse_softplus(sigma_sq) + +Diagnostics +----------- + +After fitting, check model adequacy with residual diagnostics: + +.. code-block:: python + + from dynaris.estimation import standardized_residuals, acf, ljung_box + + resid = standardized_residuals(filter_result, model) + autocorr = acf(resid, max_lag=20) + lb = ljung_box(resid, max_lag=10) + print(f"Ljung-Box p-value: {lb.p_value:.4f}") + +Or use the built-in diagnostic plot: + +.. code-block:: python + + dlm.plot(kind="diagnostics") + +This produces a QQ-plot, ACF plot, and histogram of standardized residuals. +See :doc:`/api/estimation` for the full API. diff --git a/docs/user-guide/forecasting.rst b/docs/user-guide/forecasting.rst new file mode 100644 index 0000000..55983c5 --- /dev/null +++ b/docs/user-guide/forecasting.rst @@ -0,0 +1,78 @@ +Forecasting +=========== + +dynaris produces multi-step-ahead forecasts with uncertainty intervals by +iterating the state-space prior equations forward without new observations. + +Basic forecasting +----------------- + +After fitting (and optionally smoothing), call ``forecast``: + +.. code-block:: python + + from dynaris import LocalLinearTrend, Seasonal, DLM + from dynaris.datasets import load_airline + + y = load_airline() + model = LocalLinearTrend() + Seasonal(period=12) + dlm = DLM(model) + dlm.fit(y).smooth() + + forecast_df = dlm.forecast(steps=24) + print(forecast_df) + # mean lower_95 upper_95 + # ... + +The returned DataFrame contains the forecast mean and 95% confidence bands. + +DatetimeIndex propagation +------------------------- + +If you fit with a pandas Series that has a ``DatetimeIndex``, the forecast +DataFrame continues the date index: + +.. code-block:: python + + # y has monthly DatetimeIndex 1949-01 to 1960-12 + forecast_df = dlm.forecast(steps=12) + # Index continues: 1961-01, 1961-02, ... + +Filtered vs smoothed initialization +------------------------------------ + +Forecasts can start from either the filtered or smoothed terminal state: + +- **Filtered** (default after ``fit``): uses only past observations up to + time :math:`T` +- **Smoothed** (after ``smooth``): uses the full dataset, giving a more + refined starting point + +The lower-level functions give explicit control: + +.. code-block:: python + + from dynaris.forecast import forecast_from_filter, forecast_from_smoother + + fc_filt = forecast_from_filter(filter_result, model, steps=12) + fc_smooth = forecast_from_smoother(smoother_result, model, steps=12) + +Confidence bands +---------------- + +Confidence bands widen with the forecast horizon as evolution noise +accumulates: + +.. code-block:: python + + from dynaris.forecast import confidence_bands + + lower, upper = confidence_bands(forecast_result, level=0.95) + +Visualize with: + +.. code-block:: python + + dlm.plot(kind="forecast", n_history=36) + +See :doc:`/api/forecast` for the full API. diff --git a/docs/user-guide/index.rst b/docs/user-guide/index.rst new file mode 100644 index 0000000..4a74a32 --- /dev/null +++ b/docs/user-guide/index.rst @@ -0,0 +1,15 @@ +User Guide +========== + +This guide covers each part of dynaris in detail --- from model components +and parameter estimation to forecasting, plotting, and batch processing. + +.. toctree:: + :maxdepth: 2 + + components + estimation + forecasting + plotting + batch-processing + datasets diff --git a/docs/user-guide/plotting.rst b/docs/user-guide/plotting.rst new file mode 100644 index 0000000..1f18186 --- /dev/null +++ b/docs/user-guide/plotting.rst @@ -0,0 +1,99 @@ +Plotting +======== + +dynaris provides minimalist visualization functions. All plots are accessible +through the ``DLM.plot()`` method via the ``kind`` parameter. + +Plot kinds +---------- + +filtered +~~~~~~~~ + +Overlay filtered state estimates on the observed data. + +.. code-block:: python + + dlm.fit(y) + dlm.plot(kind="filtered") + +Shows the Kalman filter's one-step-ahead estimates with confidence intervals. + +smoothed +~~~~~~~~ + +Display smoothed (retrospective) state estimates. + +.. code-block:: python + + dlm.fit(y).smooth() + dlm.plot(kind="smoothed") + +Smoothed estimates have lower variance because they use the full dataset. + +forecast +~~~~~~~~ + +Fan chart showing the forecast mean and confidence bands, with recent +historical observations for context. + +.. code-block:: python + + dlm.forecast(steps=24) + dlm.plot(kind="forecast", n_history=36) + +The ``n_history`` parameter controls how many past observations appear. + +diagnostics +~~~~~~~~~~~ + +Residual diagnostic panel with QQ-plot, ACF, and histogram. + +.. code-block:: python + + dlm.plot(kind="diagnostics") + +Use this to check whether the model's assumptions hold. + +components +~~~~~~~~~~ + +Decompose the series into individual state components. Requires smoothing +first and a mapping of component names to state dimensions: + +.. code-block:: python + + dlm.smooth() + dlm.plot(kind="components", component_dims={ + "Level": 0, + "Slope": 1, + "Seasonal": 2, + }) + +panel +~~~~~ + +A single-figure overview combining filtered, smoothed, forecast, and +diagnostics: + +.. code-block:: python + + dlm.fit(y).smooth() + dlm.forecast(steps=12) + dlm.plot(kind="panel") + +Customization +------------- + +All plot methods accept an optional ``ax`` parameter to draw on an existing +Matplotlib axes, and a ``title`` parameter: + +.. code-block:: python + + import matplotlib.pyplot as plt + + fig, ax = plt.subplots() + dlm.plot(kind="filtered", ax=ax, title="Nile River Flow") + plt.show() + +See :doc:`/api/plotting` for the full function signatures. diff --git a/pyproject.toml b/pyproject.toml index 3baa10b..17da597 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,8 @@ dev = [ "mypy>=1.14", "pandas-stubs>=3.0.0.260204", "sphinx>=9.1.0", - "alabaster>=1.0.0", + "furo>=2024.8.6", + "sphinx-copybutton>=0.5", ] [build-system] diff --git a/uv.lock b/uv.lock index 0085df1..902e2ee 100644 --- a/uv.lock +++ b/uv.lock @@ -13,6 +13,18 @@ resolution-markers = [ "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] +[[package]] +name = "accessible-pygments" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, +] + [[package]] name = "alabaster" version = "1.0.0" @@ -31,6 +43,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] +[[package]] +name = "beautifulsoup4" +version = "4.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -292,7 +317,7 @@ wheels = [ [[package]] name = "dynaris" -version = "0.1.0" +version = "0.1.1" source = { editable = "." } dependencies = [ { name = "jax" }, @@ -305,13 +330,14 @@ dependencies = [ [package.dev-dependencies] dev = [ - { name = "alabaster" }, + { name = "furo" }, { name = "mypy" }, { name = "pandas-stubs" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, { name = "sphinx" }, + { name = "sphinx-copybutton" }, ] [package.metadata] @@ -326,13 +352,14 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "alabaster", specifier = ">=1.0.0" }, + { name = "furo", specifier = ">=2024.8.6" }, { name = "mypy", specifier = ">=1.14" }, { name = "pandas-stubs", specifier = ">=3.0.0.260204" }, { name = "pytest", specifier = ">=8.0" }, { name = "pytest-cov", specifier = ">=6.0" }, { name = "ruff", specifier = ">=0.9" }, { name = "sphinx", specifier = ">=9.1.0" }, + { name = "sphinx-copybutton", specifier = ">=0.5" }, ] [[package]] @@ -376,6 +403,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fd/ba/56147c165442cc5ba7e82ecf301c9a68353cede498185869e6e02b4c264f/fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd", size = 1152647, upload-time = "2026-03-13T13:54:22.735Z" }, ] +[[package]] +name = "furo" +version = "2025.12.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accessible-pygments" }, + { name = "beautifulsoup4" }, + { name = "pygments" }, + { name = "sphinx" }, + { name = "sphinx-basic-ng" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/20/5f5ad4da6a5a27c80f2ed2ee9aee3f9e36c66e56e21c00fde467b2f8f88f/furo-2025.12.19.tar.gz", hash = "sha256:188d1f942037d8b37cd3985b955839fea62baa1730087dc29d157677c857e2a7", size = 1661473, upload-time = "2025-12-19T17:34:40.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/b2/50e9b292b5cac13e9e81272c7171301abc753a60460d21505b606e15cf21/furo-2025.12.19-py3-none-any.whl", hash = "sha256:bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f", size = 339262, upload-time = "2025-12-19T17:34:38.905Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -1220,6 +1263,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, ] +[[package]] +name = "soupsieve" +version = "2.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" }, +] + [[package]] name = "sphinx" version = "9.1.0" @@ -1248,6 +1300,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, ] +[[package]] +name = "sphinx-basic-ng" +version = "1.0.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736, upload-time = "2023-07-08T18:40:54.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/dd/018ce05c532a22007ac58d4f45232514cd9d6dd0ee1dc374e309db830983/sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b", size = 22496, upload-time = "2023-07-08T18:40:52.659Z" }, +] + +[[package]] +name = "sphinx-copybutton" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039, upload-time = "2023-04-14T08:10:22.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343, upload-time = "2023-04-14T08:10:20.844Z" }, +] + [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0"