jcalbornoz commited on
Commit
001054b
·
verified ·
1 Parent(s): 729b57b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +31 -65
app.py CHANGED
@@ -6,12 +6,12 @@ import sqlite3
6
  import os
7
  import numpy as np
8
  from datetime import datetime, timedelta
9
- from urllib.parse import urlparse, urlunparse
10
 
11
  # --- 1. CONFIGURACIÓN ---
12
  API_KEY = os.getenv("GOOGLE_API_KEY")
13
  SEARCH_ENGINE_ID = os.getenv("SEARCH_ENGINE_ID")
14
- DB_NAME = "data_cache_v13.db" # Nueva DB para limpiar filtros anteriores
15
 
16
  # --- 2. GESTIÓN DE BASE DE DATOS ---
17
  def iniciar_db():
@@ -51,26 +51,16 @@ def leer_cache(query):
51
  # --- 3. EXTRACCIÓN Y VALIDACIÓN ---
52
 
53
  def analizar_tipo_url(url):
54
- """
55
- Retorna 1 si es INMUEBLE DIRECTO, 0 si es LISTADO/BUSQUEDA.
56
- """
57
  url = url.lower()
58
-
59
- # 1. Indicadores de Inmueble Único (Fuerte)
60
  positivos = ['/inmueble/', '/proyecto/', '/propiedad/', 'detalle', 'p-', 'id-', 'cod-', 'mco-', 'mla-']
61
  if any(p in url for p in positivos): return 1
62
-
63
- # 2. Indicadores de Listado (Fuerte)
64
  negativos = ['listado', 'resultados', 'buscar', 'search', 'ordenar', 'filtrar', 'page']
65
  if any(n in url for n in negativos): return 0
66
-
67
- # 3. Casos neutros (Asumimos listado por precaución)
68
  return 0
69
 
70
  def limpiar_url(url):
71
  try:
72
  parsed = urlparse(url)
73
- # Quitamos query params para intentar limpiar redirecciones
74
  clean = urlunparse((parsed.scheme, parsed.netloc, parsed.path, '', '', ''))
75
  return clean
76
  except:
@@ -98,7 +88,6 @@ def extraer_coordenadas(item):
98
  def parsear_texto_completo(texto):
99
  texto = texto.lower()
100
 
101
- # Precio
102
  precio = 0
103
  match_precio = re.search(r'\$\s?([\d.,]+)', texto)
104
  if match_precio:
@@ -106,7 +95,6 @@ def parsear_texto_completo(texto):
106
  try: precio = float(s)
107
  except: pass
108
 
109
- # Área
110
  area = 0
111
  match_area = re.search(r'(\d+[\.,]?\d*)\s?(m2|mt|mts|metro)', texto)
112
  if match_area:
@@ -114,21 +102,18 @@ def parsear_texto_completo(texto):
114
  try: area = float(s_area)
115
  except: pass
116
 
117
- # Habitaciones
118
  habs = 0
119
  match_habs = re.search(r'(\d+)\s?(hab|alcoba|dormitorio)', texto)
120
  if match_habs:
121
  try: habs = int(match_habs.group(1))
122
  except: pass
123
 
124
- # Baños
125
  banos = 0
126
  match_banos = re.search(r'(\d+)\s?(baño|bano)', texto)
127
  if match_banos:
128
  try: banos = int(match_banos.group(1))
129
  except: pass
130
 
131
- # Garajes
132
  garajes = 0
133
  match_garaje = re.search(r'(\d+)\s?(parqueadero|garaje)', texto)
134
  if match_garaje:
@@ -137,14 +122,12 @@ def parsear_texto_completo(texto):
137
  elif "garaje" in texto or "parqueadero" in texto:
138
  garajes = 1
139
 
140
- # Estrato
141
  estrato = 0
142
  match_estrato = re.search(r'estrato\s?:?\s?(\d)', texto)
143
  if match_estrato:
144
  try: estrato = int(match_estrato.group(1))
145
  except: pass
146
 
147
- # Antigüedad
148
  antiguedad = -1
149
  if "estrenar" in texto or "nuevo" in texto or "sobre planos" in texto:
150
  antiguedad = 0
@@ -154,7 +137,6 @@ def parsear_texto_completo(texto):
154
  try: antiguedad = int(match_anos.group(1))
155
  except: pass
156
 
157
- # Seguridad
158
  seguridad = 0
159
  if any(k in texto for k in ['conjunto', 'vigilancia', 'porteria', 'seguridad', 'club house', 'cerrado']):
160
  seguridad = 1
@@ -166,8 +148,6 @@ def buscar_google(query):
166
  if not API_KEY or not SEARCH_ENGINE_ID: return []
167
  url = "https://www.googleapis.com/customsearch/v1"
168
 
169
- # Quitamos filtros negativos (-listado) porque estaban bloqueando todo.
170
- # Usamos "detalle" como sugerencia suave.
171
  query_optimizada = f"{query} detalle"
172
  query_optimizada = query_optimizada.replace(",", " OR ")
173
 
@@ -184,11 +164,7 @@ def buscar_google(query):
184
  if 'items' in data:
185
  for item in data['items']:
186
  raw_link = item.get('link', '')
187
-
188
- # Clasificamos la URL (1=Directo, 0=Listado)
189
- # NOTA: Ya no filtramos (continue), aceptamos ambos.
190
  es_directo = analizar_tipo_url(raw_link)
191
-
192
  final_link = limpiar_url(raw_link)
193
 
194
  texto = f"{item.get('title')} {item.get('snippet')}"
@@ -200,7 +176,6 @@ def buscar_google(query):
200
  elif "metrocuadrado" in raw_link: fuente = "Metrocuadrado"
201
  elif "wasi" in raw_link: fuente = "Wasi"
202
 
203
- # Guardamos si encontramos datos válidos, aunque el link sea un listado
204
  if precio > 0 or area > 0:
205
  resultados.append({
206
  'titulo': item.get('title'),
@@ -219,34 +194,28 @@ def calcular_scores(df, p_ref, a_ref, h_ref, b_ref, g_ref, e_ref, antiguedad_ref
219
  if df.empty: return df
220
  df_f = df.copy()
221
 
222
- # 1. Precio (25%)
223
  df_f['diff_p'] = abs(df_f['precio'] - p_ref) / p_ref
224
  score_p = np.maximum(0, 1 - df_f['diff_p'])
225
 
226
- # 2. Área (20%)
227
  df_f['diff_a'] = df_f['area'].apply(lambda x: abs(x - a_ref)/a_ref if x > 0 else 1.0)
228
  score_a = np.maximum(0, 1 - df_f['diff_a'])
229
 
230
- # 3. Habitaciones (10%)
231
  score_h = df_f['habs'].apply(lambda x: 1.0 if x == h_ref else (0.9 if x==0 else (0.5 if abs(x-h_ref)<=1 else 0)))
232
-
233
- # 4. Baños (10%)
234
  score_b = df_f['banos'].apply(lambda x: 1.0 if x == b_ref else (0.9 if x==0 else (0.6 if abs(x-b_ref)<=1 else 0.2)))
235
 
236
- # 5. Garajes (10%)
237
  score_g = df_f['garajes'].apply(lambda x: 1.0 if x >= g_ref else (0.5 if x < g_ref and x > 0 else 0.8 if x==0 else 0))
238
-
239
- # 6. Estrato (10%)
240
  score_e = df_f['estrato'].apply(lambda x: 1.0 if x == e_ref else (0.9 if x==0 else (0.5 if abs(x-e_ref)<=1 else 0)))
241
 
242
- # 7. Antigüedad (10%)
243
  def calc_edad(x, ref):
244
  if x == -1: return 0.8
245
  if ref == 0: return 1.0 if x == 0 else max(0, 1 - (x/20))
246
  return max(0, 1 - (abs(x - ref) / 20))
247
  score_ant = df_f['antiguedad'].apply(lambda x: calc_edad(x, antiguedad_ref))
248
 
249
- # 8. Condominio (5%)
250
  def calc_condo(x, quiere_condo):
251
  if not quiere_condo: return 1.0
252
  return 1.0 if x == 1 else 0.2
@@ -284,13 +253,8 @@ def motor(zona, tipo, precio, area, habs, banos, garajes, estrato, antiguedad, e
284
  margin-bottom: 12px;
285
  box-shadow: 0 2px 4px rgba(0,0,0,0.05);
286
  }
287
- .btn-direct {
288
- color: #2563eb !important; /* Azul */
289
- }
290
- .btn-list {
291
- color: #d97706 !important; /* Naranja */
292
- font-style: italic;
293
- }
294
  .map-btn {
295
  background-color: #ea4335 !important;
296
  color: white !important;
@@ -300,6 +264,15 @@ def motor(zona, tipo, precio, area, habs, banos, garajes, estrato, antiguedad, e
300
  display: inline-block;
301
  margin-top: 5px;
302
  }
 
 
 
 
 
 
 
 
 
303
  </style>
304
  <div class="result-container">
305
  """
@@ -313,35 +286,23 @@ def motor(zona, tipo, precio, area, habs, banos, garajes, estrato, antiguedad, e
313
  if df is None:
314
  origen = "🌐 Google API"
315
  lista = buscar_google(q)
316
-
317
  if lista and "error" in lista[0]:
318
  return f"{css_injection}<h3 style='color:red !important;'>⚠️ Límite de Cuota Excedido.</h3></div>"
319
-
320
  if lista:
321
  guardar_cache(q, lista)
322
  df = pd.DataFrame(lista)
323
 
324
  if df is None or df.empty:
325
- return f"{css_injection}<h3>❌ No se encontraron datos.</h3><p>Google no devolvió ningún resultado con precio/área legible.</p></div>"
326
 
327
  df_similares = calcular_scores(df, precio, area, habs, banos, garajes, estrato, antiguedad, es_condominio)
328
 
329
- # CÁLCULO PROMEDIOS BLINDADO
330
- # Filtro: Score decente, área > 10, precio > 1 Millón
331
- df_calc = df_similares[
332
- (df_similares['score'] >= 40) &
333
- (df_similares['area'] > 10) &
334
- (df_similares['precio'] > 1000000)
335
- ]
336
 
337
- if df_calc.empty and not df_similares.empty:
338
- df_calc = df_similares # Si filtro estricto falla, usar todo lo disponible
339
-
340
  prom_precio = df_calc['precio'].mean() if not df_calc.empty else 0
341
-
342
  if not df_calc.empty:
343
- # Calcular precio por metro cuadrado individualmente y luego promediar
344
- # Esto evita errores de sumar áreas dispares
345
  df_calc['m2_individual'] = df_calc['precio'] / df_calc['area']
346
  prom_m2 = df_calc['m2_individual'].mean()
347
  else:
@@ -367,11 +328,17 @@ def motor(zona, tipo, precio, area, habs, banos, garajes, estrato, antiguedad, e
367
 
368
  txt_area = f"{row['area']} m²" if row['area'] > 0 else "N/A"
369
 
370
- # Botones
371
- btn_mapa = ""
372
  if row['lat'] and row['lon']:
 
373
  gmaps_link = f"https://www.google.com/maps/search/?api=1&query={row['lat']},{row['lon']}"
374
- btn_mapa = f"<a href='{gmaps_link}' target='_blank' class='map-btn white-text'>📍 Ver en Mapa</a>"
 
 
 
 
 
 
375
 
376
  if row['es_directo'] == 1:
377
  btn_link = f"<a href='{row['url']}' target='_blank' class='btn-direct'>🔗 Ver Inmueble</a>"
@@ -423,8 +390,7 @@ def motor(zona, tipo, precio, area, habs, banos, garajes, estrato, antiguedad, e
423
  iniciar_db()
424
 
425
  with gr.Blocks(theme=gr.themes.Base()) as demo:
426
- gr.Markdown("# 🏢 Valuador Híbrido V13")
427
- gr.Markdown("Muestra inmuebles específicos y listados de referencia si no hay directos.")
428
 
429
  with gr.Row():
430
  with gr.Column(scale=2):
 
6
  import os
7
  import numpy as np
8
  from datetime import datetime, timedelta
9
+ from urllib.parse import urlparse, urlunparse, quote
10
 
11
  # --- 1. CONFIGURACIÓN ---
12
  API_KEY = os.getenv("GOOGLE_API_KEY")
13
  SEARCH_ENGINE_ID = os.getenv("SEARCH_ENGINE_ID")
14
+ DB_NAME = "data_cache_v14.db"
15
 
16
  # --- 2. GESTIÓN DE BASE DE DATOS ---
17
  def iniciar_db():
 
51
  # --- 3. EXTRACCIÓN Y VALIDACIÓN ---
52
 
53
  def analizar_tipo_url(url):
 
 
 
54
  url = url.lower()
 
 
55
  positivos = ['/inmueble/', '/proyecto/', '/propiedad/', 'detalle', 'p-', 'id-', 'cod-', 'mco-', 'mla-']
56
  if any(p in url for p in positivos): return 1
 
 
57
  negativos = ['listado', 'resultados', 'buscar', 'search', 'ordenar', 'filtrar', 'page']
58
  if any(n in url for n in negativos): return 0
 
 
59
  return 0
60
 
61
  def limpiar_url(url):
62
  try:
63
  parsed = urlparse(url)
 
64
  clean = urlunparse((parsed.scheme, parsed.netloc, parsed.path, '', '', ''))
65
  return clean
66
  except:
 
88
  def parsear_texto_completo(texto):
89
  texto = texto.lower()
90
 
 
91
  precio = 0
92
  match_precio = re.search(r'\$\s?([\d.,]+)', texto)
93
  if match_precio:
 
95
  try: precio = float(s)
96
  except: pass
97
 
 
98
  area = 0
99
  match_area = re.search(r'(\d+[\.,]?\d*)\s?(m2|mt|mts|metro)', texto)
100
  if match_area:
 
102
  try: area = float(s_area)
103
  except: pass
104
 
 
105
  habs = 0
106
  match_habs = re.search(r'(\d+)\s?(hab|alcoba|dormitorio)', texto)
107
  if match_habs:
108
  try: habs = int(match_habs.group(1))
109
  except: pass
110
 
 
111
  banos = 0
112
  match_banos = re.search(r'(\d+)\s?(baño|bano)', texto)
113
  if match_banos:
114
  try: banos = int(match_banos.group(1))
115
  except: pass
116
 
 
117
  garajes = 0
118
  match_garaje = re.search(r'(\d+)\s?(parqueadero|garaje)', texto)
119
  if match_garaje:
 
122
  elif "garaje" in texto or "parqueadero" in texto:
123
  garajes = 1
124
 
 
125
  estrato = 0
126
  match_estrato = re.search(r'estrato\s?:?\s?(\d)', texto)
127
  if match_estrato:
128
  try: estrato = int(match_estrato.group(1))
129
  except: pass
130
 
 
131
  antiguedad = -1
132
  if "estrenar" in texto or "nuevo" in texto or "sobre planos" in texto:
133
  antiguedad = 0
 
137
  try: antiguedad = int(match_anos.group(1))
138
  except: pass
139
 
 
140
  seguridad = 0
141
  if any(k in texto for k in ['conjunto', 'vigilancia', 'porteria', 'seguridad', 'club house', 'cerrado']):
142
  seguridad = 1
 
148
  if not API_KEY or not SEARCH_ENGINE_ID: return []
149
  url = "https://www.googleapis.com/customsearch/v1"
150
 
 
 
151
  query_optimizada = f"{query} detalle"
152
  query_optimizada = query_optimizada.replace(",", " OR ")
153
 
 
164
  if 'items' in data:
165
  for item in data['items']:
166
  raw_link = item.get('link', '')
 
 
 
167
  es_directo = analizar_tipo_url(raw_link)
 
168
  final_link = limpiar_url(raw_link)
169
 
170
  texto = f"{item.get('title')} {item.get('snippet')}"
 
176
  elif "metrocuadrado" in raw_link: fuente = "Metrocuadrado"
177
  elif "wasi" in raw_link: fuente = "Wasi"
178
 
 
179
  if precio > 0 or area > 0:
180
  resultados.append({
181
  'titulo': item.get('title'),
 
194
  if df.empty: return df
195
  df_f = df.copy()
196
 
197
+ # Precios y Áreas
198
  df_f['diff_p'] = abs(df_f['precio'] - p_ref) / p_ref
199
  score_p = np.maximum(0, 1 - df_f['diff_p'])
200
 
 
201
  df_f['diff_a'] = df_f['area'].apply(lambda x: abs(x - a_ref)/a_ref if x > 0 else 1.0)
202
  score_a = np.maximum(0, 1 - df_f['diff_a'])
203
 
204
+ # Habitaciones/Baños
205
  score_h = df_f['habs'].apply(lambda x: 1.0 if x == h_ref else (0.9 if x==0 else (0.5 if abs(x-h_ref)<=1 else 0)))
 
 
206
  score_b = df_f['banos'].apply(lambda x: 1.0 if x == b_ref else (0.9 if x==0 else (0.6 if abs(x-b_ref)<=1 else 0.2)))
207
 
208
+ # Garajes/Estrato
209
  score_g = df_f['garajes'].apply(lambda x: 1.0 if x >= g_ref else (0.5 if x < g_ref and x > 0 else 0.8 if x==0 else 0))
 
 
210
  score_e = df_f['estrato'].apply(lambda x: 1.0 if x == e_ref else (0.9 if x==0 else (0.5 if abs(x-e_ref)<=1 else 0)))
211
 
212
+ # Antigüedad/Condominio
213
  def calc_edad(x, ref):
214
  if x == -1: return 0.8
215
  if ref == 0: return 1.0 if x == 0 else max(0, 1 - (x/20))
216
  return max(0, 1 - (abs(x - ref) / 20))
217
  score_ant = df_f['antiguedad'].apply(lambda x: calc_edad(x, antiguedad_ref))
218
 
 
219
  def calc_condo(x, quiere_condo):
220
  if not quiere_condo: return 1.0
221
  return 1.0 if x == 1 else 0.2
 
253
  margin-bottom: 12px;
254
  box-shadow: 0 2px 4px rgba(0,0,0,0.05);
255
  }
256
+ .btn-direct { color: #2563eb !important; }
257
+ .btn-list { color: #d97706 !important; font-style: italic; }
 
 
 
 
 
258
  .map-btn {
259
  background-color: #ea4335 !important;
260
  color: white !important;
 
264
  display: inline-block;
265
  margin-top: 5px;
266
  }
267
+ .map-btn-gray {
268
+ background-color: #5f6368 !important;
269
+ color: white !important;
270
+ padding: 4px 10px;
271
+ border-radius: 15px;
272
+ font-size: 0.8em;
273
+ display: inline-block;
274
+ margin-top: 5px;
275
+ }
276
  </style>
277
  <div class="result-container">
278
  """
 
286
  if df is None:
287
  origen = "🌐 Google API"
288
  lista = buscar_google(q)
 
289
  if lista and "error" in lista[0]:
290
  return f"{css_injection}<h3 style='color:red !important;'>⚠️ Límite de Cuota Excedido.</h3></div>"
 
291
  if lista:
292
  guardar_cache(q, lista)
293
  df = pd.DataFrame(lista)
294
 
295
  if df is None or df.empty:
296
+ return f"{css_injection}<h3>❌ No se encontraron datos válidos.</h3></div>"
297
 
298
  df_similares = calcular_scores(df, precio, area, habs, banos, garajes, estrato, antiguedad, es_condominio)
299
 
300
+ # Matemáticas Blindadas
301
+ df_calc = df_similares[(df_similares['score'] >= 40) & (df_similares['area'] > 10) & (df_similares['precio'] > 1000000)]
302
+ if df_calc.empty: df_calc = df_similares[df_similares['area'] > 10]
 
 
 
 
303
 
 
 
 
304
  prom_precio = df_calc['precio'].mean() if not df_calc.empty else 0
 
305
  if not df_calc.empty:
 
 
306
  df_calc['m2_individual'] = df_calc['precio'] / df_calc['area']
307
  prom_m2 = df_calc['m2_individual'].mean()
308
  else:
 
328
 
329
  txt_area = f"{row['area']} m²" if row['area'] > 0 else "N/A"
330
 
331
+ # --- LÓGICA DE MAPA INTELIGENTE ---
 
332
  if row['lat'] and row['lon']:
333
+ # Coordenadas exactas encontradas
334
  gmaps_link = f"https://www.google.com/maps/search/?api=1&query={row['lat']},{row['lon']}"
335
+ btn_mapa = f"<a href='{gmaps_link}' target='_blank' class='map-btn white-text'>📍 Ver Ubicación Exacta</a>"
336
+ else:
337
+ # Búsqueda Inversa por Título (Fallback)
338
+ titulo_safe = quote(f"{row['titulo']} {zona}")
339
+ gmaps_link = f"https://www.google.com/maps/search/?api=1&query={titulo_safe}"
340
+ btn_mapa = f"<a href='{gmaps_link}' target='_blank' class='map-btn-gray white-text'>📍 Buscar en Mapa</a>"
341
+ # ----------------------------------
342
 
343
  if row['es_directo'] == 1:
344
  btn_link = f"<a href='{row['url']}' target='_blank' class='btn-direct'>🔗 Ver Inmueble</a>"
 
390
  iniciar_db()
391
 
392
  with gr.Blocks(theme=gr.themes.Base()) as demo:
393
+ gr.Markdown("# 🏢 Valuador Inteligente V14 (Mapa Activo)")
 
394
 
395
  with gr.Row():
396
  with gr.Column(scale=2):