De Excel a R: migrando análisis financieros reales
Sustituir un Excel financiero de 30 pestañas que tarda 8 minutos en abrir por un script de R que tarda 2 segundos. Pasos reales, problemas reales, y los argumentos para convencer al financiero de que merece la pena.
Migrar un análisis financiero de Excel a R se hace en cinco pasos y en este orden. Primero se extraen los datos crudos del Excel a un dataframe con read_excel(), indicando el rango exacto de la tabla y nunca las pestañas con fórmulas. Segundo, se validan tipos y valores nulos disfrazados antes de calcular nada. Tercero, se reescribe una sola métrica clave del Excel en R y se compara al céntimo contra la celda original. Cuarto, y solo cuando esa métrica cuadra, se replica el resto de pestañas como funciones hasta generar el informe entero en código. Y quinto, se programa ese script para que corra solo y se vigila que siga corriendo. El Excel original queda como fuente única de datos crudos, todo el cálculo pasa a vivir en código, y el financiero sigue recibiendo un Excel al final, generado en segundos en vez de armado a mano.
El Excel del financiero pesa 180 MB. Tiene 30 pestañas con fórmulas que apuntan a otras pestañas que apuntan a otras pestañas. Tarda 8 minutos en abrir, otros 4 en recalcular, y si tocas una celda sin querer se rompe un cuadre que nadie sabe explicar. En una reunión, alguien pregunta "¿cuánto vendimos por canal en marzo del año pasado?". El financiero abre el Excel. Espera. Espera. Encuentra la pestaña. Calcula. 15 minutos después responde.
Esto es real. Y es donde R puede cambiar la vida del equipo. Cuento cómo migro este tipo de bestias.
#Por qué Excel deja de servir
Excel es perfecto para tres cosas: prototipar un cálculo, mostrar una tabla simple, y editar a mano. Falla en:
- Reproducibilidad. Una celda cambiada sin querer y el cuadre se rompe sin trazabilidad.
- Escala. Más de ~500.000 filas y el
.xlsxempieza a sufrir. Más de 1 millón ya no abre. - Auditoría. ¿De dónde sale exactamente el total de esta celda? Sigue las flechas durante 20 minutos.
- Concurrencia. Si dos personas editan, una pierde sus cambios. SharePoint mejora algo, no resuelve.
Cuando llegamos a "para sacar el informe semanal hay que pasar 3 horas haciendo copia-pega entre pestañas", es hora.
#La migración en cinco pasos
Paso 1 — Extraer los datos, no las fórmulas
Excel es dos cosas: datos + fórmulas que operan sobre los datos. El truco está en separar. Las fórmulas las vas a reescribir en R. Los datos tienen que salir limpios a CSV.
library(readxl)
library(tidyverse)
library(janitor)
# Lee solo la pestaña de movimientos en bruto, no las pestañas calculadas
mov <- read_excel(
"ventas_2025.xlsx",
sheet = "MOVIMIENTOS",
range = "A3:M50003", # salta cabecera de adorno
col_types = c("date", "text", "text", "numeric", rep("text", 9))
) |>
clean_names()
dim(mov)
# [1] 50000 13Los Excel financieros suelen tener una cabecera de "información" con logo, fecha, autor, etc. Si pasas range = NULL, R lee esa basura. Identifica la fila donde empieza la cabecera real de la tabla y úsalo en range = "A3:...".
Paso 2 — Validar tipos y NULLs disfrazados
Excel es traicionero porque mete strings donde no debería. Una fecha mal formateada queda como número (45291), un importe como texto ("1.234,56€"), un NULL como "-" o "N/A". Antes de calcular nada:
mov |>
summarise(
rows = n(),
fecha_min = min(fecha, na.rm = TRUE),
fecha_max = max(fecha, na.rm = TRUE),
importe_na = sum(is.na(importe)),
importe_min = min(importe, na.rm = TRUE),
importe_max = max(importe, na.rm = TRUE),
categorias_unicas = n_distinct(categoria),
cuentas_unicas = n_distinct(cuenta)
)Si importe_na es > 0 cuando debería ser 0, ya sabes que hay celdas mal. Si fecha_max es del año 2099, hay un typo. Mejor saberlo ahora que tres meses después cuando alguien pregunte por qué un cuadre no encaja.
Paso 3 — Reescribir UNA fórmula del Excel y comparar contra Excel
Esto es lo crítico. No migres todo de golpe. Coge una sola métrica del Excel (la más importante o la más visible) y reprodúcela en R. Compara los dos números. Tienen que coincidir al céntimo.
# La fórmula maestra del Excel es:
# = SUMA(MOVIMIENTOS, columna IMPORTE, filtros:
# AÑO=2025, TIPO=ENTRADA, CATEGORÍA<>"INTERCO")
gmv_2025 <- mov |>
filter(
year(fecha) == 2025,
tipo == "ENTRADA",
categoria != "INTERCO"
) |>
summarise(gmv = sum(importe, na.rm = TRUE)) |>
pull(gmv)
# Formateado a la europea para poder compararlo de un vistazo con la celda:
format(gmv_2025, big.mark = ".", decimal.mark = ",", nsmall = 2)
# [1] "1.234.567,89"Comparas con la celda equivalente del Excel. Si los dos dan el mismo número hasta el último céntimo, vas bien. Si difieren aunque sea en unos euros, busca el origen ANTES de continuar. Suele ser:
- Un NULL en el importe (
na.rmcambia el comportamiento). - Una categoría con espacios al final ("INTERCO " vs "INTERCO").
- Una fecha mal parseada (Excel a veces lee "01/03/2025" como "03/01/2025").
Ningún financiero va a confiar en tu migración si el primer número que le enseñas no cuadra.
Paso 4 — Generar TODO el informe en código
Una vez la primera métrica cuadra, el resto es mecánico. Cada pestaña del Excel original = una función en R que produce el mismo dataframe. Empaquetas en un script único:
source("R/funciones.R")
mov <- read_movimientos("data/ventas_2025.xlsx")
informe <- list(
gmv_mensual = calcular_gmv_mensual(mov),
por_canal = calcular_por_canal(mov),
pyl_resumen = calcular_pyl(mov),
conciliacion = calcular_conciliacion(mov),
alertas = detectar_anomalias(mov),
forecast = forecast_resto_ano(mov)
)
# Exporta a un único Excel pulido, una pestaña por concepto
library(openxlsx)
wb <- createWorkbook()
for (nombre in names(informe)) {
addWorksheet(wb, nombre)
writeData(wb, nombre, informe[[nombre]])
}
saveWorkbook(wb, "out/informe_semanal.xlsx", overwrite = TRUE)El financiero sigue recibiendo un Excel (porque eso es lo que sabe abrir), pero ese Excel se genera en 2 segundos desde R, no a mano en 3 horas.
Paso 5 — Programar y monitorizar
Si el informe es semanal, programa el script con cron en un servidor o GitHub Actions. Que se ejecute solo cada lunes a las 7 AM y deje el Excel en una carpeta compartida o lo envíe por email.
# crontab del servidor
0 7 * * 1 cd ~/datos && Rscript scripts/informe_semanal.R >> logs/informe.log 2>&1A partir de ese momento el equipo no abre el Excel maestro nunca más. El Excel maestro se mantiene como fuente única de datos (movimientos crudos), todo el cálculo vive en código.
#Los argumentos para convencer al financiero
El financiero no quiere "migrar a R". Quiere que el informe esté listo el lunes a las 8 AM sin tener que entrar él a tocar nada. Vende eso, no el lenguaje:
| Antes (Excel) | Después (R) |
|---|---|
| 8 min en abrir el Excel | 2 segundos en generar el informe |
| 3 horas humanas/semana para preparar el informe | 0 horas (cron) |
| Imposible auditar un número raro | Cada cálculo en una función con tests |
| Una celda mal y el cuadre se rompe | Validaciones automáticas paran el job |
| No se puede comparar entre meses fácilmente | Comparativas YoY/MoM en una línea |
| Solo el financiero sabe abrirlo | Cualquiera del equipo puede leer el script |
Cuando lo enseñas funcionando en su propia mesa, deja de haber discusión.
#Errores típicos al migrar
Fechas españolas vs americanas. Si read_excel lee "03/04/2025" como 2025-04-03 cuando era 2025-03-04, vas a tener un mes mal en todo el informe. Comprueba con head(mov$fecha).
Importes con miles y decimales europeos. Excel a veces los guarda como texto "1.234,56". as.numeric los convierte a NA silenciosamente. Usa readr::parse_number(x, locale = locale(decimal_mark = ",", grouping_mark = ".")).
Pestañas ocultas con cálculos clave. El financiero olvida mencionarlas. Abre el Excel con excel_sheets("...") y mira TODO lo que hay, no solo lo visible.
#Lo que NO migras
Algunas cosas siguen siendo mejor en Excel:
- Edición manual puntual (un ajuste de tesorería al cierre de mes).
- Tablas dinámicas exploratorias para gente no técnica.
- Output final visto por externos (gestoría, banco, auditor).
R produce el dato calculado. Excel sigue siendo la capa de presentación y edición manual cuando hace falta. Es complementario, no sustituto.
Extrae datos crudos del Excel a R. Reescribe UNA métrica clave y cuadra al céntimo contra el Excel. Cuando cuadra, replica el resto en funciones. Genera el output en Excel desde R y prográmalo. El financiero gana 3 horas a la semana y duerme mejor.
Si quieres que te ayude a destripar un Excel financiero concreto, escríbeme: hola@dumaloor.es. Lo más probable es que el dolor que tienes se resuelva en una semana de trabajo.
Preguntas frecuentes
¿Y si el Excel tiene macros de VBA que hacen parte del cálculo, no solo fórmulas de celda?
Las macros de VBA no las lee read_excel(), así que hay que abrir el editor de VBA a mano y traducir esa lógica a R aparte de las fórmulas de celda: no hay atajo automático. Suele ser buen momento para preguntarle a quien mantiene el Excel qué hace exactamente esa macro, porque a menudo es el paso más frágil y menos documentado de todo el archivo.
¿Cómo automatizo la comparación 'al céntimo' del paso 3 en vez de mirarla a ojo cada vez?
Con un test que compare la diferencia absoluta contra un umbral en céntimos: guarda el número esperado del Excel como constante y escribe testthat::expect_lt(abs(gmv - 1234567.89), 0.01) en un script que corra antes de cada cron. Evita aquí expect_equal() con el argumento tolerance, porque en las versiones actuales de testthat esa tolerancia es relativa y no absoluta: un tolerance = 0.01 daría por bueno un desvío de en torno al 1 %, que sobre una cifra de siete dígitos son miles de euros pasando el test. Con la comparación absoluta, si alguien cambia una fórmula o llega un dato que rompe el cuadre, el job falla explícitamente en vez de generar en silencio un informe con un número mal.
¿Qué cambia si el volumen de movimientos pasa de 50.000 filas a varios millones?
La lógica de dplyr no cambia, pero a partir de varios millones de filas compensa sustituir read_excel() por una exportación previa a CSV o Parquet y leerla con el paquete arrow o con duckdb, porque readxl es notablemente más lento que un lector columnar en archivos grandes. Los filter(), group_by() y summarise() se trasladan casi literales, porque los dos paquetes entienden esos mismos verbos y los traducen a su propio motor; lo que cambia es solo la capa de lectura.
Seguir leyendo
Análisis exploratorio en R: del CSV al insight en 50 líneas
Una pasada real por el flow que uso para destripar un CSV nuevo sin saber qué hay dentro: lectura, perfilado, limpieza, agrupación y primeras gráficas. Todo en un script de R que cabe en una pantalla.
Excel como base de datos: las cinco señales de que ya no vale
Empezó siendo una hoja para llevar el control y ahora la abre medio equipo. Estas son las señales de que se ha convertido en un riesgo.
Python vs R para análisis de datos: cuándo usar cada uno
Llevo años alternando entre R y Python para análisis. Esta es la decisión real, sin religión: qué uso para cada cosa y por qué. Sin defender a ninguno como ganador absoluto.