pyelw 0.9.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.
- pyelw/__init__.py +9 -0
- pyelw/elw.py +165 -0
- pyelw/fracdiff.py +44 -0
- pyelw/lw.py +509 -0
- pyelw/optimization.py +143 -0
- pyelw/simulate.py +73 -0
- pyelw/twostep.py +239 -0
- pyelw-0.9.0.dist-info/METADATA +402 -0
- pyelw-0.9.0.dist-info/RECORD +12 -0
- pyelw-0.9.0.dist-info/WHEEL +5 -0
- pyelw-0.9.0.dist-info/licenses/LICENSE +27 -0
- pyelw-0.9.0.dist-info/top_level.txt +1 -0
pyelw/__init__.py
ADDED
pyelw/elw.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Optional, Dict, Any, Tuple
|
|
3
|
+
|
|
4
|
+
from .optimization import golden_section_search
|
|
5
|
+
from .fracdiff import fracdiff
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ELW:
|
|
9
|
+
"""
|
|
10
|
+
Exact Local Whittle estimator of Shimotsu and Phillips (2005).
|
|
11
|
+
|
|
12
|
+
References
|
|
13
|
+
----------
|
|
14
|
+
Shimotsu, K. and Phillips, P.C.B. (2005). Exact Local Whittle Estimation
|
|
15
|
+
of Fractional Integration. _Annals of Statistics_ 33, 1890--1933.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def objective(self, d: float, X: np.ndarray, m: int) -> float:
|
|
19
|
+
"""
|
|
20
|
+
Exact Local Whittle objective function of Shimotsu and Phillips (2005).
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
d : float
|
|
25
|
+
Memory parameter
|
|
26
|
+
X : np.ndarray
|
|
27
|
+
Time series
|
|
28
|
+
m : int
|
|
29
|
+
Number of frequencies to use
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
float
|
|
34
|
+
ELW objective function value, to be minimized
|
|
35
|
+
"""
|
|
36
|
+
n = len(X)
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
# Fractionally difference the original series
|
|
40
|
+
dx = fracdiff(X, d)
|
|
41
|
+
|
|
42
|
+
# Compute FFT and periodogram
|
|
43
|
+
fft_dx = np.fft.fft(dx)
|
|
44
|
+
I_dx = np.abs(fft_dx)**2 / (2 * np.pi * n)
|
|
45
|
+
|
|
46
|
+
# Use first m frequencies (excluding zero)
|
|
47
|
+
I_dx_m = I_dx[1:m+1] # frequencies 1, 2, ..., m
|
|
48
|
+
freqs = 2 * np.pi * np.arange(1, m+1, dtype=np.float64) / n
|
|
49
|
+
|
|
50
|
+
# ELW objective function
|
|
51
|
+
G_hat = np.mean(I_dx_m)
|
|
52
|
+
if G_hat <= 0:
|
|
53
|
+
return np.float64(np.inf)
|
|
54
|
+
|
|
55
|
+
first_term = np.log(G_hat)
|
|
56
|
+
second_term = -2 * d * np.mean(np.log(freqs))
|
|
57
|
+
obj = first_term + second_term
|
|
58
|
+
|
|
59
|
+
if not np.isfinite(obj):
|
|
60
|
+
return np.float64(np.inf)
|
|
61
|
+
|
|
62
|
+
return np.float64(obj)
|
|
63
|
+
|
|
64
|
+
except (OverflowError, ZeroDivisionError, ValueError):
|
|
65
|
+
return np.float64(np.inf)
|
|
66
|
+
|
|
67
|
+
def estimate(self,
|
|
68
|
+
X: np.ndarray,
|
|
69
|
+
m: Optional[int] = None,
|
|
70
|
+
bounds: Optional[Tuple[float, float]] = (-1.0, 2.2),
|
|
71
|
+
mean_est: Optional[str] = "none",
|
|
72
|
+
verbose: Optional[bool] = False) -> Dict[str, Any]:
|
|
73
|
+
"""
|
|
74
|
+
Exact local Whittle estimation of memory parameter d.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
X : np.ndarray
|
|
79
|
+
Time series data
|
|
80
|
+
m : int, optional
|
|
81
|
+
Number of frequencies to use
|
|
82
|
+
bounds: tuple[float, float], optional
|
|
83
|
+
Lower and upper bounds for golden section search
|
|
84
|
+
mean_est : str, optional
|
|
85
|
+
Form of mean estimation. One of ['mean', 'init', 'none'].
|
|
86
|
+
- 'mean': subtract sample mean (valid for d in (-1/2, 1))
|
|
87
|
+
- 'init': subtract initial value (valid for d > 0)
|
|
88
|
+
- 'none': no mean correction
|
|
89
|
+
verbose : bool, optional
|
|
90
|
+
Print diagnostic information
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
Dict[str, Any]
|
|
95
|
+
Dictionary with estimation results
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
# Mean adjustment (see Shimotsu, 2010, section 3)
|
|
99
|
+
if mean_est == 'mean':
|
|
100
|
+
# Subtract sample mean
|
|
101
|
+
X = X - np.mean(X)
|
|
102
|
+
elif mean_est == 'init':
|
|
103
|
+
# Subtract initial value
|
|
104
|
+
X = (X - X[0])[1:]
|
|
105
|
+
elif mean_est == 'none':
|
|
106
|
+
pass
|
|
107
|
+
else:
|
|
108
|
+
raise ValueError("mean_est must be one of 'mean', 'init', 'none'")
|
|
109
|
+
|
|
110
|
+
# Sample size
|
|
111
|
+
n = len(X)
|
|
112
|
+
if m is None:
|
|
113
|
+
m = int(n**0.65)
|
|
114
|
+
|
|
115
|
+
# ELW objective function
|
|
116
|
+
def objective_func(d: float) -> float:
|
|
117
|
+
return self.objective(d, X, m)
|
|
118
|
+
|
|
119
|
+
# Optimize using golden section search with bounds
|
|
120
|
+
result = golden_section_search(objective_func, brack=bounds)
|
|
121
|
+
|
|
122
|
+
if not result.success:
|
|
123
|
+
if verbose:
|
|
124
|
+
print(f"Warning: {result.message}")
|
|
125
|
+
|
|
126
|
+
if not np.isfinite(result.x) or not np.isfinite(result.fun):
|
|
127
|
+
d_hat = np.nan
|
|
128
|
+
final_obj = np.nan
|
|
129
|
+
else:
|
|
130
|
+
d_hat = result.x
|
|
131
|
+
final_obj = result.fun
|
|
132
|
+
|
|
133
|
+
# Standard error based on Fisher information
|
|
134
|
+
if np.isfinite(d_hat):
|
|
135
|
+
try:
|
|
136
|
+
# Finite difference approximation of second derivative
|
|
137
|
+
dl = d_hat * 0.99
|
|
138
|
+
du = d_hat * 1.01
|
|
139
|
+
fl = objective_func(dl)
|
|
140
|
+
fu = objective_func(du)
|
|
141
|
+
d2 = 1.0e4*(fl - 2*final_obj + fu)/d_hat**2
|
|
142
|
+
# Check for convexity
|
|
143
|
+
if (d2 > 0):
|
|
144
|
+
se = np.sqrt(1/(m*d2))
|
|
145
|
+
else:
|
|
146
|
+
se = np.nan
|
|
147
|
+
|
|
148
|
+
except Exception:
|
|
149
|
+
se = np.nan
|
|
150
|
+
else:
|
|
151
|
+
se = np.nan
|
|
152
|
+
|
|
153
|
+
# Asymptotic standard error
|
|
154
|
+
ase = 1 / (2 * np.sqrt(m))
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
'n': n,
|
|
158
|
+
'm': m,
|
|
159
|
+
'd_hat': d_hat,
|
|
160
|
+
'se': se,
|
|
161
|
+
'ase': ase,
|
|
162
|
+
'objective': final_obj,
|
|
163
|
+
'nfev': result.nfev,
|
|
164
|
+
'method': 'elw',
|
|
165
|
+
}
|
pyelw/fracdiff.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def fracdiff(x: np.ndarray, d: float) -> np.ndarray:
|
|
5
|
+
"""
|
|
6
|
+
Apply fractional differencing operator (1-L)^d to time series.
|
|
7
|
+
|
|
8
|
+
Fast fractional differencing algorithm of Jensen and Nielsen (2014).
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
x : np.ndarray
|
|
13
|
+
Input time series
|
|
14
|
+
d : float
|
|
15
|
+
Fractional differencing parameter
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
np.ndarray
|
|
20
|
+
Fractionally differenced series (same length as input)
|
|
21
|
+
"""
|
|
22
|
+
n = len(x)
|
|
23
|
+
|
|
24
|
+
if n == 0:
|
|
25
|
+
return x
|
|
26
|
+
|
|
27
|
+
# Find next power of 2
|
|
28
|
+
np2 = 1 << (2*n - 1).bit_length()
|
|
29
|
+
|
|
30
|
+
# Single allocation for coefficients with padding
|
|
31
|
+
b_full = np.zeros(np2)
|
|
32
|
+
b_full[0] = 1.0
|
|
33
|
+
|
|
34
|
+
# Compute coefficients in-place
|
|
35
|
+
if n > 1:
|
|
36
|
+
k = np.arange(1, n, dtype=np.float64)
|
|
37
|
+
b_full[1:n] = np.cumprod((k - d - 1) / k)
|
|
38
|
+
|
|
39
|
+
# Use rfft for real inputs
|
|
40
|
+
x_fft = np.fft.rfft(x, n=np2)
|
|
41
|
+
b_fft = np.fft.rfft(b_full)
|
|
42
|
+
|
|
43
|
+
# Compute and return
|
|
44
|
+
return np.fft.irfft(x_fft * b_fft, n=np2)[:n]
|