Expand source code
from .mdlp import BRLDiscretizer
from .simple import SimpleDiscretizer
from .discretizer import BasicDiscretizer, ExtraBasicDiscretizer, RFDiscretizer
# re-exported for callers; listed so the intent is explicit
__all__ = [
"BRLDiscretizer", "BasicDiscretizer", "ExtraBasicDiscretizer",
"RFDiscretizer", "SimpleDiscretizer",
]
Sub-modules
imodels.discretization.discretizerimodels.discretization.mdlp-
Discretization MDLP Python implementation of Fayyad and Irani's MDLP criterion discretization algorithm …
imodels.discretization.simple
Classes
class BRLDiscretizer (feature_labels, verbose=False)-
Expand source code
class BRLDiscretizer: def __init__(self, feature_labels, verbose=False): self.feature_labels_original = feature_labels self.verbose = verbose def fit(self, X, y, undiscretized_features=[]): # check which features are numeric (to be discretized) self.discretized_features = [] # _encode_strings indexes positionally, so a DataFrame has to be # unwrapped here the same way transform does if isinstance(X, (pd.DataFrame, pd.Series)): X = X.values X_str_disc = self._encode_strings(X) for fi in range(X_str_disc.shape[1]): # if not string, has values other than 0 and 1, and not specified as undiscretized if ( isinstance(X_str_disc[0][fi], numbers.Number) and (not set(np.unique(X_str_disc[:, fi])).issubset({0, 1})) and (len(self.feature_labels) == 0 or len(undiscretized_features) == 0 or self.feature_labels[fi] not in undiscretized_features ) ): self.discretized_features.append(self.feature_labels[fi]) if len(self.discretized_features) > 0: if self.verbose: print( "Warning: non-categorical data found. Trying to discretize. (Please convert categorical values to " "strings, and/or specify the argument 'undiscretized_features', to avoid this.)") X_str_and_num_disc = self.discretize(X_str_disc, y) self.discretized_X = X_str_and_num_disc else: self.discretizer = None return self def fit_transform(self, X, y, undiscretized_features=[], return_onehot=True): return self.fit(X, y, undiscretized_features).transform( X, return_onehot=return_onehot) def discretize(self, X, y): '''Discretize the features specified in self.discretized_features ''' if self.verbose: print("Discretizing ", self.discretized_features, "...") D = pd.DataFrame(np.hstack((X, np.expand_dims(y, axis=1))), columns=list(self.feature_labels) + ["y"]) self.discretizer = MDLPDiscretizer(dataset=D, class_label="y", features=self.discretized_features) # object dtype: the columns are filled with bin-label strings, which # pandas refuses to write into the float frame np.zeros_like would give cat_data = pd.DataFrame(np.zeros(np.shape(X), dtype=object)) for i in range(len(self.feature_labels)): label = self.feature_labels[i] if label in self.discretized_features: new_column = label + " : " + self.discretizer._data[label].astype(str) cat_data.iloc[:, i] = new_column else: cat_data.iloc[:, i] = D[label] return np.array(cat_data).tolist() def _encode_strings(self, X): # handle string data X_str_disc = pd.DataFrame([]) for fi in range(X.shape[1]): if issubclass(type(X[0][fi]), str): new_columns = pd.get_dummies(X[:, fi]) new_columns.columns = [self.feature_labels_original[fi] + '_' + value for value in new_columns.columns] new_columns_colon_format = new_columns.apply(lambda s: s.name + ' : ' + s.astype(str)) X_str_disc = pd.concat([X_str_disc, new_columns_colon_format], axis=1) else: X_str_disc = pd.concat([X_str_disc, pd.Series(X[:, fi], name=self.feature_labels_original[fi])], axis=1) self.feature_labels = list(X_str_disc.columns) return X_str_disc.values def transform(self, X, return_onehot=True): if type(X) in [pd.DataFrame, pd.Series]: X = X.values if self.discretizer is None: return pd.DataFrame(X, columns=self.feature_labels_original) self.data = pd.DataFrame(self._encode_strings(X), columns=self.feature_labels) self._apply_cutpoints() D = np.array(self.data) # prepend feature labels Dl = np.copy(D).astype(str).tolist() for i in range(len(Dl)): for j in range(len(Dl[0])): Dl[i][j] = self.feature_labels[j] + " : " + Dl[i][j] if not return_onehot: return Dl else: return self.get_onehot_df(Dl) @property def onehot_df(self): return self.get_onehot_df(self.discretized_X) def get_onehot_df(self, discretized_X): '''Create readable one-hot encoded DataFrame from discretized features ''' data = list(discretized_X[:]) X_colname_removed = data.copy() replace_str_entries_func = lambda s: s.split(' : ')[1] if type(s) is str else s for i in range(len(data)): X_colname_removed[i] = list(map(replace_str_entries_func, X_colname_removed[i])) X_df_categorical = pd.DataFrame(X_colname_removed, columns=self.feature_labels) X_df_onehot = pd.get_dummies(X_df_categorical) return X_df_onehot @property def data(self): return self.discretizer._data @data.setter def data(self, value): self.discretizer._data = value def _apply_cutpoints(self): return self.discretizer._apply_cutpoints()Instance variables
var data-
Expand source code
@property def data(self): return self.discretizer._data var onehot_df-
Expand source code
@property def onehot_df(self): return self.get_onehot_df(self.discretized_X)
Methods
def discretize(self, X, y)-
Discretize the features specified in self.discretized_features
Expand source code
def discretize(self, X, y): '''Discretize the features specified in self.discretized_features ''' if self.verbose: print("Discretizing ", self.discretized_features, "...") D = pd.DataFrame(np.hstack((X, np.expand_dims(y, axis=1))), columns=list(self.feature_labels) + ["y"]) self.discretizer = MDLPDiscretizer(dataset=D, class_label="y", features=self.discretized_features) # object dtype: the columns are filled with bin-label strings, which # pandas refuses to write into the float frame np.zeros_like would give cat_data = pd.DataFrame(np.zeros(np.shape(X), dtype=object)) for i in range(len(self.feature_labels)): label = self.feature_labels[i] if label in self.discretized_features: new_column = label + " : " + self.discretizer._data[label].astype(str) cat_data.iloc[:, i] = new_column else: cat_data.iloc[:, i] = D[label] return np.array(cat_data).tolist() def fit(self, X, y, undiscretized_features=[])-
Expand source code
def fit(self, X, y, undiscretized_features=[]): # check which features are numeric (to be discretized) self.discretized_features = [] # _encode_strings indexes positionally, so a DataFrame has to be # unwrapped here the same way transform does if isinstance(X, (pd.DataFrame, pd.Series)): X = X.values X_str_disc = self._encode_strings(X) for fi in range(X_str_disc.shape[1]): # if not string, has values other than 0 and 1, and not specified as undiscretized if ( isinstance(X_str_disc[0][fi], numbers.Number) and (not set(np.unique(X_str_disc[:, fi])).issubset({0, 1})) and (len(self.feature_labels) == 0 or len(undiscretized_features) == 0 or self.feature_labels[fi] not in undiscretized_features ) ): self.discretized_features.append(self.feature_labels[fi]) if len(self.discretized_features) > 0: if self.verbose: print( "Warning: non-categorical data found. Trying to discretize. (Please convert categorical values to " "strings, and/or specify the argument 'undiscretized_features', to avoid this.)") X_str_and_num_disc = self.discretize(X_str_disc, y) self.discretized_X = X_str_and_num_disc else: self.discretizer = None return self def fit_transform(self, X, y, undiscretized_features=[], return_onehot=True)-
Expand source code
def fit_transform(self, X, y, undiscretized_features=[], return_onehot=True): return self.fit(X, y, undiscretized_features).transform( X, return_onehot=return_onehot) def get_onehot_df(self, discretized_X)-
Create readable one-hot encoded DataFrame from discretized features
Expand source code
def get_onehot_df(self, discretized_X): '''Create readable one-hot encoded DataFrame from discretized features ''' data = list(discretized_X[:]) X_colname_removed = data.copy() replace_str_entries_func = lambda s: s.split(' : ')[1] if type(s) is str else s for i in range(len(data)): X_colname_removed[i] = list(map(replace_str_entries_func, X_colname_removed[i])) X_df_categorical = pd.DataFrame(X_colname_removed, columns=self.feature_labels) X_df_onehot = pd.get_dummies(X_df_categorical) return X_df_onehot def transform(self, X, return_onehot=True)-
Expand source code
def transform(self, X, return_onehot=True): if type(X) in [pd.DataFrame, pd.Series]: X = X.values if self.discretizer is None: return pd.DataFrame(X, columns=self.feature_labels_original) self.data = pd.DataFrame(self._encode_strings(X), columns=self.feature_labels) self._apply_cutpoints() D = np.array(self.data) # prepend feature labels Dl = np.copy(D).astype(str).tolist() for i in range(len(Dl)): for j in range(len(Dl[0])): Dl[i][j] = self.feature_labels[j] + " : " + Dl[i][j] if not return_onehot: return Dl else: return self.get_onehot_df(Dl)
class BasicDiscretizer (n_bins=2, dcols=[], encode='onehot', strategy='quantile', onehot_drop='if_binary')-
Discretize numeric data into bins. Provides a wrapper around KBinsDiscretizer from sklearn
Params
n_bins : int or array-like of shape (len(dcols),), default=2 Number of bins to discretize each feature into.
dcols : list of strings The names of the columns to be discretized; by default, discretize all float and int columns in X.
encode : {'onehot', 'ordinal'}, default='onehot' Method used to encode the transformed result.
onehot Encode the transformed result with one-hot encoding and return a dense array. ordinal Return the bin identifier encoded as an integer value.strategy : {'uniform', 'quantile', 'kmeans'}, default='quantile' Strategy used to define the widths of the bins.
uniform All bins in each feature have identical widths. quantile All bins in each feature have the same number of points. kmeans Values in each bin have the same nearest center of a 1D k-means cluster.onehot_drop : {‘first’, ‘if_binary’} or a array-like of shape (len(dcols),), default='if_binary' Specifies a methodology to use to drop one of the categories per feature when encode = "onehot".
None Retain all features (the default). ‘first’ Drop the first y_str in each feature. If only one y_str is present, the feature will be dropped entirely. ‘if_binary’ Drop the first y_str in each feature with two categories. Features with 1 or more than 2 categories are left intact.Attributes
discretizer_:objectofclass KBinsDiscretizer()- Primary discretization method used to bin numeric data
manual_discretizer_:dictionary- Provides bin_edges to feed into _quantile_discretization() and do quantile discretization manually for features where KBinsDiscretizer() failed. Ignored if strategy != 'quantile' or no errors in KBinsDiscretizer().
onehot_:objectofclass OneHotEncoder()- One hot encoding fit. Ignored if encode != 'onehot'
Examples
Expand source code
class BasicDiscretizer(AbstractDiscretizer): """ Discretize numeric data into bins. Provides a wrapper around KBinsDiscretizer from sklearn Params ------ n_bins : int or array-like of shape (len(dcols),), default=2 Number of bins to discretize each feature into. dcols : list of strings The names of the columns to be discretized; by default, discretize all float and int columns in X. encode : {'onehot', 'ordinal'}, default='onehot' Method used to encode the transformed result. onehot Encode the transformed result with one-hot encoding and return a dense array. ordinal Return the bin identifier encoded as an integer value. strategy : {'uniform', 'quantile', 'kmeans'}, default='quantile' Strategy used to define the widths of the bins. uniform All bins in each feature have identical widths. quantile All bins in each feature have the same number of points. kmeans Values in each bin have the same nearest center of a 1D k-means cluster. onehot_drop : {‘first’, ‘if_binary’} or a array-like of shape (len(dcols),), default='if_binary' Specifies a methodology to use to drop one of the categories per feature when encode = "onehot". None Retain all features (the default). ‘first’ Drop the first y_str in each feature. If only one y_str is present, the feature will be dropped entirely. ‘if_binary’ Drop the first y_str in each feature with two categories. Features with 1 or more than 2 categories are left intact. Attributes ---------- discretizer_ : object of class KBinsDiscretizer() Primary discretization method used to bin numeric data manual_discretizer_ : dictionary Provides bin_edges to feed into _quantile_discretization() and do quantile discretization manually for features where KBinsDiscretizer() failed. Ignored if strategy != 'quantile' or no errors in KBinsDiscretizer(). onehot_ : object of class OneHotEncoder() One hot encoding fit. Ignored if encode != 'onehot' Examples -------- """ def __init__(self, n_bins=2, dcols=[], encode='onehot', strategy='quantile', onehot_drop='if_binary'): super().__init__(n_bins=n_bins, dcols=dcols, encode=encode, strategy=strategy, onehot_drop=onehot_drop) def fit(self, X, y=None): """ Fit the estimator. Parameters ---------- X : data frame of shape (n_samples, n_features) (Training) data to be discretized. y : Ignored. This parameter exists only for compatibility with :class:`~sklearn.pipeline.Pipeline` and fit_transform method Returns ------- self """ # initialization and error checking self._fit_preprocessing(X) # apply KBinsDiscretizer to the selected columns discretizer = KBinsDiscretizer(n_bins=self.n_bins_, encode='ordinal', strategy=self.strategy) discretizer.fit(X[self.dcols_]) self.discretizer_ = discretizer if (self.encode == 'onehot') | (self.strategy == 'quantile'): discretized_df = discretizer.transform(X[self.dcols_]) discretized_df = pd.DataFrame(discretized_df, columns=self.dcols_, index=X.index).astype(int) # fix KBinsDiscretizer errors if any when strategy = "quantile" if self.strategy == "quantile": err_idx = np.where(discretized_df.nunique() != self.n_bins_)[0] self.manual_discretizer_ = dict() for idx in err_idx: col = self.dcols_[idx] if X[col].nunique() > 1: q_values = np.linspace(0, 1, self.n_bins_[idx] + 1) bin_edges = np.quantile(X[col], q_values) discretized_df[col] = self._discretize_to_bins(X[col], bin_edges, keep_pointwise_bins=True) self.manual_discretizer_[col] = bin_edges # fit onehot encoded X if specified if self.encode == "onehot": onehot = OneHotEncoder(drop=self.onehot_drop) # , sparse=False) onehot.fit(discretized_df.astype(str)) self.onehot_ = onehot return self def transform(self, X): """ Discretize the data. Parameters ---------- X : data frame of shape (n_samples, n_features) Data to be discretized. Returns ------- X_discretized : data frame Data with features in dcols transformed to the binned space. All other features remain unchanged. """ check_is_fitted(self) # transform using KBinsDiscretizer discretized_df = self.discretizer_.transform( X[self.dcols_]).astype(int) discretized_df = pd.DataFrame(discretized_df, columns=self.dcols_, index=X.index) # fix KBinsDiscretizer errors (if any) when strategy = "quantile" if self.strategy == "quantile": for col in self.manual_discretizer_.keys(): bin_edges = self.manual_discretizer_[col] discretized_df[col] = self._discretize_to_bins(X[col], bin_edges, keep_pointwise_bins=True) # return onehot encoded data if specified and # join discretized columns with rest of X X_discretized = self._transform_postprocessing(discretized_df, X) return X_discretizedAncestors
- AbstractDiscretizer
- sklearn.base.TransformerMixin
- sklearn.utils._set_output._SetOutputMixin
- sklearn.base.BaseEstimator
- sklearn.utils._estimator_html_repr._HTMLDocumentationLinkMixin
- sklearn.utils._metadata_requests._MetadataRequester
Methods
def fit(self, X, y=None)-
Fit the estimator.
Parameters
X:data frameofshape (n_samples, n_features)- (Training) data to be discretized.
y:Ignored. This parameter exists only for compatibility with- :class:
~sklearn.pipeline.Pipelineand fit_transform method
Returns
self
Expand source code
def fit(self, X, y=None): """ Fit the estimator. Parameters ---------- X : data frame of shape (n_samples, n_features) (Training) data to be discretized. y : Ignored. This parameter exists only for compatibility with :class:`~sklearn.pipeline.Pipeline` and fit_transform method Returns ------- self """ # initialization and error checking self._fit_preprocessing(X) # apply KBinsDiscretizer to the selected columns discretizer = KBinsDiscretizer(n_bins=self.n_bins_, encode='ordinal', strategy=self.strategy) discretizer.fit(X[self.dcols_]) self.discretizer_ = discretizer if (self.encode == 'onehot') | (self.strategy == 'quantile'): discretized_df = discretizer.transform(X[self.dcols_]) discretized_df = pd.DataFrame(discretized_df, columns=self.dcols_, index=X.index).astype(int) # fix KBinsDiscretizer errors if any when strategy = "quantile" if self.strategy == "quantile": err_idx = np.where(discretized_df.nunique() != self.n_bins_)[0] self.manual_discretizer_ = dict() for idx in err_idx: col = self.dcols_[idx] if X[col].nunique() > 1: q_values = np.linspace(0, 1, self.n_bins_[idx] + 1) bin_edges = np.quantile(X[col], q_values) discretized_df[col] = self._discretize_to_bins(X[col], bin_edges, keep_pointwise_bins=True) self.manual_discretizer_[col] = bin_edges # fit onehot encoded X if specified if self.encode == "onehot": onehot = OneHotEncoder(drop=self.onehot_drop) # , sparse=False) onehot.fit(discretized_df.astype(str)) self.onehot_ = onehot return self def transform(self, X)-
Discretize the data.
Parameters
X:data frameofshape (n_samples, n_features)- Data to be discretized.
Returns
X_discretized:data frame- Data with features in dcols transformed to the binned space. All other features remain unchanged.
Expand source code
def transform(self, X): """ Discretize the data. Parameters ---------- X : data frame of shape (n_samples, n_features) Data to be discretized. Returns ------- X_discretized : data frame Data with features in dcols transformed to the binned space. All other features remain unchanged. """ check_is_fitted(self) # transform using KBinsDiscretizer discretized_df = self.discretizer_.transform( X[self.dcols_]).astype(int) discretized_df = pd.DataFrame(discretized_df, columns=self.dcols_, index=X.index) # fix KBinsDiscretizer errors (if any) when strategy = "quantile" if self.strategy == "quantile": for col in self.manual_discretizer_.keys(): bin_edges = self.manual_discretizer_[col] discretized_df[col] = self._discretize_to_bins(X[col], bin_edges, keep_pointwise_bins=True) # return onehot encoded data if specified and # join discretized columns with rest of X X_discretized = self._transform_postprocessing(discretized_df, X) return X_discretized
class ExtraBasicDiscretizer (dcols, n_bins=4, strategy='quantile', onehot_drop='if_binary')-
Discretize provided columns into bins and return in one-hot format. Generates meaningful column names based on bin edges. Wraps KBinsDiscretizer from sklearn.
Params
dcols : list of strings The names of the columns to be discretized.
n_bins : int or array-like of shape (len(dcols),), default=4 Number of bins to discretize each feature into.
strategy : {'uniform', 'quantile', 'kmeans'}, default='quantile' Strategy used to define the widths of the bins.
uniform All bins in each feature have identical widths. quantile All bins in each feature have the same number of points. kmeans Values in each bin have the same nearest center of a 1D k-means cluster.onehot_drop : {'first', 'if_binary'} or a array-like of shape (len(dcols),), default='if_binary' Specifies a methodology to use to drop one of the categories per feature when encode = "onehot".
None Retain all features (the default). 'first' Drop the first y_str in each feature. If only one y_str is present, the feature will be dropped entirely. 'if_binary' Drop the first y_str in each feature with two categories. Features with 1 or more than 2 categories are left intact.Attributes
discretizer_:objectofclass KBinsDiscretizer()- Primary discretization method used to bin numeric data
Examples
Expand source code
class ExtraBasicDiscretizer(TransformerMixin): """ Discretize provided columns into bins and return in one-hot format. Generates meaningful column names based on bin edges. Wraps KBinsDiscretizer from sklearn. Params ------ dcols : list of strings The names of the columns to be discretized. n_bins : int or array-like of shape (len(dcols),), default=4 Number of bins to discretize each feature into. strategy : {'uniform', 'quantile', 'kmeans'}, default='quantile' Strategy used to define the widths of the bins. uniform All bins in each feature have identical widths. quantile All bins in each feature have the same number of points. kmeans Values in each bin have the same nearest center of a 1D k-means cluster. onehot_drop : {'first', 'if_binary'} or a array-like of shape (len(dcols),), default='if_binary' Specifies a methodology to use to drop one of the categories per feature when encode = "onehot". None Retain all features (the default). 'first' Drop the first y_str in each feature. If only one y_str is present, the feature will be dropped entirely. 'if_binary' Drop the first y_str in each feature with two categories. Features with 1 or more than 2 categories are left intact. Attributes ---------- discretizer_ : object of class KBinsDiscretizer() Primary discretization method used to bin numeric data Examples -------- """ def __init__(self, dcols, n_bins=4, strategy='quantile', onehot_drop='if_binary'): self.dcols = dcols self.n_bins = n_bins self.strategy = strategy self.onehot_drop = onehot_drop def fit(self, X, y=None): """ Fit the estimator. Parameters ---------- X : data frame of shape (n_samples, n_features) (Training) data to be discretized. y : Ignored. This parameter exists only for compatibility with :class:`~sklearn.pipeline.Pipeline` and fit_transform method Returns ------- self """ # Fit KBinsDiscretizer to the selected columns discretizer = KBinsDiscretizer( n_bins=self.n_bins, strategy=self.strategy, encode='ordinal') discretizer.fit(X[self.dcols]) self.discretizer_ = discretizer # Fit OneHotEncoder to the ordinal output of KBinsDiscretizer disc_ordinal_np = discretizer.transform(X[self.dcols]) disc_ordinal_df = pd.DataFrame(disc_ordinal_np, columns=self.dcols) disc_ordinal_df_str = disc_ordinal_df.astype(int).astype(str) encoder = OneHotEncoder(drop=self.onehot_drop) # , sparse=False) encoder.fit(disc_ordinal_df_str) self.encoder_ = encoder return self def transform(self, X): """ Discretize the data. Parameters ---------- X : data frame of shape (n_samples, n_features) Data to be discretized. Returns ------- X_discretized : data frame Data with features in dcols transformed to the binned space. All other features remain unchanged. """ # Apply discretizer transform to get ordinally coded DF disc_ordinal_np = self.discretizer_.transform(X[self.dcols]) disc_ordinal_df = pd.DataFrame(disc_ordinal_np, columns=self.dcols) disc_ordinal_df_str = disc_ordinal_df.astype(int).astype(str) # One-hot encode the ordinal DF disc_onehot_np = self.encoder_.transform(disc_ordinal_df_str) if scipy.sparse.issparse(disc_onehot_np): # OneHotEncoder returns a sparse matrix by default, which pandas # would otherwise store as a single column of sparse rows disc_onehot_np = disc_onehot_np.toarray() disc_onehot = pd.DataFrame( disc_onehot_np, columns=self.encoder_.get_feature_names_out(), index=X.index) # Name columns after the interval they represent (e.g. 0.1_to_0.5) for col, bin_edges in zip(self.dcols, self.discretizer_.bin_edges_): bin_edges = bin_edges.astype(str) # every bin learned during fit, not just the ones present in X, so # that transform gives the same columns whatever data it is given for ordinal_value in range(len(bin_edges) - 1): bin_lb = bin_edges[int(ordinal_value)] bin_ub = bin_edges[int(ordinal_value) + 1] interval_string = f'{bin_lb}_to_{bin_ub}' disc_onehot = disc_onehot.rename( columns={f'{col}_{ordinal_value}': f'{col}_' + interval_string}) # Join discretized columns with rest of X non_dcols = [col for col in X.columns if col not in self.dcols] X_discretized = pd.concat([disc_onehot, X[non_dcols]], axis=1) return X_discretizedAncestors
- sklearn.base.TransformerMixin
- sklearn.utils._set_output._SetOutputMixin
Methods
def fit(self, X, y=None)-
Fit the estimator.
Parameters
X:data frameofshape (n_samples, n_features)- (Training) data to be discretized.
y:Ignored. This parameter exists only for compatibility with- :class:
~sklearn.pipeline.Pipelineand fit_transform method
Returns
self
Expand source code
def fit(self, X, y=None): """ Fit the estimator. Parameters ---------- X : data frame of shape (n_samples, n_features) (Training) data to be discretized. y : Ignored. This parameter exists only for compatibility with :class:`~sklearn.pipeline.Pipeline` and fit_transform method Returns ------- self """ # Fit KBinsDiscretizer to the selected columns discretizer = KBinsDiscretizer( n_bins=self.n_bins, strategy=self.strategy, encode='ordinal') discretizer.fit(X[self.dcols]) self.discretizer_ = discretizer # Fit OneHotEncoder to the ordinal output of KBinsDiscretizer disc_ordinal_np = discretizer.transform(X[self.dcols]) disc_ordinal_df = pd.DataFrame(disc_ordinal_np, columns=self.dcols) disc_ordinal_df_str = disc_ordinal_df.astype(int).astype(str) encoder = OneHotEncoder(drop=self.onehot_drop) # , sparse=False) encoder.fit(disc_ordinal_df_str) self.encoder_ = encoder return self def transform(self, X)-
Discretize the data.
Parameters
X:data frameofshape (n_samples, n_features)- Data to be discretized.
Returns
X_discretized:data frame- Data with features in dcols transformed to the binned space. All other features remain unchanged.
Expand source code
def transform(self, X): """ Discretize the data. Parameters ---------- X : data frame of shape (n_samples, n_features) Data to be discretized. Returns ------- X_discretized : data frame Data with features in dcols transformed to the binned space. All other features remain unchanged. """ # Apply discretizer transform to get ordinally coded DF disc_ordinal_np = self.discretizer_.transform(X[self.dcols]) disc_ordinal_df = pd.DataFrame(disc_ordinal_np, columns=self.dcols) disc_ordinal_df_str = disc_ordinal_df.astype(int).astype(str) # One-hot encode the ordinal DF disc_onehot_np = self.encoder_.transform(disc_ordinal_df_str) if scipy.sparse.issparse(disc_onehot_np): # OneHotEncoder returns a sparse matrix by default, which pandas # would otherwise store as a single column of sparse rows disc_onehot_np = disc_onehot_np.toarray() disc_onehot = pd.DataFrame( disc_onehot_np, columns=self.encoder_.get_feature_names_out(), index=X.index) # Name columns after the interval they represent (e.g. 0.1_to_0.5) for col, bin_edges in zip(self.dcols, self.discretizer_.bin_edges_): bin_edges = bin_edges.astype(str) # every bin learned during fit, not just the ones present in X, so # that transform gives the same columns whatever data it is given for ordinal_value in range(len(bin_edges) - 1): bin_lb = bin_edges[int(ordinal_value)] bin_ub = bin_edges[int(ordinal_value) + 1] interval_string = f'{bin_lb}_to_{bin_ub}' disc_onehot = disc_onehot.rename( columns={f'{col}_{ordinal_value}': f'{col}_' + interval_string}) # Join discretized columns with rest of X non_dcols = [col for col in X.columns if col not in self.dcols] X_discretized = pd.concat([disc_onehot, X[non_dcols]], axis=1) return X_discretized
class RFDiscretizer (rf_model=None, classification=False, n_bins=2, dcols=[], encode='onehot', strategy='quantile', backup_strategy='quantile', onehot_drop='if_binary')-
Discretize numeric data into bins using RF splits.
Parameters
rf_model:RandomForestClassifer()orRandomForestRegressor()- RF model from which to extract splits for discretization. Default is RandomForestClassifer(n_estimators = 500) or RandomForestRegressor(n_estimators = 500)
classification:boolean; default=False- Used only if rf_model=None. If True, rf_model=RandomForestClassifier(n_estimators = 500). Else, rf_model=RandomForestRegressor(n_estimators = 500)
n_bins:intorarray-likeofshape (len(dcols),), default=2- Number of bins to discretize each feature into.
dcols:listofstrings- The names of the columns to be discretized; by default, discretize all float and int columns in X.
encode:{‘onehot’, ‘ordinal’}, default=’onehot’-
Method used to encode the transformed result.
onehot - Encode the transformed result with one-hot encoding and return a dense array. ordinal - Return the bin identifier encoded as an integer value.
strategy:{‘uniform’, ‘quantile’}, default=’quantile’- Strategy used to choose RF split points. uniform - RF split points chosen to be uniformly spaced out. quantile - RF split points chosen based on equally-spaced quantiles.
backup_strategy:{‘uniform’, ‘quantile’, ‘kmeans’}, default=’quantile’- Strategy used to define the widths of the bins if no rf splits exist for that feature. Used in KBinsDiscretizer. uniform All bins in each feature have identical widths. quantile All bins in each feature have the same number of points. kmeans Values in each bin have the same nearest center of a 1D k-means cluster.
onehot_drop:{‘first’, ‘if_binary’}orarray-likeofshape (len(dcols),), default='if_binary'- Specifies a methodology to use to drop one of the categories per feature when encode = "onehot". None Retain all features (the default). ‘first’ Drop the first y_str in each feature. If only one y_str is present, the feature will be dropped entirely. ‘if_binary’ Drop the first y_str in each feature with two categories. Features with 1 or more than 2 categories are left intact.
Attributes
rf_splits:dictionary where- key = feature name value = array of all RF split threshold values
bin_edges_:dictionary where- key = feature name value = array of bin edges used for discretization, taken from RF split values
missing_rf_cols_:array-like- List of features that were not used in RF
backup_discretizer_:objectofclass BasicDiscretizer- Discretization method used to bin numeric data for features in missing_rf_cols_
onehot_:objectofclass OneHotEncoder()- One hot encoding fit. Ignored if encode != 'onehot'
Expand source code
class RFDiscretizer(AbstractDiscretizer): """ Discretize numeric data into bins using RF splits. Parameters ---------- rf_model : RandomForestClassifer() or RandomForestRegressor() RF model from which to extract splits for discretization. Default is RandomForestClassifer(n_estimators = 500) or RandomForestRegressor(n_estimators = 500) classification : boolean; default=False Used only if rf_model=None. If True, rf_model=RandomForestClassifier(n_estimators = 500). Else, rf_model=RandomForestRegressor(n_estimators = 500) n_bins : int or array-like of shape (len(dcols),), default=2 Number of bins to discretize each feature into. dcols : list of strings The names of the columns to be discretized; by default, discretize all float and int columns in X. encode : {‘onehot’, ‘ordinal’}, default=’onehot’ Method used to encode the transformed result. onehot - Encode the transformed result with one-hot encoding and return a dense array. ordinal - Return the bin identifier encoded as an integer value. strategy : {‘uniform’, ‘quantile’}, default=’quantile’ Strategy used to choose RF split points. uniform - RF split points chosen to be uniformly spaced out. quantile - RF split points chosen based on equally-spaced quantiles. backup_strategy : {‘uniform’, ‘quantile’, ‘kmeans’}, default=’quantile’ Strategy used to define the widths of the bins if no rf splits exist for that feature. Used in KBinsDiscretizer. uniform All bins in each feature have identical widths. quantile All bins in each feature have the same number of points. kmeans Values in each bin have the same nearest center of a 1D k-means cluster. onehot_drop : {‘first’, ‘if_binary’} or array-like of shape (len(dcols),), default='if_binary' Specifies a methodology to use to drop one of the categories per feature when encode = "onehot". None Retain all features (the default). ‘first’ Drop the first y_str in each feature. If only one y_str is present, the feature will be dropped entirely. ‘if_binary’ Drop the first y_str in each feature with two categories. Features with 1 or more than 2 categories are left intact. Attributes ---------- rf_splits : dictionary where key = feature name value = array of all RF split threshold values bin_edges_ : dictionary where key = feature name value = array of bin edges used for discretization, taken from RF split values missing_rf_cols_ : array-like List of features that were not used in RF backup_discretizer_ : object of class BasicDiscretizer() Discretization method used to bin numeric data for features in missing_rf_cols_ onehot_ : object of class OneHotEncoder() One hot encoding fit. Ignored if encode != 'onehot' """ def __init__(self, rf_model=None, classification=False, n_bins=2, dcols=[], encode='onehot', strategy='quantile', backup_strategy='quantile', onehot_drop='if_binary'): super().__init__(n_bins=n_bins, dcols=dcols, encode=encode, strategy=strategy, onehot_drop=onehot_drop) self.backup_strategy = backup_strategy self.rf_model = rf_model if rf_model is None: self.classification = classification def _validate_args(self): """ Check if encode, strategy, backup_strategy arguments are valid. """ super()._validate_args() valid_backup_strategy = ('uniform', 'quantile', 'kmeans') if (self.backup_strategy not in valid_backup_strategy): raise ValueError("Valid options for 'strategy' are {}. Got strategy={!r} instead." .format(valid_backup_strategy, self.backup_strategy)) def _get_rf_splits(self, col_names): """ Get all splits in random forest ensemble Parameters ---------- col_names : array-like of shape (n_features,) Column names for X used to train rf_model Returns ------- rule_dict : dictionary where key = feature name value = array of all RF split threshold values """ rule_dict = {} for model in self.rf_model.estimators_: tree = model.tree_ tree_it = enumerate(zip(tree.children_left, tree.children_right, tree.feature, tree.threshold)) for node_idx, data in tree_it: left, right, feature, th = data if (left != -1) | (right != -1): feature = col_names[feature] if feature in rule_dict: rule_dict[feature].append(th) else: rule_dict[feature] = [th] return rule_dict def _fit_rf(self, X, y=None): """ Fit random forest (if necessary) and obtain RF split thresholds Parameters ---------- X : data frame of shape (n_samples, n_features) Training data used to fit RF y : array-like of shape (n_samples,) Training response vector used to fit RF Returns ------- rf_splits : dictionary where key = feature name value = array of all RF split threshold values """ # If no rf_model given, train default random forest model if self.rf_model is None: if y is None: raise ValueError("Must provide y if rf_model is not given.") if self.classification: self.rf_model = RandomForestClassifier(n_estimators=500) else: self.rf_model = RandomForestRegressor(n_estimators=500) self.rf_model.fit(X, y) else: # provided rf model has not yet been trained if not check_is_fitted(self.rf_model): if y is None: raise ValueError( "Must provide y if rf_model has not been trained.") self.rf_model.fit(X, y) # get all random forest split points self.rf_splits = self._get_rf_splits(list(X.columns)) def reweight_n_bins(self, X, y=None, by="nsplits"): """ Reallocate number of bins per feature. Parameters ---------- X : data frame of shape (n_samples, n_features) (Training) data to be discretized. y : array-like of shape (n_samples,) (Training) response vector. Required only if rf_model = None or rf_model has not yet been fitted by : {'nsplits'}, default='nsplits' Specifies how to reallocate number of bins per feature. nsplits Reallocate number of bins so that each feature in dcols get at a minimum of 2 bins with the remaining bins distributed proportionally to the number of RF splits using that feature Returns ------- self.n_bins : array of shape (len(dcols),) number of bins per feature reallocated according to 'by' argument """ # initialization and error checking self._fit_preprocessing(X) # get all random forest split points self._fit_rf(X=X, y=y) # get total number of bins to reallocate total_bins = np.asarray(self.n_bins_).sum() # reweight n_bins if by == "nsplits": # each col gets at least 2 bins; remaining bins get # reallocated based on number of RF splits using that feature n_rules = np.array([len(self.rf_splits[col]) for col in self.dcols_]) self.n_bins = np.round(n_rules / n_rules.sum() * (total_bins - 2 * len(self.dcols_))) + 2 else: valid_by = ('nsplits') raise ValueError("Valid options for 'by' are {}. Got by={!r} instead." .format(valid_by, by)) def fit(self, X, y=None): """ Fit the estimator. Parameters ---------- X : data frame of shape (n_samples, n_features) (Training) data to be discretized. y : array-like of shape (n_samples,) (Training) response vector. Required only if rf_model = None or rf_model has not yet been fitted Returns ------- self """ # initialization and error checking self._fit_preprocessing(X) # get all random forest split points self._fit_rf(X=X, y=y) # features that were not used in the rf but need to be discretized self.missing_rf_cols_ = list(set(self.dcols_) - set(self.rf_splits.keys())) if len(self.missing_rf_cols_) > 0: print("{} did not appear in random forest so were discretized via {} discretization" .format(self.missing_rf_cols_, self.strategy)) missing_n_bins = np.array([self.n_bins_[np.array(self.dcols_) == col][0] for col in self.missing_rf_cols_]) backup_discretizer = BasicDiscretizer(n_bins=missing_n_bins, dcols=self.missing_rf_cols_, encode='ordinal', strategy=self.backup_strategy) backup_discretizer.fit(X[self.missing_rf_cols_]) self.backup_discretizer_ = backup_discretizer else: self.backup_discretizer_ = None if self.encode == 'onehot': if len(self.missing_rf_cols_) > 0: discretized_df = backup_discretizer.transform( X[self.missing_rf_cols_]) else: discretized_df = pd.DataFrame({}, index=X.index) # do discretization based on rf split thresholds self.bin_edges_ = dict() for col in self.dcols_: if col in self.rf_splits.keys(): # .item(): numpy no longer converts a size-1 array to a scalar b = self.n_bins_[np.array(self.dcols_) == col].item() if self.strategy == "quantile": q_values = np.linspace(0, 1, int(b) + 1) bin_edges = np.quantile(self.rf_splits[col], q_values) elif self.strategy == "uniform": width = (max(self.rf_splits[col]) - min(self.rf_splits[col])) / b bin_edges = width * \ np.arange(0, b + 1) + min(self.rf_splits[col]) self.bin_edges_[col] = bin_edges if self.encode == 'onehot': discretized_df[col] = self._discretize_to_bins( X[col], bin_edges) # fit onehot encoded X if specified if self.encode == "onehot": onehot = OneHotEncoder(drop=self.onehot_drop) # , sparse=False) onehot.fit(discretized_df[self.dcols_].astype(str)) self.onehot_ = onehot return self def transform(self, X): """ Discretize the data. Parameters ---------- X : data frame of shape (n_samples, n_features) Data to be discretized. Returns ------- X_discretized : data frame Data with features in dcols transformed to the binned space. All other features remain unchanged. """ check_is_fitted(self) # transform features that did not appear in RF if len(self.missing_rf_cols_) > 0: discretized_df = self.backup_discretizer_.transform( X[self.missing_rf_cols_]) discretized_df = pd.DataFrame(discretized_df, columns=self.missing_rf_cols_, index=X.index) else: discretized_df = pd.DataFrame({}, index=X.index) # do discretization based on rf split thresholds for col in self.bin_edges_.keys(): discretized_df[col] = self._discretize_to_bins( X[col], self.bin_edges_[col]) # return onehot encoded data if specified and # join discretized columns with rest of X X_discretized = self._transform_postprocessing(discretized_df, X) return X_discretizedAncestors
- AbstractDiscretizer
- sklearn.base.TransformerMixin
- sklearn.utils._set_output._SetOutputMixin
- sklearn.base.BaseEstimator
- sklearn.utils._estimator_html_repr._HTMLDocumentationLinkMixin
- sklearn.utils._metadata_requests._MetadataRequester
Methods
def fit(self, X, y=None)-
Fit the estimator.
Parameters
X:data frameofshape (n_samples, n_features)- (Training) data to be discretized.
y:array-likeofshape (n_samples,)- (Training) response vector. Required only if rf_model = None or rf_model has not yet been fitted
Returns
self
Expand source code
def fit(self, X, y=None): """ Fit the estimator. Parameters ---------- X : data frame of shape (n_samples, n_features) (Training) data to be discretized. y : array-like of shape (n_samples,) (Training) response vector. Required only if rf_model = None or rf_model has not yet been fitted Returns ------- self """ # initialization and error checking self._fit_preprocessing(X) # get all random forest split points self._fit_rf(X=X, y=y) # features that were not used in the rf but need to be discretized self.missing_rf_cols_ = list(set(self.dcols_) - set(self.rf_splits.keys())) if len(self.missing_rf_cols_) > 0: print("{} did not appear in random forest so were discretized via {} discretization" .format(self.missing_rf_cols_, self.strategy)) missing_n_bins = np.array([self.n_bins_[np.array(self.dcols_) == col][0] for col in self.missing_rf_cols_]) backup_discretizer = BasicDiscretizer(n_bins=missing_n_bins, dcols=self.missing_rf_cols_, encode='ordinal', strategy=self.backup_strategy) backup_discretizer.fit(X[self.missing_rf_cols_]) self.backup_discretizer_ = backup_discretizer else: self.backup_discretizer_ = None if self.encode == 'onehot': if len(self.missing_rf_cols_) > 0: discretized_df = backup_discretizer.transform( X[self.missing_rf_cols_]) else: discretized_df = pd.DataFrame({}, index=X.index) # do discretization based on rf split thresholds self.bin_edges_ = dict() for col in self.dcols_: if col in self.rf_splits.keys(): # .item(): numpy no longer converts a size-1 array to a scalar b = self.n_bins_[np.array(self.dcols_) == col].item() if self.strategy == "quantile": q_values = np.linspace(0, 1, int(b) + 1) bin_edges = np.quantile(self.rf_splits[col], q_values) elif self.strategy == "uniform": width = (max(self.rf_splits[col]) - min(self.rf_splits[col])) / b bin_edges = width * \ np.arange(0, b + 1) + min(self.rf_splits[col]) self.bin_edges_[col] = bin_edges if self.encode == 'onehot': discretized_df[col] = self._discretize_to_bins( X[col], bin_edges) # fit onehot encoded X if specified if self.encode == "onehot": onehot = OneHotEncoder(drop=self.onehot_drop) # , sparse=False) onehot.fit(discretized_df[self.dcols_].astype(str)) self.onehot_ = onehot return self def reweight_n_bins(self, X, y=None, by='nsplits')-
Reallocate number of bins per feature.
Parameters
X:data frameofshape (n_samples, n_features)- (Training) data to be discretized.
y:array-likeofshape (n_samples,)- (Training) response vector. Required only if rf_model = None or rf_model has not yet been fitted
by:{'nsplits'}, default='nsplits'-
Specifies how to reallocate number of bins per feature.
nsplits Reallocate number of bins so that each feature in dcols get at a minimum of 2 bins with the remaining bins distributed proportionally to the number of RF splits using that feature
Returns
self.n_bins : arrayofshape (len(dcols),)- number of bins per feature reallocated according to 'by' argument
Expand source code
def reweight_n_bins(self, X, y=None, by="nsplits"): """ Reallocate number of bins per feature. Parameters ---------- X : data frame of shape (n_samples, n_features) (Training) data to be discretized. y : array-like of shape (n_samples,) (Training) response vector. Required only if rf_model = None or rf_model has not yet been fitted by : {'nsplits'}, default='nsplits' Specifies how to reallocate number of bins per feature. nsplits Reallocate number of bins so that each feature in dcols get at a minimum of 2 bins with the remaining bins distributed proportionally to the number of RF splits using that feature Returns ------- self.n_bins : array of shape (len(dcols),) number of bins per feature reallocated according to 'by' argument """ # initialization and error checking self._fit_preprocessing(X) # get all random forest split points self._fit_rf(X=X, y=y) # get total number of bins to reallocate total_bins = np.asarray(self.n_bins_).sum() # reweight n_bins if by == "nsplits": # each col gets at least 2 bins; remaining bins get # reallocated based on number of RF splits using that feature n_rules = np.array([len(self.rf_splits[col]) for col in self.dcols_]) self.n_bins = np.round(n_rules / n_rules.sum() * (total_bins - 2 * len(self.dcols_))) + 2 else: valid_by = ('nsplits') raise ValueError("Valid options for 'by' are {}. Got by={!r} instead." .format(valid_by, by)) def transform(self, X)-
Discretize the data.
Parameters
X:data frameofshape (n_samples, n_features)- Data to be discretized.
Returns
X_discretized:data frame- Data with features in dcols transformed to the binned space. All other features remain unchanged.
Expand source code
def transform(self, X): """ Discretize the data. Parameters ---------- X : data frame of shape (n_samples, n_features) Data to be discretized. Returns ------- X_discretized : data frame Data with features in dcols transformed to the binned space. All other features remain unchanged. """ check_is_fitted(self) # transform features that did not appear in RF if len(self.missing_rf_cols_) > 0: discretized_df = self.backup_discretizer_.transform( X[self.missing_rf_cols_]) discretized_df = pd.DataFrame(discretized_df, columns=self.missing_rf_cols_, index=X.index) else: discretized_df = pd.DataFrame({}, index=X.index) # do discretization based on rf split thresholds for col in self.bin_edges_.keys(): discretized_df[col] = self._discretize_to_bins( X[col], self.bin_edges_[col]) # return onehot encoded data if specified and # join discretized columns with rest of X X_discretized = self._transform_postprocessing(discretized_df, X) return X_discretized
class SimpleDiscretizer (n_bins: int = 8, strategy: str = 'uniform')-
Expand source code
class SimpleDiscretizer: def __init__(self, n_bins: int = 8, strategy: str = 'uniform'): self.n_bins = n_bins self.strategy = strategy def fit(self, X: np.array, feature_labels: np.array): self.is_categorical = np.array([set(np.unique(X[:, i])).issubset({0, 1}) for i in np.arange(X.shape[1])]) if False not in self.is_categorical: self.feature_labels = feature_labels self.discretizer = None return if isinstance(feature_labels, list): feature_labels = np.array(feature_labels) # X_categorical = X[:, self.is_categorical] X_categorical_columns = feature_labels[self.is_categorical] # X_numeric = X[:, ~self.is_categorical] X_numeric_columns = feature_labels[~self.is_categorical] self.discretizer = KBinsDiscretizer(n_bins=self.n_bins, encode='onehot', strategy=self.strategy) # X_numeric_discretized = self.discretizer.fit(X_numeric) discretized_featnames = [] for feat_name, bin_edges in zip(X_numeric_columns, self.discretizer.bin_edges_): be_str = bin_edges.astype(str) discretized_featnames += ( [f'{feat_name}_' + '_to_'.join([be_str[i], be_str[i + 1]]) for i in range(bin_edges.shape[0] - 1)] ) self.featnames_after_disc = np.append(discretized_featnames, X_categorical_columns) def transform(self, X: np.array): if self.discretizer is None: return pd.DataFrame(X, columns=self.feature_labels) X_categorical = X[:, self.is_categorical] X_numeric = X[:, ~self.is_categorical] X_numeric_discretized = self.discretizer.transform(X_numeric).toarray() X_concat = np.concatenate((X_numeric_discretized, X_categorical), axis=1) X_df_onehot = pd.DataFrame(X_concat, columns=self.featnames_after_disc) return X_df_onehot def fit_transform(self, X: np.array, feature_labels: np.array): self.fit(X, feature_labels) return self.transform(X)Methods
def fit(self, X:, feature_labels: ) -
Expand source code
def fit(self, X: np.array, feature_labels: np.array): self.is_categorical = np.array([set(np.unique(X[:, i])).issubset({0, 1}) for i in np.arange(X.shape[1])]) if False not in self.is_categorical: self.feature_labels = feature_labels self.discretizer = None return if isinstance(feature_labels, list): feature_labels = np.array(feature_labels) # X_categorical = X[:, self.is_categorical] X_categorical_columns = feature_labels[self.is_categorical] # X_numeric = X[:, ~self.is_categorical] X_numeric_columns = feature_labels[~self.is_categorical] self.discretizer = KBinsDiscretizer(n_bins=self.n_bins, encode='onehot', strategy=self.strategy) # X_numeric_discretized = self.discretizer.fit(X_numeric) discretized_featnames = [] for feat_name, bin_edges in zip(X_numeric_columns, self.discretizer.bin_edges_): be_str = bin_edges.astype(str) discretized_featnames += ( [f'{feat_name}_' + '_to_'.join([be_str[i], be_str[i + 1]]) for i in range(bin_edges.shape[0] - 1)] ) self.featnames_after_disc = np.append(discretized_featnames, X_categorical_columns) def fit_transform(self, X:, feature_labels: ) -
Expand source code
def fit_transform(self, X: np.array, feature_labels: np.array): self.fit(X, feature_labels) return self.transform(X) def transform(self, X:) -
Expand source code
def transform(self, X: np.array): if self.discretizer is None: return pd.DataFrame(X, columns=self.feature_labels) X_categorical = X[:, self.is_categorical] X_numeric = X[:, ~self.is_categorical] X_numeric_discretized = self.discretizer.transform(X_numeric).toarray() X_concat = np.concatenate((X_numeric_discretized, X_categorical), axis=1) X_df_onehot = pd.DataFrame(X_concat, columns=self.featnames_after_disc) return X_df_onehot