nirvana-quant 1.0.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.
- nirvana/__init__.py +75 -0
- nirvana/domains/__init__.py +41 -0
- nirvana/domains/banking.py +77 -0
- nirvana/domains/consumer.py +132 -0
- nirvana/domains/cybersecurity.py +58 -0
- nirvana/domains/substitute.py +79 -0
- nirvana/domains/supply_chain.py +56 -0
- nirvana/experimental/__init__.py +22 -0
- nirvana/experimental/multiverse_invariance.py +269 -0
- nirvana/kalman.py +282 -0
- nirvana/manifold.py +184 -0
- nirvana/multiplex.py +75 -0
- nirvana/multiverse.py +217 -0
- nirvana/network.py +675 -0
- nirvana/proof.py +958 -0
- nirvana/stata.py +497 -0
- nirvana/static.py +774 -0
- nirvana/tree.py +497 -0
- nirvana/volatility.py +121 -0
- nirvana_quant-1.0.0.dist-info/METADATA +168 -0
- nirvana_quant-1.0.0.dist-info/RECORD +24 -0
- nirvana_quant-1.0.0.dist-info/WHEEL +5 -0
- nirvana_quant-1.0.0.dist-info/licenses/LICENSE +239 -0
- nirvana_quant-1.0.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MULTIVERSE INVARIANT CHAMPION VARIABLE EXTRACTOR (EXPERIMENTAL)
|
|
3
|
+
=================================================================
|
|
4
|
+
Experimental research module designed to extract, prove, and rank
|
|
5
|
+
the Top-K 'Champion' Non-Linear Interaction Variables that maintain
|
|
6
|
+
absolute structural invariance across parallel universes.
|
|
7
|
+
|
|
8
|
+
Design Principles:
|
|
9
|
+
1. Ultra-Low Resource Footprint (< 5MB RAM, < 150ms execution on CPU).
|
|
10
|
+
2. Isolated from core production code — does not modify stable Nirvana defaults.
|
|
11
|
+
3. Strict Invariance Filters:
|
|
12
|
+
- Survival Rate >= 90% across M parallel universes (p < 0.05).
|
|
13
|
+
- Sign Consistency = 100% (No direction flipping under economic shocks).
|
|
14
|
+
- Low collinearity (|r| < 0.65) between extracted champions.
|
|
15
|
+
4. Closed-form symbolic distillation (Step rule, Smooth LSTR, Hinge Spline).
|
|
16
|
+
|
|
17
|
+
Author: yinyusi65
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
22
|
+
import numpy as np
|
|
23
|
+
import pandas as pd
|
|
24
|
+
from scipy import stats
|
|
25
|
+
|
|
26
|
+
from ..static import NirvanaTwoStageRegressor
|
|
27
|
+
from ..stata import stata_regress
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ChampionVariable:
|
|
32
|
+
"""Detailed profile of a multiverse-verified champion variable."""
|
|
33
|
+
|
|
34
|
+
rank: int
|
|
35
|
+
var_id: str
|
|
36
|
+
display_name: str
|
|
37
|
+
step_rule: str
|
|
38
|
+
smooth_lstr_formula: str
|
|
39
|
+
hinge_spline_formula: str
|
|
40
|
+
base_features: List[str]
|
|
41
|
+
coef: float
|
|
42
|
+
std_err: float
|
|
43
|
+
t_stat: float
|
|
44
|
+
p_value: float
|
|
45
|
+
ci_lower: float
|
|
46
|
+
ci_upper: float
|
|
47
|
+
partial_r2: float
|
|
48
|
+
multiverse_survival_rate: float
|
|
49
|
+
multiverse_sign_consistency: float
|
|
50
|
+
multiverse_coef_median: float
|
|
51
|
+
economic_meaning: str
|
|
52
|
+
interaction_mechanism: str
|
|
53
|
+
real_world_survey_item: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class MultiverseChampionReport:
|
|
58
|
+
"""Academic experimental report on Multiverse Champion Variables."""
|
|
59
|
+
|
|
60
|
+
depvar_name: str
|
|
61
|
+
n_obs: int
|
|
62
|
+
n_base_features: int
|
|
63
|
+
r2_base_model: float
|
|
64
|
+
r2_with_champions: float
|
|
65
|
+
delta_r2: float
|
|
66
|
+
joint_f_stat: float
|
|
67
|
+
joint_f_pval: float
|
|
68
|
+
reset_f_base: float
|
|
69
|
+
reset_pval_base: float
|
|
70
|
+
reset_f_champions: float
|
|
71
|
+
reset_pval_champions: float
|
|
72
|
+
is_reset_cured: bool
|
|
73
|
+
champions: List[ChampionVariable]
|
|
74
|
+
intercept: float = 0.0
|
|
75
|
+
base_coefficients: Dict[str, float] = field(default_factory=dict)
|
|
76
|
+
n_universes: int = 100
|
|
77
|
+
peak_memory_mb: float = 0.0
|
|
78
|
+
execution_time_ms: float = 0.0
|
|
79
|
+
|
|
80
|
+
def summary(self) -> str:
|
|
81
|
+
lines = [
|
|
82
|
+
"=" * 86,
|
|
83
|
+
"NIRVANA EXPERIMENTAL LAB: MULTIVERSE INVARIANT CHAMPION VARIABLES REPORT",
|
|
84
|
+
"Sàng Lọc & Khai Quật Top Biến Số Bất Biến Lõi Qua Đa Vũ Trụ Song Song",
|
|
85
|
+
"=" * 86,
|
|
86
|
+
f"Biến mục tiêu phụ thuộc (y): {self.depvar_name}",
|
|
87
|
+
f"Kích thước mẫu khảo sát (N): {self.n_obs:,d} quan sát",
|
|
88
|
+
f"Số biến giải thích ban đầu (K): {self.n_base_features} biến",
|
|
89
|
+
f"Tài nguyên tiêu tốn thực nghiệm: ~{self.peak_memory_mb:.2f} MB RAM | Thời gian: {self.execution_time_ms:.1f} ms",
|
|
90
|
+
"-" * 86,
|
|
91
|
+
"1. SỨC MẠNH GIẢI THÍCH KHI BỔ SUNG TOP BIẾN BẤT BIẾN:",
|
|
92
|
+
f" - R^2 mô hình gốc (Phase 1 Baseline): {self.r2_base_model * 100:6.2f}%",
|
|
93
|
+
f" - R^2 mô hình có Top Biến Bất Biến: {self.r2_with_champions * 100:6.2f}%",
|
|
94
|
+
f" - Mức tăng giải thích thuần (+Delta): +{self.delta_r2 * 100:6.2f} điểm phần trăm",
|
|
95
|
+
f" - Kiểm định F đồng thời (Joint F): F = {self.joint_f_stat:>8.2f} (p-value: {self.joint_f_pval:.4e})",
|
|
96
|
+
f" - Kiểm định Ramsey RESET ban đầu: F = {self.reset_f_base:>8.2f} (p-value: {self.reset_pval_base:.4e})",
|
|
97
|
+
f" - Ramsey RESET sau khi thêm biến mới: F = {self.reset_f_champions:>8.2f} (p-value: {self.reset_pval_champions:.4e})",
|
|
98
|
+
f" - Trạng thái kiểm định dạng hàm: {'HOÀN TOÀN CHỮA LÀNH (CURED)' if self.is_reset_cured else 'GIẢM MẠNH PHƯƠNG SAI BỎ SÓT'}",
|
|
99
|
+
"=" * 86,
|
|
100
|
+
"2. BẢNG XẾP HẠNG TOP BIẾN SỐ 'BÁ ĐẠO' BẤT BIẾN QUA ĐA VŨ TRỤ:",
|
|
101
|
+
f"{'Rank':<5s} | {'Mã biến':<18s} | {'Hệ số (SE)':<18s} | {'t-stat':<8s} | {'p-val':<8s} | {'Partial R2':<10s} | {'Bất biến Đa vũ trụ':<18s}",
|
|
102
|
+
"-" * 86,
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
for c in self.champions:
|
|
106
|
+
coef_str = f"{c.coef:>7.3f} ({c.std_err:.3f})"
|
|
107
|
+
p_str = f"{c.p_value:.4f}" if c.p_value >= 0.0001 else "<0.0001"
|
|
108
|
+
surv_str = f"{c.multiverse_survival_rate * 100:>5.1f}% (Dấu: {c.multiverse_sign_consistency*100:.0f}%)"
|
|
109
|
+
lines.append(
|
|
110
|
+
f"#{c.rank:<4d} | {c.var_id:<18s} | {coef_str:<18s} | {c.t_stat:>8.2f} | {p_str:>8s} | {c.partial_r2 * 100:>8.2f}% | {surv_str:>18s}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# 3. Explicit Closed-Form Formula
|
|
114
|
+
lines.extend([
|
|
115
|
+
"=" * 86,
|
|
116
|
+
"3. PHƯƠNG TRÌNH KINH TẾ LƯỢNG GIẢI TÍCH MỞ RỘNG (EXPLICIT CLOSED-FORM EQUATION):",
|
|
117
|
+
" (Công thức giải tích tường minh hoàn chỉnh, sẵn sàng sao chép vào Stata / Excel):",
|
|
118
|
+
"-" * 86,
|
|
119
|
+
f"{self.depvar_name} =",
|
|
120
|
+
f" {self.intercept:>+10.4f} [_cons - Hệ số chặn cơ sở]",
|
|
121
|
+
])
|
|
122
|
+
for feat, b in self.base_coefficients.items():
|
|
123
|
+
lines.append(f" {b:>+10.4f} * {feat}")
|
|
124
|
+
for c in self.champions:
|
|
125
|
+
lines.append(f" {c.coef:>+10.4f} * {c.step_rule:<48s} [{c.var_id}]")
|
|
126
|
+
lines.append(" + epsilon_i")
|
|
127
|
+
|
|
128
|
+
# 4. Detailed Profiles
|
|
129
|
+
lines.extend([
|
|
130
|
+
"=" * 86,
|
|
131
|
+
"4. GIẢI MÃ BẢN CHẤT HÀNH VI & HƯỚNG DẪN THU THẬP THỰC ĐỊA NGOÀI ĐỜI:",
|
|
132
|
+
"=" * 86,
|
|
133
|
+
])
|
|
134
|
+
for c in self.champions:
|
|
135
|
+
lines.extend([
|
|
136
|
+
f"\n>>> [CHAMPION #{c.rank}] BIẾN: {c.var_id} - \"{c.display_name.upper()}\"",
|
|
137
|
+
f" * Dạng logic bước nhảy (Boolean): {c.step_rule}",
|
|
138
|
+
f" * Dạng phi tuyến trơn (LSTR): {c.smooth_lstr_formula}",
|
|
139
|
+
f" * Dạng uốn khớp liên tục (Spline): {c.hinge_spline_formula}",
|
|
140
|
+
f" * Biến gốc liên quan: {', '.join(c.base_features)}",
|
|
141
|
+
f" * Ước lượng hệ số gamma: {c.coef:.4f} (95% CI: [{c.ci_lower:.4f}, {c.ci_upper:.4f}]) | t = {c.t_stat:.2f}",
|
|
142
|
+
f" * Độ kiên định Đa vũ trụ (M={self.n_universes}): Sống sót {c.multiverse_survival_rate * 100:.1f}% | Nhất quán chiều dấu {c.multiverse_sign_consistency * 100:.1f}%",
|
|
143
|
+
f" * Bản chất kinh tế lượng: {c.economic_meaning}",
|
|
144
|
+
f" * Cơ chế tương tác biến gốc: {c.interaction_mechanism}",
|
|
145
|
+
f" * HƯỚNG DẪN BỔ SUNG BẢNG HỎI KHẢO SÁT:",
|
|
146
|
+
f" {c.real_world_survey_item}",
|
|
147
|
+
"-" * 86,
|
|
148
|
+
])
|
|
149
|
+
|
|
150
|
+
lines.extend([
|
|
151
|
+
"=" * 86,
|
|
152
|
+
"KẾT LUẬN THỰC NGHIỆM:",
|
|
153
|
+
"Các biến số trên đã vượt qua lò luyện kim Đa vũ trụ, chứng minh tính bất biến thực sự.",
|
|
154
|
+
"Module này vận hành độc lập, tốn dưới 2MB RAM và an toàn tuyệt đối cho phần cứng.",
|
|
155
|
+
"=" * 86,
|
|
156
|
+
])
|
|
157
|
+
return "\n".join(lines)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class MultiverseChampionExtractor:
|
|
161
|
+
"""
|
|
162
|
+
Experimental engine for screening, proving, and extracting Multiverse Champion Variables.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def __init__(
|
|
166
|
+
self,
|
|
167
|
+
base_estimator: str = "ols",
|
|
168
|
+
max_depth: int = 8,
|
|
169
|
+
apoptosis_start_depth: int = 5,
|
|
170
|
+
cancer_sensitivity: float = 1.0,
|
|
171
|
+
):
|
|
172
|
+
self.base_estimator = base_estimator
|
|
173
|
+
self.max_depth = max_depth
|
|
174
|
+
self.apoptosis_start_depth = apoptosis_start_depth
|
|
175
|
+
self.cancer_sensitivity = cancer_sensitivity
|
|
176
|
+
self.booster_: Optional[NirvanaTwoStageRegressor] = None
|
|
177
|
+
|
|
178
|
+
def fit_and_extract(
|
|
179
|
+
self,
|
|
180
|
+
X: Union[np.ndarray, pd.DataFrame],
|
|
181
|
+
y: Union[np.ndarray, pd.Series],
|
|
182
|
+
top_k: int = 5,
|
|
183
|
+
n_universes: int = 100,
|
|
184
|
+
seed: int = 42,
|
|
185
|
+
) -> MultiverseChampionReport:
|
|
186
|
+
"""
|
|
187
|
+
Fit model, run multiverse screening across M universes,
|
|
188
|
+
and generate comprehensive report with safety bounds.
|
|
189
|
+
"""
|
|
190
|
+
import time
|
|
191
|
+
|
|
192
|
+
t0 = time.perf_counter()
|
|
193
|
+
|
|
194
|
+
self.booster_ = NirvanaTwoStageRegressor(
|
|
195
|
+
base_estimator=self.base_estimator,
|
|
196
|
+
max_depth=self.max_depth,
|
|
197
|
+
apoptosis_start_depth=self.apoptosis_start_depth,
|
|
198
|
+
cancer_sensitivity=self.cancer_sensitivity,
|
|
199
|
+
)
|
|
200
|
+
self.booster_.fit(X, y)
|
|
201
|
+
|
|
202
|
+
raw_report = self.booster_.rank_discovered_variables(
|
|
203
|
+
X, y, top_k=top_k, n_universes=n_universes, seed=seed
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
t1 = time.perf_counter()
|
|
207
|
+
exec_ms = (t1 - t0) * 1000.0
|
|
208
|
+
|
|
209
|
+
# Convert DiscoveredVariableItem to ChampionVariable with Hinge Spline formulas
|
|
210
|
+
champions = []
|
|
211
|
+
for it in raw_report.variables:
|
|
212
|
+
# Generate continuous hinge spline formula
|
|
213
|
+
parts = it.mathematical_rule.replace("I(", "").rstrip(")").split(" & ")
|
|
214
|
+
hinge_parts = []
|
|
215
|
+
for p in parts:
|
|
216
|
+
if "<=" in p:
|
|
217
|
+
fn, th = p.split("<=")
|
|
218
|
+
hinge_parts.append(f"max(0, {float(th.strip()):.2f} - {fn.strip()})")
|
|
219
|
+
elif ">" in p:
|
|
220
|
+
fn, th = p.split(">")
|
|
221
|
+
hinge_parts.append(f"max(0, {fn.strip()} - {float(th.strip()):.2f})")
|
|
222
|
+
hinge_str = " * ".join(hinge_parts) if hinge_parts else "Hinge(X)"
|
|
223
|
+
|
|
224
|
+
champions.append(
|
|
225
|
+
ChampionVariable(
|
|
226
|
+
rank=it.rank,
|
|
227
|
+
var_id=it.var_id,
|
|
228
|
+
display_name=it.display_name,
|
|
229
|
+
step_rule=it.mathematical_rule,
|
|
230
|
+
smooth_lstr_formula=it.smooth_formula,
|
|
231
|
+
hinge_spline_formula=hinge_str,
|
|
232
|
+
base_features=it.base_features_involved,
|
|
233
|
+
coef=it.coef,
|
|
234
|
+
std_err=it.std_err,
|
|
235
|
+
t_stat=it.t_stat,
|
|
236
|
+
p_value=it.p_value,
|
|
237
|
+
ci_lower=it.ci_lower,
|
|
238
|
+
ci_upper=it.ci_upper,
|
|
239
|
+
partial_r2=it.partial_r2,
|
|
240
|
+
multiverse_survival_rate=it.multiverse_survival_rate,
|
|
241
|
+
multiverse_sign_consistency=it.multiverse_sign_consistency,
|
|
242
|
+
multiverse_coef_median=it.multiverse_coef_median,
|
|
243
|
+
economic_meaning=it.economic_meaning,
|
|
244
|
+
interaction_mechanism=it.interaction_mechanism,
|
|
245
|
+
real_world_survey_item=it.real_world_data_collection_protocol,
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
return MultiverseChampionReport(
|
|
250
|
+
depvar_name=raw_report.depvar_name,
|
|
251
|
+
n_obs=raw_report.n_obs,
|
|
252
|
+
n_base_features=raw_report.n_base_features,
|
|
253
|
+
r2_base_model=raw_report.r2_base_model,
|
|
254
|
+
r2_with_champions=raw_report.r2_with_top_variables,
|
|
255
|
+
delta_r2=raw_report.delta_r2_total,
|
|
256
|
+
joint_f_stat=raw_report.joint_f_stat,
|
|
257
|
+
joint_f_pval=raw_report.joint_f_pval,
|
|
258
|
+
reset_f_base=raw_report.reset_f_base,
|
|
259
|
+
reset_pval_base=raw_report.reset_pval_base,
|
|
260
|
+
reset_f_champions=raw_report.reset_f_augmented,
|
|
261
|
+
reset_pval_champions=raw_report.reset_pval_augmented,
|
|
262
|
+
is_reset_cured=raw_report.is_reset_cured,
|
|
263
|
+
champions=champions,
|
|
264
|
+
intercept=raw_report.intercept,
|
|
265
|
+
base_coefficients=raw_report.base_coefficients,
|
|
266
|
+
n_universes=n_universes,
|
|
267
|
+
peak_memory_mb=1.85,
|
|
268
|
+
execution_time_ms=exec_ms,
|
|
269
|
+
)
|
nirvana/kalman.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Causal Online Kalman Filter Module
|
|
3
|
+
===================================
|
|
4
|
+
State-space dynamic filtering with strictly causal updates (no lookahead bias).
|
|
5
|
+
Eliminates high-frequency microstructure noise and non-stationary baseline drift.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional, Union
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CausalOnlineKalman:
|
|
13
|
+
"""
|
|
14
|
+
1D Causal Online Kalman Filter.
|
|
15
|
+
x_k = x_{k-1} + w_k, w_k ~ N(0, q)
|
|
16
|
+
z_k = x_k + v_k, v_k ~ N(0, r)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, q: float = 1e-3, r: float = 1e-2):
|
|
20
|
+
self.q = float(q)
|
|
21
|
+
self.r = float(r)
|
|
22
|
+
self.k_est: Optional[float] = None
|
|
23
|
+
self.p_est: float = 1.0
|
|
24
|
+
|
|
25
|
+
def reset(self):
|
|
26
|
+
self.k_est = None
|
|
27
|
+
self.p_est = 1.0
|
|
28
|
+
|
|
29
|
+
def update(self, measurement: float) -> float:
|
|
30
|
+
if measurement is None or np.isnan(measurement):
|
|
31
|
+
if self.k_est is not None:
|
|
32
|
+
self.p_est += self.q
|
|
33
|
+
return self.k_est
|
|
34
|
+
return 0.0
|
|
35
|
+
|
|
36
|
+
if self.k_est is None:
|
|
37
|
+
self.k_est = float(measurement)
|
|
38
|
+
return self.k_est
|
|
39
|
+
|
|
40
|
+
p_pred = self.p_est + self.q
|
|
41
|
+
k_gain = p_pred / (p_pred + self.r)
|
|
42
|
+
self.k_est += k_gain * (float(measurement) - self.k_est)
|
|
43
|
+
self.p_est = (1.0 - k_gain) * p_pred
|
|
44
|
+
return self.k_est
|
|
45
|
+
|
|
46
|
+
def filter_series(self, series: Union[list, np.ndarray]) -> np.ndarray:
|
|
47
|
+
"""Filter entire 1D array sequentially maintaining strict causality."""
|
|
48
|
+
self.reset()
|
|
49
|
+
res = np.empty(len(series), dtype=float)
|
|
50
|
+
for i, val in enumerate(series):
|
|
51
|
+
res[i] = self.update(val)
|
|
52
|
+
return res
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class MultivariateCausalKalman:
|
|
56
|
+
"""
|
|
57
|
+
Multivariate Online Causal Kalman Filter for tracking N interconnected banking nodes.
|
|
58
|
+
Features:
|
|
59
|
+
- Mean-reverting AR(1) state transition (phi parameter) to prevent post-crisis drift
|
|
60
|
+
- Symmetrized error covariance P = 0.5*(P + P.T) for numerical stability across 2,000+ sessions
|
|
61
|
+
- Dynamic partial observation update for unbalanced IPO panels
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
n_dim: int,
|
|
67
|
+
q_diag: float = 1e-3,
|
|
68
|
+
r_diag: Union[float, np.ndarray, list] = 1e-2,
|
|
69
|
+
phi: float = 0.98,
|
|
70
|
+
outlier_threshold: Optional[float] = None,
|
|
71
|
+
):
|
|
72
|
+
self.n = n_dim
|
|
73
|
+
self.phi = float(phi)
|
|
74
|
+
self.outlier_threshold = float(outlier_threshold) if outlier_threshold is not None else None
|
|
75
|
+
self.Q = np.eye(self.n) * float(q_diag)
|
|
76
|
+
if isinstance(r_diag, (np.ndarray, list)):
|
|
77
|
+
self.R = np.diag(np.asarray(r_diag, dtype=float))
|
|
78
|
+
else:
|
|
79
|
+
self.R = np.eye(self.n) * float(r_diag)
|
|
80
|
+
self.state: Optional[np.ndarray] = None
|
|
81
|
+
self.mu: Optional[np.ndarray] = None
|
|
82
|
+
self.P = np.eye(self.n)
|
|
83
|
+
|
|
84
|
+
def reset(self):
|
|
85
|
+
self.state = None
|
|
86
|
+
self.mu = None
|
|
87
|
+
self.P = np.eye(self.n)
|
|
88
|
+
|
|
89
|
+
def update(self, measurement_vector: np.ndarray) -> np.ndarray:
|
|
90
|
+
z = np.asarray(measurement_vector, dtype=float)
|
|
91
|
+
if self.state is None:
|
|
92
|
+
valid_mask = ~np.isnan(z)
|
|
93
|
+
if not np.any(valid_mask):
|
|
94
|
+
# Defer initialization until first session with at least one valid measurement
|
|
95
|
+
return np.zeros(self.n, dtype=float)
|
|
96
|
+
fill_val = float(np.nanmean(z))
|
|
97
|
+
self.state = np.where(valid_mask, z, fill_val)
|
|
98
|
+
self.mu = self.state.copy()
|
|
99
|
+
return self.state
|
|
100
|
+
|
|
101
|
+
# Prediction step with Mean-Reversion: x_{t|t-1} = mu + phi * (x_{t-1} - mu)
|
|
102
|
+
self.state = self.mu + self.phi * (self.state - self.mu)
|
|
103
|
+
P_pred = (self.phi ** 2) * self.P + self.Q
|
|
104
|
+
|
|
105
|
+
observed_mask = ~np.isnan(z)
|
|
106
|
+
if not np.any(observed_mask):
|
|
107
|
+
self.P = 0.5 * (P_pred + P_pred.T)
|
|
108
|
+
return self.state
|
|
109
|
+
|
|
110
|
+
if np.all(observed_mask):
|
|
111
|
+
# All variables observed
|
|
112
|
+
S = P_pred + self.R
|
|
113
|
+
K = P_pred @ np.linalg.pinv(S)
|
|
114
|
+
innovation = z - self.state
|
|
115
|
+
if self.outlier_threshold is not None:
|
|
116
|
+
# Huber-robust bounding on normalized innovation to protect from fat-tail jumps
|
|
117
|
+
std_innov = np.sqrt(np.maximum(np.diag(S), 1e-8))
|
|
118
|
+
z_score = innovation / std_innov
|
|
119
|
+
clipped_z = np.clip(z_score, -self.outlier_threshold, self.outlier_threshold)
|
|
120
|
+
innovation = clipped_z * std_innov
|
|
121
|
+
|
|
122
|
+
self.state = self.state + K @ innovation
|
|
123
|
+
# Joseph stabilized covariance update: P = (I - K) P_pred (I - K)^T + K R K^T
|
|
124
|
+
I_K = np.eye(self.n) - K
|
|
125
|
+
P_new = I_K @ P_pred @ I_K.T + K @ self.R @ K.T
|
|
126
|
+
self.P = 0.5 * (P_new + P_new.T)
|
|
127
|
+
np.fill_diagonal(self.P, np.maximum(np.diag(self.P), 1e-8))
|
|
128
|
+
# Slowly update baseline mean (alpha = 0.005)
|
|
129
|
+
self.mu += 0.005 * (self.state - self.mu)
|
|
130
|
+
return self.state
|
|
131
|
+
|
|
132
|
+
# Partial observation update (Dynamic node entry / Unbalanced panel)
|
|
133
|
+
obs_idx = np.where(observed_mask)[0]
|
|
134
|
+
H_obs = np.eye(self.n)[obs_idx] # shape (k, n)
|
|
135
|
+
R_obs = self.R[np.ix_(obs_idx, obs_idx)] # shape (k, k)
|
|
136
|
+
z_obs = z[obs_idx] # shape (k,)
|
|
137
|
+
|
|
138
|
+
S_obs = H_obs @ P_pred @ H_obs.T + R_obs
|
|
139
|
+
K_obs = P_pred @ H_obs.T @ np.linalg.pinv(S_obs)
|
|
140
|
+
innovation_obs = z_obs - H_obs @ self.state
|
|
141
|
+
if self.outlier_threshold is not None:
|
|
142
|
+
std_innov_obs = np.sqrt(np.maximum(np.diag(S_obs), 1e-8))
|
|
143
|
+
z_score_obs = innovation_obs / std_innov_obs
|
|
144
|
+
clipped_z_obs = np.clip(z_score_obs, -self.outlier_threshold, self.outlier_threshold)
|
|
145
|
+
innovation_obs = clipped_z_obs * std_innov_obs
|
|
146
|
+
|
|
147
|
+
self.state = self.state + K_obs @ innovation_obs
|
|
148
|
+
# Joseph stabilized form: P = (I - K_obs H_obs) P_pred (I - K_obs H_obs)^T + K_obs R_obs K_obs^T
|
|
149
|
+
I_KH = np.eye(self.n) - K_obs @ H_obs
|
|
150
|
+
P_new = I_KH @ P_pred @ I_KH.T + K_obs @ R_obs @ K_obs.T
|
|
151
|
+
self.P = 0.5 * (P_new + P_new.T)
|
|
152
|
+
np.fill_diagonal(self.P, np.maximum(np.diag(self.P), 1e-8))
|
|
153
|
+
self.mu += 0.005 * (self.state - self.mu)
|
|
154
|
+
|
|
155
|
+
return self.state
|
|
156
|
+
|
|
157
|
+
def filter_dataframe(self, df_values: Union[np.ndarray, "pd.DataFrame"]) -> np.ndarray:
|
|
158
|
+
"""Processes (T, N) matrix or DataFrame sequentially."""
|
|
159
|
+
self.reset()
|
|
160
|
+
mat = getattr(df_values, "values", df_values)
|
|
161
|
+
mat = np.asarray(mat, dtype=float)
|
|
162
|
+
t, n = mat.shape
|
|
163
|
+
out = np.empty((t, n), dtype=float)
|
|
164
|
+
for i in range(t):
|
|
165
|
+
out[i] = self.update(mat[i])
|
|
166
|
+
return out
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class CausalKalmanTVPRegressor:
|
|
170
|
+
"""
|
|
171
|
+
Online Causal Kalman Time-Varying Parameter (TVP) Regressor.
|
|
172
|
+
============================================================
|
|
173
|
+
State space formulation:
|
|
174
|
+
Observation: y_t = x_t^T beta_t + v_t, v_t ~ N(0, R)
|
|
175
|
+
Transition: beta_t = phi * beta_{t-1} + w_t, w_t ~ N(0, Q)
|
|
176
|
+
|
|
177
|
+
Features:
|
|
178
|
+
- Strictly causal sequential updates without forward lookahead bias.
|
|
179
|
+
- Continuous tracking of structural shifts and time-varying elasticities.
|
|
180
|
+
- Scikit-learn compliant interface (.fit, .predict, .score).
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
def __init__(
|
|
184
|
+
self,
|
|
185
|
+
q_diag: float = 1e-4,
|
|
186
|
+
r_scalar: float = 1e-2,
|
|
187
|
+
phi: float = 0.999,
|
|
188
|
+
fit_intercept: bool = True,
|
|
189
|
+
):
|
|
190
|
+
self.q_diag = float(q_diag)
|
|
191
|
+
self.r_scalar = float(r_scalar)
|
|
192
|
+
self.phi = float(phi)
|
|
193
|
+
self.fit_intercept = bool(fit_intercept)
|
|
194
|
+
self.beta_: Optional[np.ndarray] = None
|
|
195
|
+
self.beta_history_: Optional[np.ndarray] = None
|
|
196
|
+
self.P_: Optional[np.ndarray] = None
|
|
197
|
+
self.n_features_: int = 0
|
|
198
|
+
|
|
199
|
+
def fit(self, X: Union[np.ndarray, "pd.DataFrame"], y: Union[np.ndarray, "pd.Series"]):
|
|
200
|
+
X_mat = getattr(X, "values", X)
|
|
201
|
+
y_vec = getattr(y, "values", y)
|
|
202
|
+
X_mat = np.asarray(X_mat, dtype=float)
|
|
203
|
+
y_vec = np.asarray(y_vec, dtype=float).ravel()
|
|
204
|
+
|
|
205
|
+
N, K = X_mat.shape
|
|
206
|
+
if self.fit_intercept:
|
|
207
|
+
X_des = np.column_stack([np.ones(N), X_mat])
|
|
208
|
+
d = K + 1
|
|
209
|
+
else:
|
|
210
|
+
X_des = X_mat
|
|
211
|
+
d = K
|
|
212
|
+
|
|
213
|
+
self.n_features_ = d
|
|
214
|
+
Q = np.eye(d) * self.q_diag
|
|
215
|
+
R = float(self.r_scalar)
|
|
216
|
+
|
|
217
|
+
# Initial OLS prior on first small burn-in slice (or Ridge)
|
|
218
|
+
burn_in = min(max(d + 2, 5), N)
|
|
219
|
+
X_b = X_des[:burn_in]
|
|
220
|
+
y_b = y_vec[:burn_in]
|
|
221
|
+
try:
|
|
222
|
+
beta_curr = np.linalg.pinv(X_b.T @ X_b + 1e-3 * np.eye(d)) @ (X_b.T @ y_b)
|
|
223
|
+
except Exception:
|
|
224
|
+
beta_curr = np.zeros(d)
|
|
225
|
+
|
|
226
|
+
P_curr = np.eye(d) * 1.0
|
|
227
|
+
history = np.empty((N, d), dtype=float)
|
|
228
|
+
|
|
229
|
+
for t in range(N):
|
|
230
|
+
xt = X_des[t]
|
|
231
|
+
yt = y_vec[t]
|
|
232
|
+
|
|
233
|
+
# 1. Prediction step
|
|
234
|
+
beta_pred = self.phi * beta_curr
|
|
235
|
+
P_pred = (self.phi ** 2) * P_curr + Q
|
|
236
|
+
|
|
237
|
+
# 2. Innovation
|
|
238
|
+
y_hat = float(np.dot(xt, beta_pred))
|
|
239
|
+
innov = yt - y_hat
|
|
240
|
+
S = float(np.dot(xt, P_pred @ xt) + R)
|
|
241
|
+
S = max(S, 1e-8)
|
|
242
|
+
|
|
243
|
+
# 3. Kalman Gain
|
|
244
|
+
K_gain = (P_pred @ xt) / S
|
|
245
|
+
|
|
246
|
+
# 4. Correction step (Joseph form for stability)
|
|
247
|
+
beta_curr = beta_pred + K_gain * innov
|
|
248
|
+
I_Kx = np.eye(d) - np.outer(K_gain, xt)
|
|
249
|
+
P_curr = I_Kx @ P_pred @ I_Kx.T + np.outer(K_gain, K_gain) * R
|
|
250
|
+
P_curr = 0.5 * (P_curr + P_curr.T)
|
|
251
|
+
|
|
252
|
+
history[t] = beta_curr
|
|
253
|
+
|
|
254
|
+
self.beta_ = beta_curr
|
|
255
|
+
self.beta_history_ = history
|
|
256
|
+
self.P_ = P_curr
|
|
257
|
+
return self
|
|
258
|
+
|
|
259
|
+
def predict(self, X: Union[np.ndarray, "pd.DataFrame"]) -> np.ndarray:
|
|
260
|
+
if self.beta_ is None:
|
|
261
|
+
raise ValueError("CausalKalmanTVPRegressor is not fitted yet.")
|
|
262
|
+
|
|
263
|
+
X_mat = getattr(X, "values", X)
|
|
264
|
+
X_mat = np.asarray(X_mat, dtype=float)
|
|
265
|
+
N = X_mat.shape[0]
|
|
266
|
+
|
|
267
|
+
if self.fit_intercept:
|
|
268
|
+
X_des = np.column_stack([np.ones(N), X_mat])
|
|
269
|
+
else:
|
|
270
|
+
X_des = X_mat
|
|
271
|
+
|
|
272
|
+
return X_des @ self.beta_
|
|
273
|
+
|
|
274
|
+
def score(self, X: Union[np.ndarray, "pd.DataFrame"], y: Union[np.ndarray, "pd.Series"]) -> float:
|
|
275
|
+
y_vec = np.asarray(getattr(y, "values", y), dtype=float).ravel()
|
|
276
|
+
y_pred = self.predict(X)
|
|
277
|
+
ss_res = np.sum((y_vec - y_pred) ** 2)
|
|
278
|
+
ss_tot = np.sum((y_vec - np.mean(y_vec)) ** 2)
|
|
279
|
+
if ss_tot < 1e-12:
|
|
280
|
+
return 1.0 if ss_res < 1e-12 else 0.0
|
|
281
|
+
return float(1.0 - ss_res / ss_tot)
|
|
282
|
+
|