---
id: relationship_scatter_figure_py_datascience
name: relationship_scatter_figure
kind: function
lang: py
domain: datascience
version: "1.0.0"
purity: impure
signature: "def relationship_scatter_figure(xs: list, ys: list, x_label: str = \"\", y_label: str = \"\", classification: dict = None, max_points: int = 2000) -> \"matplotlib.figure.Figure\""
description: "Construye una figura matplotlib scatter de un par de variables numéricas con su curva/recta de ajuste y una anotación del tipo de relación (lineal, polinómica grado 2/3, monótona no-lineal, etc.) más sus métricas (r, ρ, R²lin, R²poly). Consume el dict de classify_relationship_type; si es None lo calcula internamente reusando esa función. Devuelve un matplotlib.figure.Figure listo para rasterizar por el renderer del informe EDA (PDF/PPTX). Backend Agg sin pyplot global; downsample determinista de los puntos dibujados; defensivo ante vacío/None."
tags: [eda, correlation, scatter, relationship, matplotlib, figure, visualization, datascience, impure]
uses_functions: [classify_relationship_type_py_datascience]
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [matplotlib, numpy]
example: |
  from relationship_scatter_figure import relationship_scatter_figure
  xs = [float(i) for i in range(100)]
  ys = [0.5 * x * x - x + 3 for x in xs]
  classification = {
      "tipo": "polinómica (grado 2)", "pearson": 0.97, "spearman": 0.99,
      "r2_linear": 0.92, "r2_poly2": 0.999, "r2_poly3": 0.999,
      "best_degree": 2, "coeffs": [0.5, -1.0, 3.0],
  }
  fig = relationship_scatter_figure(xs, ys, x_label="dosis", y_label="efecto", classification=classification)
tested: true
tests:
  - "test_returns_figure"
  - "test_downsample_determinista"
  - "test_empty_no_lanza"
  - "test_classification_none"
test_file_path: "python/functions/datascience/relationship_scatter_figure_test.py"
file_path: "python/functions/datascience/relationship_scatter_figure.py"
params:
  - name: xs
    desc: "Lista (o tupla) de valores x. Se emparejan por índice con ys. Valores None, bool, NaN o inf descartan ese par (lectura defensiva)."
  - name: ys
    desc: "Lista (o tupla) de valores y, paralela a xs. Mismas reglas defensivas que xs."
  - name: x_label
    desc: "Etiqueta del eje/título para la variable x. Default \"\" (en el título cae a \"x\")."
  - name: y_label
    desc: "Etiqueta del eje/título para la variable y. Default \"\" (en el título cae a \"y\")."
  - name: classification
    desc: "Opcional. Dict de classify_relationship_type con claves tipo, pearson, r2_linear, spearman, r2_poly2, r2_poly3, best_degree, coeffs. Si es None se calcula internamente importando y llamando a classify_relationship_type sobre los pares limpios (self-contained). Si el módulo hermano no está disponible, se dibuja el scatter sin curva de ajuste ni anotación. Default None."
  - name: max_points
    desc: "Tope del nº de puntos DIBUJADOS. Si los pares limpios superan el tope, la nube se submuestrea por paso fijo ceil(n/max_points) tomando pairs[::step] — DETERMINISTA, no aleatorio, reproducible. La clasificación/ajuste usa SIEMPRE todos los pares limpios; el downsample solo adelgaza el dibujo. Valor no-positivo o no-int desactiva el downsample. Default 2000."
output: "Un matplotlib.figure.Figure (figsize 6.4x4.0, dpi 150) con un Axes scatter (puntos semitransparentes alpha 0.5, color #4C72B0), la curva/recta de ajuste (numpy.polyval sobre coeffs, color #C44E52) cuando hay un ajuste polinómico disponible, título \"{x_label} ↔ {y_label}\", labels de ejes y una caja de anotación en la esquina superior izquierda con el tipo de relación y las métricas disponibles (r, ρ, R²lin, R²poly; se omiten las None). Si tras la limpieza hay menos de 2 pares válidos, devuelve igualmente una Figure con un texto centrado \"Sin datos suficientes para el scatter\" (nunca lanza). El caller rasteriza/cierra la figura; la función no la muestra ni la guarda."
---

## Ejemplo

```python
from relationship_scatter_figure import relationship_scatter_figure

# Par numérico con relación cuadrática y su clasificación (de
# classify_relationship_type). Pasándola explícita evitas recomputarla.
xs = [float(i) for i in range(100)]
ys = [0.5 * x * x - x + 3 for x in xs]
classification = {
    "tipo": "polinómica (grado 2)",
    "pearson": 0.97,
    "spearman": 0.99,
    "r2_linear": 0.92,
    "r2_poly2": 0.999,
    "r2_poly3": 0.999,
    "best_degree": 2,
    "coeffs": [0.5, -1.0, 3.0],
}

fig = relationship_scatter_figure(
    xs, ys, x_label="dosis", y_label="efecto", classification=classification
)

# El renderer del informe lo rasteriza; aquí solo persistimos para inspección.
fig.savefig("/tmp/scatter_dosis_efecto.png")

# Con classification=None la función la calcula internamente (self-contained):
fig2 = relationship_scatter_figure(xs, ys, x_label="dosis", y_label="efecto")
```

## Cuando usarla

Úsala dentro del informe EDA automático cuando quieras visualizar de un vistazo
la relación entre dos variables numéricas: la nube de puntos, la curva que mejor
la ajusta y una etiqueta legible del tipo de relación con sus métricas. Es la
pareja "vista humana" de `classify_relationship_type`: esa función decide el
tipo y los coeficientes; esta los pinta en una `Figure` que el renderer del
informe rasteriza a PDF/PPTX. Pásale el dict de clasificación si ya lo tienes
calculado (evitas recomputar el ajuste); si no, déjalo en `None` y la función lo
resuelve sola sobre los pares limpios. Pensada para móvil: anotación pequeña
(fontsize 8) y nube adelgazada por `max_points` para que el PDF no pese.

## Gotchas

- **Impura por matplotlib.** Toca la maquinaria de render. Usa el backend `Agg`
  y la API orientada a objetos `Figure`/`add_subplot` — NUNCA `pyplot.*` aquí,
  para no tocar el estado global ni filtrar figuras entre llamadas. `pyplot` NO
  es thread-safe; esta función lo evita construyendo el `Figure` directamente,
  así que es segura de llamar en bucle desde el renderer.
- **El caller cierra la figura.** Devuelve el `Figure` pero no lo muestra ni lo
  guarda. Quien la consume debe rasterizarla y luego liberarla
  (`matplotlib.pyplot.close(fig)`) para no acumular memoria en lotes grandes de
  pares de columnas.
- **Downsample determinista, solo del dibujo.** Cuando los pares limpios superan
  `max_points`, la nube DIBUJADA se adelgaza por paso fijo `pairs[::step]`
  (reproducible, no aleatorio). La clasificación y el ajuste usan SIEMPRE todos
  los pares limpios; el downsample no altera las métricas ni la curva.
- **`classification=None` ⇒ se calcula sola.** Importa y llama a
  `classify_relationship_type` sobre los pares limpios. Si ese módulo hermano no
  está disponible (entorno incompleto), NO lanza: dibuja el scatter sin curva de
  ajuste ni anotación. Pasar la clasificación explícita es más barato (no
  recomputa el ajuste).
- **Sin curva para `monótona no-lineal`.** Cuando `coeffs` es `None` o
  `best_degree` es `None` (p.ej. tipo "monótona no-lineal"), no se pinta recta
  polinómica — solo la nube y la anotación. Tampoco se dibuja la curva si el
  rango de x es nulo (todos los x iguales). Nunca falla por esto.
- **Defensiva, nunca lanza.** `xs=[]`, `ys=[]`, menos de 2 pares válidos, ends
  `None`/`bool`/`NaN`/`inf` o `coeffs` malformado se manejan sin error: en el
  peor caso devuelve una `Figure` con "Sin datos suficientes para el scatter".
  No envuelvas la llamada en try/except por miedo a un raise — no lo hay.
