1. Introducción

Este cuaderno muestra cómo hacer un mapa interactivo de la producción agrícola por municipio en un departamento de Colombia, usando R.

Se toman los datos de las Evaluaciones Agropecuarias Municipales (EVA) de la UPRA, se filtran para el departamento de Cundinamarca, el año 2022 y el grupo de cultivo frutales, se unen a los polígonos de los municipios del Marco Geoestadístico Nacional (MGN) del DANE y se visualizan con la librería Leaflet.

Se basa en dos materiales del curso de Geomática Básica: el cuaderno How to read and subset .xlsx agricultural statistics data (Lizarazo, 2023) y el Tutorial de RLeaflet (Campos, 2019).

2. Configuración

Se definen el departamento (con su código DANE), el grupo de cultivo y el año que se van a analizar:

mi_depto     <- "Cundinamarca"
mi_cod_depto <- "25"
mi_grupo     <- "FRUTALES"
mi_anio      <- 2022

Se cargan las librerías:

library(readr)       # leer archivos csv
library(dplyr)       # manipular datos (filtrar, agrupar, resumir, unir)
library(janitor)     # limpiar nombres de columnas
library(stringi)     # quitar tildes
library(sf)          # datos geoespaciales (características simples)
library(leaflet)     # mapas interactivos
library(htmlwidgets) # guardar el mapa como html

Como los nombres pueden venir con o sin tildes y en mayúsculas o minúsculas, se crea una función que los deja todos iguales para poder compararlos:

normalizar <- function(x) toupper(trimws(stri_trans_general(x, "Latin-ASCII")))

3. Lectura de los datos EVA

Los datos de la EVA 2019-2025 (Base Agrícola) están publicados por la UPRA en el Portal de Datos Abiertos de Colombia (datos.gov.co). La primera vez se descargan como archivo csv; las siguientes veces se lee el archivo ya guardado.

archivo <- "EVA_2019_2025.csv"

if (!file.exists(archivo)) {
  options(timeout = 600)
  download.file("https://www.datos.gov.co/resource/uejq-wxrr.csv?$limit=1000000",
                archivo, mode = "wb")
}

eva <- read_csv(archivo, show_col_types = FALSE) %>% clean_names()
dim(eva)
## [1] 166732     18

Los nombres originales de las columnas tienen tildes y caracteres especiales. Se seleccionan solo las columnas necesarias y se les ponen nombres sencillos. El código del municipio se deja como texto de 5 dígitos, que es el formato del código DANE:

columna <- function(patron) grep(patron, names(eva), value = TRUE)[1]

eva <- eva %>%
  transmute(
    COD_MUN    = sprintf("%05d", as.integer(.data[[columna("(cod|c_digo).*mun")]])),
    DPTO       = .data[[columna("^departamento$")]],
    MUNICIPIO  = .data[[columna("^municipio$")]],
    GRUPO      = .data[[columna("^grupo")]],
    YEAR       = as.integer(.data[[columna("^(ano|a_o|anio|year)$")]]),
    PRODUCCION = as.numeric(.data[[columna("^produc")]])
  )

head(eva)
## # A tibble: 6 × 6
##   COD_MUN DPTO      MUNICIPIO GRUPO       YEAR PRODUCCION
##   <chr>   <chr>     <chr>     <chr>      <int>      <dbl>
## 1 05001   Antioquia Medellín  Hortalizas  2021       128 
## 2 05001   Antioquia Medellín  Hortalizas  2021       152 
## 3 05001   Antioquia Medellín  Hortalizas  2022       127.
## 4 05001   Antioquia Medellín  Hortalizas  2022       152 
## 5 05001   Antioquia Medellín  Hortalizas  2023       133 
## 6 05001   Antioquia Medellín  Hortalizas  2023       152

4. Filtrar el departamento, el año y el grupo de cultivo

Primero se filtran los datos del departamento y el año:

depto_eva <- eva %>%
  filter(normalizar(DPTO) == normalizar(mi_depto), YEAR == mi_anio)

depto_eva %>% count(GRUPO, sort = TRUE)
## # A tibble: 8 × 2
##   GRUPO                                                           n
##   <chr>                                                       <int>
## 1 Frutales                                                      652
## 2 Hortalizas                                                    495
## 3 Cereales                                                      350
## 4 Leguminosas                                                   329
## 5 Raíces y tubérculos                                           317
## 6 Cultivos tropicales tradicionales                             156
## 7 Cultivos para condimentos, bebidas medicinales y aromáticas    38
## 8 Oleaginosas                                                     6

Luego se selecciona el grupo de cultivo y se suma la producción de cada municipio:

produccion <- depto_eva %>%
  filter(normalizar(GRUPO) == normalizar(mi_grupo)) %>%
  group_by(COD_MUN, MUNICIPIO) %>%
  summarize(prod_total = sum(PRODUCCION, na.rm = TRUE), .groups = "drop") %>%
  arrange(desc(prod_total))

produccion
## # A tibble: 114 × 3
##    COD_MUN MUNICIPIO    prod_total
##    <chr>   <chr>             <dbl>
##  1 25649   San Bernardo     88442.
##  2 25386   La Mesa          56226.
##  3 25035   Anapoima         41141.
##  4 25123   Cachipay         29687.
##  5 25269   Facatativá       28609.
##  6 25524   Pandi            27271.
##  7 25740   Sibaté           24359 
##  8 25599   Apulo            21331 
##  9 25815   Tocaima          20511 
## 10 25878   Viotá            18955.
## # ℹ 104 more rows

Se guarda la tabla en formato csv:

write.csv(produccion, "produccion_municipios.csv", row.names = FALSE)

5. Polígonos de los municipios (DANE)

Los límites municipales se toman del Marco Geoestadístico Nacional MGN2025 del DANE, nivel municipio, en formato GeoPackage. Se busca el archivo en la carpeta de trabajo y se lee como un objeto sf:

(mgn <- list.files(".", pattern = "MGN.*MPIO.*\\.gpkg$", recursive = TRUE, full.names = TRUE))
## [1] "./Proyecto_ciudades/MGN2025_MPIO/MGN_ADM_MPIO_GRAFICO (Geopackage).gpkg"
colombia <- st_read(mgn[1])
## Reading layer `MGN_ADM_MPIO_GRAFICO (Geopackage)' from data source 
##   `C:\GEOMATICA2026\Restudio\Proyecto_ciudades\MGN2025_MPIO\MGN_ADM_MPIO_GRAFICO (Geopackage).gpkg' 
##   using driver `GPKG'
## Simple feature collection with 1122 features and 11 fields
## Geometry type: MULTIPOLYGON
## Dimension:     XY
## Bounding box:  xmin: -81.73562 ymin: -4.229406 xmax: -66.84722 ymax: 13.39473
## Geodetic CRS:  WGS 84
names(colombia)
##  [1] "dpto_ccdgo" "mpio_ccdgo" "mpio_cdpmp" "dpto_cnmbr" "mpio_cnmbr"
##  [6] "mpio_crslc" "mpio_tipo"  "mpio_narea" "mpio_nano"  "shape_Leng"
## [11] "shape_Area" "geom"

Se seleccionan los municipios del departamento por su código DANE:

mpios <- colombia %>% filter(dpto_ccdgo == mi_cod_depto)
nrow(mpios)
## [1] 116

6. Unir la producción a los polígonos

El MGN trae el código DANE de 5 dígitos de cada municipio en la columna mpio_cdpmp, que es el mismo código de la columna COD_MUN de la EVA. Por eso la unión se hace por código y no por nombre, lo que evita problemas con tildes o nombres escritos de forma distinta.

mapa_datos <- mpios %>% left_join(produccion, by = c("mpio_cdpmp" = "COD_MUN"))

# Municipios con producción que no encontraron su polígono (debe estar vacío)
setdiff(produccion$COD_MUN, mpios$mpio_cdpmp)
## character(0)
# Municipios sin producción de frutales en el año (quedan en gris)
sum(is.na(mapa_datos$prod_total))
## [1] 2

Leaflet trabaja con coordenadas geográficas WGS84 (EPSG 4326), así que se verifica y, si hace falta, se transforma:

mapa_datos <- st_transform(mapa_datos, 4326)

7. Mapa interactivo con Leaflet

Pocos municipios producen mucho más que los demás, así que una escala continua dejaría casi todo el mapa del mismo color. Por eso se usan quintiles: los municipios se dividen en 5 grupos con la misma cantidad cada uno. Los municipios en gris no reportaron producción de frutales en 2022.

pal <- colorQuantile("viridis", domain = mapa_datos$prod_total,
                     n = 5, na.color = "#cccccc")

popup <- paste0(
  "<b>Municipio: </b>", mapa_datos$mpio_cnmbr, "<br>",
  "<b>Código DANE: </b>", mapa_datos$mpio_cdpmp, "<br>",
  "<b>Producción ", tolower(mi_grupo), " ", mi_anio, ": </b>",
  ifelse(is.na(mapa_datos$prod_total), "sin datos",
         paste(format(round(mapa_datos$prod_total), big.mark = ",", trim = TRUE), "t"))
)

mapa <- leaflet(mapa_datos) %>%
  addProviderTiles(providers$Esri.WorldTerrain) %>%
  addPolygons(
    fillColor   = ~pal(prod_total),
    fillOpacity = 0.6,
    color       = "#444444",
    weight      = 1,
    popup       = popup,
    highlightOptions = highlightOptions(weight = 3, color = "white", bringToFront = TRUE)
  ) %>%
  addLegend(
    position = "topright",
    pal      = pal,
    values   = ~prod_total,
    title    = paste0("Producción ", tolower(mi_grupo), "<br>", mi_anio, " (quintiles)")
  )

mapa

Haciendo clic en cada municipio se puede ver su nombre, su código DANE y su producción en toneladas.

8. Resultados

La mayor producción de frutales se concentra en el occidente y el sur de Cundinamarca, en municipios de clima cálido, como los de la provincia del Tequendama (La Mesa, Anapoima, Cachipay). Los municipios con menor producción están sobre todo en el centro y el oriente, en zonas altas y frías cercanas a la Sabana de Bogotá, donde predominan otros cultivos como la papa. Bogotá no aparece porque en el MGN es un distrito aparte del departamento.

Se guarda el mapa como un archivo html:

saveWidget(mapa, "mapa_produccion.html", selfcontained = TRUE)

Referencias

Reproducibilidad

sessionInfo()
## R version 4.6.1 (2026-06-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
## 
## Matrix products: default
##   LAPACK version 3.12.1
## 
## locale:
## [1] LC_COLLATE=Spanish_Mexico.utf8  LC_CTYPE=Spanish_Mexico.utf8   
## [3] LC_MONETARY=Spanish_Mexico.utf8 LC_NUMERIC=C                   
## [5] LC_TIME=Spanish_Mexico.utf8    
## 
## time zone: America/Bogota
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] htmlwidgets_1.6.4 leaflet_2.2.3     sf_1.1-3          stringi_1.8.9    
## [5] janitor_2.2.1     dplyr_1.2.1       readr_2.2.0      
## 
## loaded via a namespace (and not attached):
##  [1] utf8_1.2.6              sass_0.4.10             generics_0.1.4         
##  [4] class_7.3-23            KernSmooth_2.23-26      hms_1.1.4              
##  [7] digest_0.6.39           magrittr_2.0.5          RColorBrewer_1.1-3     
## [10] evaluate_1.0.5          grid_4.6.1              timechange_0.4.0       
## [13] fastmap_1.2.0           jsonlite_2.0.0          e1071_1.7-17           
## [16] DBI_1.3.0               scales_1.4.0            viridisLite_0.4.3      
## [19] crosstalk_1.2.2         jquerylib_0.1.4         cli_3.6.6              
## [22] crayon_1.5.3            rlang_1.3.0             units_1.0-1            
## [25] bit64_4.8.6             cachem_1.1.0            yaml_2.3.12            
## [28] parallel_4.6.1          tools_4.6.1             tzdb_0.5.0             
## [31] vctrs_0.7.3             R6_2.6.1                proxy_0.4-29           
## [34] lifecycle_1.0.5         lubridate_1.9.5         classInt_0.4-11        
## [37] snakecase_0.11.1        stringr_1.6.0           leaflet.providers_3.0.0
## [40] bit_4.6.0               vroom_1.7.1             pkgconfig_2.0.3        
## [43] pillar_1.11.1           bslib_0.12.0            glue_1.8.1             
## [46] Rcpp_1.1.2              xfun_0.62               tibble_3.3.1           
## [49] tidyselect_1.2.1        rstudioapi_0.19.0       knitr_1.52             
## [52] farver_2.1.2            htmltools_0.5.9         rmarkdown_2.32         
## [55] compiler_4.6.1