# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# En este notebook, entrenamos modelos de predicción de discursos de odio en base a distintos datasets, para poder predecir su uso en reddit.
#
# Para poder ejecutar este notebook se requiere contar con los conjuntos de datos [HatEval](#Dataset-HatEval-(SemEval-2019-Task5)), [DETOXIS](#Dataset-DETOXIS-(IberLEF-2021)) y [MeOffendMex](#Dataset-MeOffendMex-(Iberlef-2021)) (la información para descargarlos se encuentra en cada uno de los respectivos links). Los datasets descargados deben estar situados respectivamente en las carpetas
#
#     docs/hateval2019/
#
#     docs/detoxis_data/
#
#     docs/MeOffendEs/

# +
import pickle

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import spacy

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.naive_bayes import ComplementNB
from sklearn.metrics import plot_confusion_matrix, classification_report
from sklearn.model_selection import train_test_split

from prettytable import PrettyTable

from preprocessing_utils import give_emoji_free_text, url_free_text, \
email_free_text, quotes_free_text, get_lemmas, tokenize, preprocess_corpus

nlp = spacy.load("es_core_news_lg")
np.random.seed(42)
palabras_odio = pd.DataFrame(columns=['hateval_rf', 'hateval_nb', 
                                      'detoxis_rf', 'detoxis_nb', 
                                      'meoffendmex_rf', 'meoffendmex_nb'])
# -

# ## Dataset HatEval (SemEval 2019 Task5)
#
# - Paper: 
#     - SemEval-2019 Task 5: Multilingual Detection of Hate Speech Against Immigrants and Women in Twitter, por <NAME>; <NAME>; <NAME>; <NAME>; <NAME>; <NAME>, <NAME>; <NAME>; <NAME>. Proceedings of the 13th International Workshop on Semantic Evaluation. Association for Computational Linguistics, 2019. [URL](https://aclanthology.org/S19-2007/).
# - Web: https://competitions.codalab.org/competitions/19935
# - Formulario para acceder a los datos: http://hatespeech.di.unito.it/hateval.html
#
# ### Descripción
#
# Este dataset consiste en ~7000 Tweets, que posiblemente representen discurso de odio hacia mujeres o inmigrantes.
#
# El dataset está formado por 5 columnas, representando cada una:
#
# 1. ID del Tweet.
# 1. El texto del Tweet.
# 1. HS (*hate speech*): si el discurso de odio ocurre contra mujeres o inmigrantes.
# 1. TR (*target*): si HS=1, recibe un valor de 0 si el discurso de odio es contra un grupo genérico, o un valor de 1 si es específicamente contra un individuo.
# 1. AG (*aggresive*): si HS=1, indica si quien escribe el Tweet exhibe comportamiento agresivo (si es así: 1; en caso contrario: 0).
#
# ([fuente](https://competitions.codalab.org/competitions/19935#participate))
#

verbose = 1
verbose_dataset = 0
verbose_hate_words = 0

# +
# cargamos el dataset y vemos su información

hate_eval = pd.read_csv('docs/hateval2019/hateval2019_es_train.csv')
if verbose_dataset:
    print(hate_eval.describe())
# -

if verbose_dataset:
    print(hate_eval[hate_eval['HS']==1])

print('Cantidad de Tweets sin HS: {} \nCantidad de Tweets con HS: {}'.format(
    len(hate_eval[hate_eval['HS']==0]), len(hate_eval[hate_eval['HS']==1])))

# ## Dataset DETOXIS (IberLEF 2021)
#
# - Web: https://detoxisiberlef.wixsite.com/website/corpus
# - Formulario para acceder a los datos: https://forms.office.com/r/6csEeBW0w3
#
# ### Descripción
#
# Este es un dataset con cerca de 3500 comentarios de sitios de noticias/foros españoles, que posiblemente contienen toxicidad. El punto distintivo de este dataset es que distingue distintos niveles de toxicidad, desde 0 (no tóxico) hasta 3 (muy tóxico), en donde también están anotados aspectos como el uso de insultos, sarcasmo, agresividad, estereotipación, constructividad, entre otros. En el contexto de este trabajo, se entrenó un modelo en donde se intenta predecir si un comentario es o no agresivo.
#

# +
# cargamos detoxis dataset

detoxis = pd.read_csv('docs/detoxis_data/train.csv')
if verbose_dataset:
    print(detoxis.describe())
# -

if verbose_dataset:
    print(detoxis[detoxis['aggressiveness']==1])

# ## Dataset MeOffendES (Iberlef 2021)
#
# - Papers:
#     
#     * OffendES: A New Corpus in Spanish for Offensive Language Research, por Plaza-del-Arco, Fl<NAME>am; <NAME>; <NAME> y <NAME>, <NAME>. Proceedings of the International Conference on Recent Advances in Natural Language Processing (RANLP 2021). INCOMA Ltd., 2021. [URL](https://aclanthology.org/2021.ranlp-main.123).
#     * Overview of MeOffendEs at IberLEF 2021: Offensive Language Detection in Spanish Variants, por Plaza-del-Arco, Flor Miriam; <NAME>; <NAME>; <NAME>, <NAME>; <NAME>; <NAME>; <NAME> y <NAME>, Luis. Procesamiento del Lenguaje Natural, Revista nº 67, septiembre de 2021, Sociedad Española para el Procesamiento del Lenguaje Natural. [URL](https://rua.ua.es/dspace/handle/10045/117506).
#
# - Web: https://competitions.codalab.org/competitions/28679
# - Repositorio: https://github.com/pendrag/MeOffendEs
#
#
# ### Descripción
#
# Este dataset consiste tweets de usuarios tanto de España (MeOffendES) como de México (MeOffendMex, este último con alrededor de 5000 tweets), que posiblemente contienen mensajes ofensivos. En este trabajo, se optó por usar la variante MeOffendMex, ya que se consideró a los mensajes de la misma como los más cercanos al trabajo aquí abordado.
#
# El dataset está formado por  columnas, representando cada una datos como el texto del comentario, y datos de la cuenta de Twitter como por ejemplo si la cuenta se encuentra verificada, el nombre de la cuenta y si la misma tiene una imagen por defecto, entre otros. En este trabajo se optó por usar solamente el texto del tweet. Se cuenta con dos salidas, una indica si el tweet es ofensivo, y la otra si representa "lenguaje vulgar"; se optó por utilizar solamente la primera.
#

# +
# cargamos MeOffendMex dataset

meoffendmex = pd.read_csv('docs/MeOffendEs/mx-train-data-non-contextual.csv')
y_meoffendmex = pd.read_csv('docs/MeOffendEs/mx-train-outputs.sol', header=None) 

if verbose_dataset:
    print(meoffendmex.describe())
# -

if verbose_dataset:
    print(meoffendmex.head())


# # Procesamos y entrenamos cada uno de los datasets

# Definimos la función general de entrenamiento, que lee un conjunto de datos, lo vectoriza y entrena un modelo a partir de dicha vectorización, mostrando las métricas generales del mismo.

def process_training_pipeline(dataset, vectorizer, labels, model):
    vectorized_corpus = vectorizer.fit_transform(dataset)
    X_tr, X_val, y_tr, y_val = train_test_split(vectorized_corpus, labels, test_size=0.3, random_state=42)
    model.fit(X_tr, y_tr)

    print('\nMétricas (train)')
    print(classification_report(model.predict(X_tr), y_tr))
    print('Matriz de confusión (train)')
    plot_confusion_matrix(model, X_tr, y_tr,
                          cmap=plt.cm.Blues, 
                          normalize='true')
    plt.show()
    print('\nMétricas (val)')
    print(classification_report(model.predict(X_val), y_val))
    print('Matriz de confusión (val)')
    plot_confusion_matrix(model, X_val, y_val,
                          cmap=plt.cm.Blues, 
                          normalize='true')
    plt.show()
    


# ### Hateval dataset

hateval_corpus_lines = preprocess_corpus(hate_eval['text'])

y_hateval = hate_eval['HS'].values

cv_hateval = CountVectorizer(ngram_range=(1, 3), min_df=10)
lg_hateval = LogisticRegression(random_state=42)
nb_hateval = ComplementNB()
rf_hateval = RandomForestClassifier(random_state=42)

process_training_pipeline(hateval_corpus_lines, 
                          cv_hateval,
                          y_hateval,
                          lg_hateval
                         )

process_training_pipeline(hateval_corpus_lines, 
                          cv_hateval,
                          y_hateval,
                          rf_hateval
                         )

if verbose_dataset:
    print(cv_hateval.get_feature_names()[:5])

# +
# obtenemos las características que más aportan a la clasificación del modelo random forest
rf_features_hateval_idx = rf_hateval.feature_importances_.argsort()[::-1]
palabras_odio_rf_hateval = np.array(cv_hateval.get_feature_names())[rf_features_hateval_idx[:100]]
palabras_odio['hateval_rf'] = pd.Series(palabras_odio_rf_hateval)

if verbose_hate_words:
    print(palabras_odio_rf_hateval)
    print(rf_hateval.feature_importances_[rf_features_hateval_idx[:30]])
# -

process_training_pipeline(hateval_corpus_lines, 
                          cv_hateval,
                          y_hateval,
                          nb_hateval
                         )

# +
# basado en https://stackoverflow.com/questions/50526898/how-to-get-feature-importance-in-naive-bayes
pos_class_prob_sorted = nb_hateval.feature_log_prob_[1, :].argsort()[::-1]
palabras_odio_nb_hateval = np.take(cv_hateval.get_feature_names(), pos_class_prob_sorted[:100])
palabras_odio['hateval_nb'] = pd.Series(palabras_odio_nb_hateval)

if verbose_hate_words:
    print(palabras_odio_nb_hateval)

# +
# guardamos el vectorizador y los modelos entrenados

with open('docs/models/hateval_vectorizer.pkl', 'wb') as file:
    pickle.dump(cv_hateval, file)

with open('docs/models/hateval_lg_model.pkl', 'wb') as file:
    pickle.dump(lg_hateval, file)

with open('docs/models/hateval_nb_model.pkl', 'wb') as file:
    pickle.dump(nb_hateval, file)

with open('docs/models/hateval_rf_model.pkl', 'wb') as file:
    pickle.dump(rf_hateval, file)
    
# -

# ## Detoxis dataset

detoxis_corpus_lines = preprocess_corpus(detoxis['comment'])
y_detoxis = detoxis['aggressiveness']

y_detoxis[detoxis['aggressiveness']==1]

cv_detoxis = CountVectorizer(ngram_range=(1, 3), min_df=10)
lg_detoxis = LogisticRegression(random_state=42)
nb_detoxis = ComplementNB()
rf_detoxis = RandomForestClassifier(random_state=42)

process_training_pipeline(detoxis_corpus_lines, 
                          cv_detoxis,
                          y_detoxis,
                          lg_detoxis
                         )

process_training_pipeline(detoxis_corpus_lines, 
                          cv_detoxis,
                          y_detoxis,
                          nb_detoxis
                         )

# +
pos_class_prob_sorted = nb_detoxis.feature_log_prob_[1, :].argsort()[::-1]
palabras_odio_nb_detoxis = np.take(cv_detoxis.get_feature_names(), pos_class_prob_sorted[:100])
palabras_odio['detoxis_nb'] = pd.Series(palabras_odio_nb_detoxis)

if verbose_hate_words:
    print(palabras_odio_nb_detoxis)
# -

process_training_pipeline(detoxis_corpus_lines, 
                          cv_detoxis,
                          y_detoxis,
                          rf_detoxis
                         )

# +
# obtenemos las características que más aportan a la clasificación del modelo random forest
rf_features_detoxis_idx = rf_detoxis.feature_importances_.argsort()[::-1]
palabras_odio_rf_detoxis = np.array(cv_detoxis.get_feature_names())[rf_features_detoxis_idx[:100]]
palabras_odio['detoxis_rf'] = pd.Series(palabras_odio_rf_detoxis)

if verbose_hate_words:
    print(palabras_odio_rf_detoxis)
    print(rf_detoxis.feature_importances_[rf_features_detoxis_idx[:30]])

# +
# guardamos el vectorizador y los modelos entrenados

with open('docs/models/detoxis_vectorizer.pkl', 'wb') as file:
    pickle.dump(cv_detoxis, file)

with open('docs/models/detoxis_lg_model.pkl', 'wb') as file:
    pickle.dump(lg_detoxis, file)

with open('docs/models/detoxis_nb_model.pkl', 'wb') as file:
    pickle.dump(nb_detoxis, file)

with open('docs/models/detoxis_rf_model.pkl', 'wb') as file:
    pickle.dump(rf_detoxis, file)
    
# -

# ## MeOffendMex dataset

cv_meoffendmex = CountVectorizer(ngram_range=(1, 3), min_df=10)
lg_meoffendmex = LogisticRegression(random_state=42)
nb_meoffendmex = ComplementNB()
rf_meoffendmex = RandomForestClassifier(random_state=42)

meoffendmex_corpus_lines = preprocess_corpus(meoffendmex['tweet:text'])

process_training_pipeline(meoffendmex_corpus_lines, 
                          cv_meoffendmex,
                          y_meoffendmex,
                          lg_meoffendmex
                         )

# +
# TODO extracción de palabras de odio con log regression
#cv_meoffendmex


palabras_odio_lg_meoffendmex = []
# -

process_training_pipeline(meoffendmex_corpus_lines, 
                          cv_meoffendmex,
                          y_meoffendmex,
                          nb_meoffendmex
                         )

# +
pos_class_prob_sorted = nb_meoffendmex.feature_log_prob_[1, :].argsort()[::-1]
palabras_odio_nb_meoffendmex = np.take(cv_meoffendmex.get_feature_names(), pos_class_prob_sorted[:100])
palabras_odio['meoffendmex_nb'] = pd.Series(palabras_odio_nb_meoffendmex)

if verbose:
    print(palabras_odio_nb_meoffendmex)
# -

process_training_pipeline(meoffendmex_corpus_lines, 
                          cv_meoffendmex,
                          y_meoffendmex,
                          rf_meoffendmex
                         )

# +
# obtenemos las características que más aportan a la clasificación del modelo random forest
rf_features_meoffendmex_idx = rf_meoffendmex.feature_importances_.argsort()[::-1]
palabras_odio_rf_meoffendmex = np.array(cv_meoffendmex.get_feature_names())[rf_features_meoffendmex_idx[:100]]
palabras_odio['meoffendmex_rf'] = pd.Series(palabras_odio_rf_meoffendmex)

if verbose:
    print(palabras_odio_rf_meoffendmex)

# + tags=[]
# guardamos los modelos entrenados

with open('docs/models/meoffendmex_vectorizer.pkl', 'wb') as file:
    pickle.dump(cv_meoffendmex, file)

with open('docs/models/meoffendmex_lg_model.pkl', 'wb') as file:
    pickle.dump(lg_meoffendmex, file)

with open('docs/models/meoffendmex_nb_model.pkl', 'wb') as file:
    pickle.dump(nb_meoffendmex, file)

with open('docs/models/meoffendmex_rf_model.pkl', 'wb') as file:
    pickle.dump(rf_meoffendmex, file)
    
# -

# ### Guardamos las palabras de odio

palabras_odio.to_csv('docs/palabras_odio.csv')

# # Prueba de modelos en Reddit

# + tags=[]
df = pd.read_csv('docs/preprocessing_reddit_data.csv')

if verbose_dataset:
    print(df)
# -

df_reddit_original = pd.read_csv('docs/preprocessing_reddit_data.csv')

reddit_corpus = preprocess_corpus(df['body'].astype('str'))


# En base a los modelos previamente entrenados, usamos la siguiente función para detectar los comentarios de Reddit.

def predict_n_report_on_reddit_comments(cv_model_pairs):
    table = PrettyTable()
    thresholds = np.linspace(0.5, 0.9, 5)
    
    table.field_names = ['Modelo', 
                         'Dataset', 
                         *['# pred. umb. {}'.format(i) for i in thresholds]]
    
    for pair in cv_model_pairs:
        cv = pair[0][0]
        dataset_name = pair[0][1]
        model = pair[1][0]
        model_name = pair[1][1]
        predicted_by_thresh = []
        
        for thresh in thresholds:
            reddit_adapted = cv.transform(reddit_corpus)
            reddit_hs_proba = model.predict_proba(reddit_adapted)[:,1]
            hate_mask = reddit_hs_proba >= thresh
            predicted_by_thresh.append(np.shape(df_reddit_original[hate_mask])[0])
        
        table.add_row([model_name, dataset_name, *predicted_by_thresh])
        
    return table


# + tags=[]
def predict_n_save_on_reddit_comments(cv, model, threshold, output_file_name):
    reddit_adapted = cv.transform(reddit_corpus)
    reddit_hs_proba = model.predict_proba(reddit_adapted)[:,1]
    hate_mask = reddit_hs_proba >= threshold
    print('Detectados {} comentarios de un total de {}'.format(np.shape(df_reddit_original[hate_mask])[0], np.shape(df_reddit_original)[0]))
    df_reddit_original[hate_mask].to_csv('docs/{}_hate.csv'.format(output_file_name))
    df_reddit_original[~hate_mask].to_csv('docs/{}_non_hate.csv'.format(output_file_name))


# -

# ## Vemos la cantidad de predicciones en todos los modelos

# +
cv_dataset_pairs = [(cv_hateval, 'HatEval'), (cv_detoxis, 'DETOXIS'), (cv_meoffendmex, 'MeOffendMex')]

cv_model_pairs = [
        [cv_dataset_pairs[0], (lg_hateval, 'Regresión logística')],
        [cv_dataset_pairs[0], (nb_hateval, 'Naive Bayes')],
        [cv_dataset_pairs[0], (rf_hateval, 'Random forest')],
        [cv_dataset_pairs[1], (lg_detoxis, 'Regresión logística')],
        [cv_dataset_pairs[1], (nb_detoxis, 'Naive Bayes')],
        [cv_dataset_pairs[1], (rf_detoxis, 'Random forest')],
        [cv_dataset_pairs[2], (lg_meoffendmex, 'Regresión logística')],
        [cv_dataset_pairs[2], (nb_meoffendmex, 'Naive Bayes')],
        [cv_dataset_pairs[2], (rf_meoffendmex, 'Random forest')],
       ]

predict_n_report_on_reddit_comments(cv_model_pairs)
# -

# ## Guardamos algunas predicciones para visualizar los CSV generados

# ### A partir de modelos entrenados con HatEval

predict_n_save_on_reddit_comments(cv_hateval, lg_hateval, 0.8, 'test/test_reddit_hateval_lg_hate_comments')

predict_n_save_on_reddit_comments(cv_hateval, nb_hateval, 0.8, 'test/test_reddit_hateval_nb_hate_comments')

predict_n_save_on_reddit_comments(cv_hateval, rf_hateval, 0.7, 'test/test_reddit_hateval_rf_hate_comments')

# ### A partir de modelos entrenados con Detoxis

predict_n_save_on_reddit_comments(cv_detoxis, lg_detoxis, 0.5, 'test/test_reddit_detoxis_lg_hate_comments')

predict_n_save_on_reddit_comments(cv_detoxis, nb_detoxis, 0.8, 'test/test_reddit_detoxis_nb_hate_comments')

predict_n_save_on_reddit_comments(cv_detoxis, rf_detoxis, 0.5, 'test/test_reddit_detoxis_rf_hate_comments')

# ### A partir de modelos entrenados con MeOffendMex

predict_n_save_on_reddit_comments(cv_meoffendmex, lg_meoffendmex, 0.8, 'test/test_reddit_meoffendmex_lg_hate_comments')

predict_n_save_on_reddit_comments(cv_meoffendmex, nb_meoffendmex, 0.8, 'test/test_reddit_meoffendmex_nb_hate_comments')

predict_n_save_on_reddit_comments(cv_meoffendmex, rf_meoffendmex, 0.5, 'test/test_reddit_meoffendmex_rf_hate_comments')

# # Mejoras a realizar
#
# En este notebook se utilizaron modelos en su versión más básica. Como trabajos futuros, quedan mejores pendientes, que incluyen realizar una mejora iterativa de cada uno de los modelos empleados, incluir otros, realizar optimización de híper-parámetros, y tomar distintas etiquetas o información (por ejemplo, usar una etiqueta alternativa en DETOXIS), entre otras.

# FIN

StarcoderdataJupyterScriptsDedupFiltered