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 +0 -0
- arfima-1.0.0/PKG-INFO +16 -0
- arfima-1.0.0/README.md +0 -0
- arfima-1.0.0/pyproject.toml +26 -0
- arfima-1.0.0/setup.cfg +4 -0
- arfima-1.0.0/src/__init__.py +4 -0
- arfima-1.0.0/src/arfima.egg-info/PKG-INFO +16 -0
- arfima-1.0.0/src/arfima.egg-info/SOURCES.txt +10 -0
- arfima-1.0.0/src/arfima.egg-info/dependency_links.txt +1 -0
- arfima-1.0.0/src/arfima.egg-info/requires.txt +2 -0
- arfima-1.0.0/src/arfima.egg-info/top_level.txt +2 -0
- arfima-1.0.0/src/core.py +326 -0
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,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 @@
|
|
|
1
|
+
|
arfima-1.0.0/src/core.py
ADDED
|
@@ -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
|
+
|