Mapear datos de ciencia participativa de iNaturalist#

iNaturalist contiene miles de observaciones de ciencia participativa que pueden analizarse junto con los datos de GLOBE para comprender mejor los hábitats y la presencia de mosquitos.

En esta lección, mapearemos las ubicaciones de depredadores de mosquitos registradas en iNaturalist y las compararemos con observaciones de mosquitos de GLOBE.

Comenzaremos con las observaciones del petirrojo americano.

import folium
from folium.plugins import MarkerCluster
from folium.plugins import HeatMap
import pandas as pd
import geopandas as gpd

# ----------------------------------------
# Cargar datos del petirrojo americano
# registrados en iNaturalist
# ----------------------------------------
df = pd.read_csv(
    "https://raw.githubusercontent.com/geo-di-lab/"
    "emerge-lessons/refs/heads/main/docs/data/"
    "american_robin.csv"
)

required_robin_columns = [
    "observed_on",
    "latitude",
    "longitude",
    "common_name",
    "place_guess",
    "quality_grade"
]

missing_robin_columns = [
    column
    for column in required_robin_columns
    if column not in df.columns
]

if missing_robin_columns:
    raise ValueError(
        "Faltan columnas requeridas en los datos de iNaturalist: "
        f"{missing_robin_columns}"
    )

df["latitude"] = pd.to_numeric(
    df["latitude"],
    errors="coerce"
)
df["longitude"] = pd.to_numeric(
    df["longitude"],
    errors="coerce"
)
df["year"] = pd.to_datetime(
    df["observed_on"],
    errors="coerce"
).dt.year

df = df.dropna(
    subset=[
        "latitude",
        "longitude"
    ]
).copy()

# Conservar solamente observaciones del petirrojo americano.
# El nombre se mantiene en inglés porque así aparece
# en la columna common_name del archivo original.
df = df[
    df["common_name"].str.contains(
        "American Robin",
        case=False,
        na=False
    )
].copy()

# Separar las observaciones con grado de investigación
# de las demás para evitar duplicarlas entre capas.
df_research = df[
    df["quality_grade"].eq("research")
].copy()

df_other = df[
    ~df["quality_grade"].eq("research")
].copy()

# ----------------------------------------
# Cargar observaciones de mosquitos de GLOBE
# ----------------------------------------
mosquito = gpd.read_file(
    "https://github.com/geo-di-lab/emerge-lessons/raw/"
    "refs/heads/main/docs/data/globe_mosquito.zip"
)

fl = gpd.read_file(
    "https://github.com/geo-di-lab/emerge-lessons/raw/"
    "refs/heads/main/docs/data/florida_boundary.geojson"
)[["geometry"]]

if mosquito.crs is None or fl.crs is None:
    raise ValueError(
        "Los archivos espaciales deben tener un sistema "
        "de referencia de coordenadas definido."
    )

# Utilizar el mismo sistema de coordenadas para la unión espacial.
fl = fl.to_crs(
    mosquito.crs
)

# Conservar solamente las observaciones ubicadas en Florida.
mosquito = (
    gpd.sjoin(
        mosquito,
        fl,
        how="inner",
        predicate="intersects"
    )
    .drop(
        columns=["index_right"]
    )
    .reset_index(
        drop=True
    )
)

# Folium requiere coordenadas de latitud y longitud.
mosquito = mosquito.to_crs(
    epsg=4326
)

if not mosquito.geometry.geom_type.eq("Point").all():
    raise ValueError(
        "Las observaciones de mosquitos deben contener "
        "geometrías de puntos."
    )

# ----------------------------------------
# Crear el mapa base centrado en Florida
# ----------------------------------------
m = folium.Map(
    location=[
        28.263363,
        -83.497652
    ],
    tiles="CartoDB positron",
    zoom_start=6
)

# ----------------------------------------
# Agregar otras observaciones del
# petirrojo americano
# ----------------------------------------
robin_fg = folium.FeatureGroup(
    name="🟦 Otras observaciones del petirrojo americano"
).add_to(m)

robin_cluster = MarkerCluster().add_to(
    robin_fg
)

for _, row in df_other.iterrows():
    popup_text = f"""
    <b>Especie:</b> {row["common_name"]}<br>
    <b>Fecha:</b> {row["observed_on"]}<br>
    <b>Lugar:</b> {row["place_guess"]}<br>
    <b>Calidad:</b> {row["quality_grade"]}
    """

    folium.CircleMarker(
        location=[
            row["latitude"],
            row["longitude"]
        ],
        radius=6,
        color="blue",
        fill=True,
        fill_color="cyan",
        fill_opacity=0.8,
        popup=folium.Popup(
            popup_text,
            max_width=300
        ),
        tooltip="Petirrojo americano 🐦"
    ).add_to(
        robin_cluster
    )

# ----------------------------------------
# Agregar observaciones con grado
# de investigación
# ----------------------------------------
if not df_research.empty:
    research_fg = folium.FeatureGroup(
        name="🟩 Petirrojos con grado de investigación"
    ).add_to(m)

    research_cluster = MarkerCluster().add_to(
        research_fg
    )

    for _, row in df_research.iterrows():
        popup_text = f"""
        <b>Especie:</b> {row["common_name"]}<br>
        <b>Fecha:</b> {row["observed_on"]}<br>
        <b>Lugar:</b> {row["place_guess"]}<br>
        <b>Calidad:</b> {row["quality_grade"]}
        """

        folium.CircleMarker(
            location=[
                row["latitude"],
                row["longitude"]
            ],
            radius=6,
            color="green",
            fill=True,
            fill_color="lime",
            fill_opacity=0.8,
            popup=folium.Popup(
                popup_text,
                max_width=300
            ),
            tooltip="Grado de investigación 🟩"
        ).add_to(
            research_cluster
        )

# ----------------------------------------
# Agregar una leyenda personalizada
# ----------------------------------------
legend_html = """
<div style="
    position: fixed;
    bottom: 50px;
    left: 50px;
    width: 245px;
    background-color: white;
    border: 2px solid grey;
    z-index: 9999;
    font-size: 14px;
    padding: 10px;
    border-radius: 8px;
">
<b>Leyenda</b><br>
<span style="color: blue;">●</span>
Otras observaciones del petirrojo<br>
<span style="color: green;">●</span>
Observaciones con grado de investigación<br>
</div>
"""

m.get_root().html.add_child(
    folium.Element(
        legend_html
    )
)

# ----------------------------------------
# Agregar el control de capas y mostrar
# ----------------------------------------
folium.LayerControl(
    collapsed=False
).add_to(m)

display(m)

En el siguiente mapa utilizaremos nuevamente las observaciones del petirrojo americano, separadas entre observaciones con grado de investigación y otras observaciones. Esta vez, las mostraremos como puntos individuales en lugar de grupos.

También agregaremos las observaciones de mosquitos de GLOBE. Activa y desactiva las capas para explorar los lugares donde podrían superponerse.

# Crear un mapa nuevo
m = folium.Map(
    location=[
        28.263363,
        -83.497652
    ],
    tiles="CartoDB positron",
    zoom_start=6
)

# Agregar otras observaciones del petirrojo americano.
robin_fg = folium.FeatureGroup(
    name="🟦 Otras observaciones del petirrojo americano"
).add_to(m)

for _, row in df_other.iterrows():
    popup_text = f"""
    <b>Especie:</b> {row["common_name"]}<br>
    <b>Fecha:</b> {row["observed_on"]}<br>
    <b>Lugar:</b> {row["place_guess"]}<br>
    <b>Calidad:</b> {row["quality_grade"]}
    """

    folium.CircleMarker(
        location=[
            row["latitude"],
            row["longitude"]
        ],
        radius=6,
        color="blue",
        fill=True,
        fill_color="cyan",
        fill_opacity=0.8,
        popup=folium.Popup(
            popup_text,
            max_width=300
        )
    ).add_to(
        robin_fg
    )

# Agregar observaciones con grado de investigación.
research_fg = folium.FeatureGroup(
    name="🟩 Petirrojos con grado de investigación"
).add_to(m)

for _, row in df_research.iterrows():
    popup_text = f"""
    <b>Especie:</b> {row["common_name"]}<br>
    <b>Fecha:</b> {row["observed_on"]}<br>
    <b>Lugar:</b> {row["place_guess"]}<br>
    <b>Calidad:</b> {row["quality_grade"]}
    """

    folium.CircleMarker(
        location=[
            row["latitude"],
            row["longitude"]
        ],
        radius=6,
        color="green",
        fill=True,
        fill_color="lime",
        fill_opacity=0.8,
        popup=folium.Popup(
            popup_text,
            max_width=300
        )
    ).add_to(
        research_fg
    )

# Agregar las observaciones de mosquitos
# como una capa que puede activarse y desactivarse.
mosquito_fg = folium.FeatureGroup(
    name="🟥 Observaciones de mosquitos de GLOBE"
).add_to(m)

for _, row in mosquito.iterrows():
    measured_date = row.get(
        "MeasuredDate",
        "No disponible"
    )
    water_source = row.get(
        "WaterSourceType",
        "No disponible"
    )

    popup_text = f"""
    <b>Fecha:</b> {measured_date}<br>
    <b>Tipo de fuente de agua:</b> {water_source}
    """

    folium.CircleMarker(
        location=[
            row.geometry.y,
            row.geometry.x
        ],
        radius=4,
        color="darkred",
        fill=True,
        fill_color="orange",
        fill_opacity=0.8,
        popup=folium.Popup(
            popup_text,
            max_width=300
        )
    ).add_to(
        mosquito_fg
    )

folium.LayerControl(
    collapsed=False
).add_to(m)

display(m)

En el último mapa utilizaremos mapas de calor para representar las observaciones como gradientes de color. Esto permite visualizar las concentraciones de petirrojos y mosquitos.

Las áreas con colores más intensos contienen una mayor cantidad de observaciones. Los mosquitos aparecerán en tonos rojos y los petirrojos en tonos azules. Cuando ambas capas estén activadas, las zonas donde se concentran ambos tipos de observaciones pueden verse de color morado.

# ----------------------------------------
# Crear el mapa base centrado en Florida
# ----------------------------------------
m = folium.Map(
    location=[
        28.263363,
        -83.497652
    ],
    tiles="CartoDB positron",
    zoom_start=6
)

# ----------------------------------------
# Agregar el mapa de calor de mosquitos
# ----------------------------------------
mosquito_coords = [
    [
        geometry.y,
        geometry.x
    ]
    for geometry in mosquito.geometry
    if geometry is not None
    and not geometry.is_empty
]

if mosquito_coords:
    mosquito_heat = folium.FeatureGroup(
        name="Densidad de mosquitos"
    ).add_to(m)

    HeatMap(
        mosquito_coords,
        radius=15,
        blur=25,
        gradient={
            0.4: "orange",
            0.7: "red",
            1: "darkred"
        }
    ).add_to(
        mosquito_heat
    )

# ----------------------------------------
# Agregar el mapa de calor de petirrojos
# ----------------------------------------
robin_coords = (
    df[
        [
            "latitude",
            "longitude"
        ]
    ]
    .dropna()
    .values
    .tolist()
)

if robin_coords:
    robin_heat = folium.FeatureGroup(
        name="Densidad del petirrojo americano"
    ).add_to(m)

    HeatMap(
        robin_coords,
        radius=15,
        blur=25,
        gradient={
            0.4: "cyan",
            0.7: "blue",
            1: "darkblue"
        }
    ).add_to(
        robin_heat
    )

# ----------------------------------------
# Agregar el control de capas y mostrar
# ----------------------------------------
folium.LayerControl(
    collapsed=False
).add_to(m)

display(m)