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.
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
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) ycosto_ticket(Y).
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
## Variables : complejidad, horas_reales, bugs_reportados, retrabajos, satisfaccion, experiencia_dev, horas_qa, n_revisiones, costo_ticket, dias_resolucion
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.
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))
)
resumencomplejidad (Puntos de Esfuerzo)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_compggplot(df, aes(x = factor(complejidad))) +
geom_bar(fill = "#3B82F6", color = "white", alpha = 0.85, width = 0.6) +
geom_text(stat = "count",
aes(label = paste0(..count.., "\n(",
round(..count.. / nrow(df) * 100, 1), "%)")),
vjust = -0.3, size = 3.5) +
labs(
title = "Distribución de Complejidad — Escala Fibonacci",
subtitle = "n = 1.000 tickets | DF Customization & Upgrades",
x = "Puntos de Esfuerzo (Fibonacci)",
y = "Frecuencia"
) +
theme_minimal(base_size = 13) +
theme(plot.title = element_text(face = "bold"))Distribución 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.
complejidadpar(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
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.
complejidadLa prueba de Shapiro-Wilk es válida para n ≤ 5.000, por lo que aplica al dataset completo (n = 1.000).
## ══════════════════════════════════════════
## Prueba de Shapiro-Wilk — complejidad
## ══════════════════════════════════════════
## Estadístico W : 0.819201
## 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 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.
costo_ticket (Costo del Ticket en COP)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 (COP)",
subtitle = "Verde: densidad empírica | Rojo discontinuo: normal teórica",
x = "Costo del Ticket (COP)",
y = "Densidad"
) +
theme_minimal(base_size = 13) +
theme(plot.title = element_text(face = "bold"))Distribución de Costo del Ticket — histograma con densidad
ggplot(df, aes(y = costo_ticket)) +
geom_boxplot(fill = "#A7F3D0", color = "#065F46",
outlier.color = "#DC2626", outlier.shape = 19, outlier.size = 1.5) +
labs(title = "Boxplot — Costo del Ticket (COP)", y = "Costo (COP)") +
theme_minimal(base_size = 13) +
theme(plot.title = element_text(face = "bold"),
axis.text.x = element_blank())Boxplot — Costo del Ticket (valores atípicos en rojo)
Análisis: La distribución del costo muestra una forma asimetria, con sesgo hacia la derecha, producto de la mezcla de tickets de baja complejidad (costos bajos) y alta complejidad (costos elevados). La curva normal teórica no se ajusta bien a los datos. El boxplot revela valores atípicos superiores, correspondientes principalmente a tickets de 13 puntos que generan costos muy por encima de la mediana general.
costo_ticketqqnorm(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)Q-Q Plot — costo_ticket
Análisis: El Q-Q Plot muestra desviaciones sistemáticas de la línea de referencia en ambos extremos, confirmando la ausencia de normalidad. La curvatura es característica de distribuciones con colas más pesadas que la normal, coherente con la estructura real del negocio donde los costos se agrupan en clusters según el nivel de complejidad.
costo_ticket## ══════════════════════════════════════════
## Prueba de Shapiro-Wilk — costo_ticket
## ══════════════════════════════════════════
## Estadístico W : 0.939153
## 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.
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 (COP)"
) +
theme_minimal(base_size = 13) +
theme(plot.title = element_text(face = "bold"))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.
Dado que:
complejidad: discreta ordinal en escala Fibonacci →
NO normalcosto_ticket: continua con distribución multimodal →
NO normalSe 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:
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:
cor_p <- cor.test(df$complejidad, df$costo_ticket, method = "pearson")
cat("══════════════════════════════════════\n")## ══════════════════════════════════════
## Correlación de Pearson
## ══════════════════════════════════════
## r : 0.8812
## t : 58.8716
## gl : 998
## p-valor : 0.00e+00
## 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.
cor_s <- cor.test(df$complejidad, df$costo_ticket, method = "spearman")
cat("══════════════════════════════════════\n")## ══════════════════════════════════════
## Correlación de Spearman
## ══════════════════════════════════════
## rho : 0.8065
## S : 32256825.21
## 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₀.
cor_k <- cor.test(df$complejidad, df$costo_ticket, method = "kendall")
cat("══════════════════════════════════════\n")## ══════════════════════════════════════
## Correlación de Kendall
## ══════════════════════════════════════
## tau : 0.6686
## z : 28.8392
## 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.
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
)
tablamat_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
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.
| Etapa | Variable | Resultado |
|---|---|---|
| Normalidad | complejidad |
No normal — Shapiro p 4.5e-32 |
| Normalidad | costo_ticket |
No normal — Shapiro p 8.4e-20 |
| Método seleccionado | — | Spearman (no paramétrico) |
| Coeficiente Spearman ρ | — | 0.8065 |
| p-valor | — | 5.66e-230 |
| Decisión H₀ | — | Se rechaza H₀ |
Con base en el análisis estadístico sobre los 1.000 tickets del área de Mesa de Ayuda:
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.
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).
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.