En otras partes del temario, hemos visto varios tipos de datos con características diferentes.
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í, por lo que ahora llegamos al núcleo de cómo se usa R (en su mayor parte) en la práctica.
A lo largo de las décadas, R ha añadido múltiples tipos de datos para manejar datos tabulares.
Este temario se centrará principalmente en los tibbles, pero es útil conocer algunas alternativas.
data.table
Los tibbles (descritos más abajo) y los data.tables son dos intentos muy respetados de mejorar el tradicional data.frame del R base.
Inevitablemente hay mucho debate sobre cuál es «mejor».
Quizá exista 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 Tidyverse.data.table está optimizado principalmente para la potencia bruta y la escalabilidad, sobre todo al trabajar 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.
data.frame
En el R base, un data.frame es una list de vectors de igual longitud.
Se puede pensar en él como una tabla rectangular de datos, en la que cada columna es homogénea, pero cada fila puede (y suele) contener distintos tipos de datos.
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 strings, una columna de números y una columna de valores Boolean. Al ampliarlo, esta es una forma intuitiva de representar muchas colecciones de datos del mundo real.
tibble
El diseño de data.frame es antiguo.
La experiencia de varias décadas, junto con los patrones cambiantes de uso de R, llevaron a un rediseño: una alternativa modernizada en el Tidyverse, los tibbles.
En comparación con el R base, los tibbles tienen:
En resumen, un tibble pretende «hacer menos y quejarse más», descrito también como «perezoso y arisco».
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 sencillas, con as_tibble(df) y as.data.frame(tbl).
Para trabajo nuevo, usar tibbles probablemente te ayudará a crear código más robusto.
Sin embargo, el código heredado y los datos heredados son muy abundantes 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 los tipos de columna también se muestran.
Los tibbles son una parte fundamental del Tidyverse, así que añádelos con library(tibble) o library(tidyverse).
La documentación es bastante extensa, al estilo del Tidyverse:
De la forma más sencilla, podemos usar la función tibble() para unir vectores de columnas, igual que con data.frame().
En una sección anterior se mostró un ejemplo de esto.
Si te resulta más cómodo introducir valores por filas, 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 docenas 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.
El concepto de Programación funcional habló de la librería purrr para manipular vectores y listas (estructuras de datos unidimensionales).
Para los dataframes (ya sean tradicionales o tibbles), la librería correspondiente que hay que usar es dplyr.
Ya presentamos dplyr antes, en el concepto Switch.
Allí solo usamos unas pocas funciones de utilidad, pero ahora podemos empezar a explorar el resto de esta gran librería.
Los dataframes, incluidos los tibbles, pueden tratarse como listas de vectores de columnas, así que la indexación de listas recupera una columna especificada.
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
También se puede indexar un dataframe con una indexación 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 Tidyverse, las funciones de dplyr son por lo general más flexibles y cómodas, y serán el centro del resto de este concepto.
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 una secuencia de operaciones.
Para obtener varias columnas, la función adecuada es select(), que es muy versátil.
Obtén (o descarta) columnas basándote en 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 select().
Los nombres no válidos (por ejemplo, los que incluyen espacios) pueden encerrarse entre acentos graves, aunque quizá sea mejor cambiarlos de nombre.
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 RegEx.
Consulta la documentación para más detalles.
Tanto poder parece bastante ridículo con nuestro dataframe de juguete de lenguajes.
Por suerte, el tibble starwars está 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
Claramente, dplyr ofrece formas potentes de seleccionar columnas por su nombre.
¿Podemos hacer algo parecido con los nombres de fila?
¡No!
Los dataframes tradicionales de R pueden tener nombres de fila, pero (tras un historial 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 úsalos como cualquier otra columna.
Las funciones de importación, como as_tibble(), los 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 gestionan 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 tan complejos como quieras, pero siempre se basan en el contenido de las filas.
Si se conocen los números de las filas, podemos usar una serie 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().
Primera advertencia: el comportamiento predeterminado de copia al modificar hace que el tibble original normalmente permanezca sin cambios.
La mayoría de las modificaciones se aplican por columnas.
Los nombres de columna pueden cambiarse 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 espacios en blanco y forzando un formato coherente para los 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 puede cambiarse con relocate().
Las columnas especificadas se mueven a la posición 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:
NULL
Claramente, mutate() es potente, potencialmente confuso, y una razón para estar muy agradecido por la copia al modificar.
No hay ninguna razón evidente para preocuparse 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 quieras 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 resultar cómodo 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 se usan en la mutación las columnas seleccionadas con pick, pero se devuelven todas las columnas.
Las operaciones por filas 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 por 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
Los dataframes, ya sean tradicionales o tibbles, son fundamentales en la forma en que se suele usar el R moderno.
La mayoría de las funciones del Tidyverse (no solo dplyr) toman tibbles como entrada o los crean como salida.
Este concepto solo ofrece una breve introducción, apenas arañando la superficie de lo que es posible.
Conceptos posteriores tratarán otros aspectos de los dataframes (dentro de las limitaciones técnicas de Exercism).