guille03 commited on
Commit
71175ed
路
1 Parent(s): 011d50d
Files changed (6) hide show
  1. Dockerfile +37 -0
  2. analisis.py +167 -0
  3. app.py +76 -0
  4. graficos_r.py +167 -0
  5. load_dataframe.py +94 -0
  6. requirements.txt +7 -0
Dockerfile ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Imagen base con Python
2
+ FROM python:3.10-slim
3
+
4
+ # Instalar R y herramientas necesarias del sistema
5
+ RUN apt-get update && apt-get install -y \
6
+ r-base \
7
+ libcurl4-openssl-dev \
8
+ libssl-dev \
9
+ libxml2-dev \
10
+ libfontconfig1 \
11
+ libfreetype6 \
12
+ libpng-dev \
13
+ libtiff5 \
14
+ libjpeg62-turbo-dev \
15
+ build-essential \
16
+ fonts-dejavu \
17
+ unzip \
18
+ wget \
19
+ && rm -rf /var/lib/apt/lists/*
20
+
21
+ # Instalar paquetes de R necesarios
22
+ RUN R -e "install.packages(c('ggplot2', 'dplyr', 'RColorBrewer'), repos='http://cran.rstudio.com/')"
23
+
24
+ # Establecer directorio de trabajo
25
+ WORKDIR /app
26
+
27
+ # Copiar archivos del proyecto
28
+ COPY . .
29
+
30
+ # Instalar dependencias de Python
31
+ RUN pip install --no-cache-dir -r requirements.txt
32
+
33
+ # Exponer el puerto de Gradio (por convenci贸n)
34
+ EXPOSE 7860
35
+
36
+ # Ejecutar la aplicaci贸n
37
+ CMD ["python", "app.py"]
analisis.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ import pandas as pd
4
+ from ydata_profiling import ProfileReport
5
+ from load_dataframe import cargar_dataframe, analizar_nulos_por_columna, get_file_metadata
6
+ from graficos_r import generar_histograma_r, generar_barras_r, generar_boxplot_r
7
+
8
+ class AnalizadorDatos:
9
+ def __init__(self, file_path: str):
10
+ self.file_path = file_path
11
+ self.df = None
12
+ self.metadata = None
13
+ self.nulos_por_columna = None
14
+ self.reporte_path = None
15
+ self.graficos = {}
16
+
17
+ def cargar_datos(self) -> bool:
18
+ """Carga y valida los datos"""
19
+ self.df, error = cargar_dataframe(self.file_path)
20
+ if error:
21
+ print(error)
22
+ return False
23
+
24
+ self.metadata = get_file_metadata(self.file_path)
25
+ self.nulos_por_columna = analizar_nulos_por_columna(self.df)
26
+ return True
27
+
28
+ def generar_reporte_nulos(self) -> str:
29
+ """Genera un reporte detallado de valores nulos por columna"""
30
+ if not self.nulos_por_columna:
31
+ return "No hay datos de nulos disponibles"
32
+
33
+ reporte = "=== AN脕LISIS DE VALORES NULOS POR COLUMNA ===\n\n"
34
+ reporte += f"Total filas en dataset: {len(self.df)}\n"
35
+ reporte += f"Total columnas: {len(self.df.columns)}\n\n"
36
+
37
+ for col in self.nulos_por_columna:
38
+ reporte += f"Columna: {col['nombre_columna']} ({col['tipo_dato']})\n"
39
+ reporte += f" - Valores nulos: {col['total_nulos']} ({col['porcentaje_nulos']}%)\n"
40
+ reporte += f" - Valores no nulos: {col['no_nulos']} ({col['porcentaje_no_nulos']}%)\n"
41
+ reporte += f" - Valores 煤nicos: {col['valores_unicos']}\n"
42
+
43
+ if col['ejemplo_valores']:
44
+ reporte += f" - Ejemplos: {', '.join(col['ejemplo_valores'])}\n"
45
+ else:
46
+ reporte += " - Ejemplos: TODOS SON NULOS\n"
47
+
48
+ # Recomendaci贸n basada en porcentaje de nulos
49
+ if col['porcentaje_nulos'] > 30:
50
+ reporte += " - RECOMENDACI脫N: Considerar eliminar o imputar esta columna\n"
51
+ elif col['porcentaje_nulos'] > 0:
52
+ reporte += " - RECOMENDACI脫N: Evaluar imputaci贸n de valores\n"
53
+ else:
54
+ reporte += " - RECOMENDACI脫N: Sin problemas de nulos\n"
55
+
56
+ reporte += "\n"
57
+
58
+ return reporte
59
+
60
+ def generar_reporte(self, minimal: bool = True) -> str:
61
+ """Genera reporte de profiling"""
62
+ try:
63
+ self.reporte_path = tempfile.NamedTemporaryFile(
64
+ suffix='.html',
65
+ delete=False
66
+ ).name
67
+
68
+ profile = ProfileReport(
69
+ self.df,
70
+ title=f"Reporte de An谩lisis - {self.metadata['nombre']}",
71
+ minimal=minimal,
72
+ explorative=True
73
+ )
74
+ profile.to_file(self.reporte_path)
75
+ return self.reporte_path
76
+ except Exception as e:
77
+ print(f"Error generando reporte: {e}")
78
+ return None
79
+
80
+ def generar_graficos(self, columnas_analisis: list) -> dict:
81
+ """Genera gr谩ficos para las columnas especificadas"""
82
+ os.makedirs("graficos", exist_ok=True)
83
+
84
+ for col in columnas_analisis:
85
+ if col not in self.df.columns:
86
+ continue
87
+
88
+ output_path = f"graficos/{col}.png"
89
+
90
+ if pd.api.types.is_numeric_dtype(self.df[col]):
91
+ path = generar_histograma_r(self.df, col, output_path)
92
+ if path:
93
+ self.graficos[col] = {'tipo': 'histograma', 'path': path}
94
+
95
+ # Gr谩fico adicional si hay una columna categ贸rica adecuada
96
+ categoricas = [c for c in self.df.columns
97
+ if pd.api.types.is_categorical_dtype(self.df[c])
98
+ or pd.api.types.is_object_dtype(self.df[c])]
99
+ if categoricas and len(self.df[categoricas[0]].unique()) <= 10:
100
+ path = generar_boxplot_r(self.df, col, categoricas[0], f"graficos/{col}_by_{categoricas[0]}.png")
101
+ if path:
102
+ self.graficos[f"{col}_by_{categoricas[0]}"] = {'tipo': 'boxplot', 'path': path}
103
+ else:
104
+ path = generar_barras_r(self.df, col, output_path)
105
+ if path:
106
+ self.graficos[col] = {'tipo': 'barras', 'path': path}
107
+
108
+ return self.graficos
109
+
110
+ def resumen_analisis(self) -> dict:
111
+ """Genera un resumen estructurado del an谩lisis"""
112
+ return {
113
+ 'metadata': self.metadata,
114
+ 'estadisticas': {
115
+ 'filas': len(self.df),
116
+ 'columnas': len(self.df.columns),
117
+ 'columnas_con_nulos': sum(1 for col in self.nulos_por_columna if col['total_nulos'] > 0),
118
+ 'columnas_completas': sum(1 for col in self.nulos_por_columna if col['total_nulos'] == 0)
119
+ },
120
+ 'nulos_por_columna': self.nulos_por_columna,
121
+ 'graficos_generados': list(self.graficos.keys()),
122
+ 'rutas': {
123
+ 'reporte': self.reporte_path,
124
+ 'graficos': {k: v['path'] for k, v in self.graficos.items()}
125
+ }
126
+ }
127
+
128
+
129
+ def analisis_completo(file_path: str, columnas_analisis: list = None):
130
+ """Funci贸n principal para ejecutar an谩lisis completo"""
131
+ analizador = AnalizadorDatos(file_path)
132
+
133
+ if not analizador.cargar_datos():
134
+ return None
135
+
136
+ # Si no se especifican columnas, usar las que tienen menos del 50% de nulos
137
+ if not columnas_analisis:
138
+ columnas_validas = [
139
+ col['nombre_columna'] for col in analizador.nulos_por_columna
140
+ if col['porcentaje_nulos'] < 50
141
+ ]
142
+ columnas_analisis = columnas_validas # Limitar a 5 columnas por defecto
143
+
144
+ # Generar outputs
145
+ analizador.generar_reporte()
146
+ analizador.generar_graficos(columnas_analisis)
147
+
148
+ # Imprimir reporte de nulos en consola
149
+ print(analizador.generar_reporte_nulos())
150
+
151
+ return analizador.resumen_analisis()
152
+
153
+
154
+ if __name__ == "__main__":
155
+ # Ejemplo de uso con an谩lisis mejorado de nulos
156
+ resumen = analisis_completo(
157
+ file_path="Grammy Award Nominees and Winners 1958-2024.csv"
158
+ )
159
+
160
+ if resumen:
161
+ print("\n=== RESUMEN GENERAL ===")
162
+ print(f"Columnas con nulos: {resumen['estadisticas']['columnas_con_nulos']}")
163
+ print(f"Columnas completas: {resumen['estadisticas']['columnas_completas']}")
164
+
165
+ print("\nTop 5 columnas con m谩s nulos:")
166
+ for col in resumen['nulos_por_columna'][:5]:
167
+ print(f"- {col['nombre_columna']}: {col['porcentaje_nulos']}% nulos")
app.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from analisis import AnalizadorDatos
3
+
4
+ analizador_global = None
5
+
6
+ def cargar_y_mostrar(file):
7
+ global analizador_global
8
+ analizador = AnalizadorDatos(file.name)
9
+
10
+ if not analizador.cargar_datos():
11
+ return "Error al cargar archivo", None, None
12
+
13
+ analizador_global = analizador
14
+
15
+ return (
16
+ f"Archivo cargado correctamente: {analizador.metadata['nombre']}\n"
17
+ f"- Tama帽o: {analizador.metadata['tama帽o_mb']} MB\n"
18
+ f"- Filas: {len(analizador.df)}\n"
19
+ f"- Columnas: {len(analizador.df.columns)}\n",
20
+ analizador.df.head(),
21
+ [col['nombre_columna'] for col in analizador.nulos_por_columna]
22
+ )
23
+
24
+ def generar_graficos(columnas):
25
+ global analizador_global
26
+ if not analizador_global:
27
+ return None
28
+
29
+ graficos = analizador_global.generar_graficos(columnas)
30
+ return [gr.Image(value=v['path'], label=k) for k, v in graficos.items()]
31
+
32
+ def generar_reporte():
33
+ global analizador_global
34
+ if not analizador_global:
35
+ return None, None
36
+
37
+ reporte_path = analizador_global.generar_reporte()
38
+ reporte_nulos = analizador_global.generar_reporte_nulos()
39
+ return reporte_path, reporte_nulos
40
+
41
+ with gr.Blocks(title="An谩lisis exploratorio avanzado con R y Python") as demo:
42
+ gr.Markdown("## Exploraci贸n de datos con R, Python y Gradio")
43
+
44
+ with gr.Row():
45
+ archivo_input = gr.File(label="Sube un archivo", file_types=[".csv", ".xlsx", ".json", ".parquet"])
46
+ cargar_btn = gr.Button("Cargar archivo")
47
+
48
+ estado_carga = gr.Textbox(label="Estado de carga", interactive=False)
49
+ df_preview = gr.Dataframe(label="Vista previa")
50
+ columnas_selector = gr.CheckboxGroup(label="Selecciona columnas para an谩lisis", choices=[])
51
+
52
+ cargar_btn.click(
53
+ fn=cargar_y_mostrar,
54
+ inputs=[archivo_input],
55
+ outputs=[estado_carga, df_preview, columnas_selector]
56
+ )
57
+
58
+ graficos_output = gr.Group(label="Gr谩ficos generados")
59
+
60
+ graficar_btn = gr.Button("Generar gr谩ficos")
61
+ graficar_btn.click(fn=generar_graficos, inputs=[columnas_selector], outputs=[graficos_output])
62
+
63
+ with gr.Row():
64
+ generar_reporte_btn = gr.Button("Generar reporte")
65
+ reporte_html = gr.File(label="Descargar reporte")
66
+ reporte_nulos = gr.Textbox(label="An谩lisis de nulos")
67
+
68
+ generar_reporte_btn.click(
69
+ fn=generar_reporte,
70
+ inputs=[],
71
+ outputs=[reporte_html, reporte_nulos]
72
+ )
73
+
74
+
75
+ if __name__ == "__main__":
76
+ demo.launch()
graficos_r.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pandas as pd
3
+ import rpy2.robjects as ro
4
+ from rpy2.robjects import pandas2ri
5
+ from rpy2.robjects.conversion import localconverter
6
+ from rpy2.robjects.packages import importr
7
+ import matplotlib.pyplot as plt
8
+
9
+ # Configuraci贸n de temas y estilos en R
10
+ def configurar_estilos_r():
11
+ """Configura temas y estilos globales para ggplot2"""
12
+ ro.r("""
13
+ library(ggplot2)
14
+ library(RColorBrewer)
15
+
16
+ # Tema personalizado
17
+ tema_personalizado <- theme_minimal() +
18
+ theme(
19
+ text = element_text(family = "sans", size = 12),
20
+ plot.title = element_text(size = 16, face = "bold", hjust = 0.5),
21
+ plot.subtitle = element_text(size = 12, hjust = 0.5),
22
+ axis.title = element_text(face = "bold"),
23
+ legend.position = "top",
24
+ panel.grid.major = element_line(color = "gray90"),
25
+ panel.grid.minor = element_blank(),
26
+ plot.margin = unit(c(1, 1, 1, 1), "cm")
27
+ )
28
+
29
+ # Paleta de colores
30
+ colores <- brewer.pal(8, "Set2")
31
+ """)
32
+
33
+ # Configurar estilos al importar el m贸dulo
34
+ configurar_estilos_r()
35
+
36
+ def generar_histograma_r(df: pd.DataFrame, columna: str, output_path: str) -> str:
37
+ """
38
+ Genera un histograma estilizado con ggplot2
39
+ """
40
+ try:
41
+ with localconverter(ro.default_converter + pandas2ri.converter):
42
+ r_df = ro.conversion.py2rpy(df[[columna]])
43
+
44
+ ro.r(f"""
45
+ library(ggplot2)
46
+ p <- ggplot({r_df.r_repr()}, aes(x={columna})) +
47
+ geom_histogram(
48
+ binwidth = diff(range({r_df.r_repr()}${columna}, na.rm=TRUE))/30,
49
+ fill = "#2c7fb8",
50
+ color = "#ffffff",
51
+ alpha = 0.8
52
+ ) +
53
+ labs(
54
+ title = "Distribuci贸n de {columna}",
55
+ x = "{columna}",
56
+ y = "Frecuencia"
57
+ ) +
58
+ tema_personalizado +
59
+ scale_fill_brewer(palette = "Set2")
60
+
61
+ ggsave(
62
+ filename = "{output_path}",
63
+ plot = p,
64
+ width = 10,
65
+ height = 6,
66
+ dpi = 300
67
+ )
68
+ """)
69
+
70
+ return output_path if os.path.exists(output_path) else None
71
+
72
+ except Exception as e:
73
+ print(f"Error al generar histograma: {e}")
74
+ return None
75
+
76
+
77
+ def generar_barras_r(df: pd.DataFrame, columna: str, output_path: str) -> str:
78
+ """
79
+ Genera gr谩fico de barras para variables categ贸ricas
80
+ """
81
+ try:
82
+ with localconverter(ro.default_converter + pandas2ri.converter):
83
+ r_df = ro.conversion.py2rpy(df[[columna]])
84
+
85
+ ro.r(f"""
86
+ library(ggplot2)
87
+ library(dplyr)
88
+
89
+ top_data <- {r_df.r_repr()} %>%
90
+ group_by({columna}) %>%
91
+ summarise(count = n()) %>%
92
+ arrange(desc(count)) %>%
93
+ head(10)
94
+
95
+ p <- ggplot(top_data, aes(
96
+ x = reorder({columna}, count),
97
+ y = count,
98
+ fill = {columna}
99
+ )) +
100
+ geom_bar(stat = "identity") +
101
+ coord_flip() +
102
+ labs(
103
+ title = "Top 10 categor铆as en {columna}",
104
+ x = "",
105
+ y = "Conteo"
106
+ ) +
107
+ tema_personalizado +
108
+ scale_fill_brewer(palette = "Set2") +
109
+ theme(legend.position = "none")
110
+
111
+ ggsave(
112
+ filename = "{output_path}",
113
+ plot = p,
114
+ width = 10,
115
+ height = 6,
116
+ dpi = 300
117
+ )
118
+ """)
119
+
120
+ return output_path if os.path.exists(output_path) else None
121
+
122
+ except Exception as e:
123
+ print(f"Error al generar gr谩fico de barras: {e}")
124
+ return None
125
+
126
+
127
+ def generar_boxplot_r(df: pd.DataFrame, columna: str, grupo: str, output_path: str) -> str:
128
+ """
129
+ Genera boxplot comparativo por grupos
130
+ """
131
+ try:
132
+ with localconverter(ro.default_converter + pandas2ri.converter):
133
+ r_df = ro.conversion.py2rpy(df[[columna, grupo]])
134
+
135
+ ro.r(f"""
136
+ p <- ggplot({r_df.r_repr()}, aes(
137
+ x = {grupo},
138
+ y = {columna},
139
+ fill = {grupo}
140
+ )) +
141
+ geom_boxplot(
142
+ alpha = 0.7,
143
+ outlier.color = "#e34a33"
144
+ ) +
145
+ labs(
146
+ title = "Distribuci贸n de {columna} por {grupo}",
147
+ x = "{grupo}",
148
+ y = "{columna}"
149
+ ) +
150
+ tema_personalizado +
151
+ scale_fill_brewer(palette = "Set2") +
152
+ theme(axis.text.x = element_text(angle = 45, hjust = 1))
153
+
154
+ ggsave(
155
+ filename = "{output_path}",
156
+ plot = p,
157
+ width = 10,
158
+ height = 6,
159
+ dpi = 300
160
+ )
161
+ """)
162
+
163
+ return output_path if os.path.exists(output_path) else None
164
+
165
+ except Exception as e:
166
+ print(f"Error al generar boxplot: {e}")
167
+ return None
load_dataframe.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pandas as pd
3
+ from typing import Tuple, Optional, Dict, List
4
+ import datetime
5
+
6
+ def cargar_dataframe(file_path: str) -> Tuple[Optional[pd.DataFrame], Optional[str]]:
7
+ """
8
+ Carga un DataFrame desde m煤ltiples formatos con validaci贸n robusta
9
+ """
10
+ if not os.path.exists(file_path):
11
+ return None, f"Error: El archivo {file_path} no existe"
12
+
13
+ try:
14
+ ext = os.path.splitext(file_path)[1].lower()
15
+
16
+ if ext == '.csv':
17
+ df = pd.read_csv(file_path, encoding='utf-8', na_values=['', 'NA', 'NULL', 'NaN'])
18
+ elif ext in ('.xls', '.xlsx'):
19
+ df = pd.read_excel(file_path, na_values=['', 'NA', 'NULL', 'NaN'])
20
+ elif ext == '.parquet':
21
+ df = pd.read_parquet(file_path)
22
+ elif ext == '.json':
23
+ df = pd.read_json(file_path)
24
+ else:
25
+ return None, f"Error: Formato {ext} no soportado"
26
+
27
+ # Convertir strings a categor铆as para optimizaci贸n
28
+ for col in df.select_dtypes(include=['object']):
29
+ if len(df[col].unique()) / len(df[col]) < 0.5:
30
+ df[col] = df[col].astype('category')
31
+
32
+ return df, None
33
+
34
+ except Exception as e:
35
+ return None, f"Error al cargar {file_path}: {str(e)}"
36
+
37
+ def analizar_nulos_por_columna(df: pd.DataFrame) -> List[Dict]:
38
+ """
39
+ Analiza valores nulos por columna con m茅tricas detalladas
40
+
41
+ Returns:
42
+ Lista de diccionarios con:
43
+ - nombre_columna
44
+ - tipo_dato
45
+ - total_nulos
46
+ - porcentaje_nulos
47
+ - no_nulos
48
+ - porcentaje_no_nulos
49
+ - ejemplo_valores (3 primeros no nulos)
50
+ """
51
+ analisis = []
52
+ total_filas = len(df)
53
+
54
+ for columna in df.columns:
55
+ nulos = df[columna].isnull().sum()
56
+ no_nulos = total_filas - nulos
57
+ porcentaje_nulos = (nulos / total_filas) * 100 if total_filas > 0 else 0
58
+ valores_unicos = df[columna].nunique()
59
+
60
+ # Ejemplos de valores (excluyendo nulos)
61
+ ejemplos = []
62
+ if no_nulos > 0:
63
+ ejemplos = df[columna].dropna().head(3).tolist()
64
+ # Convertir a string si es necesario para serializaci贸n
65
+ ejemplos = [str(x) for x in ejemplos]
66
+
67
+ analisis.append({
68
+ 'nombre_columna': columna,
69
+ 'tipo_dato': str(df[columna].dtype),
70
+ 'total_nulos': nulos,
71
+ 'porcentaje_nulos': round(porcentaje_nulos, 2),
72
+ 'no_nulos': no_nulos,
73
+ 'porcentaje_no_nulos': round(100 - porcentaje_nulos, 2),
74
+ 'valores_unicos': valores_unicos,
75
+ 'ejemplo_valores': ejemplos
76
+ })
77
+
78
+ # Ordenar por porcentaje de nulos descendente
79
+ return sorted(analisis, key=lambda x: x['porcentaje_nulos'], reverse=True)
80
+
81
+ def get_file_metadata(file_path: str) -> Dict:
82
+ """
83
+ Obtiene metadatos detallados del archivo
84
+ """
85
+ stats = os.stat(file_path)
86
+ return {
87
+ 'nombre': os.path.basename(file_path),
88
+ 'ruta': os.path.abspath(file_path),
89
+ 'tama帽o_bytes': stats.st_size,
90
+ 'tama帽o_mb': round(stats.st_size / (1024 * 1024), 2),
91
+ 'fecha_modificacion': datetime.datetime.fromtimestamp(stats.st_mtime),
92
+ 'extension': os.path.splitext(file_path)[1].lower(),
93
+ 'encoding': 'utf-8' # Se puede detectar con chardet si es necesario
94
+ }
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ gradio
2
+ numpy
3
+ pandas
4
+ matplotlib
5
+ seaborn
6
+ rpy2
7
+ ydata-profiling