Spaces:
Sleeping
Sleeping
| 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 --- | |
| 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 --- | |
| 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) | |