From ea1ee5d34b879c37ef30ef3a0f667dbc2cf2b367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Sevilla=20Molina?= Date: Thu, 11 Sep 2025 09:06:02 +0200 Subject: [PATCH 1/9] ENH: Use validate_data and set parameters=None by default --- .../classifiers/OrdinalDecomposition.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/orca_python/classifiers/OrdinalDecomposition.py b/orca_python/classifiers/OrdinalDecomposition.py index 9ea2fbe..715c3e9 100644 --- a/orca_python/classifiers/OrdinalDecomposition.py +++ b/orca_python/classifiers/OrdinalDecomposition.py @@ -3,7 +3,7 @@ import numpy as np from sklearn.base import BaseEstimator, ClassifierMixin, _fit_context from sklearn.utils._param_validation import StrOptions -from sklearn.utils.validation import check_array, check_is_fitted, check_X_y +from sklearn.utils.validation import check_is_fitted, validate_data from orca_python.model_selection import load_classifier @@ -103,7 +103,7 @@ class OrdinalDecomposition(BaseEstimator, ClassifierMixin): ) ], "base_classifier": [str], - "parameters": [dict], + "parameters": [dict, None], } def __init__( @@ -111,7 +111,7 @@ def __init__( dtype="ordered_partitions", decision_method="frank_hall", base_classifier="LogisticRegression", - parameters={}, + parameters=None, ): self.dtype = dtype self.decision_method = decision_method @@ -142,7 +142,9 @@ def fit(self, X, y): If parameters are invalid or data has wrong format. """ - X, y = check_X_y(X, y) + X, y = validate_data( + self, X, y, accept_sparse=False, ensure_2d=True, dtype=None + ) self.X_ = X self.y_ = y @@ -158,12 +160,12 @@ def fit(self, X, y): class_labels = self.coding_matrix_[(np.digitize(y, self.classes_) - 1), :] self.classifiers_ = [] + parameters = {} if self.parameters is None else self.parameters + # Fitting n_targets - 1 classifiers for n in range(len(class_labels[0, :])): - estimator = load_classifier( - self.base_classifier, param_grid=self.parameters - ) + estimator = load_classifier(self.base_classifier, param_grid=parameters) estimator.fit( X[np.where(class_labels[:, n] != 0)], np.ravel(class_labels[np.where(class_labels[:, n] != 0), n].T), @@ -200,7 +202,7 @@ def predict(self, X): """ check_is_fitted(self, ["X_", "y_"]) - X = check_array(X) + X = validate_data(self, X, reset=False, ensure_2d=True, dtype=None) # Getting predicted labels for dataset from each classifier predictions = self._get_predictions(X) @@ -274,7 +276,7 @@ def predict_proba(self, X): """ check_is_fitted(self, ["X_", "y_"]) - X = check_array(X) + X = validate_data(self, X, reset=False, ensure_2d=True, dtype=None) # Getting predicted labels for dataset from each classifier predictions = self._get_predictions(X) From 8a03e3ae94d7712fd2f785b9806a3bbcdc8782bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Sevilla=20Molina?= Date: Thu, 11 Sep 2025 09:10:50 +0200 Subject: [PATCH 2/9] REF: Drop X_ and y_ and rename classifiers_ to estimators_ --- .../classifiers/OrdinalDecomposition.py | 22 +++++-------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/orca_python/classifiers/OrdinalDecomposition.py b/orca_python/classifiers/OrdinalDecomposition.py index 715c3e9..015f4ce 100644 --- a/orca_python/classifiers/OrdinalDecomposition.py +++ b/orca_python/classifiers/OrdinalDecomposition.py @@ -66,17 +66,10 @@ class OrdinalDecomposition(BaseEstimator, ClassifierMixin): subproblem, and in which binary class they belong inside those new models. Further explained previously. - classifiers_ : list of classifiers + estimators_ : list of classifiers Initially empty, will include all fitted models for each subproblem once the fit function for this class is called successfully. - X_ : array-like, shape (n_samples, n_features) - Training patterns array, where n_samples is the number of samples and - n_features is the number of features. - - y_ : array-like, shape (n_samples,) - Target vector relative to X. - References ---------- .. [1] P.A. Gutierrez, M. Perez-Ortiz, J. Sanchez-Monedero, F. Fernandez-Navarro @@ -146,9 +139,6 @@ def fit(self, X, y): self, X, y, accept_sparse=False, ensure_2d=True, dtype=None ) - self.X_ = X - self.y_ = y - # Get list of different labels of the dataset self.classes_ = np.unique(y) @@ -159,7 +149,7 @@ def fit(self, X, y): ) class_labels = self.coding_matrix_[(np.digitize(y, self.classes_) - 1), :] - self.classifiers_ = [] + self.estimators_ = [] parameters = {} if self.parameters is None else self.parameters # Fitting n_targets - 1 classifiers @@ -171,7 +161,7 @@ def fit(self, X, y): np.ravel(class_labels[np.where(class_labels[:, n] != 0), n].T), ) - self.classifiers_.append(estimator) + self.estimators_.append(estimator) return self @@ -201,7 +191,7 @@ def predict(self, X): If the specified loss method is not implemented. """ - check_is_fitted(self, ["X_", "y_"]) + check_is_fitted(self, ["estimators_", "classes_", "coding_matrix_"]) X = validate_data(self, X, reset=False, ensure_2d=True, dtype=None) # Getting predicted labels for dataset from each classifier @@ -275,7 +265,7 @@ def predict_proba(self, X): If the specified loss method is not implemented. """ - check_is_fitted(self, ["X_", "y_"]) + check_is_fitted(self, ["estimators_", "classes_", "coding_matrix_"]) X = validate_data(self, X, reset=False, ensure_2d=True, dtype=None) # Getting predicted labels for dataset from each classifier @@ -406,7 +396,7 @@ def _get_predictions(self, X): """ predictions = np.array( - list(map(lambda c: c.predict_proba(X)[:, 1], self.classifiers_)) + list(map(lambda c: c.predict_proba(X)[:, 1], self.estimators_)) ).T return predictions From ba9457ef504868be754c5e300491220b32c99086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Sevilla=20Molina?= Date: Thu, 11 Sep 2025 09:21:54 +0200 Subject: [PATCH 3/9] ENH: Add validation checks --- .../classifiers/OrdinalDecomposition.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/orca_python/classifiers/OrdinalDecomposition.py b/orca_python/classifiers/OrdinalDecomposition.py index 015f4ce..814142b 100644 --- a/orca_python/classifiers/OrdinalDecomposition.py +++ b/orca_python/classifiers/OrdinalDecomposition.py @@ -141,12 +141,19 @@ def fit(self, X, y): # Get list of different labels of the dataset self.classes_ = np.unique(y) + if self.classes_.size < 2: + raise ValueError("OrdinalDecomposition requires at least 2 classes.") + + dtype = str(self.dtype).lower() + decision = str(self.decision_method).lower() + if decision == "frank_hall" and dtype != "ordered_partitions": + raise ValueError( + 'decision_method="frank_hall" requires dtype="ordered_partitions".' + ) # Give each train input its corresponding output label # for each binary classifier - self.coding_matrix_ = self._coding_matrix( - self.dtype.lower(), len(self.classes_) - ) + self.coding_matrix_ = self._coding_matrix(dtype, len(self.classes_)) class_labels = self.coding_matrix_[(np.digitize(y, self.classes_) - 1), :] self.estimators_ = [] @@ -154,8 +161,12 @@ def fit(self, X, y): # Fitting n_targets - 1 classifiers for n in range(len(class_labels[0, :])): - estimator = load_classifier(self.base_classifier, param_grid=parameters) + if not hasattr(estimator, "predict_proba"): + raise TypeError( + f'Base estimator "{self.base_classifier}" must implement predict_proba.' + ) + estimator.fit( X[np.where(class_labels[:, n] != 0)], np.ravel(class_labels[np.where(class_labels[:, n] != 0), n].T), From e62589ae2a2864f1f7a788c5e086d8ebb1ff3f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Sevilla=20Molina?= Date: Thu, 11 Sep 2025 10:54:47 +0200 Subject: [PATCH 4/9] REF: Vectorize loss functions --- .../classifiers/OrdinalDecomposition.py | 62 ++++--------------- .../tests/test_ordinal_decomposition.py | 14 +++-- 2 files changed, 19 insertions(+), 57 deletions(-) diff --git a/orca_python/classifiers/OrdinalDecomposition.py b/orca_python/classifiers/OrdinalDecomposition.py index 814142b..a5cc79c 100644 --- a/orca_python/classifiers/OrdinalDecomposition.py +++ b/orca_python/classifiers/OrdinalDecomposition.py @@ -148,7 +148,8 @@ def fit(self, X, y): decision = str(self.decision_method).lower() if decision == "frank_hall" and dtype != "ordered_partitions": raise ValueError( - 'decision_method="frank_hall" requires dtype="ordered_partitions".' + "When using Frank and Hall decision method,\ + ordered_partitions must be used" ) # Give each train input its corresponding output label @@ -431,18 +432,9 @@ def _exponential_loss(self, predictions): each class label. """ - # Computing exponential losses - e_losses = np.zeros((predictions.shape[0], (predictions.shape[1] + 1))) - for i in range(predictions.shape[1] + 1): - - e_losses[:, i] = np.sum( - np.exp( - -predictions - * np.tile(self.coding_matrix_[i, :], (predictions.shape[0], 1)) - ), - axis=1, - ) - + C = self.coding_matrix_[None, :, :] + M = predictions[:, None, :] + e_losses = np.exp(-M * C).sum(axis=2) return e_losses def _hinge_loss(self, predictions): @@ -464,22 +456,9 @@ def _hinge_loss(self, predictions): class label. """ - # Computing Hinge losses - h_losses = np.zeros((predictions.shape[0], (predictions.shape[1] + 1))) - for i in range(predictions.shape[1] + 1): - - h_losses[:, i] = np.sum( - np.maximum( - 0, - ( - 1 - - np.tile(self.coding_matrix_[i, :], (predictions.shape[0], 1)) - * predictions - ), - ), - axis=1, - ) - + C = self.coding_matrix_[None, :, :] + M = predictions[:, None, :] + h_losses = np.maximum(0.0, 1.0 - C * M).sum(axis=2) return h_losses def _logarithmic_loss(self, predictions): @@ -501,22 +480,9 @@ def _logarithmic_loss(self, predictions): each class label. """ - # Computing logarithmic losses - l_losses = np.zeros((predictions.shape[0], (predictions.shape[1] + 1))) - for i in range(predictions.shape[1] + 1): - - l_losses[:, i] = np.sum( - np.log( - 1 - + np.exp( - -2 - * np.tile(self.coding_matrix_[i, :], (predictions.shape[0], 1)) - * predictions - ) - ), - axis=1, - ) - + C = self.coding_matrix_[None, :, :] + M = predictions[:, None, :] + l_losses = np.log1p(np.exp(-2.0 * C * M)).sum(axis=2) return l_losses def _frank_hall_method(self, predictions): @@ -542,12 +508,6 @@ def _frank_hall_method(self, predictions): If the decomposition type is not ordered_partitions. """ - if self.dtype.lower() != "ordered_partitions": - raise AttributeError( - "When using Frank and Hall decision method,\ - ordered_partitions must be used" - ) - y_proba = np.empty([(predictions.shape[0]), (predictions.shape[1] + 1)]) # Probabilities of each set to belong to the first ordinal class diff --git a/orca_python/classifiers/tests/test_ordinal_decomposition.py b/orca_python/classifiers/tests/test_ordinal_decomposition.py index b79d567..67f5786 100644 --- a/orca_python/classifiers/tests/test_ordinal_decomposition.py +++ b/orca_python/classifiers/tests/test_ordinal_decomposition.py @@ -158,14 +158,9 @@ def test_coding_matrix(dtype, expected_cm): npt.assert_array_equal(cm, expected_cm) -def test_frank_hall_method(X): +def test_frank_hall_method(): """Test that frank and hall method returns expected values for one toy problem (starting off predicted probabilities given by each binary classifier).""" - # Checking frank_hall cannot be used whitout ordered_partitions - classifier = OrdinalDecomposition(dtype="one_vs_next", decision_method="frank_hall") - with pytest.raises(AttributeError): - classifier._frank_hall_method(X) - classifier = OrdinalDecomposition(dtype="ordered_partitions") classifier.coding_matrix_ = classifier._coding_matrix(classifier.dtype, 5) @@ -352,3 +347,10 @@ def test_ordinal_decomposition_predict_invalid_input_raises_error(X, y): with pytest.raises(ValueError): classifier.predict([]) + + +def test_frank_hall_method_raises_error(X, y): + """Test that using frank_hall with invalid dtype raises a ValueError.""" + classifier = OrdinalDecomposition(dtype="one_vs_next", decision_method="frank_hall") + with pytest.raises(ValueError): + classifier.fit(X, y) From 1fe65f4adbfde94539fec66a925316a5e0753482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Sevilla=20Molina?= Date: Thu, 11 Sep 2025 10:56:53 +0200 Subject: [PATCH 5/9] REF: Fix mixin inheritance order (ClassifierMixin before BaseEstimator) --- orca_python/classifiers/OrdinalDecomposition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/orca_python/classifiers/OrdinalDecomposition.py b/orca_python/classifiers/OrdinalDecomposition.py index a5cc79c..00815a1 100644 --- a/orca_python/classifiers/OrdinalDecomposition.py +++ b/orca_python/classifiers/OrdinalDecomposition.py @@ -8,7 +8,7 @@ from orca_python.model_selection import load_classifier -class OrdinalDecomposition(BaseEstimator, ClassifierMixin): +class OrdinalDecomposition(ClassifierMixin, BaseEstimator): """OrdinalDecomposition ensemble classifier. This class implements an ensemble model where an ordinal problem is decomposed into From 8f0cde156b220ebc6ce384903c07ed60ea7b9101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Sevilla=20Molina?= Date: Thu, 11 Sep 2025 11:24:29 +0200 Subject: [PATCH 6/9] REF: Use boolean mask and list-comp --- .../classifiers/OrdinalDecomposition.py | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/orca_python/classifiers/OrdinalDecomposition.py b/orca_python/classifiers/OrdinalDecomposition.py index 00815a1..0a42468 100644 --- a/orca_python/classifiers/OrdinalDecomposition.py +++ b/orca_python/classifiers/OrdinalDecomposition.py @@ -168,10 +168,8 @@ def fit(self, X, y): f'Base estimator "{self.base_classifier}" must implement predict_proba.' ) - estimator.fit( - X[np.where(class_labels[:, n] != 0)], - np.ravel(class_labels[np.where(class_labels[:, n] != 0), n].T), - ) + mask = class_labels[:, n] != 0 + estimator.fit(X[mask], class_labels[mask, n].ravel()) self.estimators_.append(estimator) @@ -211,7 +209,6 @@ def predict(self, X): decision_method = self.decision_method.lower() if decision_method == "exponential_loss": - # Scaling predictions from [0,1] range to [-1,1] predictions = predictions * 2 - 1 @@ -220,7 +217,6 @@ def predict(self, X): y_pred = self.classes_[np.argmin(losses, axis=1)] elif decision_method == "hinge_loss": - # Scaling predictions from [0,1] range to [-1,1] predictions = predictions * 2 - 1 @@ -229,7 +225,6 @@ def predict(self, X): y_pred = self.classes_[np.argmin(losses, axis=1)] elif decision_method == "logarithmic_loss": - # Scaling predictions from [0,1] range to [-1,1] predictions = predictions * 2 - 1 @@ -238,7 +233,6 @@ def predict(self, X): y_pred = self.classes_[np.argmin(losses, axis=1)] elif decision_method == "frank_hall": - # Transforming from binary problems to the original problem y_proba = self._frank_hall_method(predictions) y_pred = self.classes_[np.argmax(y_proba, axis=1)] @@ -285,7 +279,6 @@ def predict_proba(self, X): decision_method = self.decision_method.lower() if decision_method == "exponential_loss": - # Scaling predictions from [0,1] range to [-1,1] predictions = predictions * 2 - 1 @@ -298,7 +291,6 @@ def predict_proba(self, X): y_proba = np.array(y_proba) elif decision_method == "hinge_loss": - # Scaling predictions from [0,1] range to [-1,1] predictions = predictions * 2 - 1 @@ -311,7 +303,6 @@ def predict_proba(self, X): y_proba = np.array(y_proba) elif decision_method == "logarithmic_loss": - # Scaling predictions from [0,1] range to [-1,1] predictions = predictions * 2 - 1 @@ -324,7 +315,6 @@ def predict_proba(self, X): y_proba = np.array(y_proba) elif decision_method == "frank_hall": - # Transforming from binary problems to the original problem y_proba = self._frank_hall_method(predictions) @@ -360,18 +350,15 @@ def _coding_matrix(self, dtype, n_classes): """ if dtype == "ordered_partitions": - coding_matrix = np.triu((-2 * np.ones(n_classes - 1))) + 1 coding_matrix = np.vstack([coding_matrix, np.ones((1, n_classes - 1))]) elif dtype == "one_vs_next": - plus_ones = np.diagflat(np.ones((1, n_classes - 1), dtype=int), -1) minus_ones = -(np.eye(n_classes, n_classes - 1, dtype=int)) coding_matrix = minus_ones + plus_ones[:, :-1] elif dtype == "one_vs_followers": - minus_ones = np.diagflat(-np.ones((1, n_classes), dtype=int)) plus_ones = np.tril(np.ones(n_classes), -1) coding_matrix = (plus_ones + minus_ones)[:, :-1] @@ -383,7 +370,6 @@ def _coding_matrix(self, dtype, n_classes): coding_matrix = np.flip((plusones + minusones)[:, :-1], axis=1) else: - raise ValueError("Decomposition type %s does not exist" % dtype) return coding_matrix.astype(int) @@ -408,7 +394,7 @@ def _get_predictions(self, X): """ predictions = np.array( - list(map(lambda c: c.predict_proba(X)[:, 1], self.estimators_)) + [est.predict_proba(X)[:, 1] for est in self.estimators_] ).T return predictions From decc9c44f3151e7546e6a58002b30d6902b9e97a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Sevilla=20Molina?= Date: Thu, 11 Sep 2025 11:30:11 +0200 Subject: [PATCH 7/9] REF: Vectorize and stabilize softmax in predict_proba --- .../classifiers/OrdinalDecomposition.py | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/orca_python/classifiers/OrdinalDecomposition.py b/orca_python/classifiers/OrdinalDecomposition.py index 0a42468..ccf149a 100644 --- a/orca_python/classifiers/OrdinalDecomposition.py +++ b/orca_python/classifiers/OrdinalDecomposition.py @@ -283,36 +283,36 @@ def predict_proba(self, X): predictions = predictions * 2 - 1 # Transforming from binary problems to the original problem - losses = self._exponential_loss(predictions) - losses = 1 / losses.astype(float) - y_proba = [] - for losse in losses: - y_proba.append((np.exp(losse) / np.sum(np.exp(losse)))) - y_proba = np.array(y_proba) + losses = self._exponential_loss(predictions).astype(float) + eps = np.finfo(float).tiny + scores = 1.0 / (losses + eps) + scores -= scores.max(axis=1, keepdims=True) + y_proba = np.exp(scores) + y_proba /= y_proba.sum(axis=1, keepdims=True) elif decision_method == "hinge_loss": # Scaling predictions from [0,1] range to [-1,1] predictions = predictions * 2 - 1 # Transforming from binary problems to the original problem - losses = self._hinge_loss(predictions) - losses = 1 / losses.astype(float) - y_proba = [] - for losse in losses: - y_proba.append((np.exp(losse) / np.sum(np.exp(losse)))) - y_proba = np.array(y_proba) + losses = self._hinge_loss(predictions).astype(float) + eps = np.finfo(float).tiny + scores = 1.0 / (losses + eps) + scores -= scores.max(axis=1, keepdims=True) + y_proba = np.exp(scores) + y_proba /= y_proba.sum(axis=1, keepdims=True) elif decision_method == "logarithmic_loss": # Scaling predictions from [0,1] range to [-1,1] predictions = predictions * 2 - 1 # Transforming from binary problems to the original problem - losses = self._logarithmic_loss(predictions) - losses = 1 / losses.astype(float) - y_proba = [] - for losse in losses: - y_proba.append((np.exp(losse) / np.sum(np.exp(losse)))) - y_proba = np.array(y_proba) + losses = self._logarithmic_loss(predictions).astype(float) + eps = np.finfo(float).tiny + scores = 1.0 / (losses + eps) + scores -= scores.max(axis=1, keepdims=True) + y_proba = np.exp(scores) + y_proba /= y_proba.sum(axis=1, keepdims=True) elif decision_method == "frank_hall": # Transforming from binary problems to the original problem @@ -364,7 +364,6 @@ def _coding_matrix(self, dtype, n_classes): coding_matrix = (plus_ones + minus_ones)[:, :-1] elif dtype == "one_vs_previous": - plusones = np.triu(np.ones(n_classes)) minusones = -np.diagflat(np.ones((1, n_classes - 1)), -1) coding_matrix = np.flip((plusones + minusones)[:, :-1], axis=1) From 9a2cc7f53eb2810b4c3cb2009091351a674fea92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Sevilla=20Molina?= Date: Thu, 11 Sep 2025 12:18:52 +0200 Subject: [PATCH 8/9] DOC: Update docstrings --- .../classifiers/OrdinalDecomposition.py | 138 +++++++++--------- 1 file changed, 68 insertions(+), 70 deletions(-) diff --git a/orca_python/classifiers/OrdinalDecomposition.py b/orca_python/classifiers/OrdinalDecomposition.py index ccf149a..5dfe941 100644 --- a/orca_python/classifiers/OrdinalDecomposition.py +++ b/orca_python/classifiers/OrdinalDecomposition.py @@ -9,7 +9,7 @@ class OrdinalDecomposition(ClassifierMixin, BaseEstimator): - """OrdinalDecomposition ensemble classifier. + """Ordinal decomposition ensemble classifier. This class implements an ensemble model where an ordinal problem is decomposed into several binary subproblems, each one of which will generate a different (binary) @@ -20,62 +20,67 @@ class OrdinalDecomposition(ClassifierMixin, BaseEstimator): Parameters ---------- - dtype : str - Type of decomposition to be performed by classifier. May be one of 4 different - types: 'ordered_partitions', 'one_vs_next', 'one_vs_followers' or - 'one_vs_previous'. - - The coding matrix generated by each method, for a problem with 5 classes will - be as follows: - - ordered_partitions one_vs_next one_vs_followers one_vs_previous - - -, -, -, -; -, , , ; -, , , ; +, +, +, +; - +, -, -, -; +, -, , ; +, -, , ; +, +, +, -; - +, +, -, -; , +, -, ; +, +, -, ; +, +, -, ; - +, +, +, -; , , +, -; +, +, +, -; +, -, , ; - +, +, +, +; , , , +; +, +, +, +; -, , , ; - - where rows represent classes and columns represent base classifiers. Plus signs - indicate that for that classifier, the label will be part of the positive - class, on the other hand, a minus sign places that class into the negative one - for that binary problem. If there is no sign, then those samples will not be - used when building the model. - - decision_method : str - Decision method that transforms the predictions of the n different base - classifiers to produce the final label (one among the real ordinal classes). - - base_classifier : str - Base classifier used to build a model for each binary subproblem. The base - classifier need to be a classifier of orca-python framework or any classifier - available in sklearn. Other classifiers implemented in sklearn's API can be - used here. - - parameters : dict - This dictionary will store the parameters used to build the base classifier. - Only one value per parameter is allowed. + dtype : {'ordered_partitions', 'one_vs_next', 'one_vs_followers', 'one_vs_previous'}, \ + default='ordered_partitions' + Type of decomposition used to build the coding matrix. Each row of the + coding matrix corresponds to a class and each column to a binary subproblem. + Entries are in {-1, 0, +1}: -1 for negative class, +1 for positive class, + and 0 if the class is ignored in that subproblem. + + decision_method : {'exponential_loss', 'hinge_loss', 'logarithmic_loss', 'frank_hall'}, \ + default='frank_hall' + Method to aggregate the predictions of the binary estimators into class + probabilities or labels. + + base_classifier : str, default='LogisticRegression' + Name of the base classifier to be instantiated via + :func:`orca_python.model_selection.load_classifier`. It can refer to + a classifier available in the orca-python framework or to a scikit-learn + compatible classifier. + + parameters : dict or None, default=None + Hyperparameters to initialize the base classifier. If ``None``, + defaults of the base classifier are used. Each key must map to a single value. Attributes ---------- - classes_ : list - List that contains all different class labels found in the original dataset. + estimators_ : list of estimators + Estimators used for predictions. + + classes_ : ndarray of shape (n_classes,) + Class labels for each output. + + n_features_in_ : int + Number of features seen during fit. coding_matrix_ : array-like, shape (n_targets, n_targets-1) Matrix that defines which classes will be used to build the model of each subproblem, and in which binary class they belong inside those new models. Further explained previously. - estimators_ : list of classifiers - Initially empty, will include all fitted models for each subproblem once the fit - function for this class is called successfully. + Notes + ----- + For ``n_classes=5``, the four decomposition types generate the following + coding matrices (rows = classes, columns = binary subproblems). Entries are + ``+1`` for positive class membership and ``-1`` for negative class membership. + + :: + + ordered_partitions one_vs_next one_vs_followers one_vs_previous + + [-1 -1 -1 -1] [-1 0 0 0] [-1 0 0 0] [ 1 1 1 1] + [ 1 -1 -1 -1] [ 1 -1 0 0] [ 1 -1 0 0] [ 1 1 1 -1] + [ 1 1 -1 -1] [ 0 1 -1 0] [ 1 1 -1 0] [ 1 1 -1 0] + [ 1 1 1 -1] [ 0 0 1 -1] [ 1 1 1 -1] [ 1 -1 0 0] + [ 1 1 1 1] [ 0 0 0 1] [ 1 1 1 1] [-1 0 0 0] References ---------- - .. [1] P.A. Gutierrez, M. Perez-Ortiz, J. Sanchez-Monedero, F. Fernandez-Navarro - and C. Hervas-Martinez, "Ordinal regression methods: survey and - experimental study", IEEE Transactions on Knowledge and Data Engineering, - Vol. 28. Issue 1, 2016, https://doi.org/10.1109/TKDE.2015.2457911 + .. [1] P.A. Gutiérrez, M. Pérez-Ortiz, J. Sánchez-Monedero, F. Fernández-Navarro + and C. Hervás-Martínez, "Ordinal regression methods: survey and + experimental study", IEEE Transactions on Knowledge and Data + Engineering, Vol. 28. Issue 1, 2016, + http://dx.doi.org/10.1109/TKDE.2015.2457911 """ @@ -113,16 +118,15 @@ def __init__( @_fit_context(prefer_skip_nested_validation=True) def fit(self, X, y): - """Fit the model with the training data. + """Fit underlying estimators to data matrix X and target(s) y. Parameters ---------- - X : {array-like, sparse matrix} of shape (n_samples, n_features) - Training patterns array, where n_samples is the number of samples and - n_features is the number of features. + X : ndarray or sparse matrix of shape (n_samples, n_features) + The input data. - y : array-like of shape (n_samples,) - Target vector relative to X. + y : ndarray of shape (n_samples,) + The target values. Returns ------- @@ -181,13 +185,12 @@ def predict(self, X): Parameters ---------- X : {array-like, sparse matrix} of shape (n_samples, n_features) - Test patterns array, where n_samples is the number of samples and - n_features is the number of features. + The input data. Returns ------- - y_pred : array, shape (n_samples,) - Class labels for samples in X. + y_pred : ndarray of shape (n_samples,) + The predicted classes. Raises ------ @@ -245,13 +248,14 @@ def predict(self, X): return y_pred def predict_proba(self, X): - """Return the probability of the sample for each class in the model. + """Probability estimates. + + The returned estimates for all classes are ordered by label of classes. Parameters ---------- X : {array-like, sparse matrix} of shape (n_samples, n_features) - Test patterns array, where n_samples is the number of samples and - n_features is the number of features. + The input data. Returns ------- @@ -383,8 +387,7 @@ def _get_predictions(self, X): Parameters ---------- X : {array-like, sparse matrix} of shape (n_samples, n_features) - Test patterns array, where n_samples is the number of samples and - n_features is the number of features. + The input data. Returns ------- @@ -412,7 +415,7 @@ def _exponential_loss(self, predictions): Returns ------- - e_losses : array, shape (n_samples, n_targets) + e_losses : ndarray of shape (n_samples, n_classes) Exponential losses for each sample of dataset X. One different value for each class label. @@ -436,7 +439,7 @@ def _hinge_loss(self, predictions): Returns ------- - h_losses : array, shape (n_samples, n_targets) + h_losses : ndarray of shape (n_samples, n_classes) Hinge losses for each sample of dataset X. One different value for each class label. @@ -460,7 +463,7 @@ def _logarithmic_loss(self, predictions): Returns ------- - l_losses : array, shape (n_samples, n_targets) + l_losses : ndarray of shape (n_samples, n_classes) Logarithmic losses for each sample of dataset X. One different value for each class label. @@ -484,13 +487,8 @@ def _frank_hall_method(self, predictions): Returns ------- - y_proba : array, shape (n_samples, n_targets) - Class labels predicted for samples in dataset X. - - Raises - ------ - AttributeError - If the decomposition type is not ordered_partitions. + y_proba : ndarray of shape (n_samples, n_classes) + Class membership probabilities for each sample. """ y_proba = np.empty([(predictions.shape[0]), (predictions.shape[1] + 1)]) From 4b7b37f2e0884ddcba867b07106b5b88f8c8bf21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20Sevilla=20Molina?= Date: Thu, 11 Sep 2025 12:50:31 +0200 Subject: [PATCH 9/9] MNT: Add validate_data shim for sklearn <1.6 compatibility --- .../classifiers/OrdinalDecomposition.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/orca_python/classifiers/OrdinalDecomposition.py b/orca_python/classifiers/OrdinalDecomposition.py index 5dfe941..ff76995 100644 --- a/orca_python/classifiers/OrdinalDecomposition.py +++ b/orca_python/classifiers/OrdinalDecomposition.py @@ -3,7 +3,25 @@ import numpy as np from sklearn.base import BaseEstimator, ClassifierMixin, _fit_context from sklearn.utils._param_validation import StrOptions -from sklearn.utils.validation import check_is_fitted, validate_data +from sklearn.utils.validation import check_is_fitted + +# scikit-learn >= 1.6 +try: + from sklearn.utils.validation import validate_data as _sk_validate_data + + def _validate_data_compat(estimator, X, y=None, *, reset=True, **kwargs): + y_arg = "no_validation" if y is None else y + return _sk_validate_data(estimator, X, y_arg, reset=reset, **kwargs) + + +# scikit-learn < 1.6 +except Exception: + + def _validate_data_compat(estimator, X, y=None, *, reset=True, **kwargs): + if y is None: + return estimator._validate_data(X, reset=reset, **kwargs) + return estimator._validate_data(X, y, reset=reset, **kwargs) + from orca_python.model_selection import load_classifier @@ -139,7 +157,7 @@ def fit(self, X, y): If parameters are invalid or data has wrong format. """ - X, y = validate_data( + X, y = _validate_data_compat( self, X, y, accept_sparse=False, ensure_2d=True, dtype=None ) @@ -205,7 +223,7 @@ def predict(self, X): """ check_is_fitted(self, ["estimators_", "classes_", "coding_matrix_"]) - X = validate_data(self, X, reset=False, ensure_2d=True, dtype=None) + X = _validate_data_compat(self, X, reset=False, ensure_2d=True, dtype=None) # Getting predicted labels for dataset from each classifier predictions = self._get_predictions(X) @@ -276,7 +294,7 @@ def predict_proba(self, X): """ check_is_fitted(self, ["estimators_", "classes_", "coding_matrix_"]) - X = validate_data(self, X, reset=False, ensure_2d=True, dtype=None) + X = _validate_data_compat(self, X, reset=False, ensure_2d=True, dtype=None) # Getting predicted labels for dataset from each classifier predictions = self._get_predictions(X)