El examen de admisión a licenciatura de la UNAM fue realizado en línea por primera vez en su historia; cuando se publicaron los resultados, las personas en internet comenzaron a notar que la distribución de los aciertos fue anormal para varias carreras, muchos de los aspirantes habían hecho trampa para poder ingresar.
En este trabajo se hará una comparación de los resultados de los años anteriores con los del concurso de selección de este año 2026. Los datos fueron recopilados de un dataset de libre acceso en GitHub (Karen Arlet Castrillo Cruz. (2026). Resultados UNAM — Datos de admisión a licenciatura (2021–2026).
En este caso, solo se usará el archivo csv con los siguientes datos:
| Columna | Tipo | Descripción |
|---|---|---|
| folio | entero | Folio del aspirante (identificador anónimo, no es información personal) |
| aciertos | entero / NA | Número de respuestas correctas en el examen. Nulo si el aspirante no presentó |
| acreditado | S / N /C / NA | S = seleccionado, N = no
seleccionado, C = cancelado. Nulo cuando no aplica |
| año | entero | Año del proceso de admisión (2021–2026) |
| area | entero (1-4) | Área de conocimiento según clasificación de la DGAE |
| carrera | texto | Carrera solicitada |
| plantel | texto | Plantel/facultad solicitada |
Este análisis busca identificar diferencias y patrones estadísticos en el número de aciertos desde el año 2021-2026.
¿Cómo ha cambiado el desempeño de los aspirantes del concurso de selección de la UNAM entre los años 2021 y 2026? ¿Existe una demanda entre las carreras y el número de aciertos?
La base de datos cuenta con más de 1 millón de registros desde el año 2021 hasta el 2026
Hay 116 carreras ofertadas y se imparten en 54 planteles
La carrera con mayor cantidad de registros es la de Médico Cirujano
library(tidyverse)
library(plotly)
library(knitr)
library(scales)
library(car)
library(broom)
library(dplyr)
datos <- read.csv("aciertos_unam_2021_2026.csv", header = TRUE, stringsAsFactors = FALSE)
names(datos)
## [1] "folio" "aciertos" "acreditado" "año" "area"
## [6] "carrera" "plantel"
dim(datos)
## [1] 1027716 7
str(datos)
## 'data.frame': 1027716 obs. of 7 variables:
## $ folio : int 1 30 56 133 148 151 167 208 283 326 ...
## $ aciertos : num 71 93 49 90 62 51 111 85 NA 106 ...
## $ acreditado: chr "" "" "" "" ...
## $ año : int 2021 2021 2021 2021 2021 2021 2021 2021 2021 2021 ...
## $ area : int 1 1 1 1 1 1 1 1 1 1 ...
## $ carrera : chr "ACTUARIA" "ACTUARIA" "ACTUARIA" "ACTUARIA" ...
## $ plantel : chr "FACULTAD DE CIENCIAS" "FACULTAD DE CIENCIAS" "FACULTAD DE CIENCIAS" "FACULTAD DE CIENCIAS" ...
head(datos)
La variable de interés principal es aciertos. Las demás variables se van a usar para hacer las comparaciones entre grupos (cada año es un grupo).
datos <- datos %>%
mutate(
año = as.factor(año),
area = as.factor(area),
carrera = as.factor(carrera),
plantel = as.factor(plantel)
)
str(datos)
## 'data.frame': 1027716 obs. of 7 variables:
## $ folio : int 1 30 56 133 148 151 167 208 283 326 ...
## $ aciertos : num 71 93 49 90 62 51 111 85 NA 106 ...
## $ acreditado: chr "" "" "" "" ...
## $ año : Factor w/ 6 levels "2021","2022",..: 1 1 1 1 1 1 1 1 1 1 ...
## $ area : Factor w/ 4 levels "1","2","3","4": 1 1 1 1 1 1 1 1 1 1 ...
## $ carrera : Factor w/ 116 levels "ACTUARIA","ADMINISTRACION",..: 1 1 1 1 1 1 1 1 1 1 ...
## $ plantel : Factor w/ 54 levels "CENTRO DE INVESTIGACIÓN EN ENERGÍA",..: 17 17 17 17 17 17 17 17 17 17 ...
Buscamos si hay folios duplicados y si hay datos faltantes:
# Folios duplicados:
sum(duplicated(datos))
## [1] 0
sum(duplicated(datos$folio))
## [1] 821526
datos <- datos %>%
distinct()
nrow(datos)
## [1] 1027716
# datos faltantes
faltantes <- data.frame(
variable = names(datos),
faltantes = sapply(datos, function(x) sum(is.na(x)))
) %>%
mutate(
porcentaje = 100 * faltantes / nrow(datos)
)
print(faltantes)
## variable faltantes porcentaje
## folio folio 0 0.00000
## aciertos aciertos 153694 14.95491
## acreditado acreditado 0 0.00000
## año año 0 0.00000
## area area 0 0.00000
## carrera carrera 0 0.00000
## plantel plantel 0 0.00000
Para los análisis que involucren el número de aciertos, solo se usarán los datos completos con información disponible !
faltantes_año <- datos %>%
group_by(año) %>%
summarise(
total = n(),
faltantes_aciertos = sum(is.na(aciertos)),
porcentaje_faltantes = 100 * faltantes_aciertos / total )
kable(
faltantes_año,
digits = 2,
caption = "Datos faltantes de aciertos por año"
)
| año | total | faltantes_aciertos | porcentaje_faltantes |
|---|---|---|---|
| 2021 | 167432 | 26986 | 16.12 |
| 2022 | 177780 | 24188 | 13.61 |
| 2023 | 178404 | 25544 | 14.32 |
| 2024 | 162463 | 23178 | 14.27 |
| 2025 | 174543 | 25110 | 14.39 |
| 2026 | 167094 | 28688 | 17.17 |
# graficamos
plot_ly(
faltantes_año,
x = ~año,
y = ~porcentaje_faltantes,
type = "bar",
marker = list(color = "#9AC0CD"),
text = ~paste0(round(porcentaje_faltantes, 2), "%"),
textposition = "outside",
hovertemplate = paste( "<b>Año:</b> %{x}<br>", "<b>Faltantes:</b> %{y:.2f}%<extra></extra>"
)
) %>%
layout(
title = "Porcentaje de valores faltantes en aciertos",
xaxis = list(title = "Año"),
yaxis = list(title = "Porcentaje de datos faltantes"),
hovermode = "x"
)
Analizar la cantidad de datos faltantes nos indica el porcentaje de personas ausentes al examen.
Se puede observar que la proporción se mantiene más o menos estable, siendo 13.61% el porcentaje más bajo (en 2022) y 17.17% el más alto (en 2026).
Ya que las personas ausentes no generan un puntaje, se tienen que eliminar para no sesgar las medias hacia abajo.
datos_aciertos <- datos %>%
filter(!is.na(aciertos))
descriptivos <- datos_aciertos %>%
summarise(
n = n(),
media = mean(aciertos),
mediana = median(aciertos),
desviacion_estandar = sd(aciertos),
minimo = min(aciertos),
Q1 = quantile(aciertos, 0.25),
Q3 = quantile(aciertos, 0.75),
maximo = max(aciertos)
)
print(descriptivos)
## n media mediana desviacion_estandar minimo Q1 Q3 maximo
## 1 874022 58.30326 54 20.97062 0 42 71 120
descriptivos_año <- datos_aciertos %>%
group_by(año) %>%
summarise(
n = n(),
media = mean(aciertos),
mediana = median(aciertos),
desviacion_estandar = sd(aciertos),
minimo = min(aciertos),
Q1 = quantile(aciertos, 0.25),
Q3 = quantile(aciertos, 0.75),
maximo = max(aciertos)
)
print(descriptivos_año, width = Inf)
## # A tibble: 6 × 9
## año n media mediana desviacion_estandar minimo Q1 Q3 maximo
## <fct> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 2021 140446 56.1 52 18.7 0 42 67 120
## 2 2022 153592 54.9 51 18.6 1 41 66 119
## 3 2023 152860 56.1 52 18.9 0 42 68 120
## 4 2024 139285 56.1 52 19.7 1 41 68 120
## 5 2025 149433 56.4 52 19.7 0 41 68 120
## 6 2026 138406 71.1 71 25.2 0 49 93 120
Primero vamos a analizar la media de aciertos por año
plot_ly(
descriptivos_año,
x = ~año,
y = ~media,
type = "scatter",
mode = "lines+markers",
line = list(color = "#87CEFA", width = 3 ),
marker = list(color = "#A2B5CD",size = 8),
text = ~paste0(
"Año: ", año,
"<br>Media: ", round(media, 2),
"<br>Mediana: ", round(mediana, 2)
),
hoverinfo = "text"
) %>%
layout(
title = "Evolución del promedio de aciertos, 2021–2026",
xaxis = list(title = "Año"),
yaxis = list(title = "Promedio de aciertos"),
hovermode = "x unified"
)
Con esta gráfica podemos ver de manera visual si hay una tendencia temporal en el promedio de aciertos. Se observa que las medias estaban muy parejas entre 2021 y 2025, pero para el año 2026 hubo un repunte enorme en el número de aciertos.
plot_ly(
datos_aciertos,
x = ~aciertos,
type = "histogram",
nbinsx = 60,
marker = list(
color = "#FFC1C1"
)
) %>%
layout(
title = "Distribución general del número de aciertos",
xaxis = list(title = "Número de aciertos"),
yaxis = list(title = "Frecuencia")
)
datos_animacion_prop <- datos_aciertos %>%
mutate(
grupo_aciertos = cut(
aciertos,
breaks = seq(0, 125, by = 5),
right = FALSE,
include.lowest = TRUE
)
) %>%
group_by(año, grupo_aciertos) %>%
summarise(
frecuencia = n(),
.groups = "drop"
) %>%
group_by(año) %>%
mutate(
porcentaje = frecuencia / sum(frecuencia) * 100
) %>%
ungroup() %>%
mutate(
aciertos_eje_x = as.numeric(
sub("\\[([0-9]+),.*", "\\1", grupo_aciertos)
)
) %>%
drop_na(aciertos_eje_x)
plot_ly(
data = datos_animacion_prop,
x = ~aciertos_eje_x,
y = ~porcentaje,
frame = ~año,
type = "bar",
marker = list(
color = "#A4D3EE",
line = list(
color = "white",
width = 0.5
)
),
width = 0.8,
hovertemplate = paste(
"Intervalo: %{x}–%{x+5} aciertos<br>",
"Porcentaje: %{y:.2f}%",
"<extra></extra>"
)
) %>%
layout(
title = list(
text = "Evolución de la distribución relativa de aciertos"
),
xaxis = list(
title = "Número de aciertos",
range = c(0, 120),
dtick = 10
),
yaxis = list(
title = "Porcentaje de aspirantes",
ticksuffix = "%",
range = c(
0,
max(datos_animacion_prop$porcentaje) * 1.10
)
)
) %>%
animation_opts(
frame = 1500,
transition = 800,
easing = "linear",
redraw = FALSE
) %>%
animation_slider(
currentvalue = list(
prefix = "Año evaluado: "
)
)
Esta gráfica ilustra el cambio drástico en el comportamiento de la población. Durante los años 2021-2025 la campana presenta un sesgo hacia la derecha. En 2026 la campana se aplana y se desplaza hacia la derecha de manera drástica.
Este desplazamiento coincide con el cambio en las condiciones de evaluación.
atipicos <- datos_aciertos %>%
group_by(año) %>%
summarise(
Q1 = quantile(aciertos, 0.25),
Q3 = quantile(aciertos, 0.75),
IQR = IQR(aciertos),
limite_inferior = Q1 - 1.5 * IQR,
limite_superior = Q3 + 1.5 * IQR,
atipicos_inferiores = sum(aciertos < limite_inferior),
atipicos_superiores = sum(aciertos > limite_superior),
porcentaje_atipicos = 100 * (atipicos_inferiores + atipicos_superiores) / n()
)
print(atipicos, width = Inf)
## # A tibble: 6 × 9
## año Q1 Q3 IQR limite_inferior limite_superior atipicos_inferiores
## <fct> <dbl> <dbl> <dbl> <dbl> <dbl> <int>
## 1 2021 42 67 25 4.5 104. 2
## 2 2022 41 66 25 3.5 104. 1
## 3 2023 42 68 26 3 107 1
## 4 2024 41 68 27 0.5 108. 0
## 5 2025 41 68 27 0.5 108. 1
## 6 2026 49 93 44 -17 159 0
## atipicos_superiores porcentaje_atipicos
## <int> <dbl>
## 1 2280 1.62
## 2 2479 1.61
## 3 1368 0.896
## 4 1337 0.960
## 5 1756 1.18
## 6 0 0
plot_ly(
datos_aciertos,
x = ~año,
y = ~aciertos,
color = ~año,
type = "box",
boxpoints = FALSE
) %>%
layout(
title = "Distribución de aciertos por año",
xaxis = list(title = "Año"),
yaxis = list(title = "Número de aciertos"),
showlegend = FALSE
)
Los diagramas de caja muestran la distribución de los aciertos por cada año. De 2021-2025 las cajas se mantienen en el mismo punto con medianas que van de 51-52 aciertos
Se observa que la mediana se desplaza hacia arriba en el año 2026, la mediana sube a 71. En este caso, en 2026 dejaron de existir valores atípicos porque obtener un puntaje perfecto ya no era algo raro.
Ahora, vamos a ver si hay un cambio en la frecuencia de resultados altos, vamos a analizar 3 indicadores:
datos_aciertos <- datos_aciertos %>%
mutate(
mayor_90 = aciertos >= 90,
mayor_100 = aciertos >= 100,
mayor_110 = aciertos >= 110
)
proporciones_altas <- datos_aciertos %>%
group_by(año) %>%
summarise(
n = n(),
prop_90 = mean(mayor_90) * 100,
prop_100 = mean(mayor_100) * 100,
prop_110 = mean(mayor_110) * 100
)
print(proporciones_altas)
## # A tibble: 6 × 5
## año n prop_90 prop_100 prop_110
## <fct> <int> <dbl> <dbl> <dbl>
## 1 2021 140446 6.81 2.92 0.680
## 2 2022 153592 6.15 2.58 0.544
## 3 2023 152860 6.92 2.82 0.566
## 4 2024 139285 7.84 3.46 0.759
## 5 2025 149433 8.14 3.72 0.964
## 6 2026 138406 29.1 17.0 5.79
Hacemos una gráfica temporal:
proporciones_long <- proporciones_altas %>%
pivot_longer(
cols = starts_with("prop_"),
names_to = "umbral",
values_to = "porcentaje"
) %>%
mutate(
umbral = dplyr::recode(
umbral,
prop_90 = "90 o más",
prop_100 = "100 o más",
prop_110 = "110 o más"
)
)
plot_ly(
proporciones_long,
x = ~año,
y = ~porcentaje,
color = ~umbral,
type = "scatter",
mode = "lines+markers"
) %>%
layout(
title = "Evolución de la proporción de resultados altos",
xaxis = list(title = "Año"),
yaxis = list(
title = "Porcentaje de aspirantes",
ticksuffix = "%"
)
)
¿Cuál es la media de aciertos de los aspirantes en 2026?
datos_2026 <- datos_aciertos %>%
filter(año == "2026")
ic_media_2026 <- t.test(datos_2026$aciertos)
ic_media_2026
##
## One Sample t-test
##
## data: datos_2026$aciertos
## t = 1048.1, df = 138405, p-value < 2.2e-16
## alternative hypothesis: true mean is not equal to 0
## 95 percent confidence interval:
## 70.98527 71.25127
## sample estimates:
## mean of x
## 71.11827
Aquí se estableció un nivel de confianza del 95%. Con ese nivel de confianza se puede afirmar que el verdadero promedio poblacional de aciertos se encuentra entre 70.98 y 71.25.
Para poder analizar estos datos, se van a crear 2 grupos: - periodo 2021-2025 - periodo 2026
datos_comparacion <- datos_aciertos %>%
mutate(
periodo = ifelse(
año == "2026",
"2026", "2021-2025"
)
)
table(datos_comparacion$periodo)
##
## 2021-2025 2026
## 735616 138406
comparacion_medias <- t.test(
aciertos ~ periodo,
data = datos_comparacion
)
comparacion_medias
##
## Welch Two Sample t-test
##
## data: aciertos by periodo
## t = -213.17, df = 169549, p-value < 2.2e-16
## alternative hypothesis: true difference in means between group 2021-2025 and group 2026 is not equal to 0
## 95 percent confidence interval:
## -15.36615 -15.08616
## sample estimates:
## mean in group 2021-2025 mean in group 2026
## 55.89211 71.11827
Se realizó una prueba t de Welch para comparar el promedio del bloque de años de 2021-2025 vs 2026.El p-value es prácticamente 0, por lo que se rechaza la hipótesis nula.
Se analiza si la proporción de aspirantes con 100 o más aciertos cambió entre 2026 y los años 2021-2025
tabla_100 <- datos_comparacion %>%
count(periodo, mayor_100)
tabla_100
prop_2026 <- sum(
datos_comparacion$periodo == "2026" &
datos_comparacion$mayor_100
)
n_2026 <- sum(
datos_comparacion$periodo == "2026"
)
prop_historico <- sum(
datos_comparacion$periodo == "2021-2025" & datos_comparacion$mayor_100
)
n_historico <- sum(
datos_comparacion$periodo == "2021-2025"
)
prop.test(
x = c(prop_2026, prop_historico),
n = c(n_2026, n_historico)
)
##
## 2-sample test for equality of proportions with continuity correction
##
## data: c(prop_2026, prop_historico) out of c(n_2026, n_historico)
## X-squared = 44893, df = 1, p-value < 2.2e-16
## alternative hypothesis: two.sided
## 95 percent confidence interval:
## 0.1369818 0.1410259
## sample estimates:
## prop 1 prop 2
## 0.16992760 0.03092374
La prueba de diferencia de proporciones confirma el salto en puntajes. De 2021-2025 solamente el 3.09% de la población lograba pasar los 100 aciertos.
En 2026 la cifra aumentó a 16.99%.
Hay asociación entre el año y la obtención de más de 100 aciertos?
Vamos a construir una tabla de contingencia:
tabla_independencia <- table(
datos_aciertos$año,
datos_aciertos$mayor_100
)
tabla_independencia
##
## FALSE TRUE
## 2021 136343 4103
## 2022 149627 3965
## 2023 148547 4313
## 2024 134471 4814
## 2025 143880 5553
## 2026 114887 23519
Se realiza una prueba chi-cuadrada de independencia:
prueba_independencia <- chisq.test(
tabla_independencia
)
prueba_independencia
##
## Pearson's Chi-squared test
##
## data: tabla_independencia
## X-squared = 45159, df = 5, p-value < 2.2e-16
La prueba de Chi-Cuadrada de Pearson mostró un p-value < 0.05. En este caso podemos concluir que hay una dependencia estadística fuerte entre el año en que se presentó el examen y la probabilidad de obtener un puntaje sobresaliente (mayor a 100 aciertos en este caso). La anomalía estadística se concentra en el año 2026.
Existe una diferencia significativa entre las medias de aciertos de los 6 años analizados
Realizamos una prueba ANOVA:
modelo_anova <- aov(
aciertos ~ año,
data = datos_aciertos
)
summary(modelo_anova)
## Df Sum Sq Mean Sq F value Pr(>F)
## año 5 27207332 5441466 13316 <2e-16 ***
## Residuals 874016 357158152 409
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
El modelo ANOVA para un factor rechza la hipótesis nula de que todos los años tienen el mismo promedio de aciertos. Este resultado confirma que al menos en uno de los años hay una población que tuvo un desempeño distinto; es por eso, que debe hacerse una prueba de Tukey para saber en qué grupo hay diferencias.
plot(modelo_anova)
En la gráfica Residuals vs Fitted los puntos no generan un embudo, lo que indica que la varianza de los datos es constante.
qqnorm(
residuals(modelo_anova),
main = "Gráfica Q-Q de los residuos"
)
qqline(
residuals(modelo_anova)
)
En esta gráfica se observan colas pesadas, lo que quiere decir que hay muchos estudiantes con puntajes muy bajos (cercanos a 0) y puntajes muy altos (cercanis a 120), al tener más de 80,000 datos se asume que los datos se comportan de forma normal.
Como se rechazó la hipótesis nula, hay que determinar en qué años existen las diferencias.
tukey <- TukeyHSD(
modelo_anova
)
tukey
## Tukey multiple comparisons of means
## 95% family-wise confidence level
##
## Fit: aov(formula = aciertos ~ año, data = datos_aciertos)
##
## $año
## diff lwr upr p adj
## 2022-2021 -1.189431319 -1.40211458 -0.9767481 0.0000000
## 2023-2021 0.008421775 -0.20450458 0.2213481 0.9999975
## 2024-2021 -0.035091491 -0.25292967 0.1827467 0.9974586
## 2025-2021 0.281860526 0.06776825 0.4959528 0.0024224
## 2026-2021 15.030170078 14.81198487 15.2483553 0.0000000
## 2023-2022 1.197853094 0.98972986 1.4059763 0.0000000
## 2024-2022 1.154339829 0.94119405 1.3674856 0.0000000
## 2025-2022 1.471291846 1.26197594 1.6806078 0.0000000
## 2026-2022 16.219601397 16.00610097 16.4331018 0.0000000
## 2024-2023 -0.043513266 -0.25690161 0.1698751 0.9922921
## 2025-2023 0.273438752 0.06387584 0.4830017 0.0027554
## 2026-2023 15.021748303 14.80800571 15.2354909 0.0000000
## 2025-2024 0.316952017 0.10240027 0.5315038 0.0003675
## 2026-2024 15.065261568 14.84662549 15.2838976 0.0000000
## 2026-2025 14.748309551 14.53340547 14.9632136 0.0000000
Visualizamos los intervalos de confianza:
# Convertir el resultado matemático de Tukey en una tabla normal (data frame)
tukey_df <- as.data.frame(tukey$año)
# Sacar los nombres de las filas a una columna real
tukey_df$comparacion <- rownames(tukey_df)
# Identificar y etiquetar las anomalías
tukey_df <- tukey_df %>%
mutate(
estado = ifelse(grepl("2026", comparacion), "Anomalía (Trampa)", "Comportamiento Normal")
)
# graficar
plot_ly(
data = tukey_df,
x = ~diff,
y = ~comparacion,
color = ~estado,
colors = c("Anomalía (Trampa)" = "#E74C3C", "Comportamiento Normal" = "#34495E"),
type = "scatter",
mode = "markers",
marker = list(size = 8),
error_x = list(
type = "data",
symmetric = FALSE,
array = ~upr - diff,
arrayminus = ~diff - lwr,
width = 4,
thickness = 1.5
),
hovertemplate = paste(
"<b>Comparación:</b> %{y}<br>",
"<b>Diferencia de aciertos:</b> %{x:.2f}<br>",
"<extra></extra>"
)
) %>%
layout(
title = "Comparaciones múltiples de medias (Tukey HSD)",
xaxis = list(title = "Diferencia en el promedio de aciertos"),
yaxis = list(title = "Años comparados", autorange = "reversed"),
legend = list(title = list(text = "<b>Diagnóstico</b>")),
# Línea punteada en el cero
shapes = list(
list(
type = "line",
x0 = 0, x1 = 0,
y0 = 0, y1 = 1,
yref = "paper",
line = list(color = "darkgray", dash = "dash")
)
)
)
La prueba de Tukey nos dice en qué grupo se encuentra la diferencia que mostró la prueba ANOVA. Se puede ver en los intervalos de confianza de la gráfica, que del año 2021-2025 hay diferencias pequeñas, algunas cruzan el 0. Por otro lado, el año 2026 se aleja por completo, mostrando diferencias positivas muy grandes y significativas contra todos los demás años.
Primero conatremos el número de aspirantes por carrera:
demanda_carrera <- datos %>%
filter(!is.na(carrera)) %>%
count(carrera, name = "demanda") %>%
arrange(desc(demanda))
head(demanda_carrera, 20)
Analizamos la demanda de las 10 carreras más solicitadas por cada año.
top10_carreras <- demanda_carrera %>%
slice_head(n = 10)
print(top10_carreras)
## carrera demanda
## 1 MEDICO CIRUJANO 158972
## 2 DERECHO 73882
## 3 PSICOLOGIA 61663
## 4 ADMINISTRACION 52285
## 5 ARQUITECTURA 51504
## 6 CIRUJANO DENTISTA 48211
## 7 ENFERMERIA 41349
## 8 CONTADURIA 38961
## 9 MEDICINA VETERINARIA Y ZOOTECNIA 38760
## 10 PEDAGOGIA 30216
Vamos a analizar si hubo algún cambio en el número de aciertos de las carreras más demandadas
desempeno_top10 <- datos_aciertos %>%
filter(carrera %in% top10_carreras$carrera) %>%
group_by(año, carrera) %>%
summarise(
n = n(),
media_aciertos = mean(aciertos, na.rm = TRUE),
mediana_aciertos = median(aciertos, na.rm = TRUE),
sd_aciertos = sd(aciertos, na.rm = TRUE),
.groups = "drop"
) %>%
mutate(
carrera = as.character(carrera),
año = as.character(año)
)
plot_ly(
desempeno_top10,
x = ~año,
y = ~media_aciertos,
color = ~carrera,
type = "scatter",
mode = "lines+markers",
hovertemplate = paste(
"<b>%{fullData.name}</b><br>",
"Año: %{x}<br>",
"Promedio de aciertos: %{y:.2f}",
"<extra></extra>"
)
) %>%
layout(
title = list(
text = "Evolución del promedio de aciertos<br><sup>10 carreras con mayor demanda</sup>"
),
xaxis = list(
title = "Año",
type = "category"
),
yaxis = list(
title = "Promedio de aciertos"
),
hovermode = "x unified",
legend = list(
title = list(text = "Carrera")
)
)
## Warning in RColorBrewer::brewer.pal(max(N, 3L), "Set2"): n too large, allowed maximum for palette Set2 is 8
## Returning the palette you asked for with that many colors
## Warning in RColorBrewer::brewer.pal(max(N, 3L), "Set2"): n too large, allowed maximum for palette Set2 is 8
## Returning the palette you asked for with that many colors
desempeno_animado <- desempeno_top10 %>%
group_by(año) %>%
arrange(desc(media_aciertos), .by_group = TRUE) %>%
mutate(
ranking = row_number()
) %>%
ungroup()
plot_ly(
desempeno_animado,
x = ~media_aciertos,
y = ~reorder(carrera, -ranking),
frame = ~año,
type = "bar",
orientation = "h",
text = ~paste0(
"<b>", carrera, "</b>",
"<br>Promedio: ", round(media_aciertos, 2),
"<br>Aspirantes: ", comma(n)
),
hoverinfo = "text"
) %>%
layout(
title = list(
text = "Desempeño de las carreras más demandadas<br><sup>Ordenadas por promedio de aciertos</sup>"
),
xaxis = list(
title = "Promedio de aciertos"
),
yaxis = list(
title = "",
autorange = "reversed"
),
margin = list(
l = 200,
r = 40,
t = 90,
b = 70
)
) %>%
animation_opts(
frame = 1200,
transition = 700,
redraw = FALSE
) %>%
animation_slider(
currentvalue = list(
prefix = "Año: ",
font = list(size = 16)
)
) %>%
animation_button(
x = 1,
xanchor = "right",
y = 0,
yanchor = "top"
)
demanda_desempeno <- datos_aciertos %>%
filter(
carrera %in% top10_carreras$carrera
) %>%
group_by(
año,
carrera
) %>%
summarise(
demanda = n(),
media_aciertos = mean(aciertos, na.rm = TRUE),
.groups = "drop"
)
head(demanda_desempeno)
plot_ly(
demanda_desempeno,
x = ~demanda,
y = ~media_aciertos,
size = ~demanda,
color = ~año,
text = ~carrera,
type = "scatter",
mode = "markers",
hovertemplate = paste(
"<b>%{text}</b><br>",
"Año: %{marker.color}<br>",
"Demanda: %{x}<br>",
"Promedio de aciertos: %{y:.2f}",
"<extra></extra>"
)
) %>%
layout(
title = "Relación entre demanda y desempeño",
xaxis = list(
title = "Número de aspirantes"
),
yaxis = list(
title = "Promedio de aciertos"
)
)
## Warning: `line.width` does not currently support multiple values.
## Warning: `line.width` does not currently support multiple values.
## Warning: `line.width` does not currently support multiple values.
## Warning: `line.width` does not currently support multiple values.
## Warning: `line.width` does not currently support multiple values.
## Warning: `line.width` does not currently support multiple values.
correlacion <- cor(
demanda_desempeno$demanda,
demanda_desempeno$media_aciertos,
use = "complete.obs"
)
correlacion
## [1] 0.311761
plot_ly(
demanda_desempeno,
x = ~demanda,
y = ~media_aciertos,
frame = ~año,
type = "scatter",
mode = "markers",
marker = list(color = "#FF82AB", size = 9),
text = ~carrera,
hovertemplate = paste(
"<b>Carrera:</b> %{text}<br>",
"<b>Demanda:</b> %{x}<br>",
"<b>Media de aciertos:</b> %{y:.2f}",
"<extra></extra>"
)
) %>%
layout(
title = "Relación entre demanda y promedio de aciertos",
xaxis = list(
title = "Número de aspirantes" ),
yaxis = list(
title = "Promedio de aciertos"
)
) %>%
animation_slider(
currentvalue = list(
prefix = "Año: "
)
)
Las variables son: - demanda - año
modelo_regresion <- lm(
media_aciertos ~ demanda + año,
data = demanda_desempeno
)
summary(modelo_regresion)
##
## Call:
## lm(formula = media_aciertos ~ demanda + año, data = demanda_desempeno)
##
## Residuals:
## Min 1Q Median 3Q Max
## -6.5054 -1.4036 0.1693 1.4733 5.5963
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 4.971e+01 1.116e+00 44.555 < 2e-16 ***
## demanda 4.814e-04 7.749e-05 6.213 8.29e-08 ***
## año2022 -1.401e+00 1.328e+00 -1.055 0.296
## año2023 -3.660e-02 1.328e+00 -0.028 0.978
## año2024 1.547e-01 1.325e+00 0.117 0.907
## año2025 4.613e-02 1.327e+00 0.035 0.972
## año2026 1.594e+01 1.325e+00 12.028 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 2.962 on 53 degrees of freedom
## Multiple R-squared: 0.8423, Adjusted R-squared: 0.8245
## F-statistic: 47.18 on 6 and 53 DF, p-value: < 2.2e-16
Se construyó un modelo de regresión usando la demanda y el año para predecir el promedio de aciertos de las carreras.Este modelo arrojó una R-cuadrada ajustada de 0.2994, lo que indica que estas variables explican el 29.9% de la variabilidad de los puntajes.
summary(modelo_regresion)$r.squared
## [1] 0.8423119
Este valor indica la proporción de variabilidad del promedio de aciertos
confint( modelo_regresion, level = 0.95 )
## 2.5 % 97.5 %
## (Intercept) 47.4706917838 5.194618e+01
## demanda 0.0003259831 6.368198e-04
## año2022 -4.0646237263 1.261810e+00
## año2023 -2.7001616778 2.626960e+00
## año2024 -2.5033534041 2.812795e+00
## año2025 -2.6154639187 2.707719e+00
## año2026 13.2806770755 1.859619e+01
Graficamos para ver visualmente como se distribuyen los datos
plot(modelo_regresion)
qqnorm(
residuals(modelo_regresion),
main = "Q-Q plot de los residuos"
)
qqline(
residuals(modelo_regresion)
)
range( demanda_desempeno$demanda )
## [1] 3608 23481
demanda_nueva <- median(
demanda_desempeno$demanda
)
nuevo <- data.frame(
demanda = demanda_nueva,
año = factor(
"2026",
levels = levels(demanda_desempeno$año)
)
)
nuevo
Intervalo de confianza:
predict(
modelo_regresion,
newdata = nuevo,
interval = "confidence",
level = 0.95
)
## fit lwr upr
## 1 69.06265 67.1762 70.9491
El valor predicho central es de 68.99 aciertos. - el intervalo de confianza va de 67.80 - 70.17, esto nos dice en qué rango cae el promedio matemático global de todas las carreras con esa demanda
Intervalo de predicción:
predict(
modelo_regresion,
newdata = nuevo,
interval = "prediction",
level = 0.95
)
## fit lwr upr
## 1 69.06265 62.82877 75.29654
El intervalo de prediccón es más amplio, va de 56.31 - 81.66, este estima el rango de aciertos con el que podría flucturar una sola carrera
Este análisis estadístico de los resultados del examen de admisión de la licenciatura de la UNAM de los años 2021-2026 confirma la existencia de una anomalía atípica en el año 2026.
El cambio a examen en línea coincide con una alteración masiva de la distribución global de los puntajes, esto respalda las sospechas de trampa sistemática por parte de una gran proporción de los aspirantes.
Se usaron técnicas de estadística descriptiva e inferencial donde se logró demostrar:
una ruptura en la tendencia histórica, de 2021-2025 el desempeño se mantuvo con una mediana de 51 a 52 aciertos, en 2026 la mediana subió a 71 aciertos, en las pruebas T y en los ANOVA se confirma que la anomalía en los aciertos no se debe al azar
la prueba de Tukey mostró que la diferencia se concentraba en el año 2026
la prueba de proporciones y la prueba chi-cuadrarda mostraron que la obtención de más de 100 aciertos dejó de ser un evento atípico, de 2021-2025 el porcentaje era de 3%, mientras que en 2026 el porcentaje se volvió de casi el 17%, las colas de la campana de distribución se movieron por completo hacia la derecha
al analizar la relación entre la demanda de las carreras y el desempeño se encontró una correlación débil, el modelo de regresión lineal demostró que la cantidad de aspirantes no explica por sí sola los puntajes altos.
el modelo de regresión confirmó que nada más por presentar el examen en 2026, añadió casi 10 puntos al promedio de aciertos de cualquier licenciatura
Con estos resultados se demuestra que los resultados del concurso de selección de 2026 rompieron los parámetros de normalidad de los años previos. Este año desaparecieron los valores atípicos superiores y hubo una inflación general en los promedios, lo que sugiere que las condiciones de evaluación comprometieron la integridad del examen.