arfima 1.0.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.
arfima-1.0.0/LICENSE ADDED
File without changes
arfima-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: arfima
3
+ Version: 1.0.0
4
+ Summary: A custom AutoRegressive Fractionally Integrated Moving Average (ARFIMA) time series library.
5
+ Author-email: Shaad Hafeez <shaadhafeezofficial@gmail.com>
6
+ Project-URL: Homepage, https://github.com/github-shaad/arfima-python
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: numpy>=1.20.0
15
+ Requires-Dist: scipy>=1.7.0
16
+ Dynamic: license-file
arfima-1.0.0/README.md ADDED
File without changes
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "arfima" # This is the pip install name. Check PyPI to ensure it is unique!
7
+ version = "1.0.0"
8
+ authors = [
9
+ { name="Shaad Hafeez", email="shaadhafeezofficial@gmail.com" },
10
+ ]
11
+ description = "A custom AutoRegressive Fractionally Integrated Moving Average (ARFIMA) time series library."
12
+ readme = "README.md"
13
+ requires-python = ">=3.9"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Scientific/Engineering :: Mathematics",
19
+ ]
20
+ dependencies = [
21
+ "numpy>=1.20.0",
22
+ "scipy>=1.7.0",
23
+ ]
24
+
25
+ [project.urls]
26
+ "Homepage" = "https://github.com/github-shaad/arfima-python"
arfima-1.0.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,4 @@
1
+ from .core import ARFIMA, _TimeSeries
2
+
3
+ __version__ = "1.0.0"
4
+
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: arfima
3
+ Version: 1.0.0
4
+ Summary: A custom AutoRegressive Fractionally Integrated Moving Average (ARFIMA) time series library.
5
+ Author-email: Shaad Hafeez <shaadhafeezofficial@gmail.com>
6
+ Project-URL: Homepage, https://github.com/github-shaad/arfima-python
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: numpy>=1.20.0
15
+ Requires-Dist: scipy>=1.7.0
16
+ Dynamic: license-file
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/__init__.py
5
+ src/core.py
6
+ src/arfima.egg-info/PKG-INFO
7
+ src/arfima.egg-info/SOURCES.txt
8
+ src/arfima.egg-info/dependency_links.txt
9
+ src/arfima.egg-info/requires.txt
10
+ src/arfima.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ numpy>=1.20.0
2
+ scipy>=1.7.0
@@ -0,0 +1,2 @@
1
+ __init__
2
+ core
@@ -0,0 +1,326 @@
1
+ import numpy as np
2
+ from scipy import signal
3
+ from scipy.optimize import minimize
4
+ import scipy.stats as stats
5
+ from numpy.typing import NDArray
6
+ """
7
+ AutoRegressive Fractionally Integrated Moving Average Time Series Library
8
+ """
9
+
10
+
11
+ class _TimeSeries:
12
+ """
13
+ Wrapper for a numpy array as a Time Series.
14
+ """
15
+ def __init__(self, data:NDArray[np.float64]):
16
+ self.values = np.array(data, dtype=float)
17
+ self.length = len(self.values)
18
+
19
+
20
+ class ARFIMA:
21
+ """
22
+ ARFIMA(p, d, q) Estimator.
23
+
24
+ Models time series data featuring long-memory processes through fractional
25
+ integration, where the difference parameter $d$ is non-integer, solving the
26
+ general equation $\phi(L)(1-L)^d y_t = \theta(L) w_t$.
27
+ """
28
+ def __init__(self):
29
+ self.p : int = 0
30
+ self.q : int = 0
31
+ self.d : float = 0
32
+ self.ar_coeffs = []
33
+ self.ma_coeffs = []
34
+
35
+ self.is_fitted = False
36
+
37
+ def __repr__(self):
38
+ return f"ARFIMA(p={self.p}, d={self.d}, q={self.q})"
39
+
40
+ def fit(self, time_series:_TimeSeries, p_list:list[int], q_list:list[int], method:str="L-BFGS-B",
41
+ criterion:str='aic'):
42
+ """
43
+ Fits the optimal ARFIMA structure using Maximum Likelihood Estimation (MLE) via a grid search.
44
+
45
+ Evaluates combinations of AR and MA lags to minimize the selected information criterion.
46
+ Upon convergence, calculates the inverse Hessian matrix to derive standard errors,
47
+ t-statistics, and p-values for statistical inference.
48
+
49
+ Parameters
50
+ ----------
51
+ time_series : _TimeSeries
52
+ The 1D time series data object to fit.
53
+ p_list : list[int]
54
+ A list of autoregressive (AR) lag orders to test (e.g., [0, 1, 2]).
55
+ q_list : list[int]
56
+ A list of moving average (MA) lag orders to test (e.g., [0, 1]).
57
+ method : str, default="L-BFGS-B"
58
+ The SciPy optimization algorithm to use.
59
+ Options:
60
+ * "L-BFGS-B" (Default, highly recommended for bounded parameters)
61
+ * "TNC"
62
+ * "SLSQP"
63
+ * "Powell"
64
+ criterion : str, default='aic'
65
+ The information criterion used to penalize model complexity.
66
+ Options:
67
+ * "aic" : Akaike Information Criterion
68
+ * "bic" : Bayesian Information Criterion
69
+ * "hqic" : Hannan-Quinn Information Criterion
70
+ * "aicc" : Corrected AIC
71
+ """
72
+ raw_data = time_series.values
73
+ best_ic_score = np.inf
74
+ best_params = None
75
+ n = len(raw_data)
76
+
77
+ ic_formulas = {
78
+ 'aic': lambda n, k, neg_ll: 2 * k + 2 * neg_ll,
79
+ 'bic': lambda n, k, neg_ll: k * np.log(n) + 2 * neg_ll,
80
+ 'hqic': lambda n, k, neg_ll: 2 * k * np.log(np.log(n)) + 2 * neg_ll,
81
+ 'aicc': lambda n, k, neg_ll: (2 * k + 2 * neg_ll) + (2 * k**2 + 2 * k) / (n - k - 1) if (n - k - 1) > 0 else np.inf
82
+ }
83
+
84
+ criterion = criterion.lower()
85
+
86
+ if criterion not in ic_formulas:
87
+ raise ValueError(f"Invalid Information Criteria {criterion}. Choose from {", ".join(ic_formulas.keys())}")
88
+
89
+ for p in p_list:
90
+ for q in q_list:
91
+
92
+ initial_guess = [0.1] + [0.0]*p + [0.0]*q
93
+ bounds = [(-0.49,0.49)] + [(-0.99,0.99)]*(p+q)
94
+
95
+ res = minimize(fun=_score_function,
96
+ x0=initial_guess,
97
+ args=(raw_data, p),
98
+ method=method,
99
+ bounds=bounds)
100
+
101
+ if res.success:
102
+ k = p+q+1
103
+ negative_likelihood = res.fun
104
+ ic_score = ic_formulas[criterion](n, k, negative_likelihood)
105
+
106
+ if ic_score < best_ic_score:
107
+ best_ic_score = ic_score
108
+ self.p = p
109
+ self.q = q
110
+ best_params = res.x
111
+ else:
112
+ print(f"Failed to fit ARFIMA({p}, d, {q}): {res.message}")
113
+
114
+ if best_params is not None:
115
+ final_initial_guesses = [0.1] + [0.0] * self.p + [0.0] * self.q
116
+ final_bnds = [(-0.49, 0.49)] + [(-0.99, 0.99)] * (self.p + self.q)
117
+
118
+ final_result = minimize(
119
+ fun=_score_function,
120
+ x0=final_initial_guesses,
121
+ args=(raw_data, self.p),
122
+ method='L-BFGS-B',
123
+ bounds=final_bnds
124
+ )
125
+
126
+ best_params = final_result.x
127
+ self.d = best_params[0]
128
+ self.ar_coeffs = best_params[1 : self.p + 1] if self.p > 0 else []
129
+ self.ma_coeffs = best_params[self.p + 1 :] if self.q > 0 else []
130
+ self.is_fitted = True
131
+
132
+
133
+ inv_hessian = final_result.hess_inv.todense()
134
+ variances = np.diag(inv_hessian)
135
+ self.std_errors = np.sqrt(np.abs(variances))
136
+ self.t_stats = best_params / self.std_errors
137
+ self.p_values = 2 * stats.norm.sf(np.abs(self.t_stats))
138
+ print(f"\nFinal Fit Complete: ARFIMA({self.p}, {self.d:.4f}, {self.q})")
139
+
140
+ def summary(self):
141
+ """
142
+ Prints a formatted statistical regression table for the fitted model.
143
+
144
+ Outputs the estimated parameters (fractional differencing $d$, AR, and MA coefficients),
145
+ along with their corresponding standard errors, t-statistics, and two-tailed p-values
146
+ derived from the inverse Hessian matrix.
147
+
148
+ Returns
149
+ -------
150
+ None
151
+ This method prints directly to the console and does not return any objects.
152
+
153
+ Prints
154
+ ------
155
+ str
156
+ A formatted ASCII table containing the model structure and statistical inference metrics.
157
+ """
158
+ if not self.is_fitted:
159
+ print("Model is not fitted yet.")
160
+ return
161
+
162
+ print("\n" + "="*50)
163
+ print(f"{'ARFIMA MODEL SUMMARY':^50}")
164
+ print("="*50)
165
+ print(f"Model Structure: ARFIMA({self.p}, d, {self.q})")
166
+ print("-" * 50)
167
+
168
+ print(f"{'Parameter':<10} | {'Coef':>8} | {'Std Err':>8} | {'t-stat':>8} | {'P>|t|':>8}")
169
+ print("-" * 50)
170
+
171
+ def print_row(name, index):
172
+ coef = getattr(self, 'd') if name == 'd' else (self.ar_coeffs[index-1] if 'AR' in name else self.ma_coeffs[index-1 - self.p])
173
+ actual_index = 0 if name == 'd' else index
174
+
175
+ print(f"{name:<10} | {coef:>8.4f} | {self.std_errors[actual_index]:>8.4f} | {self.t_stats[actual_index]:>8.4f} | {self.p_values[actual_index]:>8.4f}")
176
+
177
+ print_row('d', 0)
178
+
179
+ for i in range(1, self.p + 1):
180
+ print_row(f'AR({i})', i)
181
+
182
+ for j in range(1, self.q + 1):
183
+ print_row(f'MA({j})', self.p + j)
184
+
185
+ print("="*50 + "\n")
186
+
187
+ def predict(self, time_series:_TimeSeries, steps=0):
188
+ """
189
+ Forecasts future values of the time series out-of-sample.
190
+
191
+ Mathematically projects the series forward by assuming all future
192
+ random shocks ($w_t$) are exactly zero. The projected shocks are then
193
+ fractionally integrated using $-d$ to restore the original scale of the data.
194
+
195
+ Parameters
196
+ ----------
197
+ time_series : _TimeSeries
198
+ The historical 1D time series data object used as the baseline for the forecast.
199
+ steps : int, default=0
200
+ The number of future time periods to forecast.
201
+
202
+ Returns
203
+ -------
204
+ np.ndarray
205
+ A 1D array of length `steps` containing the out-of-sample forecasted values.
206
+
207
+ Raises
208
+ ------
209
+ ValueError
210
+ If the method is called before the model has been successfully fitted.
211
+ """
212
+ if not self.is_fitted:
213
+ raise ValueError("ARFIMA not fitted yet")
214
+
215
+ y_history = _frac_diff(time_series, self.d)
216
+ w_history = _fast_residuals(y_history, self.ar_coeffs, self.ma_coeffs)
217
+
218
+ w_future = np.zeros(steps)
219
+ w_full = np.concatenate((w_history, w_future))
220
+
221
+ y_full = _fast_residuals(w_full, self.ma_coeffs, self.ar_coeffs)
222
+ integrated_series = _frac_diff(_TimeSeries(y_full), -self.d)
223
+
224
+ return integrated_series[-steps:]
225
+
226
+ def predict_in_sample(self, time_series):
227
+ """
228
+ Generates in-sample fitted values for the historical data.
229
+
230
+ Reconstructs the model's historical fit by calculating the exact historical
231
+ residuals (shocks) via linear filtering, and subtracting those residuals
232
+ from the raw input data.
233
+
234
+ Parameters
235
+ ----------
236
+ time_series : _TimeSeries
237
+ The 1D time series data object that the model was trained on.
238
+
239
+ Returns
240
+ -------
241
+ np.ndarray
242
+ A 1D array of fitted values, equal in length to the input time series.
243
+
244
+ Raises
245
+ ------
246
+ ValueError
247
+ If the method is called before the model has been successfully fitted.
248
+ """
249
+ if not self.is_fitted:
250
+ raise ValueError("ARFIMA not fitted yet")
251
+
252
+ raw_data = time_series.values
253
+
254
+ ar_poly = [1.0] + [-phi for phi in self.ar_coeffs]
255
+ ma_poly = [1.0] + list(self.ma_coeffs)
256
+
257
+ y_history = _frac_diff(time_series, self.d)
258
+
259
+ w_history = np.asarray(signal.lfilter(ar_poly, ma_poly, y_history))
260
+
261
+ fitted_values = raw_data - w_history
262
+
263
+ return fitted_values
264
+
265
+ def _score_function(params, raw_data, p):
266
+ n = len(raw_data)
267
+
268
+ d_guess = float(params[0])
269
+ ar_coeffs = params[1 : p + 1]
270
+ ma_coeffs = params[p + 1 :]
271
+
272
+ ts_obj = _TimeSeries(raw_data)
273
+ y_differenced = _frac_diff(ts_obj, d_guess)
274
+
275
+ residuals = _fast_residuals(y_differenced, ar_coeffs, ma_coeffs)
276
+
277
+ variance = np.sum(residuals ** 2) / n
278
+
279
+
280
+ if variance <= 1e-10 or np.isnan(variance):
281
+ return 1e10
282
+
283
+ neg_ll = (n / 2) * np.log(2 * np.pi) + (n / 2) * np.log(variance) + (n / 2)
284
+
285
+ if np.isnan(neg_ll) or np.isinf(neg_ll):
286
+ return 1e10
287
+
288
+ return neg_ll
289
+
290
+ def _fast_residuals(diff_data, ar_coeffs, ma_coeffs):
291
+ """
292
+ Computes residuals (nzzz)
293
+ """
294
+ b = [1.0] + [-i for i in ar_coeffs]
295
+ a = [1.0] + list(ma_coeffs)
296
+
297
+ residuals = signal.lfilter(b, a, diff_data)
298
+
299
+ return np.asarray(residuals)
300
+
301
+
302
+ def _frac_diff(series:_TimeSeries, d:int):
303
+ """
304
+ Fast fractional differencing. Uses fftconvolve to convolute weights and
305
+ the time series to return the fractionally differenced time series.
306
+ """
307
+ w = [1.0]
308
+ n = series.length
309
+ for i in range(1,n):
310
+ w_i = ((i-1-d) / i)*w[-1]
311
+ w.append(w_i)
312
+
313
+ conv_result = signal.fftconvolve(series.values, w, mode="full")
314
+
315
+ return conv_result[:n]
316
+
317
+
318
+
319
+
320
+
321
+
322
+
323
+
324
+
325
+
326
+