Tracks
/
R
R
/
Temario
/
Dataframes
Da

Dataframes en R

1 ejercicio

Acerca de Dataframes

En otras partes del temario hemos visto varios tipos de datos con características distintas.

  • Los vectores atómicos son unidimensionales y homogéneos en cuanto a tipo.
  • Las listas son unidimensionales y sus elementos pueden ser de tipos heterogéneos.
  • Las matrices y arrays son multidimensionales y homogéneos.

Este concepto verá formas de almacenar datos multidimensionales y heterogéneos. En la práctica, la mayoría de los datos del mundo real son así, de modo que ahora llegamos al meollo de cómo se usa R (en su mayoría) en la práctica.

Variantes de dataframe

A lo largo de las décadas, R ha ido agregando múltiples tipos de datos para manejar datos tabulares.

Este temario se centrará principalmente en los tibbles, pero es útil conocer algunas alternativas.

El data.table

Los tibbles (que describimos más abajo) y los data.tables son dos intentos muy respetados de mejorar el data.frame tradicional de R base. Inevitablemente, hay mucha discusión sobre cuál es «mejor». Quizás haya cierto grado de consenso en torno a los siguientes puntos (aunque se los tache de simplistas).

  • tibble está optimizado principalmente para la facilidad de uso y la integración con el ecosistema del Tidyverse.
  • data.table está optimizado principalmente para la potencia bruta y la escalabilidad, sobre todo cuando se trabaja con conjuntos de datos muy grandes.

En cualquier caso, data.table no está disponible dentro de Exercism, así que se menciona aquí solo por completitud.

El data.frame

En R base, un data.frame es una list de vectors de igual longitud. Esto se puede pensar como una tabla rectangular de datos, en la que cada columna es homogénea, pero cada fila puede (y suele) contener tipos de datos distintos.

Un ejemplo para ilustrarlo:

# create the column vectors
languages <- c("Fortran", "R", "Python", "Julia")
created <- c(1957, 1993, 1991, 2012)
has.syllabus <- c(FALSE, TRUE, TRUE, TRUE)

# join columns to create the dataframe
df <- data.frame(languages, created, has.syllabus)
df
#>   languages created has.syllabus
#> 1   Fortran    1957        FALSE
#> 2         R    1993         TRUE
#> 3    Python    1991         TRUE
#> 4     Julia    2012         TRUE

# look at the structure
str(df)
#> 'data.frame':	4 obs. of  3 variables:
 #> $ languages   : chr  "Fortran" "R" "Python" "Julia"
 #> $ created     : num  1957 1993 1991 2012
 #> $ has.syllabus: logi  FALSE TRUE TRUE TRUE

Tenemos una columna de cadenas de caracteres, una columna de números y una columna de booleanos. Cuando se escala, esta es una forma intuitiva de representar muchas colecciones de datos del mundo real.

El tibble

El diseño de data.frame es antiguo.

La experiencia de varias décadas, junto con los patrones cambiantes de cómo se usa R, llevaron a un rediseño que creó una alternativa modernizada en el Tidyverse: los tibbles.

En comparación con R base, los tibbles tienen:

  • Valores predeterminados distintos, para reducir problemas comunes.
  • Menos propensión a forzar la conversión de tipos de datos durante la importación.
  • Mensajes de error más numerosos y más claros.
  • Formatos de visualización distintos y, por lo general, mejores.

En resumen, un tibble busca «hacer menos y quejarse más», y también se lo describe como «perezoso y malhumorado».

Los tipos suelen ser intercambiables: cualquier función que acepte un data.frame también aceptará un tibble, y viceversa.

Las conversiones entre los tipos son fáciles, con as_tibble(df) y as.data.frame(tbl).

Para trabajo nuevo, usar tibbles probablemente te ayude a crear código más robusto. Sin embargo, el código y los datos heredados abundan muchísimo en el mundo de R, así que es probable que el data.frame siga siendo común durante mucho tiempo.

# column vectors are same as for data.frame
library(tibble)
tbl <- tibble(languages, created, has.syllabus)
tbl
  # A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE       
#> 2 R            1993 TRUE        
#> 3 Python       1991 TRUE        
#> 4 Julia        2012 TRUE      
  
str(tbl)
#> tibble [4 × 3] (S3: tbl_df/tbl/data.frame)
#>  $ languages   : chr [1:4] "Fortran" "R" "Python" "Julia"
#>  $ created     : num [1:4] 1957 1993 1991 2012
#>  $ has.syllabus: logi [1:4] FALSE TRUE TRUE TRUE

Fíjate en el formato de impresión predeterminado: la línea de comentario con las dimensiones se imprime automáticamente, y también se muestran los tipos de las columnas.

Trabajar con tibbles

Los tibbles son una parte central del Tidyverse, así que agrégalos con library(tibble) o con library(tidyverse).

La documentación es bastante extensa, al estilo del Tidyverse:

Crear un tibble

De la forma más simple, podemos usar la función tibble() para unir vectores columna, igual que con data.frame(). En una sección anterior se mostró un ejemplo de esto.

Si te resulta más cómodo ingresar los valores por fila, la función correspondiente es tribble().

tbl_r <- tribble(
  # column names marked with tilde prefix
  ~languages, ~created, ~has.syllabus,
  "Fortran", 1957, FALSE,
  "R", 1993, TRUE,
  "Python", 1991, TRUE,
  "Julia", 2012, TRUE
)

tbl_r
# A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE       
#> 2 R            1993 TRUE        
#> 3 Python       1991 TRUE        
#> 4 Julia        2012 TRUE    

En la práctica, hay decenas de formas de crear tibbles, ya que son el formato de salida predeterminado de una amplia variedad de funciones del Tidyverse. Volveremos a esto en un concepto futuro.

Manipular un tibble

El concepto de programación funcional presentó la biblioteca purrr para manipular vectores y listas (estructuras de datos unidimensionales).

Para los dataframes (ya sean tradicionales o tibbles), la biblioteca que corresponde usar es dplyr.

Ya presentamos dplyr antes, en el concepto de switch. Ahí solo se usaron unas cuantas funciones de utilidad, pero ahora podemos empezar a explorar el resto de esta gran biblioteca.

Subconjuntos

Los dataframes, incluidos los tibbles, se pueden tratar como listas de vectores columna, así que el indexado de listas recupera una columna determinada.

tbl
# A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE
#> 2 R            1993 TRUE        
#> 3 Python       1991 TRUE        
#> 4 Julia        2012 TRUE    

tbl$created
#> [1] 1957 1993 1991 2012

Un dataframe también se puede indexar con un indexado al estilo de las matrices.

tbl[c(2, 4), 1:2]
  # A tibble: 2 × 2
#>   languages created
#>   <chr>       <dbl>
#> 1 R            1993
#> 2 Julia        2012

En el R moderno con el ecosistema del Tidyverse, las funciones de dplyr suelen ser más flexibles y convenientes, y serán el centro del resto de este concepto.

Operaciones por columna

Obtén una sola columna con pull() usando el nombre o el número secuencial (los números negativos cuentan de derecha a izquierda).

tbl |> pull(created)
#> [1] 1957 1993 1991 2012

Este es el mismo resultado que tbl$created, pero usando una función que se integra bien en un pipeline.

Para obtener varias columnas, la función adecuada es select(), que es muy versátil. Obtén (o descarta) columnas según las propiedades de su nombre o su tipo.

# Range with position and/or name
tbl |> select(1:created)
  # A tibble: 4 × 2
#>   languages created
#>   <chr>       <dbl>
#> 1 Fortran      1957
#> 2 R            1993
#> 3 Python       1991
#> 4 Julia        2012

# Exclude a column
tbl |> select(!created)
  # A tibble: 4 × 2
#>   languages has.syllabus
#>   <chr>     <lgl>       
#> 1 Fortran   FALSE       
#> 2 R         TRUE        
#> 3 Python    TRUE        
#> 4 Julia     TRUE        

# Use type of column
tbl |> select(where(is.numeric))
  # A tibble: 4 × 1
#>   created
#>     <dbl>
#> 1    1957
#> 2    1993
#> 3    1991
#> 4    2012

Se permiten varios criterios, usando los operadores booleanos &, | y ! (y, o, no).

Los nombres de columna que son identificadores válidos de R no necesitan comillas dentro de un select(). Los nombres no válidos (por ejemplo, los que incluyen espacios) se pueden encerrar entre comillas invertidas, aunque quizá sea mejor renombrarlos.

La función select() puede trabajar con una serie de funciones auxiliares para elegir nombres de columna: starts_with, contains, num_range y varias otras. matches permite coincidencias completas con expresiones regulares. Consulta la documentación para más detalles.

Tanto poder parece un poco absurdo con nuestro dataframe de juguete de lenguajes. Por suerte, el tibble starwars viene incluido en dplyr, lo que nos da algo más grande con lo que practicar.

# limit display to top 3 rows of non-list columns
starwars |> 
  select(!where(is.list)) |> 
  head(3)
  # A tibble: 3 × 11
#>   name           height  mass hair_color skin_color  eye_color birth_year sex   gender    homeworld species
#>   <chr>           <int> <dbl> <chr>      <chr>       <chr>          <dbl> <chr> <chr>     <chr>     <chr>  
#> 1 Luke Skywalker    172    77 blond      fair        blue              19 male  masculine Tatooine  Human  
#> 2 C-3PO             167    75 NA         gold        yellow           112 none  masculine Tatooine  Droid  
#> 3 R2-D2              96    32 NA         white, blue red               33 none  masculine Naboo     Droid  

# pick a subset of columns
starwars |> 
  select(name | ends_with("color")) |> 
  head(5)
  # A tibble: 5 × 4
#>   name           hair_color skin_color  eye_color
#>   <chr>          <chr>      <chr>       <chr>    
#> 1 Luke Skywalker blond      fair        blue     
#> 2 C-3PO          NA         gold        yellow   
#> 3 R2-D2          NA         white, blue red      
#> 4 Darth Vader    none       white       yellow   
#> 5 Leia Organa    brown      light       brown    

Operaciones por fila

Note

Claramente, dplyr ofrece formas poderosas de seleccionar columnas por nombre.

¿Podemos hacer algo similar con los nombres de fila?

¡No! Los dataframes tradicionales de R pueden tener nombres de fila, pero (tras una historia de errores y problemas de rendimiento) los nombres de fila no están permitidos en los tibbles.

Si quieres nombres, ponlos en una columna <chr> (normalmente la columna 1) y úsala como cualquier otra columna. Las funciones de importación como as_tibble() la crearán automáticamente al importar datos con filas con nombre.

Si esta limitación de los nombres de fila te parece extrañamente restrictiva, recuerda que la mayoría de los grandes sistemas de bases de datos manejan las tablas de la misma manera: Oracle, SQL Server, PostgreSQL, MySQL...

Obtén las filas que cumplan algún criterio con filter(), o exclúyelas con filter_out().

starwars |> 
  select(name:mass) |> 
  filter(between(height, 150, 165) & !is.na(mass))
  # A tibble: 4 × 3
#>   name               height  mass
#>   <chr>               <int> <dbl>
#> 1 Leia Organa           150    49
#> 2 Beru Whitesun Lars    165    75
#> 3 Nien Nunb             160    68
#> 4 Ben Quadinaros        163    65

Los criterios de filtrado pueden ser arbitrariamente complejos, pero siempre se basan en el contenido de las filas.

Si se conocen los números de fila, podemos usar una variedad de funciones slice().

starwars |> 
  select(name | homeworld) |> 
  slice(20:25)
  # A tibble: 6 × 2
#>   name             homeworld
#>   <chr>            <chr>    
#> 1 Palpatine        Naboo    
#> 2 Boba Fett        Kamino   
#> 3 IG-88            NA       
#> 4 Bossk            Trandosha
#> 5 Lando Calrissian Socorro  
#> 6 Lobot            Bespin   

# random sample of rows
starwars |> 
  select(name | homeworld) |> 
  slice_sample(n = 4)
  # A tibble: 4 × 2
#>   name            homeworld
#>   <chr>           <chr>    
#> 1 Shaak Ti        Shili    
#> 2 Luminara Unduli Mirial   
#> 3 Grievous        Kalee    
#> 4 Palpatine       Naboo    

Para eliminar filas duplicadas, usa distinct().

Modificar un tibble

Primera advertencia: el comportamiento predeterminado de copiar al modificar significa que el tibble original normalmente permanece sin cambios.

La mayoría de las modificaciones se aplican por columna.

Los nombres de columna se pueden cambiar con rename(newname = oldname), o con rename_with() para aplicar una función. Un uso típico sería limpiar los nombres importados para que sea más fácil trabajar con ellos en R, eliminando los espacios en blanco y forzando un formato consistente para nombres relacionados.

Fíjate en la sintaxis dentro de rename(). El contenido de la columna oldname se vincula al nombre newname, de ahí el orden.

El orden de las columnas se puede cambiar con relocate(). Las columnas especificadas se mueven a las posiciones más a la izquierda de forma predeterminada, pero se puede usar un argumento .before o .after para un posicionamiento más preciso.

sw <- starwars |> select(name:species) |> slice(1:4)
sw
  # A tibble: 4 × 11
#>   name           height  mass hair_color skin_color  eye_color birth_year sex   gender    homeworld species
#>   <chr>           <int> <dbl> <chr>      <chr>       <chr>          <dbl> <chr> <chr>     <chr>     <chr>  
#> 1 Luke Skywalker    172    77 blond      fair        blue            19   male  masculine Tatooine  Human  
#> 2 C-3PO             167    75 NA         gold        yellow         112   none  masculine Tatooine  Droid  
#> 3 R2-D2              96    32 NA         white, blue red             33   none  masculine Naboo     Droid  
#> 4 Darth Vader       202   136 none       white       yellow          41.9 male  masculine Tatooine  Human  

sw |> relocate(c(species, homeworld), .after = name)
  # A tibble: 4 × 11
#>   name           species homeworld height  mass hair_color skin_color  eye_color birth_year sex   gender   
#>   <chr>          <chr>   <chr>      <int> <dbl> <chr>      <chr>       <chr>          <dbl> <chr> <chr>    
#> 1 Luke Skywalker Human   Tatooine     172    77 blond      fair        blue            19   male  masculine
#> 2 C-3PO          Droid   Tatooine     167    75 NA         gold        yellow         112   none  masculine
#> 3 R2-D2          Droid   Naboo         96    32 NA         white, blue red             33   none  masculine
#> 4 Darth Vader    Human   Tatooine     202   136 none       white       yellow          41.9 male  masculine

Para cambios más grandes, mutate() te permite:

  • Crear nuevas columnas que son funciones de columnas existentes.
  • Reemplazar una columna existente, creando una nueva columna con el mismo nombre.
  • Eliminar una columna, asignando su valor a NULL

Claramente, mutate() es potente, potencialmente confuso y una razón para estar muy agradecidos por la copia al modificar.

No hay una razón evidente para interesarse por el índice de masa corporal de los personajes de Star Wars, pero por si acaso:

starwars |> 
  select(c(name, species, height, mass)) |> 
  mutate(BMI = mass / (height / 100)^2) |> 
  head(4)
  # A tibble: 4 × 5
#>   name           species height  mass   BMI
#>   <chr>          <chr>    <int> <dbl> <dbl>
#> 1 Luke Skywalker Human      172    77  26.0
#> 2 C-3PO          Droid      167    75  26.9
#> 3 R2-D2          Droid       96    32  34.7
#> 4 Darth Vader    Human      202   136  33.3

Cuando quieres operar sobre un subconjunto de las columnas con funciones como mutate(), la secuencia select() |> mutate() del ejemplo anterior es una opción. Solo las columnas seleccionadas estarán en el resultado.

Como alternativa, puede ser conveniente usar pick() dentro de la llamada a mutate():

starwars |> mutate(pick(c(name, species, height, mass)), BMI = mass / (height / 100)^2) |> head(4)
When you want to operate on a subset of the columns with functions such as `mutate()`, the `select() |> mutate()` sequence in the above example is one option.
Only the selected columns will be in the result.

Alternatively, it can be convenient to use `pick()` _within_ the `mutate()` call:

```R
starwars |> mutate(pick(c(name, species, height, mass)), BMI = mass / (height / 100)^2) |> head(4)
    # A tibble: 4 × 15
#>   name         height  mass hair_color skin_color eye_color birth_year sex   gender homeworld species films vehicles
#>   <chr>         <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr>  <chr>     <chr>   <lis> <list>  
#> 1 Luke Skywal…    172    77 blond      fair       blue            19   male  mascu… Tatooine  Human   <chr> <chr>   
#> 2 C-3PO           167    75 NA         gold       yellow         112   none  mascu… Tatooine  Droid   <chr> <chr>   
#> 3 R2-D2            96    32 NA         white, bl… red             33   none  mascu… Naboo     Droid   <chr> <chr>   
#> 4 Darth Vader     202   136 none       white      yellow          41.9 male  mascu… Tatooine  Human   <chr> <chr>   
   # ℹ 2 more variables: starships <list>, BMI <dbl>

Solo las columnas seleccionadas con pick se usan en la mutación, pero se devuelven todas las columnas.

Las operaciones por fila son menos comunes para modificar un solo tibble (la combinación de varios tibbles se tratará en un concepto posterior).

Una excepción: arrange() ordena las filas según los valores de una o más columnas.

tbl
  # A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE       
#> 2 R            1993 TRUE        
#> 3 Python       1991 TRUE        
#> 4 Julia        2012 TRUE  

tbl |> arrange(languages)
  # A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE       
#> 2 Julia        2012 TRUE        
#> 3 Python       1991 TRUE        
#> 4 R            1993 TRUE     

Resumen

Los dataframes, ya sean tradicionales o tibbles, son centrales en la forma en que normalmente se usa el R moderno.

La mayoría de las funciones del Tidyverse (no solo dplyr) reciben tibbles como entrada y/o los crean como salida.

Este concepto solo ofreció una breve introducción, apenas arañando la superficie de lo que es posible.

En conceptos posteriores se tratarán varios otros aspectos de los dataframes (dentro de las limitaciones técnicas de Exercism).

Editar en GitHub El enlace se abre en una ventana o pestaña nueva

Aprende Dataframes