Spaces:
Sleeping
Sleeping
| #teste | |
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import plotly.express as px | |
| import folium | |
| from streamlit_folium import st_folium | |
| from folium.plugins import MarkerCluster | |
| from sklearn.ensemble import RandomForestRegressor | |
| from wordcloud import WordCloud | |
| import matplotlib.pyplot as plt | |
| import unicodedata | |
| # --- CONFIGURAÇÃO DA PÁGINA --- | |
| st.set_page_config(page_title="Airbnb Intelligence Pro", layout="wide", page_icon="🏘️") | |
| # --- CSS CUSTOMIZADO --- | |
| st.markdown(""" | |
| <style> | |
| .metric-card { | |
| background-color: #f8f9fa; | |
| border-left: 5px solid #FF5A5F; | |
| padding: 15px; | |
| border-radius: 5px; | |
| box-shadow: 1px 1px 3px rgba(0,0,0,0.1); | |
| } | |
| .stExpander { border: 1px solid #ddd; border-radius: 5px; background-color: #ffffff; } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # --- FUNÇÃO DE NORMALIZAÇÃO DE TEXTO --- | |
| def normalizar_texto(texto): | |
| """Remove acentos e coloca em minúsculo para garantir merge correto.""" | |
| 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 LIMPEZA (CACHEADO) --- | |
| def load_data(): | |
| try: | |
| # Carregamento dos arquivos | |
| df = pd.read_csv('lisitng_geral.csv', sep=';', encoding='utf-8') | |
| fipe = pd.read_csv('fipezap_geral.csv', sep=';', encoding='utf-8') | |
| past = pd.read_csv('past_geral.csv', sep=';', encoding='utf-8') | |
| seg = pd.read_csv('casos_homicidios.csv', sep=';', encoding='utf-8') | |
| except FileNotFoundError: | |
| st.error("ERRO: Arquivos CSV não encontrados. Faça o upload no Hugging Face.") | |
| st.stop() | |
| # --- 1. LIMPEZA DE COORDENADAS (Sua Lógica Solicitada) --- | |
| # Garante que BH, Curitiba e POA apareçam | |
| cols_coords = ['latitude', 'longitude'] | |
| for col in cols_coords: | |
| if df[col].dtype == 'object': | |
| df[col] = df[col].str.replace(',', '.').astype(float) | |
| # Remove linhas sem coordenada (essencial para o mapa não quebrar) | |
| 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) | |
| # Normalização de Cidades | |
| df['city_norm'] = df['city'].apply(normalizar_texto) | |
| df['city'] = df['city'].str.title() # Deixar bonito para visualização | |
| # Imputação de Nulos (Quartos/Hóspedes) | |
| 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) | |
| # 3. Limpeza e Merge FipeZap | |
| fipe = fipe.dropna(subset=['city', 'preco_m2']) | |
| fipe = fipe.loc[:, ~fipe.columns.str.contains('^Unnamed')] | |
| fipe['city_norm'] = fipe['city'].apply(normalizar_texto) | |
| # Merge Preço m2 | |
| media_cidade = fipe.groupby('city_norm')['preco_m2'].mean().to_dict() | |
| df['preco_m2_estimado'] = df['city_norm'].map(media_cidade) | |
| # Fallback: Média geral se não achar a cidade | |
| df['preco_m2_estimado'] = df['preco_m2_estimado'].fillna(fipe['preco_m2'].mean()) | |
| # 4. Cálculos de KPI (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'] | |
| # Blindagem contra divisão por zero | |
| 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) | |
| # Score e Recomendação IA | |
| 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) | |
| # 5. Limpeza e Merge 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) # -1 indica sem dados | |
| # 6. Limpeza Histórico (Past) | |
| past['date'] = pd.to_datetime(past['date'], format='%d/%m/%Y', errors='coerce') | |
| past['city_norm'] = past['city'].apply(normalizar_texto) | |
| return df, past | |
| # --- TREINAMENTO MODELO IA --- | |
| def train_model(df): | |
| df_model = df[df['ttm_revenue_native'] > 0].copy() | |
| features = ['bedrooms', 'guests', 'num_reviews', 'latitude', 'longitude'] | |
| # Feature Engineering Simples | |
| 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']] | |
| # One Hot Encoding | |
| 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'] | |
| model = RandomForestRegressor(n_estimators=40, random_state=42) | |
| model.fit(X, y) | |
| return model, X.columns | |
| # --- CARREGAMENTO --- | |
| df, df_past = load_data() | |
| model, model_cols = train_model(df) | |
| # --- INTERFACE: SIDEBAR --- | |
| st.sidebar.image("https://upload.wikimedia.org/wikipedia/commons/6/69/Airbnb_Logo_Bélo.svg", width=140) | |
| st.sidebar.header("Navegação") | |
| page = st.sidebar.radio("", ["📊 Análise de Mercado", "🤖 Simulador IA"]) | |
| st.sidebar.markdown("---") | |
| st.sidebar.header("Filtros") | |
| # Filtro Cidades | |
| cidades_disponiveis = sorted(df['city'].unique()) | |
| cidades_sel = st.sidebar.multiselect("Cidades:", cidades_disponiveis, default=cidades_disponiveis) | |
| # Filtro Quartos | |
| quartos = st.sidebar.slider("Quartos:", 0, 8, (0, 8)) | |
| # Filtro Segurança (Opcional) | |
| usar_seguranca = st.sidebar.checkbox("🛡️ Filtrar por Segurança", value=False) | |
| if usar_seguranca: | |
| max_homicidios = int(df[df['Homicidios'] >= 0]['Homicidios'].max()) | |
| filtro_homicidios = st.sidebar.slider("Máx Homicídios/Ano:", 0, max_homicidios, max_homicidios) | |
| # Filtro Outliers | |
| esconder_outliers = st.sidebar.checkbox("Ocultar Outliers (Preço)", value=True) | |
| # APLICAÇÃO DOS FILTROS | |
| df_filtered = df[ | |
| (df['city'].isin(cidades_sel)) & | |
| (df['bedrooms'].between(quartos[0], quartos[1])) | |
| ] | |
| if usar_seguranca: | |
| df_filtered = df_filtered[(df_filtered['Homicidios'] >= 0) & (df_filtered['Homicidios'] <= filtro_homicidios)] | |
| if esconder_outliers: | |
| Q1 = df_filtered['ttm_avg_rate_native'].quantile(0.25) | |
| Q3 = df_filtered['ttm_avg_rate_native'].quantile(0.75) | |
| IQR = Q3 - Q1 | |
| if pd.notna(IQR): | |
| df_filtered = df_filtered[df_filtered['ttm_avg_rate_native'] <= (Q3 + 1.5 * IQR)] | |
| # --- PÁGINA 1: DASHBOARD --- | |
| if page == "📊 Análise de Mercado": | |
| st.title("📍 Inteligência Imobiliária Airbnb") | |
| # 1. KPIs | |
| kpi1, kpi2, kpi3, kpi4 = st.columns(4) | |
| kpi1.metric("Imóveis", len(df_filtered)) | |
| kpi2.metric("Preço Médio", f"R$ {df_filtered['ttm_avg_rate_native'].median():.0f}") | |
| kpi3.metric("Faturamento Médio", f"R$ {df_filtered['ttm_revenue_native'].mean():,.0f}") | |
| kpi4.metric("ROI Médio", f"{df_filtered['ROI_anual'].median():.1f}% a.a.") | |
| st.markdown("---") | |
| # 2. MAPA (Folium Limpo) | |
| st.subheader("Mapa de Oportunidades") | |
| st.caption("Passe o mouse para ver o preço. Clique para ver detalhes abaixo.") | |
| if not df_filtered.empty: | |
| center_lat = df_filtered['latitude'].mean() | |
| center_lon = df_filtered['longitude'].mean() | |
| # Mapa estilo 'CartoDB positron' para ser limpo e legível | |
| m = folium.Map(location=[center_lat, center_lon], zoom_start=12, tiles='CartoDB positron') | |
| marker_cluster = MarkerCluster().add_to(m) | |
| # Adicionar pontos (Limitado a 1000 para performance) | |
| for idx, row in df_filtered.head(1000).iterrows(): | |
| # Cor baseada na recomendação | |
| color = '#2ecc71' if row['Recomendacao'] == "💎 COMPRA RECOMENDADA" else '#3498db' | |
| folium.CircleMarker( | |
| location=[row['latitude'], row['longitude']], | |
| radius=7, | |
| color=color, | |
| fill=True, | |
| fill_color=color, | |
| fill_opacity=0.8, | |
| popup=row['listing_name'], # Chave para identificar o clique | |
| tooltip=f"R$ {row['ttm_avg_rate_native']:.0f}/noite" | |
| ).add_to(marker_cluster) | |
| # Renderizar mapa e capturar clique | |
| map_data = st_folium(m, height=500, width="100%") | |
| else: | |
| st.warning("Sem dados para exibir no mapa.") | |
| map_data = None | |
| # 3. DETALHES DO IMÓVEL (ABAIXO DO MAPA) | |
| st.markdown("### 📋 Ficha Técnica do Imóvel") | |
| selected_listing = None | |
| if map_data and map_data.get('last_object_clicked_popup'): | |
| name_clicked = map_data['last_object_clicked_popup'] | |
| match = df_filtered[df_filtered['listing_name'] == name_clicked] | |
| if not match.empty: | |
| selected_listing = match.iloc[0] | |
| if selected_listing is not None: | |
| with st.container(): | |
| st.markdown(f""" | |
| <div style="background-color: #f0f2f6; padding: 20px; border-radius: 10px; border: 1px solid #ccc;"> | |
| <h3>🏡 {selected_listing['listing_name']}</h3> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| c_img, c_info, c_kpi = st.columns([1, 1, 1]) | |
| with c_img: | |
| if pd.notna(selected_listing['cover_photo_url']): | |
| st.image(selected_listing['cover_photo_url'], use_container_width=True) | |
| else: | |
| st.info("Sem foto disponível") | |
| with c_info: | |
| st.write(f"**Cidade:** {selected_listing['city']}") | |
| st.write(f"**Tipo:** {selected_listing['room_type']}") | |
| st.write(f"**Acomoda:** {int(selected_listing['guests'])} pessoas") | |
| st.write(f"**Quartos:** {int(selected_listing['bedrooms'])}") | |
| st.metric("Nota Geral", f"⭐ {selected_listing['rating_overall']}") | |
| with c_kpi: | |
| st.metric("Faturamento Anual", f"R$ {selected_listing['ttm_revenue_native']:,.2f}") | |
| st.metric("ROI Estimado", f"{selected_listing['ROI_anual']:.2f}%") | |
| status = selected_listing['Recomendacao'] | |
| if "COMPRA" in status: | |
| st.success(f"## {status}") | |
| elif "NÃO" in status: | |
| st.error(f"## {status}") | |
| else: | |
| st.warning(f"## {status}") | |
| else: | |
| st.info("👆 Clique em uma bolinha colorida no mapa acima para ver a análise completa do imóvel aqui.") | |
| st.markdown("---") | |
| # 4. GRÁFICOS VISUAIS (Estilo Código 1) | |
| col_g1, col_g2 = st.columns(2) | |
| with col_g1: | |
| st.subheader("📉 Distribuição de Preços") | |
| fig_hist = px.histogram(df_filtered, x="ttm_avg_rate_native", nbins=40, | |
| title="Concentração de Preços de Diária", color_discrete_sequence=['#FF5A5F']) | |
| st.plotly_chart(fig_hist, use_container_width=True) | |
| with col_g2: | |
| st.subheader("💎 Qualidade vs. Faturamento") | |
| # Proteção contra erro de tamanho NaN | |
| df_filtered['size_safe'] = df_filtered['ROI_anual'].apply(lambda x: max(0.1, x)) | |
| fig_scat = px.scatter(df_filtered, x="rating_overall", y="ttm_revenue_native", | |
| size="size_safe", color="room_type", | |
| title="Nota x Receita (Tamanho = ROI)", hover_name="listing_name") | |
| st.plotly_chart(fig_scat, use_container_width=True) | |
| # 5. ANÁLISES AVANÇADAS (EXPANDER) | |
| st.markdown("---") | |
| with st.expander("🔬 Ver Análises Avançadas (Sazonalidade, Segurança e Palavras-Chave)"): | |
| tab1, tab2, tab3 = st.tabs(["📅 Sazonalidade", "🛡️ Segurança", "☁️ Palavras-Chave"]) | |
| with tab1: | |
| st.markdown("##### Ocupação Mensal Histórica") | |
| # Filtra histórico usando city_norm para garantir match | |
| cidades_norm = [normalizar_texto(c) for c in cidades_sel] | |
| past_filtered = df_past[df_past['city_norm'].isin(cidades_norm)].copy() | |
| if not past_filtered.empty: | |
| past_filtered['month'] = past_filtered['date'].dt.strftime('%Y-%m') | |
| seasonal = past_filtered.groupby('month')['occupancy'].mean().reset_index() | |
| fig_line = px.line(seasonal, x='month', y='occupancy', markers=True) | |
| st.plotly_chart(fig_line, use_container_width=True) | |
| else: | |
| st.warning("Sem dados históricos para as cidades selecionadas.") | |
| with tab2: | |
| st.markdown("##### Segurança vs Retorno Financeiro") | |
| df_seg_plot = df_filtered[df_filtered['Homicidios'] >= 0] | |
| if not df_seg_plot.empty: | |
| fig_seg = px.scatter(df_seg_plot, x="Homicidios", y="ttm_revenue_native", | |
| color="city", size="size_safe", hover_name="listing_name") | |
| st.plotly_chart(fig_seg, use_container_width=True) | |
| else: | |
| st.warning("Cidades selecionadas não possuem dados de homicídios no arquivo.") | |
| with tab3: | |
| st.markdown("##### O que os Melhores Imóveis têm?") | |
| top_performers = df_filtered[df_filtered['Recomendacao'] == "💎 COMPRA RECOMENDADA"] | |
| text = " ".join(str(a) for a in top_performers['amenities'].dropna()) | |
| if text.strip(): | |
| wordcloud = WordCloud(width=800, height=300, background_color='white').generate(text) | |
| fig, ax = plt.subplots() | |
| ax.imshow(wordcloud, interpolation='bilinear') | |
| ax.axis("off") | |
| st.pyplot(fig) | |
| else: | |
| st.info("Não há Top Performers suficientes para gerar a nuvem.") | |
| # --- PÁGINA 2: SIMULADOR --- | |
| elif page == "🤖 Simulador IA": | |
| st.title("🤖 Simulador de Investimento & IA") | |
| col_ia, col_calc = st.columns(2) | |
| with col_ia: | |
| st.subheader("🔮 Previsão de Receita (IA)") | |
| sim_city = st.selectbox("Cidade:", sorted(df['city'].unique())) | |
| 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) | |
| if st.button("Calcular Previsão"): | |
| # Monta input | |
| base_lat = df[df['city'] == sim_city]['latitude'].mean() | |
| base_lon = df[df['city'] == sim_city]['longitude'].mean() | |
| if pd.isna(base_lat): base_lat = -23.55 # Fallback | |
| if pd.isna(base_lon): base_lon = -46.63 | |
| input_df = pd.DataFrame({ | |
| 'bedrooms': [sim_bed], 'guests': [sim_guest], | |
| 'num_reviews': [30], 'latitude': [base_lat], 'longitude': [base_lon], | |
| 'is_superhost': [1 if sim_super else 0], | |
| 'has_pool': [1 if sim_pool else 0] | |
| }) | |
| # One Hot Encoding Manual | |
| 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 | |
| # Reordenar colunas | |
| input_df = input_df[model_cols] | |
| pred = model.predict(input_df)[0] | |
| st.success(f"Faturamento Estimado: R$ {pred:,.2f}/ano") | |
| st.metric("Média Mensal", f"R$ {pred/12:,.2f}") | |
| with col_calc: | |
| st.subheader("💰 Calculadora de Lucro Líquido") | |
| rec_bruta = st.number_input("Receita Bruta Anual (R$):", value=50000.0) | |
| val_imovel = st.number_input("Valor do Imóvel (R$):", value=400000.0) | |
| taxa = st.slider("Taxa Airbnb (%):", 0, 20, 15) | |
| custo_fixo = st.number_input("Custo Fixo Mensal (Condomínio/Luz) R$:", value=800.0) | |
| custo_total = (rec_bruta * (taxa/100)) + (custo_fixo * 12) | |
| lucro = rec_bruta - custo_total | |
| roi_real = (lucro / val_imovel) * 100 | |
| st.divider() | |
| st.metric("Lucro Líquido Anual", f"R$ {lucro:,.2f}") | |
| st.metric("ROI Real (Líquido)", f"{roi_real:.2f}%") | |
| if roi_real > 6: | |
| st.balloons() |