library(ggplot2)
library(corrplot)

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.