Introducción

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:

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.

Metodología

El estudio se desarrolló en tres fases:

  1. Generación de Datos Simulados
    • Se generó un conjunto de datos con 100 millones de observaciones, incluyendo variables numéricas, enteras y categóricas.
    • Se utilizó la función rnorm() para simular datos numéricos y sample() para variables categóricas.
  2. Evaluación del Desempeño
    • Se midieron los tiempos de ejecución en diversas tareas utilizando system.time().
    • Las operaciones evaluadas fueron:
      • Escritura de los datos en disco
      • Lectura de los datos desde disco
      • Carga en memoria
      • Operaciones dentro del entorno de cada librería
  3. Análisis y Visualización de Resultados
    • Se compararon los tiempos de ejecución mediante gráficos generados con ggplot2.
    • Se analizaron las diferencias en rendimiento según el tipo de almacenamiento y acceso a datos.

Descripción y Origen de los Paquetes


Procedimiento

# configurar la ruta para el trabajo

setwd("G:/Mi unidad/CursoBigData")

Instalar y cargar librerias

# 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

Simulación de la tabla para leer y guardar por cada libreria

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)
Estadística descriptiva global
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")
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")

Repositorio de los resultados

# Crear una lista para almacenar los tiempos de ejecución
resultados <- list()

Rendimiento de la librería Data.table

# ---------------------------------------------------------------------------
# 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

Rendimiento de la librería Arrow

# ---------------------------------------------------------------------------
# 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

Rendimiento de la librería fst

# ---------------------------------------------------------------------------
# 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

Rendimiento de la librería bigmemory

# ---------------------------------------------------------------------------
# 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

Rendimiento de la librería sparklyr

# ---------------------------------------------------------------------------
# 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

# ---------------------------------------------------------------------------
# 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)

Conclusión

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.

Principales hallazgos:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

Recomendaciones:

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.

Referencias