La caña de azúcar (Saccharum officinarum) es el cultivo más importante del valle geográfico del río Cauca. Para saber en qué otras partes del mundo se podría cultivar con un clima parecido, se usan datos de clima de línea base a nivel global: temperatura media mensual y precipitación mensual en una grilla regular (12 capas por variable, una por mes).
Fuente de datos. Se usa WorldClim 2.1 (Fick y Hijmans, 2017). Representa el clima promedio del periodo 1970-2000 con una resolución de 10 minutos de arco (unos 18,5 km en el ecuador), que es más fina que la de 0,5° del ejemplo.
Objetivo: identificar las zonas del mundo con condiciones climáticas aptas para la caña de azúcar con dos aproximaciones diferentes y compararlas:
Se siguen los pasos del ejemplo de la unidad (cargar las imágenes
raster mes a mes, apilarlas, renombrar las capas y visualizarlas con una
escala de color común). Como las librerías raster,
rgdal y rasterVis del ejemplo fueron retiradas
o dependen de paquetes retirados de CRAN, se usan sus reemplazos
modernos: terra para los raster
(rast() ≈ raster()/stack(),
crop()/mask() iguales,
terra::extract() igual) y sf
para los vectores (st_read() ≈ readOGR()).
Los datos deben estar en una carpeta data_global/ junto
a este archivo .Rmd, con una subcarpeta por variable
(tmean/ y prec/). En tmean/ va el
contenido de wc2.1_10m_tavg.zip y en prec/ el
de wc2.1_10m_prec.zip, descargados de https://www.worldclim.org/data/worldclim21.html.
library(terra) # lectura y álgebra de imágenes raster (reemplaza a raster)
library(sf) # datos vectoriales: shape global de países (reemplaza a rgdal)
library(dplyr) # manipulación de tablas
library(tidyr) # reestructuración de tablas (ojo: tidyr::extract enmascara terra::extract)
library(ggplot2) # mapas y gráficos
library(scales) # formatos de ejes y leyendas
library(patchwork) # unir varios gráficos en una figura
library(knitr) # tablas
ruta_datos <- "data_global"
ruta_tmean <- file.path(ruta_datos, "tmean")
ruta_prec <- file.path(ruta_datos, "prec")
meses <- c("Ene", "Feb", "Mar", "Abr", "May", "Jun",
"Jul", "Ago", "Sep", "Oct", "Nov", "Dic")
Igual que en el ejemplo, se listan los 12 archivos de cada variable
con list.files() y se cargan todos juntos. En
terra, rast() con un vector de archivos hace
lo mismo que stack().
# Lista los archivos raster (asc, tif o bil) y los ordena por el número de mes
listar_meses <- function(carpeta) {
archivos <- list.files(carpeta, pattern = "\\.(asc|tif|bil)$", full.names = TRUE)
mes <- as.integer(sub(".*_(\\d+)\\.[a-z]+$", "\\1", basename(archivos)))
archivos[order(mes)]
}
tmean <- rast(listar_meses(ruta_tmean))
prec <- rast(listar_meses(ruta_prec))
# Se asegura el sistema de coordenadas geográficas WGS84 (los .asc del ejemplo no lo traen)
crs(tmean) <- "EPSG:4326"
crs(prec) <- "EPSG:4326"
# Se cambian los nombres de las capas por los meses (como names(...) = month.name)
names(tmean) <- meses
names(prec) <- meses
# Se excluye la Antártida (al sur de 60° S), igual que la extensión de los datos del
# ejemplo: no tiene interés agrícola y sus temperaturas extremas distorsionan las estadísticas
tmean <- crop(tmean, ext(-180, 180, -60, 90))
prec <- crop(prec, ext(-180, 180, -60, 90))
tmean
## class : SpatRaster
## size : 900, 2160, 12 (nrow, ncol, nlyr)
## resolution : 0.1666667, 0.1666667 (x, y)
## extent : -180, 180, -60, 90 (xmin, xmax, ymin, ymax)
## coord. ref. : lon/lat WGS 84 (EPSG:4326)
## source(s) : memory
## names : Ene, Feb, Mar, Abr, May, Jun, ...
## min values : -45.883999, -43.172001, -42.453999, -33.623749, -21.366499, -14.2, ...
## max values : 34.009499, 32.824249, 32.9095, 34.193748, 36.25325, 38.355499, ...
En la versión 1.4 de WorldClim (la del ejemplo) la temperatura viene multiplicada por 10 para guardarla como entero; en la 2.1 ya viene en °C. Se revisa el rango y, si hace falta, se divide por 10.
rango_t <- global(tmean[[1]], range, na.rm = TRUE)
rango_t
## X1 X2
## Ene -45.884 34.0095
if (max(rango_t) > 100) tmean <- tmean / 10 # de décimas de °C a °C
global(tmean, range, na.rm = TRUE)
## X1 X2
## Ene -45.88400 34.00950
## Feb -43.17200 32.82425
## Mar -42.45400 32.90950
## Abr -33.62375 34.19375
## May -21.36650 36.25325
## Jun -14.20000 38.35550
## Jul -13.42875 39.54950
## Ago -13.99375 38.43275
## Sep -20.59575 35.79000
## Oct -29.69400 32.65125
## Nov -37.54250 32.78800
## Dic -45.32700 32.82525
global(prec, range, na.rm = TRUE)
## X1 X2
## Ene 0 908
## Feb 0 793
## Mar 0 720
## Abr 0 1004
## May 0 2068
## Jun 0 2210
## Jul 0 2381
## Ago 0 1674
## Sep 0 1955
## Oct 0 2328
## Nov 0 718
## Dic 0 806
La resolución de la grilla es de 0.1666667° (unos 18 km en el ecuador), así que cada celda resume el clima promedio de un área grande. Esto es importante cuando se extraiga el clima de puntos puntuales del Valle del Cauca.
# Si el caso trae un shape global se usa ese; si no, el mapa de países de spData
shp <- list.files(ruta_datos, pattern = "\\.shp$", recursive = TRUE, full.names = TRUE)
if (length(shp) > 0) {
paises <- st_read(shp[1], quiet = TRUE)
} else {
paises <- spData::world
}
paises <- st_transform(paises, 4326)
# Columna con el nombre del país (cambia según la fuente del shape)
col_nombre <- intersect(c("name_long", "NAME", "NAME_LONG", "name", "COUNTRY", "ADMIN"),
names(paises))[1]
paises$pais <- paises[[col_nombre]]
paises <- paises[paises$pais != "Antarctica", ] # misma extensión que los raster
nrow(paises)
## [1] 176
ggplot2 no grafica objetos SpatRaster
directamente, así que se pasan a tabla con
as.data.frame(xy = TRUE). Todas las figuras usan la misma
escala de color por variable para poder comparar los meses entre sí.
# Raster -> tabla larga (x, y, capa, valor) para ggplot.
# Solo para graficar, los raster de más de 0.25° se agregan (promedio de 3x3 celdas)
# para que las figuras sean livianas; los cálculos se hacen con la resolución completa.
raster_a_tabla <- function(r, agregar = res(r)[1] < 0.25) {
if (agregar) r <- aggregate(r, fact = 3, fun = "mean", na.rm = TRUE)
as.data.frame(r, xy = TRUE, na.rm = TRUE) %>%
pivot_longer(-c(x, y), names_to = "capa", values_to = "valor") %>%
mutate(capa = factor(capa, levels = names(r)))
}
# Mapa de un raster continuo con el contorno de los países
mapa_continuo <- function(r, titulo, leyenda, paleta = "viridis", direccion = 1,
limites = NULL, contorno = paises,
agregar = res(r)[1] < 0.25) {
ggplot(raster_a_tabla(r, agregar)) +
geom_raster(aes(x, y, fill = valor)) +
{if (!is.null(contorno)) geom_sf(data = contorno, fill = NA, colour = "grey30",
linewidth = 0.1, inherit.aes = FALSE)} +
scale_fill_viridis_c(option = paleta, direction = direccion, limits = limites,
name = leyenda) +
coord_sf(expand = FALSE) +
labs(title = titulo, x = NULL, y = NULL) +
theme_minimal(base_size = 10)
}
ggplot(raster_a_tabla(tmean)) +
geom_raster(aes(x, y, fill = valor)) +
facet_wrap(~ capa, ncol = 4) +
scale_fill_distiller(palette = "RdBu", name = "°C") + # misma escala para los 12 meses
coord_equal(expand = FALSE) +
labs(title = "Temperatura media mensual (línea base)", x = NULL, y = NULL) +
theme_void(base_size = 10)
ggplot(raster_a_tabla(prec)) +
geom_raster(aes(x, y, fill = pmin(valor, 500))) +
facet_wrap(~ capa, ncol = 4) +
scale_fill_distiller(palette = "YlGnBu", direction = 1,
name = "mm\n(≥500\nagrupado)") +
coord_equal(expand = FALSE) +
labs(title = "Precipitación mensual (línea base)", x = NULL, y = NULL) +
theme_void(base_size = 10)
Para el análisis de aptitud se resumen los 12 meses en variables anuales: la temperatura media anual, la precipitación total anual y la temperatura del mes más frío.
t_anual <- mean(tmean) # temperatura media anual (°C)
p_anual <- sum(prec) # precipitación total anual (mm)
t_mes_frio <- min(tmean) # temperatura del mes más frío (°C)
names(t_anual) <- "t_anual"; names(p_anual) <- "p_anual"; names(t_mes_frio) <- "t_frio"
mapa_continuo(t_anual, "Temperatura media anual", "°C", paleta = "inferno") +
mapa_continuo(clamp(p_anual, upper = 4000), "Precipitación anual", "mm\n(≥4000\nagrupado)",
paleta = "mako", direccion = -1) +
plot_layout(ncol = 1)
Se usan los requerimientos de la base de datos ECOCROP de la FAO para Saccharum officinarum. Para cada variable hay un rango absoluto (fuera de él el cultivo no prospera) y un rango óptimo (donde el cultivo tiene su mejor desempeño):
rangos <- tibble::tribble(
~variable, ~abs_min, ~opt_min, ~opt_max, ~abs_max,
"Temperatura media anual (°C)", 15, 24, 37, 41,
"Precipitación anual (mm)", 1000, 1500, 2500, 5000,
"Temperatura del mes más frío (°C)", 15, 18, 99, 99 # sin heladas ni frío prolongado
)
kable(rangos, caption = "Rangos de temperatura y precipitación para la caña de azúcar (FAO ECOCROP)")
| variable | abs_min | opt_min | opt_max | abs_max |
|---|---|---|---|---|
| Temperatura media anual (°C) | 15 | 24 | 37 | 41 |
| Precipitación anual (mm) | 1000 | 1500 | 2500 | 5000 |
| Temperatura del mes más frío (°C) | 15 | 18 | 99 | 99 |
La caña es una planta C4 de clima tropical: crece mejor con temperaturas altas, necesita mucha agua durante el crecimiento y no tolera heladas. Por eso se agrega la temperatura del mes más frío: con menos de 15 °C la caña prácticamente deja de crecer.
Como en el modelo ECOCROP, la aptitud de cada variable es una función trapezoidal: vale 0 fuera del rango absoluto, 1 dentro del rango óptimo y crece o decrece linealmente entre ambos. La aptitud climática total es el mínimo de las aptitudes parciales (ley del mínimo de Liebig: el factor más limitante define la aptitud).
# Aptitud trapezoidal: 0 fuera de [abs_min, abs_max], 1 en [opt_min, opt_max]
aptitud_trapecio <- function(r, abs_min, opt_min, opt_max, abs_max) {
subida <- (r - abs_min) / (opt_min - abs_min) # tramo creciente
bajada <- (abs_max - r) / (abs_max - opt_max) # tramo decreciente
apt <- min(c(subida, bajada)) # el menor de los dos tramos
clamp(apt, lower = 0, upper = 1) # se acota entre 0 y 1
}
apt_t <- aptitud_trapecio(t_anual, 15, 24, 37, 41)
apt_p <- aptitud_trapecio(p_anual, 1000, 1500, 2500, 5000)
apt_frio <- aptitud_trapecio(t_mes_frio, 15, 18, 99, 99)
aptitud <- min(c(apt_t, apt_p, apt_frio)) # ley del mínimo
names(apt_t) <- "Temperatura"; names(apt_p) <- "Precipitación"
names(apt_frio) <- "Mes más frío"; names(aptitud) <- "Aptitud climática"
parciales <- c(apt_t, apt_p, apt_frio)
ggplot(raster_a_tabla(parciales)) +
geom_raster(aes(x, y, fill = valor)) +
facet_wrap(~ capa, ncol = 1) +
scale_fill_distiller(palette = "YlGn", direction = 1, limits = c(0, 1),
name = "Aptitud") +
coord_equal(expand = FALSE) +
labs(title = "Aptitud parcial por variable climática", x = NULL, y = NULL) +
theme_void(base_size = 10)
Para leer el mapa final, el índice se agrupa en cuatro clases:
clases <- c("No apta", "Marginal", "Moderada", "Alta")
aptitud_cl <- classify(aptitud, rcl = matrix(c(-Inf, 0, 0,
0, 0.5, 1,
0.5, 0.8, 2,
0.8, Inf, 3), ncol = 3, byrow = TRUE),
include.lowest = TRUE, right = TRUE)
levels(aptitud_cl) <- data.frame(id = 0:3, clase = clases)
colores_clase <- c("No apta" = "grey85", "Marginal" = "#fee08b",
"Moderada" = "#a6d96a", "Alta" = "#1a9641")
# Para graficar se agrega con la moda (clase más frecuente en cada bloque de 3x3)
tabla_aptitud <- as.data.frame(aggregate(aptitud_cl, fact = 3, fun = "modal", na.rm = TRUE),
xy = TRUE, na.rm = TRUE)
ggplot(tabla_aptitud) +
geom_raster(aes(x, y, fill = clase)) +
geom_sf(data = paises, fill = NA, colour = "grey40", linewidth = 0.1,
inherit.aes = FALSE) +
scale_fill_manual(values = colores_clase, name = "Aptitud") +
coord_sf(expand = FALSE) +
labs(title = "Aptitud climática para la caña de azúcar (rangos óptimos FAO ECOCROP)",
x = NULL, y = NULL) +
theme_minimal(base_size = 10)
Se calcula el área de cada clase con cellSize(), que
tiene en cuenta que las celdas son más pequeñas cerca de los polos.
area_celda <- cellSize(aptitud_cl, unit = "km")
area_clase <- zonal(area_celda, aptitud_cl, fun = "sum", na.rm = TRUE)
names(area_clase) <- c("clase", "area_km2")
area_clase %>%
mutate(area_millones_km2 = round(area_km2 / 1e6, 2),
porcentaje = percent(area_km2 / sum(area_km2), accuracy = 0.1)) %>%
select(-area_km2) %>%
kable(caption = "Superficie continental por clase de aptitud")
| clase | area_millones_km2 | porcentaje |
|---|---|---|
| No apta | 111.16 | 78.5% |
| Marginal | 7.37 | 5.2% |
| Moderada | 6.13 | 4.3% |
| Alta | 16.89 | 11.9% |
Para saber qué limita la aptitud en cada lugar, se identifica la variable con el menor índice parcial en las celdas no aptas o marginales:
limitante <- which.min(parciales) # capa con la aptitud más baja
limitante <- mask(limitante, aptitud > 0.5, maskvalues = 1) # solo donde la aptitud es baja
levels(limitante) <- data.frame(id = 1:3, factor = names(parciales))
ggplot(as.data.frame(aggregate(limitante, fact = 3, fun = "modal", na.rm = TRUE),
xy = TRUE, na.rm = TRUE)) +
geom_raster(aes(x, y, fill = factor)) +
geom_sf(data = paises, fill = NA, colour = "grey40", linewidth = 0.1,
inherit.aes = FALSE) +
scale_fill_manual(values = c("Temperatura" = "#d7301f", "Precipitación" = "#2b8cbe",
"Mes más frío" = "#756bb1"), name = "Factor limitante") +
coord_sf(expand = FALSE) +
labs(title = "Factor que más limita la aptitud (celdas con aptitud ≤ 0.5)",
x = NULL, y = NULL) +
theme_minimal(base_size = 10)
Para escoger los países de forma objetiva se cruza el mapa de aptitud con el shape de países: se rasteriza el shape sobre la misma grilla del clima y se suma el área con aptitud alta en cada país.
paises_r <- rasterize(vect(paises), aptitud, field = "pais") # país de cada celda
area_alta <- area_celda * (aptitud > 0.8) # km² con aptitud alta
ranking <- zonal(c(area_alta, area_celda), paises_r, fun = "sum", na.rm = TRUE)
names(ranking) <- c("pais", "area_alta_km2", "area_total_km2")
ranking <- ranking %>%
mutate(pct_alta = area_alta_km2 / area_total_km2) %>%
arrange(desc(area_alta_km2))
ranking %>%
head(10) %>%
mutate(area_alta_miles_km2 = round(area_alta_km2 / 1000),
pct_alta = percent(pct_alta, accuracy = 0.1)) %>%
select(pais, area_alta_miles_km2, pct_alta) %>%
kable(caption = "Diez países con más superficie de aptitud climática alta")
| pais | area_alta_miles_km2 | pct_alta |
|---|---|---|
| Brazil | 5152 | 60.6% |
| Democratic Republic of the Congo | 1665 | 71.7% |
| Indonesia | 1059 | 58.3% |
| Colombia | 645 | 55.9% |
| Peru | 538 | 41.1% |
| Venezuela | 535 | 58.9% |
| Bolivia | 401 | 37.0% |
| Cameroon | 331 | 72.0% |
| Central African Republic | 315 | 50.5% |
| Republic of the Congo | 297 | 87.5% |
Se toman los dos países con más área de aptitud alta y se agrega Colombia, que es el país de referencia del caso.
paises_sel <- unique(c(head(ranking$pais, 2), "Colombia"))
paises_sel
## [1] "Brazil" "Democratic Republic of the Congo"
## [3] "Colombia"
El corte se hace en dos pasos, como con crop y
mask del paquete raster: crop()
recorta el raster a la caja que contiene el país y mask()
deja en NA las celdas que caen fuera de su polígono.
mapa_pais <- function(nombre) {
pol <- paises[paises$pais == nombre, ]
r <- mask(crop(aptitud, vect(pol)), vect(pol)) # corte con el shape del país
ggplot(as.data.frame(r, xy = TRUE, na.rm = TRUE)) +
geom_raster(aes(x, y, fill = `Aptitud climática`)) +
geom_sf(data = pol, fill = NA, colour = "black", linewidth = 0.3, inherit.aes = FALSE) +
scale_fill_distiller(palette = "YlGn", direction = 1, limits = c(0, 1),
name = "Aptitud") +
coord_sf(expand = FALSE) +
labs(title = nombre, x = NULL, y = NULL) +
theme_minimal(base_size = 9)
}
wrap_plots(lapply(paises_sel, mapa_pais), nrow = 1, guides = "collect")
Se escogieron tres sitios del Valle del Cauca con ayuda de Google Maps, buscando que caigan en celdas distintas de la grilla y que representen ambientes diferentes:
Las coordenadas se tomaron de Google Maps. Como en el ejemplo de la
unidad, también se pueden obtener geocodificando el nombre con
geocode_OSM() de tmaptools (el chunk solo se
ejecuta si la librería está instalada y hay conexión).
sitios <- tibble::tribble(
~sitio, ~lon, ~lat,
"Palmira", -76.303, 3.539,
"Cartago", -75.912, 4.747,
"Bajo Calima (Buenaventura)", -76.970, 3.990
)
library(tmaptools)
geo <- tryCatch(geocode_OSM(c("Palmira, Valle del Cauca, Colombia",
"Cartago, Valle del Cauca, Colombia")),
error = function(e) NULL)
geo
## query lat lon lat_min lat_max
## 1 Palmira, Valle del Cauca, Colombia 3.530837 -76.29880 3.451623 3.718068
## 2 Cartago, Valle del Cauca, Colombia 4.710659 -75.93239 4.612901 4.808081
## lon_min lon_max
## 1 -76.49174 -75.94967
## 2 -76.01666 -75.82212
sitios_sf <- st_as_sf(sitios, coords = c("lon", "lat"), crs = 4326)
sitios$celda <- cellFromXY(tmean, as.matrix(sitios[, c("lon", "lat")]))
sitios # se verifica que cada sitio caiga en una celda diferente
## # A tibble: 3 × 4
## sitio lon lat celda
## <chr> <dbl> <dbl> <dbl>
## 1 Palmira -76.3 3.54 1119503
## 2 Cartago -75.9 4.75 1104385
## 3 Bajo Calima (Buenaventura) -77.0 3.99 1115179
valle <- st_as_sfc(st_bbox(c(xmin = -78, xmax = -75, ymin = 2.5, ymax = 5.5), crs = 4326))
mapa_continuo(crop(t_anual, vect(valle)), "Sitios sobre la temperatura media anual",
"°C", paleta = "inferno", contorno = NULL, agregar = FALSE) +
geom_sf(data = sitios_sf, colour = "cyan", size = 3, inherit.aes = FALSE) +
geom_sf_label(data = sitios_sf, aes(label = sitio), nudge_y = 0.25, size = 3,
inherit.aes = FALSE)
Con terra::extract() se obtiene el valor de las 12 capas
de temperatura y de precipitación en cada sitio.
coords <- as.matrix(sitios[, c("lon", "lat")])
clima_sitios <- bind_rows(
data.frame(sitio = sitios$sitio, variable = "tmean", terra::extract(tmean, coords)),
data.frame(sitio = sitios$sitio, variable = "prec", terra::extract(prec, coords))
) %>%
pivot_longer(all_of(meses), names_to = "mes", values_to = "valor") %>%
mutate(mes = factor(mes, levels = meses)) %>%
pivot_wider(names_from = variable, values_from = valor)
clima_sitios %>%
group_by(sitio) %>%
summarise(`T media anual (°C)` = round(mean(tmean), 1),
`T mes más frío (°C)` = round(min(tmean), 1),
`Precipitación anual (mm)` = round(sum(prec))) %>%
kable(caption = "Resumen climático de los sitios")
| sitio | T media anual (°C) | T mes más frío (°C) | Precipitación anual (mm) |
|---|---|---|---|
| Bajo Calima (Buenaventura) | 26.1 | 25.8 | 5631 |
| Cartago | 23.1 | 22.5 | 1896 |
| Palmira | 22.4 | 21.9 | 1484 |
g_t <- ggplot(clima_sitios, aes(mes, tmean, colour = sitio, group = sitio)) +
annotate("rect", xmin = -Inf, xmax = Inf, ymin = 24, ymax = 37,
fill = "darkgreen", alpha = 0.08) + # rango óptimo de temperatura
geom_line(linewidth = 1) + geom_point() +
labs(title = "Temperatura media mensual", x = NULL, y = "°C", colour = NULL,
caption = "Franja verde: rango óptimo de temperatura (24 a 37 °C)") +
theme_minimal(base_size = 10)
g_p <- ggplot(clima_sitios, aes(mes, prec, fill = sitio)) +
geom_col(position = "dodge") +
labs(title = "Precipitación mensual", x = NULL, y = "mm", fill = NULL) +
theme_minimal(base_size = 10)
g_t / g_p + plot_layout(guides = "collect")
El clima de cada celda se describe con un vector de 24 variables (12 temperaturas y 12 precipitaciones mensuales). Antes de medir la distancia se hacen dos ajustes:
\[d(c, s) = \sqrt{\sum_{k=1}^{24} \left(z_{k}(c) - z_{k}(s)\right)^2}\]
donde \(z_k(c)\) es el valor estandarizado de la variable \(k\) en la celda \(c\) y \(z_k(s)\) el del sitio de referencia \(s\). Una distancia pequeña significa que la celda tiene un clima análogo al del sitio, con el mismo ciclo estacional. Para facilitar la lectura, la distancia se convierte en una similaridad entre 0 y 1: \(sim = 1 - d / d_{max}\).
clima <- c(tmean, log1p(prec)) # precipitación en escala log(1 + P)
names(clima) <- c(paste0("t_", meses), paste0("p_", meses))
medias <- global(clima, "mean", na.rm = TRUE)$mean
desv <- global(clima, "sd", na.rm = TRUE)$sd
clima_z <- (clima - medias) / desv # puntaje z de cada capa
z_sitios <- terra::extract(clima_z, coords) # vector z de cada sitio
similaridad_sitio <- function(i) {
d <- sqrt(sum((clima_z - unlist(z_sitios[i, ]))^2)) # distancia euclidiana
1 - d / global(d, "max", na.rm = TRUE)$max # similaridad 0-1
}
similaridad <- rast(lapply(seq_len(nrow(sitios)), similaridad_sitio))
## |---------|---------|---------|---------|========================================= |---------|---------|---------|---------|========================================= |---------|---------|---------|---------|========================================= |---------|---------|---------|---------|=========================================
names(similaridad) <- sitios$sitio
ggplot(raster_a_tabla(similaridad)) +
geom_raster(aes(x, y, fill = valor)) +
geom_sf(data = paises, fill = NA, colour = "grey40", linewidth = 0.1,
inherit.aes = FALSE) +
geom_sf(data = sitios_sf, colour = "red", size = 1.5, inherit.aes = FALSE) +
facet_wrap(~ capa, ncol = 1) +
scale_fill_viridis_c(option = "magma", name = "Similaridad") +
coord_sf(expand = FALSE) +
labs(title = "Similaridad climática con cada sitio del Valle del Cauca",
x = NULL, y = NULL) +
theme_minimal(base_size = 10)
La similaridad decae de forma continua, así que para hablar de zonas análogas se toma el 2 % de las celdas continentales más parecidas a cada sitio (unos 2,5 millones de km², un área comparable a la de los grandes países productores):
umbral <- global(similaridad, quantile, probs = 0.98, na.rm = TRUE)[, 1]
analogos <- similaridad >= umbral # TRUE en el 2 % de celdas más similares
analogos <- mask(analogos, analogos, maskvalues = 0)
# Para graficar: 1 = análogo, 0 = resto del continente (máximo en bloques de 3x3)
analogos_graf <- mask(classify(analogos, cbind(NA, 0)), t_anual)
analogos_graf <- aggregate(analogos_graf, fact = 3, fun = "max", na.rm = TRUE)
names(analogos_graf) <- sitios$sitio
ggplot(raster_a_tabla(analogos_graf, agregar = FALSE)) +
geom_raster(aes(x, y, fill = factor(valor, levels = 0:1, labels = c("No", "Sí")))) +
geom_sf(data = paises, fill = NA, colour = "grey50", linewidth = 0.1,
inherit.aes = FALSE) +
geom_sf(data = sitios_sf, colour = "black", size = 1.5, inherit.aes = FALSE) +
scale_fill_manual(values = c("No" = "grey90", "Sí" = "#d95f02"), name = "Análogo") +
facet_wrap(~ capa, ncol = 1) +
coord_sf(expand = FALSE) +
labs(title = "Zonas análogas: 2 % de celdas con clima más parecido a cada sitio",
x = NULL, y = NULL) +
theme_minimal(base_size = 10)
area_analogos <- zonal(area_celda * classify(analogos, cbind(NA, 0)), paises_r,
fun = "sum", na.rm = TRUE)
names(area_analogos) <- c("pais", sitios$sitio)
area_analogos %>%
pivot_longer(-pais, names_to = "sitio", values_to = "km2") %>%
group_by(sitio) %>%
slice_max(km2, n = 5) %>%
mutate(miles_km2 = round(km2 / 1000)) %>%
select(sitio, pais, miles_km2) %>%
kable(caption = "Cinco países con más área análoga a cada sitio")
| sitio | pais | miles_km2 |
|---|---|---|
| Bajo Calima (Buenaventura) | Indonesia | 983 |
| Bajo Calima (Buenaventura) | Brazil | 510 |
| Bajo Calima (Buenaventura) | Colombia | 491 |
| Bajo Calima (Buenaventura) | Peru | 297 |
| Bajo Calima (Buenaventura) | Malaysia | 252 |
| Cartago | Democratic Republic of the Congo | 981 |
| Cartago | Brazil | 495 |
| Cartago | Indonesia | 440 |
| Cartago | Peru | 339 |
| Cartago | Colombia | 314 |
| Palmira | Democratic Republic of the Congo | 1095 |
| Palmira | Brazil | 606 |
| Palmira | Peru | 334 |
| Palmira | Indonesia | 212 |
| Palmira | Republic of the Congo | 212 |
Se compara la aptitud alta o moderada (índice > 0.5) con las zonas análogas a Palmira, que representa la zona azucarera del Valle del Cauca. Cada celda queda en una de cuatro categorías:
apto <- aptitud > 0.5
analogo_palmira <- classify(analogos[["Palmira"]], cbind(NA, 0)) == 1
analogo_palmira <- mask(analogo_palmira, aptitud)
comparacion <- apto * 1 + analogo_palmira * 2 # 0 ninguno, 1 solo aptitud, 2 solo análogo, 3 ambos
levels(comparacion) <- data.frame(id = 0:3,
categoria = c("Ninguno", "Solo aptitud (ECOCROP)", "Solo análogo a Palmira", "Ambos"))
ggplot(as.data.frame(aggregate(comparacion, fact = 3, fun = "modal", na.rm = TRUE),
xy = TRUE, na.rm = TRUE)) +
geom_raster(aes(x, y, fill = categoria)) +
geom_sf(data = paises, fill = NA, colour = "grey40", linewidth = 0.1,
inherit.aes = FALSE) +
scale_fill_manual(values = c("Ninguno" = "grey90", "Solo aptitud (ECOCROP)" = "#a6d96a",
"Solo análogo a Palmira" = "#fdae61", "Ambos" = "#1a9641"),
name = NULL) +
coord_sf(expand = FALSE) +
labs(title = "Coincidencia entre aptitud por rangos óptimos y similaridad con Palmira",
x = NULL, y = NULL) +
theme_minimal(base_size = 10) +
theme(legend.position = "bottom")
area_comp <- zonal(area_celda, comparacion, fun = "sum", na.rm = TRUE)
names(area_comp) <- c("categoria", "area_km2")
area_comp %>%
mutate(miles_km2 = round(area_km2 / 1000),
porcentaje = percent(area_km2 / sum(area_km2), accuracy = 0.1)) %>%
select(-area_km2) %>%
kable(caption = "Área en cada categoría de coincidencia")
| categoria | miles_km2 | porcentaje |
|---|---|---|
| Ninguno | 118021 | 83.4% |
| Solo aptitud (ECOCROP) | 19606 | 13.9% |
| Solo análogo a Palmira | 506 | 0.4% |
| Ambos | 3413 | 2.4% |
a_ambos <- area_comp$area_km2[area_comp$categoria == "Ambos"]
a_apto <- sum(area_comp$area_km2[area_comp$categoria %in% c("Ambos", "Solo aptitud (ECOCROP)")])
a_analog <- sum(area_comp$area_km2[area_comp$categoria %in% c("Ambos", "Solo análogo a Palmira")])
coincidencia <- tibble::tibble(
indicador = c("% de las zonas análogas a Palmira que son aptas",
"% de las zonas aptas que son análogas a Palmira",
"Índice de Jaccard (área común / área unida)"),
valor = c(percent(a_ambos / a_analog, 0.1), percent(a_ambos / a_apto, 0.1),
round(a_ambos / (a_apto + a_analog - a_ambos), 2)))
kable(coincidencia)
| indicador | valor |
|---|---|
| % de las zonas análogas a Palmira que son aptas | 87.1% |
| % de las zonas aptas que son análogas a Palmira | 14.8% |
| Índice de Jaccard (área común / área unida) | 0.15 |
Las dos aproximaciones también se pueden comparar celda a celda con la correlación de Spearman entre el índice de aptitud y la similaridad con cada sitio:
valores <- as.data.frame(c(aptitud, similaridad), na.rm = TRUE)
sapply(sitios$sitio, function(s) round(cor(valores[[1]], valores[[s]],
method = "spearman"), 2))
## Palmira Cartago
## 0.52 0.53
## Bajo Calima (Buenaventura)
## 0.55
Antes de interpretar, hay que tener en cuenta qué representan los datos. WorldClim 2.1 es el clima promedio de 1970-2000 en celdas de unos 18 km. El modelo de aptitud solo considera el clima (no el suelo, la pendiente, el riego ni la infraestructura) y supone un cultivo de secano. Por eso los mapas muestran el potencial climático y no dónde se cultiva caña hoy.
Aptitud global (punto 1). Solo el 11,9 % de la superficie continental (16,9 millones de km²) tiene aptitud climática alta para la caña. Otro 4,3 % es moderada y el 5,2 % marginal. El 78,5 % restante no es apto. Las zonas de aptitud alta forman un cinturón en el trópico húmedo: la Amazonía y el centro de Brasil, la cuenca del Congo y el golfo de Guinea, el sudeste asiático e Indonesia, Centroamérica y buena parte de Colombia. El mapa de factor limitante explica por qué el resto del mundo queda por fuera:
Un resultado llamativo es que varios de los grandes productores mundiales de caña salen con poca aptitud alta. La India tiene solo el 9,3 % de su territorio en esa clase, China el 0,3 %, Australia el 1,5 %, y Pakistán y Sudáfrica el 0 %. En casi todos los casos el factor limitante es la lluvia. Esto no es un error del mapa: esos países cultivan la caña con riego, y el modelo de secano no lo tiene en cuenta.
Países con alto potencial (punto 2). Según el área con aptitud alta, los tres primeros son:
Sitios del Valle del Cauca (punto 3). Las series mensuales muestran lo que se espera en un clima ecuatorial:
Palmira (22,4 °C; 1.484 mm al año) y Cartago (23,1 °C; 1.896 mm) tienen aptitud alta (0,82 y 0,90). Aun así, Palmira queda un poco por debajo del óptimo de temperatura (24 °C) y de lluvia (1.500 mm). Esto concuerda con que la zona azucarera del valle complementa con riego en las temporadas secas. Bajo Calima es más cálido (26,1 °C), pero recibe 5.631 mm al año, más que el máximo absoluto de 5.000 mm, por lo que su aptitud es 0. Hay que recordar que una celda de 18 km mezcla el fondo del valle con el piedemonte, por eso la temperatura de Palmira sale un poco más baja que la medida en las estaciones del valle.
Similaridad climática (punto 4). Los mapas continuos muestran el gran contraste entre los climas tropicales húmedos (similaridad alta) y los desiertos y zonas polares (baja). Las diferencias entre sitios se ven en las zonas análogas (el 2 % más similar):
Esto confirma que la métrica distingue bien un clima de valle interandino de uno de selva pluvial, aunque estén a menos de 80 km de distancia.
Comparación de las dos aproximaciones (punto 5).