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 @st.cache_data 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}
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()