R包gbmt轨迹模型指标计算的函数

并行

r 复制代码
# Setup the backend cluster
cl <- makeCluster(detectCores()-1) # you can reduce this if needed
registerDoParallel(cl)

# Define the function to process
get_metrics <- function(df, variables){
  foreach(ng=rep(2:6,2), d=c(rep(1,5), rep(2,5))) %dopar% {
    set.seed(42)
    res <- gbmt::gbmt(x.names=variables, unit="hicid", time="time", 
                d=d, ng=ng, 
                data=df, 
                nstart = 10)
    res
  }
  
}
r 复制代码
calculate_OCC <- function(model) {
  num_groups <- length(model$appa)

  # Si el modelo tiene solo un grupo, devolver 999.0
  if (num_groups == 1) {
    return(Inf)
  }

  # Encontrar el grupo asignado para cada observación según la máxima probabilidad posterior
  assigned_groups <- apply(model$posterior, 1, which.max)

  # Calcular la proporción de población estimada para cada grupo
  population_proportion <- table(assigned_groups) / length(assigned_groups)

  # Calcular el OCC para cada grupo
  OCC <- sapply(1:num_groups, function(j) {
    # Si APPA es igual a 1, devolver 999.0
    if (model$appa[j] == 1) {
      return(999.0)
    }

    odds_correct <- model$appa[j] / (1 - model$appa[j])
    odds_random <- population_proportion[j] / (1 - population_proportion[j])
    return(odds_correct / odds_random)
  })

  # Devolver el promedio de OCC
  return(mean(OCC))
}

calculate_mismatch <- function(model) {
  # Calcular las proporciones de cada grupo estimadas por el modelo
  group_proportions <- model$prior

  # Calcular las proporciones reales de cada grupo en la muestra
  real_proportions <- table(factor(model$assign, levels = 1:length(group_proportions))) / length(model$assign)

  # Calcular el Mismatch para cada grupo
  mismatch <- group_proportions - real_proportions

  # Devolver el promedio de los valores absolutos de Mismatch de todos los grupos
  return(mean(abs(mismatch)))
}

calculate_SD_GMP <- function(model) {
  # Inicializar un vector para almacenar el SD-GMP para cada grupo
  sd_gmp <- numeric(length(model$prior))

  # Calcular el SD-GMP para cada grupo
  for (j in 1:length(model$prior)) {
    assigned_indices <- which(model$assign == j)
    group_probs <- model$posterior[assigned_indices, j]
    sd_gmp[j] <- sd(group_probs)
  }

  # Devolver la media de los SD-GMP de todos los grupos
  return(mean(sd_gmp))
}
r 复制代码
---
title: "Trajectory Analysis"
author:
  - name: Brian Peña-Calero
    email: [email protected]
    affiliation: Innovar, UPCH
date: "`r Sys.Date()`"
output: 
  html_document: 
    toc: yes
    toc_depth: 3
    toc_float: yes
    number_sections: yes
    code_folding: show
    code_download: yes
    theme: 
      bootswatch: flatly
    highlight: kate
    highlight_downlit: true
editor_options: 
  chunk_output_type: console
---


```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE)

Carga de paquetes

{r} 复制代码
library(tidyverse)
library(gbmt)
library(multidplyr)

Crear carpetas de trabajo:

{r} 复制代码
fs::dir_create("01_data/raw/")
fs::dir_create("01_data/processed/")

Importar datos

{r} 复制代码
library(sf)
salid1_pubsalid1 <- readr::read_csv("01_data/raw/SALID1_PUBSALID1.csv")
gdb_path <- "01_data/raw/Data Request MS242_09222023/MS242_L1.gdb"
gdb_salurbal <- st_layers(gdb_path)

cities_l1ad <- st_read(
  gdb_path,
  gdb_salurbal$name[[1]]
) %>%
  as_tibble() %>%
  st_as_sf() %>%
  left_join(
    salid1_pubsalid1
  ) %>%
  relocate(SALID1, .after = PUBSALID1)

cities_l1ad_noislands <- st_read(
  gdb_path,
  gdb_salurbal$name[[2]]
) %>%
  as_tibble() %>%
  st_as_sf() %>%
  left_join(
    salid1_pubsalid1
  ) %>%
  relocate(SALID1, .after = PUBSALID1)

Datos Crecimiento Urbano

Los datos de crecimiento urbano actuales solo tienen la columna ISO2 que identifica al país, y una codificación propia SALID1 para las ciudades, que es con lo que necesitamos realizar el match, para tener los datos de ciudad en cuanto a su crecimiento poblacional y a la vez en cuánto a su crecimiento urbano.

{r} 复制代码
bec_backcast_l1ux <- read_csv("01_data/raw/BEC_BACKCAST_L1UX_20230302.csv")

Se adiciona la columna SALID0 calculado como los 3 primeros dígitos de SALID1. Además se agrega PUBSALID1.

{r} 复制代码
bec_backcast_l1ux <- bec_backcast_l1ux %>%
  mutate(
    SALID0 = str_sub(SALID1, 1, 3),
    .before = SALID1
  ) %>%
  left_join(
    salid1_pubsalid1
  ) %>%
  relocate(PUBSALID1, .before = SALID1)

Datos Poblacionales

Para el caso de los datos poblacionales, se tiene la identificación en nombre del país y la ciudad. Se agrega el identificador ISO2, para fines de matching.

{r} 复制代码
population <- st_read(
  gdb_path,
  gdb_salurbal$name[[3]]
) %>%
  as_tibble() %>%
  left_join(
    salid1_pubsalid1
  ) %>%
  relocate(SALID1, .after = PUBSALID1) %>%
  drop_na(PUBSALID1)
{r} 复制代码
population <- population %>%
  mutate(
    ISO2 = case_match(
      country,
      "argentina" ~ "AR",
      "brazil" ~ "BR",
      "chile" ~ "CL",
      "colombia" ~ "CO",
      "costa rica" ~ "CR",
      "el salvador" ~ "SV",
      "guatemala" ~ "GT",
      "mexico" ~ "MX",
      "nicaragua" ~ "NI",
      "panama" ~ "PA",
      "peru" ~ "PE"
    ),
    .before = country
  ) %>%
  left_join(
    bec_backcast_l1ux %>%
      select(ISO2, SALID0) %>%
      distinct()
  ) %>%
  relocate(ISO2, SALID0) %>%
  arrange(SALID0)
population

Se aplica una pequeña corrección a los datos numéricos poblacionales que contienen caracteres como - y " " (espacios) para representar valores perdidos.

{r} 复制代码
population <- population %>%
  mutate(
    across(
      F1945:F2022,
      as.numeric
    )
  )

population

Se crea este objeto temporal a partir del formato en el objeto population que contiene la información de ISO2, SALID0, SALID1, country, City. Aquí debería reemplazarse con el una data que brinde una información equivalente.

{r 复制代码
city_info <- population %>%
  select(
    ISO2:City, PUBSALID1:SALID1
  )
{r 复制代码
saveRDS(
  city_info,
  file = "01_data/processed/city_info.rds"
)

#city_info <- readRDS("01_data/processed/city_info.rds")

Imputación de datos

Debido a que hay una gran cantidad de valores perdidos a través de los años en distintas ciudades, se realiza un proceso de imputación de datos tanto para los de crecimiento urbano como datos poblacionales.

El análisis de imputación se realiza para cada ciudad, por lo que se usará el enfoque de tidy nested para organizar los datos de tal manera que cada fila sea una ciudad; y su columna, sea un conjunto de datos que contenga la información de interés a través de los años. Para el proceso de imputación se usará el paquete imputeTS wrappeado en la función safely (paquete purrr) para evitar la detención de errores debido a problemas de convergencia o insuficiencia de datos en algunas ciudades.

{r} 复制代码
library(imputeTS)
na_kalman_safe <- safely(.f = na_kalman)

Crecimiento Urbano

Los datos de crecimiento urbano proporcionado tienen información cada 5 años, por lo que se usará la misma técnica de imputación para tener información anual. Para ello primero se completará los años faltantes en el data.frame:

{r} 复制代码
bectuareal_tidy <- bec_backcast_l1ux %>%
  distinct(PUBSALID1) %>%
  crossing(YEAR = 1985:2015) %>%
  left_join(
    bec_backcast_l1ux %>%
      select(PUBSALID1, YEAR, BECTUAREAL1UX)
  ) %>%
  janitor::clean_names()

Ahora se procede a crear el formato nest:

{r} 复制代码
bectuareal_imp <- bectuareal_tidy %>%
  group_nest(pubsalid1) %>%
  mutate(
    ts_data = map(
      data,
      ~ ts(.x$bectuareal1ux, 1985, 2015)
    ),
    kalman_data = map(
      ts_data,
      na_kalman_safe
    )
  )

En este caso también se tiene algunos errores (1)

{r} 复制代码
bectuareal_imp_errors <- bectuareal_imp %>%
  filter(map_lgl(.$kalman_data, ~ !is.null(.x$error)))

Aunque en la mayoría de ciudades se logra imputar (370 ciudades).

{r} 复制代码
bectuareal_imp_clean <- bectuareal_imp %>% 
  filter(map_lgl(.$kalman_data, ~ is.null(.x$error))) %>% 
  mutate(kalman_data = map(kalman_data, 'result'))

Finalmente, se añadirá la información sobre la cantidad original de crecimiento urbano existente para fines de futuras validaciones:

{r} 复制代码
bectuareal_imp_merge <- bectuareal_imp_clean %>%
  mutate(
    kalman_data = map(
      kalman_data,
      ~ as_tibble(.x) %>%
        mutate(
          year = 1985:2015
        )
    ),
    data_imputation = map2(
      data, kalman_data,
      ~ .x %>%
        left_join(.y,
          by = join_by(year)
        )
    )
  )

bectuareal_imp_merge <- bectuareal_imp_merge %>% 
  select(pubsalid1, data_imputation) %>% 
  unnest(cols = c(data_imputation)) %>% 
  rename(bectuareal1ux_imp = x)

Datos poblacionales

Necesitamos ordenar los datos y darle un formato nested. Observamos que los datos poblacionales con los que se trabaja tiene 648 ciudades.

{r} 复制代码
population_nested <- population %>%
  janitor::clean_names() %>%
  rename_with(
    ~ str_replace(., "f", "x"),
    f1945:f2015
  ) %>%
  select(pubsalid1, x1945:x2015) %>%
  pivot_longer(
    cols = x1945:x2015,
    names_to = "year",
    values_to = "population"
  ) %>%
  group_nest(pubsalid1)
population_nested

Se tiene una función que verifica una desviación máxima de valores poblacionales en una ciudad en particular a partir de su mediana a lo largo de los años. Por defecto, se usa el criterio de 20% máximo de variación. En caso algún valor poblacional supere este valor, se ingresará un NA en lugar del valor reportado. Así, la imputación podrá también estimar estos valores anómalos.

{r} 复制代码
fix_population <- function(data, desviacion_max = 0.2) {
  data_wo_na <- na.omit(data)
  
  if (length(data_wo_na) < 2) {
    return(data)
  }
  
  median_population <- median(data_wo_na)
  
  upper_limit <- median_population * (1 + desviacion_max)
  lower_limit <- median_population * (1 - desviacion_max)
  
  data_fix <- ifelse(
    !is.na(data) & (data > upper_limit | data < lower_limit), 
    NA, 
    data
  )
  return(data_fix)
}
{r} 复制代码
population_imp <- population_nested %>%
  mutate(
    # Aplicación de la corrección
    pop_fix = map(
      data, 
      ~ fix_population(.x$population) %>%
        enframe(
          name = "year",
          value = "population"
        ) %>%
        mutate(
          year = paste0("x", 1945:2015)
        )
    ),
    # Transformar a objeto `time series`
    ts_pop_fix = map(
      pop_fix,
      ~ ts(.x$population, 1945, 2015) %>%
        log()
    ),
    # Aplicación de la imputación
    kalman_pop_fix = map(
      ts_pop_fix, 
      na_kalman_safe
    )
  )

Debido a que algunas ciudades tienen valores perdidos en todos los años de análisis, no en todos los casos la imputación es factible. En este caso hubo un total de 208 ciudades con problemas de imputación.

{r} 复制代码
population_imp_errors <- population_imp %>%
  filter(map_lgl(.$kalman_pop_fix, ~ !is.null(.x$error)))

En tanto, la cantidad de ciudades con las que se podrá trabajar con información poblacional será de 440.

{r} 复制代码
population_imp_clean <- population_imp %>%
  filter(map_lgl(.$kalman_pop_fix, ~ is.null(.x$error))) %>%
  mutate(kalman_pop_fix = map(kalman_pop_fix, "result"))

Finalmente, se añadirá la información sobre la cantidad original de dato poblacional existente para fines de futuras validaciones:

{r} 复制代码
population_imp_merge <- population_imp_clean %>%
  mutate(
    kalman_pop_fix = map(
      kalman_pop_fix,
      ~ as_tibble(.x) %>%
        mutate(
          x = exp(x), 
          year = paste0("x", 1945:2015)
        )
    ),
    data_imputation = map2(
      pop_fix, kalman_pop_fix,
      ~ .x %>%
        left_join(.y,
          by = join_by(year)
        )
    )
  )

population_imp_merge <- population_imp_merge %>%
  select(pubsalid1, data_imputation) %>%
  unnest(cols = c(data_imputation)) %>%
  rename(population_imp = x) %>%
  mutate(
    year = str_remove(year, "x"),
    year = as.numeric(year)
  )

population_imp_merge

Unificación de datos

En este punto, ya se tiene los datos poblacionales y de crecimiento urbano limpios y ordenados. Se realiza la unificación en base al código salid1 que deberían tener ambos objetos.

Se usa la función full_join para tener la posibilidad de analizar los datos poblacionales y de crecimiento juntos y separados.

{r} 复制代码
# bectuareal_imp_merge %>%
# filter(year == 1985)

# pubsalid1_nopopulation <- anti_join(
#     bectuareal_imp_merge,
#     population_imp_merge,
#     by = join_by(pubsalid1, year)
#   ) %>%
#   filter(year == 1985) %>%
#   pull(pubsalid1)

# pubsalid1_nobectu <- anti_join(
#   population_imp_merge,
#   bectuareal_imp_merge,
#   by = join_by(pubsalid1, year)
# ) %>%
#   filter(year == 1985) %>%
#   pull(pubsalid1)

# anti_join(
#   bectuareal_imp_merge,
#   population_imp_merge,
#   by = join_by(pubsalid1, year)
# ) %>%
# filter(year == 1985)
{r} 复制代码
pop_bectuareal <- full_join(
  population_imp_merge,
  bectuareal_imp_merge,
  by = join_by(pubsalid1, year)
)

pop_bectuareal %>%
filter(pubsalid1 == 514651)

Grouped-based Model Trajectory (GBMT)

Formateo de datos

Se crearán 3 objetos:

  • pop_bectu_gbmt_df: Contiene a las coincidencias completas de datos poblacionales y crecimiento poblacional en las ciudades.
  • pop_gbmt_df: Contiene solo a los valores imputados completos de las poblaciones en ciudades.
  • bectu_gbmt_df: Contiene solo a los valores imputados completos de los crecimientos poblacionales en ciudades.
{r} 复制代码
pop_bectu_gbmt_df <- pop_bectuareal %>%
  select(
    pubsalid1, year,
    population_imp, bectuareal1ux_imp
  ) %>%
  drop_na()

pop_gbmt_df <- pop_bectuareal %>%
  select(pubsalid1, year, population_imp) %>%
  drop_na(population_imp)

bectu_gbmt_df <- pop_bectuareal %>%
  select(pubsalid1, year, bectuareal1ux_imp) %>%
  drop_na(bectuareal1ux_imp) 

Estimación

Configuración de uso multinúcleo para el análisis:

{r} 复制代码
cluster <- new_cluster(parallel::detectCores())
cluster_library(cluster, c("dplyr", "purrr", "gbmt"))
gbmt_safe <- safely(.f = gbmt)
cluster_copy(cluster, "gbmt_safe")

Se crea una pseudo-matriz que contiene los parámetros de análisis y los conjuntos de datos para analizar en cada fila.

{r} 复制代码
gbmt_matrix <- expand_grid(
  d = 2,
  ng = 1:10,
  scale = c(2, 4),
  tibble(
    data_gbmt = list(
      pop_bectu_gbmt_df,
      pop_gbmt_df,
      bectu_gbmt_df
    ),
    x_names = list(
      c("population_imp", "bectuareal1ux_imp"),
      c("population_imp"),
      c("bectuareal1ux_imp")
    )
  )  
) 

Este siguiente bloque realiza las estimaciones especificadas arriba usando todos los núcleos del procesador.

{r} 复制代码
gbmt_outputs <- gbmt_matrix %>%
  partition(cluster) %>%
  mutate(
    gbmt_out = pmap(
      list(
        data_gbmt, x_names,
        d, ng, scale
      ), 
      ~ gbmt_safe(
        x.names = ..2,
        unit = "pubsalid1",
        time = "year",
        data = as.data.frame(..1),
        d = ..3,
        ng = ..4,
        scaling = ..5
      )
    )
  )

gbmt_collect <- gbmt_outputs %>%
  collect()
{r 复制代码
fs::dir_create("01_data/processed/")

saveRDS(gbmt_collect,
  file = "01_data/processed/gbmt_collect_pop_bectu.rds"
)

#gbmt_collect <- readRDS("01_data/processed/gbmt_collect_pop_bectu.rds")
{r} 复制代码
gbmt_collect_errors <- gbmt_collect %>% 
  filter(map_lgl(.$gbmt_out, ~ !is.null(.x$error)))

gbmt_collect_clean <- gbmt_collect %>% 
  filter(map_lgl(.$gbmt_out, ~ is.null(.x$error))) %>% 
  mutate(gbmt_out = map(gbmt_out, 'result'))

Índices de validación

{r} 复制代码
source("fit-index-gbmt.r")
{r} 复制代码
gbmt_fit_total <- gbmt_collect_clean %>%
  mutate(
    AIC = map_dbl(gbmt_out, AIC),
    BIC = map_dbl(gbmt_out, BIC),
    L = map_dbl(gbmt_out, logLik),
    APPA = map_dbl(gbmt_out, ~ mean(.$appa)),
    Mismatch = map_dbl(gbmt_out, calculate_mismatch),
    SD_GMP = map_dbl(gbmt_out, calculate_SD_GMP),
    OCC = map_dbl(gbmt_out, calculate_OCC),
    smallest_group = map2_dbl(
      gbmt_out, ng,
      ~ min(table(factor(.x$assign, levels = 1:.y))) /
        sum(table(factor(.x$assign, levels = 1:.y)))
    ),
    scale_name = case_match(
      scale,
      2 ~ "standardization",
      4 ~ "logarithmic"
    ),
    city_info = list(city_info)
  ) %>%
  relocate(scale_name, .after = "scale") %>%
  arrange(ng, d, scale)

Exportar la información sobre los índices de ajuste:

{r} 复制代码
fs::dir_create("02_output/tables/")

gbmt_fit_total %>%
  select(d:scale, scale_name, x_names, 
         AIC:smallest_group) %>%
  openxlsx::write.xlsx(
    file = "02_output/tables/fit_measures_models_gbmt.xlsx"
  )

Exportar el output de las regresiones:

{r} 复制代码
# Esta función procesa un único conjunto de regresiones
process_regressions <- function(regressions, ng, scale) {
  result_list <- list()

  for (group in names(regressions)) {
    for (var_name in names(regressions[[group]])) {
      tmp <- broom::tidy(regressions[[group]][[var_name]])
      tmp$variable <- var_name
      tmp$ng <- ng
      tmp$scale <- scale
      tmp$group <- as.integer(group)

      result_list[[length(result_list) + 1]] <- tmp
    }
  }

  bind_rows(result_list)
}

# Procesa todas las regresiones en gbmt_fit_total$gbmt_out
results_all <- lapply(1:length(gbmt_fit_total$gbmt_out), function(i) {
  process_regressions(
    regressions = gbmt_fit_total$gbmt_out[[i]]$reg,
    ng = gbmt_fit_total$ng[i],
    scale = gbmt_fit_total$scale_name[i]
  )
})

# Convierte los resultados en un formato más manejable
resultados_tidy <- bind_rows(results_all)
{r 复制代码
saveRDS(
  resultados_tidy, 
  file = "02_output/tables/resultados_tidy_reg.rds"
)

Re-ingreso de información

{r} 复制代码
gbmt_fit_total <- gbmt_fit_total %>%
  mutate(
    data_gbmt_extract = map(
      gbmt_out,
      ~ .x$data.norm %>%
        rename_with(
          ~ paste0(., "_norm"),
          contains("_imp")
        ) %>%
        left_join(
          .x$data.orig,
          by = join_by(pubsalid1, year)
        ) %>%
        as_tibble()
    ),
    assign_list = map(
      gbmt_out,
      ~ enframe(.x$assign.list) %>%
      unnest(cols = c(value)) %>%
      mutate(
        pubsalid1 = as.factor(value)
      ) %>%
      select(pubsalid1, group = name)
    )
  )

En este punto se unificará la información de los datos poblacionales, crecimiento urbano, grupo al que pertenece debido al análisis gbmt y la infiormación de las ciudades que está almacenada en city_info. Si esa información es incompleta vamos a tener problemas de NA's.

{r} 复制代码
gbmt_fit_final <- gbmt_fit_total %>%
  mutate(
    gbmt_fit_total = pmap(
      list(
        data_gbmt_extract,
        assign_list,
        city_info
      ),
      ~ left_join(
        ..1,
        ..2,
        by = join_by(pubsalid1)
      ) %>%
      # mutate(
      #   SALID0 = str_sub(salid1, 1, 3)
      # ) %>%
      left_join(
        ..3 %>%
          rename(pubsalid1 = PUBSALID1) %>%
          mutate(pubsalid1 = factor(pubsalid1)),
        by = join_by(pubsalid1)
      )
    )
  )
{r} 复制代码
saveRDS(gbmt_fit_final,
  file = "01_data/processed/gbmt_fit_final.rds"
)

#gbmt_fit_final <- readRDS("01_data/processed/gbmt_fit_final.rds")

La estructura en este punto estaría quedando así:

Generación de Gráficos

Gráficos de barra y líneas

{r} 复制代码
library(patchwork)

gg_line_plot <- function(datos, y_variable, y_label, type = "standardized") {
  gg <- ggplot(datos, aes(x = year, y = !!sym(y_variable))) +
    geom_line(aes(group = pubsalid1), color = "grey80") +
    geom_smooth(method = "lm", formula = y ~ I(x^1) + I(x^2)) +
    scale_x_continuous(breaks = c(seq(1945, 2015, 15), 2015)) +
    labs(x = NULL, y = y_label) +
    theme_bw()

  if (type == "standardized") {
    gg +
      facet_wrap(vars(group))
  } else if (type == "non-standardized") {
    gg +
      scale_y_continuous(
        labels = scales::label_number()
      ) +
      facet_wrap(vars(group), scales = "free")
  }
}

gg_stacked_bar <- function(datos) {
  datos %>%
    count(country, group) %>%
    group_by(country) %>%
    mutate(
      percent = n / sum(n),
      country = str_to_title(country)
    ) %>%
    ungroup() %>%
    ggplot(aes(x = country, y = percent)) +
    geom_col(aes(fill = group)) +
    innovar::scale_fill_innova("ccvi") +
    labs(
      fill = "Cluster",
      y = NULL,
      x = NULL
    ) +
    geom_label(
      aes(
        group = group,
        label = scales::percent(percent, accuracy = 1)
      ),
      position = position_stack(vjust = 0.5)
    ) +
    scale_y_continuous(
      labels = scales::percent_format()
    ) +
    theme_minimal() +
    theme(axis.text.x = element_text(angle = 20, hjust = 1))
}

gg_bar <- function(datos, year_filter = 2015) {
  datos %>%
    filter(year == year_filter) %>%
    count(country, group) %>%
    group_by(country) %>%
    mutate(
      percent = n / sum(n),
      country = str_to_title(country)
    ) %>%
    ungroup() %>%
    ggplot(aes(x = country, y = n)) +
    geom_col(aes(fill = group)) +
    innovar::scale_fill_innova("ccvi") +
    labs(
      fill = "Cluster",
      y = NULL,
      x = NULL
    ) +
    geom_label(
      aes(
        group = group,
        label = n
      ),
      position = position_stack(vjust = 0.5)
    ) +
    theme_minimal() +
    theme(axis.text.x = element_text(angle = 20, hjust = 1))
}

Generación de los gráficos de línea estandarizados, sin estandarizar, stacked bar y barras (frecuencia).

{r} 复制代码
gbmt_fit_plots <- gbmt_fit_final %>%
  mutate(
    line_plot_std = map2(
      gbmt_fit_total,
      x_names,
      ~ {
        if (all(.y == c("population_imp", "bectuareal1ux_imp"))) {
          p1 <- gg_line_plot(.x, "population_imp_norm", "Standardized Population")
          p2 <- gg_line_plot(.x, "bectuareal1ux_imp_norm", "Standardized Bectuareal1ux")
          p1 / p2
        } else if (all(.y == c("population_imp"))) {
          gg_line_plot(.x, "population_imp_norm", "Standardized Population")
        } else {
          gg_line_plot(.x, "bectuareal1ux_imp_norm", "Standardized Bectuareal1ux")
        }
      }
    ),
    line_plot = map2(
      gbmt_fit_total,
      x_names,
      ~ {
        if (all(.y == c("population_imp", "bectuareal1ux_imp"))) {
          p1 <- gg_line_plot(.x, "population_imp", "Population", "non-standardized")
          p2 <- gg_line_plot(.x, "bectuareal1ux_imp", "Bectuareal1ux", "non-standardized")
          p1 / p2
        } else if (all(.y == c("population_imp"))) {
          gg_line_plot(.x, "population_imp", "Population", "non-standardized")
        } else {
          gg_line_plot(.x, "bectuareal1ux_imp", "Bectuareal1ux", "non-standardized")
        }
      }
    ),
    stacked_bar_plot = map(
      gbmt_fit_total,
      gg_stacked_bar
    ),
    bar_plot = map(
      gbmt_fit_total,
      gg_bar
    )
  )

Finalmente guardamos los gráficos generados:

{r} 复制代码
# Función para guardar una lista de gráficos
save_plots <- function(plots, prefix, x_names, cluster, scale_name = NULL, std = FALSE) {
  walk2(plots, seq_along(plots), function(plot, i) {
    # Crear el nombre base con las variables
    var_names <- str_replace_all(x_names[[i]], "_imp", "")
    var_string <- str_c(var_names, collapse = "-")

    # Crear el nombre del archivo usando el número de cluster
    filename <- paste0(prefix, "-", cluster[[i]], "-", var_string)

    if (!is.null(scale_name[[i]])) {
      filename <- paste0(filename, "-", scale_name[[i]])
    }

    if (std) {
      filename <- paste0(filename, "-std")
    }

    filename <- paste0(filename, ".png")

    ggsave(
      filename,
      plot,
      dpi = 300,
      bg = "white"
    )
  })
}

fs::dir_create("02_output/plots/line_plots_std/")
fs::dir_create("02_output/plots/line_plots/")
fs::dir_create("02_output/plots/stacked_bar_plots/")
fs::dir_create("02_output/plots/bar_plots/")

# Aplicar la función a cada una de las últimas cuatro columnas
save_plots(
  gbmt_fit_plots$line_plot_std, 
  "02_output/plots/line_plots_std/lineplot",
  gbmt_fit_final$x_names, 
  gbmt_fit_final$ng, 
  gbmt_fit_final$scale_name, 
  std = TRUE
)

save_plots(
  gbmt_fit_plots$line_plot, 
  "02_output/plots/line_plots/lineplot", 
  gbmt_fit_final$x_names, 
  gbmt_fit_final$ng, 
  gbmt_fit_final$scale_name
)

save_plots(
  gbmt_fit_plots$stacked_bar_plot,
   "02_output/plots/stacked_bar_plots/stackedbarplot", 
   gbmt_fit_final$x_names, 
   gbmt_fit_final$ng,
   gbmt_fit_final$scale_name
)

save_plots(
  gbmt_fit_plots$bar_plot,
  "02_output/plots/bar_plots/barplot",
  gbmt_fit_final$x_names, 
  gbmt_fit_final$ng,
  gbmt_fit_final$scale_name
)

Generación de mapas

En esta sección se unificarán las geometrías de las ciudades con la información de la columna City que ahora mismo viene de los datos poblacionales. Actualmente se está usando geometrías proporcionadas por geodata::gadm() sin embargo, no se está teniendo coincidencias adecuadas en las ciudades. Esto debería modificarse de acuerdo a como tengan las geometrías almacenadas.

{r} 复制代码
cities_l1ad_noislands %>% plot()

cities_l1ad_noislands 

cities_l1ad %>% plot()
{r} 复制代码
library(sf)
{r} 复制代码
get_shp <- function(ISO2) {
  # Obtener shp_1
  shp_0 <- geodata::gadm(
    ISO2,
    path = "01_data/raw/shp/",
    level = 0
  ) %>%
    st_as_sf()
  
  # Obtener shp_1
  shp_1 <- geodata::gadm(
    ISO2,
    path = "01_data/raw/shp/",
    level = 1
  ) %>%
    st_as_sf()

  # Obtener shp_2 y aplicar las transformaciones
  shp_2 <- cities_l1ad_noislands %>%
    filter(COUNTRYAB == ISO2) %>%
    janitor::clean_names()

  # shp_2 <- geodata::gadm(
  #   ISO2,
  #   path = "01_data/raw/shp/",
  #   level = 2
  # ) %>%
  #   st_as_sf() %>%
  #   mutate(
  #     NAME_2 = str_to_lower(NAME_2),
  #     NAME_2 = case_when(
  #       str_detect(NAME_2, "capital") ~ str_to_lower(NAME_1),
  #       TRUE ~ NAME_2
  #     )
  #   )

  # Devolver la lista con shp_1 y shp_2
  list(
    shp_0 = shp_0,
    shp_1 = shp_1,
    shp_2 = shp_2
  )
}


shp_list <- unique(gbmt_fit_final$gbmt_fit_total[[1]]$ISO2) %>%
  append(
    c("GUY", "SUR", "BOL", "PRY",
      "URY", "ECU", "BLZ", "HND",
      "TTO", "VEN")
  ) %>% 
  set_names() %>%
  map(get_shp)

Creamos una función para unr los datos y los shapefiles

{r} 复制代码
join_data_shp <- function(data, shp_list) {
  # Proceso de unión con shapefiles
  data %>%
    filter(year == 2015) %>%
    mutate(
      pubsalid1 = as.numeric(as.character(pubsalid1)),
      # City = str_to_lower(City),
      group = factor(group)
    ) %>%
    group_nest(ISO2, country) %>%
    drop_na(ISO2) %>%
    mutate(
      country = str_to_title(country),
      shp_1 = map(ISO2, ~ shp_list[[.x]]$shp_1),
      shp_2 = map(ISO2, ~ shp_list[[.x]]$shp_2),
      shp_inner = map2(
        data,
        shp_2,
        ~ inner_join(
          .x,
          .y,
          by = join_by(pubsalid1)
        ) %>%
          st_as_sf()
      )
    )
}

El cual usaremos a continuación:

{r} 复制代码
gbmt_shp <- map(
  gbmt_fit_final$gbmt_fit_total,
  ~ join_data_shp(.x, shp_list)
)

Se espera que gbmt_shp sea una lista de n tamaño cantidad de modelos se hayan estimado. Por ej. gbmt_shp[[1]] tendría la siguiente estructura:

Y ahora procederemos con la función encargada de la generación de los mapas que tienen a su vez correcciones específicas con fines de visualización en algunos países:

{r} 复制代码
plot_country_map <- function(country_data) {
  p <- ggplot(country_data$shp_inner[[1]]) +
    geom_sf(
      data = country_data$shp_1[[1]],
      lwd = 0.10
    ) +
    geom_sf(aes(fill = group)) +
    scale_fill_discrete(drop = FALSE) +
    labs(
      title = country_data$country,
      fill = "Cluster"
    ) +
    theme_minimal()

  # Si el país es Chile, ajustar los límites del gráfico
  if (country_data$country == "Chile") {
    xlim <- c(-90, -55) # Longitud
    ylim <- c(-56, -17) # Latitud
    p <- p + coord_sf(xlim = xlim, ylim = ylim) +
      scale_x_continuous(breaks = seq(-90, -50, by = 10))
  } else if (country_data$country == "Costa Rica") {
    xlim <- c(-86, -82.5) # Longitud
    ylim <- c(11.5, 8) # Latitud
    p <- p + coord_sf(xlim = xlim, ylim = ylim) +
      scale_x_continuous(breaks = seq(-86, -80, by = 10))
  } else if (country_data$country == "Colombia") {
    xlim <- c(-80, -65) # Longitud
    ylim <- c(15, -5) # Latitud
    p <- p + coord_sf(xlim = xlim, ylim = ylim) +
      scale_x_continuous(breaks = seq(-80, -65, by = 5))
  } else if (country_data$country == "Argentina") {
    xlim <- c(-82, -45) # Longitud
    p <- p + coord_sf(xlim = xlim) +
      scale_x_continuous(breaks = seq(-80, -40, by = 10))
  } else if (country_data$country == "Peru") {
    xlim <- c(-82, -68) # Longitud
    p <- p + coord_sf(xlim = xlim) +
      scale_x_continuous(breaks = seq(-80, -68, by = 4))
  }

  return(p)
}

# Función para mapear sobre los países de un modelo específico
plot_model_countries <- function(model_data) {
  map(
    model_data$ISO2,
    ~ plot_country_map(
      subset(model_data, ISO2 == .)
    )
  )
}

Y se aplica las funciones para cada uno de los modelos de gbmt_shp:

{r} 复制代码
maps_models_countries <- map(
  gbmt_shp,
  plot_model_countries
)

Finalmente con la ayuda del paquete patchwork se unifican los gráficos por modelos:

{r} 复制代码
final_plots <- list()

# Iterar sobre cada modelo en maps_models_countries
for (j in 1:length(maps_models_countries)) {
  # Obtener los gráficos del modelo actual
  current_graphs <- maps_models_countries[[j]]

  # Combinar los gráficos del modelo actual
  combined_plot <- current_graphs[[1]]
  for (i in 2:length(current_graphs)) {
    combined_plot <- combined_plot + current_graphs[[i]]
  }

  # Especificar el diseño y unificar las leyendas
  final_plot <- combined_plot +
    plot_layout(
      ncol = 4,
      nrow = 3,
      guides = "collect",
      widths = c(1, 1, 1, 1),
      heights = c(1, 1, 1)
    )

  # Almacenar el final_plot en la lista final_plots
  final_plots[[j]] <- final_plot
}

Se crea una función para guardar los gráficos de los modelos en función del nombre de su análisis:

{r} 复制代码
# Función para guardar cada gráfico
save_maps <- function(plot, index, cluster, x_names, scale_name) {
  # Crear el nombre base con las variables
  var_names <- str_replace_all(x_names[[index]], "_imp", "")
  var_string <- str_c(var_names, collapse = "-")


  filename <- paste0(
    "02_output/plots/maps/mapa-cluster-",
    cluster[[index]],
    "-",
    var_string,
    "-",
    scale_name[[index]],
    ".png"
  )

  ggsave(
    filename,
    plot,
    dpi = 600,
    bg = "white",
    width = 16,
    height = 8,
    device = grDevices::png
  )
}

Y por último, aplicamos la función a cada uno de los gráficos de final_plots:

{r} 复制代码
fs::dir_create("02_output/plots/maps/")

walk2(
  final_plots,
  seq_along(final_plots),
  save_maps,
  cluster = gbmt_fit_final$ng,
  x_names = gbmt_fit_final$x_names,
  scale_name = gbmt_fit_final$scale_name
)

Mapa con puntos

{r} 复制代码
join_data_shp2 <- function(data, shp_list) {
  # Proceso de unión con shapefiles
  data %>%
    mutate(
      pubsalid1 = as.numeric(as.character(pubsalid1)),
      # City = str_to_lower(City),
      group = factor(group)
    ) %>%
    group_nest(ISO2, country) %>%
    drop_na(ISO2) %>%
    mutate(
      country = str_to_title(country),
      shp_1 = map(ISO2, ~ shp_list[[.x]]$shp_1),
      shp_2 = map(ISO2, ~ shp_list[[.x]]$shp_2),
      shp_inner = map2(
        data,
        shp_2,
        ~ inner_join(
          .x,
          .y,
          by = join_by(pubsalid1)
        ) %>%
          st_as_sf()
      )
    )
}
{r} 复制代码
gbmt_interest <- gbmt_fit_final %>%
  filter(
    scale_name == "logarithmic",
    ng == 4
  ) %>%
  slice(1)

# Calcula el delta año tras año para cada ciudad
gbmt_interest$gbmt_fit_total[[1]] <- gbmt_interest$gbmt_fit_total[[1]] %>%
  arrange(pubsalid1, year) %>%
  group_by(ISO2, country, group, pubsalid1) %>%
  mutate(
    perc_population_imp = (population_imp - lag(population_imp))/lag(population_imp),
    perc_bectuareal1ux_imp = (bectuareal1ux_imp - lag(bectuareal1ux_imp))/lag(bectuareal1ux_imp),
    perc_population_imp_norm = (population_imp_norm - lag(population_imp_norm))/lag(population_imp_norm),
    perc_bectuareal1ux_imp_norm = (bectuareal1ux_imp_norm - lag(bectuareal1ux_imp_norm))/lag(bectuareal1ux_imp_norm)
  ) %>%
  summarise(
    mean_perc_population = mean(perc_population_imp, na.rm = TRUE),
    mean_perc_bectuareal = mean(perc_bectuareal1ux_imp, na.rm = TRUE),
    mean_perc_population_norm = mean(perc_population_imp_norm, na.rm = TRUE),
    mean_perc_bectuareal_norm = mean(perc_bectuareal1ux_imp_norm, na.rm = TRUE)
  ) %>%
  ungroup() %>%
  mutate(
    tercile_perc_population = ntile(mean_perc_population, 3),
    tercile_perc_bectuareal = ntile(mean_perc_bectuareal, 3),
    tercile_perc_population_norm = ntile(mean_perc_population_norm, 3),
    tercile_perc_bectuareal_norm = ntile(mean_perc_bectuareal_norm, 3)
  )
 
gbmt_shp_interest <- map(
  gbmt_interest$gbmt_fit_total,
  ~ join_data_shp2(.x, shp_list)
)
{r} 复制代码
country_data <- map(shp_list, 1) %>% 
  bind_rows() %>% 
  mutate(
    study = c(rep(TRUE, 11), rep(FALSE, 10))
  )

countries_by_study <- country_data %>% 
  as_tibble() %>% 
  select(GID_0, study)

level1_data <- map(shp_list, 2) %>% 
  bind_rows() %>% 
  left_join(countries_by_study)
{r} 复制代码
# country_data <- gbmt_shp_interest[[1]] %>%
#   select(ISO2, country, shp_1) %>%
#   unnest(cols = c(shp_1)) %>%
#   st_as_sf()

city_data <- gbmt_shp_interest[[1]] %>%
  select(ISO2, country, shp_inner) %>%
  unnest(cols = shp_inner) %>%
  st_as_sf() %>%
  st_centroid()
{r} 复制代码
library(biscale)
{r} 复制代码
city_data_biscale <- bi_class(city_data,
  x = mean_perc_population,
  y = mean_perc_bectuareal,
  style = "quantile", dim = 3
)

custom_pal_teal <- c(
  "1-1" = "#b2d8d8", # teal green for low x, low y
  "2-1" = "#ba8890",
  "3-1" = "#9e3547",
  "1-2" = "#8aa6c2",
  "2-2" = "#7a6b84",
  "3-2" = "#682a41",
  "1-3" = "#4279b0",
  "2-3" = "#3a4e78",
  "3-3" = "#311e3b"
)

ggmap <- level1_data %>%
  ggplot() +
  geom_sf(
    aes(fill = study),
    linewidth = 0.05
  ) +
  scale_fill_manual(
    values = c("#bdbdbd", "#e5e5e5"),
    guide = "none"
  ) + 
  geom_sf(
    data = country_data,
    linewidth = 0.35, 
    color = "#3f3f3f",
    fill = NA
  ) +
  geom_sf(
    data = city_data_biscale,
    mapping = aes(color = bi_class),
    #color = "white", 
    size = 2.5,
    show.legend = FALSE
  ) +
  bi_scale_color(pal = custom_pal_teal, dim = 3) +
  # bi_scale_color(pal = "GrPink", dim = 3) +
  bi_theme()

legend <- bi_legend(
  pal = custom_pal_teal,
  dim = 3,
  xlab = "Higher Δ% Population",
  ylab = "Higher Δ% Urban Area",
  size = 6
)

library(cowplot)

finalPlot <- ggdraw(ggmap) +
  # draw_plot(ggmap, 0, 0, 1.5, 1.5) +
  draw_plot(legend, 0.15, 0.25, 0.25, 0.25)

ggsave("ggmap.png",
       finalPlot,
       height = 7,
       width = 7,
       dpi = 600)
{r} 复制代码
ggmap2 <- country_data %>%
  group_by(country) %>%
  summarise() %>%
  ggplot() +
  geom_sf(fill = "white") +
  geom_sf(
    data = city_data_biscale,
    mapping = aes(color = bi_class),
    # color = "white",
    size = 2.5,
    show.legend = FALSE
  ) +
  bi_scale_color(pal = "GrPink", dim = 3) +
  bi_theme()

legend <- bi_legend(
  pal = "GrPink",
  dim = 3,
  xlab = "Higher Δ Population",
  ylab = "Higher Δ Urban Area",
  size = 6
)

library(cowplot)

finalPlot <- ggdraw() +
  draw_plot(ggmap, 0, 0, 1, 1) +
  draw_plot(legend, 0.1, .28, 0.25, 0.25)

ggsave("ggmap.png",
  finalPlot,
  dpi = 600
)
相关推荐
Biomamba生信基地3 小时前
R语言基础| 下载、安装
开发语言·r语言·生信·医药
Chef_Chen13 小时前
从0开始学习R语言--Day20-ARIMA与格兰杰因果检验
开发语言·学习·r语言
善木科研2 天前
读文献先读图:GO弦图怎么看?
机器学习·数据分析·r语言
Tiger Z2 天前
R 语言科研绘图第 55 期 --- 网络图-聚类
开发语言·r语言·贴图
十三画者3 天前
【数据分析】R版IntelliGenes用于生物标志物发现的可解释机器学习
python·机器学习·数据挖掘·数据分析·r语言·数据可视化
lishaoan775 天前
实验设计与分析(第6版,Montgomery著,傅珏生译) 第10章拟合回归模型10.9节思考题10.12 R语言解题
回归·r语言·线性回归·残差分析·实验设计与数据分析·回归显著性
南瓜胖胖5 天前
【R语言编程绘图-mlbench】
开发语言·机器学习·r语言
天桥下的卖艺者5 天前
R语言使用随机过采样(Random Oversampling)平衡数据集
开发语言·r语言
Biomamba生信基地6 天前
R语言基础| 创建数据集
开发语言·r语言·生信·医药
lishaoan776 天前
实验设计与分析(第6版,Montgomery)第5章析因设计引导5.7节思考题5.19 R语言解题
r语言·方差分析·实验设计与分析·残差分析·正态假设·交互作用