_voting.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
  1. """
  2. Soft Voting/Majority Rule classifier and Voting regressor.
  3. This module contains:
  4. - A Soft Voting/Majority Rule classifier for classification estimators.
  5. - A Voting regressor for regression estimators.
  6. """
  7. # Authors: Sebastian Raschka <se.raschka@gmail.com>,
  8. # Gilles Louppe <g.louppe@gmail.com>,
  9. # Ramil Nugmanov <stsouko@live.ru>
  10. # Mohamed Ali Jamaoui <m.ali.jamaoui@gmail.com>
  11. #
  12. # License: BSD 3 clause
  13. from abc import abstractmethod
  14. from numbers import Integral
  15. import numpy as np
  16. from ..base import (
  17. ClassifierMixin,
  18. RegressorMixin,
  19. TransformerMixin,
  20. _fit_context,
  21. clone,
  22. )
  23. from ..exceptions import NotFittedError
  24. from ..preprocessing import LabelEncoder
  25. from ..utils import Bunch
  26. from ..utils._estimator_html_repr import _VisualBlock
  27. from ..utils._param_validation import StrOptions
  28. from ..utils.metaestimators import available_if
  29. from ..utils.multiclass import check_classification_targets
  30. from ..utils.parallel import Parallel, delayed
  31. from ..utils.validation import _check_feature_names_in, check_is_fitted, column_or_1d
  32. from ._base import _BaseHeterogeneousEnsemble, _fit_single_estimator
  33. class _BaseVoting(TransformerMixin, _BaseHeterogeneousEnsemble):
  34. """Base class for voting.
  35. Warning: This class should not be used directly. Use derived classes
  36. instead.
  37. """
  38. _parameter_constraints: dict = {
  39. "estimators": [list],
  40. "weights": ["array-like", None],
  41. "n_jobs": [None, Integral],
  42. "verbose": ["verbose"],
  43. }
  44. def _log_message(self, name, idx, total):
  45. if not self.verbose:
  46. return None
  47. return f"({idx} of {total}) Processing {name}"
  48. @property
  49. def _weights_not_none(self):
  50. """Get the weights of not `None` estimators."""
  51. if self.weights is None:
  52. return None
  53. return [w for est, w in zip(self.estimators, self.weights) if est[1] != "drop"]
  54. def _predict(self, X):
  55. """Collect results from clf.predict calls."""
  56. return np.asarray([est.predict(X) for est in self.estimators_]).T
  57. @abstractmethod
  58. def fit(self, X, y, sample_weight=None):
  59. """Get common fit operations."""
  60. names, clfs = self._validate_estimators()
  61. if self.weights is not None and len(self.weights) != len(self.estimators):
  62. raise ValueError(
  63. "Number of `estimators` and weights must be equal; got"
  64. f" {len(self.weights)} weights, {len(self.estimators)} estimators"
  65. )
  66. self.estimators_ = Parallel(n_jobs=self.n_jobs)(
  67. delayed(_fit_single_estimator)(
  68. clone(clf),
  69. X,
  70. y,
  71. sample_weight=sample_weight,
  72. message_clsname="Voting",
  73. message=self._log_message(names[idx], idx + 1, len(clfs)),
  74. )
  75. for idx, clf in enumerate(clfs)
  76. if clf != "drop"
  77. )
  78. self.named_estimators_ = Bunch()
  79. # Uses 'drop' as placeholder for dropped estimators
  80. est_iter = iter(self.estimators_)
  81. for name, est in self.estimators:
  82. current_est = est if est == "drop" else next(est_iter)
  83. self.named_estimators_[name] = current_est
  84. if hasattr(current_est, "feature_names_in_"):
  85. self.feature_names_in_ = current_est.feature_names_in_
  86. return self
  87. def fit_transform(self, X, y=None, **fit_params):
  88. """Return class labels or probabilities for each estimator.
  89. Return predictions for X for each estimator.
  90. Parameters
  91. ----------
  92. X : {array-like, sparse matrix, dataframe} of shape \
  93. (n_samples, n_features)
  94. Input samples.
  95. y : ndarray of shape (n_samples,), default=None
  96. Target values (None for unsupervised transformations).
  97. **fit_params : dict
  98. Additional fit parameters.
  99. Returns
  100. -------
  101. X_new : ndarray array of shape (n_samples, n_features_new)
  102. Transformed array.
  103. """
  104. return super().fit_transform(X, y, **fit_params)
  105. @property
  106. def n_features_in_(self):
  107. """Number of features seen during :term:`fit`."""
  108. # For consistency with other estimators we raise a AttributeError so
  109. # that hasattr() fails if the estimator isn't fitted.
  110. try:
  111. check_is_fitted(self)
  112. except NotFittedError as nfe:
  113. raise AttributeError(
  114. "{} object has no n_features_in_ attribute.".format(
  115. self.__class__.__name__
  116. )
  117. ) from nfe
  118. return self.estimators_[0].n_features_in_
  119. def _sk_visual_block_(self):
  120. names, estimators = zip(*self.estimators)
  121. return _VisualBlock("parallel", estimators, names=names)
  122. def _more_tags(self):
  123. return {"preserves_dtype": []}
  124. class VotingClassifier(ClassifierMixin, _BaseVoting):
  125. """Soft Voting/Majority Rule classifier for unfitted estimators.
  126. Read more in the :ref:`User Guide <voting_classifier>`.
  127. .. versionadded:: 0.17
  128. Parameters
  129. ----------
  130. estimators : list of (str, estimator) tuples
  131. Invoking the ``fit`` method on the ``VotingClassifier`` will fit clones
  132. of those original estimators that will be stored in the class attribute
  133. ``self.estimators_``. An estimator can be set to ``'drop'`` using
  134. :meth:`set_params`.
  135. .. versionchanged:: 0.21
  136. ``'drop'`` is accepted. Using None was deprecated in 0.22 and
  137. support was removed in 0.24.
  138. voting : {'hard', 'soft'}, default='hard'
  139. If 'hard', uses predicted class labels for majority rule voting.
  140. Else if 'soft', predicts the class label based on the argmax of
  141. the sums of the predicted probabilities, which is recommended for
  142. an ensemble of well-calibrated classifiers.
  143. weights : array-like of shape (n_classifiers,), default=None
  144. Sequence of weights (`float` or `int`) to weight the occurrences of
  145. predicted class labels (`hard` voting) or class probabilities
  146. before averaging (`soft` voting). Uses uniform weights if `None`.
  147. n_jobs : int, default=None
  148. The number of jobs to run in parallel for ``fit``.
  149. ``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
  150. ``-1`` means using all processors. See :term:`Glossary <n_jobs>`
  151. for more details.
  152. .. versionadded:: 0.18
  153. flatten_transform : bool, default=True
  154. Affects shape of transform output only when voting='soft'
  155. If voting='soft' and flatten_transform=True, transform method returns
  156. matrix with shape (n_samples, n_classifiers * n_classes). If
  157. flatten_transform=False, it returns
  158. (n_classifiers, n_samples, n_classes).
  159. verbose : bool, default=False
  160. If True, the time elapsed while fitting will be printed as it
  161. is completed.
  162. .. versionadded:: 0.23
  163. Attributes
  164. ----------
  165. estimators_ : list of classifiers
  166. The collection of fitted sub-estimators as defined in ``estimators``
  167. that are not 'drop'.
  168. named_estimators_ : :class:`~sklearn.utils.Bunch`
  169. Attribute to access any fitted sub-estimators by name.
  170. .. versionadded:: 0.20
  171. le_ : :class:`~sklearn.preprocessing.LabelEncoder`
  172. Transformer used to encode the labels during fit and decode during
  173. prediction.
  174. classes_ : ndarray of shape (n_classes,)
  175. The classes labels.
  176. n_features_in_ : int
  177. Number of features seen during :term:`fit`. Only defined if the
  178. underlying classifier exposes such an attribute when fit.
  179. .. versionadded:: 0.24
  180. feature_names_in_ : ndarray of shape (`n_features_in_`,)
  181. Names of features seen during :term:`fit`. Only defined if the
  182. underlying estimators expose such an attribute when fit.
  183. .. versionadded:: 1.0
  184. See Also
  185. --------
  186. VotingRegressor : Prediction voting regressor.
  187. Examples
  188. --------
  189. >>> import numpy as np
  190. >>> from sklearn.linear_model import LogisticRegression
  191. >>> from sklearn.naive_bayes import GaussianNB
  192. >>> from sklearn.ensemble import RandomForestClassifier, VotingClassifier
  193. >>> clf1 = LogisticRegression(multi_class='multinomial', random_state=1)
  194. >>> clf2 = RandomForestClassifier(n_estimators=50, random_state=1)
  195. >>> clf3 = GaussianNB()
  196. >>> X = np.array([[-1, -1], [-2, -1], [-3, -2], [1, 1], [2, 1], [3, 2]])
  197. >>> y = np.array([1, 1, 1, 2, 2, 2])
  198. >>> eclf1 = VotingClassifier(estimators=[
  199. ... ('lr', clf1), ('rf', clf2), ('gnb', clf3)], voting='hard')
  200. >>> eclf1 = eclf1.fit(X, y)
  201. >>> print(eclf1.predict(X))
  202. [1 1 1 2 2 2]
  203. >>> np.array_equal(eclf1.named_estimators_.lr.predict(X),
  204. ... eclf1.named_estimators_['lr'].predict(X))
  205. True
  206. >>> eclf2 = VotingClassifier(estimators=[
  207. ... ('lr', clf1), ('rf', clf2), ('gnb', clf3)],
  208. ... voting='soft')
  209. >>> eclf2 = eclf2.fit(X, y)
  210. >>> print(eclf2.predict(X))
  211. [1 1 1 2 2 2]
  212. To drop an estimator, :meth:`set_params` can be used to remove it. Here we
  213. dropped one of the estimators, resulting in 2 fitted estimators:
  214. >>> eclf2 = eclf2.set_params(lr='drop')
  215. >>> eclf2 = eclf2.fit(X, y)
  216. >>> len(eclf2.estimators_)
  217. 2
  218. Setting `flatten_transform=True` with `voting='soft'` flattens output shape of
  219. `transform`:
  220. >>> eclf3 = VotingClassifier(estimators=[
  221. ... ('lr', clf1), ('rf', clf2), ('gnb', clf3)],
  222. ... voting='soft', weights=[2,1,1],
  223. ... flatten_transform=True)
  224. >>> eclf3 = eclf3.fit(X, y)
  225. >>> print(eclf3.predict(X))
  226. [1 1 1 2 2 2]
  227. >>> print(eclf3.transform(X).shape)
  228. (6, 6)
  229. """
  230. _parameter_constraints: dict = {
  231. **_BaseVoting._parameter_constraints,
  232. "voting": [StrOptions({"hard", "soft"})],
  233. "flatten_transform": ["boolean"],
  234. }
  235. def __init__(
  236. self,
  237. estimators,
  238. *,
  239. voting="hard",
  240. weights=None,
  241. n_jobs=None,
  242. flatten_transform=True,
  243. verbose=False,
  244. ):
  245. super().__init__(estimators=estimators)
  246. self.voting = voting
  247. self.weights = weights
  248. self.n_jobs = n_jobs
  249. self.flatten_transform = flatten_transform
  250. self.verbose = verbose
  251. @_fit_context(
  252. # estimators in VotingClassifier.estimators are not validated yet
  253. prefer_skip_nested_validation=False
  254. )
  255. def fit(self, X, y, sample_weight=None):
  256. """Fit the estimators.
  257. Parameters
  258. ----------
  259. X : {array-like, sparse matrix} of shape (n_samples, n_features)
  260. Training vectors, where `n_samples` is the number of samples and
  261. `n_features` is the number of features.
  262. y : array-like of shape (n_samples,)
  263. Target values.
  264. sample_weight : array-like of shape (n_samples,), default=None
  265. Sample weights. If None, then samples are equally weighted.
  266. Note that this is supported only if all underlying estimators
  267. support sample weights.
  268. .. versionadded:: 0.18
  269. Returns
  270. -------
  271. self : object
  272. Returns the instance itself.
  273. """
  274. check_classification_targets(y)
  275. if isinstance(y, np.ndarray) and len(y.shape) > 1 and y.shape[1] > 1:
  276. raise NotImplementedError(
  277. "Multilabel and multi-output classification is not supported."
  278. )
  279. self.le_ = LabelEncoder().fit(y)
  280. self.classes_ = self.le_.classes_
  281. transformed_y = self.le_.transform(y)
  282. return super().fit(X, transformed_y, sample_weight)
  283. def predict(self, X):
  284. """Predict class labels for X.
  285. Parameters
  286. ----------
  287. X : {array-like, sparse matrix} of shape (n_samples, n_features)
  288. The input samples.
  289. Returns
  290. -------
  291. maj : array-like of shape (n_samples,)
  292. Predicted class labels.
  293. """
  294. check_is_fitted(self)
  295. if self.voting == "soft":
  296. maj = np.argmax(self.predict_proba(X), axis=1)
  297. else: # 'hard' voting
  298. predictions = self._predict(X)
  299. maj = np.apply_along_axis(
  300. lambda x: np.argmax(np.bincount(x, weights=self._weights_not_none)),
  301. axis=1,
  302. arr=predictions,
  303. )
  304. maj = self.le_.inverse_transform(maj)
  305. return maj
  306. def _collect_probas(self, X):
  307. """Collect results from clf.predict calls."""
  308. return np.asarray([clf.predict_proba(X) for clf in self.estimators_])
  309. def _check_voting(self):
  310. if self.voting == "hard":
  311. raise AttributeError(
  312. f"predict_proba is not available when voting={repr(self.voting)}"
  313. )
  314. return True
  315. @available_if(_check_voting)
  316. def predict_proba(self, X):
  317. """Compute probabilities of possible outcomes for samples in X.
  318. Parameters
  319. ----------
  320. X : {array-like, sparse matrix} of shape (n_samples, n_features)
  321. The input samples.
  322. Returns
  323. -------
  324. avg : array-like of shape (n_samples, n_classes)
  325. Weighted average probability for each class per sample.
  326. """
  327. check_is_fitted(self)
  328. avg = np.average(
  329. self._collect_probas(X), axis=0, weights=self._weights_not_none
  330. )
  331. return avg
  332. def transform(self, X):
  333. """Return class labels or probabilities for X for each estimator.
  334. Parameters
  335. ----------
  336. X : {array-like, sparse matrix} of shape (n_samples, n_features)
  337. Training vectors, where `n_samples` is the number of samples and
  338. `n_features` is the number of features.
  339. Returns
  340. -------
  341. probabilities_or_labels
  342. If `voting='soft'` and `flatten_transform=True`:
  343. returns ndarray of shape (n_samples, n_classifiers * n_classes),
  344. being class probabilities calculated by each classifier.
  345. If `voting='soft' and `flatten_transform=False`:
  346. ndarray of shape (n_classifiers, n_samples, n_classes)
  347. If `voting='hard'`:
  348. ndarray of shape (n_samples, n_classifiers), being
  349. class labels predicted by each classifier.
  350. """
  351. check_is_fitted(self)
  352. if self.voting == "soft":
  353. probas = self._collect_probas(X)
  354. if not self.flatten_transform:
  355. return probas
  356. return np.hstack(probas)
  357. else:
  358. return self._predict(X)
  359. def get_feature_names_out(self, input_features=None):
  360. """Get output feature names for transformation.
  361. Parameters
  362. ----------
  363. input_features : array-like of str or None, default=None
  364. Not used, present here for API consistency by convention.
  365. Returns
  366. -------
  367. feature_names_out : ndarray of str objects
  368. Transformed feature names.
  369. """
  370. check_is_fitted(self, "n_features_in_")
  371. if self.voting == "soft" and not self.flatten_transform:
  372. raise ValueError(
  373. "get_feature_names_out is not supported when `voting='soft'` and "
  374. "`flatten_transform=False`"
  375. )
  376. _check_feature_names_in(self, input_features, generate_names=False)
  377. class_name = self.__class__.__name__.lower()
  378. active_names = [name for name, est in self.estimators if est != "drop"]
  379. if self.voting == "hard":
  380. return np.asarray(
  381. [f"{class_name}_{name}" for name in active_names], dtype=object
  382. )
  383. # voting == "soft"
  384. n_classes = len(self.classes_)
  385. names_out = [
  386. f"{class_name}_{name}{i}" for name in active_names for i in range(n_classes)
  387. ]
  388. return np.asarray(names_out, dtype=object)
  389. class VotingRegressor(RegressorMixin, _BaseVoting):
  390. """Prediction voting regressor for unfitted estimators.
  391. A voting regressor is an ensemble meta-estimator that fits several base
  392. regressors, each on the whole dataset. Then it averages the individual
  393. predictions to form a final prediction.
  394. Read more in the :ref:`User Guide <voting_regressor>`.
  395. .. versionadded:: 0.21
  396. Parameters
  397. ----------
  398. estimators : list of (str, estimator) tuples
  399. Invoking the ``fit`` method on the ``VotingRegressor`` will fit clones
  400. of those original estimators that will be stored in the class attribute
  401. ``self.estimators_``. An estimator can be set to ``'drop'`` using
  402. :meth:`set_params`.
  403. .. versionchanged:: 0.21
  404. ``'drop'`` is accepted. Using None was deprecated in 0.22 and
  405. support was removed in 0.24.
  406. weights : array-like of shape (n_regressors,), default=None
  407. Sequence of weights (`float` or `int`) to weight the occurrences of
  408. predicted values before averaging. Uses uniform weights if `None`.
  409. n_jobs : int, default=None
  410. The number of jobs to run in parallel for ``fit``.
  411. ``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
  412. ``-1`` means using all processors. See :term:`Glossary <n_jobs>`
  413. for more details.
  414. verbose : bool, default=False
  415. If True, the time elapsed while fitting will be printed as it
  416. is completed.
  417. .. versionadded:: 0.23
  418. Attributes
  419. ----------
  420. estimators_ : list of regressors
  421. The collection of fitted sub-estimators as defined in ``estimators``
  422. that are not 'drop'.
  423. named_estimators_ : :class:`~sklearn.utils.Bunch`
  424. Attribute to access any fitted sub-estimators by name.
  425. .. versionadded:: 0.20
  426. n_features_in_ : int
  427. Number of features seen during :term:`fit`. Only defined if the
  428. underlying regressor exposes such an attribute when fit.
  429. .. versionadded:: 0.24
  430. feature_names_in_ : ndarray of shape (`n_features_in_`,)
  431. Names of features seen during :term:`fit`. Only defined if the
  432. underlying estimators expose such an attribute when fit.
  433. .. versionadded:: 1.0
  434. See Also
  435. --------
  436. VotingClassifier : Soft Voting/Majority Rule classifier.
  437. Examples
  438. --------
  439. >>> import numpy as np
  440. >>> from sklearn.linear_model import LinearRegression
  441. >>> from sklearn.ensemble import RandomForestRegressor
  442. >>> from sklearn.ensemble import VotingRegressor
  443. >>> from sklearn.neighbors import KNeighborsRegressor
  444. >>> r1 = LinearRegression()
  445. >>> r2 = RandomForestRegressor(n_estimators=10, random_state=1)
  446. >>> r3 = KNeighborsRegressor()
  447. >>> X = np.array([[1, 1], [2, 4], [3, 9], [4, 16], [5, 25], [6, 36]])
  448. >>> y = np.array([2, 6, 12, 20, 30, 42])
  449. >>> er = VotingRegressor([('lr', r1), ('rf', r2), ('r3', r3)])
  450. >>> print(er.fit(X, y).predict(X))
  451. [ 6.8... 8.4... 12.5... 17.8... 26... 34...]
  452. In the following example, we drop the `'lr'` estimator with
  453. :meth:`~VotingRegressor.set_params` and fit the remaining two estimators:
  454. >>> er = er.set_params(lr='drop')
  455. >>> er = er.fit(X, y)
  456. >>> len(er.estimators_)
  457. 2
  458. """
  459. def __init__(self, estimators, *, weights=None, n_jobs=None, verbose=False):
  460. super().__init__(estimators=estimators)
  461. self.weights = weights
  462. self.n_jobs = n_jobs
  463. self.verbose = verbose
  464. @_fit_context(
  465. # estimators in VotingRegressor.estimators are not validated yet
  466. prefer_skip_nested_validation=False
  467. )
  468. def fit(self, X, y, sample_weight=None):
  469. """Fit the estimators.
  470. Parameters
  471. ----------
  472. X : {array-like, sparse matrix} of shape (n_samples, n_features)
  473. Training vectors, where `n_samples` is the number of samples and
  474. `n_features` is the number of features.
  475. y : array-like of shape (n_samples,)
  476. Target values.
  477. sample_weight : array-like of shape (n_samples,), default=None
  478. Sample weights. If None, then samples are equally weighted.
  479. Note that this is supported only if all underlying estimators
  480. support sample weights.
  481. Returns
  482. -------
  483. self : object
  484. Fitted estimator.
  485. """
  486. y = column_or_1d(y, warn=True)
  487. return super().fit(X, y, sample_weight)
  488. def predict(self, X):
  489. """Predict regression target for X.
  490. The predicted regression target of an input sample is computed as the
  491. mean predicted regression targets of the estimators in the ensemble.
  492. Parameters
  493. ----------
  494. X : {array-like, sparse matrix} of shape (n_samples, n_features)
  495. The input samples.
  496. Returns
  497. -------
  498. y : ndarray of shape (n_samples,)
  499. The predicted values.
  500. """
  501. check_is_fitted(self)
  502. return np.average(self._predict(X), axis=1, weights=self._weights_not_none)
  503. def transform(self, X):
  504. """Return predictions for X for each estimator.
  505. Parameters
  506. ----------
  507. X : {array-like, sparse matrix} of shape (n_samples, n_features)
  508. The input samples.
  509. Returns
  510. -------
  511. predictions : ndarray of shape (n_samples, n_classifiers)
  512. Values predicted by each regressor.
  513. """
  514. check_is_fitted(self)
  515. return self._predict(X)
  516. def get_feature_names_out(self, input_features=None):
  517. """Get output feature names for transformation.
  518. Parameters
  519. ----------
  520. input_features : array-like of str or None, default=None
  521. Not used, present here for API consistency by convention.
  522. Returns
  523. -------
  524. feature_names_out : ndarray of str objects
  525. Transformed feature names.
  526. """
  527. check_is_fitted(self, "n_features_in_")
  528. _check_feature_names_in(self, input_features, generate_names=False)
  529. class_name = self.__class__.__name__.lower()
  530. return np.asarray(
  531. [f"{class_name}_{name}" for name, est in self.estimators if est != "drop"],
  532. dtype=object,
  533. )