El objetivo de este documento es mostrar como usar el nuevo paquete
de R para la base de datos Neotoma, neotoma2
.
El librería neotoma2 está disponible en GitHub y actualmente la documentación sólo está en inglés. Para instalar en R, se debe utilizar el paquete devtools de la siguiente forma:
devtools::install_github('NeotomaDB/neotoma2')
library(neotoma2)
En este tutorial, el usuario aprenderá a:
rasters
.neotoma2
En este libro de trabajo utilizaremos diferentes librerias incluyendo
leaflet
, sf
y otras. Para cargar las librerías
utilizaremos el paquete pacman
, que nos permite instalar
automáticamente cualquier librería que no exista en nuestro sistema.
options(warn = -1)
pacman::p_load(neotoma2, dplyr, ggplot2, sf, geojsonsf, leaflet, raster, DT)
Hay que recalcar que R es sensible al orden en que se cargan las
librerías. Es por esto que utilizaremos la notación
neotoma2::
para comunicarle a R explícitamente que queremos
usar la función del paquete neotoma2
. Esto es importante
porque hay funciones como filter()
(filtrar) que existen en
otros paquetes también como en el paquete dplyr
. Si
obtienes un error que se vea así:
Error in UseMethod("filter") :
no applicable method for 'filter' applied to an object of class "sites"
Esto quiere decir que estamos tratando de ejecutar la función
filter()
con el paquete incorrecto.Por eso, agregar
dplyr::
o neotoma2::
antes de la función (por
ejemplo, neotoma2::filter()
) es una buena práctica.
Si estás planeando trabajar con Neotoma, únete a nuestro grupo en Slack donde tenemos un canal específicamente para preguntas del paquete de R. También puedes unirte a nuestra lista de correos en nuestro Grupo de Google. Contáctanos en: para ser agregado.
get_sites()
Hay diferentes maneras de encontrar sitios en neotoma2
.
Debémos pensar en los sitios
como objetos espaciales.
Tienen nombre, ubicación y pueden ser encontrados bajo en contexto de
unidades geopolíticas. Sin embargo, bajo el contexto de la API y del
paquete de R, los sitios en sí mismos no contienen datos sobre la
taxonomía, el grupo de datos o las edades. Simplemente es un contenedor
al que le podemos agregar más información. Es así que cuando buscamos
por sitio, lo hacemos usando los siguientes atributos (en inglés):
siteid
- identificador del sitiositename
- nombre del sitiolocation
- ubicaciónaltitude
- altitud (máxima y mínima)gpid
- unidad geopolíticasitename="%Lait%"
Hay ocasiones en las que sabremos exactamente el nombre del sitio que estamos buscando (“Lac Mouton”), y habrà ocasiones en las que tendremos una idea aproximada sobre el nombre (por ejemplo, sabemos que el nombre es parecido a “Lait Lake”, o “Lac du Lait”, pero no estamos seguros de como fue ingresado a la base de datos).
De forma general, utilizamos el formato:
get_sites(sitename="XXXXX")
para buscar un sitio por
nombre.
PostgreSQL (y la API) utilizan el signo de porcentaje como comodín.
De esta forma, "%Lait%"
seleccionará “Lac du Lait” y en caso de
existir, también seleccionaría “Lake Lait” y “El Viejo Pantano
Lait”. La búsqueda tampoco distingue entre mayúsculas y
minúsculas, por lo que simplemente podría escribir
"%lait%"
.
spo_sites <- neotoma2::get_sites(sitename = "%Lait%")
plotLeaflet(spo_sites)
loc=c()
El paquete neotoma
utilizaba un cuadro delimitador para
buscar por ubicación. El cuadro estaba estructurado como un vector con
valores de latitud y longitud: c(xmin, ymin, xmax, ymax)
.
En neotoma2
se puede utilizar esta misma caja delimitadora
o podemos definir objetos espaciales más complejos con el paquete sf
. El
paquete sf
nos permite trabajar con datos ráster y
polígonos en R, para seleccionar sitios existentes en objetos espaciales
más complejos. El parametro loc
trabaja con vectores
simples, objetos WKT,
objetos geoJSON y
objectos sf
en R. Notar que el paquete
neotoma2
es un función contenedora API que utiliza un URL
(api.neotomadb.org). Los URL
están limitados a tener 1028 caracteres por lo que el API no acepta
llamadas demasiado largas.
Buscar sitios utilizando una ubicación. En el siguiente código hay
tres representaciones de Sudamérica: geoJSON, WKT y con un cuadro
delimitador. También hemos transformado el elemento
sa$geoJSON
a un objeto del paquete sf
. Podemos
utilizar cualquiera de estas cuatro representaciones para trabajar con
el paquete neotoma2
.
sa <- list(geoJSON = '{"type": "Polygon",
"coordinates": [[
[-79.66, -5.97],
[-70.06, -19.07],
[-74.38, -55.59],
[-34.67, -6.52],
[-76.41, 8.37],
[-79.66, -5.97]
]]}',
WKT = 'POLYGON ((-79.66, -5.97,
-70.06, -19.07,
-74.38, -55.59,
-34.67, -6.52,
-76.41, 8.37,
-79.66, -5.97))',
bbox = c(-79.66, -55.59, -34.67, 8.37))
sa$sf <- geojsonsf::geojson_sf(sa$geoJSON)
sa_sites <- neotoma2::get_sites(loc = sa$sf, all_data = TRUE)
Puedes siempre hacer un gráfico de los sites
obtenidos
con plot()
, pero los datos perderan el contexto geográfico.
La función plotLeaflet()
regresa un mapa de la librería
leaflet()
y permite mayor personalización o agregar datos
espaciales adicionales (como nuestro cuadro delimitador,
sa$sf
, que funciona directamente con el paquete
leaflet
):
neotoma2::plotLeaflet(sa_sites) %>%
leaflet::addPolygons(map = .,
data = sa$sf,
color = "green")
Si observamos al diagrama
UML para los objetos de neotoma2
podemos ver que hay un
conjunto de funciones qeu operan a nivel de sites
(sitios).
Conforme vamos agregando información a los objetos sites
mediante las funciones get_datasets()
o
get_downloads()
, podemos utilizar un mayor número de
funciones auxiliares. Podemos así, tomar ventaja de funciones como
summary()
para tener un mejor entendimiento de los
diferentes tipos de datos que tenemos en este conjunto de sitios. El
código a continuación regresa la tabla de resumen. Hacemos después un
poco de magia con R para cambiar el formato en que los datos están
siendo representados (convirtiéndolo a un objeto
datatable()
), pero la pieza principal es la llamada a la
función summary()
.
neotoma2::summary(sa_sites)
Podemos ver que no hay cronologías asociadas con el objeto
sites
. Esto es porque, por el moemnto, no hemos extraído la
información necesaria de los dataset
. Todo lo que sabemos,
tras la llamada get_sites()
son los tipos de conjuntos de
datos con los que contamos.
Sabemos que las unidades de colecta y los conjuntos de datos están
contenidos en los sitios. Similarmente, un objeto de tipo
sites
contienen collectionunits
que contienen
datasets
. En la tabla anterior podemos ver que algunos de
los sitios contienen registros de diatomeas. Dicho esto, solo tenemos la
información de sites
, pero por conveniencia, la API
devuelve información adicional sobre los conjuntos de datos lo que nos
permite navegar de manera más fácil los registros.
Con un objeto sites
podemos llamar directamente a la
función get_datasets()
, que nos permitirá extraer metadatos
sobre los conjuntos de datos. Podemos utilizar la función
datasets()
en cualqueir momento para obtener más
información de los conjuntos de datos que un objeto sites
pueda contener. Comparemos la información impresa
datasets(sa_sites)
contra una llamada similar utilizando el
siguiente código.
sa_datasets <- neotoma2::get_datasets(sa_sites, all_data = TRUE)
datasets(sa_datasets)
Si decidimos únicamente obtener registros de un sólo tipo de datos, o
si requerimos de mayor filtración, debemos considerar filtrar antes de
descargar todos los datos y muestras. Para ello, utilizaremos la función
filter()
. Por ejemplo, si requerimos únicamente los
registros de diatomeas con sus cronologías conocidas, podemos filtrar de
la siguiente forma:
sa_diatom <- sa_datasets %>%
neotoma2::filter(datasettype == "diatom" & !is.na(age_range_young))
neotoma2::summary(sa_diatom)
Podemos ver qeu la tabla de datos se ve diferente y que hay un número menor de sitios.
sample()
.Debido a que los datos de las muestras agregan mucha sobrecarga (para
Sudamérica, los datos de diatomeas, el objeto que incluye toda la
información de muestras es 20 veces mayor que el dataset
),
por eso llamamos la función get_downloads()
después de
haber hecho un filtrado preliminar. Después de
get_datasets()
, tenemos información sufciente para filtar
basados en ubicación, límites de tiempo y tipo conjunto de datos. Cuando
ejecutamosget_downloads()
podemos hacer un filtrado más
fino a nivel de unidad de análisis o nivel de taxón.
El siguiente comando puede tomar algo de tiepo. Por eso, hemos guardado el resultado en un archivo RDS. Puedes intentar correr este comando por tu cuenta o puedes cargar el archivo RDS.
## This line is commented out because we've already run it for you.
## sa_dl <- sa_diatom %>% get_downloads(all_data = TRUE)
sa_dl <- readRDS('data/saDownload.RDS')
Una vez que hemos hecho la descarga, ahora tenemos información de
cada sitio asociado a las unidades de colecta, los tipos de conjunto de
datos, y a todas las muestras asociadas a estos conjuntos. Para extraer
toda las muestras, utilizamos la función samples
:
allSamp <- samples(sa_dl)
Una vez hecho esto, obtenemos un data.frame
esto es una
tabla con 40393 renglones y 37 columnas. La razón de que esta tabla sea
muy larga es porque estamos obteniendo los datos en un formato
largo. Cada rengón contiene toda la información que se
necesita para interpretarse correctamente:
## [1] "age" "agetype" "ageolder" "ageyounger"
## [5] "chronologyid" "chronologyname" "units" "value"
## [9] "context" "element" "taxonid" "symmetry"
## [13] "taxongroup" "elementtype" "variablename" "ecologicalgroup"
## [17] "analysisunitid" "sampleanalyst" "sampleid" "depth"
## [21] "thickness" "samplename" "datasetid" "siteid"
## [25] "sitename" "lat" "long" "area"
## [29] "sitenotes" "description" "elev" "collunitid"
## [33] "database" "datasettype" "age_range_old" "age_range_young"
## [37] "datasetnotes"
Para algunos tipos de conjunto de datos o análisis específicos,
algunas columnas podrán no ser necesarias. Sin embargo, para otros
conjuntos de datos pueden ser críticamente importantes. Para permitir
que el paquete neotoma2
sea lo más útil posible para todos
los usuarios, hemos incluido todas las columnas posibles.
Si quieres saber que taxones existen en los registros, puedes
utilizar la función taxa()
en el objeto sites
.
La función taxa()
regresa los taxones únicos junto con dos
columnas adicionales sites
y samples
que
indican en cuantos sitios y en cuantas muestras el taxón aparece, esto
nos ayuda a comprender mejor que tan común es cada taxón individual.
neotomatx <- neotoma2::taxa(sa_dl)
Los valores obtenidos de taxonid
pueden ser unidos a la
columna taxonid
de la tabla obtenida con
samples()
. Esto nos permite hacer tablas de armonización si
así lo decidimos. También puedes notar que el nombre del taxón
taxonname
está en el campo variablename
. Los
recuentos de muestras individuales se reportan en Neotoma como variables
.
Una “variable” puede ser una especie, una medida en el laboratorio, un
proxy no orgánico, como carbón o medias XRF, e incluye las unidades de
medición y su valor.
Supongamos que queremos todas las muestras en las que los taxones
Plantago han sido reportados y se desea agruparlos bajo un
pseudo-taxon llamado Plantago. Hay varias formas de hacer esto,
ya sae directamente exportando el archivo y editando cada celda
individualemnte o creando una tabla externa de armonización (lo que se
hacía en el paquete anterior neotoma
).
Programáticamente, podemos armonizar taxón por taxón aplicando
comparaciones y transformaciones. Con la librería dplyr
podemos utilizar mutate()
para crear la columna
variablename
para que siempre que detecte
(str_detect()
) que el nombre de una variable
variablename
comienza con Plantago
(el
.*
representa un comodín para cualquier caracter
[.
], cero o más veces [*
]), podamos
reemplazarlo replace()
con el texto
"Plantago"
. Hay que observar que se cambiará
Plantago en el objeto allSamp
, pero podemos
reestaurar la información volviendo a llamarsamples()
regresando las taxonomías a su forma original.
Vamos a filtar los grupos ecológicos para incluir unicamente UPHE (altiplanicie/brezo) and TRSH (árboles y arbustos). Para más información de los grupos ecológicos consultar el Manual en Línea Neotoma (Disponible sólo en inglés).
allSamp <- allSamp %>%
dplyr::filter(ecologicalgroup %in% c("UPHE", "TRSH")) %>%
mutate(variablename = replace(variablename,
stringr::str_detect(variablename, "Plantago.*"),
"Plantago"))
Originalmente, había 0 taxones diferentes identificados con el género Plantago (incluyendo Plantago, Plantago major, y Plantago alpina-type). El código anterior reduce a todos a un único grupo taxonómico llamado Plantago.
Si quisieramos tener un artefacto con nuestras selecciones, podemos usar una tabla exteerna. Por ejemplo, una tabla de pares (información que queremos cambiar y el nombre con el que queremos reemplazar) puede ser creada y puede incluir expresiones regulares (regex) si así lo deseamos:
original | reemplazo |
---|---|
Abies.* | Abies |
Vaccinium.* | Ericaceae |
Typha.* | Aquatic |
Nymphaea | Aquatic |
… | … |
Podemos obtener los nombres originales directamente de la función
taxa()
, aplicada a un objeto de tipo sites
y
exportarla con write.csv()
.
taxaplots <- taxa(sa_dl)
# Save the taxon list to file so we can edit it subsequently.
readr::write_csv(taxaplots, "data/mytaxontable.csv")
La gráfica es simplemente ilustrativa pero podemos verificar las relaciones existentes tal cual hubieramos esperado.
Puedes después exportar una de estas tablas y agregar una columna con los recuentos; también se puede agregar información contextual extra tal como el grupo ecológico o el grupo de taxones para ayudarte. Una vez qeu la tabla de transición ha sido limpiada, puedes usarla y aplicar las transformaciones:
translation <- readr::read_csv("data/taxontable.csv")
Puedes observar qeu hemos cambiado algunos de los nombres de los
taxones en la tabla (no hay que ver más allá, esto es simplemente un
ejemplo). Para reemplazar los nombres en la salida de
samples()
, tenemos que unir las dos tablas usando
inner_join()
(esto quiere decir que
variablename
debe aparecer en ambas tablas para que el
resultado sea incluido), u luego seleccionaremos únicamente los
elementos de la tabla de muestras que son relevantes para nuestro
análisis:
allSamp <- samples(sa_dl)
allSamp <- allSamp %>%
inner_join(translation, by = c("variablename" = "variablename")) %>%
dplyr::select(!c("variablename", "sites", "samples")) %>%
group_by(siteid, sitename, replacement,
sampleid, units, age,
agetype, depth, datasetid,
long, lat) %>%
summarise(value = sum(value), .groups='keep')
Podemos utilizar paquetes como rioja
para hacer trazados
estratigráficos para un único registro. Pero primero tenemos que hacer
un manejo de datos diferente. A pesar de que podríamos hacer
armonización nuevamente, vamos a tomar los 10 taxones más comúnes en un
sitio dado los trazaremos en un diagrama estratigráfico.
Utilizaremos la función arrange()
para ordenar confrome
al número de veces que un taxón aparece en un núcleo. De esta forma,
podemos tomar las muestras y seleccionar los taxones que aparecen en las
diez primeras filas del marco de datos plottingTaxa
.
plottingSite <- sa_dl[[1]]
plottingTaxa <- taxa(plottingSite) %>%
filter(ecologicalgroup %in% c("DIAT")) %>%
filter(elementtype == "valve") %>%
arrange(desc(samples)) %>%
head(n = 10)
# Limpiar y seleccionar records de diatomeas NISP.
# Repetir filtros para diatomeas & grupos ecologicos en las muestras
shortSamples <- samples(plottingSite) %>%
filter(variablename %in% plottingTaxa$variablename) %>%
filter(ecologicalgroup %in% c("DIAT")) %>%
filter(elementtype == "valve") %>%
filter(units == "NISP")
# Transform to proportion values.
onesite <- shortSamples %>%
group_by(age) %>%
mutate(count = sum(value, na.rm = TRUE)) %>%
group_by(variablename) %>%
mutate(prop = value / count) %>%
arrange(desc(age))
# Spread the data to a "wide" table, with taxa as column headings.
widetable <- onesite %>%
dplyr::select(age, variablename, prop) %>%
mutate(prop = as.numeric(prop))
counts <- tidyr::pivot_wider(widetable,
id_cols = age,
names_from = variablename,
values_from = prop,
values_fill = 0)
Aparentemente, esto es una llamada compleja de comandos. Sin embargo,
el código es bastante sencillo y brinda un control significativo sobre
los taxones, unidades y otros elementos de tus datos antes de
transformarlos en una matriz ancha (depth
x
taxon
) que muchas herramientas estadísticas como los
paquetes vegan
o rioja
usan.
Para crear gráficas, podemos usar strat.plot()
del
paquete rioja
, ordenar los taxones usando puntajes promedio
ponderados (wa.order
). También se ha agregado un gráfico
CONISS al borde del gráfico, para mostrar cómo funciona el nuevo marco
de datos amplio con funciones métricas de distancia.
clust <- rioja::chclust(dist(sqrt(counts)),
method = "coniss")
plot <- rioja::strat.plot(counts[,-1] * 100, yvar = counts$age,
title = sa_dl[[1]]$sitename,
ylabel = "Calibrated Years BP",
xlabel = "Diatom (%)",
y.rev = TRUE,
clust = clust,
wa.order = "topleft", scale.percent = TRUE)
rioja::addClustZone(plot, clust, 4, col = "red")
Ahora tenemos información de sitios en toda Sudamérica, con muestras y nombres de taxones. Para observar las distribuciones de taxones a lo largo del tiempo, su presencia/ausencia, seleccionaremos los 20 taxones principales (según la cantidad de veces que aparecen en los registros) y observaré sus distribuciones en el tiempo
plottingTaxa <- taxa(plottingSite) %>%
filter(ecologicalgroup %in% c("DIAT")) %>%
filter(elementtype == "valve") %>%
arrange(desc(sites)) %>%
head(n = 20)
taxabyage <- samples(sa_dl) %>%
filter(variablename %in% plottingTaxa$variablename) %>%
group_by(variablename, "age" = round(age * 2, -3) / 2) %>%
summarise(n = length(unique(siteid)), .groups = 'keep')
samplesbyage <- samples(sa_dl) %>%
filter(variablename %in% plottingTaxa$variablename) %>%
group_by("age" = round(age * 2, -3) / 2) %>%
summarise(samples = length(unique(siteid)), .groups = 'keep')
groupbyage <- taxabyage %>%
inner_join(samplesbyage, by = "age") %>%
mutate(proportion = n / samples)
ggplot(groupbyage, aes(x = age, y = proportion)) +
geom_point() +
geom_smooth(method = 'gam',
method.args = list(family = 'binomial')) +
facet_wrap(~variablename) +
coord_cartesian(xlim = c(20000, 0), ylim = c(0, 1)) +
scale_x_reverse(breaks = c(10000, 20000)) +
xlab("Proportion of Sites with Taxon") +
theme_bw()
Podemos ver patrones de cambio claros, y los alizados se crean con
modelos aditivos generalizados (GAM) en R, por lo que podemos tener
control sobre el modelado real usando los paquetes gam
o
mgcv
. Dependiendo de cómo dividamos los datos, también
podemos observar los cambios de altitud, latitud o longitud para
comprender mejor cómo cambiaron las distribuciones y abundancias de
especies con el tiempo en esta región.
A menudo nos interesa la interacción entre los taxones y el clima,
asumiendo que el tiempo es un indicador de los entornos cambiantes. El
desarrollo de conjuntos de datos globales a gran escala para el clima ha
hecho que sea relativamente sencillo acceder a los datos de la nube en
formato raster. R proporciona una serie de herramientas (en los paquetes
sf
y raster
) para administrar datos espaciales
y brindar soporte para el análisis espacial de datos.
El primer paso es tomar nuestros datos de muestra y convertirlos en
un objeto espacial usando el paquete sf
en R:
modern <- samples(sa_dl) %>%
filter(age < 1000) %>%
filter(ecologicalgroup == "DIAT" & elementtype == "valve" & units == "NISP")
spatial <- sf::st_as_sf(modern,
coords = c("long", "lat"),
crs = "+proj=longlat +datum=WGS84")
Los datos son los mismos, el paquete sf
crea un objeto
llamado spatial
que es un marco de datos
data.frame
con toda la información extraída de
samples()
, y una columna (geometry
) que
contiene los datos espaciales.
Podemos utilizar la funcion getData()
del paquete raster
para obtener datos de WorldClim. Las
operaciones siguientes pueden ser aplicadas a cualquier tipo de datos
raster, asumiendo que haya sido cargada en R como un objeto de tipo
raster
.
A continuación extraeremos datos raster, a una resolucion de 10
minutos para la variable \(T_{max}\),
temperatura máxima mensual. El raster en sí mismo, tiene 12 capas, una
para cada mes. Con la función extract()
obtenemos la
información sólamente para el séptimo mes, julio.
worldTmax <- raster::getData('worldclim', var = 'tmax', res = 10)
spatial$tmax7 <- raster::extract(worldTmax, spatial)[,7]
Esto agrega una columna al marco de datos o data.frame
spatial
, que contiene la temperatura máxima de julio para
cada taxón en cada sitio. (todos los taxónes en un sitio compartirán el
mismo valor). Hemos filtrado anteriormente para obtener solo los taxones
UPHE, pero eso aún nos deja con 1 distintos nombres para los taxones.
Con la función de dplyr
mutate()
vamos a
extraer únicamente el género:
spatial <- spatial %>%
mutate(variablename = stringr::str_replace(variablename, "[[:punct:]]", " ")) %>%
mutate(variablename = stringr::word(variablename, 1)) %>%
group_by(variablename, siteid) %>%
summarise(tmax7 = max(tmax7), .groups = "keep") %>%
group_by(variablename) %>%
filter(n() > 3)
queremos obtener la distribución de fondo de las temperaturas de julio en Sudamérica, para graficar las distribuciones de los taxones contra el valor máximo de temperatura. Sin embargo, como todos los valores en el sitio son el mismo (porque utilizamos una superposición espacial), el máximo es el mismo que la temperatura real de julio en el sitio.
maxsamp <- spatial %>%
dplyr::group_by(siteid) %>%
dplyr::summarise(tmax7 = max(tmax7), .groups = 'keep')
Ahora, para graficarlo, utilizaremos facet_wrap()
para
graficar cada taxón en su propio panel:
ggplot() +
geom_density(data = spatial,
aes(x = round(tmax7 / 10, 0)), col = 2) +
facet_wrap(~variablename) +
geom_density(data = maxsamp, aes(x = tmax7 / 10)) +
xlab("Maximum July Temperature") +
ylab("Kernel Density")
Hemos hecho mucho en este ejemplo. 1) buscamos sitios utilizando nombres y parámetros geográficos. 2) filtramos los resultados utilizando parametros temporales y espaciales 3) obtuvimos información para conjuntos de datos seleccionados y 4) realizamos análisis básicos con información ráster para clima
Esperamos estos ejemplos puedan ser utilizados como plantillas para trabajo futuro o para hacer algo nuevo y divertido!