moose-fs 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.
- LICENSE +21 -0
- README.md +190 -0
- moose_fs-0.1.0.dist-info/METADATA +232 -0
- moose_fs-0.1.0.dist-info/RECORD +40 -0
- moose_fs-0.1.0.dist-info/WHEEL +4 -0
- moose_fs-0.1.0.dist-info/entry_points.txt +2 -0
- moose_fs-0.1.0.dist-info/licenses/LICENSE +21 -0
- moosefs/__init__.py +6 -0
- moosefs/core/__init__.py +6 -0
- moosefs/core/data_processor.py +319 -0
- moosefs/core/feature.py +44 -0
- moosefs/core/novovicova.py +60 -0
- moosefs/core/pareto.py +90 -0
- moosefs/feature_selection_pipeline.py +548 -0
- moosefs/feature_selectors/__init__.py +26 -0
- moosefs/feature_selectors/base_selector.py +38 -0
- moosefs/feature_selectors/default_variance.py +21 -0
- moosefs/feature_selectors/elastic_net_selector.py +75 -0
- moosefs/feature_selectors/f_statistic_selector.py +42 -0
- moosefs/feature_selectors/lasso_selector.py +46 -0
- moosefs/feature_selectors/mrmr_selector.py +57 -0
- moosefs/feature_selectors/mutual_info_selector.py +45 -0
- moosefs/feature_selectors/random_forest_selector.py +48 -0
- moosefs/feature_selectors/svm_selector.py +50 -0
- moosefs/feature_selectors/variance_selectors.py +16 -0
- moosefs/feature_selectors/xgboost_selector.py +44 -0
- moosefs/merging_strategies/__init__.py +17 -0
- moosefs/merging_strategies/arithmetic_mean_merger.py +46 -0
- moosefs/merging_strategies/base_merger.py +64 -0
- moosefs/merging_strategies/borda_merger.py +46 -0
- moosefs/merging_strategies/consensus_merger.py +80 -0
- moosefs/merging_strategies/l2_norm_merger.py +42 -0
- moosefs/merging_strategies/union_of_intersections_merger.py +89 -0
- moosefs/metrics/__init__.py +23 -0
- moosefs/metrics/performance_metrics.py +239 -0
- moosefs/metrics/stability_metrics.py +49 -0
- moosefs/utils.py +161 -0
- scripts/config.yml +92 -0
- scripts/main.py +163 -0
- scripts/utils.py +186 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from collections import Counter, defaultdict
|
|
2
|
+
from itertools import chain
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from .base_merger import MergingStrategy
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConsensusMerger(MergingStrategy):
|
|
11
|
+
"""Set-based consensus merger with optional fill.
|
|
12
|
+
|
|
13
|
+
Keeps features selected by at least ``k`` selectors. If ``fill=True``,
|
|
14
|
+
trims/pads to ``num_features_to_select`` using summed, per-selector
|
|
15
|
+
min–max–normalized scores as a tie-breaker.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, k: int = 2, *, fill: bool = False) -> None:
|
|
19
|
+
super().__init__("set-based")
|
|
20
|
+
self.k = k
|
|
21
|
+
self.fill = fill
|
|
22
|
+
self.name = f"Consensus_ge_{k}"
|
|
23
|
+
|
|
24
|
+
# -----------------------------------------------------------------
|
|
25
|
+
def merge(
|
|
26
|
+
self,
|
|
27
|
+
subsets: list,
|
|
28
|
+
num_features_to_select: Optional[int] = None,
|
|
29
|
+
**kwargs,
|
|
30
|
+
) -> set:
|
|
31
|
+
"""Merge by consensus threshold.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
subsets: Feature lists (one list per selector).
|
|
35
|
+
num_features_to_select: Required when ``fill=True``.
|
|
36
|
+
**kwargs: Unused.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Set of selected feature names.
|
|
40
|
+
"""
|
|
41
|
+
self._validate_input(subsets)
|
|
42
|
+
|
|
43
|
+
if self.fill and num_features_to_select is None:
|
|
44
|
+
raise ValueError("`num_features_to_select` required when fill=True")
|
|
45
|
+
|
|
46
|
+
# ── collect names & scores (ragged‑safe) ─────────────────────
|
|
47
|
+
names_mat = [[f.name for f in s] for s in subsets]
|
|
48
|
+
|
|
49
|
+
# Consensus counts across all selectors
|
|
50
|
+
counts = Counter(chain.from_iterable(names_mat))
|
|
51
|
+
|
|
52
|
+
# Summed, per-selector min‑max–normalised scores per feature name
|
|
53
|
+
sum_scores = defaultdict(float)
|
|
54
|
+
for subset in subsets:
|
|
55
|
+
if not subset:
|
|
56
|
+
continue
|
|
57
|
+
scores = np.array([f.score for f in subset], dtype=np.float32)
|
|
58
|
+
min_v = float(scores.min())
|
|
59
|
+
rng = float(scores.max() - min_v) or 1.0
|
|
60
|
+
norm = (scores - min_v) / rng
|
|
61
|
+
for name, s in zip([f.name for f in subset], norm):
|
|
62
|
+
sum_scores[name] += float(s)
|
|
63
|
+
|
|
64
|
+
selected = {f for f, c in counts.items() if c >= self.k}
|
|
65
|
+
|
|
66
|
+
if not self.fill:
|
|
67
|
+
return selected
|
|
68
|
+
|
|
69
|
+
# ── trim / pad to desired size ───────────────────────────────
|
|
70
|
+
core_sorted = sorted(selected, key=lambda n: sum_scores[n], reverse=True)
|
|
71
|
+
if len(core_sorted) >= num_features_to_select:
|
|
72
|
+
return set(core_sorted[:num_features_to_select])
|
|
73
|
+
|
|
74
|
+
extras = sorted(
|
|
75
|
+
(n for n in counts if n not in selected),
|
|
76
|
+
key=lambda n: sum_scores[n],
|
|
77
|
+
reverse=True,
|
|
78
|
+
)
|
|
79
|
+
need = num_features_to_select - len(core_sorted)
|
|
80
|
+
return set(core_sorted + extras[:need])
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from .base_merger import MergingStrategy
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class L2NormMerger(MergingStrategy):
|
|
7
|
+
"""Rank-based merging using the L2-norm (RMS) of scores."""
|
|
8
|
+
|
|
9
|
+
name = "L2Norm"
|
|
10
|
+
|
|
11
|
+
def __init__(self, **kwargs) -> None:
|
|
12
|
+
super().__init__("rank-based")
|
|
13
|
+
self.kwargs = kwargs
|
|
14
|
+
|
|
15
|
+
def merge(
|
|
16
|
+
self,
|
|
17
|
+
subsets: list,
|
|
18
|
+
num_features_to_select: int,
|
|
19
|
+
**kwargs,
|
|
20
|
+
) -> list:
|
|
21
|
+
"""Return the top‑k feature names after L2-norm aggregation.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
subsets: Feature lists (one list per selector).
|
|
25
|
+
num_features_to_select: Number of names to return.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Feature names sorted by aggregated L2 score.
|
|
29
|
+
"""
|
|
30
|
+
self._validate_input(subsets)
|
|
31
|
+
|
|
32
|
+
if len(subsets) == 1:
|
|
33
|
+
return [f.name for f in subsets[0]][:num_features_to_select]
|
|
34
|
+
|
|
35
|
+
feature_names = [f.name for f in subsets[0]]
|
|
36
|
+
scores = np.array([[f.score for f in s] for s in subsets]).T
|
|
37
|
+
|
|
38
|
+
# Euclidean norm (root-mean-square) across selectors
|
|
39
|
+
scores_merged = np.linalg.norm(scores, ord=2, axis=1)
|
|
40
|
+
|
|
41
|
+
sorted_names = [feature_names[i] for i in np.argsort(-scores_merged, kind="stable")]
|
|
42
|
+
return sorted_names[:num_features_to_select]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from itertools import combinations
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from .base_merger import MergingStrategy
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UnionOfIntersectionsMerger(MergingStrategy):
|
|
11
|
+
"""Union of intersections across selector subsets."""
|
|
12
|
+
|
|
13
|
+
name = "UnionOfIntersections"
|
|
14
|
+
|
|
15
|
+
def __init__(self) -> None:
|
|
16
|
+
super().__init__("set-based")
|
|
17
|
+
|
|
18
|
+
def merge(
|
|
19
|
+
self,
|
|
20
|
+
subsets: list,
|
|
21
|
+
num_features_to_select: Optional[int] = None,
|
|
22
|
+
fill: bool = False,
|
|
23
|
+
**kwargs,
|
|
24
|
+
) -> set:
|
|
25
|
+
"""Merge by union of pairwise intersections.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
subsets: Feature lists (one list per selector).
|
|
29
|
+
num_features_to_select: Required when ``fill=True``.
|
|
30
|
+
fill: If True, trim/pad output to requested size.
|
|
31
|
+
**kwargs: Unused.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Set of selected feature names.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValueError: If inputs are invalid or size is missing when ``fill=True``.
|
|
38
|
+
"""
|
|
39
|
+
self._validate_input(subsets)
|
|
40
|
+
|
|
41
|
+
if fill and num_features_to_select is None:
|
|
42
|
+
raise ValueError("`num_features_to_select` must be provided when `fill=True`.")
|
|
43
|
+
|
|
44
|
+
if len(subsets) == 1:
|
|
45
|
+
feature_names = {f.name for f in subsets[0]}
|
|
46
|
+
return (
|
|
47
|
+
set(sorted(feature_names, key=lambda f: f.score, reverse=True)[:num_features_to_select])
|
|
48
|
+
if fill
|
|
49
|
+
else feature_names
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Extract feature names and scores
|
|
53
|
+
feature_names = [[f.name for f in subset] for subset in subsets]
|
|
54
|
+
feature_scores = np.array([[f.score for f in subset] for subset in subsets], dtype=np.float32).T
|
|
55
|
+
|
|
56
|
+
# Normalize scores within each subset (vectorized min-max scaling)
|
|
57
|
+
min_vals, max_vals = (
|
|
58
|
+
feature_scores.min(axis=1, keepdims=True),
|
|
59
|
+
feature_scores.max(axis=1, keepdims=True),
|
|
60
|
+
)
|
|
61
|
+
score_range = np.where(max_vals - min_vals == 0, 1, max_vals - min_vals) # Prevent division by zero
|
|
62
|
+
feature_scores = (feature_scores - min_vals) / score_range
|
|
63
|
+
|
|
64
|
+
# Compute core as the union of pairwise intersections
|
|
65
|
+
core = set().union(
|
|
66
|
+
*[set(feature_names[i]) & set(feature_names[j]) for i, j in combinations(range(len(feature_names)), 2)]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if not fill:
|
|
70
|
+
return core # Return raw core without enforcing `num_features_to_select`
|
|
71
|
+
|
|
72
|
+
# Compute global feature scores (sum of normalized values)
|
|
73
|
+
feature_score_map = defaultdict(float)
|
|
74
|
+
for subset, scores in zip(feature_names, feature_scores.T):
|
|
75
|
+
for name, score in zip(subset, scores):
|
|
76
|
+
feature_score_map[name] += score
|
|
77
|
+
|
|
78
|
+
# Prune or fill to get exactly `num_features_to_select`
|
|
79
|
+
core_list = sorted(core, key=lambda x: feature_score_map[x], reverse=True)
|
|
80
|
+
core_size = len(core_list)
|
|
81
|
+
|
|
82
|
+
if core_size >= num_features_to_select:
|
|
83
|
+
return set(core_list[:num_features_to_select])
|
|
84
|
+
|
|
85
|
+
# Fill with highest-ranked extra features
|
|
86
|
+
extras = sorted(feature_score_map.keys(), key=lambda x: feature_score_map[x], reverse=True)
|
|
87
|
+
extras = [f for f in extras if f not in core][: num_features_to_select - core_size]
|
|
88
|
+
|
|
89
|
+
return set(core_list + extras)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .performance_metrics import (
|
|
2
|
+
Accuracy,
|
|
3
|
+
BaseMetric,
|
|
4
|
+
F1Score,
|
|
5
|
+
LogLoss,
|
|
6
|
+
MeanAbsoluteError,
|
|
7
|
+
MeanSquaredError,
|
|
8
|
+
PrecisionScore,
|
|
9
|
+
R2Score,
|
|
10
|
+
RecallScore,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"BaseMetric",
|
|
15
|
+
"R2Score",
|
|
16
|
+
"MeanAbsoluteError",
|
|
17
|
+
"MeanSquaredError",
|
|
18
|
+
"LogLoss",
|
|
19
|
+
"F1Score",
|
|
20
|
+
"Accuracy",
|
|
21
|
+
"PrecisionScore",
|
|
22
|
+
"RecallScore",
|
|
23
|
+
]
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
|
|
3
|
+
from joblib import hash as joblib_hash
|
|
4
|
+
import numpy as np
|
|
5
|
+
from sklearn.ensemble import (
|
|
6
|
+
GradientBoostingClassifier,
|
|
7
|
+
GradientBoostingRegressor,
|
|
8
|
+
RandomForestClassifier,
|
|
9
|
+
RandomForestRegressor,
|
|
10
|
+
)
|
|
11
|
+
from sklearn.linear_model import LinearRegression, LogisticRegression
|
|
12
|
+
from sklearn.metrics import (
|
|
13
|
+
accuracy_score,
|
|
14
|
+
f1_score,
|
|
15
|
+
log_loss,
|
|
16
|
+
mean_absolute_error,
|
|
17
|
+
mean_squared_error,
|
|
18
|
+
precision_score,
|
|
19
|
+
r2_score,
|
|
20
|
+
recall_score,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BaseMetric:
|
|
25
|
+
"""Base class for computing evaluation metrics.
|
|
26
|
+
|
|
27
|
+
Trains a small battery of models and aggregates per-model metric values.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, name: str, task: str) -> None:
|
|
31
|
+
"""Initialize the metric with a task type.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
name: Human-readable metric name.
|
|
35
|
+
task: Either "classification" or "regression".
|
|
36
|
+
"""
|
|
37
|
+
if task not in {"classification", "regression"}:
|
|
38
|
+
raise ValueError("Task must be 'classification' or 'regression'.")
|
|
39
|
+
|
|
40
|
+
self.name = name
|
|
41
|
+
self.task = task
|
|
42
|
+
self.models = self._initialize_models()
|
|
43
|
+
|
|
44
|
+
def model_signature(self) -> str:
|
|
45
|
+
"""Return a stable signature describing the internal model set."""
|
|
46
|
+
signature_payload = {
|
|
47
|
+
name: (
|
|
48
|
+
f"{model.__class__.__module__}.{model.__class__.__qualname__}",
|
|
49
|
+
model.get_params(deep=True),
|
|
50
|
+
)
|
|
51
|
+
for name, model in self.models.items()
|
|
52
|
+
}
|
|
53
|
+
return f"{self.task}:{joblib_hash(signature_payload)}"
|
|
54
|
+
|
|
55
|
+
def _initialize_models(self) -> dict:
|
|
56
|
+
"""Initialize task-specific models.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Mapping from model label to estimator instance.
|
|
60
|
+
"""
|
|
61
|
+
# Keep inner models single-threaded to avoid nested parallelism.
|
|
62
|
+
return {
|
|
63
|
+
"classification": {
|
|
64
|
+
"Random Forest": RandomForestClassifier(n_jobs=1),
|
|
65
|
+
"Logistic Regression": LogisticRegression(max_iter=1000),
|
|
66
|
+
"Gradient Boosting": GradientBoostingClassifier(),
|
|
67
|
+
},
|
|
68
|
+
"regression": {
|
|
69
|
+
"Random Forest": RandomForestRegressor(n_jobs=1),
|
|
70
|
+
"Linear Regression": LinearRegression(),
|
|
71
|
+
"Gradient Boosting": GradientBoostingRegressor(),
|
|
72
|
+
},
|
|
73
|
+
}[self.task]
|
|
74
|
+
|
|
75
|
+
def train_and_predict(
|
|
76
|
+
self,
|
|
77
|
+
X_train: Any,
|
|
78
|
+
y_train: Any,
|
|
79
|
+
X_test: Any,
|
|
80
|
+
y_test: Any,
|
|
81
|
+
) -> dict:
|
|
82
|
+
"""Train all models and generate predictions.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
X_train: Training features.
|
|
86
|
+
y_train: Training targets.
|
|
87
|
+
X_test: Test features.
|
|
88
|
+
y_test: Test targets.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dict keyed by model name with predictions and optional probabilities.
|
|
92
|
+
"""
|
|
93
|
+
results = {}
|
|
94
|
+
|
|
95
|
+
for model_name, model in self.models.items():
|
|
96
|
+
model.fit(X_train, y_train)
|
|
97
|
+
predictions = model.predict(X_test)
|
|
98
|
+
probabilities = model.predict_proba(X_test) if self.task == "classification" else None
|
|
99
|
+
results[model_name] = {
|
|
100
|
+
"predictions": predictions,
|
|
101
|
+
"probabilities": probabilities,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return results
|
|
105
|
+
|
|
106
|
+
def compute(
|
|
107
|
+
self,
|
|
108
|
+
X_train: Any,
|
|
109
|
+
y_train: Any,
|
|
110
|
+
X_test: Any,
|
|
111
|
+
y_test: Any,
|
|
112
|
+
) -> float:
|
|
113
|
+
"""Compute the metric (implemented by subclasses)."""
|
|
114
|
+
raise NotImplementedError("This method must be overridden in subclasses.")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class RegressionMetric(BaseMetric):
|
|
118
|
+
"""Base class for regression metrics."""
|
|
119
|
+
|
|
120
|
+
def __init__(self, name: str) -> None:
|
|
121
|
+
super().__init__(name, task="regression")
|
|
122
|
+
|
|
123
|
+
def compute(
|
|
124
|
+
self,
|
|
125
|
+
X_train: Any,
|
|
126
|
+
y_train: Any,
|
|
127
|
+
X_test: Any,
|
|
128
|
+
y_test: Any,
|
|
129
|
+
) -> float:
|
|
130
|
+
"""Average the metric over the internal model set."""
|
|
131
|
+
results = self.train_and_predict(X_train, y_train, X_test, y_test)
|
|
132
|
+
return self.aggregate_from_results(y_test, results)
|
|
133
|
+
|
|
134
|
+
def aggregate_from_results(self, y_test: np.ndarray, results: dict) -> float:
|
|
135
|
+
"""Aggregate metric value from cached prediction results."""
|
|
136
|
+
return float(np.mean([self._metric_func(y_test, res["predictions"]) for res in results.values()]))
|
|
137
|
+
|
|
138
|
+
def _metric_func(self, y_true: np.ndarray, y_pred: np.ndarray) -> float:
|
|
139
|
+
"""Metric function to be overridden by subclasses."""
|
|
140
|
+
raise NotImplementedError("This method must be overridden in subclasses.")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class R2Score(RegressionMetric):
|
|
144
|
+
def __init__(self) -> None:
|
|
145
|
+
super().__init__("R2 Score")
|
|
146
|
+
|
|
147
|
+
def _metric_func(self, y_true: np.ndarray, y_pred: np.ndarray) -> float:
|
|
148
|
+
return r2_score(y_true, y_pred)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class MeanAbsoluteError(RegressionMetric):
|
|
152
|
+
def __init__(self) -> None:
|
|
153
|
+
super().__init__("Mean Absolute Error")
|
|
154
|
+
|
|
155
|
+
def _metric_func(self, y_true: np.ndarray, y_pred: np.ndarray) -> float:
|
|
156
|
+
return -mean_absolute_error(y_true, y_pred) # Return negative MAE
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class MeanSquaredError(RegressionMetric):
|
|
160
|
+
def __init__(self) -> None:
|
|
161
|
+
super().__init__("Mean Squared Error")
|
|
162
|
+
|
|
163
|
+
def _metric_func(self, y_true: np.ndarray, y_pred: np.ndarray) -> float:
|
|
164
|
+
return -mean_squared_error(y_true, y_pred) # Return negative MSE
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class ClassificationMetric(BaseMetric):
|
|
168
|
+
"""Base class for classification metrics."""
|
|
169
|
+
|
|
170
|
+
def __init__(self, name: str) -> None:
|
|
171
|
+
super().__init__(name, task="classification")
|
|
172
|
+
|
|
173
|
+
def compute(
|
|
174
|
+
self,
|
|
175
|
+
X_train: Any,
|
|
176
|
+
y_train: Any,
|
|
177
|
+
X_test: Any,
|
|
178
|
+
y_test: Any,
|
|
179
|
+
) -> float:
|
|
180
|
+
"""Average the metric over the internal model set."""
|
|
181
|
+
results = self.train_and_predict(X_train, y_train, X_test, y_test)
|
|
182
|
+
return self.aggregate_from_results(y_test, results)
|
|
183
|
+
|
|
184
|
+
def aggregate_from_results(self, y_test: np.ndarray, results: dict) -> float:
|
|
185
|
+
"""Aggregate metric value from cached prediction results."""
|
|
186
|
+
return float(
|
|
187
|
+
np.mean(
|
|
188
|
+
[self._metric_func(y_test, res["predictions"], res.get("probabilities")) for res in results.values()]
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def _metric_func(
|
|
193
|
+
self,
|
|
194
|
+
y_true: np.ndarray,
|
|
195
|
+
y_pred: np.ndarray,
|
|
196
|
+
y_proba: Optional[np.ndarray] = None,
|
|
197
|
+
) -> float:
|
|
198
|
+
"""Metric function to be overridden by subclasses."""
|
|
199
|
+
raise NotImplementedError("This method must be overridden in subclasses.")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class LogLoss(ClassificationMetric):
|
|
203
|
+
def __init__(self) -> None:
|
|
204
|
+
super().__init__("Log Loss")
|
|
205
|
+
|
|
206
|
+
def _metric_func(self, y_true: np.ndarray, y_pred: np.ndarray, y_proba: np.ndarray) -> float:
|
|
207
|
+
return -log_loss(y_true, y_proba)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class F1Score(ClassificationMetric):
|
|
211
|
+
def __init__(self) -> None:
|
|
212
|
+
super().__init__("F1 Score")
|
|
213
|
+
|
|
214
|
+
def _metric_func(self, y_true: np.ndarray, y_pred: np.ndarray, y_proba: None = None) -> float:
|
|
215
|
+
return f1_score(y_true, y_pred, average="macro")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class Accuracy(ClassificationMetric):
|
|
219
|
+
def __init__(self) -> None:
|
|
220
|
+
super().__init__("Accuracy")
|
|
221
|
+
|
|
222
|
+
def _metric_func(self, y_true: np.ndarray, y_pred: np.ndarray, y_proba: None = None) -> float:
|
|
223
|
+
return accuracy_score(y_true, y_pred)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class PrecisionScore(ClassificationMetric):
|
|
227
|
+
def __init__(self) -> None:
|
|
228
|
+
super().__init__("Precision Score")
|
|
229
|
+
|
|
230
|
+
def _metric_func(self, y_true: np.ndarray, y_pred: np.ndarray, y_proba: None = None) -> float:
|
|
231
|
+
return precision_score(y_true, y_pred, average="macro", zero_division=0)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class RecallScore(ClassificationMetric):
|
|
235
|
+
def __init__(self) -> None:
|
|
236
|
+
super().__init__("Recall Score")
|
|
237
|
+
|
|
238
|
+
def _metric_func(self, y_true: np.ndarray, y_pred: np.ndarray, y_proba: None = None) -> float:
|
|
239
|
+
return recall_score(y_true, y_pred, average="macro")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from itertools import combinations
|
|
2
|
+
|
|
3
|
+
from moosefs.core.novovicova import StabilityNovovicova
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def compute_stability_metrics(features_list: list) -> float:
|
|
7
|
+
"""Compute stability SH(S) across selections.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
features_list: Selected feature names per selector.
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
Stability in [0, 1].
|
|
14
|
+
"""
|
|
15
|
+
return StabilityNovovicova(features_list).compute_stability()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _jaccard(a: set, b: set) -> float:
|
|
19
|
+
"""Return Jaccard similarity, handling empty sets as 1.0 if both empty."""
|
|
20
|
+
return len(a & b) / len(a | b) if a | b else 1.0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def diversity_agreement(selectors: list, merged: list, alpha: float = 0.5) -> float:
|
|
24
|
+
"""Blend diversity and agreement into a single score.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
selectors: List of selected feature lists (one per selector).
|
|
28
|
+
merged: Merged/core feature names for the group.
|
|
29
|
+
alpha: Weight on agreement (0 → pure diversity, 1 → pure agreement).
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Score in [0, 1] (higher is better).
|
|
33
|
+
"""
|
|
34
|
+
k = len(selectors)
|
|
35
|
+
if k < 2:
|
|
36
|
+
return 0.0 # cannot measure diversity with a single selector
|
|
37
|
+
|
|
38
|
+
sets = [set(s) for s in selectors]
|
|
39
|
+
core = set(merged)
|
|
40
|
+
|
|
41
|
+
# 1) diversity (average Jaccard *dissimilarity* across selector pairs)
|
|
42
|
+
pair_dis = [1.0 - _jaccard(sets[i], sets[j]) for i, j in combinations(range(k), 2)]
|
|
43
|
+
diversity = sum(pair_dis) / len(pair_dis)
|
|
44
|
+
|
|
45
|
+
# 2) agreement (mean similarity of each selector to the core)
|
|
46
|
+
agree = sum(_jaccard(s, core) for s in sets) / k
|
|
47
|
+
|
|
48
|
+
# 3) linear blend
|
|
49
|
+
return (1.0 - alpha) * diversity + alpha * agree
|
moosefs/utils.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
# Mapping of class identifiers to their import paths and expected initialization parameters.
|
|
5
|
+
# Template: "identifier": ("module.path.ClassName", ["param1", "param2", ...])
|
|
6
|
+
class_path_mapping: dict = {
|
|
7
|
+
# metrics
|
|
8
|
+
"mse": (
|
|
9
|
+
"moosefs.metrics.performance_metrics.MeanSquaredError",
|
|
10
|
+
[],
|
|
11
|
+
),
|
|
12
|
+
"mae": (
|
|
13
|
+
"moosefs.metrics.performance_metrics.MeanAbsoluteError",
|
|
14
|
+
[],
|
|
15
|
+
),
|
|
16
|
+
"r2_score": (
|
|
17
|
+
"moosefs.metrics.performance_metrics.R2Score",
|
|
18
|
+
[],
|
|
19
|
+
),
|
|
20
|
+
"logloss": (
|
|
21
|
+
"moosefs.metrics.performance_metrics.LogLoss",
|
|
22
|
+
[],
|
|
23
|
+
),
|
|
24
|
+
"f1_score": (
|
|
25
|
+
"moosefs.metrics.performance_metrics.F1Score",
|
|
26
|
+
[],
|
|
27
|
+
),
|
|
28
|
+
"accuracy": (
|
|
29
|
+
"moosefs.metrics.performance_metrics.Accuracy",
|
|
30
|
+
[],
|
|
31
|
+
),
|
|
32
|
+
"precision_score": (
|
|
33
|
+
"moosefs.metrics.performance_metrics.PrecisionScore",
|
|
34
|
+
[],
|
|
35
|
+
),
|
|
36
|
+
"recall_score": (
|
|
37
|
+
"moosefs.metrics.performance_metrics.RecallScore",
|
|
38
|
+
[],
|
|
39
|
+
),
|
|
40
|
+
"f_statistic_selector": (
|
|
41
|
+
"moosefs.feature_selectors.f_statistic_selector.FStatisticSelector",
|
|
42
|
+
["task", "num_features_to_select"],
|
|
43
|
+
),
|
|
44
|
+
"random_forest_selector": (
|
|
45
|
+
"moosefs.feature_selectors.random_forest_selector.RandomForestSelector",
|
|
46
|
+
["task", "num_features_to_select", "random_state"],
|
|
47
|
+
),
|
|
48
|
+
"mutual_info_selector": (
|
|
49
|
+
"moosefs.feature_selectors.mutual_info_selector.MutualInfoSelector",
|
|
50
|
+
["task", "num_features_to_select", "random_state"],
|
|
51
|
+
),
|
|
52
|
+
"svm_selector": (
|
|
53
|
+
"moosefs.feature_selectors.svm_selector.SVMSelector",
|
|
54
|
+
["task", "num_features_to_select"],
|
|
55
|
+
),
|
|
56
|
+
"xgboost_selector": (
|
|
57
|
+
"moosefs.feature_selectors.xgboost_selector.XGBoostSelector",
|
|
58
|
+
["task", "num_features_to_select", "random_state"],
|
|
59
|
+
),
|
|
60
|
+
"mrmr_selector": (
|
|
61
|
+
"moosefs.feature_selectors.mrmr_selector.MRMRSelector",
|
|
62
|
+
["task", "num_features_to_select"],
|
|
63
|
+
),
|
|
64
|
+
"lasso_selector": (
|
|
65
|
+
"moosefs.feature_selectors.lasso_selector.LassoSelector",
|
|
66
|
+
["task", "num_features_to_select", "random_state"],
|
|
67
|
+
),
|
|
68
|
+
"elastic_net_selector": (
|
|
69
|
+
"moosefs.feature_selectors.elastic_net_selector.ElasticNetSelector",
|
|
70
|
+
["task", "num_features_to_select", "random_state"],
|
|
71
|
+
),
|
|
72
|
+
"variance_selector": (
|
|
73
|
+
"moosefs.feature_selectors.variance_selectors.VarianceSelector",
|
|
74
|
+
["task", "num_features_to_select"],
|
|
75
|
+
),
|
|
76
|
+
# mergers
|
|
77
|
+
"borda_merger": (
|
|
78
|
+
"moosefs.merging_strategies.borda_merger.BordaMerger",
|
|
79
|
+
[],
|
|
80
|
+
),
|
|
81
|
+
"union_of_intersections_merger": (
|
|
82
|
+
"moosefs.merging_strategies.union_of_intersections_merger.UnionOfIntersectionsMerger",
|
|
83
|
+
[],
|
|
84
|
+
),
|
|
85
|
+
"l2_norm_merger": (
|
|
86
|
+
"moosefs.merging_strategies.l2_norm_merger.L2NormMerger",
|
|
87
|
+
[],
|
|
88
|
+
),
|
|
89
|
+
"arithmetic_mean_merger": (
|
|
90
|
+
"moosefs.merging_strategies.arithmetic_mean_merger.ArithmeticMeanMerger",
|
|
91
|
+
[],
|
|
92
|
+
),
|
|
93
|
+
"consensus_merger": (
|
|
94
|
+
"moosefs.merging_strategies.consensus_merger.ConsensusMerger",
|
|
95
|
+
["k", "fill"],
|
|
96
|
+
),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def dynamic_import(class_path: str) -> type:
|
|
101
|
+
"""Import a class from a fully qualified path.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
class_path: Dotted path, e.g. "moosefs.module.ClassName".
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
The referenced class object.
|
|
108
|
+
"""
|
|
109
|
+
components = class_path.split(".")
|
|
110
|
+
module_path = ".".join(components[:-1])
|
|
111
|
+
class_name = components[-1]
|
|
112
|
+
module = __import__(module_path, fromlist=[class_name])
|
|
113
|
+
return getattr(module, class_name)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_class_info(identifier: str) -> tuple:
|
|
117
|
+
"""Resolve an identifier to a class and its expected params.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
identifier: Lookup key in ``class_path_mapping``.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
(class, params) where params are attribute names to fetch.
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
ValueError: If the identifier is unknown.
|
|
127
|
+
"""
|
|
128
|
+
if identifier not in class_path_mapping:
|
|
129
|
+
raise ValueError(f"Unknown class identifier: {identifier}")
|
|
130
|
+
class_path, params = class_path_mapping[identifier]
|
|
131
|
+
cls = dynamic_import(class_path)
|
|
132
|
+
return cls, params
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def extract_params(cls: type, instance: Any, params: list) -> dict:
|
|
136
|
+
"""Collect constructor parameters from an owning instance.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
cls: Class to instantiate.
|
|
140
|
+
instance: Object carrying attributes matching ``params``.
|
|
141
|
+
params: Parameter names to extract.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Mapping of parameter names to values for ``cls``.
|
|
145
|
+
"""
|
|
146
|
+
sig = inspect.signature(cls.__init__)
|
|
147
|
+
|
|
148
|
+
extracted_params: dict = {
|
|
149
|
+
param: getattr(instance, param) for param in params if param in sig.parameters and hasattr(instance, param)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# If **kwargs exists in the class signature, include additional parameters.
|
|
153
|
+
if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()):
|
|
154
|
+
additional_params = {
|
|
155
|
+
param: getattr(instance, param)
|
|
156
|
+
for param in params
|
|
157
|
+
if param not in sig.parameters and hasattr(instance, param)
|
|
158
|
+
}
|
|
159
|
+
extracted_params.update(additional_params)
|
|
160
|
+
|
|
161
|
+
return extracted_params
|