1. Paquetes

library(readxl)
library(dplyr)
library(rpart)
library(rpart.plot)
library(kableExtra)
library(ggplot2)
library(forcats)

2. Carga y preparación de datos

# ── Carga del archivo ─────────────────────────────────────────────────────────

raw  <- read_excel("C:/Users/Usuario/OneDrive - Universitat de Barcelona/0_RECERCA/Artículos/EN CURSO/Justicia Global_Bea/Artículo/Global Justice Education.xlsx")

# Referencia de columnas (índices R, 1-based):
#  [2]     Edad
#  [3]     Género
#  [4]     Formación más alta
#  [5]     Años de experiencia docente
#  [6]     Régimen del centro
#  [7]     Autoconocimiento EpJG                      (1-5)
#  [8]     Conocimiento EpJG del centro               (1-5)
#  [9]     Importancia EpJG en labor educativa        (1-5)
#  [10:16] Apoyo a movimientos sociales (7 ítems)     (1-5)
#  [17:26] Conocimiento de temáticas EpJG (10 ítems)  (1-5)
#  [27:36] Trabajo en el aula (10 ítems)              (Sí/No)
#  [37:46] Principal dificultad por temática          (categórica)
#  [47:56] Interés en formarse por temática           (1-5)
# ── Variable dependiente ──────────────────────────────────────────────────────
# Contamos cuántas de las 10 temáticas EpJG se trabajan en el aula
trabajo_cols <- raw[, 27:36]
n_temas <- rowSums(trabajo_cols == "Sí", na.rm = TRUE)

# Binarizamos en la mediana (7): "trabaja intensamente" vs "trabaja poco/no trabaja"
# Distribución: media = 6.79, mediana = 7
trabaja_bin <- factor(
  ifelse(n_temas >= 7, "Sí trabaja", "No trabaja"),
  levels = c("No trabaja", "Sí trabaja")
)

cat("Distribución de la variable dependiente:\n")
## Distribución de la variable dependiente:
print(table(trabaja_bin))
## trabaja_bin
## No trabaja Sí trabaja 
##        401        580
cat("\nProporción (%):\n")
## 
## Proporción (%):
print(round(prop.table(table(trabaja_bin)) * 100, 1))
## trabaja_bin
## No trabaja Sí trabaja 
##       40.9       59.1
cat("\nN_temas: media =", round(mean(n_temas), 2),
    "| mediana =", median(n_temas), "\n")
## 
## N_temas: media = 6.79 | mediana = 7
# ── Predictores ───────────────────────────────────────────────────────────────

# · Conoc. temáticas EpJG: media de 10 ítems 1-5 (cols 17-26)
conoc_temas <- rowMeans(
  apply(raw[, 17:26], 2, as.numeric),
  na.rm = TRUE
)

# · Autoconocimiento EpJG (col 7)
auto_conoc <- as.numeric(raw[[7]])

# · Conocimiento EpJG del centro (col 8)
centro_conoc <- as.numeric(raw[[8]])

# · Importancia percibida EpJG (col 9)
importancia <- as.numeric(raw[[9]])

# · Apoyo a movimientos sociales: media de 7 ítems 1-5 (cols 10-16)
apoyo_mov <- rowMeans(
  apply(raw[, 10:16], 2, as.numeric),
  na.rm = TRUE
)

# · Interés en formación EpJG: media de 10 ítems 1-5 (cols 47-56)
interes_form <- rowMeans(
  apply(raw[, 47:56], 2, as.numeric),
  na.rm = TRUE
)

# · Barreras (col 37: primera dificultad como proxy)
#   Codificación: barrera interna (formación + interés personal) vs externa
barrera_interna <- as.integer(
  raw[[37]] %in% c("Falta de formación", "Falta de interés personal")
)
barrera_tiempo <- as.integer(raw[[37]] == "Falta de tiempo")

# · Experiencia docente (col 5) → numérica ordinal
experiencia_num <- dplyr::recode(raw[[5]],
  "0 a 5 años"     = 1L,
  "6 a 10 años"    = 2L,
  "11 a 15 años"   = 3L,
  "16 a 20 años"   = 4L,
  "21 a 25 años"   = 5L,
  "26 a 30 años"   = 6L,
  "31 a 35 años"   = 7L,
  "Más de 35 años" = 8L,
  .default = NA_integer_
)

# · Género (col 3) → dummy femenino
genero_fem <- as.integer(raw[[3]] == "Femenino")

# · Régimen del centro (col 6) → dummy público
regimen_pub <- as.integer(raw[[6]] == "Público")

# ── Ensamblaje del dataset de modelado ───────────────────────────────────────
df_arbol <- data.frame(
  trabaja         = trabaja_bin,
  conoc_temas     = conoc_temas,
  auto_conoc      = auto_conoc,
  centro_conoc    = centro_conoc,
  importancia     = importancia,
  apoyo_mov       = apoyo_mov,
  interes_form    = interes_form,
  barrera_interna = barrera_interna,
  barrera_tiempo  = barrera_tiempo,
  experiencia_num = experiencia_num,
  genero_fem      = genero_fem,
  regimen_pub     = regimen_pub
) |> na.omit()

cat("\nN disponible para el árbol:", nrow(df_arbol), "\n")
## 
## N disponible para el árbol: 981

3. Árbol de clasificación

# ── Paso 1: árbol sin podar (CP mínimo) ──────────────────────────────────────
set.seed(2024)
arbol_full <- rpart(
  trabaja ~ .,
  data    = df_arbol,
  method  = "class",
  parms   = list(split = "gini"),
  control = rpart.control(
    cp        = 0.001,   # CP pequeño → árbol grande antes de podar
    minsplit  = 30,      # mín. obs. para intentar un split
    minbucket = 15,      # mín. obs. en un nodo hoja
    maxdepth  = 6,       # profundidad máxima
    xval      = 10       # 10-fold CV interno
  )
)

# ── Tabla de complejidad (CP) con error de validación cruzada ────────────────
cat("Tabla CP (error de validación cruzada):\n")
## Tabla CP (error de validación cruzada):
printcp(arbol_full)
## 
## Classification tree:
## rpart(formula = trabaja ~ ., data = df_arbol, method = "class", 
##     parms = list(split = "gini"), control = rpart.control(cp = 0.001, 
##         minsplit = 30, minbucket = 15, maxdepth = 6, xval = 10))
## 
## Variables actually used in tree construction:
## [1] apoyo_mov       auto_conoc      barrera_tiempo  conoc_temas    
## [5] experiencia_num importancia    
## 
## Root node error: 401/981 = 0.40877
## 
## n= 981 
## 
##          CP nsplit rel error  xerror     xstd
## 1 0.2493766      0   1.00000 1.00000 0.038398
## 2 0.0083126      1   0.75062 0.79302 0.036559
## 3 0.0074813      8   0.68329 0.82793 0.036958
## 4 0.0049875      9   0.67581 0.80050 0.036648
## 5 0.0024938     10   0.67082 0.80549 0.036706
## 6 0.0010000     13   0.66334 0.81047 0.036763
# ── Paso 2: poda con la regla 1-SE ───────────────────────────────────────────
# Seleccionamos el CP más sencillo cuyo xerror ≤ min(xerror) + xstd
cp_tabla   <- arbol_full$cptable
idx_min    <- which.min(cp_tabla[, "xerror"])
umbral_1se <- cp_tabla[idx_min, "xerror"] + cp_tabla[idx_min, "xstd"]
idx_1se    <- which(cp_tabla[, "xerror"] <= umbral_1se)[1]
cp_optimo  <- cp_tabla[idx_1se, "CP"]

arbol <- prune(arbol_full, cp = cp_optimo)

cat("CP óptimo (1-SE rule):", round(cp_optimo, 4), "\n")
## CP óptimo (1-SE rule): 0.0083
cat("Profundidad efectiva: ", arbol$control$maxdepth, "\n")
## Profundidad efectiva:  6
cat("Nodos terminales:     ", sum(arbol$frame$var == "<leaf>"), "\n")
## Nodos terminales:      2
# ── Métricas de rendimiento (CV interna de rpart) ────────────────────────────
# El error de CV relativo al nodo raíz
cp_arbol   <- arbol$cptable
xerror_opt <- cp_arbol[nrow(cp_arbol), "xerror"]
xstd_opt   <- cp_arbol[nrow(cp_arbol), "xstd"]

# Error raíz = proporción de clase minoritaria en el nodo raíz
error_raiz <- arbol_full$cptable[1, "rel error"] *
              (1 - max(prop.table(table(df_arbol$trabaja))))

# Precisión estimada en CV
prec_cv <- 1 - (xerror_opt * (1 - max(prop.table(table(df_arbol$trabaja)))))

# Precisión en entrenamiento
pred_train <- predict(arbol, type = "class")
prec_train <- mean(pred_train == df_arbol$trabaja)

# Matriz de confusión
conf_mat <- table(
  Predicho  = pred_train,
  Observado = df_arbol$trabaja
)

cat("── Rendimiento del árbol ──────────────────────────\n")
## ── Rendimiento del árbol ──────────────────────────
cat("Precisión (entrenamiento):    ", round(prec_train * 100, 1), "%\n")
## Precisión (entrenamiento):     69.3 %
cat("CP óptimo usado:              ", round(cp_optimo, 4), "\n")
## CP óptimo usado:               0.0083
cat("Error CV relativo (rpart):    ", round(xerror_opt, 3),
    "±", round(xstd_opt, 3), "\n\n")
## Error CV relativo (rpart):     0.793 ± 0.037
cat("Matriz de confusión (entrenamiento):\n")
## Matriz de confusión (entrenamiento):
print(conf_mat)
##             Observado
## Predicho     No trabaja Sí trabaja
##   No trabaja        202        102
##   Sí trabaja        199        478

4. Tabla: importancia de variables

if (!is.null(arbol$variable.importance)) {

  # Nombre legible para cada predictor
  etiquetas <- c(
    conoc_temas     = "Conocimiento de temáticas EpJG (índice)",
    auto_conoc      = "Autoconocimiento del enfoque EpJG",
    centro_conoc    = "Conocimiento EpJG del centro educativo",
    importancia     = "Importancia percibida de la EpJG",
    apoyo_mov       = "Apoyo a movimientos sociales (índice)",
    interes_form    = "Interés en formación EpJG (índice)",
    barrera_interna = "Barrera interna (formación / interés personal)",
    barrera_tiempo  = "Barrera externa: falta de tiempo",
    experiencia_num = "Experiencia docente",
    genero_fem      = "Género (femenino = 1)",
    regimen_pub     = "Régimen del centro (público = 1)"
  )

  imp_raw  <- arbol$variable.importance
  imp_df   <- data.frame(
    Variable    = etiquetas[names(imp_raw)],
    Gini        = round(imp_raw, 3),
    Pct_relativa = round(imp_raw / sum(imp_raw) * 100, 1),
    row.names   = NULL
  ) |>
    dplyr::arrange(dplyr::desc(Gini)) |>
    dplyr::mutate(Ranking = dplyr::row_number()) |>
    dplyr::select(Ranking, Variable, Gini, Pct_relativa)

  imp_df |>
    kable(
      caption   = paste0(
        "Importancia de variables en el árbol de clasificación (criterio Gini). ",
        "N = ", nrow(df_arbol), ". ",
        "Variable dependiente: trabaja ≥ 7 temáticas EpJG en el aula."
      ),
      col.names = c("Rg.", "Variable predictora",
                    "Importancia (Gini)", "% relativa"),
      align     = c("c", "l", "r", "r"),
      digits    = 3
    ) |>
    kable_styling(
      bootstrap_options = c("striped", "hover", "condensed", "responsive"),
      full_width        = FALSE,
      position          = "center",
      font_size         = 13
    ) |>
    row_spec(0, bold = TRUE, background = "#185FA5", color = "white") |>
    row_spec(
      which(imp_df$Gini > 0),
      bold = FALSE
    ) |>
    row_spec(1,
      bold       = TRUE,
      background = "#EBF5FB"
    ) |>
    column_spec(3, width = "12em") |>
    column_spec(4, width = "10em") |>
    footnote(
      general = paste0(
        "Únicamente las variables con Gini > 0 contribuyen a la partición del árbol. ",
        "Las variables con importancia = 0 (conocimiento del centro, importancia percibida, ",
        "apoyo a movimientos sociales, barreras, género, régimen del centro) ",
        "no discriminan la práctica EpJG en el aula en este modelo."
      ),
      general_title = "Nota.",
      footnote_as_chunk = TRUE
    )

} else {
  cat("El árbol es trivial: la clase mayoritaria domina sin ninguna partición.\n")
}
Importancia de variables en el árbol de clasificación (criterio Gini). N = 981. Variable dependiente: trabaja ≥ 7 temáticas EpJG en el aula.
Rg. Variable predictora Importancia (Gini) % relativa
1 Conocimiento de temáticas EpJG (índice) 57.606 83.7
2 Autoconocimiento del enfoque EpJG 8.338 12.1
3 Importancia percibida de la EpJG 1.326 1.9
4 Apoyo a movimientos sociales (índice) 0.758 1.1
5 Interés en formación EpJG (índice) 0.758 1.1
Nota. Únicamente las variables con Gini > 0 contribuyen a la partición del árbol. Las variables con importancia = 0 (conocimiento del centro, importancia percibida, apoyo a movimientos sociales, barreras, género, régimen del centro) no discriminan la práctica EpJG en el aula en este modelo.

5. Figura: árbol de decisión

# ── Paleta de colores por clase predicha ─────────────────────────────────────
# rpart.plot asigna el color según el nivel del factor en el orden de levels()
# levels = c("No trabaja", "Sí trabaja") → color[1] = No, color[2] = Sí
# levels = c("No trabaja", "Sí trabaja") → color[1] = azul claro, color[2] = azul medio
paleta_arbol <- list(
  col_no_fuerte,   # No trabaja  → azul muy claro  (#E6F1FB)
  col_si_fuerte    # Sí trabaja  → azul medio       (#85B7EB)
)

rpart.plot(
  arbol,
  type          = 4,     # etiqueta de clase en nodos internos + hojas
  extra         = 104,   # % clase mayoritaria + n de observaciones
  fallen.leaves = TRUE,  # hojas alineadas en la base
  branch        = 0.4,   # apertura de las ramas
  gap           = 3,
  tweak         = 1.1,   # tamaño relativo del texto
  box.palette   = paleta_arbol,
  border.col    = c(col_no_borde, col_si_borde),   # azul-600 / azul-800
  shadow.col    = NA,
  split.col     = col_azul,
  split.border.col = col_azul,
  split.font    = 1,
  split.cex     = 0.85,
  nn            = FALSE,
  under         = FALSE,
  main = paste0(
    "Árbol de clasificación: ¿Qué condiciona trabajar la EpJG en el aula?\n",
    "(N = ", nrow(df_arbol),
    "  |  cp = ", round(cp_optimo, 4),
    "  |  Profundidad = ", sum(arbol$frame$var == "<leaf>") - 1, " niveles",
    "  |  Precisión entrenamiento = ", round(prec_train * 100, 1), "%)"
  ),
  cex.main = 0.85,
  col.main = col_azul
)

cat(
  "<p style='font-size:0.85em; color:#73726c; text-align:center;'>",
  "En cada nodo: <b>clase predicha</b> | % de la clase mayoritaria | n de observaciones.",
  "<br>Los nodos más saturados indican mayor certeza en la predicción.</p>"
)

En cada nodo: clase predicha | % de la clase mayoritaria | n de observaciones.
Los nodos más saturados indican mayor certeza en la predicción.

6. Figura: importancia relativa de variables

if (!is.null(arbol$variable.importance)) {

  etiquetas_cortas <- c(
    conoc_temas     = "Conoc. temáticas EpJG (índice)",
    auto_conoc      = "Autoconocimiento EpJG",
    interes_form    = "Interés en formación (índice)",
    experiencia_num = "Experiencia docente",
    centro_conoc    = "Conoc. EpJG del centro",
    importancia     = "Importancia percibida EpJG",
    apoyo_mov       = "Apoyo mov. sociales (índice)",
    barrera_interna = "Barrera interna",
    barrera_tiempo  = "Barrera tiempo",
    genero_fem      = "Género",
    regimen_pub     = "Régimen del centro"
  )

  imp_plot <- data.frame(
    variable = etiquetas_cortas[names(arbol$variable.importance)],
    pct      = arbol$variable.importance / sum(arbol$variable.importance) * 100
  ) |>
    dplyr::filter(pct > 0) |>
    dplyr::arrange(pct) |>
    dplyr::mutate(
      variable = forcats::fct_inorder(variable),
      color    = ifelse(pct == max(pct), col_azul, col_azul_suave)
    )

  ggplot(imp_plot, aes(x = variable, y = pct, fill = color)) +
    geom_col(width = 0.65, show.legend = FALSE) +
    geom_text(
      aes(label = paste0(round(pct, 1), "%")),
      hjust = -0.15, size = 3.5, color = col_azul
    ) +
    scale_fill_identity() +
    scale_y_continuous(
      expand = expansion(mult = c(0, 0.15)),
      labels = function(x) paste0(x, "%")
    ) +
    coord_flip() +
    labs(
      x     = NULL,
      y     = "Importancia relativa (%)",
      title = "Importancia de variables en el árbol de clasificación EpJG",
      caption = paste0(
        "Criterio: reducción de impureza Gini. ",
        "Solo se muestran variables con importancia > 0."
      )
    ) +
    theme_minimal(base_size = 12) +
    theme(
      plot.title       = element_text(size = 12, face = "bold", color = col_azul,
                                      margin = margin(b = 8)),
      plot.caption     = element_text(size = 9, color = "#73726c",
                                      margin = margin(t = 8)),
      axis.text.y      = element_text(size = 10, color = "#3d3d3a"),
      axis.text.x      = element_text(size = 9,  color = "#73726c"),
      panel.grid.major.y = element_blank(),
      panel.grid.major.x = element_line(color = col_azul_suave, linewidth = 0.4),
      panel.grid.minor   = element_blank()
    )
}

7. Resumen de los resultados

# Extraer el split primario para el resumen
nodo_raiz <- arbol$frame[1, ]
var_split1 <- as.character(nodo_raiz$var)
thresh1    <- round(arbol$splits[1, "index"], 2)

cat(paste0(
"### Hallazgos principales\n\n",
"El árbol de clasificación (N = ", nrow(df_arbol), "; cp = ", round(cp_optimo, 4), "; ",
"precisión en entrenamiento = ", round(prec_train * 100, 1), "%) identifica como ",
"**predictor dominante** del trabajo de la EpJG en el aula ",
"el **conocimiento de las temáticas EpJG** (índice medio de 10 ítems, escala 1-5), ",
"con una importancia relativa del ", round(
  arbol$variable.importance["conoc_temas"] /
  sum(arbol$variable.importance) * 100, 1
), "% sobre el total.\n\n",
"La partición primaria se establece en el umbral de conocimiento = **", thresh1, "** (escala 1-5):\n\n",
"- Docentes con conocimiento **≤ ", thresh1, "**: tienden a **no trabajar** la EpJG ",
"de forma intensiva en el aula.\n",
"- Docentes con conocimiento **> ", thresh1, "**: tienden a **sí trabajar** la EpJG ",
"de forma intensiva en el aula.\n\n",
"Las variables **autoconocimiento EpJG** (", round(
  arbol$variable.importance["auto_conoc"] /
  sum(arbol$variable.importance) * 100, 1
), "%), **interés en formación** (", round(
  arbol$variable.importance["interes_form"] /
  sum(arbol$variable.importance) * 100, 1
), "%) y **experiencia docente** (", round(
  arbol$variable.importance["experiencia_num"] /
  sum(arbol$variable.importance) * 100, 1
), "%) actúan únicamente como diferenciadores secundarios ",
"dentro de las bandas intermedias de conocimiento.\n\n",
"**Variables sin capacidad predictiva** en el árbol: conocimiento EpJG del centro, ",
"importancia percibida, apoyo a movimientos sociales, tipo de barrera percibida, ",
"género y régimen del centro educativo.\n\n",
"### Implicaciónes\n\n",
"El conocimiento (y no el compromiso ideológico, el género, el tipo de centro, etc)",
"es la variable que más discrimina si un docente trabaja o no la EpJG en el aula. ",
"Esto orienta las intervenciones formativas hacia el **contenido disciplinar** ",
"de la EpJG, más que hacia la sensibilización o la reducción de obstáculos percibidos."
))

Hallazgos principales

El árbol de clasificación (N = 981; cp = 0.0083; precisión en entrenamiento = 69.3%) identifica como predictor dominante del trabajo de la EpJG en el aula el conocimiento de las temáticas EpJG (índice medio de 10 ítems, escala 1-5), con una importancia relativa del 83.7% sobre el total.

La partición primaria se establece en el umbral de conocimiento = 3.25 (escala 1-5):

  • Docentes con conocimiento ≤ 3.25: tienden a no trabajar la EpJG de forma intensiva en el aula.
  • Docentes con conocimiento > 3.25: tienden a sí trabajar la EpJG de forma intensiva en el aula.

Las variables autoconocimiento EpJG (12.1%), interés en formación (1.1%) y experiencia docente (NA%) actúan únicamente como diferenciadores secundarios dentro de las bandas intermedias de conocimiento.

Variables sin capacidad predictiva en el árbol: conocimiento EpJG del centro, importancia percibida, apoyo a movimientos sociales, tipo de barrera percibida, género y régimen del centro educativo.

Implicaciónes

El conocimiento (y no el compromiso ideológico, el género, el tipo de centro, etc)es la variable que más discrimina si un docente trabaja o no la EpJG en el aula. Esto orienta las intervenciones formativas hacia el contenido disciplinar de la EpJG, más que hacia la sensibilización o la reducción de obstáculos percibidos.


sessionInfo()
## R version 4.4.2 (2024-10-31 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
## 
## Matrix products: default
## 
## 
## locale:
## [1] LC_COLLATE=Spanish_Spain.utf8  LC_CTYPE=Spanish_Spain.utf8   
## [3] LC_MONETARY=Spanish_Spain.utf8 LC_NUMERIC=C                  
## [5] LC_TIME=Spanish_Spain.utf8    
## 
## time zone: Europe/Madrid
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] forcats_1.0.0    ggplot2_4.0.1    kableExtra_1.4.0 rpart.plot_3.1.2
## [5] rpart_4.1.23     dplyr_1.1.4      readxl_1.4.3    
## 
## loaded via a namespace (and not attached):
##  [1] gtable_0.3.6       jsonlite_1.8.9     compiler_4.4.2     tidyselect_1.2.1  
##  [5] xml2_1.3.6         stringr_1.5.1      jquerylib_0.1.4    systemfonts_1.2.1 
##  [9] scales_1.4.0       yaml_2.3.10        fastmap_1.2.0      R6_2.5.1          
## [13] labeling_0.4.3     generics_0.1.3     knitr_1.49         tibble_3.2.1      
## [17] svglite_2.1.3      bslib_0.8.0        pillar_1.10.1      RColorBrewer_1.1-3
## [21] rlang_1.1.7        cachem_1.1.0       stringi_1.8.4      xfun_0.50         
## [25] S7_0.2.1           sass_0.4.9         viridisLite_0.4.2  cli_3.6.5         
## [29] withr_3.0.2        magrittr_2.0.3     grid_4.4.2         digest_0.6.37     
## [33] rstudioapi_0.17.1  lifecycle_1.0.4    vctrs_0.6.5        evaluate_1.0.3    
## [37] glue_1.8.0         farver_2.1.2       cellranger_1.1.0   rmarkdown_2.29    
## [41] tools_4.4.2        pkgconfig_2.0.3    htmltools_0.5.8.1