In [1]:
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.pylab import hist, show
%matplotlib inline

import seaborn as sns; sns.set(style="ticks", color_codes=True)
import pandas as pd
import numpy as np
import re

Integrantes:

  • Gabriel Chaperón
  • Sebastian Cifuentes
  • José Terrazas

Proyecto Data Mining: Descubriendo los secretos tras emol

Motivación

Seguramente muchos de nosotros nos hemos encontrado una tarde de día sábado leyendo una noticia de actualidad en nuestro medio digital favorito. Finalizada la lectura, ante la inquietud de saber qué opiniones pudieron tener otros lectores de esta noticia, procedemos a ver la casilla de comentarios que generalmente está presente en estos medios, para encontrarnos con comentarios que siguen ciertos patrones comunes al tipo de noticia que se discuta: agresividad, nacionalismo, pesimismo, entre otros. Un ejemplo de esto es El Mercurio Online (Emol), medio digital relativamente masivo y famoso por la presencia de este tipo de comentarios, el cual es popularmente conocido por la naturaleza conservadora de quienes opinan, sobre todo en noticias que se refieren a cuestiones nacionales o sociales.

Con este contexto en mente, surge la pregunta de qué información de la población nacional podría reflejar el tipo de comentario que las personas están publicando en redes sociales. Claramente, para responder a esta interrogante haría falta agregar más variables a nuestro análisis, de manera tal de tener un panorama más completo de la realidad presente en nuestras redes sociales, agregando otros medios digitales de distintas tendencias ideológicas. Pero como una primera aproximación, comenzar a trabajar con Emol es suficiente.

Temática Central

Nuestra hipótesis inicial para trabajar nuestro dataset es la siguiente: “las personas que comentan en Emol se han vuelto más expresivas con el pasar del tiempo”.

Para poder responder a esta pregunta, tenemos que analizar los comentarios que las personas dejan en las noticias, ver qué características tienen estos comentarios y cómo se diferencian con otros (por ejemplo, ¿qué diferencias existen entre los comentarios de las secciones de Política con los de las secciones de Deportes?, ¿qué similitudes existen?).

Una pregunta preliminar es si existen grupos de comentarios que nazcan naturalmente dentro del dataset, lo cual se respondería aplicando un algoritmo de Clustering sobre ellos.

Si bien podemos generar preguntas preliminares para abordar en nuestros análisis, el tipo de preguntas se puede ampliar conociendo de mejor manera el dataset a trabajar. Para esto, se adjunta a continuación un análisis exploratorio de los datos, el cual nos da una idea más acabada de lo que el dataset contiene.

Descripción de los datos

Hicimos webscrapping para obtener los comentarios de mil noticias del sitio web EMOL. Estas noticias fueron escogidas uniformemente distribuidas a lo largo del año para poder trabajar con un subconjunto del total de noticias sin perder la capacidad para explorar el comportamiento de las noticias y comentarios a lo largo del año.

Para cada noticia se dispone de su id, fecha, categoria, número de comentarios en el nivel mas alto y título.

Para cada una de estas noticias se descargaron todos sus respectivos comentarios. De los comentarios se dispone de su id, creador, id del creador, id de comentario padre (0 si es de primer nivel), metodo de autentificación, texto del comentario, likes, dislikes, denuncias, hora, nivel, sección de noticia.

In [2]:
news = pd.read_csv("2017_emol_news_small.tsv", sep="\t")
comment = pd.read_csv("2017_emol_comments_small.tsv", sep="\t")

Análisis exploratorio noticias

1. Descripción noticias

In [3]:
print(news.columns) #variables
#print(news.describe(include="all"))
#print(news.dtypes) #estructura

#noticias
print("\ncantidad de noticias: ",len(pd.unique(news['idNoticia'])))
#fecha
print("\nfecha de las noticias")
print("Desde el:",news['fecha'].min(), ", hasta el: ",news['fecha'].max())
#categorias
print("\nFrecuencia por categorias:\n",news['categoria'].value_counts())
#nTopLevelComments
print("\ncantidad de noticias con comentarios:",len(news[news['nTopLevelComments']!=0]))
print("Descipción comentarios por noticias: \n",news["nTopLevelComments"].describe())

#titulo -> se deja para un prox procesamiento de texto
Index(['idNoticia', 'fecha', 'categoria', 'nTopLevelComments', 'titulo'], dtype='object')

cantidad de noticias:  1020

fecha de las noticias
Desde el: 2017-01-02 , hasta el:  2017-12-31

Frecuencia por categorias:
 Nacional         297
Deportes         211
Internacional    147
Espectaculos     130
Economia          99
Tecnologia        53
Tendencias        47
Autos             32
360                4
Name: categoria, dtype: int64

cantidad de noticias con comentarios: 858
Descipción comentarios por noticias:
 count    1020.000000
mean       16.920588
std        35.821970
min         0.000000
25%         1.000000
50%         6.000000
75%        19.000000
max       571.000000
Name: nTopLevelComments, dtype: float64

En el siguiente gráfico de torta se muestra la proporción de noticias de cada categoría en el 2017, donde se puede observar que las 3 categorías que más noticias tienen son Nacional, Internacional y Deportes, seguidas de cerca de espectáculos y Economia. Frente a esto se muestra una visualización con las 4 categorías con más noticias y su evolución durante el año.

In [4]:
print(np.array(news['categoria'].value_counts().index))
df = pd.DataFrame({'categoria': np.array(news['categoria'].value_counts())},
                   index = np.array(news['categoria'].value_counts().index))
df['categoria']=df['categoria']/sum(df['categoria'])
#print(df['categoria'])
#print(df.index)
colors=['']
print("\n\t\t------Porcentaje de noticias por categoría------")

labels=df.index
plt.figure(figsize=(7,7))
plt.pie(df['categoria'], labels=df.index,autopct='%1.1f%%', startangle=90)
plt.legend(labels, loc="best")
plt.axis('equal')
plt.tight_layout()

plt.show()
['Nacional' 'Deportes' 'Internacional' 'Espectaculos' 'Economia'
 'Tecnologia' 'Tendencias' 'Autos' '360']

		------Porcentaje de noticias por categoría------
In [5]:
df=news
mes= []
for i in range(0,len(df)):
    m=int(df['fecha'][i][5:7])
    mes.append(m)

df['mes']=mes
df['noticias']=np.repeat(1,len(news))
df=df[(df['categoria']=="Nacional") | (df['categoria']=="Deportes") | (df['categoria']=="Espectaculos")| (df['categoria']=="Economia")]
df=df.groupby(['mes','categoria'])['noticias'].agg(sum)
df=pd.DataFrame({'mes':df.index.get_level_values('mes'),'categoria':df.index.get_level_values('categoria'), 'noticias':np.array(df)})
#print(df)

print("------------------ Cantidad de noticias para las categorias importantes en el tiempo----------------------------")
#flatui = ["#9b59b6", "#3498db", "#95a5a6", "#e74c3c", "#34495e", "#2ecc71"]
#sns.palplot(sns.color_palette(flatui))
g = sns.FacetGrid(df, col="categoria",hue="categoria", palette=["r","b","g","c"])
g = g.map(plt.bar,"mes", "noticias",edgecolor="w")
------------------ Cantidad de noticias para las categorias importantes en el tiempo----------------------------

2. Descripción de comentarios

A continuación se presenta la distribución de comentarios que tuvieron las noticias en el 2017, donde se hace una distinción entre los comentarios totales y los comentarios de primer nivel solamente.

In [6]:
##Merge entre las tablas
df=comment
df['comentarios']=np.repeat(1,len(comment))
df=df.groupby(['pageCmsId'])['comentarios'].agg(sum)
df=pd.DataFrame({'idNoticia':df.index, 'comentarios':np.array(df)})
df=pd.merge(news,df, how= 'outer', on='idNoticia')
df=df[['fecha','categoria','comentarios']]
df['comentarios']=df['comentarios'].fillna(0)

plt.figure(2)
print("------Frecuencia de la cantidad de comentarios------")
plt.subplot(121)
plt.hist(df['comentarios'], 300)
plt.title("Frecuencia de comentarios")
plt.ylim(0,180)
plt.xlim(0,150)
plt.xlabel("Cantidad de comentarios")
plt.ylabel("Cantidad de noticias")
plt.grid(True)

#print("cantidad de noticias con más de 50 comentarios: ",len(df[df['comentarios']>50]))
#print("cantidad de noticias con 0 comentarios: ",len(df[df['comentarios']==0]))
#print("cantidad de noticias con más de 0 comentarios: ",len(df[df['comentarios']!=0]))


plt.subplot(122)
plt.hist(news['nTopLevelComments'], 200)
plt.title("Frecuencia de comentarios de primer nivel")
plt.ylim(0,180)
plt.xlim(0,150)
plt.xlabel("Cantidad de comentarios")
plt.ylabel("Cantidad de noticias")
plt.grid(True)

plt.subplots_adjust(top=1.5, bottom=0.08, left=0.10, right=4, hspace=0.34,
                    wspace=0.1)

#print("cantidad de noticias con más de 50 comentarios: ",len(news[news['nTopLevelComments']>50]))
#print("cantidad de noticias con 0 comentarios: ",len(news[news['nTopLevelComments']==0]))
#print("cantidad de noticias con más de 0 comentarios: ",len(news[news['nTopLevelComments']!=0]))


print("cantidad de noticias con más de 50 comentarios: ",len(df[df['comentarios']>50]))
print("cantidad de noticias con 0 comentarios: ",len(df[df['comentarios']==0]))
print("cantidad de noticias con más de 0 comentarios: ",len(df[df['comentarios']!=0]))
print("\ncomentarios por categoria:\n", comment['pageSection'].value_counts(normalize=True))
#print("\nnoticias por categoria:\n",news['categoria'].value_counts())
------Frecuencia de la cantidad de comentarios------
cantidad de noticias con más de 50 comentarios:  181
cantidad de noticias con 0 comentarios:  161
cantidad de noticias con más de 0 comentarios:  859

comentarios por categoria:
 Nacional         0.547056
Deportes         0.199029
Internacional    0.093502
Economia         0.073117
Espectaculos     0.049902
Tendencias       0.022893
Tecnologia       0.009357
Autos            0.003858
360              0.001286
Name: pageSection, dtype: float64

Ahora analizamos la cantidad de comentarios promedio por noticia en cada categoria, donde se observa nuevamente que la categoria Nacional es la que más comentarios tiene por noticias, donde luego vienen las categorias de deportes y economia, seguidas por Internacional y Espectaculos. En este nuevo análisis la categoria Nacional siguie en la cabeza, pero economía a paso a ser la 3 más relevante a pesar de no tener la mayor cantidad de noticias, esto quiere decir que a pesar de no tener tantas noticias como Espectaculos e Internacional es una de las más comentadas.

In [7]:
df['noticias']=np.repeat(1,len(df))
df1=df.groupby(['categoria'])['comentarios'].mean()
print("\nComentarios por noticia para las diferentes categorias: \n",df1)
df1=pd.DataFrame({'categoria':df1.index, 'comentarios/noticias':np.array(df1)})
print("\n------Comentarios/noticias para las diferentes categorias------")
sns.barplot(x="comentarios/noticias",y='categoria',data=df1, hue_order="com_not")
Comentarios por noticia para las diferentes categorias:
 categoria
360              10.000000
Autos             3.750000
Deportes         29.336493
Economia         22.969697
Espectaculos     11.938462
Internacional    19.782313
Nacional         57.286195
Tecnologia        5.490566
Tendencias       15.148936
Name: comentarios, dtype: float64

------Comentarios/noticias para las diferentes categorias------
Out[7]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f6fea081978>

En este tercer gráfico se muestra la evolución temporal de la cantidad de comentarios por mes a lo largo del 2017 para las categorías mas importantes, donde se observa un orden marcado de volumen de comentarios/noticias comenzando por Nacional seguido por Deportes, Economia y Espectaculos.

In [8]:
mes= []
for i in range(0,len(df)):
    m=int(df['fecha'][i][5:7])
    mes.append(m)
df['mes']=mes
df1=df[(df['categoria']=="Nacional") | (df['categoria']=="Deportes") | (df['categoria']=="Espectaculos")| (df['categoria']=="Economia")]
df1=df1.groupby(['categoria','mes'])['comentarios'].mean()
df1=pd.DataFrame({'categoria':df1.index.get_level_values('categoria'),'mes':df1.index.get_level_values('mes'), 'comentarios':np.array(df1)})

print("------------------ Cantidad de comentarios/noticias en el año 2017 por categoria----------------------------")
#flatui = ["#9b59b6", "#3498db", "#95a5a6", "#e74c3c", "#34495e", "#2ecc71"]
#sns.palplot(sns.color_palette(flatui))
g = sns.FacetGrid(df1, col="categoria",hue="categoria", palette=["r","b","g","c"])
g = g.map(plt.bar,"mes", "comentarios",edgecolor="w")
------------------ Cantidad de comentarios/noticias en el año 2017 por categoria----------------------------

3. Descripción de los usuarios

Primero hacemos un analisis de los deciles para la cantidad de comentarios de cada usuaria. La tabla que se muestra nos dice que la mitad de los usuarios comenta a lo mas 1 vez y que el 90% de los usuarios comenta a lo más 6 veces.

Esto levanta el interes de ver que porcentaje de usuarios comenta una sola vez. Vemos que el 55% de los usuarios comenta una sola vez.

In [9]:
print("Cantidad de usuarios que comentaron: ",len(comment['creatorId'].unique()))
print("Cantidad comentarios: ",len(comment['creatorId']))

#print(pd.qcut(comment['creatorId'],10, labels=False))
df1=comment.groupby(['creatorId'])['comentarios'].agg(sum)
df1=pd.DataFrame({'creatorId':df1.index, 'comentarios':np.array(df1)})
print("\nLos deciles de comentarios por usuarios son los siguientes:")
print(np.array(df1['comentarios'].quantile([.1, .2, .3, .4, .5, .6 ,.7,.8,.9, 1])))
print("\nCantidad de usuarios que comentaron solo una vez en el 2017: ")
print(len(df1[df1['comentarios']==1])/len(df1))

df1=df1.sort_values(['comentarios'], ascending=False)
print("\n En la siguiente tabla se ve los usuarios que comentaron más veces en el 2017")
print(df1[0:10])
Cantidad de usuarios que comentaron:  10572
Cantidad comentarios:  31101

Los deciles de comentarios por usuarios son los siguientes:
[  1.   1.   1.   1.   1.   2.   2.   3.   6. 128.]

Cantidad de usuarios que comentaron solo una vez en el 2017:
0.5524025728339008

 En la siguiente tabla se ve los usuarios que comentaron más veces en el 2017
      creatorId  comentarios
6639     156470          128
8089     241785          103
1566       7729           92
6742     161887           91
294         925           88
9361     451291           83
1606       7980           77
3193      26220           76
7570     216941           76
342        1077           72

Por otra parte es escogió analizar la participación de los usuarios más activos dentro de la plataforma. Para esto, el siguiente gráfico muestra los comentarios a lo largo del año de los 6 usuarios más activos separados por categoría. Se puede ver en el gráfico que que 5 de los 6 usuarios comentan mayoritariamente en la categoría nacional y solo uno lo hace de gran manera en deportes, siendo además la única categoria donde participa dicho usuario.

In [10]:
df=comment
mes= []
for i in range(0,len(df)):
    m=int(df['time'][i][5:7])
    mes.append(m)
df['mes']=mes
df1=df.groupby(['creatorId','pageSection','mes'])['comentarios'].agg(sum)
df1=pd.DataFrame({'creatorId':df1.index.get_level_values('creatorId'),'categoria':df1.index.get_level_values('pageSection'),
                  'mes':df1.index.get_level_values('mes'), 'comentarios':np.array(df1)})

print("------------------ Cantidad de comentarios/noticias en el año 2017 para usuarios top----------------------------")

df1=df1[(df1['categoria']=="Nacional") | (df1['categoria']=="Deportes") | (df1['categoria']=="Espectaculos")| (df1['categoria']=="Economia")]
df2=df1[(df1['creatorId']==156470) | (df1['creatorId']== 241785) | (df1['creatorId']==7729)]

g = sns.FacetGrid(df2, col="creatorId",hue="categoria", palette=["r","b","g","c"], legend_out=True)
g = g.map(plt.plot,"mes", "comentarios").add_legend()

df2=df1[(df1['creatorId']==161887) | (df1['creatorId']== 925) | (df1['creatorId']==451291)]
g = sns.FacetGrid(df2, col="creatorId",hue="categoria", palette=["r","b","g","c"])
g = g.map(plt.plot,"mes", "comentarios").add_legend()
------------------ Cantidad de comentarios/noticias en el año 2017 para usuarios top----------------------------

4. Descripción de reacciones en comentarios

In [67]:
print(comment.columns) #variables

col_comment=['likes','dislikes','denounces','level','pageSection']
df=comment[col_comment]

#likes | dislikes | denounces | level

df=df[(df['pageSection']=='Nacional') | (df['pageSection']=='Economia') | (df['pageSection']=='Espectaculos') | (df['pageSection']=='Deportes')]
print("------------------Gráfico de correlaciones para reacciones de comentarios----------------------------")
sns.pairplot(df, hue="pageSection", kind='reg')
Index(['id', 'creator', 'creatorId', 'parentId', 'authSource', 'text', 'likes',
       'dislikes', 'denounces', 'time', 'level', 'pageSection', 'pageCmsId',
       'comentarios', 'mes'],
      dtype='object')
------------------Gráfico de correlaciones para reacciones de comentarios----------------------------
Out[67]:
<seaborn.axisgrid.PairGrid at 0x7fa4e4416a20>
In [69]:
col_comment=['likes','dislikes','denounces','time','level']
df=comment[col_comment]
corr = df.corr()

mask = np.zeros_like(corr, dtype=np.bool)
mask[np.triu_indices_from(mask)] = True

cmap = sns.diverging_palette(220, 10, as_cmap=True)
print("--------Matriz de correlación-------------")
sns.heatmap(corr, mask=mask, cmap=cmap, vmax=.3, center=0,
            square=True, linewidths=.5)
--------Matriz de correlación-------------
Out[69]:
<matplotlib.axes._subplots.AxesSubplot at 0x7fa4e772a5c0>

5. Descripción del texto (análisis por caracter)

Para esta sección se analiza el texto desde el punto de vista de la cantidad de caracteres y el tipo de caracteres. En el siguiente gráfico se muestra la distribución de la cantidad de caracteres de los comentarios.

In [12]:
comentarios=np.array(comment['text'])
cant=[]
for i in range(0,len(comentarios)):
    cant.append(len(comentarios[i]))

print("--------Distribución cantidad de caracteres en los comentarios-------------")
plt.hist(cant, bins=100)
plt.title("Distribución cantidad de caracteres en los comentarios")
--------Distribución cantidad de caracteres en los comentarios-------------
Out[12]:
Text(0.5, 1.0, 'Distribución cantidad de caracteres en los comentarios')

Ahora se grafica el promedio de caracteres de los comentarios por categoría, agregando el promedio global como punto de comparación.

In [13]:
df=comment[['text','pageSection']]
df['cant']=cant
#df.loc[:,('cant')]=cant
df1=df.groupby(['pageSection'])['cant'].mean()
df1=pd.DataFrame({'pageSection':df1.index, 'cant':np.array(df1)})

print("--------Cantidad promedio de caracteres en comentarios-------------")
sns.barplot(y='pageSection', x='cant', data=df1)
plt.title("Cantidad promedio de caracteres en comentarios")
--------Cantidad promedio de caracteres en comentarios-------------
/home/jose/.local/lib/python3.6/site-packages/ipykernel_launcher.py:2: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy

Out[13]:
Text(0.5, 1.0, 'Cantidad promedio de caracteres en comentarios')

En los siguientes dos graficos se muestra la cantidad promedio de caracteres exclamativos (!) e interrogativos (?) para cada categoria.

In [14]:
def contar(com,c):
    a=0
    for i in range(0,len(com)):
        if(com[i]==c):
            a=a+1
    return a
excl=[]
preg=[]
for i in range(0,len(comentarios)):
    excl.append(contar(comentarios[i],"!"))
    preg.append(contar(comentarios[i],"?"))

df1=pd.DataFrame({'pageSection':comment['pageSection'], 'excl':excl, 'preg':preg})
df1=df1.groupby(['pageSection'])['excl','preg'].mean()
df1=pd.DataFrame({'pageSection':df1.index, 'excl': np.array(df1['excl']), 'preg': np.array(df1['preg'])})

##Cantidad de símbolos "!" por comentarios 
print("--------Cantidad de símbolos '!' por comentarios -------------")
sns.barplot(x='excl', y='pageSection', data=df1)
plt.title("Cantidad de símbolos '!' por comentarios")
--------Cantidad de símbolos '!' por comentarios -------------
Out[14]:
Text(0.5, 1.0, "Cantidad de símbolos '!' por comentarios")
In [15]:
##Cantidad de símbolos "?" por comentarios
print("--------Cantidad de símbolos '?' por comentarios -------------")
sns.barplot(x='preg', y='pageSection', data=df1)
plt.title("Cantidad de símbolos '?' por comentarios")
--------Cantidad de símbolos '?' por comentarios -------------
Out[15]:
Text(0.5, 1.0, "Cantidad de símbolos '?' por comentarios")
In [16]:
topwords = pd.read_csv("topwordsglobal.csv", sep=",")

print("--------Top words en los comentarios -------------")

sns.barplot(x='002',y='001',data=topwords)
plt.xlabel("cantidad de apariciones")
plt.ylabel("palabras top")
plt.title("Top de palabras con más apariciones")
--------Top words en los comentarios -------------
Out[16]:
Text(0.5, 1.0, 'Top de palabras con más apariciones')
In [17]:
topwords_sec = pd.read_csv("topwordssection.csv", sep=",")
col=['Deportespalabra', 'Deportescantidad', 'Economiapalabra',
       'Economiacantidad', 'Espectaculospalabra', 'Espectaculoscantidad',
       'Nacionalpalabra','Nacionalcantidad']
print(topwords_sec[col])
  Deportespalabra  Deportescantidad Economiapalabra  Economiacantidad  \
0           chile               977           chile               285
1       argentina               534           pisco               239
2          futbol               466            anos               166
3          equipo               463             afp               159
4        historia               440        gobierno               147
5            anos               374            peru               146
6         mundial               337        chilenos               105
7       jugadores               327          dinero               104
8       seleccion               323         chileno                93
9           mundo               291       pensiones                93

  Espectaculospalabra  Espectaculoscantidad Nacionalpalabra  Nacionalcantidad
0               fotos                    73           chile              1400
1               gente                    55          pinera              1109
2                arte                    53        gobierno               998
3               chile                    48           gente               691
4                vida                    43            anos               609
5                anos                    38             ley               572
6                tipo                    34        chilenos               533
7               mundo                    32      presidente               490
8             noticia                    32       izquierda               486
9                  tv                    32        personas               446

Procesamiento de texto

En esta parte vamos a trabajar con el texto de los comentarios donde vamos a crear el bags of words, luego eliminaremos las stop words (palabras que no nos entregan información como conjunciones y preposición como otras), luego vamos a hacer un proceso de stemming, donde llevaremos las palabras a su raíz y buscaremos un diccionario de sinónimos para no tener rebundancia en las palabras. Finalmente, con esto crearemos el vector space model para realizar diferentes procedimientos sobre los comentarios.

In [3]:
from sklearn.feature_extraction.text import CountVectorizer
from nltk import word_tokenize
import nltk.stem
from sklearn import metrics
from nltk.stem import SnowballStemmer

stemmer = SnowballStemmer('spanish')

def extract_words(sentence):
    ignore_words = np.array(pd.read_csv("stop_words.txt"))
    words = re.sub("[^\w]", " ",  sentence).split() #nltk.word_tokenize(sentence)
    words_cleaned = [w.lower() for w in words if w not in ignore_words]
    return words_cleaned

def stemming(sentence):
    stems = [stemmer.stem(w) for w in sentence]
    return stems

def tokenize_sentences(sentences):
    words = []
    for sentence in sentences:
        w = extract_words(sentence)
        w = stemming(w)
        words.extend(w)

    words = sorted(list(set(words)))
    return words

def fusion(vec,s):
    string=""
    vec=list(vec)
    for i in vec:
        string=string+s+str(i)
    return string

def bagofwords(sentence, words):
    sentence_words = extract_words(sentence)
    # frequency word count
    bag = np.zeros(len(words))
    for sw in sentence_words:
        for i,word in enumerate(words):
            if word == sw:
                bag[i] += 1
    return np.array(bag)

def cosine_similarity(v1,v2):
    "compute cosine similarity of v1 to v2: (v1 dot v2)/{||v1||*||v2||)"
    sumxx, sumxy, sumyy = 0, 0, 0
    for i in range(len(v1)):
     x = v1[i]; y = v2[i]
     sumxx += x*x
     sumyy += y*y
     sumxy += x*y
    return sumxy/math.sqrt(sumxx*sumyy)

"""
filtro=comment[(comment['pageSection']=="Nacional")|(comment['pageSection']=="Economia")|(comment['pageSection']=="Deportes")|(comment['pageSection']=="Espectaculos")]

ids=list(filtro['id'])
comentarios=list(filtro['text'])

bags_words = tokenize_sentences(comentarios)

#for com in comentarios:
#    bagofwords(com, bags_words)

vectorizer = CountVectorizer(analyzer = "word", tokenizer = None, preprocessor = None, stop_words = None, max_features = 5000) 
train_data_features = vectorizer.fit_transform(bags_words)

f = open("vector_space_model.txt",'w')
vec=[]
for i in range(0,len(comentarios)):
    vec.append(vectorizer.transform([comentarios[i]]).toarray())
    vectores=fusion(vec[i][0],";")
    f.write(str(ids[i])+ vectores+"\n")
f.close()
"""
Out[3]:
'\nfiltro=comment[(comment[\'pageSection\']=="Nacional")|(comment[\'pageSection\']=="Economia")|(comment[\'pageSection\']=="Deportes")|(comment[\'pageSection\']=="Espectaculos")]\n\nids=list(filtro[\'id\'])\ncomentarios=list(filtro[\'text\'])\n\nbags_words = tokenize_sentences(comentarios)\n\n#for com in comentarios:\n#    bagofwords(com, bags_words)\n\nvectorizer = CountVectorizer(analyzer = "word", tokenizer = None, preprocessor = None, stop_words = None, max_features = 5000) \ntrain_data_features = vectorizer.fit_transform(bags_words)\n\nf = open("vector_space_model.txt",\'w\')\nvec=[]\nfor i in range(0,len(comentarios)):\n    vec.append(vectorizer.transform([comentarios[i]]).toarray())\n    vectores=fusion(vec[i][0],";")\n    f.write(str(ids[i])+ vectores+"\n")\nf.close()\n'
In [4]:
"""
bla=pd.read_csv("vector_space_model.txt", sep=";")
data=bla[bla.columns[1:]]
from sklearn.cluster import KMeans
from sklearn.metrics import davies_bouldin_score
kmeans = KMeans(n_clusters=3, random_state=1).fit(data)
labels = kmeans.labels_
davies_bouldin_score(data, labels)
metrics.calinski_harabaz_score(data, labels)
"""
Out[4]:
'\nbla=pd.read_csv("vector_space_model.txt", sep=";")\ndata=bla[bla.columns[1:]]\nfrom sklearn.cluster import KMeans\nfrom sklearn.metrics import davies_bouldin_score\nkmeans = KMeans(n_clusters=3, random_state=1).fit(data)\nlabels = kmeans.labels_\ndavies_bouldin_score(data, labels)\nmetrics.calinski_harabaz_score(data, labels)\n'
In [16]:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import KMeans, AgglomerativeClustering
from sklearn.metrics import adjusted_rand_score
from nltk import word_tokenize
import nltk.stem
from sklearn import metrics
from nltk.stem import SnowballStemmer

stemmer = SnowballStemmer('spanish')

#filtro=list(comment['text'][(comment['pageSection']=="Nacional")|(comment['pageSection']=="Economia")|(comment['pageSection']=="Deportes")|(comment['pageSection']=="Espectaculos")])
filtro=list(comment['text'][(comment['pageSection']=="Espectaculos")|(comment['pageSection']=="Internacional")])
print(len(filtro))

stops=list(pd.read_csvs("stop_words.txt")['stop'])
#stops=stemming(list(pd.read_csv("stop_words.txt")['stop']))

vectorizer = TfidfVectorizer(analyzer='word', stop_words=stops, lowercase=True)#, preprocessor=stemmer.stem)
X = vectorizer.fit_transform(filtro)
4460
In [17]:
true_k = 2
model = KMeans(n_clusters=true_k, init='k-means++', max_iter=100, n_init=1)
#model = AgglomerativeClustering(n_clusters=true_k,affinity='cosine', linkage='average')
"""
print(len(comment[comment['pageSection']=="Nacional"]))
print(type(X))
print(X.shape)
print(type(X[17013]))
"""
model.fit(X)

print("Top terms per cluster:")
order_centroids = model.cluster_centers_.argsort()[:, ::-1]
terms = vectorizer.get_feature_names()
for i in range(true_k):
    print("Cluster %d:" % i),
    for ind in order_centroids[i, :10]:
        print(' %s' % terms[ind]),
    print

print("\n")
print("Prediction")
Top terms per cluster:
Cluster 0:
 chile
 venezuela
 maduro
 dictadura
 pinochet
 democracia
 youtube
 link
 allende
 pueblo
Cluster 1:
 mundo
 comentario
 falta
 favor
 noticia
 fotos
 arte
 vida
 dios
 emol
Cluster 2:
 trump
 putin
 obama
 presidente
 mundo
 eeuu
 guerra
 rusia
 mundial
 otan
Cluster 3:
 comunistas
 comunista
 maduro
 pinochet
 aviones
 rusos
 venezuela
 dictadura
 paises
 democracia
Cluster 4:
 gente
 años
 problema
 alguien
 quieren
 presidente
 tv
 importante
 festival
 vida


Prediction
In [18]:
from sklearn import metrics
#from sklearn.metrics import pairwise_distances

labels = model.labels_

print(metrics.silhouette_score(X, labels))
#metrics.calinski_harabaz_score(X, labels)
0.006949176216520322
In [28]:
#print(model.inertia_)
print(model.cluster_centers_[4])
[0.         0.00303883 0.         ... 0.00073664 0.         0.        ]
In [19]:
print(metrics.davies_bouldin_score(X.toarray(), labels))
13.950632904402585
/home/jose/.local/lib/python3.6/site-packages/sklearn/metrics/cluster/unsupervised.py:342: RuntimeWarning: divide by zero encountered in true_divide
  score = (intra_dists[:, None] + intra_dists) / centroid_distances
In [2]:
# ## Vectores para respuestas.
# Este punto consiste en cargar determinados valores para cada palabra que tienen una caracterización semantica. esto permite poder buscar un signficado semántico a casa respuesta. 
from gensim.models import Word2Vec
from gensim.models.keyedvectors import KeyedVectors

wordvectors_file_vec = 'fasttext-sbwc.3.6.e20.vec.gz'
cantidad = 100000
wordvectors = KeyedVectors.load_word2vec_format(wordvectors_file_vec, limit=cantidad).wv

# ## Promedio Vectorial
# La siguiente función va a permitir caracterizar los comentarios posteriormente. 

import pandas as pd
import numpy as np
from functools import reduce

vector_zero = np.zeros(shape=wordvectors["hola"].shape)

## fución sumas y evitar errores ortográficos 

def word_vec_sum(partial, word):
    # este try es para evitar problemas de errores ortográficos
    try:
        return partial + wordvectors[word.lower()]
    except:
        return partial

##SUMA DE LOS VECTORES, NUEVO VECTORES DE DIMENSION 300
def sentence_word_vec(sentence):
    return reduce(word_vec_sum, sentence.split(" "), vector_zero) / len(spanish_word_list)

path = 'comentarios.txt'

##seleccionamos palabras en español, cargamos todo el diccionario
with open(path, 'r', encoding="utf-8") as f:
    spanish_word_list = f.read().splitlines()

len(spanish_word_list)


##vectorizamos todas las pablar del español
spanish_words_vectors = pd.DataFrame({'WORD': spanish_word_list, "VECTOR" : list(map(sentence_word_vec, spanish_word_list))})
spanish_words_vectors.loc[:, "VECTOR"] = spanish_words_vectors["VECTOR"].apply(lambda x: None if (x == vector_zero).all() else x)
spanish_words_vectors = spanish_words_vectors.dropna(subset=['VECTOR'])

# ## Pruebas asociadas
# Los pasos anteriores, permiten poder realizar la caretización de una frase como un vector (es decir, un "número" que se caracteriza según un espacio semantico. 

sentencia='economia crecimiento'
sentence_word_vec(sentencia)

data=data.dropna(subset=["respuestas"])
data.info(verbose=True)

z=data['u_local'].value_counts().plot(kind='bar')
/home/jose/.local/lib/python3.6/site-packages/ipykernel_launcher.py:8: DeprecationWarning: Call to deprecated `wv` (Attribute will be removed in 4.0.0, use self instead).

Hito 3

Trabajando con un dataset etiquetado

Para esta iteración, se cuenta con un dataset de comentarios de Emol y otros periódicos electrónicos etiquetados por expertas. La idea será generar clasificadores que puedan discernir entre distintos comentarios y ver si se generan clases clasificables dentro de los comentarios, para luego extender esta tarea a los comentarios de Emol extraídos por nuestra cuenta.

In [9]:
import numpy as np
import pandas as pd
import re
from nltk.stem import SnowballStemmer
from unidecode import unidecode
import spacy
import sys
import random
In [10]:
data_raw = pd.read_csv('datos_profe/FINAL DATABASE - Magdalena Saldaña.csv', sep='\t')
print("Leemos las columnas de los datos: \n")
print(data_raw.columns)
Leemos las columnas de los datos:

Index(['CASODEF', 'Caso (old)', 'Noticia', 'Comentario', 'SPAM', 'Fecha',
       'Diario', 'Género usuario', 'Usuario anónimo', 'Uso de mayúsculas',
       'Grosería/Lenguaje vulgar', 'Insulto/Sobrenombre', 'Estereotipo',
       'Pregunta legítima', 'Evidencia', 'Grosería que ofende', 'Codificación',
       'codificado en ', 'Index Incivilidad', 'Index Deliberación'],
      dtype='object')
In [11]:
data=data_raw.drop(columns=['CASODEF', 'Caso (old)', 'Noticia', 'Fecha', 'Diario',
                   'Género usuario', 'Usuario anónimo', 'Codificación',
       'codificado en '])
# se realiza copia de Comentario, no se deberia modificar data de ahora en adelante
# data se usa en el futuro para la clasificacion
comments = pd.DataFrame(data['Comentario'].copy())
print("Miramos el dataset data y los comentarios que se extrajeron:\n")
print(data.head())
print(comments.head())
Miramos el dataset data y los comentarios que se extrajeron:

                                          Comentario  SPAM  Uso de mayúsculas  \
0  Se ve que la presidenta tiene gran experiencia...     0                  0
1                   El otro en tusunamis y marepotos     0                  0
2  Mario Loyola Figueroa : Cuando ganó Bachelet n...     0                  0
3  ¿El Gobierno está aquí para apoyarlos?... Esa ...     0                  0
4           Y tu los iras a ayudar, facho solidario?     0                  0

   Grosería/Lenguaje vulgar  Insulto/Sobrenombre  Estereotipo  \
0                         0                    0            0
1                         0                    0            0
2                         0                    1            0
3                         0                    0            0
4                         0                    0            1

   Pregunta legítima  Evidencia  Grosería que ofende  Index Incivilidad  \
0                  0          0                    0                  0
1                  0          0                    0                  0
2                  0          0                    0                  1
3                  0          0                    0                  0
4                  0          0                    0                  1

   Index Deliberación
0                   0
1                   0
2                   0
3                   0
4                   0
                                          Comentario
0  Se ve que la presidenta tiene gran experiencia...
1                   El otro en tusunamis y marepotos
2  Mario Loyola Figueroa : Cuando ganó Bachelet n...
3  ¿El Gobierno está aquí para apoyarlos?... Esa ...
4           Y tu los iras a ayudar, facho solidario?

Limpieza de datos

Ahora, se trabajará sobre los comentarios de la siguiente manera:

  • Primero ver si los comentarios tienen simbolos de exclamacion o de interrogacion (puedes ser util para hacer la clasificación).
  • Sacar nombres de usuario (usar archivo con los nombres de usuario).
  • Lematización.
  • Sacar acentos y mayúsculas.
  • Sacar stop words.
  • Sacar números.
  • Vectorización del espacio completo de palabras.
In [12]:
#Se agregan las columnas de interrogación y pregunta
comments = comments.assign(simbolo_interrogacion=pd.Series([(1 if ('?' in w) else 0) for w in comments.Comentario]))
comments = comments.assign(simbolo_exclamacion=pd.Series([(1 if ('!' in w) else 0) for w in comments.Comentario]))

print(comments[['Comentario', 'simbolo_interrogacion']].head(n=10))
print(comments[['Comentario', 'simbolo_exclamacion']].head(n=10))
                                          Comentario  simbolo_interrogacion
0  Se ve que la presidenta tiene gran experiencia...                      0
1                   El otro en tusunamis y marepotos                      0
2  Mario Loyola Figueroa : Cuando ganó Bachelet n...                      0
3  ¿El Gobierno está aquí para apoyarlos?... Esa ...                      1
4           Y tu los iras a ayudar, facho solidario?                      1
5                     Juan Enrique Igual que tu !!!!                      0
6            Miriam Pollak Yo no reclamo ton.torrona                      0
7  Oye Juanito Enriquito, si te enojas pierdes, o...                      1
8  Juan Enrique , y que hizo tu presi el 2010 apa...                      1
9  Jorge . Lo suficiente para ser reelegida y bar...                      0
                                          Comentario  simbolo_exclamacion
0  Se ve que la presidenta tiene gran experiencia...                    0
1                   El otro en tusunamis y marepotos                    0
2  Mario Loyola Figueroa : Cuando ganó Bachelet n...                    0
3  ¿El Gobierno está aquí para apoyarlos?... Esa ...                    0
4           Y tu los iras a ayudar, facho solidario?                    0
5                     Juan Enrique Igual que tu !!!!                    1
6            Miriam Pollak Yo no reclamo ton.torrona                    0
7  Oye Juanito Enriquito, si te enojas pierdes, o...                    1
8  Juan Enrique , y que hizo tu presi el 2010 apa...                    0
9  Jorge . Lo suficiente para ser reelegida y bar...                    0
In [13]:
# saca del string w todas las palabras que se encuentren en el set s
# ademas saca todo lo que no sea alfanumerico
# deja acentos, deja mayusculas
def remove_words(w, s):
    #w = re.sub(r'[^a-zA-Z0-9\s]', ' ', w)
    w = re.sub(r'[\W\s]', ' ', w)
    return ' '.join([w for w in w.split() if w not in s])

In [14]:
# se sacan los nombre de usuario (siempre y cuando aparezcan textuales)
names = [w.rstrip() for w in open('datos_profe/names_list.txt', 'r').readlines()]
#print(len(names))
#print(names[0:10])
comments['Comentario'] = [remove_words(w, names) for w in comments['Comentario']]
#print(comments.head(n=10))
In [21]:
# aca va la lematizacion (parecido al stemming), se hace antes de sacar stop words,
# acentos y mayusculas para no perder significado y que el lemmatizer no se pierda
nlp = spacy.load('es')
comments['Comentario'] = [' '.join([token.lemma_ for token in nlp(w)])
                          for w in comments['Comentario']]
print("Muestra de comentarios luego de la lematización: \n")
print(comments['Comentario'].head(n=10))
Muestra de comentarios luego de la lematización:

0    presidente experiencia terremoto especialmente...
1                                  tusunamis marepotos
2    ganar ganar poblar ganar lucsik monsanto aesge...
3    gobernar parir apoyarlos frase recordar cualqu...
4                           ira ayudar facho solidario
5
6                                 reclamar ton torrona
7    oír juanito enriquito enojar perder acaso ment...
8    presi apartar masticar chile repetir hipar sun...
9      suficiente parir reelegir barrer fascista comer
Name: Comentario, dtype: object
In [22]:
# sacar acentos y mayusculas
comments['Comentario'] = [unidecode(w.lower()) for w in comments['Comentario']]
# sacar stop words
stop_words = set([unidecode(w.rstrip().lower()) for w in open('stopwords-es.txt', 'r').readlines()])
#print(stop_words)
comments['Comentario'] = [' '.join(list(filter(lambda x : x not in stop_words, w.split()))) for w in comments['Comentario']]
# sacar numeros
comments['Comentario'] = [re.sub('\d', '', w) for w in comments['Comentario']]
# se guardan palabras de largo tres o más
comments['Comentario'] = [' '.join(list(filter(lambda x : len(x) > 2, w.split()))) for w in comments['Comentario']]
print("Muestra de los comentarios luego de la limpieza: \n")
print(comments['Comentario'].head(n=10))
Muestra de los comentarios luego de la limpieza:

0    presidente experiencia terremoto especialmente...
1                                  tusunamis marepotos
2    ganar ganar poblar ganar lucsik monsanto aesge...
3    gobernar parir apoyarlos frase recordar cualqu...
4                           ira ayudar facho solidario
5
6                                 reclamar ton torrona
7    oir juanito enriquito enojar perder acaso ment...
8    presi apartar masticar chile repetir hipar sun...
9      suficiente parir reelegir barrer fascista comer
Name: Comentario, dtype: object

Con todo lo anterior, generamos un set de palabras para tener el espacio completo y sin repeticiones de las palabras que aparecen en todos los comentarios del dataset etiquetado.

In [25]:
# ahora se hará un set con todas las palabras distintas para poder 
# generar luego las columnas de la vectorizacion y luego poner los 1's y 0's correspondientes
# para cada comentario

# Se guardan todas las palabras para que sea facil cargarlas despues
s = set()

for c in comments['Comentario']:
    for w in c.split():
        s.add(w)


s = sorted(s)
with open('bag_of_words.txt', 'w') as out:
    for w in s:
        out.write(w + '\n')

print(f"Cantidad de comentarios: {len(comments)}")
print('Palabras :', len(s))
print('bytes:', sys.getsizeof(s))
Cantidad de comentarios: 5985
Palabras : 10133
bytes: 91304
In [26]:
# Aca usamos un bag of words y se crea la vectorizacion para cada comentario
# MAS las columnas de tiene? y tiene!
# Se hace un diccionario con la posicion en la matriz donde va cada palabra de la bag of words

d = dict()
n = len(comments)
for i, w in enumerate(s):
    d[w] = i

arr = np.zeros([n, len(s) + 2])
for i, c in enumerate(comments.Comentario):
    for w in c.split():
        arr[i][d[w]] += 1

# ahora le agrego las dos ultimas columnas
arr[:, -1] = comments['simbolo_exclamacion'].tolist()
arr[:, -2] = comments['simbolo_interrogacion'].tolist()

print(f"Dimensión de la matriz o espacio de las palabras: {arr.shape}")
print('peso en memoria de arr:', sys.getsizeof(arr)/1e6, 'mb')
Dimensión de la matriz o espacio de las palabras: (5985, 10135)
peso en memoria de arr: 485.263912 mb

Clasificación sobre el dataset etiquetado

Teniendo ya los comentarios vectorizados, estamos en condiciones de alimentar a un clasificador para ver si es posible automatizar la detección de ciertos tipos de expresiones.

In [28]:
#Vamos a comenzar a clasificar los datos. Usaremos las clases más etiquetadas que son
#"Groseria/Lenguaje vulgar" e "Insulto/Sobrenombre".
classdata = pd.DataFrame(arr)
y1 = data['Grosería/Lenguaje vulgar']
print("Distribución de clase: 0 es otro comentario, 1 es groseria")
print(y1.value_counts())
y2 = data['Insulto/Sobrenombre']
print("Distribución de clase: 0 es otro comentario, 1 es insulto/sobrenombre")
print(y2.value_counts())
#Vemos que las clases están fuertemente desbalanceadas, por lo que es necesario subsamplear la muestra
#Generamos dos tablas nuevas para cada caso
raw1 = classdata.join(y1) #Grosería
raw2 = classdata.join(y2) #Insulto

idx1 = np.random.choice(raw1.loc[raw1['Grosería/Lenguaje vulgar'] == 0].index, size=4800, replace=False)
idx2 = np.random.choice(raw2.loc[raw2['Insulto/Sobrenombre'] == 0].index, size=3245, replace=False)
data1 = raw1.drop(raw1.iloc[idx1].index) #Clase Grosería equilibrada
data2 = raw2.drop(raw2.iloc[idx2].index) #Clase Insulto equilibrada
Distribución de clase: 0 es otro comentario, 1 es groseria
0    5418
1     567
Name: Grosería/Lenguaje vulgar, dtype: int64
Distribución de clase: 0 es otro comentario, 1 es insulto/sobrenombre
0    4674
1    1311
Name: Insulto/Sobrenombre, dtype: int64

Dado que las clases están desbalanceadas, será necesario hacer un subsampling de la muestra para así entrenar de mejor manera a los clasificadores.

In [29]:
#Revisamos que las nuevos datos estén equilibrados
print("Distribución de clase: 0 es otro comentario, 1 es groseria")
aa,be = data1['Grosería/Lenguaje vulgar'].value_counts()
print(data1['Grosería/Lenguaje vulgar'].value_counts())
print("Proporción: "+str(round(aa*1.0/be,4)))
print("Distribución de clase: 0 es otro comentario, 1 es insulto/sobrenombre")
ce,de = data2['Insulto/Sobrenombre'].value_counts()
print(data2['Insulto/Sobrenombre'].value_counts())
print("Proporción: "+str(round(ce*1.0/de,4)))
#Y preparamos los datos a usarse
X1 = data1[data1.columns[:-1]]
y1 = data1[data1.columns[-1]]
X2 = data2[data2.columns[:-1]]
y2 = data2[data2.columns[-1]]
Distribución de clase: 0 es otro comentario, 1 es groseria
0    618
1    567
Name: Grosería/Lenguaje vulgar, dtype: int64
Proporción: 1.0899
Distribución de clase: 0 es otro comentario, 1 es insulto/sobrenombre
0    1429
1    1311
Name: Insulto/Sobrenombre, dtype: int64
Proporción: 1.09

Con los datos balanceados, estamos en condiciones de entrenar nuestros clasificadores. Para evaluar su desempeño, usaremos una función vista en un laboratorio, utilizando 30 tests por clasificador. Las métricas entregadas por esta función serán f1, precisión y recall.

In [31]:
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score, recall_score, precision_score, accuracy_score
from sklearn.metrics import confusion_matrix

#Con esto ya podemos comenzar a entrenar nuestros modelos, definimos entonces una función
#para correr y evaluar los clasificadores
def runclassifier(clf, X, y, num_tests=30):
    metrics = {'f1-score': [], 'precision': [], 'recall': []}
    confusion = 0
    for _ in range(num_tests):
        X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.30, stratify=y)

        clf.fit(X_train,y_train)

        predictions=clf.predict(X_test)

        metrics['f1-score'].append(f1_score(y_test, predictions))  # X_test y y_test deben ser definidos previamente
        metrics['recall'].append(recall_score(y_test, predictions))
        metrics['precision'].append(precision_score(y_test, predictions))
        confusion= confusion + np.array(confusion_matrix(y_test, predictions))
    return metrics, confusion/num_tests

A continuación se muestran los resultados del entrenamiento de los clasificadores. Los clasificadores utilizados son:

  • Suppor Vector Machine Lineal
  • Random Forest
  • Naive Bayes
  • Decision Tree
  • Dummy Classifier
In [33]:
from sklearn.svm import LinearSVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.tree import DecisionTreeClassifier
from sklearn.dummy import DummyClassifier
#Generamos 4 clasificadores y un Dummy y los evaluamos sobre el dataset 1, "Groserías/Lenguaje vulgar
nb1 = ("Naive Bayes", GaussianNB())
rf1 = ("Random Forest", RandomForestClassifier(n_estimators=100))
svm1 = ("SVM Lineal", LinearSVC())
dt1 = ("Decision Tree", DecisionTreeClassifier())
dummy1 = ("Dummy", DummyClassifier(strategy='stratified'))
cfs1 = [nb1, rf1, svm1, dt1, dummy1]
print("Clasificadores para el dataset Groserías/Lenguaje vulgar: \n")
for name, clf in cfs1:
    metrics, confusion = runclassifier(clf, X1, y1)
    print("----------------")
    print("Resultados para clasificador:",name)
    print("Precision promedio:",np.array(metrics['precision']).mean())
    print("Recall promedio:",np.array(metrics['recall']).mean())
    print("F1-score promedio:",np.array(metrics['f1-score']).mean())
    print("matrix confusion promedio:\n", (confusion))
    print("----------------\n\n")
Clasificadores para el dataset Groserías/Lenguaje vulgar:

----------------
Resultados para clasificador: Naive Bayes
Precision promedio: 0.5618461804477114
Recall promedio: 0.7949019607843135
F1-score promedio: 0.6547609566105186
matrix confusion promedio:
 [[ 78.16666667 107.83333333]
 [ 34.86666667 135.13333333]]
----------------


----------------
Resultados para clasificador: Random Forest
Precision promedio: 0.8271904233463482
Recall promedio: 0.49235294117647055
F1-score promedio: 0.615649214169965
matrix confusion promedio:
 [[168.13333333  17.86666667]
 [ 86.3         83.7       ]]
----------------


----------------
Resultados para clasificador: SVM Lineal
Precision promedio: 0.7146311476222587
Recall promedio: 0.6103921568627452
F1-score promedio: 0.6578295764980971
matrix confusion promedio:
 [[144.43333333  41.56666667]
 [ 66.23333333 103.76666667]]
----------------


----------------
Resultados para clasificador: Decision Tree
Precision promedio: 0.7164554277041824
Recall promedio: 0.594313725490196
F1-score promedio: 0.648936488688365
matrix confusion promedio:
 [[145.66666667  40.33333333]
 [ 68.96666667 101.03333333]]
----------------


----------------
Resultados para clasificador: Dummy
Precision promedio: 0.47459235969442903
Recall promedio: 0.4690196078431373
F1-score promedio: 0.47153360387092885
matrix confusion promedio:
 [[97.76666667 88.23333333]
 [90.26666667 79.73333333]]
----------------


In [34]:
#Repetimos, pero ahora sobre el dataset de "Insulto/Sobrenombre"
nb2 = ("Naive Bayes", GaussianNB())
rf2 = ("Random Forest", RandomForestClassifier(n_estimators=100))
svm2 = ("SVM Lineal", LinearSVC())
dt2 = ("Decision Tree", DecisionTreeClassifier())
dummy2 = ("Dummy", DummyClassifier(strategy='stratified'))
cfs2 = [nb2, rf2, svm2, dt2, dummy2]
print("Clasificadores para el dataset Insulto/Sobrenombre: \n")
for name, clf in cfs2:
    metrics, confusion = runclassifier(clf, X2, y2)
    print("----------------")
    print("Resultados para clasificador: ",name)
    print("Precision promedio:",np.array(metrics['precision']).mean())
    print("Recall promedio:",np.array(metrics['recall']).mean())
    print("F1-score promedio:",np.array(metrics['f1-score']).mean())
    print("matrix confusion promedio:\n", (confusion))
    print("----------------\n\n")
Clasificadores para el dataset Insulto/Sobrenombre:

----------------
Resultados para clasificador:  Naive Bayes
Precision promedio: 0.6549993103245237
Recall promedio: 0.6821882951653943
F1-score promedio: 0.6585295692573958
matrix confusion promedio:
 [[277.9 151.1]
 [124.9 268.1]]
----------------


----------------
Resultados para clasificador:  Random Forest
Precision promedio: 0.8310437529958369
Recall promedio: 0.6030534351145037
F1-score promedio: 0.6984089948207104
matrix confusion promedio:
 [[380.6  48.4]
 [156.  237. ]]
----------------


----------------
Resultados para clasificador:  SVM Lineal
Precision promedio: 0.7543329780146182
Recall promedio: 0.658863443596268
F1-score promedio: 0.7031376159559581
matrix confusion promedio:
 [[344.56666667  84.43333333]
 [134.06666667 258.93333333]]
----------------


----------------
Resultados para clasificador:  Decision Tree
Precision promedio: 0.7506352024317758
Recall promedio: 0.6593723494486853
F1-score promedio: 0.7015892843524422
matrix confusion promedio:
 [[342.6         86.4       ]
 [133.86666667 259.13333333]]
----------------


----------------
Resultados para clasificador:  Dummy
Precision promedio: 0.474818412329396
Recall promedio: 0.4774385072094996
F1-score promedio: 0.4760033970511733
matrix confusion promedio:
 [[221.36666667 207.63333333]
 [205.36666667 187.63333333]]
----------------


Experimento con comentarios ajenos al dataset etiquetado

Con lo anterior se pudo ver que las etiquetas seleccionadas para clasificación son bien recibidas por algunos métodos, siendo Random Forest el que mejor resultados arroja. Ahora lo que haremos será seleccionar comentarios de Emol del dataset anterior al azar para intentar clasificarlos.

In [35]:
#Cargamos datos de emol y nos quedamos con los primeros 1000 comentarios
emol = pd.read_csv("2017_emol_comments_small.tsv", sep="\t")
emoltext = pd.DataFrame(emol['text'][0:1000])
emoltext = emoltext.assign(simbolo_interrogacion=pd.Series([(1 if ('?' in w) else 0) for w in emoltext.text]))
emoltext = emoltext.assign(simbolo_exclamacion=pd.Series([(1 if ('!' in w) else 0) for w in emoltext.text]))
emoltext['text'] = [remove_words(w, names) for w in emoltext['text']]
In [36]:
#Lematización, toma tiempo
emoltext['text'] = [' '.join([token.lemma_ for token in nlp(w)])
                          for w in emoltext['text']]
In [42]:
# sacar acentos y mayusculas
emoltext['text'] = [unidecode(w.lower()) for w in emoltext['text']]
# sacar stop words
emoltext['text'] = [' '.join(list(filter(lambda x : x not in stop_words, w.split()))) for w in emoltext['text']]
# sacar numeros
emoltext['text'] = [re.sub('\d', '', w) for w in emoltext['text']]
# se guardan palabras de largo tres o más
emoltext['text'] = [' '.join(list(filter(lambda x : len(x) > 2, w.split()))) for w in emoltext['text']]
print(emoltext['text'].head(n=10))
0    ojalar ultimar fanaticos jugar mil mejorar pel...
1                            pinera erecto jajajajajaj
2    debo reconocer equivocar profundamente pensar ...
3    importante evaluar candidato quedarse comer fe...
4    senor tironi llamar calificar sobretodo amaril...
5    gobernar guillar ministro guillar centrar soci...
6    pinera salir saldra guillier ensuciar gobernar...
7                     guillier candidato independiente
8                                               barbar
9               echa vistazo tweet budoia twitter link
Name: text, dtype: object
In [45]:
ceros = np.zeros([len(emoltext), len(s) + 2])
#generamos la vectorizacion para los comentarios nuevos
for i, c in enumerate(emoltext.text):
    for w in c.split():
        if(w in s):
            ceros[i][d[w]] += 1

# agregamos las columnas de tiene? y tiene!
ceros[:, -1] = emoltext['simbolo_exclamacion'].tolist()
ceros[:, -2] = emoltext['simbolo_interrogacion'].tolist()
In [46]:
#Ya teniendo los comentarios vectorizados, podemos generar un dataframe y alimentar un clasificador
#del tipo Random Forest entrenado con los datos etiquetados.
emolvec = pd.DataFrame(ceros)
emolforest1 = RandomForestClassifier(n_estimators=100) #Clasifica Groserías/Lenguaje vulgar
emolforest2 = RandomForestClassifier(n_estimators=100) #Clasifica Insulto/Sobrenombre
emolforest1.fit(X1,y1)
emolforest2.fit(X2,y2)
print("Terminó entrenamiento de los Random Forest")
Terminó entrenamiento de los Random Forest
In [47]:
#Generamos las predicciones
groserias = emolforest1.predict(emolvec)
insultos = emolforest2.predict(emolvec)

grosindices = [i for i, x in enumerate(groserias) if x == 1]
print(f"Cantidad de Groserias segun el clasificador: {len(grosindices)}")
insuindices = [i for i, x in enumerate(insultos) if x == 1]
print(f"Cantidad de Insultos segun el clasificador: {len(insuindices)}")
colisiones = set(grosindices).intersection(set(insuindices))
print(f"Cantidad de colisiones: {len(colisiones)}")
onlygros = [x for x in grosindices if x not in colisiones]
onlyinsu = [x for x in insuindices if x not in colisiones]
random.seed(567)
grsample = random.sample(onlygros, 10) #Elegimos 30 indices al azar para groserías y para insultos
insample = random.sample(onlyinsu, 10)
Cantidad de Groserias segun el clasificador: 98
Cantidad de Insultos segun el clasificador: 169
Cantidad de colisiones: 40

Con todo lo que tenemos, elegimos al azar 10 comentarios etiquetados por nuestro clasificador, para así ver la calidad de la clasificación.

In [50]:
print("Según el clasificador, contienen groserías o lenguaje vulgar los siguientes comentarios:\n ")
for i,ind in enumerate(grsample):
    print(str(i)+" "+emol['text'][ind])
    print()
Según el clasificador, contienen groserías o lenguaje vulgar los siguientes comentarios:

0  A estas alturas, ya no se si sera mejor que vaya o se quede. No se cual es el mal menor.

1  y los guillatunes para que son, medico de que? hoy en dia estas son unas chantas que lo unico que les interesa es sacar plata

2  Mujer + Edad Mayor + Chilena Mapuche+ Machi (Religión) es mayor o menor a Presunción fundada de participar en un asesinato de dos personas ancianas (Mujer + Hombre + Edad Mayor + Chilenos)???

3  Los Productores no tienen paciencia y cada vez menos don de Gentes......Pero si ella estaba postulando y tenia acento muy marcado,es logico que no la dejen......Pero esto no es discriminacion.....La niña ademas es muy Bonita..Estoy seguro de que fue un Productor mal educado y una joven que no acepta que cuando uno va a una Prueba de camara (Yo no lo hago)....es muy posible que no quedes porqwue puede haber 200 Postulantes a ese Rol..O sea tienes el 0.5% de Probabilidades de quedar.. A veces tambien se hacen Pruebas de camara para dejar contento al Productor de campo..Pero el Productor Ejecutivo ya tiene listo el papael para alguien.....O sea........Es parte de nuestro Negocio.... Muy bonita la Joven.

4  Una vez mas los jueces dando muestra que ellos se dejan llevar por presiones politicas y una vez mas queda de manifiesto que los peores delincuentes de este pais son los jueces y abogados defensores..esto si se llama incitar a la violencia y es un llamado a la delincuencia y asesinos que hagan lo que hagan,con una simple huelga de hambre quedaran libres..pais nefasto y asqueroso,cero respeto hacia las victimas

5  Tu comentario está siendo fotografiado. Si la Machi es inocente serás acusado.

6  Alguien se le pasa por la cabeza como habrán sufrido esa pareja de ancianos sabiendo que morirían calcinados??? A los diputados izquerditas que la apoyan podrán imaginar ese sufrimiento angustia y desesperación que pasaron. Si esta tipa quiere sufrir, lo hace porque quiere no porque se lo impongan. Solo les pido eso a Vallejos, defensores de derechos humanos, etc.... morir calcinados !!!!

7  Sr: Mario Medina Valdés. Me parece que es un excelente comentario, todo se ha hecho improvisando, y me parece que con mas de 16 millones de personas no se debe improvisar, creo que es el peor gobierno que hemos tenido después de la UP. Ademas la mayor autoridad del país ES responsable con lo que ocurra en el país que gobierna, no es solo para llenarse los bolsillos de plata, y quedar con una pensión que ninguna persona de trabajo la puede tener. Lo felicito por su comentario, es una gran verdad.

8  Don Matias, muy bueno su comentario, pero lo hechò a perder con las dos ultimas lineas de escrito.

9  asi roba conaf, incendiando Chile, sacando paltas del estado para combatir incendios

In [51]:
print("Según el clasificador, contienen insultos o sobrenombres los siguientes comentarios:\n ")
for i,ind in enumerate(insample):
    print(str(i)+" "+emol['text'][ind])
    print()
Según el clasificador, contienen insultos o sobrenombres los siguientes comentarios:

0  Esta nueva moda de hacerse la víctima para ganar pantalla cuando no eres lo que buscan........es tan tonto como alegar discriminacion por racismo cuando se realiza el casting de una película de pigmeos africanos y no se le permita realizar el casting para el papel de los pigmeos a un anglosajón de 2 metros.

1  POR QUE ES FOME, POR ESO... CUAL ES EL GUSTO DE LLORAR SIEMPRE A CUESTAS DE OTRO. PROBABLEMENTE SEAN CERCANOS TUYOS Y TE EMOCIONEN LOS PARTIDOS PERO A LOS DEMAS POQUITO ;)

2  el show dio sus frutos

3  Otra inconsecuencia más del poder judicial. La huelga de hambre convierte a una persona que participó en un hecho criminal (terrorista), en una mártir. Confío algún día lleguen "los buenos tiempos", donde el culpable pague justamente por lo que hizo, independientemente del lado que sea.

4  Un jugador no gana un campeonato, es todo un equipo y Alexis juega casi solo.

5  estuve leyendo los comentarios anteriores a este y que pena que las personas no sean claras para expresar ideas. Insultos van y vienen. Pobres o ricos, jóvenes o viejos, jubilados o no, todos tienen su manera respetable de pensar y con argumentos respetuosos pueden opinar. de lo contrario mejor abstenerse.! Vomitar odios por este medio NO AYUDA.

6  no pongo en duda que esta vieja recibirá indemnización de parte de la otra vieja.....tenlo por seguro.

7  Esta vieja es mala y asesina, debe cumplir en la cárcel su pena y no en su casa.. Si existiera la reencarnación debieran abortarla.

8  Totalmente de acuerdo con Luis Hernández. Vivo en el extranjero y me asombra la cantidad de incendios que hay en Chile. Se podría pensar que son provocados o que tratande desviar la atención de otros problemas.que también necesitan pronta solución..

9  Mientras se incendiaba chile estaba preocupada de los homosexuales uffff que señora mas ..... cuantos millones de dolares habra gastado solo para promocionarlos, sin darse cuenta que necesitamos aviones jumbo boeing contra incendios, en dos minutos hubiesen apagado lo que paso en valparaiso, pero deveras, es gente pobre, que no vale la pena hacer nada con ellos, jajaj y dice ser izquierda y defender los derechos de los pobres jajaja

Con esto vemos que los comentarios de Emol pueden ser clasificados a partir del contenido que posean, pero en este caso la clasificación no es tan certera como podría llegar a haber sido. Esto se debe a que sólo estamos evaluando las palabras que contiene cada comentario, sin hacer una distinción entre palabras de distinto tipo, ni asignándole peso a cierto tipo de palabras. Por ejemplo, en una oración es más probable que nos diga más información un sustantivo que un artículo, o un verbo más que un pronombre.