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,527 @@
1
+ """Core Information Coefficient (IC) metrics.
2
+
3
+ This module provides the fundamental IC calculations used for evaluating
4
+ feature predictiveness.
5
+ """
6
+
7
+ from typing import TYPE_CHECKING, Any, Union, cast
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+ import polars as pl
12
+ from scipy import stats
13
+ from scipy.stats import spearmanr
14
+
15
+ from ml4t.diagnostic.backends.adapter import DataFrameAdapter
16
+ from ml4t.diagnostic.evaluation.metrics.basic import compute_forward_returns
17
+
18
+ if TYPE_CHECKING:
19
+ from numpy.typing import NDArray
20
+
21
+
22
+ def information_coefficient(
23
+ predictions: Union[pl.Series, pd.Series, "NDArray[Any]"],
24
+ returns: Union[pl.Series, pd.Series, "NDArray[Any]"],
25
+ method: str = "spearman",
26
+ confidence_intervals: bool = False,
27
+ alpha: float = 0.05,
28
+ ) -> float | dict[str, float]:
29
+ """Calculate Information Coefficient between predictions and returns.
30
+
31
+ The Information Coefficient measures the linear relationship between model
32
+ predictions and subsequent returns. Spearman correlation is preferred as it's
33
+ robust to outliers and non-linear relationships.
34
+
35
+ Parameters
36
+ ----------
37
+ predictions : Union[pl.Series, pd.Series, np.ndarray]
38
+ Model predictions or scores
39
+ returns : Union[pl.Series, pd.Series, np.ndarray]
40
+ Forward returns corresponding to predictions
41
+ method : str, default "spearman"
42
+ Correlation method: "spearman" or "pearson"
43
+ confidence_intervals : bool, default False
44
+ Whether to return confidence intervals
45
+ alpha : float, default 0.05
46
+ Significance level for confidence intervals
47
+
48
+ Returns
49
+ -------
50
+ Union[float, dict]
51
+ If confidence_intervals=False: IC value
52
+ If confidence_intervals=True: dict with 'ic', 'lower_ci', 'upper_ci', 'p_value'
53
+
54
+ Examples
55
+ --------
56
+ >>> predictions = np.array([0.1, 0.3, -0.2, 0.5])
57
+ >>> returns = np.array([0.02, 0.05, -0.01, 0.08])
58
+ >>> ic = information_coefficient(predictions, returns)
59
+ >>> print(f"IC: {ic:.3f}")
60
+ IC: 0.800
61
+
62
+ >>> # With confidence intervals
63
+ >>> result = information_coefficient(predictions, returns, confidence_intervals=True)
64
+ >>> print(f"IC: {result['ic']:.3f} [{result['lower_ci']:.3f}, {result['upper_ci']:.3f}]")
65
+ IC: 0.800 [-0.602, 0.995]
66
+ """
67
+ # Convert inputs to numpy for consistent handling
68
+ pred_array = DataFrameAdapter.to_numpy(predictions).flatten()
69
+ ret_array = DataFrameAdapter.to_numpy(returns).flatten()
70
+
71
+ # Validate inputs
72
+ if len(pred_array) != len(ret_array):
73
+ raise ValueError("Predictions and returns must have the same length")
74
+
75
+ if len(pred_array) < 2:
76
+ if confidence_intervals:
77
+ return {
78
+ "ic": np.nan,
79
+ "lower_ci": np.nan,
80
+ "upper_ci": np.nan,
81
+ "p_value": np.nan,
82
+ }
83
+ return np.nan
84
+
85
+ # Remove NaN pairs
86
+ valid_mask = ~(np.isnan(pred_array) | np.isnan(ret_array))
87
+ pred_clean = pred_array[valid_mask]
88
+ ret_clean = ret_array[valid_mask]
89
+
90
+ if len(pred_clean) < 2:
91
+ if confidence_intervals:
92
+ return {
93
+ "ic": np.nan,
94
+ "lower_ci": np.nan,
95
+ "upper_ci": np.nan,
96
+ "p_value": np.nan,
97
+ }
98
+ return np.nan
99
+
100
+ # Calculate correlation
101
+ if method == "spearman":
102
+ ic_value, p_value = spearmanr(pred_clean, ret_clean)
103
+ elif method == "pearson":
104
+ ic_value, p_value = stats.pearsonr(pred_clean, ret_clean)
105
+ else:
106
+ raise ValueError(f"Unknown correlation method: {method}")
107
+
108
+ # Handle edge cases
109
+ if np.isnan(ic_value):
110
+ if confidence_intervals:
111
+ return {
112
+ "ic": np.nan,
113
+ "lower_ci": np.nan,
114
+ "upper_ci": np.nan,
115
+ "p_value": np.nan,
116
+ }
117
+ return np.nan
118
+
119
+ # Return simple IC if no confidence intervals requested
120
+ if not confidence_intervals:
121
+ return float(ic_value)
122
+
123
+ # Calculate confidence intervals using Fisher transformation
124
+ n = len(pred_clean)
125
+ if n < 4: # Need sufficient data for meaningful CI
126
+ return {
127
+ "ic": float(ic_value),
128
+ "lower_ci": np.nan,
129
+ "upper_ci": np.nan,
130
+ "p_value": float(p_value) if not np.isnan(p_value) else np.nan,
131
+ }
132
+
133
+ # Fisher transformation for correlation confidence intervals
134
+ z = np.arctanh(ic_value) # Fisher z-transform
135
+ se = 1 / np.sqrt(n - 3) # Standard error
136
+ z_critical = stats.norm.ppf(1 - alpha / 2)
137
+
138
+ # Transform back to correlation scale
139
+ lower_z = z - z_critical * se
140
+ upper_z = z + z_critical * se
141
+ lower_ci = np.tanh(lower_z)
142
+ upper_ci = np.tanh(upper_z)
143
+
144
+ return {
145
+ "ic": float(ic_value),
146
+ "lower_ci": float(lower_ci),
147
+ "upper_ci": float(upper_ci),
148
+ "p_value": float(p_value) if not np.isnan(p_value) else np.nan,
149
+ }
150
+
151
+
152
+ def compute_ic_series(
153
+ predictions: pl.DataFrame | pd.DataFrame,
154
+ returns: pl.DataFrame | pd.DataFrame,
155
+ pred_col: str = "prediction",
156
+ ret_col: str = "forward_return",
157
+ date_col: str = "date",
158
+ method: str = "spearman",
159
+ min_periods: int = 10,
160
+ ) -> pl.DataFrame | pd.DataFrame:
161
+ """Compute IC time series for temporal analysis (Alphalens-style).
162
+
163
+ This function computes the Information Coefficient for each time period
164
+ (typically daily), enabling temporal analysis of prediction quality.
165
+ This is THE fundamental visualization in Alphalens.
166
+
167
+ Parameters
168
+ ----------
169
+ predictions : Union[pl.DataFrame, pd.DataFrame]
170
+ DataFrame with predictions, indexed or with date column
171
+ returns : Union[pl.DataFrame, pd.DataFrame]
172
+ DataFrame with forward returns, matching predictions structure
173
+ pred_col : str, default "prediction"
174
+ Column name for predictions/features
175
+ ret_col : str, default "forward_return"
176
+ Column name for forward returns
177
+ date_col : str, default "date"
178
+ Column name for dates (for grouping by period)
179
+ method : str, default "spearman"
180
+ Correlation method: "spearman" or "pearson"
181
+ min_periods : int, default 10
182
+ Minimum observations per period for valid IC calculation
183
+
184
+ Returns
185
+ -------
186
+ Union[pl.DataFrame, pd.DataFrame]
187
+ Time series of IC values with columns: [date_col, 'ic', 'n_obs']
188
+
189
+ Examples
190
+ --------
191
+ >>> # Create sample data
192
+ >>> dates = pd.date_range("2024-01-01", periods=100)
193
+ >>> pred_df = pd.DataFrame({
194
+ ... "date": dates,
195
+ ... "prediction": np.random.randn(100)
196
+ ... })
197
+ >>> ret_df = pd.DataFrame({
198
+ ... "date": dates,
199
+ ... "forward_return": np.random.randn(100) * 0.02
200
+ ... })
201
+ >>> ic_series = compute_ic_series(pred_df, ret_df)
202
+ >>> print(ic_series.head())
203
+ """
204
+ is_polars = isinstance(predictions, pl.DataFrame)
205
+
206
+ if is_polars:
207
+ # Merge predictions and returns
208
+ predictions_pl = cast(pl.DataFrame, predictions)
209
+ returns_pl = cast(pl.DataFrame, returns)
210
+ df = predictions_pl.join(returns_pl, on=date_col, how="inner")
211
+
212
+ # Use group_by().map_groups() for efficient per-group processing
213
+ def compute_group_ic(group: pl.DataFrame) -> pl.DataFrame:
214
+ """Compute IC for a single date group."""
215
+ pred_array = group[pred_col].to_numpy()
216
+ ret_array = group[ret_col].to_numpy()
217
+
218
+ # Remove NaN pairs
219
+ valid_mask = ~(np.isnan(pred_array) | np.isnan(ret_array))
220
+ pred_clean = pred_array[valid_mask]
221
+ ret_clean = ret_array[valid_mask]
222
+
223
+ n_obs = len(pred_clean)
224
+
225
+ if n_obs >= min_periods:
226
+ ic_val = information_coefficient(
227
+ pred_clean, ret_clean, method=method, confidence_intervals=False
228
+ )
229
+ else:
230
+ ic_val = np.nan
231
+
232
+ return pl.DataFrame({date_col: [group[date_col][0]], "ic": [ic_val], "n_obs": [n_obs]})
233
+
234
+ return df.group_by(date_col).map_groups(compute_group_ic).sort(date_col)
235
+
236
+ # pandas - use different variable name to avoid type conflict
237
+ # Merge predictions and returns
238
+ predictions_pd = cast(pd.DataFrame, predictions)
239
+ returns_pd = cast(pd.DataFrame, returns)
240
+ df_pd = pd.merge(predictions_pd, returns_pd, on=date_col, how="inner")
241
+
242
+ # Group by date and compute IC
243
+ def compute_period_ic(group: pd.DataFrame) -> pd.Series:
244
+ # Explicitly convert to ndarray to handle ExtensionArray types
245
+ pred_array = np.asarray(group[pred_col].values, dtype=np.float64)
246
+ ret_array = np.asarray(group[ret_col].values, dtype=np.float64)
247
+
248
+ # Remove NaN pairs
249
+ valid_mask = ~(np.isnan(pred_array) | np.isnan(ret_array))
250
+ pred_clean = pred_array[valid_mask]
251
+ ret_clean = ret_array[valid_mask]
252
+
253
+ n_obs = len(pred_clean)
254
+
255
+ if n_obs >= min_periods:
256
+ ic_val = information_coefficient(
257
+ pred_clean, ret_clean, method=method, confidence_intervals=False
258
+ )
259
+ else:
260
+ ic_val = np.nan
261
+
262
+ return pd.Series({"ic": ic_val, "n_obs": n_obs})
263
+
264
+ ic_series = df_pd.groupby(date_col, group_keys=False).apply(compute_period_ic).reset_index()
265
+
266
+ return ic_series
267
+
268
+
269
+ def compute_ic_by_horizon(
270
+ predictions: pl.DataFrame | pd.DataFrame,
271
+ prices: pl.DataFrame | pd.DataFrame,
272
+ horizons: list[int] | None = None,
273
+ pred_col: str = "prediction",
274
+ price_col: str = "close",
275
+ date_col: str = "date",
276
+ group_col: str | None = None,
277
+ method: str = "spearman",
278
+ ) -> dict[int, float]:
279
+ """Compute IC across multiple forward return horizons.
280
+
281
+ This function computes IC for different forward-looking periods
282
+ (e.g., 1-day, 5-day, 21-day), which is essential for understanding
283
+ prediction persistence and optimal holding periods.
284
+
285
+ Parameters
286
+ ----------
287
+ predictions : Union[pl.DataFrame, pd.DataFrame]
288
+ DataFrame with predictions
289
+ prices : Union[pl.DataFrame, pd.DataFrame]
290
+ DataFrame with prices to compute forward returns
291
+ horizons : list[int], default [1, 5, 21]
292
+ Forward periods to analyze (in days/bars)
293
+ pred_col : str, default "prediction"
294
+ Column name for predictions
295
+ price_col : str, default "close"
296
+ Column name for prices
297
+ date_col : str, default "date"
298
+ Column name for dates
299
+ group_col : str | None, default None
300
+ Column for grouping (e.g., 'symbol')
301
+ method : str, default "spearman"
302
+ Correlation method
303
+
304
+ Returns
305
+ -------
306
+ dict[int, float | dict]
307
+ Dictionary mapping horizon -> IC value
308
+ Keys are horizon periods, values are IC (or dict with CI if requested)
309
+
310
+ Examples
311
+ --------
312
+ >>> pred_df = pd.DataFrame({"date": dates, "prediction": preds})
313
+ >>> price_df = pd.DataFrame({"date": dates, "close": prices})
314
+ >>> ic_by_horizon = compute_ic_by_horizon(
315
+ ... pred_df, price_df, horizons=[1, 5, 21]
316
+ ... )
317
+ >>> print(f"1-day IC: {ic_by_horizon[1]:.3f}")
318
+ >>> print(f"5-day IC: {ic_by_horizon[5]:.3f}")
319
+ """
320
+ # Compute forward returns for all horizons
321
+ if horizons is None:
322
+ horizons = [1, 5, 21]
323
+ prices_with_fwd = compute_forward_returns(
324
+ prices, periods=horizons, price_col=price_col, group_col=group_col
325
+ )
326
+
327
+ # Merge with predictions - declare type before branching
328
+ df: pl.DataFrame | pd.DataFrame
329
+
330
+ if isinstance(predictions, pl.DataFrame):
331
+ # Type is narrowed by isinstance check, but prices_with_fwd needs cast
332
+ prices_with_fwd_pl = cast(pl.DataFrame, prices_with_fwd)
333
+ df = predictions.join(prices_with_fwd_pl, on=date_col, how="inner")
334
+ elif isinstance(predictions, pd.DataFrame):
335
+ prices_with_fwd_pd = cast(pd.DataFrame, prices_with_fwd)
336
+ df = pd.merge(predictions, prices_with_fwd_pd, on=date_col, how="inner")
337
+ else:
338
+ raise TypeError(
339
+ f"predictions must be pl.DataFrame or pd.DataFrame, got {type(predictions)}"
340
+ )
341
+
342
+ # Compute IC for each horizon
343
+ ic_results: dict[int, float] = {}
344
+
345
+ for horizon in horizons:
346
+ ret_col = f"fwd_ret_{horizon}"
347
+
348
+ # Extract arrays - df type is known from construction above
349
+ if isinstance(df, pl.DataFrame):
350
+ pred_array = df[pred_col].to_numpy()
351
+ ret_array = df[ret_col].to_numpy()
352
+ else:
353
+ pred_array = df[pred_col].to_numpy()
354
+ ret_array = df[ret_col].to_numpy()
355
+
356
+ # Compute IC (confidence_intervals=False returns float)
357
+ ic_result = information_coefficient(
358
+ pred_array, ret_array, method=method, confidence_intervals=False
359
+ )
360
+ # When confidence_intervals=False, returns float; otherwise dict
361
+ if isinstance(ic_result, dict):
362
+ ic_val = float(ic_result.get("ic", np.nan))
363
+ else:
364
+ ic_val = float(ic_result)
365
+
366
+ ic_results[horizon] = ic_val
367
+
368
+ return ic_results
369
+
370
+
371
+ def compute_ic_ir(
372
+ ic_series: Union[pl.DataFrame, pd.DataFrame, "NDArray[Any]"],
373
+ ic_col: str = "ic",
374
+ annualization_factor: float = np.sqrt(252),
375
+ confidence_intervals: bool = False,
376
+ n_bootstrap: int = 10000,
377
+ alpha: float = 0.05,
378
+ ) -> float | dict[str, float]:
379
+ """Compute IC Information Ratio (IC-IR) - risk-adjusted IC metric.
380
+
381
+ IC-IR is analogous to the Sharpe ratio but for IC instead of returns.
382
+ It measures the consistency of predictive power by computing mean IC
383
+ divided by the standard deviation of IC.
384
+
385
+ Higher IC-IR indicates more consistent predictions. IC-IR > 0.5 is
386
+ generally considered good, IC-IR > 1.0 is excellent.
387
+
388
+ Parameters
389
+ ----------
390
+ ic_series : Union[pl.DataFrame, pd.DataFrame, np.ndarray]
391
+ Time series of IC values (from compute_ic_series)
392
+ ic_col : str, default "ic"
393
+ Column name for IC values (if DataFrame)
394
+ annualization_factor : float, default sqrt(252)
395
+ Factor to annualize IC-IR (sqrt(periods_per_year))
396
+ - Daily: sqrt(252) ~ 15.87
397
+ - Weekly: sqrt(52) ~ 7.21
398
+ - Monthly: sqrt(12) ~ 3.46
399
+ confidence_intervals : bool, default False
400
+ Whether to compute bootstrap confidence intervals
401
+ n_bootstrap : int, default 10000
402
+ Number of bootstrap samples for CI computation
403
+ alpha : float, default 0.05
404
+ Significance level for confidence intervals (95% CI)
405
+
406
+ Returns
407
+ -------
408
+ Union[float, dict]
409
+ If confidence_intervals=False: IC-IR value
410
+ If confidence_intervals=True: dict with 'ic_ir', 'lower_ci', 'upper_ci'
411
+
412
+ Examples
413
+ --------
414
+ >>> # Compute IC series first
415
+ >>> ic_series = compute_ic_series(pred_df, ret_df)
416
+ >>>
417
+ >>> # Compute IC-IR
418
+ >>> ic_ir = compute_ic_ir(ic_series)
419
+ >>> print(f"IC-IR: {ic_ir:.3f}")
420
+ IC-IR: 0.645
421
+ >>>
422
+ >>> # With confidence intervals
423
+ >>> result = compute_ic_ir(ic_series, confidence_intervals=True)
424
+ >>> print(f"IC-IR: {result['ic_ir']:.3f} [{result['lower_ci']:.3f}, {result['upper_ci']:.3f}]")
425
+ IC-IR: 0.645 [0.412, 0.891]
426
+
427
+ Notes
428
+ -----
429
+ IC-IR Interpretation:
430
+ - IC-IR < 0.3: Weak/inconsistent predictive power
431
+ - IC-IR 0.3-0.5: Moderate consistency
432
+ - IC-IR 0.5-1.0: Good consistency
433
+ - IC-IR > 1.0: Excellent consistency
434
+
435
+ The annualization factor adjusts IC-IR to an annual scale for easier
436
+ interpretation and comparison across different rebalancing frequencies.
437
+ """
438
+ # Extract IC values
439
+ ic_values: NDArray[Any]
440
+ if isinstance(ic_series, pl.DataFrame | pd.DataFrame):
441
+ is_polars = isinstance(ic_series, pl.DataFrame)
442
+ if is_polars:
443
+ ic_values = cast(pl.DataFrame, ic_series)[ic_col].to_numpy()
444
+ else:
445
+ ic_values = cast(pd.DataFrame, ic_series)[ic_col].to_numpy()
446
+ else:
447
+ ic_values = np.asarray(ic_series).flatten()
448
+
449
+ # Remove NaN values
450
+ ic_clean: NDArray[Any] = ic_values[~np.isnan(ic_values)]
451
+
452
+ # Validate sufficient data
453
+ if len(ic_clean) < 2:
454
+ if confidence_intervals:
455
+ return {
456
+ "ic_ir": np.nan,
457
+ "lower_ci": np.nan,
458
+ "upper_ci": np.nan,
459
+ "mean_ic": np.nan,
460
+ "std_ic": np.nan,
461
+ "n_periods": len(ic_clean),
462
+ }
463
+ return np.nan
464
+
465
+ # Compute IC-IR
466
+ mean_ic = float(np.mean(ic_clean))
467
+ std_ic = float(np.std(ic_clean, ddof=1)) # Sample std
468
+
469
+ if std_ic == 0:
470
+ # Perfect consistency (all IC values identical)
471
+ ic_ir = np.inf if mean_ic > 0 else -np.inf if mean_ic < 0 else np.nan
472
+ else:
473
+ ic_ir = (mean_ic / std_ic) * annualization_factor
474
+
475
+ # Return simple IC-IR if no CI requested
476
+ if not confidence_intervals:
477
+ return float(ic_ir)
478
+
479
+ # Bootstrap confidence intervals
480
+ if len(ic_clean) < 10:
481
+ # Insufficient data for meaningful bootstrap
482
+ return {
483
+ "ic_ir": float(ic_ir),
484
+ "lower_ci": np.nan,
485
+ "upper_ci": np.nan,
486
+ "mean_ic": float(mean_ic),
487
+ "std_ic": float(std_ic),
488
+ "n_periods": len(ic_clean),
489
+ }
490
+
491
+ # Perform bootstrap
492
+ rng = np.random.RandomState(42) # For reproducibility
493
+ bootstrap_ics = []
494
+
495
+ for _ in range(n_bootstrap):
496
+ # Resample with replacement
497
+ sample = rng.choice(ic_clean, size=len(ic_clean), replace=True)
498
+ sample_mean = np.mean(sample)
499
+ sample_std = np.std(sample, ddof=1)
500
+
501
+ if sample_std > 0:
502
+ bootstrap_ic_ir = (sample_mean / sample_std) * annualization_factor
503
+ bootstrap_ics.append(bootstrap_ic_ir)
504
+
505
+ if len(bootstrap_ics) == 0:
506
+ # Bootstrap failed (all samples had zero std)
507
+ return {
508
+ "ic_ir": float(ic_ir),
509
+ "lower_ci": np.nan,
510
+ "upper_ci": np.nan,
511
+ "mean_ic": float(mean_ic),
512
+ "std_ic": float(std_ic),
513
+ "n_periods": len(ic_clean),
514
+ }
515
+
516
+ # Compute percentile confidence intervals
517
+ lower_ci = np.percentile(bootstrap_ics, (alpha / 2) * 100)
518
+ upper_ci = np.percentile(bootstrap_ics, (1 - alpha / 2) * 100)
519
+
520
+ return {
521
+ "ic_ir": float(ic_ir),
522
+ "lower_ci": float(lower_ci),
523
+ "upper_ci": float(upper_ci),
524
+ "mean_ic": float(mean_ic),
525
+ "std_ic": float(std_ic),
526
+ "n_periods": len(ic_clean),
527
+ }