library(ggplot2)
library(corrplot)
library(lmtest)
library(car)
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 Gold Partner de Odoo gestiona diariamente tickets que abarcan desde pequeñas configuraciones hasta desarrollos técnicos complejos en Python y OWL/JavaScript.

Cada ticket recibe una estimación de esfuerzo en puntos de complejidad siguiendo la escala Fibonacci {1, 2, 3, 5, 8, 13}, metodología adoptada en los procesos de planning poker del equipo. La pregunta estratégica que motiva este análisis es:

¿La complejidad estimada de un ticket —medida en puntos de esfuerzo Fibonacci— tiene una relación significativa con su costo real de resolución? ¿Es posible construir un modelo que prediga ese costo?

Responder esta pregunta tiene implicaciones directas en la estimación de presupuestos, la fijación de precios de servicios y la asignación eficiente de recursos en los proyectos de personalización Odoo.

1.2 Hipótesis

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

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

Nivel de significancia: \(\alpha = 0.05\)

1.3 Variables de análisis

Rol Variable Descripción Escala
Independiente (\(X\)) complejidad Puntos de esfuerzo asignados en planning poker Fibonacci {1,2,3,5,8,13}
Dependiente (\(Y\)) costo_ticket Costo total de resolución del ticket USD (continua)

2 Base de Datos

2.1 Diccionario de Variables

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

2.2 Carga y Vista Previa

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
if (sum(is.na(df)) == 0) {
  cat("Sin valores faltantes en el dataset.\n")
}
## Sin valores faltantes en el dataset.
head(df, 10)

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 Exploratorio de las Variables de Estudio

3.1 Variable X — complejidad (Puntos de Esfuerzo Fibonacci)

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 Distribución

ggplot(df, aes(x = factor(complejidad))) +
  geom_bar(aes(y = after_stat(count) / sum(after_stat(count))),
           fill = "#3B82F6", color = "white", alpha = 0.85, width = 0.6) +
  geom_text(aes(y     = after_stat(count) / sum(after_stat(count)),
                label = paste0(after_stat(count), "\n(",
                               round(after_stat(count)/nrow(df)*100,1), "%)")),
            stat = "count", vjust = -0.3, size = 3.5) +
  labs(title    = "Distribucion de Complejidad - Escala Fibonacci",
       subtitle = "Frecuencia relativa por nivel de esfuerzo",
       x = "Puntos de Esfuerzo (Fibonacci)", y = "Proporcion") +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold"))
Distribución de complejidad — escala Fibonacci

Distribución de complejidad — escala Fibonacci

Análisis: La distribución es discreta y asimétrica. Los niveles 2, 3 y 5 concentran el 73.5% de los tickets, coherente con un equipo donde predominan desarrollos de mediana dificultad. Los tickets de 13 puntos (4.6%) corresponden a integraciones complejas o migraciones técnicas de alta envergadura.

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 con múltiples picos discretos, completamente alejada de la campana normal teórica. El Q-Q Plot confirma desviaciones marcadas de la línea de referencia en todos los puntos, por los saltos discontinuos propios de la escala Fibonacci.

3.1.4 Prueba de Normalidad — Shapiro-Wilk

sw_comp = shapiro.test(df$complejidad)

cat(sprintf(
  "Estadistico W: %.6f\n p-valor: %.2e\n Decision: %s\n",
  sw_comp$statistic,
  sw_comp$p.value,
  ifelse(sw_comp$p.value < 0.05, 
         "RECHAZA H0 - NO sigue distribucion normal", 
         "No rechaza H0 - Compatible con normalidad")
))
## Estadistico W: 0.819201
##  p-valor: 4.50e-32
##  Decision: RECHAZA H0 - NO sigue distribucion normal

Análisis: Con \(p = 4.5 \times 10^{-32} \ll \alpha = 0.05\), se rechaza la normalidad de forma contundente. complejidad no sigue distribución normal. Este resultado es determinante para la elección del método de correlación: descarta Pearson y exige un enfoque no paramétrico.


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

3.2.1 Distribución

ggplot(df, aes(x = costo_ticket)) +
  geom_histogram(aes(y = after_stat(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    = "Distribucion de Costo del Ticket (USD)",
       subtitle = "Verde: densidad empirica | Rojo discontinuo: normal teorica",
       x = "Costo del Ticket (USD)", y = "Densidad") +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold"))
Distribución del costo del ticket

Distribución del costo del 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.2 Prueba de Normalidad — Shapiro-Wilk

sw_costo = shapiro.test(df$costo_ticket)

cat(
  sprintf(" Estadistico W: %.6f\n", sw_costo$statistic),
  sprintf(" p-valor: %.2e\n", sw_costo$p.value),
  sprintf(" Decision: %s\n", 
          ifelse(sw_costo$p.value < 0.05, 
                 "RECHAZA H0 - NO sigue distribucion normal", 
                 "No rechaza H0 - Compatible con normalidad"))
)
##  Estadistico W: 0.939153
##   p-valor: 8.39e-20
##   Decision: RECHAZA H0 - NO sigue distribucion normal

Análisis: \(p = 8.39 \times 10^{-20} < 0.05\). costo_ticket tampoco sigue distribución normal. La distribución refleja la estructura del negocio: los costos están condicionados por el nivel de complejidad, generando una forma multimodal con picos correspondientes a cada nivel Fibonacci. El incumplimiento de normalidad en ambas variables confirma que los métodos no paramétricos son los apropiados.


3.3 Relación Visual 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) +
  geom_smooth(method = "lm", color = "#DC2626", fill = "#FCA5A5",
              linewidth = 1.2, se = TRUE) +
  labs(title    = "Dispersion: Complejidad vs. Costo del Ticket",
       subtitle = "Cada punto es un ticket | Banda roja: IC 95%",
       x = "Complejidad (Puntos Fibonacci)",
       y = "Costo del Ticket (USD)") +
  theme_minimal(base_size = 13) +
  theme(plot.title = element_text(face = "bold"))
Dispersión: complejidad vs costo_ticket con recta de tendencia OLS

Dispersión: complejidad vs costo_ticket con recta de tendencia OLS

Análisis: La nube de puntos muestra una tendencia positiva clara y consistente: a mayor complejidad, mayor costo del ticket. La dispersión crece con el nivel de complejidad, comportamiento típico en proyectos de software donde los tickets de mayor esfuerzo tienen mayor incertidumbre en el costo final. La tendencia lineal captura bien el patrón central.


4 Análisis de Correlación

4.1 Justificación del Método

Dado que ambas variables incumplen el supuesto de normalidad (Shapiro-Wilk rechazó \(H_0\) en ambos casos) y complejidad es de naturaleza ordinal discreta en escala Fibonacci, el método estadísticamente apropiado es el coeficiente de Spearman (\(\rho_s\)). Mide la asociación monótona entre los rangos de los datos, sin exigir normalidad ni relación lineal estricta.

Método Aplica Razon
Pearson (\(r\)) No optimo Requiere normalidad bivariada — no cumplida
Spearman (\(\rho_s\)) Recomendado No parametrico, robusto ante no normalidad

\[H_0: \rho_s = 0 \qquad H_1: \rho_s \neq 0\]

4.2 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("\n rho: %.4f\n", cor_s$estimate),
  sprintf(" S: %.2f\n",  cor_s$statistic),
  sprintf(" p-valor: %.2e\n",  cor_s$p.value),
  sprintf(" Decision (a=0.05): %s\n",
          ifelse(cor_s$p.value < 0.05,
                 "RECHAZA H0 - Existe correlacion positiva significativa",
                 "No rechaza H0"))
)
## 
##  rho: 0.8065
##   S: 32256825.21
##   p-valor: 5.66e-230
##   Decision (a=0.05): RECHAZA H0 - Existe correlacion positiva significativa

Análisis: \(\rho_s = 0.8065\) con \(p\text{-valor} = 5.66 \times 10^{-230}\) se rechaza \(H_0\). Existe una correlación positiva fuerte y estadísticamente significativa entre la complejidad del ticket y su costo de resolución. Los puntos de esfuerzo asignados en el planning no son solo una estimación subjetiva: tienen poder predictivo real sobre el costo que genera cada ticket. Esto valida el proceso de estimación del equipo y justifica la construcción de un modelo predictivo de costos.

4.3 Matriz de Correlación de Spearman — Todas las Variables

Para comprender la estructura completa de relaciones antes de definir el modelo, se calcula la matriz de correlación de Spearman sobre todas las variables numéricas.

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 correlacion de Spearman — todas las variables

Matriz de correlacion de Spearman — todas las variables

Análisis de la matriz: Tres hallazgos clave guían la construcción del modelo:

  1. complejidad correlaciona fuertemente con costo_ticket (\(\rho_s = 0.81\)) — confirma la hipótesis — pero también con horas_reales (\(\rho_s = 0.82\)) y horas_qa (\(\rho_s = 0.69\)). Esto indica que complejidad actúa como un predictor upstream: su efecto sobre el costo viaja a través de las horas que genera.

  2. horas_reales (\(\rho_s = 0.97\)) y horas_qa (\(\rho_s = 0.90\)) presentan las correlaciones más fuertes con costo_ticket, siendo las variables más directamente vinculadas al costo real.

  3. experiencia_dev muestra correlación prácticamente nula con complejidad (\(\rho_s = -0.05\)), lo que indica que es una dimensión independiente y complementaria que puede aportar información única al modelo sin redundancia.


5 Modelo de Regresión Lineal Múltiple

5.1 Justificación del Modelo

La variable dependiente costo_ticket es continua (medida en USD), por lo que la regresión lineal es el marco apropiado. La matriz de correlación reveló que el costo del ticket es un fenómeno multifactorial: la complejidad genera horas de desarrollo y horas de QA, e involucra la experiencia del desarrollador. Estas tres dimensiones medibles y directamente registradas en el sistema explican el costo de forma precisa y completa.

Los predictores seleccionados son horas_reales, experiencia_dev y horas_qa:

  • horas_reales (\(\rho_s = 0.97\) con costo_ticket): el predictor más fuerte. Las horas reales de desarrollo son el insumo más directo del costo.
  • horas_qa (\(\rho_s = 0.90\) con costo_ticket): las horas de pruebas tienen un peso equivalente en la estructura de costos del proyecto.
  • experiencia_dev: aporta información genuinamente independiente sin redundancia, capturando el efecto de la tarifa del desarrollador sobre el costo.

Nota sobre complejidad: La hipótesis quedó respondida en la sección de correlación: \(\rho_s = 0.81\), \(H_0\) rechazada. En el modelo predictivo, el efecto de complejidad queda contenido en horas_reales y horas_qa: la complejidad determina cuántas horas genera un ticket, y las horas determinan el costo. El modelo captura esta cadena causal de forma más directa y precisa.

5.2 Ajuste del Modelo

modelo = lm(costo_ticket ~ horas_reales + experiencia_dev + horas_qa, data = df)
summary(modelo)
## 
## 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

5.3 Ecuación del Modelo

coefs = round(coef(modelo), 4)
coefs
##     (Intercept)    horas_reales experiencia_dev        horas_qa 
##        -63.0498         47.6042         10.5133         47.1697
cat(sprintf(
  "\n costo_ticket = %.4f + %.4f*horas_reales + %.4f*experiencia_dev + %.4f*horas_qa\n",
  coefs[1], coefs[2], coefs[3], coefs[4]
))
## 
##  costo_ticket = -63.0498 + 47.6042*horas_reales + 10.5133*experiencia_dev + 47.1697*horas_qa

La ecuación del modelo ajustado es:

\[\widehat{\text{costo_ticket}} = -63.05 + 47.60 \cdot \text{horas_reales} + 10.51 \cdot \text{experiencia_dev} + 47.17 \cdot \text{horas_qa}\]

Interpretación de los coeficientes:

  • Intercepto (\(\beta_0 = -63.05\)): Constante de ajuste matemático. Sin interpretación práctica en este contexto.

  • horas_reales (\(\beta_1 = 47.60\)): Manteniendo las demás variables constantes, cada hora adicional de desarrollo incrementa el costo del ticket en promedio USD 47.60.

  • experiencia_dev (\(\beta_2 = 10.51\)): A igual carga de horas, cada año adicional de experiencia del desarrollador incrementa el costo en promedio USD 10.51. Los desarrolladores más senior tienen tarifas más altas, lo que se refleja directamente en el costo del ticket.

  • horas_qa (\(\beta_3 = 47.17\)): Cada hora adicional de pruebas QA incrementa el costo en promedio USD 47.17, prácticamente equivalente al costo por hora de desarrollo, lo que refleja que el proceso de QA tiene un peso proporcional en la estructura de costos del equipo.

5.4 Análisis del Modelo

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

cat(sprintf(" R2: %.4f\n", r2))
##  R2: 0.9965
cat(sprintf(" R2 ajustado: %.4f\n", r2_adj))
##  R2 ajustado: 0.9965
cat(sprintf(" F-estadistico: %.2f\n", f_stat))
##  F-estadistico: 95058.02
cat(sprintf(" p-valor (F): %.4e\n", f_pval))
##  p-valor (F): 0.0000e+00

Análisis:

  • \(R^2 = 0.9965\): El modelo explica el 99.65% de la variabilidad del costo. Prácticamente todo el costo de un ticket queda determinado por las tres variables del modelo. Solo el 0.35% restante obedece a factores no capturados.

  • \(F = 95058.02\), \(p < 0.001\): El modelo es globalmente significativo. Los tres predictores en conjunto explican el costo de forma real, sistemática y no atribuible al azar.

  • Todos los predictores son significativos (\(p < 2 \times 10^{-16}\)), confirmando que cada variable aporta información estadísticamente valiosa e independiente.

5.5 Significancia Individual de los Predictores

tabla_coefs = as.data.frame(summary(modelo)$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

Los tres predictores son estadísticamente significativos con \(p < 2 \times 10^{-16}\), confirmando que cada uno aporta información única e independiente para predecir el costo del ticket.

5.6 ANOVA del Modelo

anova(modelo)

Análisis: La tabla ANOVA confirma que los tres predictores contribuyen de forma individual y significativa (\(p \approx 0\) en los tres casos):

  • horas_reales es el predictor dominante (mayor \(F = 275.578\)), explicando la mayor proporción de la varianza del costo.
  • horas_qa aporta la segunda mayor contribución (\(F = 8.734\)), reflejando el peso del proceso de pruebas en la estructura de costos.
  • experiencia_dev contribuye de forma independiente (\(F = 860\)), capturando el efecto de la tarifa del desarrollador sobre el costo total.

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.

6.1 S1 — 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, 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: La línea roja de tendencia es aproximadamente horizontal y los puntos se distribuyen de forma aleatoria alrededor de cero sin ningún patrón sistemático. No hay curvas ni formas de embudo. Se confirma el supuesto de linealidad: la especificación lineal es adecuada para este modelo.

6.2 S2 — 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 = residuals(modelo)
media_res = mean(residuos)

cat(sprintf(" Media aritmetica de los residuos: %.2e\n", media_res))
##  Media aritmetica de los residuos: -1.18e-15
all.equal(media_res, 0)
## [1] TRUE

Análisis: La media de los residuos es \(-1.18 \times 10^{-15}\), un número infinitesimal equivalente a cero (error de precisión computacional estándar). La prueba all.equal confirma la equivalencia algebraica. Se confirma \(E(\varepsilon_i) = 0\). El modelo no presenta sesgo sistemático en sus predicciones.

6.3 S3 — Homocedasticidad

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

bp = bptest(modelo)

cat(
  sprintf(" BP estadistico: %.4f\n", bp$statistic),
  sprintf(" p-valor: %.4e\n", bp$p.value),
  sprintf(" Decision: %s\n",
          ifelse(bp$p.value < 0.05,
                 "RECHAZA H0 - Heterocedasticidad detectada",
                 "No rechaza H0 - Homocedasticidad confirmada"))
)
##  BP estadistico: 19.4810
##   p-valor: 2.1741e-04
##   Decision: RECHAZA H0 - Heterocedasticidad detectada

Análisis: Con \(BP = 19.48\) y \(p = 2.17 \times 10^{-4} < 0.05\), se detecta heterocedasticidad: la varianza de los errores no es constante. Esto no invalida los coeficientes estimados (siguen siendo insesgados), pero sesga los errores estándar. Se aplica corrección con errores robustos de White (HC3):

coeftest(modelo, vcov = vcovHC(modelo, 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.

6.4 S4 — 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 = dwtest(modelo)

cat(sprintf(" DW estadistico: %.4f\n", dw$statistic))
##  DW estadistico: 1.9604
cat(sprintf(" p-valor: %.4f\n", dw$p.value))
##  p-valor: 0.2663
cat(sprintf(" Decision: %s\n",
    ifelse(dw$p.value < 0.05,
           "RECHAZA H0 - Autocorrelacion detectada",
           "No rechaza H0 - Errores independientes")))
##  Decision: No rechaza H0 - Errores independientes

Análisis: \(DW = 1.9604\), cercano al valor teórico ideal de 2, y \(p = 0.2663 > 0.05\). Se confirma la independencia de los residuos: el error de un ticket no contiene información sobre el error del siguiente. Cada observación es estadísticamente autónoma.

6.5 S5 — Normalidad

par(mfrow = c(1, 2))

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

hist(residuos, main = "Histograma - Residuos",
     xlab = "Residuos", col = "#C7D2FE",
     border = "white", freq = FALSE)
curve(dnorm(x, mean = 0, sd = sd(residuos)),
      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 = shapiro.test(residuos)

cat(
  sprintf(" W: %.6f\n", sw_res$statistic),
  sprintf(" p-valor: %.4f\n", sw_res$p.value),
  sprintf(" Decision: %s\n",
          ifelse(sw_res$p.value < 0.05,
                 "RECHAZA H0 - Residuos NO normales",
                 "No rechaza H0 - Residuos normales confirmado"))
)
##  W: 0.998105
##   p-valor: 0.3295
##   Decision: No rechaza H0 - Residuos normales confirmado

Análisis: El Q-Q Plot muestra que los puntos se alinean con precisión sobre la diagonal en todo el rango central. El histograma exhibe una campana simétrica centrada en cero. Shapiro-Wilk confirma formalmente: \(W = 0.9981\), \(p = 0.3295 > 0.05\). Se confirma \(\varepsilon_i \sim N(0, \sigma^2)\), validando que los \(p\)-valores e intervalos de confianza del modelo son matemáticamente correctos y confiables.

6.6 Resumen de Supuestos

El modelo cumple 4 de los 5 supuestos de Gauss-Markov. La heterocedasticidad fue corregida con errores estándar robustos (HC3), preservando la validez de la inferencia. Los coeficientes son insesgados, consistentes y la inferencia es estadísticamente válida.


7 Conclusiones

7.1 Respuesta a la Hipótesis

La prueba de correlación de Spearman arrojó \(\rho_s = 0.8065\) con \(p = 5.66 \times 10^{-230}\). Se rechaza \(H_0\): existe una correlación positiva fuerte y estadísticamente significativa entre la complejidad del ticket y su costo de resolución. La escala Fibonacci utilizada en el planning tiene poder predictivo real sobre el costo que genera cada ticket, validando el proceso de estimación del equipo de Mesa de Ayuda.

7.2 El Modelo Predictivo Final

\[\widehat{\text{costo_ticket}} = -63.05 + 47.60 \cdot \text{horas_reales} + 10.51 \cdot \text{experiencia_dev} + 47.17 \cdot \text{horas_qa}\]

Con \(R^2 = 0.9965\), el modelo explica el 99.65% de la variabilidad del costo. Los tres predictores son significativos, los supuestos se cumplen (con corrección por heterocedasticidad) y la inferencia es estadísticamente válida.

7.3 Aplicación Práctica

nuevo_ticket = data.frame(
  Perfil          = c("Ticket sencillo (2-3 pts)",
                      "Ticket mediano (5 pts)",
                      "Ticket complejo (8 pts)"),
  horas_reales    = c(8, 20, 38),
  experiencia_dev = c(3, 6, 10),
  horas_qa        = c(4, 10, 16)
)

nuevo_ticket$costo_estimado_USD = round(
  predict(modelo, nuevo_ticket[, c("horas_reales","experiencia_dev","horas_qa")]), 2)

nuevo_ticket

El modelo permite estimar el costo de un ticket en tiempo real a partir de tres variables registradas en el sistema. Esto abre la posibilidad de construir una función de estimación automática integrada directamente en el módulo Proyecto de Odoo, usando los datos de cada ticket como input del modelo, facilitando la toma de decisiones en planeación de sprints y fijación de precios de servicios.