Manejo de datos
Stack de R para importar, transformar y explorar datos tabulares
Sobre manejo de datos en R
R ofrece dos ecosistemas de primer nivel para manipular tablas en memoria: el tidyverse (dplyr, tidyr, readr, lubridate) y data.table. No son escalones de una escalera, son dos diseños distintos con compromisos diferentes:
- tidyverse: API verbosa y legible, copy-on-modify, integración natural con
ggplot2y tidy evaluation. Coste: rendimiento moderado y consumo de memoria por las copias. - data.table: sintaxis densa, semántica por referencia (
:=), radix sort y grouping extraordinariamente rápidos. Coste: curva de aprendizaje más empinada y menos integración con el resto del tidyverse.
Una recomendación honesta: usa tidyverse por defecto para análisis exploratorios, pipelines de informe y trabajo colaborativo donde el código se lee más veces de las que se ejecuta. Cambia a data.table cuando el dataset pase de unos millones de filas, cuando agrupes y agregues en bucle, o cuando el coste de memoria de copiar columnas sea prohibitivo. Para datasets que ya no entran en RAM, salta a arrow (o duckdb) y opera sobre el dataset sin materializarlo entero.
Esta página cataloga los paquetes que sostienen ese flujo, ordenados por jerarquía conceptual: primero la entrada de datos (lectura), luego la manipulación columnar, después el reshape de formato, el manejo de fechas, la exploración / diagnóstico, los valores perdidos, y por último las alternativas para datos que exceden la memoria.
Convenciones que asumo en los ejemplos:
- Pipe nativo
|>(R ≥ 4.1) en lugar de%>%, salvo cuando el segundo aporta algo (placeholder.). - tibbles en lugar de
data.framebase. La diferencia práctica son los prints truncados y la conservación estricta de tipos al subscribir columnas. - Locale C explícito en parsing de fechas y números cuando la portabilidad importa.
readr
readr es el lector de archivos delimitados (CSV, TSV, fixed-width) del tidyverse. Devuelve tibbles, detecta tipos por muestreo de las primeras filas y reporta problemas de parsing de forma legible. Es lo que reemplaza a read.csv() y read.table() de R base en un flujo serio.
Diseñado por Hadley Wickham y Jim Hester. Comparte filosofía con vroom (mismo autor), de hecho, desde la versión 2.0 readr usa vroom por debajo, con lo que su rendimiento se acerca al de data.table::fread() en la mayoría de casos.
Cuándo usarlo
CSVs y TSVs de tamaño moderado-alto (hasta unos pocos GB en disco) donde quieras un parsing limpio con tipos explícitos y mensajes de error claros. Es la elección razonable por defecto para entrada de datos en pipelines de tidyverse.
Cuándo NO usarlo
- Velocidad bruta sobre archivos enormes (>10 GB):
data.table::fread()sigue siendo notablemente más rápido para parsing secuencial y consume menos memoria pico. - Lectura lazy o sobre datasets distribuidos en muchos archivos: usa
arrow::open_dataset()oduckdbdirectamente sobre los CSV/Parquet. - Excel:
readxl(mismo ecosistema) oopenxlsxpara escritura. - JSON o XML estructurado:
jsonlite/xml2.
Conceptos clave
read_csv()infiere tipos del guess_max (por defecto 1000 filas). Si el dataset cambia de tipo más abajo, fallarás silenciosamente, fija tipos concol_types.cols()y los shortcutscols(.default = col_character())o el string compacto"icnDT"(i=integer, c=character, n=numeric, D=date, T=datetime) son la forma idiomática de fijar el esquema.problems(x)devuelve un tibble con los parsing failures. Inspeccionarlo siempre tras una carga importante.locale()controladecimal_mark,grouping_mark,tz,encodingy el formato de fechas. Diferencia crítica entre datos exportados desde Excel enes-ES(coma decimal) vsen-US(punto decimal).write_csv()escribe en UTF-8 sin BOM. Para Excel en Windows que espera BOM, usawrite_excel_csv().
Patrón mínimo
library(readr)
# Lectura con tipos explícitos y locale español
df <- read_csv(
"ventas_2025.csv",
col_types = cols(
fecha = col_date(format = "%Y-%m-%d"),
cliente = col_character(),
importe = col_double(),
categoria = col_factor(levels = c("A", "B", "C"))
),
locale = locale(decimal_mark = ",", grouping_mark = ".", encoding = "UTF-8")
)
# Inspeccionar problemas de parsing antes de seguir
problems(df)Trampas habituales
- Tipo inferido por muestreo. Si las primeras 1000 filas son
integery la fila 1001 contiene un decimal,readrlanza warning y meteNA. Subeguess_max = Info, mejor, fijacol_typessiempre que conozcas el esquema. - Encoding latino. Archivos exportados desde sistemas Windows antiguos vienen en
Latin1oWindows-1252. Sinlocale(encoding = "Latin1"), los acentos quedan rotos. - Coma decimal vs separador de campo. Datasets
es-ESexportados como CSV usan;como separador y,como decimal. Usaread_csv2()(preset europeo) en vez deread_csv(). read.csv(base) vsread_csv. No los mezcles: el primero devuelvedata.frameconstringsAsFactors = FALSEpor defecto desde R 4.0, el segundo devuelvetibble. Las diferencias de tipo silenciosas son fuente recurrente de bugs.
Enlaces
Relacionados en esta página
data.table,fread()para archivos muy grandes.arrow, Parquet, CSV y datasets multi-archivo out-of-memory.
dplyr
dplyr es el estándar de facto para manipulación de datos tabulares en R idiomático. Expone un conjunto reducido de verbos (filter, select, mutate, summarise, arrange, group_by, *_join) que cubren el 95 % de las transformaciones que se hacen sobre un data frame. Su valor real no es la velocidad, data.table es más rápido, sino la legibilidad y la consistencia del API.
Es el corazón del tidyverse. Todo lo demás (tidyr, ggplot2, broom, recipes) está diseñado para encajar con sus verbos y con el pipe.
Cuándo usarlo
- Análisis exploratorio y pipelines de informe donde el código se lee más veces de las que se ejecuta.
- Datasets en memoria de hasta unos pocos millones de filas. A partir de ahí, evalúa
data.tableodtplyr(frontenddplyrsobredata.table). - Trabajo colaborativo: el código
dplyres legible incluso por personas que no son su autor.
Cuándo NO usarlo
- Performance crítica con muchos grupos:
data.tableagrupa y agrega 5-50× más rápido. Para summarise por miles de claves sobre millones de filas, la diferencia es real. - Modificación in-place:
dplyrsiempre copia (copy-on-modify). Si la presión de memoria importa,data.table::set()y:=modifican por referencia. - Datos out-of-memory: usa
dbplyr(traduce a SQL contra una base) oarrow(mismo verbos, ejecución sobre Arrow). - Bucles de agregación en hot path: el sobrecoste de NSE y masking se nota. Baja a
collapse(gran rendimiento manteniendo API tidy) o a vectorización pura.
Conceptos clave
- Cinco verbos básicos:
filter(filas),select(columnas),mutate(nuevas columnas),summarise(reducir),arrange(ordenar).group_bymodula los anteriores. group_by()+summarise()desagrupa un nivel por defecto desdedplyr1.0. Antes mantenía todos los niveles, verificarlo con.groups = "drop"explícito es buena práctica.mutate(across(...))ysummarise(across(...))reemplazan amutate_at/mutate_if. Aceptan tidyselect (where(is.numeric),starts_with("x_")).*_join:inner_join,left_join,right_join,full_join,semi_join,anti_join. Desdedplyr1.1, el argumentorelationshipvalida la cardinalidad ("one-to-one","many-to-one", etc.), úsalo, evita bugs por explosión de filas.- Tidy evaluation: variables de columna pasan sin comillas. Para programar funciones que reciben nombres de columna, embebe con
{ var }(curly-curly) o.data[[name]].
Patrón mínimo
library(dplyr)
resumen <- ventas |>
filter(fecha >= "2025-01-01", !is.na(importe)) |>
mutate(
mes = lubridate::floor_date(fecha, "month"),
margen = importe - coste,
margen_pct = margen / importe
) |>
group_by(mes, categoria) |>
summarise(
n = n(),
ingresos = sum(importe),
margen_medio = mean(margen_pct, na.rm = TRUE),
.groups = "drop"
) |>
arrange(mes, desc(ingresos))Trampas habituales
summarise()desagrupa un nivel. Trasgroup_by(a, b) |> summarise(...), el resultado sigue agrupado pora. Esto rompe joins y filtros posteriores. Cierra con.groups = "drop"oungroup()explícito.filterconNA:filter(x == 1)excluyeNA. Para mantenerNA, usafilter(is.na(x) | x == 1)ofilter(x %in% 1).mutatevsmutate(across).mutate(c1 = fn(c1), c2 = fn(c2), ...)se hace pesado. Pasa aacross().- NSE en funciones de usuario. Pasar
col_namecomo string falla si lo metes directo enfilter(df, col_name == ...). Usafilter(df, .data[[col_name]] == ...)o el operador{ }para nombres no entrecomillados. selectdespués de un join silencia errores. Si una columna se renombra automáticamente con sufijo.x/.ypor colisión, unselect(col)puede capturar la equivocada. Usarelationshipysuffixexplícitos.
Enlaces
Relacionados en esta página
tidyr, reshape complementario a la manipulación dedplyr.data.table, alternativa de alto rendimiento.arrow, mismos verbos sobre datasets que no caben en RAM.
tidyr
tidyr se encarga del cambio de forma (reshape) de las tablas: pasar de ancho a largo, separar y combinar columnas, anidar y desanidar listas-columna. Es el complemento natural de dplyr, uno transforma valores, el otro reorganiza la estructura.
Su API gira en torno a la idea de datos tidy: una observación por fila, una variable por columna, un tipo de observación por tabla. Conseguir esa forma es la mitad del trabajo de cualquier análisis serio. El resto es manipular ya con dplyr.
Cuándo usarlo
- Transformar tablas anchas (una columna por fecha / por condición) a formato largo para
ggplot2o modelos. - Volver al ancho para presentación de informes (
pivot_wider). - Separar columnas combinadas (
separate_wider_delim,separate_wider_regex,separate_wider_position), los antiguosseparate()yextract()están superseded. - Anidar dataframes por grupo (
nest) para mapear modelos conpurrr::map.
Cuándo NO usarlo
- Reshapes masivos sobre millones de filas:
data.table::melt()ydcast()son significativamente más rápidos y consumen menos memoria pico. - Reshapes triviales que no requieren agregación: a veces un
dplyr::mutate+selectya basta sin unpivot_*formal.
Conceptos clave
pivot_longer(cols, names_to, values_to)apila columnas en formato largo.names_patternynames_sepextraen estructura del nombre.pivot_wider(names_from, values_from, values_fill, values_fn)hace lo inverso.values_fnagrega cuando hay múltiples filas por combinación.separate_wider_*reemplaza al viejoseparate()con mejor reporte de errores y control sobre filas problemáticas (too_few,too_many).nest()yunnest()mueven entre formato long y list-column, base del patrón “modelo por grupo” conpurrr.complete()yexpand()añaden filas faltantes para combinaciones de variables, útil antes de plotear series temporales con huecos.
Patrón mínimo
library(tidyr)
library(dplyr)
# Tabla ancha → larga para ggplot
ventas_largo <- ventas_ancho |>
pivot_longer(
cols = starts_with("mes_"),
names_to = "mes",
names_prefix = "mes_",
values_to = "importe"
)
# Larga → ancha con agregación implícita
resumen <- ventas_largo |>
pivot_wider(
names_from = categoria,
values_from = importe,
values_fn = sum,
values_fill = 0
)
# Modelos por grupo via nest + map
modelos <- ventas_largo |>
group_by(region) |>
tidyr::nest() |>
mutate(fit = purrr::map(data, ~ lm(importe ~ mes, data = .x)))Trampas habituales
- Pérdida de tipos en
pivot_longer. Si las columnas que apilas tienen tipos distintos, el resultado se convierte acharacter. Solución:values_transform = list(.default = as.character)y reconvierte tras separar, o homogeneiza antes. pivot_widercon duplicados. Si la combinaciónnames_from + id_colsno es única, obtienes una list-column de avisos. Definevalues_fn(sum,mean,first) explícitamente.separate()está superseded. Funciona pero los nuevosseparate_wider_*dan mejor diagnóstico de filas malformadas. Migra el código nuevo.names_repaircontrola qué hacer con nombres duplicados ("unique","minimal","check_unique"). El default es estricto. En pipelines automáticos puede romper sin razón clara.
Enlaces
Relacionados en esta página
dplyr, manipulación que opera sobre tablas ya en forma tidy.data.table,melt/dcastpara reshapes masivos.
lubridate
lubridate es la librería de fechas y tiempos del tidyverse. Cubre lo que R base hace torpemente: parsing de strings heterogéneos, aritmética con duraciones e intervalos, manejo de zonas horarias y extracción de componentes (year, month, wday).
Forma parte del core del tidyverse desde 2020 y se carga con library(tidyverse). Reemplaza, para casi todo uso práctico, a as.Date, as.POSIXct y strptime de R base.
Cuándo usarlo
- Cualquier parsing de fechas a partir de strings con formatos variados. Las funciones
ymd(),mdy(),dmy(),ymd_hms()resuelven la mayoría de los casos sin necesidad de pasar formato. - Aritmética con duraciones (
days(7),months(1),weeks(2)) y con intervalos (%within%,int_overlaps). - Conversión y normalización de zonas horarias (
with_tz,force_tz).
Cuándo NO usarlo
- Operaciones puramente vectoriales sobre fechas ya bien tipadas donde R base es suficiente y no introduce dependencia (
as.Date,difftime,format). - Performance crítica sobre vectores enormes:
data.tableusa internamenteIDateyITimecon representación entera y operaciones mucho más rápidas. Para binning temporal masivo, considera saltar. - Manejo intensivo de time series indexadas:
xts,zoootsibbleestán diseñados para eso.lubridatesolo cubre el dominio escalar / vectorial.
Conceptos clave
- Parsers por orden de componentes:
ymd("2025-05-18"),mdy("05/18/2025"),dmy("18-05-2025"). Aceptan separadores variados y vectores de strings con formatos mezclados. - Componentes:
year(x),month(x, label = TRUE),wday(x, label = TRUE, week_start = 1),hour(x),tz(x). - Tres tipos de aritmética temporal:
- Periods (
days,months,years), calendario humano:ymd("2024-01-31") + months(1)daNA(no existe 31 feb), no salta a marzo. - Durations (
ddays,dweeks), segundos exactos: ignoran calendario. - Intervals (
interval,%within%), span entre dos fechas, permite operaciones de pertenencia.
- Periods (
- Truncado:
floor_date(x, "month"),ceiling_date(x, "week"),round_date(). Imprescindible para agregar por unidad temporal endplyr.
Patrón mínimo
library(lubridate)
library(dplyr)
eventos <- tibble::tibble(
ts = c("2025-05-18 14:23:00", "18/05/2025 14:23", "May 18, 2025 14:23")
)
eventos |>
mutate(
ts_iso = ymd_hms(ts) %||% dmy_hm(ts) %||% mdy_hm(ts),
semana = floor_date(ts_iso, "week", week_start = 1),
dia = wday(ts_iso, label = TRUE, week_start = 1),
en_q2 = ts_iso %within% interval("2025-04-01", "2025-06-30")
)Trampas habituales
- Zona horaria implícita.
ymd_hms("2025-05-18 14:00")asume UTC. Para timestamps locales sin TZ explícito, fijatz = "Europe/Madrid", confundir esto desplaza datos por horas y aparece como bug fantasma. force_tzvswith_tz.force_tz(x, "Europe/Madrid")cambia la etiqueta de TZ sin convertir.with_tz()convierte el instante a la nueva zona. Confundirlos es la causa #1 de errores de timestamps.- Periods vs Durations.
today() + years(1)puede darNAun 29 de febrero.today() + dyears(1)siempre suma 365.25 días, y ya no estás en el mismo día calendario. Elige según semántica. - Locale del sistema.
month(x, label = TRUE)devuelve nombre en el locale del SO. Para consistencia entre máquinas, fijaSys.setlocale("LC_TIME", "C")o pasalocale =explícito enparse_date_time. - Excel serial dates. Cuando
readxllee un Excel con fechas mal formateadas, llegan como número de serie. Conviértelos conas.Date(x, origin = "1899-12-30")(no1900-01-01, bug histórico de Excel).
Enlaces
Relacionados en esta página
dplyr, combinaciones típicasmutate(mes = floor_date(...)).data.table,IDate/ITimepara operaciones temporales rápidas a escala.
dlookr
dlookr se especializa en diagnóstico exploratorio y profiling automático de datasets: estadísticos descriptivos, detección de outliers, evaluación de normalidad y reportes HTML/PDF con un solo comando. Es la herramienta a la que recurres cuando heredas un dataset de origen incierto y necesitas una foto rápida antes de modelar.
Ocupa el mismo nicho que skimr, DataExplorer o summarytools, todos válidos, todos opcionales. La razón para usar dlookr por encima de las alternativas es su enfoque en diagnóstico de calidad (no solo descripción) y los reportes parametrizables.
Cuándo usarlo
- Primer contacto con un dataset desconocido:
diagnose(),diagnose_numeric(),diagnose_category(). - Detección formal de outliers (
diagnose_outlier, métodos IQR y boxplot). - Tests automáticos de normalidad (
normality()) y de transformaciones (find_skewness). - Reportes ejecutivos en HTML/PDF:
diagnose_report(),eda_report(),transformation_report().
Cuándo NO usarlo
- Profiling minimalista en consola:
skimr::skim()es más rápido y más limpio para ver de un vistazo. - EDA visual interactivo:
DataExplorer::create_report()produce un HTML con más gráficos por defecto. - Workflows reproducibles donde tú controlas cada gráfico: prefiere
ggplot2directo.dlookres una herramienta de pre-análisis, no de informe final.
Conceptos clave
diagnose(df)da una tabla con tipos, conteos de NA, conteos únicos. Punto de entrada habitual.diagnose_outlier(df)aplica IQR (1.5 * IQR como umbral). Devuelve estadísticos con/sin outliers para evaluar impacto.normality(df, ...)aplica Shapiro-Wilk a columnas numéricas. Útil pero recuerda: con n alto cualquier desviación es significativa, inspecciona también el histograma.imputate_na()yimputate_outlier()ofrecen métodos simples (media, mediana, KNN, predictive). Para imputación seria, consideramiceomissForest.- Reportes:
eda_report(df)genera HTML con descriptivos completos en un solo comando.
Patrón mínimo
library(dlookr)
library(dplyr)
# Diagnóstico general
df |> diagnose()
# Outliers en columnas numéricas
df |> diagnose_outlier() |> arrange(desc(outliers_ratio))
# Normalidad
df |> normality()
# Reporte EDA completo a HTML
df |> eda_report(output_file = "eda_report.html", output_dir = "reports/")Trampas habituales
- Reportes pesados sobre datasets grandes.
eda_report()puede tardar varios minutos y producir HTMLs de cientos de MB con columnas no relevantes. Filtra a las columnas de interés antes de llamar. - Tests de normalidad con n grande. Shapiro rechaza casi siempre con n > 5000. No conviertas el resultado en decisión automática. Complementa con inspección visual.
- Conflictos de namespace con
dplyr.dlookr::transform()choca conbase::transform. Cargadlookrantes dedplyro usadlookr::explícito. - Imputación silenciosa.
imputate_na()opera sin warning sobre columnas enteras. Audita el método elegido, la media rara vez es la opción correcta para datos sesgados.
Enlaces
- Página oficial
- Vignette introductoria
- Alternativas a considerar:
skimr,DataExplorer,summarytools
Relacionados en esta página
naniar, diagnóstico específico de valores perdidos.
naniar
naniar es la librería especializada en valores perdidos: detección, visualización y diagnóstico de patrones de NA. Va más allá del is.na() base ofreciendo summaries estructurados y un conjunto de geoms de ggplot2 para visualizar la missingness.
Diseñado por Nick Tierney. Su contribución principal no es la imputación (para eso ya están mice, missForest, Amelia) sino el diagnóstico previo: entender la estructura de los NA antes de decidir qué hacer con ellos.
Cuándo usarlo
- Auditar la missingness al recibir un dataset: ¿faltan datos al azar (MCAR), por patrón (MAR), o de forma informativa (MNAR)?
- Visualizar correlaciones de missingness entre variables (
gg_miss_upset,gg_miss_var). - Codificar y manejar sentinel values (
-99,9999,"N/A") que codifican faltantes pero llegan como valores reales (replace_with_na_*).
Cuándo NO usarlo
- Imputación propiamente dicha: usa
mice(imputación múltiple),missForest(bosques aleatorios) omice::norm/mice::pmmsegún supuestos. - Datasets donde la missingness es mínima y aleatoria: el coste de cargar y aprender la API no compensa.
Conceptos clave
miss_var_summary(df)ymiss_case_summary(df)resumen NAs por columna y por fila respectivamente.gg_miss_var(df),gg_miss_case(df),gg_miss_upset(df)visualizan patrones, el último es especialmente útil para ver co-ocurrencia de NAs.replace_with_na_all(df, condition = ~ .x == -99)convierte sentinelas explícitos aNA. Usa tambiénreplace_with_na_at()yreplace_with_na_if()con tidyselect.bind_shadow(df)añade una columna_NApor cada variable indicando si eraNA. Útil para modelar la missingness como predictor.naniardistingue missingness de implicit missingness (filas que deberían existir y no existen),tidyr::completeresuelve esto último.
Patrón mínimo
library(naniar)
library(dplyr)
# Resumen rápido
df |> miss_var_summary()
# Visualización de patrones
gg_miss_var(df, show_pct = TRUE)
gg_miss_upset(df)
# Convertir -99 / "N/A" a NA real
df_clean <- df |>
replace_with_na_all(condition = ~ .x %in% c(-99, "N/A", ""))
# Shadow matrix para modelar missingness como feature
df_shadow <- bind_shadow(df_clean)Trampas habituales
NAno es siempreNA. Datasets reales codifican faltantes como-99,9999,"","NA"(string),"NULL". Sin pasar porreplace_with_na_*primero, todo el diagnóstico subsiguiente está mal.- Confundir MCAR / MAR / MNAR.
gg_miss_upsette muestra patrones, no te dice el mecanismo. La decisión de imputación depende del mecanismo, que es un supuesto que tú haces (e idealmente justificas). - Borrar filas con
na.omitpor inercia. Si la missingness no es MCAR, eliminar filas sesga el análisis. Inspecciona antes connaniary considera imputación si el mecanismo lo justifica.
Enlaces
Relacionados en esta página
dlookr, diagnóstico general que incluye un módulo de NAs.
data.table
data.table es la alternativa de alto rendimiento al tidyverse para manipulación tabular en memoria. Implementa su propia clase (data.table, que hereda de data.frame) y una sintaxis densa de tres argumentos DT[i, j, by] que cubre filtrado, transformación y agrupación en una sola expresión.
Diseñado por Matt Dowle y Arun Srinivasan. Es notablemente el motor más rápido del mundo R para operaciones tabulares estándar, grouping, joins, ordering, y consume menos memoria que dplyr gracias a la modificación por referencia (:=, set).
Cuándo usarlo
- Datasets de millones de filas donde la diferencia de tiempos importa (minutos vs horas).
- Operaciones repetidas en bucle: agregaciones por miles de claves, joins masivos.
- Pipelines con presión de memoria:
data.tablemodifica in-place y evita las copias defensivas dedplyr. - Rolling joins, non-equi joins y update on join (
DT[i, var := ..., on = ...]), patrones que endplyrrequieren acrobacias.
Cuándo NO usarlo
- Análisis exploratorio colaborativo donde la legibilidad gana. La sintaxis
DT[, .(x = mean(y)), by = z]es densa. Un colaborador no familiarizado tarda en leerla. - Datasets pequeños donde la diferencia de rendimiento es irrelevante. Aquí
dplyrpaga su coste de legibilidad sin penalización práctica. - Si quieres
dplyrpor estética pero con velocidad dedata.table: usadtplyr, traduce verbosdplyra operacionesdata.tableen lazy evaluation.
Conceptos clave
DT[i, j, by]:ifiltra filas,jopera sobre columnas,byagrupa. Las tres dimensiones en una expresión.- Semántica por referencia con
:=:DT[, x := log(y)]modificaDTsin copiar. DevuelveDTinvisiblemente. Esto es lo que rompe la mente de quien viene de tidyverse, no hayDT <- DT |> mutate(...). fread()yfwrite(): lector/escritor extraordinariamente rápido. Detecta separadores, tipos y compresión (.gz,.bz2,.xz) automáticamente.- Keys e indexing:
setkey(DT, col)ordena físicamente y permite binary search en lookups.setindex(DT, col)añade índice secundario sin reordenar. .SDy.SDcols:.SDes el sub-data.tabledentro delby..SDcolsselecciona qué columnas verá.DT[, lapply(.SD, mean), by = grp, .SDcols = is.numeric]reemplaza asummarise(across(where(is.numeric), mean)).melt()ydcast(): equivalentes mucho más rápidos apivot_longer/pivot_wider.
Patrón mínimo
library(data.table)
# Lectura rápida
DT <- fread("ventas_2025.csv")
# Filtro + agregación por grupo
resumen <- DT[
fecha >= "2025-01-01" & !is.na(importe),
.(
n = .N,
ingresos = sum(importe),
margen_medio = mean((importe - coste) / importe, na.rm = TRUE)
),
by = .(mes = lubridate::floor_date(fecha, "month"), categoria)
][order(mes, -ingresos)]
# Modificación in-place (sin copia)
DT[, margen := importe - coste]
DT[importe > 0, margen_pct := margen / importe]
# Update on join (no tiene equivalente directo en dplyr)
ref <- data.table(categoria = c("A","B"), iva = c(0.21, 0.10))
DT[ref, iva := i.iva, on = "categoria"]Trampas habituales
:=modifica por referencia. Si hacesDT2 <- DTy luegoDT2[, x := 1],DTtambién cambia (apuntan al mismo objeto). Usacopy(DT)cuando quieras una copia real. Esta es la fuente de bugs más frecuente para quien viene de tidyverse.DT[, x]vsDT[, "x"]vsDT[, .(x)]. El primero (sin comillas) evalúaxcomo expresión (devuelve vector). El segundo (con string) extrae columna por nombre (devuelve vector). El tercero (.()que es alias delist()) devuelvedata.table. Conocer la diferencia evita errores opacos.- Print de
data.tableen RMarkdown / Quarto. Por defecto imprime las primeras y últimas 5 filas. Para presentación, considera convertir atibbleo usarhead()/tail()explícito. .SDes undata.table, no undata.frame. Funciones que esperandata.framepuro pueden comportarse raro dentro de.SD.- Encoding en
fread. Detecta UTF-8 / Latin1 automáticamente, pero con archivos exóticos pasaencoding = "Latin-1"oencoding = "UTF-8"explícito.
Enlaces
Relacionados en esta página
dplyr, el otro estándar. Complementario más que sustitutivo.tidyr,melt/dcastcumplen el mismo rol quepivot_*.arrow, siguiente paso cuando el dataset no cabe en RAM.
arrow
arrow es el binding de R para Apache Arrow: un formato columnar en memoria y un motor de ejecución out-of-core que permite trabajar con datasets que no caben en RAM. Lee y escribe Parquet, Feather y CSV en chunks, con soporte para datasets particionados en cientos o miles de archivos.
La pieza clave es que expone los mismos verbos de dplyr (filter, mutate, summarise, group_by, *_join) sobre objetos arrow_dataset o arrow_table. La ejecución se traduce a operaciones Arrow nativas y solo se materializa el resultado final con collect(), pagas memoria por lo que devuelves, no por el dataset completo.
Cuándo usarlo
- Datasets que exceden la memoria (decenas o cientos de GB en disco).
open_dataset()indexa los archivos sin cargarlos. - Datasets particionados por columna (
hive-style:año=2025/mes=05/...). Arrow aplica predicate pushdown y solo lee las particiones necesarias. - Intercambio con Python (pandas, polars), DuckDB, Spark: Arrow es el formato lingua franca cross-language.
- Almacenamiento Parquet: compresión por columna, tipos preservados, mucho más rápido y compacto que CSV.
Cuándo NO usarlo
- Datasets pequeños en RAM donde
dplyrodata.tableson más rápidos por evitar la sobrecarga de la abstracción. - Manipulación de strings complejos que aún no soporta el ejecutor Arrow, algunas funciones de
stringrno traducen y caen acollect()implícito, perdiendo la ventaja. - Consultas SQL complejas con joins múltiples: considera
duckdb(mismo nicho out-of-memory, motor SQL más maduro, mejor optimizer. Lee Parquet directo y se conecta víadbplyr).
Conceptos clave
arrow::open_dataset(path)crea una referencia lazy a uno o varios archivos. No carga datos.dplyrlazy: encadenas verbos sin ejecutar.collect()materializa el resultado atibbleen memoria.compute()materializa aarrow_table(sigue en formato Arrow).- Particionado:
write_dataset(df, "out/", partitioning = c("year", "month"))produce estructura Hive. Las consultas posteriores filtran sin leer particiones irrelevantes. - Tipos Arrow: más estrictos que R.
int32≠int64,timestamp[ns]≠timestamp[us]. La conversión a R unifica ainteger/double/POSIXctpero al volver puedes perder precisión. - Predicate pushdown: filtros se aplican antes de leer del disco cuando son sobre columnas particionadas.
mutatey otras expresiones complejas no siempre se empujan. Mirashow_exec_plan()para verificar.
Patrón mínimo
library(arrow)
library(dplyr)
# Abrir dataset particionado (no carga datos)
ds <- open_dataset("data/ventas/", format = "parquet")
# Pipeline lazy: solo se ejecuta en collect()
resumen <- ds |>
filter(año == 2025, categoria %in% c("A", "B")) |>
group_by(mes, categoria) |>
summarise(
n = n(),
ingresos = sum(importe, na.rm = TRUE),
.groups = "drop"
) |>
collect()
# Convertir un CSV grande a Parquet particionado
read_csv_arrow("ventas_2025.csv") |>
group_by(año = lubridate::year(fecha), mes = lubridate::month(fecha)) |>
write_dataset("data/ventas/", format = "parquet")Trampas habituales
- No todas las funciones de R están vectorizadas en Arrow. Si tu
mutateusa una función no soportada, Arrow materializa silenciosamente a R y pierdes la ventaja de out-of-memory. Verifica conshow_exec_plan()y mira?aceropara la lista de funciones soportadas. collect()carga todo a RAM. Es el comando que materializa. Si filtras a 200 GB, sigue sin caber. Diseña los filtros para que elcollectfinal sea manejable.- Diferencias de tipo R ↔︎ Arrow. Strings grandes que en R serían
characteren Arrow sonstring(utf8). Fechas pueden viajar comoint32(días desde epoch). Conversiones implícitas en joins entredata.frameyarrow_tablecausan errores raros, homogeniza tipos antes. - Particionado granular. Si particionas por una variable de alta cardinalidad (p. ej.
cliente_id), generas miles de ficheros pequeños y el rendimiento cae. Particiona por variables con cardinalidad baja-media (año, mes, región). - Versión de Arrow ≠ versión de R package. El binario Arrow C++ subyacente debe coincidir. En Linux a menudo hay que recompilar con
LIBARROW_BINARY=truepara que funcionen los Parquet con compresión zstd. Miraarrow_info()al instalar.
Enlaces
Relacionados en esta página
readr, lectura en memoria cuando el dataset cabe.dplyr, los verbos son los mismos. Solo cambia el backend.data.table, alternativa en memoria de alto rendimiento.