--- name: render_eda_markdown kind: function lang: py domain: datascience version: "1.0.0" purity: pure signature: "def render_eda_markdown(profile: dict) -> str" description: "Convierte un TableProfile (dict del grupo eda) en un report markdown legible y autosuficiente. Render puro: dict de entrada -> string markdown de salida. Lee todo defensivamente con .get(...) porque muchas claves del perfil pueden venir None. Genera secciones Overview, Columnas, Numéricas (con sparkline ASCII del histograma), Categóricas, Calidad, Correlaciones y Análisis LLM, omitiendo limpiamente lo que esté vacío." tags: [eda, markdown, render, report, profiling, datascience] params: - name: profile desc: "TableProfile dict del grupo eda: {table, source, profiled_at, n_rows, n_cols, size_bytes, duplicate_rows, duplicate_pct, constant_cols, all_null_cols, null_cell_pct, type_breakdown, columns:[ColumnProfile], correlations, key_candidates, quality_score, llm, models}. Cada ColumnProfile puede traer sub-dicts numeric/categorical/datetime que pueden ser None. Todas las claves se leen defensivamente." output: "String markdown con el report EDA. Empieza por '# EDA — ' y contiene las secciones disponibles (Overview, Columnas, Numéricas, Categóricas, Calidad, Correlaciones, Análisis LLM). Las secciones sin datos se omiten." uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "" imports: [] tested: true tests: ["test_contains_title_and_sections", "test_contains_column_names", "test_contains_sparkline", "test_pct_fields_scaled_by_100", "test_pct_handles_none_as_blank", "test_tolerates_none_correlations_and_llm", "test_tolerates_empty_profile", "test_tolerates_none_profile"] test_file_path: "python/functions/datascience/render_eda_markdown_test.py" file_path: "python/functions/datascience/render_eda_markdown.py" --- ## Ejemplo ```python from datascience import render_eda_markdown profile = { "table": "sales", "source": "data/sales.csv", "n_rows": 1000, "n_cols": 1, "null_cell_pct": 0.015, "type_breakdown": {"numeric": 1}, "columns": [ { "name": "price", "inferred_type": "float", "semantic_type": "currency", "null_pct": 0.0, "distinct_count": 850, "unique_pct": 0.85, "quality_score": 0.95, "flags": [], "numeric": { "min": 1.0, "median": 40.0, "mean": 42.5, "std": 12.3, "p25": 30.0, "p75": 55.0, "p95": 80.0, "p99": 95.0, "skew": 0.4, "outlier_pct": 0.012, "distribution_type": "right-skewed", "histogram": [ {"lo": 0, "hi": 25, "count": 100}, {"lo": 25, "hi": 50, "count": 500}, {"lo": 50, "hi": 75, "count": 300}, {"lo": 75, "hi": 100, "count": 50}, ], }, "categorical": None, }, ], "correlations": None, "llm": None, } md = render_eda_markdown(profile) print(md) ``` Salida (extracto): ```markdown # EDA — sales source: `data/sales.csv` · 1000 rows × 1 cols ... ### price ... histogram: `▂█▅▁` ``` ## Cuando usarla Úsala como paso final de un pipeline EDA: tras construir el `TableProfile` (con las funciones del grupo `eda` que perfilan columnas, calidad e histogramas), pásaselo a esta función para obtener un report markdown listo para volcar a un `.md`, una celda de notebook, una nota de vault o un mensaje. Es render puro: no escribe a disco, solo devuelve el string, así que tú decides dónde guardarlo. Tolera perfiles parciales (correlaciones o LLM aún no calculados) sin fallar. ## Gotchas Función pura sin efectos. El sparkline del histograma escala los `count` de cada bin linealmente sobre la rampa de bloques `▁▂▃▄▅▆▇█`; si todos los counts son iguales, se dibuja el bloque más bajo para todos. No escribe el report a ningún archivo — el caller es responsable de persistirlo. Convención de porcentajes: TODOS los campos `*_pct` (`null_pct`, `empty_pct`, `unique_pct`, `outlier_pct`, `zero_pct`, `negative_pct`, `null_cell_pct`, `duplicate_pct`, y el `pct`/`mode_pct` del sub-dict categorical) se esperan como **fracción 0-1** (p.ej. `unique_pct=0.857` = 85.7%). El render los multiplica por 100 al formatear, mostrando `85.70%`. No pases valores ya en escala 0-100 o saldrán inflados.