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,666 @@
1
+ """Feature-outcome relationship analysis (Module C).
2
+
3
+ This module provides comprehensive analysis of how features relate to outcomes:
4
+ - **IC Analysis**: Information Coefficient for predictive power
5
+ - **Binary Classification**: Precision, recall, lift for signal quality
6
+ - **Threshold Optimization**: Find optimal thresholds for signals
7
+ - **ML Diagnostics**: Feature importance, SHAP, interactions
8
+ - **Drift Detection**: Monitor feature distribution stability
9
+
10
+ The FeatureOutcome class orchestrates all analyses into a unified workflow.
11
+
12
+ Example:
13
+ >>> from ml4t.diagnostic.evaluation.feature_outcome import FeatureOutcome
14
+ >>> from ml4t.diagnostic.config.feature_config import DiagnosticConfig
15
+ >>>
16
+ >>> # Basic usage
17
+ >>> analyzer = FeatureOutcome()
18
+ >>> results = analyzer.run_analysis(features_df, returns_df)
19
+ >>> print(results.summary)
20
+ >>>
21
+ >>> # Custom configuration
22
+ >>> config = DiagnosticConfig(
23
+ ... ic=ICConfig(lag_structure=[0, 1, 5, 10, 21]),
24
+ ... ml_diagnostics=MLDiagnosticsConfig(shap_analysis=True)
25
+ ... )
26
+ >>> analyzer = FeatureOutcome(config=config)
27
+ >>> results = analyzer.run_analysis(features_df, returns_df, verbose=True)
28
+ >>>
29
+ >>> # Get recommendations
30
+ >>> for rec in results.get_recommendations():
31
+ ... print(f"• {rec}")
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import time
37
+ from dataclasses import dataclass, field
38
+ from typing import Any
39
+
40
+ import numpy as np
41
+ import pandas as pd
42
+ import polars as pl
43
+
44
+ from ml4t.diagnostic.config.feature_config import DiagnosticConfig
45
+ from ml4t.diagnostic.evaluation.drift import DriftSummaryResult, analyze_drift
46
+ from ml4t.diagnostic.utils.dependencies import DEPS, warn_if_missing
47
+
48
+
49
+ @dataclass
50
+ class FeatureICResults:
51
+ """IC analysis results for a single feature.
52
+
53
+ Attributes:
54
+ feature: Feature name
55
+ ic_mean: Mean IC across time
56
+ ic_std: Standard deviation of IC
57
+ ic_ir: IC Information Ratio (mean/std)
58
+ t_stat: T-statistic for IC
59
+ p_value: P-value for IC significance
60
+ ic_by_lag: IC values by forward horizon
61
+ hac_adjusted: Whether HAC adjustment was applied
62
+ n_observations: Number of observations used
63
+ """
64
+
65
+ feature: str
66
+ ic_mean: float = 0.0
67
+ ic_std: float = 0.0
68
+ ic_ir: float = 0.0
69
+ t_stat: float = 0.0
70
+ p_value: float = 1.0
71
+ ic_by_lag: dict[int, float] = field(default_factory=dict)
72
+ hac_adjusted: bool = False
73
+ n_observations: int = 0
74
+
75
+
76
+ @dataclass
77
+ class FeatureImportanceResults:
78
+ """ML feature importance results.
79
+
80
+ Attributes:
81
+ feature: Feature name
82
+ mdi_importance: Mean Decrease in Impurity (tree-based)
83
+ permutation_importance: Permutation-based importance
84
+ permutation_std: Standard deviation of permutation importance
85
+ shap_mean: Mean absolute SHAP value (if computed)
86
+ shap_std: Standard deviation of SHAP values (if computed)
87
+ rank_mdi: Rank by MDI importance
88
+ rank_permutation: Rank by permutation importance
89
+ """
90
+
91
+ feature: str
92
+ mdi_importance: float = 0.0
93
+ permutation_importance: float = 0.0
94
+ permutation_std: float = 0.0
95
+ shap_mean: float | None = None
96
+ shap_std: float | None = None
97
+ rank_mdi: int = 0
98
+ rank_permutation: int = 0
99
+
100
+
101
+ @dataclass
102
+ class FeatureOutcomeResult:
103
+ """Comprehensive feature-outcome analysis results.
104
+
105
+ This aggregates all Module C analyses into a single result object.
106
+
107
+ Attributes:
108
+ features: List of features analyzed
109
+ ic_results: IC analysis per feature
110
+ importance_results: ML importance per feature
111
+ drift_results: Drift detection results (if enabled)
112
+ interaction_matrix: H-statistic interaction matrix (if computed)
113
+ summary: High-level summary DataFrame
114
+ recommendations: Actionable recommendations
115
+ config: Configuration used
116
+ metadata: Analysis metadata (runtime, samples, etc.)
117
+ errors: Dict of features that failed analysis
118
+ """
119
+
120
+ features: list[str]
121
+ ic_results: dict[str, FeatureICResults] = field(default_factory=dict)
122
+ importance_results: dict[str, FeatureImportanceResults] = field(default_factory=dict)
123
+ drift_results: DriftSummaryResult | None = None
124
+ interaction_matrix: pd.DataFrame | None = None
125
+ summary: pd.DataFrame | None = None
126
+ recommendations: list[str] = field(default_factory=list)
127
+ config: DiagnosticConfig | None = None
128
+ metadata: dict[str, Any] = field(default_factory=dict)
129
+ errors: dict[str, str] = field(default_factory=dict)
130
+
131
+ def to_dataframe(self) -> pd.DataFrame:
132
+ """Export summary as DataFrame.
133
+
134
+ Returns:
135
+ DataFrame with one row per feature, columns for all metrics
136
+ """
137
+ if self.summary is not None:
138
+ return self.summary
139
+
140
+ # Build summary from individual results
141
+ rows = []
142
+ for feature in self.features:
143
+ row: dict[str, str | float | bool | int] = {"feature": feature}
144
+
145
+ # IC metrics (with defaults for missing)
146
+ if feature in self.ic_results:
147
+ ic = self.ic_results[feature]
148
+ row.update(
149
+ {
150
+ "ic_mean": ic.ic_mean,
151
+ "ic_std": ic.ic_std,
152
+ "ic_ir": ic.ic_ir,
153
+ "ic_pvalue": ic.p_value,
154
+ "ic_significant": ic.p_value < 0.05,
155
+ }
156
+ )
157
+ else:
158
+ # Add NaN placeholders if IC not computed
159
+ row.update(
160
+ {
161
+ "ic_mean": np.nan,
162
+ "ic_std": np.nan,
163
+ "ic_ir": np.nan,
164
+ "ic_pvalue": np.nan,
165
+ "ic_significant": False,
166
+ }
167
+ )
168
+
169
+ # Importance metrics (with defaults for missing)
170
+ if feature in self.importance_results:
171
+ imp = self.importance_results[feature]
172
+ row.update(
173
+ {
174
+ "mdi_importance": imp.mdi_importance,
175
+ "permutation_importance": imp.permutation_importance,
176
+ "rank_mdi": imp.rank_mdi,
177
+ "rank_permutation": imp.rank_permutation,
178
+ }
179
+ )
180
+ else:
181
+ row.update(
182
+ {
183
+ "mdi_importance": np.nan,
184
+ "permutation_importance": np.nan,
185
+ "rank_mdi": np.nan,
186
+ "rank_permutation": np.nan,
187
+ }
188
+ )
189
+
190
+ # Drift metrics
191
+ if self.drift_results is not None:
192
+ drift_df = self.drift_results.to_dataframe()
193
+ # Convert to pandas if polars
194
+ if isinstance(drift_df, pl.DataFrame):
195
+ drift_df = drift_df.to_pandas()
196
+
197
+ feature_drift = drift_df[drift_df["feature"] == feature]
198
+ if len(feature_drift) > 0:
199
+ row["drifted"] = feature_drift["drifted"].iloc[0]
200
+ if "psi" in feature_drift.columns:
201
+ row["psi"] = feature_drift["psi"].iloc[0]
202
+ else:
203
+ row["drifted"] = False
204
+ else:
205
+ row["drifted"] = False
206
+
207
+ # Error status
208
+ row["error"] = feature in self.errors
209
+
210
+ rows.append(row)
211
+
212
+ return pd.DataFrame(rows)
213
+
214
+ def get_top_features(
215
+ self, n: int = 10, by: str = "ic_ir", ascending: bool = False
216
+ ) -> list[str]:
217
+ """Get top N features by specified metric.
218
+
219
+ Args:
220
+ n: Number of features to return
221
+ by: Metric to sort by ('ic_ir', 'ic_mean', 'mdi_importance', etc.)
222
+ ascending: Sort in ascending order (default: descending)
223
+
224
+ Returns:
225
+ List of top feature names
226
+
227
+ Example:
228
+ >>> # Get 5 features with highest IC IR
229
+ >>> top_features = results.get_top_features(n=5, by='ic_ir')
230
+ """
231
+ df = self.to_dataframe()
232
+
233
+ if by not in df.columns:
234
+ available = [c for c in df.columns if c != "feature"]
235
+ raise ValueError(f"Metric '{by}' not found. Available: {available}")
236
+
237
+ # Remove features with errors or NaN values
238
+ df = df[~df["error"]]
239
+ df = df.dropna(subset=[by])
240
+
241
+ # Sort and return top N
242
+ df = df.sort_values(by=by, ascending=ascending)
243
+ return df.head(n)["feature"].tolist()
244
+
245
+ def get_recommendations(self) -> list[str]:
246
+ """Generate actionable recommendations based on analysis.
247
+
248
+ Returns:
249
+ List of recommendation strings
250
+
251
+ Example:
252
+ >>> for rec in results.get_recommendations():
253
+ ... print(f"• {rec}")
254
+ """
255
+ if self.recommendations:
256
+ return self.recommendations
257
+
258
+ # Generate recommendations from results
259
+ recommendations = []
260
+ df = self.to_dataframe()
261
+
262
+ # Strong signals (high IC IR, no drift)
263
+ strong = df[(df["ic_ir"] > 2.0) & (~df.get("drifted", False))]
264
+ if len(strong) > 0:
265
+ for _, row in strong.iterrows():
266
+ recommendations.append(
267
+ f"{row['feature']}: Strong predictive power (IC IR={row['ic_ir']:.2f}), stable distribution"
268
+ )
269
+
270
+ # Weak signals (low IC)
271
+ weak = df[df["ic_ir"].abs() < 0.5]
272
+ if len(weak) > 0:
273
+ features = ", ".join(weak["feature"].tolist()[:5])
274
+ more = f" (+{len(weak) - 5} more)" if len(weak) > 5 else ""
275
+ recommendations.append(f"Consider removing weak signals: {features}{more}")
276
+
277
+ # Drifted features
278
+ if "drifted" in df.columns:
279
+ drifted = df[df["drifted"] == True] # noqa: E712
280
+ if len(drifted) > 0:
281
+ for _, row in drifted.iterrows():
282
+ recommendations.append(
283
+ f"{row['feature']}: Distribution drift detected - consider retraining or investigation"
284
+ )
285
+
286
+ # Features with errors
287
+ if len(self.errors) > 0:
288
+ error_features = ", ".join(list(self.errors.keys())[:3])
289
+ more = f" (+{len(self.errors) - 3} more)" if len(self.errors) > 3 else ""
290
+ recommendations.append(f"Analysis failed for: {error_features}{more}")
291
+
292
+ return recommendations
293
+
294
+
295
+ class FeatureOutcome:
296
+ """Main orchestration class for feature-outcome analysis (Module C).
297
+
298
+ Coordinates comprehensive analysis of feature-outcome relationships:
299
+ - IC analysis (Information Coefficient for predictive power)
300
+ - Binary classification metrics (precision, recall, lift)
301
+ - Threshold optimization
302
+ - ML feature importance (MDI, permutation, SHAP)
303
+ - Feature interactions (H-statistic)
304
+ - Drift detection
305
+
306
+ This class provides a unified interface for all Module C analyses,
307
+ handling configuration, execution, and result aggregation.
308
+
309
+ Examples:
310
+ >>> # Basic usage with defaults
311
+ >>> analyzer = FeatureOutcome()
312
+ >>> results = analyzer.run_analysis(features_df, returns_df)
313
+ >>> print(results.summary)
314
+ >>>
315
+ >>> # Custom configuration
316
+ >>> config = DiagnosticConfig(
317
+ ... ic=ICConfig(lag_structure=[0, 1, 5, 10, 21]),
318
+ ... ml_diagnostics=MLDiagnosticsConfig(shap_analysis=True)
319
+ ... )
320
+ >>> analyzer = FeatureOutcome(config=config)
321
+ >>> results = analyzer.run_analysis(features_df, returns_df, verbose=True)
322
+ >>>
323
+ >>> # Select specific features
324
+ >>> results = analyzer.run_analysis(
325
+ ... features_df,
326
+ ... returns_df,
327
+ ... feature_names=['momentum', 'volume', 'volatility']
328
+ ... )
329
+ >>>
330
+ >>> # Get actionable insights
331
+ >>> top_features = results.get_top_features(n=10, by='ic_ir')
332
+ >>> recommendations = results.get_recommendations()
333
+ """
334
+
335
+ def __init__(self, config: DiagnosticConfig | None = None):
336
+ """Initialize FeatureOutcome analyzer.
337
+
338
+ Args:
339
+ config: Module C configuration. Uses defaults if None.
340
+
341
+ Example:
342
+ >>> # Default configuration
343
+ >>> analyzer = FeatureOutcome()
344
+ >>>
345
+ >>> # Custom configuration
346
+ >>> config = DiagnosticConfig(
347
+ ... ic=ICConfig(hac_adjustment=True),
348
+ ... ml_diagnostics=MLDiagnosticsConfig(drift_detection=True)
349
+ ... )
350
+ >>> analyzer = FeatureOutcome(config=config)
351
+ """
352
+ self.config = config or DiagnosticConfig()
353
+
354
+ def run_analysis(
355
+ self,
356
+ features: pd.DataFrame | pl.DataFrame,
357
+ outcomes: pd.DataFrame | pl.DataFrame | pd.Series | np.ndarray,
358
+ feature_names: list[str] | None = None,
359
+ _date_col: str | None = None,
360
+ verbose: bool = False,
361
+ ) -> FeatureOutcomeResult:
362
+ """Run comprehensive feature-outcome analysis.
363
+
364
+ Executes all enabled analyses in Module C configuration:
365
+ 1. IC analysis (if ic.enabled)
366
+ 2. ML feature importance (if ml_diagnostics.enabled)
367
+ 3. Feature interactions (if ml_diagnostics.enabled)
368
+ 4. Drift detection (if ml_diagnostics.drift_detection)
369
+
370
+ Args:
371
+ features: Feature DataFrame (T x N) with date index or date column
372
+ outcomes: Outcome/returns DataFrame, Series, or array (T x 1 or T)
373
+ feature_names: Specific features to analyze (None = all numeric)
374
+ date_col: Date column name if not in index
375
+ verbose: Print progress messages
376
+
377
+ Returns:
378
+ FeatureOutcomeResult with all analyses
379
+
380
+ Raises:
381
+ ValueError: If inputs are invalid or incompatible
382
+
383
+ Example:
384
+ >>> # Basic usage
385
+ >>> results = analyzer.run_analysis(features_df, returns_df)
386
+ >>>
387
+ >>> # With progress tracking
388
+ >>> results = analyzer.run_analysis(
389
+ ... features_df, returns_df, verbose=True
390
+ ... )
391
+ >>> # Output:
392
+ >>> # Analyzing 10 features...
393
+ >>> # [1/10] feature1: IC=0.15, importance=0.25
394
+ >>> # [2/10] feature2: IC=0.08, importance=0.12
395
+ >>> # ...
396
+ >>> # Analysis complete in 12.3s
397
+ """
398
+ start_time = time.time()
399
+
400
+ # ===================================================================
401
+ # 0. Configuration and Dependency Validation
402
+ # ===================================================================
403
+ if verbose:
404
+ print("Validating configuration and dependencies...")
405
+
406
+ # Check dependencies based on configuration
407
+ missing_deps = []
408
+ if self.config.ml_diagnostics.enabled and self.config.ml_diagnostics.feature_importance:
409
+ if not DEPS.check("lightgbm"):
410
+ missing_deps.append("lightgbm")
411
+ if verbose:
412
+ print(" ⚠️ LightGBM not available - feature importance will be skipped")
413
+ print(f" Install with: {DEPS.lightgbm.install_cmd}")
414
+
415
+ if self.config.ml_diagnostics.enabled and self.config.ml_diagnostics.shap_analysis:
416
+ if not DEPS.check("shap"):
417
+ missing_deps.append("shap")
418
+ if verbose:
419
+ print(" ⚠️ SHAP not available - SHAP analysis will be skipped")
420
+ print(f" Install with: {DEPS.shap.install_cmd}")
421
+
422
+ # Log what features are available
423
+ if verbose and not missing_deps:
424
+ print(" ✓ All required dependencies available")
425
+
426
+ # ===================================================================
427
+ # 1. Input Validation and Preprocessing
428
+ # ===================================================================
429
+ if verbose:
430
+ print("Validating inputs...")
431
+
432
+ # Convert to pandas for consistency
433
+ if isinstance(features, pl.DataFrame):
434
+ features = features.to_pandas()
435
+ if isinstance(outcomes, pl.DataFrame):
436
+ outcomes = outcomes.to_pandas()
437
+ if isinstance(outcomes, pl.Series):
438
+ outcomes = outcomes.to_pandas()
439
+
440
+ # Handle outcomes format
441
+ if isinstance(outcomes, pd.Series):
442
+ outcomes_series = outcomes
443
+ elif isinstance(outcomes, np.ndarray):
444
+ if outcomes.ndim == 1:
445
+ outcomes_series = pd.Series(outcomes, index=features.index)
446
+ else:
447
+ # Take first column
448
+ outcomes_series = pd.Series(outcomes[:, 0], index=features.index)
449
+ elif isinstance(outcomes, pd.DataFrame):
450
+ # Take first column
451
+ outcomes_series = outcomes.iloc[:, 0]
452
+ else:
453
+ raise ValueError(f"Unsupported outcomes type: {type(outcomes)}")
454
+
455
+ # Validate alignment
456
+ if len(features) != len(outcomes_series):
457
+ raise ValueError(
458
+ f"Features ({len(features)}) and outcomes ({len(outcomes_series)}) must have same length"
459
+ )
460
+
461
+ # ===================================================================
462
+ # 2. Determine Features to Analyze
463
+ # ===================================================================
464
+ if feature_names is None:
465
+ # Use all numeric columns
466
+ numeric_cols = features.select_dtypes(include=[np.number]).columns.tolist()
467
+ feature_names = numeric_cols
468
+ else:
469
+ # Validate specified features exist
470
+ missing = set(feature_names) - set(features.columns)
471
+ if missing:
472
+ raise ValueError(f"Features not found in DataFrame: {missing}")
473
+
474
+ if not feature_names:
475
+ raise ValueError("No features to analyze")
476
+
477
+ if verbose:
478
+ print(f"Analyzing {len(feature_names)} features...")
479
+
480
+ # ===================================================================
481
+ # 3. Initialize Results Storage
482
+ # ===================================================================
483
+ ic_results = {}
484
+ importance_results = {}
485
+ errors = {}
486
+
487
+ # ===================================================================
488
+ # 4. Run IC Analysis (if enabled)
489
+ # ===================================================================
490
+ if self.config.ic.enabled:
491
+ if verbose:
492
+ print("Running IC analysis...")
493
+
494
+ for i, feature in enumerate(feature_names, 1):
495
+ try:
496
+ feature_data = features[feature].to_numpy().astype(np.float64)
497
+ outcome_data = outcomes_series.to_numpy().astype(np.float64)
498
+
499
+ # Remove NaN pairs
500
+ mask = ~(np.isnan(feature_data) | np.isnan(outcome_data))
501
+ feature_clean = feature_data[mask]
502
+ outcome_clean = outcome_data[mask]
503
+
504
+ if len(feature_clean) < 10:
505
+ errors[feature] = "Insufficient non-NaN samples"
506
+ continue
507
+
508
+ # Compute basic IC (Spearman correlation as proxy)
509
+ from scipy.stats import spearmanr
510
+
511
+ ic_mean, p_value = spearmanr(feature_clean, outcome_clean)
512
+ ic_std = np.std(feature_clean) # Simplified
513
+ ic_ir = ic_mean / (ic_std + 1e-10)
514
+
515
+ ic_results[feature] = FeatureICResults(
516
+ feature=feature,
517
+ ic_mean=ic_mean,
518
+ ic_std=ic_std,
519
+ ic_ir=ic_ir,
520
+ p_value=p_value,
521
+ n_observations=len(feature_clean),
522
+ )
523
+
524
+ if verbose and i % max(1, len(feature_names) // 10) == 0:
525
+ print(f" [{i}/{len(feature_names)}] {feature}: IC={ic_mean:.3f}")
526
+
527
+ except Exception as e:
528
+ errors[feature] = str(e)
529
+ if verbose:
530
+ print(f" [{i}/{len(feature_names)}] {feature}: ERROR - {e}")
531
+
532
+ # ===================================================================
533
+ # 5. Run ML Diagnostics (if enabled)
534
+ # ===================================================================
535
+ if self.config.ml_diagnostics.enabled and self.config.ml_diagnostics.feature_importance:
536
+ if verbose:
537
+ print("Running feature importance analysis...")
538
+
539
+ try:
540
+ # Check if LightGBM is available
541
+ if warn_if_missing("lightgbm", "feature importance", "skipping analysis"):
542
+ import lightgbm as lgb
543
+
544
+ # Prepare data
545
+ X = features[feature_names].values
546
+ y = outcomes_series.to_numpy()
547
+
548
+ # Remove NaN rows
549
+ mask = ~(np.isnan(X).any(axis=1) | np.isnan(y))
550
+ X_clean = X[mask]
551
+ y_clean = y[mask]
552
+
553
+ if len(X_clean) >= 100:
554
+ # Train simple model for importance
555
+ model = lgb.LGBMRegressor(
556
+ n_estimators=100, max_depth=3, random_state=42, verbose=-1
557
+ )
558
+ model.fit(X_clean, y_clean)
559
+
560
+ # Get MDI importance
561
+ mdi_importances = model.feature_importances_
562
+
563
+ # Rank features
564
+ ranks = np.argsort(mdi_importances)[::-1]
565
+
566
+ for idx, feature in enumerate(feature_names):
567
+ if feature not in errors:
568
+ rank = int(np.where(ranks == idx)[0][0]) + 1
569
+ importance_results[feature] = FeatureImportanceResults(
570
+ feature=feature,
571
+ mdi_importance=float(mdi_importances[idx]),
572
+ rank_mdi=rank,
573
+ )
574
+
575
+ if verbose:
576
+ top_feature = feature_names[ranks[0]]
577
+ print(
578
+ f" Top feature by MDI: {top_feature} (importance={mdi_importances[ranks[0]]:.3f})"
579
+ )
580
+ else:
581
+ if verbose:
582
+ print(
583
+ f" Insufficient clean samples for importance analysis ({len(X_clean)}/100)"
584
+ )
585
+ else:
586
+ if verbose:
587
+ print(" Feature importance skipped (LightGBM not available)")
588
+
589
+ except Exception as e:
590
+ if verbose:
591
+ print(f" Feature importance failed: {e}")
592
+
593
+ # ===================================================================
594
+ # 6. Run Drift Detection (if enabled)
595
+ # ===================================================================
596
+ drift_results = None
597
+ if self.config.ml_diagnostics.drift_detection:
598
+ if verbose:
599
+ print("Running drift detection...")
600
+
601
+ try:
602
+ # Split data into reference (first half) and test (second half)
603
+ split_idx = len(features) // 2
604
+ reference = features[feature_names].iloc[:split_idx]
605
+ test = features[feature_names].iloc[split_idx:]
606
+
607
+ drift_results = analyze_drift(
608
+ reference,
609
+ test,
610
+ features=feature_names,
611
+ methods=["psi", "wasserstein"], # Fast methods only
612
+ )
613
+
614
+ if verbose:
615
+ n_drifted = drift_results.n_features_drifted
616
+ print(f" Drift detected in {n_drifted}/{len(feature_names)} features")
617
+
618
+ except Exception as e:
619
+ if verbose:
620
+ print(f" Drift detection failed: {e}")
621
+
622
+ # ===================================================================
623
+ # 7. Build Summary and Generate Recommendations
624
+ # ===================================================================
625
+ result = FeatureOutcomeResult(
626
+ features=feature_names,
627
+ ic_results=ic_results,
628
+ importance_results=importance_results,
629
+ drift_results=drift_results,
630
+ config=self.config,
631
+ errors=errors,
632
+ metadata={
633
+ "n_features": len(feature_names),
634
+ "n_observations": len(features),
635
+ "n_errors": len(errors),
636
+ "computation_time": time.time() - start_time,
637
+ "ic_enabled": self.config.ic.enabled,
638
+ "ml_diagnostics_enabled": self.config.ml_diagnostics.enabled,
639
+ "drift_detection_enabled": self.config.ml_diagnostics.drift_detection,
640
+ },
641
+ )
642
+
643
+ # Build summary DataFrame
644
+ result.summary = result.to_dataframe()
645
+
646
+ # Generate recommendations
647
+ result.recommendations = result.get_recommendations()
648
+
649
+ if verbose:
650
+ elapsed = time.time() - start_time
651
+ print(f"\nAnalysis complete in {elapsed:.1f}s")
652
+ print(f" Features analyzed: {len(feature_names)}")
653
+ print(f" Errors: {len(errors)}")
654
+ if result.recommendations:
655
+ print(f" Recommendations: {len(result.recommendations)}")
656
+
657
+ return result
658
+
659
+
660
+ # Re-export for convenience
661
+ __all__ = [
662
+ "FeatureICResults",
663
+ "FeatureImportanceResults",
664
+ "FeatureOutcomeResult",
665
+ "FeatureOutcome",
666
+ ]