File size: 39,478 Bytes
97a4bf8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
# 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.")