hedtorresca commited on
Commit
38048a8
·
verified ·
1 Parent(s): f78af18

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +610 -0
app.py ADDED
@@ -0,0 +1,610 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app.py – Explorador geoespacial Vasculitis ANCA (Bogotá)
2
+ # ─────────────────────────────────────────────────────────────
3
+ # • Carga el Excel “Vasculitis…2025‑04‑16_1949 (1).xlsx”.
4
+ # • Normaliza los nombres de columna a snake_case ASCII.
5
+ # • Renombra dinámicamente latitud / longitud.
6
+ # • Deriva:
7
+ # – edad_cat (quinquenios)
8
+ # – flags de antecedentes
9
+ # – patrón de biopsia resumido
10
+ # • Filtros completos por género, edad, localidad, ANCA, MPO, PR3,
11
+ # antecedentes, patrón de biopsia y compromiso renal.
12
+ # • Mapa Folium con:
13
+ # – coroplético pacientes / localidad
14
+ # – capas ambientales (PM10, PM2.5, Ozono, Temp, Precip, Viento, WQI)
15
+ # – heatmap opcional
16
+ # – una sola capa de clústeres 1 km con pop‑ups resumidos
17
+ # • Gráficos Univariado y Bivariado que aceptan TODAS las variables
18
+ # (numéricas → histograma / dispersión; categóricas → barras / box‑plot).
19
+ # • Toda etiqueta de biopsia u antecedente usa la forma corta
20
+ # (p.ej. “Crescéntica”, “Vasculitis + glom.”, “Hipertensión”, “EPOC”…).
21
+
22
+ import re, unicodedata, warnings, branca, folium, gradio as gr
23
+ import pandas as pd, geopandas as gpd, numpy as np
24
+ from shapely.geometry import Point
25
+ from folium.plugins import HeatMap
26
+ from sklearn.cluster import DBSCAN
27
+ import plotly.express as px, plotly.graph_objects as go
28
+ import pandas.api.types as ptypes
29
+ import math
30
+
31
+ warnings.filterwarnings("ignore")
32
+
33
+ def snake(cols):
34
+ out = []
35
+ for col in cols:
36
+ txt = unicodedata.normalize("NFKD", col)
37
+ txt = txt.encode("ascii", "ignore").decode("utf-8")
38
+ txt = re.sub(r"[^\w]+", "_", txt.strip().lower())
39
+ out.append(txt.strip("_"))
40
+ return out
41
+
42
+ DATA_XLSX = "VasculitisAsociadasA-Bdd3_DATA_LABELS_2025-04-16_1949 (1).xlsx"
43
+ LOCALIDADES = "loca.json"
44
+ GEO_AMBIENTALES = {
45
+ "PM10": "pm10_prom_anual.geojson",
46
+ "PM2.5": "pm25_prom_anual_2023 (2).geojson",
47
+ "Ozono": "ozono_prom_anual_2022 (2).geojson",
48
+ "Temperatura": "temp_anualprom_2023 (2).geojson",
49
+ "Precipitación": "precip_anualacum_2023 (2).geojson",
50
+ "Viento": "vel_viento_0_23h_anual_2023.geojson",
51
+ "WQI": "tramo_wqi.geojson",
52
+ "Heatmap pacientes": None
53
+ }
54
+ META_CAPAS = {
55
+ "PM10": ("conc_pm10", "µg/m³", branca.colormap.linear.OrRd_09, "id", "Zona"),
56
+ "PM2.5": ("conc_pm25", "µg/m³", branca.colormap.linear.Reds_09, "id", "Zona"),
57
+ "Ozono": ("conc_ozono", "ppb", branca.colormap.linear.PuBuGn_09, "id", "Zona"),
58
+ "Temperatura": ("temperatur", "°C", branca.colormap.linear.YlOrBr_09, "id", "Zona"),
59
+ "Precipitación": ("precip_per", "mm", branca.colormap.linear.Blues_09, "id", "Zona"),
60
+ "Viento": ("velocidad", "m/s", branca.colormap.linear.GnBu_09, "id", "Zona"),
61
+ "WQI": ("wqi", "", branca.colormap.linear.Greens_09, "tramo", "Tramo")
62
+ }
63
+
64
+ # ─── 1. Pacientes ────────────────────────────────────────
65
+ df = pd.read_excel(DATA_XLSX, dtype=str)
66
+ df.columns = snake(df.columns)
67
+
68
+ col_lat = next(c for c in df.columns if "residencia" in c and "latitud" in c)
69
+ col_lon = next(c for c in df.columns if "residencia" in c and "longitud" in c)
70
+ df = df.rename(columns={col_lat:"latitud", col_lon:"longitud"})
71
+ df["latitud"] = pd.to_numeric(df["latitud"].str.replace(",", "."), errors="coerce")
72
+ df["longitud"] = pd.to_numeric(df["longitud"].str.replace(",", "."), errors="coerce")
73
+ df = df.dropna(subset=["latitud","longitud"])
74
+ df["geometry"] = df.apply(lambda r: Point(r["longitud"], r["latitud"]), axis=1)
75
+ df = gpd.GeoDataFrame(df, geometry="geometry", crs="EPSG:4326")
76
+
77
+ # ─── 2. Localidades ─────────────────────────────────────
78
+ geo_loc = gpd.read_file(LOCALIDADES).to_crs("EPSG:4326")
79
+ geo_loc.columns = snake(geo_loc.columns)
80
+ loc_col = next(c for c in geo_loc.columns if "localidad" in c or "locnombre" in c)
81
+ geo_loc = geo_loc.rename(columns={loc_col:"localidad"})
82
+ geo_loc["localidad"] = geo_loc["localidad"].str.upper()
83
+ df = gpd.sjoin(df, geo_loc[["localidad","geometry"]], how="left", predicate="within") \
84
+ .drop(columns="index_right")
85
+
86
+ # ─── 3. Capas ambientales ───────────────────────────────
87
+ def load_gjson(path):
88
+ g = gpd.read_file(path).to_crs("EPSG:4326")
89
+ g.columns = snake(g.columns)
90
+ for c in g.columns:
91
+ if ptypes.is_datetime64_any_dtype(g[c].dtype):
92
+ g[c] = g[c].astype(str)
93
+ elif g[c].dtype == object:
94
+ txt = g[c].str.strip()
95
+ if txt.str.match(r"^-?\d+(\.\d+)?$").all():
96
+ g[c] = txt.astype(float)
97
+ else:
98
+ g[c] = txt
99
+ return g
100
+
101
+ caps_amb = {k: load_gjson(v) for k,v in GEO_AMBIENTALES.items() if v}
102
+
103
+ wqi_bins = [0, 20, 35, 50, 70, 100]
104
+ wqi_labels = ["Pobre", "Marginal", "Regular", "Buena", "Excelente"]
105
+ wqi_colors = ["red", "olive", "purple", "green", "blue"]
106
+
107
+ # 2) Extrae el GeoDataFrame de WQI y conviértelo a numérico
108
+ g_wqi = caps_amb["WQI"].copy()
109
+ g_wqi["wqi_val"] = pd.to_numeric(g_wqi["wqi"], errors="coerce")
110
+
111
+ # 3) Crea la categoría
112
+ g_wqi["wqi_cat"] = pd.cut(
113
+ g_wqi["wqi_val"],
114
+ bins=wqi_bins,
115
+ labels=wqi_labels,
116
+ include_lowest=True
117
+ )
118
+
119
+ # 4) Construye el colormap por pasos
120
+ WQI_COLORMAP = branca.colormap.StepColormap(
121
+ colors=wqi_colors,
122
+ index=wqi_bins,
123
+ vmin=wqi_bins[0],
124
+ vmax=wqi_bins[-1],
125
+ caption="WQI"
126
+ )
127
+
128
+ # 5) Guarda de nuevo en caps_amb
129
+ caps_amb["WQI"] = g_wqi
130
+ # ─── 4. Derivadas y flags ───────────────────────────────
131
+ df["genero_cat"] = df.get("genero","").str.capitalize()
132
+ df["estrato_cat"] = df.get("estrato_socioeconomico","").str.capitalize()
133
+
134
+ df["edad"] = pd.to_numeric(df.get("edad_en_anos_del_paciente","").str.replace(",", "."), errors="coerce")
135
+ bins = list(range(0,105,5))
136
+ labels = [f"{b}-{b+4}" for b in bins[:-1]]
137
+ df["edad_cat"] = pd.cut(df["edad"], bins=bins, labels=labels, right=False)
138
+
139
+ df["anca_cat"] = df.get("ancas")
140
+ df["mpo_cat"] = df.get("mpo")
141
+ df["pr3_cat"] = df.get("pr3")
142
+
143
+ df["sindrome_renal"] = df.get("sindrome_renal_al_ingreso","").str.capitalize()
144
+ df["manifestaciones_extrarenales"] = df.get("manifestaciones_extrarenales","").str.capitalize()
145
+ df["proteinuria"] = df.get("proteinuria","").str.capitalize()
146
+ df["creatinina"] = pd.to_numeric(df.get("creatinina","").str.replace(",", "."), errors="coerce")
147
+
148
+ ante_cols = {
149
+ "diabetes":"antecedente_personal_de_diabetes",
150
+ "falla_cardiaca":"antecedente_personal_de_falla_cardiaca",
151
+ "epoc":"antecedente_personal_de_epoc",
152
+ "hipertension":"antecedente_personal_de_hipertension_arterial",
153
+ "vih":"antecedente_personal_de_vih",
154
+ "autoinmune":"antecedente_personal_de_otra_enfermedad_autoinmune",
155
+ "cancer":"antecedente_personal_de_cancer"
156
+ }
157
+ resumen_ante = {
158
+ "diabetes":"Diabetes",
159
+ "falla_cardiaca":"Falla cardíaca",
160
+ "epoc":"EPOC",
161
+ "hipertension":"Hipertensión",
162
+ "vih":"VIH",
163
+ "autoinmune":"Enf. autoinmune",
164
+ "cancer":"Cáncer"
165
+ }
166
+ for key,col in ante_cols.items():
167
+ df[key] = (df.get(col,"0").astype(str).str.lower()
168
+ .map({"si":1,"sí":1,"checked":1,"1":1})
169
+ .fillna(0).astype(int)
170
+ )
171
+
172
+ bio_raw = [c for c in df.columns if c.startswith("hallazgos_histologicos_en_biopsia")]
173
+ ren_bio = {c:f"bio_{i}" for i,c in enumerate(bio_raw,1)}
174
+ df = df.rename(columns=ren_bio)
175
+ bio_cols = list(ren_bio.values())
176
+
177
+ BIO_REGEX = [
178
+ (r"sin_alteraciones$", "Sin alteraciones"),
179
+ (r"sin_proliferacion_extracapilar", "Necrosis sin PC"),
180
+ (r"menos_del_50.*focal", "Focal"),
181
+ (r"clase_mixta", "Mixta"),
182
+ (r"mas_del_50.*cresc", "Crescéntica"),
183
+ (r"sin_compromiso_glomerular$", "Vasculitis sin glom."),
184
+ (r"con_compromiso_glomerular$", "Vasculitis + glom."),
185
+ (r"sin_dato$", "Sin dato")
186
+ ]
187
+ # crear un dict raw_col → short
188
+ raw2short = {}
189
+ for patt, short in BIO_REGEX:
190
+ raw = next(c for c in bio_raw if re.search(patt, c))
191
+ raw2short[raw] = short
192
+
193
+ def patron_bio(row):
194
+ for raw, flag in ren_bio.items():
195
+ if str(row[flag]).strip().lower() in ("si","sí","checked","1"):
196
+ return raw2short.get(raw, "Sin dato")
197
+ return "Sin dato"
198
+
199
+ df["biopsia_patron"] = df.apply(patron_bio, axis=1)
200
+ df["biopsia_positiva"] = np.where(df["biopsia_patron"]=="Sin dato","No","Si")
201
+
202
+ # ─── 5. Filtrado ────────────────────────────────────────
203
+ def filtrar(d, gen, edades, locs, renal, ants, bios, anca, mpo, pr3):
204
+ d2 = d.copy()
205
+ if gen!="Todos": d2 = d2[d2["genero_cat"]==gen]
206
+ if edades: d2 = d2[d2["edad_cat"].isin(edades)]
207
+ if locs: d2 = d2[d2["localidad"].isin(locs)]
208
+ if renal!="Todos": d2 = d2[d2["biopsia_positiva"]==renal]
209
+ if bios and bios!=["Todos"]:
210
+ d2 = d2[d2["biopsia_patron"].isin(bios)]
211
+ if anca!="Todos": d2 = d2[d2["anca_cat"]==anca]
212
+ if mpo!="Todos": d2 = d2[d2["mpo_cat"]==mpo]
213
+ if pr3!="Todos": d2 = d2[d2["pr3_cat"]==pr3]
214
+ for ant in ants:
215
+ if ant=="Todos": continue
216
+ key = next(k for k,v in resumen_ante.items() if v==ant)
217
+ d2 = d2[d2[key]==1]
218
+ return d2
219
+
220
+ # ─── 6. Mapas ───────────────────────────────────────────
221
+ # ─── 6. Mapas ───────────────────────────────────────────
222
+ def choropleth(m, g, val, title, cmap, zfield, zalias):
223
+ g = g.copy()
224
+ g[val] = pd.to_numeric(g[val], errors="coerce")
225
+ vmin, vmax = g[val].min(), g[val].max()
226
+ cm = cmap.scale(vmin, vmax)
227
+ cm.caption = title
228
+ cm.add_to(m)
229
+
230
+ is_line = g.geometry.iloc[0].geom_type.startswith("Line")
231
+ style = (
232
+ lambda f,vc=val: {"color":cm(f["properties"][vc]),"weight":4,"opacity":0.9}
233
+ ) if is_line else (
234
+ lambda f,vc=val: {"fillColor":cm(f["properties"][vc]),"fillOpacity":0.8,
235
+ "color":"black","weight":0.3}
236
+ )
237
+
238
+ fields = [zfield, val]
239
+ aliases = [zalias, title]
240
+ for extra in ("nombre","rio"):
241
+ if extra in g.columns:
242
+ fields.append(extra); aliases.append("Río"); break
243
+
244
+ folium.GeoJson(
245
+ g, name=title,
246
+ style_function=style,
247
+ highlight_function=lambda f: {"weight":2,"color":"#444","fillOpacity":0.95},
248
+ tooltip=folium.GeoJsonTooltip(fields=fields, aliases=aliases, sticky=True)
249
+ ).add_to(m)
250
+
251
+ def capa_clusters(m, d):
252
+ """
253
+ Añade al mapa m una capa de clústeres de pacientes (DBSCAN 1 km),
254
+ con popups que muestran género, edad (si existe), patrón biopsia y antecedentes.
255
+ """
256
+ if d.empty:
257
+ return
258
+ coords = np.radians(d[["latitud", "longitud"]].astype(float))
259
+ if len(coords) < 3:
260
+ return
261
+ labels = DBSCAN(eps=1/6371, min_samples=3, metric="haversine").fit_predict(coords)
262
+ d = d.copy()
263
+ d["cluster"] = labels
264
+
265
+ pal = branca.colormap.linear.Set1_09
266
+ fg = folium.FeatureGroup(name="Clústeres (1 km)", overlay=True)
267
+
268
+ for cl in sorted([c for c in d["cluster"].unique() if c != -1]):
269
+ color = pal(cl / max(1, d["cluster"].nunique() - 1))
270
+ for _, r in d[d["cluster"] == cl].iterrows():
271
+ # Edad segura
272
+ if pd.notna(r["edad"]) and not math.isnan(r["edad"]):
273
+ edad_txt = f"{int(r['edad'])} años"
274
+ else:
275
+ edad_txt = "Sin dato edad"
276
+
277
+ # Antecedentes resumidos
278
+ ant = [v for k, v in resumen_ante.items() if r.get(k) == 1]
279
+ ants_txt = "; ".join(ant) if ant else "Ninguno"
280
+
281
+ popup = (
282
+ f"Clúster #{cl}<br>"
283
+ f"Género: {r['genero_cat']}<br>"
284
+ f"Edad: {edad_txt}<br>"
285
+ f"Biopsia: {r['biopsia_patron']}<br>"
286
+ f"Antecedentes: {ants_txt}"
287
+ )
288
+ folium.CircleMarker(
289
+ location=(r["latitud"], r["longitud"]),
290
+ radius=6,
291
+ color=color,
292
+ fill=True, fill_color=color, fill_opacity=0.9,
293
+ weight=1,
294
+ popup=popup
295
+ ).add_to(fg)
296
+
297
+ fg.add_to(m)
298
+
299
+
300
+ def crear_mapa(d_filt, capas, ver_cluster):
301
+ """
302
+ Construye el mapa completo:
303
+ - coroplético de pacientes por localidad
304
+ - capas ambientales
305
+ - heatmap de puntos
306
+ - marcadores individuales con popups seguros
307
+ - clústeres si ver_cluster=True
308
+ """
309
+ # 1) Coroplético por localidad
310
+ g = d_filt.groupby("localidad").size().reset_index(name="pacientes")
311
+ geo = geo_loc.merge(g, on="localidad", how="left").fillna({"pacientes": 0})
312
+
313
+ m = folium.Map(location=[4.65, -74.1], zoom_start=11, tiles="CartoDB positron")
314
+ choropleth(
315
+ m, geo, "pacientes", "Pacientes por localidad (N)",
316
+ branca.colormap.linear.Reds_09, "localidad", "Localidad"
317
+ )
318
+
319
+ # 2) Capas ambientales
320
+ for capa in capas:
321
+ # 1) Saltar el heatmap aquí
322
+ if capa == "Heatmap pacientes":
323
+ continue
324
+
325
+ # 2) WQI: paso discreto + leyenda
326
+ if capa == "WQI":
327
+ # Añadir la leyenda de WQI (continua o en pasos, como prefieras)
328
+ WQI_COLORMAP.add_to(m)
329
+
330
+ folium.GeoJson(
331
+ caps_amb["WQI"],
332
+ name="WQI (valor y categoría)",
333
+ style_function=lambda f: {
334
+ "color": WQI_COLORMAP(f["properties"]["wqi_val"]),
335
+ "fillColor": WQI_COLORMAP(f["properties"]["wqi_val"]),
336
+ "weight": 3,
337
+ "fillOpacity": 0.7
338
+ },
339
+ tooltip=folium.GeoJsonTooltip(
340
+ fields=["nombre", # nombre del río
341
+ "tramo", # identificador de tramo
342
+ "wqi_val"], # valor numérico de WQI
343
+ aliases=["Río", # alias para nombre
344
+ "Tramo", # alias para tramo
345
+ "WQI (valor)"], # alias para wqi_val
346
+ sticky=True
347
+ )
348
+ ).add_to(m)
349
+ continue # no volver a procesar esta capa
350
+
351
+ # 3) Resto de capas: color continuo con tu choropleth genérico
352
+ gdf = caps_amb.get(capa)
353
+ val, uni, cmap, zfield, zalias = META_CAPAS[capa]
354
+ if gdf is not None and val in gdf.columns:
355
+ choropleth(
356
+ m,
357
+ gdf,
358
+ val,
359
+ f"{capa}{' ('+uni+')' if uni else ''}",
360
+ cmap,
361
+ zfield,
362
+ zalias
363
+ )
364
+
365
+ # 3) Heatmap de puntos
366
+ if "Heatmap pacientes" in capas and not d_filt.empty:
367
+ HeatMap(
368
+ d_filt[["latitud", "longitud"]].astype(float).values,
369
+ radius=18, name="Heatmap pacientes"
370
+ ).add_to(m)
371
+
372
+ # 4) Marcadores individuales
373
+ fg_pts = folium.FeatureGroup(name="Puntos pacientes", overlay=True)
374
+ for _, r in d_filt.iterrows():
375
+ # Edad segura
376
+ if pd.notna(r["edad"]) and not math.isnan(r["edad"]):
377
+ edad_txt = f"{int(r['edad'])} años"
378
+ else:
379
+ edad_txt = "Sin dato edad"
380
+
381
+ # Antecedentes resumidos
382
+ ant = [v for k, v in resumen_ante.items() if r.get(k) == 1]
383
+ ants_txt = "<br>".join(ant) if ant else "Ninguno"
384
+
385
+ popup_html = (
386
+ f"Localidad: {r['localidad']}<br>"
387
+ f"Edad: {edad_txt}<br>"
388
+ f"Género: {r['genero_cat']}<br>"
389
+ f"Biopsia: {r['biopsia_patron']}<br>"
390
+ f"Antecedentes:<br>{ants_txt}"
391
+ )
392
+ folium.CircleMarker(
393
+ location=(r["latitud"], r["longitud"]),
394
+ radius=5,
395
+ color="#c00",
396
+ fill=True, fill_color="white",
397
+ fill_opacity=0.85, weight=1,
398
+ popup=popup_html
399
+ ).add_to(fg_pts)
400
+ fg_pts.add_to(m)
401
+
402
+ # 5) Capa de clústeres opcional
403
+ if ver_cluster:
404
+ capa_clusters(m, d_filt)
405
+
406
+ folium.LayerControl(collapsed=False).add_to(m)
407
+ return m._repr_html_()
408
+
409
+ # ─── 7. Gráficos ─────────────────────────────────────────
410
+ def col_of(v):
411
+ """Mapea nombre legible a columna interna."""
412
+ if v in resumen_ante.values():
413
+ return next(k for k,val in resumen_ante.items() if val==v)
414
+ if v in raw2short.values() or v=="Patrón biopsia":
415
+ return "biopsia_patron"
416
+ return v
417
+
418
+ def g_uni(var, d):
419
+ if d.empty:
420
+ return go.Figure()
421
+ col = col_of(var)
422
+ # 1) Flags de antecedentes (0/1) → barras de conteo "No"/"Si"
423
+ if var in resumen_ante.values():
424
+ s = d[col].map({0:"No",1:"Si"})
425
+ fig = px.histogram(s, x=s,
426
+ category_orders={col:["No","Si"]},
427
+ text_auto=True,
428
+ title=var)
429
+ # 2) Patrón biopsia → barras de conteo de cada categoría
430
+ elif var=="Patrón biopsia" or var in raw2short.values():
431
+ fig = px.histogram(d, x="biopsia_patron",
432
+ category_orders={"biopsia_patron": list(raw2short.values())},
433
+ text_auto=True,
434
+ title="Patrón biopsia")
435
+ # 3) Variables numéricas → histograma
436
+ elif d[col].dtype.kind in "if":
437
+ fig = px.histogram(d, x=col, nbins=20, title=var)
438
+ # 4) Resto categóricas → barras de conteo con color
439
+ else:
440
+ fig = px.histogram(d, x=col, color=col, text_auto=True, title=var)
441
+ fig.update_layout(bargap=0.1)
442
+ return fig
443
+
444
+ def g_bi(x, y, d):
445
+ """
446
+ Gráfico bivariado:
447
+ - num vs num → scatter con trendline
448
+ - num vs cat → boxplot
449
+ - cat vs cat → barras agrupadas
450
+ Reconoce correctamente:
451
+ • Patrones de biopsia (incluida la etiqueta "Patrón biopsia")
452
+ • Etiquetas de antecedentes.
453
+ """
454
+ if d.empty:
455
+ return go.Figure()
456
+
457
+ # Mapeo de la variable de UI al nombre real de columna en df
458
+ def map_var(v):
459
+ # Dropdown de patrón de biopsia (UI) → columna biop_patron
460
+ if v == "Patrón biopsia":
461
+ return "biopsia_patron"
462
+ # Cualquier etiqueta corta de biopsia
463
+ if v in resumen_bio_map.values():
464
+ return "biopsia_patron"
465
+ # Etiqueta de antecedente → nombre de flag en df
466
+ for key, lab in resumen_ante.items():
467
+ if v == lab:
468
+ return key
469
+ # Variables numéricas o de texto sin transformar
470
+ return v
471
+
472
+ cx = map_var(x)
473
+ cy = map_var(y)
474
+
475
+ # Determinar si cada una es categórica (flags, biopsia o texto)
476
+ is_cat = {}
477
+ for var in (cx, cy):
478
+ is_cat[var] = (
479
+ var == "biopsia_patron"
480
+ or var in resumen_ante.keys()
481
+ or d[var].dtype == object
482
+ )
483
+
484
+ # 1) cat vs cat → histograma agrupado
485
+ if is_cat[cx] and is_cat[cy]:
486
+ fig = px.histogram(
487
+ d,
488
+ x=cx,
489
+ color=cy,
490
+ barmode="group",
491
+ category_orders={
492
+ cx: list(resumen_bio_map.values()) if cx=="biopsia_patron" else list(resumen_ante.values()),
493
+ cy: list(resumen_bio_map.values()) if cy=="biopsia_patron" else list(resumen_ante.values()),
494
+ },
495
+ labels={cx: x, cy: y},
496
+ title=f"{x} vs {y}"
497
+ )
498
+
499
+ # 2) num vs cat → boxplot
500
+ elif is_cat[cx] ^ is_cat[cy]:
501
+ # uno es categórico, otro numérico
502
+ if is_cat[cx]:
503
+ fig = px.box(
504
+ d,
505
+ x=cx,
506
+ y=cy,
507
+ points="all",
508
+ category_orders={cx: list(resumen_bio_map.values()) if cx=="biopsia_patron" else list(resumen_ante.values())},
509
+ labels={cx: x, cy: y},
510
+ title=f"{x} vs {y}"
511
+ )
512
+ else:
513
+ fig = px.box(
514
+ d,
515
+ x=cy,
516
+ y=cx,
517
+ points="all",
518
+ category_orders={cy: list(resumen_bio_map.values()) if cy=="biopsia_patron" else list(resumen_ante.values())},
519
+ labels={cx: x, cy: y},
520
+ title=f"{x} vs {y}"
521
+ )
522
+
523
+ # 3) num vs num → scatter + trendline
524
+ else:
525
+ fig = px.scatter(
526
+ d,
527
+ x=cx,
528
+ y=cy,
529
+ trendline="ols",
530
+ labels={cx: x, cy: y},
531
+ title=f"{x} vs {y}"
532
+ )
533
+
534
+ fig.update_layout(bargap=0.1)
535
+ return fig
536
+ # ─── 8. Interfaz Gradio ───────────────────────────────────
537
+ def interfaz():
538
+ gen = ["Todos"] + sorted(df["genero_cat"].dropna().unique())
539
+ ages = sorted(df["edad_cat"].dropna().unique())
540
+ locs = sorted(df["localidad"].dropna().unique())
541
+ ancas = ["Todos"] + sorted(df["anca_cat"].dropna().unique())
542
+ mpos = ["Todos"] + sorted(df["mpo_cat"].dropna().unique())
543
+ pr3s = ["Todos"] + sorted(df["pr3_cat"].dropna().unique())
544
+
545
+ vars_cat = [
546
+ "genero_cat","estrato_cat","edad_cat","sindrome_renal",
547
+ "manifestaciones_extrarenales","proteinuria",
548
+ "anca_cat","mpo_cat","pr3_cat"
549
+ ] + ["Patrón biopsia"] + list(resumen_ante.values())
550
+ vars_num = ["edad","creatinina"]
551
+ vars_all = vars_cat + vars_num
552
+
553
+ with gr.Blocks(title="Vasculitis ANCA Bogotá") as demo:
554
+ gr.Markdown("## Explorador geoespacial – Vasculitis ANCA (Bogotá)")
555
+
556
+ with gr.Row():
557
+ ui_gen = gr.Dropdown(gen, label="Género", value="Todos")
558
+ ui_age = gr.CheckboxGroup(ages, label="Edad (quinquenios)")
559
+ ui_loc = gr.Dropdown(locs, multiselect=True, label="Localidades")
560
+ ui_renal = gr.Dropdown(["Todos","Si","No"], value="Todos", label="Compromiso renal")
561
+ ui_ant = gr.CheckboxGroup(["Todos"]+list(resumen_ante.values()), label="Antecedentes")
562
+ ui_bio = gr.CheckboxGroup(["Todos"]+list(raw2short.values()), label="Patrón biopsia")
563
+ with gr.Row():
564
+ ui_anca = gr.Dropdown(ancas, label="ANCA", value="Todos")
565
+ ui_mpo = gr.Dropdown(mpos, label="MPO", value="Todos")
566
+ ui_pr3 = gr.Dropdown(pr3s, label="PR3", value="Todos")
567
+
568
+ ui_capas = gr.CheckboxGroup(list(GEO_AMBIENTALES.keys()), label="Capas mapa")
569
+ ui_clu = gr.Checkbox(label="Mostrar clústeres (1 km)")
570
+
571
+ with gr.Tab("Mapa"):
572
+ btn_map = gr.Button("Generar mapa")
573
+ out_map = gr.HTML()
574
+ btn_map.click(
575
+ lambda *i: crear_mapa(filtrar(df,*i[:-2]), i[-2], i[-1]),
576
+ inputs=[ui_gen,ui_age,ui_loc,ui_renal,
577
+ ui_ant,ui_bio,ui_anca,ui_mpo,ui_pr3,
578
+ ui_capas,ui_clu],
579
+ outputs=out_map
580
+ )
581
+
582
+ with gr.Tab("Univariado"):
583
+ ui_var = gr.Dropdown(vars_all, label="Variable")
584
+ btn_uni = gr.Button("Graficar")
585
+ out_uni = gr.Plot()
586
+ btn_uni.click(
587
+ lambda v,*i: g_uni(v, filtrar(df,*i)),
588
+ inputs=[ui_var,ui_gen,ui_age,ui_loc,ui_renal,
589
+ ui_ant,ui_bio,ui_anca,ui_mpo,ui_pr3],
590
+ outputs=out_uni
591
+ )
592
+
593
+ with gr.Tab("Bivariado"):
594
+ ui_x = gr.Dropdown(vars_all, label="Variable X")
595
+ ui_y = gr.Dropdown(vars_all, label="Variable Y")
596
+ btn_bi = gr.Button("Graficar")
597
+ out_bi = gr.Plot()
598
+ btn_bi.click(
599
+ lambda x,y,*i: g_bi(x,y, filtrar(df,*i)),
600
+ inputs=[ui_x,ui_y,ui_gen,ui_age,ui_loc,ui_renal,
601
+ ui_ant,ui_bio,ui_anca,ui_mpo,ui_pr3],
602
+ outputs=out_bi
603
+ )
604
+
605
+ demo.launch()
606
+
607
+
608
+
609
+ if __name__ == "__main__":
610
+ interfaz()