boruta-quant 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ """
2
+ boruta-quant: Temporal-aware Boruta feature selection for quantitative finance.
3
+
4
+ OOS-only importance computation with purged cross-validation,
5
+ for financial time series where temporal integrity is critical.
6
+
7
+ Example:
8
+ >>> from boruta_quant import BorutaSelector, BorutaSelectorConfig
9
+ >>> from boruta_quant.oracle import PermutationImportanceOracle
10
+ >>> from boruta_quant.temporal import PurgedTemporalCV, PurgedCVConfig
11
+ >>>
12
+ >>> selector = BorutaSelector(
13
+ ... config=BorutaSelectorConfig(n_trials=20, percentile=100, alpha=0.05, two_step=True, random_state=42),
14
+ ... oracle=PermutationImportanceOracle(scoring="neg_mean_squared_error"),
15
+ ... cv=PurgedTemporalCV(PurgedCVConfig(n_splits=5, purge_window_days=5, embargo_window_days=5, min_train_size=100, test_size_ratio=0.2)),
16
+ ... )
17
+ >>> result = selector.fit(X, y, timestamps, model)
18
+ >>> print(result.accepted_features)
19
+ """
20
+
21
+ from boruta_quant._version import __version__
22
+ from boruta_quant.selector import BorutaResult, BorutaSelector, BorutaSelectorConfig
23
+
24
+ __all__ = [
25
+ "__version__",
26
+ "BorutaSelector",
27
+ "BorutaSelectorConfig",
28
+ "BorutaResult",
29
+ ]
@@ -0,0 +1,3 @@
1
+ """Version information for boruta-quant."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,31 @@
1
+ """
2
+ Scoring metrics for boruta-quant.
3
+
4
+ Available Metrics:
5
+ - rank_ic / rank_ic_scorer: Spearman correlation (rank information coefficient).
6
+ - directional_accuracy / directional_accuracy_scorer: Fraction of correct sign predictions.
7
+ - auc_score / auc_scorer: Area under ROC curve.
8
+
9
+ Example:
10
+ >>> from boruta_quant.metrics import rank_ic_scorer, directional_accuracy_scorer
11
+ >>> from boruta_quant.oracle import PermutationImportanceOracle
12
+ >>>
13
+ >>> oracle = PermutationImportanceOracle(scoring=rank_ic_scorer)
14
+ """
15
+
16
+ from boruta_quant.metrics.auc import auc_score, auc_scorer, auc_scorer_sklearn
17
+ from boruta_quant.metrics.directional_accuracy import (
18
+ directional_accuracy,
19
+ directional_accuracy_scorer,
20
+ )
21
+ from boruta_quant.metrics.rank_ic import rank_ic, rank_ic_scorer
22
+
23
+ __all__ = [
24
+ "rank_ic",
25
+ "rank_ic_scorer",
26
+ "directional_accuracy",
27
+ "directional_accuracy_scorer",
28
+ "auc_score",
29
+ "auc_scorer",
30
+ "auc_scorer_sklearn",
31
+ ]
@@ -0,0 +1,83 @@
1
+ """
2
+ AUC-ROC scorer for classification-based feature selection.
3
+
4
+ Use cases:
5
+ - Direction prediction (up/down)
6
+ - Regime classification
7
+ - Binary signal classification
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ import numpy as np
15
+ import numpy.typing as npt
16
+ from beartype import beartype
17
+ from sklearn.metrics import make_scorer, roc_auc_score
18
+
19
+
20
+ @beartype
21
+ def auc_score(
22
+ y_true: npt.NDArray[Any],
23
+ y_pred_proba: npt.NDArray[Any],
24
+ ) -> float:
25
+ """
26
+ Compute AUC-ROC score for binary classification.
27
+
28
+ Args:
29
+ y_true: True binary labels (0 or 1)
30
+ y_pred_proba: Predicted probabilities for positive class
31
+
32
+ Returns:
33
+ AUC-ROC score in [0, 1]. 0.5 = random, 1.0 = perfect.
34
+
35
+ Raises:
36
+ AssertionError: If inputs have wrong shape or values.
37
+ """
38
+ assert len(y_true) == len(y_pred_proba), "Length mismatch"
39
+ assert len(y_true) >= 2, "Need at least 2 samples"
40
+
41
+ # Handle edge case: single class in y_true
42
+ unique_classes = np.unique(y_true)
43
+ if len(unique_classes) == 1:
44
+ return 0.5 # Undefined AUC, return random baseline
45
+
46
+ return float(roc_auc_score(y_true, y_pred_proba))
47
+
48
+
49
+ @beartype
50
+ def auc_scorer(
51
+ estimator: Any,
52
+ X: npt.NDArray[Any],
53
+ y_true: npt.NDArray[Any],
54
+ ) -> float:
55
+ """
56
+ sklearn-compatible AUC scorer for use with permutation_importance.
57
+
58
+ Args:
59
+ estimator: Fitted classifier with predict_proba method
60
+ X: Feature matrix
61
+ y_true: True binary labels
62
+
63
+ Returns:
64
+ AUC-ROC score
65
+ """
66
+ assert hasattr(estimator, "predict_proba"), "Estimator must have predict_proba"
67
+
68
+ y_pred_proba = estimator.predict_proba(X)[:, 1]
69
+ return auc_score(y_true, y_pred_proba)
70
+
71
+
72
+ # sklearn make_scorer compatible version
73
+ auc_scorer_sklearn = make_scorer(roc_auc_score, needs_proba=True, response_method="predict_proba")
74
+ """
75
+ sklearn-compatible scorer for AUC-ROC.
76
+
77
+ Usage with PermutationImportanceOracle:
78
+ >>> oracle = PermutationImportanceOracle(scoring=auc_scorer_sklearn)
79
+
80
+ Usage with sklearn cross_val_score:
81
+ >>> from sklearn.model_selection import cross_val_score
82
+ >>> scores = cross_val_score(model, X, y, cv=cv, scoring=auc_scorer_sklearn)
83
+ """
@@ -0,0 +1,67 @@
1
+ """
2
+ Directional Accuracy scoring metric.
3
+
4
+ Measures what fraction of predictions got the sign right (up vs down).
5
+ The most intuitive alpha signal metric — "did you predict the right direction?"
6
+
7
+ Why Directional Accuracy?
8
+ - Every PM understands it immediately
9
+ - Works with continuous return predictions
10
+ - Complementary to Rank IC (IC measures rank order, DA measures sign)
11
+ - Baseline is 50% (random guessing)
12
+ """
13
+
14
+ from typing import Any
15
+
16
+ import numpy as np
17
+ import numpy.typing as npt
18
+ from beartype import beartype
19
+ from sklearn.metrics import make_scorer
20
+
21
+
22
+ @beartype
23
+ def directional_accuracy(
24
+ y_true: npt.NDArray[Any],
25
+ y_pred: npt.NDArray[Any],
26
+ ) -> float:
27
+ """
28
+ Compute directional accuracy (fraction of correct sign predictions).
29
+
30
+ Args:
31
+ y_true: Actual values (e.g., forward returns).
32
+ y_pred: Predicted values (e.g., alpha signal).
33
+
34
+ Returns:
35
+ Fraction of correct sign predictions in [0.0, 1.0].
36
+ 0.5 = random, 1.0 = perfect direction.
37
+
38
+ Raises:
39
+ AssertionError: If inputs are empty or mismatched length.
40
+
41
+ Example:
42
+ >>> y_true = np.array([0.01, -0.02, 0.03, -0.01, 0.02])
43
+ >>> y_pred = np.array([0.5, -0.3, 0.8, 0.2, 0.4])
44
+ >>> da = directional_accuracy(y_true, y_pred)
45
+ >>> print(f"DA: {da:.2%}") # 80.00% (4/5 correct)
46
+ """
47
+ assert len(y_true) == len(y_pred), (
48
+ f"Length mismatch: y_true={len(y_true)}, y_pred={len(y_pred)}"
49
+ )
50
+ assert len(y_true) > 0, "Need at least 1 sample"
51
+
52
+ true_signs = np.sign(y_true)
53
+ pred_signs = np.sign(y_pred)
54
+
55
+ # Zero returns/predictions are ambiguous — count as correct only if both zero
56
+ correct = true_signs == pred_signs
57
+ return float(np.mean(correct))
58
+
59
+
60
+ # sklearn-compatible scorer for use with permutation_importance
61
+ directional_accuracy_scorer = make_scorer(directional_accuracy, greater_is_better=True)
62
+ """
63
+ sklearn-compatible scorer for Directional Accuracy.
64
+
65
+ Usage with PermutationImportanceOracle:
66
+ >>> oracle = PermutationImportanceOracle(scoring=directional_accuracy_scorer)
67
+ """
@@ -0,0 +1,72 @@
1
+ """
2
+ Rank Information Coefficient (IC) scoring metric.
3
+
4
+ The Rank IC is the standard metric in quantitative finance for evaluating
5
+ alpha signals. It measures the Spearman correlation between predicted
6
+ values and actual forward returns.
7
+
8
+ Why Rank IC?
9
+ - Robust to outliers (uses ranks, not raw values)
10
+ - Interpretable: -1 to +1 scale
11
+ - Industry standard for alpha research
12
+ - Works with continuous predictions
13
+ """
14
+
15
+ from typing import Any
16
+
17
+ import numpy as np
18
+ import numpy.typing as npt
19
+ from beartype import beartype
20
+ from scipy.stats import spearmanr
21
+ from sklearn.metrics import make_scorer
22
+
23
+
24
+ @beartype
25
+ def rank_ic(y_true: npt.NDArray[Any], y_pred: npt.NDArray[Any]) -> float:
26
+ """
27
+ Compute Rank Information Coefficient (Spearman correlation).
28
+
29
+ Args:
30
+ y_true: Actual values (e.g., forward returns).
31
+ y_pred: Predicted values (e.g., alpha signal).
32
+
33
+ Returns:
34
+ Spearman correlation coefficient in [-1, 1].
35
+ Returns 0.0 if correlation cannot be computed (e.g., constant input).
36
+
37
+ Example:
38
+ >>> y_true = np.array([0.01, -0.02, 0.03, -0.01, 0.02])
39
+ >>> y_pred = np.array([0.5, -0.3, 0.8, -0.2, 0.4])
40
+ >>> ic = rank_ic(y_true, y_pred)
41
+ >>> print(f"IC: {ic:.4f}")
42
+ """
43
+ if len(y_true) < 2:
44
+ return 0.0
45
+
46
+ # Handle constant arrays
47
+ if np.std(y_true) == 0 or np.std(y_pred) == 0:
48
+ return 0.0
49
+
50
+ result = spearmanr(y_true, y_pred)
51
+ # Use index access for type safety (result is SignificanceResult namedtuple)
52
+ correlation: float = float(result[0]) # type: ignore[arg-type]
53
+
54
+ # Handle NaN (can occur with insufficient variance)
55
+ if np.isnan(correlation):
56
+ return 0.0
57
+
58
+ return correlation
59
+
60
+
61
+ # sklearn-compatible scorer for use with permutation_importance
62
+ rank_ic_scorer = make_scorer(rank_ic, greater_is_better=True)
63
+ """
64
+ sklearn-compatible scorer for Rank IC.
65
+
66
+ Usage with PermutationImportanceOracle:
67
+ >>> oracle = PermutationImportanceOracle(scoring=rank_ic_scorer)
68
+
69
+ Usage with sklearn cross_val_score:
70
+ >>> from sklearn.model_selection import cross_val_score
71
+ >>> scores = cross_val_score(model, X, y, cv=cv, scoring=rank_ic_scorer)
72
+ """
@@ -0,0 +1,32 @@
1
+ """
2
+ Importance oracles for boruta-quant.
3
+
4
+ All oracles compute importance on VALIDATION data only (never training data).
5
+
6
+ Available Oracles:
7
+ - PermutationImportanceOracle: Default. OOS permutation importance.
8
+ - DropColumnImportanceOracle: Ablation-based importance (refit without feature).
9
+ - BlockPermutationImportanceOracle: Temporal-aware block permutation.
10
+
11
+ Example:
12
+ >>> from boruta_quant.oracle import PermutationImportanceOracle
13
+ >>>
14
+ >>> oracle = PermutationImportanceOracle(
15
+ ... scoring="neg_mean_squared_error",
16
+ ... n_repeats=10,
17
+ ... random_state=42,
18
+ ... )
19
+ """
20
+
21
+ from boruta_quant.oracle.base import ImportanceOracle, validate_importance_inputs
22
+ from boruta_quant.oracle.block_permutation import BlockPermutationImportanceOracle
23
+ from boruta_quant.oracle.drop_column import DropColumnImportanceOracle
24
+ from boruta_quant.oracle.permutation import PermutationImportanceOracle
25
+
26
+ __all__ = [
27
+ "ImportanceOracle",
28
+ "validate_importance_inputs",
29
+ "PermutationImportanceOracle",
30
+ "DropColumnImportanceOracle",
31
+ "BlockPermutationImportanceOracle",
32
+ ]
@@ -0,0 +1,124 @@
1
+ """
2
+ Base protocol for importance oracles.
3
+
4
+ All importance oracles must implement this protocol to ensure they compute
5
+ importance on validation data only, never on training data.
6
+ """
7
+
8
+ from collections.abc import Callable
9
+ from typing import Any, Protocol
10
+
11
+ import numpy as np
12
+ import pandas as pd
13
+ from beartype import beartype
14
+ from sklearn.metrics import get_scorer
15
+
16
+ # Type alias for scorer callables (DRY: used by all oracles)
17
+ ScorerType = str | Callable[[Any, pd.DataFrame, pd.Series], float]
18
+ ScorerCallable = Callable[[Any, pd.DataFrame, pd.Series], float]
19
+
20
+ __all__ = [
21
+ "ImportanceOracle",
22
+ "validate_importance_inputs",
23
+ "get_scorer_callable",
24
+ "ScorerType",
25
+ "ScorerCallable",
26
+ ]
27
+
28
+
29
+ class ImportanceOracle(Protocol):
30
+ """
31
+ Protocol for pluggable importance computation.
32
+
33
+ All implementations MUST compute importance on validation data only.
34
+ This is the core contract that ensures temporal integrity in feature selection.
35
+
36
+ The oracle is responsible for:
37
+ 1. Fitting the model on training data (X_train, y_train)
38
+ 2. Computing feature importance on validation data (X_val, y_val)
39
+ 3. Returning importance scores for all features
40
+
41
+ CRITICAL: Importance MUST be computed on X_val, y_val only.
42
+ Computing importance on training data causes look-ahead bias.
43
+ """
44
+
45
+ @beartype
46
+ def compute_importance(
47
+ self,
48
+ model: Any,
49
+ X_train: pd.DataFrame,
50
+ y_train: pd.Series,
51
+ X_val: pd.DataFrame,
52
+ y_val: pd.Series,
53
+ feature_names: list[str],
54
+ ) -> dict[str, float]:
55
+ """
56
+ Compute feature importance on VALIDATION data only.
57
+
58
+ Args:
59
+ model: Unfitted model instance (will be cloned and fitted internally).
60
+ X_train: Training features (for fitting only).
61
+ y_train: Training target (for fitting only).
62
+ X_val: Validation features (importance computed HERE).
63
+ y_val: Validation target (importance computed HERE).
64
+ feature_names: List of feature names in X_train/X_val.
65
+
66
+ Returns:
67
+ Dict mapping feature name to importance score.
68
+ Higher values indicate more important features.
69
+
70
+ Raises:
71
+ AssertionError: If validation data is empty or has wrong shape.
72
+ """
73
+ ...
74
+
75
+
76
+ @beartype
77
+ def validate_importance_inputs(
78
+ X_train: pd.DataFrame,
79
+ y_train: pd.Series,
80
+ X_val: pd.DataFrame,
81
+ y_val: pd.Series,
82
+ feature_names: list[str],
83
+ ) -> None:
84
+ """
85
+ Validate inputs to importance computation.
86
+
87
+ This is a helper function that all oracle implementations should call
88
+ at the start of compute_importance().
89
+
90
+ Raises:
91
+ AssertionError: If any validation fails.
92
+ """
93
+ assert len(X_train) > 0, "X_train is empty"
94
+ assert len(X_val) > 0, "X_val is empty - cannot compute OOS importance"
95
+ assert len(y_train) == len(X_train), (
96
+ f"y_train length mismatch: {len(y_train)} vs {len(X_train)}"
97
+ )
98
+ assert len(y_val) == len(X_val), f"y_val length mismatch: {len(y_val)} vs {len(X_val)}"
99
+ assert list(X_train.columns) == feature_names, "X_train columns don't match feature_names"
100
+ assert list(X_val.columns) == feature_names, "X_val columns don't match feature_names"
101
+ assert not np.any(np.isnan(X_train.values)), "X_train contains NaN values"
102
+ assert not np.any(np.isnan(X_val.values)), "X_val contains NaN values"
103
+
104
+
105
+ @beartype
106
+ def get_scorer_callable(
107
+ scoring: ScorerType,
108
+ ) -> ScorerCallable:
109
+ """
110
+ Convert scoring parameter to callable scorer function.
111
+
112
+ DRY: Shared by all importance oracles that need custom scoring.
113
+
114
+ Args:
115
+ scoring: Sklearn scorer string (e.g., "r2", "neg_mean_squared_error")
116
+ or callable(estimator, X, y) -> float.
117
+
118
+ Returns:
119
+ Callable that takes (estimator, X, y) and returns score.
120
+ """
121
+ if callable(scoring):
122
+ return scoring
123
+ scorer = get_scorer(scoring)
124
+ return lambda est, X, y: scorer(est, X, y)
@@ -0,0 +1,165 @@
1
+ """
2
+ Block Permutation Importance Oracle - Temporal-aware permutation importance.
3
+
4
+ Unlike standard permutation importance which shuffles individual rows,
5
+ this oracle shuffles contiguous blocks to preserve within-block temporal structure.
6
+ This is more appropriate for time-series data.
7
+
8
+ CRITICAL: Importance is ALWAYS computed on validation data, NEVER on training.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any
14
+
15
+ import numpy as np
16
+ import pandas as pd
17
+ from beartype import beartype
18
+ from sklearn.base import clone
19
+
20
+ from .base import ScorerType, get_scorer_callable, validate_importance_inputs
21
+
22
+
23
+ @beartype
24
+ class BlockPermutationImportanceOracle:
25
+ """
26
+ Block permutation importance: shuffle temporal blocks, not individual rows.
27
+
28
+ For time-series data, shuffling individual rows destroys temporal patterns
29
+ within blocks. Block permutation preserves within-block structure while
30
+ breaking cross-block dependencies.
31
+
32
+ Importance = baseline_score - mean(permuted_scores)
33
+ Higher values indicate more important features.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ scoring: ScorerType,
39
+ block_size: int,
40
+ n_repeats: int = 10,
41
+ random_state: int | None = None,
42
+ ) -> None:
43
+ """
44
+ Initialize BlockPermutationImportanceOracle.
45
+
46
+ Args:
47
+ scoring: Sklearn scorer string or callable(estimator, X, y) -> float.
48
+ block_size: Number of consecutive rows per block.
49
+ n_repeats: Number of permutation repeats (default: 10).
50
+ random_state: Seed for reproducibility (default: None).
51
+
52
+ Raises:
53
+ AssertionError: If scoring is None, block_size <= 0, or n_repeats <= 0.
54
+ """
55
+ assert scoring is not None, "scoring is required"
56
+ assert block_size > 0, f"block_size must be > 0, got {block_size}"
57
+ assert n_repeats > 0, f"n_repeats must be > 0, got {n_repeats}"
58
+
59
+ self.scoring = scoring
60
+ self.block_size = block_size
61
+ self.n_repeats = n_repeats
62
+ self.random_state = random_state
63
+
64
+ def compute_importance(
65
+ self,
66
+ model: Any,
67
+ X_train: pd.DataFrame,
68
+ y_train: pd.Series,
69
+ X_val: pd.DataFrame,
70
+ y_val: pd.Series,
71
+ feature_names: list[str],
72
+ ) -> dict[str, float]:
73
+ """
74
+ Compute block permutation importance on VALIDATION data only.
75
+
76
+ For each feature:
77
+ 1. Divide validation data into blocks
78
+ 2. Shuffle the order of blocks (preserving within-block order)
79
+ 3. Score on shuffled data
80
+ 4. Repeat n_repeats times
81
+ 5. Importance = baseline_score - mean(permuted_scores)
82
+
83
+ Args:
84
+ model: Unfitted model instance (will be cloned and fitted once).
85
+ X_train: Training features (for fitting only).
86
+ y_train: Training target (for fitting only).
87
+ X_val: Validation features (importance computed HERE).
88
+ y_val: Validation target (importance computed HERE).
89
+ feature_names: List of feature names in X_train/X_val.
90
+
91
+ Returns:
92
+ Dict mapping feature name to importance score.
93
+ """
94
+ validate_importance_inputs(X_train, y_train, X_val, y_val, feature_names)
95
+
96
+ n_val = len(X_val)
97
+ assert self.block_size <= n_val, (
98
+ f"block_size ({self.block_size}) > validation set size ({n_val})"
99
+ )
100
+
101
+ rng = np.random.default_rng(self.random_state)
102
+ # DRY: Use shared scorer utility
103
+ scorer = get_scorer_callable(self.scoring)
104
+
105
+ # Fit model once on training data
106
+ fitted_model = clone(model)
107
+ fitted_model.fit(X_train, y_train)
108
+ baseline_score = scorer(fitted_model, X_val, y_val)
109
+
110
+ block_indices = self._compute_block_indices(n_val)
111
+ importances: dict[str, float] = {}
112
+
113
+ for feature in feature_names:
114
+ permuted_scores: list[float] = []
115
+
116
+ for _ in range(self.n_repeats):
117
+ X_permuted = X_val.copy()
118
+ X_permuted[feature] = self._permute_blocks(
119
+ X_val[feature].values, block_indices, rng
120
+ )
121
+ score = scorer(fitted_model, X_permuted, y_val)
122
+ permuted_scores.append(score)
123
+
124
+ importances[feature] = baseline_score - float(np.mean(permuted_scores))
125
+
126
+ return importances
127
+
128
+ def _compute_block_indices(self, n_samples: int) -> list[tuple[int, int]]:
129
+ """
130
+ Compute block start/end indices.
131
+
132
+ Args:
133
+ n_samples: Total number of samples.
134
+
135
+ Returns:
136
+ List of (start, end) tuples for each block.
137
+ """
138
+ blocks: list[tuple[int, int]] = []
139
+ start = 0
140
+ while start < n_samples:
141
+ end = min(start + self.block_size, n_samples)
142
+ blocks.append((start, end))
143
+ start = end
144
+ return blocks
145
+
146
+ def _permute_blocks(
147
+ self,
148
+ values: np.ndarray,
149
+ block_indices: list[tuple[int, int]],
150
+ rng: np.random.Generator,
151
+ ) -> np.ndarray:
152
+ """
153
+ Permute values by shuffling blocks.
154
+
155
+ Args:
156
+ values: 1D array of feature values.
157
+ block_indices: List of (start, end) tuples.
158
+ rng: Numpy random generator.
159
+
160
+ Returns:
161
+ Permuted values with blocks shuffled but within-block order preserved.
162
+ """
163
+ blocks = [values[start:end].copy() for start, end in block_indices]
164
+ rng.shuffle(blocks)
165
+ return np.concatenate(blocks)