lessPython 0.1.0__py3-none-any.whl

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 (82) hide show
  1. lessPy/ANOVA.py +680 -0
  2. lessPy/Chart.py +1055 -0
  3. lessPy/Correlation.py +236 -0
  4. lessPy/Flows.py +116 -0
  5. lessPy/Logit.py +615 -0
  6. lessPy/Prop_test.py +267 -0
  7. lessPy/Regression.py +1491 -0
  8. lessPy/VariableLabels.py +119 -0
  9. lessPy/X.py +426 -0
  10. lessPy/XY.py +2007 -0
  11. lessPy/__init__.py +60 -0
  12. lessPy/anova_rmd.py +227 -0
  13. lessPy/bc_plotly.py +575 -0
  14. lessPy/bubble_plotly.py +470 -0
  15. lessPy/corCFA.py +316 -0
  16. lessPy/corEFA.py +220 -0
  17. lessPy/corPrint.py +45 -0
  18. lessPy/corProp.py +73 -0
  19. lessPy/corRead.py +48 -0
  20. lessPy/corReflect.py +72 -0
  21. lessPy/corReorder.py +161 -0
  22. lessPy/corScree.py +87 -0
  23. lessPy/data/Anova_1way.csv +25 -0
  24. lessPy/data/Anova_2way.csv +49 -0
  25. lessPy/data/Anova_rb.csv +8 -0
  26. lessPy/data/Anova_rbf.csv +49 -0
  27. lessPy/data/Anova_sp.csv +57 -0
  28. lessPy/data/BodyMeas.csv +341 -0
  29. lessPy/data/Cars93.csv +94 -0
  30. lessPy/data/Employee.csv +38 -0
  31. lessPy/data/Employee_lbl.csv +9 -0
  32. lessPy/data/FreqTable99.csv +5 -0
  33. lessPy/data/Jackets.csv +1026 -0
  34. lessPy/data/Learn.csv +35 -0
  35. lessPy/data/Mach4.csv +352 -0
  36. lessPy/data/Mach4_lbl.csv +21 -0
  37. lessPy/data/Reading.csv +101 -0
  38. lessPy/data/StockPrice.csv +1489 -0
  39. lessPy/data/WeightLoss.csv +11 -0
  40. lessPy/datasets.py +46 -0
  41. lessPy/date_infer.py +112 -0
  42. lessPy/details.py +314 -0
  43. lessPy/dn_plotly.py +495 -0
  44. lessPy/dot_plotly.py +385 -0
  45. lessPy/freq_poly_plotly.py +324 -0
  46. lessPy/getColors.py +399 -0
  47. lessPy/hier_plotly.py +352 -0
  48. lessPy/hs_plotly.py +395 -0
  49. lessPy/logit_rmd.py +410 -0
  50. lessPy/order_by.py +94 -0
  51. lessPy/pie_plotly.py +292 -0
  52. lessPy/pivot.py +158 -0
  53. lessPy/plotly_utils.py +787 -0
  54. lessPy/plt_add.py +129 -0
  55. lessPy/plt_contour.py +192 -0
  56. lessPy/plt_contour_facet.py +194 -0
  57. lessPy/plt_forecast.py +677 -0
  58. lessPy/plt_mat_plotly.py +201 -0
  59. lessPy/plt_plotly.py +216 -0
  60. lessPy/plt_smooth.py +170 -0
  61. lessPy/plt_time.py +143 -0
  62. lessPy/prob_norm.py +111 -0
  63. lessPy/prob_tcut.py +131 -0
  64. lessPy/prob_znorm.py +110 -0
  65. lessPy/radar_plotly.py +201 -0
  66. lessPy/reg_rmd.py +754 -0
  67. lessPy/rename.py +33 -0
  68. lessPy/reshape.py +95 -0
  69. lessPy/showColors.py +130 -0
  70. lessPy/simCImean.py +165 -0
  71. lessPy/simCLT.py +265 -0
  72. lessPy/simFlips.py +104 -0
  73. lessPy/simMeans.py +146 -0
  74. lessPy/stats_out.py +189 -0
  75. lessPy/ttest.py +641 -0
  76. lessPy/utils.py +235 -0
  77. lessPy/vbs_plotly.py +545 -0
  78. lesspython-0.1.0.dist-info/METADATA +93 -0
  79. lesspython-0.1.0.dist-info/RECORD +82 -0
  80. lesspython-0.1.0.dist-info/WHEEL +5 -0
  81. lesspython-0.1.0.dist-info/licenses/LICENSE +338 -0
  82. lesspython-0.1.0.dist-info/top_level.txt +1 -0
lessPy/plt_forecast.py ADDED
@@ -0,0 +1,677 @@
1
+ # plt_forecast.py — analog of plt.forecast.R.
2
+ #
3
+ # ts_source="fable" (the default, as in R) maps fable's models
4
+ # to statsmodels — the plan's settled decision #3 (ETS replaces
5
+ # fable). ts_method="es" fits ETSModel over the component grid
6
+ # fable::ETS searches (error A/M; trend N/A/Ad; seasons N/A/M;
7
+ # Md also accepted when specified), selecting by AICc as fable
8
+ # does, or fixing components the caller gives via
9
+ # ts_error/ts_trend/ts_seasons. ts_method="lm" is the TSLM
10
+ # analog: OLS on trend and calendar-season dummies, with the
11
+ # lessR fable-branch prediction intervals (qnorm quantiles on
12
+ # the forecast sigma). Results track R closely but not exactly:
13
+ # initial-state estimation differs between statsmodels and
14
+ # fable.
15
+ #
16
+ # ts_source="classic":
17
+ # ts_method="es" ports stats::HoltWinters directly — the same
18
+ # initialization (decompose of the first two periods, linear fit
19
+ # for level/trend start), the same one-step filter recursion, and
20
+ # the same L-BFGS-B minimization of SSE over only the free
21
+ # smoothing parameters (start 0.3/0.1/0.1, bounds [0, 1]) — so
22
+ # results track R closely. Prediction intervals port
23
+ # predict.HoltWinters: qnorm quantiles on var(residuals) scaled
24
+ # by the psi weights.
25
+ #
26
+ # ts_method="lm" ports the stl + regression path. The stl
27
+ # periodic decomposition comes from statsmodels STL (a port of
28
+ # the same Fortran algorithm as R's stl), with R's
29
+ # s.window="periodic" mapped to seasonal=10n+1, seasonal_deg=0.
30
+ # statsmodels is imported lazily, only on this path — the
31
+ # Python analog of an R Suggests dependency.
32
+
33
+ import numpy as np
34
+ import pandas as pd
35
+ from scipy import optimize as spo
36
+ from scipy import stats as sps
37
+
38
+ from .utils import fmt
39
+
40
+ _UNITS = ("days", "days7", "weeks", "months", "quarters", "years")
41
+ _FREQ = {"days": 365, "days7": 7, "weeks": 52, "months": 12,
42
+ "quarters": 4, "years": 1}
43
+ _OFFSET = {"days": pd.DateOffset(days=1),
44
+ "days7": pd.DateOffset(days=1),
45
+ "weeks": pd.DateOffset(weeks=1),
46
+ "months": pd.DateOffset(months=1),
47
+ "quarters": pd.DateOffset(months=3),
48
+ "years": pd.DateOffset(years=1)}
49
+
50
+
51
+ def _ts_unit(dates, ts_unit):
52
+ """Time unit and seasonal frequency from the date spacing.
53
+ R analog: .tsMake() interval logic; frequencies match
54
+ tsMake.R (days 365, days7 7, weeks 52, months 12,
55
+ quarters 4, years 1)."""
56
+ if ts_unit is not None and ts_unit not in _UNITS:
57
+ raise ValueError(f"ts_unit must be one of {_UNITS}")
58
+ dd = np.diff(dates).astype("timedelta64[D]").astype(int)
59
+ if len(dd) == 0 or (dd <= 0).any():
60
+ raise ValueError("Cannot forecast without a consistent "
61
+ "time unit: need increasing dates")
62
+ med = float(np.median(dd))
63
+ if med <= 1.5:
64
+ unit = "days"
65
+ elif 6 <= med <= 8:
66
+ unit = "weeks"
67
+ elif 27 <= med <= 32:
68
+ unit = "months"
69
+ elif 88 <= med <= 93:
70
+ unit = "quarters"
71
+ elif med >= 350:
72
+ unit = "years"
73
+ else:
74
+ raise ValueError("Cannot forecast without a consistent "
75
+ f"time unit: median spacing {med} days")
76
+ if ts_unit is None:
77
+ ts_unit = unit
78
+ elif ts_unit == "days7" and unit == "days":
79
+ pass # weekly cycle in daily data
80
+ elif ts_unit != unit:
81
+ raise ValueError(f'dates are spaced as "{unit}" but '
82
+ f'ts_unit="{ts_unit}"')
83
+ return ts_unit, _FREQ[ts_unit]
84
+
85
+
86
+ def _decompose(x, f, mult):
87
+ """Classical decomposition for the Holt-Winters start values.
88
+ R analog: stats::decompose() — centered moving-average trend,
89
+ seasonal figure as centered cycle-position means."""
90
+ n = len(x)
91
+ if f % 2 == 0:
92
+ filt = np.r_[0.5, np.ones(f - 1), 0.5] / f
93
+ else:
94
+ filt = np.ones(f) / f
95
+ half = len(filt) // 2
96
+ trend = np.full(n, np.nan)
97
+ conv = np.convolve(x, filt[::-1], mode="valid")
98
+ trend[half:half + len(conv)] = conv
99
+ season = x / trend if mult else x - trend
100
+ figure = np.empty(f)
101
+ for i in range(f):
102
+ figure[i] = np.nanmean(season[i::f])
103
+ figure = figure / figure.mean() if mult \
104
+ else figure - figure.mean()
105
+ return trend, figure
106
+
107
+
108
+ def _hw_filter(x, f, alpha, beta, gamma, do_trend, do_seasons,
109
+ mult, l0, b0, s0, start):
110
+ """One-step Holt-Winters filter from observation `start`
111
+ (0-based). R analog: C_HoltWinters (src/library/stats)."""
112
+ n = len(x)
113
+ a, b = l0, b0 if do_trend else 0.0
114
+ s = list(s0) if do_seasons else []
115
+ xhat = np.empty(n - start)
116
+ sse = 0.0
117
+ for j, i in enumerate(range(start, n)):
118
+ pred = a + b
119
+ if do_seasons:
120
+ pred = pred * s[j] if mult else pred + s[j]
121
+ xhat[j] = pred
122
+ res = x[i] - pred
123
+ sse += res * res
124
+ if do_seasons:
125
+ desea = x[i] / s[j] if mult else x[i] - s[j]
126
+ anew = alpha * desea + (1 - alpha) * (a + b)
127
+ else:
128
+ anew = alpha * x[i] + (1 - alpha) * (a + b)
129
+ if do_trend:
130
+ b = beta * (anew - a) + (1 - beta) * b
131
+ if do_seasons:
132
+ s.append(gamma * ((x[i] / anew) if mult
133
+ else (x[i] - anew))
134
+ + (1 - gamma) * s[j])
135
+ a = anew
136
+ coefs = {"a": a}
137
+ if do_trend:
138
+ coefs["b"] = b
139
+ if do_seasons:
140
+ coefs["s"] = np.array(s[-f:])
141
+ return xhat, sse, coefs
142
+
143
+
144
+ def _holt_winters(x, f, do_trend, do_seasons, mult,
145
+ alpha, beta, gamma):
146
+ """stats::HoltWinters port: R start values, then minimize
147
+ SSE over only the free smoothing parameters, as R optim
148
+ (L-BFGS-B, start 0.3/0.1/0.1, bounds [0, 1])."""
149
+ if do_seasons:
150
+ if len(x) < 2 * f:
151
+ raise ValueError("need at least 2 periods to compute "
152
+ "seasonal start values")
153
+ start = f
154
+ trend, figure = _decompose(x[:2 * f], f, mult)
155
+ dat = trend[np.isfinite(trend)]
156
+ # R .lm.fit on (1..m): l.start intercept, b.start slope
157
+ b0, l0 = np.polyfit(np.arange(1, len(dat) + 1), dat, 1)
158
+ s0 = figure
159
+ else:
160
+ if do_trend:
161
+ l0, b0, s0, start = x[1], x[1] - x[0], None, 2
162
+ else:
163
+ l0, b0, s0, start = x[0], None, None, 1
164
+
165
+ free = [] # optimized parameters
166
+ if alpha is None:
167
+ free.append(("alpha", 0.3))
168
+ if do_trend and beta is None:
169
+ free.append(("beta", 0.1))
170
+ if do_seasons and gamma is None:
171
+ free.append(("gamma", 0.1))
172
+
173
+ def run(vals):
174
+ p = {"alpha": alpha, "beta": beta, "gamma": gamma}
175
+ p.update(dict(zip([nm for nm, _ in free], vals)))
176
+ return _hw_filter(x, f, p["alpha"],
177
+ p["beta"] if do_trend else 0.0,
178
+ p["gamma"] if do_seasons else 0.0,
179
+ do_trend, do_seasons, mult,
180
+ l0, b0 if do_trend else 0.0, s0, start)
181
+
182
+ if free:
183
+ if len(free) == 1:
184
+ sol = spo.minimize_scalar(
185
+ lambda v: run([v])[1], bounds=(0, 1),
186
+ method="bounded")
187
+ best = [sol.x]
188
+ else:
189
+ sol = spo.minimize(
190
+ lambda v: run(v)[1],
191
+ x0=[st for _, st in free], method="L-BFGS-B",
192
+ bounds=[(0, 1)] * len(free))
193
+ best = list(sol.x)
194
+ else:
195
+ best = []
196
+ params = {"alpha": alpha, "beta": beta, "gamma": gamma}
197
+ params.update(dict(zip([nm for nm, _ in free], best)))
198
+ if not do_trend:
199
+ params["beta"] = False
200
+ if not do_seasons:
201
+ params["gamma"] = False
202
+ xhat, sse, coefs = run(best)
203
+ return params, coefs, xhat, sse, start
204
+
205
+
206
+ def _hw_predict(x, xhat, start, f, params, coefs, mult,
207
+ do_trend, do_seasons, h, level):
208
+ """Forecasts and prediction intervals.
209
+ R analog: predict.HoltWinters — psi weights on
210
+ var(residuals), qnorm quantiles."""
211
+ steps = np.arange(1, h + 1)
212
+ y_hat = np.full(h, coefs["a"])
213
+ if do_trend:
214
+ y_hat = y_hat + steps * coefs["b"]
215
+ if do_seasons:
216
+ s_rep = np.resize(coefs["s"], h)
217
+ y_hat = y_hat * s_rep if mult else y_hat + s_rep
218
+
219
+ resid = x[start:] - xhat
220
+ s2 = float(np.var(resid, ddof=1))
221
+ alpha = params["alpha"]
222
+ beta_v = params["beta"] if do_trend else 0.0
223
+ gamma_v = params["gamma"] if do_seasons else 0.0
224
+
225
+ def psi(j):
226
+ return (alpha * (1 + j * beta_v)
227
+ + (j % f == 0) * gamma_v * (1 - alpha))
228
+
229
+ var_h = np.empty(h)
230
+ if not mult:
231
+ for hh in steps:
232
+ js = np.arange(1, hh)
233
+ var_h[hh - 1] = s2 * (1 + np.sum(psi(js) ** 2))
234
+ else:
235
+ # R indexes (a, b, s1..sf); (rel-j) mod f == 0 falls on b,
236
+ # an R quirk preserved for parity
237
+ cvec = np.r_[coefs["a"],
238
+ coefs["b"] if do_trend else np.nan,
239
+ coefs["s"]]
240
+ for hh in steps:
241
+ rel = 1 + (hh - 1) % f
242
+ tot = 0.0
243
+ for j in range(hh):
244
+ tot += (psi(j) * cvec[1 + rel]
245
+ / cvec[1 + (rel - j) % f]) ** 2
246
+ var_h[hh - 1] = s2 * tot
247
+ zq = sps.norm.ppf((1 + level) / 2)
248
+ half = zq * np.sqrt(var_h)
249
+ return y_hat, y_hat - half, y_hat + half
250
+
251
+
252
+ def _stl_periodic(y, f):
253
+ """Seasonal component, R stl(y, s.window="periodic"):
254
+ statsmodels STL with the periodic mapping s.window = 10n+1,
255
+ s.degree = 0. Lazy import — the Python analog of Suggests."""
256
+ try:
257
+ from statsmodels.tsa.seasonal import STL
258
+ except ImportError:
259
+ raise ImportError(
260
+ 'ts_method="lm" with seasonality needs statsmodels: '
261
+ "pip install statsmodels") from None
262
+ res = STL(y, period=f, seasonal=10 * len(y) + 1,
263
+ seasonal_deg=0, robust=False).fit()
264
+ return np.asarray(res.seasonal), np.asarray(res.trend)
265
+
266
+
267
+ _ETS_ERR = {"A": "add", "M": "mul"}
268
+ _ETS_TREND = {"N": (None, False), "A": ("add", False),
269
+ "M": ("mul", False), "Ad": ("add", True),
270
+ "Md": ("mul", True)}
271
+ _ETS_SEAS = {"N": None, "A": "add", "M": "mul"}
272
+
273
+
274
+ def _ets_forecast(yv, f, ts_error, ts_trend, ts_seasons,
275
+ alpha, beta, gamma, h, level, digits_d):
276
+ """fable::ETS analog on statsmodels ETSModel: fixed
277
+ components when specified, otherwise AICc selection over the
278
+ grid fable searches. R analog: plt.forecast.R fable es."""
279
+ try:
280
+ from statsmodels.tsa.exponential_smoothing.ets import (
281
+ ETSModel)
282
+ except ImportError:
283
+ raise ImportError(
284
+ 'ts_source="fable" needs statsmodels: '
285
+ "pip install statsmodels") from None
286
+
287
+ errs = [ts_error] if ts_error else ["A", "M"]
288
+ trds = [ts_trend] if ts_trend else ["N", "A", "Ad"]
289
+ seas_ok = f > 1 and len(yv) >= 2 * f
290
+ seas = ([ts_seasons] if ts_seasons
291
+ else (["N", "A", "M"] if seas_ok else ["N"]))
292
+ pos = bool((yv > 0).all())
293
+
294
+ y_ser = pd.Series(np.asarray(yv, float))
295
+ best = None
296
+ for e in errs:
297
+ for t in trds:
298
+ for s in seas:
299
+ if (e == "M" or s == "M") and not pos:
300
+ continue
301
+ trend, damped = _ETS_TREND[t]
302
+ try:
303
+ m = ETSModel(
304
+ y_ser, error=_ETS_ERR[e], trend=trend,
305
+ damped_trend=damped,
306
+ seasonal=_ETS_SEAS[s],
307
+ seasonal_periods=(f if _ETS_SEAS[s]
308
+ else None))
309
+ fixes = {}
310
+ if alpha is not None:
311
+ fixes["smoothing_level"] = alpha
312
+ if beta is not None and trend:
313
+ fixes["smoothing_trend"] = beta
314
+ if gamma is not None and _ETS_SEAS[s]:
315
+ fixes["smoothing_seasonal"] = gamma
316
+ if fixes:
317
+ with m.fix_params(fixes):
318
+ res = m.fit(disp=False)
319
+ else:
320
+ res = m.fit(disp=False)
321
+ except Exception:
322
+ continue
323
+ if np.isfinite(res.aicc) and (
324
+ best is None or res.aicc < best[0]):
325
+ best = (res.aicc, e, t, s, res)
326
+ if best is None:
327
+ raise ValueError(
328
+ "Model fitting failed. Simplify the model such as "
329
+ 'ts_trend="N" and ts_seasons="N", or use more data.')
330
+ _, e, t, s, res = best
331
+
332
+ n = len(yv)
333
+ y_fit = res.fittedvalues.to_numpy()
334
+ pred = res.get_prediction(start=n, end=n + h - 1)
335
+ y_hat = np.asarray(pred.predicted_mean)
336
+ # PI as lessR builds it from the fable distribution:
337
+ # mean +/- qnorm((1+PI)/2) * sigma (plt.forecast.R ~330)
338
+ sigma = np.sqrt(np.asarray(pred.forecast_variance))
339
+ zq = sps.norm.ppf((1 + level) / 2)
340
+ y_lwr = y_hat - zq * sigma
341
+ y_upr = y_hat + zq * sigma
342
+
343
+ mse = float(np.mean((yv - y_fit) ** 2))
344
+ nd = max(digits_d - 1, 3)
345
+ report = ["[ETS from statsmodels; standard reference: "
346
+ "https://otexts.com/fpp3/]", "",
347
+ "Estimated model\n---------------",
348
+ f"ETS({e},{t},{s})", "",
349
+ "Smoothing Parameters"]
350
+ tx = " alpha: " + fmt(res.smoothing_level, nd)
351
+ if _ETS_TREND[t][0]:
352
+ tx += " beta: " + fmt(res.smoothing_trend, nd)
353
+ if _ETS_TREND[t][1]:
354
+ tx += " phi: " + fmt(res.damping_trend, nd)
355
+ if _ETS_SEAS[s]:
356
+ tx += " gamma: " + fmt(res.smoothing_seasonal, nd)
357
+ report += [tx, "",
358
+ "AIC: " + fmt(res.aic, 2)
359
+ + " AICc: " + fmt(res.aicc, 2)
360
+ + " BIC: " + fmt(res.bic, 2), "",
361
+ "Mean squared error of fit to data: "
362
+ + fmt(mse, 3),
363
+ "Root mean squared error (RMSE) fit: "
364
+ + fmt(np.sqrt(mse), 3), ""]
365
+ return y_fit, y_hat, y_lwr, y_upr, report
366
+
367
+
368
+ def _season_pos(dates, f, ts_unit):
369
+ """Calendar season of each date for the TSLM dummies:
370
+ month or quarter number, else position in the cycle."""
371
+ dd = pd.DatetimeIndex(dates)
372
+ if ts_unit == "months":
373
+ return dd.month.to_numpy()
374
+ if ts_unit == "quarters":
375
+ return dd.quarter.to_numpy()
376
+ return (np.arange(len(dd)) % f) + 1
377
+
378
+
379
+ def _tslm_forecast(x_dates, yv, f, ts_unit, ts_trend,
380
+ ts_seasons, x_hat, level, digits_d):
381
+ """fable::TSLM analog: OLS on trend and calendar-season
382
+ dummies; prediction intervals as in the lessR fable branch
383
+ (qnorm quantiles on the forecast sigma).
384
+ R analog: plt.forecast.R fable lm."""
385
+ n = len(yv)
386
+ h = len(x_hat)
387
+ do_trend = ts_trend == "A"
388
+ do_seasons = ts_seasons == "A"
389
+
390
+ cols, names = [np.ones(n)], ["(Intercept)"]
391
+ fcols = [np.ones(h)]
392
+ if do_trend:
393
+ cols.append(np.arange(1, n + 1, dtype=float))
394
+ fcols.append(np.arange(n + 1, n + h + 1, dtype=float))
395
+ names.append("trend")
396
+ if do_seasons:
397
+ p_now = _season_pos(x_dates, f, ts_unit)
398
+ if ts_unit in ("months", "quarters"):
399
+ p_new = _season_pos(x_hat, f, ts_unit)
400
+ else:
401
+ p_new = ((n + np.arange(h)) % f) + 1
402
+ unit_lbl = ts_unit[:-1] if ts_unit.endswith("s") \
403
+ else ts_unit
404
+ for k in range(2, f + 1):
405
+ cols.append((p_now == k).astype(float))
406
+ fcols.append((p_new == k).astype(float))
407
+ names.append(f"{unit_lbl}{k}")
408
+ X = np.column_stack(cols)
409
+ Xf = np.column_stack(fcols)
410
+
411
+ coef, *_ = np.linalg.lstsq(X, yv, rcond=None)
412
+ y_fit = X @ coef
413
+ resid = yv - y_fit
414
+ p = X.shape[1]
415
+ sse = float(resid @ resid)
416
+ sigma2 = sse / (n - p)
417
+ xtx_inv = np.linalg.pinv(X.T @ X)
418
+
419
+ y_hat = Xf @ coef
420
+ se_pred = np.sqrt(sigma2
421
+ * (1 + np.einsum("ij,jk,ik->i",
422
+ Xf, xtx_inv, Xf)))
423
+ zq = sps.norm.ppf((1 + level) / 2)
424
+ y_lwr = y_hat - zq * se_pred
425
+ y_upr = y_hat + zq * se_pred
426
+
427
+ # coefficient table, as the lessR fable lm report
428
+ se_b = np.sqrt(np.diag(xtx_inv) * sigma2)
429
+ with np.errstate(divide="ignore", invalid="ignore"):
430
+ tvals = coef / se_b
431
+ pvals = 2 * sps.t.sf(np.abs(tvals), n - p)
432
+ report = ["Estimated model\n---------------",
433
+ "Model: TSLM",
434
+ "", "Coefficients:"]
435
+ w = max(len(nm) for nm in names)
436
+ report.append(f"{'':{w}} {'Estimate':>12} {'Std.Err':>12}"
437
+ f" {'t-value':>9} {'p-value':>8}")
438
+ for i, nm in enumerate(names):
439
+ report.append(
440
+ f"{nm:{w}} {fmt(coef[i], digits_d):>12}"
441
+ f" {fmt(se_b[i], digits_d):>12}"
442
+ f" {fmt(tvals[i], 3):>9}"
443
+ f" {fmt(pvals[i], 4):>8}")
444
+ tss = float(((yv - yv.mean()) ** 2).sum())
445
+ if p > 1 and tss > 0:
446
+ r2 = 1 - sse / tss
447
+ adj = 1 - (1 - r2) * (n - 1) / (n - p)
448
+ # a (numerically) perfect fit gives r2 == 1 exactly, so
449
+ # 1 - r2 underflows to 0: F is infinite, as R reports
450
+ if r2 >= 1:
451
+ fstat, fp = np.inf, 0.0
452
+ else:
453
+ fstat = (r2 / (p - 1)) / ((1 - r2) / (n - p))
454
+ fp = sps.f.sf(fstat, p - 1, n - p)
455
+ report += ["",
456
+ f"Residual standard error: "
457
+ f"{fmt(np.sqrt(sigma2), 3)} on {n - p} "
458
+ "degrees of freedom",
459
+ f"Multiple R-squared: {fmt(r2, 4)},"
460
+ f"\tAdjusted R-squared: {fmt(adj, 4)}",
461
+ f"F-statistic: {fmt(fstat, 2)} on {p - 1} "
462
+ f"and {n - p} DF, p-value: {fmt(fp, 5)}"]
463
+ else:
464
+ report += ["", "[consider adding terms: ts_trend and "
465
+ "ts_seasons]"]
466
+ mse = sse / n
467
+ report += ["",
468
+ "Mean squared error of fit to data: "
469
+ + fmt(mse, 3),
470
+ "Root mean squared error (RMSE) fit: "
471
+ + fmt(np.sqrt(mse), 3), ""]
472
+ return y_fit, y_hat, y_lwr, y_upr, report
473
+
474
+
475
+ def plt_forecast(x_dates, yv, x_name, y_name,
476
+ ts_unit=None, ts_ahead=0, ts_method="es",
477
+ ts_source="fable", ts_error=None,
478
+ ts_trend=None, ts_seasons=None, ts_alpha=None,
479
+ ts_beta=None, ts_gamma=None, ts_PI=0.95,
480
+ digits_d=None):
481
+ """Classic-path time series forecast: fitted values, forecasts
482
+ with prediction intervals, and the console report lines.
483
+ R analog: .plt.forecast(), ts_source="classic" branch."""
484
+ if digits_d is None:
485
+ digits_d = 2
486
+ ts_unit, f = _ts_unit(x_dates, ts_unit)
487
+
488
+ # seasonality eligibility (plt.forecast.R lines 36-44)
489
+ if ts_seasons is not None and ts_seasons != "N":
490
+ if ts_unit == "years":
491
+ print("Seasonal effects are not possible with "
492
+ "annual data.")
493
+ ts_seasons = "N"
494
+ elif len(yv) < 2 * f:
495
+ print("\nUsually need two years worth of data to "
496
+ "estimate seasonality.\n")
497
+ ts_seasons = "N"
498
+
499
+ # forecast dates, needed by the TSLM dummies
500
+ last = pd.Timestamp(x_dates[-1])
501
+ off = _OFFSET[ts_unit]
502
+ x_hat = pd.DatetimeIndex([last + k * off
503
+ for k in range(1, ts_ahead + 1)])
504
+
505
+ report = []
506
+ coef_nm = None # classic report tail flag
507
+
508
+ # fable-equivalent: statsmodels ETS / TSLM analog
509
+ if ts_source == "fable":
510
+ if ts_method == "es":
511
+ if ts_error is not None and ts_error not in _ETS_ERR:
512
+ raise ValueError('ts_error: "A" or "M"')
513
+ if ts_trend is not None and \
514
+ ts_trend not in _ETS_TREND:
515
+ raise ValueError(
516
+ 'ts_trend: "N", "A", "M", "Ad", or "Md"')
517
+ if ts_seasons is not None and \
518
+ ts_seasons not in _ETS_SEAS:
519
+ raise ValueError('ts_seasons: "N", "A", or "M"')
520
+ y_fit, y_hat, y_lwr, y_upr, rep = _ets_forecast(
521
+ yv, f, ts_error, ts_trend, ts_seasons,
522
+ ts_alpha, ts_beta, ts_gamma, ts_ahead, ts_PI,
523
+ digits_d)
524
+ elif ts_method == "lm":
525
+ if ts_trend is not None and \
526
+ ts_trend not in ("A", "N"):
527
+ raise ValueError('Enter either "A" for additive '
528
+ 'trend or "N" for no trend.')
529
+ if ts_seasons is not None and \
530
+ ts_seasons not in ("A", "N"):
531
+ raise ValueError(
532
+ 'Enter either "A" for additive seasons or '
533
+ '"N" for no seasons.')
534
+ y_fit, y_hat, y_lwr, y_upr, rep = _tslm_forecast(
535
+ x_dates, yv, f, ts_unit, ts_trend, ts_seasons,
536
+ x_hat, ts_PI, digits_d)
537
+ else:
538
+ raise ValueError('ts_method must be "es" or "lm"')
539
+ report += rep
540
+ x_fit = x_dates
541
+
542
+ elif ts_source != "classic":
543
+ raise ValueError('ts_source: "fable" or "classic"')
544
+
545
+ # es: Holt-Winters (plt.forecast.R classic es branch)
546
+ elif ts_method == "es":
547
+ if ts_trend is not None and ts_trend not in ("A", "N"):
548
+ raise ValueError(
549
+ 'HoltWinters() only supports "A" for additive '
550
+ 'trend or "N" for no trend.')
551
+ if ts_seasons is not None and \
552
+ ts_seasons not in ("A", "M", "N"):
553
+ raise ValueError(
554
+ 'HoltWinters() only supports "A" for additive, '
555
+ '"M" for multiplicative, or "N" for no '
556
+ "seasonality.")
557
+ do_trend = ts_trend == "A"
558
+ do_seasons = ts_seasons in ("A", "M")
559
+ mult = ts_seasons == "M"
560
+
561
+ params, coefs, xhat, sse, start = _holt_winters(
562
+ yv, f, do_trend, do_seasons, mult,
563
+ ts_alpha, ts_beta if do_trend else None,
564
+ ts_gamma if do_seasons else None)
565
+ x_fit = x_dates[start:]
566
+ y_fit = xhat
567
+ y_hat, y_lwr, y_upr = _hw_predict(
568
+ yv, xhat, start, f, params, coefs, mult,
569
+ do_trend, do_seasons, ts_ahead, ts_PI)
570
+
571
+ n_param = 1 + do_trend + do_seasons
572
+ mse = sse / (len(x_fit) - n_param)
573
+
574
+ nd = max(digits_d - 1, 3)
575
+ tx = " alpha: " + fmt(params["alpha"], nd)
576
+ if do_trend:
577
+ tx += " beta: " + fmt(params["beta"], nd)
578
+ if do_seasons:
579
+ tx += " gamma: " + fmt(params["gamma"], nd)
580
+ report += ["Smoothing Parameters", tx, ""]
581
+ coef_v = np.r_[coefs["a"],
582
+ coefs["b"] if do_trend else [],
583
+ coefs["s"] if do_seasons else []]
584
+ coef_nm = (["b0"] + (["b1"] if do_trend else [])
585
+ + ([f"s{i+1}" for i in range(f)]
586
+ if do_seasons else []))
587
+
588
+ # lm: regression on (usually) deseasonalized data
589
+ elif ts_method == "lm":
590
+ if ts_trend is not None and ts_trend not in ("A", "N"):
591
+ raise ValueError('Enter either "A" for additive '
592
+ 'trend or "N" for no trend.')
593
+ if ts_seasons is not None and ts_seasons not in ("A", "N"):
594
+ raise ValueError('Enter either "A" for additive '
595
+ 'seasons or "N" for no seasons.')
596
+ do_trend = ts_trend == "A"
597
+ do_seasons = ts_seasons == "A"
598
+ n = len(yv)
599
+
600
+ if do_seasons:
601
+ seas, stl_trend = _stl_periodic(yv, f)
602
+ # R: trend+remainder, or remainder + mean(y)
603
+ y_trend = yv - seas if do_trend \
604
+ else (yv - seas - stl_trend) + yv.mean()
605
+ else:
606
+ seas = np.zeros(n)
607
+ y_trend = yv if do_trend \
608
+ else np.full(n, yv.mean())
609
+
610
+ x_seq = np.arange(1, n + 1)
611
+ b1, b0 = np.polyfit(x_seq, y_trend, 1)
612
+ y_fit = (b0 + b1 * x_seq) + seas
613
+ x_fit = x_dates
614
+
615
+ sse = float(np.sum((yv - y_fit) ** 2))
616
+ n_param = 2 + (f if do_seasons else 0)
617
+ mse = sse / (n - n_param)
618
+
619
+ # seasonal indices of the forecast (plt.forecast.R
620
+ # lines 512-529): continue the cycle past the data
621
+ new_seq = np.arange(n + 1, n + ts_ahead + 1)
622
+ if do_seasons:
623
+ start_ind = (n % f) # 0-based next position
624
+ new_ind = (start_ind + np.arange(ts_ahead)) % f
625
+ new_seas = seas[:f][new_ind]
626
+ else:
627
+ new_seas = np.zeros(ts_ahead)
628
+ y_hat = (b0 + b1 * new_seq) + new_seas
629
+
630
+ x_mean = x_seq.mean()
631
+ sxx = np.sum((x_seq - x_mean) ** 2)
632
+ se_fore = np.sqrt(mse * (1 + 1 / n
633
+ + (new_seq - x_mean) ** 2 / sxx))
634
+ tq = sps.t.ppf((1 + ts_PI) / 2, n - n_param)
635
+ y_lwr = y_hat - tq * se_fore
636
+ y_upr = y_hat + tq * se_fore
637
+
638
+ coef_v = np.r_[b0, b1, seas[:f] if do_seasons else []]
639
+ coef_nm = ["b0", "b1"] + ([f"s{i+1}" for i in range(f)]
640
+ if do_seasons else [])
641
+ else:
642
+ raise ValueError('ts_method must be "es" or "lm"')
643
+
644
+ if coef_nm is not None: # classic report tail
645
+ report += ["Mean squared error of fit to data: "
646
+ + fmt(mse, digits_d + 1), ""]
647
+ ttl = "Coefficients for Linear Trend"
648
+ if ts_seasons not in (None, "N"):
649
+ ttl += " and Seasonality"
650
+ report.append(ttl)
651
+ line = ""
652
+ for nm, v in zip(coef_nm, coef_v):
653
+ if nm == "s1":
654
+ report.append(" " + line)
655
+ line = ""
656
+ line += f"{nm}: {fmt(v, digits_d + 1)} "
657
+ report.append(" " + line)
658
+
659
+ # forecast output data frame
660
+ dfmt = {"months": "%b %Y", "years": "%Y"}.get(ts_unit)
661
+ if ts_unit == "quarters":
662
+ dates_out = [f"{d.year} Q{d.quarter}" for d in x_hat]
663
+ elif dfmt:
664
+ dates_out = x_hat.strftime(dfmt)
665
+ else:
666
+ dates_out = x_hat.strftime("%Y-%m-%d")
667
+ forecast = pd.DataFrame({
668
+ x_name: dates_out,
669
+ "predicted": y_hat,
670
+ "lower": y_lwr,
671
+ "upper": y_upr,
672
+ "width": y_upr - y_lwr})
673
+
674
+ return {"x_fit": x_fit, "y_fit": y_fit,
675
+ "x_hat": x_hat, "y_hat": y_hat,
676
+ "y_lwr": y_lwr, "y_upr": y_upr,
677
+ "forecast": forecast, "report": report}