Integrantes:
En el contexto del curso CC5206 Introducción a la Minería de Datos, se realiza un proyecto sobre una base de datos escogida por los integrantes del grupo, con el objetivo de aplicar herramientas del curso sobre los datos y predecir su comportamiento. La base de datos escogida por el grupo es "stack overflow dataset", la cuál posee información de preguntas y respuestas realizadas por usuarios del sitio web Stack Overflow, el cuál es utilizado por la comunidad de desarrolladores informáticos, básicamente un usuario hace pública una pregunta, esperando recibir respuestas de otros usuarios. Además, los usuarios pueden calificar preguntas y respuestas según su calidad, asignandole un puntaje o score.
La mayoría de las veces que uno publica una preguntas en Stack Overflow es porque se necesita un respuesta confiable y a corto plazo. Probablemente se esté estancado en un bugg y se necesite seguir avanzando con la tarea de programación. Stack overflow es uno de los foros de programación con más flujo de preguntas/respuestas, en general es un foro bastante popular. Sin embargo, no está ausente de preguntas abandonadas, o threads que nunca fueron cerrados, ya que todas las respuestas que recibió el cuestionador fueron inútiles al momento de resolver su problema. Basado en esta problemática se decide proveer herramientas que intenten predecir el Score de una respuesta asociada a una pregunta en particular, a partir de los atributos que estas posean.
El score de una respuesta es directamente proporcional a la cantidad de “comentarios positivos” e inversamente proporcional a la cantidad de “comentarios negativos”
El historial de un usuario es un buen atributo para la creación de un clasificador de calidad. Una respuesta tiene mejor calidad (mayor score) si proviene de un usuario con mejor reputación.
El DataSet es poco privado respecto a la información entregada y los comentarios expuestos por los usuarios
En primer lugar, se realiza un análisis de los datos para seleccionar las tablas que son importantes para poder predecir el Score de una respuesta. Estas tablas son Post_Answers, Post_Questions, Users y Comments. Sobre estas tablas se seleccionan los atributos más relevantes para lograr un buen clasificador, como se muestra:
Post_Answers
Post_Questions
Users
Comments
Para reducir el número de datos, se filtran las tablas Comments, Anwers y Questions, seleccionando sólo los años 2018 y 2019. Las tablas de bigquery se filtran, y se seleccionan sólo los atributos de interés y sus llaves foráneas, para luego guardarlas en un archivo .csv con la librería pandas.
Finalmente se combinan estas cuatro tablas en una sola, realizando los siguientes cruces entre llaves foráneas:
Antes de trabajar con los atributos es necesario pasarlos a un formato numérico, es decir pasar de datos crudos como fechas y texto, a datos que servirán para clasificar. Es importante recalcar que los métodos actuales de clasificación sólo trabajan sobre escalares, por lo cual se deben crear nuevos atributos. Se crean los siguientes atributos para las siguientes Tablas:
Comments
Corresponde al puntaje atribuido al texto en cuanto a qué tan positivo o negativo es, usando un conjunto de palabras base llamado Sentiment Lexicon "sentiwordnet", del autor Andrea Esuli. Para obtener este puntaje se recorre el comentario como una bolsa de palabras, y por cada palabra se verifica si ocurre o no en el diccionarios de palabras. Este diccionario es del tipo llave: par, donde el primer valor del par corresponde al puntaje positivo de la palabra, mientras que el segundo valor corresponde al puntaje negativo. Query_Answers
Query_Question
También, se crearon atributos que almacenan el largo en caracteres del texto de la respuesta y pregunta (length_answer / length_question), eliminando cuidadosamente el código html que contienen para no considerarlo en el valor.
Otra información importante es el transcurso de tiempo, en milisegundos entre la publicación de la pregunta y la respuesta, la cuál se almacena en el atributo delta_time.
Query_Answers
Se crean tres clases para clasificar el puntaje, usando un método similar a Equal Frequency. Es decir, se intenta dividir el dominio de puntajes en clases de la misma frecuencia. Debido a que el dominio es discreto esta división es parcialmente posible. Se realiza la siguiente división:
En el hito anterior se utilizaron los datos del año 2018, solamente. Obteniendo así un total de 1000 datos por calse, mediante el uso de subsampling y oversamling:
Los experimentos corridos sobre este set de datos arrojaron un f1-score del orden de 0.6 puntos. Lo cual no era muy bueno.También se corrieron experimentos de clustering, sobre los cuales no se vio ninguna agrupación concluyente. (ver anexo de clustering)
A difetrencia del hito anterior en éste se realizan experimentos sobre una cantidad de datos 10 veces mayor. esperando así ver mejoras en la calidad del calsificador.
import pandas as pd
print('cargando Joined-Table')
joinT = pd.read_csv('Joined_table_3.tsv', sep='\t')
joinT.head()
El siguiente código crea los atributos mencionados anteriormente, y además obtiene 2 atributos adicionales, positive_realtive y negative_relative, que corresponden al cuociente entre positivos y el total, y negativos y el total.
import time
from datetime import datetime
import re
joinT['delta_time'] = joinT['creation_date_answer'].map(lambda x: time.mktime(datetime.strptime(x[:19], '%Y-%m-%d %H:%M:%S').timetuple())) - joinT['creation_date_question'].map(lambda x: time.mktime(datetime.strptime(x[:19], '%Y-%m-%d %H:%M:%S').timetuple()))
joinT =joinT[["reputation_user", "likes_user", "dislikes_user", "location_user", "body_answer", "body_question", "delta_time"
,"positive", "negative", "score_discrete" ]]
TAG_RE = re.compile(r'<[^>]+>') #sirve para remover tags HTML de las respuestas
joinT['length_answer'] = joinT['body_answer'].map(lambda x: len(TAG_RE.sub('', x)))
joinT['length_question'] = joinT['body_question'].map(lambda x: len(TAG_RE.sub('', x)))
joinT.head()
Creación de atributo location
import pandas as pd
import math
countries = pd.read_csv('country-codes.csv', sep=',')
import numpy as np
countries['English short name lower case'] = countries['English short name lower case'].str.lower()
joinT['location_user']=joinT['location_user'].str.lower()
countries = countries[['English short name lower case', 'Numeric code']]
dic=countries.set_index("English short name lower case").T.to_dict('list')
joinT['location']= np.nan
for index, row in joinT.iterrows():
key = row['location_user']
if type(key)== str:
if key in dic:
joinT.at[index,'location']= dic.get(key)[0]
elif 'usa' in key or 'united states' in key:
joinT.at[index,'location']= 840.0
else:
arr=dic.keys()
for k in arr:
if k in key or key in k:
joinT.at[index,'location']= dic.get(k)[0]
break
else:
joinT.at[index,'location']= 0
joinT['location'].fillna(0, inplace = True)
joinT.head()
Debido a la poca equitatividad entre cantidad de datos por clase. Se realizo subsamplig de la clase 0 y oversampling de la clase 2, para obtener 3 clases con alrededor de 1000 datos.
filteredT= joinT[joinT["score_discrete"]!=-1]
print(filteredT['score_discrete'].value_counts())
# subsampling sobre la clase 2 y de clase 1
data_subsampled = pd.concat([filteredT[filteredT["score_discrete"]==2].sample(10000),filteredT[filteredT["score_discrete"]==1].sample(10000),filteredT[filteredT["score_discrete"]==0]])
print("Data subsampled on class '1' & '2'")
print(data_subsampled['score_discrete'].value_counts())
En primer lugar se quiere entender cómo se realaciona cada uno de los atributos entre ellos. Por lo que se grafica un diagrama de densidades, en el cual es posible ver las ocurrencias de dos atributos en un gráfico. (El siguiente gráfico sólo muestra una porción de los atributos)
import matplotlib.pyplot as plt
import seaborn as sns; sns.set(style="ticks", color_codes=True)
data = data_subsampled
g = sns.pairplot(data[["score_discrete", "reputation_user", "likes_user", "dislikes_user","delta_time", "positive", "negative","length_question","location"]]) # Parametro kind="reg" agrega una recta
plt.show()
En el gráfico anterior es posible ver que los atributos más densos entre sí, corresponden a aquellos relacionados con cualidades del usuario, por ejemplo, los pares: {reputation, likes}, {reputation, dislikes}. Por otro lado, aquellos atributos que mejor se agrupan con la clase a predecir, a partir del gráfico anterior, son : reputation, dislikes y delta_time.
Para probar el poder de clasificación de los atributos seleccionados se realizan experimentos de clasificación con los métodos Decision Tree y K-means evaluados bajo cross-validation. Se realizan los siguientes experimentos:
# IMPORTACIONES
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import cross_validate
from sklearn.neighbors import KNeighborsClassifier
from sklearn.datasets import load_breast_cancer
from sklearn.dummy import DummyClassifier
from sklearn.svm import SVC # support vector machine classifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.naive_bayes import GaussianNB # naive bayes
from sklearn.neighbors import KNeighborsClassifier
import warnings
warnings.simplefilter("ignore")
# DEFINICION DE FUNCIONES
# CROSS-VALIDATION
def cross_val(func, _X, _Y, name):
scoring = ['precision_macro', 'recall_macro', 'accuracy', 'f1_macro']
cv_results = cross_validate(func, _X, _Y, cv = 7, scoring = scoring, return_train_score= True)
print('f1_score '+ name +':', np.mean(cv_results['test_f1_macro']))
print("------------------\n")
return np.mean(cv_results['test_f1_macro'])
#DECISION TREE
def DT(_X, _Y):
X_train, X_test, y_train, y_test = train_test_split(_X, _Y, test_size=.33, random_state=37, stratify=_Y)
clf = DecisionTreeClassifier()
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
cross_val(clf, X, Y, 'DT')
#Naive Bayes
def NB(_X, _Y):
X_train, X_test, y_train, y_test = train_test_split(_X, _Y, test_size=.33, random_state=37, stratify=_Y)
clf = GaussianNB()
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
cross_val(clf, X, Y, 'NB')
#Dummy
def Dummy(_X, _Y):
X_train, X_test, y_train, y_test = train_test_split(_X, _Y, test_size=.33, random_state=37, stratify=_Y)
clf = DummyClassifier(strategy='stratified')
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
cross_val(clf, X, Y, 'Dummy')
#K NEAREST NEIGHBOURS
def KNN(_X, _Y):
X_train, X_test, y_train, y_test = train_test_split(_X, _Y, test_size=.33, random_state=37, stratify=_Y)
K = 5# numero de vecinos
knn = KNeighborsClassifier(n_neighbors=K)
cross_val(knn, X,Y.values.ravel(),'KNN')
Predicción con todos los atributos
X = data_subsampled[["reputation_user", "likes_user", "dislikes_user","delta_time", "positive", "negative","length_answer", "length_question", "location"]]
Y = data_subsampled[["score_discrete"]]
#DECISION TREE
DT(X,Y)
# K NEAREST NEIGHBOURS
KNN(X,Y)
# NAIVE BAYES
NB(X,Y)
# Dummy
Dummy(X,Y)
Predicción con 3 atributos
X = data_subsampled[["reputation_user", "likes_user", "dislikes_user"]]
#DECISION TREE
DT(X,Y)
# K NEAREST NEIGHBOURS
KNN(X,Y)
# NAIVE BAYES
NB(X,Y)
# Dummy
Dummy(X,Y)
Con bajo número de datos (hito 2) el modelo predictivo generado alcanza un accuracy de 60%. Como son 3 clases, sin un clasificador el porcentaje de acierto es de 33%. Por lo tanto el clasificador es bastante mejor que "random", equivocandose 1 de cada 3 clasificaciones. Sin embargo, con un mayor número de datos no se logran clasificadores de calidad. Mientras que al aumentar el número de datos, el f1_scores disminuía considerablemente, alcanzando resultados cercanos a random con decision tree, y incluso peores con otros métodos. Por lo que se puede indicar que una base de datos más grande no asegura un mejor modelo de clasificación, dada la diversidad de los datos.
Tras realizar experimentos es posible ver que cierta combinación de atributos mejora la precisión del modelo de clasificación, por ejemplo {reputation, likes , dislikes}. Mientras que al incluirlos otros en el modelo, como similarity, pueden empeorar mucho la precisión. Descubrir cuales son los atributos que mejoran el desempeño de un modelo predictivo no es una tarea fácil y por lo general conlleva mucho trabajo explorativo y pruebas de ensayo-error.
De los resultados anteriores, es posible ver que el modelo que mejor se ajusta a los datos es Decision Tree, a pesar de que KNN es un modelo de mejor desempeño en general. Esto se puede debe a que los datos utilizados son un conjunto muy disperso, es decir existen muchos clusters de distintas densidades, por lo que no es posible clasificarlos bajo un mismo parámetro.
Con respecto a las hipótesis planteadas se observó que:
# DEFINICION DE FUNCIONES
# CROSS-VALIDATION
def cross_val(func, _X, _Y, name):
scoring = ['precision_macro', 'recall_macro', 'accuracy', 'f1_macro']
cv_results = cross_validate(func, _X, _Y, cv = 7, scoring = scoring, return_train_score= True)
print('Promedio Precision ' + name +':', np.mean(cv_results['test_precision_macro']))
print('Promedio Accucary '+ name +':', np.mean(cv_results['test_accuracy']))
print('Recall ' + name +':', np.mean(cv_results['test_recall_macro']))
print('f1_score '+ name +':', np.mean(cv_results['test_f1_macro']))
#DECISION TREE
def DT(_X, _Y):
X_train, X_test, y_train, y_test = train_test_split(_X, _Y, test_size=.33, random_state=37, stratify=_Y)
clf = DecisionTreeClassifier()
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
cross_val(clf, X, Y, 'DT')
#K NEAREST NEIGHBOURS
def KNN(_X, _Y):
X_train, X_test, y_train, y_test = train_test_split(_X, _Y, test_size=.33, random_state=37, stratify=_Y)
K = 5# numero de vecinos
knn = KNeighborsClassifier(n_neighbors=K)
cross_val(knn, X,Y.values.ravel(),'KNN')
#Predicción sobre cada atributo:
import warnings
warnings.filterwarnings("ignore")
attributes=["reputation_user", "likes_user", "dislikes_user","delta_time", "positive", "negative", "length_answer", "length_question", "location"]
for attr in attributes:
X = data_subsampled[[attr]]
print(attr)
DT(X,Y)
KNN(X,Y)
NB(X,Y)
Dummy(X,Y)
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
# Make fake dataset
values_dummy = [0.329,0.336, 0.331,0.335, 0.333,0.333, 0.332, 0.325, 0.331]
values_dt = [0.388,0.375,0.370,0.341, 0.338, 0.329, 0.334, 0.321, 0.315]
values_nb = [0.235, 0.246, 0.212, 0.242, 0.179, 0.275, 0.227, 0.274, 0.269]
values_knn = [0.381, 0.364, 0.337, 0.333, 0.330, 0.324, 0.324, 0.335, 0.311]
bars = ('reputation','likes', 'dislikes', 'positive', 'delta time', 'length question', 'length answer', 'negative', 'location' )
y_pos = np.arange(len(bars))
ind = np.arange(len(values_dummy))
width = 0.80
fig, ax = plt.subplots(figsize=(12, 12))
rects1 = ax.barh(ind - width/2, values_dummy, width/4,
label='Dummy')
rects2 = ax.barh(ind- width/4, values_dt, width/4,
label='Decision Tree')
rects3 = ax.barh(ind, values_nb, width/4,
label='Naive Bayes')
rects4 = ax.barh(ind + width/4, values_knn, width/4,
label='KNN')
plt.yticks(y_pos, bars, fontsize=16)
plt.legend(loc= 0)
ax.set_xlabel('F1-score',fontsize=16)
ax.set_ylabel('Atributos', fontsize=16)
ax.set_title('F1-score para clasificadores por atributo', fontsize=20)
# Show graphic
plt.show()
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
# Make fake dataset
values_dummy = [0.328, 0.333]
values_dt = [0.348, 0.371]
values_nb = [0.253, 0.268]
values_knn = [0.357, 0.368]
bars = ('reputation/likes/dislikes','todos los atributos')
y_pos = np.arange(len(bars))
ind = np.arange(len(values_dummy))
width = 0.80
fig, ax = plt.subplots(figsize=(12, 12))
rects1 = ax.barh(ind - width/2, values_dummy, width/4,
label='Dummy')
rects2 = ax.barh(ind- width/4, values_dt, width/4,
label='Decision Tree')
rects3 = ax.barh(ind, values_nb, width/4,
label='Naive Bayes')
rects4 = ax.barh(ind + width/4, values_knn, width/4,
label='KNN')
plt.yticks(y_pos, bars, fontsize=16)
plt.legend(loc= 0)
ax.set_xlabel('F1-score',fontsize=16)
ax.set_ylabel('Atributos', fontsize=16)
ax.set_title('F1-score para clasificadores por atributo', fontsize=20)
# Show graphic
plt.show()