Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Debido a que estas APIs se actualizan con relativa frecuencia, y para evitar
que el documento deje de mostrar código funcional, se emplean tweets ya
extraídos y que pueden encontrarse en el repositorio de github.
Los tweets pertenecen a:
Librerías
Las librerías utilizadas en este documento son:
In [42]:
# Tratamiento de datos
#
=========================================================================
=====
import numpy as np
import pandas as pd
import string
import re
# Gráficos
#
=========================================================================
=====
import matplotlib.pyplot as plt
from matplotlib import style
import seaborn as sns
#style.use('ggplot') or plt.style.use('ggplot')
# Preprocesado y modelado
#
=========================================================================
=====
from sklearn import svm
from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV
from sklearn.metrics import confusion_matrix
from sklearn.feature_extraction.text import TfidfVectorizer
import nltk
#nltk.download('stopwords')
from nltk.corpus import stopwords
# Configuración warnings
#
=========================================================================
=====
import warnings
warnings.filterwarnings('ignore')
Datos
In [43]:
# Lectura de datos
#
=========================================================================
=====
url = 'https://raw.githubusercontent.com/JoaquinAmatRodrigo/Estadistica-
con-R/master/datos/'
tweets_elon = pd.read_csv(url + "datos_tweets_@elonmusk.csv")
tweets_edlee = pd.read_csv(url + "datos_tweets_@mayoredlee.csv")
tweets_bgates = pd.read_csv(url + "datos_tweets_@BillGates.csv")
# Parseo de fechas
tweets['fecha'] = pd.to_datetime(tweets['fecha'])
tweets.head(3)
Out[44]:
In [45]:
# Distribución temporal de los tweets
#
=========================================================================
=====
fig, ax = plt.subplots(figsize=(9,4))
Limpieza y Tokenización
In [46]:
def limpiar_tokenizar(texto):
'''
Esta función limpia y tokeniza el texto en palabras individuales.
El orden en el que se va limpiando el texto no es arbitrario.
El listado de signos de puntuación se ha obtenido de:
print(string.punctuation)
y re.escape(string.punctuation)
'''
return(nuevo_texto)
In [47]:
# Se aplica la función de limpieza y tokenización a cada tweet
#
=========================================================================
=====
tweets['texto_tokenizado'] = tweets['texto'].apply(lambda x:
limpiar_tokenizar(x))
tweets[['texto', 'texto_tokenizado']].head()
Out[47]:
Análisis exploratorio
A la hora de entender que caracteriza la escritura de cada autor, es
interesante estudiar qué palabras emplea, con qué frecuencia, así como el
significado de estas.
In [48]:
# Unnest de la columna texto_tokenizado
#
=========================================================================
=====
tweets_tidy = tweets.explode(column='texto_tokenizado')
tweets_tidy = tweets_tidy.drop(columns='texto')
tweets_tidy = tweets_tidy.rename(columns={'texto_tokenizado':'token'})
tweets_tidy.head(3)
Out[48]:
Frecuencia de palabras
In [49]:
# Palabras totales utilizadas por cada autor
#
=========================================================================
=====
print('--------------------------')
print('Palabras totales por autor')
print('--------------------------')
tweets_tidy.groupby(by='autor')['token'].count()
In [50]:
autor
elonmusk 1069
elonmusk the 983
9
1081
elonmusk to 913
6
mayoredlee 1665
mayoredlee to 1684
0
1656
mayoredlee the 1338
8
autor
1495
mayoredlee our 1096
7
1596
mayoredlee sf 909
4
Stop words
In [53]:
# Obtención de listado de stopwords del inglés
#
=========================================================================
=====
stop_words = list(stopwords.words('english'))
# Se añade la stoprword: amp, ax, ex
stop_words.extend(("amp", "xa", "xe"))
print(stop_words[:10])
['i', 'me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves', 'you',
"you're"]
In [54]:
# Filtrado para excluir stopwords
#
=========================================================================
=====
tweets_tidy = tweets_tidy[~(tweets_tidy["token"].isin(stop_words))]
In [55]:
# Top 10 palabras por autor (sin stopwords)
#
=========================================================================
=====
fig, axs = plt.subplots(nrows=3, ncols=1,figsize=(6, 7))
for i, autor in enumerate(tweets_tidy.autor.unique()):
df_temp = tweets_tidy[tweets_tidy.autor == autor]
counts = df_temp['token'].value_counts(ascending=False).head(10)
counts.plot(kind='barh', color='firebrick', ax=axs[i])
axs[i].invert_yaxis()
axs[i].set_title(autor)
fig.tight_layout()
Los resultados obtenidos tienen sentido si ponemos en contexto la actividad
profesional de los usuarios analizados. Mayor Ed Lee es alcalde de San
Francisco (sf), por lo que sus tweets están relacionados con la ciudad,
residentes, familias, casas... Elon Musk dirige varias empresas tecnológicas
entre las que destacan Tesla y SpaceX, dedicadas a los coches y a la
aeronáutica. Por último, Bill Gates, además de propietario de microsoft,
dedica parte de su capital a fundaciones de ayuda, de ahí las palabras
mundo, polio, ayuda...
In [56]:
# Pivotado de datos
#
=========================================================================
=====
tweets_pivot = tweets_tidy.groupby(["autor","token"])["token"] \
.agg(["count"]).reset_index() \
.pivot(index = "token" , columns="autor", values=
"count")
tweets_pivot.columns.name = None
In [57]:
# Test de correlación (coseno) por el uso y frecuencia de palabras
#
=========================================================================
=====
from scipy.spatial.distance import cosine
def similitud_coseno(a,b):
distancia = cosine(a,b)
return 1-distancia
tweets_pivot.corr(method=similitud_coseno)
Out[57]:
BillGates elonmusk mayoredlee
mayoredle
0.496346 0.276732 1.000000
e
In [58]:
# Gráfico de correlación
#
=========================================================================
=====
f, ax = plt.subplots(figsize=(6, 4))
temp = tweets_pivot.dropna()
sns.regplot(
x = np.log(temp.elonmusk),
y = np.log(temp.BillGates),
scatter_kws = {'alpha': 0.05},
ax = ax
);
for i in np.random.choice(range(temp.shape[0]), 100):
ax.annotate(
s = temp.index[i],
xy = (np.log(temp.elonmusk[i]), np.log(temp.BillGates[i])),
alpha = 0.7
)
In [59]:
# Número de palabras comunes
#
=========================================================================
=====
palabras_elon = set(tweets_tidy[tweets_tidy.autor == 'elonmusk']
['token'])
palabras_bill = set(tweets_tidy[tweets_tidy.autor == 'BillGates']
['token'])
palabras_edlee = set(tweets_tidy[tweets_tidy.autor == 'mayoredlee']
['token'])
Aunque el número de palabras comunes entre Elon Musk y Bill Gates y entre
Elon Musk y Ed Lee es similar, la correlación basada en su uso es mayor entre
Elon Musk y Bill Gates. Esto tiene sentido si se contempla el hecho de que
ambos trabajan como directivos de empresas tecnológicas.
Para realizar este cálculo es necesario que, para todos los usuarios, se
cuantifique la frecuencia de cada una de las palabras que aparecen en el
conjunto de tweets, es decir, si un autor no ha utilizado una de las palabras
que sí ha utilizado otro, debe aparecer esa palabra en su registro con
frecuencia igual a cero. Existen varias formas de conseguir esto, una de ellas
es pivotar y despivotar el dataframe sustituyendo los NaN por cero.
In [60]:
# Cálculo del log of odds ratio de cada palabra (elonmusk vs mayoredlee)
#
=========================================================================
=====
# Pivotaje y despivotaje
tweets_pivot = tweets_tidy.groupby(["autor","token"])["token"] \
.agg(["count"]).reset_index() \
.pivot(index = "token" , columns="autor", values=
"count")
tweets_pivot = tweets_pivot.fillna(value=0)
tweets_pivot.columns.name = None
tweets_logOdds['log_odds'] =
np.log(tweets_logOdds.elonmusk/tweets_logOdds.mayoredlee)
tweets_logOdds['abs_log_odds'] = np.abs(tweets_logOdds.log_odds)
token
token
communit
0.000046 0.005960 -4.863336 4.863336 mayoredlee
y
In [62]:
# Top 15 palabras más características de cada autor
#
=========================================================================
=====
f, ax = plt.subplots(figsize=(4, 7))
sns.barplot(
x = 'log_odds',
y = 'token',
hue = 'autor_frecuente',
data = top_30,
ax = ax
)
ax.set_title('Top 15 palabras más características de cada autor')
ax.set_xlabel('log odds ratio (@elonmusk / mayoredlee)');
Estas palabras posiblemente tendrán mucho peso a la hora de clasificar los
tweets.
Estadístico tf-idf
In [63]:
# Cálculo term-frecuency (tf)
#
=========================================================================
=====
tf = tweets_tidy.copy()
# Número de veces que aparece cada término en cada tweet
tf = tf.groupby(["id", "token"])["token"].agg(["count"]).reset_index()
# Se añade una columna con el total de términos por tweet
tf['total_count'] = tf.groupby('id')['count'].transform(sum)
# Se calcula el tf
tf['tf'] = tf["count"] / tf["total_count"]
tf.sort_values(by = "tf").head(3)
Out[63]:
total_coun
id token count tf
t
In [64]:
# Inverse document frequency
#
=========================================================================
=====
idf = tweets_tidy.copy()
total_documents = idf["id"].drop_duplicates().count()
# Número de documentos (tweets) en los que aparece cada término
idf = idf.groupby(["token", "id"])["token"].agg(["count"]).reset_index()
idf['n_documentos'] = idf.groupby('token')['count'].transform(sum)
# Cálculo del idf
idf['idf'] = np.log(total_documents / idf['n_documentos'])
idf = idf[["token","n_documentos", "idf"]].drop_duplicates()
idf.sort_values(by="idf").head(3)
Out[64]:
5047
sf 914 2.061781
1
2282
great 327 3.089651
3
In [65]:
# Term Frequency - Inverse Document Frequency
#
=========================================================================
=====
tf_idf = pd.merge(left=tf, right=idf, on="token")
tf_idf["tf_idf"] = tf_idf["tf"] * tf_idf["idf"]
tf_idf.sort_values(by="id").head()
Out[65]:
1.195196e+1 5.44562
0 efforts 1 13 0.076923 31 0.418894
7 4
1.195196e+1 5.29609
46 job 1 13 0.076923 36 0.407392
7 3
1.195196e+1 6.57702
229 nigeria 1 13 0.076923 10 0.505925
7 7
id token count total_count tf n_documentos idf tf_idf
1.195196e+1 6.24055
239 phenomenal 1 13 0.076923 14 0.480043
7 4
1.195196e+1 4.25463
253 polio 1 13 0.076923 102 0.327280
7 9
Puede observarse que para el primer tweet (id = 1.195196e+17), todos los
términos que aparecen una vez, tienen el mismo valor de tf, sin embargo,
dado que no todos los términos aparecen con la misma frecuencia en el
conjunto de todos los tweets, la corrección de idf es distinta para cada uno.
Clasificación de tweets
Train-Test
In [66]:
# Reparto train y test
#
=========================================================================
=====
datos_X = tweets.loc[tweets.autor.isin(['elonmusk', 'mayoredlee']),
'texto']
datos_y = tweets.loc[tweets.autor.isin(['elonmusk', 'mayoredlee']),
'autor']
In [67]:
value, counts = np.unique(y_train, return_counts=True)
print(dict(zip(value, 100 * counts / sum(counts))))
value, counts = np.unique(y_test, return_counts=True)
print(dict(zip(value, 100 * counts / sum(counts))))
{'elonmusk': 52.68292682926829, 'mayoredlee': 47.31707317073171}
{'elonmusk': 50.53658536585366, 'mayoredlee': 49.46341463414634}
Vectorización tf-idf
Empleando los tweets de entrenamiento se crea un matriz tf-idf en la que
cada columna es un término, cada fila un documento y el valor de
intersección el tf-idf correspondiente. Esta matriz representa el espacio n-
dimensional en el que se proyecta cada tweet.
In [68]:
def limpiar_tokenizar(texto):
'''
Esta función limpia y tokeniza el texto en palabras individuales.
El orden en el que se va limpiando el texto no es arbitrario.
El listado de signos de puntuación se ha obtenido de:
print(string.punctuation)
y re.escape(string.punctuation)
'''
return(nuevo_texto)
In [69]:
stop_words = list(stopwords.words('english'))
# Se añade la stopword: amp, ax, ex
stop_words.extend(("amp", "xa", "xe"))
In [70]:
# Creación de la matriz tf-idf
#
=========================================================================
=====
tfidf_vectorizador = TfidfVectorizer(
tokenizer = limpiar_tokenizar,
min_df = 3,
stop_words = stop_words
)
tfidf_vectorizador.fit(X_train)
Out[70]:
TfidfVectorizer(min_df=3,
stop_words=['i', 'me', 'my', 'myself', 'we', 'our',
'ours',
'ourselves', 'you', "you're", "you've",
"you'll",
"you'd", 'your', 'yours', 'yourself',
'yourselves',
'he', 'him', 'his', 'himself', 'she',
"she's",
'her', 'hers', 'herself', 'it', "it's",
'its',
'itself', ...],
tokenizer=<function limpiar_tokenizar at 0x7fef27f17e60>)
In [71]:
tfidf_train = tfidf_vectorizador.transform(X_train)
tfidf_test = tfidf_vectorizador.transform(X_test)
In [72]:
print(f" Número de tokens creados:
{len(tfidf_vectorizador.get_feature_names())}")
print(tfidf_vectorizador.get_feature_names()[:10])
Número de tokens creados: 2738
['aa', 'aapi', 'aaronpaul', 'able', 'abort', 'abt', 'ac', 'aca', 'accel',
'acceleration']
In [73]:
# Entrenamiento del modelo SVM
#
=========================================================================
=====
modelo_svm_lineal = svm.SVC(kernel= "linear", C = 1.0)
modelo_svm_lineal.fit(X=tfidf_train, y= y_train)
Out[73]:
SVC(kernel='linear')
In [74]:
# Grid de hiperparámetros
#
=========================================================================
=====
param_grid = {'C': np.logspace(-5, 3, 10)}
mean_train_scor
param_C mean_test_score std_test_score std_train_score
e
In [75]:
# Mejores hiperparámetros por validación cruzada
#
=========================================================================
=====
print("----------------------------------------")
print("Mejores hiperparámetros encontrados (cv)")
print("----------------------------------------")
print(grid.best_params_, ":", grid.best_score_, grid.scoring)
modelo_final = grid.best_estimator_
----------------------------------------
Mejores hiperparámetros encontrados (cv)
----------------------------------------
{'C': 2.154434690031882} : 0.9673170731707316 accuracy
In [76]:
# Error predicciones test
#
=========================================================================
=====
predicciones_test = modelo_final.predict(X=tfidf_test)
print("-------------")
print("Error de test")
print("-------------")
print("")
print("-------------------")
print("Matriz de confusión")
print("-------------------")
pd.DataFrame(confusion_matrix(y_true = y_test, y_pred=
predicciones_test),
columns= ["Elon Musk", "Mayor Ed Lee"],
index = ["Elon Musk", "Mayor Ed Lee"])
-------------
Error de test
-------------
Número de clasificaciones erróneas de un total de 1025 clasificaciones:
21
% de error: 2.048780487804878
-------------------
Matriz de confusión
-------------------
Out[76]:
Análisis de sentimientos
In [77]:
# Descarga lexicon sentimientos
#
=========================================================================
=====
lexicon = pd.read_table(
'https://raw.githubusercontent.com/fnielsen/afinn/master/afinn/data/
AFINN-en-165.txt',
names = ['termino', 'sentimiento']
)
lexicon.head()
Out[77]:
termino sentimiento
0 abandon -2
1 abandoned -2
2 abandons -2
3 abducted -2
4 abduction -2
In [78]:
# Sentimiento promedio de cada tweet
#
=========================================================================
=====
tweets_sentimientos = pd.merge(
left = tweets_tidy,
right = lexicon,
left_on = "token",
right_on = "termino",
how = "inner"
)
In [79]:
def perfil_sentimientos(df):
print(autor)
print("=" * 12)
print(f"Positivos: {round(100 * np.mean(df.sentimiento > 0), 2)}")
print(f"Neutros : {round(100 * np.mean(df.sentimiento == 0), 2)}")
print(f"Negativos: {round(100 * np.mean(df.sentimiento < 0), 2)}")
print(" ")
elonmusk
============
Positivos: 73.14
Neutros : 4.29
Negativos: 22.57
mayoredlee
============
Positivos: 80.73
Neutros : 4.07
Negativos: 15.2
Los tres autores tienen un perfil muy similar. La gran mayoría de tweets son
de tipo positivo. Este patrón es común en redes sociales, donde se suele
participar mostrando aspectos o actividades positivas. Los usuarios no
tienden a mostrar las cosas malas de sus vidas.
Evolución temporal