Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -7,8 +7,9 @@ from plotly.subplots import make_subplots
|
|
| 7 |
import folium
|
| 8 |
from streamlit_folium import st_folium
|
| 9 |
from folium.plugins import MarkerCluster, HeatMap
|
| 10 |
-
from sklearn.ensemble import RandomForestRegressor
|
| 11 |
from sklearn.neighbors import NearestNeighbors
|
|
|
|
| 12 |
from wordcloud import WordCloud
|
| 13 |
import matplotlib
|
| 14 |
import matplotlib.pyplot as plt
|
|
@@ -50,6 +51,13 @@ st.markdown("""
|
|
| 50 |
border-radius: 5px;
|
| 51 |
margin: 15px 0;
|
| 52 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
h1 {
|
| 54 |
color: #1e3a8a;
|
| 55 |
font-weight: 700;
|
|
@@ -206,10 +214,10 @@ def load_and_process_data():
|
|
| 206 |
|
| 207 |
score_df = score_df.sort_values('Score_Final', ascending=False)
|
| 208 |
|
| 209 |
-
return df, past, ranking, score_df, pop, ibge, fipe
|
| 210 |
|
| 211 |
# --- CARREGAMENTO ---
|
| 212 |
-
df, df_past, ranking_cidades, score_cidades, pop_data, ibge_data, fipe_data = load_and_process_data()
|
| 213 |
|
| 214 |
# --- TREINAMENTO MODELO IA ---
|
| 215 |
@st.cache_resource
|
|
@@ -225,11 +233,29 @@ def train_model(df):
|
|
| 225 |
X = pd.concat([X, pd.get_dummies(df_model['room_type'], prefix='type')], axis=1)
|
| 226 |
y = df_model['ttm_revenue_native']
|
| 227 |
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
-
model, model_cols, df_training = train_model(df)
|
| 233 |
|
| 234 |
# --- SIDEBAR ---
|
| 235 |
st.sidebar.image("https://upload.wikimedia.org/wikipedia/commons/6/69/Airbnb_Logo_Bélo.svg", width=140)
|
|
@@ -314,12 +340,82 @@ if page == "🎯 Painel Executivo":
|
|
| 314 |
cresc_melhor = ranking_cidades.loc[melhor_cidade, 'crescimento_pop_%']
|
| 315 |
st.metric(
|
| 316 |
"🌱 Crescimento Populacional",
|
| 317 |
-
f"+{cresc_melhor:.1f}%",
|
| 318 |
delta="2010-2023"
|
| 319 |
)
|
| 320 |
|
| 321 |
st.markdown("---")
|
| 322 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
# Ranking visual
|
| 324 |
st.subheader("🏆 Ranking Completo de Cidades")
|
| 325 |
|
|
@@ -524,7 +620,7 @@ elif page == "📈 Análise de Mercado":
|
|
| 524 |
|
| 525 |
st.plotly_chart(fig_pop, use_container_width=True)
|
| 526 |
|
| 527 |
-
# Crescimento percentual
|
| 528 |
pop_2010 = pop_data[pop_data['date'] == 2010].copy()
|
| 529 |
pop_2023 = pop_data[pop_data['date'] == 2023].copy()
|
| 530 |
|
|
@@ -541,6 +637,7 @@ elif page == "📈 Análise de Mercado":
|
|
| 541 |
pop_merged['Crescimento_%'] = ((pop_merged['Populacao_2023'] - pop_merged['Populacao_2010']) / pop_merged['Populacao_2010'] * 100).round(2)
|
| 542 |
pop_merged['city'] = pop_merged['city_norm'].str.title()
|
| 543 |
|
|
|
|
| 544 |
fig_cresc = px.bar(
|
| 545 |
pop_merged,
|
| 546 |
x='city',
|
|
@@ -549,11 +646,30 @@ elif page == "📈 Análise de Mercado":
|
|
| 549 |
labels={'city': 'Cidade', 'Crescimento_%': 'Crescimento (%)'},
|
| 550 |
color='Crescimento_%',
|
| 551 |
color_continuous_scale='RdYlGn',
|
| 552 |
-
template="plotly_white"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 553 |
)
|
| 554 |
|
| 555 |
st.plotly_chart(fig_cresc, use_container_width=True)
|
| 556 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
st.markdown("---")
|
| 558 |
|
| 559 |
# Segurança
|
|
@@ -687,7 +803,11 @@ elif page == "📅 Sazonalidade":
|
|
| 687 |
template="plotly_white"
|
| 688 |
)
|
| 689 |
|
| 690 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 691 |
st.plotly_chart(fig_sazon, use_container_width=True)
|
| 692 |
|
| 693 |
st.markdown("---")
|
|
@@ -752,12 +872,12 @@ elif page == "📅 Sazonalidade":
|
|
| 752 |
st.warning("⚠️ Sem dados históricos para as cidades selecionadas.")
|
| 753 |
|
| 754 |
# ============================================================================
|
| 755 |
-
# PÁGINA 5: MAPA INTERATIVO
|
| 756 |
# ============================================================================
|
| 757 |
elif page == "🗺️ Mapa Interativo":
|
| 758 |
st.title("🗺️ Mapa de Oportunidades")
|
| 759 |
|
| 760 |
-
st.markdown("**Clique nos marcadores para ver detalhes dos imóveis**")
|
| 761 |
|
| 762 |
if not df_filtered.empty:
|
| 763 |
center_lat = df_filtered['latitude'].mean()
|
|
@@ -765,7 +885,7 @@ elif page == "🗺️ Mapa Interativo":
|
|
| 765 |
|
| 766 |
m = folium.Map(location=[center_lat, center_lon], zoom_start=5, tiles='CartoDB positron')
|
| 767 |
|
| 768 |
-
# Adicionar marcadores por cidade
|
| 769 |
for cidade in cidades_sel:
|
| 770 |
df_cidade = df_filtered[df_filtered['city'] == cidade]
|
| 771 |
|
|
@@ -780,22 +900,36 @@ elif page == "🗺️ Mapa Interativo":
|
|
| 780 |
else:
|
| 781 |
color = 'red'
|
| 782 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 783 |
popup_html = f"""
|
| 784 |
-
<div style="width:
|
| 785 |
-
|
| 786 |
-
<
|
| 787 |
-
<
|
| 788 |
-
<p><strong>
|
| 789 |
-
<p><strong>
|
| 790 |
-
<p><strong>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 791 |
</div>
|
| 792 |
"""
|
| 793 |
|
| 794 |
folium.Marker(
|
| 795 |
location=[row['latitude'], row['longitude']],
|
| 796 |
popup=folium.Popup(popup_html, max_width=300),
|
| 797 |
-
tooltip=f"R$ {row['ttm_avg_rate_native']:.0f}/noite",
|
| 798 |
-
icon=folium.Icon(color=color, icon='home')
|
| 799 |
).add_to(m)
|
| 800 |
|
| 801 |
st_folium(m, height=600, width="100%")
|
|
@@ -808,28 +942,69 @@ elif page == "🗺️ Mapa Interativo":
|
|
| 808 |
st.subheader("🏆 Top 10 Imóveis por ROI")
|
| 809 |
|
| 810 |
top_imoveis = df_filtered.nlargest(10, 'ROI_anual')[
|
| 811 |
-
['listing_name', 'city', 'ttm_revenue_native', 'ROI_anual', 'ttm_occupancy', 'Recomendacao']
|
| 812 |
].copy()
|
| 813 |
|
| 814 |
-
top_imoveis.columns = ['Nome', 'Cidade', 'Faturamento Anual', 'ROI (%)', 'Ocupação', 'Recomendação']
|
| 815 |
top_imoveis['Ocupação'] = (top_imoveis['Ocupação'] * 100).round(1)
|
| 816 |
|
| 817 |
st.dataframe(
|
| 818 |
top_imoveis.style.format({
|
| 819 |
'Faturamento Anual': 'R$ {:,.2f}',
|
| 820 |
'ROI (%)': '{:.2f}%',
|
| 821 |
-
'Ocupação': '{:.1f}%'
|
|
|
|
| 822 |
}).background_gradient(subset=['ROI (%)'], cmap='Greens'),
|
| 823 |
use_container_width=True,
|
| 824 |
hide_index=True
|
| 825 |
)
|
| 826 |
|
| 827 |
# ============================================================================
|
| 828 |
-
# PÁGINA 6: SIMULADOR DE INVESTIMENTO
|
| 829 |
# ============================================================================
|
| 830 |
elif page == "🤖 Simulador de Investimento":
|
| 831 |
st.title("🤖 Simulador Inteligente de Investimento")
|
| 832 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 833 |
# Inicializa session state
|
| 834 |
if 'sim_result' not in st.session_state:
|
| 835 |
st.session_state['sim_result'] = None
|
|
@@ -853,13 +1028,13 @@ elif page == "🤖 Simulador de Investimento":
|
|
| 853 |
}
|
| 854 |
def_lat, def_lon = coords_dict.get(sim_city, [-23.55, -46.63])
|
| 855 |
|
| 856 |
-
sim_lat = st.number_input("Latitude:", value=def_lat, format="%.5f")
|
| 857 |
sim_lon = st.number_input("Longitude:", value=def_lon, format="%.5f")
|
| 858 |
sim_type = st.selectbox("Tipo:", df['room_type'].unique())
|
| 859 |
sim_bed = st.slider("Quartos:", 0, 8, 2)
|
| 860 |
sim_guest = st.slider("Hóspedes:", 1, 16, 4)
|
| 861 |
sim_pool = st.checkbox("Tem Piscina?", value=False)
|
| 862 |
-
sim_super = st.checkbox("Será Superhost?", value=True)
|
| 863 |
|
| 864 |
if st.button("🔮 Calcular Previsão", type="primary"):
|
| 865 |
input_df = pd.DataFrame({
|
|
@@ -890,13 +1065,15 @@ elif page == "🤖 Simulador de Investimento":
|
|
| 890 |
|
| 891 |
st.success(f"### 💰 Faturamento Estimado: R$ {pred:,.2f} / ano")
|
| 892 |
|
|
|
|
|
|
|
| 893 |
# Mapa
|
| 894 |
st.markdown("#### 📍 Localização Simulada")
|
| 895 |
m_sim = folium.Map(location=[lat, lon], zoom_start=14, tiles='CartoDB positron')
|
| 896 |
folium.Marker(
|
| 897 |
[lat, lon],
|
| 898 |
popup="Seu Imóvel Simulado",
|
| 899 |
-
icon=folium.Icon(color="red", icon="home")
|
| 900 |
).add_to(m_sim)
|
| 901 |
st_folium(m_sim, height=300, width="100%")
|
| 902 |
|
|
@@ -929,8 +1106,8 @@ elif page == "🤖 Simulador de Investimento":
|
|
| 929 |
|
| 930 |
with col_calc1:
|
| 931 |
val_imovel = st.number_input("Valor do Imóvel (R$):", value=400000.0, step=10000.0)
|
| 932 |
-
custo_fixo = st.number_input("Custo Mensal (R$):", value=800.0, step=100.0)
|
| 933 |
-
taxa_airbnb = st.slider("Taxa Airbnb (%):", 0, 20, 15)
|
| 934 |
|
| 935 |
with col_calc2:
|
| 936 |
custo_total = (pred * (taxa_airbnb/100)) + (custo_fixo * 12)
|
|
@@ -948,13 +1125,16 @@ elif page == "🤖 Simulador de Investimento":
|
|
| 948 |
st.info("⚠️ Investimento MODERADO")
|
| 949 |
else:
|
| 950 |
st.error("❌ Investimento NÃO RECOMENDADO")
|
|
|
|
|
|
|
| 951 |
|
| 952 |
# --- FOOTER ---
|
| 953 |
st.sidebar.markdown("---")
|
| 954 |
-
st.sidebar.markdown("""
|
| 955 |
<div style="text-align: center; color: #666; font-size: 0.8rem;">
|
| 956 |
<p><strong>Dashboard de Consultoria Imobiliária</strong></p>
|
| 957 |
<p>Análise de Investimento em Curta Temporada</p>
|
|
|
|
| 958 |
<p>Dados: Airbnb, FipeZap, IBGE</p>
|
| 959 |
</div>
|
| 960 |
""", unsafe_allow_html=True)
|
|
|
|
| 7 |
import folium
|
| 8 |
from streamlit_folium import st_folium
|
| 9 |
from folium.plugins import MarkerCluster, HeatMap
|
| 10 |
+
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
|
| 11 |
from sklearn.neighbors import NearestNeighbors
|
| 12 |
+
from sklearn.model_selection import cross_val_score
|
| 13 |
from wordcloud import WordCloud
|
| 14 |
import matplotlib
|
| 15 |
import matplotlib.pyplot as plt
|
|
|
|
| 51 |
border-radius: 5px;
|
| 52 |
margin: 15px 0;
|
| 53 |
}
|
| 54 |
+
.methodology-box {
|
| 55 |
+
background-color: #f0f4f8;
|
| 56 |
+
border: 2px solid #3b82f6;
|
| 57 |
+
padding: 20px;
|
| 58 |
+
border-radius: 10px;
|
| 59 |
+
margin: 20px 0;
|
| 60 |
+
}
|
| 61 |
h1 {
|
| 62 |
color: #1e3a8a;
|
| 63 |
font-weight: 700;
|
|
|
|
| 214 |
|
| 215 |
score_df = score_df.sort_values('Score_Final', ascending=False)
|
| 216 |
|
| 217 |
+
return df, past, ranking, score_df, pop, ibge, fipe, pesos
|
| 218 |
|
| 219 |
# --- CARREGAMENTO ---
|
| 220 |
+
df, df_past, ranking_cidades, score_cidades, pop_data, ibge_data, fipe_data, pesos_score = load_and_process_data()
|
| 221 |
|
| 222 |
# --- TREINAMENTO MODELO IA ---
|
| 223 |
@st.cache_resource
|
|
|
|
| 233 |
X = pd.concat([X, pd.get_dummies(df_model['room_type'], prefix='type')], axis=1)
|
| 234 |
y = df_model['ttm_revenue_native']
|
| 235 |
|
| 236 |
+
# Treinar múltiplos modelos e escolher o melhor
|
| 237 |
+
models = {
|
| 238 |
+
'Random Forest': RandomForestRegressor(n_estimators=100, max_depth=15, random_state=42, n_jobs=-1),
|
| 239 |
+
'Gradient Boosting': GradientBoostingRegressor(n_estimators=100, max_depth=5, random_state=42)
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
best_model = None
|
| 243 |
+
best_score = -np.inf
|
| 244 |
+
best_name = ""
|
| 245 |
+
|
| 246 |
+
for name, model in models.items():
|
| 247 |
+
scores = cross_val_score(model, X, y, cv=5, scoring='r2')
|
| 248 |
+
mean_score = scores.mean()
|
| 249 |
+
if mean_score > best_score:
|
| 250 |
+
best_score = mean_score
|
| 251 |
+
best_model = model
|
| 252 |
+
best_name = name
|
| 253 |
+
|
| 254 |
+
best_model.fit(X, y)
|
| 255 |
+
|
| 256 |
+
return best_model, X.columns, df_model, best_name, best_score
|
| 257 |
|
| 258 |
+
model, model_cols, df_training, model_name, model_score = train_model(df)
|
| 259 |
|
| 260 |
# --- SIDEBAR ---
|
| 261 |
st.sidebar.image("https://upload.wikimedia.org/wikipedia/commons/6/69/Airbnb_Logo_Bélo.svg", width=140)
|
|
|
|
| 340 |
cresc_melhor = ranking_cidades.loc[melhor_cidade, 'crescimento_pop_%']
|
| 341 |
st.metric(
|
| 342 |
"🌱 Crescimento Populacional",
|
| 343 |
+
f"+{cresc_melhor:.1f}%" if cresc_melhor >= 0 else f"{cresc_melhor:.1f}%",
|
| 344 |
delta="2010-2023"
|
| 345 |
)
|
| 346 |
|
| 347 |
st.markdown("---")
|
| 348 |
|
| 349 |
+
# NOVA SEÇÃO: EXPLICAÇÃO DO CÁLCULO DO SCORE
|
| 350 |
+
st.subheader("📐 Como Calculamos o Score de Investimento?")
|
| 351 |
+
|
| 352 |
+
st.markdown("""
|
| 353 |
+
<div class="methodology-box">
|
| 354 |
+
<h3 style="color: #1e3a8a; margin-top: 0;">🔬 Metodologia de Cálculo</h3>
|
| 355 |
+
<p>O <strong>Score de Investimento</strong> é uma métrica ponderada que integra <strong>6 indicadores-chave</strong>
|
| 356 |
+
para fornecer uma avaliação objetiva e comparável entre as cidades.</p>
|
| 357 |
+
</div>
|
| 358 |
+
""", unsafe_allow_html=True)
|
| 359 |
+
|
| 360 |
+
col_method1, col_method2 = st.columns([1, 1])
|
| 361 |
+
|
| 362 |
+
with col_method1:
|
| 363 |
+
st.markdown("#### 📊 Indicadores e Pesos")
|
| 364 |
+
|
| 365 |
+
# Criar DataFrame com os pesos
|
| 366 |
+
pesos_df = pd.DataFrame({
|
| 367 |
+
'Indicador': ['ROI Anual', 'Faturamento Médio', 'Taxa de Ocupação',
|
| 368 |
+
'Crescimento Populacional', 'Segurança', 'Demanda (Reviews)'],
|
| 369 |
+
'Peso (%)': [pesos_score['ROI']*100, pesos_score['Faturamento']*100,
|
| 370 |
+
pesos_score['Ocupacao']*100, pesos_score['Crescimento_Pop']*100,
|
| 371 |
+
pesos_score['Seguranca']*100, pesos_score['Demanda']*100],
|
| 372 |
+
'Justificativa': [
|
| 373 |
+
'Rentabilidade é o fator mais importante',
|
| 374 |
+
'Receita absoluta indica potencial de mercado',
|
| 375 |
+
'Demanda consistente reduz risco de vacância',
|
| 376 |
+
'Indica potencial de valorização futura',
|
| 377 |
+
'Afeta atratividade turística',
|
| 378 |
+
'Histórico de procura indica demanda'
|
| 379 |
+
]
|
| 380 |
+
})
|
| 381 |
+
|
| 382 |
+
st.dataframe(
|
| 383 |
+
pesos_df.style.background_gradient(subset=['Peso (%)'], cmap='Blues'),
|
| 384 |
+
use_container_width=True,
|
| 385 |
+
hide_index=True
|
| 386 |
+
)
|
| 387 |
+
|
| 388 |
+
with col_method2:
|
| 389 |
+
st.markdown("#### 🧮 Fórmula de Cálculo")
|
| 390 |
+
|
| 391 |
+
st.latex(r'''
|
| 392 |
+
Score = \sum_{i=1}^{6} (Indicador_i^{norm} \times Peso_i)
|
| 393 |
+
''')
|
| 394 |
+
|
| 395 |
+
st.markdown("""
|
| 396 |
+
**Onde:**
|
| 397 |
+
- **Indicador<sub>i</sub><sup>norm</sup>**: Valor normalizado (0-100) de cada indicador
|
| 398 |
+
- **Peso<sub>i</sub>**: Peso percentual do indicador
|
| 399 |
+
|
| 400 |
+
**Normalização:**
|
| 401 |
+
""", unsafe_allow_html=True)
|
| 402 |
+
|
| 403 |
+
st.latex(r'''
|
| 404 |
+
Valor^{norm} = \frac{Valor - Min}{Max - Min} \times 100
|
| 405 |
+
''')
|
| 406 |
+
|
| 407 |
+
st.markdown("""
|
| 408 |
+
**Exceção:** Segurança é invertida (menos homicídios = maior score)
|
| 409 |
+
|
| 410 |
+
**Resultado:** Score final entre 0 e 100
|
| 411 |
+
- **≥ 70**: ALTAMENTE RECOMENDADO
|
| 412 |
+
- **50-69**: RECOMENDADO
|
| 413 |
+
- **30-49**: MODERADO
|
| 414 |
+
- **< 30**: NÃO RECOMENDADO
|
| 415 |
+
""")
|
| 416 |
+
|
| 417 |
+
st.markdown("---")
|
| 418 |
+
|
| 419 |
# Ranking visual
|
| 420 |
st.subheader("🏆 Ranking Completo de Cidades")
|
| 421 |
|
|
|
|
| 620 |
|
| 621 |
st.plotly_chart(fig_pop, use_container_width=True)
|
| 622 |
|
| 623 |
+
# Crescimento percentual - CORRIGIDO PARA MOSTRAR VALORES NEGATIVOS
|
| 624 |
pop_2010 = pop_data[pop_data['date'] == 2010].copy()
|
| 625 |
pop_2023 = pop_data[pop_data['date'] == 2023].copy()
|
| 626 |
|
|
|
|
| 637 |
pop_merged['Crescimento_%'] = ((pop_merged['Populacao_2023'] - pop_merged['Populacao_2010']) / pop_merged['Populacao_2010'] * 100).round(2)
|
| 638 |
pop_merged['city'] = pop_merged['city_norm'].str.title()
|
| 639 |
|
| 640 |
+
# CORREÇÃO: Gráfico que mostra valores negativos corretamente
|
| 641 |
fig_cresc = px.bar(
|
| 642 |
pop_merged,
|
| 643 |
x='city',
|
|
|
|
| 646 |
labels={'city': 'Cidade', 'Crescimento_%': 'Crescimento (%)'},
|
| 647 |
color='Crescimento_%',
|
| 648 |
color_continuous_scale='RdYlGn',
|
| 649 |
+
template="plotly_white",
|
| 650 |
+
text='Crescimento_%'
|
| 651 |
+
)
|
| 652 |
+
|
| 653 |
+
# Adicionar linha de referência em zero
|
| 654 |
+
fig_cresc.add_hline(y=0, line_dash="dash", line_color="gray", annotation_text="Zero")
|
| 655 |
+
|
| 656 |
+
# Formatar texto das barras
|
| 657 |
+
fig_cresc.update_traces(texttemplate='%{text:.2f}%', textposition='outside')
|
| 658 |
+
|
| 659 |
+
# Garantir que o eixo Y inclui valores negativos
|
| 660 |
+
fig_cresc.update_yaxes(
|
| 661 |
+
title="Crescimento (%)",
|
| 662 |
+
zeroline=True,
|
| 663 |
+
zerolinewidth=2,
|
| 664 |
+
zerolinecolor='gray'
|
| 665 |
)
|
| 666 |
|
| 667 |
st.plotly_chart(fig_cresc, use_container_width=True)
|
| 668 |
|
| 669 |
+
# Adicionar insight sobre Porto Alegre
|
| 670 |
+
if any(pop_merged['Crescimento_%'] < 0):
|
| 671 |
+
st.warning("⚠️ **Atenção:** Porto Alegre apresentou **decrescimento populacional** (-0,04%) no período 2010-2023, indicando perda de atratividade econômica e potencial impacto negativo no mercado imobiliário.")
|
| 672 |
+
|
| 673 |
st.markdown("---")
|
| 674 |
|
| 675 |
# Segurança
|
|
|
|
| 803 |
template="plotly_white"
|
| 804 |
)
|
| 805 |
|
| 806 |
+
# CORREÇÃO: Usar update_layout ao invés de update_yaxis
|
| 807 |
+
fig_sazon.update_layout(
|
| 808 |
+
yaxis=dict(tickformat='.0%')
|
| 809 |
+
)
|
| 810 |
+
|
| 811 |
st.plotly_chart(fig_sazon, use_container_width=True)
|
| 812 |
|
| 813 |
st.markdown("---")
|
|
|
|
| 872 |
st.warning("⚠️ Sem dados históricos para as cidades selecionadas.")
|
| 873 |
|
| 874 |
# ============================================================================
|
| 875 |
+
# PÁGINA 5: MAPA INTERATIVO (MELHORADO)
|
| 876 |
# ============================================================================
|
| 877 |
elif page == "🗺️ Mapa Interativo":
|
| 878 |
st.title("🗺️ Mapa de Oportunidades")
|
| 879 |
|
| 880 |
+
st.markdown("**Clique nos marcadores para ver detalhes completos dos imóveis (incluindo foto)**")
|
| 881 |
|
| 882 |
if not df_filtered.empty:
|
| 883 |
center_lat = df_filtered['latitude'].mean()
|
|
|
|
| 885 |
|
| 886 |
m = folium.Map(location=[center_lat, center_lon], zoom_start=5, tiles='CartoDB positron')
|
| 887 |
|
| 888 |
+
# Adicionar marcadores por cidade com FOTO e informações detalhadas
|
| 889 |
for cidade in cidades_sel:
|
| 890 |
df_cidade = df_filtered[df_filtered['city'] == cidade]
|
| 891 |
|
|
|
|
| 900 |
else:
|
| 901 |
color = 'red'
|
| 902 |
|
| 903 |
+
# HTML melhorado com FOTO
|
| 904 |
+
foto_url = row.get('cover_photo_url', '')
|
| 905 |
+
foto_html = f'<img src="{foto_url}" width="250" style="border-radius: 5px; margin-bottom: 10px;">' if pd.notna(foto_url) and foto_url != '' else ''
|
| 906 |
+
|
| 907 |
popup_html = f"""
|
| 908 |
+
<div style="width: 270px; font-family: Arial, sans-serif;">
|
| 909 |
+
{foto_html}
|
| 910 |
+
<h4 style="margin: 5px 0; color: #1e3a8a;">{row['listing_name']}</h4>
|
| 911 |
+
<hr style="margin: 8px 0;">
|
| 912 |
+
<p style="margin: 3px 0;"><strong>📍 Cidade:</strong> {row['city']}</p>
|
| 913 |
+
<p style="margin: 3px 0;"><strong>🏠 Tipo:</strong> {row['room_type']}</p>
|
| 914 |
+
<p style="margin: 3px 0;"><strong>🛏️ Quartos:</strong> {int(row['bedrooms'])}</p>
|
| 915 |
+
<p style="margin: 3px 0;"><strong>👥 Hóspedes:</strong> {int(row['guests'])}</p>
|
| 916 |
+
<hr style="margin: 8px 0;">
|
| 917 |
+
<p style="margin: 3px 0;"><strong>💰 Faturamento Anual:</strong> <span style="color: #10b981;">R$ {row['ttm_revenue_native']:,.2f}</span></p>
|
| 918 |
+
<p style="margin: 3px 0;"><strong>📈 ROI:</strong> <span style="color: #3b82f6;">{row['ROI_anual']:.2f}%</span></p>
|
| 919 |
+
<p style="margin: 3px 0;"><strong>📊 Ocupação:</strong> {row['ttm_occupancy']*100:.1f}%</p>
|
| 920 |
+
<p style="margin: 3px 0;"><strong>⭐ Avaliação:</strong> {row['rating_overall']:.1f}/5.0</p>
|
| 921 |
+
<hr style="margin: 8px 0;">
|
| 922 |
+
<p style="margin: 5px 0; padding: 5px; background-color: #f0f4f8; border-radius: 3px; text-align: center;">
|
| 923 |
+
<strong>{row['Recomendacao']}</strong>
|
| 924 |
+
</p>
|
| 925 |
</div>
|
| 926 |
"""
|
| 927 |
|
| 928 |
folium.Marker(
|
| 929 |
location=[row['latitude'], row['longitude']],
|
| 930 |
popup=folium.Popup(popup_html, max_width=300),
|
| 931 |
+
tooltip=f"💵 R$ {row['ttm_avg_rate_native']:.0f}/noite | {row['listing_name'][:30]}...",
|
| 932 |
+
icon=folium.Icon(color=color, icon='home', prefix='fa')
|
| 933 |
).add_to(m)
|
| 934 |
|
| 935 |
st_folium(m, height=600, width="100%")
|
|
|
|
| 942 |
st.subheader("🏆 Top 10 Imóveis por ROI")
|
| 943 |
|
| 944 |
top_imoveis = df_filtered.nlargest(10, 'ROI_anual')[
|
| 945 |
+
['listing_name', 'city', 'ttm_revenue_native', 'ROI_anual', 'ttm_occupancy', 'rating_overall', 'Recomendacao']
|
| 946 |
].copy()
|
| 947 |
|
| 948 |
+
top_imoveis.columns = ['Nome', 'Cidade', 'Faturamento Anual', 'ROI (%)', 'Ocupação', 'Avaliação', 'Recomendação']
|
| 949 |
top_imoveis['Ocupação'] = (top_imoveis['Ocupação'] * 100).round(1)
|
| 950 |
|
| 951 |
st.dataframe(
|
| 952 |
top_imoveis.style.format({
|
| 953 |
'Faturamento Anual': 'R$ {:,.2f}',
|
| 954 |
'ROI (%)': '{:.2f}%',
|
| 955 |
+
'Ocupação': '{:.1f}%',
|
| 956 |
+
'Avaliação': '{:.1f}'
|
| 957 |
}).background_gradient(subset=['ROI (%)'], cmap='Greens'),
|
| 958 |
use_container_width=True,
|
| 959 |
hide_index=True
|
| 960 |
)
|
| 961 |
|
| 962 |
# ============================================================================
|
| 963 |
+
# PÁGINA 6: SIMULADOR DE INVESTIMENTO (APRIMORADO)
|
| 964 |
# ============================================================================
|
| 965 |
elif page == "🤖 Simulador de Investimento":
|
| 966 |
st.title("🤖 Simulador Inteligente de Investimento")
|
| 967 |
|
| 968 |
+
# NOVA SEÇÃO: Explicação do Modelo
|
| 969 |
+
with st.expander("ℹ️ Sobre o Modelo de Predição", expanded=False):
|
| 970 |
+
st.markdown(f"""
|
| 971 |
+
### 🔬 Modelo Utilizado: **{model_name}**
|
| 972 |
+
|
| 973 |
+
Este simulador utiliza **Machine Learning** para prever o faturamento anual de imóveis de curta temporada.
|
| 974 |
+
|
| 975 |
+
#### 📊 Características do Modelo
|
| 976 |
+
|
| 977 |
+
- **Algoritmo:** {model_name}
|
| 978 |
+
- **Acurácia (R²):** {model_score:.3f} ({model_score*100:.1f}%)
|
| 979 |
+
- **Variáveis Preditoras:**
|
| 980 |
+
- Número de quartos
|
| 981 |
+
- Capacidade de hóspedes
|
| 982 |
+
- Número de reviews (histórico de demanda)
|
| 983 |
+
- Localização (latitude e longitude)
|
| 984 |
+
- Status de Superhost
|
| 985 |
+
- Amenidades (ex: piscina)
|
| 986 |
+
- Cidade
|
| 987 |
+
- Tipo de imóvel
|
| 988 |
+
|
| 989 |
+
#### 🎯 Como Funciona?
|
| 990 |
+
|
| 991 |
+
O modelo foi treinado com **{len(df_training)} imóveis reais** e aprendeu padrões entre as características
|
| 992 |
+
dos imóveis e seu faturamento anual. Ele considera:
|
| 993 |
+
|
| 994 |
+
1. **Localização:** Imóveis em áreas turísticas tendem a faturar mais
|
| 995 |
+
2. **Capacidade:** Mais quartos/hóspedes = maior potencial de receita
|
| 996 |
+
3. **Reputação:** Superhosts e imóveis bem avaliados têm ocupação maior
|
| 997 |
+
4. **Amenidades:** Piscina e outras comodidades aumentam o valor da diária
|
| 998 |
+
5. **Cidade:** Cada cidade tem perfil de rentabilidade diferente
|
| 999 |
+
|
| 1000 |
+
#### ⚠️ Limitações
|
| 1001 |
+
|
| 1002 |
+
- Previsões são baseadas em dados históricos (2023-2024)
|
| 1003 |
+
- Não considera eventos futuros (mudanças econômicas, regulação, etc.)
|
| 1004 |
+
- Margem de erro: ±15-20% em média
|
| 1005 |
+
- Use como **referência inicial**, não como garantia
|
| 1006 |
+
""")
|
| 1007 |
+
|
| 1008 |
# Inicializa session state
|
| 1009 |
if 'sim_result' not in st.session_state:
|
| 1010 |
st.session_state['sim_result'] = None
|
|
|
|
| 1028 |
}
|
| 1029 |
def_lat, def_lon = coords_dict.get(sim_city, [-23.55, -46.63])
|
| 1030 |
|
| 1031 |
+
sim_lat = st.number_input("Latitude:", value=def_lat, format="%.5f", help="Use Google Maps para encontrar coordenadas exatas")
|
| 1032 |
sim_lon = st.number_input("Longitude:", value=def_lon, format="%.5f")
|
| 1033 |
sim_type = st.selectbox("Tipo:", df['room_type'].unique())
|
| 1034 |
sim_bed = st.slider("Quartos:", 0, 8, 2)
|
| 1035 |
sim_guest = st.slider("Hóspedes:", 1, 16, 4)
|
| 1036 |
sim_pool = st.checkbox("Tem Piscina?", value=False)
|
| 1037 |
+
sim_super = st.checkbox("Será Superhost?", value=True, help="Superhosts têm maior ocupação e podem cobrar diárias mais altas")
|
| 1038 |
|
| 1039 |
if st.button("🔮 Calcular Previsão", type="primary"):
|
| 1040 |
input_df = pd.DataFrame({
|
|
|
|
| 1065 |
|
| 1066 |
st.success(f"### 💰 Faturamento Estimado: R$ {pred:,.2f} / ano")
|
| 1067 |
|
| 1068 |
+
st.info(f"📊 **Modelo:** {model_name} | **Confiança:** {model_score*100:.1f}%")
|
| 1069 |
+
|
| 1070 |
# Mapa
|
| 1071 |
st.markdown("#### 📍 Localização Simulada")
|
| 1072 |
m_sim = folium.Map(location=[lat, lon], zoom_start=14, tiles='CartoDB positron')
|
| 1073 |
folium.Marker(
|
| 1074 |
[lat, lon],
|
| 1075 |
popup="Seu Imóvel Simulado",
|
| 1076 |
+
icon=folium.Icon(color="red", icon="home", prefix='fa')
|
| 1077 |
).add_to(m_sim)
|
| 1078 |
st_folium(m_sim, height=300, width="100%")
|
| 1079 |
|
|
|
|
| 1106 |
|
| 1107 |
with col_calc1:
|
| 1108 |
val_imovel = st.number_input("Valor do Imóvel (R$):", value=400000.0, step=10000.0)
|
| 1109 |
+
custo_fixo = st.number_input("Custo Mensal (R$):", value=800.0, step=100.0, help="Condomínio, IPTU, manutenção, etc.")
|
| 1110 |
+
taxa_airbnb = st.slider("Taxa Airbnb (%):", 0, 20, 15, help="Comissão da plataforma")
|
| 1111 |
|
| 1112 |
with col_calc2:
|
| 1113 |
custo_total = (pred * (taxa_airbnb/100)) + (custo_fixo * 12)
|
|
|
|
| 1125 |
st.info("⚠️ Investimento MODERADO")
|
| 1126 |
else:
|
| 1127 |
st.error("❌ Investimento NÃO RECOMENDADO")
|
| 1128 |
+
else:
|
| 1129 |
+
st.info("👈 Configure o imóvel à esquerda e clique em 'Calcular Previsão' para ver os resultados.")
|
| 1130 |
|
| 1131 |
# --- FOOTER ---
|
| 1132 |
st.sidebar.markdown("---")
|
| 1133 |
+
st.sidebar.markdown(f"""
|
| 1134 |
<div style="text-align: center; color: #666; font-size: 0.8rem;">
|
| 1135 |
<p><strong>Dashboard de Consultoria Imobiliária</strong></p>
|
| 1136 |
<p>Análise de Investimento em Curta Temporada</p>
|
| 1137 |
+
<p>Modelo: {model_name} (R²: {model_score:.2f})</p>
|
| 1138 |
<p>Dados: Airbnb, FipeZap, IBGE</p>
|
| 1139 |
</div>
|
| 1140 |
""", unsafe_allow_html=True)
|