RM0303 commited on
Commit
4339f2c
·
verified ·
1 Parent(s): 3c9b49a

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +902 -0
app.py ADDED
@@ -0,0 +1,902 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """Untitled0.ipynb
3
+
4
+ Automatically generated by Colab.
5
+
6
+ Original file is located at
7
+ https://colab.research.google.com/drive/1lA4vvx9sbWFfjQHAmGs8ADgwhNOypfsM
8
+ """
9
+
10
+ import pandas as pd
11
+ import numpy as np
12
+ import matplotlib.pyplot as plt
13
+ import gradio as gr
14
+ import io
15
+ import base64
16
+ from datetime import datetime
17
+ from sklearn.ensemble import RandomForestClassifier
18
+ from sklearn.model_selection import train_test_split
19
+ from sklearn.metrics import classification_report, accuracy_score
20
+ import tempfile
21
+ import os
22
+
23
+ print("🚀 Iniciando CorrosionPredict en Hugging Face...")
24
+
25
+ # ==============================================================================
26
+ # BLOQUE 1: CARGA Y ENTRENAMIENTO DEL MODELO
27
+ # ==============================================================================
28
+
29
+ print("📂 Cargando y entrenando modelo de corrosión...")
30
+
31
+ try:
32
+ # Cargar datos
33
+ df = pd.read_excel("Acero_bajo_OFICIAL.xlsx")
34
+ print(f"✅ Datos cargados: {len(df)} registros")
35
+
36
+ # Limpiar datos
37
+ df['Humedad relativa (%)'] = df['Humedad relativa (%)'].astype(str).str.replace('%', '').astype(float)
38
+ df['Clasificacion ISO 9223'] = df['Clasificacion ISO 9223'].str.upper()
39
+
40
+ # Preparar variables
41
+ X = df[['Distancia al mar (km)', 'Altitud (msnm)', 'Humedad relativa (%)']]
42
+ y = df['Clasificacion ISO 9223']
43
+
44
+ # Añadir ruido controlado para robustez
45
+ np.random.seed(42)
46
+ X_noisy = X.copy()
47
+ X_noisy['Distancia al mar (km)'] += np.random.uniform(-2, 2, size=len(X))
48
+ X_noisy['Altitud (msnm)'] += np.random.uniform(-10, 10, size=len(X))
49
+ X_noisy['Humedad relativa (%)'] += np.random.uniform(-3, 3, size=len(X))
50
+
51
+ # Asegurar rangos razonables
52
+ X_noisy['Distancia al mar (km)'] = X_noisy['Distancia al mar (km)'].clip(lower=0, upper=25)
53
+ X_noisy['Altitud (msnm)'] = X_noisy['Altitud (msnm)'].clip(lower=0, upper=400)
54
+ X_noisy['Humedad relativa (%)'] = X_noisy['Humedad relativa (%)'].clip(lower=65, upper=95)
55
+
56
+ # Entrenar modelo final con todos los datos
57
+ modelo = RandomForestClassifier(n_estimators=100, random_state=42, class_weight='balanced')
58
+ modelo.fit(X_noisy, y)
59
+
60
+ print("✅ Modelo entrenado correctamente")
61
+
62
+ except Exception as e:
63
+ print(f"❌ Error cargando datos: {e}")
64
+ # Crear datos de ejemplo para demo
65
+ modelo = None
66
+
67
+ # ==============================================================================
68
+ # BLOQUE 2: CONFIGURACIÓN Y DATOS
69
+ # ==============================================================================
70
+
71
+ # Configuración profesional de matplotlib
72
+ plt.rcParams['font.size'] = 12
73
+ plt.rcParams['axes.grid'] = True
74
+ plt.rcParams['grid.alpha'] = 0.3
75
+
76
+ # Diccionario mejorado de recomendaciones
77
+ RECOMENDACIONES = {
78
+ "C1": {
79
+ "nivel": "MUY BAJA",
80
+ "color": "#2E8B57", # Verde
81
+ "ejemplos": "Oficinas climatizadas, escuelas, museos, bibliotecas, hospitales.",
82
+ "descripcion": "Ambientes interiores controlados con baja contaminación.",
83
+ "protecciones": [
84
+ {"proteccion": "Pintura anticorrosiva básica", "costo": "30-45", "vida_util": "5-8", "frecuencia": "Cada 5-10 años", "notas": "Protección adecuada para ambiente controlado"},
85
+ {"proteccion": "Recubrimiento ligero", "costo": "15-25", "vida_util": "3-5", "frecuencia": "Cada 3 años", "notas": "Opción económica para baja exposición"}
86
+ ]
87
+ },
88
+ "C2": {
89
+ "nivel": "BAJA",
90
+ "color": "#9ACD32", # Amarillo verdoso
91
+ "ejemplos": "Zonas rurales, áreas suburbanas, almacenamiento techado, interiores no climatizados.",
92
+ "descripcion": "Ambientes con exposición mínima a contaminantes corrosivos.",
93
+ "protecciones": [
94
+ {"proteccion": "Pintura anticorrosiva estándar", "costo": "35-50", "vida_util": "5-8", "frecuencia": "Cada 3-4 años", "notas": "Balance óptimo costo-beneficio"},
95
+ {"proteccion": "Galvanizado ligero", "costo": "60-80", "vida_util": "10-15", "frecuencia": "Inspección cada 5 años", "notas": "Mayor durabilidad con inversión moderada"}
96
+ ]
97
+ },
98
+ "C3": {
99
+ "nivel": "MODERADA",
100
+ "color": "#FFA500", # Naranja
101
+ "ejemplos": "Zonas urbanas, áreas costeras con baja salinidad, plantas de procesamiento de alimentos.",
102
+ "descripcion": "Ambientes urbanos y semi-industriales con exposición moderada.",
103
+ "protecciones": [
104
+ {"proteccion": "Galvanizado en caliente", "costo": "80-120", "vida_util": "15-20", "frecuencia": "Inspección cada 5 años", "notas": "Protección robusta y duradera"},
105
+ {"proteccion": "Pintura epóxica industrial", "costo": "50-70", "vida_util": "8-12", "frecuencia": "Cada 4-6 años", "notas": "Excelente resistencia química"}
106
+ ]
107
+ },
108
+ "C4": {
109
+ "nivel": "ALTA",
110
+ "color": "#FF4500", # Rojo naranja
111
+ "ejemplos": "Áreas industriales, zonas costeras sin rociado de sal, plantas químicas.",
112
+ "descripcion": "Ambientes industriales y costeros con alta contaminación.",
113
+ "protecciones": [
114
+ {"proteccion": "Sistema duplex (galvanizado + pintura)", "costo": "120-180", "vida_util": "20-25", "frecuencia": "Inspección cada 5-7 años", "notas": "Máxima protección sin acero inoxidable"},
115
+ {"proteccion": "Recubrimiento de zinc-aleación", "costo": "100-150", "vida_util": "15-20", "frecuencia": "Inspección cada 5 años", "notas": "Alta resistencia a la corrosión"}
116
+ ]
117
+ },
118
+ "C5": {
119
+ "nivel": "MUY ALTA",
120
+ "color": "#DC143C", # Rojo
121
+ "ejemplos": "Áreas costeras con rociado de sal, industrias pesadas, plataformas costeras.",
122
+ "descripcion": "Ambientes marinos e industriales extremadamente agresivos.",
123
+ "protecciones": [
124
+ {"proteccion": "Acero inoxidable 316", "costo": "300-500", "vida_util": "50+", "frecuencia": "Limpieza periódica", "notas": "Solución premium para ambientes severos"},
125
+ {"proteccion": "Sistema de protección catódica", "costo": "200-350", "vida_util": "25-30", "frecuencia": "Mantenimiento anual", "notas": "Ideal para estructuras fijas"}
126
+ ]
127
+ },
128
+ "CX": {
129
+ "nivel": "EXTREMA",
130
+ "color": "#8B008B", # Púrpura
131
+ "ejemplos": "Zonas tropicales con humedad permanente, industrias extremas, offshore.",
132
+ "descripcion": "Ambientes tropicales y offshore con exposición constante.",
133
+ "protecciones": [
134
+ {"proteccion": "Acero inoxidable superduplex", "costo": "400-600", "vida_util": "50+", "frecuencia": "Limpieza periódica", "notas": "Máxima resistencia para ambientes extremos"},
135
+ {"proteccion": "Recubrimientos especiales marinos", "costo": "250-400", "vida_util": "20-30", "frecuencia": "Inspección cada 3-5 años", "notas": "Tecnología avanzada de protección"}
136
+ ]
137
+ }
138
+ }
139
+
140
+ # ==============================================================================
141
+ # BLOQUE 3: FUNCIONES PRINCIPALES
142
+ # ==============================================================================
143
+
144
+ def generar_graficos(clase_predicha, estacion):
145
+ """Genera gráficos profesionales para el reporte"""
146
+ graficos_base64 = []
147
+
148
+ try:
149
+ # 1. Gráfico de Barras: Vida Útil
150
+ fig, ax = plt.subplots(figsize=(10, 6))
151
+ protecciones = RECOMENDACIONES[clase_predicha]["protecciones"]
152
+ sistemas = [p['proteccion'] for p in protecciones]
153
+ vida_util = [float(p['vida_util'].split('-')[1].replace('+', '')) if '-' in p['vida_util'] else float(p['vida_util'].replace('+', '')) for p in protecciones]
154
+
155
+ bars = ax.bar(sistemas, vida_util, color=[RECOMENDACIONES[clase_predicha]["color"], '#FFD700'])
156
+ ax.set_title('Comparación de Vida Útil entre Sistemas de Protección', fontsize=14, fontweight='bold')
157
+ ax.set_ylabel('Vida Útil (años)', fontsize=12)
158
+ ax.set_xlabel('Sistemas de Protección', fontsize=12)
159
+
160
+ # Añadir valores en las barras
161
+ for bar in bars:
162
+ height = bar.get_height()
163
+ ax.text(bar.get_x() + bar.get_width()/2., height + 0.5,
164
+ f'{height} años', ha='center', va='bottom', fontweight='bold')
165
+
166
+ plt.xticks(rotation=15)
167
+ plt.tight_layout()
168
+
169
+ # Convertir a base64
170
+ buf = io.BytesIO()
171
+ plt.savefig(buf, format='png', dpi=120, bbox_inches='tight')
172
+ buf.seek(0)
173
+ graficos_base64.append(base64.b64encode(buf.getvalue()).decode('utf-8'))
174
+ plt.close()
175
+
176
+ # 2. Gráfico Circular: Distribución de Costos
177
+ fig, ax = plt.subplots(figsize=(8, 8))
178
+ costos = [float(p['costo'].split('-')[0]) for p in protecciones]
179
+ colors = [RECOMENDACIONES[clase_predicha]["color"], '#FFD700']
180
+
181
+ wedges, texts, autotexts = ax.pie(costos, labels=sistemas, autopct='%1.1f%%',
182
+ colors=colors, startangle=90)
183
+
184
+ for autotext in autotexts:
185
+ autotext.set_color('white')
186
+ autotext.set_fontweight('bold')
187
+
188
+ ax.set_title('Distribución de Costos Iniciales\n(S/ m²)', fontsize=14, fontweight='bold')
189
+ plt.tight_layout()
190
+
191
+ buf = io.BytesIO()
192
+ plt.savefig(buf, format='png', dpi=120, bbox_inches='tight')
193
+ buf.seek(0)
194
+ graficos_base64.append(base64.b64encode(buf.getvalue()).decode('utf-8'))
195
+ plt.close()
196
+
197
+ # 3. Gráfico de Líneas: Degradación
198
+ fig, ax = plt.subplots(figsize=(10, 6))
199
+ tiempo = np.arange(0, 26, 5) # 0 a 25 años
200
+
201
+ for i, proteccion in enumerate(protecciones):
202
+ vida = float(proteccion['vida_util'].split('-')[1].replace('+', '')) if '-' in proteccion['vida_util'] else float(proteccion['vida_util'].replace('+', ''))
203
+ eficacia = [100 - (100/vida) * t for t in tiempo]
204
+ eficacia = [max(0, e) for e in eficacia] # No menor que 0
205
+
206
+ ax.plot(tiempo, eficacia, marker='o', linewidth=2,
207
+ label=proteccion['proteccion'], color=colors[i])
208
+
209
+ ax.set_title('Degradación de Protección en el Tiempo', fontsize=14, fontweight='bold')
210
+ ax.set_xlabel('Años', fontsize=12)
211
+ ax.set_ylabel('Eficacia de Protección (%)', fontsize=12)
212
+ ax.legend()
213
+ ax.grid(True, alpha=0.3)
214
+ plt.tight_layout()
215
+
216
+ buf = io.BytesIO()
217
+ plt.savefig(buf, format='png', dpi=120, bbox_inches='tight')
218
+ buf.seek(0)
219
+ graficos_base64.append(base64.b64encode(buf.getvalue()).decode('utf-8'))
220
+ plt.close()
221
+
222
+ # 4. NUEVO GRÁFICO: Real vs Predicho
223
+ grafico_comparativo = generar_grafico_real_vs_predicho()
224
+ if grafico_comparativo:
225
+ graficos_base64.append(grafico_comparativo)
226
+
227
+ except Exception as e:
228
+ print(f"Error generando gráficos: {e}")
229
+
230
+ return graficos_base64
231
+
232
+ def generar_grafico_real_vs_predicho():
233
+ """Genera gráfico comparativo entre valores reales y predichos"""
234
+ try:
235
+ print("🔄 Generando gráfico real vs predicho...")
236
+
237
+ # Obtener datos reales del Excel
238
+ df_real = pd.read_excel("Acero_bajo_OFICIAL.xlsx")
239
+ print(f"✅ Datos cargados: {len(df_real)} registros")
240
+
241
+ # Preparar datos para predicción
242
+ X_real = df_real[['Distancia al mar (km)', 'Altitud (msnm)', 'Humedad relativa (%)']]
243
+ y_real = df_real['Clasificacion ISO 9223']
244
+
245
+ # Generar predicciones para todos los datos reales
246
+ y_pred = modelo.predict(X_real)
247
+
248
+ # Crear gráfico comparativo
249
+ fig, ax = plt.subplots(figsize=(12, 8))
250
+
251
+ # Convertir clases categóricas a numéricas para gráfico
252
+ clases_ordenadas = ['C1', 'C2', 'C3', 'C4', 'C5', 'CX']
253
+ y_real_num = [clases_ordenadas.index(clase) for clase in y_real]
254
+ y_pred_num = [clases_ordenadas.index(clase) for clase in y_pred]
255
+
256
+ # Gráfico de dispersión
257
+ scatter = ax.scatter(y_real_num, y_pred_num, alpha=0.6, c=y_real_num,
258
+ cmap='viridis', s=60)
259
+
260
+ # Línea de perfecta predicción
261
+ min_val = min(min(y_real_num), min(y_pred_num))
262
+ max_val = max(max(y_real_num), max(y_pred_num))
263
+ ax.plot([min_val, max_val], [min_val, max_val], 'r--', alpha=0.8,
264
+ label='Predicción Perfecta')
265
+
266
+ ax.set_xlabel('Valores Reales', fontsize=12, fontweight='bold')
267
+ ax.set_ylabel('Valores Predichos', fontsize=12, fontweight='bold')
268
+ ax.set_title('Comparación: Valores Reales vs Predichos\nModelo Random Forest',
269
+ fontsize=14, fontweight='bold')
270
+
271
+ # Configurar ejes con etiquetas de clases
272
+ ax.set_xticks(range(len(clases_ordenadas)))
273
+ ax.set_yticks(range(len(clases_ordenadas)))
274
+ ax.set_xticklabels(clases_ordenadas)
275
+ ax.set_yticklabels(clases_ordenadas)
276
+
277
+ # Añadir grid y leyenda
278
+ ax.grid(True, alpha=0.3)
279
+ ax.legend()
280
+
281
+ # Añadir barra de color
282
+ cbar = plt.colorbar(scatter)
283
+ cbar.set_label('Nivel de Corrosión', rotation=270, labelpad=15)
284
+
285
+ # Calcular y mostrar precisión
286
+ accuracy = accuracy_score(y_real, y_pred)
287
+ ax.text(0.05, 0.95, f'Precisión del Modelo: {accuracy:.2%}',
288
+ transform=ax.transAxes, fontsize=12,
289
+ bbox=dict(boxstyle="round,pad=0.3", facecolor="white", alpha=0.8))
290
+
291
+ plt.tight_layout()
292
+
293
+ # Convertir a base64 para HTML
294
+ buf = io.BytesIO()
295
+ plt.savefig(buf, format='png', dpi=120, bbox_inches='tight')
296
+ buf.seek(0)
297
+ img_base64 = base64.b64encode(buf.getvalue()).decode('utf-8')
298
+ plt.close()
299
+
300
+ print("✅ Gráfico real vs predicho generado exitosamente")
301
+ return img_base64
302
+
303
+ except Exception as e:
304
+ print(f"❌ Error generando gráfico real vs predicho: {e}")
305
+ return None
306
+
307
+ def predecir_corrosion(estacion, distancia, altitud, humedad, nombre_ing, proyecto, ubicacion):
308
+ """Función principal para predecir corrosión"""
309
+ try:
310
+ # Validaciones
311
+ if not all([estacion, nombre_ing, proyecto, ubicacion]):
312
+ return "❌ Complete todos los campos obligatorios", "", "", ""
313
+
314
+ distancia = float(distancia)
315
+ altitud = float(altitud)
316
+ humedad = float(humedad)
317
+
318
+ # Validar rangos
319
+ if not (0 <= distancia <= 25):
320
+ return "❌ Distancia al mar debe estar entre 0-25 km", "", "", ""
321
+ if not (0 <= altitud <= 400):
322
+ return "❌ Altitud debe estar entre 0-400 msnm", "", "", ""
323
+ if not (65 <= humedad <= 95):
324
+ return "❌ Humedad relativa debe estar entre 65-95%", "", "", ""
325
+
326
+ if modelo is None:
327
+ return "❌ Modelo no disponible. Verifique los datos de entrenamiento.", "", "", ""
328
+
329
+ # Realizar predicción
330
+ nuevo_dato = pd.DataFrame({
331
+ 'Distancia al mar (km)': [distancia],
332
+ 'Altitud (msnm)': [altitud],
333
+ 'Humedad relativa (%)': [humedad]
334
+ })
335
+
336
+ prediccion = modelo.predict(nuevo_dato)[0].upper()
337
+ probabilidades = modelo.predict_proba(nuevo_dato)[0]
338
+ clases = modelo.classes_
339
+
340
+ # Generar gráficos
341
+ graficos_base64 = generar_graficos(prediccion, estacion)
342
+
343
+ # Generar reporte COMPLETO
344
+ reporte_completo_html = generar_reporte_completo(
345
+ estacion, distancia, altitud, humedad, prediccion,
346
+ probabilidades, clases, graficos_base64,
347
+ nombre_ing, proyecto, ubicacion
348
+ )
349
+
350
+ # Generar reporte SOLO GRÁFICOS
351
+ reporte_graficos_html = generar_reporte_solo_graficos(
352
+ estacion, distancia, altitud, humedad, prediccion,
353
+ probabilidades, clases, graficos_base64,
354
+ nombre_ing, proyecto, ubicacion
355
+ )
356
+
357
+ # Resultados para mostrar en interfaz
358
+ resultados_html = generar_resultados_html(
359
+ estacion, distancia, altitud, humedad, prediccion,
360
+ probabilidades, clases
361
+ )
362
+
363
+ graficas_html = generar_graficas_html(graficos_base64)
364
+
365
+ return resultados_html, graficas_html, reporte_completo_html, reporte_graficos_html
366
+
367
+ except Exception as e:
368
+ return f"❌ Error en la predicción: {str(e)}", "", "", ""
369
+
370
+ # ==============================================================================
371
+ # BLOQUE 4: GENERACIÓN DE HTML Y REPORTES
372
+ # ==============================================================================
373
+
374
+ def generar_resultados_html(estacion, distancia, altitud, humedad, prediccion, probabilidades, clases):
375
+ """Genera HTML para mostrar resultados en la interfaz"""
376
+
377
+ info_clase = RECOMENDACIONES.get(prediccion, {})
378
+
379
+ # Tabla de probabilidades
380
+ prob_table = ""
381
+ for clase, prob in zip(clases, probabilidades):
382
+ prob_table += f"<tr><td>{clase}</td><td style='text-align: center;'>{prob:.2%}</td></tr>"
383
+
384
+ return f"""
385
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
386
+ color: white; padding: 20px; border-radius: 10px; margin-bottom: 20px;">
387
+ <h2 style="margin: 0; text-align: center;">🏭 CorrosionPredict Pro</h2>
388
+ <p style="text-align: center; margin: 5px 0 0 0; opacity: 0.9;">
389
+ {estacion} | {datetime.now().strftime('%d/%m/%Y %H:%M')}
390
+ </p>
391
+ </div>
392
+
393
+ <div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 15px 0;">
394
+ <h3 style="color: #2c3e50; margin-top: 0;">📊 Resultado de Predicción</h3>
395
+
396
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px;">
397
+ <div style="background: white; padding: 15px; border-radius: 8px; text-align: center; border-left: 4px solid {info_clase.get('color', '#666')};">
398
+ <div style="font-weight: bold; color: #2c3e50;">Clasificación ISO 9223</div>
399
+ <div style="font-size: 1.8em; font-weight: bold; color: {info_clase.get('color', '#666')};">{prediccion}</div>
400
+ <div style="font-size: 0.9em; color: #666;">{info_clase.get('nivel', 'N/A')}</div>
401
+ </div>
402
+ </div>
403
+
404
+ <div style="background: white; padding: 15px; border-radius: 8px; margin-bottom: 15px;">
405
+ <h4 style="margin-top: 0; color: #2c3e50;">📈 Probabilidades por Clase</h4>
406
+ <table style="width: 100%; border-collapse: collapse;">
407
+ <tr style="background: #f8f9fa;">
408
+ <th style="padding: 10px; text-align: left; border-bottom: 2px solid #dee2e6;">Clase</th>
409
+ <th style="padding: 10px; text-align: center; border-bottom: 2px solid #dee2e6;">Probabilidad</th>
410
+ </tr>
411
+ {prob_table}
412
+ </table>
413
+ </div>
414
+
415
+ <div style="background: white; padding: 15px; border-radius: 8px;">
416
+ <h4 style="margin-top: 0; color: #2c3e50;">📍 Datos de Entrada</h4>
417
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px;">
418
+ <div style="text-align: center;">
419
+ <div style="font-size: 0.9em; color: #666;">Distancia al mar</div>
420
+ <div style="font-size: 1.2em; font-weight: bold;">{distancia} km</div>
421
+ </div>
422
+ <div style="text-align: center;">
423
+ <div style="font-size: 0.9em; color: #666;">Altitud</div>
424
+ <div style="font-size: 1.2em; font-weight: bold;">{altitud} msnm</div>
425
+ </div>
426
+ <div style="text-align: center;">
427
+ <div style="font-size: 0.9em; color: #666;">Humedad</div>
428
+ <div style="font-size: 1.2em; font-weight: bold;">{humedad}%</div>
429
+ </div>
430
+ </div>
431
+ </div>
432
+ </div>
433
+ """
434
+
435
+ def generar_graficas_html(graficos_base64):
436
+ """Genera HTML para mostrar gráficas"""
437
+ if not graficos_base64:
438
+ return "<p>No se pudieron generar las gráficas</p>"
439
+
440
+ graficas_html = "<div style='margin-top: 20px;'>"
441
+ titulos = [
442
+ "Comparación de Vida Útil entre Sistemas de Protección",
443
+ "Distribución de Costos Iniciales",
444
+ "Degradación de Protección en el Tiempo",
445
+ "Validación del Modelo: Valores Reales vs Predichos"
446
+ ]
447
+
448
+ for i, img_base64 in enumerate(graficos_base64):
449
+ graficas_html += f"""
450
+ <div style="background: white; padding: 20px; border-radius: 8px; margin: 15px 0;
451
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);">
452
+ <h4 style="margin-top: 0; color: #2c3e50;">{titulos[i] if i < len(titulos) else 'Gráfico'}</h4>
453
+ <img src="data:image/png;base64,{img_base64}"
454
+ style="max-width: 100%; height: auto; border-radius: 5px; border: 1px solid #dee2e6;">
455
+ </div>
456
+ """
457
+ graficas_html += "</div>"
458
+ return graficas_html
459
+
460
+ def generar_reporte_solo_graficos(estacion, distancia, altitud, humedad, prediccion,
461
+ probabilidades, clases, graficos_base64, nombre_ing, proyecto, ubicacion):
462
+ """Genera reporte HTML SOLO con el gráfico Real vs Predicho"""
463
+
464
+ # Tomar el cuarto gráfico (Real vs Predicho)
465
+ grafico_real_vs_predicho = graficos_base64[3] if len(graficos_base64) >= 4 else None
466
+
467
+ if not grafico_real_vs_predicho:
468
+ return "<p>No se pudo generar el gráfico de validación</p>"
469
+
470
+ html_content = f"""<!DOCTYPE html>
471
+ <html lang="es">
472
+ <head>
473
+ <meta charset="UTF-8">
474
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
475
+ <title>Validación del Modelo - {estacion}</title>
476
+ <style>
477
+ body {{
478
+ font-family: 'Arial', sans-serif;
479
+ line-height: 1.6;
480
+ color: #333;
481
+ max-width: 1000px;
482
+ margin: 0 auto;
483
+ padding: 20px;
484
+ background: #f8f9fa;
485
+ }}
486
+ .header {{
487
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
488
+ color: white;
489
+ padding: 25px;
490
+ border-radius: 15px;
491
+ text-align: center;
492
+ margin-bottom: 25px;
493
+ }}
494
+ .section {{
495
+ background: white;
496
+ padding: 25px;
497
+ margin: 20px 0;
498
+ border-radius: 10px;
499
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
500
+ }}
501
+ </style>
502
+ </head>
503
+ <body>
504
+ <div class="header">
505
+ <h1>📊 Validación del Modelo Predictivo</h1>
506
+ <p>Análisis Real vs Predicho - CorrosionPredict Pro</p>
507
+ <p>{estacion} | {datetime.now().strftime('%d/%m/%Y %H:%M')}</p>
508
+ </div>
509
+
510
+ <div class="section">
511
+ <h2>📈 Gráfico de Validación: Real vs Predicho</h2>
512
+ <img src="data:image/png;base64,{grafico_real_vs_predicho}"
513
+ style="max-width: 90%; height: auto; border: 1px solid #ddd; border-radius: 8px;">
514
+ </div>
515
+
516
+ <div class="section" style="text-align: center; background: #f8f9fa;">
517
+ <h3>CorrosionPredict Pro - Módulo de Validación</h3>
518
+ <p>Reporte de validación generado el {datetime.now().strftime('%d/%m/%Y')}</p>
519
+ </div>
520
+ </body>
521
+ </html>"""
522
+
523
+ return html_content
524
+
525
+ def generar_reporte_completo(estacion, distancia, altitud, humedad, prediccion,
526
+ probabilidades, clases, graficos_base64, nombre_ing, proyecto, ubicacion):
527
+ """Genera reporte HTML completo para descargar"""
528
+
529
+ info_clase = RECOMENDACIONES.get(prediccion, {})
530
+
531
+ # Tabla de protecciones
532
+ protecciones_html = ""
533
+ for proteccion in info_clase.get("protecciones", []):
534
+ protecciones_html += f"""
535
+ <tr>
536
+ <td>{proteccion['proteccion']}</td>
537
+ <td>S/ {proteccion['costo']}</td>
538
+ <td>{proteccion['vida_util']} años</td>
539
+ <td>{proteccion['frecuencia']}</td>
540
+ <td>{proteccion['notas']}</td>
541
+ </tr>
542
+ """
543
+
544
+ # Tabla de probabilidades
545
+ prob_html = ""
546
+ for clase, prob in zip(clases, probabilidades):
547
+ prob_html += f"<tr><td>{clase}</td><td>{prob:.2%}</td></tr>"
548
+
549
+ # Gráficos en base64
550
+ graficos_html = ""
551
+ titulos_graficos = [
552
+ "Comparación de Vida Útil entre Sistemas de Protección",
553
+ "Distribución de Costos Iniciales",
554
+ "Degradación de Protección en el Tiempo",
555
+ "Validación del Modelo: Valores Reales vs Predichos"
556
+ ]
557
+
558
+ for i, img_base64 in enumerate(graficos_base64):
559
+ graficos_html += f"""
560
+ <div style="page-break-inside: avoid; margin: 20px 0;">
561
+ <h3>{titulos_graficos[i]}</h3>
562
+ <img src="data:image/png;base64,{img_base64}"
563
+ style="max-width: 100%; height: auto; border: 1px solid #ddd; border-radius: 5px;">
564
+ </div>
565
+ """
566
+
567
+ html_content = f"""<!DOCTYPE html>
568
+ <html lang="es">
569
+ <head>
570
+ <meta charset="UTF-8">
571
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
572
+ <title>Reporte de Corrosión - {estacion}</title>
573
+ <style>
574
+ body {{
575
+ font-family: 'Arial', sans-serif;
576
+ line-height: 1.6;
577
+ color: #333;
578
+ max-width: 1200px;
579
+ margin: 0 auto;
580
+ padding: 20px;
581
+ background: #f8f9fa;
582
+ }}
583
+ .header {{
584
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
585
+ color: white;
586
+ padding: 30px;
587
+ border-radius: 15px;
588
+ text-align: center;
589
+ margin-bottom: 30px;
590
+ }}
591
+ .section {{
592
+ background: white;
593
+ padding: 25px;
594
+ margin: 20px 0;
595
+ border-radius: 10px;
596
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
597
+ }}
598
+ .clasificacion {{
599
+ background: {info_clase.get('color', '#666')};
600
+ color: white;
601
+ padding: 20px;
602
+ border-radius: 10px;
603
+ text-align: center;
604
+ margin: 15px 0;
605
+ }}
606
+ table {{
607
+ width: 100%;
608
+ border-collapse: collapse;
609
+ margin: 15px 0;
610
+ }}
611
+ th, td {{
612
+ border: 1px solid #ddd;
613
+ padding: 12px;
614
+ text-align: left;
615
+ }}
616
+ th {{
617
+ background-color: #f8f9fa;
618
+ font-weight: bold;
619
+ }}
620
+ .proteccion-card {{
621
+ background: #f8f9fa;
622
+ padding: 15px;
623
+ margin: 10px 0;
624
+ border-radius: 8px;
625
+ border-left: 4px solid {info_clase.get('color', '#666')};
626
+ }}
627
+ @media print {{
628
+ body {{ background: white; }}
629
+ .section {{ box-shadow: none; border: 1px solid #ddd; }}
630
+ .header {{ background: #667eea; }}
631
+ }}
632
+ </style>
633
+ </head>
634
+ <body>
635
+ <div class="header">
636
+ <h1>🏭 CorrosionPredict Pro</h1>
637
+ <p>Sistema de Predicción de Corrosión ISO 9223</p>
638
+ <p>{estacion} | {datetime.now().strftime('%d/%m/%Y %H:%M')}</p>
639
+ </div>
640
+
641
+ <div class="section">
642
+ <h2>📋 Información del Proyecto</h2>
643
+ <table>
644
+ <tr><th>Proyecto:</th><td>{proyecto}</td></tr>
645
+ <tr><th>Ubicación:</th><td>{ubicacion}</td></tr>
646
+ <tr><th>Ingeniero:</th><td>{nombre_ing}</td></tr>
647
+ <tr><th>Estación/Distrito:</th><td>{estacion}</td></tr>
648
+ <tr><th>Fecha:</th><td>{datetime.now().strftime('%d/%m/%Y %H:%M')}</td></tr>
649
+ </table>
650
+ </div>
651
+
652
+ <div class="section">
653
+ <h2>📊 Datos de Entrada</h2>
654
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
655
+ <div style="text-align: center; padding: 15px; background: #f8f9fa; border-radius: 8px;">
656
+ <h3>🌊 Distancia al mar</h3>
657
+ <p style="font-size: 1.5em; font-weight: bold; margin: 0;">{distancia} km</p>
658
+ </div>
659
+ <div style="text-align: center; padding: 15px; background: #f8f9fa; border-radius: 8px;">
660
+ <h3>⛰️ Altitud</h3>
661
+ <p style="font-size: 1.5em; font-weight: bold; margin: 0;">{altitud} msnm</p>
662
+ </div>
663
+ <div style="text-align: center; padding: 15px; background: #f8f9fa; border-radius: 8px;">
664
+ <h3>💧 Humedad relativa</h3>
665
+ <p style="font-size: 1.5em; font-weight: bold; margin: 0;">{humedad}%</p>
666
+ </div>
667
+ </div>
668
+ </div>
669
+
670
+ <div class="section">
671
+ <h2>🎯 Resultado de Predicción</h2>
672
+ <div class="clasificacion">
673
+ <h3 style="margin: 0; font-size: 2em;">CLASE {prediccion}</h3>
674
+ <p style="margin: 10px 0 0 0; font-size: 1.2em;">Corrosión {info_clase.get('nivel', 'N/A')}</p>
675
+ </div>
676
+
677
+ <h3>Probabilidades por Clase:</h3>
678
+ <table>
679
+ <tr><th>Clase ISO 9223</th><th>Probabilidad</th></tr>
680
+ {prob_html}
681
+ </table>
682
+ </div>
683
+
684
+ <div class="section">
685
+ <h2>🛡️ Recomendaciones de Protección</h2>
686
+ <p><strong>Descripción del ambiente:</strong> {info_clase.get('descripcion', 'N/A')}</p>
687
+ <p><strong>Ejemplos típicos:</strong> {info_clase.get('ejemplos', 'N/A')}</p>
688
+
689
+ <h3>Sistemas de Protección Recomendados:</h3>
690
+ <table>
691
+ <tr>
692
+ <th>Sistema de Protección</th>
693
+ <th>Costo (S/ m²)</th>
694
+ <th>Vida Útil</th>
695
+ <th>Mantenimiento</th>
696
+ <th>Notas</th>
697
+ </tr>
698
+ {protecciones_html}
699
+ </table>
700
+ </div>
701
+
702
+ <div class="section">
703
+ <h2>📈 Análisis Gráfico</h2>
704
+ {graficos_html}
705
+ </div>
706
+
707
+ <div class="section" style="text-align: center; background: #f8f9fa;">
708
+ <h3>CorrosionPredict Pro</h3>
709
+ <p>Sistema de predicción de corrosión basado en Machine Learning</p>
710
+ <p>Reporte generado automáticamente el {datetime.now().strftime('%d/%m/%Y')}</p>
711
+ </div>
712
+ </body>
713
+ </html>"""
714
+
715
+ return html_content
716
+
717
+ # ==============================================================================
718
+ # BLOQUE 5: INTERFAZ GRADIO
719
+ # ==============================================================================
720
+
721
+ with gr.Blocks(theme=gr.themes.Soft(), title="CorrosionPredict Pro") as demo:
722
+
723
+ gr.Markdown("""
724
+ # 🏭 CorrosionPredict Pro
725
+ ### Sistema de Predicción de Corrosión ISO 9223 con Machine Learning
726
+
727
+ Predice la clasificación de corrosividad según ISO 9223 para acero al carbono
728
+ basado en condiciones ambientales.
729
+ """)
730
+
731
+ with gr.Row():
732
+ with gr.Column(scale=1):
733
+ gr.Markdown("### ⚙️ Parámetros de Entrada")
734
+
735
+ estacion = gr.Textbox(
736
+ label="🏢 Estación/Distrito",
737
+ placeholder="Ej: Miraflores, Callao, Ate",
738
+ info="Nombre de la ubicación de estudio"
739
+ )
740
+
741
+ with gr.Row():
742
+ distancia = gr.Number(
743
+ label="🌊 Distancia al mar (km)",
744
+ value=5.0,
745
+ minimum=0,
746
+ maximum=25,
747
+ info="0-25 km"
748
+ )
749
+
750
+ altitud = gr.Number(
751
+ label="⛰️ Altitud (msnm)",
752
+ value=100,
753
+ minimum=0,
754
+ maximum=400,
755
+ info="0-400 msnm"
756
+ )
757
+
758
+ humedad = gr.Slider(
759
+ label="💧 Humedad relativa (%)",
760
+ value=80,
761
+ minimum=65,
762
+ maximum=95,
763
+ step=1,
764
+ info="65-95%"
765
+ )
766
+
767
+ nombre_ing = gr.Textbox(
768
+ label="👤 Ingeniero Responsable",
769
+ value="Ing. Ambiental",
770
+ placeholder="Nombre del profesional"
771
+ )
772
+
773
+ proyecto = gr.Textbox(
774
+ label="📋 Nombre del Proyecto",
775
+ value="Estudio de Corrosión",
776
+ placeholder="Nombre del proyecto"
777
+ )
778
+
779
+ ubicacion = gr.Textbox(
780
+ label="📍 Ubicación",
781
+ value="Lima Metropolitana",
782
+ placeholder="Región/Departamento"
783
+ )
784
+
785
+ btn_predict = gr.Button(
786
+ "🚀 Ejecutar Predicción",
787
+ size="lg",
788
+ variant="primary"
789
+ )
790
+
791
+ with gr.Column(scale=2):
792
+ gr.Markdown("### 📊 Resultados del Análisis")
793
+ resultados = gr.HTML(label="Predicciones")
794
+ graficas = gr.HTML(label="Gráficas del Análisis")
795
+
796
+ gr.Markdown("### 💾 Descargar Reportes")
797
+ descarga_html = gr.HTML("""
798
+ <div style="background: #d4edda; color: #155724; padding: 15px; border-radius: 8px;
799
+ border: 1px solid #c3e6cb; text-align: center;">
800
+ <p style="margin: 0;">📄 Ejecuta una predicción para generar los reportes</p>
801
+ </div>
802
+ """)
803
+
804
+ with gr.Row():
805
+ descarga_btn_completo = gr.Button(
806
+ "📥 Reporte Completo",
807
+ size="lg",
808
+ variant="primary",
809
+ visible=False
810
+ )
811
+
812
+ descarga_btn_graficos = gr.Button(
813
+ "📊 Solo Validación",
814
+ size="lg",
815
+ variant="secondary",
816
+ visible=False
817
+ )
818
+
819
+ archivo_descarga_completo = gr.File(
820
+ label="Reporte Completo",
821
+ visible=False
822
+ )
823
+
824
+ archivo_descarga_graficos = gr.File(
825
+ label="Reporte de Validación",
826
+ visible=False
827
+ )
828
+
829
+ # Ejemplos rápidos
830
+ gr.Markdown("### 🚀 Ejemplos Rápidos")
831
+ with gr.Row():
832
+ gr.Examples(
833
+ examples=[
834
+ ["Miraflores", 2.5, 80, 85, "Ing. Ejemplo", "Edificio Costero", "Lima"],
835
+ ["Ate", 15.0, 350, 75, "Ing. Ejemplo", "Planta Industrial", "Lima"],
836
+ ["Callao", 0.5, 5, 90, "Ing. Ejemplo", "Puerto Marítimo", "Callao"]
837
+ ],
838
+ inputs=[estacion, distancia, altitud, humedad, nombre_ing, proyecto, ubicacion]
839
+ )
840
+
841
+ # Estado para almacenar reportes
842
+ reporte_actual_completo = gr.State("")
843
+ reporte_actual_graficos = gr.State("")
844
+
845
+ # Funciones de conexión
846
+ def procesar_prediccion(estacion, distancia, altitud, humedad, nombre_ing, proyecto, ubicacion):
847
+ resultados_html, graficas_html, reporte_completo, reporte_graficos = predecir_corrosion(
848
+ estacion, distancia, altitud, humedad, nombre_ing, proyecto, ubicacion
849
+ )
850
+
851
+ mostrar_descarga = reporte_completo != ""
852
+
853
+ return resultados_html, graficas_html, gr.Button(visible=mostrar_descarga), gr.Button(visible=mostrar_descarga), reporte_completo, reporte_graficos
854
+
855
+ def generar_descarga_completo(reporte_html):
856
+ if reporte_html:
857
+ filename = f"reporte_completo_{datetime.now().strftime('%Y%m%d_%H%M')}.html"
858
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.html', mode='w', encoding='utf-8')
859
+ temp_file.write(reporte_html)
860
+ temp_file.close()
861
+ return gr.File(value=temp_file.name, label=filename, visible=True)
862
+ return gr.File(visible=False)
863
+
864
+ def generar_descarga_graficos(reporte_html):
865
+ if reporte_html:
866
+ filename = f"validacion_modelo_{datetime.now().strftime('%Y%m%d_%H%M')}.html"
867
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.html', mode='w', encoding='utf-8')
868
+ temp_file.write(reporte_html)
869
+ temp_file.close()
870
+ return gr.File(value=temp_file.name, label=filename, visible=True)
871
+ return gr.File(visible=False)
872
+
873
+ # Conectar componentes
874
+ btn_predict.click(
875
+ fn=procesar_prediccion,
876
+ inputs=[estacion, distancia, altitud, humedad, nombre_ing, proyecto, ubicacion],
877
+ outputs=[resultados, graficas, descarga_btn_completo, descarga_btn_graficos, reporte_actual_completo, reporte_actual_graficos]
878
+ )
879
+
880
+ descarga_btn_completo.click(
881
+ fn=generar_descarga_completo,
882
+ inputs=[reporte_actual_completo],
883
+ outputs=[archivo_descarga_completo]
884
+ )
885
+
886
+ descarga_btn_graficos.click(
887
+ fn=generar_descarga_graficos,
888
+ inputs=[reporte_actual_graficos],
889
+ outputs=[archivo_descarga_graficos]
890
+ )
891
+
892
+ # ==============================================================================
893
+ # EJECUCIÓN
894
+ # ==============================================================================
895
+
896
+ if __name__ == "__main__":
897
+ demo.launch(
898
+ debug=False,
899
+ show_error=True,
900
+ server_name="0.0.0.0",
901
+ server_port=7860
902
+ )