Files
fn_registry/python/functions/datascience/build_geo_scatter.md
T
egutierrez cd658cc703 feat(eda): primitivas geoespaciales del grupo eda (detección lat/lon + extensión + scatter)
Tres funciones puras nuevas del dominio datascience (tags eda + geospatial) que
sostienen el capítulo GEOSPATIAL del AutomaticEDA, delegadas a fn-constructor:

- detect_latlon_columns: identifica el par (lat, lon) por nombre de columna +
  rango de valores ([-90,90] / [-180,180]) desde profile['columns']. Devuelve
  {lat_col, lon_col, confidence, reason}. 9 tests.
- analyze_geo_extent: bbox, centroide, span haversine, conteo por zona/país
  (lookup offline con bounding boxes embebidos, KISS sin geopandas) y
  hemisferios. 7 tests.
- build_geo_scatter: prepara los puntos del scatter en orden [lon, lat] con
  downsampling determinista por paso fijo + aspect equirectangular 1/cos(lat)
  clampado. 6 tests.

Registradas en datascience/__init__.py. Todas pure, params_schema completo,
.md autosuficiente (Ejemplo + Cuando usarla + Gotchas).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:29:33 +02:00

5.6 KiB

name, kind, lang, domain, version, purity, signature, description, tags, params, output, uses_functions, uses_types, returns, returns_optional, error_type, imports, tested, tests, test_file_path, file_path
name kind lang domain version purity signature description tags params output uses_functions uses_types returns returns_optional error_type imports tested tests test_file_path file_path
build_geo_scatter function py datascience 1.0.0 pure def build_geo_scatter(lats: list, lons: list, max_points: int = 2000) -> dict Prepara los datos de un scatter geografico en proyeccion equirectangular para el grupo eda. Empareja lats/lons por indice, descarta pares None/NaN/inf/bool o fuera de rango (lat en [-90,90], lon en [-180,180]) y aplica downsampling DETERMINISTA por paso fijo (pairs[::step]) cuando hay mas pares validos que max_points, para no saturar el PDF/PPTX en moviles. Devuelve los puntos en orden [lon, lat] listos para ax.scatter, el bbox, el aspect 1/cos(centroid_lat) clampado a [0.3,5.0] y un pad sugerido (~5% del rango con suelo minimo). Lectura defensiva; NUNCA lanza ni dibuja: el capitulo se encarga de matplotlib.
eda
geospatial
datascience
scatter
map
downsample
equirectangular
profiling
name desc
lats Lista (o tupla) de latitudes en grados, paralela a lons. Se empareja por indice. Un valor None, NaN, infinito, bool o fuera de [-90,90] descarta ese par. Lectura defensiva.
name desc
lons Lista (o tupla) de longitudes en grados, paralela a lats. Un valor None, NaN, infinito, bool o fuera de [-180,180] descarta ese par.
name desc
max_points Tope de puntos a devolver (default 2000). Si los pares validos superan el tope, se hace downsampling determinista por paso fijo step=ceil(n_total/max_points) tomando pairs[::step] (NO aleatorio, reproducible). Un valor no entero o <=0 desactiva el downsampling.
Dict listo para dibujar: {points: [[lon, lat], ...] en orden x=lon/y=lat para ax.scatter; n_total: pares validos antes del downsample (int); n_shown: puntos devueltos tras el downsample (int); downsampled: bool (n_shown<n_total); bbox: {lat_min, lat_max, lon_min, lon_max} o None si no hay puntos; aspect: 1/cos(centroid_lat) clampado a [0.3,5.0] para no estirar la proyeccion equirectangular; pad: {lon, lat} ~5% del rango respectivo con suelo minimo 0.01 grados}. Si no hay pares validos: points=[], n_total=0, n_shown=0, downsampled=False, bbox=None, aspect=1.0, pad={lon:0.0, lat:0.0}.
false
true
test_geo_scatter_nube_espana
test_downsampling_determinista_y_reproducible
test_listas_vacias_no_lanza
test_un_solo_punto_pad_minimo_y_aspect_finito
test_filtra_none_nan_y_fuera_de_rango
test_latitud_alta_aspect_clamped
python/functions/datascience/build_geo_scatter_test.py python/functions/datascience/build_geo_scatter.py

Ejemplo

import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from datascience.build_geo_scatter import build_geo_scatter

# Nube de coordenadas (lat, lon) alrededor de Madrid:
lats = [40.0, 41.0, 39.0, 40.5]
lons = [-3.7, -3.0, -4.0, -3.5]
geo = build_geo_scatter(lats, lons, max_points=2000)

print(geo["points"][0])     # [-3.7, 40.0]  -> orden [x=lon, y=lat]
print(geo["bbox"])          # {'lat_min': 39.0, 'lat_max': 41.0, 'lon_min': -4.0, 'lon_max': -3.0}
print(round(geo["aspect"], 3))  # 1.308  -> ensancha el eje x en latitudes medias
print(geo["pad"])           # {'lon': 0.05, 'lat': 0.1}  -> margen ~5%

# El capitulo dibuja con matplotlib (esta funcion NO dibuja):
#   xs = [p[0] for p in geo["points"]]; ys = [p[1] for p in geo["points"]]
#   ax.scatter(xs, ys); ax.set_aspect(geo["aspect"])
#   ax.set_xlim(geo["bbox"]["lon_min"] - geo["pad"]["lon"], geo["bbox"]["lon_max"] + geo["pad"]["lon"])
#   ax.set_ylim(geo["bbox"]["lat_min"] - geo["pad"]["lat"], geo["bbox"]["lat_max"] + geo["pad"]["lat"])

Cuando usarla

  • Usala antes de dibujar un scatter geografico (mapa de puntos en proyeccion equirectangular) en el capitulo geospatial de AutomaticEDA: limpia los pares de coordenadas, los reduce a un tamano razonable para el PDF/PPTX y te da bbox, aspect y pad listos para fijar los ejes.
  • Cuando tengas dos columnas de lat/lon ya extraidas y quieras un punto de entrada determinista (mismo dataset -> mismo dibujo) que no sature el documento en moviles.
  • Cuando necesites el aspect correcto para que un grado de longitud no se vea estirado respecto a uno de latitud (integridad visual, Tufte) sin calcularlo a mano.

Gotchas

  • Funcion pura, sin I/O y determinista. NO dibuja: solo PREPARA los datos; el capitulo se encarga de matplotlib. Lectura defensiva: pares con None/NaN/inf/bool o coordenadas fuera de rango se descartan en silencio y NUNCA lanza.
  • El downsampling es DETERMINISTA por paso fijo (step = ceil(n_total / max_points), pairs[::step]), NO aleatorio: la misma entrada produce siempre la misma salida (reproducible en tests). El primer punto mostrado es siempre el primer par valido. No es un muestreo uniforme aleatorio — es un barrido regular del orden de entrada.
  • points va en orden [lon, lat] (x, y), no [lat, lon]: pasalo directo a ax.scatter(xs, ys) sin invertir. Confundir el orden espeja el mapa.
  • aspect = 1/cos(centroid_lat) se clampa a [0.3, 5.0]. En latitudes altas cos -> 0 y el valor real explota: por encima de ~78 grados el aspect queda fijado en 5.0. Si el centroide cae justo en un polo (+-90) se usa el clamp en vez de dividir por cero.
  • pad es ~5% del rango de cada eje con un suelo minimo de 0.01 grados: con un solo punto o todos iguales (rango 0) el pad cae al suelo para que el punto no quede en una linea. En el caso sin puntos validos el pad es {lon:0.0, lat:0.0} y bbox es None.
  • bbox, aspect y pad se calculan sobre los puntos YA mostrados (tras el downsample), de modo que los ejes encajan exactamente con lo que se dibuja.