fix(eda): keep-together de grafico+titulo+descripcion en 5 capitulos
modelos, timeseries, geospatial, agregacion y missingness (bloque de ranking) emitian Heading+Figure sueltos, de modo que el paginador podia dejar el titulo y la descripcion de una columna/par en una pagina y su grafico en la siguiente. Se envuelve cada unidad (Heading + descripcion/tablas + Figure) en un model.Group, la unidad keep-together que ambos renderers (PDF/PPTX) miden entera y mueven en bloque cuando no cabe, siguiendo el patron ya usado por num_distr y correlacion. Orden y contenido de bloques identicos: solo se envuelven. La degradacion honesta se conserva (una figura None nunca queda dentro de un Group vacio). Los tests que asertaban figuras sueltas se ajustaron para comprobar la Figure DENTRO del Group, sin relajar ningun assert. Bump CHAPTER_VERSION PATCH (1.0.0->1.0.1) en los 5 capitulos. El heatmap de co-ocurrencia de missingness ya iba agrupado y no se toca.
This commit is contained in:
@@ -73,7 +73,10 @@ try:
|
||||
except Exception: # noqa: BLE001
|
||||
suggest_aggregations_llm = None # type: ignore[assignment]
|
||||
|
||||
CHAPTER_VERSION = "1.0.0"
|
||||
# 1.0.1 — keep-together: cada gráfico (barras por grupo, barras del pivot) se
|
||||
# envuelve con su Heading + Markdown + tabla resumen en un model.Group para que el
|
||||
# paginador no separe el gráfico de su título/descripción. Cada unidad, su grupo.
|
||||
CHAPTER_VERSION = "1.0.1"
|
||||
CHAPTER_ID = "agregacion"
|
||||
CHAPTER_TITLE = "Agregación por grupos"
|
||||
|
||||
@@ -395,11 +398,11 @@ def _groupby_section(group_by: str, measures: list, result: dict, why: str) -> l
|
||||
return []
|
||||
eff_measures = result.get("measures") or measures or []
|
||||
|
||||
blocks = [model.Heading(text=f"Agrupado por «{group_by}»", level=2)]
|
||||
head = model.Heading(text=f"Agrupado por «{group_by}»", level=2)
|
||||
intro = f"**{why}.** " if why else ""
|
||||
intro += (f"{_fmt_num(result.get('n_groups') or len(groups))} grupos"
|
||||
f"{' (top por tamaño)' if result.get('truncated') else ''}.")
|
||||
blocks.append(model.Markdown(text=intro))
|
||||
intro_md = model.Markdown(text=intro)
|
||||
|
||||
# Summary table: one row per group, count + mean of every measure.
|
||||
header = ["Grupo", "n"] + [f"{m} (media)" for m in eff_measures]
|
||||
@@ -409,20 +412,16 @@ def _groupby_section(group_by: str, measures: list, result: dict, why: str) -> l
|
||||
for m in eff_measures:
|
||||
row.append(_fmt_num(_measure_mean(g, m), 2))
|
||||
rows.append(row)
|
||||
blocks.append(model.DataTable(
|
||||
summary_tbl = model.DataTable(
|
||||
header=header, rows=rows, title=f"Resumen por «{group_by}»",
|
||||
note="Conteo de filas y media de cada medida por grupo."))
|
||||
note="Conteo de filas y media de cada medida por grupo.")
|
||||
|
||||
if not eff_measures:
|
||||
return blocks
|
||||
return [head, intro_md, summary_tbl]
|
||||
|
||||
# Primary measure: a bar chart + a detail table (mean/median/std/min/max).
|
||||
primary = eff_measures[0]
|
||||
bars = _make_group_bars(group_by, primary, groups)
|
||||
if bars is not None:
|
||||
blocks.append(model.Figure(
|
||||
make=_group_bars_maker(group_by, primary, groups),
|
||||
caption=f"Media de «{primary}» por «{group_by}» (barras desde cero)."))
|
||||
|
||||
det_header = ["Grupo", "n", "media", "mediana", "σ", "mín", "máx"]
|
||||
det_rows = []
|
||||
@@ -435,10 +434,20 @@ def _groupby_section(group_by: str, measures: list, result: dict, why: str) -> l
|
||||
_fmt_num(ms.get("std"), 2), _fmt_num(ms.get("min"), 2),
|
||||
_fmt_num(ms.get("max"), 2),
|
||||
])
|
||||
blocks.append(model.DataTable(
|
||||
detail_tbl = model.DataTable(
|
||||
header=det_header, rows=det_rows,
|
||||
title=f"Detalle de «{primary}» por «{group_by}»"))
|
||||
return blocks
|
||||
title=f"Detalle de «{primary}» por «{group_by}»")
|
||||
|
||||
if bars is not None:
|
||||
# Keep-together: heading + intro + summary table + the bar chart ride on
|
||||
# the same page/slide (the renderers move the whole Group when it does not
|
||||
# fit), so the chart never gets stranded from its title. The per-measure
|
||||
# detail table (split-safe) flows after the group.
|
||||
fig = model.Figure(
|
||||
make=_group_bars_maker(group_by, primary, groups),
|
||||
caption=f"Media de «{primary}» por «{group_by}» (barras desde cero).")
|
||||
return [model.Group(blocks=[head, intro_md, summary_tbl, fig]), detail_tbl]
|
||||
return [head, intro_md, summary_tbl, detail_tbl]
|
||||
|
||||
|
||||
def _pivot_section(pivot_spec: dict, result: dict) -> list:
|
||||
@@ -457,13 +466,13 @@ def _pivot_section(pivot_spec: dict, result: dict) -> list:
|
||||
agg = result.get("agg") or pivot_spec.get("agg") or "mean"
|
||||
why = pivot_spec.get("why") or ""
|
||||
|
||||
blocks = [model.Heading(text=f"Pivot: «{index}» × «{columns}»", level=2)]
|
||||
head = model.Heading(text=f"Pivot: «{index}» × «{columns}»", level=2)
|
||||
intro = f"**{why}.** " if why else ""
|
||||
intro += (f"{agg} de «{value}» cruzando «{index}» (filas) y «{columns}» "
|
||||
f"(columnas).")
|
||||
if result.get("truncated_rows") or result.get("truncated_cols"):
|
||||
intro += " Limitado a las filas/columnas más frecuentes."
|
||||
blocks.append(model.Markdown(text=intro))
|
||||
intro_md = model.Markdown(text=intro)
|
||||
|
||||
header = [model._safe_str(index)] + [model._safe_str(c) for c in col_labels]
|
||||
rows = []
|
||||
@@ -474,20 +483,23 @@ def _pivot_section(pivot_spec: dict, result: dict) -> list:
|
||||
cell = cells[j] if j < len(cells) else None
|
||||
row.append(_fmt_num(cell, 2))
|
||||
rows.append(row)
|
||||
blocks.append(model.DataTable(
|
||||
matrix_tbl = model.DataTable(
|
||||
header=header, rows=rows,
|
||||
title=f"{agg} de «{value}»",
|
||||
note=f"Cada celda es {agg} de «{value}» para esa combinación."))
|
||||
note=f"Cada celda es {agg} de «{value}» para esa combinación.")
|
||||
|
||||
fig_pivot = {"row_labels": row_labels, "col_labels": col_labels,
|
||||
"matrix": matrix, "index": index, "columns": columns,
|
||||
"value": value, "agg": agg}
|
||||
if _make_pivot_bars(fig_pivot) is not None:
|
||||
blocks.append(model.Figure(
|
||||
# Keep-together: heading + intro + pivot table + the grouped-bar chart on
|
||||
# one page/slide, so the chart is never stranded from its title/table.
|
||||
fig = model.Figure(
|
||||
make=_pivot_bars_maker(fig_pivot),
|
||||
caption=f"{agg} de «{value}» por «{index}» y «{columns}» "
|
||||
f"(barras agrupadas)."))
|
||||
return blocks
|
||||
f"(barras agrupadas).")
|
||||
return [model.Group(blocks=[head, intro_md, matrix_tbl, fig])]
|
||||
return [head, intro_md, matrix_tbl]
|
||||
|
||||
|
||||
def _insights_section(ctx: dict) -> list:
|
||||
|
||||
Reference in New Issue
Block a user