--- 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.