brunaaaz commited on
Commit
628cd64
·
verified ·
1 Parent(s): 825b247

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +209 -200
app.py CHANGED
@@ -6,14 +6,14 @@ import folium
6
  from streamlit_folium import st_folium
7
  from folium.plugins import MarkerCluster
8
  from sklearn.ensemble import RandomForestRegressor
9
- from sklearn.cluster import KMeans
10
  from wordcloud import WordCloud
11
  import matplotlib.pyplot as plt
12
  import unicodedata
13
 
14
- # --- CONFIGURAÇÃO E ESTILO DA PÁGINA ---
15
  st.set_page_config(page_title="Airbnb Intelligence Pro", layout="wide", page_icon="🏘️")
16
 
 
17
  st.markdown("""
18
  <style>
19
  .metric-card {
@@ -23,184 +23,157 @@ st.markdown("""
23
  border-radius: 5px;
24
  box-shadow: 1px 1px 3px rgba(0,0,0,0.1);
25
  }
26
- h1, h2, h3 { color: #2c3e50; }
27
  .stExpander { border: 1px solid #ddd; border-radius: 5px; background-color: #ffffff; }
28
  </style>
29
  """, unsafe_allow_html=True)
30
 
31
- # --- FUNÇÕES UTILITÁRIAS ---
32
-
33
  def normalizar_texto(texto):
34
- """Remove acentos e minúsculo para garantir merge correto (Ex: Floriánópolis -> florianopolis)."""
35
  if pd.isna(texto): return ""
36
  nfkd = unicodedata.normalize('NFKD', str(texto))
37
  return u"".join([c for c in nfkd if not unicodedata.combining(c)]).lower().strip()
38
 
39
- # --- CARREGAMENTO E LIMPEZA DE DADOS (CACHING) ---
40
  @st.cache_data
41
  def load_data():
42
- """Carrega, limpa e calcula os KPIs financeiros a partir de todos os CSVs."""
43
  try:
 
44
  df = pd.read_csv('lisitng_geral.csv', sep=';', encoding='utf-8')
45
  fipe = pd.read_csv('fipezap_geral.csv', sep=';', encoding='utf-8')
46
  past = pd.read_csv('past_geral.csv', sep=';', encoding='utf-8')
47
  seg = pd.read_csv('casos_homicidios.csv', sep=';', encoding='utf-8')
48
  except FileNotFoundError:
49
- st.error("ERRO CRÍTICO: Arquivos CSV não encontrados.")
50
  st.stop()
51
 
52
- # 1. Limpeza Crítica de Coordenadas (CORREÇÃO BH/Curitiba/POA)
 
53
  cols_coords = ['latitude', 'longitude']
54
  for col in cols_coords:
55
  if df[col].dtype == 'object':
56
- df[col] = df[col].astype(str).str.replace(',', '.').astype(float)
57
- df = df.dropna(subset=cols_coords)
58
 
59
- # 2. Limpeza e Normalização de Finanças/Cidades
 
 
 
 
60
  cols_fin = ['ttm_revenue_native', 'ttm_avg_rate_native', 'ttm_occupancy', 'rating_overall']
61
  for col in cols_fin:
62
- if df[col].dtype == 'object':
63
  df[col] = df[col].astype(str).str.replace('R$', '').str.replace('.', '').str.replace(',', '.').astype(float)
64
-
 
65
  df['city_norm'] = df['city'].apply(normalizar_texto)
66
- df['city'] = df['city'].str.title()
 
 
67
  df['bedrooms'] = df['bedrooms'].fillna(df.groupby(['city', 'room_type'])['bedrooms'].transform('median')).fillna(1)
68
-
69
- # 3. Cálculo de ROI (Cruzando FIPEZAP)
 
 
 
70
  fipe['city_norm'] = fipe['city'].apply(normalizar_texto)
 
 
71
  media_cidade = fipe.groupby('city_norm')['preco_m2'].mean().to_dict()
72
- df['preco_m2_estimado'] = df['city_norm'].map(media_cidade).fillna(fipe['preco_m2'].mean())
73
- df['metragem_estimada'] = df['bedrooms'].apply(lambda n: 45 if n <= 1 else (75 if n == 2 else 110))
 
 
 
 
 
 
 
74
  df['valor_imovel_estimado'] = df['metragem_estimada'] * df['preco_m2_estimado']
75
 
 
76
  df['ROI_anual'] = (df['ttm_revenue_native'] / df['valor_imovel_estimado']) * 100
77
- df['ROI_anual'] = df['ROI_anual'].replace([np.inf, -np.inf], 0).fillna(0) # Blindagem
 
 
 
 
78
 
79
- # 4. Score e Recomendação
80
- def normalize(series): return (series - series.min()) / (series.max() - series.min())
81
  df['Score'] = ((normalize(df['ROI_anual']) * 60) + (normalize(df['rating_overall'].fillna(0)) * 40)) * 100
82
- df['Recomendacao'] = df.apply(lambda row: "💎 COMPRA RECOMENDADA" if row['Score'] >= 60 else ("✅ POTENCIAL" if row['Score'] >= 40 else "❌ NÃO RECOMENDADO"), axis=1)
83
 
84
- # 5. Merge Segurança
 
 
 
 
 
 
 
 
85
  seg['city_norm'] = seg['city'].apply(normalizar_texto)
86
  seg_recent = seg.sort_values('date', ascending=False).groupby('city_norm')['Homicidios'].first().reset_index()
 
87
  df = pd.merge(df, seg_recent, on='city_norm', how='left')
88
- df['Homicidios'] = df['Homicidios'].fillna(-1) # -1 para indicar sem dados
89
 
90
- # 6. Preparação Histórico (para Sazonalidade)
91
  past['date'] = pd.to_datetime(past['date'], format='%d/%m/%Y', errors='coerce')
92
  past['city_norm'] = past['city'].apply(normalizar_texto)
93
 
94
  return df, past
95
 
96
- # --- TREINAMENTO MODELO IA E CLUSTERING (CACHING) ---
97
  @st.cache_resource
98
- def train_and_cluster(df):
99
- """Treina os modelos de predição de Receita e Preço e aplica K-Means para nichos."""
100
  df_model = df[df['ttm_revenue_native'] > 0].copy()
 
101
 
 
102
  df_model['is_superhost'] = df_model['superhost'].astype(str).apply(lambda x: 1 if x.upper() in ['VERDADEIRO', 'TRUE'] else 0)
103
  df_model['has_pool'] = df_model['amenities'].astype(str).str.contains('pool|Piscina', case=False).astype(int)
104
 
105
- features = ['bedrooms', 'guests', 'num_reviews', 'latitude', 'longitude', 'is_superhost', 'has_pool', 'ROI_anual']
106
-
107
- X = df_model[features]
108
  X = pd.concat([X, pd.get_dummies(df_model['city'], prefix='city')], axis=1)
109
  X = pd.concat([X, pd.get_dummies(df_model['room_type'], prefix='type')], axis=1)
110
- y_rev = df_model['ttm_revenue_native']
111
- y_adr = df_model['ttm_avg_rate_native']
112
-
113
- # Treinamento de Modelos
114
- model_rev = RandomForestRegressor(n_estimators=40, random_state=42).fit(X, y_rev)
115
- model_adr = RandomForestRegressor(n_estimators=40, random_state=42).fit(X, y_adr)
116
-
117
- # Calculo do GAP de ADR para Clustering
118
- df_model['ADR_Predito'] = model_adr.predict(X)
119
- df_model['ADR_GAP'] = df_model['ADR_Predito'] - df_model['ttm_avg_rate_native']
120
-
121
- # Clustering (K-Means)
122
- X_cluster = df_model[['latitude', 'longitude', 'ROI_anual', 'ttm_avg_rate_native']].dropna()
123
- scaler = X_cluster.copy()
124
- for col in scaler.columns:
125
- scaler[col] = (scaler[col] - scaler[col].mean()) / scaler[col].std()
126
-
127
- kmeans = KMeans(n_clusters=5, random_state=42, n_init='auto').fit(scaler)
128
- df_model['Cluster_Mercado'] = kmeans.labels_
129
-
130
- return df_model, model_rev, model_adr, X.columns
131
-
132
- # --- FUNÇÕES DE RENDERIZAÇÃO (REUSÁVEIS) ---
133
-
134
- def render_property_card(df_row, title="Ficha Técnica"):
135
- """Renderiza a ficha técnica detalhada de um único imóvel."""
136
- if df_row is None or df_row.empty:
137
- st.info("Clique em um ponto no mapa ou selecione um imóvel.")
138
- return
139
-
140
- row = df_row.iloc[0] # Pega a primeira (e única) linha
141
-
142
- st.markdown(f"""
143
- <div style="background-color: #f0f2f6; padding: 20px; border-radius: 10px; border: 1px solid #ccc;">
144
- <h3 style='margin-top: 0px;'>{title}</h3>
145
- </div>
146
- """, unsafe_allow_html=True)
147
 
148
- c_img, c_info, c_kpi = st.columns([1, 1, 1])
149
-
150
- with c_img:
151
- if pd.notna(row['cover_photo_url']):
152
- st.image(row['cover_photo_url'], use_container_width=True)
153
- else:
154
- st.info("Sem foto disponível")
155
-
156
- with c_info:
157
- st.write(f"**Cidade:** {row['city']}")
158
- st.write(f"**Tipo:** {row['room_type']}")
159
- st.write(f"**Acomoda:** {int(row['guests'])} pessoas")
160
- st.write(f"**Quartos:** {int(row['bedrooms'])}")
161
- st.metric("Nota Geral", f"⭐ {row['rating_overall']:.2f}")
162
-
163
- with c_kpi:
164
- st.metric("Faturamento Anual", f"R$ {row['ttm_revenue_native']:,.0f}")
165
- st.metric("ROI Estimado", f"{row['ROI_anual']:.1f}%")
166
- st.metric("Preço Imóvel Est.", f"R$ {row['valor_imovel_estimado']:,.0f}")
167
-
168
- status = row['Recomendacao']
169
- if "COMPRA" in status:
170
- st.success(f"## {status}")
171
- else:
172
- st.warning(f"## {status}")
173
-
174
- # --- INÍCIO DA APLICAÇÃO ---
175
 
 
176
  df, df_past = load_data()
177
- df_model_comp, model_rev, model_adr, model_cols = train_and_cluster(df)
178
 
179
- # --- SIDEBAR (FILTROS) ---
180
  st.sidebar.image("https://upload.wikimedia.org/wikipedia/commons/6/69/Airbnb_Logo_Bélo.svg", width=140)
181
  st.sidebar.header("Navegação")
182
- page = st.sidebar.radio("", ["📊 Dashboard de Mercado", "🤖 Simulador IA"])
183
 
184
  st.sidebar.markdown("---")
185
  st.sidebar.header("Filtros")
186
 
 
187
  cidades_disponiveis = sorted(df['city'].unique())
188
  cidades_sel = st.sidebar.multiselect("Cidades:", cidades_disponiveis, default=cidades_disponiveis)
189
 
 
190
  quartos = st.sidebar.slider("Quartos:", 0, 8, (0, 8))
191
- usar_seguranca = st.sidebar.checkbox("🛡️ Filtrar por Segurança", value=False)
192
 
 
 
193
  if usar_seguranca:
194
- max_homicidios = int(df[df['Homicidios'] >= 0]['Homicidios'].max()) if not df.empty else 1
195
  filtro_homicidios = st.sidebar.slider("Máx Homicídios/Ano:", 0, max_homicidios, max_homicidios)
196
 
 
197
  esconder_outliers = st.sidebar.checkbox("Ocultar Outliers (Preço)", value=True)
198
 
199
- # --- APLICAÇÃO DOS FILTROS ---
200
  df_filtered = df[
201
  (df['city'].isin(cidades_sel)) &
202
  (df['bedrooms'].between(quartos[0], quartos[1]))
203
- ].copy()
204
 
205
  if usar_seguranca:
206
  df_filtered = df_filtered[(df_filtered['Homicidios'] >= 0) & (df_filtered['Homicidios'] <= filtro_homicidios)]
@@ -212,60 +185,104 @@ if esconder_outliers:
212
  if pd.notna(IQR):
213
  df_filtered = df_filtered[df_filtered['ttm_avg_rate_native'] <= (Q3 + 1.5 * IQR)]
214
 
215
-
216
- # --- PÁGINA 1: DASHBOARD DE MERCADO ---
217
-
218
- if page == "📊 Dashboard de Mercado":
219
- st.title("📍 Inteligência Imobiliária Airbnb Pro")
220
 
221
  # 1. KPIs
222
  kpi1, kpi2, kpi3, kpi4 = st.columns(4)
223
- kpi1.metric("Imóveis Filtrados", len(df_filtered))
224
- kpi2.metric("Preço Diária (Mediana)", f"R$ {df_filtered['ttm_avg_rate_native'].median():.0f}")
225
- kpi3.metric("Faturamento Anual (Médio)", f"R$ {df_filtered['ttm_revenue_native'].mean():,.0f}")
226
  kpi4.metric("ROI Médio", f"{df_filtered['ROI_anual'].median():.1f}% a.a.")
227
 
228
  st.markdown("---")
229
 
230
- # 2. MAPA INTERATIVO (Folium Limpo)
231
  st.subheader("Mapa de Oportunidades")
232
- st.caption("Passe o mouse para ver o preço. Clique no ponto para ver a Ficha Técnica abaixo.")
233
 
234
  if not df_filtered.empty:
235
  center_lat = df_filtered['latitude'].mean()
236
  center_lon = df_filtered['longitude'].mean()
237
- m = folium.Map(location=[center_lat, center_lon], zoom_start=11, tiles='CartoDB positron')
 
 
238
  marker_cluster = MarkerCluster().add_to(m)
239
 
 
240
  for idx, row in df_filtered.head(1000).iterrows():
 
241
  color = '#2ecc71' if row['Recomendacao'] == "💎 COMPRA RECOMENDADA" else '#3498db'
242
 
243
  folium.CircleMarker(
244
  location=[row['latitude'], row['longitude']],
245
- radius=7, color=color, fill=True, fill_color=color, fill_opacity=0.8,
246
- popup=row['listing_name'], # Chave de retorno para o clique
247
- tooltip=f"{row['listing_name']}: R$ {row['ttm_avg_rate_native']:.0f}"
 
 
 
 
248
  ).add_to(marker_cluster)
249
 
 
250
  map_data = st_folium(m, height=500, width="100%")
251
  else:
252
- st.warning("Sem dados para exibir no mapa com os filtros atuais.")
253
  map_data = None
254
 
255
- # 3. PAINEL DE DETALHES (RENDERIZAÇÃO DO CLIQUE ABAIXO DO MAPA)
256
- st.markdown("---")
257
- st.subheader("📋 Ficha Técnica Detalhada")
258
 
259
  selected_listing = None
260
  if map_data and map_data.get('last_object_clicked_popup'):
261
  name_clicked = map_data['last_object_clicked_popup']
262
  match = df_filtered[df_filtered['listing_name'] == name_clicked]
263
  if not match.empty:
264
- selected_listing = match
 
 
 
 
 
 
 
 
265
 
266
- render_property_card(selected_listing)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
268
- # 4. GRÁFICOS V.1 (VISÍVEIS)
269
  col_g1, col_g2 = st.columns(2)
270
 
271
  with col_g1:
@@ -276,45 +293,45 @@ if page == "📊 Dashboard de Mercado":
276
 
277
  with col_g2:
278
  st.subheader("💎 Qualidade vs. Faturamento")
 
279
  df_filtered['size_safe'] = df_filtered['ROI_anual'].apply(lambda x: max(0.1, x))
280
  fig_scat = px.scatter(df_filtered, x="rating_overall", y="ttm_revenue_native",
281
  size="size_safe", color="room_type",
282
  title="Nota x Receita (Tamanho = ROI)", hover_name="listing_name")
283
  st.plotly_chart(fig_scat, use_container_width=True)
284
 
 
285
  st.markdown("---")
286
-
287
- # 5. ANÁLISES AVANÇADAS (EXPANDER V.2)
288
- with st.expander("🔬 Ver Análises Avançadas (Sazonalidade, Segurança e Clustering)"):
289
 
290
- tab1, tab2, tab3, tab4 = st.tabs(["📅 Sazonalidade", "🛡️ Risco/Segurança", "☁️ Palavras-Chave", "⚔️ Comparador"])
291
 
292
  with tab1:
293
  st.markdown("##### Ocupação Mensal Histórica")
 
294
  cidades_norm = [normalizar_texto(c) for c in cidades_sel]
295
  past_filtered = df_past[df_past['city_norm'].isin(cidades_norm)].copy()
296
 
297
  if not past_filtered.empty:
298
  past_filtered['month'] = past_filtered['date'].dt.strftime('%Y-%m')
299
- seasonal = past_filtered.groupby('month')['occupancy_real'].mean().reset_index()
300
- fig_line = px.line(seasonal, x='month', y='occupancy_real', markers=True)
301
  st.plotly_chart(fig_line, use_container_width=True)
302
  else:
303
- st.info("Histórico de sazonalidade não disponível.")
304
 
305
  with tab2:
306
- st.markdown("##### Segurança vs Retorno")
307
- df_seg_plot = df_filtered[df_filtered['Homicidios'] >= 0].copy()
308
  if not df_seg_plot.empty:
309
- df_seg_plot['Tamanho_Bolha'] = df_seg_plot['ROI_anual'].apply(lambda x: max(0.1, x))
310
  fig_seg = px.scatter(df_seg_plot, x="Homicidios", y="ttm_revenue_native",
311
- color="city", size="Tamanho_Bolha", hover_name="listing_name")
312
  st.plotly_chart(fig_seg, use_container_width=True)
313
  else:
314
- st.warning("Sem dados de segurança para exibir.")
315
 
316
  with tab3:
317
- st.markdown("##### O que os Melhores Imóveis (Diamantes) Têm?")
318
  top_performers = df_filtered[df_filtered['Recomendacao'] == "💎 COMPRA RECOMENDADA"]
319
  text = " ".join(str(a) for a in top_performers['amenities'].dropna())
320
  if text.strip():
@@ -324,72 +341,64 @@ if page == "📊 Dashboard de Mercado":
324
  ax.axis("off")
325
  st.pyplot(fig)
326
  else:
327
- st.info("Não há Top Performers suficientes.")
328
-
329
- with tab4:
330
- st.markdown("##### Comparador Lado a Lado")
331
-
332
- all_listings = df_filtered['listing_name'].unique().tolist()
333
- col_sel1, col_sel2 = st.columns(2)
334
-
335
- with col_sel1:
336
- sel_1 = st.selectbox("Imóvel 1:", all_listings, index=None, key='comp1')
337
- with col_sel2:
338
- sel_2 = st.selectbox("Imóvel 2:", all_listings, index=None, key='comp2')
339
-
340
- st.markdown("---")
341
-
342
- col_comp1, col_comp_spacer, col_comp2 = st.columns([1, 0.1, 1])
343
-
344
- if sel_1:
345
- comp_1 = df_filtered[df_filtered['listing_name'] == sel_1]
346
- with col_comp1:
347
- render_property_card(comp_1, title="Imóvel 1")
348
-
349
- if sel_2:
350
- comp_2 = df_filtered[df_filtered['listing_name'] == sel_2]
351
- with col_comp2:
352
- render_property_card(comp_2, title="Imóvel 2")
353
 
354
- # --- PÁGINA 2: SIMULADOR IA ---
355
  elif page == "🤖 Simulador IA":
356
- st.title("🤖 Oráculo de Investimentos e Cluster")
357
 
358
- tab_sim, tab_calc, tab_cluster = st.tabs(["🔮 Previsão com IA", "💰 Lucro Líquido", "🗺️ Nichos de Mercado"])
359
 
360
- with tab_sim:
361
- # Lógica de Previsão de Receita (Revenue Prediction)
362
- col_input, col_result = st.columns([1, 1])
363
- # ... (código de input e previsão se mantém) ...
364
-
365
- with tab_calc:
366
- # Lógica de Calculadora de Lucro Líquido
367
- # ... (código de input e cálculo se mantém) ...
368
 
369
- with tab_cluster:
370
- st.markdown("### 🗺️ Análise de Nichos de Mercado (Clustering)")
371
- st.caption("Agrupamentos baseados em Localização, Preço e ROI. Cada cluster representa um 'nicho' diferente de mercado.")
372
-
373
- # Gráfico de Dispersão do Cluster
374
- df_plot = df_model_comp.head(2000).dropna(subset=['Cluster_Mercado', 'ROI_anual', 'ttm_avg_rate_native', 'ADR_GAP'])
375
- fig_cluster = px.scatter(df_plot,
376
- x='ROI_anual',
377
- y='ttm_avg_rate_native',
378
- color='Cluster_Mercado',
379
- hover_data=['city', 'ROI_anual', 'ADR_GAP'],
380
- title="Clusters por ROI vs Preço (Cor = Nicho)")
381
- st.plotly_chart(fig_cluster, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
382
 
383
- # Tabela de Insights do Cluster
384
- cluster_summary = df_model_comp.groupby('Cluster_Mercado').agg(
385
- Count=('listing_id', 'count'),
386
- ROI_Medio=('ROI_anual', 'median'),
387
- Preco_Diaria_Medio=('ttm_avg_rate_native', 'median'),
388
- GAP_Medio_ADR=('ADR_GAP', 'median')
389
- ).reset_index().sort_values('ROI_Medio', ascending=False)
390
 
391
- st.subheader("Resumo dos Nichos de Mercado")
392
- st.dataframe(cluster_summary.style.format(
393
- {'ROI_Medio': '{:.1f}%', 'Preco_Diaria_Medio': 'R$ {:.0f}', 'GAP_Medio_ADR': 'R$ {:.0f}'}
394
- ))
395
- st.info("O cluster com maior 'GAP Médio de ADR' e 'ROI Alto' é o 'Pote de Ouro': O mercado cobra pouco por imóveis com grande potencial de lucro.")
 
6
  from streamlit_folium import st_folium
7
  from folium.plugins import MarkerCluster
8
  from sklearn.ensemble import RandomForestRegressor
 
9
  from wordcloud import WordCloud
10
  import matplotlib.pyplot as plt
11
  import unicodedata
12
 
13
+ # --- CONFIGURAÇÃO DA PÁGINA ---
14
  st.set_page_config(page_title="Airbnb Intelligence Pro", layout="wide", page_icon="🏘️")
15
 
16
+ # --- CSS CUSTOMIZADO ---
17
  st.markdown("""
18
  <style>
19
  .metric-card {
 
23
  border-radius: 5px;
24
  box-shadow: 1px 1px 3px rgba(0,0,0,0.1);
25
  }
 
26
  .stExpander { border: 1px solid #ddd; border-radius: 5px; background-color: #ffffff; }
27
  </style>
28
  """, unsafe_allow_html=True)
29
 
30
+ # --- FUNÇÃO DE NORMALIZAÇÃO DE TEXTO ---
 
31
  def normalizar_texto(texto):
32
+ """Remove acentos e coloca em minúsculo para garantir merge correto."""
33
  if pd.isna(texto): return ""
34
  nfkd = unicodedata.normalize('NFKD', str(texto))
35
  return u"".join([c for c in nfkd if not unicodedata.combining(c)]).lower().strip()
36
 
37
+ # --- CARREGAMENTO E LIMPEZA (CACHEADO) ---
38
  @st.cache_data
39
  def load_data():
 
40
  try:
41
+ # Carregamento dos arquivos
42
  df = pd.read_csv('lisitng_geral.csv', sep=';', encoding='utf-8')
43
  fipe = pd.read_csv('fipezap_geral.csv', sep=';', encoding='utf-8')
44
  past = pd.read_csv('past_geral.csv', sep=';', encoding='utf-8')
45
  seg = pd.read_csv('casos_homicidios.csv', sep=';', encoding='utf-8')
46
  except FileNotFoundError:
47
+ st.error("ERRO: Arquivos CSV não encontrados. Faça o upload no Hugging Face.")
48
  st.stop()
49
 
50
+ # --- 1. LIMPEZA DE COORDENADAS (Sua Lógica Solicitada) ---
51
+ # Garante que BH, Curitiba e POA apareçam
52
  cols_coords = ['latitude', 'longitude']
53
  for col in cols_coords:
54
  if df[col].dtype == 'object':
55
+ df[col] = df[col].str.replace(',', '.').astype(float)
 
56
 
57
+ # Remove linhas sem coordenada (essencial para o mapa não quebrar)
58
+ df = df.dropna(subset=cols_coords)
59
+ # ---------------------------------------------------------
60
+
61
+ # 2. Limpeza Financeira
62
  cols_fin = ['ttm_revenue_native', 'ttm_avg_rate_native', 'ttm_occupancy', 'rating_overall']
63
  for col in cols_fin:
64
+ if col in df.columns and df[col].dtype == 'object':
65
  df[col] = df[col].astype(str).str.replace('R$', '').str.replace('.', '').str.replace(',', '.').astype(float)
66
+
67
+ # Normalização de Cidades
68
  df['city_norm'] = df['city'].apply(normalizar_texto)
69
+ df['city'] = df['city'].str.title() # Deixar bonito para visualização
70
+
71
+ # Imputação de Nulos (Quartos/Hóspedes)
72
  df['bedrooms'] = df['bedrooms'].fillna(df.groupby(['city', 'room_type'])['bedrooms'].transform('median')).fillna(1)
73
+ df['guests'] = df['guests'].fillna(df.groupby(['city', 'room_type'])['guests'].transform('median')).fillna(2)
74
+
75
+ # 3. Limpeza e Merge FipeZap
76
+ fipe = fipe.dropna(subset=['city', 'preco_m2'])
77
+ fipe = fipe.loc[:, ~fipe.columns.str.contains('^Unnamed')]
78
  fipe['city_norm'] = fipe['city'].apply(normalizar_texto)
79
+
80
+ # Merge Preço m2
81
  media_cidade = fipe.groupby('city_norm')['preco_m2'].mean().to_dict()
82
+ df['preco_m2_estimado'] = df['city_norm'].map(media_cidade)
83
+ # Fallback: Média geral se não achar a cidade
84
+ df['preco_m2_estimado'] = df['preco_m2_estimado'].fillna(fipe['preco_m2'].mean())
85
+
86
+ # 4. Cálculos de KPI (ROI)
87
+ def estimar_metragem(n):
88
+ return 45 if n <= 1 else (75 if n == 2 else 110)
89
+
90
+ df['metragem_estimada'] = df['bedrooms'].apply(estimar_metragem)
91
  df['valor_imovel_estimado'] = df['metragem_estimada'] * df['preco_m2_estimado']
92
 
93
+ # Blindagem contra divisão por zero
94
  df['ROI_anual'] = (df['ttm_revenue_native'] / df['valor_imovel_estimado']) * 100
95
+ df['ROI_anual'] = df['ROI_anual'].replace([np.inf, -np.inf], 0).fillna(0)
96
+
97
+ # Score e Recomendação IA
98
+ def normalize(series):
99
+ return (series - series.min()) / (series.max() - series.min())
100
 
 
 
101
  df['Score'] = ((normalize(df['ROI_anual']) * 60) + (normalize(df['rating_overall'].fillna(0)) * 40)) * 100
 
102
 
103
+ def gerar_laudo(row):
104
+ if row['Score'] >= 60: return "💎 COMPRA RECOMENDADA"
105
+ elif row['Score'] >= 40: return "✅ POTENCIAL"
106
+ elif row['Score'] >= 20: return "⚠️ ARRISCADO"
107
+ else: return "❌ NÃO RECOMENDADO"
108
+
109
+ df['Recomendacao'] = df.apply(gerar_laudo, axis=1)
110
+
111
+ # 5. Limpeza e Merge Segurança
112
  seg['city_norm'] = seg['city'].apply(normalizar_texto)
113
  seg_recent = seg.sort_values('date', ascending=False).groupby('city_norm')['Homicidios'].first().reset_index()
114
+
115
  df = pd.merge(df, seg_recent, on='city_norm', how='left')
116
+ df['Homicidios'] = df['Homicidios'].fillna(-1) # -1 indica sem dados
117
 
118
+ # 6. Limpeza Histórico (Past)
119
  past['date'] = pd.to_datetime(past['date'], format='%d/%m/%Y', errors='coerce')
120
  past['city_norm'] = past['city'].apply(normalizar_texto)
121
 
122
  return df, past
123
 
124
+ # --- TREINAMENTO MODELO IA ---
125
  @st.cache_resource
126
+ def train_model(df):
 
127
  df_model = df[df['ttm_revenue_native'] > 0].copy()
128
+ features = ['bedrooms', 'guests', 'num_reviews', 'latitude', 'longitude']
129
 
130
+ # Feature Engineering Simples
131
  df_model['is_superhost'] = df_model['superhost'].astype(str).apply(lambda x: 1 if x.upper() in ['VERDADEIRO', 'TRUE'] else 0)
132
  df_model['has_pool'] = df_model['amenities'].astype(str).str.contains('pool|Piscina', case=False).astype(int)
133
 
134
+ X = df_model[features + ['is_superhost', 'has_pool']]
135
+ # One Hot Encoding
 
136
  X = pd.concat([X, pd.get_dummies(df_model['city'], prefix='city')], axis=1)
137
  X = pd.concat([X, pd.get_dummies(df_model['room_type'], prefix='type')], axis=1)
138
+ y = df_model['ttm_revenue_native']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
+ model = RandomForestRegressor(n_estimators=40, random_state=42)
141
+ model.fit(X, y)
142
+ return model, X.columns
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
+ # --- CARREGAMENTO ---
145
  df, df_past = load_data()
146
+ model, model_cols = train_model(df)
147
 
148
+ # --- INTERFACE: SIDEBAR ---
149
  st.sidebar.image("https://upload.wikimedia.org/wikipedia/commons/6/69/Airbnb_Logo_Bélo.svg", width=140)
150
  st.sidebar.header("Navegação")
151
+ page = st.sidebar.radio("", ["📊 Análise de Mercado", "🤖 Simulador IA"])
152
 
153
  st.sidebar.markdown("---")
154
  st.sidebar.header("Filtros")
155
 
156
+ # Filtro Cidades
157
  cidades_disponiveis = sorted(df['city'].unique())
158
  cidades_sel = st.sidebar.multiselect("Cidades:", cidades_disponiveis, default=cidades_disponiveis)
159
 
160
+ # Filtro Quartos
161
  quartos = st.sidebar.slider("Quartos:", 0, 8, (0, 8))
 
162
 
163
+ # Filtro Segurança (Opcional)
164
+ usar_seguranca = st.sidebar.checkbox("🛡️ Filtrar por Segurança", value=False)
165
  if usar_seguranca:
166
+ max_homicidios = int(df[df['Homicidios'] >= 0]['Homicidios'].max())
167
  filtro_homicidios = st.sidebar.slider("Máx Homicídios/Ano:", 0, max_homicidios, max_homicidios)
168
 
169
+ # Filtro Outliers
170
  esconder_outliers = st.sidebar.checkbox("Ocultar Outliers (Preço)", value=True)
171
 
172
+ # APLICAÇÃO DOS FILTROS
173
  df_filtered = df[
174
  (df['city'].isin(cidades_sel)) &
175
  (df['bedrooms'].between(quartos[0], quartos[1]))
176
+ ]
177
 
178
  if usar_seguranca:
179
  df_filtered = df_filtered[(df_filtered['Homicidios'] >= 0) & (df_filtered['Homicidios'] <= filtro_homicidios)]
 
185
  if pd.notna(IQR):
186
  df_filtered = df_filtered[df_filtered['ttm_avg_rate_native'] <= (Q3 + 1.5 * IQR)]
187
 
188
+ # --- PÁGINA 1: DASHBOARD ---
189
+ if page == "📊 Análise de Mercado":
190
+ st.title("📍 Inteligência Imobiliária Airbnb")
 
 
191
 
192
  # 1. KPIs
193
  kpi1, kpi2, kpi3, kpi4 = st.columns(4)
194
+ kpi1.metric("Imóveis", len(df_filtered))
195
+ kpi2.metric("Preço Médio", f"R$ {df_filtered['ttm_avg_rate_native'].median():.0f}")
196
+ kpi3.metric("Faturamento Médio", f"R$ {df_filtered['ttm_revenue_native'].mean():,.0f}")
197
  kpi4.metric("ROI Médio", f"{df_filtered['ROI_anual'].median():.1f}% a.a.")
198
 
199
  st.markdown("---")
200
 
201
+ # 2. MAPA (Folium Limpo)
202
  st.subheader("Mapa de Oportunidades")
203
+ st.caption("Passe o mouse para ver o preço. Clique para ver detalhes abaixo.")
204
 
205
  if not df_filtered.empty:
206
  center_lat = df_filtered['latitude'].mean()
207
  center_lon = df_filtered['longitude'].mean()
208
+
209
+ # Mapa estilo 'CartoDB positron' para ser limpo e legível
210
+ m = folium.Map(location=[center_lat, center_lon], zoom_start=12, tiles='CartoDB positron')
211
  marker_cluster = MarkerCluster().add_to(m)
212
 
213
+ # Adicionar pontos (Limitado a 1000 para performance)
214
  for idx, row in df_filtered.head(1000).iterrows():
215
+ # Cor baseada na recomendação
216
  color = '#2ecc71' if row['Recomendacao'] == "💎 COMPRA RECOMENDADA" else '#3498db'
217
 
218
  folium.CircleMarker(
219
  location=[row['latitude'], row['longitude']],
220
+ radius=7,
221
+ color=color,
222
+ fill=True,
223
+ fill_color=color,
224
+ fill_opacity=0.8,
225
+ popup=row['listing_name'], # Chave para identificar o clique
226
+ tooltip=f"R$ {row['ttm_avg_rate_native']:.0f}/noite"
227
  ).add_to(marker_cluster)
228
 
229
+ # Renderizar mapa e capturar clique
230
  map_data = st_folium(m, height=500, width="100%")
231
  else:
232
+ st.warning("Sem dados para exibir no mapa.")
233
  map_data = None
234
 
235
+ # 3. DETALHES DO IMÓVEL (ABAIXO DO MAPA)
236
+ st.markdown("### 📋 Ficha Técnica do Imóvel")
 
237
 
238
  selected_listing = None
239
  if map_data and map_data.get('last_object_clicked_popup'):
240
  name_clicked = map_data['last_object_clicked_popup']
241
  match = df_filtered[df_filtered['listing_name'] == name_clicked]
242
  if not match.empty:
243
+ selected_listing = match.iloc[0]
244
+
245
+ if selected_listing is not None:
246
+ with st.container():
247
+ st.markdown(f"""
248
+ <div style="background-color: #f0f2f6; padding: 20px; border-radius: 10px; border: 1px solid #ccc;">
249
+ <h3>🏡 {selected_listing['listing_name']}</h3>
250
+ </div>
251
+ """, unsafe_allow_html=True)
252
 
253
+ c_img, c_info, c_kpi = st.columns([1, 1, 1])
254
+
255
+ with c_img:
256
+ if pd.notna(selected_listing['cover_photo_url']):
257
+ st.image(selected_listing['cover_photo_url'], use_container_width=True)
258
+ else:
259
+ st.info("Sem foto disponível")
260
+
261
+ with c_info:
262
+ st.write(f"**Cidade:** {selected_listing['city']}")
263
+ st.write(f"**Tipo:** {selected_listing['room_type']}")
264
+ st.write(f"**Acomoda:** {int(selected_listing['guests'])} pessoas")
265
+ st.write(f"**Quartos:** {int(selected_listing['bedrooms'])}")
266
+ st.metric("Nota Geral", f"⭐ {selected_listing['rating_overall']}")
267
+
268
+ with c_kpi:
269
+ st.metric("Faturamento Anual", f"R$ {selected_listing['ttm_revenue_native']:,.2f}")
270
+ st.metric("ROI Estimado", f"{selected_listing['ROI_anual']:.2f}%")
271
+
272
+ status = selected_listing['Recomendacao']
273
+ if "COMPRA" in status:
274
+ st.success(f"## {status}")
275
+ elif "NÃO" in status:
276
+ st.error(f"## {status}")
277
+ else:
278
+ st.warning(f"## {status}")
279
+
280
+ else:
281
+ st.info("👆 Clique em uma bolinha colorida no mapa acima para ver a análise completa do imóvel aqui.")
282
+
283
+ st.markdown("---")
284
 
285
+ # 4. GRÁFICOS VISUAIS (Estilo Código 1)
286
  col_g1, col_g2 = st.columns(2)
287
 
288
  with col_g1:
 
293
 
294
  with col_g2:
295
  st.subheader("💎 Qualidade vs. Faturamento")
296
+ # Proteção contra erro de tamanho NaN
297
  df_filtered['size_safe'] = df_filtered['ROI_anual'].apply(lambda x: max(0.1, x))
298
  fig_scat = px.scatter(df_filtered, x="rating_overall", y="ttm_revenue_native",
299
  size="size_safe", color="room_type",
300
  title="Nota x Receita (Tamanho = ROI)", hover_name="listing_name")
301
  st.plotly_chart(fig_scat, use_container_width=True)
302
 
303
+ # 5. ANÁLISES AVANÇADAS (EXPANDER)
304
  st.markdown("---")
305
+ with st.expander("🔬 Ver Análises Avançadas (Sazonalidade, Segurança e Palavras-Chave)"):
 
 
306
 
307
+ tab1, tab2, tab3 = st.tabs(["📅 Sazonalidade", "🛡️ Segurança", "☁️ Palavras-Chave"])
308
 
309
  with tab1:
310
  st.markdown("##### Ocupação Mensal Histórica")
311
+ # Filtra histórico usando city_norm para garantir match
312
  cidades_norm = [normalizar_texto(c) for c in cidades_sel]
313
  past_filtered = df_past[df_past['city_norm'].isin(cidades_norm)].copy()
314
 
315
  if not past_filtered.empty:
316
  past_filtered['month'] = past_filtered['date'].dt.strftime('%Y-%m')
317
+ seasonal = past_filtered.groupby('month')['occupancy'].mean().reset_index()
318
+ fig_line = px.line(seasonal, x='month', y='occupancy', markers=True)
319
  st.plotly_chart(fig_line, use_container_width=True)
320
  else:
321
+ st.warning("Sem dados históricos para as cidades selecionadas.")
322
 
323
  with tab2:
324
+ st.markdown("##### Segurança vs Retorno Financeiro")
325
+ df_seg_plot = df_filtered[df_filtered['Homicidios'] >= 0]
326
  if not df_seg_plot.empty:
 
327
  fig_seg = px.scatter(df_seg_plot, x="Homicidios", y="ttm_revenue_native",
328
+ color="city", size="size_safe", hover_name="listing_name")
329
  st.plotly_chart(fig_seg, use_container_width=True)
330
  else:
331
+ st.warning("Cidades selecionadas não possuem dados de homicídios no arquivo.")
332
 
333
  with tab3:
334
+ st.markdown("##### O que os Melhores Imóveis têm?")
335
  top_performers = df_filtered[df_filtered['Recomendacao'] == "💎 COMPRA RECOMENDADA"]
336
  text = " ".join(str(a) for a in top_performers['amenities'].dropna())
337
  if text.strip():
 
341
  ax.axis("off")
342
  st.pyplot(fig)
343
  else:
344
+ st.info("Não há Top Performers suficientes para gerar a nuvem.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
 
346
+ # --- PÁGINA 2: SIMULADOR ---
347
  elif page == "🤖 Simulador IA":
348
+ st.title("🤖 Simulador de Investimento & IA")
349
 
350
+ col_ia, col_calc = st.columns(2)
351
 
352
+ with col_ia:
353
+ st.subheader("🔮 Previsão de Receita (IA)")
354
+ sim_city = st.selectbox("Cidade:", sorted(df['city'].unique()))
355
+ sim_type = st.selectbox("Tipo:", df['room_type'].unique())
356
+ sim_bed = st.slider("Quartos:", 0, 8, 2)
357
+ sim_guest = st.slider("Hóspedes:", 1, 16, 4)
358
+ sim_pool = st.checkbox("Tem Piscina?", value=False)
359
+ sim_super = st.checkbox("Será Superhost?", value=True)
360
 
361
+ if st.button("Calcular Previsão"):
362
+ # Monta input
363
+ base_lat = df[df['city'] == sim_city]['latitude'].mean()
364
+ base_lon = df[df['city'] == sim_city]['longitude'].mean()
365
+ if pd.isna(base_lat): base_lat = -23.55 # Fallback
366
+ if pd.isna(base_lon): base_lon = -46.63
367
+
368
+ input_df = pd.DataFrame({
369
+ 'bedrooms': [sim_bed], 'guests': [sim_guest],
370
+ 'num_reviews': [30], 'latitude': [base_lat], 'longitude': [base_lon],
371
+ 'is_superhost': [1 if sim_super else 0],
372
+ 'has_pool': [1 if sim_pool else 0]
373
+ })
374
+
375
+ # One Hot Encoding Manual
376
+ for col in model_cols:
377
+ if col not in input_df.columns:
378
+ if f"city_{sim_city}" == col: input_df[col] = 1
379
+ elif f"type_{sim_type}" == col: input_df[col] = 1
380
+ else: input_df[col] = 0
381
+
382
+ # Reordenar colunas
383
+ input_df = input_df[model_cols]
384
+ pred = model.predict(input_df)[0]
385
+
386
+ st.success(f"Faturamento Estimado: R$ {pred:,.2f}/ano")
387
+ st.metric("Média Mensal", f"R$ {pred/12:,.2f}")
388
+
389
+ with col_calc:
390
+ st.subheader("💰 Calculadora de Lucro Líquido")
391
+ rec_bruta = st.number_input("Receita Bruta Anual (R$):", value=50000.0)
392
+ val_imovel = st.number_input("Valor do Imóvel (R$):", value=400000.0)
393
+ taxa = st.slider("Taxa Airbnb (%):", 0, 20, 15)
394
+ custo_fixo = st.number_input("Custo Fixo Mensal (Condomínio/Luz) R$:", value=800.0)
395
 
396
+ custo_total = (rec_bruta * (taxa/100)) + (custo_fixo * 12)
397
+ lucro = rec_bruta - custo_total
398
+ roi_real = (lucro / val_imovel) * 100
 
 
 
 
399
 
400
+ st.divider()
401
+ st.metric("Lucro Líquido Anual", f"R$ {lucro:,.2f}")
402
+ st.metric("ROI Real (Líquido)", f"{roi_real:.2f}%")
403
+ if roi_real > 6:
404
+ st.balloons()