Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| from plotly.subplots import make_subplots | |
| from prophet import Prophet | |
| from prophet.plot import plot_plotly, plot_components_plotly | |
| from datetime import datetime | |
| import statsmodels.api as sm | |
| from datetime import datetime | |
| import io | |
| # Configuración de la página | |
| st.set_page_config(page_title="Analytics Dashboard", layout="wide", page_icon="📈") | |
| if 'df' not in st.session_state: | |
| st.session_state.df = pd.DataFrame() | |
| # Función para cargar datos | |
| def load_data(uploaded_file): | |
| try: | |
| if uploaded_file.name.endswith('.csv'): | |
| # Para CSV, detectar automáticamente el separador | |
| content = uploaded_file.read().decode('utf-8') | |
| dialect = io.StringIO(content[:1024]).read() | |
| sniffer = pd.io.parsing.Sniffer() | |
| delimiter = sniffer.sniff(dialect).delimiter | |
| uploaded_file.seek(0) # Resetear el puntero del archivo | |
| df = pd.read_csv(uploaded_file, delimiter=delimiter) | |
| else: | |
| # Para Excel | |
| df = pd.read_excel(uploaded_file) | |
| # Convertir automáticamente columnas de fecha | |
| df = df.apply(lambda col: pd.to_datetime(col, errors='ignore') | |
| if col.dtypes == object else col) | |
| return df | |
| except Exception as e: | |
| st.error(f"Error al cargar el archivo: {e}") | |
| return None | |
| # Función para combinar columnas de fecha | |
| def create_date_column(df): | |
| date_cols = st.columns(3) | |
| with date_cols[0]: | |
| year_col = st.selectbox("Selecciona columna de AÑO", df.columns) | |
| with date_cols[1]: | |
| month_col = st.selectbox("Selecciona columna de MES", df.columns[df.columns != year_col]) | |
| with date_cols[2]: | |
| condition = [(column != year_col and column != month_col) for column in df.columns] | |
| day_col = st.selectbox("Selecciona columna de DÍA", df.columns[condition]) | |
| if st.button("Crear fecha combinada"): | |
| try: | |
| # Convertir a enteros primero para mayor seguridad | |
| df['year'] = df[year_col].astype(int) | |
| df['month'] = df[month_col].astype(int) | |
| df['day'] = df[day_col].astype(int) | |
| # Crear la columna de fecha | |
| df['ds'] = pd.to_datetime(df[['year', 'month', 'day']]) | |
| # Mantener solo la columna resultante | |
| df = df.drop(['year', 'month', 'day'], axis=1) | |
| st.session_state.df = df | |
| st.success("Fecha creada exitosamente! Muestra de fechas:") | |
| st.dataframe(st.session_state.df[['ds']].head()) | |
| except Exception as e: | |
| st.error(f"Error al crear fecha: {str(e)}") | |
| return df | |
| # Función para descomposición de series temporales | |
| def plot_decomposition(df, date_col, value_col): | |
| st.header("📈 Descomposición de Series Temporales") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| decomposition_model = st.selectbox("Modelo de descomposición", ["aditiva", "multiplicativa"]) | |
| with col2: | |
| period = st.number_input("Periodo estacional", min_value=2, max_value=365, value=12) | |
| if st.button("Analizar componentes"): | |
| try: | |
| ts = df.set_index(date_col)[value_col].sort_index() | |
| decomposition = sm.tsa.seasonal_decompose(ts.dropna(), | |
| model=decomposition_model, | |
| period=period) | |
| fig = make_subplots(rows=4, cols=1, shared_xaxes=True, vertical_spacing=0.1, | |
| subplot_titles=("Serie Original", "Tendencia", | |
| "Estacionalidad", "Residuales")) | |
| # Serie Original | |
| fig.add_trace(go.Scatter(x=ts.index, y=ts, mode='lines', name="Original", | |
| line=dict(color='#1f77b4')), row=1, col=1) | |
| # Tendencia | |
| fig.add_trace(go.Scatter(x=decomposition.trend.index, y=decomposition.trend, | |
| mode='lines', name="Tendencia", line=dict(color='#ff7f0e')), | |
| row=2, col=1) | |
| # Estacionalidad | |
| fig.add_trace(go.Scatter(x=decomposition.seasonal.index, y=decomposition.seasonal, | |
| mode='lines', name="Estacionalidad", line=dict(color='#2ca02c')), | |
| row=3, col=1) | |
| # Residuales | |
| fig.add_trace(go.Scatter(x=decomposition.resid.index, y=decomposition.resid, | |
| mode='lines', name="Residuales", line=dict(color='#d62728')), | |
| row=4, col=1) | |
| fig.update_layout(title_text=f"Descomposición de {value_col}", | |
| height=800, showlegend=False, | |
| hovermode="x unified") | |
| st.plotly_chart(fig, use_container_width=True) | |
| except Exception as e: | |
| st.error(f"Error en descomposición: {str(e)}") | |
| # Análisis de datos | |
| def perform_analysis(df): | |
| with st.expander("🔍 Análisis Rápido", expanded=True): | |
| c1, c2, c3 = st.columns(3) | |
| with c1: | |
| st.metric("Registros", df.shape[0]) | |
| with c2: | |
| st.metric("Variables", df.shape[1]) | |
| with c3: | |
| missing = df.isna().sum().sum() | |
| st.metric("Valores Faltantes", missing) | |
| # Análisis estadístico | |
| with st.expander("📊 Estadísticas Descriptivas"): | |
| st.dataframe(df.describe().T.style.background_gradient(cmap='Blues')) | |
| with st.expander("📊 Vista General", expanded=True): | |
| date_col = 'ds' | |
| values = [column for column in df.select_dtypes(include=np.number).columns if column not in ['ds', "Año", "Mes", "Día", "Dia"]] | |
| value_col = st.selectbox("Seleccionar variable para análisis", | |
| values) | |
| plot_decomposition(df, date_col, value_col) | |
| # Análisis de correlación | |
| with st.expander("🧮 Matriz de Correlación"): | |
| numeric_df = df.select_dtypes(include=np.number) | |
| if len(numeric_df.columns) > 1: | |
| corr = numeric_df.corr() | |
| fig = go.Figure(data=go.Heatmap( | |
| z=corr.values, | |
| x=corr.columns, | |
| y=corr.index, | |
| colorscale='RdBu', | |
| zmin=-1, | |
| zmax=1, | |
| hoverongaps=True, | |
| text=np.round(corr.values, 2) | |
| )) | |
| fig.update_layout(title='Matriz de Correlación', | |
| width=800, | |
| height=600) | |
| st.plotly_chart(fig, use_container_width=True) | |
| else: | |
| st.warning("No hay suficientes columnas numéricas para calcular correlaciones") | |
| # Visualización temporal interactiva | |
| def time_analysis(df, date_col): | |
| with st.expander("⏳ Análisis Temporal", expanded=True): | |
| col1, col2 = st.columns([1, 3]) | |
| with col1: | |
| values = [column for column in df.select_dtypes(include=np.number).columns if column not in ['ds', "Año", "Mes", "Día", "Dia"]] | |
| value_col = st.selectbox("Seleccionar variable para análisis temporal", | |
| values) | |
| resample_freq = st.selectbox("Frecuencia de muestreo", | |
| ['D', 'W', 'M', 'Q', 'Y']) | |
| with col2: | |
| try: | |
| ts_df = df.set_index(date_col)[value_col] | |
| resampled = ts_df.resample(resample_freq).mean() | |
| fig = px.line(resampled, | |
| title=f"Evolución Temporal - {value_col}", | |
| labels={'value': value_col, 'index': 'Fecha'}, | |
| markers=True) | |
| fig.update_traces(line_width=2, | |
| hovertemplate="Fecha: %{x}<br>Valor: %{y:.2f}") | |
| fig.update_layout(hovermode="x unified") | |
| st.plotly_chart(fig, use_container_width=True) | |
| except Exception as e: | |
| st.error(f"Error en análisis temporal: {e}") | |
| # Pronósticos con Prophet | |
| def forecast_section(df, date_col): | |
| st.header("🔮 Modelo Predictivo") | |
| # Verificar y convertir a datetime si es necesario | |
| if not np.issubdtype(df[date_col].dtype, np.datetime64): | |
| df[date_col] = pd.to_datetime(df[date_col], errors='coerce') | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| # Selección de agrupación temporal | |
| freq_agrupacion = st.selectbox( | |
| "Frecuencia de agrupación", | |
| options=['Diaria', 'Semanal', 'Mensual'], | |
| help="Agrupa los datos en intervalos temporales antes de modelar" | |
| ) | |
| # Mapeo de frecuencias de pandas | |
| freq_map = { | |
| 'Diaria': 'D', | |
| 'Semanal': 'W-MON', | |
| 'Mensual': 'MS' | |
| } | |
| # Selección de método de agregación | |
| metodo_agregacion = st.selectbox( | |
| "Método de agregación", | |
| options=['Suma', 'Promedio', 'Maximo', 'Minimo'], | |
| index=1 | |
| ) | |
| with col2: | |
| values = [column for column in df.select_dtypes(include=np.number).columns | |
| if column not in ['ds', "Año", "Mes", "Día", "Dia"]] | |
| target_col = st.selectbox("Variable a predecir", values) | |
| min_value = 1 | |
| max_value = 14 | |
| value = 7 | |
| if freq_agrupacion == "Mensual": | |
| max_value = 2 | |
| value = 1 | |
| elif freq_agrupacion == "Semanal": | |
| max_value = 4 | |
| value = 2 | |
| periods = st.number_input( | |
| "Periodos a predecir", | |
| min_value=min_value, | |
| max_value=max_value, | |
| value=value, | |
| help="Número de unidades temporales a pronosticar según la frecuencia seleccionada" | |
| ) | |
| # Agrupación temporal de los datos | |
| try: | |
| df_temp = df.set_index(date_col)[target_col] | |
| if metodo_agregacion == 'Suma': | |
| df_grouped = df_temp.resample(freq_map[freq_agrupacion]).sum() | |
| elif metodo_agregacion == 'Promedio': | |
| df_grouped = df_temp.resample(freq_map[freq_agrupacion]).mean() | |
| elif metodo_agregacion == 'Maximo': | |
| df_grouped = df_temp.resample(freq_map[freq_agrupacion]).max() | |
| elif metodo_agregacion == 'Minimo': | |
| df_grouped = df_temp.resample(freq_map[freq_agrupacion]).min() | |
| df_grouped = df_grouped.reset_index() | |
| df_grouped.columns = [date_col, target_col] | |
| df = df_grouped.dropna() | |
| # Mostrar vista previa de los datos agrupados | |
| with st.expander("📊 Vista de datos agrupados", expanded=False): | |
| st.write(f"Formato actual: {freq_agrupacion} ({len(df)} registros)") | |
| fig = px.line(df, x=date_col, y=target_col, title=f"Datos Agrupados - {target_col}") | |
| fig.update_traces(mode='lines+markers') | |
| st.plotly_chart(fig, use_container_width=True) | |
| except Exception as e: | |
| st.error(f"Error al agrupar datos: {str(e)}") | |
| return | |
| # Resto de la configuración del modelo | |
| if st.button("Ejecutar Pronóstico", type="primary"): | |
| with st.spinner("Creando modelo..."): | |
| try: | |
| prophet_df = df[[date_col, target_col]].rename(columns={ | |
| date_col: 'ds', | |
| target_col: 'y' | |
| }).dropna() | |
| model = Prophet() | |
| model.fit(prophet_df) | |
| # Generar futuro considerando la frecuencia | |
| future = model.make_future_dataframe( | |
| periods=periods, | |
| freq=freq_map[freq_agrupacion][0] # Tomar primer carácter (D, W, M) | |
| ) | |
| forecast = model.predict(future) | |
| # Resultados | |
| st.subheader("Predicción y Componentes") | |
| fig1 = plot_plotly(model, forecast) | |
| fig1.update_layout( | |
| height=600, | |
| hovermode="x unified", | |
| xaxis_title="Fecha", | |
| yaxis_title=target_col, | |
| annotations=[ | |
| dict( | |
| text=f"Frecuencia: {freq_agrupacion} | Agregación: {metodo_agregacion}", | |
| xref="paper", | |
| yref="paper", | |
| x=0.5, | |
| y=-0.15, | |
| showarrow=False | |
| ) | |
| ] | |
| ) | |
| st.plotly_chart(fig1, use_container_width=True) | |
| st.subheader("Descomposición de Componentes") | |
| fig2 = plot_components_plotly(model, forecast) | |
| st.plotly_chart(fig2, use_container_width=True) | |
| # Mostrar métricas de rendimiento | |
| with st.expander("📈 Métricas de Rendimiento", expanded=True): | |
| from sklearn.metrics import mean_absolute_error, mean_squared_error | |
| merged = forecast.set_index('ds')[['yhat']].join( | |
| prophet_df.set_index('ds')['y'] | |
| ).dropna() | |
| mae = mean_absolute_error(merged['y'], merged['yhat']) | |
| rmse = np.sqrt(mean_squared_error(merged['y'], merged['yhat'])) | |
| col_met1, col_met2 = st.columns(2) | |
| with col_met1: | |
| st.metric("MAE (Error Absoluto Medio)", f"{mae:.2f}") | |
| with col_met2: | |
| st.metric("RMSE (Raíz del Error Cuadrático Medio)", f"{rmse:.2f}") | |
| except Exception as e: | |
| st.error(f"Error en el modelo: {str(e)}") | |
| def main(): | |
| st.title("📈 Dashboard Analítico Interactivo") | |
| # Crear pestañas | |
| tab1, tab2, tab3 = st.tabs(["Carga de Datos", "Análisis Exploratorio", "Pronósticos"]) | |
| with tab1: | |
| st.header("📤 Carga de Datos") | |
| uploaded_file = st.file_uploader("Sube tu archivo (Excel o CSV)", type=["xlsx", "xls", "csv"]) | |
| if uploaded_file: | |
| df = load_data(uploaded_file) | |
| if df is not None: | |
| st.success("Datos cargados exitosamente!") | |
| # Mostrar información básica del archivo | |
| file_details = { | |
| "Nombre": uploaded_file.name, | |
| "Tipo": uploaded_file.type, | |
| "Tamaño": f"{uploaded_file.size / 1024:.2f} KB" | |
| } | |
| st.json(file_details) | |
| # Nueva sección de análisis de fecha | |
| if 'ds' not in st.session_state.df.columns: | |
| st.warning("⚠️ No se detectó columna de fecha. Se creará una nueva columna 'ds'") | |
| create_date_column(df) | |
| st.dataframe(df.head(), use_container_width=True) | |
| if 'df' in st.session_state: | |
| with tab2: | |
| st.header("🔍 Análisis Exploratorio") | |
| perform_analysis(st.session_state.df) | |
| # Selección de columna de fecha para análisis temporal | |
| date_cols = st.session_state.df.select_dtypes(include=['datetime', 'datetimetz']).columns | |
| if len(date_cols) > 0: | |
| selected_date = st.selectbox("Seleccionar columna de fecha", date_cols) | |
| time_analysis(st.session_state.df, selected_date) | |
| else: | |
| st.warning("No se detectaron columnas de fecha en los datos") | |
| with tab3: | |
| st.header("🔮 Pronósticos con Prophet") | |
| if len(date_cols) > 0: | |
| selected_date = st.selectbox("Seleccionar fecha para pronóstico", date_cols) | |
| forecast_section(st.session_state.df, selected_date) | |
| else: | |
| st.error("Se requiere al menos una columna de fecha para pronósticos") | |
| if __name__ == "__main__": | |
| main() |