hedtorresca commited on
Commit
26fe535
·
verified ·
1 Parent(s): adc723d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +517 -247
app.py CHANGED
@@ -1,277 +1,547 @@
1
- # app.py
2
- import gradio as gr
3
- import pandas as pd
4
- import numpy as np
5
- import geopandas as gpd
6
- import plotly.express as px
7
- import plotly.graph_objects as go
8
- import folium
9
- from folium.plugins import HeatMap
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  from shapely.geometry import Point
11
- import warnings
 
 
 
 
 
12
  warnings.filterwarnings("ignore")
13
 
14
- # ======================
15
- # 1. CARGA DE DATOS
16
- # ======================
17
- data = pd.read_excel("Vasculitis_Versión_Excel.xlsx")
18
- data.columns = data.columns.str.strip().str.lower()
19
-
20
- # Coordenadas
21
- data = data.rename(columns={
22
- "coordenada de residencia-latitud": "latitud",
23
- "coordenada de residencia-longitud": "longitud"
24
- })
25
-
26
- data = data.dropna(subset=["latitud", "longitud"])
27
- data["geometry"] = data.apply(lambda row: Point(row["longitud"], row["latitud"]), axis=1)
28
- data = gpd.GeoDataFrame(data, geometry="geometry", crs="EPSG:4326")
29
-
30
- # ======================
31
- # 2. GEOLOCALIZACIÓN
32
- # ======================
33
- geo_localidades = gpd.read_file("loca.json")
34
- geo_localidades.columns = geo_localidades.columns.str.lower()
35
- geo_localidades = geo_localidades.rename(columns={"locnombre": "localidad"})
36
- geo_localidades['localidad'] = geo_localidades['localidad'].str.upper()
37
- data = gpd.sjoin(data.to_crs(geo_localidades.crs), geo_localidades[['localidad', 'geometry']], how='left', predicate='within')
38
- data.drop(columns='index_right', inplace=True)
39
-
40
- # ======================
41
- # 3. CAPAS AMBIENTALES
42
- # ======================
43
- def cargar_geojson(path):
44
- gdf = gpd.read_file(path).to_crs("EPSG:4326")
45
- for col in gdf.columns:
46
- if pd.api.types.is_datetime64_any_dtype(gdf[col]):
47
- gdf[col] = gdf[col].astype(str)
48
- return gdf
49
-
50
- capas_ambientales = {
51
- "PM2.5": cargar_geojson("pm25_prom_anual_2023.geojson"),
52
- "Ozono": cargar_geojson("ozono_prom_anual_2022.geojson"),
53
- "Temperatura": cargar_geojson("temp_anualprom_2023.geojson"),
54
- "Precipitación": cargar_geojson("precip_anualacum_2023.geojson"),
55
- "Viento": cargar_geojson("vel_viento_0_23h_anual_2023.geojson"),
56
- "Estaciones de calidad del aire": cargar_geojson("estacion_calidad_aire.geojson")
57
  }
58
 
59
- # ======================
60
- # 4. VARIABLES DERIVADAS
61
- # ======================
62
- data["genero_cat"] = data["género"]
63
- data["edad"] = data["edad en años del paciente"]
64
- data["estrato_cat"] = data["estrato socioeconómico"]
65
- data["anca_cat"] = data["ancas"]
66
- data["mpo_cat"] = data["mpo"]
67
- data["pr3_cat"] = data["pr3"]
68
- data["sindrome_renal"] = data["síndrome renal al ingreso"]
69
- data["manifestaciones_extrarrenales"] = data["manifestaciones extrarenales"]
70
- data["proteinuria"] = data["proteinuria"]
71
- data["creatinina"] = data["creatinina"]
72
-
73
- # Antecedentes
74
- antecedentes = {
75
- "diabetes": "antecedente personal de diabetes",
76
- "falla_cardiaca": "antecedente personal de falla cardíaca",
77
- "epoc": "antecedente personal de epoc",
78
- "hta": "antecedente personal de hipertensión arterial",
79
- "vih": "antecedente personal de vih",
80
- "autoinmune": "antecedente personal de otra enfermedad autoinmune",
81
- "cáncer": "antecedente personal de cáncer"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  }
83
- for clave, col in antecedentes.items():
84
- data[clave] = data[col].map({"Si": 1, "No": 0})
85
-
86
- # Hallazgos histológicos
87
- biopsia_cols = [col for col in data.columns if col.startswith("hallazgos histológicos en biopsia")]
88
- data["biopsia_positiva"] = data[biopsia_cols].apply(lambda row: "Si" if "Checked" in row.values else "No", axis=1)
89
-
90
- data = data.rename(columns={
91
- "hallazgos histológicos en biopsia (choice=sin alteraciones)": "Biopsia_SinAlteraciones",
92
- "hallazgos histológicos en biopsia (choice=presencia de cualquier número de glomérulos con lesiones necrozantes en cualquier fase de desarrollo sin proliferación extracapilar.)": "Biopsia_NecrosisPura",
93
- "hallazgos histológicos en biopsia (choice=presencia de cualquier número de glomérulos con lesiones necrotizantes con proliferación extracapilar en cualquier fase de desarrollo en menos del 50% de los glomérulos (focal).)": "Biopsia_Focal",
94
- "hallazgos histológicos en biopsia (choice=presencia de cualquier número de glomérulos con lesiones necrotizantes con proliferación extracapilar en cualquier fase de desarrollo (clase mixta).)": "Biopsia_Mixta",
95
- "hallazgos histológicos en biopsia (choice=presencia de glomérulonefritis necrotizante con proliferación extracapilar en cualquier fase de desarrollo en más del 50% de los glomérulos (clase crescéntica).)": "Biopsia_Crescentica",
96
- "hallazgos histológicos en biopsia (choice=vasculitis (compromiso de arteriolas o arterias musculares con vasculitis o necrosis fibrinoide) sin compromiso glomerular.)": "Biopsia_VasculitisSinGlom",
97
- "hallazgos histológicos en biopsia (choice=vasculitis (compromiso de arteriolas o arterias musculares con vasculitis o necrosis fibrinoide) con compromiso glomerular.)": "Biopsia_VasculitisConGlom",
98
- "hallazgos histológicos en biopsia (choice=sin dato)": "Biopsia_SinDato"
99
- })
100
-
101
-
102
- # Categorías edad (quinquenios)
103
- bins = list(range(0, 105, 5))
104
- labels = [f"{i}-{i+4}" for i in bins[:-1]]
105
- data["edad_cat"] = pd.cut(data["edad en años del paciente"], bins=bins, labels=labels, right=False)
106
- # ======================
107
- # 5. FUNCIONES FILTRADO Y VISUALIZACIÓN
108
- # ======================
109
- def aplicar_filtros(df, genero, edades, localidades, compromiso_renal, antecedentes_selec):
110
- df_filtrado = df.copy()
111
- if genero != "Todos":
112
- df_filtrado = df_filtrado[df_filtrado["genero_cat"] == genero]
113
- if edades:
114
- df_filtrado = df_filtrado[df_filtrado["edad_cat"].isin(edades)]
115
- if localidades:
116
- df_filtrado = df_filtrado[df_filtrado["localidad"].isin(localidades)]
117
- if compromiso_renal != "Todos":
118
- df_filtrado = df_filtrado[df_filtrado["biopsia_positiva"] == compromiso_renal]
119
- for ant in antecedentes_selec:
120
- if ant in df_filtrado.columns:
121
- df_filtrado = df_filtrado[df_filtrado[ant] == 1]
122
- return df_filtrado
123
-
124
- # Mapa coroplético
125
- def generar_mapa_coropletico(df, capas):
126
- df_grouped = df.groupby('localidad').size().reset_index(name='casos')
127
- geo_local_copy = geo_localidades.merge(df_grouped, on='localidad', how='left').fillna({'casos': 0})
128
- m = folium.Map(location=[4.65, -74.1], zoom_start=11)
129
- folium.Choropleth(
130
- geo_data=geo_local_copy,
131
- name='Casos por localidad',
132
- data=geo_local_copy,
133
- columns=['localidad', 'casos'],
134
- key_on='feature.properties.localidad',
135
- fill_color='YlOrRd',
136
- fill_opacity=0.7,
137
- line_opacity=0.3,
138
- legend_name='Número de casos'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  ).add_to(m)
140
 
141
- for _, row in geo_local_copy.iterrows():
142
- folium.Marker(
143
- location=row['geometry'].centroid.coords[0][::-1],
144
- popup=f"{row['localidad']}: {int(row['casos'])} casos",
145
- icon=folium.Icon(color='blue', icon='info-sign')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  ).add_to(m)
147
 
148
- for _, row in df.iterrows():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  folium.CircleMarker(
150
- location=(row['latitud'], row['longitud']),
151
- radius=4,
152
- popup=f"Edad: {row['edad']}, Género: {row['genero_cat']}, Estrato: {row['estrato_cat']}, Creatinina: {row.get('creatinina', '')}",
153
- color='black', fill=True, fill_opacity=0.6
154
- ).add_to(m)
 
 
 
155
 
156
- for capa in capas:
157
- if capa in capas_ambientales:
158
- gdf = capas_ambientales[capa]
159
- folium.GeoJson(gdf, name=capa, tooltip=folium.GeoJsonTooltip(fields=gdf.columns[:2].tolist())).add_to(m)
160
 
161
- folium.LayerControl().add_to(m)
162
  return m._repr_html_()
163
 
164
- # Mapa calor
165
- def generar_mapa_calor(df):
166
- m = folium.Map(location=[4.65, -74.1], zoom_start=11)
167
- if df.empty:
168
- return m._repr_html_()
169
- heat_data = df[['latitud', 'longitud']].dropna().values.tolist()
170
- HeatMap(heat_data, radius=14).add_to(m)
171
- return m._repr_html_()
172
 
173
- def generar_univariado(var, df):
174
- if df.empty: return go.Figure()
175
- if df[var].dtype == "object":
176
- fig = px.histogram(df, x=var, color=var, text_auto=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
  else:
178
- fig = px.histogram(df, x=var)
179
- fig.update_layout(title=f"Distribución de {var}", xaxis_title=var, yaxis_title="Frecuencia")
180
  return fig
181
 
182
- def generar_bivariado(x, y, df):
183
- if x not in df.columns or y not in df.columns:
184
- return None
185
- if pd.api.types.is_numeric_dtype(df[x]) and pd.api.types.is_numeric_dtype(df[y]):
186
- fig = px.scatter(df, x=x, y=y, title=f"{x} vs {y}")
187
- elif pd.api.types.is_numeric_dtype(df[x]) or pd.api.types.is_numeric_dtype(df[y]):
188
- fig = px.box(df, x=x, y=y, points="all", title=f"{x} vs {y}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  else:
190
- fig = px.histogram(df, x=x, color=y, barmode="group", title=f"{x} vs {y}")
191
- fig.update_layout(height=400)
192
- return fig
 
 
 
 
 
193
 
194
- # ======================
195
- # 6. INTERFAZ GRADIO
196
- # ======================
197
  def interfaz():
198
- with gr.Blocks() as demo:
199
- gr.Markdown("## Estudio Geoespacial: Vasculitis ANCA")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
  with gr.Row():
202
- genero = gr.Dropdown(["Todos", "Femenino", "Masculino"], label="Género", value="Todos")
203
- edad_cat = gr.CheckboxGroup(label="Rango Etario (Quinquenios)", choices=sorted(data["edad_cat"].dropna().unique().astype(str)))
204
-
205
- localidades = gr.Dropdown(label="Localidades", choices=sorted(data["localidad"].dropna().unique()), multiselect=True)
206
- compromiso = gr.Dropdown(["Todos", "Si", "No"], label="Compromiso Renal", value="Todos")
207
- antecedentes_ui = gr.CheckboxGroup(label="Antecedentes", choices=list(antecedentes.keys()))
208
-
209
- with gr.Tab("Mapa Coroplético"):
210
- capas = gr.CheckboxGroup(label="Capas Ambientales", choices=list(capas_ambientales.keys()))
211
- btn = gr.Button("Mostrar")
212
- mapa = gr.HTML()
213
- btn.click(fn=lambda g, e, l, c, a, cap: generar_mapa_coropletico(aplicar_filtros(data,g, e, l, c, a), cap),
214
- inputs=[genero, edad_cat, localidades, compromiso, antecedentes_ui, capas], outputs=mapa)
215
-
216
- with gr.Tab("Mapa de Calor"):
217
- btn2 = gr.Button("Mostrar")
218
- salida = gr.HTML()
219
- btn2.click(fn=lambda g, e, l, c, a: generar_mapa_calor(aplicar_filtros(data, g, e, l, c, a)),
220
- inputs=[genero, edad_cat, localidades, compromiso, antecedentes_ui], outputs=salida)
 
 
 
 
 
221
 
222
  with gr.Tab("Univariado"):
223
- var_uni = gr.Dropdown(label="Variable", choices=["genero_cat", "estrato_cat", "edad_cat", "proteinuria", "sindrome_renal", "manifestaciones_extrarrenales",
224
- "Biopsia_SinAlteraciones",
225
- "Biopsia_NecrosisPura",
226
- "Biopsia_Focal",
227
- "Biopsia_Mixta",
228
- "Biopsia_Crescentica",
229
- "Biopsia_VasculitisSinGlom",
230
- "Biopsia_VasculitisConGlom",
231
- "Biopsia_SinDato"
232
- ])
233
  btn_uni = gr.Button("Graficar")
234
- plot1 = gr.Plot()
235
- btn_uni.click(fn=lambda v, g, e, l, c, a: generar_univariado(v, aplicar_filtros(data, g, e, l, c, a)),
236
- inputs=[var_uni, genero, edad_cat, localidades, compromiso, antecedentes_ui],
237
- outputs=plot1)
 
 
 
238
 
239
  with gr.Tab("Bivariado"):
240
- var_x = gr.Dropdown(label="Variable X", choices=["genero_cat", "estrato_cat", "edad", "edad_cat", "sindrome_renal", "manifestaciones_extrarrenales", "proteinuria", "mpo_cat", "pr3_cat",
241
- "Biopsia_SinAlteraciones",
242
- "Biopsia_NecrosisPura",
243
- "Biopsia_Focal",
244
- "Biopsia_Mixta",
245
- "Biopsia_Crescentica",
246
- "Biopsia_VasculitisSinGlom",
247
- "Biopsia_VasculitisConGlom",
248
- "Biopsia_SinDato"
249
- ])
250
- var_y = gr.Dropdown(label="Variable Y", choices=["edad_cat","genero_cat", "anca_cat", "biopsia_positiva", "proteinuria", "creatinina", "edad", "sindrome_renal",
251
- "Biopsia_SinAlteraciones",
252
- "Biopsia_NecrosisPura",
253
- "Biopsia_Focal",
254
- "Biopsia_Mixta",
255
- "Biopsia_Crescentica",
256
- "Biopsia_VasculitisSinGlom",
257
- "Biopsia_VasculitisConGlom",
258
- "Biopsia_SinDato"
259
- ])
260
  btn_bi = gr.Button("Graficar")
261
- plot2 = gr.Plot()
262
- btn_bi.click(fn=lambda x, y, g, e, l, c, a: generar_bivariado(x, y, aplicar_filtros(data, g, e, l, c, a)),
263
- inputs=[var_x, var_y, genero, edad_cat, localidades, compromiso, antecedentes_ui],
264
- outputs=plot2)
265
-
266
- with gr.Tab("Ayuda"):
267
- gr.Markdown("""
268
- **Instrucciones**
269
- - Selecciona filtros por género, edad, localidad, compromiso renal o antecedentes.
270
- - Visualiza mapas con capas ambientales.
271
- - Explora distribución de variables univariadas y relaciones bivariadas.
272
- - Los hallazgos de biopsia se visualizan como variables categóricas con valores *Checked / Unchecked*.
273
- """)
274
-
275
- demo.launch()
276
-
277
- interfaz()
 
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 = "/content/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
+ # ─── 4. Derivadas y flags ───────────────────────────────
104
+ df["genero_cat"] = df.get("genero","").str.capitalize()
105
+ df["estrato_cat"] = df.get("estrato_socioeconomico","").str.capitalize()
106
+
107
+ df["edad"] = pd.to_numeric(df.get("edad_en_anos_del_paciente","").str.replace(",", "."), errors="coerce")
108
+ bins = list(range(0,105,5))
109
+ labels = [f"{b}-{b+4}" for b in bins[:-1]]
110
+ df["edad_cat"] = pd.cut(df["edad"], bins=bins, labels=labels, right=False)
111
+
112
+ df["anca_cat"] = df.get("ancas")
113
+ df["mpo_cat"] = df.get("mpo")
114
+ df["pr3_cat"] = df.get("pr3")
115
+
116
+ df["sindrome_renal"] = df.get("sindrome_renal_al_ingreso","").str.capitalize()
117
+ df["manifestaciones_extrarenales"] = df.get("manifestaciones_extrarenales","").str.capitalize()
118
+ df["proteinuria"] = df.get("proteinuria","").str.capitalize()
119
+ df["creatinina"] = pd.to_numeric(df.get("creatinina","").str.replace(",", "."), errors="coerce")
120
+
121
+ ante_cols = {
122
+ "diabetes":"antecedente_personal_de_diabetes",
123
+ "falla_cardiaca":"antecedente_personal_de_falla_cardiaca",
124
+ "epoc":"antecedente_personal_de_epoc",
125
+ "hipertension":"antecedente_personal_de_hipertension_arterial",
126
+ "vih":"antecedente_personal_de_vih",
127
+ "autoinmune":"antecedente_personal_de_otra_enfermedad_autoinmune",
128
+ "cancer":"antecedente_personal_de_cancer"
129
+ }
130
+ resumen_ante = {
131
+ "diabetes":"Diabetes",
132
+ "falla_cardiaca":"Falla cardíaca",
133
+ "epoc":"EPOC",
134
+ "hipertension":"Hipertensión",
135
+ "vih":"VIH",
136
+ "autoinmune":"Enf. autoinmune",
137
+ "cancer":"Cáncer"
138
  }
139
+ for key,col in ante_cols.items():
140
+ df[key] = (df.get(col,"0").astype(str).str.lower()
141
+ .map({"si":1,"sí":1,"checked":1,"1":1})
142
+ .fillna(0).astype(int)
143
+ )
144
+
145
+ bio_raw = [c for c in df.columns if c.startswith("hallazgos_histologicos_en_biopsia")]
146
+ ren_bio = {c:f"bio_{i}" for i,c in enumerate(bio_raw,1)}
147
+ df = df.rename(columns=ren_bio)
148
+ bio_cols = list(ren_bio.values())
149
+
150
+ BIO_REGEX = [
151
+ (r"sin_alteraciones$", "Sin alteraciones"),
152
+ (r"sin_proliferacion_extracapilar", "Necrosis sin PC"),
153
+ (r"menos_del_50.*focal", "Focal"),
154
+ (r"clase_mixta", "Mixta"),
155
+ (r"mas_del_50.*cresc", "Crescéntica"),
156
+ (r"sin_compromiso_glomerular$", "Vasculitis sin glom."),
157
+ (r"con_compromiso_glomerular$", "Vasculitis + glom."),
158
+ (r"sin_dato$", "Sin dato")
159
+ ]
160
+ # crear un dict raw_col short
161
+ raw2short = {}
162
+ for patt, short in BIO_REGEX:
163
+ raw = next(c for c in bio_raw if re.search(patt, c))
164
+ raw2short[raw] = short
165
+
166
+ def patron_bio(row):
167
+ for raw, flag in ren_bio.items():
168
+ if str(row[flag]).strip().lower() in ("si","sí","checked","1"):
169
+ return raw2short.get(raw, "Sin dato")
170
+ return "Sin dato"
171
+
172
+ df["biopsia_patron"] = df.apply(patron_bio, axis=1)
173
+ df["biopsia_positiva"] = np.where(df["biopsia_patron"]=="Sin dato","No","Si")
174
+
175
+ # ─── 5. Filtrado ────────────────────────────────────────
176
+ def filtrar(d, gen, edades, locs, renal, ants, bios, anca, mpo, pr3):
177
+ d2 = d.copy()
178
+ if gen!="Todos": d2 = d2[d2["genero_cat"]==gen]
179
+ if edades: d2 = d2[d2["edad_cat"].isin(edades)]
180
+ if locs: d2 = d2[d2["localidad"].isin(locs)]
181
+ if renal!="Todos": d2 = d2[d2["biopsia_positiva"]==renal]
182
+ if bios and bios!=["Todos"]:
183
+ d2 = d2[d2["biopsia_patron"].isin(bios)]
184
+ if anca!="Todos": d2 = d2[d2["anca_cat"]==anca]
185
+ if mpo!="Todos": d2 = d2[d2["mpo_cat"]==mpo]
186
+ if pr3!="Todos": d2 = d2[d2["pr3_cat"]==pr3]
187
+ for ant in ants:
188
+ if ant=="Todos": continue
189
+ key = next(k for k,v in resumen_ante.items() if v==ant)
190
+ d2 = d2[d2[key]==1]
191
+ return d2
192
+
193
+ # ─── 6. Mapas ───────────────────────────────────────────
194
+ def choropleth(m, g, val, title, cmap, zfield, zalias):
195
+ g = g.copy()
196
+ g[val] = pd.to_numeric(g[val], errors="coerce")
197
+ vmin, vmax = g[val].min(), g[val].max()
198
+ cm = cmap.scale(vmin, vmax)
199
+ cm.caption = title
200
+ cm.add_to(m)
201
+
202
+ is_line = g.geometry.iloc[0].geom_type.startswith("Line")
203
+ style = (
204
+ lambda f,vc=val: {"color":cm(f["properties"][vc]),"weight":4,"opacity":0.9}
205
+ ) if is_line else (
206
+ lambda f,vc=val: {"fillColor":cm(f["properties"][vc]),"fillOpacity":0.8,
207
+ "color":"black","weight":0.3}
208
+ )
209
+
210
+ fields = [zfield, val]
211
+ aliases = [zalias, title]
212
+ for extra in ("nombre","rio"):
213
+ if extra in g.columns:
214
+ fields.append(extra); aliases.append("Río"); break
215
+
216
+ folium.GeoJson(
217
+ g, name=title,
218
+ style_function=style,
219
+ highlight_function=lambda f: {"weight":2,"color":"#444","fillOpacity":0.95},
220
+ tooltip=folium.GeoJsonTooltip(fields=fields, aliases=aliases, sticky=True)
221
  ).add_to(m)
222
 
223
+ def capa_clusters(m, d):
224
+ """
225
+ Añade al mapa m una capa de clústeres de pacientes (DBSCAN 1 km),
226
+ con popups que muestran género, edad (si existe), patrón biopsia y antecedentes.
227
+ """
228
+ if d.empty:
229
+ return
230
+ coords = np.radians(d[["latitud", "longitud"]].astype(float))
231
+ if len(coords) < 3:
232
+ return
233
+ labels = DBSCAN(eps=1/6371, min_samples=3, metric="haversine").fit_predict(coords)
234
+ d = d.copy()
235
+ d["cluster"] = labels
236
+
237
+ pal = branca.colormap.linear.Set1_09
238
+ fg = folium.FeatureGroup(name="Clústeres (1 km)", overlay=True)
239
+
240
+ for cl in sorted([c for c in d["cluster"].unique() if c != -1]):
241
+ color = pal(cl / max(1, d["cluster"].nunique() - 1))
242
+ for _, r in d[d["cluster"] == cl].iterrows():
243
+ # Edad segura
244
+ if pd.notna(r["edad"]) and not math.isnan(r["edad"]):
245
+ edad_txt = f"{int(r['edad'])} años"
246
+ else:
247
+ edad_txt = "Sin dato edad"
248
+
249
+ # Antecedentes resumidos
250
+ ant = [v for k, v in resumen_ante.items() if r.get(k) == 1]
251
+ ants_txt = "; ".join(ant) if ant else "Ninguno"
252
+
253
+ popup = (
254
+ f"Clúster #{cl}<br>"
255
+ f"Género: {r['genero_cat']}<br>"
256
+ f"Edad: {edad_txt}<br>"
257
+ f"Biopsia: {r['biopsia_patron']}<br>"
258
+ f"Antecedentes: {ants_txt}"
259
+ )
260
+ folium.CircleMarker(
261
+ location=(r["latitud"], r["longitud"]),
262
+ radius=6,
263
+ color=color,
264
+ fill=True, fill_color=color, fill_opacity=0.9,
265
+ weight=1,
266
+ popup=popup
267
+ ).add_to(fg)
268
+
269
+ fg.add_to(m)
270
+
271
+
272
+ def crear_mapa(d_filt, capas, ver_cluster):
273
+ """
274
+ Construye el mapa completo:
275
+ - coroplético de pacientes por localidad
276
+ - capas ambientales
277
+ - heatmap de puntos
278
+ - marcadores individuales con popups seguros
279
+ - clústeres si ver_cluster=True
280
+ """
281
+ # 1) Coroplético por localidad
282
+ g = d_filt.groupby("localidad").size().reset_index(name="pacientes")
283
+ geo = geo_loc.merge(g, on="localidad", how="left").fillna({"pacientes": 0})
284
+
285
+ m = folium.Map(location=[4.65, -74.1], zoom_start=11, tiles="CartoDB positron")
286
+ choropleth(
287
+ m, geo, "pacientes", "Pacientes por localidad (N)",
288
+ branca.colormap.linear.Reds_09, "localidad", "Localidad"
289
+ )
290
+
291
+ # 2) Capas ambientales
292
+ for capa in capas:
293
+ if capa == "Heatmap pacientes":
294
+ continue
295
+ gdf = caps_amb.get(capa)
296
+ val, uni, cmap, zf, za = META_CAPAS[capa]
297
+ if gdf is not None and val in gdf.columns:
298
+ choropleth(
299
+ m, gdf, val,
300
+ f"{capa}{' ('+uni+')' if uni else ''}",
301
+ cmap, zf, za
302
+ )
303
+
304
+ # 3) Heatmap de puntos
305
+ if "Heatmap pacientes" in capas and not d_filt.empty:
306
+ HeatMap(
307
+ d_filt[["latitud", "longitud"]].astype(float).values,
308
+ radius=18, name="Heatmap pacientes"
309
  ).add_to(m)
310
 
311
+ # 4) Marcadores individuales
312
+ fg_pts = folium.FeatureGroup(name="Puntos pacientes", overlay=True)
313
+ for _, r in d_filt.iterrows():
314
+ # Edad segura
315
+ if pd.notna(r["edad"]) and not math.isnan(r["edad"]):
316
+ edad_txt = f"{int(r['edad'])} años"
317
+ else:
318
+ edad_txt = "Sin dato edad"
319
+
320
+ # Antecedentes resumidos
321
+ ant = [v for k, v in resumen_ante.items() if r.get(k) == 1]
322
+ ants_txt = "<br>".join(ant) if ant else "Ninguno"
323
+
324
+ popup_html = (
325
+ f"Localidad: {r['localidad']}<br>"
326
+ f"Edad: {edad_txt}<br>"
327
+ f"Género: {r['genero_cat']}<br>"
328
+ f"Biopsia: {r['biopsia_patron']}<br>"
329
+ f"Antecedentes:<br>{ants_txt}"
330
+ )
331
  folium.CircleMarker(
332
+ location=(r["latitud"], r["longitud"]),
333
+ radius=5,
334
+ color="#c00",
335
+ fill=True, fill_color="white",
336
+ fill_opacity=0.85, weight=1,
337
+ popup=popup_html
338
+ ).add_to(fg_pts)
339
+ fg_pts.add_to(m)
340
 
341
+ # 5) Capa de clústeres opcional
342
+ if ver_cluster:
343
+ capa_clusters(m, d_filt)
 
344
 
345
+ folium.LayerControl(collapsed=False).add_to(m)
346
  return m._repr_html_()
347
 
348
+ # ─── 7. Gráficos ─────────────────────────────────────────
349
+ def col_of(v):
350
+ """Mapea nombre legible a columna interna."""
351
+ if v in resumen_ante.values():
352
+ return next(k for k,val in resumen_ante.items() if val==v)
353
+ if v in raw2short.values() or v=="Patrón biopsia":
354
+ return "biopsia_patron"
355
+ return v
356
 
357
+ def g_uni(var, d):
358
+ if d.empty:
359
+ return go.Figure()
360
+ col = col_of(var)
361
+ # 1) Flags de antecedentes (0/1) → barras de conteo "No"/"Si"
362
+ if var in resumen_ante.values():
363
+ s = d[col].map({0:"No",1:"Si"})
364
+ fig = px.histogram(s, x=s,
365
+ category_orders={col:["No","Si"]},
366
+ text_auto=True,
367
+ title=var)
368
+ # 2) Patrón biopsia → barras de conteo de cada categoría
369
+ elif var=="Patrón biopsia" or var in raw2short.values():
370
+ fig = px.histogram(d, x="biopsia_patron",
371
+ category_orders={"biopsia_patron": list(raw2short.values())},
372
+ text_auto=True,
373
+ title="Patrón biopsia")
374
+ # 3) Variables numéricas → histograma
375
+ elif d[col].dtype.kind in "if":
376
+ fig = px.histogram(d, x=col, nbins=20, title=var)
377
+ # 4) Resto categóricas → barras de conteo con color
378
  else:
379
+ fig = px.histogram(d, x=col, color=col, text_auto=True, title=var)
380
+ fig.update_layout(bargap=0.1)
381
  return fig
382
 
383
+ def g_bi(x, y, d):
384
+ """
385
+ Gráfico bivariado:
386
+ - num vs num → scatter con trendline
387
+ - num vs cat → boxplot
388
+ - cat vs cat → barras agrupadas
389
+ Reconoce correctamente:
390
+ • Patrones de biopsia (incluida la etiqueta "Patrón biopsia")
391
+ • Etiquetas de antecedentes.
392
+ """
393
+ if d.empty:
394
+ return go.Figure()
395
+
396
+ # Mapeo de la variable de UI al nombre real de columna en df
397
+ def map_var(v):
398
+ # Dropdown de patrón de biopsia (UI) → columna biop_patron
399
+ if v == "Patrón biopsia":
400
+ return "biopsia_patron"
401
+ # Cualquier etiqueta corta de biopsia
402
+ if v in resumen_bio_map.values():
403
+ return "biopsia_patron"
404
+ # Etiqueta de antecedente → nombre de flag en df
405
+ for key, lab in resumen_ante.items():
406
+ if v == lab:
407
+ return key
408
+ # Variables numéricas o de texto sin transformar
409
+ return v
410
+
411
+ cx = map_var(x)
412
+ cy = map_var(y)
413
+
414
+ # Determinar si cada una es categórica (flags, biopsia o texto)
415
+ is_cat = {}
416
+ for var in (cx, cy):
417
+ is_cat[var] = (
418
+ var == "biopsia_patron"
419
+ or var in resumen_ante.keys()
420
+ or d[var].dtype == object
421
+ )
422
+
423
+ # 1) cat vs cat → histograma agrupado
424
+ if is_cat[cx] and is_cat[cy]:
425
+ fig = px.histogram(
426
+ d,
427
+ x=cx,
428
+ color=cy,
429
+ barmode="group",
430
+ category_orders={
431
+ cx: list(resumen_bio_map.values()) if cx=="biopsia_patron" else list(resumen_ante.values()),
432
+ cy: list(resumen_bio_map.values()) if cy=="biopsia_patron" else list(resumen_ante.values()),
433
+ },
434
+ labels={cx: x, cy: y},
435
+ title=f"{x} vs {y}"
436
+ )
437
+
438
+ # 2) num vs cat → boxplot
439
+ elif is_cat[cx] ^ is_cat[cy]:
440
+ # uno es categórico, otro numérico
441
+ if is_cat[cx]:
442
+ fig = px.box(
443
+ d,
444
+ x=cx,
445
+ y=cy,
446
+ points="all",
447
+ category_orders={cx: list(resumen_bio_map.values()) if cx=="biopsia_patron" else list(resumen_ante.values())},
448
+ labels={cx: x, cy: y},
449
+ title=f"{x} vs {y}"
450
+ )
451
+ else:
452
+ fig = px.box(
453
+ d,
454
+ x=cy,
455
+ y=cx,
456
+ points="all",
457
+ category_orders={cy: list(resumen_bio_map.values()) if cy=="biopsia_patron" else list(resumen_ante.values())},
458
+ labels={cx: x, cy: y},
459
+ title=f"{x} vs {y}"
460
+ )
461
+
462
+ # 3) num vs num → scatter + trendline
463
  else:
464
+ fig = px.scatter(
465
+ d,
466
+ x=cx,
467
+ y=cy,
468
+ trendline="ols",
469
+ labels={cx: x, cy: y},
470
+ title=f"{x} vs {y}"
471
+ )
472
 
473
+ fig.update_layout(bargap=0.1)
474
+ return fig
475
+ # ─── 8. Interfaz Gradio ───────────────────────────────────
476
  def interfaz():
477
+ gen = ["Todos"] + sorted(df["genero_cat"].dropna().unique())
478
+ ages = sorted(df["edad_cat"].dropna().unique())
479
+ locs = sorted(df["localidad"].dropna().unique())
480
+ ancas = ["Todos"] + sorted(df["anca_cat"].dropna().unique())
481
+ mpos = ["Todos"] + sorted(df["mpo_cat"].dropna().unique())
482
+ pr3s = ["Todos"] + sorted(df["pr3_cat"].dropna().unique())
483
+
484
+ vars_cat = [
485
+ "genero_cat","estrato_cat","edad_cat","sindrome_renal",
486
+ "manifestaciones_extrarenales","proteinuria",
487
+ "anca_cat","mpo_cat","pr3_cat"
488
+ ] + ["Patrón biopsia"] + list(resumen_ante.values())
489
+ vars_num = ["edad","creatinina"]
490
+ vars_all = vars_cat + vars_num
491
+
492
+ with gr.Blocks(title="Vasculitis ANCA Bogotá") as demo:
493
+ gr.Markdown("## Explorador geoespacial – Vasculitis ANCA (Bogotá)")
494
 
495
  with gr.Row():
496
+ ui_gen = gr.Dropdown(gen, label="Género", value="Todos")
497
+ ui_age = gr.CheckboxGroup(ages, label="Edad (quinquenios)")
498
+ ui_loc = gr.Dropdown(locs, multiselect=True, label="Localidades")
499
+ ui_renal = gr.Dropdown(["Todos","Si","No"], value="Todos", label="Compromiso renal")
500
+ ui_ant = gr.CheckboxGroup(["Todos"]+list(resumen_ante.values()), label="Antecedentes")
501
+ ui_bio = gr.CheckboxGroup(["Todos"]+list(raw2short.values()), label="Patrón biopsia")
502
+ with gr.Row():
503
+ ui_anca = gr.Dropdown(ancas, label="ANCA", value="Todos")
504
+ ui_mpo = gr.Dropdown(mpos, label="MPO", value="Todos")
505
+ ui_pr3 = gr.Dropdown(pr3s, label="PR3", value="Todos")
506
+
507
+ ui_capas = gr.CheckboxGroup(list(GEO_AMBIENTALES.keys()), label="Capas mapa")
508
+ ui_clu = gr.Checkbox(label="Mostrar clústeres (1 km)")
509
+
510
+ with gr.Tab("Mapa"):
511
+ btn_map = gr.Button("Generar mapa")
512
+ out_map = gr.HTML()
513
+ btn_map.click(
514
+ lambda *i: crear_mapa(filtrar(df,*i[:-2]), i[-2], i[-1]),
515
+ inputs=[ui_gen,ui_age,ui_loc,ui_renal,
516
+ ui_ant,ui_bio,ui_anca,ui_mpo,ui_pr3,
517
+ ui_capas,ui_clu],
518
+ outputs=out_map
519
+ )
520
 
521
  with gr.Tab("Univariado"):
522
+ ui_var = gr.Dropdown(vars_all, label="Variable")
 
 
 
 
 
 
 
 
 
523
  btn_uni = gr.Button("Graficar")
524
+ out_uni = gr.Plot()
525
+ btn_uni.click(
526
+ lambda v,*i: g_uni(v, filtrar(df,*i)),
527
+ inputs=[ui_var,ui_gen,ui_age,ui_loc,ui_renal,
528
+ ui_ant,ui_bio,ui_anca,ui_mpo,ui_pr3],
529
+ outputs=out_uni
530
+ )
531
 
532
  with gr.Tab("Bivariado"):
533
+ ui_x = gr.Dropdown(vars_all, label="Variable X")
534
+ ui_y = gr.Dropdown(vars_all, label="Variable Y")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
535
  btn_bi = gr.Button("Graficar")
536
+ out_bi = gr.Plot()
537
+ btn_bi.click(
538
+ lambda x,y,*i: g_bi(x,y, filtrar(df,*i)),
539
+ inputs=[ui_x,ui_y,ui_gen,ui_age,ui_loc,ui_renal,
540
+ ui_ant,ui_bio,ui_anca,ui_mpo,ui_pr3],
541
+ outputs=out_bi
542
+ )
543
+
544
+ demo.launch(debug=True)
545
+
546
+ if __name__ == "__main__":
547
+ interfaz()