2 Aspectos básicos de R
Fecha de compilación: 2026-10-13
Instalación y carga de librerías
# Se instalan automáticamente si no están disponibles en el sistema
if (!require(fpp3)) install.packages('fpp3')
library(fpp3) # Series temporales: proporciona los tipos especiales yearmonth, yearquarter, yearweek
library(utils) # Utilidades de R base: incluye adist() para calcular distancias entre strings
if (!require(tidyverse)) install.packages('tidyverse')
library(tidyverse) # Ecosistema tidyverse: dplyr, tidyr, stringr, ggplot2, readr, entre otros
source("../utilidades.R") # Funciones auxiliares del cursoLibrerías utilizadas en este capítulo
tidyverse: la herramienta central del capítulo. No es una librería sino un conjunto de ellas, que se cargan de una vez:dplyr(transformación de tablas),tidyr(cambios de formato),stringr(manejo de texto),readr(lectura de ficheros) yggplot2(gráficos), entre otras.fpp3: ecosistema de series temporales. Aquí se usa solo por sus tipos de fecha especiales (yearmonth,yearquarter,yearweek); se retomará a fondo en el capítulo de series temporales.utils: utilidades deRbase. En este capítulo aportaadist(), que mide la distancia entre dos cadenas de texto.
Cómo estudiar este capítulo: núcleo mínimo vs. consulta
Este capítulo es extenso y funciona como referencia. No es necesario memorizarlo entero de una vez. El núcleo mínimo que hay que dominar para seguir el resto del libro es:
- Los tipos de datos básicos y la coerción (
as.Date,as.numeric,as.factor), prestando especial atención a las fechas. - La estructura
tibbley las operaciones básicas sobre filas y columnas. - La definición y uso de funciones y el operador pipe
|>. - La tabla de errores comunes del final del capítulo.
El resto de secciones (manipulación avanzada de strings, familia apply, listas, etc.) puede consultarse cuando se necesite, sin necesidad de estudiarlas linealmente en la primera lectura.
Además, cuando escribas tu primer fichero .Rmd, consulta el Apéndice 1 con la sintaxis básica de RMarkdown. Como referencia rápida de las funciones de R base puede consultarse la ficha resumen de R base.
2.1 Tipos y estructuras de datos
El operador de asignación en R es el símbolo = o, más comúnmente, <-. Por ejemplo, a <- 5 asigna a a el valor 5. Ambos operadores producen el mismo resultado en la mayoría de los casos, aunque no son idénticos: en determinadas situaciones, el uso de uno en lugar del otro puede generar un error.
Error frecuente en principiantes
Si se ejecuta cualquier operación o llamada a una función sin asignar el resultado a una variable, dicho resultado se imprime en la consola pero no se almacena. Un error frecuente en principiantes es creer que se ha actualizado una tabla cuando, en realidad, únicamente se ha impreso el resultado de una operación sobre ella, sin haberla reasignado.
Tipos de datos
| Tipo | Ejemplos |
|---|---|
| character |
|
| factor |
|
| numeric |
|
| integer |
|
| logical |
|
| dato no disponible |
|
| date |
|
Cualquier número escrito directamente en R se guarda como numeric (decimal). El sufijo L de 2024L fuerza que se almacene como entero (integer), que es el tipo que devuelven, por ejemplo, las secuencias creadas con el operador :.
La coerción nos permite cambiar los tipos de los datos. Por ejemplo as.Date("2022-12-31") pasa el campo de texto “2022-12-31” a un dato tipo fecha. Otros ejemplos de funciones de coerción son as.numeric(), as.factor() o as.character(). Cuidado con as_factor() (de forcats, incluida en tidyverse): aunque el nombre se parece, no es equivalente a as.factor(), porque ordena los niveles por orden de aparición en el vector, mientras que as.factor() los ordena alfabéticamente.
Junto a las funciones as.*, que convierten, existen las funciones is.*, que comprueban el tipo de un objeto y devuelven TRUE o FALSE. La función class() va un paso más allá y devuelve directamente el nombre del tipo:
## [1] TRUE
## [1] FALSE
## [1] TRUE
## [1] "character"
## [1] "numeric"
Cuando leemos los datos de una tabla, debemos prestar especial atención a examinar los tipos asignados a los campos y comprobar, en particular, que los campos con números y fechas se han identificado como tales. En caso contrario, lo primero que hay que hacer es convertir los datos a los tipos deseados (especialmente las fechas).
El tipo factor se utiliza para clasificar, y se asocia, a las denominadas variables categóricas. Por ejemplo, en una tabla con datos de población por países, edad y sexo, el valor observado sería el dato de población y el valor de las variables categóricas, país, edad y sexo, determinan/identifican a qué corresponde dicha observación.
Para acelerar su gestión, las variables tipo factor se almacenan internamente como una mezcla entre caracteres y números (los números se comparan entre sí mucho más rápido que los strings), y por tanto no son lo mismo que las variables tipo character y esto puede ocasionar algún problema al comparar ambos tipos de variables.
Los valores posibles de un factor se llaman niveles y se consultan con levels(); la función table() cuenta cuántas veces aparece cada uno, que es la primera pregunta que nos hacemos ante una variable categórica:
## [1] Europa Asia Europa África Asia Europa
## Levels: África Asia Europa
## [1] "África" "Asia" "Europa"
## continentes
## África Asia Europa
## 1 2 3
## [1] 3 2 3 1 2 3
La última línea muestra el almacenamiento interno del que hablábamos: as.numeric() sobre un factor no devuelve los valores originales sino el código de cada nivel (1 para “África”, 2 para “Asia”, 3 para “Europa”). Es una fuente habitual de errores cuando una columna numérica se ha leído por error como factor: para recuperar los números hay que pasar antes por character, con as.numeric(as.character(f)).
Manipulación de strings
Un caso especialmente importante de tipo de datos son los strings de caracteres que con frecuencia tenemos que manipular para obtener los resultados que necesitamos. Por ello vamos a ver a continuación algunos procesos básicos en la manipulación de strings.
Función paste y paste0
La función paste permite combinar texto y variables para construir un string.
# Construimos un mensaje combinando texto literal y el valor de la variable 'a'
a <- 5
paste("la velocidad es",a, "km/h")## [1] "la velocidad es 5 km/h"
La función paste inserta espacios en blanco entre los elementos que combina. paste0 hace lo mismo pero sin insertar espacios.
Extraer un fragmento de un string
s <- "esto es un string"
# Extraemos los caracteres de la posición 3 a la 14 (ambas inclusive)
substr(s,3,14)## [1] "to es un str"
Extraer una palabra de un string
Cuando el string es una frase con palabras separadas por espacios, la función word de la librería stringr permite extraer la palabra que ocupa una posición determinada. Por ejemplo, el siguiente código extrae la tercera palabra del string s definido anteriormente.
## [1] "un"
## [1] "string"
## [1] "es un string"
Distancia entre strings con adist()
Comparación aproximada de strings con adist()
En ocasiones es necesario comparar strings que son similares pero no idénticos. Por ejemplo, puede interesarnos reconocer como equivalentes “Antonio”, “San antonio” y “S. Antonio”. La función adist de la librería utils calcula una distancia entre dos strings: vale cero cuando son idénticos y aumenta progresivamente a medida que difieren. Utilizaremos esta función en secciones posteriores para encontrar, dado un string, el elemento más cercano en un vector de strings.
## [,1]
## [1,] 5
## [,1]
## [1,] 3
## [,1]
## [1,] 4
Manejo de fechas
Las fechas constituyen un tipo de dato de uso constante en el análisis de datos. Es fundamental entender cómo se representan y transforman en R. A continuación se muestran los ejemplos más habituales de manejo de formatos de fecha:
## [1] "2022-12-25"
## [1] "2022-12-25"
## [1] "domingo, diciembre, 25, 22"
## [1] "do., dic., 25, 2022, día dentro de la semana: 7"
## [1] "Nº semana del año: 51, día del año : 359"
Para consultar todos los códigos de formato disponibles, ejecutar en la consola ??strptime y seleccionar la opción base::format.POSIXct.
Algunos programas, como Excel, codifican las fechas como números enteros contados desde una fecha de referencia (“1899-12-30” en el caso de Excel). La función as.Date permite convertir esta codificación numérica al formato fecha estándar:
## [1] "1899-12-30"
## [1] "2023-03-15"
En ocasiones, las fechas asociadas a los datos vienen expresadas por mes —por ejemplo “2008-02”— o por trimestre —por ejemplo “2008 Q2”—. La librería lubridate, incluida en tidyverse, proporciona herramientas específicas para estos casos:
## [1] "2008-02-01"
## [1] "2008-02-29"
## [1] "2008-02-01"
## [1] "2008-02-01"
## [1] "2008-02-01"
## [1] "2008-04-01"
## [1] "2008-06-30"
## [1] "2008-04-01"
Cuando trabajamos con series temporales (datos que varían con el tiempo), es fundamental conocer la frecuencia de observación: diaria, semanal, mensual, trimestral o anual. Si usamos ym() para convertir una fecha mensual, el resultado se trata como una fecha diaria, lo que puede generar errores en el análisis temporal. Para evitarlo, la librería fpp3 incorpora tipos de fecha que conservan la granularidad temporal:
## [1] "2008-02-02"
## <yearmonth[1]>
## [1] "2008 mar."
## [1] "2008-04-02"
## <yearquarter[1]>
## [1] "2008 Q3"
## # Year starts on: January
## <yearweek[1]>
## [1] "2008 W03"
## # Week starts on: Monday
## <yearweek[1]>
## [1] "2009 W01"
## # Week starts on: Monday
yearweek() también se puede aplicar a una fecha diaria para saber a qué semana pertenece. Conviene fijarse en el resultado porque no siempre es el esperado:
## <yearweek[1]>
## [1] "2024 W52"
## # Week starts on: Monday
## <yearweek[1]>
## [1] "2025 W01"
## # Week starts on: Monday
Las semanas empiezan en lunes y se asignan al año en el que cae la mayor parte de sus días. Por eso el año de un yearweek no coincide necesariamente con el año de la fecha de la que procede, algo que hay que tener presente al agrupar datos diarios por semanas en torno al cambio de año.
Para extraer una parte de una fecha, lubridate incluye funciones como year(), que devuelve el año, y quarter(), que devuelve el trimestre del año (de 1 a 4). Funcionan igual sobre una fecha diaria que sobre los tipos yearmonth y yearquarter de fpp3, y son útiles para agrupar los datos por año o para comparar el mismo trimestre de años distintos:
## [1] 2022
## [1] 4
## [1] 2008
## [1] 2
## [1] 4
Vectores
Creación de un vector concatenando valores
## [1] "el segundo elemento de v es : 7"
Un vector solo puede contener datos de un único tipo. Si mezclamos tipos dentro de c(), R no da ningún error: convierte silenciosamente todos los elementos al tipo más general de los presentes.
## [1] "1" "dos" "3"
## [1] "character"
## [1] "numeric"
Es un error frecuente que una columna numérica acabe convertida en texto porque alguna de sus celdas contiene un guion o una nota. En ese caso, al hacer una coerción con as.numeric los elementos que no son números pasan a ser NA.
Creación de un vector usando fechas consecutivas
## [1] "2022-12-31" "2023-01-01" "2023-01-02" "2023-01-03"
Con frecuencia es necesario crear un vector de fechas espaciadas por meses, semanas, etc. Esto se consigue con la función seq:
# Vector con los primeros días de cada mes entre enero y junio de 2023
seq(as.Date("2023-01-01"), as.Date("2023-06-01"), by = "months")## [1] "2023-01-01" "2023-02-01" "2023-03-01" "2023-04-01" "2023-05-01"
## [6] "2023-06-01"
Usando el tipo yearmonth de fpp3, la misma operación resulta más directa y conserva la granularidad mensual:
## <yearmonth[6]>
## [1] "2023 ene." "2023 feb." "2023 mar." "2023 abr." "2023 may." "2023 jun."
Reemplazar elementos de un vector
Vamos a usar la función gsub para reemplazar un texto por otro dentro de cada elemento de un vector de strings. Ojo: gsub busca el patrón en cualquier parte del string (y lo interpreta como una expresión regular), así que gsub('my','your',x) también cambiaría “myth” por “yourth”. La función equivalente en tidyverse es str_replace_all.
## [1] "your book" "your car" "your car"
## [1] "my book" "my tree" "your tree"
Encontrar la posición de un valor en un vector
Vamos a usar la función which para encontrar las posiciones donde aparece un elemento en un vector
x <- c("my book", "my car","your car","my car")
which(x=='my car') # posiciones donde aparece "my car"## [1] 2 4
Si nos interesa la posición de la primera vez que encuentra el elemento haríamos
## [1] 2
Extraer los elementos sin repetir de un vector
Con frecuencia, cuando leemos una tabla con variables categóricas, necesitamos saber los valores posibles de dichas variables (sin repetir). Para ello usamos la función unique que nos devuelve los elementos sin repetir de un vector:
## [1] "rojo" "azul"
Operaciones de conjuntos entre dos vectores
Cuando hay que comparar dos vectores como si fueran conjuntos, R ofrece tres funciones muy usadas: union (todos los elementos de los dos, sin repetir), intersect (solo los que están en los dos) y setdiff (los elementos del primero que no están en el segundo).
a <- c("rojo", "azul", "verde")
b <- c("azul", "amarillo")
union(a, b) # todos los colores, sin repetir## [1] "rojo" "azul" "verde" "amarillo"
## [1] "azul"
## [1] "rojo" "verde"
setdiff es especialmente útil para descartar de un vector los valores que ya se han usado en un paso anterior: por ejemplo, al quitar de una lista de candidatos los que ya se han emparejado con otro texto, como se hace en el emparejamiento aproximado de textos del capítulo 3.
Resumen numérico de un vector
Las funciones estadísticas básicas operan sobre el vector completo. Todas ellas comparten un detalle esencial: si el vector contiene algún valor faltante el resultado es NA, salvo que se indique na.rm = TRUE para excluirlos del cálculo.
## [1] 5
## [1] 30
## [1] 7.5
## [1] 15
## [1] 2
Dataframes y Tibbles
Un dataframe es una matriz que puede combinar columnas con diferentes tipos de datos, por ejemplo fechas y valores numéricos. La estructura tibble es una versión mejorada de dataframe que se encuentra en la librería tidyverse. En este curso utilizaremos principalmente esta estructura de datos para manejar tablas. De hecho, salvo indicación de lo contrario, cuando hablemos en el curso de tablas, nos estaremos refiriendo a estructuras tibbles. Cuando hablemos de campos o variables nos estaremos refiriendo a las columnas (col) de la tabla y cuando hablemos de registros, nos estaremos refiriendo a las filas (row) de la tabla.
2.1.0.1 Creación manual de un tibble
tb <- tibble( # tibble con dos columnas
date=as.Date("2022-12-31")+0:5,
value=c(9,6,1,7,5,8))
tb # impresión por consola de tb## # A tibble: 6 × 2
## date value
## <date> <dbl>
## 1 2022-12-31 9
## 2 2023-01-01 6
## 3 2023-01-02 1
## 4 2023-01-03 7
## 5 2023-01-04 5
## 6 2023-01-05 8
## [1] "El segundo valor del campo date del tibble es 2023-01-01"
## [1] "El tercer valor del campo value del tibble es 1"
## [1] "El tercer valor del campo value del tibble es 1"
La función tribble construye la misma tabla fila a fila en lugar de columna a columna: primero se declaran los nombres de las columnas precedidos de ~ y después se escriben los valores tal como se verían en la tabla. Resulta más legible cuando los datos son pocos y se escriben directamente en el código.
tb_tri <- tribble( # misma tabla, fila a fila
~date, ~value,
as.Date("2022-12-31"), 9,
as.Date("2023-01-01"), 6
)
tb_tri## # A tibble: 2 × 2
## date value
## <date> <dbl>
## 1 2022-12-31 9
## 2 2023-01-01 6
2.1.0.2 Acceso primeros y últimos valores deltibble
## # A tibble: 3 × 2
## date value
## <date> <dbl>
## 1 2022-12-31 9
## 2 2023-01-01 6
## 3 2023-01-02 1
## # A tibble: 4 × 2
## date value
## <date> <dbl>
## 1 2023-01-02 1
## 2 2023-01-03 7
## 3 2023-01-04 5
## 4 2023-01-05 8
Manejo básico de filas y columnas
## [1] 6
## [1] 2
## # A tibble: 3 × 2
## date value
## <date> <dbl>
## 1 2022-12-31 9
## 2 2023-01-01 6
## 3 2023-01-02 1
## # A tibble: 6 × 1
## value
## <dbl>
## 1 9
## 2 6
## 3 1
## 4 7
## 5 5
## 6 8
## # A tibble: 3 × 1
## value
## <dbl>
## 1 9
## 2 6
## 3 1
## [1] 9 6 1 7 5 8
## [1] "date" "value"
## # A tibble: 6 × 2
## date_new value_new
## <date> <dbl>
## 1 2022-12-31 9
## 2 2023-01-01 6
## 3 2023-01-02 1
## 4 2023-01-03 7
## 5 2023-01-04 5
## 6 2023-01-05 8
2.1.0.3 Añadir un registro (fila) a un tibble
## # A tibble: 7 × 2
## date value
## <date> <dbl>
## 1 2022-12-31 9
## 2 2023-01-01 6
## 3 2023-01-02 1
## 4 2023-01-03 7
## 5 2023-01-04 5
## 6 2023-01-05 8
## 7 2023-01-25 11
Concatenar los registros de varios tibbles
La función rbind() nos permite concatenar los registros de varios tibbles que tengan los mismos campos. Veamos un ejemplo:
tb1 <- tibble(
date=as.Date("2022-12-31")+0:2,
value=c(9,6,1))
tb2 <- tibble(
date=as.Date("2022-12-31")+3:5,
value=c(7,5,8))
rbind(tb1,tb2) # concatenamos las filas## # A tibble: 6 × 2
## date value
## <date> <dbl>
## 1 2022-12-31 9
## 2 2023-01-01 6
## 3 2023-01-02 1
## 4 2023-01-03 7
## 5 2023-01-04 5
## 6 2023-01-05 8
Una alternativa más flexible es bind_rows(), de dplyr. Con las mismas columnas el resultado es idéntico a rbind(), pero si los tibbles no tienen exactamente las mismas columnas, bind_rows() no da error: rellena con NA las columnas que faltan en cada tabla, mientras que rbind() se detiene con un error de “el número de columnas de los argumentos no coincide”. Esta situación es habitual cuando se combinan datos de fuentes distintas, por ejemplo un fichero con una columna de país que otro no tiene:
tb3 <- tibble( # con una columna extra: pais
date=as.Date("2023-01-01")+0:1,
value=c(2,3),
pais="España")
bind_rows(tb1,tb3) # rellena con NA lo que falta## # A tibble: 5 × 3
## date value pais
## <date> <dbl> <chr>
## 1 2022-12-31 9 <NA>
## 2 2023-01-01 6 <NA>
## 3 2023-01-02 1 <NA>
## 4 2023-01-01 2 España
## 5 2023-01-02 3 España
2.1.0.4 Añadir un campo (columna) a un tibble
Para añadir un campo a un tibble basta crear un vector del tamaño del número de filas del tibble y añadir el campo al tibble. Si el valor asignado es constante, podemos asignar directamente el valor
tb$new_value <- 1:nrow(tb) # nueva columna con valores 1..n
tb$country <- "Spain" # nueva columna con valor constante
tb## # A tibble: 6 × 4
## date value new_value country
## <date> <dbl> <int> <chr>
## 1 2022-12-31 9 1 Spain
## 2 2023-01-01 6 2 Spain
## 3 2023-01-02 1 3 Spain
## 4 2023-01-03 7 4 Spain
## 5 2023-01-04 5 5 Spain
## 6 2023-01-05 8 6 Spain
Cambiar un valor en todo el tibble
## # A tibble: 6 × 4
## date value new_value country
## <date> <dbl> <int> <chr>
## 1 2022-12-31 9 1 Spain
## 2 2023-01-01 6 2 Spain
## 3 2023-01-02 1 3 Spain
## 4 2023-01-03 7 4 Spain
## 5 2023-01-04 NA NA Spain
## 6 2023-01-05 8 6 Spain
2.1.0.5 Seleccionar registros usando slice
## # A tibble: 3 × 4
## date value new_value country
## <date> <dbl> <int> <chr>
## 1 2023-01-01 6 2 Spain
## 2 2023-01-02 1 3 Spain
## 3 2023-01-03 7 4 Spain
2.1.0.6 Seleccionar los registros con el valor máximo o mínimo: slice_max/slice_min
## # A tibble: 2 × 4
## date value new_value country
## <date> <dbl> <int> <chr>
## 1 2022-12-31 9 1 Spain
## 2 2023-01-05 8 6 Spain
## # A tibble: 2 × 4
## date value new_value country
## <date> <dbl> <int> <chr>
## 1 2023-01-02 1 3 Spain
## 2 2023-01-01 6 2 Spain
Hacer una operación en todos los elementos de campos seleccionados
Vamos a convertir en tipo character todos los valores de los campos valuey new_value
## # A tibble: 6 × 4
## date value new_value country
## <date> <chr> <chr> <chr>
## 1 2022-12-31 9 1 Spain
## 2 2023-01-01 6 2 Spain
## 3 2023-01-02 1 3 Spain
## 4 2023-01-03 7 4 Spain
## 5 2023-01-04 <NA> <NA> Spain
## 6 2023-01-05 8 6 Spain
la función mutate pertenece a la librería dplyr y se verá en detalle más adelante
2.1.0.7 Imprimir información de un tibble
Además de ejecutar el nombre del tibble en la consola, otras formas de obtener información sobre el tibble por consola son:
## # A tibble: 6 × 4
## date value new_value country
## <date> <dbl> <int> <chr>
## 1 2022-12-31 9 1 Spain
## 2 2023-01-01 6 2 Spain
## 3 2023-01-02 1 3 Spain
## # ℹ 3 more rows
## tibble [6 × 4] (S3: tbl_df/tbl/data.frame)
## $ date : Date[1:6], format: "2022-12-31" "2023-01-01" ...
## $ value : num [1:6] 9 6 1 7 NA 8
## $ new_value: int [1:6] 1 2 3 4 NA 6
## $ country : chr [1:6] "Spain" "Spain" "Spain" "Spain" ...
## Rows: 6
## Columns: 4
## $ date <date> 2022-12-31, 2023-01-01, 2023-01-02, 2023-01-03, 2023-01-04,…
## $ value <dbl> 9, 6, 1, 7, NA, 8
## $ new_value <int> 1, 2, 3, 4, NA, 6
## $ country <chr> "Spain", "Spain", "Spain", "Spain", "Spain", "Spain"
## date value new_value country
## Min. :2022-12-31 Min. :1.0 Min. :1.0 Length:6
## 1st Qu.:2023-01-01 1st Qu.:6.0 1st Qu.:2.0 Class :character
## Median :2023-01-02 Median :7.0 Median :3.0 Mode :character
## Mean :2023-01-02 Mean :6.2 Mean :3.2
## 3rd Qu.:2023-01-03 3rd Qu.:8.0 3rd Qu.:4.0
## Max. :2023-01-05 Max. :9.0 Max. :6.0
## NA's :1 NA's :1
Cuando queremos imprimir algo dentro de un bucle, usaremos la función print.
Imprimir tablas anchas con slider horizontal
Cuando un tibble tiene muchas columnas, su impresión por consola se adapta sola al ancho disponible: muestra las columnas que caben y resume las restantes en una línea del tipo # ℹ n more variables. Veámoslo con una tabla de temperaturas medias mensuales (una fila por ciudad y una columna por mes; los valores son aproximados, cercanos a las medias climatológicas de AEMET en el aeropuerto de Gran Canaria, el Retiro de Madrid y Barcelona, y solo tienen fines ilustrativos):
# Tabla ancha: 13 columnas (la ciudad y los 12 meses)
temperaturas <- tibble(
ciudad = c("Las Palmas", "Madrid", "Barcelona"),
enero = c(18.3, 6.3, 9.8),
febrero = c(18.5, 7.9, 10.9),
marzo = c(19.4, 11.2, 12.7),
abril = c(19.8, 12.9, 14.5),
mayo = c(20.8, 16.7, 17.6),
junio = c(22.3, 22.2, 21.5),
julio = c(23.8, 25.6, 24.3),
agosto = c(24.6, 25.1, 24.8),
septiembre = c(24.4, 20.9, 22.1),
octubre = c(23.2, 15.1, 18.4),
noviembre = c(21.3, 9.9, 13.5),
diciembre = c(19.4, 6.9, 10.6)
)
temperaturas # la impresión por consola trunca las columnas que no caben## # A tibble: 3 × 13
## ciudad enero febrero marzo abril mayo junio julio agosto septiembre octubre
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Las Pal… 18.3 18.5 19.4 19.8 20.8 22.3 23.8 24.6 24.4 23.2
## 2 Madrid 6.3 7.9 11.2 12.9 16.7 22.2 25.6 25.1 20.9 15.1
## 3 Barcelo… 9.8 10.9 12.7 14.5 17.6 21.5 24.3 24.8 22.1 18.4
## # ℹ 2 more variables: noviembre <dbl>, diciembre <dbl>
El problema aparece cuando queremos ver la tabla completa en el documento HTML: si la imprimimos como tabla —por ejemplo con knitr::kable(), que veremos con detalle más adelante— la tabla resulta más ancha que la página y se corta por el borde derecho. La solución que usaremos en este curso es envolver el chunk en un <div> con la clase tabla-scroll, que añade una barra de desplazamiento (slider) horizontal cuando la tabla no cabe:
El resultado es la tabla completa, que se puede recorrer con el slider horizontal que aparece bajo la tabla:
| ciudad | enero | febrero | marzo | abril | mayo | junio | julio | agosto | septiembre | octubre | noviembre | diciembre |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Las Palmas | 18.3 | 18.5 | 19.4 | 19.8 | 20.8 | 22.3 | 23.8 | 24.6 | 24.4 | 23.2 | 21.3 | 19.4 |
| Madrid | 6.3 | 7.9 | 11.2 | 12.9 | 16.7 | 22.2 | 25.6 | 25.1 | 20.9 | 15.1 | 9.9 | 6.9 |
| Barcelona | 9.8 | 10.9 | 12.7 | 14.5 | 17.6 | 21.5 | 24.3 | 24.8 | 22.1 | 18.4 | 13.5 | 10.6 |
Nota: dónde está definida la clase tabla-scroll
La clase tabla-scroll no forma parte de R ni de RMarkdown: está definida en la hoja de estilos estilos.css de este libro y de los documentos del curso (ejercicios, exámenes y modelo de memoria del proyecto). Para usarla en un documento RMarkdown propio basta con incluir estas líneas en un bloque <style> al principio del documento, justo después de la cabecera:
<style>
.tabla-scroll { overflow-x: auto; }
.tabla-scroll table { width: auto; min-width: 100%; }
.tabla-scroll table td, .tabla-scroll table th { white-space: nowrap; }
</style>El truco funciona con cualquier chunk cuya salida sea una tabla HTML (kable, gt o el resumen de skim() que veremos en el próximo capítulo). En la salida PDF el <div> se ignora sin causar errores. Las tablas que sí caben en la página no se ven afectadas: el slider solo aparece cuando hace falta.
Listas
Una lista es una colección de objetos que pueden tener estructuras y tamaños distintos. Nosotros no haremos mucho uso de las listas en este curso dado que nuestra principal fuente de información van a ser tablas de datos que vamos a manejar con tibbles. Típicamente, usaremos listas cuando definamos funciones que queremos que retornen varias cosas.
## $date
## [1] "2022-12-25"
##
## $value1
## [1] 1 2 3
##
## $value2
## [1] 1 2 3 4
Para acceder a los elementos de la lista haremos:
## [1] 1 2 3
## [1] "2022-12-25"
## $date
## [1] "2022-12-25"
## [1] 1 2 3 4
La diferencia entre [[ ]] y [ ] es importante: el doble corchete extrae el contenido del elemento (aquí, una fecha), mientras que el corchete simple devuelve una sublista que contiene ese elemento, que sigue siendo una lista.
2.2 Funciones en R
Las funciones permiten simplificar y organizar el código empaquetando operaciones para ser reutilizadas de forma compacta y legible. En R, las funciones suelen definirse en ficheros con extensión .R. Para poder utilizarlas desde un script de R es necesario incluir al principio del script la instrucción:
La estructura general de una función es la siguiente:
NombreFuncion <- function(parámetro1, parámetro2, ...){
# líneas de código
.....
return(resultado)
}A continuación se define una función sencilla que suma dos números y se usa para calcular 2+3:
# recibe dos argumentos y devuelve su suma
suma <- function(x,y){
z <- x+y # suma
return(z)
}
suma(2,3) # llamada de prueba## [1] 5
return() no es obligatorio: si se omite, la función devuelve el valor de la última expresión evaluada, de modo que la función anterior daría el mismo resultado terminando simplemente en z. Aun así, escribirlo de forma explícita hace el código más legible y resulta imprescindible cuando se quiere salir de la función antes de llegar al final, como en los if encadenados que veremos más adelante en asignar_grupo_edad().
Argumentos con nombre y valores por defecto
Los valores que se pasan a una función (sus argumentos) se pueden indicar de dos formas: por posición, en el mismo orden en que se declararon los parámetros —suma(2,3)—, o por nombre, diciendo a qué parámetro corresponde cada valor —suma(x=2, y=3), o incluso suma(y=3, x=2), porque al escribir el nombre el orden deja de importar—.
Además, un parámetro puede declararse con un valor por defecto: si quien llama a la función no lo indica, se usa ese valor. Es lo que convierte una función con muchas opciones en algo cómodo de usar, porque solo hay que escribir las que se quieren cambiar.
# 'decimales' tiene un valor por defecto: si no se indica, vale 2
redondear <- function(valor, decimales = 2){
return(round(valor, digits = decimales))
}
redondear(3.14159) # sin indicar decimales: usa 2## [1] 3.14
## [1] 3.1416
## [1] 3
Este mecanismo explica el aspecto de casi todas las llamadas a funciones de librería que iremos viendo en el curso. En read_csv(url, skip = 1) (del próximo capítulo) se pasa la ruta del fichero por posición y se ajusta por nombre uno de los varios parámetros que la función admite; todos los demás se quedan con su valor por defecto. Lo mismo ocurre en adist(a, b, ignore.case = TRUE) o en round(x, digits = 2). Por eso, al consultar la documentación de una función con help(), lo útil no es memorizar el orden de sus parámetros, sino localizar cuál hay que cambiar: escrito con el nombre delante, se pone siempre igual y en cualquier posición.
La mayor parte de las funciones de las librerías estándar —como paste, as.Date, etc.— están diseñadas para operar sobre vectores: cuando reciben un vector como argumento, aplican la función a cada elemento y devuelven otro vector. Por ejemplo:
# ym() se aplica elemento a elemento sobre el vector de fechas 'd'
d <- c("2008-01","2014-10","2023-12")
paste("la función ym() convierte",d,"en",ym(d))## [1] "la función ym() convierte 2008-01 en 2008-01-01"
## [2] "la función ym() convierte 2014-10 en 2014-10-01"
## [3] "la función ym() convierte 2023-12 en 2023-12-01"
Cuando una función vectorizada recibe dos vectores, lo habitual es que los combine elemento a elemento: el primero con el primero, el segundo con el segundo, etc. (y, si uno es más corto, lo recicla). Por ejemplo, paste(c("a","b"), c("x","y")) devuelve "a x" y "b y". Algunas funciones, en cambio, están diseñadas para devolver una matriz con el resultado de cada combinación de elementos de los dos vectores. Es el caso de adist:
# adist() calcula la distancia entre cada par de strings de los dos vectores → resultado: matriz 2×3
adist(c("Teror","Galdar"),c("Telde","Arucas","San Mateo"))## [,1] [,2] [,3]
## [1,] 3 6 9
## [2,] 4 5 7
Esa matriz es lo que permite resolver el problema que anunciábamos al presentar adist: encontrar, dado un string, el elemento más parecido dentro de un vector. Basta quedarse con la fila que corresponde al string buscado y localizar su valor mínimo con which.min():
# Cada fila corresponde a un municipio buscado y cada columna a uno de referencia
referencia <- c("Telde","Arucas","Gáldar")
d <- adist(c("Teror","Galdar"),referencia,ignore.case=TRUE)
d## [,1] [,2] [,3]
## [1,] 3 6 5
## [2,] 4 5 1
## [1] "Gáldar"
Esta capacidad de las funciones para actuar en función del tipo de argumento que recibe depende de cómo se haya implementado la función.
Estructuras de control
Las siguientes estructuras de control permiten controlar el flujo de ejecución de un código:
if-elsepermite controlar el flujo a través de una condición.forgenera un bucle con un iteradorwhilegenera un bucle con una condición de paradabreakfuerza la terminación de un buclenextfuerza el paso a la siguiente iteración de un bucle.
Operadores lógicos
==igual a (compara valor a valor; para comprobar si dos objetos son exactamente iguales se usaidentical(x, y))!=distinto dex %in% vcomprueba si cada elemento dexestá en el vectorvx | yx OR yx & yx AND y!xnot xisTRUE(x)comprueba si x es TRUEis.na(x)comprueba si x es NA
Estos operadores se combinan para seleccionar los elementos de un vector que cumplen una condición. Hay que prestar atención cuando el vector contiene valores faltantes, porque cualquier comparación con NA da NA y ese NA se cuela en el resultado de la selección:
## [1] FALSE TRUE TRUE NA TRUE
## [1] 8 15 NA 30
## [1] 8 15 30
Ejemplos de funciones
Vamos a implementar una función que calcule la media de un vector de números dejando fuera los posibles valores no disponibles NA
MiMedia <- function(V){
suma=0 # acumulador de la suma
Nvalores=0 # contador de valores válidos
for(i in seq_along(V)){ # seq_along: 1, 2, ..., length(V) (y nada si V está vacío)
if(is.na(V[i])==TRUE) next # saltamos los NA
suma=suma+V[i]
Nvalores=Nvalores+1
}
return(suma/Nvalores) # media
}
V <- c(1.,NA,2.,3.)
MiMedia(V)## [1] 2
Este mismo resultado se puede obtener llamando a la función mean pero previamente omitiendo los NA del cálculo, lo cual se puede hacer de dos formas:
## [1] 2
## [1] 2
La función na.omit se usa mucho cuando queremos hacer cálculos numéricos en vectores que pueden contener NA.
Vamos ahora a implementar una función para calcular la primera posición en un vector de strings, sV donde se encuentra el string s. Si no se encuentra el string se devuelve NA
FindString <- function(s,sV){
for(i in seq_along(sV)){
if(s==sV[i]){
return(i) # posición donde coincide
}
}
return(NA) # no encontrado
}
sV <- c("abc","cde","efg")
FindString("cde",sV) # se encuentra## [1] 2
## [1] NA
Un resultado parecido se obtiene con la función which, con dos diferencias: devuelve todas las posiciones donde aparece el string (no solo la primera) y, si no lo encuentra, devuelve un vector vacío, integer(0), en lugar de NA:
## [1] 2
## integer(0)
La función que hace exactamente lo mismo que FindString es match, que devuelve la primera posición o NA si no encuentra el valor:
## [1] 2
## [1] NA
El ámbito de las funciones
Las funciones son la base para compartir código y funcionalidades entre programas y usuarios. Además, conceptualmente, identifica un objetivo (lo que queremos obtener con la función) e identifica también qué información (parámetros) es requerida para obtener ese objetivo, de tal manera que en la mayoría de los casos, el interior de una función es una caja negra que el usuario no necesita entender para usar la función. En un primer nivel, las funciones en R se almacenan en ficheros con extensión .R. A lo largo del curso utilizaremos el fichero utilidades.R, donde hemos implementado algunas funciones auxiliares. Para incorporar estas funciones (y poder usarlas) en un fichero Rmd, basta con incluir al inicio:
El nombre de todas las funciones declaradas en utilidades.R empieza por “aedv_”. Ello permite identificar que estamos usando funciones locales creadas para la asignatura AEDV. En un segundo nivel, las funciones se empaquetan en librerías que pueden compartirse con otros usuarios a través de repositorios como GitHub o CRAN.
Conflictos entre librerías: cómo evitarlos
Un problema habitual es que dos librerías cargadas simultáneamente contengan funciones con el mismo nombre pero comportamientos distintos. En ese caso, R utiliza por defecto la función de la última librería cargada, lo que puede producir resultados inesperados. Las funciones de dplyr como filter(), rename() o select() son especialmente propensas a estos conflictos. Para evitarlos se puede especificar explícitamente la librería al llamar a la función, siguiendo la sintaxis dplyr::filter() que nos indica que se debe usar la función filter de la librería dplyr. Aunque esto alarga el código, es obligatorio si se publica una librería en CRAN. En la práctica, una estrategia eficaz es declarar al principio del script las asignaciones conflictivas:
⚠️ Reasignar nombres de funciones
La estrategia de reasignar nombres (filter <- dplyr::filter) es cómoda y evita tener que explicitar la librería, pero oculta un riesgo importante para el principiante: si más adelante cargas otra librería que redefine filter, o si copias este código a otro script sin las mismas librerías, filter puede apuntar a una función distinta de la que crees y producir errores difíciles de diagnosticar (el código “funciona” pero da resultados incorrectos, sin mensaje de error). Como norma segura:
- Ante la duda, usa siempre la forma explícita
dplyr::filter(), que nunca es ambigua. - Si reasignas nombres, hazlo al principio del script y de forma visible, y sé consciente de que estás modificando el significado de esos nombres en todo el documento.
# evitamos conflictos de nombres entre librerías
select <- dplyr::select
mutate <- dplyr::mutate
filter <- dplyr::filter
group_by <- dplyr::group_by
summarise <- dplyr::summariseHay tanta gente que trabaja en R que, con frecuencia, nos encontramos que la misma funcionalidad está implementada en varias librerías. Para decidir qué librería utilizar podemos combinar dos criterios: el primero es el número de descargas que ha tenido la librería en CRAN (se puede consultar, por ejemplo, en datasciencemeta.com) y el segundo es si la librería ha sido actualizada recientemente.
La forma más inmediata de obtener ayuda sobre el uso de una función es ejecutar en la consola el comando help(NombreFuncion)
La familia de funciones apply
Supongamos que hemos implementado una función que no está diseñada para trabajar con vectores pero queremos aplicarla a todos los elementos de un vector. Esto se podría hacer con un bucle, pero la familia de funciones apply permite escribirlo en una sola instrucción, sin tener que inicializar previamente el vector del resultado ni gestionar los índices. Veamos un ejemplo: vamos a implementar la función asignar_grupo_edad() que, a partir de una edad cualquiera asigna un string con el grupo de edad que le corresponde:
asignar_grupo_edad <- function(edad){
if(edad<=25) return("0-25 años") # primer tramo
if(edad<=50) return("26-50 años")
if(edad<=75) return("51-75 años")
return("76 años o más") # el resto
}Ahora vamos a aplicar esta función a un vector de edades usando sapply
## [1] "0-25 años" "0-25 años" "26-50 años" "51-75 años"
## [5] "51-75 años" "76 años o más"
Veamos otro ejemplo: supongamos que tenemos 2 vectores de strings, sV1 y sV2 que queremos comparar usando la función FindString(s,sV). Las funciones sapply y lapply se pueden usar para hacer un bucle aplicando la función FindString(s,sV). La diferencia entre ellas es que lapply devuelve una lista y sapply un vector. Veamos cómo se aplica sapplypara aplicar, con una sola instrucción, la función FindString(s,sV) a todos los elementos de sV1
sV1 <- c("abc","cde","efg")
sV2 <- c("efg","abc","cde")
sapply(sV1,FindString,sV=sV2) # posición de cada elemento de sV1 dentro de sV2## abc cde efg
## 2 3 1
Los parámetros de sapply() son : (1) el vector sV1, (2) la función FindString que actúa sobre los elementos de sV1 y (3) sV=sV2 el segundo argumento de la función FindString.
La regla práctica para elegir entre lapply y sapply es sencilla: usar sapply() cuando se sabe que el resultado será un vector simple (un escalar por elemento), ya que simplifica automáticamente la lista a un vector con nombre. Usar lapply() cuando los resultados tienen tamaños distintos entre sí o cuando se necesita la lista explícitamente para seguir procesando. Si sapply() no puede simplificar el resultado, devuelve una lista igual que lapply().
Las funciones de la familia apply permiten hacer operaciones más complejas que no vamos a estudiar en este curso.
Clasificar en intervalos: cut()
El ejemplo de los grupos de edad responde a un patrón muy concreto —clasificar valores numéricos en intervalos— y para ese caso R tiene una herramienta específica, cut(), que no necesita ninguna función auxiliar ni sapply. Recibe los puntos de corte en breaks y las etiquetas de cada intervalo en labels:
cut(edades,
breaks = c(-Inf, 25, 50, 75, Inf), # puntos de corte
labels = c("0-25 años","26-50 años","51-75 años","76 años o más"))## [1] 0-25 años 0-25 años 26-50 años 51-75 años 51-75 años
## [6] 76 años o más
## Levels: 0-25 años 26-50 años 51-75 años 76 años o más
El resultado es el mismo que obtuvimos con sapply, pero escrito en una sola instrucción, y coincide para cualquier edad, también con decimales, porque los dos aplican exactamente los mismos límites.
Conviene fijarse en un parámetro que decide a qué intervalo va a parar cada punto de corte: right. Por defecto vale TRUE, es decir, cada intervalo excluye su extremo izquierdo e incluye el derecho —(25,50]—, que es lo que aquí encaja con la condición edad<=25 de la función original. Con right = FALSE ocurre lo contrario, [25,50), que es lo que hay que usar cuando la regla se escribe como edad<25:
# Con right = FALSE cada intervalo incluye su extremo izquierdo: [-Inf,25), [25,50)...
cut(c(24,25,26),
breaks = c(-Inf, 25, 50, Inf),
labels = c("menos de 25","de 25 a menos de 50","50 o más"),
right = FALSE)## [1] menos de 25 de 25 a menos de 50 de 25 a menos de 50
## Levels: menos de 25 de 25 a menos de 50 50 o más
Cuando la regla de clasificación no es un intervalo —por ejemplo, si depende de dos variables a la vez— la alternativa es case_when(), que veremos en el próximo capítulo.
Velocidad: sapply no es más rápido que un bucle
Es una creencia muy extendida que la familia apply es más rápida que un bucle for. No es así: las dos formas recorren el vector elemento a elemento, llamando a la función una vez por cada valor, y sapply añade además el trabajo de simplificar el resultado. Lo que sí marca una diferencia grande es vectorizar, es decir, usar una función que resuelva todo el vector de una vez, como cut(). Podemos medirlo con system.time(), que devuelve el tiempo transcurrido en segundos:
set.seed(1) # reproducibilidad
edades_grandes <- sample(0:100, 1000000, replace = TRUE) # un millón de edades
t_for <- system.time({ # con bucle for
res <- character(length(edades_grandes))
for(i in seq_along(edades_grandes)) res[i] <- asignar_grupo_edad(edades_grandes[i])
})
t_sapply <- system.time(sapply(edades_grandes, asignar_grupo_edad)) # con sapply
t_cut <- system.time(cut(edades_grandes, # con cut() vectorizado
breaks = c(-Inf, 25, 50, 75, Inf),
labels = c("0-25 años","26-50 años","51-75 años","76 años o más")))
tibble(metodo = c("bucle for","sapply","cut() vectorizado"), # comparamos tiempos
milisegundos = round(1000*c(t_for[["elapsed"]], t_sapply[["elapsed"]], t_cut[["elapsed"]])))## # A tibble: 3 × 2
## metodo milisegundos
## <chr> <dbl>
## 1 bucle for 750
## 2 sapply 610
## 3 cut() vectorizado 30
El bucle y sapply tardan tiempos del mismo orden —sapply suele salir incluso algo más lento, por el trabajo añadido de simplificar el resultado—, mientras que cut() resuelve lo mismo en una fracción del tiempo, más de diez veces más rápido. La razón para preferir apply frente a un bucle es, por tanto, la concisión y la legibilidad, no la velocidad; y cuando el rendimiento importa de verdad, lo que hay que buscar es una solución vectorizada.
Nótese que los tiempos concretos dependen del ordenador y cambian de una ejecución a otra: lo que importa aquí no son las cifras exactas, sino la relación entre ellas.
El operador de pila (pipe) |> y %>%
El operador de pila (pipe) permite concatenar las llamadas a funciones de forma más clara y sin necesidad de usar paréntesis anidados. Es decir, si normalmente escribimos la concatenación de funciones como
función2(función1(x))
usando el operador pipe, la misma llamada se escribiría como:
Como veremos, en muchas ocasiones, esta forma de escribir la llamada a las funciones concatenando la salida de una operación con la entrada de la siguiente, resulta más práctica y natural que la forma habitual que presenta el problema de que lo último que hacemos (la llamada a la última función) es lo primero que tenemos que escribir, es decir, escribimos en el orden opuesto al que vamos haciendo las operaciones. En general, resulta mucho más práctico cuando añadimos, al resultado obtenido, una nueva operación, escribirla al final del todo y no al principio de cuando empiezan a hacerse las operaciones. Haremos un uso intensivo de este operador de pila a partir del siguiente tema.
Diferencias entre |> y %>%
En R existen dos versiones del operador pipe:
|>(pipe nativo de R): Introducido en R 4.1 (2021), no requiere cargar ninguna librería y es el recomendado actualmente para código nuevo: es el que usan la guía de estilo detidyversey la segunda edición de R for Data Science. No admite el uso del punto.como placeholder; en su lugar se puede usar el guion bajo_con argumento con nombre (x |> función(argumento = _), disponible desde R 4.2) o una función anónima:x |> (\(d) función(argumento, d))().%>%(pipe de magrittr/tidyverse): Fue el primer operador pipe en R, introducido por la libreríamagrittry adoptado masivamente portidyverse. Está disponible al cargartidyverseodplyr. Admite el uso del punto.como placeholder para el argumento:x %>% función(argumento, .)pasaxcomo segundo argumento.
En la práctica, para la mayor parte de las operaciones que haremos en este curso, ambos operadores son intercambiables. En este libro usaremos el pipe nativo |>, que es hoy el estándar; no obstante, el estudiante debe reconocer también %>% porque abunda en el código existente (documentación, Stack Overflow, proyectos anteriores a 2021). Veamos un ejemplo equivalente con ambos operadores:
# con |> (nativo de R, el que usaremos en el curso)
c(3, 1, 4, 1, 5) |> sort() |> rev() # ordenamos y revertimos## [1] 5 4 3 1 1
## [1] 5 4 3 1 1
2.3 Errores comunes de principiante
Al empezar a programar en R es frecuente encontrarse con errores que, aunque sencillos, pueden consumir mucho tiempo si no se reconocen rápidamente. La siguiente tabla recoge los más habituales:
| Error | Código problemático | Solución |
|---|---|---|
| Olvidar asignar el resultado | sort(mi_vector) sin asignar |
mi_vector <- sort(mi_vector) |
Confundir = con == |
if(x = 5) en vez de if(x == 5) |
Usar == para comparar |
Mezclar tipos en c() |
c(1, "dos", 3) → todo se convierte a character |
Usar vectores del mismo tipo o listas |
Usar $ en nombre incorrecto |
tb$columa (typo) → devuelve NULL sin error |
Verificar con colnames(tb) |
| No cerrar paréntesis o comillas | paste("hola → error de sintaxis |
Revisar pares de paréntesis y comillas |
Confundir <- dentro de función |
f(x = 5) asigna el argumento; f(x <- 5) asigna en el entorno |
Usar = para argumentos de funciones |
Olvidar na.rm = TRUE |
mean(c(1, NA, 3)) → devuelve NA |
mean(c(1, NA, 3), na.rm = TRUE) |
| Índice fuera de rango | v[10] cuando v tiene 5 elementos → devuelve NA |
Verificar length(v) antes de acceder |
Comparar con NA |
x == NA siempre da NA |
Usar is.na(x) para comprobar |
Filtrar un vector con NA |
x[x > 5] deja un NA en el resultado |
x[!is.na(x) & x > 5] |