Cultivando datos de la web

En esta entrada, cultivaremos datos de la web con la biblioteca de Rrvest. Actualmente, innumerables aplicaciones dependen directamente de la tecnología de raspado o cultivo de información, la cual nos permite generar información actualizada por medio de la extracción directa de la misma a partir de la red. La librería rvest contiene funciones que facilitan este proceso. Por otro lado, utilizaremos la librería stringr para manipular cuerdas de texto por medio de patrones y expresiones regulares.

En este ejemplo se aprecia el proceso de cultivo de los datos de artistas amigos a partir de información del artista de interés. La información mencionada será raspada de la página web https://www.discogs.com automáticamente. Con lo que podemos escribir un script, de unas veinte líneas, que reciba un nombre de artista y regrese una lista de sus colaboradores.

Primero ingresemos a la página web https://www.discogs.com y hagamos una búsqueda de algún músico. En la siguiente imagen vemos que se realiza una búsqueda del artista Lassi Nikko. Observe la dirección de enlace en la barra de direcciones de su explorador después de realizar la búsqueda (encerrada en un elipse rojo en la siguiente imagen).

search_artiststructure
Estructura de enlace de búsqueda de artista

Aquí se presenta el enlace

En él podemos ver que la estructura parece ser: el nombre del artista separado por signos de más, es decir, «lassi+nikko». Patrón que se encuentra entre “https://www.discogs.com/search/?q=” y “&type=all”. Nos gustaría que el programa reciba el nombre de un artista y que de forma automática se realice la búsqueda de dicho artista, en caso de existir, que se ingrese a la página respectiva del artista, para raspar y regresar una lista con los colaboradores.

Para recibir el nombre de artista ingresado por el usuario utilizaremos la función de R base readLines

# INPUT
cat("Nombre Artista: "); # instrucción para usuario
nombre_artista <- readLines("stdin", n = 1); # almacenando entrada
cat("Usted ingreso") # Mensaje de confirmación
str(nombre_artista);
cat( "\n Generando lista de artista amigos \n" ) # Mensaje de espera

Utilizaremos la función de R base paste0 para construir la petición http con lainformación mencionada anteriormente y almacenarla en la variable «busqueda», no sin antes cambiar los posibles espacios que ingresó el usuario por signos de más «+», para después realizar la petición http con la función read_html, contenida en rvest

#PROCESS
library(stringr)
nombre_artista <- str_replace_all(nombre_artista, " ", "+") #cambiando espacio por +
busqueda <- paste0("https://www.discogs.com/search/?q=", nombre_artista, "&type=all") # construyendo petición de búsqueda (URL) con paste0
library(rvest)
html_page <- read_html(busqueda) # guardando respuesta a petición

Ahora vamos a extraer a partir del primer elemento de la lista de resultados, el enlace que nos lleva a la pagina del artista. Para lo cual nos serviremos de la tecnología css, utilizada ampliamente para dar estilo a las páginas html. Utilizaremos la ruta css de los elementos requeridos de forma que podamos extraer información de nodos html. Para conocer dicha ruta vamos a hacer uso del  «inspector de código» que viene en casi cualquier navegador web. Aquí se observa cómo abrirlo en el explorador de internet firefox

inspector
Inspector de código (firefox)

Haciendo flotar el cursor sobre los distintos elementos de la página web, el inspector resaltará el código respectivo al elemento en la parte inferior, como se muestra en la siguiente imagen. Al dar clic sobre algún elemento se resalta el bloque de código al que corresponde

css_id
«ruta CSS»

Se puede ver que la información que requerimos (subdirectorio de artista) se encuentra dentro del divisor seleccionado, con ruta «div.card.card…»

Para obtener la información de secciones css podemos utilizar la función html_nodes, la cual recibe como argumentos la pagina web y la ruta css de los elementos de los cuales queremos extraer el código.

enlace_artista <- html_nodes(html_page, "div.card.card_large.float_fix.shortcut_navigable")
enlace_artista

## {xml_nodeset (50)}
##  [1] <div class="card card_large float_fix\n        \n        shortcut_n ...
##  [2] <div class="card card_large float_fix\n        \n        shortcut_n ...
##  [3] <div class="card card_large float_fix\n        \n        shortcut_n ...
##  [4] <div class="card card_large float_fix\n        \n        shortcut_n ...
##  [5] <div class="card card_large float_fix\n        \n        shortcut_n ...
##  [6] <div class="card card_large float_fix\n        \n        shortcut_n ...
##  [7] <div class="card card_large float_fix\n        \n        shortcut_n ...
##  [8] <div class="card card_large float_fix\n        \n        shortcut_n ...
##  [9] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [10] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [11] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [12] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [13] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [14] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [15] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [16] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [17] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [18] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [19] <div class="card card_large float_fix\n        \n        shortcut_n ...
## [20] <div class="card card_large float_fix\n        \n        shortcut_n ...
## ...

Observe que html_nodes nos entrega una lista con varios nodos que cumplieron con tener esa ruta. Estos nodos representan el código que hay en cada resultado de la búsqueda, queremos el que se encuentra en el primer elemento de la lista. Vamos a acceder a él y a convertirlo a texto con la función as.character de R base, observe que ahí dentro se encuentra el subdirectorio del artista, información que queremos obtener.

enlace_artista_char <- as.character(enlace_artista[1]) #accediendo a primer elemento de la lista de nodos y convirtiéndolo a texto
  enlace_artista_char # imprimir
## [1] "<div class=\"card card_large float_fix\n        \n        shortcut_navigable\" data-id=\"a182658\" data-object-id=\"182658\" data-object-type=\"artist\">\n                                                            \n    \n                <a href=\"/artist/182658-Lassi-Nikko\" class=\"thumbnail_link\n            thumbnail_size_large\n            thumbnail_orientation_landscape\n            thumbnail-lazyload\n        \">\n        <span class=\"thumbnail_border\"></span>\n        <span class=\"thumbnail_center\">\n                                <img data-src=\"https://img.discogs.com/oqpsbvuIwt87D9aqvSx7w6SqaU0=/300x300/smart/filters:strip_icc():format(jpeg):mode_rgb():quality(40)/discogs-images/A-182658-1433076899-8016.jpeg.jpg\" src=\"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\" alt=\"Lassi Nikko\"></span>\n    </a>\n    \n    <h4><a href=\"/artist/182658-Lassi-Nikko\" class=\"search_result_title\" title=\"Lassi Nikko\" data-followable=\"true\">Lassi Nikko</a></h4>\n            \n    <div class=\"card_actions skittles\">\n                        <span class=\"skittle skittle_collection needs_delegated_tooltip\" data-title=\"0 in Collection\" aria-label=\"0 in Collection\" title=\"0 in Collection\" style=\"display: none;\"><i class=\"icon icon-collection\"></i><span class=\"count\">0</span>\n    </span>                <span class=\"skittle skittle_wantlist needs_delegated_tooltip\" data-title=\"0 in Wantlist\" aria-label=\"0 in Wantlist\" title=\"0 in Wantlist\" style=\"display: none;\"><i class=\"icon icon-wantlist\"></i><span class=\"count\">0</span>\n    </span>                            <a class=\"skittle skittle_inventory needs_delegated_tooltip\" data-title=\"0 in Inventory\" aria-label=\"0 in Inventory\" title=\"0 in Inventory\" style=\"display: none;\" href=\"/sell/release/2662813?seller=Anonymous\"><i class=\"icon icon-store\"></i><span class=\"count\">0</span>\n    </a>\n    </div>\n\n    \n    </div>"

Ahora, de toda esta maraña, extraemos únicamente el subdirectorio del artista. Para extraer exactamente este pedazo de texto utilizaremos la función str_extract de la librería stringr y una expresión regular para obtener uno o más caracteres alfanuméricos, guiones medios y diagonales que existan después del primer ‘href=\»‘. Una vez obtenido el subdirectorio lo pegamos al resto de la url utilizando paste0.

artist_link <- str_extract(enlace_artista_char, "(?<= href=\")[[:alnum:]-/]*") # expresión regular
artist_link <- paste0("https://www.discogs.com", artist_link) # construyendo petición
artist_link # imprimir
## [1] "https://www.discogs.com/artist/182658-Lassi-Nikko"

Resta entrar a esta página y raspar la información de colaboradores. Si entramos a la página del artista e inspeccionamos el bloque de código del nombre de los artistas en los discos, podemos ver que su ruta css es “span.artist_in_title a”

Obtenemos la lista de artistas en el título extrayendo estos nodos y extraemos el texto html de dichos nodos con html_text de rvest. Al final pasamos este texto por la función unique de R base para eliminar elementos repetidos

artist_html_page <- read_html(artis_link)
colaborators <- html_nodes(artist_html_page, "span.artist_in_title a")
# OUTPUT
(lista_colab <- unique(html_text(colaborators)))
##  [1] "MD"               "Seremoniamestari" "brothomStates"   
##  [4] "Brothomstates"    "Origami (9)"      "Blamstrain"      
##  [7] "Lackluster"       "Various"          "Luke Slater"     
## [10] "Surgeon"          "Sundial Aeon"     "Dune (31)"

Basta juntar los bloques de código para construir el script. Recuerda que debemos guardarlo con extensión .R (en la siguiente ejemplificación lo guardamos como ask_discogs.R) y para ejecutarlo basta escribir el siguiente comando dentro de la terminal, desde el directorio en donde hayamos guardado el script

Rscript "ask_discogs.R"

¡Ánimo!

El modelo logístico y COVID-19

La modelación de crecimiento de poblaciones es una de las tareas científicas más recurrentes (y en alguna forma también lo es dentro de la ciencia de datos). Cuando nos referimos a población nos estamos refiriendo, a personas pero también puede verse como dinero, en este caso personas enfermas, etcétera.

En esta entrada abordaremos la modelación del crecimiento de COVID-19, enfermedad respiratoria causada por el virus SARS-CoV-2. Para esto ocuparemos el modelo logístico, que es el más popular y común para modelar crecimientos poblacionales. Usaremos los datos del caso italiano pero puede ser aplicable para cualquier otro conjunto de datos con las respectivas consideraciones específicas de cada elección.

Puede que haya otros modelos mucho más refinados, sin embargo la intención de esta entrada es abordar el proceso de interpretación de un modelo así como su ajuste de parámetros. No ocuparemos el mexicano debido a que todavía nos encontramos en la fase exponencial. Los paquetes de Python que ocuparemos son los siguientes:

import pandas as pd
import numpy as np
from datetime import datetime,timedelta
from sklearn.metrics import mean_squared_error
from scipy.optimize import curve_fit
from scipy.optimize import fsolve
import matplotlib.pyplot as plt
import seaborn as sns
from pprint import pprint
%matplotlib inline

Obtención y preprocesamiento de datos.

Una de las partes más importantes de los procesos de modelación es la obtención de datos y la limpieza de los mismos. Pandas puede recuperar datos desde varias fuentes de datos, en este caso lo recuperaremos desde una url como se muestra a continuación.

url = "https://raw.githubusercontent.com/pcm-dpc/COVID-19/master/dati-andamento-nazionale/dpc-covid19-ita-andamento-nazionale.csv"
df = pd.read_csv(url)
df.iloc[:,[0,11]].tail()

Nos vamos a quedar con las columnas 0 (fecha)y 11 (casos_totales). La última línea nos muestra las últimas 5 líneas del dataset.

Selección_212

Ahora, si examinamos las fechas podemos ver que tienen un formato que no podemos usar así que usaremos las funciones python.datetime. Primero hacemos que nuestro dataframe sea la selección de columnas de nuestro interés (0 y 11). Después de eso pondremos las fechas en la variable date y con el formato definido "%Y-%m-%dT%H:%M:%S", que es el formato en el que vienen los dato, aplicamos la función fdt para generar toda una columna de fechas manipulables por python.

df = df.iloc[:umb,[0,11]]
formato = "%Y-%m-%dT%H:%M:%S"
date = df.data
fdt = lambda x : (datetime.strptime(x,formato) - datetime.strptime("2020-01-01T00:00:00", formato)).days
df['data'] = date.map( fdt )

Nuestro dataframe df ahora tiene dos columnas, una de fechas manipulables por Python y la otra de número de casos totales. Vamos a usar el paquete Seaborn para ver como se ven estos puntos en scatter plot.

sns.scatterplot(x=df['data'], y=df['totale_casi'])

La gráfica se ve así

Selección_213

Modelo Logístico

Hemos escuchado el término de «crecimiento exponencial» y hemos visto las gráficas que lo exhiben ya que han estado en redes sociales, medio de comunicación, vaya, en todos lados. Esto puede resultar angustiante porque efectivamente muestran el crecimiento que parece desmedido de este particular virus que tiene la característica de ser muy contagioso. Una de las características del crecimiento exponencial es que una proporción de la población, al infectarse, deja de ser susceptible y se convierte en infectada con la capacidad de transmitir el virus.

Sin embargo, este proceso no puede ser infinito, no puede crecer para siempre básicamente porque esa población susceptible va disminuyendo conforme pasa el tiempo y los que eran infectados se convierten en recuperados (o bien en decesos pero el modelo que ocuparemos hace la consideración de que la población es fija). En algún momento la capacidad del virus para infectar agentes susceptibles se detiene debido a que una buena parte de la población ya tiene memoria inmunológica que impide que el virus se propague con la misma facilidad que al inicio del proceso. La expresión que modela este comportamiento es la siguiente:

f_{a,b,c} (x) = \frac {c} {1 + \exp( -(x-b)/a)}

A esta función la llamamos modelo logístico y tiene tres parámetros. En donde 𝑥 representa la población cuyo crecimiento deseamos medir como crece, y los números 𝑎,𝑏,𝑐 son parámetros de nuestro modelo. 𝑎 mide la rapidez de la infección, 𝑏 es el día con la mayor cantidad de infecciones, 𝑐 es el número total de casos registrados al final de la infección. Decimos que c regula el final del proceso de infección ya que cuando el denominador se acerca a 𝑐 entonces 𝑓(𝑥) vale 1 que es el valor máximo que puede tomar.

Para encontrar los parámetros que mejor ajustan nuestros datos. Esto lo podemos hacer con la función scipy.optimize.curve_fit. Primero definiremos nuestra función y luego nuestras variables dependiente (x) y variable de respuesta (y).

def logistico(x, a,b,c):
    """
    Modelo logístico con argumento x (días) y con parámetros
    P=[a,b,c]
    Regresa la función f(x) = 1/(1+e(-(x-b)/a))
    """
    return c/(1 + np.exp(-(x-b)/a))

x = df.iloc[:,0]
y = df.iloc[:,1]

Para encontrar el mejor ajuste usaremos curve_fit pasándole x e y con un punto inicial para cada parámetro. Estos valores iniciales se pueden encontrar a través de otros métodos.

popt, pcov = curve_fit(logistico,x,y,p0=[2,100,20000])

En popt se guardan los valores encontrados mientras que en pcov se guarda la matriz de covarianza de los parámetros encontrados. Los parámetros encontrados son los siguientes

pprint(popt)
a, b, c = popt
array([5.22809110e+00, 8.13290036e+01, 1.20569392e+05])

La matriz de covarianza pcov nos muestra como variaron los parámetros entre sí, la diagonal es la varianza de cada parámetro. Vamos a tomar el la raíz cuadrada para calcular los errores estándar.

errores = [np.sqrt(pcov[i,i]) for i in [0,1,2]]
print(errores, sep='\t')
[0.07688318769831534, 0.22581878353538162, 2354.0038555475603]

Entonces la cantidad total de infectados se espera alrededor de c error_c

c_rango = list(map(np.round, [ popt[2] - errores[2] , popt[2] + errores[2]]))
c_rango
[118215.0, 122923.0]

El pico de la infección se espera el día b del año, es decir el día 81 del año, eso corresponde al 22 de Marzo.

pico = datetime.fromordinal(int(b))
print(b)
print(pico)
81.32900363914275
0001-03-22 00:00:00

Ahora nos gustaría saber cuándo terminará el proceso de infección y esto se puede determinar a partir cuando el acumulado de las personas infectadas iguale al parámetro 𝑐. Para esto requerimos usar la función fsolve para encontrar las raíces de 𝑓(𝑥) tomando como punto inicial el punto 𝑏

sol = int(fsolve(lambda x: logistico(x,a,b,c) - int(c),b))
sol
147

El día 147 corresponde al 27 de mayo

fin = datetime.fromordinal(sol)
fin
datetime.datetime(1, 5, 27, 0, 0)

Al momento de la publicación la cantidad de casos confirmados es de 147,577. Es importante notar que estos modelos son muy sensibles a las condiciones iniciales por lo que una variación pequeña puede resultar en cambios importantes en el resultado final.

Los datos graficados vs. la predicción del modelo lo podemos ver en la siguiente gráfica.

pred_x = list(range(max(x),sol))
plt.rcParams['figure.figsize'] = [7, 7]
preds = list(set(x).union(pred_x))
plt.rc('font', size=14)

plt.scatter(x,y,label="Real data",color="red")

plt.plot(preds, [logistico(i,popt[0],popt[1],popt[2]) for i in preds], label="Modelo logístico" )

Selección_214

En esta imagen la línea azul representa la predicción mientras que los puntos rojos representan los datos originales.

Conclusiones

Hay modelos más precisos para ajustar los datos de un proceso infeccioso. Particularmente hay modelos basados en ecuaciones diferenciales que pueden ser más precisos al tomar más consideraciones, de hecho parece que dadas las medidas de distanciamiento social la evolución de casos confirmados parece variar y se requieren modelos con parámetros distintos para diferentes etapas. Sin embargo en esta entrada lo que hicimos fue ajustar un modelo logístico, que es de los más comunes para modelar el crecimiento de una población, usando algunos parámetros que hay que determinar. En este particular caso la población es la cantidad de enfermos confirmados y los parámetros encontrados a través de resolución de ecuaciones no lineales.