Su richiesta dell’azienda Neonatal Health Solutions è stato sviluppato un modello statistico in grado di prevedere il peso dei neonati. Le fasi che hanno portato alla realizzazione del modello sono illustrate in questo documento.
Dopo un’analisi descrittiva dei dati forniti, si effettueranno test statistici al fine di verificare alcune ipotesi richieste dall’azienda committente. Infine, dopo aver costruito il modello e dopo averne valutata la qualità, si svolgeranno predizioni su dati nuovi e si analizzeranno i risultati ottenuti.
Questa sezione è dedicata all’importazione e a una prima esplorazione dei dati.
Iniziamo importando i dati da un file CSV fornito dall’azienda in un DataFrame R chiamato df.
df <- read.csv("neonati.csv", sep = ",", stringsAsFactors = T)
La cella che segue mostra le dimensioni iniziali del dataset.
dim(df)
## [1] 2500 10
Il dataset consiste in 2500 osservazioni, una per ogni neonato, di cui sono state misurate 10 variabili. La cella che segue mostra una panoramica delle variabili in esame.
summary(df)
## Anni.madre N.gravidanze Fumatrici Gestazione
## Min. : 0.00 Min. : 0.0000 Min. :0.0000 Min. :25.00
## 1st Qu.:25.00 1st Qu.: 0.0000 1st Qu.:0.0000 1st Qu.:38.00
## Median :28.00 Median : 1.0000 Median :0.0000 Median :39.00
## Mean :28.16 Mean : 0.9812 Mean :0.0416 Mean :38.98
## 3rd Qu.:32.00 3rd Qu.: 1.0000 3rd Qu.:0.0000 3rd Qu.:40.00
## Max. :46.00 Max. :12.0000 Max. :1.0000 Max. :43.00
## Peso Lunghezza Cranio Tipo.parto Ospedale Sesso
## Min. : 830 Min. :310.0 Min. :235 Ces: 728 osp1:816 F:1256
## 1st Qu.:2990 1st Qu.:480.0 1st Qu.:330 Nat:1772 osp2:849 M:1244
## Median :3300 Median :500.0 Median :340 osp3:835
## Mean :3284 Mean :494.7 Mean :340
## 3rd Qu.:3620 3rd Qu.:510.0 3rd Qu.:350
## Max. :4930 Max. :565.0 Max. :390
Notiamo subito la presenza di anomalie nelle colonne Anni.madre e Fumatrici. Questo e il sospetto della presenza di record anomali ci ha spinto a effettuare delle operazioni di pulizia del dataset, che approfondiremo nella prossima sezione.
In questa sezione ci occuperemo delle operazioni di pulizia del dataset fornito.
Iniziamo dalla conversione della colonna Fumatrici in una variabile categorica. Essa, infatti, è una variabile di controllo binaria che indica se la madre fuma o meno.
df$Fumatrici <- factor(df$Fumatrici, levels = c(0, 1), labels = c("No", "Sì"))
Per quanto riguarda Anni.madre, osserviamo che il valore minimo osservato è 0. Dal momento che è impossibile per una neonata partorire, l’ipotesi è che si tratti di un errore di battitura nell’inserimento dei dati. Escludiamone altri filtrando questi valori e sostituendoli con la mediana dei valori validi, assumendo che il minimo valore valido sia 12 anni.
mediana_anni <- median(df$Anni.madre[df$Anni.madre >= 12], na.rm = TRUE)
df$Anni.madre <- ifelse(df$Anni.madre<12, mediana_anni, df$Anni.madre)
La cella che segue mostra un nuovo riepilogo dei dati validi.
summary(df)
## Anni.madre N.gravidanze Fumatrici Gestazione Peso
## Min. :13.00 Min. : 0.0000 No:2396 Min. :25.00 Min. : 830
## 1st Qu.:25.00 1st Qu.: 0.0000 Sì: 104 1st Qu.:38.00 1st Qu.:2990
## Median :28.00 Median : 1.0000 Median :39.00 Median :3300
## Mean :28.19 Mean : 0.9812 Mean :38.98 Mean :3284
## 3rd Qu.:32.00 3rd Qu.: 1.0000 3rd Qu.:40.00 3rd Qu.:3620
## Max. :46.00 Max. :12.0000 Max. :43.00 Max. :4930
## Lunghezza Cranio Tipo.parto Ospedale Sesso
## Min. :310.0 Min. :235 Ces: 728 osp1:816 F:1256
## 1st Qu.:480.0 1st Qu.:330 Nat:1772 osp2:849 M:1244
## Median :500.0 Median :340 osp3:835
## Mean :494.7 Mean :340
## 3rd Qu.:510.0 3rd Qu.:350
## Max. :565.0 Max. :390
Malgrado questi valori siano plausibili dal punto di vista logico, non ce la sentiamo di escludere la presenza di record che in seguito possono inficiare negativamente il modello. Ci riferiamo, in particolare, alle variabili Gestazione, Lunghezza e Cranio in relazione al Peso.
Le celle che seguono mostrano in un grafico la relazione tra queste tre variabili e il peso del neonato.
plot(df$Gestazione, df$Peso, pch=20)
plot(df$Lunghezza, df$Peso, pch=20)
plot(df$Cranio, df$Peso, pch=20)
Notiamo la presenza di punti isolati che si staccano dal corpo principale dei dati. In particolare, nel grafico Gestazione-Peso, evidenziamo un neonato alla 35° settimana con peso intorno ai 4500 grammi, così come un neonato dello stesso peso lungo poco più di 30 centimetri nel grafico Lunghezza-Peso o anche i due neonati sotto i 2 kg di peso con una circonferenza cranica superiore ai 35 cm.
Per capire come comportarci, dobbiamo isolare questi valori. Innanzitutto, aggiungiamo le colonne Lunghezza_CL e Cranio_CL, in modo da dividere in fasce la lunghezza e il cranio dei neonati.
min_limit <- round(min(df$Lunghezza), digits = -2)
max_limit <- round(max(df$Lunghezza), digits = -1)
df$Lunghezza_CL <- cut(df$Lunghezza, breaks = seq(min_limit, max_limit+20, 20))
min_limit <- round(min(df$Cranio), digits = -1)
max_limit <- round(max(df$Cranio), digits = -1)
df$Cranio_CL <- cut(df$Cranio, breaks = seq(min_limit-10, max_limit, 10))
La funzione is_outlier, definita nella cella che segue, prende in ingresso un dataframe ed etichetta come outlier i pesi che si trovano al di sotto del primo percentile e al di sopra del 99° percentile.
is_outlier <- function(dati) {
if(nrow(dati) < 5) {
dati$is_outlier <- FALSE
return(dati)
}
s_bassa <- quantile(dati$Peso, 0.01)
s_alta <- quantile(dati$Peso, 0.99)
dati$is_outlier <- dati$Peso < s_bassa | dati$Peso > s_alta
return(dati)
}
Per le future fasi di pulizia dei dati, creiamo la colonna ID che identifica con un numero ciascun record.
df$ID <- 1:nrow(df)
Dividiamo i dati in base ai valori di Gestazione, Lunghezza_CL e Cranio_CL e applichiamo la funzione is_outlier, per poi salvare i valori anomali nel DataFrame df_outlier.
Gestazione_list <- split(df, df$Gestazione)
lista_processata <- lapply(Gestazione_list, is_outlier)
df <- do.call(rbind, lista_processata)
df_outlier <- df[df$is_outlier==T, ]
Lunghezza_list <- split(df, df$Lunghezza_CL)
lista_processata <- lapply(Lunghezza_list, is_outlier)
df <- do.call(rbind, lista_processata)
df_outlier <- rbind(df_outlier, df[df$is_outlier==T,])
Cranio_list <- split(df, df$Cranio_CL)
lista_processata <- lapply(Cranio_list, is_outlier)
df <- do.call(rbind, lista_processata)
df_outlier <- rbind(df_outlier, df[df$is_outlier==T,])
Rimuoviamo le colonne create finora tranne ID ed estraiamo i valori unici.
df_outlier$is_outlier <- NULL
df_outlier$Lunghezza_CL <- NULL
df_outlier$Cranio_CL <- NULL
df_outlier <- unique(df_outlier)
I tre grafici che seguono mostrano gli outlier (in rosso) all’interno del dataset completo.
plot(df$Gestazione, df$Peso, pch=20, col=rgb(0,0,0,0.2))
points(df_outlier$Gestazione, df_outlier$Peso, col="red", pch=19, cex=1.2)
plot(df$Lunghezza, df$Peso, pch=20, col=rgb(0,0,0,0.2))
points(df_outlier$Lunghezza, df_outlier$Peso, col="red", pch=19, cex=1.2)
plot(df$Cranio, df$Peso, pch=20, col=rgb(0,0,0,0.2))
points(df_outlier$Cranio, df_outlier$Peso, col="red", pch=19, cex=1.2)
La pulizia dei dati non solo ha individuato i punti anomali già segnalati nelle prime visualizzazioni, ma ha riguardato tutti quei record che si trovano intorno alla massa principale di punti. Quantifichiamo la percentuale di questi record rispetto al dataset intero nella cella che segue.
nrow(df_outlier); (nrow(df_outlier)/nrow(df))*100
## [1] 150
## [1] 6
Gli outlier così individuati sono 150, che costituiscono il 6% dell’intero dataset. Vediamo un riepilogo di questi dati.
summary(df_outlier)
## Anni.madre N.gravidanze Fumatrici Gestazione Peso
## Min. :15.00 Min. :0.000 No:147 Min. :27.00 Min. : 990
## 1st Qu.:24.00 1st Qu.:0.000 Sì: 3 1st Qu.:37.00 1st Qu.:2265
## Median :29.00 Median :1.000 Median :39.00 Median :2775
## Mean :28.45 Mean :1.033 Mean :37.86 Mean :3085
## 3rd Qu.:32.00 3rd Qu.:1.000 3rd Qu.:40.00 3rd Qu.:4068
## Max. :42.00 Max. :7.000 Max. :42.00 Max. :4930
## Lunghezza Cranio Tipo.parto Ospedale Sesso ID
## Min. :315.0 Min. :267.0 Ces: 40 osp1:49 F:80 Min. : 29.0
## 1st Qu.:456.2 1st Qu.:315.0 Nat:110 osp2:59 M:70 1st Qu.: 727.8
## Median :480.0 Median :332.5 osp3:42 Median :1374.5
## Mean :480.2 Mean :332.6 Mean :1316.5
## 3rd Qu.:515.0 3rd Qu.:351.0 3rd Qu.:1868.8
## Max. :560.0 Max. :390.0 Max. :2478.0
Questo sottoinsieme è bilanciato e sensato dal punto di vista delle variabili di controllo e, per il criterio con cui è stato estratto dal dataset, mostra una variabilità nelle colonne quantitative. La nostra soluzione a beneficio del futuro modello è di eliminare questi dati, dal momento che l’informazione necessaria è stata comunque mantenuta nel dataset principale.
df$is_outlier <- NULL
df$Cranio_CL <- NULL
df$Lunghezza_CL <- NULL
df <- df[!(df$ID %in% df_outlier$ID), ]
df$ID <- NULL
Ora che abbiamo un dataset quanto più pulito e informativo possibile, possiamo procedere con le prossime fasi.
Questa sezione, dedicata a un’esplorazione più approfondita del dataset, sarà divisa in tre sezioni.
Nella prima ci occuperemo della variabile target del modello, nella seconda esploreremo la relazione delle variabili quantitative con il target, mentre nella terza verificheremo la relazione tra le variabili qualitative e il target.
Iniziamo questa sezione dedicata alla colonna Peso calcolando la media e la deviazione standard del target.
mu <- mean(df$Peso, na.rm = TRUE)
sigma <- sd(df$Peso, na.rm = TRUE)
La cella che segue confronta in un grafico la distribuzione del peso dei neonati con una distribuzione normale avente stessa media e stessa deviazione standard.
library(ggplot2)
## Warning: package 'ggplot2' was built under R version 4.4.3
ggplot(df, aes(x = Peso)) +
geom_density(fill = "grey", alpha = 0.3, color = "black", linewidth = 0.8) +
stat_function(fun = dnorm,
args = list(mean = mu, sd = sigma),
color = "red",
linewidth = 1) +
theme_minimal() +
labs(title = "Confronto tra Densità del Peso e Distribuzione Normale",
x = "Peso (grammi)",
y = "Densità") +
annotate("text", x = mu + 2*sigma, y = 0.0001, label = "Normale Teorica", color = "red", hjust = 0)
Osserviamo una lieve asimmetria verso i valori più alti di peso e una campana più alta rispetto alla distribuzione normale. Inoltre, la distribuzione del peso presenta una coda a sinistra del grafico rappresentata dai neonati più piccoli.
Verifichiamo l’asimmetria e la curtosi della distribuzione nella cella che segue.
library(moments)
skewness(df$Peso); kurtosis(df$Peso)-3
## [1] -0.7183713
## [1] 2.506875
Come anticipato, la distribuzione è asimmetrica negativa e leptocurtica rispetto alla normale. Con questi valori, sospettiamo che la distribuzione della popolazione del peso non sia una normale come richiederebbero le assunzioni del modello che utilizzeremo.
Verifichiamolo con un test di Shapiro-Wilk, che ha come ipotesi nulla che la distribuzione del peso sia normale.
shapiro.test(df$Peso)
##
## Shapiro-Wilk normality test
##
## data: df$Peso
## W = 0.96676, p-value < 2.2e-16
Il p-value del test è pressoché nullo, il che ci porta a rifiutare l’ipotesi di normalità. Ciononostante, valuteremo comunque un modello statistico di regressione multipla prima di optare per un’eventuale trasformazione della colonna target.
Questa sottosezione è dedicata alle variabili quantitative e alla loro relazione con il target.
Nella cella che segue, raccogliamo in un elenco le variabili di nostro interesse.
cols_num <- c("Anni.madre", "N.gravidanze", "Gestazione", "Peso", "Lunghezza", "Cranio")
Costruiamo la matrice di correlazione tra le variabili quantitative e mostriamola con una mappa termica.
library(reshape2)
corr_matrix <- cor(df[, cols_num], use = "complete.obs")
melted_corr <- melt(corr_matrix)
ggplot(data = melted_corr, aes(x = Var1, y = Var2, fill = value)) +
geom_tile(color = "white") +
scale_fill_gradient2(low = "red", high = "blue", mid = "white",
midpoint = 0, limit = c(-1,1), space = "Lab",
name="Correlazione") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, vjust = 1, hjust = 1)) +
coord_fixed() +
geom_text(aes(label = round(value, 2)), color = "black", size = 3) +
labs(title = "Matrice di Correlazione Variabili Cliniche", x = "", y = "")
Concentrandoci su Peso, notiamo una scarsa correlazione con l’età della madre e le gravidanze pregresse, mentre una correlazione più forte le abbiamo con le misure antropometriche e la durata della gestazione. Inoltre, osserviamo una correlazione tra lunghezza e cranio che ci fa sospettare che le due colonne indichino in realtà la stessa caratteristica, ovvero la taglia del bambino. Valuteremo se e come eliminare una delle due colonne nella sezione dedicata alla costruzione del modello.
Questa sottosezione è dedicata alle variabili di controllo e alla loro relazione con il target.
Iniziamo dal fumo materno, come anche richiesto dall’azienda. La cella che segue mostra un boxplot della variabile target in base ai valori della colonna Fumatrici.
boxplot(df$Peso~df$Fumatrici)
Al di là della presenza di outlier nel boxplot delle non fumatrici, osserviamo un range di valori più ristretto tra le fumatrici, oltre che un range interquartile più spostato verso il basso. Verifichiamo se il fumo materno incide sul peso tramite un T test, che assume come ipotesi nulla che le medie di peso dei due sottocampioni siano significativamente uguali.
t.test(df$Peso~df$Fumatrici)
##
## Welch Two Sample t-test
##
## data: df$Peso by df$Fumatrici
## t = 1.7693, df = 110.67, p-value = 0.07959
## alternative hypothesis: true difference in means between group No and group Sì is not equal to 0
## 95 percent confidence interval:
## -9.466406 167.249641
## sample estimates:
## mean in group No mean in group Sì
## 3300.179 3221.287
Il valore del p-value ci porta a rifiutare l’ipotesi nulla, ma non il senso comune. Il sospetto è che il fumo influenzi altre variabili quantitative che, a loro volta, influenzano il peso, senza tuttavia che queste due variabili siano correlate tra loro.
Per verificarlo, creiamo una tabella che raccoglie i p-value del T test eseguito sulle variabili quantitative influenzate dal fumo e ordiniamo questi valori in modo crescente.
p_values <- data.frame(cols = c(), p_vals = c())
for(i in 1:length(cols_num)){
test_num <- t.test(df[[cols_num[i]]]~df$Fumatrici)
p_values <- rbind(p_values, list(cols=cols_num[i], p_vals=test_num$p.value))
}
p_values[order(p_values$p_vals),]
## cols p_vals
## 2 N.gravidanze 0.02239669
## 5 Lunghezza 0.03949287
## 4 Peso 0.07959402
## 3 Gestazione 0.08330045
## 6 Cranio 0.39581625
## 1 Anni.madre 0.82515572
Se escludiamo le colonne con p-value superiore a 0.05 (e che di conseguenza non rifiutano l’ipotesi nulla), notiamo che il fumo materno potrebbe essere correlato a Lunghezza e a N.gravidanze. Sull’influenza del fumo nella sezione dedicata alla costruzione del modello.
Proseguiamo con le variabili qualitative tracciando i boxplot del peso in base al tipo di parto eseguito.
boxplot(df$Peso~df$Tipo.parto)
I due grafici sono pressoché identici, il che ci porta a concludere che non ci sia differenza significativa nel tipo di parto.
In modo analogo, tracciamo il boxplot del peso in base all’ospedale del circuito in cui è avvenuta la nascita.
boxplot(df$Peso~df$Ospedale)
Anche qui, non rileviamo alcuna differenza significativa tra i boxplot, il che ci porta a supporre che l’ospedale non abbia alcun ruolo nel peso del neonato.
Concludiamo l’analisi delle variabili qualitative con il sesso del bambino.
boxplot(df$Peso~df$Sesso)
Notiamo che il boxplot dei maschi è spostato verso i valori più elevati e che la sua mediana coincide con il terzo quartile del boxplot delle femmine. Questo suggerisce quindi una differenza significativa tra le medie dei pesi che possiamo verificare tramite il T test.
t.test(df$Peso~df$Sesso)
##
## Welch Two Sample t-test
##
## data: df$Peso by df$Sesso
## t = -11.919, df = 2345.8, p-value < 2.2e-16
## alternative hypothesis: true difference in means between group F and group M is not equal to 0
## 95 percent confidence interval:
## -260.9943 -187.2470
## sample estimates:
## mean in group F mean in group M
## 3184.823 3408.944
Il valore del p-value ci porta ad accettare come ipotesi alternativa una significativa differenza di peso in media tra maschi e femmine e, quindi, un’influenza del sesso del bambino sul peso.
Riassumendo, delle variabili di controllo esaminate è più probabile che manterremo Sesso, mentre di Fumatrici si potrebbe valutare un’interazione con altre variabili.
Questa sezione è dedicata all’esecuzione di test statistici per la verifica delle seguenti ipotesi richieste dall’azienda committente:
A ciascuna di queste ipotesi sarà dedicata una sottosezione di questo documento.
Questa sottosezione verifica l’ipotesi per cui in certi ospedali si preferisca praticare un determinato tipo di parto. Questa ipotesi equivale a quella per cui le variabili Ospedale e Tipo.parto siano dipendenti tra loro.
Il primo passo è stato ottenere una tabella di contingenza dalle colonne Ospedale e Tipo.parto. I risultati sono riepilogati nel balloon plot della cella seguente.
osservazioni <- table(df$Ospedale,df$Tipo.parto)
ggpubr::ggballoonplot(data = as.data.frame(osservazioni))
Osserviamo che la prevalenza del parto naturale rispetto al cesareo si mantiene per tutti e tre gli ospedali e che, anzi, nell’ospedale 3 i parti cesarei sono addirittura meno frequenti rispetto agli altri due ospedali. Graficamente, quindi, possiamo supporre che non esiste associazione tra le due variabili in esame.
Verifichiamo numericamente questa supposizione costruendo la tabella delle osservazioni attese.
attese <- osservazioni
for(i in 1:nrow(osservazioni)){
for(j in 1:ncol(osservazioni)){
attese[i,j] <- table(df$Ospedale)[i]*table(df$Tipo.parto)[j]/nrow(df)
}
}
La cella che segue calcola il valore della statistica \(X^2\) di Pearson sulla tabella di contingenza e sulle frequenze attese.
X2 <- sum((osservazioni-attese)^2/attese)
Il grafico seguente mostra dove si posiziona questo valore (in verde) nella distribuzione \(\chi^2\) con i gradi di libertà ottenuti dal numero di righe e di colonne della tabella di contingenza. La retta tratteggiata di rosso delimita la zona del grafico oltre la quale rifiutiamo l’ipotesi nulla di indipendenza.
plot(density(rchisq(nrow(df), df=(ncol(osservazioni)-1)*(nrow(osservazioni)-1))),main = "Relazione tra ospedale e tipo di parto")
abline(v=qchisq(0.95, df=(ncol(osservazioni)-1)*(nrow(osservazioni)-1)),col="red",lty=2)
abline(v=X2, col="green")
Osserviamo che il valore ottenuto ricade nella zona di accettazione, pertanto possiamo accettare l’ipotesi nulla per cui Ospedale e Tipo.parto sono due variabili indipendenti. I risultati ottenuti sono confermati dal test \(\chi^2\) nella cella seguente, dove il p-value supera la significatività al \(5\%\).
chisq.test(osservazioni)
##
## Pearson's Chi-squared test
##
## data: osservazioni
## X-squared = 1.7949, df = 2, p-value = 0.4076
In questa sottosezione verificheremo se la media di lunghezza e peso del campione siano una buona stima delle medie della popolazione. Queste medie, impostate nella cella che segue, sono state ottenute dal WHO Child Growth Standards (2006) e dal Intergrowth-21st Project (University of Oxford).
LUNGHEZZA.POP <- 495
PESO.POP <- 3300
Dal momento che non conosciamo la varianza delle due variabili nella popolazione, opteremo per un T test a due code con una significatività del \(5\%\).
La cella seguente effettua il test su Lunghezza e ne memorizza i risultati.
t.lunghezza <- t.test(df$Lunghezza, mu = LUNGHEZZA.POP, conf.level = 0.95, alternative = "two.sided")
Il grafico che segue mostra la posizione dei risultati ottenuti nel test (in verde) all’interno della distribuzione T di Student.
plot(density(rt(nrow(df), df = nrow(df)-1)), main="Differenza tra media della popolazione e media campionaria")
abline(v=c(qt(0.025,df=nrow(df)-1), qt(0.975,df=nrow(df)-1)),col="red",lty=2)
abline(v=qt(t.lunghezza$p.value, df=nrow(df)-1), col="green")
Il valore ottenuto dal test ricade nella zona di accettazione delimitata dalle due rette rosse, pertanto non rifiutiamo l’ipotesi nulla per cui la media del campione è una buona stima della media della popolazione.
In modo analogo, eseguiamo lo stesso tipo di test sulla colonna Peso.
t.peso <- t.test(df$Peso, mu = PESO.POP, conf.level = 0.95, alternative = "two.sided")
Il grafico nella cella seguente mostra graficamente i risultati del test.
plot(density(rt(nrow(df), df = nrow(df)-1)), main = "Differenza tra media della popolazione e media campionaria")
abline(v=c(qt(0.025,df=nrow(df)-1), qt(0.975,df=nrow(df)-1)),col="red",lty=2)
abline(v=qt(t.peso$p.value, df=nrow(df)-1), col="green")
Anche qui, il valore ottenuto dal test ricade nella zona di accettazione, perciò non possiamo rifiutare l’ipotesi nulla per cui la media stimata dal campione è significativamente uguale a quella della popolazione.
In questa sottosezione, testiamo l’associazione tra sesso e misure antropometriche dei neonati.
Riportiamo nella cella che segue le prime cinque righe delle colonne Sesso, Peso, Lunghezza e Cranio.
head(df[c("Sesso","Peso","Lunghezza","Cranio")],5)
## Sesso Peso Lunghezza Cranio
## (230,240] F 930 355 235
## (240,250] F 930 345 245
## (250,260].(300,320].28.928 F 830 310 254
## (250,260].(320,340].25 F 900 325 253
## (260,270].(300,320].27.2437 M 980 320 265
Ripetiamo il test T eseguito sulla variabile Peso nella sezione precedente. Ricordiamo che il p-value di questo test ci aveva spinto a rifiutare l’ipotesi nulla a favore dell’alternativa, per cui la media del peso era influenzata dal sesso.
t.test(df$Peso~df$Sesso)
##
## Welch Two Sample t-test
##
## data: df$Peso by df$Sesso
## t = -11.919, df = 2345.8, p-value < 2.2e-16
## alternative hypothesis: true difference in means between group F and group M is not equal to 0
## 95 percent confidence interval:
## -260.9943 -187.2470
## sample estimates:
## mean in group F mean in group M
## 3184.823 3408.944
In modo analogo, eseguiamo lo stesso tipo di test su Lunghezza e Cranio. I risultati sono riportati nelle due celle seguenti.
t.test(df$Lunghezza~df$Sesso)
##
## Welch Two Sample t-test
##
## data: df$Lunghezza by df$Sesso
## t = -9.179, df = 2329.4, p-value < 2.2e-16
## alternative hypothesis: true difference in means between group F and group M is not equal to 0
## 95 percent confidence interval:
## -10.927112 -7.080099
## sample estimates:
## mean in group F mean in group M
## 491.1165 500.1201
t.test(df$Cranio~df$Sesso)
##
## Welch Two Sample t-test
##
## data: df$Cranio by df$Sesso
## t = -6.6581, df = 2344.7, p-value = 3.442e-11
## alternative hypothesis: true difference in means between group F and group M is not equal to 0
## 95 percent confidence interval:
## -5.474464 -2.983421
## sample estimates:
## mean in group F mean in group M
## 338.3895 342.6184
I due test, pur essendo eseguiti su colonne diverse, ci portano a concludere che le misure antropometriche dei neonati sono condizionate dal loro sesso e pertanto terremo in considerazione l’interazione tra queste variabili nel modello.
In questa sezione ci occuperemo della costruzione del modello seguendo una procedura di tipo stepwise backward.
Iniziamo con un modello baseline creato in modo automatico nella cella che segue e visualizziamone i risultati principali.
model_base <- MASS::stepAIC(lm(Peso~.,data=df,direction="both",k=2))
## Warning: In lm.fit(x, y, offset = offset, singular.ok = singular.ok, ...) :
## extra arguments 'direction', 'k' will be disregarded
## Start: AIC=25726.77
## Peso ~ Anni.madre + N.gravidanze + Fumatrici + Gestazione + Lunghezza +
## Cranio + Tipo.parto + Ospedale + Sesso
##
## Df Sum of Sq RSS AIC
## - Anni.madre 1 23932 132296117 25725
## <none> 132272185 25727
## - Fumatrici 1 127286 132399471 25727
## - Tipo.parto 1 244850 132517035 25729
## - Ospedale 2 419589 132691774 25730
## - N.gravidanze 1 436395 132708581 25732
## - Sesso 1 3305421 135577606 25783
## - Gestazione 1 3854058 136126243 25792
## - Cranio 1 31542477 163814662 26227
## - Lunghezza 1 70691331 202963516 26731
## Warning: In lm.fit(x, y, offset = offset, singular.ok = singular.ok, ...) :
## extra arguments 'direction', 'k' will be disregarded
##
## Step: AIC=25725.2
## Peso ~ N.gravidanze + Fumatrici + Gestazione + Lunghezza + Cranio +
## Tipo.parto + Ospedale + Sesso
##
## Df Sum of Sq RSS AIC
## <none> 132296117 25725
## - Fumatrici 1 128284 132424401 25726
## - Tipo.parto 1 246422 132542539 25728
## - Ospedale 2 420749 132716866 25729
## - N.gravidanze 1 595674 132891791 25734
## - Sesso 1 3314745 135610863 25781
## - Gestazione 1 3832204 136128322 25790
## - Cranio 1 31721415 164017532 26228
## - Lunghezza 1 70672846 202968964 26729
summary(model_base)
##
## Call:
## lm(formula = Peso ~ N.gravidanze + Fumatrici + Gestazione + Lunghezza +
## Cranio + Tipo.parto + Ospedale + Sesso, data = df, direction = "both",
## k = 2)
##
## Residuals:
## Min 1Q Median 3Q Max
## -694.07 -168.15 -12.77 155.52 875.51
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -6340.7298 129.7062 -48.885 < 2e-16 ***
## N.gravidanze 12.6507 3.8974 3.246 0.00119 **
## FumatriciSì -36.5991 24.2968 -1.506 0.13212
## Gestazione 29.6410 3.6003 8.233 3.00e-16 ***
## Lunghezza 10.3783 0.2935 35.356 < 2e-16 ***
## Cranio 9.6135 0.4059 23.687 < 2e-16 ***
## Tipo.partoNat 22.5496 10.8010 2.088 0.03693 *
## Ospedaleosp2 -19.3756 12.0646 -1.606 0.10841
## Ospedaleosp3 13.0990 12.0467 1.087 0.27699
## SessoM 76.5557 9.9981 7.657 2.76e-14 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 237.8 on 2340 degrees of freedom
## Multiple R-squared: 0.7442, Adjusted R-squared: 0.7433
## F-statistic: 756.6 on 9 and 2340 DF, p-value: < 2.2e-16
Questo modello non può essere preso in considerazione come candidato. L’intercetta è impossibile e le variabili generate da Ospedale hanno una significatività diversa l’una dall’altra, per non parlare delle ipotesi formulate in precedenza contraddette dai regressori individuati dal modello.
Procediamo quindi manualmente e iniziamo definendo la funzione collect_results, che dato un modello in ingresso raccoglie i dati di BIC, percentuale di varianza spiegata e radice dell’errore quadratico medio.
collect_results <- function(model_name,model){
return(list(
name=model_name,
bic = BIC(model),
r_adj = summary(model)$adj.r.squared,
rmse = sqrt(mean(model$residuals^2))
))
}
Iniziamo dal primo modello, che contiene tutte le variabili. Il dataframe results salverà i risultati ottenuti dai modelli per le nostre considerazioni finali.
model_1 <- lm(Peso~., data=df)
results <- data.frame(collect_results("model_1",model_1))
summary(model_1)
##
## Call:
## lm(formula = Peso ~ ., data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -695.52 -167.80 -12.15 156.15 873.62
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -6363.9873 134.5585 -47.295 < 2e-16 ***
## Anni.madre 0.6705 1.0307 0.651 0.51541
## N.gravidanze 11.6464 4.1925 2.778 0.00551 **
## FumatriciSì -36.4578 24.3008 -1.500 0.13368
## Gestazione 29.8809 3.6195 8.255 2.50e-16 ***
## Lunghezza 10.3802 0.2936 35.356 < 2e-16 ***
## Cranio 9.5996 0.4065 23.617 < 2e-16 ***
## Tipo.partoNat 22.4787 10.8029 2.081 0.03756 *
## Ospedaleosp2 -19.4991 12.0676 -1.616 0.10627
## Ospedaleosp3 12.9107 12.0516 1.071 0.28415
## SessoM 76.4568 10.0005 7.645 3.02e-14 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 237.8 on 2339 degrees of freedom
## Multiple R-squared: 0.7443, Adjusted R-squared: 0.7432
## F-statistic: 680.8 on 10 and 2339 DF, p-value: < 2.2e-16
Come atteso anche dalle considerazioni iniziali, le variabili meno significative per questo modello sono l’età della madre, il fumo e l’ospedale dove è avvenuto il parto, mentre il numero di gravidanze pregresse e il tipo di parto hanno una significatività superiore a quella prevista.
Creiamo un secondo modello, ottenuto dal primo togliendo Anni.madre.
model_2 <- update(model_1, ~.-Anni.madre)
results <- rbind(results, collect_results("model_2",model_2))
summary(model_2)
##
## Call:
## lm(formula = Peso ~ N.gravidanze + Fumatrici + Gestazione + Lunghezza +
## Cranio + Tipo.parto + Ospedale + Sesso, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -694.07 -168.15 -12.77 155.52 875.51
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -6340.7298 129.7062 -48.885 < 2e-16 ***
## N.gravidanze 12.6507 3.8974 3.246 0.00119 **
## FumatriciSì -36.5991 24.2968 -1.506 0.13212
## Gestazione 29.6410 3.6003 8.233 3.00e-16 ***
## Lunghezza 10.3783 0.2935 35.356 < 2e-16 ***
## Cranio 9.6135 0.4059 23.687 < 2e-16 ***
## Tipo.partoNat 22.5496 10.8010 2.088 0.03693 *
## Ospedaleosp2 -19.3756 12.0646 -1.606 0.10841
## Ospedaleosp3 13.0990 12.0467 1.087 0.27699
## SessoM 76.5557 9.9981 7.657 2.76e-14 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 237.8 on 2340 degrees of freedom
## Multiple R-squared: 0.7442, Adjusted R-squared: 0.7433
## F-statistic: 756.6 on 9 and 2340 DF, p-value: < 2.2e-16
In questo secondo modello è lievemente aumentata la percentuale di varianza spiegata, ma mantiene la scarsa significatività di Fumatrici e di Ospedale. Quest’ultima variabile viene rimossa dal terzo modello.
model_3 <- update(model_2, ~.-Ospedale)
results <- rbind(results, collect_results("model_3",model_3))
summary(model_3)
##
## Call:
## lm(formula = Peso ~ N.gravidanze + Fumatrici + Gestazione + Lunghezza +
## Cranio + Tipo.parto + Sesso, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -691.8 -169.7 -11.4 156.8 876.7
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -6342.3275 129.7323 -48.888 < 2e-16 ***
## N.gravidanze 13.0113 3.8996 3.337 0.000862 ***
## FumatriciSì -37.3597 24.3212 -1.536 0.124649
## Gestazione 29.8648 3.6035 8.288 < 2e-16 ***
## Lunghezza 10.3469 0.2936 35.237 < 2e-16 ***
## Cranio 9.6290 0.4063 23.700 < 2e-16 ***
## Tipo.partoNat 23.3233 10.8094 2.158 0.031054 *
## SessoM 76.9900 10.0084 7.693 2.11e-14 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 238.1 on 2342 degrees of freedom
## Multiple R-squared: 0.7434, Adjusted R-squared: 0.7427
## F-statistic: 969.5 on 7 and 2342 DF, p-value: < 2.2e-16
A fronte della diminuzione della percentuale di varianza spiegata, abbiamo ottenuto un aumento della significatività della variabile N.gravidanze.
Nel prossimo modello, rimuoviamo la variabile Fumatrici.
model_4 <- update(model_3, ~.-Fumatrici)
results <- rbind(results, collect_results("model_4",model_4))
summary(model_4)
##
## Call:
## lm(formula = Peso ~ N.gravidanze + Gestazione + Lunghezza + Cranio +
## Tipo.parto + Sesso, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -690.27 -169.93 -10.53 157.93 879.58
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -6342.8834 129.7694 -48.878 < 2e-16 ***
## N.gravidanze 12.7180 3.8961 3.264 0.00111 **
## Gestazione 29.4824 3.5959 8.199 3.95e-16 ***
## Lunghezza 10.3733 0.2932 35.377 < 2e-16 ***
## Cranio 9.6333 0.4064 23.704 < 2e-16 ***
## Tipo.partoNat 22.9745 10.8102 2.125 0.03367 *
## SessoM 76.6880 10.0093 7.662 2.67e-14 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 238.1 on 2343 degrees of freedom
## Multiple R-squared: 0.7432, Adjusted R-squared: 0.7425
## F-statistic: 1130 on 6 and 2343 DF, p-value: < 2.2e-16
In questo modello abbiamo ottenuto almeno una stella in tutte le variabili rimaste, pur con un’ulteriore diminuzione della variabilità spiegata.
Aggiorniamo questo modello aggiungendo l’iterazione del fumo materno con la lunghezza del bambino e le gravidanze pregresse, ossia le variabili più correlate a Fumatrici nel dataset.
model_5 <- update(model_4, ~.+ Lunghezza*Fumatrici + N.gravidanze*Fumatrici)
results <- rbind(results, collect_results("model_5",model_5))
summary(model_5)
##
## Call:
## lm(formula = Peso ~ N.gravidanze + Gestazione + Lunghezza + Cranio +
## Tipo.parto + Sesso + Fumatrici + Lunghezza:Fumatrici + N.gravidanze:Fumatrici,
## data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -691.75 -170.24 -11.32 157.53 873.66
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -6322.4557 130.7150 -48.368 < 2e-16 ***
## N.gravidanze 13.2794 3.9804 3.336 0.000862 ***
## Gestazione 29.9602 3.6049 8.311 < 2e-16 ***
## Lunghezza 10.2972 0.2960 34.785 < 2e-16 ***
## Cranio 9.6321 0.4063 23.706 < 2e-16 ***
## Tipo.partoNat 23.3844 10.8101 2.163 0.030627 *
## SessoM 76.3527 10.0231 7.618 3.72e-14 ***
## FumatriciSì -787.8833 589.8946 -1.336 0.181799
## Lunghezza:FumatriciSì 1.5400 1.1925 1.291 0.196687
## N.gravidanze:FumatriciSì -5.2490 19.7496 -0.266 0.790435
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 238.1 on 2340 degrees of freedom
## Multiple R-squared: 0.7436, Adjusted R-squared: 0.7426
## F-statistic: 754.2 on 9 and 2340 DF, p-value: < 2.2e-16
Pur essendo aumentata la variabilità spiegata e la significatività di N.gravidanze, notiamo che le nuove variabili inserite non sono per nulla significative per il modello.
Esaminiamo ulteriormente questo modello calcolando il VIF delle variabili coinvolte.
car::vif(model_5)
## there are higher-order terms (interactions) in this model
## consider setting type = 'predictor'; see ?vif
## N.gravidanze Gestazione Lunghezza
## 1.068715 1.650505 2.126198
## Cranio Tipo.parto Sesso
## 1.651854 1.003331 1.041461
## Fumatrici Lunghezza:Fumatrici N.gravidanze:Fumatrici
## 593.501629 586.827251 2.106994
Le nuove variabili introdotte presentano un problema di estrema multicollinearità che ci portano a scartare sin da subito questo modello e a riprendere la costruzione dei nostri candidati dal modello 4.
Concludiamo derivando due ulteriori modelli dal modello 4:
il modello 6 aggiorna il modello rimuovendo la variabile Lunghezza;
il modello 7 aggiorna il modello rimuovendo la variabile Cranio.
Questo perché, come accennato nell’analisi esplorativa, le due variabili hanno una moderata correlazione positiva e indicano in modo analogo la taglia del bambino. Otteniamo quindi i modelli sopra descritti nelle celle che seguono.
model_6 <- update(model_4, ~.-Lunghezza)
results <- rbind(results, collect_results("model_6",model_6))
summary(model_6)
##
## Call:
## lm(formula = Peso ~ N.gravidanze + Gestazione + Cranio + Tipo.parto +
## Sesso, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -914.5 -208.5 -7.3 198.6 846.8
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -5743.0161 159.3213 -36.047 <2e-16 ***
## N.gravidanze 3.4034 4.8137 0.707 0.480
## Gestazione 87.2764 3.9669 22.001 <2e-16 ***
## Cranio 16.3413 0.4451 36.712 <2e-16 ***
## Tipo.partoNat 7.3725 13.3756 0.551 0.582
## SessoM 117.5309 12.3123 9.546 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 294.9 on 2344 degrees of freedom
## Multiple R-squared: 0.606, Adjusted R-squared: 0.6052
## F-statistic: 721 on 5 and 2344 DF, p-value: < 2.2e-16
model_7 <- update(model_4, ~.-Cranio)
results <- rbind(results, collect_results("model_7",model_7))
summary(model_7)
##
## Call:
## lm(formula = Peso ~ N.gravidanze + Gestazione + Lunghezza + Tipo.parto +
## Sesso, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -804.56 -184.16 -16.77 180.99 1215.64
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -5186.7651 133.8724 -38.744 < 2e-16 ***
## N.gravidanze 21.6354 4.3170 5.012 5.80e-07 ***
## Gestazione 42.2843 3.9577 10.684 < 2e-16 ***
## Lunghezza 13.6163 0.2887 47.161 < 2e-16 ***
## Tipo.partoNat 30.4926 12.0291 2.535 0.0113 *
## SessoM 82.2782 11.1396 7.386 2.09e-13 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 265.1 on 2344 degrees of freedom
## Multiple R-squared: 0.6816, Adjusted R-squared: 0.6809
## F-statistic: 1003 on 5 and 2344 DF, p-value: < 2.2e-16
Nel modello 6, la rimozione di Lunghezza ha portato a un drastico calo della variabilità spiegata e alla perdita di significatività delle gravidanze pregresse e del tipo di parto. Dall’altra parte, il modello 7 ha consolidato la significatività delle variabili rimaste al netto di una minore perdita di variabilità spiegata.
Questa sezione è dedicata alla selezione e all’analisi del modello migliore tra i candidati costruiti in precedenza.
Mostriamo allora la tabella results ottenuta nella sezione precedente.
results
## name bic r_adj rmse
## 1 model_1 32466.93 0.7432005 237.2468
## 2 model_2 32459.59 0.7432638 237.2682
## 3 model_3 32451.53 0.7426672 237.6452
## 4 model_4 32446.14 0.7425179 237.7649
## 5 model_5 32465.21 0.7426491 237.5521
## 6 model_6 33444.12 0.6051527 294.4974
## 7 model_7 32943.54 0.6809049 264.7446
Notiamo che dopo il quinto modello (quello dell’iterazione del fumo materno), l’errore medio dei candidati è aumentato insieme al BIC, mentre la variabilità spiegata è scesa drasticamente sotto il 70%.
Mostriamo questi valori sui grafici, contrassegnando i modelli in cui raggiungono l’ottimo (il minimo nel BIC e nell’errore medio, il massimo nell’R aggiustato).
indice_ottimo<- which.min(results$bic)
valore_ottimo <- min(results$bic)
plot(results$bic, type = "o", pch = 20, col = "darkgray", xlab = "Modelli", ylab = "BIC", main = "Selezione del Modello tramite BIC")
points(indice_ottimo, valore_ottimo, col = "green2", pch = 20, cex = 2)
indice_ottimo<- which.max(results$r_adj)
valore_ottimo <- max(results$r_adj)
plot(results$r_adj, type = "o", pch = 20, col = "darkgray", xlab = "Modelli", ylab = "Var. Spiegata", main = "Selezione del Modello tramite R Aggiustato")
points(indice_ottimo, valore_ottimo, col = "green2", pch = 20, cex = 2)
indice_ottimo<- which.min(results$rmse)
valore_ottimo <- min(results$rmse)
plot(results$rmse, type = "o", pch = 20, col = "darkgray", xlab = "Modelli", ylab = "RMSE", main = "Selezione del Modello tramite RMSE")
points(indice_ottimo, valore_ottimo, col = "green2", pch = 20, cex = 2)
Tra i tre modelli ottimali selezionati, scegliamo il modello 4 per il principio di parsimonia, dal momento che richiede meno variabili rispetto agli altri due ed è quindi meno complesso.
Impostiamo quindi il quarto modello come modello finale ed esaminiamolo.
final_model <- model_4
summary(final_model)
##
## Call:
## lm(formula = Peso ~ N.gravidanze + Gestazione + Lunghezza + Cranio +
## Tipo.parto + Sesso, data = df)
##
## Residuals:
## Min 1Q Median 3Q Max
## -690.27 -169.93 -10.53 157.93 879.58
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -6342.8834 129.7694 -48.878 < 2e-16 ***
## N.gravidanze 12.7180 3.8961 3.264 0.00111 **
## Gestazione 29.4824 3.5959 8.199 3.95e-16 ***
## Lunghezza 10.3733 0.2932 35.377 < 2e-16 ***
## Cranio 9.6333 0.4064 23.704 < 2e-16 ***
## Tipo.partoNat 22.9745 10.8102 2.125 0.03367 *
## SessoM 76.6880 10.0093 7.662 2.67e-14 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 238.1 on 2343 degrees of freedom
## Multiple R-squared: 0.7432, Adjusted R-squared: 0.7425
## F-statistic: 1130 on 6 and 2343 DF, p-value: < 2.2e-16
Il modello finale è riassumibile come segue:
L’intercetta ha il peso maggiore in termini di valore assoluto. Dal momento che è biologicamente impossibile per un bambino avere lunghezza nulla e peso negativo, ipotizziamo che lo scopo dell’intercetta sia quella di calibrare l’influenza degli altri regressori;
Il predittore continuo più potente è Lunghezza, che aggiunge 10.4 grammi di peso per ogni millimetro di lunghezza del bambino, mentre dal punto di vista discreto abbiamo Gestazione con circa 29.5 grammi in più per ogni settimana di gravidanza. Per quanto riguarda le variabili di controllo, come anche anticipato nelle sezioni precedenti, è il sesso del neonato a prevalere, dove a parità delle altre variabili un maschio pesa in media 76.7 grammi in più delle femmine;
Contrariamente a quanto ipotizzato in precedenza, N.gravidanze e Tipo.parto sono rientrate nel modello con una discreta significatività. Per ogni gravidanza pregressa, infatti, abbiamo 12.7 grammi in più di peso, mentre a parità di condizioni il parto naturale aumenta il peso fino a quasi 23 grammi.
Continuiamo analizzando i residui del modello. La cella che segue traccia la diagnostica dei residui in quattro grafici.
par(mfrow = c(2,2))
plot(final_model)
La diagnostica dei residui ci mette di fronte a un modello valido. Il grafico Q-Q, dove i punti sono allineati lungo la bisettrice, attesta la normalità degli errori, mentre il plot Scale-Location e il Residuals vs Fitted mostrano rispettivamente la minima eteroschedasticità e non-linearità. L’assenza di punti critici nel grafico del Leverage conferma che la strategia di rimozione degli outlier all’inizio del documento è stata efficace per ottenere un modello robusto e non influenzato da singole osservazioni anomale.
Verifichiamo ulteriormente questo sguardo di insieme tracciando la distribuzione dei residui nella cella che segue. La linea tratteggiata rossa mostra la distribuzione normale a parità di media e deviazione standard dei residui.
residui=final_model$residuals
plot(density(residui))
lines(density(rnorm(nrow(df), mean=mean(residui), sd=sd(residui))),col="red", lty=2)
Le due curve sembrano molto simili nella loro forma. Verifichiamo se i residui hanno distribuzione normale tramite il test di Shapiro-Wilk.
shapiro.test(residui)
##
## Shapiro-Wilk normality test
##
## data: residui
## W = 0.9966, p-value = 4.07e-05
Il test di Shapiro-Wilk ha restituito un valore statistico vicino a 1, indicando una quasi perfetta aderenza dei residui alla distribuzione normale. Il risultato ottenuto dal p-value è da attribuirsi all’elevata numerosità del campione, mentre già l’ispezione visiva del Q-Q Plot aveva confermato che i residui si distribuiscono lungo la diagonale, validando l’assunzione di normalità necessaria per l’inferenza statistica.
Eseguiamo ora il test di Breusch-Pagan per valutare l’omoschedasticità dei residui.
lmtest::bptest(final_model)
##
## studentized Breusch-Pagan test
##
## data: final_model
## BP = 14.383, df = 6, p-value = 0.02564
Il test di Breusch-Pagan ha rilevato una lieve eteroschedasticità. Tuttavia, data l’elevata numerosità campionaria e l’ispezione visiva del grafico Scale-Location, il modello può essere comunque ritenuto affidabile per l’inferenza e la stima dei coefficienti, dal momento che la stabilità della varianza è mantenuta per la quasi totalità del range osservato.
Valutiamo ora l’autocorrelazione con il test di Durbin-Watson.
lmtest::dwtest(final_model)
##
## Durbin-Watson test
##
## data: final_model
## DW = 1.9916, p-value = 0.3985
## alternative hypothesis: true autocorrelation is greater than 0
Il p-value ci permette di non rifiutare l’assenza di autocorrelazione tra le osservazioni e, di conseguenza, garantisce che le stime dei coefficienti siano corrette e non distorte da dipendenze interne al campione.
Concludiamo l’analisi del modello identificando i valori anomali. Procediamo con un’ispezione visiva nelle tre celle che seguono, che mostrano rispettivamente i punti di leva, gli outliers e le distanze di Cook.
plot(hatvalues(final_model))
abline(h=2*sum(hatvalues(final_model))/nrow(df), col="red")
plot(rstudent(final_model))
abline(h=c(-2,2), col="red")
plot(cooks.distance(final_model))
abline(h=0.5, col="red")
Tolte le distanze di Cook, dove nessun residuo supera la soglia critica di 0.5, osserviamo una forte quantità di valori anomali. Estraiamoli dal dataset nella cella che segue e calcoliamone la percentuale su tutto il dataset.
leverage_pts <- which(hatvalues(final_model) > 2*sum(hatvalues(final_model))/nrow(df))
outlier_pts <- which(rstudent(final_model)< -2 | rstudent(final_model)>2)
cook_pts <- which(cooks.distance(final_model)>0.5)
strange_vals <- unique(c(leverage_pts, outlier_pts, cook_pts))
df_strange <- df[strange_vals, c("Peso","N.gravidanze","Gestazione","Lunghezza","Cranio","Tipo.parto","Sesso")]
nrow(df_strange); (nrow(df_strange)/nrow(df))*100
## [1] 203
## [1] 8.638298
La cella che segue riassume il comportamento delle variabili di questi record.
summary(df_strange)
## Peso N.gravidanze Gestazione Lunghezza
## Min. : 830 Min. : 0.000 Min. :25.00 Min. :310.0
## 1st Qu.:2635 1st Qu.: 0.000 1st Qu.:36.00 1st Qu.:455.0
## Median :3100 Median : 1.000 Median :39.00 Median :490.0
## Mean :3037 Mean : 2.177 Mean :37.52 Mean :475.5
## 3rd Qu.:3750 3rd Qu.: 3.000 3rd Qu.:40.00 3rd Qu.:500.0
## Max. :4470 Max. :12.000 Max. :43.00 Max. :555.0
## Cranio Tipo.parto Sesso
## Min. :235.0 Ces: 64 F:112
## 1st Qu.:316.5 Nat:139 M: 91
## Median :337.0
## Mean :331.8
## 3rd Qu.:350.0
## Max. :390.0
Le 203 osservazioni (8,6% rispetto al dataset completo) inserite in df_strange riguardano condizioni cliniche estreme, come nascite a 25 settimane o pesi inferiori al chilogrammo, che il modello fatica a individuare correttamente. Ciononostante, il modello proposto rimane comunque valido per via dei coefficienti altamente significativi e l’R-quadro elevato.
L’ultima sezione di questo documento è dedicata a una previsione richiesta dall’azienda committente. È stato chiesto infatti di stimare il peso di una neonata alla trentanovesima settimana la cui madre ha già avuto due gravidanze precedenti.
Con queste specifiche, abbiamo estratto casi simili dal dataset e li abbiamo riassunti nella cella che segue.
df_to_predict <- df[df$Sesso=="F"&df$Gestazione==39&df$N.gravidanze==2,]
summary(df_to_predict)
## Anni.madre N.gravidanze Fumatrici Gestazione Peso
## Min. :21.00 Min. :2 No:40 Min. :39 Min. :2630
## 1st Qu.:27.00 1st Qu.:2 Sì: 1 1st Qu.:39 1st Qu.:3090
## Median :32.00 Median :2 Median :39 Median :3220
## Mean :30.59 Mean :2 Mean :39 Mean :3259
## 3rd Qu.:34.00 3rd Qu.:2 3rd Qu.:39 3rd Qu.:3440
## Max. :42.00 Max. :2 Max. :39 Max. :4100
## Lunghezza Cranio Tipo.parto Ospedale Sesso
## Min. :445.0 Min. :305 Ces:14 osp1: 9 F:41
## 1st Qu.:480.0 1st Qu.:330 Nat:27 osp2:15 M: 0
## Median :495.0 Median :335 osp3:17
## Mean :492.8 Mean :336
## 3rd Qu.:500.0 3rd Qu.:345
## Max. :520.0 Max. :362
Limitandoci ai regressori individuati nella costruzione del modello, osserviamo quanto segue:
La bambina avrà un peso stimato intorno ai 3260 grammi, che coincide con la media del peso delle bambine con le stesse caratteristiche;
Anche la lunghezza e la circonferenza cranica si aggirano sui 493 mm e i 336 mm rispettivamente;
C’è una prevalenza di parti naturali tra i precedenti, ma non possiamo escludere un parto cesareo.
Non avendo ulteriori informazioni oltre quelle fornite dall’azienda, abbiamo deciso di essere quanto più esaustivi possibile nelle previsioni. In particolare esamineremo i seguenti casi:
La bambina ha lunghezza e circonferenza cranica pari alle medie del campione e nasce con parto naturale;
La bambina nasce con parto cesareo e ha lunghezza e circonferenza cranica pari alle medie del sottocampione di bambine nate con cesareo;
La bambina nasce con parto naturale e ha lunghezza e circonferenza cranica pari alle medie del sottocampione di bambine nate con parto naturale;
Creiamo le nostre casistiche e mostriamole nella cella che segue.
df_to_evaluate = data.frame(
N.gravidanze = c(2, 2, 2),
Gestazione = c(39, 39, 39),
Tipo.parto = c("Nat", "Ces", "Nat"),
Sesso = c("F","F","F")
)
splitted_means <- split(df_to_predict, df_to_predict$Tipo.parto)
df_to_evaluate$Lunghezza <- c(mean(df_to_predict$Lunghezza),mean(splitted_means$Ces$Lunghezza),mean(splitted_means$Nat$Lunghezza))
df_to_evaluate$Cranio <- c(mean(df_to_predict$Cranio),mean(splitted_means$Ces$Cranio),mean(splitted_means$Nat$Cranio))
df_to_evaluate$Tipo.parto <- as.factor(df_to_evaluate$Tipo.parto)
df_to_evaluate$Sesso <- as.factor(df_to_evaluate$Sesso)
df_to_evaluate
## N.gravidanze Gestazione Tipo.parto Sesso Lunghezza Cranio
## 1 2 39 Nat F 492.8049 336.0244
## 2 2 39 Ces F 493.5714 333.2143
## 3 2 39 Nat F 492.4074 337.4815
Effettuiamo le predizioni con il modello che abbiamo costruito.
df_to_evaluate$Pred_Peso <- predict(final_model, df_to_evaluate)
df_to_evaluate
## N.gravidanze Gestazione Tipo.parto Sesso Lunghezza Cranio Pred_Peso
## 1 2 39 Nat F 492.8049 336.0244 3204.391
## 2 2 39 Ces F 493.5714 333.2143 3162.298
## 3 2 39 Nat F 492.4074 337.4815 3214.305
Il confronto tra il peso medio osservato e i valori predetti dal modello evidenzia un’elevata affidabilità della stima. In particolare, si osserva come il modello colga correttamente le variazioni dovute alla circonferenza cranica e alla tipologia di parto.
Validiamo ulteriormente i risultati ottenuti tramite il T test. Per farlo, abbiamo assunto che la predizione del peso sia equiparabile alla stima della media del peso della popolazione di bambine con le stesse caratteristiche e che il campione di riferimento equivalga a un campione estratto da questa popolazione.
Eseguiamo i T test per ognuno dei casi sopra esposti nelle celle che seguono.
t.test(df_to_predict$Peso, mu=df_to_evaluate$Pred_Peso[1], alternative = "two.sided")
##
## One Sample t-test
##
## data: df_to_predict$Peso
## t = 1.1615, df = 40, p-value = 0.2523
## alternative hypothesis: true mean is not equal to 3204.391
## 95 percent confidence interval:
## 3163.782 3354.755
## sample estimates:
## mean of x
## 3259.268
t.test(splitted_means$Ces$Peso, mu=df_to_evaluate$Pred_Peso[2], alternative = "two.sided")
##
## One Sample t-test
##
## data: splitted_means$Ces$Peso
## t = 1.6247, df = 13, p-value = 0.1282
## alternative hypothesis: true mean is not equal to 3162.298
## 95 percent confidence interval:
## 3131.263 3381.594
## sample estimates:
## mean of x
## 3256.429
t.test(splitted_means$Nat$Peso, mu=df_to_evaluate$Pred_Peso[3], alternative = "two.sided")
##
## One Sample t-test
##
## data: splitted_means$Nat$Peso
## t = 0.70453, df = 26, p-value = 0.4874
## alternative hypothesis: true mean is not equal to 3214.305
## 95 percent confidence interval:
## 3125.261 3396.221
## sample estimates:
## mean of x
## 3260.741
In tutti gli scenari analizzati, i p-value ci hanno permesso di non rifiutare l’ipotesi nulla e, di conseguenza, di validare la bontà della previsione del modello. Questi risultati ci portano a concludere che il modello possiede un’elevata accuratezza e capacità di generalizzazione sia per la specifica classe di neonati analizzata che per tutte le tipologie di record sottoposte in addestramento.
In questo documento sono state mostrate le fasi di analisi e realizzazione di un modello statistico per la previsione del peso dei neonati, come richiesto da Neonatal Health Solutions.
Dopo aver importato i dati nella prima sezione, nella seconda abbiamo rimosso i valori anomali che avrebbero inficiato in modo negativo sulle prestazioni del modello. Il risultato è stato un dataset ridotto nelle dimensioni, ma con sufficiente informazione da ottenere un modello robusto.
Nella terza sezione abbiamo esplorato i dati con l’obiettivo di formulare le prime ipotesi sul futuro modello. Abbiamo così osservato che il target non rispettava l’assunzione di normalità e abbiamo individuato dei possibili regressori forti per il modello come il sesso e le misure antropometriche.
Nella quarta sezione abbiamo effettuato dei test statistici richiesti dall’azienda. Abbiamo in particolare smentito la dipendenza tra ospedale e tipo di parto, abbiamo confrontato le medie di lunghezza e peso con quelle stimate dal WHO e abbiamo visto una forte correlazione tra sesso e misure antropometriche.
Dopo aver costruito una serie di modelli nella quinta sezione, nella sesta sezione abbiamo scelto e analizzato il modello migliore. Malgrado i residui non rispettassero numericamente le assunzioni di normalità e di omoschedasticità, la diagnostica grafica unita a un’ispezione dei valori anomali ci ha permesso di stabilire che il modello è solido e che le mancate assunzioni sono perlopiù dovute alla numerosità del campione e a record estremi come nati prematuri o bambini molto pesanti.
Infine, abbiamo testato il modello effettuando una predizione richiesta dall’azienda. Esaminando tre possibili casistiche, abbiamo confermato la validità del modello tramite T test che equiparano la predizione a una stima della media del peso su una popolazione di bambine con caratteristiche simili.