T-Flex-Prophet / app.py
luisbv's picture
update
3efbba6
Raw
History Blame Contribute Delete
16.4 kB
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}<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()