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