Analisando o acidente do Titanic com Ciência de Dados

Gravatar publicou em

Ciência de Dados Tutoriais

Neste artigo vamos participar de um desafio de Ciência de Dados proposto pelo site Kaggle. O desafio consiste em analisar diversos dados de passageiros do Titanic e construir previsões sobre o destino de cada um na trágica noite do desastre.

O único requisito necessário é que o ambiente R esteja instalado na máquina. Recomendo baixar a IDE RStudio que provê algumas facilidades como: visualização de variáveis, amostragem de tabelas e gráficos, entre outros mimos. Outra possibilidade é baixar apenas o ambiente R básico por algum dos mirrors em http://cran.r-project.org/mirrors.html.

Bem, eu menti um pouco acima. Há outro requisito para começarmos a analisar os dados: os próprios dados. Cada desafio possui uma  página no Kaggle, a do nosso é www.kaggle.com/c/titanic-gettingStarted. No painel à esquerda clique em Data e baixe os arquivos train.csv  e test.csv. Faça login ou um novo cadastro quando for pedido.

O arquivo train.csv contém os dados para criação do modelo de previsão, incluindo a informação se o passageiro sobreviveu (1) ou não (0) na coluna Survived. Árvores aleatórias (Random forests) utilizam aprendizado supervisionado, ou seja, para construirmos o modelo é necessário incluir as saídas esperadas para cada entrada, por isso Survived é fornecida. O arquivo test.csv não contém esta informação e é com ele que iremos testar o modelo final de previsão.

Na mesma página do desafio, logo abaixo do link para os dados se encontra o link para enviarmos nossas previsões, mas falaremos mais sobre isso daqui a pouco.

Finalmente estamos prontos para começar o desafio. Mãos a obra!

Programando em R

Abra o ambiente R. No console, configure o diretório de trabalho para o local onde estão os arquivos csv e carregue ambos.

# Configure o caminho para a pasta de trabalho
setwd("/Users/henrique/Documents/R")

# Leia os arquivos como data frames
train <- read.csv("~/Documents/R/train.csv", stringsAsFactors = FALSE)
test <- read.csv("~/Documents/R/test.csv", stringsAsFactors = FALSE)

Vamos dar uma olhada nos dados em train.csv. Se você estiver usando o RStudio entre com View("train") no console. Para ambos ambientes pode-se usar a função head(x, n) para mostrar os n primeiros elementos da variável x.

Colunas presentes no arquivo train.csv
Coluna Descrição
PassengerID identificador do passageiro
Survived informa se o passageiro sobreviveu
Pclass classe sócio-econômica (1 Alta; 2 Média; 3 Baixa)
Name nome do passageiro, com pronome de tratamento
Sex gênero
Age idade
SibSp número de irmãos/cônjuges a bordo
Parch número de pais/filhos a bordo
Ticket código do bilhete de passagem
Fare preço da passagem
cabin cabine
Embarked local de embarque (Cherbourg, Queenstown ou Southampton)

Varrendo a tabela podemos notar um possível problema: ausência de valores nas colunas Age Cabin. A princípio ambas informações parecem bastante relevantes no contexto do desastre. Os passageiros hospedados próximos à região de colisão com o iceberg teriam uma chance menor de se salvarem, considerando que o desastre ocorreu à noite e, possivelmente, a maioria das pessoas já estariam em seus dormitórios. Quanto à idade, sabe-se que foi dada prioridade de uso dos botes a mulheres e crianças, aumentando consideravelmente a probabilidade de sobrevivência desses dois grupos.

Outras duas colunas da tabela que também possuem valores ausentes são Embarked e Fare. Para encontrá-los, é necessário usar os comandos which(combi$Embarked == "") which(is.na(combi$Fare)), respectivamente. Vamos arrumar isto agora para não esquecermos depois. Para estimar o preço da passagem, podemos olhar outros passageiros que tenham local de embarque, classe e idade iguais ou semelhantes, e calcular o preço médio de suas passagens. Os dois passageiros que não possuem local de embarque receberão o local 'S', apenas porque a maioria embarcou ali. Também vamos converter Embarked para factor (ie categorias) para podermos usá-lo como um classificador. Antes de trabalharmos com os dados, porém, vamos unir os conjuntos de treinamento e de teste para que as nossas descobertas sejam reproduzidas em ambos.

# Junte os dois conjuntos
test$Survived <- NA
combi <- rbind(train, test)

# Corrija única Fare ausente
combi$Fare[1044] <- mean(combi[combi$Pclass == 3 & combi$Embarked == 'S' & combi$Age > 60, 'Fare'], na.rm = TRUE)

# Corrija Embarked ausentes
combi$Embarked[c(62,830)] = "S"
combi$Embarked <- factor(combi$Embarked)

Voltando à tabela, agora vemos que todas as outras colunas estão completas, e que a de nomes é particularmente interessante. "Por quê?", escuto você perguntar. Bem, podemos agrupar as pessoas segundo o sobrenome e tentar extrair algo, inclusive comparar com as informações de número de parentes (talvez alguém tenha sido esquecido na contabilização de pessoas). Ainda há algo além disso. Dê uma olhada na estrutura dos nomes. Todos possuem um título: Master, Mr, Miss etc. (Verdade que estão em inglês, e alguns nem são mais usados, mas a internet está aí para isso). Segundo a Wikipedia, Master era usado para se referir a crianças e jovens do sexo masculino. Miss cabe a mulheres não casadas. Se desconsiderarmos as exceções a regra para aquele período (como mulheres solteiras de 90 anos) podemos usar a média de idades dos passageiros agrupados por título para estimar a idade de outros passageiros. Ainda bem! Nada de sessões de ouija!

# Extraia o sobrenome e o título
aux <- strsplit(combi$Name, "(,\\s)|[.]")
# O split divide um dos nomes em muitas partes. Corrija com a linha abaixo.
aux[514][[1]] <- aux[514][[1]][-4]
aux <- do.call('rbind', aux)
combi$Surname <- aux[,1]
combi$Title <- aux[,2]

# Apresente em tabela
table(combi$Title)

No script acima, aproveitei e já extraí o sobrenome e o título de cada nome e adicionei-os nas novas colunas Surname e Title da nova tabela (ou data frame, para os puristas). Obs.: um dos nomes é dividido em mais partes pelo modo como o split (linha 5) é realizado, isso causaria um erro se aplicássemos diretamente o rbind (linha 7); isso é corrigido pelo código na linha 6.

Vejamos quais títulos estão presentes. Temos vários que designam pessoas de patamar econômico-social próximo, por exemplo: Dona Lady; Capt e Major. Depois de obter as idades médias por título, vamos substituir alguns da lista para facilitar a construção do nosso modelo de árvores aleatórias.

# Calcule a média de idade dos passageiros por título e atribua aos passageiros sem idade
for (n in 1:nrow(combi)) {
  if (is.na(combi$Age[n])) {
    combi$Age[n] <- mean(combi$Age[combi$Title == combi$Title[n]], na.rm = TRUE)
  } else {
    combi$Age[n] <- combi$Age[n]
  }
}

# Una títulos semelhantes
combi$Title[combi$Title %in% c('Mme', 'Mlle')] <- 'Mlle'
combi$Title[combi$Title %in% c('Capt', 'Don', 'Major', 'Sir', 'Jonkheer')] <- 'Sir'
combi$Title[combi$Title %in% c('Dona', 'Lady', 'the Countess')] <- 'Lady'

# Defina como categorias
combi$Title <- factor(combi$Title)

Agora vamos utilizar a coluna de sobrenomes criada anteriormente para agrupar pessoas com seus familiares. A premissa é que integrantes de famílias maiores teriam mais dificuldade em manter-se próximos dentro de uma multidão em polvorosa. Mas antes de começarmos precisamos considerar que alguns sobrenomes são bem comuns. Podemos concatenar o tamanho da família ao sobrenome para tentar remediar isso, ou ao menos reduzir a chance de que famílias homônimas acabem juntas.

# Agrupe pessoas em famílias
combi$FamilySize <- combi$SibSp + combi$Parch + 1
combi$FamilyID <- paste(combi$FamilySize, combi$Surname, sep="")

# Priorize famílias grandes
combi$FamilyID[combi$FamilySize <= 2] <- 'Small'
combi$FamilyID <- factor(combi$FamilyID)

Pronto! Acredito que conseguimos extrair conclusões suficientes sobre os dados. Podemos agora construir nosso modelo de predição. Comece separando combi nos dois conjuntos originais, train test

# Retorne aos dois conjuntos iniciais
train <- combi[1:891,]
test <- combi[892:1309,]

Você deve instalar o pacote party caso ainda não tenha instalado. Este pacote contém as ferramentas para construção de florestas de árvores aleatórias (random forest trees).

install.packages('party')
library(party)

Agora que temos os dados formatados e as ferramentas necessárias, vamos treinar nosso modelo com o conjunto de treinamento e aplicá-lo sobre o conjunto de teste.

set.seed(456) # Parâmetro usado no gerador de números aleatórios.
# Guarde este número se quiser reproduzir os resultados futuramente!

# Crie o modelo de predição
fit <- cforest(as.factor(Survived) ~ Pclass + Sex + Age + SibSp + Parch + Fare + Embarked + Title + FamilySize + FamilyID, data = train, controls=cforest_unbiased(ntree=2000, mtry=3))

# Realize a predição sobre o conjunto de teste
Prediction <- predict(fit, test, OOB=TRUE, type = "response")

# Crie o arquivo de saída
submit <- data.frame(PassengerId = test$PassengerId, Survived = Prediction)
write.csv(submit, file = "cforest.csv", row.names = FALSE)

Passamos como parâmetro para ctree o conjunto de treinamento e as variáveis mais importantes dele, além do número de árvores a serem usadas (ntree) e quantas das variáveis serão amostradas a cada nó de uma árvore (mtry). Aplicamos predict sobre os dados de teste e salvamos o resultado no arquivo cforest.csv. Tudo ajeitado e empacotado, pronto para enviarmos ao Kaggle.

Resultados

Ao subtermos nossos resultados ao Kaggle, descobrimos que a taxa de acertos da predição é de aproximadamente 80%. Nada mal para os métodos rápidos e simples de análises demonstrados neste artigo.

Envio de desafio de ciência de dados ao Kaggle.com

Atualmente este desafio aceita até 10 submissões diárias, então não tenha medo de testar outras hipóteses e modelar melhor os dados. Ainda há muito que pode ser feito! Por exemplo, não consideramos o número dos bilhetes. Há casos de pessoas com bilhetes iguais, mas nomes de família diferentes. Muito provavelmente amigos que dividiram uma cabine. Talvez pudéssemos considerar casos assim como membros de uma família, afinal, no meio de um desastre, também nos preocupamos com amigos.

Outro ponto a ser analisado é a localização da cabine em relação à região de impacto. Apesar desta informação ter a menor porcentagem de valores dentro do conjunto de dados, poderíamos extrapolar as cabines que conhecemos de membros de famílias para outros membros da mesma família que não possuem a informação de cabine. E quem sabe analisando o layout do navio não descobrimos que a classe do passageiro ajuda a localizá-lo dentro da embarcação.

Veja o que mais você consegue descobrir neste desafio, e não se esqueça de olhar outros no Kaggle. Vou bater mais alguns números na minha calculadora e já te encontro lá. Agora, onde foi que eu deixei ela?





Leia mais sobre: