Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -155,7 +155,6 @@ model, model_cols, df_training = train_model(df)
|
|
| 155 |
st.sidebar.image("https://upload.wikimedia.org/wikipedia/commons/6/69/Airbnb_Logo_Bélo.svg", width=140)
|
| 156 |
st.sidebar.header("Navegação")
|
| 157 |
|
| 158 |
-
# Label visibility adicionado para evitar warning
|
| 159 |
page = st.sidebar.radio(
|
| 160 |
"Menu Principal",
|
| 161 |
["📊 Dashboard de Mercado", "📈 Análise Exploratória", "🤖 Simulador IA"],
|
|
@@ -187,6 +186,11 @@ if page == "📊 Dashboard de Mercado":
|
|
| 187 |
st.markdown("---")
|
| 188 |
|
| 189 |
st.subheader("Mapa de Oportunidades")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
if not df_filtered.empty:
|
| 191 |
center_lat = df_filtered['latitude'].mean()
|
| 192 |
center_lon = df_filtered['longitude'].mean()
|
|
@@ -206,14 +210,52 @@ if page == "📊 Dashboard de Mercado":
|
|
| 206 |
map_data = st_folium(m, height=450, width="100%")
|
| 207 |
else:
|
| 208 |
st.warning("Sem dados.")
|
| 209 |
-
map_data = None
|
| 210 |
|
|
|
|
| 211 |
if map_data and map_data.get('last_object_clicked_popup'):
|
| 212 |
-
|
| 213 |
-
match = df_filtered[df_filtered['listing_name'] ==
|
| 214 |
if not match.empty:
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
|
| 218 |
st.markdown("---")
|
| 219 |
|
|
@@ -237,7 +279,6 @@ if page == "📊 Dashboard de Mercado":
|
|
| 237 |
st.subheader("💰 Preço Médio por Mês (R$)")
|
| 238 |
price_table = past_filt.groupby(['month_str', 'city'])['native_rate_avg'].mean().unstack().fillna(0)
|
| 239 |
|
| 240 |
-
# --- CORREÇÃO: use_container_width é válido no streamlit recente, mas se der aviso, é visual ---
|
| 241 |
st.dataframe(price_table.style.format("R$ {:.2f}"), use_container_width=True)
|
| 242 |
else:
|
| 243 |
st.warning("Sem dados históricos.")
|
|
@@ -260,7 +301,7 @@ if page == "📊 Dashboard de Mercado":
|
|
| 260 |
ax.imshow(wc, interpolation='bilinear')
|
| 261 |
ax.axis("off")
|
| 262 |
st.pyplot(fig)
|
| 263 |
-
plt.close(fig)
|
| 264 |
|
| 265 |
# --- PÁGINA 2: EXPLORATÓRIA ---
|
| 266 |
elif page == "📈 Análise Exploratória":
|
|
@@ -296,8 +337,6 @@ elif page == "📈 Análise Exploratória":
|
|
| 296 |
with col4:
|
| 297 |
st.subheader("Tipos de Imóveis")
|
| 298 |
|
| 299 |
-
# --- CORREÇÃO DEFINITIVA DO ERRO 'VALUE OF X' ---
|
| 300 |
-
# Reseta o index e renomeia EXPLICITAMENTE para garantir que o Plotly ache as colunas
|
| 301 |
df_counts = df_filtered['room_type'].value_counts().reset_index()
|
| 302 |
df_counts.columns = ['Tipo_Imovel', 'Contagem']
|
| 303 |
|
|
@@ -308,10 +347,18 @@ elif page == "📈 Análise Exploratória":
|
|
| 308 |
template="plotly_white")
|
| 309 |
st.plotly_chart(fig_bar, use_container_width=True)
|
| 310 |
|
| 311 |
-
# --- PÁGINA 3: SIMULADOR ---
|
| 312 |
elif page == "🤖 Simulador IA":
|
| 313 |
st.title("🤖 Simulador Inteligente")
|
| 314 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
c1, c2 = st.columns([1, 1.5])
|
| 316 |
|
| 317 |
with c1:
|
|
@@ -334,10 +381,8 @@ elif page == "🤖 Simulador IA":
|
|
| 334 |
sim_pool = st.checkbox("Tem Piscina?", value=False)
|
| 335 |
sim_super = st.checkbox("Será Superhost?", value=True)
|
| 336 |
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
with c2:
|
| 340 |
-
if btn_calc:
|
| 341 |
input_df = pd.DataFrame({
|
| 342 |
'bedrooms': [sim_bed], 'guests': [sim_guest],
|
| 343 |
'num_reviews': [30], 'latitude': [sim_lat], 'longitude': [sim_lon],
|
|
@@ -354,26 +399,37 @@ elif page == "🤖 Simulador IA":
|
|
| 354 |
input_df = input_df[model_cols]
|
| 355 |
pred = model.predict(input_df)[0]
|
| 356 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
st.success(f"### 💰 Faturamento Estimado: R$ {pred:,.2f} / ano")
|
| 358 |
|
| 359 |
st.markdown("#### 📍 Localização Simulada")
|
| 360 |
-
m_sim = folium.Map(location=[
|
| 361 |
folium.Marker(
|
| 362 |
-
[
|
| 363 |
popup="Seu Imóvel Simulado",
|
| 364 |
icon=folium.Icon(color="red", icon="home")
|
| 365 |
).add_to(m_sim)
|
| 366 |
st_folium(m_sim, height=300, width="100%")
|
| 367 |
|
| 368 |
st.markdown("#### 🏠 Imóveis Reais Semelhantes (Vizinhança)")
|
| 369 |
-
df_city_neighbors = df_training[df_training['city'] ==
|
| 370 |
|
| 371 |
if not df_city_neighbors.empty:
|
| 372 |
nn = NearestNeighbors(n_neighbors=5, algorithm='ball_tree')
|
| 373 |
nn.fit(df_city_neighbors[['latitude', 'longitude']])
|
| 374 |
|
| 375 |
-
|
| 376 |
-
X_query = pd.DataFrame([[sim_lat, sim_lon]], columns=['latitude', 'longitude'])
|
| 377 |
distances, indices = nn.kneighbors(X_query)
|
| 378 |
|
| 379 |
neighbors = df_city_neighbors.iloc[indices[0]]
|
|
|
|
| 155 |
st.sidebar.image("https://upload.wikimedia.org/wikipedia/commons/6/69/Airbnb_Logo_Bélo.svg", width=140)
|
| 156 |
st.sidebar.header("Navegação")
|
| 157 |
|
|
|
|
| 158 |
page = st.sidebar.radio(
|
| 159 |
"Menu Principal",
|
| 160 |
["📊 Dashboard de Mercado", "📈 Análise Exploratória", "🤖 Simulador IA"],
|
|
|
|
| 186 |
st.markdown("---")
|
| 187 |
|
| 188 |
st.subheader("Mapa de Oportunidades")
|
| 189 |
+
st.caption("Clique nos círculos coloridos para ver a Ficha Técnica completa abaixo.")
|
| 190 |
+
|
| 191 |
+
map_data = None
|
| 192 |
+
selected_listing = None
|
| 193 |
+
|
| 194 |
if not df_filtered.empty:
|
| 195 |
center_lat = df_filtered['latitude'].mean()
|
| 196 |
center_lon = df_filtered['longitude'].mean()
|
|
|
|
| 210 |
map_data = st_folium(m, height=450, width="100%")
|
| 211 |
else:
|
| 212 |
st.warning("Sem dados.")
|
|
|
|
| 213 |
|
| 214 |
+
# --- RESTAURAÇÃO DA FICHA TÉCNICA DETALHADA ---
|
| 215 |
if map_data and map_data.get('last_object_clicked_popup'):
|
| 216 |
+
name_clicked = map_data['last_object_clicked_popup']
|
| 217 |
+
match = df_filtered[df_filtered['listing_name'] == name_clicked]
|
| 218 |
if not match.empty:
|
| 219 |
+
selected_listing = match.iloc[0]
|
| 220 |
+
|
| 221 |
+
if selected_listing is not None:
|
| 222 |
+
st.markdown("### 📋 Ficha Técnica do Imóvel")
|
| 223 |
+
with st.container():
|
| 224 |
+
# Cabeçalho estilizado
|
| 225 |
+
st.markdown(f"""
|
| 226 |
+
<div style="background-color: #f0f2f6; padding: 20px; border-radius: 10px; border: 1px solid #ccc; margin-bottom: 20px;">
|
| 227 |
+
<h3 style="margin:0; color:#31333F;">🏡 {selected_listing['listing_name']}</h3>
|
| 228 |
+
</div>
|
| 229 |
+
""", unsafe_allow_html=True)
|
| 230 |
+
|
| 231 |
+
c_img, c_info, c_kpi = st.columns([1, 1, 1])
|
| 232 |
+
|
| 233 |
+
with c_img:
|
| 234 |
+
if pd.notna(selected_listing['cover_photo_url']):
|
| 235 |
+
st.image(selected_listing['cover_photo_url'], use_container_width=True)
|
| 236 |
+
else:
|
| 237 |
+
st.info("Sem foto disponível")
|
| 238 |
+
|
| 239 |
+
with c_info:
|
| 240 |
+
st.markdown(f"**📍 Cidade:** {selected_listing['city']}")
|
| 241 |
+
st.markdown(f"**🏠 Tipo:** {selected_listing['room_type']}")
|
| 242 |
+
st.markdown(f"**👥 Acomoda:** {int(selected_listing['guests'])} pessoas")
|
| 243 |
+
st.markdown(f"**🛏️ Quartos:** {int(selected_listing['bedrooms'])}")
|
| 244 |
+
st.metric("Nota Geral", f"⭐ {selected_listing['rating_overall']}")
|
| 245 |
+
|
| 246 |
+
with c_kpi:
|
| 247 |
+
st.metric("Faturamento Anual", f"R$ {selected_listing['ttm_revenue_native']:,.2f}")
|
| 248 |
+
st.metric("ROI Estimado", f"{selected_listing['ROI_anual']:.2f}%")
|
| 249 |
+
|
| 250 |
+
status = selected_listing['Recomendacao']
|
| 251 |
+
if "COMPRA" in status:
|
| 252 |
+
st.success(f"## {status}")
|
| 253 |
+
elif "NÃO" in status:
|
| 254 |
+
st.error(f"## {status}")
|
| 255 |
+
else:
|
| 256 |
+
st.warning(f"## {status}")
|
| 257 |
+
else:
|
| 258 |
+
st.info("👆 Clique em uma bolinha colorida no mapa acima para ver a análise completa do imóvel aqui.")
|
| 259 |
|
| 260 |
st.markdown("---")
|
| 261 |
|
|
|
|
| 279 |
st.subheader("💰 Preço Médio por Mês (R$)")
|
| 280 |
price_table = past_filt.groupby(['month_str', 'city'])['native_rate_avg'].mean().unstack().fillna(0)
|
| 281 |
|
|
|
|
| 282 |
st.dataframe(price_table.style.format("R$ {:.2f}"), use_container_width=True)
|
| 283 |
else:
|
| 284 |
st.warning("Sem dados históricos.")
|
|
|
|
| 301 |
ax.imshow(wc, interpolation='bilinear')
|
| 302 |
ax.axis("off")
|
| 303 |
st.pyplot(fig)
|
| 304 |
+
plt.close(fig)
|
| 305 |
|
| 306 |
# --- PÁGINA 2: EXPLORATÓRIA ---
|
| 307 |
elif page == "📈 Análise Exploratória":
|
|
|
|
| 337 |
with col4:
|
| 338 |
st.subheader("Tipos de Imóveis")
|
| 339 |
|
|
|
|
|
|
|
| 340 |
df_counts = df_filtered['room_type'].value_counts().reset_index()
|
| 341 |
df_counts.columns = ['Tipo_Imovel', 'Contagem']
|
| 342 |
|
|
|
|
| 347 |
template="plotly_white")
|
| 348 |
st.plotly_chart(fig_bar, use_container_width=True)
|
| 349 |
|
| 350 |
+
# --- PÁGINA 3: SIMULADOR (COM SESSION STATE PARA NÃO SUMIR) ---
|
| 351 |
elif page == "🤖 Simulador IA":
|
| 352 |
st.title("🤖 Simulador Inteligente")
|
| 353 |
|
| 354 |
+
# Inicializa variáveis de estado se não existirem
|
| 355 |
+
if 'sim_result' not in st.session_state:
|
| 356 |
+
st.session_state['sim_result'] = None
|
| 357 |
+
if 'sim_coords' not in st.session_state:
|
| 358 |
+
st.session_state['sim_coords'] = None
|
| 359 |
+
if 'sim_params' not in st.session_state:
|
| 360 |
+
st.session_state['sim_params'] = None
|
| 361 |
+
|
| 362 |
c1, c2 = st.columns([1, 1.5])
|
| 363 |
|
| 364 |
with c1:
|
|
|
|
| 381 |
sim_pool = st.checkbox("Tem Piscina?", value=False)
|
| 382 |
sim_super = st.checkbox("Será Superhost?", value=True)
|
| 383 |
|
| 384 |
+
if st.button("🔮 Calcular Previsão"):
|
| 385 |
+
# Salva parâmetros na sessão
|
|
|
|
|
|
|
| 386 |
input_df = pd.DataFrame({
|
| 387 |
'bedrooms': [sim_bed], 'guests': [sim_guest],
|
| 388 |
'num_reviews': [30], 'latitude': [sim_lat], 'longitude': [sim_lon],
|
|
|
|
| 399 |
input_df = input_df[model_cols]
|
| 400 |
pred = model.predict(input_df)[0]
|
| 401 |
|
| 402 |
+
# ATUALIZA O ESTADO
|
| 403 |
+
st.session_state['sim_result'] = pred
|
| 404 |
+
st.session_state['sim_coords'] = [sim_lat, sim_lon]
|
| 405 |
+
st.session_state['sim_params'] = {'city': sim_city}
|
| 406 |
+
|
| 407 |
+
with c2:
|
| 408 |
+
# VERIFICA SE HÁ RESULTADO GRAVADO NA SESSÃO
|
| 409 |
+
if st.session_state['sim_result'] is not None:
|
| 410 |
+
pred = st.session_state['sim_result']
|
| 411 |
+
lat, lon = st.session_state['sim_coords']
|
| 412 |
+
city = st.session_state['sim_params']['city']
|
| 413 |
+
|
| 414 |
st.success(f"### 💰 Faturamento Estimado: R$ {pred:,.2f} / ano")
|
| 415 |
|
| 416 |
st.markdown("#### 📍 Localização Simulada")
|
| 417 |
+
m_sim = folium.Map(location=[lat, lon], zoom_start=14, tiles='CartoDB positron')
|
| 418 |
folium.Marker(
|
| 419 |
+
[lat, lon],
|
| 420 |
popup="Seu Imóvel Simulado",
|
| 421 |
icon=folium.Icon(color="red", icon="home")
|
| 422 |
).add_to(m_sim)
|
| 423 |
st_folium(m_sim, height=300, width="100%")
|
| 424 |
|
| 425 |
st.markdown("#### 🏠 Imóveis Reais Semelhantes (Vizinhança)")
|
| 426 |
+
df_city_neighbors = df_training[df_training['city'] == city].copy()
|
| 427 |
|
| 428 |
if not df_city_neighbors.empty:
|
| 429 |
nn = NearestNeighbors(n_neighbors=5, algorithm='ball_tree')
|
| 430 |
nn.fit(df_city_neighbors[['latitude', 'longitude']])
|
| 431 |
|
| 432 |
+
X_query = pd.DataFrame([[lat, lon]], columns=['latitude', 'longitude'])
|
|
|
|
| 433 |
distances, indices = nn.kneighbors(X_query)
|
| 434 |
|
| 435 |
neighbors = df_city_neighbors.iloc[indices[0]]
|