library(ggplot2) #visualization
library(ggthemes)#visualization
library(rpart) # ml algorithm:trees
library(randomForest) # ml algorithm: random forests
#library(caret) ml algorithms
library(dplyr) #data manipulation
library(lubridate) #date manipulation
library(caret)#machinelearning
library(rattle) #tree visualization
Read in test & training data
#setwd('/Users/giulia/datasci_course_materials/Assignments/assignment6')
data = read.csv('train.csv',na.strings=c(""))
test = read.csv('test.csv',na.strings=c(""))
and check consistency between columns of data and test datasets
names(data)
## [1] "AnimalID" "Name" "DateTime" "OutcomeType"
## [5] "OutcomeSubtype" "AnimalType" "SexuponOutcome" "AgeuponOutcome"
## [9] "Breed" "Color"
names(test)
## [1] "ID" "Name" "DateTime" "AnimalType"
## [5] "SexuponOutcome" "AgeuponOutcome" "Breed" "Color"
length(names(data)) == length(names(test))+1
## [1] FALSE
the two datasets contain a different number of columns and some the ‘ID’ columns are named differently. First thing, we fix this inconsistency
names(data)[1]='ID'
In addition, from visual inspection, we can also see that the test dataset does not contain the ‘OutcomeSubtype’ column which means that we won’t use it as a feature.
data$OutcomeSubtype<- NULL
Now we can check for consistency in terms of variable types usin the str() function and we can fix the inconsistency in the ID columns
data$ID = as.integer(data$ID)
Let’s fist start with an overview of the data
summary(data)
## ID Name DateTime
## Min. : 1 Max : 136 2015-08-11 00:00:00: 19
## 1st Qu.: 6683 Bella : 135 2015-11-17 00:00:00: 17
## Median :13365 Charlie: 107 2015-07-02 00:00:00: 13
## Mean :13365 Daisy : 106 2015-04-02 00:00:00: 11
## 3rd Qu.:20047 Lucy : 94 2014-08-31 09:00:00: 10
## Max. :26729 (Other):18460 2014-08-26 09:00:00: 9
## NA's : 7691 (Other) :26650
## OutcomeType AnimalType SexuponOutcome AgeuponOutcome
## Adoption :10769 Cat:11134 Intact Female:3511 1 year : 3969
## Died : 197 Dog:15595 Intact Male :3525 2 years : 3742
## Euthanasia : 1555 Neutered Male:9779 2 months: 3397
## Return_to_owner: 4786 Spayed Female:8820 3 years : 1823
## Transfer : 9422 Unknown :1093 1 month : 1281
## NA's : 1 (Other) :12499
## NA's : 18
## Breed Color
## Domestic Shorthair Mix : 8810 Black/White : 2824
## Pit Bull Mix : 1906 Black : 2292
## Chihuahua Shorthair Mix : 1766 Brown Tabby : 1635
## Labrador Retriever Mix : 1363 Brown Tabby/White: 940
## Domestic Medium Hair Mix: 839 White : 931
## German Shepherd Mix : 575 Brown/White : 884
## (Other) :11470 (Other) :17223
The data summary shows that approx 60% of data contained in the training set is about dogs, and, as a general remark, we can guess that on potentially relevant features (like dog breed) there will be some cleaning to do.
In order to go a little deeper, let’s see how the outcomes are distributed for cats and dogs
ggplot(data, aes(OutcomeType,col=AnimalType,fill=AnimalType)) + geom_bar(aes(y = (..count..)/ sapply(PANEL, FUN=function(x) sum(count[PANEL == x])))) +facet_wrap(~ AnimalType)+ labs(y = 'Frequency',
x = 'Outcome',
title = 'OutcomeType: Cats vs Dogs') +
theme(axis.text.x = element_text(face="bold", angle=45))
From the bar plot it seems that cats are more likely to be transferred than dogs, who are instead more likely to be returned to their owners. Luckily, in both cases, the dreadful options (death & euthanasia) are not that frequent (less than 10% of occurrences in both categories)
In the following I will first examine animal-related features (e.g. Age, breed, etc) and then the ‘environmental’ variable, i.e. date-time
The date/time feature might contain relevant information to guess the outcome of an animal. I would expect some seasonality dependence not only because humans might be more prone to get a pet during periods where they do not plan to go on holiday, but also because some animal deseases might depend on the climate as perhaps those insect-related. I might also guess that the chances of return to owner and adoption peak in the week ends and those of transfer or euthanasia during week days, with death, obviously, being evenly spread. Same goes for the hour of the day, as death can occurr at any time of the day whereas outcomes which involve human intervention will tend to be more concentrated during daily hours.
Ok let’s get to work!
which(is.na(data$DateTime))
## integer(0)
which(is.na(test$DateTime))
## integer(0)
data$month=month(data$DateTime)
data$day=wday(data$DateTime)
data$year = year(data$DateTime)
data$hour = hour(data$DateTime)
winter = c(12,1,2)
spring = c(3,4,5)
summer = c(6,7,8)
autumn = c(9,10,11)
weekend = c(6,7)
working = c(1,4)
morning=c(8,9,10,11)
earlymor = c(5,6,7)
midday = c(12,13,14,15)
afternoon =c(16,17,18)
evening = c(19,20,21)
night = c(22,23,0,1,2,3,4)
data$season = ifelse(is.element(data$month,winter),'winter',
ifelse(is.element(data$month,spring),'spring',
ifelse(is.element(data$month,autumn),'autumn','summer')))
data$daytype = ifelse(is.element(data$day,weekend),'weekend',
ifelse(is.element(data$day,working),'working','Friday'))
data$time = ifelse(is.element(data$hour,earlymor),'early morning',
ifelse(is.element(data$hour,morning),'morning',
ifelse(is.element(data$hour,midday),'midday',
ifelse(is.element(data$hour,evening),'evening','night'))))
data$season = as.factor(data$season)
data$daytype = as.factor(data$daytype)
data$time = as.factor(data$time)
where I kept Friday as a special daytype as it is often a shorter working day and usually the mood is already week-end oriented so I felt it should be treated differently with respect to, say, a Tuesday
bycat = group_by(data,AnimalType,season,OutcomeType)
sumc = summarise(bycat,count = n())
ggplot(sumc, aes(x = season, y = count, fill = OutcomeType)) +
geom_bar(stat = 'identity', position = 'fill', colour = 'black') +
facet_wrap(~AnimalType) +
coord_flip() +
labs(y = 'percentage of animals',
x = 'Season',
title = 'Outcomes by season')
Interestingly, the data show no sensational seasonal pattern. Nevertheless I will still keep this feature as I feel overlooking it might be a mistake. Let’s check if we have better luck with weekly patterns
bycat = group_by(data,AnimalType,daytype,OutcomeType)
sumc = summarise(bycat,count = n())
ggplot(sumc, aes(x = daytype, y = count, fill = OutcomeType)) +
geom_bar(stat = 'identity', position = 'fill', colour = 'black') +
facet_wrap(~AnimalType) +
coord_flip() +
labs(y = 'percentage of animals',
x = 'Type of day',
title = 'Outcomes by weekdays')
Nope, from our data we do not see striking weekly patters. I will nevertheless keep this variable and include it in the test set because it might be helpful at later stages.
test$month=month(test$DateTime)
test$day=wday(test$DateTime)
test$year = year(test$DateTime)
test$hour = hour(test$DateTime)
test$season = ifelse(is.element(test$month,winter),'winter',
ifelse(is.element(test$month,spring),'spring',
ifelse(is.element(test$month,autumn),'autumn','summer')))
test$daytype = ifelse(is.element(test$day,weekend),'weekend',
ifelse(is.element(test$day,working),'working','Friday'))
test$time = ifelse(is.element(test$hour,earlymor),'early morning',
ifelse(is.element(test$hour,morning),'morning',
ifelse(is.element(test$hour,midday),'midday',
ifelse(is.element(test$hour,evening),'evening','night'))))
test$season = as.factor(test$season)
test$daytype = as.factor(test$daytype)
test$time = as.factor(test$time)
After our various manipulations we ended up summarizing information into the new columns AgeCat,IsPureBreed,operated,season,daytipe. Breed,Color,DateTime,OutcomeSubtype will surely not be included in the classification algorithm.
Now we are ready to proceed to the classification. First thing I want to do is to split the data into training and validation, in order to be able to compare various models and algorithms.
set.seed(111)
ind = createDataPartition(data$OutcomeType,p=0.75)[[1]]#must access the first (and only) element
#of the list to be used as an index array
train = data[ind,]
valid = data[-ind,]
First I want to create a benchmark model where the probability of each outcome is assigned based onits frequency in the sample. I do not use the logloss function to score the results as in the valid set I do not have probabilities of outcomes but the actual outcomes so it seems to me more meaningful to simply count the number of entries which the model got right and divide by the total
freq = table(data$OutcomeType)[1:5]/nrow(data)
bench = as.data.frame(matrix(ncol=2,nrow=nrow(valid)))
names(bench)=c('ID','OutcomeType')
bench$ID = valid$ID
outcomes = levels(data$OutcomeType)
p= c(freq[[1]],freq[[2]],freq[[3]],freq[[4]],freq[[5]])
sampleDist = function(n) { sample(x = outcomes, n, replace = T, prob = p)}
bench$OutcomeType=sampleDist(nrow(bench))
score = sum(bench$OutcomeType==as.character(valid$OutcomeType))/nrow(valid)
score
## [1] 0.3132115
Ok, so we see that our benchmark model scored pretty badly with only 30% of correct predictions, so hopefully it should not be that hard to do better!
As a second step I want to classify the outcomes using a decision tree. Even though in the end I will also use a random forest, I want to check how the two compare.
formula = OutcomeType~ AnimalType + AgeCat + IsPureBreed + operated + season + daytype + time
modeldt <-rpart(formula, method="class", data=train)
#print(modeldt)
fancyRpartPlot(modeldt)
according to the decision tree the most important variables in determining the outcome type are whether the animal has been neutered/spayed, its age and then the animal type. Interestingly we see that, according to the decision tree, if an animal has not been operated then transfer is the only option.
Despite using all our variables we see that the decision tree manages to distinguish only 3 out of 5 outcomes, not detecting Death and Euthanasia.
Let’s see how this is translated in terms of accuracy
pred = predict(modeldt, newdata = valid)
prediction = (colnames(pred)[max.col(pred,ties.method="first")])
acc = sum(prediction== as.character(valid$OutcomeType))/length(prediction)
acc
## [1] 0.6417016
With the decision tree we only got 64% of instances right. Let’s see if using a random forest we can do better
model1rf <- randomForest(formula, data=train)
print(model1rf)
##
## Call:
## randomForest(formula = formula, data = train)
## Type of random forest: classification
## Number of trees: 500
## No. of variables tried at each split: 2
##
## OOB estimate of error rate: 36.38%
## Confusion matrix:
## Adoption Died Euthanasia Return_to_owner Transfer
## Adoption 7146 0 0 448 483
## Died 18 0 0 6 124
## Euthanasia 236 0 25 200 704
## Return_to_owner 2088 0 5 880 617
## Transfer 1936 0 9 415 4695
## class.error
## Adoption 0.1152656
## Died 1.0000000
## Euthanasia 0.9785408
## Return_to_owner 0.7548747
## Transfer 0.3345145
we see that even the random forest does not manage to predict Death and that, overall, there is still considerable class error, except for Adoption (which is nevertheless the most frequent occurence)
In terms of performances we have
pred = predict(model1rf, newdata = valid)
accrf = sum(pred == as.character(valid$OutcomeType))/length(pred)
accrf
## [1] 0.6446974
We see that the improvement over the decision tree is only marginal (at the level of the third decimal digit) indicating that perhaps all the information that could have been extracted from our aggregated variables has been used and that, due to the aggregation, the decision tree was not overfitting. Let’s check the variable importance
importance(model1rf)
## MeanDecreaseGini
## AnimalType 376.16487
## AgeCat 656.35646
## IsPureBreed 36.75881
## operated 2038.86619
## season 117.46951
## daytype 99.36731
## time 398.11176
As a last informative effort I want to see if, given the importance of the related aggreated variables, the Date/Time aggregation caused any information loss
formula2<- OutcomeType~ AnimalType + AgeCat + IsPureBreed + operated +
month + day + time
model2rf <- randomForest(formula2, data=train)
which definitely seems to be the case!
pred2 = predict(model2rf, newdata = valid)
acc2rf = sum(pred2 == as.character(valid$OutcomeType))/length(pred2)
acc2rf
## [1] 0.6509886
importance(model2rf)
## MeanDecreaseGini
## AnimalType 375.06582
## AgeCat 714.37886
## IsPureBreed 43.52733
## operated 2112.05688
## month 214.82721
## day 249.11009
## time 422.98671
In order to try to understand a bit better how the day and month variables influence the outcome, let’s run a decision tree and visualize it
modeldt2 <-rpart(formula2, method="class", data=train)
fancyRpartPlot(modeldt2)
So, the story we learned so far is the following: if an animal has not been operated then most likely it gets transferred. If she has been operated then, if she is still a kitten/puppy, then adoption is most likely. If she’s older then the animal type enters into play. If she’s a dog then age matters as younger animals are more likely to end up adopted and older ones returned to their owners. If she’s a kitty then apparently what matters most is the time of the day. If we’re outside working hours then adoption is most likely (perhaps because people tend to visit shelters in their free time) whereas during working hours transfer is most likely.
No need to say, death and euthanasia go undetected, as well as return to owner if the animal is a cat. Also, the split on the hours suggests that a binary distinction between working hours vs non-working hours might be best.
Breed purity does not seem to be extremely relevant but this only tells us that purity is not the most significant breed feature and I am pretty sure that other extremely informative ‘macro’ features can be extracted and used.. So, more work on the breeds needs to be done!
# recast the final prediction in the format required for submission
model2rf <- randomForest(formula2, data=data)
prediction <- predict(model2rf, test, type = 'vote')
submission <- data.frame('ID' = test$ID, prediction)
write.csv(submission, 'rf2submission.csv', row.names = F)
Any feedback is more than welcome!