Expand source code
from copy import deepcopy
from typing import List

import numpy as np
from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.model_selection import cross_val_score

from imodels.tree.hierarchical_shrinkage import HSTreeRegressor, HSTreeClassifier
from imodels.util.tree import compute_tree_complexity
from imodels.util.introspection import RuleInspectionMixin
from imodels.util.arguments import explicit_get_params, explicit_set_params


class DecisionTreeCCPClassifier(RuleInspectionMixin, ClassifierMixin, BaseEstimator):
    def __init__(self, estimator_: BaseEstimator, desired_complexity: int = 1, complexity_measure='max_rules', *args,
                 **kwargs):
        self.desired_complexity = desired_complexity
        self.estimator_ = estimator_
        self.complexity_measure = complexity_measure

    #: __init__ takes *args/**kwargs, which sklearn's introspection rejects,
    #: so the parameters are spelled out here instead
    _PARAM_NAMES = ("estimator_", "desired_complexity", "complexity_measure")

    def get_params(self, deep=True):
        return explicit_get_params(self, self._PARAM_NAMES, deep=deep)

    def set_params(self, **params):
        return explicit_set_params(self, self._PARAM_NAMES, **params)

    def _copy_fitted_attributes(self):
        """Mirror the wrapped estimator's fitted sklearn attributes onto self."""
        for attr in ("classes_", "n_features_in_", "feature_names_in_"):
            if hasattr(self.estimator_, attr):
                setattr(self, attr, getattr(self.estimator_, attr))

    def _get_alpha(self, X, y, sample_weight=None, *args, **kwargs):
        path = self.estimator_.cost_complexity_pruning_path(
            X, y, sample_weight=sample_weight)
        ccp_alphas = path.ccp_alphas
        low = 0
        high = len(ccp_alphas) - 1
        cur = 0
        while low <= high:
            cur = (high + low) // 2
            est_params = self.estimator_.get_params()
            est_params['ccp_alpha'] = ccp_alphas[cur]
            copied_estimator = deepcopy(self.estimator_).set_params(**est_params)
            copied_estimator.fit(X, y, sample_weight=sample_weight)
            if self._get_complexity(copied_estimator, self.complexity_measure) < self.desired_complexity:
                high = cur - 1
            elif self._get_complexity(copied_estimator, self.complexity_measure) > self.desired_complexity:
                low = cur + 1
            else:
                break
        self.alpha = ccp_alphas[cur]

        # for alpha in ccp_alphas:
        #    est_params = self.estimator_.get_params()
        #    est_params['ccp_alpha'] = alpha
        #    copied_estimator =  deepcopy(self.estimator_).set_params(**est_params)
        #    copied_estimator.fit(X, y)
        #    complexities[alpha] = self._get_complexity(copied_estimator,self.complexity_measure)
        # closest_alpha, closest_leaves = min(complexities.items(), key=lambda x: abs(self.desired_complexity - x[1]))
        # self.alpha = closest_alpha

    def fit(self, X, y, sample_weight=None, *args, **kwargs):
        # fit a copy, so the estimator passed to __init__ is left untouched and
        # two models sharing one estimator cannot interfere with each other
        self.estimator_ = deepcopy(self.estimator_)
        params_for_fitting = self.estimator_.get_params()
        self._get_alpha(X, y, sample_weight, *args, **kwargs)
        params_for_fitting['ccp_alpha'] = self.alpha
        self.estimator_.set_params(**params_for_fitting)
        self.estimator_.fit(X, y, *args, sample_weight=sample_weight, **kwargs)
        self._copy_fitted_attributes()
        return self

    @property
    def feature_importances_(self):
        """Mean decrease in impurity of the pruned tree, as in sklearn."""
        return self.estimator_.feature_importances_
    def _get_complexity(self, BaseEstimator, complexity_measure):
        return compute_tree_complexity(BaseEstimator.tree_, complexity_measure)

    def predict_proba(self, X, *args, **kwargs):
        if hasattr(self.estimator_, 'predict_proba'):
            return self.estimator_.predict_proba(X, *args, **kwargs)
        else:
            return NotImplemented

    def predict(self, X, *args, **kwargs):
        return self.estimator_.predict(X, *args, **kwargs)

    def score(self, X, y, *args, **kwargs):
        if hasattr(self.estimator_, 'score'):
            return self.estimator_.score(X, y, *args, **kwargs)
        else:
            return NotImplemented


class DecisionTreeCCPRegressor(RuleInspectionMixin, BaseEstimator):

    def __init__(self, estimator_: BaseEstimator, desired_complexity: int = 1, complexity_measure='max_rules', *args,
                 **kwargs):
        self.desired_complexity = desired_complexity
        self.estimator_ = estimator_
        self.alpha = 0.0
        self.complexity_measure = complexity_measure

    #: __init__ takes *args/**kwargs, which sklearn's introspection rejects,
    #: so the parameters are spelled out here instead
    _PARAM_NAMES = ("estimator_", "desired_complexity", "complexity_measure")

    def get_params(self, deep=True):
        return explicit_get_params(self, self._PARAM_NAMES, deep=deep)

    def set_params(self, **params):
        return explicit_set_params(self, self._PARAM_NAMES, **params)

    def _copy_fitted_attributes(self):
        """Mirror the wrapped estimator's fitted sklearn attributes onto self."""
        for attr in ("n_features_in_", "feature_names_in_"):
            if hasattr(self.estimator_, attr):
                setattr(self, attr, getattr(self.estimator_, attr))

    def _get_alpha(self, X, y, sample_weight=None):
        path = self.estimator_.cost_complexity_pruning_path(
            X, y, sample_weight=sample_weight)
        ccp_alphas = path.ccp_alphas
        low = 0
        high = len(ccp_alphas) - 1
        cur = 0
        while low <= high:
            cur = (high + low) // 2
            est_params = self.estimator_.get_params()
            est_params['ccp_alpha'] = ccp_alphas[cur]
            copied_estimator = deepcopy(self.estimator_).set_params(**est_params)
            copied_estimator.fit(X, y, sample_weight=sample_weight)
            if self._get_complexity(copied_estimator, self.complexity_measure) < self.desired_complexity:
                high = cur - 1
            elif self._get_complexity(copied_estimator, self.complexity_measure) > self.desired_complexity:
                low = cur + 1
            else:
                break
        self.alpha = ccp_alphas[cur]

    #  path = self.estimator_.cost_complexity_pruning_path(X,y)
    #  ccp_alphas, impurities = path.ccp_alphas, path.impurities
    #  complexities = {}
    #  for alpha in ccp_alphas:
    #      est_params = self.estimator_.get_params()
    #      est_params['ccp_alpha'] = alpha
    #      copied_estimator =  deepcopy(self.estimator_).set_params(**est_params)
    #      copied_estimator.fit(X, y)
    #      complexities[alpha] = self._get_complexity(copied_estimator,self.complexity_measure)
    #  closest_alpha, closest_leaves = min(complexities.items(), key=lambda x: abs(self.desired_complexity - x[1]))
    #  self.alpha = closest_alpha

    def fit(self, X, y, sample_weight=None):
        # fit a copy (see DecisionTreeCCPClassifier.fit)
        self.estimator_ = deepcopy(self.estimator_)
        params_for_fitting = self.estimator_.get_params()
        self._get_alpha(X, y, sample_weight)
        params_for_fitting['ccp_alpha'] = self.alpha
        self.estimator_.set_params(**params_for_fitting)
        self.estimator_.fit(X, y, sample_weight=sample_weight)
        self._copy_fitted_attributes()
        return self

    @property
    def feature_importances_(self):
        """Mean decrease in impurity of the pruned tree, as in sklearn."""
        return self.estimator_.feature_importances_
    def _get_complexity(self, BaseEstimator, complexity_measure):
        return compute_tree_complexity(BaseEstimator.tree_, self.complexity_measure)

    def predict(self, X, *args, **kwargs):
        return self.estimator_.predict(X, *args, **kwargs)

    def score(self, X, y, *args, **kwargs):
        if hasattr(self.estimator_, 'score'):
            return self.estimator_.score(X, y, *args, **kwargs)
        else:
            return NotImplemented


class HSDecisionTreeCCPRegressorCV(HSTreeRegressor):
    def __init__(self, estimator_: BaseEstimator, reg_param_list: List[float] = [0.1, 1, 10, 50, 100, 500],
                 desired_complexity: int = 1, cv: int = 3, scoring=None, *args, **kwargs):
        super().__init__(estimator_=estimator_, reg_param=None)
        self.reg_param_list = np.array(reg_param_list)
        self.cv = cv
        self.scoring = scoring
        self.desired_complexity = desired_complexity

    def fit(self, X, y, sample_weight=None, *args, **kwargs):
        m = DecisionTreeCCPRegressor(self.estimator_, desired_complexity=self.desired_complexity)
        m.fit(X, y, sample_weight, *args, **kwargs)
        self.scores_ = []
        for reg_param in self.reg_param_list:
            est = HSTreeRegressor(deepcopy(m.estimator_), reg_param)
            cv_scores = cross_val_score(est, X, y, cv=self.cv, scoring=self.scoring)
            self.scores_.append(np.mean(cv_scores))
        self.reg_param = self.reg_param_list[np.argmax(self.scores_)]
        return super().fit(X=X, y=y)


class HSDecisionTreeCCPClassifierCV(HSTreeClassifier):
    def __init__(self, estimator_: BaseEstimator, reg_param_list: List[float] = [0.1, 1, 10, 50, 100, 500],
                 desired_complexity: int = 1, cv: int = 3, scoring=None, *args, **kwargs):
        super().__init__(estimator_=estimator_, reg_param=None)
        self.reg_param_list = np.array(reg_param_list)
        self.cv = cv
        self.scoring = scoring
        self.desired_complexity = desired_complexity

    def fit(self, X, y, sample_weight=None, *args, **kwargs):
        m = DecisionTreeCCPClassifier(self.estimator_, desired_complexity=self.desired_complexity)
        m.fit(X, y, sample_weight, *args, **kwargs)
        self.scores_ = []
        for reg_param in self.reg_param_list:
            est = HSTreeClassifier(deepcopy(m.estimator_), reg_param)
            cv_scores = cross_val_score(est, X, y, cv=self.cv, scoring=self.scoring)
            self.scores_.append(np.mean(cv_scores))
        self.reg_param = self.reg_param_list[np.argmax(self.scores_)]
        return super().fit(X=X, y=y)

Classes

class DecisionTreeCCPClassifier (estimator_: sklearn.base.BaseEstimator, desired_complexity: int = 1, complexity_measure='max_rules', *args, **kwargs)

Both of the above, for tree-based models that support each.

Expand source code
class DecisionTreeCCPClassifier(RuleInspectionMixin, ClassifierMixin, BaseEstimator):
    def __init__(self, estimator_: BaseEstimator, desired_complexity: int = 1, complexity_measure='max_rules', *args,
                 **kwargs):
        self.desired_complexity = desired_complexity
        self.estimator_ = estimator_
        self.complexity_measure = complexity_measure

    #: __init__ takes *args/**kwargs, which sklearn's introspection rejects,
    #: so the parameters are spelled out here instead
    _PARAM_NAMES = ("estimator_", "desired_complexity", "complexity_measure")

    def get_params(self, deep=True):
        return explicit_get_params(self, self._PARAM_NAMES, deep=deep)

    def set_params(self, **params):
        return explicit_set_params(self, self._PARAM_NAMES, **params)

    def _copy_fitted_attributes(self):
        """Mirror the wrapped estimator's fitted sklearn attributes onto self."""
        for attr in ("classes_", "n_features_in_", "feature_names_in_"):
            if hasattr(self.estimator_, attr):
                setattr(self, attr, getattr(self.estimator_, attr))

    def _get_alpha(self, X, y, sample_weight=None, *args, **kwargs):
        path = self.estimator_.cost_complexity_pruning_path(
            X, y, sample_weight=sample_weight)
        ccp_alphas = path.ccp_alphas
        low = 0
        high = len(ccp_alphas) - 1
        cur = 0
        while low <= high:
            cur = (high + low) // 2
            est_params = self.estimator_.get_params()
            est_params['ccp_alpha'] = ccp_alphas[cur]
            copied_estimator = deepcopy(self.estimator_).set_params(**est_params)
            copied_estimator.fit(X, y, sample_weight=sample_weight)
            if self._get_complexity(copied_estimator, self.complexity_measure) < self.desired_complexity:
                high = cur - 1
            elif self._get_complexity(copied_estimator, self.complexity_measure) > self.desired_complexity:
                low = cur + 1
            else:
                break
        self.alpha = ccp_alphas[cur]

        # for alpha in ccp_alphas:
        #    est_params = self.estimator_.get_params()
        #    est_params['ccp_alpha'] = alpha
        #    copied_estimator =  deepcopy(self.estimator_).set_params(**est_params)
        #    copied_estimator.fit(X, y)
        #    complexities[alpha] = self._get_complexity(copied_estimator,self.complexity_measure)
        # closest_alpha, closest_leaves = min(complexities.items(), key=lambda x: abs(self.desired_complexity - x[1]))
        # self.alpha = closest_alpha

    def fit(self, X, y, sample_weight=None, *args, **kwargs):
        # fit a copy, so the estimator passed to __init__ is left untouched and
        # two models sharing one estimator cannot interfere with each other
        self.estimator_ = deepcopy(self.estimator_)
        params_for_fitting = self.estimator_.get_params()
        self._get_alpha(X, y, sample_weight, *args, **kwargs)
        params_for_fitting['ccp_alpha'] = self.alpha
        self.estimator_.set_params(**params_for_fitting)
        self.estimator_.fit(X, y, *args, sample_weight=sample_weight, **kwargs)
        self._copy_fitted_attributes()
        return self

    @property
    def feature_importances_(self):
        """Mean decrease in impurity of the pruned tree, as in sklearn."""
        return self.estimator_.feature_importances_
    def _get_complexity(self, BaseEstimator, complexity_measure):
        return compute_tree_complexity(BaseEstimator.tree_, complexity_measure)

    def predict_proba(self, X, *args, **kwargs):
        if hasattr(self.estimator_, 'predict_proba'):
            return self.estimator_.predict_proba(X, *args, **kwargs)
        else:
            return NotImplemented

    def predict(self, X, *args, **kwargs):
        return self.estimator_.predict(X, *args, **kwargs)

    def score(self, X, y, *args, **kwargs):
        if hasattr(self.estimator_, 'score'):
            return self.estimator_.score(X, y, *args, **kwargs)
        else:
            return NotImplemented

Ancestors

  • RuleInspectionMixin
  • RulesMixin
  • LeavesMixin
  • sklearn.base.ClassifierMixin
  • sklearn.base.BaseEstimator
  • sklearn.utils._estimator_html_repr._HTMLDocumentationLinkMixin
  • sklearn.utils._metadata_requests._MetadataRequester

Instance variables

var feature_importances_

Mean decrease in impurity of the pruned tree, as in sklearn.

Expand source code
@property
def feature_importances_(self):
    """Mean decrease in impurity of the pruned tree, as in sklearn."""
    return self.estimator_.feature_importances_

Methods

def fit(self, X, y, sample_weight=None, *args, **kwargs)
Expand source code
def fit(self, X, y, sample_weight=None, *args, **kwargs):
    # fit a copy, so the estimator passed to __init__ is left untouched and
    # two models sharing one estimator cannot interfere with each other
    self.estimator_ = deepcopy(self.estimator_)
    params_for_fitting = self.estimator_.get_params()
    self._get_alpha(X, y, sample_weight, *args, **kwargs)
    params_for_fitting['ccp_alpha'] = self.alpha
    self.estimator_.set_params(**params_for_fitting)
    self.estimator_.fit(X, y, *args, sample_weight=sample_weight, **kwargs)
    self._copy_fitted_attributes()
    return self
def get_params(self, deep=True)

Get parameters for this estimator.

Parameters

deep : bool, default=True
If True, will return the parameters for this estimator and contained subobjects that are estimators.

Returns

params : dict
Parameter names mapped to their values.
Expand source code
def get_params(self, deep=True):
    return explicit_get_params(self, self._PARAM_NAMES, deep=deep)
def predict(self, X, *args, **kwargs)
Expand source code
def predict(self, X, *args, **kwargs):
    return self.estimator_.predict(X, *args, **kwargs)
def predict_proba(self, X, *args, **kwargs)
Expand source code
def predict_proba(self, X, *args, **kwargs):
    if hasattr(self.estimator_, 'predict_proba'):
        return self.estimator_.predict_proba(X, *args, **kwargs)
    else:
        return NotImplemented
def score(self, X, y, *args, **kwargs)

Return the mean accuracy on the given test data and labels.

In multi-label classification, this is the subset accuracy which is a harsh metric since you require for each sample that each label set be correctly predicted.

Parameters

X : array-like of shape (n_samples, n_features)
Test samples.
y : array-like of shape (n_samples,) or (n_samples, n_outputs)
True labels for X.
sample_weight : array-like of shape (n_samples,), default=None
Sample weights.

Returns

score : float
Mean accuracy of self.predict(X) w.r.t. y.
Expand source code
def score(self, X, y, *args, **kwargs):
    if hasattr(self.estimator_, 'score'):
        return self.estimator_.score(X, y, *args, **kwargs)
    else:
        return NotImplemented
def set_fit_request(self: DecisionTreeCCPClassifier, *, sample_weight: bool | str | None = '$UNCHANGED$') ‑> DecisionTreeCCPClassifier

Request metadata passed to the fit method.

Note that this method is only relevant if enable_metadata_routing=True (see :func:sklearn.set_config). Please see :ref:User Guide <metadata_routing> on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to fit if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to fit.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version: 1.3

Note

This method is only relevant if this estimator is used as a sub-estimator of a meta-estimator, e.g. used inside a :class:~sklearn.pipeline.Pipeline. Otherwise it has no effect.

Parameters

sample_weight : str, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED
Metadata routing for sample_weight parameter in fit.

Returns

self : object
The updated object.
Expand source code
def func(*args, **kw):
    """Updates the request for provided parameters

    This docstring is overwritten below.
    See REQUESTER_DOC for expected functionality
    """
    if not _routing_enabled():
        raise RuntimeError(
            "This method is only available when metadata routing is enabled."
            " You can enable it using"
            " sklearn.set_config(enable_metadata_routing=True)."
        )

    if self.validate_keys and (set(kw) - set(self.keys)):
        raise TypeError(
            f"Unexpected args: {set(kw) - set(self.keys)} in {self.name}. "
            f"Accepted arguments are: {set(self.keys)}"
        )

    # This makes it possible to use the decorated method as an unbound method,
    # for instance when monkeypatching.
    # https://github.com/scikit-learn/scikit-learn/issues/28632
    if instance is None:
        _instance = args[0]
        args = args[1:]
    else:
        _instance = instance

    # Replicating python's behavior when positional args are given other than
    # `self`, and `self` is only allowed if this method is unbound.
    if args:
        raise TypeError(
            f"set_{self.name}_request() takes 0 positional argument but"
            f" {len(args)} were given"
        )

    requests = _instance._get_metadata_request()
    method_metadata_request = getattr(requests, self.name)

    for prop, alias in kw.items():
        if alias is not UNCHANGED:
            method_metadata_request.add_request(param=prop, alias=alias)
    _instance._metadata_request = requests

    return _instance
def set_params(self, **params)

Set the parameters of this estimator.

The method works on simple estimators as well as on nested objects (such as :class:~sklearn.pipeline.Pipeline). The latter have parameters of the form <component>__<parameter> so that it's possible to update each component of a nested object.

Parameters

**params : dict
Estimator parameters.

Returns

self : estimator instance
Estimator instance.
Expand source code
def set_params(self, **params):
    return explicit_set_params(self, self._PARAM_NAMES, **params)

Inherited members

class DecisionTreeCCPRegressor (estimator_: sklearn.base.BaseEstimator, desired_complexity: int = 1, complexity_measure='max_rules', *args, **kwargs)

Both of the above, for tree-based models that support each.

Expand source code
class DecisionTreeCCPRegressor(RuleInspectionMixin, BaseEstimator):

    def __init__(self, estimator_: BaseEstimator, desired_complexity: int = 1, complexity_measure='max_rules', *args,
                 **kwargs):
        self.desired_complexity = desired_complexity
        self.estimator_ = estimator_
        self.alpha = 0.0
        self.complexity_measure = complexity_measure

    #: __init__ takes *args/**kwargs, which sklearn's introspection rejects,
    #: so the parameters are spelled out here instead
    _PARAM_NAMES = ("estimator_", "desired_complexity", "complexity_measure")

    def get_params(self, deep=True):
        return explicit_get_params(self, self._PARAM_NAMES, deep=deep)

    def set_params(self, **params):
        return explicit_set_params(self, self._PARAM_NAMES, **params)

    def _copy_fitted_attributes(self):
        """Mirror the wrapped estimator's fitted sklearn attributes onto self."""
        for attr in ("n_features_in_", "feature_names_in_"):
            if hasattr(self.estimator_, attr):
                setattr(self, attr, getattr(self.estimator_, attr))

    def _get_alpha(self, X, y, sample_weight=None):
        path = self.estimator_.cost_complexity_pruning_path(
            X, y, sample_weight=sample_weight)
        ccp_alphas = path.ccp_alphas
        low = 0
        high = len(ccp_alphas) - 1
        cur = 0
        while low <= high:
            cur = (high + low) // 2
            est_params = self.estimator_.get_params()
            est_params['ccp_alpha'] = ccp_alphas[cur]
            copied_estimator = deepcopy(self.estimator_).set_params(**est_params)
            copied_estimator.fit(X, y, sample_weight=sample_weight)
            if self._get_complexity(copied_estimator, self.complexity_measure) < self.desired_complexity:
                high = cur - 1
            elif self._get_complexity(copied_estimator, self.complexity_measure) > self.desired_complexity:
                low = cur + 1
            else:
                break
        self.alpha = ccp_alphas[cur]

    #  path = self.estimator_.cost_complexity_pruning_path(X,y)
    #  ccp_alphas, impurities = path.ccp_alphas, path.impurities
    #  complexities = {}
    #  for alpha in ccp_alphas:
    #      est_params = self.estimator_.get_params()
    #      est_params['ccp_alpha'] = alpha
    #      copied_estimator =  deepcopy(self.estimator_).set_params(**est_params)
    #      copied_estimator.fit(X, y)
    #      complexities[alpha] = self._get_complexity(copied_estimator,self.complexity_measure)
    #  closest_alpha, closest_leaves = min(complexities.items(), key=lambda x: abs(self.desired_complexity - x[1]))
    #  self.alpha = closest_alpha

    def fit(self, X, y, sample_weight=None):
        # fit a copy (see DecisionTreeCCPClassifier.fit)
        self.estimator_ = deepcopy(self.estimator_)
        params_for_fitting = self.estimator_.get_params()
        self._get_alpha(X, y, sample_weight)
        params_for_fitting['ccp_alpha'] = self.alpha
        self.estimator_.set_params(**params_for_fitting)
        self.estimator_.fit(X, y, sample_weight=sample_weight)
        self._copy_fitted_attributes()
        return self

    @property
    def feature_importances_(self):
        """Mean decrease in impurity of the pruned tree, as in sklearn."""
        return self.estimator_.feature_importances_
    def _get_complexity(self, BaseEstimator, complexity_measure):
        return compute_tree_complexity(BaseEstimator.tree_, self.complexity_measure)

    def predict(self, X, *args, **kwargs):
        return self.estimator_.predict(X, *args, **kwargs)

    def score(self, X, y, *args, **kwargs):
        if hasattr(self.estimator_, 'score'):
            return self.estimator_.score(X, y, *args, **kwargs)
        else:
            return NotImplemented

Ancestors

Instance variables

var feature_importances_

Mean decrease in impurity of the pruned tree, as in sklearn.

Expand source code
@property
def feature_importances_(self):
    """Mean decrease in impurity of the pruned tree, as in sklearn."""
    return self.estimator_.feature_importances_

Methods

def fit(self, X, y, sample_weight=None)
Expand source code
def fit(self, X, y, sample_weight=None):
    # fit a copy (see DecisionTreeCCPClassifier.fit)
    self.estimator_ = deepcopy(self.estimator_)
    params_for_fitting = self.estimator_.get_params()
    self._get_alpha(X, y, sample_weight)
    params_for_fitting['ccp_alpha'] = self.alpha
    self.estimator_.set_params(**params_for_fitting)
    self.estimator_.fit(X, y, sample_weight=sample_weight)
    self._copy_fitted_attributes()
    return self
def get_params(self, deep=True)

Get parameters for this estimator.

Parameters

deep : bool, default=True
If True, will return the parameters for this estimator and contained subobjects that are estimators.

Returns

params : dict
Parameter names mapped to their values.
Expand source code
def get_params(self, deep=True):
    return explicit_get_params(self, self._PARAM_NAMES, deep=deep)
def predict(self, X, *args, **kwargs)
Expand source code
def predict(self, X, *args, **kwargs):
    return self.estimator_.predict(X, *args, **kwargs)
def score(self, X, y, *args, **kwargs)
Expand source code
def score(self, X, y, *args, **kwargs):
    if hasattr(self.estimator_, 'score'):
        return self.estimator_.score(X, y, *args, **kwargs)
    else:
        return NotImplemented
def set_fit_request(self: DecisionTreeCCPRegressor, *, sample_weight: bool | str | None = '$UNCHANGED$') ‑> DecisionTreeCCPRegressor

Request metadata passed to the fit method.

Note that this method is only relevant if enable_metadata_routing=True (see :func:sklearn.set_config). Please see :ref:User Guide <metadata_routing> on how the routing mechanism works.

The options for each parameter are:

  • True: metadata is requested, and passed to fit if provided. The request is ignored if metadata is not provided.

  • False: metadata is not requested and the meta-estimator will not pass it to fit.

  • None: metadata is not requested, and the meta-estimator will raise an error if the user provides it.

  • str: metadata should be passed to the meta-estimator with this given alias instead of the original name.

The default (sklearn.utils.metadata_routing.UNCHANGED) retains the existing request. This allows you to change the request for some parameters and not others.

Added in version: 1.3

Note

This method is only relevant if this estimator is used as a sub-estimator of a meta-estimator, e.g. used inside a :class:~sklearn.pipeline.Pipeline. Otherwise it has no effect.

Parameters

sample_weight : str, True, False, or None, default=sklearn.utils.metadata_routing.UNCHANGED
Metadata routing for sample_weight parameter in fit.

Returns

self : object
The updated object.
Expand source code
def func(*args, **kw):
    """Updates the request for provided parameters

    This docstring is overwritten below.
    See REQUESTER_DOC for expected functionality
    """
    if not _routing_enabled():
        raise RuntimeError(
            "This method is only available when metadata routing is enabled."
            " You can enable it using"
            " sklearn.set_config(enable_metadata_routing=True)."
        )

    if self.validate_keys and (set(kw) - set(self.keys)):
        raise TypeError(
            f"Unexpected args: {set(kw) - set(self.keys)} in {self.name}. "
            f"Accepted arguments are: {set(self.keys)}"
        )

    # This makes it possible to use the decorated method as an unbound method,
    # for instance when monkeypatching.
    # https://github.com/scikit-learn/scikit-learn/issues/28632
    if instance is None:
        _instance = args[0]
        args = args[1:]
    else:
        _instance = instance

    # Replicating python's behavior when positional args are given other than
    # `self`, and `self` is only allowed if this method is unbound.
    if args:
        raise TypeError(
            f"set_{self.name}_request() takes 0 positional argument but"
            f" {len(args)} were given"
        )

    requests = _instance._get_metadata_request()
    method_metadata_request = getattr(requests, self.name)

    for prop, alias in kw.items():
        if alias is not UNCHANGED:
            method_metadata_request.add_request(param=prop, alias=alias)
    _instance._metadata_request = requests

    return _instance
def set_params(self, **params)

Set the parameters of this estimator.

The method works on simple estimators as well as on nested objects (such as :class:~sklearn.pipeline.Pipeline). The latter have parameters of the form <component>__<parameter> so that it's possible to update each component of a nested object.

Parameters

**params : dict
Estimator parameters.

Returns

self : estimator instance
Estimator instance.
Expand source code
def set_params(self, **params):
    return explicit_set_params(self, self._PARAM_NAMES, **params)

Inherited members

class HSDecisionTreeCCPClassifierCV (estimator_: sklearn.base.BaseEstimator, reg_param_list: List[float] = [0.1, 1, 10, 50, 100, 500], desired_complexity: int = 1, cv: int = 3, scoring=None, *args, **kwargs)

Hierarchical shrinkage: post-hoc regularization for any decision tree or tree ensemble.

SHAP values

shap.TreeExplainer dispatches on the model class, so it does not recognize this wrapper. Pass the shrunk estimator it wraps:

import shap

model = HSTreeClassifier(...).fit(X, y)
explainer = shap.TreeExplainer(model.estimator_)   # not model itself
shap_values = explainer.shap_values(X)

Shrinkage rewrites the node values of that tree in place, so the explainer sees the shrunk model: the SHAP values differ from the unshrunk tree's and sum, with the expected value, to model.predict_proba(X). This reproduces the SHAP summary plots in the paper.

Expand source code
class HSDecisionTreeCCPClassifierCV(HSTreeClassifier):
    def __init__(self, estimator_: BaseEstimator, reg_param_list: List[float] = [0.1, 1, 10, 50, 100, 500],
                 desired_complexity: int = 1, cv: int = 3, scoring=None, *args, **kwargs):
        super().__init__(estimator_=estimator_, reg_param=None)
        self.reg_param_list = np.array(reg_param_list)
        self.cv = cv
        self.scoring = scoring
        self.desired_complexity = desired_complexity

    def fit(self, X, y, sample_weight=None, *args, **kwargs):
        m = DecisionTreeCCPClassifier(self.estimator_, desired_complexity=self.desired_complexity)
        m.fit(X, y, sample_weight, *args, **kwargs)
        self.scores_ = []
        for reg_param in self.reg_param_list:
            est = HSTreeClassifier(deepcopy(m.estimator_), reg_param)
            cv_scores = cross_val_score(est, X, y, cv=self.cv, scoring=self.scoring)
            self.scores_.append(np.mean(cv_scores))
        self.reg_param = self.reg_param_list[np.argmax(self.scores_)]
        return super().fit(X=X, y=y)

Ancestors

Methods

def fit(self, X, y, sample_weight=None, *args, **kwargs)
Expand source code
def fit(self, X, y, sample_weight=None, *args, **kwargs):
    m = DecisionTreeCCPClassifier(self.estimator_, desired_complexity=self.desired_complexity)
    m.fit(X, y, sample_weight, *args, **kwargs)
    self.scores_ = []
    for reg_param in self.reg_param_list:
        est = HSTreeClassifier(deepcopy(m.estimator_), reg_param)
        cv_scores = cross_val_score(est, X, y, cv=self.cv, scoring=self.scoring)
        self.scores_.append(np.mean(cv_scores))
    self.reg_param = self.reg_param_list[np.argmax(self.scores_)]
    return super().fit(X=X, y=y)

Inherited members

class HSDecisionTreeCCPRegressorCV (estimator_: sklearn.base.BaseEstimator, reg_param_list: List[float] = [0.1, 1, 10, 50, 100, 500], desired_complexity: int = 1, cv: int = 3, scoring=None, *args, **kwargs)

Hierarchical shrinkage: post-hoc regularization for any decision tree or tree ensemble.

SHAP values

shap.TreeExplainer dispatches on the model class, so it does not recognize this wrapper. Pass the shrunk estimator it wraps:

import shap

model = HSTreeRegressor(...).fit(X, y)
explainer = shap.TreeExplainer(model.estimator_)   # not model itself
shap_values = explainer.shap_values(X)

Shrinkage rewrites the node values of that tree in place, so the explainer sees the shrunk model: the SHAP values differ from the unshrunk tree's and sum, with the expected value, to model.predict(X). This reproduces the SHAP summary plots in the paper.

Expand source code
class HSDecisionTreeCCPRegressorCV(HSTreeRegressor):
    def __init__(self, estimator_: BaseEstimator, reg_param_list: List[float] = [0.1, 1, 10, 50, 100, 500],
                 desired_complexity: int = 1, cv: int = 3, scoring=None, *args, **kwargs):
        super().__init__(estimator_=estimator_, reg_param=None)
        self.reg_param_list = np.array(reg_param_list)
        self.cv = cv
        self.scoring = scoring
        self.desired_complexity = desired_complexity

    def fit(self, X, y, sample_weight=None, *args, **kwargs):
        m = DecisionTreeCCPRegressor(self.estimator_, desired_complexity=self.desired_complexity)
        m.fit(X, y, sample_weight, *args, **kwargs)
        self.scores_ = []
        for reg_param in self.reg_param_list:
            est = HSTreeRegressor(deepcopy(m.estimator_), reg_param)
            cv_scores = cross_val_score(est, X, y, cv=self.cv, scoring=self.scoring)
            self.scores_.append(np.mean(cv_scores))
        self.reg_param = self.reg_param_list[np.argmax(self.scores_)]
        return super().fit(X=X, y=y)

Ancestors

Methods

def fit(self, X, y, sample_weight=None, *args, **kwargs)
Expand source code
def fit(self, X, y, sample_weight=None, *args, **kwargs):
    m = DecisionTreeCCPRegressor(self.estimator_, desired_complexity=self.desired_complexity)
    m.fit(X, y, sample_weight, *args, **kwargs)
    self.scores_ = []
    for reg_param in self.reg_param_list:
        est = HSTreeRegressor(deepcopy(m.estimator_), reg_param)
        cv_scores = cross_val_score(est, X, y, cv=self.cv, scoring=self.scoring)
        self.scores_.append(np.mean(cv_scores))
    self.reg_param = self.reg_param_list[np.argmax(self.scores_)]
    return super().fit(X=X, y=y)

Inherited members