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.
- ml4t/diagnostic/AGENT.md +25 -0
- ml4t/diagnostic/__init__.py +166 -0
- ml4t/diagnostic/backends/__init__.py +10 -0
- ml4t/diagnostic/backends/adapter.py +192 -0
- ml4t/diagnostic/backends/polars_backend.py +899 -0
- ml4t/diagnostic/caching/__init__.py +40 -0
- ml4t/diagnostic/caching/cache.py +331 -0
- ml4t/diagnostic/caching/decorators.py +131 -0
- ml4t/diagnostic/caching/smart_cache.py +339 -0
- ml4t/diagnostic/config/AGENT.md +24 -0
- ml4t/diagnostic/config/README.md +267 -0
- ml4t/diagnostic/config/__init__.py +219 -0
- ml4t/diagnostic/config/barrier_config.py +277 -0
- ml4t/diagnostic/config/base.py +301 -0
- ml4t/diagnostic/config/event_config.py +148 -0
- ml4t/diagnostic/config/feature_config.py +404 -0
- ml4t/diagnostic/config/multi_signal_config.py +55 -0
- ml4t/diagnostic/config/portfolio_config.py +215 -0
- ml4t/diagnostic/config/report_config.py +391 -0
- ml4t/diagnostic/config/sharpe_config.py +202 -0
- ml4t/diagnostic/config/signal_config.py +206 -0
- ml4t/diagnostic/config/trade_analysis_config.py +310 -0
- ml4t/diagnostic/config/validation.py +279 -0
- ml4t/diagnostic/core/__init__.py +29 -0
- ml4t/diagnostic/core/numba_utils.py +315 -0
- ml4t/diagnostic/core/purging.py +372 -0
- ml4t/diagnostic/core/sampling.py +471 -0
- ml4t/diagnostic/errors/__init__.py +205 -0
- ml4t/diagnostic/evaluation/AGENT.md +26 -0
- ml4t/diagnostic/evaluation/__init__.py +437 -0
- ml4t/diagnostic/evaluation/autocorrelation.py +531 -0
- ml4t/diagnostic/evaluation/barrier_analysis.py +1050 -0
- ml4t/diagnostic/evaluation/binary_metrics.py +910 -0
- ml4t/diagnostic/evaluation/dashboard.py +715 -0
- ml4t/diagnostic/evaluation/diagnostic_plots.py +1037 -0
- ml4t/diagnostic/evaluation/distribution/__init__.py +499 -0
- ml4t/diagnostic/evaluation/distribution/moments.py +299 -0
- ml4t/diagnostic/evaluation/distribution/tails.py +777 -0
- ml4t/diagnostic/evaluation/distribution/tests.py +470 -0
- ml4t/diagnostic/evaluation/drift/__init__.py +139 -0
- ml4t/diagnostic/evaluation/drift/analysis.py +432 -0
- ml4t/diagnostic/evaluation/drift/domain_classifier.py +517 -0
- ml4t/diagnostic/evaluation/drift/population_stability_index.py +310 -0
- ml4t/diagnostic/evaluation/drift/wasserstein.py +388 -0
- ml4t/diagnostic/evaluation/event_analysis.py +647 -0
- ml4t/diagnostic/evaluation/excursion.py +390 -0
- ml4t/diagnostic/evaluation/feature_diagnostics.py +873 -0
- ml4t/diagnostic/evaluation/feature_outcome.py +666 -0
- ml4t/diagnostic/evaluation/framework.py +935 -0
- ml4t/diagnostic/evaluation/metric_registry.py +255 -0
- ml4t/diagnostic/evaluation/metrics/AGENT.md +23 -0
- ml4t/diagnostic/evaluation/metrics/__init__.py +133 -0
- ml4t/diagnostic/evaluation/metrics/basic.py +160 -0
- ml4t/diagnostic/evaluation/metrics/conditional_ic.py +469 -0
- ml4t/diagnostic/evaluation/metrics/feature_outcome.py +475 -0
- ml4t/diagnostic/evaluation/metrics/ic_statistics.py +446 -0
- ml4t/diagnostic/evaluation/metrics/importance_analysis.py +338 -0
- ml4t/diagnostic/evaluation/metrics/importance_classical.py +375 -0
- ml4t/diagnostic/evaluation/metrics/importance_mda.py +371 -0
- ml4t/diagnostic/evaluation/metrics/importance_shap.py +715 -0
- ml4t/diagnostic/evaluation/metrics/information_coefficient.py +527 -0
- ml4t/diagnostic/evaluation/metrics/interactions.py +772 -0
- ml4t/diagnostic/evaluation/metrics/monotonicity.py +226 -0
- ml4t/diagnostic/evaluation/metrics/risk_adjusted.py +324 -0
- ml4t/diagnostic/evaluation/multi_signal.py +550 -0
- ml4t/diagnostic/evaluation/portfolio_analysis/__init__.py +83 -0
- ml4t/diagnostic/evaluation/portfolio_analysis/analysis.py +734 -0
- ml4t/diagnostic/evaluation/portfolio_analysis/metrics.py +589 -0
- ml4t/diagnostic/evaluation/portfolio_analysis/results.py +334 -0
- ml4t/diagnostic/evaluation/report_generation.py +824 -0
- ml4t/diagnostic/evaluation/signal_selector.py +452 -0
- ml4t/diagnostic/evaluation/stat_registry.py +139 -0
- ml4t/diagnostic/evaluation/stationarity/__init__.py +97 -0
- ml4t/diagnostic/evaluation/stationarity/analysis.py +518 -0
- ml4t/diagnostic/evaluation/stationarity/augmented_dickey_fuller.py +296 -0
- ml4t/diagnostic/evaluation/stationarity/kpss_test.py +308 -0
- ml4t/diagnostic/evaluation/stationarity/phillips_perron.py +365 -0
- ml4t/diagnostic/evaluation/stats/AGENT.md +43 -0
- ml4t/diagnostic/evaluation/stats/__init__.py +191 -0
- ml4t/diagnostic/evaluation/stats/backtest_overfitting.py +219 -0
- ml4t/diagnostic/evaluation/stats/bootstrap.py +228 -0
- ml4t/diagnostic/evaluation/stats/deflated_sharpe_ratio.py +591 -0
- ml4t/diagnostic/evaluation/stats/false_discovery_rate.py +295 -0
- ml4t/diagnostic/evaluation/stats/hac_standard_errors.py +108 -0
- ml4t/diagnostic/evaluation/stats/minimum_track_record.py +408 -0
- ml4t/diagnostic/evaluation/stats/moments.py +164 -0
- ml4t/diagnostic/evaluation/stats/rademacher_adjustment.py +436 -0
- ml4t/diagnostic/evaluation/stats/reality_check.py +155 -0
- ml4t/diagnostic/evaluation/stats/sharpe_inference.py +219 -0
- ml4t/diagnostic/evaluation/themes.py +330 -0
- ml4t/diagnostic/evaluation/threshold_analysis.py +957 -0
- ml4t/diagnostic/evaluation/trade_analysis.py +1136 -0
- ml4t/diagnostic/evaluation/trade_dashboard/__init__.py +32 -0
- ml4t/diagnostic/evaluation/trade_dashboard/app.py +315 -0
- ml4t/diagnostic/evaluation/trade_dashboard/export/__init__.py +18 -0
- ml4t/diagnostic/evaluation/trade_dashboard/export/csv.py +82 -0
- ml4t/diagnostic/evaluation/trade_dashboard/export/html.py +276 -0
- ml4t/diagnostic/evaluation/trade_dashboard/io.py +166 -0
- ml4t/diagnostic/evaluation/trade_dashboard/normalize.py +304 -0
- ml4t/diagnostic/evaluation/trade_dashboard/stats.py +386 -0
- ml4t/diagnostic/evaluation/trade_dashboard/style.py +79 -0
- ml4t/diagnostic/evaluation/trade_dashboard/tabs/__init__.py +21 -0
- ml4t/diagnostic/evaluation/trade_dashboard/tabs/patterns.py +354 -0
- ml4t/diagnostic/evaluation/trade_dashboard/tabs/shap_analysis.py +280 -0
- ml4t/diagnostic/evaluation/trade_dashboard/tabs/stat_validation.py +186 -0
- ml4t/diagnostic/evaluation/trade_dashboard/tabs/worst_trades.py +236 -0
- ml4t/diagnostic/evaluation/trade_dashboard/types.py +129 -0
- ml4t/diagnostic/evaluation/trade_shap/__init__.py +102 -0
- ml4t/diagnostic/evaluation/trade_shap/alignment.py +188 -0
- ml4t/diagnostic/evaluation/trade_shap/characterize.py +413 -0
- ml4t/diagnostic/evaluation/trade_shap/cluster.py +302 -0
- ml4t/diagnostic/evaluation/trade_shap/explain.py +208 -0
- ml4t/diagnostic/evaluation/trade_shap/hypotheses/__init__.py +23 -0
- ml4t/diagnostic/evaluation/trade_shap/hypotheses/generator.py +290 -0
- ml4t/diagnostic/evaluation/trade_shap/hypotheses/matcher.py +251 -0
- ml4t/diagnostic/evaluation/trade_shap/hypotheses/templates.yaml +467 -0
- ml4t/diagnostic/evaluation/trade_shap/models.py +386 -0
- ml4t/diagnostic/evaluation/trade_shap/normalize.py +116 -0
- ml4t/diagnostic/evaluation/trade_shap/pipeline.py +263 -0
- ml4t/diagnostic/evaluation/trade_shap_dashboard.py +283 -0
- ml4t/diagnostic/evaluation/trade_shap_diagnostics.py +588 -0
- ml4t/diagnostic/evaluation/validated_cv.py +535 -0
- ml4t/diagnostic/evaluation/visualization.py +1050 -0
- ml4t/diagnostic/evaluation/volatility/__init__.py +45 -0
- ml4t/diagnostic/evaluation/volatility/analysis.py +351 -0
- ml4t/diagnostic/evaluation/volatility/arch.py +258 -0
- ml4t/diagnostic/evaluation/volatility/garch.py +460 -0
- ml4t/diagnostic/integration/__init__.py +48 -0
- ml4t/diagnostic/integration/backtest_contract.py +671 -0
- ml4t/diagnostic/integration/data_contract.py +316 -0
- ml4t/diagnostic/integration/engineer_contract.py +226 -0
- ml4t/diagnostic/logging/__init__.py +77 -0
- ml4t/diagnostic/logging/logger.py +245 -0
- ml4t/diagnostic/logging/performance.py +234 -0
- ml4t/diagnostic/logging/progress.py +234 -0
- ml4t/diagnostic/logging/wandb.py +412 -0
- ml4t/diagnostic/metrics/__init__.py +9 -0
- ml4t/diagnostic/metrics/percentiles.py +128 -0
- ml4t/diagnostic/py.typed +1 -0
- ml4t/diagnostic/reporting/__init__.py +43 -0
- ml4t/diagnostic/reporting/base.py +130 -0
- ml4t/diagnostic/reporting/html_renderer.py +275 -0
- ml4t/diagnostic/reporting/json_renderer.py +51 -0
- ml4t/diagnostic/reporting/markdown_renderer.py +117 -0
- ml4t/diagnostic/results/AGENT.md +24 -0
- ml4t/diagnostic/results/__init__.py +105 -0
- ml4t/diagnostic/results/barrier_results/__init__.py +36 -0
- ml4t/diagnostic/results/barrier_results/hit_rate.py +304 -0
- ml4t/diagnostic/results/barrier_results/precision_recall.py +266 -0
- ml4t/diagnostic/results/barrier_results/profit_factor.py +297 -0
- ml4t/diagnostic/results/barrier_results/tearsheet.py +397 -0
- ml4t/diagnostic/results/barrier_results/time_to_target.py +305 -0
- ml4t/diagnostic/results/barrier_results/validation.py +38 -0
- ml4t/diagnostic/results/base.py +177 -0
- ml4t/diagnostic/results/event_results.py +349 -0
- ml4t/diagnostic/results/feature_results.py +787 -0
- ml4t/diagnostic/results/multi_signal_results.py +431 -0
- ml4t/diagnostic/results/portfolio_results.py +281 -0
- ml4t/diagnostic/results/sharpe_results.py +448 -0
- ml4t/diagnostic/results/signal_results/__init__.py +74 -0
- ml4t/diagnostic/results/signal_results/ic.py +581 -0
- ml4t/diagnostic/results/signal_results/irtc.py +110 -0
- ml4t/diagnostic/results/signal_results/quantile.py +392 -0
- ml4t/diagnostic/results/signal_results/tearsheet.py +456 -0
- ml4t/diagnostic/results/signal_results/turnover.py +213 -0
- ml4t/diagnostic/results/signal_results/validation.py +147 -0
- ml4t/diagnostic/signal/AGENT.md +17 -0
- ml4t/diagnostic/signal/__init__.py +69 -0
- ml4t/diagnostic/signal/_report.py +152 -0
- ml4t/diagnostic/signal/_utils.py +261 -0
- ml4t/diagnostic/signal/core.py +275 -0
- ml4t/diagnostic/signal/quantile.py +148 -0
- ml4t/diagnostic/signal/result.py +214 -0
- ml4t/diagnostic/signal/signal_ic.py +129 -0
- ml4t/diagnostic/signal/turnover.py +182 -0
- ml4t/diagnostic/splitters/AGENT.md +19 -0
- ml4t/diagnostic/splitters/__init__.py +36 -0
- ml4t/diagnostic/splitters/base.py +501 -0
- ml4t/diagnostic/splitters/calendar.py +421 -0
- ml4t/diagnostic/splitters/calendar_config.py +91 -0
- ml4t/diagnostic/splitters/combinatorial.py +1064 -0
- ml4t/diagnostic/splitters/config.py +322 -0
- ml4t/diagnostic/splitters/cpcv/__init__.py +57 -0
- ml4t/diagnostic/splitters/cpcv/combinations.py +119 -0
- ml4t/diagnostic/splitters/cpcv/partitioning.py +263 -0
- ml4t/diagnostic/splitters/cpcv/purge_engine.py +379 -0
- ml4t/diagnostic/splitters/cpcv/windows.py +190 -0
- ml4t/diagnostic/splitters/group_isolation.py +329 -0
- ml4t/diagnostic/splitters/persistence.py +316 -0
- ml4t/diagnostic/splitters/utils.py +207 -0
- ml4t/diagnostic/splitters/walk_forward.py +757 -0
- ml4t/diagnostic/utils/__init__.py +42 -0
- ml4t/diagnostic/utils/config.py +542 -0
- ml4t/diagnostic/utils/dependencies.py +318 -0
- ml4t/diagnostic/utils/sessions.py +127 -0
- ml4t/diagnostic/validation/__init__.py +54 -0
- ml4t/diagnostic/validation/dataframe.py +274 -0
- ml4t/diagnostic/validation/returns.py +280 -0
- ml4t/diagnostic/validation/timeseries.py +299 -0
- ml4t/diagnostic/visualization/AGENT.md +19 -0
- ml4t/diagnostic/visualization/__init__.py +223 -0
- ml4t/diagnostic/visualization/backtest/__init__.py +98 -0
- ml4t/diagnostic/visualization/backtest/cost_attribution.py +762 -0
- ml4t/diagnostic/visualization/backtest/executive_summary.py +895 -0
- ml4t/diagnostic/visualization/backtest/interactive_controls.py +673 -0
- ml4t/diagnostic/visualization/backtest/statistical_validity.py +874 -0
- ml4t/diagnostic/visualization/backtest/tearsheet.py +565 -0
- ml4t/diagnostic/visualization/backtest/template_system.py +373 -0
- ml4t/diagnostic/visualization/backtest/trade_plots.py +1172 -0
- ml4t/diagnostic/visualization/barrier_plots.py +782 -0
- ml4t/diagnostic/visualization/core.py +1060 -0
- ml4t/diagnostic/visualization/dashboards/__init__.py +36 -0
- ml4t/diagnostic/visualization/dashboards/base.py +582 -0
- ml4t/diagnostic/visualization/dashboards/importance.py +801 -0
- ml4t/diagnostic/visualization/dashboards/interaction.py +263 -0
- ml4t/diagnostic/visualization/dashboards.py +43 -0
- ml4t/diagnostic/visualization/data_extraction/__init__.py +48 -0
- ml4t/diagnostic/visualization/data_extraction/importance.py +649 -0
- ml4t/diagnostic/visualization/data_extraction/interaction.py +504 -0
- ml4t/diagnostic/visualization/data_extraction/types.py +113 -0
- ml4t/diagnostic/visualization/data_extraction/validation.py +66 -0
- ml4t/diagnostic/visualization/feature_plots.py +888 -0
- ml4t/diagnostic/visualization/interaction_plots.py +618 -0
- ml4t/diagnostic/visualization/portfolio/__init__.py +41 -0
- ml4t/diagnostic/visualization/portfolio/dashboard.py +514 -0
- ml4t/diagnostic/visualization/portfolio/drawdown_plots.py +341 -0
- ml4t/diagnostic/visualization/portfolio/returns_plots.py +487 -0
- ml4t/diagnostic/visualization/portfolio/risk_plots.py +301 -0
- ml4t/diagnostic/visualization/report_generation.py +1343 -0
- ml4t/diagnostic/visualization/signal/__init__.py +103 -0
- ml4t/diagnostic/visualization/signal/dashboard.py +911 -0
- ml4t/diagnostic/visualization/signal/event_plots.py +514 -0
- ml4t/diagnostic/visualization/signal/ic_plots.py +635 -0
- ml4t/diagnostic/visualization/signal/multi_signal_dashboard.py +974 -0
- ml4t/diagnostic/visualization/signal/multi_signal_plots.py +603 -0
- ml4t/diagnostic/visualization/signal/quantile_plots.py +625 -0
- ml4t/diagnostic/visualization/signal/turnover_plots.py +400 -0
- ml4t/diagnostic/visualization/trade_shap/__init__.py +90 -0
- ml4t_diagnostic-0.1.0a1.dist-info/METADATA +1044 -0
- ml4t_diagnostic-0.1.0a1.dist-info/RECORD +242 -0
- ml4t_diagnostic-0.1.0a1.dist-info/WHEEL +4 -0
- 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
|
+
]
|