Código
# Oxígeno disuelto promedio (mg/L) en las seis estaciones, de cabecera a desembocadura
oxigeno <- c(8.2, 7.8, 6.7, 4.4, 5.9, 6.2)
oxigeno[1] 8.2 7.8 6.7 4.4 5.9 6.2
| Módulo | 2 de 6 — Entorno RStudio y manejo de datos |
| Intensidad del módulo | 3 horas (curso completo: 16 horas) |
| Modalidad | Virtual asincrónica (autoguiada), con sesiones en vivo de acompañamiento |
| Dirigido a | Profesionales egresados en Biología |
| Herramientas | R, RStudio, tidyverse, Quarto y asistentes de IA |
| Institución | Universidad de La Guajira — Facultad de Ciencias Básicas y Aplicadas, Programa de Biología |
| Coordinación académica | Dr. Cristian Enrique Granados Martínez |
Sobre el autor
Cristian Enrique Granados Martínez es biólogo y Doctor en Ciencias Naturales para el Desarrollo (DOCINADE, Instituto Tecnológico de Costa Rica), con énfasis en Gestión de Recursos Naturales. Es docente-investigador del Programa de Biología de la Universidad de La Guajira, donde coordina el Comité de Investigación y Extensión de la Facultad de Ciencias Básicas y Aplicadas, e integra los grupos de investigación en Biotecnología y en Ecología y Biodiversidad de Ecosistemas Tropicales (EBET). Es Investigador Asociado reconocido por Minciencias.
Con más de 17 años de experiencia de campo en la Sierra Nevada de Santa Marta y La Guajira, su investigación se centra en los macroinvertebrados acuáticos, la limnología y la biodiversidad de agua dulce del Caribe colombiano. Ha descrito cuatro especies nuevas para la ciencia y es autor de más de 28 artículos indexados, 4 libros y 17 capítulos de libro.
ORCID: 0000-0003-0161-9537 · Sitio web: biolocristiam.github.io
Cómo citar esta guía: Granados-Martínez, C. E. (2026). Guía de R y RStudio con IA como copiloto. Módulo 2: Entorno RStudio y manejo de datos biológicos. Curso Inteligencia Artificial aplicada a los datos en Ciencias Biológicas. Universidad de La Guajira, Riohacha, Colombia.
Esta guía reúne en un solo recorrido todo lo que necesitas para trabajar con datos biológicos en R, desde abrir RStudio por primera vez hasta producir una matriz de comunidad lista para los análisis de diversidad y bioindicación de los módulos siguientes.
Está organizada en tres bloques. Al final de cada uno hay un punto de control: un ejercicio corto que te dice si estás listo para seguir.
| Bloque | Contenido | Tiempo |
|---|---|---|
| A | RStudio, proyectos, Quarto y la IA como copiloto; fundamentos de R | ≈ 60 min |
| B | Importar, limpiar, transformar y visualizar datos con tidyverse | ≈ 85 min |
| C | Datos abiertos de biodiversidad (GBIF e iNaturalist) | ≈ 35 min |
A lo largo del texto encontrarás cuatro tipos de recuadros:
Buenas prácticas y atajos que te ahorran tiempo.
Una instrucción lista para copiar en tu asistente de IA. Úsala, modifícala y compara lo que te responde con lo que dice esta guía.
Errores frecuentes o situaciones en las que la IA suele equivocarse.
Ejercicio para verificar lo aprendido, con solución desplegable.
Al terminar este módulo podrás:
Durante todo el módulo trabajaremos con un único caso de estudio: el monitoreo de macroinvertebrados acuáticos y variables fisicoquímicas en seis estaciones de un río de la vertiente norte de la Sierra Nevada de Santa Marta, desde la cabecera (≈ 870 m) hasta la desembocadura (≈ 15 m), en época seca y de lluvias, con tres réplicas por estación.
Los datos son simulados con fines didácticos. Reproducen patrones realistas (gradiente de perturbación, efecto de una descarga municipal, arrastre en lluvias), pero no corresponden a un muestreo real y no deben citarse como tales.
Descarga del aula virtual los tres archivos y guárdalos en una carpeta llamada datos/ dentro de tu proyecto (sección Sección 2.2):
| Archivo | Contenido | Formato |
|---|---|---|
macroinvertebrados.csv |
Una fila por familia encontrada en cada muestra: orden, familia y abundancia | Largo |
fisicoquimicos.xlsx |
Una fila por muestra: temperatura, oxígeno disuelto, pH, conductividad, turbidez, velocidad | Ancho |
estaciones.csv |
Altitud, coordenadas y uso del suelo de las seis estaciones | Tabla de referencia |
Los archivos traen a propósito algunos errores típicos de digitación y datos faltantes, como los que aparecen en cualquier libreta de campo. Aprenderás a detectarlos y corregirlos en el Bloque B.
R es un lenguaje de programación libre y gratuito, creado para el análisis estadístico y la visualización de datos. Es el estándar de facto en ecología: paquetes como vegan, iNEXT o betapart existen principalmente en R.
RStudio es el entorno de desarrollo (IDE) que usamos para escribir y ejecutar código R de forma cómoda. R es el motor; RStudio es el tablero.
Para el trabajo en biología, R permite:
Instala en este orden:
Si ya tenías R instalado hace más de un año, actualízalo. Muchos errores de instalación de paquetes se deben a versiones antiguas de R.
RStudio se divide en cuatro paneles:
| Panel | Para qué sirve |
|---|---|
| Editor (arriba a la izquierda) | Escribir y guardar scripts (.R) y documentos Quarto (.qmd) |
| Consola (abajo a la izquierda) | Donde R ejecuta el código y muestra los resultados |
| Entorno / Historial (arriba a la derecha) | Los objetos creados en la sesión (tablas, vectores, modelos) |
| Archivos / Gráficos / Paquetes / Ayuda (abajo a la derecha) | Explorar carpetas, ver gráficos, consultar la ayuda |
Si tienes activado un asistente de IA (sección Sección 2.4), aparecerá además un panel lateral de chat.
Un proyecto de RStudio es una carpeta con un archivo .Rproj que le dice a RStudio: “todo lo de este análisis vive aquí”. Al abrir el proyecto, el directorio de trabajo se ubica automáticamente en esa carpeta y las rutas relativas (datos/archivo.csv) funcionan en cualquier computador.
Para crearlo:
curso_ia_modulo2.curso_ia_modulo2/
├── curso_ia_modulo2.Rproj
├── datos/ ← archivos originales (¡nunca se modifican!)
├── resultados/ ← tablas generadas por el análisis
├── figuras/ ← gráficos exportados
└── modulo2.qmd ← tu documento de trabajo
Evita setwd("C:/Users/...") al inicio de tus scripts. Esa ruta solo existe en tu computador; si compartes el análisis, fallará en cualquier otro. Con proyectos no la necesitas.
RStudio permite trabajar de dos formas:
.R (File → New File → R Script): solo código y comentarios. Es ideal para funciones o pruebas rápidas..qmd (File → New File → Quarto Document): mezcla texto, código y resultados en un solo informe que se renderiza a HTML, Word o PDF. Esta guía está escrita en Quarto.Un documento Quarto tiene tres partes:
---
title: "Mi análisis de macroinvertebrados"
author: "Tu nombre"
format: html
---
## Introducción
Texto normal en *Markdown*: **negritas**, listas, enlaces...
```{r}
# Bloque de código R (chunk)
mean(c(8.2, 7.8, 6.7))
```---, define el título, el autor y el formato de salida.Ctrl + Alt + I.El botón Render (Ctrl + Shift + K) ejecuta todo el código desde cero y genera el informe. Si el documento renderiza sin errores, tu análisis es reproducible.
Crea un documento modulo2.qmd y ve copiando y ejecutando allí los bloques de código de esta guía. Al final tendrás tu propio informe del módulo.
En este curso la IA no reemplaza tu criterio estadístico ni biológico: es un copiloto que escribe borradores de código, explica errores y sugiere alternativas. El piloto, quien decide y responde por el análisis, eres tú.
| Opción | Cómo funciona | Costo | Ventaja principal |
|---|---|---|---|
| Posit Assistant (servicio Posit AI) | Agente integrado en el panel lateral de RStudio. Ve tu sesión: paquetes cargados, tablas en memoria, errores | Suscripción desde USD 20/mes, con prueba gratuita | Responde con base en tus datos reales; puede ejecutar código |
| GitHub Copilot | Autocompletado de código mientras escribes, dentro del editor | Plan gratuito con límites; gratuito para docentes y estudiantes verificados | Sugerencias en línea, rápido |
| Chat externo (Claude, ChatGPT, Gemini…) | Copias código, errores o la estructura de tus datos y pegas la respuesta | Planes gratuitos disponibles | Accesible para todos; no requiere configuración |
Para seguir el curso basta con un chat externo gratuito. Si tienes acceso a Posit Assistant o Copilot, aprovéchalos.
Posit Assistant. Actualiza RStudio a la versión más reciente, haz clic en el ícono de Posit Assistant en la barra de herramientas y crea tu cuenta de Posit AI cuando te lo pida.
GitHub Copilot. Necesitas una cuenta de GitHub con Copilot activo. En RStudio ve a Tools → Global Options → Copilot, marca Enable GitHub Copilot e inicia sesión.
Chat externo. Abre el asistente en tu navegador junto a RStudio. Es la opción más flexible, pero recuerda que la IA no ve tu sesión: tú debes darle el contexto.
La calidad de la respuesta depende del contexto que des. Un buen prompt tiene cinco elementos:
str() o glimpse(), nunca la base completa).Actúa como un ecólogo experto en R y tidyverse.
Tengo un data frame llamado `macro` con esta estructura:
muestra (chr), estacion (chr), epoca (chr), orden (chr),
familia (chr), abundancia (dbl)
Necesito calcular, para cada estación y época, la riqueza de
familias y la abundancia total.
Usa dplyr con el operador |>. No uses bucles for.
Explica cada línea con un comentario breve.
Compáralo con “hazme la riqueza en R”: con esa instrucción la IA tendría que adivinar el nombre de tus columnas, tus paquetes y tu nivel.
?nombre_funcion.str()) o un ejemplo inventado..qmd cuándo y para qué usaste la IA. Es parte de la transparencia científica.En R guardamos información en objetos con el operador de asignación <- (atajo: Alt + -):
# Oxígeno disuelto promedio (mg/L) en las seis estaciones, de cabecera a desembocadura
oxigeno <- c(8.2, 7.8, 6.7, 4.4, 5.9, 6.2)
oxigeno[1] 8.2 7.8 6.7 4.4 5.9 6.2
# Operaciones sobre todo el vector a la vez (vectorización)
oxigeno_porcentaje_max <- oxigeno / max(oxigeno) * 100
round(oxigeno_porcentaje_max, 1)[1] 100.0 95.1 81.7 53.7 72.0 75.6
Las funciones reciben argumentos entre paréntesis y devuelven un resultado:
mean(oxigeno) # promedio[1] 6.533333
round(sd(oxigeno), 2) # desviación estándar, redondeada a 2 decimales[1] 1.38
length(oxigeno) # número de elementos[1] 6
Para consultar la ayuda de cualquier función escribe en la consola ?mean o help(mean). La sección Examples, al final de cada página de ayuda, suele ser lo más útil.
| Atajo | Acción |
|---|---|
Ctrl + Enter |
Ejecuta la línea actual o la selección |
Ctrl + Shift + Enter |
Ejecuta todo el bloque o script |
Alt + - |
Inserta <- |
Ctrl + Shift + M |
Inserta el pipe |> |
Ctrl + Alt + I |
Inserta un bloque de código en Quarto |
Ctrl + Shift + K |
Renderiza el documento Quarto |
Ctrl + Shift + C |
Comenta o descomenta líneas |
Tab |
Autocompleta nombres de objetos y funciones |
En macOS reemplaza Ctrl por Cmd y Alt por Option.
Para que Ctrl + Shift + M inserte el pipe nativo |> en lugar de %>%, activa Tools → Global Options → Code → Use native pipe operator.
Los datos biológicos combinan distintos tipos de información, y R tiene un tipo para cada uno:
# Numérico (double): mediciones continuas
conductividad <- c(52, 98, 160, 410, 255, 230)
# Entero: conteos (la L indica entero)
individuos_baetidae <- c(84L, 61L, 22L, 0L, 9L, 17L)
# Carácter: nombres y categorías de texto
familias <- c("Baetidae", "Leptophlebiidae", "Perlidae", "Chironomidae")
# Lógico: verdadero/falso
es_EPT <- c(TRUE, TRUE, TRUE, FALSE)
# Factor: variables categóricas con niveles definidos
epoca <- factor(c("Seca", "Lluvias", "Seca", "Lluvias"),
levels = c("Seca", "Lluvias"))
class(conductividad); class(individuos_baetidae); class(familias)[1] "numeric"
[1] "integer"
[1] "character"
class(es_EPT); levels(epoca)[1] "logical"
[1] "Seca" "Lluvias"
Con un factor tú decides el orden de los niveles, y ese orden se respeta en tablas y gráficos. Sin factor, R ordena alfabéticamente: “Lluvias” quedaría antes que “Seca” y las estaciones E10 antes que E2.
familias[2] # el segundo elemento[1] "Leptophlebiidae"
familias[es_EPT] # solo las familias EPT (indexación lógica)[1] "Baetidae" "Leptophlebiidae" "Perlidae"
conductividad[conductividad > 200] # estaciones con conductividad alta[1] 410 255 230
Un data frame organiza los datos en filas (observaciones) y columnas (variables). Cada columna es un vector y todas tienen la misma longitud. Es el equivalente a una hoja de Excel bien organizada.
estaciones_mini <- data.frame(
estacion = paste0("E", 1:6),
oxigeno_mg_l = oxigeno,
conductividad = conductividad,
baetidae = individuos_baetidae
)
estaciones_miniestaciones_mini$oxigeno_mg_l # una columna con $[1] 8.2 7.8 6.7 4.4 5.9 6.2
estaciones_mini[estaciones_mini$oxigeno_mg_l < 5, ] # filas que cumplen una condiciónsummary(estaciones_mini$conductividad) Min. 1st Qu. Median Mean 3rd Qu. Max.
52.0 113.5 195.0 200.8 248.8 410.0
quantile(estaciones_mini$conductividad, probs = c(0.1, 0.5, 0.9)) 10% 50% 90%
75.0 195.0 332.5
# Coeficiente de variación (%): dispersión relativa a la media
cv <- sd(conductividad) / mean(conductividad) * 100
round(cv, 1)[1] 63.7
R base también hace gráficos rápidos, útiles para explorar antes de invertir tiempo en un gráfico final:
plot(estaciones_mini$oxigeno_mg_l, estaciones_mini$baetidae,
xlab = "Oxígeno disuelto (mg/L)", ylab = "Individuos de Baetidae",
pch = 19, col = "steelblue")Para gráficos de publicación usaremos ggplot2 (sección Sección 3.8).
Ejercicio. Crea un vector con la temperatura (°C) de las seis estaciones: 19.7, 22.3, 23.9, 26.4, 27.1, 28.0. Luego:
temperatura a estaciones_mini.Pregúntale a tu copiloto por qué el oxígeno disuelto tiende a bajar cuando sube la temperatura, y verifica su respuesta con tus conocimientos de limnología.
Solución:
temperatura <- c(19.7, 22.3, 23.9, 26.4, 27.1, 28.0)
mean(temperatura); sd(temperatura)[1] 24.56667
[1] 3.188521
estaciones_mini$temperatura <- temperatura
estaciones_mini[estaciones_mini$temperatura > 25 & estaciones_mini$oxigeno_mg_l < 6, ]Un paquete es una colección de funciones, datos y documentación que amplía lo que R puede hacer. Es como instalar una aplicación en el celular.
# Instalar: UNA sola vez por computador
install.packages(c("tidyverse", # dplyr, ggplot2, readr, tidyr, stringr...
"readxl", # leer archivos de Excel
"janitor", # limpiar nombres de columnas
"writexl", # exportar a Excel
"rgbif")) # descargar datos de GBIF (Bloque C)# Cargar: al inicio de CADA sesión o documento
library(tidyverse)
library(readxl)
library(janitor)install.packages() se ejecuta una vez. library() va al inicio de cada documento. No dejes install.packages() dentro de tu .qmd: se reinstalaría cada vez que renderizas.
Al cargar tidyverse verás mensajes como dplyr::filter() masks stats::filter(). Significa que dos paquetes tienen una función con el mismo nombre y se usará la del último cargado. Si alguna vez necesitas la otra, llámala explícitamente: stats::filter().
read_csv()macro_crudo <- read_csv("datos/macroinvertebrados.csv")
estaciones <- read_csv("datos/estaciones.csv")read_excel()fq_crudo <- read_excel("datos/fisicoquimicos.xlsx", sheet = "fisicoquimicos")read_csv() de readr es más rápido que read.csv() de R base, detecta los tipos de columna con más acierto y no convierte el texto en factores sin avisar. Si tu CSV usa ; como separador y , como decimal (configuración regional de Excel en español), usa read_csv2().
Nunca analices sin antes mirar los datos. Estas funciones te dicen qué hay en ellos:
glimpse(macro_crudo) # estructura: columnas, tipos y primeros valoresRows: 775
Columns: 8
$ muestra <chr> "E1-S-R1", "E1-S-R1", "E1-S-R1", "E1-S-R1", "E1-S-R1", "E1-…
$ estacion <chr> "E1", "E1", "E1", "E1", "E1", "E1", "E1", "E1", "E1", "E1",…
$ epoca <chr> "Seca", "Seca", "Seca", "Seca", "Seca", "Seca", "Seca", "Se…
$ replica <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
$ fecha <date> 2025-02-18, 2025-02-18, 2025-02-18, 2025-02-18, 2025-02-18…
$ orden <chr> "Amphipoda", "Basommatophora", "Basommatophora", "Coleopter…
$ familia <chr> "Hyalellidae", "Physidae", "Planorbidae", "Elmidae", "Pseph…
$ abundancia <dbl> 5, 30, 7, 49, 4, 109, 2, 25, 7, 50, 131, 12, 29, 16, 2, 5, …
head(estaciones) # primeras filasnames(fq_crudo) # nombres de columnas [1] "Muestra" "Estación"
[3] "Época" "Réplica"
[5] "Fecha muestreo" "Temperatura (°C)"
[7] "Oxígeno disuelto (mg/L)" "pH"
[9] "Conductividad (µS/cm)" "Turbidez (NTU)"
[11] "Velocidad corriente (m/s)"
Pega la salida de glimpse() de tus propios datos y pregunta:
Esta es la estructura de mi base de datos de campo [pegar glimpse].
¿Qué problemas potenciales ves (tipos de columna, nombres,
valores sospechosos)? ¿Qué verificaciones me recomiendas hacer
en R antes de analizarla?
clean_names()Los nombres del archivo de Excel tienen espacios, tildes y símbolos (Oxígeno disuelto (mg/L)), y en R eso obliga a escribirlos entre comillas invertidas. clean_names() los convierte a un formato limpio: minúsculas, sin tildes y con guion bajo.
fq <- fq_crudo |> clean_names()
names(fq) [1] "muestra" "estacion"
[3] "epoca" "replica"
[5] "fecha_muestreo" "temperatura_c"
[7] "oxigeno_disuelto_mg_l" "p_h"
[9] "conductividad_m_s_cm" "turbidez_ntu"
[11] "velocidad_corriente_m_s"
Funcionó casi todo, pero mira con atención: pH quedó como p_h y el símbolo µ de µS/cm se convirtió en m (conductividad_m_s_cm). Esto pasaría a ser milisiemens, un error de mil veces. Corregimos con rename(), poniendo primero el nombre nuevo:
fq <- fq |>
rename(ph = p_h,
conductividad_us_cm = conductividad_m_s_cm,
od_mg_l = oxigeno_disuelto_mg_l,
velocidad_m_s = velocidad_corriente_m_s)
names(fq) [1] "muestra" "estacion" "epoca"
[4] "replica" "fecha_muestreo" "temperatura_c"
[7] "od_mg_l" "ph" "conductividad_us_cm"
[10] "turbidez_ntu" "velocidad_m_s"
Las herramientas automáticas, incluida la IA, ahorran trabajo, pero siempre hay que revisar el resultado. Un cambio de unidad silencioso puede invalidar un análisis completo.
Los errores de digitación en nombres taxonómicos son de los más comunes en bases de campo. count() nos ayuda a detectarlos:
macro_crudo |>
count(familia) |>
filter(str_detect(str_to_lower(familia), "baetid|chironomid"))Aparecen baetidae y CHIRONOMIDAE. R las trata como familias distintas de Baetidae y Chironomidae, lo que inflaría la riqueza. (En el archivo original también hay un "Baetidae " con un espacio al final, pero read_csv() elimina esos espacios automáticamente; read_excel() y otras funciones no siempre lo hacen.) Las corregimos con funciones de stringr:
macro <- macro_crudo |>
mutate(familia = familia |>
str_trim() |> # quita espacios al inicio y al final
str_to_title()) # primera letra en mayúscula, resto minúscula
n_distinct(macro_crudo$familia) # antes[1] 35
n_distinct(macro$familia) # después[1] 33
NA)# ¿Cuántos NA hay en cada columna?
fq |> summarise(across(everything(), \(x) sum(is.na(x))))# Las funciones devuelven NA si hay un faltante...
mean(fq$od_mg_l)[1] NA
# ...a menos que indiquemos explícitamente que se ignoren
mean(fq$od_mg_l, na.rm = TRUE)[1] 6.518286
drop_na() elimina las filas con faltantes, pero en una base con muchas variables puedes perder muestras completas por un solo sensor fallido. Normalmente es mejor conservar la fila y usar na.rm = TRUE en cada cálculo. Si decides imputar valores, hazlo de forma justificada y documentada; ese es un buen tema para discutir con tu copiloto y con tu estadístico.
Convertimos a factor las variables categóricas, con un orden con sentido ecológico:
orden_estaciones <- paste0("E", 1:6)
macro <- macro |>
mutate(estacion = factor(estacion, levels = orden_estaciones),
epoca = factor(epoca, levels = c("Seca", "Lluvias")))
fq <- fq |>
mutate(estacion = factor(estacion, levels = orden_estaciones),
epoca = factor(epoca, levels = c("Seca", "Lluvias")),
fecha_muestreo = as.Date(fecha_muestreo))dplyr ofrece un conjunto de verbos que se encadenan con el pipe |>, que se lee como “y luego”:
| Verbo | Qué hace |
|---|---|
filter() |
Selecciona filas que cumplen una condición |
select() |
Selecciona columnas |
mutate() |
Crea o modifica columnas |
group_by() + summarise() |
Resume datos por grupos |
arrange() |
Ordena filas |
count() |
Cuenta observaciones por categoría |
filter(): seleccionar filas# Solo los órdenes EPT (Ephemeroptera, Plecoptera, Trichoptera)
ept <- macro |>
filter(orden %in% c("Ephemeroptera", "Plecoptera", "Trichoptera"))
ept |> count(orden, sort = TRUE)select(): seleccionar columnasmacro |>
select(muestra, estacion, familia, abundancia) |>
head()mutate() y case_when(): crear columnasClasificamos cada muestra según su oxígeno disuelto, usando como referencia el umbral de 4 mg/L, un mínimo comúnmente aceptado para la vida acuática en aguas cálidas:
fq <- fq |>
mutate(estado_od = case_when(
is.na(od_mg_l) ~ NA_character_,
od_mg_l >= 6 ~ "Bueno",
od_mg_l >= 4 ~ "Aceptable",
TRUE ~ "Crítico"
))
fq |> count(estacion, estado_od)group_by() + summarise(): resúmenes por grupoEste es el corazón del análisis de comunidades. Calculamos métricas por muestra:
metricas_muestra <- macro |>
group_by(muestra, estacion, epoca) |>
summarise(
riqueza = n_distinct(familia),
abundancia_total = sum(abundancia),
riqueza_ept = n_distinct(familia[orden %in% c("Ephemeroptera", "Plecoptera", "Trichoptera")]),
prop_chironomidae = sum(abundancia[familia == "Chironomidae"]) / sum(abundancia),
.groups = "drop"
)
metricas_muestraDentro de summarise(), cada línea ve los resultados de las anteriores. Si hubiéramos llamado abundancia (igual que la columna original) al total, en la línea de prop_chironomidae la palabra abundancia ya sería el total resumido —un solo número— y no los conteos por familia, y la proporción saldría errónea sin ningún mensaje de error. Por eso el total se llama abundancia_total. Es un error sutil que la IA comete a menudo: revísalo siempre.
Y un resumen por estación y época, ordenado con arrange():
resumen_estacion <- metricas_muestra |>
group_by(estacion, epoca) |>
summarise(
riqueza_media = mean(riqueza),
riqueza_ee = sd(riqueza) / sqrt(n()), # error estándar
ept_media = mean(riqueza_ept),
n_replicas = n(),
.groups = "drop"
) |>
arrange(desc(riqueza_media))
resumen_estacionTengo esta tabla de métricas por estación [pegar resumen_estacion].
Las estaciones van de E1 (cabecera, 870 m) a E6 (desembocadura,
15 m) y E4 está aguas abajo de una descarga municipal.
¿Qué patrones ecológicos observas? ¿Qué hipótesis plantearías
y qué análisis estadístico usarías para ponerlas a prueba?
Contrasta su interpretación con la tuya. ¿Propone pruebas adecuadas para conteos con réplicas? Lo verás en el Módulo 3.
left_join()Nuestros datos están en tres tablas relacionadas por columnas en común. left_join() las une conservando todas las filas de la tabla de la izquierda:
datos_integrados <- metricas_muestra |>
left_join(fq |> select(muestra, temperatura_c, od_mg_l, ph,
conductividad_us_cm, turbidez_ntu, velocidad_m_s),
by = "muestra") |>
left_join(estaciones |> mutate(estacion = factor(estacion, levels = orden_estaciones)),
by = "estacion")
glimpse(datos_integrados)Rows: 36
Columns: 18
$ muestra <chr> "E1-L-R1", "E1-L-R2", "E1-L-R3", "E1-S-R1", "E1-S-…
$ estacion <fct> E1, E1, E1, E1, E1, E1, E2, E2, E2, E2, E2, E2, E3…
$ epoca <fct> Lluvias, Lluvias, Lluvias, Seca, Seca, Seca, Lluvi…
$ riqueza <int> 29, 28, 29, 27, 29, 29, 27, 28, 26, 30, 27, 28, 19…
$ abundancia_total <dbl> 180, 239, 195, 568, 307, 420, 158, 286, 340, 665, …
$ riqueza_ept <int> 12, 12, 11, 10, 9, 11, 8, 10, 9, 11, 9, 11, 4, 7, …
$ prop_chironomidae <dbl> 0.361111111, 0.259414226, 0.112820513, 0.191901408…
$ temperatura_c <dbl> 18.6, 18.6, 19.3, 20.6, 20.1, 20.7, 21.5, 21.5, 21…
$ od_mg_l <dbl> 8.39, 8.54, 8.22, 8.09, 7.83, 8.24, 7.61, 8.61, 8.…
$ ph <dbl> 7.13, 7.12, 7.32, 7.22, 7.12, 7.11, 7.11, 7.17, 7.…
$ conductividad_us_cm <dbl> 36, 51, 43, 58, 40, 67, 80, 81, 101, 89, 79, 97, 1…
$ turbidez_ntu <dbl> 9.0, 8.6, 9.9, 3.1, 2.8, 3.4, 11.4, 12.1, 14.5, 5.…
$ velocidad_m_s <dbl> 1.10, 1.07, 1.25, 0.85, 0.82, 0.92, 1.00, 0.85, 0.…
$ nombre <chr> "Cabecera", "Cabecera", "Cabecera", "Cabecera", "C…
$ altitud_m <dbl> 870, 870, 870, 870, 870, 870, 610, 610, 610, 610, …
$ latitud <dbl> 11.0312, 11.0312, 11.0312, 11.0312, 11.0312, 11.03…
$ longitud <dbl> -73.4521, -73.4521, -73.4521, -73.4521, -73.4521, …
$ uso_suelo <chr> "Bosque", "Bosque", "Bosque", "Bosque", "Bosque", …
Esta tabla, con una fila por muestra, las métricas biológicas, las variables ambientales y las características de la estación, es la base para relacionar biología y ambiente en el Módulo 3.
Este es probablemente el concepto más importante del módulo para el análisis ecológico.
pivot_wider()matriz_comunidad <- macro |>
group_by(muestra, familia) |>
summarise(abundancia = sum(abundancia), .groups = "drop") |> # por si hay duplicados
pivot_wider(names_from = familia,
values_from = abundancia,
values_fill = 0) |> # ausencia = 0 individuos
arrange(muestra) |>
column_to_rownames("muestra") # los nombres de muestra pasan a nombres de fila
dim(matriz_comunidad) # muestras × familias[1] 36 33
matriz_comunidad[1:6, 1:6]values_fill = 0?
En formato largo solo registramos lo que se encontró. Al pasar a matriz, las combinaciones muestra–familia que no existían quedarían como NA. En ecología de comunidades, no haber encontrado una familia en una muestra es una ausencia (0), no un dato faltante. Confundir ambos casos altera todos los índices de diversidad.
La matriz también se puede construir a nivel de estación, sumando las réplicas y épocas:
matriz_estacion <- macro |>
group_by(estacion, familia) |>
summarise(abundancia = sum(abundancia), .groups = "drop") |>
pivot_wider(names_from = familia, values_from = abundancia, values_fill = 0) |>
column_to_rownames("estacion")
matriz_estacion[, 1:6]pivot_longer()La operación inversa es muy útil para graficar varias variables a la vez. Por ejemplo, todas las variables fisicoquímicas en un solo gráfico con paneles:
fq_largo <- fq |>
pivot_longer(cols = c(temperatura_c, od_mg_l, ph, conductividad_us_cm,
turbidez_ntu, velocidad_m_s),
names_to = "variable",
values_to = "valor")
fq_largo |> select(muestra, estacion, epoca, variable, valor) |> head(8)ggplot2 construye los gráficos por capas, siguiendo la gramática de gráficos:
ggplot(datos, aes(x = ..., y = ..., color = ...)) + # datos y mapeo estético
geom_*() + # tipo de geometría
labs() + # títulos y etiquetas
theme_*() # apariencia
Definimos primero un tema base para que todos los gráficos sean consistentes:
theme_set(theme_minimal(base_size = 13) +
theme(plot.title = element_text(face = "bold"),
legend.position = "bottom"))
colores_epoca <- c("Seca" = "#D9822B", "Lluvias" = "#2B6CB0")macro |>
group_by(estacion, orden) |>
summarise(abundancia = sum(abundancia), .groups = "drop") |>
mutate(orden = fct_lump_n(orden, n = 7, w = abundancia, other_level = "Otros")) |>
ggplot(aes(x = estacion, y = abundancia, fill = orden)) +
geom_col(position = "fill", color = "white", linewidth = 0.2) +
scale_y_continuous(labels = scales::percent) +
scale_fill_viridis_d(option = "turbo", begin = 0.05, end = 0.95) +
labs(title = "Composición de la comunidad a lo largo del río",
x = "Estación (cabecera → desembocadura)", y = "Abundancia relativa",
fill = "Orden") +
guides(fill = guide_legend(nrow = 2))ggplot(metricas_muestra, aes(x = estacion, y = riqueza, fill = epoca)) +
geom_boxplot(alpha = 0.6, outlier.shape = NA) +
geom_point(position = position_jitterdodge(jitter.width = 0.15, seed = 1),
size = 2, alpha = 0.8, aes(color = epoca), show.legend = FALSE) +
scale_fill_manual(values = colores_epoca) +
scale_color_manual(values = colores_epoca) +
labs(title = "Riqueza de familias por estación",
x = "Estación", y = "Riqueza (n.º de familias)", fill = "Época")metricas_muestra |>
group_by(estacion, epoca) |>
summarise(media = mean(riqueza_ept),
ee = sd(riqueza_ept) / sqrt(n()),
.groups = "drop") |>
ggplot(aes(x = estacion, y = media, fill = epoca)) +
geom_col(position = position_dodge(0.8), width = 0.7, alpha = 0.85) +
geom_errorbar(aes(ymin = media - ee, ymax = media + ee),
position = position_dodge(0.8), width = 0.25) +
scale_fill_manual(values = colores_epoca) +
labs(title = "Riqueza EPT por estación",
x = "Estación", y = "Riqueza EPT (media ± EE)", fill = "Época")La desviación estándar (DE) describe la variabilidad de los datos. El error estándar (EE = DE/√n) describe la precisión de la media. Indica siempre en la leyenda cuál estás mostrando: es uno de los errores más frecuentes en manuscritos y en código generado por IA.
datos_integrados |>
filter(!is.na(od_mg_l)) |>
ggplot(aes(x = od_mg_l, y = riqueza_ept)) +
geom_smooth(method = "lm", se = TRUE, color = "grey30", linewidth = 0.8) +
geom_point(aes(color = estacion, shape = epoca), size = 3, alpha = 0.85) +
scale_color_viridis_d(option = "viridis", end = 0.9) +
labs(title = "Oxígeno disuelto y riqueza EPT",
x = "Oxígeno disuelto (mg/L)", y = "Riqueza EPT",
color = "Estación", shape = "Época")facet_wrap()etiquetas_variables <- c(
temperatura_c = "Temperatura (°C)",
od_mg_l = "Oxígeno disuelto (mg/L)",
ph = "pH",
conductividad_us_cm = "Conductividad (µS/cm)",
turbidez_ntu = "Turbidez (NTU)",
velocidad_m_s = "Velocidad (m/s)"
)
fq_largo |>
group_by(estacion, epoca, variable) |>
summarise(valor = mean(valor, na.rm = TRUE), .groups = "drop") |>
ggplot(aes(x = estacion, y = valor, color = epoca, group = epoca)) +
geom_line(linewidth = 1) +
geom_point(size = 2.5) +
facet_wrap(~ variable, scales = "free_y", ncol = 3,
labeller = labeller(variable = etiquetas_variables)) +
scale_color_manual(values = colores_epoca) +
labs(title = "Gradiente longitudinal de variables fisicoquímicas",
x = "Estación", y = NULL, color = "Época")matriz_estacion |>
rownames_to_column("estacion") |>
pivot_longer(-estacion, names_to = "familia", values_to = "abundancia") |>
mutate(familia = fct_reorder(familia, abundancia * (estacion == "E1"), .fun = sum)) |>
ggplot(aes(x = estacion, y = familia, fill = log10(abundancia + 1))) +
geom_tile(color = "white") +
scale_fill_viridis_c(option = "mako", direction = -1) +
labs(title = "Matriz de comunidad",
x = "Estación", y = NULL, fill = "log10(N + 1)") +
theme(legend.position = "right",
axis.text.y = element_text(face = "italic", size = 9))Este es mi código de ggplot2 [pegar código]. Quiero que el gráfico
cumpla los estándares de una revista científica: tipografía legible,
paleta accesible para daltónicos, sin elementos innecesarios y
exportable a 300 dpi. Sugiere cambios y explica cada uno.
# Crear carpetas si no existen
dir.create("resultados", showWarnings = FALSE)
dir.create("figuras", showWarnings = FALSE)
# Tablas
write_csv(datos_integrados, "resultados/datos_integrados.csv")
write_csv(matriz_comunidad |> rownames_to_column("muestra"),
"resultados/matriz_comunidad.csv")
writexl::write_xlsx(list(metricas = metricas_muestra,
resumen = resumen_estacion),
"resultados/metricas_macroinvertebrados.xlsx")
# Figuras: guardar un gráfico en un objeto y exportarlo
fig_ept <- ggplot(datos_integrados, aes(od_mg_l, riqueza_ept)) +
geom_point(aes(color = estacion), size = 3)
ggsave("figuras/od_vs_ept.png", plot = fig_ept,
width = 18, height = 12, units = "cm", dpi = 300)Guarda la matriz de comunidad y la tabla de datos integrados: serán el punto de partida de los Módulos 3 y 4.
Ejercicio.
macro, calcula para cada muestra la abundancia de los órdenes Ephemeroptera, Plecoptera y Trichoptera y la proporción EPT (abundancia EPT / abundancia total).fq y grafica la proporción EPT contra la conductividad, coloreando por estación.Solución:
prop_ept <- macro |>
group_by(muestra) |>
summarise(abund_ept = sum(abundancia[orden %in% c("Ephemeroptera", "Plecoptera", "Trichoptera")]),
abund_total = sum(abundancia),
.groups = "drop") |>
mutate(prop_ept = abund_ept / abund_total) |>
left_join(fq |> select(muestra, estacion, epoca, conductividad_us_cm), by = "muestra")
ggplot(prop_ept, aes(conductividad_us_cm, prop_ept, color = estacion)) +
geom_point(size = 3) +
scale_y_continuous(labels = scales::percent) +
scale_color_viridis_d(end = 0.9) +
labs(x = "Conductividad (µS/cm)", y = "Proporción EPT", color = "Estación")Además de tus datos de campo, puedes complementar tus análisis con millones de registros públicos. La Infraestructura Mundial de Información en Biodiversidad (GBIF) reúne datos de museos, colecciones y ciencia ciudadana de todo el mundo, incluidas las observaciones de iNaturalist con grado de investigación. En Colombia, el nodo nacional es el SiB Colombia, que publica sus datos a través de GBIF.
El paquete rgbif permite consultarlos directamente desde R.
Los bloques de esta sección no se ejecutan al renderizar la guía porque dependen de la conexión y de la disponibilidad del servicio. Ejecútalos tú en tu sesión.
Antes de descargar registros conviene verificar el nombre y obtener su identificador (usageKey). Usaremos Anacroneuria (Plecoptera: Perlidae), un género de perlas sensible a la contaminación y común en los ríos de la Sierra Nevada:
library(rgbif)
taxon <- name_backbone(name = "Anacroneuria", rank = "genus")
taxon |> select(usageKey, scientificName, rank, status, family, order)# ¿Cuántos registros hay en Colombia?
occ_count(taxonKey = taxon$usageKey, country = "CO")
# Descargar hasta 1000 registros con coordenadas
anacroneuria <- occ_search(
taxonKey = taxon$usageKey,
country = "CO",
hasCoordinate = TRUE,
limit = 1000
)$data
anacroneuria |>
select(scientificName, decimalLatitude, decimalLongitude,
stateProvince, year, basisOfRecord, institutionCode) |>
head()Las observaciones de iNaturalist con grado de investigación llegan a GBIF como un conjunto de datos propio, así que se pueden filtrar por su identificador:
inat_key <- "50c9509d-22c7-4a22-a47d-8c48425ef4a7" # iNaturalist Research-grade Observations
odonatos_guajira <- occ_search(
orderKey = name_backbone(name = "Odonata", rank = "order")$usageKey,
datasetKey = inat_key,
country = "CO",
stateProvince = "La Guajira",
limit = 500
)$data
odonatos_guajira |> count(family, sort = TRUE)Los datos abiertos también necesitan limpieza: coordenadas imprecisas, registros duplicados y nombres sin resolver.
anacroneuria_limpio <- anacroneuria |>
filter(!is.na(decimalLatitude),
is.na(coordinateUncertaintyInMeters) | coordinateUncertaintyInMeters < 5000) |>
distinct(species, decimalLatitude, decimalLongitude, .keep_all = TRUE)
ggplot(anacroneuria_limpio, aes(decimalLongitude, decimalLatitude)) +
borders("world", regions = "Colombia", fill = "grey95", colour = "grey60") + # requiere el paquete maps
geom_point(aes(color = basisOfRecord), alpha = 0.7, size = 2) +
coord_quickmap() +
labs(title = "Registros de Anacroneuria en Colombia (GBIF)",
x = "Longitud", y = "Latitud", color = "Tipo de registro")occ_search() es ideal para explorar. Para un artículo o una tesis usa occ_download(), que requiere una cuenta gratuita en gbif.org y genera un DOI citable para exactamente los registros que usaste. Citar el DOI reconoce el trabajo de las colecciones y hace tu análisis reproducible.
Descargué registros de GBIF con rgbif para un género de
macroinvertebrados acuáticos en Colombia. Tengo las columnas
decimalLatitude, decimalLongitude, coordinateUncertaintyInMeters,
basisOfRecord, year y species. ¿Qué pasos de control de calidad
me recomiendas antes de usarlos en un análisis de distribución?
Sugiere código con dplyr y menciona paquetes especializados.
Verifica que los paquetes que te sugiera (por ejemplo CoordinateCleaner) existan y estén vigentes.
Ejercicio. Elige una familia de macroinvertebrados de nuestra base (por ejemplo, Psephenidae o Corydalidae) y:
usageKey con name_backbone().stateProvince).¿Qué departamentos tienen más registros? ¿Refleja eso la distribución real de la familia o el esfuerzo de muestreo? Discútelo con tu copiloto.
En el Módulo 3 usarás datos_integrados y matriz_comunidad para:
| Situación | Prompt sugerido |
|---|---|
| Error que no entiendes | “Ejecuté este código [código] y obtuve este error [error]. Explícame en lenguaje sencillo qué significa, por qué ocurre y cómo corregirlo.” |
| Entender código ajeno | “Explícame línea por línea qué hace este código de R, como si yo fuera un biólogo que empieza a programar: [código]” |
| Traducir de R base a tidyverse | “Reescribe este código usando dplyr y el pipe nativo |>, manteniendo el mismo resultado: [código]” |
| Revisar un análisis | “Revisa este código de análisis ecológico y señala posibles errores lógicos o estadísticos, no solo de sintaxis: [código]” |
| Elegir un gráfico | “Tengo estas variables [glimpse]. Quiero mostrar [objetivo]. ¿Qué tipo de gráfico de ggplot2 me recomiendas y por qué?” |
| Mensaje de error | Qué significa | Solución habitual |
|---|---|---|
could not find function "..." |
El paquete no está cargado o el nombre está mal escrito | Ejecuta library(paquete) y revisa la ortografía |
object '...' not found |
El objeto no existe en la sesión | Ejecuta antes el bloque que lo crea; revisa mayúsculas y minúsculas |
there is no package called '...' |
El paquete no está instalado | install.packages("paquete") |
'...' does not exist in current working directory |
R no encuentra el archivo | Verifica que abriste el proyecto y que el archivo está en datos/ |
unexpected symbol / unexpected ')' |
Error de sintaxis: falta una coma, un paréntesis o unas comillas | Revisa la línea indicada y la anterior |
argument is not numeric or logical |
Aplicaste una función numérica a texto | Revisa el tipo de la columna con glimpse() |
Incluir sessionInfo() al final de tus documentos permite saber con qué versiones de R y de los paquetes se obtuvieron los resultados:
sessionInfo()R version 4.3.3 (2024-02-29 ucrt)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 11 x64 (build 26200)
Matrix products: default
locale:
[1] LC_COLLATE=Spanish_Spain.utf8 LC_CTYPE=Spanish_Spain.utf8
[3] LC_MONETARY=Spanish_Spain.utf8 LC_NUMERIC=C
[5] LC_TIME=Spanish_Spain.utf8
time zone: America/Bogota
tzcode source: internal
attached base packages:
[1] stats graphics grDevices utils datasets methods base
other attached packages:
[1] janitor_2.2.1 readxl_1.4.5 lubridate_1.9.3 forcats_1.0.0
[5] stringr_1.6.0 dplyr_1.2.0 purrr_1.0.2 readr_2.1.5
[9] tidyr_1.3.2 tibble_3.2.1 ggplot2_4.0.2 tidyverse_2.0.0
loaded via a namespace (and not attached):
[1] generics_0.1.4 lattice_0.22-6 stringi_1.8.3 hms_1.1.3
[5] digest_0.6.35 magrittr_2.0.3 evaluate_1.0.5 grid_4.3.3
[9] timechange_0.3.0 RColorBrewer_1.1-3 fastmap_1.1.1 Matrix_1.6-5
[13] cellranger_1.1.0 jsonlite_1.8.8 mgcv_1.9-1 viridisLite_0.4.2
[17] scales_1.4.0 cli_3.6.2 rlang_1.1.7 crayon_1.5.2
[21] splines_4.3.3 bit64_4.0.5 withr_3.0.2 yaml_2.3.10
[25] tools_4.3.3 parallel_4.3.3 tzdb_0.4.0 vctrs_0.7.1
[29] R6_2.6.1 lifecycle_1.0.5 snakecase_0.11.1 htmlwidgets_1.6.4
[33] bit_4.0.5 vroom_1.6.5 pkgconfig_2.0.3 pillar_1.11.0
[37] gtable_0.3.6 glue_1.7.0 xfun_0.52 tidyselect_1.2.1
[41] rstudioapi_0.16.0 knitr_1.50 farver_2.1.2 nlme_3.1-164
[45] htmltools_0.5.8.1 rmarkdown_2.29 labeling_0.4.3 compiler_4.3.3
[49] S7_0.2.1