import csv
import math
import random
from collections import defaultdict

# retorna uma função que tenta converter uma string
#    em int
#    se não conseguir, retorna o valor associado à string
#    no map not_int_dict
def try_int(not_int_dict):
    
    def _try_int(x):
        
        try:
            return int(x)
        except:
            return not_int_dict[x]
    return _try_int

# lista de funções de mapeamento dos valores string em numérico
map_classe_into_int = {'vgood': 4, 'good': 3, 'acc': 2, 'unacc': 1}
map_int_into_classe = {v:k for k, v in map_classe_into_int.items()}
preprocess_list = [
    lambda x: {'vhigh': 4, 'high': 3, 'med': 2, 'low': 1}[x],
    lambda x: {'vhigh': 4, 'high': 3, 'med': 2, 'low': 1}[x],
    try_int({'5more': 6}),
    try_int({'more': 5}),
    lambda x: {'big': 3, 'med': 2, 'small': 1}[x],
    lambda x: {'high': 3, 'med': 2, 'low': 1}[x],
    lambda x: map_classe_into_int[x]
]

# converte uma linha contendo valores string em valores numéricos
def preprocess_row(row):
    
    return [f(v) for f, v in zip(preprocess_list, row)]

def loadCsv(filename):
    rows = csv.reader(open(filename, "r"))
    ds = [preprocess_row(row) for row in rows]

    return ds

def splitDataset(dataset, splitRatio):
    trainSize = int(len(dataset) * splitRatio)
    trainSet = []
    copy = list(dataset)
    while len(trainSet) < trainSize:
        index = random.randrange(len(copy))
        trainSet.append(copy.pop(index))
    return [trainSet, copy]

def separateByClass(dataset):
    separated = {}
    for i in range(len(dataset)):
        vector = dataset[i]
        if (vector[-1] not in separated):
            separated[vector[-1]] = []
        separated[vector[-1]].append(vector)
    return separated

def mean(numbers):
    return sum(numbers)/float(len(numbers))
 
def stdev(numbers):
    avg = mean(numbers)
    variance = sum([pow(x-avg,2) for x in numbers])/float(len(numbers)-1)
    return math.sqrt(variance)

def summarize(dataset):
    summaries = [(mean(attribute), stdev(attribute)) for attribute in zip(*dataset)]
    del summaries[-1]
    return summaries

def summarizeByClass(dataset):
    separated = separateByClass(dataset)
    summaries = {}
    for classValue, instances in separated.items():
        summaries[classValue] = summarize(instances)
    return summaries

def calculateProbability(x, mean, stdev):
    
    if stdev == 0:
        return 1. if x == mean else .1        
    
    exponent = math.exp(-(x-mean)**2/(2*stdev**2))
    return (1 / ((2*math.pi) * stdev ** 2)**.5) * exponent

def calculateClassProbabilities(summaries, inputVector):
    probabilities = {}
    for classValue, classSummaries in summaries.items():
        probabilities[classValue] = 1
        for i in range(len(classSummaries)):
            mean, stdev = classSummaries[i]
            x = inputVector[i]
            probabilities[classValue] *= calculateProbability(x, mean, stdev)
    return probabilities

def predict(summaries, inputVector):
    probabilities = calculateClassProbabilities(summaries, inputVector)
    bestLabel, bestProb = None, -1
    for classValue, probability in probabilities.items():
        if bestLabel is None or probability > bestProb:
            bestProb = probability
            bestLabel = classValue
    return bestLabel, bestProb

def getPredictions(summaries, testSet):
    predictions = []
    probs = []
    for i in range(len(testSet)):
        result, prob = predict(summaries, testSet[i])
        predictions.append(result)
        probs.append(prob)
    return predictions, probs

def getAccuracy(testSet, predictions):
    correct = 0
    for i in range(len(testSet)):
        if testSet[i][-1] == predictions[i]:
            correct += 1
    return (correct/float(len(testSet))) * 100.0

def train_and_test(filepath):
    
    accs = []
    
    ds = loadCsv(filepath)
    
    for i in range(100):

        train, test = splitDataset(ds, splitRatio=.67)
        summaries = summarizeByClass(train)

        predictions, _ = getPredictions(summaries, test)
        
        accs.append(getAccuracy(test, predictions))
    
    return accs

accs = train_and_test("carData.csv")

import pandas as pd
pd.Series(accs).describe().to_frame().T

"""
Explanation: Naive Bayes
Questão 1
Implemente um classifador 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
Modelo 1
Mapeando features em numéricos e utilizando distribuição normal
End of explanation
"""


# leitura do arquivo
df = pd.read_csv("carData.csv", header=None)

# índice das colunas de features
f_cols = list(range(0, 6))

# frequência de registros por classe
freq_classes = df.groupby(6).size()

# classes
classes = df[6].unique()

# probabilidade atribuída às combinações de feature/classe
#    que não possuem exemplos no dataset
DEFAULT_NO_FREQ_PROB = 0.01

def fit(df):
    # probabilidades estimadas como #(feature, classe)/#classe
    base_probs = {}

    # para cada feature
    for f_col in f_cols:

        # matriz de probabilidades da feature x classes
        #              agrupa pela classe e pela feature
        #              manda índice da feature para as colunas
        #              divide pela frequência das classes
        #              preenche os missing values(pares não presentes no dataset)
        base_probs[f_col] = df.groupby([6, f_col]).size()\
                         .unstack() \
                         .div(freq_classes, axis=0)\
                         .fillna(DEFAULT_NO_FREQ_PROB) 

    # função que realiza a predição sobre um dataset
    #    a partir das probabilidades estimadas no passo anterior
    def predict(ds):

        predictions = []
        probs = []
        # para cada exemplo
        for ix, row in ds.iterrows():
            prob = {}
            # para cada classe
            for classe in classes:
                # calcula a probabilidade do exemplo estar associado à classe
                prob[classe] = 1.
                # como o produto das probabilidades de cada uma de suas features pertencer à classe
                for f_col in f_cols:
                    prob[classe] = prob[classe] * base_probs[f_col].loc[classe, str(row[f_col])]
            
            predictions.append(max(prob.keys(), key=lambda key: prob[key]))
            probs.append(prob)

        return probs, predictions
    
    return predict

from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

accs = []

for x in range(100):

    # particiona o dataset em treino(77%) e test(33%)
    df_train, df_test = train_test_split(df, test_size=.33)
    
    # treina
    nb = fit(df_train)
    # prediz
    _, predictions = nb(df_test)
    
    # calcula acurácia
    acc = accuracy_score(df_test[6], predictions)
    accs.append(acc)

pd.Series(accs).describe().to_frame().T

"""
Explanation: Modelo utilizando frequência dos valores das features
End of explanation
"""


from sklearn.naive_bayes import GaussianNB
import numpy as np
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import confusion_matrix

ds = loadCsv("carData.csv")
df = pd.DataFrame(ds, columns=range(7))

X = df[list(range(6))]
y = df[6]

accs = []

for x in range(100):

    # particiona dataset em treino(77%) e teste(33%)
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.33)
    
    nb = GaussianNB()
    # prediz
    nb.fit(X_train, y_train)
    
    # calcula acurácia
    acc = accuracy_score(y_test, nb.predict(X_test))
    accs.append(acc)
    
pd.Series(accs).describe().to_frame().T

"""
Explanation: 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)
End of explanation
"""


ix_train, ix_test = train_test_split(np.arange(df.shape[0]), test_size=.33, random_state=100)

# modelo 1
#    estimativa de probabilidade utilizando a frequência das features
df_train, df_test = df.iloc[ix_train, :], df.iloc[ix_test, :]

nb1 = fit(df_train)
probs_1, predictions_1 = nb1(df_test)
probs_1 = pd.DataFrame(probs_1, index=df_test.index)

# modelo 2
#    estimativa de probabilidade utilizando distribuição normal das features
ds = pd.DataFrame(loadCsv("carData.csv"), columns=range(7))
X_train, y_train, X_test, y_test = ds.iloc[ix_train, list(range(6))], ds.iloc[ix_train, 6],\
                                        ds.iloc[ix_test, list(range(6))], ds.iloc[ix_test, 6]
    
nb2 = GaussianNB().fit(X_train, y_train)
predictions_2 = [map_int_into_classe[p] for p in nb2.predict(X_test)]
probs_2 = nb2.predict_proba(X_test)
probs_2 = pd.DataFrame(probs_2, columns=[map_int_into_classe[v] for v in nb2.classes_], index=df_test.index)

# juntando as predições
predictions = pd.DataFrame({'model_1': predictions_1, 'model_2': predictions_2, 'correct': df_test.loc[:, 6]}, index=df_test.index)
predictions = pd.merge(df_test, predictions, left_index=True, right_index=True)
predictions.loc[:, 'model_1'] = predictions.model_1.astype('category', categories=['unacc', 'acc', 'good', 'vgood'], ordered=True)
predictions.loc[:, 'model_2'] = predictions.model_2.astype('category', categories=['unacc', 'acc', 'good', 'vgood'], ordered=True)

"""
Explanation: Questão 3
Analise a acurácia dos dois algoritmos e discuta a sua solução.
Comparando os dois últimos modelos
End of explanation
"""


predictions.groupby(['model_1', 'model_2']).size().unstack().fillna(0)

"""
Explanation: O que deu de diferente?
End of explanation
"""


predictions[(predictions.model_1 == 'vgood') & (predictions.model_2 == 'unacc')].sample()

probs_1.loc[1662]

probs_2.loc[1662]

# ter a feature lug_boot = big parece contribuir bastante para a probabilidade do modelo 1
#    dado que a frequência de lug_boot = big na classe vgood é bastante alta(68%)
df_train.groupby([6, 4]).size().unstack().div(df_train.groupby(6).size(), axis=0).sort_values('big', ascending=False)

"""
Explanation: Analisando um caso em que model_1 classificou como muito bom e model_2 classificou como muito ruim
End of explanation
"""


predictions[(predictions.model_2 == 'vgood') & (predictions.model_1 == 'unacc')]

"""
Explanation: Analisando um caso em que model_2 classificou como muito bom e model_1 classificou como muito ruim
End of explanation
"""


predictions[(predictions.model_2 == 'unacc') & (predictions.model_1 == 'unacc') & (predictions.correct == "vgood")]

"""
Explanation: Analisando um caso em que ambos classificaram como muito ruim quando era muito bom
End of explanation
"""


predictions[(predictions.model_2 == 'vgood') & (predictions.model_1 == 'vgood') & (predictions.correct == "unacc")]

# observar que a frequência de exemplos da classe vgood é bem menor que da classe unacc
df_train.groupby(6).size()

"""
Explanation: Analisando um caso em que ambos classificaram como muito bom quando era muito ruim
End of explanation
"""


