perellorets commited on
Commit
4d0651e
·
verified ·
1 Parent(s): 25f0e56

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +571 -0
app.py ADDED
@@ -0,0 +1,571 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ VUTIA - Sistema de Análisis de Viviendas de Uso Turístico
3
+ Ayuntamiento de Dénia - Plataforma de Análisis Institucional
4
+ """
5
+
6
+ import streamlit as st
7
+ import pandas as pd
8
+ import plotly.express as px
9
+ import plotly.graph_objects as go
10
+ from plotly.subplots import make_subplots
11
+ import numpy as np
12
+ from datetime import datetime
13
+ from io import BytesIO
14
+ import os
15
+
16
+ # Configuración de la página
17
+ st.set_page_config(
18
+ page_title="VUTIA - Sistema de Análisis VUT",
19
+ page_icon="📊",
20
+ layout="wide",
21
+ initial_sidebar_state="expanded"
22
+ )
23
+
24
+ # CSS Profesional Institucional (copiado del original)
25
+ st.markdown("""
26
+ <style>
27
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
28
+
29
+ /* VARIABLES */
30
+ :root {
31
+ --bg-primary: #0a0e27;
32
+ --bg-secondary: #141832;
33
+ --bg-tertiary: #1a1f3a;
34
+ --bg-card: rgba(20, 24, 50, 0.6);
35
+ --border-color: rgba(99, 102, 241, 0.2);
36
+ --border-subtle: rgba(99, 102, 241, 0.1);
37
+ --text-primary: rgba(255, 255, 255, 0.95);
38
+ --text-secondary: rgba(255, 255, 255, 0.7);
39
+ --text-tertiary: rgba(255, 255, 255, 0.5);
40
+ --accent-primary: #006AA7;
41
+ --accent-hover: #017CB5;
42
+ --accent-light: rgba(0, 106, 167, 0.2);
43
+ --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3);
44
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
45
+ --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5);
46
+ }
47
+
48
+ /* RESET BASE */
49
+ .stApp {
50
+ background: linear-gradient(135deg, #0a0e27 0%, #141832 100%);
51
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
52
+ color: var(--text-primary);
53
+ }
54
+
55
+ /* === HEADER INSTITUCIONAL MODERNO Y SOBRIO === */
56
+ .header-institucional {
57
+ position: relative;
58
+ padding: 3.5rem 3rem;
59
+ background: linear-gradient(135deg,
60
+ rgba(0, 106, 167, 0.15) 0%,
61
+ rgba(1, 124, 181, 0.12) 25%,
62
+ rgba(0, 153, 146, 0.10) 50%,
63
+ rgba(0, 170, 152, 0.08) 75%,
64
+ rgba(67, 193, 121, 0.05) 100%);
65
+ border-bottom: 1px solid rgba(67, 193, 121, 0.2);
66
+ margin-bottom: 2rem;
67
+ display: flex;
68
+ justify-content: space-between;
69
+ align-items: center;
70
+ overflow: hidden;
71
+ backdrop-filter: blur(10px);
72
+ box-shadow:
73
+ 0 4px 30px rgba(0, 0, 0, 0.3),
74
+ inset 0 1px 0 rgba(255, 255, 255, 0.05);
75
+ }
76
+
77
+ .header-institucional::before {
78
+ content: '';
79
+ position: absolute;
80
+ top: 0;
81
+ left: 0;
82
+ right: 0;
83
+ bottom: 0;
84
+ background:
85
+ linear-gradient(90deg,
86
+ transparent 0%,
87
+ rgba(67, 193, 121, 0.03) 50%,
88
+ transparent 100%),
89
+ linear-gradient(0deg,
90
+ rgba(0, 106, 167, 0.08) 0%,
91
+ transparent 100%);
92
+ pointer-events: none;
93
+ z-index: 0;
94
+ }
95
+
96
+ /* Línea de escaneo animada - Efecto sobrio */
97
+ .header-scan-line {
98
+ position: absolute;
99
+ top: 0;
100
+ left: -100%;
101
+ width: 100%;
102
+ height: 2px;
103
+ background: linear-gradient(90deg,
104
+ transparent 0%,
105
+ rgba(67, 193, 121, 0.4) 50%,
106
+ transparent 100%);
107
+ animation: scan 6s ease-in-out infinite;
108
+ z-index: 2;
109
+ opacity: 0.4;
110
+ }
111
+
112
+ @keyframes scan {
113
+ 0%, 100% { left: -100%; opacity: 0; }
114
+ 50% { left: 100%; opacity: 0.4; }
115
+ }
116
+
117
+ /* Efectos de luz sutiles */
118
+ .header-light-effect-1,
119
+ .header-light-effect-2 {
120
+ position: absolute;
121
+ width: 500px;
122
+ height: 160%;
123
+ pointer-events: none;
124
+ z-index: 1;
125
+ animation: glow-pulse-advanced 6s ease-in-out infinite;
126
+ filter: blur(30px);
127
+ }
128
+
129
+ .header-light-effect-1 {
130
+ top: -40%;
131
+ right: -10%;
132
+ background: radial-gradient(ellipse at center,
133
+ rgba(67, 193, 121, 0.2) 0%,
134
+ rgba(0, 170, 152, 0.12) 25%,
135
+ rgba(139, 182, 64, 0.06) 50%,
136
+ transparent 70%);
137
+ }
138
+
139
+ .header-light-effect-2 {
140
+ top: -30%;
141
+ left: -10%;
142
+ background: radial-gradient(ellipse at center,
143
+ rgba(0, 106, 167, 0.18) 0%,
144
+ rgba(1, 124, 181, 0.10) 25%,
145
+ rgba(0, 153, 146, 0.05) 50%,
146
+ transparent 70%);
147
+ animation-delay: 1s;
148
+ }
149
+
150
+ @keyframes glow-pulse-advanced {
151
+ 0%, 100% { opacity: 0.5; transform: scale(1); }
152
+ 50% { opacity: 0.8; transform: scale(1.08); }
153
+ }
154
+
155
+ /* Contenido del header */
156
+ .header-content {
157
+ position: relative;
158
+ z-index: 3;
159
+ flex: 1;
160
+ }
161
+
162
+ .header-title {
163
+ font-size: 2.8rem;
164
+ font-weight: 900;
165
+ color: white;
166
+ margin: 0;
167
+ letter-spacing: 0.02em;
168
+ line-height: 1.2;
169
+ text-shadow:
170
+ 0 0 12px rgba(67, 193, 121, 0.8),
171
+ 0 0 25px rgba(67, 193, 121, 0.6),
172
+ 0 0 40px rgba(0, 170, 152, 0.4),
173
+ 0 4px 20px rgba(0, 0, 0, 0.7);
174
+ position: relative;
175
+ animation: text-glow-holographic 4s ease-in-out infinite;
176
+ filter: drop-shadow(0 0 25px rgba(67, 193, 121, 0.6));
177
+ }
178
+
179
+ @keyframes text-glow-holographic {
180
+ 0%, 100% {
181
+ text-shadow:
182
+ 0 0 12px rgba(67, 193, 121, 0.8),
183
+ 0 0 25px rgba(67, 193, 121, 0.6),
184
+ 0 0 40px rgba(0, 170, 152, 0.4),
185
+ 0 4px 20px rgba(0, 0, 0, 0.7);
186
+ }
187
+ 50% {
188
+ text-shadow:
189
+ 0 0 15px rgba(139, 182, 64, 0.8),
190
+ 0 0 30px rgba(181, 164, 53, 0.6),
191
+ 0 0 50px rgba(67, 193, 121, 0.4),
192
+ 0 4px 20px rgba(0, 0, 0, 0.7);
193
+ }
194
+ }
195
+
196
+ .header-subtitle {
197
+ font-size: 1rem;
198
+ font-weight: 400;
199
+ color: rgba(255, 255, 255, 0.85);
200
+ margin: 0.5rem 0 0 0;
201
+ letter-spacing: 0.05em;
202
+ text-transform: uppercase;
203
+ opacity: 0.9;
204
+ }
205
+
206
+ /* Badge VUT Moderno y Sobrio */
207
+ .header-vut-badge {
208
+ display: flex;
209
+ align-items: center;
210
+ justify-content: center;
211
+ position: relative;
212
+ z-index: 4;
213
+ padding: 1rem 1.8rem;
214
+ background: linear-gradient(135deg,
215
+ rgba(0, 0, 0, 0.5) 0%,
216
+ rgba(0, 0, 0, 0.3) 100%);
217
+ border-radius: 16px;
218
+ border: 2px solid rgba(67, 193, 121, 0.4);
219
+ backdrop-filter: blur(20px);
220
+ box-shadow:
221
+ 0 8px 25px rgba(0, 0, 0, 0.5),
222
+ inset 0 1px 0 rgba(255, 255, 255, 0.15);
223
+ transition: all 0.3s ease;
224
+ }
225
+
226
+ .header-vut-badge:hover {
227
+ transform: translateY(-2px);
228
+ box-shadow:
229
+ 0 12px 35px rgba(0, 0, 0, 0.6),
230
+ inset 0 1px 0 rgba(255, 255, 255, 0.2);
231
+ border-color: rgba(67, 193, 121, 0.6);
232
+ }
233
+
234
+ .header-vut-text {
235
+ display: flex;
236
+ flex-direction: column;
237
+ align-items: center;
238
+ gap: 0.2rem;
239
+ }
240
+
241
+ .header-vut-acronym {
242
+ font-size: 2.8rem;
243
+ font-weight: 900;
244
+ color: white;
245
+ margin: 0;
246
+ letter-spacing: 0.12em;
247
+ line-height: 1;
248
+ text-shadow:
249
+ 0 0 15px rgba(67, 193, 121, 0.8),
250
+ 0 0 30px rgba(139, 182, 64, 0.6),
251
+ 0 0 45px rgba(181, 164, 53, 0.4),
252
+ 0 4px 15px rgba(0, 0, 0, 0.7);
253
+ animation: vut-glow 5s ease-in-out infinite;
254
+ }
255
+
256
+ @keyframes vut-glow {
257
+ 0%, 100% {
258
+ text-shadow:
259
+ 0 0 15px rgba(67, 193, 121, 0.8),
260
+ 0 0 30px rgba(139, 182, 64, 0.6),
261
+ 0 0 45px rgba(181, 164, 53, 0.4),
262
+ 0 4px 15px rgba(0, 0, 0, 0.7);
263
+ }
264
+ 50% {
265
+ text-shadow:
266
+ 0 0 20px rgba(139, 182, 64, 0.9),
267
+ 0 0 40px rgba(67, 193, 121, 0.7),
268
+ 0 0 60px rgba(0, 170, 152, 0.5),
269
+ 0 4px 15px rgba(0, 0, 0, 0.7);
270
+ }
271
+ }
272
+
273
+ .header-vut-subtext {
274
+ font-size: 0.75rem;
275
+ font-weight: 500;
276
+ color: rgba(255, 255, 255, 0.7);
277
+ letter-spacing: 0.1em;
278
+ text-transform: uppercase;
279
+ margin: 0;
280
+ }
281
+
282
+ /* Resto del CSS original... */
283
+ #MainMenu {visibility: hidden;}
284
+ footer {visibility: hidden;}
285
+ header {visibility: hidden;}
286
+ </style>
287
+ """, unsafe_allow_html=True)
288
+
289
+ # Colores corporativos
290
+ COLORS = {
291
+ 'VUT_CONFIRMADA': '#006AA7',
292
+ 'VUT_POSIBLE': '#017CB5',
293
+ 'VUT_BAJACONFIANZA_NOREGLADA': '#009992',
294
+ 'HABITUAL': '#00AA98',
295
+ 'SEGUNDA_RESIDENCIA': '#43C179',
296
+ 'VACIA': '#8BB640'
297
+ }
298
+
299
+ def get_chart_layout():
300
+ return {
301
+ 'paper_bgcolor': 'rgba(20, 24, 50, 0.5)',
302
+ 'plot_bgcolor': 'rgba(26, 31, 58, 0.5)',
303
+ 'font': {'family': 'Inter', 'size': 12, 'color': 'white'},
304
+ 'margin': {'t': 40, 'r': 20, 'b': 40, 'l': 50},
305
+ 'xaxis': {'gridcolor': 'rgba(67, 193, 121, 0.1)', 'color': 'white'},
306
+ 'yaxis': {'gridcolor': 'rgba(67, 193, 121, 0.1)', 'color': 'white'}
307
+ }
308
+
309
+ @st.cache_data
310
+ def cargar_datos(uploaded_file=None):
311
+ """Carga datos desde archivo subido o usa datos de demostración"""
312
+ if uploaded_file is not None:
313
+ try:
314
+ df = pd.read_excel(uploaded_file)
315
+ return df
316
+ except Exception as e:
317
+ st.error(f"Error al cargar el archivo: {e}")
318
+ return None
319
+ else:
320
+ # Datos de demostración
321
+ st.info("📊 Usando datos de demostración. Sube tu archivo Excel para ver tus datos reales.")
322
+ return crear_datos_demo()
323
+
324
+ def crear_datos_demo():
325
+ """Crea datos de demostración para pruebas"""
326
+ np.random.seed(42)
327
+ n_samples = 1000
328
+
329
+ categorias = ['VUT_CONFIRMADA', 'VUT_POSIBLE', 'VUT_BAJACONFIANZA_NOREGLADA',
330
+ 'HABITUAL', 'SEGUNDA_RESIDENCIA', 'VACIA']
331
+ barrios = ['Centro', 'Les Rotes', 'Les Marines', 'La Xara', 'Jesus Pobre',
332
+ 'Els Poblets', 'Dénia Nord', 'Dénia Sud']
333
+
334
+ data = {
335
+ 'Direccion': [f'Calle Demo {i}' for i in range(n_samples)],
336
+ 'Barrio': np.random.choice(barrios, n_samples),
337
+ 'Categoria': np.random.choice(categorias, n_samples,
338
+ p=[0.15, 0.20, 0.15, 0.25, 0.15, 0.10]),
339
+ 'Confianza': np.random.uniform(0.3, 1.0, n_samples),
340
+ 'Consumo_Total_Periodo_m3': np.random.uniform(50, 500, n_samples),
341
+ 'Ratio_Verano_Invierno': np.random.uniform(0.5, 3.0, n_samples)
342
+ }
343
+
344
+ return pd.DataFrame(data)
345
+
346
+ # Header institucional
347
+ st.markdown("""
348
+ <div class="header-institucional">
349
+ <div class="header-scan-line"></div>
350
+ <div class="header-light-effect-1"></div>
351
+ <div class="header-light-effect-2"></div>
352
+ <div class="header-content">
353
+ <div class="header-title">VUTIA</div>
354
+ <div class="header-subtitle">Sistema de Análisis de Viviendas de Uso Turístico</div>
355
+ </div>
356
+ <div class="header-vut-badge">
357
+ <div class="header-vut-text">
358
+ <div class="header-vut-acronym">VUT</div>
359
+ <div class="header-vut-subtext">Sistema de Análisis</div>
360
+ </div>
361
+ </div>
362
+ </div>
363
+ """, unsafe_allow_html=True)
364
+
365
+ # Subida de archivo
366
+ with st.sidebar:
367
+ st.markdown("### 📁 CARGAR DATOS")
368
+ uploaded_file = st.file_uploader(
369
+ "Sube tu archivo Excel",
370
+ type=['xlsx', 'xls'],
371
+ help="Archivo con datos de clasificación VUT"
372
+ )
373
+ st.markdown("---")
374
+
375
+ # Cargar datos
376
+ df = cargar_datos(uploaded_file)
377
+
378
+ if df is None:
379
+ st.error("No se pudieron cargar los datos")
380
+ st.stop()
381
+
382
+ # SIDEBAR DE FILTROS
383
+ with st.sidebar:
384
+ st.markdown("### 🔍 FILTROS")
385
+
386
+ categorias = df['Categoria'].unique().tolist()
387
+ cat_sel = st.multiselect("Categorías", categorias, default=categorias)
388
+
389
+ barrios = ['Todos'] + sorted(df['Barrio'].unique().tolist())
390
+ barrio_sel = st.selectbox("Barrio", barrios)
391
+
392
+ confianza_min = st.slider("Confianza Mínima (%)", 0, 100, 0, 5)
393
+
394
+ # Aplicar filtros
395
+ df_filtrado = df.copy()
396
+ if cat_sel:
397
+ df_filtrado = df_filtrado[df_filtrado['Categoria'].isin(cat_sel)]
398
+ if barrio_sel != 'Todos':
399
+ df_filtrado = df_filtrado[df_filtrado['Barrio'] == barrio_sel]
400
+ df_filtrado = df_filtrado[df_filtrado['Confianza'] >= confianza_min / 100]
401
+
402
+ # Estadísticas
403
+ st.markdown("### ESTADÍSTICAS")
404
+ col1, col2 = st.columns(2)
405
+ with col1:
406
+ st.metric("Filtradas", f"{len(df_filtrado):,}")
407
+ with col2:
408
+ st.metric("Total", f"{len(df):,}")
409
+
410
+ if len(df_filtrado) < len(df):
411
+ pct = (len(df_filtrado) / len(df)) * 100
412
+ st.caption(f"Mostrando {pct:.1f}% del total")
413
+
414
+ st.markdown("---")
415
+ st.caption("**VUTIA v3.0**")
416
+ st.caption("© 2026 Ayuntamiento de Dénia")
417
+
418
+ # TABS
419
+ tab1, tab2, tab3, tab4 = st.tabs([
420
+ "Panel General",
421
+ "Análisis VUT",
422
+ "Datos",
423
+ "Exportación"
424
+ ])
425
+
426
+ # TAB 1: PANEL GENERAL
427
+ with tab1:
428
+ st.markdown("## Panel General")
429
+ st.markdown("")
430
+
431
+ # Métricas principales
432
+ col1, col2, col3, col4, col5, col6 = st.columns(6)
433
+
434
+ with col1:
435
+ n = len(df_filtrado[df_filtrado['Categoria'] == 'VUT_CONFIRMADA'])
436
+ st.metric("VUT Confirmada", f"{n:,}",
437
+ f"{(n/len(df_filtrado)*100):.1f}%" if len(df_filtrado) > 0 else "0%")
438
+
439
+ with col2:
440
+ n = len(df_filtrado[df_filtrado['Categoria'] == 'VUT_POSIBLE'])
441
+ st.metric("VUT Posible", f"{n:,}",
442
+ f"{(n/len(df_filtrado)*100):.1f}%" if len(df_filtrado) > 0 else "0%")
443
+
444
+ with col3:
445
+ n = len(df_filtrado[df_filtrado['Categoria'] == 'VUT_BAJACONFIANZA_NOREGLADA'])
446
+ st.metric("VUT Baja Conf.", f"{n:,}",
447
+ f"{(n/len(df_filtrado)*100):.1f}%" if len(df_filtrado) > 0 else "0%")
448
+
449
+ with col4:
450
+ n = len(df_filtrado[df_filtrado['Categoria'] == 'HABITUAL'])
451
+ st.metric("Habitual", f"{n:,}",
452
+ f"{(n/len(df_filtrado)*100):.1f}%" if len(df_filtrado) > 0 else "0%")
453
+
454
+ with col5:
455
+ n = len(df_filtrado[df_filtrado['Categoria'] == 'SEGUNDA_RESIDENCIA'])
456
+ st.metric("Segunda Res.", f"{n:,}",
457
+ f"{(n/len(df_filtrado)*100):.1f}%" if len(df_filtrado) > 0 else "0%")
458
+
459
+ with col6:
460
+ n = len(df_filtrado[df_filtrado['Categoria'] == 'VACIA'])
461
+ st.metric("Vacía", f"{n:,}",
462
+ f"{(n/len(df_filtrado)*100):.1f}%" if len(df_filtrado) > 0 else "0%")
463
+
464
+ st.markdown("---")
465
+
466
+ # Gráfico de distribución
467
+ st.markdown("### Distribución por Categorías")
468
+ dist = df_filtrado['Categoria'].value_counts()
469
+
470
+ fig = go.Figure(data=[go.Pie(
471
+ labels=dist.index,
472
+ values=dist.values,
473
+ hole=.45,
474
+ marker=dict(
475
+ colors=[COLORS.get(c, '#017CB5') for c in dist.index],
476
+ line=dict(color='rgba(10, 14, 39, 0.8)', width=3)
477
+ ),
478
+ textfont=dict(size=12, family='Inter', color='white'),
479
+ hovertemplate='<b>%{label}</b><br>%{value:,} viviendas<br>%{percent}<extra></extra>'
480
+ )])
481
+
482
+ layout = get_chart_layout()
483
+ layout['height'] = 400
484
+ fig.update_layout(layout)
485
+
486
+ st.plotly_chart(fig, use_container_width=True)
487
+
488
+ # TAB 2: ANÁLISIS VUT
489
+ with tab2:
490
+ st.markdown("## Análisis Detallado de VUT")
491
+
492
+ vut_categories = ['VUT_CONFIRMADA', 'VUT_POSIBLE', 'VUT_BAJACONFIANZA_NOREGLADA']
493
+ df_vut = df_filtrado[df_filtrado['Categoria'].isin(vut_categories)]
494
+
495
+ col1, col2, col3 = st.columns(3)
496
+ with col1:
497
+ st.metric("Total VUT", f"{len(df_vut):,}")
498
+ with col2:
499
+ st.metric("Confirmadas", f"{len(df_vut[df_vut['Categoria']=='VUT_CONFIRMADA']):,}")
500
+ with col3:
501
+ avg_conf = df_vut['Confianza'].mean() * 100 if len(df_vut) > 0 else 0
502
+ st.metric("Confianza Media", f"{avg_conf:.1f}%")
503
+
504
+ st.markdown("---")
505
+
506
+ # Top 20 VUT
507
+ st.markdown("### Top 20 VUT por Confianza")
508
+ top20 = df_vut.nlargest(20, 'Confianza')
509
+
510
+ for idx, row in top20.iterrows():
511
+ with st.container():
512
+ col1, col2, col3, col4 = st.columns([3, 2, 2, 2])
513
+ with col1:
514
+ st.write(f"**{row['Direccion']}**")
515
+ with col2:
516
+ st.write(f"Barrio: {row['Barrio']}")
517
+ with col3:
518
+ st.write(f"Categoría: {row['Categoria']}")
519
+ with col4:
520
+ st.write(f"Confianza: {row['Confianza']*100:.1f}%")
521
+ st.markdown("---")
522
+
523
+ # TAB 3: DATOS
524
+ with tab3:
525
+ st.markdown("## Datos Detallados")
526
+
527
+ # Búsqueda
528
+ busqueda = st.text_input("🔍 Buscar por dirección o barrio")
529
+
530
+ df_mostrar = df_filtrado.copy()
531
+ if busqueda:
532
+ mask = (df_mostrar['Direccion'].str.contains(busqueda, case=False, na=False) |
533
+ df_mostrar['Barrio'].str.contains(busqueda, case=False, na=False))
534
+ df_mostrar = df_mostrar[mask]
535
+
536
+ st.dataframe(
537
+ df_mostrar[['Direccion', 'Barrio', 'Categoria', 'Confianza', 'Consumo_Total_Periodo_m3']],
538
+ use_container_width=True,
539
+ height=600
540
+ )
541
+
542
+ st.caption(f"Mostrando {len(df_mostrar):,} de {len(df_filtrado):,} viviendas")
543
+
544
+ # TAB 4: EXPORTACIÓN
545
+ with tab4:
546
+ st.markdown("## Exportación de Datos")
547
+
548
+ st.markdown("### Descargar Datos Filtrados")
549
+
550
+ # Convertir a Excel
551
+ buffer = BytesIO()
552
+ with pd.ExcelWriter(buffer, engine='openpyxl') as writer:
553
+ df_filtrado.to_excel(writer, index=False, sheet_name='Datos')
554
+ buffer.seek(0)
555
+
556
+ st.download_button(
557
+ label="📊 Descargar Excel",
558
+ data=buffer,
559
+ file_name=f"vutia_datos_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx",
560
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
561
+ )
562
+
563
+ # Convertir a CSV
564
+ csv = df_filtrado.to_csv(index=False).encode('utf-8')
565
+
566
+ st.download_button(
567
+ label="📄 Descargar CSV",
568
+ data=csv,
569
+ file_name=f"vutia_datos_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv",
570
+ mime="text/csv"
571
+ )