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
nirvana/static.py
ADDED
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NIRVANA STATIC DATA ECONOMETRICS & GENERAL-PURPOSE ESTIMATOR MODULE
|
|
3
|
+
====================================================================
|
|
4
|
+
General-purpose econometric and machine learning tools for static cross-sectional
|
|
5
|
+
and tabular datasets. Provides scikit-learn compliant estimators, universal two-stage
|
|
6
|
+
residual boosters, and formula interfaces.
|
|
7
|
+
|
|
8
|
+
Components:
|
|
9
|
+
1. NirvanaTreeRegressor:
|
|
10
|
+
Standalone 12-layer non-linear decision tree regressor with Empirical Fisher
|
|
11
|
+
Information and Margin-Density Apoptosis. Fully scikit-learn compliant (BaseEstimator,
|
|
12
|
+
RegressorMixin, Pipeline, GridSearchCV compatible).
|
|
13
|
+
|
|
14
|
+
2. NirvanaTwoStageRegressor (Universal Meta-Estimator):
|
|
15
|
+
Accepts ANY arbitrary base model (OLS, Ridge, Lasso, ElasticNet, SVR, DecisionTree, etc.)
|
|
16
|
+
as Phase 1, and fits Nirvana Phase 2 Apoptosis Tree on its residuals to rescue weak R^2 models.
|
|
17
|
+
|
|
18
|
+
3. parse_formula:
|
|
19
|
+
R / Stata-style formula parser ("y ~ x1 + x2 + x3") for DataFrame workflows.
|
|
20
|
+
|
|
21
|
+
Author: yinyusi65 & Nirvana Quant Team
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
26
|
+
import numpy as np
|
|
27
|
+
import pandas as pd
|
|
28
|
+
from scipy import stats
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
from sklearn.base import BaseEstimator, RegressorMixin
|
|
32
|
+
from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet
|
|
33
|
+
from sklearn.tree import DecisionTreeRegressor
|
|
34
|
+
from sklearn.model_selection import train_test_split
|
|
35
|
+
SKLEARN_AVAILABLE = True
|
|
36
|
+
except ImportError:
|
|
37
|
+
class BaseEstimator:
|
|
38
|
+
def get_params(self, deep=True):
|
|
39
|
+
return {}
|
|
40
|
+
|
|
41
|
+
def set_params(self, **params):
|
|
42
|
+
for k, v in params.items():
|
|
43
|
+
setattr(self, k, v)
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
class RegressorMixin:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
SKLEARN_AVAILABLE = False
|
|
50
|
+
|
|
51
|
+
from .tree import NirvanaTreeEngine, TrainingSample
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_formula(formula: str, df: pd.DataFrame) -> Tuple[pd.DataFrame, pd.Series, List[str]]:
|
|
55
|
+
"""
|
|
56
|
+
Parse an R / Stata-style regression formula (e.g. 'y ~ x1 + x2 + x3').
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
formula : str
|
|
61
|
+
Formula string formatted as 'depvar ~ indepvar1 + indepvar2 + ...'
|
|
62
|
+
df : pd.DataFrame
|
|
63
|
+
Dataset containing the variables.
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
Tuple[pd.DataFrame, pd.Series, List[str]]
|
|
68
|
+
(X dataframe, y series, list of feature names)
|
|
69
|
+
"""
|
|
70
|
+
if "~" not in formula:
|
|
71
|
+
raise ValueError(f"Invalid formula '{formula}'. Must contain '~' separating dependent and independent variables.")
|
|
72
|
+
|
|
73
|
+
lhs, rhs = formula.split("~", 1)
|
|
74
|
+
depvar = lhs.strip()
|
|
75
|
+
indepvars = [v.strip() for v in rhs.split("+") if v.strip()]
|
|
76
|
+
|
|
77
|
+
missing_cols = [c for c in [depvar] + indepvars if c not in df.columns]
|
|
78
|
+
if missing_cols:
|
|
79
|
+
raise KeyError(f"Columns not found in DataFrame: {missing_cols}")
|
|
80
|
+
|
|
81
|
+
clean_df = df[[depvar] + indepvars].dropna()
|
|
82
|
+
return clean_df[indepvars], clean_df[depvar], indepvars
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class StaticEconometricReport:
|
|
87
|
+
"""
|
|
88
|
+
Academic econometric report comparing Phase 1 (Baseline Weak Model)
|
|
89
|
+
against Phase 2 (Nirvana Empirical Fisher Apoptosis Enhancement).
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
n_samples: int
|
|
93
|
+
n_features: int
|
|
94
|
+
r2_phase1: float
|
|
95
|
+
r2_phase2: float
|
|
96
|
+
r2_gain: float
|
|
97
|
+
r2_relative_improvement_pct: float
|
|
98
|
+
residual_variance_reduction_pct: float
|
|
99
|
+
f_statistic: float
|
|
100
|
+
f_p_value: float
|
|
101
|
+
phase1_coefficients: Dict[str, float]
|
|
102
|
+
feature_importances: Dict[str, float]
|
|
103
|
+
pruning_stats: Dict[str, Union[int, float]]
|
|
104
|
+
top_substitution_thresholds: List[str] = field(default_factory=list)
|
|
105
|
+
pathology_alerts: List[Dict[str, Any]] = field(default_factory=list)
|
|
106
|
+
|
|
107
|
+
def summary(self) -> str:
|
|
108
|
+
lines = [
|
|
109
|
+
"=" * 78,
|
|
110
|
+
"NIRVANA STATIC ECONOMETRICS REPORT: PHASE 2 MODEL ENHANCEMENT",
|
|
111
|
+
"=" * 78,
|
|
112
|
+
f"Sample Size (N): {self.n_samples}",
|
|
113
|
+
f"Feature Count (K): {self.n_features}",
|
|
114
|
+
"-" * 78,
|
|
115
|
+
"1. MODEL EXPLANATORY POWER & R-SQUARED ELEVATION:",
|
|
116
|
+
f" - Phase 1 Baseline R^2 (Weak Model): {self.r2_phase1:.4f} ({self.r2_phase1 * 100:.2f}%)",
|
|
117
|
+
f" - Phase 2 Nirvana R^2 (Enhanced): {self.r2_phase2:.4f} ({self.r2_phase2 * 100:.2f}%)",
|
|
118
|
+
f" - Absolute R^2 Gain (+Delta R^2): +{self.r2_gain:.4f} (+{self.r2_gain * 100:.2f} percentage points)",
|
|
119
|
+
f" - Relative Explanatory Gain: +{self.r2_relative_improvement_pct:.2f}% of unexplained variance resolved",
|
|
120
|
+
f" - Residual Variance Reduction: {self.residual_variance_reduction_pct:.2f}%",
|
|
121
|
+
f" - Incremental F-Statistic: {self.f_statistic:.2f} (p-value: {self.f_p_value:.4e})",
|
|
122
|
+
"-" * 78,
|
|
123
|
+
"2. PHASE 2 EMPIRICAL FISHER MARGIN-DENSITY APOPTOSIS AUDIT:",
|
|
124
|
+
f" - Total Tree Nodes Generated: {self.pruning_stats.get('total_nodes', 0)}",
|
|
125
|
+
f" - Active Structural Leaves Retained: {self.pruning_stats.get('active_leaves', 0)}",
|
|
126
|
+
f" - Cancerous Overfitting Nodes Excised: {self.pruning_stats.get('pruned_apoptosis_nodes', 0)}",
|
|
127
|
+
f" - Cancer Pruning Ratio: {self.pruning_stats.get('prune_rate', 0.0) * 100:.2f}%",
|
|
128
|
+
"-" * 78,
|
|
129
|
+
"3. TOP NON-LINEAR FEATURE DRIVERS (PHASE 2 VARIANCE ABSORPTION):",
|
|
130
|
+
]
|
|
131
|
+
for feat, imp in list(self.feature_importances.items())[:6]:
|
|
132
|
+
lines.append(f" * {feat:25s}: {imp * 100:6.2f}%")
|
|
133
|
+
|
|
134
|
+
if self.top_substitution_thresholds:
|
|
135
|
+
lines.append("-" * 78)
|
|
136
|
+
lines.append("4. DISCOVERED STRUCTURAL & SUBSTITUTION THRESHOLDS:")
|
|
137
|
+
for rule in self.top_substitution_thresholds[:6]:
|
|
138
|
+
lines.append(f" -> {rule}")
|
|
139
|
+
|
|
140
|
+
if self.pathology_alerts:
|
|
141
|
+
lines.append("-" * 78)
|
|
142
|
+
lines.append("5. PATHOLOGY BIOPSY ALERTS (SENTINEL COUNTERFACTUAL CASE STUDIES):")
|
|
143
|
+
for alert in self.pathology_alerts[:5]:
|
|
144
|
+
lines.append(
|
|
145
|
+
f" [BIOPSY #{alert['node_idx']}] Depth {alert['depth']} | "
|
|
146
|
+
f"Divergence: {alert['divergence_sigma']} sigma | Support: {alert['sample_count']} samples "
|
|
147
|
+
f"| Indices: {alert['sample_indices']} | Trigger: {alert.get('trigger_rule', 'N/A')}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
lines.append("=" * 78)
|
|
151
|
+
return "\n".join(lines)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class NirvanaTreeRegressor(BaseEstimator, RegressorMixin):
|
|
155
|
+
"""
|
|
156
|
+
General-Purpose Non-Linear Tree Regressor with Empirical Fisher Apoptosis.
|
|
157
|
+
Fully compliant with the Scikit-Learn Estimator API.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
def __init__(
|
|
161
|
+
self,
|
|
162
|
+
max_depth: int = 10,
|
|
163
|
+
cancer_sensitivity: float = 1.0,
|
|
164
|
+
oof_consistency: float = 0.5,
|
|
165
|
+
apoptosis_start_depth: int = 6,
|
|
166
|
+
split_quantiles: Optional[List[float]] = None,
|
|
167
|
+
min_samples_split: int = 4,
|
|
168
|
+
min_samples_leaf: int = 1,
|
|
169
|
+
hierarchical_shrinkage: float = 0.0,
|
|
170
|
+
soft_splits: bool = False,
|
|
171
|
+
transition_gamma: float = 3.0,
|
|
172
|
+
sign_constraints: Optional[Dict[Union[int, str], int]] = None,
|
|
173
|
+
):
|
|
174
|
+
self.max_depth = max_depth
|
|
175
|
+
self.cancer_sensitivity = cancer_sensitivity
|
|
176
|
+
self.oof_consistency = oof_consistency
|
|
177
|
+
self.apoptosis_start_depth = apoptosis_start_depth
|
|
178
|
+
self.split_quantiles = split_quantiles
|
|
179
|
+
self.min_samples_split = min_samples_split
|
|
180
|
+
self.min_samples_leaf = min_samples_leaf
|
|
181
|
+
self.hierarchical_shrinkage = hierarchical_shrinkage
|
|
182
|
+
self.soft_splits = soft_splits
|
|
183
|
+
self.transition_gamma = transition_gamma
|
|
184
|
+
self.sign_constraints = sign_constraints
|
|
185
|
+
|
|
186
|
+
self.tree_engine_: Optional[NirvanaTreeEngine] = None
|
|
187
|
+
self.feature_names_: List[str] = []
|
|
188
|
+
self.n_features_in_: int = 0
|
|
189
|
+
|
|
190
|
+
def fit(self, X: Union[np.ndarray, pd.DataFrame], y: Union[np.ndarray, pd.Series]):
|
|
191
|
+
"""
|
|
192
|
+
Fit decision tree with Fisher margin apoptosis on static tabular data.
|
|
193
|
+
"""
|
|
194
|
+
if hasattr(X, "columns"):
|
|
195
|
+
self.feature_names_ = [str(c) for c in X.columns]
|
|
196
|
+
elif hasattr(X, "shape") and len(X.shape) > 1:
|
|
197
|
+
self.feature_names_ = [f"X{i}" for i in range(X.shape[1])]
|
|
198
|
+
else:
|
|
199
|
+
self.feature_names_ = ["X0"]
|
|
200
|
+
|
|
201
|
+
X_mat = getattr(X, "values", X)
|
|
202
|
+
y_vec = getattr(y, "values", y)
|
|
203
|
+
X_mat = np.asarray(X_mat, dtype=float)
|
|
204
|
+
y_vec = np.asarray(y_vec, dtype=float).ravel()
|
|
205
|
+
|
|
206
|
+
self.n_features_in_ = X_mat.shape[1] if len(X_mat.shape) > 1 else 1
|
|
207
|
+
|
|
208
|
+
self.tree_engine_ = NirvanaTreeEngine(
|
|
209
|
+
cancer_sensitivity=self.cancer_sensitivity,
|
|
210
|
+
oof_consistency=self.oof_consistency,
|
|
211
|
+
max_depth=self.max_depth,
|
|
212
|
+
apoptosis_start_depth=self.apoptosis_start_depth,
|
|
213
|
+
split_quantiles=self.split_quantiles or [10.0, 20.0, 35.0, 50.0, 65.0, 80.0, 90.0],
|
|
214
|
+
min_samples_split=self.min_samples_split,
|
|
215
|
+
min_samples_leaf=self.min_samples_leaf,
|
|
216
|
+
hierarchical_shrinkage=self.hierarchical_shrinkage,
|
|
217
|
+
soft_splits=self.soft_splits,
|
|
218
|
+
transition_gamma=self.transition_gamma,
|
|
219
|
+
sign_constraints=self.sign_constraints,
|
|
220
|
+
)
|
|
221
|
+
self.tree_engine_.feature_names = self.feature_names_
|
|
222
|
+
self.tree_engine_.fit(X_mat, y_vec)
|
|
223
|
+
return self
|
|
224
|
+
|
|
225
|
+
def predict(self, X: Union[np.ndarray, pd.DataFrame]) -> np.ndarray:
|
|
226
|
+
if self.tree_engine_ is None:
|
|
227
|
+
raise ValueError("NirvanaTreeRegressor is not fitted yet.")
|
|
228
|
+
return self.tree_engine_.predict(X)
|
|
229
|
+
|
|
230
|
+
def score(self, X: Union[np.ndarray, pd.DataFrame], y: Union[np.ndarray, pd.Series]) -> float:
|
|
231
|
+
y_vec = np.asarray(getattr(y, "values", y), dtype=float).ravel()
|
|
232
|
+
y_pred = self.predict(X)
|
|
233
|
+
ss_res = np.sum((y_vec - y_pred) ** 2)
|
|
234
|
+
ss_tot = np.sum((y_vec - np.mean(y_vec)) ** 2)
|
|
235
|
+
if ss_tot < 1e-12:
|
|
236
|
+
return 1.0 if ss_res < 1e-12 else 0.0
|
|
237
|
+
return float(1.0 - ss_res / ss_tot)
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def feature_importances_(self) -> np.ndarray:
|
|
241
|
+
if self.tree_engine_ is None:
|
|
242
|
+
raise ValueError("Model not fitted yet.")
|
|
243
|
+
imp_dict = self.tree_engine_.get_feature_importances()
|
|
244
|
+
return np.array([imp_dict.get(feat, 0.0) for feat in self.feature_names_])
|
|
245
|
+
|
|
246
|
+
def get_params(self, deep: bool = True) -> Dict[str, Any]:
|
|
247
|
+
return {
|
|
248
|
+
"max_depth": self.max_depth,
|
|
249
|
+
"cancer_sensitivity": self.cancer_sensitivity,
|
|
250
|
+
"oof_consistency": self.oof_consistency,
|
|
251
|
+
"apoptosis_start_depth": self.apoptosis_start_depth,
|
|
252
|
+
"split_quantiles": self.split_quantiles,
|
|
253
|
+
"min_samples_split": self.min_samples_split,
|
|
254
|
+
"min_samples_leaf": self.min_samples_leaf,
|
|
255
|
+
"hierarchical_shrinkage": self.hierarchical_shrinkage,
|
|
256
|
+
"soft_splits": self.soft_splits,
|
|
257
|
+
"transition_gamma": self.transition_gamma,
|
|
258
|
+
"sign_constraints": self.sign_constraints,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
def set_params(self, **params):
|
|
262
|
+
for k, v in params.items():
|
|
263
|
+
setattr(self, k, v)
|
|
264
|
+
return self
|
|
265
|
+
|
|
266
|
+
def export_all_symbolic_rules(self) -> List[dict]:
|
|
267
|
+
"""Export every single active branch/leaf path as an exact symbolic formula."""
|
|
268
|
+
if self.tree_engine_ is None:
|
|
269
|
+
raise ValueError("Model is not fitted yet.")
|
|
270
|
+
return self.tree_engine_.export_all_symbolic_rules()
|
|
271
|
+
|
|
272
|
+
def export_pathology_alerts(self) -> List[Dict[str, Any]]:
|
|
273
|
+
"""Export all excised pathology anomalies as structured case study records."""
|
|
274
|
+
if self.tree_engine_ is None:
|
|
275
|
+
raise ValueError("Model is not fitted yet.")
|
|
276
|
+
return self.tree_engine_.export_pathology_alerts()
|
|
277
|
+
|
|
278
|
+
def evaluate_stress_proximity(self, features: Union[List[float], np.ndarray, Any]) -> Dict[str, Any]:
|
|
279
|
+
"""
|
|
280
|
+
Reverse Stress-Testing Radar:
|
|
281
|
+
Computes proximity [0.0, 100.0]% of a given observation to the nearest excised pathology case study.
|
|
282
|
+
"""
|
|
283
|
+
if self.tree_engine_ is None:
|
|
284
|
+
raise ValueError("Model is not fitted yet.")
|
|
285
|
+
if hasattr(features, "values"):
|
|
286
|
+
features = features.values
|
|
287
|
+
return self.tree_engine_.evaluate_stress_proximity(features)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class NirvanaTwoStageRegressor(BaseEstimator, RegressorMixin):
|
|
292
|
+
"""
|
|
293
|
+
Universal Two-Stage Residual Booster Meta-Estimator.
|
|
294
|
+
|
|
295
|
+
Wraps ANY base regression model (Phase 1: OLS, Ridge, Lasso, ElasticNet, SVR, etc.)
|
|
296
|
+
and elevates its R^2 by training Nirvana Phase 2 (Apoptosis Tree) on its residuals:
|
|
297
|
+
y_final = y_hat_base + learning_rate * e_hat_phase2
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
def __init__(
|
|
301
|
+
self,
|
|
302
|
+
base_estimator: Any = "ols",
|
|
303
|
+
cancer_sensitivity: float = 1.0,
|
|
304
|
+
oof_consistency: float = 0.5,
|
|
305
|
+
max_depth: int = 10,
|
|
306
|
+
apoptosis_start_depth: int = 6,
|
|
307
|
+
learning_rate: float = 1.0,
|
|
308
|
+
ridge_lambda: float = 1e-4,
|
|
309
|
+
split_quantiles: Optional[List[float]] = None,
|
|
310
|
+
min_samples_split: int = 4,
|
|
311
|
+
min_samples_leaf: int = 1,
|
|
312
|
+
hierarchical_shrinkage: float = 0.0,
|
|
313
|
+
soft_splits: bool = False,
|
|
314
|
+
transition_gamma: float = 3.0,
|
|
315
|
+
sign_constraints: Optional[Dict[Union[int, str], int]] = None,
|
|
316
|
+
factor_compression: bool = False,
|
|
317
|
+
n_components: Optional[int] = None,
|
|
318
|
+
manifold_lstr: bool = False,
|
|
319
|
+
):
|
|
320
|
+
self.base_estimator = base_estimator
|
|
321
|
+
self.cancer_sensitivity = cancer_sensitivity
|
|
322
|
+
self.oof_consistency = oof_consistency
|
|
323
|
+
self.max_depth = max_depth
|
|
324
|
+
self.apoptosis_start_depth = apoptosis_start_depth
|
|
325
|
+
self.learning_rate = learning_rate
|
|
326
|
+
self.ridge_lambda = ridge_lambda
|
|
327
|
+
self.split_quantiles = split_quantiles
|
|
328
|
+
self.min_samples_split = min_samples_split
|
|
329
|
+
self.min_samples_leaf = min_samples_leaf
|
|
330
|
+
self.hierarchical_shrinkage = hierarchical_shrinkage
|
|
331
|
+
self.soft_splits = soft_splits or manifold_lstr
|
|
332
|
+
self.transition_gamma = transition_gamma
|
|
333
|
+
self.sign_constraints = sign_constraints
|
|
334
|
+
self.factor_compression = factor_compression
|
|
335
|
+
self.n_components = n_components
|
|
336
|
+
self.manifold_lstr = manifold_lstr
|
|
337
|
+
|
|
338
|
+
self.fitted_base_estimator_: Any = None
|
|
339
|
+
self.tree_engine_: Optional[NirvanaTreeRegressor] = None
|
|
340
|
+
self.beta_: Optional[np.ndarray] = None
|
|
341
|
+
self.feature_names_: List[str] = []
|
|
342
|
+
self.pca_mean_: Optional[np.ndarray] = None
|
|
343
|
+
self.pca_components_: Optional[np.ndarray] = None
|
|
344
|
+
self.conformal_residuals_: Optional[np.ndarray] = None
|
|
345
|
+
self.report_: Optional[StaticEconometricReport] = None
|
|
346
|
+
|
|
347
|
+
def fit(self, X: Union[np.ndarray, pd.DataFrame], y: Union[np.ndarray, pd.Series]):
|
|
348
|
+
"""
|
|
349
|
+
Fit Phase 1 base model + Phase 2 Nirvana Apoptosis Tree on residuals.
|
|
350
|
+
"""
|
|
351
|
+
if hasattr(X, "columns"):
|
|
352
|
+
self.feature_names_ = [str(c) for c in X.columns]
|
|
353
|
+
elif hasattr(X, "shape") and len(X.shape) > 1:
|
|
354
|
+
self.feature_names_ = [f"X{i}" for i in range(X.shape[1])]
|
|
355
|
+
else:
|
|
356
|
+
self.feature_names_ = ["X0"]
|
|
357
|
+
|
|
358
|
+
X_mat = getattr(X, "values", X)
|
|
359
|
+
y_vec = getattr(y, "values", y)
|
|
360
|
+
X_mat = np.asarray(X_mat, dtype=float)
|
|
361
|
+
y_vec = np.asarray(y_vec, dtype=float).ravel()
|
|
362
|
+
N, K = X_mat.shape
|
|
363
|
+
|
|
364
|
+
# --- Optional Factor Compression (SVD Orthogonalization) to protect degrees of freedom ---
|
|
365
|
+
self.pca_mean_ = None
|
|
366
|
+
self.pca_components_ = None
|
|
367
|
+
X_reg = X_mat
|
|
368
|
+
if self.factor_compression and K > 1:
|
|
369
|
+
k_comp = self.n_components or max(1, min(K - 1, max(1, N // 3)))
|
|
370
|
+
self.pca_mean_ = np.mean(X_mat, axis=0)
|
|
371
|
+
X_center = X_mat - self.pca_mean_
|
|
372
|
+
_, _, Vt = np.linalg.svd(X_center, full_matrices=False)
|
|
373
|
+
self.pca_components_ = Vt[:k_comp]
|
|
374
|
+
X_reg = X_center @ self.pca_components_.T
|
|
375
|
+
|
|
376
|
+
# --- Phase 1: Fit Base Model ---
|
|
377
|
+
K_reg = X_reg.shape[1]
|
|
378
|
+
if self.base_estimator == "ols" or not SKLEARN_AVAILABLE:
|
|
379
|
+
X_design = np.column_stack([np.ones(N), X_reg])
|
|
380
|
+
reg_eye = self.ridge_lambda * np.eye(K_reg + 1)
|
|
381
|
+
reg_eye[0, 0] = 0.0
|
|
382
|
+
self.beta_ = np.linalg.solve(X_design.T @ X_design + reg_eye, X_design.T @ y_vec)
|
|
383
|
+
|
|
384
|
+
# Enforce Economic Sign Constraints via Gauss-Seidel Coordinate Descent
|
|
385
|
+
if self.sign_constraints and not self.factor_compression:
|
|
386
|
+
beta_c = self.beta_.copy()
|
|
387
|
+
XTX = X_design.T @ X_design + reg_eye
|
|
388
|
+
XTy = X_design.T @ y_vec
|
|
389
|
+
diag_XTX = np.diag(XTX)
|
|
390
|
+
for _ in range(50):
|
|
391
|
+
for j in range(1, K + 1):
|
|
392
|
+
col_name = self.feature_names_[j - 1]
|
|
393
|
+
s = self.sign_constraints.get(col_name, self.sign_constraints.get(j - 1, 0))
|
|
394
|
+
if s != 0:
|
|
395
|
+
r_j = XTy[j] - (XTX[j] @ beta_c - XTX[j, j] * beta_c[j])
|
|
396
|
+
new_b = r_j / max(diag_XTX[j], 1e-12)
|
|
397
|
+
if s > 0:
|
|
398
|
+
new_b = max(0.0, new_b)
|
|
399
|
+
elif s < 0:
|
|
400
|
+
new_b = min(0.0, new_b)
|
|
401
|
+
beta_c[j] = new_b
|
|
402
|
+
self.beta_ = beta_c
|
|
403
|
+
|
|
404
|
+
y_pred_phase1 = X_design @ self.beta_
|
|
405
|
+
self.fitted_base_estimator_ = "ols"
|
|
406
|
+
elif isinstance(self.base_estimator, str):
|
|
407
|
+
est_name = self.base_estimator.lower()
|
|
408
|
+
if est_name == "ridge":
|
|
409
|
+
base = Ridge(alpha=self.ridge_lambda * 100.0)
|
|
410
|
+
elif est_name == "lasso":
|
|
411
|
+
base = Lasso(alpha=0.01)
|
|
412
|
+
elif est_name == "elasticnet":
|
|
413
|
+
base = ElasticNet(alpha=0.01, l1_ratio=0.5)
|
|
414
|
+
elif est_name in ("kalman", "tvp"):
|
|
415
|
+
from .kalman import CausalKalmanTVPRegressor
|
|
416
|
+
base = CausalKalmanTVPRegressor()
|
|
417
|
+
else:
|
|
418
|
+
base = LinearRegression()
|
|
419
|
+
base.fit(X_reg, y_vec)
|
|
420
|
+
self.fitted_base_estimator_ = base
|
|
421
|
+
y_pred_phase1 = base.predict(X_reg)
|
|
422
|
+
else:
|
|
423
|
+
# Custom user-supplied scikit-learn regressor
|
|
424
|
+
base = self.base_estimator
|
|
425
|
+
base.fit(X_reg, y_vec)
|
|
426
|
+
self.fitted_base_estimator_ = base
|
|
427
|
+
y_pred_phase1 = base.predict(X_reg)
|
|
428
|
+
|
|
429
|
+
residuals_phase1 = y_vec - y_pred_phase1
|
|
430
|
+
ss_tot = float(np.sum((y_vec - np.mean(y_vec)) ** 2))
|
|
431
|
+
ss_res_1 = float(np.sum(residuals_phase1 ** 2))
|
|
432
|
+
r2_phase1 = max(0.0, float(1.0 - ss_res_1 / max(ss_tot, 1e-12)))
|
|
433
|
+
|
|
434
|
+
# --- Phase 2: Fit Nirvana Apoptosis Tree on Residuals ---
|
|
435
|
+
self.tree_engine_ = NirvanaTreeRegressor(
|
|
436
|
+
max_depth=self.max_depth,
|
|
437
|
+
cancer_sensitivity=self.cancer_sensitivity,
|
|
438
|
+
oof_consistency=self.oof_consistency,
|
|
439
|
+
apoptosis_start_depth=self.apoptosis_start_depth,
|
|
440
|
+
split_quantiles=self.split_quantiles,
|
|
441
|
+
min_samples_split=self.min_samples_split,
|
|
442
|
+
min_samples_leaf=self.min_samples_leaf,
|
|
443
|
+
hierarchical_shrinkage=self.hierarchical_shrinkage,
|
|
444
|
+
soft_splits=self.soft_splits,
|
|
445
|
+
transition_gamma=self.transition_gamma,
|
|
446
|
+
sign_constraints=self.sign_constraints,
|
|
447
|
+
)
|
|
448
|
+
self.tree_engine_.fit(X, residuals_phase1)
|
|
449
|
+
|
|
450
|
+
residuals_pred_phase2 = self.tree_engine_.predict(X_mat)
|
|
451
|
+
y_pred_final = y_pred_phase1 + self.learning_rate * residuals_pred_phase2
|
|
452
|
+
residuals_final = y_vec - y_pred_final
|
|
453
|
+
self.conformal_residuals_ = np.abs(residuals_final)
|
|
454
|
+
|
|
455
|
+
ss_res_final = float(np.sum(residuals_final ** 2))
|
|
456
|
+
r2_phase2 = max(0.0, float(1.0 - ss_res_final / max(ss_tot, 1e-12)))
|
|
457
|
+
r2_gain = max(0.0, r2_phase2 - r2_phase1)
|
|
458
|
+
|
|
459
|
+
unexplained_phase1 = max(1.0 - r2_phase1, 1e-6)
|
|
460
|
+
r2_relative_improvement = float((r2_gain / unexplained_phase1) * 100.0)
|
|
461
|
+
res_var_reduction = float(((ss_res_1 - ss_res_final) / max(ss_res_1, 1e-12)) * 100.0)
|
|
462
|
+
|
|
463
|
+
# Incremental F-statistic
|
|
464
|
+
df_1 = K + 1
|
|
465
|
+
active_leaves = max(
|
|
466
|
+
1, sum(1 for n in self.tree_engine_.tree_engine_.nodes if n.is_leaf and not n.is_pruned_apoptosis)
|
|
467
|
+
)
|
|
468
|
+
df_2 = active_leaves
|
|
469
|
+
df_denom = max(1, N - df_1 - df_2)
|
|
470
|
+
ms_num = max(0.0, ss_res_1 - ss_res_final) / df_2
|
|
471
|
+
ms_denom = max(ss_res_final, 1e-12) / df_denom
|
|
472
|
+
f_stat = float(ms_num / ms_denom)
|
|
473
|
+
f_pval = float(1.0 - stats.f.cdf(f_stat, df_2, df_denom))
|
|
474
|
+
|
|
475
|
+
coef_dict = {}
|
|
476
|
+
if self.beta_ is not None:
|
|
477
|
+
coef_dict["Intercept"] = float(self.beta_[0])
|
|
478
|
+
if self.pca_components_ is not None:
|
|
479
|
+
orig_beta = self.pca_components_.T @ self.beta_[1:]
|
|
480
|
+
for idx, col in enumerate(self.feature_names_):
|
|
481
|
+
coef_dict[col] = float(orig_beta[idx])
|
|
482
|
+
else:
|
|
483
|
+
for idx, col in enumerate(self.feature_names_):
|
|
484
|
+
coef_dict[col] = float(self.beta_[idx + 1])
|
|
485
|
+
elif hasattr(self.fitted_base_estimator_, "coef_"):
|
|
486
|
+
for idx, col in enumerate(self.feature_names_):
|
|
487
|
+
coef_dict[col] = float(self.fitted_base_estimator_.coef_[idx])
|
|
488
|
+
|
|
489
|
+
pruning_stats = self.tree_engine_.tree_engine_.get_pruning_stats()
|
|
490
|
+
feat_importances = self.tree_engine_.tree_engine_.get_feature_importances()
|
|
491
|
+
|
|
492
|
+
threshold_rules = []
|
|
493
|
+
for node in self.tree_engine_.tree_engine_.nodes[:15]:
|
|
494
|
+
if not node.is_leaf and not node.is_pruned_apoptosis and node.feature_idx >= 0:
|
|
495
|
+
feat = (
|
|
496
|
+
self.feature_names_[node.feature_idx]
|
|
497
|
+
if node.feature_idx < len(self.feature_names_)
|
|
498
|
+
else f"X{node.feature_idx}"
|
|
499
|
+
)
|
|
500
|
+
threshold_rules.append(
|
|
501
|
+
f"Layer {node.depth} Split: {feat} <= {node.threshold:.3f} "
|
|
502
|
+
f"(Samples: {node.sample_count}, Value: {node.value:.3f})"
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
self.report_ = StaticEconometricReport(
|
|
506
|
+
n_samples=N,
|
|
507
|
+
n_features=K,
|
|
508
|
+
r2_phase1=r2_phase1,
|
|
509
|
+
r2_phase2=r2_phase2,
|
|
510
|
+
r2_gain=r2_gain,
|
|
511
|
+
r2_relative_improvement_pct=r2_relative_improvement,
|
|
512
|
+
residual_variance_reduction_pct=res_var_reduction,
|
|
513
|
+
f_statistic=f_stat,
|
|
514
|
+
f_p_value=f_pval,
|
|
515
|
+
phase1_coefficients=coef_dict,
|
|
516
|
+
feature_importances=feat_importances,
|
|
517
|
+
pruning_stats=pruning_stats,
|
|
518
|
+
top_substitution_thresholds=threshold_rules,
|
|
519
|
+
pathology_alerts=self.tree_engine_.export_pathology_alerts(),
|
|
520
|
+
)
|
|
521
|
+
return self
|
|
522
|
+
|
|
523
|
+
def __getattr__(self, name: str) -> Any:
|
|
524
|
+
report = self.__dict__.get("report_", None)
|
|
525
|
+
if report is not None and hasattr(report, name):
|
|
526
|
+
return getattr(report, name)
|
|
527
|
+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
|
|
528
|
+
|
|
529
|
+
def predict(self, X: Union[np.ndarray, pd.DataFrame]) -> np.ndarray:
|
|
530
|
+
if self.fitted_base_estimator_ is None or self.tree_engine_ is None:
|
|
531
|
+
raise ValueError("NirvanaTwoStageRegressor is not fitted yet.")
|
|
532
|
+
|
|
533
|
+
X_mat = getattr(X, "values", X)
|
|
534
|
+
X_mat = np.asarray(X_mat, dtype=float)
|
|
535
|
+
|
|
536
|
+
if self.factor_compression and self.pca_components_ is not None:
|
|
537
|
+
X_center = X_mat - self.pca_mean_
|
|
538
|
+
X_reg = X_center @ self.pca_components_.T
|
|
539
|
+
else:
|
|
540
|
+
X_reg = X_mat
|
|
541
|
+
|
|
542
|
+
if self.fitted_base_estimator_ == "ols":
|
|
543
|
+
X_design = np.column_stack([np.ones(X_reg.shape[0]), X_reg])
|
|
544
|
+
y_pred_base = X_design @ self.beta_
|
|
545
|
+
else:
|
|
546
|
+
y_pred_base = self.fitted_base_estimator_.predict(X_reg)
|
|
547
|
+
|
|
548
|
+
residuals_pred_phase2 = self.tree_engine_.predict(X_mat)
|
|
549
|
+
return y_pred_base + self.learning_rate * residuals_pred_phase2
|
|
550
|
+
|
|
551
|
+
def predict_interval(
|
|
552
|
+
self,
|
|
553
|
+
X: Union[np.ndarray, pd.DataFrame],
|
|
554
|
+
alpha: float = 0.05,
|
|
555
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
556
|
+
"""
|
|
557
|
+
Split Conformal Prediction Interval with Distribution-Free Finite-Sample Guarantee.
|
|
558
|
+
==================================================================================
|
|
559
|
+
Guarantees: P(Y_true in [Y_lower, Y_upper]) >= 1 - alpha.
|
|
560
|
+
|
|
561
|
+
Parameters
|
|
562
|
+
----------
|
|
563
|
+
X : Union[np.ndarray, pd.DataFrame]
|
|
564
|
+
Input feature matrix.
|
|
565
|
+
alpha : float, default=0.05
|
|
566
|
+
Significance level (e.g. 0.05 for 95% coverage interval).
|
|
567
|
+
|
|
568
|
+
Returns
|
|
569
|
+
-------
|
|
570
|
+
Tuple[np.ndarray, np.ndarray, np.ndarray]
|
|
571
|
+
(y_pred, y_lower, y_upper)
|
|
572
|
+
"""
|
|
573
|
+
y_pred = self.predict(X)
|
|
574
|
+
if self.conformal_residuals_ is None or len(self.conformal_residuals_) == 0:
|
|
575
|
+
return y_pred, y_pred, y_pred
|
|
576
|
+
|
|
577
|
+
n_cal = len(self.conformal_residuals_)
|
|
578
|
+
# Finite-sample exact quantile index: ceil((n + 1) * (1 - alpha)) / n
|
|
579
|
+
q_level = min(1.0, np.ceil((n_cal + 1) * (1.0 - alpha)) / n_cal)
|
|
580
|
+
q = float(np.quantile(self.conformal_residuals_, q_level))
|
|
581
|
+
|
|
582
|
+
y_lower = y_pred - q
|
|
583
|
+
y_upper = y_pred + q
|
|
584
|
+
return y_pred, y_lower, y_upper
|
|
585
|
+
|
|
586
|
+
def evaluate_conformal_coverage(
|
|
587
|
+
self,
|
|
588
|
+
X_test: Union[np.ndarray, pd.DataFrame],
|
|
589
|
+
y_test: Union[np.ndarray, pd.Series],
|
|
590
|
+
alpha: float = 0.05,
|
|
591
|
+
) -> Dict[str, float]:
|
|
592
|
+
"""
|
|
593
|
+
Evaluate empirical coverage and average interval width on test set.
|
|
594
|
+
"""
|
|
595
|
+
y_vec = np.asarray(getattr(y_test, "values", y_test), dtype=float).ravel()
|
|
596
|
+
y_pred, y_lower, y_upper = self.predict_interval(X_test, alpha=alpha)
|
|
597
|
+
covered = (y_vec >= y_lower) & (y_vec <= y_upper)
|
|
598
|
+
coverage_rate = float(np.mean(covered))
|
|
599
|
+
avg_width = float(np.mean(y_upper - y_lower))
|
|
600
|
+
return {
|
|
601
|
+
"target_coverage": float(1.0 - alpha),
|
|
602
|
+
"empirical_coverage": coverage_rate,
|
|
603
|
+
"average_interval_width": avg_width,
|
|
604
|
+
"conformal_quantile_q": float(np.mean(y_upper - y_pred)),
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
def score(self, X: Union[np.ndarray, pd.DataFrame], y: Union[np.ndarray, pd.Series]) -> float:
|
|
608
|
+
y_vec = np.asarray(getattr(y, "values", y), dtype=float).ravel()
|
|
609
|
+
y_pred = self.predict(X)
|
|
610
|
+
ss_res = np.sum((y_vec - y_pred) ** 2)
|
|
611
|
+
ss_tot = np.sum((y_vec - np.mean(y_vec)) ** 2)
|
|
612
|
+
if ss_tot < 1e-12:
|
|
613
|
+
return 1.0 if ss_res < 1e-12 else 0.0
|
|
614
|
+
return float(1.0 - ss_res / ss_tot)
|
|
615
|
+
|
|
616
|
+
def get_params(self, deep: bool = True) -> Dict[str, Any]:
|
|
617
|
+
return {
|
|
618
|
+
"base_estimator": self.base_estimator,
|
|
619
|
+
"cancer_sensitivity": self.cancer_sensitivity,
|
|
620
|
+
"oof_consistency": self.oof_consistency,
|
|
621
|
+
"max_depth": self.max_depth,
|
|
622
|
+
"apoptosis_start_depth": self.apoptosis_start_depth,
|
|
623
|
+
"learning_rate": self.learning_rate,
|
|
624
|
+
"ridge_lambda": self.ridge_lambda,
|
|
625
|
+
"split_quantiles": self.split_quantiles,
|
|
626
|
+
"min_samples_split": self.min_samples_split,
|
|
627
|
+
"min_samples_leaf": self.min_samples_leaf,
|
|
628
|
+
"hierarchical_shrinkage": self.hierarchical_shrinkage,
|
|
629
|
+
"soft_splits": self.soft_splits,
|
|
630
|
+
"transition_gamma": self.transition_gamma,
|
|
631
|
+
"sign_constraints": self.sign_constraints,
|
|
632
|
+
"factor_compression": self.factor_compression,
|
|
633
|
+
"n_components": self.n_components,
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
def set_params(self, **params):
|
|
637
|
+
for k, v in params.items():
|
|
638
|
+
setattr(self, k, v)
|
|
639
|
+
return self
|
|
640
|
+
|
|
641
|
+
def evaluate_cv(
|
|
642
|
+
self,
|
|
643
|
+
X: Union[np.ndarray, pd.DataFrame],
|
|
644
|
+
y: Union[np.ndarray, pd.Series],
|
|
645
|
+
k_folds: int = 5,
|
|
646
|
+
random_seed: int = 42,
|
|
647
|
+
) -> Dict[str, float]:
|
|
648
|
+
"""
|
|
649
|
+
Run K-fold Out-Of-Sample Cross Validation.
|
|
650
|
+
"""
|
|
651
|
+
X_mat = getattr(X, "values", X)
|
|
652
|
+
y_vec = getattr(y, "values", y)
|
|
653
|
+
X_mat = np.asarray(X_mat, dtype=float)
|
|
654
|
+
y_vec = np.asarray(y_vec, dtype=float).ravel()
|
|
655
|
+
|
|
656
|
+
N = len(y_vec)
|
|
657
|
+
rng = np.random.default_rng(random_seed)
|
|
658
|
+
indices = np.arange(N)
|
|
659
|
+
rng.shuffle(indices)
|
|
660
|
+
|
|
661
|
+
fold_size = N // k_folds
|
|
662
|
+
r2_phase1_list = []
|
|
663
|
+
r2_phase2_list = []
|
|
664
|
+
|
|
665
|
+
for f in range(k_folds):
|
|
666
|
+
val_idx = indices[f * fold_size : (f + 1) * fold_size]
|
|
667
|
+
train_idx = np.setdiff1d(indices, val_idx)
|
|
668
|
+
|
|
669
|
+
X_tr, y_tr = X_mat[train_idx], y_vec[train_idx]
|
|
670
|
+
X_val, y_val = X_mat[val_idx], y_vec[val_idx]
|
|
671
|
+
|
|
672
|
+
reg = NirvanaTwoStageRegressor(
|
|
673
|
+
base_estimator=self.base_estimator,
|
|
674
|
+
cancer_sensitivity=self.cancer_sensitivity,
|
|
675
|
+
oof_consistency=self.oof_consistency,
|
|
676
|
+
max_depth=self.max_depth,
|
|
677
|
+
apoptosis_start_depth=self.apoptosis_start_depth,
|
|
678
|
+
learning_rate=self.learning_rate,
|
|
679
|
+
ridge_lambda=self.ridge_lambda,
|
|
680
|
+
split_quantiles=self.split_quantiles,
|
|
681
|
+
min_samples_split=self.min_samples_split,
|
|
682
|
+
)
|
|
683
|
+
reg.fit(X_tr, y_tr)
|
|
684
|
+
|
|
685
|
+
# Base model score on val
|
|
686
|
+
if reg.fitted_base_estimator_ == "ols":
|
|
687
|
+
X_val_des = np.column_stack([np.ones(len(val_idx)), X_val])
|
|
688
|
+
y_val_pred_1 = X_val_des @ reg.beta_
|
|
689
|
+
else:
|
|
690
|
+
y_val_pred_1 = reg.fitted_base_estimator_.predict(X_val)
|
|
691
|
+
|
|
692
|
+
ss_tot_val = float(np.sum((y_val - np.mean(y_val)) ** 2))
|
|
693
|
+
ss_res_1_val = float(np.sum((y_val - y_val_pred_1) ** 2))
|
|
694
|
+
r2_1_val = max(0.0, 1.0 - ss_res_1_val / max(ss_tot_val, 1e-12))
|
|
695
|
+
r2_phase1_list.append(r2_1_val)
|
|
696
|
+
|
|
697
|
+
r2_2_val = reg.score(X_val, y_val)
|
|
698
|
+
r2_phase2_list.append(r2_2_val)
|
|
699
|
+
|
|
700
|
+
return {
|
|
701
|
+
"r2_phase1_cv_mean": float(np.mean(r2_phase1_list)),
|
|
702
|
+
"r2_phase1_cv_std": float(np.std(r2_phase1_list)),
|
|
703
|
+
"r2_phase2_cv_mean": float(np.mean(r2_phase2_list)),
|
|
704
|
+
"r2_phase2_cv_std": float(np.std(r2_phase2_list)),
|
|
705
|
+
"delta_r2_cv_mean": float(np.mean(r2_phase2_list) - np.mean(r2_phase1_list)),
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
def prove_phase2_multiverse(
|
|
709
|
+
self,
|
|
710
|
+
X: Union[np.ndarray, pd.DataFrame],
|
|
711
|
+
y: Union[np.ndarray, pd.Series],
|
|
712
|
+
n_universes: int = 100,
|
|
713
|
+
seed: int = 42,
|
|
714
|
+
):
|
|
715
|
+
"""
|
|
716
|
+
Rigorously prove the statistical validity, Ramsey RESET cure,
|
|
717
|
+
and multiverse invariance of the discovered variable.
|
|
718
|
+
"""
|
|
719
|
+
from .proof import prove_phase2_variable
|
|
720
|
+
return prove_phase2_variable(self, X, y, n_universes=n_universes, seed=seed)
|
|
721
|
+
|
|
722
|
+
def rank_discovered_variables(
|
|
723
|
+
self,
|
|
724
|
+
X: Union[np.ndarray, pd.DataFrame],
|
|
725
|
+
y: Union[np.ndarray, pd.Series],
|
|
726
|
+
top_k: int = 5,
|
|
727
|
+
n_universes: int = 100,
|
|
728
|
+
seed: int = 42,
|
|
729
|
+
):
|
|
730
|
+
"""
|
|
731
|
+
Rank top-K discovered variables from Phase 2, prove their econometric
|
|
732
|
+
significance and multiverse invariance, and map to real-world collection protocols.
|
|
733
|
+
"""
|
|
734
|
+
from .proof import rank_phase2_discovered_variables
|
|
735
|
+
return rank_phase2_discovered_variables(
|
|
736
|
+
self, X, y, top_k=top_k, n_universes=n_universes, seed=seed
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
def export_all_symbolic_rules(self) -> List[dict]:
|
|
740
|
+
"""Export every single active branch/leaf path in Phase 2 as an exact symbolic formula."""
|
|
741
|
+
if self.tree_engine_ is None:
|
|
742
|
+
raise ValueError("Model is not fitted yet.")
|
|
743
|
+
return self.tree_engine_.export_all_symbolic_rules()
|
|
744
|
+
|
|
745
|
+
def export_pathology_alerts(self) -> List[Dict[str, Any]]:
|
|
746
|
+
"""Export all excised pathology anomalies as structured case study records."""
|
|
747
|
+
if self.tree_engine_ is None:
|
|
748
|
+
raise ValueError("Model is not fitted yet.")
|
|
749
|
+
return self.tree_engine_.export_pathology_alerts()
|
|
750
|
+
|
|
751
|
+
def evaluate_stress_proximity(self, features: Union[List[float], np.ndarray, Any]) -> Dict[str, Any]:
|
|
752
|
+
"""
|
|
753
|
+
Reverse Stress-Testing Radar:
|
|
754
|
+
Computes proximity [0.0, 100.0]% of a given observation to the nearest excised pathology case study.
|
|
755
|
+
"""
|
|
756
|
+
if self.tree_engine_ is None:
|
|
757
|
+
raise ValueError("Model is not fitted yet.")
|
|
758
|
+
return self.tree_engine_.evaluate_stress_proximity(features)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
class NirvanaStaticBooster(NirvanaTwoStageRegressor):
|
|
763
|
+
"""
|
|
764
|
+
Two-Stage Econometric Booster for Static Cross-Sectional Data.
|
|
765
|
+
Returns StaticEconometricReport on .fit(X, y) for econometric workflows.
|
|
766
|
+
"""
|
|
767
|
+
|
|
768
|
+
def fit(
|
|
769
|
+
self,
|
|
770
|
+
X: Union[np.ndarray, pd.DataFrame],
|
|
771
|
+
y: Union[np.ndarray, pd.Series],
|
|
772
|
+
) -> StaticEconometricReport:
|
|
773
|
+
super().fit(X, y)
|
|
774
|
+
return self.report_
|