BhalalaDaksh commited on
Commit
6a65e1e
·
verified ·
1 Parent(s): 90f24c4

docs: update license header in compactts/agent/feature_extractor.py to BSL-1.1

Browse files
Files changed (1) hide show
  1. compactts/agent/feature_extractor.py +220 -0
compactts/agent/feature_extractor.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Mathematically Rigorous, Scale-Invariant Time-Series Feature Extractor.
2
+
3
+ Extracts principled statistical, spectral, temporal, and information-theoretic
4
+ signatures from raw time-series trajectories for leak-free autonomous agent dispatching.
5
+
6
+ Guarantees:
7
+ - Strict Scale Invariance: Multiplying y by any scalar c > 0 yields identical routing features.
8
+ - Strict Zero Leakage: No dataset names, metadata lookups, or benchmark splits.
9
+ - Pure Numerical Input: Operates exclusively on array y in R^T and runtime constraints (H, freq).
10
+
11
+ Attribution: Daksh Bhalala
12
+ License: Business Source License 1.1 (BSL-1.1)
13
+ """
14
+
15
+ from typing import Dict, Any, Union
16
+ import numpy as np
17
+ import pandas as pd
18
+ from scipy import stats
19
+ from scipy.signal import periodogram
20
+
21
+
22
+ class TimeSeriesFeatureExtractor:
23
+ """Scale-invariant, robust feature extractor for autonomous time-series profiling."""
24
+
25
+ FREQ_TO_SECONDS = {
26
+ "10S": 10.0,
27
+ "5T": 300.0,
28
+ "10T": 600.0,
29
+ "15T": 900.0,
30
+ "H": 3600.0,
31
+ "D": 86400.0,
32
+ "W": 604800.0,
33
+ "M": 2629746.0,
34
+ "Q": 7889238.0,
35
+ "A": 31556952.0,
36
+ }
37
+
38
+ FREQ_TO_SEASONALITY = {
39
+ "10S": 60,
40
+ "5T": 12,
41
+ "10T": 6,
42
+ "15T": 4,
43
+ "H": 24,
44
+ "D": 7,
45
+ "W": 52,
46
+ "M": 12,
47
+ "Q": 4,
48
+ "A": 1,
49
+ }
50
+
51
+ def __init__(self, freq: str = "H"):
52
+ self.freq = freq
53
+ self.period = self.FREQ_TO_SEASONALITY.get(freq, 24)
54
+ self.sampling_interval_seconds = self.FREQ_TO_SECONDS.get(freq, 3600.0)
55
+
56
+ def extract(self, series: Union[np.ndarray, list], horizon: int = 24) -> Dict[str, float]:
57
+ """Extract a scale-invariant, comprehensive feature vector.
58
+
59
+ Parameters
60
+ ----------
61
+ series : Union[np.ndarray, list]
62
+ Raw historical trajectory y in R^T.
63
+ horizon : int
64
+ Forecast horizon H requested by the user.
65
+
66
+ Returns
67
+ -------
68
+ Dict[str, float]
69
+ Scale-invariant features capturing dynamics, intermittency, shifts, and spectrum.
70
+ """
71
+ y = np.asarray(series, dtype=np.float64)
72
+ if y.ndim > 1:
73
+ y = y.squeeze()
74
+ if y.ndim > 1:
75
+ # Multivariate: select primary variate
76
+ y = y[0] if y.shape[0] < y.shape[1] else y[:, 0]
77
+
78
+ # Handle missing values and infs cleanly
79
+ if np.isnan(y).any() or np.isinf(y).any():
80
+ s = pd.Series(y).ffill().bfill().to_numpy()
81
+ y = np.nan_to_num(s, nan=0.0).astype(np.float64)
82
+
83
+ n = len(y)
84
+ if n < 4:
85
+ return self._fallback_features(n, horizon)
86
+
87
+ # 1. Physical Support Characteristics
88
+ min_val = float(np.min(y))
89
+ max_val = float(np.max(y))
90
+ is_strictly_positive = 1.0 if min_val >= 0.0 else 0.0
91
+ val_range = max_val - min_val
92
+
93
+ # 2. Intermittency & Sparsity
94
+ # Ratio of exact or near-zeros (relative to overall scale)
95
+ threshold = 1e-5 if val_range < 1e-4 else 1e-4 * val_range
96
+ zero_ratio = float(np.mean(np.abs(y) <= threshold))
97
+
98
+ # 3. Scale-Normalized Moments
99
+ mean_val = float(np.mean(y))
100
+ std_val = float(np.std(y))
101
+ if std_val < 1e-8:
102
+ return self._constant_series_features(n, horizon, is_strictly_positive)
103
+
104
+ # Z-score normalization for scale invariance
105
+ z = (y - mean_val) / std_val
106
+
107
+ # Higher-order normalized moments
108
+ try:
109
+ kurt = float(stats.kurtosis(z))
110
+ skew = float(stats.skew(z))
111
+ except Exception:
112
+ kurt, skew = 0.0, 0.0
113
+
114
+ # 4. Non-Stationarity & Regime Shifts
115
+ # Evaluate divergence between historical past and recent window
116
+ half = n // 2
117
+ past_z = z[:half]
118
+ recent_z = z[half:]
119
+ mean_shift = abs(float(np.mean(recent_z)) - float(np.mean(past_z)))
120
+ std_shift = abs(float(np.std(recent_z)) - float(np.std(past_z)))
121
+
122
+ # 5. Temporal Autocorrelation & Stationarity
123
+ # First lag autocorrelation (persistence)
124
+ if n > 2:
125
+ try:
126
+ ac1 = float(np.corrcoef(z[1:], z[:-1])[0, 1])
127
+ if not np.isfinite(ac1):
128
+ ac1 = 0.0
129
+ except Exception:
130
+ ac1 = 0.0
131
+ else:
132
+ ac1 = 0.0
133
+
134
+ # Seasonal lag autocorrelation
135
+ S = self.period
136
+ seasonal_corr = 0.0
137
+ if n > 2 * S and S > 1:
138
+ try:
139
+ ac_s = float(np.corrcoef(z[S:], z[:-S])[0, 1])
140
+ if np.isfinite(ac_s):
141
+ seasonal_corr = max(0.0, ac_s)
142
+ except Exception:
143
+ seasonal_corr = 0.0
144
+
145
+ # 6. Spectral Energy & Seasonality Purity
146
+ # Computes power spectrum via periodogram
147
+ spectral_entropy = 0.5
148
+ dominant_freq_energy_ratio = 0.0
149
+ if n >= 16:
150
+ try:
151
+ _, pxx = periodogram(z)
152
+ pxx_sum = np.sum(pxx)
153
+ if pxx_sum > 1e-8:
154
+ pxx_norm = pxx / pxx_sum
155
+ # Shannon entropy of power spectrum (normalized to [0, 1])
156
+ pxx_pos = pxx_norm[pxx_norm > 1e-12]
157
+ h_spec = -np.sum(pxx_pos * np.log(pxx_pos))
158
+ spectral_entropy = float(h_spec / np.log(len(pxx)))
159
+ dominant_freq_energy_ratio = float(np.max(pxx_norm))
160
+ except Exception:
161
+ pass
162
+
163
+ # 7. ARCH Volatility Clustering (Autocorrelation of squared returns)
164
+ diff_z = np.diff(z)
165
+ vol_clustering = 0.0
166
+ if len(diff_z) > 4:
167
+ sq_diff = diff_z**2
168
+ sq_std = float(np.std(sq_diff))
169
+ if sq_std > 1e-6:
170
+ try:
171
+ c_arch = float(np.corrcoef(sq_diff[1:], sq_diff[:-1])[0, 1])
172
+ if np.isfinite(c_arch):
173
+ vol_clustering = max(0.0, c_arch)
174
+ except Exception:
175
+ pass
176
+
177
+ # 8. Horizon-to-Context Ratio
178
+ horizon_ratio = float(horizon / min(n, 512))
179
+
180
+ return {
181
+ "length": float(n),
182
+ "zero_ratio": zero_ratio,
183
+ "is_strictly_positive": is_strictly_positive,
184
+ "kurtosis": kurt,
185
+ "skewness": skew,
186
+ "mean_shift": mean_shift,
187
+ "std_shift": std_shift,
188
+ "autocorr_lag1": ac1,
189
+ "seasonal_corr": seasonal_corr,
190
+ "spectral_entropy": spectral_entropy,
191
+ "dominant_spectral_energy": dominant_freq_energy_ratio,
192
+ "volatility_clustering": vol_clustering,
193
+ "horizon_ratio": horizon_ratio,
194
+ "sampling_seconds_log": float(np.log10(max(self.sampling_interval_seconds, 1.0))),
195
+ }
196
+
197
+ def _fallback_features(self, n: int, horizon: int) -> Dict[str, float]:
198
+ return {
199
+ "length": float(n),
200
+ "zero_ratio": 0.0,
201
+ "is_strictly_positive": 1.0,
202
+ "kurtosis": 0.0,
203
+ "skewness": 0.0,
204
+ "mean_shift": 0.0,
205
+ "std_shift": 0.0,
206
+ "autocorr_lag1": 0.0,
207
+ "seasonal_corr": 0.0,
208
+ "spectral_entropy": 1.0,
209
+ "dominant_spectral_energy": 0.0,
210
+ "volatility_clustering": 0.0,
211
+ "horizon_ratio": float(horizon / max(n, 1)),
212
+ "sampling_seconds_log": float(np.log10(max(self.sampling_interval_seconds, 1.0))),
213
+ }
214
+
215
+ def _constant_series_features(self, n: int, horizon: int, is_pos: float) -> Dict[str, float]:
216
+ res = self._fallback_features(n, horizon)
217
+ res["is_strictly_positive"] = is_pos
218
+ res["autocorr_lag1"] = 1.0
219
+ res["spectral_entropy"] = 0.0
220
+ return res