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 curso

Librerí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) y ggplot2 (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 de R base. En este capítulo aporta adist(), 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 tibble y 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
  • "mi texto"
factor
  • as.factor("Europe")
numeric
  • 5
  • as.numeric("5")
integer
  • 2024L
  • as.integer("2024")
logical
  • TRUE
  • FALSE
dato no disponible
  • NA
date
  • as.Date("2022-12-31")

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:

x <- "3.14"
is.character(x)        # ¿es texto?
## [1] TRUE
is.numeric(x)          # ¿es numérico?
## [1] FALSE
is.integer(2024L)      # ¿es un entero?
## [1] TRUE
class(x)               # tipo del objeto
## [1] "character"
class(as.numeric(x))   # tipo tras la conversión
## [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:

continentes <- as.factor(c("Europa","Asia","Europa","África","Asia","Europa"))
continentes
## [1] Europa Asia   Europa África Asia   Europa
## Levels: África Asia Europa
levels(continentes)      # valores posibles, ordenados alfabéticamente
## [1] "África" "Asia"   "Europa"
table(continentes)       # número de apariciones de cada nivel
## continentes
## África   Asia Europa 
##      1      2      3
as.numeric(continentes)  # ojo: devuelve los códigos internos, no los valores
## [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.

word(s,3)      # tercera palabra
## [1] "un"
word(s,-1)     # los índices negativos cuentan las palabras desde el final
## [1] "string"
word(s,2,4)    # con dos índices se extrae un rango de palabras
## [1] "es un string"

Comparación de strings

s1 <- "ABC"  
s2 <- "abc"
s1==s2                              # Comparación literal (distingue mayúsculas de minúsculas)
## [1] FALSE
tolower(s1)==tolower(s2)            # Comparación insensible a mayúsculas/minúsculas
## [1] TRUE
str_detect("UNITED NATIONS","NAT") # Comprueba si un string está contenido en otro
## [1] TRUE
str_detect("UNITED NATIONS","nat") # str_detect también distingue mayúsculas: FALSE
## [1] FALSE
str_detect(tolower("UNITED NATIONS"),tolower("nat")) # Búsqueda insensible
## [1] TRUE

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.

adist("Antonio","San antonio")                          # Comparación literal
##      [,1]
## [1,]    5
adist("S. Antonio","San antonio")
##      [,1]
## [1,]    3
adist("Antonio","San antonio",ignore.case=TRUE)         # Ignora mayúsculas/minúsculas
##      [,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:

as.Date("2022-12-25")                                        # Coerción desde el formato estándar ISO 8601
## [1] "2022-12-25"
as.Date("25/12/2022",format = "%d/%m/%Y")                    # Lectura de un formato de fecha no estándar
## [1] "2022-12-25"
format(as.Date("2022-12-25"),"%A, %B, %d, %y")              # Extracción: nombre del día y del mes
## [1] "domingo, diciembre, 25, 22"
format(as.Date("2022-12-25"),"%a, %b, %d, %Y, día dentro de la semana: %u") 
## [1] "do., dic., 25, 2022, día dentro de la semana: 7"
format(as.Date("2022-12-25"),"Nº semana del año: %W, día del año : %j") 
## [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:

as.Date(0, origin = "1899-12-30")      # El número 0 corresponde a la fecha de referencia de Excel
## [1] "1899-12-30"
as.Date(45000, origin = "1899-12-30")  # Conversión de un número arbitrario a fecha
## [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:

ym("2008-02")                            # Devuelve el primer día del mes indicado
## [1] "2008-02-01"
ceiling_date(ym("2008-02"),"month")-1   # Último día del mes
## [1] "2008-02-29"
ym("2008/02")                            # ym() acepta distintos separadores
## [1] "2008-02-01"
ym("200802")                             # También sin separador
## [1] "2008-02-01"
my("02/2008")                            # Si el mes precede al año, usar my()
## [1] "2008-02-01"
yq("2008 Q2")                            # Primer día del segundo trimestre de 2008
## [1] "2008-04-01"
ceiling_date(yq("2008 Q2"),"quarter")-1 # Último día del trimestre
## [1] "2008-06-30"
yq("2008Q2")                             # También sin espacio
## [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:

ym("2008-02")+1          # Con ym(): suma 1 día → pasa al segundo día del mes
## [1] "2008-02-02"
yearmonth("2008-02")+1   # Con yearmonth() de fpp3: suma 1 mes → marzo de 2008
## <yearmonth[1]>
## [1] "2008 mar."
yq("2008-Q2")+1          # Con yq(): suma 1 día
## [1] "2008-04-02"
yearquarter("2008-Q2")+1 # Con yearquarter() de fpp3: suma 1 trimestre → Q3 2008
## <yearquarter[1]>
## [1] "2008 Q3"
## # Year starts on: January
yearweek("2008-W2")+1    # Semana siguiente a la segunda semana de 2008
## <yearweek[1]>
## [1] "2008 W03"
## # Week starts on: Monday
yearweek("2008-W52")+1   # Semana siguiente a la última semana de 2008 (pasa al año siguiente)
## <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(as.Date("2024-12-29"))  # domingo: última semana de 2024
## <yearweek[1]>
## [1] "2024 W52"
## # Week starts on: Monday
yearweek(as.Date("2024-12-30"))  # lunes: pertenece ya a la primera semana de 2025
## <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:

year(as.Date("2022-12-25"))      # Año de una fecha diaria: 2022
## [1] 2022
quarter(as.Date("2022-12-25"))   # Trimestre del año (1 a 4): diciembre está en el 4
## [1] 4
year(yearquarter("2008 Q2"))     # También con los tipos de fpp3: 2008
## [1] 2008
quarter(yearquarter("2008 Q2"))  # 2
## [1] 2
quarter(yearmonth("2008-11"))    # Noviembre está en el cuarto trimestre: 4
## [1] 4

Vectores

Creación de un vector concatenando valores

v <- c(1,7,9)
paste("el segundo elemento de v es : ",v[2])  # Acceso por índice (base 1 en R)
## [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.

c(1,"dos",3)           # todo pasa a character
## [1] "1"   "dos" "3"
class(c(1,"dos",3))
## [1] "character"
class(c(1,TRUE,3))     # TRUE vale 1 y FALSE vale 0: el resultado es numérico
## [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 repitiendo valores

v <- rep(1,6)   # Repite el valor 1 seis veces
v
## [1] 1 1 1 1 1 1
paste("la longitud del vector es : ",length(v)) 
## [1] "la longitud del vector es :  6"

Creación de un vector usando números consecutivos

3:7  # El operador ':' genera una secuencia de enteros consecutivos
## [1] 3 4 5 6 7

Creación de un vector con valores descendentes

seq(7, 3, by = -1)  # seq() permite controlar inicio, fin e incremento
## [1] 7 6 5 4 3

Creación de un vector con tamaño asignado pero sin rellenar

numeric(5)    # Vector de ceros de tipo numérico con 5 posiciones
## [1] 0 0 0 0 0
character(5)  # Vector de strings vacíos con 5 posiciones
## [1] "" "" "" "" ""

Creación de un vector usando fechas consecutivas

as.Date("2022-12-31")+0:3  # Suma 0, 1, 2 y 3 días a la fecha inicial
## [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("2023-01")+ 0:5  # Añade 0, 1, ..., 5 meses al periodo inicial
## <yearmonth[6]>
## [1] "2023 ene." "2023 feb." "2023 mar." "2023 abr." "2023 may." "2023 jun."

Eliminar elementos de un vector

x <- c(2,3,2,3,5)
x[-2] # eliminamos la segunda posición 
## [1] 2 2 3 5
x[-(2:3)] # eliminamos la segunda y tercera posición 
## [1] 2 3 5
x[! x %in% c(2,5)]  #eliminamos todos los 2 y 5 existentes
## [1] 3 3

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.

x <- c("my book", "my car","your car")
gsub('my','your',x) # reemplaza "my" por "your"
## [1] "your book" "your car"  "your car"
gsub('car','tree',x) # reemplaza "car" por "tree"
## [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

which(x=='my car')[1] # solo la primera posición
## [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:

unique(c("rojo","rojo","rojo","azul","azul")) # valores sin repetir
## [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"
intersect(a, b) # colores que están en los dos vectores
## [1] "azul"
setdiff(a, b)   # colores de 'a' que no están en 'b'
## [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.

v <- c(4,15,2,NA,9)
length(v)               # número de elementos, incluidos los NA
## [1] 5
sum(v, na.rm = TRUE)    # sin na.rm el resultado sería NA
## [1] 30
mean(v, na.rm = TRUE)
## [1] 7.5
max(v, na.rm = TRUE)
## [1] 15
min(v, na.rm = TRUE)
## [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
paste("El segundo valor del campo date del tibble es ",tb$date[2])
## [1] "El segundo valor del campo date del tibble es  2023-01-01"
paste("El tercer valor del campo value del tibble es ",tb$value[3])
## [1] "El tercer valor del campo value del tibble es  1"
paste("El tercer valor del campo value del tibble es ",tb[3,2])
## [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

head(tb,3) # primeras 3 filas de tb
## # A tibble: 3 × 2
##   date       value
##   <date>     <dbl>
## 1 2022-12-31     9
## 2 2023-01-01     6
## 3 2023-01-02     1
tail(tb,4) # últimas 4 filas de tb
## # 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

nrow(tb)    # número de filas
## [1] 6
ncol(tb)    # número de columnas
## [1] 2
tb[1:3,] # selección de las 3 primeras filas
## # A tibble: 3 × 2
##   date       value
##   <date>     <dbl>
## 1 2022-12-31     9
## 2 2023-01-01     6
## 3 2023-01-02     1
tb[,2:2] # selección de la segunda columna (como tibble de 1 columna)
## # A tibble: 6 × 1
##   value
##   <dbl>
## 1     9
## 2     6
## 3     1
## 4     7
## 5     5
## 6     8
tb[1:3,2:2] # selección combinada
## # A tibble: 3 × 1
##   value
##   <dbl>
## 1     9
## 2     6
## 3     1
pull(tb,value) # extrae el campo value como VECTOR, no como tibble
## [1] 9 6 1 7 5 8
colnames(tb) # nombres de las columnas
## [1] "date"  "value"
tb2 <- set_names(tb,c("date_new","value_new")) # cambiamos los nombres de las columnas  
tb2
## # 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

add_row(tb,date = as.Date("2023-01-25"),value=11) # añadimos una fila
## # 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

tb[tb==5] <- NA # sustituimos todos los 5 por NA
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    NA        NA Spain  
## 6 2023-01-05     8         6 Spain

2.1.0.5 Seleccionar registros usando slice

slice(tb,2:4) # filas de la 2 a la 4
## # 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

slice_max(tb,value,n=2) # los 2 registros con mayor value
## # 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
slice_min(tb,value,n=2) # los 2 registros con menor value
## # 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

tb |> mutate(across(value:new_value,as.character)) # ambas columnas a texto
## # 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:

print(tb,n=3)   # las 3 primeras filas
## # 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
str(tb)         # estructura del tibble
## 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" ...
glimpse(tb)     # como str(), una línea por columna
## 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"
summary(tb)     # resumen estadístico
##       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:

<div class="tabla-scroll">

```{r}
 knitr::kable(temperaturas)
```

</div>

El resultado es la tabla completa, que se puede recorrer con el slider horizontal que aparece bajo la tabla:

 knitr::kable(temperaturas)
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.

li <- list("date"=as.Date("2022-12-25"), "value1"= 1:3,"value2"=1:4) # creación lista
li
## $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:

li[[2]] # acceso por posición: devuelve el elemento
## [1] 1 2 3
li[["date"]] # acceso por nombre: devuelve el elemento
## [1] "2022-12-25"
li["date"] # con corchete simple: devuelve una lista con ese elemento
## $date
## [1] "2022-12-25"
li$value2 # acceso con el operador $ (equivale a li[["value2"]])
## [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:

source("NombreFichero.R")

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
redondear(3.14159, 4)              # por posición
## [1] 3.1416
redondear(3.14159, decimales = 0)  # por nombre
## [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
referencia[which.min(d[2,])]  # fila 2: el más parecido a "Galdar"
## [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 - else permite controlar el flujo a través de una condición.

  • for genera un bucle con un iterador

  • while genera un bucle con una condición de parada

  • break fuerza la terminación de un bucle

  • next fuerza 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 usa identical(x, y))

  • != distinto de

  • x %in% v comprueba si cada elemento de x está en el vector v

  • x | y x OR y

  • x & y x AND y

  • !x not x

  • isTRUE(x) comprueba si x es TRUE

  • is.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:

x <- c(3,8,15,NA,30)
x > 5                  # la comparación sobre el NA devuelve NA
## [1] FALSE  TRUE  TRUE    NA  TRUE
x[x > 5]               # el NA aparece en el resultado
## [1]  8 15 NA 30
x[!is.na(x) & x > 5]   # forma correcta: descartar antes los NA
## [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:

mean(V,na.rm=TRUE) # usando el parámetro na.rm de la función mean
## [1] 2
mean(na.omit(V))   # eliminar previamente los NA con la función na.omit
## [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
FindString("cd",sV) # no se encuentra
## [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:

which(sV=="cde")
## [1] 2
which(sV=="cd")
## 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:

match("cde", sV)
## [1] 2
match("cd", sV)
## [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:

source("utilidades.R")

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::summarise

Hay 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

edades <- c(5,20,40,60,70,90)
sapply(edades,asignar_grupo_edad) # aplicamos la función a cada edad
## [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:

x |> función1() |> función2()

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 de tidyverse y 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ía magrittr y adoptado masivamente por tidyverse. Está disponible al cargar tidyverse o dplyr. Admite el uso del punto . como placeholder para el argumento: x %>% función(argumento, .) pasa x como 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
# con %>% (magrittr/tidyverse; frecuente en código antiguo)
c(3, 1, 4, 1, 5) %>% sort() %>% rev()
## [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:

Table 2.1: Errores comunes de principiante en R y sus soluciones
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]

Referencias

[Bo] Juan Bosco Mendoza. R para principiantes.

[Pe15] Roger D. Peng. R Programming for Data Science, Lulu, 2015.

[WiÇeGa23] Wickham, Hadley, Mine Çetinkaya-Rundel and Garrett Grolemund. R for Data Science (2e), O’Reilly Media, 2023.