knitr::opts_chunk$set(echo = TRUE)
library(readr)
library(tidyverse)
## ── Attaching packages ─────────────────────────────────────── tidyverse 1.3.2 ──
## âś” ggplot2 3.4.0 âś” dplyr 1.0.9
## âś” tibble 3.1.7 âś” stringr 1.4.1
## âś” tidyr 1.2.1 âś” forcats 0.5.2
## âś” purrr 0.3.4
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## âś– dplyr::filter() masks stats::filter()
## âś– dplyr::lag() masks stats::lag()
library(dplyr)
library(tree)
library(caret)
## Caricamento del pacchetto richiesto: lattice
##
## Caricamento pacchetto: 'caret'
##
## Il seguente oggetto è mascherato da 'package:purrr':
##
## lift
library(rpart)
library(rpart.plot)
library(glmnet)
## Caricamento del pacchetto richiesto: Matrix
##
## Caricamento pacchetto: 'Matrix'
##
## I seguenti oggetti sono mascherati da 'package:tidyr':
##
## expand, pack, unpack
##
## Loaded glmnet 4.1-4
library(ggplot2)
library(coefplot)
library(earth)
## Caricamento del pacchetto richiesto: Formula
## Caricamento del pacchetto richiesto: plotmo
## Caricamento del pacchetto richiesto: plotrix
## Caricamento del pacchetto richiesto: TeachingDemos
Importo i dati e trasformo le variabili Face e
Income.
tl = read.csv("dataset/TL.csv") %>% tibble %>% select(-X)
tl = tl %>% mutate_at(c( "MARSTAT"), as.factor)
colLog = c("FACE","INCOME")
tl = tl %>% mutate_at(colLog,log) %>% rename_at(colLog, function(c) paste("L",c,sep = ""))
tl
Creo una partizione del dataset, l’80% dei dati saranno destinati al training del modello mentre il restante 20% verrà utilizzato per la valutazione del modello creato.
set.seed(1)
train = createDataPartition(tl$AGE,p = .7,list = F)
Nella raffigurazione dell’albero si nota come la variabile più
influente e importe è LINCOME, seguita da AGE.
Essa, infatti, non è solo quella che compare sul primo nodo ma è anche
la piĂą utilizzata nei nodi.
I nodi terminali sono 10 e sono stati scelti automaticamente dalla
funzione tree. In alternativa, potevano essere dati in
input il numero di nodi foglia desiderati, da un minimo di 1 (soluzione
banale), fino ad un massimo di \(n\)
nodi foglia dove \(n\) è il numero di
osservazioni utlizzate per allenare il modello. Ovviamente entrambe le
solouzioni non vanno bene, la prima perché non generebbe nessun
risultato utile in quanto tutte le osservazioni del dataset
apprterebbero alla medesima regione dello spazio in cui si è divisa la
partizione dei dati. Nel secondo caso si andrebbe incontro
all’overfitting massimo.
albero = tree(LFACE ~ ., data = tl[train,])
{
plot(albero)
text(albero, cex = 0.7)
}
Per scegliere il modello migliore di regressione lineare utilizzo il
criterio dell’informazione di Akaike (AIC), che va penalizzare i modelli
con troppe variabili e poche informazioni aggiuntive, come \(R_{adj}^2\). Tramite la funzione
stepAIC andiamo a selezionare il modello con il minore
AIC.
modLm = MASS::stepAIC(lm(LFACE ~ ., data = tl[train,]))
## Start: AIC=140.03
## LFACE ~ AGE + MARSTAT + EDUCATION + NUMHH + LINCOME
##
## Df Sum of Sq RSS AIC
## - MARSTAT 1 2.434 378.42 139.29
## - AGE 1 3.535 379.52 139.85
## <none> 375.99 140.03
## - NUMHH 1 19.560 395.55 147.92
## - EDUCATION 1 27.044 403.03 151.57
## - LINCOME 1 77.135 453.12 174.42
##
## Step: AIC=139.29
## LFACE ~ AGE + EDUCATION + NUMHH + LINCOME
##
## Df Sum of Sq RSS AIC
## - AGE 1 2.847 381.27 138.75
## <none> 378.42 139.29
## - EDUCATION 1 25.846 404.27 150.17
## - NUMHH 1 32.756 411.18 153.47
## - LINCOME 1 87.505 465.93 177.85
##
## Step: AIC=138.75
## LFACE ~ EDUCATION + NUMHH + LINCOME
##
## Df Sum of Sq RSS AIC
## <none> 381.27 138.75
## - EDUCATION 1 25.671 406.94 149.46
## - NUMHH 1 42.432 423.70 157.33
## - LINCOME 1 84.659 465.93 175.85
modLm %>% summary
##
## Call:
## lm(formula = LFACE ~ EDUCATION + NUMHH + LINCOME, data = tl[train,
## ])
##
## Residuals:
## Min 1Q Median 3Q Max
## -5.8398 -0.7949 0.1235 0.9196 5.2222
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 1.43477 1.01113 1.419 0.157534
## EDUCATION 0.15547 0.04335 3.586 0.000426 ***
## NUMHH 0.32051 0.06952 4.611 7.34e-06 ***
## LINCOME 0.65302 0.10027 6.512 6.38e-10 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 1.413 on 191 degrees of freedom
## Multiple R-squared: 0.4035, Adjusted R-squared: 0.3941
## F-statistic: 43.06 on 3 and 191 DF, p-value: < 2.2e-16
modLasso = glmnet(x = makeX(tl[train,names(tl)!= "LFACE"]),
y = tl$LFACE[train],
alpha = 1)
cvModLasso = cv.glmnet(makeX(tl[train,names(tl)!= "LFACE"]),
tl$LFACE[train],
alpha = 1)
{
plot(cvModLasso)
abline(v = log(cvModLasso$lambda.min), col = "blue", lty = 2)
}
modLasso = glmnet(x = makeX(tl[train,names(tl)!= "LFACE"]),
y = tl$LFACE[train],
alpha = 1,
lambda = cvModLasso$lambda.min,
standardize = TRUE,
family = "gaussian")
coefplot(modLasso)
coef.glmnet(modLasso, s = cvModLasso$lambda.min)
## 7 x 1 sparse Matrix of class "dgCMatrix"
## s1
## (Intercept) 1.94394349
## AGE -0.01146647
## MARSTATnot single 0.30992159
## MARSTATsingle .
## EDUCATION 0.15876524
## NUMHH 0.25386762
## LINCOME 0.64806305
modRidge = glmnet(x = makeX(tl[train,names(tl)!= "LFACE"]),
y = tl$LFACE[train],
alpha = 0)
cvModRidge = cv.glmnet(makeX(tl[train,names(tl)!= "LFACE"]),
tl$LFACE[train],
alpha = 0)
{
plot(cvModRidge)
abline(v = log(cvModRidge$lambda.min), col = "blue", lty = 2)
}
modRidge = glmnet(x = makeX(tl[train,names(tl)!= "LFACE"]),
y = tl$LFACE[train],
alpha = 0,
lambda = cvModRidge$lambda.min,
standardize = TRUE,
family = "gaussian")
coefplot(modRidge)
Come deducibile la variabile LINCOME è la più influente
su tutti gli modelli. Interessante notare come i modelli ridge e lasso
contengano la variabile MARSTAT mentre il modello di
regressione lineare no, oltretutto il coefficiente è elvato ma
giustificatodal fatto che la variabile può assumere solo valori 0 e
1.
Produco un albero di regressione e stampo il grafico delle variabili piĂą influenti nel modello.
alberoRpart = rpart::rpart(LFACE ~ .,
data = tl,
subset = train,
method = "anova",
control = rpart.control(minsplit = 10, cp = 0.003))
vip::vip(alberoRpart)
Di seguito viene raffigurato il grafico ad albero con un elevato numero
di nodi. Successivamente è presente un grafico rappresentate l’errore
relativo dell’albero al variare del cp (cost complexity
pruning) e si noti come inizialmente la curva decresce per poi
tornare a crescere.
rpart.plot(alberoRpart)
{
plotcp(alberoRpart)
points(x = which.min(alberoRpart$cptable[,"xerror"]),
y = min(alberoRpart$cptable[,"xerror"]),
col = "red",
pch = 18)
}
Il cp ottimale risulta essere 0.025. Stampo, allora, il nuovo albero potato. In questo caso l’albero avrà solamente 4 nodi foglia e verranno utilizzate 2 variabili.
rpart.plot(rpart(LFACE ~ .,
data = tl,
subset = train,
method = "anova",
control = rpart.control(cp = alberoRpart$cptable[which.min(alberoRpart$cptable[,"xerror"]),"CP"])))
Ora eseguo la stessa operazione ma utilizzando la cross-validation come metodo per potare l’albero.
set.seed(1)
alberoTree = tree(LFACE ~ .,
data = tl,
subset = train,
control = tree.control(nobs = length(train),
minsize = 5))
cvAlbero = cv.tree(alberoTree)
cv = data.frame(size = cvAlbero$size, dev = cvAlbero$dev)
ggplot(cv, mapping = aes(x = size,y = dev), size = 10) +
geom_line() +
geom_vline(mapping = aes(xintercept = which.min(dev)), color = "red", linetype = 2)
L’albero migliore risulta essere quello con 10 nodi terminali.
alberoPotato = prune.tree(alberoTree, best = which.min(cv$dev))
{
plot(alberoPotato)
text(alberoPotato)
}
set.seed(1)
calcolaMSE = function(nodiFinali, dati, varDipendente, subsTrain, subsTest)
{
t = prune.tree(tree(get(varDipendente) ~ .,
data = dati,
subset = subsTrain,
control = tree.control(nobs = length(subsTrain),minsize = nodiFinali)),
best = nodiFinali)
return(sqrt(mean((unlist(dati[subsTest,colnames(dati) == varDipendente] - predict(t,newdata = dati[subsTest,])))^2)))
}
mseTrain = sapply(c(2:10), function(X) calcolaMSE(X,dati = tl,varDipendente = "LFACE",subsTrain = train, subsTest = train))
## Warning in prune.tree(tree(get(varDipendente) ~ ., data = dati, subset =
## subsTrain, : best is bigger than tree size
## Warning in prune.tree(tree(get(varDipendente) ~ ., data = dati, subset =
## subsTrain, : best is bigger than tree size
mseTest = sapply(c(2:10), function(X) calcolaMSE(X,dati = tl,varDipendente = "LFACE",subsTrain = c(1:length(tl$AGE))[-train], subsTest = c(1:length(tl$AGE))[-train]))
## Warning in prune.tree(tree(get(varDipendente) ~ ., data = dati, subset =
## subsTrain, : best is bigger than tree size
## Warning in prune.tree(tree(get(varDipendente) ~ ., data = dati, subset =
## subsTrain, : best is bigger than tree size
mseCV = sapply(c(2:10), function(X) calcolaMSE(X,dati = tl,varDipendente = "LFACE",subsTrain = c(1:length(tl$AGE))[train], subsTest = c(1:length(tl$AGE))[-train]))
## Warning in prune.tree(tree(get(varDipendente) ~ ., data = dati, subset =
## subsTrain, : best is bigger than tree size
## Warning in prune.tree(tree(get(varDipendente) ~ ., data = dati, subset =
## subsTrain, : best is bigger than tree size
Nel grafico seguente sono rappresentati gli errori quadratici medi sugli alberi con diversi nodi e diversi dati. La curva train corrisponde ai modelli addestrati sui dati train e validati con i medesimi dati, infatti è la curva con l’MSE minore delle altre. La curva test, invece, corrisponde ai modelli addesstrati sui dati test e validati con i medesimi dati, questa curva è molto simile alla precedente ma tende a essere maggiore in alcuni punti in quanto la dimensione del dataset test è minore di quella del dataset train. La curva CV, corrisponde ai modelli addestrati con i dati train ma validati sui dati test, essa risulta essere la più alta ma anche la più veritiera (meno distorta).
df = tibble(nodi = c(2:10),mseTrain,mseTest,mseCV) %>% na.omit()
ggplot(df,mapping = aes(x = nodi)) +
geom_line(aes(y = mseTrain,
color = "Train")) +
geom_line(aes(y = mseTest,
color = "Test")) +
geom_line(aes(y = mseCV,
color = "CV")) +
labs(y = "MSE", x = "Nodi",color = "Legenda")
Stimo un primo modello MARS.
modMars = earth(LFACE ~ ., data = tl, nprune = 7, degree = 1)
modMars %>% summary
## Call: earth(formula=LFACE~., data=tl, degree=1, nprune=7)
##
## coefficients
## (Intercept) 11.5522454
## h(EDUCATION-14) -1.6308494
## h(EDUCATION-15) 4.1403612
## h(EDUCATION-16) -2.4230637
## h(4-NUMHH) -0.4417816
## h(LINCOME-10.6919) 0.7900094
##
## Selected 6 of 15 terms, and 3 of 5 predictors (nprune=7)
## Termination condition: Reached nk 21
## Importance: LINCOME, NUMHH, EDUCATION, AGE-unused, MARSTATsingle-unused
## Number of terms at each degree of interaction: 1 5 (additive model)
## GCV 2.194151 RSS 556.0856 GRSq 0.3753119 RSq 0.4200775
plot(modMars)
Provo a vedere se includendo delle interazioni migliori il modello. Il miglior modello, secondo \(R^2\), è il modello con 3 gradi d’interazione.
gradi = which.max(sapply(c(1:5), function(d) earth(LFACE ~ ., data = tl, nprune = 7, degree = d) %>% summary %>% .$rsq))
modMars = earth(LFACE ~ ., data = tl, nprune = 7, degree = gradi)
modMars %>% summary
## Call: earth(formula=LFACE~., data=tl, degree=gradi, nprune=7)
##
## coefficients
## (Intercept) 10.7741142
## h(EDUCATION-15) 2.8382491
## h(EDUCATION-16) -1.8479160
## h(4-NUMHH) -0.4034998
## h(LINCOME-10.1266) 1.3528800
## h(EDUCATION-14) * h(LINCOME-9.39266) -0.7683679
## h(EDUCATION-15) * h(LINCOME-10.7144) 0.8783589
##
## Selected 7 of 19 terms, and 3 of 5 predictors (nprune=7)
## Termination condition: Reached nk 21
## Importance: LINCOME, NUMHH, EDUCATION, AGE-unused, MARSTATsingle-unused
## Number of terms at each degree of interaction: 1 4 2
## GCV 2.130717 RSS 519.7476 GRSq 0.393372 RSq 0.4579731
La variabile AGE non viene utilzzita, mentre vengono
considerate alcune interazioni tra le variabili EDUCATION e
LINCOME.
vip::vip(modMars)