Spaces:
Sleeping
Sleeping
| """EDA Express — análisis exploratorio automático de datasets. | |
| Sube un CSV o Excel (o usa un dataset de ejemplo) y obtén: | |
| visión general, calidad de datos, distribuciones, correlaciones, | |
| análisis respecto a una variable objetivo, alertas accionables, | |
| conclusiones narrativas con LLM (opcional) e informe descargable. | |
| Ejecutar en local: streamlit run app.py | |
| """ | |
| import os | |
| import pandas as pd | |
| import plotly.express as px | |
| import streamlit as st | |
| from eda import alerts as alerts_mod | |
| from eda import insights as insights_mod | |
| from eda import profiler, report | |
| st.set_page_config(page_title="EDA Express", page_icon=None, layout="wide") | |
| DATASETS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "datasets") | |
| EXAMPLES = { | |
| "Titanic (pasajeros y supervivencia)": "titanic.csv", | |
| "Pingüinos de Palmer (medidas por especie)": "penguins.csv", | |
| } | |
| ALERT_STYLE = { | |
| "critico": ("Crítico", "#c0392b"), | |
| "aviso": ("Aviso", "#d68910"), | |
| "info": ("Info", "#2471a3"), | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Carga de datos (cacheada: cambiar de pestaña no recalcula nada) | |
| # --------------------------------------------------------------------------- | |
| def load_uploaded(data: bytes, filename: str) -> pd.DataFrame: | |
| return profiler.load_dataframe(data, filename) | |
| def load_example(filename: str) -> pd.DataFrame: | |
| with open(os.path.join(DATASETS_DIR, filename), "rb") as f: | |
| return profiler.load_dataframe(f.read(), filename) | |
| # --------------------------------------------------------------------------- | |
| # Barra lateral: origen de datos y opciones | |
| # --------------------------------------------------------------------------- | |
| st.sidebar.title("EDA Express") | |
| st.sidebar.caption("Análisis exploratorio automático") | |
| uploaded = st.sidebar.file_uploader("Sube un CSV o Excel", type=["csv", "xlsx", "xls"]) | |
| example_choice = st.sidebar.selectbox( | |
| "... o elige un dataset de ejemplo", | |
| ["(ninguno)"] + list(EXAMPLES.keys()), | |
| ) | |
| df = None | |
| dataset_name = None | |
| if uploaded is not None: | |
| try: | |
| df = load_uploaded(uploaded.getvalue(), uploaded.name) | |
| dataset_name = uploaded.name | |
| except Exception as e: | |
| st.sidebar.error(f"No se pudo leer el archivo: {e}") | |
| elif example_choice != "(ninguno)": | |
| df = load_example(EXAMPLES[example_choice]) | |
| dataset_name = EXAMPLES[example_choice] | |
| if df is None: | |
| st.title("EDA Express") | |
| st.markdown( | |
| "Sube un dataset en la barra lateral (o elige un ejemplo) y obtén un " | |
| "análisis exploratorio completo en segundos: calidad de datos, " | |
| "distribuciones, correlaciones, alertas accionables e informe descargable." | |
| ) | |
| st.markdown( | |
| "- **Todo se calcula en tu sesión** — los datos no salen del servidor.\n" | |
| "- Las **conclusiones narrativas con LLM** son opcionales y solo envían " | |
| "estadísticas agregadas, nunca filas.\n" | |
| "- Formatos: CSV (detecta `,` `;` y tabulador) y Excel." | |
| ) | |
| st.stop() | |
| if df.empty or df.shape[1] == 0: | |
| st.error("El archivo se leyó pero no contiene datos.") | |
| st.stop() | |
| target = st.sidebar.selectbox( | |
| "Columna objetivo (opcional)", | |
| ["(ninguna)"] + df.columns.tolist(), | |
| help="Si la eliges, se añade análisis de balance de clases y correlación con el objetivo.", | |
| ) | |
| target = None if target == "(ninguna)" else target | |
| st.sidebar.markdown("---") | |
| llm_on = st.sidebar.toggle( | |
| "Conclusiones con LLM", | |
| value=False, | |
| help="Requiere GROQ_API_KEY. Envía solo estadísticas agregadas, nunca los datos.", | |
| disabled=not insights_mod.llm_available(), | |
| ) | |
| if not insights_mod.llm_available(): | |
| st.sidebar.caption("Sin GROQ_API_KEY: conclusiones LLM desactivadas. El resto funciona igual.") | |
| # --------------------------------------------------------------------------- | |
| # Cálculo del perfil | |
| # --------------------------------------------------------------------------- | |
| ov = profiler.overview(df) | |
| all_alerts = alerts_mod.run_all_checks(df, target) | |
| plot_df = profiler.sample_for_plots(df) | |
| st.title(f"Análisis de {dataset_name}") | |
| if len(plot_df) < len(df): | |
| st.caption( | |
| f"Dataset grande: los gráficos usan una muestra de {len(plot_df):,} filas; " | |
| "las estadísticas están calculadas con todas." | |
| ) | |
| col1, col2, col3, col4, col5 = st.columns(5) | |
| col1.metric("Filas", f"{ov['filas']:,}") | |
| col2.metric("Columnas", ov["columnas"]) | |
| col3.metric("Faltantes", f"{ov['faltantes_pct']}%") | |
| col4.metric("Duplicadas", f"{ov['duplicadas_pct']}%") | |
| n_criticas = sum(1 for a in all_alerts if a["nivel"] == "critico") | |
| col5.metric("Alertas críticas", n_criticas) | |
| tabs = st.tabs( | |
| ["Alertas", "Columnas", "Faltantes", "Numéricas", "Categóricas", | |
| "Correlaciones", "Objetivo", "Conclusiones", "Informe"] | |
| ) | |
| # --- Alertas --------------------------------------------------------------- | |
| with tabs[0]: | |
| if not all_alerts: | |
| st.success("Sin alertas: el dataset pasa todas las comprobaciones de calidad.") | |
| for a in all_alerts: | |
| label, color = ALERT_STYLE[a["nivel"]] | |
| st.markdown( | |
| f"<div style='border-left: 4px solid {color}; padding: 0.4em 0.8em; margin-bottom: 0.6em;'>" | |
| f"<strong style='color:{color}'>{label}</strong> · <strong>{a['columna']}</strong><br>" | |
| f"{a['mensaje']}<br><em>{a['recomendacion']}</em></div>", | |
| unsafe_allow_html=True, | |
| ) | |
| # --- Columnas ---------------------------------------------------------------- | |
| with tabs[1]: | |
| st.dataframe(profiler.column_table(df), width="stretch", hide_index=True) | |
| with st.expander("Primeras 20 filas del dataset"): | |
| st.dataframe(df.head(20), width="stretch") | |
| # --- Faltantes --------------------------------------------------------------- | |
| with tabs[2]: | |
| missing = profiler.missing_table(df) | |
| if missing.empty: | |
| st.success("No hay valores faltantes.") | |
| else: | |
| fig = px.bar( | |
| missing, x="porcentaje", y="columna", orientation="h", | |
| labels={"porcentaje": "% faltante", "columna": ""}, | |
| title="Porcentaje de valores faltantes por columna", | |
| ) | |
| fig.update_layout(yaxis={"categoryorder": "total ascending"}) | |
| st.plotly_chart(fig, width="stretch") | |
| st.dataframe(missing, width="stretch", hide_index=True) | |
| # --- Numéricas --------------------------------------------------------------- | |
| with tabs[3]: | |
| numeric_cols = profiler.numeric_columns(df) | |
| if not numeric_cols: | |
| st.info("El dataset no tiene columnas numéricas.") | |
| else: | |
| st.dataframe(profiler.numeric_table(df), width="stretch", hide_index=True) | |
| selected = st.selectbox("Distribución de", numeric_cols) | |
| c1, c2 = st.columns(2) | |
| with c1: | |
| fig = px.histogram(plot_df, x=selected, nbins=40, title=f"Histograma de {selected}") | |
| st.plotly_chart(fig, width="stretch") | |
| with c2: | |
| fig = px.box(plot_df, y=selected, title=f"Caja y bigotes de {selected}") | |
| st.plotly_chart(fig, width="stretch") | |
| # --- Categóricas ------------------------------------------------------------- | |
| with tabs[4]: | |
| cat_cols = profiler.categorical_columns(df) | |
| if not cat_cols: | |
| st.info("El dataset no tiene columnas categóricas.") | |
| else: | |
| st.dataframe(profiler.categorical_table(df), width="stretch", hide_index=True) | |
| selected = st.selectbox("Frecuencias de", cat_cols) | |
| counts = profiler.value_counts_for(df, selected) | |
| fig = px.bar( | |
| counts, x="frecuencia", y="valor", orientation="h", | |
| title=f"Valores más frecuentes de {selected}", | |
| ) | |
| fig.update_layout(yaxis={"categoryorder": "total ascending"}) | |
| st.plotly_chart(fig, width="stretch") | |
| # --- Correlaciones ----------------------------------------------------------- | |
| with tabs[5]: | |
| corr = profiler.correlation_matrix(df) | |
| if corr.empty: | |
| st.info("Hacen falta al menos 2 columnas numéricas para correlacionar.") | |
| else: | |
| fig = px.imshow( | |
| corr, text_auto=True, color_continuous_scale="RdBu_r", | |
| zmin=-1, zmax=1, title="Matriz de correlación (Pearson)", aspect="auto", | |
| ) | |
| st.plotly_chart(fig, width="stretch") | |
| st.markdown("**Pares más correlacionados:**") | |
| st.dataframe(profiler.top_correlations(corr), width="stretch", hide_index=True) | |
| # --- Objetivo ---------------------------------------------------------------- | |
| with tabs[6]: | |
| if not target: | |
| st.info("Elige una columna objetivo en la barra lateral para activar este análisis.") | |
| else: | |
| kind = profiler.target_kind(df, target) | |
| if kind == "categorico": | |
| balance = profiler.class_balance(df, target) | |
| c1, c2 = st.columns([1, 1]) | |
| with c1: | |
| fig = px.pie(balance, names="clase", values="n", title=f"Balance de clases de {target}") | |
| st.plotly_chart(fig, width="stretch") | |
| with c2: | |
| st.dataframe(balance, width="stretch", hide_index=True) | |
| numeric_cols = [c for c in profiler.numeric_columns(df) if c != target] | |
| if numeric_cols: | |
| sel = st.selectbox("Distribución por clase de", numeric_cols) | |
| fig = px.box(plot_df, x=target, y=sel, title=f"{sel} según {target}") | |
| st.plotly_chart(fig, width="stretch") | |
| else: | |
| fig = px.histogram(plot_df, x=target, nbins=40, title=f"Distribución del objetivo {target}") | |
| st.plotly_chart(fig, width="stretch") | |
| corr_target = profiler.correlations_with_target(df, target) | |
| if not corr_target.empty: | |
| st.markdown("**Correlación de cada variable numérica con el objetivo:**") | |
| st.dataframe(corr_target, width="stretch", hide_index=True) | |
| # --- Conclusiones LLM ---------------------------------------------------------- | |
| with tabs[7]: | |
| if not llm_on: | |
| st.info( | |
| "Activa 'Conclusiones con LLM' en la barra lateral (requiere GROQ_API_KEY). " | |
| "Solo se envían estadísticas agregadas al LLM, nunca tus datos." | |
| ) | |
| else: | |
| if st.button("Generar conclusiones", type="primary"): | |
| with st.spinner("El LLM está leyendo las estadísticas..."): | |
| try: | |
| summary = profiler.compact_summary(df, all_alerts) | |
| st.session_state["insights"] = insights_mod.generate_insights(summary) | |
| except RuntimeError as e: | |
| st.error(str(e)) | |
| if "insights" in st.session_state: | |
| st.markdown(st.session_state["insights"]) | |
| # --- Informe ------------------------------------------------------------------- | |
| with tabs[8]: | |
| st.markdown( | |
| "Informe completo en Markdown: visión general, alertas con recomendaciones " | |
| "y todas las tablas del análisis. Ábrelo en cualquier editor o conviértelo a PDF." | |
| ) | |
| insights_text = st.session_state.get("insights") if llm_on else None | |
| report_md = report.build_report(df, dataset_name, all_alerts, target, insights_text) | |
| st.download_button( | |
| "Descargar informe (.md)", | |
| report_md, | |
| file_name=f"informe_{dataset_name.rsplit('.', 1)[0]}.md", | |
| mime="text/markdown", | |
| ) | |
| with st.expander("Vista previa del informe"): | |
| st.markdown(report_md) | |