Spaces:
Sleeping
Sleeping
| 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 sklearn.neighbors import NearestNeighbors | |
| from wordcloud import WordCloud | |
| import matplotlib | |
| import matplotlib.pyplot as plt | |
| import unicodedata | |
| # --- CONFIGURAÇÃO DE BACKEND (Evita erros de memória/thread) --- | |
| matplotlib.use('Agg') | |
| # --- 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); | |
| } | |
| .stDataFrame, .stTable { | |
| color: #31333F !important; | |
| } | |
| div[data-testid="stExpander"] div[role="button"] p { | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # --- FUNÇÃO DE NORMALIZAÇÃO DE TEXTO --- | |
| 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 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: | |
| # Fallback de encoding e nomes | |
| try: | |
| df = pd.read_csv('lisitng_geral.csv', sep=';', encoding='latin1') | |
| fipe = pd.read_csv('fipezap_geral (1).csv', sep=';', encoding='latin1') | |
| past = pd.read_csv('past_geral (1).csv', sep=';', encoding='latin1') | |
| seg = pd.read_csv('casos_homicidios (1).csv', sep=';', encoding='latin1') | |
| except FileNotFoundError: | |
| st.error("ERRO CRÍTICO: Arquivos CSV não encontrados. Verifique o upload.") | |
| 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) | |
| # Normalização | |
| df['city_norm'] = df['city'].apply(normalizar_texto) | |
| df['city'] = df['city'].str.title() | |
| # 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) | |
| # 3. Limpeza FipeZap | |
| fipe = fipe.dropna(subset=['city', 'preco_m2']) | |
| fipe['city_norm'] = fipe['city'].apply(normalizar_texto) | |
| media_cidade = fipe.groupby('city_norm')['preco_m2'].mean().to_dict() | |
| df['preco_m2_estimado'] = df['city_norm'].map(media_cidade) | |
| df['preco_m2_estimado'] = df['preco_m2_estimado'].fillna(fipe['preco_m2'].mean()) | |
| # 4. Cálculos KPI | |
| 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) | |
| # Score | |
| 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. 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) | |
| # 6. Histórico | |
| past['date'] = pd.to_datetime(past['date'], dayfirst=True, 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'] | |
| 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'] | |
| model = RandomForestRegressor(n_estimators=50, random_state=42, n_jobs=-1) | |
| model.fit(X, y) | |
| return model, X.columns, df_model | |
| # --- CARREGAMENTO --- | |
| df, df_past = load_data() | |
| model, model_cols, df_training = train_model(df) | |
| # --- 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( | |
| "Menu Principal", | |
| ["📊 Dashboard de Mercado", "📈 Análise Exploratória", "🤖 Simulador IA"], | |
| label_visibility="collapsed" | |
| ) | |
| 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: DASHBOARD --- | |
| if page == "📊 Dashboard de Mercado": | |
| st.title("📍 Inteligência Imobiliária Airbnb") | |
| kpi1, kpi2, kpi3, kpi4 = st.columns(4) | |
| kpi1.metric("Imóveis Analisados", len(df_filtered)) | |
| kpi2.metric("Preço Médio/Noite", 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 Estimado", f"{df_filtered['ROI_anual'].median():.1f}% a.a.") | |
| st.markdown("---") | |
| st.subheader("Mapa de Oportunidades") | |
| st.caption("Clique nos círculos coloridos para ver a Ficha Técnica completa abaixo.") | |
| map_data = None | |
| selected_listing = None | |
| 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=12, tiles='CartoDB positron') | |
| marker_cluster = MarkerCluster().add_to(m) | |
| for idx, row in df_filtered.head(1000).iterrows(): | |
| color = '#2ecc71' if row['Recomendacao'] == "💎 COMPRA RECOMENDADA" else '#3498db' | |
| folium.CircleMarker( | |
| location=[row['latitude'], row['longitude']], | |
| radius=6, color=color, fill=True, fill_color=color, fill_opacity=0.8, | |
| popup=row['listing_name'], | |
| tooltip=f"R$ {row['ttm_avg_rate_native']:.0f}" | |
| ).add_to(marker_cluster) | |
| map_data = st_folium(m, height=450, width="100%") | |
| else: | |
| st.warning("Sem dados.") | |
| # --- RESTAURAÇÃO DA FICHA TÉCNICA DETALHADA --- | |
| 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: | |
| st.markdown("### 📋 Ficha Técnica do Imóvel") | |
| with st.container(): | |
| # Cabeçalho estilizado | |
| st.markdown(f""" | |
| <div style="background-color: #f0f2f6; padding: 20px; border-radius: 10px; border: 1px solid #ccc; margin-bottom: 20px;"> | |
| <h3 style="margin:0; color:#31333F;">🏡 {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.markdown(f"**📍 Cidade:** {selected_listing['city']}") | |
| st.markdown(f"**🏠 Tipo:** {selected_listing['room_type']}") | |
| st.markdown(f"**👥 Acomoda:** {int(selected_listing['guests'])} pessoas") | |
| st.markdown(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("---") | |
| tab_sazonal, tab_seg, tab_word = st.tabs(["📅 Sazonalidade & Preços", "🛡️ Segurança", "☁️ Amenities"]) | |
| with tab_sazonal: | |
| cidades_norm = [normalizar_texto(c) for c in cidades_sel] | |
| past_filt = df_past[df_past['city_norm'].isin(cidades_norm)].copy() | |
| col_graf, col_tab = st.columns([2, 1]) | |
| if not past_filt.empty: | |
| past_filt['month_str'] = past_filt['date'].dt.strftime('%Y-%m') | |
| seasonal = past_filt.groupby('month_str')['occupancy'].mean().reset_index() | |
| fig_line = px.line(seasonal, x='month_str', y='occupancy', markers=True, | |
| title="Taxa de Ocupação Média", template="plotly_white") | |
| fig_line.update_traces(line_color='#FF5A5F') | |
| col_graf.plotly_chart(fig_line, use_container_width=True) | |
| st.subheader("💰 Preço Médio por Mês (R$)") | |
| price_table = past_filt.groupby(['month_str', 'city'])['native_rate_avg'].mean().unstack().fillna(0) | |
| st.dataframe(price_table.style.format("R$ {:.2f}"), use_container_width=True) | |
| else: | |
| st.warning("Sem dados históricos.") | |
| with tab_seg: | |
| st.subheader("Relação Violência Urbana x Receita") | |
| if 'Homicidios' in df_filtered.columns: | |
| fig_seg = px.scatter(df_filtered[df_filtered['Homicidios'] >= 0], | |
| x="Homicidios", y="ttm_revenue_native", color="city", | |
| template="plotly_white", hover_name="listing_name") | |
| st.plotly_chart(fig_seg, use_container_width=True) | |
| with tab_word: | |
| st.subheader("O que os imóveis Top Performers oferecem?") | |
| top = df_filtered[df_filtered['Recomendacao'] == "💎 COMPRA RECOMENDADA"] | |
| text = " ".join(str(a) for a in top['amenities'].dropna()) | |
| if text: | |
| wc = WordCloud(width=800, height=300, background_color='white').generate(text) | |
| fig, ax = plt.subplots() | |
| ax.imshow(wc, interpolation='bilinear') | |
| ax.axis("off") | |
| st.pyplot(fig) | |
| plt.close(fig) | |
| # --- PÁGINA 2: EXPLORATÓRIA --- | |
| elif page == "📈 Análise Exploratória": | |
| st.title("📈 Análise Exploratória de Dados") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.subheader("Distribuição de Preços") | |
| fig_hist = px.histogram(df_filtered, x="ttm_avg_rate_native", nbins=50, | |
| title="Frequência de Preços de Diária", | |
| color_discrete_sequence=['#FF5A5F'], template="plotly_white") | |
| st.plotly_chart(fig_hist, use_container_width=True) | |
| with col2: | |
| st.subheader("Preços por Cidade") | |
| fig_box = px.box(df_filtered, x="city", y="ttm_avg_rate_native", | |
| title="Variação de Preço por Cidade", | |
| color="city", template="plotly_white") | |
| st.plotly_chart(fig_box, use_container_width=True) | |
| st.markdown("---") | |
| col3, col4 = st.columns(2) | |
| with col3: | |
| st.subheader("Correlação: Nota vs Faturamento") | |
| fig_corr = px.scatter(df_filtered, x="rating_overall", y="ttm_revenue_native", | |
| color="room_type", size="bedrooms", | |
| title="Nota impacta no Faturamento?", | |
| template="plotly_white", opacity=0.7) | |
| st.plotly_chart(fig_corr, use_container_width=True) | |
| with col4: | |
| st.subheader("Tipos de Imóveis") | |
| df_counts = df_filtered['room_type'].value_counts().reset_index() | |
| df_counts.columns = ['Tipo_Imovel', 'Contagem'] | |
| fig_bar = px.bar(df_counts, | |
| x='Tipo_Imovel', y='Contagem', | |
| labels={'Tipo_Imovel': 'Tipo', 'Contagem': 'Quantidade'}, | |
| title="Contagem por Tipo de Imóvel", | |
| template="plotly_white") | |
| st.plotly_chart(fig_bar, use_container_width=True) | |
| # --- PÁGINA 3: SIMULADOR (COM SESSION STATE PARA NÃO SUMIR) --- | |
| elif page == "🤖 Simulador IA": | |
| st.title("🤖 Simulador Inteligente") | |
| # Inicializa variáveis de estado se não existirem | |
| 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 | |
| c1, c2 = st.columns([1, 1.5]) | |
| with c1: | |
| st.subheader("Parâmetros 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") | |
| 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) | |
| if st.button("🔮 Calcular Previsão"): | |
| # Salva parâmetros na sessão | |
| 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] | |
| # ATUALIZA O ESTADO | |
| st.session_state['sim_result'] = pred | |
| st.session_state['sim_coords'] = [sim_lat, sim_lon] | |
| st.session_state['sim_params'] = {'city': sim_city} | |
| with c2: | |
| # VERIFICA SE HÁ RESULTADO GRAVADO NA SESSÃO | |
| 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.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") | |
| ).add_to(m_sim) | |
| st_folium(m_sim, height=300, width="100%") | |
| st.markdown("#### 🏠 Imóveis Reais Semelhantes (Vizinhança)") | |
| df_city_neighbors = df_training[df_training['city'] == city].copy() | |
| if not df_city_neighbors.empty: | |
| nn = NearestNeighbors(n_neighbors=5, 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 | |
| ) | |
| else: | |
| st.info("Não há imóveis suficientes nesta cidade para comparação.") | |
| st.divider() | |
| st.subheader("📊 Calculadora de Lucro Líquido") | |
| col_calc1, col_calc2 = st.columns(2) | |
| with col_calc1: | |
| val_imovel = st.number_input("Valor do Imóvel (R$):", value=400000.0) | |
| custo_fixo = st.number_input("Custo Mensal (R$):", value=800.0) | |
| with col_calc2: | |
| custo_total = (pred * 0.15) + (custo_fixo * 12) | |
| lucro = pred - custo_total | |
| roi_real = (lucro / val_imovel) * 100 | |
| st.metric("Lucro Líquido Anual", f"R$ {lucro:,.2f}") | |
| st.metric("ROI Líquido Real", f"{roi_real:.2f}%") |