Repository: sebp/scikit-survival (language: python)

Issue:
Support scikit-learn >=1.2
Sklearn [v1.2.0 ](https://pypi.org/project/scikit-learn/)released on Dec 8, and the Oct 24 release of sksurv pinned the upper bound to [<1.2](https://github.com/sebp/scikit-survival/blob/master/requirements/prod.txt#L8).

**Describe the solution you'd like**
Attempt to open the upper bound on the dependency and cut a new release.


Discussion / hints:

Fix patch:
diff --git a/README.rst b/README.rst
index 6211d330..c0a32387 100644
--- a/README.rst
+++ b/README.rst
@@ -39,7 +39,7 @@ Requirements
 - numpy 1.17.3 or later
 - osqp
 - pandas 1.0.5 or later
-- scikit-learn 1.1
+- scikit-learn 1.2
 - scipy 1.3.2 or later
 - C/C++ compiler
 
diff --git a/ci/appveyor/requirements_310.yaml b/ci/appveyor/requirements_310.yaml
index 2bee078a..ddb02ac4 100644
--- a/ci/appveyor/requirements_310.yaml
+++ b/ci/appveyor/requirements_310.yaml
@@ -12,5 +12,5 @@ dependencies:
   - pytest
   - pytest-cov
   - scipy!=1.3.0
-  - scikit-learn=1.1
+  - scikit-learn=1.2
   - pandas=1.4
diff --git a/ci/appveyor/requirements_38.yaml b/ci/appveyor/requirements_38.yaml
index 26816a1e..ed7518c4 100644
--- a/ci/appveyor/requirements_38.yaml
+++ b/ci/appveyor/requirements_38.yaml
@@ -12,5 +12,5 @@ dependencies:
   - pytest
   - pytest-cov
   - scipy!=1.3.0
-  - scikit-learn=1.1
+  - scikit-learn=1.2
   - pandas=1.1
diff --git a/ci/appveyor/requirements_39.yaml b/ci/appveyor/requirements_39.yaml
index 154151d3..55979eba 100644
--- a/ci/appveyor/requirements_39.yaml
+++ b/ci/appveyor/requirements_39.yaml
@@ -12,5 +12,5 @@ dependencies:
   - pytest
   - pytest-cov
   - scipy!=1.3.0
-  - scikit-learn=1.1
+  - scikit-learn=1.2
   - pandas=1.3
diff --git a/ci/deps/py310_pandas14_numpy121_sklearn11.sh b/ci/deps/py310_pandas14_numpy121_sklearn12.sh
similarity index 81%
rename from ci/deps/py310_pandas14_numpy121_sklearn11.sh
rename to ci/deps/py310_pandas14_numpy121_sklearn12.sh
index b012d5c7..130fe741 100644
--- a/ci/deps/py310_pandas14_numpy121_sklearn11.sh
+++ b/ci/deps/py310_pandas14_numpy121_sklearn12.sh
@@ -2,5 +2,5 @@
 export CONDA_PYTHON_VERSION='3.10.*'
 export PANDAS_VERSION='1.4.*'
 export NUMPY_VERSION='1.21.*'
-export SKLEARN_VERSION='1.1.*'
+export SKLEARN_VERSION='1.2.*'
 export NO_SLOW=false
diff --git a/ci/deps/py38_pandas10_numpy119_sklearn11.sh b/ci/deps/py38_pandas10_numpy119_sklearn12.sh
similarity index 81%
rename from ci/deps/py38_pandas10_numpy119_sklearn11.sh
rename to ci/deps/py38_pandas10_numpy119_sklearn12.sh
index 42712c3e..184aaa1e 100644
--- a/ci/deps/py38_pandas10_numpy119_sklearn11.sh
+++ b/ci/deps/py38_pandas10_numpy119_sklearn12.sh
@@ -2,5 +2,5 @@
 export CONDA_PYTHON_VERSION='3.8.*'
 export PANDAS_VERSION='1.0.*'
 export NUMPY_VERSION='1.19.*'
-export SKLEARN_VERSION='1.1.*'
+export SKLEARN_VERSION='1.2.*'
 export NO_SLOW=false
diff --git a/ci/deps/py38_pandas11_numpy120_sklearn11.sh b/ci/deps/py38_pandas11_numpy120_sklearn12.sh
similarity index 81%
rename from ci/deps/py38_pandas11_numpy120_sklearn11.sh
rename to ci/deps/py38_pandas11_numpy120_sklearn12.sh
index 5232d39e..16ff7de8 100644
--- a/ci/deps/py38_pandas11_numpy120_sklearn11.sh
+++ b/ci/deps/py38_pandas11_numpy120_sklearn12.sh
@@ -2,5 +2,5 @@
 export CONDA_PYTHON_VERSION='3.8.*'
 export PANDAS_VERSION='1.1.*'
 export NUMPY_VERSION='1.20.*'
-export SKLEARN_VERSION='1.1.*'
+export SKLEARN_VERSION='1.2.*'
 export NO_SLOW=true
diff --git a/ci/deps/py39_pandas13_numpy121_sklearn11.sh b/ci/deps/py39_pandas13_numpy121_sklearn12.sh
similarity index 81%
rename from ci/deps/py39_pandas13_numpy121_sklearn11.sh
rename to ci/deps/py39_pandas13_numpy121_sklearn12.sh
index 6c1784f5..44a94cc1 100644
--- a/ci/deps/py39_pandas13_numpy121_sklearn11.sh
+++ b/ci/deps/py39_pandas13_numpy121_sklearn12.sh
@@ -2,5 +2,5 @@
 export CONDA_PYTHON_VERSION='3.9.*'
 export PANDAS_VERSION='1.3.*'
 export NUMPY_VERSION='1.21.*'
-export SKLEARN_VERSION='1.1.*'
+export SKLEARN_VERSION='1.2.*'
 export NO_SLOW=true
diff --git a/doc/conf.py b/doc/conf.py
index 2d5cbb7c..4a907f44 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -49,6 +49,7 @@
     'sphinx.ext.autosummary',
     'sphinx.ext.coverage',
     'sphinx.ext.extlinks',
+    'sphinx.ext.intersphinx',
     'sphinx.ext.linkcode',
     'sphinx.ext.mathjax',
     'sphinx.ext.napoleon',
@@ -199,6 +200,7 @@
     'issue': ('https://github.com/sebp/scikit-survival/issues/%s', '#'),
 }
 
+intersphinx_mapping = {'sklearn': ('https://scikit-learn.org/1.2', None)}
 
 def linkcode_resolve(domain, info):
     """
diff --git a/doc/install.rst b/doc/install.rst
index 6f7f3503..f55875d1 100644
--- a/doc/install.rst
+++ b/doc/install.rst
@@ -91,6 +91,6 @@ The current minimum dependencies to run scikit-survival are:
 - numpy 1.17.3 or later
 - osqp
 - pandas 1.0.5 or later
-- scikit-learn 1.1
+- scikit-learn 1.2
 - scipy 1.3.2 or later
 - C/C++ compiler
diff --git a/pyproject.toml b/pyproject.toml
index bf26c9d6..e7d821f0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -18,8 +18,8 @@ requires = [
     "numpy; python_version>'3.10'",
 
     # scikit-learn requirements
-    "scikit-learn~=1.1.0; python_version<='3.9'",
-    "scikit-learn~=1.1.0; python_version=='3.10'",
+    "scikit-learn~=1.2.0; python_version<='3.9'",
+    "scikit-learn~=1.2.0; python_version=='3.10'",
     "scikit-learn; python_version>'3.10'",
 ]
 build-backend = "setuptools.build_meta"
diff --git a/requirements/prod.txt b/requirements/prod.txt
index bc436ccd..82f6dc6c 100644
--- a/requirements/prod.txt
+++ b/requirements/prod.txt
@@ -5,4 +5,4 @@ numpy
 osqp !=0.6.0,!=0.6.1
 pandas >=1.0.5
 scipy >=1.3.2
-scikit-learn >=1.1.2,<1.2
+scikit-learn >=1.2.0,<1.3
diff --git a/sksurv/ensemble/boosting.py b/sksurv/ensemble/boosting.py
index 2fa67c98..a4c521b2 100644
--- a/sksurv/ensemble/boosting.py
+++ b/sksurv/ensemble/boosting.py
@@ -21,6 +21,7 @@
 from sklearn.tree import DecisionTreeRegressor
 from sklearn.tree._tree import DTYPE
 from sklearn.utils import check_consistent_length, check_random_state, column_or_1d
+from sklearn.utils._param_validation import Interval, StrOptions
 from sklearn.utils.extmath import squared_norm
 from sklearn.utils.validation import check_is_fitted
 
@@ -96,11 +97,13 @@ class ComponentwiseGradientBoostingSurvivalAnalysis(BaseEnsemble, SurvivalAnalys
     learning_rate : float, optional, default: 0.1
         learning rate shrinks the contribution of each base learner by `learning_rate`.
         There is a trade-off between `learning_rate` and `n_estimators`.
+        Values must be in the range `[0.0, inf)`.
 
     n_estimators : int, default: 100
         The number of boosting stages to perform. Gradient boosting
         is fairly robust to over-fitting so a large number usually
         results in better performance.
+        Values must be in the range `[1, inf)`.
 
     subsample : float, optional, default: 1.0
         The fraction of samples to be used for fitting the individual base
@@ -108,6 +111,7 @@ class ComponentwiseGradientBoostingSurvivalAnalysis(BaseEnsemble, SurvivalAnalys
         Boosting. `subsample` interacts with the parameter `n_estimators`.
         Choosing `subsample < 1.0` leads to a reduction of variance
         and an increase in bias.
+        Values must be in the range `(0.0, 1.0]`.
 
     dropout_rate : float, optional, default: 0.0
         If larger than zero, the residuals at each iteration are only computed
@@ -115,6 +119,7 @@ class ComponentwiseGradientBoostingSurvivalAnalysis(BaseEnsemble, SurvivalAnalys
         percentage of base learners that are dropped. In each iteration,
         at least one base learner is dropped. This is an alternative regularization
         to shrinkage, i.e., setting `learning_rate < 1.0`.
+        Values must be in the range `[0.0, 1.0)`.
 
     random_state : int seed, RandomState instance, or None, default: None
         The seed of the pseudo random number generator to use when
@@ -122,8 +127,8 @@ class ComponentwiseGradientBoostingSurvivalAnalysis(BaseEnsemble, SurvivalAnalys
 
     verbose : int, default: 0
         Enable verbose output. If 1 then it prints progress and performance
-        once in a while (the more trees the lower the frequency). If greater
-        than 1 then it prints progress and performance for every tree.
+        once in a while.
+        Values must be in the range `[0, inf)`.
 
     Attributes
     ----------
@@ -163,6 +168,17 @@ class ComponentwiseGradientBoostingSurvivalAnalysis(BaseEnsemble, SurvivalAnalys
     .. [1] Hothorn, T., Bühlmann, P., Dudoit, S., Molinaro, A., van der Laan, M. J.,
            "Survival ensembles", Biostatistics, 7(3), 355-73, 2006
     """
+
+    _parameter_constraints = {
+        "loss": [StrOptions(frozenset(LOSS_FUNCTIONS.keys()))],
+        "learning_rate": [Interval(numbers.Real, 0.0, None, closed="left")],
+        "n_estimators": [Interval(numbers.Integral, 1, None, closed="left")],
+        "subsample": [Interval(numbers.Real, 0.0, 1.0, closed="right")],
+        "dropout_rate": [Interval(numbers.Real, 0.0, 1.0, closed="left")],
+        "random_state": ["random_state"],
+        "verbose": ["verbose"],
+    }
+
     def __init__(self, loss="coxph", learning_rate=0.1, n_estimators=100, subsample=1.0,
                  dropout_rate=0, random_state=None, verbose=0):
         self.loss = loss
@@ -181,27 +197,6 @@ def loss_(self):
     def _predict_risk_score(self):
         return isinstance(self._loss, CoxPH)
 
-    def _check_params(self):
-        """Check validity of parameters and raise ValueError if not valid. """
-        if self.n_estimators <= 0:
-            raise ValueError("n_estimators must be greater than 0 but "
-                             "was %r" % self.n_estimators)
-
-        if not 0.0 < self.subsample <= 1.0:
-            raise ValueError("subsample must be in ]0; 1] but "
-                             "was %r" % self.subsample)
-
-        if not 0.0 < self.learning_rate <= 1.0:
-            raise ValueError("learning_rate must be within ]0; 1] but "
-                             "was %r" % self.learning_rate)
-
-        if not 0.0 <= self.dropout_rate < 1.0:
-            raise ValueError("dropout_rate must be within [0; 1[, but "
-                             "was %r" % self.dropout_rate)
-
-        if self.loss not in LOSS_FUNCTIONS:
-            raise ValueError("Loss {!r} not supported.".format(self.loss))
-
     def _fit(self, X, event, time, sample_weight, random_state):  # noqa: C901
         n_samples = X.shape[0]
         # account for intercept
@@ -300,7 +295,7 @@ def fit(self, X, y, sample_weight=None):
 
         random_state = check_random_state(self.random_state)
 
-        self._check_params()
+        self._validate_params()
 
         self.estimators_ = []
         self._loss = LOSS_FUNCTIONS[self.loss]()
@@ -550,12 +545,14 @@ class GradientBoostingSurvivalAnalysis(BaseGradientBoosting, SurvivalAnalysisMix
 
     learning_rate : float, optional, default: 0.1
         learning rate shrinks the contribution of each tree by `learning_rate`.
-        There is a trade-off between learning_rate and n_estimators.
+        There is a trade-off between `learning_rate` and `n_estimators`.
+        Values must be in the range `[0.0, inf)`.
 
     n_estimators : int, default: 100
         The number of regression trees to create. Gradient boosting
         is fairly robust to over-fitting so a large number usually
         results in better performance.
+        Values must be in the range `[1, inf)`.
 
     criterion : string, optional, default: 'friedman_mse'
         The function to measure the quality of a split. Supported criteria
@@ -565,22 +562,38 @@ class GradientBoostingSurvivalAnalysis(BaseGradientBoosting, SurvivalAnalysisMix
         generally the best as it can provide a better approximation in
         some cases.
 
-    min_samples_split : integer, optional, default: 2
-        The minimum number of samples required to split an internal node.
+    min_samples_split : int or float, optional, default: 2
+        The minimum number of samples required to split an internal node:
+
+        - If int, values must be in the range `[2, inf)`.
+        - If float, values must be in the range `(0.0, 1.0]` and `min_samples_split`
+          will be `ceil(min_samples_split * n_samples)`.
 
-    min_samples_leaf : integer, optional, default: 1
+    min_samples_leaf : int or float, default: 1
         The minimum number of samples required to be at a leaf node.
+        A split point at any depth will only be considered if it leaves at
+        least ``min_samples_leaf`` training samples in each of the left and
+        right branches.  This may have the effect of smoothing the model,
+        especially in regression.
+
+        - If int, values must be in the range `[1, inf)`.
+        - If float, values must be in the range `(0.0, 1.0)` and `min_samples_leaf`
+          will be `ceil(min_samples_leaf * n_samples)`.
 
     min_weight_fraction_leaf : float, optional, default: 0.
-        The minimum weighted fraction of the input samples required to be at a
-        leaf node.
+        The minimum weighted fraction of the sum total of weights (of all
+        the input samples) required to be at a leaf node. Samples have
+        equal weight when `sample_weight` is not provided.
+        Values must be in the range `[0.0, 0.5]`.
 
-    max_depth : integer, optional, default: 3
-        maximum depth of the individual regression estimators. The maximum
+    max_depth : int or None, optional, default: 3
+        Maximum depth of the individual regression estimators. The maximum
         depth limits the number of nodes in the tree. Tune this parameter
         for best performance; the best value depends on the interaction
-        of the input variables.
-        Ignored if ``max_leaf_nodes`` is not None.
+        of the input variables. If None, then nodes are expanded until
+        all leaves are pure or until all leaves contain less than
+        `min_samples_split` samples.
+        If int, values must be in the range `[1, inf)`.
 
     min_impurity_decrease : float, optional, default: 0.
         A node will be split if this split induces a decrease of the impurity
@@ -599,19 +612,24 @@ class GradientBoostingSurvivalAnalysis(BaseGradientBoosting, SurvivalAnalysisMix
         if ``sample_weight`` is passed.
 
     random_state : int seed, RandomState instance, or None, default: None
-        The seed of the pseudo random number generator to use when
-        shuffling the data.
+        Controls the random seed given to each Tree estimator at each
+        boosting iteration.
+        In addition, it controls the random permutation of the features at
+        each split.
+        It also controls the random splitting of the training data to obtain a
+        validation set if `n_iter_no_change` is not None.
+        Pass an int for reproducible output across multiple function calls.
 
     max_features : int, float, string or None, optional, default: None
         The number of features to consider when looking for the best split:
-          - If int, then consider `max_features` features at each split.
-          - If float, then `max_features` is a percentage and
-            `int(max_features * n_features)` features are considered at each
-            split.
-          - If "auto", then `max_features=n_features`.
-          - If "sqrt", then `max_features=sqrt(n_features)`.
-          - If "log2", then `max_features=log2(n_features)`.
-          - If None, then `max_features=n_features`.
+
+        - If int, values must be in the range `[1, inf)`.
+        - If float, values must be in the range `(0.0, 1.0]` and the features
+          considered at each split will be `max(1, int(max_features * n_features_in_))`.
+        - If 'auto', then `max_features=sqrt(n_features)`.
+        - If 'sqrt', then `max_features=sqrt(n_features)`.
+        - If 'log2', then `max_features=log2(n_features)`.
+        - If None, then `max_features=n_features`.
 
         Choosing `max_features < n_features` leads to a reduction of variance
         and an increase in bias.
@@ -623,14 +641,16 @@ class GradientBoostingSurvivalAnalysis(BaseGradientBoosting, SurvivalAnalysisMix
     max_leaf_nodes : int or None, optional, default: None
         Grow trees with ``max_leaf_nodes`` in best-first fashion.
         Best nodes are defined as relative reduction in impurity.
-        If None then unlimited number of leaf nodes.
+        Values must be in the range `[2, inf)`.
+        If `None`, then unlimited number of leaf nodes.
 
     subsample : float, optional, default: 1.0
-        The fraction of samples to be used for fitting the individual regression
-        trees. If smaller than 1.0, this results in Stochastic Gradient
+        The fraction of samples to be used for fitting the individual base
+        learners. If smaller than 1.0 this results in Stochastic Gradient
         Boosting. `subsample` interacts with the parameter `n_estimators`.
         Choosing `subsample < 1.0` leads to a reduction of variance
         and an increase in bias.
+        Values must be in the range `(0.0, 1.0]`.
 
     dropout_rate : float, optional, default: 0.0
         If larger than zero, the residuals at each iteration are only computed
@@ -638,16 +658,19 @@ class GradientBoostingSurvivalAnalysis(BaseGradientBoosting, SurvivalAnalysisMix
         percentage of base learners that are dropped. In each iteration,
         at least one base learner is dropped. This is an alternative regularization
         to shrinkage, i.e., setting `learning_rate < 1.0`.
+        Values must be in the range `[0.0, 1.0)`.
 
     verbose : int, default: 0
         Enable verbose output. If 1 then it prints progress and performance
         once in a while (the more trees the lower the frequency). If greater
         than 1 then it prints progress and performance for every tree.
+        Values must be in the range `[0, inf)`.
 
     ccp_alpha : non-negative float, optional, default: 0.0.
         Complexity parameter used for Minimal Cost-Complexity Pruning. The
         subtree with the largest cost complexity that is smaller than
         ``ccp_alpha`` will be chosen. By default, no pruning is performed.
+        Values must be in the range `[0.0, inf)`.
 
     Attributes
     ----------
@@ -698,6 +721,13 @@ class GradientBoostingSurvivalAnalysis(BaseGradientBoosting, SurvivalAnalysisMix
            in 18th International Conference on Artificial Intelligence and Statistics,
            2015, 489–497.
     """
+
+    _parameter_constraints = {
+        **BaseGradientBoosting._parameter_constraints,
+        "loss": [StrOptions(frozenset(LOSS_FUNCTIONS.keys()))],
+        "dropout_rate": [Interval(numbers.Real, 0.0, 1.0, closed="left")]
+    }
+
     def __init__(self, loss="coxph", learning_rate=0.1, n_estimators=100,
                  criterion='friedman_mse',
                  min_samples_split=2,
@@ -738,36 +768,12 @@ def _predict_risk_score(self):
 
     def _check_params(self):
         """Check validity of parameters and raise ValueError if not valid. """
-        self.n_estimators = int(self.n_estimators)
-        if self.n_estimators <= 0:
-            raise ValueError("n_estimators must be greater than 0 but "
-                             "was %r" % self.n_estimators)
-
-        if not 0.0 < self.learning_rate <= 1.0:
-            raise ValueError("learning_rate must be within ]0; 1] but "
-                             "was %r" % self.learning_rate)
-
-        if not 0.0 < self.subsample <= 1.0:
-            raise ValueError("subsample must be in ]0; 1] but "
-                             "was %r" % self.subsample)
-
-        if not 0.0 <= self.dropout_rate < 1.0:
-            raise ValueError("dropout_rate must be within [0; 1[, but "
-                             "was %r" % self.dropout_rate)
+        self._validate_params()
 
         max_features = self._check_max_features()
 
-        self.min_samples_split = int(self.min_samples_split)
-        self.min_samples_leaf = int(self.min_samples_leaf)
-        self.max_depth = int(self.max_depth)
-        if self.max_leaf_nodes:
-            self.max_leaf_nodes = int(self.max_leaf_nodes)
-
         self.max_features_ = max_features
 
-        if self.loss not in LOSS_FUNCTIONS:
-            raise ValueError("Loss {!r} not supported.".format(self.loss))
-
         self._loss = LOSS_FUNCTIONS[self.loss]()
 
     def _check_max_features(self):
@@ -778,21 +784,14 @@ def _check_max_features(self):
                 max_features = max(1, int(np.sqrt(self.n_features_in_)))
             elif self.max_features == "log2":
                 max_features = max(1, int(np.log2(self.n_features_in_)))
-            else:
-                raise ValueError("Invalid value for max_features: %r. "
-                                 "Allowed string values are 'auto', 'sqrt' "
-                                 "or 'log2'." % self.max_features)
         elif self.max_features is None:
             max_features = self.n_features_in_
         elif isinstance(self.max_features, (numbers.Integral, np.integer)):
-            if self.max_features < 1:
-                raise ValueError("max_features must be in (0, n_features_in_]")
             max_features = self.max_features
         else:  # float
             if 0. < self.max_features <= 1.:
                 max_features = max(int(self.max_features * self.n_features_in_), 1)
-            else:
-                raise ValueError("max_features must be in (0, 1.0]")
+
         return max_features
 
     def _fit_stage(self, i, X, y, raw_predictions, sample_weight, sample_mask,
diff --git a/sksurv/ensemble/forest.py b/sksurv/ensemble/forest.py
index 488dc4b9..47b08f5f 100644
--- a/sksurv/ensemble/forest.py
+++ b/sksurv/ensemble/forest.py
@@ -39,7 +39,7 @@ class _BaseSurvivalForest(BaseForest,
 
     @abstractmethod
     def __init__(self,
-                 base_estimator,
+                 estimator,
                  n_estimators=100, *,
                  estimator_params=tuple(),
                  bootstrap=False,
@@ -50,7 +50,7 @@ def __init__(self,
                  warm_start=False,
                  max_samples=None):
         super().__init__(
-            base_estimator,
+            estimator,
             n_estimators=n_estimators,
             estimator_params=estimator_params,
             bootstrap=bootstrap,
@@ -84,6 +84,8 @@ def fit(self, X, y, sample_weight=None):
         -------
         self
         """
+        self._validate_params()
+
         X = self._validate_data(X, dtype=DTYPE, accept_sparse="csc", ensure_min_samples=2)
         event, time = check_array_survival(X, y)
 
@@ -123,8 +125,10 @@ def fit(self, X, y, sample_weight=None):
                              % (self.n_estimators, len(self.estimators_)))
 
         if n_more_estimators == 0:
-            warnings.warn("Warm-start fitting without increasing n_estimators "
-                          "does not fit new trees.")
+            warnings.warn(
+                "Warm-start fitting without increasing n_estimators does not fit new trees.",
+                stacklevel=2
+            )
         else:
             if self.warm_start and len(self.estimators_) > 0:
                 # We draw from the random state to get the random state we
@@ -182,7 +186,9 @@ def _set_oob_score_and_attributes(self, X, y):
             warnings.warn(
                 "Some inputs do not have OOB scores. "
                 "This probably means too few trees were used "
-                "to compute any reliable oob estimates.")
+                "to compute any reliable oob estimates.",
+                stacklevel=3
+            )
             n_predictions[n_predictions == 0] = 1
 
         predictions /= n_predictions
@@ -315,13 +321,13 @@ class RandomSurvivalForest(_BaseSurvivalForest):
     max_features : int, float, string or None, optional, default: None
         The number of features to consider when looking for the best split:
 
-            - If int, then consider `max_features` features at each split.
-            - If float, then `max_features` is a fraction and
-              `int(max_features * n_features)` features are considered at each
-              split.
-            - If "sqrt", then `max_features=sqrt(n_features)`.
-            - If "log2", then `max_features=log2(n_features)`.
-            - If None, then `max_features=n_features`.
+        - If int, then consider `max_features` features at each split.
+        - If float, then `max_features` is a fraction and
+          `int(max_features * n_features)` features are considered at each
+          split.
+        - If "sqrt", then `max_features=sqrt(n_features)`.
+        - If "log2", then `max_features=log2(n_features)`.
+        - If None, then `max_features=n_features`.
 
         Note: the search for a split does not stop until at least one
         valid partition of the node samples is found, even if it requires to
@@ -340,16 +346,17 @@ class RandomSurvivalForest(_BaseSurvivalForest):
         Whether to use out-of-bag samples to estimate
         the generalization accuracy.
 
-    n_jobs : int or None, optional (default=None)
-        The number of jobs to run in parallel for both `fit` and `predict`.
-        ``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
-        ``-1`` means using all processors.
+    n_jobs : int or None, optional, default: None
+        The number of jobs to run in parallel. :meth:`fit`, :meth:`predict`,
+        :meth:`decision_path` and :meth:`apply` are all parallelized over the
+        trees. ``None`` means 1 unless in a :obj:`joblib.parallel_backend`
+        context. ``-1`` means using all processors.
 
     random_state : int, RandomState instance or None, optional, default: None
-        If int, random_state is the seed used by the random number generator;
-        If RandomState instance, random_state is the random number generator;
-        If None, the random number generator is the RandomState instance used
-        by `np.random`.
+        Controls both the randomness of the bootstrapping of the samples used
+        when building trees (if ``bootstrap=True``) and the sampling of the
+        features to consider when looking for the best split at each node
+        (if ``max_features < n_features``).
 
     verbose : int, optional, default: 0
         Controls the verbosity when fitting and predicting.
@@ -362,10 +369,11 @@ class RandomSurvivalForest(_BaseSurvivalForest):
     max_samples : int or float, optional, default: None
         If bootstrap is True, the number of samples to draw from X
         to train each base estimator.
+
         - If None (default), then draw `X.shape[0]` samples.
         - If int, then draw `max_samples` samples.
         - If float, then draw `max_samples * X.shape[0]` samples. Thus,
-        `max_samples` should be in the interval `(0.0, 1.0]`.
+          `max_samples` should be in the interval `(0.0, 1.0]`.
 
     Attributes
     ----------
@@ -423,6 +431,12 @@ class RandomSurvivalForest(_BaseSurvivalForest):
            R News, 7(2), 25–31. https://cran.r-project.org/doc/Rnews/Rnews_2007-2.pdf.
     """
 
+    _parameter_constraints = {
+        **BaseForest._parameter_constraints,
+        **SurvivalTree._parameter_constraints,
+    }
+    _parameter_constraints.pop("splitter")
+
     def __init__(self,
                  n_estimators=100,
                  max_depth=None,
@@ -439,7 +453,7 @@ def __init__(self,
                  warm_start=False,
                  max_samples=None):
         super().__init__(
-            base_estimator=SurvivalTree(),
+            estimator=SurvivalTree(),
             n_estimators=n_estimators,
             estimator_params=("max_depth",
                               "min_samples_split",
@@ -639,13 +653,13 @@ class ExtraSurvivalTrees(_BaseSurvivalForest):
     max_features : int, float, string or None, optional, default: None
         The number of features to consider when looking for the best split:
 
-            - If int, then consider `max_features` features at each split.
-            - If float, then `max_features` is a fraction and
-              `int(max_features * n_features)` features are considered at each
-              split.
-            - If "sqrt", then `max_features=sqrt(n_features)`.
-            - If "log2", then `max_features=log2(n_features)`.
-            - If None, then `max_features=n_features`.
+        - If int, then consider `max_features` features at each split.
+        - If float, then `max_features` is a fraction and
+          `int(max_features * n_features)` features are considered at each
+          split.
+        - If "sqrt", then `max_features=sqrt(n_features)`.
+        - If "log2", then `max_features=log2(n_features)`.
+        - If None, then `max_features=n_features`.
 
         Note: the search for a split does not stop until at least one
         valid partition of the node samples is found, even if it requires to
@@ -664,16 +678,17 @@ class ExtraSurvivalTrees(_BaseSurvivalForest):
         Whether to use out-of-bag samples to estimate
         the generalization accuracy.
 
-    n_jobs : int or None, optional (default=None)
-        The number of jobs to run in parallel for both `fit` and `predict`.
-        ``None`` means 1 unless in a :obj:`joblib.parallel_backend` context.
-        ``-1`` means using all processors.
+    n_jobs : int or None, optional, default: None
+        The number of jobs to run in parallel. :meth:`fit`, :meth:`predict`,
+        :meth:`decision_path` and :meth:`apply` are all parallelized over the
+        trees. ``None`` means 1 unless in a :obj:`joblib.parallel_backend`
+        context. ``-1`` means using all processors.
 
     random_state : int, RandomState instance or None, optional, default: None
-        If int, random_state is the seed used by the random number generator;
-        If RandomState instance, random_state is the random number generator;
-        If None, the random number generator is the RandomState instance used
-        by `np.random`.
+        Controls both the randomness of the bootstrapping of the samples used
+        when building trees (if ``bootstrap=True``) and the sampling of the
+        features to consider when looking for the best split at each node
+        (if ``max_features < n_features``).
 
     verbose : int, optional, default: 0
         Controls the verbosity when fitting and predicting.
@@ -686,10 +701,11 @@ class ExtraSurvivalTrees(_BaseSurvivalForest):
     max_samples : int or float, optional, default: None
         If bootstrap is True, the number of samples to draw from X
         to train each base estimator.
+
         - If None (default), then draw `X.shape[0]` samples.
         - If int, then draw `max_samples` samples.
         - If float, then draw `max_samples * X.shape[0]` samples. Thus,
-        `max_samples` should be in the interval `(0.0, 1.0]`.
+          `max_samples` should be in the interval `(0.0, 1.0]`.
 
     Attributes
     ----------
@@ -715,6 +731,13 @@ class ExtraSurvivalTrees(_BaseSurvivalForest):
     sksurv.tree.SurvivalTree
         A single survival tree.
     """
+
+    _parameter_constraints = {
+        **BaseForest._parameter_constraints,
+        **SurvivalTree._parameter_constraints,
+    }
+    _parameter_constraints.pop("splitter")
+
     def __init__(self,
                  n_estimators=100,
                  max_depth=None,
@@ -731,7 +754,7 @@ def __init__(self,
                  warm_start=False,
                  max_samples=None):
         super().__init__(
-            base_estimator=SurvivalTree(splitter="random"),
+            estimator=SurvivalTree(splitter="random"),
             n_estimators=n_estimators,
             estimator_params=("max_depth",
                               "min_samples_split",
diff --git a/sksurv/linear_model/aft.py b/sksurv/linear_model/aft.py
index f7c0eb64..49cbbae6 100644
--- a/sksurv/linear_model/aft.py
+++ b/sksurv/linear_model/aft.py
@@ -39,12 +39,86 @@ class IPCRidge(Ridge, SurvivalAnalysisMixin):
     alpha : float, optional, default: 1.0
         Small positive values of alpha improve the conditioning of the problem
         and reduce the variance of the estimates.
+        `alpha` must be a non-negative float i.e. in `[0, inf)`.
+
+        For numerical reasons, using `alpha = 0` is not advised.
+
+    fit_intercept : bool, default: True
+        Whether to fit the intercept for this model. If set
+        to false, no intercept will be used in calculations
+        (i.e. ``X`` and ``y`` are expected to be centered).
+
+    copy_X : bool, default: True
+        If True, X will be copied; else, it may be overwritten.
+
+    max_iter : int, default: None
+        Maximum number of iterations for conjugate gradient solver.
+        For 'sparse_cg' and 'lsqr' solvers, the default value is determined
+        by scipy.sparse.linalg. For 'sag' solver, the default value is 1000.
+        For 'lbfgs' solver, the default value is 15000.
+
+    tol : float, default: 1e-4
+        Precision of the solution. Note that `tol` has no effect for solvers 'svd' and
+        'cholesky'.
+
+    solver : {'auto', 'svd', 'cholesky', 'lsqr', 'sparse_cg', \
+            'sag', 'saga', 'lbfgs'}, default: 'auto'
+        Solver to use in the computational routines:
+
+        - 'auto' chooses the solver automatically based on the type of data.
+
+        - 'svd' uses a Singular Value Decomposition of X to compute the Ridge
+          coefficients. It is the most stable solver, in particular more stable
+          for singular matrices than 'cholesky' at the cost of being slower.
+
+        - 'cholesky' uses the standard scipy.linalg.solve function to
+          obtain a closed-form solution.
+
+        - 'sparse_cg' uses the conjugate gradient solver as found in
+          scipy.sparse.linalg.cg. As an iterative algorithm, this solver is
+          more appropriate than 'cholesky' for large-scale data
+          (possibility to set `tol` and `max_iter`).
+
+        - 'lsqr' uses the dedicated regularized least-squares routine
+          scipy.sparse.linalg.lsqr. It is the fastest and uses an iterative
+          procedure.
+
+        - 'sag' uses a Stochastic Average Gradient descent, and 'saga' uses
+          its improved, unbiased version named SAGA. Both methods also use an
+          iterative procedure, and are often faster than other solvers when
+          both n_samples and n_features are large. Note that 'sag' and
+          'saga' fast convergence is only guaranteed on features with
+          approximately the same scale. You can preprocess the data with a
+          scaler from sklearn.preprocessing.
+
+        - 'lbfgs' uses L-BFGS-B algorithm implemented in
+          `scipy.optimize.minimize`. It can be used only when `positive`
+          is True.
+
+        All solvers except 'svd' support both dense and sparse data. However, only
+        'lsqr', 'sag', 'sparse_cg', and 'lbfgs' support sparse input when
+        `fit_intercept` is True.
+
+    positive : bool, default: False
+        When set to ``True``, forces the coefficients to be positive.
+        Only 'lbfgs' solver is supported in this case.
+
+    random_state : int, RandomState instance, default: None
+        Used when ``solver`` == 'sag' or 'saga' to shuffle the data.
 
     Attributes
     ----------
     coef_ : ndarray, shape = (n_features,)
         Weight vector.
 
+    intercept_ : float or ndarray of shape (n_targets,)
+        Independent term in decision function. Set to 0.0 if
+        ``fit_intercept = False``.
+
+    n_iter_ : None or ndarray of shape (n_targets,)
+        Actual number of iterations for each target. Available only for
+        sag and lsqr solvers. Other solvers will return None.
+
     n_features_in_ : int
         Number of features seen during ``fit``.
 
@@ -58,11 +132,32 @@ class IPCRidge(Ridge, SurvivalAnalysisMixin):
            present", Journal of Multivariate Analysis, vol. 45, no. 1, pp. 89-103, 1993.
            doi:10.1006/jmva.1993.1028.
     """
-    def __init__(self, alpha=1.0, fit_intercept=True, normalize="deprecated",
-                 copy_X=True, max_iter=None, tol=1e-3, solver="auto"):
-        super().__init__(alpha=alpha, fit_intercept=fit_intercept,
-                         normalize=normalize, copy_X=copy_X,
-                         max_iter=max_iter, tol=tol, solver=solver)
+
+    _parameter_constraints = {
+        **Ridge._parameter_constraints
+    }
+
+    def __init__(
+        self,
+        alpha=1.0,
+        fit_intercept=True,
+        copy_X=True,
+        max_iter=None,
+        tol=1e-3,
+        solver="auto",
+        positive=False,
+        random_state=None,
+    ):
+        super().__init__(
+            alpha=alpha,
+            fit_intercept=fit_intercept,
+            copy_X=copy_X,
+            max_iter=max_iter,
+            tol=tol,
+            solver=solver,
+            positive=positive,
+            random_state=random_state,
+        )
 
     @property
     def _predict_risk_score(self):
diff --git a/sksurv/linear_model/coxnet.py b/sksurv/linear_model/coxnet.py
index 55e55bb0..384b5583 100644
--- a/sksurv/linear_model/coxnet.py
+++ b/sksurv/linear_model/coxnet.py
@@ -10,12 +10,14 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
+import numbers
 import warnings
 
 import numpy as np
 from sklearn.base import BaseEstimator
 from sklearn.exceptions import ConvergenceWarning
 from sklearn.preprocessing import normalize as f_normalize
+from sklearn.utils._param_validation import Interval, StrOptions
 from sklearn.utils.validation import assert_all_finite, check_is_fitted, check_non_negative, column_or_1d
 
 from ..base import SurvivalAnalysisMixin
@@ -63,9 +65,10 @@ class CoxnetSurvivalAnalysis(BaseEstimator, SurvivalAnalysisMixin):
         This is a number that multiplies alpha to allow differential
         shrinkage.  Can be 0 for some variables, which implies no shrinkage,
         and that variable is always included in the model.
-        Default is 1 for all variables. Note: the penalty factors are
-        internally rescaled to sum to n_features, and the alphas sequence
-        will reflect this change.
+        Default is 1 for all variables.
+
+        Note: the penalty factors are internally rescaled to sum to
+        `n_features`, and the alphas sequence will reflect this change.
 
     normalize : boolean, optional, default: False
         If True, the features X will be normalized before optimization by
@@ -131,6 +134,20 @@ class CoxnetSurvivalAnalysis(BaseEstimator, SurvivalAnalysisMixin):
            Journal of statistical software. 2011 Mar;39(5):1.
     """
 
+    _parameter_constraints: dict = {
+        "n_alphas": [Interval(numbers.Integral, 1, None, closed="left")],
+        "alphas": ["array-like", None],
+        "alpha_min_ratio": [Interval(numbers.Real, 0, None, closed="neither"), StrOptions({"auto"})],
+        "l1_ratio": [Interval(numbers.Real, 0.0, 1.0, closed="right")],
+        "penalty_factor": ["array-like", None],
+        "normalize": ["boolean"],
+        "copy_X": ["boolean"],
+        "tol": [Interval(numbers.Real, 0, None, closed="left")],
+        "max_iter": [Interval(numbers.Integral, 1, None, closed="left")],
+        "verbose": ["verbose"],
+        "fit_baseline_model": ["boolean"],
+    }
+
     def __init__(self, n_alphas=100, alphas=None, alpha_min_ratio="auto", l1_ratio=0.5,
                  penalty_factor=None, normalize=False, copy_X=True,
                  tol=1e-7, max_iter=100000, verbose=False, fit_baseline_model=False):
@@ -193,27 +210,17 @@ def _check_alphas(self):
         return alphas, create_path
 
     def _check_alpha_min_ratio(self, n_samples, n_features):
-        if isinstance(self.alpha_min_ratio, str):
-            if self.alpha_min_ratio == "auto":
-                if n_samples > n_features:
-                    alpha_min_ratio = 0.0001
-                else:
-                    alpha_min_ratio = 0.01
+        alpha_min_ratio = self.alpha_min_ratio
+        if isinstance(alpha_min_ratio, str) and self.alpha_min_ratio == "auto":
+            if n_samples > n_features:
+                alpha_min_ratio = 0.0001
             else:
-                raise ValueError("Invalid value for alpha_min_ratio. "
-                                 "Allowed string values are 'auto'.")
-        else:
-            alpha_min_ratio = float(self.alpha_min_ratio)
-            if alpha_min_ratio <= 0 or not np.isfinite(alpha_min_ratio):
-                raise ValueError("alpha_min_ratio must be positive")
+                alpha_min_ratio = 0.01
+
         return alpha_min_ratio
 
     def _check_params(self, n_samples, n_features):
-        if not 0 < self.l1_ratio <= 1:
-            raise ValueError("l1_ratio must be in interval ]0;1], but was %f" % self.l1_ratio)
-
-        if self.tol <= 0:
-            raise ValueError("tolerance must be positive, but was %f" % self.tol)
+        self._validate_params()
 
         penalty_factor = self._check_penalty_factor(n_features)
 
diff --git a/sksurv/linear_model/coxph.py b/sksurv/linear_model/coxph.py
index e5c5cdf8..356aba7a 100644
--- a/sksurv/linear_model/coxph.py
+++ b/sksurv/linear_model/coxph.py
@@ -17,6 +17,7 @@
 from scipy.linalg import solve
 from sklearn.base import BaseEstimator
 from sklearn.exceptions import ConvergenceWarning
+from sklearn.utils._param_validation import Interval, StrOptions
 from sklearn.utils.validation import check_array, check_is_fitted
 
 from ..base import SurvivalAnalysisMixin
@@ -138,8 +139,6 @@ def __init__(self, X, event, time, alpha, ties):
         self.time = time[o]
         self.alpha = alpha
         self.no_alpha = np.all(self.alpha < np.finfo(self.alpha.dtype).eps)
-        if ties not in ("breslow", "efron"):
-            raise ValueError("ties must be one of 'breslow', 'efron'")
         self._is_breslow = ties == "breslow"
 
     def nlog_likelihood(self, w):
@@ -318,7 +317,7 @@ class CoxPHSurvivalAnalysis(BaseEstimator, SurvivalAnalysisMixin):
         If you want to include a subset of features without penalization,
         set the corresponding entries to 0.
 
-    ties : "breslow" | "efron", optional, default: "breslow"
+    ties : {'breslow', 'efron'}, optional, default: 'breslow'
         The method to handle tied event times. If there are
         no tied event times all the methods are equivalent.
 
@@ -331,7 +330,7 @@ class CoxPHSurvivalAnalysis(BaseEstimator, SurvivalAnalysisMixin):
         |1 - (new neg. log-likelihood / old neg. log-likelihood) | < tol
 
     verbose : int, optional, default: 0
-        Specified the amount of additional debug information
+        Specifies the amount of additional debug information
         during optimization.
 
     Attributes
@@ -370,6 +369,14 @@ class CoxPHSurvivalAnalysis(BaseEstimator, SurvivalAnalysisMixin):
            Journal of the American Statistical Association 72 (1977): 557–565.
     """
 
+    _parameter_constraints: dict = {
+        "alpha": [Interval(numbers.Real, 0, None, closed="left"), np.ndarray],
+        "ties": [StrOptions({"breslow", "efron"})],
+        "n_iter": [Interval(numbers.Integral, 1, None, closed="left")],
+        "tol": [Interval(numbers.Real, 0, None, closed="left")],
+        "verbose": ["verbose"],
+    }
+
     def __init__(self, alpha=0, ties="breslow", n_iter=100, tol=1e-9, verbose=0):
         self.alpha = alpha
         self.ties = ties
@@ -408,6 +415,8 @@ def fit(self, X, y):
         -------
         self
         """
+        self._validate_params()
+
         X = self._validate_data(X, ensure_min_samples=2, dtype=np.float64)
         event, time = check_array_survival(X, y)
 
diff --git a/sksurv/meta/ensemble_selection.py b/sksurv/meta/ensemble_selection.py
index 520eb20c..f71340a1 100644
--- a/sksurv/meta/ensemble_selection.py
+++ b/sksurv/meta/ensemble_selection.py
@@ -17,6 +17,7 @@
 from scipy.stats import kendalltau, rankdata, spearmanr
 from sklearn.base import BaseEstimator, clone
 from sklearn.model_selection import check_cv
+from sklearn.utils._param_validation import Interval, StrOptions
 
 from .base import _fit_and_score
 from .stacking import Stacking
@@ -93,6 +94,22 @@ def _score_regressor(estimator, X, y, idx):
 
 class BaseEnsembleSelection(Stacking):
 
+    _parameter_constraints = {
+        **Stacking._parameter_constraints,
+        "scorer": [callable],
+        "n_estimators": [
+            Interval(numbers.Integral, 1, None, closed="left"),
+            Interval(numbers.Real, 0.0, 1.0, closed="right"),
+        ],
+        "min_score": [numbers.Real],
+        "correlation": [StrOptions({"pearson", "kendall", "spearman"})],
+        "min_correlation": [Interval(numbers.Real, -1, 1, closed="both")],
+        "cv": ["cv_object"],
+        "n_jobs": [Interval(numbers.Integral, 1, None, closed="left")],
+        "verbose": ["verbose"],
+    }
+    _parameter_constraints.pop("probabilities")
+
     def __init__(self, meta_estimator, base_estimators, scorer=None, n_estimators=0.2,
                  min_score=0.66, correlation="pearson", min_correlation=0.6,
                  cv=None, n_jobs=1, verbose=0):
@@ -117,36 +134,24 @@ def __len__(self):
         return 0
 
     def _check_params(self):
-        if self.n_estimators <= 0:
-            raise ValueError("n_estimators must not be zero or negative")
+        self._validate_params()
 
-        if self.n_estimators >= 1.0:
-            self.n_estimators = int(self.n_estimators)
-            if self.n_estimators > len(self.base_estimators):
-                raise ValueError(
-                    "n_estimators (%d) must not exceed number of base learners (%d)" % (self.n_estimators,
-                                                                                        len(self.base_estimators)))
+        if self.n_estimators > len(self.base_estimators):
+            raise ValueError(
+                "n_estimators (%d) must not exceed number of base learners (%d)" % (self.n_estimators,
+                                                                                    len(self.base_estimators)))
 
         if isinstance(self.n_estimators, numbers.Integral):
             self.n_estimators_ = self.n_estimators
         else:
             self.n_estimators_ = max(int(self.n_estimators * len(self.base_estimators)), 1)
 
-        if not callable(self.scorer):
-            raise TypeError("scorer is not callable")
-
-        if not -1. <= self.min_correlation <= 1:
-            raise ValueError("min_correlation must be in [-1; 1], but was %r" % self.min_correlation)
-
         if self.correlation == "pearson":
             self._corr_func = lambda x: np.corrcoef(x, rowvar=0)
         elif self.correlation == "kendall":
             self._corr_func = _corr_kendalltau
         elif self.correlation == "spearman":
             self._corr_func = lambda x: spearmanr(x, axis=0).correlation
-        else:
-            raise ValueError("correlation must be one of 'pearson', 'kendall', and 'spearman', "
-                             "but got %r" % self.correlation)
 
     def _create_base_ensemble(self, out, n_estimators, n_folds):
         """For each base estimator collect models trained on each fold"""
@@ -384,6 +389,11 @@ class EnsembleSelection(BaseEnsembleSelection):
         Lecture Notes in Computer Science, vol. 3181, 164-173, 2004
     """
 
+    _parameter_constraints = {
+        **BaseEnsembleSelection._parameter_constraints,
+    }
+    _parameter_constraints.pop("meta_estimator")
+
     def __init__(self, base_estimators, scorer=None, n_estimators=0.2,
                  min_score=0.2, correlation="pearson", min_correlation=0.6,
                  cv=None, n_jobs=1, verbose=0):
@@ -521,6 +531,12 @@ class EnsembleSelectionRegressor(BaseEnsembleSelection):
         "Dynamic integration of regression models. International Workshop on Multiple Classifier Systems".
         Lecture Notes in Computer Science, vol. 3181, 164-173, 2004
     """
+
+    _parameter_constraints = {
+        **BaseEnsembleSelection._parameter_constraints,
+    }
+    _parameter_constraints.pop("meta_estimator")
+
     def __init__(self, base_estimators, scorer=None, n_estimators=0.2,
                  min_score=0.66, correlation="pearson", min_correlation=0.6,
                  cv=None, n_jobs=1, verbose=0):
diff --git a/sksurv/meta/stacking.py b/sksurv/meta/stacking.py
index 43cbf185..2915cca5 100644
--- a/sksurv/meta/stacking.py
+++ b/sksurv/meta/stacking.py
@@ -12,6 +12,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import numpy as np
 from sklearn.base import MetaEstimatorMixin, clone
+from sklearn.utils._param_validation import HasMethods
 from sklearn.utils.metaestimators import _BaseComposition, available_if
 
 from ..base import SurvivalAnalysisMixin
@@ -69,6 +70,13 @@ class Stacking(MetaEstimatorMixin, SurvivalAnalysisMixin, _BaseComposition):
         Names of features seen during ``fit``. Defined only when `X`
         has feature names that are all strings.
     """
+
+    _parameter_constraints = {
+        "meta_estimator": [HasMethods(["fit"])],
+        "base_estimators": [list],
+        "probabilities": ["boolean"],
+    }
+
     def __init__(self, meta_estimator, base_estimators, probabilities=True):
         self.meta_estimator = meta_estimator
         self.base_estimators = base_estimators
@@ -87,11 +95,6 @@ def _validate_estimators(self):
                                 "fit and predict/predict_proba"
                                 " '%s' (type %s) doesn't)" % (t, type(t)))
 
-        if not hasattr(self.meta_estimator, "fit"):
-            raise TypeError("meta estimator should implement fit "
-                            "'%s' (type %s) doesn't)"
-                            % (self.meta_estimator, type(self.meta_estimator)))
-
     def set_params(self, **params):
         """
         Set the parameters of an estimator from the ensemble.
@@ -208,6 +211,7 @@ def fit(self, X, y=None, **fit_params):
         -------
         self
         """
+        self._validate_params()
         self._validate_estimators()
         self._fit_estimators(X, y, **fit_params)
         Xt = self._predict_estimators(X)
diff --git a/sksurv/svm/minlip.py b/sksurv/svm/minlip.py
index 04ff5288..e08a4681 100644
--- a/sksurv/svm/minlip.py
+++ b/sksurv/svm/minlip.py
@@ -1,4 +1,5 @@
 from abc import ABCMeta, abstractmethod
+import numbers
 import warnings
 
 import numpy as np
@@ -6,6 +7,7 @@
 from sklearn.base import BaseEstimator
 from sklearn.exceptions import ConvergenceWarning
 from sklearn.metrics.pairwise import pairwise_kernels
+from sklearn.utils._param_validation import Interval, StrOptions
 
 from ..base import SurvivalAnalysisMixin
 from ..exceptions import NoComparablePairException
@@ -199,31 +201,44 @@ class MinlipSurvivalAnalysis(BaseEstimator, SurvivalAnalysisMixin):
 
     Parameters
     ----------
-    solver : "ecos" | "osqp", optional, default: ecos
+    solver : {'ecos', 'osqp'}, optional, default: 'ecos'
         Which quadratic program solver to use.
 
     alpha : float, positive, default: 1
         Weight of penalizing the hinge loss in the objective function.
 
-    kernel : "linear" | "poly" | "rbf" | "sigmoid" | "cosine" | "precomputed"
-        Kernel.
-        Default: "linear"
-
-    gamma : float, optional
-        Kernel coefficient for rbf and poly kernels. Default: ``1/n_features``.
+    kernel : {'linear', 'poly', 'rbf', 'sigmoid', 'cosine', 'precomputed'} or callable, default: 'linear'.
+        Kernel mapping used internally. This parameter is directly passed to
+        :func:`sklearn.metrics.pairwise.pairwise_kernels`.
+        If `kernel` is a string, it must be one of the metrics
+        in `sklearn.pairwise.PAIRWISE_KERNEL_FUNCTIONS` or "precomputed".
+        If `kernel` is "precomputed", X is assumed to be a kernel matrix.
+        Alternatively, if `kernel` is a callable function, it is called on
+        each pair of instances (rows) and the resulting value recorded. The
+        callable should take two rows from X as input and return the
+        corresponding kernel value as a single number. This means that
+        callables from :mod:`sklearn.metrics.pairwise` are not allowed, as
+        they operate on matrices, not single samples. Use the string
+        identifying the kernel instead.
+
+    gamma : float, optional, default: None
+        Gamma parameter for the RBF, laplacian, polynomial, exponential chi2
+        and sigmoid kernels. Interpretation of the default value is left to
+        the kernel; see the documentation for :mod:`sklearn.metrics.pairwise`.
         Ignored by other kernels.
 
     degree : int, default: 3
-        Degree for poly kernels. Ignored by other kernels.
+        Degree of the polynomial kernel. Ignored by other kernels.
 
     coef0 : float, optional
-        Independent term in poly and sigmoid kernels.
+        Zero coefficient for polynomial and sigmoid kernels.
         Ignored by other kernels.
 
     kernel_params : mapping of string to any, optional
-        Parameters (keyword arguments) and values for kernel passed as call
+        Additional parameters (keyword arguments) for kernel function passed
+        as callable object.
 
-    pairs : "all" | "nearest" | "next", optional, default: "nearest"
+    pairs : {'all', 'nearest', 'next'}, optional, default: 'nearest'
         Which constraints to use in the optimization problem.
 
         - all: Use all comparable pairs. Scales quadratic in number of samples
@@ -235,14 +250,14 @@ class MinlipSurvivalAnalysis(BaseEstimator, SurvivalAnalysisMixin):
           disregarding its censoring status. Scales linear in number of samples.
 
     verbose : bool, default: False
-        Enable verbose output of solver
+        Enable verbose output of solver.
 
-    timeit : False or int
+    timeit : False, int or None, default: None
         If non-zero value is provided the time it takes for optimization is measured.
         The given number of repetitions are performed. Results can be accessed from the
         ``timings_`` attribute.
 
-    max_iter : int, optional
+    max_iter : int or None, optional, default: None
         Maximum number of iterations to perform. By default
         use solver's default value.
 
@@ -271,6 +286,23 @@ class MinlipSurvivalAnalysis(BaseEstimator, SurvivalAnalysisMixin):
            The Journal of Machine Learning Research, 12, 819-862. 2011
     """
 
+    _parameter_constraints = {
+        "solver": [StrOptions({"ecos", "osqp"})],
+        "alpha": [Interval(numbers.Real, 0, None, closed="neither")],
+        "kernel": [
+            StrOptions({"linear", "poly", "rbf", "sigmoid", "precomputed"}),
+            callable,
+        ],
+        "degree": [Interval(numbers.Integral, 0, None, closed="left")],
+        "gamma": [Interval(numbers.Real, 0.0, None, closed="left"), None],
+        "coef0": [Interval(numbers.Real, None, None, closed="neither")],
+        "kernel_params": [dict, None],
+        "pairs": [StrOptions({"all", "nearest", "next"})],
+        "verbose": ["boolean"],
+        "timeit": [Interval(numbers.Integral, 1, None, closed="left"), None],
+        "max_iter": [Interval(numbers.Integral, 1, None, closed="left"), None],
+    }
+
     def __init__(self, solver="ecos",
                  alpha=1.0, kernel="linear", gamma=None, degree=3, coef0=1, kernel_params=None,
                  pairs="nearest", verbose=False, timeit=None, max_iter=None):
@@ -326,14 +358,10 @@ def _fit(self, x, event, time):
             raise NoComparablePairException("Data has no comparable pairs, cannot fit model.")
 
         max_iter = self.max_iter
-        if max_iter is not None:
-            max_iter = int(max_iter)
         if self.solver == "ecos":
             solver = EcosSolver(max_iter=max_iter, verbose=self.verbose)
         elif self.solver == "osqp":
             solver = OsqpSolver(max_iter=max_iter, verbose=self.verbose)
-        else:
-            raise ValueError("unknown solver: {}".format(self.solver))
 
         K = self._get_kernel(x)
         problem_data = self._setup_qp(K, D, time)
@@ -372,6 +400,7 @@ def fit(self, X, y):
         -------
         self
         """
+        self._validate_params()
         X = self._validate_data(X, ensure_min_samples=2)
         event, time = check_array_survival(X, y)
         self._fit(X, event, time)
@@ -425,49 +454,62 @@ class HingeLossSurvivalSVM(MinlipSurvivalAnalysis):
 
     Parameters
     ----------
-    solver : "ecos" | "osqp", optional, default: ecos
+    solver : {'ecos', 'osqp'}, optional, default: 'ecos'
         Which quadratic program solver to use.
 
     alpha : float, positive, default: 1
         Weight of penalizing the hinge loss in the objective function.
 
-    kernel : "linear" | "poly" | "rbf" | "sigmoid" | "cosine" | "precomputed"
-        Kernel.
-        Default: "linear"
-
-    gamma : float, optional
-        Kernel coefficient for rbf and poly kernels. Default: ``1/n_features``.
+    kernel : {'linear', 'poly', 'rbf', 'sigmoid', 'cosine', 'precomputed'} or callable, default: 'linear'.
+        Kernel mapping used internally. This parameter is directly passed to
+        :func:`sklearn.metrics.pairwise.pairwise_kernels`.
+        If `kernel` is a string, it must be one of the metrics
+        in `sklearn.pairwise.PAIRWISE_KERNEL_FUNCTIONS` or "precomputed".
+        If `kernel` is "precomputed", X is assumed to be a kernel matrix.
+        Alternatively, if `kernel` is a callable function, it is called on
+        each pair of instances (rows) and the resulting value recorded. The
+        callable should take two rows from X as input and return the
+        corresponding kernel value as a single number. This means that
+        callables from :mod:`sklearn.metrics.pairwise` are not allowed, as
+        they operate on matrices, not single samples. Use the string
+        identifying the kernel instead.
+
+    gamma : float, optional, default: None
+        Gamma parameter for the RBF, laplacian, polynomial, exponential chi2
+        and sigmoid kernels. Interpretation of the default value is left to
+        the kernel; see the documentation for :mod:`sklearn.metrics.pairwise`.
         Ignored by other kernels.
 
     degree : int, default: 3
-        Degree for poly kernels. Ignored by other kernels.
+        Degree of the polynomial kernel. Ignored by other kernels.
 
     coef0 : float, optional
-        Independent term in poly and sigmoid kernels.
+        Zero coefficient for polynomial and sigmoid kernels.
         Ignored by other kernels.
 
     kernel_params : mapping of string to any, optional
-        Parameters (keyword arguments) and values for kernel passed as call
+        Additional parameters (keyword arguments) for kernel function passed
+        as callable object.
 
-    pairs : "all" | "nearest" | "next", optional, default: "all"
+    pairs : {'all', 'nearest', 'next'}, optional, default: 'all'
         Which constraints to use in the optimization problem.
 
         - all: Use all comparable pairs. Scales quadratic in number of samples.
         - nearest: Only considers comparable pairs :math:`(i, j)` where :math:`j` is the
           uncensored sample with highest survival time smaller than :math:`y_i`.
-          Scales linear in number of samples (cf. :class:`sksurv.svm.MinlipSurvivalSVM`).
+          Scales linear in number of samples (cf. :class:`sksurv.svm.MinlipSurvivalAnalysis`).
         - next: Only compare against direct nearest neighbor according to observed time,
           disregarding its censoring status. Scales linear in number of samples.
 
     verbose : bool, default: False
         Enable verbose output of solver.
 
-    timeit : False or int
+    timeit : False, int or None, default: None
         If non-zero value is provided the time it takes for optimization is measured.
         The given number of repetitions are performed. Results can be accessed from the
         ``timings_`` attribute.
 
-    max_iter : int, optional
+    max_iter : int or None, optional, default: None
         Maximum number of iterations to perform. By default
         use solver's default value.
 
@@ -505,6 +547,8 @@ class HingeLossSurvivalSVM(MinlipSurvivalAnalysis):
            89-94, 2008.
     """
 
+    _parameter_constraints = MinlipSurvivalAnalysis._parameter_constraints
+
     def __init__(self, solver="ecos",
                  alpha=1.0, kernel="linear", gamma=None, degree=3, coef0=1, kernel_params=None,
                  pairs="all", verbose=False, timeit=None, max_iter=None):
diff --git a/sksurv/svm/naive_survival_svm.py b/sksurv/svm/naive_survival_svm.py
index 436904d7..d9bdc1f5 100644
--- a/sksurv/svm/naive_survival_svm.py
+++ b/sksurv/svm/naive_survival_svm.py
@@ -52,12 +52,12 @@ class NaiveSurvivalSVM(SurvivalAnalysisMixin, LinearSVC):
     alpha : float, positive, default: 1.0
         Weight of penalizing the squared hinge loss in the objective function.
 
-    loss : string, 'hinge' or 'squared_hinge', default: 'squared_hinge'
+    loss : {'hinge', 'squared_hinge'}, default: 'squared_hinge'
         Specifies the loss function. 'hinge' is the standard SVM loss
         (used e.g. by the SVC class) while 'squared_hinge' is the
         square of the hinge loss.
 
-    penalty : 'l1' | 'l2', default: 'l2'
+    penalty : {'l1', 'l2'}, default: 'l2'
         Specifies the norm used in the penalization. The 'l2'
         penalty is the standard used in SVC. The 'l1' leads to `coef_`
         vectors that are sparse.
@@ -102,6 +102,18 @@ class NaiveSurvivalSVM(SurvivalAnalysisMixin, LinearSVC):
            Bioinformatics 24(14), 1632-8, 2008.
 
     """
+
+    _parameter_constraints = {
+        "penalty": LinearSVC._parameter_constraints["penalty"],
+        "loss": LinearSVC._parameter_constraints["loss"],
+        "dual": LinearSVC._parameter_constraints["dual"],
+        "tol": LinearSVC._parameter_constraints["tol"],
+        "alpha": LinearSVC._parameter_constraints["C"],
+        "verbose": LinearSVC._parameter_constraints["verbose"],
+        "random_state": LinearSVC._parameter_constraints["random_state"],
+        "max_iter": LinearSVC._parameter_constraints["max_iter"],
+    }
+
     def __init__(self, penalty='l2', loss='squared_hinge', dual=False, tol=1e-4,
                  alpha=1.0, verbose=0, random_state=None, max_iter=1000):
         super().__init__(penalty=penalty,
diff --git a/sksurv/svm/survival_svm.py b/sksurv/svm/survival_svm.py
index bd1b8e1b..cd5ccc86 100644
--- a/sksurv/svm/survival_svm.py
+++ b/sksurv/svm/survival_svm.py
@@ -11,6 +11,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 from abc import ABCMeta, abstractmethod
+from numbers import Integral, Real
 import warnings
 
 import numexpr
@@ -20,6 +21,7 @@
 from sklearn.exceptions import ConvergenceWarning
 from sklearn.metrics.pairwise import pairwise_kernels
 from sklearn.utils import check_array, check_consistent_length, check_random_state, check_X_y
+from sklearn.utils._param_validation import Interval, StrOptions
 from sklearn.utils.extmath import safe_sparse_dot, squared_norm
 from sklearn.utils.validation import check_is_fitted
 
@@ -608,6 +610,18 @@ def _hessian_func(self, _beta, s):
 
 
 class BaseSurvivalSVM(BaseEstimator, metaclass=ABCMeta):
+
+    _parameter_constraints = {
+        "alpha": [Interval(Real, 0.0, None, closed="neither")],
+        "rank_ratio": [Interval(Real, 0.0, 1.0, closed="both")],
+        "fit_intercept": ["boolean"],
+        "max_iter": [Interval(Integral, 1, None, closed="left")],
+        "verbose": ["verbose"],
+        "tol": [Interval(Real, 0.0, None, closed="neither"), None],
+        "random_state": ["random_state"],
+        "timeit": [Interval(Integral, 1, None, closed="left"), "boolean"],
+    }
+
     @abstractmethod
     def __init__(self, alpha=1, rank_ratio=1.0, fit_intercept=False,
                  max_iter=20, verbose=False, tol=None,
@@ -647,8 +661,6 @@ def _create_optimizer(self, X, y, status):
             optimizer = LargeScaleOptimizer(self.alpha, self.rank_ratio, self.fit_intercept,
                                             OrderStatisticTreeSurvivalCounter(X, ranks, status, AVLTree, times),
                                             timeit=self.timeit)
-        else:
-            raise ValueError('unknown optimizer: {0}'.format(self.optimizer))
 
         return optimizer
 
@@ -687,11 +699,7 @@ def fit(self, X, y):
         X = self._validate_for_fit(X)
         event, time = check_array_survival(X, y)
 
-        if self.alpha <= 0:
-            raise ValueError("alpha must be positive")
-
-        if not 0 <= self.rank_ratio <= 1:
-            raise ValueError("rank_ratio must be in [0; 1]")
+        self._validate_params()
 
         if self.fit_intercept and self.rank_ratio == 1.0:
             raise ValueError("fit_intercept=True is only meaningful if rank_ratio < 1.0")
@@ -806,17 +814,17 @@ class FastSurvivalSVM(BaseSurvivalSVM, SurvivalAnalysisMixin):
     verbose : bool, optional, default: False
         Whether to print messages during optimization
 
-    tol : float, optional
+    tol : float or None, optional, default: None
         Tolerance for termination. For detailed control, use solver-specific
         options.
 
-    optimizer : "avltree" | "direct-count" | "PRSVM" | "rbtree" | "simple", optional, default: avltree
+    optimizer : {'avltree', 'direct-count', 'PRSVM', 'rbtree', 'simple'}, optional, default: 'avltree'
         Which optimizer to use.
 
     random_state : int or :class:`numpy.random.RandomState` instance, optional
         Random number generator (used to resolve ties in survival times).
 
-    timeit : False or int
+    timeit : False, int or None, default: None
         If non-zero value is provided the time it takes for optimization is measured.
         The given number of repetitions are performed. Results can be accessed from the
         ``optimizer_result_`` attribute.
@@ -852,6 +860,12 @@ class FastSurvivalSVM(BaseSurvivalSVM, SurvivalAnalysisMixin):
            ECML PKDD 2015, Porto, Portugal,
            Lecture Notes in Computer Science, vol. 9285, pp. 243-259 (2015)
     """
+
+    _parameter_constraints = {
+        **BaseSurvivalSVM._parameter_constraints,
+        "optimizer": [StrOptions({"simple", "PRSVM", "direct-count", "rbtree", "avltree"}), None],
+    }
+
     def __init__(self, alpha=1, rank_ratio=1.0, fit_intercept=False,
                  max_iter=20, verbose=False, tol=None,
                  optimizer=None, random_state=None, timeit=False):
@@ -920,23 +934,36 @@ class FastKernelSurvivalSVM(BaseSurvivalSVM, SurvivalAnalysisMixin):
         Whether to calculate an intercept for the regression model. If set to ``False``, no intercept
         will be calculated. Has no effect if ``rank_ratio = 1``, i.e., only ranking is performed.
 
-    kernel : "linear" | "poly" | "rbf" | "sigmoid" | "cosine" | "precomputed"
-        Kernel.
-        Default: "linear"
+    kernel : {'linear', 'poly', 'rbf', 'sigmoid', 'cosine', 'precomputed'} or callable, default: 'linear'.
+        Kernel mapping used internally. This parameter is directly passed to
+        :func:`sklearn.metrics.pairwise.pairwise_kernels`.
+        If `kernel` is a string, it must be one of the metrics
+        in `pairwise.PAIRWISE_KERNEL_FUNCTIONS` or "precomputed".
+        If `kernel` is "precomputed", X is assumed to be a kernel matrix.
+        Alternatively, if `kernel` is a callable function, it is called on
+        each pair of instances (rows) and the resulting value recorded. The
+        callable should take two rows from X as input and return the
+        corresponding kernel value as a single number. This means that
+        callables from :mod:`sklearn.metrics.pairwise` are not allowed, as
+        they operate on matrices, not single samples. Use the string
+        identifying the kernel instead.
+
+    gamma : float, optional, default: None
+        Gamma parameter for the RBF, laplacian, polynomial, exponential chi2
+        and sigmoid kernels. Interpretation of the default value is left to
+        the kernel; see the documentation for :mod:`sklearn.metrics.pairwise`.
+        Ignored by other kernels.
 
     degree : int, default: 3
-        Degree for poly kernels. Ignored by other kernels.
-
-    gamma : float, optional
-        Kernel coefficient for rbf and poly kernels. Default: ``1/n_features``.
-        Ignored by other kernels.
+        Degree of the polynomial kernel. Ignored by other kernels.
 
     coef0 : float, optional
-        Independent term in poly and sigmoid kernels.
+        Zero coefficient for polynomial and sigmoid kernels.
         Ignored by other kernels.
 
     kernel_params : mapping of string to any, optional
-        Parameters (keyword arguments) and values for kernel passed as call
+        Additional parameters (keyword arguments) for kernel function passed
+        as callable object.
 
     max_iter : int, optional, default: 20
         Maximum number of iterations to perform in Newton optimization
@@ -944,17 +971,17 @@ class FastKernelSurvivalSVM(BaseSurvivalSVM, SurvivalAnalysisMixin):
     verbose : bool, optional, default: False
         Whether to print messages during optimization
 
-    tol : float, optional
+    tol : float or None, optional, default: None
         Tolerance for termination. For detailed control, use solver-specific
         options.
 
-    optimizer : "avltree" | "rbtree", optional, default: "rbtree"
+    optimizer : {'avltree', 'rbtree'}, optional, default: 'rbtree'
         Which optimizer to use.
 
     random_state : int or :class:`numpy.random.RandomState` instance, optional
         Random number generator (used to resolve ties in survival times).
 
-    timeit : False or int
+    timeit : False, int or None, default: None
         If non-zero value is provided the time it takes for optimization is measured.
         The given number of repetitions are performed. Results can be accessed from the
         ``optimizer_result_`` attribute.
@@ -993,6 +1020,20 @@ class FastKernelSurvivalSVM(BaseSurvivalSVM, SurvivalAnalysisMixin):
            4th Workshop on Machine Learning in Life Sciences,
            23 September 2016, Riva del Garda, Italy. arXiv:1611.07054
     """
+
+    _parameter_constraints = {
+        **FastSurvivalSVM._parameter_constraints,
+        "kernel": [
+            StrOptions({"linear", "poly", "rbf", "sigmoid", "precomputed"}),
+            callable,
+        ],
+        "gamma": [Interval(Real, 0.0, None, closed="left"), None],
+        "degree": [Interval(Integral, 0, None, closed="left")],
+        "coef0": [Interval(Real, None, None, closed="neither")],
+        "kernel_params": [dict, None],
+        "optimizer": [StrOptions({"rbtree", "avltree"}), None],
+    }
+
     def __init__(self, alpha=1, rank_ratio=1.0, fit_intercept=False, kernel="rbf",
                  gamma=None, degree=3, coef0=1, kernel_params=None, max_iter=20, verbose=False, tol=None,
                  optimizer=None, random_state=None, timeit=False):
@@ -1036,8 +1077,6 @@ def _create_optimizer(self, kernel_mat, y, status):
                 self.alpha, self.rank_ratio, self.fit_intercept,
                 OrderStatisticTreeSurvivalCounter(kernel_mat, ranks, status, AVLTree, times),
                 timeit=self.timeit)
-        else:
-            raise ValueError('unknown optimizer: {0}'.format(self.optimizer))
 
         return optimizer
 
diff --git a/sksurv/tree/_criterion.pyx b/sksurv/tree/_criterion.pyx
index fa5e2f47..b5c3fabc 100644
--- a/sksurv/tree/_criterion.pyx
+++ b/sksurv/tree/_criterion.pyx
@@ -99,8 +99,6 @@ cdef class LogrankCriterion(Criterion):
 
     def __cinit__(self, SIZE_t n_outputs, SIZE_t n_samples, const DOUBLE_t[::1] event_times):
         # Default values
-        self.sample_weight = NULL
-
         self.samples = NULL
         self.start = 0
         self.pos = 0
@@ -128,7 +126,7 @@ cdef class LogrankCriterion(Criterion):
     def __reduce__(self):
         return (type(self), (self.n_outputs, self.n_samples, self.event_times), self.__getstate__())
 
-    cdef int init(self, const DOUBLE_t[:, ::1] y, DOUBLE_t* sample_weight,
+    cdef int init(self, const DOUBLE_t[:, ::1] y, const DOUBLE_t[:] sample_weight,
                   double weighted_n_samples, SIZE_t* samples, SIZE_t start,
                   SIZE_t end) nogil except -1:
         """Initialize the criterion at node samples[start:end] and
@@ -157,7 +155,7 @@ cdef class LogrankCriterion(Criterion):
             i = samples[p]
             set_time_point(&time_arr[k], i, self.y)
 
-            if sample_weight is not NULL:
+            if sample_weight is not None:
                 w = sample_weight[i]
 
             self.weighted_n_node_samples += w
@@ -206,7 +204,7 @@ cdef class LogrankCriterion(Criterion):
 
     cdef int update(self, SIZE_t new_pos) nogil except -1:
         """Updated statistics by moving samples[pos:new_pos] to the left."""
-        cdef const double* sample_weight = self.sample_weight
+        cdef const DOUBLE_t[:] sample_weight = self.sample_weight
         cdef const SIZE_t* samples = self.samples
 
         cdef SIZE_t pos = self.start  # always start from the beginning
@@ -227,7 +225,7 @@ cdef class LogrankCriterion(Criterion):
             i = samples[idx_left]
             set_time_point(&time_arr[k], i, self.y)
 
-            if sample_weight is not NULL:
+            if sample_weight is not None:
                 w = sample_weight[i]
 
             self.weighted_n_left += w
diff --git a/sksurv/tree/tree.py b/sksurv/tree/tree.py
index 2abfb3c9..901374cf 100644
--- a/sksurv/tree/tree.py
+++ b/sksurv/tree/tree.py
@@ -1,6 +1,5 @@
 from math import ceil
-import numbers
-import warnings
+from numbers import Integral, Real
 
 import numpy as np
 from scipy.sparse import issparse
@@ -9,6 +8,7 @@
 from sklearn.tree._classes import DENSE_SPLITTERS, SPARSE_SPLITTERS
 from sklearn.tree._splitter import Splitter
 from sklearn.tree._tree import BestFirstTreeBuilder, DepthFirstTreeBuilder, Tree
+from sklearn.utils._param_validation import Interval, StrOptions
 from sklearn.utils.validation import check_is_fitted, check_random_state
 
 from ..base import SurvivalAnalysisMixin
@@ -40,15 +40,15 @@ class SurvivalTree(BaseEstimator, SurvivalAnalysisMixin):
 
     Parameters
     ----------
-    splitter : string, optional, default: "best"
+    splitter : {'best', 'random'}, default: 'best'
         The strategy used to choose the split at each node. Supported
-        strategies are "best" to choose the best split and "random" to choose
+        strategies are 'best' to choose the best split and 'random' to choose
         the best random split.
 
     max_depth : int or None, optional, default: None
         The maximum depth of the tree. If None, then nodes are expanded until
         all leaves are pure or until all leaves contain less than
-        min_samples_split samples.
+        `min_samples_split` samples.
 
     min_samples_split : int, float, optional, default: 6
         The minimum number of samples required to split an internal node:
@@ -78,24 +78,29 @@ class SurvivalTree(BaseEstimator, SurvivalAnalysisMixin):
     max_features : int, float, string or None, optional, default: None
         The number of features to consider when looking for the best split:
 
-            - If int, then consider `max_features` features at each split.
-            - If float, then `max_features` is a fraction and
-              `int(max_features * n_features)` features are considered at each
-              split.
-            - If "auto", then `max_features=sqrt(n_features)`.
-            - If "sqrt", then `max_features=sqrt(n_features)`.
-            - If "log2", then `max_features=log2(n_features)`.
-            - If None, then `max_features=n_features`.
+        - If int, then consider `max_features` features at each split.
+        - If float, then `max_features` is a fraction and
+          `int(max_features * n_features)` features are considered at each
+          split.
+        - If "auto", then `max_features=sqrt(n_features)`.
+        - If "sqrt", then `max_features=sqrt(n_features)`.
+        - If "log2", then `max_features=log2(n_features)`.
+        - If None, then `max_features=n_features`.
 
         Note: the search for a split does not stop until at least one
         valid partition of the node samples is found, even if it requires to
         effectively inspect more than ``max_features`` features.
 
     random_state : int, RandomState instance or None, optional, default: None
-        If int, random_state is the seed used by the random number generator;
-        If RandomState instance, random_state is the random number generator;
-        If None, the random number generator is the RandomState instance used
-        by `np.random`.
+        Controls the randomness of the estimator. The features are always
+        randomly permuted at each split, even if ``splitter`` is set to
+        ``"best"``. When ``max_features < n_features``, the algorithm will
+        select ``max_features`` at random at each split before finding the best
+        split among them. But the best found split may vary across different
+        runs, even if ``max_features=n_features``. That is the case, if the
+        improvement of the criterion is identical for several splits and one
+        split has to be selected at random. To obtain a deterministic behaviour
+        during fitting, ``random_state`` has to be fixed to an integer.
 
     max_leaf_nodes : int or None, optional, default: None
         Grow a tree with ``max_leaf_nodes`` in best-first fashion.
@@ -138,6 +143,28 @@ class SurvivalTree(BaseEstimator, SurvivalAnalysisMixin):
            R News, 7(2), 25–31. https://cran.r-project.org/doc/Rnews/Rnews_2007-2.pdf.
     """
 
+    _parameter_constraints = {
+        "splitter": [StrOptions({"best", "random"})],
+        "max_depth": [Interval(Integral, 1, None, closed="left"), None],
+        "min_samples_split": [
+            Interval(Integral, 2, None, closed="left"),
+            Interval(Real, 0.0, 1.0, closed="neither"),
+        ],
+        "min_samples_leaf": [
+            Interval(Integral, 1, None, closed="left"),
+            Interval(Real, 0.0, 0.5, closed="right"),
+        ],
+        "min_weight_fraction_leaf": [Interval(Real, 0.0, 0.5, closed="both")],
+        "max_features": [
+            Interval(Integral, 1, None, closed="left"),
+            Interval(Real, 0.0, 1.0, closed="right"),
+            StrOptions({"auto", "sqrt", "log2"}, deprecated={"auto"}),
+            None,
+        ],
+        "random_state": ["random_state"],
+        "max_leaf_nodes": [Interval(Integral, 2, None, closed="left"), None],
+    }
+
     def __init__(self,
                  splitter="best",
                  max_depth=None,
@@ -156,8 +183,7 @@ def __init__(self,
         self.random_state = random_state
         self.max_leaf_nodes = max_leaf_nodes
 
-    def fit(self, X, y, sample_weight=None, check_input=True,
-            X_idx_sorted="deprecated"):
+    def fit(self, X, y, sample_weight=None, check_input=True):
         """Build a survival tree from the training set (X, y).
 
         Parameters
@@ -174,9 +200,6 @@ def fit(self, X, y, sample_weight=None, check_input=True,
             Allow to bypass several input checking.
             Don't use this parameter unless you know what you do.
 
-        X_idx_sorted : deprecated, default="deprecated"
-            This parameter is deprecated and has no effect
-
         Returns
         -------
         self
@@ -200,15 +223,6 @@ def fit(self, X, y, sample_weight=None, check_input=True,
         n_samples, self.n_features_in_ = X.shape
         params = self._check_params(n_samples)
 
-        if not isinstance(X_idx_sorted, str) or X_idx_sorted != "deprecated":
-            warnings.warn(
-                "The parameter 'X_idx_sorted' is deprecated and has no "
-                "effect. It will be removed in sklearn 1.1 (renaming of 0.26). "
-                "You can suppress this warning by not passing any value to the "
-                "'X_idx_sorted' parameter.",
-                FutureWarning
-            )
-
         self.n_outputs_ = self.event_times_.shape[0]
         # one "class" for CHF, one for survival function
         self.n_classes_ = np.ones(self.n_outputs_, dtype=np.intp) * 2
@@ -255,15 +269,26 @@ def fit(self, X, y, sample_weight=None, check_input=True,
         return self
 
     def _check_params(self, n_samples):
+        self._validate_params()
+
         # Check parameters
         max_depth = ((2 ** 31) - 1 if self.max_depth is None
                      else self.max_depth)
-        if max_depth <= 0:
-            raise ValueError("max_depth must be greater than zero.")
 
-        max_leaf_nodes = self._check_max_leaf_nodes()
-        min_samples_leaf = self._check_min_samples_leaf(n_samples)
-        min_samples_split = self._check_min_samples_split(n_samples)
+        max_leaf_nodes = (-1 if self.max_leaf_nodes is None
+                          else self.max_leaf_nodes)
+
+        if isinstance(self.min_samples_leaf, (Integral, np.integer)):
+            min_samples_leaf = self.min_samples_leaf
+        else:  # float
+            min_samples_leaf = int(ceil(self.min_samples_leaf * n_samples))
+
+        if isinstance(self.min_samples_split, Integral):
+            min_samples_split = self.min_samples_split
+        else:  # float
+            min_samples_split = int(ceil(self.min_samples_split * n_samples))
+            min_samples_split = max(2, min_samples_split)
+
         min_samples_split = max(min_samples_split, 2 * min_samples_leaf)
 
         self._check_max_features()
@@ -281,64 +306,16 @@ def _check_params(self, n_samples):
             "min_weight_leaf": min_weight_leaf,
         }
 
-    def _check_max_leaf_nodes(self):
-        max_leaf_nodes = (-1 if self.max_leaf_nodes is None
-                          else self.max_leaf_nodes)
-        if not isinstance(max_leaf_nodes, (numbers.Integral, np.integer)):
-            raise ValueError("max_leaf_nodes must be integral number but was "
-                             "%r" % max_leaf_nodes)
-        if -1 < max_leaf_nodes < 2:
-            raise ValueError(("max_leaf_nodes {} must be either None "
-                              "or larger than 1").format(max_leaf_nodes))
-        return max_leaf_nodes
-
-    def _check_min_samples_leaf(self, n_samples):
-        if isinstance(self.min_samples_leaf, (numbers.Integral, np.integer)):
-            if self.min_samples_leaf < 1:
-                raise ValueError("min_samples_leaf must be at least 1 "
-                                 "or in (0, 0.5], got %s"
-                                 % self.min_samples_leaf)
-            min_samples_leaf = self.min_samples_leaf
-        else:  # float
-            if not 0. < self.min_samples_leaf <= 0.5:
-                raise ValueError("min_samples_leaf must be at least 1 "
-                                 "or in (0, 0.5], got %s"
-                                 % self.min_samples_leaf)
-            min_samples_leaf = int(ceil(self.min_samples_leaf * n_samples))
-        # FIXME throw exception if min_samples_leaf < 2
-        return min_samples_leaf
-
-    def _check_min_samples_split(self, n_samples):
-        if isinstance(self.min_samples_split, (numbers.Integral, np.integer)):
-            if self.min_samples_split < 2:
-                raise ValueError("min_samples_split must be an integer "
-                                 "greater than 1 or a float in (0.0, 1.0]; "
-                                 "got the integer %s"
-                                 % self.min_samples_split)
-            min_samples_split = self.min_samples_split
-        else:  # float
-            if not 0. < self.min_samples_split <= 1.:
-                raise ValueError("min_samples_split must be an integer "
-                                 "greater than 1 or a float in (0.0, 1.0]; "
-                                 "got the float %s"
-                                 % self.min_samples_split)
-            min_samples_split = int(ceil(self.min_samples_split * n_samples))
-            min_samples_split = max(2, min_samples_split)
-        return min_samples_split
-
     def _check_max_features(self):
         if isinstance(self.max_features, str):
             if self.max_features in ("auto", "sqrt"):
                 max_features = max(1, int(np.sqrt(self.n_features_in_)))
             elif self.max_features == "log2":
                 max_features = max(1, int(np.log2(self.n_features_in_)))
-            else:
-                raise ValueError(
-                    'Invalid value for max_features. Allowed string '
-                    'values are "auto", "sqrt" or "log2".')
+
         elif self.max_features is None:
             max_features = self.n_features_in_
-        elif isinstance(self.max_features, (numbers.Integral, np.integer)):
+        elif isinstance(self.max_features, (Integral, np.integer)):
             max_features = self.max_features
         else:  # float
             if self.max_features > 0.0:


Test patch:
diff --git a/.github/workflows/tests-workflow.yaml b/.github/workflows/tests-workflow.yaml
index 7792908f..a8bd7945 100644
--- a/.github/workflows/tests-workflow.yaml
+++ b/.github/workflows/tests-workflow.yaml
@@ -12,10 +12,10 @@ jobs:
       fail-fast: false
       matrix:
         config:
-          - py38_pandas10_numpy119_sklearn11
-          - py38_pandas11_numpy120_sklearn11
-          - py39_pandas13_numpy121_sklearn11
-          - py310_pandas14_numpy121_sklearn11
+          - py38_pandas10_numpy119_sklearn12
+          - py38_pandas11_numpy120_sklearn12
+          - py39_pandas13_numpy121_sklearn12
+          - py310_pandas14_numpy121_sklearn12
         runner:
           - ubuntu-latest
           - macos-latest
@@ -106,7 +106,7 @@ jobs:
       - name: Set dependencies
         id: dependencies
         env:
-          DEPS_CONFIG: py310_pandas14_numpy121_sklearn11
+          DEPS_CONFIG: py310_pandas14_numpy121_sklearn12
         run: |
           source ci/deps/${DEPS_CONFIG}.sh
           echo "CONDA_PYTHON_VERSION=${CONDA_PYTHON_VERSION}" >> $GITHUB_ENV
diff --git a/tests/test_boosting.py b/tests/test_boosting.py
index e7a4394c..fb68fd90 100644
--- a/tests/test_boosting.py
+++ b/tests/test_boosting.py
@@ -25,6 +25,13 @@ def early_stopping_monitor(i, est, locals_):  # pylint: disable=unused-argument
 
 
 class MaxFeaturesCases(FixtureParameterFactory):
+
+    _prefix = (
+        "The 'max_features' parameter of GradientBoostingSurvivalAnalysis must be "
+        r"an int in the range \[1, inf\), a float in the range \(0\.0, 1\.0\], a str "
+        r"among {.+} or None\. "
+    )
+
     @property
     def n_features(self):
         return 14
@@ -45,15 +52,15 @@ def data_5(self):
         return 5, 5, does_not_raise()
 
     def data_m1(self):
-        return -1, None, pytest.raises(ValueError, match=r"max_features must be in \(0, n_features_in_\]")
+        return -1, None, pytest.raises(ValueError, match=MaxFeaturesCases._prefix + r"Got -1 instead\.")
 
     def data_m1p125(self):
-        return -1.125, None, pytest.raises(ValueError, match=r"max_features must be in \(0, 1.0\]")
+        return -1.125, None, pytest.raises(ValueError, match=MaxFeaturesCases._prefix + r"Got -1\.125 instead\.")
 
     def data_fail_me(self):
         return "fail_me", None, pytest.raises(
             ValueError,
-            match="Invalid value for max_features: 'fail_me'. Allowed string values are 'auto', 'sqrt' or 'log2'"
+            match=MaxFeaturesCases._prefix + r"Got 'fail_me' instead\."
         )
 
 
@@ -145,19 +152,25 @@ def test_fit_dropout(self):
         assert not hasattr(model, "oob_improvement_")
         assert model.max_features_ == 8
 
-    def test_fit_int_param_as_float(self):
-        # Account for https://github.com/scikit-learn/scikit-learn/pull/12344
-        max_depth = 4
+    @pytest.mark.parametrize(
+        "parameter,value",
+        [
+            ("max_depth", 4.0),
+            ("n_estimators", 100.0),
+            ("max_leaf_nodes", 15.0),
+            ("min_samples_split", 10.0),
+        ]
+    )
+    def test_fit_int_param_as_float(self, parameter, value):
+        kwargs = {parameter: value}
+        model = GradientBoostingSurvivalAnalysis(**kwargs)
 
-        model = self.assert_fit_and_predict(
-            expected_cindex=(0.90256690042449006, 67826, 7321, 2, 14),
-            n_estimators=100.0, max_depth=float(max_depth), max_leaf_nodes=15.0, min_samples_split=10.0,
-        )
-        params = model.get_params()
-        assert 100 == params["n_estimators"]
-        assert max_depth == params["max_depth"]
-        assert 10 == params["min_samples_split"]
-        assert 15 == model.get_params()["max_leaf_nodes"]
+        msg = f"The '{parameter}' parameter of GradientBoostingSurvivalAnalysis " \
+              "must be an int in the range "
+
+        X, y = self.data
+        with pytest.raises(ValueError, match=msg):
+            model.fit(X, y)
 
     @pytest.mark.parametrize("fn,expected_file",
                              [("predict_survival_function", GBOOST_SURV_FILE),
@@ -220,7 +233,8 @@ def test_negative_ccp_alpha(make_whas500):
         whas500_data = make_whas500(with_std=False, to_numeric=True)
 
         clf = GradientBoostingSurvivalAnalysis()
-        msg = "ccp_alpha == -1.0, must be >= 0.0"
+        msg = "The 'ccp_alpha' parameter of GradientBoostingSurvivalAnalysis must be a float in the range " \
+              r"\[0\.0, inf\). Got -1\.0 instead\."
 
         clf.set_params(ccp_alpha=-1.0)
         with pytest.raises(ValueError, match=msg):
@@ -544,51 +558,55 @@ def sample_gb_class(request):
     return request.param, x, y
 
 
-def test_param_n_estimators(sample_gb_class):
+@pytest.mark.parametrize("n_estimators", [0, -1])
+def test_param_n_estimators(sample_gb_class, n_estimators):
     est_cls, x, y = sample_gb_class
-    model = est_cls(n_estimators=0)
+    model = est_cls(n_estimators=n_estimators)
 
-    with pytest.raises(ValueError, match="n_estimators must be greater than 0 but was 0"):
-        model.fit(x, y)
+    msg = f"The 'n_estimators' parameter of {est_cls.__name__} must be an int " \
+          r"in the range \[1, inf\)\. " \
+          f"Got {n_estimators} instead\\."
 
-    model.set_params(n_estimators=-1)
-    with pytest.raises(ValueError, match="n_estimators must be greater than 0 but was -1"):
+    with pytest.raises(ValueError, match=msg):
         model.fit(x, y)
 
 
-def test_param_learning_rate(sample_gb_class):
+@pytest.mark.parametrize("learning_rate", [-np.finfo(float).eps, -1])
+def test_param_learning_rate(sample_gb_class, learning_rate):
     est_cls, x, y = sample_gb_class
-    model = est_cls(learning_rate=0)
+    model = est_cls(learning_rate=learning_rate)
 
-    with pytest.raises(ValueError, match="learning_rate must be within ]0; 1] but was 0"):
-        model.fit(x, y)
+    msg = f"The 'learning_rate' parameter of {est_cls.__name__} must be a float " \
+          r"in the range \[0\.0, inf\)\. " \
+          f"Got {learning_rate} instead\\."
 
-    model.set_params(learning_rate=1.2)
-    with pytest.raises(ValueError, match="learning_rate must be within ]0; 1] but was 1.2"):
+    with pytest.raises(ValueError, match=msg):
         model.fit(x, y)
 
 
-def test_param_subsample(sample_gb_class):
+@pytest.mark.parametrize("subsample", [0, 1.2])
+def test_param_subsample(sample_gb_class, subsample):
     est_cls, x, y = sample_gb_class
-    model = est_cls(subsample=0)
+    model = est_cls(subsample=subsample)
 
-    with pytest.raises(ValueError, match="subsample must be in ]0; 1] but was 0"):
-        model.fit(x, y)
+    msg = f"The 'subsample' parameter of {est_cls.__name__} must be a float " \
+          r"in the range \(0\.0, 1\.0\]\. " \
+          f"Got {subsample} instead\\."
 
-    model.set_params(subsample=1.2)
-    with pytest.raises(ValueError, match="subsample must be in ]0; 1] but was 1.2"):
+    with pytest.raises(ValueError, match=msg):
         model.fit(x, y)
 
 
-def test_param_dropout_rate(sample_gb_class):
+@pytest.mark.parametrize("dropout_rate", [-0.1, 1.0, 1.2])
+def test_param_dropout_rate(sample_gb_class, dropout_rate):
     est_cls, x, y = sample_gb_class
-    model = est_cls(dropout_rate=-0.1)
+    model = est_cls(dropout_rate=dropout_rate)
 
-    with pytest.raises(ValueError, match=r"dropout_rate must be within \[0; 1\[, but was -0.1"):
-        model.fit(x, y)
+    msg = f"The 'dropout_rate' parameter of {est_cls.__name__} must be a float " \
+          r"in the range \[0\.0, 1\.0\)\. " \
+          f"Got {dropout_rate} instead\\."
 
-    model.set_params(dropout_rate=1.2)
-    with pytest.raises(ValueError, match=r"dropout_rate must be within \[0; 1\[, but was 1.2"):
+    with pytest.raises(ValueError, match=msg):
         model.fit(x, y)
 
 
@@ -604,17 +622,14 @@ def test_param_sample_weight(sample_gb_class):
         model.fit(x, y, [2, 4, 5, 6, 7, 1, 2, 7])
 
 
-def test_param_loss(sample_gb_class):
+@pytest.mark.parametrize("loss", ["", "unknown", None])
+def test_param_loss(sample_gb_class, loss):
     est_cls, x, y = sample_gb_class
-    model = est_cls(loss="")
-
-    with pytest.raises(ValueError, match="Loss '' not supported"):
-        model.fit(x, y)
+    model = est_cls(loss=loss)
 
-    model.set_params(loss="unknown")
-    with pytest.raises(ValueError, match="Loss 'unknown' not supported"):
-        model.fit(x, y)
+    msg = f"The 'loss' parameter of {est_cls.__name__} must be a str among " \
+          r"{.+}\. " \
+          f"Got {loss!r} instead\\."
 
-    model.set_params(loss=None)
-    with pytest.raises(ValueError, match="Loss None not supported"):
+    with pytest.raises(ValueError, match=msg):
         model.fit(x, y)
diff --git a/tests/test_clinical_kernel.py b/tests/test_clinical_kernel.py
index 730ee152..4bdb1d31 100644
--- a/tests/test_clinical_kernel.py
+++ b/tests/test_clinical_kernel.py
@@ -192,7 +192,10 @@ def test_kernel_transform_feature_names_mismatch(make_data):
             np.zeros((2, data.shape[1] + 1), dtype=float), columns=data.columns.tolist() + ["XYZ"]
         )
 
-        error_msg = r"X has 5 features, but ClinicalKernelTransform is expecting 4 features as input\."
+        error_msg = r"""The feature names should match those that were passed during fit\.
+Feature names unseen at fit time:
+- XYZ
+"""
         warn_msg = r"The feature names should match those that were passed during fit\."
         with pytest.raises(ValueError, match=error_msg), pytest.warns(FutureWarning, match=warn_msg):
             t.transform(df_test)
diff --git a/tests/test_compare.py b/tests/test_compare.py
index 7cd8e178..81a5eda4 100644
--- a/tests/test_compare.py
+++ b/tests/test_compare.py
@@ -47,9 +47,9 @@ def test_logrank_veterans():
         columns=["counts", "observed", "expected", "statistic"],
         index=pd.Index(["adeno", "large", "smallcell", "squamous"], name="group")
     )
-    expected_stats.loc[:, "counts"] = np.array([27, 27, 48, 35], dtype=np.intp)
-    expected_stats.loc[:, "observed"] = np.array([26, 26, 45, 31], dtype=int)
-    expected_stats.loc[:, "expected"] = [15.6937646143605, 34.5494783863493, 30.1020793268148, 47.6546776724754]
+    expected_stats["counts"] = np.array([27, 27, 48, 35], dtype=np.intp)
+    expected_stats["observed"] = np.array([26, 26, 45, 31], dtype=int)
+    expected_stats["expected"] = [15.6937646143605, 34.5494783863493, 30.1020793268148, 47.6546776724754]
     assert_stats_frame_equal(stats, expected_stats)
 
     expected_var = np.array([
diff --git a/tests/test_coxnet.py b/tests/test_coxnet.py
index b4499b2c..85a9c4b9 100644
--- a/tests/test_coxnet.py
+++ b/tests/test_coxnet.py
@@ -677,23 +677,36 @@ def test_max_iter(self):
 
     @pytest.mark.parametrize('val', [0, -1, -1e-6, 1 + 1e-6, 1512, np.nan, np.infty])
     def test_invalid_l1_ratio(self, val):
-        with pytest.raises(ValueError,
-                           match=r"l1_ratio must be in interval \]0;1\]"):
+        msg = "The 'l1_ratio' parameter of CoxnetSurvivalAnalysis must be a float " \
+              r"in the range \(0\.0, 1\.0]\. " \
+              f"Got {val} instead\\."
+
+        with pytest.raises(ValueError, match=msg):
             self._fit(alpha_min_ratio=0.0001, l1_ratio=val)
 
-    def test_invalid_tol(self, invalid_positive_int):
-        with pytest.raises(ValueError,
-                           match="tolerance must be positive"):
-            self._fit(alpha_min_ratio=0.0001, tol=invalid_positive_int)
+    @pytest.mark.parametrize("tol", [-1, -1e-6, -np.infty])
+    def test_invalid_tol(self, tol):
+        msg = "The 'tol' parameter of CoxnetSurvivalAnalysis must be a float " \
+              r"in the range \[0, inf\)\. " \
+              f"Got {tol} instead\\."
+
+        with pytest.raises(ValueError, match=msg):
+            self._fit(alpha_min_ratio=0.0001, tol=tol)
 
     def test_invalid_max_iter(self, invalid_positive_int):
-        with pytest.raises(ValueError,
-                           match="max_iter must be a positive integer"):
+        msg = "The 'max_iter' parameter of CoxnetSurvivalAnalysis must be an int " \
+              r"in the range \[1, inf\)\. " \
+              f"Got {invalid_positive_int} instead\\."
+
+        with pytest.raises(ValueError, match=msg):
             self._fit(alpha_min_ratio=0.0001, max_iter=invalid_positive_int)
 
     def test_invalid_n_alphas(self, invalid_positive_int):
-        with pytest.raises(ValueError,
-                           match="n_alphas must be a positive integer"):
+        msg = "The 'n_alphas' parameter of CoxnetSurvivalAnalysis must be an int " \
+              r"in the range \[1, inf\)\. " \
+              f"Got {invalid_positive_int} instead\\."
+
+        with pytest.raises(ValueError, match=msg):
             self._fit(alpha_min_ratio=0.0001, n_alphas=invalid_positive_int)
 
     @pytest.mark.parametrize('length', [0, 1, 29, 31])
@@ -736,14 +749,20 @@ def test_nan_alphas(self, nan_float_array):
             self._fit(alpha_min_ratio=0.0001, alphas=nan_float_array)
 
     def test_invalid_alpha_min_ratio_string(self):
-        with pytest.raises(ValueError,
-                           match="Invalid value for alpha_min_ratio"):
+        msg = "The 'alpha_min_ratio' parameter of CoxnetSurvivalAnalysis must be a float " \
+              r"in the range \(0, inf\) or a str among {'auto'}\. " \
+              r"Got 'max' instead\."
+
+        with pytest.raises(ValueError, match=msg):
             self._fit(alpha_min_ratio="max")
 
     @pytest.mark.parametrize("value", [0.0, -1e-12, -1, -np.infty, np.nan])
     def test_invalid_alpha_min_ratio_float(self, value):
-        with pytest.raises(ValueError,
-                           match="alpha_min_ratio must be positive"):
+        msg = "The 'alpha_min_ratio' parameter of CoxnetSurvivalAnalysis must be a float " \
+              r"in the range \(0, inf\) or a str among {'auto'}\. " \
+              f"Got {value} instead\\."
+
+        with pytest.raises(ValueError, match=msg):
             self._fit(alpha_min_ratio=value)
 
     @staticmethod
@@ -760,7 +779,7 @@ def test_alpha_too_small(breast_cancer):
             184, 185, 186, 187, 188, 190, 191, 192, 193, 194, 195, 196, 197
         ])
 
-        nn = CoxnetSurvivalAnalysis(alphas=[0.007295025406624247], alpha_min_ratio=0.0001, l1_ratio=1.0)
+        nn = CoxnetSurvivalAnalysis(alphas=np.array([0.007295025406624247]), alpha_min_ratio=0.0001, l1_ratio=1.0)
         Xf, yf = Xt.iloc[index], y[index]
 
         with pytest.raises(ArithmeticError,
diff --git a/tests/test_coxph.py b/tests/test_coxph.py
index 8a5bb232..bd37726c 100644
--- a/tests/test_coxph.py
+++ b/tests/test_coxph.py
@@ -356,14 +356,15 @@ def test_fit_unpenalized():
         assert_array_almost_equal(cph.coef_, coef)
 
     @staticmethod
-    def test_alpha(rossi):
-        cph = CoxPHSurvivalAnalysis(alpha=-0.0001)
+    @pytest.mark.parametrize("alpha", [-0.0001, -1.25])
+    def test_alpha(rossi, alpha):
+        cph = CoxPHSurvivalAnalysis(alpha=alpha)
 
-        with pytest.raises(ValueError, match=r"alpha must be positive, but was -0\.0001"):
-            cph.fit(rossi.x.values, rossi.y)
+        msg = "The 'alpha' parameter of CoxPHSurvivalAnalysis must be a float " \
+              r"in the range \[0, inf\) or an instance of 'numpy\.ndarray'\. " \
+              f"Got {alpha} instead\\."
 
-        cph.set_params(alpha=-1.25)
-        with pytest.raises(ValueError, match=r"alpha must be positive, but was -1\.25"):
+        with pytest.raises(ValueError, match=msg):
             cph.fit(rossi.x.values, rossi.y)
 
     @staticmethod
@@ -405,7 +406,23 @@ def test_alpha_array(rossi):
     @staticmethod
     def test_ties(rossi):
         cph = CoxPHSurvivalAnalysis(ties="xyz")
-        with pytest.raises(ValueError, match="ties must be one of 'breslow', 'efron'"):
+
+        msg = "The 'ties' parameter of CoxPHSurvivalAnalysis must be a str " \
+              r"among {.+}\. Got 'xyz' instead\."
+
+        with pytest.raises(ValueError, match=msg):
+            cph.fit(rossi.x.values, rossi.y)
+
+    @staticmethod
+    @pytest.mark.parametrize("n_iter", [0, -1, 10.0])
+    def test_n_iter(rossi, n_iter):
+        cph = CoxPHSurvivalAnalysis(n_iter=n_iter)
+
+        msg = "The 'n_iter' parameter of CoxPHSurvivalAnalysis must be an int " \
+              r"in the range \[1, inf\)\. " \
+              f"Got {n_iter} instead\\."
+
+        with pytest.raises(ValueError, match=msg):
             cph.fit(rossi.x.values, rossi.y)
 
     @staticmethod
diff --git a/tests/test_ensemble_selection.py b/tests/test_ensemble_selection.py
index 37ce999b..017153fe 100644
--- a/tests/test_ensemble_selection.py
+++ b/tests/test_ensemble_selection.py
@@ -159,6 +159,21 @@ def test_feature_names_in(make_whas500):
 
 
 class EnsembleSelectionFailureCases(FixtureParameterFactory):
+    _msg_min_correlation = (
+        "The 'min_correlation' parameter of EnsembleSelection must be "
+        r"a float in the range \[-1, 1\]\. "
+        "Got {value} instead\\."
+    )
+    _msg_correlation = (
+        "The 'correlation' parameter of EnsembleSelection must be "
+        r"a str among {{.+}}\. "
+        "Got {value!r} instead\\."
+    )
+    _msg_scorer = (
+        "The 'scorer' parameter of EnsembleSelection must be "
+        r"a callable\. Got {value} instead\."
+    )
+
     def data_min_score(self):
         params = {"scorer": score_cindex, "min_score": 1.0, "cv": 3}
         match = "no base estimator exceeds min_score, try decreasing it"
@@ -166,32 +181,37 @@ def data_min_score(self):
 
     def data_min_correlation_0(self):
         params = {"scorer": score_cindex, "min_correlation": 1.2}
-        match = r"min_correlation must be in \[-1; 1\], but was 1.2"
+        match = self._msg_min_correlation.format(value=1.2)
         return params, ValueError, match
 
     def data_min_correlation_1(self):
         params = {"scorer": score_cindex, "min_correlation": -2.1}
-        match = r"min_correlation must be in \[-1; 1\], but was -2.1"
+        match = self._msg_min_correlation.format(value=-2.1)
         return params, ValueError, match
 
     def data_min_correlation_2(self):
         params = {"scorer": score_cindex, "min_correlation": np.nan}
-        match = r"min_correlation must be in \[-1; 1\], but was nan"
+        match = self._msg_min_correlation.format(value=np.nan)
         return params, ValueError, match
 
     def data_scorer_none(self):
         params = {"scorer": None}
-        match = "scorer is not callable"
+        match = self._msg_scorer.format(value=None)
         return params, TypeError, match
 
     def data_scorer_not_callable(self):
         params = {"scorer": np.zeros(10)}
-        match = "scorer is not callable"
+        value = r"array\(\[0\., 0\., 0\., 0\., 0\., 0\., 0\., 0\., 0\., 0\.\]\)"
+        match = self._msg_scorer.format(value=value)
         return params, TypeError, match
 
     def data_n_estimators_0(self):
         params = {"scorer": score_cindex, "n_estimators": 0}
-        match = "n_estimators must not be zero or negative"
+        match = (
+            "The 'n_estimators' parameter of EnsembleSelection must be "
+            r"an int in the range \[1, inf\) or a float in the range \(0\.0, 1\.0\]\. "
+            r"Got 0 instead\."
+        )
         return params, ValueError, match
 
     def data_n_estimators_1000(self):
@@ -201,17 +221,17 @@ def data_n_estimators_1000(self):
 
     def data_correlation_none(self):
         params = {"scorer": score_cindex, "correlation": None}
-        match = "correlation must be one of 'pearson', 'kendall', and 'spearman', but got None"
+        match = self._msg_correlation.format(value=None)
         return params, ValueError, match
 
     def data_correlation_int(self):
         params = {"scorer": score_cindex, "correlation": 2143}
-        match = "correlation must be one of 'pearson', 'kendall', and 'spearman', but got 2143"
+        match = self._msg_correlation.format(value=2143)
         return params, ValueError, match
 
     def data_correlation_str(self):
         params = {"scorer": score_cindex, "correlation": "clearly wrong"}
-        match = "correlation must be one of 'pearson', 'kendall', and 'spearman', but got 'clearly wrong'"
+        match = self._msg_correlation.format(value='clearly wrong')
         return params, ValueError, match
 
 
diff --git a/tests/test_forest.py b/tests/test_forest.py
index 3f2c4e4a..acf57758 100644
--- a/tests/test_forest.py
+++ b/tests/test_forest.py
@@ -238,30 +238,48 @@ def test_pipeline_predict(breast_cancer, forest_cls, func):
 
 @pytest.mark.parametrize('forest_cls', FORESTS)
 @pytest.mark.parametrize(
-    'max_samples, exc_type, exc_msg',
+    'max_samples, exc_type, exc_msg, with_prefix',
     [(int(1e9), ValueError,
-      "`max_samples` must be in range 1 to 500 but got value 1000000000"),
+      "`max_samples` must be <= n_samples=500 but got value 1000000000", False),
      (1.0 + 1e-7, ValueError,
-      r"`max_samples` must be in range \(0\.0, 1\.0] but got value 1.0"),
+      r"Got 1\.0000001 instead", True),
      (2.0, ValueError,
-      r"`max_samples` must be in range \(0\.0, 1\.0] but got value 2.0"),
+      r"Got 2\.0 instead", True),
      (0.0, ValueError,
-      r"`max_samples` must be in range \(0\.0, 1\.0] but got value 0.0"),
+      r"Got 0\.0 instead", True),
      (np.nan, ValueError,
-      r"`max_samples` must be in range \(0\.0, 1\.0] but got value nan"),
+      "Got nan instead", True),
      (np.inf, ValueError,
-      r"`max_samples` must be in range \(0\.0, 1\.0] but got value inf"),
+      r"Got inf instead", True),
      ('str max_samples?!', TypeError,
-      r"`max_samples` should be int or float, but got "
-      r"type '\<class 'str'\>'"),
+      r"Got 'str max_samples\?!' instead", True),
      (np.ones(2), TypeError,
-      r"`max_samples` should be int or float, but got type "
-      r"'\<class 'numpy.ndarray'\>'")]
+      r"Got array\(\[1\., 1\.\]\) instead", True),
+     (0, ValueError, r"Got 0 instead", True)]
 )
-def test_fit_max_samples(make_whas500, forest_cls, max_samples, exc_type, exc_msg):
+def test_fit_max_samples(make_whas500, forest_cls, max_samples, exc_type, exc_msg, with_prefix):
     whas500 = make_whas500(to_numeric=True)
     forest = forest_cls(max_samples=max_samples)
-    with pytest.raises(exc_type, match=exc_msg):
+    prefix = f"The 'max_samples' parameter of {forest_cls.__name__} must be None, " \
+             r"a float in the range \(0\.0, 1\.0] or an int in the range \[1, inf\)\. "
+    if with_prefix:
+        msg = prefix + exc_msg
+    else:
+        msg = exc_msg
+    with pytest.raises(exc_type, match=msg):
+        forest.fit(whas500.x, whas500.y)
+
+
+@pytest.mark.parametrize('forest_cls', FORESTS)
+@pytest.mark.parametrize('max_features', [0, 0.0, 3.0, "", "None", "sqrt_", "log10", "car"])
+def test_fit_max_features(make_whas500, forest_cls, max_features):
+    whas500 = make_whas500(to_numeric=True)
+    forest = forest_cls(max_features=max_features)
+
+    msg = f"The 'max_features' parameter of {forest_cls.__name__} must be " \
+          r"an int in the range \[1, inf\), a float in the range \(0\.0, 1\.0\], " \
+          r"a str among {.+} or None\."
+    with pytest.raises(ValueError, match=msg):
         forest.fit(whas500.x, whas500.y)
 
 
diff --git a/tests/test_io.py b/tests/test_io.py
index 78a7eed3..14176ff0 100644
--- a/tests/test_io.py
+++ b/tests/test_io.py
@@ -63,7 +63,7 @@ def data_nominal(self):
 
     def data_nominal_with_quotes(self):
         data, rel_name, expected = self.data_nominal()
-        data.loc[:, 'attr_nominal_spaces'] = [
+        data['attr_nominal_spaces'] = [
             "'red wine'", "'hard liquor'", None, 'mate', "'hard liquor'", 'mate'
         ]
         return data, rel_name, expected
@@ -71,14 +71,14 @@ def data_nominal_with_quotes(self):
     def data_nominal_as_category(self):
         data, rel_name, expected = self.data_nominal_with_quotes()
         for name, series in data.iteritems():
-            data.loc[:, name] = pd.Categorical(series, ordered=False)
+            data[name] = pd.Categorical(series, ordered=False)
 
         expected[3] = "@attribute attr_nominal_spaces\t{\"hard liquor\",\"red wine\",mate}\n"
         return data, rel_name, expected
 
     def data_nominal_as_category_extra(self):
         data, rel_name, expected = self.data_nominal_as_category()
-        data.loc[:, 'attr_nominal'] = pd.Categorical(
+        data['attr_nominal'] = pd.Categorical(
             ['water', 'wine', 'beer', None, 'wine', 'water'],
             categories=['beer', 'coke', 'water', 'wine'],
             ordered=False,
@@ -89,7 +89,7 @@ def data_nominal_as_category_extra(self):
 
     def data_nominal_with_category_ordering(self):
         data, rel_name, expected = self.data_nominal_with_quotes()
-        data.loc[:, 'attr_nominal'] = pd.Categorical(
+        data['attr_nominal'] = pd.Categorical(
             ['water', 'wine', 'beer', None, 'wine', 'water'],
             categories=['water', 'coke', 'beer', 'wine'],
             ordered=False,
diff --git a/tests/test_metrics.py b/tests/test_metrics.py
index 1e309528..e330e4e0 100644
--- a/tests/test_metrics.py
+++ b/tests/test_metrics.py
@@ -949,7 +949,7 @@ def test_brier_wrong_estimate_shape(nottingham_prognostic_index):
 
 def test_brier_coxph():
     X, y = load_gbsg2()
-    X.loc[:, "tgrade"] = X.loc[:, "tgrade"].map(len).astype(int)
+    X["tgrade"] = X.loc[:, "tgrade"].map(len).astype(int)
 
     Xt = OneHotEncoder().fit_transform(X)
 
diff --git a/tests/test_minlip.py b/tests/test_minlip.py
index 2cb902bf..e612f846 100644
--- a/tests/test_minlip.py
+++ b/tests/test_minlip.py
@@ -409,7 +409,7 @@ def test_kernel_precomputed(gbsg2_scaled, solver):
     @pytest.mark.parametrize("solver", ["osqp", "ecos"])
     def test_max_iter(gbsg2_scaled, solver):
         x, y = gbsg2_scaled
-        m = MinlipSurvivalAnalysis(solver=solver, alpha=1, kernel="polynomial",
+        m = MinlipSurvivalAnalysis(solver=solver, alpha=1, kernel="poly",
                                    degree=2, pairs="next", max_iter=5)
 
         with pytest.warns(ConvergenceWarning,
@@ -417,15 +417,19 @@ def test_max_iter(gbsg2_scaled, solver):
             m.fit(x, y)
 
 
-@pytest.mark.parametrize("solver,error", [
-    (None, "unknown solver: None"),
-    ("i don't know", "unknown solver: i don't know"),
-    ([('why', 'are'), ('you', 'doing this')], r"unknown solver: \[\('why', 'are'\), \('you', 'doing this'\)\]"),
+@pytest.mark.parametrize("solver", [
+    None,
+    "i don't know",
+    [('why', 'are'), ('you', 'doing this')],
 ])
-def test_unknown_solver(gbsg2, solver, error):
+def test_unknown_solver(gbsg2, solver):
     x, y = gbsg2
     m = MinlipSurvivalAnalysis(solver=solver)
-    with pytest.raises(ValueError, match=error):
+
+    msg = "The 'solver' parameter of MinlipSurvivalAnalysis must be " \
+          r"a str among \{.+\}\. " \
+          r"Got .+ instead\."
+    with pytest.raises(ValueError, match=msg):
         m.fit(x, y)
 
 
diff --git a/tests/test_pandas_inputs.py b/tests/test_pandas_inputs.py
index 52187e8f..a1d16d6e 100644
--- a/tests/test_pandas_inputs.py
+++ b/tests/test_pandas_inputs.py
@@ -23,9 +23,9 @@ def test_pandas_inputs(estimator_cls):
     assert_array_equal(estimator.feature_names_in_, np.asarray(X_df.columns, dtype=object))
     estimator.predict(X_df)
 
-    msg = "The feature names should match those that were passed"
+    msg = "Feature names must be in the same order as they were in fit"
     X_bad = pd.DataFrame(X_np, columns=X_df.columns.tolist()[::-1])
-    with pytest.warns(FutureWarning, match=msg):
+    with pytest.raises(ValueError, match=msg):
         estimator.predict(X_bad)
 
     # warns when fitted on dataframe and transforming a ndarray
diff --git a/tests/test_stacking.py b/tests/test_stacking.py
index e5d4e2c4..b06e7bdc 100644
--- a/tests/test_stacking.py
+++ b/tests/test_stacking.py
@@ -83,14 +83,17 @@ def test_base_estimator(estimator, dummy_data):
     def test_meta_no_fit(dummy_data):
         est = Stacking(_NoFitEstimator(), [("m1", _PredictDummy())])
         X, y = dummy_data
-        with pytest.raises(TypeError,
-                           match=r"meta estimator should implement fit (.+) doesn't"):
+
+        msg = "The 'meta_estimator' parameter of Stacking must be an object " \
+              r"implementing 'fit'\. Got _NoFitEstimator\(\) instead\."
+
+        with pytest.raises(TypeError, match=msg):
             est.fit(X, y)
 
     @staticmethod
     def test_names_not_unique(dummy_data):
         est = Stacking(
-            _NoFitEstimator(),
+            _NoPredictDummy(),
             [("m1", _PredictDummy()), ("m2", _PredictDummy()), ("m1", _PredictDummy())]
         )
         X, y = dummy_data
diff --git a/tests/test_survival_svm.py b/tests/test_survival_svm.py
index bdc43cc1..7df9bbd0 100644
--- a/tests/test_survival_svm.py
+++ b/tests/test_survival_svm.py
@@ -69,23 +69,33 @@ def test_unknown_optimizer(svm_cls, fake_data):
     x, y = fake_data
 
     ssvm = svm_cls(optimizer="random stuff")
-    with pytest.raises(ValueError, match="unknown optimizer: random stuff"):
+    msg = f"The 'optimizer' parameter of {svm_cls.__name__} must be " \
+          r"a str among \{.+\} or None\. Got 'random stuff' instead\."
+    with pytest.raises(ValueError, match=msg):
         ssvm.fit(x, y)
 
 
 class FastSurvivalSVMFailureCases(FixtureParameterFactory):
+    _prefix = "The '{name}' parameter of {estimator} must be "
+
     @property
     def x(self):
         return np.arange(80).reshape(10, 8)
 
     def data_alpha_negative(self):
         params = {"alpha": -1}
-        error = pytest.raises(ValueError, match="alpha must be positive")
+        msg = "The 'alpha' parameter of Fast(Kernel)?SurvivalSVM must be " \
+              r"a float in the range \(0\.0, inf\)\. " \
+              r"Got -1 instead\."
+        error = pytest.raises(ValueError, match=msg)
         return params, None, None, error
 
     def _rank_ratio_out_of_bounds(self, rank_ratio):
         params = {"rank_ratio": rank_ratio}
-        error = pytest.raises(ValueError, match=r"rank_ratio must be in \[0; 1\]")
+        msg = "The 'rank_ratio' parameter of Fast(Kernel)?SurvivalSVM must be " \
+              r"a float in the range \[0\.0, 1\.0\]\. " \
+              f"Got {rank_ratio!r} instead\\."
+        error = pytest.raises(ValueError, match=msg)
         return params, None, None, error
 
     def data_rank_ratio_out_of_bounds_0(self):
@@ -577,13 +587,13 @@ def test_compare_builtin_kernel(make_whas500):
         x = normalize(whas500.x)
 
         rsvm = FastKernelSurvivalSVM(
-            optimizer="rbtree", kernel="polynomial",
+            optimizer="rbtree", kernel="poly",
             gamma=0.5, degree=2, coef0=0,
             tol=2.5e-8, max_iter=100, random_state=0xf38
         )
 
         kpca = KernelPCA(
-            kernel="polynomial", copy_X=True, gamma=0.5, degree=2, coef0=0, random_state=0xf38
+            kernel="poly", copy_X=True, gamma=0.5, degree=2, coef0=0, random_state=0xf38
         )
         xt = kpca.fit_transform(x)
         nrsvm = FastSurvivalSVM(optimizer="rbtree", tol=2.5e-8, max_iter=100, random_state=0xf38)
diff --git a/tests/test_tree.py b/tests/test_tree.py
index 5c427712..5ff35957 100644
--- a/tests/test_tree.py
+++ b/tests/test_tree.py
@@ -24,8 +24,8 @@ def veterans():
 @pytest.fixture()
 def breast_cancer():
     X, y = load_breast_cancer()
-    X.loc[:, "er"] = X.loc[:, "er"].replace({"negative": 0, "positive": 1})
-    X.loc[:, "grade"] = X.loc[:, "grade"].replace({
+    X["er"] = X.loc[:, "er"].replace({"negative": 0, "positive": 1})
+    X["grade"] = X.loc[:, "grade"].replace({
         "intermediate": 0,
         "poorly differentiated": 1,
         "unkown": 2,
@@ -417,8 +417,9 @@ def test_max_depth(fake_data, val):
     X, y = fake_data
     tree = SurvivalTree(max_depth=val)
 
-    with pytest.raises(ValueError,
-                       match="max_depth must be greater than zero."):
+    msg = r"The 'max_depth' parameter of SurvivalTree must be " \
+          r"an int in the range \[1, inf\) or None\. Got {!r} instead\.".format(val)
+    with pytest.raises(ValueError, match=msg):
         tree.fit(X, y)
 
 
@@ -427,21 +428,20 @@ def test_min_samples_leaf(fake_data, val):
     X, y = fake_data
     tree = SurvivalTree(min_samples_leaf=val)
 
-    with pytest.raises(ValueError,
-                       match=r"min_samples_leaf must be at least 1 "
-                             r"or in \(0, 0\.5\], got"):
+    msg = r"The 'min_samples_leaf' parameter of SurvivalTree must be an int in the range \[1, inf\) or " \
+          r"a float in the range \(0\.0, 0\.5\]\. Got {!r} instead\.".format(val)
+    with pytest.raises(ValueError, match=msg):
         tree.fit(X, y)
 
 
-@pytest.mark.parametrize("val", [0, 0.0, 1, -1, -1e-6, -1512, 10.0, 1.000001, np.nan, -np.infty, np.infty])
+@pytest.mark.parametrize("val", [0, 0.0, 1, 1.0, -1, -1e-6, -1512, 10.0, 1.000001, np.nan, -np.infty, np.infty])
 def test_min_samples_split(fake_data, val):
     X, y = fake_data
     tree = SurvivalTree(min_samples_split=val)
 
-    with pytest.raises(ValueError,
-                       match="min_samples_split must be an integer "
-                             r"greater than 1 or a float in \(0\.0, 1\.0\]; "
-                             "got "):
+    msg = r"The 'min_samples_split' parameter of SurvivalTree must be an int in the range \[2, inf\) or " \
+          r"a float in the range \(0\.0, 1\.0\)\. Got {!r} instead\.".format(val)
+    with pytest.raises(ValueError, match=msg):
         tree.fit(X, y)
 
 
@@ -450,23 +450,25 @@ def test_min_weight_fraction_leaf(fake_data, val):
     X, y = fake_data
     tree = SurvivalTree(min_weight_fraction_leaf=val)
 
-    with pytest.raises(ValueError,
-                       match=r"min_weight_fraction_leaf must in \[0, 0\.5\]"):
+    msg = r"The 'min_weight_fraction_leaf' parameter of SurvivalTree must be " \
+          r"a float in the range \[0\.0, 0\.5\]\. Got {!r} instead\.".format(val)
+    with pytest.raises(ValueError, match=msg):
         tree.fit(X, y)
 
 
-@pytest.mark.parametrize("val", ["", "None", "sqrt_", "log10", "car"])
+@pytest.mark.parametrize("val", [0, 0.0, "", "None", "sqrt_", "log10", "car"])
 def test_max_features_invalid(fake_data, val):
     X, y = fake_data
     tree = SurvivalTree(max_features=val)
 
-    with pytest.raises(ValueError,
-                       match='Invalid value for max_features. Allowed string '
-                             'values are "auto", "sqrt" or "log2".'):
+    msg = r"The 'max_features' parameter of SurvivalTree must be " \
+          r"an int in the range \[1, inf\), a float in the range \(0\.0, 1\.0\], " \
+          r"a str among {.+} or None\."
+    with pytest.raises(ValueError, match=msg):
         tree.fit(X, y)
 
 
-@pytest.mark.parametrize("val", [0, 0.0, 12, 13, 100, 865411])
+@pytest.mark.parametrize("val", [12, 13, 100, 865411])
 def test_max_features_too_large(fake_data, val):
     X, y = fake_data
     tree = SurvivalTree(max_features=val)
@@ -481,8 +483,8 @@ def test_max_leaf_nodes_no_int(fake_data, val):
     X, y = fake_data
     tree = SurvivalTree(max_leaf_nodes=val)
 
-    with pytest.raises(ValueError,
-                       match="max_leaf_nodes must be integral number but was "):
+    msg = r"The 'max_leaf_nodes' parameter of SurvivalTree must be an int in the range \[2, inf\) or None\."
+    with pytest.raises(ValueError, match=msg):
         tree.fit(X, y)
 
 
@@ -491,29 +493,11 @@ def test_max_leaf_nodes_too_small(fake_data, val):
     X, y = fake_data
     tree = SurvivalTree(max_leaf_nodes=val)
 
-    with pytest.raises(ValueError,
-                       match="max_leaf_nodes {} must be either None "
-                             "or larger than 1".format(val)):
+    msg = r"The 'max_leaf_nodes' parameter of SurvivalTree must be an int in the range \[2, inf\) or None\."
+    with pytest.raises(ValueError, match=msg):
         tree.fit(X, y)
 
 
-@pytest.mark.parametrize("val", [0, 1, None, "sort"])
-def test_X_idx_sorted(fake_data, val):
-    X, y = fake_data
-    tree = SurvivalTree()
-
-    if val == "sort":
-        X_idx_sorted = np.argsort(X, axis=0)
-    else:
-        X_idx_sorted = val
-
-    with pytest.warns(
-            FutureWarning,
-            match="The parameter 'X_idx_sorted' is deprecated and has no effect."
-    ):
-        tree.fit(X, y, X_idx_sorted=X_idx_sorted)
-
-
 def test_apply(veterans):
     X, y = veterans
     X = X.loc[:, "Karnofsky_score"].values[:, np.newaxis]