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)
## 
## Attaching package: 'sparklyr'
## The following object is masked from 'package:stats':
## 
##     filter
# 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
## 
## Attaching package: 'arrow'
## The following object is masked from 'package:utils':
## 
##     timestamp
library(fst)          # Para lectura y escritura ultrarrápida
library(ff)           # Para trabajar con datos que exceden la memoria RAM
## Loading required package: bit
## 
## Attaching package: 'bit'
## The following object is masked from 'package:data.table':
## 
##     setattr
## The following object is masked from 'package:base':
## 
##     xor
## Attaching package ff
## - getOption("fftempdir")=="C:/Users/Diego/AppData/Local/Temp/RtmpywlVNe/ff"
## - getOption("ffextension")=="ff"
## - getOption("ffdrop")==TRUE
## - getOption("fffinonexit")==TRUE
## - getOption("ffpagesize")==65536
## - getOption("ffcaching")=="mmnoflush"  -- consider "ffeachflush" if your system stalls on large writes
## - getOption("ffbatchbytes")==16777216 -- consider a different value for tuning your system
## - getOption("ffmaxbytes")==536870912 -- consider a different value for tuning your system
## 
## Attaching package: 'ff'
## The following object is masked from 'package:arrow':
## 
##     boolean
## The following objects are masked from 'package:utils':
## 
##     write.csv, write.csv2
## The following objects are masked from 'package:base':
## 
##     is.factor, is.ordered
library(bigmemory)    # Para manejar matrices grandes (solo numéricas)
## 
## Attaching package: 'bigmemory'
## The following object is masked from 'package:ff':
## 
##     is.readonly
library(sparklyr)     # Para procesamiento distribuido con Apache Spark
library(dplyr)        # Para manipulación de datos con sintaxis dplyr
## 
## Attaching package: 'dplyr'
## The following object is masked from 'package:bit':
## 
##     symdiff
## The following objects are masked from 'package:data.table':
## 
##     between, first, last
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union

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

# ---------------------------------------------------------------------------
# Simulación de datos: 100 millones de filas con distintos tipos de datos
# ---------------------------------------------------------------------------
num_rows <- 100000000  # 100 millones de filas
#num_rows <- 10000000  # 10 millones de filas
set.seed(123)
df <- data.frame(
  ID = 1:num_rows,
  Valor_Numerico = rnorm(num_rows),
  Valor_Entero = sample(1:1000, num_rows, replace = TRUE),
  Categoria = sample(c("A", "B", "C", "D"), num_rows, replace = TRUE),
  Booleano = sample(c(TRUE, FALSE), num_rows, replace = TRUE)
)
# Convertir variables de tipo character a factor antes de usar ff
df$Categoria <- as.factor(df$Categoria)
#cantidad de filas de la tabla
nrow(df)
## [1] 100000000

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 
##   16.11    1.82   55.97 
## 
## $read
##    user  system elapsed 
##    9.79    3.00   51.98

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 
##   11.82    0.71   43.84 
## 
## $read
##    user  system elapsed 
##    2.49    1.66    9.44

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 
##    1.05    0.56   36.80 
## 
## $read
##    user  system elapsed 
##    1.20    0.63    4.91

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", "Valor_Numerico", "Valor_Entero")])
})
resultados$bigmemory <- list(create = time_bigmemory)
print(resultados$bigmemory)
## $create
##    user  system elapsed 
##   60.50   37.75  148.83

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
  })
})
## Error al conectar con Spark: Gateway in localhost:8880 did not respond.
## 
## 
## Try running `options(sparklyr.log.console = TRUE)` followed by `sc <- spark_connect(...)` for more debugging info.
# 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.09   71.75 
## 
## $copy
## [1] NA
## 
## $operation
## [1] NA

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 
##   16.11    1.82   55.97 
## 
## $data_table$read
##    user  system elapsed 
##    9.79    3.00   51.98 
## 
## 
## $arrow
## $arrow$save
##    user  system elapsed 
##   11.82    0.71   43.84 
## 
## $arrow$read
##    user  system elapsed 
##    2.49    1.66    9.44 
## 
## 
## $fst
## $fst$save
##    user  system elapsed 
##    1.05    0.56   36.80 
## 
## $fst$read
##    user  system elapsed 
##    1.20    0.63    4.91 
## 
## 
## $bigmemory
## $bigmemory$create
##    user  system elapsed 
##   60.50   37.75  148.83 
## 
## 
## $sparklyr
## $sparklyr$connect
##    user  system elapsed 
##    0.08    0.09   71.75 
## 
## $sparklyr$copy
## [1] NA
## 
## $sparklyr$operation
## [1] NA
# 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  55.97
## elapsed1     data_table - read  51.98
## elapsed2          arrow - save  43.84
## elapsed11         arrow - read   9.44
## elapsed3            fst - save  36.80
## elapsed12           fst - read   4.91
## elapsed4    bigmemory - create 148.83
## elapsed5    sparklyr - connect  71.75
## 1              sparklyr - copy     NA
## 11        sparklyr - operation     NA
# Crear gráfico de barras con ggplot2
ggplot(df_resultados, aes(x = reorder(Metodo, Tiempo), y = Tiempo, fill = Metodo)) +
  geom_bar(stat = "identity") +
  coord_flip() +  # Invertir el eje para mejor visualización
  labs(title = "Comparación de Tiempos de Ejecución en R para 100 millones de registros",
       x = "Método",
       y = "Tiempo (segundos)") +
  theme_minimal() +
  theme(legend.position = "none")  # Ocultar la leyenda

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