File size: 9,808 Bytes
71175ed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316a44d
71175ed
 
 
 
 
316a44d
71175ed
 
 
 
 
0da3765
 
 
 
 
71175ed
f7eed9c
71175ed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52ee224
8266ec5
52ee224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8266ec5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52ee224
71175ed
 
 
 
 
 
 
 
 
 
 
 
 
 
52ee224
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
import os
import tempfile
import pandas as pd
from ydata_profiling import ProfileReport
from load_dataframe import cargar_dataframe, analizar_nulos_por_columna, get_file_metadata
from graficos_r import generar_histograma_r, generar_barras_r, generar_boxplot_r

class AnalizadorDatos:
    def __init__(self, file_path: str):
        self.file_path = file_path
        self.df = None
        self.metadata = None
        self.nulos_por_columna = None
        self.reporte_path = None
        self.graficos = {}
        
    def cargar_datos(self) -> bool:
        """Carga y valida los datos"""
        self.df, error = cargar_dataframe(self.file_path)
        if error:
            print(error)
            return False
        
        self.metadata = get_file_metadata(self.file_path)
        self.nulos_por_columna = analizar_nulos_por_columna(self.df)
        return True
    
    def generar_reporte_nulos(self) -> str:
        """Genera un reporte detallado de valores nulos por columna"""
        if not self.nulos_por_columna:
            return "No hay datos de nulos disponibles"
            
        reporte = "=== ANÁLISIS DE VALORES NULOS POR COLUMNA ===\n\n"
        reporte += f"Total filas en dataset: {len(self.df)}\n"
        reporte += f"Total columnas: {len(self.df.columns)}\n\n"
        
        for col in self.nulos_por_columna:
            reporte += f"Columna: {col['nombre_columna']} ({col['tipo_dato']})\n"
            reporte += f"  - Valores nulos: {col['total_nulos']} ({col['porcentaje_nulos']}%)\n"
            reporte += f"  - Valores no nulos: {col['no_nulos']} ({col['porcentaje_no_nulos']}%)\n"
            reporte += f"  - Valores únicos: {col['valores_unicos']}\n"
            
            if col['ejemplo_valores']:
                reporte += f"  - Ejemplos: {', '.join(col['ejemplo_valores'])}\n"
            else:
                reporte += "  - Ejemplos: TODOS SON NULOS\n"
            
            # Recomendación basada en porcentaje de nulos
            if col['porcentaje_nulos'] > 30:
                reporte += "  - RECOMENDACIÓN: Considerar eliminar o imputar esta columna\n"
            elif col['porcentaje_nulos'] > 0:
                reporte += "  - RECOMENDACIÓN: Evaluar imputación de valores\n"
            else:
                reporte += "  - RECOMENDACIÓN: Sin problemas de nulos\n"
            
            reporte += "\n"
        
        return reporte
    
    def generar_reporte(self, minimal: bool = True) -> str:
        """Genera reporte de profiling"""
        try:
            self.reporte_path = tempfile.NamedTemporaryFile(
                suffix='.html', 
                delete=False
            ).name
            
            profile = ProfileReport(
                self.df,
                title=f"Reporte de Análisis - {self.metadata['nombre']}",
                minimal=minimal,
                explorative=True
            )
            profile.to_file(self.reporte_path)
            return self.reporte_path
        except Exception as e:
            print(f"Error generando reporte: {e}")
            return None
    
    def generar_graficos(self, columnas_analisis: list) -> dict:
        """Genera gráficos para las columnas especificadas"""
        os.makedirs("graficos", exist_ok=True)
        
        for col in columnas_analisis:
            if col not in self.df.columns:
                continue
                
            output_path = f"graficos/{col}.png"
            
            if pd.api.types.is_numeric_dtype(self.df[col]):
                path = generar_histograma_r(self.df, col, output_path)
                if path:
                    self.graficos[col] = {'tipo': 'histograma', 'path': path}
                    
                # Gráfico adicional si hay una columna categórica adecuada
                categoricas = [c for c in self.df.columns 
                             if pd.api.types.is_categorical_dtype(self.df[c]) 
                             or pd.api.types.is_object_dtype(self.df[c])]
                if categoricas and len(self.df[categoricas[0]].unique()) <= 10:
                    path = generar_boxplot_r(self.df, col, categoricas[0], f"graficos/{col}_by_{categoricas[0]}.png")
                    if path:
                        self.graficos[f"{col}_by_{categoricas[0]}"] = {'tipo': 'boxplot', 'path': path}
            else:
                path = generar_barras_r(self.df, col, output_path)
                if path:
                    self.graficos[col] = {'tipo': 'barras', 'path': path}
        
        return self.graficos
    
    def resumen_analisis(self) -> dict:
        """Genera un resumen estructurado del análisis"""
        return {
            'metadata': self.metadata,
            'estadisticas': {
                'filas': len(self.df),
                'columnas': len(self.df.columns),
                'columnas_con_nulos': sum(1 for col in self.nulos_por_columna if col['total_nulos'] > 0),
                'columnas_completas': sum(1 for col in self.nulos_por_columna if col['total_nulos'] == 0)
            },
            'nulos_por_columna': self.nulos_por_columna,
            'graficos_generados': list(self.graficos.keys()),
            'rutas': {
                'reporte': self.reporte_path,
                'graficos': {k: v['path'] for k, v in self.graficos.items()}
            }
        }
    
    def procesar_valores_nulos(self, metodo: str) -> str:
        """Procesa los valores nulos del DataFrame según el método especificado"""
        if self.df is None:
            return "Error: No hay datos cargados"
        
        df_original = self.df.copy()
        columnas_numericas = self.df.select_dtypes(include=['number']).columns
        columnas_categoricas = self.df.select_dtypes(include=['object', 'category']).columns
        
        try:
            if metodo == "media":
                # Para columnas numéricas: llenar con la media
                for col in columnas_numericas:
                    if self.df[col].isnull().any():
                        media = self.df[col].mean()
                        self.df[col].fillna(media, inplace=True)
                
                # Para columnas categóricas: llenar con la moda
                for col in columnas_categoricas:
                    if self.df[col].isnull().any():
                        moda = self.df[col].mode()
                        if not moda.empty:
                            self.df[col].fillna(moda[0], inplace=True)
                            
            elif metodo == "mediana":
                # Para columnas numéricas: llenar con la mediana
                for col in columnas_numericas:
                    if self.df[col].isnull().any():
                        mediana = self.df[col].median()
                        self.df[col].fillna(mediana, inplace=True)
                
                # Para columnas categóricas: llenar con la moda
                for col in columnas_categoricas:
                    if self.df[col].isnull().any():
                        moda = self.df[col].mode()
                        if not moda.empty:
                            self.df[col].fillna(moda[0], inplace=True)
                            
            elif metodo == "moda":
                # Para todas las columnas: llenar con la moda
                for col in self.df.columns:
                    if self.df[col].isnull().any():
                        moda = self.df[col].mode()
                        if not moda.empty:
                            self.df[col].fillna(moda[0], inplace=True)
            
            # Actualizar análisis de nulos
            self.nulos_por_columna = analizar_nulos_por_columna(self.df)
            
            nulos_antes = df_original.isnull().sum().sum()
            nulos_despues = self.df.isnull().sum().sum()
            
            return (f"Procesamiento completado con método: {metodo}\n"
                   f"Valores nulos antes: {nulos_antes}\n"
                   f"Valores nulos después: {nulos_despues}\n"
                   f"Valores procesados: {nulos_antes - nulos_despues}")
                   
        except Exception as e:
            # Restaurar DataFrame original en caso de error
            self.df = df_original
            return f"Error al procesar valores nulos: {str(e)}"
    

def analisis_completo(file_path: str, columnas_analisis: list = None):
    """Función principal para ejecutar análisis completo"""
    analizador = AnalizadorDatos(file_path)
    
    if not analizador.cargar_datos():
        return None
    
    # Si no se especifican columnas, usar las que tienen menos del 50% de nulos
    if not columnas_analisis:
        columnas_validas = [
            col['nombre_columna'] for col in analizador.nulos_por_columna 
            if col['porcentaje_nulos'] < 50
        ]
        columnas_analisis = columnas_validas  # Limitar a 5 columnas por defecto
    
    # Generar outputs
    analizador.generar_reporte()
    analizador.generar_graficos(columnas_analisis)
    
    # Imprimir reporte de nulos en consola
    print(analizador.generar_reporte_nulos())
    
    return analizador.resumen_analisis()




if __name__ == "__main__":
    # Ejemplo de uso con análisis mejorado de nulos
    resumen = analisis_completo(
        file_path="Grammy Award Nominees and Winners 1958-2024.csv"
    )
    
    if resumen:
        print("\n=== RESUMEN GENERAL ===")
        print(f"Columnas con nulos: {resumen['estadisticas']['columnas_con_nulos']}")
        print(f"Columnas completas: {resumen['estadisticas']['columnas_completas']}")
        
        print("\nTop 5 columnas con más nulos:")
        for col in resumen['nulos_por_columna'][:5]:
            print(f"- {col['nombre_columna']}: {col['porcentaje_nulos']}% nulos")