Introducción a Modelos Nulos: Ejercicio 1 Creado por: J. Sebastián Tello Center for Conservation and Sustainable Development Missouri Botanical Garden Objetivo: El propósito de esta sesión es familiarizarse con algunas de las capacidades básicas de R para realizar análisis de modelos nulos. En este ejercicio, vamos a ver algunas opciones que ya han sido implementadas como funciones en R. A pesar de que los modelos nulos han sido una herramienta importante en la ecología y la evolución desde hace varias décadas, todavía hay pocos paquetes que tienen funciones capaces de análisis de modelos nulos. Esto está cambiando, y es probable que en los próximos años muchos nuevos modelos nulos esten disponibles como funciones en R. Como ejemplos de cómo generar resultados de modelos nulos en R, vamos a utilizar dos tipos de análisis: patrones de coocurrencia y estructura filogenética de comunidades. 1. Patrones de co-ocurrencia y la competencia inter-específica En este primer ejemplo, vamos a realizar un análisis de modelos nulos para buscar evidencia de exclusión competitiva en la distribución de las especies a través de sitios o comunidades. Más concretamente, vamos a poner a prueba la predicción de que si la competencia es importante en la distribución de especies, entonces deberíamos observar bajos niveles de co-ocurrencia (es decir, alta segregación) entre especies a traves de comunidades. Este tipo de análisis representa una de las primeras importantes aplicaciones de los modelos nulos en ecología. Vamos a empezar por eliminar todo de la sesión actual de R. Esto no es necesario, pero se recomienda. De esta manera las cosas "viejas" no se pueden causar conlictos y genera problemas. Haga esto sólo si está seguro de que usted no necesita nada en su sesión de R existente: rm(list=objects()) A continuación, instalar (si es necesario) y cargar los paquetes vegan y bipartite que necesitaremos más adelante: install.packages(c("vegan","bipartite")) library(vegan) library(bipartite) Abra el set de datos de ácaros de vegan: data(mite) 1 Este conjunto de datos fue descrito por Borcard et al. (1992) y consiste en 70 muestras de musgo donde se contaron y se identificaron los ácaros de la familia orbatidae. Las muestras se recogieron en una zona 10x2.5m y provienen de una alfombra de musgo entre un bosque y un lago en Quebec, Canadá. Se trata de una matriz de la composición de especies, donde las filas son los sitios y las columnas son especies. Vamos a examinar brevemente las propiedades de este conjunto de datos: class(mite) # type of object mite <- as.matrix(mite) # this transforms the data frame into a matrix, which will simplify the code that follows class(mite) head(mite) # prints the first few rows of data – it shows that columns are species and rows are sites. Cell entries are abundances. dim(mite) # the dimensions of the matrix – it shows that we have 35 species and 70 sites Echemos un vistazo a la matriz en un gráfico. Observe la figura utiliza log + 1 valores de abundancia. Colores rojos indican abundancias bajas; amarillo a blancos indican abundancias altas: image(t(log(mite+1)), axes=FALSE, ylab="sites", xlab="species") Vamos a trabajar con sólo los datos de presencia / ausencia, así que vamos a crear una copia de los datos que contiene sólo 1s (presencia) y 0s (ausencia): mite.PA <- mite>0 # each element (cell) in "mite" is transformed to TRUE or FALSE depending on whether its value is greater than 0 or not. mode(mite.PA) <- "integer" # in R, TRUE = 1; FALSE = 0. head(mite) head(mite.PA) par(mfrow=c(1,2)) # creates a window with panels for two figures image(t(log(mite+1)), axes=FALSE, ylab="Sites", xlab="Species", main="Abundance") image(t(mite.PA), axes=FALSE, ylab="Sites", xlab="Species", main="Presence/Absence") Hay varias maneras de medir los niveles de co-ocurrencia en una matriz de composición, pero uno de los índices más utilizados en la literatura es el “C-score”, el cual vamos a utilizer en este ejemplo como estadístico que mide co-ocurrencia. Para calcular la C-score, primero hay que 2 calcular el número de "unidades de tablero de ajedrez" (checkererboard units) para cada par de especies. Una unidad de tablero de ajedrez es una sub-matriz de 2x2, donde la especie A está presente en el sitio 1, pero no en el sitio 2, mientras que las especies B está presente en el sitio 2, pero no en el sitio 1. Tal sub-matriz se vería así: A B 1 1 0 2 0 1 El C-score es simplemente el promedio de unidades de tablero de ajedrez entre todos los pares de species. Como tal, el C-score es una medida de la segregación, e inversamente relacionada con la co-ocurrencia. La Función C.score en el paquete bipartite calcula este valor para una matriz de presencia/ausencia. Vamos a calcular el C-score en los datos empíricos de las comunidades de ácaros: emp.C <- C.score(web=mite.PA, normalise=FALSE) emp.C En promedio, hay ~ 154 unidades de tablero de ajedrez para cada par de especies. Si la competencia es importante en la distribución de las especies de ácaros en nuestros datos, las especies co-existirian raramente con sus competidores conduciendo a un valor alto de C-score. Un algoritmo de aleatorización puede ayudar a determinar si este valor es mayor o menor de lo esperado en ausencia de interacciones entre especies. En vegan, hay funciones que implementan varios algoritmos para aleatorizar una matriz decomposición de especies. Vamos a comenzar con una prueba simple usando la función commsimulator: null.mite.PA <- commsimulator(x=mite.PA, method="r00") head(null.mite.PA) null.mite.PA tiene ahora el resultado de una única matriz aleatoria. Vamos a repetir el proceso un par de veces, y comparar gráficamente las matrices aleatorias con la empírica. En la figura creada a continuación, la matriz empírica está en el centro: par(mfrow=c(3,3)) for(i in 1:9) { if(i!=5) matrix.i <- commsimulator(x=mite.PA, method="r00") 3 if(i==5) matrix.i <- mite.PA image(t(matrix.i), axes=FALSE) } En todos los paneles, las columnas y las filas son las mismas. Nótese cómo ha cambiado la distribución de las presencias. En este ejemplo, el argumento method en commsimulator define el algoritmo utilizado para la aleatorización. Estamos tratando de encontrar un algoritmo que elimina los efectos de las interacciones entre especies en la distribución de las especies, en particular que elimina los efectos de la competencia inter-especifica. También estamos tratando de mantener en los datos de la estructura que no fue creada por la competencia. En este caso, dando el valor "r00" para el argumento method, hemos utilizado un algoritmo de "filas aleatorias - columnas aleatorias". Este algoritmo mantiene el número total de presencias (numero de 1s), pero estas presencias se aleatorizan completamente a través de la matriz. sum(mite.PA) sum(null.mite.PA) Como se puede ver, el número total de 1s en las matrices empírica y nula son los mismos. Este algoritmo ciertamente destruye todos los efectos de la competencia que pueden causar una relación entre la distribución de una especie y otra. Sin embargo, también elimina las diferencias en ocupancia (número de sitios ocupados, totales de las columnas) entre las especies y las diferencias en la riqueza de especies (totales de las filas) entre los sitios. par(mfrow=c(1,2)) plot(colSums(mite.PA)~ colSums(null.mite.PA), ylab="Empirical Occupancy", xlab="Null Occupancy") plot(rowSums(mite.PA)~ rowSums(null.mite.PA), ylab="Empirical Richness", xlab="Null Richness") Como se puede ver, la ocupancia y la riqueza son diferentes en las matrices empírica y nula. Los patrones de variación en la ocupancia y la riqueza podrían ser creados por procesos distintos a la competencia, por lo que puede ser que desee utilizar un algoritmo que 1) aleatoriza la distribución de las especies entre ellas, pero 2) conserva el número de sitios ocupados por cada especie y el número de especies observadas en cada sitio. Hay varias maneras de hacer esto, pero vamos a utilizar el algoritmo de "trial-swap" (method = "tswap"). En el sencillo ejemplo anterior, utilizamos la función commsimulator para crear matrices aleatorias una por una. La función oecosimu es una función envoltura ("wrapper") para 4 commsimulator la cual se puede utilizar para calcular un número N de de matrices y extraer una estadística en particular de cada uno. C.score.2 <- function(x) C.score(web=x, normalise=FALSE) # this is a small trick we need to calculate C.scores with the option normalize=FALSE. null.mite.res.r00 <- oecosimu(comm=mite.PA, nestfun=C.score.2, method="r00", nsimul=999, alternative="greater") null.mite.res.tswap <- oecosimu(comm=mite.PA, nestfun=C.score.2, method="tswap", nsimul=999, alternative="greater", burnin=100000, thin=10000) Para obtener más información sobre todas las opciones de algoritmos, vaya a la ayuda para la función oecosimu. Ahora, vamos a comparar los resultados empíricos y nulos generados por los dos algoritmos. null.mite.res.r00 hist(null.mite.res.r00$oecosimu$simulated, breaks=30, xlim=range(c(null.mite.res.r00$oecosimu$simulated,emp.C)), border="darkorange2", col="darkorange2", ylab="Number of Iterations", xlab="C-score", main="'r00' algorithm") lines(y=c(-100, 1000), x=c(emp.C,emp.C), lwd=3, col="black") El C-score empírico parece ser diferente de los valores nulos producidos por el algoritmo de "r00", pero lo es en la dirección opuesta a la esperada por competencia – la co-ocurrencia parece ser mayor de lo esperado por este modelo nulo. ¿Qué sucede con el algoritmo "tswap"? null.mite.res.tswap hist(null.mite.res.tswap$oecosimu$simulated, breaks=30, xlim=range(c(null.mite.res.tswap$oecosimu$simulated,emp.C)), border="darkorange2", col="darkorange2", ylab="Number of iterations", xlab="C-score", main="'Trial swap' algorithm") lines(y=c(-100, 1000), x=c(emp.C,emp.C), lwd=3, col="black") Cuando se utiliza un algoritmo más restrictivo, los resultados cambian drásticamente. En este caso, el C-score empírico parece ser significativamente diferente de lo esperado en la dirección predicha – la co-ocurrencia es menor de lo esperado por este modelo nulo. Para las comparaciones entre los resultados de diferentes conjuntos de datos (diferentes localidades, grupos de organismos, etc), a menudo es útil calcular el tamaño del efecto (“effect size”). Los tamaños del efecto se pueden interpretar típicamente como una medida de la fuerza de la estructura en los datos y por lo tanto la fuerza de los procesos que quedan fuera de la 5 aleatorización en el algoritmo del modelo nulo. Hay dos valores típicamente usados como tamaños del efecto: 1) la diferencia entre el valor empírico y la media de los valores nulos, y 2) esta diferencia estandarizada por la variación (desviación estándar) de los valores nulos. ES.r00 <- emp.C - mean(null.mite.res.r00$oecosimu$simulated) ES.tswap <- emp.C - mean(null.mite.res.tswap$oecosimu$simulated) SES.r00 <- ES.r00 / sd(null.mite.res.r00$oecosimu$simulated) SES.tswap <- ES.tswap / sd(null.mite.res.tswap$oecosimu$simulated) par(mfrow=c(1,2)) barplot(c(ES.r00, ES.tswap), ylab="Effect Size", names.arg=c("'r00'","'tswap'"), col="darkorange2") lines(x=c(-100, 100), y=c(0, 0), lwd=3, col="black") barplot(c(SES.r00, SES.tswap), ylab="Standardized Effect Size", names.arg=c("'r00'","'tswap'"), col="darkorange2") lines(x=c(-100, 100), y=c(0, 0), lwd=3, col="black") Al utilizar un modelo nulo, la elección del estadístico y el algoritmo es fundamental! Uno de los principales beneficios de los modelos nulos es su flexibilidad. Sin embargo, esta misma flexibilidad también puede ser un problema. Algoritmos deben ser considedos cuidadosamente a la luz del mecanismo de interés. Si el algoritmo no restringe la aleatorización de los datos lo suficiente, existe el riesgo de que más que el proceso de interés sea eliminado de los datos. Esto podría conducir a un rechazo fácil de la hipótesis nula, pero no porque el proceso de interés es importante en la estructuración de los datos. Por otro lado, si el algoritmo no eleatoriza los datos lo suficiente, hay un grave riesgo de que el proceso de interés no se elimine completamente de los datos. Esto puede conducir a un efecto significativo que no es detectado por el análisis (el efecto Narcissus). En el caso particular de los patrones de co-ocurrencia, ha habido una considerable cantidad de trabajo sobre las propiedades estadísticas de los modelos nulos. Desafortunadamente, la mayoría de otros modelos nulos no han recibido el mismo nivel de evaluación. 2. Estructura filogenética de comunidades Modificado de Kembel 2010: Una introducción al paquete de picante En los últimos ~ 15 años, datos filogenéticos se ha aprovechado para obtener información acerca de los mecanismos ecológicos y evolutivos detrás del ensamblaje de comunidades. Una de las principales formas en que esta información se ha utilizado es intentar separar los efectos de la competencia interespecífica y filtros por hábitat. Suponiendo que las especies cercanamente 6 relacionadas tienen ecologías similares y explotan recursos parecidos, la competencia predice que las especies que coexisten a escala local deben ser más alejadas filogenéticamente de lo esperado por el ensamblaje aleatorio de comunidades. Por otro lado, el filtrado por hábitat predice que las especies coexistentes deben estar más cercanamente relacionadas de lo esperado. En este ejemplo, utilizaremos un análisis de modelo nulo para estudiar si las comunidades ecológicas están compuestas de especies que son más o menos cercanamente relacionadas de lo esperado por el ensamblaje aleatorio de comunidades. Vamos a empezar abriendo de los paquetes y los datos. El paquete picante incluye un conjunto de datos llamado phylocom. Este set de datos tiene algunos datos artificiales que se incluyen también con el programa Phylocom. install.packages("picante") library(picante) data(phylocom) names(phylocom) Picante utiliza el formato phylo implementado en el paquete ape para representar relaciones filogenéticas entre taxones. Si usted tiene una filogenia en formato Newick o Nexus puede ser importado en R con las funciones read.tree o read.nexus. phy <- phylocom$phylo plot(phy) Picante utiliza el mismo formato de datos de comunidades que el paquete vegan - un marco de datos o matriz con sitios/muestras en las filas y taxones (especies) en las columnas. Los elementos de esta matriz o marco de datos deben ser valores numéricos que indican la abundancia o la presencia / ausencia (1/0) de taxones. comm <- phylocom$sample head(comm) image(t(log(comm +1)), axes=FALSE, ylab="Sites", xlab="Species") Hay muchas medidas de la estructura filogenética de las comunidades. Algunos de los más utilizados son MPD - la distancia filogenética media por parejas entre todas las especies en cada comunidad, y MNTD - la distancia filogenética media que separa a cada especie en la comunidad de su pariente más cercano. En picante, las funciones mpd y mntd pueden ser utilizadas para calcular estos valores. 7 Antes de utilizar estas funciones, sin embargo, tenemos que transformar la filogenia en una matriz de distancias filogenéticas utilizando la función cophenetic: phydist <- cophenetic(phy) phydist emp.mpd <- mpd(samp=comm, dis=phydist, abundance.weighted=FALSE) emp.mpd emp.mntd <- mntd(samp=comm, dis=phydist, abundance.weighted=FALSE) emp.mntd Tenga en cuenta que hay un valor para cada sitio / comunidad (es decir, para cada fila de la matriz comm). Tenga en cuenta también que, dado que las funciones mpd y mntd pueden utilizar cualquier matriz de distancia como entrada, podríamos calcular fácilmente medidas de estructura funcional de comunidades sustituyendo la matriz de distancias filogenética con una matriz de distancias de caracteres. Para averiguar si estos valores empíricos de MPD y MNTD son diferentes de lo esperado por el ensamblaje aleatorio de las comunidades (y la distribución aleatoria de las especies), tenemos que llevar a cabo un análisis de modelos nulos. Las funciones ses.mpd y ses.mntd en el paquete picante hacen esto: ses.mpd.result <- ses.mpd(samp=comm, dis=phydist, null.model="trialswap", abundance.weighted=FALSE, runs=999, iterations=100000) ses.mntd.result <- ses.mntd (samp=comm, dis=phydist, null.model="trialswap", abundance.weighted=FALSE, runs=999, iterations=100000) En estos ejemplos, tenga en cuenta que hemos utilizado otra vez el algoritmo trial-swap, el cual aleatoriza la distribución de las especies, pero mantiene la riqueza por comunidad y la ocupancia por especie. Para obtener información sobre todos los algoritmos posibles, consulte la ayuda R para estas funciones. ses.mpd.result ses.mntd.result Estos resultados para cada comunidad incluyen los valores observados, los valores medios producidos por el modelo nulo, y el valor de p que indica si existe una diferencia significativa 8 entre los valores observados y los esperados por la hipótesis nula (ensamblaje aleatorio de comunidades). Algunas comunidades muestran diferencias significativas con respecto al azar, y en la mayoría de los casos los valores observados son inferiores a la hipótesis nula. Esto sugiere que las especies de estas comunidades están más relacionados de lo esperado por la distribución aleatoria de las especies. Otra forma de crear distribuciones al azar (con respecto a la filogenia) es aleatorizar los nombres de taxones vinculados con las puntas del árbol filogenético. Este algoritmo es también una opción en estas funciones: ses.mpd.result.tip.rand <- ses.mpd(samp=comm, dis=phydist, null.model="taxa.labels", abundance.weighted=FALSE, runs=999) ses.mntd.result.tip.rand <- ses.mntd (samp=comm, dis=phydist, null.model="taxa.labels", abundance.weighted=FALSE, runs=999) ses.mpd.result.tip.rand ses.mntd.result.tip.rand Las funciones ses.mpd y ses.mntd también producen medidas del tamaño del efecto estandarizado (SES) de la estructura filogenética de las comunidades (mpd.obs.z o mntd.obs.z en el resultado de la función). Estos tamaños del efecto estandarizados están calculados como (1) la diferencia entre las distancias filogenéticas en las comunidades observadas frente a las comunidades nulas, y (2) esos valores divididos por las desviaciones estándar de las distancias filogenéticas en los datos nulos. par(mfrow=c(2,2)) barplot(ses.mpd.result$mpd.obs.z, ylab="MPD Standardized Effect Size", names.arg=rownames(comm), col="darkorange2", main="Trial-swap Algorithm") lines(x=c(-100, 100), y=c(0, 0), lwd=3, col="black") barplot(ses.mntd.result$mntd.obs.z, ylab="MNTD Standardized Effect Size", names.arg=rownames(comm), col="darkorange2", main="Trial-swap Algorithm ") lines(x=c(-100, 100), y=c(0, 0), lwd=3, col="black") barplot(ses.mpd.result.tip.rand$mpd.obs.z, ylab="MPD Standardized Effect Size", names.arg=rownames(comm), col="darkorange2", main="Tip Randomization Algorithm") lines(x=c(-100, 100), y=c(0, 0), lwd=3, col="black") 9 barplot(ses.mntd.result.tip.rand$mntd.obs.z, ylab="MNTD Standardized Effect Size", names.arg=rownames(comm), col="darkorange2", main="Tip Randomization Algorithm") lines(x=c(-100, 100), y=c(0, 0), lwd=3, col="black") Valores positivos de SES indican “segregación filogenética”, o una mayor distancia filogenética entre las especies coexistentes de lo esperado por el azar. Valores negativos de SES indican “agregación filogenética”, o distancias filogenéticas entre las especies coexistentes más pequeñas de lo esperado por el azar. Generalmente se considera que MPD es más sensibles a los patrones de agregación o segregación filogenética a nivel de todo el árbol, mientras que MNTD es más sensible a las patrones de agregación o segregación cerca de las puntas de la filogenia. Por ejemplo, la comunidad 'clump4' contiene especies que se distribuyen al azar en todo el árbol (SES de MPD cercano a cero), pero las especies están filogenéticamente agrupadas hacia las puntas (SES negativo de MNTD). Todas estas medidas pueden incorporar información de abundancia utilizando el argumento abundance.weighted. Esto va a cambiar la interpretación de estos medidas de distancias filogenéticas del promedio por especie a el promedio por individuos. 10