library(ggplot2)
library(corrplot)
library(lmtest)
library(car)
library(lmtest)
library(sandwich)

1 Planteamiento del Problema e Hipótesis

1.1 Contexto Operativo

En los proyectos de implementación y personalización de Odoo ERP, el equipo de Mesa de Ayuda de una empresa Colombiana Partner Gold de Odoo gestiona diariamente cientos de tickets que van desde pequeñas configuraciones hasta desarrollos técnicos complejos en Python y OWL/JavaScript.

Una pregunta estratégica para la dirección de proyectos es:

¿Existe una relación entre el tiempo real que tarda un desarrollador en resolver un ticket (horas invertidas) y el nivel de satisfacción que reporta el cliente al cierre de ese ticket?

Esta pregunta tiene implicaciones directas en la asignación de recursos, los acuerdos de nivel de servicio con clientes Gold y la rentabilidad del proyecto.

1.2 Hipótesis

  • H₀ (Hipótesis nula): No existe correlación significativa entre la complejidad del ticket y su costo.

  • H₁ (Hipótesis alternativa): Existe una correlación positiva significativa entre la complejidad del ticket y su costo.

Nivel de significancia: α = 0.05


2 Base de Datos

2.1 Diccionario de Variables

El dataset odoo_tickets_1000.csv contiene información de 1.000 tickets registrados en el área de Mesa de Ayuda. Las 10 variables son:

Las variables de análisis principal son: > complejidad (X) y costo_ticket (Y).

2.2 Carga y Vista Previa de Datos

df <- read.csv("odoo_tickets_1000.csv", stringsAsFactors = FALSE)

cat("Dimensiones:", nrow(df), "filas x", ncol(df), "columnas\n")
## Dimensiones: 1000 filas x 10 columnas
cat("Variables  :", paste(names(df), collapse = ", "), "\n")
## Variables  : complejidad, horas_reales, bugs_reportados, retrabajos, satisfaccion, experiencia_dev, horas_qa, n_revisiones, costo_ticket, dias_resolucion
head(df, 10)
na_counts <- colSums(is.na(df))
if (sum(na_counts) == 0) {
  cat("No se detectaron valores faltantes en el dataset.\n")
} else {
  cat("Valores faltantes por variable:\n")
  print(na_counts[na_counts > 0])
}
## No se detectaron valores faltantes en el dataset.

2.3 Estadísticas Descriptivas

vars_num <- c("complejidad","horas_reales","bugs_reportados","retrabajos",
              "satisfaccion","experiencia_dev","horas_qa",
              "n_revisiones","costo_ticket","dias_resolucion")

resumen <- data.frame(
  Variable = vars_num,
  n        = sapply(df[vars_num], function(x) sum(!is.na(x))),
  Media    = sapply(df[vars_num], function(x) round(mean(x, na.rm=TRUE), 2)),
  Mediana  = sapply(df[vars_num], function(x) round(median(x, na.rm=TRUE), 2)),
  DE       = sapply(df[vars_num], function(x) round(sd(x, na.rm=TRUE), 2)),
  Min      = sapply(df[vars_num], function(x) round(min(x, na.rm=TRUE), 2)),
  Max      = sapply(df[vars_num], function(x) round(max(x, na.rm=TRUE), 2))
)

resumen

3 Análisis Individual de Variables

3.1 Variable X — complejidad (Puntos de Esfuerzo)

3.1.1 Tabla de Frecuencias

freq_comp <- as.data.frame(table(df$complejidad))
names(freq_comp) <- c("Puntos", "Frecuencia")
freq_comp$Porcentaje <- round(freq_comp$Frecuencia / nrow(df) * 100, 1)
freq_comp

3.1.2 Gráfica de Distribución

ggplot(df, aes(x = factor(complejidad))) +
  geom_bar(aes(y = ..count.. / sum(..count..)),
           fill = "#3B82F6", color = "white", alpha = 0.85, width = 0.6) +
  geom_text(aes(y = ..count.. / sum(..count..),
                label = paste0(..count.., "\n(",
                               round(..count.. / nrow(df) * 100, 1), "%)")),
            stat = "count", vjust = -0.3, size = 3.5) +
  stat_function(fun = function(x) {
                  dnorm(as.numeric(levels(factor(df$complejidad)))[x],
                        mean = mean(df$complejidad),
                        sd   = sd(df$complejidad))
                },
                color = "#DC2626", linetype = "dashed", linewidth = 1) +
  labs(
    title    = "Distribución de Complejidad — Escala Fibonacci",
    subtitle = "Azul: frecuencia relativa | Rojo discontinuo: normal teórica",
    x        = "Puntos de Esfuerzo (Fibonacci)",
    y        = "Proporción"
  ) +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold"))
Frecuencia por nivel de complejidad — escala Fibonacci

Frecuencia por nivel de complejidad — escala Fibonacci

Análisis: La variable complejidad es discreta y toma únicamente los valores de la escala Fibonacci {1, 2, 3, 5, 8, 13}. Los tickets de complejidad 2, 3 y 5 concentran la mayor proporción, coherente con la carga habitual de un sprint donde predominan desarrollos de mediana dificultad. Los tickets de 13 puntos son escasos y representan desarrollos de alta complejidad: integraciones API, migraciones con lógica extensa o módulos altamente personalizados.

Nota metodológica: La curva normal teórica (línea roja discontinua) aparece segmentada en lugar de continua. Esto ocurre porque complejidad es una variable discreta con únicamente 6 valores posibles — no existen valores intermedios entre ellos sobre los cuales trazar una curva suavizada. Esta característica por sí misma ya es un indicador visual de no normalidad: una variable verdaderamente normal requiere ser continua y sus valores deben distribuirse simétricamente alrededor de la media, condición que la escala Fibonacci no cumple por construcción.


3.1.3 Histograma y Q-Q Plot

par(mfrow = c(1, 2))

hist(df$complejidad,
     main  = "Histograma — Complejidad",
     xlab  = "Puntos de Esfuerzo",
     col   = "#BFDBFE", border = "white", freq = FALSE)
curve(dnorm(x, mean = mean(df$complejidad), sd = sd(df$complejidad)),
      col = "#1D4ED8", lwd = 2, add = TRUE)

qqnorm(df$complejidad,
       main = "Q-Q Plot — Complejidad",
       col  = "#3B82F6", pch = 19, cex = 0.5)
qqline(df$complejidad, col = "#DC2626", lwd = 2)
Histograma y Q-Q Plot — complejidad

Histograma y Q-Q Plot — complejidad

par(mfrow = c(1, 1))

Análisis: El histograma evidencia una distribución asimétrica, con sesgo hacia la derecha indicando que la mayoría de los datos se concentran en el lado izquierdo (valores “pequeños”), muy alejada de la curva normal teórica. El Q-Q Plot confirma marcadas desviaciones de la línea de referencia, especialmente por los saltos discretos propios de la escala Fibonacci. Los puntos de esfuerzo no son una variable continua ni se distribuyen normalmente.


3.1.4 Boxplot

ggplot(df, aes(y = complejidad)) +
  geom_boxplot(fill = "#BFDBFE", color = "#1D4ED8",
               outlier.color = "#DC2626", outlier.shape = 19, outlier.size = 2) +
  labs(
    title    = "Boxplot — Complejidad (Puntos de Esfuerzo)",
    subtitle = "Valores atípicos marcados en rojo",
    y        = "Puntos de Esfuerzo (Fibonacci)"
  ) +
  theme_minimal(base_size = 13) +
  theme(plot.title  = element_text(face = "bold"),
        axis.text.x = element_blank())
Boxplot — complejidad (valores atípicos en rojo)

Boxplot — complejidad (valores atípicos en rojo)

Análisis: El boxplot confirma que la mediana de complejidad se ubica en tickets de baja-mediana dificultad. La caja (IQR) es relativamente estrecha en comparación con el rango total de la escala, mientras que los valores de 8 y 13 puntos aparecen como valores atípicos superiores (puntos rojos). Esto es coherente con la realidad operativa: la mayoría del trabajo diario corresponde a tickets pequeños y medianos, siendo los de alta complejidad excepcionales dentro del flujo normal de trabajo.


3.1.5 Prueba de Shapiro-Wilk

La prueba de Shapiro-Wilk es válida para n ≤ 5.000, por lo que aplica al dataset completo (n = 1.000).

sw_comp <- shapiro.test(df$complejidad)
sw_comp
## 
##  Shapiro-Wilk normality test
## 
## data:  df$complejidad
## W = 0.8192, p-value < 2.2e-16
cat(sprintf("  Estadístico W : %.6f\n", sw_comp$statistic))
##   Estadístico W : 0.819201
cat(sprintf("  p-valor       : %.2e\n",  sw_comp$p.value))
##   p-valor       : 4.50e-32
cat(sprintf("  Decisión (α=0.05): %s\n",
    ifelse(sw_comp$p.value < 0.05,
           "RECHAZA H0 — NO sigue distribucion normal",
           "No rechaza H0 — Compatible con normalidad")))
##   Decisión (α=0.05): RECHAZA H0 — NO sigue distribucion normal

Análisis: Con p-valor = 4.5e-32<< α = 0.05, se rechaza H₀ de normalidad de forma contundente. La variable complejidad no sigue distribución normal. Este hallazgo, consistente con todo lo observado en las gráficas previas, es determinante para la selección del método de correlación: se descarta el uso exclusivo de Pearson y se prioriza un enfoque no paramétrico.


3.2 Variable Y — costo_ticket (Costo del Ticket en USD)

3.2.1 Tabla de Frecuencias

costo_ticket es una variable continua medida en USD. En 1.000 registros prácticamente no se repite ningún valor exacto (o muy pocos). Una tabla de frecuencias tendría 1.000 filas, una por cada valor único, lo cual no aporta información interpretable. Para variables continuas, la herramienta equivalente que cumple el mismo propósito informativo es el histograma: agrupa los datos en intervalos (bins) y muestra cómo se distribuye la frecuencia a lo largo del rango.


3.2.2 Gráfica de Distribución

ggplot(df, aes(x = costo_ticket)) +
  geom_histogram(aes(y = ..density..), bins = 30,
                 fill = "#10B981", color = "white", alpha = 0.80) +
  geom_density(color = "#065F46", linewidth = 1.2) +
  stat_function(fun  = dnorm,
                args = list(mean = mean(df$costo_ticket), sd = sd(df$costo_ticket)),
                color = "#DC2626", linetype = "dashed", linewidth = 1) +
  labs(
    title    = "Distribución de Costo del Ticket (USD)",
    subtitle = "Verde: densidad empírica | Rojo discontinuo: normal teórica",
    x        = "Costo del Ticket (USD)",
    y        = "Densidad"
  ) +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold"))
Histograma con densidad empírica y normal teórica — costo_ticket

Histograma con densidad empírica y normal teórica — costo_ticket

Análisis: La distribución del costo muestra una forma asimétrica con sesgo positivo (hacia la derecha), producto de la mezcla de tickets de baja complejidad (costos bajos) y alta complejidad (costos elevados). La curva normal teórica (línea roja discontinua) no se ajusta a la distribución empírica en ninguna región, evidenciando visualmente la no normalidad de la variable.


3.2.3 Histograma y Q-Q Plot

par(mfrow = c(1, 2))

hist(df$costo_ticket,
     main  = "Histograma — Costo del Ticket",
     xlab  = "Costo (COP)",
     col   = "#A7F3D0", border = "white", freq = FALSE)
curve(dnorm(x, mean = mean(df$costo_ticket), sd = sd(df$costo_ticket)),
      col = "#065F46", lwd = 2, add = TRUE)

qqnorm(df$costo_ticket,
       main = "Q-Q Plot — Costo del Ticket",
       col  = "#10B981", pch = 19, cex = 0.5)
qqline(df$costo_ticket, col = "#DC2626", lwd = 2)
Histograma y Q-Q Plot — costo_ticket

Histograma y Q-Q Plot — costo_ticket

par(mfrow = c(1, 1))

Análisis: El histograma confirma la forma multimodal de la distribución: se observan múltiples “picos” que corresponden a los grupos de costos generados por cada nivel de complejidad Fibonacci. La curva normal teórica (línea verde oscura) no se ajusta en ningún punto. El Q-Q Plot presenta desviaciones sistemáticas en ambos extremos: la curvatura ascendente en la cola superior es característica de distribuciones con colas más pesadas que la normal, reflejo de los tickets de 13 puntos con costos muy elevados.


3.2.4 Boxplot

ggplot(df, aes(y = costo_ticket)) +
  geom_boxplot(fill = "#A7F3D0", color = "#065F46",
               outlier.color = "#DC2626", outlier.shape = 19, outlier.size = 2) +
  labs(
    title    = "Boxplot — Costo del Ticket (COP)",
    subtitle = "Valores atípicos marcados en rojo",
    y        = "Costo (COP)"
  ) +
  theme_minimal(base_size = 13) +
  theme(plot.title  = element_text(face = "bold"),
        axis.text.x = element_blank())
Boxplot — costo_ticket (valores atípicos en rojo)

Boxplot — costo_ticket (valores atípicos en rojo)

Análisis: El boxplot revela una distribución con cola derecha pronunciada: la caja (IQR) concentra los costos de tickets de complejidad media, mientras que los valores atípicos superiores (puntos rojos) corresponden principalmente a los tickets de 8 y 13 puntos, cuyos costos superan ampliamente la mediana general. Al igual que en complejidad, la asimetría es evidente y confirma la no normalidad de la variable.


3.2.5 Prueba de Shapiro-Wilk

sw_costo <- shapiro.test(df$costo_ticket)
sw_costo
## 
##  Shapiro-Wilk normality test
## 
## data:  df$costo_ticket
## W = 0.93915, p-value < 2.2e-16
cat(sprintf("  Estadístico W : %.6f\n", sw_costo$statistic))
##   Estadístico W : 0.939153
cat(sprintf("  p-valor       : %.2e\n",  sw_costo$p.value))
##   p-valor       : 8.39e-20
cat(sprintf("  Decisión (α=0.05): %s\n",
    ifelse(sw_costo$p.value < 0.05,
           "RECHAZA H0 — NO sigue distribucion normal",
           "No rechaza H0 — Compatible con normalidad")))
##   Decisión (α=0.05): RECHAZA H0 — NO sigue distribucion normal

Análisis: p-valor = 8.39e-20< α = 0.05. Se rechaza H₀: costo_ticket no sigue distribución normal. La distribución refleja la estructura subyacente del negocio: los costos están fuertemente condicionados por el nivel de complejidad del ticket, generando esa forma multimodal observada en el histograma. El incumplimiento de normalidad en ambas variables determina que los métodos no paramétricos son los apropiados para el análisis de correlación.


4 Relación entre complejidad y costo_ticket

ggplot(df, aes(x = complejidad, y = costo_ticket)) +
  geom_jitter(color = "#6366F1", alpha = 0.25, size = 1,
              width = 0.15, height = 0) +
  labs(
    title    = "Dispersión: Complejidad vs. Costo del Ticket",
    subtitle = "Análisis visual de correlación",
    x        = "Complejidad (Puntos de Esfuerzo)",
    y        = "Costo del Ticket (USD)"
  ) +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold"))
Diagrama de dispersión con tendencias lineal y suavizada

Diagrama de dispersión con tendencias lineal y suavizada

Análisis: La gráfica revela una tendencia positiva clara y consistente: a mayor complejidad, mayor costo del ticket. Se observa variabilidad creciente a medida que aumenta la complejidad, comportamiento típico en proyectos de software donde los tickets de mayor complejidad tienen mayor incertidumbre de esfuerzo.


5 Pruebas de Correlación

5.1 Justificación de la Selección de Métodos

Dado que:

  • complejidad: discreta ordinal en escala Fibonacci → NO normal
  • costo_ticket: continua con distribución multimodal → NO normal

Se aplicarán los tres métodos de correlación para comparar resultados:

Método Supuesto principal Recomendado para este caso
Pearson (r) Normalidad bivariada + relación lineal No óptimo — normalidad no cumplida
Spearman (ρ) Solo requiere orden de rangos Sí — robusto ante no normalidad
Kendall (τ) Solo requiere orden de rangos Sí — especialmente con empates

Método prioritario: Spearman, por incumplimiento de normalidad en ambas variables y naturaleza ordinal de complejidad.

Se plantean las siguientes hipótesis sobre el coeficiente de correlación poblacional:

  • H0: ρ = 0 (no existe correlación entre las variables)
  • H1: ρ ≠ 0 (existe correlación entre las variables)

El p-valor se utiliza para evaluar la evidencia en contra de la hipótesis nula (H0), pero no indica la magnitud o fuerza de la relación.

La regla de decisión es:

  • Si p-valor < α → se rechaza H0
  • Si p-valor ≥ α → no se rechaza H0

5.2 Correlación de Pearson

cor_p <- cor.test(df$complejidad, df$costo_ticket, method = "pearson")
cor_p
## 
##  Pearson's product-moment correlation
## 
## data:  df$complejidad and df$costo_ticket
## t = 58.872, df = 998, p-value < 2.2e-16
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
##  0.8664901 0.8942930
## sample estimates:
##       cor 
## 0.8811509
cat(sprintf("  r        : %.4f\n", cor_p$estimate))
##   r        : 0.8812
cat(sprintf("  t        : %.4f\n", cor_p$statistic))
##   t        : 58.8716
cat(sprintf("  gl       : %d\n",   cor_p$parameter))
##   gl       : 998
cat(sprintf("  p-valor  : %.2e\n", cor_p$p.value))
##   p-valor  : 0.00e+00
cat(sprintf("  IC 95%%   : [%.4f, %.4f]\n",
            cor_p$conf.int[1], cor_p$conf.int[2]))
##   IC 95%   : [0.8665, 0.8943]

Análisis Pearson: r = 0.881. El coeficiente indica una correlación positiva fuerte, y el p-valor < 0.001 confirma significancia estadística. No obstante, como la normalidad bivariada no se cumple, este resultado debe tomarse como referencial. Los coeficientes no paramétricos son los que guiarán la conclusión final.


5.3 Correlación de Spearman

cor_s <- cor.test(df$complejidad, df$costo_ticket, method = "spearman")
cor_s
## 
##  Spearman's rank correlation rho
## 
## data:  df$complejidad and df$costo_ticket
## S = 32256825, p-value < 2.2e-16
## alternative hypothesis: true rho is not equal to 0
## sample estimates:
##       rho 
## 0.8064589
cat(sprintf("  rho      : %.4f\n", cor_s$estimate))
##   rho      : 0.8065
cat(sprintf("  S        : %.2f\n", cor_s$statistic))
##   S        : 32256825.21
cat(sprintf("  p-valor  : %.2e\n", cor_s$p.value))
##   p-valor  : 5.66e-230

Análisis Spearman: ρ = 0.806. Este coeficiente mide la asociación monotónica entre rangos sin exigir normalidad. El resultado confirma una correlación positiva fuerte, con p-valor < 0.001. Este es el método estadísticamente más apropiado para este par de variables y el que sustenta la decisión sobre H₀.


5.4 Correlación de Kendall

cor_k <- cor.test(df$complejidad, df$costo_ticket, method = "kendall")
cor_k
## 
##  Kendall's rank correlation tau
## 
## data:  df$complejidad and df$costo_ticket
## z = 28.839, p-value < 2.2e-16
## alternative hypothesis: true tau is not equal to 0
## sample estimates:
##       tau 
## 0.6686355
cat(sprintf("  tau      : %.4f\n", cor_k$estimate))
##   tau      : 0.6686
cat(sprintf("  z        : %.4f\n", cor_k$statistic))
##   z        : 28.8392
cat(sprintf("  p-valor  : %.2e\n", cor_k$p.value))
##   p-valor  : 6.92e-183

Análisis Kendall: τ = 0.669. El tau de Kendall es generalmente más conservador que Spearman y es especialmente útil cuando existen empates en los datos, que es exactamente lo que ocurre con los valores discretos de la escala Fibonacci en complejidad. Aun con este enfoque conservador, el resultado es significativo (p < 0.001), confirmando la correlación positiva.


5.5 Tabla Comparativa de Resultados

interpretar_r <- function(r) {
  ar <- abs(r)
  fuerza <- ifelse(ar >= 0.90, "Muy fuerte",
            ifelse(ar >= 0.70, "Fuerte",
            ifelse(ar >= 0.50, "Moderada-alta",
            ifelse(ar >= 0.30, "Moderada", "Débil"))))
  paste(fuerza, ifelse(r > 0, "positiva", "negativa"))
}

tabla <- data.frame(
  Método = c("Pearson (r)", "Spearman (rho)", "Kendall (tau)"),
  Coeficiente = c(round(cor_p$estimate, 4),
                  round(cor_s$estimate, 4),
                  round(cor_k$estimate, 4)),
  pvalor = c(format(cor_p$p.value, scientific=TRUE, digits=3),
             format(cor_s$p.value, scientific=TRUE, digits=3),
             format(cor_k$p.value, scientific=TRUE, digits=3)),
  Significativo = c(
    ifelse(cor_p$p.value < 0.05, "Si", "No"),
    ifelse(cor_s$p.value < 0.05, "Si", "No"),
    ifelse(cor_k$p.value < 0.05, "Si", "No")
  ),
  Fuerza = c(interpretar_r(cor_p$estimate),
             interpretar_r(cor_s$estimate),
             interpretar_r(cor_k$estimate)),
  Observacion = c(
    "Requiere normalidad bivariada — no se cumple en este caso",
    "RECOMENDADO — robusto ante no normalidad",
    "Conservador — util con empates en variables discretas"
  ),
  check.names = FALSE
)

tabla

5.6 Matriz de Correlación — Todas las Variables

mat_cor <- cor(df[, vars_num], method = "spearman", use = "complete.obs")

corrplot(mat_cor,
         method      = "color",
         type        = "upper",
         order       = "hclust",
         addCoef.col = "black",
         number.cex  = 0.65,
         tl.col      = "black",
         tl.srt      = 45,
         tl.cex      = 0.85,
         col         = colorRampPalette(c("#DC2626","white","#1D4ED8"))(200),
         title       = "Matriz de Correlacion de Spearman",
         mar         = c(0, 0, 2, 0))
Matriz de correlación de Spearman — todas las variables

Matriz de correlación de Spearman — todas las variables

Análisis de la matriz: La variable complejidad presenta correlaciones positivas altas con costo_ticket, horas_reales y horas_qa, lo que es coherente operativamente: tickets más complejos demandan más horas de desarrollo, más horas de QA y, en consecuencia, mayor costo. Por otro lado, satisfaccion muestra correlaciones negativas con complejidad y costo, sugiriendo que los tickets más grandes y costosos tienden a generar menor satisfacción en el cliente, posiblemente por tiempos de entrega más largos o mayor cantidad de retrabajos.


5.7 Conclusiones

Con base en el análisis estadístico sobre los 1.000 tickets del área de Mesa de Ayuda:

  1. Ninguna de las dos variables sigue distribución normal, por lo que el método estadísticamente apropiado es Spearman, confirmado además por Kendall como alternativa conservadora.

  2. Los tres métodos aplicados coinciden en detectar una correlación positiva fuerte y altamente significativa entre complejidad y costo_ticket (p < 0.001 en los tres casos).

  3. Implicación práctica para el equipo: La escala de puntos de historia utilizada en los sprints del equipo de Mesa de Ayuda (MDA) tiene poder predictivo sobre el costo del ticket. Esto valida el proceso de estimation y abre la puerta para construir una función de estimación automática de costos dentro del módulo Proyectos de Odoo, usando los puntos asignados en planning poker como input.


6 Modelo de Regresión Lineal Simple

6.1 Ajuste del Modelo

modelo_simple = lm(costo_ticket ~ complejidad, data = df)
summary(modelo_simple)
## 
## Call:
## lm(formula = costo_ticket ~ complejidad, data = df)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -838.72 -208.46    0.78  188.54 1124.28 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  446.566     16.235   27.51   <2e-16 ***
## complejidad  185.630      3.153   58.87   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 282.7 on 998 degrees of freedom
## Multiple R-squared:  0.7764, Adjusted R-squared:  0.7762 
## F-statistic:  3466 on 1 and 998 DF,  p-value: < 2.2e-16

6.2 Ecuación del Modelo

b0 = round(coef(modelo_simple)[1], 2)
b1 = round(coef(modelo_simple)[2], 2)

cat(sprintf("  costo_ticket = %.2f + %.2f * complejidad\n", b0, b1))
##   costo_ticket = 446.57 + 185.63 * complejidad

La ecuación del modelo ajustado es:

\[\hat{Y} = 446.57 + 185.63 \cdot complejidad\]

Donde:

  • \(\hat{Y}\) = costo estimado del ticket (USD)
  • \(X\) = complejidad (puntos de esfuerzo Fibonacci)
  • \(446.57\) = intercepto: costo base estimado cuando la complejidad es 0
  • \(185.63\) = pendiente: incremento promedio en el costo por cada punto adicional de complejidad

6.3 Análisis del Modelo

r2  = round(summary(modelo_simple)$r.squared, 4)
r2_adj = round(summary(modelo_simple)$adj.r.squared, 4)
f_stat = round(summary(modelo_simple)$fstatistic[1], 2)
f_pval = pf(summary(modelo_simple)$fstatistic[1],
               summary(modelo_simple)$fstatistic[2],
               summary(modelo_simple)$fstatistic[3],
               lower.tail = FALSE)

cat(sprintf("  R²           : %.4f\n", r2))
##   R²           : 0.7764
cat(sprintf("  R² ajustado  : %.4f\n", r2_adj))
##   R² ajustado  : 0.7762
cat(sprintf("  F-estadístico: %.2f\n", f_stat))
##   F-estadístico: 3465.87
cat(sprintf("  p-valor (F)  : %.2e\n", f_pval))
##   p-valor (F)  : 0.00e+00

Análisis: El coeficiente de determinación R² = 0.7764 indica que la variable complejidad explica el 77.6% de la variabilidad en el costo del ticket. El estadístico F = 3465.87 con p-valor < 0.001 confirma que el modelo es globalmente significativo. La pendiente positiva de 185.63 USD por punto de complejidad es coherente con la hipótesis planteada: a mayor complejidad, mayor costo. Sin embargo, el R² sugiere que existe variabilidad no explicada por la complejidad sola, lo que motiva el ajuste de un modelo múltiple.


6.4 ANOVA

anova(modelo_simple)

Análisis: Con un estadístico F = 3.465,865 y p-valor ≈ 0, se rechaza H₀ (β₁ = 0), confirmando que la variable complejidad tiene un efecto real y significativo sobre el costo del ticket. Lo anterior valida que el modelo lineal simple es útil para predecir el costo de un ticket a partir de sus puntos de esfuerzo.


6.5 Evaluación del Modelo — Supuestos de Markov

Para que el estimador sea el Mejor Estimador Lineal Insesgado y para que la inferencia paramétrica sea válida, los residuos del modelo deben cumplir cinco condiciones fundamentales:

# Supuesto Fórmula Herramienta
S1 Linealidad E(Y|X) = β₀ + β₁X Gráfica residuos vs. ajustados
S2 Media cero del error E(εᵢ) = 0 Media aritmética de los residuos
S3 Homocedasticidad Var(εᵢ) = σ² Scale-Location + Breusch-Pagan
S4 Independencia Cov(εᵢ, εⱼ) = 0 Durbin-Watson
S5 Normalidad εᵢ ~ N(0, σ²) Q-Q Plot + Shapiro-Wilk

6.5.1 Linealidad

El modelo postula que la relación entre complejidad y costo_ticket es estrictamente lineal.

Tambien, se evalúa visualmente: si los residuos se distribuyen de forma aleatoria alrededor de la línea horizontal en cero, sin ningún patrón sistemático, el supuesto se cumple. La presencia de curvaturas o patrones en forma de embudo indicaría una mala especificación del modelo.

plot(modelo_simple, which = 1, col = "#6366F1", pch = 19, cex = 0.5,
     main = "Residuos vs. Ajustados — Linealidad")
Residuos vs. Ajustados — Supuesto de Linealidad

Residuos vs. Ajustados — Supuesto de Linealidad

Análisis: Debido a que la línea roja de tendencia es aproximadamente horizontal y los puntos no muestran ningún patrón sistemático (curva, embudo), se confirma que la relación lineal es una especificación adecuada para estos datos.


6.5.2 Media Cero del Error

El valor esperado de los residuos debe ser exactamente cero: E(εᵢ) = 0. Esto garantiza que el modelo no tiene sesgo.

residuos_mco  = residuals(modelo_simple)
media_residuos = mean(residuos_mco)

cat(sprintf("  Media aritmética de los residuos : %.2e\n", media_residuos))
##   Media aritmética de los residuos : -4.07e-15

Análisis: El valor obtenido en el calculo de la media de los residuos es un número infinitesimal, prácticamente cero. Se confirma el cumplimiento del supuesto: el intercepto β₀ ha centrado correctamente el modelo, anulando cualquier sesgo sistemático global en las predicciones.


6.5.3 Homocedasticidad

La varianza de los residuos debe ser constante a lo largo de todo el rango de valores ajustados: Var(εᵢ) = σ².

#Breusch Pagan

bp_simple = bptest(modelo_simple)
cat(sprintf("  BP estadístico : %.4f\n", bp_simple$statistic))
##   BP estadístico : 2.6848
cat(sprintf("  p-valor        : %.4e\n", bp_simple$p.value))
##   p-valor        : 1.0131e-01
cat(sprintf("  Decisión       : %s\n",
    ifelse(bp_simple$p.value < 0.05,
           "RECHAZA H0 — Heterocedasticidad detectada",
           "No rechaza H0 — Homocedasticidad confirmada")))
##   Decisión       : No rechaza H0 — Homocedasticidad confirmada

Análisis: Obtenemos los resultados de la prueba Breusch-Pagan, donde nos interesará de forma particular que el p-value es igual a 0.1013, notando que este valor es mayor que 0.05, entonces no rechazamos la hipótesis nula y podemos asumir que existe homocedasticidad.


6.5.4 Independencia

Los residuos no deben estar correlacionados entre sí: Cov(εᵢ, εⱼ) = 0 para todo i ≠ j. Se evalúa con la prueba de Durbin-Watson, cuyo estadístico toma valores entre 0 y 4: valores cercanos a 2 indican ausencia de autocorrelación, valores < 1.5 sugieren autocorrelación positiva y valores > 2.5 autocorrelación negativa.

dw_simple = dwtest(modelo_simple)
cat(sprintf("  DW estadístico : %.4f\n", dw_simple$statistic))
##   DW estadístico : 1.9357
cat(sprintf("  p-valor        : %.4f\n", dw_simple$p.value))
##   p-valor        : 0.1546
cat(sprintf("  Decisión       : %s\n",
    ifelse(dw_simple$p.value < 0.05,
           "RECHAZA H0 — Autocorrelacion detectada",
           "No rechaza H0 — Errores independientes")))
##   Decisión       : No rechaza H0 — Errores independientes

Análisis: Un estadístico DW cercano a 2 (1.9357) y p-valor > 0.05 (0.1546) confirman que los residuos son independientes entre sí: el error de un ticket no contiene información sobre el error del siguiente.


6.5.5 Normalidad

Los residuos deben seguir una distribución normal: εᵢ ~ N(0, σ²). Se evalúa gráficamente con el Q-Q Plot y formalmente con Shapiro-Wilk.

par(mfrow = c(1, 2))

qqnorm(residuals(modelo_simple),
       main = "Q-Q Plot — Residuos",
       col  = "#6366F1", pch = 19, cex = 0.5)
qqline(residuals(modelo_simple), col = "#DC2626", lwd = 2)

hist(residuals(modelo_simple),
     main  = "Histograma — Residuos",
     xlab  = "Residuos",
     col   = "#C7D2FE", border = "white", freq = FALSE)
curve(dnorm(x, mean = 0, sd = sd(residuals(modelo_simple))),
      col = "#4338CA", lwd = 2, add = TRUE)
Q-Q Plot e Histograma de Residuos — Normalidad

Q-Q Plot e Histograma de Residuos — Normalidad

par(mfrow = c(1, 1))
sw_res_simple <- shapiro.test(residuals(modelo_simple))
cat(sprintf("  W        : %.6f\n", sw_res_simple$statistic))
##   W        : 0.997153
cat(sprintf("  p-valor  : %.4f\n", sw_res_simple$p.value))
##   p-valor  : 0.0736
cat(sprintf("  Decisión : %s\n",
    ifelse(sw_res_simple$p.value < 0.05,
           "RECHAZA H0 — Residuos NO siguen distribucion normal",
           "No rechaza H0 — Residuos normales confirmado")))
##   Decisión : No rechaza H0 — Residuos normales confirmado

Análisis: Se confirma el supuesto de normalidad ya que se obtiene un p-valor > 0.05 (0.0736), lo que indicaría que no hay evidencia suficiente para rechazar la normalidad de los errores. Los puntos del Q-Q Plot se alinean sobre la diagonal y Shapiro-Wilk no rechaza H₀, por lo tanto, se concluye que εᵢ ~ N(0, σ²), validando que los p-valores y los intervalos de confianza del modelo son matemáticamente correctos y confiables.


6.5.6 Conclusión de los Supuestos

El modelo de regresión lineal simple cumple satisfactoriamente los cinco supuestos de Gauss-Markov. Esto implica que los coeficientes estimados son insesgados, consistentes y de varianza mínima. El modelo es, por tanto, estadísticamente válido para predecir el costo de un ticket a partir de su complejidad.


7 Modelo de Regresión Lineal Múltiple

7.1 Ajuste del Modelo

Se incorporan las siguientes las variables numéricas disponibles como predictores: complejidad, horas_reales, experiencia_dev, horas_qa.

La variable dependiente continua siendo costo_ticket.

modelo_multiple = lm(costo_ticket ~ complejidad + horas_reales + experiencia_dev + horas_qa,
                      data = df)
summary(modelo_multiple)
## 
## Call:
## lm(formula = costo_ticket ~ complejidad + horas_reales + experiencia_dev + 
##     horas_qa, data = df)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -100.393  -24.152    0.306   21.879  137.099 
## 
## Coefficients:
##                 Estimate Std. Error t value Pr(>|t|)    
## (Intercept)     -62.5964     3.5081 -17.843   <2e-16 ***
## complejidad       0.8822     0.8808   1.002    0.317    
## horas_reales     47.3650     0.3348 141.456   <2e-16 ***
## experiencia_dev  10.5344     0.3389  31.081   <2e-16 ***
## horas_qa         47.1373     0.5057  93.205   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 35.31 on 995 degrees of freedom
## Multiple R-squared:  0.9965, Adjusted R-squared:  0.9965 
## F-statistic: 7.129e+04 on 4 and 995 DF,  p-value: < 2.2e-16

7.2 Ecuación del Modelo

coefs = round(coef(modelo_multiple), 4)
coefs
##     (Intercept)     complejidad    horas_reales experiencia_dev        horas_qa 
##        -62.5964          0.8822         47.3650         10.5344         47.1373

\[\hat{Y} = \beta_0 + \beta_1 X_1 + \beta_2 X_2 + \beta_3 X_3 + \beta_4 X_4\]

Donde cada \(X_i\) corresponde a: complejidad, horas_reales, experiencia_dev y horas_qa, respectivamente.

\[\hat{Y} = -62.5964 + 0.8822 \cdot complejidad + 47.365 \cdot horas\_reales + 10.5344 \cdot experiencia\_dev + 47.1373 \cdot horas\_qa\]

7.3 Análisis del Modelo — Revisión de Variables Predictoras

r2_m = round(summary(modelo_multiple)$r.squared, 4)
r2_adj_m = round(summary(modelo_multiple)$adj.r.squared, 4)
f_m = round(summary(modelo_multiple)$fstatistic[1], 2)
f_pval_m = pf(summary(modelo_multiple)$fstatistic[1],
               summary(modelo_multiple)$fstatistic[2],
               summary(modelo_multiple)$fstatistic[3],
               lower.tail = FALSE)

cat(sprintf("  R²           : %.4f\n", r2_m))
##   R²           : 0.9965
cat(sprintf("  R² ajustado  : %.4f\n", r2_adj_m))
##   R² ajustado  : 0.9965
cat(sprintf("  F-estadístico: %.2f\n", f_m))
##   F-estadístico: 71294.01
cat(sprintf("  p-valor (F)  : %.2e\n", f_pval_m))
##   p-valor (F)  : 0.00e+00

Análisis: El R² = 0.9965 indica que el modelo múltiple explica el 99.7% de la variabilidad del costo, frente al 77.6% del modelo simple. La ganancia en poder explicativo justifica la inclusión de más predictores.


7.3.1 Significancia Individual de los Predictores

tabla_coefs = as.data.frame(summary(modelo_multiple)$coefficients)
tabla_coefs$Variable  = rownames(tabla_coefs)
tabla_coefs$Significativo = ifelse(tabla_coefs[,4] < 0.05, "Si", "No")
tabla_coefs <- tabla_coefs[, c("Variable","Estimate","Std. Error","t value","Pr(>|t|)","Significativo")]
tabla_coefs[, 2:5] = round(tabla_coefs[, 2:5], 4)
tabla_coefs

Análisis: Se identifican las variables con p-valor < 0.05 como predictores estadísticamente significativos: horas_reales, experiencia_dev y horas_qa. También se identifica que el inctercepto es significativo. Las variables no significativas (con p ≥ 0.05), como complejidad, son candidatas a ser excluidas del modelo para obtener un modelo “más simple”. Lo anterior motivará la comparación mediante AIC en la siguiente sección.


7.3.2 Análisis del Fenómeno: ¿Por qué complejidad pierde significancia en el modelo múltiple?

El comportamiento de complejidad —predictor significativo en el modelo de regresión simple (\(p < 0.05\)) pero no en el modelo múltiple (\(p \geq 0.05\))— no es una contradicción. Es una señal estadística de alto valor diagnóstico conocida como pérdida de significancia por mediación o redundancia informativa.

Evidencia desde la Matriz de Correlación de Spearman (Sección 5.6)

La respuesta está directamente en la matriz ya calculada en la sección 5.6 (Matriz de Correlación — Todas las Variables). complejidad presenta correlaciones positivas fuertes con los predictores que sí resultaron significativos en el modelo múltiple:

Par de variables \(\rho_s\) (Spearman) Interpretación
complejidadhoras_reales \(0.82\) Correlación fuerte positiva
complejidadhoras_qa \(0.69\) Correlación moderada-alta positiva
complejidadexperiencia_dev \(-0.05\) Correlación prácticamente nula

complejidad es redundante con horas_reales y horas_qa —variables que ya están en el modelo y que capturan su información—, pero es no esta relacionada con experiencia_dev**. Precisamente por eso experiencia_dev sí retiene su significancia en el modelo múltiple: aporta información que complejidad no contiene y que ningún otro predictor captura. La complejidad de un ticket no determina qué tan experimentado es el desarrollador asignado; son dimensiones independientes. Esto confirma que la pérdida de significancia decomplejidad es una consecuencia directa de su redundancia con los predictores más informativos del modelo.


7.3.3 Multicolinealidad — Factor de Inflación de la Varianza (VIF)

vif_vals = vif(modelo_multiple)
vif_tabla = data.frame(
  Variable = names(vif_vals),
  VIF      = round(vif_vals, 4),
  Diagnostico = ifelse(vif_vals > 10, "PROBLEMA GRAVE",
                ifelse(vif_vals > 5,  "Moderado — revisar",
                                      "Aceptable"))
)
vif_tabla

Los valores VIF obtenidos revelan un patrón coherente con lo observado en la matriz de Spearman:

  • horas_reales presenta el VIF más alto del modelo (\(7.0758\)), confirmando que es el predictor con mayor solapamiento informativo respecto a los demás, consecuencia directa de su correlación fuerte con complejidad (\(\rho_s = 0.82\)).

  • complejidad alcanza un VIF de \(5.0020\), situándose exactamente en el umbral de alerta moderada. Este valor no es casual: refleja que su varianza está siendo inflada por su correlación simultánea con horas_reales (\(\rho_s = 0.82\)) y horas_qa (\(\rho_s = 0.69\)). Es decir, el modelo tiene dificultad para aislar el efecto propio de complejidad porque parte de ese efecto ya está siendo explicado por las otras dos variables.

  • horas_qa obtiene un VIF de \(3.4920\), dentro del rango aceptable, a pesar de su correlación moderada-alta con complejidad. Esto indica que, aunque comparte información, su contribución al modelo sigue siendo suficientemente independiente como para no generar inestabilidad.

  • experiencia_dev registra un VIF de \(1.0043\), prácticamente igual a \(1\), confirmando matemáticamente su no relación con respecto al resto de predictores. Es la variable más estable del modelo y la que aporta información genuinamente nueva, lo cual explica por qué retiene su significancia estadística aun en presencia de todos los demás predictores.

En conjunto, los valores VIF corroboran el diagnóstico: complejidad no es excluida del modelo por ser irrelevante en términos sustantivos, sino porque su efecto ya está contenido —y mejor estimado— en horas_reales y horas_qa. La colinealidad moderada de estas variables es el mecanismo que suprime su \(p\)-valor en el modelo múltiple.


7.4 Ajuste Modelo Múltiple (Significativo)

Se incorporan las siguientes las variables numéricas disponibles como predictores: horas_reales, experiencia_dev, horas_qa.

De acuerdo a los puntos anteriores, se descarta la variable complejidad.

La variable dependiente continua siendo costo_ticket.

modelo_multiple_sig = lm(costo_ticket ~ horas_reales + experiencia_dev + horas_qa,
                      data = df)
summary(modelo_multiple_sig)
## 
## Call:
## lm(formula = costo_ticket ~ horas_reales + experiencia_dev + 
##     horas_qa, data = df)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -100.379  -24.140    0.374   21.821  138.252 
## 
## Coefficients:
##                 Estimate Std. Error t value Pr(>|t|)    
## (Intercept)     -63.0498     3.4788  -18.12   <2e-16 ***
## horas_reales     47.6042     0.2347  202.82   <2e-16 ***
## experiencia_dev  10.5133     0.3383   31.08   <2e-16 ***
## horas_qa         47.1697     0.5047   93.46   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 35.31 on 996 degrees of freedom
## Multiple R-squared:  0.9965, Adjusted R-squared:  0.9965 
## F-statistic: 9.506e+04 on 3 and 996 DF,  p-value: < 2.2e-16

7.4.1 Ecuación del Modelo

coefs_sig = round(coef(modelo_multiple_sig), 4)
coefs_sig
##     (Intercept)    horas_reales experiencia_dev        horas_qa 
##        -63.0498         47.6042         10.5133         47.1697

\[\hat{Y} = \beta_0 + \beta_1 X_1 + \beta_2 X_2 + \beta_3 X_3\]

Donde cada \(X_i\) corresponde a: horas_reales, experiencia_dev y horas_qa, respectivamente.

\[\hat{Y} = -63.0498 + 47.6042 \cdot horas\_reales + 10.5133 \cdot experiencia\_dev + 47.1697 \cdot horas\_qa\]

7.4.2 Análisis del Modelo — Revisión de Variables Predictoras

r2_m = round(summary(modelo_multiple_sig)$r.squared, 4)
r2_adj_m = round(summary(modelo_multiple_sig)$adj.r.squared, 4)
f_m = round(summary(modelo_multiple_sig)$fstatistic[1], 2)
f_pval_m = pf(summary(modelo_multiple_sig)$fstatistic[1],
               summary(modelo_multiple_sig)$fstatistic[2],
               summary(modelo_multiple_sig)$fstatistic[3],
               lower.tail = FALSE)

cat(sprintf("  R²           : %.4f\n", r2_m))
##   R²           : 0.9965
cat(sprintf("  R² ajustado  : %.4f\n", r2_adj_m))
##   R² ajustado  : 0.9965
cat(sprintf("  F-estadístico: %.2f\n", f_m))
##   F-estadístico: 95058.02
cat(sprintf("  p-valor (F)  : %.2e\n", f_pval_m))
##   p-valor (F)  : 0.00e+00

Análisis: El R² = 0.9965 indica que el modelo múltiple explica el 99.7% de la variabilidad del costo, frente al 77.6% del modelo simple y es el mismo R² obtenido con el modelo completo (incluyendo complejidad), lo que justifica la eliminación de la variable complejidad por no ser significativa.


7.5 ANOVA

anova(modelo_multiple_sig)

Análisis: La tabla ANOVA confirma que los tres predictores del modelo — horas_reales, experiencia_dev y horas_qa — son individualmente significativos (p-valor ≈ 0 en los tres casos). horas_reales es el predictor dominante con el mayor F (275.578,62), seguido de horas_qa (F = 8.734,71) y experiencia_dev (F = 860,74). El modelo es válido y cada variable aporta información estadísticamente relevante e independiente para predecir el costo del ticket.


7.6 Evaluación del Modelo — Supuestos de Gauss-Markov

Para que el estimador sea el Mejor Estimador Lineal Insesgado y para que la inferencia paramétrica sea válida, los residuos del modelo deben cumplir los mismos cinco supuestos evaluados en el modelo simple.


7.6.1 Linealidad

El modelo postula que la relación entre horas_reales, experiencia_dev y horas_qa con costo_ticket es estrictamente lineal.

Se evalúa visualmente: si los residuos se distribuyen de forma aleatoria alrededor de la línea horizontal en cero, sin ningún patrón sistemático, el supuesto se cumple. La presencia de curvaturas o patrones en forma de embudo indicaría una mala especificación del modelo.

plot(modelo_multiple_sig, which = 1, col = "#6366F1", pch = 19, cex = 0.5,
     main = "Residuos vs. Ajustados — Linealidad")
Residuos vs. Ajustados — Linealidad modelo múltiple

Residuos vs. Ajustados — Linealidad modelo múltiple

Análisis: Si la línea roja de tendencia es aproximadamente horizontal y los puntos no muestran ningún patrón sistemático, se confirma que la especificación lineal es adecuada para el modelo con horas_reales, experiencia_dev y horas_qa como predictores.


7.6.2 Media Cero del Error

El valor esperado de los residuos debe ser exactamente cero: E(εᵢ) = 0. Esto garantiza que el modelo no tiene sesgo.

residuos_mult  = residuals(modelo_multiple_sig)
media_residuos_mult = mean(residuos_mult)

cat(sprintf("  Media aritmética de los residuos : %.2e\n", media_residuos_mult))
##   Media aritmética de los residuos : -1.18e-15

Análisis: El valor obtenido en el calculo de la media de los residuos es un número infinitesimal, prácticamente cero. Se confirma el cumplimiento del supuesto: el intercepto β₀ ha centrado correctamente el modelo, anulando cualquier sesgo sistemático global en las predicciones.


7.6.3 Homocedasticidad

La varianza de los residuos debe ser constante a lo largo de todo el rango de valores ajustados: Var(εᵢ) = σ².

bp_multiple = bptest(modelo_multiple_sig)
cat(sprintf("  BP estadístico : %.4f\n", bp_multiple$statistic))
##   BP estadístico : 19.4810
cat(sprintf("  p-valor        : %.4e\n", bp_multiple$p.value))
##   p-valor        : 2.1741e-04
cat(sprintf("  Decisión       : %s\n",
    ifelse(bp_multiple$p.value < 0.05,
           "RECHAZA H0 — Heterocedasticidad detectada",
           "No rechaza H0 — Homocedasticidad confirmada")))
##   Decisión       : RECHAZA H0 — Heterocedasticidad detectada

Análisis: La prueba de Breusch-Pagan, con BP = 19.4810 y p-valor = 2.1741e-04 < α = 0.05, confirma que se rechaza H₀ de varianza constante. Se detecta heterocedasticidad en el modelo, lo que indica que la variabilidad del error no es uniforme y que los errores estándar de los coeficientes están sesgados. Para corregir este problema y devolver validez a los p-valores sin alterar los coeficientes estimados, se recomienda aplicar errores estándar robustos (estimador de White).

# Errores estándar robustos (estimador de White HC3)
coeftest(modelo_multiple_sig, vcov = vcovHC(modelo_multiple_sig, type = "HC3"))
## 
## t test of coefficients:
## 
##                  Estimate Std. Error t value  Pr(>|t|)    
## (Intercept)     -63.04981    3.85251 -16.366 < 2.2e-16 ***
## horas_reales     47.60416    0.23714 200.746 < 2.2e-16 ***
## experiencia_dev  10.51331    0.37545  28.002 < 2.2e-16 ***
## horas_qa         47.16969    0.46795 100.802 < 2.2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Análisis post-corrección: Tras aplicar los errores estándar robustos de White, los tres predictores del modelo mantienen su significancia estadística con p-valor < 2.2e-16 en los tres casos. Los coeficientes β̂ permanecen idénticos a los del modelo original (horas_reales = 47.60, experiencia_dev = 10.51, horas_qa = 47.17), confirmando que la heterocedasticidad no introdujo sesgo en la estimación. Lo que cambia son los errores estándar, que ahora son robustos y confiables. Se concluye que el modelo costo_ticket ~ horas_reales + experiencia_dev + horas_qa es estadísticamente válido, los p-valores son correctos y la inferencia sobre los coeficientes es confiable.


7.6.4 Independencia

Los residuos no deben estar correlacionados entre sí: Cov(εᵢ, εⱼ) = 0 para todo i ≠ j. Se evalúa con la prueba de Durbin-Watson, cuyo estadístico toma valores entre 0 y 4: valores cercanos a 2 indican ausencia de autocorrelación, valores < 1.5 sugieren autocorrelación positiva y valores > 2.5 autocorrelación negativa.

dw_multiple = dwtest(modelo_multiple_sig)
cat(sprintf("  DW estadístico : %.4f\n", dw_multiple$statistic))
##   DW estadístico : 1.9604
cat(sprintf("  p-valor        : %.4f\n", dw_multiple$p.value))
##   p-valor        : 0.2663
cat(sprintf("  Decisión       : %s\n",
    ifelse(dw_multiple$p.value < 0.05,
           "RECHAZA H0 — Autocorrelacion detectada",
           "No rechaza H0 — Errores independientes")))
##   Decisión       : No rechaza H0 — Errores independientes

Análisis: El estadístico DW toma valores entre 0 y 4; valores cercanos a 2 (1.9604) indican ausencia de autocorrelación. Un p-valor > 0.05 (0.2663) confirma que los residuos son independientes entre sí: el error de un ticket no contiene información sobre el error del siguiente, garantizando la validez de la inferencia estadística del modelo.


7.6.5 Normalidad

Los residuos deben seguir una distribución normal: εᵢ ~ N(0, σ²). Se evalúa gráficamente con el Q-Q Plot y formalmente con Shapiro-Wilk.

par(mfrow = c(1, 2))

qqnorm(residuals(modelo_multiple_sig),
       main = "Q-Q Plot — Residuos",
       col  = "#6366F1", pch = 19, cex = 0.5)
qqline(residuals(modelo_multiple_sig), col = "#DC2626", lwd = 2)

hist(residuals(modelo_multiple_sig),
     main  = "Histograma — Residuos",
     xlab  = "Residuos",
     col   = "#C7D2FE", border = "white", freq = FALSE)
curve(dnorm(x, mean = 0, sd = sd(residuals(modelo_multiple_sig))),
      col = "#4338CA", lwd = 2, add = TRUE)
Q-Q Plot e Histograma de Residuos — modelo múltiple

Q-Q Plot e Histograma de Residuos — modelo múltiple

par(mfrow = c(1, 1))
sw_res_multiple = shapiro.test(residuals(modelo_multiple_sig))
cat(sprintf("  W        : %.6f\n", sw_res_multiple$statistic))
##   W        : 0.998105
cat(sprintf("  p-valor  : %.4f\n", sw_res_multiple$p.value))
##   p-valor  : 0.3295
cat(sprintf("  Decisión : %s\n",
    ifelse(sw_res_multiple$p.value < 0.05,
           "RECHAZA H0 — Residuos NO siguen distribucion normal",
           "No rechaza H0 — Residuos normales confirmado")))
##   Decisión : No rechaza H0 — Residuos normales confirmado

Análisis: El Q-Q Plot muestra que los puntos se alinean de forma muy precisa sobre la diagonal de referencia a lo largo de todo el rango, con desviaciones mínimas únicamente en los extremos de las colas. El histograma de residuos exhibe una forma de campana simétrica que se ajusta con precisión a la curva normal teórica (línea azul oscura), centrada en cero. La prueba de Shapiro-Wilk confirma este resultado formalmente: con W = 0.998105 y p-valor = 0.3295 > α = 0.05, no se rechaza H₀. Se concluye que εᵢ ~ N(0, σ²), validando que los p-valores y los intervalos de confianza del modelo múltiple son matemáticamente correctos y confiables.


7.6.6 Conclusión de los Supuestos

El modelo múltiple cumple cuatro de los cinco supuestos de Gauss-Markov. La heterocedasticidad detectada mediante la prueba de Breusch-Pagan (BP = 19.4810, p = 2.1741e-04) fue corregida aplicando errores estándar robustos de White (HC3), tras lo cual los tres predictores (horas_reales, experiencia_dev y horas_qa) mantienen su significancia estadística con p-valor < 2.2e-16. Los coeficientes β̂ permanecen insesgados y consistentes, y la inferencia estadística es confiable. El modelo es, por tanto, estadísticamente válido y confiable para predecir el costo de un ticket a partir de las horas de desarrollo, la experiencia del desarrollador y las horas de QA invertidas.


8 Selección del Modelo de Regresión Lineal— Criterio de Información de Akaike (AIC)

8.1 Cálculo del AIC

El AIC penaliza la complejidad del modelo: un modelo con más parámetros solo es preferible si la ganancia en ajuste compensa la penalización por complejidad.

Menor AIC indica mejor modelo.

\[AIC = 2k - 2\ln(\hat{L})\]

Donde \(k\) es el número de parámetros estimados y \(\hat{L}\) es el valor máximo de la función de verosimilitud.

aic_simple   = AIC(modelo_simple)
aic_multiple = AIC(modelo_multiple_sig)

tabla_seleccion = data.frame(
  Modelo      = c("Regresión Simple", "Regresión Múltiple"),
  Predictores = c(1, 3),
  R2          = c(r2, r2_m),
  R2_ajustado = c(r2_adj, r2_adj_m),
  AIC         = c(round(aic_simple, 2), round(aic_multiple, 2)),
  Seleccionado = c(
    ifelse(aic_simple < aic_multiple, "SI", ""),
    ifelse(aic_multiple < aic_simple, "SI", "")
  )
)

tabla_seleccion
delta_aic = round(abs(aic_simple - aic_multiple), 2)
cat(sprintf("  ΔAIC (Simple - Múltiple) : %.2f\n", aic_simple - aic_multiple))
##   ΔAIC (Simple - Múltiple) : 4158.58
cat(sprintf("  Modelo seleccionado      : %s\n",
    ifelse(aic_multiple < aic_simple,
           "Regresión Múltiple (AIC menor)",
           "Regresión Simple (AIC menor)")))
##   Modelo seleccionado      : Regresión Múltiple (AIC menor)

Análisis: Con ΔAIC = 4158.58, la evidencia a favor del modelo con menor AIC es muy fuerte.

El modelo múltiple incorpora 3 predictores (horas_reales, experiencia_dev y horas_qa) frente al predictor único (complejidad) del modelo simple.

La diferencia en R² ajustado entre ambos modelos (0.7762 vs 0.9965) indica una ganancia sustancial en capacidad explicativa que justifica la complejidad adicional.


8.2 Selección Final

modelo_ganador = ifelse(aic_multiple < aic_simple,
                         "Regresión Múltiple", "Regresión Simple")


cat(sprintf("  Modelo   : %s\n",        modelo_ganador))
##   Modelo   : Regresión Múltiple
cat(sprintf("  AIC      : %.2f\n",      min(aic_simple, aic_multiple)))
##   AIC      : 9971.93
cat(sprintf("  R²       : %.4f\n",      ifelse(aic_multiple < aic_simple, r2_m, r2)))
##   R²       : 0.9965
cat(sprintf("  R² Ajust.: %.4f\n",      ifelse(aic_multiple < aic_simple, r2_adj_m, r2_adj)))
##   R² Ajust.: 0.9965

Conclusión de selección: El criterio AIC selecciona el modelo Regresión Múltiple como el más óptimo para predecir costo_ticket a partir de las variables disponibles en el dataset de tickets de Mesa de Ayuda. Este modelo balancea de forma óptima el ajuste a los datos y la parsimonia, principio estadístico que privilegia la explicación más simple que sea suficientemente precisa.


9 Regresión Logística

9.1 ¿Por qué necesitamos una variable dicotómica?

La regresión logística requiere que la variable dependiente sea binaria: solo puede tomar dos valores posibles (0 o 1), donde 1 representa la ocurrencia del evento de interés y 0 su ausencia. A diferencia de la regresión lineal, que predice una cantidad continua (¿cuánto cuesta un ticket?), la regresión logística responde a una pregunta diferente: ¿ocurrirá o no ocurrirá este evento?

El dataset odoo_tickets_1000.csv no contiene ninguna variable binaria de forma nativa. Sin embargo, es posible construir una a partir de las variables existentes, transformando una variable continua en una categoría de riesgo con criterio operativo claro.

9.2 Creación de la Variable Dicotómica — ticket_critico

9.2.1 Justificación de la variable base y el umbral

Se utilizará la variable satisfaccion como base para construir la variable dicotómica. La razón es directa: la satisfacción del cliente es el indicador de resultado más relevante en un equipo de Mesa de Ayuda — es la variable que resume si el trabajo realizado cumplió o no las expectativas del cliente. Las demás variables del dataset (horas_reales, costo_ticket, complejidad) describen el esfuerzo invertido, pero satisfaccion describe el resultado percibido por el cliente.

La escala de satisfacción va de 1 a 10. Se define como ticket crítico (valor = 1) aquel cuya satisfacción es menor o igual a 5, lo que representa una experiencia claramente negativa para el cliente. Los tickets con satisfacción mayor a 5 se clasifican como no críticos (valor = 0). Este umbral es operativamente coherente: en una escala del 1 al 10, una calificación de 5 o menos indica que el cliente está insatisfecho y el ticket representa un riesgo para la relación comercial con el Partner Gold.

La pregunta de investigación que guía este análisis es:

¿Es posible predecir si un ticket será crítico (insatisfacción del cliente) a partir de la complejidad del ticket y otras variables operativas del proceso?

9.2.2 Crear la variable dicotómica ticket_critico

# 1 = Ticket crítico (satisfaccion <= 5): cliente insatisfecho
# 0 = Ticket no crítico (satisfaccion > 5): cliente satisfecho

df$ticket_critico = ifelse(df$satisfaccion <= 5, 1, 0)

# Distribución de la nueva variable
tabla_critico = table(Clasificacion = df$ticket_critico)

cat(sprintf("\n  Tickets críticos     (1): %d (%.1f%%)\n  Tickets no críticos  (0): %d (%.1f%%)\n", tabla_critico[2], (tabla_critico[2]/nrow(df))*100, tabla_critico[1], (tabla_critico[1]/nrow(df))*100))
## 
##   Tickets críticos     (1): 540 (54.0%)
##   Tickets no críticos  (0): 460 (46.0%)

Análisis: La distribución de la variable ticket_critico muestra la proporción de tickets que generaron insatisfacción en el cliente. Este balance es clave para garantizar que el modelo logístico tenga suficiente representación de ambas categorías y pueda aprender a discriminar entre ellas.


10 Modelo de Regresión Logística Simple

10.1 Ajuste del Modelo

Se ajusta un modelo de regresión logística simple utilizando complejidad como único predictor de ticket_critico. La elección de complejidad como variable independiente mantiene coherencia con el hilo analítico del documento: si la complejidad predijo el costo en los modelos lineales, es razonable preguntarse si también predice la insatisfacción del cliente.

modelo_logit_simple = glm(ticket_critico ~ complejidad, data   = df, family = "binomial")
summary(modelo_logit_simple)
## 
## Call:
## glm(formula = ticket_critico ~ complejidad, family = "binomial", 
##     data = df)
## 
## Coefficients:
##             Estimate Std. Error z value Pr(>|z|)    
## (Intercept) -3.09788    0.21593  -14.35   <2e-16 ***
## complejidad  0.89100    0.06162   14.46   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 1379.89  on 999  degrees of freedom
## Residual deviance:  916.91  on 998  degrees of freedom
## AIC: 920.91
## 
## Number of Fisher Scoring iterations: 6

10.2 Ecuación del Modelo

b0_l = round(coef(modelo_logit_simple)[1], 4)
b1_l = round(coef(modelo_logit_simple)[2], 4)

cat(sprintf("  Log-Odds = %.4f + %.4f * complejidad\n", b0_l, b1_l))
##   Log-Odds = -3.0979 + 0.8910 * complejidad

La ecuación del modelo en escala Log-Odds (Logit) es:

\[\ln\left(\frac{P}{1-P}\right) = -3.0979 + 0.891 \cdot \text{complejidad}\]

Y la ecuación en escala de probabilidad (Función Sigmoide) es:

\[P(\text{ticket crítico}) = \frac{1}{1 + e^{-(-3.0979 + 0.891 \cdot \text{complejidad})}}\]

Donde:

  • \(P\) = probabilidad estimada de que un ticket sea crítico
  • \(-3.0979\) = intercepto: log-odds de que un ticket sea crítico cuando la complejidad es 0
  • \(0.891\) = pendiente: cambio en los log-odds por cada punto adicional de complejidad

10.3 Análisis del Modelo

# Desglose de coeficientes
coef_ls <- summary(modelo_logit_simple)$coefficients

cat(sprintf(
  "\n  Intercepto:\n    Estimado : %.4f\n    p-valor  : %.4e\n  complejidad:\n    Estimado : %.4f\n    p-valor  : %.4e\n  AIC del modelo: %.2f\n",
  coef_ls[1,1], coef_ls[1,4], 
  coef_ls[2,1], coef_ls[2,4], 
  AIC(modelo_logit_simple)
))
## 
##   Intercepto:
##     Estimado : -3.0979
##     p-valor  : 1.1192e-46
##   complejidad:
##     Estimado : 0.8910
##     p-valor  : 2.2078e-47
##   AIC del modelo: 920.91

Análisis: El p-valor asociado a complejidad es 2.2078e-47 << α = 0.05, confirmando que es un predictor altamente significativo. El coeficiente positivo (0.8910) indica que a mayor complejidad, mayor probabilidad de que el ticket sea crítico: por cada punto adicional de esfuerzo en escala Fibonacci, los log-odds de insatisfacción aumentan en 0.891 unidades.

10.4 Odds Ratios — Interpretación de los Coeficientes

Los coeficientes del modelo logístico están en escala logarítmica y no son directamente interpretables. Al aplicar la función exponencial (exp()), se obtienen los Odds Ratios (OR), que expresan cuántas veces aumentan (o disminuyen) las probabilidades de que el ticket sea crítico por cada unidad adicional de la variable predictora.

or_simple = exp(cbind(OR = coef(modelo_logit_simple), confint(modelo_logit_simple)))
round(or_simple, 4)
##                 OR  2.5 % 97.5 %
## (Intercept) 0.0451 0.0292 0.0681
## complejidad 2.4376 2.1698 2.7631

Análisis: El OR de complejidad es 2.4376. Esto significa que por cada punto adicional de complejidad, las probabilidades de que un ticket sea crítico se multiplican por 2.44. Es un factor de riesgo claro y sustancial: un ticket de 5 puntos tiene probabilidades de insatisfacción considerablemente más altas que uno de 2 puntos.

10.5 Evaluación del Poder Predictivo — Matriz de Confusión

# Probabilidades predichas
prob_simple = predict(modelo_logit_simple, type = "response")

# Clasificación binaria con umbral de 0.5
pred_simple = ifelse(prob_simple > 0.5, 1, 0)

# Matriz de confusión
matriz_simple = table(Prediccion = pred_simple, Real=modelo_logit_simple$y)

# Visualización de la Matriz de Confusión — Modelo Logístico Simple
datos_conf_simple = as.data.frame(as.table(matriz_simple))
colnames(datos_conf_simple) <- c("Predicho", "Real", "Frecuencia")

datos_conf_simple$Predicho = factor(
  ifelse(datos_conf_simple$Predicho == 1, "Crítico (1)", "No Crítico (0)"),
  levels = c("No Crítico (0)", "Crítico (1)"))

datos_conf_simple$Real <- factor(
  ifelse(datos_conf_simple$Real == 1, "Crítico (1)", "No Crítico (0)"),
  levels = c("Crítico (1)", "No Crítico (0)"))

ggplot(datos_conf_simple, aes(x = Predicho, y = Real, fill = Frecuencia)) +
  geom_tile(color = "black", linewidth = 1) +
  geom_text(aes(label = Frecuencia), size = 10, fontface = "bold") +
  scale_fill_gradient(low = "white", high = "#E74C3C") +
  labs(
    title    = "Matriz de Confusión — Modelo Logístico Simple",
    subtitle = "Evaluación sobre n = 1.000 tickets",
    x        = "Predicción del Modelo",
    y        = "Realidad Observada"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    axis.text = element_text(size = 12, face = "bold"),
    title = element_text(size = 14, face = "bold"),
    legend.position = "none"
  )

# Precisión global
precision_simple = sum(diag(matriz_simple)) / sum(matriz_simple)
cat(sprintf("\n  Precisión global del modelo: %.2f%%\n",
            precision_simple * 100))
## 
##   Precisión global del modelo: 76.80%

Análisis: El modelo logístico simple alcanza una precisión global del 76.80%. Clasificó correctamente 404 tickets no críticos (Verdaderos Negativos) y 364 tickets críticos (Verdaderos Positivos). Sin embargo, cometió 176 Falsos Negativos — tickets que el modelo predijo como no críticos pero que terminaron siendo críticos — y 56 Falsos Positivos. En el contexto de Mesa de Ayuda, los 176 Falsos Negativos son el error más costoso: representan clientes insatisfechos que el equipo no detectó a tiempo.

10.6 Métricas de Desempeño

VP_s = matriz_simple[2, 2]   # Crítico predicho como Crítico
VN_s = matriz_simple[1, 1]   # No Crítico predicho como No Crítico
FP_s = matriz_simple[2, 1]   # No Crítico predicho como Crítico
FN_s = matriz_simple[1, 2]   # Crítico predicho como No Crítico

sensibilidad_s = VP_s / (VP_s + FN_s)
especificidad_s = VN_s / (VN_s + FP_s)
precision_s = sum(diag(matriz_simple)) / sum(matriz_simple)

cat(sprintf(
  " Sensibilidad (Recall): %.2f%%\n Especificidad: %.2f%%\n Precisión Global: %.2f%%\n",
  sensibilidad_s * 100,
  especificidad_s * 100,
  precision_s * 100
))
##  Sensibilidad (Recall): 67.41%
##  Especificidad: 87.83%
##  Precisión Global: 76.80%

Análisis:

  • Precisión Global (76.80%): De cada 100 tickets analizados, el modelo clasifica correctamente 77. Es un punto de partida razonable para un modelo con un solo predictor.

  • Sensibilidad o Recall (67.41%): El modelo identifica correctamente el 67.4% de los tickets que en realidad son críticos. Dicho de otro modo, de cada 10 tickets que un cliente calificaría negativamente, el modelo detecta aproximadamente 7 y “se le escapa” 1 de cada 3. En el contexto de Mesa de Ayuda, este es el error más costoso operativamente: un ticket crítico no detectado a tiempo es un cliente insatisfecho que no recibe atención prioritaria.

  • Especificidad (87.83%): El modelo reconoce correctamente el 87.8% de los tickets que no son críticos. Cuando el modelo dice “este ticket va bien”, acierta en casi 9 de cada 10 casos. Esto indica que el modelo es conservador: prefiere no generar falsas alarmas, pero a costa de dejar pasar tickets problemáticos.

Diagnóstico: El modelo simple presenta un desequilibrio claro entre sensibilidad y especificidad. Es bueno evitando falsos positivos, pero no lo suficientemente sensible para capturar todos los tickets críticos. Esto motiva la incorporación de más predictores en el modelo múltiple.


11 Modelo de Regresión Logística Múltiple

11.1 Ajuste del Modelo

Se incorporan múltiples predictores para mejorar la capacidad del modelo de identificar tickets críticos. Se incluyen complejidad, horas_reales, retrabajos, n_revisiones y dias_resolucion, variables que desde una perspectiva operativa tienen sentido como factores que pueden deteriorar la experiencia del cliente: tickets que toman más tiempo, requieren más correcciones o generan más retrabajos son candidatos naturales a producir insatisfacción.

modelo_logit_multiple = glm(ticket_critico ~ complejidad + horas_reales +
                               retrabajos + n_revisiones + dias_resolucion,
                             data   = df,
                             family = "binomial")
summary(modelo_logit_multiple)
## 
## Call:
## glm(formula = ticket_critico ~ complejidad + horas_reales + retrabajos + 
##     n_revisiones + dias_resolucion, family = "binomial", data = df)
## 
## Coefficients:
##                 Estimate Std. Error z value Pr(>|z|)    
## (Intercept)     -7.12531    0.48313 -14.748  < 2e-16 ***
## complejidad      0.07830    0.08770   0.893    0.372    
## horas_reales     0.43900    0.04183  10.494  < 2e-16 ***
## retrabajos       0.47481    0.11947   3.974 7.06e-05 ***
## n_revisiones    -0.07141    0.06536  -1.093    0.275    
## dias_resolucion  0.03929    0.11760   0.334    0.738    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 1379.89  on 999  degrees of freedom
## Residual deviance:  647.73  on 994  degrees of freedom
## AIC: 659.73
## 
## Number of Fisher Scoring iterations: 7

11.2 Ecuación del Modelo

coefs_lm = round(coef(modelo_logit_multiple), 4)
coefs_lm
##     (Intercept)     complejidad    horas_reales      retrabajos    n_revisiones 
##         -7.1253          0.0783          0.4390          0.4748         -0.0714 
## dias_resolucion 
##          0.0393

\[\ln\left(\frac{P}{1-P}\right) = -7.1253 + 0.0783 \cdot \text{complejidad} + 0.439 \cdot \text{horas_reales} + 0.4748 \cdot \text{retrabajos} -0.0714 \cdot \text{n_revisiones} + 0.0393 \cdot \text{dias_resolucion}\]

Y la ecuación en escala de probabilidad (Función Sigmoide) es:

\[P(\text{ticket crítico}) = \frac{1}{1 + e^{-(-7.1253 + 0.0783 \cdot \text{complejidad} + 0.439 \cdot \text{horas_reales} + 0.4748 \cdot \text{retrabajos} -0.0714 \cdot \text{n_revisiones} + 0.0393 \cdot \text{dias_resolucion})}}\]

11.2.1 Ecuación del modelo seleccionando las variables más significativas

# Modelo logístico significativo — solo predictores con p < 0.05
modelo_logit_multiple_sig = glm(ticket_critico ~ horas_reales + retrabajos,
                        data   = df,
                        family = "binomial")
summary(modelo_logit_multiple_sig)
## 
## Call:
## glm(formula = ticket_critico ~ horas_reales + retrabajos, family = "binomial", 
##     data = df)
## 
## Coefficients:
##              Estimate Std. Error z value Pr(>|z|)    
## (Intercept)  -7.10479    0.47757 -14.877  < 2e-16 ***
## horas_reales  0.45052    0.03017  14.935  < 2e-16 ***
## retrabajos    0.50116    0.10465   4.789 1.68e-06 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## (Dispersion parameter for binomial family taken to be 1)
## 
##     Null deviance: 1379.89  on 999  degrees of freedom
## Residual deviance:  649.87  on 997  degrees of freedom
## AIC: 655.87
## 
## Number of Fisher Scoring iterations: 7
coefs_lm_sig = round(coef(modelo_logit_multiple_sig), 4)
coefs_lm_sig
##  (Intercept) horas_reales   retrabajos 
##      -7.1048       0.4505       0.5012

\[\ln\left(\frac{P}{1-P}\right) = -7.1048+ 0.4505 \cdot \text{horas_reales} + 0.5012 \cdot \text{retrabajos}\]

Y la ecuación en escala de probabilidad (Función Sigmoide) es:

\[P(\text{ticket crítico}) = \frac{1}{1 + e^{-(-7.1048+ 0.4505 \cdot \text{horas_reales} + 0.5012 \cdot \text{retrabajos})}}\] Se toma este modelo con horas_reales y retrabajos como variables ya que tiene un AIC menor (655.87) comparado con el primero modelo de regresión logística múltiple que se generó agregando variables como complejidad, n_revisiones y dias_resolucion (AIC= 659.73).

11.3 Análisis del Modelo — Revisión de Variables Predictoras

cat(sprintf(
  "  AIC modelo múltiple  : %.2f\n  AIC modelo simple    : %.2f\n  Devianza nula        : %.2f\n  Devianza residual    : %.2f\n",
  AIC(modelo_logit_multiple_sig),
  AIC(modelo_logit_simple),
  modelo_logit_multiple$null.deviance,
  modelo_logit_multiple$deviance
))
##   AIC modelo múltiple  : 655.87
##   AIC modelo simple    : 920.91
##   Devianza nula        : 1379.89
##   Devianza residual    : 647.73

Análisis: La devianza residual se redujo de 916.91 (modelo simple) a 647.73 (modelo múltiple) y el AIC tambien se redujo significativamente de 920.91 (modelo simple) a 655.87 (modelo múltple); lo que indica que la incorporación de horas_reales y retrabajos resuelve sustancialmente más incertidumbre sobre qué tickets serán críticos.

11.4 Odds Ratios — Modelo Múltiple

or_multiple = exp(cbind(OR = coef(modelo_logit_multiple_sig), confint(modelo_logit_multiple_sig)))
round(or_multiple, 4)
##                  OR  2.5 % 97.5 %
## (Intercept)  0.0008 0.0003 0.0020
## horas_reales 1.5691 1.4832 1.6697
## retrabajos   1.6506 1.3488 2.0347

Análisis: Los únicos predictores con OR estadísticamente significativo son horas_reales (OR = 1.5691) y retrabajos (OR = 1.6506). Esto significa que por cada hora real adicional invertida en el ticket, las probabilidades de insatisfacción aumentan casi un 57%, y por cada retrabajo adicional, aumentan un 65%.

11.5 Evaluación del Poder Predictivo — Matriz de Confusión

prob_multiple = predict(modelo_logit_multiple_sig, type = "response")
pred_multiple = ifelse(prob_multiple > 0.5, 1, 0)

matriz_multiple = table(Prediccion = pred_multiple,
                         Real       = modelo_logit_multiple_sig$y)

# Visualización de la Matriz de Confusión — Modelo Logístico Múltple
datos_conf_multiple = as.data.frame(as.table(matriz_multiple))
colnames(datos_conf_multiple) <- c("Predicho", "Real", "Frecuencia")

datos_conf_multiple$Predicho = factor(
  ifelse(datos_conf_multiple$Predicho == 1, "Crítico (1)", "No Crítico (0)"),
  levels = c("No Crítico (0)", "Crítico (1)"))

datos_conf_multiple$Real <- factor(
  ifelse(datos_conf_multiple$Real == 1, "Crítico (1)", "No Crítico (0)"),
  levels = c("Crítico (1)", "No Crítico (0)"))

ggplot(datos_conf_multiple, aes(x = Predicho, y = Real, fill = Frecuencia)) +
  geom_tile(color = "black", linewidth = 1) +
  geom_text(aes(label = Frecuencia), size = 10, fontface = "bold") +
  scale_fill_gradient(low = "white", high = "#E74C3C") +
  labs(
    title    = "Matriz de Confusión — Modelo Logístico Múltple",
    subtitle = "Evaluación sobre n = 1.000 tickets",
    x        = "Predicción del Modelo",
    y        = "Realidad Observada"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    axis.text = element_text(size = 12, face = "bold"),
    title = element_text(size = 14, face = "bold"),
    legend.position = "none"
  )

precision_multiple = sum(diag(matriz_multiple)) / sum(matriz_multiple)
cat(sprintf("\n  Precisión global del modelo: %.2f%%\n", precision_multiple * 100))
## 
##   Precisión global del modelo: 83.70%

Análisis: El modelo múltiple mejora la precisión global al 83.70% frente al 76.80% del modelo simple. Los Falsos Negativos se redujeron a la mitad: de 176 a 87, lo que significa que el equipo de Mesa de Ayuda ahora detecta correctamente el doble de los tickets críticos que antes pasaban desapercibidos. Los Verdaderos Positivos aumentaron de 364 a 453, una mejora del 24.5% en la detección de insatisfacción real del cliente.

11.6 Métricas de Desempeño

VP_m = matriz_multiple[2, 2]   # Crítico predicho como Crítico
VN_m = matriz_multiple[1, 1]   # No Crítico predicho como No Crítico
FP_m = matriz_multiple[2, 1]   # No Crítico predicho como Crítico
FN_m = matriz_multiple[1, 2]   # Crítico predicho como No Crítico

sensibilidad_m = VP_m / (VP_m + FN_m)
especificidad_m = VN_m / (VN_m + FP_m)
precision_m = sum(diag(matriz_multiple)) / sum(matriz_multiple)

cat(sprintf(
  " Sensibilidad (Recall): %.2f%%\n Especificidad: %.2f%%\n Precisión Global: %.2f%%\n",
  sensibilidad_m * 100,
  especificidad_m * 100,
  precision_m * 100
))
##  Sensibilidad (Recall): 83.89%
##  Especificidad: 83.48%
##  Precisión Global: 83.70%

Análisis

  • Precisión Global (83.70%): El modelo clasifica correctamente 84 de cada 100 tickets, una mejora de 7 puntos porcentuales respecto al modelo simple.

  • Sensibilidad o Recall (83.89%): El modelo identifica correctamente el 83.9% de los tickets que realmente generan insatisfacción en el cliente. Esto representa una mejora sustancial respecto al 67.4% del modelo simple: de los 540 tickets críticos existentes, el modelo ahora detecta 453 en lugar de 364, es decir, 89 tickets críticos adicionales que antes pasaban desapercibidos.

  • Especificidad (83.48%): El modelo sigue reconociendo correctamente el 83.5% de los tickets no críticos. Aunque esta métrica bajó ligeramente respecto al modelo simple (87.8%), la ganancia en sensibilidad es operativamente mucho más valiosa: es preferible generar algunas alarmas adicionales (falsos positivos) que dejar sin atención a clientes insatisfechos reales (falsos negativos).

Diagnóstico: A diferencia del modelo simple, el modelo múltiple logra un equilibrio entre sensibilidad y especificidad (83.89% vs 83.48%), lo cual es una característica deseable en modelos de clasificación. El modelo ya no sacrifica la detección de casos críticos para evitar falsas alarmas: ambos tipos de error están controlados de forma similar.


12 Selección del Modelo Logístico — Criterio AIC

El AIC penaliza la complejidad del modelo: un modelo con más parámetros solo es preferible si la ganancia en ajuste compensa la penalización por complejidad. Menor AIC indica mejor modelo.

aic_logit_simple  = AIC(modelo_logit_simple)
aic_logit_multiple = AIC(modelo_logit_multiple_sig)

tabla_logit_sel = data.frame(
  Modelo       = c("Logística Simple", "Logística Múltiple"),
  Predictores  = c(1, 2),
  AIC          = c(round(aic_logit_simple,   2),
                   round(aic_logit_multiple, 2)),
  Devianza_Res = c(round(modelo_logit_simple$deviance,   2),
                   round(modelo_logit_multiple_sig$deviance, 2)),
  Seleccionado = c(
    ifelse(aic_logit_simple   < aic_logit_multiple, "SI", ""),
    ifelse(aic_logit_multiple < aic_logit_simple,   "SI", "")
  )
)
tabla_logit_sel
delta_aic_l = round(abs(aic_logit_simple - aic_logit_multiple), 2)

cat(sprintf(
  "\n  ΔAIC (Simple - Múltiple) : %.2f\n  Modelo seleccionado      : %s\n",
  aic_logit_simple - aic_logit_multiple,
  ifelse(aic_logit_multiple < aic_logit_simple,
         "Logística Múltiple (AIC menor)",
         "Logística Simple (AIC menor)")
))
## 
##   ΔAIC (Simple - Múltiple) : 265.04
##   Modelo seleccionado      : Logística Múltiple (AIC menor)

Análisis: Con ΔAIC = 265.04, la evidencia a favor del modelo con menor AIC es

muy fuerte — descartar el modelo con mayor AIC.

Con ΔAIC = 265.04, la evidencia a favor del modelo logístico múltiple es muy fuerte. El modelo simple con solo complejidad tiene AIC = 920.91, mientras que el múltiple alcanza AIC = 655.87 — una diferencia de más de 260 unidades. El modelo logístico múltiple es claramente el mejor para predecir si un ticket será crítico en el equipo de Mesa de Ayuda.

12.1 Tabla comparativa de métricas de desempeño

metricas_comparativas = data.frame(
  Metrica = c("Precisión Global", "Sensibilidad (Recall)", "Especificidad"),
  Modelo_Simple = c(
    round(precision_simple * 100, 2),
    round(sensibilidad_s * 100, 2),
    round(especificidad_s * 100, 2)
  ),
  Modelo_Multiple = c(
    round(precision_multiple * 100, 2),
    round(sensibilidad_m * 100, 2),
    round(especificidad_m * 100, 2)
  ),
  Mejora_pp = c(
    round((precision_multiple - precision_simple) * 100, 2),
    round((sensibilidad_m - sensibilidad_s) * 100, 2),
    round((especificidad_m - especificidad_s) * 100, 2)
  )
)
metricas_comparativas

Análisis : La ganancia más significativa del modelo múltiple está en la sensibilidad (+16.5 puntos porcentuales): el salto de 67.4% a 83.9% representa el principal argumento para elegir el modelo múltiple en producción. La ligera caída en especificidad (-4.4 pp) es un intercambio aceptable: el equipo de Mesa de Ayuda prefiere investigar algunos tickets que resultan no ser críticos (falsos positivos) antes que ignorar clientes genuinamente insatisfechos (falsos negativos). El modelo múltiple basado en horas_reales y retrabajos es, por tanto, el más adecuado tanto estadística como operativamente para la gestión preventiva de la satisfacción del cliente.