PSP / app.py
enzograndino's picture
Update app.py
b961d3b verified
import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import folium
from streamlit_folium import st_folium
from folium.plugins import MarkerCluster, HeatMap
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.neighbors import NearestNeighbors
from sklearn.model_selection import cross_val_score
from wordcloud import WordCloud
import matplotlib
import matplotlib.pyplot as plt
import unicodedata
# --- CONFIGURAÇÃO DE BACKEND ---
matplotlib.use('Agg')
# --- CONFIGURAÇÃO DA PÁGINA ---
st.set_page_config(
page_title="Consultoria Imobiliária - Curta Temporada",
layout="wide",
page_icon="🏖️",
initial_sidebar_state="expanded"
)
# --- CSS CUSTOMIZADO ---
st.markdown("""
<style>
.metric-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
text-align: center;
}
.ranking-card {
background-color: #f8f9fa;
border-left: 5px solid #FF5A5F;
padding: 15px;
border-radius: 5px;
margin: 10px 0;
}
.insight-box {
background-color: #e3f2fd;
border-left: 4px solid #2196F3;
padding: 15px;
border-radius: 5px;
margin: 15px 0;
}
.methodology-box {
background-color: #f0f4f8;
border: 2px solid #3b82f6;
padding: 20px;
border-radius: 10px;
margin: 20px 0;
}
h1 {
color: #1e3a8a;
font-weight: 700;
}
h2 {
color: #3b82f6;
font-weight: 600;
}
.stTabs [data-baseweb="tab-list"] {
gap: 8px;
}
.stTabs [data-baseweb="tab"] {
background-color: #f1f5f9;
border-radius: 5px;
padding: 10px 20px;
}
</style>
""", unsafe_allow_html=True)
# --- FUNÇÃO DE NORMALIZAÇÃO ---
def normalizar_texto(texto):
if pd.isna(texto): return ""
nfkd = unicodedata.normalize('NFKD', str(texto))
return u"".join([c for c in nfkd if not unicodedata.combining(c)]).lower().strip()
# --- CARREGAMENTO E PROCESSAMENTO DE DADOS ---
@st.cache_data(ttl=3600)
def load_and_process_data():
try:
df = pd.read_csv('lisitng_geral.csv', sep=';', encoding='latin1')
fipe = pd.read_csv('fipezap_geral.csv', sep=';', encoding='latin1')
past = pd.read_csv('past_geral.csv', sep=';', encoding='latin1')
seg = pd.read_csv('casos_homicidios.csv', sep=';', encoding='latin1')
pop = pd.read_csv('População_geral.csv', sep=';', encoding='latin1')
ibge = pd.read_csv('dados_ibge.csv', sep=';', encoding='latin1')
except:
st.error("❌ Erro ao carregar arquivos CSV. Verifique se todos os arquivos estão no diretório.")
st.stop()
# 1. Limpeza de coordenadas
cols_coords = ['latitude', 'longitude']
for col in cols_coords:
if df[col].dtype == 'object':
df[col] = df[col].str.replace(',', '.').astype(float)
df = df.dropna(subset=cols_coords)
# 2. Limpeza financeira
cols_fin = ['ttm_revenue_native', 'ttm_avg_rate_native', 'ttm_occupancy', 'rating_overall']
for col in cols_fin:
if col in df.columns and df[col].dtype == 'object':
df[col] = df[col].astype(str).str.replace('R$', '').str.replace('.', '').str.replace(',', '.').astype(float)
# 3. Normalização de nomes
df['city_norm'] = df['city'].apply(normalizar_texto)
df['city'] = df['city'].str.title()
# 4. Imputação
df['bedrooms'] = df['bedrooms'].fillna(df.groupby(['city', 'room_type'])['bedrooms'].transform('median')).fillna(1)
df['guests'] = df['guests'].fillna(df.groupby(['city', 'room_type'])['guests'].transform('median')).fillna(2)
# 5. Preço m² (FipeZap)
fipe = fipe.dropna(subset=['city', 'preco_m2'])
fipe['city_norm'] = fipe['city'].apply(normalizar_texto)
fipe_recent = fipe.sort_values('date', ascending=False).groupby('city_norm')['preco_m2'].first().to_dict()
df['preco_m2_estimado'] = df['city_norm'].map(fipe_recent)
df['preco_m2_estimado'] = df['preco_m2_estimado'].fillna(fipe['preco_m2'].mean())
# 6. Cálculo de valor do imóvel e ROI
def estimar_metragem(n):
return 45 if n <= 1 else (75 if n == 2 else 110)
df['metragem_estimada'] = df['bedrooms'].apply(estimar_metragem)
df['valor_imovel_estimado'] = df['metragem_estimada'] * df['preco_m2_estimado']
df['ROI_anual'] = (df['ttm_revenue_native'] / df['valor_imovel_estimado']) * 100
df['ROI_anual'] = df['ROI_anual'].replace([np.inf, -np.inf], 0).fillna(0)
# 7. Score individual de imóveis
def normalize(series):
return (series - series.min()) / (series.max() - series.min())
df['Score'] = ((normalize(df['ROI_anual']) * 60) + (normalize(df['rating_overall'].fillna(0)) * 40)) * 100
def gerar_laudo(row):
if row['Score'] >= 60: return "💎 COMPRA RECOMENDADA"
elif row['Score'] >= 40: return "✅ POTENCIAL"
elif row['Score'] >= 20: return "⚠️ ARRISCADO"
else: return "❌ NÃO RECOMENDADO"
df['Recomendacao'] = df.apply(gerar_laudo, axis=1)
# 8. Dados de segurança
seg['city_norm'] = seg['city'].apply(normalizar_texto)
seg_recent = seg.sort_values('date', ascending=False).groupby('city_norm')['Homicidios'].first().reset_index()
df = pd.merge(df, seg_recent, on='city_norm', how='left')
df['Homicidios'] = df['Homicidios'].fillna(-1)
# 9. Dados históricos
past['date'] = pd.to_datetime(past['date'], dayfirst=True, errors='coerce')
past['city_norm'] = past['city'].apply(normalizar_texto)
past['month'] = past['date'].dt.to_period('M').astype(str)
past['month_name'] = past['date'].dt.month_name()
# 10. População
pop['city_norm'] = pop['city'].apply(normalizar_texto)
pop_recent = pop.sort_values('date', ascending=False).groupby('city_norm')['Populacao'].first()
# 11. IBGE
ibge['city_norm'] = ibge['city'].apply(normalizar_texto)
# 12. CRIAR RANKING DE CIDADES
ranking = df.groupby('city').agg({
'ttm_revenue_native': 'mean',
'ttm_avg_rate_native': 'mean',
'ttm_occupancy': 'mean',
'valor_imovel_estimado': 'mean',
'ROI_anual': 'mean',
'num_reviews': 'mean'
}).round(2)
# Adicionar dados externos
seg_recent_dict = seg.sort_values('date', ascending=False).groupby('city_norm')['Homicidios'].first()
pop_2010 = pop[pop['date'] == 2010].set_index('city_norm')['Populacao']
pop_2023 = pop[pop['date'] == 2023].set_index('city_norm')['Populacao']
crescimento = ((pop_2023 - pop_2010) / pop_2010 * 100).round(2)
ranking['populacao'] = ranking.index.map(lambda x: pop_recent.get(normalizar_texto(x), 0))
ranking['homicidios'] = ranking.index.map(lambda x: seg_recent_dict.get(normalizar_texto(x), 0))
ranking['crescimento_pop_%'] = ranking.index.map(lambda x: crescimento.get(normalizar_texto(x), 0))
ranking['taxa_homicidios_100k'] = (ranking['homicidios'] / ranking['populacao'] * 100000).round(2)
# 13. SCORE DE CIDADES
def normalize_series(series):
return (series - series.min()) / (series.max() - series.min()) * 100
score_df = pd.DataFrame()
score_df['ROI'] = normalize_series(ranking['ROI_anual'])
score_df['Ocupacao'] = normalize_series(ranking['ttm_occupancy'])
score_df['Faturamento'] = normalize_series(ranking['ttm_revenue_native'])
score_df['Crescimento_Pop'] = normalize_series(ranking['crescimento_pop_%'])
score_df['Seguranca'] = 100 - normalize_series(ranking['taxa_homicidios_100k'])
score_df['Demanda'] = normalize_series(ranking['num_reviews'])
pesos = {'ROI': 0.30, 'Faturamento': 0.25, 'Ocupacao': 0.20,
'Crescimento_Pop': 0.10, 'Seguranca': 0.10, 'Demanda': 0.05}
score_df['Score_Final'] = (
score_df['ROI'] * pesos['ROI'] +
score_df['Faturamento'] * pesos['Faturamento'] +
score_df['Ocupacao'] * pesos['Ocupacao'] +
score_df['Crescimento_Pop'] * pesos['Crescimento_Pop'] +
score_df['Seguranca'] * pesos['Seguranca'] +
score_df['Demanda'] * pesos['Demanda']
).round(2)
score_df = score_df.sort_values('Score_Final', ascending=False)
return df, past, ranking, score_df, pop, ibge, fipe, pesos
# --- CARREGAMENTO ---
df, df_past, ranking_cidades, score_cidades, pop_data, ibge_data, fipe_data, pesos_score = load_and_process_data()
# --- TREINAMENTO MODELO IA ---
@st.cache_resource
def train_model(df):
df_model = df[df['ttm_revenue_native'] > 0].copy()
features = ['bedrooms', 'guests', 'num_reviews', 'latitude', 'longitude']
df_model['is_superhost'] = df_model['superhost'].astype(str).apply(lambda x: 1 if x.upper() in ['VERDADEIRO', 'TRUE'] else 0)
df_model['has_pool'] = df_model['amenities'].astype(str).str.contains('pool|Piscina', case=False).astype(int)
X = df_model[features + ['is_superhost', 'has_pool']]
X = pd.concat([X, pd.get_dummies(df_model['city'], prefix='city')], axis=1)
X = pd.concat([X, pd.get_dummies(df_model['room_type'], prefix='type')], axis=1)
y = df_model['ttm_revenue_native']
# Treinar múltiplos modelos e escolher o melhor
models = {
'Random Forest': RandomForestRegressor(n_estimators=100, max_depth=15, random_state=42, n_jobs=-1),
'Gradient Boosting': GradientBoostingRegressor(n_estimators=100, max_depth=5, random_state=42)
}
best_model = None
best_score = -np.inf
best_name = ""
for name, model in models.items():
scores = cross_val_score(model, X, y, cv=5, scoring='r2')
mean_score = scores.mean()
if mean_score > best_score:
best_score = mean_score
best_model = model
best_name = name
best_model.fit(X, y)
return best_model, X.columns, df_model, best_name, best_score
model, model_cols, df_training, model_name, model_score = train_model(df)
# --- SIDEBAR ---
st.sidebar.image("https://upload.wikimedia.org/wikipedia/commons/6/69/Airbnb_Logo_Bélo.svg", width=140)
st.sidebar.title("🏖️ Consultoria Imobiliária")
st.sidebar.markdown("**Análise de Investimento em Curta Temporada**")
st.sidebar.markdown("---")
page = st.sidebar.radio(
"📍 Navegação Principal",
[
"🎯 Painel Executivo",
"📊 Comparação de Cidades",
"📈 Análise de Mercado",
"📅 Sazonalidade",
"🗺️ Mapa Interativo",
"🤖 Simulador de Investimento"
]
)
st.sidebar.markdown("---")
st.sidebar.header("🔍 Filtros Globais")
cidades_disponiveis = sorted(df['city'].unique())
cidades_sel = st.sidebar.multiselect("Cidades:", cidades_disponiveis, default=cidades_disponiveis)
quartos = st.sidebar.slider("Quartos:", 0, 8, (0, 8))
df_filtered = df[
(df['city'].isin(cidades_sel)) &
(df['bedrooms'].between(quartos[0], quartos[1]))
]
# ============================================================================
# PÁGINA 1: PAINEL EXECUTIVO
# ============================================================================
if page == "🎯 Painel Executivo":
st.title("🎯 Painel Executivo - Recomendação de Investimento")
# Insight principal
melhor_cidade = score_cidades.index[0]
score_melhor = score_cidades.loc[melhor_cidade, 'Score_Final']
st.markdown(f"""
<div class="insight-box">
<h2>💡 Recomendação Principal</h2>
<h1 style="color: #10b981; font-size: 2.5rem;">🏆 {melhor_cidade.upper()}</h1>
<p style="font-size: 1.2rem;"><strong>Score de Investimento: {score_melhor}/100</strong></p>
<p>Com base na análise de <strong>rentabilidade, ocupação, crescimento populacional, segurança e demanda</strong>,
<strong>{melhor_cidade}</strong> é a cidade mais recomendada para investimento em imóveis de curta temporada.</p>
</div>
""", unsafe_allow_html=True)
st.markdown("---")
# KPIs principais
col1, col2, col3, col4 = st.columns(4)
with col1:
faturamento_melhor = ranking_cidades.loc[melhor_cidade, 'ttm_revenue_native']
st.metric(
"💰 Faturamento Médio Anual",
f"R$ {faturamento_melhor:,.0f}",
delta=f"+{((faturamento_melhor / ranking_cidades['ttm_revenue_native'].mean() - 1) * 100):.1f}% vs. média"
)
with col2:
roi_melhor = ranking_cidades.loc[melhor_cidade, 'ROI_anual']
st.metric(
"📈 ROI Anual",
f"{roi_melhor:.2f}%",
delta=f"+{((roi_melhor / ranking_cidades['ROI_anual'].mean() - 1) * 100):.1f}% vs. média"
)
with col3:
ocupacao_melhor = ranking_cidades.loc[melhor_cidade, 'ttm_occupancy']
st.metric(
"🏠 Taxa de Ocupação",
f"{ocupacao_melhor*100:.1f}%",
delta=f"{((ocupacao_melhor - ranking_cidades['ttm_occupancy'].mean()) * 100):.1f}pp vs. média"
)
with col4:
cresc_melhor = ranking_cidades.loc[melhor_cidade, 'crescimento_pop_%']
st.metric(
"🌱 Crescimento Populacional",
f"+{cresc_melhor:.1f}%" if cresc_melhor >= 0 else f"{cresc_melhor:.1f}%",
delta="2010-2023"
)
st.markdown("---")
# NOVA SEÇÃO: EXPLICAÇÃO DO CÁLCULO DO SCORE
st.subheader("📐 Como Calculamos o Score de Investimento?")
st.markdown("""
<div class="methodology-box">
<h3 style="color: #1e3a8a; margin-top: 0;">🔬 Metodologia de Cálculo</h3>
<p>O <strong>Score de Investimento</strong> é uma métrica ponderada que integra <strong>6 indicadores-chave</strong>
para fornecer uma avaliação objetiva e comparável entre as cidades.</p>
</div>
""", unsafe_allow_html=True)
col_method1, col_method2 = st.columns([1, 1])
with col_method1:
st.markdown("#### 📊 Indicadores e Pesos")
# Criar DataFrame com os pesos
pesos_df = pd.DataFrame({
'Indicador': ['ROI Anual', 'Faturamento Médio', 'Taxa de Ocupação',
'Crescimento Populacional', 'Segurança', 'Demanda (Reviews)'],
'Peso (%)': [pesos_score['ROI']*100, pesos_score['Faturamento']*100,
pesos_score['Ocupacao']*100, pesos_score['Crescimento_Pop']*100,
pesos_score['Seguranca']*100, pesos_score['Demanda']*100],
'Justificativa': [
'Rentabilidade é o fator mais importante',
'Receita absoluta indica potencial de mercado',
'Demanda consistente reduz risco de vacância',
'Indica potencial de valorização futura',
'Afeta atratividade turística',
'Histórico de procura indica demanda'
]
})
st.dataframe(
pesos_df.style.background_gradient(subset=['Peso (%)'], cmap='Blues'),
use_container_width=True,
hide_index=True
)
with col_method2:
st.markdown("#### 🧮 Fórmula de Cálculo")
st.latex(r'''
Score = \sum_{i=1}^{6} (Indicador_i^{norm} \times Peso_i)
''')
st.markdown("""
**Onde:**
- **Indicador<sub>i</sub><sup>norm</sup>**: Valor normalizado (0-100) de cada indicador
- **Peso<sub>i</sub>**: Peso percentual do indicador
**Normalização:**
""", unsafe_allow_html=True)
st.latex(r'''
Valor^{norm} = \frac{Valor - Min}{Max - Min} \times 100
''')
st.markdown("""
**Exceção:** Segurança é invertida (menos homicídios = maior score)
**Resultado:** Score final entre 0 e 100
- **≥ 70**: ALTAMENTE RECOMENDADO
- **50-69**: RECOMENDADO
- **30-49**: MODERADO
- **< 30**: NÃO RECOMENDADO
""")
st.markdown("---")
# Ranking visual
st.subheader("🏆 Ranking Completo de Cidades")
# Criar tabela de ranking visual
ranking_display = score_cidades[['Score_Final']].copy()
ranking_display['Posição'] = range(1, len(ranking_display) + 1)
ranking_display['Cidade'] = ranking_display.index
# Adicionar emojis e recomendações
def get_recomendacao(score):
if score >= 70: return "🥇 ALTAMENTE RECOMENDADO"
elif score >= 50: return "🥈 RECOMENDADO"
elif score >= 30: return "🥉 MODERADO"
else: return "❌ NÃO RECOMENDADO"
ranking_display['Recomendação'] = ranking_display['Score_Final'].apply(get_recomendacao)
ranking_display = ranking_display[['Posição', 'Cidade', 'Score_Final', 'Recomendação']]
# Gráfico de barras horizontal
fig_ranking = go.Figure(go.Bar(
y=score_cidades.index,
x=score_cidades['Score_Final'],
orientation='h',
marker=dict(
color=score_cidades['Score_Final'],
colorscale='RdYlGn',
showscale=True,
colorbar=dict(title="Score")
),
text=score_cidades['Score_Final'].round(1),
textposition='outside'
))
fig_ranking.update_layout(
title="Score de Investimento por Cidade",
xaxis_title="Score (0-100)",
yaxis_title="Cidade",
height=400,
template="plotly_white"
)
st.plotly_chart(fig_ranking, use_container_width=True)
# Tabela detalhada
st.dataframe(
ranking_display.style.background_gradient(subset=['Score_Final'], cmap='RdYlGn'),
use_container_width=True,
hide_index=True
)
st.markdown("---")
# Análise comparativa rápida
st.subheader("📊 Comparação Rápida - Top 2 Cidades")
if len(score_cidades) >= 2:
cidade1 = score_cidades.index[0]
cidade2 = score_cidades.index[1]
col_comp1, col_comp2 = st.columns(2)
with col_comp1:
st.markdown(f"### 🥇 {cidade1}")
st.metric("Faturamento Anual", f"R$ {ranking_cidades.loc[cidade1, 'ttm_revenue_native']:,.0f}")
st.metric("ROI", f"{ranking_cidades.loc[cidade1, 'ROI_anual']:.2f}%")
st.metric("Ocupação", f"{ranking_cidades.loc[cidade1, 'ttm_occupancy']*100:.1f}%")
st.metric("Segurança", f"{ranking_cidades.loc[cidade1, 'taxa_homicidios_100k']:.1f} homicídios/100k")
with col_comp2:
st.markdown(f"### 🥈 {cidade2}")
st.metric("Faturamento Anual", f"R$ {ranking_cidades.loc[cidade2, 'ttm_revenue_native']:,.0f}")
st.metric("ROI", f"{ranking_cidades.loc[cidade2, 'ROI_anual']:.2f}%")
st.metric("Ocupação", f"{ranking_cidades.loc[cidade2, 'ttm_occupancy']*100:.1f}%")
st.metric("Segurança", f"{ranking_cidades.loc[cidade2, 'taxa_homicidios_100k']:.1f} homicídios/100k")
# ============================================================================
# PÁGINA 2: COMPARAÇÃO DE CIDADES
# ============================================================================
elif page == "📊 Comparação de Cidades":
st.title("📊 Análise Comparativa Detalhada")
# Gráfico Radar
st.subheader("🎯 Comparação Multi-Dimensional")
fig_radar = go.Figure()
for cidade in score_cidades.index:
fig_radar.add_trace(go.Scatterpolar(
r=[
score_cidades.loc[cidade, 'ROI'],
score_cidades.loc[cidade, 'Faturamento'],
score_cidades.loc[cidade, 'Ocupacao'],
score_cidades.loc[cidade, 'Crescimento_Pop'],
score_cidades.loc[cidade, 'Seguranca'],
score_cidades.loc[cidade, 'Demanda']
],
theta=['ROI', 'Faturamento', 'Ocupação', 'Crescimento Pop.', 'Segurança', 'Demanda'],
fill='toself',
name=cidade
))
fig_radar.update_layout(
polar=dict(radialaxis=dict(visible=True, range=[0, 100])),
showlegend=True,
height=500,
title="Comparação de Indicadores Normalizados (0-100)"
)
st.plotly_chart(fig_radar, use_container_width=True)
st.markdown("---")
# Tabela comparativa completa
st.subheader("📋 Tabela Comparativa Completa")
ranking_display = ranking_cidades.copy()
ranking_display.columns = [
'Faturamento Anual (R$)',
'Preço Diária (R$)',
'Taxa Ocupação (%)',
'Valor Imóvel Est. (R$)',
'ROI Anual (%)',
'Nº Reviews Médio',
'População',
'Homicídios Totais',
'Crescimento Pop. (%)',
'Taxa Homicídios/100k'
]
# Formatar valores
ranking_display['Taxa Ocupação (%)'] = (ranking_display['Taxa Ocupação (%)'] * 100).round(1)
st.dataframe(
ranking_display.style.format({
'Faturamento Anual (R$)': 'R$ {:,.2f}',
'Preço Diária (R$)': 'R$ {:,.2f}',
'Taxa Ocupação (%)': '{:.1f}%',
'Valor Imóvel Est. (R$)': 'R$ {:,.0f}',
'ROI Anual (%)': '{:.2f}%',
'Nº Reviews Médio': '{:.1f}',
'População': '{:,.0f}',
'Homicídios Totais': '{:.0f}',
'Crescimento Pop. (%)': '{:.2f}%',
'Taxa Homicídios/100k': '{:.2f}'
}).background_gradient(subset=['ROI Anual (%)'], cmap='RdYlGn'),
use_container_width=True
)
st.markdown("---")
# Gráficos de barras comparativos
col_g1, col_g2 = st.columns(2)
with col_g1:
fig_fat = px.bar(
ranking_cidades.reset_index(),
x='city',
y='ttm_revenue_native',
title="Faturamento Anual Médio por Cidade",
labels={'city': 'Cidade', 'ttm_revenue_native': 'Faturamento (R$)'},
color='ttm_revenue_native',
color_continuous_scale='Blues',
template="plotly_white"
)
st.plotly_chart(fig_fat, use_container_width=True)
with col_g2:
fig_roi = px.bar(
ranking_cidades.reset_index(),
x='city',
y='ROI_anual',
title="ROI Anual por Cidade",
labels={'city': 'Cidade', 'ROI_anual': 'ROI (%)'},
color='ROI_anual',
color_continuous_scale='Greens',
template="plotly_white"
)
st.plotly_chart(fig_roi, use_container_width=True)
# ============================================================================
# PÁGINA 3: ANÁLISE DE MERCADO
# ============================================================================
elif page == "📈 Análise de Mercado":
st.title("📈 Análise de Mercado e Indicadores Socioeconômicos")
# População e crescimento
st.subheader("👥 Crescimento Populacional (2010-2023)")
pop_data_norm = pop_data.copy()
pop_data_norm['city_display'] = pop_data_norm['city'].str.title()
fig_pop = px.line(
pop_data_norm,
x='date',
y='Populacao',
color='city_display',
title="Evolução Populacional por Cidade",
labels={'date': 'Ano', 'Populacao': 'População', 'city_display': 'Cidade'},
markers=True,
template="plotly_white"
)
st.plotly_chart(fig_pop, use_container_width=True)
# Crescimento percentual - CORRIGIDO PARA MOSTRAR VALORES NEGATIVOS
pop_2010 = pop_data[pop_data['date'] == 2010].copy()
pop_2023 = pop_data[pop_data['date'] == 2023].copy()
pop_2010['city_norm'] = pop_2010['city'].apply(normalizar_texto)
pop_2023['city_norm'] = pop_2023['city'].apply(normalizar_texto)
pop_merged = pd.merge(
pop_2010[['city_norm', 'Populacao']],
pop_2023[['city_norm', 'Populacao']],
on='city_norm',
suffixes=('_2010', '_2023')
)
pop_merged['Crescimento_%'] = ((pop_merged['Populacao_2023'] - pop_merged['Populacao_2010']) / pop_merged['Populacao_2010'] * 100).round(2)
pop_merged['city'] = pop_merged['city_norm'].str.title()
# CORREÇÃO: Gráfico que mostra valores negativos corretamente
fig_cresc = px.bar(
pop_merged,
x='city',
y='Crescimento_%',
title="Crescimento Populacional 2010-2023 (%)",
labels={'city': 'Cidade', 'Crescimento_%': 'Crescimento (%)'},
color='Crescimento_%',
color_continuous_scale='RdYlGn',
template="plotly_white",
text='Crescimento_%'
)
# Adicionar linha de referência em zero
fig_cresc.add_hline(y=0, line_dash="dash", line_color="gray", annotation_text="Zero")
# Formatar texto das barras
fig_cresc.update_traces(texttemplate='%{text:.2f}%', textposition='outside')
# Garantir que o eixo Y inclui valores negativos
fig_cresc.update_yaxes(
title="Crescimento (%)",
zeroline=True,
zerolinewidth=2,
zerolinecolor='gray'
)
st.plotly_chart(fig_cresc, use_container_width=True)
# Adicionar insight sobre Porto Alegre
if any(pop_merged['Crescimento_%'] < 0):
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.")
st.markdown("---")
# Segurança
st.subheader("🛡️ Análise de Segurança")
col_seg1, col_seg2 = st.columns(2)
with col_seg1:
# Taxa de homicídios por 100k
seg_display = ranking_cidades[['taxa_homicidios_100k']].reset_index()
seg_display.columns = ['Cidade', 'Taxa_Homicidios_100k']
fig_seg = px.bar(
seg_display,
x='Cidade',
y='Taxa_Homicidios_100k',
title="Taxa de Homicídios por 100k Habitantes",
labels={'Taxa_Homicidios_100k': 'Homicídios/100k'},
color='Taxa_Homicidios_100k',
color_continuous_scale='Reds_r',
template="plotly_white"
)
st.plotly_chart(fig_seg, use_container_width=True)
with col_seg2:
# Correlação segurança x faturamento
corr_data = ranking_cidades[['taxa_homicidios_100k', 'ttm_revenue_native']].reset_index()
corr_data.columns = ['Cidade', 'Taxa_Homicidios', 'Faturamento']
fig_corr_seg = px.scatter(
corr_data,
x='Taxa_Homicidios',
y='Faturamento',
text='Cidade',
title="Correlação: Segurança x Faturamento",
labels={'Taxa_Homicidios': 'Taxa Homicídios/100k', 'Faturamento': 'Faturamento Anual (R$)'},
template="plotly_white",
size=[100, 100, 100, 100]
)
fig_corr_seg.update_traces(textposition='top center')
st.plotly_chart(fig_corr_seg, use_container_width=True)
st.markdown("---")
# Indicadores IBGE
st.subheader("📊 Indicadores IBGE")
# Filtrar indicadores disponíveis
indicadores_disp = ibge_data['Indicador'].dropna().unique()
if len(indicadores_disp) > 0:
indicador_sel = st.selectbox("Selecione um indicador:", indicadores_disp)
ibge_filtered = ibge_data[ibge_data['Indicador'] == indicador_sel].copy()
ibge_filtered['city_display'] = ibge_filtered['city'].str.title()
if not ibge_filtered.empty:
fig_ibge = px.bar(
ibge_filtered,
x='city_display',
y='Valor',
title=f"{indicador_sel} por Cidade",
labels={'city_display': 'Cidade', 'Valor': 'Valor'},
color='Valor',
color_continuous_scale='Viridis',
template="plotly_white"
)
st.plotly_chart(fig_ibge, use_container_width=True)
else:
st.info("Dados IBGE não disponíveis para visualização detalhada.")
st.markdown("---")
# Análise de saturação de mercado
st.subheader("🏘️ Saturação de Mercado")
saturacao = ranking_cidades[['populacao']].copy()
saturacao['num_imoveis'] = df.groupby('city').size()
saturacao['imoveis_por_100k_hab'] = (saturacao['num_imoveis'] / saturacao['populacao'] * 100000).round(2)
saturacao = saturacao.reset_index()
saturacao.columns = ['Cidade', 'População', 'Nº Imóveis', 'Imóveis/100k hab']
fig_sat = px.scatter(
saturacao,
x='População',
y='Imóveis/100k hab',
size='Nº Imóveis',
color='Cidade',
title="Saturação de Mercado: Imóveis por 100k Habitantes",
labels={'Imóveis/100k hab': 'Imóveis por 100k habitantes'},
template="plotly_white",
hover_data=['Nº Imóveis']
)
st.plotly_chart(fig_sat, use_container_width=True)
st.dataframe(saturacao, use_container_width=True, hide_index=True)
# ============================================================================
# PÁGINA 4: SAZONALIDADE
# ============================================================================
elif page == "📅 Sazonalidade":
st.title("📅 Análise de Sazonalidade")
st.subheader("📈 Ocupação Média por Mês")
# Preparar dados de sazonalidade
past_filtered = df_past[df_past['city_norm'].isin([normalizar_texto(c) for c in cidades_sel])].copy()
if not past_filtered.empty:
# Ocupação mensal
past_filtered['month_num'] = past_filtered['date'].dt.month
past_filtered['city_display'] = past_filtered['city'].str.title()
ocupacao_mensal = past_filtered.groupby(['month_num', 'city_display'])['occupancy'].mean().reset_index()
meses_pt = {
1: 'Jan', 2: 'Fev', 3: 'Mar', 4: 'Abr', 5: 'Mai', 6: 'Jun',
7: 'Jul', 8: 'Ago', 9: 'Set', 10: 'Out', 11: 'Nov', 12: 'Dez'
}
ocupacao_mensal['month_name'] = ocupacao_mensal['month_num'].map(meses_pt)
fig_sazon = px.line(
ocupacao_mensal,
x='month_name',
y='occupancy',
color='city_display',
title="Taxa de Ocupação Média por Mês",
labels={'month_name': 'Mês', 'occupancy': 'Taxa de Ocupação', 'city_display': 'Cidade'},
markers=True,
template="plotly_white"
)
# CORREÇÃO: Usar update_layout ao invés de update_yaxis
fig_sazon.update_layout(
yaxis=dict(tickformat='.0%')
)
st.plotly_chart(fig_sazon, use_container_width=True)
st.markdown("---")
# Heatmap de ocupação
st.subheader("🔥 Heatmap de Ocupação")
pivot_ocupacao = ocupacao_mensal.pivot(index='city_display', columns='month_name', values='occupancy')
pivot_ocupacao = pivot_ocupacao[['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez']]
fig_heatmap = go.Figure(data=go.Heatmap(
z=pivot_ocupacao.values * 100,
x=pivot_ocupacao.columns,
y=pivot_ocupacao.index,
colorscale='RdYlGn',
text=np.round(pivot_ocupacao.values * 100, 1),
texttemplate='%{text}%',
textfont={"size": 12},
colorbar=dict(title="Ocupação (%)")
))
fig_heatmap.update_layout(
title="Taxa de Ocupação (%) por Cidade e Mês",
xaxis_title="Mês",
yaxis_title="Cidade",
height=400,
template="plotly_white"
)
st.plotly_chart(fig_heatmap, use_container_width=True)
st.markdown("---")
# Preço médio por mês
st.subheader("💰 Variação de Preço por Mês")
preco_mensal = past_filtered.groupby(['month_num', 'city_display'])['native_rate_avg'].mean().reset_index()
preco_mensal['month_name'] = preco_mensal['month_num'].map(meses_pt)
fig_preco = px.line(
preco_mensal,
x='month_name',
y='native_rate_avg',
color='city_display',
title="Preço Médio de Diária por Mês",
labels={'month_name': 'Mês', 'native_rate_avg': 'Preço Médio (R$)', 'city_display': 'Cidade'},
markers=True,
template="plotly_white"
)
st.plotly_chart(fig_preco, use_container_width=True)
# Tabela de preços
pivot_preco = preco_mensal.pivot(index='city_display', columns='month_name', values='native_rate_avg')
pivot_preco = pivot_preco[['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez']]
st.dataframe(
pivot_preco.style.format("R$ {:.2f}").background_gradient(cmap='YlGn'),
use_container_width=True
)
else:
st.warning("⚠️ Sem dados históricos para as cidades selecionadas.")
# ============================================================================
# PÁGINA 5: MAPA INTERATIVO (MELHORADO)
# ============================================================================
elif page == "🗺️ Mapa Interativo":
st.title("🗺️ Mapa de Oportunidades")
st.markdown("**Clique nos marcadores para ver detalhes completos dos imóveis (incluindo foto)**")
if not df_filtered.empty:
center_lat = df_filtered['latitude'].mean()
center_lon = df_filtered['longitude'].mean()
m = folium.Map(location=[center_lat, center_lon], zoom_start=5, tiles='CartoDB positron')
# Adicionar marcadores por cidade com FOTO e informações detalhadas
for cidade in cidades_sel:
df_cidade = df_filtered[df_filtered['city'] == cidade]
for idx, row in df_cidade.head(200).iterrows():
# Cor baseada na recomendação
if row['Recomendacao'] == "💎 COMPRA RECOMENDADA":
color = 'green'
elif row['Recomendacao'] == "✅ POTENCIAL":
color = 'blue'
elif row['Recomendacao'] == "⚠️ ARRISCADO":
color = 'orange'
else:
color = 'red'
# HTML melhorado com FOTO
foto_url = row.get('cover_photo_url', '')
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 ''
popup_html = f"""
<div style="width: 270px; font-family: Arial, sans-serif;">
{foto_html}
<h4 style="margin: 5px 0; color: #1e3a8a;">{row['listing_name']}</h4>
<hr style="margin: 8px 0;">
<p style="margin: 3px 0;"><strong>📍 Cidade:</strong> {row['city']}</p>
<p style="margin: 3px 0;"><strong>🏠 Tipo:</strong> {row['room_type']}</p>
<p style="margin: 3px 0;"><strong>🛏️ Quartos:</strong> {int(row['bedrooms'])}</p>
<p style="margin: 3px 0;"><strong>👥 Hóspedes:</strong> {int(row['guests'])}</p>
<hr style="margin: 8px 0;">
<p style="margin: 3px 0;"><strong>💰 Faturamento Anual:</strong> <span style="color: #10b981;">R$ {row['ttm_revenue_native']:,.2f}</span></p>
<p style="margin: 3px 0;"><strong>📈 ROI:</strong> <span style="color: #3b82f6;">{row['ROI_anual']:.2f}%</span></p>
<p style="margin: 3px 0;"><strong>📊 Ocupação:</strong> {row['ttm_occupancy']*100:.1f}%</p>
<p style="margin: 3px 0;"><strong>⭐ Avaliação:</strong> {row['rating_overall']:.1f}/5.0</p>
<hr style="margin: 8px 0;">
<p style="margin: 5px 0; padding: 5px; background-color: #f0f4f8; border-radius: 3px; text-align: center;">
<strong>{row['Recomendacao']}</strong>
</p>
</div>
"""
folium.Marker(
location=[row['latitude'], row['longitude']],
popup=folium.Popup(popup_html, max_width=300),
tooltip=f"💵 R$ {row['ttm_avg_rate_native']:.0f}/noite | {row['listing_name'][:30]}...",
icon=folium.Icon(color=color, icon='home', prefix='fa')
).add_to(m)
st_folium(m, height=600, width="100%")
else:
st.warning("⚠️ Nenhum imóvel encontrado com os filtros selecionados.")
st.markdown("---")
# Top 10 imóveis
st.subheader("🏆 Top 10 Imóveis por ROI")
top_imoveis = df_filtered.nlargest(10, 'ROI_anual')[
['listing_name', 'city', 'ttm_revenue_native', 'ROI_anual', 'ttm_occupancy', 'rating_overall', 'Recomendacao']
].copy()
top_imoveis.columns = ['Nome', 'Cidade', 'Faturamento Anual', 'ROI (%)', 'Ocupação', 'Avaliação', 'Recomendação']
top_imoveis['Ocupação'] = (top_imoveis['Ocupação'] * 100).round(1)
st.dataframe(
top_imoveis.style.format({
'Faturamento Anual': 'R$ {:,.2f}',
'ROI (%)': '{:.2f}%',
'Ocupação': '{:.1f}%',
'Avaliação': '{:.1f}'
}).background_gradient(subset=['ROI (%)'], cmap='Greens'),
use_container_width=True,
hide_index=True
)
# ============================================================================
# PÁGINA 6: SIMULADOR DE INVESTIMENTO (APRIMORADO)
# ============================================================================
elif page == "🤖 Simulador de Investimento":
st.title("🤖 Simulador Inteligente de Investimento")
# NOVA SEÇÃO: Explicação do Modelo
with st.expander("ℹ️ Sobre o Modelo de Predição", expanded=False):
st.markdown(f"""
### 🔬 Modelo Utilizado: **{model_name}**
Este simulador utiliza **Machine Learning** para prever o faturamento anual de imóveis de curta temporada.
#### 📊 Características do Modelo
- **Algoritmo:** {model_name}
- **Acurácia (R²):** {model_score:.3f} ({model_score*100:.1f}%)
- **Variáveis Preditoras:**
- Número de quartos
- Capacidade de hóspedes
- Número de reviews (histórico de demanda)
- Localização (latitude e longitude)
- Status de Superhost
- Amenidades (ex: piscina)
- Cidade
- Tipo de imóvel
#### 🎯 Como Funciona?
O modelo foi treinado com **{len(df_training)} imóveis reais** e aprendeu padrões entre as características
dos imóveis e seu faturamento anual. Ele considera:
1. **Localização:** Imóveis em áreas turísticas tendem a faturar mais
2. **Capacidade:** Mais quartos/hóspedes = maior potencial de receita
3. **Reputação:** Superhosts e imóveis bem avaliados têm ocupação maior
4. **Amenidades:** Piscina e outras comodidades aumentam o valor da diária
5. **Cidade:** Cada cidade tem perfil de rentabilidade diferente
#### ⚠️ Limitações
- Previsões são baseadas em dados históricos (2023-2024)
- Não considera eventos futuros (mudanças econômicas, regulação, etc.)
- Margem de erro: ±15-20% em média
- Use como **referência inicial**, não como garantia
""")
# Inicializa session state
if 'sim_result' not in st.session_state:
st.session_state['sim_result'] = None
if 'sim_coords' not in st.session_state:
st.session_state['sim_coords'] = None
if 'sim_params' not in st.session_state:
st.session_state['sim_params'] = None
col_sim1, col_sim2 = st.columns([1, 1.5])
with col_sim1:
st.subheader("⚙️ Configuração do Imóvel")
sim_city = st.selectbox("Cidade:", sorted(df['city'].unique()))
coords_dict = {
'Florianopolis': [-27.59, -48.54],
'Belo Horizonte': [-19.91, -43.93],
'Curitiba': [-25.42, -49.27],
'Porto Alegre': [-30.03, -51.22]
}
def_lat, def_lon = coords_dict.get(sim_city, [-23.55, -46.63])
sim_lat = st.number_input("Latitude:", value=def_lat, format="%.5f", help="Use Google Maps para encontrar coordenadas exatas")
sim_lon = st.number_input("Longitude:", value=def_lon, format="%.5f")
sim_type = st.selectbox("Tipo:", df['room_type'].unique())
sim_bed = st.slider("Quartos:", 0, 8, 2)
sim_guest = st.slider("Hóspedes:", 1, 16, 4)
sim_pool = st.checkbox("Tem Piscina?", value=False)
sim_super = st.checkbox("Será Superhost?", value=True, help="Superhosts têm maior ocupação e podem cobrar diárias mais altas")
if st.button("🔮 Calcular Previsão", type="primary"):
input_df = pd.DataFrame({
'bedrooms': [sim_bed], 'guests': [sim_guest],
'num_reviews': [30], 'latitude': [sim_lat], 'longitude': [sim_lon],
'is_superhost': [1 if sim_super else 0],
'has_pool': [1 if sim_pool else 0]
})
for col in model_cols:
if col not in input_df.columns:
if f"city_{sim_city}" == col: input_df[col] = 1
elif f"type_{sim_type}" == col: input_df[col] = 1
else: input_df[col] = 0
input_df = input_df[model_cols]
pred = model.predict(input_df)[0]
st.session_state['sim_result'] = pred
st.session_state['sim_coords'] = [sim_lat, sim_lon]
st.session_state['sim_params'] = {'city': sim_city}
with col_sim2:
if st.session_state['sim_result'] is not None:
pred = st.session_state['sim_result']
lat, lon = st.session_state['sim_coords']
city = st.session_state['sim_params']['city']
st.success(f"### 💰 Faturamento Estimado: R$ {pred:,.2f} / ano")
st.info(f"📊 **Modelo:** {model_name} | **Confiança:** {model_score*100:.1f}%")
# Mapa
st.markdown("#### 📍 Localização Simulada")
m_sim = folium.Map(location=[lat, lon], zoom_start=14, tiles='CartoDB positron')
folium.Marker(
[lat, lon],
popup="Seu Imóvel Simulado",
icon=folium.Icon(color="red", icon="home", prefix='fa')
).add_to(m_sim)
st_folium(m_sim, height=300, width="100%")
# Imóveis semelhantes
st.markdown("#### 🏠 Imóveis Reais Semelhantes")
df_city_neighbors = df_training[df_training['city'] == city].copy()
if not df_city_neighbors.empty:
nn = NearestNeighbors(n_neighbors=min(5, len(df_city_neighbors)), algorithm='ball_tree')
nn.fit(df_city_neighbors[['latitude', 'longitude']])
X_query = pd.DataFrame([[lat, lon]], columns=['latitude', 'longitude'])
distances, indices = nn.kneighbors(X_query)
neighbors = df_city_neighbors.iloc[indices[0]]
cols_show = ['listing_name', 'bedrooms', 'guests', 'ttm_revenue_native', 'rating_overall']
st.dataframe(
neighbors[cols_show].style.format({'ttm_revenue_native': 'R$ {:,.2f}'}),
use_container_width=True,
hide_index=True
)
st.divider()
# Calculadora de lucro
st.subheader("📊 Análise de Viabilidade")
col_calc1, col_calc2 = st.columns(2)
with col_calc1:
val_imovel = st.number_input("Valor do Imóvel (R$):", value=400000.0, step=10000.0)
custo_fixo = st.number_input("Custo Mensal (R$):", value=800.0, step=100.0, help="Condomínio, IPTU, manutenção, etc.")
taxa_airbnb = st.slider("Taxa Airbnb (%):", 0, 20, 15, help="Comissão da plataforma")
with col_calc2:
custo_total = (pred * (taxa_airbnb/100)) + (custo_fixo * 12)
lucro = pred - custo_total
roi_real = (lucro / val_imovel) * 100
payback = val_imovel / lucro if lucro > 0 else 0
st.metric("💵 Lucro Líquido Anual", f"R$ {lucro:,.2f}")
st.metric("📈 ROI Líquido Real", f"{roi_real:.2f}%")
st.metric("⏱️ Payback Period", f"{payback:.1f} anos" if payback > 0 else "N/A")
if roi_real >= 8:
st.success("✅ Investimento ALTAMENTE VIÁVEL")
elif roi_real >= 5:
st.info("⚠️ Investimento MODERADO")
else:
st.error("❌ Investimento NÃO RECOMENDADO")
else:
st.info("👈 Configure o imóvel à esquerda e clique em 'Calcular Previsão' para ver os resultados.")
# --- FOOTER ---
st.sidebar.markdown("---")
st.sidebar.markdown(f"""
<div style="text-align: center; color: #666; font-size: 0.8rem;">
<p><strong>Dashboard de Consultoria Imobiliária</strong></p>
<p>Análise de Investimento em Curta Temporada</p>
<p>Modelo: {model_name} (R²: {model_score:.2f})</p>
<p>Dados: Airbnb, FipeZap, IBGE</p>
</div>
""", unsafe_allow_html=True)