Como ejercicio para adquirir experiencia práctica trabajando con criterios de selección de modelos calculé los ajustes de los datos de mpg en carretera frente a la cilindrada del motor a partir del tidyverse mpg conjunto de datos de ejemplo utilizando polinomio y B-spline modelos de complejidad paramétrica creciente que van de 3 a 19 grados de libertad (obsérvese que el degree
o df
en cada familia de modelos cuenta el número de coeficientes adicionales ajustados además de la intercepción).
Los resultados del procedimiento de ajuste se muestran en el siguiente gráfico. La línea azul muestra el resultado de la regresión prevista a través de un conjunto de valores de desplazamiento del motor espaciados uniformemente a lo largo de la gama del conjunto de datos de entrada, mientras que los puntos naranja-rojo muestran el resultado en los valores dentro del conjunto de datos original que se utilizaron realmente para derivar el ajuste:
A partir de alrededor de ~11 grados de libertad, las líneas azules (que son más visibles cuando no hay observaciones en el conjunto de datos de entrada) empiezan a mostrar signos clásicos de sobreajuste: se contonean y giran salvajemente, variando mucho más que los propios datos de entrada, de hecho en algunos casos se extienden hasta la región no-fiscal (es decir, cayendo hasta mpg negativo, que no tiene interpretación física). Además, las dos clases de modelos (polinómico frente a B-spline) muestran aleatoriedad en las ubicaciones de estas caídas y ondulaciones. Un gráfico adicional a continuación muestra las diferencias entre las dos familias de modelos frente al aumento del número de parámetros del modelo. Para modelos más simples con menos parámetros, la diferencia es uniformemente pequeña, normalmente < 1-2 mpg en todo el rango del conjunto de datos, mientras que para modelos con más parámetros la diferencia es grande, y generalmente se vuelve más divergente a medida que aumenta el número de parámetros:
Basándome en el sobreajuste aparente que puedo ver con un mayor número de parámetros del modelo ajustado, esperaría que la mayoría de los criterios de selección de modelos eligieran un modelo óptimo con < 10 coeficientes ajustados. Sin embargo, extraje los valores del criterio de información de Akaike (AIC) devueltos con cada uno de los modelos ajustados, y eso no es realmente lo que ocurre en este caso. En su lugar, los modelos más complejos con el mayor número de parámetros ajustados presentan los valores AIC más pequeños y, por lo tanto, se ven favorecidos por el AIC:
Edita: Basándome en la respuesta de otro colaborador, he modificado el gráfico anterior para mostrar tanto el AICc como el AIC. Como era de esperar, el uso de AICc en lugar de AIC da lugar a una corrección que es de hecho mayor para los modelos con un mayor número de parámetros, pero no lo suficientemente grande como para hacer ninguna diferencia en el resultado final:
Mi pregunta: ¿qué está pasando aquí? ¿Por qué el AIC da un resultado contrario a la intuición, favoreciendo aparentemente a los modelos sobreajustados? ¿Existe algún criterio alternativo bien establecido que pueda funcionar mejor en este caso, seleccionando un modelo menos complicado que no presente un sobreajuste tan obvio?
Como referencia, el código que produce estos gráficos está aquí ( editar: actualizado para producir la versión 2 del gráfico AIC frente a los grados de libertad de entrada):
library(tidyverse)
library(splines)
library(AICcmodavg)
# ---- Part 1: Fit various models to data and plot results
# Select degrees of freedom (i.e., number of coefficients,
# not counting
# the intercept coefficient) for polynomial or B-spline models
dflo <- 3
dfhi <- 19
inputdf <- seq(dflo,dfhi,2)
ndf <- length(inputdf)
dflist <- as.list(inputdf)
names(dflist) <- sprintf("%2d", inputdf)
# Fit all of the models, and save the fit objects
# to a nested list
# (outer list level: model family (poly or spline),
# inner list level: dof)
fitobj <- list()
fitobj$poly <- map(dflist, ~lm(formula=hwy~poly(displ,
degree=.), data=mpg))
fitobj$bspl <- map(dflist, ~lm(formula=hwy~bs(displ, df=.),
data=mpg))
# Make a list of data points at which to predict the
#fitted models (grid:
# evenly spaced 1D series of points ranging from minimum engine
# displacement
# in the original data set to maximum engine displacement,
# orig: the input
# data set itself)
ngrid <- 200
dval <- list()
dval$grid <- data.frame(displ=seq(min(mpg$displ), max(mpg$displ),
length.out=ngrid))
dval$orig <- data.frame(displ=mpg$displ)
# Key elements for a new list
mtype <- list("poly"="poly", "bspl"="spline")
dtype <- list("grid"="Evenly spaced grid",
"orig"="Original values")
# For both models (poly and spline), and both sets of prediction points (grid
# and original), predict all the models, and dump all of the results to a list
# of data frames, keyed by the cross product of the 2 pairs of input keys
pred <- list()
for(mkey in c("poly", "bspl")) {
for(dkey in c("grid", "orig")) {
crosskey <- paste(mkey, dkey, sep="_")
pred[[crosskey]] <- map_dfr(map(fitobj[[mkey]],
predict.lm,
newdata=dval[[dkey]],
interval="confidence"),
as_tibble, .id="inputdf") %>%
bind_cols(displ=rep(dval[[dkey]]$displ, ndf),
modeltype=mtype[[mkey]],
prediction_points=dtype[[dkey]])
}
}
# Reorganize the list of data frames into a single giant unified data frame
dfpred <- map_dfr(pred, bind_rows) %>%
mutate(modelspec=paste(inputdf, modeltype, sep=" "))
# Plot all of the fitted models. Evenly spaced 1D grid
# is shown as a blue
# line, the original data points from the parent data set are
# orange-red
# dots superimposed on top of it. Gray ribbons are confidence
# intervals
# produced by predict.lm in previous step above.
plt_overview <- ggplot() +
geom_ribbon(aes(x=displ, ymin=lwr, ymax=upr),
data=dfpred,
fill="grey70") +
geom_point(aes(x=displ, y=hwy), data=mpg,
size=1.5) +
geom_line(aes(x=displ, y=fit,
color=prediction_points),
data=filter(dfpred,
prediction_points==dtype$grid),
size=2) +
geom_point(aes(x=displ, y=fit,
color=prediction_points),
data=filter(dfpred,
prediction_points==dtype$orig),
size=3) +
scale_color_manual(breaks=dtype,
values=c("blue", "tomato")) +
xlab("Engine Displacment (liters)") +
ylab("Highway Miles Per Gallon (mpg)") +
coord_cartesian(ylim=c(0,50)) +
facet_wrap(~modelspec, ncol=4) +
theme(text=element_text(size=32),
legend.position="bottom")
png(filename="fit_overview.png", width=1200, height=1600)
print(plt_overview)
dev.off()
# ---- Part 2: Plot differences between poly / spline model families ----
# For each input degree of freedom, calculate the difference between the
# poly and B-spline model fits, at both the grid and original set of
# prediction points
dfdiff <- bind_cols(filter(dfpred, modeltype=="poly") %>%
select(inputdf, displ, fit, prediction_points) %>%
rename(fit_poly=fit),
filter(dfpred, modeltype=="spline") %>%
select(fit) %>%
rename(fit_bspl=fit)) %>%
mutate(fit_diff=fit_poly-fit_bspl)
# Plot differences between two model families
plt_diff <- ggplot(mapping=aes(x=displ, y=fit_diff, color=prediction_points)) +
geom_line(data=filter(dfdiff, prediction_points==dtype$grid),
size=2) +
geom_point(data=filter(dfdiff, prediction_points==dtype$orig),
size=3) +
scale_color_manual(breaks=dtype, values=c("blue", "tomato")) +
xlab("Engine Displacment (liters)") +
ylab("Difference (Poly - B-Spline) of Fit Results (mpg)") +
coord_cartesian(ylim=c(-10,10)) +
facet_wrap(~inputdf, ncol=4) +
theme(text=element_text(size=32),
legend.position="bottom")
png(filename="fit_diff.png", width=1200, height=800)
print(plt_diff)
dev.off()
# ---- Part 3: Plot Akaike Information Criterion (AIC) for all models ----
# Compute both AIC and AICc for both model families (polynomial and B-spline)
# and organize the result into a single unified data frame
sord <- list("AIC"=FALSE, "AICc"=TRUE)
aictbl <- map(sord,
function(so) map(fitobj,
function(fo) map(fo, AICc, second.ord=so) %>%
unlist())) %>%
map_dfr(function(md) map_dfr(md, enframe,
.id="mt"), .id="aictype") %>%
mutate(modeltype=unlist(mtype[mt]),
inputdf=as.numeric(name)) %>%
rename(aic=value) %>%
select(inputdf, aic, modeltype, aictype)
# Plot AIC
plt_aic <- ggplot(aictbl, aes(x=inputdf, y=aic, color=aictype)) +
geom_line() +
geom_point(size=3) +
xlab("Input Degrees of Freedom") +
ylab("Akaike Information Criterion (AIC)") +
labs(color="AIC Correction Type") +
facet_wrap(~modeltype, ncol=1) +
theme(text=element_text(size=32),
legend.position="bottom")
png(filename="aic_vs_inputdf.png", width=800, height=800)
print(plt_aic)
dev.off()