La mayoría del contenido visto en Introducción a R se ha enfocado al uso del lenguaje R y al uso de ciertas funciones sin considerar su rendimiento. Como bien se menciono en su momento, R no esta diseñado para ser rápido; lo que a través de los años ha sido un tema de interés a medida que la información aumenta y las necesidades por analizar y ejecutar procesos que traten con grandes cantidades de datos. Por tales razones, el uso de paquetes que estén diseñados para trabajar de manera eficiente ha sido fundamental en la mayoría de lenguajes de programación; tal es el caso del Tidyverse.
Como bien se especifica en su página oficial, “The tidyverse is an opinionated collection of R packages designed for data science. All packages share an underlying design philosophy, grammar, and data structures”. Estos paquetes están enfocados a tener un mejor flujo de escritura, a tener un mejor entendimiento de la estructura de los procesos, dar funciones que solucionen problemas comunes y, como ya se menciono, mejorar el rendimiento de las funciones.
Dentro de este conjunto de paquetes se encuentran funciones para leer distintos tipos de archivos como aquellos con extensión .csv y .xls, además aquellas para reconocer archivos dados por SPSS, Stata y SAS, manipular archivos JSON, XML, dar una interfaz para trabajar con APIs, hacer web scraping y tener comunicación con diferentes administradores de bases de datos como SQL, MariaDB, etc.
Se tienen paquetes especializados en la manipulación y limpieza de datos, también para crear modelos con estos, dar características especiales como catalogar y tratar a ciertas variables como fechas y factores, aplicar técnicas de expresiones regulares en dichos datos, aplicar funciones optimizadas que pueden remplazar a las de la familia apply y crear gráficas profesionales con una mayor fluidez.
Aquí se verán solo algunos de los paquetes que componen todo este “universo limpio” y se comenzará con un operador fundamental proporcionado por el paquete magritt: %>%, el cual se podrá utilizar casi siempre en R.
Dicho operador tiene por nombre pipe, el cual tiene un uso similar al dado en otros lenguajes de programación como Python: . y bash: |. Este tiene como objetivo encadenar procesos de tal forma que el resultado dado en la cadena sirve como input del siguiente eslabón en dicha cadena, lo cual ayuda a evitar el anidamiento de funciones, minimizar la cantidad de objetos locales y facilitar la lectura e implementación de una secuencia de operaciones.
Supongase que se desea resolver la tarea sencilla de aplicar una cantidad definida de operaciones sobre un número; por ejemplo, obtener la raíz cuadrada del logaritmo natural de un número multiplicado por el cuadrado de otro, a dicho valor sumarlo con los primeros 10 naturales y, finalmente, obtener la raíz cuadrada de dicho resultado. Esto se puede resolver de la siguiente manera; supongamos que los números son 10 y 20.
[1] 7.607887
En tal caso se tuvo que anidar las funciones para aplicar estas cada resultado obtenido. Con %>% la solución se ve de esta forma
library(magrittr)
10 %>% # 10
prod(20^2) %>% #se multiplica por 20^2
log() %>% #se obtiene el logaritmo de dicho número
sqrt() %>% #Se obtiene la raíz cuadrada
sum(1:10) %>% #A eso se le suman los primeros 10 naturales
sqrt() #Se obtiene la raíz cuadrada.[1] 7.607887
La anterior solución tiene una estructura más fiel a como se fue resolviendo el problema poco a poco sin tiene que escribir hacia la izquierda para aplicar resultados de funciones anidadas. Más adelante se verá que el uso de dicho operador resulta fundamental para ahorra tiempo al momento de escribir código.
Otro ejemplo:
matrix(1:100, ncol = 5, byrow = 20) %>% #Creación de una matriz con los primeros 100 naturales.
rowSums(10) %>% #Se suma por renglón añadiendo 10 unidades a dicho resultado.
as.matrix() %>% #Se convierte dicho resultado a matriz.
scale() %>% #Se normalizan los datos.
sum() #Se comprueba que estos sumen media cero.[1] 0
Como la mayoría de la información que se utilizará se tendrá que cargar con alguna librería, aquí se dejan algunos ejemplos de algunas funciones útiles en la lectura de información.
La función read_csv() es una de las más comunes para leer archivos separados por comas, en la cual se puede dar el path del archivo o la dirección URL de dichos datos. En este caso se esta utilizando unos datos donde se relaciona la información sobre exceso de velocidad de ciertos automóviles y señales de advertencia. Para más información consúltese el siguiente enlace.
En caso de que se desee leer un archivo por algún otro delimitador, se puede usar la función read_delim() y en el caso de tener archivos donde el delimitador sea “\t” usar la función read_tsv(). Los datos que se utilizan son proporcionados por el US Census Bureau, los cuales fueron indirectamente obtenidos del siguiente enlance.
El paquete readxl esta diseño para leer archivos excel. La siguiente información proporciona la población por sexo y entidad federativa según grupos de edad quinquenales de acuerdo a los tabulados básicos de la CONAPO.
Algo que hay que tener en cuenta al trabajar con los paquetes del tidyverse es el uso de tibbles en lugar de data frames.
[1] "tbl_df" "tbl" "data.frame"
Un tibble es una versión moderna de un data frame que trabaja de manera perezosa (es decir, que realiza menos operaciones) evitando problemas comunes y supuestos que un data frame puede llegar a asumir. Por ejemplo, los tibbles no coercionan automáticamente los caracteres a factores, no crean nombres para las observaciones y no cambia los nombres de columnas que sean nombres no sintácticos, por ejemplo
Se pueden utilizar variables desde la construcción del tibble
data.frame(x = 1:20, y = x-1)?Además se tienen diferencias respecto a la impresión entre un tibble y un data frame, un tibble permite agregar listas directamente sin tener que usar la función I() y los tibbles nunca hacen emparejamiento parcial.
NULL
int [1:20] 1 2 3 4 5 6 7 8 9 10 ...
Finalmente, los tibbles permiten hacer substracciones con el operador pipe.
[1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
[1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
tribble()?En un análisis de datos, gran parte del tiempo que se utiliza es dando un grado de limpieza a estos, para así ya solo obtener información resumen, aplicar modelos o manipularlos para descubrir algo en ellos. De acuerdo a las fuentes que se consulten, esto puede tomar hasta un 80% del tiempo que se dispone, por lo que es evidente la necesidad de tener funciones que ayuden con dicha tarea.
La limpieza de datos varía dependiendo de los fines, pero para Hadley Wickham (Chief Scientist en RStudio y alguien importante en desarrollo y mantenimiento del tidyverse) tener una limpieza en los datos requiere de ciertas características. La información completa se puede encontrar en artículo de Hadley, Tidy Data, pero básicamente se tienen 3 puntos importantes para considerar que los datos son limpios.
Las siguientes gráficas representan dichos puntos sobre un subconjunto de Iris.
Con lo anterior establecido y suponiendo que se esta analizando una pequeña base de datos correspondientes a las calificaciones de ciertos alumnos ¿Cuál de las siguientes estructuras es correcta?
| Matemáticas | Química | |
|---|---|---|
| Juan | 8 | 9 |
| Carlos | 9 | 7 |
| Luis | 7 | 8 |
| Allison | 9 | 9 |
| Leticia | 8 | 9 |
| Juan | Carlos | Luis | Allison | Leticia | |
|---|---|---|---|---|---|
| Matemáticas | 8 | 9 | 7 | 9 | 8 |
| Química | 9 | 7 | 8 | 9 | 9 |
De hecho, ninguna de las dos opciones anteriores es correcta. De acuerdo a los principios anteriores para tener datos limpios, cada variable debe formar una columna, lo cual no sucede en este caso, ya que las variables aquí son estudiante o el nombre, la materia y las calificaciones. Es decir, que las configuración correcta es la siguiente
| Nombre | Asignatura | Calificaciones |
|---|---|---|
| Juan | Matemáticas | 8 |
| Carlos | Matemáticas | 9 |
| Luis | Matemáticas | 7 |
| Allison | Matemáticas | 9 |
| Leticia | Matemáticas | 8 |
| Juan | Química | 9 |
| Carlos | Química | 7 |
| Luis | Química | 8 |
| Allison | Química | 9 |
| Leticia | Química | 9 |
Esto es uno de los tantos ejemplos que se pueden dar cuando se trabajando con datos. En el momento en que se identifican estos problemas, se deberá usar toda la creatividad para resolverlos y obtener una estructura con la que ya se pueda trabajar. El paquete tidyr ayudará con la mayoría de estos.
Como recomendación, se aconseja tener a la mano siempre la respectiva Cheat Sheet que pueda ser de utilidad, en este caso de la librería tidyr se pueden consultar Data import y Data Wrangling; las respectivas traducciones al español se pueden encontrar en la página oficial de Cheat Sheets de RStudio.
Como bien dice Hadley en Tidy Data, hay cinco problemas comunes en los messy datasets:
En la sección anterior se presentaron los datos para la población por sexo y entidad federativa según grupos de edad quinquenales de acuerdo a los tabulados básicos de la CONAPO
En las primeras dos columnas se cambiaron los nombres con las herramientas que hasta este momento se tienen. Véase que las demás columnas representan rangos de edad, por lo que se tiene el primer problema en los messy datasets. De hecho, en estos datos las variables son la Entidad Federativa, la Población Total, el Rango de Edad y el Porcentaje de la población total en ese rango de edad.
En estos casos se dice los datos son anchos, y lo que se desea es que estos estén en un formato largo (ancho por la cantidad de columnas y largo por la cantidad de observaciones). Para tales casos se utiliza la función tidyr::gather() la cual recibe como parámetros múltiples columnas y colapsa la información de estas en dos variables.
(Population_FEntity <- gather(data = Population_FEntity, key = "Rango_edad", value = "Porcentaje", -c("Entidad Federativa", "Población Total")))Con lo anterior se logro tener una mejor estructura en los datos, con lo cual fácilmente se podrían crear gráficas, modelos y estadísticas de resumen por Entidad Federativa o por rango de edad. Después de tener la información con una estructura adecuada, la imaginación es el único limitante. En el siguiente capítulo se verá como manipular internamente los datos para que se tenga una mejor semántica en ellos.
Si se desea por algún motivo, lo cual puede suceder, tener los datos en formato ancho, se puede usar la función complementaria la cual es tidyr::spread()
Finalmente:
tidyr:: gather() la proporciona el paquete reshape con su función melt().tidyr:: spread() se tiene la función reshape::dcast().gather() y spread() podrían haber sido remplazadas por las funciones tidyr::pivot_longer() y pivot_wider() respectivamente.Para ver un ejemplo del segundo punto, supóngase que los datos vistos al inicio donde se relacionaban las calificaciones de ciertos alumnos tienen originalmente la siguiente estructura
En tal caso, la segunda variable contiene la información tanto de la materia como de la calificación. Aquí la función tidyr::separate() es de gran ayuda
(Student_grades <- Student_grades %>% separate(col = "A/C", into = c("Asignatura", "Calificaciones"), sep = 1))sep se da la posición dentro del texto para separar los datos.En el caso en que se desee tener el caso inverso, se utiliza la función tidyr::unite()
Otro ejemplo lo proporciona la Cheat Sheet Data Wrangling.
tidyr::separate_rows()separate() esta dada por reshape::colsplit().En el caso que se tenga el problema donde algunas variables esten almacenadas tanto en renglones como columnas se debe tratar el problema como lo anterior visto, primero abstraerse al problema de juntar las columnas necesarias en dos variables con la función gather() para posteriormente separar las variables que lo requieran con la función separate().
Para los últimos dos casos se necesita hacer uso de unas funciones del paquete dplyr, el cual ser verá en la siguiente sección. Por el momento, se pueden ver otras funciones útiles de tidyr, como aquellas que ayudan en la obtención de diferentes operaciones de conjuntos en los datos.
La función tidyr::expand_grid() crea un tibble de todas las combinaciones de sus inputs, estos pueden ser data frames o tibbles, matrices y hasta vectores. Se puede entender a esta función como el producto cartesiano de dos conjuntos.
La función tidyr::crossing() regresa un tibble y tiene un comportamiento similar a expand_grid() con la diferencia de eliminar registros duplicados.
set.seed(20)
x <- data_frame(sample(letters[1:3], size = 5, replace = T))
y <- data_frame(y = sample(1:3, size = 5, replace = T))
tidyr::nesting(x, y)La función tidyr::nesting() encuentra las posibles combinaciones entre los datos, es decir, las combinaciones entre los datos de entrada si estos se unieran en uno solo.
set.seed(20)
reduced_iris <- iris %>% head()
df <- reduced_iris[sample(1:6, size = 10, replace = T),]
df %>% tidyr::expand(Petal.Length)La función tidyr::expand() genera todas las combinaciones de variables que se encuentran en un data set.
df %>% tidyr::expand(Sepal.Width, Petal.Width)?df %>% tidyr::expand(nesting(Sepal.Width, Petal.Width))?Otras funciones útiles son las siguientes
tidyr::nest(). Dicha función anida un data frame o un subconjunto de este en listas y coloca estos como observaciones; esto puede ser muy útil en la aplicación de modelos.tidyr::unnest(). Función inversa a tidyr::nest().tidyr::replace_na(). Dicha función encuentra los valores perdidos en un data set y los cambia por un valor dado.Así como es de importante la limpieza en un análisis de datos, lo es la manipulación de estos, ya que la información que contienen debe ser extraída y será poco probable que se vean a simple vista. Para dichas tareas el paquete dplyr, parte del tidyverse, es una de las mejores opciones.
dplyr tiene tres metas principales:
Para este paquete se puede obtener su Cheat Sheet oficial y es mencionada en otras como Data import y Data Wrangling. También se puede usar el comando vignette("dplyr", package="dplyr") para ver una guía con ejemplos de este paquete donde utilizan una base de datos de Star Wars obtenida directamente de su API.
Para este caso, se tomará las bases de datos que ya se utilizaron en la sección anterior para ver el funcionamiento de algunas de las funciones de dplyr.
Las funciones en dplyr son vectorizadas, por lo que podemos estar seguros que se esta trabajando de manera adecuada. Es común pensar en una función vectorizada como un remplazo eficiente de un ciclo con una operación dada; para este caso, filtrar un conjunto de datos puede ser tedioso mediante ciclos anidados y es lo que se puede remplazar con la función dplyr::filter().
En la base de datos donde se reúne la información correspondiente a la población de México por rangos de edad quinquenales, es evidente que se puede obtener la población total de todo el territorio con la suma de las cantidades dadas por cada estado, así que este registro será eliminado.
Véase que dplyr::filter() utilizó la variable Entidad Federativa y elimino los registros que no cumplieran con la condición dada: !="República Mexicana", al igual que se hubiera hecho con un condicional if() en ciclos anidados. En esta función se pueden aplicar los operados lógicos conocidos ==, <=, %in%, etc. Además de los operados de conjuntos como |, & y xor(). En el siguiente ejemplo solo se toman aquellos estados donde el rango de edad sea 25 a 29 años y tengan un porcentaje de su población total en dicha edad entre 7.5 y 8.5.
Como el resultado es un tibble, bien podría utilizarse las veces que se desee la función utilizando %>% a los resultados previos. En este caso no se tienen valores perdidos pero es bueno recordar que la función is.na() regresa un vector booleano indicando si en el input dado se tienen valores perdidos, por lo que podría usarse con la función filter() para detectar dichos valores de una manera rápida.
Además de obtener un subconjunto de los datos con filter(), dplyr ofrece otras funciones útiles para obtener un subconjunto específico de datos.
set.seed(20)
data_easy <- tibble(x = sample(letters, 50, replace = TRUE), y = sample(1:25, 50, replace = TRUE))
data_easy %>% distinct(y)La función anterior sólo trabaja con observaciones, por lo que también debe existir un análogo para las variables, dicha función es dplyr::select(), la cual permite un conjunto de funciones útiles para hacer selecciones específicas de variables.
Lo anterior es equivalente a la siguiente instrucción:
Utilizar el operando ! se indica que dicha variable no se desea observar, dicha instrucción se pudo haber sustituido con Population_FEntity %>% select(-'Población Total'). También se puede usar un rango de columnas con :.
La función select tiene algunas funciones auxiliares para trabajar con texto como el uso de expresiones regulares. Algunas de estas funciones son contains(), ends_with() y start_with(). Para el uso de este tipo de filtros en observaciones, véase el paquete stringr.
Finalmente, para obtener todas las columnas se puede usar la función dplyr::everything(), para obtener variables donde sus nombres estén en un grupo dplyr::one_of y para seleccionar las variables por rango dplyr::num_range().
Para ordenar una variable de forma ascendente o descendente se utiliza la función dplyr::arrange(). Al ordenar una variable, se ordenan todos los datos de acuerdo a esa variable de referencia.
Además, se pueden ordenar los registros por múltiples variables. En este ejemplo se ordenan los datos de la población mexicana por la Entidad Federativa y, de manera descendente, el porcentaje de la población total por grupos quinquenales de edad.
La función arrange() modificó de una forma sutil los datos, sólo el orden; pero en el caso de que se desee modificar o agregar una nueva variable, ya sea en función de los valores de otra o nuevos valores independientes, se utiliza la función dplyr::mutate().
En esta función se pueden aplicar funciones ya pre-establecidas, además de window functions (véase la Cheat Sheet Data Wrangling) que se pueden obtener de R o de dplyr como dplyr::lag() y dplyr::between(). Véase los siguientes ejemplos:
Population_FEntity %>%
mutate(Porcentaje_entidad = `Población Total`/103498524) %>%
select(c(`Entidad Federativa`, `Población Total`, Porcentaje_entidad))library(scales)
Population_FEntity %>%
mutate(Porcentaje_entidad = `Población Total`/103498524,
Porcentaje = Porcentaje/100) %>%
mutate(Porcentaje_quinquenal_P = percent(Porcentaje*Porcentaje_entidad, accuracy = 0.01)) %>% select(`Entidad Federativa`, Rango_edad, Porcentaje_quinquenal_P)Population_FEntity %>%
mutate(Porcentaje = Porcentaje/100) %>%
mutate("Población" = Porcentaje * `Población Total`)En este último ejemplo se modifico la base de datos de la población donde puede notarse que la anterior función no elimina las variables con las que se opera, esto podría hacerse con la función select(), o bien con la función dplyr::transmute(), aunque elimina también aquellas columnas que no se utilizan.
Population_FEntity %>%
mutate(Porcentaje = Porcentaje/100) %>%
transmute("Población" = Porcentaje * `Población Total`)Así que se hará lo siguiente
(Population_FEntity <- Population_FEntity %>%
mutate(Porcentaje = Porcentaje/100) %>%
mutate("Población" = Porcentaje * `Población Total`) %>% select(-Porcentaje))Un funcionalidad muy importante es el uso de datos agrupados. Dichos grupos serán subconjuntos de datos que compartan una característica en común. Esto tiene muchas ventajas ya que se pueden obtener funciones resumen con una mayor fluidez y aplicaciones de funciones por bloques en lugar de observaciones individuales. La función para otorgar esta propiedad es dplyr::group_by() en donde sus parámetros indican que variables servirán para crear los grupos de acuerdo a los diferentes valores de esas variables.
Por ejemplo, véase que es natural pensar que en los datos de la población ya vistos se tomen resúmenes por la variable Rango_edad.
A simple vista no se ve algún cambio, pero internamente ya se tiene más estructura a dicha base de datos, ahora existe un atributo llamado “groups”.
tibble [448 × 4] (S3: grouped_df/tbl_df/tbl/data.frame)
$ Entidad Federativa: chr [1:448] "Aguascalientes" "Baja California" "Baja California Sur" "Campeche" ...
$ Población Total : num [1:448] 1066233 2856361 512030 755703 2501413 ...
$ Rango_edad : chr [1:448] "0 a 4" "0 a 4" "0 a 4" "0 a 4" ...
$ Población : num [1:448] 109853 268842 46070 63034 244624 ...
- attr(*, "groups")= tibble [14 × 2] (S3: tbl_df/tbl/data.frame)
..$ Rango_edad: chr [1:14] "0 a 4" "10 a 14" "15 a 19" "20 a 24" ...
..$ .rows : list<int> [1:14]
.. ..$ : int [1:32] 1 2 3 4 5 6 7 8 9 10 ...
.. ..$ : int [1:32] 65 66 67 68 69 70 71 72 73 74 ...
.. ..$ : int [1:32] 97 98 99 100 101 102 103 104 105 106 ...
.. ..$ : int [1:32] 129 130 131 132 133 134 135 136 137 138 ...
.. ..$ : int [1:32] 161 162 163 164 165 166 167 168 169 170 ...
.. ..$ : int [1:32] 193 194 195 196 197 198 199 200 201 202 ...
.. ..$ : int [1:32] 225 226 227 228 229 230 231 232 233 234 ...
.. ..$ : int [1:32] 257 258 259 260 261 262 263 264 265 266 ...
.. ..$ : int [1:32] 289 290 291 292 293 294 295 296 297 298 ...
.. ..$ : int [1:32] 33 34 35 36 37 38 39 40 41 42 ...
.. ..$ : int [1:32] 321 322 323 324 325 326 327 328 329 330 ...
.. ..$ : int [1:32] 353 354 355 356 357 358 359 360 361 362 ...
.. ..$ : int [1:32] 385 386 387 388 389 390 391 392 393 394 ...
.. ..$ : int [1:32] 417 418 419 420 421 422 423 424 425 426 ...
.. ..@ ptype: int(0)
..- attr(*, ".drop")= logi TRUE
Entonces, al aplicar funciones con mutate, estas se harán por grupos.
Population_FEntity %>%
group_by(Rango_edad) %>%
mutate("Población Mínima" = min(Población), "Población Máxima" = max(Población))Igual se puede agrupar por distintas variables
Population_FEntity %>%
mutate(division = rep(c(rep(1,16),rep(2,16)), 14)) %>%
group_by(Rango_edad, division) %>%
mutate(index = row_number()) %>%
select(`Entidad Federativa`, Rango_edad, division, index)Algo interesante que se puede hacer con datos agrupados es el uso de funciones para resumir información, tales como dplyr::first(), dplyr::min(), dplyr::n(), dplyr::n_distinct() y dplyr::mean(); estas funciones también se pueden usar sin datos agrupados. Véase los siguientes ejemplos
Otras funciones que ofrece dplyr para resumir información son dplyr::summarise_each() y dplyr::count(). La primera aplica la función resumen a cada una de las columnas y la segunda cuenta el número de valores únicos con o sin un peso establecido.
Para eliminar esta propiedad a los datos se utiliza la función dplyr::ungroup() y así la manipulación de datos continuará sin eliminar los resultados ya obtenidos.
Antes de pasar a las funciones de dplyr especializadas en las operaciones de conjuntos con bases de datos,se verá la función dplyr::rename() la cual, como indica su nombre, da un nuevo nombre a una variable existente.
Lo cual se pudo haber hecho con mutate() y select()
Anteriormente ya se había mencionado la unión entre matrices, por renglón o columna y de la función base::merge(), las cuales tienen su optimización por parte de dplyr.
Por parte de las funciones que unen registros o variables, se tienen las funciones dplyr::bind_rows() y bind_cols(). Las mismas reglas que cbind() y rbind() se deben respetar.
set.seed(20)
x <- tibble(One_Two = sample(1:2, size = 5, replace = T),
Three_Four= sample(3:4, size = 5, replace = T),
Five_Six = sample(5:6, size = 5, replace = T))
y <- tibble(One_Two = rep(2, 3), Three_Four = rep(4, 3), Five_Six = rep(5, 3))
bind_rows(x,y)Respecto a las operaciones de conjuntos se tienen las funciones dplyr::interset(), dplyr::union y dplyr::setdiff().
Las operaciones tipo Join son muy importantes en la extracción de información en bases de datos, dplyr por su cuenta tiene varias funciones para trabajar.
dplyr::left_join(a, b, by = x): Une las bases de datos de acuerdo a la variable x tomando la preferencia sobre a.dplyr::rigth_join(a, b, by = x): Une las bases de datos de acuerdo a la variable x tomando la preferencia sobre b.dplyr::inner_join(a, b, by = x): Une las bases de datos de acuerdo a la variable x considerando a y b.dplyr::full_join(a, b, by = x): Une las bases de datos de acuerdo a la variable x sin ninguna preferencia, por lo que contiene todos los datos posibles.Para ver más claro esto, véase los siguientes ejemplos.
set.seed(20)
a <- tibble(L = sample(LETTERS[1:5], size = 5, replace = T),
N = sample(1:100, size = 5, replace = T))
b <- tibble(L = sample(LETTERS[1:5], size = 5, replace = T),
Lo = sample(c(T, F), size = 5, replace = T))
aY un ejemplo práctico: Tomando la base de datos de la población mexicana, se puede obtener los datos para los hombres y mujeres y modificarlos para que tengan un estructura similar a Population_FEntity.
normalization <- function(data){
colnames(data) <- c("Entidad Federativa", "Población Total", unique(Population_FEntity$Rango_edad))
data <- data %>%
gather(key = "Rango_edad",
value = "Porcentaje", -c("Entidad Federativa", "Población Total")) %>%
mutate(Porcentaje = Porcentaje/100) %>%
mutate("Población" = Porcentaje * `Población Total`) %>%
select(-Porcentaje, -`Población Total`)
}Population_FEntity_M <- readxl::read_xls("Data/Population_Sex_FEntity.xls", range = "B43:Q74", col_names = F)
(Population_FEntity_M <- normalization(Population_FEntity_M))Population_FEntity_F <- readxl::read_xls("Data/Population_Sex_FEntity.xls", range = "B79:Q110", col_names = F)
(Population_FEntity_F <- normalization(Population_FEntity_F))Así es fácil crear un sólo conjunto de datos donde se reúna toda la información relevante
Population_FEntity <- Population_FEntity %>% select(-`Población Total`)
Population_FEntityWS <- left_join(Population_FEntity, Population_FEntity_M, by = c("Entidad Federativa", "Rango_edad"))
(Population_FEntityWS <- left_join(Population_FEntityWS, Population_FEntity_F, by = c("Entidad Federativa", "Rango_edad")) %>%
rename( "Población Total" = "Población.x", "PoblaciónM" = "Población.y", "PoblaciónF" = "Población"))Population_FEntityWS %>%
mutate(PPredominante = if_else(PoblaciónM>=PoblaciónF, "M", "F")) %>%
select(`Entidad Federativa`, Rango_edad, PPredominante)Population_FEntityWS %>%
mutate(SPredominante = if_else(PoblaciónM>=PoblaciónF, "M", "F")) %>%
mutate(S = if_else(SPredominante == "M", PoblaciónM, PoblaciónF)) %>%
group_by(Rango_edad) %>%
top_n(1, S) %>% select(`Entidad Federativa`, Rango_edad, SPredominante)Population_by_sex_Mexico <- Population_FEntityWS %>% group_by(`Entidad Federativa`) %>%
summarise(SMasculino = sum(PoblaciónM),
SFemenino = sum(PoblaciónF))
full_join(
Population_by_sex_Mexico %>% arrange(desc(SMasculino)) %>%
head(1) %>% select(-SFemenino),
Population_by_sex_Mexico %>% arrange(desc(SFemenino)) %>%
head(1) %>% select(-SMasculino),
by = "Entidad Federativa")¿Cuál es el rango de edad con mayor población en mujeres y hombres?
Population_by_sex_age_Mexico <- Population_FEntityWS %>%
group_by(Rango_edad) %>%
summarise(PoblaciónF = sum(PoblaciónF), PoblaciónM = sum(PoblaciónM))
full_join(
Population_by_sex_age_Mexico %>% arrange(desc(PoblaciónM)) %>%
head(1) %>% select(-PoblaciónF),
Population_by_sex_age_Mexico %>% arrange(desc(PoblaciónF)) %>%
head(1) %>% select(-PoblaciónM),
by = "Rango_edad")Finalmente, algunos puntos extra
En el ejemplo que se ha tomado de la población mexicana tiene el primer defecto, ya que la población total bien podría quedar en una sola tabla junto con el nombre de la entidad y en otra los porcentajes de acuerdo a los rangos de edad. En general en este punto se debe tener en cuenta que cada unidad observacional debe tener su propia tabla, así logrando un técnica en bases de datos llamada normalización. Para este caso, se tendrían dos tablas
Population_FEntity <- readxl::read_xls("Data/Population_Sex_FEntity.xls", range = "B5:Q38", col_names = TRUE)
colnames(Population_FEntity)[1:2] <- c("Entidad Federativa", "Población Total")
Population_FEntity <- Population_FEntity %>% gather(key = "Rango_edad",
value = "Porcentaje", -c("Entidad Federativa", "Población Total")) %>%
mutate(Porcentaje = Porcentaje/100) %>%
mutate("Población" = Porcentaje * `Población Total`)
(t1 <- Population_FEntity %>% select(`Entidad Federativa`, `Población Total`))Para el último punto se entiende que los datos están dispersos entre distintos conjuntos de datos, por lo que usar las funciones para combinar bases de datos ayudan en la resolución de este problema.
Es recomendable estudiar el paquete purrr, el cual es parte del tidyverse y ofrece una versión ideal para trabajar con funciones anónimas en data frames, ya que al tratar de aplicar alguna función de la familia apply con un data frame estos son convertidos primero a una matriz lo cual hace perder muchos de sus atributos. Así, con las funciones de la “familia” purrr::map se pueden hacer todas las aplicaciones que se pueden hacer con alguna de la familia apply e incluso algunas más. Además de que tienen una aplicación ideal con dplyr. Véase Functional Programming.
Se pueden usar las librerías dtplyr, dbplyr o sparklyr como interfaz para usar la sintaxis de dplyr en data.tables, bases de datos en SQL o en Apache Spark.
Recordar que la función dplyr::near() compara dos elementos numéricos en punto flotante.
La función dplyr::case_when() es una versión equivalente a dplyr::if_else() pero más general
tibble(x = 1:5) %>% mutate(new_variable = dplyr::case_when(x == 1 ~ "Hola", x == 5 ~ "Adios", TRUE ~ as.character(x)))dplyr proviene de la paqueteria plyr la cual contiene muchas más funciones que pueden ser de utilidad en ciertas ocasiones.Finalmente, uno de los paquetes más famosos del tidyverse es ggplot, el cual esta diseñada para representar la información obtenida de los datos mediante gráficas con un excelente control de todos los elementos gráficos tratados como objetos. Dicha paquetería está basada en el libro The Grammar of Graphics de Leland Wilkinson.
Como siempre, una de las mejores fuentes rápidas para recordar alguna función o característica de ellas, es la Cheat Sheet oficial, aunque hay una enorme cantidad de ejemplos y tutoriales para esta paquetería. A criterio personal recomendaría los siguientes enlaces para estudiar más a fondo todo lo relacionado con este gran mundo que aún sigue en crecimiento.
Independientemente de la gráfica que se esté realizando, la idea básica en ggplot para construir una gráfica es mediante el uso de capas. Algunos que tengan experiencia en programas diseñados para la creación de imágenes entenderán que el uso de capas otorga un mayor control sobre los elementos gráficos del resultado final. Mediante cada capa se agregan elementos gráficos que proporcionan algún tipo de atributo extra a toda la imagen. Se comenzará siempre con la capa donde se adjuntarán las capas restantes.
Para este ejemplo se tomará la base de datos que ya se ha estado utilizando en las anteriores secciones con el objetivo de crear una gráfica donde se represente el comportamiento de la población total a medida que avanza el rango de edad.
Population_total_range <- Population_FEntityWS %>%
mutate(Rango_edad = factor(Rango_edad, levels = unique(Rango_edad))) %>%
group_by(Rango_edad) %>% summarise(Y = sum(`Población Total`))
ggplot(Population_total_range)Esta primera capa se puede pensar como la hoja o la superficie donde se comenzaría a realizar un dibujo.
En la bibliografía que se menciono al inicio se puede encontrar más información sobre el comportamiento interno de ggplot, por ahora es bueno saber que la función ggplot y sus subsecuentes toman los datos y aplican transformaciones para traducir dicho contenido en información que pueda ser utilizada para la determinación de colores, geometrías, escalas y hasta espacio geométrico. Algo que hay que tomar en cuenta al hacer una gráfica con ggplot es el uso del operador +, el cual tendrá una función similar a %>%. Con este se añadirá una capa más a toda la gráfica con la que ya se estaba trabajando.
En esta segunda capa se estableció mediante la función aes() aquellas variables que servirán como ejes para mapear la información. Dicha función se puede modificar en cada capa y puede ser un parámetro en la mayoría de funciones de ggplot. Ahora, sólo falta agregar los elementos gráficos para representar las observaciones dadas, es decir la geometría.
¡Así de fácil se obtiene ya la gráfica! Faltan aplicar algunos detalles como el diseño de esta, lo cual sería añadiendo más capas. Véase que en las siguientes instrucciones la gráfica se guardo como un objeto el cual puede ser usado más adelante.
graph_population <- Population_total_range %>% ggplot(aes(x=Rango_edad, y=Y)) + geom_point()
(graph_population_per <- graph_population +
theme(axis.line = element_line(linetype = "solid"),
axis.ticks = element_line(colour = "gray0"),
panel.grid.major = element_line(colour = "gray25"),
panel.grid.minor = element_line(colour = "gray25"),
axis.title = element_text(colour = "gray93"),
axis.text = element_text(colour = "gray82"),
plot.title = element_text(colour = "gray90"),
panel.background = element_rect(fill = "gray33", size = 1.5, linetype = "dashed"),
plot.background = element_rect(fill = "gray21", colour = "gray0",
size = 1.6, linetype = "solid")))Cada uno de los comandos anteriores son dados para cambiar solo el diseño, bien pudo hacerse todo en capas individuales con la función theme() o, como en este caso, englobar todo el diseño en una capa. Existen muchos elementos que se pueden cambiar y lo más recomendable es ver la documentación oficial de la función theme desde la página Modify components of a theme, la cual contiene ejemplos muy útiles.
Dando más diseño con más capas se puede mejorar mucho la presentación de esta, ya que aún la propia gráfica en si misma no proporciona mucha información para alguien que no tenga conocimiento de la información con la que se esta trabajando. Es decir que falta agregar títulos, nombres informativos en los ejes, una mejor distribución de las etiquetas en los ejes y algunos atributos extra que ayuda en la interpretación y obtención visual más rápida de la información.
(graph_population_per <- graph_population_per +
geom_point(color = "white") + #Esta instrucción pudo colocarse la primera vez de su uso
labs(x = "Rango de edad", y = "Población total",
caption = "Datos obtenidos de los tabulados básicos de la CONAPO (2020)")+
ggtitle("Población total de México por rangos de edad quinquenales en el 2020")+
theme(axis.text.x = element_text(angle = 25))+
scale_y_continuous(labels = scales::comma) +
theme(text = element_text(size=12, family = "Leelawadee UI Semilight"),
axis.title = element_text(size = 9.6),
axis.text = element_text(size = 9),
plot.title = element_text(hjust = 0.5),
plot.caption = element_text(colour = "gray99", size = 7)))En la siguiente página: Ggplot title, Subtitle and caption, se puede ver un poco sobre la manipulación de títulos, subtitulos y captions.
Tal vez sea tedioso estar modificando manualmente cada aspecto estético de la gráfica, por lo que se han dado algunos temas predefinidos para hacer de dicha tarea algo sencillo, estos se pueden obtener con el prefijo theme_; aquí se dejan un par de ejemplos.
library(patchwork)
(graph_population+
labs(x = "Rango de edad", y = "Población total",
caption = "Datos obtenidos de los tabulados básicos de la CONAPO (2020)")+
ggtitle("Población total de México por rangos de edad quinquenales en el 2020")+
scale_y_continuous(labels = scales::comma) + theme_minimal())+(graph_population+
labs(x = "Rango de edad", y = "Población total",
caption = "Datos obtenidos de los tabulados básicos de la CONAPO (2020)")+
ggtitle("Población total de México por rangos de edad quinquenales en el 2020")+
scale_y_continuous(labels = scales::comma) + theme_classic())Lo anterior solo fue un ejemplo de como usar el paquete patchwork, el cual se menciona más adelante en una lista de paquetes útiles, por lo que no se toma en cuenta la mal distribución que se consiguió en los ejes de las gráficas anteriores.
Las siguientes gráficas muestran un poco más de todas las geometrías que incluye ggplot, sin importar por el momento el diseño de dichas gráficas.
(Population_total_range %>% ggplot(aes(x = Rango_edad, y = Y))+geom_bar(stat = "identity"))+
(Population_by_sex_age_Mexico %>% ggplot(aes(x = PoblaciónF))+geom_boxplot())+
(Population_by_sex_age_Mexico %>% ggplot(aes(x = PoblaciónM))+geom_density())Además de poder asignar las variables a los ejes, también se pueden usar las variables como atributos estéticos para las gráficas. Véase en el siguiente ejemplo el uso de la variable Entidad Federativa para agregar color a las barras en el siguiente gráfico de barras.
#Por rango de edad ¿Qué sexo predomina y en que estado se encuentra dicho resultado?
Population_FEntityWS %>%
mutate(SPredominante = if_else(PoblaciónM>=PoblaciónF, "M", "F")) %>%
mutate(S = if_else(SPredominante == "M", PoblaciónM, PoblaciónF)) %>%
group_by(Rango_edad) %>%
top_n(1, S) %>%
ggplot(aes(x = Rango_edad, y = `Población Total`, fill = `Entidad Federativa`))+
geom_bar(stat = "identity")+
labs(x = "Rango de edad", y = "Población total")+
ggtitle("Población total por rangos de edad quinquenales del sexo dominante en México 2020")+
theme(axis.text.x = element_text(angle = 25))+
scale_y_continuous(labels = scales::comma) +
theme(text = element_text(size=12, family = "Leelawadee UI Semilight"),
axis.title = element_text(size = 9.6),
axis.text = element_text(size = 9),
plot.title = element_text(hjust = 0.5),
plot.caption = element_text(colour = "gray99", size = 7))Bien se podría hacer más patriótica la gráfica anterior.
Population_FEntityWS %>%
mutate(SPredominante = if_else(PoblaciónM>=PoblaciónF, "M", "F")) %>%
mutate(S = if_else(SPredominante == "M", PoblaciónM, PoblaciónF)) %>%
group_by(Rango_edad) %>%
top_n(1, S) %>%
ggplot(aes(x = Rango_edad, y = `Población Total`, fill = `Entidad Federativa`, color = SPredominante))+
geom_bar(stat = "identity")+
labs(x = "Rango de edad", y = "Población total", fill = "Entidad Federativa: ", color = "Sexo Predominante: ")+
ggtitle("Población total por rangos de edad quinquenales del sexo dominante en México 2020")+
theme(axis.text.x = element_text(angle = 25))+
scale_y_continuous(labels = scales::comma) +
theme(text = element_text(size=12, family = "Leelawadee UI Semilight"),
axis.title = element_text(size = 9.6),
axis.text = element_text(size = 9),
plot.title = element_text(hjust = 0.5),
plot.caption = element_text(colour = "gray99", size = 7),
legend.position = "top",
legend.direction = "horizontal",
panel.grid.major = element_line(linetype = "blank"),
panel.background = element_rect(fill = "gray93"))+
scale_fill_manual(values = c("#CE0626","#2C6846"))+
scale_color_manual(values = c("dimgrey", "black"))Además se pueden usar múltiples geometrías en una gráfica. La gráfica siguiente es un gráfico de dispersión de la longitud de los sépalos contra el ancho de los sépalos en la base de datos iris agregando un poco de ruido (se agrega un poco de variación aleatoria a las ubicaciones) junto con un ajuste polinómico a los datos originales.
Hay muchas más otras funciones y geometrías que se podrían explorar y que haría de este sección muy larga. Para resumir un poco, se deja una lista de paquetes y funciones que pueden ser de utilidad a la hora de graficar y que han ampliado de gran manera todo el universo de ggplot para hacerlo una de las herramientas principales para crear contenido visual a partir de una análisis de datos.
library(plotly)
graph_example_interactive <- Population_FEntityWS %>%
mutate(SPredominante = if_else(PoblaciónM>=PoblaciónF, "M", "F")) %>%
mutate(S = if_else(SPredominante == "M", PoblaciónM, PoblaciónF)) %>%
group_by(Rango_edad) %>%
top_n(1, S) %>%
ggplot(aes(x = Rango_edad, y = `Población Total`, fill = `Entidad Federativa`, color = SPredominante))+
geom_bar(stat = "identity")+
labs(x = "Rango de edad", y = "Población total", fill = "Entidad Federativa: ", color = "Sexo Predominante: ")+
ggtitle("Población total por rangos de edad quinquenales del sexo dominante en México 2020")+
theme(axis.text.x = element_text(angle = 25))+
scale_y_continuous(labels = scales::comma) +
theme(text = element_text(size=12, family = "Leelawadee UI Semilight"),
axis.title = element_text(size = 9.6),
axis.text = element_text(size = 9),
plot.title = element_text(hjust = 0.5),
plot.caption = element_text(colour = "gray99", size = 7),
legend.position = "top",
legend.direction = "horizontal",
panel.grid.major = element_line(linetype = "blank"),
panel.background = element_rect(fill = "gray93"))+
scale_fill_manual(values = c("#CE0626","#2C6846"))+
scale_color_manual(values = c("dimgrey", "black"))
ggplotly(graph_example_interactive)library(gganimate)
library(gapminder) #Para obtener la base de datos gapminder: Extracto de los datos de Gapminder sobre esperanza de vida, PIB per cápita y población por país.
ggplot(
gapminder,
aes(x = gdpPercap, y=lifeExp, size = pop, colour = country)
) +
geom_point(show.legend = FALSE, alpha = 0.7) +
scale_color_viridis_d() +
scale_size(range = c(2, 12)) +
scale_x_log10() +
labs(x = "GDP per capita", y = "Life expectancy") + transition_time(year) +
labs(title = "Year: {frame_time}")Faltarían muchos otros tipos de gráficas y referencias que hacer, pero una de las mejores formas de obtener más ejemplos son con galerías, algunas son las siguientes
En la sección Limpieza de datos: Tidyr se vieron varios ejemplos donde se mencionaba que estos venían como sugerencias de las respectivas Cheat Sheets. Dichos datos fueron manipulados para ver el uso de las funciones que se estaban presentando. Obtén la estructura de los datos a los cuales se les aplicaron dichas funciones.
Investigar otras funciones de tidyr como chop(), complete() y pack().
Utiliza la función dplyr::top_n().
En la siguiente página se puede aprender sobre el uso de expresiones regulares. Usando las funciones dplyr::select() y dplyr::matches() replica los resultados que se dieron como ejemplo cuando se explico la función dplyr::select().
Crea un data frame o tibble con 5 variables numéricas. Utiliza la función dplyr::mutate_each() para aplicar la función dplyr::percent_rank() en cada una de las variables.
Investigar algunas funciones de purrr como reduce(), map() y accumulate().
Investiga el uso de dplyr::semi_join() y dplyr::anti_join().
Si eres meticuloso, verás que algo anda en los datos de la población. ¿Por qué? Hint: Ve la tabla que se uso para los últimos ejemplos de la sección dplyr.
Obtén resultados necesarios para contestar las siguientes preguntas sobre la base de datos population que ofrece dplyr.
who de dplyr contiene la información sobre los reportes de tuberculosis en el mundo. Haz una limpieza en esta base utilizando las funciones de tidyr y obtén la siguiente información.Las bases de datos who y population están relacionadas, por lo que se pueden unir para responder lo siguiente: ¿Existen países en los que el año donde se tuvo el máximo número de enfermos por tuberculosis sean los años donde se obtuvo un gran descenso en la población?
La base de datos storms proporcionada por dplyr contiene la información acerca de 198 tormentas tropicales con diferentes atributos. Obtén la siguiente información.
Utiliza las funciones Join que proporciona dplyr con las bases de datos dplyr::band_instruments y dplyr::and_members.
Investiga el uso de la función ggplot2::qplot().
¿Qué sucede en el primer ejemplo de la sección ggplot si los datos que sirvieron para el parámetro data de dicha función fueran los siguientes: Population_total_range<-Population_FEntityWS %>% group_by(Rango_edad) %>% summarise(Y = sum(Población Total))?
Usar la función ggplot2::facet_grid() para graficar, por los cinco principales estados (los de mayor población), un gráfico de barras donde se vea la distribución por rangos quinquenales de edad de su población.
Utiliza la función ggplot2::scale_function() para gráficar la función normal en el rango (-5,5).
Utiliza la función latex2exp::TeX() para agregar la ecuación de la función normal en la gráfica anterior.
Wickham, H. (2014). Tidy data. Journal of Statistical Software, 59(10), 1-23.
A work by Carlos Vásquez
cvasquezfguerra@gmail.com