"""Ordinal or label encoding"""importnumpyasnpimportpandasaspdfromsklearn.baseimportBaseEstimator,TransformerMixinimportcategory_encoders.utilsasutilimportwarnings__author__='willmcginnis'
[docs]classOrdinalEncoder(BaseEstimator,TransformerMixin):"""Encodes categorical features as ordinal, in one ordered feature. Ordinal encoding uses a single column of integers to represent the classes. An optional mapping dict can be passed in; in this case, we use the knowledge that there is some true order to the classes themselves. Otherwise, the classes are assumed to have no true order and integers are selected at random. 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 columns with 0 variance. return_df: bool boolean for whether to return a pandas DataFrame from transform (otherwise it will be a numpy array). mapping: list of dicts a mapping of class to label to use for the encoding, optional. the dict contains the keys 'col' and 'mapping'. the value of 'col' should be the feature name. the value of 'mapping' should be a dictionary of 'original_label' to 'encoded_label'. example mapping: [ {'col': 'col1', 'mapping': {None: 0, 'a': 1, 'b': 2}}, {'col': 'col2', 'mapping': {None: 0, 'x': 1, 'y': 2}} ] handle_unknown: str options are 'error', 'return_nan' and 'value', defaults to 'value', which will impute the category -1. handle_missing: str options are 'error', 'return_nan', and 'value, default to 'value', which treat nan as a category at fit time, or -2 at transform time if nan is not a category during fit. Example ------- >>> from category_encoders import * >>> import pandas as pd >>> from sklearn.datasets import load_boston >>> bunch = load_boston() >>> y = bunch.target >>> X = pd.DataFrame(bunch.data, columns=bunch.feature_names) >>> enc = OrdinalEncoder(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 int64 NOX 506 non-null float64 RM 506 non-null float64 AGE 506 non-null float64 DIS 506 non-null float64 RAD 506 non-null int64 TAX 506 non-null float64 PTRATIO 506 non-null float64 B 506 non-null float64 LSTAT 506 non-null float64 dtypes: float64(11), int64(2) memory usage: 51.5 KB None References ---------- .. [1] Contrast Coding Systems for Categorical Variables, from https://stats.idre.ucla.edu/r/library/r-library-contrast-coding-systems-for-categorical-variables/ .. [2] Gregory Carey (2003). Coding Categorical Variables, from http://psych.colorado.edu/~carey/Courses/PSYC5741/handouts/Coding%20Categorical%20Variables%202006-03-03.pdf """def__init__(self,verbose=0,mapping=None,cols=None,drop_invariant=False,return_df=True,handle_unknown='value',handle_missing='value'):self.return_df=return_dfself.drop_invariant=drop_invariantself.drop_cols=[]self.verbose=verboseself.cols=colsself.mapping=mappingself.handle_unknown=handle_unknownself.handle_missing=handle_missingself._dim=Noneself.feature_names=None@propertydefcategory_mapping(self):returnself.mapping
[docs]deffit(self,X,y=None,**kwargs):"""Fit encoder according to X and 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] Target values. Returns ------- self : encoder Returns self. """# first check the typeX=util.convert_input(X)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')_,categories=self.ordinal_encoding(X,mapping=self.mapping,cols=self.cols,handle_unknown=self.handle_unknown,handle_missing=self.handle_missing)self.mapping=categoriesX_temp=self.transform(X,override_return_df=True)self.feature_names=X_temp.columns.tolist()# drop all output columns with 0 variance.ifself.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,override_return_df=False):"""Perform the transformation to new categorical data. Will use the mapping (if available) and the column list (if available, otherwise every column) to encode the data ordinarily. Parameters ---------- X : array-like, shape = [n_samples, n_features] 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.')# first check the typeX=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,))ifnotlist(self.cols):returnXifself.return_dfelseX.valuesX,_=self.ordinal_encoding(X,mapping=self.mapping,cols=self.cols,handle_unknown=self.handle_unknown,handle_missing=self.handle_missing)ifself.drop_invariant:forcolinself.drop_cols:X.drop(col,1,inplace=True)ifself.return_dforoverride_return_df:returnXelse:returnX.values
[docs]definverse_transform(self,X_in):""" Perform the inverse transformation to encoded data. Will attempt best case reconstruction, which means it will return nan for handle_missing and handle_unknown settings that break the bijection. We issue warnings when some of those cases occur. Parameters ---------- X_in : array-like, shape = [n_samples, n_features] Returns ------- p: array, the same size of X_in """# fail fastifself._dimisNone:raiseValueError('Must train encoder before it can be used to inverse_transform data')# first check the type and make deep copyX=util.convert_input(X_in,deep=True)# then make sure that it is the right sizeifX.shape[1]!=self._dim:ifself.drop_invariant:raiseValueError("Unexpected input dimension %d, the attribute drop_invariant should ""be False when transforming the data"%(X.shape[1],))else:raiseValueError('Unexpected input dimension %d, expected %d'%(X.shape[1],self._dim,))ifnotlist(self.cols):returnXifself.return_dfelseX.valuesifself.handle_unknown=='value':forcolinself.cols:ifany(X[col]==-1):warnings.warn("inverse_transform is not supported because transform impute ""the unknown category -1 when encode %s"%(col,))ifself.handle_unknown=='return_nan'andself.handle_missing=='return_nan':forcolinself.cols:ifX[col].isnull().any():warnings.warn("inverse_transform is not supported because transform impute ""the unknown category nan when encode %s"%(col,))forswitchinself.mapping:column_mapping=switch.get('mapping')inverse=pd.Series(data=column_mapping.index,index=column_mapping.values)X[switch.get('col')]=X[switch.get('col')].map(inverse).astype(switch.get('data_type'))returnXifself.return_dfelseX.values
[docs]@staticmethoddefordinal_encoding(X_in,mapping=None,cols=None,handle_unknown='value',handle_missing='value'):""" Ordinal encoding uses a single column of integers to represent the classes. An optional mapping dict can be passed in, in this case we use the knowledge that there is some true order to the classes themselves. Otherwise, the classes are assumed to have no true order and integers are selected at random. """return_nan_series=pd.Series(data=[np.nan],index=[-2])X=X_in.copy(deep=True)ifcolsisNone:cols=X.columns.valuesifmappingisnotNone:mapping_out=mappingforswitchinmapping:column=switch.get('col')col_mapping=switch['mapping']X[column]=X[column].map(col_mapping)ifutil.is_category(X[column].dtype):ifnotisinstance(col_mapping,pd.Series):col_mapping=pd.Series(col_mapping)nan_identity=col_mapping.loc[col_mapping.index.isna()].values[0]X[column]=X[column].cat.add_categories(nan_identity)X[column]=X[column].fillna(nan_identity)try:X[column]=X[column].astype(int)exceptValueErrorase:X[column]=X[column].astype(float)ifhandle_unknown=='value':X[column].fillna(-1,inplace=True)elifhandle_unknown=='error':missing=X[column].isnull()ifany(missing):raiseValueError('Unexpected categories found in column %s'%column)ifhandle_missing=='return_nan':X[column]=X[column].map(return_nan_series).where(X[column]==-2,X[column])else:mapping_out=[]forcolincols:nan_identity=np.nancategories=list(X[col].unique())ifutil.is_category(X[col].dtype):# Avoid using pandas category dtype meta-data if possible, see #235, #238.ifX[col].dtype.ordered:categories=[cforcinX[col].dtype.categoriesifcincategories]ifX[col].isna().any():categories+=[np.nan]index=pd.Series(categories).fillna(nan_identity).unique()data=pd.Series(index=index,data=range(1,len(index)+1))ifhandle_missing=='value'and~data.index.isnull().any():data.loc[nan_identity]=-2elifhandle_missing=='return_nan':data.loc[nan_identity]=-2mapping_out.append({'col':col,'mapping':data,'data_type':X[col].dtype},)returnX,mapping_out
[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