psp2_ / app.py
brunaaaz's picture
Update app.py
33f133d verified
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) ---
@st.cache_data(ttl=3600)
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 ---
@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']
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}%")