# Libraries
import numpy as np
import pandas as pd
from sklearn.naive_bayes import MultinomialNB, GaussianNB
from sklearn.cross_validation import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score, classification_report

# Creating a class for the Naive Bayes
class NaiveBayes:
    def __init__(self):
        ''' Default Constructor '''
        self.lEncoder = LabelEncoder()
        self.X = None; self.y = None
        self.classProb = None; self.likeTable = {}
    
    def separateByClass(self):
        ''' This functions separates all the dataset indexing dictionaries by the classes '''
        separated = {}
        for i in range(len(self.y)):
            if (self.y[i] not in separated):
                separated[self.y[i]] = []
            separated[self.y[i]].append(self.X[i])
        return separated
    
    def makeLikeTable(self):
        ''' This functions counts the occurences of each attribute based on the classes
        and construct the Likelihood table (in this case a Dictionary) calculating all
        the propers probabilities '''
        separateClass = self.separateByClass()
        classSizes = [len(separateClass[i]) for i in separateClass.keys()] 
        self.classProb = np.array(classSizes) / sum(classSizes)
        
        self.likeTable = {}
        for label in separateClass.keys():
            auxiliar = np.column_stack(separateClass[label])
            for attribute,idx in zip(auxiliar, range(len(auxiliar))):
                counts = np.asarray(np.unique(attribute, return_counts=True)).T
                for i in range(4):
                    self.likeTable[(label, idx, i)] = 0
                for count_it in counts:
                    self.likeTable[(label, idx, count_it[0])] = count_it[1] / len(separateClass[label])

    def calculateProbability(self, inputVector):
        ''' Utilizes the maximum likelihood estimation to calculate the probabilty of
        each row in inputVector belongs to each possible class '''
        separateClass = self.separateByClass()
        
        probabilities = {}
        for label,_ in separateClass.items():
            probabilities[label] = self.classProb[label]
            for i in range(len(inputVector)):
                probabilities[label] *= self.likeTable[label, i, inputVector[i]]
                
        return probabilities

    def fit(self, X_train, y_train):
        ''' Assign the training data and calls the Likelihood Table creator '''
        self.X = X_train
        self.y = y_train
        
        self.makeLikeTable()
    
    def predict(self, inputArray):
        ''' Return a list of predictions for each row in inputArray correspondent to
        the label of the class with the maximum probability '''
        predictions = []
        for row in inputArray:
            probabilities = self.calculateProbability(row)
            predictions.append(max(probabilities, key=probabilities.get))
        return predictions

"""
Explanation: Naive Bayes - Trabalho
Questão 1
Implemente um classifacor Naive Bayes para o problema de predizer a qualidade de um carro. Para este fim, utilizaremos um conjunto de dados referente a qualidade de carros, disponível no UCI. Este dataset de carros possui as seguintes features e classe:
 Attributos 
1. buying: vhigh, high, med, low
2. maint: vhigh, high, med, low
3. doors: 2, 3, 4, 5, more
4. persons: 2, 4, more
5. lug_boot: small, med, big
6. safety: low, med, high
 Classes 
1. unacc, acc, good, vgood
Questão 2
Crie uma versão de sua implementação usando as funções disponíveis na biblioteca SciKitLearn para o Naive Bayes (veja aqui) 
Questão 3
Analise a acurácia dos dois algoritmos e discuta a sua solução.
Questão 1
End of explanation
"""


# Carregando o Dataset
data = pd.read_csv("car.data", header=None)

# Transformando as variáveis categóricas em valores discretos contáveis
# Apesar de diminuir a interpretação dos atributos, essa medida facilita bastante a vida dos métodos de contagem
for i in range(0, data.shape[1]):
    data.iloc[:,i] = LabelEncoder().fit_transform(data.iloc[:,i])

# Separação do Conjunto de Treino e Conjunto de Teste (80%/20%)
X_train, X_test, y_train, y_test = train_test_split(data.iloc[:,:-1], data.iloc[:,-1], test_size=0.2)

# Implementação do Naive Bayes Multinomial do SKLearn
# O Naive Bayes Multinomial é a implementação do SKLearn que funciona para variáveis discretas,
# ao invés de utilizar o modelo da função gaussiana (a classe GaussianNB faz dessa forma)
clf = MultinomialNB()
clf.fit(X_train.values, y_train.values)

y_pred = clf.predict(X_test.values)

# Impressão dos Resultados
print("Multinomial Naive Bayes (SKlearn Version)")
print("Total Accuracy: {}%".format(accuracy_score(y_true=y_test, y_pred=y_pred)))

print("\nClassification Report:")
print(classification_report(y_true=y_test, y_pred=y_pred, target_names=["unacc", "acc", "good", "vgood"]))

"""
Explanation: Questão 2
End of explanation
"""


# Utilização da minha função própria de Naive Bayes para o mesmo conjunto de dados
nBayes = NaiveBayes()
nBayes.fit(X_train.values, y_train.values)
y_pred = nBayes.predict(X_test.values)

# Impressão dos Resultados
print("Naive Bayes (My Version :D)")
print("Total Accuracy: {}%".format(accuracy_score(y_true=y_test, y_pred=y_pred)))

print("\nClassification Report:")
print(classification_report(y_true=y_test, y_pred=y_pred, target_names=["unacc", "acc", "good", "vgood"]))

"""
Explanation: Questão 3
End of explanation
"""


