En esta sección trabajaremos con el método de clasificación de Regresión Logística sobre el conocido dataset del Titanic de Kaggle. El objetivo es trabajar sobre una variable discreta, normalmente se relaciona con grupos binarios (es decir, 0 y 1).
Si usásemos el modelo de regresión lineal para intentar predecir en grupos binarios obtendríamos un modelo erróneo, es por ello que lo mejor es usar la regresión logística, que junto a la función sigmoide nos dará una curva continua de nuestros datos para predecir la probabilidad. La función sigmoide viene dada por la siguiente fórmula:
\[P(t)=\dfrac{1}{1+e^{-t}}\] Podemos apreciar la diferencia entre los dos modelos comentados previamente en la siguiente imagen:
One
Supongamos que el eje de las Y representa la probabilidad de la devolución de un préstamo. Con el modelo lineal \[y=b_{0}+b_{1}·x\] no logramos obtener los resultados correctamente, sin embargo, si pasamos el modelo lineal como la variable t en la curva sigmoide, obtenemos una transformación del modelo en la curva de la derecha.
\[p=\dfrac{1}{1+e^{-(b_{0}+b{1}·x)}}\]
Se toma el valor 0.5 como punto de corte para determinar si un valor pertenece a las clase 0 o a la clase 1.
setwd("~/R/R-Course-HTML-Notes/R-for-Data-Science-and-Machine-Learning/Machine Learning with R")
df.train <- read.csv("titanic_train.csv")
str(df.train)
## 'data.frame': 891 obs. of 12 variables:
## $ PassengerId: int 1 2 3 4 5 6 7 8 9 10 ...
## $ Survived : int 0 1 1 1 0 0 0 0 1 1 ...
## $ Pclass : int 3 1 3 1 3 3 1 3 3 2 ...
## $ Name : Factor w/ 891 levels "Abbing, Mr. Anthony",..: 109 191 358 277 16 559 520 629 417 581 ...
## $ Sex : Factor w/ 2 levels "female","male": 2 1 1 1 2 2 2 2 1 1 ...
## $ Age : num 22 38 26 35 35 NA 54 2 27 14 ...
## $ SibSp : int 1 1 0 1 0 0 0 3 0 1 ...
## $ Parch : int 0 0 0 0 0 0 0 1 2 0 ...
## $ Ticket : Factor w/ 681 levels "110152","110413",..: 524 597 670 50 473 276 86 396 345 133 ...
## $ Fare : num 7.25 71.28 7.92 53.1 8.05 ...
## $ Cabin : Factor w/ 148 levels "","A10","A14",..: 1 83 1 57 1 1 131 1 1 1 ...
## $ Embarked : Factor w/ 4 levels "","C","Q","S": 4 2 4 4 4 3 4 4 4 2 ...
library(Amelia)
## Loading required package: Rcpp
## ##
## ## Amelia II: Multiple Imputation
## ## (Version 1.7.5, built: 2018-05-07)
## ## Copyright (C) 2005-2019 James Honaker, Gary King and Matthew Blackwell
## ## Refer to http://gking.harvard.edu/amelia/ for more information
## ##
missmap(df.train, main='Missing Map', col=c('yellow','black'), legend=FALSE)
library(ggplot2)
## Registered S3 methods overwritten by 'ggplot2':
## method from
## [.quosures rlang
## c.quosures rlang
## print.quosures rlang
Veamos la proporción de pasajeros que sobrevivieron frente a los que no.
ggplot(df.train, aes(Survived)) + geom_bar()
Ahora, veamos la distribución de la clase según el ticket.
ggplot(df.train, aes(Pclass)) + geom_bar(aes(fill=factor(Pclass))) + scale_fill_discrete(name="Ticket class")
Veamos la distribución de pasajeros según su sexo.
ggplot(df.train, aes(Sex)) + geom_bar(aes(fill=factor(Sex))) + scale_fill_discrete(name="Sex")
Veamos ahora en función de su edad.
ggplot(df.train,aes(Age)) + geom_histogram(bins=20, alpha=0.5, fill="blue")
## Warning: Removed 177 rows containing non-finite values (stat_bin).
La mayoría de gente a bordo era gente entre los 20 y 40 años de edad.
ggplot(df.train, aes(SibSp)) + geom_bar()
Los pasajeros que más frecuentaban el barco en el viaje eran gente que iba sola a bordo.
ggplot(df.train, aes(Fare)) + geom_histogram(fill="green", col="black", alpha=0.5)
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
Tal y como era esperado, la gente había comprado el ticket más barato, lo que tiene sentido pues el mayor número de pasajeros iban en tercera clase.
Veamos ahora la cantidad de gente que no sobrevivió en función de la clase en la que viajaban.
ggplot(df.train, aes(Survived, fill=factor(Pclass))) + geom_bar(position="dodge") + scale_fill_discrete(name="Passenger's Class")
Podemos observar que el mayor número de gente que no sobrevivió al hundimiento eran viajeros de la tercera clase.
Por último veamos en función de su sexo.
ggplot(df.train, aes(Survived, fill=factor(Sex))) + geom_bar(position="dodge") + scale_fill_discrete(name="Passenger's Sex")
Un mayor número de hombres fallecieron en el hundimiento.
En muchas ocasiones lo mejor ante este tipo de situaciones es eliminar las observaciones con NA’s, pero en este caso hay una cantidad muy alta de observaciones con esa condición.
table(is.na(df.train))
##
## FALSE TRUE
## 10515 177
Una de las soluciones típicas es usar la media de las edades de los pasajeros, pero en este caso, vamos a usar la media de los pasajeros en función de su clase. Podemos hacerlo numéricamente:
library(dplyr)
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
df.train %>% filter(Age!='Na') %>% group_by(Pclass) %>% summarize(media=median(Age))
## # A tibble: 3 x 2
## Pclass media
## <int> <dbl>
## 1 1 37
## 2 2 29
## 3 3 24
O verlo gráficamente:
ggplot(df.train, aes(Pclass, Age)) + geom_boxplot(aes(group=Pclass, fill=factor(Pclass)), alpha=0.4) + scale_fill_discrete(name="Passenger's class") + scale_y_continuous(breaks= seq(0,70, by=2)) + theme_bw()
## Warning: Removed 177 rows containing non-finite values (stat_boxplot).
Vamos a modificar la columna edad para esas observaciones:
fix_age <- function(age, class){
out <- age
for (i in 1:length(age)){
if (is.na(age[i])){
if (class[i]==1){
out[i] <- 37
}
else if (class[i]==2){
out[i] <- 29
}
else{
out[i] <- 24
}
}
else{
out[i] <- age[i]
}
}
return(out)
}
fixed.ages <- fix_age(df.train$Age, df.train$Pclass)
df.train$Age <- fixed.ages
Comprobamos que no existan NA’s en el dataset.
missmap(df.train, main="Missing Map", col=c("yellow","black"), legend=FALSE)
Eliminamos las variables con demasiados niveles pues no las vamos a usar.
df.train <- df.train %>% select(-PassengerId, -Ticket,-Cabin, -Name)
head(df.train)
## Survived Pclass Sex Age SibSp Parch Fare Embarked
## 1 0 3 male 22 1 0 7.2500 S
## 2 1 1 female 38 1 0 71.2833 C
## 3 1 3 female 26 0 0 7.9250 S
## 4 1 1 female 35 1 0 53.1000 S
## 5 0 3 male 35 0 0 8.0500 S
## 6 0 3 male 24 0 0 8.4583 Q
Cambiamos el formato de las columnas, aunque la columna SibSp se puede interpretar como una variable continua, en realidad el número de hermanos y esposos va a estar “limitado”. Es por ello que mejor es tratarla como una variable discreta.
df.train$Survived <- factor(df.train$Survived)
df.train$Pclass <- factor(df.train$Pclass)
df.train$SibSp <- factor(df.train$SibSp)
df.train$Parch <- factor(df.train$Parch)
str(df.train)
## 'data.frame': 891 obs. of 8 variables:
## $ Survived: Factor w/ 2 levels "0","1": 1 2 2 2 1 1 1 1 2 2 ...
## $ Pclass : Factor w/ 3 levels "1","2","3": 3 1 3 1 3 3 1 3 3 2 ...
## $ Sex : Factor w/ 2 levels "female","male": 2 1 1 1 2 2 2 2 1 1 ...
## $ Age : num 22 38 26 35 35 24 54 2 27 14 ...
## $ SibSp : Factor w/ 7 levels "0","1","2","3",..: 2 2 1 2 1 1 1 4 1 2 ...
## $ Parch : Factor w/ 7 levels "0","1","2","3",..: 1 1 1 1 1 1 1 2 3 1 ...
## $ Fare : num 7.25 71.28 7.92 53.1 8.05 ...
## $ Embarked: Factor w/ 4 levels "","C","Q","S": 4 2 4 4 4 3 4 4 4 2 ...
log.model <- glm(Survived~., family=binomial(link='logit'), data=df.train)
summary(log.model)
##
## Call:
## glm(formula = Survived ~ ., family = binomial(link = "logit"),
## data = df.train)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -2.8158 -0.6134 -0.4138 0.5808 2.4896
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 1.845e+01 1.660e+03 0.011 0.991134
## Pclass2 -1.079e+00 3.092e-01 -3.490 0.000484 ***
## Pclass3 -2.191e+00 3.161e-01 -6.930 4.20e-12 ***
## Sexmale -2.677e+00 2.040e-01 -13.123 < 2e-16 ***
## Age -3.971e-02 8.758e-03 -4.534 5.79e-06 ***
## SibSp1 8.135e-02 2.245e-01 0.362 0.717133
## SibSp2 -2.897e-01 5.368e-01 -0.540 0.589361
## SibSp3 -2.241e+00 7.202e-01 -3.111 0.001862 **
## SibSp4 -1.675e+00 7.620e-01 -2.198 0.027954 *
## SibSp5 -1.595e+01 9.588e+02 -0.017 0.986731
## SibSp8 -1.607e+01 7.578e+02 -0.021 0.983077
## Parch1 3.741e-01 2.895e-01 1.292 0.196213
## Parch2 3.862e-02 3.824e-01 0.101 0.919560
## Parch3 3.655e-01 1.056e+00 0.346 0.729318
## Parch4 -1.586e+01 1.055e+03 -0.015 0.988007
## Parch5 -1.152e+00 1.172e+00 -0.983 0.325771
## Parch6 -1.643e+01 2.400e+03 -0.007 0.994536
## Fare 2.109e-03 2.490e-03 0.847 0.397036
## EmbarkedC -1.458e+01 1.660e+03 -0.009 0.992995
## EmbarkedQ -1.456e+01 1.660e+03 -0.009 0.993001
## EmbarkedS -1.486e+01 1.660e+03 -0.009 0.992857
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 1186.66 on 890 degrees of freedom
## Residual deviance: 763.41 on 870 degrees of freedom
## AIC: 805.41
##
## Number of Fisher Scoring iterations: 15
Abrimos el dataset para testear el modelo.
setwd("~/R/R-Course-HTML-Notes/R-for-Data-Science-and-Machine-Learning/Machine Learning with R")
df.test <- read.csv('titanic_test.csv')
Al igual que ocurría con el dataset para entrenar el modelo, tenemos que modificar los valores NA’s que se encuentran en la columna Age.
ggplot(df.test, aes(Pclass, Age)) + geom_boxplot(aes(group=Pclass, fill=factor(Pclass)), alpha=0.4) + scale_fill_discrete(name="Passenger's class") + scale_y_continuous(breaks= seq(0,70, by=2)) + theme_bw()
## Warning: Removed 86 rows containing non-finite values (stat_boxplot).
df.test %>% filter(Age!='Na') %>% group_by(Pclass) %>% summarize(media=median(Age))
## # A tibble: 3 x 2
## Pclass media
## <int> <dbl>
## 1 1 42
## 2 2 26.5
## 3 3 24
fix_age_test <- function(age, class){
out <- age
for (i in 1:length(age)){
if (is.na(age[i])){
if (class[i]==1){
out[i] <- 42
}
else if (class[i]==2){
out[i] <- 26.5
}
else{
out[i] <- 24
}
}
else{
out[i] <- age[i]
}
}
return(out)
}
fixed.ages.test <- fix_age_test(df.test$Age, df.test$Pclass)
df.test$Age <- fixed.ages.test
df.test$Pclass <- factor(df.test$Pclass)
df.test$SibSp <- factor(df.test$SibSp)
df.test$Parch <- factor(df.test$Parch)
str(df.test)
## 'data.frame': 418 obs. of 11 variables:
## $ PassengerId: int 892 893 894 895 896 897 898 899 900 901 ...
## $ Pclass : Factor w/ 3 levels "1","2","3": 3 3 2 3 3 3 3 2 3 3 ...
## $ Name : Factor w/ 418 levels "Abbott, Master. Eugene Joseph",..: 210 409 273 414 182 370 85 58 5 104 ...
## $ Sex : Factor w/ 2 levels "female","male": 2 1 2 2 1 2 1 2 1 2 ...
## $ Age : num 34.5 47 62 27 22 14 30 26 18 21 ...
## $ SibSp : Factor w/ 7 levels "0","1","2","3",..: 1 2 1 1 2 1 1 2 1 3 ...
## $ Parch : Factor w/ 8 levels "0","1","2","3",..: 1 1 1 1 2 1 1 2 1 1 ...
## $ Ticket : Factor w/ 363 levels "110469","110489",..: 153 222 74 148 139 262 159 85 101 270 ...
## $ Fare : num 7.83 7 9.69 8.66 12.29 ...
## $ Cabin : Factor w/ 77 levels "","A11","A18",..: 1 1 1 1 1 1 1 1 1 1 ...
## $ Embarked : Factor w/ 3 levels "C","Q","S": 2 3 2 3 3 3 2 3 1 3 ...
library(plyr)
## -------------------------------------------------------------------------
## You have loaded plyr after dplyr - this is likely to cause problems.
## If you need functions from both plyr and dplyr, please load plyr first, then dplyr:
## library(plyr); library(dplyr)
## -------------------------------------------------------------------------
##
## Attaching package: 'plyr'
## The following objects are masked from 'package:dplyr':
##
## arrange, count, desc, failwith, id, mutate, rename, summarise,
## summarize
df.test$Parch <- revalue(df.test$Parch,c("9" = "6"))
fitted.probabilities <- predict(log.model, df.test, type='response')
A continuación tenemos que dividir esas probabilidades en aquellas mayores que 0.5y las que son menores, para incluír al pasajero en el grupo de supervivientes o en otro.
fitted.results <- ifelse(fitted.probabilities>0.5, 1,0)
df.test$Survived_predict <- fitted.results
Esta columna sería la correspondiente para saber si los pasajeros sobrevivirían o no aplicando el método de regresión logística. No podemos saber la eficacia del modelo pues en el dataset de test no existe una columna denominada Survived. Sin embargo, para realizar esa comprobación podemos usar el dataset df.train y tomar el 70% de sus observaciones para ver la eficacia del modelo.
library(caTools)
sample <- sample.split(df.train, 0.7)
train_final <- subset(df.train, sample=TRUE)
test_final <- subset(df.train, sample=FALSE)
Entrenamos el modelo sobre el dataset de entrenamiento:
final_log_model <- glm(Survived ~., family=binomial(link='logit'),train_final)
summary(final_log_model)
##
## Call:
## glm(formula = Survived ~ ., family = binomial(link = "logit"),
## data = train_final)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -2.8158 -0.6134 -0.4138 0.5808 2.4896
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 1.845e+01 1.660e+03 0.011 0.991134
## Pclass2 -1.079e+00 3.092e-01 -3.490 0.000484 ***
## Pclass3 -2.191e+00 3.161e-01 -6.930 4.20e-12 ***
## Sexmale -2.677e+00 2.040e-01 -13.123 < 2e-16 ***
## Age -3.971e-02 8.758e-03 -4.534 5.79e-06 ***
## SibSp1 8.135e-02 2.245e-01 0.362 0.717133
## SibSp2 -2.897e-01 5.368e-01 -0.540 0.589361
## SibSp3 -2.241e+00 7.202e-01 -3.111 0.001862 **
## SibSp4 -1.675e+00 7.620e-01 -2.198 0.027954 *
## SibSp5 -1.595e+01 9.588e+02 -0.017 0.986731
## SibSp8 -1.607e+01 7.578e+02 -0.021 0.983077
## Parch1 3.741e-01 2.895e-01 1.292 0.196213
## Parch2 3.862e-02 3.824e-01 0.101 0.919560
## Parch3 3.655e-01 1.056e+00 0.346 0.729318
## Parch4 -1.586e+01 1.055e+03 -0.015 0.988007
## Parch5 -1.152e+00 1.172e+00 -0.983 0.325771
## Parch6 -1.643e+01 2.400e+03 -0.007 0.994536
## Fare 2.109e-03 2.490e-03 0.847 0.397036
## EmbarkedC -1.458e+01 1.660e+03 -0.009 0.992995
## EmbarkedQ -1.456e+01 1.660e+03 -0.009 0.993001
## EmbarkedS -1.486e+01 1.660e+03 -0.009 0.992857
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 1186.66 on 890 degrees of freedom
## Residual deviance: 763.41 on 870 degrees of freedom
## AIC: 805.41
##
## Number of Fisher Scoring iterations: 15
Igual que antes podemos observar que tanto pertenecer a la clase 2 como a la clase 3 parece estar relacionado con la variable survived, así como ser hombre. Esto tiene sentido viendo los gráficos de la sección 3.
Aplicamos el método al dataset para testear.
fitted.probabilities.final <- predict(final_log_model, test_final, type='response')
fitted.results.final <- ifelse(fitted.probabilities.final>0.5, 1,0)
Calculamos ahora la eficacia del modelo.
1-mean(fitted.results.final != test_final$Survived)
## [1] 0.8181818
Ha acertado en un 81% de los datos!
Creamos, finalmente, una matriz de confusión.
table(test_final$Survived, fitted.results.final)
## fitted.results.final
## 0 1
## 0 482 67
## 1 95 247