gui-sparim's picture
Update app.py
91dc8a7 verified
# ============================================================
# IMPORTAÇÕES
# ============================================================
import gradio as gr
import pandas as pd
import numpy as np
import folium
from folium import plugins
from joblib import load
import os
import re
# Importações para gráficos (trazidas de graficos.py)
from scipy import stats
import plotly.graph_objects as go
from statsmodels.stats.outliers_influence import OLSInfluence
# ============================================================
# CONSTANTES
# ============================================================
CHAVES_ESPERADAS = [
"Xy_preview_out_coords",
"estatisticas",
"formatted_top_transformation_info",
"top_X_esc",
"top_y_esc",
"modelos_resumos",
"tabelas_coef",
"tabelas_obs_calc",
# "graf_model", # Não é mais estritamente necessário pois vamos gerar
"modelos_sm",
]
# Cores consistentes (trazidas de graficos.py)
COR_PRINCIPAL = '#FF8C00' # Laranja
COR_LINHA = '#dc3545' # Vermelho para linhas de referência
# ============================================================
# FUNÇÃO: CARREGAR CSS EXTERNO
# ============================================================
def carregar_css():
"""Carrega o arquivo CSS externo."""
css_path = os.path.join(os.path.dirname(__file__), "styles.css")
try:
with open(css_path, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
print(f"Aviso: Arquivo CSS não encontrado em {css_path}")
return ""
# ============================================================
# LÓGICA DE GERAÇÃO DE GRÁFICOS (ADAPTADA DE GRAFICOS.PY)
# ============================================================
def _criar_grafico_obs_calc(y_obs, y_calc):
"""Cria gráfico de valores observados vs calculados (Plotly Figure)."""
try:
fig = go.Figure()
# Scatter plot
fig.add_trace(go.Scatter(
x=y_calc,
y=y_obs,
mode='markers',
marker=dict(
color=COR_PRINCIPAL,
size=10,
line=dict(color='black', width=1)
),
name='Dados'
))
# Linha de identidade (45 graus)
min_val = min(min(y_obs), min(y_calc))
max_val = max(max(y_obs), max(y_calc))
margin = (max_val - min_val) * 0.05
fig.add_trace(go.Scatter(
x=[min_val - margin, max_val + margin],
y=[min_val - margin, max_val + margin],
mode='lines',
line=dict(color=COR_LINHA, dash='dash', width=2),
name='Linha de identidade'
))
fig.update_layout(
title=dict(text='Valores Observados vs Calculados', x=0.5),
xaxis_title='Valores Calculados',
yaxis_title='Valores Observados',
showlegend=True,
plot_bgcolor='white',
margin=dict(l=60, r=40, t=60, b=60)
)
fig.update_xaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
fig.update_yaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
return fig
except Exception as e:
print(f"Erro ao criar gráfico obs vs calc: {e}")
return None
def _criar_grafico_residuos(y_calc, residuos):
"""Cria gráfico de resíduos vs valores ajustados (Plotly Figure)."""
try:
fig = go.Figure()
fig.add_trace(go.Scatter(
x=y_calc,
y=residuos,
mode='markers',
marker=dict(
color=COR_PRINCIPAL,
size=10,
line=dict(color='black', width=1)
),
name='Resíduos'
))
fig.add_hline(y=0, line_dash="dash", line_color=COR_LINHA, line_width=2)
fig.update_layout(
title=dict(text='Resíduos vs Valores Ajustados', x=0.5),
xaxis_title='Valores Ajustados',
yaxis_title='Resíduos',
showlegend=False,
plot_bgcolor='white',
margin=dict(l=60, r=40, t=60, b=60)
)
fig.update_xaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
fig.update_yaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
return fig
except Exception as e:
print(f"Erro ao criar gráfico de resíduos: {e}")
return None
def _criar_histograma_residuos(residuos):
"""Cria histograma dos resíduos com curva normal (Plotly Figure)."""
try:
fig = go.Figure()
fig.add_trace(go.Histogram(
x=residuos,
histnorm='probability density',
marker=dict(color=COR_PRINCIPAL, line=dict(color='black', width=1)),
opacity=0.7,
name='Resíduos'
))
mu, sigma = np.mean(residuos), np.std(residuos)
x_norm = np.linspace(min(residuos) - sigma, max(residuos) + sigma, 100)
y_norm = stats.norm.pdf(x_norm, mu, sigma)
fig.add_trace(go.Scatter(
x=x_norm,
y=y_norm,
mode='lines',
line=dict(color=COR_LINHA, width=3),
name='Curva Normal'
))
fig.update_layout(
title=dict(text='Distribuição dos Resíduos', x=0.5),
xaxis_title='Resíduos',
yaxis_title='Densidade',
showlegend=True,
plot_bgcolor='white',
barmode='overlay',
margin=dict(l=60, r=40, t=60, b=60)
)
fig.update_xaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
fig.update_yaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
return fig
except Exception as e:
print(f"Erro ao criar histograma: {e}")
return None
def _criar_grafico_cook(modelos_sm):
"""Cria gráfico de Distância de Cook (Plotly Figure)."""
try:
if modelos_sm is None: return None
influence = OLSInfluence(modelos_sm)
cooks_d = influence.cooks_distance[0]
n = len(cooks_d)
indices = np.arange(1, n + 1)
limite = 4 / n
fig = go.Figure()
# Hastes (linhas verticais)
for idx, valor in zip(indices, cooks_d):
cor = COR_LINHA if valor > limite else COR_PRINCIPAL
fig.add_trace(go.Scatter(
x=[idx, idx],
y=[0, valor],
mode='lines',
line=dict(color=cor, width=1.5),
showlegend=False,
hoverinfo='skip'
))
# Pontos
cores_pontos = [COR_LINHA if v > limite else COR_PRINCIPAL for v in cooks_d]
fig.add_trace(go.Scatter(
x=indices,
y=cooks_d,
mode='markers',
marker=dict(color=cores_pontos, size=8, line=dict(color='black', width=1)),
name='Distância de Cook',
hovertemplate='Obs: %{x}<br>Cook: %{y:.4f}<extra></extra>'
))
fig.add_hline(
y=limite,
line_dash="dash",
line_color='gray',
annotation_text=f"4/n = {limite:.4f}",
annotation_position="top right"
)
fig.update_layout(
title=dict(text='Distância de Cook', x=0.5),
xaxis_title='Observação',
yaxis_title='Distância de Cook',
plot_bgcolor='white',
margin=dict(l=60, r=40, t=60, b=60)
)
fig.update_xaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
fig.update_yaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
return fig
except Exception as e:
print(f"Erro ao criar gráfico de Cook: {e}")
return None
def _criar_grafico_correlacao(modelos_sm):
"""Gera heatmap de correlação (Plotly Figure)."""
try:
if modelos_sm is None or not hasattr(modelos_sm, 'model'):
return None
model = modelos_sm.model
if not hasattr(model, 'exog') or not hasattr(model, 'endog'):
return None
X = model.exog
X_names = model.exog_names
y = model.endog
y_name = getattr(model, 'endog_names', 'Variável Dependente')
df_X = pd.DataFrame(X, columns=X_names)
# Remover constantes explícitas
df_X = df_X.drop(
columns=[c for c in df_X.columns if c.lower() in ('const', 'intercept')],
errors='ignore'
)
df_y = pd.DataFrame({y_name: y})
df = pd.concat([df_y, df_X], axis=1)
# Remover colunas constantes
variancias = df.var(ddof=0)
df = df.loc[:, variancias.fillna(0) > 0]
if df.shape[1] < 2: return None
corr = df.corr()
if corr.isnull().values.all(): return None
text = np.round(corr.values, 2).astype(str)
fig = go.Figure(data=go.Heatmap(
z=corr.values,
x=corr.columns,
y=corr.index,
text=text,
texttemplate="%{text}",
textfont=dict(size=10),
zmin=-1, zmax=1,
colorscale='RdBu',
reversescale=True,
colorbar=dict(title='Correlação')
))
fig.update_layout(
title=dict(text="Matriz de Correlação", x=0.5),
height=600,
template='plotly_white',
xaxis=dict(tickangle=45, showgrid=False),
yaxis=dict(autorange='reversed', showgrid=True)
)
return fig
except Exception:
return None
def gerar_todos_graficos(pacote):
"""Orquestra a geração de todos os gráficos a partir do pacote."""
graficos = {
"obs_calc": None,
"residuos": None,
"hist": None,
"cook": None,
"corr": None
}
obs_calc = pacote.get("tabelas_obs_calc")
modelos_sm = pacote.get("modelos_sm")
# Identificar vetores
y_obs = None
y_calc = None
residuos = None
# Tenta pegar do DataFrame obs_calc
if obs_calc is not None and not obs_calc.empty:
cols_lower = {c.lower(): c for c in obs_calc.columns}
# Encontrar coluna observada
for nome in ['observado', 'obs', 'y_obs', 'y', 'valor_observado']:
if nome in cols_lower:
y_obs = obs_calc[cols_lower[nome]].values
break
# Encontrar coluna calculada
for nome in ['calculado', 'calc', 'y_calc', 'y_hat', 'previsto']:
if nome in cols_lower:
y_calc = obs_calc[cols_lower[nome]].values
break
# Encontrar coluna resíduos
for nome in ['residuo', 'residuos', 'resid']:
if nome in cols_lower:
residuos = obs_calc[cols_lower[nome]].values
break
# Fallback para o objeto statsmodels
if modelos_sm is not None:
try:
if y_obs is None and hasattr(modelos_sm.model, 'endog'):
y_obs = modelos_sm.model.endog
if y_calc is None and hasattr(modelos_sm, 'fittedvalues'):
y_calc = modelos_sm.fittedvalues
if residuos is None and hasattr(modelos_sm, 'resid'):
residuos = modelos_sm.resid
except Exception:
pass
# Cálculo manual se necessário
if residuos is None and y_obs is not None and y_calc is not None:
residuos = np.array(y_obs) - np.array(y_calc)
# Converter para numpy
y_obs = np.array(y_obs) if y_obs is not None else None
y_calc = np.array(y_calc) if y_calc is not None else None
residuos = np.array(residuos) if residuos is not None else None
# Gerar cada gráfico
if y_obs is not None and y_calc is not None:
graficos["obs_calc"] = _criar_grafico_obs_calc(y_obs, y_calc)
if residuos is not None and y_calc is not None:
graficos["residuos"] = _criar_grafico_residuos(y_calc, residuos)
if residuos is not None:
graficos["hist"] = _criar_histograma_residuos(residuos)
if modelos_sm is not None:
graficos["cook"] = _criar_grafico_cook(modelos_sm)
graficos["corr"] = _criar_grafico_correlacao(modelos_sm)
return graficos
# ============================================================
# FUNÇÃO: REORGANIZAR MODELOS_RESUMOS
# ============================================================
def reorganizar_modelos_resumos(resumo_original):
"""
Reorganiza a estrutura do dicionário modelos_resumos.
"""
return {
"estatisticas_gerais": {
"n": {"nome": "Número de observações", "valor": resumo_original.get("n")},
"k": {"nome": "Número de variáveis independentes", "valor": resumo_original.get("k")},
"desvio_padrao_residuos": {"nome": "Desvio padrão dos resíduos", "valor": resumo_original.get("desvio_padrao_residuos")},
"mse": {"nome": "MSE", "valor": resumo_original.get("mse")},
"r2": {"nome": "R²", "valor": resumo_original.get("r2")},
"r2_ajustado": {"nome": "R² ajustado", "valor": resumo_original.get("r2_ajustado")},
"r_pearson": {"nome": "Correlação Pearson", "valor": resumo_original.get("r_pearson")}
},
"teste_f": {
"nome": "Teste F",
"estatistica": resumo_original.get("Fc"),
"pvalor": resumo_original.get("p_valor_F"),
"interpretacao": resumo_original.get("Interpretacao_F")
},
"teste_ks": {
"nome": "Teste de Normalidade (Kolmogorov-Smirnov)",
"estatistica": resumo_original.get("ks_stat"),
"pvalor": resumo_original.get("ks_p"),
"interpretacao": resumo_original.get("Interpretacao_KS")
},
"perc_resid": {
"nome": "Teste de Normalidade (Comparação com a Curva Normal)",
"valor": resumo_original.get("perc_resid"),
"interpretacao": [
"Ideal 68% → aceitável entre 64% e 75%",
"Ideal 90% → aceitável entre 88% e 95%",
"Ideal 95% → aceitável entre 95% e 100%"
]
},
"teste_dw": {
"nome": "Teste de Autocorrelação (Durbin-Watson)",
"estatistica": resumo_original.get("dw"),
"interpretacao": resumo_original.get("Interpretacao_DW")
},
"teste_bp": {
"nome": "Teste de Homocedasticidade (Breusch-Pagan)",
"estatistica": resumo_original.get("bp_lm"),
"pvalor": resumo_original.get("bp_p"),
"interpretacao": resumo_original.get("Interpretacao_BP")
},
"equacao": resumo_original.get("equacao")
}
# ============================================================
# FUNÇÃO: FORMATAR VALOR MONETÁRIO
# ============================================================
def formatar_monetario(valor):
if pd.isna(valor): return "N/A"
return f"R$ {valor:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
# ============================================================
# FUNÇÃO: FORMATAR RESUMO COMO HTML
# ============================================================
def formatar_resumo_html(resumo_reorganizado):
def criar_titulo_secao(titulo):
return f'<div class="section-title-orange-solid">{titulo}</div>'
def criar_linha_campo(campo, valor):
return f"""<div class="field-row"><span class="field-row-label">{campo}</span><span class="field-row-value">{valor}</span></div>"""
def criar_linha_interpretacao(interpretacao):
return f"""<div class="field-row"><span class="field-row-label">Interpretação</span><span class="field-row-value-italic">{interpretacao}</span></div>"""
def formatar_numero(valor, casas_decimais=4):
if valor is None: return "N/A"
if isinstance(valor, (int, float, np.floating)): return f"{valor:.{casas_decimais}f}"
return str(valor)
linhas_html = []
# 1. Estatísticas Gerais
estat_gerais = resumo_reorganizado.get("estatisticas_gerais", {})
if estat_gerais:
linhas_html.append(criar_titulo_secao("Estatísticas Gerais"))
for chave, dados in estat_gerais.items():
linhas_html.append(criar_linha_campo(dados.get("nome", chave), formatar_numero(dados.get("valor"))))
# 2. Testes (Simplificado para brevidade, lógica idêntica ao original)
for chave_teste, label in [("teste_f", "Estatística F"), ("teste_ks", "Estatística KS"), ("teste_dw", "Estatística DW"), ("teste_bp", "Estatística LM")]:
teste = resumo_reorganizado.get(chave_teste, {})
if teste.get("estatistica") is not None:
linhas_html.append(criar_titulo_secao(teste.get("nome")))
linhas_html.append(criar_linha_campo(label, formatar_numero(teste["estatistica"])))
if teste.get("pvalor") is not None:
linhas_html.append(criar_linha_campo("P-valor", formatar_numero(teste["pvalor"])))
if teste.get("interpretacao"):
linhas_html.append(criar_linha_interpretacao(teste["interpretacao"]))
# Percentuais
perc_resid = resumo_reorganizado.get("perc_resid", {})
if perc_resid.get("valor") is not None:
linhas_html.append(criar_titulo_secao(perc_resid.get("nome")))
linhas_html.append(criar_linha_campo("Percentuais Atingidos", perc_resid["valor"]))
if perc_resid.get("interpretacao"):
linhas_html.append('<div class="interpretation-label">Interpretação</div>')
for ideal in perc_resid["interpretacao"]:
linhas_html.append(f'<div class="interpretation-item">• {ideal}</div>')
# Equação
equacao = resumo_reorganizado.get("equacao")
if equacao:
linhas_html.append(criar_titulo_secao("Equação do Modelo"))
linhas_html.append(f'<div class="equation-box">{equacao}</div>')
return f"""<div class="dai-card scrollable-container">{"".join(linhas_html)}</div>"""
# ============================================================
# FUNÇÃO: CRIAR TÍTULO DE SEÇÃO ESTILIZADO
# ============================================================
def criar_titulo_secao_html(titulo):
return f'<div class="section-title-orange">{titulo}</div>'
# ============================================================
# FUNÇÃO: FORMATAR ESCALAS/TRANSFORMAÇÕES
# ============================================================
def formatar_escalas_html(escalas_raw):
if isinstance(escalas_raw, pd.DataFrame): itens = escalas_raw.iloc[:, 0].tolist()
elif isinstance(escalas_raw, list): itens = escalas_raw
else: itens = [str(escalas_raw)]
itens = [str(item) for item in itens if item and str(item).strip()]
if not itens: return "<p style='color: #6c757d; font-style: italic;'>Nenhuma transformação disponível.</p>"
max_chars = max(len(item) for item in itens)
largura_min = max(150, max_chars * 7 + 28)
cards_html = ""
for item in itens:
if ":" in item:
partes = item.split(":", 1)
conteudo = f"""<span style="font-weight: 600; color: #495057;">{partes[0].strip()}:</span><span style="font-weight: 400; color: #6c757d; margin-left: 4px;">{partes[1].strip()}</span>"""
else:
conteudo = f"""<span style="font-weight: 600; color: #495057;">{item}</span>"""
cards_html += f"""<div class="dai-card-light" style="min-width: {largura_min}px;">{conteudo}</div>"""
return f"""<div class="dai-card">{criar_titulo_secao_html("Escalas / Transformações")}<div class="dai-cards-grid" style="grid-template-columns: repeat(auto-fill, minmax({largura_min}px, 1fr));">{cards_html}</div></div>"""
# ============================================================
# FUNÇÃO: GERAR MAPA FOLIUM
# ============================================================
def geo_folium(df):
if "lat" not in df.columns or "lon" not in df.columns: raise ValueError("Colunas lat/lon ausentes.")
df_mapa = df.dropna(subset=["lat", "lon"]).copy()
if df_mapa.empty: raise ValueError("Sem coordenadas válidas.")
m = folium.Map(location=[df_mapa["lat"].mean(), df_mapa["lon"].mean()], zoom_start=12, tiles=None)
folium.TileLayer(tiles="OpenStreetMap", name="OpenStreetMap", control=True).add_to(m)
folium.TileLayer(tiles="CartoDB positron", name="Positron", control=True).add_to(m)
for _, row in df_mapa.iterrows():
itens = []
for col, val in row.items():
if col.lower() in ["lat", "lon"]: continue
col_norm = col.lower()
if isinstance(val, (int, float, np.floating)):
if any(k in col_norm for k in ["valor", "preco", "vu", "vunit"]): val_fmt = formatar_monetario(val)
else: val_fmt = f"{val:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
else: val_fmt = str(val)
itens.append((col, val_fmt))
MAX_ITENS = 8
colunas_html = []
for i in range(0, len(itens), MAX_ITENS):
chunk = itens[i:i+MAX_ITENS]
trs = "".join([f"<tr style='border-bottom: 1px solid #e9ecef;'><td style='padding:4px 8px 4px 0; color:#6c757d; font-weight:500;'>{c}</td><td style='padding:4px 0; text-align:right; color:#495057;'>{v}</td></tr>" for c, v in chunk])
style = "border-left: 2px solid #dee2e6; padding-left: 20px;" if i > 0 else ""
colunas_html.append(f"<div style='flex: 0 0 auto; {style}'><table style='border-collapse:collapse; font-size:12px;'>{trs}</table></div>")
popup_html = f"""<div style="font-family:'Segoe UI'; border-radius:8px; overflow:hidden;"><div style="background:#6c757d; color:white; padding:10px 15px; font-weight:600;">Dados do Registro</div><div style="padding:12px 15px; background:#f8f9fa;"><div style="display:flex; gap:20px;">{"".join(colunas_html)}</div></div></div>"""
folium.CircleMarker(
location=[row["lat"], row["lon"]], radius=8,
popup=folium.Popup(popup_html, max_width=280 * len(colunas_html)),
color="#FF8C00", fill=True, fillColor="#FF8C00", fillOpacity=0.7
).add_to(m)
folium.LayerControl().add_to(m)
plugins.Fullscreen().add_to(m)
m.fit_bounds([df_mapa[["lat", "lon"]].min().values.tolist(), df_mapa[["lat", "lon"]].max().values.tolist()])
return m._repr_html_()
# ============================================================
# FUNÇÃO: CARREGAR + VALIDAR MODELO (.dai)
# ============================================================
def carregar_modelo_gradio(arquivo):
if arquivo is None: return None, "Nenhum arquivo enviado."
try:
pacote = load(arquivo.name)
if not isinstance(pacote, dict): return None, "Arquivo inválido."
faltantes = [k for k in CHAVES_ESPERADAS if k not in pacote]
if faltantes: return None, f"Pacote incompleto. Faltando: {faltantes}"
return pacote, f"Modelo carregado: {os.path.basename(arquivo.name)}"
except Exception as e: return None, f"Erro: {e}"
# ============================================================
# FUNÇÃO: DESEMPACOTAR + EXIBIR CONTEÚDO
# ============================================================
def exibir_modelo(pacote):
# Retorna Nones se pacote vazio. Total de outputs = 13
if pacote is None:
return [None] * 13
# 1. Dados
dados = pacote["Xy_preview_out_coords"].reset_index()
for col in dados.columns:
if col.lower() in ["lat", "lon"]: dados[col] = dados[col].round(6)
elif pd.api.types.is_numeric_dtype(dados[col]): dados[col] = dados[col].round(2)
# 2. Estatísticas
estat = pd.DataFrame(pacote["estatisticas"])
if not isinstance(estat.index, pd.RangeIndex):
estat.insert(0, "Variável", estat.index.astype(str))
estat = estat.reset_index(drop=True)
estat = estat.round(2)
# 3. Escalas
escalas_html = formatar_escalas_html(pacote["formatted_top_transformation_info"])
# 4. X e y
X = pacote["top_X_esc"].reset_index()
y = pacote["top_y_esc"].reset_index()
if 'index' in y.columns and 'index' in X.columns: y = y.drop(columns=['index'])
df_X_y = pd.concat([X, y], axis=1).loc[:, ~pd.concat([X, y], axis=1).columns.duplicated()].round(2)
# 5. Resumo
resumo_html = formatar_resumo_html(reorganizar_modelos_resumos(pacote["modelos_resumos"]))
# 6. Coeficientes
tab_coef = pd.DataFrame(pacote["tabelas_coef"])
if not isinstance(tab_coef.index, pd.RangeIndex):
tab_coef.insert(0, "Variável", tab_coef.index.astype(str))
tab_coef = tab_coef.reset_index(drop=True)
mask = tab_coef["Variável"].str.lower().isin(["intercept", "const", "(intercept)"])
if mask.any(): tab_coef = pd.concat([tab_coef[mask], tab_coef[~mask]], ignore_index=True)
tab_coef = tab_coef.round(2)
# 7. Obs x Calc
tab_obs_calc = pacote["tabelas_obs_calc"].reset_index().round(2)
# 8. Gráficos (GERAÇÃO DINÂMICA)
figs_dict = gerar_todos_graficos(pacote)
# 9. Mapa
mapa_html = geo_folium(dados)
return (
dados,
estat,
escalas_html,
df_X_y,
resumo_html,
tab_coef,
tab_obs_calc,
figs_dict["obs_calc"], # Plot 1
figs_dict["residuos"], # Plot 2
figs_dict["hist"], # Plot 3
figs_dict["cook"], # Plot 4
figs_dict["corr"], # Plot 5
mapa_html,
)
def toggle_mapa(visivel):
novo_estado = not visivel
return novo_estado, gr.update(visible=novo_estado), gr.update(value="Mostrar mapa" if not novo_estado else "Recolher mapa")
def limpar_tudo():
return (
None, "", None, None, None, "", None, "", None, None,
None, None, None, None, None, # 5 gráficos nulos
"", True, gr.update(visible=True), gr.update(value="Recolher mapa")
)
# ============================================================
# INTERFACE GRADIO
# ============================================================
description = f"""
# <p style="text-align: center;">MODELOS ESTATÍSTICOS</p>
<p style="text-align: center;">Divisão de Avaliação de Imóveis</p>
<hr style="color: #333; background-color: #333; height: 1px; border: none;">
"""
with gr.Blocks() as app:
gr.Markdown(description)
estado_pacote = gr.State(None)
estado_mapa_visivel = gr.State(True)
with gr.Group(elem_classes="upload-area"):
upload = gr.File(label="Enviar modelo salvo (.dai)", file_types=[".dai"], scale=1)
with gr.Row(equal_height=True):
status = gr.Textbox(show_label=False, interactive=False, scale=3, lines=1)
btn_exibir = gr.Button("Exibir modelo", scale=2, variant="primary")
btn_toggle_mapa = gr.Button("Recolher mapa", scale=1, variant="secondary")
btn_limpar = gr.Button("Limpar tudo", scale=1, variant="secondary")
with gr.Row(elem_classes="main-row"):
# COLUNA ESQUERDA (1/4)
with gr.Column(scale=1, elem_classes="left-panel"):
with gr.Tabs(elem_classes="tabs-container"):
with gr.Tab("Dados"):
gr.HTML(criar_titulo_secao_html("Dados Utilizados"))
out_dados = gr.Dataframe(show_label=False, max_height=300)
gr.HTML(criar_titulo_secao_html("Estatísticas"))
out_estat = gr.Dataframe(show_label=False, max_height=300)
with gr.Tab("Transformações"):
out_escalas = gr.HTML()
gr.HTML(criar_titulo_secao_html("X e y Transformados"))
out_df_xy = gr.Dataframe(show_label=False, max_height=400)
with gr.Tab("Resumo"):
out_resumo = gr.HTML()
with gr.Tab("Coeficientes"):
gr.HTML(criar_titulo_secao_html("Tabela de Coeficientes"))
out_coef = gr.Dataframe(show_label=False, max_height=700)
with gr.Tab("Obs x Calc"):
gr.HTML(criar_titulo_secao_html("Tabela Obs x Calc"))
out_obs = gr.Dataframe(show_label=False, max_height=600)
with gr.Tab("Gráficos"):
gr.HTML(criar_titulo_secao_html("Diagnóstico Visual"))
# Layout vertical para os gráficos Plotly
with gr.Column():
out_plot_obs = gr.Plot(label="Obs vs Calc")
out_plot_res = gr.Plot(label="Resíduos vs Ajustados")
out_plot_hist = gr.Plot(label="Histograma Resíduos")
out_plot_cook = gr.Plot(label="Distância de Cook")
out_plot_corr = gr.Plot(label="Correlação")
with gr.Tab("Avaliação"):
gr.HTML(criar_titulo_secao_html("Avaliação Individual"))
gr.HTML("""<div class="placeholder-alert"><p>Módulo em desenvolvimento</p></div>""")
with gr.Tab("Avaliação em Massa"):
gr.HTML(criar_titulo_secao_html("Avaliação em Lote"))
gr.HTML("""<div class="placeholder-alert"><p>Módulo em desenvolvimento</p></div>""")
# COLUNA DIREITA (3/4)
with gr.Column(scale=1, visible=True, elem_classes="map-panel") as coluna_mapa:
gr.HTML('<div class="section-title-orange">Mapa de Distribuição dos Dados</div>')
out_mapa = gr.HTML(label="", elem_id="map-frame")
# --------------------------------------------------------
# EVENTOS
# --------------------------------------------------------
upload.upload(carregar_modelo_gradio, inputs=upload, outputs=[estado_pacote, status])
btn_exibir.click(
exibir_modelo,
inputs=estado_pacote,
outputs=[
out_dados, out_estat, out_escalas, out_df_xy, out_resumo, out_coef, out_obs,
out_plot_obs, out_plot_res, out_plot_hist, out_plot_cook, out_plot_corr, # Novos outputs gráficos
out_mapa,
],
)
btn_toggle_mapa.click(toggle_mapa, inputs=estado_mapa_visivel, outputs=[estado_mapa_visivel, coluna_mapa, btn_toggle_mapa])
btn_limpar.click(
limpar_tudo,
outputs=[
estado_pacote, status, upload,
out_dados, out_estat, out_escalas, out_df_xy, out_resumo, out_coef, out_obs,
out_plot_obs, out_plot_res, out_plot_hist, out_plot_cook, out_plot_corr,
out_mapa, estado_mapa_visivel, coluna_mapa, btn_toggle_mapa,
]
)
if __name__ == "__main__":
custom_css = carregar_css()
app.launch(css=custom_css)