Aluode/PerceptionLabPortable
0
1"""Multioutput regression and classification.
2
3The estimators provided in this module are meta-estimators: they require
4a base estimator to be provided in their constructor. The meta-estimator
5extends single output estimators to multioutput estimators.
6"""
7
8# Authors: The scikit-learn developers
9# SPDX-License-Identifier: BSD-3-Clause
10
11import warnings
12from abc import ABCMeta, abstractmethod
13from numbers import Integral
14
15import numpy as np
16import scipy.sparse as sp
17
18from .base import (
19 BaseEstimator,
20 ClassifierMixin,
21 MetaEstimatorMixin,
22 RegressorMixin,
23 _fit_context,
24 clone,
25 is_classifier,
26)
27from .model_selection import cross_val_predict
28from .utils import Bunch, check_random_state, get_tags
29from .utils._param_validation import (
30 HasMethods,
31 Hidden,
32 StrOptions,
33)
34from .utils._response import _get_response_values
35from .utils._user_interface import _print_elapsed_time
36from .utils.metadata_routing import (
37 MetadataRouter,
38 MethodMapping,
39 _raise_for_params,
40 _routing_enabled,
41 process_routing,
42)
43from .utils.metaestimators import available_if
44from .utils.multiclass import check_classification_targets
45from .utils.parallel import Parallel, delayed
46from .utils.validation import (
47 _check_method_params,
48 _check_response_method,
49 check_is_fitted,
50 has_fit_parameter,
51 validate_data,
52)
53
54__all__ = [
55 "ClassifierChain",
56 "MultiOutputClassifier",
57 "MultiOutputRegressor",
58 "RegressorChain",
59]
60
61
62def _fit_estimator(estimator, X, y, sample_weight=None, **fit_params):
63 estimator = clone(estimator)
64 if sample_weight is not None:
65 estimator.fit(X, y, sample_weight=sample_weight, **fit_params)
66 else:
67 estimator.fit(X, y, **fit_params)
68 return estimator
69
70
71def _partial_fit_estimator(
72 estimator, X, y, classes=None, partial_fit_params=None, first_time=True
73):
74 partial_fit_params = {} if partial_fit_params is None else partial_fit_params
75 if first_time:
76 estimator = clone(estimator)
77
78 if classes is not None:
79 estimator.partial_fit(X, y, classes=classes, **partial_fit_params)
80 else:
81 estimator.partial_fit(X, y, **partial_fit_params)
82 return estimator
83
84
85def _available_if_estimator_has(attr):
86 """Return a function to check if the sub-estimator(s) has(have) `attr`.
87
88 Helper for Chain implementations.
89 """
90
91 def _check(self):
92 if hasattr(self, "estimators_"):
93 return all(hasattr(est, attr) for est in self.estimators_)
94
95 if hasattr(self.estimator, attr):
96 return True
97
98 return False
99
100 return available_if(_check)
101
102
103class _MultiOutputEstimator(MetaEstimatorMixin, BaseEstimator, metaclass=ABCMeta):
104 _parameter_constraints: dict = {
105 "estimator": [HasMethods(["fit", "predict"])],
106 "n_jobs": [Integral, None],
107 }
108
109 @abstractmethod
110 def __init__(self, estimator, *, n_jobs=None):
111 self.estimator = estimator
112 self.n_jobs = n_jobs
113
114 @_available_if_estimator_has("partial_fit")
115 @_fit_context(
116 # MultiOutput*.estimator is not validated yet
117 prefer_skip_nested_validation=False
118 )
119 def partial_fit(self, X, y, classes=None, sample_weight=None, **partial_fit_params):
120 """Incrementally fit a separate model for each class output.
121
122 Parameters
123 ----------
124 X : {array-like, sparse matrix} of shape (n_samples, n_features)
125 The input data.
126
127 y : {array-like, sparse matrix} of shape (n_samples, n_outputs)
128 Multi-output targets.
129
130 classes : list of ndarray of shape (n_outputs,), default=None
131 Each array is unique classes for one output in str/int.
132 Can be obtained via
133 ``[np.unique(y[:, i]) for i in range(y.shape[1])]``, where `y`
134 is the target matrix of the entire dataset.
135 This argument is required for the first call to partial_fit
136 and can be omitted in the subsequent calls.
137 Note that `y` doesn't need to contain all labels in `classes`.
138
139 sample_weight : array-like of shape (n_samples,), default=None
140 Sample weights. If `None`, then samples are equally weighted.
141 Only supported if the underlying regressor supports sample
142 weights.
143
144 **partial_fit_params : dict of str -> object
145 Parameters passed to the ``estimator.partial_fit`` method of each
146 sub-estimator.
147
148 Only available if `enable_metadata_routing=True`. See the
149 :ref:`User Guide <metadata_routing>`.
150
151 .. versionadded:: 1.3
152
153 Returns
154 -------
155 self : object
156 Returns a fitted instance.
157 """
158 _raise_for_params(partial_fit_params, self, "partial_fit")
159
160 first_time = not hasattr(self, "estimators_")
161
162 y = validate_data(self, X="no_validation", y=y, multi_output=True)
163
164 if y.ndim == 1:
165 raise ValueError(
166 "y must have at least two dimensions for "
167 "multi-output regression but has only one."
168 )
169
170 if _routing_enabled():
171 if sample_weight is not None:
172 partial_fit_params["sample_weight"] = sample_weight
173 routed_params = process_routing(
174 self,
175 "partial_fit",
176 **partial_fit_params,
177 )
178 else:
179 if sample_weight is not None and not has_fit_parameter(
180 self.estimator, "sample_weight"
181 ):
182 raise ValueError(
183 "Underlying estimator does not support sample weights."
184 )
185
186 if sample_weight is not None:
187 routed_params = Bunch(
188 estimator=Bunch(partial_fit=Bunch(sample_weight=sample_weight))
189 )
190 else:
191 routed_params = Bunch(estimator=Bunch(partial_fit=Bunch()))
192
193 self.estimators_ = Parallel(n_jobs=self.n_jobs)(
194 delayed(_partial_fit_estimator)(
195 self.estimators_[i] if not first_time else self.estimator,
196 X,
197 y[:, i],
198 classes[i] if classes is not None else None,
199 partial_fit_params=routed_params.estimator.partial_fit,
200 first_time=first_time,
201 )
202 for i in range(y.shape[1])
203 )
204
205 if first_time and hasattr(self.estimators_[0], "n_features_in_"):
206 self.n_features_in_ = self.estimators_[0].n_features_in_
207 if first_time and hasattr(self.estimators_[0], "feature_names_in_"):
208 self.feature_names_in_ = self.estimators_[0].feature_names_in_
209
210 return self
211
212 @_fit_context(
213 # MultiOutput*.estimator is not validated yet
214 prefer_skip_nested_validation=False
215 )
216 def fit(self, X, y, sample_weight=None, **fit_params):
217 """Fit the model to data, separately for each output variable.
218
219 Parameters
220 ----------
221 X : {array-like, sparse matrix} of shape (n_samples, n_features)
222 The input data.
223
224 y : {array-like, sparse matrix} of shape (n_samples, n_outputs)
225 Multi-output targets. An indicator matrix turns on multilabel
226 estimation.
227
228 sample_weight : array-like of shape (n_samples,), default=None
229 Sample weights. If `None`, then samples are equally weighted.
230 Only supported if the underlying regressor supports sample
231 weights.
232
233 **fit_params : dict of string -> object
234 Parameters passed to the ``estimator.fit`` method of each step.
235
236 .. versionadded:: 0.23
237
238 Returns
239 -------
240 self : object
241 Returns a fitted instance.
242 """
243 if not hasattr(self.estimator, "fit"):
244 raise ValueError("The base estimator should implement a fit method")
245
246 y = validate_data(self, X="no_validation", y=y, multi_output=True)
247
248 if is_classifier(self):
249 check_classification_targets(y)
250
251 if y.ndim == 1:
252 raise ValueError(
253 "y must have at least two dimensions for "
254 "multi-output regression but has only one."
255 )
256
257 if _routing_enabled():
258 if sample_weight is not None:
259 fit_params["sample_weight"] = sample_weight
260 routed_params = process_routing(
261 self,
262 "fit",
263 **fit_params,
264 )
265 else:
266 if sample_weight is not None and not has_fit_parameter(
267 self.estimator, "sample_weight"
268 ):
269 raise ValueError(
270 "Underlying estimator does not support sample weights."
271 )
272
273 fit_params_validated = _check_method_params(X, params=fit_params)
274 routed_params = Bunch(estimator=Bunch(fit=fit_params_validated))
275 if sample_weight is not None:
276 routed_params.estimator.fit["sample_weight"] = sample_weight
277
278 self.estimators_ = Parallel(n_jobs=self.n_jobs)(
279 delayed(_fit_estimator)(
280 self.estimator, X, y[:, i], **routed_params.estimator.fit
281 )
282 for i in range(y.shape[1])
283 )
284
285 if hasattr(self.estimators_[0], "n_features_in_"):
286 self.n_features_in_ = self.estimators_[0].n_features_in_
287 if hasattr(self.estimators_[0], "feature_names_in_"):
288 self.feature_names_in_ = self.estimators_[0].feature_names_in_
289
290 return self
291
292 def predict(self, X):
293 """Predict multi-output variable using model for each target variable.
294
295 Parameters
296 ----------
297 X : {array-like, sparse matrix} of shape (n_samples, n_features)
298 The input data.
299
300 Returns
301 -------
302 y : {array-like, sparse matrix} of shape (n_samples, n_outputs)
303 Multi-output targets predicted across multiple predictors.
304 Note: Separate models are generated for each predictor.
305 """
306 check_is_fitted(self)
307 if not hasattr(self.estimators_[0], "predict"):
308 raise ValueError("The base estimator should implement a predict method")
309
310 y = Parallel(n_jobs=self.n_jobs)(
311 delayed(e.predict)(X) for e in self.estimators_
312 )
313
314 return np.asarray(y).T
315
316 def __sklearn_tags__(self):
317 tags = super().__sklearn_tags__()
318 tags.input_tags.sparse = get_tags(self.estimator).input_tags.sparse
319 tags.target_tags.single_output = False
320 tags.target_tags.multi_output = True
321 return tags
322
323 def get_metadata_routing(self):
324 """Get metadata routing of this object.
325
326 Please check :ref:`User Guide <metadata_routing>` on how the routing
327 mechanism works.
328
329 .. versionadded:: 1.3
330
331 Returns
332 -------
333 routing : MetadataRouter
334 A :class:`~sklearn.utils.metadata_routing.MetadataRouter` encapsulating
335 routing information.
336 """
337 router = MetadataRouter(owner=self.__class__.__name__).add(
338 estimator=self.estimator,
339 method_mapping=MethodMapping()
340 .add(caller="partial_fit", callee="partial_fit")
341 .add(caller="fit", callee="fit"),
342 )
343 return router
344
345
346class MultiOutputRegressor(RegressorMixin, _MultiOutputEstimator):
347 """Multi target regression.
348
349 This strategy consists of fitting one regressor per target. This is a
350 simple strategy for extending regressors that do not natively support
351 multi-target regression.
352
353 .. versionadded:: 0.18
354
355 Parameters
356 ----------
357 estimator : estimator object
358 An estimator object implementing :term:`fit` and :term:`predict`.
359
360 n_jobs : int or None, optional (default=None)
361 The number of jobs to run in parallel.
362 :meth:`fit`, :meth:`predict` and :meth:`partial_fit` (if supported
363 by the passed estimator) will be parallelized for each target.
364
365 When individual estimators are fast to train or predict,
366 using ``n_jobs > 1`` can result in slower performance due
367 to the parallelism overhead.
368
369 ``None`` means `1` unless in a :obj:`joblib.parallel_backend` context.
370 ``-1`` means using all available processes / threads.
371 See :term:`Glossary <n_jobs>` for more details.
372
373 .. versionchanged:: 0.20
374 `n_jobs` default changed from `1` to `None`.
375
376 Attributes
377 ----------
378 estimators_ : list of ``n_output`` estimators
379 Estimators used for predictions.
380
381 n_features_in_ : int
382 Number of features seen during :term:`fit`. Only defined if the
383 underlying `estimator` exposes such an attribute when fit.
384
385 .. versionadded:: 0.24
386
387 feature_names_in_ : ndarray of shape (`n_features_in_`,)
388 Names of features seen during :term:`fit`. Only defined if the
389 underlying estimators expose such an attribute when fit.
390
391 .. versionadded:: 1.0
392
393 See Also
394 --------
395 RegressorChain : A multi-label model that arranges regressions into a
396 chain.
397 MultiOutputClassifier : Classifies each output independently rather than
398 chaining.
399
400 Examples
401 --------
402 >>> import numpy as np
403 >>> from sklearn.datasets import load_linnerud
404 >>> from sklearn.multioutput import MultiOutputRegressor
405 >>> from sklearn.linear_model import Ridge
406 >>> X, y = load_linnerud(return_X_y=True)
407 >>> regr = MultiOutputRegressor(Ridge(random_state=123)).fit(X, y)
408 >>> regr.predict(X[[0]])
409 array([[176, 35.1, 57.1]])
410 """
411
412 def __init__(self, estimator, *, n_jobs=None):
413 super().__init__(estimator, n_jobs=n_jobs)
414
415 @_available_if_estimator_has("partial_fit")
416 def partial_fit(self, X, y, sample_weight=None, **partial_fit_params):
417 """Incrementally fit the model to data, for each output variable.
418
419 Parameters
420 ----------
421 X : {array-like, sparse matrix} of shape (n_samples, n_features)
422 The input data.
423
424 y : {array-like, sparse matrix} of shape (n_samples, n_outputs)
425 Multi-output targets.
426
427 sample_weight : array-like of shape (n_samples,), default=None
428 Sample weights. If `None`, then samples are equally weighted.
429 Only supported if the underlying regressor supports sample
430 weights.
431
432 **partial_fit_params : dict of str -> object
433 Parameters passed to the ``estimator.partial_fit`` method of each
434 sub-estimator.
435
436 Only available if `enable_metadata_routing=True`. See the
437 :ref:`User Guide <metadata_routing>`.
438
439 .. versionadded:: 1.3
440
441 Returns
442 -------
443 self : object
444 Returns a fitted instance.
445 """
446 super().partial_fit(X, y, sample_weight=sample_weight, **partial_fit_params)
447
448
449class MultiOutputClassifier(ClassifierMixin, _MultiOutputEstimator):
450 """Multi target classification.
451
452 This strategy consists of fitting one classifier per target. This is a
453 simple strategy for extending classifiers that do not natively support
454 multi-target classification.
455
456 Parameters
457 ----------
458 estimator : estimator object
459 An estimator object implementing :term:`fit` and :term:`predict`.
460 A :term:`predict_proba` method will be exposed only if `estimator` implements
461 it.
462
463 n_jobs : int or None, optional (default=None)
464 The number of jobs to run in parallel.
465 :meth:`fit`, :meth:`predict` and :meth:`partial_fit` (if supported
466 by the passed estimator) will be parallelized for each target.
467
468 When individual estimators are fast to train or predict,
469 using ``n_jobs > 1`` can result in slower performance due
470 to the parallelism overhead.
471
472 ``None`` means `1` unless in a :obj:`joblib.parallel_backend` context.
473 ``-1`` means using all available processes / threads.
474 See :term:`Glossary <n_jobs>` for more details.
475
476 .. versionchanged:: 0.20
477 `n_jobs` default changed from `1` to `None`.
478
479 Attributes
480 ----------
481 classes_ : ndarray of shape (n_classes,)
482 Class labels.
483
484 estimators_ : list of ``n_output`` estimators
485 Estimators used for predictions.
486
487 n_features_in_ : int
488 Number of features seen during :term:`fit`. Only defined if the
489 underlying `estimator` exposes such an attribute when fit.
490
491 .. versionadded:: 0.24
492
493 feature_names_in_ : ndarray of shape (`n_features_in_`,)
494 Names of features seen during :term:`fit`. Only defined if the
495 underlying estimators expose such an attribute when fit.
496
497 .. versionadded:: 1.0
498
499 See Also
500 --------
501 ClassifierChain : A multi-label model that arranges binary classifiers
502 into a chain.
503 MultiOutputRegressor : Fits one regressor per target variable.
504
505 Examples
506 --------
507 >>> import numpy as np
508 >>> from sklearn.datasets import make_multilabel_classification
509 >>> from sklearn.multioutput import MultiOutputClassifier
510 >>> from sklearn.linear_model import LogisticRegression
511 >>> X, y = make_multilabel_classification(n_classes=3, random_state=0)
512 >>> clf = MultiOutputClassifier(LogisticRegression()).fit(X, y)
513 >>> clf.predict(X[-2:])
514 array([[1, 1, 1],
515 [1, 0, 1]])
516 """
517
518 def __init__(self, estimator, *, n_jobs=None):
519 super().__init__(estimator, n_jobs=n_jobs)
520
521 def fit(self, X, Y, sample_weight=None, **fit_params):
522 """Fit the model to data matrix X and targets Y.
523
524 Parameters
525 ----------
526 X : {array-like, sparse matrix} of shape (n_samples, n_features)
527 The input data.
528
529 Y : array-like of shape (n_samples, n_classes)
530 The target values.
531
532 sample_weight : array-like of shape (n_samples,), default=None
533 Sample weights. If `None`, then samples are equally weighted.
534 Only supported if the underlying classifier supports sample
535 weights.
536
537 **fit_params : dict of string -> object
538 Parameters passed to the ``estimator.fit`` method of each step.
539
540 .. versionadded:: 0.23
541
542 Returns
543 -------
544 self : object
545 Returns a fitted instance.
546 """
547 super().fit(X, Y, sample_weight=sample_weight, **fit_params)
548 self.classes_ = [estimator.classes_ for estimator in self.estimators_]
549 return self
550
551 def _check_predict_proba(self):
552 if hasattr(self, "estimators_"):
553 # raise an AttributeError if `predict_proba` does not exist for
554 # each estimator
555 [getattr(est, "predict_proba") for est in self.estimators_]
556 return True
557 # raise an AttributeError if `predict_proba` does not exist for the
558 # unfitted estimator
559 getattr(self.estimator, "predict_proba")
560 return True
561
562 @available_if(_check_predict_proba)
563 def predict_proba(self, X):
564 """Return prediction probabilities for each class of each output.
565
566 This method will raise a ``ValueError`` if any of the
567 estimators do not have ``predict_proba``.
568
569 Parameters
570 ----------
571 X : array-like of shape (n_samples, n_features)
572 The input data.
573
574 Returns
575 -------
576 p : array of shape (n_samples, n_classes), or a list of n_outputs \
577 such arrays if n_outputs > 1.
578 The class probabilities of the input samples. The order of the
579 classes corresponds to that in the attribute :term:`classes_`.
580
581 .. versionchanged:: 0.19
582 This function now returns a list of arrays where the length of
583 the list is ``n_outputs``, and each array is (``n_samples``,
584 ``n_classes``) for that particular output.
585 """
586 check_is_fitted(self)
587 results = [estimator.predict_proba(X) for estimator in self.estimators_]
588 return results
589
590 def score(self, X, y):
591 """Return the mean accuracy on the given test data and labels.
592
593 Parameters
594 ----------
595 X : array-like of shape (n_samples, n_features)
596 Test samples.
597
598 y : array-like of shape (n_samples, n_outputs)
599 True values for X.
600
601 Returns
602 -------
603 scores : float
604 Mean accuracy of predicted target versus true target.
605 """
606 check_is_fitted(self)
607 n_outputs_ = len(self.estimators_)
608 if y.ndim == 1:
609 raise ValueError(
610 "y must have at least two dimensions for "
611 "multi target classification but has only one"
612 )
613 if y.shape[1] != n_outputs_:
614 raise ValueError(
615 "The number of outputs of Y for fit {0} and"
616 " score {1} should be same".format(n_outputs_, y.shape[1])
617 )
618 y_pred = self.predict(X)
619 return np.mean(np.all(y == y_pred, axis=1))
620
621 def __sklearn_tags__(self):
622 tags = super().__sklearn_tags__()
623 # FIXME
624 tags._skip_test = True
625 return tags
626
627
628def _available_if_base_estimator_has(attr):
629 """Return a function to check if `base_estimator` or `estimators_` has `attr`.
630
631 Helper for Chain implementations.
632 """
633
634 def _check(self):
635 return hasattr(self._get_estimator(), attr) or all(
636 hasattr(est, attr) for est in self.estimators_
637 )
638
639 return available_if(_check)
640
641
642class _BaseChain(BaseEstimator, metaclass=ABCMeta):
643 _parameter_constraints: dict = {
644 "base_estimator": [
645 HasMethods(["fit", "predict"]),
646 StrOptions({"deprecated"}),
647 ],
648 "estimator": [
649 HasMethods(["fit", "predict"]),
650 Hidden(None),
651 ],
652 "order": ["array-like", StrOptions({"random"}), None],
653 "cv": ["cv_object", StrOptions({"prefit"})],
654 "random_state": ["random_state"],
655 "verbose": ["boolean"],
656 }
657
658 # TODO(1.9): Remove base_estimator
659 def __init__(
660 self,
661 estimator=None,
662 *,
663 order=None,
664 cv=None,
665 random_state=None,
666 verbose=False,
667 base_estimator="deprecated",
668 ):
669 self.estimator = estimator
670 self.base_estimator = base_estimator
671 self.order = order
672 self.cv = cv
673 self.random_state = random_state
674 self.verbose = verbose
675
676 # TODO(1.8): This is a temporary getter method to validate input wrt deprecation.
677 # It was only included to avoid relying on the presence of self.estimator_
678 def _get_estimator(self):
679 """Get and validate estimator."""
680
681 if self.estimator is not None and (self.base_estimator != "deprecated"):
682 raise ValueError(
683 "Both `estimator` and `base_estimator` are provided. You should only"
684 " pass `estimator`. `base_estimator` as a parameter is deprecated in"
685 " version 1.7, and will be removed in version 1.9."
686 )
687
688 if self.base_estimator != "deprecated":
689 warning_msg = (
690 "`base_estimator` as an argument was deprecated in 1.7 and will be"
691 " removed in 1.9. Use `estimator` instead."
692 )
693 warnings.warn(warning_msg, FutureWarning)
694 return self.base_estimator
695 else:
696 return self.estimator
697
698 def _log_message(self, *, estimator_idx, n_estimators, processing_msg):
699 if not self.verbose:
700 return None
701 return f"({estimator_idx} of {n_estimators}) {processing_msg}"
702
703 def _get_predictions(self, X, *, output_method):
704 """Get predictions for each model in the chain."""
705 check_is_fitted(self)
706 X = validate_data(self, X, accept_sparse=True, reset=False)
707 Y_output_chain = np.zeros((X.shape[0], len(self.estimators_)))
708 Y_feature_chain = np.zeros((X.shape[0], len(self.estimators_)))
709
710 # `RegressorChain` does not have a `chain_method_` parameter so we
711 # default to "predict"
712 chain_method = getattr(self, "chain_method_", "predict")
713 hstack = sp.hstack if sp.issparse(X) else np.hstack
714 for chain_idx, estimator in enumerate(self.estimators_):
715 previous_predictions = Y_feature_chain[:, :chain_idx]
716 # if `X` is a scipy sparse dok_array, we convert it to a sparse
717 # coo_array format before hstacking, it's faster; see
718 # https://github.com/scipy/scipy/issues/20060#issuecomment-1937007039:
719 if sp.issparse(X) and not sp.isspmatrix(X) and X.format == "dok":
720 X = sp.coo_array(X)
721 X_aug = hstack((X, previous_predictions))
722
723 feature_predictions, _ = _get_response_values(
724 estimator,
725 X_aug,
726 response_method=chain_method,
727 )
728 Y_feature_chain[:, chain_idx] = feature_predictions
729
730 output_predictions, _ = _get_response_values(
731 estimator,
732 X_aug,
733 response_method=output_method,
734 )
735 Y_output_chain[:, chain_idx] = output_predictions
736
737 inv_order = np.empty_like(self.order_)
738 inv_order[self.order_] = np.arange(len(self.order_))
739 Y_output = Y_output_chain[:, inv_order]
740
741 return Y_output
742
743 @abstractmethod
744 def fit(self, X, Y, **fit_params):
745 """Fit the model to data matrix X and targets Y.
746
747 Parameters
748 ----------
749 X : {array-like, sparse matrix} of shape (n_samples, n_features)
750 The input data.
751
752 Y : array-like of shape (n_samples, n_classes)
753 The target values.
754
755 **fit_params : dict of string -> object
756 Parameters passed to the `fit` method of each step.
757
758 .. versionadded:: 0.23
759
760 Returns
761 -------
762 self : object
763 Returns a fitted instance.
764 """
765 X, Y = validate_data(self, X, Y, multi_output=True, accept_sparse=True)
766
767 random_state = check_random_state(self.random_state)
768 self.order_ = self.order
769 if isinstance(self.order_, tuple):
770 self.order_ = np.array(self.order_)
771
772 if self.order_ is None:
773 self.order_ = np.array(range(Y.shape[1]))
774 elif isinstance(self.order_, str):
775 if self.order_ == "random":
776 self.order_ = random_state.permutation(Y.shape[1])
777 elif sorted(self.order_) != list(range(Y.shape[1])):
778 raise ValueError("invalid order")
779
780 self.estimators_ = [clone(self._get_estimator()) for _ in range(Y.shape[1])]
781
782 if self.cv is None:
783 Y_pred_chain = Y[:, self.order_]
784 if sp.issparse(X):
785 X_aug = sp.hstack((X, Y_pred_chain), format="lil")
786 X_aug = X_aug.tocsr()
787 else:
788 X_aug = np.hstack((X, Y_pred_chain))
789
790 elif sp.issparse(X):
791 # TODO: remove this condition check when the minimum supported scipy version
792 # doesn't support sparse matrices anymore
793 if not sp.isspmatrix(X):
794 # if `X` is a scipy sparse dok_array, we convert it to a sparse
795 # coo_array format before hstacking, it's faster; see
796 # https://github.com/scipy/scipy/issues/20060#issuecomment-1937007039:
797 if X.format == "dok":
798 X = sp.coo_array(X)
799 # in case that `X` is a sparse array we create `Y_pred_chain` as a
800 # sparse array format:
801 Y_pred_chain = sp.coo_array((X.shape[0], Y.shape[1]))
802 else:
803 Y_pred_chain = sp.coo_matrix((X.shape[0], Y.shape[1]))
804 X_aug = sp.hstack((X, Y_pred_chain), format="lil")
805
806 else:
807 Y_pred_chain = np.zeros((X.shape[0], Y.shape[1]))
808 X_aug = np.hstack((X, Y_pred_chain))
809
810 del Y_pred_chain
811
812 if _routing_enabled():
813 routed_params = process_routing(self, "fit", **fit_params)
814 else:
815 routed_params = Bunch(estimator=Bunch(fit=fit_params))
816
817 if hasattr(self, "chain_method"):
818 chain_method = _check_response_method(
819 self._get_estimator(),
820 self.chain_method,
821 ).__name__
822 self.chain_method_ = chain_method
823 else:
824 # `RegressorChain` does not have a `chain_method` parameter
825 chain_method = "predict"
826
827 for chain_idx, estimator in enumerate(self.estimators_):
828 message = self._log_message(
829 estimator_idx=chain_idx + 1,
830 n_estimators=len(self.estimators_),
831 processing_msg=f"Processing order {self.order_[chain_idx]}",
832 )
833 y = Y[:, self.order_[chain_idx]]
834 with _print_elapsed_time("Chain", message):
835 estimator.fit(
836 X_aug[:, : (X.shape[1] + chain_idx)],
837 y,
838 **routed_params.estimator.fit,
839 )
840
841 if self.cv is not None and chain_idx < len(self.estimators_) - 1:
842 col_idx = X.shape[1] + chain_idx
843 cv_result = cross_val_predict(
844 self._get_estimator(),
845 X_aug[:, :col_idx],
846 y=y,
847 cv=self.cv,
848 method=chain_method,
849 )
850 # `predict_proba` output is 2D, we use only output for classes[-1]
851 if cv_result.ndim > 1:
852 cv_result = cv_result[:, 1]
853 if sp.issparse(X_aug):
854 X_aug[:, col_idx] = np.expand_dims(cv_result, 1)
855 else:
856 X_aug[:, col_idx] = cv_result
857
858 return self
859
860 def predict(self, X):
861 """Predict on the data matrix X using the ClassifierChain model.
862
863 Parameters
864 ----------
865 X : {array-like, sparse matrix} of shape (n_samples, n_features)
866 The input data.
867
868 Returns
869 -------
870 Y_pred : array-like of shape (n_samples, n_classes)
871 The predicted values.
872 """
873 return self._get_predictions(X, output_method="predict")
874
875 def __sklearn_tags__(self):
876 tags = super().__sklearn_tags__()
877 tags.input_tags.sparse = get_tags(self._get_estimator()).input_tags.sparse
878 return tags
879
880
881class ClassifierChain(MetaEstimatorMixin, ClassifierMixin, _BaseChain):
882 """A multi-label model that arranges binary classifiers into a chain.
883
884 Each model makes a prediction in the order specified by the chain using
885 all of the available features provided to the model plus the predictions
886 of models that are earlier in the chain.
887
888 For an example of how to use ``ClassifierChain`` and benefit from its
889 ensemble, see
890 :ref:`ClassifierChain on a yeast dataset
891 <sphx_glr_auto_examples_multioutput_plot_classifier_chain_yeast.py>` example.
892
893 Read more in the :ref:`User Guide <classifierchain>`.
894
895 .. versionadded:: 0.19
896
897 Parameters
898 ----------
899 estimator : estimator
900 The base estimator from which the classifier chain is built.
901
902 order : array-like of shape (n_outputs,) or 'random', default=None
903 If `None`, the order will be determined by the order of columns in
904 the label matrix Y.::
905
906 order = [0, 1, 2, ..., Y.shape[1] - 1]
907
908 The order of the chain can be explicitly set by providing a list of
909 integers. For example, for a chain of length 5.::
910
911 order = [1, 3, 2, 4, 0]
912
913 means that the first model in the chain will make predictions for
914 column 1 in the Y matrix, the second model will make predictions
915 for column 3, etc.
916
917 If order is `random` a random ordering will be used.
918
919 cv : int, cross-validation generator or an iterable, default=None
920 Determines whether to use cross validated predictions or true
921 labels for the results of previous estimators in the chain.
922 Possible inputs for cv are:
923
924 - None, to use true labels when fitting,
925 - integer, to specify the number of folds in a (Stratified)KFold,
926 - :term:`CV splitter`,
927 - An iterable yielding (train, test) splits as arrays of indices.
928
929 chain_method : {'predict', 'predict_proba', 'predict_log_proba', \
930 'decision_function'} or list of such str's, default='predict'
931
932 Prediction method to be used by estimators in the chain for
933 the 'prediction' features of previous estimators in the chain.
934
935 - if `str`, name of the method;
936 - if a list of `str`, provides the method names in order of
937 preference. The method used corresponds to the first method in
938 the list that is implemented by `base_estimator`.
939
940 .. versionadded:: 1.5
941
942 random_state : int, RandomState instance or None, optional (default=None)
943 If ``order='random'``, determines random number generation for the
944 chain order.
945 In addition, it controls the random seed given at each `base_estimator`
946 at each chaining iteration. Thus, it is only used when `base_estimator`
947 exposes a `random_state`.
948 Pass an int for reproducible output across multiple function calls.
949 See :term:`Glossary <random_state>`.
950
951 verbose : bool, default=False
952 If True, chain progress is output as each model is completed.
953
954 .. versionadded:: 1.2
955
956 base_estimator : estimator, default="deprecated"
957 Use `estimator` instead.
958
959 .. deprecated:: 1.7
960 `base_estimator` is deprecated and will be removed in 1.9.
961 Use `estimator` instead.
962
963 Attributes
964 ----------
965 classes_ : list
966 A list of arrays of length ``len(estimators_)`` containing the
967 class labels for each estimator in the chain.
968
969 estimators_ : list
970 A list of clones of base_estimator.
971
972 order_ : list
973 The order of labels in the classifier chain.
974
975 chain_method_ : str
976 Prediction method used by estimators in the chain for the prediction
977 features.
978
979 n_features_in_ : int
980 Number of features seen during :term:`fit`. Only defined if the
981 underlying `base_estimator` exposes such an attribute when fit.
982
983 .. versionadded:: 0.24
984
985 feature_names_in_ : ndarray of shape (`n_features_in_`,)
986 Names of features seen during :term:`fit`. Defined only when `X`
987 has feature names that are all strings.
988
989 .. versionadded:: 1.0
990
991 See Also
992 --------
993 RegressorChain : Equivalent for regression.
994 MultiOutputClassifier : Classifies each output independently rather than
995 chaining.
996
997 References
998 ----------
999 Jesse Read, Bernhard Pfahringer, Geoff Holmes, Eibe Frank, "Classifier
1000 Chains for Multi-label Classification", 2009.
1001
1002 Examples
1003 --------
1004 >>> from sklearn.datasets import make_multilabel_classification
1005 >>> from sklearn.linear_model import LogisticRegression
1006 >>> from sklearn.model_selection import train_test_split
1007 >>> from sklearn.multioutput import ClassifierChain
1008 >>> X, Y = make_multilabel_classification(
1009 ... n_samples=12, n_classes=3, random_state=0
1010 ... )
1011 >>> X_train, X_test, Y_train, Y_test = train_test_split(
1012 ... X, Y, random_state=0
1013 ... )
1014 >>> base_lr = LogisticRegression(solver='lbfgs', random_state=0)
1015 >>> chain = ClassifierChain(base_lr, order='random', random_state=0)
1016 >>> chain.fit(X_train, Y_train).predict(X_test)
1017 array([[1., 1., 0.],
1018 [1., 0., 0.],
1019 [0., 1., 0.]])
1020 >>> chain.predict_proba(X_test)
1021 array([[0.8387, 0.9431, 0.4576],
1022 [0.8878, 0.3684, 0.2640],
1023 [0.0321, 0.9935, 0.0626]])
1024 """
1025
1026 _parameter_constraints: dict = {
1027 **_BaseChain._parameter_constraints,
1028 "chain_method": [
1029 list,
1030 tuple,
1031 StrOptions(
1032 {"predict", "predict_proba", "predict_log_proba", "decision_function"}
1033 ),
1034 ],
1035 }
1036
1037 # TODO(1.9): Remove base_estimator from __init__
1038 def __init__(
1039 self,
1040 estimator=None,
1041 *,
1042 order=None,
1043 cv=None,
1044 chain_method="predict",
1045 random_state=None,
1046 verbose=False,
1047 base_estimator="deprecated",
1048 ):
1049 super().__init__(
1050 estimator,
1051 order=order,
1052 cv=cv,
1053 random_state=random_state,
1054 verbose=verbose,
1055 base_estimator=base_estimator,
1056 )
1057 self.chain_method = chain_method
1058
1059 @_fit_context(
1060 # ClassifierChain.base_estimator is not validated yet
1061 prefer_skip_nested_validation=False
1062 )
1063 def fit(self, X, Y, **fit_params):
1064 """Fit the model to data matrix X and targets Y.
1065
1066 Parameters
1067 ----------
1068 X : {array-like, sparse matrix} of shape (n_samples, n_features)
1069 The input data.
1070
1071 Y : array-like of shape (n_samples, n_classes)
1072 The target values.
1073
1074 **fit_params : dict of string -> object
1075 Parameters passed to the `fit` method of each step.
1076
1077 Only available if `enable_metadata_routing=True`. See the
1078 :ref:`User Guide <metadata_routing>`.
1079
1080 .. versionadded:: 1.3
1081
1082 Returns
1083 -------
1084 self : object
1085 Class instance.
1086 """
1087 _raise_for_params(fit_params, self, "fit")
1088
1089 super().fit(X, Y, **fit_params)
1090 self.classes_ = [estimator.classes_ for estimator in self.estimators_]
1091 return self
1092
1093 @_available_if_base_estimator_has("predict_proba")
1094 def predict_proba(self, X):
1095 """Predict probability estimates.
1096
1097 Parameters
1098 ----------
1099 X : {array-like, sparse matrix} of shape (n_samples, n_features)
1100 The input data.
1101
1102 Returns
1103 -------
1104 Y_prob : array-like of shape (n_samples, n_classes)
1105 The predicted probabilities.
1106 """
1107 return self._get_predictions(X, output_method="predict_proba")
1108
1109 def predict_log_proba(self, X):
1110 """Predict logarithm of probability estimates.
1111
1112 Parameters
1113 ----------
1114 X : {array-like, sparse matrix} of shape (n_samples, n_features)
1115 The input data.
1116
1117 Returns
1118 -------
1119 Y_log_prob : array-like of shape (n_samples, n_classes)
1120 The predicted logarithm of the probabilities.
1121 """
1122 return np.log(self.predict_proba(X))
1123
1124 @_available_if_base_estimator_has("decision_function")
1125 def decision_function(self, X):
1126 """Evaluate the decision_function of the models in the chain.
1127
1128 Parameters
1129 ----------
1130 X : array-like of shape (n_samples, n_features)
1131 The input data.
1132
1133 Returns
1134 -------
1135 Y_decision : array-like of shape (n_samples, n_classes)
1136 Returns the decision function of the sample for each model
1137 in the chain.
1138 """
1139 return self._get_predictions(X, output_method="decision_function")
1140
1141 def get_metadata_routing(self):
1142 """Get metadata routing of this object.
1143
1144 Please check :ref:`User Guide <metadata_routing>` on how the routing
1145 mechanism works.
1146
1147 .. versionadded:: 1.3
1148
1149 Returns
1150 -------
1151 routing : MetadataRouter
1152 A :class:`~sklearn.utils.metadata_routing.MetadataRouter` encapsulating
1153 routing information.
1154 """
1155
1156 router = MetadataRouter(owner=self.__class__.__name__).add(
1157 estimator=self._get_estimator(),
1158 method_mapping=MethodMapping().add(caller="fit", callee="fit"),
1159 )
1160 return router
1161
1162 def __sklearn_tags__(self):
1163 tags = super().__sklearn_tags__()
1164 # FIXME
1165 tags._skip_test = True
1166 tags.target_tags.single_output = False
1167 tags.target_tags.multi_output = True
1168 return tags
1169
1170
1171class RegressorChain(MetaEstimatorMixin, RegressorMixin, _BaseChain):
1172 """A multi-label model that arranges regressions into a chain.
1173
1174 Each model makes a prediction in the order specified by the chain using
1175 all of the available features provided to the model plus the predictions
1176 of models that are earlier in the chain.
1177
1178 Read more in the :ref:`User Guide <regressorchain>`.
1179
1180 .. versionadded:: 0.20
1181
1182 Parameters
1183 ----------
1184 estimator : estimator
1185 The base estimator from which the regressor chain is built.
1186
1187 order : array-like of shape (n_outputs,) or 'random', default=None
1188 If `None`, the order will be determined by the order of columns in
1189 the label matrix Y.::
1190
1191 order = [0, 1, 2, ..., Y.shape[1] - 1]
1192
1193 The order of the chain can be explicitly set by providing a list of
1194 integers. For example, for a chain of length 5.::
1195
1196 order = [1, 3, 2, 4, 0]
1197
1198 means that the first model in the chain will make predictions for
1199 column 1 in the Y matrix, the second model will make predictions
1200 for column 3, etc.
