Joins e importación: como combinar, exportar e importar en R

Cuadernos prácticos de Software II del Grado en Ciencia de Datos Aplicada (curso 2024-2025)

Author

Javier Álvarez Liébana

1 Relacionando datos: joins

Al trabajar con datos no siempre tendremos la información en una sola tabla y a veces nos interesará cruzar la información de distintas fuentes.

Para ello usaremos un clásico de todo lenguaje que maneja datos: los famosos join, que nos permitirán cruzar una o varias tablas, haciendo uso de una columna identificadora de cada una de ellas (por ejemplo, imagina que cruzamos datos de hacienda y de antecedentes penales, haciendo join por la columna DNI).

 

La estructura básica es la siguiente:

tabla_1 |>
  xxx_join(tabla_2, by = id)

Vamos a probar los distintos joins con un ejemplo sencillo

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.5.1     ✔ tibble    3.2.1
✔ lubridate 1.9.3     ✔ tidyr     1.3.1
✔ purrr     1.0.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
tb_1 <- tibble("key" = 1:3, "val_x" = c("x1", "x2", "x3"))
tb_2 <- tibble("key" = c(1, 2, 4), "val_y" = c("y1", "y2", "y3"))
tb_1
# A tibble: 3 × 2
    key val_x
  <int> <chr>
1     1 x1   
2     2 x2   
3     3 x3   
tb_2
# A tibble: 3 × 2
    key val_y
  <dbl> <chr>
1     1 y1   
2     2 y2   
3     4 y3   

1.1 Left join

  • left_join(): mantiene todos los registros de la primera tabla, y busca cuales tienen id también en la segunda (en caso de no tenerlo se rellena con NA los campos de la 2ª tabla).

En nuestra caso queremos incorporar a tb_1 la información de tb_2, identificando los registros por la columna key (by = "key", la columna por la que tiene que cruzar)

tb_1  |> 
  left_join(tb_2, by = "key")
# A tibble: 3 × 3
    key val_x val_y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   
3     3 x3    <NA> 

tb_1 |> 
  left_join(tb_2, by = "key")
# A tibble: 3 × 3
    key val_x val_y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   
3     3 x3    <NA> 

Fíjate que los registros de la primera cuya key no ha encontrado en la segunda les ha dado el valor de ausente.

1.2 Right join

  • right_join(): mantiene todos los registros de la segunda tabla, y busca cuales tienen id también en la primera.

Vamos ahora a incorporar a tb_2 la información de tb_1, identificando los registros por la columna key (by = "key")

tb_1 |> 
  right_join(tb_2, by = "key")
# A tibble: 3 × 3
    key val_x val_y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   
3     4 <NA>  y3   

tb_1 |> 
  right_join(tb_2, by = "key")
# A tibble: 3 × 3
    key val_x val_y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   
3     4 <NA>  y3   

Fíjate que ahora los registros de la segunda cuya key no ha encontrado en la primera son los que les ha dado el valor de ausente.

1.3 Claves y sufijos

Las columnas clave que usaremos para el cruce no siempre se llamarán igual.

tb_1 <- tibble("key_1" = 1:3, "val_x" = c("x1", "x2", "x3"))
tb_2 <- tibble("key_2" = c(1, 2, 4), "val_y" = c("y1", "y2", "y3"))
  • by = c("key_2" = "key_2"): le indicaremos en qué columna de cada tabla están las claves por las que vamos a cruzar.
# Left
tb_1 |> 
  left_join(tb_2, by = c("key_1" = "key_2"))
# A tibble: 3 × 3
  key_1 val_x val_y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   
3     3 x3    <NA> 
# Right
tb_1  |> 
  right_join(tb_2, by = c("key_1" = "key_2"))
# A tibble: 3 × 3
  key_1 val_x val_y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   
3     4 <NA>  y3   

Además podemos cruzar por varias columnas a la vez (interpretará como igual registro aquel que tenga el conjunto de claves igual), con by = c("var1_t1" = "var1_t2", "var2_t1" = "var2_t2", ...). Modifiquemos el ejemplo anterior

tb_1 <- tibble("k_11" = 1:3, "k_12" = c("a", "b", "c"),  "val_x" = c("x1", "x2", "x3"))
tb_2 <- tibble("k_21" = c(1, 2, 4), "k_22" = c("a", "b", "e"), "val_y" = c("y1", "y2", "y3"))
# Left
tb_1 |> 
  left_join(tb_2,
            by = c("k_11" = "k_21", "k_12" = "k_22"))
# A tibble: 3 × 4
   k_11 k_12  val_x val_y
  <dbl> <chr> <chr> <chr>
1     1 a     x1    y1   
2     2 b     x2    y2   
3     3 c     x3    <NA> 

También podría suceder que al cruzar dos tablas, haya columnas de valores que se llamen igual

tb_1 <- tibble("key_1" = 1:3, "val" = c("x1", "x2", "x3"))
tb_2 <- tibble("key_2" = c(1, 2, 4), "val" = c("y1", "y2", "y3"))
# Left
tb_1 |> 
  left_join(tb_2, by = c("key_1" = "key_2"))
# A tibble: 3 × 3
  key_1 val.x val.y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   
3     3 x3    <NA> 

Fíjate que por defecto nos añade los sufijos .x y .y para indicarnos de que tabla vienen. Dicho sufijo podemos especificárselo en el argumento opcional suffix = ..., que nos permita distinguir las variables de una tabla y de otra.

# Left
tb_1 |>
  left_join(tb_2, by = c("key_1" = "key_2"), suffix = c("_tabla1", "_tabla2"))
# A tibble: 3 × 3
  key_1 val_tabla1 val_tabla2
  <dbl> <chr>      <chr>     
1     1 x1         y1        
2     2 x2         y2        
3     3 x3         <NA>      

1.4 Full join

  • full_join(): mantiene todos los registros de ambas tablas.

Los dos anteriores casos forman lo que se conoce como outer joins: cruces donde se mantienen observaciones que salgan en al menos una tabla. El tercer outer join es el conocido como full_join() que nos mantendrá las observaciones de ambas tablas, añadiendo las filas que no casen con la otra tabla.

tb_1 |> 
  full_join(tb_2, by = c("key_1" = "key_2"))
# A tibble: 4 × 3
  key_1 val.x val.y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   
3     3 x3    <NA> 
4     4 <NA>  y3   

1.5 Inner join

  • inner_join(): solo sobreviven los registros cuyo id esté en ambas tablas.

Frente a los outer join está lo que se conoce como inner join, con inner_join(): un cruce en el que solo se mantienen las observaciones que salgan en ambas tablas, solo mantiene aquellos registros matcheados.

tb_1 |> 
  inner_join(tb_2, by = c("key_1" = "key_2"))
# A tibble: 2 × 3
  key_1 val.x val.y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   

Fíjate que en términos de registros, inner_join si es conmutativa, nos da igual el orden de las tablas: lo único que cambia es el orden de las columnas que añade.

tb_1 |> 
  inner_join(tb_2, by = c("key_1" = "key_2"))
# A tibble: 2 × 3
  key_1 val.x val.y
  <dbl> <chr> <chr>
1     1 x1    y1   
2     2 x2    y2   
tb_2 |> 
  inner_join(tb_1, by = c("key_2" = "key_1"))
# A tibble: 2 × 3
  key_2 val.x val.y
  <dbl> <chr> <chr>
1     1 y1    x1   
2     2 y2    x2   

1.6 Anti/semi join

Por último tenemos dos herramientas interesantes para filtrar (no cruzar) registros: semi_join() y anti_join(). El semi join nos deja en la primera tabla los registros que cuya clave está también en la segunda (como un inner join pero sin añadir la info de la segunda tabla). Y el segundo, los anti join, hace justo lo contrario (aquellos que no están).

# semijoin
tb_1 |> 
  semi_join(tb_2, by = c("key_1" = "key_2"))
# A tibble: 2 × 2
  key_1 val  
  <int> <chr>
1     1 x1   
2     2 x2   
# antijoin
tb_1 |> 
  anti_join(tb_2, by = c("key_1" = "key_2"))
# A tibble: 1 × 2
  key_1 val  
  <int> <chr>
1     3 x3   

1.7 💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

Para los ejercicios usaremos las tablas disponibles en el paquete {nycflights13} (echa un vistazo antes)

library(nycflights13)
  • airlines: nombre de aerolíneas (con su abreviatura).
  • airports: datos de aeropuertos (nombres, longitud, latitud, altitud, etc).
  • flights: datos de vuelos.
  • planes: datos de los aviones.
  • weather: datos meteorológicos horarios de las estaciones LGA, JFK y EWR.

📝 Del paquete {nycflights13} cruza la tabla flights con airlines. Queremos mantener todos los registros de vuelos, añadiendo la información de las aerolíneas a dicha tabla.

Code
flights_airlines <-
  flights |> 
  left_join(airlines, by = "carrier")
flights_airlines

📝 A la tabla obtenida del cruce del apartado anterior, cruza después con los datos de los aviones en planes, pero incluyendo solo aquellos vuelos de los que tengamos información de sus aviones (y viceversa).

Code
flights_airlines_planes <- 
  flights_airlines |> 
  inner_join(planes, by = "tailnum")
flights_airlines_planes

📝 Repite el ejercicio anterior pero conservando ambas variables year (en una es el año del vuelo, en la otra es el año de construcción del avión), y distinguiéndolas entre sí

Code
flights_airlines_planes <- 
  flights_airlines |> 
  inner_join(planes, by = "tailnum",
             suffix = c("_flight", "_build_aircraft"))
flights_airlines_planes

📝 Al cruce obtenido del ejercicio anterior incluye la longitud y latitud de los aeropuertos en airports, distinguiendo entre la latitud/longitud del aeropuerto en destino y en origen.

Code
flights_airlines_planes %>%
  left_join(airports %>% select(faa, lat, lon),
            by = c("origin" = "faa")) |> 
  rename(lat_origin = lat, lon_origin = lon) |> 
  left_join(airports %>% select(faa, lat, lon),
            by = c("dest" = "faa")) |> 
  rename(lat_dest = lat, lon_dest = lon)

📝 Filtra de airports solo aquellos aeropuertos de los que salgan vuelos. Repite el proceso filtrado solo aquellos a los que lleguen vuelos

Code
airports |> 
  semi_join(flights, by = c("faa" = "origin"))
airports |> 
  semi_join(flights, by = c("faa" = "dest"))

📝 ¿De cuántos vuelos no disponemos información del avión? Elimina antes los vuelos que no tengan identificar (diferente a NA) del avión

Code
flights |> 
  drop_na(tailnum) |>
  anti_join(planes, by = "tailnum") |>
  count(tailnum, sort = TRUE) # de mayor a menor ya de paso

2 🐣Caso práctico I: Beatles y Rolling Stones

Vamos a empezar a practicar joins sencillos con la tabla band_members y band_instruments ya incluidos en el paquete {dplyr}.

library(dplyr)
band_members
# A tibble: 3 × 2
  name  band   
  <chr> <chr>  
1 Mick  Stones 
2 John  Beatles
3 Paul  Beatles
band_instruments
# A tibble: 3 × 2
  name  plays 
  <chr> <chr> 
1 John  guitar
2 Paul  bass  
3 Keith guitar

En la primera tenemos una serie de artistas y la banda a la que pertenecen; en la segunda tenemos una serie de artistas y el instrumento que tocan. Además de realizar las acciones solicitadas intenta visualizar qué tabla final tendrías antes de ejecutar el código..

2.1 Pregunta 1

Dada la tabla band_members, incorpora la información de qué instrumento toca cada miembro (band_instruments) de los que tienes en esa tabla.

Code
left_join_band <-
  band_members |> 
  left_join(band_instruments, by = "name")

2.2 Pregunta 2

Dadas las tablas band_members y band_instruments, ¿qué tipo de join deberías hacer para tener una tabla completa, sin ausencias, donde todos los miembros de la banda tengan la información de su instrumento, y cada instrumento tenga un miembro asociado a él?

Code
inner_join_band <-
  band_members |>
  inner_join(band_instruments, by = "name")

2.3 Pregunta 3

Dada la tabla band_instruments, ¿cómo incorporar la información de quién toca cada instrumento (en caso de que la conozcamos)?

Code
right_join_band <-
  band_members |>
  right_join(band_instruments, by = "name")

# other option
left_join_instruments <-
  band_instruments |> 
  left_join(band_members, by = "name")

2.4 Pregunta 4

Dadas las tablas band_members y band_instruments, ¿qué tipo de join deberías hacer para tener una tabla con toda la información, tanto de los miembros como de los instrumentos, aunque haya miembros cuyo instrumento no conozcas, e instrumentos cuyo portador no conozcas?

Code
full_join_band <-
  band_members |>
  full_join(band_instruments, by = "name")

3 🐣 Caso práctico II: renta municipios

En el archivo municipios.csv tenemos guardada la información de los municipios de España a fecha de 2019.

# datos 2019
mun_data <- read_csv(file = "./datos/municipios.csv")
Rows: 8212 Columns: 8
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (8): codauto, ine.ccaa.name, cpro, ine.prov.name, cmun, name, LAU_CODE, ...

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
mun_data
# A tibble: 8,212 × 8
   codauto ine.ccaa.name cpro  ine.prov.name cmun  name      LAU_CODE codigo_ine
   <chr>   <chr>         <chr> <chr>         <chr> <chr>     <chr>    <chr>     
 1 01      Andalucía     04    Almería       001   Abla      04001    04001     
 2 01      Andalucía     04    Almería       002   Abrucena  04002    04002     
 3 01      Andalucía     04    Almería       003   Adra      04003    04003     
 4 01      Andalucía     04    Almería       004   Albanchez 04004    04004     
 5 01      Andalucía     04    Almería       005   Alboloduy 04005    04005     
 6 01      Andalucía     04    Almería       006   Albox     04006    04006     
 7 01      Andalucía     04    Almería       007   Alcolea   04007    04007     
 8 01      Andalucía     04    Almería       008   Alcóntar  04008    04008     
 9 01      Andalucía     04    Almería       009   Alcudia … 04009    04009     
10 01      Andalucía     04    Almería       010   Alhabia   04010    04010     
# ℹ 8,202 more rows

Por otro lado el archivo renta_mun contiene datos de la renta per capita edmai de cada unidad administrativa (municipios, distritos, provincias, comunidades autonónomas, etc) para diferentes años.

renta_mun <- read_csv(file = "./datos/renta_mun.csv")
Warning: One or more parsing issues, call `problems()` on your data frame for details,
e.g.:
  dat <- vroom(...)
  problems(dat)
Rows: 55273 Columns: 7
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (2): Unidad, codigo_ine
dbl (5): 2019, 2018, 2017, 2016, 2015

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
renta_mun
# A tibble: 55,273 × 7
   Unidad                          `2019` `2018` `2017` `2016` `2015` codigo_ine
   <chr>                            <dbl>  <dbl>  <dbl>  <dbl>  <dbl> <chr>     
 1 44001 Ababuj                        NA     NA     NA     NA     NA 44001     
 2 4400101 Ababuj distrito 01          NA     NA     NA     NA     NA 4400101   
 3 4400101001 Ababuj sección 01001     NA     NA     NA     NA     NA 4400101001
 4 40001 Abades                     11429  10731  10314   9816   9904 40001     
 5 4000101 Abades distrito 01       11429  10731  10314   9816   9904 4000101   
 6 4000101001 Abades sección 01001  11429  10731  10314   9816   9904 4000101001
 7 10001 Abadía                      8954   8589   8207   7671   8416 10001     
 8 1000101 Abadía distrito 01        8954   8589   8207   7671   8416 1000101   
 9 1000101001 Abadía sección 01001   8954   8589   8207   7671   8416 1000101001
10 27001 Abadín                     10791  10258   9762   9478   9116 27001     
# ℹ 55,263 more rows

Antes de empezar vamos a normalizar nombres de variables haciendo uso de clean_names() del paquete {janitor}.

mun_data <-
  mun_data |> 
  janitor::clean_names()
renta_mun <-
  renta_mun |> 
  janitor::clean_names()

3.1 Pregunta 1

Convierte a tidydata renta_mun obteniendo una tabla de 4 columnas: unidad, year, renta y codigo_ine (sin ausentes y cada dato del tipo correcto)

Code
renta_mun_tidy <-
  renta_mun |> 
  pivot_longer(cols = contains("x"), names_to = "year",
               values_to = "renta", names_prefix = "x",
               names_transform = list(year = as.numeric),
               values_drop_na = TRUE)

3.2 Pregunta 2

Si te fijas en la tabla anterior, tenemos datos de diferentes unidades administrativas que no siempre son municipios. Sabiendo que todos los municipios tienen un código de 5 caracteres (que representan todos ellos números), filtra sólo aquellos registros que correspondan a unidades municipales.

Code
renta_mun_tidy <-
  renta_mun_tidy |>
  filter(str_detect(codigo_ine, pattern = "[0-9]{5}") & 
           str_length(codigo_ine) == 5)

3.3 Pregunta 3

A continuación separa adecuadamente la variable de unidad en dos columnas: una con el código (que ya tiene, por lo que debe eliminar uno de los dos) y el nombre. Elimina los espacios sobrantes (echa un vistazo a las opciones del paquete {stringr}).

Code
renta_mun_tidy <-
  renta_mun_tidy |>
  separate(col = "unidad", into = c("cod_rm", "name"), sep = 5) |> 
  select(-cod_rm) |> 
  mutate(name = str_trim(name)) 

3.4 Pregunta 4

¿En qué año fue mayor la renta media? ¿Y más baja? ¿Cuál fue la renta mediana de los municipios de España en 2019?

summary_renta <-
  renta_mun_tidy |> 
  summarise("mean_renta" = mean(renta, na.rm = TRUE),
            .by = year)
summary_renta |>
  slice_min(mean_renta, n = 1)
# A tibble: 1 × 2
   year mean_renta
  <dbl>      <dbl>
1  2015     10083.
summary_renta |>
  slice_max(mean_renta, n = 1)
# A tibble: 1 × 2
   year mean_renta
  <dbl>      <dbl>
1  2019     11672.
renta_mun_tidy |> 
  filter(year == 2019) |> 
  summarise("median_renta" = median(renta, na.rm = TRUE))
# A tibble: 1 × 1
  median_renta
         <dbl>
1        11462

3.5 Pregunta 5

Haz lo que consideres para obtener la provincia con la renta media más alta en 2019 y la más baja. Asegúrate de obtener su nombre.

Code
summary_by_prov <-
  renta_mun_tidy |> 
  filter(year == 2019) |> 
  left_join(mun_data, by = "codigo_ine", suffix = c("", "_rm")) |> 
  select(-contains("rm")) |> 
  summarise("mean_by_prov" = mean(renta, na.rm = TRUE),
            .by = c("cpro", "ine_prov_name"))

summary_by_prov |> 
  slice_max(mean_by_prov, n = 1)
# A tibble: 1 × 3
  cpro  ine_prov_name mean_by_prov
  <chr> <chr>                <dbl>
1 20    Gipuzkoa            15890.
Code
summary_by_prov |> 
  slice_min(mean_by_prov, n = 1)
# A tibble: 1 × 3
  cpro  ine_prov_name mean_by_prov
  <chr> <chr>                <dbl>
1 06    Badajoz              8805.

3.6 Pregunta 6

Obten de cada ccaa el nombre del municipio con mayor renta en 2019.

Code
renta_mun_tidy |> 
  filter(year == 2019) |> 
  left_join(mun_data, by = "codigo_ine", suffix = c("", "_rm")) |> 
  select(-contains("rm")) |> 
  slice_max(renta, by = "codauto")
# A tibble: 19 × 10
   name   codigo_ine  year renta codauto ine_ccaa_name cpro  ine_prov_name cmun 
   <chr>  <chr>      <dbl> <dbl> <chr>   <chr>         <chr> <chr>         <chr>
 1 Navas… 40904       2019 20242 07      Castilla y L… 40    Segovia       904  
 2 Almar… 10019       2019 13940 11      Extremadura   10    Cáceres       019  
 3 Oleir… 15058       2019 16447 12      Galicia       15    Coruña, A     058  
 4 Laukiz 48053       2019 21672 16      País Vasco    48    Bizkaia       053  
 5 Lumbr… 26091       2019 24007 17      Rioja, La     26    Rioja, La     091  
 6 Murcia 30030       2019 11631 14      Murcia, Regi… 30    Murcia        030  
 7 Rajad… 08178       2019 23401 09      Cataluña      08    Barcelona     178  
 8 Alocén 19023       2019 19700 08      Castilla - L… 19    Guadalajara   023  
 9 Arguis 22037       2019 19219 02      Aragón        22    Huesca        037  
10 Alcud… 04009       2019 14196 01      Andalucía     04    Almería       009  
11 Arang… 31023       2019 15517 15      Navarra, Com… 31    Navarra       023  
12 Santa… 35021       2019 15982 05      Canarias      35    Palmas, Las   021  
13 Rocaf… 46216       2019 17872 10      Comunitat Va… 46    Valencia/Val… 216  
14 Pozue… 28115       2019 26367 13      Madrid, Comu… 28    Madrid        115  
15 Valld… 07063       2019 19565 04      Balears, Ill… 07    Balears, Ill… 063  
16 Valde… 39093       2019 15574 06      Cantabria     39    Cantabria     093  
17 Tever… 33072       2019 15409 03      Asturias, Pr… 33    Asturias      072  
18 Ceuta  51001       2019 12038 18      Ceuta         51    Ceuta         001  
19 Melil… 52001       2019 11463 19      Melilla       52    Melilla       001  
# ℹ 1 more variable: lau_code <chr>

4 Importar/exportar

Hasta ahora sólo hemos utilizado datos ya cargados en paquetes, pero muchas veces necesitaremos importar datos externamente. Una de las principales fortalezas de R es que podemos importar datos muy fácilmente en diferentes formatos:

  • Formatos nativos de R: archivos .rda, .RData y .rds

  • Rectangular data (datos tabulados): archivos .csv y .tsv

  • Datos sin tabular: archivos .txt.

  • Datos en excel: archivos .xls y.xlsx.

  • Datos desde SAS/Stata/SPSS: archivos .sas7bdat, .sav y .dat.

  • Datos desde Google Drive

  • Datos desde API’s: aemet, catastro, censo, spotify, etc.

4.1 Formatos nativos de R

Los ficheros más sencillos para importar a R (y que suelen ocupar menos espacio en disco) son sus propias extensiones nativas: ficheros en formatos .RData, .rda y .rds. Para cargar los primeros basta con utilizar la función load() proporcionándole la ruta del fichero.

  • Archivos RData: vamos a importar el archivo world_bank_pop.RData que incluye la tabla world_bank_pop
load("./datos/world_bank_pop.RData")
world_bank_pop
# A tibble: 1,064 × 20
   country indicator      `2000`  `2001`  `2002`  `2003`  `2004`  `2005`  `2006`
   <chr>   <chr>           <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>   <dbl>
 1 ABW     SP.URB.TOTL    4.16e4 4.20e+4 4.22e+4 4.23e+4 4.23e+4 4.24e+4 4.26e+4
 2 ABW     SP.URB.GROW    1.66e0 9.56e-1 4.01e-1 1.97e-1 9.46e-2 1.94e-1 3.67e-1
 3 ABW     SP.POP.TOTL    8.91e4 9.07e+4 9.18e+4 9.27e+4 9.35e+4 9.45e+4 9.56e+4
 4 ABW     SP.POP.GROW    2.54e0 1.77e+0 1.19e+0 9.97e-1 9.01e-1 1.00e+0 1.18e+0
 5 AFE     SP.URB.TOTL    1.16e8 1.20e+8 1.24e+8 1.29e+8 1.34e+8 1.39e+8 1.44e+8
 6 AFE     SP.URB.GROW    3.60e0 3.66e+0 3.72e+0 3.71e+0 3.74e+0 3.81e+0 3.81e+0
 7 AFE     SP.POP.TOTL    4.02e8 4.12e+8 4.23e+8 4.34e+8 4.45e+8 4.57e+8 4.70e+8
 8 AFE     SP.POP.GROW    2.58e0 2.59e+0 2.61e+0 2.62e+0 2.64e+0 2.67e+0 2.70e+0
 9 AFG     SP.URB.TOTL    4.31e6 4.36e+6 4.67e+6 5.06e+6 5.30e+6 5.54e+6 5.83e+6
10 AFG     SP.URB.GROW    1.86e0 1.15e+0 6.86e+0 7.95e+0 4.59e+0 4.47e+0 5.03e+0
# ℹ 1,054 more rows
# ℹ 11 more variables: `2007` <dbl>, `2008` <dbl>, `2009` <dbl>, `2010` <dbl>,
#   `2011` <dbl>, `2012` <dbl>, `2013` <dbl>, `2014` <dbl>, `2015` <dbl>,
#   `2016` <dbl>, `2017` <dbl>
  • Archivos .rda: vamos a importar el dataset airquality desde airquality.rda
load("./datos/airquality.rda")
airquality |> as_tibble()
# A tibble: 153 × 6
   Ozone Solar.R  Wind  Temp Month   Day
   <int>   <int> <dbl> <int> <int> <int>
 1    41     190   7.4    67     5     1
 2    36     118   8      72     5     2
 3    12     149  12.6    74     5     3
 4    18     313  11.5    62     5     4
 5    NA      NA  14.3    56     5     5
 6    28      NA  14.9    66     5     6
 7    23     299   8.6    65     5     7
 8    19      99  13.8    59     5     8
 9     8      19  20.1    61     5     9
10    NA     194   8.6    69     5    10
# ℹ 143 more rows

Ten en cuenta que los archivos cargados con load() se cargan automáticamente en el entorno (con el nombre guardado originalmente), y no sólo se pueden cargar conjuntos de datos: load() nos permite cargar múltiples objetos (no sólo datos tabulares).

Los archivos nativos .rda y .RData son una forma adecuada de guardar el entorno.

load(file = "./datos/multiple_objects.rda")
  • Archivos .rds: para este tipo debemos utilizar readRDS(), y necesitamos incorporar un argumento file con la ruta. En este caso vamos a importar datos de cáncer de pulmón del North Central Cancer Treatment Group. Observe que ahora los archivos .rds incorporar solo una tabla, no un objeto en general
lung_cancer <-
  readRDS(file = "./datos/NCCTG_lung_cancer.rds") |>
  as_tibble()
lung_cancer
# A tibble: 228 × 10
    inst  time status   age   sex ph.ecog ph.karno pat.karno meal.cal wt.loss
   <dbl> <dbl>  <dbl> <dbl> <dbl>   <dbl>    <dbl>     <dbl>    <dbl>   <dbl>
 1     3   306      2    74     1       1       90       100     1175      NA
 2     3   455      2    68     1       0       90        90     1225      15
 3     3  1010      1    56     1       0       90        90       NA      15
 4     5   210      2    57     1       1       90        60     1150      11
 5     1   883      2    60     1       0      100        90       NA       0
 6    12  1022      1    74     1       1       50        80      513       0
 7     7   310      2    68     2       2       70        60      384      10
 8    11   361      2    71     2       2       60        80      538       1
 9     1   218      2    53     1       1       70        80      825      16
10     7   166      2    61     1       2       70        70      271      34
# ℹ 218 more rows
Importante

Las [rutas]{.hl-yellow deben ser siempre sin espacios, ñ, ni acentos.

4.2 Datos tabulados: readr

El paquete {readr} dentro del entorno {tidyverse} contiene varias funciones útiles para cargar datos rectangulares (sin formatear pero tabulados).

:::: columns ::: {.column width=“50%”}

  • read_csv(): archivos .csv variables separadas por comas
  • read_csv2(): variables separadas por punto y coma
  • read_tsv(): variables separadas por tabuladores.
  • read_table(): variables separadas por espacios.
  • read_delim(): función generar con opción de especificar el delimitador.

Todos ellos necesitan como argumento la ruta del fichero más otros opcionales (saltar cabecera o no, decimales, etc). Ver más en https://readr.tidyverse.org/

4.2.1 Archivos .csv

La principal ventaja de {readr} es que automatiza el formato para pasar de un fichero plano (sin formato) a un tibble (en filas y columnas, con formato).

  • Archivo .csv: con read_csv() cargaremos archivos separados por comas, pasando como argumento la ruta en file = .... Vamos a importar el conjunto de datos chickens.csv (sobre pollos de dibujos animados, por qué no). Si nos fijamos en la salida nos da el tipo de variables.
library(readr)
chickens <- read_csv(file = "./datos/chickens.csv")
Rows: 5 Columns: 4
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (3): chicken, sex, motto
dbl (1): eggs_laid

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
chickens
# A tibble: 5 × 4
  chicken                 sex     eggs_laid motto                               
  <chr>                   <chr>       <dbl> <chr>                               
1 Foghorn Leghorn         rooster         0 That's a joke, ah say, that's a jok…
2 Chicken Little          hen             3 The sky is falling!                 
3 Ginger                  hen            12 Listen. We'll either die free chick…
4 Camilla the Chicken     hen             7 Bawk, buck, ba-gawk.                
5 Ernie The Giant Chicken rooster         0 Put Captain Solo in the cargo hold. 

El formato de la variable se hará normalmente automáticamente por read_csv(), y podemos consultarlo con spec().

spec(chickens)
cols(
  chicken = col_character(),
  sex = col_character(),
  eggs_laid = col_double(),
  motto = col_character()
)

Aunque normalmente lo hace bien automáticamente podemos especificar el formato explícitamente en col_types = lista() (en formato lista, con col_xxx() para cada tipo de variable, por ejemplo eggs_laid se importará como carácter).

chickens <-
  read_csv(file = "./datos/chickens.csv",
           col_types = list(col_character(), col_character(),
                            col_character(), col_character()))
chickens
# A tibble: 5 × 4
  chicken                 sex     eggs_laid motto                               
  <chr>                   <chr>   <chr>     <chr>                               
1 Foghorn Leghorn         rooster 0         That's a joke, ah say, that's a jok…
2 Chicken Little          hen     3         The sky is falling!                 
3 Ginger                  hen     12        Listen. We'll either die free chick…
4 Camilla the Chicken     hen     7         Bawk, buck, ba-gawk.                
5 Ernie The Giant Chicken rooster 0         Put Captain Solo in the cargo hold. 

Incluso podemos indicar que variables queremos seleccionar (sin ocupar memoria), indicándolo en col_select = ... (en formato lista, con col_select = ...).

chickens <-
  read_csv(file = "./datos/chickens.csv",
           col_select = c(chicken, sex, eggs_laid))
Rows: 5 Columns: 3
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (2): chicken, sex
dbl (1): eggs_laid

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
chickens
# A tibble: 5 × 3
  chicken                 sex     eggs_laid
  <chr>                   <chr>       <dbl>
1 Foghorn Leghorn         rooster         0
2 Chicken Little          hen             3
3 Ginger                  hen            12
4 Camilla the Chicken     hen             7
5 Ernie The Giant Chicken rooster         0

4.2.2 Archivos .txt, .tsv

¿Qué ocurre cuando el separador no es correcto?

Si usamos read_csv() espera que el separador entre columnas sea una coma pero, como puedes ver con el siguiente .txt, lo interpreta todo como una sola columna: no tiene coma y no sabe dónde separar

datos_txt <- read_csv(file = "./datos/massey-rating.txt")
Rows: 10 Columns: 1
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (1): UCC PAY LAZ KPK  RT   COF BIH DII ENG ACU Rank Team            Conf

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
dim(datos_txt)
[1] 10  1
as_tibble(datos_txt)
# A tibble: 10 × 1
   `UCC PAY LAZ KPK  RT   COF BIH DII ENG ACU Rank Team            Conf`
   <chr>                                                                
 1 1   1   1   1   1     1   1   1   1   1    1 Ohio St          B10    
 2 2   2   2   2   2     2   2   2   4   2    2 Oregon           P12    
 3 3   4   3   4   3     4   3   4   2   3    3 Alabama          SEC    
 4 4   3   4   3   4     3   5   3   3   4    4 TCU              B12    
 5 6   6   6   5   5     7   6   5   6  11    5 Michigan St      B10    
 6 7   7   7   6   7     6  11   8   7   8    6 Georgia          SEC    
 7 5   5   5   7   6     8   4   6   5   5    7 Florida St       ACC    
 8 8   8   9   9  10     5   7   7  10   7    8 Baylor           B12    
 9 9  11   8  13  11    11  12   9  14   9    9 Georgia Tech     ACC    
10 13  10  13  11   8     9  10  11   9  10   10 Mississippi      SEC   

Para ello tenemos.

  • read_csv2() cuando el separador es punto y coma, read_tsv() cuando es un tabulador y read_table() cuando es un espacio.

  • read_delim() en general.

datos_txt <- read_table(file = "./datos/massey-rating.txt")

── Column specification ────────────────────────────────────────────────────────
cols(
  UCC = col_double(),
  PAY = col_double(),
  LAZ = col_double(),
  KPK = col_double(),
  RT = col_double(),
  COF = col_double(),
  BIH = col_double(),
  DII = col_double(),
  ENG = col_double(),
  ACU = col_double(),
  Rank = col_double(),
  Team = col_character(),
  Conf = col_character()
)
Warning: 10 parsing failures.
row col   expected     actual                        file
  1  -- 13 columns 15 columns './datos/massey-rating.txt'
  2  -- 13 columns 14 columns './datos/massey-rating.txt'
  3  -- 13 columns 14 columns './datos/massey-rating.txt'
  4  -- 13 columns 14 columns './datos/massey-rating.txt'
  5  -- 13 columns 15 columns './datos/massey-rating.txt'
... ... .......... .......... ...........................
See problems(...) for more details.
as_tibble(datos_txt)
# A tibble: 10 × 13
     UCC   PAY   LAZ   KPK    RT   COF   BIH   DII   ENG   ACU  Rank Team  Conf 
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <chr> <chr>
 1     1     1     1     1     1     1     1     1     1     1     1 Ohio  St   
 2     2     2     2     2     2     2     2     2     4     2     2 Oreg… P12  
 3     3     4     3     4     3     4     3     4     2     3     3 Alab… SEC  
 4     4     3     4     3     4     3     5     3     3     4     4 TCU   B12  
 5     6     6     6     5     5     7     6     5     6    11     5 Mich… St   
 6     7     7     7     6     7     6    11     8     7     8     6 Geor… SEC  
 7     5     5     5     7     6     8     4     6     5     5     7 Flor… St   
 8     8     8     9     9    10     5     7     7    10     7     8 Bayl… B12  
 9     9    11     8    13    11    11    12     9    14     9     9 Geor… Tech 
10    13    10    13    11     8     9    10    11     9    10    10 Miss… SEC  

4.3 Datos Excel (.xls, .xlsx)

Otro paquete de importación clave será el paquete {readxl} para importar datos desde Excel. Tres funciones serán clave:

  • read_xls() específica para .xls, read_xlsx() específica para .xlsx.
  • read_excel(): tanto para .xls como para .xlsx.

Vamos a importar deaths.xlsx con los registros de defunciones de famosos.

library(readxl)
deaths <- read_xlsx(path = "./datos/deaths.xlsx")
New names:
• `` -> `...2`
• `` -> `...3`
• `` -> `...4`
• `` -> `...5`
• `` -> `...6`
deaths
# A tibble: 18 × 6
   `Lots of people`             ...2                     ...3  ...4  ...5  ...6 
   <chr>                        <chr>                    <chr> <chr> <chr> <chr>
 1 simply cannot resist writing <NA>                     <NA>  <NA>  <NA>  some…
 2 at                           the                      top   <NA>  of    thei…
 3 or                           merging                  <NA>  <NA>  <NA>  cells
 4 Name                         Profession               Age   Has … Date… Date…
 5 David Bowie                  musician                 69    TRUE  17175 42379
 6 Carrie Fisher                actor                    60    TRUE  20749 42731
 7 Chuck Berry                  musician                 90    TRUE  9788  42812
 8 Bill Paxton                  actor                    61    TRUE  20226 42791
 9 Prince                       musician                 57    TRUE  21343 42481
10 Alan Rickman                 actor                    69    FALSE 16854 42383
11 Florence Henderson           actor                    82    TRUE  12464 42698
12 Harper Lee                   author                   89    FALSE 9615  42419
13 Zsa Zsa Gábor                actor                    99    TRUE  6247  42722
14 George Michael               musician                 53    FALSE 23187 42729
15 Some                         <NA>                     <NA>  <NA>  <NA>  <NA> 
16 <NA>                         also like to write stuff <NA>  <NA>  <NA>  <NA> 
17 <NA>                         <NA>                     at t… bott… <NA>  <NA> 
18 <NA>                         <NA>                     <NA>  <NA>  <NA>  too! 
deaths |> slice(1:6)
# A tibble: 6 × 6
  `Lots of people`             ...2       ...3  ...4     ...5          ...6     
  <chr>                        <chr>      <chr> <chr>    <chr>         <chr>    
1 simply cannot resist writing <NA>       <NA>  <NA>     <NA>          some not…
2 at                           the        top   <NA>     of            their sp…
3 or                           merging    <NA>  <NA>     <NA>          cells    
4 Name                         Profession Age   Has kids Date of birth Date of …
5 David Bowie                  musician   69    TRUE     17175         42379    
6 Carrie Fisher                actor      60    TRUE     20749         42731    

Una desgracia muy común es que haya algún tipo de comentario o texto al principio del fichero, teniendo que saltar esas filas.

Podemos saltar estas filas directamente en la carga con skip = ... (indicando el número de filas a saltar).

deaths <- read_xlsx(path = "./datos/deaths.xlsx", skip = 4)
deaths
# A tibble: 14 × 6
   Name          Profession Age   `Has kids` `Date of birth`     `Date of death`
   <chr>         <chr>      <chr> <chr>      <dttm>              <chr>          
 1 David Bowie   musician   69    TRUE       1947-01-08 00:00:00 42379          
 2 Carrie Fisher actor      60    TRUE       1956-10-21 00:00:00 42731          
 3 Chuck Berry   musician   90    TRUE       1926-10-18 00:00:00 42812          
 4 Bill Paxton   actor      61    TRUE       1955-05-17 00:00:00 42791          
 5 Prince        musician   57    TRUE       1958-06-07 00:00:00 42481          
 6 Alan Rickman  actor      69    FALSE      1946-02-21 00:00:00 42383          
 7 Florence Hen… actor      82    TRUE       1934-02-14 00:00:00 42698          
 8 Harper Lee    author     89    FALSE      1926-04-28 00:00:00 42419          
 9 Zsa Zsa Gábor actor      99    TRUE       1917-02-06 00:00:00 42722          
10 George Micha… musician   53    FALSE      1963-06-25 00:00:00 42729          
11 Some          <NA>       <NA>  <NA>       NA                  <NA>           
12 <NA>          also like… <NA>  <NA>       NA                  <NA>           
13 <NA>          <NA>       at t… bottom,    NA                  <NA>           
14 <NA>          <NA>       <NA>  <NA>       NA                  too!           

Además con col_names = ... ya podemos renombrar las columnas en la importación (proporcionar nombres supone 1ª línea ya como dato)

deaths <-
  read_xlsx(path = "./datos/deaths.xlsx", skip = 5,
            col_names = c("nombre", "profesion", "edad", "hijos", "nacimiento", "muerte"))
deaths
# A tibble: 14 × 6
   nombre             profesion           edad  hijos nacimiento          muerte
   <chr>              <chr>               <chr> <chr> <dttm>              <chr> 
 1 David Bowie        musician            69    TRUE  1947-01-08 00:00:00 42379 
 2 Carrie Fisher      actor               60    TRUE  1956-10-21 00:00:00 42731 
 3 Chuck Berry        musician            90    TRUE  1926-10-18 00:00:00 42812 
 4 Bill Paxton        actor               61    TRUE  1955-05-17 00:00:00 42791 
 5 Prince             musician            57    TRUE  1958-06-07 00:00:00 42481 
 6 Alan Rickman       actor               69    FALSE 1946-02-21 00:00:00 42383 
 7 Florence Henderson actor               82    TRUE  1934-02-14 00:00:00 42698 
 8 Harper Lee         author              89    FALSE 1926-04-28 00:00:00 42419 
 9 Zsa Zsa Gábor      actor               99    TRUE  1917-02-06 00:00:00 42722 
10 George Michael     musician            53    FALSE 1963-06-25 00:00:00 42729 
11 Some               <NA>                <NA>  <NA>  NA                  <NA>  
12 <NA>               also like to write… <NA>  <NA>  NA                  <NA>  
13 <NA>               <NA>                at t… bott… NA                  <NA>  
14 <NA>               <NA>                <NA>  <NA>  NA                  too!  

A veces las fechas de Excel tienen un formato incorrecto (sorpresa): podemos utilizar convertToDate() del paquete {openxlsx} para convertirlo.

library(openxlsx)
deaths$muerte <- convertToDate(deaths$muerte)
Warning in convertToDate(deaths$muerte): NAs introduced by coercion
deaths
# A tibble: 14 × 6
   nombre             profesion       edad  hijos nacimiento          muerte    
   <chr>              <chr>           <chr> <chr> <dttm>              <date>    
 1 David Bowie        musician        69    TRUE  1947-01-08 00:00:00 2016-01-10
 2 Carrie Fisher      actor           60    TRUE  1956-10-21 00:00:00 2016-12-27
 3 Chuck Berry        musician        90    TRUE  1926-10-18 00:00:00 2017-03-18
 4 Bill Paxton        actor           61    TRUE  1955-05-17 00:00:00 2017-02-25
 5 Prince             musician        57    TRUE  1958-06-07 00:00:00 2016-04-21
 6 Alan Rickman       actor           69    FALSE 1946-02-21 00:00:00 2016-01-14
 7 Florence Henderson actor           82    TRUE  1934-02-14 00:00:00 2016-11-24
 8 Harper Lee         author          89    FALSE 1926-04-28 00:00:00 2016-02-19
 9 Zsa Zsa Gábor      actor           99    TRUE  1917-02-06 00:00:00 2016-12-18
10 George Michael     musician        53    FALSE 1963-06-25 00:00:00 2016-12-25
11 Some               <NA>            <NA>  <NA>  NA                  NA        
12 <NA>               also like to w… <NA>  <NA>  NA                  NA        
13 <NA>               <NA>            at t… bott… NA                  NA        
14 <NA>               <NA>            <NA>  <NA>  NA                  NA        

También podemos cargar un Excel con varias hojas: para indicar la hoja (ya sea por su nombre o por su número) utilizaremos el argumento sheet = ....

mtcars <- read_xlsx(path = "./datos/datasets.xlsx", sheet = "mtcars")
mtcars
# A tibble: 32 × 11
     mpg   cyl  disp    hp  drat    wt  qsec    vs    am  gear  carb
   <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1  21       6  160    110  3.9   2.62  16.5     0     1     4     4
 2  21       6  160    110  3.9   2.88  17.0     0     1     4     4
 3  22.8     4  108     93  3.85  2.32  18.6     1     1     4     1
 4  21.4     6  258    110  3.08  3.22  19.4     1     0     3     1
 5  18.7     8  360    175  3.15  3.44  17.0     0     0     3     2
 6  18.1     6  225    105  2.76  3.46  20.2     1     0     3     1
 7  14.3     8  360    245  3.21  3.57  15.8     0     0     3     4
 8  24.4     4  147.    62  3.69  3.19  20       1     0     4     2
 9  22.8     4  141.    95  3.92  3.15  22.9     1     0     4     2
10  19.2     6  168.   123  3.92  3.44  18.3     1     0     4     4
# ℹ 22 more rows

Incluso podemos indicar el rango de celdas a cargar con range = ....

iris <- read_xlsx(path = "./datos/datasets.xlsx", sheet = "iris", range = "C1:E4")
iris
# A tibble: 3 × 3
  Petal.Length Petal.Width Species
         <dbl>       <dbl> <chr>  
1          1.4         0.2 setosa 
2          1.4         0.2 setosa 
3          1.3         0.2 setosa 

4.4 Importar desde SAS/STATA/SPSS

El paquete {haven} dentro del universo tidyverse nos permitirá importar ficheros de los 3 software de pago más importantes: SAS, SPSS y Stata.

library(haven)

# SAS
iris_sas <- read_sas(data_file = "./datos/iris.sas7bdat")

# SPSS
iris_spss <- read_sav(file = "./datos/iris.sav")

# Stata
iris_stata <- read_dta(file = "./datos/iris.dta")

4.5 Exportar

De la misma forma que podemos importar también podemos exportar

  • exportar en .RData (opción recomendada para variables almacenadas en R). Recuerda que esta extensión sólo se puede utilizar en R. Para ello, basta con utilizar save(object, file = path).
table <- tibble("a" = 1:4, "b" = 1:4)
save(table, file = "./datos/table.RData")
rm(table) # eliminar
load("./datos/table.RData")
table
# A tibble: 4 × 2
      a     b
  <int> <int>
1     1     1
2     2     2
3     3     3
4     4     4

La ventaja de .RData es que podemos exportar múltiples variables de nuestro environment, no solo un único dataset.

table <- tibble("a" = 1:4, "b" = 1:4)
a <- 1
b <- c("javi", "sandra")
save(table, a, b, file = "./datos/mult_obj.RData")
rm(list = c("a", "b", "table"))
load("./datos/mult_obj.RData")
table
# A tibble: 4 × 2
      a     b
  <int> <int>
1     1     1
2     2     2
3     3     3
4     4     4
  • exportado en .csv. Para ello simplemente utilizamos write_csv(object, file = path), y es el más recomendable para exportar bases de datos de tamaño pequeño o mediano. Ver https://arrow.apache.org/docs/r/ para bases de datos masivas.
write_csv(table, file = "./datos/table.csv")
read_csv(file = "./datos/table.csv")
Rows: 4 Columns: 2
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
dbl (2): a, b

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# A tibble: 4 × 2
      a     b
  <dbl> <dbl>
1     1     1
2     2     2
3     3     3
4     4     4

4.6 Importar desde web

Una de las principales ventajas de R es que podemos hacer uso de todas las funciones anteriores de importar pero directamente desde una web, sin necesidad de realizar la descarga manual: en lugar de pasarle la ruta local le indicaremos el enlace. Por ejemplo, vamos a descargar los datos covid del ISCIII (https://cnecovid.isciii.es/covid19/#documentaci%C3%B3n-y-datos)

covid_data <-
  read_csv(file = "https://cnecovid.isciii.es/covid19/resources/casos_hosp_uci_def_sexo_edad_provres.csv")
covid_data
Rows: 500 Columns: 8
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr  (3): provincia_iso, sexo, grupo_edad
dbl  (4): num_casos, num_hosp, num_uci, num_def
date (1): fecha

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# A tibble: 500 × 8
   provincia_iso sexo  grupo_edad fecha      num_casos num_hosp num_uci num_def
   <chr>         <chr> <chr>      <date>         <dbl>    <dbl>   <dbl>   <dbl>
 1 A             H     0-9        2020-01-01         0        0       0       0
 2 A             H     10-19      2020-01-01         0        0       0       0
 3 A             H     20-29      2020-01-01         0        0       0       0
 4 A             H     30-39      2020-01-01         0        0       0       0
 5 A             H     40-49      2020-01-01         0        0       0       0
 6 A             H     50-59      2020-01-01         0        0       0       0
 7 A             H     60-69      2020-01-01         0        0       0       0
 8 A             H     70-79      2020-01-01         0        0       0       0
 9 A             H     80+        2020-01-01         0        0       0       0
10 A             H     NC         2020-01-01         0        0       0       0
# ℹ 490 more rows

4.7 Importar desde wikipedia

El paquete {rvest}, uno de los más útiles de {tidyverse} nos permite importar (scrappear) directamente desde un html. Por ejemplo, para exportar tablas de wikipedia basta con read_html() para importar el html, html_element(«table») para extraer los objetos tabla, y html_table() para convertir la tabla html a tibble.

library(rvest)

Attaching package: 'rvest'
The following object is masked from 'package:readr':

    guess_encoding
wiki_jump <- 'https://en.wikipedia.org/wiki/Men%27s_long_jump_world_record_progression'
wiki_jump |> read_html() |> 
  html_element("table") |> 
  html_table()
# A tibble: 19 × 5
   Mark                       Wind   Athlete                  Place        Date 
   <chr>                      <chr>  <chr>                    <chr>        <chr>
 1 7.61 m (24 ft 11+1⁄2 in)   ""     Peter O'Connor (IRE)     Dublin, Ire… 5 Au…
 2 7.69 m (25 ft 2+3⁄4 in)    ""     Edward Gourdin (USA)     Cambridge, … 23 J…
 3 7.76 m (25 ft 5+1⁄2 in)    ""     Robert LeGendre (USA)    Paris, Fran… 7 Ju…
 4 7.89 m (25 ft 10+1⁄2 in)   ""     DeHart Hubbard (USA)     Chicago, Un… 13 J…
 5 7.90 m (25 ft 11 in)       ""     Edward Hamm (USA)        Cambridge, … 7 Ju…
 6 7.93 m (26 ft 0 in)        "0.0"  Sylvio Cator (HAI)       Paris, Fran… 9 Se…
 7 7.98 m (26 ft 2 in)        "0.5"  Chuhei Nambu (JPN)       Tokyo, Japan 27 O…
 8 8.13 m (26 ft 8 in)        "1.5"  Jesse Owens (USA)        Ann Arbor, … 25 M…
 9 8.21 m (26 ft 11 in)       "0.0"  Ralph Boston (USA)       Walnut, Uni… 12 A…
10 8.24 m (27 ft 1⁄4 in)      "1.8"  Ralph Boston (USA)       Modesto, Un… 27 M…
11 8.28 m (27 ft 1+3⁄4 in)    "1.2"  Ralph Boston (USA)       Moscow, Sov… 16 J…
12 8.31 m (27 ft 3 in) A      "−0.1" Igor Ter-Ovanesyan (URS) Yerevan, So… 10 J…
13 8.33 m (27 ft 3+3⁄4 in)[2] ""     Phil Shinnick (USA)      Modesto, Un… 25 M…
14 8.31 m (27 ft 3 in)        "0.0"  Ralph Boston (USA)       Kingston, J… 15 A…
15 8.34 m (27 ft 4+1⁄4 in)    "1.0"  Ralph Boston (USA)       Los Angeles… 12 S…
16 8.35 m (27 ft 4+1⁄2 in)[5] "0.0"  Ralph Boston (USA)       Modesto, Un… 29 M…
17 8.35 m (27 ft 4+1⁄2 in) A  "0.0"  Igor Ter-Ovanesyan (URS) Mexico City… 19 O…
18 8.90 m (29 ft 2+1⁄4 in) A  "2.0"  Bob Beamon (USA)         Mexico City… 18 O…
19 8.95 m (29 ft 4+1⁄4 in)    "0.3"  Mike Powell (USA)        Tokyo, Japan 30 A…

4.8 Importar desde google drive

Otra opción disponible (especialmente si trabajamos con otras personas trabajando) es importar desde una hoja de cálculo de Google Drive, haciendo uso de read_sheet() del paquete {googlesheets4}.

La primera vez se te pedirá un permiso tidyverse para interactuar con tu drive

library(googlesheets4)
google_sheet <-
  read_sheet("https://docs.google.com/spreadsheets/d/1Uz38nHjl3bmftxDpcXj--DYyPo1I39NHVf-xjeg1_wI/edit?usp=sharing")
! Using an auto-discovered, cached token.
  To suppress this message, modify your code or options to clearly consent to
  the use of a cached token.
  See gargle's "Non-interactive auth" vignette for more details:
  <https://gargle.r-lib.org/articles/non-interactive-auth.html>
ℹ The googlesheets4 package is using a cached token for 'javalv09@ucm.es'.
✔ Reading from "import-google-sheet".
✔ Range 'Hoja 1'.
google_sheet
# A tibble: 3 × 4
  a         b     c d     
  <chr> <dbl> <dbl> <chr> 
1 d1      0.5     1 hombre
2 d2     -0.3     2 hombre
3 d3      1.1     3 mujer 

4.9 Importar desde API

4.9.1 owid

Otra opción interesante es la descarga de datos desde una API: un intermediario entre una app o proveedor de datos y nuestro R. Por ejemplo, carguemos la librería {owidR}, que nos permite descargar datos de la web https://ourworldindata.org/. Por ejemplo, la función owid_covid() carga sin darnos cuenta más de 400 000 registros con más de 60 variables de 238 países.

library(owidR)
owid_covid() |> as_tibble()
# A tibble: 7 × 67
  iso_code continent location    date       total_cases new_cases
  <chr>    <chr>     <chr>       <IDate>          <int>     <int>
1 AFG      Asia      Afghanistan 2020-01-05           0         0
2 AFG      Asia      Afghanistan 2020-01-06           0         0
3 AFG      Asia      Afghanistan 2020-01-07           0         0
4 AFG      Asia      Afghanistan 2020-01-08           0         0
5 AFG      Asia      Afghanistan 2020-01-09           0         0
6 AFG      Asia      Afghanistan 2020-01-10           0         0
7 AFG      Asia      Afghanistan 2020-01-11           0         0
# ℹ 61 more variables: new_cases_smoothed <dbl>, total_deaths <int>,
#   new_deaths <int>, new_deaths_smoothed <dbl>, total_cases_per_million <dbl>,
#   new_cases_per_million <dbl>, new_cases_smoothed_per_million <dbl>,
#   total_deaths_per_million <dbl>, new_deaths_per_million <dbl>,
#   new_deaths_smoothed_per_million <dbl>, reproduction_rate <dbl>,
#   icu_patients <int>, icu_patients_per_million <dbl>, hosp_patients <int>,
#   hosp_patients_per_million <dbl>, weekly_icu_admissions <int>, …

4.9.2 aemet

En muchas ocasiones para conectarnos a la API primero tendremos que registrarnos y obtener una clave, este es el caso del paquete {climaemet} para acceder a datos meteorológicos de España (https://opendata.aemet.es/centrodedescargas/inicio).

Una vez tengamos la clave API la registramos en nuestro RStudio para poder utilizarla en el futuro.

library(climaemet)

# Api key
apikey <- "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqYXZhbHYwOUB1Y20uZXMiLCJqdGkiOiI4YTU1ODUxMS01MTE3LTQ4MTYtYmM4OS1hYmVkNDhiODBkYzkiLCJpc3MiOiJBRU1FVCIsImlhdCI6MTY2NjQ2OTcxNSwidXNlcklkIjoiOGE1NTg1MTEtNTExNy00ODE2LWJjODktYWJlZDQ4YjgwZGM5Iiwicm9sZSI6IiJ9.HEMR77lZy2ASjmOxJa8ppx2J8Za1IViurMX3p1reVBU"

aemet_api_key(apikey, install = TRUE)

Con este paquete podemos hacer una búsqueda de estaciones para conocer tanto su código postal como su código identificador dentro de la red AEMET

stations <- aemet_stations()
stations
# A tibble: 947 × 7
   indicativo indsinop nombre                 provincia altitud longitud latitud
   <chr>      <chr>    <chr>                  <chr>       <dbl>    <dbl>   <dbl>
 1 B013X      "08304"  ESCORCA, LLUC          ILLES BA…     490     2.89    39.8
 2 B051A      "08316"  SÓLLER, PUERTO         ILLES BA…       5     2.69    39.8
 3 B087X      ""       BANYALBUFAR            ILLES BA…      60     2.51    39.7
 4 B103B      "99103"  ANDRATX - SANT ELM     ILLES BA…      52     2.37    39.6
 5 B158X      ""       CALVIÀ, ES CAPDELLÀ    ILLES BA…      50     2.47    39.6
 6 B228       "08301"  PALMA, PUERTO          ILLES BA…       3     2.63    39.6
 7 B236C      ""       PALMA, UNIVERSIDAD     ILLES BA…      95     2.64    39.6
 8 B248       "08303"  SIERRA DE ALFABIA, BU… ILLES BA…    1030     2.71    39.7
 9 B275E      "08302"  SON BONET, AEROPUERTO  BALEARES       49     2.71    39.6
10 B278       "08306"  PALMA DE MALLORCA, AE… ILLES BA…       8     2.74    39.6
# ℹ 937 more rows

Por ejemplo, para obtener datos de la estación del aeropuerto de El Prat, Barcelona, el código a proporcionar es «0076», obteniendo datos horarios.

aemet_last_obs("0076")
# A tibble: 13 × 23
   idema   lon fint                 prec   alt  vmax    vv    dv   lat  dmax
   <chr> <dbl> <dttm>              <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
 1 0076   2.07 2024-11-02 23:00:00   0       4   5.7   3     360  41.3   350
 2 0076   2.07 2024-11-03 00:00:00   0       4   5.7   3.9   360  41.3   360
 3 0076   2.07 2024-11-03 01:00:00   0       4   6.2   3.6    10  41.3    20
 4 0076   2.07 2024-11-03 02:00:00   0       4   5.1   3.8   360  41.3    10
 5 0076   2.07 2024-11-03 03:00:00   0       4   5.7   4.3   360  41.3   350
 6 0076   2.07 2024-11-03 04:00:00   0       4   6.7   4.6   360  41.3    20
 7 0076   2.07 2024-11-03 05:00:00   0       4   7.2   5.7   360  41.3    10
 8 0076   2.07 2024-11-03 06:00:00   0.2     4   7.2   5     360  41.3    20
 9 0076   2.07 2024-11-03 07:00:00   0       4   6.2   4.6    10  41.3    10
10 0076   2.07 2024-11-03 08:00:00   0       4  11.3   5      30  41.3    10
11 0076   2.07 2024-11-03 09:00:00   0.1     4  10.3   4.5    40  41.3    30
12 0076   2.07 2024-11-03 10:00:00   9       4   6.2   3.4   290  41.3   320
13 0076   2.07 2024-11-03 11:00:00   0.1     4  10.3   3.5   360  41.3    20
# ℹ 13 more variables: ubi <chr>, pres <dbl>, hr <dbl>, stdvv <dbl>, ts <dbl>,
#   pres_nmar <dbl>, tamin <dbl>, ta <dbl>, tamax <dbl>, tpr <dbl>, vis <dbl>,
#   stddv <dbl>, inso <dbl>

4.9.3 US census

Una de las herramientas más útiles de los últimos años es la conocida como {tidycensus}: una herramienta para facilitar el proceso de descarga de datos censales de Estados Unidos desde R

library(tidycensus)
  • get_decennial(): para acceder a los datos de censos (US Decennial Census), se hacen cada 10 años (años 2000, 2010 y 2020).

  • get_acs(): para acceder a los datos anuales y quinquenales (5 años) de la ACS (American Community Survey) (censo != encuesta)

  • get_estimates(): para acceder a las estimaciones anuales de población, natalidad y mortalidad

  • get_pums(): para acceder a los microdatos (datos sin agregar) de la ACS (anonimizados a nivel individual)

  • get_flows(): para acceder a los datos de flujo de migraciones

Por ejemplo vamos a descargarnos los datos censales (get_decennial()) a nivel estado (geography = "state") de la población (variable variables = "P001001") para el año 2010 (ver variables en tidycensus::load_variables())

total_population_10 <-
  get_decennial(geography = "state", 
  variables = "P001001",
  year = 2010)
Getting data from the 2010 decennial Census
Using Census Summary File 1
total_population_10
# A tibble: 52 × 4
   GEOID NAME        variable    value
   <chr> <chr>       <chr>       <dbl>
 1 01    Alabama     P001001   4779736
 2 02    Alaska      P001001    710231
 3 04    Arizona     P001001   6392017
 4 05    Arkansas    P001001   2915918
 5 06    California  P001001  37253956
 6 22    Louisiana   P001001   4533372
 7 21    Kentucky    P001001   4339367
 8 08    Colorado    P001001   5029196
 9 09    Connecticut P001001   3574097
10 10    Delaware    P001001    897934
# ℹ 42 more rows

4.9.4 otras

Otras opciones

4.10 💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 El conjunto de datos who2 del paquete {tidyr} que hemos utilizado en ejercicios anteriores, expórtalo a un formato nativo R en la carpeta datos de tu proyecto de RStudio

Code
library(tidyr)
save(who2, file = "./datos/who2.RData")

📝 Carga el conjunto de datos who2 pero desde la carpeta data (importa el fichero creado en el ejercicio anterior)

Code
load("./datos/who2.RData")

📝 Repite lo mismo (exportar e importar) en 4 formatos: .csv, .xlsx, .sav (spss) y .dta (stata)

Code
# csv
library(readr)
write_csv(who2, file = "./datos/who2.csv")
who2_data <- read_csv(file = "./datos/who2.csv")

# excel
library(openxlsx)
write.xlsx(who2, file = "./datos/who2.xlsx")
who2_data <- read_xlsx(path = "./datos/who2.xlsx")

# sas y stata
library(haven)
write_sav(who2, path = "./datos/who2.sav")
who2_data <- read_spss(path = "./datos/who2.sav")

write_dta(who2, path = "./datos/who2.dta")
who2_data <- read_dta(path = "./datos/who2.dta")

📝 Repita la carga de who2.csv pero seleccione sólo las 4 primeras columnas ya cargadas.

Code
who_select <-
  read_csv(file = "./datos/who2.csv",
           col_select = c("country", "iso2", "iso3", "year"))
who_select

5 🐣 Caso práctico III: encuesta CIS

📊 Datos

Vamos a poner en práctica la carga y preprocesado de un fichero generado por uno de los programas informáticos más utilizados (SPSS). El fichero contiene datos del barómetro del CIS (Centro de Investigaciones Sociológicas) «Percepciones sobre igualdad entre hombres y mujeres y estereotipos de género» cuyo trabajo muestral se realizó del 6 al 14 de noviembre (4000 entrevistas a mayores de 16 años de ambos sexos en 1174 municipios y 50 provincias).

5.1 Pregunta 1

Carga el archivo de extensión .sav que tienes en la subcarpeta CIS-feminismo dentro de la carpeta de datos. Tras la carga normaliza los nombres de las variables con el paquete {janitor}.

Code
library(haven)
data <-
  read_sav(file = "./datos/CIS-feminismo/3428.sav") |> 
  janitor::clean_names()

5.2 Pregunta 2

Usando tidyverse calcula la cantidad de elementos diferentes de cada variable y procede a eliminar aquellas cuyo valor sea constante (es decir solo haya una modalidad). Primero obtén el vector de nombres de dichas variables para luego poder usarlo en un select() (de manera que no dependa de los nombres de variables de la tabla)

Code
rm_variables <-
  data |> 
  summarise(across(everything(), n_distinct)) |> 
  pivot_longer(cols = everything(), names_to = "variable", values_to = "n_values") |> 
  filter(n_values <= 1) |> 
  pull(variable)
data <-
  data |> 
  select(-rm_variables)
Warning: Using an external vector in selections was deprecated in tidyselect 1.1.0.
ℹ Please use `all_of()` or `any_of()` instead.
  # Was:
  data %>% select(rm_variables)

  # Now:
  data %>% select(all_of(rm_variables))

See <https://tidyselect.r-lib.org/reference/faq-external-vector.html>.

5.3 Pregunta 3

Haz lo mismo pero para eliminar aquellas variables que en cada fila tomen un valor distinto (no haya ni un solo repetido). Estas columnas harán funciones de id del encuestado pero solo necesitamos una de ellas, elimina las demás.

Code
rm_variables <-
  data |> 
  summarise(across(everything(), n_distinct)) |> 
  pivot_longer(cols = everything(), names_to = "variable", values_to = "n_values") |> 
  filter(n_values == nrow(data)) |> 
  pull(variable)
data <-
  data |> 
  select(-rm_variables[-1])

5.4 Pregunta 4

Para entender qué significan las variables, tienes en la carpeta CIS-feminismos el cuestionario que se hizo a cada encuestada en cues3428.pdf y la ficha técnica en FT3428.pdf. En estos documentos verás cómo, por ejemplo, las variables del cuestionario comienzan casi todas por p..., aunque también tenemos otro tipo de variables relacionadas con las tareas del hogar (tarhog...), los hijos (hijomenor...), el cuidado de los niños (cuidadohijos...), las tareas de cuidado en general (tareascuid...) y otras variables relacionadas con la escala ideológica, la religión, el recuerdo de voto, etc. Además, todas las variables que comienzan por ia_xxx se refieren a códigos relativos a posibles incidencias en la recogida de datos y las variables peso... al tipo de ponderación utilizado en el muestreo. Proceda a eliminar estos dos últimos tipos de variables.

Code
data <-
  data |> 
  select(-contains("ia_"), -contains("peso"))

5.5 Pregunta 5

Calcula el número de entrevistas por comunidad autónoma (ccaa) y extrae las 5 con menos entrevistas.

Code
data |>
  count(ccaa, sort = TRUE)

data |>
  count(ccaa) |> 
  slice_min(n = 5, n)

5.6 Pregunta 6

Usa el paquete {datapasta} para copiar de la página del INE la población de cada provincia e importarlo a un tibble

Code
# este código debería generarse automáticamente: cuando instalas el
# paquete se instala un addin en R Studio (parte superior) que te
# permite ctrl+c una tabla y diferentes opciones para importarla
population <- 
  tibble::tribble(~Total, ~`47.385.107`,
                    "01 Andalucía",   "8.472.407",
                       "02 Aragón",   "1.326.261",
      "03 Asturias, Principado de",   "1.011.792",
               "04 Balears, Illes",   "1.173.008",
                     "05 Canarias",   "2.172.944",
                    "06 Cantabria",     "584.507",
              "07 Castilla y León",   "2.383.139",
         "08 Castilla - La Mancha",   "2.049.562",
                     "09 Cataluña",   "7.763.362",
         "10 Comunitat Valenciana",   "5.058.138",
                  "11 Extremadura",   "1.059.501",
                      "12 Galicia",   "2.695.645",
         "13 Madrid, Comunidad de",   "6.751.251",
            "14 Murcia, Región de",   "1.518.486",
  "15 Navarra, Comunidad Foral de",     "661.537",
                   "16 País Vasco",   "2.213.993",
                    "17 Rioja, La",     "319.796",
                        "18 Ceuta",      "83.517",
                      "19 Melilla",      "86.261"
  )

5.7 Pregunta 7

Renombra de manera adecuada las variables de population, convierte la población a variable numérica y separa la variable de la comunidad autónoma en 2, una para el código y otro para el nomnbre (procesa ete último de manera adecuada con el paquete {stringr} para eliminar espacios en blanco).

Code
population <-
  population |> 
  rename(ccaa = Total, pop = `47.385.107`) |> 
  mutate("pop" = as.numeric(str_replace_all(pop, "\\.", ""))) |> 
  separate(col = ccaa, into = c("cod_INE", "name"), sep = 2) |> 
  mutate(name = str_squish(name))

5.8 Pregunta 8

Incorpora la información de la población a nuestra tabla del CIS. Tras incorporar dicha información calcula una tabla resumen con el ratio entre el porcentaje de población que representa cada ccaa respecto al total de España y el porcentaje de encuestados de dicha comunidad respecto al total de encuestados. ¿Cuáles son las 3 provincias más sobrerepresentadas? ¿Y las 3 más infrarrepresentads?

Code
data <-
  data |> 
  left_join(population |> mutate("cod_INE" = as.numeric(cod_INE)),
            by = c("ccaa" = "cod_INE")) |> 
  relocate(name, pop, .after = "ccaa")

prop_surv_pop <-
  data |>
  summarise("prop_surv" = n() / sum(nrow(data)),
            "pop" = unique(pop),
            .by = ccaa) |> 
  mutate("prop_pop" = pop / sum(pop),
         "ratio" = prop_pop / prop_surv)

# sobre
prop_surv_pop |> 
  slice_min(ratio, n = 3)

# infra
prop_surv_pop |> 
  slice_max(ratio, n = 3)

6 Listas

Ya hemos visto que las listas son un objeto en R que nos permite almacenar colecciones de variables de distinto tipo (como con data.frame y tibble) pero también diferentes longitudes, con estructuras totalmente heterogéneas (incluso una lista puede tener dentro otra lista).

nombre <- "Javi"
edad <- 34
notas <- c(7, 8, 5, 3, 10, 9)
progenitores <- c("Paloma", "Goyo")

# Podemos no ponerle nombre pero solo podremos acceder por índice
list_var <- list("nombre" = nombre, "edad" = edad,
                 "notas" = notas, "parents" = progenitores)
list_var$nombre
[1] "Javi"
list_var$progenitores
NULL

7 Listas

También podemos hacer listas con otras listas dentro, de forma que para acceder a cada nivel debamos utilizar el operador [[]].

list_of_lists <- list("list_1" = list_var[1:2], "list_2" = list_var[3:4])
names(list_of_lists)
[1] "list_1" "list_2"
names(list_of_lists[[1]])
[1] "nombre" "edad"  
list_of_lists[[1]][[1]]
[1] "Javi"

¡Se nos permite almacenar datos n-dimensionales!

Sin embargo una de las desventajas es no puede ser vectorizada inmediatamente, por lo que cualquier operación aritmética aplicada a una lista dará error. Por ejemplo, en el código anterior, uno esperaría que hiciese la operación en cada elemento de la lista (en a y en b) pero no funciona.

data <- list("a" = 1:5, "b" = 10:20)
data / 2
Error in data/2: non-numeric argument to binary operator

Para ello una de las opciones habituales (de R base) era hacer uso de la familia lapply(lista, FUN = ...): aplica una función FUN = ... a cada elemento de la lista

lapply(data, FUN = function(x) { x / 2})
$a
[1] 0.5 1.0 1.5 2.0 2.5

$b
 [1]  5.0  5.5  6.0  6.5  7.0  7.5  8.0  8.5  9.0  9.5 10.0

Pero por defecto la salida de lapply() es siempre una lista de igual longitud.

7.1 Paquete purrr

Una opción más flexible y versátil es hacer uso del paquete {purrr} del entorno {tidyverse}.

library(purrr)

Este paquete pretende imitar la programación funcional de otros lenguajes como Scala o la estrategia map-reduce de Hadoop (de Google).

7.1.1 map()

La función más sencilla del paquete {purrr} es la función map(), que aplica una función vectorizada a cada uno de los elementos de una lista. Veamos un primer ejemplo aplicado a vectores: imagina que tenemos la siguiente lista y queremos aplicar, a cada uno de sus elementos, la suma.

x <- list("x1" = 1:4, "x2" = 11:20)

map(lista, función) nos permite mapear la lista y aplicar la función que queramos elemento a elemento

map(x, sum) 
$x1
[1] 10

$x2
[1] 155

Ten cuidado…

La salida por defecto de map es a su vez otra lista.

Veamos otro ejemplo que puedes sernos útil: imagina que tenemos dos muestras aleatorias de distinto tamaño. La única forma en la que podemos almacenarlas juntas es con una lista pero…¿cómo hacer la media a ambas distribuciones sino podemos vectorizarlas?

x <- list(rnorm(n = 1500, mean = 0, sd = 0.7),
          rnorm(n = 2800, mean = 2, sd = 1.5))

Con map() y la función mean() dentro

map(x, mean)
[[1]]
[1] -0.02559818

[[2]]
[1] 1.999454

¿Y si quisiéramos calcular la media de sus valores al cuadrado? En este caso no disponemos de una función ya definida así que tenemos dos opciones: definirla antes (con un nombre) o bien definir la función dentro del propio map

map(x, function(x) { mean(x^2) })
[[1]]
[1] 0.4670886

[[2]]
[1] 6.294425

Una forma de definir la función más rápido es así:

map(x, \(x) mean(x^2))
[[1]]
[1] 0.4670886

[[2]]
[1] 6.294425

7.1.2 map_xxx()

Además de ser más legible y eficiente, con {purrr} podemos decidir el formato de salida tras la operación

  • salida como un vector numérico (decimales) con map_dbl()
  • salida como un vector numérico (enteros) con map_int()
  • salida como un vector de caracteres con map_chr()
  • salida como un vector numérico lógico con map_lgl()
library(glue)
map_dbl(x, mean)
[1] -0.02559818  1.99945417
map_chr(x, function(x) { glue("Mean is {round(mean(x), 3)}") })
[1] "Mean is -0.026" "Mean is 1.999" 
map_lgl(x, function(x) { all(x < 3)})
[1]  TRUE FALSE

También puede sernos útil para acceder a un elemento de CADA lista si le pasas un número en lugar de una función.

c(x[[1]][3], x[[2]][3])
[1] 0.3515323 3.5592939
map_dbl(x, 3)
[1] 0.3515323 3.5592939

7.1.3 pluck()

También podemos hacer uso de pluck() para acceder al i-th objeto de la lista.

lista <- list("a" = starwars, "b" = billboard)
lista |> pluck(1)
# A tibble: 87 × 14
   name     height  mass hair_color skin_color eye_color birth_year sex   gender
   <chr>     <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr> 
 1 Luke Sk…    172    77 blond      fair       blue            19   male  mascu…
 2 C-3PO       167    75 <NA>       gold       yellow         112   none  mascu…
 3 R2-D2        96    32 <NA>       white, bl… red             33   none  mascu…
 4 Darth V…    202   136 none       white      yellow          41.9 male  mascu…
 5 Leia Or…    150    49 brown      light      brown           19   fema… femin…
 6 Owen La…    178   120 brown, gr… light      blue            52   male  mascu…
 7 Beru Wh…    165    75 brown      light      blue            47   fema… femin…
 8 R5-D4        97    32 <NA>       white, red red             NA   none  mascu…
 9 Biggs D…    183    84 black      light      brown           24   male  mascu…
10 Obi-Wan…    182    77 auburn, w… fair       blue-gray       57   male  mascu…
# ℹ 77 more rows
# ℹ 5 more variables: homeworld <chr>, species <chr>, films <list>,
#   vehicles <list>, starships <list>

7.1.4 map2()

También tenemos la opción de generalizarlo para poder utilizar funciones que necesiten dos argumentos (operaciones binarias) con map2(). Por ejemplo, supongamos que tenemos las siguientes dos listas, y queremos que suma el vector del primer elemento de ambas entre sí, y lo mismo con el segundo

x <- list("a" = 1:3, "b" = 4:6)
y <- list("a" = c(-1, 4, 0), "b" = c(5, -4, -1))
x$a + y$a
[1] 0 6 3
x$b + x$b
[1]  8 10 12

En map2() introducimos las 2 listas y una función con 2 argumentos

map2(x, y, function(x, y) { x + y})
$a
[1] 0 6 3

$b
[1] 9 1 5

Podemos también obtener la salida en forma de data.frame añadiendo list_rbind() o list_cbind(), que convierte una lista en una tabla.

x <- c("a", "b")
y <- 1:2
map2(x, y, function(x, y) { tibble(x, y) })
[[1]]
# A tibble: 1 × 2
  x         y
  <chr> <int>
1 a         1

[[2]]
# A tibble: 1 × 2
  x         y
  <chr> <int>
1 b         2
map2(x, y, function(x, y) { tibble(x, y) }) |> list_rbind()
# A tibble: 2 × 2
  x         y
  <chr> <int>
1 a         1
2 b         2

7.1.5 pmap()

Podemos generalizarlo aún más con pmap_xxx() que nos permite utilizar múltiples argumentos (múltiples listas para funciónes multivariantes).

x <- list(1, 1, 1)
y <- list(10, 20, 30)
z <- list(100, 200, 300)
pmap_dbl(list(x, y, z), sum)
[1] 111 221 331

7.1.6 walk()

Tenemos otros tipos de iteradores que, aunque asumen entradas, no devuelven nada, como walk() (sólo un argumento de entrada), walk2() (dos argumentos) y pwalk() (múltiples argumentos), todos devuelven algo invisible, sólo llaman a una función por sus efectos secundarios en lugar de por su valor de retorno.

list("a" = 1:3, "b" = 4:6) |>
  map2(list("a" = 11:13, "b" = 14:16),
       function(x, y) { x + y }) |> 
  walk(print)
[1] 12 14 16
[1] 18 20 22

7.2 💻 Tu turno

📝 Define una lista de 4 elementos de distintos tipos y accede al segundo de ellos (incluiré uno que sea un tibble para que veas que en una lista cabe de todo).

Code
list_example <-
  list("name" = "Javier", "cp" = 28019,
       "siblings" = TRUE,
       "marks" = tibble("maths" = c(7.5, 8, 9),
                        "lang" = c(10, 5, 6)))
list_example

📝 From the list above, access the elements that occupy places 1 and 4 of the list defined above.

Code
list_example[c(1, 4)]

list_example$name
list_example$marks

list_example[c("name", "marks")]

📝 Load the starwars dataset from the {dplyr} package and access the second movie that appears in starwars$films (for each character). Determine which ones do not appear in more than one movie.

second_film <- map(starwars$films, 2)
map_lgl(second_film, is.null)
 [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE  TRUE FALSE FALSE FALSE
[13] FALSE FALSE  TRUE FALSE FALSE  TRUE FALSE FALSE FALSE  TRUE  TRUE FALSE
[25]  TRUE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE FALSE  TRUE FALSE FALSE  TRUE
[37]  TRUE  TRUE FALSE  TRUE  TRUE FALSE  TRUE  TRUE FALSE  TRUE  TRUE  TRUE
[49]  TRUE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE FALSE  TRUE  TRUE
[61]  TRUE FALSE FALSE  TRUE  TRUE FALSE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE
[73]  TRUE FALSE  TRUE  TRUE FALSE  TRUE  TRUE FALSE FALSE  TRUE  TRUE  TRUE
[85]  TRUE  TRUE  TRUE

📝 Convierte a tibble la base de datos iris incluida en R base (no necesitas ningún paquete, ya la tienes). Vamos a agrupar por species para después usar group_split(): nos genera una lista tal que cada elemento es la tabla para cada uno de los grupos (como un filter de cada grupo pero en un mismo objeto).

iris_list <-
  iris |> as_tibble() |>
  group_by(Species) |> 
  group_split()

Accede a cada variable Sepal.Length para cada una de las subtablas y haz la media (nos tendría que salir un vector de medias)

Code
iris_list |> 
  map("Sepal.Length") |> 
  map_dbl(mean)

8 🐣 Caso práctico IV: encuesta de satisfacción

Para este ejercicio usaremos la tabla de datos de satisfacción de pacientes en un hospital guardada en el archivo .csv llamada SatisfaccionPacientes.csv

library(tidyverse)
datos <-
  read_csv("./datos/SatisfaccionPacientes.csv") |> 
  # normalización de nombres de variables
  janitor::clean_names()
Rows: 100 Columns: 8
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (3): Genero, EstadoCivil, EstadoSalud
dbl (5): ID, Edad, TiempoEspera, GradoSatisfaccion, NumeroVisitas

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
datos
# A tibble: 100 × 8
      id  edad genero    estado_civil tiempo_espera grado_satisfaccion
   <dbl> <dbl> <chr>     <chr>                <dbl>              <dbl>
 1     1    60 Masculino Casado                  28                  8
 2     2    44 Femenino  Soltero                 22                  8
 3     3    43 Masculino Soltero                  8                  9
 4     4    32 Masculino Soltero                 21                  8
 5     5    66 Masculino Divorciado               7                 10
 6     6    43 Masculino Divorciado              20                  8
 7     7    54 Masculino Casado                  18                  6
 8     8    55 Masculino Soltero                 29                  6
 9     9    56 Masculino Viudo                   17                  9
10    10    34 Femenino  Casado                  34                  8
# ℹ 90 more rows
# ℹ 2 more variables: numero_visitas <dbl>, estado_salud <chr>

8.1 Pregunta 1

Aplica el código que sea necesario para determinar el tamaño muestral, el número de variables y el tipo de estas.

Code
# Tamaño muestral / número de observaciones
n <- datos |> nrow()

# Número de variables
p <- datos |> ncol()

# Tipo de variables
dplyr::glimpse(datos)
Rows: 100
Columns: 8
$ id                 <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, …
$ edad               <dbl> 60, 44, 43, 32, 66, 43, 54, 55, 56, 34, 56, 57, 38,…
$ genero             <chr> "Masculino", "Femenino", "Masculino", "Masculino", …
$ estado_civil       <chr> "Casado", "Soltero", "Soltero", "Soltero", "Divorci…
$ tiempo_espera      <dbl> 28, 22, 8, 21, 7, 20, 18, 29, 17, 34, 20, 29, 2, 31…
$ grado_satisfaccion <dbl> 8, 8, 9, 8, 10, 8, 6, 6, 9, 8, 9, 8, 8, 8, 9, 8, 10…
$ numero_visitas     <dbl> 10, 1, 2, 8, 3, 10, 7, 1, 2, 3, 3, 6, 10, 5, 4, 2, …
$ estado_salud       <chr> "Regular", "Bueno", "Bueno", "Bueno", "Malo", "Buen…

8.2 Pregunta 2

Usando tidyverse, obtén la tabla de frecuencias absolutas de la variable genero.

Code
tabla_freq <-
  datos |> 
  count(genero)

8.3 Pregunta 3

Incorpora a la tabla anterior las frecuencias relativas. ¿Podríamos calcular las frecuencias acumuladas? ¿Por qué?

Code
tabla_freq <-
  tabla_freq |> 
  rename(frecuencia_abs = n) |> 
  mutate(frecuencia_rel = frecuencia_abs/sum(frecuencia_abs))

# No podemos acumuladas ya que necesitamos una jerarquía de orden
# son cualis nominales, no ordinales

Haciendo uso de la tabla, ¿qué % de pacientes son mujeres?

Haz lo mismo tu solo con la variable estado_civil y determina el número de personas casadas (deberían ser 26).

8.4 Pregunta 4

En R las variables cualitativas pueden ser tratadas como tales convirtiendo una cadena de texto a lo que se conoce como factor. Por ejemplo, supongamos que tenemos un vector de notas

notas <- c("suspenso", "notable", "suspenso", "aprobado", "notable", "suspenso")
notas
[1] "suspenso" "notable"  "suspenso" "aprobado" "notable"  "suspenso"

Para convertir a factor nos basta con factor(). ¿Qué notas diferente?

notas_fct <- factor(notas)
notas_fct
[1] suspenso notable  suspenso aprobado notable  suspenso
Levels: aprobado notable suspenso

Si te fijas ahora tenemos disponibles unos niveles (levels): son las posibles modalidades de nuestra variable cualitativa, el soporte, de manera que aunque borremos uno de ellos (vamos a borrar todos los aprobados), la opción sigue disponible si entrase un dato nuevo (algo así como un menú de opciones permitidas)

notas_fct[notas_fct != "aprobado"]
[1] suspenso notable  suspenso notable  suspenso
Levels: aprobado notable suspenso

En el caso de las cualitativas ordinales podemos incluso establecer una jerarquía, indicando explícitamente los niveles y ordered = TRUE

notas_fct_ord <- factor(notas, levels = c("aprobado", "notable", "suspenso"),
                        ordered = TRUE)
notas_fct_ord
[1] suspenso notable  suspenso aprobado notable  suspenso
Levels: aprobado < notable < suspenso

Fíjate que ahora tenemos una jerarquía y aunque sea cualitativa podemos buscar elementos <= o >= que otros

notas_fct_ord[notas_fct_ord <= "notable"]
[1] notable  aprobado notable 
Levels: aprobado < notable < suspenso

Haz que tu variable estado_salud sea una factor representando una cualitativa ordinal

Code
datos <-
  datos |>
  mutate(estado_salud =
           factor(estado_salud, levels = c("Malo", "Regular", "Bueno", "Excelente"),
                  ordered = TRUE))

8.5 Pregunta 5

Calcula la tabla de frecuencias para estado_salud incluyendo todo lo que puedas (¿se puede ahora calcular las frecuencias acumuladas). ¿Cuántos pacientes están en un estado de salud regular o peor? (pisa: 59 personas).

Code
tabla_freq <- 
  datos |> 
  count(estado_salud) |> 
  rename(frecuencia_abs = n) |> 
  mutate(frecuencia_rel = frecuencia_abs/sum(frecuencia_abs),
         frecuencia_acum_abs = cumsum(frecuencia_abs),
         frecuencia_acum_rel = cumsum(frecuencia_rel))

datos |>
  count(estado_salud <= "Regular")

8.6 Pregunta 6

Calcula la media, mediana, cuartiles y desviación típica de edad y tiempo de espera. Guarda los resultados en resumen y expórtalo a un resumen.csv

Code
resumen <-
  datos |>
  summarise(media_edad = mean(edad), sd_edad = sd(edad),
            mediana_edad = median(edad),
            Q1_edad = quantile(edad, probs = 0.25),
            Q3_edad = quantile(edad, probs = 0.75),
            # tiempo espera
            media_tiempo_espera = mean(tiempo_espera), 
            sd_tiempo_espera = sd(tiempo_espera),
            mediana_tiempo_espera = median(tiempo_espera),
            Q1_tiempo_espera = quantile(tiempo_espera, probs = 0.25),
            Q3_tiempo_espera = quantile(tiempo_espera, probs = 0.75))
write_csv(resumen, file = "./datos/resumen.csv")

8.7 Pregunta 7

Haz una tabla de frecuencias (absolutas) cruzada entre genero y estado_salud con tidyverse para que quede como la tabla inferior

Code
# Primero cuento de manera bidimensional con count(var1, var2)
conteo_bidim <-
  datos |>
  count(genero, estado_salud)
conteo_bidim
# A tibble: 7 × 3
  genero    estado_salud     n
  <chr>     <ord>        <int>
1 Femenino  Malo             5
2 Femenino  Regular         24
3 Femenino  Bueno           24
4 Masculino Malo            10
5 Masculino Regular         20
6 Masculino Bueno           16
7 Masculino Excelente        1

8.8 Pregunta 8

Haciendo uso de la tabla anterior, ¿qué debes hacer para obtener una tabla de frecuencias bidimensional como la de abajo? (la tabla de frecuencias en formato habitual)

Code
# Después pivoto para que use la columna estado_salud
# como futuros nombres de variables (pivota de vertical a horizontal)
#   - names_from: de donde saldrán los futuros nombres de columnas
#   - values_from: de donde sacamos los valores numéricos (en este caso n)
#     para rellenar la tabla
tabla_freq_abs <-
  conteo_bidim |> pivot_wider(names_from = estado_salud, values_from = n)
tabla_freq_abs
# A tibble: 2 × 5
  genero     Malo Regular Bueno Excelente
  <chr>     <int>   <int> <int>     <int>
1 Femenino      5      24    24        NA
2 Masculino    10      20    16         1

8.9 Pregunta 9

También se puede hacer en R base con table() (que además de ser más sencillo respeta la jerarquía de la ordinal). Moraleja: a veces R base nos facilita la vida, no lo olvidemos porque lo vas a necesitar

tabla_freq <- table(datos$genero, datos$estado_salud)
tabla_freq
           
            Malo Regular Bueno Excelente
  Femenino     5      24    24         0
  Masculino   10      20    16         1

La tabla anterior se puede calcular con frecuencias relativa por filas y por columnas (es decir,una que toda las filas sumen el total, 1, y otra que las columnas sumen 1) haciendo uso de prop.table() aplicada a la tabla anterior (si margin = 1 normaliza por filas, si margin = 2 por columnas).

Code
prop.table(tabla_freq, margin = 1)
           
                  Malo    Regular      Bueno  Excelente
  Femenino  0.09433962 0.45283019 0.45283019 0.00000000
  Masculino 0.21276596 0.42553191 0.34042553 0.02127660
Code
prop.table(tabla_freq, margin = 2)
           
                 Malo   Regular     Bueno Excelente
  Femenino  0.3333333 0.5454545 0.6000000 0.0000000
  Masculino 0.6666667 0.4545455 0.4000000 1.0000000

Moraleja: tablas de frecuencia podemos hacerlas en tidyverse pero bidimensionales R base nos ayuda mejor. Deberemos viajar entre los dos mundos muchas veces

Haciendo uso de las tablas anteriores contesta a las siguientes preguntas:

  • ¿Qué porcentaje de entre las mujeres tiene un buen estado de salud?

  • ¿Qué porcentaje de entre los hombres tiene un estado de salud regular?

  • ¿Qué porcentaje de los que tienen estado de salud malo son mujeres?

Clicka debajo para ver la respuesta cuando lo tengas

Code
# De entre las mujeres un 45.28% tiene un buen estado de salud
# De entre los hombres un 34.04% tiene un estado de salud regular
# Un 33.33% de los que tienen un estado de salud malo, son mujeres

8.10 Pregunta 10

Un poquito de reminder de inferencia. Haciendo uso de la tabla de frecuencias absolutas, ejecuta el código que consideres para responder a la pregunta: ¿están estas dos variables (estado_salud y genero) asociadas? ¿Existe algún tipo de dependencia entre ellas? Hazlo considerando \(\alpha = 0.05\).

Code
# Opción 1: prueba de chi-cuadrado que nos permite sacar conclusiones
# sobre la independencia de dos variables cualitativas

# chisq.test() hace el contraste haciendo uso de la tabla de frec
chisq.test(tabla_freq)

# Como el p-value = 0.2322 y alpha = 5%, no podemos rechazar la
# hipótesis nula de independencia: no hay evidencias suficientes 
# CON LA MUESTRA QUE TENEMOS para concluir que haya alguna asociación
# entre género y estado de salud

# Opción 2: test exacto de Fisher, especialmente útil cuando
# las frecuencias esperadas son bajas.
fisher.test(tabla_freq)
# Diferente p-valor pero misma conclusión

Las funciones chisq.test() y fisher.test() pueden tomar como argumento una tabla de frecuencias ya resumida o puede tomar [dos variables y la función ya realiza el conteo]{.hl-yellow. Por ejemplo si hacemos chisq.test(var1, var2) obtenemos un objeto htest que dentro contiene el p-valor.

test_chisq <- chisq.test(datos$genero, datos$estado_salud)
Warning in chisq.test(datos$genero, datos$estado_salud): Chi-squared
approximation may be incorrect
names(test_chisq)
[1] "statistic" "parameter" "p.value"   "method"    "data.name" "observed" 
[7] "expected"  "residuals" "stdres"   

Por tanto si aplicamos la función y hacemos después $p.value podemos obtener directamente el valor numérico que nos interesa

chisq.test(datos$genero, datos$estado_salud)$p.value
Warning in chisq.test(datos$genero, datos$estado_salud): Chi-squared
approximation may be incorrect
[1] 0.2322175

8.11 Pregunta 11

¿Cómo usar tidyverse para tener en una tabla resumen ambos p-valores?

Code
tabla_p_valores <-
  datos |> 
  summarise("sig_chisq" = chisq.test(genero, estado_salud)$p.value,
            "sig_fisher" = fisher.test(genero, estado_salud)$p.value)
Warning: There was 1 warning in `summarise()`.
ℹ In argument: `sig_chisq = chisq.test(genero, estado_salud)$p.value`.
Caused by warning in `chisq.test()`:
! Chi-squared approximation may be incorrect

8.12 Pregunta 12

Cálcula la matriz correlaciones de Pearson entre las variables numéricas. ¿Existe dependencia LINEAL entre la variable edad y el tiempo de espera? (recuerda: correlación de Pearson solo mide asociación lineal). Echa un vistazo al paquete {corrr}

Code
# Opción 1
mat_cor <-
  datos |> 
  select(where(is.numeric)) |> 
  cor()

# Opción 2
mat_cor <-
  datos |> 
  select(where(is.numeric)) |> 
  corrr::correlate()
Correlation computed with
• Method: 'pearson'
• Missing treated using: 'pairwise.complete.obs'
Code
# La correlación es de 0.0669 por lo que no parezca exista relación

8.13 Pregunta 13

Calcula en una tabla resumen la correlación y el p-valor derivado de un test de correlaciones (cor.test()) entre ambas variables

Code
cor_summary <-
  datos |> 
  summarise("cor" = cor(edad, tiempo_espera),
            "sig_cor" = cor.test(edad, tiempo_espera)$p.value)
# No parece existir evidencia significativa de dependencia lineal

8.14 Pregunta 14

Repite todo lo anterior con las variables tiempo de espera y grado de satisfacción

Code
mat_cor <- 
  datos |> 
  select(where(is.numeric)) |> 
  corrr::correlate()
Correlation computed with
• Method: 'pearson'
• Missing treated using: 'pairwise.complete.obs'
Code
cor_summary <-
  datos |> 
  summarise("cor" = cor(grado_satisfaccion, tiempo_espera),
            "sig_cor" = cor.test(grado_satisfaccion, tiempo_espera)$p.value)
# Sí parece existir evidencia significativa de dependencia lineal
# concretamente negativa: a más espera, menor satisfacción