# AUTOGENERATED! DO NOT EDIT! File to edit: 00_dimensionReducer.ipynb (unless otherwise specified).

__all__ = ['logger', 'dimensionReducer', 'fitting_PCA', 'fitting_1layer_encoder', 'fitting_2layer_encoder',
           'calculate_rmse', 'plot_output']

# Cell
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pylab as plt

import logging
logger = logging.getLogger(__name__)

from sklearn.preprocessing import StandardScaler

# Cell
class dimensionReducer:

    def __init__(self, input_data, reduce_dim, learning_rate = 0.005):
        """
        Intialize the dimension reducer
        : param input_data: (pd.DataFrame or numpy.ndarray) An input data that will be used for dimension reduction
        : param reduce_dim: Number of the output's dimension (reduced dimension)
        : param learning_rate: the learning rate for the Adam optimization during the autoencoder model fitting
        : return None
        """
        self.reduce_dim = reduce_dim
        self.X = input_data
        self.learning_rate = learning_rate
        return None

    def validateInputs(self):
        """
        Validating the input
        : is it pd.DataFrame or numpy.ndarray?
        : any missing data/NaN/null?
        """
        if (type(self.X) != np.ndarray) or (type(self.X) != pd.core.frame.DataFrame) :
            return True
        else:
            raise ValueError('Incorrect input type: the input must be either numpy.ndarray or pandas.DataFrame')


    def fit(self):
        """
        Fit Transform PCA and Autoencoder
        """

        if (self.validateInputs()):

            ### Standardization
            # StandardScaler: this is necessary since PC's are obtained by maximizing the variation within the data.
            # sklearn.preprocessing.StandardScaler - z = (x - u) / s
            # Why Scaling instead of normalizing?
            # - https://stats.stackexchange.com/questions/385775/normalizing-vs-scaling-before-pca
            # - https://scikit-learn.org/stable/auto_examples/preprocessing/plot_all_scaling.html
            scaler = StandardScaler()
            X_scale = scaler.fit_transform(self.X)
            self.X_scale = X_scale

            ### Fitting PCA
            self.X_PCA_reconstruct, self.dfLowDimPCA = fitting_PCA(X_scale, self.reduce_dim)

            ### Fitting 1-layer AutoEncoder
            self.X_1AE_reconstruct, self.dfLowDim1AE, self.model1AE, self.hist1AE = fitting_1layer_encoder(X_scale, self.reduce_dim, self.learning_rate)

            ### Fitting 2-layer AutoEncoder
            self.X_2AE_reconstruct, self.dfLowDim2AE, self.model2AE, self.hist2AE = fitting_2layer_encoder(X_scale, self.reduce_dim, self.learning_rate)


            rmse_PCA = calculate_rmse(X_scale, self.X_PCA_reconstruct)
            rmse_1AE = calculate_rmse(X_scale, self.X_1AE_reconstruct)
            rmse_2AE = calculate_rmse(X_scale, self.X_2AE_reconstruct)
            self.rmse_result = pd.DataFrame({"PCA":rmse_PCA, "1AE": rmse_1AE, "2AE":rmse_2AE}, index=["MSE"])

            return None


    def plot_autoencoder_performance(self):

        hist1 = self.hist1AE
        plt.figure(figsize=(8, 4))
        plt.plot(hist1.history['loss'])
        plt.plot(hist1.history['val_loss'])
        plt.title('Model 1-layer AE: loss')
        plt.ylabel('loss'); plt.xlabel('epoch')
        plt.legend(['Training_MSE', 'Validation_MSE'], loc='upper right')

        hist2 = self.hist2AE
        plt.figure(figsize=(8, 4))
        plt.plot(hist2.history['loss'])
        plt.plot(hist2.history['val_loss'])
        plt.title('Model 2-layer AE: loss')
        plt.ylabel('loss'); plt.xlabel('epoch')
        plt.legend(['Training_MSE', 'Validation_MSE'], loc='upper right')


# Cell
from sklearn.decomposition import PCA

def fitting_PCA(X, reduce_dim):

    pca = PCA(n_components=reduce_dim)
    X_PCA_transform = pca.fit_transform(X)
    X_PCA_reconstruct = pca.inverse_transform(X_PCA_transform)
    dfLowDimPCA = pd.DataFrame(data = X_PCA_transform, columns=list(range(X_PCA_transform.shape[1])))

    return X_PCA_reconstruct, dfLowDimPCA

# Cell
from keras.layers import Input, Dense
from keras.models import Model
from keras.callbacks import EarlyStopping, ModelCheckpoint
from keras.optimizers import Adam

# Cell
def fitting_1layer_encoder(X, reduce_dim, learning_rate):

    in_out_dimension = X.shape[1]

    # Model Construction
    input_layer = Input(shape=(in_out_dimension,))
    encoded = Dense(reduce_dim, activation='linear')(input_layer)
    decoded = Dense(in_out_dimension, activation=None)(encoded)

    # Autoencoder Model
    autoencoder = Model(input_layer, decoded)
    encoder = Model(input_layer, encoded)

    # Compiling the model
    autoencoder.compile(optimizer=Adam(lr=learning_rate), loss='mean_squared_error')

    # Fitting the model
    history = autoencoder.fit(X, X, epochs=1000, batch_size=32, shuffle=True, verbose=0, validation_split=0.2
                              , callbacks = [EarlyStopping(monitor='val_loss', patience=50)
                                             ,ModelCheckpoint(filepath='best_model.h5'
                                                              , monitor='val_loss'
                                                              , save_best_only=True)])

    # output: reduced dimension data and reconstruction data
    encoded_dim = encoder.predict(X)
    dfLowDim1AE = pd.DataFrame(data = encoded_dim, columns=list(range(encoded_dim.shape[1])))
    decoded_dim = autoencoder.predict(X)


    return decoded_dim, dfLowDim1AE, autoencoder, history

# Cell
def fitting_2layer_encoder(X, reduce_dim, learning_rate):

    in_out_dimension = X.shape[1]
    half_dimension = int((in_out_dimension - reduce_dim) / 2)

    # Model Construction
    input_layer = Input(shape=(in_out_dimension,))
    encoded1 = Dense(half_dimension, activation='relu')(input_layer)
    encoded2 = Dense(reduce_dim, activation='relu')(encoded1)
    decoded1 = Dense(half_dimension, activation='relu')(encoded2)
    decoded2 = Dense(in_out_dimension, activation=None)(decoded1)

    # Autoencoder Model
    autoencoder = Model(input_layer, decoded2)
    encoder = Model(input_layer, encoded2)

    # Compiling the model
    autoencoder.compile(optimizer=Adam(lr=learning_rate), loss='mean_squared_error')

    # Fitting the model
    history = autoencoder.fit(X, X, epochs=1000, batch_size=32, shuffle=True, verbose=0, validation_split=0.2
                              , callbacks = [EarlyStopping(monitor='val_loss', patience=50)
                                             ,ModelCheckpoint(filepath='best_model.h5'
                                                              , monitor='val_loss'
                                                              , save_best_only=True)])

    # output: reduced dimension data and reconstruction data
    encoded_dim = encoder.predict(X)
    dfLowDim2AE = pd.DataFrame(data = encoded_dim, columns=list(range(encoded_dim.shape[1])))
    decoded_dim = autoencoder.predict(X)

    return decoded_dim, dfLowDim2AE, autoencoder, history

# Cell
def calculate_rmse(np_arr1, np_arr2):
    """ Calculating the Root Mean Squared Error"""
    return ((((np_arr1 - np_arr2)**2).sum()) / (np_arr1.shape[0] * np_arr1.shape[1]) )

# Cell
def plot_output(df):
    """
    Visualize the output.
    """
    if (len(df.columns) != 2):
        raise ValueError("The embedding must be 2-dimensional vectors.")

    plt.figure(figsize=(8, 8))
    plt.scatter(df[0], df[1])
    plt.title("2D Projection of the data")