--- id: build_boxplots_figure_py_datascience name: build_boxplots_figure kind: function lang: py domain: datascience version: "1.0.0" purity: impure signature: "def build_boxplots_figure(boxes: list, title: str = \"\", max_boxes: int = 12) -> \"matplotlib.figure.Figure\"" description: "Construye una unica figura matplotlib con boxplots de Tukey HORIZONTALES (uno por columna) usando ax.bxp: caja Q1-Q3, bigotes hasta 1.5*IQR, linea de mediana y puntos atipicos. Consume la salida de build_boxplot_stats (un dict box por columna, leido con .get) mas una lista opcional de outliers crudos por columna; si vienen los dibuja como puntos (showfliers), si no marca solo box[min]/box[max] cuando hay outliers de cola (igual que num_distr). Dibuja como mucho max_boxes cajas (las primeras, ya ordenadas por contaminacion por el caller) y avisa de la truncacion con (mostrando N de M). Backend Agg sin pyplot global; alto adaptativo al nº de cajas. Defensiva: omite entradas invalidas y NUNCA lanza — sin cajas validas devuelve una figura placeholder (sin boxplots). Es la version small-multiples del capitulo num_distr para responder que columnas tienen mas outliers de un vistazo." tags: [eda, outliers, boxplot, tukey, iqr, bxp, matplotlib, figure, visualization, small-multiples, datascience, impure] uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "error_go_core" imports: [matplotlib] example: | from datascience.build_boxplot_stats import build_boxplot_stats from datascience.build_boxplots_figure import build_boxplots_figure boxes = [ {"name": "ingresos", "box": build_boxplot_stats({"min": 1.0, "max": 9e3, "p25": 1e3, "median": 2e3, "p75": 3e3, "n_outliers": 7}), "fliers": None}, {"name": "edad", "box": build_boxplot_stats({"min": 0.0, "max": 99.0, "p25": 25.0, "median": 38.0, "p75": 52.0}), "fliers": None}, ] fig = build_boxplots_figure(boxes, title="Outliers por columna", max_boxes=12) tested: true tests: - "test_returns_figure_with_axes" - "test_empty_list_returns_placeholder_figure" - "test_invalid_box_is_skipped_not_raised" - "test_all_invalid_returns_placeholder" - "test_raw_fliers_are_drawn" - "test_max_boxes_truncates_and_does_not_raise" test_file_path: "python/functions/datascience/build_boxplots_figure_test.py" file_path: "python/functions/datascience/build_boxplots_figure.py" params: - name: boxes desc: "Lista de dicts, cada uno {\"name\": str, \"box\": dict, \"fliers\": list|None}. box es EXACTAMENTE la salida de build_boxplot_stats (claves leidas con .get: q1, median, q3, whisker_lo, whisker_hi, min, max, has_low_outliers, has_high_outliers, lower_fence, upper_fence, n_outliers). fliers es la lista opcional de outliers crudos: si viene se dibuja como puntos; si es None/ausente solo se marcan los extremos box[min]/box[max] cuando hay outliers de cola. Entradas que no son dict, sin box dict, o sin q1/median/q3 se omiten. El caller las pasa ya ordenadas por contaminacion (la mayor primera)." - name: title desc: "Titulo de la figura (fig.suptitle, alineado a la izquierda). Vacio => sin titulo. Si len(boxes) > max_boxes se le anade una nota \"(mostrando N de M)\" para que la truncacion no sea silenciosa. Default \"\"." - name: max_boxes desc: "Numero maximo de cajas a dibujar (las primeras de la lista). Default 12. Un valor no entero o <= 0 cae a 12. Si la lista trae mas entradas, las sobrantes se descartan pero se reporta en el titulo con (mostrando N de M)." output: "Un matplotlib.figure.Figure (figsize 7.0 x alto adaptativo = max(2.0, 0.5*n + 1.0), dpi 150) con un unico Axes que apila boxplots horizontales de Tukey (ax.bxp, orientation=horizontal con fallback vert=False), uno por columna valida, de arriba a abajo en el orden recibido. Cada caja: relleno #9ec6df, borde/bigotes/caps #5b8aa6, mediana #2e8b57, atipicos #c0392b. Etiquetas del eje Y = nombres de columna; eje X etiquetado \"valor\". Outliers dibujados desde fliers crudos (showfliers) o, si faltan, marcados en box[min]/box[max] segun has_low/high_outliers. Si no queda ninguna caja valida (lista vacia o todas invalidas) devuelve una Figure placeholder con texto centrado \"(sin boxplots)\"; cualquier error inesperado se captura y devuelve una Figure con el mensaje de error. NUNCA lanza. El caller rasteriza/cierra la figura; la funcion no la muestra ni la guarda." --- ## Ejemplo ```python import sys, os sys.path.insert(0, os.path.join("python", "functions")) from datascience.build_boxplot_stats import build_boxplot_stats from datascience.build_boxplots_figure import build_boxplots_figure # Un `box` por columna numérica, derivado del sub-bloque `numeric` del profile # (salida de describe_numeric). El caller los pasa ya ordenados por outlier_pct. boxes = [ { "name": "ingresos", "box": build_boxplot_stats({ "min": 1.0, "max": 9000.0, "p25": 1000.0, "median": 2000.0, "p75": 3000.0, "n_outliers": 7, }), "fliers": None, # valores crudos desconocidos -> se marca solo el extremo. }, { "name": "edad", "box": build_boxplot_stats({ "min": 0.0, "max": 99.0, "p25": 25.0, "median": 38.0, "p75": 52.0, }), "fliers": [88.0, 95.0, 99.0], # outliers crudos -> se dibujan como puntos. }, ] fig = build_boxplots_figure(boxes, title="Outliers por columna", max_boxes=12) # El renderer del informe lo rasteriza; aquí solo persistimos para inspección. fig.savefig("/tmp/boxplots.png") ``` ## Cuando usarla Úsala en el capítulo de outliers de un informe EDA cuando quieras comparar de un vistazo *qué columnas están más contaminadas por valores atípicos*: a diferencia de `num_distr` (que dibuja un histograma+boxplot por columna en figuras separadas), aquí apilas todos los boxplots horizontales en **una sola figura** (small multiples). Primero deriva el `box` de cada columna con `build_boxplot_stats`, ordénalas por `outlier_pct` descendente, envuélvelas como `{"name", "box", "fliers"}` y pásaselas. Si tienes los valores crudos fuera de las vallas, métele la lista `fliers` y se dibujarán como puntos; si no, la función marca solo los extremos `min`/`max` cuando hay cola. ## 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 construye 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. - **`fliers` opcional, semántica distinta.** Si pasas la lista de outliers crudos se dibujan todos como puntos (`showfliers=True`). Si es `None`/ausente los valores son desconocidos y solo se marca un punto en `box["min"]` / `box["max"]` cuando `has_low_outliers` / `has_high_outliers` — mismo criterio que `num_distr`. No inventes fliers a partir del profile: el `box` no trae los valores crudos, solo si los extremos superan las vallas. - **API de orientación de `ax.bxp`.** matplotlib reciente usa `orientation="horizontal"`; las versiones antiguas usan `vert=False`. La función prueba la primera y cae a la segunda en `except TypeError`, así que funciona en ambas. Si `bxp` falla del todo, el Axes degrada a un texto "(boxplot no disponible)" en vez de propagar. - **Truncación visible.** `max_boxes` (default 12) limita el nº de cajas para que ninguna se solape; si la lista trae más, las sobrantes se descartan pero se avisa en el título con "(mostrando N de M)". Pasa las columnas ya ordenadas por contaminación para que las descartadas sean las menos relevantes. - **Defensiva, nunca lanza.** Lista vacía, entradas no-dict, sin `box`, o sin `q1`/`median`/`q3` se omiten sin propagar; sin cajas válidas devuelve un placeholder "(sin boxplots)" y cualquier error inesperado se captura en una figura con el texto del error. No envuelvas la llamada en try/except por miedo a un raise — no lo hay.