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 ADDED
@@ -0,0 +1,9 @@
1
+ from .lw import LW
2
+ from .elw import ELW
3
+ from .twostep import TwoStepELW
4
+
5
+ __all__ = [
6
+ 'LW',
7
+ 'ELW',
8
+ 'TwoStepELW'
9
+ ]
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]