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 <- BreastCancerProyecto: Ensamble de Algoritmos - Random Forest, AdaBoost y XGBoost
Detección de Cáncer de Mama - Análisis Comparativo con Desbalanceo Moderado
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
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\):
- Extraer muestra bootstrap de tamaño \(N\) del conjunto de entrenamiento
- 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\):
Ajustar clasificador \(G_m(x)\) a los datos con pesos \(w_i\)
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)
Calcular coeficiente del clasificador: \[\alpha_m = \log\left(\frac{1 - \text{err}_m}{\text{err}_m}\right)\]
Actualizar pesos: \[w_i \leftarrow w_i \cdot \exp[\alpha_m \cdot \mathbb{I}(y_i \neq G_m(x_i))]\]
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\):
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}}\]
Ajustar árbol a \(\{(x_i, r_{im})\}_{i=1}^N\), obteniendo regiones \(R_{jm}\)
Optimizar coeficientes: \[\gamma_{jm} = \arg\min_\gamma \sum_{x_i \in R_{jm}} L(y_i, f_{m-1}(x_i) + \gamma)\]
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
- Random Forest: Degradación mínima, rendimiento más estable
- AdaBoost: Buen balance, mejora en sensitivity
- 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:
- Bare.nuclei - Máxima importancia (RF/AdaBoost)
- Cell.size/Cell.shape - Dominantes (XGBoost)
- Cl.thickness - Alta relevancia consistente
- Bl.cromatin - Fuerte capacidad discriminativa
- 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.