"""Genera un notebook Jupyter de EDA (nbformat v4) para una tabla DuckDB. Construye un .ipynb listo para abrir/ejecutar que perfila una tabla con el grupo `eda` del registry (profile_table + render_eda_markdown + run_eda_models + eda_llm_insights). La funcion NO ejecuta el notebook: solo escribe el archivo con las celdas. Es la base de la entrega "analysis EDA" que luego se lanza en el navegador colaborativo con las funciones del grupo `notebook`. """ import json import os def _code_cell(source: str) -> dict: """Construye una celda de codigo nbformat v4.""" return { "cell_type": "code", "source": source, "metadata": {}, "outputs": [], "execution_count": None, } def _markdown_cell(source: str) -> dict: """Construye una celda markdown nbformat v4.""" return {"cell_type": "markdown", "source": source, "metadata": {}} def build_eda_notebook( db_path: str, table: str, notebook_path: str, run_models: bool = False, run_llm: bool = False, ) -> dict: """Genera un notebook Jupyter de EDA para una tabla DuckDB. Construye un dict nbformat v4 (a mano, sin depender de la libreria nbformat) con celdas que perfilan la tabla usando el grupo `eda` del registry, lo serializa como JSON a disco y devuelve un resumen. NO ejecuta el notebook. Args: db_path: ruta al archivo DuckDB que contiene la tabla a perfilar. table: nombre de la tabla a perfilar dentro de la DuckDB. notebook_path: ruta de salida del .ipynb. El directorio padre se crea si no existe. run_models: si True, añade una celda que muestra prof["models"] (PCA explained_variance_ratio, kmeans best_k, outliers n_outliers). Tambien pasa run_models=True a profile_table dentro del notebook. run_llm: si True, añade una celda que llama eda_llm_insights(prof) para obtener insights generados por LLM. Returns: dict. En exito: {status:'ok', notebook_path: str, n_cells: int}. En error (sin lanzar): {status:'error', error: str}. """ try: cells = [] # 1) Titulo. cells.append( _markdown_cell( f"# EDA — {table}\nGenerado por el grupo `eda` del registry." ) ) # 2) Setup: sys.path + import de profile_table. cells.append( _code_cell( "import sys, os\n" "# El kernel startup del analysis (00_fn_registry.py) ya suele\n" "# exponer python/functions en sys.path. Como fallback asumimos\n" "# el repo en ~/fn_registry.\n" '_fns = os.path.join(os.path.expanduser("~"), "fn_registry", "python", "functions")\n' "if _fns not in sys.path:\n" " sys.path.insert(0, _fns)\n" "from pipelines.profile_table import profile_table" ) ) # 3) Perfilar la tabla. cells.append( _code_cell( f"r = profile_table({db_path!r}, {table!r}, run_models={run_models}, write_report=False)\n" 'prof = r["profile"]\n' 'prof["n_rows"], prof["n_cols"], prof["quality_score"]' ) ) # 4) Report markdown renderizado. cells.append( _code_cell( "from datascience import render_eda_markdown\n" "from IPython.display import Markdown, display\n" "display(Markdown(render_eda_markdown(prof)))" ) ) # 5) Tabla de columnas con pandas. cells.append( _code_cell( "import pandas as pd\n" "pd.DataFrame([\n" " {k: c.get(k) for k in (\n" ' "name", "inferred_type", "semantic_type", "null_pct",\n' ' "distinct_count", "unique_pct", "quality_score",\n' " )}\n" ' for c in prof["columns"]\n' "])" ) ) # 6) Correlaciones fuertes. cells.append( _code_cell( 'corr = prof.get("correlations")\n' 'pd.DataFrame(corr["strong"]) if corr and corr.get("strong") else "sin correlaciones fuertes"' ) ) # 7) Modelos (solo si run_models). if run_models: cells.append( _markdown_cell("## Modelos no supervisados") ) cells.append( _code_cell( 'models = prof.get("models") or {}\n' 'pca = models.get("pca") or {}\n' 'kmeans = models.get("kmeans") or {}\n' 'outliers = models.get("outliers") or {}\n' "{\n" ' "pca_explained_variance_ratio": pca.get("explained_variance_ratio"),\n' ' "kmeans_best_k": kmeans.get("best_k"),\n' ' "outliers_n_outliers": outliers.get("n_outliers"),\n' "}" ) ) # 8) Insights LLM (solo si run_llm). if run_llm: cells.append(_markdown_cell("## Insights (LLM)")) cells.append( _code_cell( "from datascience import eda_llm_insights\n" "ins = eda_llm_insights(prof)\n" "ins" ) ) # 9) Notas finales. cells.append( _markdown_cell( "## Notas\n\n" "- Este notebook fue generado por `build_eda_notebook` del grupo `eda`.\n" "- Ejecuta las celdas en orden. La primera celda de codigo asume que\n" " python/functions del registry esta en `sys.path` (kernel startup\n" " del analysis o `~/fn_registry`).\n" "- `profile_table` se llama con `write_report=False`: no escribe reports\n" " a disco, todo el perfil vive en la variable `prof`.\n" "- Para regenerar con modelos o insights LLM, vuelve a llamar a\n" " `build_eda_notebook(..., run_models=True, run_llm=True)`." ) ) notebook = { "cells": cells, "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3", }, "language_info": {"name": "python"}, }, "nbformat": 4, "nbformat_minor": 5, } parent = os.path.dirname(os.path.abspath(notebook_path)) if parent: os.makedirs(parent, exist_ok=True) with open(notebook_path, "w", encoding="utf-8") as f: json.dump(notebook, f, indent=1) return { "status": "ok", "notebook_path": notebook_path, "n_cells": len(cells), } except Exception as exc: # noqa: BLE001 - dict-no-throw return {"status": "error", "error": str(exc)}