Hito 3: Informe Final

Por Joaquín Aliaga, Pablo Torres y Juan Urrutia (Equipo 4)

Introducción

Twitter presenta una gran cantidad de información; ¿Dónde nos encontramos? ¿Qué hacemos? ¿Qué opinamos?, entre muchas otras preguntas, incluso, en que invertimos. La vida, como hoy la conocemos nos muestra una clara dependencia a las redes sociales ya sea como medio de entretención, de información e incluso como mecanismo de control social.

En este hito se presenta la exploración de los datos mediante las técnicas aprendidas en los laboratorios para la clasificación de resultados mediante aprendizaje supervisado.

Resumen anterior

En un principio, los datos de Twitter fueron recolectados de personas al azar diariamente durante un mes, los cuales contenían principalmente los atributos: texto, fecha, cantidad de reTweets, etc. Estos datos luego fueron procesados de manera que se buscaban palabras claves que tenían que ver con la cryptomoneda escogida, en ese momento BitCoin.

Se creó un nuevo dataset que contenía datos de distintas fechas, con las columnas fecha, frecuencia y precio en dólares. La columna fecha se llenó con números partiendo de 1 correspondiente a la primera fecha en la que se tiene datos de Twitter hasta el último día en que se tienen datos de Twitter. La columna frecuencia se llenó sumando la cantidad de Tweets que contenían ciertas palabras clave de la cryptomoneda a analizar en cierta fecha (Si un Tweet contiene más de una, de igual manera vale como uno para la frecuencia). Finalmente la columna precio en dólares se llenó con el precio de la cryptomoneda en la fecha correspondiente. Finalmente, se creó la columna clase cuyo valor era 1 si el precio había subido ese día respecto al día anterior y 0 en caso contrario.

Este dataset no funcionó bien para clasificar dada la baja frecuencia, por lo que se realizaron los siguientes cambios:

  • Se cambió la recolección de datos de personas al azar a principales influencers de la cryptomoneda analizada, de los cuales se escogieron los siguientes nombres de usuario de Twitter correspondiente a cada influencer: 'NickSzabo4', 'satoshilite', 'iohk_charles', 'vitalikbuterin', 'brian_armstrong', 'peterktodd', 'adam3us', 'aantonop'.
  • Se cambió la columna frecuencia por la columna reTweets correspondiente a la suma de la cantidad de reTweets de todos los tweets que contenían al menos una palabra clave por cada día.
  • Con la nueva columna de reTweets se realizó una agregación de datos dado el supuesto de que las subidas y bajadas de precio están desfasadas respecto a cuando se habló de la moneda. La agregación consistió en reemplazar la columna reTweets por la columna 'Suma últimos d días de reTweets' que corresponde a la suma de los reTweets de los últimos d días. Además, se reemplazó la columna precio en dólares por la columna 'promedio n días en dólares' que promediaba n días de precios promediando la misma cantidad de días hacia adelante y hacia atrás más el día actual (n impar). Con lo cual la clase se calcula con esta última columna.

Descripción de los datos y experimentación

Para este hito los datos de Bitcoin, Ripple, BitCash y DogeCoin se recolectaron de coinmetrics.io y los de Twitter se recolectaron mediante un programa para MacOS que automáticamente buscaba en Twitter los datos uno a uno.

Los datos de coinmetrics consisten de información del precio de cada moneda nombrada anteriormente desde el primero de enero del año 2016 tomados diariamente, sin embargo, hay monedas que no se encuentran desde esta fecha y por esto, para estas monedas se utilizaron las fechas de lanzamiento. Los datos de Twitter son registros desde el 01 de enero de 2016.

Para obtener los datos de Twitter, en MacOS se ejecuta el programa obtenido del repositorio https://github.com/bpb27/twitter_scraping. Con esto se obtiene un archivo CSV con los ID de cada tweet recolectado para cada influencer desde el 1 de enero de 2016 hasta el 1 de abril de 2018. Este CSV se procesa y se transforma en una lista de ids separados por '\n' en un archivo de texto, la cual se ingresa en el programa Hydrator obtenido en el siguiente repositorio https://github.com/DocNow/hydrator. Para procesar los datos de Twitter se cargan primero todas las librerías a utilizar y se definen las siguientes funciones:

In [65]:
import numpy as np
import pandas as pd
import re

def procesamientoInfluencers(df, keyWords):
    # Listas para pasar la fecha a numeros a partir del 1
    month2num = {'Jan': 0, 'Feb': 31, 'Mar': 59, 'Apr': 90, 'May': 120, 'Jun': 151, 'Jul': 181, 'Aug': 212, 'Sep': 243, 'Oct': 273, 'Nov': 304, 'Dec': 334}
    month2num2016 = {'Jan': 0, 'Feb': 31, 'Mar': 60, 'Apr': 91, 'May': 121, 'Jun': 152, 'Jul': 182, 'Aug': 213, 'Sep': 244, 'Oct': 274, 'Nov': 305, 'Dec': 335}
        
    d = {}
    rt = {}
    fv = {}
    
    # Funcion que se utiliza como wrapper dentro del loop for, esto es para poder detener el loop apenas
    # se encuentre una palabra de las que se buscan en keyWords
    
    def loopAdentro(i, txt, d, rt, df, fv, keyWords):
        date = df['created_at'][i].split()
        dayNum = int(date[2])
        monthNum = month2num[date[1]] if int(date[-1]) != 2016 else month2num2016[date[1]]
        yearNum = (int(date[-1]) - 2016)*365
        
        dateStr = (dayNum + monthNum + yearNum)
        
        # Se itera cada palabra del texto de Twitter quitando el hashtag y sumando los retweets y favoritos
        for word in txt.split():
            if re.sub(r'[^\w]', ' ', word).strip().lower() in keyWords:
                if dateStr in d.keys():
                    d[dateStr] += 1
                    rt[dateStr] += df['retweet_count'][i]
                    fv[dateStr] += df['favorite_count'][i]
                    return
                else:
                    d[dateStr] = 1
                    rt[dateStr] = df['retweet_count'][i]
                    fv[dateStr] = df['favorite_count'][i]
                    return
            else:
                if dateStr not in d.keys():
                    d[dateStr] = 0
                    rt[dateStr] = 0
                    fv[dateStr] = 0
            
        return
            
    # Se itera sobre cada tweet
    for i, txt in enumerate(df['text']):
        loopAdentro(i, txt, d, rt, df, fv, keyWords)
    
    # Se ordenan las fechas
    sortedKeys = sorted(d.keys())
    
    # Se crea un diccionario con todos los datos obtenidos anteriormente y se retorna este objeto
    influencersProcessedDict = {}
    for i in sortedKeys:
        influencersProcessedDict[i] = {'frecuencia': d[i], 'retweets': rt[i], 'favoritos': fv[i]}
    
    return influencersProcessedDict

Para ordenar el dataset de coinmetrics se utilizó el siguiente script en R:

In [ ]:
## Cargar dataset de la criptomoneda, asignar nombres a columnas, limpiar columnas vacias y filtrar por fechas

# Leer datos
btc <- read.csv("~/CC5206/doge.csv", header=FALSE, na.strings="null")

# Asignar nombres a columnas
colnames(btc)[colnames(btc)=="V1"] <- "date"
colnames(btc)[colnames(btc)=="V2"] <- "txVolume(USD)"
colnames(btc)[colnames(btc)=="V3"] <- "txCount"
colnames(btc)[colnames(btc)=="V4"] <- "marketcap(USD)"
colnames(btc)[colnames(btc)=="V5"] <- "price(USD)"
colnames(btc)[colnames(btc)=="V6"] <- "exchangeVolume(USD)"
colnames(btc)[colnames(btc)=="V7"] <- "generatedCoins"
colnames(btc)[colnames(btc)=="V8"] <- "fees"

#Borrar primera fila (que contiene los nombres de las columnas)
btc1 <- btc[-c(1): ]

#Borrar ultima columna que no tiene datos
btc1 <- btc[ :-c(1)]

# Transformar columna "date" a datos tipo fecha
datos$fecha <- as.Date(datos$fecha,format="%Y-%m-%d")

# Filtrar por fecha
datos_filtrados <- subset(datos,fecha<="2013-05-30")

# Guardar datos como csv
write.csv(datos_filtrados,file="dogos.csv")

Teniendo los dos datasets cargados, se procede a cruzarlos y crear los nuevos datasets, uno para cada cryptomoneda de la manera en que se explicó anteriormente. Se define la siguiente función para esto:

In [66]:
def cryptoMasInfluencers(cryptoDf, ifProcessedDict, startPoint):
    # Se definen los parametros que se utilizaran en los datos
    diasAPromediar = 7
    ultimosDiasInfluencers = 20
    ultimosDiasFavs= 7
    
    datosAEntrenar = {}
    
    lastInfluencersDays = []
    lastInfluencersDaysfv = []
    # Se agregan la cantidad de dias pertinentes hacia adelante para el primer dia
    btcPricesAround = [cryptoDf['price(USD)'][i] for i in range(int(diasAPromediar/2))]
    lastPrice = 0
    lastDayPrice = 0
    
    # Se itera sobre las fechas
    for dat in ifProcessedDict.keys():
        if dat + startPoint < len(ifProcessedDict.keys()):
            
            # Se verifica el tamaño de las listas que contienen la cantidad de retweets, precios y favoritos
            if len(btcPricesAround) >= diasAPromediar:
                btcPricesAround.pop(0)
            if len(lastInfluencersDays) >= ultimosDiasInfluencers:
                lastInfluencersDays.pop(0)
            if len(lastInfluencersDaysfv) >= ultimosDiasFavs:
                lastInfluencersDaysfv.pop(0)
            
            if dat <= len(ifProcessedDict.keys()) - 2:
                btcPricesAround.append(list(cryptoDf['price(USD)'])[(dat + startPoint - 1) + 2])
            
            lastInfluencersDays.append(ifProcessedDict[dat + startPoint]['retweets'])
            lastInfluencersDaysfv.append(ifProcessedDict[dat + startPoint]['favoritos'])
            
            # Se determina la clase
            if lastPrice <= np.mean(btcPricesAround):
                clase = 1
            else:
                clase = 0
            
            lastPrice = np.mean(btcPricesAround)
            
            # Se determina la diferencia de precios para el dato agregado fallido
            if dat != 0:
                lastDayPrice = list(cryptoDf['price(USD)'])[dat + startPoint] - list(cryptoDf['price(USD)'])[dat + startPoint - 1]
            
            # Se llena el dataset con los datos obtenidos
            datosAEntrenar[dat + startPoint] = {'ultimos %i dias de retweets' % (ultimosDiasInfluencers) : sum(lastInfluencersDays), 'ultimos %i dias favoritos' % (ultimosDiasFavs) : sum(lastInfluencersDaysfv), 'Diferencia de precio': lastDayPrice , 'precio promedio %i dias' % (diasAPromediar) : lastPrice, 'clase' : clase}
            
            #datosAEntrenar['favorites'].append(sum(lastInfluencersDaysfv))   
            
    datosAEntrenarDF = pd.DataFrame(data = datosAEntrenar)
    # Se ordenan las categorias del dataset
    datosAEntrenarDF = datosAEntrenarDF.transpose()[['ultimos %i dias de retweets' % (ultimosDiasInfluencers), 'ultimos %i dias favoritos' % (ultimosDiasFavs), 'Diferencia de precio', 'precio promedio %i dias' % (diasAPromediar), 'clase']]
    
    return datosAEntrenarDF

Luego se ejecuta el siguiente script, importando los archivos csv correspondientes a los datasets de twitter y las cryptomonedas, y se crean las listas correspondientes a las key words para cada cryptomoneda, se elimina BtcCash debido a la poca cantidad de datos que contiene, se consideró que no aporta al proyecto.

In [67]:
df = pd.read_csv('D:/Docs universidad/Mineria de Datos/Proyecto/influencers_allcsvhito3.csv', encoding='latin1', low_memory = False)
    
btcdf = pd.read_csv('D:/Docs universidad/Mineria de Datos/Proyecto/btc_1enero2016_6julio2018.csv', encoding='latin1', low_memory = False)
dfDoge = pd.read_csv('D:/Docs universidad/Mineria de Datos/Proyecto/new cryptos/dogos3.csv', encoding='latin1', low_memory = False)
dfRipple = pd.read_csv('D:/Docs universidad/Mineria de Datos/Proyecto/new cryptos/Ripple3.csv', encoding='latin1', low_memory = False)

btcKeyWords = ['Bitcoin', 'bitcoin', 'blockchain', 'ICO', 'cryptocurrency', 'crypto', 'ethereum', 'btc', 'Blockchain']
dogeKeyWords = ['dogecoin', 'doge', 'DOGE']
rippleKeyWords = ['Ripple', 'xrp', 'XRP', 'ripple']

Después se generan los datasets agregados:

In [68]:
ifProcRipple = procesamientoInfluencers(df, rippleKeyWords)
datosAEntrenarDFRipple = cryptoMasInfluencers(dfRipple, ifProcRipple, 400)

ifProcBtc = procesamientoInfluencers(df, btcKeyWords)
datosAEntrenarDFBtc = cryptoMasInfluencers(btcdf, ifProcBtc, 400)

ifProcDoge = procesamientoInfluencers(df, dogeKeyWords)
datosAEntrenarDFDoge = cryptoMasInfluencers(dfDoge, ifProcDoge, 400)

Observando los datos, obtenemos los siguientes gráficos.

In [69]:
%matplotlib inline
datosAEntrenarDFBtc['ultimos 7 dias favoritos'].plot(legend=True, title='Precio del Bitcoin vs. reTweets de influencers')
datosAEntrenarDFBtc['ultimos 20 dias de retweets'].plot(legend=True, title='Retweets y tweets favoritos relacionados a Bitcoin')
Out[69]:
<matplotlib.axes._subplots.AxesSubplot at 0x1cd01aa4240>
In [70]:
%matplotlib inline
datosAEntrenarDFRipple['ultimos 7 dias favoritos'].plot(legend=True, title='Precio del Ripple vs. reTweets de influencers')
datosAEntrenarDFRipple['ultimos 20 dias de retweets'].plot(legend=True, title='Retweets y tweets favoritos relacionados a Ripple')
Out[70]:
<matplotlib.axes._subplots.AxesSubplot at 0x1cd70efbcc0>
In [71]:
%matplotlib inline
datosAEntrenarDFDoge['ultimos 7 dias favoritos'].plot(legend=True, title='Precio del Dogecoin vs. reTweets de influencers')
datosAEntrenarDFDoge['ultimos 20 dias de retweets'].plot(legend=True, title='Retweets y tweets favoritos relacionados a Dogecoin')
Out[71]:
<matplotlib.axes._subplots.AxesSubplot at 0x1cd720f6978>

Teniendo todos los datasets listos, se procede a definir funciones para clasificar, donde se incluye una red neuronal de 4 capas escondidas con 10 neuronas cada capa y función de activación ReLU

In [72]:
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score, recall_score, precision_score
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
from sklearn.neural_network import MLPClassifier

def run_classifier(clf, X, y, num_tests=100):
        metrics = {'f1-score': [], 'precision': [], 'recall': []}
        
        for _ in range(num_tests):
            
            X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=.40, 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))
            
        return metrics


def runAllClassifiers(X, y):
    c0 = ("Base Dummy", DummyClassifier(strategy='stratified'))
    c1 = ("Decision Tree", DecisionTreeClassifier())
    c2 = ("Gaussian Naive Bayes", GaussianNB())
    c3 = ("KNN", KNeighborsClassifier(n_neighbors=5))
    c4 = ('nn', MLPClassifier(solver='lbfgs', alpha=1e-5, hidden_layer_sizes=(10, 10, 10, 10), random_state=1))
    
    classifiers = [c0, c1, c2, c3, c4]
    
    for name, clf in classifiers:
        if name == 'nn':
            X = (X - np.mean(X))/np.std(X)
        metrics = run_classifier(clf, X, y)   # hay que implementarla en el bloque anterior.
        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("----------------\n\n")

Para clasificar se seleccionan los datos a clasificar y las clases por separado (Se seleccionan todas las columnas para entrenar menos las últimas dos que corresponden al precio y a la clase y se selecciona la última columna para la clase):

In [73]:
X_orig_btc = datosAEntrenarDFBtc[datosAEntrenarDFBtc.columns[:2]]
y_orig_btc = datosAEntrenarDFBtc[datosAEntrenarDFBtc.columns[-1]]

X_orig_ripple = datosAEntrenarDFRipple[datosAEntrenarDFRipple.columns[:2]]
y_orig_ripple = datosAEntrenarDFRipple[datosAEntrenarDFRipple.columns[-1]]

X_orig_doge = datosAEntrenarDFDoge[datosAEntrenarDFDoge.columns[:2]]
y_orig_doge = datosAEntrenarDFDoge[datosAEntrenarDFDoge.columns[-1]]

Finalmente, se clasifican estos datos.

In [74]:
# Bitcoin

runAllClassifiers(X_orig_btc, y_orig_btc)
----------------
Resultados para clasificador:  Base Dummy
Precision promedio: 0.6155135176269331
Recall promedio: 0.6143689320388349
F1-score promedio: 0.6142885748079773
----------------


----------------
Resultados para clasificador:  Decision Tree
Precision promedio: 0.7429317722214054
Recall promedio: 0.7305825242718446
F1-score promedio: 0.7355904397394017
----------------


----------------
Resultados para clasificador:  Gaussian Naive Bayes
Precision promedio: 0.63089471121323
Recall promedio: 0.9211650485436893
F1-score promedio: 0.7476036627378372
----------------


----------------
Resultados para clasificador:  KNN
Precision promedio: 0.7430764813325722
Recall promedio: 0.7659223300970872
F1-score promedio: 0.7535136303615638
----------------


----------------
Resultados para clasificador:  nn
Precision promedio: 0.7227605371270194
Recall promedio: 0.7393203883495145
F1-score promedio: 0.729261476400129
----------------


In [75]:
#Ripple

runAllClassifiers(X_orig_ripple, y_orig_ripple)
----------------
Resultados para clasificador:  Base Dummy
Precision promedio: 0.4901544562711784
Recall promedio: 0.5048780487804879
F1-score promedio: 0.4966219722329719
----------------


----------------
Resultados para clasificador:  Decision Tree
Precision promedio: 0.6698368450384382
Recall promedio: 0.739878048780488
F1-score promedio: 0.6903996905305134
----------------


----------------
Resultados para clasificador:  Gaussian Naive Bayes
Precision promedio: 0.5536893259728886
Recall promedio: 0.10292682926829269
F1-score promedio: 0.14450014508195333
----------------


----------------
Resultados para clasificador:  KNN
Precision promedio: 0.6739256239963565
Recall promedio: 0.6239024390243902
F1-score promedio: 0.6265237677930787
----------------


----------------
Resultados para clasificador:  nn
Precision promedio: 0.5416026732402998
Recall promedio: 0.689390243902439
F1-score promedio: 0.5996931758519846
----------------


In [76]:
# Dogecoin

runAllClassifiers(X_orig_doge, y_orig_doge)
----------------
Resultados para clasificador:  Base Dummy
Precision promedio: 0.5665133932212524
Recall promedio: 0.5682105263157894
F1-score promedio: 0.56678968401242
----------------


----------------
Resultados para clasificador:  Decision Tree
Precision promedio: 0.6886880787301957
Recall promedio: 0.8861052631578946
F1-score promedio: 0.7701386323376949
----------------


----------------
Resultados para clasificador:  Gaussian Naive Bayes
Precision promedio: 0.5726786748418345
Recall promedio: 0.9356842105263158
F1-score promedio: 0.7103963643084983
----------------


----------------
Resultados para clasificador:  KNN
Precision promedio: 0.739894372186439
Recall promedio: 0.7375789473684212
F1-score promedio: 0.7147400002890678
----------------


----------------
Resultados para clasificador:  nn
Precision promedio: 0.647330500890422
Recall promedio: 0.9493684210526315
F1-score promedio: 0.7694465857156669
----------------


Análisis

Acotando el tiempo de análisis se obtuvieron mejores resultados, abarcando los meses peak de las cryptomonedas; esto pues el mayor cambio en el valor de las criptomonedas se logra en el intervalo de tiempo en el cual se realizó el proyecto, luego tomar en el clasificador fechas donde la variación fue despreciable solo disminuía los valores de las métricas.

Los mejores resultados para las clasificaciones se obtuvieron del BitCoin; se espera que sea así pues es la moneda más popular y el índice de retweets se acerca más a la curva del precio (de acuerdo al promedio de éste establecido en los gráficos), además ésta cryptomoneda aparece en una mayor cantidad de Tweets, por lo que se tiene mayor información para entrenar.

La métrica F1-Score supera (en general) el 70%, lo que para el modelo propuesto es bueno (mejor que lanzar una moneda), sin embargo, esto puede significar una mayor cantidad de datos con el valor de la clase 0 debido a un recall alto, por lo tanto confiar plenamente en F1-Score no es recomendable y se prefiere observar las métricas en general.

Debido a que las clases estaban balanceadas no fue necesario realizar subsampling ni oversampling.

Conclusiones

Para mejorar las métricas en los resultados de las criptomonedas se hace necesario buscar influencers y personas que twitteen más sobre esas criptomonedas (que la sigan más) ya sea porque invierten en ella o por interés, esto es porque se les llama influencers debido a que causan cierta influencia en la gente, y dado que las cryptomonedas tienen precios que dependen mucho de la gente, los influencers son la mejor opción para analizar.

El modelo propuesto: seguir el rastro de la variación de precios de una criptomoneda mediante el indicador retweet entrega buenos resultados para las criptomonedas que se testearon (aunque es mucho mejor en la moneda más popular: Bitcoin)

Se obtiene una tendencia cuando se empieza a hacer más relevante (mayor cantidad de retweets y favoritos) lo que dice un influencer a fechas cercanas a alzas y variaciones de precio.

Twitter tiene un grado de influencia en la variación de precio de las criptomonedas gracias a la acción de los influencers dentro de la red social.

Para el futuro

Sería interesante un análisis de volatilidad (medida de la frecuencia e intensidad de los cambios del precio de un activo, en este caso de la criptomoneda) y de sentimientos de los twitter para obtener predicciones de acuerdo a estos indicadores y de esta manera incorporar un análisis de si cierto tweet afectará positiva o negativamente a la cryptomoneda analizada.