Análisis de Consumos Eléctricos - App Shiny

Authors

Andrés Guiu

Profesores: Mag. Marcos Prunelloe

Profesores: Mag. Diego Marfetán Molina

Universidad Austral

Introducción

En este documento se presenta el código completo y comentado de una aplicación Shiny para analizar consumos eléctricos de clientes de la EPESF.
La app incluye:

  • Un tablero con indicadores y un gráfico de los 5 mayores consumidores.
  • Un mapa interactivo con georreferenciación y capa de heatmap.
  • Una tabla interactiva con los datos filtrados.
  • Un encabezado con un pequeño menú de mensajes y un enlace a la página de la EPE.

El objetivo es que el código sea fácil de leer y reutilizar como base para trabajos prácticos o futuros desarrollos.

Estructura general de la app

La app sigue la estructura clásica de Shiny:

  1. Carga de librerías y datos.
  2. Definición de la interfaz de usuario (UI).
  3. Definición de la lógica del servidor (server).
  4. Llamado a shinyApp(ui, server) para ejecutar la aplicación.

A continuación se muestra el código completo, con comentarios en cada bloque.

Código completo de la app

# =========================
#  LIBRERÍAS
# =========================
library(shiny)           # Crea apps web
library(bslib)           # Temas modernos y layout responsive
library(bsicons)         # Íconos Bootstrap
library(reactable)       # Tablas interactivas
library(tidyverse)       # dplyr, ggplot2, etc.
library(leaflet)         # Mapas interactivos
library(leaflet.extras)  # Heatmaps y capas extra

# =========================
#  DATOS
# =========================
dfConsumos <- read.csv(                      # Lee el archivo CSV
  file        = "datos/consuEpeAnonGeo.csv", # Ruta del archivo
  sep         = ";",                         # Separador ;
  fileEncoding = "ISO-8859-1"                # Codificación de caracteres
)

Agencia <- dfConsumos %>%                    # Crea el vector de agencias
  pull(Agencia) %>%                          # Extrae la columna Agencia
  unique() %>%                               # Elimina repetidos
  sort()                                     # Orden alfabético

# =========================
#  UI
# =========================
ui <- bslib::page_navbar(                          # Página con barra superior
  theme = bslib::bs_theme(bootswatch = "flatly"),  # Tema visual Flatly
  title = "Análisis de Consumos Eléctricos",       # Título en la barra

  sidebar = bslib::sidebar(                        # Sidebar lateral
    title = "Panel de Control",                    # Título sidebar

    numericInput(                                  # Input año
      inputId = "Anio",                            # ID interno
      label   = "Año",                             # Texto visible
      value   = 2020,                              # Valor inicial
      min     = min(dfConsumos$Anio),              # Mínimo permitido
      max     = max(dfConsumos$Anio)               # Máximo permitido
    ),

    radioButtons(                                  # Input agencia
      inputId  = "Agencia",                        # ID interno
      label    = "Agencia",                        # Texto visible
      choices  = Agencia,                          # Lista de agencias
      selected = "Oficina Comercial Rioja"         # Valor inicial
    )
  ),

  # =========================
  #  PESTAÑA TABLERO
  # =========================
  bslib::nav_panel(
    title = "Tablero",                             # Nombre pestaña

    bslib::layout_columns(                         # 2 value boxes lado a lado
      fill = FALSE,                                # No ocupar alto completo

      bslib::value_box(                            # Primer valor
        title    = "Cantidad de Usuarios",         # Nombre
        value    = textOutput("n_usu"),            # Valor mostrado
        showcase = bs_icon("person-fill")          # Ícono
      ),

      bslib::value_box(                            # Segundo valor
        title    = "Consumo Total (kWh)",          # Nombre
        value    = textOutput("n_consumo"),        # Valor mostrado
        showcase = bs_icon("lightning-charge-fill")# Ícono
      )
    ),

    bslib::card(                                   # Tarjeta con gráfico
      full_screen  = TRUE,                         # Puede pantalla completa
      bslib::card_header("Top 5 Consumidores del Año Seleccionado"), # Título
      plotOutput(outputId = "gg")                  # Aquí va el gráfico
    )
  ),

  # =========================
  #  PESTAÑA MAPA
  # =========================
  bslib::nav_panel(
    title = "Mapa",                                 # Título pestaña

    bslib::card(                                    # Tarjeta del mapa
      full_screen = TRUE,                           # Modo full screen
      bslib::card_header("Clientes georreferenciados"), # Encabezado
      leafletOutput("mapa", height = "600px")       # Mapa de leaflet
    )
  ),

  # =========================
  #  PESTAÑA DATOS
  # =========================
  bslib::nav_panel(
    title = "Datos",                                # Nombre pestaña

    bslib::card(                                    # Tarjeta con tabla
      full_screen = TRUE,                           # Ocupa pantalla
      bslib::card_header("Tabla de Datos Filtrados"), # Título
      reactableOutput("tabla")                      # Tabla reactable
    )
  ),

  # =========================
  #  ÍTEMS A LA DERECHA DEL NAVBAR
  # =========================
  bslib::nav_spacer(),                # Empuja lo siguiente a la derecha

  bslib::nav_item(                                  # Menú Mensajes
    div(
      class = "dropdown",                           # Dropdown Bootstrap

      a(
        "Mensajes",                                 # Texto del botón
        class = "nav-link dropdown-toggle",         # Estilo + comportamiento
        href  = "#",                                # No navega
        `data-bs-toggle` = "dropdown"               # Activa el menú
      ),

      div(
        class = "dropdown-menu dropdown-menu-end",  # Menú alineado derecha

div(class = "dropdown-item", strong("Alumno: "),   "Andrés Guiu"),
div(class = "dropdown-item", strong("Profesor: "), "Mag. Marcos Prunello"),
div(class = "dropdown-item", strong("Profesor: "), "Mag. Diego Marfetán Molina")
      )
    )
  ),

  bslib::nav_item(                                  # Link externo EPE
    a(
      "EPE",                                         # Texto link
      href   = "https://epe.santafe.gov.ar/",        # URL
      target = "_blank"                              # Abrir nueva pestaña
    )
  )
)

# =========================
#  SERVER
# =========================
server <- function(input, output) {                 # Lógica de la app

  datos_filtrados <- reactive({                     # Filtra datos seg inputs
    dfConsumos %>%
      filter(
        Anio    == input$Anio,                      # Año elegido
        Agencia == input$Agencia                    # Agencia elegida
      )
  })

  output$n_usu <- renderText({                      # ValueBox 1
    n <- nrow(datos_filtrados())                    # Cuenta registros

    scales::label_number(
      big.mark=".", decimal.mark=",") (n)           # Formato 12.345
  })

  output$n_consumo <- renderText({                  # ValueBox 2
    total <- datos_filtrados() %>%                  # Toma datos filtrados
      pull(UltCons) %>%                             # Extrae consumos
      sum(na.rm = TRUE)                             # Suma todo

    scales::label_number(
      big.mark=".", decimal.mark=",") (total)       # Formato 999.999
  })

  output$gg <- renderPlot({                         # Gráfico Top 5
    datos_filtrados() %>%
      slice_max(UltCons, n = 5) %>%                 # Los 5 mayores
      arrange(UltCons) %>%                          # Orden ascendente
      mutate(
        Nombre   = factor(Nombre, levels = Nombre), # Mantiene orden
        etiqueta = paste0(Nombre, ": ", round(UltCons), " kWh")
      ) %>%
      ggplot(aes(x = UltCons, y = Nombre, fill = UltCons)) +
      geom_bar(stat = "identity") +                 # Barras
      geom_text(aes(x = 0, label = etiqueta), hjust = 0) + # Texto izq
      scale_fill_gradient(low="#f7fcba", high="#cd2710") + # Paleta
      scale_x_continuous(name = "
Consumo (kWh)") +
      theme_bw() +
      theme(
        legend.position = "none",
        axis.text.y     = element_blank(),
        axis.title.y    = element_blank(),
        axis.ticks.y    = element_blank()
      )
  })

  output$tabla <- renderReactable({                 # Tabla de datos
    datos_filtrados() %>%
      select(
        `Nro. Cliente`     = NroCli,
        `Nombre`           = Nombre,
        `BT/RE`            = Btre,
        `Consumo Promedio` = ConProm,
        `Último Consumo`   = UltCons
      ) %>%
      reactable()
  })

  output$mapa <- renderLeaflet({                    # Construye mapa

    df_map <- datos_filtrados() %>%                 # Filtrado
      mutate(
        lat_sf = as.numeric(lat_sf),                # Asegura numeric
        lon_sf = as.numeric(lon_sf)
      ) %>%
      filter(!is.na(lat_sf), !is.na(lon_sf))        # Quita NA coordenadas

    if (nrow(df_map) == 0) {                        # Si no hay puntos
      return(leaflet() %>% addProviderTiles("OpenStreetMap"))
    }

    lon_min <- min(df_map$lon_sf)                   # Bounds min lon
    lon_max <- max(df_map$lon_sf)                   # Bounds max lon
    lat_min <- min(df_map$lat_sf)                   # Bounds min lat
    lat_max <- max(df_map$lat_sf)                   # Bounds max lat

    leaflet(df_map) %>%                             # Mapa base
      addProviderTiles("OpenStreetMap") %>%

      addCircleMarkers(                             # Marcadores
        lng = ~lon_sf,
        lat = ~lat_sf,
        radius = 4,
        stroke = FALSE,
        fillOpacity = 0.8,
        popup = ~paste0(
          "<b>Cliente:</b> ", Nombre, "<br>",
          "<b>Agencia:</b> ", Agencia, "<br>",
          "<b>Último consumo:</b> ", round(UltCons), " kWh"
        )
      ) %>%

      fitBounds(lon_min, lat_min, lon_max, lat_max) %>% # Centrado mapa

      addHeatmap(                                     # Capa heatmap
        lng       = ~lon_sf,
        lat       = ~lat_sf,
        intensity = ~1,
        blur      = 25,
        radius    = 20
      )
  })
}

# =========================
#  Iniciar la app
# =========================
shinyApp(ui, server)   # Ejecuta la app