mlquantify 0.0.11__py3-none-any.whl → 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.
Files changed (85) hide show
  1. mlquantify/__init__.py +32 -6
  2. mlquantify/base.py +559 -256
  3. mlquantify/classification/__init__.py +1 -1
  4. mlquantify/classification/methods.py +160 -0
  5. mlquantify/evaluation/__init__.py +14 -2
  6. mlquantify/evaluation/measures.py +215 -0
  7. mlquantify/evaluation/protocol.py +647 -0
  8. mlquantify/methods/__init__.py +37 -40
  9. mlquantify/methods/aggregative.py +1030 -0
  10. mlquantify/methods/meta.py +472 -0
  11. mlquantify/methods/mixture_models.py +1003 -0
  12. mlquantify/methods/non_aggregative.py +136 -0
  13. mlquantify/methods/threshold_optimization.py +959 -0
  14. mlquantify/model_selection.py +377 -232
  15. mlquantify/plots.py +367 -0
  16. mlquantify/utils/__init__.py +2 -2
  17. mlquantify/utils/general.py +334 -0
  18. mlquantify/utils/method.py +449 -0
  19. {mlquantify-0.0.11.dist-info → mlquantify-0.1.0.dist-info}/METADATA +137 -126
  20. mlquantify-0.1.0.dist-info/RECORD +22 -0
  21. {mlquantify-0.0.11.dist-info → mlquantify-0.1.0.dist-info}/WHEEL +1 -1
  22. mlquantify/classification/pwkclf.py +0 -73
  23. mlquantify/evaluation/measures/__init__.py +0 -26
  24. mlquantify/evaluation/measures/ae.py +0 -11
  25. mlquantify/evaluation/measures/bias.py +0 -16
  26. mlquantify/evaluation/measures/kld.py +0 -8
  27. mlquantify/evaluation/measures/mse.py +0 -12
  28. mlquantify/evaluation/measures/nae.py +0 -16
  29. mlquantify/evaluation/measures/nkld.py +0 -13
  30. mlquantify/evaluation/measures/nrae.py +0 -16
  31. mlquantify/evaluation/measures/rae.py +0 -12
  32. mlquantify/evaluation/measures/se.py +0 -12
  33. mlquantify/evaluation/protocol/_Protocol.py +0 -202
  34. mlquantify/evaluation/protocol/__init__.py +0 -2
  35. mlquantify/evaluation/protocol/app.py +0 -146
  36. mlquantify/evaluation/protocol/npp.py +0 -34
  37. mlquantify/methods/aggregative/ThreholdOptm/_ThreholdOptimization.py +0 -62
  38. mlquantify/methods/aggregative/ThreholdOptm/__init__.py +0 -7
  39. mlquantify/methods/aggregative/ThreholdOptm/acc.py +0 -27
  40. mlquantify/methods/aggregative/ThreholdOptm/max.py +0 -23
  41. mlquantify/methods/aggregative/ThreholdOptm/ms.py +0 -21
  42. mlquantify/methods/aggregative/ThreholdOptm/ms2.py +0 -25
  43. mlquantify/methods/aggregative/ThreholdOptm/pacc.py +0 -41
  44. mlquantify/methods/aggregative/ThreholdOptm/t50.py +0 -21
  45. mlquantify/methods/aggregative/ThreholdOptm/x.py +0 -23
  46. mlquantify/methods/aggregative/__init__.py +0 -9
  47. mlquantify/methods/aggregative/cc.py +0 -32
  48. mlquantify/methods/aggregative/emq.py +0 -86
  49. mlquantify/methods/aggregative/fm.py +0 -72
  50. mlquantify/methods/aggregative/gac.py +0 -96
  51. mlquantify/methods/aggregative/gpac.py +0 -87
  52. mlquantify/methods/aggregative/mixtureModels/_MixtureModel.py +0 -81
  53. mlquantify/methods/aggregative/mixtureModels/__init__.py +0 -5
  54. mlquantify/methods/aggregative/mixtureModels/dys.py +0 -55
  55. mlquantify/methods/aggregative/mixtureModels/dys_syn.py +0 -89
  56. mlquantify/methods/aggregative/mixtureModels/hdy.py +0 -46
  57. mlquantify/methods/aggregative/mixtureModels/smm.py +0 -27
  58. mlquantify/methods/aggregative/mixtureModels/sord.py +0 -77
  59. mlquantify/methods/aggregative/pcc.py +0 -33
  60. mlquantify/methods/aggregative/pwk.py +0 -38
  61. mlquantify/methods/meta/__init__.py +0 -1
  62. mlquantify/methods/meta/ensemble.py +0 -236
  63. mlquantify/methods/non_aggregative/__init__.py +0 -1
  64. mlquantify/methods/non_aggregative/hdx.py +0 -71
  65. mlquantify/plots/__init__.py +0 -2
  66. mlquantify/plots/distribution_plot.py +0 -109
  67. mlquantify/plots/protocol_plot.py +0 -157
  68. mlquantify/utils/general_purposes/__init__.py +0 -8
  69. mlquantify/utils/general_purposes/convert_col_to_array.py +0 -13
  70. mlquantify/utils/general_purposes/generate_artificial_indexes.py +0 -29
  71. mlquantify/utils/general_purposes/get_real_prev.py +0 -9
  72. mlquantify/utils/general_purposes/load_quantifier.py +0 -4
  73. mlquantify/utils/general_purposes/make_prevs.py +0 -23
  74. mlquantify/utils/general_purposes/normalize.py +0 -20
  75. mlquantify/utils/general_purposes/parallel.py +0 -10
  76. mlquantify/utils/general_purposes/round_protocol_df.py +0 -14
  77. mlquantify/utils/method_purposes/__init__.py +0 -6
  78. mlquantify/utils/method_purposes/distances.py +0 -21
  79. mlquantify/utils/method_purposes/getHist.py +0 -13
  80. mlquantify/utils/method_purposes/get_scores.py +0 -33
  81. mlquantify/utils/method_purposes/moss.py +0 -16
  82. mlquantify/utils/method_purposes/ternary_search.py +0 -14
  83. mlquantify/utils/method_purposes/tprfpr.py +0 -42
  84. mlquantify-0.0.11.dist-info/RECORD +0 -73
  85. {mlquantify-0.0.11.dist-info → mlquantify-0.1.0.dist-info}/top_level.txt +0 -0
mlquantify/base.py CHANGED
@@ -1,256 +1,559 @@
1
- from abc import abstractmethod, ABC
2
- from sklearn.base import BaseEstimator
3
- from copy import deepcopy
4
- import numpy as np
5
- import joblib
6
-
7
-
8
- from .utils import parallel, normalize_prevalence
9
-
10
- class Quantifier(ABC, BaseEstimator):
11
- """ Abstract Class for quantifiers."""
12
-
13
- @abstractmethod
14
- def fit(self, X, y) -> object: ...
15
-
16
- @abstractmethod
17
- def predict(self, X) -> dict: ...
18
-
19
- @property
20
- def classes(self) -> list:
21
- return self._classes
22
-
23
- @classes.setter
24
- def classes(self, classes):
25
- self._classes = sorted(list(classes))
26
-
27
- @property
28
- def n_class(self) -> list:
29
- return len(self._classes)
30
-
31
- @property
32
- def multiclass_method(self) -> bool:
33
- return True
34
-
35
- @property
36
- def binary_data(self) -> bool:
37
- return len(self._classes) == 2
38
-
39
-
40
- def save_quantifier(self, path: str=None) -> None:
41
- if not path:
42
- path = f"{self.__class__.__name__}.joblib"
43
- joblib.dump(self, path)
44
-
45
-
46
-
47
- class AggregativeQuantifier(Quantifier, ABC):
48
- """Abstract class for all Aggregative quantifiers, it means that each one of the quantifiers,
49
- uses a learner or possibly a classifier to generate predictions.
50
- This class is mostly used to detect whether or not its a binary or multiclass problem, and doing
51
- One-Vs-All in case of multiclass dataset and not multiclass quantifier method.
52
- """
53
-
54
-
55
- def __init__(self):
56
- # Dictionary to hold binary quantifiers for each class.
57
- self.binary_quantifiers = {}
58
- self.learner_fitted = False
59
- self.cv_folds = 10
60
-
61
- def fit(self, X, y, learner_fitted=False, cv_folds: int = 10, n_jobs:int=1):
62
- """Fit the quantifier model.
63
-
64
- Args:
65
- X (array-like): Training features.
66
- y (array-like): Training labels.
67
- learner_fitted (bool, optional): Whether the learner is already fitted. Defaults to False.
68
- cv_folds (int, optional): Number of cross-validation folds. Defaults to 10.
69
-
70
- Returns:
71
- self: Fitted quantifier.
72
- """
73
- self.n_jobs = n_jobs
74
- self.learner_fitted = learner_fitted
75
- self.cv_folds = cv_folds
76
-
77
- self.classes = np.unique(y)
78
- if self.binary_data or self.multiclass_method:
79
- return self._fit_method(X, y)
80
-
81
- # Making one vs all
82
- self.binary_quantifiers = {class_: deepcopy(self) for class_ in self.classes}
83
- parallel(self.delayed_fit, self.classes, self.n_jobs, X, y)
84
-
85
- return self
86
-
87
- def predict(self, X) -> dict:
88
- """Predict class prevalences for the given data.
89
-
90
- Args:
91
- X (array-like): Test features.
92
-
93
- Returns:
94
- dict: Dictionary with class prevalences.
95
- """
96
- if self.binary_data or self.multiclass_method:
97
- prevalences = self._predict_method(X)
98
- return normalize_prevalence(prevalences, self.classes)
99
-
100
- # Making one vs all
101
- prevalences = np.asarray(parallel(self.delayed_predict, self.classes, self.n_jobs, X))
102
- return normalize_prevalence(prevalences, self.classes)
103
-
104
- @abstractmethod
105
- def _fit_method(self, X, y):
106
- """Abstract fit method that each quantification method must implement.
107
-
108
- Args:
109
- X (array-like): Training features.
110
- y (array-like): Training labels.
111
- learner_fitted (bool): Whether the learner is already fitted.
112
- cv_folds (int): Number of cross-validation folds.
113
- """
114
- ...
115
-
116
- @abstractmethod
117
- def _predict_method(self, X) -> dict:
118
- """Abstract predict method that each quantification method must implement.
119
-
120
- Args:
121
- X (array-like): Test data to generate class prevalences.
122
-
123
- Returns:
124
- dict: Dictionary with class:prevalence for each class.
125
- """
126
- ...
127
-
128
- @property
129
- def learner(self):
130
- return self.learner_
131
-
132
- @learner.setter
133
- def learner(self, value):
134
- self.learner_ = value
135
-
136
-
137
- def get_params(self, deep=True):
138
- return self.learner.get_params()
139
-
140
- def set_params(self, **params):
141
- # Model Params
142
- for key, value in params.items():
143
- if hasattr(self, key):
144
- setattr(self, key, value)
145
-
146
- # Learner Params
147
- if self.learner:
148
- learner_params = {k.replace('learner__', ''): v for k, v in params.items() if 'learner__' in k}
149
- if learner_params:
150
- self.learner.set_params(**learner_params)
151
-
152
- return self
153
-
154
-
155
- # MULTICLASS METHODS
156
-
157
- def delayed_fit(self, class_, X, y):
158
- """Delayed fit method for one-vs-all strategy, with parallel running.
159
-
160
- Args:
161
- class_ (Any): The class for which the model is being fitted.
162
- X (array-like): Training features.
163
- y (array-like): Training labels.
164
- learner_fitted (bool): Whether the learner is already fitted.
165
- cv_folds (int): Number of cross-validation folds.
166
-
167
- Returns:
168
- self: Fitted binary quantifier for the given class.
169
- """
170
- y_class = (y == class_).astype(int)
171
- return self.binary_quantifiers[class_].fit(X, y_class)
172
-
173
- def delayed_predict(self, class_, X):
174
- """Delayed predict method for one-vs-all strategy, with parallel running.
175
-
176
- Args:
177
- class_ (Any): The class for which the model is making predictions.
178
- X (array-like): Test features.
179
-
180
- Returns:
181
- float: Predicted prevalence for the given class.
182
- """
183
- return self.binary_quantifiers[class_].predict(X)[1]
184
-
185
-
186
- class NonAggregativeQuantifier(Quantifier):
187
- """Abstract class for Non Aggregative quantifiers, it means that
188
- theses methods does not use a classifier or specift learner on it's
189
- predictions.
190
- """
191
-
192
-
193
- def fit(self, X, y, n_jobs:int=1):
194
- """Fit the quantifier model.
195
-
196
- Args:
197
- X (array-like): Training features.
198
- y (array-like): Training labels.
199
- learner_fitted (bool, optional): Whether the learner is already fitted. Defaults to False.
200
- cv_folds (int, optional): Number of cross-validation folds. Defaults to 10.
201
-
202
- Returns:
203
- self: Fitted quantifier.
204
- """
205
- self.n_jobs = n_jobs
206
- self.classes = np.unique(y)
207
- if self.binary_data or self.multiclass_method:
208
- return self._fit_method(X, y)
209
-
210
- # Making one vs all
211
- self.binary_quantifiers = {class_: deepcopy(self) for class_ in self.classes}
212
- parallel(self.delayed_fit, self.classes, self.n_jobs, X, y)
213
-
214
- return self
215
-
216
- def predict(self, X) -> dict:
217
- """Predict class prevalences for the given data.
218
-
219
- Args:
220
- X (array-like): Test features.
221
-
222
- Returns:
223
- dict: Dictionary with class prevalences.
224
- """
225
- if self.binary_data or self.multiclass_method:
226
- prevalences = self._predict_method(X)
227
- return normalize_prevalence(prevalences, self.classes)
228
-
229
- # Making one vs all
230
- prevalences = np.asarray(parallel(self.delayed_predict, self.classes, self.n_jobs, X))
231
- return normalize_prevalence(prevalences, self.classes)
232
-
233
-
234
- @abstractmethod
235
- def _fit_method(self, X, y):
236
- """Abstract fit method that each quantification method must implement.
237
-
238
- Args:
239
- X (array-like): Training features.
240
- y (array-like): Training labels.
241
- learner_fitted (bool): Whether the learner is already fitted.
242
- cv_folds (int): Number of cross-validation folds.
243
- """
244
- ...
245
-
246
- @abstractmethod
247
- def _predict_method(self, X) -> dict:
248
- """Abstract predict method that each quantification method must implement.
249
-
250
- Args:
251
- X (array-like): Test data to generate class prevalences.
252
-
253
- Returns:
254
- dict: Dictionary with class:prevalence for each class.
255
- """
256
- ...
1
+ from abc import abstractmethod, ABC
2
+ from sklearn.base import BaseEstimator
3
+ from copy import deepcopy
4
+ import numpy as np
5
+ import joblib
6
+
7
+ import mlquantify as mq
8
+ from .utils.general import parallel, normalize_prevalence
9
+
10
+ class Quantifier(ABC, BaseEstimator):
11
+ """Base class for all quantifiers, it defines the basic structure of a quantifier.
12
+
13
+ Warning: Inheriting from this class does not provide dynamic use of multiclass or binary methods, it is necessary to implement the logic in the quantifier itself. If you want to use this feature, inherit from AggregativeQuantifier or NonAggregativeQuantifier.
14
+
15
+ Inheriting from this class, it provides the following implementations:
16
+
17
+ - properties for classes, n_class, is_multiclass and binary_data.
18
+ - save_quantifier method to save the quantifier
19
+
20
+ Read more in the :ref:`User Guide <creating_your_own_quantifier>`.
21
+
22
+
23
+ Notes
24
+ -----
25
+ It's recommended to inherit from AggregativeQuantifier or NonAggregativeQuantifier, as they provide more functionality and flexibility for quantifiers.
26
+ """
27
+
28
+ @abstractmethod
29
+ def fit(self, X, y) -> object: ...
30
+
31
+ @abstractmethod
32
+ def predict(self, X) -> dict: ...
33
+
34
+ @property
35
+ def classes(self) -> list:
36
+ return self._classes
37
+
38
+ @classes.setter
39
+ def classes(self, classes):
40
+ self._classes = sorted(list(classes))
41
+
42
+ @property
43
+ def n_class(self) -> list:
44
+ return len(self._classes)
45
+
46
+ @property
47
+ def is_multiclass(self) -> bool:
48
+ return True
49
+
50
+ @property
51
+ def binary_data(self) -> bool:
52
+ return len(self._classes) == 2
53
+
54
+
55
+ def save_quantifier(self, path: str=None) -> None:
56
+ if not path:
57
+ path = f"{self.__class__.__name__}.joblib"
58
+ joblib.dump(self, path)
59
+
60
+
61
+
62
+ class AggregativeQuantifier(Quantifier, ABC):
63
+ """A base class for aggregative quantifiers.
64
+
65
+ This class provides the basic structure for aggregative quantifiers, which are quantifiers that aggregates a classifier or learner inside to generate predictions.
66
+
67
+ Inheriting from this class, it provides dynamic prediction for multiclass and binary data, making one-vs-all strategy for multiclass data with binary quantifiers.
68
+
69
+ Read more in the :ref:`User Guide <creating_your_own_quantifier>`.
70
+
71
+
72
+ Notes
73
+ -----
74
+ All quantifiers should specify at least the learner attribute. Wich should inherit from BaseEstimator of scikit-learn.
75
+
76
+ All quantifiers can return a dictionary with class:prevalence, a list or a numpy array.
77
+
78
+
79
+ Examples
80
+ --------
81
+ Example 1: Multiclass Quantifier
82
+ >>> from mlquantify.base import AggregativeQuantifier
83
+ >>> from mlquantify.utils.general import get_real_prev
84
+ >>> from sklearn.ensemble import RandomForestClassifier
85
+ >>> from sklearn.model_selection import train_test_split
86
+ >>> import numpy as np
87
+ >>> class MyQuantifier(AggregativeQuantifier):
88
+ ... def __init__(self, learner, *, param):
89
+ ... self.learner = learner
90
+ ... self.param = param
91
+ ... def _fit_method(self, X, y):
92
+ ... self.learner.fit(X, y)
93
+ ... return self
94
+ ... def _predict_method(self, X):
95
+ ... predicted_labels = self.learner.predict(X)
96
+ ... class_counts = np.array([np.count_nonzero(predicted_labels == _class) for _class in self.classes])
97
+ ... return class_counts / len(predicted_labels)
98
+ >>> quantifier = MyQuantifier(learner=RandomForestClassifier(), param=1)
99
+ >>> quantifier.get_params(deep=False)
100
+ {'learner': RandomForestClassifier(), 'param': 1}
101
+ >>> # Sample data
102
+ >>> X = np.array([[0.1, 0.2], [0.2, 0.1], [0.3, 0.4], [0.4, 0.3],
103
+ ... [0.5, 0.6], [0.6, 0.5], [0.7, 0.8], [0.8, 0.7],
104
+ ... [0.9, 1.0], [1.0, 0.9]])
105
+ >>> y = np.array([0, 0, 0, 1, 0, 1, 0, 1, 0, 1]) # 40% positive (4 out of 10)
106
+ >>> # Split the data into training and testing sets
107
+ >>> X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
108
+ >>> # Fit the quantifier
109
+ >>> quantifier.fit(X_train, y_train)
110
+ None
111
+ >>> # Real prevalence in the training set
112
+ >>> get_real_prev(y_train)
113
+ {0: 0.5714285714285714, 1: 0.42857142857142855}
114
+ >>> # Predicted prevalence in the test set
115
+ >>> quantifier.predict(X_test)
116
+ {0: 0.6666666666666666, 1: 0.3333333333333333}
117
+
118
+ Example 2: Binary Quantifier
119
+ >>> from sklearn.svm import SVC
120
+ >>> class BinaryQuantifier(AggregativeQuantifier):
121
+ ... @property
122
+ ... def is_multiclass(self):
123
+ ... return False
124
+ ... def __init__(self, learner):
125
+ ... self.learner = learner
126
+ ... def _fit_method(self, X, y):
127
+ ... self.learner.fit(X, y)
128
+ ... return self
129
+ ... def _predict_method(self, X):
130
+ ... predicted_labels = self.learner.predict(X)
131
+ ... class_counts = np.array([np.count_nonzero(predicted_labels == _class) for _class in self.classes])
132
+ ... return class_counts / len(predicted_labels)
133
+ >>> binary_quantifier = BinaryQuantifier(learner=SVC(probability=True))
134
+ >>> # Sample multiclass data
135
+ >>> X = np.array([
136
+ ... [0.1, 0.2], [0.2, 0.1], [0.3, 0.4], [0.4, 0.3],
137
+ ... [0.5, 0.6], [0.6, 0.5], [0.7, 0.8], [0.8, 0.7],
138
+ ... [0.9, 1.0], [1.0, 0.9], [1.1, 1.2], [1.2, 1.1],
139
+ ... [1.3, 1.4], [1.4, 1.3], [1.5, 1.6], [1.6, 1.5],
140
+ ... [1.7, 1.8], [1.8, 1.7], [1.9, 2.0], [2.0, 1.9]
141
+ ... ])
142
+ >>> # Update the labels to include a third class
143
+ >>> y = np.array([0, 0, 0, 1, 0, 1, 0, 1, 2, 2, 0, 1, 0, 1, 0, 1, 2, 2, 0, 1])
144
+ >>> # Split the data into training and testing sets
145
+ >>> X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.4, random_state=42)
146
+ >>> # Fit the binary quantifier
147
+ >>> binary_quantifier.fit(X_train, y_train)
148
+ None
149
+ >>> # Real prevalence in the training set
150
+ >>> get_real_prev(y_test)
151
+ {0: 0.25, 1: 0.5, 2: 0.25}
152
+ >>> preds = binary_quantifier.predict(X_test)
153
+ >>> preds
154
+ {0: 1.0, 1: 0.0, 2: 0.0}
155
+ """
156
+
157
+
158
+ def __init__(self):
159
+ # Dictionary to hold binary quantifiers for each class.
160
+ self.binary_quantifiers = {}
161
+ self.learner_fitted = False
162
+ self.cv_folds = 10
163
+
164
+ def fit(self, X, y, learner_fitted=False, cv_folds: int = 10, n_jobs:int=1):
165
+ """Fit the quantifier model.
166
+
167
+ Parameters
168
+ ----------
169
+ X : array-like
170
+ Training features.
171
+ y : array-like
172
+ Training labels.
173
+ learner_fitted : bool, default=False
174
+ Whether the learner is already fitted.
175
+ cv_folds : int, default=10
176
+ Number of cross-validation folds.
177
+ n_jobs : int, default=1
178
+ Number of parallel jobs to run.
179
+
180
+
181
+ Returns
182
+ -------
183
+ self : object
184
+ The fitted quantifier instance.
185
+
186
+
187
+ Notes
188
+ -----
189
+ The model dynamically determines whether to perform one-vs-all classification or
190
+ to directly fit the data based on the type of the problem:
191
+ - If the data is binary or inherently multiclass, the model fits directly using
192
+ `_fit_method` without creating binary quantifiers.
193
+ - For other cases, the model creates one binary quantifier per class using the
194
+ one-vs-all approach, allowing for dynamic prediction based on the provided dataset.
195
+ """
196
+
197
+ self.n_jobs = n_jobs
198
+ self.learner_fitted = learner_fitted
199
+ self.cv_folds = cv_folds
200
+
201
+ self.classes = np.unique(y)
202
+
203
+ if self.binary_data or self.is_multiclass:
204
+ return self._fit_method(X, y)
205
+
206
+ # Making one vs all
207
+ self.binary_quantifiers = {class_: deepcopy(self) for class_ in self.classes}
208
+ parallel(self.delayed_fit, self.classes, self.n_jobs, X, y)
209
+
210
+ return self
211
+
212
+ def predict(self, X) -> dict:
213
+ """Predict class prevalences for the given data.
214
+
215
+ Parameters
216
+ ----------
217
+ X : array-like
218
+ Test features.
219
+
220
+ Returns
221
+ -------
222
+ dict
223
+ A dictionary where keys are class labels and values are their predicted prevalences.
224
+
225
+ Notes
226
+ -----
227
+ The prediction approach is dynamically chosen based on the data type:
228
+ - For binary or inherently multiclass data, the model uses `_predict_method` to directly
229
+ estimate class prevalences.
230
+ - For other cases, the model performs one-vs-all prediction, where each binary quantifier
231
+ estimates the prevalence of its respective class. The results are then normalized to
232
+ ensure they form valid proportions.
233
+ """
234
+
235
+ if self.binary_data or self.is_multiclass:
236
+ prevalences = self._predict_method(X)
237
+ return normalize_prevalence(prevalences, self.classes)
238
+
239
+ # Making one vs all
240
+ prevalences = np.asarray(parallel(self.delayed_predict, self.classes, self.n_jobs, X))
241
+ return normalize_prevalence(prevalences, self.classes)
242
+
243
+ @abstractmethod
244
+ def _fit_method(self, X, y):
245
+ """Abstract fit method that each aggregative quantification method must implement.
246
+
247
+ Parameters
248
+ ----------
249
+ X : array-like
250
+ Training features.
251
+ y : array-like
252
+ Training labels.
253
+ """
254
+ ...
255
+
256
+ @abstractmethod
257
+ def _predict_method(self, X) -> dict:
258
+ """Abstract predict method that each aggregative quantification method must implement.
259
+
260
+ Parameters
261
+ ----------
262
+ X : array-like
263
+ Test data to generate class prevalences.
264
+
265
+ Returns
266
+ -------
267
+ dict, list, or numpy array
268
+ The predicted prevalences, which can be a dictionary where keys are class labels
269
+ and values are their predicted prevalences, a list, or a numpy array.
270
+ """
271
+
272
+ ...
273
+
274
+ @property
275
+ def is_probabilistic(self) -> bool:
276
+ """Check if the learner is probabilistic or not.
277
+
278
+ Returns
279
+ -------
280
+ bool
281
+ True if the learner is probabilistic, False otherwise.
282
+ """
283
+ return False
284
+
285
+
286
+ @property
287
+ def learner(self):
288
+ """Returns the learner_ object.
289
+ Returns
290
+ -------
291
+ learner_ : object
292
+ The learner_ object stored in the class instance.
293
+ """
294
+ return self.learner_
295
+
296
+ @learner.setter
297
+ def learner(self, value):
298
+ """
299
+ Sets the learner attribute.
300
+ Parameters:
301
+ value : any
302
+ The value to be assigned to the learner_ attribute.
303
+ """
304
+ assert isinstance(value, BaseEstimator) or mq.ARGUMENTS_SETTED, "learner object is not an estimator, or you may change ARGUMENTS_SETTED to True"
305
+ self.learner_ = value
306
+
307
+ def fit_learner(self, X, y):
308
+ """Fit the learner to the training data.
309
+
310
+ Parameters
311
+ ----------
312
+ X : array-like
313
+ Training features.
314
+ y : array-like
315
+ Training labels.
316
+ """
317
+ if self.learner is not None:
318
+ if not self.learner_fitted:
319
+ self.learner_.fit(X, y)
320
+ elif mq.ARGUMENTS_SETTED:
321
+ if self.is_probabilistic and mq.arguments["posteriors_test"] is not None:
322
+ return
323
+ elif not self.is_probabilistic and mq.arguments["y_pred"] is not None:
324
+ return
325
+
326
+ def predict_learner(self, X):
327
+ """Predict the class labels or probabilities for the given data.
328
+
329
+ Parameters
330
+ ----------
331
+ X : array-like
332
+ Test features.
333
+
334
+ Returns
335
+ -------
336
+ array-like
337
+ The predicted class labels or probabilities.
338
+ """
339
+ if self.learner is not None:
340
+ if self.is_probabilistic:
341
+ return self.learner_.predict_proba(X)
342
+ return self.learner_.predict(X)
343
+ else:
344
+ if mq.ARGUMENTS_SETTED:
345
+ if self.is_probabilistic:
346
+ return mq.arguments["posteriors_test"]
347
+ return mq.arguments["y_pred"]
348
+ else:
349
+ raise ValueError("No learner object was set and no arguments were setted")
350
+
351
+ def set_params(self, **params):
352
+ """
353
+ Set the parameters of this estimator.
354
+ The method allows setting parameters for both the model and the learner.
355
+ Parameters that match the model's attributes will be set directly on the model.
356
+ Parameters prefixed with 'learner__' will be set on the learner if it exists.
357
+ Parameters:
358
+ -----------
359
+ **params : dict
360
+ Dictionary of parameters to set. Keys can be model attribute names or
361
+ 'learner__' prefixed names for learner parameters.
362
+ Returns:
363
+ --------
364
+ self : Quantifier
365
+ Returns the instance of the quantifier with updated parameters itself.
366
+ """
367
+
368
+
369
+ # Model Params
370
+ for key, value in params.items():
371
+ if hasattr(self, key):
372
+ setattr(self, key, value)
373
+
374
+ # Learner Params
375
+ if self.learner is not None:
376
+ learner_params = {k.replace('learner__', ''): v for k, v in params.items() if 'learner__' in k}
377
+ if learner_params:
378
+ self.learner.set_params(**learner_params)
379
+
380
+ return self
381
+
382
+
383
+ # MULTICLASS METHODS
384
+
385
+ def delayed_fit(self, class_, X, y):
386
+ """Delayed fit method for one-vs-all strategy, with parallel execution.
387
+
388
+ Parameters
389
+ ----------
390
+ class_ : Any
391
+ The class for which the model is being fitted.
392
+ X : array-like
393
+ Training features.
394
+ y : array-like
395
+ Training labels.
396
+
397
+ Returns
398
+ -------
399
+ self : object
400
+ Fitted binary quantifier for the given class.
401
+ """
402
+
403
+ y_class = (y == class_).astype(int)
404
+ return self.binary_quantifiers[class_].fit(X, y_class)
405
+
406
+ def delayed_predict(self, class_, X):
407
+ """Delayed predict method for one-vs-all strategy, with parallel execution.
408
+
409
+ Parameters
410
+ ----------
411
+ class_ : Any
412
+ The class for which the model is making predictions.
413
+ X : array-like
414
+ Test features.
415
+
416
+ Returns
417
+ -------
418
+ float
419
+ Predicted prevalence for the given class.
420
+ """
421
+
422
+ return self.binary_quantifiers[class_].predict(X)[1]
423
+
424
+
425
+ class NonAggregativeQuantifier(Quantifier):
426
+ """Abstract base class for non-aggregative quantifiers.
427
+
428
+ Non-aggregative quantifiers differ from aggregative quantifiers as they do not use
429
+ an underlying classifier or specific learner for their predictions.
430
+
431
+ This class defines the general structure and behavior for non-aggregative quantifiers,
432
+ including support for multiclass data and dynamic handling of binary and multiclass problems.
433
+
434
+ Notes
435
+ -----
436
+ This class requires implementing the `_fit_method` and `_predict_method` in subclasses
437
+ to define how the quantification is performed. These methods handle the core logic for
438
+ fitting and predicting class prevalences.
439
+
440
+ Examples
441
+ --------
442
+ >>> from myquantify.base import NonAggregativeQuantifier
443
+ >>> import numpy as np
444
+ >>> class MyNonAggregativeQuantifier(NonAggregativeQuantifier):
445
+ ... def _fit_method(self, X, y):
446
+ ... # Custom logic for fitting
447
+ ... pass
448
+ ... def _predict_method(self, X):
449
+ ... # Custom logic for predicting
450
+ ... return {0: 0.5, 1: 0.5}
451
+ >>> quantifier = MyNonAggregativeQuantifier()
452
+ >>> X = np.random.rand(10, 2)
453
+ >>> y = np.array([0, 1, 0, 1, 0, 1, 0, 1, 0, 1])
454
+ >>> quantifier.fit(X, y)
455
+ <MyNonAggregativeQuantifier>
456
+ >>> quantifier.predict(X)
457
+ {0: 0.5, 1: 0.5}
458
+ """
459
+
460
+ def fit(self, X, y, n_jobs: int = 1):
461
+ """Fit the quantifier model to the training data.
462
+
463
+ Parameters
464
+ ----------
465
+ X : array-like
466
+ Training features.
467
+ y : array-like
468
+ Training labels.
469
+ n_jobs : int, default=1
470
+ Number of parallel jobs to run.
471
+
472
+ Returns
473
+ -------
474
+ self : NonAggregativeQuantifier
475
+ The fitted quantifier instance.
476
+
477
+ Notes
478
+ -----
479
+ - For binary or inherently multiclass data, the model directly calls `_fit_method`
480
+ to process the data.
481
+ - For other cases, it creates one quantifier per class using a one-vs-all strategy
482
+ and fits each quantifier independently in parallel.
483
+ """
484
+ self.n_jobs = n_jobs
485
+ self.classes = np.unique(y)
486
+ if self.binary_data or self.is_multiclass:
487
+ return self._fit_method(X, y)
488
+
489
+ # One-vs-all approach
490
+ self.binary_quantifiers = {class_: deepcopy(self) for class_ in self.classes}
491
+ parallel(self.delayed_fit, self.classes, self.n_jobs, X, y)
492
+ return self
493
+
494
+ def predict(self, X) -> dict:
495
+ """Predict class prevalences for the given data.
496
+
497
+ Parameters
498
+ ----------
499
+ X : array-like
500
+ Test features.
501
+
502
+ Returns
503
+ -------
504
+ dict
505
+ A dictionary where keys are class labels and values are their predicted prevalences.
506
+
507
+ Notes
508
+ -----
509
+ - For binary or inherently multiclass data, the model directly calls `_predict_method`.
510
+ - For other cases, it performs one-vs-all prediction, combining the results into a normalized
511
+ dictionary of class prevalences.
512
+ """
513
+ if self.binary_data or self.is_multiclass:
514
+ prevalences = self._predict_method(X)
515
+ return normalize_prevalence(prevalences, self.classes)
516
+
517
+ # One-vs-all approach
518
+ prevalences = np.asarray(parallel(self.delayed_predict, self.classes, self.n_jobs, X))
519
+ return normalize_prevalence(prevalences, self.classes)
520
+
521
+ @abstractmethod
522
+ def _fit_method(self, X, y):
523
+ """Abstract method for fitting the quantifier.
524
+
525
+ Parameters
526
+ ----------
527
+ X : array-like
528
+ Training features.
529
+ y : array-like
530
+ Training labels.
531
+
532
+ Notes
533
+ -----
534
+ This method must be implemented in subclasses to define the fitting logic for
535
+ the non-aggregative quantifier.
536
+ """
537
+ ...
538
+
539
+ @abstractmethod
540
+ def _predict_method(self, X) -> dict:
541
+ """Abstract method for predicting class prevalences.
542
+
543
+ Parameters
544
+ ----------
545
+ X : array-like
546
+ Test features.
547
+
548
+ Returns
549
+ -------
550
+ dict, list, or numpy array
551
+ The predicted prevalences, which can be a dictionary where keys are class labels
552
+ and values are their predicted prevalences, a list, or a numpy array.
553
+
554
+ Notes
555
+ -----
556
+ This method must be implemented in subclasses to define the prediction logic for
557
+ the non-aggregative quantifier.
558
+ """
559
+ ...