| INTEGRANTES | CARNE |
|---|---|
| Neira García, Kevin Alexander | NG23003 |
| Quijano Vásquez, Luis Daniel | QV13001 |
| Ramos Rivas, Marco Antonio | RR14082 |
DOCENTE: MSF. Carlos Ademir Pérez Alas.
El valor económico de un jugador de fútbol profesional es un fenómeno complejo que ha captado la atención tanto de economistas como de analistas deportivos. Desde la publicación del concepto de Moneyball, popularizado por Michael Lewis (2003), los clubes de fútbol han adoptado metodologías cuantitativas para identificar y valorar el talento deportivo de manera más eficiente. Este enfoque, denominado analítica deportiva, busca descomponer el rendimiento de un jugador en atributos medibles que expliquen su precio de mercado.
La teoría del capital humano (Becker, 1964) establece que los individuos acumulan habilidades y conocimientos que se traducen en mayor productividad y, por ende, en mayores ingresos. En el contexto deportivo, el capital humano de un jugador se manifiesta en sus habilidades técnicas, físicas y tácticas. Autores como Franck y Nüesch (2012) extendieron este marco para el fútbol, argumentando que el valor de mercado de un jugador refleja su productividad esperada futura, ajustada por factores institucionales como la edad, el contrato vigente y la posición dentro del campo.
Los modelos de precios hedónicos (Rosen, 1974) descomponen el precio de un bien heterogéneo en función de sus atributos individuales. Aplicados al fútbol, estos modelos estiman cuánto contribuye cada característica observable de un jugador -como su velocidad, precisión en los pases o capacidad de disparo- al valor total de transferencia. Estudios como el de Müller et al. (2017) y Herm et al. (2014) han evidenciado que los atributos técnicos y la visibilidad mediática del jugador son determinantes estadísticamente significativos del valor de mercado, con la base de datos de EA Sports (FIFA/FC) siendo ampliamente utilizada como proxy de las capacidades de los jugadores.
La literatura identifica cuatro grandes grupos de determinantes del valor económico de un jugador:
La base de datos de videojuegos FIFA/FC de EA Sports ha sido validada como un instrumento confiable para aproximar las capacidades reales de los jugadores. Pappalardo et al. (2019) y Deloitte (2022) demuestran que las puntuaciones asignadas en el videojuego correlacionan significativamente con estadísticas reales de desempeño. El dataset FIFA 21 Messy Raw (Kaggle) contiene más de 18,000 observaciones de jugadores con variables detalladas sobre sus atributos técnicos, salarios, valores de mercado y características personales, siendo idóneo para estimar un Modelo Clásico de Regresión Lineal Múltiple (MCRLM) de determinantes del valor económico de un jugador.
Variable dependiente (Y): Valor de mercado del jugador (Value), expresado en euros. Esta variable representa el precio estimado de transferencia de un jugador y es la principal señal del valor económico percibido en el mercado futbolístico.
Se proponen seis regresores (superando el mínimo de cuatro exigido), con base en la literatura revisada:
| Var. | Nombre | Descripción | Fuente / rango |
|---|---|---|---|
| X1 | OVA (Overall Rating) | Puntuación global de habilidades del jugador | EA Sports, 0-99 |
| X2 | Age (Edad) | Edad del jugador en años | 16-43 años |
| X3 | Age² (Edad al cuadrado) | Captura la no linealidad del efecto de la edad | construida a partir de Age |
| X4 | Wage (Salario) | Salario semanal del jugador, en euros | EA Sports / club |
| X5 | POT (Potencial) | Puntuación de potencial máximo estimado | EA Sports, 0-99 |
| X6 | IR (Reputación internacional) | Variable ordinal de fama internacional | EA Sports, 1-5 |
La inclusión de Age al cuadrado (X3) responde directamente a la hipótesis de no linealidad planteada en la literatura (Poli et al., 2020): el valor de un jugador no crece de forma indefinida con la edad, sino que sigue un patrón de “U invertida” -aumenta durante la etapa de desarrollo y consolidación, alcanza un máximo alrededor de los 26-28 años, y luego decrece por la depreciación física del capital humano deportivo. Para representar correctamente esta curvatura dentro de un modelo lineal en los parámetros, es necesario incorporar el término cuadrático como un regresor adicional explícito.
Con base en la teoría del capital humano deportivo y los modelos hedónicos, se plantean las siguientes hipótesis:
El modelo de Regresión Lineal Múltiple (MCRLM) propuesto es el siguiente:
\[Value_i = \beta_0 + \beta_1 OVA_i + \beta_2 Age_i + \beta_3 Age_i^2 + \beta_4 Wage_i + \beta_5 POT_i + \beta_6 IR_i + \varepsilon_i\]
Donde \(\beta_0\) es la constante del modelo, \(\beta_1,\dots,\beta_6\) son los coeficientes de regresión parcial de cada regresor, y \(\varepsilon_i\) es el término de error aleatorio que captura los factores no observados. El subíndice \(i\) denota cada jugador individual en la muestra.
Restricciones esperadas sobre los parámetros (signos):
| Parámetro | Variable | Signo esperado | Justificación |
|---|---|---|---|
| \(\beta_1\) | OVA | \(+\) | Mayor habilidad global implica mayor productividad esperada en cancha (capital humano). |
| \(\beta_2\) | Age | \(+\) | En la etapa de ascenso, mayor edad refleja experiencia y madurez competitiva. |
| \(\beta_3\) | Age² | \(-\) | Captura la depreciación del capital humano tras el pico de rendimiento físico. |
| \(\beta_4\) | Wage | \(+\) | El salario actúa como señal de calidad y compromiso del club (Akerlof, 1970). |
| \(\beta_5\) | POT | \(+\) | El potencial es un “valor de opción” sobre el rendimiento futuro del jugador. |
| \(\beta_6\) | IR | \(+\) | Mayor reputación genera externalidades positivas de marca y patrocinio. |
Como se documenta en la sección de evidencia empírica más abajo, la variable Value presenta una fuerte asimetría positiva, por lo que también se especifica una versión en logaritmos del modelo, a estimarse en la Parte 2 junto con el modelo en niveles:
\[\ln(Value_i) = \beta_0 + \beta_1 OVA_i + \beta_2 Age_i + \beta_3 Age_i^2 + \beta_4 Wage_i + \beta_5 POT_i + \beta_6 IR_i + \varepsilon_i\]
En esta especificación semi-logarítmica, cada coeficiente \(\beta_k\) se interpreta como una semi-elasticidad: el cambio porcentual aproximado en el valor de mercado ante un cambio de una unidad en el regresor correspondiente.
El modelo estadístico establece que el término de error cumple con los supuestos clásicos del MCRLM:
\[E(\varepsilon_i) = 0, \quad Var(\varepsilon_i) = \sigma^2 \text{ (homocedasticidad)}, \quad Cov(\varepsilon_i,\varepsilon_j) = 0 \text{ para } i \neq j \text{ (no autocorrelación)}, \quad \varepsilon_i \sim N(0,\sigma^2) \text{ (normalidad)}\]
Los parámetros serán estimados por Mínimos Cuadrados Ordinarios (MCO), método que, bajo los supuestos de Gauss-Markov, produce estimadores MELI (Mejores Estimadores Lineales e Insesgados). La verificación formal de estos supuestos (normalidad, homocedasticidad, autocorrelación y multicolinealidad), así como las correcciones correspondientes, se desarrollará en la Parte 2 y Parte 3 de este trabajo.
El dataset FIFA 21 Messy Raw contiene información de 18,979 jugadores profesionales de todo el mundo, con variables disponibles en SoFIFA.com. La muestra supera ampliamente el mínimo de 200 observaciones requerido por la guía del trabajo.
Tal como advierte el nombre del dataset (“messy raw data”), varias variables requieren limpieza antes de poder usarse en el modelo:
"€103.5M"."4 ★"."↓OVA"), por lo que se
renombra.# Ajustar la ruta de archivo según la ubicación en su computadora
datos <- read.csv("fifa21 raw data v2.csv",
stringsAsFactors = FALSE,
encoding = "UTF-8")
# La columna 8 es la calificación "Overall", su nombre llega corrupto por el
# símbolo "↓" -> se renombra a OVA
names(datos)[8] <- "OVA"
# --- Limpieza de variables monetarias: "€103.5M" / "€560K" -> numérico en euros ---
limpiar_moneda <- function(x) {
x <- gsub("\u20ac", "", x) # quita el símbolo de euro
mult <- ifelse(grepl("M", x), 1e6,
ifelse(grepl("K", x), 1e3, 1)) # detecta millones / miles
as.numeric(gsub("[MK]", "", x)) * mult
}
# --- Limpieza de variables tipo estrella: "4 ★" -> 4 ---
limpiar_estrellas <- function(x) as.numeric(gsub("[^0-9.]", "", x))
datos$Value <- limpiar_moneda(datos$Value)
datos$Wage <- limpiar_moneda(datos$Wage)
datos$IR <- limpiar_estrellas(datos$IR)
datos$OVA <- as.numeric(datos$OVA)
datos$POT <- as.numeric(datos$POT)
datos$Age <- as.numeric(datos$Age)
# Se excluyen jugadores con Value = 0: corresponden a perfiles sin valor de
# mercado asignado (agentes libres / datos incompletos), no a un valor económico
# real de cero, y además impiden aplicar la transformación logarítmica.
n_antes <- nrow(datos)
datos <- subset(datos, Value > 0)
# Variables derivadas para el modelo
datos$lnValue <- log(datos$Value)
datos$Age2 <- datos$Age^2
cat("Observaciones originales:", n_antes, "\n")## Observaciones originales: 18979
## Observaciones tras excluir Value = 0: 18731
Tras la limpieza, la muestra final cuenta con 18,731 observaciones, todas con datos completos en las seis variables del modelo.
asimetria <- function(x) mean((x - mean(x))^3) / sd(x)^3
curtosis <- function(x) mean((x - mean(x))^4) / sd(x)^4 - 3
vars <- c("Value", "lnValue", "OVA", "Age", "Wage", "POT", "IR")
etiquetas <- c("Value (€)", "ln(Value)", "OVA (0-99)", "Age (años)",
"Wage (€/semana)", "POT (0-99)", "IR (1-5)")
tabla_desc <- t(sapply(vars, function(v) {
x <- datos[[v]]
c(Media = mean(x), Mediana = median(x), `Desv. Est.` = sd(x),
Min = min(x), Max = max(x), Asimetria = asimetria(x), Curtosis = curtosis(x))
}))
rownames(tabla_desc) <- etiquetas
knitr::kable(round(tabla_desc, 2),
caption = "Estadísticos descriptivos de las variables del modelo")| Media | Mediana | Desv. Est. | Min | Max | Asimetria | Curtosis | |
|---|---|---|---|---|---|---|---|
| Value (€) | 2902996.58 | 975000.00 | 7728744.66 | 9000.0 | 185500000.00 | 7.95 | 90.91 |
| ln(Value) | 13.89 | 13.79 | 1.24 | 9.1 | 19.04 | 0.61 | 0.79 |
| OVA (0-99) | 65.68 | 66.00 | 6.97 | 47.0 | 93.00 | 0.10 | 0.02 |
| Age (años) | 25.13 | 25.00 | 4.68 | 16.0 | 43.00 | 0.39 | -0.51 |
| Wage (€/semana) | 9210.03 | 3000.00 | 19809.48 | 500.0 | 560000.00 | 7.27 | 92.52 |
| POT (0-99) | 71.15 | 71.00 | 6.11 | 47.0 | 95.00 | 0.21 | 0.08 |
| IR (1-5) | 1.09 | 1.00 | 0.36 | 1.0 | 5.00 | 4.65 | 25.01 |
Interpretación: la variable Value presenta una asimetría positiva muy fuerte (coeficiente \(\approx 8\)) y un exceso de curtosis muy elevado, confirmando que su distribución está dominada por una gran masa de jugadores de valor bajo-moderado y una “cola larga” de superestrellas con valores extremos. Al aplicar la transformación logarítmica, la asimetría se reduce drásticamente (de \(\approx 8\) a \(\approx 0.6\)) y la curtosis se acerca a la de una distribución normal, lo que confirma empíricamente la necesidad de trabajar con \(\ln(Value)\) planteada en la sección de especificación del modelo estadístico. El resto de regresores (OVA, Age, POT) muestran distribuciones razonablemente simétricas, mientras que Wage e IR replican el mismo patrón de cola larga que Value, coherente con la alta concentración de salarios y fama en un grupo reducido de jugadores de élite.
par(mfrow = c(1, 2))
hist(datos$Value, breaks = 50, col = "steelblue",
main = "Value (niveles)", xlab = "Euros")
hist(datos$lnValue, breaks = 50, col = "darkorange",
main = "ln(Value)", xlab = "ln(Euros)")Como insumo preliminar para la futura verificación de multicolinealidad (Parte 2), se presenta la matriz de correlación entre los regresores propuestos:
# Si no tienen el paquete corrplot instalado: install.packages("corrplot")
library(corrplot)
m_cor <- cor(datos[c("OVA", "Age", "Wage", "POT", "IR")])
knitr::kable(round(m_cor, 3), caption = "Matriz de correlación entre regresores")| OVA | Age | Wage | POT | IR | |
|---|---|---|---|---|---|
| OVA | 1.000 | 0.469 | 0.597 | 0.631 | 0.446 |
| Age | 0.469 | 1.000 | 0.166 | -0.270 | 0.275 |
| Wage | 0.597 | 0.166 | 1.000 | 0.488 | 0.613 |
| POT | 0.631 | -0.270 | 0.488 | 1.000 | 0.310 |
| IR | 0.446 | 0.275 | 0.613 | 0.310 | 1.000 |
corrplot(m_cor, method = "color", type = "upper", addCoef.col = "black",
tl.col = "black", number.cex = 0.9)Interpretación: ninguna correlación entre regresores supera 0.65, lo que sugiere a priori que no existen problemas severos de multicolinealidad, aunque esto se confirmará formalmente con el Factor de Inflación de la Varianza (VIF) en la Parte 2. Las correlaciones más altas se observan entre OVA y POT (0.63) -esperable, ya que ambas son calificaciones de habilidad de EA Sports-, y entre Wage e IR (0.61), consistente con la idea de que los jugadores más mediáticos también perciben los salarios más altos. Llama la atención la correlación negativa entre Age y POT (-0.27): refleja que el potencial de mejora (a diferencia de la calificación actual) es naturalmente mayor en jugadores jóvenes, lo cual refuerza la justificación teórica de incluir Age y Age² como regresores separados de OVA y POT.
La multicolinealidad ocurre cuando dos o más variables regresoras del modelo están altamente correlacionadas entre sí, lo que dificulta estimar de forma precisa el efecto individual de cada regresor sobre la variable dependiente. Aunque los estimadores MCO siguen siendo insesgados, sus varianzas se inflan, provocando coeficientes estadísticamente no significativos aunque el modelo en conjunto sea significativo (R² alto con t-estadísticos bajos).
El modelo propuesto es:
Valorᵢ = β₀ + β₁ Puntuaciónᵢ + β₂ Edadᵢ + β₃ Salarioᵢ + β₄ Potencialᵢ + β₅ Reputaciónᵢ + εᵢ
# datos ya viene limpio de Parte 1 (Value, Wage, IR ya son numéricos, OVA ya renombrado)
datos_raw <- read.csv("fifa21 raw data v2.csv", stringsAsFactors = FALSE, encoding = "UTF-8")
names(datos_raw)[8] <- "OVA"
limpiar_moneda <- function(x) { x <- gsub("\u20ac","",x); m <- ifelse(grepl("M",x),1e6,ifelse(grepl("K",x),1e3,1)); as.numeric(gsub("[KM]","",x))*m }
limpiar_estrellas <- function(x) as.numeric(gsub("[^0-9]","",x))
datos <- datos_raw
datos$Value <- limpiar_moneda(datos_raw$Value)
datos$Wage <- limpiar_moneda(datos_raw$Wage)
datos$IR <- limpiar_estrellas(datos_raw$IR)
datos$OVA <- as.numeric(datos_raw$OVA)
muestra_df <- datos |>
select(
Valor = Value,
Puntuacion = OVA,
Edad = Age,
Salario = Wage,
Potencial = POT,
Reputacion = IR
) |>
filter(
!is.na(Valor), !is.na(Puntuacion), !is.na(Edad),
!is.na(Salario), !is.na(Potencial), !is.na(Reputacion),
Valor > 0, Salario > 0
) |>
mutate(
lnValor = log(Valor),
lnSalario = log(Salario)
)
set.seed(123)
muestra <- muestra_df |> slice_sample(n = 200)
cat("Dimensiones de la muestra:", nrow(muestra), "x", ncol(muestra), "\n")## Dimensiones de la muestra: 200 x 8
vars_modelo <- muestra |>
select(lnValor, Puntuacion, Edad, lnSalario, Potencial, Reputacion)
summary(vars_modelo) |>
kable(caption = "Estadísticas Descriptivas — Muestra (n = 200)", digits = 3) |>
kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE)| lnValor | Puntuacion | Edad | lnSalario | Potencial | Reputacion | |
|---|---|---|---|---|---|---|
| Min. :10.46 | Min. :48.00 | Min. :16.00 | Min. : 6.215 | Min. :60.00 | Min. :1.00 | |
| 1st Qu.:13.12 | 1st Qu.:61.00 | 1st Qu.:21.00 | 1st Qu.: 6.908 | 1st Qu.:67.00 | 1st Qu.:1.00 | |
| Median :13.62 | Median :65.00 | Median :25.00 | Median : 8.006 | Median :70.00 | Median :1.00 | |
| Mean :13.85 | Mean :65.42 | Mean :24.89 | Mean : 8.089 | Mean :71.01 | Mean :1.09 | |
| 3rd Qu.:14.46 | 3rd Qu.:69.25 | 3rd Qu.:28.00 | 3rd Qu.: 8.854 | 3rd Qu.:75.00 | 3rd Qu.:1.00 | |
| Max. :17.84 | Max. :87.00 | Max. :37.00 | Max. :12.388 | Max. :91.00 | Max. :4.00 |
modelo <- lm(lnValor ~ Puntuacion + Edad + lnSalario + Potencial + Reputacion,
data = muestra)
summary(modelo)##
## Call:
## lm(formula = lnValor ~ Puntuacion + Edad + lnSalario + Potencial +
## Reputacion, data = muestra)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1.05420 -0.10199 0.01004 0.11921 0.48727
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 4.227458 0.317075 13.333 < 0.0000000000000002 ***
## Puntuacion 0.190544 0.006909 27.580 < 0.0000000000000002 ***
## Edad -0.129898 0.007371 -17.622 < 0.0000000000000002 ***
## lnSalario 0.047964 0.023111 2.075 0.03927 *
## Potencial -0.002618 0.006133 -0.427 0.66988
## Reputacion 0.171394 0.054512 3.144 0.00193 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2349 on 194 degrees of freedom
## Multiple R-squared: 0.9634, Adjusted R-squared: 0.9624
## F-statistic: 1021 on 5 and 194 DF, p-value: < 0.00000000000000022
La primera señal de multicolinealidad es una correlación bilateral alta (|r| > 0.80) entre pares de regresores.
regresores <- muestra |>
select(Puntuacion, Edad, lnSalario, Potencial, Reputacion)
mat_cor <- cor(regresores, use = "complete.obs")
mat_cor |>
round(4) |>
kable(caption = "Matriz de Correlaciones entre Regresores", align = "c") |>
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Puntuacion | Edad | lnSalario | Potencial | Reputacion | |
|---|---|---|---|---|---|
| Puntuacion | 1.0000 | 0.4463 | 0.8209 | 0.5780 | 0.4946 |
| Edad | 0.4463 | 1.0000 | 0.2841 | -0.3396 | 0.2205 |
| lnSalario | 0.8209 | 0.2841 | 1.0000 | 0.5318 | 0.4798 |
| Potencial | 0.5780 | -0.3396 | 0.5318 | 1.0000 | 0.3720 |
| Reputacion | 0.4946 | 0.2205 | 0.4798 | 0.3720 | 1.0000 |
corrplot(
mat_cor,
method = "color",
type = "lower",
addCoef.col = "black",
number.cex = 0.85,
tl.col = "black",
tl.srt = 45,
col = colorRampPalette(c("#E74C3C", "white", "#2980B9"))(200),
title = "Correlaciones entre Variables Explicativas",
mar = c(0, 0, 1.5, 0)
)Figura 1. Mapa de calor de correlaciones entre regresores
Interpretación: Correlaciones superiores a |0.80| entre Puntuación y Potencial son señal de posible multicolinealidad. Se procede con pruebas formales.
El VIF mide cuánto se infla la varianza de cada coeficiente por la correlación con los demás regresores:
vif_vals <- vif(modelo)
vif_df <- data.frame(
Variable = names(vif_vals),
VIF = round(vif_vals, 4),
Diagnostico = case_when(
vif_vals < 5 ~ "Sin problema",
vif_vals < 10 ~ "Moderada",
TRUE ~ "Grave"
)
)
vif_df |>
kable(caption = "Factor de Inflación de la Varianza (VIF)", row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) |>
row_spec(which(vif_df$VIF >= 10), background = "#FADBD8") |>
row_spec(which(vif_df$VIF >= 5 & vif_df$VIF < 10), background = "#FEF9E7") |>
row_spec(which(vif_df$VIF < 5), background = "#EAFAF1") |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Variable | VIF | Diagnostico |
|---|---|---|
| Puntuacion | 7.4883 | Moderada |
| Edad | 3.9673 | Sin problema |
| lnSalario | 3.2285 | Sin problema |
| Potencial | 4.7684 | Sin problema |
| Reputacion | 1.4207 | Sin problema |
ggplot(vif_df, aes(x = reorder(Variable, VIF), y = VIF, fill = VIF)) +
geom_col(width = 0.6, color = "white") +
geom_hline(yintercept = 5, linetype = "dashed", color = "#E67E22", linewidth = 1) +
geom_hline(yintercept = 10, linetype = "dashed", color = "#E74C3C", linewidth = 1) +
geom_text(aes(label = round(VIF, 2)), hjust = -0.2, fontface = "bold", size = 4) +
scale_fill_gradient(low = "#AED6F1", high = "#C0392B") +
coord_flip(ylim = c(0, max(vif_df$VIF) * 1.2)) +
annotate("text", x = 0.6, y = 5.2, label = "VIF = 5 (moderada)", color = "#E67E22", size = 3.5, hjust = 0) +
annotate("text", x = 0.6, y = 10.2, label = "VIF = 10 (grave)", color = "#E74C3C", size = 3.5, hjust = 0) +
labs(title = "Factor de Inflación de la Varianza (VIF)",
subtitle = "Modelo: lnValor ~ Puntuación + Edad + lnSalario + Potencial + Reputación",
x = "Regresor", y = "VIF") +
theme_minimal(base_size = 13) +
theme(legend.position = "none",
plot.title = element_text(face = "bold", hjust = 0.5),
plot.subtitle = element_text(hjust = 0.5, color = "grey40"))Figura 2. VIF por regresor
La Tolerancia es el inverso del VIF: TOL = 1 /
VIF.
- TOL < 0.10 indica multicolinealidad severa
- TOL < 0.20 señala problema moderado
tol_df <- data.frame(
Variable = names(vif_vals),
VIF = round(vif_vals, 4),
Tolerancia = round(1 / vif_vals, 4),
Diagnostico = case_when(
1/vif_vals > 0.20 ~ "Sin problema",
1/vif_vals > 0.10 ~ " Moderada",
TRUE ~ "Grave"
)
)
tol_df |>
kable(caption = "Tolerancia y VIF — Diagnóstico conjunto", row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) |>
row_spec(which(tol_df$Tolerancia < 0.10), background = "#FADBD8") |>
row_spec(which(tol_df$Tolerancia >= 0.10 & tol_df$Tolerancia < 0.20), background = "#FEF9E7") |>
row_spec(which(tol_df$Tolerancia >= 0.20), background = "#EAFAF1") |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Variable | VIF | Tolerancia | Diagnostico |
|---|---|---|---|
| Puntuacion | 7.4883 | 0.1335 | Moderada |
| Edad | 3.9673 | 0.2521 | Sin problema |
| lnSalario | 3.2285 | 0.3097 | Sin problema |
| Potencial | 4.7684 | 0.2097 | Sin problema |
| Reputacion | 1.4207 | 0.7039 | Sin problema |
X <- scale(regresores)
eigenvalues <- eigen(t(X) %*% X)$values
kappa_val <- sqrt(max(eigenvalues) / min(eigenvalues))
indices_cond <- sqrt(max(eigenvalues) / eigenvalues)
cond_df <- data.frame(
`Valor Propio` = round(eigenvalues, 6),
`Indice Condicion` = round(indices_cond, 4),
Diagnostico = case_when(
indices_cond < 10 ~ "Sin problema",
indices_cond < 30 ~ " Moderada",
TRUE ~ "Grave"
)
)
cat("Número de Condición Global (κ):", round(kappa_val, 4), "\n")## Número de Condición Global (κ): 6.2076
cat("Diagnóstico:",
ifelse(kappa_val < 10, "Sin multicolinealidad",
ifelse(kappa_val < 30, "Multicolinealidad moderada",
"Multicolinealidad severa")), "\n")## Diagnóstico: Sin multicolinealidad
cond_df |>
kable(caption = "Valores Propios e Índices de Condición de la Matriz X'X",
row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) |>
row_spec(which(cond_df$Diagnostico == "Grave"), background = "#FADBD8") |>
row_spec(which(cond_df$Diagnostico == " Moderada"), background = "#FEF9E7") |>
row_spec(which(cond_df$Diagnostico == "Sin problema"), background = "#EAFAF1") |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Valor.Propio | Indice.Condicion | Diagnostico |
|---|---|---|
| 544.57200 | 1.0000 | Sin problema |
| 263.62375 | 1.4373 | Sin problema |
| 123.69051 | 2.0983 | Sin problema |
| 48.98156 | 3.3344 | Sin problema |
| 14.13218 | 6.2076 | Sin problema |
Existe multicolinealidad problemática si el R² de una regresión auxiliar supera el R² global del modelo original.
R2_global <- summary(modelo)$r.squared
vars_exog <- c("Puntuacion","Edad","lnSalario","Potencial","Reputacion")
r2_aux <- sapply(vars_exog, function(y_var) {
summary(lm(as.formula(
paste(y_var, "~", paste(setdiff(vars_exog, y_var), collapse = " + "))
), data = muestra))$r.squared
})
klein_df <- data.frame(
Variable = vars_exog,
R2_Auxiliar = round(r2_aux, 4),
R2_Global = round(R2_global, 4),
Klein = ifelse(r2_aux > R2_global,
" R²aux > R²global → Multicolinealidad",
"R²aux ≤ R²global → Sin problema")
)
cat(sprintf("R² del modelo global: %.4f\n\n", R2_global))## R² del modelo global: 0.9634
klein_df |>
kable(caption = "Regla de Klein — R² Auxiliares vs R² Global",
row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) |>
row_spec(which(r2_aux > R2_global), background = "#FEF9E7") |>
row_spec(which(r2_aux <= R2_global), background = "#EAFAF1") |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Variable | R2_Auxiliar | R2_Global | Klein |
|---|---|---|---|
| Puntuacion | 0.8665 | 0.9634 | R²aux ≤ R²global → Sin problema |
| Edad | 0.7479 | 0.9634 | R²aux ≤ R²global → Sin problema |
| lnSalario | 0.6903 | 0.9634 | R²aux ≤ R²global → Sin problema |
| Potencial | 0.7903 | 0.9634 | R²aux ≤ R²global → Sin problema |
| Reputacion | 0.2961 | 0.9634 | R²aux ≤ R²global → Sin problema |
ggpairs(
regresores,
upper = list(continuous = wrap("cor", size = 4, color = "#2C3E50")),
lower = list(continuous = wrap("smooth", alpha = 0.3, color = "#2980B9", se = FALSE)),
diag = list(continuous = wrap("densityDiag", fill = "#AED6F1", alpha = 0.7)),
title = "Relaciones entre Variables Explicativas del Modelo"
) +
theme_minimal(base_size = 11) +
theme(plot.title = element_text(face = "bold", hjust = 0.5))Figura 3. Matriz de dispersión entre regresores
resumen <- data.frame(
Prueba = c(
"Correlación bilateral",
"VIF (Variable Inflation Factor)",
"Tolerancia (TOL = 1/VIF)",
"Número de Condición (κ)",
"Regla de Klein (R² auxiliares)"
),
`Umbral Crítico` = c(
"|r| > 0.80",
"VIF ≥ 10 (grave), ≥ 5 (moderado)",
"TOL < 0.10 (grave), < 0.20 (moderado)",
"κ ≥ 30 (grave), ≥ 10 (moderado)",
"R²aux > R²global"
),
`Resultado Obtenido` = c(
paste0("Máx. |r| = ", round(max(abs(mat_cor[lower.tri(mat_cor)])), 3)),
paste0("Máx. VIF = ", round(max(vif_vals), 3)),
paste0("Mín. TOL = ", round(min(1/vif_vals), 4)),
paste0("κ = ", round(kappa_val, 3)),
paste0(sum(r2_aux > R2_global), " variable(s) con R²aux > R²global")
),
Diagnostico = c(
ifelse(max(abs(mat_cor[lower.tri(mat_cor)])) > 0.80, " Correlación alta", " Aceptable"),
ifelse(max(vif_vals) >= 10, " Grave", ifelse(max(vif_vals) >= 5, " Moderada", " Sin problema")),
ifelse(min(1/vif_vals) < 0.10, " Grave", ifelse(min(1/vif_vals) < 0.20, " Moderada", " Sin problema")),
ifelse(kappa_val >= 30, " Grave", ifelse(kappa_val >= 10, " Moderada", " Sin problema")),
ifelse(any(r2_aux > R2_global), " Posible multicolinealidad", " Sin problema")
)
)
resumen |>
kable(caption = "Tabla Resumen: Diagnóstico de Multicolinealidad",
col.names = c("Prueba","Umbral Crítico","Resultado","Diagnóstico"), align = "l") |>
kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = TRUE) |>
column_spec(4, bold = TRUE) |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Prueba | Umbral Crítico | Resultado | Diagnóstico |
|---|---|---|---|
| Correlación bilateral | |r| > 0.80 | Máx. |r| = 0.821 | Correlación alta |
| VIF (Variable Inflation Factor) | VIF ≥ 10 (grave), ≥ 5 (moderado) | Máx. VIF = 7.488 | Moderada |
| Tolerancia (TOL = 1/VIF) | TOL < 0.10 (grave), < 0.20 (moderado) | Mín. TOL = 0.1335 | Moderada |
| Número de Condición (κ) | κ ≥ 30 (grave), ≥ 10 (moderado) | κ = 6.208 | Sin problema |
| Regla de Klein (R² auxiliares) | R²aux > R²global | 0 variable(s) con R²aux > R²global | Sin problema |
Con base en los resultados de las cinco pruebas aplicadas:
Matriz de correlaciones: El par de regresores con mayor correlación bilateral presenta un coeficiente de 0.821, lo que supera el umbral de |r| > 0.80, sugiriendo correlación elevada.
VIF: El valor máximo obtenido fue de 7.49. Esto señala una multicolinealidad moderada que merece atención.
Número de condición: κ = 6.21, lo que indica ausencia de multicolinealidad sistémica.
Uno de los supuestos del MCRLM es que el término de error sigue una distribución normal: \(\varepsilon_i \sim N(0, \sigma^2)\). Este supuesto es necesario para que las pruebas t y F de hipótesis sean válidas en muestras pequeñas. Se aplican tres pruebas sobre los residuos del modelo estimado.
residuos <- residuals(modelo)
par(mfrow = c(1, 2))
hist(residuos, breaks = 20, col = "steelblue", border = "white",
main = "Histograma de Residuos", xlab = "Residuos")
curve(dnorm(x, mean(residuos), sd(residuos)) * length(residuos) * diff(range(residuos))/20,
add = TRUE, col = "red", lwd = 2)
qqnorm(residuos, main = "Q-Q Plot de Residuos")
qqline(residuos, col = "red", lwd = 2)\[JB = \frac{n}{6}\left(S^2 + \frac{(K-3)^2}{4}\right)\]
donde \(S\) = asimetría y \(K\) = curtosis de los residuos. \(H_0\): los residuos son normales.
##
## Jarque Bera Test
##
## data: residuos
## X-squared = 252.84, df = 2, p-value < 0.00000000000000022
data.frame(
Estadístico = round(jb_test$statistic, 4),
`p-valor` = round(jb_test$p.value, 6),
Conclusión = ifelse(jb_test$p.value < 0.05,
"Se rechaza H₀ → No normalidad",
"No se rechaza H₀ → Normalidad")
) |>
kable(caption = "Prueba de Jarque-Bera", row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)| Estadístico | p.valor | Conclusión |
|---|---|---|
| 252.8399 | 0 | Se rechaza H₀ → No normalidad |
Compara la distribución empírica de los residuos con una distribución normal teórica con la misma media y desviación estándar. \(H_0\): los residuos siguen una distribución normal.
options(scipen = 999)
ks_test <- ks.test(residuos, "pnorm",
mean = mean(residuos),
sd = sd(residuos))
print(ks_test)##
## Asymptotic one-sample Kolmogorov-Smirnov test
##
## data: residuos
## D = 0.1174, p-value = 0.008064
## alternative hypothesis: two-sided
data.frame(
Estadístico = round(ks_test$statistic, 4),
`p-valor` = round(ks_test$p.value, 6),
Conclusión = ifelse(ks_test$p.value < 0.05,
"Se rechaza H₀ → No normalidad",
"No se rechaza H₀ → Normalidad")
) |>
kable(caption = "Prueba de Kolmogorov-Smirnov", row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)| Estadístico | p.valor | Conclusión |
|---|---|---|
| 0.1174 | 0.008064 | Se rechaza H₀ → No normalidad |
La más potente para muestras pequeñas y medianas (n ≤ 5000). \(H_0\): los residuos provienen de una distribución normal.
##
## Shapiro-Wilk normality test
##
## data: residuos
## W = 0.90535, p-value = 0.0000000005579
data.frame(
Estadístico = round(sw_test$statistic, 4),
`p-valor` = round(sw_test$p.value, 6),
Conclusión = ifelse(sw_test$p.value < 0.05,
"Se rechaza H₀ → No normalidad",
"No se rechaza H₀ → Normalidad")
) |>
kable(caption = "Prueba de Shapiro-Wilk", row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)| Estadístico | p.valor | Conclusión |
|---|---|---|
| 0.9053 | 0 | Se rechaza H₀ → No normalidad |
resumen_norm <- data.frame(
Prueba = c("Jarque-Bera", "Kolmogorov-Smirnov", "Shapiro-Wilk"),
Estadístico = round(c(jb_test$statistic, ks_test$statistic, sw_test$statistic), 4),
`p-valor` = round(c(jb_test$p.value, ks_test$p.value, sw_test$p.value), 6),
Decisión = ifelse(c(jb_test$p.value, ks_test$p.value, sw_test$p.value) < 0.05,
" Rechaza H₀", "No rechaza H₀")
)
resumen_norm |>
kable(caption = "Resumen Pruebas de Normalidad (α = 0.05)",
row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE) |>
row_spec(which(c(jb_test$p.value, ks_test$p.value, sw_test$p.value) < 0.05),
background = "#FADBD8") |>
row_spec(which(c(jb_test$p.value, ks_test$p.value, sw_test$p.value) >= 0.05),
background = "#EAFAF1") |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Prueba | Estadístico | p.valor | Decisión |
|---|---|---|---|
| Jarque-Bera | 252.8399 | 0.000000 | Rechaza H₀ |
| Kolmogorov-Smirnov | 0.1174 | 0.008064 | Rechaza H₀ |
| Shapiro-Wilk | 0.9053 | 0.000000 | Rechaza H₀ |
Las tres pruebas rechazan la hipótesis nula de normalidad al 5%. Los residuos presentan una distribución no normal, lo que puede afectar la validez de las pruebas de hipótesis individuales. Como medida correctiva, en la Parte 3 se construirá una matriz de covarianzas robusta mediante estimadores HAC, que son válidos sin requerir el supuesto de normalidad.
La homocedasticidad exige que la varianza del error sea constante para todas las observaciones: \(Var(\varepsilon_i) = \sigma^2\). Cuando esto no se cumple (heterocedasticidad), los estimadores MCO siguen siendo insesgados pero dejan de ser eficientes, y los errores estándar son incorrectos, invalidando las pruebas t y F.
La prueba de White (1980) es más general que Breusch-Pagan: incluye los cuadrados y productos cruzados de los regresores como variables auxiliares, sin asumir una forma específica de heterocedasticidad.
\(H_0\): homocedasticidad (varianza
constante)
\(H_1\): heterocedasticidad
white_test <- bptest(modelo,
varformula = ~ Puntuacion + Edad + lnSalario + Potencial + Reputacion +
I(Puntuacion^2) + I(Edad^2) + I(lnSalario^2) +
I(Potencial^2) + I(Reputacion^2) +
Puntuacion:Edad + Puntuacion:lnSalario +
Puntuacion:Potencial + Puntuacion:Reputacion +
Edad:lnSalario + Edad:Potencial + Edad:Reputacion +
lnSalario:Potencial + lnSalario:Reputacion +
Potencial:Reputacion,
data = muestra)
print(white_test)##
## studentized Breusch-Pagan test
##
## data: modelo
## BP = 133.7, df = 20, p-value < 0.00000000000000022
data.frame(
`Estadístico BP` = round(white_test$statistic, 4),
`Grados de libertad` = white_test$parameter,
`p-valor` = round(white_test$p.value, 6),
Conclusión = ifelse(white_test$p.value < 0.05,
"Rechaza H₀ → Heterocedasticidad",
"No rechaza H₀ → Homocedasticidad")
) |>
kable(caption = "Prueba de Heterocedasticidad de White", row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) |>
row_spec(1,
background = ifelse(white_test$p.value < 0.05, "#FADBD8", "#EAFAF1")) |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Estadístico.BP | Grados.de.libertad | p.valor | Conclusión |
|---|---|---|---|
| 133.7018 | 20 | 0 | Rechaza H₀ → Heterocedasticidad |
par(mfrow = c(1, 2))
plot(fitted(modelo), residuals(modelo),
main = "Residuos vs Valores Ajustados",
xlab = "Valores ajustados (ŷ)", ylab = "Residuos",
col = "steelblue", pch = 16, cex = 0.7)
abline(h = 0, col = "red", lwd = 2, lty = 2)
plot(fitted(modelo), sqrt(abs(residuals(modelo))),
main = "Scale-Location",
xlab = "Valores ajustados (ŷ)", ylab = "√|Residuos|",
col = "darkorange", pch = 16, cex = 0.7)
abline(lm(sqrt(abs(residuals(modelo))) ~ fitted(modelo)),
col = "red", lwd = 2)La prueba de White reporta un estadístico BP = 133.702 con p-valor = 0. Al ser p < 0.05, se rechaza H₀ de homocedasticidad. Existe evidencia de heterocedasticidad en los residuos del modelo, lo que implica que la varianza del error no es constante a lo largo de las observaciones. Esta violación no invalida la insesgadez de los estimadores MCO, pero sí hace que los errores estándar sean incorrectos. Como medida correctiva, en la Parte 3 se estimará el modelo con errores estándar robustos (HAC) que son válidos en presencia de heterocedasticidad y/o autocorrelación.
La autocorrelación implica que los errores del modelo están correlacionados entre sí: \(Cov(\varepsilon_i, \varepsilon_j) \neq 0\) para \(i \neq j\). Aunque es más común en series de tiempo, puede aparecer en corte transversal cuando las observaciones tienen algún orden natural (ranking, posiciones). Al igual que la heterocedasticidad, no invalida la insesgadez de MCO pero sí la eficiencia y la validez de los errores estándar.
Se aplican dos pruebas:
\[DW = \frac{\sum_{t=2}^{n}(\hat{\varepsilon}_t - \hat{\varepsilon}_{t-1})^2}{\sum_{t=1}^{n}\hat{\varepsilon}_t^2}\]
El estadístico DW toma valores entre 0 y 4:
- DW ≈ 2: no hay autocorrelación
- DW < 1.5: autocorrelación positiva
- DW > 2.5: autocorrelación negativa
\(H_0\): no hay autocorrelación de primer orden
##
## Durbin-Watson test
##
## data: modelo
## DW = 2.0121, p-value = 0.5386
## alternative hypothesis: true autocorrelation is greater than 0
data.frame(
`Estadístico DW` = round(dw_test$statistic, 4),
`p-valor` = round(dw_test$p.value, 6),
Diagnóstico = case_when(
dw_test$statistic < 1.5 ~ "Autocorrelación positiva",
dw_test$statistic > 2.5 ~ "Autocorrelación negativa",
TRUE ~ "Sin autocorrelación aparente"
),
Conclusión = ifelse(dw_test$p.value < 0.05,
"Se rechaza H₀ → Autocorrelación AR(1)",
"No rechaza H₀ → Sin autocorrelación AR(1)")
) |>
kable(caption = "Prueba de Durbin-Watson", row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) |>
row_spec(1,
background = ifelse(dw_test$p.value < 0.05, "#FADBD8", "#EAFAF1")) |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Estadístico.DW | p.valor | Diagnóstico | Conclusión |
|---|---|---|---|
| 2.0121 | 0.538627 | Sin autocorrelación aparente | No rechaza H₀ → Sin autocorrelación AR(1) |
##
## Breusch-Godfrey test for serial correlation of order up to 1
##
## data: modelo
## LM test = 0.026182, df = 1, p-value = 0.8715
data.frame(
`Estadístico LM` = round(bg1$statistic, 4),
`Grados libertad` = bg1$parameter,
`p-valor` = round(bg1$p.value, 6),
Conclusión = ifelse(bg1$p.value < 0.05,
" Rechaza H₀ → Autocorrelación orden 1",
" No rechaza H₀ → Sin autocorrelación orden 1")
) |>
kable(caption = "Prueba BG — Multiplicador de Lagrange (Orden 1)",
row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) |>
row_spec(1,
background = ifelse(bg1$p.value < 0.05, "#FADBD8", "#EAFAF1")) |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Estadístico.LM | Grados.libertad | p.valor | Conclusión |
|---|---|---|---|
| 0.0262 | 1 | 0.871458 | No rechaza H₀ → Sin autocorrelación orden 1 |
##
## Breusch-Godfrey test for serial correlation of order up to 2
##
## data: modelo
## LM test = 2.6952, df = 2, p-value = 0.2599
data.frame(
`Estadístico LM` = round(bg2$statistic, 4),
`Grados libertad` = bg2$parameter,
`p-valor` = round(bg2$p.value, 6),
Conclusión = ifelse(bg2$p.value < 0.05,
" Rechaza H₀ → Autocorrelación orden 2",
" No rechaza H₀ → Sin autocorrelación orden 2")
) |>
kable(caption = "Prueba BG — Multiplicador de Lagrange (Orden 2)",
row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) |>
row_spec(1,
background = ifelse(bg2$p.value < 0.05, "#FADBD8", "#EAFAF1")) |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Estadístico.LM | Grados.libertad | p.valor | Conclusión |
|---|---|---|---|
| 2.6952 | 2 | 0.259866 | No rechaza H₀ → Sin autocorrelación orden 2 |
par(mfrow = c(1, 2))
plot(residuos, type = "l", col = "steelblue", lwd = 1.2,
main = "Residuos por orden de observación",
xlab = "Observación", ylab = "Residuo")
abline(h = 0, col = "red", lty = 2, lwd = 1.5)
acf(residuos, main = "ACF de Residuos", col = "steelblue", lwd = 2)resumen_ac <- data.frame(
Prueba = c("Durbin-Watson (AR1)",
"Breusch-Godfrey orden 1",
"Breusch-Godfrey orden 2"),
Estadístico = round(c(dw_test$statistic, bg1$statistic, bg2$statistic), 4),
`p-valor` = round(c(dw_test$p.value, bg1$p.value, bg2$p.value), 6),
Decisión = ifelse(c(dw_test$p.value, bg1$p.value, bg2$p.value) < 0.05,
" Rechaza H₀", " No rechaza H₀")
)
resumen_ac |>
kable(caption = "Resumen Pruebas de Autocorrelación (α = 0.05)",
row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover","condensed"), full_width = FALSE) |>
row_spec(which(c(dw_test$p.value, bg1$p.value, bg2$p.value) < 0.05),
background = "#FADBD8") |>
row_spec(which(c(dw_test$p.value, bg1$p.value, bg2$p.value) >= 0.05),
background = "#EAFAF1") |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Prueba | Estadístico | p.valor | Decisión |
|---|---|---|---|
| Durbin-Watson (AR1) | 2.0121 | 0.538627 | No rechaza H₀ |
| Breusch-Godfrey orden 1 | 0.0262 | 0.871458 | No rechaza H₀ |
| Breusch-Godfrey orden 2 | 2.6952 | 0.259866 | No rechaza H₀ |
Las tres pruebas de autocorrelación no rechazan H₀ al nivel del 5%. No existe evidencia de estructuras de autocorrelación de primer ni segundo orden en los residuos del modelo. El supuesto de no autocorrelación del MCRLM se cumple, lo que valida la eficiencia de los estimadores MCO y la correcta construcción de los errores estándar.
Con base en los resultados de la Parte 2, se identificaron posibles violaciones a los supuestos de homocedasticidad y no autocorrelación del MCRLM. Los errores estándar MCO convencionales son inconsistentes bajo estas condiciones, invalidando las pruebas t y F. La solución adoptada es estimar una matriz de covarianzas robusta HAC (Heteroskedasticity and Autocorrelation Consistent) mediante el estimador de Newey-West (1987), que corrige ambos problemas simultáneamente sin necesidad de especificar la forma exacta de ninguna de las dos violaciones.
options(scipen = 999)
hac_vcov <- vcovHAC(modelo)
modelo_hac <- coeftest(modelo, vcov = hac_vcov)
print(modelo_hac)##
## t test of coefficients:
##
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 4.2274578 0.4694040 9.0060 < 0.00000000000000022 ***
## Puntuacion 0.1905438 0.0104644 18.2087 < 0.00000000000000022 ***
## Edad -0.1298975 0.0160881 -8.0741 0.00000000000007021 ***
## lnSalario 0.0479639 0.0218808 2.1921 0.029565 *
## Potencial -0.0026185 0.0100428 -0.2607 0.794578
## Reputacion 0.1713938 0.0476449 3.5973 0.000408 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
options(scipen = 999)
mco_sum <- summary(modelo)$coefficients
comp_df <- data.frame(
Variable = rownames(mco_sum),
Coef = round(mco_sum[, 1], 5),
SE_MCO = round(mco_sum[, 2], 5),
p_MCO = round(mco_sum[, 4], 5),
SE_HAC = round(modelo_hac[, 2], 5),
p_HAC = round(modelo_hac[, 4], 5),
Sig_MCO = ifelse(mco_sum[, 4] < 0.01, "***",
ifelse(mco_sum[, 4] < 0.05, "**",
ifelse(mco_sum[, 4] < 0.10, "*", ""))),
Sig_HAC = ifelse(modelo_hac[, 4] < 0.01, "***",
ifelse(modelo_hac[, 4] < 0.05, "**",
ifelse(modelo_hac[, 4] < 0.10, "*", "")))
)
colnames(comp_df) <- c("Variable", "Coeficiente",
"SE MCO", "p-val MCO",
"SE HAC", "p-val HAC",
"Sig. MCO", "Sig. HAC")
comp_df |>
kable(caption = "Comparacion: Errores Estandar MCO vs HAC (Newey-West)",
row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = TRUE) |>
column_spec(4, bold = TRUE,
color = ifelse(mco_sum[,4] < 0.05, "#27AE60", "#E74C3C")) |>
column_spec(6, bold = TRUE,
color = ifelse(modelo_hac[,4] < 0.05, "#27AE60", "#E74C3C")) |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Variable | Coeficiente | SE MCO | p-val MCO | SE HAC | p-val HAC | Sig. MCO | Sig. HAC |
|---|---|---|---|---|---|---|---|
| (Intercept) | 4.22746 | 0.31708 | 0.00000 | 0.46940 | 0.00000 | *** | *** |
| Puntuacion | 0.19054 | 0.00691 | 0.00000 | 0.01046 | 0.00000 | *** | *** |
| Edad | -0.12990 | 0.00737 | 0.00000 | 0.01609 | 0.00000 | *** | *** |
| lnSalario | 0.04796 | 0.02311 | 0.03927 | 0.02188 | 0.02957 | ** | ** |
| Potencial | -0.00262 | 0.00613 | 0.66988 | 0.01004 | 0.79458 | ||
| Reputacion | 0.17139 | 0.05451 | 0.00193 | 0.04764 | 0.00041 | *** | *** |
La corrección HAC modifica los errores estándar de forma relevante (> 10%) en: (Intercept), Puntuacion, Edad, Potencial, Reputacion. Esto confirma que las violaciones detectadas en la Parte 2 afectan la precisión de la inferencia MCO convencional. Los resultados con errores HAC son los válidos para la interpretación final del modelo.
Para cada parámetro \(\beta_k\) se contrasta: \[H_0: \beta_k = 0 \quad \text{vs} \quad H_1: \beta_k \neq 0\]
options(scipen = 999)
hip_df <- data.frame(
Parámetro = rownames(modelo_hac),
Coeficiente = round(modelo_hac[, 1], 5),
SE_HAC = round(modelo_hac[, 2], 5),
t_stat = round(modelo_hac[, 3], 4),
p_valor = round(modelo_hac[, 4], 6),
Decisión = ifelse(modelo_hac[, 4] < 0.01, "*** Sig. al 1%",
ifelse(modelo_hac[, 4] < 0.05, "** Sig. al 5%",
ifelse(modelo_hac[, 4] < 0.10, "* Sig. al 10%",
" No significativo"))),
Signo_esp = c("N/A", "+", "+/-", "+", "+", "+")
)
colnames(hip_df) <- c("Parametro", "Coeficiente", "Error Std. HAC",
"t-stat", "p-valor", "Decision", "Signo esp.")
hip_df |>
kable(caption = "Pruebas de Hipotesis Individuales con Errores HAC (a = 0.05)",
row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover"), full_width = TRUE) |>
row_spec(which(modelo_hac[, 4] < 0.05), background = "#EAFAF1") |>
row_spec(which(modelo_hac[, 4] >= 0.05), background = "#FADBD8") |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Parametro | Coeficiente | Error Std. HAC | t-stat | p-valor | Decision | Signo esp. |
|---|---|---|---|---|---|---|
| (Intercept) | 4.22746 | 0.46940 | 9.0060 | 0.000000 | *** Sig. al 1% | N/A |
| Puntuacion | 0.19054 | 0.01046 | 18.2087 | 0.000000 | *** Sig. al 1% |
|
| Edad | -0.12990 | 0.01609 | -8.0741 | 0.000000 | *** Sig. al 1% | +/- |
| lnSalario | 0.04796 | 0.02188 | 2.1921 | 0.029565 | ** Sig. al 5% |
|
| Potencial | -0.00262 | 0.01004 | -0.2607 | 0.794578 | No significativo |
|
| Reputacion | 0.17139 | 0.04764 | 3.5973 | 0.000408 | *** Sig. al 1% |
|
\[H_0: \beta_1 = \beta_2 = \beta_3 = \beta_4 = \beta_5 = 0\]
options(scipen = 999)
f_stat <- summary(modelo)$fstatistic
f_valor <- f_stat[1]
f_df1 <- f_stat[2]
f_df2 <- f_stat[3]
f_pvalue <- pf(f_valor, f_df1, f_df2, lower.tail = FALSE)
r2 <- summary(modelo)$r.squared
r2_adj <- summary(modelo)$adj.r.squared
data.frame(
Metrica = c("R2", "R2 ajustado", "Estadistico F",
"Grados de libertad", "p-valor F"),
Valor = c(round(r2, 4), round(r2_adj, 4), round(f_valor, 4),
paste0(f_df1, " y ", f_df2),
round(f_pvalue, 8))
) |>
kable(caption = "Prueba Global de Significancia del Modelo",
row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Metrica | Valor |
|---|---|
| R2 | 0.9634 |
| R2 ajustado | 0.9624 |
| Estadistico F | 1021.0233 |
| Grados de libertad | 5 y 194 |
| p-valor F | 0 |
Con F = 1021.023 y p-valor = 0, se rechaza H₀ al 1% de significancia. El modelo en conjunto es estadísticamente significativo. El R² = 0.9634 indica que el modelo explica el 96.3% de la variabilidad en ln(Valor de mercado). El R² ajustado = 0.9624 confirma el buen ajuste penalizando el número de regresores.
El modelo está en forma semi-logarítmica (ln(Valor) como variable dependiente), por lo que cada coeficiente representa la variación porcentual aproximada en el valor de mercado ante un cambio unitario en el regresor, ceteris paribus:
Puntuacion (OVA): Un punto adicional en la calificacion global se asocia con un aumento aproximado del 19.05% en el valor de mercado (significativo al 5%).
Edad: Un año adicional de edad se asocia con un cambio del -12.99% en el valor (significativo al 5%).
ln(Salario): Un incremento del 1% en el salario semanal se asocia con un 4.8% de cambio en el valor de mercado (significativo al 5%).
Potencial (POT): Un punto adicional en el potencial se asocia con un -0.26% de cambio en el valor (no significativo al 5%).
Reputación (IR): Un nivel adicional de reputación internacional se asocia con un 17.14% de cambio en el valor (significativo al 5%).
options(scipen = 999)
ic_mco <- confint(modelo, level = 0.95)
ic_hac <- coefci(modelo, vcov = hac_vcov, level = 0.95)
ic_df <- data.frame(
Parametro = rownames(ic_mco),
Coeficiente = round(coef(modelo), 5),
IC_MCO_inf = round(ic_mco[, 1], 5),
IC_MCO_sup = round(ic_mco[, 2], 5),
IC_HAC_inf = round(ic_hac[, 1], 5),
IC_HAC_sup = round(ic_hac[, 2], 5)
)
colnames(ic_df) <- c("Parametro", "Coeficiente",
"IC MCO inf (2.5%)", "IC MCO sup (97.5%)",
"IC HAC inf (2.5%)", "IC HAC sup (97.5%)")
ic_df |>
kable(caption = "Intervalos de Confianza al 95% - MCO Estandar vs HAC",
row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = TRUE) |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Parametro | Coeficiente | IC MCO inf (2.5%) | IC MCO sup (97.5%) | IC HAC inf (2.5%) | IC HAC sup (97.5%) |
|---|---|---|---|---|---|
| (Intercept) | 4.22746 | 3.60210 | 4.85281 | 3.30167 | 5.15325 |
| Puntuacion | 0.19054 | 0.17692 | 0.20417 | 0.16991 | 0.21118 |
| Edad | -0.12990 | -0.14444 | -0.11536 | -0.16163 | -0.09817 |
| lnSalario | 0.04796 | 0.00238 | 0.09355 | 0.00481 | 0.09112 |
| Potencial | -0.00262 | -0.01471 | 0.00948 | -0.02243 | 0.01719 |
| Reputacion | 0.17139 | 0.06388 | 0.27891 | 0.07743 | 0.26536 |
vars_plot <- rownames(ic_hac)[-1]
ic_plot_df <- data.frame(
Variable = rep(vars_plot, 2),
Tipo = rep(c("MCO estandar", "HAC Newey-West"), each = length(vars_plot)),
Coef = rep(coef(modelo)[-1], 2),
LI = c(ic_mco[-1, 1], ic_hac[-1, 1]),
LS = c(ic_mco[-1, 2], ic_hac[-1, 2])
)
ggplot(ic_plot_df, aes(x = Variable, y = Coef, color = Tipo, shape = Tipo)) +
geom_point(position = position_dodge(0.5), size = 3.5) +
geom_errorbar(aes(ymin = LI, ymax = LS),
width = 0.25, linewidth = 0.9,
position = position_dodge(0.5)) +
geom_hline(yintercept = 0, linetype = "dashed", color = "grey50") +
scale_color_manual(values = c("steelblue", "#E74C3C")) +
labs(title = "IC al 95% por Parametro",
subtitle = "MCO estandar vs HAC",
x = "Regresor", y = "Coeficiente",
color = "Metodo", shape = "Metodo") +
theme_minimal(base_size = 12) +
theme(legend.position = "top")Un IC que no cruza el cero indica que el parámetro es significativamente distinto de cero al 95% de confianza.
Con errores HAC, los parámetros de Puntuacion, Edad, lnSalario, Reputacion tienen IC que no cruzan el cero: son estadísticamente significativos.
Los parámetros de Potencial tienen IC que cruzan el cero con la corrección HAC: no son significativamente distintos de cero al 5%.
data.frame(
Medida = c("ECM - Error Cuadratico Medio",
"RECM - Raiz del ECM",
"EAM - Error Absoluto Medio",
"EPM - Error Porcentual Medio",
"MAPE - Error Porcentual Abs. Medio"),
Formula = c("mean(e^2)",
"sqrt(mean(e^2))",
"mean(|e|)",
"mean(e/Y) x 100",
"mean(|e/Y|) x 100"),
Interpretacion = c("Penaliza errores grandes",
"En las mismas unidades que Y",
"Error promedio en valor absoluto",
"Sesgo direccional del modelo",
"Error relativo promedio")
) |>
kable(caption = "Medidas de desempeno predictivo evaluadas en la simulacion",
row.names = FALSE, align = "l") |>
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) |>
column_spec(1, bold = TRUE) |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Medida | Formula | Interpretacion |
|---|---|---|
| ECM - Error Cuadratico Medio | mean(e^2) | Penaliza errores grandes |
| RECM - Raiz del ECM | sqrt(mean(e^2)) | En las mismas unidades que Y |
| EAM - Error Absoluto Medio | mean(|e|) | Error promedio en valor absoluto |
| EPM - Error Porcentual Medio | mean(e/Y) x 100 | Sesgo direccional del modelo |
| MAPE - Error Porcentual Abs. Medio | mean(|e/Y|) x 100 | Error relativo promedio |
En cada una de las 5,000 iteraciones se divide la muestra aleatoriamente en 80% entrenamiento / 20% prueba, se estima el modelo con los datos de entrenamiento y se calculan las cinco medidas sobre el conjunto de prueba.
set.seed(42)
n_iter <- 5000
n_obs <- nrow(muestra)
n_train <- round(0.80 * n_obs)
resultados_sim <- matrix(NA, nrow = n_iter, ncol = 5)
colnames(resultados_sim) <- c("ECM","RECM","EAM","EPM","MAPE")
for (i in 1:n_iter) {
idx <- sample(1:n_obs, size = n_train, replace = FALSE)
train <- muestra[idx, ]
test <- muestra[-idx, ]
mod <- lm(lnValor ~ Puntuacion + Edad + lnSalario + Potencial + Reputacion,
data = train)
pred <- predict(mod, newdata = test)
e <- test$lnValor - pred
resultados_sim[i, "ECM"] <- mean(e^2)
resultados_sim[i, "RECM"] <- sqrt(mean(e^2))
resultados_sim[i, "EAM"] <- mean(abs(e))
resultados_sim[i, "EPM"] <- mean(e / test$lnValor) * 100
resultados_sim[i, "MAPE"] <- mean(abs(e / test$lnValor)) * 100
}
sim_df <- as.data.frame(resultados_sim)
cat("Simulación completada:", n_iter, "iteraciones — partición 80/20\n")## Simulación completada: 5000 iteraciones — partición 80/20
options(scipen = 999)
resumen_sim <- data.frame(
Medida = c("ECM", "RECM", "EAM", "EPM (%)", "MAPE (%)"),
Media = round(colMeans(sim_df), 5),
Mediana = round(apply(sim_df, 2, median), 5),
Desv_Est = round(apply(sim_df, 2, sd), 5),
P5 = round(apply(sim_df, 2, quantile, .05),5),
P95 = round(apply(sim_df, 2, quantile, .95),5)
)
colnames(resumen_sim) <- c("Medida", "Media", "Mediana",
"Desv. Est.", "Percentil 5", "Percentil 95")
resumen_sim |>
kable(caption = paste0("Desempeño predictivo — ", n_iter,
" iteraciones Bootstrap (80/20)"),
row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover","condensed"),
full_width = FALSE) |>
row_spec(5, bold = TRUE, background = "#EBF5FB") |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Medida | Media | Mediana | Desv. Est. | Percentil 5 | Percentil 95 |
|---|---|---|---|---|---|
| ECM | 0.06010 | 0.05748 | 0.02352 | 0.02975 | 0.10187 |
| RECM | 0.24062 | 0.23974 | 0.04694 | 0.17249 | 0.31918 |
| EAM | 0.16938 | 0.16793 | 0.02452 | 0.13159 | 0.21213 |
| EPM (%) | -0.04094 | -0.02891 | 0.32969 | -0.61768 | 0.48517 |
| MAPE (%) | 1.24493 | 1.23062 | 0.19479 | 0.95337 | 1.58175 |
par(mfrow = c(2, 3))
colores <- c("steelblue","darkorange","darkgreen","purple","#E74C3C")
titulos <- c("ECM","RECM","EAM","EPM (%)","MAPE (%)")
for (i in 1:5) {
hist(sim_df[[i]], breaks = 60, col = colores[i], border = "white",
main = titulos[i], xlab = titulos[i], cex.main = 1.1)
abline(v = mean(sim_df[[i]]), col = "black", lwd = 2, lty = 2)
}
# Convergencia del MAPE
mape_acum <- cumsum(sim_df$MAPE) / seq_along(sim_df$MAPE)
plot(mape_acum, type = "l", col = "steelblue", lwd = 1.5,
main = "Convergencia MAPE acumulado",
xlab = "Iteración", ylab = "MAPE promedio acumulado (%)")
abline(h = mean(sim_df$MAPE), col = "red", lwd = 2, lty = 2)
legend("topright", legend = paste0("Media = ", round(mean(sim_df$MAPE), 3), "%"),
col = "red", lty = 2, lwd = 2, bty = "n")Con base en las 5000 iteraciones de validacion cruzada bootstrap:
MAPE medio = 1.245%: La capacidad predictiva del modelo es excelente. En promedio, el pronostico del ln(Valor) se desvia un 1.245% del valor real.
RECM medio = 0.24062: Confirma un error cuadratico consistente y estable a traves de las particiones.
EAM medio = 0.16938: El error absoluto promedio de prediccion en escala logaritmica.
EPM medio = -0.041%: El modelo no presenta sesgo sistematico de prediccion.
La grafica de convergencia del MAPE acumulado confirma la estabilidad del modelo: a partir de aproximadamente 1,000 iteraciones el promedio se estabiliza, validando que 5,000 iteraciones son suficientes para obtener estimaciones confiables del desempeno predictivo.
options(scipen = 999)
perfiles <- data.frame(
Perfil = c("Joven promesa", "Titular consolidado", "Estrella mundial",
"Veterano experimentado", "Mediocampista estandar"),
Puntuacion = c(72, 82, 91, 80, 76),
Edad = c(20, 26, 28, 33, 25),
lnSalario = c(log(5000), log(25000), log(150000), log(50000), log(10000)),
Potencial = c(85, 86, 93, 81, 82),
Reputacion = c(1, 3, 5, 3, 2)
)
pred_ic <- predict(modelo,
newdata = perfiles[, c("Puntuacion","Edad","lnSalario","Potencial","Reputacion")],
interval = "prediction", level = 0.95)
proy_df <- data.frame(
Perfil = perfiles$Perfil,
ln_Valor_est = round(pred_ic[, "fit"], 4),
Valor_est_EUR = format(round(exp(pred_ic[, "fit"])), big.mark = ","),
IC_inf_95 = format(round(exp(pred_ic[, "lwr"])), big.mark = ","),
IC_sup_95 = format(round(exp(pred_ic[, "upr"])), big.mark = ",")
)
colnames(proy_df) <- c("Perfil", "ln(Valor) est.",
"Valor est. (EUR)",
"IC inf. 95% (EUR)",
"IC sup. 95% (EUR)")
proy_df |>
kable(caption = "Proyecciones de Valor de Mercado para Perfiles Hipoteticos",
row.names = FALSE, align = "c") |>
kable_styling(bootstrap_options = c("striped","hover"), full_width = TRUE) |>
row_spec(3, bold = TRUE, background = "#EBF5FB") |>
row_spec(0, bold = TRUE, background = "#2C3E50", color = "white")| Perfil | ln(Valor) est. | Valor est. (EUR) | IC inf. 95% (EUR) | IC sup. 95% (EUR) |
|---|---|---|---|---|
| Joven promesa | 15.7060 | 6,622,612 | 4,123,845 | 10,635,461 |
| Titular consolidado | 17.2494 | 30,997,490 | 18,806,023 | 51,092,373 |
| Estrella mundial | 19.1149 | 200,217,252 | 110,797,605 | 361,803,380 |
| Veterano experimentado | 16.0054 | 8,934,091 | 5,429,588 | 14,700,560 |
| Mediocampista estandar | 16.0312 | 9,167,571 | 5,704,261 | 14,733,611 |
proy_plot <- data.frame(
Perfil = perfiles$Perfil,
Est = exp(pred_ic[, "fit"]) / 1e6,
LI = exp(pred_ic[, "lwr"]) / 1e6,
LS = exp(pred_ic[, "upr"]) / 1e6
)
ggplot(proy_plot, aes(x = reorder(Perfil, Est), y = Est)) +
geom_col(fill = "steelblue", alpha = 0.85, width = 0.6) +
geom_errorbar(aes(ymin = LI, ymax = LS),
width = 0.3, color = "#E74C3C", linewidth = 1) +
geom_text(aes(label = paste0("EUR ", round(Est, 1), "M")),
hjust = -0.15, fontface = "bold", size = 3.8) +
coord_flip(ylim = c(0, max(proy_plot$LS) * 1.2)) +
labs(title = "Valor de Mercado Estimado por Perfil de Jugador",
subtitle = "Barras: estimacion puntual | Lineas: IC prediccion al 95%",
x = NULL, y = "Valor estimado (millones EUR)") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold", hjust = 0.5),
plot.subtitle = element_text(hjust = 0.5, color = "grey40"))Las proyecciones reflejan correctamente la jerarquía esperada de valores de mercado según los atributos de cada perfil:
La estrella mundial (OVA=91, Reputación=5) alcanza un valor estimado de €200.2M, el más alto del escenario.
La joven promesa (OVA=72, 20 años, alto potencial) se valúa en €6.6M, reflejando que el mercado descuenta la experiencia aún no acumulada.
El veterano experimentado (33 años) presenta un valor de €8.9M, consistente con la hipótesis de depreciación del capital humano deportivo planteada en la Parte 1.
Los amplios intervalos de predicción al 95% son normales en modelos de cross-section con alta heterogeneidad individual, y refuerzan la necesidad de complementar el modelo con variables adicionales (posición, liga, club) en futuras especificaciones.
Dataset: FIFA 21 Messy Raw Data — Kaggle / sofifa.com Ciclo I 2026 — Escuela de Economía, Universidad de El Salvador.
Akerlof, G. A. (1970). The Market for ‘Lemons’: Quality Uncertainty and the Market Mechanism. The Quarterly Journal of Economics, 84(3), 488-500.
Becker, G. S. (1964). Human Capital: A Theoretical and Empirical Analysis. University of Chicago Press.
Carmichael, F., Thomas, D., & Ward, R. (2011). Production and Efficiency in Association Football. Journal of Sports Economics, 2(3), 228-243.
Franck, E., & Nüesch, S. (2012). Talent and/or Popularity: What Does It Take to Be a Superstar? Economic Inquiry, 50(1), 202-216.
Herm, S., Callsen-Bracker, H. M., & Kreis, H. (2014). When the crowd evaluates soccer players’ market values: Accuracy and evaluation attributes of an opinion exchange platform. Sport Management Review, 17(4), 484-492.
Lewis, M. (2003). Moneyball: The Art of Winning an Unfair Game. W. W. Norton & Company.
Müller, O., Simons, A., & Weinmann, M. (2017). Beyond crowd judgments: Data-driven estimation of market value in association football. European Journal of Operational Research, 263(2), 611-624.
Pappalardo, L., Cintia, P., Rossi, A., Massucco, E., et al. (2019). A public data set of spatio-temporal match events in soccer competitions. Scientific Data, 6, 236.
Poli, R., Ravenel, L., & Besson, R. (2020). Predicting transfer fees. CIES Football Observatory Monthly Report, 52.
Rosen, S. (1974). Hedonic Prices and Implicit Markets: Product Differentiation in Pure Competition. Journal of Political Economy, 82(1), 34-55.
Kaggle. (2021). FIFA 21 Messy, Raw Dataset for Cleaning/Exploring. Recuperado de: https://www.kaggle.com/datasets/yagunnersya/fifa-21-messy-raw-dataset-for-cleaning-exploring
SoFIFA. (2021). Descripción de variables y atributos de jugadores. Recuperado de: https://sofifa.com/