Visualización geoespacial de cables submarinos con GeoPandas y Folium
Gran parte de “Internet” no vive en satélites ni en nubes abstractas: viaja por el fondo del mar. Los cables submarinos de fibra óptica son autopistas de datos que conectan continentes y países, y hacen posible desde una videollamada hasta el streaming o la banca online. Son infraestructura crítica: si un tramo se corta (por anclas, terremotos, pesca de arrastre o fallos técnicos), el tráfico se reencamina, pero la capacidad y la resiliencia dependen de cómo esté tejida la red.
En esta actividad de análisis geoespacial, veremos cómo: convertir un dataset real de cables en un mapa interactivo que permita ver las rutas físicas y explorar cada conexión con su nombre. El resultado es una visualización clara, exportable a HTML, y lista para integrarse en una web.
1) La tecnología detrás del mapa
Un cable submarino moderno suele contener varios pares de fibras ópticas y repetidores distribuidos a lo largo de la ruta. Las fibras transmiten luz modulada (información), y los repetidores compensan la pérdida de señal para mantener el enlace estable. En la costa, los cables llegan a landing stations (estaciones de amarre) que conectan con redes terrestres.
Desde el punto de vista de datos, este tipo de infraestructura se representa como geometrías lineales (líneas o multilíneas) con atributos asociados (nombre, operador, etc.).
2) Paso a paso del proyecto
Paso 0 — Preparar el entorno
Antes de empezar, vamos a dejar listo el notebook con lo imprescindible:
pandas para manejar los datos, geopandas para trabajar con geometrías,
folium para construir el mapa interactivo y numpy para cálculos rápidos cuando nos haga falta.
Además, más adelante usaremos el plugin AntPath de Folium para animar las rutas y que se entiendan mejor de un vistazo.
%pip install folium pandas geopandas numpy
import folium
from folium import plugins
import pandas as pd
import geopandas as gpd
import numpy as np
Paso 1 — Descargar el dataset (WFS → CSV)
Para conseguir los datos, vamos a tirar de un servicio WFS (Web Feature Service): le pedimos la capa de cables y, para no traernos el planeta entero, acotamos la descarga con un bounding box centrado en el Mediterráneo Occidental. Así la respuesta nos llega en un CSV con los atributos y una columna de geometría que luego convertiremos en objetos espaciales para poder pintarlos en el mapa.
enlace = "https://ows.emodnet-humanactivities.eu/wfs?service=WFS&version=2.0.0&request=GetFeature&typeName=sigcables&bbox=3.91,42.31,7.14,43.84,EPSG:4326&outputFormat=csv"
df = pd.read_csv(enlace)
Paso 2 — Convertir la geometría (WKT → Shapely)
Aquí nos encontramos con el principal “bache” del proyecto: la geometría viene en formato WKT (Well-Known Text), es decir, geometría escrita como texto. Tranquilo, es totalmente normal: WKT es un estándar muy usado para representar formas 2D/3D (puntos, líneas y polígonos) dentro de una sola cadena, así que encaja perfecto en CSV, logs o respuestas de APIs. En el siguiente paso la convertiremos a geometrías reales para poder trabajar con ellas en GeoPandas sin dolor.
Un par de ejemplos típicos:
POINT (2.1744 41.4036)
LINESTRING (-3.7038 40.4168, 2.1744 41.4036)
POLYGON ((0 0, 3 0, 3 2, 0 2, 0 0))
En estos ejemplos, las coordenadas suelen venir en el orden (x, y), que en EPSG:4326 normalmente significa
(longitud, latitud). En mi caso, la columna the_geom del WFS venía como WKT.
Para poder operar con esas formas (dibujarlas, recorrer sus puntos, calcular longitudes, etc.) necesitaba convertir ese texto
a un objeto geométrico real de Python.
Ahí entra Shapely: su módulo shapely.wkt “parsea” el string WKT y lo transforma en una geometría
(Point, LineString, MultiLineString, etc.). Luego, con GeoPandas construyo
un GeoDataFrame que ya entiende la columna geometry como geometría.
from shapely import wkt
df["geometry"] = df["the_geom"].apply(wkt.loads)
cables = gpd.GeoDataFrame(df, geometry="geometry")
Nota: existen otros formatos binarios como WKB o GeoPackage, pero WKT tiene una ventaja enorme en prácticas: es fácil de inspeccionar a simple vista y muy cómodo cuando la fuente de datos devuelve CSV.
Paso 3 — Crear el mapa base
Para la estética, vamos a usar un tema oscuro: CartoDB dark_matter. En mapas donde lo importante son las líneas (rutas, cables, trayectos),
un fondo oscuro hace que el trazado destaque muchísimo más y, además, reduce el “ruido visual” frente a mapas muy cargados de detalles.
m_cables = folium.Map(
location=[43.0, 6.0],
zoom_start=7,
tiles="CartoDB dark_matter"
)
Paso 4 — Dibujar los cables con animación (AntPath)
En este punto ya tenemos un GeoDataFrame con una columna geometry que guarda la forma de cada cable.
Ojo con un detalle importante: muchos cables no vienen como una única línea continua, sino como una
MultiLineString, es decir, una “colección de líneas” con varios tramos que, juntos, forman el cable completo.
Por eso no podemos dibujar una ruta de golpe: primero tenemos que recorrer cada tramo y pintarlo.
Además, aquí suele aparecer el típico “bug silencioso” de los mapas: el orden de coordenadas. En datos geoespaciales (EPSG:4326) lo habitual es que las coordenadas vengan como (longitud, latitud) = (x, y). Pero Folium/Leaflet espera justo lo contrario: (latitud, longitud). Si no las invertimos, el cable se dibuja en una ubicación incorrecta (a veces incluso fuera del mapa).
Y para que el resultado se lea mejor a simple vista, vamos a usar AntPath: dibuja la línea con un efecto animado tipo “flujo”,
lo que hace que las rutas destaquen mucho más sobre el mapa oscuro.
for _, row in cables.iterrows():
try:
# Un cable puede ser MultiLineString -> varios tramos (líneas)
for linea in row.geometry.geoms:
# Coordenadas originales del tramo (normalmente x,y = lon,lat)
coords = list(linea.coords)
# Folium necesita lat,lon -> invertimos el orden
ruta = []
for x, y in coords: # x=lon, y=lat
ruta.append([y, x]) # [lat, lon]
# Dibujamos el tramo con animación
plugins.AntPath(
locations=ruta,
delay=1200, # velocidad de la animación (más alto = más lento)
weight=3, # grosor de la línea
color="#00FFFF", # color principal
pulse_color="white",
opacity=0.8,
tooltip=row["name"], # nombre del cable al pasar el ratón
).add_to(m_cables)
except Exception as e:
# Si alguna geometría viene rara o incompleta, evitamos que se rompa todo el mapa
print(f"Error: {e}")
continue
El try/except está ahí a propósito: en fuentes públicas (como WFS) a veces te encuentras geometrías vacías,
cambios de esquema o valores raros. La idea es simple: preferimos que el mapa se genere igualmente y que, como mucho,
falle un cable concreto, pero que no se rompa todo el proceso por un caso aislado.
Paso 5 — Mejorar la legibilidad (overlay HTML) y exportar
Para rematar, vamos a añadir un pequeño “título flotante” incrustando un bloque HTML/CSS dentro del mapa, y después exportaremos el resultado como un HTML independiente para poder abrirlo y compartirlo fácilmente.
titulo_html = """
<div style="position: fixed; bottom: 50px; left: 50px; z-index:9999;
background-color:black; color:white; padding: 10px; border: 1px solid cyan; opacity:0.8;">
<b>📡 RED DE CABLES</b><br>
Mediterráneo Occidental
</div>
"""
m_cables.get_root().html.add_child(folium.Element(titulo_html))
m_cables.save("mapa_sigcables.html")
3) Resultado
Si quieres reproducir el mapa rápidamente, aquí tienes el bloque completo para copiar y pegar
(asumiendo que ya has importado pandas, geopandas, folium y plugins):
from shapely import wkt
enlace = "https://ows.emodnet-humanactivities.eu/wfs?service=WFS&version=1.1.0&request=GetFeature&srsName=EPSG:4326&typeName=sigcables&bbox=3.91,42.31,7.14,43.84,EPSG:4326&outputFormat=csv"
print(f"Descargando dataset...")
df = pd.read_csv(enlace)
# Usamos wkt para convertir el formato
df["geometry"] = df["the_geom"].apply(wkt.loads)
cables = gpd.GeoDataFrame(df, geometry="geometry")
m_cables = folium.Map(location=[43.0, 6.0], zoom_start=7, tiles="CartoDB dark_matter")
for idx, row in cables.iterrows():
# He puesto un trycatch porque como este dataset puede cambiar en el futuro para controlar un posible error
try:
# Como un cable tiene muchos trozos tenemos que recorrer cada trozo
for linea in row.geometry.geoms:
coords = list(linea.coords)
# Correccion: El formato viene en (Lon, Lat) y folium lo necesita al revés por tanto lo corregimos
ruta = []
for x, y in coords:
ruta.append([y, x])
plugins.AntPath(
locations=ruta,
delay=1200,
weight=3,
color="#00FFFF",
pulse_color="white",
opacity=0.8,
tooltip=row["name"],
).add_to(m_cables)
except Exception as e:
print(f"Error: {e}")
continue
# Para mejorar la visualización he creado un titulo usando html y css
titulo_html = """
<div style="position: fixed; bottom: 50px; left: 50px; z-index:9999;
background-color:black; color:white; padding: 10px; border: 1px solid cyan; opacity:0.8;">
<b>📡 RED DE CABLES</b><br>
Mediterráneo Occidental
</div>
"""
m_cables.get_root().html.add_child(folium.Element(titulo_html))
print("Se ha generado el mapa: mapa_sigcables.html'")
m_cables.save("mapa_sigcables.html")
m_cables
Descarga el notebook
Si quieres replicarlo exactamente igual y jugar con él, aquí tienes el notebook completo listo para ejecutar.
Fuentes y enlaces
- Report/visualización del área (referencia rápida del recorte geográfico). my-beach.eu
-
Servicio WFS (EMODnet Human Activities) – capa
sigcablesen CSV. ows.emodnet-humanactivities.eu