import numpy as np
import pandas as pd
from imblearn.over_sampling import RandomOverSampler
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler


class Preprocessor:
    """
            This class shall  be used to clean and transform the data before training.

            Written By: <NAME>
            Version: 1.0
            Revisions: None

            """

    def __init__(self, file_object, logger_object):
        self.file_object = file_object
        self.logger_object = logger_object

    def remove_unwanted_spaces(self, data):
        """
                                Method Name: remove_unwanted_spaces
                                Description: This method removes the unwanted spaces from a pandas dataframe.
                                Output: A pandas DataFrame after removing the spaces.
                                On Failure: Raise Exception

                                Written By: <NAME>
                                Version: 1.0
                                Revisions: None

                        """
        self.logger_object.log(self.file_object, "Entered the remove_unwanted_spaces method of the Preprocessor class")
        self.data = data
        try:
            self.df_without_spaces = self.data.apply(
                lambda x: x.str.strip() if x.dtype == "object" else x)  # drop the labels specified in the columns
            self.logger_object.log(self.file_object,
                                   "Unwanted spaces removal Successful. Exited the remove_unwanted_spaces method of the Preprocess class")
            return self.df_without_spaces
        except Exception as e:
            self.logger_object.log(self.file_object,
                                   "Exception occured in remove_unwanted_spaces method of the Preprocessor class. Exception message:: " + str(
                                       e))
            self.logger_object.log(self.file_object,
                                   "Unwanted space removal Unsuccessful! Exited the remove_unwanted_spaces method of the Prepcoessor class")
            raise Exception()

    def remove_clumns(self, data, columns):
        """
                        Method Name: remove_columns
                        Description: This method removes the given columns from a pandas dataframe.
                        Output: A pandas DataFrame after removing the specified columns.
                        On Failure: Raise Exception

                        Written By: <NAME>
                        Version: 1.0
                        Revisions: None

                """
        self.logger_object.log(self.file_object, "Entered the remove_columns method of the Preprocessor class")
        self.data = data
        self.columns = columns
        try:
            self.useful_data = self.data.drop(labels=self.columns, axis=1)  # drop the labels specified in the columns
            self.logger_object.log(self.file_object,
                                   'Columns removal Successful. Exited the removal_columns method of the preprocessor class')
            return self.useful_data
        except Exception as e:
            self.logger_object.log(self.file_object,
                                   "Exception occured in remove_columns mehod of the Preprocessor class. Exception:: " + str(
                                       e))
            self.logger_object.log(self.file_object,
                                   'Column removal Unsuccessful. Exited the remove_columns method of the Preprocessor class')
            raise Exception

    def separate_label_features(self, data, label_column_name):
        """
                                Method Name: separate_label_feature
                                Description: This method separates the features and a Label Coulmns.
                                Output: Returns two separate Dataframes, one containing features and the other containing Labels .
                                On Failure: Raise Exception

                                Written By: <NAME>
                                Version: 1.0
                                Revisions: None

                        """
        self.logger_object.log(self.file_object, "Entered the separate_label_feature method of the Preprocesor class")
        try:
            self.X = data.drop(labels=label_column_name,
                               axis=1)  # drop the columns specified and separate the feature columns
            self.Y = data[label_column_name]  # Filter the label column
            self.logger_object.log(self.file_object,
                                   "Label Separation Successful. Exited the separate_label_features method of the Preprocessor claa")
            return self.X, self.Y
        except Exception as e:
            self.logger_object.log(self.file_object,
                                   'Exception occured in separate_label_feature method of the Preprocessor class. Exception message::  ' + str(
                                       e))
            self.logger_object.log(self.file_object,
                                   'Label Separation Unsuccessful. Exited the separate_label_feature method of the Preprocessor class')
            raise Exception()

    def is_null_present(self, data):
        """
                                        Method Name: is_null_present
                                        Description: This method checks whether there are null values present in the pandas Dataframe or not.
                                        Output: Returns True if null values are present in the DataFrame, False if they are not present and
                                                returns the list of columns for which null values are present.
                                        On Failure: Raise Exception

                                        Written By: <NAME>
                                        Version: 1.0
                                        Revisions: None

                                """
        self.logger_object.log(self.file_object, "Entered the is_null_present method of the Preprocessor class")
        self.null_present = False
        self.cols_with_missing_values = []
        self.col = data.columns

        try:
            self.null_counts = data.isna().sum()  # check for the count of null values per columns
            for i in range(len(self.null_counts)):
                if self.null_counts[i] > 0:
                    self.null_present = True
                    self.cols_with_missing_values.append(self.col[i])
            if self.null_present:  # write the logs to see which columns have null values
                self.dataframe_with_null = pd.DataFrame()
                self.dataframe_with_null['columns'] = data.columns
                self.dataframe_with_null['missing value count'] = np.asarray(data.isna().sum())
                self.dataframe_with_null.to_csv(
                    'preprocessing_data/null_values.csv')  # storing the null column information to file
            self.logger_object.log(self.file_object,
                                   "Finding missing values is a successful .Data written to the null values file. Exited the is_null_present method of the Preprocessor class")
            return self.null_present, self.cols_with_missing_values
        except Exception as e:
            self.logger_object.log(self.file_object,
                                   'Exception occured in is_null_present method of the Preprocessor class. Exception message::  ' + str(
                                       e))
            self.logger_object.log(self.file_object,
                                   'Finding missing values failed. Exited the is_null_present method of the Preprocessor class')
            raise Exception()

    def impute_missing_values(self, data, cols_with_missing_values):
        """
                                                Method Name: impute_missing_values
                                                Description: This method replaces all the missing values in the Dataframe using KNN Imputer.
                                                Output: A Dataframe which has all the missing values imputed.
                                                On Failure: Raise Exception

                                                Written By: <NAME>
                                                Version: 1.0
                                                Revisions: None
                             """
        self.logger_object.log(self.file_object, "Entered the impute_missing_values method of the Preprocessor class")
        self.data = data
        self.cols_with_missing_values = cols_with_missing_values
        try:
            self.imputer = SimpleImputer(strategy="most_frequent")
            for col in self.cols_with_missing_values:
                self.data[col] = self.imputer.fit_transform(self.data[col])
            self.logger_object.log(self.file_object,
                                   'Imputing missing values Successful. Exited the impute_missing_values method of the Preprocessor class')
            return self.data
        except Exception as e:
            self.logger_object.log(self.file_object,
                                   'Exception occured in impute_missing_values method of the Preprocessor class. Exception message::  ' + str(
                                       e))
            self.logger_object.log(self.file_object,
                                   'Imputing missing values failed. Exited the impute_missing_values method of the Preprocessor class')
            raise Exception()

    def scale_numerical_columns(self, data):
        """
                                                                Method Name: scale_numerical_columns
                                                                Description: This method scales the numerical values using the Standard scaler.
                                                                Output: A dataframe with scaled
                                                                On Failure: Raise Exception

                                                                Written By: <NAME>
                                                                Version: 1.0
                                                                Revisions: None
                                             """
        self.logger_object.log(self.file_object, "Entered the scale_numerical_columns method of the Preprocessor class")
        self.data = data
        try:
            self.num_df = self.data.select_dtypes(include=['int64', 'float64']).copy()
            self.scaler = StandardScaler()
            self.scaled_data = self.scaler.fit_transform(self.num_df)
            self.scaled_num_df = pd.DataFrame(data=self.scaled_data, columns=self.num_df.columns)

            self.logger_object.log(self.file_object,
                                   'scaling for numerical values successful. Exited the scale_numerical_columns method of the Preprocessor class')
            return self.scaled_num_df
        except Exception as e:
            self.logger_object.log(self.file_object,
                                   'Exception occured in scale_numerical_columns method of the Preprocessor class. Exception message::  ' + str(
                                       e))
            self.logger_object.log(self.file_object,
                                   'scaling for numerical columns Failed. Exited the scale_numerical_columns method of the Preprocessor class')
            raise Exception()

    def encode_categorical_columns(self, data):
        """
                                                        Method Name: encode_categorical_columns
                                                        Description: This method encodes the categorical values to numeric values.
                                                        Output: only the columns with categorical values converted to numerical values
                                                        On Failure: Raise Exception

                                                        Written By: <NAME>
                                                        Version: 1.0
                                                        Revisions: None
                                     """
        self.logger_object.log(self.file_object,
                               "Entered the encode_categorical_columns method of the Preprocessor class")
        try:
            self.cat_df = data.select_dtypes(include=['object']).copy()
            # using the dummy encoding to encode the categorical columns to numerical ones
            for col in self.cat_df.columns:
                self.cat_df = pd.get_dummies(self.cat_df, columns=[col], prefix=[col], drop_first=True)
            self.logger_object.log(self.file_object,
                                   "encoding for categorical values successful. Exited the encode_categorical_columns method of the Preprocessor class")
            return self.cat_df
        except Exception as e:
            self.logger_object.log(self.file_object,
                                   'Exception occured in encode_categorical_columns method of the Preprocessor class. Exception message:  ' + str(
                                       e))
            self.logger_object.log(self.file_object,
                                   'encoding for categorical columns Failed. Exited the encode_categorical_columns method of the Preprocessor class')
            raise Exception()

    def handle_imbalanced_dataset(self, x, y):
        """
                Method Name: handle_imbalanced_dataset
                Description: This method handles the imbalanced dataset to make it a balanced one.
                Output: new balanced feature and target columns
                On Failure: Raise Exception

                Written By: <NAME>
                Version: 1.0
                Revisions: None
        """
        self.logger_object.log(self.file_object,
                               "Entered the handle_imbalanced_dataset method of the Preprocessor class")
        try:
            self.rdsmple = RandomOverSampler()
            self.x_sampled, self.y_sampled = self.rdsmple.fit_resample(x, y)
            self.logger_object.log(self.file_object,
                                   "dataset balancing successful. Exited the handle_imbalanced_dataset method of the Preprocessor class")
            return self.x_sampled, self.y_sampled
        except Exception as e:
            self.logger_object.log(self.file_object,
                                   'Exception occured in handle_imbalanced_dataset method of the Preprocessor class. Exception message:  ' + str(
                                       e))
            self.logger_object.log(self.file_object,
                                   'dataset balancing Failed. Exited the handle_imbalanced_dataset method of the Preprocessor class')
            raise Exception()

import numpy as np
import pandas as pd
from imblearn.over_sampling import RandomOverSampler
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler


class Preprocessor:
    """
            This class shall  be used to clean and transform the data before training.

            Written By: <NAME>
            Version: 1.0
            Revisions: None

            """

    def __init__(self, file_object, logger_object):
        self.file_object = file_object
        self.logger_object = logger_object

    def remove_unwanted_spaces(self, data):
        """
                                Method Name: remove_unwanted_spaces
                                Description: This method removes the unwanted spaces from a pandas dataframe.
                                Output: A pandas DataFrame after removing the spaces.
                                On Failure: Raise Exception

                                Written By: <NAME>
                                Version: 1.0
                                Revisions: None

                        """
        self.logger_object.log(self.file_object, "Entered the remove_unwanted_spaces method of the Preprocessor class")
        self.data = data
        try:
            self.df_without_spaces = self.data.apply(
                lambda x: x.str.strip() if x.dtype == "object" else x)  # drop the labels specified in the columns
            self.logger_object.log(self.file_object,
                                   "Unwanted spaces removal Successful. Exited the remove_unwanted_spaces method of the Preprocess class")
            return self.df_without_spaces
        except Exception as e:
            self.logger_object.log(self.file_object,
                                   "Exception occured in remove_unwanted_spaces method of the Preprocessor class. Exception message:: " + str(
                                       e))
            self.logger_object.log(self.file_object,
                                   "Unwanted space removal Unsuccessful! Exited the remove_unwanted_spaces method of the Prepcoessor class")
            raise Exception()

    def remove_clumns(self, data, columns):
        """
                        Method Name: remove_columns
                        Description: This method removes the given columns from a pandas dataframe.
                        Output: A pandas DataFrame after removing the specified columns.
                        On Failure: Raise Exception

                        Written By: <NAME>
                        Version: 1.0
                        Revisions: None

                """
        self.logger_object.log(self.file_object, "Entered the remove_columns method of the Preprocessor class")
        self.data = data
        self.columns = columns
        try:
            self.useful_data = self.data.drop(labels=self.columns, axis=1)  # drop the labels specified in the columns
            self.logger_object.log(self.file_object,
                                   'Columns removal Successful. Exited the removal_columns method of the preprocessor class')
            return self.useful_data
        except Exception as e:
            self.logger_object.log(self.file_object,
                                   "Exception occured in remove_columns mehod of the Preprocessor class. Exception:: " + str(
                                       e))
            self.logger_object.log(self.file_object,
                                   'Column removal Unsuccessful. Exited the remove_columns method of the Preprocessor class')
            raise Exception

    def separate_label_features(self, data, label_column_name):
        """
                                Method Name: separate_label_feature
                                Description: This method separates the features and a Label Coulmns.
                                Output: Returns two separate Dataframes, one containing features and the other containing Labels .
                                On Failure: Raise Exception

                                Written By: <NAME>
                                Version: 1.0
                                Revisions: None

                        """
        self.logger_object.log(self.file_object, "Entered the separate_label_feature method of the Preprocesor class")
        try:
            self.X = data.drop(labels=label_column_name,
                               axis=1)  # drop the columns specified and separate the feature columns
            self.Y = data[label_column_name]  # Filter the label column
            self.logger_object.log(self.file_object,
                                   "Label Separation Successful. Exited the separate_label_features method of the Preprocessor claa")
            return self.X, self.Y
        except Exception as e:
            self.logger_object.log(self.file_object,
                                   'Exception occured in separate_label_feature method of the Preprocessor class. Exception message::  ' + str(
                                       e))
            self.logger_object.log(self.file_object,
                                   'Label Separation Unsuccessful. Exited the separate_label_feature method of the Preprocessor class')
            raise Exception()

    def is_null_present(self, data):
        """
                                        Method Name: is_null_present
                                        Description: This method checks whether there are null values present in the pandas Dataframe or not.
                                        Output: Returns True if null values are present in the DataFrame, False if they are not present and
                                                returns the list of columns for which null values are present.
                                        On Failure: Raise Exception

                                        Written By: <NAME>
                                        Version: 1.0
                                        Revisions: None

                                """
        self.logger_object.log(self.file_object, "Entered the is_null_present method of the Preprocessor class")
        self.null_present = False
        self.cols_with_missing_values = []
        self.col = data.columns

        try:
            self.null_counts = data.isna().sum()  # check for the count of null values per columns
            for i in range(len(self.null_counts)):
                if self.null_counts[i] > 0:
                    self.null_present = True
                    self.cols_with_missing_values.append(self.col[i])
            if self.null_present:  # write the logs to see which columns have null values
                self.dataframe_with_null = pd.DataFrame()
                self.dataframe_with_null['columns'] = data.columns
                self.dataframe_with_null['missing value count'] = np.asarray(data.isna().sum())
                self.dataframe_with_null.to_csv(
                    'preprocessing_data/null_values.csv')  # storing the null column information to file
            self.logger_object.log(self.file_object,
                                   "Finding missing values is a successful .Data written to the null values file. Exited the is_null_present method of the Preprocessor class")
            return self.null_present, self.cols_with_missing_values
        except Exception as e:
            self.logger_object.log(self.file_object,
                                   'Exception occured in is_null_present method of the Preprocessor class. Exception message::  ' + str(
                                       e))
            self.logger_object.log(self.file_object,
                                   'Finding missing values failed. Exited the is_null_present method of the Preprocessor class')
            raise Exception()

    def impute_missing_values(self, data, cols_with_missing_values):
        """
                                                Method Name: impute_missing_values
                                                Description: This method replaces all the missing values in the Dataframe using KNN Imputer.
                                                Output: A Dataframe which has all the missing values imputed.
                                                On Failure: Raise Exception

                                                Written By: <NAME>
                                                Version: 1.0
                                                Revisions: None
                             """
        self.logger_object.log(self.file_object, "Entered the impute_missing_values method of the Preprocessor class")
        self.data = data
        self.cols_with_missing_values = cols_with_missing_values
        try:
            self.imputer = SimpleImputer(strategy="most_frequent")
            for col in self.cols_with_missing_values:
                self.data[col] = self.imputer.fit_transform(self.data[col])
            self.logger_object.log(self.file_object,
                                   'Imputing missing values Successful. Exited the impute_missing_values method of the Preprocessor class')
            return self.data
        except Exception as e:
            self.logger_object.log(self.file_object,
                                   'Exception occured in impute_missing_values method of the Preprocessor class. Exception message::  ' + str(
                                       e))
            self.logger_object.log(self.file_object,
                                   'Imputing missing values failed. Exited the impute_missing_values method of the Preprocessor class')
            raise Exception()

    def scale_numerical_columns(self, data):
        """
                                                                Method Name: scale_numerical_columns
                                                                Description: This method scales the numerical values using the Standard scaler.
                                                                Output: A dataframe with scaled
                                                                On Failure: Raise Exception

                                                                Written By: <NAME>
                                                                Version: 1.0
                                                                Revisions: None
                                             """
        self.logger_object.log(self.file_object, "Entered the scale_numerical_columns method of the Preprocessor class")
        self.data = data
        try:
            self.num_df = self.data.select_dtypes(include=['int64', 'float64']).copy()
            self.scaler = StandardScaler()
            self.scaled_data = self.scaler.fit_transform(self.num_df)
            self.scaled_num_df = pd.DataFrame(data=self.scaled_data, columns=self.num_df.columns)

            self.logger_object.log(self.file_object,
                                   'scaling for numerical values successful. Exited the scale_numerical_columns method of the Preprocessor class')
            return self.scaled_num_df
        except Exception as e:
            self.logger_object.log(self.file_object,
                                   'Exception occured in scale_numerical_columns method of the Preprocessor class. Exception message::  ' + str(
                                       e))
            self.logger_object.log(self.file_object,
                                   'scaling for numerical columns Failed. Exited the scale_numerical_columns method of the Preprocessor class')
            raise Exception()

    def encode_categorical_columns(self, data):
        """
                                                        Method Name: encode_categorical_columns
                                                        Description: This method encodes the categorical values to numeric values.
                                                        Output: only the columns with categorical values converted to numerical values
                                                        On Failure: Raise Exception

                                                        Written By: <NAME>
                                                        Version: 1.0
                                                        Revisions: None
                                     """
        self.logger_object.log(self.file_object,
                               "Entered the encode_categorical_columns method of the Preprocessor class")
        try:
            self.cat_df = data.select_dtypes(include=['object']).copy()
            # using the dummy encoding to encode the categorical columns to numerical ones
            for col in self.cat_df.columns:
                self.cat_df = pd.get_dummies(self.cat_df, columns=[col], prefix=[col], drop_first=True)
            self.logger_object.log(self.file_object,
                                   "encoding for categorical values successful. Exited the encode_categorical_columns method of the Preprocessor class")
            return self.cat_df
        except Exception as e:
            self.logger_object.log(self.file_object,
                                   'Exception occured in encode_categorical_columns method of the Preprocessor class. Exception message:  ' + str(
                                       e))
            self.logger_object.log(self.file_object,
                                   'encoding for categorical columns Failed. Exited the encode_categorical_columns method of the Preprocessor class')
            raise Exception()

    def handle_imbalanced_dataset(self, x, y):
        """
                Method Name: handle_imbalanced_dataset
                Description: This method handles the imbalanced dataset to make it a balanced one.
                Output: new balanced feature and target columns
                On Failure: Raise Exception

                Written By: <NAME>
                Version: 1.0
                Revisions: None
        """
        self.logger_object.log(self.file_object,
                               "Entered the handle_imbalanced_dataset method of the Preprocessor class")
        try:
            self.rdsmple = RandomOverSampler()
            self.x_sampled, self.y_sampled = self.rdsmple.fit_resample(x, y)
            self.logger_object.log(self.file_object,
                                   "dataset balancing successful. Exited the handle_imbalanced_dataset method of the Preprocessor class")
            return self.x_sampled, self.y_sampled
        except Exception as e:
            self.logger_object.log(self.file_object,
                                   'Exception occured in handle_imbalanced_dataset method of the Preprocessor class. Exception message:  ' + str(
                                       e))
            self.logger_object.log(self.file_object,
                                   'dataset balancing Failed. Exited the handle_imbalanced_dataset method of the Preprocessor class')
            raise Exception()

This class shall  be used to clean and transform the data before training.              Written By: <NAME>             Version: 1.0             Revisions: None Method Name: remove_unwanted_spaces                                 Description: This method removes the unwanted spaces from a pandas dataframe.                                 Output: A pandas DataFrame after removing the spaces.                                 On Failure: Raise Exception                                  Written By: <NAME>                                 Version: 1.0                                 Revisions: None # drop the labels specified in the columns Method Name: remove_columns                         Description: This method removes the given columns from a pandas dataframe.                         Output: A pandas DataFrame after removing the specified columns.                         On Failure: Raise Exception                          Written By: <NAME>                         Version: 1.0                         Revisions: None # drop the labels specified in the columns Method Name: separate_label_feature                                 Description: This method separates the features and a Label Coulmns.                                 Output: Returns two separate Dataframes, one containing features and the other containing Labels .                                 On Failure: Raise Exception                                  Written By: <NAME>                                 Version: 1.0                                 Revisions: None # drop the columns specified and separate the feature columns # Filter the label column Method Name: is_null_present                                         Description: This method checks whether there are null values present in the pandas Dataframe or not.                                         Output: Returns True if null values are present in the DataFrame, False if they are not present and                                                 returns the list of columns for which null values are present.                                         On Failure: Raise Exception                                          Written By: <NAME>                                         Version: 1.0                                         Revisions: None # check for the count of null values per columns # write the logs to see which columns have null values # storing the null column information to file Method Name: impute_missing_values                                                 Description: This method replaces all the missing values in the Dataframe using KNN Imputer.                                                 Output: A Dataframe which has all the missing values imputed.                                                 On Failure: Raise Exception                                                  Written By: <NAME>                                                 Version: 1.0                                                 Revisions: None Method Name: scale_numerical_columns                                                                 Description: This method scales the numerical values using the Standard scaler.                                                                 Output: A dataframe with scaled                                                                 On Failure: Raise Exception                                                                  Written By: <NAME>                                                                 Version: 1.0                                                                 Revisions: None Method Name: encode_categorical_columns                                                         Description: This method encodes the categorical values to numeric values.                                                         Output: only the columns with categorical values converted to numerical values                                                         On Failure: Raise Exception                                                          Written By: <NAME>                                                         Version: 1.0                                                         Revisions: None # using the dummy encoding to encode the categorical columns to numerical ones Method Name: handle_imbalanced_dataset                 Description: This method handles the imbalanced dataset to make it a balanced one.                 Output: new balanced feature and target columns                 On Failure: Raise Exception                  Written By: <NAME>                 Version: 1.0                 Revisions: None