from sklearn.base import TransformerMixin
import numpy as np
import pandas as pd

class Imputer(TransformerMixin):
    
    """
    Imputes missing values based on 'sklearn.base.TransformerMixin'. Numerics values can be imputed by a given metric (default=np.mean) or by a constant value when passed in manual_values. Non numeric values can be imputed by the most frequent value or by a constant value when passed in manual_values. 
    
    Parameters
    ----------
    
        missing_value: value, default=np.NaN
            value to be imputed
            
        metric_numeric: function, default=np.mean
            function to determine imputing value for numerical columns
            
        manual_values: dictionary, default={}
            dictionary with key/value paris of column: value, sometimes missing values contain an information and need to be set to a specific value
            
            
    Output
    ------
    
        pd.DataFrame with imputed values
        
            
    Functions
    ---------
    
        fit(X, y=None):
            Fit the Imputer by genereating the values to impute. Either takes manual values or values generated by a metric for numerical values or the most frequent value for non numeric values. Manual values have the highest priority.
            
        transform(X, y=None)
            Replaces mising values with the fitted values and returns a pd.DataFrame with the imputed values.
            
        fit_transform(X, y=None)
            Performs fit and transform.
            
            
    Examples
    --------
    
        impute numeric missing values by using the mean of the column
        >>>df = pd.DataFrame({'numeric':[1, 2, 3, np.NaN]})
        >>>im = Imputer(missing_value=np.NaN, metric_numeric=np.mean,  manual_values={})
        >>>im.fit_transform(df)
        	numeric
        0	1.0
        1	2.0
        2	3.0
        3	2.0
        
        impute non numeric values by using most frequent class
        >>>df = pd.DataFrame({'non_numeric':['1', '2', '1', np.NaN]})
        >>>im = Imputer(missing_value=np.NaN, manual_values={})
        >>>im.fit_transform(df)
        	non_numeric
        0	1
        1	2
        2	1
        3	1
        
        impute by using manual values
        >>>df_test = pd.DataFrame({'non_numeric':['1', '2', '1', np.NaN], 'numeric':[1, 2, 3, np.NaN]})
        >>>im2 = Imputer(manual_values={'non_numeric':'beer', 'numeric':42})
        >>>im2.fit_transform(df_test)
        	non_numeric	numeric
        0	1	1.0
        1	2	2.0
        2	1	3.0
        3	beer	42.0
        
    """
    
    
    def __init__(self, missing_value=np.NaN, metric_numeric=np.mean,  manual_values={}):
        
        self.missing_value=missing_value
        self.metric_numeric=metric_numeric
        self.manual_values=manual_values
        self.imputed_values={}
        
        
    def fit(self, X, y=None):
        
        """
        fits the Imputer
        
        Parameters
        ----------
        
            X: pd.DataFrame
                DataFrame with the features to impute
                
            y: pd.Series
                will not be used, just necessary for sklearn syntax
                
        """
              
        #dtypes for numeric values
        numerics = ['int16', 'int32', 'int64', 'float16', 'float32', 'float64']
        
        for column in X.columns:
            
            #if manual values are given use them for imputing, otherwise use numeric metric for numerical values and most frequent value for non numerical values
            if column in self.manual_values.keys():
            
                self.imputed_values[column] = self.manual_values[column]
                
            else:            
            
                if X[column].dtype in numerics:
                    
                    self.imputed_values[column] = self.metric_numeric(X[column])
                    
                else:
                    
                    self.imputed_values[column] = X[column].value_counts().sort_values(ascending=False).index[0]
            

    def transform(self, X, y=None):
        
        """
        imputes missing values and returns the transformed DataFrame
        
        Parameters
        ----------
        
            X: pd.DataFrame
                DataFrame with the features to impute
                
            y: pd.Series
                will not be used, just necessary for sklearn syntax
             
        """
        
        X_transformed = X.copy()
        
        for column in self.imputed_values.keys():
            
            X_transformed.loc[:,column] = X[column].replace(self.missing_value, self.imputed_values[column])
        
        return X_transformed
    
    
    def fit_transform(self, X, y=None):
        
        """
        performs fit and transform
        """
        
        self.fit(X, y=None)
        return self.transform(X, y=None)
