Vectores y fundamentos
Variables, tipos y asignación
R usa <- como operador de asignación preferido (= funciona pero puede ser ambiguo en argumentos de función). Todo en R es un vector — incluso un único número es un vector de longitud 1. Los tipos core son character, numeric (double), integer, logical y complex. NA representa datos faltantes y se propaga a través de operaciones (use na.rm=TRUE para omitir). NULL es la ausencia de un valor (un objeto vacío), distinto de NA. Siempre compruebe si hay NAs antes del análisis.
# assignment operators (all equivalent for simple values)
name <- "Alice" # preferred
age = 30L # = also works (avoid in functions)
30L -> age2 # rightward assignment
# basic types (called "modes" in R)
class("text") # "character"
class(42) # "numeric" (double)
class(42L) # "integer"
class(3.14) # "numeric"
class(TRUE) # "logical"
class(2 + 3i) # "complex"
# check and convert types
is.numeric(age) # TRUE
is.character(name) # TRUE
as.integer(3.9) # 3 (truncates, not rounds)
as.character(42) # "42"
as.numeric("3.14") # 3.14
# special values
NA # missing value (Not Available)
NULL # null object (no value)
NaN # Not a Number (0/0)
Inf # infinity (1/0)
is.na(NA) # TRUE
is.null(NULL) # TRUECreación e indexación de vectores
c() combina valores en un vector — la función R más fundamental. R usa indexación base 1 (el primer elemento es [1], no [0]). Los índices negativos EXCLUYEN elementos: nums[-1] elimina el primero. La indexación lógica (nums[nums > 3]) filtra por condición — extremadamente potente. Los vectores pueden tener nombres, habilitando acceso por etiqueta. Todos los elementos de un vector deben ser del mismo tipo; si mezcla tipos, R los coacciona (p. ej., c(1, 'a') se convierte en c('1', 'a')).
# create vectors with c() (combine)
nums <- c(1, 2, 3, 4, 5)
chars <- c("a", "b", "c")
logs <- c(TRUE, FALSE, TRUE)
# sequences
1:5 # 1 2 3 4 5
seq(1, 10, by = 2) # 1 3 5 7 9
seq(0, 1, length.out = 5) # 0 0.25 0.5 0.75 1
rep(1:3, times = 2) # 1 2 3 1 2 3
rep(1:3, each = 2) # 1 1 2 2 3 3
# indexing (1-indexed!)
nums[1] # 1 (first element)
nums[length(nums)] # 5 (last element)
nums[c(1, 3, 5)] # 1 3 5 (multiple indices)
nums[-1] # 2 3 4 5 (all EXCEPT first)
nums[-c(1, 2)] # 3 4 5 (exclude first two)
nums[2:4] # 2 3 4 (range)
# logical indexing
nums[nums > 2] # 3 4 5
nums[nums %% 2 == 0] # 2 4 (even numbers)
nums[nums > 2] <- 0 # modify in place
# named vectors
ages <- c(alice = 30, bob = 25, carol = 28)
ages["alice"] # 30
ages[c("alice", "bob")] # 30 25Operaciones y funciones de vectores
La vectorización de R es su característica distintiva — las operaciones se aplican elemento a elemento automáticamente, sin necesidad de bucles. El reciclaje reutiliza el vector más corto para coincidir con el más largo (c(1,2,3,4) + c(10,20) da 11,22,13,24). order() devuelve los índices que ordenarían el vector — esencial para ordenar un vector por otro. unique() elimina duplicados. Todas estas funciones son código C optimizado por debajo, haciendo R rápido para operaciones vectoriales.
# arithmetic is element-wise (vectorized)
a <- c(1, 2, 3)
b <- c(4, 5, 6)
a + b # 5 7 9
a * b # 4 10 18
a ^ 2 # 1 4 9
a / b # 0.25 0.4 0.5
# recycling: shorter vector repeats
c(1, 2, 3, 4) + c(10, 20) # 11 22 13 24
# summary functions
sum(a) # 6
mean(a) # 2
median(a) # 2
sd(a) # 1
var(a) # 1
min(a); max(a) # 1; 3
range(a) # 1 3
cumsum(a) # 1 3 6
cumprod(a) # 1 2 6
# sorting and ordering
sort(c(3, 1, 2)) # 1 2 3
sort(c(3, 1, 2), decreasing = TRUE) # 3 2 1
order(c(3, 1, 2)) # 2 3 1 (indices that would sort)
v[order(v)] # sort using order
# useful functions
length(a) # 3
unique(c(1, 1, 2, 2, 3)) # 1 2 3
rev(a) # 3 2 1
head(a, 2) # 1 2 (first n)
tail(a, 2) # 2 3 (last n)Manipulación de caracteres y cadenas
paste/paste0 son las funciones de concatenación de cadenas de R — paste0 no tiene separador (como + de Python). substr extrae subcadenas (indexación base 1). gsub reemplaza todas las coincidencias; sub reemplaza solo la primera. grep devuelve índices de elementos coincidentes; grepl devuelve un vector lógico (más útil para filtrado). R usa regex POSIX extendido por defecto. sprintf proporciona formato estilo C. El paquete stringr (tidyverse) ofrece una API más limpia y consistente.
# paste and paste0 (concatenation)
paste("Hello", "World") # "Hello World"
paste("Hello", "World", sep = "_") # "Hello_World"
paste0("a", "b", "c") # "abc" (no separator)
paste(c("a", "b", "c"), collapse = "-") # "a-b-c"
paste("file", 1:3, ".csv", sep = "") # "file1.csv" "file2.csv" "file3.csv"
# case conversion
toupper("hello") # "HELLO"
tolower("WORLD") # "world"
# substring
substr("Hello World", 1, 5) # "Hello"
nchar("Hello") # 5 (character count)
# split and replace
strsplit("a,b,c", ",")[[1]] # "a" "b" "c"
gsub("o", "0", "Hello World") # "Hell0 W0rld" (all matches)
sub("o", "0", "Hello World") # "Hell0 World" (first match only)
gsub("[0-9]+", "N", "a1b22c333") # "aNbNcN" (regex)
# grep and grepl (pattern matching)
grep("^A", c("Alice", "Bob", "Anna")) # 1 3 (indices)
grepl("^A", c("Alice", "Bob")) # TRUE FALSE
# sprintf (C-style formatting)
sprintf("Pi = %.2f", pi) # "Pi = 3.14"
sprintf("%s is %d", "Alice", 30) # "Alice is 30"Valores faltantes y coerción
NA (Not Available) representa datos faltantes y se propaga a través de la mayoría de operaciones — siempre use na.rm=TRUE o fíltrelos. NULL es diferente: es la ausencia de un valor y se elimina de los vectores. R coacciona al tipo más general al combinar (logical < integer < numeric < character). as.numeric sobre cadenas no numéricas produce NA con una advertencia. ifelse es el operador ternario vectorizado — extremadamente útil para crear variables categóricas a partir de continuas.
# NA propagation and handling
x <- c(1, 2, NA, 4, 5)
mean(x) # NA (NA propagates!)
mean(x, na.rm = TRUE) # 3 (skip NA)
sum(x, na.rm = TRUE) # 12
is.na(x) # FALSE FALSE TRUE FALSE FALSE
sum(is.na(x)) # 1 (count of NAs)
x[!is.na(x)] # 1 2 4 5 (remove NAs)
# NULL vs NA
length(c(1, NA, 3)) # 3 (NA is an element)
length(c(1, NULL, 3)) # 2 (NULL is dropped)
# type coercion hierarchy: logical < integer < double < character
c(TRUE, 1) # 1 1 (logical -> numeric)
c(1L, 2.5) # 1.0 2.5 (integer -> double)
c(1, "a") # "1" "a" (numeric -> character)
# explicit coercion
as.numeric(c("1", "2", "abc")) # 1 2 NA (with warning)
as.logical(c(0, 1, 2)) # FALSE TRUE TRUE
factor(c("low", "high", "low")) # factor with 2 levels
# ifelse vectorized conditional
ifelse(x > 2, "big", "small") # "small" "small" NA "big" "big"Estructuras de datos
Listas (contenedores heterogéneos)
Las listas son la estructura de datos más flexible de R — pueden contener elementos de cualquier tipo y tamaño (como dicts de Python u objetos de JavaScript). El operador $ es un atajo conveniente para acceso por nombre. La distinción crítica: [ ] devuelve una sublista (todavía una lista), mientras [[ ]] extrae el elemento real. Esta es la fuente #1 de confusión para principiantes de R. Use [[ ]] cuando quiera el valor mismo, [ ] cuando quiera un subconjunto. lapply/sapply iteran sobre elementos de la lista aplicando una función.
# lists can hold different types and sizes
user <- list(
name = "Alice",
age = 30,
scores = c(90, 85, 88),
active = TRUE
)
# access by name ($ or [[]])
user$name # "Alice"
user[["age"]] # 30
user[["scores"]][2] # 85
# [] returns a sublist (list); [[]] returns the element
user["name"] # list with one element
user[["name"]] # "Alice" (the string itself)
user[1:2] # sublist with first 2 elements
# modify and add
user$age <- 31
user$email <- "[email protected]" # add new element
user[["scores"]] <- NULL # remove element
# iterate over a list
for (key in names(user)) {
cat(key, ":", user[[key]], "\n")
}
# lapply and sapply on lists
lapply(user$scores, sqrt) # list of square roots
sapply(user$scores, sqrt) # vector of square rootsData Frames
Los data frames son la estructura de datos tabular principal de R — como una hoja de cálculo o tabla SQL donde cada columna puede ser de un tipo diferente. Acceda a columnas con $ o [[ ]]; filtre filas con indexación lógica (df[df$age > 25, ]). subset() es una alternativa más limpia. cbind añade columnas; rbind añade filas. str() muestra la estructura (tipos y vista previa). Siempre establezca stringsAsFactors=FALSE (o use R 4.0+ donde esto es el valor por defecto) para mantener cadenas como caracteres, no factores.
# create a data frame (like a table/spreadsheet)
df <- data.frame(
name = c("Alice", "Bob", "Carol"),
age = c(30, 25, 28),
score = c(90, 85, 88),
stringsAsFactors = FALSE
)
# dimensions
nrow(df) # 3
ncol(df) # 3
dim(df) # 3 3
names(df) # "name" "age" "score"
str(df) # structure summary
head(df, 2) # first 2 rows
# access columns
df$name # vector (by name)
df[["age"]] # vector (by name, alternative)
df[, "age"] # vector (column)
df[, 2] # vector (by index)
df[2] # data frame with one column
# access rows
df[1, ] # first row (as data frame)
df[1:2, ] # first 2 rows
df[c(1, 3), ] # rows 1 and 3
# filter rows (logical indexing)
df[df$age > 26, ] # rows where age > 26
df[df$score >= 88, c("name", "score")]
subset(df, age > 26, select = c(name, score))
# add and modify columns
df$grade <- c("A", "B", "A") # add column
df$age <- df$age + 1 # modify column
df <- cbind(df, pass = df$score > 60) # column bind
# add rows
new_row <- data.frame(name = "Dan", age = 22, score = 75, grade = "C")
df <- rbind(df, new_row)Matrices y arrays
Las matrices son arrays 2D donde TODOS los elementos deben ser del mismo tipo (a diferencia de los data frames). %*% es multiplicación matricial; * es elemento a elemento. solve() computa la inversa matricial; det() el determinante. rowSums/colSums/rowMeans/colMeans son atajos integrados rápidos. apply(m, MARGIN, FUN) es la forma general de aplicar una función a filas (MARGIN=1) o columnas (MARGIN=2). Los arrays extienden matrices a n dimensiones. Para análisis de datos, prefiera data frames; use matrices para álgebra lineal.
# create a matrix (2D, all same type)
m <- matrix(1:12, nrow = 3, ncol = 4)
# [,1] [,2] [,3] [,4]
# [1,] 1 4 7 10
# [2,] 2 5 8 11
# [3,] 3 6 9 12
# fill by row instead of column
m2 <- matrix(1:6, nrow = 2, byrow = TRUE)
# dimensions and attributes
dim(m) # 3 4
nrow(m) # 3
ncol(m) # 4
rownames(m) <- c("r1", "r2", "r3")
colnames(m) <- c("c1", "c2", "c3", "c4")
# indexing
m[2, 3] # 8 (single element)
m[1, ] # 1 4 7 10 (first row)
m[, 2] # 4 5 6 (second column)
m[1:2, 2:3] # 2x2 submatrix
m["r1", "c2"] # 4 (by name)
# matrix operations
t(m) # transpose
m * m # element-wise multiply
m %*% t(m) # matrix multiplication
solve(m[, 1:3]) # inverse (square matrix)
det(matrix(1:4, 2)) # determinant
rowSums(m) # sum of each row
colMeans(m) # mean of each column
apply(m, 1, sum) # row sums (general)
apply(m, 2, max) # column maxima
# arrays (n-dimensional)
arr <- array(1:24, dim = c(2, 3, 4)) # 2x3x4 array
arr[1, 2, 3] # single elementFactores y datos categóricos
Los factores almacenan datos categóricos eficientemente como códigos enteros con mapeos de etiqueta — esenciales para modelado estadístico (lm, glm usan factores para agrupar). Un error común: as.numeric(factor) da los CÓDIGOS enteros, no los valores originales — siempre convierta via as.character primero. cut() agrupa datos continuos en niveles de factor. ordered=TRUE crea factores ordinales que soportan operadores de comparación. relevel cambia la categoría de referencia (importante para interpretación de regresión). table() produce conteos de frecuencia.
# factors represent categorical data (stored as integers with labels)
gender <- factor(c("male", "female", "male", "female"))
print(gender)
# [1] male female male female
# Levels: female male
levels(gender) # "female" "male" (sorted alphabetically)
table(gender) # frequency table
nlevels(gender) # 2
# ordered factor (for ordinal data)
size <- factor(c("M", "S", "L", "XL"),
levels = c("S", "M", "L", "XL"),
ordered = TRUE)
size[1] > size[2] # TRUE (M > S)
# convert to factor and back
nums <- factor(c(1, 2, 3, 2, 1))
as.numeric(nums) # 1 2 3 2 1 (level codes, NOT original values!)
as.numeric(as.character(nums)) # 1 2 3 2 1 (correct way)
# relevel (change reference level)
gender <- relevel(gender, ref = "male")
# cut: convert numeric to factor (binning)
ages <- c(15, 25, 35, 45, 55, 65)
age_groups <- cut(ages, breaks = c(0, 18, 35, 50, 100),
labels = c("child", "young", "adult", "senior"))
table(age_groups) # frequency per group
# interaction of factors
f1 <- factor(c("A", "A", "B", "B"))
f2 <- factor(c("X", "Y", "X", "Y"))
interaction(f1, f2) # A.X A.Y B.X B.YListas a Data Frames y reshape
El reshape entre formatos wide y long es una tarea común de data wrangling. pivot_longer/pivot_wider (tidyr, tidyverse) son las funciones modernas e intuitivas. El formato wide tiene una fila por sujeto con columnas para cada punto temporal; el formato long tiene una fila por observación. El formato long se prefiere para ggplot2 y la mayoría de análisis. split() divide un data frame en una lista por un factor; do.call(rbind, ...) recombinar. La función reshape() de R base es potente pero tiene una interfaz confusa — prefiera tidyr.
# convert list to data frame
my_list <- list(
a = 1:3,
b = c("x", "y", "z")
)
df <- as.data.frame(my_list)
# stack multiple vectors
do.call(rbind, list(
data.frame(name = "A", val = 1),
data.frame(name = "B", val = 2)
))
# reshape: wide to long
library(tidyr)
wide_df <- data.frame(
id = 1:2,
q1 = c(10, 20),
q2 = c(15, 25)
)
long_df <- pivot_longer(wide_df, cols = starts_with("q"),
names_to = "quarter", values_to = "value")
# long to wide
pivot_wider(long_df, names_from = quarter, values_from = value)
# base R reshape (without tidyr)
reshape(wide_df, direction = "long",
varying = c("q1", "q2"), v.names = "value",
timevar = "quarter", idvar = "id")
# split and combine
split_results <- split(df, df$group) # list of data frames by group
combined <- do.call(rbind, split_results) # recombine
# melt and cast (reshape2 package, legacy)
# library(reshape2)
# melt(wide_df, id.vars = "id")
# dcast(long_df, id ~ quarter, value.var = "value")Flujo de control y funciones
If / Else y Switch
El if/else de R requiere llaves para cuerpos multilínea y el else debe estar en la misma línea que la llave de cierre (o R piensa que el if está completo). ifelse(test, yes, no) está vectorizado — se aplica a vectores enteros a la vez, devolviendo un vector de resultados. Para múltiples condiciones, case_when de dplyr es mucho más limpio que ifelse anidado. switch despacha sobre una cadena (o posición numérica) — una alternativa limpia a largas cadenas if-else.
# if-else if-else
score <- 85
if (score >= 90) {
grade <- "A"
} else if (score >= 80) {
grade <- "B"
} else {
grade <- "C"
}
# ifelse (vectorized ternary)
ages <- c(15, 25, 35, 45)
ifelse(ages >= 18, "adult", "minor")
# "minor" "adult" "adult" "adult"
# nested ifelse (avoid deep nesting!)
ifelse(ages < 18, "minor",
ifelse(ages < 65, "adult", "senior"))
# dplyr::case_when is cleaner for multiple conditions
# case_when(
# ages < 18 ~ "minor",
# ages < 65 ~ "adult",
# TRUE ~ "senior"
# )
# switch (dispatch on a value)
day_type <- function(day) {
switch(day,
"Mon" = "weekday",
"Tue" = "weekday",
"Wed" = "weekday",
"Thu" = "weekday",
"Fri" = "weekday",
"Sat" = "weekend",
"Sun" = "weekend",
"unknown"
)
}Bucles: For, While, Repeat
Los bucles for en R iteran sobre elementos (o una secuencia). seq_along(x) es más seguro que 1:length(x) cuando x podría estar vacío (devuelve integer(0) en lugar de c(1,0)). next salta a la siguiente iteración (como continue); break sale. repeat es un bucle infinito que debe romperse explícitamente. SIEMPRE preasigne vectores de resultado (result <- numeric(N)) — hacer crecer un vector en un bucle con c() es O(n²) y extremadamente lento. Sin embargo, prefiera operaciones vectorizadas o la familia apply sobre bucles cuando sea posible.
# for loop
for (i in 1:5) {
print(i)
}
# iterate over a vector
fruits <- c("apple", "banana", "cherry")
for (fruit in fruits) {
print(paste("Fruit:", fruit))
}
# iterate with index
for (i in seq_along(fruits)) {
print(paste(i, fruits[i]))
}
# while loop
count <- 0
while (count < 5) {
count <- count + 1
if (count == 3) next # skip (like continue)
print(count)
}
# repeat loop (infinite, must break)
i <- 0
repeat {
i <- i + 1
if (i >= 3) break # exit loop
print(i)
}
# preallocate for speed (IMPORTANT!)
result <- numeric(1000)
for (i in 1:1000) {
result[i] <- i^2
}
# nested loops
mat <- matrix(0, 3, 3)
for (i in 1:3) {
for (j in 1:3) {
mat[i, j] <- i * j
}
}Definición de funciones y argumentos
Las funciones de R devuelven la última expresión evaluada automáticamente (no se necesita return explícito, aunque return() es más claro para salidas tempranas). Los argumentos por defecto hacen las funciones flexibles. Los ... (ellipsis) capturan argumentos extra para pasar — esenciales para funciones wrapper. Los argumentos con nombre pueden estar en cualquier orden. R usa evaluación perezosa: los argumentos solo se evalúan cuando se usan por primera vez, por lo que los argumentos no usados no causan errores. Devuelva múltiples valores empaquetándolos en una lista.
# basic function (last expression is returned)
add <- function(a, b) {
a + b
}
add(3, 4) # 7
# explicit return
is_positive <- function(x) {
if (x > 0) return(TRUE)
FALSE
}
# default arguments
greet <- function(name, greeting = "Hello", punctuation = "!") {
paste0(greeting, ", ", name, punctuation)
}
greet("Alice") # "Hello, Alice!"
greet("Bob", greeting = "Hi") # "Hi, Bob!"
greet(name = "Carol", punctuation = "?") # named args
# ... (ellipsis: pass arguments through)
my_plot <- function(x, y, ...) {
plot(x, y, col = "blue", ...)
}
my_plot(1:10, 1:10, main = "My Plot", type = "l")
# return multiple values via list
stats <- function(x) {
list(mean = mean(x), sd = sd(x), n = length(x))
}
result <- stats(1:10)
result$mean # 5.5
# lazy evaluation (args evaluated only when used)
f <- function(a, b) {
a * 2 # b is never used, so not evaluated
}
f(5) # 10 (no error despite missing b)La familia Apply
La familia apply reemplaza bucles con iteración funcional — más idiomático y a menudo más rápido. lapply siempre devuelve una lista; sapply intenta simplificar a un vector/matriz (conveniente pero impredecible); vapply es la versión segura con un tipo de retorno garantizado. apply funciona en matrices (MARGIN=1 para filas, 2 para columnas). tapply agrupa datos por un factor y aplica una función — como un mini GROUP BY. replicate repite simulaciones aleatorias. Para data frames, el paquete purrr (tidyverse) ofrece una familia map() más limpia y consistente.
# lapply: apply function to each element of a list, returns list
my_list <- list(a = 1:3, b = 4:6, c = 7:9)
lapply(my_list, mean) # list: 2, 5, 8
lapply(my_list, sum) # list: 6, 15, 24
# sapply: like lapply but simplifies to vector/matrix
sapply(my_list, mean) # 2 5 8 (named vector)
sapply(my_list, range) # matrix with 2 rows
# vapply: like sapply but with guaranteed output type (safer)
vapply(my_list, mean, numeric(1)) # always numeric vector
# apply: apply function over matrix/array margins
m <- matrix(1:12, nrow = 3)
apply(m, 1, sum) # row sums: 22 26 30
apply(m, 2, mean) # column means: 2 5 8 11
apply(m, c(1, 2), sqrt) # element-wise sqrt
# mapply: multivariate apply (vectorized over multiple args)
mapply(rep, 1:3, 3:1) # rep(1,3), rep(2,2), rep(3,1)
# tapply: apply function by groups
df <- data.frame(
group = c("A", "A", "B", "B", "B"),
value = c(10, 20, 30, 40, 50)
)
tapply(df$value, df$group, mean) # A: 15, B: 40
# replicate: repeat an expression n times
replicate(3, mean(rnorm(10))) # 3 random meansÁmbito y entornos
R usa ámbito léxico: las funciones buscan variables libres en el entorno donde fueron definidas (no donde se llaman). Esto habilita closures — funciones que capturan su entorno envolvente. El operador <<- asigna a una variable en el entorno padre (super-asignación), que es como los closures mantienen estado (como el ejemplo del contador). Cada llamada a función crea un nuevo entorno. La ruta de búsqueda (search()) determina dónde R busca objetos — globalenv es su espacio de trabajo, seguido de los paquetes adjuntos.
# lexical scoping: functions look up variables in defining environment
y <- 10
f <- function(x) {
x + y # y found in global env
}
f(5) # 15
# local variables shadow globals
g <- function(x) {
y <- 100 # local y
x + y
}
g(5) # 105
y # 10 (global unchanged)
# <<- assigns to parent environment (super-assignment)
counter <- function() {
count <- 0
function() {
count <<- count + 1 # modifies count in enclosing scope
count
}
}
c1 <- counter()
c1() # 1
c1() # 2
# search path for variables
search() # shows environments: globalenv, package namespaces
ls() # list objects in current environment
ls(envir = .GlobalEnv) # explicitly global
exists("y") # TRUE if variable exists
# new environment (isolated scope)
e <- new.env()
e$x <- 42
e$x # 42 (separate from global x)Manipulación de datos (dplyr y tidyr)
dplyr: Filter, Select y Arrange
dplyr (parte de tidyverse) proporciona verbos intuitivos para manipulación de datos que reflejan operaciones SQL. filter selecciona filas por condición; select elige columnas; arrange ordena. El operador %in% prueba pertenencia. Funciones helper como starts_with, ends_with, contains hacen la selección de columnas flexible. rename() cambia nombres de columnas sin copiar. Estos verbos se componen con el pipe (%>%) para pipelines de datos legibles. dplyr es mucho más rápido que R base para datos grandes porque usa C++ internamente.
library(dplyr)
df <- data.frame(
name = c("Alice", "Bob", "Carol", "Dan"),
age = c(30, 25, 28, 35),
dept = c("Eng", "Sales", "Eng", "Sales"),
salary = c(80000, 50000, 75000, 90000)
)
# filter rows (like WHERE in SQL)
filter(df, age > 26)
filter(df, dept == "Eng" & salary > 70000)
filter(df, dept %in% c("Eng", "Sales"))
filter(df, age > 26 | salary > 85000)
# select columns (like SELECT in SQL)
select(df, name, age)
select(df, name:dept) # range of columns
select(df, -salary) # exclude column
select(df, starts_with("s")) # columns starting with 's'
select(df, ends_with("e")) # columns ending with 'e'
select(df, contains("am")) # columns containing 'am'
select(df, everything()) # all columns (useful for reordering)
# arrange (sort, like ORDER BY)
arrange(df, age) # ascending
arrange(df, desc(age)) # descending
arrange(df, dept, desc(salary)) # sort by dept, then salary desc
# rename columns
rename(df, years = age, department = dept)
# distinct rows
distinct(df, dept) # unique departments
distinct(df, dept, .keep_all = TRUE) # keep all columnsdplyr: Mutate, Summarize y Group By
mutate añade o modifica columnas (vectorizado). summarize reduce cada grupo a una única fila resumen. El poder viene de group_by + summarize — el equivalente de R a SQL GROUP BY. n() cuenta filas; across() aplica una función a múltiples columnas (nuevo en dplyr 1.0). Las funciones de ventana (rank, cumsum, lag, lead) operan dentro de grupos, habilitando cálculos como 'rank dentro de departamento'. El pipe %>% encadena operaciones de izquierda a derecha, haciendo los pipelines complejos legibles.
library(dplyr)
# mutate: add/modify columns
df %>%
mutate(
bonus = salary * 0.1,
total = salary + bonus,
category = ifelse(age > 30, "senior", "junior")
)
# transmute: like mutate but only keeps new columns
transmute(df, name, annual = salary, monthly = salary / 12)
# summarize (reduces to single row per group)
summarize(df,
avg_salary = mean(salary),
max_age = max(age),
n = n()
)
# group_by + summarize (like GROUP BY in SQL)
df %>%
group_by(dept) %>%
summarize(
count = n(),
avg_salary = mean(salary),
avg_age = mean(age)
) %>%
arrange(desc(avg_salary))
# multiple summaries
df %>%
group_by(dept) %>%
summarize(across(everything(), list(mean, sd)))
# count and tally
count(df, dept) # count per dept
df %>% group_by(dept) %>% tally()
# window functions (within groups)
df %>%
group_by(dept) %>%
mutate(
rank = rank(desc(salary)),
cumsum = cumsum(salary)
) %>%
arrange(dept, rank)El operador Pipe (%>%)
El pipe (%>% de magrittr/dplyr) pasa el lado izquierdo como primer argumento al lado derecho, transformando llamadas a funciones anidadas en pipelines legibles de izquierda a derecha. Esta es la característica definitoria del estilo tidyverse. El punto (.) representa los datos del pipe cuando los necesita explícitamente. %$% expone nombres de columnas; %<>% asigna de vuelta; %T>% continúa el pipe tras un efecto secundario (como graficar). R 4.1+ tiene un pipe nativo |>, pero es menos flexible (sin placeholder de punto). Los pipes hacen el código de data wrangling dramáticamente más legible.
library(magrittr) # or library(dplyr)
# without pipe: nested, hard to read
result <- arrange(
filter(
select(df, name, age, salary),
age > 25
),
desc(salary)
)
# with pipe: linear, readable
result <- df %>%
select(name, age, salary) %>%
filter(age > 25) %>%
arrange(desc(salary))
# the dot (.) refers to the piped data
df %>% plot(.$age, .$salary) # . = df
# %$% exposes column names (magrittr)
df %$% cor(age, salary) # correlation
# %<>% assigns result back (compound assignment)
df %<>% filter(age > 20)
# T pipe: returns first argument (for side effects)
rnorm(100) %T>%
hist() %>% # plot histogram (returns input)
mean() # then compute mean
# native pipe (R 4.1+): |>
df |>
subset(age > 25) |>
colMeans()Unión de Data Frames
Las funciones join de dplyr reflejan los joins de SQL: inner_join (intersección), left_join (todas las filas izquierdas), right_join, full_join (unión). semi_join filtra a filas con coincidencias (sin añadir columnas); anti_join encuentra filas sin coincidencias — ambos útiles para validación de datos. El argumento by especifica la clave de join; use un vector con nombre (c('id' = 'emp_id')) cuando los nombres de columna difieren. Los joins son mucho más rápidos que merge() de R base. Siempre compruebe los conteos de filas antes y después de unir para detectar duplicados inesperados.
library(dplyr)
employees <- data.frame(
id = c(1, 2, 3, 4),
name = c("Alice", "Bob", "Carol", "Dan")
)
salaries <- data.frame(
id = c(1, 2, 3, 5),
salary = c(80000, 50000, 75000, 60000)
)
# inner join: only matching rows
inner_join(employees, salaries, by = "id")
# id name salary
# 1 Alice 80000
# 2 Bob 50000
# 3 Carol 75000
# left join: all rows from left, NA for non-matches
left_join(employees, salaries, by = "id")
# Dan has NA salary
# right join: all rows from right
right_join(employees, salaries, by = "id")
# id 5 has NA name
# full join: all rows from both
full_join(employees, salaries, by = "id")
# different column names
left_join(employees, salaries, by = c("id" = "id"))
# semi join: rows in left that have a match (no right columns)
semi_join(employees, salaries, by = "id")
# anti join: rows in left with NO match
anti_join(employees, salaries, by = "id")
# Dan (id 4 has no salary)
# multiple keys
left_join(df1, df2, by = c("dept" = "department", "year" = "yr"))tidyr: Reshape de datos
tidyr (tidyverse) maneja el reshape de datos. pivot_longer/wider reemplazan los heredados gather/spread — son más intuitivos y flexibles. Los datos tidy tienen una fila por observación y una columna por variable; pivot_longer convierte datos wide a este formato (necesario para ggplot2). separate/unite dividen y combinan columnas. separate_rows explota valores delimitados en múltiples filas. drop_na/replace_na/fill manejan datos faltantes limpiamente. Estos verbos se componen con dplyr via el pipe para pipelines de datos potentes.
library(tidyr)
# wide to long
wide_df <- data.frame(
id = 1:2,
q1_sales = c(100, 200),
q2_sales = c(150, 250),
q3_sales = c(120, 220)
)
long_df <- wide_df %>%
pivot_longer(
cols = starts_with("q"),
names_to = "quarter",
values_to = "sales"
)
# long to wide
long_df %>%
pivot_wider(
names_from = quarter,
values_from = sales
)
# separate one column into multiple
df <- data.frame(id = 1, name = "Alice_Smith")
df %>% separate(name, into = c("first", "last"), sep = "_")
# unite multiple columns into one
df %>% unite("full_name", first, last, sep = " ")
# separate_rows: split delimited values into rows
df <- data.frame(id = 1:2, tags = c("a,b,c", "x,y"))
df %>% separate_rows(tags, sep = ",")
# drop NA values
df %>% drop_na() # drop rows with any NA
df %>% drop_na(salary) # drop rows where salary is NA
# replace NA with a value
df %>% replace_na(list(salary = 0, name = "Unknown"))
# fill missing values (carry forward)
df %>% fill(salary, .direction = "down")Estadística y modelado
Estadística descriptiva
summary() es la forma más rápida de obtener una visión general de cualquier dato — muestra min, cuartiles, mediana, media y max. sd() y var() computan las estadísticas de MUESTRA (n-1). cor() mide asociación lineal (Pearson) o de rango (Spearman). Siempre use na.rm=TRUE con datos del mundo real que contienen NAs. Para data frames, summary(df) da estadísticas por columna. Los paquetes psych y Hmisc proporcionan estadística descriptiva extendida.
data <- c(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
# central tendency
mean(data) # 5.5
median(data) # 5.5
# R has no built-in mode; use:
as.numeric(names(sort(table(data), decreasing = TRUE)[1]))
# spread
sd(data) # 3.028 (sample standard deviation)
var(data) # 9.167 (sample variance)
IQR(data) # 5 (interquartile range)
range(data) # 1 10
diff(range(data)) # 9
# quantiles
quantile(data) # 0% 25% 50% 75% 100%
quantile(data, c(0.1, 0.9)) # 10th and 90th percentiles
# summary (all at once)
summary(data)
# Min. 1st Qu. Median Mean 3rd Qu. Max.
# 1.00 3.25 5.50 5.50 7.75 10.00
# correlation and covariance
x <- c(1, 2, 3, 4, 5)
y <- c(2, 4, 5, 4, 5)
cor(x, y) # 0.77 (Pearson correlation)
cor(x, y, method = "spearman") # rank correlation
cov(x, y) # covariance
# handling NA
data_na <- c(1, 2, NA, 4, 5)
mean(data_na, na.rm = TRUE) # 3
sd(data_na, na.rm = TRUE) # 1.826Distribuciones de probabilidad
R tiene todas las distribuciones comunes con una convención de nombres consistente: prefijo d/p/q/r + nombre de distribución. d da densidad (para continuas) o masa de probabilidad (para discretas); p da probabilidad acumulada; q da cuantiles (percentiles); r genera muestras aleatorias. Distribuciones comunes: norm (normal), binom (binomial), pois (Poisson), unif (uniforme), exp (exponencial), t, chisq, f. Siempre llame set.seed() antes de operaciones aleatorias para resultados reproducibles. sample() extrae elementos aleatorios de un vector.
# R uses d/p/q/r prefix for distributions:
# d = density (PDF/PMF)
# p = cumulative (CDF: P(X <= x))
# q = quantile (inverse CDF)
# r = random samples
# Normal distribution
dnorm(0) # 0.399 (density at 0)
pnorm(1.96) # 0.975 (P(Z <= 1.96))
qnorm(0.975) # 1.96 (97.5th percentile)
rnorm(5, mean = 100, sd = 15) # 5 random samples
# Binomial distribution
dbinom(3, size = 10, prob = 0.5) # P(X=3)
pbinom(3, size = 10, prob = 0.5) # P(X<=3)
rbinom(10, size = 1, prob = 0.5) # 10 coin flips
# Poisson distribution
dpois(2, lambda = 3) # P(X=2) with rate 3
rpois(100, lambda = 5) # 100 random samples
# Uniform distribution
runif(5, min = 0, max = 1) # 5 uniform random numbers
# Exponential
rexp(10, rate = 0.5) # 10 exponential samples
# set random seed for reproducibility
set.seed(42)
rnorm(3) # same results every time with same seed
# sample from a vector
sample(1:10, 5) # 5 random numbers without replacement
sample(1:10, 5, replace = TRUE) # with replacement
sample(c("A", "B", "C"), 2) # sample from categoriesPruebas de hipótesis
R hace las pruebas de hipótesis sencillas. t.test compara medias (una muestra, dos muestras o pareadas). El p-value < 0.05 típicamente indica significancia estadística. var.equal=TRUE asume varianzas iguales (t de Student); el valor por defecto es Welch (más robusto). chisq.test comprueba independencia de variables categóricas. wilcox.test es la alternativa no paramétrica (sin asunción de normalidad). aov realiza ANOVA para comparar 3+ grupos. Todas las funciones de prueba devuelven una lista con $p.value, $statistic, $conf.int que puede extraer programáticamente.
# one-sample t-test (is mean different from hypothesized?)
data <- c(5.1, 4.9, 5.0, 5.2, 4.8, 5.1, 5.0)
t.test(data, mu = 5.0)
# t = 0.527, df = 6, p-value = 0.616
# (fail to reject H0: mean = 5)
# two-sample t-test (are two means different?)
group1 <- c(5.1, 4.9, 5.0, 5.2)
group2 <- c(4.5, 4.7, 4.6, 4.4)
t.test(group1, group2)
t.test(group1, group2, var.equal = TRUE) # assume equal variance
# paired t-test (before/after)
before <- c(70, 80, 65, 90, 75)
after <- c(75, 85, 70, 92, 80)
t.test(before, after, paired = TRUE)
# chi-squared test (independence of categorical vars)
tbl <- table(c("A","A","B","B"), c("X","Y","X","Y"))
chisq.test(tbl)
# Wilcoxon (non-parametric alternative to t-test)
wilcox.test(group1, group2)
# ANOVA (compare means across multiple groups)
df <- data.frame(
value = c(5, 6, 7, 8, 9, 10),
group = factor(c("A","A","B","B","C","C"))
)
result <- aov(value ~ group, data = df)
summary(result) # F-test p-value
# extract p-value from any test
test_result <- t.test(data, mu = 5)
test_result$p.value # 0.616Regresión lineal (lm)
lm() ajusta modelos lineales usando sintaxis de fórmula: y ~ x (simple), y ~ x1 + x2 (múltiple), y ~ . (todas las columnas), y ~ x1*x2 (con interacción), y ~ I(x^2) (transformaciones). summary() muestra coeficientes, errores estándar, t-values, p-values, R² y el F-test. Los factores se convierten automáticamente en variables dummy. El mini-lenguaje de fórmulas es potente: - elimina términos, : es interacción, * es efectos principales + interacción. Siempre examine los gráficos diagnósticos (residuos, Q-Q plot) para comprobar asunciones del modelo.
# simple linear regression: y ~ x
x <- c(1, 2, 3, 4, 5)
y <- c(2, 4, 5, 4, 5)
model <- lm(y ~ x)
# view results
print(model) # coefficients
summary(model) # full summary with R², t-tests, F-test
coef(model) # (Intercept) 2.2, x 0.6
fitted(model) # predicted values
residuals(model) # y - predicted
confint(model) # 95% confidence intervals
# make predictions
predict(model, newdata = data.frame(x = c(6, 7)))
# multiple regression
df <- data.frame(
y = c(10, 12, 15, 18, 20),
x1 = c(1, 2, 3, 4, 5),
x2 = c(5, 4, 3, 2, 1)
)
model2 <- lm(y ~ x1 + x2, data = df)
model3 <- lm(y ~ ., data = df) # all predictors
model4 <- lm(y ~ x1 + I(x1^2), data = df) # polynomial
# interactions
model5 <- lm(y ~ x1 * x2, data = df) # x1 + x2 + x1:x2
# diagnostic plots
par(mfrow = c(2, 2))
plot(model) # 4 diagnostic plots
# with factors (automatic dummy variables)
model6 <- lm(y ~ x1 + factor(group), data = df)GLM y otros modelos
glm() generaliza lm() a resultados no normales via el argumento family: binomial (regresión logística para binario), poisson (datos de conteo), gaussian (igual que lm). predict() con type='response' da probabilidades (no log-odds) para modelos logísticos. step() realiza selección automática de variables (basada en AIC). anova(model1, model2) prueba si el modelo más grande es significativamente mejor. Para métodos avanzados, R tiene paquetes para todo: lme4 (modelos mixtos), survival (Kaplan-Meier, Cox), randomForest, caret (pipeline ML), glmnet (regularización).
# logistic regression (binary outcome)
df <- data.frame(
admit = c(0, 1, 0, 1, 1, 0),
gre = c(380, 660, 800, 640, 520, 760),
gpa = c(3.6, 3.7, 3.8, 3.9, 3.5, 3.6)
)
logit <- glm(admit ~ gre + gpa, data = df, family = binomial)
summary(logit)
# predict probabilities (type = "response")
predict(logit, newdata = df, type = "response")
# Poisson regression (count data)
counts <- c(2, 3, 1, 4, 2, 5)
pois <- glm(counts ~ x, family = poisson)
# stepwise model selection
full_model <- lm(y ~ x1 + x2 + x3, data = df)
step_model <- step(full_model) # AIC-based selection
# anova to compare models
model1 <- lm(y ~ x1, data = df)
model2 <- lm(y ~ x1 + x2, data = df)
anova(model1, model2) # is x2 significant?
# other models
# nls() - nonlinear least squares
# glm.nb() - negative binomial (MASS package)
# lme()/lmer() - mixed effects (nlme/lme4)
# rpart()/randomForest() - tree-based methods
# coxph() - survival analysis (survival package)
# cross-validation
library(boot)
cv_result <- cv.glm(df, logit, K = 10) # 10-fold CV
cv_result$delta # cross-validation errorGráficos (R base y ggplot2)
R base: plot, hist y boxplot
Los gráficos de R base son rápidos y suficientes para análisis exploratorio. plot() es genérico — despacha basado en el tipo de entrada (scatter para dos vectores, boxplot para una fórmula). type controla el estilo de punto/línea; pch establece el símbolo de punto; lty establece el tipo de línea. par(mfrow=c(r,c)) organiza múltiples gráficos en una cuadrícula. hist() con freq=FALSE muestra densidad (para poder superponer una curva de densidad). Para gráficos de calidad de publicación, use ggplot2. Los gráficos de R base son imperativos — los construye paso a paso.
# scatter plot
x <- 1:10
y <- x^2
plot(x, y, type = "p", # p=points, l=lines, b=both, o=overplotted
main = "Quadratic", # title
xlab = "x", ylab = "y", # axis labels
col = "blue", pch = 16, # color and point character
xlim = c(0, 10), ylim = c(0, 100))
# add lines and points to existing plot
lines(x, x*10, col = "red", lty = 2) # dashed line
points(x, y + 5, col = "green")
legend("topleft", legend = c("x^2", "10x"),
col = c("blue", "red"), lty = c(NA, 2), pch = c(16, NA))
# histogram
data <- rnorm(1000)
hist(data, breaks = 30, col = "lightblue",
main = "Normal Distribution", xlab = "Value", freq = FALSE)
lines(density(data), col = "red", lwd = 2) # add density curve
# boxplot (compare groups)
boxplot(count ~ spray, data = InsectSprays,
col = "lightgreen", main = "Insect Count by Spray")
# barplot
counts <- table(mtcars$cyl)
barplot(counts, col = c("red","green","blue"),
main = "Cars by Cylinders", xlab = "Cylinders")
# multiple plots in one window
par(mfrow = c(2, 2)) # 2x2 grid
plot(x, y); hist(data); boxplot(data); plot(density(data))
par(mfrow = c(1, 1)) # resetggplot2: Gramática de gráficos
ggplot2 (tidyverse) implementa la Gramática de Gráficos: los gráficos se construyen a partir de capas (datos, estéticas, geometría, escalas, facets, temas) combinadas con +. aes() mapea columnas de datos a propiedades visuales (x, y, color, size). Cada geom_* añade una capa: geom_point (scatter), geom_line, geom_bar, geom_histogram, geom_boxplot, geom_smooth (línea de tendencia). Este enfoque por capas significa que puede construir gráficos complejos incrementalmente. A diferencia de R base, ggplot2 es declarativo — describe qué quiere, no cómo dibujarlo.
library(ggplot2)
# basic structure: data + aesthetic mapping + geometry
ggplot(data = mtcars, aes(x = wt, y = mpg)) +
geom_point()
# add aesthetics (color, size, shape mapped to data)
ggplot(mtcars, aes(x = wt, y = mpg, color = cyl, size = hp)) +
geom_point()
# add layers
ggplot(mtcars, aes(x = wt, y = mpg)) +
geom_point(color = "blue", size = 3) +
geom_smooth(method = "lm", se = TRUE) + # regression line
labs(title = "MPG vs Weight",
x = "Weight (1000 lbs)", y = "MPG",
color = "Cylinders") +
theme_minimal()
# the + operator adds layers (like %>% but for ggplot)
p <- ggplot(mtcars, aes(x = wt, y = mpg)) +
geom_point()
p + geom_smooth() # add to saved plot
p + facet_wrap(~ cyl) # facet by cylinders
# key components:
# data - the data frame
# aes() - aesthetic mappings (x, y, color, size, shape)
# geom_*() - geometry (point, line, bar, histogram, etc.)
# scale_*() - control axes, colors, legends
# facet_*() - small multiples (subplots)
# theme_*() - overall appearance
# labs() - titles and labelsggplot2: Geoms y estéticas
ggplot2 tiene docenas de geoms para cada tipo de gráfico. geom_bar/geom_col para barras, geom_histogram/geom_density para distribuciones, geom_boxplot para comparaciones, geom_line/geom_area para series temporales. stat_summary computa y grafica resúmenes (media, barras de error). alpha controla transparencia (0-1) — esencial para puntos superpuestos. coord_flip rota el gráfico. geom_jitter añade ruido para prevenir overplotting. Cada geom tiene estéticas específicas que entiende (p. ej., geom_point necesita x e y; geom_bar solo necesita x).
library(ggplot2)
# bar chart (counts)
ggplot(mpg, aes(class)) +
geom_bar(fill = "steelblue") +
coord_flip() # horizontal bars
# histogram
ggplot(mpg, aes(hwy)) +
geom_histogram(binwidth = 2, fill = "orange", color = "black")
# density plot
ggplot(mpg, aes(hwy, fill = class)) +
geom_density(alpha = 0.5) # semi-transparent
# boxplot by group
ggplot(mpg, aes(class, hwy)) +
geom_boxplot() +
geom_jitter(width = 0.2, alpha = 0.5) # overlay points
# line plot (time series)
ggplot(economics, aes(date, unemploy)) +
geom_line(color = "red") +
geom_area(fill = "pink", alpha = 0.3)
# scatter with smoothing
ggplot(mpg, aes(displ, hwy, color = drv)) +
geom_point(size = 2) +
geom_smooth(method = "loess", se = FALSE)
# error bars
ggplot(df, aes(group, value)) +
stat_summary(fun = mean, geom = "bar") +
stat_summary(fun.data = mean_se, geom = "errorbar")
# text labels
ggplot(mtcars, aes(wt, mpg, label = rownames(mtcars))) +
geom_text(size = 3)ggplot2: Facets, escalas y temas
Las facets crean small multiples — un subgráfico por categoría — la mejor forma de comparar grupos. facet_wrap(~var) crea una cinta 1D; facet_grid(row~col) crea una cuadrícula 2D. Las escalas controlan cómo los datos se mapean a propiedades visuales: scale_x_log10 para ejes logarítmicos, scale_color_brewer para paletas aptas para daltónicos, scale_fill_manual para colores personalizados. Los temas controlan elementos no de datos (fuentes, líneas de cuadrícula, leyenda). theme_minimal/bw/classic son presets; theme() personaliza elementos individuales. ggsave exporta a PNG/PDF/SVG con control sobre tamaño y DPI.
library(ggplot2)
# facets: small multiples (subplots by a variable)
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~ class) # one panel per class
# facet_grid: 2D grid of panels
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_grid(drv ~ cyl) # rows ~ cols
# scales: control axes and colors
ggplot(mpg, aes(displ, hwy, color = class)) +
geom_point() +
scale_x_log10() + # log scale
scale_color_brewer(palette = "Set1") + # color palette
scale_size_continuous(range = c(2, 8))
# manual colors
ggplot(mpg, aes(class, fill = drv)) +
geom_bar() +
scale_fill_manual(values = c("red", "green", "blue"))
# themes: overall appearance
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
theme_minimal() + # or theme_classic, theme_bw
theme(
text = element_text(size = 14, family = "serif"),
plot.title = element_text(face = "bold", hjust = 0.5),
axis.text.x = element_text(angle = 45, hjust = 1),
legend.position = "bottom",
panel.grid.major = element_line(color = "gray90")
)
# save plot
ggsave("plot.png", width = 8, height = 6, dpi = 300)
ggsave("plot.pdf", device = "pdf")Guardar y exportar datos
RDS es mejor para guardar un único objeto R (preserva tipos, rápido, compacto). RData guarda múltiples objetos. CSV es el más portable (legible por Excel, Python, etc.) pero pierde información de tipo — siempre establezca stringsAsFactors=FALSE. El paquete readr (tidyverse) proporciona E/S CSV más rápida y consistente que devuelve tibbles (data frames mejorados). readxl/writexl manejan Excel. Para datasets grandes, considere data.table::fread (muy rápido) o parquet (paquete arrow) para almacenamiento columnar. Siempre use row.names=FALSE al escribir CSVs.
# save single object to RDS (binary, preserves type)
saveRDS(my_data, "data.rds")
restored <- readRDS("data.rds")
# save multiple objects to RData
save(df1, df2, model, file = "workspace.RData")
load("workspace.RData") # loads all objects into workspace
# CSV (most portable)
write.csv(df, "data.csv", row.names = FALSE)
df <- read.csv("data.csv")
df <- read.csv("data.csv", stringsAsFactors = FALSE)
df <- read.csv("data.csv", na.strings = c("", "NA", "N/A"))
# readr (tidyverse): faster, smarter CSV I/O
library(readr)
write_csv(df, "data.csv")
df <- read_csv("data.csv") # returns a tibble
df <- read_csv("data.csv", col_types = cols(
age = col_integer(),
name = col_character()
))
# Excel (requires readxl/writexl packages)
library(readxl)
df <- read_excel("data.xlsx", sheet = 1, range = "A1:D100")
library(writexl)
write_xlsx(df, "output.xlsx")
# RDS vs RData vs CSV
# RDS: one object, binary, fast, preserves types
# RData: multiple objects, binary, fast
# CSV: text, portable to other tools, loses type info
# save and load workspace
save.image("project.RData") # save everything
load("project.RData") # restore everything
# serialize to JSON (jsonlite package)
library(jsonlite)
write_json(df, "data.json")
df <- fromJSON("data.json")OOP y programación funcional
Clases y métodos S3 (OOP simple)
S3 es el sistema OOP más común de R — ligero e informal. Una 'clase' es solo una lista con un atributo de clase; los métodos son funciones llamadas generic.classname. UseMethod() dentro de un genérico despacha al método apropiado basado en la clase del objeto. La mayoría de objetos R (data.frame, lm, ggplot) son S3. print(), summary(), plot() son genéricos que puede extender. S3 es informal (sin validación), lo que lo hace flexible pero propenso a errores. Use methods(generic) para ver todos los métodos, methods(class='x') para métodos de una clase.
# S3 is R's simplest OOP system: a class is just an attribute
# create an object: list + class attribute
account <- list(owner = "Alice", balance = 1000)
class(account) <- "BankAccount"
# generic function dispatches by class
print.BankAccount <- function(x, ...) {
cat("Account owner:", x$owner, "\n")
cat("Balance: $", x$balance, "\n", sep = "")
}
print(account)
# Account owner: Alice
# Balance: $1000
# define a custom method for an existing generic
deposit <- function(obj, amount) {
UseMethod("deposit") # dispatch based on class of obj
}
deposit.BankAccount <- function(obj, amount) {
obj$balance <- obj$balance + amount
obj
}
account <- deposit(account, 500)
account$balance # 1500
# check available methods for a generic
methods(print) # all print methods
methods(class = "lm") # methods for lm objects
# default method (fallback)
deposit.default <- function(obj, amount) {
stop("No deposit method for this class")
}Clases S4 (OOP formal)
S4 es el sistema OOP formal de R: las clases tienen slots (campos) definidos con tipos, validación y herencia. Definidas con setClass(); instanciadas con new(). Acceda a los slots con @ (no $). setMethod() define métodos para genéricos. setValidity() impone restricciones. S4 es usado por Bioconductor y paquetes que necesitan contratos estrictos (p. ej., Matrix, sp). S4 es más robusto pero más verboso que S3. El R del día a día usa S3; recurra a S4 cuando necesite seguridad de tipos y herencia formal.
# S4 is formal: classes are defined with setClass and validated
setClass("Person",
slots = list(
name = "character",
age = "numeric"
),
prototype = list(name = NA_character_, age = NA_real_)
)
# constructor: new(ClassName, ...)
alice <- new("Person", name = "Alice", age = 30)
# access slots with @ (not $)
alice@name # "Alice"
slot(alice, "age") # 30
# define a method
setMethod("show", "Person", function(object) {
cat("Person:", object@name, "(", object@age, "y)\n")
})
show(alice) # Person: Alice ( 30 y)
# validity check
setValidity("Person", function(object) {
if (object@age < 0) return("age must be non-negative")
TRUE
})
# new("Person", name="Bob", age=-5) # error
# inheritance
setClass("Student", contains = "Person",
slots = list(gpa = "numeric"))
stu <- new("Student", name="Bob", age=20, gpa=3.8)Closures y fábricas de funciones
Un closure es una función que retiene acceso a variables en su entorno de definición — la forma principal de R de crear funciones con estado y encapsular datos privados. El operador <<- asigna en el entorno envolvente (padre), no en el local. Las fábricas de funciones (funciones que devuelven funciones) son potentes para crear funciones especializadas. Usos comunes: contadores, memoización (cachear resultados) y el patrón módulo (devolver una lista de funciones que comparten estado privado). Este es el análogo más cercano de R a clases con campos privados.
# a closure is a function that remembers its enclosing environment
# function factory: creates functions with private state
make_counter <- function() {
count <- 0
function() {
count <<- count + 1 # <<- modifies in enclosing env
count
}
}
c1 <- make_counter()
c1() # 1
c1() # 2
c1() # 3
c2 <- make_counter()
c2() # 1 (independent counter)
# memoization (cache expensive results)
memoize <- function(f) {
cache <- new.env(parent = emptyenv())
function(x) {
key <- as.character(x)
if (!exists(key, envir = cache)) {
assign(key, f(x), envir = cache)
}
get(key, envir = cache)
}
}
slow_sqrt <- function(x) { Sys.sleep(0.1); sqrt(x) }
fast_sqrt <- memoize(slow_sqrt)
fast_sqrt(16) # slow first time
fast_sqrt(16) # instant second time
# capturing state in a list (module pattern)
bank_account <- function(initial) {
balance <- initial
list(
deposit = function(amt) { balance <<- balance + amt },
withdraw = function(amt) { balance <<- balance - amt },
get_balance = function() balance
)
}
acc <- bank_account(100)
acc$deposit(50)
acc$get_balance() # 150Programación funcional: Map/Reduce/Filter
R tiene primitivas de programación funcional integradas: Map (aplicar función a cada elemento), Reduce (fold/acumular), Filter (mantener elementos coincidentes), Find/Position (buscar), Negate (invertir predicado). Estas devuelven listas o vectores y evitan bucles explícitos. El paquete purrr (tidyverse) proporciona una API más consistente: map_dbl/map_chr devuelven vectores tipados, keep/discard filtran, reduce acumula. La sintaxis de fórmula ~ .x crea funciones anónimas concisamente. La programación funcional hace el código más declarativo y fácil de razonar, especialmente para pipelines de transformación de datos.
# Map: apply a function to each element (returns list)
Map(function(x) x^2, 1:5)
# [[1]] 1 [[2]] 4 [[3]] 9 [[4]] 16 [[5]] 25
# Reduce: combine elements with a binary function
Reduce("+", 1:5) # 15 (1+2+3+4+5)
Reduce("*", 1:5) # 120 (factorial 5!)
Reduce(c, list(1:2, 3:4, 5:6)) # 1 2 3 4 5 6
# Filter: keep elements satisfying a predicate
Filter(function(x) x > 2, 1:5) # 3 4 5
Filter(is.numeric, list(1, "a", 2, "b")) # 1 2
# Negate: invert a predicate
Negate(is.null)
is_not_null <- Negate(is.null)
# Position: find first index satisfying a predicate
Position(function(x) x > 3, 1:10) # 4
# Find: first element satisfying a predicate
Find(function(x) x > 3, 1:10) # 4
# purrr (tidyverse): cleaner functional programming
library(purrr)
map_dbl(1:5, ~ .x^2) # 1 4 9 16 25 (vector output)
map_chr(c("a","b"), toupper) # "A" "B"
keep(1:5, ~ .x > 2) # 3 4 5
reduce(1:5, `+`) # 15
walk(1:3, print) # side effects onlyDepuración y manejo de errores
browser() es el depurador interactivo de R — insértelo en código para pausar e inspeccionar variables (n=next, c=continue, Q=quit). debug(fn) recorre una función línea por línea. traceback() muestra la pila de llamadas tras un fallo. tryCatch() es el try/catch de R: captura errores y advertencias via funciones handler, devolviendo un valor de respaldo. withCallingHandlers() maneja advertencias sin interrumpir la ejecución. Establecer options(warn=2) convierte advertencias en errores (útil para encontrar la fuente). options(error=browser) auto-entra al depurador en cualquier error no capturado. Dominar estas herramientas es esencial para diagnosticar problemas en código R complejo.
# browser(): pause execution and inspect
f <- function(x) {
browser() # pauses here; use n, c, Q, ls, print
y <- x * 2
if (y > 10) stop("y too big")
y
}
# f(6) # enters browser at the browser() line
# debug() / undebug(): debug a function
debug(lm)
# lm(y ~ x) # steps through lm line by line
undebug(lm)
# traceback(): see call stack after an error
# log("abc") # error
# traceback() # shows the call stack
# tryCatch(): handle errors gracefully
safe_log <- function(x) {
tryCatch(
log(x),
error = function(e) {
message("Error: ", conditionMessage(e))
NA_real_
},
warning = function(w) {
message("Warning: ", conditionMessage(w))
log(x) # retry or handle
}
)
}
safe_log("abc") # NA with message
safe_log(-1) # NA with warning (NaN)
# withCallingHandlers(): handle warnings without stopping
withCallingHandlers(
{ warn("oops"); 42 },
warning = function(w) { message("caught: ", w$message); invokeRestart("muffleWarning") }
)
# options for debugging
options(warn = 2) # warnings become errors (for debugging)
options(error = browser) # enter browser on error
options(warn = 0) # resetSeries temporales y forecasting
Creación de objetos de series temporales (ts)
ts() es la clase de series temporales de R base — un vector con atributos temporales (start, frequency). frequency codifica el periodo: 12 para mensual, 4 para trimestral, 52 para semanal. window() subconjunta por rango temporal. diff() computa diferencias (útil para hacer una serie estacionaria). lag() desplaza valores. aggregate() convierte a frecuencia menor (p. ej., mensual a trimestral). ts funciona bien para datos regulares de frecuencia fija. Para timestamps irregulares (p. ej., precios de acciones con huecos), use los paquetes xts/zoo.
# ts(): create a regular time series
# frequency: 12=monthly, 4=quarterly, 52=weekly, 365.25=daily
monthly_sales <- ts(c(30, 35, 40, 38, 42, 45, 50, 48, 52, 55, 60, 58),
start = c(2020, 1), frequency = 12)
print(monthly_sales)
# Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
# 2020 30 35 40 38 42 45 50 48 52 55 60 58
# attributes
start(monthly_sales) # 2020 1
end(monthly_sales) # 2020 12
frequency(monthly_sales) # 12
time(monthly_sales) # time points
# window(): subset a time range
q1 <- window(monthly_sales, start = c(2020, 3), end = c(2020, 5))
# multiple series (multivariate)
ts1 <- ts(rnorm(12), start = 2020, frequency = 12)
ts2 <- ts(rnorm(12), start = 2020, frequency = 12)
mts <- cbind(ts1, ts2) # multivariate ts
# lag and diff
lag(monthly_sales, k = 1) # shift by 1 period
diff(monthly_sales) # first differences (month-over-month change)
diff(monthly_sales, lag = 12) # year-over-year change
# aggregate to lower frequency
aggregate(monthly_sales, nfrequency = 4, FUN = mean) # monthly -> quarterlyDescomposición y suavizado
La descomposición de series temporales separa una serie en componentes de tendencia, estacional y residual. decompose() usa descomposición clásica de media móvil (aditiva o multiplicativa). stl() usa suavizado LOESS y es más robusto a outliers y maneja estacionalidad cambiante. HoltWinters() ajusta suavizado exponencial (nivel + tendencia + estación). El paquete forecast (ahora fable en tidyverse) proporciona forecast() para predicción con intervalos de confianza. Siempre grafique la descomposición para entender la estructura antes de modelar. La descomposición multiplicativa es apropiada cuando la amplitud estacional crece con la tendencia.
# decompose(): split a series into trend/seasonal/random
decomp <- decompose(monthly_sales, type = "additive")
# type = "additive" (T+S+R) or "multiplicative" (T*S*R)
plot(decomp) # shows observed, trend, seasonal, random panels
decomp$trend # trend component
decomp$seasonal # seasonal component
decomp$random # remainder
# stl(): Seasonal-Trend-Loess decomposition (more flexible)
stl_fit <- stl(monthly_sales, s.window = "periodic")
plot(stl_fit)
stl_fit$time.series[, "trend"] # extract trend
# moving average smoothing
ma3 <- filter(monthly_sales, filter = rep(1/3, 3), sides = 2)
ma12 <- filter(monthly_sales, filter = rep(1/12, 12), sides = 1)
# HoltWinters(): exponential smoothing
hw <- HoltWinters(monthly_sales, seasonal = "additive")
plot(hw)
hw$fitted # fitted values
hw$coefficients # alpha, beta, gamma
# forecast with HoltWinters
library(forecast)
fc <- forecast(hw, h = 12) # 12-step ahead forecast
plot(fc) # with prediction intervals
autoplot(fc) # ggplot2 versionACF, PACF y modelado ARIMA
Los gráficos ACF/PACF diagnostican la estructura de autocorrelación: ACF muestra correlación total en cada lag, PACF muestra correlación directa (parcial). Sus patrones sugieren órdenes ARIMA: ACF cortando sugiere MA, PACF cortando sugiere AR. adf.test() comprueba estacionariedad (p<0.05 significa estacionaria). auto.arima() (paquete forecast) selecciona automáticamente el mejor modelo ARIMA(p,d,q)(P,D,Q) por AICc. d es el orden de diferenciación (para lograr estacionariedad); (P,D,Q) son componentes estacionales. checkresiduals() verifica que el modelo ajusta bien (los residuos deberían ser ruido blanco). ARIMA es el caballo de batalla del forecasting de series temporales.
# ACF (autocorrelation) and PACF (partial autocorrelation)
acf(monthly_sales, main = "ACF") # correlation with lagged self
pacf(monthly_sales, main = "PACF") # direct correlation at each lag
# interpret:
# ACF tails off slowly -> need differencing (non-stationary)
# ACF cuts off at lag p -> AR(p) process
# PACF cuts off at lag q -> MA(q) process
# stationarity test
library(tseries)
adf.test(monthly_sales) # Augmented Dickey-Fuller
# p < 0.05 -> stationary
# auto.arima(): automatically select ARIMA order
library(forecast)
fit <- auto.arima(monthly_sales)
summary(fit) # shows ARIMA(p,d,q)(P,D,Q)[m]
# ARIMA(0,1,1)(1,0,0)[12] example output
# forecast
fc <- forecast(fit, h = 12)
plot(fc)
accuracy(fc) # ME, RMSE, MAE, MAPE
# manual ARIMA
fit2 <- arima(monthly_sales, order = c(1, 1, 1),
seasonal = list(order = c(1, 0, 0), period = 12))
# residuals should look like white noise
checkresiduals(fit) # Ljung-Box test
tsdisplay(residuals(fit)) # ACF + PACF of residualsxts y zoo (series temporales irregulares)
xts/zoo extienden ts para series temporales irregulares (p. ej., datos financieros con fines de semana/festivos faltantes). Los objetos xts se indexan por fechas/horas reales, habilitando subconjuntos intuitivos como prices['2024-01'] para todo enero. merge() alinea múltiples series por fecha, rellenando huecos con NA (use fill=na.locf para arrastrar hacia adelante). rollmean/rollapply computan estadísticas rodantes. endpoints/period.apply agregan a periodos más gruesos (semanas, meses). xts es la base de la mayoría de paquetes de finanzas de R (quantmod, TTR, PerformanceAnalytics). Para series temporales tidy, los paquetes tsibble/fable ofrecen una alternativa moderna.
library(xts)
library(zoo)
# create xts from a matrix and time index
dates <- as.Date(c("2024-01-01", "2024-01-03", "2024-01-10"))
prices <- xts(c(100, 102, 98), order.by = dates)
colnames(prices) <- "AAPL"
# subset by date range (very intuitive)
prices["2024-01-03"] # specific date
prices["2024-01-01/2024-01-05"] # date range
prices["2024-01"] # entire January
prices["/2024-01-05"] # up to a date
# lag and diff (returns xts)
lag(prices, k = 1) # previous day's price
diff(prices) # daily change
daily_returns <- diff(prices) / lag(prices, 1)
# rolling operations (zoo)
rollmean(prices, k = 3) # 3-day rolling mean
rollapply(prices, 3, sd) # 3-day rolling std dev
rollmax(prices, 3) # 3-day rolling max
# period.apply: aggregate by period
ep <- endpoints(prices, on = "weeks") # week endpoints
period.apply(prices, ep, mean) # weekly averages
# merge multiple series (aligns by date)
aapl <- xts(c(100, 102, 98), as.Date(c("2024-01-01","2024-01-02","2024-01-03")))
msft <- xts(c(200, 201), as.Date(c("2024-01-01","2024-01-03")))
merged <- merge(aapl, msft) # NA fills gaps
merged <- merge(aapl, msft, fill = na.locf) # carry forward
# to.ts <- as.ts(prices) # convert to base ts (loses dates)Evaluación y visualización de forecasting
Siempre evalúe pronósticos en un conjunto de test retenido, no in-sample. accuracy(fit, test) computa métricas de error: MAE y RMSE (escala absoluta), MAPE (porcentaje, sin escala pero inestable cerca de cero), MASE (escala por error de pronóstico naive; <1 significa mejor que naive). tsCV() realiza validación cruzada de series temporales (rolling origin). Compare múltiples modelos (baseline naive, ETS, ARIMA) y elija el de menor error. autoplot() + autolayer() visualizan pronósticos con intervalos de predicción. El pronóstico naive (último valor) es un baseline crítico — su modelo debe superarlo para ser útil. Nunca use divisiones train/test aleatorias para series temporales (filtran información futura); siempre divida cronológicamente.
library(forecast)
library(ggplot2)
# split into train/test
train <- window(monthly_sales, end = c(2020, 9))
test <- window(monthly_sales, start = c(2020, 10))
# fit multiple models
fit_naive <- naive(train, h = 3) # naive: last value
fit_snaive <- snaive(train, h = 3) # seasonal naive
fit_ets <- ets(train) %>% forecast(h = 3) # exponential smoothing
fit_arima <- auto.arima(train) %>% forecast(h = 3)
# compare accuracy on test set
accuracy(fit_naive, test)
accuracy(fit_arima, test)
# metrics: ME, RMSE, MAE, MPE, MAPE, MASE
# time series cross-validation
ts_cv <- tsCV(train, forecastfunction = naive, h = 3)
sqrt(mean(ts_cv^2, na.rm = TRUE)) # CV RMSE
# visualize forecasts with ggplot
autoplot(monthly_sales, series = "Actual") +
autolayer(fit_arima, series = "ARIMA", PI = FALSE) +
autolayer(fit_ets, series = "ETS", PI = FALSE) +
labs(title = "Forecast Comparison", x = "Time", y = "Sales") +
theme_minimal()
# prediction intervals
fc <- forecast(auto.arima(train), h = 3, level = c(80, 95))
autoplot(fc) +
autolayer(test, series = "Actual")
# accuracy measures explained:
# MAE = Mean Absolute Error (same units as data)
# RMSE = Root Mean Squared Error (penalizes large errors)
# MAPE = Mean Absolute Percentage Error (scale-free, %)
# MASE = Mean Absolute Scaled Error (< 1 beats naive)dplyr en profundidad
Verbos core: filter, select, mutate, summarize
Los cinco verbos core de dplyr cubren la mayoría de manipulación de datos: filter (filas por condición), select (columnas por nombre), mutate (añadir/transformar columnas), summarize (colapsar a estadísticas resumen) y arrange (ordenar). El pipe %>% (o nativo |>) encadena operaciones de izquierda a derecha, haciendo el código legible. Funciones helper como starts_with, ends_with, contains y everything() hacen la selección de columnas concisa. Siempre group_by antes de summarize para estadísticas por grupo; n() cuenta filas por grupo.
library(dplyr)
df <- tibble(
name = c("Alice","Bob","Carol","Dave"),
dept = c("Eng","Sales","Eng","Sales"),
salary = c(90000, 70000, 95000, 72000),
years = c(5, 3, 8, 4)
)
# filter rows
eng <- df %>% filter(dept == "Eng", salary > 85000)
# select columns (with helpers)
df %>% select(name, salary)
df %>% select(starts_with("s"))
df %>% select(-years)
df %>% select(name:dept) # range
df %>% select(dept, everything())# reorder
# mutate: add/modify columns
df %>% mutate(
bonus = salary * 0.1,
total = salary + bonus,
level = if_else(years >= 5, "Senior", "Junior")
)
# transmute: keep only new columns
df %>% transmute(name, annual_k = salary / 1000)
# summarize (with group_by)
df %>%
group_by(dept) %>%
summarize(
avg_salary = mean(salary),
max_years = max(years),
n = n()
)Joins y operaciones de conjuntos
Los joins de dplyr reflejan SQL: inner, left, right, full para combinar; semi y anti para filtrar por otra tabla. Siempre especifique by para evitar coincidencias silenciosas en columnas no deseadas. Para nombres de clave diferentes use by = c('left_col' = 'right_col'). Las operaciones de conjuntos (union, intersect, setdiff) requieren conjuntos de columnas idénticos. bind_rows apila (rellenando columnas faltantes con NA); bind_cols pega lado a lado sin comprobar claves — usualmente quiere un join. Los joins son la fuente más común de bugs de datos sutiles, así que verifique los conteos de filas antes y después.
employees <- tibble(id = 1:4, name = c("Al","Bo","Cy","Di"))
salaries <- tibble(id = c(1,2,4,5), salary = c(50,60,80,90))
# mutating joins (combine columns)
inner_join(employees, salaries, by = "id") # only matching ids
left_join(employees, salaries, by = "id") # all from left
right_join(employees, salaries, by = "id") # all from right
full_join(employees, salaries, by = "id") # all rows
# filtering joins (filter rows, no new columns)
semi_join(employees, salaries, by = "id") # rows in left with match
anti_join(employees, salaries, by = "id") # rows in left WITHOUT match
# different column names
left_join(x, y, by = c("emp_id" = "id"))
# set operations (require identical columns)
union(a, b) # unique rows in either
union_all(a, b) # all rows (with dups)
intersect(a, b) # rows in both
setdiff(a, b) # rows in a but not b
# bind rows/columns
bind_rows(list(df1, df2)) # stack vertically
bind_cols(df1, df2) # side by side (no key check!)Funciones de ventana y Mutate agrupado
Las funciones de ventana operan dentro de grupos definidos por group_by. row_number, min_rank y dense_rank difieren en el manejo de empates. lead/lag acceden a filas adyacentes — esenciales para series temporales y detección de cambios. Las funciones acumulativas (cumsum, cummax, cummean) computan agregados rodantes. slice_max/min/head/sample extraen filas específicas por grupo. rowwise() + c_across() habilita operaciones fila por fila a través de columnas (más lento que vectorizado, pero a veces necesario). Estas funciones hacen dplyr tan potente como las funciones de ventana de SQL.
df <- tibble(
dept = c("A","A","A","B","B","B"),
name = c("x","y","z","p","q","r"),
salary = c(50, 70, 60, 80, 90, 75)
)
# ranking within groups
df %>% group_by(dept) %>%
mutate(
rank = row_number(), # 1, 2, 3 (ties get distinct)
min_rank = min_rank(salary), # ties get same rank, gaps
dense_rank = dense_rank(salary),
pct_rank = percent_rank(salary)
)
# offsets (lead / lag)
df %>% group_by(dept) %>%
mutate(
prev = lag(salary),
next = lead(salary),
change = salary - lag(salary)
)
# cumulative aggregates
df %>% group_by(dept) %>%
mutate(
cum_total = cumsum(salary),
cum_max = cummax(salary),
running_avg = cummean(salary)
)
# top N per group
df %>% group_by(dept) %>%
slice_max(salary, n = 2) # or slice_min, slice_head, slice_sample
# row-wise operations
df %>% rowwise() %>%
mutate(total = sum(c_across(where(is.numeric))))across, where y operaciones multi-columna
across() (dplyr 1.0+) es la forma moderna de aplicar una función a múltiples columnas — reemplazando los viejos sufijos _at, _if, _all. where(is.numeric) selecciona columnas por predicado. El argumento .names controla los nombres de salida con plantillas {col} y {fn}. if_all/if_any filtran filas donde todas/algunas columnas seleccionadas cumplen una condición. El pronombre .data habilita acceso programático a columnas (útil en funciones y apps Shiny). Estas herramientas hacen dplyr altamente expresivo para operaciones por lotes en muchas columnas.
df <- tibble(id = 1:3, a = c(1,2,3), b = c(4,5,6), c = c("x","y","z"))
# apply function to multiple columns
df %>% mutate(across(a:b, ~ .x * 10))
df %>% mutate(across(where(is.numeric), log))
df %>% mutate(across(everything(), as.character))
# summarize multiple columns
df %>% summarize(across(where(is.numeric), list(
mean = ~mean(.x),
sd = ~sd(.x)
), .names = "{.col}_{.fn}"))
# rename multiple columns
df %>% rename_with(toupper, starts_with("a"))
# conditional transformation
df %>% mutate(across(where(is.numeric), ~ if_else(.x > 3, "high", "low")))
# use .data pronoun for programmatic access
col <- "a"
df %>% mutate(new = .data[[col]] * 2)
# pick columns by type in any verb
df %>% filter(if_all(a:b, ~ .x > 1)) # all must satisfy
df %>% filter(if_any(a:b, ~ .x > 4)) # any must satisfyBackends de base de datos y dbplyr
dbplyr traduce verbos dplyr a SQL, dejándole manipular tablas de base de datos con la misma sintaxis que data frames locales. Esto es enorme para big data: el cómputo pesado ocurre en la base de datos (a menudo columnar/paralelo), y solo los resultados se traen a R via collect(). show_query() revela el SQL generado para depuración. La mayoría de verbos dplyr se traducen directamente; las funciones de ventana y algunas operaciones de cadena pueden necesitar funciones específicas de SQL. Siempre dbDisconnect cuando termine. Para producción, use un pool de conexiones y consultas parametrizadas para prevenir inyección SQL.
library(DBI)
library(dbplyr)
# connect to a database
con <- dbConnect(RSQLite::SQLite(), "my.db")
# or: con <- dbConnect(odbc::odbc(), "PostgreSQL")
# copy data to database
copy_to(con, mtcars, "mtcars_db", temporary = FALSE)
# reference a remote table
mtcars_remote <- tbl(con, "mtcars_db")
# dplyr verbs translate to SQL!
mtcars_remote %>%
group_by(cyl) %>%
summarize(avg_mpg = mean(mpg), n = n()) %>%
arrange(desc(avg_mpg))
# see the generated SQL
mtcars_remote %>%
filter(mpg > 20) %>%
select(mpg, cyl, hp) %>%
show_query()
# collect: pull results into local tibble
result <- mtcars_remote %>%
filter(cyl == 4) %>%
collect()
# write back to database
copy_to(con, result, "efficient_cars")
dbDisconnect(con)ggplot2 en profundidad
Gramática de gráficos y gráficos por capas
ggplot2 se construye sobre la Gramática de Gráficos: cada gráfico es una combinación de datos, mapeos estéticos (aes), objetos geométricos (geom_*), estadísticas (stat_*), escalas, sistemas de coordenadas y facets. Las capas se añaden con +. Las estéticas mapean columnas de datos a propiedades visuales (x, y, color, size, shape); los valores fijos van fuera de aes(). facet_wrap y facet_grid crean small multiples — una de las herramientas exploratorias más potentes. La mayoría de geoms tienen un stat por defecto (p. ej., geom_bar usa stat_count), pero puede sobrescribir con stat_summary para agregaciones personalizadas.
library(ggplot2)
df <- data.frame(x = 1:10, y = (1:10)^2 + rnorm(10))
# basic structure: data + aesthetic + geom
ggplot(df, aes(x = x, y = y)) +
geom_point()
# multiple layers
ggplot(df, aes(x, y)) +
geom_point(color = "blue", size = 3) +
geom_smooth(method = "lm", se = TRUE)
# aesthetics map data to visual properties
ggplot(mpg, aes(displ, hwy, color = class)) +
geom_point() +
geom_smooth(method = "loess", se = FALSE)
# facets: small multiples
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~ class, ncol = 4)
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_grid(drv ~ cyl) # rows ~ cols
# statistics (compute then plot)
ggplot(diamonds, aes(price)) +
geom_histogram(bins = 50) # stat_bin
ggplot(diamonds, aes(cut, price)) +
stat_summary(fun = "mean", geom = "bar")Escalas, temas y anotaciones
Las escalas controlan el mapeo de datos a propiedades visuales — cada estética tiene una escala correspondiente (scale_x_*, scale_color_*, etc.). scale_*_log10, scale_*_sqrt transforman ejes; scale_*_continuous/discrete/manual personalizan valores. Las paletas Viridis son aptas para daltónicos e imprimen bien en escala de grises. labs() establece todas las etiquetas en una llamada. theme() controla elementos no de datos (fuentes, líneas de cuadrícula, posición de leyenda); comience desde theme_minimal o theme_classic y ajuste. annotate() añade elementos fijos (texto, rectángulos, segmentos) independientes de los datos.
p <- ggplot(mpg, aes(displ, hwy, color = class)) +
geom_point(size = 3)
# scales control how data maps to visuals
p + scale_x_log10() +
scale_y_continuous(limits = c(0, 50), breaks = seq(0, 50, 10))
# manual colors
p + scale_color_manual(values = c("red","blue","green"))
# color brewer / viridis (colorblind-safe)
p + scale_color_brewer(palette = "Set1")
p + scale_color_viridis_d()
# labels
p + labs(
title = "Fuel Efficiency",
subtitle = "By vehicle class",
x = "Engine Displacement (L)",
y = "Highway MPG",
color = "Class",
caption = "Source: EPA"
)
# themes
p + theme_minimal()
p + theme_classic()
p + theme(
plot.title = element_text(face = "bold", size = 16),
panel.grid.minor = element_blank(),
legend.position = "bottom"
)
# annotations
p + annotate("text", x = 5, y = 40, label = "Outlier", color = "red") +
annotate("rect", xmin = 4, xmax = 6, ymin = 30, ymax = 45,
alpha = 0.2, fill = "blue")Geoms estadísticos y distribuciones
Los geoms estadísticos visualizan distribuciones y resúmenes. Los boxplots muestran cuartiles y outliers; los violines añaden la forma de densidad. geom_density suaviza histogramas; geom_bin2d/hex muestran distribuciones 2D para datasets grandes. geom_qq comprueba normalidad (los puntos deberían seguir la línea). geom_errorbar/geom_pointrange muestran incertidumbre. Para comparaciones pareadas, ggsignif añade corchetes de significancia. El paquete ggridges crea gráficos de cresta — excelentes para comparar distribuciones entre muchos grupos. Siempre elija geoms que representen honestamente los datos subyacentes.
# boxplot by group
ggplot(mpg, aes(class, hwy)) +
geom_boxplot() +
coord_flip()
# violin + boxplot overlay
ggplot(mpg, aes(class, hwy, fill = class)) +
geom_violin() +
geom_boxplot(width = 0.1)
# density plot
ggplot(mpg, aes(hwy, fill = drv)) +
geom_density(alpha = 0.5)
# 2D density / heatmap
ggplot(diamonds, aes(carat, price)) +
geom_bin2d(bins = 50) # or geom_hex()
# quantile-quantile plot
ggplot(mtcars, aes(sample = mpg)) +
geom_qq() + geom_qq_line()
# error bars
df <- data.frame(group = c("A","B","C"),
mean = c(5, 7, 4), se = c(0.5, 0.8, 0.3))
ggplot(df, aes(group, mean)) +
geom_col() +
geom_errorbar(aes(ymin = mean - se, ymax = mean + se), width = 0.2)
# ridgeline plot (ggridges)
# library(ggridges)
# ggplot(diamonds, aes(price, cut, fill = cut)) + geom_density_ridges()Facets, sistemas de coordenadas y extensiones
Las facets con scales = 'free' dejan cada panel tener su propio rango de eje — útil cuando los grupos tienen escalas muy diferentes. coord_polar convierte gráficos de barras en gráficos de pastel/radar; coord_flip intercambia ejes (útil para gráficos de barras horizontales). El paquete sf integra datos espaciales con geom_sf para mapas. patchwork combina múltiples gráficos con operadores +, / y | — mucho más flexible que gridExtra. ggsave exporta a PNG/PDF/SVG; cairo_pdf maneja fuentes personalizadas. ggplotly convierte ggplots a widgets HTML interactivos para despliegue web.
# free scales per facet
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~ class, scales = "free")
# polar coordinates (pie chart from bar)
ggplot(mtcars, aes(x = factor(1), fill = factor(cyl))) +
geom_bar(width = 1) +
coord_polar(theta = "y")
# flipped coordinates
ggplot(mpg, aes(class, hwy)) +
geom_boxplot() +
coord_flip()
# map coordinates (sf)
# library(sf)
# ggplot(nc_sf) + geom_sf(aes(fill = AREA))
# patchwork: combine multiple plots
# library(patchwork)
# p1 <- ggplot(...)
# p2 <- ggplot(...)
# p1 + p2
# p1 / (p2 + p3)
# save plots
ggsave("plot.png", width = 8, height = 6, dpi = 300)
ggsave("plot.pdf", device = cairo_pdf) # vector, supports fonts
# interactive (plotly)
# library(plotly)
# ggplotly(p)Gráficos reproducibles y funciones
Envolver llamadas ggplot en funciones las hace reutilizables. El operador {{ }} (tidy evaluation) le permite pasar nombres de columna sin comillas; .data[[string]] maneja acceso programático. Los objetos de gráfico guardados (.rds) pueden recargarse y modificarse — útil para informes que necesitan variaciones ligeras. Los temas personalizados pueden definirse una vez y aplicarse en todas partes, asegurando consistencia visual en un proyecto. Para generación por lotes, recorra nombres de columna con .data[[]] y ggsave. Este enfoque funcional es esencial para apps Shiny y pipelines de informes automatizados.
# wrap plots in functions for reuse
make_scatter <- function(data, x, y, color = NULL) {
ggplot(data, aes({{ x }}, {{ y }}, color = {{ color }})) +
geom_point() +
theme_minimal()
}
make_scatter(mpg, displ, hwy, class)
# using .data pronoun (string column names)
plot_col <- function(data, col) {
ggplot(data, aes(.data[[col]])) +
geom_bar() +
theme_minimal()
}
plot_col(mpg, "class")
# save and load plot objects
p <- ggplot(mpg, aes(displ, hwy)) + geom_point()
saveRDS(p, "plot.rds")
p_loaded <- readRDS("plot.rds")
# modify saved plot
p_loaded + geom_smooth()
# themes as reusable objects
my_theme <- theme_minimal() +
theme(text = element_text(family = "Helvetica"),
plot.title = element_text(face = "bold"))
ggplot(mpg, aes(displ, hwy)) + geom_point() + my_theme
# programmatic plot generation
for (col in c("hwy","cty","cyl")) {
p <- ggplot(mpg, aes(.data[[col]])) + geom_histogram()
ggsave(paste0("hist_", col, ".png"), p)
}Limpieza de datos con tidyr
Pivot Longer y Wider
Los datos tidy tienen una fila por observación y una columna por variable — la mayoría de funciones de análisis esperan este formato. pivot_longer convierte wide a long (reuniendo columnas en pares clave-valor); pivot_wider hace lo inverso. El centinela .value en names_to mantiene partes de nombres de columna como columnas separadas (p. ej., 'a_1' se convierte en a=1, b=1). Siempre reshape antes de graficar o modelar: ggplot2 quiere formato long para estéticas agrupadas; algunas funciones de modelado quieren formato wide. El argumento names_pattern maneja estructuras de nombres de columna más complejas con regex.
library(tidyr)
# wide format (one row per observation, columns for each variable)
wide <- tibble(
country = c("A","B","C"),
`2020` = c(100, 150, 200),
`2021` = c(110, 160, 210),
`2022` = c(120, 170, 220)
)
# pivot_longer: wide -> long
long <- wide %>%
pivot_longer(
cols = -country, # all columns except country
names_to = "year",
values_to = "gdp"
)
# A tibble: 9 x 3
# pivot_wider: long -> wide
wide2 <- long %>%
pivot_wider(names_from = year, values_from = gdp)
# multiple value columns
df <- tibble(id = 1:2, a_1 = c(1,2), a_2 = c(3,4), b_1 = c(5,6), b_2 = c(7,8))
df %>%
pivot_longer(
-id,
names_to = c(".value", "time"), # .value keeps a, b as columns
names_sep = "_"
)
# id time a b
# 1 1 1 5
# 1 2 3 7Separate, Unite y Extract
separate divide una columna en un delimitador en múltiples columnas; unite combina múltiples columnas en una. extract usa grupos de captura regex para división más flexible. separate_rows explota cadenas delimitadas en múltiples filas — esencial cuando una celda contiene una lista (p. ej., tags, categorías). La opción convert = TRUE auto-convierte tipos (números, fechas). Estas funciones limpian datos del mundo real desordenados: dividir nombres completos, parsear fechas, normalizar campos delimitados. Combinadas con pivot_*, manejan casi cualquier tarea de reshape.
df <- tibble(
name = c("John_Smith", "Jane_Doe", "Bob_Jones"),
date_range = c("2020-01-01_2020-12-31", "2021-01-01_2021-12-31", "2022-01-01_2022-12-31")
)
# separate one column into many
df %>% separate(name, into = c("first", "last"), sep = "_")
# separate with conversion
df %>% separate(name, into = c("first","last"), sep = "_", convert = TRUE)
# extract with regex groups
df %>%
extract(date_range,
into = c("start", "end"),
regex = "(.+)_(.+)")
# unite multiple columns into one
df2 <- tibble(first = c("John","Jane"), last = c("Smith","Doe"))
df2 %>% unite("full_name", first:last, sep = " ")
# separate_rows: split delimited values into rows
df3 <- tibble(id = 1:2, tags = c("a,b,c", "x,y"))
df3 %>% separate_rows(tags, sep = ",")
# id tags
# 1 a
# 1 b
# 1 c
# 2 x
# 2 yManejo de valores faltantes
Los valores faltantes están en todas partes en datos reales. drop_na elimina filas con NAs; replace_na las rellena con constantes. fill arrastra la última observación hacia adelante (LOCF) — común en series temporales. coalesce elige el primer no-NA a través de columnas (útil para fusionar fuentes solapantes). na_if convierte un valor centinela (como -99 o 'N/A') a NA apropiado. Siempre investigue POR QUÉ los valores faltan antes de imputar; MCAR (missing completely at random), MAR y MNAR tienen diferentes implicaciones. Para imputación sofisticada, use los paquetes mice o Amelia.
df <- tibble(
x = c(1, 2, NA, 4, 5),
y = c(NA, 2, 3, NA, 5),
z = c("a", NA, "c", "d", NA)
)
# drop rows with any NA
df %>% drop_na()
df %>% drop_na(x) # only column x
# replace NA with a value
df %>% replace_na(list(x = 0, y = -1, z = "unknown"))
# fill NA with previous/next value
df %>% fill(x, y, .direction = "down") # carry forward
df %>% fill(x, y, .direction = "up") # carry backward
# coalesce: first non-missing value
df %>% mutate(x_filled = coalesce(x, y, 0))
# na_if: replace specific value with NA
df %>% mutate(z = na_if(z, "a"))
# detect missing values
is.na(df$x)
sum(is.na(df)) # total NAs
df %>% map_df(~ sum(is.na(.))) # NAs per column
# imputation (simple)
df %>% mutate(x = if_else(is.na(x), mean(x, na.rm = TRUE), x))Nesting y columnas de lista
Las list-columns almacenan múltiples valores (o incluso tibbles enteros, modelos, gráficos) por fila — potentes para flujos split-apply-combine. nest() agrupa filas en tibbles anidados; map() aplica una función a cada uno; unnest() expande los resultados de vuelta. Este patrón (nest → map → unnest) reemplaza muchos for-loops y es la forma idiomática tidyverse de hacer análisis por grupo. broom::tidy/glance/augment convierten objetos modelo en tibbles, haciéndolos nest-friendly. Las list-columns son también la base de la programación funcional basada en purrr en R.
# nest: group rows into list-columns
nested <- mtcars %>%
group_by(cyl) %>%
nest()
# cyl data
# 4 <tibble 11x10>
# 6 <tibble 7x10>
# 8 <tibble 14x10>
# access nested data
nested$data[[1]] # first group's tibble
# fit models to each group
models <- nested %>%
mutate(
model = map(data, ~ lm(mpg ~ wt, data = .x)),
glance = map(model, broom::glance),
tidy = map(model, broom::tidy)
)
# unnest results
models %>% unnest(glance)
models %>% unnest(tidy)
# unnest a list-column back to rows
df <- tibble(id = 1:2, vals = list(c(1,2,3), c(4,5)))
df %>% unnest(vals)
# id vals
# 1 1
# 1 2
# 1 3
# 2 4
# 2 5
# chop/unchop (similar but keeps other columns as lists)
df %>% chop(vals)
df %>% unchop(vals)Rectangling y JSON
El rectangling convierte datos anidados/jerárquicos (JSON, respuestas API) en tibbles tidy. hoist() extrae elementos específicos de una list-column; unnest_wider() esparce una lista en columnas; unnest_longer() expande cada elemento en una fila. Para estructuras profundamente anidadas, combine las funciones map de purrr con construcción de tibble. jsonlite::fromJSON con simplifyDataFrame = TRUE auto-aplana JSON simple. Este flujo de trabajo es esencial para trabajar con REST APIs, bases de datos NoSQL y archivos de configuración — el pan diario del ingeniero de datos moderno.
library(jsonlite)
library(tidyr)
# parse JSON
json <- '[
{"name":"Alice","age":30,"skills":["R","Python"]},
{"name":"Bob","age":25,"skills":["SQL"]}
]'
parsed <- fromJSON(json, simplifyDataFrame = FALSE)
# convert list to tibble
tibble(person = parsed) %>%
hoist(person,
name = "name",
age = "age",
skills = "skills"
)
# name age skills
# Alice 30 <chr [2]>
# Bob 25 <chr [1]>
# unnest longer for nested lists
tibble(person = parsed) %>%
unnest_wider(person) # spread list to columns
tibble(skills = list(c("R","Python"), c("SQL"))) %>%
unnest_longer(skills)
# deeply nested: use purrr + tibble
flatten_df <- function(lst) {
tibble(
name = lst$name,
age = lst$age,
n_skills = length(lst$skills)
)
}
map_dfr(parsed, flatten_df)
# rectangularize arrays
jsonlite::fromJSON(json, simplifyDataFrame = TRUE) # auto-flattenProgramación funcional con purrr
Familia map y variantes type-safe
La familia map de purrr reemplaza lapply/sapply con variantes consistentes y type-safe. map_dbl/chr/int/lgl devuelven vectores tipados (errorando en desajuste) — mucho más seguras que sapply que coacciona silenciosamente. map2 y pmap iteran sobre múltiples vectores en paralelo. imap proporciona tanto valor como índice. walk es para efectos secundarios (imprimir, escribir archivos) donde no necesita el valor de retorno. El atajo ~ .x crea funciones anónimas; .x es el primer argumento, .y el segundo. Siempre prefiera map_* sobre sapply en código de producción para evitar inestabilidad de tipos.
library(purrr)
# map: apply function to each element, return list
map(1:3, ~ .x ^ 2) # list(1, 4, 9)
# type-specific variants (safer, faster)
map_dbl(1:3, ~ .x ^ 2) # numeric vector: 1 4 9
map_chr(c(1,2,3), ~ paste("n=", .x))
map_int(c(1.5, 2.7), floor)
map_lgl(c(1, NA, 3), ~ !is.na(.x))
# map over two vectors in parallel
map2_dbl(c(1,2,3), c(10,20,30), ~ .x + .y) # 11 22 33
# map over multiple vectors (pmap)
pmap_dbl(list(a = 1:3, b = 4:6, c = 7:9), sum) # 12 15 18
# map over indices (imap)
imap_chr(c("a","b","c"), ~ paste0(.y, ":", .x))
# "1:a" "2:b" "3:c"
# walk: side effects (no return value)
walk(c("file1.csv","file2.csv"), ~ print(read.csv(.x)))
# map over columns of a data frame
map_dbl(mtcars, mean) # mean of each column
map(mtcars, class) # class of each columnFunciones anónimas y sintaxis de fórmula
purrr ofrece múltiples formas de especificar funciones: el atajo de fórmula (~ .x + 1) para casos simples, sintaxis completa function(x) para cuerpos complejos y funciones con nombre para reutilización. Las funciones map también aceptan cadenas/números para extraer por nombre/posición — sin función wrapper necesaria. pluck() navega de forma segura estructuras profundamente anidadas con un fallback .default; chuck() es la versión estricta que errora en rutas faltantes. Esto hace purrr ideal para trabajar con JSON, respuestas API y otros datos jerárquicos donde la extracción [[ de R base se vuelve difícil de manejar.
# formula syntax (~ creates a function)
map(1:3, ~ .x + 10) # .x is the argument
map2(1:3, 10:12, ~ .x + .y) # .x, .y for two args
pmap(list(1:3, 4:6, 7:9), ~ ..1 + ..2 + ..3) # ..1, ..2 for many
# full function syntax (for complex bodies)
map(1:3, function(x) {
if (x > 2) return(x * 10)
x
})
# named function
square <- function(x) x^2
map(1:3, square)
# extract by name/position (no function needed)
map(list(a = list(x = 1), b = list(x = 2)), "x")
map(list(list(1,2), list(3,4)), 1) # first element of each
# pluck: safely extract deeply nested
deep <- list(a = list(b = list(c = 42)))
pluck(deep, "a", "b", "c") # 42
pluck(deep, "a", "x", "y", .default = NA) # NA (no error)
# chuck: same but errors if missing
# chuck(deep, "a", "x") # errorReduce, Accumulate y funciones predicado
reduce() combina elementos por pares (fold izquierdo) — perfecto para fusionar muchos data frames o computar productos. accumulate() mantiene resultados intermedios, útil para totales acumulados. keep/discard filtran elementos por un predicado (como dplyr::filter pero para vectores/listas). some/every prueban si algún/todos los elementos satisfacen una condición. detect encuentra el primer elemento coincidente. Estas funciones de orden superior reemplazan muchos bucles con código conciso y declarativo. negate() invierte una función predicado — útil para componer condiciones. Juntos hacen purrr un toolkit completo de programación funcional.
# reduce: combine elements pairwise
reduce(c(1,2,3,4), `+`) # 10 (sum)
reduce(c(1,2,3,4), `*`) # 24 (product)
reduce(list(df1, df2, df3), full_join, by = "id") # merge many
# accumulate: keep intermediate results
accumulate(c(1,2,3,4), `+`) # 1 3 6 10 (running sum)
accumulate(c(2,3,4), `*`) # 2 6 24 (running product)
# keep/discard: filter by predicate
keep(1:10, ~ .x %% 2 == 0) # 2 4 6 8 10
discard(1:10, ~ .x %% 2 == 0) # 1 3 5 7 9
keep(mtcars, is.numeric) # keep numeric columns
# some/every/detect: test predicates
some(1:10, ~ .x > 5) # TRUE
every(1:10, ~ is.numeric(.x)) # TRUE
detect(1:10, ~ .x > 5) # 6 (first match)
detect_index(1:10, ~ .x > 5) # 6 (position)
# head_while/tail_while
head_while(1:10, ~ .x < 5) # 1 2 3 4
tail_while(10:1, ~ .x > 5) # 10 9 8 7 6
# negate a predicate
negate(is.na)(5) # TRUE
keep(c(1, NA, 3), negate(is.na)) # 1 3Safely, Possibly y manejo de errores
safely() envuelve una función para siempre devolver una lista con 'result' y 'error' — nunca lanza. Esto es esencial para operaciones por lotes donde un fallo no debería parar toda la ejecución. possibly() devuelve un valor por defecto en error (más limpio cuando no necesita los detalles del error). quietly() captura advertencias y mensajes. transpose() convierte una lista de pares {result, error} en listas separadas — conveniente para separar éxitos de fallos. Use estos siempre que procese muchos elementos que podrían fallar individualmente (llamadas API, lecturas de archivos, ajustes de modelo).
# safely: capture errors without stopping
safe_log <- safely(log)
safe_log(10) # list(result = 2.3, error = NULL)
safe_log(-1) # list(result = NULL, error = <error>)
# process many, capturing failures
results <- map(c(10, -1, 0, "x"), safely(log))
successes <- map(results, "result") %>% discard(is.null)
failures <- map(results, "error") %>% discard(is.null)
# possibly: return default on error
safe_log2 <- possibly(log, otherwise = NA_real_)
map_dbl(c(10, -1, 0, "x"), safe_log2) # 2.3 NA NA NA
# quietly: capture warnings/messages
quiet_log <- quietly(log)
quiet_log(10) # list(result, warnings, messages)
# transpose: restructure list-of-lists
transposed <- transpose(results)
transposed$result # all results
transposed$error # all errors
# rate-limited / retried operations
# library(purrr)
# safely_slow <- slowly(safely(f), rate = rate_backoff())
# walk + safely for batch file processing
walk(files, ~ safely(read.csv)(.x))purrr vectorizado y paralelo
modify() es como map() pero preserva el tipo de entrada — perfecto para transformar columnas de data frame in situ. modify_if y modify_at apuntan a columnas específicas. list_modify/list_merge actualizan listas no destructivamente. Para paralelismo, furrr proporciona future_map (reemplazo directo de map) usando el backend future — cambie de secuencial a paralelo cambiando plan(). La opción .progress = TRUE muestra una barra de progreso, invalorable para maps de larga duración. Estas herramientas hacen purrr adecuado tanto para exploración interactiva como para pipelines de producción.
# vectorized map (faster for simple operations)
# map_vec returns a vector, auto-detecting type
map_vec(1:3, ~ .x * 2) # numeric vector
map_vec(c("a","b"), ~ toupper(.x)) # character vector
# modify: like map but preserves type
modify(mtcars, ~ .x * 2) # still a data frame
modify_if(mtcars, is.numeric, ~ .x * 2)
modify_at(mtcars, c("mpg","cyl"), ~ .x * 2)
# list_modify / list_merge
l1 <- list(a = 1, b = 2)
list_modify(l1, b = 20, c = 30) # a=1, b=20, c=30
list_merge(l1, list(b = 20, c = 30))
# parallel mapping with future + furrr
# library(furrr)
# plan(multisession) # parallel backend
# future_map(1:100, slow_function, .options = furrr_options(seed = TRUE))
# progress bar
# walk(1:100, ~ Sys.sleep(0.1), .progress = TRUE)
# conditional execution in map
map(1:5, ~ if (.x > 3) .x * 10 else .x)
# 1 2 3 40 50stringr y lubridate
stringr: emparejamiento y extracción de patrones
stringr proporciona una API consistente y pipe-friendly envolviendo el motor regex ICU. Todas las funciones empiezan con str_ para fácil autocompletado. str_detect/which/subset filtran por patrón. str_extract/match extraen coincidencias (match captura grupos). str_replace/remove modifican texto. str_split divide cadenas en piezas. La sintaxis regex subyacente es estándar (tipo PCRE), con helpers como \\w, \\d, \\s. Para cadenas fijas (sin regex), use str_detect(text, fixed('a.b')) para coincidir literalmente. stringr es mucho más consistente que la familia grep/sub de R base.
library(stringr)
text <- c("apple pie", "banana bread", "cherry tart")
# detect pattern
str_detect(text, "pie") # TRUE FALSE FALSE
str_which(text, "pie") # 1
str_count(text, "a") # 1 3 1
# subset strings
str_subset(text, "pie") # "apple pie"
str_extract(text, "[aeiou]") # first vowel
str_extract_all(text, "[aeiou]") # list of all vowels
# match groups
str_match(text, "(\\w+) (\\w+)") # matrix with groups
str_match_all(text, "(\\w+)")
# locate pattern positions
str_locate(text, "a") # start/end matrix
str_locate_all(text, "a")
# replace
str_replace(text, "a", "A") # first match
str_replace_all(text, "a", "A") # all matches
str_remove(text, "a") # str_replace(text, "a", "")
# split
str_split("a,b,c", ",") # list of vectors
str_split_fixed("a,b,c", ",", 3) # matrixManipulación y transformación de cadenas
stringr cubre todas las operaciones comunes de cadena con una interfaz consistente. La conversión de mayúsculas (str_to_upper/lower/title/sentence) respeta locale. str_trim elimina espacios en blanco; str_squish también colapsa espacios internos. str_pad alinea cadenas a un ancho fijo (útil para formatear tablas). str_sub extrae o reemplaza subcadenas con la indexación base 1 de R (índices negativos cuentan desde el final). str_c es el equivalente pipe-friendly de paste0. Estas funciones hacen la manipulación de cadenas predecible y legible comparada con las funciones de cadena dispersas de R base.
library(stringr)
# case conversion
str_to_upper("hello") # "HELLO"
str_to_lower("WORLD") # "world"
str_to_title("the wind in the willows") # "The Wind In The Willows"
str_to_sentence("hello world") # "Hello world"
# trimming and padding
str_trim(" hello ") # "hello" (both sides)
str_trim(" hello ", side = "left")
str_squish(" hello world ") # "hello world" (collapse spaces)
str_pad("5", width = 3, pad = "0") # "005"
str_pad("5", width = 3, side = "left", pad = "0") # "005"
# subsetting
str_sub("hello", 1, 3) # "hel"
str_sub("hello", -3, -1) # "llo" (negative from end)
str_sub("hello", 2) # "ello" (to end)
str_sub("hello", 1, 1) <- "H" # assignment: "Hello"
# length
str_length("hello") # 5
str_length(c("a","bb","ccc")) # 1 2 3
# combine and duplicate
str_c("a", "b", "c", sep = "-") # "a-b-c"
str_c(c("x","y"), collapse = ",") # "x,y"
str_dup("ab", 3) # "ababab"
# truncate
str_trunc("Hello World", 8) # "Hello..."Expresiones regulares en profundidad
stringr usa el motor regex ICU con sintaxis estándar. ^ y $ anclan a inicio/fin. Las clases de caracteres [a-z] coinciden rangos; \w, \d, \s son atajos. Los cuantificadores {n,m}, ?, +, * controlan repetición. Los paréntesis crean grupos de captura; | es alternancia. Lookahead (?=) y lookbehind (?<=) afirman sin consumir. Los grupos con nombre (?<name>...) hacen la extracción autodocumentada. Siempre use fixed() al coincidir cadenas literales que contienen metacaracteres regex — también es más rápido. Para parseo complejo, considere el paquete rebus para construir regex legiblemente.
library(stringr)
# anchors
str_detect(c("cat","scat","catch"), "^cat") # starts with: T F T
str_detect(c("cat","scat","catch"), "cat$") # ends with: T F F
# character classes
str_extract_all("a1 b2 c3", "[a-z]") # letters
str_extract_all("a1 b2 c3", "[0-9]") # digits
str_extract_all("a1 b2 c3", "[^0-9]") # non-digits
str_extract_all("a1 b2 c3", "\\w") # word chars
str_extract_all("a1 b2 c3", "\\s") # whitespace
# quantifiers
str_detect("aaa", "a{2,3}") # 2-3 a's
str_detect("color", "colou?r") # u optional
str_detect("abbbbc", "ab+c") # one or more
str_detect("ac", "ab*c") # zero or more
# groups and alternation
str_match("2024-01-15", "(\\d{4})-(\\d{2})-(\\d{2})")
str_detect("cat", "cat|dog|bird") # alternation
# lookahead/lookbehind
str_extract("abc123", "(?<=abc)\\d+") # 123 (lookbehind)
str_extract("abc123", "\\d+(?=end)") # nothing (no 'end')
# named capture (stringr 1.5+)
str_match("John 30", "(?<name>[A-Za-z]+) (?<age>\\d+)")
# use fixed() for literal matching
str_detect("a.b.c", fixed(".")) # TRUE (no regex)lubridate: parseo de fecha y hora
Las funciones de parseo de lubridate (ymd, mdy, dmy) auto-detectan separadores y formatos — mucho más indulgentes que strptime de R base. El nombre de la función indica el orden: ymd = año-mes-día. make_date/assemble construye desde componentes. today() y now() devuelven la fecha/hora actual. Las funciones de componente (year, month, day, wday, yday) tanto obtienen como establecen valores; wday(label=TRUE) devuelve nombres de día de la semana. update() modifica múltiples componentes a la vez. Siempre especifique tz (zona horaria) explícitamente para datetime para evitar asunciones UTC silenciosas.
library(lubridate)
# parse dates (auto-detect format)
ymd("2024-01-15") # 2024-01-15
ymd("2024/01/15")
ymd("24/1/15")
mdy("01-15-2024") # month-day-year
dmy("15/01/2024") # day-month-year
ymd_hms("2024-01-15 14:30:00")
ymd_hm("2024-01-15 14:30")
hms("14:30:45")
# from components
make_date(2024, 1, 15)
make_datetime(2024, 1, 15, 14, 30, 0, tz = "UTC")
# current date/time
today() # 2024-01-15
now() # 2024-01-15 14:30:00 UTC
# extract components
d <- ymd("2024-01-15")
year(d) # 2024
month(d) # 1
month(d, label = TRUE) # "Jan"
day(d) # 15
wday(d, label = TRUE) # "Mon"
yday(d) # 15 (day of year)
quarter(d) # 1
semester(d) # 1
# set components
year(d) <- 2025
month(d) <- 6
d <- update(d, year = 2025, month = 6, day = 1)Zonas horarias, duraciones e intervalos
Las zonas horarias son la parte más complicada del manejo de datetime. with_tz muestra el mismo instante en otra zona; force_tz cambia la zona sin cambiar el reloj (útil para arreglar datos mal etiquetados). Las duraciones (dseconds, dhours) son segundos exactos — buenas para física. Los periodos (minutes, hours, days) son conscientes del calendario: añadir months(1) a 31 de enero da 28 de febrero, y days(1) maneja transiciones DST. Los intervalos (start %--% end) representan spans con endpoints fijos. Use periodos para aritmética a escala humana (planificación) y duraciones para medición de tiempo transcurrido.
library(lubridate)
# timezones
t <- ymd_hms("2024-01-15 14:30:00", tz = "UTC")
with_tz(t, "America/New_York") # 09:30 EST
with_tz(t, "Asia/Shanghai") # 22:30 CST
force_tz(t, "America/New_York") # keeps clock, changes zone
# list timezones
OlsonNames()[1:5]
# durations (exact seconds)
d <- dseconds(60) + dminutes(2) # 180 seconds
as.numeric(d, "seconds")
dhours(1) + ddays(1)
# periods (calendar-aware, variable length)
p <- minutes(5) + hours(2)
ymd("2024-01-15") + p # 2024-01-15 02:05:00
ymd("2024-03-10") + days(1) # handles DST
months(1) # variable length!
# intervals (start to end)
int <- interval(ymd("2024-01-01"), ymd("2024-02-01"))
int_length(int) # seconds
int %within% int2 # test overlap
as.period(int) # "1M 0S"
# arithmetic
ymd("2024-01-15") %--% ymd("2024-02-15") # interval
ymd("2024-01-15") + weeks(2) # period
ymd("2024-01-15") + dweeks(2) # duration (exact)
# rounding dates
floor_date(t, "hour") # round down
ceiling_date(t, "day")
round_date(t, "hour")R Markdown e informes
Fundamentos de R Markdown y chunks
R Markdown combina prosa (Markdown), código (R/Python/SQL) y salida (tablas, gráficos) en informes reproducibles. El encabezado YAML establece metadatos y formato de salida. Los chunks de código delimitados por triple backtick se ejecutan al tejer; las opciones de chunk controlan el comportamiento (echo=FALSE oculta código, include=FALSE ejecuta pero no muestra nada, fig.width establece tamaño de gráfico). El código inline con backticks simples inserta valores en texto. kable() formatea tablas; para tablas más elegantes use kableExtra o gt. El botón Knit renderiza a HTML/PDF/Word.
---
title: "Analysis Report"
author: "Data Team"
date: "`r format(Sys.Date(), '%B %d, %Y')`"
output: html_document
---
## Introduction
This is **Markdown** with embedded R.
Inline code: the mean is `r mean(mtcars$mpg)`.
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = FALSE, warning = FALSE, message = FALSE)
library(dplyr)
library(ggplot2)
```
```{r load-data}
df <- mtcars
head(df)
```
```{r plot, fig.width=8, fig.height=6, fig.cap="MPG by weight"}
ggplot(df, aes(wt, mpg)) + geom_point() + theme_minimal()
```
```{r table}
library(knitr)
kable(summary(df), caption = "Summary statistics")
```Formatos de salida y parámetros
Un archivo R Markdown puede producir múltiples formatos de salida (HTML, PDF, Word, diapositivas) desde la misma fuente — solo liste bajo output. Las opciones específicas de HTML (toc_float, code_folding, theme) crean documentos interactivos. Los parámetros (params) le permiten renderizar el mismo informe con diferentes entradas — esencial para informes por lotes (uno por región, cliente o periodo). Renderice programáticamente con rmarkdown::render() para automatizar la generación de informes en jobs cron o apps Shiny. El objeto params está disponible dentro de chunks para filtrar datos.
---
title: "Quarterly Report"
output:
html_document:
toc: true
toc_float: true
code_folding: hide
theme: flatly
df_print: paged
pdf_document:
toc: true
number_sections: true
word_document: default
params:
quarter: "Q1"
region: "North"
---
## Report for `r params$quarter` - `r params$region`
```{r}
data <- filter(all_data, quarter == params$quarter, region == params$region)
```
# Render with parameters:
# rmarkdown::render("report.Rmd", params = list(quarter = "Q2"))
# parameterized batch rendering
quarters <- c("Q1","Q2","Q3","Q4")
for (q in quarters) {
rmarkdown::render("report.Rmd",
params = list(quarter = q),
output_file = paste0("report_", q, ".html")
)
}Tablas con kable, gt y DT
kable + kableExtra produce tablas de calidad de publicación con estilo, agrupación y formato condicional. gt es una alternativa moderna con una gramática más expresiva (como ggplot para tablas). DT crea tablas HTML interactivas con búsqueda, ordenación y paginación — perfectas para informes exploratorios. reactable ofrece aún más interactividad. Elija basándose en la salida: kable/gt para PDF/Word, DT/reactable para HTML. Siempre formatee números consistentemente (fmt_number, formatRound) y añada captions para contexto. Las buenas tablas son tan importantes como los buenos gráficos para comunicar resultados.
library(knitr); library(kableExtra)
# basic kable
kable(head(mtcars), caption = "First 6 rows")
# styled kable
kable(head(mtcars), "html") %>%
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = FALSE) %>%
row_spec(0, bold = TRUE, background = "lightblue") %>%
column_spec(1, bold = TRUE)
# gt package (modern, flexible)
library(gt)
mtcars %>%
head() %>%
gt() %>%
tab_header(title = "Car Specifications", subtitle = "Top 6") %>%
fmt_number(columns = mpg, decimals = 1) %>%
data_color(columns = mpg, colors = scales::col_numeric("Reds", NULL))
# DT: interactive tables
library(DT)
datatable(mtcars,
filter = "top",
options = list(pageLength = 10),
caption = "Searchable table"
) %>%
formatRound(columns = c("mpg","disp"), digits = 2)
# reactable for even more interactivity
# library(reactable)Quarto y características avanzadas
Quarto es el sucesor de R Markdown, soportando R, Python, Julia y Observable en un documento. Las referencias cruzadas (@fig-label, @tbl-label) auto-numeran figuras y tablas. La sintaxis #| para opciones de chunk es más limpia que los viejos opts de knitr. El plegado de código crea bloques de código plegables para HTML interactivo. El soporte multi-lenguaje de Quarto lo hace ideal para equipos que usan tanto R como Python. Los archivos .Rmd existentes pueden convertirse; la sintaxis es similar pero más consistente. Quarto también produce presentaciones (revealjs), sitios web y libros desde la misma fuente.
---
title: "Modern Report"
format:
html:
toc: true
code-fold: true
pdf:
documentclass: article
execute:
echo: false
warning: false
filters:
- lightbox
---
## Cross-references
See Figure @fig-scatter and Table @tbl-summary.
```{r}
#| label: fig-scatter
#| fig-cap: "MPG vs Weight"
ggplot(mtcars, aes(wt, mpg)) + geom_point()
```
```{r}
#| label: tbl-summary
#| tbl-cap: "Summary by cylinder"
mtcars %>%
group_by(cyl) %>%
summarize(mean_mpg = mean(mpg)) %>%
gt()
```
## Multiple languages
```{python}
import pandas as pd
df = pd.DataFrame({'a': [1,2,3]})
print(df)
```
```{sql}
SELECT * FROM mtcars WHERE mpg > 20
```
# Quarto supports Python, Julia, Observable, and more
# in the same document, sharing data via ojs.Informes automatizados y Cron
Los informes automatizados convierten análisis puntuales en entregables recurrentes. rmarkdown::render() genera informes programáticamente; combine con params para personalización. Envíe resultados por email con blastula o emayili. Programe con cronR (Linux/Mac) o taskscheduleR (Windows) para ejecuciones diarias/semanales. Para generación por lotes, recorra parámetros con purrr::walk. Habilite el caché de chunks (cache=TRUE) para acelerar la iteración en cómputos costosos — solo los chunks cambiados se re-ejecutan. Este pipeline es la columna vertebral de business intelligence y productos de datos automatizados en R.
# render a report programmatically
rmarkdown::render("daily_report.Rmd",
output_dir = "reports",
output_file = paste0("report_", Sys.Date(), ".html"),
params = list(date = Sys.Date() - 1),
quiet = TRUE
)
# email the report
# library(blastula)
# render_email("email_template.Rmd") %>%
# smtp_send(
# to = "[email protected]",
# from = "[email protected]",
# subject = paste("Daily Report -", Sys.Date())
# )
# schedule with cron (Linux/Mac) or Task Scheduler (Windows)
# crontab -e:
# 0 8 * * * cd /path/to/project && Rscript -e 'rmarkdown::render("daily.Rmd")'
# or use the cronR package
# library(cronR)
# cmd <- cron_rscript("render_report.R")
# cron_add(cmd, frequency = "daily", at = "08:00")
# batch render multiple reports
purrr::walk(regions, function(r) {
rmarkdown::render("regional.Rmd",
params = list(region = r),
output_file = paste0("report_", r, ".html")
)
})
# version control: cache expensive computations
# knitr::opts_chunk$set(cache = TRUE)Apps web Shiny
Estructura de la app: UI y Server
Cada app Shiny tiene dos partes: ui (el diseño HTML) y server (la lógica R). La UI usa fluidPage y funciones de diseño (sidebarLayout, tabsetPanel, navbarPage). Las entradas (sliderInput, selectInput, etc.) recopilan datos del usuario; las salidas (plotOutput, textOutput) muestran resultados. La función server las conecta via funciones render*. Las expresiones reactivas (reactive({...})) cachean cómputos y solo se re-ejecutan cuando las entradas cambian. Guarde como app.R y ejecute con runApp() o aloje en shinyapps.io / RStudio Connect / Shiny Server.
library(shiny)
ui <- fluidPage(
titlePanel("My First Shiny App"),
sidebarLayout(
sidebarPanel(
sliderInput("n", "Number of points:", min = 1, max = 100, value = 50),
selectInput("color", "Color:", choices = c("red","blue","green"))
),
mainPanel(
plotOutput("scatter"),
verbatimTextOutput("summary")
)
)
)
server <- function(input, output, session) {
data <- reactive({
data.frame(x = rnorm(input$n), y = rnorm(input$n))
})
output$scatter <- renderPlot({
plot(data()$x, data()$y, col = input$color, pch = 19,
xlab = "X", ylab = "Y", main = paste("n =", input$n))
})
output$summary <- renderPrint({
summary(data())
})
}
shinyApp(ui, server)
# save as app.R in a folder, then runApp("folder")
# or run with shiny::runApp()Programación reactiva
La reactividad es el concepto central de Shiny. reactive() crea expresiones perezosas y cacheadas que se re-ejecutan solo cuando cambian las dependencias. observe() se ejecuta eagerly para efectos secundarios (actualizar entradas, logging). observeEvent/eventReactive se disparan en eventos específicos (clics de botón). reactiveVal y reactiveValues mantienen estado mutable. isolate() lee un valor sin crear una dependencia. La idea clave: las salidas se re-renderizan automáticamente cuando cambian sus dependencias reactivas. Malentender la reactividad es la fuente #1 de bugs de Shiny — use reactiveLogViewer() para depurar grafos de dependencias.
server <- function(input, output, session) {
# reactive expression: lazy, cached
filtered <- reactive({
mtcars %>% filter(cyl == input$cyl)
})
# observe: eager, for side effects
observe({
updateSelectInput(session, "model",
choices = unique(filtered()$model))
})
# observeEvent: triggered by specific input
observeEvent(input$reset, {
updateSliderInput(session, "cyl", value = 4)
})
# eventReactive: lazy, triggered by event
result <- eventReactive(input$go, {
run_analysis(input$param)
})
# reactiveVal: mutable value
counter <- reactiveVal(0)
observeEvent(input$add, counter(counter() + 1))
# reactiveValues: multiple mutable values
rv <- reactiveValues(data = NULL, status = "ready")
observeEvent(input$load, {
rv$data <- read.csv(input$file$datapath)
rv$status <- "loaded"
})
# isolate: read without dependency
output$plot <- renderPlot({
plot(isolate(rv$data))
})
}UI dinámica y módulos
La UI dinámica (renderUI + uiOutput) genera controles basados en datos o elecciones del usuario. insertUI/removeUI añaden/eliminan elementos sin re-renderizar toda la página. Los módulos (NS + moduleServer) encapsulan lógica UI+server para reutilización — esenciales para apps complejas con componentes repetidos. Cada instancia de módulo obtiene un namespace único (ns) para que los IDs de input no colisionen. Los módulos son la clave para construir apps Shiny mantenibles: divida su app en módulos pequeños y testeables (un módulo de gráfico, un módulo de filtro, un módulo de subida de datos) y compóngalos.
# dynamic UI
ui <- fluidPage(
uiOutput("dynamic_controls"),
plotOutput("plot")
)
server <- function(input, output, session) {
output$dynamic_controls <- renderUI({
cols <- names(input$data)
selectInput("xvar", "X variable:", choices = cols)
})
# insert/remove UI
observeEvent(input$add, {
insertUI("#placeholder", "beforeEnd",
sliderInput(paste0("s", input$add), "Slider", 0, 100, 50))
})
# modules: reusable UI+server
histogramUI <- function(id) {
ns <- NS(id)
tagList(
selectInput(ns("var"), "Variable:", names(mtcars)),
plotOutput(ns("hist"))
)
}
histogramServer <- function(id) {
moduleServer(id, function(input, output, session) {
output$hist <- renderPlot(hist(mtcars[[input$var]]))
})
}
# use module
ui <- fluidPage(histogramUI("hist1"), histogramUI("hist2"))
server <- function(input, output, session) {
histogramServer("hist1")
histogramServer("hist2")
}
}Entradas, salidas y renderizado
Shiny soporta muchos tipos de entrada (file, date, slider, selectize, checkbox) y tipos de salida (plot, table, text, image, UI). DT::renderDataTable crea tablas interactivas con búsqueda/ordenación. downloadHandler deja a los usuarios exportar datos. .data[[]] habilita selección dinámica de columnas en ggplot. Para datos grandes, use el procesamiento del lado servidor de DT o plotly para gráficos interactivos. renderImage muestra archivos pre-generados (más rápido que renderPlot para visuales complejos). Combine entradas con expresiones reactivas para construir dashboards sofisticados y responsivos.
# file upload
fileInput("data", "Upload CSV:", accept = ".csv")
# in server: input$data$datapath (temp file path)
# date range
dateRangeInput("dates", "Period:", start = Sys.Date() - 30, end = Sys.Date())
# tabset with conditional panels
tabsetPanel(
tabPanel("Plot", plotOutput("p")),
tabPanel("Table", DT::dataTableOutput("t")),
tabPanel("Summary", verbatimTextOutput("s"))
)
# render DataTable (interactive)
output$t <- DT::renderDataTable({
datatable(filtered(), filter = "top", options = list(pageLength = 25))
})
# render UI from ggplot
output$p <- renderPlot({
ggplot(filtered(), aes(.data[[input$x]], .data[[input$y]])) +
geom_point() + theme_minimal()
})
# download handlers
output$download <- downloadHandler(
filename = function() paste0("data_", Sys.Date(), ".csv"),
content = function(file) write.csv(filtered(), file)
)
# render image (pre-generated)
output$img <- renderImage({
list(src = "plot.png", contentType = "image/png", width = 400)
}, deleteFile = FALSE)Rendimiento, despliegue y escalado
Rendimiento de Shiny: debounce/throttle entradas rápidas (cajas de búsqueda) para evitar re-cómputo excesivo. Use future + promises para operaciones async (no bloquee el event loop). bindCache cachea resultados render por clave — grandes ganancias para gráficos costosos accedidos por muchos usuarios. Despliegue a shinyapps.io (nube gestionada), RStudio Connect (comercial) o Shiny Server (open source) detrás de Docker. Para alto tráfico, ejecute múltiples procesos worker y balancee carga. Monitorice uso con shinylogs para entender comportamiento del usuario y capturar errores. Haga profiling de apps lentas con profvis::profvis() para encontrar cuellos de botella.
# performance: debounce/throttle rapid inputs
input_debounced <- debounce(reactive({ input$text }), 500)
# async with future
library(future)
plan(multisession)
result <- reactive({
future_promise({ expensive_computation(input$params) })
})
# caching expensive computations
# in UI: add resourcePath or use bindCache
output$plot <- renderPlot({ ... }) %>% bindCache(input$dataset)
# deploy to shinyapps.io
# library(rsconnect)
# deployApp("myapp/")
# run multiple Shiny apps behind a load balancer
# (shinyapps.io / RStudio Connect handle this automatically)
# dockerize for self-hosting
# Dockerfile:
# FROM rocker/shiny
# COPY app.R /srv/shiny-server/
# EXPOSE 3838
# shiny server config (/etc/shiny-server/shiny-server.conf)
# run_as shiny;
# server { listen 3838; location / { site_dir /srv/shiny-server; } }
# monitor with shinylogs or shiny.telemetryPruebas estadísticas e inferencia
Framework de pruebas de hipótesis
Las pruebas de hipótesis evalúan si los datos observados son consistentes con una hipótesis nula. El t-test compara medias (paramétrico, asume normalidad); Wilcoxon es la alternativa no paramétrica. Siempre reporte tamaños de efecto e intervalos de confianza, no solo p-values — p depende del tamaño muestral mientras los CIs muestran significancia práctica. Compruebe asunciones antes de interpretar: normalidad (Shapiro-Wilk), varianza igual (Levene). El análisis de potencia (paquete pwr) determina el tamaño muestral requerido antes de recopilar datos. El umbral 0.05 es convencional, no mágico — considere tamaño de efecto y contexto.
# one-sample t-test: is mean different from hypothesized value?
t.test(mtcars$mpg, mu = 20)
# t = 0.085, df = 31, p-value = 0.933
# 95% CI: [17.92, 22.26]
# mean of x: 20.09
# two-sample t-test
t.test(mpg ~ cyl, data = subset(mtcars, cyl %in% c(4, 6)))
# paired t-test
t.test(pre, post, paired = TRUE)
# Wilcoxon (non-parametric alternative)
wilcox.test(mpg ~ cyl, data = subset(mtcars, cyl %in% c(4,6)))
# interpret results:
# p < 0.05: reject null hypothesis (significant)
# p > 0.05: fail to reject (not enough evidence)
# CI shows plausible range of effect
# check assumptions: normality (shapiro.test), equal variance
# power analysis
library(pwr)
pwr.t.test(d = 0.5, sig.level = 0.05, power = 0.8)
# n = 64 per group needed to detect medium effectANOVA y comparaciones múltiples
ANOVA prueba si las medias difieren entre 3+ grupos. El F-test le dice si existe ALGUNA diferencia; las pruebas post-hoc (Tukey HSD, pairwise.t.test con corrección) identifican CUÁLES grupos difieren. Siempre compruebe asunciones (normalidad, homocedasticidad) y use alternativas no paramétricas (Kruskal-Wallis) si se violan. Para medidas repetidas o datos jerárquicos, use modelos de efectos mixtos (lme4::lmer) que manejan la correlación within-subject apropiadamente. Las correcciones de comparación múltiple (Bonferroni, FDR) previenen falsos positivos al ejecutar muchas pruebas. El paquete afex simplifica el ANOVA de medidas repetidas.
# one-way ANOVA: compare means across 3+ groups
fit <- aov(mpg ~ factor(cyl), data = mtcars)
summary(fit)
# Df Sum Sq Mean Sq F value Pr(>F)
# factor(cyl) 2 824.8 412.4 39.7 4.98e-09
# check assumptions
plot(fit) # residuals vs fitted, QQ
shapiro.test(resid(fit)) # normality of residuals
library(car); leveneTest(fit) # equal variance
# post-hoc tests (which groups differ?)
TukeyHSD(fit) # Tukey HSD
pairwise.t.test(mtcars$mpg, mtcars$cyl, p.adjust = "bonferroni")
# two-way ANOVA with interaction
fit2 <- aov(mpg ~ cyl * am, data = mtcars)
summary(fit2)
# Kruskal-Wallis (non-parametric ANOVA)
kruskal.test(mpg ~ factor(cyl), data = mtcars)
# repeated measures ANOVA
# library(afex)
# aov_ez(id = "subject", dv = "score", data = df, within = "condition")
# mixed-effects models (lme4)
library(lme4)
lmer(score ~ treatment + (1|subject), data = df)Correlación y diagnósticos de regresión
La correlación mide asociación lineal (-1 a 1); cor.test añade inferencia. La regresión lineal (lm) ajusta y = β0 + β1*x + ε. Los cuatro gráficos diagnósticos revelan violaciones de asunciones: no linealidad, residuos no normales, heterocedasticidad y puntos influyentes. VIF > 5-10 indica multicolinealidad (predictores demasiado correlacionados). La distancia de Cook identifica observaciones influyentes. Use intervalos de confianza (respuesta media) vs intervalos de predicción (nueva observación) apropiadamente. Compare modelos con anova (anidados) o AIC/BIC (no anidados, menor es mejor). Siempre visualice antes de confiar en p-values.
# correlation
cor(mtcars$mpg, mtcars$wt) # -0.867
cor.test(mtcars$mpg, mtcars$wt) # with p-value
cor(mtcars[, c("mpg","wt","hp","disp")]) # correlation matrix
library(corrplot); corrplot(cor(mtcars[,1:4]))# visualize
# linear regression
fit <- lm(mpg ~ wt + hp, data = mtcars)
summary(fit)
confint(fit) # CIs for coefficients
# diagnostics
par(mfrow = c(2,2)); plot(fit)
# 1. Residuals vs Fitted: check linearity
# 2. Normal Q-Q: check normality
# 3. Scale-Location: check homoscedasticity
# 4. Residuals vs Leverage: check influential points
# check for multicollinearity
library(car); vif(fit) # variance inflation factor
# influential observations
cooks.distance(fit) |> plot()
influence.measures(fit)
# predictions with intervals
predict(fit, newdata, interval = "confidence") # CI for mean
predict(fit, newdata, interval = "prediction") # PI for new obs
# compare nested models
anova(fit1, fit2) # F-test
AIC(fit1, fit2); BIC(fit1, fit2) # information criteriaDatos categóricos: Chi-cuadrado y Fisher
Chi-cuadrado prueba independencia entre variables categóricas; la prueba exacta de Fisher es más precisa para muestras pequeñas (conteos esperados < 5). Compruebe los conteos esperados antes de confiar en resultados de chi-cuadrado. Goodness-of-fit compara observado con proporciones teóricas. McNemar prueba datos nominales pareados (antes/después). Los tamaños de efecto (V de Cramer, phi) cuantifican la fuerza de asociación más allá de p-values. Los gráficos de mosaico visualizan tablas de contingencia intuitivamente. Para datos ordinales, considere correlación de Spearman o pruebas de tendencia. El paquete vcd (Visualizing Categorical Data) proporciona herramientas completas.
# contingency table
tbl <- table(mtcars$cyl, mtcars$am)
# 0 1
# 4 3 8
# 6 4 3
# 8 12 2
# chi-square test of independence
chisq.test(tbl)
# X-squared = 8.74, df = 2, p-value = 0.0126
# expected counts (should be > 5 for valid chi-square)
chisq.test(tbl)$expected
# Fisher's exact test (small samples)
fisher.test(tbl)
# goodness of fit (one variable vs expected proportions)
chisq.test(c(10, 20, 30), p = c(1/3, 1/3, 1/3))
# McNemar's test (paired nominal data)
mcnemar.test(table(pre, post))
# Cochran-Mantel-Haenszel (stratified)
mantelhaen.test(tbl3d)
# visualize
library(ggplot2); library(ggmosaic)
ggplot(as.data.frame(tbl)) +
geom_mosaic(aes(weight = Freq, x = product(Var1), fill = Var2))
# effect size
library(vcd); assocstats(tbl) # Cramer's V, phiInferencia bayesiana con brms
La inferencia bayesiana (via brms, que envuelve Stan) proporciona distribuciones posteriores completas en lugar de estimaciones puntuales. Especifique priors para codificar conocimiento de dominio; los priors débilmente informativos (normal(0, 10)) regularizan sin sesgar. Los resúmenes posteriores dan intervalos creíbles (declaraciones de probabilidad directa, a diferencia de los CIs frecuentistas). pp_check valida el ajuste del modelo comparando datos simulados con observados. LOO-CV y WAIC comparan modelos via precisión predictiva validada cruzadamente. Los modelos jerárquicos manejan datos agrupados naturalmente. Los métodos bayesianos brillan para muestras pequeñas, modelos complejos y cuando necesita cuantificación de incertidumbre.
library(brms)
# Bayesian linear regression
fit <- brm(
mpg ~ wt + hp,
data = mtcars,
family = gaussian(),
prior = c(
prior(normal(0, 10), class = "b"), # weakly informative
prior(cauchy(0, 2), class = "sigma")
),
chains = 4, cores = 4, iter = 2000
)
# summary
summary(fit)
plot(fit) # posterior distributions
pp_check(fit) # posterior predictive check
# extract posterior samples
posterior <- as_draws_df(fit)
mean(posterior$b_wt > 0) # P(coefficient > 0)
# predictions
posterior_predict(fit, newdata) |> as.data.frame() -> preds
# model comparison
fit_null <- brm(mpg ~ 1, data = mtcars, ...)
loo(fit, fit_null) # leave-one-out CV
waic(fit, fit_null)
# hierarchical model
fit_h <- brm(
score ~ treatment + (1 + treatment|subject),
data = df, family = gaussian()
)
# Stan code behind the model
stancode(fit)Machine Learning con caret
División de datos y preprocesamiento
La división y preprocesamiento apropiados de datos son el 80% del éxito ML. createDataPartition hace muestreo estratificado (preserva balance de clases). trainControl configura remuestreo (CV, bootstrap, CV repetido). preProcess maneja estandarización, transformación, PCA e imputación — siempre ajuste solo en datos de entrenamiento y aplique a test para evitar fuga. nzv elimina columnas no informativas. Para series temporales, use createTimeSlices en lugar de CV aleatorio. La interfaz unificada de caret significa que el mismo preprocesamiento funciona en todos los tipos de modelo.
library(caret)
# split data
set.seed(42)
idx <- createDataPartition(iris$Species, p = 0.8, list = FALSE)
train <- iris[idx, ]; test <- iris[-idx, ]
# stratified split for classification
# createDataPartition preserves class proportions
# k-fold cross-validation
ctrl <- trainControl(
method = "cv", number = 10,
savePredictions = "final",
classProbs = TRUE, # for AUC
summaryFunction = multiClassSummary
)
# preprocessing pipeline
preproc <- preProcess(train[, -5],
method = c("center","scale","YeoJohnson","nzv"))
train_processed <- predict(preproc, train[, -5])
test_processed <- predict(preproc, test[, -5])
# common preprocessing methods:
# center, scale: standardize
# BoxCox, YeoJohnson: transform to normality
# pca: principal components
# nzv: remove near-zero variance
# knnImpute, bagImpute: impute missing values
# dummy variables for categorical
dummies <- dummyVars(Species ~ ., data = train)
predict(dummies, train)Entrenamiento de modelos y tuning
La función train() de caret proporciona una interfaz unificada a 200+ modelos — solo cambie la cadena method. tuneLength auto-genera una cuadrícula de tuning; tuneGrid da control total. resamples() compara múltiples modelos via rendimiento remuestreado (más honesto que evaluación de un único conjunto de test). Siempre use el mismo trControl entre modelos para comparación justa. El dotplot de resamples muestra solapamiento en rendimiento — si los CIs se solapan, los modelos no son significativamente diferentes. Elija el modelo más simple dentro de un error estándar del mejor (la 'regla one-SE').
library(caret)
# train a random forest
ctrl <- trainControl(method = "cv", number = 5)
rf_fit <- train(
Species ~ ., data = train,
method = "rf",
trControl = ctrl,
tuneGrid = expand.grid(mtry = 1:4),
tuneLength = 5 # auto-tune grid
)
print(rf_fit) # shows accuracy by tuning param
plot(rf_fit) # tuning curve
# try multiple models
models <- c("rf","gbm","svmRadial","knn","rpart")
fits <- lapply(models, function(m) {
train(Species ~ ., data = train, method = m, trControl = ctrl)
})
names(fits) <- models
# compare models
resamps <- resamples(fits)
summary(resamps)
dotplot(resamps, metric = "Accuracy")
# custom tuning grid
grid <- expand.grid(
mtry = c(2, 4, 8),
splitrule = c("gini","extratrees"),
min.node.size = c(1, 5, 10)
)
fit <- train(Species ~ ., data = train, method = "ranger",
trControl = ctrl, tuneGrid = grid)Métricas de clasificación y matriz de confusión
La precisión sola es engañosa para datos desbalanceados. confusionMatrix proporciona por clase sensitivity (recall), specificity, precision y F1. Para problemas binarios, ROC-AUC mide discriminación; las curvas PR son mejores cuando la clase positiva es rara. Para multi-clase, use métricas macro/micro-promediadas o log-loss. Siempre evalúe en un conjunto de test retenido (o via CV anidado para estimaciones no sesgadas). En caret, establezca summaryFunction en trainControl para optimizar la métrica correcta (p. ej., mnLogLoss para predicciones probabilísticas). Reporte intervalos de confianza en rendimiento, no solo estimaciones puntuales.
# predictions
pred <- predict(rf_fit, test)
prob <- predict(rf_fit, test, type = "prob")
# confusion matrix
cm <- confusionMatrix(pred, test$Species)
print(cm)
# Reference
# Prediction setosa versicolor virginica
# setosa 10 0 0
# versicolor 0 10 1
# virginica 0 0 9
# Overall Accuracy: 0.9667
# byClass: Sensitivity, Specificity, etc.
# two-class metrics
cm$byClass[, c("Sensitivity","Specificity","Precision","Recall","F1")]
# ROC and AUC
library(pROC)
roc_curve <- roc(test$Species == "versicolor", prob$versicolor)
auc(roc_curve)
plot(roc_curve)
# multi-class AUC
library(pRoc)
multiclass.roc(test$Species, prob)
# precision-recall curve (better for imbalanced data)
library(PRROC)
pr <- pr.curve(scores.class0 = prob$versicolor,
weights.class0 = test$Species == "versicolor",
curve = TRUE)
plot(pr)
# custom metrics in trainControl
twoClassSummary # built-in for 2-class
mnLogLoss # multi-class log lossSelección de características e interpretación
La selección de características mejora el rendimiento y la interpretabilidad del modelo. RFE (recursive feature elimination) envuelve un modelo y elimina iterativamente las características menos importantes. varImp extrae importancia de cualquier modelo entrenado con caret (random forest, gbm, etc.). Los métodos de filtro (findCorrelation, findLinearCombos) eliminan características redundantes antes del entrenamiento. Para interpretación de caja negra, los valores SHAP (fastshap, shapviz) atribuyen predicciones a características. Siempre realice selección de características dentro de validación cruzada para evitar sesgo de selección. Los modelos más simples con menos características a menudo generalizan mejor.
library(caret)
# recursive feature elimination (RFE)
ctrl <- rfeControl(functions = rfFuncs, method = "cv", number = 10)
rfe_fit <- rfe(train[,-5], train$Species,
sizes = c(1:4), rfeControl = ctrl)
print(rfe_fit) # optimal subset
predictors(rfe_fit) # selected features
plot(rfe_fit)
# variable importance from trained model
varImp(rf_fit) # caret extracts importance
plot(varImp(rf_fit))
# filter methods (before training)
# remove highly correlated features
cor_matrix <- cor(train[,-5])
high_cor <- findCorrelation(cor_matrix, cutoff = 0.9)
train_filtered <- train[,-high_cor]
# remove linear dependencies
findLinearCombos(train[,-5])
# genetic algorithm / simulated annealing selection
# gafsFit <- gafs(x, y, iters = 20)
# safsFit <- safs(x, y, iters = 20)
# SHAP values (model-agnostic interpretation)
# library(fastshap)
# explain(rf_fit, X = test, nsim = 100)Ensembles y stacking
Los ensembles combinan múltiples modelos para mejor rendimiento que cualquier modelo único. caretEnsemble entrena modelos con remuestreo idéntico (requerido para stacking justo). caretStack entrena un meta-modelo sobre predicciones base — el meta-modelo aprende cuándo confiar en cada modelo base. El promedio simple funciona sorprendentemente bien para modelos de precisión similar. Los ensembles ponderados le permiten enfatizar mejores modelos. Bagging (treebag) reduce varianza promediando modelos bootstrap. La diversidad de modelos base importa más que su precisión individual — combine modelos que cometen diferentes errores.
library(caretEnsemble)
# train multiple models with same resampling
ctrl <- trainControl(method = "cv", number = 5,
savePredictions = "final",
classProbs = TRUE)
models <- caretList(
Species ~ ., data = train,
trControl = ctrl,
methodList = c("rf","gbm","svmRadial","knn")
)
# simple ensemble (average predictions)
ens <- caretEnsemble(models)
summary(ens)
plot(ens)
# stack: train a meta-model on base predictions
stack <- caretStack(models, method = "glm",
metric = "Accuracy", trControl = ctrl)
print(stack)
# predict with ensemble
pred_ens <- predict(ens, test)
pred_stack <- predict(stack, test)
# weighted ensemble (custom weights)
weights <- c(0.4, 0.3, 0.2, 0.1)
probs <- lapply(models, function(m) predict(m, test, type = "prob"))
final_prob <- Reduce('+', Map(function(p, w) p * w, probs, weights))
# bagging (bootstrap aggregating)
bag_fit <- train(Species ~ ., data = train, method = "treebag",
trControl = ctrl)Familia Apply y rendimiento
apply, lapply, sapply, vapply
La familia apply es el toolkit de programación funcional de R base. apply funciona en arrays (use margin 1 para filas, 2 para columnas) pero los rowSums/colSums integrados son más rápidos. lapply siempre devuelve una lista; sapply intenta simplificar a un vector (conveniente pero type-unstable). vapply es la versión segura — especifica la plantilla de salida, por lo que errora en desajuste en lugar de coaccionar silenciosamente. Use vapply en código de producción, sapply interactivamente. replicate es útil para simulaciones. mapply (o Map) itera sobre múltiples argumentos en paralelo. Para código moderno, prefiera la familia map de purrr por consistencia.
# apply: over array margins (rows or columns)
m <- matrix(1:12, 3, 4)
apply(m, 1, sum) # row sums: 22 26 30
apply(m, 2, mean) # column means
apply(m, c(1,2), sqrt) # element-wise (silly but valid)
# built-in row/col functions are faster
rowSums(m); rowMeans(m)
colSums(m); colMeans(m)
# lapply: list in, list out
lapply(list(a=1:3, b=4:6), mean) # list(a=2, b=5)
lapply(mtcars, class) # class of each column
# sapply: list in, vector out (simplifies)
sapply(mtcars, mean) # named numeric vector
sapply(mtcars, is.numeric) # logical vector
# vapply: type-safe sapply (specify output template)
vapply(mtcars, mean, numeric(1)) # always numeric(1)
vapply(mtcars, class, character(1)) # always character(1)
# replicate: repeat expression n times
replicate(5, rnorm(3)) # 3x5 matrix
replicate(100, mean(rexp(30))) # 100 sample means
# mapply: multivariate map
mapply(rep, 1:3, 3:1) # list(1,1,1, 2,2, 3)
mapply(function(a,b) a+b, 1:3, 4:6) # 5 7 9Vectorización y velocidad
La vectorización es la optimización de rendimiento #1 de R. La aritmética, comparación y funciones matemáticas de R operan en vectores enteros via código C optimizado — los bucles en R son interpretados y lentos. ifelse está vectorizado pero todavía tiene sobrecarga; la indexación lógica directa (x * (x > 5)) es la más rápida. Evite apply en data frames (coacciona a matriz); use operaciones de columna vectorizadas. Siempre preasigne vectores de resultado — hacerlos crecer con c() es O(n^2). Para bucles verdaderamente calientes, Rcpp le permite escribir C++ inline. Microbenchmark con microbenchmark para verificar mejoras; system.time es demasiado grueso.
# BAD: element-wise loop
x <- 1:1e6
y <- numeric(length(x))
for (i in seq_along(x)) y[i] <- x[i]^2
# GOOD: vectorized
y <- x^2 # ~100x faster
# ifelse vs vectorized
# BAD
y <- numeric(length(x))
for (i in seq_along(x)) {
y[i] <- if (x[i] > 5) x[i] else 0
}
# GOOD
y <- ifelse(x > 5, x, 0)
# BEST (fastest)
y <- x * (x > 5)
# row-wise operations (avoid if possible)
df <- data.frame(a = 1:1000, b = 1001:2000)
# BAD
df$sum <- apply(df, 1, sum)
# GOOD
df$sum <- df$a + df$b
# preallocate!
n <- 1000
result <- numeric(n) # not result <- c()
for (i in 1:n) result[i] <- i^2
# use Rcpp for hot loops
# library(Rcpp)
# cppFunction('double sumc(NumericVector x) { return sum(x); }')
# benchmark
library(microbenchmark)
microbenchmark(
loop = {y <- numeric(length(x)); for(i in seq_along(x)) y[i] <- x[i]^2},
vectorized = x^2,
times = 10
)Memoria y data.table
data.table es dramáticamente más rápido y eficiente en memoria que data.frame/dplyr para datos grandes (1M+ filas). La sintaxis dt[i, j, by] combina filtrado, selección y agrupación en una expresión. La semántica de referencia (:=) modifica in situ sin copiar — crucial para memoria. setkey crea un índice habilitando búsquedas binarias y merges rápidos. fread/fwrite son 5-10x más rápidos que read.csv/write.csv. Para datos que caben en memoria, data.table a menudo supera incluso Spark. El trade-off es una curva de aprendizaje más pronunciada y sintaxis menos legible comparado con dplyr.
# data.table: faster, memory-efficient alternative to data.frame
library(data.table)
dt <- data.table(mtcars)
# syntax: dt[i, j, by]
dt[mpg > 20, .(mean_hp = mean(hp)), by = .(cyl, gear)]
# cyl gear mean_hp
# 1: 4 4 76.0
# 2: 4 5 102.0
# reference semantics (modify in place, no copy)
dt[, mpg_dbl := mpg * 2] # add column in place
dt[1:5, mpg := 0] # modify subset in place
dt[, c("a","b") := .(mpg*2, hp/10)] # multiple columns
# setkey for fast lookups and joins
setkey(dt, cyl)
dt[.(4)] # all rows where cyl == 4 (binary search)
dt[.(4), mean(mpg)] # fast aggregation
# joins
dt1 <- data.table(id = 1:3, x = letters[1:3])
dt2 <- data.table(id = 2:4, y = LETTERS[2:4])
setkey(dt1, id); setkey(dt2, id)
dt1[dt2] # right join
dt2[dt1] # left join
merge(dt1, dt2, by = "id") # inner join
# fread/fwrite: fast I/O
big <- fread("huge.csv") # ~10x faster than read.csv
fwrite(big, "out.csv")Computación paralela
R es single-threaded por defecto, pero el paralelismo es sencillo. El paquete parallel (integrado) proporciona mclapply (fork, solo Linux/Mac) y parLapply (clusters, todas las plataformas). foreach + doParallel es una alternativa popular. El ecosistema future (con furrr) es moderno y unificado — cambie backends cambiando plan(). Para caret, establezca allowParallel = TRUE y registre un backend. Siempre exporte las variables necesarias y cargue paquetes en los workers. El paralelismo tiene sobrecarga — solo ayuda cuando cada tarea es sustancial (>100ms). Haga benchmark para verificar aceleración; la ley de Amdahl limita las ganancias.
# parallel package (built-in)
library(parallel)
ncores <- detectCores() - 1
# parallel lapply
cl <- makeCluster(ncores)
results <- parLapply(cl, 1:100, function(i) slow_function(i))
stopCluster(cl)
# foreach with doParallel
library(foreach); library(doParallel)
registerDoParallel(ncores)
results <- foreach(i = 1:100, .combine = "c") %dopar% {
slow_function(i)
}
stopImplicitCluster()
# future ecosystem (modern, flexible)
library(future); library(furrr)
plan(multisession, workers = ncores) # or multicore (Linux/Mac)
results <- future_map(1:100, slow_function)
# parallel caret
library(caret)
ctrl <- trainControl(method = "cv", number = 10, allowParallel = TRUE)
fit <- train(y ~ ., data = train, method = "rf", trControl = ctrl)
# benchmark
library(microbenchmark)
microbenchmark(
serial = lapply(1:100, slow_function),
parallel = parLapply(makeCluster(4), 1:100, slow_function),
times = 5
)
# export variables to workers
clusterExport(cl, c("my_data", "my_function"))
clusterEvalQ(cl, library(dplyr))Profiling y flujo de optimización
Haga profiling antes de optimizar — la intuición sobre cuellos de botella suele ser errónea. profvis proporciona un flame graph interactivo mostrando tiempo por línea y llamada. Rprof es el equivalente de R base. Rprofmem rastrea asignaciones. object.size mide memoria; gc() fuerza recolección de basura y reporta uso. La jerarquía de optimización: (1) vectorizar, (2) preasignar, (3) cambiar a data.table, (4) Rcpp para bucles irreducibles, (5) paralelizar. memoise cachea resultados de funciones — ideal para funciones puras costosas llamadas repetidamente con los mismos args. Siempre mida antes y después para confirmar mejoras.
# profile to find bottlenecks
library(profvis)
profvis({
result <- my_analysis(big_data)
})
# opens interactive flame graph
# Rprof (base R)
Rprof("profile.out")
my_analysis(big_data)
Rprof(NULL)
summaryRprof("profile.out")
# memory profiling
Rprofmem("mem.out")
my_analysis(big_data)
Rprofmem(NULL)
# object sizes
object.size(my_data) # bytes
format(object.size(my_data), "MB")
# find memory hogs
sort(sapply(ls(), function(x) object.size(get(x))))
# garbage collection
gc() # force collection, show memory
gcinfo(TRUE) # report on auto GC
# common optimizations:
# 1. Vectorize (avoid loops)
# 2. Preallocate
# 3. Use data.table instead of data.frame
# 4. Use Rcpp for hot loops
# 5. Avoid growing objects (c(), rbind())
# 6. Use appropriate data types (integer vs double)
# 7. Cache expensive computations (memoise)
library(memoise)
slow_cached <- memoise(slow_function)dplyr en profundidad
Verbos core
Los cinco verbos core de dplyr: filter (filas por condición), select (columnas), mutate (nuevas columnas), arrange (ordenar), summarize (agregar). Encadene con %>%. group_by + summarize es el caballo de batalla para agregación. na.rm = TRUE es esencial — de lo contrario cualquier NA en los datos hace el resumen NA.
library(dplyr)
starwars %>%
filter(species == "Human", height > 170) %>%
select(name, height, mass, homeworld) %>%
mutate(bmi = mass / (height/100)^2) %>%
arrange(desc(height)) %>%
slice_head(n = 5)
# group_by + summarize
starwars %>%
group_by(species) %>%
summarize(
n = n(),
avg_height = mean(height, na.rm = TRUE),
avg_mass = mean(mass, na.rm = TRUE)
) %>%
arrange(desc(n)) %>%
filter(n >= 2)Joins
Los mutating joins combinan columnas; los filtering joins subconjuntan filas. left_join es el más común — mantiene todas las filas de x, rellena NA para no coincidencias. Siempre compruebe si hay claves duplicadas en la tabla derecha (causa multiplicación de filas). semi_join/anti_join son ideales para filtrar basándose en otra tabla sin traer sus columnas.
library(dplyr)
# mutating joins (add columns from y to x)
left_join(x, y, by = "id") # all rows in x
right_join(x, y, by = "id") # all rows in y
inner_join(x, y, by = "id") # only matching rows
full_join(x, y, by = "id") # all rows from both
# different column names
left_join(x, y, by = c("id" = "user_id"))
# multiple keys
left_join(x, y, by = c("first", "last"))
# filtering joins (filter x, no columns added)
semi_join(x, y, by = "id") # rows in x that match y
anti_join(x, y, by = "id") # rows in x that DON'T match y
# check for duplicates
x %>% count(id) %>% filter(n > 1)Funciones de ventana
Las funciones de ventana computan valores a través de filas relacionadas con la fila actual. lag/lead acceden a filas previas/siguientes — esenciales para series temporales. cumsum/cummean son agregados acumulativos. slice_max/slice_min son atajos para top-n por grupo. Siempre group_by primero para computar dentro de grupos.
library(dplyr)
# row numbering and ranking
df %>% group_by(group) %>%
mutate(
row_num = row_number(),
rank = rank(value),
dense_rank = dense_rank(value),
min_rank = min_rank(value)
)
# offsets
df %>% group_by(group) %>%
mutate(
prev = lag(value),
next = lead(value, 2),
diff = value - lag(value)
)
# cumulative
df %>% group_by(group) %>%
mutate(
cum_sum = cumsum(value),
cum_mean = cummean(value),
cum_max = cummax(value),
cum_min = cummin(value)
)
# top n per group
df %>% group_by(group) %>%
slice_max(value, n = 3) # top 3
df %>% group_by(group) %>%
slice_min(value, n = 2)across y múltiples columnas
across (dplyr 1.0+) reemplaza los viejos sufijos _at, _if, _all. Use where(is.numeric) para elegir columnas por predicado. El pronombre .x se refiere a la columna actual dentro de lambdas (~). rename_with renombra columnas usando una función. across es la forma moderna y consistente de operar en múltiples columnas.
library(dplyr)
# apply function to multiple columns
starwars %>%
mutate(across(where(is.numeric), ~ replace_na(.x, 0)))
# summarize multiple columns
starwars %>%
summarize(across(where(is.numeric), mean, na.rm = TRUE))
# rename multiple columns
starwars %>%
rename_with(tolower, everything())
# transform specific columns
df %>%
mutate(across(c(height, mass), ~ .x * 0.453592)) # lb to kg
# multiple functions
df %>%
summarize(across(value, list(mean = mean, sd = sd, n = ~ n()), na.rm = TRUE))
# conditional transformation
df %>%
mutate(across(where(is.character), tolower))Patrones de summarize
summarize reduce grupos a valores únicos. n() cuenta filas; n_distinct() cuenta valores únicos. Siempre pase na.rm = TRUE a funciones estadísticas o los NAs se propagan. across + list le permite computar múltiples estadísticas a la vez. count() es atajo para group_by + summarize(n = n()) + ungroup.
library(dplyr)
# basic
df %>%
group_by(category) %>%
summarize(
count = n(),
distinct = n_distinct(id),
total = sum(value, na.rm = TRUE),
avg = mean(value, na.rm = TRUE),
median = median(value, na.rm = TRUE),
sd = sd(value, na.rm = TRUE),
min = min(value, na.rm = TRUE),
max = max(value, na.rm = TRUE),
first = first(value),
last = last(value),
q25 = quantile(value, 0.25, na.rm = TRUE)
)
# multiple summary stats at once
df %>%
group_by(category) %>%
summarize(
across(value, list(mean = mean, sd = sd, n = ~ n()), na.rm = TRUE)
)
# count and proportions
df %>%
count(category) %>%
mutate(pct = n / sum(n))ggplot2 avanzado
Capas y estéticas
ggplot construye gráficos en capas conectadas por +. aes() mapea columnas de datos a propiedades visuales. geom_* define la geometría; scale_* controla ejes/colores; labs etiqueta todo; theme_* estiliza elementos no de datos. facet_wrap divide por una variable; facet_grid hace una cuadrícula 2D de dos variables.
library(ggplot2)
ggplot(mpg, aes(displ, hwy, color = class)) +
geom_point(size = 2, alpha = 0.7) +
geom_smooth(method = "lm", se = TRUE) +
scale_color_brewer(palette = "Set2") +
labs(
title = "Fuel efficiency by engine size",
subtitle = "Source: EPA mpg dataset",
x = "Engine displacement (L)",
y = "Highway MPG",
color = "Vehicle class"
) +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom")
# facets
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_wrap(~ class, ncol = 4)
ggplot(mpg, aes(displ, hwy)) +
geom_point() +
facet_grid(drv ~ cyl)Escalas y coordenadas
Las funciones scale_* controlan cómo los datos se mapean a propiedades visuales. scale_x_log10 transforma logarítmicamente; scale_color_viridis_c da colormaps perceptualmente uniformes. coord_cartesian hace zoom sin soltar datos (a diferencia de xlim). coord_flip intercambia ejes. coord_polar convierte barras en porciones de pastel. scale_x_date formatea ejes de fecha.
ggplot(mpg, aes(displ, hwy, color = cty)) +
geom_point() +
scale_x_log10() +
scale_y_continuous(limits = c(10, 50), breaks = seq(10, 50, 5)) +
scale_color_viridis_c(option = "plasma") +
coord_cartesian(xlim = c(1, 7)) # zoom without removing data
# coord flip
ggplot(mpg, aes(class, hwy)) +
geom_boxplot() +
coord_flip()
# coord polar (pie chart from bar)
ggplot(mtcars, aes(factor(1), fill = factor(cyl))) +
geom_bar(width = 1) +
coord_polar(theta = "y")
# date axis
ggplot(economics, aes(date, unemploy)) +
geom_line() +
scale_x_date(date_labels = "%Y", date_breaks = "5 years")Temas y personalización
theme() controla cada elemento no de datos. element_text/rect/line/blank son los bloques de construcción. Ajustes comunes: rotar etiquetas del eje x (angle, hjust), títulos en negrita, ocultar cuadrícula menor (panel.grid.minor = element_blank()). ggsave exporta a PNG/PDF/SVG — especifique dimensiones en pulgadas y dpi para formatos raster.
library(ggplot2)
p <- ggplot(mpg, aes(class, hwy)) + geom_boxplot()
# built-in themes
p + theme_minimal()
p + theme_classic()
p + theme_bw()
# custom theme
p + theme(
panel.background = element_rect(fill = "white"),
panel.grid.major = element_line(color = "gray90"),
panel.grid.minor = element_blank(),
axis.text.x = element_text(angle = 45, hjust = 1, size = 10),
axis.title = element_text(face = "bold"),
plot.title = element_text(size = 16, hjust = 0.5),
plot.subtitle = element_text(color = "gray40"),
legend.position = "right",
legend.key = element_blank(),
strip.background = element_rect(fill = "lightblue"),
strip.text = element_text(face = "bold")
)
# save
ggsave("plot.png", width = 8, height = 6, dpi = 300)
ggsave("plot.pdf", device = cairo_pdf)Estadísticas y suavizado
stat_summary computa resúmenes personalizados (mean, median, mean_se, mean_cl_normal). geom_smooth añade líneas de tendencia — method='lm' para lineal, 'loess' para regresión local, 'gam' para aditivo generalizado. geom_density/geom_density_2d muestran distribuciones. geom_violin muestra la forma completa de distribución junto a boxplots.
library(ggplot2)
# stat_summary for custom summaries
ggplot(mtcars, aes(factor(cyl), mpg)) +
stat_summary(fun = "mean", geom = "point", size = 3) +
stat_summary(fun.data = "mean_se", geom = "errorbar")
# smooth
ggplot(mtcars, aes(wt, mpg)) +
geom_point() +
geom_smooth(method = "lm", formula = y ~ x, se = TRUE) +
geom_smooth(method = "loess", se = FALSE, color = "red")
# density
ggplot(iris, aes(Sepal.Length, fill = Species)) +
geom_density(alpha = 0.5)
# 2d density
ggplot(diamonds, aes(carat, price)) +
geom_density_2d() +
scale_x_log10() + scale_y_log10()
# histograms with binning
ggplot(diamonds, aes(price)) +
geom_histogram(bins = 50, fill = "steelblue") +
scale_x_log10()
# boxplot and violin
ggplot(iris, aes(Species, Sepal.Length)) +
geom_boxplot() +
geom_violin(alpha = 0.3, fill = "orange")Extensiones y patchwork
patchwork combina múltiples gráficos con + (lado a lado), / (apilados) y plot_layout para control fino. plot_annotation añade títulos generales y tags (A, B, C...). El ecosistema de extensiones de ggplot2 es enorme: ggrepel para etiquetas, ggridges para gráficos de cresta, gganimate para animaciones, ggiraph para interactividad, geom_sf para mapas.
library(ggplot2)
library(patchwork)
p1 <- ggplot(mtcars, aes(wt, mpg)) + geom_point()
p2 <- ggplot(mtcars, aes(factor(cyl))) + geom_bar()
p3 <- ggplot(mtcars, aes(mpg)) + geom_histogram(bins = 10)
p4 <- ggplot(mtcars, aes(factor(cyl), mpg)) + geom_boxplot()
# combine plots
p1 + p2 # side by side
p1 / p2 # stacked
(p1 + p2) / p3 # grouped
p1 + p2 + p3 + plot_layout(nrow = 2, byrow = FALSE)
# annotations
p1 + p2 + plot_annotation(
title = "MT cars analysis",
tag_levels = "A"
)
# other extensions:
# ggrepel: non-overlapping labels
# ggridges: joy plots
# gganimate: animated plots
# ggiraph: interactive
# ggplot2::geom_sf: maps
library(ggrepel)
ggplot(mtcars, aes(wt, mpg, label = rownames(mtcars))) +
geom_point() +
geom_text_repel()Data wrangling con tidyr
Pivot longer y wider
pivot_longer/pivot_wider (reemplazando gather/spread) reshape datos. El formato long es mejor para ggplot y agregación dplyr; el wide es mejor para lectura humana. names_pattern con .value le permite dividir en múltiples columnas basándose en la estructura del nombre de columna. Siempre especifique cols, names_to y values_to explícitamente.
library(tidyr)
# wide to long
# id q1 q2 q3
# A 10 20 30
wide_data %>%
pivot_longer(
cols = starts_with("q"),
names_to = "quarter",
values_to = "score"
)
# id quarter score
# A q1 10
# A q2 20
# A q3 30
# long to wide
long_data %>%
pivot_wider(
names_from = quarter,
values_from = score
)
# multiple value columns
df %>%
pivot_longer(
cols = c(starts_with("q"), starts_with("r")),
names_to = c(".value", "quarter"),
names_pattern = "([a-z])([0-9])"
)Separate y unite
separate divide una columna en muchas; unite combina muchas en una. separate_rows divide en múltiples filas (útil para listas de tags). extract usa grupos de captura regex. Todas toman un argumento sep (por defecto no alfanumérico). Convierta tipos automáticamente con convert = TRUE en separate.
library(tidyr)
# split a column
df <- tibble(x = c("a_1", "b_2", "c_3"))
df %>% separate(x, c("letter", "number"), sep = "_")
# letter number
# a 1
# b 2
# split into rows
df %>% separate_rows(x, sep = "_")
# x
# a
# 1
# b
# 2
# unite columns
df %>%
separate(x, c("letter", "number")) %>%
unite("combined", letter, number, sep = "-")
# combined
# a-1
# b-2
# extract with regex
df %>% extract(x, c("letter", "number"), "([a-z])_([0-9])")Valores faltantes
replace_na rellena NAs con constantes; fill propaga valores (ideal para series temporales); drop_na elimina filas incompletas; coalesce elige el primer no-NA a través de columnas. complete expande a todas las combinaciones de columnas especificadas (como un producto cartesiano) — útil para asegurar que los grupos faltantes aparezcan en resúmenes.
library(tidyr)
# replace NA
df %>% replace_na(list(value = 0, name = "unknown"))
# fill NA with previous/next value
df %>% fill(value, .direction = "down") # forward fill
df %>% fill(value, .direction = "up") # backward fill
df %>% fill(value, .direction = "downup") # down then up
# drop rows with NA
df %>% drop_na()
df %>% drop_na(value) # only specific columns
# detect NA
df %>% filter(!is.na(value))
sum(is.na(df$value))
df %>% mutate_all(~ sum(is.na(.))) # NA count per column
# coalesce: first non-NA
df %>% mutate(value = coalesce(value, fallback, 0))
# complete: expand combinations
df %>% complete(group, year) # all group-year combos, NA filled
df %>% complete(group, year, fill = list(value = 0))Nesting y columnas de lista
nest empaqueta filas agrupadas en list-columns — la base de split-apply-combine con purrr. unnest lo revierte. unnest_wider esparce elementos de lista en columnas; unnest_longer los esparce en filas. Las list-columns le permiten contener objetos arbitrarios (modelos, data frames, vectores) dentro de un tibble.
library(tidyr)
library(dplyr)
# nest: pack rows into list columns
nested <- mtcars %>%
nest(data = -cyl)
# cyl data
# 4 <tibble 11x10>
# 6 <tibble 7x10>
# 8 <tibble 14x10>
# operate on each group
nested %>%
mutate(model = map(data, ~ lm(mpg ~ wt, data = .x)))
# unnest
df %>%
unnest(data)
# unnest specific column
df %>%
unnest_wider(data) # list-col of named lists -> cols
df %>%
unnest_longer(data) # list-col of vectors -> rows
# chop (similar to nest)
df %>% chop(value)
# pack (opposite of unnest)
df %>% pack(x = c(a, b))Tibbles y lectura de datos
Los Tibbles son data frames mejorados: sin conversión de cadena a factor, sin manipulación de row names, mejor impresión. read_csv es mucho más rápido que read.csv y devuelve un tibble. Especifique col_types para evitar sorpresas (p. ej., IDs leídos como numéricos). El argumento na le permite tratar múltiples cadenas como NA. Siempre use readr sobre base para datos tabulares.
library(tibble)
library(readr)
# create tibble
tibble(
x = 1:5,
y = c("a", "b", "c", "d", "e"),
z = runif(5)
)
# tribble: row-wise construction
tribble(
~name, ~age, ~score,
"Alice", 30, 90,
"Bob", 25, 80
)
# read csv
df <- read_csv("data.csv", col_names = TRUE, col_types = "cdd")
df <- read_csv("data.csv", na = c("", "NA", "N/A"))
df <- read_tsv("data.tsv")
df <- read_delim("data.txt", delim = "|")
# write
write_csv(df, "out.csv")
write_tsv(df, "out.tsv")
# read from clipboard (Excel)
df <- read_clipboard()
# column types
# c = character, d = double, i = integer, l = logical, f = factor, D = datePruebas estadísticas
t-tests
Los t-tests comparan medias. Una muestra (vs un valor), dos muestras (entre grupos), pareado (dentro de sujetos). El valor por defecto es Welch (varianzas desiguales) — usualmente lo que quiere. Siempre compruebe asunciones: normalidad (Shapiro) y varianza igual (var.test). Reporte tamaño de efecto (d de Cohen), no solo p-values.
# one-sample t-test
t.test(x, mu = 5)
# H0: mean of x equals 5
# two-sample t-test
t.test(x, y)
t.test(x, y, var.equal = TRUE) # Student's (assume equal var)
t.test(x, y, alternative = "greater")
# paired t-test
t.test(before, after, paired = TRUE)
# output interpretation
result <- t.test(x, y)
result$p.value # < 0.05 -> reject H0
result$conf.int # 95% CI of difference
result$statistic # t value
# check assumptions
shapiro.test(x) # normality
var.test(x, y) # equal variances
# effect size (effsize package)
library(effsize)
cohen.d(x, y)ANOVA
ANOVA prueba diferencias entre 3+ grupos. Use * para diseños factoriales con interacciones. Siempre haga pruebas post-hoc (TukeyHSD) tras un ANOVA significativo para encontrar qué pares difieren. Compruebe homogeneidad de varianza (Levene). Para datos no normales, use Kruskal-Wallis. Para medidas repetidas, use lmer de lme4.
# one-way ANOVA
result <- aov(yield ~ fertilizer, data = df)
summary(result)
# two-way ANOVA with interaction
result <- aov(yield ~ fertilizer + density + fertilizer:density, data = df)
result <- aov(yield ~ fertilizer * density, data = df) # shorthand
# Tukey post-hoc
TukeyHSD(result)
# check assumptions
plot(result) # diagnostic plots
library(car)
leveneTest(yield ~ fertilizer, data = df) # equal variances
# Kruskal-Wallis (non-parametric alternative)
kruskal.test(yield ~ fertilizer, data = df)
# repeated measures
result <- aov(score ~ time + Error(subject/time), data = df)
# mixed effects (lme4)
library(lme4)
model <- lmer(score ~ time + (1 | subject), data = df)
anova(model)Chi-cuadrado y categóricos
Chi-cuadrado prueba si dos variables categóricas son independientes. Las frecuencias esperadas deberían ser >= 5 en cada celda; si no, use la prueba exacta de Fisher. McNemar es para datos binarios pareados (antes/después). Los residuos de Pearson muestran qué celdas se desvían más de lo esperado. assocstats da V de Cramer para tamaño de efecto.
# chi-square test of independence
tbl <- table(df$gender, df$vote)
chisq.test(tbl)
# goodness of fit
chisq.test(table(df$color), p = c(0.25, 0.25, 0.25, 0.25))
# Fisher's exact (small samples)
fisher.test(tbl)
# McNemar (paired binary)
mcnemar.test(tbl)
# expected frequencies
ct <- chisq.test(tbl)
ct$expected
ct$observed
ct$residuals # Pearson residuals
# association measures
library(vcd)
assocstats(tbl)
# visualize
library(ggplot2)
ggplot(df, aes(gender, fill = vote)) +
geom_bar(position = "fill")Correlación
Pearson mide correlación lineal; Spearman/Kendall miden monótona (basada en rangos) — robusta a outliers y no lineal. cor.test da un p-value y CI. corrplot visualiza matrices; ggpairs muestra scatterplots y correlaciones. Use use='pairwise.complete.obs' para manejar datos faltantes sin soltar filas enteras.
# Pearson (linear, normal)
cor(x, y, method = "pearson")
cor.test(x, y, method = "pearson")
# Spearman (rank, non-parametric)
cor(x, y, method = "spearman")
# Kendall (rank, smaller samples)
cor(x, y, method = "kendall")
# correlation matrix
cor(mtcars[, c("mpg", "wt", "hp", "disp")])
# with p-values
library(psych)
corr.test(mtcars[, 1:6])
# visualize
library(corrplot)
M <- cor(mtcars)
corrplot(M, method = "circle", type = "lower", order = "hclust")
# GGally for scatterplot matrix
library(GGally)
ggpairs(mtcars[, c("mpg", "wt", "hp", "cyl")])
# handle missing
cor(df, use = "complete.obs") # listwise deletion
cor(df, use = "pairwise.complete.obs")Pruebas no paramétricas
Las pruebas no paramétricas no asumen normalidad — úselas cuando las muestras son pequeñas, los datos están sesgados o tiene datos ordinales. Mann-Whitney/Wilcoxon son las contrapartes basadas en rangos de los t-tests. Las pruebas de permutación y bootstrap son intensivas en computación pero hacen asunciones mínimas. Siempre reporte tamaños de efecto junto a p-values.
# Mann-Whitney U (alternative to two-sample t)
wilcox.test(x, y)
wilcox.test(x, y, paired = FALSE)
# Wilcoxon signed-rank (alternative to paired t)
wilcox.test(before, after, paired = TRUE)
# Kruskal-Wallis (alternative to one-way ANOVA)
kruskal.test(y ~ group, data = df)
# Friedman (alternative to repeated measures ANOVA)
friedman.test(y ~ group | subject, data = df)
# permutation test
library(coin)
oneway_test(y ~ group, data = df, distribution = approximate(nresample = 9999))
# bootstrap CI
library(boot)
boot_mean <- function(data, idx) mean(data[idx])
b <- boot(x, boot_mean, R = 10000)
boot.ci(b, type = "perc")
# sign test
library(BSDA)
SIGN.test(x, md = 5) # median different from 5?Análisis de regresión
Regresión lineal
lm ajusta modelos lineales. summary muestra coeficientes, errores estándar, t-values, p-values, R² y F-statistic. Siempre compruebe diagnósticos: gráficos de residuos para linealidad/homocedasticidad, VIF para multicolinealidad (>5 es preocupante). I() protege aritmética en fórmulas; poly(x, 2) da polinomios ortogonales.
# simple linear
model <- lm(mpg ~ wt, data = mtcars)
summary(model)
coef(model)
confint(model)
fitted(model)
residuals(model)
predict(model, newdata = new_df, interval = "confidence")
# multiple regression
model <- lm(mpg ~ wt + hp + cyl, data = mtcars)
# interactions
model <- lm(mpg ~ wt * hp, data = mtcars)
model <- lm(mpg ~ wt + hp + wt:hp, data = mtcars)
# transformations
model <- lm(log(mpg) ~ wt, data = mtcars)
model <- lm(mpg ~ poly(wt, 2), data = mtcars) # quadratic
model <- lm(mpg ~ I(wt^2) + wt, data = mtcars)
# categorical predictors
model <- lm(mpg ~ factor(cyl), data = mtcars)
# diagnostics
plot(model) # 4 diagnostic plots
library(car)
vif(model) # variance inflation factorModelos lineales generalizados
glm extiende lm a respuestas no normales. family=binomial para logística (resultados binarios); family=poisson para conteos. Para logística, exp(coef) da odds ratios. Compare modelos anidados con anova(..., test='Chisq'). Para conteos sobredispersados (varianza > media), use glm.nb (binomial negativo) en lugar de Poisson.
# logistic regression (binary)
model <- glm(am ~ wt + hp, data = mtcars, family = binomial)
summary(model)
# predicted probabilities
predict(model, type = "response")
# odds ratios
exp(coef(model))
# Poisson regression (counts)
model <- glm(count ~ group, data = df, family = poisson)
# negative binomial (overdispersed counts)
library(MASS)
model <- glm.nb(count ~ group, data = df)
# compare models
model1 <- glm(y ~ x1, family = binomial, data = df)
model2 <- glm(y ~ x1 + x2, family = binomial, data = df)
anova(model1, model2, test = "Chisq")
# goodness of fit
1 - pchisq(model$deviance, model$df.residual)
# McFadden pseudo R²
1 - model$deviance / model$null.devianceSelección de modelo
La selección stepwise es fácil pero sesgada — prefiera all-subsets (regsubsets) o regularización (glmnet). AIC/BIC balancean ajuste y complejidad (menor es mejor). La validación cruzada da estimaciones honestas out-of-sample. glmnet con alpha=0 es ridge, alpha=1 es lasso (que puede poner a cero coeficientes — selección de características).
# stepwise selection
full <- lm(mpg ~ ., data = mtcars)
step(full, direction = "both")
step(full, direction = "backward")
step(full, direction = "forward", scope = list(lower = ~1, upper = full))
# AIC and BIC
AIC(model1, model2)
BIC(model1, model2)
# all subsets
library(leaps)
leaps <- regsubsets(mpg ~ ., data = mtcars, nvmax = 10)
plot(leaps, scale = "adjr2")
plot(leaps, scale = "Cp")
# cross-validation
library(caret)
train(mpg ~ ., data = mtcars, method = "lm",
trControl = trainControl(method = "cv", number = 10))
# regularized (glmnet)
library(glmnet)
X <- model.matrix(mpg ~ ., mtcars)[, -1]
y <- mtcars$mpg
cv_model <- cv.glmnet(X, y, alpha = 0) # ridge
cv_model <- cv.glmnet(X, y, alpha = 1) # lassoModelos de efectos mixtos
Los modelos de efectos mixtos manejan datos correlacionados (medidas repetidas, muestras agrupadas). (1 | subject) es una intercepción aleatoria por sujeto; (time | subject) añade una pendiente aleatoria. Los grupos anidados usan /. Los grupos cruzados usan +. lmer para continuos, glmer para no continuos. Use lmerTest para p-values (lme4 no los computa por defecto).
library(lme4)
library(lmerTest)
# random intercept
model <- lmer(score ~ treatment + (1 | subject), data = df)
# random intercept and slope
model <- lmer(score ~ time + (time | subject), data = df)
# nested random effects
model <- lmer(score ~ 1 + (1 | school/class), data = df)
# crossed random effects
model <- lmer(score ~ 1 + (1 | subject) + (1 | item), data = df)
# generalized mixed model
glmer(outcome ~ treatment + (1 | subject), data = df, family = binomial)
# summary and CIs
summary(model)
confint(model, method = "Wald")
confint(model, method = "boot")
# anova
anova(model)
# random effects
ranef(model)
VarCorr(model)
# predict
predict(model, newdata = df, allow.new.levels = TRUE)Diagnósticos y predicción
Siempre compruebe diagnósticos: los gráficos de residuos revelan no linealidad y heterocedasticidad; la distancia de Cook (>4/n) marca puntos influyentes; hatvalues (>2p/n) marca puntos de alto apalancamiento. predict con interval='confidence' para la media, 'prediction' para valores individuales (más ancho). ggeffects computa efectos marginales para visualización.
model <- lm(mpg ~ wt + hp, data = mtcars)
# diagnostic plots
par(mfrow = c(2, 2))
plot(model)
par(mfrow = c(1, 1))
# residuals vs leverage
plot(model, which = 5)
# influence measures
influence.measures(model)
hatvalues(model) # leverage
cooks.distance(model) # Cook's distance
dfbeta(model) # change in coef per obs
# identify outliers
which(cooks.distance(model) > 4 / nrow(mtcars))
# predictions
new_data <- data.frame(wt = c(2, 3, 4), hp = c(100, 150, 200))
predict(model, newdata = new_data, interval = "confidence")
predict(model, newdata = new_data, interval = "prediction")
# marginal effects
library(ggeffects)
ggeffect(model, terms = "wt")
plot(ggeffect(model, terms = c("wt", "hp")))
# R squared variants
library(rsq)
rsq(model, type = "v") # variance-based
rsq(model, type = "kl") # Kullback-LeiblerR Markdown e informes
Fundamentos de R Markdown
R Markdown combina narrativa (Markdown), código (chunks R) y salida (tablas/gráficos). El encabezado YAML establece metadatos y formato de salida. Opciones de chunk: echo=FALSE oculta código, include=FALSE ejecuta pero oculta todo, fig.cap añade captions. Código r inline con backticks inserta valores en prosa. Knit (Ctrl+Shift+K) renderiza.
---
title: "Quarterly Report"
author: "Data Team"
date: "2024-12-31"
output: html_document
---
## Section
Inline code: the mean is `r mean(x)`.
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = FALSE, warning = FALSE, message = FALSE)
library(dplyr)
library(ggplot2)
data <- read_csv("data.csv")
```
```{r plot, fig.cap="Sales over time"}
ggplot(data, aes(date, sales)) + geom_line()
```
```{r table}
library(knitr)
kable(head(data), caption = "First 6 rows")
```Opciones de chunk
Las opciones de chunk controlan ejecución de código y salida. cache=TRUE acelera re-knits pero puede ocultar cambios — establezca dependson para invalidación de caché. R Markdown soporta muchos lenguajes via motores knitr: python, bash, sql, javascript, etc. Para Python, use el paquete reticulate para compartir objetos entre R y Python.
```{r chunk-name, options}
# code here
```
# Common options:
# echo=FALSE hide code, show output
# eval=FALSE show code, don't run
# include=FALSE run, hide code and output
# results="hide" show code, hide text output
# warning=FALSE hide warnings
# message=FALSE hide messages
# fig.width=6 figure width (inches)
# fig.height=4 figure height
# fig.cap="..." figure caption
# fig.path="figs/" where to save figures
# cache=TRUE cache results (faster re-knits)
# dependson="..." cache dependencies
# dev="svg" output device
# global options
knitr::opts_chunk$set(
echo = TRUE,
fig.width = 6,
fig.height = 4,
fig.path = "figures/",
cache = TRUE
)
# language engines
```{python}
# Python code
```
```{bash}
# Shell commands
```
```{sql, connection=db}
# SQL queries
```Formatos de salida
html_document es el más flexible (interactivo, code_folding, tablas paginadas). pdf_document requiere LaTeX (instale TinyTeX via tinytex::install_tinytex()). word_document genera archivos Word usando un docx de referencia para estilo. Para presentaciones, use ioslides (integrado) o revealjs (más pulido). Múltiples salidas pueden especificarse juntas.
---
output:
html_document:
toc: true
toc_float: true
toc_depth: 3
number_sections: true
theme: flatly
highlight: tango
code_folding: hide
df_print: paged
---
---
output:
pdf_document:
toc: true
number_sections: true
fig_caption: true
latex_engine: xelatex
---
---
output:
word_document:
toc: true
reference_docx: template.docx
---
---
output:
ioslides_presentation:
widescreen: true
---
---
output:
revealjs::revealjs_presentation:
theme: solarized
transition: slide
---
# Multiple formats
---
output:
html_document: default
pdf_document: default
---Tablas con kable y gt
kable + kableExtra produce tablas de calidad de publicación con agrupación, formato condicional y estilo. gt es una alternativa más nueva, más estilo gramática de gráficos. DT crea tablas HTML interactivas con ordenación, filtrado y paginación — ideal para informes HTML. Elija basándose en formato de salida (kable funciona en todas partes; DT solo en HTML).
library(knitr)
library(kableExtra)
library(gt)
# basic kable
kable(head(mtcars))
# styled kable
kable(head(mtcars), format = "html", caption = "Top cars") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE) %>%
add_header_above(c(" " = 1, "Specs" = 3, "Performance" = 7))
# conditional formatting
kable(df) %>%
cell_spec(value, color = ifelse(value > 0, "green", "red")) %>%
row_spec(1, bold = TRUE, background = "yellow")
# gt (modern alternative)
mtcars %>%
head() %>%
gt() %>%
tab_header(title = "MT Cars", subtitle = "First 6 rows") %>%
cols_label(mpg = "MPG", cyl = "Cylinders") %>%
tab_options(table.width = pct(80))
# DT for interactive tables
library(DT)
datatable(mtcars, filter = "top", options = list(pageLength = 5))Parámetros y automatización
params hacen los informes reutilizables — defínalos en YAML, acceda via params$<name>. Renderice con params personalizados via rmarkdown::render(). Recorra valores de parámetros para generar múltiples informes (uno por región, por trimestre, etc.). Esta es la base de pipelines de informes automatizados. Combine con cron/R programado para informes periódicos.
---
title: "Regional Sales Report"
output: html_document
params:
region: "West"
year: 2024
data_file: "sales.csv"
---
## Report for `r params$region` in `r params$year`
```{r}
data <- read_csv(params$data_file) %>%
filter(region == params$region, year == params$year)
```
# Render from R
rmarkdown::render("report.Rmd",
params = list(region = "East", year = 2024),
output_file = "east_2024.html")
# Render from command line
# Rscript -e 'rmarkdown::render("report.Rmd", params = list(region = "East"))'
# Loop over parameters
for (r in c("West", "East", "North")) {
rmarkdown::render("report.Rmd",
params = list(region = r),
output_file = paste0(r, "_report.html"))
}Programación funcional con purrr
Familia map
map es el lapply de tidyverse — siempre devuelve una lista. map_dbl/chr/int/lgl devuelven vectores tipados (más seguros que map). map2 y pmap iteran sobre múltiples args en paralelo. walk es para efectos secundarios (imprimir, guardar). El pronombre .x se refiere al elemento actual; en pmap, use ..1, ..2, etc. map_dfr une data frames por fila.
library(purrr)
# map: list output
map(1:5, ~ .x^2) # list(1, 4, 9, 16, 25)
map_dbl(1:5, ~ .x^2) # numeric vector
map_chr(1:5, ~ paste0("n", .x)) # character vector
map_int(1:5, ~ .x * 2L) # integer vector
map_lgl(1:5, ~ .x > 3) # logical vector
map_dfr(1:3, ~ data.frame(id = .x, val = .x^2)) # row-bound df
map_dfc(1:3, ~ data.frame(x = .x)) # col-bound df
# multiple arguments
map2(1:3, 4:6, ~ .x + .y)
pmap(list(1:3, 4:6, 7:9), ~ ..1 + ..2 + ..3)
# walk (for side effects)
walk(1:5, ~ print(.x))
walk(files, ~ process(.x))
walk2(plots, filenames, ~ ggsave(.y, .x))
# in pipelines
mtcars %>%
split(.$cyl) %>%
map(~ lm(mpg ~ wt, data = .x)) %>%
map(summary) %>%
map_dbl(~ .x$r.squared)Safely y quietly
safely envuelve una función para devolver (result, error) en lugar de lanzar — esencial para procesamiento por lotes donde un fallo no debería parar todo. possibly devuelve un valor por defecto. quietly captura mensajes/advertencias. transpose voltea una lista de pares en un par de listas. insistently reintenta con backoff — ideal para APIs inestables.
library(purrr)
# safely: capture errors without stopping
safe_log <- safely(log)
safe_log(10) # list(result = 2.3, error = NULL)
safe_log("a") # list(result = NULL, error = <error>)
# process many, keep going on error
results <- map(values, safely(my_function))
successes <- map(results, "result") %>% compact()
errors <- map(results, "error") %>% compact()
# quietly: capture messages/warnings
quiet_log <- quietly(log)
quiet_log(10) # list(result, output, warnings, messages)
# possibly: return default on error
safe_log <- possibly(log, otherwise = NA_real_)
map(values, safe_log) # never errors
# transpose for cleaner output
results <- map(values, safely(fun)) %>% transpose()
results$result # all results
results$error # all errors
# insistently: retry on failure
slow_fn <- insistently(api_call, rate = rate_backoff())Reduce y accumulate
reduce combina elementos por pares (como foldl); accumulate mantiene intermedios. Útil para unir muchos data frames o construir cómputos acumulativos. detect/find primera coincidencia; keep/discard filtran. every/some prueban predicados. Estos reemplazan bucles con operaciones concisas y componibles.
library(purrr)
# reduce: combine pairwise
reduce(c(1, 2, 3, 4), ) # 10
reduce(c(1, 2, 3, 4), ~ .x * .y) # 24
reduce(list(df1, df2, df3), full_join, by = "id")
# accumulate: keep intermediate results
accumulate(c(1, 2, 3, 4), ) # 1 3 6 10
accumulate(c(2, 3, 4), ~ .x * .y) # 2 6 24
# right reduce
reduce(c(1, 2, 3, 4), , .dir = "backward")
# with init
reduce(c(1, 2, 3), ~ c(.x, .y), .init = numeric(0))
# detect
detect(1:10, ~ .x > 5) # 6 (first match)
detect_index(1:10, ~ .x > 5) # 6 (index)
detect(1:10, ~ .x > 5, .right = TRUE) # 10 (last match)
# keep/discard
keep(1:10, ~ .x %% 2 == 0) # 2 4 6 8 10
discard(1:10, ~ .x %% 2 == 0) # 1 3 5 7 9
every(1:10, ~ .x > 0) # TRUE
some(1:10, ~ .x > 5) # TRUEAplicación parcial y composición
partial pre-rellena argumentos — útil para crear funciones especializadas a partir de generales. compose encadena funciones (de derecha a izquierda). negate invierte un predicado. lift convierte una función para tomar una lista de args. imap es map2 con el índice como segundo argumento. Estas herramientas hacen la composición funcional limpia y legible.
library(purrr)
# partial: pre-fill arguments
add_one <- partial(, 1)
add_one(5) # 6
round2 <- partial(round, digits = 2)
round2(3.14159) # 3.14
# compose: chain functions
clean_string <- compose(
str_trim,
str_to_lower,
~ str_replace_all(.x, "[^a-z]", " ")
)
clean_string(" Hello, World! ") # "hello world "
# %>% (magrittr pipe) vs compose
# pipe: data %>% f %>% g %>% h
# compose: g %>% f creates a new function
# negate
is_missing <- negate(is.na)
is_missing(5) # TRUE
# lift (vectorize)
sum2 <- lift() # takes a list of 2 args
sum2(list(1, 2)) # 3
# walk with index
imap(letters[1:3], ~ paste0(.y, ": ", .x))
# list("1: a", "2: b", "3: c")Columnas de lista y datos anidados
El patrón nest + map + unnest es el split-apply-combine de tidyverse. Las columnas de lista contienen cualquier objeto (modelos, predicciones, sub-datos). map sobre un data frame itera sobre columnas. pluck extrae de forma segura elementos anidados. modify_if cambia elementos que coinciden con un predicado, preservando la estructura original.
library(purrr)
library(dplyr)
library(tidyr)
# nest data, fit models, predict
mtcars %>%
group_by(cyl) %>%
nest() %>%
mutate(
model = map(data, ~ lm(mpg ~ wt, data = .x)),
rsq = map_dbl(model, ~ summary(.x)$r.squared),
pred = map2(model, data, predict)
)
# unnest predictions
mtcars %>%
group_by(cyl) %>%
nest() %>%
mutate(model = map(data, ~ lm(mpg ~ wt, .x))) %>%
mutate(pred = map2(model, data, predict)) %>%
select(cyl, data, pred) %>%
unnest(c(data, pred))
# apply function to each column
mtcars %>% map_dbl(mean)
mtcars %>% map_dbl(sd)
# apply to columns of specific type
iris %>%
map_if(is.numeric, mean) %>%
map_dbl(round, 2)
# pluck
list(a = list(b = list(c = 42))) %>% pluck("a", "b", "c")
# 42
# modify (returns same type)
modify_if(iris, is.numeric, ~ .x * 2)
modify_depth(nested_list, 2, ~ .x + 1)Apps Shiny
Estructura básica de app
Una app Shiny tiene ui (diseño) y server (lógica). Las entradas vienen de input$<id>; las salidas van a output$<id> via funciones render*. fluidPage es el diseño básico; sidebarLayout divide en sidebar (controles) y main (salida). Guarde como app.R en su propia carpeta; el nombre de la carpeta se convierte en el nombre de la app.
library(shiny)
ui <- fluidPage(
titlePanel("Hello Shiny"),
sidebarLayout(
sidebarPanel(
sliderInput("n", "Number of points", 1, 100, 50)
),
mainPanel(
plotOutput("scatter")
)
)
)
server <- function(input, output, session) {
output$scatter <- renderPlot({
plot(1:input$n, rnorm(input$n))
})
}
shinyApp(ui, server)
# Save as app.R in a folder, run with:
# shiny::runApp("path/to/folder")
# Or click "Run App" in RStudioEntradas y reactividad
Shiny tiene muchos controles de entrada. observeEvent ejecuta código cuando una entrada cambia; eventReactive crea un valor reactivo desde un evento. reactive() cachea su resultado hasta que las entradas cambian. reactiveVal/reactiveValues mantienen estado mutable. Use actionButton + observeEvent para disparadores explícitos (no reaccione a cada pulsación de tecla).
library(shiny)
ui <- fluidPage(
numericInput("n", "N", value = 10, min = 1, max = 100),
textInput("label", "Label", value = "Data"),
selectInput("color", "Color", choices = c("red", "blue", "green")),
dateInput("date", "Date"),
dateRangeInput("range", "Range"),
checkboxInput("show", "Show?", value = TRUE),
checkboxGroupInput("vars", "Variables", choices = names(mtcars)),
radioButtons("dist", "Distribution",
choices = c("Normal", "Uniform", "Exponential")),
fileInput("file", "Upload CSV", accept = ".csv"),
actionButton("go", "Go!")
)
server <- function(input, output, session) {
# event-triggered
observeEvent(input$go, {
showNotification(paste("Clicked", input$go))
})
# reactive expression (cached)
data <- reactive({
rnorm(input$n)
})
# reactive value
val <- reactiveVal(0)
observeEvent(input$go, val(val() + 1))
}Salidas y renderizado
Cada tipo de salida tiene una función render* que coincide: renderPlot para gráficos, renderTable para tablas, renderPrint para salida de consola, renderText para cadenas, renderUI para UI dinámica. Las salidas son reactivas — se re-ejecutan cuando cambian sus dependencias de input. DT::dataTableOutput es el estándar para tablas interactivas.
library(shiny)
library(ggplot2)
library(DT)
ui <- fluidPage(
plotOutput("plot", click = "plot_click"),
tableOutput("table"),
DT::dataTableOutput("dt"),
verbatimTextOutput("summary"),
textOutput("text"),
uiOutput("dynamic")
)
server <- function(input, output, session) {
output$plot <- renderPlot({
ggplot(mtcars, aes(wt, mpg)) + geom_point()
})
output$table <- renderTable({
head(mtcars)
})
output$dt <- DT::renderDataTable({
datatable(mtcars, filter = "top")
})
output$summary <- renderPrint({
summary(mtcars)
})
output$text <- renderText({
paste("You clicked:", input$plot_click$x)
})
output$dynamic <- renderUI({
if (input$n > 5) strong("Big!") else em("Small")
})
}
# observe vs reactive
# observe: side effects (output$ assignments, updates)
# reactive: returns a value, used by other reactivesProgramación reactiva
reactive() es el caballo de batalla — cacheado y perezoso (solo recomputa al leerse). observe() es eager (se ejecuta inmediatamente al cambiar una dependencia) — para efectos secundarios. eventReactive espera un evento. reactiveValues mantiene múltiples valores mutables (como un pequeño objeto reactivo). isolate lee un valor sin crear dependencia. debounce regula cambios rápidos de input.
library(shiny)
server <- function(input, output, session) {
# reactive: cached, lazy
filtered <- reactive({
mtcars %>% filter(cyl == input$cyl)
})
# observe: eager, side effects
observe({
updateSelectInput(session, "model",
choices = unique(filtered()$model))
})
# eventReactive: triggered by event
result <- eventReactive(input$go, {
run_analysis(input$params)
})
# reactiveVal: single mutable value
counter <- reactiveVal(0)
observeEvent(input$add, counter(counter() + 1))
# reactiveValues: multiple values
rv <- reactiveValues(data = NULL, status = "idle")
observeEvent(input$load, {
rv$data <- read.csv(input$file$datapath)
rv$status <- "loaded"
})
# debounce (throttle rapid changes)
search <- reactive({ input$search }) %>% debounce(500)
# isolate (use value without dependency)
output$plot <- renderPlot({
plot(isolate(rv$data)) # not reactive on rv$data
})
}Despliegue
shinyapps.io es el alojamiento más fácil (tier gratuito disponible). Para auto-alojamiento, instale Shiny Server en Linux o use Docker (imagen rocker/shiny). Para rendimiento: cachee gráficos, use async (future/promises) para tareas largas y evite re-leer archivos en funciones render. Habilite reactlog (Ctrl+F3) para visualizar el grafo reactivo para depuración.
# Deploy to shinyapps.io
# 1. Create account at shinyapps.io
# 2. Install rsconnect
install.packages("rsconnect")
# 3. Authorize (paste token from shinyapps.io)
rsconnect::setAccountInfo(name = "<name>", token = "<token>", secret = "<secret>")
# 4. Deploy
rsconnect::deployApp("path/to/app")
# Run on local network
shiny::runApp("app.R", host = "0.0.0.0", port = 3838)
# Shiny Server (self-hosted on Linux)
# Install shiny-server, put apps in /srv/shiny-server/
# Config: /etc/shiny-server/shiny-server.conf
# Access at http://server:3838/appname
# Docker
# FROM rocker/shiny
# COPY app.R /srv/shiny-server/myapp/
# EXPOSE 3838
# Performance tips:
# - Use renderCachedPlot for expensive plots
# - Use future + promises for async work
# - Move heavy compute to reactive({...})
# - Avoid re-reading files in render functions
# Debugging
options(shiny.fullstacktrace = TRUE)
options(shiny.reactlog = TRUE) # press Ctrl+F3 in appRendimiento y profiling
Profiling con profvis
profvis es el profiler moderno — produce un flame graph interactivo mostrando dónde se gasta el tiempo. Busque barras anchas (operaciones lentas) y stacks altos (cadenas de llamadas profundas). Haga profiling con tamaños de datos realistas; inputs diminutos ocultan problemas O(n²). summaryRprof es la alternativa de R base. Siempre haga profiling antes de optimizar — la intuición suele ser errónea.
library(profvis)
# profile a block of code
profvis({
data <- read.csv("large.csv")
result <- lapply(data, function(x) {
x[x > 0]
})
plot(result)
})
# profile a function call
profvis(my_function(arg1, arg2))
# save profile
p <- profvis({ ... })
htmlwidgets::saveWidget(p, "profile.html")
# Rprof (base R)
Rprof("profile.out")
# ... code to profile ...
Rprof(NULL)
summaryRprof("profile.out")
# tips:
# - profile realistic inputs (not too small)
# - run multiple times for stable results
# - look for the widest bars in the flame graph
# - focus on hotspots, not micro-optimizationsVectorización
La vectorización es la mayor palanca de rendimiento de R — las operaciones en vectores enteros son 10-100x más rápidas que los bucles porque entran en C. La preasignación (numeric(n)) es la segunda mayor ganancia — nunca haga crecer un vector dentro de un bucle. vapply es más seguro y rápido que sapply (salida tipada). rowMeans/colSums están altamente optimizados — úselos sobre apply.
# BAD: loop with growing result
slow <- function(n) {
result <- numeric(0)
for (i in 1:n) {
result <- c(result, i^2)
}
result
}
# BETTER: preallocate
better <- function(n) {
result <- numeric(n)
for (i in 1:n) {
result[i] <- i^2
}
result
}
# BEST: vectorize
fast <- function(n) {
(1:n)^2
}
# benchmarks
microbenchmark::microbenchmark(
slow = slow(1000),
better = better(1000),
fast = fast(1000),
times = 10
)
# apply family
# sapply/lapply: list apply
# vapply: typed sapply (safer, faster)
# map_dbl: tidyverse, typed
# rowMeans/colSums: faster than apply(x, 1, mean)Rcpp para hotspots
Rcpp le permite escribir funciones C++ llamables desde R — típicamente 10-100x más rápidas para bucles que no pueden vectorizarse. cppFunction para one-liners; sourceCpp para archivos. El 'sugar' de Rcpp proporciona operadores C++ vectorizados (así x*2+1 funciona en vectores). Use para hotspots identados por profiling, no para todo — las operaciones vectorizadas de R ya son C.
library(Rcpp)
# inline C++ in R
cppFunction('
double sumC(NumericVector x) {
double s = 0;
for (int i = 0; i < x.size(); i++) {
s += x[i];
}
return s;
}
')
sumC(1:1e6)
# source from file
# file: code.cpp
# #include <Rcpp.h>
# using namespace Rcpp;
# // [[Rcpp::export]]
# double meanC(NumericVector x) {
# return std::accumulate(x.begin(), x.end(), 0.0) / x.size();
# }
sourceCpp("code.cpp")
# use Rcpp sugar (vectorized C++)
cppFunction('
NumericVector funC(NumericVector x) {
return x * 2 + 1; // vectorized via Rcpp sugar
}
')
# data frames
cppFunction('
DataFrame subsetC(DataFrame df, LogicalVector mask) {
return df[mask];
}
')Memoria y data.table
data.table es dramáticamente más rápido que dplyr para datos grandes (>1M filas) — a menudo 5-50x. La sintaxis dt[i, j, by] es concisa una vez aprendida. := modifica in situ (sin copia) — enormes ahorros de memoria. setkey habilita búsquedas y joins rápidos. fread/fwrite son los lectores/escritores CSV más rápidos en R. Use data.table cuando la velocidad importa; dplyr para legibilidad.
library(data.table)
# data.table is much faster than data.frame for large data
dt <- fread("huge.csv") # fast CSV reader
fwrite(dt, "out.csv") # fast CSV writer
# syntax: dt[i, j, by]
dt[, mean(value), by = group] # group by + summarize
dt[order(-value)] # sort
dt[value > 100, .N, by = group] # filter + count by group
dt[, .(avg = mean(value), n = .N), by = group]
# reference semantics (no copy)
dt[, new_col := value * 2] # add column in place
dt[value < 0, value := 0] # modify in place
# keys for fast lookups
setkey(dt, id)
dt["abc"] # fast lookup
dt["abc", mult = "first"]
# joins
dt1[dt2, on = "id"] # left join
dt1[dt2, on = .(id), nomatch = 0] # inner join
# memory tips
# - gc() to force garbage collection
# - object.size(x) to check size
# - rm() large objects when done
# - read data in chunks for huge filesComputación paralela
parallel (R base) es portable pero verboso. future + furrr es el enfoque moderno y tidyverse-friendly — cambie backends con plan(). multicore usa fork (rápido, solo Linux/Mac); multisession usa sesiones R separadas (portable, más lento). El paralelismo tiene sobrecarga — solo vale la pena para tareas que toman >100ms cada una. Siempre haga benchmark antes y después.
library(parallel)
library(future)
library(furrr)
# parallel lapply
cl <- makeCluster(detectCores() - 1)
result <- parLapply(cl, items, function(x) {
# work on x
})
stopCluster(cl)
# future: modern, simpler
library(future)
plan(multisession) # parallel backend
result <- future_lapply(items, fun)
# or
plan(multicore) # fork (Linux/Mac, faster)
plan(cluster, workers = 4)
# furrr: parallel purrr
library(furrr)
plan(multisession, workers = 4)
result <- future_map(items, fun)
result <- future_map_dbl(items, ~ .x^2)
# foreach
library(foreach)
library(doParallel)
registerDoParallel(4)
result <- foreach(i = 1:10, .combine = c) %dopar% {
i^2
}
# always benchmark — parallel overhead can outweigh gains
# for small tasks
microbenchmark::microbenchmark(
serial = lapply(1:100, slow_fn),
parallel = future_lapply(1:100, slow_fn),
times = 5
)Fragmentos de R relacionados
Copy-paste ready code for common tasks.
Data Frames
Create, inspect, filter, mutate, sort, aggregate, and merge data frames.
Vectors
Build atomic vectors, apply vectorized ops, index, and recycle.
ggplot2
Build layered plots with geoms, facets, and themes using the grammar of graphics.
dplyr
Chain mutate, filter, group_by, summarise, and joins with the native pipe.
Statistics
Compute summaries, run t-tests, linear models, ANOVA, and use distributions.
Apply Family
Apply functions over arrays, lists, and groups with apply, lapply, sapply, tapply.
Data Import/Export
Read and write CSV, TSV, RDS, RData, and text files with base R.
Functions
Define functions with defaults, variadic args, closures, and higher-order use.
Was this helpful?