El paquete httr

Antes de empezar con un ejemplo real, veamos la lógica de los dos paquetes que usaremos a lo largo de esta primera parte:

library(httr) ## Para interactuar con API
library(jsonlite) ## Para interactuar con JSON

El paquete httr nos permite interacciones con API y, entre otras functions, nos provee de GET para crear peticiones a una página. Por ejemplo:

r <- GET("http://httpbin.org/get")
r

Podemos inspeccionar la estructura del objeto que acabamos de importar:

str(r)

y podemos ver que incluye información acerca del header, el request que hemos hecho, el status code que ha sido devuelto y, por supuesto, el content que la página devuelve. El paquete ofrece formas de acceder a esta información:

status_code(r)
headers(r)
http_status(r)

Quizás la parte más interesante para nosotros ahora mismo sea el contenido de la petición:

str(content(r))

Podemos extraerla de muchas formas, pero la función content nos ofrece la posibilidad de retornarla como texto:

content(r, "text")

La cadena de texto que acabamos de recuperar está en formato JSON. Podemos convertirla manualmente a un objeto nativo de R

nr <- fromJSON(content(r, "text"))
nr
nr$headers

aunque el paquete httr permite devolver el contenido directamente en una lista

content(r, "parsed")

Recolección de datos de una REST API. Datos Abiertos de Colombia.

Muchos países están inmersos en un proceso de apertura de datos. Portales como data.gov (en Estados Unidos) o data.gov.uk (en Reino Unido) iniciaron una tendencia de compartir datos para que puedan ser usados por la comunidad de desarrolladores. Estos procesos tienen impacto en la capacidad de rendición de cuentas de los políticos al tiempo que facilitan la creación de productos y servicios que los gobiernos no tienen posibilidades de incubar. La mayor parte de los datos se comparten un portal centralizado que, además, normaliza el modo de distribución y el formato de los datos en un estándar común. La mayoría, además, facilita el acceso a los datos a través de una REST API similar a la que usaremos para recolectar datos de, por ejemplo, Twitter.

El portal de Datos Abiertos de Colombia ofrece acceso a una gran cantidad de datos de diferentes tipos y orígenes. Tenemos información general sobre el portal y una introducción a cómo usar los datos y conseguir la llave de acceso en el área de desarrolladores.

Como ejemplo de uso, examinaremos los datos de hurtos de celulares durante el año 2018. Información general sobre la base de datos y qué significa cada campo se puede encontrar en la página de documentación.

Empezaremos por fijar el endpoint que da acceso a los datos, que tienen el código ea3y-gmxk

URL <- "https://www.datos.gov.co/resource/ea3y-gmxk.json"

Tal y como se explica en la documentación, el acceso a los datos está regulado mediante un token que debe ser introducido en el header de la petición.

Una consideración de seguridad importante: nunca almacenéis o uséis esta token directamente en el código. En mi caso, he guardado el token en un archivo de texto plano que puedo leer con la función readLines

creds <- readLines("./credentials-datos-abiertos.txt")

r <- GET(URL, 
        add_headers("X-App-Token"=creds))

Antes de continuar veamos un poco sobre la respuesta. Los status codes de la web API se pueden consultar en la documentación

status_code(r)

Comprobemos también el tipo de información que es devuelta por la API. Es importante fijarse en los campos x-soda2-fields y x-soda-types, así como en el encoding de la respuesta.

headers(r)

Con esto podemos hacernos una idea de la información que está disponible.

Veamos ahora el contenido

datos <- content(r, "text")

Como veíamos antes, el contendo es devuelto en un texto en formato JSON que podemos transformar en una lista usando la función fromJSON del paquete jsonlite. La función además nos ofrece la posibilidad de simplificar en resultado a un data.frame en el caso de que sea posible:

jsonlist <- fromJSON(datos, simplifyDataFrame=FALSE)
jsondf <- fromJSON(datos, simplifyDataFrame=TRUE)

Comprobemos que los resultados son correctos, o al menos consistentes:

length(jsonlist)
dim(jsondf)

El número de casos está determinado por los parámetros por defecto de la búsqueda. Claramente es algo que debemos poder modificar. Para ello, podemos ir a la documentación y comprobar que lo que necesitamos es añadir a la dirección los parámetros $limit y $offset:

URL2 <- paste0(URL, "?$limit=100&$offset=100")
r <- GET(URL2,         
         add_headers("X-App-Token"=creds))

y ahora podemos comprobar la nueva longitud del resultado

length(content(r, "parse"))

Pasar parámetros a la URL es la forma manual de ejecutar búsquedas parametrizadas, pero eso nos limita mucho. Por fortuna, GET admite un argumento que nos permite añadir parámetros a la búsqueda de una forma más natural:

r <- GET(URL,
         query=list("$limit"=25),
         add_headers("X-App-Token"=creds))
length(content(r, "parse"))

Por supuesto, podemos pasar más parámetros a la búsqueda para ir a observaciones más concretas, como por ejemplo:

r <- GET(URL,
         query=list("d_a"="Lunes",
                    "municipio"="CARTAGENA (CT)",
                    "$limit"=1000),
         add_headers("X-App-Token"=creds))
head(content(r, "parse"))

Un ejercicio más práctico

Imaginemos que queremos recuperar todos los datos de la base de datos de hurtos de celulares. Para eso tenemos que pasar páginas en nuestro código y también guardar los resultados intermedios. Como a priori no sabemos cuántas páginas hay, tenemos que verificar en cada llamada que hemos recibido datos.

Para hacer nuestra vida más fácil, crearemos una función que nos permite crear la lista con limit y offset que se corresponde con cada página

pasar_pagina <- function(i, block=1000) {
    return(list("$limit"=block,
                "$offset"=block*(i - 1))) 
}
pasar_pagina(1)
pasar_pagina(3)

Ahora lo que haremos será escribir un while que cree la llamada correspondiente a esa página y que, si el resultado es correcto, acumule los resultados en una lista (res en este caso). Cuando encontremos que la llamada devuelve un objeto vacío, pararemos la ejecución.

status <- TRUE
i <- 1
res <- list()
while(status) {
    print(sprintf("Recogiendo datos de pagina %s", i))
    r <- GET(URL,
             query=pasar_pagina(i),
             add_headers("X-App-Token"=creds))
    status <- status_code(r) == 200
    if (status) {
        print("...Guardando datos")
        res[[i]] <- fromJSON(content(r, "text"))
    }
    if (i >= 10) { ## Para no eternizarnos
        status <- FALSE
    }
    Sys.sleep(5)
    i <- i + 1
}

Un par de cosas importantes a notar. En primer lugar, que hemos creado el objeto res con antelación sin especificar qué tamaño tiene. Solo hemos dicho que es una lista. A medida que vamos pidiendo páginas, hacemos crecer la lista al asignar nuevos elementos res[[i]] <-. En segundo lugar, imprimimos en pantalla información acerca de dónde está operando ahora nuestro while. Esto no servirá de ayuda para investigar si hubiese problemas. En tercer lugar, hemos puesto una llamada a Sys.sleep(5) entre cada iteración. Esto consigue que la ejecución se detenga durante 5 segundos. Es buena práctica y, de hecho, puede ser necesario para evitar los límites que el servidor puede haber establecido (veremos esto con más detalle en la próxima sección). Por último, hemos actualizado el valor de status dinámicamente durante el código para contrastar si realmente estamos ante la última página de datos. Cuando status pasa a ser FALSE, la ejecución se para.

Una vez finaliza la ejecución, tendremos una lista de bases de datos.

str(res)

Pero, para poder hacer análisis, queremos una base de datos única. Para eso tenemos varias estrategias. La que encuentro más sencilla es usar do.call para ir añadiendo recursivamente cada elemento de la lista a la anterior.

res <- do.call(plyr::rbind.fill, res) ## Algunas paginas tienen mas campos
head(res)

Ahora tenemos ya una base de datos que podemos usar para responder preguntas de investigación. Por ejemplo, podemos ver la distribución de hurtos por departamento:

table(res$departamento)

o, más interesante, el arma usada durante el hurto:

prop.table(table(res$arma_empleada, res$departamento), 2)