El análisis y la manipulación de grandes volúmenes de datos son tareas fundamentales en diversos campos como la estadística, la ciencia de datos, la inteligencia artificial y la investigación en general. En este contexto, el lenguaje de programación R ofrece múltiples librerías diseñadas para mejorar la eficiencia en la lectura, escritura y procesamiento de conjuntos de datos masivos, optimizando el rendimiento computacional y reduciendo el tiempo de ejecución de las operaciones.
El presente documento tiene como objetivo comparar el rendimiento de distintas librerías disponibles en R para el manejo de datos de gran escala. Se evaluará el tiempo de ejecución de tareas fundamentales como la lectura, escritura y conversión de archivos, considerando diferentes formatos y estructuras de datos. Las librerías analizadas incluyen:
fwrite()
y fread()
.Para llevar a cabo la comparación, se genera un conjunto de datos simulado con 100 millones de observaciones, conteniendo variables de distintos tipos (numéricas, enteras, categóricas y lógicas). Posteriormente, se mide el tiempo de ejecución de cada librería en distintas operaciones clave, proporcionando una evaluación objetiva del desempeño de cada una.
Los resultados obtenidos permitirán identificar las opciones más eficientes para distintos escenarios de análisis de datos, ayudando a los usuarios de R a seleccionar la herramienta más adecuada según sus necesidades específicas.
El estudio se desarrolló en tres fases:
rnorm()
para simular datos
numéricos y sample()
para variables categóricas.system.time()
.ggplot2
.data.frame
en operaciones sobre grandes
volúmenes de datos.tidyverse
,
diseñado por Hadley Wickham, proporciona una sintaxis clara y eficiente
para manipulación de datos.# configurar la ruta para el trabajo
setwd("G:/Mi unidad/CursoBigData")
# Lista de paquetes necesarios
paquetes <- c("data.table", "arrow", "fst", "ff", "bigmemory", "sparklyr", "dplyr")
# Verificar e instalar los paquetes que falten
paquetes_faltantes <- paquetes[!(paquetes %in% installed.packages()[,"Package"])]
if(length(paquetes_faltantes) > 0){
install.packages(paquetes_faltantes, dependencies = TRUE)
}
library(sparklyr)
# Instalar Spark en la carpeta local predeterminada
#spark_install(version = "3.4.0")
# Cargar las librerías
library(data.table) # Para operaciones eficientes en tablas
library(arrow) # Para trabajar con formatos Parquet y Feather
library(fst) # Para lectura y escritura ultrarrápida
library(ff) # Para trabajar con datos que exceden la memoria RAM
library(bigmemory) # Para manejar matrices grandes (solo numéricas)
library(sparklyr) # Para procesamiento distribuido con Apache Spark
library(dplyr) # Para manipulación de datos con sintaxis dplyr
Variable | Tipo | Descripción (contexto bancario‑financiero) |
---|---|---|
id_cliente |
Entero único | Identificador secuencial del cliente; actúa como clave primaria del registro. |
producto |
Factor | Tipo de producto financiero principal (CuentaCorriente, TarjetaCredito, Prestamo, etc.). |
saldo_actual |
Numérico | Saldo vigente en guaraníes (₲) al cierre del día. Simulado con distribución normal N(3 000 000 ; 800 000²). |
importe_txn |
Numérico | Monto de la transacción más reciente (depósito, retiro, pago) en guaraníes. Generado con distribución log‑normal (sesgo positivo). |
score_crediticio |
Entero | Puntaje crediticio en escala 300 – 850 (similar a FICO); representa el riesgo de impago del cliente. |
moroso |
Lógico (TRUE / FALSE ) |
Indicador de morosidad: TRUE si el cliente presenta atraso > 90 días en algún producto; FALSE en caso contrario (prevalencia ≈ 5 %). |
# ---------------------------------------------------------------------------
# Simulación de datos: 100 millones de filas con distintos tipos de datos
# ---------------------------------------------------------------------------
set.seed(123)
n_reg <- 1e7 # 10 M
prod_vec <- c("CuentaCorriente", "TarjetaCredito", "Prestamo")
score_vec <- 300:850 # puntaje crediticio típico
df <- data.frame(
id_cliente = 1:n_reg,
producto = sample(prod_vec, n_reg, TRUE),
saldo_actual = round(rnorm(n_reg, mean = 3e6, sd = 8e5), 0),
importe_txn = round(rlnorm(n_reg, meanlog = 8, sdlog = 0.8), 0),
score_crediticio= sample(score_vec, n_reg, TRUE),
moroso = sample(c(TRUE, FALSE), n_reg, TRUE, prob = c(0.05, 0.95))
)
# si tu tabla se llama df → pasarla a data.table
dt <- as.data.table(df)
## ────────────────── Descriptiva para 10 M filas ──────────────────
library(data.table)
library(dplyr)
library(ggplot2)
library(kableExtra)
# 1) Estadísticos globales para numéricas
desc_num <- dt[, .(
media = round(mean(saldo_actual), 0),
mediana = round(median(saldo_actual), 0),
desvío = round(sd(saldo_actual), 0),
p95 = round(quantile(saldo_actual, .95), 0)
)]
desc_num
## media mediana desvío p95
## <num> <num> <num> <num>
## 1: 2999854 2999875 799951 4316083
desc_txn <- dt[, .(
media = round(mean(importe_txn), 0),
mediana = round(median(importe_txn), 0),
desvío = round(sd(importe_txn), 0),
p95 = round(quantile(importe_txn, .95), 0)
)]
desc_txn
## media mediana desvío p95
## <num> <num> <num> <num>
## 1: 4106 2982 3892 11110
desc_scores <- dt[, .(
media = mean(score_crediticio),
mediana = median(score_crediticio),
desvío = sd(score_crediticio),
p95 = quantile(score_crediticio, .95)
)]
desc_scores
## media mediana desvío p95
## <num> <num> <num> <num>
## 1: 575.0074 575 159.062 823
tabla_desc <- rbindlist(list(
cbind(variable = "saldo_actual", desc_num),
cbind(variable = "importe_txn", desc_txn),
cbind(variable = "score_crediticio", desc_scores)
))
kable(tabla_desc, caption = "Estadística descriptiva global") |>
kable_styling(full_width = FALSE)
variable | media | mediana | desvío | p95 |
---|---|---|---|---|
saldo_actual | 2999854.0000 | 2999875 | 799951.000 | 4316083 |
importe_txn | 4106.0000 | 2982 | 3892.000 | 11110 |
score_crediticio | 575.0074 | 575 | 159.062 | 823 |
# 2) Distribución por producto y morosidad
tx_prod <- dt[, .(
Saldo_Medio = mean(saldo_actual),
Importe_Medio = mean(importe_txn),
Morosos_Pct = round(100 * mean(moroso), 2)
), by = producto]
kable(tx_prod, caption = "Indicadores medios por producto")
producto | Saldo_Medio | Importe_Medio | Morosos_Pct |
---|---|---|---|
Prestamo | 3000594 | 4104.213 | 5.01 |
TarjetaCredito | 2999903 | 4103.806 | 5.01 |
CuentaCorriente | 2999066 | 4109.229 | 5.01 |
# 3) Muestras para gráficos (100 k para no saturar)
set.seed(999)
sample_dt <- dt[sample(.N, 1e5)]
## Histograma / densidad de saldo por producto
ggplot(sample_dt, aes(saldo_actual, fill = producto)) +
geom_histogram(bins = 60, alpha = .7, position = "identity") +
facet_wrap(~ producto, scales = "free_y") +
scale_x_continuous(labels = scales::comma) +
labs(title = "Distribución de saldos por producto",
x = "Saldo actual (₲)", y = "Frecuencia") +
theme_minimal()
## Boxplot de importe de transacción por producto
ggplot(sample_dt, aes(producto, importe_txn, fill = producto)) +
geom_boxplot(outlier.alpha = .1) +
scale_y_log10(labels = scales::comma) +
labs(title = "Importe de transacción (escala log10)",
x = NULL, y = "₲ (log10)") +
theme_minimal()
## Score crediticio: densidad morosos vs no morosos
ggplot(sample_dt, aes(score_crediticio, color = moroso, fill = moroso)) +
geom_density(alpha = .3) +
labs(title = "Distribución de puntaje crediticio según morosidad",
x = "Score", y = "Densidad", color = NULL, fill = NULL) +
theme_minimal()
## Barras proporción de morosos por producto
morosidad_prod <- dt[, .(Morosos = mean(moroso)), by = producto]
ggplot(morosidad_prod, aes(producto, Morosos*100, fill = producto)) +
geom_col(width = .6) +
labs(title = "Tasa de morosidad por producto",
x = NULL, y = "% clientes morosos") +
theme_minimal() +
scale_y_continuous(labels = function(x) paste0(x, "%")) +
theme(legend.position = "none")
# Crear una lista para almacenar los tiempos de ejecución
resultados <- list()
# ---------------------------------------------------------------------------
# 1. data.table: Uso de fwrite y fread para guardar y leer archivos CSV
# ---------------------------------------------------------------------------
cat("data.table:\n")
## data.table:
time_dt_save <- system.time({
fwrite(df, "data_dt.csv")
})
time_dt_read <- system.time({
df_dt <- fread("data_dt.csv")
})
resultados$data_table <- list(save = time_dt_save, read = time_dt_read)
print(resultados$data_table)
## $save
## user system elapsed
## 1.58 0.12 0.86
##
## $read
## user system elapsed
## 1.87 0.44 0.76
# ---------------------------------------------------------------------------
# 2. arrow: Escritura y lectura en formato Parquet
# ---------------------------------------------------------------------------
cat("arrow (Parquet):\n")
## arrow (Parquet):
time_arrow_save <- system.time({
write_parquet(df, "data_arrow.parquet")
})
time_arrow_read <- system.time({
df_arrow <- read_parquet("data_arrow.parquet")
})
resultados$arrow <- list(save = time_arrow_save, read = time_arrow_read)
print(resultados$arrow)
## $save
## user system elapsed
## 1.50 0.12 1.83
##
## $read
## user system elapsed
## 0.33 0.20 0.66
# ---------------------------------------------------------------------------
# 3. fst: Escritura y lectura con formato .fst
# ---------------------------------------------------------------------------
cat("fst:\n")
## fst:
time_fst_save <- system.time({
write_fst(df, "data_fst.fst")
})
time_fst_read <- system.time({
df_fst <- read_fst("data_fst.fst")
})
resultados$fst <- list(save = time_fst_save, read = time_fst_read)
print(resultados$fst)
## $save
## user system elapsed
## 0.62 0.16 1.37
##
## $read
## user system elapsed
## 0.69 0.08 1.54
# ---------------------------------------------------------------------------
# 5. bigmemory: Conversión de columnas numéricas a una big.matrix
# (bigmemory trabaja sobre matrices numéricas, por lo que se seleccionan las
# columnas ID, Valor_Numerico y Valor_Entero)
# ---------------------------------------------------------------------------
cat("bigmemory:\n")
## bigmemory:
time_bigmemory <- system.time({
big_mat <- as.big.matrix(df[, c("id_cliente", "producto", "saldo_actual","importe_txn", "score_crediticio","moroso")])
})
resultados$bigmemory <- list(create = time_bigmemory)
print(resultados$bigmemory)
## $create
## user system elapsed
## 6.14 4.03 10.31
# ---------------------------------------------------------------------------
# 6. sparklyr: Copiar el data frame a Spark y realizar una operación simple
# ---------------------------------------------------------------------------
cat("sparklyr:\n")
## sparklyr:
# Intentar desconectar cualquier sesión previa
if (exists("sc") && spark_connection_is_open(sc)) {
spark_disconnect(sc)
}
# Conectar a Spark
time_spark_connect <- system.time({
sc <- tryCatch({
spark_connect(master = "local")
}, error = function(e) {
message("Error al conectar con Spark: ", e$message)
NULL
})
})
# Verificar si la conexión se estableció correctamente
if (!is.null(sc)) {
# Seleccionar una muestra del DataFrame para evitar consumo excesivo de memoria
df_sample <- df[sample(nrow(df), min(100000, nrow(df))), ] # Evita errores si df tiene <100000 filas
# Copiar datos a Spark
time_spark_copy <- system.time({
df_spark <- tryCatch({
copy_to(sc, df_sample, overwrite = TRUE)
}, error = function(e) {
message("Error al copiar datos a Spark: ", e$message)
NULL
})
})
# Ejecutar operación solo si la copia fue exitosa
if (!is.null(df_spark)) {
time_spark_op <- system.time({
df_spark_result <- tryCatch({
df_spark %>%
filter(Valor_Numerico > 0) %>%
summarise(Media = mean(Valor_Numerico)) %>%
collect()
}, error = function(e) {
message("Error en la operación con Spark: ", e$message)
NULL
})
})
} else {
time_spark_op <- NA
}
# Desconectar Spark
spark_disconnect(sc)
} else {
time_spark_copy <- NA
time_spark_op <- NA
}
# Almacenar resultados en la lista
resultados$sparklyr <- list(connect = time_spark_connect, copy = time_spark_copy, operation = time_spark_op)
# Imprimir los resultados
print(resultados$sparklyr)
## $connect
## user system elapsed
## 0.08 0.07 9.16
##
## $copy
## user system elapsed
## 0.22 0.02 5.84
##
## $operation
## user system elapsed
## 0.09 0.05 0.25
# ---------------------------------------------------------------------------
# Mostrar resultados finales de los tiempos de ejecución
# ---------------------------------------------------------------------------
print(resultados)
## $data_table
## $data_table$save
## user system elapsed
## 1.58 0.12 0.86
##
## $data_table$read
## user system elapsed
## 1.87 0.44 0.76
##
##
## $arrow
## $arrow$save
## user system elapsed
## 1.50 0.12 1.83
##
## $arrow$read
## user system elapsed
## 0.33 0.20 0.66
##
##
## $fst
## $fst$save
## user system elapsed
## 0.62 0.16 1.37
##
## $fst$read
## user system elapsed
## 0.69 0.08 1.54
##
##
## $bigmemory
## $bigmemory$create
## user system elapsed
## 6.14 4.03 10.31
##
##
## $sparklyr
## $sparklyr$connect
## user system elapsed
## 0.08 0.07 9.16
##
## $sparklyr$copy
## user system elapsed
## 0.22 0.02 5.84
##
## $sparklyr$operation
## user system elapsed
## 0.09 0.05 0.25
# Cargar librerías necesarias
library(ggplot2)
library(dplyr)
# Convertir la lista de resultados en un data frame
df_resultados <- do.call(rbind, lapply(names(resultados), function(metodo) {
subresultados <- resultados[[metodo]]
do.call(rbind, lapply(names(subresultados), function(accion) {
data.frame(
Metodo = paste(metodo, accion, sep = " - "),
Tiempo = subresultados[[accion]]["elapsed"]
)
}))
}))
# Asegurar que los valores sean numéricos
df_resultados$Tiempo <- as.numeric(df_resultados$Tiempo)
df_resultados
## Metodo Tiempo
## elapsed data_table - save 0.86
## elapsed1 data_table - read 0.76
## elapsed3 arrow - save 1.83
## elapsed11 arrow - read 0.66
## elapsed4 fst - save 1.37
## elapsed12 fst - read 1.54
## elapsed5 bigmemory - create 10.31
## elapsed6 sparklyr - connect 9.16
## elapsed13 sparklyr - copy 5.84
## elapsed2 sparklyr - operation 0.25
library(ggplot2)
library(dplyr)
library(dplyr)
df_resultados <- df_resultados %>%
mutate(
MetodoPrincipal = sub(" - .*", "", Metodo), # Todo lo antes de " - "
Operacion = sub(".* - ", "", Metodo) # Todo lo después de " - "
)
library(ggplot2)
ggplot(df_resultados, aes(x = reorder(MetodoPrincipal, Tiempo), y = Tiempo, fill = MetodoPrincipal)) +
geom_col(width = 0.7) +
coord_flip() +
facet_wrap(~ Operacion, scales = "free_x") +
labs(
title = "Comparación de Tiempos de Ejecución en R para 100 Millones de Registros",
x = "Método Principal",
y = "Tiempo (segundos)"
) +
theme_minimal(base_size = 12) +
theme(
legend.position = "none",
axis.text.x = element_text(angle = 45, hjust = 1)
)
library(dplyr)
library(ggplot2)
df_plot <- df_resultados %>% # resultado largo
filter(Operacion %in% c("save", "read")) %>% # << solo estas dos
mutate(
Metodo = sub(" - .*", "", Metodo), # método “limpio”
OperacionLab = recode(Operacion, # etiquetas en español
save = "Guardado",
read = "Carga")
)
ggplot(df_plot,
aes(x = reorder(Metodo, Tiempo), y = Tiempo, fill = Metodo)) +
geom_col(width = 0.7, show.legend = FALSE) +
coord_flip() +
facet_wrap(~ OperacionLab, ncol = 1, scales = "free_x") + # panel vertical
labs(
title = "Tiempos de guardado y carga (10 M registros)",
x = "Método",
y = "Tiempo (segundos)"
) +
theme_minimal(base_size = 12)
El presente análisis comparativo de librerías para el manejo de grandes volúmenes de datos en R, basado en la simulación de 100 millones de registros, ha permitido evaluar el desempeño de diversas herramientas en términos de tiempo de ejecución para las tareas de almacenamiento, lectura y procesamiento de datos. Los resultados obtenidos muestran diferencias significativas en la eficiencia de cada método, lo que sugiere que la elección de la librería más adecuada dependerá de los requerimientos específicos del usuario y las características del conjunto de datos.
data.table
exhibe tiempos de
escritura y lectura relativamente elevados para 100 millones de
registros (22.95s en guardado, 88.40s en lectura), lo que podría
penalizar su uso en escenarios donde la velocidad de acceso a disco sea
esencial.
arrow
(Parquet) mantiene tiempos de
escritura aceptables (21.75s) y mejora notablemente en lectura (11.88s)
en comparación con data.table
, situándose como una buena
alternativa cuando se busca un equilibrio entre velocidad y
compatibilidad con formatos avanzados.
fst
continúa destacándose por su
eficiencia, con 14.10s en escritura y 6.00s en lectura,
constituyendo así la mejor opción para quienes priorizan una rápida
lectura/escritura en archivos locales de gran tamaño.
bigmemory
muestra tiempos altos en
la creación de objetos (97.64s), lo que sugiere que su principal ventaja
es trabajar con subconjuntos de datos en memoria compartida, más que en
la etapa de inicialización, que resulta costosa.
sparklyr
presenta un tiempo
moderado de conexión (20.20s), pero muestra gran rapidez en la
copia de datos (2.27s) y operación (1.02s). Esto lo hace
adecuado para entornos de procesamiento distribuido, donde, tras la fase
de inicialización, las operaciones sucesivas se benefician de la
infraestructura de Spark.
fst
sigue siendo la mejor opción.arrow
se mantiene como una alternativa
sólida para formatos de almacenamiento modernos y un tiempo de
lectura razonable en comparación con
data.table
.data.table
es potente para
manipulación en memoria, pero sus tiempos de lectura y escritura con 100
millones de registros pueden llegar a ser significativamente
altos.bigmemory
puede ser beneficioso cuando
se trabaja con grandes matrices en memoria compartida, aunque la
inicialización sea costosa.sparklyr
resulta ventajoso para
volúmenes de datos muy grandes en entornos distribuidos, especialmente
cuando las operaciones posteriores (filtrado, agregados) compensan el
costo inicial de conexión.En conclusión, la elección de la librería dependerá del tipo
de almacenamiento, disponibilidad de recursos de
hardware y flujo de trabajo requerido. Para la
mayoría de los casos en que se necesita rápida lectura y
escritura de datos muy voluminosos de forma local,
fst
mantiene la mejor relación entre velocidad de guardado
y acceso.
data.frame
. R package version 1.14.2.