Estructuración de datos
Baldomero Romero Ressendi, Autorretrato
El código completo lo encontraras aquí
Normalmente cuando nos encontramos trabajando con datos en pocas ocasiones se encuentran organizados o estructurados, y la mayor parte del tiempo radica en la estructuración de estos, por lo que a continuación veremos como:
- Solicitar datos de una APIweb, ya que es normal que muchos datos se entreguen de esta manera.
- Dar forma a datos en formato json para poder analizarlos, ya que muchas de las herramientas estadísticas funcionan con dataframes.
Hora de ensuciarse las manos!
Primero cargaremos las librerías necesarias
# En caso de no tener los paquetes necesarios debemos de instalarlos con los siguientes comandos
# install.packages(c("tidyverse","rjson","httr"))
library(tidyverse) #Estructuración de datos
library(rjson) #Lectura de formato json
library(httr) #Estatus de la conexión a la api
La api donde haremos la conexión es la de https://swapi.co/ la cual tiene una base de datos de los personajes, planetas, razas etc. Del universo de Star Wars.
Primero veremos de que manera están estructurados los datos, nos apoyaremos en la documentación. Necesitaremos esta información principalmente para saber cuantos elementos contiene cada base
json_file <- "https://swapi.co/api/people/?page=1&format=json"
json_data <- fromJSON(file=json_file)
names(json_data)
## [1] "count" "next" "previous" "results"
Después de consultar la api observamos cuantos personajes existen
json_data$count
## [1] 87
Como vemos son 87 por lo tanto consultaremos uno a uno la url correspondiente a cada personaje, y crearemos nuestro propio dataset, de esta manera se tendrá un mejor control en su elaboración
# Creamos una lista vacía para ir llenándola
data <- list()
count <- json_data$count
for (i in 1:count) {
#Creamos la url
url <- str_interp("https://swapi.co/api/people/${i}/?format=json")
#Vemos el estatus de la conexión
resp <- GET(url)$status_code
if(resp == 404) next #En caso de que dicha conexión nos regrese un estatus 404 pasaremos a la siguiente solicitud
# Guardamos en la lista
data[[i]] <- fromJSON(file=url)
if(i %% 5 == 0 | i == count ) cat(paste0(round(i/count*100),"% \n")) #Solo para ver el progreso
}
Veamos la primer lista que contiene los datos de Luke Skywalker
data[[1]]
## $name
## [1] "Luke Skywalker"
##
## $height
## [1] "172"
##
## $mass
## [1] "77"
##
## $hair_color
## [1] "blond"
##
## $skin_color
## [1] "fair"
##
## $eye_color
## [1] "blue"
##
## $birth_year
## [1] "19BBY"
##
## $gender
## [1] "male"
##
## $homeworld
## [1] "https://swapi.co/api/planets/1/"
##
## $films
## [1] "https://swapi.co/api/films/2/" "https://swapi.co/api/films/6/"
## [3] "https://swapi.co/api/films/3/" "https://swapi.co/api/films/1/"
## [5] "https://swapi.co/api/films/7/"
##
## $species
## [1] "https://swapi.co/api/species/1/"
##
## $vehicles
## [1] "https://swapi.co/api/vehicles/14/" "https://swapi.co/api/vehicles/30/"
##
## $starships
## [1] "https://swapi.co/api/starships/12/"
## [2] "https://swapi.co/api/starships/22/"
##
## $created
## [1] "2014-12-09T13:50:51.644000Z"
##
## $edited
## [1] "2014-12-20T21:17:56.891000Z"
##
## $url
## [1] "https://swapi.co/api/people/1/"
Muy bien todo salió como esperábamos, ahora crearemos una función para hacer la obtención de datos mas rápida, y poder elegir que categoría solicitaremos.
swapi <- function(resource) {
url_resource <- str_interp("https://swapi.co/api/${resource}/?page=1&format=json")
json_data <- fromJSON(file=url_resource)
data <- list()
count <- json_data$count
for (i in 1:count) {
#Creamos la url
url <- str_interp("https://swapi.co/api/${resource}/${i}/?format=json")
#Vemos el estatus de la conexión
resp <- GET(url)$status_code
if(resp == 404) next #En caso de que dicha conexión nos regrese un estatus 404 pasaremos a la siguiente solicitud
# Guardamos en la lista
data[[i]] <- fromJSON(file=url)
if(i %% 5 == 0 | i == count ) cat(paste0(round(i/count*100),"% \n"))
}
data
}
Las posibles categorías de búsqueda son:
- films
- people
- planets
- species
- starships
- vehicles
Comencemos con los personajes (people)
personajes <- swapi("people")
Ahora con las razas(species)
especie <- swapi("species")
Y por último los planetas(planets)
planeta <- swapi("planets")
Ya tenemos los datos que nos interesan sin embargo al provenir de un formato json o por así decirlo “Nosql”, tendremos problemas al convertir en una forma estructura por tablas, por lo tanto cambiaremos algunas cosas
# Pasamos de lista a dataframe
personajes_df <- do.call(rbind, personajes) %>% data.frame()
planeta_df <- do.call(rbind,planeta) %>% data.frame()
especie_df <- do.call(rbind,especie) %>% data.frame()
Si observamos el caso de Luke Skywalker veremos que en la columna de films se encuentran las url a cada una de las películas en las que aparece, sin embargo es mejor solo saber el numero de la película y no la url, de manera que sea mas legible.
personajes_df$films[[1]]
## [1] "https://swapi.co/api/films/2/" "https://swapi.co/api/films/6/"
## [3] "https://swapi.co/api/films/3/" "https://swapi.co/api/films/1/"
## [5] "https://swapi.co/api/films/7/"
Por lo tanto haremos uso de las expresiones regulares y al paquete stringr incluido en tidyverse
Crearemos una función que eliminara todos los caracteres excepto los números
extract <- function(variables) {
lapply(variables, function(x)(str_replace_all(x,"\\D","")) %>% paste0(collapse = ","))
}
Reescribiremos ciertas variables aplicando la función antes hecha, crearemos la variable ID la cual es el número correspondiente a cada elemento
personajes_di <- personajes_df %>% mutate(species = extract(species), films = extract(films),
homeworld = extract(homeworld),vehicles = extract(vehicles),
starships = extract(vehicles),id_personajes = extract(url))
especie_di <- especie_df %>% mutate(homeworld = extract(homeworld), people = extract(people),
films = extract(films),id_especie = extract(url) )
planeta_di <- planeta_df %>% mutate(residents = extract(residents),films = extract(films),
id_planeta = extract(url) )
Veamos ahora
personajes_di$films[[1]]
## [1] "2,6,3,1,7"
Muy bien ya está en un mejor formato, es importante mencionar que las películas están enumeradas de acuerdo al orden cronológico de estreno por lo tanto el número 1 hace referencia a “Episodio 4 - A New Hope” y no a “Episodio 1 - The Phantom Menace” (Obviamente esto no seria star wars sin el conflicto del orden de las películas)
Perfecto ahora solo queda convertir todo en un dataframe de vectores, ya que actualmente son dataframes de listas
personajes_ti <- personajes_di %>% map_dfr(unlist)
especie_ti <- especie_di %>% map_dfr(unlist)
planeta_ti <- planeta_di %>% map_dfr(unlist)
personajes_ti %>% head()
## # A tibble: 6 x 17
## name height mass hair_color skin_color eye_color birth_year gender
## <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr>
## 1 Luke~ 172 77 blond fair blue 19BBY male
## 2 C-3PO 167 75 n/a gold yellow 112BBY n/a
## 3 R2-D2 96 32 n/a white, bl~ red 33BBY n/a
## 4 Dart~ 202 136 none white yellow 41.9BBY male
## 5 Leia~ 150 49 brown light brown 19BBY female
## 6 Owen~ 178 120 brown, gr~ light blue 52BBY male
## # ... with 9 more variables: homeworld <chr>, films <chr>, species <chr>,
## # vehicles <chr>, starships <chr>, created <chr>, edited <chr>,
## # url <chr>, id_personajes <chr>
especie_ti %>% head()
## # A tibble: 6 x 16
## name classification designation average_height skin_colors hair_colors
## <chr> <chr> <chr> <chr> <chr> <chr>
## 1 Human mammal sentient 180 caucasian,~ blonde, br~
## 2 Droid artificial sentient n/a n/a n/a
## 3 Wook~ mammal sentient 210 gray black, bro~
## 4 Rodi~ sentient reptilian 170 green, blue n/a
## 5 Hutt gastropod sentient 300 green, bro~ n/a
## 6 Yoda~ mammal sentient 66 green, yel~ brown, whi~
## # ... with 10 more variables: eye_colors <chr>, average_lifespan <chr>,
## # homeworld <chr>, language <chr>, people <chr>, films <chr>,
## # created <chr>, edited <chr>, url <chr>, id_especie <chr>
planeta_ti %>% head()
## # A tibble: 6 x 15
## name rotation_period orbital_period diameter climate gravity terrain
## <chr> <chr> <chr> <chr> <chr> <chr> <chr>
## 1 Tato~ 23 304 10465 arid 1 stan~ desert
## 2 Alde~ 24 364 12500 temper~ 1 stan~ grassl~
## 3 Yavi~ 24 4818 10200 temper~ 1 stan~ jungle~
## 4 Hoth 23 549 7200 frozen 1.1 st~ tundra~
## 5 Dago~ 23 341 8900 murky N/A swamp,~
## 6 Besp~ 12 5110 118000 temper~ 1.5 (s~ gas gi~
## # ... with 8 more variables: surface_water <chr>, population <chr>,
## # residents <chr>, films <chr>, created <chr>, edited <chr>, url <chr>,
## # id_planeta <chr>
Ahora es momento de darle un formato homogéneo a los NAs ya que tienen la forma de “n/a” “unknown” o simplemente es un caracter vacío
personajes_ti[personajes_ti == "n/a"|personajes_ti =="unknown"|personajes_ti ==""] <- NA
especie_ti[especie_ti == "n/a"|especie_ti =="unknown"|especie_ti ==""] <- NA
planeta_ti[planeta_ti == "n/a"|planeta_ti =="unknown"|planeta_ti ==""] <- NA
Y ahora el paso final! convertir las variables que hasta este momento eran del tipo caracter a integer
planeta_ti[c(2,3,4,8,9,15)] <- lapply(planeta_ti[c(2,3,4,8,9,15)], as.integer)
## Warning in lapply(planeta_ti[c(2, 3, 4, 8, 9, 15)], as.integer): NAs
## introduced by coercion to integer range
especie_ti[c(4,8,16)] <- lapply(especie_ti[c(4,8,16)], as.integer)
## Warning in lapply(especie_ti[c(4, 8, 16)], as.integer): NAs introducidos
## por coerción
personajes_ti[c(2,3,17)] <- lapply(personajes_ti[c(2,3,17)], as.integer)
## Warning in lapply(personajes_ti[c(2, 3, 17)], as.integer): NAs introducidos
## por coerción
Y este es el resultado
personajes_ti %>% head() %>% knitr::kable()
name | height | mass | hair_color | skin_color | eye_color | birth_year | gender | homeworld | films | species | vehicles | starships | created | edited | url | id_personajes |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Luke Skywalker | 172 | 77 | blond | fair | blue | 19BBY | male | 1 | 2,6,3,1,7 | 1 | 14,30 | 1430 | 2014-12-09T13:50:51.644000Z | 2014-12-20T21:17:56.891000Z | https://swapi.co/api/people/1/ | 1 |
C-3PO | 167 | 75 | NA | gold | yellow | 112BBY | NA | 1 | 2,5,4,6,3,1 | 2 | NA | NA | 2014-12-10T15:10:51.357000Z | 2014-12-20T21:17:50.309000Z | https://swapi.co/api/people/2/ | 2 |
R2-D2 | 96 | 32 | NA | white, blue | red | 33BBY | NA | 8 | 2,5,4,6,3,1,7 | 2 | NA | NA | 2014-12-10T15:11:50.376000Z | 2014-12-20T21:17:50.311000Z | https://swapi.co/api/people/3/ | 3 |
Darth Vader | 202 | 136 | none | white | yellow | 41.9BBY | male | 1 | 2,6,3,1 | 1 | NA | NA | 2014-12-10T15:18:20.704000Z | 2014-12-20T21:17:50.313000Z | https://swapi.co/api/people/4/ | 4 |
Leia Organa | 150 | 49 | brown | light | brown | 19BBY | female | 2 | 2,6,3,1,7 | 1 | 30 | 30 | 2014-12-10T15:20:09.791000Z | 2014-12-20T21:17:50.315000Z | https://swapi.co/api/people/5/ | 5 |
Owen Lars | 178 | 120 | brown, grey | light | blue | 52BBY | male | 1 | 5,6,1 | 1 | NA | NA | 2014-12-10T15:52:14.024000Z | 2014-12-20T21:17:50.317000Z | https://swapi.co/api/people/6/ | 6 |
especie_ti %>% head() %>% knitr::kable()
name | classification | designation | average_height | skin_colors | hair_colors | eye_colors | average_lifespan | homeworld | language | people | films | created | edited | url | id_especie |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Human | mammal | sentient | 180 | caucasian, black, asian, hispanic | blonde, brown, black, red | brown, blue, green, hazel, grey, amber | 120 | 9 | Galactic Basic | 1,4,5,6,7,9,10,11,12,14,18,19,21,22,25,26,28,29,32,34,43,51,60,61,62,66,67,68,69,74,81,84,85,86,35 | 2,7,5,4,6,3,1 | 2014-12-10T13:52:11.567000Z | 2015-04-17T06:59:55.850671Z | https://swapi.co/api/species/1/ | 1 |
Droid | artificial | sentient | NA | NA | NA | NA | NA | NA | NA | 2,3,8,23,87 | 2,7,5,4,6,3,1 | 2014-12-10T15:16:16.259000Z | 2015-04-17T06:59:43.869528Z | https://swapi.co/api/species/2/ | 2 |
Wookiee | mammal | sentient | 210 | gray | black, brown | blue, green, yellow, brown, golden, red | 400 | 14 | Shyriiwook | 13,80 | 2,7,6,3,1 | 2014-12-10T16:44:31.486000Z | 2015-01-30T21:23:03.074598Z | https://swapi.co/api/species/3/ | 3 |
Rodian | sentient | reptilian | 170 | green, blue | NA | black | NA | 23 | Galactic Basic | 15 | 1 | 2014-12-10T17:05:26.471000Z | 2016-07-19T13:27:03.156498Z | https://swapi.co/api/species/4/ | 4 |
Hutt | gastropod | sentient | 300 | green, brown, tan | NA | yellow, red | 1000 | 24 | Huttese | 16 | 3,1 | 2014-12-10T17:12:50.410000Z | 2014-12-20T21:36:42.146000Z | https://swapi.co/api/species/5/ | 5 |
Yoda’s species | mammal | sentient | 66 | green, yellow | brown, white | brown, green, yellow | 900 | 28 | Galactic basic | 20 | 2,5,4,6,3 | 2014-12-15T12:27:22.877000Z | 2014-12-20T21:36:42.148000Z | https://swapi.co/api/species/6/ | 6 |
planeta_ti %>% head() %>% knitr::kable()
name | rotation_period | orbital_period | diameter | climate | gravity | terrain | surface_water | population | residents | films | created | edited | url | id_planeta |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Tatooine | 23 | 304 | 10465 | arid | 1 standard | desert | 1 | 2e+05 | 1,2,4,6,7,8,9,11,43,62 | 5,4,6,3,1 | 2014-12-09T13:50:49.641000Z | 2014-12-21T20:48:04.175778Z | https://swapi.co/api/planets/1/ | 1 |
Alderaan | 24 | 364 | 12500 | temperate | 1 standard | grasslands, mountains | 40 | 2e+09 | 5,68,81 | 6,1 | 2014-12-10T11:35:48.479000Z | 2014-12-20T20:58:18.420000Z | https://swapi.co/api/planets/2/ | 2 |
Yavin IV | 24 | 4818 | 10200 | temperate, tropical | 1 standard | jungle, rainforests | 8 | 1e+03 | NA | 1 | 2014-12-10T11:37:19.144000Z | 2014-12-20T20:58:18.421000Z | https://swapi.co/api/planets/3/ | 3 |
Hoth | 23 | 549 | 7200 | frozen | 1.1 standard | tundra, ice caves, mountain ranges | 100 | NA | NA | 2 | 2014-12-10T11:39:13.934000Z | 2014-12-20T20:58:18.423000Z | https://swapi.co/api/planets/4/ | 4 |
Dagobah | 23 | 341 | 8900 | murky | N/A | swamp, jungles | 8 | NA | NA | 2,6,3 | 2014-12-10T11:42:22.590000Z | 2014-12-20T20:58:18.425000Z | https://swapi.co/api/planets/5/ | 5 |
Bespin | 12 | 5110 | 118000 | temperate | 1.5 (surface), 1 standard (Cloud City) | gas giant | 0 | 6e+06 | 26 | 2 | 2014-12-10T11:43:55.240000Z | 2014-12-20T20:58:18.427000Z | https://swapi.co/api/planets/6/ | 6 |
Conclusiones
Sin duda alguna la ingeniería de datos (data engineering) dentro de la ciencia de datos es lo menos “glamuroso” sin embargo es el paso mas importante ya que sin datos que analizar, de nada sirve el ultimo algoritmo de deeplearning. Los datos en la vida real no se encuentran estructurados sin embargo aquí se mostraron formas para lograr esto, iniciando por una consulta de una API, modificando documentos Json hasta pasando por expresiones regulares y manejos de NAs incluso intentar explicar el conflicto del número en las películas de star wars.
Te invito a que por tu cuenta hagas la solicitud a films, starships, vehicles. De tal manera que veas lo fácil que es!
Fuentes:
https://www.tutorialspoint.com/r/r_json_files.htm https://github.com/Ironholds/rwars