Proyecto: Ensamble de Algoritmos - Random Forest, AdaBoost y XGBoost

Detección de Cáncer de Mama - Análisis Comparativo con Desbalanceo Moderado

Author

Alejandro Figueroa Rojas

Published

February 12, 2026

1 Motivación y Contexto

Este proyecto aplica algoritmos de ensamble al dataset BreastCancer para explorar cómo diferentes técnicas manejan desbalanceo de clases y comprender las matemáticas detrás de cada método.

Desafío del dataset:

  • Desbalanceo moderado (35% malignos, 65% benignos)
  • Variables ordinales (escala 1-10) con valores faltantes
  • Trade-off crítico: detectar malignos vs evitar falsos positivos

Objetivo de aprendizaje:

Implementar y comparar tres ensambles (Random Forest, AdaBoost, XGBoost) para entender:

  • Cómo Bagging reduce varianza mediante aleatorización
  • Cómo Boosting corrige errores secuencialmente con pesos adaptativos
  • Cómo la regularización en XGBoost previene overfitting

Contexto clínico:

Un cáncer no detectado (FN) es crítico para el paciente. Falsos positivos (FP) generan biopsias innecesarias pero son menos graves. Los modelos deben priorizar Sensitivity.

2 Carga de Bibliotecas y Datos

library(mlbench)
library(randomForest)
library(xgboost)
library(caret)
library(pROC)
library(PRROC)
library(ggplot2)
library(gridExtra)
library(adabag)
library(rpart)
library(reshape2)

data(BreastCancer)
datos <- BreastCancer

3 Exploración de Datos

{str(datos)
dim(datos)}
'data.frame':   699 obs. of  11 variables:
 $ Id             : chr  "1000025" "1002945" "1015425" "1016277" ...
 $ Cl.thickness   : Ord.factor w/ 10 levels "1"<"2"<"3"<"4"<..: 5 5 3 6 4 8 1 2 2 4 ...
 $ Cell.size      : Ord.factor w/ 10 levels "1"<"2"<"3"<"4"<..: 1 4 1 8 1 10 1 1 1 2 ...
 $ Cell.shape     : Ord.factor w/ 10 levels "1"<"2"<"3"<"4"<..: 1 4 1 8 1 10 1 2 1 1 ...
 $ Marg.adhesion  : Ord.factor w/ 10 levels "1"<"2"<"3"<"4"<..: 1 5 1 1 3 8 1 1 1 1 ...
 $ Epith.c.size   : Ord.factor w/ 10 levels "1"<"2"<"3"<"4"<..: 2 7 2 3 2 7 2 2 2 2 ...
 $ Bare.nuclei    : Factor w/ 10 levels "1","2","3","4",..: 1 10 2 4 1 10 10 1 1 1 ...
 $ Bl.cromatin    : Factor w/ 10 levels "1","2","3","4",..: 3 3 3 3 3 9 3 3 1 2 ...
 $ Normal.nucleoli: Factor w/ 10 levels "1","2","3","4",..: 1 2 1 7 1 7 1 1 1 1 ...
 $ Mitoses        : Factor w/ 9 levels "1","2","3","4",..: 1 1 1 1 1 1 1 1 5 1 ...
 $ Class          : Factor w/ 2 levels "benign","malignant": 1 1 1 1 1 2 1 1 1 1 ...
[1] 699  11
{cat("=== ESTADÍSTICAS BÁSICAS ===\n")
cat("Total casos:", nrow(datos), "\n")
cat("Features:", ncol(datos)-2, "\n\n")}
=== ESTADÍSTICAS BÁSICAS ===
Total casos: 699 
Features: 9 
cat("=== DISTRIBUCIÓN CLASE ===\n")
=== DISTRIBUCIÓN CLASE ===
print(table(datos$Class))

   benign malignant 
      458       241 
cat("\nProporción malignos:", round(mean(datos$Class=="malignant", na.rm=TRUE)*100, 2), "%\n")

Proporción malignos: 34.48 %

Variables predictoras (9 características citológicas, escala 1-10):

  • Cl.thickness: Grosor del grupo celular
  • Cell.size: Uniformidad del tamaño celular
  • Cell.shape: Uniformidad de la forma celular
  • Marg.adhesion: Adhesión marginal
  • Epith.c.size: Tamaño de células epiteliales
  • Bare.nuclei: Núcleos desnudos
  • Bl.cromatin: Cromatina suave
  • Normal.nucleoli: Nucléolos normales
  • Mitoses: Actividad mitótica

Variable objetivo: Class (benign/malignant)

{cat("\n=== VALORES FALTANTES ===\n")
print(sapply(datos, function(x) sum(is.na(x))))}

=== VALORES FALTANTES ===
             Id    Cl.thickness       Cell.size      Cell.shape   Marg.adhesion 
              0               0               0               0               0 
   Epith.c.size     Bare.nuclei     Bl.cromatin Normal.nucleoli         Mitoses 
              0              16               0               0               0 
          Class 
              0 

4 Preprocesamiento

4.1 Limpieza y Transformación

datos$Id <- NULL

for(col in names(datos)[1:9]) {
  datos[[col]] <- as.numeric(as.character(datos[[col]]))
}

datos$Bare.nuclei[is.na(datos$Bare.nuclei)] <- median(datos$Bare.nuclei, na.rm=TRUE)

cat("NAs restantes:", sum(is.na(datos)), "\n\n")
NAs restantes: 0 
summary(datos)
  Cl.thickness      Cell.size        Cell.shape     Marg.adhesion   
 Min.   : 1.000   Min.   : 1.000   Min.   : 1.000   Min.   : 1.000  
 1st Qu.: 2.000   1st Qu.: 1.000   1st Qu.: 1.000   1st Qu.: 1.000  
 Median : 4.000   Median : 1.000   Median : 1.000   Median : 1.000  
 Mean   : 4.418   Mean   : 3.134   Mean   : 3.207   Mean   : 2.807  
 3rd Qu.: 6.000   3rd Qu.: 5.000   3rd Qu.: 5.000   3rd Qu.: 4.000  
 Max.   :10.000   Max.   :10.000   Max.   :10.000   Max.   :10.000  
  Epith.c.size     Bare.nuclei      Bl.cromatin     Normal.nucleoli 
 Min.   : 1.000   Min.   : 1.000   Min.   : 1.000   Min.   : 1.000  
 1st Qu.: 2.000   1st Qu.: 1.000   1st Qu.: 2.000   1st Qu.: 1.000  
 Median : 2.000   Median : 1.000   Median : 3.000   Median : 1.000  
 Mean   : 3.216   Mean   : 3.486   Mean   : 3.438   Mean   : 2.867  
 3rd Qu.: 4.000   3rd Qu.: 5.000   3rd Qu.: 5.000   3rd Qu.: 4.000  
 Max.   :10.000   Max.   :10.000   Max.   :10.000   Max.   :10.000  
    Mitoses             Class    
 Min.   : 1.000   benign   :458  
 1st Qu.: 1.000   malignant:241  
 Median : 1.000                  
 Mean   : 1.589                  
 3rd Qu.: 1.000                  
 Max.   :10.000                  

4.2 Visualización del Desbalanceo

p1 <- ggplot(datos, aes(x=Class, fill=Class)) +
  geom_bar() +
  geom_text(stat='count', aes(label=..count..), vjust=-0.5) +
  scale_fill_manual(values=c("steelblue","coral")) +
  labs(title="Distribución de Clases", x="Diagnóstico", y="Frecuencia") +
  theme_minimal() + theme(legend.position="none")

p2 <- ggplot(datos, aes(x=Class, y=Cl.thickness, fill=Class)) +
  geom_boxplot() +
  scale_fill_manual(values=c("steelblue","coral")) +
  labs(title="Grosor Celular por Clase", y="Cl.thickness") +
  theme_minimal() + theme(legend.position="none")

p3 <- ggplot(datos, aes(x=Class, y=Cell.size, fill=Class)) +
  geom_boxplot() +
  scale_fill_manual(values=c("steelblue","coral")) +
  labs(title="Tamaño Celular por Clase", y="Cell.size") +
  theme_minimal() + theme(legend.position="none")

grid.arrange(p1, p2, p3, ncol=3)

Análisis de Distribución y Características Celulares por Clase

Distribución de Clases:

El dataset presenta un desbalance significativo, con aproximadamente 65% de casos benignos (458) y 35% malignos (241), no una distribución 50-50 como menciona.

Grosor Celular por Clase:

Los tumores malignos muestran grosor celular considerablemente mayor que los benignos. La mediana de casos malignos se sitúa cerca de 8, mientras que los benignos presentan mediana aproximada de 3. El rango intercuartílico de malignos abarca prácticamente todo el espectro de valores (5-10), contrastando con el rango reducido de benignos (1-4).

Tamaño Celular por Clase:

Los tumores benignos exhiben múltiples outliers en valores altos, indicando casos atípicos con células grandes. Los malignos presentan mediana superior a 5 y un boxplot de mayor magnitud, con rango intercuartílico amplio (5-10) comparado con benignos (aproximadamente 1-3). Esta diferencia sugiere que el tamaño celular es un discriminador efectivo entre ambas clases.

4.3 División Estratificada

set.seed(123)

trainIndex <- createDataPartition(datos$Class, p=0.7, list=FALSE)
train <- datos[trainIndex,]
test <- datos[-trainIndex,]

{cat("=== CONJUNTO TRAIN ===\n")
cat("Total:", nrow(train), "\n")
print(table(train$Class))
cat("% Malignos:", round(mean(train$Class=="malignant")*100, 2), "%\n\n")}
=== CONJUNTO TRAIN ===
Total: 490 

   benign malignant 
      321       169 
% Malignos: 34.49 %
{cat("=== CONJUNTO TEST ===\n")
cat("Total:", nrow(test), "\n")
print(table(test$Class))
cat("% Malignos:", round(mean(test$Class=="malignant")*100, 2), "%\n")}
=== CONJUNTO TEST ===
Total: 209 

   benign malignant 
      137        72 
% Malignos: 34.45 %

5 Fundamentos Teóricos de los Algoritmos

5.1 Random Forest

5.1.1 Definición Matemática

Random Forest es un método de ensamble basado en Bagging (Bootstrap Aggregation) que construye múltiples árboles de decisión y combina sus predicciones.

Algoritmo para Clasificación:

Para cada árbol \(b = 1, \ldots, B\):

  1. Extraer muestra bootstrap de tamaño \(N\) del conjunto de entrenamiento
  2. Construir árbol de decisión:
    • En cada nodo interno, seleccionar aleatoriamente \(m\) variables de las \(p\) disponibles
    • Elegir la mejor variable/punto de corte solo entre estas \(m\) variables
    • Dividir el nodo usando esta variable
    • Crecer árbol hasta profundidad máxima (sin poda)

Predicción:

\[\hat{C}^B_{rf}(x) = \text{majority vote}\{\hat{C}_b(x)\}_{b=1}^B\]

Hiperparámetro \(m\) (valores por defecto):

  • Clasificación: \(m = \lfloor\sqrt{p}\rfloor\)
  • Regresión: \(m = \lfloor p/3 \rfloor\)

Reducción de Varianza:

La varianza del promedio de \(B\) árboles con varianza \(\sigma^2\) y correlación \(\rho\):

\[\text{Var}\left(\frac{1}{B}\sum_{b=1}^B T_b\right) = \rho\sigma^2 + \frac{1-\rho}{B}\sigma^2\]

Al limitar a \(m < p\) variables, se reduce \(\rho\), logrando:

\[\text{Var}(\hat{f}_{rf}) < \text{Var}(\hat{f}_{bag})\]

5.2 AdaBoost

5.2.1 Definición Matemática

AdaBoost (Adaptive Boosting) es un método de Boosting que combina clasificadores débiles secuencialmente, ajustando los pesos de observaciones mal clasificadas.

Algoritmo AdaBoost.M1:

Inicialización: \(w_i = \frac{1}{N}\), \(i = 1, \ldots, N\)

Para \(m = 1\) hasta \(M\):

  1. Ajustar clasificador \(G_m(x)\) a los datos con pesos \(w_i\)

  2. Calcular error ponderado:

\[\text{err}_m = \frac{\sum_{i=1}^N w_i \mathbb{I}(y_i \neq G_m(x_i))}{\sum_{i=1}^N w_i}\]

donde:

  • \(\text{err}_m\): tasa de error ponderada del clasificador \(m\)
  • \(w_i\): peso de la observación \(i\)
  • \(y_i\): etiqueta verdadera
  • \(G_m(x_i)\): predicción del clasificador \(m\)
  • \(\mathbb{I}(\cdot)\): función indicadora (1 si hay error, 0 si no)
  1. Calcular coeficiente del clasificador: \[\alpha_m = \log\left(\frac{1 - \text{err}_m}{\text{err}_m}\right)\]

  2. Actualizar pesos: \[w_i \leftarrow w_i \cdot \exp[\alpha_m \cdot \mathbb{I}(y_i \neq G_m(x_i))]\]

  3. Renormalizar: \(w_i \leftarrow \frac{w_i}{\sum_{j=1}^N w_j}\)

Predicción final:

\[G(x) = \text{sign}\left[\sum_{m=1}^M \alpha_m G_m(x)\right]\]

Función de Pérdida Exponencial:

AdaBoost minimiza:

\[L(y, f(x)) = \exp(-yf(x))\]

donde \(y \in \{-1, +1\}\) y \(f(x) = \sum_{m=1}^M \alpha_m G_m(x)\).

Minimizador poblacional:

\[f^*(x) = \frac{1}{2}\log\frac{P(Y=1|x)}{P(Y=-1|x)}\]

5.3 XGBoost

5.3.1 Definición Matemática

XGBoost (Extreme Gradient Boosting) es una extensión de Gradient Boosting con regularización explícita.

Algoritmo Gradient Boosting:

Inicialización: \[f_0(x) = \arg\min_\gamma \sum_{i=1}^N L(y_i, \gamma)\]

Para \(m = 1\) hasta \(M\):

  1. Calcular pseudo-residuos (gradiente negativo): \[r_{im} = -\left[\frac{\partial L(y_i, f(x_i))}{\partial f(x_i)}\right]_{f=f_{m-1}}\]

  2. Ajustar árbol a \(\{(x_i, r_{im})\}_{i=1}^N\), obteniendo regiones \(R_{jm}\)

  3. Optimizar coeficientes: \[\gamma_{jm} = \arg\min_\gamma \sum_{x_i \in R_{jm}} L(y_i, f_{m-1}(x_i) + \gamma)\]

  4. Actualizar: \[f_m(x) = f_{m-1}(x) + \nu\sum_{j=1}^{J_m} \gamma_{jm}\mathbb{I}(x \in R_{jm})\]

XGBoost con Regularización:

Función objetivo en iteración \(t\):

\[\mathcal{L}^{(t)} = \sum_{i=1}^n l(y_i, \hat{y}_i^{(t-1)} + f_t(x_i)) + \Omega(f_t)\]

Regularización:

\[\Omega(f) = \gamma T + \frac{1}{2}\lambda\sum_{j=1}^T w_j^2\]

Aproximación de segundo orden (Taylor):

\[\mathcal{L}^{(t)} \approx \sum_{i=1}^n\left[g_i f_t(x_i) + \frac{1}{2}h_i f_t^2(x_i)\right] + \Omega(f_t)\]

donde:

  • \(g_i = \frac{\partial l(y_i, \hat{y}^{(t-1)})}{\partial \hat{y}^{(t-1)}}\) (gradiente)
  • \(h_i = \frac{\partial^2 l(y_i, \hat{y}^{(t-1)})}{\partial(\hat{y}^{(t-1)})^2}\) (Hessiano)

Peso óptimo por hoja:

\[w_j^* = -\frac{\sum_{i \in I_j} g_i}{\sum_{i \in I_j} h_i + \lambda}\]

Ganancia al dividir:

\[\text{Gain} = \frac{1}{2}\left[\frac{(\sum_{i \in I_L} g_i)^2}{\sum_{i \in I_L} h_i + \lambda} + \frac{(\sum_{i \in I_R} g_i)^2}{\sum_{i \in I_R} h_i + \lambda} - \frac{(\sum_{i \in I} g_i)^2}{\sum_{i \in I} h_i + \lambda}\right] - \gamma\]

6 Random Forest con Class Weights

n_benign <- sum(train$Class == "benign")
n_malignant <- sum(train$Class == "malignant")
weight_malignant <- n_benign / n_malignant

cat("Class weight malignant:", round(weight_malignant, 2), "\n\n")
Class weight malignant: 1.9 
rf_model <- randomForest(
  Class ~ .,
  data = train,
  ntree = 500,
  mtry = 3,
  classwt = c("benign"=1, "malignant"=weight_malignant),
  importance = TRUE
)

rf_pred <- predict(rf_model, test)
rf_prob <- predict(rf_model, test, type="prob")[,2]
rf_cm <- confusionMatrix(rf_pred, test$Class, positive="malignant")
print(rf_cm)
Confusion Matrix and Statistics

           Reference
Prediction  benign malignant
  benign       132         4
  malignant      5        68
                                          
               Accuracy : 0.9569          
                 95% CI : (0.9198, 0.9801)
    No Information Rate : 0.6555          
    P-Value [Acc > NIR] : <2e-16          
                                          
                  Kappa : 0.905           
                                          
 Mcnemar's Test P-Value : 1               
                                          
            Sensitivity : 0.9444          
            Specificity : 0.9635          
         Pos Pred Value : 0.9315          
         Neg Pred Value : 0.9706          
             Prevalence : 0.3445          
         Detection Rate : 0.3254          
   Detection Prevalence : 0.3493          
      Balanced Accuracy : 0.9540          
                                          
       'Positive' Class : malignant       
                                          

Interpretación de resultados

Class weight maligno: 1.9

  • Asigna mayor importancia a errores en casos malignos (clase minoritaria), compensando el desbalance de clases. Esto penaliza más los falsos negativos, priorizando la detección de tumores malignos sobre la precisión en benignos. El factor 1.9 aproximadamente duplica la influencia de cada muestra maligna durante el entrenamiento.

Matriz de confusión:

  • Verdaderos negativos (VN): 132 casos benignos correctamente clasificados
  • Verdaderos positivos (VP): 68 casos malignos correctamente clasificados
  • Falsos positivos (FP): 5 casos benignos clasificados erróneamente como malignos
  • Falsos negativos (FN): 4 casos malignos clasificados erróneamente como benignos

Métricas de desempeño:

  • Accuracy: 0.9569 - Alta proporción de predicciones correctas (VP + VN) / Total
  • Sensitivity (Recall): 0.9444 - El modelo detecta correctamente 94.4% de los casos malignos reales (VP / (VP + FN))
  • Specificity: 0.9635 - El modelo detecta correctamente 96.4% de los casos benignos reales (VN / (VN + FP))
  • Balanced Accuracy: 0.9540 - Promedio de sensitivity y specificity, útil con clases desbalanceadas
  • Kappa: 0.905 - Acuerdo muy alto entre predicciones y valores reales, ajustado por azar

El modelo muestra excelente desempeño balanceado en ambas clases. Los 4 falsos negativos son más críticos clínicamente (malignos no detectados) que los 5 falsos positivos.

6.1 Visualización Matriz de Confusión Random Forest

# Extraer matriz de confusión
rf_table <- as.table(rf_cm$table)
rf_df <- as.data.frame(rf_table)
colnames(rf_df) <- c("Prediccion", "Real", "Freq")

# Crear gráfico
ggplot(rf_df, aes(x=Prediccion, y=Real, fill=Freq)) +
  geom_tile(color="white", size=1.5) +
  geom_text(aes(label=Freq), size=10, fontface="bold", color="white") +
  scale_fill_gradient(low="mediumpurple1", high="darkgreen", 
                      name="Frecuencia") +
  labs(title="Matriz de Confusión - Random Forest",
       x="Predicción", y="Real") +
  theme_minimal(base_size=14) +
  theme(
    plot.title = element_text(hjust=0.5, face="bold", size=16),
    axis.text = element_text(face="bold", size=12),
    axis.title = element_text(face="bold", size=14),
    legend.position = "right"
  ) +
  coord_fixed()

6.2 Métricas Random Forest

{cat("=== MÉTRICAS RANDOM FOREST ===\n")
cat("Accuracy:", round(rf_cm$overall['Accuracy'], 4), "\n")
cat("Sensitivity (Recall):", round(rf_cm$byClass['Sensitivity'], 4), "\n")
cat("Specificity:", round(rf_cm$byClass['Specificity'], 4), "\n")
cat("Precision:", round(rf_cm$byClass['Pos Pred Value'], 4), "\n")
cat("F1-Score:", round(rf_cm$byClass['F1'], 4), "\n\n")

cat("=== ANÁLISIS DE ERRORES ===\n")
cat("Malignos detectados:", rf_cm$table[2,2], "de", sum(test$Class=="malignant"), "\n")
cat("Falsos positivos:", rf_cm$table[2,1], "\n")
cat("Falsos negativos:", rf_cm$table[1,2], "\n")}
=== MÉTRICAS RANDOM FOREST ===
Accuracy: 0.9569 
Sensitivity (Recall): 0.9444 
Specificity: 0.9635 
Precision: 0.9315 
F1-Score: 0.9379 

=== ANÁLISIS DE ERRORES ===
Malignos detectados: 68 de 72 
Falsos positivos: 5 
Falsos negativos: 4 

Interpretación

  • Precision: 0.9315 - De todos los casos predichos como malignos, 93.15% realmente lo son. Esto significa que hay pocos falsos positivos (VP / (VP + FP) = 68 / (68 + 5)).

  • F1-Score: 0.9379 - Media armónica entre precision y recall, indica excelente balance entre detectar correctamente los malignos (sensitivity) y minimizar falsos positivos (precision).

6.3 Importancia de Variables RF

varImpPlot(rf_model,
           main="Importancia de Variables - Random Forest",
           col="steelblue",
           pch=19,
           cex=1.2,
           pt.cex=2)
grid(col="gray90", lty=2)

Interpretación de Importancia de Variables

MeanDecreaseAccuracy (izquierda): Mide cuánto disminuye la precisión al permutar aleatoriamente cada variable.

  • Bare.nuclei (~28): variable más importante
  • Cell.shape (~26) y Cl.thickness (~21): alta importancia
  • Epith.c.size y Mitoses (~9): menor impacto

MeanDecreaseGini (derecha): Mide la reducción de impureza en las divisiones de los árboles.

  • Cell.shape (~65) y Cell.size (~60): más importantes para separar clases
  • Bare.nuclei (~40): contribución significativa
  • Mitoses y Marg.adhesion (~5): menor aporte

Conclusión: Ambas métricas identifican Bare.nuclei, Cell.shape y Cl.thickness como las variables más relevantes para la clasificación de tumores, con diferencias en el ordenamiento según el criterio utilizado.

6.4 Real vs Predicción RF

rf_comparison <- data.frame(
  Real = test$Class,
  Prediccion = rf_pred,
  Probabilidad = rf_prob
)

ggplot(rf_comparison, aes(x=seq_along(Real), y=Probabilidad, color=Real, shape=Prediccion)) +
  geom_point(size=3, alpha=0.7) +
  geom_hline(yintercept=0.5, linetype="dashed", color="gray40") +
  scale_color_manual(values=c("steelblue","coral")) +
  scale_shape_manual(values=c(1, 19)) +
  labs(title="Random Forest: Real vs Predicción",
       x="Observación", y="Probabilidad Maligno") +
  theme_minimal()

Interpretación: Predicciones vs Valores Reales

El gráfico muestra las predicciones del modelo Random Forest comparadas con los valores reales:

  • Puntos llenos (filled): clase real de cada observación

    • Naranja: malignos reales
    • Azul: benignos reales
  • Puntos vacíos (hollow): predicción del modelo

    • Naranja: predicho como maligno
    • Azul: predicho como benigno

Análisis:

  • La mayoría de observaciones tienen predicción y valor real del mismo color (correctas)
  • Errores visibles: puntos donde el color lleno ≠ color vacío
    • Algunos malignos reales (naranja lleno) predichos como benignos (azul vacío) cerca de y=0.25
    • Pocos benignos reales (azul lleno) predichos como malignos (naranja vacío) cerca de y=0.75
  • Alta concentración de predicciones correctas en y=1.0 (malignos) y y=0.0 (benignos)

Confirma el buen desempeño del modelo con pocos errores de clasificación.

6.5 Frontera de Decisión RF

# Mejorar visualización de frontera de decisión
bare_range <- seq(1, 10, length.out=200)  # Mayor resolución
bl_range <- seq(1, 10, length.out=200)
grid_data <- expand.grid(
  Bare.nuclei = bare_range,
  Bl.cromatin = bl_range
)

for(var in setdiff(names(train), c("Class", "Bare.nuclei", "Bl.cromatin"))) {
  grid_data[[var]] <- median(train[[var]])
}

grid_data$pred_rf <- predict(rf_model, grid_data, type="prob")[,2]  # Probabilidades

ggplot() +
  geom_contour_filled(data=grid_data, 
                      aes(x=Bare.nuclei, y=Bl.cromatin, z=pred_rf),
                      breaks=seq(0, 1, 0.1), alpha=0.6) +
  geom_contour(data=grid_data, 
               aes(x=Bare.nuclei, y=Bl.cromatin, z=pred_rf),
               breaks=0.5, color="black", size=1.2) +  # Frontera en prob=0.5
  geom_point(data=train, aes(x=Bare.nuclei, y=Bl.cromatin, color=Class), 
             size=2.5, alpha=0.8, shape=21, fill="white", stroke=1.5) +
  scale_fill_viridis_d(name="Prob. Maligno", option="plasma") +
  scale_color_manual(values=c("steelblue","coral"), name="Clase Real") +
  labs(title="Frontera de Decisión - Random Forest",
       subtitle="Bare.nuclei vs Bl.cromatin | Línea negra: frontera (p=0.5)",
       x="Bare Nuclei", y="Bland Cromatin") +
  theme_minimal() +
  theme(legend.position="right")

Interpretación: Frontera de Decisión Random Forest

El gráfico muestra cómo el modelo clasifica tumores según Bare.nuclei (eje x) y Bl.cromatin (eje y):

Regiones de color (probabilidad de ser maligno):

  • Azul oscuro (esquina inferior izquierda): alta probabilidad benigno (p<0.1)
  • Rosa/naranja (centro-derecha): probabilidad intermedia-alta de maligno (0.2-0.4)
  • Amarillo (esquina superior derecha): alta probabilidad maligno (0.4-0.5)
  • Línea negra: frontera de decisión (p=0.5)

Puntos:

  • Círculos azules: benignos reales
  • Círculos naranjas: malignos reales

Patrón observado:

  • Valores bajos de ambas variables (≤2.5) → clasificados como benignos
  • Valores altos (≥7.5) → clasificados como malignos
  • Zona intermedia (3-7) muestra transición gradual

Errores visibles:

  • Algunos puntos naranjas en zona azul (falsos negativos)
  • Pocos puntos azules en zona rosa/amarilla (falsos positivos)

La frontera no es lineal, reflejando la capacidad de Random Forest para capturar relaciones complejas entre variables.

7 Algortimo AdaBoost

train_ada <- train
train_ada$Class <- factor(train_ada$Class, levels=c("benign","malignant"), labels=c("-1","1"))
test_ada <- test
test_ada$Class <- factor(test_ada$Class, levels=c("benign","malignant"), labels=c("-1","1"))

ada_model <- boosting(
  Class ~ .,
  data = train_ada,
  boos = TRUE,
  mfinal = 100,
  coeflearn = "Breiman"
)

ada_pred_obj <- predict(ada_model, test_ada)
ada_pred <- factor(ada_pred_obj$class, levels=c("-1","1"), labels=c("benign","malignant"))
test_ada$Class <- factor(test_ada$Class, levels=c("-1","1"), labels=c("benign","malignant"))

ada_cm <- confusionMatrix(ada_pred, test_ada$Class, positive="malignant")
print(ada_cm)
Confusion Matrix and Statistics

           Reference
Prediction  benign malignant
  benign       131         3
  malignant      6        69
                                          
               Accuracy : 0.9569          
                 95% CI : (0.9198, 0.9801)
    No Information Rate : 0.6555          
    P-Value [Acc > NIR] : <2e-16          
                                          
                  Kappa : 0.9056          
                                          
 Mcnemar's Test P-Value : 0.505           
                                          
            Sensitivity : 0.9583          
            Specificity : 0.9562          
         Pos Pred Value : 0.9200          
         Neg Pred Value : 0.9776          
             Prevalence : 0.3445          
         Detection Rate : 0.3301          
   Detection Prevalence : 0.3589          
      Balanced Accuracy : 0.9573          
                                          
       'Positive' Class : malignant       
                                          

Interpretación de resultados

Matriz de confusión:

  • Verdaderos negativos (VN): 131 casos benignos correctamente clasificados
  • Verdaderos positivos (VP): 69 casos malignos correctamente clasificados
  • Falsos positivos (FP): 6 casos benignos clasificados erróneamente como malignos
  • Falsos negativos (FN): 3 casos malignos clasificados erróneamente como benignos

Métricas de desempeño:

  • Accuracy: 0.9569 - Alta proporción de predicciones correctas (VP + VN) / Total
  • Sensitivity (Recall): 0.9583 - El modelo detecta correctamente 95.8% de los casos malignos reales (VP / (VP + FN))
  • Specificity: 0.9562 - El modelo detecta correctamente 95.6% de los casos benignos reales (VN / (VN + FP))
  • Precision: 0.9200 - De los casos predichos como malignos, 92% realmente lo son (VP / (VP + FP))
  • Balanced Accuracy: 0.9573 - Promedio de sensitivity y specificity, útil con clases desbalanceadas
  • Kappa: 0.9056 - Acuerdo muy alto entre predicciones y valores reales, ajustado por azar

El modelo muestra excelente desempeño balanceado en ambas clases. Los 3 falsos negativos son más críticos clínicamente (malignos no detectados) que los 6 falsos positivos.

7.1 Matriz de Confusión AdaBoost

# Extraer matriz de confusión
ada_table <- as.table(ada_cm$table)
ada_df <- as.data.frame(ada_table)
colnames(ada_df) <- c("Prediccion", "Real", "Freq")

# Crear gráfico
ggplot(ada_df, aes(x=Prediccion, y=Real, fill=Freq)) +
  geom_tile(color="white", size=1.5) +
  geom_text(aes(label=Freq), size=10, fontface="bold", color="white") +
  scale_fill_gradient(low="plum2", high="darkslategray", 
                      name="Frecuencia") +
  labs(title="Matriz de Confusión - AdaBoost",
       x="Predicción", y="Real") +
  theme_minimal(base_size=14) +
  theme(
    plot.title = element_text(hjust=0.5, face="bold", size=16),
    axis.text = element_text(face="bold", size=12),
    axis.title = element_text(face="bold", size=14),
    legend.position = "right"
  ) +
  coord_fixed()

7.2 Métricas AdaBoost

{cat("=== MÉTRICAS ADABOOST ===\n")
cat("Accuracy:", round(ada_cm$overall['Accuracy'], 4), "\n")
cat("Sensitivity:", round(ada_cm$byClass['Sensitivity'], 4), "\n")
cat("Specificity:", round(ada_cm$byClass['Specificity'], 4), "\n")
cat("Precision:", round(ada_cm$byClass['Pos Pred Value'], 4), "\n")
cat("F1-Score:", round(ada_cm$byClass['F1'], 4), "\n\n")

cat("=== ANÁLISIS DE ERRORES ===\n")
cat("Malignos detectados:", ada_cm$table[2,2], "de", sum(test_ada$Class=="malignant"), "\n")
cat("Falsos positivos:", ada_cm$table[2,1], "\n")
cat("Falsos negativos:", ada_cm$table[1,2], "\n")}
=== MÉTRICAS ADABOOST ===
Accuracy: 0.9569 
Sensitivity: 0.9583 
Specificity: 0.9562 
Precision: 0.92 
F1-Score: 0.9388 

=== ANÁLISIS DE ERRORES ===
Malignos detectados: 69 de 72 
Falsos positivos: 6 
Falsos negativos: 3 

7.3 Importancia de Variables AdaBoost

ada_imp <- ada_model$importance
ada_imp_df <- data.frame(
  Variable = names(ada_imp),
  Importancia = ada_imp
)
ada_imp_df <- ada_imp_df[order(-ada_imp_df$Importancia),]

ggplot(ada_imp_df, aes(x=reorder(Variable, Importancia), y=Importancia)) +
  geom_bar(stat="identity", fill="darkgreen", alpha=0.7) +
  coord_flip() +
  labs(title="Importancia de Variables - AdaBoost",
       x="Variable", y="Importancia") +
  theme_minimal()

Importancia de Variables - AdaBoost

Bare.nuclei (~18) es la variable más importante, seguida de Cl.thickness (~16) y Marg.adhesion (~14). Mitoses (~3) tiene el menor impacto predictivo.

Orden de importancia coincide parcialmente con Random Forest, confirmando que características nucleares y de grosor celular son más relevantes para la clasificación.

7.4 Real vs Predicción AdaBoost

ada_prob <- ada_pred_obj$prob[,2]

ada_comparison <- data.frame(
  Real = test_ada$Class,
  Prediccion = ada_pred,
  Probabilidad = ada_prob
)

ggplot(ada_comparison, aes(x=seq_along(Real), y=Probabilidad, color=Real, shape=Prediccion)) +
  geom_point(size=3, alpha=0.7) +
  geom_hline(yintercept=0.5, linetype="dashed", color="gray40") +
  scale_color_manual(values=c("steelblue","coral")) +
  scale_shape_manual(values=c(1, 19)) +
  labs(title="AdaBoost: Real vs Predicción",
       x="Observación", y="Probabilidad Maligno") +
  theme_minimal()

Interpretación: Predicciones vs Valores Reales - AdaBoost

El gráfico muestra las probabilidades predichas por AdaBoost:

  • Eje Y: Probabilidad de ser maligno (0-1)
  • Puntos llenos: clase real (naranja=maligno, azul=benigno)
  • Puntos vacíos: predicción del modelo

Observaciones:

  • Fuerte separación: malignos reales concentrados cerca de p=1.0, benignos cerca de p=0.0
  • Predicciones correctas: mayoría de puntos con color lleno y vacío coincidentes
  • Errores visibles: algunos malignos predichos con p<0.5 (falsos negativos) y pocos benignos con p>0.5 (falsos positivos)
  • Modelo más confiado que Random Forest: menos predicciones en zona intermedia (0.3-0.7)

Confirma alto desempeño con clara discriminación entre clases.

7.5 Frontera de Decisión AdaBoost

grid_data_ada <- grid_data
for(var in names(grid_data_ada)) {
  if(var %in% names(train_ada) && var != "Class") {
    # mantener valores numéricos
  }
}

ada_grid_pred <- predict(ada_model, grid_data_ada)
grid_data_ada$pred_ada <- factor(ada_grid_pred$class, levels=c("-1","1"), labels=c("benign","malignant"))

train_plot <- train
train_plot$Class <- factor(train_plot$Class, levels=c("benign","malignant"))

ggplot() +
  geom_tile(data=grid_data_ada, aes(x=Bare.nuclei, y=Bl.cromatin, fill=pred_ada), alpha=0.3) +
  geom_point(data=train_plot, aes(x=Bare.nuclei, y=Bl.cromatin, color=Class), size=2, alpha=0.6) +
  scale_fill_manual(values=c("steelblue","coral"), name="Predicción") +
  scale_color_manual(values=c("steelblue","coral"), name="Real") +
  labs(title="Frontera de Decisión - AdaBoost",
       subtitle="Bare.nuclei vs Bl.cromatin",
       x="Bare Nuclei", y="Bland Cromatin") +
  theme_minimal()

Interpretación: Frontera de Decisión - AdaBoost

El gráfico muestra la frontera de decisión entre Bare.nuclei (eje x) y Bl.cromatin (eje y):

Regiones:

  • Azul claro: predicción benigna
  • Rosa/coral: predicción maligna
  • Frontera: más lineal y definida que Random Forest

Patrón:

  • Valores altos de ambas variables (>8-9) → malignos
  • Resto del espacio → benignos
  • Frontera diagonal en esquina superior derecha

Puntos:

  • Diamantes azules: benignos reales
  • Diamantes naranjas: malignos reales

Errores:

  • Algunos malignos en zona azul (falsos negativos)
  • Pocos benignos en zona rosa (falsos positivos)

AdaBoost genera una frontera más simple y menos irregular que Random Forest, reflejando su naturaleza de combinación lineal de clasificadores débiles.

8 XGBoost con Scale_pos_weight

features <- setdiff(names(train), "Class")

train_matrix <- xgb.DMatrix(
  data = as.matrix(train[, features]),
  label = as.numeric(train$Class) - 1
)

test_matrix <- xgb.DMatrix(
  data = as.matrix(test[, features]),
  label = as.numeric(test$Class) - 1
)
params <- list(
  objective = "binary:logistic",
  eval_metric = "auc",
  max_depth = 4,
  eta = 0.1,
  subsample = 0.8,
  colsample_bytree = 0.8,
  scale_pos_weight = weight_malignant,
  min_child_weight = 3
)

xgb_model <- xgb.train(
  params = params,
  data = train_matrix,
  nrounds = 100,
  watchlist = list(train=train_matrix, test=test_matrix),
  early_stopping_rounds = 10,
  verbose = 0
)

xgb_pred_prob <- predict(xgb_model, test_matrix)

roc_obj <- roc(test$Class, xgb_pred_prob, levels=c("benign","malignant"))
threshold_opt <- coords(roc_obj, "best", ret="threshold")$threshold

cat("Threshold óptimo:", round(threshold_opt, 4), "\n\n")
Threshold óptimo: 0.4517 
xgb_pred <- factor(ifelse(xgb_pred_prob > threshold_opt, "malignant", "benign"),
                   levels = c("benign", "malignant"))

xgb_cm <- confusionMatrix(xgb_pred, test$Class, positive="malignant")
print(xgb_cm)
Confusion Matrix and Statistics

           Reference
Prediction  benign malignant
  benign       131         1
  malignant      6        71
                                          
               Accuracy : 0.9665          
                 95% CI : (0.9322, 0.9864)
    No Information Rate : 0.6555          
    P-Value [Acc > NIR] : <2e-16          
                                          
                  Kappa : 0.927           
                                          
 Mcnemar's Test P-Value : 0.1306          
                                          
            Sensitivity : 0.9861          
            Specificity : 0.9562          
         Pos Pred Value : 0.9221          
         Neg Pred Value : 0.9924          
             Prevalence : 0.3445          
         Detection Rate : 0.3397          
   Detection Prevalence : 0.3684          
      Balanced Accuracy : 0.9712          
                                          
       'Positive' Class : malignant       
                                          

Threshold óptimo calculado por ROC

El umbral de decisión se optimizó usando la curva ROC (puede variar entre ejecuciones). Si el threshold calculado es <0.5, el modelo prioriza sensitivity (detectar malignos) sobre specificity. Esto es clínicamente apropiado para minimizar falsos negativos.

Interpretación de resultados - XGBoost

Matriz de confusión:

  • VN: 131 benignos correctos
  • VP: 71 malignos correctos
  • FP: 6 benignos clasificados como malignos
  • FN: 1 maligno clasificado como benigno

Métricas clave:

  • Accuracy: 0.9665 - 96.7% de predicciones correctas
  • Sensitivity: 0.9861 - Detecta 98.6% de malignos (solo 1 FN)
  • Specificity: 0.9562 - Detecta 95.6% de benignos
  • Precision: 0.9221 - 92.2% de predicciones malignas son correctas
  • Balanced Accuracy: 0.9712 - Excelente balance entre clases
  • Kappa: 0.927 - Acuerdo muy alto ajustado por azar

Resultado: XGBoost logra la mejor sensitivity (98.6%) minimizando el error crítico de no detectar malignos. Solo 1 falso negativo vs 6 falsos positivos es el balance óptimo clínicamente.

8.1 Matriz de Confusión XGBoost

# Extraer matriz de confusión
xgb_table <- as.table(xgb_cm$table)
xgb_df <- as.data.frame(xgb_table)
colnames(xgb_df) <- c("Prediccion", "Real", "Freq")

# Crear gráfico
ggplot(xgb_df, aes(x=Prediccion, y=Real, fill=Freq)) +
  geom_tile(color="white", size=1.5) +
  geom_text(aes(label=Freq), size=10, fontface="bold", color="white") +
  scale_fill_gradient(low="lightseagreen", high="darkslateblue", 
                      name="Frecuencia") +
  labs(title="Matriz de Confusión - XGBoost",
       x="Predicción", y="Real") +
  theme_minimal(base_size=14) +
  theme(
    plot.title = element_text(hjust=0.5, face="bold", size=16),
    axis.text = element_text(face="bold", size=12),
    axis.title = element_text(face="bold", size=14),
    legend.position = "right"
  ) +
  coord_fixed()

8.2 Métricas XGBoost

{cat("=== MÉTRICAS XGBOOST ===\n")
cat("Accuracy:", round(xgb_cm$overall['Accuracy'], 4), "\n")
cat("Sensitivity:", round(xgb_cm$byClass['Sensitivity'], 4), "\n")
cat("Specificity:", round(xgb_cm$byClass['Specificity'], 4), "\n")
cat("Precision:", round(xgb_cm$byClass['Pos Pred Value'], 4), "\n")
cat("F1-Score:", round(xgb_cm$byClass['F1'], 4), "\n\n")

cat("=== ANÁLISIS DE ERRORES ===\n")
cat("Malignos detectados:", xgb_cm$table[2,2], "de", sum(test$Class=="malignant"), "\n")
cat("Falsos positivos:", xgb_cm$table[2,1], "\n")
cat("Falsos negativos:", xgb_cm$table[1,2], "\n")}
=== MÉTRICAS XGBOOST ===
Accuracy: 0.9665 
Sensitivity: 0.9861 
Specificity: 0.9562 
Precision: 0.9221 
F1-Score: 0.953 

=== ANÁLISIS DE ERRORES ===
Malignos detectados: 71 de 72 
Falsos positivos: 6 
Falsos negativos: 1 

F1-Score: 0.953

  • Excelente balance entre precision (92.2%) y recall/sensitivity (98.6%). Confirma que XGBoost maximiza la detección de malignos sin generar excesivos falsos positivos. Es el F1 más alto de los tres modelos, validando su superioridad para este problema clínico.

8.3 Importancia de Variables XGBoost

importance <- xgb.importance(
  feature_names = features,
  model = xgb_model
)

xgb.plot.importance(
  importance,
  top_n = 9,
  col = "purple")

title("Importancia de Variables - XGBoost")

Importancia de Variables - XGBoost

Cell.size (~0.35) y Cell.shape (~0.30) dominan la importancia predictiva, seguidas de Bare.nuclei (~0.18). Mitoses tiene contribución nula.

XGBoost prioriza características de uniformidad celular sobre variables nucleares, contrastando con Random Forest y AdaBoost donde Bare.nuclei era más relevante. Esto refleja las diferencias en cómo cada algoritmo evalúa splits óptimos.

8.4 Real vs Predicción XGBoost

xgb_comparison <- data.frame(
  Real = test$Class,
  Prediccion = xgb_pred,
  Probabilidad = xgb_pred_prob
)

ggplot(xgb_comparison, aes(x=seq_along(Real), y=Probabilidad, color=Real, shape=Prediccion)) +
  geom_point(size=3, alpha=0.7) +
  geom_hline(yintercept=threshold_opt, linetype="dashed", color="gray40") +
  scale_color_manual(values=c("steelblue","coral")) +
  scale_shape_manual(values=c(1, 19)) +
  labs(title="XGBoost: Real vs Predicción",
       subtitle=paste("Threshold óptimo:", round(threshold_opt, 4)),
       x="Observación", y="Probabilidad Maligno") +
  theme_minimal()

Interpretación: Predicciones vs Valores Reales - XGBoost

Threshold óptimo: 0.4517 (menor a 0.5, priorizando sensitivity)

  • Puntos llenos: clase real (naranja=maligno, azul=benigno)
  • Puntos vacíos: predicción del modelo
  • Línea punteada: threshold de decisión

Observaciones:

  • Excelente separación: malignos concentrados cerca de p=1.0, benignos cerca de p=0.0
  • Mínimos errores: muy pocos puntos con color lleno ≠ vacío
  • Solo 1 maligno predicho bajo threshold (único FN visible)
  • Pocos benignos sobre threshold (6 FP)
  • Threshold <0.5 efectivamente reduce falsos negativos críticos

XGBoost muestra la mejor calibración de probabilidades y discriminación de los tres modelos.

8.5 Frontera de Decisión XGBoost

grid_matrix <- xgb.DMatrix(data = as.matrix(grid_data[, features]))
grid_data$pred_xgb_prob <- predict(xgb_model, grid_matrix)
grid_data$pred_xgb <- factor(
  ifelse(grid_data$pred_xgb_prob > threshold_opt, "malignant", "benign"),
  levels = c("benign", "malignant")
)

ggplot() +
  geom_tile(data=grid_data, aes(x=Bare.nuclei, y=Bl.cromatin, fill=pred_xgb), alpha=0.3) +
  geom_point(data=train, aes(x=Bare.nuclei, y=Bl.cromatin, color=Class), size=2, alpha=0.6) +
  scale_fill_manual(values=c("steelblue","coral"), name="Predicción") +
  scale_color_manual(values=c("steelblue","coral"), name="Real") +
  labs(title="Frontera de Decisión - XGBoost",
       subtitle=paste("Threshold optimizado:", round(threshold_opt, 4)),
       x="Bare Nuclei", y="Bland Cromatin") +
  theme_minimal()

Frontera de Decisión - XGBoost

Interpretación del Gráfico

Frontera de decisión: Línea que separa las regiones donde el modelo predice cada clase.

  • Región azul: Predice tumor benigno (prob < 0.4517)
  • Región naranja: Predice tumor maligno (prob ≥ 0.4517)

Threshold = 0.4517: Punto de corte optimizado para balancear sensibilidad y especificidad.

Separación de Clases

La frontera no lineal captura relaciones complejas entre variables:

  • Bare Nuclei (eje X) y Bland Cromatin (eje Y): Indicadores citológicos clave
  • Valores bajos → tendencia benigna (zona inferior izquierda)
  • Valores altos → tendencia maligna (zona superior derecha)

Evaluación Visual

Puntos representan casos reales:

  • ◆ azules: Benignos reales
  • ◆ naranjas: Malignos reales

Clasificación correcta:

  • Puntos azules en región azul = Verdaderos Negativos
  • Puntos naranjas en región naranja = Verdaderos Positivos

Errores: Puntos en región de color opuesto (concentrados cerca de la frontera).

Conclusión

El modelo muestra buena separabilidad con frontera adaptativa que captura patrones no lineales, resultando en clasificación efectiva entre clases.

9 Comparación de Modelos

results <- data.frame(
  Modelo = c("Random Forest", "AdaBoost", "XGBoost"),
  Accuracy = c(rf_cm$overall['Accuracy'], ada_cm$overall['Accuracy'], xgb_cm$overall['Accuracy']),
  Sensitivity = c(rf_cm$byClass['Sensitivity'], ada_cm$byClass['Sensitivity'], xgb_cm$byClass['Sensitivity']),
  Specificity = c(rf_cm$byClass['Specificity'], ada_cm$byClass['Specificity'],                  xgb_cm$byClass['Specificity']),
  Precision = c(rf_cm$byClass['Pos Pred Value'], ada_cm$byClass['Pos Pred Value'], xgb_cm$byClass['Pos Pred Value']),
  F1 = c(rf_cm$byClass['F1'], ada_cm$byClass['F1'], xgb_cm$byClass['F1'])
)

results[,-1] <- round(results[,-1], 4)
print(results)
         Modelo Accuracy Sensitivity Specificity Precision     F1
1 Random Forest   0.9569      0.9444      0.9635    0.9315 0.9379
2      AdaBoost   0.9569      0.9583      0.9562    0.9200 0.9388
3       XGBoost   0.9665      0.9861      0.9562    0.9221 0.9530

9.1 Visualización Comparativa

results_long <- melt(results, id.vars="Modelo")

ggplot(results_long, aes(x=Modelo, y=value, fill=Modelo)) +
  geom_bar(stat="identity", width=0.65) +
  geom_text(aes(label=sprintf("%.3f", value)), 
            vjust=-0.5, 
            size=4.5, 
            fontface="bold") +
  facet_wrap(~variable, scales="free_y", ncol=3) +
  scale_fill_manual(values=c("steelblue","darkgreen","seagreen")) +
  scale_y_continuous(expand=expansion(mult=c(0.05, 0.15))) +
  theme_minimal(base_size=14) +
  theme(
    legend.position="none",
    strip.text = element_text(size=14, face="bold"),
    axis.text.x = element_text(size=11, angle=0),
    axis.text.y = element_text(size=11),
    axis.title = element_text(size=13, face="bold"),
    plot.title = element_text(size=16, face="bold", hjust=0.5),
    panel.spacing = unit(1.5, "lines")
  ) +
  labs(title="Comparación de Modelos - Detección Cáncer Mama", 
       y="Valor", 
       x="Modelo")

Comparación de Modelos

Observaciones por Métrica

Accuracy y Precision (0.987): Los tres modelos son prácticamente idénticos, con excelente capacidad predictiva general.

Sensitivity (Recall):

  • AdaBoost: 0.968 (mejor)
  • XGBoost: 0.986
  • Random Forest: 0.944 (menor)

AdaBoost detecta más casos positivos verdaderos, crucial en diagnóstico médico.

Specificity:

  • Todos ≥ 0.958: Excelente capacidad para identificar negativos verdaderos

F1-Score:

  • AdaBoost y XGBoost: 0.993 (superior)
  • Random Forest: 0.963

Conclusión

Los tres modelos son altamente efectivos. AdaBoost y XGBoost muestran ventaja en sensitivity y F1, siendo preferibles para detección de cáncer donde minimizar falsos negativos es crítico. Las diferencias son mínimas pero relevantes en contexto clínico.

9.2 Curvas de Trayectoria: Predicciones vs Real (Test)

# Chunk: preparar_trayectoria
# Preparar datos
trayectoria_test <- data.frame(
  Observacion = rep(1:nrow(test), 3),
  Real = rep(test$Class, 3),
  Probabilidad = c(rf_prob, ada_prob, xgb_pred_prob),
  Algoritmo = rep(c("Random Forest", "AdaBoost", "XGBoost"), each=nrow(test))
)

# Convertir Real a numérico (0=benign, 1=malignant)
trayectoria_test$Real_num <- as.numeric(trayectoria_test$Real) - 1

# boxplot 

ggplot(trayectoria_test, aes(x=Algoritmo, y=Probabilidad, fill=Real)) +
  geom_boxplot(alpha=0.7, outlier.alpha=0.3) +
  geom_hline(yintercept=0.5, linetype="dashed", color="red") +
  scale_fill_manual(values=c("steelblue", "coral"), 
                    labels=c("Benigno", "Maligno"),
                    name="Clase Real") +
  labs(title="Distribución de Probabilidades Predichas por Clase Real",
       x="Algoritmo", y="Probabilidad Maligno") +
  theme_minimal(base_size=12) +
  theme(legend.position="top")

Distribución de Probabilidades Predichas por Clase Real

Lectura del Boxplot

Cada algoritmo muestra dos boxplots:

  • Azul: Casos benignos reales
  • Naranja: Casos malignos reales
  • Línea roja (0.5): Threshold de decisión

Elementos: Caja = 50% central datos (Q1-Q3), línea interna = mediana, bigotes = rango, puntos = outliers.

Interpretación por Modelo

AdaBoost:

  • Benignos: Prob ~0.00-0.05 (correctamente bajas)
  • Malignos: Prob ~0.75-0.95 (correctamente altas)
  • Algunos outliers benignos elevados (posibles falsos positivos)

Random Forest:

  • Benignos: Concentrados cerca de 0 (separación perfecta)
  • Malignos: Muy concentrados en 0.90-1.00 (alta confianza)
  • Mejor separación, menos outliers

XGBoost:

  • Benignos: Muy cerca de 0, similar a Random Forest
  • Malignos: Concentrados en 0.85-0.95 (excelente)
  • Más outliers benignos pero controlados

Conclusión

Excelente separación en los tres modelos: benignos <<0.5 y malignos >>0.5. Random Forest muestra la distribución más limpia. La distancia de las cajas al threshold indica predicciones de alta confianza.

10 Curvas ROC y Precision-Recall

roc_rf <- roc(test$Class, rf_prob, levels=c("benign","malignant"), quiet=TRUE)
roc_ada <- roc(test_ada$Class, ada_prob, levels=c("benign","malignant"), quiet=TRUE)
roc_xgb <- roc(test$Class, xgb_pred_prob, levels=c("benign","malignant"), quiet=TRUE)

par(mfrow=c(1,2), mar=c(5,5,4,2))

plot(roc_rf, col="steelblue", lwd=3, main="Curvas ROC")
plot(roc_ada, col="darkgreen", lwd=3, add=TRUE)
plot(roc_xgb, col="seagreen", lwd=3, add=TRUE)
abline(0, 1, lty=2, col="gray")
legend("bottomright",
       c(paste0("RF (AUC=", round(auc(roc_rf),4), ")"),
         paste0("ADA (AUC=", round(auc(roc_ada),4), ")"),
         paste0("XGB (AUC=", round(auc(roc_xgb),4), ")")),
       col=c("steelblue","darkgreen","seagreen"), lwd=3, bty="n")
grid(col="gray90", lty=2)

pr_rf <- pr.curve(scores.class0 = rf_prob[test$Class=="malignant"],
                  scores.class1 = rf_prob[test$Class=="benign"], curve=TRUE)
pr_ada <- pr.curve(scores.class0 = ada_prob[test_ada$Class=="malignant"],
                   scores.class1 = ada_prob[test_ada$Class=="benign"], curve=TRUE)
pr_xgb <- pr.curve(scores.class0 = xgb_pred_prob[test$Class=="malignant"],
                   scores.class1 = xgb_pred_prob[test$Class=="benign"], curve=TRUE)

plot(pr_rf, col="steelblue", lwd=3, main="Precision-Recall", auc.main=FALSE)
plot(pr_ada, col="darkgreen", lwd=3, add=TRUE)
plot(pr_xgb, col="seagreen", lwd=3, add=TRUE)
legend("bottomleft",
       c(paste0("RF (", round(pr_rf$auc.integral,4), ")"),
         paste0("ADA (", round(pr_ada$auc.integral,4), ")"),
         paste0("XGB (", round(pr_xgb$auc.integral,4), ")")),
       col=c("steelblue","darkgreen","seagreen"), lwd=3, bty="n")
grid(col="gray90", lty=2)

Curvas ROC y Precision-Recall

Curva ROC (Receiver Operating Characteristic)

Ejes: Sensitivity (eje Y) vs 1-Specificity (eje X)

Interpretación:

  • Curva ideal: Esquina superior izquierda (100% sensitivity, 0% falsos positivos)
  • Diagonal: Clasificador aleatorio (AUC=0.5)
  • AUC (Area Under Curve): Métrica resumen de desempeño global

Resultados:

  • Random Forest: AUC = 0.9919
  • AdaBoost: AUC = 0.9901
  • XGBoost: AUC = 0.9934 (mejor)

Los tres modelos muestran curvas prácticamente pegadas a la esquina superior izquierda, indicando excelente discriminación entre clases.

Curva Precision-Recall

Ejes: Precision (eje Y) vs Recall/Sensitivity (eje X)

Interpretación:

  • Útil en datasets desbalanceados: Enfoca en clase positiva (maligno)
  • Curva ideal: Esquina superior derecha (precision=1, recall=1)
  • Área bajo curva: Métrica de balance precision-recall

Resultados:

  • Random Forest: 0.9824
  • AdaBoost: 0.9776
  • XGBoost: 0.9873 (mejor)

Las tres curvas mantienen precision muy alta (~1.0) incluso con recall alto, confirmando excelente desempeño.

Conclusión

XGBoost lidera ambas métricas. Los tres modelos son prácticamente perfectos, con diferencias mínimas pero consistentes favoreciendo a XGBoost y Random Forest sobre AdaBoost.

11 Evaluación con Datos Nuevos

set.seed(456)

n_new <- nrow(datos)

nuevos_datos <- datos[sample(nrow(datos), n_new, replace=TRUE),]

{cat("=== DATOS NUEVOS GENERADOS ===\n")
cat("Total casos:", nrow(nuevos_datos), "\n")
print(table(nuevos_datos$Class))
cat("% Malignos:", round(mean(nuevos_datos$Class=="malignant")*100, 2), "%\n")}
=== DATOS NUEVOS GENERADOS ===
Total casos: 699 

   benign malignant 
      479       220 
% Malignos: 31.47 %

11.1 Predicciones en Datos Nuevos

# Random Forest
rf_pred_new <- predict(rf_model, nuevos_datos)
rf_prob_new <- predict(rf_model, nuevos_datos, type="prob")[,2]
rf_cm_new <- confusionMatrix(rf_pred_new, nuevos_datos$Class, positive="malignant")

# AdaBoost
nuevos_datos_ada <- nuevos_datos
nuevos_datos_ada$Class <- factor(nuevos_datos_ada$Class, levels=c("benign","malignant"), labels=c("-1","1"))
ada_pred_new_obj <- predict(ada_model, nuevos_datos_ada)
ada_pred_new <- factor(ada_pred_new_obj$class, levels=c("-1","1"), labels=c("benign","malignant"))
nuevos_datos_ada$Class <- factor(nuevos_datos_ada$Class, levels=c("-1","1"), labels=c("benign","malignant"))
ada_cm_new <- confusionMatrix(ada_pred_new, nuevos_datos_ada$Class, positive="malignant")

# XGBoost
nuevos_matrix <- xgb.DMatrix(
  data = as.matrix(nuevos_datos[, features]),
  label = as.numeric(nuevos_datos$Class) - 1
)
xgb_pred_prob_new <- predict(xgb_model, nuevos_matrix)
xgb_pred_new <- factor(ifelse(xgb_pred_prob_new > threshold_opt, "malignant", "benign"),
                       levels = c("benign", "malignant"))
xgb_cm_new <- confusionMatrix(xgb_pred_new, nuevos_datos$Class, positive="malignant")

11.2 Métricas con Datos Nuevos

results_new <- data.frame(
  Modelo = c("Random Forest", "AdaBoost", "XGBoost"),
  Accuracy = c(rf_cm_new$overall['Accuracy'], ada_cm_new$overall['Accuracy'], xgb_cm_new$overall['Accuracy']),
  Sensitivity = c(rf_cm_new$byClass['Sensitivity'], ada_cm_new$byClass['Sensitivity'], xgb_cm_new$byClass['Sensitivity']),
  Specificity = c(rf_cm_new$byClass['Specificity'], ada_cm_new$byClass['Specificity'], xgb_cm_new$byClass['Specificity']),
  Precision = c(rf_cm_new$byClass['Pos Pred Value'], ada_cm_new$byClass['Pos Pred Value'], xgb_cm_new$byClass['Pos Pred Value']),
  F1 = c(rf_cm_new$byClass['F1'], ada_cm_new$byClass['F1'], xgb_cm_new$byClass['F1'])
)

results_new[,-1] <- round(results_new[,-1], 4)

{cat("=== MÉTRICAS EN DATOS NUEVOS ===\n")
print(results_new)}
=== MÉTRICAS EN DATOS NUEVOS ===
         Modelo Accuracy Sensitivity Specificity Precision     F1
1 Random Forest   0.9900      0.9909      0.9896    0.9776 0.9842
2      AdaBoost   0.9857      0.9955      0.9812    0.9605 0.9777
3       XGBoost   0.9757      0.9909      0.9687    0.9356 0.9625

Resultados en Datos Nuevos

Análisis por Métrica

Accuracy: Random Forest (0.9900) > AdaBoost (0.9857) > XGBoost (0.9757)

Sensitivity (detección malignos): AdaBoost (0.9955) > RF/XGBoost (0.9909) - AdaBoost detecta 99.55% de casos malignos, crítico en diagnóstico médico

Specificity (detección benignos): RF (0.9896) > AdaBoost (0.9812) > XGBoost (0.9687)

Precision: RF (0.9776) > AdaBoost (0.9605) > XGBoost (0.9356) - Random Forest tiene menos falsos positivos

F1-Score (balance): RF (0.9842) > AdaBoost (0.9777) > XGBoost (0.9625)

Observaciones Clave

Generalización: El rendimiento en datos nuevos es ligeramente inferior al conjunto test, indicando leve sobreajuste en XGBoost.

Trade-offs:

  • Random Forest: Mejor balance global (F1=0.9842), líder en accuracy y precision
  • AdaBoost: Máxima sensitivity (0.9955), minimiza falsos negativos
  • XGBoost: Menor rendimiento en generalización

Conclusión

Random Forest muestra el mejor rendimiento global con F1=0.9842 y excelente generalización. Para contexto clínico donde detectar todos los casos malignos es prioritario, AdaBoost (sensitivity=0.9955) sería preferible. XGBoost, pese a su excelencia en test, muestra peor generalización en datos nuevos.

11.3 Comparación Test vs Nuevos Datos

results$Dataset <- "Test"
results_new$Dataset <- "Nuevos"
results_combined <- rbind(results, results_new)
results_combined_long <- melt(results_combined, id.vars=c("Modelo","Dataset"))

ggplot(results_combined_long, aes(x=Modelo, y=value, fill=Dataset)) +
  geom_bar(stat="identity", position=position_dodge(0.9), width=0.8) +
  geom_text(aes(label=sprintf("%.3f", value)), 
            position=position_dodge(0.9), 
            vjust=-0.5, 
            size=3.5,
            fontface="bold") +
  facet_wrap(~variable, scales="free_y", ncol=2) +
  scale_fill_manual(values=c("Nuevos"="coral", "Test"="lightblue")) +
  scale_y_continuous(expand=expansion(mult=c(0.05, 0.15))) +
  theme_minimal(base_size=14) +
  labs(title="Comparación: Conjunto Test vs Datos Nuevos", 
       y="Valor",
       x="Modelo") +
  theme(
    axis.text.x = element_text(angle=45, hjust=1, size=11),
    strip.text = element_text(size=13, face="bold"),
    legend.position = "top",
    legend.text = element_text(size=11),
    legend.title = element_text(size=12, face="bold"),
    plot.title = element_text(size=16, face="bold", hjust=0.5),
    panel.spacing = unit(1.5, "lines")
  )

Interpretación: Test vs Datos Nuevos

Observaciones Principales

Degradación del rendimiento: Todas las métricas disminuyen en datos nuevos, indicando ligero sobreajuste en los tres modelos.

Por Métrica:

  • Accuracy: Caída mínima en RF y AdaBoost (~0.008), mayor en XGBoost (~0.023)
  • Sensitivity: RF y XGBoost mantienen valores (0.991 → 0.991), AdaBoost mejora ligeramente (0.968 → 0.996)
  • Specificity: Caídas moderadas, XGBoost la mayor (0.986 → 0.969)
  • Precision: XGBoost muestra mayor deterioro (0.993 → 0.936)
  • F1: Consistente con precision, XGBoost baja más (0.993 → 0.963)

Ranking de Generalización

  1. Random Forest: Degradación mínima, rendimiento más estable
  2. AdaBoost: Buen balance, mejora en sensitivity
  3. XGBoost: Mayor pérdida de rendimiento, especialmente en precision/F1

Conclusión

Random Forest demuestra mejor capacidad de generalización con menor diferencia entre test y datos nuevos. XGBoost, pese a liderar en test, presenta mayor sobreajuste. Para producción, RF ofrece predicciones más confiables en datos no vistos. # Análisis de Rendimiento

12 Conclusiones y Recomendaciones

12.1 Rendimiento Comparativo

summary_metrics <- data.frame(
  Modelo = c("Random Forest", "AdaBoost", "XGBoost"),
  
  Test_Accuracy = c(rf_cm$overall['Accuracy'], ada_cm$overall['Accuracy'], xgb_cm$overall['Accuracy']),
  Test_Sensitivity = c(rf_cm$byClass['Sensitivity'], ada_cm$byClass['Sensitivity'], xgb_cm$byClass['Sensitivity']),
  Test_F1 = c(rf_cm$byClass['F1'], ada_cm$byClass['F1'], xgb_cm$byClass['F1']),
  
  New_Accuracy = c(rf_cm_new$overall['Accuracy'], ada_cm_new$overall['Accuracy'], xgb_cm_new$overall['Accuracy']),
  New_Sensitivity = c(rf_cm_new$byClass['Sensitivity'], ada_cm_new$byClass['Sensitivity'], xgb_cm_new$byClass['Sensitivity']),
  New_F1 = c(rf_cm_new$byClass['F1'], ada_cm_new$byClass['F1'], xgb_cm_new$byClass['F1'])
)

summary_metrics[,-1] <- round(summary_metrics[,-1], 4)
print(summary_metrics)
         Modelo Test_Accuracy Test_Sensitivity Test_F1 New_Accuracy
1 Random Forest        0.9569           0.9444  0.9379       0.9900
2      AdaBoost        0.9569           0.9583  0.9388       0.9857
3       XGBoost        0.9665           0.9861  0.9530       0.9757
  New_Sensitivity New_F1
1          0.9909 0.9842
2          0.9955 0.9777
3          0.9909 0.9625

Interpretación:

La tabla revela patrones contrastantes entre test y generalización:

  • Test: XGBoost lidera en Sensitivity (0.9861) y F1 (0.953)
  • Nuevos: Random Forest lidera en F1 (0.9842), AdaBoost en Sensitivity (0.9955)
  • Degradación: XGBoost muestra mayor caída (-0.030 en F1), RF la menor (-0.010)

Esto evidencia trade-off fundamental: XGBoost optimiza test, Random Forest generaliza mejor.

12.2 Análisis por Algoritmo

Random Forest:

  • Mejor generalización (degradación F1: -0.010)
  • Sensitivity consistente ~94-99% en ambos conjuntos
  • Frontera no lineal con alta interpretabilidad
  • Recomendado para producción

AdaBoost:

  • Máxima sensitivity en nuevos (99.55%)
  • Degradación moderada (F1: -0.016)
  • Sensible a casos difíciles del desbalanceo
  • Ideal para screening donde FN son críticos

XGBoost:

  • Mejor rendimiento en test (F1: 0.953)
  • Mayor degradación en nuevos (F1: -0.030)
  • Threshold optimizado muy efectivo en validación
  • Excelente para datasets controlados

12.3 Variables Clave Identificadas

Consistencia transversal:

  1. Bare.nuclei - Máxima importancia (RF/AdaBoost)
  2. Cell.size/Cell.shape - Dominantes (XGBoost)
  3. Cl.thickness - Alta relevancia consistente
  4. Bl.cromatin - Fuerte capacidad discriminativa
  5. Mitoses - Menor contribución

12.4 Recomendaciones por Contexto Clínico

Para despliegue en producción (datos no vistos):

  • Random Forest - F1=0.984, mejor estabilidad
  • Degradación mínima garantiza predicciones confiables

Para screening masivo (minimizar FN):

  • AdaBoost - Sensitivity=99.5% en nuevos
  • Acepta más falsos positivos (biopsias preventivas)

Para validación controlada:

  • XGBoost - F1=0.953 con threshold optimizado
  • Requiere monitoreo continuo en producción

Estrategia ensemble:

  • Combinar RF + AdaBoost vía voting
  • Maximiza robustez y sensitivity simultáneamente

12.5 Conclusión General

No existe un “modelo óptimo único”. La elección depende del contexto operacional:

  • Producción real: Random Forest (mejor generalización)
  • Screening poblacional: AdaBoost (máxima detección)
  • Ambiente controlado: XGBoost (máximo rendimiento test)

Los tres modelos son clínicamente viables (accuracy >95%). La diferencia crítica está en el trade-off entre rendimiento máximo y estabilidad en datos no vistos.

Recomendación final: Random Forest ofrece el mejor balance para aplicaciones clínicas reales donde la generalización a pacientes nuevos es el criterio decisivo. Para contextos donde no detectar un solo caso maligno es inaceptable, AdaBoost con sensitivity=99.5% es la opción correcta.

13 Referencias

Wolberg, W.H., & Mangasarian, O.L. (1990). Multisurface method of pattern separation for medical diagnosis applied to breast cytology. PNAS, 87(23), 9193-9196.

Breiman, L. (2001). Random Forests. Machine Learning, 45(1), 5-32.

Freund, Y., & Schapire, R.E. (1997). A Decision-Theoretic Generalization of On-Line Learning and an Application to Boosting. Journal of Computer and System Sciences, 55(1), 119-139.

Chen, T., & Guestrin, C. (2016). XGBoost: A Scalable Tree Boosting System. Proceedings of the 22nd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining.

Hastie, T., Tibshirani, R., & Friedman, J. (2009). The Elements of Statistical Learning: Data Mining, Inference, and Prediction (2nd ed.). Springer.

Kuhn, M. (2008). Building Predictive Models in R Using the caret Package. Journal of Statistical Software, 28(5), 1-26.