# prepare.py - Módulo para datos import streamlit as st import pandas as pd import numpy as np import plotly.express as px from sklearn.preprocessing import StandardScaler, LabelEncoder from datetime import datetime from ydata_profiling import ProfileReport import os def show_prepare(): # Crear un contenedor para mensajes de estado status_container = st.empty() # Verificar si hay datos cargados if 'er_data' not in st.session_state: status_container.warning("⚠️ No hay datos cargados. Por favor, carga un dataset en la página Upload primero.") return try: # Usar los datos preparados si existen, si no, usar los datos originales if 'temp_prepared_data' in st.session_state: prepare = st.session_state.temp_prepared_data.copy() else: # Si no hay datos temporales, intentar usar datos preparados permanentes if 'prepared_data' in st.session_state: prepare = st.session_state.prepared_data.copy() else: prepare = st.session_state.er_data.copy() st.session_state.temp_prepared_data = prepare.copy() except AttributeError: status_container.warning("⚠️ No hay datos cargados o los datos son inválidos.") return # Análisis de valores únicos por columna st.markdown("### Análisis de Valores Únicos por Columna") # Selección de columnas para analizar - sin selección por defecto all_columns = prepare.columns.tolist() selected_columns = st.multiselect( "Seleccionar columnas para analizar", all_columns, default=[], # Sin selección por defecto key="unique_values_columns", help="Selecciona las columnas que deseas analizar" ) if not selected_columns: st.info("👆 Selecciona una o más columnas para ver su análisis detallado.") else: # Controles para número de valores a mostrar col1, col2 = st.columns(2) with col1: n_first = st.number_input( "Número de primeros valores a mostrar", min_value=1, max_value=20, value=5, key="n_first_values" ) with col2: n_last = st.number_input( "Número de últimos valores a mostrar", min_value=1, max_value=20, value=5, key="n_last_values" ) # Crear tabs para cada columna seleccionada tabs = st.tabs([f"📊 {col}" for col in selected_columns]) # Análisis por cada columna seleccionada for tab, col in zip(tabs, selected_columns): with tab: try: st.markdown(f"### Análisis de {col} ({prepare[col].dtype})") # Safely convert column to handle mixed types column_data = prepare[col].fillna('Sin valor').astype(str) valores_unicos = column_data.unique() n_valores = len(valores_unicos) # Información general en columnas col1, col2, col3 = st.columns([2, 1, 1]) with col1: st.write(f"Total de valores únicos: {n_valores}") with col2: st.write(f"Valores nulos: {prepare[col].isnull().sum()}") with col3: st.write(f"% nulos: {(prepare[col].isnull().sum() / len(prepare) * 100).round(2)}%") st.markdown("---") # Visualización de valores únicos if n_valores > (n_first + n_last): col1, col2 = st.columns(2) with col1: st.write("🔼 Primeros valores:") for valor in valores_unicos[:n_first]: st.write(f"• {str(valor)}") with col2: st.write("🔽 Últimos valores:") for valor in valores_unicos[-n_last:]: st.write(f"• {str(valor)}") else: st.write("📝 Todos los valores únicos:") for valor in valores_unicos: st.write(f"• '{str(valor)}'") st.markdown("---") # Distribución de frecuencias value_counts = column_data.value_counts() # Gráfico de barras con conversión segura fig_bar = { 'data': [{ 'type': 'bar', 'x': value_counts.index, 'y': value_counts.values, 'name': 'Frecuencia' }], 'layout': { 'title': f'Distribución de valores en {col}', 'xaxis': {'title': 'Valor'}, 'yaxis': {'title': 'Frecuencia'}, 'height': 400, 'showlegend': False } } st.plotly_chart(fig_bar, use_container_width=True) # Tabla de frecuencias freq_df = pd.DataFrame({ 'Valor': value_counts.index, 'Frecuencia': value_counts.values, 'Porcentaje': (value_counts.values / len(prepare) * 100).round(2) }) st.dataframe(freq_df, use_container_width=True) except Exception as e: st.error(f"Error al procesar la columna {col}") st.error(str(e)) st.write(f"Detalles técnicos del error en la columna {col}:", e) st.subheader("Preparación de Datos") st.subheader("Eliminación de Columnas") # Verificar el estado actual de valores nulos current_null_count = prepare.isnull().sum().sum() all_columns = prepare.columns.tolist() columns_to_drop = st.multiselect( "Seleccionar columnas a eliminar", all_columns, key="columns_to_drop" ) if columns_to_drop: if st.button("Eliminar columnas seleccionadas", key="drop_columns_button"): try: # Crear una copia temporal antes de eliminar columnas temp_prepare = prepare.copy() # Verificar que las columnas existen antes de eliminarlas missing_cols = [col for col in columns_to_drop if col not in temp_prepare.columns] if missing_cols: st.error(f"❌ Las siguientes columnas no existen: {', '.join(missing_cols)}") return # Eliminar las columnas temp_prepare = temp_prepare.drop(columns=columns_to_drop) # Verificar valores nulos después de la eliminación new_null_count = temp_prepare.isnull().sum().sum() if new_null_count <= current_null_count: # Permitir igual o menor cantidad de nulos # Actualizar el DataFrame y el estado prepare = temp_prepare st.session_state.temp_prepared_data = prepare.copy() # Mostrar mensaje de éxito st.success(f"✅ Columnas eliminadas exitosamente: {', '.join(columns_to_drop)}") # Actualizar información sobre valores nulos if new_null_count == 0: st.success("✅ No hay valores nulos en los datos.") else: st.warning(f"⚠️ Hay {new_null_count} valores nulos en los datos.") # Mostrar resumen de las columnas restantes st.write(f"Columnas restantes: {len(prepare.columns)}") if st.checkbox("Ver lista de columnas restantes", key="remaining_columns_checkbox"): st.write(prepare.columns.tolist()) else: st.error(f"❌ La operación incrementaría los valores nulos de {current_null_count} a {new_null_count}. Operación cancelada.") except Exception as e: st.error(f"Error durante la eliminación de columnas: {str(e)}") st.exception(e) # Manejo de valores faltantes st.subheader("Manejo de Valores Faltantes") missing_values = prepare.isnull().sum() missing_percentages = (missing_values / len(prepare) * 100).round(2) # Crear DataFrame y ordenar por número de valores faltantes de mayor a menor missing_df = pd.DataFrame({ 'Columna': missing_values.index, 'Valores Faltantes': missing_values.values, 'Porcentaje': missing_percentages.values, 'Tipo': prepare[missing_values.index].dtypes }) missing_df = missing_df[missing_df['Valores Faltantes'] > 0].sort_values('Valores Faltantes', ascending=False) if not missing_df.empty: # Mostrar advertencia si hay columnas de tipo object con valores faltantes object_cols = missing_df[missing_df['Tipo'] == 'object'] if not object_cols.empty: st.warning("⚠️ Se detectaron columnas de tipo texto/categórico (object) con valores faltantes. " "Se recomienda revisar estos casos con especial atención ya que el método de imputación " "podría afectar significativamente el análisis.") st.write("Columnas de tipo texto/categórico con valores faltantes:") st.dataframe(object_cols) st.write("Valores faltantes por columna (ordenados de mayor a menor):") st.dataframe(missing_df) # Checkbox para manejo especial de columnas object handle_objects = st.checkbox("Especificar valor de reemplazo para columnas de texto", help="Marca esta opción para especificar un valor personalizado para rellenar " "los valores faltantes en columnas de texto/categóricas") object_replacement = None if handle_objects: object_replacement = st.text_input("Valor de reemplazo para columnas de texto:", value="MISSING", help="Este valor se usará para rellenar los valores faltantes " "en todas las columnas de texto/categóricas") missing_strategy = st.radio( "Selecciona estrategia para valores faltantes:", ["Eliminar filas", "Rellenar con media", "Rellenar con mediana", "Rellenar con moda"] ) if st.button("Aplicar estrategia de valores faltantes", key="apply_missing_strategy_button"): try: # Guardar el estado anterior de prepare para verificación nulls_before = prepare.isnull().sum().sum() if missing_strategy == "Eliminar filas": # Guardar el número de filas antes rows_before = len(prepare) # Crear una copia para no modificar el original prepare_cleaned = prepare.copy() # Eliminar filas con valores nulos prepare_cleaned = prepare_cleaned.dropna(how='any') # Verificar que no queden valores nulos if prepare_cleaned.isnull().sum().sum() == 0: prepare = prepare_cleaned # Actualizar prepare solo si la limpieza fue exitosa st.session_state.temp_prepared_data = prepare.copy() # Actualizar el estado temporal rows_removed = rows_before - len(prepare) st.success(f"Se eliminaron {rows_removed} filas con valores faltantes. No quedan valores nulos.") else: st.error(f"Error: Aún quedan {prepare_cleaned.isnull().sum().sum()} valores faltantes después de la eliminación.") return else: # Separar columnas numéricas y no numéricas numeric_cols = prepare.select_dtypes(include=['int64', 'float64']).columns non_numeric_cols = prepare.select_dtypes(exclude=['int64', 'float64']).columns # Manejar columnas object primero si se especificó un valor de reemplazo if handle_objects and object_replacement is not None: object_cols = prepare.select_dtypes(include=['object']).columns for col in object_cols: prepare[col] = prepare[col].fillna(object_replacement) if missing_strategy == "Rellenar con media": # Para columnas numéricas usar media if len(numeric_cols) > 0: prepare[numeric_cols] = prepare[numeric_cols].fillna(prepare[numeric_cols].mean()) # Para columnas no numéricas sin valor especificado usar moda if len(non_numeric_cols) > 0 and not handle_objects: for col in non_numeric_cols: prepare[col] = prepare[col].fillna(prepare[col].mode()[0] if not prepare[col].mode().empty else 'NA') elif missing_strategy == "Rellenar con mediana": # Para columnas numéricas usar mediana if len(numeric_cols) > 0: prepare[numeric_cols] = prepare[numeric_cols].fillna(prepare[numeric_cols].median()) # Para columnas no numéricas sin valor especificado usar moda if len(non_numeric_cols) > 0 and not handle_objects: for col in non_numeric_cols: prepare[col] = prepare[col].fillna(prepare[col].mode()[0] if not prepare[col].mode().empty else 'NA') else: # Rellenar con moda # Usar moda para todas las columnas que no son object o no tienen valor especificado for col in prepare.columns: if prepare[col].dtype in ['object']: if handle_objects: continue # Ya se manejaron las columnas object mode_value = prepare[col].mode() prepare[col] = prepare[col].fillna(mode_value[0] if not mode_value.empty else ('NA' if col in non_numeric_cols else 0)) # Actualizar el estado temporal después de la imputación st.session_state.temp_prepared_data = prepare.copy() # Verificar los cambios nulls_after = prepare.isnull().sum().sum() values_filled = nulls_before - nulls_after if missing_strategy == "Eliminar filas": st.success(f"Se eliminaron {values_filled} filas con valores faltantes") else: st.success(f"Se rellenaron {values_filled} valores faltantes") # Verificar si quedan valores nulos remaining_nulls = prepare.isnull().sum() remaining_nulls = remaining_nulls[remaining_nulls > 0] if not remaining_nulls.empty: st.error("⚠️ Error: Aún quedan valores faltantes en las siguientes columnas:") for col in remaining_nulls.index: st.write(f"- {col}: {remaining_nulls[col]} valores faltantes") st.write("Por favor, contacta al equipo de desarrollo para revisar este error.") # Mostrar un resumen de los datos actualizados st.write("\n### Resumen después del procesamiento:") st.write(f"- Total de filas: {len(prepare)}") st.write(f"- Total de columnas: {len(prepare.columns)}") st.write(f"- Valores faltantes totales: {prepare.isnull().sum().sum()}") # Verificación final de valores nulos final_null_check = prepare.isnull().sum().sum() if final_null_check == 0: st.success("✅ ¡No quedan valores faltantes en el dataset!") else: st.error(f"⚠️ Aún quedan {final_null_check} valores nulos en el dataset.") return # Actualizar la sesión solo si no hay valores nulos if final_null_check == 0: st.session_state.prepared_data = prepare.copy() st.session_state.temp_prepared_data = prepare.copy() # No sobrescribir 'er_data' except Exception as e: st.error(f"Error al procesar valores faltantes: {str(e)}") st.error("Detalles técnicos del error:") st.code(str(e)) # Manejo de fechas st.subheader("Manejo de Fechas") with st.expander("Procesamiento de Fechas"): date_columns = st.multiselect( "Seleccionar columnas de fecha", prepare.columns, key="date_columns" ) if date_columns: date_format = st.selectbox( "Formato de fecha", [ "yyyy-mm-dd", "dd-mm-yyyy", "mm-dd-yyyy", "yyyy-mm-dd hh:mm", "dd-mm-yyyy hh:mm", "mm-dd-yyyy hh:mm", "yyyy-mm-dd hh:mm:ss", "dd-mm-yyyy hh:mm:ss", "mm-dd-yyyy hh:mm:ss", "hh:mm", "hh:mm:ss" ], help="Selecciona el formato que coincida con tus datos de fecha/hora." ) time_format = st.radio( "Formato de hora", ["24 horas", "12 horas (AM/PM)"], help="Selecciona si el formato de hora está en 12 o 24 horas" ) # Ajustar las características disponibles según el formato if date_format in ["hh:mm", "hh:mm:ss"]: available_features = ["Hora del día", "Periodo del día", "Minutos", "Segundos"] else: if "hh:mm:ss" in date_format: available_features = [ "Año", "Mes", "Día", "Día de la semana", "Trimestre", "Estación", "Es fin de semana", "Hora del día", "Periodo del día", "Minutos", "Segundos" ] else: available_features = [ "Año", "Mes", "Día", "Día de la semana", "Trimestre", "Estación", "Es fin de semana", "Hora del día", "Periodo del día", "Minutos" ] date_features = st.multiselect( "Seleccionar características a extraer", available_features ) if st.button("Procesar fechas", key="process_dates_button"): for col in date_columns: try: if date_format in ["hh:mm", "hh:mm:ss"]: # Procesar solo tiempo if date_format == "hh:mm:ss": time_parse_format = '%I:%M:%S %p' if time_format == "12 horas (AM/PM)" else '%H:%M:%S' time_with_seconds = True else: time_parse_format = '%I:%M %p' if time_format == "12 horas (AM/PM)" else '%H:%M' time_with_seconds = False def convert_time(time_str): try: time_obj = datetime.strptime(time_str.strip(), time_parse_format) if time_with_seconds: return time_obj.hour, time_obj.minute, time_obj.second else: return time_obj.hour, time_obj.minute, None except ValueError: st.warning(f"⚠️ Formato de hora inesperado en {col}: '{time_str}'") return None, None, None if time_with_seconds else None # Aplicar la conversión y crear nuevas columnas hours_minutes_seconds = prepare[col].apply(convert_time) # Depuración: Mostrar una vista previa de la conversión st.write(f"Vista previa de la conversión de tiempo para la columna {col}:") st.write(hours_minutes_seconds.head()) if "Hora del día" in date_features: prepare[f'{col}_hora'] = hours_minutes_seconds.apply(lambda x: x[0] if x and x[0] is not None else None) if "Minutos" in date_features: prepare[f'{col}_minutos'] = hours_minutes_seconds.apply(lambda x: x[1] if x and x[1] is not None else None) if "Segundos" in date_features and time_with_seconds: prepare[f'{col}_segundos'] = hours_minutes_seconds.apply(lambda x: x[2] if x and x[2] is not None else None) # Agregar periodo del día si se seleccionó if "Periodo del día" in date_features: def get_period(hour): if hour is None: return None if 5 <= hour < 12: return 'Mañana' elif 12 <= hour < 17: return 'Tarde' elif 17 <= hour < 21: return 'Noche' else: return 'Madrugada' prepare[f'{col}_periodo'] = prepare[f'{col}_hora'].apply(get_period) else: # Definir el formato de parsing según la selección if date_format == "yyyy-mm-dd": date_parse_format = '%Y-%m-%d' elif date_format == "dd-mm-yyyy": date_parse_format = '%d-%m-%Y' elif date_format == "mm-dd-yyyy": date_parse_format = '%m-%d-%Y' elif date_format == "yyyy-mm-dd hh:mm": date_parse_format = '%Y-%m-%d %H:%M' if time_format == "24 horas" else '%Y-%m-%d %I:%M %p' elif date_format == "dd-mm-yyyy hh:mm": date_parse_format = '%d-%m-%Y %H:%M' if time_format == "24 horas" else '%d-%m-%Y %I:%M %p' elif date_format == "mm-dd-yyyy hh:mm": date_parse_format = '%m-%d-%Y %H:%M' if time_format == "24 horas" else '%m-%d-%Y %I:%M %p' elif date_format == "yyyy-mm-dd hh:mm:ss": date_parse_format = '%Y-%m-%d %H:%M:%S' if time_format == "24 horas" else '%Y-%m-%d %I:%M:%S %p' elif date_format == "dd-mm-yyyy hh:mm:ss": date_parse_format = '%d-%m-%Y %H:%M:%S' if time_format == "24 horas" else '%d-%m-%Y %I:%M:%S %p' elif date_format == "mm-dd-yyyy hh:mm:ss": date_parse_format = '%m-%d-%Y %H:%M:%S' if time_format == "24 horas" else '%m-%d-%Y %I:%M:%S %p' else: st.error(f"Formato de fecha no reconocido: {date_format}") continue # Convertir a datetime con manejo de errores temp_dates = pd.to_datetime(prepare[col], format=date_parse_format, errors='coerce') # Depuración: Mostrar una vista previa de las fechas parseadas st.write(f"Vista previa de las fechas parseadas para la columna {col}:") st.write(temp_dates.head()) # Manejo de valores que no se pudieron parsear if temp_dates.isnull().any(): st.warning(f"⚠️ Algunas fechas en la columna {col} no pudieron ser parseadas y se asignaron como NaT.") # Extraer características según selección if "Año" in date_features: prepare[f'{col}_año'] = temp_dates.dt.year if "Mes" in date_features: prepare[f'{col}_mes'] = temp_dates.dt.month if "Día" in date_features: prepare[f'{col}_dia'] = temp_dates.dt.day if "Día de la semana" in date_features: prepare[f'{col}_dia_semana'] = temp_dates.dt.dayofweek + 1 if "Trimestre" in date_features: prepare[f'{col}_trimestre'] = temp_dates.dt.quarter if "Es fin de semana" in date_features: prepare[f'{col}_fin_semana'] = temp_dates.dt.dayofweek.isin([5, 6]).astype(int) if "Estación" in date_features: def get_season(month): if month in [12, 1, 2]: return 'Invierno' elif month in [3, 4, 5]: return 'Primavera' elif month in [6, 7, 8]: return 'Verano' else: return 'Otoño' prepare[f'{col}_estacion'] = temp_dates.dt.month.apply(get_season) if "Hora del día" in date_features and any(sub in date_format for sub in ["hh:mm", "hh:mm:ss"]): prepare[f'{col}_hora'] = temp_dates.dt.hour if "Minutos" in date_features and any(sub in date_format for sub in ["hh:mm", "hh:mm:ss"]): prepare[f'{col}_minutos'] = temp_dates.dt.minute if "Segundos" in date_features and "hh:mm:ss" in date_format: prepare[f'{col}_segundos'] = temp_dates.dt.second if "Periodo del día" in date_features and any(sub in date_format for sub in ["hh:mm", "hh:mm:ss"]): def get_period(hour): if hour is None: return None if 5 <= hour < 12: return 'Mañana' elif 12 <= hour < 17: return 'Tarde' elif 17 <= hour < 21: return 'Noche' else: return 'Madrugada' prepare[f'{col}_periodo'] = temp_dates.dt.hour.apply(get_period) # Eliminar la columna original de fecha prepare = prepare.drop(columns=[col]) st.success(f"Columna {col} procesada exitosamente") except Exception as e: st.error(f"Error procesando {col}: {str(e)}") st.exception(e) # Actualizar el estado temporal después de procesar fechas st.session_state.temp_prepared_data = prepare.copy() # Codificación de variables categóricas st.subheader("Codificación de Variables Categóricas") categorical_columns = prepare.select_dtypes(include=['object']).columns if len(categorical_columns) > 0: encoding_method = st.radio( "Método de codificación:", ["Label Encoding", "One-Hot Encoding"] ) cols_to_encode = st.multiselect( "Seleccionar columnas para codificar", categorical_columns, key="cols_to_encode" ) if st.button("Aplicar codificación", key="apply_encoding_button"): if encoding_method == "Label Encoding": le = LabelEncoder() for col in cols_to_encode: try: prepare[col] = le.fit_transform(prepare[col].astype(str)) st.success(f"✅ Label Encoding aplicado a la columna '{col}'") except Exception as e: st.error(f"Error al codificar la columna {col} con Label Encoding: {str(e)}") # Actualizar el estado temporal después de la codificación st.session_state.temp_prepared_data = prepare.copy() else: # One-Hot Encoding try: prepare = pd.get_dummies(prepare, columns=cols_to_encode) st.success("✅ One-Hot Encoding aplicado") # Actualizar el estado temporal después de la codificación st.session_state.temp_prepared_data = prepare.copy() except Exception as e: st.error(f"Error al aplicar One-Hot Encoding: {str(e)}") # Normalización de variables numéricas st.subheader("Normalización de Variables Numéricas") numeric_columns = prepare.select_dtypes(include=['int64', 'float64']).columns if len(numeric_columns) > 0: cols_to_normalize = st.multiselect( "Seleccionar columnas para normalizar", numeric_columns, key="cols_to_normalize" ) if cols_to_normalize and st.button("Aplicar normalización", key="apply_normalization_button"): try: scaler = StandardScaler() prepare[cols_to_normalize] = scaler.fit_transform(prepare[cols_to_normalize]) st.success("✅ Normalización aplicada") # Actualizar el estado temporal después de la normalización st.session_state.temp_prepared_data = prepare.copy() except Exception as e: st.error(f"Error al aplicar normalización: {str(e)}") # Guardar datos preparados y mostrar matriz de correlación st.write("### Vista previa de los datos:") st.dataframe(prepare.head()) # Información sobre valores nulos null_count = prepare.isnull().sum().sum() if null_count > 0: st.warning(f"⚠️ Hay {null_count} valores nulos en los datos.") else: st.success("✅ No hay valores nulos en los datos.") # Matriz de correlación st.subheader("Matriz de Correlación") numerical_columns = prepare.select_dtypes(include=['int64', 'float64']).columns.tolist() if len(numerical_columns) > 1: corr_variables = st.multiselect( "Selecciona las variables para incluir en la matriz de correlación", options=numerical_columns, default=numerical_columns[:min(5, len(numerical_columns))] # Seleccionar hasta 5 columnas por defecto ) if corr_variables: try: # ------------------------------------------- # NUEVO: Detección de Outliers y Visualización # ------------------------------------------- for var in corr_variables: # Cálculo de Q1, Q3 e IQR Q1 = prepare[var].quantile(0.25) Q3 = prepare[var].quantile(0.75) IQR = Q3 - Q1 lower_bound = Q1 - 1.5 * IQR upper_bound = Q3 + 1.5 * IQR # Identificación de outliers outliers = prepare[(prepare[var] < lower_bound) | (prepare[var] > upper_bound)][var] num_outliers = outliers.shape[0] # Mostrar advertencia si hay outliers if num_outliers > 0: st.warning(f"⚠️ La variable **{var}** tiene {num_outliers} datos atípicos (outliers) detectados.") # Mostrar boxplot usando Plotly fig_box = px.box(prepare, y=var, title=f'Boxplot de {var}') st.plotly_chart(fig_box, use_container_width=True) # Calcular y mostrar la matriz de correlación corr_matrix = prepare[corr_variables].corr(method='pearson') # Mapa de calor de correlación fig_corr = px.imshow( corr_matrix, text_auto=True, aspect="auto", color_continuous_scale='RdBu_r', title='Matriz de Correlación de Pearson' ) st.plotly_chart(fig_corr, use_container_width=True) # Botón de descarga csv_corr = corr_matrix.to_csv(index=True).encode('utf-8') st.download_button( label="Descargar Matriz de Correlación como CSV", data=csv_corr, file_name='matriz_correlacion.csv', mime='text/csv', ) # Análisis de correlaciones significativas st.write("### Análisis de Correlaciones Significativas") threshold = st.slider( "Selecciona el umbral mínimo de correlación para considerar significativa", min_value=0.0, max_value=1.0, value=0.5, step=0.05 ) # Obtener y mostrar correlaciones significativas corr_pairs = corr_matrix.unstack() significant_corr = corr_pairs[ (abs(corr_pairs) >= threshold) & (abs(corr_pairs) < 1) ].drop_duplicates().sort_values(ascending=False) if not significant_corr.empty: st.write(f"Correlaciones significativas (|correlación| ≥ {threshold}):") for (var1, var2), corr_value in significant_corr.items(): st.write(f"- **{var1}** y **{var2}**: correlación de **{corr_value:.2f}**") else: st.write("No se encontraron correlaciones significativas con el umbral seleccionado.") except Exception as e: st.error(f"Error al calcular la matriz de correlación: {str(e)}") else: st.warning("Por favor, selecciona al menos una variable para mostrar la matriz de correlación.") else: st.warning("No hay suficientes variables numéricas para calcular correlaciones.") # Button para guardar datos preparados if st.button("Guardar datos preparados", key="save_prepared_data_button"): try: null_count = prepare.isnull().sum().sum() if null_count == 0: st.session_state.prepared_data = prepare.copy() st.session_state.temp_prepared_data = prepare.copy() st.session_state.data_saved = True st.success("✅ Datos preparados guardados exitosamente") # Generar reporte progress_container = st.empty() with progress_container: with st.spinner('Generando reporte del dataset...'): profile = ProfileReport(prepare, title="Dataset Report", explorative=True) st.session_state.report_html = profile.to_html() st.success("¡Reporte generado exitosamente!") else: st.error(f"❌ No se pueden guardar los datos. Aún hay {null_count} valores nulos.") st.warning("Por favor, aplica una estrategia de manejo de valores faltantes antes de guardar.") except Exception as e: st.error(f"Error al guardar los datos preparados: {str(e)}") # Botones de descarga fuera del bloque principal if 'data_saved' in st.session_state and st.session_state.data_saved: col1, col2 = st.columns(2) with col1: csv = st.session_state.prepared_data.to_csv(index=False).encode('utf-8') st.download_button( label="Descargar Dataset Preparado", data=csv, file_name="prepared_dataset.csv", mime="text/csv" ) with col2: st.download_button( label="Descargar Reporte del Dataset", data=st.session_state.report_html, file_name="dataset_report.html", mime="text/html" ) st.info("👆 No te olvides de guardar los datos preparados antes de continuar con el análisis en la página Training o Test.")