library(tidyverse)
library(cluster)
library(factoextra)
library(ggplot2)
library(pdxTrees)
library(gridExtra)
library(gglm)
library(rpart)
library(rpart.plot)
library(parttree)
library(yardstick)
Much of this is taken from Professor Wells
Create a Tree (set of rules) to partition data
Step 1: Divide the predictor space into J distinct and non overlapping regions
Step 2: For every observation in region \(R_j\) predict the mean (\(\bar{y}\)) for the training observations in region \(R_j\)
In theory, these regions could be any shape. In practice, we will create high dimensional boxes.
Goal: Find boxes to minimize RSS \(\sum_{j=1}^J\sum_{i\in R_j}(y_i-\hat{y_{R_j}})^2\)
Computationally infeasible to consider every possible partition, so instead we will use recursive binary splitting.
Step 1: Find the predictor \(X_j\) and the cutpoint \(s\) such that splitting the predictor space into the two regions leads to the greates possible reduction in RSS. In other words given \[R_1(j,s)=\{X|X_j<s\}\text{ and } R_2(j,s)=\{X|X_j\geq s\}\] find j that minimizes \[\sum_{i:x_i\in R_1(j,s)}(y_i-\hat{y_{R_1}})^2+\sum_{i:x_i\in R_2(j,s)}(y_i-\hat{y_{R_2}})^2\]
Step 2 onwards: Repeat
Much of this is taken from Professor Wells
This should give good predictions on the training set but will likely overfit the data. Smaller trees are likely to have lower variance and easier interpretations, however they may have more bias.
Options:
Also called weakest link pruning. We can consider a sequence of trees indexed by a tuning parameter \(\alpha\)
set.seed(2020)
X=rep(1:4,4)
Y=c(1,2,3,4,2,3,4,1,3,4,1,2,4,1,2,3)
Z=sample(1:5,16,TRUE)
toyData=data.frame(X,Y,Z)
ggplot(data=toyData,aes(x=X,y=Y,color=Z))+
geom_point()
groupSSE<-function(dataFrame){
Zmean=mean(dataFrame$Z)
SSE=sum((dataFrame$Z-Zmean)^2)
return(SSE)
}
splitChoices<-function(dataFrame,otherSSE){
### Split X
for (i in c(1.5,2.5,3.5)){
group1<-dataFrame%>%filter(X>i)
group2<-dataFrame%>%filter(X<i)
print(groupSSE(group1)+groupSSE(group2)+otherSSE)
}
### Split Y
for (i in c(1.5,2.5,3.5)){
group1<-dataFrame%>%filter(Y>i)
group2<-dataFrame%>%filter(Y<i)
print(groupSSE(group1)+groupSSE(group2)+otherSSE)
}
}
splitChoicesGeneral<-function(DataFrames){
otherSSE=0
for(i in 1:length(DataFrames)){
otherSSE=otherSSE+groupSSE(DataFrames[[i]])
}
for(i in 1:length(DataFrames)){
print(paste("i =",i))
tempotherSSE=otherSSE-groupSSE(DataFrames[[i]])
splitChoices(DataFrames[[i]],tempotherSSE)
}
}
DataFrames<-list()
DataFrames[[1]]<-toyData
splitChoicesGeneral(DataFrames)
## [1] "i = 1"
## [1] 32.41667
## [1] 32.875
## [1] 32.91667
## [1] 26.91667
## [1] 18.875
## [1] 25.41667
First Split is Y=2.5
toyDataL<-toyData%>%filter(Y<2.5)
toyDataR<-toyData%>%filter(Y>2.5)
ggplot(data=toyData,aes(x=X,y=Y,color=Z))+
geom_point()+
geom_hline(yintercept=2.5)
DataFrames<-list()
DataFrames[[1]]<-toyDataL
DataFrames[[2]]<-toyDataR
splitChoicesGeneral(DataFrames)
## [1] "i = 1"
## [1] 18.83333
## [1] 17.75
## [1] 18.5
## [1] 18.75
## [1] 18.875
## [1] 18.875
## [1] "i = 2"
## [1] 18.20833
## [1] 18.375
## [1] 18.20833
## [1] 18.875
## [1] 18.875
## [1] 18.375
toyDataLL<-toyDataL%>%filter(X<2.5)
toyDataLR<-toyDataL%>%filter(X>2.5)
ggplot(data=toyData,aes(x=X,y=Y,color=Z))+
geom_point()+
geom_hline(yintercept=2.5)+
geom_vline(xintercept=2.5)
The below graph is purely for spacing, ignore it
toyDataLL<-toyDataL%>%filter(X<2.5)
toyDataLR<-toyDataL%>%filter(X>2.5)
ggplot(data=toyData,aes(x=X,y=Y,color=Z))+
geom_point()+
geom_hline(yintercept=2.5)+
geom_vline(xintercept=2.5)
This is an example of how to do it in R
Question 3 What does flip=TRUE do, and why do we need it?
DataFrames<-list()
DataFrames[[1]]<-toyDataLL
DataFrames[[2]]<-toyDataLR
DataFrames[[3]]<-toyDataR
mean(DataFrames[[1]]$Z)
## [1] 3.5
mean(DataFrames[[2]]$Z)
## [1] 4.25
mean(DataFrames[[3]]$Z)
## [1] 2
my_tree <- rpart(Z ~
X+Y,
control = rpart.control(
minsplit = 1, xval = 1, maxdepth = 2, cp = 0.03),
data = toyData)
rpart.plot(my_tree)
ggplot(data=toyData,aes(x=X,y=Y,color=Z))+
geom_point()+
geom_parttree(data =my_tree, aes(fill=Z),flip= TRUE, alpha = 0.2 )+scale_colour_viridis_c(aesthetics = c('fill'), option = "magma", name = "Pred", begin = 0, end = 1 )
- Question 4 Repeat the above process for seed=1 going
1 level deeper (not necessarily 1 layer deeper)
Walking through an example using trees from Professor Wells
my_pdxTrees <- get_pdxTrees_parks(park = c("Kenilworth Park", "Westmoreland Park",
"Woodstock Park","Berkeley Park", "Powell Park"))
## Create average crown width (East West vs North South)
my_pdxTrees <- my_pdxTrees %>% mutate(Crown_Width = (Crown_Width_EW + Crown_Width_NS)/2)
dim(my_pdxTrees)
## [1] 1039 35
names(my_pdxTrees)
## [1] "Longitude" "Latitude"
## [3] "UserID" "Genus"
## [5] "Family" "DBH"
## [7] "Inventory_Date" "Species"
## [9] "Common_Name" "Condition"
## [11] "Tree_Height" "Crown_Width_NS"
## [13] "Crown_Width_EW" "Crown_Base_Height"
## [15] "Collected_By" "Park"
## [17] "Scientific_Name" "Functional_Type"
## [19] "Mature_Size" "Native"
## [21] "Edible" "Nuisance"
## [23] "Structural_Value" "Carbon_Storage_lb"
## [25] "Carbon_Storage_value" "Carbon_Sequestration_lb"
## [27] "Carbon_Sequestration_value" "Stormwater_ft"
## [29] "Stormwater_value" "Pollution_Removal_value"
## [31] "Pollution_Removal_oz" "Total_Annual_Services"
## [33] "Origin" "Species_Factoid"
## [35] "Crown_Width"
## Goal: Predict Carbon Sequestration
my_pdxTrees %>% select(Carbon_Sequestration_lb, Crown_Width, Tree_Height) %>% drop_na() %>% cor()
## Carbon_Sequestration_lb Crown_Width Tree_Height
## Carbon_Sequestration_lb 1.0000000 0.6126951 0.4362020
## Crown_Width 0.6126951 1.0000000 0.5980118
## Tree_Height 0.4362020 0.5980118 1.0000000
g1 <- ggplot(my_pdxTrees, aes(x = Crown_Width, y = Carbon_Sequestration_lb ))+geom_point(alpha =.5, shape = 16)+theme_bw()
g2<-ggplot(my_pdxTrees, aes(x = Tree_Height, y = Carbon_Sequestration_lb ))+geom_point(alpha =.5, shape = 16)+theme_bw()
g3<-ggplot(my_pdxTrees, aes( x = Carbon_Sequestration_lb ))+geom_histogram(color = "white", bins = 20)+theme_bw()
g4<-ggplot(my_pdxTrees, aes( y = Tree_Height, x = Crown_Width, color = Carbon_Sequestration_lb ))+geom_point(alpha =.75, shape = 16)+theme_bw()+
scale_colour_viridis_c(begin = .15, end = .8, option = "magma", name = "Carbon")
grid.arrange(g1,g2,g3,g4,ncol =2)
tree_lm<-lm(Carbon_Sequestration_lb ~Crown_Width + Tree_Height, data=my_pdxTrees)
summary(tree_lm)
##
## Call:
## lm(formula = Carbon_Sequestration_lb ~ Crown_Width + Tree_Height,
## data = my_pdxTrees)
##
## Residuals:
## Min 1Q Median 3Q Max
## -87.395 -13.283 -4.912 10.982 121.950
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -3.08819 2.03721 -1.516 0.129853
## Crown_Width 0.88769 0.04947 17.944 < 2e-16 ***
## Tree_Height 0.10140 0.02848 3.560 0.000388 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 26.46 on 1031 degrees of freedom
## (5 observations deleted due to missingness)
## Multiple R-squared: 0.383, Adjusted R-squared: 0.3818
## F-statistic: 320 on 2 and 1031 DF, p-value: < 2.2e-16
## Diagnostic Plots
gglm(tree_lm)
my_tree <- rpart(Carbon_Sequestration_lb ~ Tree_Height + Crown_Width, data = my_pdxTrees,
control = rpart.control(cp = .005))
rpart.plot(my_tree,extra = 0, box.palette = "-Pu")
g4+
geom_parttree(data =my_tree, aes(fill=Carbon_Sequestration_lb,flipaxes = TRUE), alpha = 0.2 )+scale_colour_viridis_c(aesthetics = c('fill'), option = "magma", name = "Pred", begin = 0, end = 1 )
Crown_Width is the most important predictor of
Carbon_Sequestration_lb
After accounting for width, Tree_Height has some
impact on Carbon_Sequestration_lb
Very narrow and very wide trees tend to have low
Carbon_Sequestration_lb
Trees of moderate width and height have largest
Carbon_Sequestration_lb
my_pdxTrees_test <- get_pdxTrees_parks(park = c("Mt Scott Park", "Glenwood Park"))
my_pdxTrees_test<- my_pdxTrees_test %>% mutate(Crown_Width = (Crown_Width_EW + Crown_Width_NS)/2)
\[\textrm{rMSE} = \sqrt{\frac{1}{n} \sum_{i = 1}^n (y_i - \hat{y}_i)^2}\]
tree_preds<-predict(my_tree, newdata = my_pdxTrees_test)
Tree_rMSE <- sqrt(mean((tree_preds - my_pdxTrees_test$Carbon_Sequestration_lb)^2, na.rm = T))
data.frame(Tree_rMSE)
## Tree_rMSE
## 1 15.37258
lm_preds<-predict(tree_lm, my_pdxTrees_test)
lm_rMSE<-sqrt(mean((lm_preds - my_pdxTrees_test$Carbon_Sequestration_lb)^2, na.rm = T))
data.frame(lm_rMSE)
## lm_rMSE
## 1 16.87355
Why did the tree model outperform the linear model?
plotcp(my_tree, upper = "size")
results <- data.frame(model = "full", obs = my_pdxTrees_test$Carbon_Sequestration_lb, preds = predict(my_tree, my_pdxTrees_test))
pruned_tree <- prune(my_tree, cp = .01)
results <- results %>% rbind(data.frame(model = "pruned", obs = my_pdxTrees_test$Carbon_Sequestration_lb, preds = predict(pruned_tree, my_pdxTrees_test)))
very_pruned_tree <- prune(my_tree, cp = .021)
results <- results %>% rbind(data.frame(model = "very pruned", obs = my_pdxTrees_test$Carbon_Sequestration_lb, preds = predict(very_pruned_tree, my_pdxTrees_test)))
results <- results %>% rbind(data.frame(model = "linear", obs = my_pdxTrees_test$Carbon_Sequestration_lb, preds = lm_preds ))
results %>% group_by(model) %>% rmse(truth = obs, estimate = preds) %>% arrange(.estimate)
## # A tibble: 4 × 4
## model .metric .estimator .estimate
## <chr> <chr> <chr> <dbl>
## 1 pruned rmse standard 14.3
## 2 full rmse standard 15.4
## 3 linear rmse standard 16.9
## 4 very pruned rmse standard 16.9
Horizontal axis gives values of complexity parameter (cp)
Upper scale indicates number of terminal nodes for given tree
Vertical axis gives the cross-validated relative root mean squared error
Dotted horizontal line has height equal to 1 standard error above smallest rMSE
par(mfrow=c(1,3))
rpart.plot(my_tree, main = "Full Tree", extra = 0, box.palette = "-Pu", cex = 1)
rpart.plot(pruned_tree, main = "Pruned Tree", extra = 0, box.palette = "-Pu", cex = 1 )
rpart.plot(very_pruned_tree, main = "Very Pruned Tree", extra = 0, box.palette = "-Pu", cex = 1)
set.seed(1)
tree_model1 <- rpart(Carbon_Sequestration_lb ~
Tree_Height + Crown_Width,
data = my_pdxTrees)
set.seed(1)
tree_model2 <- rpart(Carbon_Sequestration_lb ~
Tree_Height + Crown_Width,
control = rpart.control(
minsplit = 20, xval = 10, maxdepth = 10, cp = 0.005),
data = my_pdxTrees)
plot(tree_model2)
text(tree_model2, pretty = 0, cex = .5)
rpart.plot(tree_model2)
plotcp(tree_model2)
pruned_tree <- prune(tree_model2, cp = 0.0077)
par(mfrow=c(1,2))
rpart.plot(tree_model2)
rpart.plot(pruned_tree)
results <- data.frame(model = "full",
obs = my_pdxTrees_test$Carbon_Sequestration_lb,
preds = predict(tree_model2, my_pdxTrees_test))
results <- rbind(results,
data.frame(model = "pruned",
obs = my_pdxTrees_test$Carbon_Sequestration_lb,
preds = predict(pruned_tree, my_pdxTrees_test)))
rmse from yardstick to
assess:results %>% group_by(model) %>%
rmse(truth = obs, estimate = preds) %>% arrange(.estimate)
## # A tibble: 2 × 4
## model .metric .estimator .estimate
## <chr> <chr> <chr> <dbl>
## 1 pruned rmse standard 14.2
## 2 full rmse standard 15.4
In this week we covered Tree based models.