--- # PASO 4: TÍTULO DEL CUADRO DE MANDOS title: "Comparación de indicadores por país (fuente: OWID)" output: flexdashboard::flex_dashboard: orientation: columns runtime: shiny --- ```{r setup, include=FALSE} library(flexdashboard) # Estructura del cuadro de mando en columnas library(shiny) # Reactividad library(openxlsx) # Lectura de Excel library(plotly) # Gráficas dinámicas: ggplotly convierte los ggplot library(gt) # Tabla de los tests coloreada (aedv_tabla_lm) library(tidyverse) # Manipulación de datos y ggplot2 (se carga el último # para que dplyr::filter() no quede tapado por plotly) source("https://ctim.es/AEDV/utilidades.R") # Funciones auxiliares del curso ``` ```{r datos, include=FALSE} # ============================================================================== # PASO 1: LOS DATOS # # Se leen aquí, UNA SOLA VEZ al arrancar. Ha de ser una tabla en formato ANCHO # de "observaciones x atributos": una fila por observación (país, municipio, # provincia...) y una columna por indicador numérico. # ============================================================================== DATOS <- read.xlsx("https://ctim.es/AEDV/data/owid_country.xlsx", sheet = 1) |> as_tibble() # ============================================================================== # PASO 2: LA COLUMNA QUE IDENTIFICA CADA OBSERVACIÓN # # El nombre que se enseña al pasar el ratón por un punto y en la tabla de la # derecha. Es la ÚNICA columna que hay que nombrar: los indicadores del # desplegable son todas las columnas numéricas de DATOS, se deducen solas. # ============================================================================== COLUMNA_ETIQUETA <- "location" # ============================================================================== # PASO 3: OPCIONES DEL CUADRO DE MANDOS # ============================================================================== # Indicadores con los que arranca el cuadro de mandos. Si alguno no existe en # DATOS se usa el primero disponible, así que la plantilla no se rompe al # cambiar de datos y olvidarse de tocar estas dos líneas. X_INICIAL <- "life_expectancy" Y_INICIAL <- "gdp_per_capit" # Nivel de significación con el que se decide si cada test se supera ALFA <- 0.05 ``` ```{r preparacion, include=FALSE} # ============================================================================== # A PARTIR DE AQUÍ NO HAY QUE TOCAR NADA # ============================================================================== # LOS INDICADORES SON LAS COLUMNAS NUMÉRICAS de la tabla. Deducirlos así, y no # por su posición, es lo que hace que la plantilla se adapte sola: da igual # cuántas columnas de texto haya ni en qué orden estén. INDICADORES <- DATOS |> select(where(is.numeric)) |> names() # Valor inicial de un desplegable, con el primer indicador como red de seguridad inicial <- function(nombre) if (nombre %in% INDICADORES) nombre else INDICADORES[1] # Transformaciones ofrecidas para cada indicador. Se aplican conservando el # signo del dato, para que valgan también con valores negativos. # Yeo-Johnson es distinta y solo se ofrece en y: necesita un lambda, que se # calcula buscando el mejor ajuste lineal con la x ya transformada. TRANSFORMACIONES_X <- c("none", "sqrt", "log") TRANSFORMACIONES_Y <- c("none", "sqrt", "log", "YeoJohnson") transforma <- function(v, tipo) { switch(tipo, none = v, sqrt = sign(v) * sqrt(abs(v)), log = sign(v) * log(abs(v)), v ) } # Rótulo de un eje: el nombre del indicador y, entre paréntesis, lo que se le ha # hecho: la normalización previa (si está marcada) y su transformación rotulo <- function(indicador, tipo, normalizado = FALSE) { partes <- c(if (normalizado) "normalizado", if (tipo != "none") tipo) if (length(partes) == 0) indicador else paste0(indicador, " (", paste(partes, collapse = ", "), ")") } ``` Column {.sidebar data-width=320} -------------------------------------------------- ```{r} # NORMALIZACIÓN PREVIA. Aplica Yeo-Johnson a CADA variable POR SEPARADO, con el # lambda que más aproxima esa variable a una normal (aedv_yeojohnson_lambda). # Es un criterio DISTINTO del de la opción "YeoJohnson" del desplegable de y, # que busca el lambda del mejor ajuste lineal ENTRE LAS DOS variables. checkboxInput("normalizar", label = "Normalizar antes cada variable (Yeo-Johnson)", value = FALSE) # Los desplegables se construyen con INDICADORES, deducido de los datos selectInput("x", label = "x: indicador 1", choices = INDICADORES, selected = inicial(X_INICIAL)) selectInput("x_scale", label = "Transformación del indicador x", choices = TRANSFORMACIONES_X, selected = "none") selectInput("y", label = "y: indicador 2", choices = INDICADORES, selected = inicial(Y_INICIAL)) selectInput("y_scale", label = "Transformación del indicador y", choices = TRANSFORMACIONES_Y, selected = "none") # Lanza el cálculo: hasta pulsarlo no se calcula nada, ni al arrancar ni al # cambiar los controles actionButton("calcular", "Calcular") ``` ```{r} # CÁLCULO CENTRAL, del que beben los tres paneles: selecciona los dos # indicadores, les aplica la transformación elegida, ajusta la recta de # regresión y evalúa el ajuste. Devuelve una lista con nombres (no una lista # posicional) para que cada panel tome solo lo que necesita. # # Es un eventReactive(): solo se recalcula al pulsar el botón «Calcular», no # cada vez que se toca un control. Así se pueden cambiar varios controles # seguidos y hacer el cálculo una sola vez. Al arrancar NO se calcula nada, # para que el cuadro de mandos cargue antes: los paneles quedan vacíos hasta # el primer clic. calculo <- eventReactive(input$calcular, { tb <- DATOS |> select(etiqueta = all_of(COLUMNA_ETIQUETA), x = all_of(input$x), y = all_of(input$y)) |> na.omit() # PASO PREVIO OPCIONAL: normalizar cada variable por separado. Aquí solo # interviene la propia variable —no la otra—, y el lambda se busca para que # los datos transformados se parezcan lo más posible a una normal. lambda_norm_x <- NA_real_ lambda_norm_y <- NA_real_ if (isTRUE(input$normalizar)) { lambda_norm_x <- aedv_yeojohnson_lambda(tb$x) lambda_norm_y <- aedv_yeojohnson_lambda(tb$y) tb$x <- aedv_yeo_johnson(tb$x, lambda_norm_x) tb$y <- aedv_yeo_johnson(tb$y, lambda_norm_y) } tb$x <- transforma(tb$x, input$x_scale) # Yeo-Johnson calcula su lambda; el resto de transformaciones son directas lambda <- NA_real_ if (input$y_scale == "YeoJohnson") { lambda <- aedv_lambda_optimo(tb$x, tb$y) tb$y <- aedv_yeo_johnson(tb$y, lambda) } else { tb$y <- transforma(tb$y, input$y_scale) } # El logaritmo de un cero deja valores no finitos, que romperían la regresión tb <- tb |> filter(is.finite(x), is.finite(y)) req(nrow(tb) >= 3) # con menos de 3 observaciones no hay nada que ajustar fit <- lm(y ~ x, data = tb) tb$residuo <- residuals(fit) # aedv_lm_analisis() ajusta esta misma recta y devuelve, en una fila, los # indicadores de calidad del ajuste y los p-valores de los tres tests. # Su test de Shapiro-Wilk admite como mucho 5.000 observaciones, así que con # tablas mayores se avisa en vez de dejar caer el cuadro de mandos. valores <- if (nrow(tb) <= 5000) aedv_lm_analisis(tb$x, tb$y) else NULL list(tb = tb, fit = fit, lambda = lambda, valores = valores, lambda_norm_x = lambda_norm_x, lambda_norm_y = lambda_norm_y) }) ``` ### ```{r} # Resumen del análisis en HTML. La recta, R2, sigma y los tests van en la tabla # de la pestaña del error (aedv_tabla_lm); aquí queda lo que no está en ella. renderUI({ resultado <- calculo() tb <- resultado$tb texto <- paste( "ANÁLISIS DE REGRESIÓN LINEAL
", "x:", rotulo(input$x, input$x_scale, input$normalizar), "
", "y:", rotulo(input$y, input$y_scale, input$normalizar), "
", "observaciones:", nrow(tb), "
correlación:", round(cor(tb$x, tb$y), digits = 4) ) # Los lambda de la normalización previa: uno por variable, cada uno calculado # con la suya y sin mirar a la otra if (isTRUE(input$normalizar)) { texto <- paste(texto, "
lambda normalidad (x):", formatC(resultado$lambda_norm_x, format = "e", digits = 3), "
lambda normalidad (y):", formatC(resultado$lambda_norm_y, format = "e", digits = 3)) } # El lambda del mejor ajuste lineal, que es otra cosa: se calcula con las dos # variables a la vez y solo existe si se ha elegido YeoJohnson en el eje y if (input$y_scale == "YeoJohnson") { texto <- paste(texto, "
lambda ajuste lineal (y):", formatC(resultado$lambda, format = "e", digits = 3)) } HTML(texto) }) ``` Column {.tabset} -------------------------------------------------- ### Diagrama de dispersión ```{r} # Diagrama de dispersión con la recta de regresión renderPlotly({ resultado <- calculo() p <- resultado$tb |> ggplot(aes(x, y, label = etiqueta)) + geom_point() + geom_smooth(method = lm, se = FALSE, formula = y ~ x) + # recta de regresión labs(x = rotulo(input$x, input$x_scale, input$normalizar), y = rotulo(input$y, input$y_scale, input$normalizar)) ggplotly(p) }) ``` ### Error del ajuste y tests ```{r} # Residuos (error del ajuste) frente a x. Es la lectura gráfica de los tests de # Shapiro-Wilk y Breusch-Pagan de la tabla de debajo: una nube sin forma y de # anchura constante alrededor del cero es lo que se espera de un buen ajuste; # un embudo o una curva delatan lo que esos tests detectan. output$grafico_error <- renderPlotly({ resultado <- calculo() p <- resultado$tb |> ggplot(aes(x, residuo, label = etiqueta)) + geom_hline(yintercept = 0, linetype = "dashed", colour = "steelblue") + geom_point(alpha = 0.6) + labs(x = rotulo(input$x, input$x_scale, input$normalizar), y = "residuo") ggplotly(p) }) # La recta, R2, sigma y los tres tests de aedv_lm_analisis(), con el p-valor en # verde si el test se supera y en rojo si no. aedv_tabla_lm() devuelve una tabla # gt, que se muestra con renderUI. output$tabla_lm <- renderUI({ resultado <- calculo() validate(need(!is.null(resultado$valores), "Tests no disponibles: el test de Shapiro-Wilk admite como mucho 5.000 observaciones.")) aedv_tabla_lm(resultado$valores, titulo = "Análisis del ajuste de la recta", alpha = ALFA) }) # Dos salidas en la misma pestaña: la gráfica arriba, ocupando el alto que quede # libre, y la tabla debajo con su altura natural (flex = NA). fillCol() es lo # que reparte el alto de la pestaña entre las dos. fillCol(flex = c(1, NA), plotlyOutput("grafico_error", height = "100%"), uiOutput("tabla_lm")) ``` Column {data-width=300} -------------------------------------------------- ### Valor de los indicadores transformados ```{r} # Tabla de valores, ordenada de mayor a menor por el primer indicador renderTable({ tabla <- calculo()$tb |> arrange(desc(x)) |> transmute( etiqueta, x = prettyNum(round(x, digits = 2), big.mark = ","), y = prettyNum(round(y, digits = 2), big.mark = ",") ) # Las cabeceras toman el nombre real de las columnas elegidas names(tabla) <- c(COLUMNA_ETIQUETA, input$x, input$y) tabla }) ```