Producto Academico - Herramientas Informáticas I

Prof.: Joel Turco Quinto

Tema: Dashboard Gapminder - América (PIBpc)

Grupo 03 Integrantes:

Introducción

El dashboard elaborado es una aplicación interactiva desarrollada bajo los paquetes de R: Shiny y la interfaz estandarizada de shinydashboard. Su propósito fundamental es guiar al usuario desde una vista animada de los datos, para conocer la evolución de los datos, hasta presentar resultados econométricos relacionados a la relación entre la Esperanza de Vida y el Crecimiento Económico.

Librerías

library(shiny)
library(shinydashboard)
library(ggplot2)
library(gapminder)
library(dplyr)
library(plotly)
library(DT)
library(maps)
library(viridis)
library(stringr)
# 2. PREPARACIÓN DE DATOS  ---------------------------------
DATA_PAC <- gapminder::gapminder %>%
  filter(continent == "Americas", year >= 1970) %>%
  mutate(
    country = as.character(country),
    continent = as.character(continent)
  ) %>% 
  rename(
    País = country,
    Continente = continent,
    Año = year,
    Esperanza_de_vida = lifeExp,
    Población = pop,
    PIBpc = gdpPercap
  ) %>%
  mutate(
    Continente = "América",
    Subregion = case_when(
      País %in% c("United States", "Canada") ~ "América del Norte",
      País %in% c("Mexico", "Guatemala", "Honduras", "Nicaragua", "El Salvador", 
                  "Costa Rica", "Panama") ~ "Centroamérica",
      País %in% c("Brazil", "Argentina", "Chile", "Uruguay", "Paraguay", "Bolivia",
                  "Peru", "Colombia", "Venezuela", "Ecuador") ~ "América del Sur",
      País %in% c("Cuba", "Dominican Republic", "Haiti", "Jamaica", "Trinidad and Tobago","Puerto Rico") ~ "El Caribe",
      TRUE ~ "Otros"
    ),
    iso_code = case_when(
      País == "Argentina" ~ "ARG", País == "Bolivia" ~ "BOL", País == "Brazil" ~ "BRA",
      País == "Canada" ~ "CAN", País == "Chile" ~ "CHL", País == "Colombia" ~ "COL",
      País == "Costa Rica" ~ "CRI", País == "Cuba" ~ "CUB", País == "Dominican Republic" ~ "DOM",
      País == "Ecuador" ~ "ECU", País == "El Salvador" ~ "SLV", País == "Guatemala" ~ "GTM",
      País == "Haiti" ~ "HTI", País == "Honduras" ~ "HND", País == "Jamaica" ~ "JAM",
      País == "Mexico" ~ "MEX", País == "Nicaragua" ~ "NIC", País == "Panama" ~ "PAN",
      País == "Paraguay" ~ "PRY", País == "Peru" ~ "PER", País == "Trinidad and Tobago" ~ "TTO",
      País == "United States" ~ "USA", País == "Uruguay" ~ "URY", País == "Venezuela" ~ "VEN",País == "Puerto Rico" ~ "PR",
      TRUE ~ NA_character_
    )
  )

# Paleta de colores para diferenciar regiones
colores_sobres <- c(
  "América del Norte" = "#00D2FF", 
  "Centroamérica"     = "#00E676", 
  "América del Sur"   = "#7C4DFF", 
  "El Caribe"         = "#FF5252", 
  "Otros"             = "#FF9100"  
)

# 3. INTERFAZ DE USUARIO (UI) --------------------------------------------------
ui <- dashboardPage(
  dashboardHeader(
    title = tags$div(
      tags$strong("Dashboard Gapminder-América", style = "color: #FFFFFF; font-size: 16px; letter-spacing: 0.5px;"),
      tags$small("GRUPO 03", style = "color: #A0B2C6; font-size: 11px;")
    ), 
    titleWidth = 350
  ),
  
  dashboardSidebar(
    width = 280,
    sidebarMenu(
      menuItem("Desarrollo y Salud", tabName = "desarrollo", icon = icon("heartbeat")),
      menuItem("Ranking y Competitividad", tabName = "ranking", icon = icon("trophy")),
      menuItem("Mapa e Histórico", tabName = "mapa", icon = icon("map")),
      menuItem("Estadísticas Descriptivas", tabName = "estadisticas", icon = icon("calculator")),
      menuItem("Modelo de Regresión", tabName = "regresion", icon = icon("chart-line")) 
    ),
    
    tags$div(
      style = "padding: 18px; color: #ECEFF1;",
      tags$hr(style = "border-color: #37474F; margin-top: 5px; margin-bottom: 15px;"),
      
      tags$label("Filtrar por Subregión:", style = "font-weight: 600; color: #B0BEC5; font-size: 12px;"),
      selectInput("subregion", NULL, choices = c("Todas", sort(unique(DATA_PAC$Subregion))), selected = "Todas"),
      
      tags$label("Filtrar por País:", style = "font-weight: 600; color: #B0BEC5; font-size: 12px;"),
      uiOutput("selector_pais_ui"),
      
      tags$label("Año de Análisis:", style = "font-weight: 600; color: #B0BEC5; font-size: 12px;"),
      sliderInput("anio", NULL, 
                  min = min(DATA_PAC$Año), max = max(DATA_PAC$Año), 
                  value = 2007, step = 5, sep = "",
                  animate = animationOptions(interval = 1800, loop = FALSE)),
      
      tags$label("Países en Ranking:", style = "font-weight: 600; color: #B0BEC5; font-size: 12px;"),
      numericInput("top_n", NULL, value = 10, min = 1, max = 25)
    )
  ),
  
  dashboardBody(
    tags$head(tags$style(HTML('
      .skin-blue .main-header .logo { background-color: #0A1424; font-family: "Segoe UI", sans-serif; }
      .skin-blue .main-header .navbar { background-color: #0F1E36; }
      .skin-blue .main-sidebar { background-color: #16222F; font-family: "Segoe UI", sans-serif; }
      .skin-blue .main-sidebar .sidebar .sidebar-menu .active a { border-left-color: #00D2FF; background-color: #0F1E36; }
      .content-wrapper { background-color: #F8F9FA; }
      .box { border-top: 3px solid #0F1E36 !important; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); margin-bottom: 20px; }
      .box.box-solid.box-primary { border: 1px solid #0F1E36; }
      .box.box-solid.box-primary>.box-header { background: #0F1E36; color: #fff; }
      .box.box-solid.box-warning>.box-header { background: #FF9100; color: #fff; }
      .small-box { border-radius: 8px; box-shadow: 0 4px 10px rgba(0,0,0,0.08); transition: transform .15s ease-in-out; }
      .small-box:hover { transform: translateY(-2px); }
    '))),
    
    fluidRow(
      valueBoxOutput("vbox_vida", width = 4),
      valueBoxOutput("vbox_pib", width = 4),
      valueBoxOutput("vbox_pop", width = 4)
    ),
    
    tabItems(
      # PESTAÑA 1: DESARROLLO Y SALUD
      tabItem(tabName = "desarrollo",
              fluidRow(
                box(title = "Evolución del PIB per Cápita vs Esperanza de Vida (Animación)", plotlyOutput("plot_animacion", height = "500px"), width = 12, status = "primary", solidHeader = TRUE)
              ),
              fluidRow(
                box(title = "Datos Analizados", DTOutput("tabla_resumen"), width = 12, status = "primary")
              )
      ),
      
      # PESTAÑA 2: RANKING
      tabItem(tabName = "ranking",
              fluidRow(
                box(title = "Top Países por PIB per Cápita (USD)", plotlyOutput("plot_ranking_gdp", height = "450px"), width = 6, status = "primary"),
                box(title = "Dispersión entre PIBpc vs Salud", plotlyOutput("plot_matriz", height = "450px"), width = 6, status = "primary")
              ),
              fluidRow(
                box(title = "Distribución de la Esperanza de Vida", plotlyOutput("plot_boxplot_vida", height = "400px"), width = 12, status = "primary")
              )
      ),
      
      # PESTAÑA 3: MAPA E HISTÓRICO
      tabItem(tabName = "mapa",
              fluidRow(
                box(title = "Distribución Geográfica de la Esperanza de Vida", plotlyOutput("mapa_plotly", height = "500px"), width = 12, status = "primary", solidHeader = TRUE)
              ),
              fluidRow(
                box(title = "Evolución Histórica del PIB per Cápita Promedio (USD)", plotlyOutput("plot_tendencias", height = "400px"), width = 12, status = "primary")
              )
      ),
      
      # PESTAÑA 4: ESTADÍSTICAS DESCRIPTIVAS
      tabItem(tabName = "estadisticas",
              fluidRow(
                box(title = "Configuración del Análisis Descriptivo", status = "warning", solidHeader = TRUE, width = 12,
                    p("Los cálculos de las tablas se procesan automáticamente sobre la serie histórica completa."),
                    selectInput("var_analisis", "Variable a analizar:",
                                choices = c("PIB Per Cápita (USD)" = "PIBpc", "Esperanza de Vida (Años)" = "Esperanza_de_vida", "Población Total" = "Población"), selected = "PIBpc")),
                box(title = "Métricas Agrupadas por Subregión", status = "primary", solidHeader = TRUE, width = 12, DTOutput("tabla_stats_subregion")),
                box(title = "Métricas Históricas Detalladas por País", status = "primary", solidHeader = TRUE, width = 12, DTOutput("tabla_stats_pais"))
              ),
              fluidRow(
                box(title = "Matriz de Correlación de Pearson", status = "primary", solidHeader = TRUE, width = 12,
                    p(tags$em("Nota: Muestra el nivel de asociación lineal entre las variables numéricas segun los datos filtrados.")),
                    plotlyOutput("plot_correlacion", height = "450px"))
              )
      ),
      
      # PESTAÑA 5: MODELO DE REGRESIÓN LOG-LOG
      tabItem(tabName = "regresion",
              fluidRow(
                valueBoxOutput("vbox_beta", width = 6),
                valueBoxOutput("vbox_r2", width = 6)
              ),
              fluidRow(
                box(
                  title = "Modelo Log-Log Estimado: log(Esperanza de Vida) ~ log(PIB per Cápita)",
                  status = "primary", solidHeader = TRUE, width = 12,
                  p(tags$em("Nota: Ambas variables se encuentran transformadas logarítmicamente. La línea roja representa el ajuste lineal, interpretándose como la elasticidad (Variación Porcentual).")),
                  plotlyOutput("plot_regresion_log", height = "500px")
                )
              )
      )
    )
  )
)

# 4. SERVIDOR (SERVER) ---------------------------------------------------------
server <- function(input, output, session) {
  
  output$selector_pais_ui <- renderUI({
    paises <- DATA_PAC
    if(input$subregion != "Todas") paises <- paises %>% filter(Subregion == input$subregion)
    selectInput("pais", NULL, choices = c("Todos", sort(unique(paises$País))), selected = "Todos")
  })
  
  datos_filtrados <- reactive({
    res <- DATA_PAC %>% filter(Año == input$anio)
    if(input$subregion != "Todas") res <- res %>% filter(Subregion == input$subregion)
    if(!is.null(input$pais) && input$pais != "Todos") res <- res %>% filter(País == input$pais)
    return(res)
  })
  
  datos_historicos_analisis <- reactive({
    res <- DATA_PAC
    if(input$subregion != "Todas") res <- res %>% filter(Subregion == input$subregion)
    if(!is.null(input$pais) && input$pais != "Todos") res <- res %>% filter(País == input$pais)
    return(res)
  })
  
  datos_regresion <- reactive({
    if(!is.null(input$pais) && input$pais != "Todos") {
      res <- DATA_PAC %>% filter(País == input$pais)
    } else {
      res <- DATA_PAC %>% filter(Año == input$anio)
      if(input$subregion != "Todas") res <- res %>% filter(Subregion == input$subregion)
    }
    res <- res %>% filter(!is.na(PIBpc), PIBpc > 0, 
                          !is.na(Esperanza_de_vida), Esperanza_de_vida > 0)
    return(res)
  })
  
  output$vbox_vida <- renderValueBox({
    df <- datos_filtrados()
    val <- if(nrow(df) > 0) round(mean(df$Esperanza_de_vida, na.rm=TRUE), 0) else 0
    valueBox(paste(val, "años"), "Esperanza de Vida Promedio", icon = icon("heart"), color = "teal")
  })
  
  output$vbox_pib <- renderValueBox({
    df <- datos_filtrados()
    val <- if(nrow(df) > 0) mean(df$PIBpc, na.rm=TRUE) else 0
    valueBox(paste("$", scales::comma(round(val, 0))), "PIB per Cápita Promedio", icon = icon("money-bill-wave"), color = "light-blue")
  })
  
  output$vbox_pop <- renderValueBox({
    df <- datos_filtrados()
    val <- if(nrow(df) > 0) sum(as.numeric(df$Población), na.rm=TRUE) else 0
    valueBox(
      scales::number(val / 1e6, accuracy = 0.1, suffix = "Mill.", big.mark = ",", decimal.mark = "."), 
      "Población Total", 
      icon = icon("users"), 
      color = "blue"
    )
  })
  
  output$plot_animacion <- renderPlotly({
    df_anim <- DATA_PAC
    if(input$subregion != "Todas") df_anim <- df_anim %>% filter(Subregion == input$subregion)
    if(!is.null(input$pais) && input$pais != "Todos") df_anim <- df_anim %>% filter(País == input$pais)
    p <- ggplot(df_anim, aes(x = PIBpc, y = Esperanza_de_vida, color = Subregion)) +
      geom_point(aes(size = Población, frame = Año, ids = País, text = paste("País:", País, "<br>PIBpc: $", scales::comma(round(PIBpc, 0)), "USD", "<br>Esp. Vida:", round(Esperanza_de_vida, 1), "años")), alpha = 0.75) +
      scale_x_log10(labels = scales::label_number(prefix = "$", big.mark = ",")) + scale_color_manual(values = colores_sobres) + scale_size_continuous(range = c(4, 18), guide = "none") +
      labs(x = "PIB per Cápita (Escala en USD)", y = "Esperanza de Vida (Años)", color = "Subregión") + 
      theme_minimal() + theme(panel.grid.minor = element_blank())
    ggplotly(p, tooltip = "text") %>% animation_opts(frame = 900, transition = 300, redraw = TRUE)
  })
  
  output$plot_ranking_gdp <- renderPlotly({
    df <- datos_filtrados()
    validate(need(nrow(df) > 0, "Sin datos disponibles"))
    top_df <- df %>% arrange(desc(PIBpc)) %>% head(input$top_n)
    p <- ggplot(top_df, aes(x = reorder(País, PIBpc), y = PIBpc, fill = Subregion, text = paste("País:", País, "<br>PIBpc: $", scales::comma(round(PIBpc, 0)), "USD"))) +
      geom_col(alpha = 0.85, width = 0.7) + coord_flip() + scale_fill_manual(values = colores_sobres) + scale_y_continuous(labels = scales::label_number(prefix = "$", big.mark = ",")) +
      labs(x = NULL, y = "PIB per Cápita (USD)") + theme_minimal() + theme(legend.position = "none")
    ggplotly(p, tooltip = "text")
  })
  
  output$plot_matriz <- renderPlotly({
    df <- datos_filtrados()
    validate(need(nrow(df) > 0, "Sin datos disponibles"))
    pib_m <- mean(df$PIBpc, na.rm = TRUE)
    vida_m <- mean(df$Esperanza_de_vida, na.rm = TRUE)
    df <- df %>% mutate(Cuadrante = case_when(PIBpc >= pib_m & Esperanza_de_vida >= vida_m ~ "Alto PBIpc / Alto EV", PIBpc < pib_m & Esperanza_de_vida >= vida_m ~ "Bajo PIBpc / Alta EV", PIBpc >= pib_m & Esperanza_de_vida < vida_m ~ "Alto PIBpc / Baja EV", TRUE ~ "Bajo PBIpc / Bajo EV"))
    
    paleta_cuadrantes <- c("Alto PBIpc / Alto EV" = "#00E676", "Bajo PIBpc / Alta EV" = "#00D2FF", "Alto PIBpc / Baja EV" = "#FF9100", "Bajo PBIpc / Bajo EV" = "#FF5252")
    
    p <- ggplot(df, aes(x = PIBpc, y = Esperanza_de_vida, color = Cuadrante, label = País)) +
      geom_point(size = 3.5, alpha = 0.8, aes(text = paste("País:", País, "<br>PIBpc: $", scales::comma(round(PIBpc, 0)), "USD", "<br>Esp. Vida:", round(Esperanza_de_vida, 1), "años"))) +
      geom_vline(xintercept = pib_m, linetype = "dashed", color = "#B0BEC5") + geom_hline(yintercept = vida_m, linetype = "dashed", color = "#B0BEC5") + scale_x_log10(labels = scales::label_number(prefix = "$", big.mark = ",")) + 
      scale_color_manual(values = paleta_cuadrantes) +
      labs(x = "PIB per Cápita (Escala USD)", y = "Esperanza de Vida (Años)", color = "Cuadrante") + theme_minimal()
    ggplotly(p, tooltip = "text")
  })
  
  output$plot_boxplot_vida <- renderPlotly({
    df <- datos_filtrados()
    validate(need(nrow(df) > 0, "Sin datos"))
    p <- ggplot(df, aes(x = Subregion, y = Esperanza_de_vida, fill = Subregion)) + 
      geom_boxplot(outlier.shape = NA, alpha = 0.5, color = "#37474F") + 
      geom_jitter(width = 0.15, alpha = 0.7, aes(label = País, color = Subregion), size = 2) + 
      scale_fill_manual(values = colores_sobres) + scale_color_manual(values = colores_sobres) +
      labs(x = NULL, y = "Años de Vida") + theme_minimal() + theme(legend.position = "none")
    ggplotly(p)
  })
  
  output$mapa_plotly <- renderPlotly({
    df <- datos_filtrados()
    validate(need(nrow(df) > 0, "Sin datos para el mapa"))
    plot_geo(df) %>% 
      add_trace(z = ~Esperanza_de_vida, color = ~Esperanza_de_vida, colors = "Purples", 
                text = ~paste(País, "<br>Esp. Vida:", round(Esperanza_de_vida, 1), "años"), 
                locations = ~iso_code, marker = list(line = list(color = toRGB("white"), width = 0.5))) %>% 
      layout(geo = list(scope = 'americas', projection = list(type = 'natural earth'), bgcolor = 'rgba(0,0,0,0)'))
  })
  
  output$plot_tendencias <- renderPlotly({
    df_tend <- DATA_PAC
    if(input$subregion != "Todas") df_tend <- df_tend %>% filter(Subregion == input$subregion)
    if(!is.null(input$pais) && input$pais != "Todos") {
      df_linea <- df_tend %>% filter(País == input$pais)
      p <- ggplot(df_linea, aes(x = Año, y = PIBpc, group = País, text = paste("País:", País, "<br>Año:", Año, "<br>PIBpc: $", scales::comma(round(PIBpc, 0)), "USD"))) +
        geom_line(linewidth = 1.2, color = "#7C4DFF") + geom_point(size = 2.5, color = "#0F1E36") + scale_y_continuous(labels = scales::label_number(prefix = "$", big.mark = ",")) + labs(title = paste("Trayectoria Histórica:", input$pais), x = "Año", y = "PIB per Cápita (USD)") + theme_minimal()
    } else {
      df_linea <- df_tend %>% group_by(Subregion, Año) %>% summarise(PIBpc_prom = mean(PIBpc, na.rm=TRUE), .groups = "drop")
      p <- ggplot(df_linea, aes(x = Año, y = PIBpc_prom, color = Subregion, text = paste("Subregión:", Subregion, "<br>Año:", Año, "<br>PIBpc Promedio: $", scales::comma(round(PIBpc_prom, 0)), "USD"))) +
        geom_line(linewidth = 1.2) + geom_point(size = 2) + scale_color_manual(values = colores_sobres) + scale_y_continuous(labels = scales::label_number(prefix = "$", big.mark = ",")) + labs(x = "Año", y = "PIB per Cápita Promedio (USD)", color = "Subregión") + theme_minimal() + theme(legend.position = "bottom")
    }
    ggplotly(p, tooltip = "text")
  })
  
  output$tabla_resumen <- renderDT({
    datos_filtrados() %>% select(País, Subregion, PIBpc, Esperanza_de_vida, Población) %>% datatable(extensions = 'Buttons', options = list(pageLength = 5, dom = 'Bfrtip', buttons = c('csv', 'excel'), scrollX = TRUE), rownames = FALSE, colnames = c("País", "Subregión", "PIB per Cápita (USD)", "Esperanza de Vida (Años)", "Población")) %>% formatCurrency("PIBpc", currency = "$", digits = 0) %>% formatRound("Esperanza_de_vida", digits = 1) %>% formatRound("Población", digits = 0)
  })
  
  output$tabla_stats_subregion <- renderDT({
    df_hist <- datos_historicos_analisis()
    v_target <- input$var_analisis
    df_stats <- df_hist %>% group_by(Subregion) %>% summarise(Promedio = mean(.data[[v_target]], na.rm = TRUE), Minimo = min(.data[[v_target]], na.rm = TRUE), Q25 = quantile(.data[[v_target]], 0.25, na.rm = TRUE), Q50_Med = median(.data[[v_target]], na.rm = TRUE), Q75 = quantile(.data[[v_target]], 0.75, na.rm = TRUE), Q100_Max = max(.data[[v_target]], na.rm = TRUE), Desv_Std = sd(.data[[v_target]], na.rm = TRUE), .groups = "drop") %>% mutate(Coef_Variacion = (Desv_Std / Promedio) * 100) %>% select(-Desv_Std)
    datatable(df_stats, extensions = 'Buttons', options = list(dom = 'Bfrtip', buttons = c('csv', 'excel'), scrollX = TRUE, pageLength = 5), rownames = FALSE, colnames = c("Subregión", "Promedio", "Mínimo", "Cuartil 25%", "Cuartil 50% (Mediana)", "Cuartil 75%", "Máximo (Q100)", "Coef. Variación (%)")) %>% formatRound(columns = 2:8, digits = 2)
  })
  
  output$tabla_stats_pais <- renderDT({
    df_hist <- datos_historicos_analisis()
    v_target <- input$var_analisis
    df_stats_pais <- df_hist %>% group_by(País, Subregion) %>% summarise(Promedio = mean(.data[[v_target]], na.rm = TRUE), Minimo = min(.data[[v_target]], na.rm = TRUE), Q25 = quantile(.data[[v_target]], 0.25, na.rm = TRUE), Q50_Med = median(.data[[v_target]], na.rm = TRUE), Q75 = quantile(.data[[v_target]], 0.75, na.rm = TRUE), Q100_Max = max(.data[[v_target]], na.rm = TRUE), Desv_Std = sd(.data[[v_target]], na.rm = TRUE), .groups = "drop") %>% mutate(Coef_Variacion = (Desv_Std / Promedio) * 100) %>% select(-Desv_Std)
    datatable(df_stats_pais, extensions = 'Buttons', options = list(dom = 'Bfrtip', buttons = c('csv', 'excel'), scrollX = TRUE, pageLength = 10), 
              rownames = FALSE, colnames = c("País", "Subregión", "Promedio", "Mínimo", "Cuartil 25%", "Cuartil 50% (Mediana)", "Cuartil 75%", "Máximo (Q100)", "Coef. Variación (%)")) %>% formatRound(columns = 3:9, digits = 2)
  })
  
  output$plot_correlacion <- renderPlotly({
    df <- datos_historicos_analisis()
    vars_corr <- df %>% 
      select(
        `PIB per Cápita` = PIBpc, 
        `Esp. Vida`      = Esperanza_de_vida, 
        `Población`      = Población
      )
    validate(need(nrow(vars_corr) >= 5, "Se requieren al menos 5 observaciones históricas acumuladas."))
    
    matriz_cor <- cor(vars_corr, use = "complete.obs", method = "pearson")
    df_cor <- as.data.frame(as.table(matriz_cor))
    names(df_cor) <- c("Variable_X", "Variable_Y", "Coeficiente")
    
    p <- ggplot(df_cor, aes(x = Variable_X, y = Variable_Y, fill = Coeficiente,
                            text = paste("Variables:", Variable_X, "vs", Variable_Y,
                                         "<br>Correlación (r):", round(Coeficiente, 3)))) +
      geom_tile(color = "white", lwd = 0.4) +
      scale_fill_gradient2(low = "#FF5252", high = "#7C4DFF", mid = "#FFFFFF", 
                           midpoint = 0, limit = c(-1, 1), name = "r") +
      geom_text(aes(label = round(Coeficiente, 2)), color = "#263238", size = 4) +
      theme_minimal() + labs(x = NULL, y = NULL) +
      theme(
        axis.text.x = element_text(angle = 25, vjust = 1, hjust = 1),
        panel.grid.major = element_blank(), panel.grid.minor = element_blank()
      )
    ggplotly(p, tooltip = "text")
  })
  
  output$vbox_beta <- renderValueBox({
    df <- datos_regresion()
    validate(need(nrow(df) >= 3, "Insuficientes puntos de datos."))
    modelo <- lm(log(Esperanza_de_vida) ~ log(PIBpc), data = df)
    beta_val <- coef(modelo)[2]
    valueBox(round(beta_val, 4), "Elasticidad β (variación porcentual)", icon = icon("calculator"), color = "purple")
  })
  
  output$vbox_r2 <- renderValueBox({
    df <- datos_regresion()
    validate(need(nrow(df) >= 3, ""))
    modelo <- lm(log(Esperanza_de_vida) ~ log(PIBpc), data = df)
    r2_val <- summary(modelo)$r.squared
    valueBox(paste0(round(r2_val * 100, 1), "%"), "Bondad de Ajuste R² Explicado", icon = icon("percentage"), color = "teal")
  })
  
  output$plot_regresion_log <- renderPlotly({
    df <- datos_regresion()
    validate(need(nrow(df) >= 3, "Se requieren por lo menos 3 observaciones válidas."))
    
    p <- ggplot(df, aes(x = PIBpc, y = Esperanza_de_vida)) +
      geom_point(aes(color = Subregion, size = Población,
                     text = paste("País:", País, "<br>Año:", Año, "<br>PIBpc: $", scales::comma(round(PIBpc, 0)), "USD", "<br>Esp. Vida:", round(Esperanza_de_vida, 1), "años")), alpha = 0.75) +
      stat_smooth(method = "lm", formula = y ~ x, color = "#FF5252", lwd = 1, se = TRUE, fill = "#FFCDD2") +
      scale_x_log10(labels = scales::label_number(prefix = "$", big.mark = ",")) +
      scale_y_log10(labels = scales::label_number(suffix = " años", accuracy = 1)) +
      scale_color_manual(values = colores_sobres) + scale_size_continuous(range = c(3, 14), guide = "none") +
      labs(x = "PIB per Cápita (Escala en USD)", y = "Esperanza de Vida (Escala en Años)", color = "Subregión") +
      theme_minimal() + theme(panel.grid.minor = element_blank())
    
    ggplotly(p, tooltip = "text")
  })
}

# 5. EJECUCIÓN DE LA APLICACIÓN ------------------------------------------------
shinyApp(ui, server)
Shiny applications not supported in static R Markdown documents

Conclusiones

Aplicación Shiny

El proyecto también cuenta con una aplicación interactiva desarrollada en Shiny para explorar dinámicamente los indicadores económicos y sociales.