Spaces:
Sleeping
Sleeping
| # ============================================================ | |
| # 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) |