--- title: "Rendimiento de rcamelspe: comparación reproducible con RCamelsPE" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Rendimiento de rcamelspe: comparación reproducible con RCamelsPE} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = '#>', fig.width = 8, fig.height = 6.5, dpi = 144) candidates <- c(system.file('benchmarks', package = 'rcamelspe'), '../inst/benchmarks', 'inst/benchmarks') bench_dir <- candidates[file.exists(file.path(candidates, 'results', 'summary.csv'))][1] stopifnot(!is.na(bench_dir)) results <- read.csv(file.path(bench_dir, 'results', 'summary.csv')) checks <- read.csv(file.path(bench_dir, 'results', 'equivalence.csv')) manifest <- read.csv(file.path(bench_dir, 'results', 'data-manifest.csv')) commit <- trimws(readLines(file.path(bench_dir, 'upstream-commit.txt'))[1]) labels <- c(metadata = 'Metadatos: 136 estaciones', attributes = 'Atributos: siete tablas', one = 'Una cuenca: todas las variables', five = 'Cinco cuencas: dos variables', ten = 'Diez cuencas: dos variables', twenty = 'Veinte cuencas: dos variables', forty = 'Cuarenta cuencas: dos variables', global_all = 'Nacional: todas las variables', global_projected = 'Nacional: dos variables', global_window = 'Nacional: dos variables, año 2000', gauges = 'Geoespacial: estaciones') official <- results[results$package == 'RCamelsPE', ] ours <- results[results$package == 'rcamelspe', ] comparison <- merge(official, ours, by = 'scenario', suffixes = c('_ref', '_ours')) comparison <- comparison[match(names(labels), comparison$scenario), ] comparison$speedup <- comparison$median_s_ref / comparison$median_s_ours comparison$saving <- 100 * (1 - comparison$median_s_ours / comparison$median_s_ref) best <- comparison[which.max(comparison$speedup), ] ``` `rcamelspe` combina Arrow para leer y seleccionar datos con `collapse` para uniones y transformaciones. Esta viñeta evalúa el beneficio práctico frente al paquete [RCamelsPE de Harold Llauca](https://github.com/hllauca/RCamelsPE), utilizando los mismos archivos locales y consultas con resultados equivalentes. Destaca las ventajas **medidas** de nuestra implementación y permite identificar las operaciones donde conviene usarla. Los resultados corresponden a esta máquina y estas revisiones; no garantizan la misma aceleración en otros equipos. ## Resultados: dónde es más eficiente rcamelspe En esta ejecución, `rcamelspe` obtuvo una mediana menor en **`r sum(comparison$speedup > 1)` de `r nrow(comparison)` escenarios**. La mayor aceleración fue **`r sprintf('%.2f', best$speedup)` veces** en **`r labels[best$scenario]`**, con una reducción de tiempo del **`r sprintf('%.1f', best$saving)` %**. Se verificó la equivalencia en todos los escenarios antes de medir. Cada celda muestra **mediana [percentil 25–percentil 75]**, en segundos, de siete repeticiones. La aceleración es `mediana(RCamelsPE) / mediana(rcamelspe)`: valores mayores que uno favorecen a `rcamelspe`; valores menores que uno favorecen a `RCamelsPE`. ```{r results-table, echo=FALSE} format_time <- function(m, q1, q3) sprintf('%.3f [%.3f–%.3f]', m, q1, q3) table <- data.frame( Escenario = unname(labels[comparison$scenario]), RCamelsPE = format_time(comparison$median_s_ref, comparison$q25_s_ref, comparison$q75_s_ref), rcamelspe = format_time(comparison$median_s_ours, comparison$q25_s_ours, comparison$q75_s_ours), Aceleracion = sprintf('%.2fx', comparison$speedup), check.names = FALSE) knitr::kable(table) ``` ```{r speedup-plot, echo=FALSE, fig.cap='Aceleración calculada a partir de las medianas. La línea vertical indica igualdad de tiempo.'} library(ggplot2) comparison$label <- factor(unname(labels[comparison$scenario]), levels = rev(unname(labels))) ggplot(comparison, aes(speedup, label)) + geom_vline(xintercept = 1, colour = '#89939E', linetype = 2) + geom_segment(aes(x = 1, xend = speedup, yend = label), colour = '#A8BCCB', linewidth = 1) + geom_point(colour = '#245A73', size = 3) + geom_text(aes(label = sprintf('%.2fx', speedup)), nudge_y = .23, size = 3.2) + scale_x_log10() + labs(x = 'Aceleración (escala logarítmica; mayor favorece a rcamelspe)', y = NULL) + theme_minimal(base_size = 11) + theme(panel.grid.minor = element_blank()) ``` Los intervalos intercuartílicos describen dispersión, no son intervalos de confianza. En operaciones de pocos milisegundos, diferencias pequeñas pueden cambiar con el ruido del sistema. Se muestran todos los escenarios y no se descartan repeticiones con recolección de basura. ### Lectura práctica de los resultados ```{r interpretation, echo=FALSE, results='asis'} for (key in c('global_all', 'global_projected', 'global_window', 'ten')) { row <- comparison[comparison$scenario == key, ] cat(sprintf('- **%s:** %.3f s con `rcamelspe` y %.3f s con `RCamelsPE` (%.2fx).\n', labels[key], row$median_s_ours, row$median_s_ref, row$speedup)) } ``` ### Corrección del cuello de botella de diez cuencas La implementación anterior cambiaba al maestro al superar cinco cuencas: escaneaba unos 178 MiB incluso para una selección pequeña. El perfil de rutas mostró aproximadamente 0,16 s para diez archivos frente a 0,53 s para el maestro; con cuarenta archivos, la relación se invertía (0,66 s frente a 0,53 s). Estas mediciones exploratorias motivaron el cambio; la tabla principal contiene la validación repetida del código final. El perfil exploratorio (medianas de tres llamadas por ruta, dos hilos) se conserva en `inst/benchmarks/routing-profile.csv`; no se mezcla con el benchmark final. Ahora se compara el tamaño de los CSV solicitados, más una estimación de 4 MiB por apertura de archivo, con el tamaño del maestro. Así se tiene en cuenta tanto el volumen a leer como la sobrecarga de múltiples lectores. Es una heurística calibrada con estos datos, no una garantía del punto óptimo para todos los discos. `global = TRUE` mantiene el control explícito. No se introduce una caché de datos. ```{r before-after, echo=FALSE} previous <- read.csv(file.path(bench_dir, 'before-routing', 'summary.csv')) old <- previous$median_s[previous$scenario == 'ten' & previous$package == 'rcamelspe'] new <- comparison$median_s_ours[comparison$scenario == 'ten'] knitr::kable(data.frame(Consulta = 'Diez cuencas', Anterior_s = round(old, 3), Actual_s = round(new, 3), Mejora_interna = sprintf('%.2fx', old / new))) ``` La referencia anterior se conserva en `inst/benchmarks/before-routing/`, junto con sus mediciones, entorno y hashes. Corresponde a una ejecución anterior; para comparar paquetes en la misma ejecución se debe usar la tabla principal. ## Escenarios y decisiones de implementación El archivo maestro medido ocupa **`r sprintf('%.2f', manifest$bytes[manifest$file == '03_timeseries/timeseries.csv'] / 1024^2)` MiB** y contiene **`r format(checks$rows[checks$scenario == 'global_all'], big.mark = '.', decimal.mark = ',', scientific = FALSE)` filas**. Las consultas nacionales abarcan las 136 estaciones; las consultas por cuenca usan los mismos identificadores en ambos paquetes. | Escenario | Trabajo realizado por ambos paquetes | Utilidad | |---|---|---| | Metadatos | Leer `stations.csv` completo | Inventario de estaciones | | Atributos | Leer y unir las siete tablas por `gauge_id` | Regionalización | | Una cuenca | Serie completa, diez variables | Exploración de una estación | | Cinco, diez, veinte y cuarenta cuencas | Serie completa de `prec` y `flow_obs`, con identificadores | Procesamiento por lotes | | Nacional completo | Archivo maestro, todas las filas y variables | Análisis nacional | | Nacional proyectado | Archivo maestro, `prec` y `flow_obs`, con identificadores | Selección de variables | | Nacional, año 2000 | Misma selección, limitada a un año | Calibración y validación | | Geoespacial | GeoPackage de estaciones completo | Control basado en `sf` | Para los lotes se usa el modo habitual por cuenca de `RCamelsPE`. `rcamelspe` decide su ruta automáticamente según el coste estimado de los CSV. Se compara así la experiencia de uso sin forzar al paquete de referencia a una ruta menos conveniente. Los escenarios nacionales fuerzan `global = TRUE` en ambos paquetes. La revisión evaluada de `RCamelsPE` no ofrece argumentos de fecha en `read_timeseries()`. En el escenario anual, se añade `dplyr::filter()` a su lectura **dentro del tiempo medido**. `rcamelspe` aplica `start_date` y `end_date` en su consulta. Ambos entregan la misma ventana y las mismas columnas. ### Cómo contribuyen nuestras mejoras - **Lectura selectiva:** `arrow::read_csv_arrow()` proyecta columnas para consultas pequeñas. En la ruta nacional, `arrow::open_dataset()` aplica selección y filtros antes de materializar el `data.frame` final. - **Uniones y lotes:** `collapse::join()` combina atributos y `dplyr::bind_rows()` reúne archivos individuales conservando sus tipos. Se reemplazó `collapse::rowbind()` tras detectar atributos corruptos en lotes repetidos de diez cuencas durante la verificación. - **Consulta temporal integrada:** los límites de fecha se validan en el lector, facilitando ventanas de trabajo reproducibles. - **Control de lectura:** `global = TRUE` permite elegir el maestro; `date` y `gauge_id` pueden incluirse en la selección de variables. CSV es texto: proyectar y filtrar **no implica saltarse físicamente todos los bytes o filas descartados**, como podría ocurrir con datos columnarios particionados. El beneficio se evalúa con los resultados, sin equipararlo a una reducción proporcional de lectura del disco. La descarga de CAMELS-PE 1.0.1, el respeto de `version` y `set_path` y la protección frente a descargas parciales mejoran el uso del paquete, pero no intervienen en estos tiempos. Se excluye la red para separar lectura local y conexión. ## Comprobación de equivalencia Antes del cronómetro, el script crea copias independientes para no alterar los resultados de los lectores. Ordena filas por `gauge_id` y `date`, ordena las columnas y normaliza contenedores (`tibble`/`data.frame`) y representaciones numéricas. Compara **todas las columnas**, con tolerancia numérica `1e-8`, conservando los valores faltantes. En objetos espaciales verifica geometrías y sistema de referencia. Una discrepancia detiene la ejecución. Esta validación no forma parte del tiempo medido. ```{r equivalence-table, echo=FALSE} checks <- checks[match(names(labels), checks$scenario), ] knitr::kable(data.frame(Escenario = unname(labels[checks$scenario]), Filas = checks$rows, Columnas = checks$columns, Equivalente = ifelse(checks$equivalent, 'Sí', 'No'))) ``` CAMELS-PE usa el calendario común 1981–2025. `flow_obs` y `flow_sim` están en mm/día y `prec_var` es varianza de precipitación. Los `NA` permanecen como faltantes: no se rellenan para mejorar los tiempos. El conjunto local se identifica mediante hashes; no se le atribuye una versión de Zenodo sin verificar su procedencia. La tabla de equivalencia describe los archivos efectivamente medidos, no una certificación del contenido de una publicación. Por ejemplo, el número de columnas de la unión de atributos incluye `gauge_id` y debe interpretarse según el diccionario local, sin sustituirlo por el total anunciado de otra versión. ### Qué sabemos sobre memoria Se conserva `object.size()` de la salida en `equivalence.csv`. **No mide el pico de RAM ni asignaciones temporales**. Arrow puede asignar memoria fuera del gestor de R; un perfil de asignaciones de R tampoco bastaría para comparar consumo total. No se afirma un ahorro porcentual de memoria sin medirlo a nivel del proceso. ## Método reproducible 1. Ambos paquetes se cargan antes de medir y leen el mismo directorio local. 2. Se eligen los primeros identificadores ordenados alfabéticamente, evitando seleccionar manualmente cuencas favorables. 3. Se fijan dos hilos tanto para Arrow como para `readr`. 4. Se ejecuta un calentamiento por paquete y escenario, utilizado también para verificar equivalencia. La prueba es de **caché caliente**: no se vacía la caché del sistema operativo. 5. Se realizan siete repeticiones, aleatorizando el orden de los paquetes en cada par con semilla `20260905`. La recolección explícita de basura ocurre antes del cronómetro; la que ocurra dentro de la llamada sí queda incluida. 6. Se registra tiempo de pared con `Sys.time()`. Para metadatos y estaciones espaciales se promedian 20 llamadas por repetición, evitando tiempos nulos por resolución del reloj; los demás escenarios usan una llamada por repetición. Se excluyen instalación, calentamiento, validación y generación del informe. Referencia: `RCamelsPE` 1.0.1, revisión [`40af1422d2b9`](https://github.com/hllauca/RCamelsPE/tree/40af1422d2b9893973ffae7f4678136860f09c52). La versión local es `rcamelspe` 0.1.0 con las mejoras del repositorio; `code-manifest.csv` registra hashes del código utilizado. ### Volver a ejecutar La medición intensiva es explícita: no se dispara al construir la documentación. Los CSV incluidos permiten regenerar esta viñeta sin descargar datos ni instalar el paquete de referencia. Para medir de nuevo, desde la raíz del repositorio: ```sh git clone https://github.com/hllauca/RCamelsPE.git tmp/benchmark-reference git -C tmp/benchmark-reference checkout 40af1422d2b9893973ffae7f4678136860f09c52 ``` ```{r reproduce, eval=FALSE} # Preparación (solo una vez): install.packages(c('pkgload', 'remotes')) remotes::install_deps('.', dependencies = TRUE) remotes::install_deps('tmp/benchmark-reference', dependencies = TRUE) # Desde una terminal, sustituir las rutas: # Rscript inst/benchmarks/run-benchmark.R /datos/CAMELS-PE tmp/benchmark-reference inst/benchmarks/results 7 rmarkdown::render('vignettes/benchmark.Rmd') ``` `inst/benchmarks/results/` contiene `timings.csv` (cada repetición), `summary.csv` (estadísticos), `equivalence.csv` (verificaciones), `data-manifest.csv` (tamaños y hashes de entrada), `code-manifest.csv` (código local) y `session.txt` (entorno). Si se cambia la revisión de referencia, hay que actualizar `upstream-commit.txt` junto con los resultados. ```{r environment, echo=FALSE} cat(readLines(file.path(bench_dir, 'results', 'session.txt')), sep = '\n') ``` ## Cómo aprovechar rcamelspe Solicita las variables y fechas necesarias, reutiliza la ruta local y consulta el diccionario antes del análisis. Este patrón aprovecha la lectura selectiva: ```{r example, eval=FALSE} path <- rcamelspe::get_camels_pe_path() daily <- rcamelspe::read_timeseries( global = TRUE, vars = c('date', 'gauge_id', 'prec', 'flow_obs'), start_date = '2000-01-01', end_date = '2000-12-31', path = path ) ``` La elección depende del escenario. Consultas selectivas y lotes muestran el valor del motor de lectura; operaciones pequeñas o basadas en el mismo lector espacial pueden tener diferencias reducidas. La tabla completa permite justificar esa elección sin extrapolar la aceleración máxima a todo el paquete.