6.7 Joins y Set Operations

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)
# A tibble: 8 x 3
  One_Two Three_Four Five_Six
    <dbl>      <dbl>    <dbl>
1       2          3        5
2       1          4        5
3       1          3        5
4       2          4        5
5       2          4        5
6       2          4        5
7       2          4        5
8       2          4        5
z <- tibble(One_Eigth = 1:8)
bind_cols(bind_rows(x,y), z)
# A tibble: 8 x 4
  One_Two Three_Four Five_Six One_Eigth
    <dbl>      <dbl>    <dbl>     <int>
1       2          3        5         1
2       1          4        5         2
3       1          3        5         3
4       2          4        5         4
5       2          4        5         5
6       2          4        5         6
7       2          4        5         7
8       2          4        5         8

Respecto a las operaciones de conjuntos se tienen las funciones dplyr::interset(), dplyr::union y dplyr::setdiff().

intersect(x, y)
# A tibble: 1 x 3
  One_Two Three_Four Five_Six
    <dbl>      <dbl>    <dbl>
1       2          4        5
union(x, y)
# A tibble: 4 x 3
  One_Two Three_Four Five_Six
    <dbl>      <dbl>    <dbl>
1       2          3        5
2       1          4        5
3       1          3        5
4       2          4        5
setdiff(x, y)
# A tibble: 3 x 3
  One_Two Three_Four Five_Six
    <dbl>      <dbl>    <dbl>
1       2          3        5
2       1          4        5
3       1          3        5

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))
a
# A tibble: 5 x 2
  L         N
  <chr> <int>
1 C        94
2 B        62
3 A        45
4 B        41
5 E        67
b
# A tibble: 5 x 2
  L     Lo   
  <chr> <lgl>
1 E     TRUE 
2 A     TRUE 
3 E     TRUE 
4 B     TRUE 
5 D     FALSE
left_join(a, b, by = "L")
# A tibble: 6 x 3
  L         N Lo   
  <chr> <int> <lgl>
1 C        94 NA   
2 B        62 TRUE 
3 A        45 TRUE 
4 B        41 TRUE 
5 E        67 TRUE 
6 E        67 TRUE 
right_join(a, b, by = "L")
# A tibble: 6 x 3
  L         N Lo   
  <chr> <int> <lgl>
1 B        62 TRUE 
2 A        45 TRUE 
3 B        41 TRUE 
4 E        67 TRUE 
5 E        67 TRUE 
6 D        NA FALSE
inner_join(a, b, by = "L")
# A tibble: 5 x 3
  L         N Lo   
  <chr> <int> <lgl>
1 B        62 TRUE 
2 A        45 TRUE 
3 B        41 TRUE 
4 E        67 TRUE 
5 E        67 TRUE 
full_join(a, b, by = "L")
# A tibble: 7 x 3
  L         N Lo   
  <chr> <int> <lgl>
1 C        94 NA   
2 B        62 TRUE 
3 A        45 TRUE 
4 B        41 TRUE 
5 E        67 TRUE 
6 E        67 TRUE 
7 D        NA FALSE

Y 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))
# A tibble: 448 x 3
   `Entidad Federativa` Rango_edad Población
   <chr>                <chr>          <dbl>
 1 Aguascalientes       0 a 4         51587.
 2 Baja California      0 a 4        124995.
 3 Baja California Sur  0 a 4         22289.
 4 Campeche             0 a 4         37749.
 5 Coahuila             0 a 4        104304.
 6 Colima               0 a 4         34496.
 7 Chiapas              0 a 4        170085.
 8 Chihuahua            0 a 4        122434.
 9 Distrito Federal     0 a 4        462096.
10 Durango              0 a 4         76977.
# … with 438 more rows
Population_FEntity_F <- readxl::read_xls("Data/Population_Sex_FEntity.xls", range = "B79:Q110", col_names = F)
(Population_FEntity_F <- normalization(Population_FEntity_F))
# A tibble: 448 x 3
   `Entidad Federativa` Rango_edad Población
   <chr>                <chr>          <dbl>
 1 Aguascalientes       0 a 4         56336 
 2 Baja California      0 a 4        127107.
 3 Baja California Sur  0 a 4         22569 
 4 Campeche             0 a 4         32170 
 5 Coahuila             0 a 4        115765 
 6 Colima               0 a 4         23751.
 7 Chiapas              0 a 4        227851 
 8 Chihuahua            0 a 4        122055 
 9 Distrito Federal     0 a 4        366480 
10 Durango              0 a 4         68470 
# … with 438 more rows

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"))
# A tibble: 448 x 5
   `Entidad Federativa` Rango_edad `Población Total` PoblaciónM PoblaciónF
   <chr>                <chr>                  <dbl>      <dbl>      <dbl>
 1 Aguascalientes       0 a 4                109853      51587.     56336 
 2 Baja California      0 a 4                268842     124995.    127107.
 3 Baja California Sur  0 a 4                 46070      22289.     22569 
 4 Campeche             0 a 4                 63034.     37749.     32170 
 5 Coahuila             0 a 4                244624.    104304.    115765 
 6 Colima               0 a 4                 45985      34496.     23751.
 7 Chiapas              0 a 4                491525     170085.    227851 
 8 Chihuahua            0 a 4                253834.    122434.    122055 
 9 Distrito Federal     0 a 4                682914.    462096.    366480 
10 Durango              0 a 4                150485      76977.     68470 
# … with 438 more rows
  • Por rango de edad ¿Qué sexo predomina en cada población?
Population_FEntityWS %>% 
  mutate(PPredominante = if_else(PoblaciónM>=PoblaciónF, "M", "F")) %>% 
  select(`Entidad Federativa`, Rango_edad, PPredominante)
# A tibble: 448 x 3
   `Entidad Federativa` Rango_edad PPredominante
   <chr>                <chr>      <chr>        
 1 Aguascalientes       0 a 4      F            
 2 Baja California      0 a 4      F            
 3 Baja California Sur  0 a 4      F            
 4 Campeche             0 a 4      M            
 5 Coahuila             0 a 4      F            
 6 Colima               0 a 4      M            
 7 Chiapas              0 a 4      F            
 8 Chihuahua            0 a 4      M            
 9 Distrito Federal     0 a 4      M            
10 Durango              0 a 4      M            
# … with 438 more rows
  • 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) %>% select(`Entidad Federativa`, Rango_edad, SPredominante)
# A tibble: 14 x 3
# Groups:   Rango_edad [14]
   `Entidad Federativa` Rango_edad SPredominante
   <chr>                <chr>      <chr>        
 1 México               0 a 4      M            
 2 México               5 a 9      M            
 3 México               10 a 14    M            
 4 México               15 a 19    F            
 5 México               20 a 24    F            
 6 México               25 a 29    F            
 7 México               30 a 34    F            
 8 México               35 a 39    F            
 9 México               40 a 44    F            
10 México               45 a 49    F            
11 México               50 a 54    F            
12 México               55 a 59    F            
13 Distrito Federal     60 y más   F            
14 México               NE         M            
  • ¿Cuál estado tiene el mayor número de mujeres y cuál el de hombres?
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")
# A tibble: 1 x 3
  `Entidad Federativa` SMasculino SFemenino
  <chr>                     <dbl>     <dbl>
1 México                 6871269.   7305419

¿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")
# A tibble: 1 x 3
  Rango_edad PoblaciónM PoblaciónF
  <chr>           <dbl>      <dbl>
1 10 a 14      5881838.    5790366

Finalmente, algunos puntos extra

  • Recordando los cinco problemas en unos datos sin limpieza, dos que quedaron pendientes
  1. Múltiples tipos de observaciones están en la misma tabla.
  2. Una sola observación esta en múltiples tablas.

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`))
# A tibble: 462 x 2
   `Entidad Federativa` `Población Total`
   <chr>                            <dbl>
 1 República Mexicana           103498524
 2 Aguascalientes                 1066233
 3 Baja California                2856361
 4 Baja California Sur             512030
 5 Campeche                        755703
 6 Coahuila                       2501413
 7 Colima                          568642
 8 Chiapas                        4293414
 9 Chihuahua                      3241513
10 Distrito Federal               8737172
# … with 452 more rows
(t2 <- Population_FEntity %>% select(`Entidad Federativa`, Rango_edad,  Población))
# A tibble: 462 x 3
   `Entidad Federativa` Rango_edad Población
   <chr>                <chr>          <dbl>
 1 República Mexicana   0 a 4       9637687 
 2 Aguascalientes       0 a 4        109853 
 3 Baja California      0 a 4        268842 
 4 Baja California Sur  0 a 4         46070 
 5 Campeche             0 a 4         63034.
 6 Coahuila             0 a 4        244624.
 7 Colima               0 a 4         45985 
 8 Chiapas              0 a 4        491525 
 9 Chihuahua            0 a 4        253834.
10 Distrito Federal     0 a 4        682914.
# … with 452 more rows

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)))
# A tibble: 5 x 2
      x new_variable
  <int> <chr>       
1     1 Hola        
2     2 2           
3     3 3           
4     4 4           
5     5 Adios       
  • dplyr proviene de la paquetería plyr la cual contiene muchas más funciones que pueden ser de utilidad en ciertas ocasiones.

Ejercicios

  1. Utiliza la función dplyr::top_n().

  2. 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().

  3. 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.

  4. Investigar algunas funciones de purrr como reduce(), map() y accumulate().

  5. Investiga el uso de dplyr::semi_join() y dplyr::anti_join().

  6. 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.

  7. Obtén resultados necesarios para contestar las siguientes preguntas sobre la base de datos population que ofrece dplyr.

  • ¿Cuantos años están registrados por país?
  • ¿Qué países tienen mayor población por año?
  • ¿Cuál es la edad promedio, con los años dados, de la población en cada país?
  • ¿Qué países son aquellos que tienen más de un millón de habitantes en algún año?
  • ¿Qué países tienen menos de un millón de habitantes en al menos un año?
  • ¿Que país ha sido el que ha tenido la peor reducción en su población y en que año?
  • ¿Qué país tuvo la mayor cantidad población en el año de tu nacimiento?
  • ¿Cuál fue el año en que cada país tuvo su mayor numero de habitantes?
  • ¿Cuál fue el año en que cada país tuvo su menor numero de habitantes?
  • ¿Cuántos países considera la base de datos?
  1. La base de datos 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.
  • Por país ¿Cuál fue el año con mayor número de enfermos por tuberculosis?
  • Por país ¿Cuál de los tres diagnósticos diferentes tiene una mayor ocurrencia?
  • ¿Cuál fue el año con mayor número de enfermos por país?
  • ¿Qué país ha tenido el mayor número de enfermos por tuberculosis?
  • ¿Cuál es el top-10 de países con mayor número de casos con tuberculosis?
  • ¿Hay algún país sin casos de tuberculosis en los años registrados?
  • ¿Cuál sexo es el más afectado, por país, con tuberculosis?
  • ¿Cuál es el rango de edad con mayor número de casos con tuberculosis?
  • ¿Cuál es el rango de edad con menor número de casos con tuberculosis?
  • Por rango de edad ¿Qué sexo es el más afectado con tuberculosis?
  1. 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?

  2. 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.

  • ¿Cuántos huracanes tuvieron las tres clasificaciones (Depresión Tropical, Tormenta Tropical y Huracán)?
  • ¿Cuál año fue el que tuvo el mayor número de huracanes?
  • ¿Cuál año fue el que tuvo el mayor número de Tormentas Tropicales?
  • ¿Cuál es el mes con mayor actividad?
  • ¿Qué tormenta ha sido la que tuvo mayor número de cambios en su categoría Saffir-Simpson?
  • ¿Cuál ha sido la tormenta con mayor velocidad promedio del viento en nudos?
  • ¿Por tormenta, cuál ha sido la velocidad máxima que alcanzo en \(k/h\)?
  • Considerando que una tormenta se puede clasificar por el estatus que predomina en ella, ¿Qué tipo de tormenta es la más recurrente en el Atlántico?
  • ¿Cuál ha sido la tormenta con mayor duración?
  • Considerando latitud y longitud en términos enteros (pensando así en segmentos del territorio), ¿Cuál ha sido la ubicación donde se ha tenido la mayor cantidad de tormentas?
  1. Utiliza las funciones Join que proporciona dplyr con las bases de datos dplyr::band_instruments y dplyr::and_members.