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/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_