---
id: categorical_top_pie_figure_py_datascience
name: categorical_top_pie_figure
kind: function
lang: py
domain: datascience
version: "1.0.0"
purity: impure
signature: "def categorical_top_pie_figure(top: list, n_distinct: int = 0, title: str = \"\", top_k: int = 6, n_rows=None) -> \"matplotlib.figure.Figure\""
description: "Construye una figura matplotlib tipo donut (pie con agujero central) de las top_k categorías más frecuentes de una columna categórica, agregando el resto en un sector gris \"Otros (N categorías)\". Consume el bloque `top` de summarize_categorical y devuelve un matplotlib.figure.Figure listo para rasterizar por el renderer del informe EDA. Backend Agg sin pyplot global; defensivo ante top vacío/None."
tags: [eda, categorical, pie, donut, matplotlib, figure, visualization, datascience, impure]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: [matplotlib]
example: |
  from categorical_top_pie_figure import categorical_top_pie_figure
  top = [
      {"value": "rojo", "count": 40, "pct": 0.4},
      {"value": "azul", "count": 30, "pct": 0.3},
      {"value": "verde", "count": 20, "pct": 0.2},
  ]
  fig = categorical_top_pie_figure(top, n_distinct=12, title="color", top_k=6, n_rows=100)
tested: true
tests:
  - "test_returns_figure"
  - "test_ten_items_topk_six_yields_seven_wedges"
  - "test_empty_top_does_not_raise_and_returns_figure"
  - "test_long_value_truncated_in_legend"
  - "test_none_value_and_none_count_are_handled"
  - "test_n_rows_adds_exact_others_slice"
test_file_path: "python/functions/datascience/categorical_top_pie_figure_test.py"
file_path: "python/functions/datascience/categorical_top_pie_figure.py"
params:
  - name: top
    desc: "Lista de dicts {value, count, pct} ordenada de mayor a menor por count (salida del bloque `top` de summarize_categorical). Puede venir vacía o con dicts incompletos: items no-dict, sin count, con count None o count <= 0 se descartan. value None se admite (sin etiqueta)."
  - name: n_distinct
    desc: "Nº total de categorías distintas de la columna. Etiqueta el sector agregado como \"Otros (n_distinct - top_k)\" (mínimo 0). Si no supera el nº de sectores mostrados, se usa el overflow real de `top` como nº de categorías agregadas. Default 0."
  - name: title
    desc: "Título de la figura (nombre de la columna). Se trunca a ~48 chars con elipsis si es muy largo. Default \"\" (sin título)."
  - name: top_k
    desc: "Nº máximo de sectores explícitos. Default 6. El sector \"Otros\" no cuenta contra este límite. Con top_k <= 0 se muestra al menos la categoría mayor."
  - name: n_rows
    desc: "Opcional. Total de filas del dataset. Si se da y la suma de counts mostrados < n_rows, el sector \"Otros\" usa (n_rows - suma_mostrada) como count para que los ángulos sean exactos respecto al total real. Si se omite, \"Otros\" usa la suma de counts fuera del top_k mostrado (solo cuando top trae más de top_k items). Default None."
output: "Un matplotlib.figure.Figure (figsize 6.4x4.0, dpi 150) con un Axes donut (wedgeprops width 0.42) más una leyenda lateral con value truncado a 20 chars + count; el sector \"Otros\" en gris. Anotación central con el total n. Si no hay counts válidos, devuelve igualmente una Figure con un texto centrado \"sin datos categóricos\" (nunca lanza). El caller rasteriza/cierra la figura; la función no la muestra ni la guarda."
---

## Ejemplo

```python
from categorical_top_pie_figure import categorical_top_pie_figure

# `top` es la salida del bloque "top" de summarize_categorical (ya ordenado desc).
top = [
    {"value": "rojo", "count": 40, "pct": 0.40},
    {"value": "azul", "count": 30, "pct": 0.30},
    {"value": "verde", "count": 20, "pct": 0.20},
    {"value": "amarillo", "count": 5, "pct": 0.05},
]

fig = categorical_top_pie_figure(
    top,
    n_distinct=12,            # 12 categorías distintas en total
    title="color_producto",
    top_k=6,                  # hasta 6 sectores explícitos
    n_rows=100,               # "Otros" = 100 - 95 = 5, sobre 8 categorías agregadas
)

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

## Cuando usarla

Úsala dentro de un informe EDA cuando quieras visualizar la composición de una
columna categórica de un vistazo: cuántas filas caen en las categorías
dominantes frente a la cola larga. Pásale directamente el bloque `top` de
`summarize_categorical` (ya ordenado de mayor a menor) más `n_distinct` para que
el sector "Otros" indique cuántas categorías quedan agrupadas. Es la pareja
"composición" del gráfico de barras top-k: el donut comunica proporciones del
total, las barras comunican magnitudes comparables.

## 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 evita ese riesgo construyendo el `Figure`
  directamente, así que es segura de llamar en bucle desde el renderer.
- **El caller cierra la figura.** La función devuelve el `Figure` pero no lo
  muestra ni lo guarda. Quien la consume debe rasterizarla y luego liberarla
  (`fig.clf()` / `matplotlib.pyplot.close(fig)` si se usó pyplot en el caller)
  para no acumular memoria en lotes grandes de columnas.
- **Pie engaña con muchos sectores.** Por eso `top_k` por defecto es 6 y el
  resto se agrega en "Otros": donuts con 15+ sectores son ilegibles. Para
  cardinalidad muy alta el donut solo muestra la cabeza de la distribución; la
  cola vive en el sector gris.
- **Ángulos exactos solo con `n_rows`.** Sin `n_rows`, el sector "Otros" se
  calcula con el overflow presente en `top`; si `top` ya viene recortado a
  `top_k` por el productor, no habrá "Otros" aunque existan más categorías. Pasa
  `n_rows` (total de filas del dataset) para ángulos correctos respecto al total
  real.
- **Defensiva, nunca lanza.** `top=[]`, `value=None`, `count=None` o counts no
  numéricos se manejan sin error: en el peor caso devuelve una `Figure` con
  "sin datos categóricos". No envuelvas la llamada en try/except por miedo a un
  raise — no lo hay.
