[docs]classMEstimateEncoder(BaseEstimator,util.TransformerWithTargetMixin):"""M-probability estimate of likelihood. Supported targets: binomial and continuous. For polynomial target support, see PolynomialWrapper. This is a simplified version of target encoder, which goes under names like m-probability estimate or additive smoothing with known incidence rates. In comparison to target encoder, m-probability estimate has only one tunable parameter (`m`), while target encoder has two tunable parameters (`min_samples_leaf` and `smoothing`). Parameters ---------- verbose: int integer indicating verbosity of the output. 0 for none. cols: list a list of columns to encode, if None, all string columns will be encoded. drop_invariant: bool boolean for whether or not to drop encoded columns with 0 variance. return_df: bool boolean for whether to return a pandas DataFrame from transform (otherwise it will be a numpy array). handle_missing: str options are 'return_nan', 'error' and 'value', defaults to 'value', which returns the prior probability. handle_unknown: str options are 'return_nan', 'error' and 'value', defaults to 'value', which returns the prior probability. randomized: bool, adds normal (Gaussian) distribution noise into training data in order to decrease overfitting (testing data are untouched). sigma: float standard deviation (spread or "width") of the normal distribution. m: float this is the "m" in the m-probability estimate. Higher value of m results into stronger shrinking. M is non-negative. Example ------- >>> from category_encoders import * >>> import pandas as pd >>> from sklearn.datasets import load_boston >>> bunch = load_boston() >>> y = bunch.target > 22.5 >>> X = pd.DataFrame(bunch.data, columns=bunch.feature_names) >>> enc = MEstimateEncoder(cols=['CHAS', 'RAD']).fit(X, y) >>> numeric_dataset = enc.transform(X) >>> print(numeric_dataset.info()) <class 'pandas.core.frame.DataFrame'> RangeIndex: 506 entries, 0 to 505 Data columns (total 13 columns): CRIM 506 non-null float64 ZN 506 non-null float64 INDUS 506 non-null float64 CHAS 506 non-null float64 NOX 506 non-null float64 RM 506 non-null float64 AGE 506 non-null float64 DIS 506 non-null float64 RAD 506 non-null float64 TAX 506 non-null float64 PTRATIO 506 non-null float64 B 506 non-null float64 LSTAT 506 non-null float64 dtypes: float64(13) memory usage: 51.5 KB None References ---------- .. [1] A Preprocessing Scheme for High-Cardinality Categorical Attributes in Classification and Prediction Problems, equation 7, from https://dl.acm.org/citation.cfm?id=507538 .. [2] On estimating probabilities in tree pruning, equation 1, from https://link.springer.com/chapter/10.1007/BFb0017010 .. [3] Additive smoothing, from https://en.wikipedia.org/wiki/Additive_smoothing#Generalized_to_the_case_of_known_incidence_rates """def__init__(self,verbose=0,cols=None,drop_invariant=False,return_df=True,handle_unknown='value',handle_missing='value',random_state=None,randomized=False,sigma=0.05,m=1.0):self.verbose=verboseself.return_df=return_dfself.drop_invariant=drop_invariantself.drop_cols=[]self.cols=colsself.ordinal_encoder=Noneself._dim=Noneself.mapping=Noneself.handle_unknown=handle_unknownself.handle_missing=handle_missingself._sum=Noneself._count=Noneself.random_state=random_stateself.randomized=randomizedself.sigma=sigmaself.m=mself.feature_names=None# noinspection PyUnusedLocal
[docs]deffit(self,X,y,**kwargs):"""Fit encoder according to X and binary or continuous y. Parameters ---------- X : array-like, shape = [n_samples, n_features] Training vectors, where n_samples is the number of samples and n_features is the number of features. y : array-like, shape = [n_samples] Binary target values. Returns ------- self : encoder Returns self. """# Unite parameters into pandas typesX=util.convert_input(X)y=util.convert_input_vector(y,X.index).astype(float)# The lengths must be equalifX.shape[0]!=y.shape[0]:raiseValueError("The length of X is "+str(X.shape[0])+" but length of y is "+str(y.shape[0])+".")self._dim=X.shape[1]# If columns aren't passed, just use every string columnifself.colsisNone:self.cols=util.get_obj_cols(X)else:self.cols=util.convert_cols_to_list(self.cols)ifself.handle_missing=='error':ifX[self.cols].isnull().any().any():raiseValueError('Columns to be encoded can not contain null')self.ordinal_encoder=OrdinalEncoder(verbose=self.verbose,cols=self.cols,handle_unknown='value',handle_missing='value')self.ordinal_encoder=self.ordinal_encoder.fit(X)X_ordinal=self.ordinal_encoder.transform(X)# Trainingself.mapping=self._train(X_ordinal,y)X_temp=self.transform(X,override_return_df=True)self.feature_names=X_temp.columns.tolist()# Store column names with approximately constant variance on the training dataifself.drop_invariant:self.drop_cols=[]generated_cols=util.get_generated_cols(X,X_temp,self.cols)self.drop_cols=[xforxingenerated_colsifX_temp[x].var()<=10e-5]try:[self.feature_names.remove(x)forxinself.drop_cols]exceptKeyErrorase:ifself.verbose>0:print("Could not remove column from feature names.""Not found in generated cols.\n{}".format(e))returnself
[docs]deftransform(self,X,y=None,override_return_df=False):"""Perform the transformation to new categorical data. When the data are used for model training, it is important to also pass the target in order to apply leave one out. Parameters ---------- X : array-like, shape = [n_samples, n_features] y : array-like, shape = [n_samples] when transform by leave one out None, when transform without target information (such as transform test set) Returns ------- p : array, shape = [n_samples, n_numeric + N] Transformed values with encoding applied. """ifself.handle_missing=='error':ifX[self.cols].isnull().any().any():raiseValueError('Columns to be encoded can not contain null')ifself._dimisNone:raiseValueError('Must train encoder before it can be used to transform data.')# Unite the input into pandas typesX=util.convert_input(X)# Then make sure that it is the right sizeifX.shape[1]!=self._dim:raiseValueError('Unexpected input dimension %d, expected %d'%(X.shape[1],self._dim,))# If we are encoding the training data, we have to check the targetifyisnotNone:y=util.convert_input_vector(y,X.index).astype(float)ifX.shape[0]!=y.shape[0]:raiseValueError("The length of X is "+str(X.shape[0])+" but length of y is "+str(y.shape[0])+".")ifnotlist(self.cols):returnX# Do not modify the input argumentX=X.copy(deep=True)X=self.ordinal_encoder.transform(X)ifself.handle_unknown=='error':ifX[self.cols].isin([-1]).any().any():raiseValueError('Unexpected categories found in dataframe')# Loop over the columns and replace the nominal values with the numbersX=self._score(X,y)# Postprocessing# Note: We should not even convert these columns.ifself.drop_invariant:forcolinself.drop_cols:X.drop(col,1,inplace=True)ifself.return_dforoverride_return_df:returnXelse:returnX.values
def_train(self,X,y):# Initialize the outputmapping={}# Calculate global statisticsself._sum=y.sum()self._count=y.count()prior=self._sum/self._countforswitchinself.ordinal_encoder.category_mapping:col=switch.get('col')values=switch.get('mapping')# Calculate sum and count of the target for each unique value in the feature colstats=y.groupby(X[col]).agg(['sum','count'])# Count of x_{i,+} and x_i# Calculate the m-probability estimateestimate=(stats['sum']+prior*self.m)/(stats['count']+self.m)# Ignore unique columns. This helps to prevent overfitting on id-like columnsiflen(stats['count'])==self._count:estimate[:]=priorifself.handle_unknown=='return_nan':estimate.loc[-1]=np.nanelifself.handle_unknown=='value':estimate.loc[-1]=priorifself.handle_missing=='return_nan':estimate.loc[values.loc[np.nan]]=np.nanelifself.handle_missing=='value':estimate.loc[-2]=prior# Store the m-probability estimate for transform() functionmapping[col]=estimatereturnmappingdef_score(self,X,y):forcolinself.cols:# Score the columnX[col]=X[col].map(self.mapping[col])# Randomization is meaningful only for training data -> we do it only if y is presentifself.randomizedandyisnotNone:random_state_generator=check_random_state(self.random_state)X[col]=(X[col]*random_state_generator.normal(1.,self.sigma,X[col].shape[0]))returnX
[docs]defget_feature_names(self):""" Returns the names of all transformed / added columns. Returns ------- feature_names: list A list with all feature names transformed or added. Note: potentially dropped features are not included! """ifnotisinstance(self.feature_names,list):raiseValueError("Estimator has to be fitted to return feature names.")else:returnself.feature_names