library(tidyverse) # Manipulación de datos
library(rpart) # Árboles de decisión
library(rpart.plot) # Visualización de árboles
library(caret) # Matrices de confusión
library(knitr) # Tablas bonitas1. Introducción
En esta práctica implementaremos árboles de decisión para clasificar el éxito de proyectos de software en tres niveles: Alto, Medio y Bajo. Se realizará:
- Construcción de árboles de decisión
- Poda del árbol para evitar sobreajuste
- Análisis de tres escenarios diferentes
- Evaluación y comparación de modelos
1.1 Librerías necesarias
1.2 Carga y preparación de datos
# Cargar datos
datos <- read.csv("datos_proyectos_software_limpio.csv")
# Primero extraer los niveles existentes en los datos
niveles_exito <- sort(unique(datos$exito_proyecto))
niveles_metodologia <- sort(unique(datos$metodologia_num))
niveles_stack <- sort(unique(datos$stack_tecnologico_num))
niveles_pruebas <- sort(unique(datos$pruebas_automatizadas_num))
datos <-datos %>%
mutate(
exito_proyecto = factor(exito_proyecto, levels = niveles_exito),
metodologia_num = factor(metodologia_num, levels = niveles_metodologia),
stack_tecnologico_num = factor(stack_tecnologico_num, levels = niveles_stack),
pruebas_automatizadas_num = factor(pruebas_automatizadas_num,
levels = niveles_pruebas)
)
# Dividir datos en entrenamiento y prueba
set.seed(1987)
indice_train <- createDataPartition(datos$exito_proyecto, p = 0.7, list = FALSE)
datos_train <- datos[indice_train, ]
datos_test <- datos[-indice_train, ]
# Mostrar distribución de clases
print(prop.table(table(datos_train$exito_proyecto)))
Alto Bajo Medio
0.5786963 0.1717011 0.2496025
print(prop.table(table(datos_test$exito_proyecto)))
Alto Bajo Medio
0.5805243 0.1685393 0.2509363
2. Escenarios de modelado
2.1 Escenario 1: Todas las variables
# Definir todas las variables predictoras
todas_vars <- setdiff(names(datos), c("exito_proyecto", "id_proyecto", "fecha_inicio"))
formula_todas <- as.formula(paste("exito_proyecto ~", paste(todas_vars, collapse = " + ")))
# Entrenar árbol inicial
arbol_todas <- rpart(
formula_todas,
data = datos_train,
method = "class",
parms = list(split = "information"), # usar ganancia de información
control = rpart.control(cp = 0.001) # árbol grande inicial
)
# Análisis de complejidad del árbol
printcp(arbol_todas)
Classification tree:
rpart(formula = formula_todas, data = datos_train, method = "class",
parms = list(split = "information"), control = rpart.control(cp = 0.001))
Variables actually used in tree construction:
[1] errores_por_kloc metodologia_num puntuacion_calidad
[4] satisfaccion_cliente
Root node error: 265/629 = 0.4213
n= 629
CP nsplit rel error xerror xstd
1 0.430189 0 1.000000 1.000000 0.046731
2 0.286792 1 0.569811 0.603774 0.041217
3 0.139623 2 0.283019 0.290566 0.031020
4 0.098113 3 0.143396 0.147170 0.022824
5 0.018868 4 0.045283 0.056604 0.014440
6 0.001000 5 0.026415 0.064151 0.015347
plotcp(arbol_todas)De todas las variables disponibles en el dataset, solo estas 4 (errores_por_kloc, metodologia_num, puntuacion_calidad, satisfaccion_cliente) fueron consideradas útiles por el algoritmo para hacer las divisiones.
De las 629 observaciones totales, 265 están mal clasificadas en el nodo raíz. Esto representa un error inicial del 42.13%. Este es el error antes de hacer cualquier división en el árbol.
Donde:
CP (Complexity Parameter): Es el parámetro de complejidad que controla el tamaño del árbol. Un CP más alto significa un árbol más pequeño. Un CP más bajo permite más divisiones.
nsplit: Número de divisiones realizadas en el árbol. Va de 0 (árbol sin divisiones) a 5 (árbol con 5 divisiones).
rel error: Error relativo en los datos de entrenamiento. Comienza en 1 y va disminuyendo con cada división. Muestra cómo mejora el modelo en los datos de entrenamiento.
xerror: Error de validación cruzada. Es una estimación más realista del error verdadero Si aumenta, puede indicar sobreajuste.
xstd: Desviación estándar del error de validación cruzada. Indica la variabilidad del error en la validación cruzada.
Podemos observar que:
El error relativo disminuye constantemente (de 1.0 a 0.026)
La mejor solución podría ser usar 3 o 4 divisiones, ya que después:
El xerror no mejora significativamente
Incluso aumenta un poco (de 0.056 a 0.064)
Esto sugiere que más divisiones podrían llevar a sobreajuste
# Encontrar CP óptimo
cp_optimo_todas <- arbol_todas$cptable[which.min(arbol_todas$cptable[,"xerror"]), "CP"]
# Podar el árbol
arbol_todas_podado <- prune(arbol_todas, cp = cp_optimo_todas)
# Visualizar árbol podado
rpart.plot(arbol_todas_podado,
type = 1,
extra = 104,
main = "Árbol de decisión - todas las variables")En este árbol de clasificación, la variable más importante para separar las clases es satisfaccion_cliente (nivel de satisfacción mayor o igual a 80). El árbol se visualiza mediante rectángulos (nodos) de diferentes colores: anaranjado para la categoría “Alto”, gris para “Bajo” y verde para “Medio”. Cada nodo muestra tres tipos de información clave: la clase predicha en la primera línea (por ejemplo, “Alto”), seguida de tres números que representan la distribución de las clases (por ejemplo, 1.00 .00 .00 significa que el 100% son “Alto”, 0% “Bajo” y 0% “Medio”), y finalmente el porcentaje de datos totales en ese nodo (por ejemplo, 58%). El color de cada nodo se determina por la clase más frecuente en ese grupo de datos - por ejemplo, si vemos un nodo anaranjado con valores 1.00 .00 .00, significa que todos los datos en ese nodo son de la categoría “Alto” (100% de pureza), representando el 58% del total de datos. Esta visualización nos permite entender fácilmente cómo el árbol va tomando decisiones y clasificando los datos en grupos cada vez más puros.
# Evaluar modelo
pred_todas <- predict(arbol_todas_podado, datos_test, type = "class")
conf_matrix_todas <- confusionMatrix(pred_todas, as.factor(datos_test$exito_proyecto))
print("Resultados - modelo con todas las variables:")[1] "Resultados - modelo con todas las variables:"
print(conf_matrix_todas)Confusion Matrix and Statistics
Reference
Prediction Alto Bajo Medio
Alto 155 0 0
Bajo 0 42 1
Medio 0 3 66
Overall Statistics
Accuracy : 0.985
95% CI : (0.9621, 0.9959)
No Information Rate : 0.5805
P-Value [Acc > NIR] : < 2.2e-16
Kappa : 0.9738
Mcnemar's Test P-Value : NA
Statistics by Class:
Class: Alto Class: Bajo Class: Medio
Sensitivity 1.0000 0.9333 0.9851
Specificity 1.0000 0.9955 0.9850
Pos Pred Value 1.0000 0.9767 0.9565
Neg Pred Value 1.0000 0.9866 0.9949
Prevalence 0.5805 0.1685 0.2509
Detection Rate 0.5805 0.1573 0.2472
Detection Prevalence 0.5805 0.1610 0.2584
Balanced Accuracy 1.0000 0.9644 0.9850
El modelo de clasificación muestra un rendimiento excepcional, con una precisión global del 98.5% y un coeficiente Kappa de 0.9738, lo que indica un acuerdo casi perfecto más allá del azar. La matriz de confusión revela que el modelo es particularmente efectivo en identificar la clase “Alto” con un 100% de precisión y sensibilidad, mientras que mantiene un rendimiento muy sólido en las clases “Bajo” y “Medio” con tasas de acierto superiores al 93%. Los pocos errores de clasificación se limitan a confusiones menores entre las categorías “Bajo” y “Medio” (solo 4 casos mal clasificados de un total de 267), lo que demuestra la robustez y fiabilidad del modelo para esta tarea de clasificación. Las altas métricas en todas las categorías (sensibilidad, especificidad y valores predictivos) confirman que el modelo es equilibrado y efectivo en su capacidad de discriminación, independientemente de la clase que se esté prediciendo.
2.2 Escenario 2: Variables numéricas
# Variables numericas continuas unicamente
vars_numericas <- c("tamano_equipo",
"duracion_meses",
"errores_por_kloc",
"puntuacion_calidad",
"satisfaccion_cliente")
formula_numericas <- as.formula(paste("exito_proyecto ~", paste(vars_numericas, collapse = " + ")))
# Entrenar árbol inicial
arbol_numericas <- rpart(
formula_numericas,
data = datos_train,
method = "class",
parms = list(split = "information"),
control = rpart.control(cp = 0.001)
)
# Análisis de complejidad y poda
plotcp(arbol_numericas)printcp(arbol_numericas)
Classification tree:
rpart(formula = formula_numericas, data = datos_train, method = "class",
parms = list(split = "information"), control = rpart.control(cp = 0.001))
Variables actually used in tree construction:
[1] errores_por_kloc puntuacion_calidad satisfaccion_cliente
[4] tamano_equipo
Root node error: 265/629 = 0.4213
n= 629
CP nsplit rel error xerror xstd
1 0.4301887 0 1.000000 1.000000 0.046731
2 0.2867925 1 0.569811 0.584906 0.040783
3 0.0981132 2 0.283019 0.298113 0.031363
4 0.0566038 3 0.184906 0.207547 0.026734
5 0.0188679 5 0.071698 0.094340 0.018489
6 0.0018868 6 0.052830 0.098113 0.018840
7 0.0010000 10 0.045283 0.098113 0.018840
cp_optimo_numericas <- arbol_numericas$cptable[which.min(arbol_numericas$cptable[,"xerror"]), "CP"]
arbol_numericas <- prune(arbol_numericas, cp = cp_optimo_numericas)
# Visualizar árbol podado
rpart.plot(arbol_numericas,
type = 1,
extra = 104,
main = "Árbol de Decisión - variables numéricas")# Evaluar modelo
pred_numericas <- predict(arbol_numericas, datos_test, type = "class")
conf_matrix_numericas <- confusionMatrix(pred_numericas, as.factor(datos_test$exito_proyecto))
print("Resultados - Modelo con variables numéricas")[1] "Resultados - Modelo con variables numéricas"
print(conf_matrix_numericas)Confusion Matrix and Statistics
Reference
Prediction Alto Bajo Medio
Alto 155 0 5
Bajo 0 42 1
Medio 0 3 61
Overall Statistics
Accuracy : 0.9663
95% CI : (0.937, 0.9845)
No Information Rate : 0.5805
P-Value [Acc > NIR] : < 2.2e-16
Kappa : 0.9403
Mcnemar's Test P-Value : NA
Statistics by Class:
Class: Alto Class: Bajo Class: Medio
Sensitivity 1.0000 0.9333 0.9104
Specificity 0.9554 0.9955 0.9850
Pos Pred Value 0.9688 0.9767 0.9531
Neg Pred Value 1.0000 0.9866 0.9704
Prevalence 0.5805 0.1685 0.2509
Detection Rate 0.5805 0.1573 0.2285
Detection Prevalence 0.5993 0.1610 0.2397
Balanced Accuracy 0.9777 0.9644 0.9477
En este segundo árbol de clasificación, la variable principal sigue siendo satisfaccion_cliente con un umbral de 80, pero ahora se observa una estructura más compleja con más niveles de decisión. Los nodos mantienen el mismo esquema de colores (anaranjado para “Alto”, gris para “Bajo” y verde para “Medio”) y la misma estructura de información: clase predicha, distribución de probabilidades y porcentaje del total de datos. En este segundo árbol de clasificación, aunque tiene una estructura más compleja con múltiples niveles de decisión basados en distintos umbrales de satisfacción del cliente (80, 82, 75), errores por kloc, puntuación de calidad, mantiene un rendimiento excepcional. El modelo alcanza una precisión global del 96.63% (con un intervalo de confianza del 95% entre 93.7% y 98.45%) y un coeficiente Kappa de 0.940, indicando un acuerdo casi perfecto.
2.3 Escenario 3: Variables correlacionadas
# Variables correlacionadas segun analisis previo
vars_correlacionada <- c("satisfaccion_cliente", "puntuacion_calidad","metodologia_num")
formula_correlacionada <- as.formula(paste("exito_proyecto ~", paste(vars_correlacionada, collapse = " + ")))
# Entrenar árbol inicial
arbol_correlacionada <- rpart(
formula_correlacionada,
data = datos_train,
method = "class",
parms = list(split = "information"),
control = rpart.control(cp = 0.001)
)
# Análisis de complejidad y poda
plotcp(arbol_correlacionada)cp_optimo_correlacionada <- arbol_correlacionada$cptable[which.min(arbol_correlacionada$cptable[,"xerror"]), "CP"]
arbol_correlacionada <- prune(arbol_correlacionada, cp = cp_optimo_correlacionada)
# Visualizar árbol podado
rpart.plot(arbol_correlacionada,
type = 1,
extra = 104,
main = "Árbol de Decisión - variables correlacionadas")# Evaluar modelo
pred_correlacionada <- predict(arbol_correlacionada, datos_test, type = "class")
conf_matrix_correlacionada <- confusionMatrix(pred_correlacionada, as.factor(datos_test$exito_proyecto))
print("Resultados - Modelo con variables correlacionadas:")[1] "Resultados - Modelo con variables correlacionadas:"
print(conf_matrix_correlacionada)Confusion Matrix and Statistics
Reference
Prediction Alto Bajo Medio
Alto 155 0 0
Bajo 0 42 1
Medio 0 3 66
Overall Statistics
Accuracy : 0.985
95% CI : (0.9621, 0.9959)
No Information Rate : 0.5805
P-Value [Acc > NIR] : < 2.2e-16
Kappa : 0.9738
Mcnemar's Test P-Value : NA
Statistics by Class:
Class: Alto Class: Bajo Class: Medio
Sensitivity 1.0000 0.9333 0.9851
Specificity 1.0000 0.9955 0.9850
Pos Pred Value 1.0000 0.9767 0.9565
Neg Pred Value 1.0000 0.9866 0.9949
Prevalence 0.5805 0.1685 0.2509
Detection Rate 0.5805 0.1573 0.2472
Detection Prevalence 0.5805 0.1610 0.2584
Balanced Accuracy 1.0000 0.9644 0.9850
3. Comparación de modelos
# Crear tabla comparativa
comparacion_modelos <- data.frame(
Modelo = c("Todas las variables", "Variables numéricas", "Variables correlacionadas"),
Accuracy = c(
conf_matrix_todas$overall["Accuracy"],
conf_matrix_numericas$overall["Accuracy"],
conf_matrix_correlacionada$overall["Accuracy"]
),
Kappa = c(
conf_matrix_todas$overall["Kappa"],
conf_matrix_numericas$overall["Kappa"],
conf_matrix_correlacionada$overall["Kappa"]
),
Complejidad_CP = c(
cp_optimo_todas,
cp_optimo_numericas,
cp_optimo_correlacionada
),
Nodos_Terminales = c(
sum(arbol_todas_podado$frame$var == "<leaf>"),
sum(arbol_numericas$frame$var == "<leaf>"),
sum(arbol_correlacionada$frame$var == "<leaf>")
)
)
# Mostrar tabla
kable(comparacion_modelos,
caption = "Comparación de modelos",
digits = 3)| Modelo | Accuracy | Kappa | Complejidad_CP | Nodos_Terminales |
|---|---|---|---|---|
| Todas las variables | 0.985 | 0.974 | 0.019 | 5 |
| Variables numéricas | 0.966 | 0.940 | 0.019 | 6 |
| Variables correlacionadas | 0.985 | 0.974 | 0.001 | 5 |
# Convertir a formato largo para visualización
comparacion_larga <- comparacion_modelos %>%
select(Modelo, Accuracy, Kappa) %>%
pivot_longer(
cols = c(Accuracy, Kappa),
names_to = "Metrica",
values_to = "Valor"
)
# Visualizar comparación
ggplot(comparacion_larga, aes(x = Modelo, y = Valor, fill = Metrica)) +
geom_bar(stat = "identity", position = "dodge") +
labs(title = "Comparación de modelos por métrica",
y = "Valor",
x = "Modelo") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
scale_fill_brewer(palette = "Set2")4. Análisis de variables importantes
# Función para extraer importancia de variables
obtener_importancia <- function(modelo, nombre) {
# Verificar si el modelo tiene variables importantes
if(is.null(modelo$variable.importance) || length(modelo$variable.importance) == 0) {
cat("No se encontraron variables importantes para el modelo:", nombre, "\n")
return(NULL)
}
# Crear dataframe con las variables importantes
data.frame(
Modelo = rep(nombre, length(modelo$variable.importance)),
Variable = names(modelo$variable.importance),
Importancia = as.numeric(modelo$variable.importance),
stringsAsFactors = FALSE
) %>%
arrange(desc(Importancia))
}
# Obtener importancia para cada modelo
imp_todas <- obtener_importancia(arbol_todas_podado, "Todas las variables")
imp_numericas <- obtener_importancia(arbol_numericas, "Variables numéricas")
imp_correlacionada <- obtener_importancia(arbol_correlacionada, "Variables correlacionadas")
# Combinar resultados, excluyendo NULLs
importancia_total <- bind_rows(
list(imp_todas, imp_numericas, imp_correlacionada)
)
# Visualizar importancia si hay datos
if(!is.null(importancia_total) && nrow(importancia_total) > 0) {
# Crear gráfico
plot_importancia <- ggplot(importancia_total,
aes(x = reorder(Variable, Importancia),
y = Importancia)) +
geom_bar(stat = "identity", fill = "steelblue") +
facet_wrap(~Modelo, scales = "free_y") +
coord_flip() +
labs(title = "Importancia de variables por modelo",
x = "Variable",
y = "Importancia") +
theme_minimal()
# Mostrar gráfico
print(plot_importancia)
# Mostrar tabla
kable(importancia_total %>%
arrange(Modelo, desc(Importancia)),
caption = "Importancia de variables por modelo",
digits = 3)
} else {
cat("No se encontraron variables importantes en ninguno de los modelos\n")
}| Modelo | Variable | Importancia |
|---|---|---|
| Todas las variables | satisfaccion_cliente | 420.678 |
| Todas las variables | metodologia_num | 331.067 |
| Todas las variables | puntuacion_calidad | 149.865 |
| Todas las variables | errores_por_kloc | 4.273 |
| Todas las variables | tamano_equipo | 3.831 |
| Variables correlacionadas | satisfaccion_cliente | 420.678 |
| Variables correlacionadas | metodologia_num | 331.067 |
| Variables correlacionadas | puntuacion_calidad | 149.865 |
| Variables numéricas | satisfaccion_cliente | 455.830 |
| Variables numéricas | puntuacion_calidad | 210.524 |
| Variables numéricas | errores_por_kloc | 7.314 |
| Variables numéricas | tamano_equipo | 3.831 |
| Variables numéricas | duracion_meses | 1.014 |
Al analizar los tres modelos de árboles de decisión (todas las variables, variables correlacionadas y variables numéricas), se observa un patrón consistente en la importancia de las variables:
La variable
satisfaccion_clientees claramente la más influyente en los tres modelos, manteniendo el nivel más alto de importancia (aproximadamente 400 unidades) en todas las versiones.En el modelo de “todas las variables”:
metodologia_numes la segunda más importante.puntuacion_calidadtiene una importancia moderada.errores_por_klocy tamano_equipotienen una importancia muy baja.
Para el modelo de “variables correlacionadas”:
metodologia_nummantiene una importancia alta.puntuacion_calidadmuestra una importancia moderada,
En el modelo de “variables numéricas”:
puntuacion_calidades la segunda más importante.Las demás variables (
errores_por_kloc,tamano_equipo,duracion_meses) tienen una importancia mínima.
Esto sugiere que la satisfacción del cliente es el factor más determinante para la clasificación, seguido por la metodología utilizada y la puntuación de calidad, independientemente del conjunto de variables consideradas.
5. Selección de mejor modelo
Ahora obtendremos y guardaremos el mejor modelo de árbol de clasificación.
# Identificar y guardar mejor modelo
nombres_modelos <- c("Árbol con todas las variables",
"Árbol con variables numéricas",
"Árbol con variables correlacionadas")
indice_mejor <- which.max(comparacion_modelos$Accuracy)
mejor_modelo <- list(
arbol_todas_podado,
arbol_numericas,
arbol_correlacionada
)[[indice_mejor]]
# Métricas del mejor modelo
metricas_mejor <- data.frame(
Metrica = c("Modelo Seleccionado", "Accuracy", "Kappa", "Nodos Terminales", "CP Óptimo"),
Valor = c(
nombres_modelos[indice_mejor],
max(comparacion_modelos$Accuracy),
comparacion_modelos$Kappa[indice_mejor],
comparacion_modelos$Nodos_Terminales[indice_mejor],
comparacion_modelos$Complejidad_CP[indice_mejor]
)
)
kable(metricas_mejor,
caption = "Métricas del mejor modelo",
align = c('l', 'r')) # Alineación: texto a la izquierda, números a la derecha| Metrica | Valor |
|---|---|
| Modelo Seleccionado | Árbol con todas las variables |
| Accuracy | 0.98501872659176 |
| Kappa | 0.973763081609591 |
| Nodos Terminales | 5 |
| CP Óptimo | 0.0188679245283019 |
El análisis de los tres modelos de árboles de decisión revela que el “Árbol con todas las variables” fue el de mejor rendimiento. Este modelo alcanzó una precisión (Accuracy) excepcional del 98.5% y un coeficiente Kappa de 0.974, lo que indica un acuerdo casi perfecto más allá del azar. La estructura del árbol es relativamente simple, con solo 5 nodos terminales, lo que sugiere un buen balance entre complejidad y capacidad predictiva. El parámetro de complejidad (CP) óptimo de 0.019 indica que el modelo fue podado adecuadamente para evitar el sobreajuste mientras mantiene su alto rendimiento. Estos resultados sugieren que considerar todas las variables disponibles proporcionó la mejor capacidad predictiva sin sacrificar la interpretabilidad del modelo.
Reglas del mejor modelo
# Visualización de reglas de decisión
# Obtener y mostrar reglas
reglas_texto <- capture.output(rpart.rules(mejor_modelo, style = "wide"))
# Crear tabla de reglas
reglas_df <- data.frame(
"Regla N°" = seq_along(reglas_texto[-1]),
"Descripción" = reglas_texto[-1]
)
# Mostrar tabla
kable(reglas_df,
caption = "Reglas de decisión del modelo",
col.names = c("Regla N°", "Descripción de la regla"),
align = c('c', 'l'))| Regla N° | Descripción de la regla |
|---|---|
| 1 | Alto [ .98 .00 .02] when satisfaccion_cliente >= 80 & metodologia_num is 1 or 3 |
| 2 | Bajo [ .00 1.00 .00] when satisfaccion_cliente < 75 & puntuacion_calidad >= 55 |
| 3 | Medio [ .00 .16 .84] when satisfaccion_cliente < 75 & puntuacion_calidad < 55 |
| 4 | Medio [ .00 .00 1.00] when satisfaccion_cliente >= 80 & metodologia_num is 2 |
| 5 | Medio [ .00 .00 1.00] when satisfaccion_cliente is 75 to 80 |
Preparación para siguientes prácticas
El mejor modelo generado en esta práctica servirá como base para: - Práctica 4: Desarrollo de una aplicación Shiny para predicciones.
# 1. Obtener importancia del mejor modelo
importancia_mejor <- obtener_importancia(mejor_modelo, "Mejor modelo") %>%
arrange(desc(Importancia))
# 2. Preparar información necesaria para el modelo
modelo_info <- list(
modelo = mejor_modelo,
variables = names(mejor_modelo$variable.importance),
niveles_metodologia = niveles_metodologia,
niveles_stack = niveles_stack,
niveles_pruebas = niveles_pruebas,
niveles_exito = niveles_exito,
metricas = metricas_mejor,
importancia_variables = importancia_mejor
)
# 3. Guardar toda la información
# Guardar mejor modelo
saveRDS(modelo_info, "mejor_modelo_arbol.rds")
# 4. Verificar que se guardó correctamente
modelo_cargado <- readRDS("mejor_modelo_arbol.rds")
# 5. Mostrar importancia de variables guardadas
kable(modelo_cargado$importancia_variables,
caption = "Importancia de variables en el modelo guardado",
digits = 3)| Modelo | Variable | Importancia |
|---|---|---|
| Mejor modelo | satisfaccion_cliente | 420.678 |
| Mejor modelo | metodologia_num | 331.067 |
| Mejor modelo | puntuacion_calidad | 149.865 |
| Mejor modelo | errores_por_kloc | 4.273 |
| Mejor modelo | tamano_equipo | 3.831 |