CR
Caio Graco Rodrigues
2 years ago

A couple of weeks ago there was a post about making a figure using some Functional diversity metrics (The figure is by Mouillout et al. 2013 - ). https://www.sciencedirect.com/science/article/pii/S0169534712002650#bbib0260

Since the answer for this is a bit long I decided to provide a separate post with all the code and a guideline on how to make it.

Please feel free to comment your doubts if any. The code is formatted as a Markdown, so you can just copy it here, paste it into a markdown file and run it.

This figure is based on data generated by {FD} package. Some of the figures will use the package {ggConvexHull }and {emstreeR}. Make sure you have these before running the code.
If you need a separate explanation of each figure in the Panel, please see the comments. I will post the code for each figure in there.
#begin of the code



Author: Caio Graco-Roza
output: html_document
editor_options:
chunk_output_type: console

First, we generate the data:


require(tidyverse)
require(FD)
require(emstreeR)
require(ggConvexHull)

data <- FD::simul.dbFD() #Generate random data.

#pass the simulated community data. I choose community 38 and 39 because of the number of species
comm <- data.frame(data$abun[38:39,]); rownames(comm)<-c("Before","After")

#Trait data
trait<- data.frame(data$traits);colnames(trait)<-c("T1","T2","T3")
trait <- trait %>% rownames_to_column("name") #Species name to column to use as index when we merge the comm and trait table


Regarding data preparation, we need to use the data in a long format for easier plotting. Also, we need only the trait information of the species that are present in a site so we first need to make a list object with the trait information of the species that are present in each site.


trait_comm<- comm %>%
rownames_to_column("Site") %>% #We need to make site as a column for using as index when making pivot_longer()
pivot_longer(cols=!Site) %>% #all species but not the sites
full_join(trait,by="name") %>% #merge add species trait values to each of the lines in the table
filter(value > 0) %>% #keep only species that are present
mutate_at("Site", factor, levels=c("Before","After")) %>% #Set the levels of the factor (just for the legend)
group_by(Site) %>% #group by site to estimate the centroid
mutate(centroid.T1 = mean(T1),
centroid.T2 = mean(T2)) %>% #centroid is simply the mean of coordinates in each axis
group_by(Site,name) %>%
mutate(dist.cen = sqrt(sum((c(T1,T2) - c(centroid.T1,centroid.T2)) ^ 2))) %>% #calculate the distance of each species to its centroid
ungroup() #ungroup the data


If one wants to estimate the centroid with abundance weights, just uncomment the lines of the following code

#Abundance weighted centroid

trait_centroid<- trait_comm %>%
group_by(Site) %>%
#mutate(value = value/sum(value)) %>% #uncomment for weighted centroid
#mutate(T1 = T1 * value) %>% #uncomment for weighted centroid
#mutate(T2 = T2 * value) %>% #uncomment for weighted centroid
summarise(across(T1:T2, ~mean(.x)))

Then we set the plot aesthetics

theme_set(theme_bw())
theme_update(
legend.text = element_text(size=10),
legend.position ="bottom",
legend.box="horizontal",
legend.margin=margin(),
panel.grid = element_blank(),
axis.text = element_blank(),
axis.ticks=element_blank(),
plot.title = element_text(face = "bold", size=14),
axis.title = element_text(size=12))

cpalette <- c("#E69F00", "#56B4E9") #The colours of the figure go here

Functional identity (Plot A)

(F_id<- trait_comm %>% 
ggplot(aes(x=T1,y=T2, colour=Site))+
geom_point(aes(size=value), alpha=.7) +
geom_point(data=trait_centroid, aes(x=T1,y=T2,fill=Site), shape=22,colour="black",size=7)+
labs(x="Trait 1", y="Trait 2", colour = "Species", size= "Abundance",title="Functional identity", fill="Centroid")+
guides(colour=guide_legend(order=1, nrow=1), size=guide_legend(order=2, nrow=1)) +
scale_colour_manual(values=cpalette)+
scale_fill_manual(values=cpalette)+
scale_size(range=c(1,7))+
coord_fixed(ylim=c(0,1),xlim=c(0,1)))


#Funcional richnes

Usually the convexhulls are estimated by the {geometry} pachage with the function convhulln. However, we only need species coordinates to make a plot.

#We will need the package ggConvexhull. install with devtools::install_github(%22cmartin/ggConvexHull%22)


(F_rich<- trait_comm %>%
ggplot(aes(x=T1,y=T2, colour= Site, fill=Site))+
geom_convexhull(alpha=.3)+
geom_point(shape=19,size = 2) +
geom_point(data=trait_centroid, aes(x=T1,y=T2,fill=Site),shape=22, colour="black",size=7)+
labs(x="Trait 1", y="Trait 2", colour = "Species", size= "Abundance",title="Functional Richness")+
scale_colour_manual(values=cpalette)+
scale_fill_manual(values=cpalette)+
coord_fixed(ylim=c(0,1),xlim=c(0,1)) +
theme(legend.position="none"))

Funcional eveness

Functional evennes is a bit more tricky because we need to estimate the minimum spanning tree of the communities. For this we will use the emstreeR package.


library(emstreeR)
#first for site one
mst_before <- trait_comm %>% filter(Site == "Before") %>% select(T1,T2) %>% data.frame() %>% ComputeMST()
#after for size two
mst_after <- trait_comm %>% filter(Site == "After") %>% select(T1,T2) %>% data.frame() %>% ComputeMST()
#then we rbind the table
mst_total <- bind_rows(mst_before, mst_after) #do.call(rbind, list(mst_before,mst_after)) also works

#One could also run all within an lapply function if the number of sites is too large for spliting manually

split_trait <- split(trait_comm, trait_comm$Site) #splits dataframe by site and outputs a list
list_MST <- lapply(split_trait, function(x) ComputeMST(x[,c("T1","T2")]))
mst_total <- map_df(list_MST, ~ data.frame(.x), .id="Site") #combine the list and create a column with the index of each list for easing the interpretation of the table

#add columns to dataset
trait_comm$from <- mst_total$from
trait_comm$to <- mst_total$to

Functional evenness

For the plot of the minimum spanning tree we will use the stat_MST(). We need one line of code for each stat_MST plot if we want to change the figure color. I still dont know how to solve this, but when I find out I will update the code.

(F_eve <- trait_comm %>% ggplot(aes(x=T1,y=T2, colour=Site, group=Site)) +
geom_point(aes(size=value,fill=Site), alpha=.7, shape=21)+
stat_MST(data = trait_comm %>% filter(Site == "Before"), aes(x=T1,y=T2,from=from,to=to), colour=cpalette[1])+
stat_MST(data = trait_comm %>% filter(Site == "After"), aes(x=T1,y=T2,from=from,to=to), colour=cpalette[2])+
labs(x="Trait 1", y="Trait 2", colour = "Species", fill = "Species",size= "Abundance", title="Functional Eveness", subtitle = "Dependency: *emstreeR*")+
guides(colour=guide_legend(order=1, nrow=1), size=guide_legend(order=2, nrow=1)) +
scale_colour_manual(values=cpalette)+
scale_fill_manual(values=cpalette)+
scale_size(range=c(1,7))+
coord_fixed(ylim=c(0,1),xlim=c(0,1))+
theme(legend.position = "none"))

Functional divergence

According to Mouillout et a. 2013 the divergence measures the relative contribution of the most extreme species to the community. For visualizing this, the authors make a circle with a radius equivalent to the distance from the most abundant species to its centroid. Larger radius mean that the most abundant species is far from the centroid and therefore is an functional outlier. As we already estimated the distance of the species to their centroids, we just need to select the most abundance one and see the distance

#Most abundant species
trait_max <- trait_comm %>%
group_by(Site) %>%
top_n(n=1, wt=value)

(F_div<- trait_comm %>%
group_by(Site) %>%
mutate(centroid.T1 = mean(T1),
centroid.T2 = mean(T2)) %>%
group_by(Site,name) %>%
mutate(dist.cen = sqrt(sum((c(T1,T2) - c(centroid.T1,centroid.T2)) ^ 2))) %>%
ggplot(aes(x=T1,y=T2))+
geom_point(aes(size=value, colour=Site), alpha=.3) +
geom_point(data=trait_centroid, aes(x=T1,y=T2,fill=Site),shape=22, colour="black",size=7)+
stat_ellipse(data = subset(trait_comm, Site == "Before"), aes(x=T1,y=T2), type="euclid", level=trait_max$dist.cen[1], colour=cpalette[1])+
stat_ellipse(data = subset(trait_comm, Site == "After"), aes(x=T1,y=T2), type="euclid", level=trait_max$dist.cen[2], colour=cpalette[2])+
scale_colour_manual(values=cpalette)+
scale_fill_manual(values=cpalette)+
scale_size(range=c(1,7))+
labs(x="Trait 1", y="Trait 2", colour = "Species", size= "Abundance",title="Functional divergence")+
guides(colour=guide_legend(order=1, nrow=1), size=guide_legend(order=2, nrow=1)) +
coord_fixed(ylim=c(0,1),xlim=c(0,1))+
theme(legend.position = "none"))

Functional dispersion

The dispersion is simply the average distance of all species to their centroid. We already have the distance estimated, we just need to use geom_segment() to draw the lines. I also like to outline the convex hull (this is optional) so I leave the geom_convexhull here too


(F_disp<- trait_comm %>%
group_by(Site) %>%
mutate(centroid.T1 = mean(T1),
centroid.T2 = mean(T2)) %>%
group_by(Site,name) %>%
mutate(dist.cen = sqrt(sum((c(T1,T2) - c(centroid.T1,centroid.T2)) ^ 2))) %>%
ggplot(aes(x=T1,y=T2, colour=Site))+
geom_convexhull(fill=NA)+
geom_point(aes(size=value, colour=Site), alpha=.3) +
geom_segment(aes(x=T1,xend=centroid.T1,y=T2,yend=centroid.T2), size=.1, linetype=2)+
geom_point(data=trait_centroid, aes(x=T1,y=T2,fill=Site),shape=22, colour="black",size=7)+
scale_colour_manual(values=cpalette)+
scale_fill_manual(values=cpalette)+
scale_size(range=c(1,7))+
labs(x="Trait 1", y="Trait 2", colour = "Species", size= "Abundance",title="Functional dispersion")+
guides(colour=guide_legend(order=1, nrow=1), size=guide_legend(order=2, nrow=1)) +
coord_fixed(ylim=c(0,1),xlim=c(0,1))+
theme(legend.position = "none"))

Functional entropy

The entropy is based on the Rao quadratic index, so to measure the entropy we will estimate the abundance differece between all species and connect the species with lines. I don't like this plot much, but since it is in the paper I am reproducing it too.


#We first redo the trait data because we modified it above

trait <- data.frame(data$traits); colnames(trait)<-c("T1","T2","T3")

#Here we make a table of pairwise coordinates and absolute differences in abundance (it can be the mean, but I am not sure)
entropy_data<-sapply(c("Before","After"), function(j){

subtrait <- trait[colnames(comm)[which(comm[j,]>0)],] #subtrait means only the the trait information of species present in site j
subcomm <- comm[j,which(comm[j,] > 0)]#subcomm means only the the abundance information of species present in site j

comb_dat<-t(combn(nrow(subtrait),2)) #combinatory matrix to extract the correct values for each possible combination
entropy_dat<-matrix(NA, nrow=choose(nrow(subtrait),2), ncol=5) #create empty matrix
colnames(entropy_dat) <- c("T1","T1_end","T2","T2_end","abun")
for (i in 1:nrow(entropy_dat)){ #fill it with for loop
entropy_dat[i,] <- c(subtrait[comb_dat[i,1],1], #coordinate x of spp 1
subtrait[comb_dat[i,2],1],#coordinate x of spp 2
subtrait[comb_dat[i,1],2], #coordinate y of spp 1
subtrait[comb_dat[i,2],2], #coordinate y of spp 2
as.numeric(abs(subcomm[comb_dat[i,1]]-subcomm[comb_dat[i,2]]))) #difference in abundance (absolute)
}
return(entropy_dat)
}, simplify=FALSE, USE.NAMES=TRUE) #return a list object with all pairwise for each site

pair_data_ent <- map_df(entropy_data, ~ data.frame(.x), .id="Site") #combine lists


Finally we can plot the data

 #Here, the size of the line depends on the abundance difference between species
(F_ent<-
trait_comm %>%
ggplot(aes(x=T1,y=T2, colour=Site))+
geom_segment(data = pair_data_ent, aes(x=T1,xend=T1_end,y=T2,yend=T2_end, alpha=abun, colour=Site))+
geom_point(aes(size=value), alpha=.5)+
geom_point(data=trait_centroid, aes(x=T1,y=T2,fill=Site),shape=22, colour="black",size=7)+
scale_colour_manual(values=cpalette)+
scale_fill_manual(values=cpalette)+
labs(x="Trait 1", y="Trait 2", colour = "Species", size= "Abundance",title="Functional entropy")+
guides(colour=guide_legend(order=1, nrow=1), size=guide_legend(order=2, nrow=1))+
coord_fixed()+
theme(legend.position = "none"))


Functional specialization

How each species is different from the overall mean trait value

(F_esp <- trait_comm %>% 
mutate(overall.cen.T1 = mean(T1),
overall.cen.T2 = mean(T2)) %>%
ggplot(aes(x=T1,y=T2, colour=Site))+
geom_point(aes(size=value), alpha=.7)+
geom_segment(aes(xend=T1,x=overall.cen.T1,yend=T2,y=overall.cen.T2), arrow=arrow(length = unit(0.01, "npc")), colour="black", size=.2)+
scale_colour_manual(values=cpalette)+
scale_fill_manual(values=cpalette)+
labs(x="Trait 1", y="Trait 2", colour = "Species", size= "Abundance",title="Functional especialization")+
guides(colour=guide_legend(order=1, nrow=1), size=guide_legend(order=2, nrow=1))+
coord_fixed()+
theme(legend.position = "none"))


Functional originality

The distance of the species from its nearest functional neighbour.
We will use same approach as in the entropy, but now we keep for each species, only the line with the closes neightbour. Note that instead of combn we now use expand grid, because the order of the comparison does not matter, meaning that even if sp60 is the nearest neighbour of sp36, we still want to evaluate the nearest neighbour using sp60 as the focal one.

trait <- data.frame(data$traits); colnames(trait)<-c("T1","T2","T3")

ori_data<-sapply(c("Before","After"), function(j){

subtrait <- trait[colnames(comm)[which(comm[j,]>0)],]
subcomm <- comm[j,which(comm[j,] > 0)]
comb_dat<-expand.grid(rownames(subtrait),rownames(subtrait))
entropy_dat<-matrix(NA, nrow(comb_dat), ncol=7)
colnames(entropy_dat) <- c("Focal_spp_1","Focal_spp_2","T1","T1_end","T2","T2_end","dist")
for (i in 1:nrow(comb_dat)) {
entropy_dat[i, ] <- c(
rownames(subtrait)[comb_dat[i,1]],
rownames(subtrait)[comb_dat[i,2]],#I need to add the name of the species as an index for later
subtrait[comb_dat[i,1], 1],
subtrait[comb_dat[i,2], 1],
subtrait[comb_dat[i,1], 2],
subtrait[comb_dat[i,2], 2],
as.numeric(dist(rbind(
subtrait[comb_dat[i,1], 1:2], subtrait[comb_dat[i,2],1:2 ]
)))
)
}
return(entropy_dat)
}, simplify=FALSE, USE.NAMES=TRUE)

ori_data<- lapply(ori_data, function(x){ data.frame(x) %>%
mutate_at(vars(-starts_with("Focal_spp")), as.numeric) %>%
mutate_at(vars(starts_with("Focal_spp")), as.factor)})

pair_data_ori <- map_df(ori_data, ~ data.frame(.x), .id="Site") %>%
group_by(Site, Focal_spp_1) %>% #we group by site and species so we have only one pair of species per site
slice_min(dist,n=2) %>%
filter(dist > 0)


Plot the originality

(F_ori <- trait_comm %>% 
mutate(overall.cen.T1 = mean(T1),
overall.cen.T2 = mean(T2)) %>%
ggplot(aes(x=T1,y=T2, colour=Site))+
geom_point(aes(size=value), alpha=.7)+
# ggrepel::geom_text_repel(aes(label=name))+
geom_segment(data= pair_data_ori, aes(xend=T1,x=T1_end,yend=T2,y=T2_end, colour=Site), size=.2)+
scale_colour_manual(values=cpalette)+
scale_fill_manual(values=cpalette)+
scale_size(range=c(0,7))+
labs(x="Trait 1", y="Trait 2", colour = "Species", size= "Abundance",title="Functional originality")+
guides(colour=guide_legend(order=1, nrow=1), size=guide_legend(order=2, nrow=1))+
coord_fixed()+
theme(legend.position = "none"))


Now we combine the plots using {patchwork}

library(patchwork)
F_id + F_rich + F_eve + F_div + F_disp + F_ent + F_esp + F_ori + plot_layout(nrow=2, guides="collect") +plot_annotation(tag_levels = "A", title = "Functional diversity metrics")

310 Likes

Replies

Caio Graco Rodrigues 2 years ago

Functional evenness:

library(emstreeR) #first for site one mst_before <- trait_comm %>% filter(Site == "Before") %>% select(T1,T2) %>% data.frame() %>% ComputeMST() #after for size two mst_after <- trait_comm %>% filter(Site == "After") %>% select(T1,T2) %>% data.frame() %>% ComputeMST() #then we rbind the table mst_total <- bind_rows(mst_before, mst_after) #do.call(rbind, list(mst_before,mst_after)) also works

#One could also run all within an lapply function if the number of sites is too large for spliting manually

split_trait <- split(trait_comm, trait_comm$Site) #splits dataframe by site and outputs a list list_MST <- lapply(split_trait, function(x) ComputeMST(x[,c("T1","T2")])) mst_total <- map_df(list_MST, ~ data.frame(.x), .id="Site") #combine the list and create a column with the index of each list for easing the interpretation of the table

#add columns to dataset trait_comm$from <- mst_total$from trait_comm$to <- mst_total$to

(F_eve <- trait_comm %>% ggplot(aes(x=T1,y=T2, colour=Site, group=Site)) + geom_point(aes(size=value,fill=Site), alpha=.7, shape=21)+ stat_MST(data = trait_comm %>% filter(Site == "Before"), aes(x=T1,y=T2,from=from,to=to), colour=cpalette[1])+ stat_MST(data = trait_comm %>% filter(Site == "After"), aes(x=T1,y=T2,from=from,to=to), colour=cpalette[2])+ labs(x="Trait 1", y="Trait 2", colour = "Species", fill = "Species",size= "Abundance", title="Functional Eveness")+ guides(colour=guide_legend(order=1, nrow=1), size=guide_legend(order=2, nrow=1)) + scale_colour_manual(values=cpalette)+ scale_fill_manual(values=cpalette)+ scale_size(range=c(1,7))+ coord_fixed(ylim=c(0,1),xlim=c(0,1))+ theme(legend.position = "none"))

1 Like
Shreya Nemani 2 years ago

Mary Clinton

1 Like
Uriel CC 2 years ago

Thank you so much for sharing it!

1 Like
Caio Graco Rodrigues 2 years ago

Functional dispersion:

(F_disp<- trait_comm %>% group_by(Site) %>% mutate(centroid.T1 = mean(T1), centroid.T2 = mean(T2)) %>% group_by(Site,name) %>% mutate(dist.cen = sqrt(sum((c(T1,T2) - c(centroid.T1,centroid.T2)) ^ 2))) %>% ggplot(aes(x=T1,y=T2, colour=Site))+ geom_convexhull(fill=NA)+ geom_point(aes(size=value, colour=Site), alpha=.7) + geom_segment(aes(x=T1,xend=centroid.T1,y=T2,yend=centroid.T2), size=.1, linetype=2)+ geom_point(data=trait_centroid, aes(x=T1,y=T2,fill=Site),shape=22, colour="black",size=7)+ scale_colour_manual(values=cpalette)+ scale_fill_manual(values=cpalette)+ scale_size(range=c(1,7))+ labs(x="Trait 1", y="Trait 2", colour = "Species", size= "Abundance",title="Functional dispersion")+ guides(colour=guide_legend(order=1, nrow=1), size=guide_legend(order=2, nrow=1)) + coord_fixed(ylim=c(0,1),xlim=c(0,1))+ theme(legend.position = "none"))

3 Likes
Caio Graco Rodrigues 2 years ago

Functional originality:

(F_ori <- trait_comm %>% mutate(overall.cen.T1 = mean(T1), overall.cen.T2 = mean(T2)) %>% ggplot(aes(x=T1,y=T2, colour=Site))+ geom_point(aes(size=value), alpha=.7)+

ggrepel::geom_text_repel(aes(label=name))+

geom_segment(data= pair_data_ori, aes(xend=T1,x=T1_end,yend=T2,y=T2_end, colour=Site), size=.2)+
 scale_colour_manual(values=cpalette)+
 scale_fill_manual(values=cpalette)+
scale_size(range=c(0,7))+
labs(x="Trait 1", y="Trait 2", colour = "Species", size= "Abundance",title="Functional originality")+
guides(colour=guide_legend(order=1, nrow=1), size=guide_legend(order=2, nrow=1))+
coord_fixed()+
theme(legend.position = "none"))
3 Likes
Franco Teixeira de Mello 2 years ago

Giancarlo Tesitore Margenny Barrios

0 Likes
Aldo Zavala Jiménez 2 years ago

Diana Morales

1 Like
Karen Elizabeth Peña Joya 2 years ago

Veronica Rosas Fabian Rodriguez

1 Like
Omar Valencia Méndez 2 years ago

Juan Perusquía Bren Maya Patricia Silva Montoya

4 Likes
Kate Sheridan 2 years ago
1 Like
Relate Discussions