psp2_ / app.py
enzograndino's picture
Update app.py
73b1cf1 verified
raw
history blame
17.6 kB
#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) ---
@st.cache_data
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 ---
@st.cache_resource
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()