← Volver

Terremotos en tiempo real: últimas 24h + mapa de calor anual

Terremotos (imagen ilustrativa)

La Tierra no “se queda quieta”: cada día ocurren cientos de pequeños movimientos y, de vez en cuando, alguno lo bastante fuerte como para notarse a escala global. En esta actividad montamos un pequeño monitor sísmico para ver ese pulso del planeta en mapas interactivos.

A partir de los datos públicos del USGS, construimos tres piezas que se complementan: (1) un mapa con los terremotos de las últimas 24 horas con marcadores coloreados por magnitud, (2) un mapa de calor del último año filtrado a M ≥ 4.5 para localizar “zonas calientes” sin saturar el mapa, y (3) un monitor de escritorio en Tkinter que permite refrescar los eventos al momento, como si fuese un panel en vivo.

1) Los datos (USGS) y qué significan

Trabajamos con datos públicos del USGS (United States Geological Survey), que publica terremotos en formato CSV. El CSV es muy directo: cada fila representa un evento sísmico con su fecha, coordenadas, magnitud, profundidad y metadatos.

En este artículo usamos dos CSV según el objetivo:

1.1) Columnas clave del CSV

Aunque el CSV trae muchas columnas, para nuestras visualizaciones nos quedamos con las esenciales:

Además aparecen campos útiles como id (identificador), updated (última actualización), status (automatic o reviewed) y métricas de calidad/errores. No son necesarias para dibujar el mapa, pero son útiles si queremos profundizar en fiabilidad.

2) Preparar el entorno

Antes de empezar con los dos mini-proyectos, dejamos listo el entorno del notebook: instalamos (si hace falta) e importamos librerías para cargar CSVs, crear mapas con Folium y exportar a HTML.

%pip install folium pandas

import pandas as pd
import folium
from folium import plugins
import datetime

import warnings
warnings.filterwarnings("ignore")

print("Entorno listo.")

3) Mapa de terremotos de las últimas 24 horas

Paso 1 — Descargar el feed 24h y cargarlo

Empezamos obteniendo los datos “en vivo” desde el feed de las últimas 24 horas del USGS. Ese feed es simplemente una URL que devuelve un CSV actualizado, donde cada fila es un terremoto registrado en el último día.

# Feed 24h: todos los terremotos del último día (actualización continua)
url_24h = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.csv"

# 1) Descargamos y cargamos el CSV en un DataFrame
df = pd.read_csv(url_24h)

# 2) Convertimos 'time' a datetime para poder mostrarlo/formatearlo en el mapa
df["time"] = pd.to_datetime(df["time"])

# 3) Verificación rápida: cuántos eventos tenemos en las últimas 24h
print(f"Cargados {len(df)} terremotos (últimas 24h).")

Paso 2 — Crear el mapa base y definir el código de colores por magnitud

Con el dataset ya cargado, montamos el “lienzo” donde vamos a dibujar los terremotos: un mapa mundial. Lo centramos en [20, 0] y usamos un tema oscuro (CartoDB dark_matter) porque hace que los puntos (círculos) destaquen mucho más y el contraste sea más limpio.

Después definimos una regla visual muy simple para leer el mapa de un vistazo: el color del marcador depende de la magnitud. No buscamos precisión científica con esta clasificación, sino una señal rápida para distinguir eventos pequeños de los más relevantes.

Nota: estos umbrales son una convención práctica para visualización. Si queremos, podemos ajustarlos (por ejemplo, marcar en rojo a partir de 5.5) según el objetivo del análisis.
# 1) Mapa base global (tema oscuro para maximizar contraste)
m = folium.Map(location=[20, 0], zoom_start=2, tiles="CartoDB dark_matter")

# 2) Regla de colores por magnitud (lectura rápida del mapa)
def color_por_magnitud(mag):
    if pd.isna(mag):
        return "gray"
    if mag < 4:
        return "green"
    elif mag <= 6:
        return "orange"
    else:
        return "red"

Paso 3 — Pintar los marcadores, añadir popups y exportar a HTML

Ahora viene la parte visual: recorremos la tabla y dibujamos un punto por cada terremoto. Usamos CircleMarker (círculos) en lugar de marcadores “clásicos” porque: carga más rápido, se ve bien en mapas con muchos eventos y nos deja controlar tamaño y opacidad.

Para cada evento extraemos lo mínimo necesario: latitude y longitude para la posición, mag para elegir el color, y place + time para mostrar un popup informativo al hacer clic. Así el mapa no es solo “puntos”, también es una forma de inspeccionar cada evento.

# Dibujamos un punto por terremoto (últimas 24h)
for _, row in df.iterrows():
    mag = row.get("mag", None)
    lat, lon = row["latitude"], row["longitude"]
    lugar = row.get("place", "Ubicación desconocida")
    tiempo = row["time"].strftime("%Y-%m-%d %H:%M:%S")

    folium.CircleMarker(
        location=[lat, lon],
        radius=5,
        color=color_por_magnitud(mag),
        fill=True,
        fill_opacity=0.85,
        popup=f"<b>Lugar:</b> {lugar}<br><b>Magnitud:</b> {mag}<br><b>Fecha:</b> {tiempo}"
    ).add_to(m)

# Exportamos el mapa a HTML para embeberlo en el blog
m.save("monitor_terremotos_24h.html")
print("Mapa generado: 'monitor_terremotos_24h.html'.")

Código final

url_24h = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.csv"

df = pd.read_csv(url_24h)
df["time"] = pd.to_datetime(df["time"])

m = folium.Map(location=[20, 0], zoom_start=2, tiles="CartoDB dark_matter")

def color_por_magnitud(mag):
    if pd.isna(mag):
        return "gray"
    if mag < 4:
        return "green"
    elif mag <= 6:
        return "orange"
    else:
        return "red"

for _, row in df.iterrows():
    mag = row.get("mag", None)
    lat, lon = row["latitude"], row["longitude"]
    lugar = row.get("place", "Ubicación desconocida")
    tiempo = row["time"].strftime("%Y-%m-%d %H:%M:%S")

    folium.CircleMarker(
        location=[lat, lon],
        radius=5,
        color=color_por_magnitud(mag),
        fill=True,
        fill_opacity=0.85,
        popup=f"<b>Lugar:</b> {lugar}<br><b>Magnitud:</b> {mag}<br><b>Fecha:</b> {tiempo}"
    ).add_to(m)

m.save("monitor_terremotos_24h.html")
print("OK → monitor_terremotos_24h.html")

Resultado

Abrir el mapa 24h en una pestaña
Nota: el mapa que estás viendo en el blog no es “tiempo real”. Es un HTML que generamos en un momento concreto (una “captura” del feed en ese instante). Si quieres ver los terremotos actualizados, solo tienes que volver a ejecutar el script.

4) Mapa de calor del último año (M≥4.5)

Paso 1 — Calcular fechas y construir la URL anual (muestra M≥4.5)

Para el mapa de calor no nos interesa “absolutamente todo” lo que ocurre en un año, porque el USGS registra también muchísimos microsismos (magnitudes muy bajas) que no aportan una visión clara del patrón global y, además, pueden cargar demasiado el mapa (rendimiento y saturación visual).

Por eso construimos una consulta anual pero la convertimos en una muestra más interpretable: nos quedamos con terremotos de magnitud ≥ 4.5. Este umbral es un buen compromiso porque:

Técnicamente, lo hacemos con el servicio FDSN Event Web Service del USGS: calculamos el rango temporal (starttime, endtime) y añadimos el parámetro minmagnitude=4.5 en la URL. Esa URL nos devuelve un CSV listo para cargar con pandas.

hoy = datetime.date.today()
hace_un_ano = hoy - datetime.timedelta(days=365)

url_anual = (
    "https://earthquake.usgs.gov/fdsnws/event/1/query"
    f"?format=csv&starttime={hace_un_ano}&endtime={hoy}&minmagnitude=4.5"
)

df_terremotos_mes = pd.read_csv(url_anual)
print(f"Cargados {len(df_terremotos_mes)} registros sísmicos del último año (M≥4.5).")

Paso 2 — Preparar puntos para el HeatMap (densidad de epicentros)

Con el CSV anual ya cargado, el siguiente paso es quedarnos con lo que realmente necesita un mapa de calor: coordenadas. Un HeatMap no dibuja “marcadores” uno a uno, sino que calcula una densidad espacial: donde hay muchos epicentros cerca, el mapa se “calienta”.

Por eso convertimos el DataFrame a una lista simple de puntos [lat, lon]. Además, limpiamos nulos con dropna() para evitar errores (si un evento viniese sin coordenadas, no tendría sentido incluirlo). El resultado es nuestro “dataset final” para el heatmap: una muestra de epicentros del último año (M≥4.5).

# Nos quedamos con las coordenadas y eliminamos filas sin lat/lon
datos_calor = (
    df_terremotos_mes[["latitude", "longitude"]]
    .dropna()
    .values
    .tolist()
)

print(f"Puntos para el heatmap (M≥4.5, último año): {len(datos_calor)}")

Paso 3 — Crear el heatmap, ajustar la visualización y exportar a HTML

Con la lista de epicentros datos_calor lista, ya podemos construir la visualización. Primero creamos un mapa base global y elegimos un tema oscuro (CartoDB dark_matter) para que el heatmap destaque con más contraste.

Después añadimos la capa HeatMap, que es la que transforma nuestros puntos en una “nube” de intensidad. Aquí es donde ajustamos los parámetros clave para controlar cómo se ve el patrón:

mapa_calor = folium.Map(
    location=[20, 0],
    zoom_start=2,
    tiles="CartoDB dark_matter"
)

plugins.HeatMap(
    datos_calor,
    radius=8,
    blur=10,
    min_opacity=0.4,
    gradient={0.4: "cyan", 0.6: "lime", 1: "red"},
).add_to(mapa_calor)

mapa_calor.save("monitor_terremotos_anual.html")
print("Mapa generado: 'monitor_terremotos_anual.html'.")

Código final

hoy = datetime.date.today()
hace_un_ano = hoy - datetime.timedelta(days=365)

url_anual = (
    "https://earthquake.usgs.gov/fdsnws/event/1/query"
    f"?format=csv&starttime={hace_un_ano}&endtime={hoy}&minmagnitude=4.5"
)

df_terremotos_mes = pd.read_csv(url_anual)
datos_calor = df_terremotos_mes[["latitude", "longitude"]].dropna().values.tolist()

mapa_calor = folium.Map(location=[20, 0], zoom_start=2, tiles="CartoDB dark_matter")

plugins.HeatMap(
    datos_calor,
    radius=8,
    blur=10,
    min_opacity=0.4,
    gradient={0.4: "cyan", 0.6: "lime", 1: "red"},
).add_to(mapa_calor)

mapa_calor.save("monitor_terremotos_anual.html")
print("OK → monitor_terremotos_anual.html")

Resultado

Abrir el mapa de calor anual en una pestaña
Nota: el mapa que estás viendo en el blog no es “tiempo real”. Es un HTML que generamos en un momento concreto (una “captura” del feed en ese instante). Si quieres ver los terremotos actualizados, solo tienes que volver a ejecutar el script.

5) Monitor de terremotos en escritorio (Tkinter)

Hasta ahora hemos generado HTMLs para integrarlos en el blog. Ahora damos un paso más: montamos un monitor en escritorio con Tkinter que muestra un mapa y permite actualizar los terremotos con un clic (y, si queremos, con refresco automático). La idea es la misma que en el mapa 24h: consumimos un CSV del USGS, pero en lugar de exportar a HTML lo mostramos en una ventana.

Paso 1 — Preparar la URL del feed y las dependencias

Igual que en el mapa web, el monitor de escritorio parte de un feed CSV del USGS. La diferencia es práctica: aquí lo vamos a descargar y repintar en una ventana cada vez que pulsemos “Actualizar”, así que nos interesa que el dataset no sea enorme para que la experiencia sea rápida.

Por eso usamos 2.5_day.csv: sigue siendo “último día”, pero trae una muestra filtrada (solo terremotos con magnitud ≥ 2.5). Ese umbral suele ser un buen equilibrio:

Si tu objetivo es ver absolutamente todo lo registrado en 24h, puedes cambiar la URL a all_day.csv. Ten en cuenta que, dependiendo del día, puede tardar más en cargar o dejar el mapa muy saturado.

# Si hace falta instalar dependencias (entorno local/notebook):
# %pip install pandas tkintermapview

# Feed del último día filtrado a magnitud >= 2.5 (más fluido para un monitor de escritorio)
URL = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.csv"

# Alternativa (sin filtro, puede ser más pesado):
# URL = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_day.csv"

Paso 2 — Crear la ventana y el mapa

Ahora montamos la “interfaz” del monitor. Con Tkinter creamos una ventana de escritorio y, dentro, incrustamos un mapa interactivo gracias a tkintermapview. La idea es replicar la lógica del HTML pero en local: una vista mundial donde después iremos colocando marcadores.

Aquí hacemos tres cosas clave:

Consejo: si quieres arrancar enfocando una zona concreta (por ejemplo España o el Pacífico), solo cambia set_position y/o set_zoom.
import tkinter as tk
import tkintermapview
import pandas as pd

# 1) Ventana principal
root = tk.Tk()
root.title("Monitor de Terremotos (USGS - 24h)")
root.geometry("1100x720")

# 2) Mapa embebido en la ventana (ocupa todo el espacio)
map_widget = tkintermapview.TkinterMapView(root, corner_radius=0)
map_widget.pack(fill="both", expand=True)

# 3) Vista inicial global (encuadre tipo “planetario”)
map_widget.set_position(20, 0)
map_widget.set_zoom(2)

Paso 3 — Descargar el CSV, pintar marcadores y refrescar

Aquí hacemos que el monitor “cobre vida”. La idea es tener una función de actualización que se pueda ejecutar cada cierto tiempo: descarga el CSV más reciente, limpia los marcadores anteriores y vuelve a pintarlos con la información actual. Así conseguimos un panel en directo (en el sentido de “se actualiza al refrescar”), sin necesidad de abrir un navegador.

El flujo es siempre el mismo:

  1. Descargar el CSV desde la URL del feed y cargarlo en un DataFrame.
  2. Limpiar marcadores previos para no “duplicar” puntos en cada actualización.
  3. Recorrer los eventos y, por cada terremoto, extraer: latitude/longitude para la posición, mag para el color y un texto resumen para el popup.
  4. Crear marcadores en el mapa con un color que nos dé lectura rápida (verde/naranja/rojo).

Para que el mapa sea legible, mantenemos la misma convención visual del HTML: verde si mag < 4, naranja si 4 ≤ mag ≤ 6 y rojo si mag > 6. Si falta la magnitud, usamos gris.

Nota: en Tkinter el “tiempo real” depende de cada cuánto llamemos a la función de refresco. Más frecuente = más actualizado, pero también más peticiones al feed.
def color_por_magnitud(mag):
    # Magnitud no disponible → gris para no romper la lógica
    if pd.isna(mag):
        return "gray"

    # Convención visual rápida (igual que en el mapa HTML)
    if mag < 4:
        return "green"
    elif mag <= 6:
        return "orange"
    else:
        return "red"

Paso 4 — Añadir controles y feedback en la parte superior

En un monitor de escritorio conviene tener un control mínimo para el usuario: un botón para refrescar y un texto de estado. Por eso se crea una barra superior (Frame) con:

self.top = tk.Frame(self.root)
self.top.pack(fill="x")

self.btn = tk.Button(self.top, text="Actualizar", command=self.actualizar)
self.btn.pack(side="left", padx=10, pady=8)

self.estado = tk.Label(self.top, text="Listo", anchor="w")
self.estado.pack(side="left", padx=10)

Paso 5 — Gestionar marcadores entre refrescos (evitar duplicados)

Cuando refrescamos, si simplemente volvemos a pintar sin borrar lo anterior, el mapa se llena de duplicados. Por eso se mantiene una lista self.markers con los marcadores creados en la última carga. Antes de repintar, llamamos a limpiar_marcadores().

Dentro de esa función se intenta borrar cada marcador (cuando el objeto lo soporta) y luego se reinicia la lista. Se usa try/except porque, según la versión del paquete o el tipo de marcador, el método de borrado puede variar. Lo importante es que el refresco siempre funcione sin romper la app.

self.markers = []

def limpiar_marcadores(self):
    try:
        for mk in self.markers:
            try:
                mk.delete()
            except Exception:
                pass
    except Exception:
        pass
    self.markers = []

Paso 6 — Implementar la actualización: descargar, convertir y repintar

La función actualizar() es el corazón del monitor. Hace tres cosas en orden:

  1. Feedback inmediato: cambia el texto de estado a “Descargando datos…” y fuerza una actualización ligera con update_idletasks() para que el mensaje se vea antes de la descarga.
  2. Descarga + carga del CSV: usa pd.read_csv(URL) y convierte time a datetime con errors="coerce" para evitar errores si alguna fila viene con un formato raro.
  3. Repintado: limpia marcadores previos y crea uno nuevo por evento con set_marker(). Se colorea el marcador usando self.color_por_magnitud(mag) y se añade un texto corto para identificarlo.

Un detalle importante: set_marker devuelve un objeto marcador. Lo guardamos en self.markers para poder borrarlo en el siguiente refresco. Y al final actualizamos el estado con el número de eventos cargados.

self.estado.config(text="Descargando datos...")
self.root.update_idletasks()

df = pd.read_csv(URL)
df["time"] = pd.to_datetime(df["time"], errors="coerce")

self.limpiar_marcadores()

for _, row in df.iterrows():
    lat = row["latitude"]
    lon = row["longitude"]
    mag = row.get("mag", None)
    lugar = row.get("place", "Ubicación desconocida")
    texto = f"M {mag} | {lugar}"

    mk = self.map_widget.set_marker(
        lat,
        lon,
        text=texto,
        marker_color_outside=self.color_por_magnitud(mag),
        marker_color_circle=self.color_por_magnitud(mag),
    )
    self.markers.append(mk)

self.estado.config(text=f"Eventos cargados: {len(df)}")

Paso 7 — Manejo de errores y por qué usamos messagebox

En un entorno de escritorio es normal que a veces fallen cosas externas: sin internet, caída del feed, o un CSV temporalmente mal formado. Por eso actualizar() va dentro de un try/except:

except Exception as e:
    self.estado.config(text="Error al cargar datos")
    messagebox.showerror("Error", f"No se pudo cargar el feed:\n{e}")

Paso 8 — Arranque de la aplicación: event loop de Tkinter

Para que la ventana sea interactiva, Tkinter necesita ejecutar su bucle principal (mainloop()). Ahí es donde la app espera clicks, repinta el mapa y responde a eventos. Por eso el script termina creando el root, instanciando el monitor y lanzando el bucle.

root = tk.Tk()
app = MonitorTerremotos(root)
root.mainloop()

Paso 9 — Refresco automático (opcional)

Si quieres que el monitor se refresque solo cada X segundos, Tkinter trae una solución simple: after. La idea es programar la siguiente actualización al final de actualizar(). Por ejemplo, cada 60 segundos:

# Al final de actualizar(), si quieres refresco automático:
self.root.after(60_000, self.actualizar)
Nota: si activas refresco automático, el feed se consultará de forma periódica. Ajusta el intervalo para no hacer demasiadas peticiones.

Código final

import tkinter as tk
from tkinter import messagebox
import pandas as pd
import tkintermapview

URL = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.csv"

class MonitorTerremotos:
    def __init__(self, root):
        self.root = root
        self.root.title("Monitor de Terremotos (USGS - 24h)")
        self.root.geometry("1100x720")

        self.top = tk.Frame(self.root)
        self.top.pack(fill="x")

        self.btn = tk.Button(self.top, text="Actualizar", command=self.actualizar)
        self.btn.pack(side="left", padx=10, pady=8)

        self.estado = tk.Label(self.top, text="Listo", anchor="w")
        self.estado.pack(side="left", padx=10)

        self.map_widget = tkintermapview.TkinterMapView(self.root, corner_radius=0)
        self.map_widget.pack(fill="both", expand=True)

        self.map_widget.set_position(20, 0)
        self.map_widget.set_zoom(2)

        self.markers = []
        self.actualizar()

    def color_por_magnitud(self, mag):
        if pd.isna(mag):
            return "gray"
        if mag < 4:
            return "green"
        elif mag <= 6:
            return "orange"
        else:
            return "red"

    def limpiar_marcadores(self):
        # Intentamos borrar marcadores si el objeto lo soporta. Si no, recreamos el mapa.
        try:
            for mk in self.markers:
                try:
                    mk.delete()
                except Exception:
                    pass
        except Exception:
            pass
        self.markers = []

    def actualizar(self):
        try:
            self.estado.config(text="Descargando datos...")
            self.root.update_idletasks()

            df = pd.read_csv(URL)
            df["time"] = pd.to_datetime(df["time"], errors="coerce")

            self.limpiar_marcadores()

            for _, row in df.iterrows():
                lat = row["latitude"]
                lon = row["longitude"]
                mag = row.get("mag", None)
                lugar = row.get("place", "Ubicación desconocida")

                # Texto corto para el marcador
                texto = f"M {mag} | {lugar}"

                mk = self.map_widget.set_marker(
                    lat,
                    lon,
                    text=texto,
                    marker_color_outside=self.color_por_magnitud(mag),
                    marker_color_circle=self.color_por_magnitud(mag),
                )
                self.markers.append(mk)

            self.estado.config(text=f"Eventos cargados: {len(df)}")

        except Exception as e:
            self.estado.config(text="Error al cargar datos")
            messagebox.showerror("Error", f"No se pudo cargar el feed:\n{e}")

root = tk.Tk()
app = MonitorTerremotos(root)
root.mainloop()

Resultado

En la siguiente imagen se ve el monitor de escritorio funcionando, con el mapa y los marcadores cargados desde el feed del USGS.

Monitor de terremotos en escritorio (Tkinter)
📓 Notebook incluido

Descarga el notebook

Si quieres replicarlo exactamente igual y jugar con él, aquí tienes el notebook completo listo para ejecutar.

Fuentes y enlaces

  1. USGS (feed de terremotos del último día). earthquake.usgs.gov — all_day.csv
  2. USGS (API de consulta: último año + minmagnitude=4.5). earthquake.usgs.gov — FDSN Event Web Service