deskit 1.2.8__tar.gz → 1.3__tar.gz

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 (33) hide show
  1. {deskit-1.2.8/src/deskit.egg-info → deskit-1.3}/PKG-INFO +1 -1
  2. {deskit-1.2.8 → deskit-1.3}/pyproject.toml +1 -1
  3. {deskit-1.2.8 → deskit-1.3}/src/deskit/_config.py +27 -0
  4. {deskit-1.2.8 → deskit-1.3}/src/deskit/des/dewsi.py +11 -7
  5. {deskit-1.2.8 → deskit-1.3}/src/deskit/des/dewsiv.py +15 -9
  6. {deskit-1.2.8 → deskit-1.3}/src/deskit/des/dewst.py +16 -9
  7. {deskit-1.2.8 → deskit-1.3}/src/deskit/des/dewsu.py +14 -9
  8. {deskit-1.2.8 → deskit-1.3}/src/deskit/des/dewsv.py +16 -9
  9. {deskit-1.2.8 → deskit-1.3}/src/deskit/des/knorae.py +14 -11
  10. {deskit-1.2.8 → deskit-1.3}/src/deskit/des/knoraiu.py +13 -10
  11. {deskit-1.2.8 → deskit-1.3}/src/deskit/des/knorau.py +13 -10
  12. {deskit-1.2.8 → deskit-1.3}/src/deskit/des/lwsei.py +1 -1
  13. {deskit-1.2.8 → deskit-1.3}/src/deskit/des/lwseu.py +1 -1
  14. {deskit-1.2.8 → deskit-1.3}/src/deskit/des/ola.py +1 -1
  15. deskit-1.3/src/deskit/norms.py +82 -0
  16. {deskit-1.2.8 → deskit-1.3/src/deskit.egg-info}/PKG-INFO +1 -1
  17. {deskit-1.2.8 → deskit-1.3}/src/deskit.egg-info/SOURCES.txt +1 -0
  18. {deskit-1.2.8 → deskit-1.3}/LICENSE +0 -0
  19. {deskit-1.2.8 → deskit-1.3}/README.md +0 -0
  20. {deskit-1.2.8 → deskit-1.3}/setup.cfg +0 -0
  21. {deskit-1.2.8 → deskit-1.3}/src/deskit/__init__.py +0 -0
  22. {deskit-1.2.8 → deskit-1.3}/src/deskit/base/__init__.py +0 -0
  23. {deskit-1.2.8 → deskit-1.3}/src/deskit/base/base.py +0 -0
  24. {deskit-1.2.8 → deskit-1.3}/src/deskit/base/knnbase.py +0 -0
  25. {deskit-1.2.8 → deskit-1.3}/src/deskit/base/predictbase.py +0 -0
  26. {deskit-1.2.8 → deskit-1.3}/src/deskit/des/__init__.py +0 -0
  27. {deskit-1.2.8 → deskit-1.3}/src/deskit/metrics.py +0 -0
  28. {deskit-1.2.8 → deskit-1.3}/src/deskit/neighbors.py +0 -0
  29. {deskit-1.2.8 → deskit-1.3}/src/deskit/router.py +0 -0
  30. {deskit-1.2.8 → deskit-1.3}/src/deskit/utils.py +0 -0
  31. {deskit-1.2.8 → deskit-1.3}/src/deskit.egg-info/dependency_links.txt +0 -0
  32. {deskit-1.2.8 → deskit-1.3}/src/deskit.egg-info/requires.txt +0 -0
  33. {deskit-1.2.8 → deskit-1.3}/src/deskit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deskit
3
- Version: 1.2.8
3
+ Version: 1.3
4
4
  Summary: A Python library for Dynamic Ensemble Selection
5
5
  Author: Tikhon Vodyanov
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deskit"
7
- version = "1.2.8"
7
+ version = "1.3"
8
8
  description = "A Python library for Dynamic Ensemble Selection"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -4,6 +4,7 @@ Not part of the public API.
4
4
  """
5
5
  import numpy as np
6
6
  from deskit.metrics import _METRICS, _PROB_METRICS, _SCALAR_METRICS
7
+ from deskit.norms import _NORMS
7
8
  from deskit.utils import to_numpy
8
9
 
9
10
 
@@ -83,6 +84,32 @@ def resolve_metric(metric):
83
84
  return name, _METRICS[name]
84
85
  return None, metric
85
86
 
87
+ # ---------------------------------------------------------------------------
88
+ # Normalization resolution
89
+ # ---------------------------------------------------------------------------
90
+
91
+ def resolve_norm(norm):
92
+ """
93
+ Convert a normalization string or callable to (name_or_None, callable).
94
+
95
+ Returns
96
+ -------
97
+ norm_name : str or None
98
+ String name if normalization was passed as a string; None for callables.
99
+ norm_fn : callable
100
+ The actual normalization function.
101
+ """
102
+ if isinstance(norm, str):
103
+ name = norm.lower()
104
+ if name not in _NORMS:
105
+ raise ValueError(
106
+ f"Unknown normalization '{norm}'. "
107
+ f"Built-in options: {sorted(_NORMS)}. "
108
+ f"Pass a callable for custom normalizations."
109
+ )
110
+ return name, _NORMS[name]
111
+ return None, norm
112
+
86
113
 
87
114
  # ---------------------------------------------------------------------------
88
115
  # Neighbor finder construction
@@ -2,7 +2,7 @@
2
2
  DEWS-I: K-Nearest Neighbors with Distance-Weighted Softmax — Inverse-weighted.
3
3
  """
4
4
  from deskit.base.knnbase import KNNBase
5
- from deskit._config import make_finder, resolve_metric, prep_fit_inputs
5
+ from deskit._config import make_finder, resolve_metric, resolve_norm, prep_fit_inputs
6
6
  import numpy as np
7
7
 
8
8
 
@@ -44,14 +44,17 @@ class DEWSI(KNNBase):
44
44
  """
45
45
 
46
46
  def __init__(self, task, metric='mae', mode='min', k=10,
47
- threshold=0.5, temperature=None, preset='balanced', distance_metric='euclidian', **kwargs):
47
+ threshold=0.5, temperature=None, preset='balanced', distance_metric='euclidean', normalization='bestrel', **kwargs):
48
48
  metric_name, metric_fn = resolve_metric(metric)
49
+ norm_name, norm_fn = resolve_norm(normalization)
49
50
  finder = make_finder(preset, k, distance_metric=distance_metric, **kwargs)
50
51
  super().__init__(metric=metric_fn, mode=mode, neighbor_finder=finder, task=task)
51
52
  self.task = task
52
53
  self.threshold = threshold
53
54
  self._temperature = temperature
54
55
  self._metric_name = metric_name
56
+ self.norm = norm_fn
57
+ self.norm_name = norm_name
55
58
 
56
59
  def fit(self, features, y, preds_dict):
57
60
  """
@@ -72,7 +75,7 @@ class DEWSI(KNNBase):
72
75
  )
73
76
  super().fit(features, y, preds_dict)
74
77
 
75
- def _weights_batch(self, x, temperature=None, threshold=None, k=None, loo=False):
78
+ def _weights_batch(self, x, temperature=None, threshold=None, k=None, loo=False, normalization=None):
76
79
  """
77
80
  Core weight computation. x is a 2-D float64 numpy array (batch, n_features).
78
81
  Returns (batch, n_models) weight array.
@@ -87,6 +90,10 @@ class DEWSI(KNNBase):
87
90
  self._temperature if self._temperature is not None else
88
91
  (0.5 if self.mode == 'min' else 1.0))
89
92
  th = threshold if threshold is not None else self.threshold
93
+ if normalization is not None:
94
+ _, norm_fn = resolve_norm(normalization)
95
+ else:
96
+ norm_fn = self.norm
90
97
 
91
98
  distances, indices = self._kneighbors(x, k=k, loo=loo) # both (batch, k)
92
99
 
@@ -97,10 +104,7 @@ class DEWSI(KNNBase):
97
104
  avg_scores = (neighbor_scores * inv_dist_w[:, :, np.newaxis]).sum(axis=1) # (batch, n_models)
98
105
 
99
106
  # Normalize per neighborhood: best = 1.0, worst = 0.0
100
- local_min = avg_scores.min(axis=1, keepdims=True)
101
- local_max = avg_scores.max(axis=1, keepdims=True)
102
- local_range = local_max - local_min
103
- norm_scores = (avg_scores - local_min) / np.where(local_range > 0, local_range, 1.0)
107
+ norm_scores = norm_fn(avg_scores)
104
108
 
105
109
  # Zero out models below threshold; fall back to single best if none pass
106
110
  if th > 0:
@@ -2,7 +2,7 @@
2
2
  DEWS-IV: Distance-weighted Ensemble with Softmax — Inverse-distance + Variance-penalised.
3
3
  """
4
4
  from deskit.base.knnbase import KNNBase
5
- from deskit._config import make_finder, resolve_metric, prep_fit_inputs
5
+ from deskit._config import make_finder, resolve_metric, resolve_norm, prep_fit_inputs
6
6
  import numpy as np
7
7
 
8
8
 
@@ -46,8 +46,9 @@ class DEWSIV(KNNBase):
46
46
  """
47
47
 
48
48
  def __init__(self, task, metric='mae', mode='min', k=10,
49
- threshold=0.5, temperature=None, preset='balanced', distance_metric='euclidian', **kwargs):
49
+ threshold=0.5, temperature=None, preset='balanced', distance_metric='euclidean', normalization='bestrel', **kwargs):
50
50
  metric_name, metric_fn = resolve_metric(metric)
51
+ norm_name, norm_fn = resolve_norm(normalization)
51
52
  finder = make_finder(preset, k, distance_metric=distance_metric, **kwargs)
52
53
 
53
54
  self._use_signed = metric_name in _SIGNED_METRICS
@@ -59,6 +60,8 @@ class DEWSIV(KNNBase):
59
60
  self.threshold = threshold
60
61
  self._temperature = temperature
61
62
  self._var_matrix = None # (n_val, n_models) signed residuals, MAE/MSE only
63
+ self.norm = norm_fn
64
+ self.norm_name = norm_name
62
65
 
63
66
  def fit(self, features, y, preds_dict):
64
67
  """
@@ -87,7 +90,7 @@ class DEWSIV(KNNBase):
87
90
  preds = np.asarray(preds_dict[name])
88
91
  self._var_matrix[:, j] = np.vectorize(_signed_residual)(y, preds)
89
92
 
90
- def _weights_batch(self, x, temperature=None, threshold=None, k=None, loo=False):
93
+ def _weights_batch(self, x, temperature=None, threshold=None, k=None, loo=False, normalization=None):
91
94
  """
92
95
  Core weight computation. x is a 2-D float64 numpy array (batch, n_features).
93
96
  Returns (batch, n_models) weight array.
@@ -96,6 +99,10 @@ class DEWSIV(KNNBase):
96
99
  self._temperature if self._temperature is not None else
97
100
  (0.5 if self.mode == 'min' else 1.0))
98
101
  th = threshold if threshold is not None else self.threshold
102
+ if normalization is not None:
103
+ _, norm_fn = resolve_norm(normalization)
104
+ else:
105
+ norm_fn = self.norm
99
106
 
100
107
  distances, indices = self._kneighbors(x, k=k, loo=loo) # both (batch, k)
101
108
 
@@ -128,12 +135,11 @@ class DEWSIV(KNNBase):
128
135
  var_range = var_max - var_min
129
136
  norm_var = (local_var - var_min) / np.where(var_range > 0, var_range, 1.0)
130
137
 
131
- # Penalise inconsistent models, then re-normalize
132
- norm_scores = norm_scores / (1.0 + norm_var)
133
- local_min = norm_scores.min(axis=1, keepdims=True)
134
- local_max = norm_scores.max(axis=1, keepdims=True)
135
- local_range = local_max - local_min
136
- norm_scores = (norm_scores - local_min) / np.where(local_range > 0, local_range, 1.0)
138
+ # Penalise inconsistent models
139
+ penalized = norm_scores / (1.0 + norm_var)
140
+
141
+ # Apply the user-selected normalization
142
+ norm_scores = norm_fn(penalized)
137
143
 
138
144
  # Zero out models below threshold; fall back to single best if none pass
139
145
  if th > 0:
@@ -2,7 +2,7 @@
2
2
  DEWS-T: Distance-weighted Ensemble with Softmax — Trend.
3
3
  """
4
4
  from deskit.base.knnbase import KNNBase
5
- from deskit._config import make_finder, resolve_metric, prep_fit_inputs
5
+ from deskit._config import make_finder, resolve_metric, resolve_norm, prep_fit_inputs
6
6
  import numpy as np
7
7
 
8
8
 
@@ -50,9 +50,10 @@ class DEWST(KNNBase):
50
50
 
51
51
  def __init__(self, task, metric='mae', mode='min', k=10,
52
52
  threshold=0.5, temperature=None, r2_threshold=0.7,
53
- preset='balanced', distance_metric='euclidean', **kwargs):
53
+ preset='balanced', distance_metric='euclidean', normalization='bestrel', **kwargs):
54
54
  metric_name, metric_fn = resolve_metric(metric)
55
- finder = make_finder(preset, k, **kwargs)
55
+ norm_name, norm_fn = resolve_norm(normalization)
56
+ finder = make_finder(preset, k, distance_metric=distance_metric, **kwargs)
56
57
 
57
58
  self._use_signed = metric_name in _SIGNED_METRICS
58
59
  self._metric_name = metric_name
@@ -71,6 +72,9 @@ class DEWST(KNNBase):
71
72
  self._temperature = temperature
72
73
  self.r2_threshold = r2_threshold
73
74
 
75
+ self.norm = norm_fn
76
+ self.norm_name = norm_name
77
+
74
78
  def fit(self, features, y, preds_dict):
75
79
  """
76
80
  Fit the routing model on validation data.
@@ -89,7 +93,7 @@ class DEWST(KNNBase):
89
93
  )
90
94
  super().fit(features, y, preds_dict)
91
95
 
92
- def _weights_batch(self, x, temperature=None, threshold=None, k=None, r2_threshold=None, loo=False):
96
+ def _weights_batch(self, x, temperature=None, threshold=None, k=None, r2_threshold=None, loo=False, normalization=None):
93
97
  """
94
98
  Core weight computation. x is a 2-D float64 numpy array (batch, n_features).
95
99
  Returns (batch, n_models) weight array.
@@ -99,6 +103,10 @@ class DEWST(KNNBase):
99
103
  (0.5 if self._real_mode == 'min' else 1.0))
100
104
  th = threshold if threshold is not None else self.threshold
101
105
  r2_th = r2_threshold if r2_threshold is not None else self.r2_threshold
106
+ if normalization is not None:
107
+ _, norm_fn = resolve_norm(normalization)
108
+ else:
109
+ norm_fn = self.norm
102
110
 
103
111
  distances, indices = self._kneighbors(x, k=k, loo=loo) # (batch, k)
104
112
  k = distances.shape[1]
@@ -164,18 +172,17 @@ class DEWST(KNNBase):
164
172
  use_trend = r2 >= r2_th
165
173
  avg_scores = np.where(use_trend, trend_scores, dewsi_scores)
166
174
 
167
- # Standard DEWS softmax
168
- local_min = avg_scores.min(axis=1, keepdims=True)
169
- local_max = avg_scores.max(axis=1, keepdims=True)
170
- local_range = local_max - local_min
171
- norm_scores = (avg_scores - local_min) / np.where(local_range > 0, local_range, 1.0)
175
+ # Normalize
176
+ norm_scores = norm_fn(avg_scores)
172
177
 
178
+ # Threshold
173
179
  if th > 0:
174
180
  gate = norm_scores >= th
175
181
  any_pass = gate.any(axis=1, keepdims=True)
176
182
  gate = np.where(any_pass, gate, norm_scores == 1.0)
177
183
  norm_scores = norm_scores * gate
178
184
 
185
+ # Softmax
179
186
  max_scores = norm_scores.max(axis=1, keepdims=True)
180
187
  exp_scores = np.exp((norm_scores - max_scores) / t)
181
188
  if th > 0:
@@ -2,7 +2,7 @@
2
2
  DEWS-U: K-Nearest Neighbors with Distance-Weighted Softmax.
3
3
  """
4
4
  from deskit.base.knnbase import KNNBase
5
- from deskit._config import make_finder, resolve_metric, prep_fit_inputs
5
+ from deskit._config import make_finder, resolve_metric, resolve_norm, prep_fit_inputs
6
6
  import numpy as np
7
7
 
8
8
 
@@ -40,14 +40,17 @@ class DEWSU(KNNBase):
40
40
  """
41
41
 
42
42
  def __init__(self, task, metric='mae', mode='min', k=10,
43
- threshold=0.5, temperature=None, preset='balanced', distance_metric='euclidean', **kwargs):
43
+ threshold=0.5, temperature=None, preset='balanced', distance_metric='euclidean', normalization='bestrel', **kwargs):
44
44
  metric_name, metric_fn = resolve_metric(metric)
45
- finder = make_finder(preset, k, **kwargs)
45
+ norm_name, norm_fn = resolve_norm(normalization)
46
+ finder = make_finder(preset, k, distance_metric=distance_metric, **kwargs)
46
47
  super().__init__(metric=metric_fn, mode=mode, neighbor_finder=finder, task=task)
47
48
  self.task = task
48
49
  self.threshold = threshold
49
50
  self._temperature = temperature
50
51
  self._metric_name = metric_name
52
+ self.norm = norm_fn
53
+ self.norm_name = norm_name
51
54
 
52
55
  def fit(self, features, y, preds_dict):
53
56
  """
@@ -68,7 +71,7 @@ class DEWSU(KNNBase):
68
71
  )
69
72
  super().fit(features, y, preds_dict)
70
73
 
71
- def _weights_batch(self, x, temperature=None, threshold=None, k=None, loo=False):
74
+ def _weights_batch(self, x, temperature=None, threshold=None, k=None, loo=False, normalization=None):
72
75
  """
73
76
  Core weight computation. x is a 2-D float64 numpy array (batch, n_features).
74
77
  Returns (batch, n_models) weight array.
@@ -77,6 +80,10 @@ class DEWSU(KNNBase):
77
80
  self._temperature if self._temperature is not None else
78
81
  (0.5 if self.mode == 'min' else 1.0))
79
82
  th = threshold if threshold is not None else self.threshold
83
+ if normalization is not None:
84
+ _, norm_fn = resolve_norm(normalization)
85
+ else:
86
+ norm_fn = self.norm
80
87
 
81
88
  _, indices = self._kneighbors(x, k=k, loo=loo) # (batch, k)
82
89
 
@@ -84,16 +91,14 @@ class DEWSU(KNNBase):
84
91
  avg_scores = self.matrix[indices].mean(axis=1) # (batch, n_models)
85
92
 
86
93
  # Normalize per neighborhood: best = 1.0, worst = 0.0
87
- local_min = avg_scores.min(axis=1, keepdims=True)
88
- local_max = avg_scores.max(axis=1, keepdims=True)
89
- local_range = local_max - local_min
90
- norm_scores = (avg_scores - local_min) / np.where(local_range > 0, local_range, 1.0)
94
+
95
+ norm_scores = norm_fn(avg_scores)
91
96
 
92
97
  # Zero out models below threshold; fall back to single best if none pass
93
98
  if th > 0:
94
99
  gate = norm_scores >= th
95
100
  any_pass = gate.any(axis=1, keepdims=True)
96
- gate = np.where(any_pass, gate, norm_scores == 1.0)
101
+ gate = np.where(any_pass, gate, norm_scores == norm_scores.max(axis=1, keepdims=True))
97
102
  norm_scores = norm_scores * gate
98
103
 
99
104
  # Softmax
@@ -2,7 +2,7 @@
2
2
  DEWS-V: Distance-weighted Ensemble with Softmax — Variance-penalised.
3
3
  """
4
4
  from deskit.base.knnbase import KNNBase
5
- from deskit._config import make_finder, resolve_metric, prep_fit_inputs
5
+ from deskit._config import make_finder, resolve_metric, resolve_norm, prep_fit_inputs
6
6
  import numpy as np
7
7
 
8
8
 
@@ -46,8 +46,9 @@ class DEWSV(KNNBase):
46
46
  """
47
47
 
48
48
  def __init__(self, task, metric='mae', mode='min', k=10,
49
- threshold=0.5, temperature=None, preset='balanced', distance_metric='euclidean', **kwargs):
49
+ threshold=0.5, temperature=None, preset='balanced', distance_metric='euclidean', normalization='bestrel', **kwargs):
50
50
  metric_name, metric_fn = resolve_metric(metric)
51
+ norm_name, norm_fn = resolve_norm(normalization)
51
52
  finder = make_finder(preset, k, distance_metric=distance_metric, **kwargs)
52
53
 
53
54
  self._use_signed = metric_name in _SIGNED_METRICS
@@ -60,6 +61,9 @@ class DEWSV(KNNBase):
60
61
  self._temperature = temperature
61
62
  self._var_matrix = None # (n_val, n_models) signed residuals, MAE/MSE only
62
63
 
64
+ self.norm = norm_fn
65
+ self.norm_name = norm_name
66
+
63
67
  def fit(self, features, y, preds_dict):
64
68
  """
65
69
  Fit the routing model on validation data.
@@ -87,7 +91,7 @@ class DEWSV(KNNBase):
87
91
  preds = np.asarray(preds_dict[name])
88
92
  self._var_matrix[:, j] = np.vectorize(_signed_residual)(y, preds)
89
93
 
90
- def _weights_batch(self, x, temperature=None, threshold=None, k=None, loo=False):
94
+ def _weights_batch(self, x, temperature=None, threshold=None, k=None, loo=False, normalization=None):
91
95
  """
92
96
  Core weight computation. x is a 2-D float64 numpy array (batch, n_features).
93
97
  Returns (batch, n_models) weight array.
@@ -96,6 +100,10 @@ class DEWSV(KNNBase):
96
100
  self._temperature if self._temperature is not None else
97
101
  (0.5 if self.mode == 'min' else 1.0))
98
102
  th = threshold if threshold is not None else self.threshold
103
+ if normalization is not None:
104
+ _, norm_fn = resolve_norm(normalization)
105
+ else:
106
+ norm_fn = self.norm
99
107
 
100
108
  _, indices = self._kneighbors(x, k=k, loo=loo) # (batch, k)
101
109
 
@@ -119,12 +127,11 @@ class DEWSV(KNNBase):
119
127
  var_range = var_max - var_min
120
128
  norm_var = (local_var - var_min) / np.where(var_range > 0, var_range, 1.0)
121
129
 
122
- # Penalise inconsistent models, then re-normalize
123
- norm_scores = norm_scores / (1.0 + norm_var)
124
- local_min = norm_scores.min(axis=1, keepdims=True)
125
- local_max = norm_scores.max(axis=1, keepdims=True)
126
- local_range = local_max - local_min
127
- norm_scores = (norm_scores - local_min) / np.where(local_range > 0, local_range, 1.0)
130
+ # Penalise inconsistent models
131
+ penalized = norm_scores / (1.0 + norm_var)
132
+
133
+ # Apply the user-selected normalization
134
+ norm_scores = norm_fn(penalized)
128
135
 
129
136
  # Zero out models below threshold; fall back to single best if none pass
130
137
  if th > 0:
@@ -2,7 +2,7 @@
2
2
  KNORA-E: K-Nearest Oracles — Eliminate.
3
3
  """
4
4
  from deskit.base.knnbase import KNNBase
5
- from deskit._config import make_finder, resolve_metric, prep_fit_inputs
5
+ from deskit._config import make_finder, resolve_metric, resolve_norm, prep_fit_inputs
6
6
  import numpy as np
7
7
 
8
8
 
@@ -35,14 +35,18 @@ class KNORAE(KNNBase):
35
35
  """
36
36
 
37
37
  def __init__(self, task, metric='mae', mode='min', k=10,
38
- threshold=0.5, preset='balanced', distance_metric='euclidean', **kwargs):
38
+ threshold=0.5, preset='balanced', distance_metric='euclidean', normalization='bestrel', **kwargs):
39
39
  metric_name, metric_fn = resolve_metric(metric)
40
- finder = make_finder(preset, k, **kwargs)
41
- super().__init__(metric=metric_fn, mode=mode, neighbor_finder=finder, task=task)
40
+ norm_name, norm_fn = resolve_norm(normalization)
41
+ finder = make_finder(preset, k, distance_metric=distance_metric, **kwargs)
42
+ super().__init__(metric=metric_fn, mode=mode, neighbor_finder=finder, normalization='bestrel', task=task)
42
43
  self.task = task
43
44
  self.threshold = threshold
44
45
  self._metric_name = metric_name
45
46
 
47
+ self.norm = norm_fn
48
+ self.norm_name = norm_name
49
+
46
50
  def fit(self, features, y, preds_dict):
47
51
  """
48
52
  Fit the routing model on validation data.
@@ -58,13 +62,17 @@ class KNORAE(KNNBase):
58
62
  )
59
63
  super().fit(features, y, preds_dict)
60
64
 
61
- def _weights_batch(self, x, temperature=None, threshold=None, k=None, loo=False):
65
+ def _weights_batch(self, x, temperature=None, threshold=None, k=None, loo=False, normalization=None):
62
66
  """
63
67
  Core weight computation. x is a 2-D float64 numpy array (batch, n_features).
64
68
  Returns (batch, n_models) weight array.
65
69
  temperature is accepted for API compatibility but has no effect.
66
70
  """
67
71
  th = threshold if threshold is not None else self.threshold
72
+ if normalization is not None:
73
+ _, norm_fn = resolve_norm(normalization)
74
+ else:
75
+ norm_fn = self.norm
68
76
  n_models = len(self.models)
69
77
 
70
78
  _, indices = self._kneighbors(x, k=k, loo=loo)
@@ -72,12 +80,7 @@ class KNORAE(KNNBase):
72
80
  neighbor_scores = self.matrix[indices] # (batch, k, n_models)
73
81
 
74
82
  # Normalize per neighbor: best model = 1.0, worst = 0.0
75
- n_min = neighbor_scores.min(axis=2, keepdims=True)
76
- n_max = neighbor_scores.max(axis=2, keepdims=True)
77
- n_range = n_max - n_min
78
- norm = np.where(n_range > 0,
79
- (neighbor_scores - n_min) / n_range,
80
- 1.0)
83
+ norm = norm_fn(neighbor_scores)
81
84
 
82
85
  competent = norm >= th # (batch, k, n_models)
83
86
  batch_size = x.shape[0]
@@ -2,7 +2,7 @@
2
2
  KNORA-IU: K-Nearest Oracles — Inverse-weighted Union.
3
3
  """
4
4
  from deskit.base.knnbase import KNNBase
5
- from deskit._config import make_finder, resolve_metric, prep_fit_inputs
5
+ from deskit._config import make_finder, resolve_metric, resolve_norm, prep_fit_inputs
6
6
  import numpy as np
7
7
 
8
8
 
@@ -36,14 +36,18 @@ class KNORAIU(KNNBase):
36
36
  """
37
37
 
38
38
  def __init__(self, task, metric='mae', mode='min', k=10,
39
- threshold=0.5, preset='balanced', distance_metric='euclidean', **kwargs):
39
+ threshold=0.5, preset='balanced', distance_metric='euclidean', normalization='bestrel', **kwargs):
40
40
  metric_name, metric_fn = resolve_metric(metric)
41
- finder = make_finder(preset, k, **kwargs)
41
+ norm_name, norm_fn = resolve_norm(normalization)
42
+ finder = make_finder(preset, k, distance_metric=distance_metric, **kwargs)
42
43
  super().__init__(metric=metric_fn, mode=mode, neighbor_finder=finder, task=task)
43
44
  self.task = task
44
45
  self.threshold = threshold
45
46
  self._metric_name = metric_name
46
47
 
48
+ self.norm = norm_fn
49
+ self.norm_name = norm_name
50
+
47
51
  def fit(self, features, y, preds_dict):
48
52
  """
49
53
  Fit the routing model on validation data.
@@ -59,24 +63,23 @@ class KNORAIU(KNNBase):
59
63
  )
60
64
  super().fit(features, y, preds_dict)
61
65
 
62
- def _weights_batch(self, x, temperature=None, threshold=None, k=None, loo=False):
66
+ def _weights_batch(self, x, temperature=None, threshold=None, k=None, loo=False, normalization=None):
63
67
  """
64
68
  Core weight computation. x is a 2-D float64 numpy array (batch, n_features).
65
69
  Returns (batch, n_models) weight array.
66
70
  temperature is accepted for API compatibility but has no effect.
67
71
  """
68
72
  th = threshold if threshold is not None else self.threshold
73
+ if normalization is not None:
74
+ _, norm_fn = resolve_norm(normalization)
75
+ else:
76
+ norm_fn = self.norm
69
77
 
70
78
  distances, indices = self._kneighbors(x, k=k, loo=loo) # both (batch, k)
71
79
  neighbor_scores = self.matrix[indices] # (batch, k, n_models)
72
80
 
73
81
  # Normalize per neighbor: best model = 1.0, worst = 0.0
74
- n_min = neighbor_scores.min(axis=2, keepdims=True)
75
- n_max = neighbor_scores.max(axis=2, keepdims=True)
76
- n_range = n_max - n_min
77
- norm = np.where(n_range > 0,
78
- (neighbor_scores - n_min) / n_range,
79
- 1.0)
82
+ norm = norm_fn(neighbor_scores)
80
83
 
81
84
  competent = norm >= th # (batch, k, n_models)
82
85
 
@@ -2,7 +2,7 @@
2
2
  KNORA-U: K-Nearest Oracles — Union.
3
3
  """
4
4
  from deskit.base.knnbase import KNNBase
5
- from deskit._config import make_finder, resolve_metric, prep_fit_inputs
5
+ from deskit._config import make_finder, resolve_metric, resolve_norm, prep_fit_inputs
6
6
  import numpy as np
7
7
 
8
8
 
@@ -36,14 +36,18 @@ class KNORAU(KNNBase):
36
36
  """
37
37
 
38
38
  def __init__(self, task, metric='mae', mode='min', k=10,
39
- threshold=0.5, preset='balanced', distance_metric='euclidean', **kwargs):
39
+ threshold=0.5, preset='balanced', distance_metric='euclidean', normalization='bestrel', **kwargs):
40
40
  metric_name, metric_fn = resolve_metric(metric)
41
- finder = make_finder(preset, k, **kwargs)
41
+ norm_name, norm_fn = resolve_norm(normalization)
42
+ finder = make_finder(preset, k, distance_metric=distance_metric, **kwargs)
42
43
  super().__init__(metric=metric_fn, mode=mode, neighbor_finder=finder, task=task)
43
44
  self.task = task
44
45
  self.threshold = threshold
45
46
  self._metric_name = metric_name
46
47
 
48
+ self.norm = norm_fn
49
+ self.norm_name = norm_name
50
+
47
51
  def fit(self, features, y, preds_dict):
48
52
  """
49
53
  Fit the routing model on validation data.
@@ -59,24 +63,23 @@ class KNORAU(KNNBase):
59
63
  )
60
64
  super().fit(features, y, preds_dict)
61
65
 
62
- def _weights_batch(self, x, temperature=None, threshold=None, k=None, loo=False):
66
+ def _weights_batch(self, x, temperature=None, threshold=None, k=None, loo=False, normalization=None):
63
67
  """
64
68
  Core weight computation. x is a 2-D float64 numpy array (batch, n_features).
65
69
  Returns (batch, n_models) weight array.
66
70
  temperature is accepted for API compatibility but has no effect.
67
71
  """
68
72
  th = threshold if threshold is not None else self.threshold
73
+ if normalization is not None:
74
+ _, norm_fn = resolve_norm(normalization)
75
+ else:
76
+ norm_fn = self.norm
69
77
 
70
78
  _, indices = self._kneighbors(x, k=k, loo=loo)
71
79
  neighbor_scores = self.matrix[indices] # (batch, k, n_models)
72
80
 
73
81
  # Normalize per neighbor: best model = 1.0, worst = 0.0
74
- n_min = neighbor_scores.min(axis=2, keepdims=True)
75
- n_max = neighbor_scores.max(axis=2, keepdims=True)
76
- n_range = n_max - n_min
77
- norm = np.where(n_range > 0,
78
- (neighbor_scores - n_min) / n_range,
79
- 1.0) # tied → all equally competent
82
+ norm = norm_fn(neighbor_scores)
80
83
 
81
84
  # votes[b, j] = number of neighbours where model j exceeds the threshold
82
85
  votes = (norm >= th).sum(axis=1).astype(float) # (batch, n_models)
@@ -30,7 +30,7 @@ class LWSEI(PredictBase):
30
30
  def __init__(self, task, k=10, preset='balanced', distance_metric='euclidean', **kwargs):
31
31
  self.task = task
32
32
  self.k = k
33
- self._finder = make_finder(preset, k, **kwargs)
33
+ self._finder = make_finder(preset, k, distance_metric=distance_metric, **kwargs)
34
34
  self.models = None
35
35
 
36
36
  self._val_preds = None # (n_val, n_models) or (n_val, n_models, n_classes)
@@ -30,7 +30,7 @@ class LWSEU(PredictBase):
30
30
  def __init__(self, task, k=10, preset='balanced', distance_metric='euclidean', **kwargs):
31
31
  self.task = task
32
32
  self.k = k
33
- self._finder = make_finder(preset, k, **kwargs)
33
+ self._finder = make_finder(preset, k, distance_metric=distance_metric, **kwargs)
34
34
  self.models = None
35
35
 
36
36
  self._val_preds = None # (n_val, n_models) or (n_val, n_models, n_classes)
@@ -33,7 +33,7 @@ class OLA(KNNBase):
33
33
  def __init__(self, task, metric='mae', mode='min', k=10,
34
34
  preset='balanced', threshold=None, distance_metric='euclidean', **kwargs):
35
35
  metric_name, metric_fn = resolve_metric(metric)
36
- finder = make_finder(preset, k, **kwargs)
36
+ finder = make_finder(preset, k, distance_metric=distance_metric, **kwargs)
37
37
  super().__init__(metric=metric_fn, mode=mode, neighbor_finder=finder, task=task)
38
38
  self.task = task
39
39
  self._metric_name = metric_name
@@ -0,0 +1,82 @@
1
+ """
2
+ Normalization functions for Dynamic Ensemble Selection.
3
+
4
+ All functions operate row-wise: each row corresponds to a query instance,
5
+ and columns are the base models. They return a matrix of the same shape,
6
+ with values in a suitable range (usually [0,1]) and "higher = better".
7
+
8
+ Built‑in options:
9
+ 'minmax' : classic min‑max scaling per row → [0,1]
10
+ 'zscore' : z‑score per row (mean 0, std 1) – may be negative
11
+ 'bestrel' : ratio‑to‑best, automatically handles sign -> [0,1]:
12
+ - if scores are all negative (e.g., -errors): best / score
13
+ - if scores are all non‑negative (e.g., V/IV): score / best
14
+ 'softmax' : softmax with temperature=1 (optional)
15
+ 'rank' : rank‑based (0 for worst, 1 for best) – robust to outliers
16
+
17
+ Custom functions can be passed via the `normalization` parameter in DEWS.
18
+ """
19
+
20
+ import numpy as np
21
+
22
+ EPS = 1e-12
23
+
24
+ def minmax(x):
25
+ """Row‑wise min‑max scaling to [0,1]."""
26
+ x_min = x.min(axis=1, keepdims=True)
27
+ x_max = x.max(axis=1, keepdims=True)
28
+ x_range = x_max - x_min
29
+ # Avoid division by zero (if range is 0, set to 1.0)
30
+ safe_range = np.where(x_range > 0, x_range, 1.0)
31
+ return (x - x_min) / safe_range
32
+
33
+ def zscore(x):
34
+ """Row‑wise z‑score (standard score) with bias‑corrected std."""
35
+ mean = x.mean(axis=1, keepdims=True)
36
+ std = x.std(axis=1, keepdims=True, ddof=1) # sample std
37
+ safe_std = np.where(std > EPS, std, 1.0)
38
+ return (x - mean) / safe_std
39
+
40
+ def bestrel(x):
41
+ """
42
+ Ratio‑to‑best normalization.
43
+
44
+ """
45
+ max_val = x.max(axis=1, keepdims=True)
46
+ # Determine per‑row which normalization to use based on the max
47
+ eps = 1e-12
48
+ safe_x = np.minimum(x, -eps)
49
+ norm_neg = max_val / safe_x # this will be <=1 because max is the least negative
50
+ safe_max = np.where(max_val > eps, max_val, eps)
51
+ norm_pos = x / safe_max # will be in [0,1]
52
+
53
+ neg_mask = max_val < -eps
54
+ result = np.where(neg_mask, norm_neg, norm_pos)
55
+ return result
56
+
57
+ def softmax(x, temperature=1.0):
58
+ """Row‑wise softmax with optional temperature (higher temperature → more uniform)."""
59
+ x_scaled = x / temperature
60
+ x_shifted = x_scaled - x_scaled.max(axis=1, keepdims=True) # stability
61
+ exp_x = np.exp(x_shifted)
62
+ return exp_x / exp_x.sum(axis=1, keepdims=True)
63
+
64
+ def rank_norm(x):
65
+ """
66
+ Convert each row to fractional ranks: worst → 0, best → 1.
67
+ Ties are averaged.
68
+ """
69
+ # Get ranks along axis=1 (method='average' gives average rank for ties)
70
+ ranks = x.argsort(axis=1).argsort(axis=1) + 1 # ranks from 1 to M
71
+ # Convert to [0,1] with 1 for best (highest original value)
72
+ M = x.shape[1]
73
+ # For 'higher is better', we invert: rank 1 (lowest original) gets 0, rank M (highest) gets 1
74
+ return (ranks - 1) / (M - 1) if M > 1 else np.ones_like(x) * 0.5
75
+
76
+ _NORMS = {
77
+ 'minmax': minmax,
78
+ 'z': zscore,
79
+ 'bestrel': bestrel,
80
+ 'softmax': softmax,
81
+ 'rank_norm': rank_norm
82
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deskit
3
- Version: 1.2.8
3
+ Version: 1.3
4
4
  Summary: A Python library for Dynamic Ensemble Selection
5
5
  Author: Tikhon Vodyanov
6
6
  License-Expression: MIT
@@ -5,6 +5,7 @@ src/deskit/__init__.py
5
5
  src/deskit/_config.py
6
6
  src/deskit/metrics.py
7
7
  src/deskit/neighbors.py
8
+ src/deskit/norms.py
8
9
  src/deskit/router.py
9
10
  src/deskit/utils.py
10
11
  src/deskit.egg-info/PKG-INFO
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes