ml4t-diagnostic 0.1.0a1__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.
Files changed (242) hide show
  1. ml4t/diagnostic/AGENT.md +25 -0
  2. ml4t/diagnostic/__init__.py +166 -0
  3. ml4t/diagnostic/backends/__init__.py +10 -0
  4. ml4t/diagnostic/backends/adapter.py +192 -0
  5. ml4t/diagnostic/backends/polars_backend.py +899 -0
  6. ml4t/diagnostic/caching/__init__.py +40 -0
  7. ml4t/diagnostic/caching/cache.py +331 -0
  8. ml4t/diagnostic/caching/decorators.py +131 -0
  9. ml4t/diagnostic/caching/smart_cache.py +339 -0
  10. ml4t/diagnostic/config/AGENT.md +24 -0
  11. ml4t/diagnostic/config/README.md +267 -0
  12. ml4t/diagnostic/config/__init__.py +219 -0
  13. ml4t/diagnostic/config/barrier_config.py +277 -0
  14. ml4t/diagnostic/config/base.py +301 -0
  15. ml4t/diagnostic/config/event_config.py +148 -0
  16. ml4t/diagnostic/config/feature_config.py +404 -0
  17. ml4t/diagnostic/config/multi_signal_config.py +55 -0
  18. ml4t/diagnostic/config/portfolio_config.py +215 -0
  19. ml4t/diagnostic/config/report_config.py +391 -0
  20. ml4t/diagnostic/config/sharpe_config.py +202 -0
  21. ml4t/diagnostic/config/signal_config.py +206 -0
  22. ml4t/diagnostic/config/trade_analysis_config.py +310 -0
  23. ml4t/diagnostic/config/validation.py +279 -0
  24. ml4t/diagnostic/core/__init__.py +29 -0
  25. ml4t/diagnostic/core/numba_utils.py +315 -0
  26. ml4t/diagnostic/core/purging.py +372 -0
  27. ml4t/diagnostic/core/sampling.py +471 -0
  28. ml4t/diagnostic/errors/__init__.py +205 -0
  29. ml4t/diagnostic/evaluation/AGENT.md +26 -0
  30. ml4t/diagnostic/evaluation/__init__.py +437 -0
  31. ml4t/diagnostic/evaluation/autocorrelation.py +531 -0
  32. ml4t/diagnostic/evaluation/barrier_analysis.py +1050 -0
  33. ml4t/diagnostic/evaluation/binary_metrics.py +910 -0
  34. ml4t/diagnostic/evaluation/dashboard.py +715 -0
  35. ml4t/diagnostic/evaluation/diagnostic_plots.py +1037 -0
  36. ml4t/diagnostic/evaluation/distribution/__init__.py +499 -0
  37. ml4t/diagnostic/evaluation/distribution/moments.py +299 -0
  38. ml4t/diagnostic/evaluation/distribution/tails.py +777 -0
  39. ml4t/diagnostic/evaluation/distribution/tests.py +470 -0
  40. ml4t/diagnostic/evaluation/drift/__init__.py +139 -0
  41. ml4t/diagnostic/evaluation/drift/analysis.py +432 -0
  42. ml4t/diagnostic/evaluation/drift/domain_classifier.py +517 -0
  43. ml4t/diagnostic/evaluation/drift/population_stability_index.py +310 -0
  44. ml4t/diagnostic/evaluation/drift/wasserstein.py +388 -0
  45. ml4t/diagnostic/evaluation/event_analysis.py +647 -0
  46. ml4t/diagnostic/evaluation/excursion.py +390 -0
  47. ml4t/diagnostic/evaluation/feature_diagnostics.py +873 -0
  48. ml4t/diagnostic/evaluation/feature_outcome.py +666 -0
  49. ml4t/diagnostic/evaluation/framework.py +935 -0
  50. ml4t/diagnostic/evaluation/metric_registry.py +255 -0
  51. ml4t/diagnostic/evaluation/metrics/AGENT.md +23 -0
  52. ml4t/diagnostic/evaluation/metrics/__init__.py +133 -0
  53. ml4t/diagnostic/evaluation/metrics/basic.py +160 -0
  54. ml4t/diagnostic/evaluation/metrics/conditional_ic.py +469 -0
  55. ml4t/diagnostic/evaluation/metrics/feature_outcome.py +475 -0
  56. ml4t/diagnostic/evaluation/metrics/ic_statistics.py +446 -0
  57. ml4t/diagnostic/evaluation/metrics/importance_analysis.py +338 -0
  58. ml4t/diagnostic/evaluation/metrics/importance_classical.py +375 -0
  59. ml4t/diagnostic/evaluation/metrics/importance_mda.py +371 -0
  60. ml4t/diagnostic/evaluation/metrics/importance_shap.py +715 -0
  61. ml4t/diagnostic/evaluation/metrics/information_coefficient.py +527 -0
  62. ml4t/diagnostic/evaluation/metrics/interactions.py +772 -0
  63. ml4t/diagnostic/evaluation/metrics/monotonicity.py +226 -0
  64. ml4t/diagnostic/evaluation/metrics/risk_adjusted.py +324 -0
  65. ml4t/diagnostic/evaluation/multi_signal.py +550 -0
  66. ml4t/diagnostic/evaluation/portfolio_analysis/__init__.py +83 -0
  67. ml4t/diagnostic/evaluation/portfolio_analysis/analysis.py +734 -0
  68. ml4t/diagnostic/evaluation/portfolio_analysis/metrics.py +589 -0
  69. ml4t/diagnostic/evaluation/portfolio_analysis/results.py +334 -0
  70. ml4t/diagnostic/evaluation/report_generation.py +824 -0
  71. ml4t/diagnostic/evaluation/signal_selector.py +452 -0
  72. ml4t/diagnostic/evaluation/stat_registry.py +139 -0
  73. ml4t/diagnostic/evaluation/stationarity/__init__.py +97 -0
  74. ml4t/diagnostic/evaluation/stationarity/analysis.py +518 -0
  75. ml4t/diagnostic/evaluation/stationarity/augmented_dickey_fuller.py +296 -0
  76. ml4t/diagnostic/evaluation/stationarity/kpss_test.py +308 -0
  77. ml4t/diagnostic/evaluation/stationarity/phillips_perron.py +365 -0
  78. ml4t/diagnostic/evaluation/stats/AGENT.md +43 -0
  79. ml4t/diagnostic/evaluation/stats/__init__.py +191 -0
  80. ml4t/diagnostic/evaluation/stats/backtest_overfitting.py +219 -0
  81. ml4t/diagnostic/evaluation/stats/bootstrap.py +228 -0
  82. ml4t/diagnostic/evaluation/stats/deflated_sharpe_ratio.py +591 -0
  83. ml4t/diagnostic/evaluation/stats/false_discovery_rate.py +295 -0
  84. ml4t/diagnostic/evaluation/stats/hac_standard_errors.py +108 -0
  85. ml4t/diagnostic/evaluation/stats/minimum_track_record.py +408 -0
  86. ml4t/diagnostic/evaluation/stats/moments.py +164 -0
  87. ml4t/diagnostic/evaluation/stats/rademacher_adjustment.py +436 -0
  88. ml4t/diagnostic/evaluation/stats/reality_check.py +155 -0
  89. ml4t/diagnostic/evaluation/stats/sharpe_inference.py +219 -0
  90. ml4t/diagnostic/evaluation/themes.py +330 -0
  91. ml4t/diagnostic/evaluation/threshold_analysis.py +957 -0
  92. ml4t/diagnostic/evaluation/trade_analysis.py +1136 -0
  93. ml4t/diagnostic/evaluation/trade_dashboard/__init__.py +32 -0
  94. ml4t/diagnostic/evaluation/trade_dashboard/app.py +315 -0
  95. ml4t/diagnostic/evaluation/trade_dashboard/export/__init__.py +18 -0
  96. ml4t/diagnostic/evaluation/trade_dashboard/export/csv.py +82 -0
  97. ml4t/diagnostic/evaluation/trade_dashboard/export/html.py +276 -0
  98. ml4t/diagnostic/evaluation/trade_dashboard/io.py +166 -0
  99. ml4t/diagnostic/evaluation/trade_dashboard/normalize.py +304 -0
  100. ml4t/diagnostic/evaluation/trade_dashboard/stats.py +386 -0
  101. ml4t/diagnostic/evaluation/trade_dashboard/style.py +79 -0
  102. ml4t/diagnostic/evaluation/trade_dashboard/tabs/__init__.py +21 -0
  103. ml4t/diagnostic/evaluation/trade_dashboard/tabs/patterns.py +354 -0
  104. ml4t/diagnostic/evaluation/trade_dashboard/tabs/shap_analysis.py +280 -0
  105. ml4t/diagnostic/evaluation/trade_dashboard/tabs/stat_validation.py +186 -0
  106. ml4t/diagnostic/evaluation/trade_dashboard/tabs/worst_trades.py +236 -0
  107. ml4t/diagnostic/evaluation/trade_dashboard/types.py +129 -0
  108. ml4t/diagnostic/evaluation/trade_shap/__init__.py +102 -0
  109. ml4t/diagnostic/evaluation/trade_shap/alignment.py +188 -0
  110. ml4t/diagnostic/evaluation/trade_shap/characterize.py +413 -0
  111. ml4t/diagnostic/evaluation/trade_shap/cluster.py +302 -0
  112. ml4t/diagnostic/evaluation/trade_shap/explain.py +208 -0
  113. ml4t/diagnostic/evaluation/trade_shap/hypotheses/__init__.py +23 -0
  114. ml4t/diagnostic/evaluation/trade_shap/hypotheses/generator.py +290 -0
  115. ml4t/diagnostic/evaluation/trade_shap/hypotheses/matcher.py +251 -0
  116. ml4t/diagnostic/evaluation/trade_shap/hypotheses/templates.yaml +467 -0
  117. ml4t/diagnostic/evaluation/trade_shap/models.py +386 -0
  118. ml4t/diagnostic/evaluation/trade_shap/normalize.py +116 -0
  119. ml4t/diagnostic/evaluation/trade_shap/pipeline.py +263 -0
  120. ml4t/diagnostic/evaluation/trade_shap_dashboard.py +283 -0
  121. ml4t/diagnostic/evaluation/trade_shap_diagnostics.py +588 -0
  122. ml4t/diagnostic/evaluation/validated_cv.py +535 -0
  123. ml4t/diagnostic/evaluation/visualization.py +1050 -0
  124. ml4t/diagnostic/evaluation/volatility/__init__.py +45 -0
  125. ml4t/diagnostic/evaluation/volatility/analysis.py +351 -0
  126. ml4t/diagnostic/evaluation/volatility/arch.py +258 -0
  127. ml4t/diagnostic/evaluation/volatility/garch.py +460 -0
  128. ml4t/diagnostic/integration/__init__.py +48 -0
  129. ml4t/diagnostic/integration/backtest_contract.py +671 -0
  130. ml4t/diagnostic/integration/data_contract.py +316 -0
  131. ml4t/diagnostic/integration/engineer_contract.py +226 -0
  132. ml4t/diagnostic/logging/__init__.py +77 -0
  133. ml4t/diagnostic/logging/logger.py +245 -0
  134. ml4t/diagnostic/logging/performance.py +234 -0
  135. ml4t/diagnostic/logging/progress.py +234 -0
  136. ml4t/diagnostic/logging/wandb.py +412 -0
  137. ml4t/diagnostic/metrics/__init__.py +9 -0
  138. ml4t/diagnostic/metrics/percentiles.py +128 -0
  139. ml4t/diagnostic/py.typed +1 -0
  140. ml4t/diagnostic/reporting/__init__.py +43 -0
  141. ml4t/diagnostic/reporting/base.py +130 -0
  142. ml4t/diagnostic/reporting/html_renderer.py +275 -0
  143. ml4t/diagnostic/reporting/json_renderer.py +51 -0
  144. ml4t/diagnostic/reporting/markdown_renderer.py +117 -0
  145. ml4t/diagnostic/results/AGENT.md +24 -0
  146. ml4t/diagnostic/results/__init__.py +105 -0
  147. ml4t/diagnostic/results/barrier_results/__init__.py +36 -0
  148. ml4t/diagnostic/results/barrier_results/hit_rate.py +304 -0
  149. ml4t/diagnostic/results/barrier_results/precision_recall.py +266 -0
  150. ml4t/diagnostic/results/barrier_results/profit_factor.py +297 -0
  151. ml4t/diagnostic/results/barrier_results/tearsheet.py +397 -0
  152. ml4t/diagnostic/results/barrier_results/time_to_target.py +305 -0
  153. ml4t/diagnostic/results/barrier_results/validation.py +38 -0
  154. ml4t/diagnostic/results/base.py +177 -0
  155. ml4t/diagnostic/results/event_results.py +349 -0
  156. ml4t/diagnostic/results/feature_results.py +787 -0
  157. ml4t/diagnostic/results/multi_signal_results.py +431 -0
  158. ml4t/diagnostic/results/portfolio_results.py +281 -0
  159. ml4t/diagnostic/results/sharpe_results.py +448 -0
  160. ml4t/diagnostic/results/signal_results/__init__.py +74 -0
  161. ml4t/diagnostic/results/signal_results/ic.py +581 -0
  162. ml4t/diagnostic/results/signal_results/irtc.py +110 -0
  163. ml4t/diagnostic/results/signal_results/quantile.py +392 -0
  164. ml4t/diagnostic/results/signal_results/tearsheet.py +456 -0
  165. ml4t/diagnostic/results/signal_results/turnover.py +213 -0
  166. ml4t/diagnostic/results/signal_results/validation.py +147 -0
  167. ml4t/diagnostic/signal/AGENT.md +17 -0
  168. ml4t/diagnostic/signal/__init__.py +69 -0
  169. ml4t/diagnostic/signal/_report.py +152 -0
  170. ml4t/diagnostic/signal/_utils.py +261 -0
  171. ml4t/diagnostic/signal/core.py +275 -0
  172. ml4t/diagnostic/signal/quantile.py +148 -0
  173. ml4t/diagnostic/signal/result.py +214 -0
  174. ml4t/diagnostic/signal/signal_ic.py +129 -0
  175. ml4t/diagnostic/signal/turnover.py +182 -0
  176. ml4t/diagnostic/splitters/AGENT.md +19 -0
  177. ml4t/diagnostic/splitters/__init__.py +36 -0
  178. ml4t/diagnostic/splitters/base.py +501 -0
  179. ml4t/diagnostic/splitters/calendar.py +421 -0
  180. ml4t/diagnostic/splitters/calendar_config.py +91 -0
  181. ml4t/diagnostic/splitters/combinatorial.py +1064 -0
  182. ml4t/diagnostic/splitters/config.py +322 -0
  183. ml4t/diagnostic/splitters/cpcv/__init__.py +57 -0
  184. ml4t/diagnostic/splitters/cpcv/combinations.py +119 -0
  185. ml4t/diagnostic/splitters/cpcv/partitioning.py +263 -0
  186. ml4t/diagnostic/splitters/cpcv/purge_engine.py +379 -0
  187. ml4t/diagnostic/splitters/cpcv/windows.py +190 -0
  188. ml4t/diagnostic/splitters/group_isolation.py +329 -0
  189. ml4t/diagnostic/splitters/persistence.py +316 -0
  190. ml4t/diagnostic/splitters/utils.py +207 -0
  191. ml4t/diagnostic/splitters/walk_forward.py +757 -0
  192. ml4t/diagnostic/utils/__init__.py +42 -0
  193. ml4t/diagnostic/utils/config.py +542 -0
  194. ml4t/diagnostic/utils/dependencies.py +318 -0
  195. ml4t/diagnostic/utils/sessions.py +127 -0
  196. ml4t/diagnostic/validation/__init__.py +54 -0
  197. ml4t/diagnostic/validation/dataframe.py +274 -0
  198. ml4t/diagnostic/validation/returns.py +280 -0
  199. ml4t/diagnostic/validation/timeseries.py +299 -0
  200. ml4t/diagnostic/visualization/AGENT.md +19 -0
  201. ml4t/diagnostic/visualization/__init__.py +223 -0
  202. ml4t/diagnostic/visualization/backtest/__init__.py +98 -0
  203. ml4t/diagnostic/visualization/backtest/cost_attribution.py +762 -0
  204. ml4t/diagnostic/visualization/backtest/executive_summary.py +895 -0
  205. ml4t/diagnostic/visualization/backtest/interactive_controls.py +673 -0
  206. ml4t/diagnostic/visualization/backtest/statistical_validity.py +874 -0
  207. ml4t/diagnostic/visualization/backtest/tearsheet.py +565 -0
  208. ml4t/diagnostic/visualization/backtest/template_system.py +373 -0
  209. ml4t/diagnostic/visualization/backtest/trade_plots.py +1172 -0
  210. ml4t/diagnostic/visualization/barrier_plots.py +782 -0
  211. ml4t/diagnostic/visualization/core.py +1060 -0
  212. ml4t/diagnostic/visualization/dashboards/__init__.py +36 -0
  213. ml4t/diagnostic/visualization/dashboards/base.py +582 -0
  214. ml4t/diagnostic/visualization/dashboards/importance.py +801 -0
  215. ml4t/diagnostic/visualization/dashboards/interaction.py +263 -0
  216. ml4t/diagnostic/visualization/dashboards.py +43 -0
  217. ml4t/diagnostic/visualization/data_extraction/__init__.py +48 -0
  218. ml4t/diagnostic/visualization/data_extraction/importance.py +649 -0
  219. ml4t/diagnostic/visualization/data_extraction/interaction.py +504 -0
  220. ml4t/diagnostic/visualization/data_extraction/types.py +113 -0
  221. ml4t/diagnostic/visualization/data_extraction/validation.py +66 -0
  222. ml4t/diagnostic/visualization/feature_plots.py +888 -0
  223. ml4t/diagnostic/visualization/interaction_plots.py +618 -0
  224. ml4t/diagnostic/visualization/portfolio/__init__.py +41 -0
  225. ml4t/diagnostic/visualization/portfolio/dashboard.py +514 -0
  226. ml4t/diagnostic/visualization/portfolio/drawdown_plots.py +341 -0
  227. ml4t/diagnostic/visualization/portfolio/returns_plots.py +487 -0
  228. ml4t/diagnostic/visualization/portfolio/risk_plots.py +301 -0
  229. ml4t/diagnostic/visualization/report_generation.py +1343 -0
  230. ml4t/diagnostic/visualization/signal/__init__.py +103 -0
  231. ml4t/diagnostic/visualization/signal/dashboard.py +911 -0
  232. ml4t/diagnostic/visualization/signal/event_plots.py +514 -0
  233. ml4t/diagnostic/visualization/signal/ic_plots.py +635 -0
  234. ml4t/diagnostic/visualization/signal/multi_signal_dashboard.py +974 -0
  235. ml4t/diagnostic/visualization/signal/multi_signal_plots.py +603 -0
  236. ml4t/diagnostic/visualization/signal/quantile_plots.py +625 -0
  237. ml4t/diagnostic/visualization/signal/turnover_plots.py +400 -0
  238. ml4t/diagnostic/visualization/trade_shap/__init__.py +90 -0
  239. ml4t_diagnostic-0.1.0a1.dist-info/METADATA +1044 -0
  240. ml4t_diagnostic-0.1.0a1.dist-info/RECORD +242 -0
  241. ml4t_diagnostic-0.1.0a1.dist-info/WHEEL +4 -0
  242. ml4t_diagnostic-0.1.0a1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,787 @@
1
+ """Result schemas for feature evaluation modules (A, B, C).
2
+
3
+ Module A: Feature Diagnostics (stationarity, ACF, volatility clustering)
4
+ Module B: Cross-Feature Analysis (correlations, PCA, clustering)
5
+ Module C: Feature-Outcome Relationships (IC analysis, threshold analysis)
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ import polars as pl
13
+ from pydantic import Field
14
+
15
+ from ml4t.diagnostic.results.base import BaseResult
16
+
17
+ if TYPE_CHECKING:
18
+ from ml4t.diagnostic.integration.engineer_contract import EngineerConfig
19
+
20
+
21
+ # =============================================================================
22
+ # Module A: Feature Diagnostics
23
+ # =============================================================================
24
+
25
+
26
+ class StationarityTestResult(BaseResult):
27
+ """Results from stationarity tests (ADF, KPSS, PP).
28
+
29
+ Tests whether a time series is stationary (mean-reverting) or has unit root.
30
+
31
+ Attributes:
32
+ feature_name: Name of feature tested
33
+ adf_statistic: Augmented Dickey-Fuller test statistic
34
+ adf_pvalue: ADF p-value (reject H0 if < alpha => stationary)
35
+ adf_is_stationary: Whether ADF indicates stationarity
36
+ adf_critical_values: ADF critical values at 1%, 5%, 10% levels
37
+ adf_lags_used: Number of lags used in ADF test
38
+ adf_n_obs: Number of observations used in ADF test
39
+ kpss_statistic: KPSS test statistic
40
+ kpss_pvalue: KPSS p-value (reject H0 if < alpha => non-stationary)
41
+ kpss_is_stationary: Whether KPSS indicates stationarity
42
+ pp_statistic: Phillips-Perron test statistic
43
+ pp_pvalue: PP p-value
44
+ pp_is_stationary: Whether PP indicates stationarity
45
+ """
46
+
47
+ analysis_type: str = "stationarity_test"
48
+ feature_name: str = Field(..., description="Feature name")
49
+
50
+ # ADF test
51
+ adf_statistic: float | None = Field(None, description="ADF test statistic")
52
+ adf_pvalue: float | None = Field(None, description="ADF p-value")
53
+ adf_is_stationary: bool | None = Field(None, description="ADF stationarity")
54
+ adf_critical_values: dict[str, float] | None = Field(
55
+ None, description="ADF critical values (1%, 5%, 10%)"
56
+ )
57
+ adf_lags_used: int | None = Field(None, description="Lags used in ADF test")
58
+ adf_n_obs: int | None = Field(None, description="Observations in ADF test")
59
+
60
+ # KPSS test
61
+ kpss_statistic: float | None = Field(None, description="KPSS test statistic")
62
+ kpss_pvalue: float | None = Field(None, description="KPSS p-value")
63
+ kpss_is_stationary: bool | None = Field(None, description="KPSS stationarity")
64
+
65
+ # Phillips-Perron test
66
+ pp_statistic: float | None = Field(None, description="PP test statistic")
67
+ pp_pvalue: float | None = Field(None, description="PP p-value")
68
+ pp_is_stationary: bool | None = Field(None, description="PP stationarity")
69
+
70
+ def list_available_dataframes(self) -> list[str]:
71
+ """List available DataFrame views.
72
+
73
+ Returns:
74
+ List with single 'primary' view containing all test results
75
+ """
76
+ return ["primary"]
77
+
78
+ def get_dataframe(self, name: str | None = None) -> pl.DataFrame:
79
+ """Get test results as DataFrame.
80
+
81
+ Args:
82
+ name: DataFrame name (ignored, only 'primary' available)
83
+
84
+ Returns:
85
+ DataFrame with test statistics and conclusions
86
+
87
+ Raises:
88
+ ValueError: If name is provided but not 'primary'
89
+ """
90
+ if name is not None and name != "primary":
91
+ raise ValueError(
92
+ f"Unknown DataFrame name: {name}. Available: {self.list_available_dataframes()}"
93
+ )
94
+
95
+ data = {
96
+ "feature": [self.feature_name],
97
+ "adf_statistic": [self.adf_statistic],
98
+ "adf_pvalue": [self.adf_pvalue],
99
+ "adf_stationary": [self.adf_is_stationary],
100
+ "adf_lags_used": [self.adf_lags_used],
101
+ "adf_n_obs": [self.adf_n_obs],
102
+ "kpss_statistic": [self.kpss_statistic],
103
+ "kpss_pvalue": [self.kpss_pvalue],
104
+ "kpss_stationary": [self.kpss_is_stationary],
105
+ "pp_statistic": [self.pp_statistic],
106
+ "pp_pvalue": [self.pp_pvalue],
107
+ "pp_stationary": [self.pp_is_stationary],
108
+ }
109
+ return pl.DataFrame(data)
110
+
111
+ def summary(self) -> str:
112
+ """Human-readable summary of stationarity tests."""
113
+ lines = [f"Stationarity Tests: {self.feature_name}"]
114
+ if self.adf_is_stationary is not None:
115
+ lines.append(
116
+ f" ADF: {'Stationary' if self.adf_is_stationary else 'Non-stationary'} (p={self.adf_pvalue:.4f})"
117
+ )
118
+ if self.kpss_is_stationary is not None:
119
+ lines.append(
120
+ f" KPSS: {'Stationary' if self.kpss_is_stationary else 'Non-stationary'} (p={self.kpss_pvalue:.4f})"
121
+ )
122
+ if self.pp_is_stationary is not None:
123
+ lines.append(
124
+ f" PP: {'Stationary' if self.pp_is_stationary else 'Non-stationary'} (p={self.pp_pvalue:.4f})"
125
+ )
126
+ return "\n".join(lines)
127
+
128
+
129
+ class ACFResult(BaseResult):
130
+ """Autocorrelation Function (ACF) and Partial ACF analysis results.
131
+
132
+ Detects serial correlation and lag structure in time series.
133
+
134
+ Attributes:
135
+ feature_name: Name of feature analyzed
136
+ acf_values: ACF values at each lag
137
+ pacf_values: PACF values at each lag
138
+ significant_lags_acf: List of lags with significant ACF
139
+ significant_lags_pacf: List of lags with significant PACF
140
+ ljung_box_statistic: Ljung-Box test statistic
141
+ ljung_box_pvalue: Ljung-Box p-value (reject H0 => autocorrelation present)
142
+ """
143
+
144
+ analysis_type: str = "acf_analysis"
145
+ feature_name: str = Field(..., description="Feature name")
146
+
147
+ acf_values: list[float] = Field(..., description="ACF at each lag")
148
+ pacf_values: list[float] = Field(..., description="PACF at each lag")
149
+ significant_lags_acf: list[int] = Field(
150
+ default_factory=list, description="Lags with significant ACF"
151
+ )
152
+ significant_lags_pacf: list[int] = Field(
153
+ default_factory=list, description="Lags with significant PACF"
154
+ )
155
+
156
+ ljung_box_statistic: float | None = Field(None, description="Ljung-Box statistic")
157
+ ljung_box_pvalue: float | None = Field(None, description="Ljung-Box p-value")
158
+
159
+ def list_available_dataframes(self) -> list[str]:
160
+ """List available DataFrame views.
161
+
162
+ Returns:
163
+ List with single 'primary' view containing ACF/PACF values
164
+ """
165
+ return ["primary"]
166
+
167
+ def get_dataframe(self, name: str | None = None) -> pl.DataFrame:
168
+ """Get ACF/PACF values as DataFrame.
169
+
170
+ Args:
171
+ name: DataFrame name (ignored, only 'primary' available)
172
+
173
+ Returns:
174
+ DataFrame with lag, ACF, and PACF values
175
+
176
+ Raises:
177
+ ValueError: If name is provided but not 'primary'
178
+ """
179
+ if name is not None and name != "primary":
180
+ raise ValueError(
181
+ f"Unknown DataFrame name: {name}. Available: {self.list_available_dataframes()}"
182
+ )
183
+
184
+ n_lags = len(self.acf_values)
185
+ data = {
186
+ "lag": list(range(n_lags)),
187
+ "acf": self.acf_values,
188
+ "pacf": self.pacf_values,
189
+ }
190
+ return pl.DataFrame(data)
191
+
192
+ def summary(self) -> str:
193
+ """Human-readable summary of autocorrelation analysis."""
194
+ lines = [f"ACF/PACF Analysis: {self.feature_name}"]
195
+ lines.append(f" Lags analyzed: {len(self.acf_values)}")
196
+ lines.append(f" Significant ACF lags: {self.significant_lags_acf}")
197
+ lines.append(f" Significant PACF lags: {self.significant_lags_pacf}")
198
+ if self.ljung_box_pvalue is not None:
199
+ lines.append(
200
+ f" Ljung-Box test: p={self.ljung_box_pvalue:.4f} "
201
+ f"({'Autocorrelation present' if self.ljung_box_pvalue < 0.05 else 'No autocorrelation'})"
202
+ )
203
+ return "\n".join(lines)
204
+
205
+
206
+ class FeatureDiagnosticsResult(BaseResult):
207
+ """Complete results from Module A: Feature Diagnostics.
208
+
209
+ Comprehensive analysis of individual feature properties:
210
+ - Stationarity testing (ADF, KPSS, PP)
211
+ - Autocorrelation structure (ACF, PACF)
212
+ - Volatility clustering (GARCH effects)
213
+ - Distribution characteristics (normality, skewness, kurtosis)
214
+
215
+ Attributes:
216
+ stationarity_tests: Stationarity test results for each feature
217
+ acf_results: ACF/PACF analysis for each feature
218
+ volatility_clustering: GARCH detection results
219
+ distribution_stats: Distribution characteristics
220
+ """
221
+
222
+ analysis_type: str = "feature_diagnostics"
223
+
224
+ stationarity_tests: list[StationarityTestResult] = Field(
225
+ default_factory=list, description="Stationarity test results"
226
+ )
227
+ acf_results: list[ACFResult] = Field(
228
+ default_factory=list, description="ACF/PACF analysis results"
229
+ )
230
+ volatility_clustering: dict[str, Any] = Field(
231
+ default_factory=dict, description="GARCH detection results"
232
+ )
233
+ distribution_stats: dict[str, Any] = Field(
234
+ default_factory=dict, description="Distribution characteristics"
235
+ )
236
+
237
+ def get_stationarity_dataframe(self) -> pl.DataFrame:
238
+ """Get stationarity test results as DataFrame.
239
+
240
+ Returns:
241
+ DataFrame with all stationarity tests
242
+ """
243
+ if not self.stationarity_tests:
244
+ return pl.DataFrame()
245
+
246
+ # Combine all test results
247
+ dfs = [test.get_dataframe() for test in self.stationarity_tests]
248
+ return pl.concat(dfs)
249
+
250
+ def get_acf_dataframe(self, feature_name: str | None = None) -> pl.DataFrame:
251
+ """Get ACF/PACF results as DataFrame.
252
+
253
+ Args:
254
+ feature_name: Optional filter by feature
255
+
256
+ Returns:
257
+ DataFrame with ACF/PACF values
258
+ """
259
+ if not self.acf_results:
260
+ return pl.DataFrame()
261
+
262
+ results = self.acf_results
263
+ if feature_name:
264
+ results = [r for r in results if r.feature_name == feature_name]
265
+
266
+ dfs = []
267
+ for result in results:
268
+ df = result.get_dataframe()
269
+ df = df.with_columns(pl.lit(result.feature_name).alias("feature"))
270
+ dfs.append(df)
271
+
272
+ return pl.concat(dfs) if dfs else pl.DataFrame()
273
+
274
+ def get_dataframe(self, name: str | None = None) -> pl.DataFrame:
275
+ """Get results as DataFrame.
276
+
277
+ Args:
278
+ name: 'stationarity' or 'acf'
279
+
280
+ Returns:
281
+ Requested DataFrame
282
+ """
283
+ if name == "stationarity":
284
+ return self.get_stationarity_dataframe()
285
+ elif name == "acf":
286
+ return self.get_acf_dataframe()
287
+ else:
288
+ return self.get_stationarity_dataframe()
289
+
290
+ def summary(self) -> str:
291
+ """Human-readable summary of diagnostics."""
292
+ lines = ["Feature Diagnostics Summary", "=" * 40]
293
+ lines.append(f"Features analyzed: {len(self.stationarity_tests)}")
294
+ lines.append("")
295
+
296
+ # Stationarity summary
297
+ if self.stationarity_tests:
298
+ stationary = sum(
299
+ 1 for t in self.stationarity_tests if t.adf_is_stationary or t.kpss_is_stationary
300
+ )
301
+ lines.append(f"Stationary features: {stationary}/{len(self.stationarity_tests)}")
302
+
303
+ # ACF summary
304
+ if self.acf_results:
305
+ with_autocorr = sum(1 for r in self.acf_results if r.significant_lags_acf)
306
+ lines.append(f"Features with autocorrelation: {with_autocorr}/{len(self.acf_results)}")
307
+
308
+ return "\n".join(lines)
309
+
310
+ def to_engineer_config(self) -> EngineerConfig:
311
+ """Generate preprocessing recommendations for ML4T Engineer.
312
+
313
+ Analyzes diagnostic results to recommend appropriate transforms:
314
+ - Non-stationary → DIFF (first difference)
315
+ - High skewness (>2) → LOG or SQRT transform
316
+ - Outliers detected → WINSORIZE
317
+ - Already good quality → NONE
318
+
319
+ Returns:
320
+ EngineerConfig with preprocessing recommendations
321
+
322
+ Example:
323
+ >>> diagnostics = evaluator.evaluate_diagnostics(features_df)
324
+ >>> eng_config = diagnostics.to_engineer_config()
325
+ >>> preprocessing_dict = eng_config.to_dict()
326
+ """
327
+ from ml4t.diagnostic.integration.engineer_contract import (
328
+ EngineerConfig,
329
+ PreprocessingRecommendation,
330
+ TransformType,
331
+ )
332
+
333
+ recommendations = []
334
+
335
+ # Process stationarity tests
336
+ for stationarity in self.stationarity_tests:
337
+ feature_name = stationarity.feature_name
338
+
339
+ # Check if non-stationary (both ADF and KPSS should agree ideally)
340
+ adf_non_stationary = (
341
+ stationarity.adf_is_stationary is not None and not stationarity.adf_is_stationary
342
+ )
343
+ kpss_non_stationary = (
344
+ stationarity.kpss_is_stationary is not None and not stationarity.kpss_is_stationary
345
+ )
346
+ pp_non_stationary = (
347
+ stationarity.pp_is_stationary is not None and not stationarity.pp_is_stationary
348
+ )
349
+
350
+ # Count non-stationary signals
351
+ non_stationary_count = sum([adf_non_stationary, kpss_non_stationary, pp_non_stationary])
352
+
353
+ if non_stationary_count >= 2:
354
+ # At least 2 tests indicate non-stationarity
355
+ confidence = 0.9 if non_stationary_count == 3 else 0.8
356
+ diagnostics_dict = {}
357
+ if stationarity.adf_pvalue is not None:
358
+ diagnostics_dict["adf_pvalue"] = stationarity.adf_pvalue
359
+ if stationarity.kpss_pvalue is not None:
360
+ diagnostics_dict["kpss_pvalue"] = stationarity.kpss_pvalue
361
+
362
+ recommendations.append(
363
+ PreprocessingRecommendation(
364
+ feature_name=feature_name,
365
+ transform=TransformType.DIFF,
366
+ reason=f"Feature is non-stationary ({non_stationary_count}/3 tests)",
367
+ confidence=confidence,
368
+ diagnostics=diagnostics_dict if diagnostics_dict else None,
369
+ )
370
+ )
371
+ elif non_stationary_count == 1:
372
+ # Only 1 test indicates non-stationarity - lower confidence
373
+ test_name = "ADF" if adf_non_stationary else "KPSS" if kpss_non_stationary else "PP"
374
+ pvalue: float | None = getattr(stationarity, f"{test_name.lower()}_pvalue")
375
+ single_test_diagnostics: dict[str, float] | None = (
376
+ {f"{test_name.lower()}_pvalue": pvalue} if pvalue is not None else None
377
+ )
378
+ recommendations.append(
379
+ PreprocessingRecommendation(
380
+ feature_name=feature_name,
381
+ transform=TransformType.DIFF,
382
+ reason=f"Possible non-stationarity ({test_name} test)",
383
+ confidence=0.6,
384
+ diagnostics=single_test_diagnostics,
385
+ )
386
+ )
387
+ else:
388
+ # Stationary - no transform needed
389
+ recommendations.append(
390
+ PreprocessingRecommendation(
391
+ feature_name=feature_name,
392
+ transform=TransformType.NONE,
393
+ reason="Feature is stationary (all tests)",
394
+ confidence=0.9,
395
+ )
396
+ )
397
+
398
+ # Check distribution stats for skewness/outliers
399
+ # (This is a placeholder - actual implementation depends on what's in distribution_stats)
400
+ if self.distribution_stats:
401
+ for feature_name, stats in self.distribution_stats.items():
402
+ # Skip if already recommended differencing
403
+ if any(
404
+ r.feature_name == feature_name and r.transform == TransformType.DIFF
405
+ for r in recommendations
406
+ ):
407
+ continue
408
+
409
+ # Check for high skewness
410
+ skewness = stats.get("skewness")
411
+ if skewness is not None and abs(skewness) > 2:
412
+ # High positive skew → log transform
413
+ if skewness > 2:
414
+ recommendations.append(
415
+ PreprocessingRecommendation(
416
+ feature_name=feature_name,
417
+ transform=TransformType.LOG,
418
+ reason=f"High right skew (skewness={skewness:.2f})",
419
+ confidence=0.85,
420
+ diagnostics={"skewness": skewness},
421
+ )
422
+ )
423
+ # High negative skew → reflect and log (but we'll use sqrt as milder)
424
+ else:
425
+ recommendations.append(
426
+ PreprocessingRecommendation(
427
+ feature_name=feature_name,
428
+ transform=TransformType.SQRT,
429
+ reason=f"High left skew (skewness={skewness:.2f})",
430
+ confidence=0.75,
431
+ diagnostics={"skewness": skewness},
432
+ )
433
+ )
434
+
435
+ # Check for outliers
436
+ has_outliers = stats.get("has_outliers", False)
437
+ if has_outliers:
438
+ recommendations.append(
439
+ PreprocessingRecommendation(
440
+ feature_name=feature_name,
441
+ transform=TransformType.WINSORIZE,
442
+ reason="Outliers detected at tail percentiles",
443
+ confidence=0.8,
444
+ )
445
+ )
446
+
447
+ return EngineerConfig(
448
+ recommendations=recommendations,
449
+ metadata={
450
+ "created_at": self.created_at,
451
+ "diagnostic_version": self.version,
452
+ },
453
+ )
454
+
455
+
456
+ # =============================================================================
457
+ # Module B: Cross-Feature Analysis
458
+ # =============================================================================
459
+
460
+
461
+ class CrossFeatureResult(BaseResult):
462
+ """Results from Module B: Cross-Feature Analysis.
463
+
464
+ Analysis of relationships between features:
465
+ - Correlation matrix
466
+ - PCA (dimensionality reduction)
467
+ - Clustering (feature groups)
468
+ - Redundancy detection
469
+
470
+ Attributes:
471
+ correlation_matrix: Correlation matrix (stored as nested list for JSON)
472
+ feature_names: List of feature names
473
+ pca_results: PCA analysis results (variance explained, loadings)
474
+ clustering_results: Feature clustering results
475
+ redundant_features: Highly correlated feature pairs
476
+ """
477
+
478
+ analysis_type: str = "cross_feature"
479
+
480
+ correlation_matrix: list[list[float]] = Field(
481
+ ..., description="Correlation matrix as nested list"
482
+ )
483
+ feature_names: list[str] = Field(..., description="Feature names in matrix order")
484
+
485
+ pca_results: dict[str, Any] | None = Field(
486
+ None, description="PCA analysis (variance explained, loadings)"
487
+ )
488
+ clustering_results: dict[str, Any] | None = Field(
489
+ None, description="Feature clustering results"
490
+ )
491
+ redundant_features: list[tuple[str, str, float]] | None = Field(
492
+ None, description="Redundant pairs: (feature1, feature2, correlation)"
493
+ )
494
+
495
+ def get_correlation_dataframe(self) -> pl.DataFrame:
496
+ """Get correlation matrix as DataFrame.
497
+
498
+ Returns:
499
+ DataFrame with correlations in long format
500
+ """
501
+ # Convert to long format for easier manipulation
502
+ n = len(self.feature_names)
503
+ rows = []
504
+ for i in range(n):
505
+ for j in range(n):
506
+ rows.append(
507
+ {
508
+ "feature_1": self.feature_names[i],
509
+ "feature_2": self.feature_names[j],
510
+ "correlation": self.correlation_matrix[i][j],
511
+ }
512
+ )
513
+ return pl.DataFrame(rows)
514
+
515
+ def get_redundancy_dataframe(self) -> pl.DataFrame:
516
+ """Get redundant feature pairs as DataFrame.
517
+
518
+ Returns:
519
+ DataFrame with redundant pairs
520
+ """
521
+ if not self.redundant_features:
522
+ return pl.DataFrame(
523
+ schema={"feature_1": pl.Utf8, "feature_2": pl.Utf8, "correlation": pl.Float64}
524
+ )
525
+
526
+ rows = [
527
+ {"feature_1": f1, "feature_2": f2, "correlation": corr}
528
+ for f1, f2, corr in self.redundant_features
529
+ ]
530
+ return pl.DataFrame(rows)
531
+
532
+ def get_dataframe(self, name: str | None = None) -> pl.DataFrame:
533
+ """Get results as DataFrame.
534
+
535
+ Args:
536
+ name: 'correlation' or 'redundancy'
537
+
538
+ Returns:
539
+ Requested DataFrame
540
+ """
541
+ if name == "redundancy":
542
+ return self.get_redundancy_dataframe()
543
+ else:
544
+ return self.get_correlation_dataframe()
545
+
546
+ def summary(self) -> str:
547
+ """Human-readable summary of cross-feature analysis."""
548
+ lines = ["Cross-Feature Analysis Summary", "=" * 40]
549
+ lines.append(f"Features analyzed: {len(self.feature_names)}")
550
+
551
+ if self.redundant_features:
552
+ lines.append(f"Redundant pairs detected: {len(self.redundant_features)}")
553
+ for f1, f2, corr in self.redundant_features[:5]: # Show top 5
554
+ lines.append(f" {f1} <-> {f2}: {corr:.3f}")
555
+ if len(self.redundant_features) > 5:
556
+ lines.append(f" ... and {len(self.redundant_features) - 5} more")
557
+
558
+ if self.pca_results:
559
+ variance = self.pca_results.get("variance_explained", [])
560
+ if variance:
561
+ lines.append(
562
+ f"PCA: {len(variance)} components explain {sum(variance):.1%} variance"
563
+ )
564
+
565
+ return "\n".join(lines)
566
+
567
+
568
+ # =============================================================================
569
+ # Module C: Feature-Outcome Relationships
570
+ # =============================================================================
571
+
572
+
573
+ class ICAnalysisResult(BaseResult):
574
+ """Information Coefficient (IC) analysis for a single feature.
575
+
576
+ Measures correlation between feature ranks and outcome ranks,
577
+ with HAC adjustment for autocorrelation.
578
+
579
+ Attributes:
580
+ feature_name: Feature being analyzed
581
+ ic_values: IC at each lag (if lagged analysis)
582
+ mean_ic: Average IC across lags
583
+ ic_std: Standard deviation of IC
584
+ ic_ir: Information Ratio (mean_ic / ic_std)
585
+ pvalue: P-value for IC significance
586
+ hac_adjusted_pvalue: HAC-adjusted p-value
587
+ """
588
+
589
+ analysis_type: str = "ic_analysis"
590
+ feature_name: str = Field(..., description="Feature name")
591
+
592
+ ic_values: list[float] = Field(..., description="IC at each lag")
593
+ mean_ic: float = Field(..., description="Mean IC")
594
+ ic_std: float = Field(..., description="IC standard deviation")
595
+ ic_ir: float = Field(..., description="Information Ratio (mean / std)")
596
+
597
+ pvalue: float | None = Field(None, description="P-value for IC significance")
598
+ hac_adjusted_pvalue: float | None = Field(None, description="HAC-adjusted p-value")
599
+
600
+ def get_dataframe(self, name: str | None = None) -> pl.DataFrame:
601
+ """Get IC values as DataFrame.
602
+
603
+ Args:
604
+ name: Unused, included for base class compatibility.
605
+
606
+ Returns:
607
+ DataFrame with lag and IC values
608
+ """
609
+ del name # Unused, base class compatibility
610
+ data = {
611
+ "feature": [self.feature_name] * len(self.ic_values),
612
+ "lag": list(range(len(self.ic_values))),
613
+ "ic": self.ic_values,
614
+ }
615
+ return pl.DataFrame(data)
616
+
617
+ def summary(self) -> str:
618
+ """Human-readable summary of IC analysis."""
619
+ lines = [f"IC Analysis: {self.feature_name}"]
620
+ lines.append(f" Mean IC: {self.mean_ic:.4f}")
621
+ lines.append(f" IC IR: {self.ic_ir:.4f}")
622
+ if self.hac_adjusted_pvalue is not None:
623
+ sig = "Significant" if self.hac_adjusted_pvalue < 0.05 else "Not significant"
624
+ lines.append(f" HAC p-value: {self.hac_adjusted_pvalue:.4f} ({sig})")
625
+ return "\n".join(lines)
626
+
627
+
628
+ class ThresholdAnalysisResult(BaseResult):
629
+ """Binary classification threshold analysis for a single feature.
630
+
631
+ Evaluates feature as binary signal using optimal threshold.
632
+
633
+ Attributes:
634
+ feature_name: Feature being analyzed
635
+ optimal_threshold: Threshold value that optimizes target metric
636
+ precision: Precision at optimal threshold
637
+ recall: Recall at optimal threshold
638
+ f1_score: F1 score at optimal threshold
639
+ lift: Lift over base rate
640
+ coverage: Fraction of observations with positive signal
641
+ """
642
+
643
+ analysis_type: str = "threshold_analysis"
644
+ feature_name: str = Field(..., description="Feature name")
645
+
646
+ optimal_threshold: float = Field(..., description="Optimal threshold value")
647
+ precision: float = Field(..., description="Precision at optimal threshold")
648
+ recall: float = Field(..., description="Recall at optimal threshold")
649
+ f1_score: float = Field(..., description="F1 score at optimal threshold")
650
+ lift: float = Field(..., description="Lift over base rate")
651
+ coverage: float = Field(..., description="Signal coverage (fraction positive)")
652
+
653
+ def get_dataframe(self, name: str | None = None) -> pl.DataFrame:
654
+ """Get threshold analysis as DataFrame.
655
+
656
+ Args:
657
+ name: Unused, included for base class compatibility.
658
+
659
+ Returns:
660
+ Single-row DataFrame with all metrics
661
+ """
662
+ del name # Unused, base class compatibility
663
+ data = {
664
+ "feature": [self.feature_name],
665
+ "threshold": [self.optimal_threshold],
666
+ "precision": [self.precision],
667
+ "recall": [self.recall],
668
+ "f1_score": [self.f1_score],
669
+ "lift": [self.lift],
670
+ "coverage": [self.coverage],
671
+ }
672
+ return pl.DataFrame(data)
673
+
674
+ def summary(self) -> str:
675
+ """Human-readable summary of threshold analysis."""
676
+ lines = [f"Threshold Analysis: {self.feature_name}"]
677
+ lines.append(f" Optimal threshold: {self.optimal_threshold:.4f}")
678
+ lines.append(f" Precision: {self.precision:.2%}")
679
+ lines.append(f" Recall: {self.recall:.2%}")
680
+ lines.append(f" F1 Score: {self.f1_score:.2%}")
681
+ lines.append(f" Lift: {self.lift:.2f}x")
682
+ lines.append(f" Coverage: {self.coverage:.2%}")
683
+ return "\n".join(lines)
684
+
685
+
686
+ class FeatureOutcomeResult(BaseResult):
687
+ """Complete results from Module C: Feature-Outcome Relationships.
688
+
689
+ Analysis of how features relate to outcomes:
690
+ - IC analysis (rank correlations)
691
+ - Threshold analysis (binary classification)
692
+ - ML feature importance (if applicable)
693
+
694
+ Attributes:
695
+ ic_results: IC analysis for each feature
696
+ threshold_results: Threshold analysis for each feature
697
+ ml_importance: ML feature importance scores
698
+ """
699
+
700
+ analysis_type: str = "feature_outcome"
701
+
702
+ ic_results: list[ICAnalysisResult] = Field(
703
+ default_factory=list, description="IC analysis per feature"
704
+ )
705
+ threshold_results: list[ThresholdAnalysisResult] | None = Field(
706
+ None, description="Threshold analysis per feature"
707
+ )
708
+ ml_importance: dict[str, float] | None = Field(
709
+ None, description="ML feature importance: {feature: importance}"
710
+ )
711
+
712
+ def get_ic_dataframe(self) -> pl.DataFrame:
713
+ """Get IC analysis as DataFrame.
714
+
715
+ Returns:
716
+ DataFrame with IC metrics for all features
717
+ """
718
+ if not self.ic_results:
719
+ return pl.DataFrame()
720
+
721
+ rows = []
722
+ for result in self.ic_results:
723
+ rows.append(
724
+ {
725
+ "feature": result.feature_name,
726
+ "mean_ic": result.mean_ic,
727
+ "ic_std": result.ic_std,
728
+ "ic_ir": result.ic_ir,
729
+ "pvalue": result.pvalue,
730
+ "hac_pvalue": result.hac_adjusted_pvalue,
731
+ }
732
+ )
733
+ return pl.DataFrame(rows)
734
+
735
+ def get_threshold_dataframe(self) -> pl.DataFrame:
736
+ """Get threshold analysis as DataFrame.
737
+
738
+ Returns:
739
+ DataFrame with threshold metrics for all features
740
+ """
741
+ if not self.threshold_results:
742
+ return pl.DataFrame()
743
+
744
+ dfs = [result.get_dataframe() for result in self.threshold_results]
745
+ return pl.concat(dfs)
746
+
747
+ def get_dataframe(self, name: str | None = None) -> pl.DataFrame:
748
+ """Get results as DataFrame.
749
+
750
+ Args:
751
+ name: 'ic' or 'threshold'
752
+
753
+ Returns:
754
+ Requested DataFrame
755
+ """
756
+ if name == "threshold":
757
+ return self.get_threshold_dataframe()
758
+ else:
759
+ return self.get_ic_dataframe()
760
+
761
+ def summary(self) -> str:
762
+ """Human-readable summary of feature-outcome relationships."""
763
+ lines = ["Feature-Outcome Analysis Summary", "=" * 40]
764
+
765
+ if self.ic_results:
766
+ lines.append(f"IC analysis: {len(self.ic_results)} features")
767
+ significant = sum(
768
+ 1 for r in self.ic_results if r.hac_adjusted_pvalue and r.hac_adjusted_pvalue < 0.05
769
+ )
770
+ lines.append(f" Significant features: {significant}")
771
+
772
+ # Top features by IC
773
+ top = sorted(self.ic_results, key=lambda r: abs(r.mean_ic), reverse=True)[:3]
774
+ lines.append(" Top 3 by |IC|:")
775
+ for r in top:
776
+ lines.append(f" {r.feature_name}: IC={r.mean_ic:.4f}, IR={r.ic_ir:.4f}")
777
+
778
+ if self.threshold_results:
779
+ lines.append("")
780
+ lines.append(f"Threshold analysis: {len(self.threshold_results)} features")
781
+ # Top features by F1
782
+ top = sorted(self.threshold_results, key=lambda r: r.f1_score, reverse=True)[:3]
783
+ lines.append(" Top 3 by F1:")
784
+ for r in top:
785
+ lines.append(f" {r.feature_name}: F1={r.f1_score:.2%}, Lift={r.lift:.2f}x")
786
+
787
+ return "\n".join(lines)