import pandas as pd
tweets = pd.read_csv('./data/text_emotion.csv')
tweets.head()

clases = pd.DataFrame(tweets['sentiment'].value_counts().reset_index())
clases.columns = ["sentiment", "count"]
clases

"""
Explanation: 3. Análisis de emociones en tweets
a) Construya un dataframe con los datos a analizar. Determine cuántas clases existen, cuántos registros por clase y describa el dataset.
End of explanation
"""


from nltk.stem import PorterStemmer
from nltk.tokenize import sent_tokenize, TweetTokenizer
from nltk.corpus import stopwords
import nltk
import re

nltk.download('punkt')
nltk.download('stopwords')

df = tweets.copy()

ps = PorterStemmer()
sw = stopwords.words('english')
tknzr = TweetTokenizer(strip_handles=True, reduce_len=True)

df['norm_content'] = df['content'].apply(lambda x: tknzr.tokenize(x.lower()))
df['norm_content'] = df['norm_content'].apply(lambda x: [w for w in x if (w not in sw and len(w)>2)])
df['norm_content'] = df['norm_content'].apply(lambda x: [ps.stem(w) for w in x])
df.head()

"""
Explanation: El dataset cuenta con cuatro columnas, las que corresponden a el id del tweet, el autor y el contenido de este, junto con el sentimiento asociado al contenido.
Los sentimientos encontrados son 13, contando "neutral" como uno de ellos.
b) Construya un conjunto de entrenamiento y otro de pruebas, a través de una máscara aleatoria, para verificar los resultados de los algoritmos.
La máscara se aplicará después de pre-procesar el dataset y antes de entrenar el clasificador
c) Implemente y explique un pre-procesamiento para los tweets para dejarlos en un formato estándarizado en el cual se podrán trabajar.
End of explanation
"""


import numpy as np
np.random.seed(14)
msk = np.random.rand(len(df)) < 0.8

# manual preprocess
df_train = df[msk].copy()
df_test = df[~msk].copy()

# default countvector preprocess
df_train_df = df[msk].copy()

import matplotlib.pyplot as plt

length = df_train['norm_content'].map(len)

plt.figure(figsize=(20,10))
plt.hist(length, bins=(max(length)-min(length)+1), align='right')
plt.title("Tokens count distribution")
plt.xlabel("Lenght")
plt.ylabel("Frequency")
plt.xticks(np.arange(min(length), max(length)+1, 1))
plt.show()

length = df_train['norm_content'].map(len)
df_train = df_train[np.abs(length-length.mean())<=(3*length.std())] 
length = df_train['norm_content'].map(len)

plt.figure(figsize=(20,10))
plt.hist(length, bins=(max(length)-min(length)+1), align='right')
plt.title("Tokens count distribution")
plt.xlabel("Lenght")
plt.ylabel("Frequency")
plt.xticks(np.arange(min(length), max(length)+1, 1))
plt.show()

"""
Explanation: Ahora se hace la separación de test - train
End of explanation
"""


pos = ['neutral', 'happiness', 'love', 'surprise', 'fun', 'relief', 'enthusiasm']

df_train['clase'] = df_train['sentiment'].apply(lambda x: 1 if x in pos else -1)
df_train_df['clase'] = df_train_df['sentiment'].apply(lambda x: 1 if x in pos else -1)

df_test['clase'] = df_test['sentiment'].apply(lambda x: 1 if x in pos else -1)

print(df_train_df['clase'].value_counts()+df_test['clase'].value_counts())
df.head()

"""
Explanation: d) Haga una reducción binaria al problema, para trabajarlo como un problema de clasificación de dos clases. Para esto, agrupe las distintas emociones como positivas y negativas (defina un criterio), se recomienda codificar las clases como +1 y −1 respectivamente. Recuerde tener presente que el desbalanceo de los datos puede afectar considerablemente al modelo.
End of explanation
"""


from sklearn.feature_extraction.text import CountVectorizer

def echo(args):
    return args

# Pre-proceso propio
vectorizer = CountVectorizer(tokenizer=echo, preprocessor=echo, lowercase=False)
X_train = vectorizer.fit_transform(df_train['norm_content'])
X_test = vectorizer.transform(df_test['norm_content'])

# Pre-proseso por default de CountVectorizer
vectorizer_df = CountVectorizer()
X_train_df = vectorizer_df.fit_transform(df_train_df['content'])
X_test_df = vectorizer_df.transform(df_test['content'])

"""
Explanation: e) Construir un clasificador que determine automáticamente la polaridad de un trozo de texto.
End of explanation
"""


def train(name, model, X_train, y_train, X_test, y_test):
    model.fit(X_train, y_train)
    score_train = model.score(X_train, y_train)
    score_test = model.score(X_test, y_test)
    
    print(name+" scores:    \t train: %.3f \t test: %.3f"%(score_train,score_test))

    return (name, model, score_train, score_test)

from sklearn.naive_bayes import BernoulliNB
naive_bayes = train("Naive Bayes",BernoulliNB(), X_train, df_train['clase'], X_test, df_test['clase'])
naive_bayes_df = train("Naive Bayes df",BernoulliNB(), X_train_df, df_train_df['clase'], X_test_df, df_test['clase'])

from sklearn.naive_bayes import MultinomialNB
multi_naive_bayes = train("Multi Naive Bayes",MultinomialNB(), X_train, df_train['clase'], X_test, df_test['clase'])
multi_naive_bayes_df = train("Multi Naive Bayes df", MultinomialNB(), X_train_df, df_train_df['clase'], X_test_df, df_test['clase'])

from sklearn.linear_model import LogisticRegression
logistic = train("LogisticRegression",LogisticRegression(), X_train, df_train['clase'], X_test, df_test['clase'])
logistic_df = train("LogisticRegression df",LogisticRegression(), X_train_df, df_train_df['clase'], X_test_df, df_test['clase'])

from sklearn.tree import DecisionTreeClassifier
decision_tree = train("DecisionTree",DecisionTreeClassifier(), X_train, df_train['clase'], X_test, df_test['clase'])
decision_tree_df = train("DecisionTree df",DecisionTreeClassifier(), X_train_df, df_train_df['clase'], X_test_df, df_test['clase'])

from sklearn.linear_model import SGDClassifier
gradient_descent = train("SGD",SGDClassifier(max_iter=5,tol=None), X_train, df_train['clase'], X_test, df_test['clase'])
gradient_descent_df = train("SGD df",SGDClassifier(max_iter=5,tol=None), X_train_df, df_train_df['clase'], X_test_df, df_test['clase'])

import matplotlib.pyplot as plt
def plot_scores(title,models):
    N = len(models)
    ind = np.arange(N)  # the x locations for the groups
    width = 0.45         # the width of the bars

    fig, ax = plt.subplots()

    test_scores = list(m[2] for m in models)
    train_scores = list(m[3] for m in models)

    rects1 = ax.bar(ind, test_scores, width, color='#00b1ff')
    rects1b = ax.bar(ind+width, train_scores, width, color='#0070ff')

    # add some text for labels, title and axes ticks
    ax.set_ylabel('Score')
    ax.set_title(title)
    ax.set_xticks(ind + width/2)
    ax.set_xticklabels((m[0] for m in models),fontsize='xx-small')

    ax.legend((rects1[0], rects1b[0]), ('Train', 'Test'), loc='lower right')


    def autolabel(rects):
        for rect in rects:
            height = rect.get_height()
            ax.text(rect.get_x() + rect.get_width()/2., height,
                    '%.1f%%' % (100*height),
                    ha='center', va='bottom', fontsize='x-small')

    autolabel(rects1)
    autolabel(rects1b)
    fig.set_dpi(170)

    plt.show()

plot_scores("Pre-procesamiento manual",[naive_bayes, multi_naive_bayes, logistic, decision_tree, gradient_descent])
plot_scores("Pre-procesamiento automático",[naive_bayes_df, multi_naive_bayes_df, logistic_df, decision_tree_df, gradient_descent_df])

"""
Explanation: f) Entrene y compare al menos 5 de los diferentes clasificadores vistos en clases para clasificación binaria (por ejemplo: Navie Bayes, Multinomial Naive Bayes, LDA, QDA, Regresión logística, SVM y Arboles de decisión) sobre el conjunto de entrenamiento verificando su desempeño sobre ambos conjuntos (entrenamiento y de pruebas), construyendo un gráfico resumen del error de éstos.
End of explanation
"""


from sklearn.metrics import classification_report
def score_report(model, X, y):
    name = model[0]
    acc_tr = model[2]
    acc_test = model[3]
    print("Scores for "+name+":")
    print("\tTraining Accuracy: %f"%(acc_tr))
    print("\tTest Accuracy: %f"%(acc_test))
    print("\tDetailed Analysis Testing Results ...")
    print(classification_report(y, model[1].predict(X), target_names=['+','-']))
    print("")

score_report(naive_bayes, X_test, df_test['clase'])
score_report(multi_naive_bayes, X_test, df_test['clase'])
score_report(logistic, X_test, df_test['clase'])
score_report(decision_tree, X_test, df_test['clase'])
score_report(gradient_descent, X_test, df_test['clase'])

"""
Explanation: Cabe notar que modelos como SVM, LDA y QDA no fueron posibles de estudiar debido a que las operaciones entre matrices que estos necesitan son muy complicadas, probocando que el cálculo fuera muy largo o simplemente matando el kernel de python.
g) Utilice y explique las métricas que calcula la función classification report de la librería sklearn. En base a las distintas métricas calculadas ¿Cuáles clasificadores son los que mejor se comportan?
End of explanation
"""


import random
random.seed(14)
test_pred = logistic[1].predict_proba(X_test)
spl = random.sample(range(len(test_pred)), 15)

print("Negative \t Positive \t Tweet")
for i in spl:
    print(test_pred[i],"\t",df_test.reset_index()['content'][i])

"""
Explanation: f1-score puede ser interpretado como el promedio poderado de precision y recall, siendo 0 y 1 los puntajes mínimos y máximos respectivamente.
precision está definido por $\frac{tp}{tp+fp}$, donde $tp$ y $fp$ son la cantidad de verdaderos y falsos positivos respectivamente, es decir, precision denota la habilidad del clasificador de no etiquetar una muestra que es negativa como positiva.
recall está definido por $\frac{tp}{tp+fn}$, donde $fn$ es el número de falsos negativos. recall denota la habilidad del clasificador de etiquetar todas las muestras positivas como positiva. 
support corresponde al número de ocurrencias de cada clase.
h) [Opcional] Visualice las predicciones de algún modelo generativo (probabilístico) definido anteriormente, tomando un subconjunto aleatorio de tweets de pruebas y explorando las probabilidades que asigna el clasificador a cada clase.
End of explanation
"""


clases['value']=[0,-1,1,-2,2,3,4,5,-3,-4,6,-5,-6]
sent = list(clases['sentiment'])
sent_v = list(clases['value'])
df_train['sentiment_class'] = df_train['sentiment'].apply(lambda x: sent_v[sent.index(x)])
df_test['sentiment_class'] = df_test['sentiment'].apply(lambda x: sent_v[sent.index(x)])

df_train.head()

"""
Explanation: i) Ahora deberá extender el problema a las múltiples clases que tiene presente (las distintas emociones), es decir, su trabajo será el de predecir una de las distintas emociones de cada tweet. Para esto utilice el mismo pre-procesamiento realizado en el punto c) y las características generadas mediante las técnicas en el punto e). Recuerde que tendrá que codificar las distintas clases como valores numéricos enteros.
End of explanation
"""


dt = train("DecisionTree",DecisionTreeClassifier(), X_train, df_train['sentiment_class'], X_test, df_test['sentiment_class'])
mnb = train("MultinomialNB",MultinomialNB(), X_train, df_train['sentiment_class'], X_test, df_test['sentiment_class'])
#mlp = train("Multi-layer Perceptron",MLPClassifier(), X_train, df_train['sentiment_class'], X_test, df_test['sentiment_class'])

"""
Explanation: j) Utilice los clasificadores que son extendidos por defecto a múltiples clases para detectar emociones en cada tweet, muestre sus desempeños a través del error de pruebas en un gráfico resumen.
End of explanation
"""


from sklearn.multiclass import OneVsRestClassifier
from sklearn.multiclass import OneVsOneClassifier

nb_ovo = train("Naive Bayes OVO",OneVsOneClassifier(BernoulliNB()), X_train, df_train['sentiment_class'], X_test, df_test['sentiment_class'])
nb_ovr = train("Naive Bayes OVR",OneVsRestClassifier(BernoulliNB()), X_train, df_train['sentiment_class'], X_test, df_test['sentiment_class'])

"""
Explanation: Gráfico en item m)
k) Utilice clasificadores binarios que pueden ser extendidos a través de otras técnicas, tal como One vs One y One vs All/Rest.
End of explanation
"""


lr_mtn = train("Log. Reg. Multi",LogisticRegression(), X_train, df_train['sentiment_class'], X_test, df_test['sentiment_class'])
lr_ovr = train("Log. Reg. OVR",OneVsRestClassifier(LogisticRegression()), X_train, df_train['sentiment_class'], X_test, df_test['sentiment_class'])

plot_scores("Modelos Multi-clase",[dt, mnb, nb_ovo, nb_ovr, lr_mtn, lr_ovr])

"""
Explanation: l) Para el caso de la Regresión Logística compare sus dos métodos para ser extendidos a múltiples clases. Uno a través de One vs Rest y otro definiendo que la variable a predecir se distribuye Multinomial.
End of explanation
"""


