divinity-forecast 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. divinity_forecast-0.3.0/PKG-INFO +471 -0
  2. divinity_forecast-0.3.0/README.md +448 -0
  3. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/divinity/__init__.py +3 -2
  4. divinity_forecast-0.3.0/divinity/datasets.py +160 -0
  5. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/divinity/forecaster.py +49 -15
  6. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/divinity/plotting.py +18 -2
  7. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/divinity/residuals.py +32 -11
  8. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/divinity/selection.py +13 -1
  9. divinity_forecast-0.3.0/divinity_forecast.egg-info/PKG-INFO +471 -0
  10. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/divinity_forecast.egg-info/SOURCES.txt +3 -0
  11. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/pyproject.toml +1 -1
  12. divinity_forecast-0.3.0/tests/test_datasets.py +49 -0
  13. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/tests/test_forecaster.py +81 -0
  14. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/tests/test_legacy.py +10 -0
  15. divinity_forecast-0.3.0/tests/test_readme_examples.py +23 -0
  16. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/tests/test_residuals.py +30 -0
  17. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/tests/test_scenarios.py +1 -1
  18. divinity_forecast-0.2.0/PKG-INFO +0 -251
  19. divinity_forecast-0.2.0/README.md +0 -228
  20. divinity_forecast-0.2.0/divinity_forecast.egg-info/PKG-INFO +0 -251
  21. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/LICENSE +0 -0
  22. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/divinity/features.py +0 -0
  23. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/divinity/legacy.py +0 -0
  24. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/divinity/periods.py +0 -0
  25. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/divinity/synthetic.py +0 -0
  26. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/divinity_forecast.egg-info/dependency_links.txt +0 -0
  27. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/divinity_forecast.egg-info/requires.txt +0 -0
  28. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/divinity_forecast.egg-info/top_level.txt +0 -0
  29. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/setup.cfg +0 -0
  30. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/tests/test_features.py +0 -0
  31. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/tests/test_periods.py +0 -0
  32. {divinity_forecast-0.2.0 → divinity_forecast-0.3.0}/tests/test_selection.py +0 -0
@@ -0,0 +1,471 @@
1
+ Metadata-Version: 2.4
2
+ Name: divinity-forecast
3
+ Version: 0.3.0
4
+ Summary: Automatic timeseries forecasting with honest prediction intervals
5
+ Author-email: dstarkey23 <drds.gh1@gmail.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/drds1/divinity
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: numpy>=1.24
12
+ Requires-Dist: pandas>=2.0
13
+ Requires-Dist: scipy>=1.10
14
+ Requires-Dist: statsmodels>=0.14
15
+ Requires-Dist: matplotlib>=3.7
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=8; extra == "dev"
18
+ Requires-Dist: pre-commit>=3; extra == "dev"
19
+ Requires-Dist: nbclient>=0.10; extra == "dev"
20
+ Requires-Dist: nbformat>=5.10; extra == "dev"
21
+ Requires-Dist: ipykernel>=6; extra == "dev"
22
+ Dynamic: license-file
23
+
24
+ # Divinity
25
+
26
+ [![PyPI](https://img.shields.io/pypi/v/divinity-forecast.svg)](https://pypi.org/project/divinity-forecast/)
27
+ [![Tests](https://github.com/drds1/divinity/actions/workflows/tests.yml/badge.svg)](https://github.com/drds1/divinity/actions/workflows/tests.yml)
28
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
29
+
30
+ Automatic timeseries forecasting with prediction intervals you can trust.
31
+
32
+ Give it a series and a horizon. Divinity decides whether there is a trend, which seasonal cycles are real and how much autocorrelation is left over. It returns a forecast with an interval that widens as much as the data say it should, and no more.
33
+
34
+ ![Daily data with weekly and yearly cycles](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/daily_multi_seasonal.png)
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install divinity-forecast
40
+ ```
41
+
42
+ The package installs as `divinity-forecast` but is imported as `divinity`. Version 0.1 was published as `divinity` and is no longer maintained there.
43
+
44
+ Or from a clone, with the development tools
45
+
46
+ ```bash
47
+ pip install -e ".[dev]"
48
+ ```
49
+
50
+ Python 3.10 or later.
51
+
52
+ ## Quick start
53
+
54
+ ```python
55
+ import divinity as dv
56
+
57
+ model = dv.Forecaster().fit(y) # y is a pandas Series, numpy array or list
58
+ result = model.predict(steps=30) # DataFrame with forecast, lower, upper
59
+ print(model.summary())
60
+ model.plot(result)
61
+ ```
62
+
63
+ ```text
64
+ Divinity forecaster
65
+ observations 1095
66
+ frequency D
67
+ log scale no
68
+ trend linear
69
+ seasonality 365.25 (1 harmonic), 7 (3 harmonics)
70
+ residuals ARIMA(0, 0, 0)
71
+ ```
72
+
73
+ Or in one line
74
+
75
+ ```python
76
+ result = dv.forecast(y, steps=30)
77
+ ```
78
+
79
+ `result` is indexed by future time. A Series with a `DatetimeIndex` gives future dates, anything else gives integer steps following on from the data.
80
+
81
+ ```text
82
+ forecast lower upper
83
+ 2024-12-31 61.48 59.55 63.41
84
+ 2025-01-01 61.55 59.62 63.49
85
+ ...
86
+ ```
87
+
88
+ The [demo notebook](https://github.com/drds1/divinity/blob/master/notebooks/demo.ipynb) walks through the API and the tricky cases, and the [applications notebook](https://github.com/drds1/divinity/blob/master/notebooks/applications.ipynb) works through real uses, from stock planning to anomaly detection.
89
+
90
+ ## How it works
91
+
92
+ The series is modelled as a constant, plus an optional linear trend, plus Fourier terms for each seasonal period, plus ARIMA errors (regression with ARIMA errors).
93
+
94
+ 1. **Candidate periods.** These come from the date frequency (7 and 365.25 for daily data, 12 for monthly, 24 and 168 for hourly and so on) and from peaks in the periodogram, so a cycle nobody mentioned can still be found. A period is only considered if the data cover at least two full cycles.
95
+ 2. **Feature selection.** Trend and seasonal terms are added one at a time while they improve BIC. Extra harmonics of a period (for sharper, non-sinusoidal shapes) are only tried once the lower harmonic is in. There is an additional penalty for searching over many candidates. Without it, testing dozens of periods on noise will find one by chance.
96
+ 3. **Random walk or not?** Before choosing any features, an augmented Dickey-Fuller test is run on what is left after removing the trend and the known periods. Detected periods are left out because they might be spurious. Testing first matters. On a random walk, spurious features soak up the wander and make the leftovers look stationary, so a test afterwards confirms its own mistake.
97
+ 4. **Allowing for autocorrelation.** Least squares assumes independent errors, and on a random walk it declares almost any trend "significant". If the series has a unit root, features are selected on differenced data. Otherwise the autocorrelation of the residuals is estimated and features are reselected on quasi-differenced data (feasible GLS), repeating until the choice settles. The ARIMA order for what remains is chosen by BIC over a small grid. An order whose fit collapses onto the edge of the parameter space (an AR root on the unit circle cancelled by an MA root) is skipped in favour of the next best. For a differenced series a drift is kept at the AIC price rather than BIC, as R's `auto.arima` does, because a missed drift costs more the further ahead you forecast.
98
+ 5. **Joint fit and intervals.** Features and ARIMA errors are fitted together by maximum likelihood. Forecast intervals combine the ARIMA forecast variance with the uncertainty in the trend and seasonal coefficients, which matters most when extrapolating a trend a long way. Student t quantiles allow for the noise level being estimated, which matters for short series.
99
+
100
+ The fitted model does not depend on the forecast horizon, so `predict(10)` is exactly the first ten rows of `predict(100)`.
101
+
102
+ ## Can it be fooled?
103
+
104
+ The cases below are built to catch out a naive forecaster. Each was generated by [`divinity.synthetic`](https://github.com/drds1/divinity/blob/master/divinity/synthetic.py) with seed 0, the same seed the smoke tests use, so none of them were picked because they looked good. In every chart the orange line is the true future, which the model never saw.
105
+
106
+ ### The standard case
107
+
108
+ A linear trend, a 20 step cycle and noise. Neither component is specified.
109
+
110
+ ![Trend plus seasonality](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/trend_seasonal.png)
111
+
112
+ ### Multiplicative seasonality
113
+
114
+ The seasonal swing grows with the level. Fitting with `log=True` handles this.
115
+
116
+ ![Monthly multiplicative](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/monthly_multiplicative.png)
117
+
118
+ ### Pure noise
119
+
120
+ There is nothing to find. A model that looks hard enough for structure will find some anyway. Divinity returns a flat forecast at the mean.
121
+
122
+ ![White noise](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/white_noise.png)
123
+
124
+ ### A random walk
125
+
126
+ Random walks look trendy. Ordinary least squares picks a "significant" trend in the large majority of them, which is then extrapolated with false confidence. Divinity finds no trend or seasonality, switches to a differenced model and lets the interval fan out.
127
+
128
+ ![Random walk](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/random_walk.png)
129
+
130
+ ### Autocorrelated but stationary
131
+
132
+ The opposite trap. The series wanders but always returns to its mean. The forecast decays back towards the mean rather than treating it as a random walk.
133
+
134
+ ![AR(1)](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/ar1.png)
135
+
136
+ ### A period nobody mentioned
137
+
138
+ A cycle of 17.3 steps on integer data with no frequency. It is found from the periodogram, including the fractional part.
139
+
140
+ ![Unknown period](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/unknown_period.png)
141
+
142
+ ### Missing data
143
+
144
+ 15% of values missing at random plus a 25 step gap. Gaps are handled natively, with no interpolation needed.
145
+
146
+ ![Missing data](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/missing_data.png)
147
+
148
+ ### Outliers
149
+
150
+ Three large one-off spikes.
151
+
152
+ ![Outliers](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/outliers.png)
153
+
154
+ ### Very short history
155
+
156
+ Five weeks of daily data. The yearly default period cannot be estimated from that and is dropped automatically, while the weekly cycle is kept.
157
+
158
+ ![Short daily](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/short_daily.png)
159
+
160
+ ### Across 100 random seeds per case
161
+
162
+ One chart per case could still be luck, so [`benchmarks/run_benchmark.py`](https://github.com/drds1/divinity/blob/master/benchmarks/run_benchmark.py) refits every scenario on 100 different seeds. *Interval coverage* is how often the true future falls inside the 95% interval, so it should be close to 95%. *Structure found* is how often the model chose the components the series was built with (for example no seasonality on noise, differencing on a random walk, period 17.3 when it is hidden). *RMSE vs naive* compares the error with simply repeating the last observed value, where below 1 is better.
163
+
164
+ | scenario | runs | 95% interval coverage | structure found | RMSE vs naive |
165
+ |---|---:|---:|---:|---:|
166
+ | `trend_seasonal` | 100 | 94.3% | 100% | 0.24 |
167
+ | `daily_multi_seasonal` | 100 | 95.1% | 100% | 0.32 |
168
+ | `monthly_multiplicative` | 100 | 94.0% | 100% | 0.19 |
169
+ | `white_noise` | 100 | 94.8% | 98% | 0.75 |
170
+ | `random_walk` | 100 | 93.8% | 94% | 1.06 |
171
+ | `ar1` | 100 | 91.9% | 88% | 0.83 |
172
+ | `missing_data` | 100 | 93.9% | 100% | 0.23 |
173
+ | `unknown_period` | 100 | 95.2% | 100% | 0.27 |
174
+ | `outliers` | 100 | 100.0% | 100% | 0.43 |
175
+ | `short_daily` | 100 | 92.4% | 100% | 0.38 |
176
+
177
+ How to read the weaker rows
178
+
179
+ - **`ar1` (91.9%).** Autoregressive coefficients estimated from 200 points are biased slightly towards zero, so the intervals come out a little narrow. In 12% of runs the model reads the series as a random walk or adds a small trend. Both are hard to tell apart from a strongly autocorrelated series of this length.
180
+ - **`random_walk` (RMSE 1.06).** For a random walk the naive forecast is the best possible point forecast, so about 1.0 is the floor. The small excess is the price of keeping a drift when one is supported, which is what makes trending series like CO₂ forecast well. What matters here is the interval, and 94% coverage means the fan is honest.
181
+ - **`short_daily` (92.4%).** 35 points carrying seven seasonal coefficients leaves little to estimate the noise from, even with t quantiles.
182
+ - **`outliers` (100%).** The spikes inflate the estimated noise, so the intervals are wider than they need to be. This errs in the safe direction.
183
+
184
+ Regenerate it with
185
+
186
+ ```bash
187
+ python benchmarks/run_benchmark.py --seeds 100
188
+ ```
189
+
190
+ ## Practical applications
191
+
192
+ Anything measured regularly over time, where the next value matters and so does how sure you can be of it. Each example below is a few lines of code, and every chart in this section is produced by running exactly that code. The [applications notebook](https://github.com/drds1/divinity/blob/master/notebooks/applications.ipynb) goes further with each one.
193
+
194
+ Real data comes bundled with statsmodels, so nothing needs downloading. Business data is simulated (and labelled as such), because real company figures are not public.
195
+
196
+ | Application | What the forecast gives you | Data |
197
+ |---|---|---|
198
+ | [Retail demand planning](#retail-demand-planning) | how much stock to order so you rarely run out | simulated daily sales |
199
+ | [Capacity planning](#capacity-planning) | how many servers or staff to have on at peak | simulated hourly web traffic |
200
+ | [Revenue budgeting](#revenue-budgeting) | next year's monthly budget with a realistic range | simulated monthly revenue |
201
+ | [Environmental monitoring](#environmental-monitoring) | where a measurement is heading, despite gaps | real Mauna Loa CO₂, weekly |
202
+ | [Seasonal climate outlook](#seasonal-climate-outlook) | the expected seasonal pattern and its spread | real Pacific sea temperature, monthly |
203
+ | [Discovering cycles](#discovering-cycles) | which cycles are real, found from the data | real sunspot counts, yearly |
204
+ | [Anomaly detection](#anomaly-detection) | readings that the past says should not happen | simulated hourly sensor with a fault |
205
+ | [Financial risk ranges](#financial-risk-ranges) | a plausible range when the value itself is unpredictable | real US Treasury bill rate, quarterly |
206
+ | [Knowing the limits](#knowing-the-limits) | what no forecast from history alone can see | real US GDP, quarterly |
207
+
208
+ Other uses with the same shape include call centre staffing, energy demand, hospital admissions, footfall and app sign-ups.
209
+
210
+ ### Retail demand planning
211
+
212
+ Order enough stock to meet demand on most days without over-ordering. The upper end of a 90% interval is the 95th percentile, so ordering that much covers demand on about 19 days in 20.
213
+
214
+ <!-- example: retail -->
215
+ ```python
216
+ import divinity as dv
217
+ from divinity import datasets
218
+
219
+ sales = datasets.retail_sales() # simulated daily units sold
220
+ history, actual = sales[:-90], sales[-90:]
221
+
222
+ model = dv.Forecaster().fit(history)
223
+ plan = model.predict(90, interval=0.9)
224
+ plan["order_quantity"] = plan["upper"].round()
225
+
226
+ model.plot(plan, actual=actual, history=180, title="Retail demand, next 90 days")
227
+ ```
228
+
229
+ ![Retail demand planning](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/applications/retail.png)
230
+
231
+ The model picks up the busy weekends, the summer lull and the run up to Christmas without being told about any of them.
232
+
233
+ ### Capacity planning
234
+
235
+ Provision servers (or staff) for the busiest hour you can realistically expect. With `interval=0.98` the upper bound is the 99th percentile for each hour.
236
+
237
+ <!-- example: capacity -->
238
+ ```python
239
+ import divinity as dv
240
+ from divinity import datasets
241
+
242
+ visits = datasets.web_traffic() # simulated hourly visits
243
+ history, actual = visits[: -7 * 24], visits[-7 * 24 :]
244
+
245
+ model = dv.Forecaster().fit(history)
246
+ week = model.predict(7 * 24, interval=0.98)
247
+ peak = week["upper"].max() # provision for this many visits an hour
248
+
249
+ model.plot(week, actual=actual, history=14 * 24, title=f"Next week, provision for {peak:,.0f} visits an hour")
250
+ ```
251
+
252
+ ![Capacity planning](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/applications/capacity.png)
253
+
254
+ Daily and weekly cycles come from the hourly frequency automatically.
255
+
256
+ ### Revenue budgeting
257
+
258
+ A growing business has seasonal swings that grow with it, so fit on the log scale.
259
+
260
+ <!-- example: budget -->
261
+ ```python
262
+ import divinity as dv
263
+ from divinity import datasets
264
+
265
+ revenue = datasets.monthly_revenue() # simulated, thousands of pounds
266
+ history, actual = revenue[:-24], revenue[-24:]
267
+
268
+ model = dv.Forecaster(log=True).fit(history)
269
+ budget = model.predict(24)
270
+
271
+ model.plot(budget, actual=actual, history=72, title="Monthly revenue (£k), two year budget")
272
+ ```
273
+
274
+ ![Revenue budgeting](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/applications/budget.png)
275
+
276
+ The interval is asymmetric because it is symmetric on the log scale. Upside is larger than downside, as it is for real growth.
277
+
278
+ ### Environmental monitoring
279
+
280
+ Weekly CO₂ readings from Mauna Loa, with weeks missing where the instruments were down. The gaps need no special handling.
281
+
282
+ <!-- example: co2 -->
283
+ ```python
284
+ import divinity as dv
285
+ from divinity import datasets
286
+
287
+ co2 = datasets.co2() # real weekly data, with missing weeks
288
+ model = dv.Forecaster().fit(co2["1988":"1999"])
289
+ outlook = model.predict(104) # two years ahead
290
+
291
+ model.plot(outlook, actual=co2["2000":], history=260, title="Atmospheric CO₂ (ppm) at Mauna Loa")
292
+ ```
293
+
294
+ ![Environmental monitoring](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/applications/co2.png)
295
+
296
+ The yearly cycle is found from the weekly frequency, and the upward drift carries on through the forecast.
297
+
298
+ ### Seasonal climate outlook
299
+
300
+ Monthly sea surface temperature in the El Niño region of the Pacific. The seasonal shape is predictable. El Niño and La Niña years are not, and the interval reflects that.
301
+
302
+ <!-- example: climate -->
303
+ ```python
304
+ import divinity as dv
305
+ from divinity import datasets
306
+
307
+ sst = datasets.elnino() # real monthly sea surface temperature
308
+ model = dv.Forecaster().fit(sst[:"2005"])
309
+ outlook = model.predict(60)
310
+
311
+ model.plot(outlook, actual=sst["2006":], history=180, title="Pacific sea surface temperature (°C), five year outlook")
312
+ ```
313
+
314
+ ![Seasonal climate outlook](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/applications/climate.png)
315
+
316
+ ### Discovering cycles
317
+
318
+ Yearly sunspot counts since 1700. Nobody tells the model about the solar cycle.
319
+
320
+ <!-- example: sunspots -->
321
+ ```python
322
+ import divinity as dv
323
+ from divinity import datasets
324
+
325
+ sun = datasets.sunspots() # real yearly counts
326
+ model = dv.Forecaster().fit(sun[:"1979"])
327
+ print(model.summary())
328
+
329
+ model.plot(model.predict(29), actual=sun["1980":], history=100, title="Sunspots, cycle found from the data")
330
+ ```
331
+
332
+ ![Discovering cycles](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/applications/sunspots.png)
333
+
334
+ The summary reports cycles of about 11 and 10 years, the well known solar cycle and its variation in length. The cycle is not perfectly regular, so the forecast drifts out of phase over several cycles, and the interval widens to allow for it. The lower bound dips below zero because the model does not know counts cannot be negative. Clip it at zero, or use `log=True` on a strictly positive series.
335
+
336
+ ### Anomaly detection
337
+
338
+ Forecast what a sensor should read, then flag readings outside a 99% interval. Here a simulated temperature sensor develops a fault on its last day.
339
+
340
+ <!-- example: anomaly -->
341
+ ```python
342
+ import divinity as dv
343
+ from divinity import datasets
344
+
345
+ readings = datasets.sensor_readings() # simulated hourly readings, fault on the last day
346
+ history, recent = readings[:-48], readings[-48:]
347
+
348
+ model = dv.Forecaster(interval=0.99).fit(history)
349
+ expected = model.predict(48)
350
+ alarms = recent[(recent < expected["lower"]) | (recent > expected["upper"])]
351
+
352
+ model.plot(expected, actual=recent, highlight=alarms, history=96, title=f"{len(alarms)} readings outside the expected range")
353
+ ```
354
+
355
+ ![Anomaly detection](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/applications/anomaly.png)
356
+
357
+ All six faulty readings are flagged and nothing else is. A fixed threshold would either miss the fault (it starts small, in the warm part of the day) or fire every afternoon.
358
+
359
+ ### Financial risk ranges
360
+
361
+ Interest rates and prices behave much like random walks, so no method can say where they will go. What a forecast can give is an honest range.
362
+
363
+ <!-- example: rates -->
364
+ ```python
365
+ import divinity as dv
366
+ from divinity import datasets
367
+
368
+ rate = datasets.us_macro("tbilrate") # real 3 month Treasury bill rate (%)
369
+ model = dv.Forecaster().fit(rate[:"2004"])
370
+
371
+ model.plot(model.predict(19), actual=rate["2005":], history=80, title="US Treasury bill rate (%), range not prediction")
372
+ ```
373
+
374
+ ![Financial risk ranges](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/applications/rates.png)
375
+
376
+ The model finds no structure, says so with a flat forecast, and lets the range fan out. The 2008 cut to near zero falls inside it. The lower end goes well below zero, which a model that knows nothing about interest rates cannot rule out, so treat it as "towards zero".
377
+
378
+ ### Knowing the limits
379
+
380
+ Any forecast built from a series' own history assumes the future behaves like the past. US GDP fitted up to 2006 shows what happens when it does not.
381
+
382
+ <!-- example: limits -->
383
+ ```python
384
+ import divinity as dv
385
+ from divinity import datasets
386
+
387
+ gdp = datasets.us_macro("realgdp") # real GDP, billions of 2005 dollars
388
+ model = dv.Forecaster(log=True).fit(gdp[:"2006"])
389
+
390
+ model.plot(model.predict(11), actual=gdp["2007":], history=60, title="US real GDP, the 2008 crash was not in the history")
391
+ ```
392
+
393
+ ![Knowing the limits](https://raw.githubusercontent.com/drds1/divinity/master/docs/images/applications/limits.png)
394
+
395
+ The forecast is a sensible continuation of the trend and the 2008 recession falls outside it. Use forecasts for planning under normal conditions and keep the limitations below in mind.
396
+
397
+ ## Options
398
+
399
+ ```python
400
+ dv.Forecaster(
401
+ seasonal_periods="auto", # "auto" (from the frequency), a list of periods, or None
402
+ detect_periods=True, # also look for periods in the data
403
+ max_detected_periods=3,
404
+ max_harmonics=3, # Fourier harmonics per period, more allow sharper shapes
405
+ trend="auto", # "auto", "linear" or None
406
+ arima="auto", # "auto", a (p, d, q) tuple, or None for white noise
407
+ log=False, # fit on the log scale, for growth with growing seasonality
408
+ interval=0.95, # default interval coverage
409
+ search_penalty=1.0, # higher is more reluctant to add seasonality, 0 is plain BIC
410
+ )
411
+ ```
412
+
413
+ Useful things on a fitted model
414
+
415
+ | | |
416
+ |---|---|
417
+ | `predict(steps, interval=None)` | forecast DataFrame (`forecast`, `lower`, `upper`) |
418
+ | `plot(forecast, actual=None, history=None)` | chart, returns the matplotlib Axes |
419
+ | `summary()` | readable description of what was chosen |
420
+ | `components_` | the same as a dict |
421
+ | `fitted_` | in-sample one step ahead predictions |
422
+
423
+ If dates are missing entirely from a `DatetimeIndex` the frequency cannot be inferred. Pass it, for example `fit(y, freq="D")`, and the gaps become missing values.
424
+
425
+ ## Limitations
426
+
427
+ - Trends are linear. A curve or saturation will be extrapolated as a straight line.
428
+ - Level shifts and regime changes are not modelled.
429
+ - Outliers are not removed. They widen the interval a little instead.
430
+ - Seasonal shape is fixed (or proportional to the level with `log=True`).
431
+ - One series at a time, with no external regressors.
432
+
433
+ ## Development
434
+
435
+ ```bash
436
+ pip install -e ".[dev]"
437
+ pre-commit install
438
+ pytest -m "not slow" # fast unit and smoke tests
439
+ pytest # also the coverage checks over many seeds (a few minutes)
440
+ python benchmarks/run_benchmark.py --seeds 100 # the coverage table above
441
+ python docs/make_figures.py # the charts above
442
+ jupyter execute --inplace notebooks/*.ipynb # rerun both notebooks
443
+ ```
444
+
445
+ To try the scenarios yourself, `divinity.synthetic.make(name, seed)` returns a training history, the true future and any options the scenario needs. The names are in `divinity.synthetic.SCENARIOS`.
446
+
447
+ The code is in [`divinity/`](https://github.com/drds1/divinity/tree/master/divinity/), with one module per stage: `periods.py` (candidate periods), `features.py` (trend and Fourier terms), `selection.py` (feature selection), `residuals.py` (ARIMA order) and `forecaster.py` (the `Forecaster` class tying them together). `synthetic.py` holds the test scenarios and `datasets.py` the example data used under Practical applications.
448
+
449
+ ### Upgrading from 0.1
450
+
451
+ Version 0.1 was installed as `divinity`, which uses the same import name. Remove it before installing the new package, or the two will overwrite each other's files.
452
+
453
+ ```bash
454
+ pip uninstall divinity
455
+ pip install divinity-forecast
456
+ ```
457
+
458
+ The old `divinity.divinity(forecast_length=...)` class still works but is deprecated and now runs the new model underneath. Replace
459
+
460
+ ```python
461
+ model = dv.divinity(forecast_length=20)
462
+ model.fit(y)
463
+ y_pred = model.predict()
464
+ ```
465
+
466
+ with
467
+
468
+ ```python
469
+ result = dv.Forecaster().fit(y).predict(20)
470
+ y_pred = result["forecast"]
471
+ ```