Spaces:
Running
Running
Guilherme Silberfarb Costa commited on
Commit ·
55eda43
1
Parent(s): b75b945
Remove Gradio legado e automatiza release portátil
Browse files- .github/workflows/build-windows-portable.yml +1 -11
- backend/app/core/elaboracao/app.py +0 -1908
- backend/app/core/elaboracao/carregamento.py +0 -639
- backend/app/core/elaboracao/modelo.py +0 -991
- backend/app/core/elaboracao/outliers.py +0 -300
- backend/app/core/visualizacao/app.py +283 -1045
- backend/app/runtime_config.py +10 -0
- backend/requirements.txt +0 -1
- build/windows/appsettings.example.json +1 -3
- build/windows/build_portable.ps1 +1 -0
- build/windows/mesa_frame_portable.spec +0 -2
- build/windows/package_portable_release.py +181 -0
.github/workflows/build-windows-portable.yml
CHANGED
|
@@ -40,18 +40,8 @@ jobs:
|
|
| 40 |
shell: pwsh
|
| 41 |
run: ./build/windows/smoke_test_portable.ps1
|
| 42 |
|
| 43 |
-
- name: Archive portable folder
|
| 44 |
-
shell: pwsh
|
| 45 |
-
run: |
|
| 46 |
-
if (Test-Path "dist/MesaFrame-portable.zip") {
|
| 47 |
-
Remove-Item "dist/MesaFrame-portable.zip" -Force
|
| 48 |
-
}
|
| 49 |
-
Compress-Archive -Path "dist/MesaFrame/*" -DestinationPath "dist/MesaFrame-portable.zip"
|
| 50 |
-
|
| 51 |
- name: Upload artifact
|
| 52 |
uses: actions/upload-artifact@v4
|
| 53 |
with:
|
| 54 |
name: MesaFrame-portable
|
| 55 |
-
path:
|
| 56 |
-
dist/MesaFrame
|
| 57 |
-
dist/MesaFrame-portable.zip
|
|
|
|
| 40 |
shell: pwsh
|
| 41 |
run: ./build/windows/smoke_test_portable.ps1
|
| 42 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
- name: Upload artifact
|
| 44 |
uses: actions/upload-artifact@v4
|
| 45 |
with:
|
| 46 |
name: MesaFrame-portable
|
| 47 |
+
path: dist/MesaFrame-portable.zip
|
|
|
|
|
|
backend/app/core/elaboracao/app.py
DELETED
|
@@ -1,1908 +0,0 @@
|
|
| 1 |
-
# -*- coding: utf-8 -*-
|
| 2 |
-
"""
|
| 3 |
-
app.py - Interface Gradio para elaboração de modelos estatísticos.
|
| 4 |
-
|
| 5 |
-
Contém criar_aba() (construção de UI + event wiring) e callbacks auxiliares pequenos.
|
| 6 |
-
Lógica de negócio delegada a módulos: modelo.py, carregamento.py, outliers.py, formatadores.py.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
import gradio as gr
|
| 10 |
-
import pandas as pd
|
| 11 |
-
import os
|
| 12 |
-
import json
|
| 13 |
-
|
| 14 |
-
from app.runtime_config import resolve_core_path
|
| 15 |
-
|
| 16 |
-
_avaliadores_path = resolve_core_path("elaboracao", "avaliadores.json")
|
| 17 |
-
with open(_avaliadores_path, encoding="utf-8") as _f:
|
| 18 |
-
_avaliadores_raw = json.load(_f)
|
| 19 |
-
_avaliadores_lista = _avaliadores_raw.get("avaliadores", [])
|
| 20 |
-
_avaliadores_nomes = [a["nome_completo"] for a in _avaliadores_lista]
|
| 21 |
-
_avaliadores_dict = {a["nome_completo"]: a for a in _avaliadores_lista}
|
| 22 |
-
|
| 23 |
-
# Módulos locais — lógica de negócio
|
| 24 |
-
from .core import (
|
| 25 |
-
obter_colunas_numericas,
|
| 26 |
-
exportar_base_csv,
|
| 27 |
-
TRANSFORMACOES,
|
| 28 |
-
)
|
| 29 |
-
from .charts import criar_mapa
|
| 30 |
-
from .csv_export import exportar_dataframe_csv_temporario
|
| 31 |
-
|
| 32 |
-
# Módulos locais — formatação
|
| 33 |
-
from .formatadores import TITULO, carregar_css, criar_header_secao
|
| 34 |
-
|
| 35 |
-
# Módulos locais — callbacks de domínio
|
| 36 |
-
from .modelo import (
|
| 37 |
-
MAX_VARS_X,
|
| 38 |
-
ao_mudar_tipo_grafico,
|
| 39 |
-
ao_mudar_y_sem_estatisticas,
|
| 40 |
-
aplicar_selecao_callback,
|
| 41 |
-
buscar_transformacoes_callback,
|
| 42 |
-
ajustar_modelo_callback,
|
| 43 |
-
_atualizar_campos_transformacoes_com_flag,
|
| 44 |
-
adotar_sugestao,
|
| 45 |
-
exportar_modelo_callback,
|
| 46 |
-
popular_campos_avaliacao_callback,
|
| 47 |
-
avaliar_imovel_callback,
|
| 48 |
-
limpar_avaliacoes_callback,
|
| 49 |
-
excluir_avaliacao_callback,
|
| 50 |
-
atualizar_base_avaliacao_callback,
|
| 51 |
-
exportar_avaliacoes_excel_callback,
|
| 52 |
-
atualizar_interativo_dicotomicas,
|
| 53 |
-
popular_dicotomicas_callback,
|
| 54 |
-
)
|
| 55 |
-
from .carregamento import (
|
| 56 |
-
ao_carregar_arquivo,
|
| 57 |
-
confirmar_aba_callback,
|
| 58 |
-
limpar_historico_callback,
|
| 59 |
-
)
|
| 60 |
-
from .geocodificacao import (
|
| 61 |
-
padronizar_coords,
|
| 62 |
-
geocodificar,
|
| 63 |
-
aplicar_correcoes_e_regeodificar,
|
| 64 |
-
formatar_status_geocodificacao,
|
| 65 |
-
preparar_display_falhas,
|
| 66 |
-
)
|
| 67 |
-
from .outliers import (
|
| 68 |
-
aplicar_filtros_callback,
|
| 69 |
-
adicionar_filtro_callback,
|
| 70 |
-
remover_ultimo_filtro_callback,
|
| 71 |
-
limpar_filtros_callback,
|
| 72 |
-
reiniciar_iteracao_callback,
|
| 73 |
-
atualizar_resumo_outliers,
|
| 74 |
-
)
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
# ============================================================
|
| 78 |
-
# CALLBACKS AUXILIARES (pequenos, diretamente ligados ao event wiring)
|
| 79 |
-
# ============================================================
|
| 80 |
-
|
| 81 |
-
def download_tabela_callback(df, prefixo="tabela"):
|
| 82 |
-
"""Callback genérico para download de DataFrame como CSV."""
|
| 83 |
-
if df is None:
|
| 84 |
-
return gr.update(value=None, visible=False)
|
| 85 |
-
|
| 86 |
-
try:
|
| 87 |
-
if isinstance(df, pd.DataFrame):
|
| 88 |
-
caminho = exportar_dataframe_csv_temporario(df, prefixo=prefixo, incluir_indice=True)
|
| 89 |
-
if not caminho:
|
| 90 |
-
return gr.update(value=None, visible=False)
|
| 91 |
-
return gr.update(value=caminho, visible=True)
|
| 92 |
-
else:
|
| 93 |
-
return gr.update(value=None, visible=False)
|
| 94 |
-
except Exception as e:
|
| 95 |
-
print(f"Erro ao exportar tabela: {e}")
|
| 96 |
-
return gr.update(value=None, visible=False)
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
def _extrair_var_mapa(var_mapa):
|
| 100 |
-
"""Retorna None se for 'Visualização Padrão' ou vazio, senão retorna o nome da variável."""
|
| 101 |
-
if not var_mapa or var_mapa == "Visualização Padrão":
|
| 102 |
-
return None
|
| 103 |
-
return var_mapa
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
def ao_clicar_tabela(df, var_mapa, evt: gr.SelectData):
|
| 107 |
-
"""Callback quando clica em linha da tabela."""
|
| 108 |
-
if df is None or evt is None:
|
| 109 |
-
return "<p>Carregue dados para ver o mapa.</p>"
|
| 110 |
-
|
| 111 |
-
# Pega o índice da linha clicada
|
| 112 |
-
indice = evt.index[0] + 1 # Ajusta para índice baseado em 1
|
| 113 |
-
|
| 114 |
-
return criar_mapa(df, indice_destacado=indice, tamanho_col=_extrair_var_mapa(var_mapa))
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
def atualizar_mapa_callback(df_filtrado, df_original, var_mapa):
|
| 118 |
-
"""Callback quando a variável de dimensionamento do mapa é alterada."""
|
| 119 |
-
df = df_filtrado if df_filtrado is not None else df_original
|
| 120 |
-
if df is None:
|
| 121 |
-
return "<p>Carregue dados para ver o mapa.</p>"
|
| 122 |
-
return criar_mapa(df, tamanho_col=_extrair_var_mapa(var_mapa))
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
MAX_FALHAS_GEO = 20 # Slots pré-alocados para correção individual de falhas de geocodificação
|
| 126 |
-
|
| 127 |
-
# ============================================================
|
| 128 |
-
# CALLBACKS DE RESOLUÇÃO DE COORDENADAS (Seção 1 — painel lat/lon)
|
| 129 |
-
# ============================================================
|
| 130 |
-
|
| 131 |
-
def _revelar_secao_2(df, mapa):
|
| 132 |
-
"""Retorna os 9 valores comuns a todos os callbacks de resolução de coords."""
|
| 133 |
-
from datetime import datetime, timezone, timedelta
|
| 134 |
-
gmt_minus_3 = timezone(timedelta(hours=-3))
|
| 135 |
-
ts = datetime.now(gmt_minus_3).strftime("%H:%M:%S")
|
| 136 |
-
return (
|
| 137 |
-
df, # estado_df
|
| 138 |
-
df, # estado_df_filtrado
|
| 139 |
-
mapa, # mapa_html
|
| 140 |
-
gr.update(visible=False), # row_coords_panel
|
| 141 |
-
gr.update(visible=True, value=criar_header_secao(2, "Visualizar Dados", ts)), # header_secao_2
|
| 142 |
-
gr.update(visible=True, open=True), # accordion_secao_2
|
| 143 |
-
gr.update(visible=True), # header_secao_3
|
| 144 |
-
gr.update(visible=True, open=True), # accordion_secao_3
|
| 145 |
-
)
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
def confirmar_mapeamento_callback(df, col_lat, col_lon):
|
| 149 |
-
"""Opção 1: copia col_lat → 'lat' e col_lon → 'lon', libera Seção 2.
|
| 150 |
-
|
| 151 |
-
CONTRACT: 10 itens — html_erro_mapeamento + 8 de _revelar_secao_2 + status
|
| 152 |
-
|
| 153 |
-
Valida 4 aspectos antes de aceitar as colunas:
|
| 154 |
-
1. Conversibilidade numérica (≥50% dos valores)
|
| 155 |
-
2. Limites teóricos (lat −90/+90, lon −180/+180, ≥80%)
|
| 156 |
-
3. Inversão entre colunas (lat parece lon e vice-versa)
|
| 157 |
-
4. Casas decimais (coordenadas geográficas raramente são inteiros exatos)
|
| 158 |
-
"""
|
| 159 |
-
def _erro_mapeamento(linhas_html):
|
| 160 |
-
html = (
|
| 161 |
-
'<div style="color:#c0392b;margin-top:6px;padding:8px 12px;'
|
| 162 |
-
'background:#fdf2f2;border-radius:6px;border-left:3px solid #c0392b">'
|
| 163 |
-
'<strong>⚠ Diagnóstico das colunas selecionadas:</strong><br>'
|
| 164 |
-
+ linhas_html +
|
| 165 |
-
'</div>'
|
| 166 |
-
)
|
| 167 |
-
return (
|
| 168 |
-
gr.update(value=html),
|
| 169 |
-
gr.update(), gr.update(), gr.update(),
|
| 170 |
-
gr.update(), gr.update(), gr.update(), gr.update(), gr.update(),
|
| 171 |
-
gr.update(),
|
| 172 |
-
)
|
| 173 |
-
|
| 174 |
-
if df is None or not col_lat or not col_lon:
|
| 175 |
-
return _erro_mapeamento("Selecione as colunas de latitude e longitude antes de confirmar.")
|
| 176 |
-
|
| 177 |
-
n_total = len(df)
|
| 178 |
-
if n_total == 0:
|
| 179 |
-
return _erro_mapeamento("O DataFrame está vazio.")
|
| 180 |
-
|
| 181 |
-
# Aspecto 1 — Conversibilidade numérica
|
| 182 |
-
lat_num = pd.to_numeric(df[col_lat], errors="coerce")
|
| 183 |
-
lon_num = pd.to_numeric(df[col_lon], errors="coerce")
|
| 184 |
-
lat_vals = lat_num.dropna()
|
| 185 |
-
lon_vals = lon_num.dropna()
|
| 186 |
-
lat_conv = len(lat_vals) / n_total
|
| 187 |
-
lon_conv = len(lon_vals) / n_total
|
| 188 |
-
|
| 189 |
-
erros_conv = []
|
| 190 |
-
if lat_conv < 0.5:
|
| 191 |
-
erros_conv.append(
|
| 192 |
-
f"• <strong>{col_lat}</strong>: apenas {lat_conv:.0%} dos valores são numéricos "
|
| 193 |
-
f"— a coluna provavelmente não contém coordenadas."
|
| 194 |
-
)
|
| 195 |
-
if lon_conv < 0.5:
|
| 196 |
-
erros_conv.append(
|
| 197 |
-
f"• <strong>{col_lon}</strong>: apenas {lon_conv:.0%} dos valores são numéricos "
|
| 198 |
-
f"— a coluna provavelmente não contém coordenadas."
|
| 199 |
-
)
|
| 200 |
-
if erros_conv:
|
| 201 |
-
return _erro_mapeamento("<br>".join(erros_conv))
|
| 202 |
-
|
| 203 |
-
# Aspecto 2 — Limites teóricos
|
| 204 |
-
lat_in_range = ((lat_vals >= -90) & (lat_vals <= 90)).mean()
|
| 205 |
-
lon_in_range = ((lon_vals >= -180) & (lon_vals <= 180)).mean()
|
| 206 |
-
|
| 207 |
-
# Aspecto 3 — Inversão de colunas
|
| 208 |
-
lat_in_lon_range = ((lat_vals >= -180) & (lat_vals <= 180)).mean()
|
| 209 |
-
lon_in_lat_range = ((lon_vals >= -90) & (lon_vals <= 90)).mean()
|
| 210 |
-
inversao = lat_in_range < 0.5 and lat_in_lon_range > 0.8 and lon_in_lat_range > 0.8
|
| 211 |
-
|
| 212 |
-
# Aspecto 4 — Casas decimais
|
| 213 |
-
lat_tem_decimais = (lat_vals % 1 != 0).mean() if len(lat_vals) > 0 else 0.0
|
| 214 |
-
lon_tem_decimais = (lon_vals % 1 != 0).mean() if len(lon_vals) > 0 else 0.0
|
| 215 |
-
|
| 216 |
-
linhas = []
|
| 217 |
-
|
| 218 |
-
if inversao:
|
| 219 |
-
linhas.append(
|
| 220 |
-
f"• As colunas parecem estar <strong>invertidas</strong>: "
|
| 221 |
-
f"<strong>{col_lat}</strong> tem {1 - lat_in_range:.0%} dos valores fora de −90 a +90 "
|
| 222 |
-
f"(fora do intervalo de latitude), mas dentro de −180 a +180 (intervalo de longitude). "
|
| 223 |
-
f"<strong>{col_lon}</strong> tem {lon_in_lat_range:.0%} dos valores em −90 a +90 "
|
| 224 |
-
f"(faixa típica de latitude). Tente trocar as colunas."
|
| 225 |
-
)
|
| 226 |
-
else:
|
| 227 |
-
if lat_in_range < 0.8:
|
| 228 |
-
linhas.append(
|
| 229 |
-
f"• <strong>{col_lat}</strong>: apenas {lat_in_range:.0%} dos valores estão "
|
| 230 |
-
f"em −90 a +90 (esperado para latitude)."
|
| 231 |
-
)
|
| 232 |
-
if lon_in_range < 0.8:
|
| 233 |
-
linhas.append(
|
| 234 |
-
f"• <strong>{col_lon}</strong>: apenas {lon_in_range:.0%} dos valores estão "
|
| 235 |
-
f"em −180 a +180 (esperado para longitude)."
|
| 236 |
-
)
|
| 237 |
-
|
| 238 |
-
# Aspecto 4 só é avaliado se não há outros problemas (seria ruído adicional)
|
| 239 |
-
if not linhas:
|
| 240 |
-
if lat_tem_decimais < 0.1:
|
| 241 |
-
linhas.append(
|
| 242 |
-
f"• <strong>{col_lat}</strong>: {lat_tem_decimais:.0%} dos valores têm casas decimais "
|
| 243 |
-
f"— coordenadas geográficas normalmente são valores de ponto flutuante, não inteiros exatos."
|
| 244 |
-
)
|
| 245 |
-
if lon_tem_decimais < 0.1:
|
| 246 |
-
linhas.append(
|
| 247 |
-
f"• <strong>{col_lon}</strong>: {lon_tem_decimais:.0%} dos valores têm casas decimais "
|
| 248 |
-
f"— coordenadas geográficas normalmente são valores de ponto flutuante, não inteiros exatos."
|
| 249 |
-
)
|
| 250 |
-
|
| 251 |
-
if linhas:
|
| 252 |
-
return _erro_mapeamento("<br>".join(linhas))
|
| 253 |
-
|
| 254 |
-
df_novo = padronizar_coords(df, col_lat, col_lon)
|
| 255 |
-
mapa = criar_mapa(df_novo)
|
| 256 |
-
return (
|
| 257 |
-
gr.update(value=""), # html_erro_mapeamento — limpar
|
| 258 |
-
) + _revelar_secao_2(df_novo, mapa) + ("Coordenadas mapeadas. Seção 2 liberada.",)
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
def geocodificar_callback(df, col_cdlog, col_num, auto_200):
|
| 262 |
-
"""Opção 3: executa geocodificação por interpolação em eixos.
|
| 263 |
-
|
| 264 |
-
CONTRACT: 65 itens —
|
| 265 |
-
estado_geo_temp, estado_df_falhas, html_geo_status,
|
| 266 |
-
20×falha_rows, 20×falha_htmls, 20×falha_inputs,
|
| 267 |
-
btn_aplicar_correcoes_geo, btn_usar_coords_geo
|
| 268 |
-
"""
|
| 269 |
-
N = MAX_FALHAS_GEO
|
| 270 |
-
_no_slots = (
|
| 271 |
-
*[gr.update(visible=False)] * N,
|
| 272 |
-
*[gr.update(value="")] * N,
|
| 273 |
-
*[gr.update(value=None)] * N,
|
| 274 |
-
)
|
| 275 |
-
|
| 276 |
-
if df is None or not col_cdlog or not col_num:
|
| 277 |
-
return (
|
| 278 |
-
None, None,
|
| 279 |
-
"<p>Selecione as colunas CDLOG e Número Predial.</p>",
|
| 280 |
-
*_no_slots,
|
| 281 |
-
gr.update(visible=False), gr.update(visible=False),
|
| 282 |
-
)
|
| 283 |
-
try:
|
| 284 |
-
df_resultado, df_falhas, ajustados = geocodificar(df, col_cdlog, col_num, auto_200)
|
| 285 |
-
except RuntimeError as e:
|
| 286 |
-
return (
|
| 287 |
-
None, None,
|
| 288 |
-
f'<div style="color:red;padding:8px">{e}</div>',
|
| 289 |
-
*_no_slots,
|
| 290 |
-
gr.update(visible=False), gr.update(visible=False),
|
| 291 |
-
)
|
| 292 |
-
|
| 293 |
-
html_status = formatar_status_geocodificacao(df_resultado, df_falhas, ajustados)
|
| 294 |
-
n_falhas = len(df_falhas)
|
| 295 |
-
tem_coords = df_resultado["lat"].notna().any() if "lat" in df_resultado.columns else False
|
| 296 |
-
completo = n_falhas == 0 and tem_coords
|
| 297 |
-
|
| 298 |
-
if n_falhas > N:
|
| 299 |
-
html_status += (
|
| 300 |
-
f'<div style="color:#c0392b;margin-top:6px">⚠ {n_falhas} falhas excedem o limite '
|
| 301 |
-
f'de {N} correções manuais. Corrija os endereços na planilha-fonte e recarregue.</div>'
|
| 302 |
-
)
|
| 303 |
-
return (
|
| 304 |
-
df_resultado, None,
|
| 305 |
-
html_status,
|
| 306 |
-
*_no_slots,
|
| 307 |
-
gr.update(visible=False), gr.update(visible=False),
|
| 308 |
-
)
|
| 309 |
-
|
| 310 |
-
# Monta atualizações para os N slots
|
| 311 |
-
row_updates, html_updates, num_updates = [], [], []
|
| 312 |
-
for i in range(N):
|
| 313 |
-
if i < n_falhas:
|
| 314 |
-
row = df_falhas.iloc[i]
|
| 315 |
-
sugestoes = row.get("sugestoes", "")
|
| 316 |
-
linha = row["_idx"]
|
| 317 |
-
row_updates.append(gr.update(visible=True))
|
| 318 |
-
html_updates.append(gr.update(value=(
|
| 319 |
-
f'<div style="padding:4px 8px;background:#fff8f0;border-left:3px solid #f39c12;'
|
| 320 |
-
f'border-radius:4px;font-size:0.9em">'
|
| 321 |
-
f'<strong>Linha {linha}</strong> · CDLOG {row["cdlog"]} · '
|
| 322 |
-
f'Nº atual: <strong>{row["numero_atual"]}</strong> · {row["motivo"]}'
|
| 323 |
-
+ (f'<br><span style="color:#555">Sugestões: {sugestoes}</span>' if sugestoes else '')
|
| 324 |
-
+ '</div>'
|
| 325 |
-
)))
|
| 326 |
-
num_updates.append(gr.update(value=None, label=f"Nº Corrigido (linha {linha})"))
|
| 327 |
-
else:
|
| 328 |
-
row_updates.append(gr.update(visible=False))
|
| 329 |
-
html_updates.append(gr.update(value=""))
|
| 330 |
-
num_updates.append(gr.update(value=None))
|
| 331 |
-
|
| 332 |
-
return (
|
| 333 |
-
df_resultado, # estado_geo_temp
|
| 334 |
-
df_falhas if n_falhas > 0 else None, # estado_df_falhas
|
| 335 |
-
html_status, # html_geo_status
|
| 336 |
-
*row_updates, # 20 falha_rows
|
| 337 |
-
*html_updates, # 20 falha_htmls
|
| 338 |
-
*num_updates, # 20 falha_inputs
|
| 339 |
-
gr.update(visible=n_falhas > 0), # btn_aplicar_correcoes_geo
|
| 340 |
-
gr.update(visible=completo), # btn_usar_coords_geo
|
| 341 |
-
)
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
def aplicar_correcoes_geo_callback(df_original, df_falhas, *args):
|
| 345 |
-
"""Opção 3: aplica correções manuais e re-geocodifica.
|
| 346 |
-
|
| 347 |
-
CONTRACT: inputs = estado_geo_temp (1) + estado_df_falhas (1) + 20 falha_inputs + 3 params = 25.
|
| 348 |
-
CONTRACT: 65 itens retornados (mesmo formato de geocodificar_callback).
|
| 349 |
-
"""
|
| 350 |
-
N = MAX_FALHAS_GEO
|
| 351 |
-
correcoes_vals = list(args[:N])
|
| 352 |
-
col_cdlog, col_num, auto_200 = args[N], args[N + 1], args[N + 2]
|
| 353 |
-
|
| 354 |
-
_no_slots = (
|
| 355 |
-
*[gr.update(visible=False)] * N,
|
| 356 |
-
*[gr.update(value="")] * N,
|
| 357 |
-
*[gr.update(value=None)] * N,
|
| 358 |
-
)
|
| 359 |
-
|
| 360 |
-
if df_original is None or df_falhas is None or not col_cdlog or not col_num:
|
| 361 |
-
return (
|
| 362 |
-
None, None,
|
| 363 |
-
"<p>Sem dados para processar.</p>",
|
| 364 |
-
*_no_slots,
|
| 365 |
-
gr.update(visible=False), gr.update(visible=False),
|
| 366 |
-
)
|
| 367 |
-
|
| 368 |
-
# Monta coluna "numero_corrigido" a partir dos valores dos gr.Number
|
| 369 |
-
df_falhas_fmt = df_falhas.copy()
|
| 370 |
-
num_corrigidos = []
|
| 371 |
-
for i, val in enumerate(correcoes_vals[:len(df_falhas_fmt)]):
|
| 372 |
-
if val is not None and not (isinstance(val, float) and pd.isna(val)):
|
| 373 |
-
num_corrigidos.append(str(int(val)))
|
| 374 |
-
else:
|
| 375 |
-
num_corrigidos.append("")
|
| 376 |
-
# Preenche com "" os slots além do tamanho real de df_falhas
|
| 377 |
-
num_corrigidos += [""] * max(0, len(df_falhas_fmt) - len(num_corrigidos))
|
| 378 |
-
df_falhas_fmt["numero_corrigido"] = num_corrigidos
|
| 379 |
-
|
| 380 |
-
try:
|
| 381 |
-
df_resultado, df_falhas_novas, ajustados, manuais = aplicar_correcoes_e_regeodificar(
|
| 382 |
-
df_original, df_falhas_fmt, col_cdlog, col_num, auto_200
|
| 383 |
-
)
|
| 384 |
-
except RuntimeError as e:
|
| 385 |
-
return (
|
| 386 |
-
None, None,
|
| 387 |
-
f'<div style="color:red;padding:8px">{e}</div>',
|
| 388 |
-
*_no_slots,
|
| 389 |
-
gr.update(visible=False), gr.update(visible=False),
|
| 390 |
-
)
|
| 391 |
-
|
| 392 |
-
html_status = formatar_status_geocodificacao(df_resultado, df_falhas_novas, ajustados, manuais)
|
| 393 |
-
n_falhas = len(df_falhas_novas)
|
| 394 |
-
tem_coords = df_resultado["lat"].notna().any() if "lat" in df_resultado.columns else False
|
| 395 |
-
completo = n_falhas == 0 and tem_coords
|
| 396 |
-
|
| 397 |
-
if n_falhas > N:
|
| 398 |
-
html_status += (
|
| 399 |
-
f'<div style="color:#c0392b;margin-top:6px">⚠ {n_falhas} falhas excedem o limite '
|
| 400 |
-
f'de {N} correções manuais. Corrija os endereços na planilha-fonte e recarregue.</div>'
|
| 401 |
-
)
|
| 402 |
-
return (
|
| 403 |
-
df_resultado, None,
|
| 404 |
-
html_status,
|
| 405 |
-
*_no_slots,
|
| 406 |
-
gr.update(visible=False), gr.update(visible=False),
|
| 407 |
-
)
|
| 408 |
-
|
| 409 |
-
row_updates, html_updates, num_updates = [], [], []
|
| 410 |
-
for i in range(N):
|
| 411 |
-
if i < n_falhas:
|
| 412 |
-
row = df_falhas_novas.iloc[i]
|
| 413 |
-
sugestoes = row.get("sugestoes", "")
|
| 414 |
-
linha = row["_idx"]
|
| 415 |
-
row_updates.append(gr.update(visible=True))
|
| 416 |
-
html_updates.append(gr.update(value=(
|
| 417 |
-
f'<div style="padding:4px 8px;background:#fff8f0;border-left:3px solid #f39c12;'
|
| 418 |
-
f'border-radius:4px;font-size:0.9em">'
|
| 419 |
-
f'<strong>Linha {linha}</strong> · CDLOG {row["cdlog"]} · '
|
| 420 |
-
f'Nº atual: <strong>{row["numero_atual"]}</strong> · {row["motivo"]}'
|
| 421 |
-
+ (f'<br><span style="color:#555">Sugestões: {sugestoes}</span>' if sugestoes else '')
|
| 422 |
-
+ '</div>'
|
| 423 |
-
)))
|
| 424 |
-
num_updates.append(gr.update(value=None, label=f"Nº Corrigido (linha {linha})"))
|
| 425 |
-
else:
|
| 426 |
-
row_updates.append(gr.update(visible=False))
|
| 427 |
-
html_updates.append(gr.update(value=""))
|
| 428 |
-
num_updates.append(gr.update(value=None))
|
| 429 |
-
|
| 430 |
-
return (
|
| 431 |
-
df_resultado,
|
| 432 |
-
df_falhas_novas if n_falhas > 0 else None,
|
| 433 |
-
html_status,
|
| 434 |
-
*row_updates,
|
| 435 |
-
*html_updates,
|
| 436 |
-
*num_updates,
|
| 437 |
-
gr.update(visible=n_falhas > 0), # btn_aplicar_correcoes_geo
|
| 438 |
-
gr.update(visible=completo), # btn_usar_coords_geo
|
| 439 |
-
)
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
def confirmar_geocodificacao_callback(df_geo):
|
| 443 |
-
"""Opção 3: confirma uso das coords geocodificadas e libera Seção 2."""
|
| 444 |
-
if df_geo is None:
|
| 445 |
-
return (
|
| 446 |
-
gr.update(), gr.update(), gr.update(),
|
| 447 |
-
gr.update(), gr.update(), gr.update(), gr.update(), gr.update(),
|
| 448 |
-
"Sem dados geocodificados.",
|
| 449 |
-
)
|
| 450 |
-
mapa = criar_mapa(df_geo)
|
| 451 |
-
return _revelar_secao_2(df_geo, mapa) + ("Geocodificação concluída. Seção 2 liberada.",)
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
def confirmar_sem_coords_callback(df):
|
| 455 |
-
"""Prossegue para Seção 2 sem coordenadas completas (decisão explícita do usuário)."""
|
| 456 |
-
if df is None:
|
| 457 |
-
return (
|
| 458 |
-
gr.update(), gr.update(), gr.update(),
|
| 459 |
-
gr.update(), gr.update(), gr.update(), gr.update(), gr.update(),
|
| 460 |
-
"Sem dados carregados.",
|
| 461 |
-
)
|
| 462 |
-
mapa = criar_mapa(df)
|
| 463 |
-
return _revelar_secao_2(df, mapa) + ("Seção 2 liberada sem coordenadas completas.",)
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
# ============================================================
|
| 467 |
-
# INTERFACE GRADIO
|
| 468 |
-
# ============================================================
|
| 469 |
-
|
| 470 |
-
def criar_aba():
|
| 471 |
-
"""Cria conteúdo da aba de elaboração (sem wrapper gr.Blocks)."""
|
| 472 |
-
|
| 473 |
-
# Estados
|
| 474 |
-
estado_df = gr.State(None)
|
| 475 |
-
estado_modelo = gr.State(None)
|
| 476 |
-
estado_estatisticas = gr.State(None)
|
| 477 |
-
estado_outliers_anteriores = gr.State([]) # Lista de índices excluídos em iterações anteriores
|
| 478 |
-
estado_iteracao = gr.State(1) # Contador de iterações
|
| 479 |
-
estado_avaliacoes = gr.State([]) # Lista de avaliações acumuladas
|
| 480 |
-
|
| 481 |
-
# ========================================
|
| 482 |
-
# SEÇÃO 1: IMPORTAR DADOS (sempre visível e aberta)
|
| 483 |
-
# ========================================
|
| 484 |
-
# Estado para armazenar o arquivo temporariamente (usado quando há múltiplas abas)
|
| 485 |
-
estado_arquivo_temp = gr.State(None)
|
| 486 |
-
estado_flag_carregamento = gr.State(False)
|
| 487 |
-
estado_geo_temp = gr.State(None) # DataFrame durante geocodificação (antes de confirmar)
|
| 488 |
-
estado_df_falhas = gr.State(None) # df_falhas mais recente (geocodificação)
|
| 489 |
-
|
| 490 |
-
header_secao_1 = gr.HTML(criar_header_secao(1, "Importar Dados"))
|
| 491 |
-
with gr.Accordion(
|
| 492 |
-
label="▼ Mostrar / Ocultar",
|
| 493 |
-
open=True,
|
| 494 |
-
elem_classes="section-accordion"
|
| 495 |
-
) as accordion_secao_1:
|
| 496 |
-
with gr.Group(elem_classes="section-content") as conteudo_secao_1:
|
| 497 |
-
# Estado A: Upload (visível inicialmente)
|
| 498 |
-
with gr.Row(visible=True) as row_upload:
|
| 499 |
-
upload = gr.File(
|
| 500 |
-
label="Carregar arquivo (Excel, CSV ou Modelo .dai)",
|
| 501 |
-
scale=2
|
| 502 |
-
)
|
| 503 |
-
status = gr.Textbox(
|
| 504 |
-
label="Status",
|
| 505 |
-
interactive=False,
|
| 506 |
-
scale=2
|
| 507 |
-
)
|
| 508 |
-
|
| 509 |
-
# Estado B: Seleção de aba — apenas para Excel multi-aba (oculto inicialmente)
|
| 510 |
-
with gr.Group(visible=False) as row_selecao_aba:
|
| 511 |
-
with gr.Row():
|
| 512 |
-
dropdown_aba = gr.Dropdown(
|
| 513 |
-
label="Selecionar Aba",
|
| 514 |
-
choices=[],
|
| 515 |
-
interactive=True,
|
| 516 |
-
)
|
| 517 |
-
with gr.Row():
|
| 518 |
-
btn_confirmar_aba = gr.Button("Selecionar Aba", variant="primary")
|
| 519 |
-
|
| 520 |
-
# Estado C: Pós-carregamento (oculto inicialmente)
|
| 521 |
-
with gr.Row(visible=False) as row_pos_carga:
|
| 522 |
-
with gr.Column(scale=5):
|
| 523 |
-
html_nome_arquivo_carregado = gr.HTML(value="")
|
| 524 |
-
with gr.Column(scale=2):
|
| 525 |
-
btn_reiniciar_app = gr.Button(
|
| 526 |
-
"Reiniciar Aplicação",
|
| 527 |
-
variant="danger"
|
| 528 |
-
)
|
| 529 |
-
html_aviso_reiniciar = gr.HTML(value=(
|
| 530 |
-
'<div style="background-color:#fff3cd; border:1px solid #ffc107; '
|
| 531 |
-
'border-radius:8px; padding:8px 12px; margin-top:8px; font-size:1em;">'
|
| 532 |
-
'<strong>Atenção:</strong> Ao reiniciar, toda a aplicação MESA será '
|
| 533 |
-
'recarregada e <strong>o conteúdo de todas as abas será perdido</strong>. '
|
| 534 |
-
'Para iniciar uma nova sessão MESA sem perder o trabalho atual, '
|
| 535 |
-
'abra uma <strong>nova aba do navegador</strong>.'
|
| 536 |
-
'</div>'
|
| 537 |
-
))
|
| 538 |
-
|
| 539 |
-
# Estado D: Resolução de Coordenadas (oculto inicialmente)
|
| 540 |
-
with gr.Row(visible=False) as row_coords_panel:
|
| 541 |
-
with gr.Column():
|
| 542 |
-
html_aviso_coords = gr.HTML(value="")
|
| 543 |
-
|
| 544 |
-
# Tela de escolha (visível inicialmente)
|
| 545 |
-
with gr.Row() as row_escolha_opcao:
|
| 546 |
-
with gr.Column():
|
| 547 |
-
with gr.Row():
|
| 548 |
-
btn_escolher_mapear = gr.Button(
|
| 549 |
-
"Mapear colunas existentes para lat/lon",
|
| 550 |
-
variant="primary",
|
| 551 |
-
scale=10,
|
| 552 |
-
)
|
| 553 |
-
gr.HTML(
|
| 554 |
-
'<div style="align-self:center;text-align:center;'
|
| 555 |
-
'min-width:36px">'
|
| 556 |
-
'<span style="display:inline-block;font-size:0.75rem;'
|
| 557 |
-
'font-weight:600;color:#9ca3af;letter-spacing:0.05em;'
|
| 558 |
-
'text-transform:uppercase;background:#f3f4f6;'
|
| 559 |
-
'border-radius:999px;padding:3px 8px">ou</span>'
|
| 560 |
-
'</div>',
|
| 561 |
-
scale=1,
|
| 562 |
-
)
|
| 563 |
-
btn_escolher_geocodificar = gr.Button(
|
| 564 |
-
"Geocodificar automaticamente",
|
| 565 |
-
variant="primary",
|
| 566 |
-
scale=10,
|
| 567 |
-
)
|
| 568 |
-
gr.HTML(
|
| 569 |
-
'<div style="align-self:center;text-align:center;'
|
| 570 |
-
'min-width:36px">'
|
| 571 |
-
'<span style="display:inline-block;font-size:0.75rem;'
|
| 572 |
-
'font-weight:600;color:#9ca3af;letter-spacing:0.05em;'
|
| 573 |
-
'text-transform:uppercase;background:#f3f4f6;'
|
| 574 |
-
'border-radius:999px;padding:3px 8px">ou</span>'
|
| 575 |
-
'</div>',
|
| 576 |
-
scale=1,
|
| 577 |
-
)
|
| 578 |
-
btn_prosseguir_sem_coords = gr.Button(
|
| 579 |
-
"Prosseguir sem mapear coordenadas",
|
| 580 |
-
variant="primary",
|
| 581 |
-
scale=10,
|
| 582 |
-
)
|
| 583 |
-
with gr.Row(visible=False) as row_confirmacao_prosseguir:
|
| 584 |
-
with gr.Column():
|
| 585 |
-
gr.HTML(
|
| 586 |
-
'<div style="background:#fff3cd;border:1px solid #ffc107;'
|
| 587 |
-
'border-radius:8px;padding:8px 12px;margin-top:4px">'
|
| 588 |
-
'<strong>Atenção:</strong> Funcionalidades dependentes de '
|
| 589 |
-
'geolocalização (mapa, análises espaciais) não funcionarão '
|
| 590 |
-
'para registros sem coordenadas ou apresentarão resultados '
|
| 591 |
-
'incompletos. Deseja prosseguir mesmo assim?'
|
| 592 |
-
'</div>'
|
| 593 |
-
)
|
| 594 |
-
btn_confirmar_prosseguir = gr.Button(
|
| 595 |
-
"Confirmar — prosseguir mesmo assim",
|
| 596 |
-
variant="stop",
|
| 597 |
-
)
|
| 598 |
-
|
| 599 |
-
# Painel Mapear (oculto — botão Voltar no topo)
|
| 600 |
-
with gr.Row(visible=False) as row_opcao_mapear:
|
| 601 |
-
with gr.Column():
|
| 602 |
-
btn_voltar_mapear = gr.Button("← Voltar", variant="secondary", size="sm")
|
| 603 |
-
with gr.Row():
|
| 604 |
-
dropdown_col_lat_manual = gr.Dropdown(
|
| 605 |
-
label="Coluna → lat",
|
| 606 |
-
choices=[],
|
| 607 |
-
interactive=True,
|
| 608 |
-
)
|
| 609 |
-
dropdown_col_lon_manual = gr.Dropdown(
|
| 610 |
-
label="Coluna → lon",
|
| 611 |
-
choices=[],
|
| 612 |
-
interactive=True,
|
| 613 |
-
)
|
| 614 |
-
html_erro_mapeamento = gr.HTML(value="")
|
| 615 |
-
btn_confirmar_mapeamento = gr.Button(
|
| 616 |
-
"Confirmar mapeamento",
|
| 617 |
-
variant="primary",
|
| 618 |
-
)
|
| 619 |
-
|
| 620 |
-
# Painel Geocodificar (oculto — botão Voltar no topo)
|
| 621 |
-
with gr.Row(visible=False) as row_opcao_geocodificar:
|
| 622 |
-
with gr.Column():
|
| 623 |
-
btn_voltar_geocodificar = gr.Button("← Voltar", variant="secondary", size="sm")
|
| 624 |
-
with gr.Row():
|
| 625 |
-
dropdown_cdlog_geo = gr.Dropdown(
|
| 626 |
-
label="Coluna CDLOG (código do logradouro)",
|
| 627 |
-
choices=[],
|
| 628 |
-
interactive=True,
|
| 629 |
-
)
|
| 630 |
-
dropdown_num_geo = gr.Dropdown(
|
| 631 |
-
label="Coluna Número Predial",
|
| 632 |
-
choices=[],
|
| 633 |
-
interactive=True,
|
| 634 |
-
)
|
| 635 |
-
checkbox_auto_200 = gr.Checkbox(
|
| 636 |
-
label=(
|
| 637 |
-
"Permitir correção automática de ofício para números cuja "
|
| 638 |
-
"diferença do intervalo válido mais próximo seja ≤ 200"
|
| 639 |
-
),
|
| 640 |
-
value=True,
|
| 641 |
-
)
|
| 642 |
-
btn_geocodificar = gr.Button("Geocodificar", variant="primary")
|
| 643 |
-
html_geo_status = gr.HTML(value="<div style='min-height:40px'></div>")
|
| 644 |
-
|
| 645 |
-
# 20 slots de correção individual (ocultos inicialmente)
|
| 646 |
-
falha_rows = []
|
| 647 |
-
falha_htmls = []
|
| 648 |
-
falha_inputs = []
|
| 649 |
-
for i in range(MAX_FALHAS_GEO):
|
| 650 |
-
with gr.Row(visible=False) as _fr:
|
| 651 |
-
with gr.Column(scale=3):
|
| 652 |
-
_fh = gr.HTML(value="")
|
| 653 |
-
with gr.Column(scale=1):
|
| 654 |
-
_fi = gr.Number(label="Nº Corrigido", precision=0, value=None)
|
| 655 |
-
falha_rows.append(_fr)
|
| 656 |
-
falha_htmls.append(_fh)
|
| 657 |
-
falha_inputs.append(_fi)
|
| 658 |
-
|
| 659 |
-
btn_aplicar_correcoes_geo = gr.Button(
|
| 660 |
-
"Aplicar correções e re-geocodificar",
|
| 661 |
-
visible=False,
|
| 662 |
-
)
|
| 663 |
-
btn_usar_coords_geo = gr.Button(
|
| 664 |
-
"Confirmar",
|
| 665 |
-
variant="primary",
|
| 666 |
-
visible=False,
|
| 667 |
-
)
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
# ========================================
|
| 671 |
-
# SEÇÃO 2: VISUALIZAR DADOS (ativada ao carregar arquivo)
|
| 672 |
-
# ========================================
|
| 673 |
-
header_secao_2 = gr.HTML(criar_header_secao(2, "Visualizar Dados"), visible=True)
|
| 674 |
-
with gr.Accordion(
|
| 675 |
-
label="▼ Mostrar / Ocultar",
|
| 676 |
-
open=True,
|
| 677 |
-
visible=False,
|
| 678 |
-
elem_classes="section-accordion"
|
| 679 |
-
) as accordion_secao_2:
|
| 680 |
-
with gr.Group(elem_classes="section-content") as conteudo_secao_2:
|
| 681 |
-
with gr.Accordion("Outliers Excluídos", open=True, visible=False) as accordion_outliers_anteriores:
|
| 682 |
-
html_outliers_anteriores = gr.HTML(value="")
|
| 683 |
-
btn_limpar_historico = gr.Button("Limpar Histórico e Reiniciar do Zero", variant="secondary")
|
| 684 |
-
|
| 685 |
-
with gr.Accordion("Dados de Mercado", open=True):
|
| 686 |
-
tabela_dados = gr.Dataframe(
|
| 687 |
-
label="",
|
| 688 |
-
interactive=False,
|
| 689 |
-
max_height=400
|
| 690 |
-
)
|
| 691 |
-
with gr.Row():
|
| 692 |
-
btn_download_dados = gr.Button("Baixar Dados (CSV)", variant="secondary", size="sm")
|
| 693 |
-
download_dados_file = gr.File(label="", visible=False)
|
| 694 |
-
|
| 695 |
-
dropdown_mapa_var = gr.Dropdown(
|
| 696 |
-
label="Variável para dimensionar pontos no mapa",
|
| 697 |
-
choices=["Visualização Padrão"],
|
| 698 |
-
value="Visualização Padrão",
|
| 699 |
-
interactive=True,
|
| 700 |
-
allow_custom_value=False
|
| 701 |
-
)
|
| 702 |
-
|
| 703 |
-
with gr.Accordion("Mapa", open=True):
|
| 704 |
-
mapa_html = gr.HTML(
|
| 705 |
-
value="<p>Carregue dados para ver o mapa.</p>",
|
| 706 |
-
label=""
|
| 707 |
-
)
|
| 708 |
-
|
| 709 |
-
# ========================================
|
| 710 |
-
# SEÇÃO 3: SELECIONAR VARIÁVEL DEPENDENTE (ativada ao carregar arquivo)
|
| 711 |
-
# ========================================
|
| 712 |
-
header_secao_3 = gr.HTML(criar_header_secao(3, "Selecionar Variável Dependente"), visible=True)
|
| 713 |
-
with gr.Accordion(
|
| 714 |
-
label="▼ Mostrar / Ocultar",
|
| 715 |
-
open=True,
|
| 716 |
-
visible=False,
|
| 717 |
-
elem_classes="section-accordion"
|
| 718 |
-
) as accordion_secao_3:
|
| 719 |
-
with gr.Group(elem_classes="section-content") as conteudo_secao_3:
|
| 720 |
-
with gr.Row():
|
| 721 |
-
dropdown_y = gr.Dropdown(
|
| 722 |
-
label="Variável Dependente (y)",
|
| 723 |
-
choices=[],
|
| 724 |
-
interactive=True,
|
| 725 |
-
scale=2
|
| 726 |
-
)
|
| 727 |
-
with gr.Row():
|
| 728 |
-
btn_aplicar_y = gr.Button("Aplicar Seleção", variant="primary", scale=1)
|
| 729 |
-
|
| 730 |
-
# ========================================
|
| 731 |
-
# SEÇÃO 4: SELECIONAR VARIÁVEIS INDEPENDENTES (ativada ao selecionar Y)
|
| 732 |
-
# ========================================
|
| 733 |
-
header_secao_4 = gr.HTML(criar_header_secao(4, "Selecionar Variáveis Independentes"), visible=True)
|
| 734 |
-
with gr.Accordion(
|
| 735 |
-
label="▼ Mostrar / Ocultar",
|
| 736 |
-
open=True,
|
| 737 |
-
visible=False,
|
| 738 |
-
elem_classes="section-accordion"
|
| 739 |
-
) as accordion_secao_4:
|
| 740 |
-
with gr.Group(elem_classes="section-content") as conteudo_secao_4:
|
| 741 |
-
with gr.Row(elem_classes="checkbox-selecionar-todos"):
|
| 742 |
-
checkbox_selecionar_todos = gr.Checkbox(
|
| 743 |
-
label="Marcar ou desmarcar todas as variáveis",
|
| 744 |
-
value=True,
|
| 745 |
-
interactive=True
|
| 746 |
-
)
|
| 747 |
-
with gr.Row():
|
| 748 |
-
checkboxes_x = gr.CheckboxGroup(
|
| 749 |
-
label="Variáveis Independentes (X)",
|
| 750 |
-
choices=[],
|
| 751 |
-
interactive=True
|
| 752 |
-
)
|
| 753 |
-
with gr.Row():
|
| 754 |
-
checkboxes_dicotomicas = gr.CheckboxGroup(
|
| 755 |
-
label="Variáveis Dicotômicas (0/1)",
|
| 756 |
-
choices=[], value=[], visible=False, interactive=True,
|
| 757 |
-
info="Transformação fixa em (x). Avaliação: apenas 0 ou 1."
|
| 758 |
-
)
|
| 759 |
-
with gr.Row():
|
| 760 |
-
checkboxes_codigo_alocado = gr.CheckboxGroup(
|
| 761 |
-
label="Variáveis Categóricas Codificadas",
|
| 762 |
-
choices=[], value=[], visible=False, interactive=True,
|
| 763 |
-
info="Transformação livre. Avaliação: apenas inteiros no intervalo observado."
|
| 764 |
-
)
|
| 765 |
-
with gr.Row():
|
| 766 |
-
checkboxes_percentuais = gr.CheckboxGroup(
|
| 767 |
-
label="Variáveis Percentuais (0 a 1)",
|
| 768 |
-
choices=[], value=[], visible=False, interactive=True,
|
| 769 |
-
info="Transformação fixa em (x). Avaliação: apenas valores entre 0 e 1."
|
| 770 |
-
)
|
| 771 |
-
with gr.Row():
|
| 772 |
-
btn_aplicar_selecao_x = gr.Button("Aplicar Seleção", variant="primary", scale=1)
|
| 773 |
-
html_aviso_multicolinearidade = gr.HTML("", visible=False)
|
| 774 |
-
|
| 775 |
-
# Estados para outliers (usados na seção de Análise de Outliers após o modelo)
|
| 776 |
-
estado_metricas = gr.State(None)
|
| 777 |
-
estado_df_filtrado = gr.State(None)
|
| 778 |
-
|
| 779 |
-
# ========================================
|
| 780 |
-
# SEÇÃO 5: ESTATÍSTICAS DAS VARIÁVEIS SELECIONADAS (ativada por Aplicar Seleção)
|
| 781 |
-
# ========================================
|
| 782 |
-
header_secao_5 = gr.HTML(criar_header_secao(5, "Estat��sticas das Variáveis Selecionadas"), visible=True)
|
| 783 |
-
with gr.Accordion(
|
| 784 |
-
label="▼ Mostrar / Ocultar",
|
| 785 |
-
open=True,
|
| 786 |
-
visible=False,
|
| 787 |
-
elem_classes="section-accordion"
|
| 788 |
-
) as accordion_secao_5:
|
| 789 |
-
with gr.Group(elem_classes="section-content") as conteudo_secao_5:
|
| 790 |
-
tabela_estatisticas = gr.Dataframe(
|
| 791 |
-
label="",
|
| 792 |
-
interactive=False,
|
| 793 |
-
value=None,
|
| 794 |
-
wrap=True
|
| 795 |
-
)
|
| 796 |
-
with gr.Row():
|
| 797 |
-
btn_download_estatisticas = gr.Button("Baixar Estatísticas (CSV)", variant="secondary", size="sm")
|
| 798 |
-
download_estatisticas_file = gr.File(label="", visible=False)
|
| 799 |
-
|
| 800 |
-
# ========================================
|
| 801 |
-
# SEÇÃO 6: TESTE DE MICRONUMEROSIDADE (ativada por Aplicar Seleção)
|
| 802 |
-
# ========================================
|
| 803 |
-
header_secao_6 = gr.HTML(criar_header_secao(6, "Teste de Micronumerosidade"), visible=True)
|
| 804 |
-
with gr.Accordion(
|
| 805 |
-
label="▼ Mostrar / Ocultar",
|
| 806 |
-
open=True,
|
| 807 |
-
visible=False,
|
| 808 |
-
elem_classes="section-accordion"
|
| 809 |
-
) as accordion_secao_6:
|
| 810 |
-
with gr.Group(elem_classes="section-content") as conteudo_secao_6:
|
| 811 |
-
html_micronumerosidade = gr.HTML(value="")
|
| 812 |
-
|
| 813 |
-
# ========================================
|
| 814 |
-
# SEÇÃO 7: GRÁFICOS DE DISPERSÃO (ativada por Aplicar Seleção)
|
| 815 |
-
# ========================================
|
| 816 |
-
header_secao_7 = gr.HTML(criar_header_secao(7, "Gráficos de Dispersão das Variáveis Independentes"), visible=True)
|
| 817 |
-
with gr.Accordion(
|
| 818 |
-
label="▼ Mostrar / Ocultar",
|
| 819 |
-
open=True,
|
| 820 |
-
visible=False,
|
| 821 |
-
elem_classes="section-accordion"
|
| 822 |
-
) as accordion_secao_7:
|
| 823 |
-
with gr.Group(elem_classes="section-content") as conteudo_secao_7:
|
| 824 |
-
plot_dispersao = gr.Plot(label="")
|
| 825 |
-
|
| 826 |
-
# ========================================
|
| 827 |
-
# SEÇÃO 8: TRANSFORMAÇÕES SUGERIDAS (ativada por Aplicar Seleção)
|
| 828 |
-
# ========================================
|
| 829 |
-
# Estado para armazenar resultados da busca
|
| 830 |
-
estado_resultados_busca = gr.State([])
|
| 831 |
-
|
| 832 |
-
header_secao_8 = gr.HTML(criar_header_secao(8, "Transformações Sugeridas"), visible=True)
|
| 833 |
-
with gr.Accordion(
|
| 834 |
-
label="▼ Mostrar / Ocultar",
|
| 835 |
-
open=True,
|
| 836 |
-
visible=False,
|
| 837 |
-
elem_classes="section-accordion"
|
| 838 |
-
) as accordion_secao_8:
|
| 839 |
-
with gr.Group(elem_classes="section-content") as conteudo_secao_8:
|
| 840 |
-
with gr.Row():
|
| 841 |
-
slider_grau_coef = gr.Radio(
|
| 842 |
-
choices=[("Sem enquadramento", 0), ("Grau I (p≤30%)", 1), ("Grau II (p≤20%)", 2), ("Grau III (p≤10%)", 3)],
|
| 843 |
-
value=3,
|
| 844 |
-
label="Grau mínimo de significância dos coeficientes",
|
| 845 |
-
type="value"
|
| 846 |
-
)
|
| 847 |
-
slider_grau_f = gr.Radio(
|
| 848 |
-
choices=[("Sem enquadramento", 0), ("Grau I (α=5%)", 1), ("Grau II (α=2%)", 2), ("Grau III (α=1%)", 3)],
|
| 849 |
-
value=3,
|
| 850 |
-
label="Grau mínimo do Teste F",
|
| 851 |
-
type="value"
|
| 852 |
-
)
|
| 853 |
-
busca_html = gr.HTML(value="")
|
| 854 |
-
|
| 855 |
-
# ========================================
|
| 856 |
-
# SEÇÃO 9: APLICAÇÃO DAS TRANSFORMAÇÕES (ativada por Aplicar Seleção)
|
| 857 |
-
# ========================================
|
| 858 |
-
header_secao_9 = gr.HTML(criar_header_secao(9, "Aplicação das Transformações"), visible=True)
|
| 859 |
-
with gr.Accordion(
|
| 860 |
-
label="▼ Mostrar / Ocultar",
|
| 861 |
-
open=True,
|
| 862 |
-
visible=False,
|
| 863 |
-
elem_classes="section-accordion"
|
| 864 |
-
) as accordion_secao_9:
|
| 865 |
-
with gr.Group(elem_classes="section-content") as conteudo_secao_9:
|
| 866 |
-
# Área destacada para botões de adoção
|
| 867 |
-
gr.HTML("""
|
| 868 |
-
<div class="adotar-header">
|
| 869 |
-
<span class="adotar-titulo">Adote uma das otimizações de transformação sugeridas na seção anterior</span>
|
| 870 |
-
</div>
|
| 871 |
-
""")
|
| 872 |
-
with gr.Row(elem_classes="adotar-row"):
|
| 873 |
-
btn_adotar_1 = gr.Button("✓ Adotar #1", visible=False, elem_classes="btn-adotar", variant="primary")
|
| 874 |
-
btn_adotar_2 = gr.Button("✓ Adotar #2", visible=False, elem_classes="btn-adotar", variant="primary")
|
| 875 |
-
btn_adotar_3 = gr.Button("✓ Adotar #3", visible=False, elem_classes="btn-adotar", variant="primary")
|
| 876 |
-
btn_adotar_4 = gr.Button("✓ Adotar #4", visible=False, elem_classes="btn-adotar", variant="primary")
|
| 877 |
-
btn_adotar_5 = gr.Button("✓ Adotar #5", visible=False, elem_classes="btn-adotar", variant="primary")
|
| 878 |
-
|
| 879 |
-
gr.HTML("<div class='adotar-separator'><span>ou configure manualmente</span></div>")
|
| 880 |
-
|
| 881 |
-
gr.Markdown("*Selecione a transformação para cada variável (dicotômicas ficam fixas em (x))*", elem_classes="transf-instrucao")
|
| 882 |
-
|
| 883 |
-
# Cards: os 3 gr.Row são transparentes (display:contents via CSS) e fluem
|
| 884 |
-
# num único container flex (.transf-all-cards), garantindo distribuição uniforme
|
| 885 |
-
# 3 linhas × 8 = 24 slots; apenas os 20 primeiros são rastreados
|
| 886 |
-
# Card Y fica antes dos cards X (mesmo container flex)
|
| 887 |
-
transf_x_rows = []
|
| 888 |
-
transf_x_columns = []
|
| 889 |
-
transf_x_labels = []
|
| 890 |
-
transf_x_dropdowns = []
|
| 891 |
-
|
| 892 |
-
with gr.Column(elem_classes="transf-all-cards"):
|
| 893 |
-
# Card da variável dependente Y (borda azul, label dinâmico com nome + "(Y)")
|
| 894 |
-
with gr.Row(visible=False, elem_classes="transf-cards-row") as transf_y_row:
|
| 895 |
-
with gr.Column(scale=1, min_width=110, elem_classes="transf-card transf-card-y", visible=False) as transf_y_col:
|
| 896 |
-
transf_y_label = gr.HTML(value="", visible=False, elem_classes="transf-card-label")
|
| 897 |
-
transformacao_y = gr.Dropdown(
|
| 898 |
-
choices=TRANSFORMACOES,
|
| 899 |
-
value="(x)",
|
| 900 |
-
label="",
|
| 901 |
-
show_label=False,
|
| 902 |
-
interactive=True,
|
| 903 |
-
visible=True,
|
| 904 |
-
)
|
| 905 |
-
|
| 906 |
-
for i in range((MAX_VARS_X + 7) // 8): # 3 rows
|
| 907 |
-
row = gr.Row(visible=False, elem_classes="transf-cards-row")
|
| 908 |
-
with row:
|
| 909 |
-
for j in range(8):
|
| 910 |
-
k = i * 8 + j
|
| 911 |
-
col = gr.Column(scale=1, min_width=110, elem_classes="transf-card", visible=False)
|
| 912 |
-
with col:
|
| 913 |
-
label = gr.HTML(value="", visible=False, elem_classes="transf-card-label")
|
| 914 |
-
dropdown = gr.Dropdown(
|
| 915 |
-
choices=TRANSFORMACOES,
|
| 916 |
-
value="(x)",
|
| 917 |
-
label="",
|
| 918 |
-
show_label=False,
|
| 919 |
-
interactive=True,
|
| 920 |
-
visible=False,
|
| 921 |
-
)
|
| 922 |
-
if k < MAX_VARS_X:
|
| 923 |
-
transf_x_columns.append(col)
|
| 924 |
-
transf_x_labels.append(label)
|
| 925 |
-
transf_x_dropdowns.append(dropdown)
|
| 926 |
-
transf_x_rows.append(row)
|
| 927 |
-
|
| 928 |
-
with gr.Row():
|
| 929 |
-
btn_ajustar = gr.Button("Aplicar transformações e ajustar modelo", variant="primary", scale=1)
|
| 930 |
-
|
| 931 |
-
# ========================================
|
| 932 |
-
# SEÇÃO 10: GRÁFICOS DE DISPERSÃO (VARIÁVEIS TRANSFORMADAS) (ativada por Ajustar Modelo)
|
| 933 |
-
# ========================================
|
| 934 |
-
header_secao_10 = gr.HTML(criar_header_secao(10, "Gráficos de Dispersão (Variáveis Transformadas)"), visible=True)
|
| 935 |
-
with gr.Accordion(
|
| 936 |
-
label="▼ Mostrar / Ocultar",
|
| 937 |
-
open=True,
|
| 938 |
-
visible=False,
|
| 939 |
-
elem_classes="section-accordion"
|
| 940 |
-
) as accordion_secao_10:
|
| 941 |
-
with gr.Column(elem_classes="section-content") as conteudo_secao_10:
|
| 942 |
-
dropdown_tipo_grafico_dispersao = gr.Dropdown(
|
| 943 |
-
choices=[
|
| 944 |
-
"Variáveis Independentes Transformadas X Variável Dependente Transformada",
|
| 945 |
-
"Variáveis Independentes Transformadas X Resíduo Padronizado"
|
| 946 |
-
],
|
| 947 |
-
value="Variáveis Independentes Transformadas X Variável Dependente Transformada",
|
| 948 |
-
label="Tipo de Gráfico",
|
| 949 |
-
interactive=True,
|
| 950 |
-
visible=True,
|
| 951 |
-
elem_id="dropdown_dispersao"
|
| 952 |
-
)
|
| 953 |
-
plot_dispersao_transf = gr.Plot(label="", visible=True)
|
| 954 |
-
|
| 955 |
-
# ========================================
|
| 956 |
-
# SEÇÃO 11: DIAGNÓSTICO DE MODELO (ativada por Ajustar Modelo)
|
| 957 |
-
# ========================================
|
| 958 |
-
header_secao_11 = gr.HTML(criar_header_secao(11, "Diagnóstico de Modelo"), visible=True)
|
| 959 |
-
with gr.Accordion(
|
| 960 |
-
label="▼ Mostrar / Ocultar",
|
| 961 |
-
open=True,
|
| 962 |
-
visible=False,
|
| 963 |
-
elem_classes="section-accordion"
|
| 964 |
-
) as accordion_secao_11:
|
| 965 |
-
with gr.Group(elem_classes="section-content") as conteudo_secao_11:
|
| 966 |
-
diagnosticos_html = gr.HTML(value="")
|
| 967 |
-
|
| 968 |
-
with gr.Row():
|
| 969 |
-
with gr.Accordion("Tabela de Coeficientes", open=True):
|
| 970 |
-
tabela_coef = gr.Dataframe(
|
| 971 |
-
label="",
|
| 972 |
-
interactive=False,
|
| 973 |
-
max_height=400
|
| 974 |
-
)
|
| 975 |
-
with gr.Row():
|
| 976 |
-
btn_download_coef = gr.Button("Baixar Coeficientes (CSV)", variant="secondary", size="sm")
|
| 977 |
-
download_coef_file = gr.File(label="", visible=False)
|
| 978 |
-
|
| 979 |
-
with gr.Accordion("Valores Observados vs Calculados", open=True):
|
| 980 |
-
tabela_obs_calc = gr.Dataframe(
|
| 981 |
-
label="",
|
| 982 |
-
interactive=False,
|
| 983 |
-
max_height=400
|
| 984 |
-
)
|
| 985 |
-
with gr.Row():
|
| 986 |
-
btn_download_obs_calc = gr.Button("Baixar Obs vs Calc (CSV)", variant="secondary", size="sm")
|
| 987 |
-
download_obs_calc_file = gr.File(label="", visible=False)
|
| 988 |
-
|
| 989 |
-
# ========================================
|
| 990 |
-
# SEÇÃO 12: GRÁFICOS DE DIAGNÓSTICO DO MODELO (ativada por Ajustar Modelo)
|
| 991 |
-
# ========================================
|
| 992 |
-
header_secao_12 = gr.HTML(criar_header_secao(12, "Gráficos de Diagnóstico do Modelo"), visible=True)
|
| 993 |
-
with gr.Accordion(
|
| 994 |
-
label="▼ Mostrar / Ocultar",
|
| 995 |
-
open=True,
|
| 996 |
-
visible=False,
|
| 997 |
-
elem_classes="section-accordion"
|
| 998 |
-
) as accordion_secao_12:
|
| 999 |
-
with gr.Group(elem_classes="section-content") as conteudo_secao_12:
|
| 1000 |
-
with gr.Row():
|
| 1001 |
-
plot_obs_calc = gr.Plot(label="Observados vs Calculados")
|
| 1002 |
-
plot_residuos = gr.Plot(label="Resíduos")
|
| 1003 |
-
|
| 1004 |
-
with gr.Row():
|
| 1005 |
-
plot_hist = gr.Plot(label="Histograma dos Resíduos")
|
| 1006 |
-
plot_cook = gr.Plot(label="Distância de Cook")
|
| 1007 |
-
|
| 1008 |
-
with gr.Row():
|
| 1009 |
-
plot_corr = gr.Plot(label="Matriz de Correlação")
|
| 1010 |
-
|
| 1011 |
-
# ========================================
|
| 1012 |
-
# SEÇÃO 13: ANALISAR OUTLIERS (ativada por Ajustar Modelo)
|
| 1013 |
-
# ========================================
|
| 1014 |
-
# Estado para armazenar o número de filtros visíveis
|
| 1015 |
-
estado_n_filtros = gr.State(2) # Começa com 2 filtros padrão
|
| 1016 |
-
|
| 1017 |
-
# Opções de operadores
|
| 1018 |
-
OPERADORES = ["<=", ">=", "<", ">", "="]
|
| 1019 |
-
# Opções base de variáveis (serão atualizadas dinamicamente)
|
| 1020 |
-
VARIAVEIS_BASE = ["Resíduo Pad.", "Resíduo Stud.", "Cook"]
|
| 1021 |
-
|
| 1022 |
-
header_secao_13 = gr.HTML(criar_header_secao(13, "Analisar Outliers"), visible=True)
|
| 1023 |
-
with gr.Accordion(
|
| 1024 |
-
label="▼ Mostrar / Ocultar",
|
| 1025 |
-
open=True,
|
| 1026 |
-
visible=False,
|
| 1027 |
-
elem_classes="section-accordion"
|
| 1028 |
-
) as accordion_secao_13:
|
| 1029 |
-
with gr.Group(elem_classes="section-content") as conteudo_secao_13:
|
| 1030 |
-
gr.HTML('<div class="outlier-subheader">Métricas de Outliers</div>')
|
| 1031 |
-
gr.HTML('<div class="outlier-dica">Métricas calculadas com base no modelo ajustado (resíduos com transformações aplicadas)</div>')
|
| 1032 |
-
|
| 1033 |
-
tabela_metricas = gr.Dataframe(
|
| 1034 |
-
label="Métricas para identificação de outliers",
|
| 1035 |
-
interactive=False,
|
| 1036 |
-
max_height=300
|
| 1037 |
-
)
|
| 1038 |
-
with gr.Row():
|
| 1039 |
-
btn_download_metricas = gr.Button("Baixar Métricas (CSV)", variant="secondary", size="sm")
|
| 1040 |
-
download_metricas_file = gr.File(label="", visible=False)
|
| 1041 |
-
|
| 1042 |
-
# ========================================
|
| 1043 |
-
# SEÇÃO 14: EXCLUSÃO DE OUTLIERS (ativada por Ajustar Modelo)
|
| 1044 |
-
# ========================================
|
| 1045 |
-
header_secao_14 = gr.HTML(criar_header_secao(14, "Exclusão ou Reinclusão de Outliers"), visible=True)
|
| 1046 |
-
with gr.Accordion(
|
| 1047 |
-
label="▼ Mostrar / Ocultar",
|
| 1048 |
-
open=True,
|
| 1049 |
-
visible=False,
|
| 1050 |
-
elem_classes="section-accordion"
|
| 1051 |
-
) as accordion_secao_14:
|
| 1052 |
-
with gr.Group(elem_classes="section-content") as conteudo_secao_14:
|
| 1053 |
-
html_outliers_sec14 = gr.HTML(value="")
|
| 1054 |
-
gr.HTML('<div class="outlier-subheader">Filtrar Outliers</div>')
|
| 1055 |
-
gr.HTML('<div class="outlier-dica">Outliers = linhas que satisfazem QUALQUER filtro (lógica OR / União)</div>')
|
| 1056 |
-
|
| 1057 |
-
# Filtros dinâmicos (máximo 4 filtros)
|
| 1058 |
-
filtro_rows = []
|
| 1059 |
-
filtro_vars = []
|
| 1060 |
-
filtro_ops = []
|
| 1061 |
-
filtro_vals = []
|
| 1062 |
-
|
| 1063 |
-
for i in range(4):
|
| 1064 |
-
visible = i < 2 # Filtros 1 e 2 visíveis por padrão
|
| 1065 |
-
# Só define valores padrão para os filtros visíveis (0 e 1)
|
| 1066 |
-
# Filtros ocultos (2 e 3) começam com None para não serem aplicados
|
| 1067 |
-
if i == 0:
|
| 1068 |
-
valor_padrao = -2.0
|
| 1069 |
-
operador_padrao = "<="
|
| 1070 |
-
var_padrao = "Resíduo Pad."
|
| 1071 |
-
elif i == 1:
|
| 1072 |
-
valor_padrao = 2.0
|
| 1073 |
-
operador_padrao = ">="
|
| 1074 |
-
var_padrao = "Resíduo Pad."
|
| 1075 |
-
else:
|
| 1076 |
-
valor_padrao = None
|
| 1077 |
-
operador_padrao = None
|
| 1078 |
-
var_padrao = None
|
| 1079 |
-
|
| 1080 |
-
with gr.Row(visible=visible, elem_classes="filtro-row") as row:
|
| 1081 |
-
var_dropdown = gr.Dropdown(
|
| 1082 |
-
label=f"Variável {i+1}",
|
| 1083 |
-
choices=VARIAVEIS_BASE,
|
| 1084 |
-
value=var_padrao,
|
| 1085 |
-
scale=2
|
| 1086 |
-
)
|
| 1087 |
-
op_dropdown = gr.Dropdown(
|
| 1088 |
-
label="Operador",
|
| 1089 |
-
choices=OPERADORES,
|
| 1090 |
-
value=operador_padrao,
|
| 1091 |
-
scale=1
|
| 1092 |
-
)
|
| 1093 |
-
val_input = gr.Number(
|
| 1094 |
-
label="Valor",
|
| 1095 |
-
value=valor_padrao,
|
| 1096 |
-
scale=1
|
| 1097 |
-
)
|
| 1098 |
-
|
| 1099 |
-
filtro_rows.append(row)
|
| 1100 |
-
filtro_vars.append(var_dropdown)
|
| 1101 |
-
filtro_ops.append(op_dropdown)
|
| 1102 |
-
filtro_vals.append(val_input)
|
| 1103 |
-
|
| 1104 |
-
with gr.Row(elem_classes="btn-filtro-acao-row"):
|
| 1105 |
-
btn_adicionar_filtro = gr.Button("+", variant="secondary", scale=0, min_width=50, elem_classes="btn-filtro-acao btn-adicionar-filtro")
|
| 1106 |
-
btn_remover_ultimo = gr.Button("−", variant="secondary", scale=0, min_width=50, elem_classes="btn-filtro-acao btn-remover-filtro")
|
| 1107 |
-
btn_resetar_filtros = gr.Button("↺", variant="secondary", scale=0, min_width=50, elem_classes="btn-filtro-acao btn-voltar-padrao")
|
| 1108 |
-
|
| 1109 |
-
with gr.Row():
|
| 1110 |
-
btn_aplicar_filtro = gr.Button("Aplicar Filtros", variant="primary", scale=1)
|
| 1111 |
-
|
| 1112 |
-
gr.HTML('<div class="outlier-divider"><span class="arrow">▼</span></div>')
|
| 1113 |
-
gr.HTML('<div class="outlier-subheader">Confirmar Filtros Selecionados ou Ajustar Manualmente</div>')
|
| 1114 |
-
gr.HTML('<div class="outlier-dica">Edite os índices manualmente ou confirme os resultados dos filtros acima</div>')
|
| 1115 |
-
|
| 1116 |
-
with gr.Row():
|
| 1117 |
-
outliers_texto = gr.Textbox(
|
| 1118 |
-
label="Índices a Excluir",
|
| 1119 |
-
placeholder="Ex: 5, 12, 23",
|
| 1120 |
-
scale=3
|
| 1121 |
-
)
|
| 1122 |
-
reincluir_texto = gr.Textbox(
|
| 1123 |
-
label="Índices a Reincluir",
|
| 1124 |
-
placeholder="Ex: 5, 12",
|
| 1125 |
-
scale=3
|
| 1126 |
-
)
|
| 1127 |
-
|
| 1128 |
-
with gr.Row():
|
| 1129 |
-
txt_resumo_outliers = gr.Textbox(
|
| 1130 |
-
label="Resumo",
|
| 1131 |
-
value="Excluídos: 0 | A excluir: 0 | A reincluir: 0 | Total: 0",
|
| 1132 |
-
interactive=False,
|
| 1133 |
-
elem_classes="resumo-outliers"
|
| 1134 |
-
)
|
| 1135 |
-
|
| 1136 |
-
with gr.Row():
|
| 1137 |
-
btn_reiniciar_iteracao = gr.Button(
|
| 1138 |
-
"Atualizar Modelo (Excluir/Reincluir Outliers)",
|
| 1139 |
-
variant="primary",
|
| 1140 |
-
scale=2,
|
| 1141 |
-
elem_classes="btn-reiniciar-iteracao"
|
| 1142 |
-
)
|
| 1143 |
-
btn_download_base = gr.Button("Baixar Base Tratada (CSV)", variant="secondary", scale=1)
|
| 1144 |
-
download_base_file = gr.File(label="", visible=False)
|
| 1145 |
-
|
| 1146 |
-
# ========================================
|
| 1147 |
-
# SEÇÃO 15: AVALIAÇÃO DE IMÓVEL (ativada por Ajustar Modelo)
|
| 1148 |
-
# ========================================
|
| 1149 |
-
N_COLS_AVAL = 4
|
| 1150 |
-
N_ROWS_AVAL = MAX_VARS_X // N_COLS_AVAL # 5
|
| 1151 |
-
|
| 1152 |
-
header_secao_15 = gr.HTML(criar_header_secao(15, "Avaliação de Imóvel"), visible=True)
|
| 1153 |
-
with gr.Accordion(
|
| 1154 |
-
label="▼ Mostrar / Ocultar",
|
| 1155 |
-
open=True,
|
| 1156 |
-
visible=False,
|
| 1157 |
-
elem_classes="section-accordion"
|
| 1158 |
-
) as accordion_secao_15:
|
| 1159 |
-
with gr.Group(elem_classes="section-content"):
|
| 1160 |
-
# Grid de inputs (5 rows × 4 cols = 20 inputs pré-criados) — visual de cards
|
| 1161 |
-
aval_rows = []
|
| 1162 |
-
aval_inputs = []
|
| 1163 |
-
with gr.Column(elem_classes="aval-all-cards"):
|
| 1164 |
-
for i in range(N_ROWS_AVAL):
|
| 1165 |
-
with gr.Row(visible=False, elem_classes="aval-cards-row") as aval_row:
|
| 1166 |
-
for j in range(N_COLS_AVAL):
|
| 1167 |
-
inp = gr.Number(label="", visible=False, interactive=True, elem_classes="aval-card")
|
| 1168 |
-
aval_inputs.append(inp)
|
| 1169 |
-
aval_rows.append(aval_row)
|
| 1170 |
-
|
| 1171 |
-
with gr.Row():
|
| 1172 |
-
btn_calcular_avaliacao = gr.Button("Calcular Avaliação", variant="primary", scale=2)
|
| 1173 |
-
btn_limpar_avaliacoes = gr.Button("Limpar Avaliações", variant="secondary", scale=1)
|
| 1174 |
-
with gr.Row():
|
| 1175 |
-
dropdown_base_avaliacao = gr.Dropdown(
|
| 1176 |
-
label="Base p/ comparação",
|
| 1177 |
-
choices=[],
|
| 1178 |
-
value=None,
|
| 1179 |
-
interactive=True,
|
| 1180 |
-
scale=1,
|
| 1181 |
-
)
|
| 1182 |
-
|
| 1183 |
-
resultado_avaliacao_html = gr.HTML("")
|
| 1184 |
-
excluir_aval_trigger = gr.Textbox(
|
| 1185 |
-
label="", elem_id="excluir-aval-elab", container=False,
|
| 1186 |
-
elem_classes="trigger-hidden"
|
| 1187 |
-
)
|
| 1188 |
-
|
| 1189 |
-
with gr.Row():
|
| 1190 |
-
btn_exportar_avaliacoes = gr.Button("Salvar Avaliações em Excel", variant="secondary")
|
| 1191 |
-
download_avaliacoes_file = gr.File(label="", visible=False)
|
| 1192 |
-
|
| 1193 |
-
# ========================================
|
| 1194 |
-
# SEÇÃO 16: EXPORTAR MODELO (ativada por Ajustar Modelo)
|
| 1195 |
-
# ========================================
|
| 1196 |
-
header_secao_16 = gr.HTML(criar_header_secao(16, "Exportar Modelo"), visible=True)
|
| 1197 |
-
with gr.Accordion(
|
| 1198 |
-
label="▼ Mostrar / Ocultar",
|
| 1199 |
-
open=True,
|
| 1200 |
-
visible=False,
|
| 1201 |
-
elem_classes="section-accordion"
|
| 1202 |
-
) as accordion_secao_16:
|
| 1203 |
-
with gr.Group(elem_classes="section-content") as conteudo_secao_16:
|
| 1204 |
-
with gr.Row():
|
| 1205 |
-
nome_arquivo = gr.Textbox(
|
| 1206 |
-
label="Nome do arquivo",
|
| 1207 |
-
placeholder="modelo_01",
|
| 1208 |
-
scale=2
|
| 1209 |
-
)
|
| 1210 |
-
dropdown_elaborador = gr.Dropdown(
|
| 1211 |
-
label="Elaborador",
|
| 1212 |
-
choices=_avaliadores_nomes,
|
| 1213 |
-
value=None,
|
| 1214 |
-
scale=2
|
| 1215 |
-
)
|
| 1216 |
-
btn_exportar = gr.Button("Exportar .dai", variant="primary", scale=1)
|
| 1217 |
-
|
| 1218 |
-
status_exportar = gr.Textbox(
|
| 1219 |
-
label="Status da exportação",
|
| 1220 |
-
interactive=False
|
| 1221 |
-
)
|
| 1222 |
-
|
| 1223 |
-
download_modelo_file = gr.File(
|
| 1224 |
-
label="",
|
| 1225 |
-
visible=False
|
| 1226 |
-
)
|
| 1227 |
-
|
| 1228 |
-
# ========================================
|
| 1229 |
-
# EVENTOS
|
| 1230 |
-
# ========================================
|
| 1231 |
-
|
| 1232 |
-
# --- Output lists compartilhadas ---
|
| 1233 |
-
|
| 1234 |
-
# CONTRACT: 196 itens — usada por upload.upload e btn_confirmar_aba.click
|
| 1235 |
-
# Se alterar, atualizar: carregamento.py (5 funções retornam 196 itens)
|
| 1236 |
-
_outputs_carregar = [
|
| 1237 |
-
estado_df,
|
| 1238 |
-
status,
|
| 1239 |
-
dropdown_aba,
|
| 1240 |
-
dropdown_y,
|
| 1241 |
-
tabela_dados,
|
| 1242 |
-
tabela_estatisticas,
|
| 1243 |
-
checkboxes_x,
|
| 1244 |
-
mapa_html,
|
| 1245 |
-
estado_df_filtrado,
|
| 1246 |
-
estado_outliers_anteriores,
|
| 1247 |
-
estado_iteracao,
|
| 1248 |
-
accordion_outliers_anteriores,
|
| 1249 |
-
html_outliers_anteriores,
|
| 1250 |
-
estado_arquivo_temp,
|
| 1251 |
-
# Seções 2 e 3
|
| 1252 |
-
header_secao_2,
|
| 1253 |
-
accordion_secao_2,
|
| 1254 |
-
dropdown_mapa_var,
|
| 1255 |
-
header_secao_3,
|
| 1256 |
-
accordion_secao_3,
|
| 1257 |
-
# Reset seções 4-16 (states)
|
| 1258 |
-
estado_modelo, estado_metricas, estado_resultados_busca, estado_avaliacoes,
|
| 1259 |
-
# Headers, accordions e conteúdo das seções 4-16
|
| 1260 |
-
header_secao_4, accordion_secao_4, checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais,
|
| 1261 |
-
html_aviso_multicolinearidade,
|
| 1262 |
-
header_secao_5, accordion_secao_5,
|
| 1263 |
-
header_secao_6, accordion_secao_6, html_micronumerosidade,
|
| 1264 |
-
header_secao_7, accordion_secao_7, plot_dispersao,
|
| 1265 |
-
header_secao_8, accordion_secao_8, slider_grau_coef, slider_grau_f, busca_html,
|
| 1266 |
-
header_secao_9, accordion_secao_9, transformacao_y,
|
| 1267 |
-
btn_adotar_1, btn_adotar_2, btn_adotar_3, btn_adotar_4, btn_adotar_5,
|
| 1268 |
-
] + [transf_y_row, transf_y_col, transf_y_label] + transf_x_rows + transf_x_columns + transf_x_labels + transf_x_dropdowns + [
|
| 1269 |
-
header_secao_10, accordion_secao_10, dropdown_tipo_grafico_dispersao, plot_dispersao_transf,
|
| 1270 |
-
header_secao_11, accordion_secao_11, diagnosticos_html, tabela_coef, tabela_obs_calc,
|
| 1271 |
-
header_secao_12, accordion_secao_12, plot_obs_calc, plot_residuos, plot_hist, plot_cook, plot_corr,
|
| 1272 |
-
header_secao_13, accordion_secao_13, tabela_metricas,
|
| 1273 |
-
header_secao_14, accordion_secao_14, html_outliers_sec14, outliers_texto, reincluir_texto, txt_resumo_outliers,
|
| 1274 |
-
# Seção 15: Avaliação de Imóvel
|
| 1275 |
-
header_secao_15, accordion_secao_15,
|
| 1276 |
-
] + aval_rows + aval_inputs + [
|
| 1277 |
-
resultado_avaliacao_html, dropdown_base_avaliacao, excluir_aval_trigger, download_avaliacoes_file,
|
| 1278 |
-
# Seção 16: Exportar Modelo
|
| 1279 |
-
header_secao_16, accordion_secao_16, nome_arquivo, status_exportar,
|
| 1280 |
-
# Controles de visibilidade da seção 1 (pós-carregamento)
|
| 1281 |
-
row_upload, row_selecao_aba, row_pos_carga, html_nome_arquivo_carregado,
|
| 1282 |
-
# Flag para evitar que dropdown_y.change() sobrescreva valores durante carregamento
|
| 1283 |
-
estado_flag_carregamento,
|
| 1284 |
-
] + filtro_vars + [ # 4 dropdowns de variável dos filtros (choices atualizados no fluxo .dai)
|
| 1285 |
-
# Painel de coordenadas (Seção 1, Estado D) — itens 184-192
|
| 1286 |
-
row_coords_panel, # 184
|
| 1287 |
-
html_aviso_coords, # 185
|
| 1288 |
-
dropdown_col_lat_manual, # 186
|
| 1289 |
-
dropdown_col_lon_manual, # 187
|
| 1290 |
-
dropdown_cdlog_geo, # 188
|
| 1291 |
-
dropdown_num_geo, # 189
|
| 1292 |
-
row_escolha_opcao, # 190
|
| 1293 |
-
row_opcao_mapear, # 191
|
| 1294 |
-
row_opcao_geocodificar, # 192
|
| 1295 |
-
]
|
| 1296 |
-
|
| 1297 |
-
# CONTRACT: 33 itens — usada por btn_ajustar.click e btn_reiniciar_iteracao.then
|
| 1298 |
-
# Se alterar, atualizar: modelo.py (ajustar_modelo_callback retorna 33 itens)
|
| 1299 |
-
_outputs_ajustar = [
|
| 1300 |
-
estado_modelo,
|
| 1301 |
-
diagnosticos_html,
|
| 1302 |
-
tabela_coef,
|
| 1303 |
-
tabela_obs_calc,
|
| 1304 |
-
plot_dispersao_transf,
|
| 1305 |
-
dropdown_tipo_grafico_dispersao,
|
| 1306 |
-
plot_obs_calc,
|
| 1307 |
-
plot_residuos,
|
| 1308 |
-
plot_hist,
|
| 1309 |
-
plot_cook,
|
| 1310 |
-
plot_corr,
|
| 1311 |
-
tabela_metricas,
|
| 1312 |
-
estado_metricas,
|
| 1313 |
-
txt_resumo_outliers,
|
| 1314 |
-
estado_avaliacoes,
|
| 1315 |
-
# Seções 10-16
|
| 1316 |
-
header_secao_10, accordion_secao_10,
|
| 1317 |
-
header_secao_11, accordion_secao_11,
|
| 1318 |
-
header_secao_12, accordion_secao_12,
|
| 1319 |
-
header_secao_13, accordion_secao_13,
|
| 1320 |
-
header_secao_14, accordion_secao_14,
|
| 1321 |
-
header_secao_15, accordion_secao_15,
|
| 1322 |
-
header_secao_16, accordion_secao_16,
|
| 1323 |
-
] + filtro_vars
|
| 1324 |
-
|
| 1325 |
-
# CONTRACT: 27 itens — usada por .then() após ajustar/reiniciar para popular campos de avaliação
|
| 1326 |
-
_outputs_popular_avaliacao = aval_rows + aval_inputs + [resultado_avaliacao_html, dropdown_base_avaliacao]
|
| 1327 |
-
|
| 1328 |
-
# --- Inputs compartilhados para ajustar modelo ---
|
| 1329 |
-
_inputs_ajustar = [
|
| 1330 |
-
estado_df_filtrado, estado_df, dropdown_y, checkboxes_x,
|
| 1331 |
-
transformacao_y, estado_outliers_anteriores, checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais
|
| 1332 |
-
] + transf_x_dropdowns
|
| 1333 |
-
|
| 1334 |
-
# Upload de arquivo
|
| 1335 |
-
upload.upload(
|
| 1336 |
-
ao_carregar_arquivo,
|
| 1337 |
-
inputs=[upload],
|
| 1338 |
-
outputs=_outputs_carregar
|
| 1339 |
-
).then(
|
| 1340 |
-
popular_campos_avaliacao_callback,
|
| 1341 |
-
inputs=[estado_modelo, tabela_estatisticas],
|
| 1342 |
-
outputs=_outputs_popular_avaliacao
|
| 1343 |
-
).then(
|
| 1344 |
-
popular_dicotomicas_callback,
|
| 1345 |
-
inputs=[estado_modelo, checkboxes_x, estado_df],
|
| 1346 |
-
outputs=[checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais]
|
| 1347 |
-
)
|
| 1348 |
-
|
| 1349 |
-
# Confirmação de aba (para Excel multi-aba)
|
| 1350 |
-
btn_confirmar_aba.click(
|
| 1351 |
-
confirmar_aba_callback,
|
| 1352 |
-
inputs=[estado_arquivo_temp, dropdown_aba],
|
| 1353 |
-
outputs=_outputs_carregar
|
| 1354 |
-
).then(
|
| 1355 |
-
popular_campos_avaliacao_callback,
|
| 1356 |
-
inputs=[estado_modelo, tabela_estatisticas],
|
| 1357 |
-
outputs=_outputs_popular_avaliacao
|
| 1358 |
-
).then(
|
| 1359 |
-
popular_dicotomicas_callback,
|
| 1360 |
-
inputs=[estado_modelo, checkboxes_x, estado_df],
|
| 1361 |
-
outputs=[checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais]
|
| 1362 |
-
)
|
| 1363 |
-
|
| 1364 |
-
# Reiniciar aplicação (reload da página)
|
| 1365 |
-
btn_reiniciar_app.click(
|
| 1366 |
-
fn=None,
|
| 1367 |
-
inputs=None,
|
| 1368 |
-
outputs=None,
|
| 1369 |
-
js="() => { window.location.reload(); }"
|
| 1370 |
-
)
|
| 1371 |
-
|
| 1372 |
-
# ---- Painel de coordenadas (Seção 1, Estado D) ----
|
| 1373 |
-
|
| 1374 |
-
# Tela de escolha → entrar no painel Mapear
|
| 1375 |
-
btn_escolher_mapear.click(
|
| 1376 |
-
fn=lambda: (gr.update(visible=False), gr.update(visible=True)),
|
| 1377 |
-
outputs=[row_escolha_opcao, row_opcao_mapear],
|
| 1378 |
-
)
|
| 1379 |
-
|
| 1380 |
-
# Tela de escolha → entrar no painel Geocodificar
|
| 1381 |
-
btn_escolher_geocodificar.click(
|
| 1382 |
-
fn=lambda: (gr.update(visible=False), gr.update(visible=True)),
|
| 1383 |
-
outputs=[row_escolha_opcao, row_opcao_geocodificar],
|
| 1384 |
-
)
|
| 1385 |
-
|
| 1386 |
-
# Voltar do painel Mapear (limpa erro, reseta confirmação, volta à escolha)
|
| 1387 |
-
btn_voltar_mapear.click(
|
| 1388 |
-
fn=lambda: (gr.update(visible=False), gr.update(visible=True), gr.update(value=""), gr.update(visible=False)),
|
| 1389 |
-
outputs=[row_opcao_mapear, row_escolha_opcao, html_erro_mapeamento, row_confirmacao_prosseguir],
|
| 1390 |
-
)
|
| 1391 |
-
|
| 1392 |
-
# Voltar do painel Geocodificar (reset completo do estado de geocodificação — 68 itens)
|
| 1393 |
-
N = MAX_FALHAS_GEO
|
| 1394 |
-
btn_voltar_geocodificar.click(
|
| 1395 |
-
fn=lambda: (
|
| 1396 |
-
None, None, "",
|
| 1397 |
-
*[gr.update(visible=False)] * N,
|
| 1398 |
-
*[gr.update(value="")] * N,
|
| 1399 |
-
*[gr.update(value=None)] * N,
|
| 1400 |
-
gr.update(visible=False), gr.update(visible=False),
|
| 1401 |
-
gr.update(visible=False),
|
| 1402 |
-
gr.update(visible=False), gr.update(visible=True),
|
| 1403 |
-
),
|
| 1404 |
-
outputs=(
|
| 1405 |
-
[estado_geo_temp, estado_df_falhas, html_geo_status]
|
| 1406 |
-
+ falha_rows + falha_htmls + falha_inputs
|
| 1407 |
-
+ [btn_aplicar_correcoes_geo, btn_usar_coords_geo,
|
| 1408 |
-
row_confirmacao_prosseguir,
|
| 1409 |
-
row_opcao_geocodificar, row_escolha_opcao]
|
| 1410 |
-
),
|
| 1411 |
-
)
|
| 1412 |
-
|
| 1413 |
-
# Opção 1: confirmar mapeamento manual de colunas
|
| 1414 |
-
btn_confirmar_mapeamento.click(
|
| 1415 |
-
fn=confirmar_mapeamento_callback,
|
| 1416 |
-
inputs=[estado_df, dropdown_col_lat_manual, dropdown_col_lon_manual],
|
| 1417 |
-
outputs=[
|
| 1418 |
-
html_erro_mapeamento,
|
| 1419 |
-
estado_df, estado_df_filtrado, mapa_html,
|
| 1420 |
-
row_coords_panel, header_secao_2, accordion_secao_2,
|
| 1421 |
-
header_secao_3, accordion_secao_3, status,
|
| 1422 |
-
]
|
| 1423 |
-
)
|
| 1424 |
-
|
| 1425 |
-
# Opção 3: geocodificar por eixos (CONTRACT: 67 itens)
|
| 1426 |
-
_outputs_geo = (
|
| 1427 |
-
[estado_geo_temp, estado_df_falhas, html_geo_status]
|
| 1428 |
-
+ falha_rows + falha_htmls + falha_inputs
|
| 1429 |
-
+ [btn_aplicar_correcoes_geo, btn_usar_coords_geo]
|
| 1430 |
-
)
|
| 1431 |
-
|
| 1432 |
-
btn_geocodificar.click(
|
| 1433 |
-
fn=geocodificar_callback,
|
| 1434 |
-
inputs=[estado_df, dropdown_cdlog_geo, dropdown_num_geo, checkbox_auto_200],
|
| 1435 |
-
outputs=_outputs_geo,
|
| 1436 |
-
show_progress="full",
|
| 1437 |
-
)
|
| 1438 |
-
|
| 1439 |
-
btn_aplicar_correcoes_geo.click(
|
| 1440 |
-
fn=aplicar_correcoes_geo_callback,
|
| 1441 |
-
inputs=(
|
| 1442 |
-
[estado_geo_temp, estado_df_falhas]
|
| 1443 |
-
+ falha_inputs
|
| 1444 |
-
+ [dropdown_cdlog_geo, dropdown_num_geo, checkbox_auto_200]
|
| 1445 |
-
),
|
| 1446 |
-
outputs=_outputs_geo,
|
| 1447 |
-
show_progress="full",
|
| 1448 |
-
)
|
| 1449 |
-
|
| 1450 |
-
btn_usar_coords_geo.click(
|
| 1451 |
-
fn=confirmar_geocodificacao_callback,
|
| 1452 |
-
inputs=[estado_geo_temp],
|
| 1453 |
-
outputs=[
|
| 1454 |
-
estado_df, estado_df_filtrado, mapa_html,
|
| 1455 |
-
row_coords_panel, header_secao_2, accordion_secao_2,
|
| 1456 |
-
header_secao_3, accordion_secao_3, status,
|
| 1457 |
-
]
|
| 1458 |
-
)
|
| 1459 |
-
|
| 1460 |
-
# Prosseguir sem coordenadas completas (dois cliques: exibe aviso → confirma)
|
| 1461 |
-
btn_prosseguir_sem_coords.click(
|
| 1462 |
-
fn=lambda: gr.update(visible=True),
|
| 1463 |
-
outputs=[row_confirmacao_prosseguir]
|
| 1464 |
-
)
|
| 1465 |
-
|
| 1466 |
-
btn_confirmar_prosseguir.click(
|
| 1467 |
-
fn=lambda geo_df, orig_df: confirmar_sem_coords_callback(geo_df if geo_df is not None else orig_df),
|
| 1468 |
-
inputs=[estado_geo_temp, estado_df],
|
| 1469 |
-
outputs=[
|
| 1470 |
-
estado_df, estado_df_filtrado, mapa_html,
|
| 1471 |
-
row_coords_panel, header_secao_2, accordion_secao_2,
|
| 1472 |
-
header_secao_3, accordion_secao_3, status,
|
| 1473 |
-
]
|
| 1474 |
-
)
|
| 1475 |
-
|
| 1476 |
-
# Mudança de y (NÃO mostra seção 4, NÃO atualiza estatísticas - só atualiza checkboxes)
|
| 1477 |
-
dropdown_y.change(
|
| 1478 |
-
ao_mudar_y_sem_estatisticas,
|
| 1479 |
-
inputs=[estado_df, dropdown_y, estado_flag_carregamento],
|
| 1480 |
-
outputs=[checkboxes_x, header_secao_4, accordion_secao_4, estado_flag_carregamento]
|
| 1481 |
-
).then(
|
| 1482 |
-
popular_dicotomicas_callback,
|
| 1483 |
-
inputs=[estado_modelo, checkboxes_x, estado_df],
|
| 1484 |
-
outputs=[checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais]
|
| 1485 |
-
)
|
| 1486 |
-
|
| 1487 |
-
# Selecionar/Desselecionar todos via checkbox
|
| 1488 |
-
def toggle_selecionar_todos(selecionar, df, coluna_y):
|
| 1489 |
-
"""Marca ou desmarca todas as variáveis independentes."""
|
| 1490 |
-
if selecionar:
|
| 1491 |
-
# Marcar todos
|
| 1492 |
-
colunas_x = [col for col in obter_colunas_numericas(df) if col != coluna_y] if df is not None and coluna_y else []
|
| 1493 |
-
return gr.update(value=colunas_x)
|
| 1494 |
-
else:
|
| 1495 |
-
# Desmarcar todos
|
| 1496 |
-
return gr.update(value=[])
|
| 1497 |
-
|
| 1498 |
-
checkbox_selecionar_todos.change(
|
| 1499 |
-
toggle_selecionar_todos,
|
| 1500 |
-
inputs=[checkbox_selecionar_todos, estado_df, dropdown_y],
|
| 1501 |
-
outputs=[checkboxes_x]
|
| 1502 |
-
)
|
| 1503 |
-
|
| 1504 |
-
# Clique na tabela -> atualiza mapa
|
| 1505 |
-
tabela_dados.select(
|
| 1506 |
-
ao_clicar_tabela,
|
| 1507 |
-
inputs=[estado_df, dropdown_mapa_var],
|
| 1508 |
-
outputs=[mapa_html]
|
| 1509 |
-
)
|
| 1510 |
-
|
| 1511 |
-
# Mudança de variável no dropdown do mapa -> atualiza mapa
|
| 1512 |
-
dropdown_mapa_var.input(
|
| 1513 |
-
atualizar_mapa_callback,
|
| 1514 |
-
inputs=[estado_df_filtrado, estado_df, dropdown_mapa_var],
|
| 1515 |
-
outputs=[mapa_html]
|
| 1516 |
-
)
|
| 1517 |
-
|
| 1518 |
-
# Seleção de X -> atualiza campos de transformação (preview)
|
| 1519 |
-
checkboxes_x.change(
|
| 1520 |
-
_atualizar_campos_transformacoes_com_flag,
|
| 1521 |
-
inputs=[estado_df, checkboxes_x, estado_flag_carregamento, dropdown_y],
|
| 1522 |
-
outputs=[transf_y_row, transf_y_col, transf_y_label] + transf_x_rows + transf_x_columns + transf_x_labels + transf_x_dropdowns + [estado_flag_carregamento, checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais]
|
| 1523 |
-
)
|
| 1524 |
-
|
| 1525 |
-
# Mudança de qualquer checkbox de tipo -> atualiza interactive dos dropdowns de transformação
|
| 1526 |
-
_inputs_interativo = [checkboxes_x, checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais, estado_df]
|
| 1527 |
-
checkboxes_dicotomicas.change(
|
| 1528 |
-
atualizar_interativo_dicotomicas,
|
| 1529 |
-
inputs=_inputs_interativo,
|
| 1530 |
-
outputs=transf_x_dropdowns
|
| 1531 |
-
)
|
| 1532 |
-
checkboxes_codigo_alocado.change(
|
| 1533 |
-
atualizar_interativo_dicotomicas,
|
| 1534 |
-
inputs=_inputs_interativo,
|
| 1535 |
-
outputs=transf_x_dropdowns
|
| 1536 |
-
)
|
| 1537 |
-
checkboxes_percentuais.change(
|
| 1538 |
-
atualizar_interativo_dicotomicas,
|
| 1539 |
-
inputs=_inputs_interativo,
|
| 1540 |
-
outputs=transf_x_dropdowns
|
| 1541 |
-
)
|
| 1542 |
-
|
| 1543 |
-
# Aplicar seleção de Y -> atualiza X disponíveis e MOSTRA seção 4 (NÃO atualiza estatísticas)
|
| 1544 |
-
btn_aplicar_y.click(
|
| 1545 |
-
lambda df, y: ao_mudar_y_sem_estatisticas(df, y, mostrar_secao_x=True),
|
| 1546 |
-
inputs=[estado_df, dropdown_y],
|
| 1547 |
-
outputs=[checkboxes_x, header_secao_4, accordion_secao_4, estado_flag_carregamento]
|
| 1548 |
-
).then(
|
| 1549 |
-
popular_dicotomicas_callback,
|
| 1550 |
-
inputs=[estado_modelo, checkboxes_x, estado_df],
|
| 1551 |
-
outputs=[checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais]
|
| 1552 |
-
)
|
| 1553 |
-
|
| 1554 |
-
# Aplicar seleção de variáveis X -> atualiza estatísticas, busca transformações, dispersão e micronumerosidade
|
| 1555 |
-
btn_aplicar_selecao_x.click(
|
| 1556 |
-
aplicar_selecao_callback,
|
| 1557 |
-
inputs=[estado_df, dropdown_y, checkboxes_x, estado_outliers_anteriores, checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais],
|
| 1558 |
-
outputs=[
|
| 1559 |
-
estado_df_filtrado,
|
| 1560 |
-
tabela_estatisticas,
|
| 1561 |
-
html_micronumerosidade,
|
| 1562 |
-
plot_dispersao,
|
| 1563 |
-
slider_grau_coef, slider_grau_f,
|
| 1564 |
-
busca_html,
|
| 1565 |
-
estado_resultados_busca,
|
| 1566 |
-
btn_adotar_1, btn_adotar_2, btn_adotar_3, btn_adotar_4, btn_adotar_5,
|
| 1567 |
-
# Seções 5-9
|
| 1568 |
-
header_secao_5, accordion_secao_5,
|
| 1569 |
-
header_secao_6, accordion_secao_6,
|
| 1570 |
-
header_secao_7, accordion_secao_7,
|
| 1571 |
-
header_secao_8, accordion_secao_8,
|
| 1572 |
-
header_secao_9, accordion_secao_9,
|
| 1573 |
-
] + [transf_y_row, transf_y_col, transf_y_label] + transf_x_rows + transf_x_columns + transf_x_labels + transf_x_dropdowns + [html_aviso_multicolinearidade]
|
| 1574 |
-
)
|
| 1575 |
-
|
| 1576 |
-
# Re-executar busca ao alterar grau (modo manual, sem fallback)
|
| 1577 |
-
def _rebuscar_transformacoes(df, coluna_y, colunas_x, dicotomicas, codigo_alocado, percentuais, grau_coef, grau_f):
|
| 1578 |
-
if df is None or coluna_y is None or not colunas_x:
|
| 1579 |
-
return (gr.update(), gr.update(), *[gr.update()] * 5)
|
| 1580 |
-
html, resultados, _, *btn_updates = buscar_transformacoes_callback(
|
| 1581 |
-
df, coluna_y, colunas_x, dicotomicas, codigo_alocado, percentuais, int(grau_coef), int(grau_f)
|
| 1582 |
-
)
|
| 1583 |
-
return (html, resultados, *btn_updates)
|
| 1584 |
-
|
| 1585 |
-
_inputs_rebuscar = [estado_df_filtrado, dropdown_y, checkboxes_x, checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais, slider_grau_coef, slider_grau_f]
|
| 1586 |
-
_outputs_rebuscar = [busca_html, estado_resultados_busca,
|
| 1587 |
-
btn_adotar_1, btn_adotar_2, btn_adotar_3, btn_adotar_4, btn_adotar_5]
|
| 1588 |
-
|
| 1589 |
-
slider_grau_coef.change(
|
| 1590 |
-
_rebuscar_transformacoes,
|
| 1591 |
-
inputs=_inputs_rebuscar,
|
| 1592 |
-
outputs=_outputs_rebuscar
|
| 1593 |
-
)
|
| 1594 |
-
|
| 1595 |
-
slider_grau_f.change(
|
| 1596 |
-
_rebuscar_transformacoes,
|
| 1597 |
-
inputs=_inputs_rebuscar,
|
| 1598 |
-
outputs=_outputs_rebuscar
|
| 1599 |
-
)
|
| 1600 |
-
|
| 1601 |
-
# Aplicar filtros de outliers
|
| 1602 |
-
btn_aplicar_filtro.click(
|
| 1603 |
-
aplicar_filtros_callback,
|
| 1604 |
-
inputs=[estado_metricas, estado_n_filtros] + filtro_vars + filtro_ops + filtro_vals,
|
| 1605 |
-
outputs=[outliers_texto]
|
| 1606 |
-
)
|
| 1607 |
-
|
| 1608 |
-
# Adicionar filtro
|
| 1609 |
-
btn_adicionar_filtro.click(
|
| 1610 |
-
adicionar_filtro_callback,
|
| 1611 |
-
inputs=[estado_n_filtros],
|
| 1612 |
-
outputs=[estado_n_filtros] + filtro_rows
|
| 1613 |
-
)
|
| 1614 |
-
|
| 1615 |
-
# Remover último filtro
|
| 1616 |
-
btn_remover_ultimo.click(
|
| 1617 |
-
remover_ultimo_filtro_callback,
|
| 1618 |
-
inputs=[estado_n_filtros],
|
| 1619 |
-
outputs=[estado_n_filtros] + filtro_rows
|
| 1620 |
-
)
|
| 1621 |
-
|
| 1622 |
-
# Resetar filtros ao padrão
|
| 1623 |
-
btn_resetar_filtros.click(
|
| 1624 |
-
limpar_filtros_callback,
|
| 1625 |
-
inputs=[],
|
| 1626 |
-
outputs=[estado_n_filtros] + filtro_rows + filtro_vars + filtro_ops + filtro_vals + [outliers_texto]
|
| 1627 |
-
)
|
| 1628 |
-
|
| 1629 |
-
# Atualizar resumo de outliers quando usuário edita os campos
|
| 1630 |
-
outliers_texto.change(
|
| 1631 |
-
atualizar_resumo_outliers,
|
| 1632 |
-
inputs=[estado_outliers_anteriores, outliers_texto, reincluir_texto],
|
| 1633 |
-
outputs=[txt_resumo_outliers]
|
| 1634 |
-
)
|
| 1635 |
-
|
| 1636 |
-
reincluir_texto.change(
|
| 1637 |
-
atualizar_resumo_outliers,
|
| 1638 |
-
inputs=[estado_outliers_anteriores, outliers_texto, reincluir_texto],
|
| 1639 |
-
outputs=[txt_resumo_outliers]
|
| 1640 |
-
)
|
| 1641 |
-
|
| 1642 |
-
# Aplicar filtro também atualiza o resumo
|
| 1643 |
-
btn_aplicar_filtro.click(
|
| 1644 |
-
atualizar_resumo_outliers,
|
| 1645 |
-
inputs=[estado_outliers_anteriores, outliers_texto, reincluir_texto],
|
| 1646 |
-
outputs=[txt_resumo_outliers]
|
| 1647 |
-
)
|
| 1648 |
-
|
| 1649 |
-
# Download da base tratada (CSV)
|
| 1650 |
-
def download_base_callback(df_filtrado, df_original):
|
| 1651 |
-
"""Callback para download da base tratada."""
|
| 1652 |
-
# Usa df_filtrado se disponível, senão usa df_original
|
| 1653 |
-
df_para_exportar = df_filtrado if df_filtrado is not None else df_original
|
| 1654 |
-
|
| 1655 |
-
if df_para_exportar is None:
|
| 1656 |
-
return gr.update(value=None, visible=False)
|
| 1657 |
-
|
| 1658 |
-
try:
|
| 1659 |
-
if hasattr(df_para_exportar, 'empty') and df_para_exportar.empty:
|
| 1660 |
-
return gr.update(value=None, visible=False)
|
| 1661 |
-
|
| 1662 |
-
caminho = exportar_base_csv(df_para_exportar)
|
| 1663 |
-
if caminho:
|
| 1664 |
-
return gr.update(value=caminho, visible=True)
|
| 1665 |
-
else:
|
| 1666 |
-
return gr.update(value=None, visible=False)
|
| 1667 |
-
except Exception as e:
|
| 1668 |
-
print(f"Erro ao exportar CSV: {e}")
|
| 1669 |
-
return gr.update(value=None, visible=False)
|
| 1670 |
-
|
| 1671 |
-
btn_download_base.click(
|
| 1672 |
-
download_base_callback,
|
| 1673 |
-
inputs=[estado_df_filtrado, estado_df],
|
| 1674 |
-
outputs=[download_base_file]
|
| 1675 |
-
)
|
| 1676 |
-
|
| 1677 |
-
# Botões "Adotar Sugestão"
|
| 1678 |
-
btn_adotar_1.click(
|
| 1679 |
-
lambda res, cols_x: adotar_sugestao(0, res, cols_x),
|
| 1680 |
-
inputs=[estado_resultados_busca, checkboxes_x],
|
| 1681 |
-
outputs=[transformacao_y] + transf_x_dropdowns
|
| 1682 |
-
)
|
| 1683 |
-
btn_adotar_2.click(
|
| 1684 |
-
lambda res, cols_x: adotar_sugestao(1, res, cols_x),
|
| 1685 |
-
inputs=[estado_resultados_busca, checkboxes_x],
|
| 1686 |
-
outputs=[transformacao_y] + transf_x_dropdowns
|
| 1687 |
-
)
|
| 1688 |
-
btn_adotar_3.click(
|
| 1689 |
-
lambda res, cols_x: adotar_sugestao(2, res, cols_x),
|
| 1690 |
-
inputs=[estado_resultados_busca, checkboxes_x],
|
| 1691 |
-
outputs=[transformacao_y] + transf_x_dropdowns
|
| 1692 |
-
)
|
| 1693 |
-
btn_adotar_4.click(
|
| 1694 |
-
lambda res, cols_x: adotar_sugestao(3, res, cols_x),
|
| 1695 |
-
inputs=[estado_resultados_busca, checkboxes_x],
|
| 1696 |
-
outputs=[transformacao_y] + transf_x_dropdowns
|
| 1697 |
-
)
|
| 1698 |
-
btn_adotar_5.click(
|
| 1699 |
-
lambda res, cols_x: adotar_sugestao(4, res, cols_x),
|
| 1700 |
-
inputs=[estado_resultados_busca, checkboxes_x],
|
| 1701 |
-
outputs=[transformacao_y] + transf_x_dropdowns
|
| 1702 |
-
)
|
| 1703 |
-
|
| 1704 |
-
# Ajustar modelo (usa dados filtrados se disponíveis) e calcula métricas de outliers
|
| 1705 |
-
dropdown_tipo_grafico_dispersao.change(
|
| 1706 |
-
fn=ao_mudar_tipo_grafico,
|
| 1707 |
-
inputs=[dropdown_tipo_grafico_dispersao, estado_modelo],
|
| 1708 |
-
outputs=[plot_dispersao_transf]
|
| 1709 |
-
)
|
| 1710 |
-
|
| 1711 |
-
btn_ajustar.click(
|
| 1712 |
-
lambda df_filt, df_orig, col_y, cols_x, transf_y, outliers_ant, dicot, cod_aloc, perc, *dd_vals: ajustar_modelo_callback(
|
| 1713 |
-
df_filt if df_filt is not None else df_orig, col_y, cols_x, transf_y, outliers_ant, dicot, cod_aloc, perc, *dd_vals
|
| 1714 |
-
),
|
| 1715 |
-
inputs=_inputs_ajustar,
|
| 1716 |
-
outputs=_outputs_ajustar
|
| 1717 |
-
).then(
|
| 1718 |
-
popular_campos_avaliacao_callback,
|
| 1719 |
-
inputs=[estado_modelo, tabela_estatisticas],
|
| 1720 |
-
outputs=_outputs_popular_avaliacao
|
| 1721 |
-
)
|
| 1722 |
-
|
| 1723 |
-
# Reiniciar iteração (combina outliers e recomeça) e depois ajusta o modelo
|
| 1724 |
-
btn_reiniciar_iteracao.click(
|
| 1725 |
-
reiniciar_iteracao_callback,
|
| 1726 |
-
inputs=[
|
| 1727 |
-
estado_df, estado_outliers_anteriores, outliers_texto, reincluir_texto,
|
| 1728 |
-
estado_iteracao, dropdown_y, checkboxes_x, checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais, slider_grau_coef, slider_grau_f
|
| 1729 |
-
],
|
| 1730 |
-
outputs=[
|
| 1731 |
-
estado_outliers_anteriores,
|
| 1732 |
-
estado_iteracao,
|
| 1733 |
-
estado_df_filtrado,
|
| 1734 |
-
tabela_dados,
|
| 1735 |
-
tabela_estatisticas,
|
| 1736 |
-
header_secao_5,
|
| 1737 |
-
html_outliers_anteriores,
|
| 1738 |
-
html_outliers_sec14,
|
| 1739 |
-
accordion_outliers_anteriores,
|
| 1740 |
-
outliers_texto,
|
| 1741 |
-
reincluir_texto,
|
| 1742 |
-
tabela_metricas,
|
| 1743 |
-
estado_metricas,
|
| 1744 |
-
header_secao_13,
|
| 1745 |
-
txt_resumo_outliers,
|
| 1746 |
-
mapa_html,
|
| 1747 |
-
# Novos outputs para seções 2, 6, 7, 8
|
| 1748 |
-
header_secao_2,
|
| 1749 |
-
html_micronumerosidade,
|
| 1750 |
-
header_secao_6,
|
| 1751 |
-
plot_dispersao,
|
| 1752 |
-
header_secao_7,
|
| 1753 |
-
busca_html,
|
| 1754 |
-
estado_resultados_busca,
|
| 1755 |
-
header_secao_8,
|
| 1756 |
-
btn_adotar_1, btn_adotar_2, btn_adotar_3, btn_adotar_4, btn_adotar_5,
|
| 1757 |
-
slider_grau_coef,
|
| 1758 |
-
slider_grau_f,
|
| 1759 |
-
]
|
| 1760 |
-
).then(
|
| 1761 |
-
# Após reiniciar, ajusta modelo automaticamente para calcular métricas de outliers
|
| 1762 |
-
lambda df_filt, df_orig, col_y, cols_x, transf_y, outliers_ant, dicot, cod_aloc, perc, *dd_vals: ajustar_modelo_callback(
|
| 1763 |
-
df_filt if df_filt is not None else df_orig, col_y, cols_x, transf_y, outliers_ant, dicot, cod_aloc, perc, *dd_vals
|
| 1764 |
-
),
|
| 1765 |
-
inputs=_inputs_ajustar,
|
| 1766 |
-
outputs=_outputs_ajustar
|
| 1767 |
-
).then(
|
| 1768 |
-
popular_campos_avaliacao_callback,
|
| 1769 |
-
inputs=[estado_modelo, tabela_estatisticas],
|
| 1770 |
-
outputs=_outputs_popular_avaliacao
|
| 1771 |
-
).then(
|
| 1772 |
-
fn=None,
|
| 1773 |
-
inputs=None,
|
| 1774 |
-
outputs=None,
|
| 1775 |
-
js="""() => {
|
| 1776 |
-
if ('parentIFrame' in window) {
|
| 1777 |
-
window.parentIFrame.scrollTo({top: 0, behavior: 'smooth'});
|
| 1778 |
-
} else {
|
| 1779 |
-
window.scrollTo({top: 0, behavior: 'smooth'});
|
| 1780 |
-
}
|
| 1781 |
-
}"""
|
| 1782 |
-
)
|
| 1783 |
-
|
| 1784 |
-
# Limpar histórico de outliers
|
| 1785 |
-
btn_limpar_historico.click(
|
| 1786 |
-
limpar_historico_callback,
|
| 1787 |
-
inputs=[estado_df],
|
| 1788 |
-
outputs=[
|
| 1789 |
-
estado_outliers_anteriores,
|
| 1790 |
-
estado_iteracao,
|
| 1791 |
-
estado_df_filtrado,
|
| 1792 |
-
tabela_dados,
|
| 1793 |
-
tabela_estatisticas,
|
| 1794 |
-
mapa_html,
|
| 1795 |
-
html_outliers_anteriores,
|
| 1796 |
-
accordion_outliers_anteriores,
|
| 1797 |
-
header_secao_2,
|
| 1798 |
-
# Reset seções 4-16 (states)
|
| 1799 |
-
estado_modelo, estado_metricas, estado_resultados_busca, estado_avaliacoes,
|
| 1800 |
-
# Headers, accordions e conteúdo das seções 4-16
|
| 1801 |
-
header_secao_4, accordion_secao_4, checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais,
|
| 1802 |
-
html_aviso_multicolinearidade,
|
| 1803 |
-
header_secao_5, accordion_secao_5,
|
| 1804 |
-
header_secao_6, accordion_secao_6, html_micronumerosidade,
|
| 1805 |
-
header_secao_7, accordion_secao_7, plot_dispersao,
|
| 1806 |
-
header_secao_8, accordion_secao_8, slider_grau_coef, slider_grau_f, busca_html,
|
| 1807 |
-
header_secao_9, accordion_secao_9, transformacao_y,
|
| 1808 |
-
btn_adotar_1, btn_adotar_2, btn_adotar_3, btn_adotar_4, btn_adotar_5,
|
| 1809 |
-
] + transf_x_rows + transf_x_columns + transf_x_labels + transf_x_dropdowns + [
|
| 1810 |
-
header_secao_10, accordion_secao_10, dropdown_tipo_grafico_dispersao, plot_dispersao_transf,
|
| 1811 |
-
header_secao_11, accordion_secao_11, diagnosticos_html, tabela_coef, tabela_obs_calc,
|
| 1812 |
-
header_secao_12, accordion_secao_12, plot_obs_calc, plot_residuos, plot_hist, plot_cook, plot_corr,
|
| 1813 |
-
header_secao_13, accordion_secao_13, tabela_metricas,
|
| 1814 |
-
header_secao_14, accordion_secao_14, html_outliers_sec14, outliers_texto, reincluir_texto, txt_resumo_outliers,
|
| 1815 |
-
# Seção 15: Avaliação de Imóvel
|
| 1816 |
-
header_secao_15, accordion_secao_15,
|
| 1817 |
-
] + aval_rows + aval_inputs + [
|
| 1818 |
-
resultado_avaliacao_html, dropdown_base_avaliacao, excluir_aval_trigger, download_avaliacoes_file,
|
| 1819 |
-
# Seção 16: Exportar Modelo
|
| 1820 |
-
header_secao_16, accordion_secao_16, nome_arquivo, status_exportar,
|
| 1821 |
-
] + filtro_vars
|
| 1822 |
-
)
|
| 1823 |
-
|
| 1824 |
-
# Exportar (usa dados filtrados se disponíveis)
|
| 1825 |
-
btn_exportar.click(
|
| 1826 |
-
lambda res_modelo, df_filt, df_orig, stats, nome, elab_nome, outliers: exportar_modelo_callback(
|
| 1827 |
-
res_modelo, df_filt if df_filt is not None else df_orig, df_orig, stats, nome,
|
| 1828 |
-
_avaliadores_dict.get(elab_nome) if elab_nome else None,
|
| 1829 |
-
outliers or []
|
| 1830 |
-
),
|
| 1831 |
-
inputs=[estado_modelo, estado_df_filtrado, estado_df, tabela_estatisticas, nome_arquivo, dropdown_elaborador, estado_outliers_anteriores],
|
| 1832 |
-
outputs=[status_exportar, download_modelo_file]
|
| 1833 |
-
)
|
| 1834 |
-
|
| 1835 |
-
# Avaliação de imóvel (seção 15)
|
| 1836 |
-
btn_calcular_avaliacao.click(
|
| 1837 |
-
avaliar_imovel_callback,
|
| 1838 |
-
inputs=[estado_modelo, tabela_estatisticas, estado_avaliacoes, dropdown_base_avaliacao] + aval_inputs,
|
| 1839 |
-
outputs=[resultado_avaliacao_html, estado_avaliacoes, dropdown_base_avaliacao]
|
| 1840 |
-
)
|
| 1841 |
-
|
| 1842 |
-
btn_limpar_avaliacoes.click(
|
| 1843 |
-
limpar_avaliacoes_callback,
|
| 1844 |
-
inputs=[],
|
| 1845 |
-
outputs=[resultado_avaliacao_html, estado_avaliacoes, dropdown_base_avaliacao]
|
| 1846 |
-
)
|
| 1847 |
-
|
| 1848 |
-
excluir_aval_trigger.change(
|
| 1849 |
-
excluir_avaliacao_callback,
|
| 1850 |
-
inputs=[excluir_aval_trigger, estado_avaliacoes, dropdown_base_avaliacao],
|
| 1851 |
-
outputs=[resultado_avaliacao_html, estado_avaliacoes, dropdown_base_avaliacao, excluir_aval_trigger]
|
| 1852 |
-
)
|
| 1853 |
-
|
| 1854 |
-
dropdown_base_avaliacao.change(
|
| 1855 |
-
atualizar_base_avaliacao_callback,
|
| 1856 |
-
inputs=[estado_avaliacoes, dropdown_base_avaliacao],
|
| 1857 |
-
outputs=[resultado_avaliacao_html]
|
| 1858 |
-
)
|
| 1859 |
-
|
| 1860 |
-
btn_exportar_avaliacoes.click(
|
| 1861 |
-
exportar_avaliacoes_excel_callback,
|
| 1862 |
-
inputs=[estado_avaliacoes],
|
| 1863 |
-
outputs=[download_avaliacoes_file]
|
| 1864 |
-
)
|
| 1865 |
-
|
| 1866 |
-
# Downloads de tabelas
|
| 1867 |
-
btn_download_estatisticas.click(
|
| 1868 |
-
lambda df: download_tabela_callback(df, "estatisticas"),
|
| 1869 |
-
inputs=[tabela_estatisticas],
|
| 1870 |
-
outputs=[download_estatisticas_file]
|
| 1871 |
-
)
|
| 1872 |
-
|
| 1873 |
-
btn_download_dados.click(
|
| 1874 |
-
lambda df: download_tabela_callback(df, "dados"),
|
| 1875 |
-
inputs=[tabela_dados],
|
| 1876 |
-
outputs=[download_dados_file]
|
| 1877 |
-
)
|
| 1878 |
-
|
| 1879 |
-
btn_download_coef.click(
|
| 1880 |
-
lambda df: download_tabela_callback(df, "coeficientes"),
|
| 1881 |
-
inputs=[tabela_coef],
|
| 1882 |
-
outputs=[download_coef_file]
|
| 1883 |
-
)
|
| 1884 |
-
|
| 1885 |
-
btn_download_obs_calc.click(
|
| 1886 |
-
lambda df: download_tabela_callback(df, "obs_calc"),
|
| 1887 |
-
inputs=[tabela_obs_calc],
|
| 1888 |
-
outputs=[download_obs_calc_file]
|
| 1889 |
-
)
|
| 1890 |
-
|
| 1891 |
-
btn_download_metricas.click(
|
| 1892 |
-
lambda df: download_tabela_callback(df, "metricas"),
|
| 1893 |
-
inputs=[tabela_metricas],
|
| 1894 |
-
outputs=[download_metricas_file]
|
| 1895 |
-
)
|
| 1896 |
-
|
| 1897 |
-
|
| 1898 |
-
|
| 1899 |
-
# ============================================================
|
| 1900 |
-
# MAIN
|
| 1901 |
-
# ============================================================
|
| 1902 |
-
|
| 1903 |
-
if __name__ == "__main__":
|
| 1904 |
-
css = carregar_css()
|
| 1905 |
-
with gr.Blocks(title="Elaboração de Modelos", css=css) as app:
|
| 1906 |
-
gr.Markdown(TITULO)
|
| 1907 |
-
criar_aba()
|
| 1908 |
-
app.queue().launch()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/app/core/elaboracao/carregamento.py
DELETED
|
@@ -1,639 +0,0 @@
|
|
| 1 |
-
# -*- coding: utf-8 -*-
|
| 2 |
-
"""
|
| 3 |
-
carregamento.py - Carga de arquivos, reset de estado e inicialização.
|
| 4 |
-
|
| 5 |
-
Callbacks que lidam com upload de CSV/Excel/.dai,
|
| 6 |
-
seleção de aba Excel, reset de seções e limpar histórico.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
import gradio as gr
|
| 10 |
-
import os
|
| 11 |
-
from datetime import datetime, timezone, timedelta
|
| 12 |
-
|
| 13 |
-
from .formatadores import arredondar_df, criar_header_secao, formatar_lista_variaveis_html
|
| 14 |
-
from .modelo import (
|
| 15 |
-
aplicar_selecao_callback,
|
| 16 |
-
ajustar_modelo_callback,
|
| 17 |
-
MAX_VARS_X,
|
| 18 |
-
)
|
| 19 |
-
from .core import (
|
| 20 |
-
detectar_abas_excel,
|
| 21 |
-
carregar_arquivo,
|
| 22 |
-
carregar_dai,
|
| 23 |
-
obter_colunas_numericas,
|
| 24 |
-
identificar_coluna_y_padrao,
|
| 25 |
-
formatar_transformacao,
|
| 26 |
-
)
|
| 27 |
-
from .charts import criar_mapa
|
| 28 |
-
from .geocodificacao import verificar_coords, auto_detectar_colunas_geo, padronizar_coords
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
# ============================================================
|
| 32 |
-
# RESET DE SEÇÕES
|
| 33 |
-
# ============================================================
|
| 34 |
-
|
| 35 |
-
def _valores_reset_secoes_4_a_16():
|
| 36 |
-
"""Valores de reset para seções 4-16 (headers, accordions e conteúdo).
|
| 37 |
-
|
| 38 |
-
Headers são resetados para remover timestamps. Accordions são fechados.
|
| 39 |
-
Usado por carregar_dados_do_arquivo e limpar_historico_callback para limpar
|
| 40 |
-
todas as seções quando um novo arquivo é carregado ou o histórico é resetado.
|
| 41 |
-
|
| 42 |
-
CONTRACT: Retorna 156 itens.
|
| 43 |
-
Se alterar, atualizar: TODAS as funções neste arquivo que retornam 193 itens
|
| 44 |
-
(ao_carregar_arquivo, _resultado_selecao_aba, confirmar_aba_callback,
|
| 45 |
-
carregar_dados_de_dai, carregar_dados_do_arquivo) + limpar_historico_callback.
|
| 46 |
-
"""
|
| 47 |
-
n_rows = (MAX_VARS_X + 7) // 8 # 3
|
| 48 |
-
return (
|
| 49 |
-
# States
|
| 50 |
-
None, # estado_modelo
|
| 51 |
-
None, # estado_metricas
|
| 52 |
-
[], # estado_resultados_busca
|
| 53 |
-
[], # estado_avaliacoes
|
| 54 |
-
# Section 4
|
| 55 |
-
gr.update(value=criar_header_secao(4, "Selecionar Variáveis Independentes")), # header_secao_4
|
| 56 |
-
gr.update(visible=False), # accordion_secao_4
|
| 57 |
-
gr.update(choices=[], value=[], visible=False), # checkboxes_dicotomicas
|
| 58 |
-
gr.update(choices=[], value=[], visible=False), # checkboxes_codigo_alocado
|
| 59 |
-
gr.update(choices=[], value=[], visible=False), # checkboxes_percentuais
|
| 60 |
-
gr.update(value="", visible=False), # html_aviso_multicolinearidade
|
| 61 |
-
# Section 5
|
| 62 |
-
gr.update(value=criar_header_secao(5, "Estatísticas das Variáveis Selecionadas")), # header_secao_5
|
| 63 |
-
gr.update(visible=False), # accordion_secao_5
|
| 64 |
-
# Section 6
|
| 65 |
-
gr.update(value=criar_header_secao(6, "Teste de Micronumerosidade (NBR 14.653-2)")), # header_secao_6
|
| 66 |
-
gr.update(visible=False), # accordion_secao_6
|
| 67 |
-
"", # html_micronumerosidade
|
| 68 |
-
# Section 7
|
| 69 |
-
gr.update(value=criar_header_secao(7, "Gráficos de Dispersão das Variáveis Independentes")), # header_secao_7
|
| 70 |
-
gr.update(visible=False), # accordion_secao_7
|
| 71 |
-
None, # plot_dispersao
|
| 72 |
-
# Section 8
|
| 73 |
-
gr.update(value=criar_header_secao(8, "Transformações Sugeridas")), # header_secao_8
|
| 74 |
-
gr.update(visible=False), # accordion_secao_8
|
| 75 |
-
gr.update(value=3), # slider_grau_coef
|
| 76 |
-
gr.update(value=3), # slider_grau_f
|
| 77 |
-
"", # busca_html
|
| 78 |
-
# Section 9
|
| 79 |
-
gr.update(value=criar_header_secao(9, "Aplicação das Transformações")), # header_secao_9
|
| 80 |
-
gr.update(visible=False), # accordion_secao_9
|
| 81 |
-
gr.update(value="(x)"), # transformacao_y
|
| 82 |
-
*[gr.update(visible=False) for _ in range(5)], # btn_adotar 1-5
|
| 83 |
-
gr.update(visible=False), # transf_y_row
|
| 84 |
-
gr.update(visible=False), # transf_y_col
|
| 85 |
-
gr.update(value="", visible=False), # transf_y_label
|
| 86 |
-
# transf_x_rows, transf_x_columns, transf_x_labels, transf_x_dropdowns (63 itens)
|
| 87 |
-
*[gr.update(visible=False) for _ in range(n_rows)],
|
| 88 |
-
*[gr.update(visible=False) for _ in range(MAX_VARS_X)],
|
| 89 |
-
*[gr.update(value="", visible=False) for _ in range(MAX_VARS_X)],
|
| 90 |
-
*[gr.update(value="(x)", interactive=True, visible=False) for _ in range(MAX_VARS_X)],
|
| 91 |
-
# Section 10
|
| 92 |
-
gr.update(value=criar_header_secao(10, "Gráficos de Dispersão (Variáveis Transformadas)")), # header_secao_10
|
| 93 |
-
gr.update(visible=False), # accordion_secao_10
|
| 94 |
-
gr.update(value="Variáveis Independentes Transformadas X Variável Dependente Transformada", visible=False), # dropdown_tipo_grafico_dispersao
|
| 95 |
-
None, # plot_dispersao_transf
|
| 96 |
-
# Section 11
|
| 97 |
-
gr.update(value=criar_header_secao(11, "Diagnóstico de Modelo")), # header_secao_11
|
| 98 |
-
gr.update(visible=False), # accordion_secao_11
|
| 99 |
-
"", # diagnosticos_html
|
| 100 |
-
None, # tabela_coef
|
| 101 |
-
None, # tabela_obs_calc
|
| 102 |
-
# Section 12
|
| 103 |
-
gr.update(value=criar_header_secao(12, "Gráficos de Diagnóstico do Modelo")), # header_secao_12
|
| 104 |
-
gr.update(visible=False), # accordion_secao_12
|
| 105 |
-
None, None, None, None, None, # plots: obs_calc, residuos, hist, cook, corr
|
| 106 |
-
# Section 13
|
| 107 |
-
gr.update(value=criar_header_secao(13, "Analisar Outliers")), # header_secao_13
|
| 108 |
-
gr.update(visible=False), # accordion_secao_13
|
| 109 |
-
None, # tabela_metricas
|
| 110 |
-
# Section 14 (exclusão)
|
| 111 |
-
gr.update(value=criar_header_secao(14, "Exclusão ou Reinclusão de Outliers")), # header_secao_14
|
| 112 |
-
gr.update(visible=False), # accordion_secao_14
|
| 113 |
-
"", # html_outliers_sec14
|
| 114 |
-
"", # outliers_texto
|
| 115 |
-
"", # reincluir_texto
|
| 116 |
-
"Excluídos: 0 | A excluir: 0 | A reincluir: 0 | Total: 0", # txt_resumo_outliers
|
| 117 |
-
# Section 15 (Avaliação de Imóvel)
|
| 118 |
-
gr.update(value=criar_header_secao(15, "Avaliação de Imóvel")), # header_secao_15
|
| 119 |
-
gr.update(visible=False), # accordion_secao_15
|
| 120 |
-
*[gr.update(visible=False) for _ in range(MAX_VARS_X // 4)], # aval_rows (5)
|
| 121 |
-
*[gr.update(visible=False, value=None, label="") for _ in range(MAX_VARS_X)], # aval_inputs (20)
|
| 122 |
-
"", # resultado_avaliacao_html
|
| 123 |
-
gr.update(choices=[], value=None), # dropdown_base_avaliacao
|
| 124 |
-
"", # excluir_aval_trigger
|
| 125 |
-
gr.update(value=None, visible=False), # download_avaliacoes_file
|
| 126 |
-
# Section 16 (Exportar Modelo)
|
| 127 |
-
gr.update(value=criar_header_secao(16, "Exportar Modelo")), # header_secao_16
|
| 128 |
-
gr.update(visible=False), # accordion_secao_16
|
| 129 |
-
gr.update(value=""), # nome_arquivo
|
| 130 |
-
gr.update(value=""), # status_exportar
|
| 131 |
-
)
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
# ============================================================
|
| 135 |
-
# CALLBACKS DE CARREGAMENTO
|
| 136 |
-
# ============================================================
|
| 137 |
-
|
| 138 |
-
def ao_carregar_arquivo(arquivo):
|
| 139 |
-
"""Callback quando arquivo é carregado. Detecta se há múltiplas abas no Excel.
|
| 140 |
-
|
| 141 |
-
CONTRACT: Retorna 193 itens para _outputs_carregar (app.py:criar_aba).
|
| 142 |
-
Se alterar, atualizar: _outputs_carregar em app.py + TODAS as 5 funções neste arquivo.
|
| 143 |
-
"""
|
| 144 |
-
caminho_arquivo = arquivo.name if hasattr(arquivo, 'name') else str(arquivo)
|
| 145 |
-
nome_exibicao = "Arquivo carregado: " + os.path.basename(caminho_arquivo)
|
| 146 |
-
|
| 147 |
-
# Se for arquivo .dai, carrega modelo completo
|
| 148 |
-
if caminho_arquivo.endswith('.dai'):
|
| 149 |
-
return carregar_dados_de_dai(caminho_arquivo)
|
| 150 |
-
|
| 151 |
-
# Detecta abas do Excel
|
| 152 |
-
abas, msg_abas, sucesso_abas = detectar_abas_excel(caminho_arquivo)
|
| 153 |
-
|
| 154 |
-
# Se há múltiplas abas, mostra seletor de aba (não carrega dados ainda)
|
| 155 |
-
if sucesso_abas and len(abas) > 1:
|
| 156 |
-
return _resultado_selecao_aba(caminho_arquivo, abas)
|
| 157 |
-
|
| 158 |
-
# Se não há múltiplas abas, carrega diretamente
|
| 159 |
-
return carregar_dados_do_arquivo(caminho_arquivo, None,
|
| 160 |
-
nome_arquivo_exibicao=nome_exibicao)
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
def _resultado_selecao_aba(caminho_arquivo, abas):
|
| 164 |
-
"""Retorna tupla para estado B: mostra seletor de aba, não carrega dados ainda.
|
| 165 |
-
|
| 166 |
-
CONTRACT: Retorna 193 itens para _outputs_carregar (app.py:criar_aba).
|
| 167 |
-
"""
|
| 168 |
-
return (
|
| 169 |
-
None, # estado_df (sem dados ainda)
|
| 170 |
-
"Arquivo com múltiplas abas detectado. Selecione uma aba e confirme.", # status
|
| 171 |
-
gr.update(choices=abas, value=abas[0]), # dropdown_aba
|
| 172 |
-
gr.update(choices=[], value=None), # dropdown_y
|
| 173 |
-
None, # tabela_dados
|
| 174 |
-
None, # tabela_estatisticas
|
| 175 |
-
gr.update(choices=[]), # checkboxes_x
|
| 176 |
-
"<p>Carregue um arquivo para ver o mapa.</p>", # mapa
|
| 177 |
-
None, # estado_df_filtrado
|
| 178 |
-
[], # estado_outliers_anteriores
|
| 179 |
-
1, # estado_iteracao
|
| 180 |
-
gr.update(visible=False), # accordion_outliers_anteriores
|
| 181 |
-
"", # html_outliers_anteriores
|
| 182 |
-
caminho_arquivo, # estado_arquivo_temp (manter caminho para confirmar_aba)
|
| 183 |
-
# Seções 2 e 3 - ocultas
|
| 184 |
-
gr.update(visible=False), # header_secao_2
|
| 185 |
-
gr.update(visible=False), # accordion_secao_2
|
| 186 |
-
gr.update(choices=["Visualização Padrão"], value="Visualização Padrão"), # dropdown_mapa_var
|
| 187 |
-
gr.update(visible=False), # header_secao_3
|
| 188 |
-
gr.update(visible=False), # accordion_secao_3
|
| 189 |
-
# Reset seções 4-16
|
| 190 |
-
*_valores_reset_secoes_4_a_16(),
|
| 191 |
-
# Controles de visibilidade da seção 1
|
| 192 |
-
gr.update(visible=False), # row_upload (ocultar upload)
|
| 193 |
-
gr.update(visible=True), # row_selecao_aba (mostrar seletor de aba)
|
| 194 |
-
gr.update(visible=False), # row_pos_carga (ocultar restart)
|
| 195 |
-
gr.update(value=""), # html_nome_arquivo_carregado
|
| 196 |
-
False, # estado_flag_carregamento
|
| 197 |
-
*[gr.update()] * 4, # filtro_vars (no-op)
|
| 198 |
-
# Painel de coordenadas (no-op — dados ainda não carregados)
|
| 199 |
-
gr.update(visible=False), gr.update(value=""),
|
| 200 |
-
gr.update(choices=[]), gr.update(choices=[]),
|
| 201 |
-
gr.update(choices=[], value=None), gr.update(choices=[], value=None),
|
| 202 |
-
gr.update(visible=True), # row_escolha_opcao — reset para tela de escolha
|
| 203 |
-
gr.update(visible=False), # row_opcao_mapear
|
| 204 |
-
gr.update(visible=False), # row_opcao_geocodificar
|
| 205 |
-
)
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
def confirmar_aba_callback(caminho_arquivo, nome_aba):
|
| 209 |
-
"""Callback quando usuário confirma seleção de aba para Excel multi-aba.
|
| 210 |
-
|
| 211 |
-
CONTRACT: Retorna 193 itens para _outputs_carregar (app.py:criar_aba).
|
| 212 |
-
"""
|
| 213 |
-
if caminho_arquivo is None or nome_aba is None:
|
| 214 |
-
return (
|
| 215 |
-
None, "Erro: arquivo não encontrado. Recarregue a página.",
|
| 216 |
-
gr.update(choices=[], value=None), # dropdown_aba
|
| 217 |
-
gr.update(choices=[], value=None), # dropdown_y
|
| 218 |
-
None, None, gr.update(choices=[]),
|
| 219 |
-
"<p>Carregue um arquivo para ver o mapa.</p>",
|
| 220 |
-
None, [], 1, gr.update(visible=False),
|
| 221 |
-
"", None,
|
| 222 |
-
gr.update(visible=False), gr.update(visible=False),
|
| 223 |
-
gr.update(choices=["Visualização Padrão"], value="Visualização Padrão"),
|
| 224 |
-
gr.update(visible=False), gr.update(visible=False),
|
| 225 |
-
*_valores_reset_secoes_4_a_16(),
|
| 226 |
-
# Volta ao estado de upload em caso de erro
|
| 227 |
-
gr.update(visible=True), # row_upload
|
| 228 |
-
gr.update(visible=False), # row_selecao_aba
|
| 229 |
-
gr.update(visible=False), # row_pos_carga
|
| 230 |
-
gr.update(value=""), # html_nome_arquivo_carregado
|
| 231 |
-
False, # estado_flag_carregamento
|
| 232 |
-
*[gr.update()] * 4, # filtro_vars (no-op)
|
| 233 |
-
# Painel de coordenadas (no-op — erro)
|
| 234 |
-
gr.update(visible=False), gr.update(value=""),
|
| 235 |
-
gr.update(choices=[]), gr.update(choices=[]),
|
| 236 |
-
gr.update(choices=[], value=None), gr.update(choices=[], value=None),
|
| 237 |
-
gr.update(visible=True), # row_escolha_opcao — reset para tela de escolha
|
| 238 |
-
gr.update(visible=False), # row_opcao_mapear
|
| 239 |
-
gr.update(visible=False), # row_opcao_geocodificar
|
| 240 |
-
)
|
| 241 |
-
|
| 242 |
-
nome_exibicao = "Arquivo carregado: "+os.path.basename(caminho_arquivo)
|
| 243 |
-
nome_exibicao_completo = f"{nome_exibicao} — Aba: {nome_aba}"
|
| 244 |
-
return carregar_dados_do_arquivo(caminho_arquivo, nome_aba,
|
| 245 |
-
nome_arquivo_exibicao=nome_exibicao_completo)
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
def carregar_dados_de_dai(caminho_arquivo):
|
| 249 |
-
"""Carrega arquivo .dai e popula toda a interface com o modelo reconstruído.
|
| 250 |
-
|
| 251 |
-
Executa o mesmo fluxo que o usuário faria manualmente:
|
| 252 |
-
1. Carrega dados (como carregar_dados_do_arquivo)
|
| 253 |
-
2. Aplica seleção de variáveis (como aplicar_selecao_callback)
|
| 254 |
-
3. Sobrescreve transformações com valores do .dai
|
| 255 |
-
4. Ajusta modelo (como ajustar_modelo_callback)
|
| 256 |
-
|
| 257 |
-
CONTRACT: Retorna 196 itens para _outputs_carregar (app.py:criar_aba).
|
| 258 |
-
Consome aplicar_selecao_callback por índice (r[0], r[1], ..., r[23:]).
|
| 259 |
-
Consome ajustar_modelo_callback por índice (m[0], ..., m[32]).
|
| 260 |
-
"""
|
| 261 |
-
(
|
| 262 |
-
df,
|
| 263 |
-
coluna_y,
|
| 264 |
-
colunas_x,
|
| 265 |
-
transformacao_y,
|
| 266 |
-
transformacoes_x,
|
| 267 |
-
dicotomicas,
|
| 268 |
-
codigo_alocado,
|
| 269 |
-
percentuais,
|
| 270 |
-
msg,
|
| 271 |
-
sucesso,
|
| 272 |
-
elaborador,
|
| 273 |
-
_observacao_modelo,
|
| 274 |
-
outliers_excluidos,
|
| 275 |
-
_periodo_dados_mercado,
|
| 276 |
-
_config_avaliacao,
|
| 277 |
-
) = carregar_dai(caminho_arquivo)
|
| 278 |
-
|
| 279 |
-
nome_exibicao = os.path.basename(caminho_arquivo)
|
| 280 |
-
html_nome = f"<h2 style='margin:0 0 12px 0; font-size:1.4em;'>{nome_exibicao}</h2>"
|
| 281 |
-
if elaborador:
|
| 282 |
-
nome_elab = elaborador.get("nome_completo", "")
|
| 283 |
-
cargo = elaborador.get("cargo", "")
|
| 284 |
-
conselho = elaborador.get("conselho", "")
|
| 285 |
-
num_conselho = elaborador.get("numero_conselho", "")
|
| 286 |
-
estado_conselho = elaborador.get("estado_conselho", "")
|
| 287 |
-
matricula = elaborador.get("matricula_sem_digito", "")
|
| 288 |
-
lotacao = elaborador.get("lotacao", "")
|
| 289 |
-
linha2 = f"{cargo} · {conselho}/{estado_conselho} {num_conselho}" if cargo else ""
|
| 290 |
-
linha3 = f"Matrícula: {matricula} · {lotacao}" if matricula else ""
|
| 291 |
-
if nome_elab:
|
| 292 |
-
info_variaveis = (
|
| 293 |
-
[f"{coluna_y}: {formatar_transformacao(transformacao_y, is_y=True)}"]
|
| 294 |
-
+ [f"{col}: {formatar_transformacao(transformacoes_x.get(col, '(x)'))}"
|
| 295 |
-
for col in colunas_x]
|
| 296 |
-
)
|
| 297 |
-
variaveis_lado_direito = formatar_lista_variaveis_html(info_variaveis)
|
| 298 |
-
html_nome += (
|
| 299 |
-
'<div style="display:flex; justify-content:space-between; align-items:flex-start; '
|
| 300 |
-
'gap:24px; background:#e9ecef; border-left:5px solid #6c757d; '
|
| 301 |
-
'border-radius:6px; padding:14px 18px; color:#495057; line-height:1.8; margin-top:8px;">'
|
| 302 |
-
'<div>'
|
| 303 |
-
f'<span style="display:block; font-size:1.15em; font-weight:600; color:#212529; margin-bottom:4px;">{nome_elab}</span>'
|
| 304 |
-
+ (f'<span style="display:block; font-size:1em;">{linha2}</span>' if linha2 else '')
|
| 305 |
-
+ (f'<span style="display:block; font-size:0.95em; color:#6c757d;">{linha3}</span>' if linha3 else '')
|
| 306 |
-
+ '</div>'
|
| 307 |
-
+ f'<div>{variaveis_lado_direito}</div>'
|
| 308 |
-
+ '</div>'
|
| 309 |
-
)
|
| 310 |
-
|
| 311 |
-
if outliers_excluidos:
|
| 312 |
-
lista_str = ", ".join(map(str, sorted(outliers_excluidos)))
|
| 313 |
-
n = len(outliers_excluidos)
|
| 314 |
-
html_outliers_content = (
|
| 315 |
-
'<div style="display:flex; gap:16px; align-items:baseline; padding:10px 16px; '
|
| 316 |
-
'background:var(--background-fill-secondary,#f8f9fa); border-radius:8px; '
|
| 317 |
-
'border:1px solid var(--border-color-primary,#e2e8f0); flex-wrap:wrap;">'
|
| 318 |
-
f'<span style="font-weight:600; color:var(--body-text-color,#495057); white-space:nowrap;">'
|
| 319 |
-
f'{n} outlier(s) excluídos do modelo ajustado</span>'
|
| 320 |
-
f'<span style="color:var(--body-text-color-subdued,#6c757d); font-size:0.92em;">'
|
| 321 |
-
f'Índices: {lista_str}</span>'
|
| 322 |
-
'</div>'
|
| 323 |
-
)
|
| 324 |
-
else:
|
| 325 |
-
html_outliers_content = ""
|
| 326 |
-
|
| 327 |
-
if not sucesso:
|
| 328 |
-
return (
|
| 329 |
-
None, msg, gr.update(), gr.update(choices=[], value=None),
|
| 330 |
-
None, None, gr.update(choices=[]), "<p>Erro ao carregar modelo.</p>",
|
| 331 |
-
None, [], 1, gr.update(visible=False),
|
| 332 |
-
"", None,
|
| 333 |
-
gr.update(visible=False), gr.update(visible=False),
|
| 334 |
-
gr.update(choices=["Visualização Padrão"], value="Visualização Padrão"),
|
| 335 |
-
gr.update(visible=False), gr.update(visible=False),
|
| 336 |
-
*_valores_reset_secoes_4_a_16(),
|
| 337 |
-
# Volta ao estado de upload em caso de erro
|
| 338 |
-
gr.update(visible=True), # row_upload
|
| 339 |
-
gr.update(visible=False), # row_selecao_aba
|
| 340 |
-
gr.update(visible=False), # row_pos_carga
|
| 341 |
-
gr.update(value=""), # html_nome_arquivo_carregado
|
| 342 |
-
False, # estado_flag_carregamento
|
| 343 |
-
*[gr.update()] * 4, # filtro_vars (no-op)
|
| 344 |
-
# Painel de coordenadas (no-op — .dai isento)
|
| 345 |
-
gr.update(visible=False), gr.update(value=""),
|
| 346 |
-
gr.update(choices=[]), gr.update(choices=[]),
|
| 347 |
-
gr.update(choices=[], value=None), gr.update(choices=[], value=None),
|
| 348 |
-
gr.update(visible=True), # row_escolha_opcao — reset para tela de escolha
|
| 349 |
-
gr.update(visible=False), # row_opcao_mapear
|
| 350 |
-
gr.update(visible=False), # row_opcao_geocodificar
|
| 351 |
-
)
|
| 352 |
-
|
| 353 |
-
colunas_numericas = obter_colunas_numericas(df)
|
| 354 |
-
gmt_minus_3 = timezone(timedelta(hours=-3))
|
| 355 |
-
timestamp = datetime.now(gmt_minus_3).strftime("%H:%M:%S")
|
| 356 |
-
mapa_html_val = criar_mapa(df)
|
| 357 |
-
|
| 358 |
-
# --- Passo 2: Simula "Aplicar Seleção" (seções 5-9) ---
|
| 359 |
-
r = aplicar_selecao_callback(df, coluna_y, colunas_x, outliers_excluidos, dicotomicas, codigo_alocado, percentuais)
|
| 360 |
-
# r: [df_filtrado, tabela_est, html_micro, plot_disp, slider_coef, slider_f,
|
| 361 |
-
# busca_html, resultados, btn1-5(5), secoes5-9 headers/accordions(10), campos_transf(66)]
|
| 362 |
-
|
| 363 |
-
# --- Passo 3: Sobrescreve dropdowns de transformação com valores do .dai ---
|
| 364 |
-
campos_transf = list(r[23:89]) # 66 itens: y_row(1)+y_col(1)+y_label(1)+rows(3)+columns(20)+labels(20)+dropdowns(20)
|
| 365 |
-
n_rows = (MAX_VARS_X + 7) // 8
|
| 366 |
-
dropdown_offset = 3 + n_rows + MAX_VARS_X + MAX_VARS_X # 3 + 3 + 20 + 20 = 46
|
| 367 |
-
for i, col in enumerate(colunas_x):
|
| 368 |
-
if i < MAX_VARS_X:
|
| 369 |
-
# Dicotômicas e percentuais: travar. Variáveis categóricas codificadas: livres.
|
| 370 |
-
travar = col in dicotomicas or col in percentuais
|
| 371 |
-
campos_transf[dropdown_offset + i] = gr.update(
|
| 372 |
-
value=transformacoes_x.get(col, "(x)"),
|
| 373 |
-
interactive=not travar,
|
| 374 |
-
visible=True
|
| 375 |
-
)
|
| 376 |
-
|
| 377 |
-
# --- Passo 4: Simula "Ajustar Modelo" (seções 10-16) ---
|
| 378 |
-
df_para_ajuste = r[0] if r[0] is not None else df # df filtrado (sem outliers excluídos)
|
| 379 |
-
dropdown_vals = [transformacoes_x.get(colunas_x[i], "(x)") if i < len(colunas_x) else "(x)"
|
| 380 |
-
for i in range(MAX_VARS_X)]
|
| 381 |
-
m = ajustar_modelo_callback(df_para_ajuste, coluna_y, colunas_x, transformacao_y, outliers_excluidos, dicotomicas, codigo_alocado, percentuais, *dropdown_vals)
|
| 382 |
-
# m[0]:resultado [1]:diag [2]:coef [3]:obs_calc [4]:plot_disp [5]:dropdown
|
| 383 |
-
# m[6-10]:plots [11]:metricas [12]:est_metricas [13]:resumo
|
| 384 |
-
# m[14]:estado_avaliacoes
|
| 385 |
-
# m[15-16]:h10,a10 [17-18]:h11,a11 [19-20]:h12,a12 [21-22]:h13,a13
|
| 386 |
-
# m[23-24]:h14,a14 [25-26]:h15_aval,a15_aval [27-28]:h16_exp,a16_exp
|
| 387 |
-
# m[29-32]:filtro_vars
|
| 388 |
-
|
| 389 |
-
n_aval_rows = MAX_VARS_X // 4 # 5
|
| 390 |
-
|
| 391 |
-
return (
|
| 392 |
-
# --- Seções 1-3: dados básicos ---
|
| 393 |
-
df, # estado_df
|
| 394 |
-
msg, # status
|
| 395 |
-
gr.update(), # dropdown_aba (no-op)
|
| 396 |
-
gr.update(choices=colunas_numericas, value=coluna_y), # dropdown_y
|
| 397 |
-
arredondar_df(df), # tabela_dados
|
| 398 |
-
r[1], # tabela_estatisticas (de aplicar_selecao)
|
| 399 |
-
gr.update(choices=colunas_numericas, value=colunas_x), # checkboxes_x
|
| 400 |
-
mapa_html_val, # mapa
|
| 401 |
-
r[0], # estado_df_filtrado (de aplicar_selecao)
|
| 402 |
-
outliers_excluidos, # estado_outliers_anteriores
|
| 403 |
-
1, # estado_iteracao
|
| 404 |
-
gr.update(visible=bool(outliers_excluidos)), # accordion_outliers_anteriores
|
| 405 |
-
html_outliers_content, # html_outliers_anteriores
|
| 406 |
-
None, # estado_arquivo_temp
|
| 407 |
-
gr.update(visible=True, value=criar_header_secao(2, "Visualizar Dados", timestamp)),
|
| 408 |
-
gr.update(visible=True, open=True), # accordion_secao_2
|
| 409 |
-
gr.update(choices=["Visualização Padrão"] + colunas_numericas, value="Visualização Padrão"),
|
| 410 |
-
gr.update(visible=True), # header_secao_3
|
| 411 |
-
gr.update(visible=True, open=True), # accordion_secao_3
|
| 412 |
-
# --- States seções 4-16 ---
|
| 413 |
-
m[0], # estado_modelo (de ajustar)
|
| 414 |
-
m[12], # estado_metricas (de ajustar)
|
| 415 |
-
r[7], # estado_resultados_busca (de aplicar_selecao)
|
| 416 |
-
m[14], # estado_avaliacoes (de ajustar — reset [])
|
| 417 |
-
# --- Seção 4: seleção de variáveis ---
|
| 418 |
-
gr.update(visible=True, value=criar_header_secao(4, "Selecionar Variáveis Independentes", timestamp)),
|
| 419 |
-
gr.update(visible=True, open=True), # accordion_secao_4
|
| 420 |
-
gr.update(choices=list(colunas_x), value=list(dicotomicas), visible=True), # checkboxes_dicotomicas
|
| 421 |
-
gr.update(choices=list(colunas_x), value=list(codigo_alocado), visible=True), # checkboxes_codigo_alocado
|
| 422 |
-
gr.update(choices=list(colunas_x), value=list(percentuais), visible=True), # checkboxes_percentuais
|
| 423 |
-
gr.update(value="", visible=False), # html_aviso_multicolinearidade
|
| 424 |
-
# --- Seções 5-9: headers/accordions (de aplicar_selecao) ---
|
| 425 |
-
r[13], r[14], # header_secao_5, accordion_secao_5
|
| 426 |
-
r[15], r[16], # header_secao_6, accordion_secao_6
|
| 427 |
-
r[2], # html_micronumerosidade
|
| 428 |
-
r[17], r[18], # header_secao_7, accordion_secao_7
|
| 429 |
-
r[3], # plot_dispersao
|
| 430 |
-
r[19], r[20], # header_secao_8, accordion_secao_8
|
| 431 |
-
r[4], r[5], # slider_grau_coef, slider_grau_f
|
| 432 |
-
r[6], # busca_html
|
| 433 |
-
r[21], r[22], # header_secao_9, accordion_secao_9
|
| 434 |
-
gr.update(value=transformacao_y), # transformacao_y dropdown (do .dai)
|
| 435 |
-
r[8], r[9], r[10], r[11], r[12], # btn_adotar 1-5
|
| 436 |
-
# --- Campos de transformação (com dropdowns sobrescritos) ---
|
| 437 |
-
*campos_transf,
|
| 438 |
-
# --- Seções 10-16 (de ajustar_modelo_callback) ---
|
| 439 |
-
m[15], m[16], # header_secao_10, accordion_secao_10
|
| 440 |
-
m[5], # dropdown_tipo_grafico_dispersao
|
| 441 |
-
m[4], # plot_dispersao_transf
|
| 442 |
-
m[17], m[18], # header_secao_11, accordion_secao_11
|
| 443 |
-
m[1], # diagnosticos_html
|
| 444 |
-
m[2], # tabela_coef
|
| 445 |
-
m[3], # tabela_obs_calc
|
| 446 |
-
m[19], m[20], # header_secao_12, accordion_secao_12
|
| 447 |
-
m[6], m[7], m[8], m[9], m[10], # plots: obs_calc, resid, hist, cook, corr
|
| 448 |
-
m[21], m[22], # header_secao_13, accordion_secao_13
|
| 449 |
-
m[11], # tabela_metricas
|
| 450 |
-
m[23], m[24], # header_secao_14, accordion_secao_14
|
| 451 |
-
html_outliers_content, # html_outliers_sec14
|
| 452 |
-
"", # outliers_texto
|
| 453 |
-
"", # reincluir_texto
|
| 454 |
-
m[13], # txt_resumo_outliers
|
| 455 |
-
# --- Seção 15: Avaliação de Imóvel ---
|
| 456 |
-
m[25], m[26], # header_secao_15, accordion_secao_15
|
| 457 |
-
*[gr.update(visible=False) for _ in range(n_aval_rows)], # aval_rows (populados pelo .then)
|
| 458 |
-
*[gr.update(visible=False, value=None, label="") for _ in range(MAX_VARS_X)], # aval_inputs (populados pelo .then)
|
| 459 |
-
"", # resultado_avaliacao_html
|
| 460 |
-
gr.update(choices=[], value=None), # dropdown_base_avaliacao
|
| 461 |
-
"", # excluir_aval_trigger
|
| 462 |
-
gr.update(value=None, visible=False), # download_avaliacoes_file
|
| 463 |
-
# --- Seção 16: Exportar Modelo ---
|
| 464 |
-
m[27], m[28], # header_secao_16, accordion_secao_16
|
| 465 |
-
gr.update(value=""), # nome_arquivo
|
| 466 |
-
gr.update(value=""), # status_exportar
|
| 467 |
-
# Transição para estado C (pós-carregamento)
|
| 468 |
-
gr.update(visible=False), # row_upload
|
| 469 |
-
gr.update(visible=False), # row_selecao_aba
|
| 470 |
-
gr.update(visible=True), # row_pos_carga
|
| 471 |
-
gr.update(value=html_nome), # html_nome_arquivo_carregado
|
| 472 |
-
2, # estado_flag_carregamento (contador: 2 side-effects esperados)
|
| 473 |
-
m[29], m[30], m[31], m[32], # filtro_vars (choices com colunas originais)
|
| 474 |
-
# Painel de coordenadas (no-op — .dai isento)
|
| 475 |
-
gr.update(visible=False), gr.update(value=""),
|
| 476 |
-
gr.update(choices=[]), gr.update(choices=[]),
|
| 477 |
-
gr.update(choices=[], value=None), gr.update(choices=[], value=None),
|
| 478 |
-
gr.update(visible=True), # row_escolha_opcao — reset para tela de escolha
|
| 479 |
-
gr.update(visible=False), # row_opcao_mapear
|
| 480 |
-
gr.update(visible=False), # row_opcao_geocodificar
|
| 481 |
-
)
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
def carregar_dados_do_arquivo(caminho_arquivo, nome_aba, nome_arquivo_exibicao=""):
|
| 485 |
-
"""Carrega dados de um arquivo, opcionalmente de uma aba específica.
|
| 486 |
-
|
| 487 |
-
Args:
|
| 488 |
-
caminho_arquivo: Caminho do arquivo a carregar
|
| 489 |
-
nome_aba: Nome da aba a carregar (apenas para Excel)
|
| 490 |
-
nome_arquivo_exibicao: Nome do arquivo para exibir na interface pós-carregamento
|
| 491 |
-
|
| 492 |
-
CONTRACT: Retorna 193 itens para _outputs_carregar (app.py:criar_aba).
|
| 493 |
-
"""
|
| 494 |
-
df, msg, sucesso = carregar_arquivo(caminho_arquivo, nome_aba)
|
| 495 |
-
|
| 496 |
-
if not sucesso:
|
| 497 |
-
return (
|
| 498 |
-
None, # estado_df
|
| 499 |
-
msg, # status
|
| 500 |
-
gr.update(), # dropdown_aba
|
| 501 |
-
gr.update(choices=[], value=None), # dropdown_y
|
| 502 |
-
None, # tabela_dados
|
| 503 |
-
None, # tabela_estatisticas
|
| 504 |
-
gr.update(choices=[]), # checkboxes_x
|
| 505 |
-
"<p>Carregue um arquivo para ver o mapa.</p>", # mapa
|
| 506 |
-
None, # estado_df_filtrado
|
| 507 |
-
[], # estado_outliers_anteriores
|
| 508 |
-
1, # estado_iteracao
|
| 509 |
-
gr.update(visible=False), # accordion_outliers_anteriores
|
| 510 |
-
"", # html_outliers_anteriores
|
| 511 |
-
None, # estado_arquivo_temp
|
| 512 |
-
# Seções 2 e 3 - ocultas quando erro
|
| 513 |
-
gr.update(visible=False), # header_secao_2
|
| 514 |
-
gr.update(visible=False), # accordion_secao_2
|
| 515 |
-
gr.update(choices=["Visualização Padrão"], value="Visualização Padrão"), # dropdown_mapa_var
|
| 516 |
-
gr.update(visible=False), # header_secao_3
|
| 517 |
-
gr.update(visible=False), # accordion_secao_3
|
| 518 |
-
# Reset seções 4-16
|
| 519 |
-
*_valores_reset_secoes_4_a_16(),
|
| 520 |
-
# Volta ao estado de upload em caso de erro
|
| 521 |
-
gr.update(visible=True), # row_upload
|
| 522 |
-
gr.update(visible=False), # row_selecao_aba
|
| 523 |
-
gr.update(visible=False), # row_pos_carga
|
| 524 |
-
gr.update(value=""), # html_nome_arquivo_carregado
|
| 525 |
-
False, # estado_flag_carregamento
|
| 526 |
-
*[gr.update()] * 4, # filtro_vars (no-op)
|
| 527 |
-
# Painel de coordenadas (no-op — erro)
|
| 528 |
-
gr.update(visible=False), gr.update(value=""),
|
| 529 |
-
gr.update(choices=[]), gr.update(choices=[]),
|
| 530 |
-
gr.update(choices=[], value=None), gr.update(choices=[], value=None),
|
| 531 |
-
gr.update(visible=True), # row_escolha_opcao — reset para tela de escolha
|
| 532 |
-
gr.update(visible=False), # row_opcao_mapear
|
| 533 |
-
gr.update(visible=False), # row_opcao_geocodificar
|
| 534 |
-
)
|
| 535 |
-
|
| 536 |
-
# Verificar e padronizar coordenadas
|
| 537 |
-
tem_coords, col_lat, col_lon = verificar_coords(df)
|
| 538 |
-
todas_colunas = df.columns.tolist()
|
| 539 |
-
|
| 540 |
-
if tem_coords:
|
| 541 |
-
df = padronizar_coords(df, col_lat, col_lon)
|
| 542 |
-
aviso_coords_html = ""
|
| 543 |
-
cdlog_auto = None
|
| 544 |
-
num_auto = None
|
| 545 |
-
else:
|
| 546 |
-
cdlog_auto, num_auto = auto_detectar_colunas_geo(df)
|
| 547 |
-
n = len(df)
|
| 548 |
-
aviso_coords_html = (
|
| 549 |
-
'<div style="background:#f8d7da;border:1px solid #f5c2c7;border-radius:8px;'
|
| 550 |
-
f'padding:10px 14px;margin-bottom:8px"><strong>⚠️ Colunas lat/lon não '
|
| 551 |
-
f'encontradas</strong> — {n} registro(s) sem coordenadas padronizadas.<br>'
|
| 552 |
-
'</div>'
|
| 553 |
-
)
|
| 554 |
-
|
| 555 |
-
# Identifica colunas (após padronização de coords)
|
| 556 |
-
colunas_numericas = obter_colunas_numericas(df)
|
| 557 |
-
coluna_y_padrao = identificar_coluna_y_padrao(df)
|
| 558 |
-
|
| 559 |
-
# Variáveis X padrão: todas exceto Y
|
| 560 |
-
colunas_x_padrao = [col for col in colunas_numericas if col != coluna_y_padrao]
|
| 561 |
-
|
| 562 |
-
# NÃO calcula estatísticas no carregamento - apenas ao clicar em "Aplicar Seleção"
|
| 563 |
-
|
| 564 |
-
# Mapa
|
| 565 |
-
mapa_html = criar_mapa(df)
|
| 566 |
-
|
| 567 |
-
# Timestamp para seção 2
|
| 568 |
-
gmt_minus_3 = timezone(timedelta(hours=-3))
|
| 569 |
-
timestamp = datetime.now(gmt_minus_3).strftime("%H:%M:%S")
|
| 570 |
-
|
| 571 |
-
return (
|
| 572 |
-
df, # estado_df
|
| 573 |
-
msg, # status
|
| 574 |
-
gr.update(), # dropdown_aba (no-op)
|
| 575 |
-
gr.update(choices=colunas_numericas, value=coluna_y_padrao), # dropdown_y
|
| 576 |
-
arredondar_df(df), # tabela_dados
|
| 577 |
-
gr.update(value=None), # tabela_estatisticas - NÃO mostra até clicar em Aplicar Seleção
|
| 578 |
-
gr.update(choices=colunas_numericas, value=[]), # checkboxes_x (nenhuma marcada por padrão)
|
| 579 |
-
mapa_html, # mapa
|
| 580 |
-
df, # estado_df_filtrado (inicia com dados completos)
|
| 581 |
-
[], # estado_outliers_anteriores
|
| 582 |
-
1, # estado_iteracao
|
| 583 |
-
gr.update(visible=False), # accordion_outliers_anteriores
|
| 584 |
-
"", # html_outliers_anteriores
|
| 585 |
-
None, # estado_arquivo_temp
|
| 586 |
-
# Seções 2 e 3 — header sempre visível; accordion oculto até coords resolvidas
|
| 587 |
-
gr.update(visible=True, value=criar_header_secao(2, "Visualizar Dados", timestamp) if tem_coords else criar_header_secao(2, "Visualizar Dados")), # header_secao_2
|
| 588 |
-
gr.update(visible=tem_coords, open=True), # accordion_secao_2
|
| 589 |
-
gr.update(choices=["Visualização Padrão"] + colunas_numericas, value="Visualização Padrão"), # dropdown_mapa_var
|
| 590 |
-
gr.update(visible=True), # header_secao_3
|
| 591 |
-
gr.update(visible=tem_coords, open=True), # accordion_secao_3
|
| 592 |
-
# Reset seções 4-16
|
| 593 |
-
*_valores_reset_secoes_4_a_16(),
|
| 594 |
-
# Transição para estado C (pós-carregamento)
|
| 595 |
-
gr.update(visible=False), # row_upload
|
| 596 |
-
gr.update(visible=False), # row_selecao_aba
|
| 597 |
-
gr.update(visible=True), # row_pos_carga
|
| 598 |
-
gr.update(value=f"<h3>{nome_arquivo_exibicao}</h3>"), # html_nome_arquivo_carregado
|
| 599 |
-
2, # estado_flag_carregamento (contador: 2 side-effects esperados)
|
| 600 |
-
*[gr.update()] * 4, # filtro_vars (no-op — seções 13-14 ocultas)
|
| 601 |
-
# Painel de coordenadas (Estado D) — mostrado apenas se coords ausentes
|
| 602 |
-
gr.update(visible=not tem_coords), # row_coords_panel
|
| 603 |
-
gr.update(value=aviso_coords_html), # html_aviso_coords
|
| 604 |
-
gr.update(choices=todas_colunas if not tem_coords else []), # dropdown_col_lat_manual
|
| 605 |
-
gr.update(choices=todas_colunas if not tem_coords else []), # dropdown_col_lon_manual
|
| 606 |
-
gr.update(choices=todas_colunas if not tem_coords else [], value=cdlog_auto), # dropdown_cdlog_geo
|
| 607 |
-
gr.update(choices=todas_colunas if not tem_coords else [], value=num_auto), # dropdown_num_geo
|
| 608 |
-
gr.update(visible=True), # row_escolha_opcao — reset para tela de escolha
|
| 609 |
-
gr.update(visible=False), # row_opcao_mapear
|
| 610 |
-
gr.update(visible=False), # row_opcao_geocodificar
|
| 611 |
-
)
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
# ============================================================
|
| 615 |
-
# LIMPAR HISTÓRICO
|
| 616 |
-
# ============================================================
|
| 617 |
-
|
| 618 |
-
def limpar_historico_callback(df_original):
|
| 619 |
-
"""Limpa o histórico de outliers e reinicia do zero.
|
| 620 |
-
Reseta tudo como se o arquivo tivesse acabado de ser carregado.
|
| 621 |
-
|
| 622 |
-
CONTRACT: Retorna 169 itens para btn_limpar_historico.click (app.py:criar_aba).
|
| 623 |
-
Se alterar, atualizar: app.py (outputs de btn_limpar_historico.click).
|
| 624 |
-
"""
|
| 625 |
-
mapa = criar_mapa(df_original)
|
| 626 |
-
|
| 627 |
-
return (
|
| 628 |
-
[], # estado_outliers_anteriores (vazio)
|
| 629 |
-
1, # estado_iteracao (reinicia)
|
| 630 |
-
df_original, # estado_df_filtrado (dados originais)
|
| 631 |
-
arredondar_df(df_original), # tabela_dados
|
| 632 |
-
gr.update(value=None), # tabela_estatisticas (limpa)
|
| 633 |
-
mapa, # mapa_html
|
| 634 |
-
"", # html_outliers_anteriores
|
| 635 |
-
gr.update(visible=False), # accordion_outliers_anteriores (esconde)
|
| 636 |
-
gr.update(value=criar_header_secao(2, "Visualizar Dados")), # header_secao_2
|
| 637 |
-
*_valores_reset_secoes_4_a_16(), # reset completo seções 4-15
|
| 638 |
-
*[gr.update()] * 4, # filtro_vars (no-op — seções ocultas após reset)
|
| 639 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/app/core/elaboracao/modelo.py
DELETED
|
@@ -1,991 +0,0 @@
|
|
| 1 |
-
# -*- coding: utf-8 -*-
|
| 2 |
-
"""
|
| 3 |
-
modelo.py - Ajuste de modelo OLS, transformações e seleção de variáveis.
|
| 4 |
-
|
| 5 |
-
Callbacks que lidam com a lógica do modelo estatístico:
|
| 6 |
-
seleção X/Y, ajuste OLS, busca de transformações, exportação.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
import gradio as gr
|
| 10 |
-
import pandas as pd
|
| 11 |
-
from datetime import datetime, timezone, timedelta
|
| 12 |
-
|
| 13 |
-
from .formatadores import (
|
| 14 |
-
criar_header_secao,
|
| 15 |
-
formatar_diagnosticos_html,
|
| 16 |
-
formatar_busca_html,
|
| 17 |
-
formatar_micronumerosidade_html,
|
| 18 |
-
formatar_aviso_multicolinearidade,
|
| 19 |
-
)
|
| 20 |
-
from .core import (
|
| 21 |
-
obter_colunas_numericas,
|
| 22 |
-
calcular_estatisticas_variaveis,
|
| 23 |
-
ajustar_modelo,
|
| 24 |
-
buscar_melhores_transformacoes,
|
| 25 |
-
testar_micronumerosidade,
|
| 26 |
-
detectar_dicotomicas,
|
| 27 |
-
detectar_codigo_alocado,
|
| 28 |
-
detectar_percentuais,
|
| 29 |
-
exportar_modelo_dai,
|
| 30 |
-
avaliar_imovel,
|
| 31 |
-
exportar_avaliacoes_excel,
|
| 32 |
-
verificar_multicolinearidade,
|
| 33 |
-
)
|
| 34 |
-
from .formatadores import formatar_avaliacao_html
|
| 35 |
-
from .charts import (
|
| 36 |
-
criar_graficos_dispersao,
|
| 37 |
-
criar_graficos_dispersao_residuos,
|
| 38 |
-
criar_painel_diagnostico,
|
| 39 |
-
criar_matriz_correlacao,
|
| 40 |
-
)
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
# ============================================================
|
| 44 |
-
# CONSTANTES
|
| 45 |
-
# ============================================================
|
| 46 |
-
|
| 47 |
-
MAX_VARS_X = 20
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
# ============================================================
|
| 51 |
-
# UTILITÁRIOS DE TRANSFORMAÇÃO
|
| 52 |
-
# ============================================================
|
| 53 |
-
|
| 54 |
-
def obter_transformacoes_dos_dropdowns(colunas_x, *valores_dropdowns):
|
| 55 |
-
"""Coleta transformações dos valores dos dropdowns."""
|
| 56 |
-
transformacoes = {}
|
| 57 |
-
for i, col in enumerate(colunas_x):
|
| 58 |
-
if i < len(valores_dropdowns) and valores_dropdowns[i]:
|
| 59 |
-
transformacoes[col] = valores_dropdowns[i]
|
| 60 |
-
else:
|
| 61 |
-
transformacoes[col] = "(x)"
|
| 62 |
-
return transformacoes
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def atualizar_campos_transformacoes(df, colunas_x, dicotomicas=None, coluna_y=None):
|
| 66 |
-
"""Atualiza visibilidade e valores dos campos de transformação.
|
| 67 |
-
|
| 68 |
-
CONTRACT: Retorna 66 itens para transf_y_row(1) + transf_y_col(1) + transf_y_label(1)
|
| 69 |
-
+ transf_x_rows(3) + transf_x_columns(20) + transf_x_labels(20) + transf_x_dropdowns(20).
|
| 70 |
-
Se alterar, atualizar: app.py (outputs de checkboxes_x.change e btn_aplicar_selecao_x.click)
|
| 71 |
-
+ _atualizar_campos_transformacoes_com_flag (neste arquivo)
|
| 72 |
-
+ aplicar_selecao_callback (neste arquivo, campos_vazios).
|
| 73 |
-
"""
|
| 74 |
-
n_rows = (MAX_VARS_X + 7) // 8 # 3 rows (8 cards por linha)
|
| 75 |
-
|
| 76 |
-
# Y card updates
|
| 77 |
-
if coluna_y and df is not None and colunas_x:
|
| 78 |
-
y_label_html = f'<span class="transf-label-text transf-label-y">{coluna_y} (Y)</span>'
|
| 79 |
-
y_updates = [
|
| 80 |
-
gr.update(visible=True), # transf_y_row
|
| 81 |
-
gr.update(visible=True), # transf_y_col
|
| 82 |
-
gr.update(value=y_label_html, visible=True), # transf_y_label
|
| 83 |
-
]
|
| 84 |
-
else:
|
| 85 |
-
y_updates = [
|
| 86 |
-
gr.update(visible=False), # transf_y_row
|
| 87 |
-
gr.update(visible=False), # transf_y_col
|
| 88 |
-
gr.update(value="", visible=False), # transf_y_label
|
| 89 |
-
]
|
| 90 |
-
|
| 91 |
-
if df is None or not colunas_x:
|
| 92 |
-
# Esconde todos
|
| 93 |
-
row_updates = [gr.update(visible=False)] * n_rows
|
| 94 |
-
column_updates = [gr.update(visible=False)] * MAX_VARS_X
|
| 95 |
-
label_updates = [gr.update(value="", visible=False)] * MAX_VARS_X
|
| 96 |
-
dropdown_updates = [gr.update(value="(x)", interactive=True, visible=False)] * MAX_VARS_X
|
| 97 |
-
return y_updates + row_updates + column_updates + label_updates + dropdown_updates
|
| 98 |
-
|
| 99 |
-
if dicotomicas is None:
|
| 100 |
-
dicotomicas = detectar_dicotomicas(df, colunas_x)
|
| 101 |
-
|
| 102 |
-
# Updates para rows (visibilidade) - 8 por linha
|
| 103 |
-
row_updates = []
|
| 104 |
-
for i in range(n_rows):
|
| 105 |
-
idx_base = i * 8
|
| 106 |
-
visivel = idx_base < len(colunas_x)
|
| 107 |
-
row_updates.append(gr.update(visible=visivel))
|
| 108 |
-
|
| 109 |
-
# Updates para columns, labels e dropdowns
|
| 110 |
-
column_updates = []
|
| 111 |
-
label_updates = []
|
| 112 |
-
dropdown_updates = []
|
| 113 |
-
|
| 114 |
-
for i in range(MAX_VARS_X):
|
| 115 |
-
if i < len(colunas_x):
|
| 116 |
-
col = colunas_x[i]
|
| 117 |
-
eh_marcada = col in dicotomicas
|
| 118 |
-
# Só trava transformação se variável marcada E tem valores zero (dicotômica 0/1)
|
| 119 |
-
travar = False
|
| 120 |
-
if eh_marcada and col in df.columns:
|
| 121 |
-
valores_col = set(df[col].dropna().unique())
|
| 122 |
-
travar = 0 in valores_col or 0.0 in valores_col
|
| 123 |
-
column_updates.append(gr.update(visible=True))
|
| 124 |
-
label_updates.append(gr.update(value=f'<span class="transf-label-text">{col}</span>', visible=True))
|
| 125 |
-
dropdown_updates.append(gr.update(
|
| 126 |
-
value="(x)",
|
| 127 |
-
interactive=not travar,
|
| 128 |
-
visible=True
|
| 129 |
-
))
|
| 130 |
-
else:
|
| 131 |
-
column_updates.append(gr.update(visible=False))
|
| 132 |
-
label_updates.append(gr.update(value="", visible=False))
|
| 133 |
-
dropdown_updates.append(gr.update(value="(x)", interactive=True, visible=False))
|
| 134 |
-
|
| 135 |
-
return y_updates + row_updates + column_updates + label_updates + dropdown_updates
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
def _atualizar_campos_transformacoes_com_flag(df, colunas_x, flag_carregamento, coluna_y=None):
|
| 139 |
-
"""Wrapper que pula reset de transformações durante carregamento programático.
|
| 140 |
-
|
| 141 |
-
CONTRACT: Retorna 70 itens (66 campos + estado_flag_carregamento + 3 checkboxes).
|
| 142 |
-
Se alterar, atualizar: app.py (outputs de checkboxes_x.change).
|
| 143 |
-
"""
|
| 144 |
-
# Flag é um contador inteiro: decrementa a cada side-effect processado
|
| 145 |
-
if flag_carregamento:
|
| 146 |
-
n_items = 3 + (MAX_VARS_X + 7) // 8 + MAX_VARS_X + MAX_VARS_X + MAX_VARS_X # 66
|
| 147 |
-
return [gr.update() for _ in range(n_items)] + [max(0, int(flag_carregamento) - 1), gr.update(), gr.update(), gr.update()]
|
| 148 |
-
|
| 149 |
-
if df is not None and colunas_x:
|
| 150 |
-
dicotomicas_01 = detectar_dicotomicas(df, colunas_x)
|
| 151 |
-
codigo_alocado = detectar_codigo_alocado(df, colunas_x)
|
| 152 |
-
percentuais = detectar_percentuais(df, colunas_x)
|
| 153 |
-
else:
|
| 154 |
-
dicotomicas_01 = []
|
| 155 |
-
codigo_alocado = []
|
| 156 |
-
percentuais = []
|
| 157 |
-
|
| 158 |
-
todas_marcadas = dicotomicas_01 + codigo_alocado + percentuais
|
| 159 |
-
campos = atualizar_campos_transformacoes(df, colunas_x, todas_marcadas, coluna_y=coluna_y)
|
| 160 |
-
choices = list(colunas_x) if colunas_x else []
|
| 161 |
-
vis = bool(colunas_x)
|
| 162 |
-
return campos + [
|
| 163 |
-
False,
|
| 164 |
-
gr.update(choices=choices, value=dicotomicas_01, visible=vis),
|
| 165 |
-
gr.update(choices=choices, value=codigo_alocado, visible=vis),
|
| 166 |
-
gr.update(choices=choices, value=percentuais, visible=vis),
|
| 167 |
-
]
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
# ============================================================
|
| 171 |
-
# HANDLERS DE MUDANÇA DE SELEÇÃO
|
| 172 |
-
# ============================================================
|
| 173 |
-
|
| 174 |
-
def ao_mudar_tipo_grafico(tipo, resultado_modelo):
|
| 175 |
-
"""Atualiza o gráfico de dispersão com base na seleção do dropdown."""
|
| 176 |
-
if not resultado_modelo:
|
| 177 |
-
return None
|
| 178 |
-
|
| 179 |
-
try:
|
| 180 |
-
X_transf = resultado_modelo["X_transformado"]
|
| 181 |
-
|
| 182 |
-
if "Resíduo" in tipo:
|
| 183 |
-
# Gráfico X vs Resíduos
|
| 184 |
-
tabela = resultado_modelo.get("tabela_obs_calc")
|
| 185 |
-
if tabela is not None and "Resíduo Pad." in tabela.columns:
|
| 186 |
-
residuos = tabela["Resíduo Pad."].values
|
| 187 |
-
return criar_graficos_dispersao_residuos(X_transf, residuos)
|
| 188 |
-
else:
|
| 189 |
-
return None
|
| 190 |
-
else:
|
| 191 |
-
# Gráfico X vs Y (Padrão)
|
| 192 |
-
y_transf = resultado_modelo["y_transformado"]
|
| 193 |
-
return criar_graficos_dispersao(X_transf, y_transf)
|
| 194 |
-
|
| 195 |
-
except Exception as e:
|
| 196 |
-
print(f"Erro ao atualizar gráfico: {e}")
|
| 197 |
-
return None
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
def ao_mudar_y_sem_estatisticas(df, coluna_y, flag_carregamento=False, mostrar_secao_x=False):
|
| 201 |
-
"""Callback quando variável y é alterada (sem calcular estatísticas).
|
| 202 |
-
|
| 203 |
-
Apenas atualiza os checkboxes de X disponíveis.
|
| 204 |
-
As estatísticas são calculadas apenas ao clicar em 'Aplicar Seleção' na seção 4.
|
| 205 |
-
|
| 206 |
-
Args:
|
| 207 |
-
df: DataFrame com os dados
|
| 208 |
-
coluna_y: Nome da coluna Y selecionada
|
| 209 |
-
flag_carregamento: Se True, mudança veio de carregamento programático (skip reset)
|
| 210 |
-
mostrar_secao_x: Se True, mostra a seção 4 (usado pelo botão Aplicar da seção 3)
|
| 211 |
-
"""
|
| 212 |
-
# Se mudança veio de carregamento programático, não sobrescrever valores
|
| 213 |
-
# Flag é um contador inteiro: decrementa a cada side-effect processado
|
| 214 |
-
if flag_carregamento:
|
| 215 |
-
return (
|
| 216 |
-
gr.update(), # checkboxes_x — manter valor do carregamento
|
| 217 |
-
gr.update(), # header_secao_4 — manter visibilidade
|
| 218 |
-
gr.update(), # accordion_secao_4 — manter visibilidade
|
| 219 |
-
max(0, int(flag_carregamento) - 1), # estado_flag_carregamento — decrementar
|
| 220 |
-
)
|
| 221 |
-
|
| 222 |
-
if df is None or coluna_y is None:
|
| 223 |
-
return (
|
| 224 |
-
gr.update(choices=[]), # checkboxes_x
|
| 225 |
-
gr.update(visible=False), # header_secao_4
|
| 226 |
-
gr.update(visible=False), # accordion_secao_4
|
| 227 |
-
False, # estado_flag_carregamento
|
| 228 |
-
)
|
| 229 |
-
|
| 230 |
-
# Lista de X disponíveis (exclui y) - todas marcadas por padrão
|
| 231 |
-
colunas_x = [col for col in obter_colunas_numericas(df) if col != coluna_y]
|
| 232 |
-
|
| 233 |
-
return (
|
| 234 |
-
gr.update(choices=colunas_x, value=colunas_x), # checkboxes_x
|
| 235 |
-
gr.update(visible=mostrar_secao_x), # header_secao_4
|
| 236 |
-
gr.update(visible=mostrar_secao_x, open=mostrar_secao_x), # accordion_secao_4
|
| 237 |
-
False, # estado_flag_carregamento
|
| 238 |
-
)
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
# ============================================================
|
| 242 |
-
# ESTATÍSTICAS
|
| 243 |
-
# ============================================================
|
| 244 |
-
|
| 245 |
-
def atualizar_estatisticas_auto(df_filtrado, coluna_y, colunas_x):
|
| 246 |
-
"""Recalcula estatísticas automaticamente e retorna timestamp.
|
| 247 |
-
|
| 248 |
-
CONTRACT: Retorna 2 itens (estatisticas, timestamp).
|
| 249 |
-
Se alterar, atualizar: outliers.py (reiniciar_iteracao_callback, destructuring).
|
| 250 |
-
"""
|
| 251 |
-
if df_filtrado is None or coluna_y is None:
|
| 252 |
-
return None, None
|
| 253 |
-
|
| 254 |
-
# Filtra apenas colunas selecionadas + Y
|
| 255 |
-
colunas_usar = [coluna_y] + list(colunas_x) if colunas_x else [coluna_y]
|
| 256 |
-
colunas_disponiveis = [c for c in colunas_usar if c in df_filtrado.columns]
|
| 257 |
-
|
| 258 |
-
if not colunas_disponiveis:
|
| 259 |
-
return None, None
|
| 260 |
-
|
| 261 |
-
estatisticas = calcular_estatisticas_variaveis(df_filtrado, coluna_y, colunas=colunas_disponiveis)
|
| 262 |
-
gmt_minus_3 = timezone(timedelta(hours=-3))
|
| 263 |
-
timestamp = datetime.now(gmt_minus_3).strftime("%H:%M:%S")
|
| 264 |
-
|
| 265 |
-
return estatisticas.round(4), timestamp
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
# ============================================================
|
| 269 |
-
# CALLBACKS PRINCIPAIS
|
| 270 |
-
# ============================================================
|
| 271 |
-
|
| 272 |
-
def ajustar_modelo_callback(df, coluna_y, colunas_x, transformacao_y, outliers_anteriores, dicotomicas, codigo_alocado, percentuais, *valores_dropdowns):
|
| 273 |
-
"""Callback para ajustar o modelo e calcular métricas de outliers.
|
| 274 |
-
|
| 275 |
-
CONTRACT: Retorna 33 itens para _outputs_ajustar (app.py:criar_aba).
|
| 276 |
-
Se alterar, atualizar: _outputs_ajustar em app.py + carregamento.py (indices em carregar_dados_de_dai).
|
| 277 |
-
"""
|
| 278 |
-
# Variáveis disponíveis para filtro (métricas + todas as colunas originais do DataFrame)
|
| 279 |
-
colunas_metricas = {"Observado", "Calculado", "Resíduo", "Resíduo Pad.", "Resíduo Stud.", "Cook"}
|
| 280 |
-
colunas_originais = [c for c in df.columns if c not in colunas_metricas] if df is not None else []
|
| 281 |
-
variaveis_filtro = ["Resíduo Pad.", "Resíduo Stud.", "Cook"] + colunas_originais
|
| 282 |
-
filtro_var_updates = [gr.update(choices=variaveis_filtro)] * 4
|
| 283 |
-
|
| 284 |
-
# Updates para seções 10-16 ocultas (7 seções × 2 = 14 itens)
|
| 285 |
-
secoes_ocultas = (
|
| 286 |
-
gr.update(visible=False), gr.update(visible=False), # header_secao_10, accordion_secao_10
|
| 287 |
-
gr.update(visible=False), gr.update(visible=False), # header_secao_11, accordion_secao_11
|
| 288 |
-
gr.update(visible=False), gr.update(visible=False), # header_secao_12, accordion_secao_12
|
| 289 |
-
gr.update(visible=False), gr.update(visible=False), # header_secao_13, accordion_secao_13
|
| 290 |
-
gr.update(visible=False), gr.update(visible=False), # header_secao_14, accordion_secao_14
|
| 291 |
-
gr.update(visible=False), gr.update(visible=False), # header_secao_15, accordion_secao_15 (Avaliação)
|
| 292 |
-
gr.update(visible=False), gr.update(visible=False), # header_secao_16, accordion_secao_16 (Exportar)
|
| 293 |
-
)
|
| 294 |
-
|
| 295 |
-
if df is None or coluna_y is None or not colunas_x:
|
| 296 |
-
return (
|
| 297 |
-
None, # estado_modelo
|
| 298 |
-
"", # diagnosticos_html
|
| 299 |
-
None, # tabela_coef
|
| 300 |
-
None, # tabela_obs_calc
|
| 301 |
-
None, # plot_dispersao_transf
|
| 302 |
-
gr.update(visible=False), # dropdown_tipo_grafico_dispersao
|
| 303 |
-
None, None, None, None, None, # gráficos
|
| 304 |
-
None, # tabela_metricas
|
| 305 |
-
None, # estado_metricas
|
| 306 |
-
f"Excluídos: {len(outliers_anteriores) if outliers_anteriores else 0} | A excluir: 0 | A reincluir: 0 | Total: {len(outliers_anteriores) if outliers_anteriores else 0}", # txt_resumo_outliers
|
| 307 |
-
[], # estado_avaliacoes (reset)
|
| 308 |
-
*secoes_ocultas, # seções 10-16 ocultas
|
| 309 |
-
*filtro_var_updates # atualiza dropdowns de filtro
|
| 310 |
-
)
|
| 311 |
-
|
| 312 |
-
# Extrai transformações dos dropdowns
|
| 313 |
-
transformacoes_x = obter_transformacoes_dos_dropdowns(colunas_x, *valores_dropdowns)
|
| 314 |
-
|
| 315 |
-
# Ajusta modelo
|
| 316 |
-
resultado = ajustar_modelo(
|
| 317 |
-
df, coluna_y, colunas_x,
|
| 318 |
-
transformacao_y, transformacoes_x
|
| 319 |
-
)
|
| 320 |
-
|
| 321 |
-
if resultado is None:
|
| 322 |
-
return (
|
| 323 |
-
None, # estado_modelo
|
| 324 |
-
"", # diagnosticos_html
|
| 325 |
-
None, # tabela_coef
|
| 326 |
-
None, # tabela_obs_calc
|
| 327 |
-
None, # plot_dispersao_transf
|
| 328 |
-
gr.update(visible=False), # dropdown_tipo_grafico_dispersao
|
| 329 |
-
None, None, None, None, None, # gráficos
|
| 330 |
-
None, # tabela_metricas
|
| 331 |
-
None, # estado_metricas
|
| 332 |
-
f"Outliers anteriores: {len(outliers_anteriores) if outliers_anteriores else 0} | Excluir: 0 | Reincluir: 0 | Total: {len(outliers_anteriores) if outliers_anteriores else 0}",
|
| 333 |
-
[], # estado_avaliacoes (reset)
|
| 334 |
-
*secoes_ocultas, # seções 10-16 ocultas
|
| 335 |
-
*filtro_var_updates # atualiza dropdowns de filtro
|
| 336 |
-
)
|
| 337 |
-
|
| 338 |
-
# Formata diagnósticos
|
| 339 |
-
diagnosticos_html = formatar_diagnosticos_html(resultado["diagnosticos"])
|
| 340 |
-
gmt_minus_3 = timezone(timedelta(hours=-3))
|
| 341 |
-
timestamp = datetime.now(gmt_minus_3).strftime("%H:%M:%S")
|
| 342 |
-
|
| 343 |
-
# Gráficos de dispersão com variáveis transformadas
|
| 344 |
-
try:
|
| 345 |
-
X_transf = resultado["X_transformado"]
|
| 346 |
-
y_transf = resultado["y_transformado"]
|
| 347 |
-
fig_dispersao_transf = criar_graficos_dispersao(X_transf, y_transf)
|
| 348 |
-
except Exception as e:
|
| 349 |
-
print(f"Erro ao gerar gráficos de dispersão transformados: {e}")
|
| 350 |
-
fig_dispersao_transf = None
|
| 351 |
-
|
| 352 |
-
# Gr��ficos de diagnóstico
|
| 353 |
-
graficos = criar_painel_diagnostico(resultado)
|
| 354 |
-
|
| 355 |
-
# Correlação (com variáveis transformadas)
|
| 356 |
-
try:
|
| 357 |
-
X_transf_corr = resultado["X_transformado"].copy()
|
| 358 |
-
y_transf_corr = resultado["y_transformado"].copy()
|
| 359 |
-
|
| 360 |
-
# Cria DataFrame temporário para correlação
|
| 361 |
-
df_corr_temp = X_transf_corr
|
| 362 |
-
# Adiciona y (garantindo nome correto)
|
| 363 |
-
df_corr_temp[coluna_y] = y_transf_corr
|
| 364 |
-
|
| 365 |
-
colunas_corr = [coluna_y] + list(colunas_x)
|
| 366 |
-
fig_corr = criar_matriz_correlacao(df_corr_temp, colunas_corr, coluna_y=coluna_y)
|
| 367 |
-
except Exception as e:
|
| 368 |
-
print(f"Erro ao gerar matriz de correlação transformada: {e}")
|
| 369 |
-
fig_corr = None
|
| 370 |
-
|
| 371 |
-
# Extrai métricas de outliers da tabela_obs_calc (já contém resíduos, Cook, etc.)
|
| 372 |
-
tabela_metricas = resultado["tabela_obs_calc"].copy()
|
| 373 |
-
|
| 374 |
-
# Para o estado de filtros:
|
| 375 |
-
# 1. Usa set_index para que o índice do DataFrame seja os índices originais
|
| 376 |
-
# 2. Adiciona as variáveis X para permitir filtros por variáveis
|
| 377 |
-
tabela_metricas_estado = tabela_metricas.set_index("Índice")
|
| 378 |
-
|
| 379 |
-
# Adiciona todas as colunas originais ao estado para filtros
|
| 380 |
-
indices_usados = resultado["indices_usados"]
|
| 381 |
-
df_filtrado = df.loc[indices_usados]
|
| 382 |
-
colunas_novas = {
|
| 383 |
-
col: df_filtrado[col].values
|
| 384 |
-
for col in df.columns
|
| 385 |
-
if col not in tabela_metricas_estado.columns
|
| 386 |
-
}
|
| 387 |
-
if colunas_novas:
|
| 388 |
-
tabela_metricas_estado = pd.concat(
|
| 389 |
-
[tabela_metricas_estado, pd.DataFrame(colunas_novas, index=tabela_metricas_estado.index)],
|
| 390 |
-
axis=1
|
| 391 |
-
)
|
| 392 |
-
|
| 393 |
-
# Resumo de outliers
|
| 394 |
-
n_anteriores = len(outliers_anteriores) if outliers_anteriores else 0
|
| 395 |
-
resumo = f"Excluídos: {n_anteriores} | A excluir: 0 | A reincluir: 0 | Total: {n_anteriores}"
|
| 396 |
-
|
| 397 |
-
# Updates para seções 10-16 visíveis com timestamp nos headers (7 seções × 2 = 14 itens)
|
| 398 |
-
secoes_visiveis = (
|
| 399 |
-
gr.update(visible=True, value=criar_header_secao(10, "Gráficos de Dispersão com Modelo Ajustado", timestamp)), # header_secao_10
|
| 400 |
-
gr.update(visible=True, open=True), # accordion_secao_10
|
| 401 |
-
gr.update(visible=True, value=criar_header_secao(11, "Diagnóstico de Modelo", timestamp)), # header_secao_11
|
| 402 |
-
gr.update(visible=True, open=True), # accordion_secao_11
|
| 403 |
-
gr.update(visible=True, value=criar_header_secao(12, "Gráficos de Diagnóstico do Modelo", timestamp)), # header_secao_12
|
| 404 |
-
gr.update(visible=True, open=True), # accordion_secao_12
|
| 405 |
-
gr.update(visible=True, value=criar_header_secao(13, "Analisar Outliers", timestamp)), # header_secao_13
|
| 406 |
-
gr.update(visible=True, open=True), # accordion_secao_13
|
| 407 |
-
gr.update(visible=True, value=criar_header_secao(14, "Exclusão ou Reinclusão de Outliers", timestamp)), # header_secao_14
|
| 408 |
-
gr.update(visible=True, open=True), # accordion_secao_14
|
| 409 |
-
gr.update(visible=True, value=criar_header_secao(15, "Avaliação de Imóvel", timestamp)), # header_secao_15
|
| 410 |
-
gr.update(visible=True, open=True), # accordion_secao_15
|
| 411 |
-
gr.update(visible=True, value=criar_header_secao(16, "Exportar Modelo", timestamp)), # header_secao_16
|
| 412 |
-
gr.update(visible=True, open=True), # accordion_secao_16
|
| 413 |
-
)
|
| 414 |
-
|
| 415 |
-
resultado["dicotomicas"] = list(dicotomicas) if dicotomicas else []
|
| 416 |
-
resultado["codigo_alocado"] = list(codigo_alocado) if codigo_alocado else []
|
| 417 |
-
resultado["percentuais"] = list(percentuais) if percentuais else []
|
| 418 |
-
|
| 419 |
-
return (
|
| 420 |
-
resultado,
|
| 421 |
-
diagnosticos_html,
|
| 422 |
-
resultado["tabela_coef"].round(4),
|
| 423 |
-
resultado["tabela_obs_calc"].round(4),
|
| 424 |
-
fig_dispersao_transf, # plot_dispersao_transf
|
| 425 |
-
gr.update(value="Variáveis Independentes Transformadas X Variável Dependente Transformada", visible=True), # dropdown_tipo_grafico_dispersao
|
| 426 |
-
graficos.get("obs_calc"),
|
| 427 |
-
graficos.get("residuos"),
|
| 428 |
-
graficos.get("histograma"),
|
| 429 |
-
graficos.get("cook"),
|
| 430 |
-
fig_corr,
|
| 431 |
-
tabela_metricas.round(4), # tabela_metricas (para exibição - com coluna Índice)
|
| 432 |
-
tabela_metricas_estado, # estado_metricas (com set_index - para filtros)
|
| 433 |
-
resumo, # txt_resumo_outliers
|
| 434 |
-
[], # estado_avaliacoes (reset ao re-ajustar)
|
| 435 |
-
*secoes_visiveis, # seções 10-16 visíveis
|
| 436 |
-
*filtro_var_updates # atualiza dropdowns de filtro
|
| 437 |
-
)
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
def buscar_transformacoes_callback(df, coluna_y, colunas_x, dicotomicas=None, codigo_alocado=None, percentuais=None, grau_min_coef=1, grau_min_f=0):
|
| 441 |
-
"""Callback para busca automática de transformações.
|
| 442 |
-
|
| 443 |
-
CONTRACT: Retorna 8 itens (html, resultados, timestamp, btn1..btn5).
|
| 444 |
-
Se alterar, atualizar: outliers.py (reiniciar_iteracao_callback, destructuring)
|
| 445 |
-
+ app.py (outputs de btn_buscar_transf.click).
|
| 446 |
-
"""
|
| 447 |
-
btn_hidden = [gr.update(visible=False)] * 5
|
| 448 |
-
|
| 449 |
-
if df is None or coluna_y is None or not colunas_x:
|
| 450 |
-
return ("<p>Selecione as variáveis primeiro.</p>", [], "", *btn_hidden)
|
| 451 |
-
|
| 452 |
-
# Verifica se colunas têm dados válidos (não 100% NaN)
|
| 453 |
-
colunas_vazias = []
|
| 454 |
-
for col in colunas_x:
|
| 455 |
-
if col in df.columns and df[col].isna().all():
|
| 456 |
-
colunas_vazias.append(col)
|
| 457 |
-
|
| 458 |
-
if colunas_vazias:
|
| 459 |
-
msg = f"<p style='color: red;'><b>Erro:</b> As seguintes colunas estão completamente vazias (sem dados): <b>{', '.join(colunas_vazias)}</b></p>"
|
| 460 |
-
msg += "<p>Selecione apenas variáveis que contenham dados válidos.</p>"
|
| 461 |
-
return (msg, [], "", *btn_hidden)
|
| 462 |
-
|
| 463 |
-
# Verifica se Y tem dados válidos
|
| 464 |
-
if coluna_y in df.columns and df[coluna_y].isna().all():
|
| 465 |
-
msg = f"<p style='color: red;'><b>Erro:</b> A variável dependente <b>{coluna_y}</b> está completamente vazia.</p>"
|
| 466 |
-
return (msg, [], "", *btn_hidden)
|
| 467 |
-
|
| 468 |
-
# Fixa dicotômicas e percentuais em (x) — variáveis categóricas codificadas ficam livres
|
| 469 |
-
transformacoes_fixas = {}
|
| 470 |
-
if dicotomicas is None:
|
| 471 |
-
dicotomicas = detectar_dicotomicas(df, colunas_x)
|
| 472 |
-
if percentuais is None:
|
| 473 |
-
percentuais = detectar_percentuais(df, colunas_x)
|
| 474 |
-
for col in (dicotomicas or []) + (percentuais or []):
|
| 475 |
-
transformacoes_fixas[col] = "(x)"
|
| 476 |
-
|
| 477 |
-
# Busca melhores transformações
|
| 478 |
-
resultados = buscar_melhores_transformacoes(
|
| 479 |
-
df, coluna_y, colunas_x,
|
| 480 |
-
transformacoes_fixas=transformacoes_fixas,
|
| 481 |
-
top_n=5,
|
| 482 |
-
grau_min_coef=int(grau_min_coef),
|
| 483 |
-
grau_min_f=int(grau_min_f)
|
| 484 |
-
)
|
| 485 |
-
|
| 486 |
-
if not resultados:
|
| 487 |
-
msg = (
|
| 488 |
-
"<p style='color: orange;'><b>Aviso:</b> Nenhuma combinação de transformações resultou "
|
| 489 |
-
"em todos os coeficientes com ao menos Grau I de significância (p ≤ 30%).</p>"
|
| 490 |
-
"<p>Considere revisar as variáveis selecionadas ou verificar a qualidade dos dados.</p>"
|
| 491 |
-
)
|
| 492 |
-
return (msg, [], "", *btn_hidden)
|
| 493 |
-
|
| 494 |
-
html = formatar_busca_html(resultados)
|
| 495 |
-
gmt_minus_3 = timezone(timedelta(hours=-3))
|
| 496 |
-
timestamp = datetime.now(gmt_minus_3).strftime("%H:%M:%S")
|
| 497 |
-
|
| 498 |
-
# Atualiza visibilidade dos botões "Adotar"
|
| 499 |
-
btn_updates = [gr.update(visible=(i < len(resultados))) for i in range(5)]
|
| 500 |
-
|
| 501 |
-
return (html, resultados, timestamp, *btn_updates)
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
def aplicar_selecao_callback(df, coluna_y, colunas_x, outliers_anteriores, dicotomicas=None, codigo_alocado=None, percentuais=None):
|
| 505 |
-
"""Aplica seleção de variáveis, atualiza estatísticas e busca transformações automaticamente.
|
| 506 |
-
|
| 507 |
-
NÃO calcula métricas de outliers aqui - isso é feito ao ajustar o modelo.
|
| 508 |
-
Filtra dados excluindo outliers de iterações anteriores.
|
| 509 |
-
Gera gráficos de dispersão e teste de micronumerosidade.
|
| 510 |
-
Busca transformações com fallback automático (começa em Grau III/III e reduz se necessário).
|
| 511 |
-
|
| 512 |
-
CONTRACT: Retorna 90 itens para btn_aplicar_selecao_x.click (app.py:criar_aba).
|
| 513 |
-
Se alterar, atualizar: app.py (outputs) + carregamento.py (indices em carregar_dados_de_dai).
|
| 514 |
-
"""
|
| 515 |
-
n_rows = (MAX_VARS_X + 7) // 8 # 3 rows (8 por linha)
|
| 516 |
-
|
| 517 |
-
# Valores padrão quando não há dados
|
| 518 |
-
campos_vazios = (
|
| 519 |
-
[gr.update(visible=False)] * 3 + # 3: transf_y_row, transf_y_col, transf_y_label
|
| 520 |
-
[gr.update(visible=False)] * n_rows + # 3: transf_x_rows
|
| 521 |
-
[gr.update(visible=False)] * MAX_VARS_X + # 20: transf_x_columns
|
| 522 |
-
[gr.update(value="", visible=False)] * MAX_VARS_X + # 20: transf_x_labels
|
| 523 |
-
[gr.update(value="(x)", interactive=True, visible=False)] * MAX_VARS_X # 20: transf_x_dropdowns
|
| 524 |
-
)
|
| 525 |
-
|
| 526 |
-
# Botões de adotar escondidos por padrão
|
| 527 |
-
btn_hidden = [gr.update(visible=False)] * 5
|
| 528 |
-
|
| 529 |
-
# Valores padrão para dispersão e micronumerosidade
|
| 530 |
-
dispersao_vazio = None
|
| 531 |
-
micro_vazio = "<p>Clique em 'Aplicar Seleção' para verificar micronumerosidade.</p>"
|
| 532 |
-
|
| 533 |
-
# Updates para seções 5-9 ocultas
|
| 534 |
-
secoes_ocultas = (
|
| 535 |
-
gr.update(visible=False), gr.update(visible=False), # header_secao_5, accordion_secao_5
|
| 536 |
-
gr.update(visible=False), gr.update(visible=False), # header_secao_6, accordion_secao_6
|
| 537 |
-
gr.update(visible=False), gr.update(visible=False), # header_secao_7, accordion_secao_7
|
| 538 |
-
gr.update(visible=False), gr.update(visible=False), # header_secao_8, accordion_secao_8
|
| 539 |
-
gr.update(visible=False), gr.update(visible=False), # header_secao_9, accordion_secao_9
|
| 540 |
-
)
|
| 541 |
-
|
| 542 |
-
if df is None or coluna_y is None:
|
| 543 |
-
return (
|
| 544 |
-
None, # estado_df_filtrado
|
| 545 |
-
gr.update(value=None), # tabela_estatisticas
|
| 546 |
-
micro_vazio, # html_micronumerosidade
|
| 547 |
-
dispersao_vazio, # plot_dispersao
|
| 548 |
-
gr.update(), # slider_grau_coef (no-op)
|
| 549 |
-
gr.update(), # slider_grau_f (no-op)
|
| 550 |
-
"", # busca_html
|
| 551 |
-
[], # estado_resultados_busca
|
| 552 |
-
*btn_hidden, # botões adotar
|
| 553 |
-
*secoes_ocultas, # seções 5-9 ocultas
|
| 554 |
-
*campos_vazios, # campos de transformação
|
| 555 |
-
gr.update(value="", visible=False), # html_aviso_multicolinearidade
|
| 556 |
-
)
|
| 557 |
-
|
| 558 |
-
if not colunas_x:
|
| 559 |
-
return (
|
| 560 |
-
df,
|
| 561 |
-
gr.update(value=None), # tabela_estatisticas
|
| 562 |
-
micro_vazio, # html_micronumerosidade
|
| 563 |
-
dispersao_vazio, # plot_dispersao
|
| 564 |
-
gr.update(), # slider_grau_coef (no-op)
|
| 565 |
-
gr.update(), # slider_grau_f (no-op)
|
| 566 |
-
"", # busca_html
|
| 567 |
-
[], # estado_resultados_busca
|
| 568 |
-
*btn_hidden, # botões adotar
|
| 569 |
-
*secoes_ocultas, # seções 5-9 ocultas
|
| 570 |
-
*campos_vazios, # campos de transformação
|
| 571 |
-
gr.update(value="", visible=False), # html_aviso_multicolinearidade
|
| 572 |
-
)
|
| 573 |
-
|
| 574 |
-
# Filtra dados excluindo outliers de iterações anteriores
|
| 575 |
-
df_filtrado = df.copy()
|
| 576 |
-
if outliers_anteriores:
|
| 577 |
-
df_filtrado = df_filtrado.drop(index=outliers_anteriores, errors='ignore')
|
| 578 |
-
|
| 579 |
-
# Calcula estatísticas com dados filtrados
|
| 580 |
-
estatisticas, _ = atualizar_estatisticas_auto(df_filtrado, coluna_y, colunas_x)
|
| 581 |
-
|
| 582 |
-
# Timestamp para as novas seções
|
| 583 |
-
gmt_minus_3 = timezone(timedelta(hours=-3))
|
| 584 |
-
timestamp = datetime.now(gmt_minus_3).strftime("%H:%M:%S")
|
| 585 |
-
|
| 586 |
-
# Teste de micronumerosidade
|
| 587 |
-
try:
|
| 588 |
-
resultado_micro = testar_micronumerosidade(df_filtrado, list(colunas_x),
|
| 589 |
-
dicotomicas=dicotomicas, codigo_alocado=codigo_alocado)
|
| 590 |
-
html_micro = formatar_micronumerosidade_html(resultado_micro)
|
| 591 |
-
except Exception as e:
|
| 592 |
-
print(f"Erro ao testar micronumerosidade: {e}")
|
| 593 |
-
html_micro = f"<p style='color: red;'>Erro ao calcular micronumerosidade: {e}</p>"
|
| 594 |
-
|
| 595 |
-
# Gera gráficos de dispersão
|
| 596 |
-
try:
|
| 597 |
-
X = df_filtrado[list(colunas_x)]
|
| 598 |
-
y = df_filtrado[coluna_y]
|
| 599 |
-
fig_dispersao = criar_graficos_dispersao(X, y)
|
| 600 |
-
except Exception as e:
|
| 601 |
-
print(f"Erro ao gerar gráficos de dispersão: {e}")
|
| 602 |
-
fig_dispersao = None
|
| 603 |
-
|
| 604 |
-
# Atualiza campos de transformação (junta todas as marcadas para lock)
|
| 605 |
-
todas_marcadas = (dicotomicas or []) + (codigo_alocado or []) + (percentuais or [])
|
| 606 |
-
campos_updates = atualizar_campos_transformacoes(df, colunas_x, todas_marcadas, coluna_y=coluna_y)
|
| 607 |
-
|
| 608 |
-
# Busca automática de transformações: critério mínimo (Grau I nos coeficientes, sem filtro F)
|
| 609 |
-
busca_html_result, resultados_busca, _, *btn_updates = buscar_transformacoes_callback(
|
| 610 |
-
df_filtrado, coluna_y, colunas_x, dicotomicas, codigo_alocado, percentuais,
|
| 611 |
-
grau_min_coef=1, grau_min_f=0
|
| 612 |
-
)
|
| 613 |
-
|
| 614 |
-
# Radio buttons refletem o grau mínimo REAL presente nos resultados
|
| 615 |
-
if resultados_busca:
|
| 616 |
-
grau_coef_usado = min(
|
| 617 |
-
min(r.get("graus_coef", {}).values(), default=0)
|
| 618 |
-
for r in resultados_busca
|
| 619 |
-
)
|
| 620 |
-
grau_f_usado = min(r.get("grau_f", 0) for r in resultados_busca)
|
| 621 |
-
else:
|
| 622 |
-
grau_coef_usado = 1
|
| 623 |
-
grau_f_usado = 0
|
| 624 |
-
|
| 625 |
-
# Updates para seções 5-9 visíveis com timestamp nos headers
|
| 626 |
-
secoes_visiveis = (
|
| 627 |
-
gr.update(visible=True, value=criar_header_secao(5, "Estatísticas das Variáveis Selecionadas", timestamp)), # header_secao_5
|
| 628 |
-
gr.update(visible=True, open=True), # accordion_secao_5
|
| 629 |
-
gr.update(visible=True, value=criar_header_secao(6, "Teste de Micronumerosidade (NBR 14.653-2)", timestamp)), # header_secao_6
|
| 630 |
-
gr.update(visible=True, open=True), # accordion_secao_6
|
| 631 |
-
gr.update(visible=True, value=criar_header_secao(7, "Gráficos de Dispersão das Variáveis Independentes", timestamp)), # header_secao_7
|
| 632 |
-
gr.update(visible=True, open=True), # accordion_secao_7
|
| 633 |
-
gr.update(visible=True, value=criar_header_secao(8, "Transformações Sugeridas", timestamp)), # header_secao_8
|
| 634 |
-
gr.update(visible=True, open=True), # accordion_secao_8
|
| 635 |
-
gr.update(visible=True, value=criar_header_secao(9, "Aplicação das Transformações", timestamp)), # header_secao_9
|
| 636 |
-
gr.update(visible=True, open=True), # accordion_secao_9
|
| 637 |
-
)
|
| 638 |
-
|
| 639 |
-
# Verifica multicolinearidade (dados brutos, antes das transformações da seção 9)
|
| 640 |
-
resultado_multi = verificar_multicolinearidade(df_filtrado, list(colunas_x))
|
| 641 |
-
html_multi, visivel_multi = formatar_aviso_multicolinearidade(resultado_multi)
|
| 642 |
-
|
| 643 |
-
return (
|
| 644 |
-
df_filtrado, # estado_df_filtrado
|
| 645 |
-
gr.update(value=estatisticas), # tabela_estatisticas
|
| 646 |
-
html_micro, # html_micronumerosidade
|
| 647 |
-
fig_dispersao, # plot_dispersao
|
| 648 |
-
gr.update(value=grau_coef_usado), # slider_grau_coef
|
| 649 |
-
gr.update(value=grau_f_usado), # slider_grau_f
|
| 650 |
-
busca_html_result, # busca_html
|
| 651 |
-
resultados_busca, # estado_resultados_busca
|
| 652 |
-
*btn_updates, # botões adotar
|
| 653 |
-
*secoes_visiveis, # seções 5-9 visíveis
|
| 654 |
-
*campos_updates, # campos de transformação
|
| 655 |
-
gr.update(value=html_multi, visible=visivel_multi), # html_aviso_multicolinearidade (item 90)
|
| 656 |
-
)
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
def adotar_sugestao(indice, resultados, colunas_x):
|
| 660 |
-
"""Preenche os dropdowns com a sugestão selecionada.
|
| 661 |
-
|
| 662 |
-
CONTRACT: Retorna 21 itens (1 transf_y + 20 transf_x_dropdowns).
|
| 663 |
-
Se alterar, atualizar: app.py (outputs de btn_adotar_N.click).
|
| 664 |
-
"""
|
| 665 |
-
if not resultados or indice >= len(resultados):
|
| 666 |
-
return [gr.update()] * (1 + MAX_VARS_X)
|
| 667 |
-
|
| 668 |
-
sugestao = resultados[indice]
|
| 669 |
-
|
| 670 |
-
# Update para transformação de Y
|
| 671 |
-
updates = [gr.update(value=sugestao["transformacao_y"])]
|
| 672 |
-
|
| 673 |
-
# Updates para transformações de X
|
| 674 |
-
transf_x = sugestao["transformacoes_x"]
|
| 675 |
-
for i in range(MAX_VARS_X):
|
| 676 |
-
if i < len(colunas_x):
|
| 677 |
-
col = colunas_x[i]
|
| 678 |
-
updates.append(gr.update(value=transf_x.get(col, "(x)")))
|
| 679 |
-
else:
|
| 680 |
-
updates.append(gr.update())
|
| 681 |
-
|
| 682 |
-
return updates
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
# ============================================================
|
| 686 |
-
# CALLBACKS DE AVALIAÇÃO (Seção 15)
|
| 687 |
-
# ============================================================
|
| 688 |
-
|
| 689 |
-
N_AVAL_ROWS = MAX_VARS_X // 4 # 5 rows (4 inputs por linha)
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
def popular_campos_avaliacao_callback(estado_modelo, tabela_estatisticas):
|
| 693 |
-
"""Popula inputs da seção 15 com labels dinâmicos após ajuste.
|
| 694 |
-
|
| 695 |
-
Chamado via .then() após ajustar_modelo_callback ou carregar .dai.
|
| 696 |
-
|
| 697 |
-
CONTRACT: Retorna 27 itens (5 aval_rows + 20 aval_inputs + resultado_html + dropdown_base).
|
| 698 |
-
Se alterar, atualizar: app.py (_outputs_popular_avaliacao).
|
| 699 |
-
"""
|
| 700 |
-
rows_hidden = [gr.update(visible=False)] * N_AVAL_ROWS
|
| 701 |
-
inputs_hidden = [gr.update(visible=False, value=None, label="")] * MAX_VARS_X
|
| 702 |
-
dropdown_reset = gr.update(choices=[], value=None)
|
| 703 |
-
|
| 704 |
-
if estado_modelo is None:
|
| 705 |
-
return (*rows_hidden, *inputs_hidden, "", dropdown_reset)
|
| 706 |
-
|
| 707 |
-
try:
|
| 708 |
-
colunas_x = estado_modelo["colunas_x"]
|
| 709 |
-
n_vars = len(colunas_x)
|
| 710 |
-
|
| 711 |
-
# Extrair min/max das estatísticas
|
| 712 |
-
import pandas as pd
|
| 713 |
-
if tabela_estatisticas is not None and isinstance(tabela_estatisticas, pd.DataFrame):
|
| 714 |
-
if "Variável" in tabela_estatisticas.columns:
|
| 715 |
-
est_idx = tabela_estatisticas.set_index("Variável")
|
| 716 |
-
else:
|
| 717 |
-
est_idx = tabela_estatisticas
|
| 718 |
-
else:
|
| 719 |
-
est_idx = pd.DataFrame()
|
| 720 |
-
|
| 721 |
-
# Rows visíveis
|
| 722 |
-
import math
|
| 723 |
-
n_rows_vis = math.ceil(n_vars / 4)
|
| 724 |
-
rows_updates = [gr.update(visible=(r < n_rows_vis)) for r in range(N_AVAL_ROWS)]
|
| 725 |
-
|
| 726 |
-
# Inputs com labels dinâmicos
|
| 727 |
-
dicotomicas = estado_modelo.get("dicotomicas", [])
|
| 728 |
-
codigo_alocado = estado_modelo.get("codigo_alocado", [])
|
| 729 |
-
percentuais = estado_modelo.get("percentuais", [])
|
| 730 |
-
inputs_updates = []
|
| 731 |
-
for i in range(MAX_VARS_X):
|
| 732 |
-
if i < n_vars:
|
| 733 |
-
col = colunas_x[i]
|
| 734 |
-
if col in dicotomicas:
|
| 735 |
-
placeholder = "0 ou 1"
|
| 736 |
-
elif col in codigo_alocado and col in est_idx.index:
|
| 737 |
-
min_val = est_idx.loc[col, "Mínimo"]
|
| 738 |
-
max_val = est_idx.loc[col, "Máximo"]
|
| 739 |
-
placeholder = f"cód. {int(min_val)} a {int(max_val)}"
|
| 740 |
-
elif col in percentuais:
|
| 741 |
-
placeholder = "0 a 1"
|
| 742 |
-
elif col in est_idx.index:
|
| 743 |
-
min_val = est_idx.loc[col, "Mínimo"]
|
| 744 |
-
max_val = est_idx.loc[col, "Máximo"]
|
| 745 |
-
placeholder = f"{min_val} — {max_val}"
|
| 746 |
-
else:
|
| 747 |
-
placeholder = ""
|
| 748 |
-
inputs_updates.append(gr.update(visible=True, value=None, label=col, placeholder=placeholder, interactive=True))
|
| 749 |
-
else:
|
| 750 |
-
inputs_updates.append(gr.update(visible=False, value=None, label="", placeholder=""))
|
| 751 |
-
|
| 752 |
-
return (*rows_updates, *inputs_updates, "", dropdown_reset)
|
| 753 |
-
|
| 754 |
-
except Exception as e:
|
| 755 |
-
print(f"Erro ao popular campos de avaliação: {e}")
|
| 756 |
-
return (*rows_hidden, *inputs_hidden, "", dropdown_reset)
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
def avaliar_imovel_callback(estado_modelo, tabela_estatisticas, estado_avaliacoes, indice_base_str, *aval_inputs):
|
| 760 |
-
"""Avalia um imóvel usando o modelo ajustado (seção 15).
|
| 761 |
-
|
| 762 |
-
CONTRACT: Retorna 3 itens (resultado_html, estado_avaliacoes, dropdown_update).
|
| 763 |
-
Se alterar, atualizar: app.py (outputs de btn_calcular_avaliacao.click).
|
| 764 |
-
"""
|
| 765 |
-
_err = lambda msg: (msg, estado_avaliacoes or [], gr.update())
|
| 766 |
-
if estado_modelo is None:
|
| 767 |
-
return _err("Ajuste um modelo primeiro.")
|
| 768 |
-
|
| 769 |
-
import pandas as pd
|
| 770 |
-
colunas_x = estado_modelo["colunas_x"]
|
| 771 |
-
n_vars = len(colunas_x)
|
| 772 |
-
|
| 773 |
-
# Extrair valores dos inputs
|
| 774 |
-
valores_x = {}
|
| 775 |
-
for i, col in enumerate(colunas_x):
|
| 776 |
-
if i >= len(aval_inputs) or aval_inputs[i] is None:
|
| 777 |
-
return _err("Preencha todos os campos antes de calcular.")
|
| 778 |
-
valores_x[col] = float(aval_inputs[i])
|
| 779 |
-
|
| 780 |
-
# Extrair transformações do resultado do modelo
|
| 781 |
-
transformacoes_x = estado_modelo.get("transformacoes_x", {})
|
| 782 |
-
transformacao_y = estado_modelo.get("transformacao_y", "(x)")
|
| 783 |
-
|
| 784 |
-
# Obter estatísticas
|
| 785 |
-
if tabela_estatisticas is not None and isinstance(tabela_estatisticas, pd.DataFrame):
|
| 786 |
-
estatisticas_df = tabela_estatisticas
|
| 787 |
-
else:
|
| 788 |
-
return _err("Estatísticas não disponíveis.")
|
| 789 |
-
|
| 790 |
-
# Validar variáveis dicotômicas, categóricas codificadas e percentuais ANTES de avaliar
|
| 791 |
-
dicotomicas = estado_modelo.get("dicotomicas", [])
|
| 792 |
-
codigo_alocado = estado_modelo.get("codigo_alocado", [])
|
| 793 |
-
percentuais = estado_modelo.get("percentuais", [])
|
| 794 |
-
|
| 795 |
-
if "Variável" in estatisticas_df.columns:
|
| 796 |
-
est_idx = estatisticas_df.set_index("Variável")
|
| 797 |
-
else:
|
| 798 |
-
est_idx = estatisticas_df
|
| 799 |
-
|
| 800 |
-
for col in colunas_x:
|
| 801 |
-
val = valores_x[col]
|
| 802 |
-
if col in dicotomicas:
|
| 803 |
-
if val not in (0, 0.0, 1, 1.0):
|
| 804 |
-
return _err(f"<p style='color:red;'><b>Erro:</b> A variável <b>{col}</b> é dicotômica e aceita apenas valores 0 ou 1. "
|
| 805 |
-
f"Valor informado: {val}</p>")
|
| 806 |
-
elif col in codigo_alocado and col in est_idx.index:
|
| 807 |
-
min_val = float(est_idx.loc[col, "Mínimo"])
|
| 808 |
-
max_val = float(est_idx.loc[col, "Máximo"])
|
| 809 |
-
eh_inteiro = (float(val) == int(float(val)))
|
| 810 |
-
if not eh_inteiro or val < min_val or val > max_val:
|
| 811 |
-
return _err(f"<p style='color:red;'><b>Erro:</b> A variável <b>{col}</b> é categórica codificada e aceita apenas "
|
| 812 |
-
f"valores inteiros de {int(min_val)} a {int(max_val)}. Valor informado: {val}</p>")
|
| 813 |
-
elif col in percentuais:
|
| 814 |
-
if val < 0 or val > 1:
|
| 815 |
-
return _err(f"<p style='color:red;'><b>Erro:</b> A variável <b>{col}</b> é percentual e aceita apenas "
|
| 816 |
-
f"valores entre 0 e 1. Valor informado: {val}</p>")
|
| 817 |
-
|
| 818 |
-
resultado = avaliar_imovel(
|
| 819 |
-
modelo_sm=estado_modelo["modelo_sm"],
|
| 820 |
-
valores_x=valores_x,
|
| 821 |
-
colunas_x=colunas_x,
|
| 822 |
-
transformacoes_x=transformacoes_x,
|
| 823 |
-
transformacao_y=transformacao_y,
|
| 824 |
-
estatisticas_df=estatisticas_df,
|
| 825 |
-
dicotomicas=dicotomicas,
|
| 826 |
-
codigo_alocado=codigo_alocado,
|
| 827 |
-
percentuais=percentuais,
|
| 828 |
-
)
|
| 829 |
-
|
| 830 |
-
if resultado is None:
|
| 831 |
-
return _err("Erro ao calcular avaliação.")
|
| 832 |
-
|
| 833 |
-
# Acumular resultado
|
| 834 |
-
nova_lista = list(estado_avaliacoes or []) + [resultado]
|
| 835 |
-
indice_base = int(indice_base_str) - 1 if indice_base_str else 0
|
| 836 |
-
html = formatar_avaliacao_html(nova_lista, indice_base=indice_base, elem_id_excluir="excluir-aval-elab")
|
| 837 |
-
|
| 838 |
-
# Atualizar choices do dropdown de base
|
| 839 |
-
choices = [str(i + 1) for i in range(len(nova_lista))]
|
| 840 |
-
# Se é a primeira avaliação, setar base como "1"
|
| 841 |
-
base_val = indice_base_str if indice_base_str else "1"
|
| 842 |
-
|
| 843 |
-
return html, nova_lista, gr.update(choices=choices, value=base_val)
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
def limpar_avaliacoes_callback():
|
| 847 |
-
"""Limpa o histórico de avaliações da seção 15.
|
| 848 |
-
|
| 849 |
-
CONTRACT: Retorna 3 itens (resultado_html, estado_avaliacoes, dropdown_update).
|
| 850 |
-
Se alterar, atualizar: app.py (outputs de btn_limpar_avaliacoes.click).
|
| 851 |
-
"""
|
| 852 |
-
return "", [], gr.update(choices=[], value=None)
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
def excluir_avaliacao_callback(indice_str, estado_avaliacoes, indice_base_str):
|
| 856 |
-
"""Exclui uma avaliação da lista (seção 15).
|
| 857 |
-
|
| 858 |
-
CONTRACT: Retorna 4 itens (resultado_html, estado_avaliacoes, dropdown_update, trigger_reset).
|
| 859 |
-
"""
|
| 860 |
-
if not indice_str or not indice_str.strip() or not estado_avaliacoes:
|
| 861 |
-
return gr.update(), estado_avaliacoes or [], gr.update(), ""
|
| 862 |
-
|
| 863 |
-
try:
|
| 864 |
-
idx = int(indice_str.strip()) - 1
|
| 865 |
-
except ValueError:
|
| 866 |
-
return gr.update(), estado_avaliacoes or [], gr.update(), ""
|
| 867 |
-
|
| 868 |
-
if idx < 0 or idx >= len(estado_avaliacoes):
|
| 869 |
-
return gr.update(), estado_avaliacoes or [], gr.update(), ""
|
| 870 |
-
|
| 871 |
-
nova_lista = [a for i, a in enumerate(estado_avaliacoes) if i != idx]
|
| 872 |
-
|
| 873 |
-
if not nova_lista:
|
| 874 |
-
return "", [], gr.update(choices=[], value=None), ""
|
| 875 |
-
|
| 876 |
-
# Ajustar índice base
|
| 877 |
-
base = int(indice_base_str) - 1 if indice_base_str else 0
|
| 878 |
-
if base >= len(nova_lista):
|
| 879 |
-
base = len(nova_lista) - 1
|
| 880 |
-
if base < 0:
|
| 881 |
-
base = 0
|
| 882 |
-
|
| 883 |
-
choices = [str(i + 1) for i in range(len(nova_lista))]
|
| 884 |
-
html = formatar_avaliacao_html(nova_lista, indice_base=base, elem_id_excluir="excluir-aval-elab")
|
| 885 |
-
return html, nova_lista, gr.update(choices=choices, value=str(base + 1)), ""
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
def atualizar_base_avaliacao_callback(estado_avaliacoes, indice_base_str):
|
| 889 |
-
"""Re-renderiza HTML quando o dropdown de base muda (seção 15).
|
| 890 |
-
|
| 891 |
-
CONTRACT: Retorna 1 item (resultado_html).
|
| 892 |
-
"""
|
| 893 |
-
if not estado_avaliacoes:
|
| 894 |
-
return ""
|
| 895 |
-
indice = int(indice_base_str) - 1 if indice_base_str else 0
|
| 896 |
-
return formatar_avaliacao_html(estado_avaliacoes, indice_base=indice, elem_id_excluir="excluir-aval-elab")
|
| 897 |
-
|
| 898 |
-
|
| 899 |
-
def exportar_avaliacoes_excel_callback(estado_avaliacoes):
|
| 900 |
-
"""Exporta avaliações para Excel (seção 15).
|
| 901 |
-
|
| 902 |
-
CONTRACT: Retorna 1 item (download_file_update).
|
| 903 |
-
"""
|
| 904 |
-
if not estado_avaliacoes:
|
| 905 |
-
return gr.update(value=None, visible=False)
|
| 906 |
-
caminho = exportar_avaliacoes_excel(estado_avaliacoes)
|
| 907 |
-
if caminho:
|
| 908 |
-
return gr.update(value=caminho, visible=True)
|
| 909 |
-
return gr.update(value=None, visible=False)
|
| 910 |
-
|
| 911 |
-
|
| 912 |
-
def exportar_modelo_callback(resultado_modelo, df, df_completo, estatisticas,
|
| 913 |
-
nome_arquivo, elaborador=None, outliers_excluidos=None):
|
| 914 |
-
"""Callback para exportar modelo com download."""
|
| 915 |
-
if resultado_modelo is None:
|
| 916 |
-
return "Nenhum modelo para exportar. Ajuste o modelo primeiro.", gr.update(value=None, visible=False)
|
| 917 |
-
|
| 918 |
-
if not nome_arquivo or not nome_arquivo.strip():
|
| 919 |
-
return "Informe o nome do arquivo.", gr.update(value=None, visible=False)
|
| 920 |
-
|
| 921 |
-
caminho, msg = exportar_modelo_dai(
|
| 922 |
-
resultado_modelo, df, df_completo, estatisticas, nome_arquivo.strip(),
|
| 923 |
-
elaborador=elaborador, outliers_excluidos=outliers_excluidos or []
|
| 924 |
-
)
|
| 925 |
-
|
| 926 |
-
if caminho:
|
| 927 |
-
return msg, gr.update(value=caminho, visible=True)
|
| 928 |
-
else:
|
| 929 |
-
return msg, gr.update(value=None, visible=False)
|
| 930 |
-
|
| 931 |
-
|
| 932 |
-
# ============================================================
|
| 933 |
-
# CALLBACKS DE DICOTÔMICAS
|
| 934 |
-
# ============================================================
|
| 935 |
-
|
| 936 |
-
def atualizar_interativo_dicotomicas(colunas_x, dicotomicas, codigo_alocado, percentuais, df):
|
| 937 |
-
"""Atualiza interactive dos dropdowns de transformação conforme os 3 tipos.
|
| 938 |
-
|
| 939 |
-
Dicotômicas e percentuais: transformação travada em (x).
|
| 940 |
-
Variáveis categóricas codificadas: transformação livre.
|
| 941 |
-
|
| 942 |
-
Handler para checkboxes_dicotomicas.change(), checkboxes_codigo_alocado.change(),
|
| 943 |
-
checkboxes_percentuais.change().
|
| 944 |
-
|
| 945 |
-
CONTRACT: Retorna 20 itens (transf_x_dropdowns).
|
| 946 |
-
Se alterar, atualizar: app.py (outputs dos .change dos 3 checkboxes).
|
| 947 |
-
"""
|
| 948 |
-
updates = []
|
| 949 |
-
for i in range(MAX_VARS_X):
|
| 950 |
-
if i < len(colunas_x):
|
| 951 |
-
col = colunas_x[i]
|
| 952 |
-
if col in (dicotomicas or []) or col in (percentuais or []):
|
| 953 |
-
updates.append(gr.update(interactive=False, value="(x)"))
|
| 954 |
-
else:
|
| 955 |
-
updates.append(gr.update(interactive=True))
|
| 956 |
-
else:
|
| 957 |
-
updates.append(gr.update())
|
| 958 |
-
return updates
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
def popular_dicotomicas_callback(estado_modelo, colunas_x, estado_df):
|
| 962 |
-
"""Popula os 3 checkboxes: dicotômicas, variáveis categóricas codificadas e percentuais.
|
| 963 |
-
|
| 964 |
-
Para .dai: usa listas salvas no modelo.
|
| 965 |
-
Para CSV/Excel: auto-detecta os 3 tipos.
|
| 966 |
-
|
| 967 |
-
CONTRACT: Retorna 3 itens (checkboxes_dicotomicas, checkboxes_codigo_alocado, checkboxes_percentuais).
|
| 968 |
-
Se alterar, atualizar: app.py (outputs do .then de popular_dicotomicas).
|
| 969 |
-
"""
|
| 970 |
-
empty = gr.update(choices=[], value=[], visible=False)
|
| 971 |
-
if not colunas_x:
|
| 972 |
-
return empty, empty, empty
|
| 973 |
-
|
| 974 |
-
choices = list(colunas_x)
|
| 975 |
-
|
| 976 |
-
if estado_modelo is not None:
|
| 977 |
-
dic = estado_modelo.get("dicotomicas", [])
|
| 978 |
-
cod = estado_modelo.get("codigo_alocado", [])
|
| 979 |
-
perc = estado_modelo.get("percentuais", [])
|
| 980 |
-
elif estado_df is not None:
|
| 981 |
-
dic = detectar_dicotomicas(estado_df, colunas_x)
|
| 982 |
-
cod = detectar_codigo_alocado(estado_df, colunas_x)
|
| 983 |
-
perc = detectar_percentuais(estado_df, colunas_x)
|
| 984 |
-
else:
|
| 985 |
-
return empty, empty, empty
|
| 986 |
-
|
| 987 |
-
return (
|
| 988 |
-
gr.update(choices=choices, value=dic, visible=True),
|
| 989 |
-
gr.update(choices=choices, value=cod, visible=True),
|
| 990 |
-
gr.update(choices=choices, value=perc, visible=True),
|
| 991 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/app/core/elaboracao/outliers.py
DELETED
|
@@ -1,300 +0,0 @@
|
|
| 1 |
-
# -*- coding: utf-8 -*-
|
| 2 |
-
"""
|
| 3 |
-
outliers.py - Filtros dinâmicos, exclusão/reinclusão e iteração de outliers.
|
| 4 |
-
|
| 5 |
-
Callbacks que lidam com filtros de outliers, exclusão,
|
| 6 |
-
reinclusão e reinício de iteração do modelo.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
import gradio as gr
|
| 10 |
-
|
| 11 |
-
from .formatadores import (
|
| 12 |
-
arredondar_df,
|
| 13 |
-
criar_header_secao,
|
| 14 |
-
formatar_micronumerosidade_html,
|
| 15 |
-
formatar_outliers_anteriores_html,
|
| 16 |
-
)
|
| 17 |
-
from .modelo import buscar_transformacoes_callback, atualizar_estatisticas_auto
|
| 18 |
-
from .core import testar_micronumerosidade
|
| 19 |
-
from .charts import criar_graficos_dispersao, criar_mapa
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
# ============================================================
|
| 23 |
-
# FILTROS
|
| 24 |
-
# ============================================================
|
| 25 |
-
|
| 26 |
-
def aplicar_filtros_callback(metricas_estado, n_filtros, *args):
|
| 27 |
-
"""Aplica múltiplos filtros de outliers.
|
| 28 |
-
|
| 29 |
-
n_filtros: número de filtros visíveis (apenas esses serão aplicados)
|
| 30 |
-
args: var1, var2, var3, var4, op1, op2, op3, op4, val1, val2, val3, val4
|
| 31 |
-
Retorna lista de índices que satisfazem QUALQUER filtro (OR lógico).
|
| 32 |
-
"""
|
| 33 |
-
if metricas_estado is None:
|
| 34 |
-
return ""
|
| 35 |
-
|
| 36 |
-
# Extrai filtros dos argumentos (apenas os filtros visíveis)
|
| 37 |
-
# Ordem: [4 vars] + [4 ops] + [4 vals]
|
| 38 |
-
filtros = []
|
| 39 |
-
for i in range(n_filtros): # Só processa os filtros visíveis
|
| 40 |
-
var = args[i] # vars estão nos índices 0-3
|
| 41 |
-
op = args[i + 4] # ops estão nos índices 4-7
|
| 42 |
-
val = args[i + 8] # vals estão nos índices 8-11
|
| 43 |
-
if var is not None and op is not None and val is not None:
|
| 44 |
-
# Converte valor para float (pode vir como string)
|
| 45 |
-
try:
|
| 46 |
-
val_float = float(val)
|
| 47 |
-
except (ValueError, TypeError):
|
| 48 |
-
continue
|
| 49 |
-
filtros.append({"variavel": var, "operador": op, "valor": val_float})
|
| 50 |
-
|
| 51 |
-
if not filtros:
|
| 52 |
-
return ""
|
| 53 |
-
|
| 54 |
-
# Aplica filtros com lógica OR
|
| 55 |
-
indices_outliers = set()
|
| 56 |
-
|
| 57 |
-
for filtro in filtros:
|
| 58 |
-
var = filtro["variavel"]
|
| 59 |
-
op = filtro["operador"]
|
| 60 |
-
val = filtro["valor"]
|
| 61 |
-
|
| 62 |
-
if var not in metricas_estado.columns:
|
| 63 |
-
continue
|
| 64 |
-
|
| 65 |
-
# Aplica operador
|
| 66 |
-
if op == "<=":
|
| 67 |
-
mask = metricas_estado[var] <= val
|
| 68 |
-
elif op == ">=":
|
| 69 |
-
mask = metricas_estado[var] >= val
|
| 70 |
-
elif op == "<":
|
| 71 |
-
mask = metricas_estado[var] < val
|
| 72 |
-
elif op == ">":
|
| 73 |
-
mask = metricas_estado[var] > val
|
| 74 |
-
elif op == "=":
|
| 75 |
-
mask = metricas_estado[var] == val
|
| 76 |
-
else:
|
| 77 |
-
continue
|
| 78 |
-
|
| 79 |
-
# Usa o índice do DataFrame (que agora são os índices originais dos dados via set_index)
|
| 80 |
-
indices_filtro = metricas_estado[mask].index.tolist()
|
| 81 |
-
indices_outliers.update(indices_filtro)
|
| 82 |
-
|
| 83 |
-
# Converte para inteiros e ordena
|
| 84 |
-
indices_ordenados = sorted([int(i) for i in indices_outliers])
|
| 85 |
-
return ", ".join(map(str, indices_ordenados))
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
def adicionar_filtro_callback(n_filtros_atual):
|
| 89 |
-
"""Callback para adicionar um novo filtro (mostra a próxima row oculta).
|
| 90 |
-
|
| 91 |
-
CONTRACT: Retorna 5 itens (n_filtros + 4 row updates).
|
| 92 |
-
Se alterar, atualizar: app.py (outputs de btn_adicionar_filtro.click).
|
| 93 |
-
"""
|
| 94 |
-
n_novo = min(n_filtros_atual + 1, 4)
|
| 95 |
-
|
| 96 |
-
# Retorna: novo estado + 4 updates de visibilidade para rows
|
| 97 |
-
updates = [n_novo]
|
| 98 |
-
for i in range(4):
|
| 99 |
-
updates.append(gr.update(visible=(i < n_novo)))
|
| 100 |
-
|
| 101 |
-
return tuple(updates)
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
def remover_ultimo_filtro_callback(n_filtros_atual):
|
| 105 |
-
"""Remove o último filtro visível.
|
| 106 |
-
|
| 107 |
-
CONTRACT: Retorna 5 itens (n_filtros + 4 row updates).
|
| 108 |
-
Se alterar, atualizar: app.py (outputs de btn_remover_filtro.click).
|
| 109 |
-
"""
|
| 110 |
-
n_novo = max(0, n_filtros_atual - 1)
|
| 111 |
-
return (n_novo,) + tuple(gr.update(visible=(i < n_novo)) for i in range(4))
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
def limpar_filtros_callback():
|
| 115 |
-
"""Limpa todos os filtros e restaura os padrões.
|
| 116 |
-
|
| 117 |
-
CONTRACT: Retorna 18 itens (n_filtros + 4 rows + 4 vars + 4 ops + 4 vals + outliers_texto).
|
| 118 |
-
Se alterar, atualizar: app.py (outputs de btn_limpar_filtros.click).
|
| 119 |
-
"""
|
| 120 |
-
return (
|
| 121 |
-
2, # estado_n_filtros (volta para 2 filtros padrão)
|
| 122 |
-
gr.update(visible=True), # row 0
|
| 123 |
-
gr.update(visible=True), # row 1
|
| 124 |
-
gr.update(visible=False), # row 2
|
| 125 |
-
gr.update(visible=False), # row 3
|
| 126 |
-
gr.update(value="Resíduo Pad."), # var 0
|
| 127 |
-
gr.update(value="Resíduo Pad."), # var 1
|
| 128 |
-
gr.update(value="Resíduo Pad."), # var 2
|
| 129 |
-
gr.update(value="Resíduo Pad."), # var 3
|
| 130 |
-
gr.update(value="<="), # op 0
|
| 131 |
-
gr.update(value=">="), # op 1
|
| 132 |
-
gr.update(value=">="), # op 2
|
| 133 |
-
gr.update(value=">="), # op 3
|
| 134 |
-
gr.update(value=-2.0), # val 0
|
| 135 |
-
gr.update(value=2.0), # val 1
|
| 136 |
-
gr.update(value=0.0), # val 2
|
| 137 |
-
gr.update(value=0.0), # val 3
|
| 138 |
-
"" # outliers_texto
|
| 139 |
-
)
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
# ============================================================
|
| 143 |
-
# ITERAÇÃO DE OUTLIERS
|
| 144 |
-
# ============================================================
|
| 145 |
-
|
| 146 |
-
def reiniciar_iteracao_callback(df_original, outliers_anteriores, outliers_texto, reincluir_texto, iteracao_atual, coluna_y, colunas_x, dicotomicas, codigo_alocado, percentuais, grau_min_coef=3, grau_min_f=3):
|
| 147 |
-
"""Combina outliers anteriores com novos (excluindo reincluídos), atualiza estado e reinicia análise.
|
| 148 |
-
Recalcula todas as seções (2 em diante) considerando a ausência dos outliers.
|
| 149 |
-
|
| 150 |
-
CONTRACT: Retorna 30 itens para btn_reiniciar_iteracao.click (app.py:criar_aba).
|
| 151 |
-
Se alterar, atualizar: app.py (outputs de btn_reiniciar_iteracao.click).
|
| 152 |
-
Consome buscar_transformacoes_callback por destructuring (html, resultados, _, *btns).
|
| 153 |
-
Consome atualizar_estatisticas_auto por destructuring (stats, timestamp).
|
| 154 |
-
"""
|
| 155 |
-
# Parse dos novos outliers
|
| 156 |
-
novos_outliers = []
|
| 157 |
-
if outliers_texto and outliers_texto.strip():
|
| 158 |
-
try:
|
| 159 |
-
novos_outliers = [int(x.strip()) for x in outliers_texto.split(",") if x.strip()]
|
| 160 |
-
except:
|
| 161 |
-
pass
|
| 162 |
-
|
| 163 |
-
# Parse dos índices a reincluir
|
| 164 |
-
reincluir = []
|
| 165 |
-
if reincluir_texto and reincluir_texto.strip():
|
| 166 |
-
try:
|
| 167 |
-
reincluir = [int(x.strip()) for x in reincluir_texto.split(",") if x.strip()]
|
| 168 |
-
except:
|
| 169 |
-
pass
|
| 170 |
-
|
| 171 |
-
# Remove reincluídos dos anteriores, depois combina com novos (sem duplicatas)
|
| 172 |
-
anteriores_atualizados = [i for i in (outliers_anteriores or []) if i not in reincluir]
|
| 173 |
-
outliers_combinados = list(set(anteriores_atualizados + novos_outliers))
|
| 174 |
-
outliers_combinados.sort()
|
| 175 |
-
|
| 176 |
-
# Incrementa iteração
|
| 177 |
-
nova_iteracao = (iteracao_atual or 1) + 1
|
| 178 |
-
|
| 179 |
-
# Cria DataFrame filtrado
|
| 180 |
-
df_filtrado = df_original.copy()
|
| 181 |
-
if outliers_combinados:
|
| 182 |
-
df_filtrado = df_filtrado.drop(index=outliers_combinados, errors='ignore')
|
| 183 |
-
|
| 184 |
-
# Recalcula estatísticas (seção 5)
|
| 185 |
-
estatisticas, timestamp = atualizar_estatisticas_auto(df_filtrado, coluna_y, colunas_x)
|
| 186 |
-
|
| 187 |
-
# Recalcula micronumerosidade (seção 6)
|
| 188 |
-
try:
|
| 189 |
-
resultado_micro = testar_micronumerosidade(df_filtrado, list(colunas_x),
|
| 190 |
-
dicotomicas=dicotomicas, codigo_alocado=codigo_alocado)
|
| 191 |
-
html_micro = formatar_micronumerosidade_html(resultado_micro)
|
| 192 |
-
except Exception as e:
|
| 193 |
-
html_micro = f"<p style='color: red;'>Erro ao calcular micronumerosidade: {e}</p>"
|
| 194 |
-
|
| 195 |
-
# Recalcula gráficos de dispersão originais (seção 7)
|
| 196 |
-
try:
|
| 197 |
-
X = df_filtrado[list(colunas_x)]
|
| 198 |
-
y = df_filtrado[coluna_y]
|
| 199 |
-
fig_dispersao = criar_graficos_dispersao(X, y)
|
| 200 |
-
except Exception as e:
|
| 201 |
-
print(f"Erro ao gerar gráficos de dispersão: {e}")
|
| 202 |
-
fig_dispersao = None
|
| 203 |
-
|
| 204 |
-
# Recalcula busca de transformações (seção 8)
|
| 205 |
-
try:
|
| 206 |
-
busca_html_result, resultados_busca, _, *btn_updates = buscar_transformacoes_callback(
|
| 207 |
-
df_filtrado, coluna_y, colunas_x, dicotomicas, codigo_alocado, percentuais, int(grau_min_coef), int(grau_min_f)
|
| 208 |
-
)
|
| 209 |
-
except Exception as e:
|
| 210 |
-
busca_html_result = f"<p style='color: red;'>Erro na busca: {e}</p>"
|
| 211 |
-
resultados_busca = []
|
| 212 |
-
btn_updates = [gr.update(visible=False) for _ in range(5)]
|
| 213 |
-
|
| 214 |
-
# Radio buttons coerentes com os novos resultados
|
| 215 |
-
if resultados_busca:
|
| 216 |
-
grau_coef_min = min(
|
| 217 |
-
min(r.get("graus_coef", {}).values(), default=0)
|
| 218 |
-
for r in resultados_busca
|
| 219 |
-
)
|
| 220 |
-
grau_f_min = min(r.get("grau_f", 0) for r in resultados_busca)
|
| 221 |
-
else:
|
| 222 |
-
grau_coef_min = int(grau_min_coef)
|
| 223 |
-
grau_f_min = int(grau_min_f)
|
| 224 |
-
|
| 225 |
-
# Atualiza textos
|
| 226 |
-
html_outliers_ant = formatar_outliers_anteriores_html(
|
| 227 |
-
len(outliers_combinados),
|
| 228 |
-
", ".join(map(str, outliers_combinados)) if outliers_combinados else ""
|
| 229 |
-
)
|
| 230 |
-
|
| 231 |
-
# Visibilidade do accordion
|
| 232 |
-
accordion_visivel = len(outliers_combinados) > 0
|
| 233 |
-
|
| 234 |
-
# Mapa atualizado
|
| 235 |
-
mapa_html = criar_mapa(df_filtrado)
|
| 236 |
-
|
| 237 |
-
# Header updates com timestamp
|
| 238 |
-
header_2_update = gr.update(value=criar_header_secao(2, "Visualizar Dados", timestamp))
|
| 239 |
-
header_5_update = gr.update(value=criar_header_secao(5, "Estatísticas das Variáveis Selecionadas", timestamp))
|
| 240 |
-
header_6_update = gr.update(value=criar_header_secao(6, "Teste de Micronumerosidade (NBR 14.653-2)", timestamp))
|
| 241 |
-
header_7_update = gr.update(value=criar_header_secao(7, "Gráficos de Dispersão das Variáveis Independentes", timestamp))
|
| 242 |
-
header_8_update = gr.update(value=criar_header_secao(8, "Transformações Sugeridas", timestamp))
|
| 243 |
-
|
| 244 |
-
return (
|
| 245 |
-
outliers_combinados, # estado_outliers_anteriores
|
| 246 |
-
nova_iteracao, # estado_iteracao
|
| 247 |
-
df_filtrado, # estado_df_filtrado
|
| 248 |
-
arredondar_df(df_filtrado), # tabela_dados
|
| 249 |
-
estatisticas, # tabela_estatisticas
|
| 250 |
-
header_5_update, # header_secao_5
|
| 251 |
-
html_outliers_ant, # html_outliers_anteriores
|
| 252 |
-
html_outliers_ant, # html_outliers_sec14
|
| 253 |
-
gr.update(visible=accordion_visivel), # accordion_outliers_anteriores
|
| 254 |
-
"", # outliers_texto (limpo)
|
| 255 |
-
"", # reincluir_texto (limpo)
|
| 256 |
-
None, # tabela_metricas (limpa)
|
| 257 |
-
None, # estado_metricas (limpo)
|
| 258 |
-
gr.update(value=criar_header_secao(13, "Analisar Outliers")), # header_secao_13
|
| 259 |
-
f"Excluídos: {len(outliers_combinados)} | A excluir: 0 | A reincluir: 0 | Total: {len(outliers_combinados)}", # txt_resumo_outliers
|
| 260 |
-
mapa_html, # mapa_html atualizado
|
| 261 |
-
# Novos outputs para seções 2, 6, 7, 8
|
| 262 |
-
header_2_update, # header_secao_2
|
| 263 |
-
html_micro, # html_micronumerosidade
|
| 264 |
-
header_6_update, # header_secao_6
|
| 265 |
-
fig_dispersao, # plot_dispersao
|
| 266 |
-
header_7_update, # header_secao_7
|
| 267 |
-
busca_html_result, # busca_html
|
| 268 |
-
resultados_busca, # estado_resultados_busca
|
| 269 |
-
header_8_update, # header_secao_8
|
| 270 |
-
*btn_updates, # botões adotar (5 botões)
|
| 271 |
-
gr.update(value=grau_coef_min), # slider_grau_coef
|
| 272 |
-
gr.update(value=grau_f_min), # slider_grau_f
|
| 273 |
-
)
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
def atualizar_resumo_outliers(outliers_anteriores, outliers_texto, reincluir_texto):
|
| 277 |
-
"""Atualiza o resumo de outliers quando o usuário edita os campos."""
|
| 278 |
-
n_anteriores = len(outliers_anteriores) if outliers_anteriores else 0
|
| 279 |
-
|
| 280 |
-
novos_outliers = []
|
| 281 |
-
if outliers_texto and outliers_texto.strip():
|
| 282 |
-
try:
|
| 283 |
-
novos_outliers = [int(x.strip()) for x in outliers_texto.split(",") if x.strip()]
|
| 284 |
-
except:
|
| 285 |
-
pass
|
| 286 |
-
|
| 287 |
-
reincluir = []
|
| 288 |
-
if reincluir_texto and reincluir_texto.strip():
|
| 289 |
-
try:
|
| 290 |
-
reincluir = [int(x.strip()) for x in reincluir_texto.split(",") if x.strip()]
|
| 291 |
-
except:
|
| 292 |
-
pass
|
| 293 |
-
|
| 294 |
-
n_novos = len(novos_outliers)
|
| 295 |
-
n_reincluir = len(reincluir)
|
| 296 |
-
# Calcula total: anteriores menos reincluídos, mais novos
|
| 297 |
-
anteriores_atualizados = [i for i in (outliers_anteriores or []) if i not in reincluir]
|
| 298 |
-
n_total = len(set(anteriores_atualizados + novos_outliers))
|
| 299 |
-
|
| 300 |
-
return f"Excluídos: {n_anteriores} | A excluir: {n_novos} | A reincluir: {n_reincluir} | Total: {n_total}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/app/core/visualizacao/app.py
CHANGED
|
@@ -1,46 +1,20 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
-
|
| 4 |
-
# IMPORTAÇÕES
|
| 5 |
-
# ============================================================
|
| 6 |
-
import pandas as pd
|
| 7 |
-
import numpy as np
|
| 8 |
-
import folium
|
| 9 |
-
from folium import plugins
|
| 10 |
-
from joblib import load
|
| 11 |
-
import os
|
| 12 |
import re
|
| 13 |
import traceback
|
| 14 |
from datetime import datetime
|
| 15 |
from html import escape
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
except Exception: # pragma: no cover - runtime portatil nao precisa da UI gradio
|
| 22 |
-
class _GradioPlaceholder:
|
| 23 |
-
class SelectData:
|
| 24 |
-
pass
|
| 25 |
-
|
| 26 |
-
@staticmethod
|
| 27 |
-
def update(*args, **kwargs):
|
| 28 |
-
raise RuntimeError("Gradio indisponivel neste runtime")
|
| 29 |
-
|
| 30 |
-
def __getattr__(self, name: str):
|
| 31 |
-
raise RuntimeError(f"Gradio indisponivel neste runtime: {name}")
|
| 32 |
-
|
| 33 |
-
gr = _GradioPlaceholder()
|
| 34 |
-
|
| 35 |
-
# Importações para gráficos (trazidas de graficos.py)
|
| 36 |
-
from scipy import stats
|
| 37 |
import plotly.graph_objects as go
|
|
|
|
|
|
|
| 38 |
from statsmodels.stats.outliers_influence import OLSInfluence
|
| 39 |
-
import branca.colormap as cm
|
| 40 |
-
import math
|
| 41 |
|
| 42 |
-
from app.core.elaboracao.core import avaliar_imovel, _migrar_pacote_v1_para_v2, exportar_avaliacoes_excel
|
| 43 |
-
from app.core.elaboracao.formatadores import formatar_avaliacao_html
|
| 44 |
from app.core.map_layers import (
|
| 45 |
add_bairros_layer,
|
| 46 |
add_indice_marker,
|
|
@@ -49,430 +23,297 @@ from app.core.map_layers import (
|
|
| 49 |
add_zoom_responsive_circle_markers,
|
| 50 |
)
|
| 51 |
|
| 52 |
-
# ============================================================
|
| 53 |
-
# CONSTANTES
|
| 54 |
-
# ============================================================
|
| 55 |
-
CHAVES_ESPERADAS = ["versao", "dados", "transformacoes", "modelo"]
|
| 56 |
-
|
| 57 |
-
# Constantes para avaliação
|
| 58 |
-
MAX_VARS_AVAL = 20
|
| 59 |
-
N_COLS_AVAL = 2
|
| 60 |
-
N_ROWS_AVAL = MAX_VARS_AVAL // N_COLS_AVAL # 10
|
| 61 |
-
|
| 62 |
-
# Cores consistentes (trazidas de graficos.py)
|
| 63 |
-
COR_PRINCIPAL = '#FF8C00' # Laranja
|
| 64 |
-
COR_LINHA = '#dc3545' # Vermelho para linhas de referência
|
| 65 |
-
|
| 66 |
-
# ============================================================
|
| 67 |
-
# FUNÇÃO: CARREGAR CSS EXTERNO
|
| 68 |
-
# ============================================================
|
| 69 |
-
def carregar_css():
|
| 70 |
-
"""Carrega o arquivo CSS externo."""
|
| 71 |
-
css_path = resolve_core_path("visualizacao", "styles.css")
|
| 72 |
-
try:
|
| 73 |
-
with open(css_path, "r", encoding="utf-8") as f:
|
| 74 |
-
return f.read()
|
| 75 |
-
except FileNotFoundError:
|
| 76 |
-
print(f"Aviso: Arquivo CSS não encontrado em {css_path}")
|
| 77 |
-
return ""
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
|
| 83 |
def _criar_grafico_obs_calc(y_obs, y_calc, indices=None):
|
| 84 |
-
"""Cria gráfico de valores observados vs calculados (Plotly Figure)."""
|
| 85 |
try:
|
| 86 |
fig = go.Figure()
|
| 87 |
-
|
| 88 |
scatter_args = dict(
|
| 89 |
x=y_calc,
|
| 90 |
y=y_obs,
|
| 91 |
-
mode=
|
| 92 |
-
marker=dict(
|
| 93 |
-
|
| 94 |
-
size=10,
|
| 95 |
-
line=dict(color='black', width=1)
|
| 96 |
-
),
|
| 97 |
-
name='Dados',
|
| 98 |
)
|
| 99 |
if indices is not None:
|
| 100 |
-
scatter_args[
|
| 101 |
-
scatter_args[
|
|
|
|
|
|
|
|
|
|
| 102 |
else:
|
| 103 |
-
scatter_args[
|
|
|
|
|
|
|
| 104 |
|
| 105 |
-
# Scatter plot
|
| 106 |
fig.add_trace(go.Scatter(**scatter_args))
|
| 107 |
|
| 108 |
-
# Linha de identidade (45 graus)
|
| 109 |
min_val = min(min(y_obs), min(y_calc))
|
| 110 |
max_val = max(max(y_obs), max(y_calc))
|
| 111 |
margin = (max_val - min_val) * 0.05
|
| 112 |
|
| 113 |
-
fig.add_trace(
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
| 120 |
|
| 121 |
fig.update_layout(
|
| 122 |
-
title=dict(text=
|
| 123 |
-
xaxis_title=
|
| 124 |
-
yaxis_title=
|
| 125 |
showlegend=True,
|
| 126 |
-
plot_bgcolor=
|
| 127 |
-
margin=dict(l=60, r=40, t=60, b=60)
|
| 128 |
)
|
| 129 |
-
|
| 130 |
-
fig.
|
| 131 |
-
fig.update_yaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
|
| 132 |
-
|
| 133 |
return fig
|
| 134 |
-
except Exception as
|
| 135 |
-
print(f"Erro ao criar gráfico obs vs calc: {
|
| 136 |
return None
|
| 137 |
|
|
|
|
| 138 |
def _criar_grafico_residuos(y_calc, residuos, indices=None):
|
| 139 |
-
"""Cria gráfico de resíduos vs valores ajustados (Plotly Figure)."""
|
| 140 |
try:
|
| 141 |
fig = go.Figure()
|
| 142 |
-
|
| 143 |
scatter_args = dict(
|
| 144 |
x=y_calc,
|
| 145 |
y=residuos,
|
| 146 |
-
mode=
|
| 147 |
-
marker=dict(
|
| 148 |
-
|
| 149 |
-
size=10,
|
| 150 |
-
line=dict(color='black', width=1)
|
| 151 |
-
),
|
| 152 |
-
name='Resíduos',
|
| 153 |
)
|
| 154 |
if indices is not None:
|
| 155 |
-
scatter_args[
|
| 156 |
-
scatter_args[
|
|
|
|
|
|
|
|
|
|
| 157 |
else:
|
| 158 |
-
scatter_args[
|
|
|
|
|
|
|
| 159 |
|
| 160 |
fig.add_trace(go.Scatter(**scatter_args))
|
| 161 |
-
|
| 162 |
fig.add_hline(y=0, line_dash="dash", line_color=COR_LINHA, line_width=2)
|
| 163 |
-
|
| 164 |
fig.update_layout(
|
| 165 |
-
title=dict(text=
|
| 166 |
-
xaxis_title=
|
| 167 |
-
yaxis_title=
|
| 168 |
showlegend=False,
|
| 169 |
-
plot_bgcolor=
|
| 170 |
-
margin=dict(l=60, r=40, t=60, b=60)
|
| 171 |
)
|
| 172 |
-
|
| 173 |
-
fig.
|
| 174 |
-
fig.update_yaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
|
| 175 |
-
|
| 176 |
return fig
|
| 177 |
-
except Exception as
|
| 178 |
-
print(f"Erro ao criar gráfico de resíduos: {
|
| 179 |
return None
|
| 180 |
|
|
|
|
| 181 |
def _criar_histograma_residuos(residuos):
|
| 182 |
-
"""Cria histograma dos resíduos com curva normal (Plotly Figure)."""
|
| 183 |
try:
|
| 184 |
fig = go.Figure()
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
|
|
|
| 193 |
|
| 194 |
mu, sigma = np.mean(residuos), np.std(residuos)
|
| 195 |
x_norm = np.linspace(min(residuos) - sigma, max(residuos) + sigma, 100)
|
| 196 |
y_norm = stats.norm.pdf(x_norm, mu, sigma)
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
|
|
|
| 205 |
|
| 206 |
fig.update_layout(
|
| 207 |
-
title=dict(text=
|
| 208 |
-
xaxis_title=
|
| 209 |
-
yaxis_title=
|
| 210 |
showlegend=True,
|
| 211 |
-
plot_bgcolor=
|
| 212 |
-
barmode=
|
| 213 |
-
margin=dict(l=60, r=40, t=60, b=60)
|
| 214 |
)
|
| 215 |
-
|
| 216 |
-
fig.
|
| 217 |
-
fig.update_yaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
|
| 218 |
-
|
| 219 |
return fig
|
| 220 |
-
except Exception as
|
| 221 |
-
print(f"Erro ao criar histograma: {
|
| 222 |
return None
|
| 223 |
|
| 224 |
|
| 225 |
def _criar_grafico_cook(modelos_sm):
|
| 226 |
-
"""Cria gráfico de Distância de Cook (Plotly Figure)."""
|
| 227 |
try:
|
| 228 |
-
if modelos_sm is None:
|
|
|
|
| 229 |
|
| 230 |
influence = OLSInfluence(modelos_sm)
|
| 231 |
cooks_d = influence.cooks_distance[0]
|
| 232 |
-
|
| 233 |
n = len(cooks_d)
|
| 234 |
indices = np.arange(1, n + 1)
|
| 235 |
limite = 4 / n
|
| 236 |
|
| 237 |
fig = go.Figure()
|
| 238 |
-
|
| 239 |
-
# Hastes (linhas verticais)
|
| 240 |
for idx, valor in zip(indices, cooks_d):
|
| 241 |
cor = COR_LINHA if valor > limite else COR_PRINCIPAL
|
| 242 |
fig.add_trace(
|
| 243 |
-
go.Scatter(
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
]
|
| 254 |
fig.add_trace(
|
| 255 |
go.Scatter(
|
| 256 |
x=indices,
|
| 257 |
y=cooks_d,
|
| 258 |
-
mode=
|
| 259 |
-
marker=dict(color=cores_pontos,
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
fig.update_layout(
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
linecolor='black')
|
| 281 |
-
fig.update_yaxes(showgrid=True,
|
| 282 |
-
gridcolor='lightgray',
|
| 283 |
-
showline=True,
|
| 284 |
-
linecolor='black')
|
| 285 |
-
|
| 286 |
return fig
|
| 287 |
-
except Exception as
|
| 288 |
-
print(f"Erro ao criar gráfico de Cook: {
|
| 289 |
return None
|
| 290 |
|
|
|
|
| 291 |
def _criar_grafico_correlacao(modelos_sm, nome_y: str | None = None):
|
| 292 |
-
"""Gera heatmap de correlação com cores customizadas e diagonal limpa."""
|
| 293 |
try:
|
| 294 |
-
if modelos_sm is None or not hasattr(modelos_sm,
|
| 295 |
return None
|
| 296 |
|
| 297 |
model = modelos_sm.model
|
| 298 |
X = model.exog
|
| 299 |
X_names = model.exog_names
|
| 300 |
y = model.endog
|
| 301 |
-
y_name_base =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
y_name = y_name_base if re.search(r"\(Y\)$", y_name_base, flags=re.IGNORECASE) else f"{y_name_base} (Y)"
|
| 303 |
|
| 304 |
df_X = pd.DataFrame(X, columns=X_names)
|
| 305 |
df_X = df_X.drop(
|
| 306 |
-
columns=[c for c in df_X.columns if str(c).lower() in (
|
| 307 |
-
errors=
|
| 308 |
)
|
| 309 |
|
| 310 |
df_y = pd.DataFrame({y_name: y})
|
| 311 |
-
df = pd.concat([df_y, df_X], axis=1)
|
| 312 |
-
|
| 313 |
-
# garantir numérico
|
| 314 |
-
df = df.apply(pd.to_numeric, errors='coerce')
|
| 315 |
-
|
| 316 |
-
# remover colunas constantes
|
| 317 |
variancias = df.var(ddof=0)
|
| 318 |
df = df.loc[:, variancias.fillna(0) > 0]
|
| 319 |
-
|
| 320 |
if df.shape[1] < 2:
|
| 321 |
return None
|
| 322 |
|
| 323 |
-
# ✅ CRIAR corr
|
| 324 |
corr = df.corr()
|
| 325 |
-
|
| 326 |
if corr.isnull().values.all():
|
| 327 |
return None
|
| 328 |
|
| 329 |
-
# ✅ remover diagonal (SEM numpy in-place)
|
| 330 |
mask = np.eye(len(corr), dtype=bool)
|
| 331 |
corr = corr.where(~mask)
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
)
|
| 339 |
-
|
| 340 |
-
fig = go.Figure(go.Heatmap(
|
| 341 |
-
z=corr.values,
|
| 342 |
-
x=corr.columns,
|
| 343 |
-
y=corr.index,
|
| 344 |
-
text=text,
|
| 345 |
-
texttemplate="%{text}",
|
| 346 |
-
textfont=dict(size=10),
|
| 347 |
-
zmin=-1,
|
| 348 |
-
zmax=1,
|
| 349 |
-
zmid=0,
|
| 350 |
-
colorscale = [
|
| 351 |
-
[0.00, "rgb(103,0,31)"],
|
| 352 |
-
[0.08, "rgb(178,24,43)"],
|
| 353 |
-
[0.16, "rgb(214,96,77)"],
|
| 354 |
-
[0.24, "rgb(244,165,130)"],
|
| 355 |
-
[0.32, "rgb(253,219,199)"],
|
| 356 |
-
|
| 357 |
-
# faixa branca larga (inalterada)
|
| 358 |
-
[0.45, "rgb(255,255,255)"],
|
| 359 |
-
[0.55, "rgb(255,255,255)"],
|
| 360 |
-
|
| 361 |
-
[0.68, "rgb(209,229,240)"],
|
| 362 |
-
[0.76, "rgb(146,197,222)"],
|
| 363 |
-
[0.84, "rgb(67,147,195)"],
|
| 364 |
-
[0.92, "rgb(33,102,172)"],
|
| 365 |
-
[1.00, "rgb(5,48,97)"]
|
| 366 |
-
],
|
| 367 |
-
colorbar=dict(title='Correlação'),
|
| 368 |
-
hovertemplate="%{x} × %{y}<br>ρ = %{z:.3f}<extra></extra>"
|
| 369 |
-
))
|
| 370 |
-
|
| 371 |
-
# ✅ diagonal VISUAL (robusta)
|
| 372 |
fig.add_shape(
|
| 373 |
type="line",
|
| 374 |
xref="paper",
|
| 375 |
yref="paper",
|
| 376 |
-
x0=0,
|
| 377 |
-
|
|
|
|
|
|
|
| 378 |
line=dict(color="rgba(0,0,0,0.35)", width=1),
|
| 379 |
-
layer="above"
|
| 380 |
)
|
| 381 |
-
|
| 382 |
fig.update_layout(
|
| 383 |
title=dict(text="Matriz de Correlação", x=0.5),
|
| 384 |
height=600,
|
| 385 |
-
template=
|
| 386 |
xaxis=dict(tickangle=45, showgrid=False),
|
| 387 |
-
yaxis=dict(autorange=
|
| 388 |
)
|
| 389 |
-
|
| 390 |
return fig
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
print(f"Erro na geração do gráfico: {e}")
|
| 394 |
traceback.print_exc()
|
| 395 |
return None
|
| 396 |
|
| 397 |
-
# def _criar_grafico_correlacao(modelos_sm):
|
| 398 |
-
# """Gera heatmap de correlação (Plotly Figure)."""
|
| 399 |
-
# try:
|
| 400 |
-
# if modelos_sm is None or not hasattr(modelos_sm, 'model'):
|
| 401 |
-
# return None
|
| 402 |
-
|
| 403 |
-
# model = modelos_sm.model
|
| 404 |
-
# if not hasattr(model, 'exog') or not hasattr(model, 'endog'):
|
| 405 |
-
# return None
|
| 406 |
-
|
| 407 |
-
# X = model.exog
|
| 408 |
-
# X_names = model.exog_names
|
| 409 |
-
# y = model.endog
|
| 410 |
-
# y_name = getattr(model, 'endog_names', 'Variável Dependente')
|
| 411 |
-
|
| 412 |
-
# df_X = pd.DataFrame(X, columns=X_names)
|
| 413 |
-
|
| 414 |
-
# # Remover constantes explícitas
|
| 415 |
-
# df_X = df_X.drop(
|
| 416 |
-
# columns=[c for c in df_X.columns if c.lower() in ('const', 'intercept')],
|
| 417 |
-
# errors='ignore'
|
| 418 |
-
# )
|
| 419 |
-
|
| 420 |
-
# df_y = pd.DataFrame({y_name: y})
|
| 421 |
-
# df = pd.concat([df_y, df_X], axis=1)
|
| 422 |
-
|
| 423 |
-
# # Remover colunas constantes
|
| 424 |
-
# variancias = df.var(ddof=0)
|
| 425 |
-
# df = df.loc[:, variancias.fillna(0) > 0]
|
| 426 |
-
|
| 427 |
-
# if df.shape[1] < 2: return None
|
| 428 |
-
|
| 429 |
-
# corr = df.corr()
|
| 430 |
-
# if corr.isnull().values.all(): return None
|
| 431 |
-
|
| 432 |
-
# text = np.round(corr.values, 2).astype(str)
|
| 433 |
-
|
| 434 |
-
# fig = go.Figure(data=go.Heatmap(
|
| 435 |
-
# z=corr.values,
|
| 436 |
-
# x=corr.columns,
|
| 437 |
-
# y=corr.index,
|
| 438 |
-
# text=text,
|
| 439 |
-
# texttemplate="%{text}",
|
| 440 |
-
# textfont=dict(size=10),
|
| 441 |
-
# zmin=-1, zmax=1,
|
| 442 |
-
# colorscale = [
|
| 443 |
-
# [0.0, "rgb(178,24,43)"], # red
|
| 444 |
-
# [0.35, "rgb(178,24,43)"],
|
| 445 |
-
|
| 446 |
-
# [0.45, "rgb(255,255,255)"], # start white
|
| 447 |
-
# [0.55, "rgb(255,255,255)"], # end white
|
| 448 |
-
|
| 449 |
-
# [0.65, "rgb(33,102,172)"],
|
| 450 |
-
# [1.0, "rgb(33,102,172)"] # blue
|
| 451 |
-
# ],
|
| 452 |
-
# colorbar=dict(title='Correlação')
|
| 453 |
-
# ))
|
| 454 |
-
|
| 455 |
-
# fig.update_layout(
|
| 456 |
-
# title=dict(text="Matriz de Correlação", x=0.5),
|
| 457 |
-
# height=600,
|
| 458 |
-
# template='plotly_white',
|
| 459 |
-
# xaxis=dict(tickangle=45, showgrid=False),
|
| 460 |
-
# yaxis=dict(autorange='reversed', showgrid=True)
|
| 461 |
-
# )
|
| 462 |
-
|
| 463 |
-
# return fig
|
| 464 |
-
# except Exception:
|
| 465 |
-
# return None
|
| 466 |
|
| 467 |
def gerar_todos_graficos(pacote):
|
| 468 |
-
""
|
| 469 |
-
graficos = {
|
| 470 |
-
"obs_calc": None,
|
| 471 |
-
"residuos": None,
|
| 472 |
-
"hist": None,
|
| 473 |
-
"cook": None,
|
| 474 |
-
"corr": None
|
| 475 |
-
}
|
| 476 |
|
| 477 |
obs_calc = pacote["modelo"]["obs_calc"]
|
| 478 |
modelos_sm = pacote["modelo"]["sm"]
|
|
@@ -483,81 +324,60 @@ def gerar_todos_graficos(pacote):
|
|
| 483 |
if primeira_linha:
|
| 484 |
nome_y = primeira_linha.split(": ", 1)[0].strip() or None
|
| 485 |
|
| 486 |
-
# Identificar vetores
|
| 487 |
y_obs = None
|
| 488 |
y_calc = None
|
| 489 |
residuos = None
|
| 490 |
indices = None
|
| 491 |
|
| 492 |
-
# Tenta pegar do DataFrame obs_calc
|
| 493 |
if obs_calc is not None and not obs_calc.empty:
|
| 494 |
cols_lower = {str(c).lower(): c for c in obs_calc.columns}
|
| 495 |
-
|
| 496 |
-
# Extrair índices do DataFrame
|
| 497 |
indices = obs_calc.index.values if obs_calc.index is not None else None
|
| 498 |
|
| 499 |
-
|
| 500 |
-
for nome in ['observado', 'obs', 'y_obs', 'y', 'valor_observado']:
|
| 501 |
if nome in cols_lower:
|
| 502 |
y_obs = obs_calc[cols_lower[nome]].values
|
| 503 |
break
|
| 504 |
-
|
| 505 |
-
# Encontrar coluna calculada
|
| 506 |
-
for nome in ['calculado', 'calc', 'y_calc', 'y_hat', 'previsto']:
|
| 507 |
if nome in cols_lower:
|
| 508 |
y_calc = obs_calc[cols_lower[nome]].values
|
| 509 |
break
|
| 510 |
-
|
| 511 |
-
# Encontrar coluna resíduos
|
| 512 |
-
for nome in ['residuo', 'residuos', 'resid']:
|
| 513 |
if nome in cols_lower:
|
| 514 |
residuos = obs_calc[cols_lower[nome]].values
|
| 515 |
break
|
| 516 |
|
| 517 |
-
# Fallback para o objeto statsmodels
|
| 518 |
if modelos_sm is not None:
|
| 519 |
try:
|
| 520 |
-
if y_obs is None and hasattr(modelos_sm.model,
|
| 521 |
y_obs = modelos_sm.model.endog
|
| 522 |
-
if y_calc is None and hasattr(modelos_sm,
|
| 523 |
y_calc = modelos_sm.fittedvalues
|
| 524 |
-
if residuos is None and hasattr(modelos_sm,
|
| 525 |
residuos = modelos_sm.resid
|
| 526 |
except Exception:
|
| 527 |
pass
|
| 528 |
|
| 529 |
-
# Cálculo manual se necessário
|
| 530 |
if residuos is None and y_obs is not None and y_calc is not None:
|
| 531 |
residuos = np.array(y_obs) - np.array(y_calc)
|
| 532 |
|
| 533 |
-
# Converter para numpy
|
| 534 |
y_obs = np.array(y_obs) if y_obs is not None else None
|
| 535 |
y_calc = np.array(y_calc) if y_calc is not None else None
|
| 536 |
residuos = np.array(residuos) if residuos is not None else None
|
| 537 |
|
| 538 |
-
# Gerar cada gráfico
|
| 539 |
if y_obs is not None and y_calc is not None:
|
| 540 |
graficos["obs_calc"] = _criar_grafico_obs_calc(y_obs, y_calc, indices)
|
| 541 |
-
|
| 542 |
if residuos is not None and y_calc is not None:
|
| 543 |
graficos["residuos"] = _criar_grafico_residuos(y_calc, residuos, indices)
|
| 544 |
-
|
| 545 |
if residuos is not None:
|
| 546 |
graficos["hist"] = _criar_histograma_residuos(residuos)
|
| 547 |
-
|
| 548 |
if modelos_sm is not None:
|
| 549 |
graficos["cook"] = _criar_grafico_cook(modelos_sm)
|
| 550 |
graficos["corr"] = _criar_grafico_correlacao(modelos_sm, nome_y=nome_y)
|
| 551 |
|
| 552 |
return graficos
|
| 553 |
|
| 554 |
-
|
| 555 |
-
# FUNÇÃO: REORGANIZAR DIAGNOSTICOS PARA EXIBIÇÃO
|
| 556 |
-
# ============================================================
|
| 557 |
def reorganizar_modelos_resumos(diagnosticos):
|
| 558 |
-
"""
|
| 559 |
-
Converte diagnosticos v2 (já agrupado) para formato de exibição HTML.
|
| 560 |
-
"""
|
| 561 |
gerais = diagnosticos.get("gerais", {})
|
| 562 |
return {
|
| 563 |
"estatisticas_gerais": {
|
|
@@ -567,19 +387,19 @@ def reorganizar_modelos_resumos(diagnosticos):
|
|
| 567 |
"mse": {"nome": "MSE", "valor": gerais.get("mse")},
|
| 568 |
"r2": {"nome": "R²", "valor": gerais.get("r2")},
|
| 569 |
"r2_ajustado": {"nome": "R² ajustado", "valor": gerais.get("r2_ajustado")},
|
| 570 |
-
"r_pearson": {"nome": "Correlação Pearson", "valor": gerais.get("r_pearson")}
|
| 571 |
},
|
| 572 |
"teste_f": {
|
| 573 |
"nome": "Teste F",
|
| 574 |
"estatistica": diagnosticos.get("teste_f", {}).get("estatistica"),
|
| 575 |
"pvalor": diagnosticos.get("teste_f", {}).get("p_valor"),
|
| 576 |
-
"interpretacao": diagnosticos.get("teste_f", {}).get("interpretacao")
|
| 577 |
},
|
| 578 |
"teste_ks": {
|
| 579 |
"nome": "Teste de Normalidade (Kolmogorov-Smirnov)",
|
| 580 |
"estatistica": diagnosticos.get("teste_ks", {}).get("estatistica"),
|
| 581 |
"pvalor": diagnosticos.get("teste_ks", {}).get("p_valor"),
|
| 582 |
-
"interpretacao": diagnosticos.get("teste_ks", {}).get("interpretacao")
|
| 583 |
},
|
| 584 |
"perc_resid": {
|
| 585 |
"nome": "Teste de Normalidade (Comparação com a Curva Normal)",
|
|
@@ -587,59 +407,54 @@ def reorganizar_modelos_resumos(diagnosticos):
|
|
| 587 |
"interpretacao": [
|
| 588 |
"Ideal 68% → aceitável entre 64% e 75%",
|
| 589 |
"Ideal 90% → aceitável entre 88% e 95%",
|
| 590 |
-
"Ideal 95% → aceitável entre 95% e 100%"
|
| 591 |
-
]
|
| 592 |
},
|
| 593 |
"teste_dw": {
|
| 594 |
"nome": "Teste de Autocorrelação (Durbin-Watson)",
|
| 595 |
"estatistica": diagnosticos.get("teste_dw", {}).get("estatistica"),
|
| 596 |
-
"interpretacao": diagnosticos.get("teste_dw", {}).get("interpretacao")
|
| 597 |
},
|
| 598 |
"teste_bp": {
|
| 599 |
"nome": "Teste de Homocedasticidade (Breusch-Pagan)",
|
| 600 |
"estatistica": diagnosticos.get("teste_bp", {}).get("estatistica"),
|
| 601 |
"pvalor": diagnosticos.get("teste_bp", {}).get("p_valor"),
|
| 602 |
-
"interpretacao": diagnosticos.get("teste_bp", {}).get("interpretacao")
|
| 603 |
},
|
| 604 |
-
"equacao": diagnosticos.get("equacao")
|
| 605 |
}
|
| 606 |
|
| 607 |
-
|
| 608 |
-
# FUNÇÃO: FORMATAR VALOR MONETÁRIO
|
| 609 |
-
# ============================================================
|
| 610 |
def formatar_monetario(valor):
|
| 611 |
-
if pd.isna(valor):
|
|
|
|
| 612 |
return f"R$ {valor:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
| 613 |
|
| 614 |
-
# ============================================================
|
| 615 |
-
# FUNÇÃO: FORMATAR RESUMO COMO HTML
|
| 616 |
-
# ============================================================
|
| 617 |
-
def formatar_resumo_html(resumo_reorganizado):
|
| 618 |
|
|
|
|
| 619 |
def criar_titulo_secao(titulo):
|
| 620 |
return f'<div class="section-title-orange-solid">{titulo}</div>'
|
| 621 |
|
| 622 |
def criar_linha_campo(campo, valor):
|
| 623 |
-
return f
|
| 624 |
|
| 625 |
def criar_linha_interpretacao(interpretacao):
|
| 626 |
-
return f
|
| 627 |
|
| 628 |
def formatar_numero(valor, casas_decimais=4):
|
| 629 |
-
if valor is None:
|
| 630 |
-
|
|
|
|
|
|
|
| 631 |
return str(valor)
|
| 632 |
|
| 633 |
linhas_html = []
|
| 634 |
-
|
| 635 |
-
# 1. Estatísticas Gerais
|
| 636 |
estat_gerais = resumo_reorganizado.get("estatisticas_gerais", {})
|
| 637 |
if estat_gerais:
|
| 638 |
linhas_html.append(criar_titulo_secao("Estatísticas Gerais"))
|
| 639 |
for chave, dados in estat_gerais.items():
|
| 640 |
linhas_html.append(criar_linha_campo(dados.get("nome", chave), formatar_numero(dados.get("valor"))))
|
| 641 |
|
| 642 |
-
# 2. Testes (ordem: F, KS, Curva Normal, DW, BP)
|
| 643 |
for chave_teste, label in [("teste_f", "Estatística F"), ("teste_ks", "Estatística KS")]:
|
| 644 |
teste = resumo_reorganizado.get(chave_teste, {})
|
| 645 |
if teste.get("estatistica") is not None:
|
|
@@ -650,7 +465,6 @@ def formatar_resumo_html(resumo_reorganizado):
|
|
| 650 |
if teste.get("interpretacao"):
|
| 651 |
linhas_html.append(criar_linha_interpretacao(teste["interpretacao"]))
|
| 652 |
|
| 653 |
-
# Percentuais (Comparação com a Curva Normal — logo após KS)
|
| 654 |
perc_resid = resumo_reorganizado.get("perc_resid", {})
|
| 655 |
if perc_resid.get("valor") is not None:
|
| 656 |
linhas_html.append(criar_titulo_secao(perc_resid.get("nome")))
|
|
@@ -670,24 +484,24 @@ def formatar_resumo_html(resumo_reorganizado):
|
|
| 670 |
if teste.get("interpretacao"):
|
| 671 |
linhas_html.append(criar_linha_interpretacao(teste["interpretacao"]))
|
| 672 |
|
| 673 |
-
return f
|
|
|
|
| 674 |
|
| 675 |
-
# ============================================================
|
| 676 |
-
# FUNÇÃO: CRIAR TÍTULO DE SEÇÃO ESTILIZADO
|
| 677 |
-
# ============================================================
|
| 678 |
def criar_titulo_secao_html(titulo):
|
| 679 |
return f'<div class="section-title-orange">{titulo}</div>'
|
| 680 |
|
| 681 |
-
|
| 682 |
-
# FUNÇÃO: FORMATAR ESCALAS/TRANSFORMAÇÕES
|
| 683 |
-
# ============================================================
|
| 684 |
def formatar_escalas_html(escalas_raw):
|
| 685 |
-
if isinstance(escalas_raw, pd.DataFrame):
|
| 686 |
-
|
| 687 |
-
|
|
|
|
|
|
|
|
|
|
| 688 |
|
| 689 |
itens = [str(item) for item in itens if item and str(item).strip()]
|
| 690 |
-
if not itens:
|
|
|
|
| 691 |
|
| 692 |
max_chars = max(len(item) for item in itens)
|
| 693 |
largura_min = max(150, max_chars * 7 + 28)
|
|
@@ -695,19 +509,22 @@ def formatar_escalas_html(escalas_raw):
|
|
| 695 |
for item in itens:
|
| 696 |
if ":" in item:
|
| 697 |
partes = item.split(":", 1)
|
| 698 |
-
conteudo =
|
|
|
|
|
|
|
|
|
|
| 699 |
else:
|
| 700 |
-
conteudo = f
|
| 701 |
-
cards_html += f
|
| 702 |
|
| 703 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 704 |
|
| 705 |
|
| 706 |
def _aplicar_jitter_sobrepostos(df_mapa, lat_col, lon_col, lat_plot_col, lon_plot_col):
|
| 707 |
-
"""
|
| 708 |
-
Aplica jitter visual mínimo para separar pontos com coordenadas idênticas.
|
| 709 |
-
Não altera as coordenadas originais da base.
|
| 710 |
-
"""
|
| 711 |
df_plot = df_mapa.copy()
|
| 712 |
df_plot[lat_plot_col] = pd.to_numeric(df_plot[lat_col], errors="coerce")
|
| 713 |
df_plot[lon_plot_col] = pd.to_numeric(df_plot[lon_col], errors="coerce")
|
|
@@ -722,7 +539,6 @@ def _aplicar_jitter_sobrepostos(df_mapa, lat_col, lon_col, lat_plot_col, lon_plo
|
|
| 722 |
passo_metros = 4.0
|
| 723 |
max_raio_metros = 22.0
|
| 724 |
metros_por_grau_lat = 111_320.0
|
| 725 |
-
|
| 726 |
lat_plot_pos = int(df_plot.columns.get_loc(lat_plot_col))
|
| 727 |
lon_plot_pos = int(df_plot.columns.get_loc(lon_plot_col))
|
| 728 |
|
|
@@ -743,7 +559,6 @@ def _aplicar_jitter_sobrepostos(df_mapa, lat_col, lon_col, lat_plot_col, lon_plo
|
|
| 743 |
for pos, pos_idx in enumerate(posicoes):
|
| 744 |
if pos == 0:
|
| 745 |
continue
|
| 746 |
-
|
| 747 |
pos_ring = pos - 1
|
| 748 |
ring = 1
|
| 749 |
while pos_ring >= (6 * ring):
|
|
@@ -753,7 +568,6 @@ def _aplicar_jitter_sobrepostos(df_mapa, lat_col, lon_col, lat_plot_col, lon_plo
|
|
| 753 |
slots_ring = max(6 * ring, 1)
|
| 754 |
angulo = angulo_base + (2.0 * math.pi * (pos_ring / slots_ring))
|
| 755 |
raio_m = min(ring * passo_metros, max_raio_metros)
|
| 756 |
-
|
| 757 |
delta_lat = (raio_m * math.sin(angulo)) / metros_por_grau_lat
|
| 758 |
delta_lon = (raio_m * math.cos(angulo)) / metros_por_grau_lon
|
| 759 |
|
|
@@ -791,8 +605,18 @@ def _montar_popup_registro_paginado(itens, popup_uid, max_itens_pagina=8):
|
|
| 791 |
itens_por_pagina = max_itens_pagina * max_colunas_por_pagina
|
| 792 |
paginas = [itens[i:i + itens_por_pagina] for i in range(0, len(itens), itens_por_pagina)]
|
| 793 |
itens_primeira_pagina = len(paginas[0]) if paginas else 0
|
| 794 |
-
colunas_visiveis = max(
|
| 795 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 796 |
|
| 797 |
pages_html = []
|
| 798 |
for page_idx, page_items in enumerate(paginas):
|
|
@@ -834,11 +658,11 @@ def _montar_popup_registro_paginado(itens, popup_uid, max_itens_pagina=8):
|
|
| 834 |
if len(paginas) > 1:
|
| 835 |
controls_html = (
|
| 836 |
"<div class='mesa-popup-controls' style='display:flex; gap:5px; flex-wrap:nowrap; margin-top:8px; align-items:center; justify-content:center; white-space:nowrap; width:100%;'>"
|
| 837 |
-
|
| 838 |
-
|
| 839 |
"<div data-page-number-wrap='1' style='display:flex; gap:5px; align-items:center; justify-content:center; flex-wrap:nowrap;'></div>"
|
| 840 |
-
|
| 841 |
-
|
| 842 |
"</div>"
|
| 843 |
)
|
| 844 |
|
|
@@ -901,9 +725,7 @@ def _montar_popup_registro_placeholder(session_id, row_id, popup_uid, popup_endp
|
|
| 901 |
)
|
| 902 |
return html, 380
|
| 903 |
|
| 904 |
-
|
| 905 |
-
# FUNÇÃO: GERAR MAPA FOLIUM (com suporte a dimensionamento por variável)
|
| 906 |
-
# ============================================================
|
| 907 |
def criar_mapa(
|
| 908 |
df,
|
| 909 |
lat_col="lat",
|
|
@@ -916,20 +738,6 @@ def criar_mapa(
|
|
| 916 |
popup_auth_token=None,
|
| 917 |
avaliandos_tecnicos=None,
|
| 918 |
):
|
| 919 |
-
"""
|
| 920 |
-
Cria mapa Folium com os dados, com suporte a dimensionamento proporcional.
|
| 921 |
-
|
| 922 |
-
Parâmetros:
|
| 923 |
-
df: DataFrame com os dados
|
| 924 |
-
lat_col: nome da coluna de latitude
|
| 925 |
-
lon_col: nome da coluna de longitude
|
| 926 |
-
cor_col: coluna para colorir os pontos (opcional)
|
| 927 |
-
tamanho_col: coluna numérica para dimensionar os círculos (opcional)
|
| 928 |
-
|
| 929 |
-
Retorna:
|
| 930 |
-
HTML do mapa
|
| 931 |
-
"""
|
| 932 |
-
# Verifica se colunas existem (primeira ocorrência, robusto para colunas duplicadas)
|
| 933 |
lat_real = None
|
| 934 |
lon_real = None
|
| 935 |
for col in df.columns:
|
|
@@ -950,7 +758,6 @@ def criar_mapa(
|
|
| 950 |
return None
|
| 951 |
return dataframe.iloc[:, matches[0]]
|
| 952 |
|
| 953 |
-
# Filtra dados válidos (numéricos e dentro dos limites geográficos)
|
| 954 |
df_mapa = df.copy()
|
| 955 |
lat_key = "__mesa_lat__"
|
| 956 |
lon_key = "__mesa_lon__"
|
|
@@ -963,34 +770,30 @@ def criar_mapa(
|
|
| 963 |
df_mapa[lon_key] = pd.to_numeric(lon_serie, errors="coerce")
|
| 964 |
df_mapa = df_mapa.dropna(subset=[lat_key, lon_key])
|
| 965 |
df_mapa = df_mapa[
|
| 966 |
-
(df_mapa[lat_key] >= -90.0)
|
| 967 |
-
|
|
|
|
|
|
|
| 968 |
].copy()
|
| 969 |
if df_mapa.empty:
|
| 970 |
return "<p>Sem coordenadas válidas para exibir.</p>"
|
| 971 |
|
| 972 |
-
# Cria mapa
|
| 973 |
centro_lat = float(df_mapa[lat_key].median())
|
| 974 |
centro_lon = float(df_mapa[lon_key].median())
|
| 975 |
-
|
| 976 |
-
m = folium.Map(
|
| 977 |
location=[centro_lat, centro_lon],
|
| 978 |
zoom_start=12,
|
| 979 |
tiles=None,
|
| 980 |
prefer_canvas=True,
|
| 981 |
control_scale=True,
|
| 982 |
)
|
|
|
|
|
|
|
|
|
|
| 983 |
|
| 984 |
-
# Camadas base
|
| 985 |
-
folium.TileLayer(tiles="OpenStreetMap", name="OpenStreetMap", control=True, show=True).add_to(m)
|
| 986 |
-
folium.TileLayer(tiles="CartoDB positron", name="Positron", control=True, show=False).add_to(m)
|
| 987 |
-
add_bairros_layer(m, show=True)
|
| 988 |
-
|
| 989 |
-
# Se tamanho_col fornecido mas cor_col não, usa mesma variável para cor
|
| 990 |
if tamanho_col and tamanho_col != "Visualização Padrão" and not cor_col:
|
| 991 |
cor_col = tamanho_col
|
| 992 |
|
| 993 |
-
# Colormap se houver coluna de cor (verde → vermelho)
|
| 994 |
colormap = None
|
| 995 |
cor_key = None
|
| 996 |
if cor_col and cor_col in df_mapa.columns:
|
|
@@ -1005,11 +808,10 @@ def criar_mapa(
|
|
| 1005 |
colors=["#2ecc71", "#a8e06c", "#f1c40f", "#e67e22", "#e74c3c"],
|
| 1006 |
vmin=vmin,
|
| 1007 |
vmax=vmax,
|
| 1008 |
-
caption=cor_col
|
| 1009 |
)
|
| 1010 |
-
colormap.add_to(
|
| 1011 |
|
| 1012 |
-
# Escala de tamanho proporcional
|
| 1013 |
raio_min, raio_max = 3, 18
|
| 1014 |
tamanho_func = None
|
| 1015 |
tamanho_key = None
|
|
@@ -1055,15 +857,9 @@ def criar_mapa(
|
|
| 1055 |
contorno_padrao = 0.8 if total_pontos_plot <= 2500 else 0.55
|
| 1056 |
opacidade_preenchimento = 0.68 if total_pontos_plot <= 2500 else 0.6
|
| 1057 |
|
| 1058 |
-
# Adiciona pontos
|
| 1059 |
for marker_ordem, (idx, row) in enumerate(df_plot_pontos.iterrows()):
|
| 1060 |
-
|
| 1061 |
-
if colormap and cor_key and pd.notna(row[cor_key]):
|
| 1062 |
-
cor = colormap(row[cor_key])
|
| 1063 |
-
else:
|
| 1064 |
-
cor = COR_PRINCIPAL
|
| 1065 |
|
| 1066 |
-
# Calcula raio
|
| 1067 |
if tamanho_func and tamanho_key and pd.notna(row[tamanho_key]):
|
| 1068 |
raio = tamanho_func(row[tamanho_key])
|
| 1069 |
peso_contorno = 1
|
|
@@ -1071,8 +867,6 @@ def criar_mapa(
|
|
| 1071 |
raio = raio_padrao
|
| 1072 |
peso_contorno = contorno_padrao
|
| 1073 |
|
| 1074 |
-
# Tooltip (hover): índice + variável selecionada no dropdown (ou dependente como fallback)
|
| 1075 |
-
# Usa coluna "index" (original, gerada pelo reset_index) quando disponível
|
| 1076 |
idx_display = int(row["index"]) if "index" in row.index else idx
|
| 1077 |
popup_uid = f"mesa-pop-{marker_ordem}"
|
| 1078 |
popup_html = None
|
|
@@ -1092,9 +886,9 @@ def criar_mapa(
|
|
| 1092 |
popup_width = None
|
| 1093 |
if popup_html is None or popup_width is None:
|
| 1094 |
popup_html, popup_width = montar_popup_registro_html(row, popup_uid, max_itens_pagina=8)
|
|
|
|
| 1095 |
tooltip_html = (
|
| 1096 |
-
"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:14px;"
|
| 1097 |
-
" line-height:1.7; padding:2px 4px;'>"
|
| 1098 |
f"<b>Índice {idx_display}</b>"
|
| 1099 |
)
|
| 1100 |
if tooltip_col and tooltip_key and tooltip_key in row.index:
|
|
@@ -1107,10 +901,7 @@ def criar_mapa(
|
|
| 1107 |
val_str = f"{val_t:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
| 1108 |
else:
|
| 1109 |
val_str = str(val_t)
|
| 1110 |
-
tooltip_html +=
|
| 1111 |
-
f"<br><span style='color:#555;'>{tooltip_col}:</span>"
|
| 1112 |
-
f" <b>{val_str}</b>"
|
| 1113 |
-
)
|
| 1114 |
tooltip_html += "</div>"
|
| 1115 |
|
| 1116 |
marcador = folium.CircleMarker(
|
|
@@ -1118,12 +909,12 @@ def criar_mapa(
|
|
| 1118 |
radius=raio,
|
| 1119 |
popup=folium.Popup(popup_html, max_width=popup_width),
|
| 1120 |
tooltip=folium.Tooltip(tooltip_html, sticky=True),
|
| 1121 |
-
color=
|
| 1122 |
weight=peso_contorno,
|
| 1123 |
fill=True,
|
| 1124 |
fillColor=cor,
|
| 1125 |
-
fillOpacity=opacidade_preenchimento
|
| 1126 |
-
).add_to(
|
| 1127 |
marcador.options["mesaBaseRadius"] = float(max(1.0, raio))
|
| 1128 |
|
| 1129 |
if mostrar_indices and camada_indices is not None:
|
|
@@ -1135,26 +926,24 @@ def criar_mapa(
|
|
| 1135 |
)
|
| 1136 |
|
| 1137 |
if mostrar_indices and camada_indices is not None:
|
| 1138 |
-
camada_indices.add_to(
|
| 1139 |
|
| 1140 |
if avaliandos_tecnicos:
|
| 1141 |
camada_trabalhos_tecnicos = folium.FeatureGroup(name="Avaliandos que usaram o modelo", show=True)
|
| 1142 |
add_trabalhos_tecnicos_markers(camada_trabalhos_tecnicos, avaliandos_tecnicos)
|
| 1143 |
-
camada_trabalhos_tecnicos.add_to(
|
| 1144 |
|
| 1145 |
-
|
| 1146 |
-
|
| 1147 |
-
plugins.Fullscreen().add_to(m)
|
| 1148 |
plugins.MeasureControl(
|
| 1149 |
-
primary_length_unit=
|
| 1150 |
-
secondary_length_unit=
|
| 1151 |
-
primary_area_unit=
|
| 1152 |
-
secondary_area_unit=
|
| 1153 |
-
).add_to(
|
| 1154 |
-
add_zoom_responsive_circle_markers(
|
| 1155 |
-
add_popup_pagination_handlers(
|
| 1156 |
-
|
| 1157 |
-
# Ajusta bounds robustos para evitar "salto" por pontos geocodificados distantes.
|
| 1158 |
df_bounds = df_mapa
|
| 1159 |
if len(df_mapa) >= 8:
|
| 1160 |
lat_vals = df_mapa[lat_key]
|
|
@@ -1163,12 +952,10 @@ def criar_mapa(
|
|
| 1163 |
lon_med = float(lon_vals.median())
|
| 1164 |
lat_mad = float((lat_vals - lat_med).abs().median())
|
| 1165 |
lon_mad = float((lon_vals - lon_med).abs().median())
|
| 1166 |
-
|
| 1167 |
lat_span = float(lat_vals.max() - lat_vals.min())
|
| 1168 |
lon_span = float(lon_vals.max() - lon_vals.min())
|
| 1169 |
lat_scale = max(lat_mad, lat_span / 30.0, 1e-6)
|
| 1170 |
lon_scale = max(lon_mad, lon_span / 30.0, 1e-6)
|
| 1171 |
-
|
| 1172 |
score = ((lat_vals - lat_med) / lat_scale) ** 2 + ((lon_vals - lon_med) / lon_scale) ** 2
|
| 1173 |
lim = float(score.quantile(0.75))
|
| 1174 |
df_core = df_mapa[score <= lim]
|
|
@@ -1196,326 +983,16 @@ def criar_mapa(
|
|
| 1196 |
lon_min = float(lon_min) - lon_delta
|
| 1197 |
lon_max = float(lon_max) + lon_delta
|
| 1198 |
|
| 1199 |
-
|
| 1200 |
-
|
| 1201 |
-
|
| 1202 |
-
# Evita o wrapper de notebook (_repr_html_), que pode falhar dentro de iframe srcDoc.
|
| 1203 |
-
return m.get_root().render()
|
| 1204 |
-
|
| 1205 |
-
# ============================================================
|
| 1206 |
-
# FUNÇÃO: CARREGAR + VALIDAR MODELO (.dai)
|
| 1207 |
-
# ============================================================
|
| 1208 |
-
def carregar_modelo_gradio(arquivo):
|
| 1209 |
-
if arquivo is None: return None, "Nenhum arquivo enviado."
|
| 1210 |
-
try:
|
| 1211 |
-
pacote = load(arquivo.name)
|
| 1212 |
-
if not isinstance(pacote, dict): return None, "Arquivo inválido."
|
| 1213 |
-
# Retrocompatibilidade: converte v1 (flat) para v2 (nested)
|
| 1214 |
-
if "versao" not in pacote:
|
| 1215 |
-
pacote = _migrar_pacote_v1_para_v2(pacote)
|
| 1216 |
-
faltantes = [k for k in CHAVES_ESPERADAS if k not in pacote]
|
| 1217 |
-
if faltantes: return None, f"Pacote incompleto. Faltando: {faltantes}"
|
| 1218 |
-
return pacote, f"Modelo carregado: {os.path.basename(arquivo.name)}"
|
| 1219 |
-
except Exception as e: return None, f"Erro: {e}"
|
| 1220 |
-
|
| 1221 |
-
# ============================================================
|
| 1222 |
-
# FUNÇÃO: DESEMPACOTAR + EXIBIR CONTEÚDO
|
| 1223 |
-
# ============================================================
|
| 1224 |
-
def exibir_modelo(pacote):
|
| 1225 |
-
# Retorna Nones se pacote vazio. Total de outputs = 14 (+ dropdown choices)
|
| 1226 |
-
if pacote is None:
|
| 1227 |
-
return [None] * 13 + [gr.update(choices=["Visualização Padrão"])]
|
| 1228 |
-
|
| 1229 |
-
# 1. Dados
|
| 1230 |
-
dados = pacote["dados"]["df"].reset_index()
|
| 1231 |
-
for col in dados.columns:
|
| 1232 |
-
if str(col).lower() in ["lat", "lon"]: dados[col] = dados[col].round(6)
|
| 1233 |
-
elif pd.api.types.is_numeric_dtype(dados[col]): dados[col] = dados[col].round(2)
|
| 1234 |
-
|
| 1235 |
-
# 2. Estatísticas
|
| 1236 |
-
estat = pd.DataFrame(pacote["dados"]["estatisticas"])
|
| 1237 |
-
if not isinstance(estat.index, pd.RangeIndex):
|
| 1238 |
-
estat.insert(0, "Variável", estat.index.astype(str))
|
| 1239 |
-
estat = estat.reset_index(drop=True)
|
| 1240 |
-
estat = estat.round(2)
|
| 1241 |
-
|
| 1242 |
-
# 3. Escalas
|
| 1243 |
-
escalas_html = formatar_escalas_html(pacote["transformacoes"]["info"])
|
| 1244 |
-
|
| 1245 |
-
# 4. X e y
|
| 1246 |
-
X = pacote["transformacoes"]["X"].reset_index()
|
| 1247 |
-
y = pacote["transformacoes"]["y"].reset_index()
|
| 1248 |
-
if 'index' in y.columns and 'index' in X.columns: y = y.drop(columns=['index'])
|
| 1249 |
-
df_X_y = pd.concat([X, y], axis=1).loc[:, ~pd.concat([X, y], axis=1).columns.duplicated()].round(2)
|
| 1250 |
-
|
| 1251 |
-
# 5. Resumo
|
| 1252 |
-
resumo_html = formatar_resumo_html(reorganizar_modelos_resumos(pacote["modelo"]["diagnosticos"]))
|
| 1253 |
-
|
| 1254 |
-
# 6. Coeficientes
|
| 1255 |
-
tab_coef = pd.DataFrame(pacote["modelo"]["coeficientes"])
|
| 1256 |
-
if not isinstance(tab_coef.index, pd.RangeIndex):
|
| 1257 |
-
tab_coef.insert(0, "Variável", tab_coef.index.astype(str))
|
| 1258 |
-
tab_coef = tab_coef.reset_index(drop=True)
|
| 1259 |
-
mask = tab_coef["Variável"].str.lower().isin(["intercept", "const", "(intercept)"])
|
| 1260 |
-
if mask.any(): tab_coef = pd.concat([tab_coef[mask], tab_coef[~mask]], ignore_index=True)
|
| 1261 |
-
tab_coef = tab_coef.round(2)
|
| 1262 |
-
|
| 1263 |
-
# 7. Obs x Calc
|
| 1264 |
-
tab_obs_calc = pacote["modelo"]["obs_calc"].reset_index().round(2)
|
| 1265 |
-
|
| 1266 |
-
# 8. Gráficos (GERAÇÃO DINÂMICA)
|
| 1267 |
-
figs_dict = gerar_todos_graficos(pacote)
|
| 1268 |
-
|
| 1269 |
-
# 9. Mapa
|
| 1270 |
-
info_transf = pacote["transformacoes"]["info"]
|
| 1271 |
-
nome_y = info_transf[0].split(": ", 1)[0].strip() if info_transf else None
|
| 1272 |
-
mapa_html = criar_mapa(dados, col_y=nome_y)
|
| 1273 |
-
|
| 1274 |
-
# 10. Dropdown de variáveis para o mapa
|
| 1275 |
-
colunas_numericas = [col for col in dados.select_dtypes(include=[np.number]).columns
|
| 1276 |
-
if str(col).lower() not in ['lat', 'lon', 'latitude', 'longitude', 'index']]
|
| 1277 |
-
choices_mapa = ["Visualização Padrão"] + colunas_numericas
|
| 1278 |
-
|
| 1279 |
-
return (
|
| 1280 |
-
dados,
|
| 1281 |
-
estat,
|
| 1282 |
-
escalas_html,
|
| 1283 |
-
df_X_y,
|
| 1284 |
-
resumo_html,
|
| 1285 |
-
tab_coef,
|
| 1286 |
-
tab_obs_calc,
|
| 1287 |
-
figs_dict["obs_calc"], # Plot 1
|
| 1288 |
-
figs_dict["residuos"], # Plot 2
|
| 1289 |
-
figs_dict["hist"], # Plot 3
|
| 1290 |
-
figs_dict["cook"], # Plot 4
|
| 1291 |
-
figs_dict["corr"], # Plot 5
|
| 1292 |
-
mapa_html,
|
| 1293 |
-
gr.update(choices=choices_mapa, value="Visualização Padrão") # Dropdown update
|
| 1294 |
-
)
|
| 1295 |
-
|
| 1296 |
-
|
| 1297 |
-
def atualizar_mapa_callback(dados, var_mapa, pacote):
|
| 1298 |
-
"""Atualiza o mapa quando a variável de dimensionamento é alterada."""
|
| 1299 |
-
if dados is None or dados.empty:
|
| 1300 |
-
return "<p>Carregue dados para ver o mapa.</p>"
|
| 1301 |
-
|
| 1302 |
-
tamanho_col = None if var_mapa == "Visualização Padrão" else var_mapa
|
| 1303 |
-
nome_y = None
|
| 1304 |
-
if pacote:
|
| 1305 |
-
info_transf = pacote["transformacoes"]["info"]
|
| 1306 |
-
nome_y = info_transf[0].split(": ", 1)[0].strip() if info_transf else None
|
| 1307 |
-
return criar_mapa(dados, tamanho_col=tamanho_col, col_y=nome_y)
|
| 1308 |
-
|
| 1309 |
-
def popular_inputs_avaliacao(pacote):
|
| 1310 |
-
"""Popula campos de avaliação a partir do pacote .dai carregado.
|
| 1311 |
-
|
| 1312 |
-
CONTRACT: Retorna 30 itens (N_ROWS_AVAL + MAX_VARS_AVAL).
|
| 1313 |
-
"""
|
| 1314 |
-
rows_hidden = [gr.update(visible=False)] * N_ROWS_AVAL
|
| 1315 |
-
inputs_hidden = [gr.update(visible=False, value=None, label="")] * MAX_VARS_AVAL
|
| 1316 |
-
|
| 1317 |
-
if pacote is None:
|
| 1318 |
-
return (*rows_hidden, *inputs_hidden)
|
| 1319 |
-
|
| 1320 |
-
info_transf = pacote["transformacoes"]["info"]
|
| 1321 |
-
estatisticas = pacote["dados"]["estatisticas"]
|
| 1322 |
-
# Normaliza para string — colunas_x vem de split de strings, mas listas do DAI
|
| 1323 |
-
# podem ter inteiros se o CSV original tinha colunas numéricas (ex: anos 2015, 2016...)
|
| 1324 |
-
dicotomicas = [str(d) for d in (pacote["transformacoes"].get("dicotomicas", []) or [])]
|
| 1325 |
-
codigo_alocado = [str(c) for c in (pacote["transformacoes"].get("codigo_alocado", []) or [])]
|
| 1326 |
-
percentuais = [str(p) for p in (pacote["transformacoes"].get("percentuais", []) or [])]
|
| 1327 |
-
|
| 1328 |
-
colunas_x = []
|
| 1329 |
-
for item in info_transf[1:]:
|
| 1330 |
-
nome_x, _ = item.split(": ", 1)
|
| 1331 |
-
colunas_x.append(nome_x.strip())
|
| 1332 |
-
|
| 1333 |
-
n_vars = len(colunas_x)
|
| 1334 |
-
n_rows_vis = math.ceil(n_vars / N_COLS_AVAL)
|
| 1335 |
-
|
| 1336 |
-
if estatisticas is not None:
|
| 1337 |
-
est = pd.DataFrame(estatisticas)
|
| 1338 |
-
if "Variável" in est.columns:
|
| 1339 |
-
est_idx = est.set_index("Variável")
|
| 1340 |
-
elif not isinstance(est.index, pd.RangeIndex):
|
| 1341 |
-
est_idx = est
|
| 1342 |
-
else:
|
| 1343 |
-
est_idx = pd.DataFrame()
|
| 1344 |
-
# Normaliza index para string (mesmo motivo das listas acima)
|
| 1345 |
-
if not est_idx.empty:
|
| 1346 |
-
est_idx.index = est_idx.index.map(str)
|
| 1347 |
-
else:
|
| 1348 |
-
est_idx = pd.DataFrame()
|
| 1349 |
-
|
| 1350 |
-
rows_updates = [gr.update(visible=(r < n_rows_vis)) for r in range(N_ROWS_AVAL)]
|
| 1351 |
-
|
| 1352 |
-
inputs_updates = []
|
| 1353 |
-
for i in range(MAX_VARS_AVAL):
|
| 1354 |
-
if i < n_vars:
|
| 1355 |
-
col = colunas_x[i]
|
| 1356 |
-
if col in dicotomicas:
|
| 1357 |
-
placeholder = "0 ou 1"
|
| 1358 |
-
elif col in codigo_alocado and col in est_idx.index:
|
| 1359 |
-
min_val = est_idx.loc[col, "Mínimo"]
|
| 1360 |
-
max_val = est_idx.loc[col, "Máximo"]
|
| 1361 |
-
placeholder = f"cód. {int(min_val)} a {int(max_val)}"
|
| 1362 |
-
elif col in percentuais:
|
| 1363 |
-
placeholder = "0 a 1"
|
| 1364 |
-
elif col in est_idx.index:
|
| 1365 |
-
min_val = est_idx.loc[col, "Mínimo"]
|
| 1366 |
-
max_val = est_idx.loc[col, "Máximo"]
|
| 1367 |
-
placeholder = f"{min_val} — {max_val}"
|
| 1368 |
-
else:
|
| 1369 |
-
placeholder = ""
|
| 1370 |
-
inputs_updates.append(gr.update(visible=True, value=None, label=col, placeholder=placeholder, interactive=True))
|
| 1371 |
-
else:
|
| 1372 |
-
inputs_updates.append(gr.update(visible=False, value=None, label="", placeholder=""))
|
| 1373 |
-
|
| 1374 |
-
return (*rows_updates, *inputs_updates)
|
| 1375 |
-
|
| 1376 |
-
|
| 1377 |
-
def calcular_avaliacao_viz(pacote, estado_avaliacoes, indice_base_str, *aval_inputs):
|
| 1378 |
-
"""Calcula avaliação usando o modelo carregado na aba Visualização.
|
| 1379 |
-
|
| 1380 |
-
CONTRACT: Retorna 3 itens (resultado_html, estado_avaliacoes, dropdown_update).
|
| 1381 |
-
"""
|
| 1382 |
-
_err = lambda msg: (msg, estado_avaliacoes or [], gr.update())
|
| 1383 |
-
if pacote is None:
|
| 1384 |
-
return _err("Carregue e exiba um modelo primeiro.")
|
| 1385 |
-
|
| 1386 |
-
info_transf = pacote["transformacoes"]["info"]
|
| 1387 |
-
nome_y, transf_y = info_transf[0].split(": ", 1)
|
| 1388 |
-
transformacao_y = transf_y.strip().replace("(y)", "(x)")
|
| 1389 |
-
|
| 1390 |
-
colunas_x = []
|
| 1391 |
-
transformacoes_x = {}
|
| 1392 |
-
for item in info_transf[1:]:
|
| 1393 |
-
nome_x, transf_x = item.split(": ", 1)
|
| 1394 |
-
nome_x = nome_x.strip()
|
| 1395 |
-
colunas_x.append(nome_x)
|
| 1396 |
-
transformacoes_x[nome_x] = transf_x.strip()
|
| 1397 |
-
|
| 1398 |
-
valores_x = {}
|
| 1399 |
-
for i, col in enumerate(colunas_x):
|
| 1400 |
-
if i >= len(aval_inputs) or aval_inputs[i] is None:
|
| 1401 |
-
return _err("Preencha todos os campos.")
|
| 1402 |
-
valores_x[col] = float(aval_inputs[i])
|
| 1403 |
-
|
| 1404 |
-
# Validar variáveis dicotômicas, categóricas codificadas e percentuais ANTES de avaliar
|
| 1405 |
-
dicotomicas = pacote["transformacoes"].get("dicotomicas", [])
|
| 1406 |
-
codigo_alocado = pacote["transformacoes"].get("codigo_alocado", [])
|
| 1407 |
-
percentuais = pacote["transformacoes"].get("percentuais", [])
|
| 1408 |
-
estatisticas_df = pacote["dados"]["estatisticas"]
|
| 1409 |
-
import pandas as pd
|
| 1410 |
-
if isinstance(estatisticas_df, pd.DataFrame):
|
| 1411 |
-
if "Variável" in estatisticas_df.columns:
|
| 1412 |
-
est_idx = estatisticas_df.set_index("Variável")
|
| 1413 |
-
else:
|
| 1414 |
-
est_idx = estatisticas_df
|
| 1415 |
-
else:
|
| 1416 |
-
est_idx = pd.DataFrame()
|
| 1417 |
-
|
| 1418 |
-
for col in colunas_x:
|
| 1419 |
-
val = valores_x[col]
|
| 1420 |
-
if col in (dicotomicas or []):
|
| 1421 |
-
if val not in (0, 0.0, 1, 1.0):
|
| 1422 |
-
return _err(f"<p style='color:red;'><b>Erro:</b> A variável <b>{col}</b> é dicotômica e aceita apenas valores 0 ou 1. "
|
| 1423 |
-
f"Valor informado: {val}</p>")
|
| 1424 |
-
elif col in (codigo_alocado or []) and col in est_idx.index:
|
| 1425 |
-
min_val = float(est_idx.loc[col, "Mínimo"])
|
| 1426 |
-
max_val = float(est_idx.loc[col, "Máximo"])
|
| 1427 |
-
eh_inteiro = (float(val) == int(float(val)))
|
| 1428 |
-
if not eh_inteiro or val < min_val or val > max_val:
|
| 1429 |
-
return _err(f"<p style='color:red;'><b>Erro:</b> A variável <b>{col}</b> é categórica codificada e aceita apenas "
|
| 1430 |
-
f"valores inteiros de {int(min_val)} a {int(max_val)}. Valor informado: {val}</p>")
|
| 1431 |
-
elif col in (percentuais or []):
|
| 1432 |
-
if val < 0 or val > 1:
|
| 1433 |
-
return _err(f"<p style='color:red;'><b>Erro:</b> A variável <b>{col}</b> é percentual e aceita apenas "
|
| 1434 |
-
f"valores entre 0 e 1. Valor informado: {val}</p>")
|
| 1435 |
-
|
| 1436 |
-
resultado = avaliar_imovel(
|
| 1437 |
-
modelo_sm=pacote["modelo"]["sm"],
|
| 1438 |
-
valores_x=valores_x,
|
| 1439 |
-
colunas_x=colunas_x,
|
| 1440 |
-
transformacoes_x=transformacoes_x,
|
| 1441 |
-
transformacao_y=transformacao_y,
|
| 1442 |
-
estatisticas_df=estatisticas_df,
|
| 1443 |
-
dicotomicas=dicotomicas,
|
| 1444 |
-
codigo_alocado=codigo_alocado,
|
| 1445 |
-
percentuais=percentuais,
|
| 1446 |
-
)
|
| 1447 |
-
|
| 1448 |
-
if resultado is None:
|
| 1449 |
-
return _err("Erro ao calcular avaliação.")
|
| 1450 |
-
|
| 1451 |
-
nova_lista = list(estado_avaliacoes or []) + [resultado]
|
| 1452 |
-
indice_base = int(indice_base_str) - 1 if indice_base_str else 0
|
| 1453 |
-
html = formatar_avaliacao_html(nova_lista, indice_base=indice_base, elem_id_excluir="excluir-aval-viz")
|
| 1454 |
-
choices = [str(i + 1) for i in range(len(nova_lista))]
|
| 1455 |
-
base_val = indice_base_str if indice_base_str else "1"
|
| 1456 |
-
return html, nova_lista, gr.update(choices=choices, value=base_val)
|
| 1457 |
-
|
| 1458 |
-
|
| 1459 |
-
def excluir_avaliacao_viz(indice_str, estado_avaliacoes, indice_base_str):
|
| 1460 |
-
"""Exclui uma avaliação (Visualização).
|
| 1461 |
-
|
| 1462 |
-
CONTRACT: Retorna 4 itens (resultado_html, estado_avaliacoes, dropdown_update, trigger_reset).
|
| 1463 |
-
"""
|
| 1464 |
-
if not indice_str or not indice_str.strip() or not estado_avaliacoes:
|
| 1465 |
-
return gr.update(), estado_avaliacoes or [], gr.update(), ""
|
| 1466 |
-
try:
|
| 1467 |
-
idx = int(indice_str.strip()) - 1
|
| 1468 |
-
except ValueError:
|
| 1469 |
-
return gr.update(), estado_avaliacoes or [], gr.update(), ""
|
| 1470 |
-
if idx < 0 or idx >= len(estado_avaliacoes):
|
| 1471 |
-
return gr.update(), estado_avaliacoes or [], gr.update(), ""
|
| 1472 |
-
|
| 1473 |
-
nova_lista = [a for i, a in enumerate(estado_avaliacoes) if i != idx]
|
| 1474 |
-
if not nova_lista:
|
| 1475 |
-
return "", [], gr.update(choices=[], value=None), ""
|
| 1476 |
-
|
| 1477 |
-
base = int(indice_base_str) - 1 if indice_base_str else 0
|
| 1478 |
-
if base >= len(nova_lista):
|
| 1479 |
-
base = len(nova_lista) - 1
|
| 1480 |
-
if base < 0:
|
| 1481 |
-
base = 0
|
| 1482 |
-
|
| 1483 |
-
choices = [str(i + 1) for i in range(len(nova_lista))]
|
| 1484 |
-
html = formatar_avaliacao_html(nova_lista, indice_base=base, elem_id_excluir="excluir-aval-viz")
|
| 1485 |
-
return html, nova_lista, gr.update(choices=choices, value=str(base + 1)), ""
|
| 1486 |
-
|
| 1487 |
-
|
| 1488 |
-
def atualizar_base_avaliacao_viz(estado_avaliacoes, indice_base_str):
|
| 1489 |
-
"""Re-renderiza HTML quando o dropdown de base muda (Visualização)."""
|
| 1490 |
-
if not estado_avaliacoes:
|
| 1491 |
-
return ""
|
| 1492 |
-
indice = int(indice_base_str) - 1 if indice_base_str else 0
|
| 1493 |
-
return formatar_avaliacao_html(estado_avaliacoes, indice_base=indice, elem_id_excluir="excluir-aval-viz")
|
| 1494 |
-
|
| 1495 |
-
|
| 1496 |
-
def exportar_avaliacoes_excel_viz(estado_avaliacoes):
|
| 1497 |
-
"""Exporta avaliações para Excel (Visualização)."""
|
| 1498 |
-
if not estado_avaliacoes:
|
| 1499 |
-
return gr.update(value=None, visible=False)
|
| 1500 |
-
caminho = exportar_avaliacoes_excel(estado_avaliacoes)
|
| 1501 |
-
if caminho:
|
| 1502 |
-
return gr.update(value=caminho, visible=True)
|
| 1503 |
-
return gr.update(value=None, visible=False)
|
| 1504 |
|
| 1505 |
|
| 1506 |
def _formatar_badge_completo(pacote, nome_modelo=""):
|
| 1507 |
-
"""Retorna HTML do badge no mesmo padrão visual da aba Elaboração."""
|
| 1508 |
if not pacote:
|
| 1509 |
return ""
|
| 1510 |
|
| 1511 |
def _esc(value):
|
| 1512 |
-
return (
|
| 1513 |
-
str(value or "")
|
| 1514 |
-
.replace("&", "&")
|
| 1515 |
-
.replace("<", "<")
|
| 1516 |
-
.replace(">", ">")
|
| 1517 |
-
.replace('"', """)
|
| 1518 |
-
)
|
| 1519 |
|
| 1520 |
def _data_br(value):
|
| 1521 |
texto = str(value or "").strip()
|
|
@@ -1525,24 +1002,21 @@ def _formatar_badge_completo(pacote, nome_modelo=""):
|
|
| 1525 |
match_iso = re.match(r"^(\d{4})-(\d{2})-(\d{2})(?:[T\s].*)?$", texto)
|
| 1526 |
if match_iso:
|
| 1527 |
try:
|
| 1528 |
-
|
| 1529 |
-
return dt.strftime("%d/%m/%Y")
|
| 1530 |
except Exception:
|
| 1531 |
return texto
|
| 1532 |
|
| 1533 |
match_iso_slash = re.match(r"^(\d{4})/(\d{2})/(\d{2})(?:[T\s].*)?$", texto)
|
| 1534 |
if match_iso_slash:
|
| 1535 |
try:
|
| 1536 |
-
|
| 1537 |
-
return dt.strftime("%d/%m/%Y")
|
| 1538 |
except Exception:
|
| 1539 |
return texto
|
| 1540 |
|
| 1541 |
match_br = re.match(r"^(\d{2})/(\d{2})/(\d{4})(?:[T\s].*)?$", texto)
|
| 1542 |
if match_br:
|
| 1543 |
try:
|
| 1544 |
-
|
| 1545 |
-
return dt.strftime("%d/%m/%Y")
|
| 1546 |
except Exception:
|
| 1547 |
return texto
|
| 1548 |
|
|
@@ -1550,7 +1024,6 @@ def _formatar_badge_completo(pacote, nome_modelo=""):
|
|
| 1550 |
|
| 1551 |
model_name = str(nome_modelo or "").strip() or "-"
|
| 1552 |
observacao_modelo = str(pacote.get("observacao_modelo") or "").strip()
|
| 1553 |
-
|
| 1554 |
periodo = pacote.get("periodo_dados_mercado") or {}
|
| 1555 |
data_inicial = _data_br(periodo.get("data_inicial"))
|
| 1556 |
data_final = _data_br(periodo.get("data_final"))
|
|
@@ -1659,254 +1132,19 @@ def _formatar_badge_completo(pacote, nome_modelo=""):
|
|
| 1659 |
"<div class='modelo-info-card'>"
|
| 1660 |
"<div class='modelo-info-split'>"
|
| 1661 |
"<div class='modelo-info-col'>"
|
| 1662 |
-
+ nome_modelo_html
|
| 1663 |
-
"<div class='modelo-info-stack-block'>"
|
| 1664 |
"<div class='elaborador-badge-title'>ELABORADO POR:</div>"
|
| 1665 |
-
+ elaborador_html
|
| 1666 |
-
"</div>"
|
| 1667 |
"</div>"
|
| 1668 |
"<div class='modelo-info-col modelo-info-col-vars'>"
|
| 1669 |
"<div class='elaborador-badge-title'>Variáveis selecionadas:</div>"
|
| 1670 |
-
+ y_html
|
| 1671 |
-
|
| 1672 |
-
|
| 1673 |
-
"</div>"
|
| 1674 |
"</div>"
|
| 1675 |
"</div>"
|
| 1676 |
"</div>"
|
| 1677 |
)
|
| 1678 |
-
|
| 1679 |
-
|
| 1680 |
-
def limpar_tudo():
|
| 1681 |
-
return (
|
| 1682 |
-
None, "", None, None, None, "", None, "", None, None,
|
| 1683 |
-
None, None, None, None, None, # 5 gráficos nulos
|
| 1684 |
-
"", None,
|
| 1685 |
-
gr.update(choices=["Visualização Padrão"], value="Visualização Padrão"), # Dropdown reset
|
| 1686 |
-
# Reset avaliação
|
| 1687 |
-
*[gr.update(visible=False) for _ in range(N_ROWS_AVAL)],
|
| 1688 |
-
*[gr.update(visible=False, value=None, label="") for _ in range(MAX_VARS_AVAL)],
|
| 1689 |
-
"", # resultado_aval_html
|
| 1690 |
-
[], # estado_avaliacoes
|
| 1691 |
-
gr.update(choices=[], value=None), # dropdown_base_aval
|
| 1692 |
-
"", # excluir_aval_trigger_viz
|
| 1693 |
-
gr.update(value=None, visible=False), # download_aval_file
|
| 1694 |
-
"", # elaborador_badge
|
| 1695 |
-
)
|
| 1696 |
-
|
| 1697 |
-
# ============================================================
|
| 1698 |
-
# INTERFACE GRADIO
|
| 1699 |
-
# ============================================================
|
| 1700 |
-
|
| 1701 |
-
description = f"""
|
| 1702 |
-
# <p style="text-align: center;">MODELOS ESTATÍSTICOS</p>
|
| 1703 |
-
<p style="text-align: center;">Divisão de Avaliação de Imóveis</p>
|
| 1704 |
-
<hr style="color: #333; background-color: #333; height: 1px; border: none;">
|
| 1705 |
-
"""
|
| 1706 |
-
|
| 1707 |
-
def criar_aba():
|
| 1708 |
-
"""Cria conteúdo da aba de visualização (sem wrapper gr.Blocks)."""
|
| 1709 |
-
# --------------------------------------------------------
|
| 1710 |
-
# ESTADO
|
| 1711 |
-
# --------------------------------------------------------
|
| 1712 |
-
estado_pacote = gr.State(None)
|
| 1713 |
-
estado_dados = gr.State(None) # Armazena os dados para atualizar o mapa
|
| 1714 |
-
|
| 1715 |
-
# --------------------------------------------------------
|
| 1716 |
-
# CONTROLES (TOPO)
|
| 1717 |
-
# --------------------------------------------------------
|
| 1718 |
-
with gr.Group(elem_classes="upload-area"):
|
| 1719 |
-
upload = gr.File(label="Enviar modelo salvo (.dai)", file_types=[".dai"], scale=1)
|
| 1720 |
-
with gr.Row(equal_height=True):
|
| 1721 |
-
status = gr.Textbox(show_label=False, interactive=False, scale=4, lines=1)
|
| 1722 |
-
elaborador_badge = gr.HTML("")
|
| 1723 |
-
with gr.Row(equal_height=True):
|
| 1724 |
-
btn_exibir = gr.Button("Exibir modelo", scale=2, variant="primary")
|
| 1725 |
-
btn_limpar = gr.Button("Limpar tudo", scale=1, variant="secondary")
|
| 1726 |
-
|
| 1727 |
-
# --------------------------------------------------------
|
| 1728 |
-
# MAPA (RETRÁTIL VIA ACCORDION)
|
| 1729 |
-
# --------------------------------------------------------
|
| 1730 |
-
with gr.Accordion("Mapa de Distribuição dos Dados", open=True, elem_classes="map-accordion"):
|
| 1731 |
-
dropdown_mapa_var = gr.Dropdown(
|
| 1732 |
-
label="Variável para dimensionar pontos no mapa",
|
| 1733 |
-
choices=["Visualização Padrão"],
|
| 1734 |
-
value="Visualização Padrão",
|
| 1735 |
-
interactive=True,
|
| 1736 |
-
allow_custom_value=False
|
| 1737 |
-
)
|
| 1738 |
-
out_mapa = gr.HTML(label="", elem_id="map-frame")
|
| 1739 |
-
|
| 1740 |
-
# --------------------------------------------------------
|
| 1741 |
-
# CONTEÚDO (LARGURA TOTAL)
|
| 1742 |
-
# --------------------------------------------------------
|
| 1743 |
-
with gr.Column(elem_classes="content-panel"):
|
| 1744 |
-
with gr.Tabs(elem_classes="tabs-container"):
|
| 1745 |
-
with gr.Tab("Dados"):
|
| 1746 |
-
gr.HTML(criar_titulo_secao_html("Dados Utilizados"))
|
| 1747 |
-
out_dados = gr.Dataframe(show_label=False, max_height=300)
|
| 1748 |
-
gr.HTML(criar_titulo_secao_html("Estatísticas"))
|
| 1749 |
-
out_estat = gr.Dataframe(show_label=False, max_height=300)
|
| 1750 |
-
|
| 1751 |
-
with gr.Tab("Transformações"):
|
| 1752 |
-
out_escalas = gr.HTML()
|
| 1753 |
-
gr.HTML(criar_titulo_secao_html("X e y Transformados"))
|
| 1754 |
-
out_df_xy = gr.Dataframe(show_label=False, max_height=400)
|
| 1755 |
-
|
| 1756 |
-
with gr.Tab("Resumo"):
|
| 1757 |
-
out_resumo = gr.HTML()
|
| 1758 |
-
|
| 1759 |
-
with gr.Tab("Coeficientes"):
|
| 1760 |
-
gr.HTML(criar_titulo_secao_html("Tabela de Coeficientes"))
|
| 1761 |
-
out_coef = gr.Dataframe(show_label=False, max_height=700)
|
| 1762 |
-
|
| 1763 |
-
with gr.Tab("Obs x Calc"):
|
| 1764 |
-
gr.HTML(criar_titulo_secao_html("Tabela Obs x Calc"))
|
| 1765 |
-
out_obs = gr.Dataframe(show_label=False, max_height=600)
|
| 1766 |
-
|
| 1767 |
-
with gr.Tab("Gráficos") as tab_graficos:
|
| 1768 |
-
with gr.Group():
|
| 1769 |
-
# Linha 1 — dois gráficos, 50% / 50%
|
| 1770 |
-
with gr.Row():
|
| 1771 |
-
out_plot_obs = gr.Plot(label="Obs vs Calc")
|
| 1772 |
-
out_plot_res = gr.Plot(label="Resíduos vs Ajustados")
|
| 1773 |
-
|
| 1774 |
-
# Linha 2 — dois gráficos, 50% / 50%
|
| 1775 |
-
with gr.Row():
|
| 1776 |
-
out_plot_hist = gr.Plot(label="Histograma Resíduos")
|
| 1777 |
-
out_plot_cook = gr.Plot(label="Distância de Cook")
|
| 1778 |
-
|
| 1779 |
-
# Linha 3 — gráfico sozinho, largura máxima
|
| 1780 |
-
with gr.Row():
|
| 1781 |
-
out_plot_corr = gr.Plot(label="Correlação")
|
| 1782 |
-
|
| 1783 |
-
with gr.Tab("Avaliação"):
|
| 1784 |
-
gr.HTML(criar_titulo_secao_html("Avaliação Individual"))
|
| 1785 |
-
aval_rows = []
|
| 1786 |
-
aval_inputs = []
|
| 1787 |
-
with gr.Column(elem_classes="aval-all-cards"):
|
| 1788 |
-
for _i_row in range(N_ROWS_AVAL):
|
| 1789 |
-
with gr.Row(visible=False, elem_classes="aval-cards-row") as _aval_row:
|
| 1790 |
-
for _j_col in range(N_COLS_AVAL):
|
| 1791 |
-
_inp = gr.Number(label="", visible=False, interactive=True, elem_classes="aval-card")
|
| 1792 |
-
aval_inputs.append(_inp)
|
| 1793 |
-
aval_rows.append(_aval_row)
|
| 1794 |
-
with gr.Row():
|
| 1795 |
-
btn_calcular_aval = gr.Button("Calcular", variant="primary", scale=2)
|
| 1796 |
-
btn_limpar_aval = gr.Button("Limpar", variant="secondary", scale=1)
|
| 1797 |
-
dropdown_base_aval = gr.Dropdown(
|
| 1798 |
-
label="Base p/ comparação",
|
| 1799 |
-
choices=[],
|
| 1800 |
-
value=None,
|
| 1801 |
-
interactive=True,
|
| 1802 |
-
scale=1,
|
| 1803 |
-
)
|
| 1804 |
-
resultado_aval_html = gr.HTML("")
|
| 1805 |
-
excluir_aval_trigger_viz = gr.Textbox(
|
| 1806 |
-
label="", elem_id="excluir-aval-viz", container=False,
|
| 1807 |
-
elem_classes="trigger-hidden"
|
| 1808 |
-
)
|
| 1809 |
-
estado_avaliacoes = gr.State([])
|
| 1810 |
-
with gr.Row():
|
| 1811 |
-
btn_exportar_aval = gr.Button("Salvar Avaliações em Excel", variant="secondary")
|
| 1812 |
-
download_aval_file = gr.File(label="", visible=False)
|
| 1813 |
-
|
| 1814 |
-
with gr.Tab("Avaliação em Massa"):
|
| 1815 |
-
gr.HTML(criar_titulo_secao_html("Avaliação em Lote"))
|
| 1816 |
-
gr.HTML("""<div class="placeholder-alert"><p>Módulo em desenvolvimento</p></div>""")
|
| 1817 |
-
|
| 1818 |
-
# --------------------------------------------------------
|
| 1819 |
-
# EVENTOS
|
| 1820 |
-
# --------------------------------------------------------
|
| 1821 |
-
upload.upload(
|
| 1822 |
-
carregar_modelo_gradio, inputs=upload, outputs=[estado_pacote, status]
|
| 1823 |
-
).then(
|
| 1824 |
-
_formatar_badge_completo, inputs=estado_pacote, outputs=elaborador_badge
|
| 1825 |
-
)
|
| 1826 |
-
|
| 1827 |
-
btn_exibir.click(
|
| 1828 |
-
exibir_modelo,
|
| 1829 |
-
inputs=estado_pacote,
|
| 1830 |
-
outputs=[
|
| 1831 |
-
out_dados, out_estat, out_escalas, out_df_xy, out_resumo, out_coef, out_obs,
|
| 1832 |
-
out_plot_obs, out_plot_res, out_plot_hist, out_plot_cook, out_plot_corr,
|
| 1833 |
-
out_mapa, dropdown_mapa_var
|
| 1834 |
-
],
|
| 1835 |
-
).then(
|
| 1836 |
-
popular_inputs_avaliacao,
|
| 1837 |
-
inputs=estado_pacote,
|
| 1838 |
-
outputs=aval_rows + aval_inputs
|
| 1839 |
-
).then(
|
| 1840 |
-
lambda x: x, # Copia out_dados para estado_dados
|
| 1841 |
-
inputs=out_dados,
|
| 1842 |
-
outputs=estado_dados
|
| 1843 |
-
)
|
| 1844 |
-
|
| 1845 |
-
# Atualiza mapa quando dropdown muda
|
| 1846 |
-
dropdown_mapa_var.change(
|
| 1847 |
-
atualizar_mapa_callback,
|
| 1848 |
-
inputs=[estado_dados, dropdown_mapa_var, estado_pacote],
|
| 1849 |
-
outputs=out_mapa
|
| 1850 |
-
)
|
| 1851 |
-
|
| 1852 |
-
btn_limpar.click(
|
| 1853 |
-
limpar_tudo,
|
| 1854 |
-
outputs=[
|
| 1855 |
-
estado_pacote, status, upload,
|
| 1856 |
-
out_dados, out_estat, out_escalas, out_df_xy, out_resumo, out_coef, out_obs,
|
| 1857 |
-
out_plot_obs, out_plot_res, out_plot_hist, out_plot_cook, out_plot_corr,
|
| 1858 |
-
out_mapa, estado_dados,
|
| 1859 |
-
dropdown_mapa_var,
|
| 1860 |
-
] + aval_rows + aval_inputs + [resultado_aval_html, estado_avaliacoes,
|
| 1861 |
-
dropdown_base_aval, excluir_aval_trigger_viz, download_aval_file,
|
| 1862 |
-
elaborador_badge]
|
| 1863 |
-
)
|
| 1864 |
-
|
| 1865 |
-
# Avaliação individual
|
| 1866 |
-
btn_calcular_aval.click(
|
| 1867 |
-
calcular_avaliacao_viz,
|
| 1868 |
-
inputs=[estado_pacote, estado_avaliacoes, dropdown_base_aval] + aval_inputs,
|
| 1869 |
-
outputs=[resultado_aval_html, estado_avaliacoes, dropdown_base_aval]
|
| 1870 |
-
)
|
| 1871 |
-
|
| 1872 |
-
btn_limpar_aval.click(
|
| 1873 |
-
lambda: ("", [], gr.update(choices=[], value=None)),
|
| 1874 |
-
outputs=[resultado_aval_html, estado_avaliacoes, dropdown_base_aval]
|
| 1875 |
-
)
|
| 1876 |
-
|
| 1877 |
-
excluir_aval_trigger_viz.change(
|
| 1878 |
-
excluir_avaliacao_viz,
|
| 1879 |
-
inputs=[excluir_aval_trigger_viz, estado_avaliacoes, dropdown_base_aval],
|
| 1880 |
-
outputs=[resultado_aval_html, estado_avaliacoes, dropdown_base_aval, excluir_aval_trigger_viz]
|
| 1881 |
-
)
|
| 1882 |
-
|
| 1883 |
-
dropdown_base_aval.change(
|
| 1884 |
-
atualizar_base_avaliacao_viz,
|
| 1885 |
-
inputs=[estado_avaliacoes, dropdown_base_aval],
|
| 1886 |
-
outputs=[resultado_aval_html]
|
| 1887 |
-
)
|
| 1888 |
-
|
| 1889 |
-
btn_exportar_aval.click(
|
| 1890 |
-
exportar_avaliacoes_excel_viz,
|
| 1891 |
-
inputs=[estado_avaliacoes],
|
| 1892 |
-
outputs=[download_aval_file]
|
| 1893 |
-
)
|
| 1894 |
-
|
| 1895 |
-
# Força Plotly a recalcular tamanho quando a tab Gráficos é selecionada
|
| 1896 |
-
tab_graficos.select(
|
| 1897 |
-
fn=None,
|
| 1898 |
-
inputs=None,
|
| 1899 |
-
outputs=None,
|
| 1900 |
-
js="() => { setTimeout(() => window.dispatchEvent(new Event('resize')), 100) }"
|
| 1901 |
-
)
|
| 1902 |
-
|
| 1903 |
-
|
| 1904 |
-
# ============================================================
|
| 1905 |
-
# EXECUÇÃO
|
| 1906 |
-
# ============================================================
|
| 1907 |
-
if __name__ == "__main__":
|
| 1908 |
-
custom_css = carregar_css()
|
| 1909 |
-
with gr.Blocks(css=custom_css) as app:
|
| 1910 |
-
gr.Markdown(description)
|
| 1911 |
-
criar_aba()
|
| 1912 |
-
app.launch()
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
+
import math
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import re
|
| 5 |
import traceback
|
| 6 |
from datetime import datetime
|
| 7 |
from html import escape
|
| 8 |
|
| 9 |
+
import branca.colormap as cm
|
| 10 |
+
import folium
|
| 11 |
+
import numpy as np
|
| 12 |
+
import pandas as pd
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
import plotly.graph_objects as go
|
| 14 |
+
from folium import plugins
|
| 15 |
+
from scipy import stats
|
| 16 |
from statsmodels.stats.outliers_influence import OLSInfluence
|
|
|
|
|
|
|
| 17 |
|
|
|
|
|
|
|
| 18 |
from app.core.map_layers import (
|
| 19 |
add_bairros_layer,
|
| 20 |
add_indice_marker,
|
|
|
|
| 23 |
add_zoom_responsive_circle_markers,
|
| 24 |
)
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
+
COR_PRINCIPAL = "#FF8C00"
|
| 28 |
+
COR_LINHA = "#dc3545"
|
| 29 |
+
|
| 30 |
|
| 31 |
def _criar_grafico_obs_calc(y_obs, y_calc, indices=None):
|
|
|
|
| 32 |
try:
|
| 33 |
fig = go.Figure()
|
|
|
|
| 34 |
scatter_args = dict(
|
| 35 |
x=y_calc,
|
| 36 |
y=y_obs,
|
| 37 |
+
mode="markers",
|
| 38 |
+
marker=dict(color=COR_PRINCIPAL, size=10, line=dict(color="black", width=1)),
|
| 39 |
+
name="Dados",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
)
|
| 41 |
if indices is not None:
|
| 42 |
+
scatter_args["customdata"] = indices
|
| 43 |
+
scatter_args["hovertemplate"] = (
|
| 44 |
+
"<b>Índice:</b> %{customdata}<br><b>Calculado:</b> %{x:.2f}<br>"
|
| 45 |
+
"<b>Observado:</b> %{y:.2f}<extra></extra>"
|
| 46 |
+
)
|
| 47 |
else:
|
| 48 |
+
scatter_args["hovertemplate"] = (
|
| 49 |
+
"<b>Calculado:</b> %{x:.2f}<br><b>Observado:</b> %{y:.2f}<extra></extra>"
|
| 50 |
+
)
|
| 51 |
|
|
|
|
| 52 |
fig.add_trace(go.Scatter(**scatter_args))
|
| 53 |
|
|
|
|
| 54 |
min_val = min(min(y_obs), min(y_calc))
|
| 55 |
max_val = max(max(y_obs), max(y_calc))
|
| 56 |
margin = (max_val - min_val) * 0.05
|
| 57 |
|
| 58 |
+
fig.add_trace(
|
| 59 |
+
go.Scatter(
|
| 60 |
+
x=[min_val - margin, max_val + margin],
|
| 61 |
+
y=[min_val - margin, max_val + margin],
|
| 62 |
+
mode="lines",
|
| 63 |
+
line=dict(color=COR_LINHA, dash="dash", width=2),
|
| 64 |
+
name="Linha de identidade",
|
| 65 |
+
)
|
| 66 |
+
)
|
| 67 |
|
| 68 |
fig.update_layout(
|
| 69 |
+
title=dict(text="Valores Observados vs Calculados", x=0.5),
|
| 70 |
+
xaxis_title="Valores Calculados",
|
| 71 |
+
yaxis_title="Valores Observados",
|
| 72 |
showlegend=True,
|
| 73 |
+
plot_bgcolor="white",
|
| 74 |
+
margin=dict(l=60, r=40, t=60, b=60),
|
| 75 |
)
|
| 76 |
+
fig.update_xaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black")
|
| 77 |
+
fig.update_yaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black")
|
|
|
|
|
|
|
| 78 |
return fig
|
| 79 |
+
except Exception as exc:
|
| 80 |
+
print(f"Erro ao criar gráfico obs vs calc: {exc}")
|
| 81 |
return None
|
| 82 |
|
| 83 |
+
|
| 84 |
def _criar_grafico_residuos(y_calc, residuos, indices=None):
|
|
|
|
| 85 |
try:
|
| 86 |
fig = go.Figure()
|
|
|
|
| 87 |
scatter_args = dict(
|
| 88 |
x=y_calc,
|
| 89 |
y=residuos,
|
| 90 |
+
mode="markers",
|
| 91 |
+
marker=dict(color=COR_PRINCIPAL, size=10, line=dict(color="black", width=1)),
|
| 92 |
+
name="Resíduos",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
)
|
| 94 |
if indices is not None:
|
| 95 |
+
scatter_args["customdata"] = indices
|
| 96 |
+
scatter_args["hovertemplate"] = (
|
| 97 |
+
"<b>Índice:</b> %{customdata}<br><b>Ajustado:</b> %{x:.2f}<br>"
|
| 98 |
+
"<b>Resíduo:</b> %{y:.4f}<extra></extra>"
|
| 99 |
+
)
|
| 100 |
else:
|
| 101 |
+
scatter_args["hovertemplate"] = (
|
| 102 |
+
"<b>Ajustado:</b> %{x:.2f}<br><b>Resíduo:</b> %{y:.4f}<extra></extra>"
|
| 103 |
+
)
|
| 104 |
|
| 105 |
fig.add_trace(go.Scatter(**scatter_args))
|
|
|
|
| 106 |
fig.add_hline(y=0, line_dash="dash", line_color=COR_LINHA, line_width=2)
|
|
|
|
| 107 |
fig.update_layout(
|
| 108 |
+
title=dict(text="Resíduos vs Valores Ajustados", x=0.5),
|
| 109 |
+
xaxis_title="Valores Ajustados",
|
| 110 |
+
yaxis_title="Resíduos",
|
| 111 |
showlegend=False,
|
| 112 |
+
plot_bgcolor="white",
|
| 113 |
+
margin=dict(l=60, r=40, t=60, b=60),
|
| 114 |
)
|
| 115 |
+
fig.update_xaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black")
|
| 116 |
+
fig.update_yaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black")
|
|
|
|
|
|
|
| 117 |
return fig
|
| 118 |
+
except Exception as exc:
|
| 119 |
+
print(f"Erro ao criar gráfico de resíduos: {exc}")
|
| 120 |
return None
|
| 121 |
|
| 122 |
+
|
| 123 |
def _criar_histograma_residuos(residuos):
|
|
|
|
| 124 |
try:
|
| 125 |
fig = go.Figure()
|
| 126 |
+
fig.add_trace(
|
| 127 |
+
go.Histogram(
|
| 128 |
+
x=residuos,
|
| 129 |
+
histnorm="probability density",
|
| 130 |
+
marker=dict(color=COR_PRINCIPAL, line=dict(color="black", width=1)),
|
| 131 |
+
opacity=0.7,
|
| 132 |
+
name="Resíduos",
|
| 133 |
+
)
|
| 134 |
+
)
|
| 135 |
|
| 136 |
mu, sigma = np.mean(residuos), np.std(residuos)
|
| 137 |
x_norm = np.linspace(min(residuos) - sigma, max(residuos) + sigma, 100)
|
| 138 |
y_norm = stats.norm.pdf(x_norm, mu, sigma)
|
| 139 |
+
fig.add_trace(
|
| 140 |
+
go.Scatter(
|
| 141 |
+
x=x_norm,
|
| 142 |
+
y=y_norm,
|
| 143 |
+
mode="lines",
|
| 144 |
+
line=dict(color=COR_LINHA, width=3),
|
| 145 |
+
name="Curva Normal",
|
| 146 |
+
)
|
| 147 |
+
)
|
| 148 |
|
| 149 |
fig.update_layout(
|
| 150 |
+
title=dict(text="Distribuição dos Resíduos", x=0.5),
|
| 151 |
+
xaxis_title="Resíduos",
|
| 152 |
+
yaxis_title="Densidade",
|
| 153 |
showlegend=True,
|
| 154 |
+
plot_bgcolor="white",
|
| 155 |
+
barmode="overlay",
|
| 156 |
+
margin=dict(l=60, r=40, t=60, b=60),
|
| 157 |
)
|
| 158 |
+
fig.update_xaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black")
|
| 159 |
+
fig.update_yaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black")
|
|
|
|
|
|
|
| 160 |
return fig
|
| 161 |
+
except Exception as exc:
|
| 162 |
+
print(f"Erro ao criar histograma: {exc}")
|
| 163 |
return None
|
| 164 |
|
| 165 |
|
| 166 |
def _criar_grafico_cook(modelos_sm):
|
|
|
|
| 167 |
try:
|
| 168 |
+
if modelos_sm is None:
|
| 169 |
+
return None
|
| 170 |
|
| 171 |
influence = OLSInfluence(modelos_sm)
|
| 172 |
cooks_d = influence.cooks_distance[0]
|
|
|
|
| 173 |
n = len(cooks_d)
|
| 174 |
indices = np.arange(1, n + 1)
|
| 175 |
limite = 4 / n
|
| 176 |
|
| 177 |
fig = go.Figure()
|
|
|
|
|
|
|
| 178 |
for idx, valor in zip(indices, cooks_d):
|
| 179 |
cor = COR_LINHA if valor > limite else COR_PRINCIPAL
|
| 180 |
fig.add_trace(
|
| 181 |
+
go.Scatter(
|
| 182 |
+
x=[idx, idx],
|
| 183 |
+
y=[0, valor],
|
| 184 |
+
mode="lines",
|
| 185 |
+
line=dict(color=cor, width=1.5),
|
| 186 |
+
showlegend=False,
|
| 187 |
+
hoverinfo="skip",
|
| 188 |
+
)
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
cores_pontos = [COR_LINHA if v > limite else COR_PRINCIPAL for v in cooks_d]
|
| 192 |
fig.add_trace(
|
| 193 |
go.Scatter(
|
| 194 |
x=indices,
|
| 195 |
y=cooks_d,
|
| 196 |
+
mode="markers",
|
| 197 |
+
marker=dict(color=cores_pontos, size=8, line=dict(color="black", width=1)),
|
| 198 |
+
name="Distância de Cook",
|
| 199 |
+
hovertemplate="Obs: %{x}<br>Cook: %{y:.4f}<extra></extra>",
|
| 200 |
+
)
|
| 201 |
+
)
|
| 202 |
+
fig.add_hline(
|
| 203 |
+
y=limite,
|
| 204 |
+
line_dash="dash",
|
| 205 |
+
line_color="gray",
|
| 206 |
+
annotation_text=f"4/n = {limite:.4f}",
|
| 207 |
+
annotation_position="top right",
|
| 208 |
+
)
|
| 209 |
+
fig.update_layout(
|
| 210 |
+
title=dict(text="Distância de Cook", x=0.5),
|
| 211 |
+
xaxis_title="Observação",
|
| 212 |
+
yaxis_title="Distância de Cook",
|
| 213 |
+
plot_bgcolor="white",
|
| 214 |
+
margin=dict(l=60, r=40, t=60, b=60),
|
| 215 |
+
)
|
| 216 |
+
fig.update_xaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black")
|
| 217 |
+
fig.update_yaxes(showgrid=True, gridcolor="lightgray", showline=True, linecolor="black")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
return fig
|
| 219 |
+
except Exception as exc:
|
| 220 |
+
print(f"Erro ao criar gráfico de Cook: {exc}")
|
| 221 |
return None
|
| 222 |
|
| 223 |
+
|
| 224 |
def _criar_grafico_correlacao(modelos_sm, nome_y: str | None = None):
|
|
|
|
| 225 |
try:
|
| 226 |
+
if modelos_sm is None or not hasattr(modelos_sm, "model"):
|
| 227 |
return None
|
| 228 |
|
| 229 |
model = modelos_sm.model
|
| 230 |
X = model.exog
|
| 231 |
X_names = model.exog_names
|
| 232 |
y = model.endog
|
| 233 |
+
y_name_base = (
|
| 234 |
+
str(nome_y or "").strip()
|
| 235 |
+
or str(getattr(model, "endog_names", "Variável Dependente") or "").strip()
|
| 236 |
+
or "Variável Dependente"
|
| 237 |
+
)
|
| 238 |
y_name = y_name_base if re.search(r"\(Y\)$", y_name_base, flags=re.IGNORECASE) else f"{y_name_base} (Y)"
|
| 239 |
|
| 240 |
df_X = pd.DataFrame(X, columns=X_names)
|
| 241 |
df_X = df_X.drop(
|
| 242 |
+
columns=[c for c in df_X.columns if str(c).lower() in ("const", "intercept")],
|
| 243 |
+
errors="ignore",
|
| 244 |
)
|
| 245 |
|
| 246 |
df_y = pd.DataFrame({y_name: y})
|
| 247 |
+
df = pd.concat([df_y, df_X], axis=1).apply(pd.to_numeric, errors="coerce")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
variancias = df.var(ddof=0)
|
| 249 |
df = df.loc[:, variancias.fillna(0) > 0]
|
|
|
|
| 250 |
if df.shape[1] < 2:
|
| 251 |
return None
|
| 252 |
|
|
|
|
| 253 |
corr = df.corr()
|
|
|
|
| 254 |
if corr.isnull().values.all():
|
| 255 |
return None
|
| 256 |
|
|
|
|
| 257 |
mask = np.eye(len(corr), dtype=bool)
|
| 258 |
corr = corr.where(~mask)
|
| 259 |
+
text = np.where(np.isnan(corr.values), "", np.round(corr.values, 2).astype(str))
|
| 260 |
+
|
| 261 |
+
fig = go.Figure(
|
| 262 |
+
go.Heatmap(
|
| 263 |
+
z=corr.values,
|
| 264 |
+
x=corr.columns,
|
| 265 |
+
y=corr.index,
|
| 266 |
+
text=text,
|
| 267 |
+
texttemplate="%{text}",
|
| 268 |
+
textfont=dict(size=10),
|
| 269 |
+
zmin=-1,
|
| 270 |
+
zmax=1,
|
| 271 |
+
zmid=0,
|
| 272 |
+
colorscale=[
|
| 273 |
+
[0.00, "rgb(103,0,31)"],
|
| 274 |
+
[0.08, "rgb(178,24,43)"],
|
| 275 |
+
[0.16, "rgb(214,96,77)"],
|
| 276 |
+
[0.24, "rgb(244,165,130)"],
|
| 277 |
+
[0.32, "rgb(253,219,199)"],
|
| 278 |
+
[0.45, "rgb(255,255,255)"],
|
| 279 |
+
[0.55, "rgb(255,255,255)"],
|
| 280 |
+
[0.68, "rgb(209,229,240)"],
|
| 281 |
+
[0.76, "rgb(146,197,222)"],
|
| 282 |
+
[0.84, "rgb(67,147,195)"],
|
| 283 |
+
[0.92, "rgb(33,102,172)"],
|
| 284 |
+
[1.00, "rgb(5,48,97)"],
|
| 285 |
+
],
|
| 286 |
+
colorbar=dict(title="Correlação"),
|
| 287 |
+
hovertemplate="%{x} × %{y}<br>ρ = %{z:.3f}<extra></extra>",
|
| 288 |
+
)
|
| 289 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
fig.add_shape(
|
| 291 |
type="line",
|
| 292 |
xref="paper",
|
| 293 |
yref="paper",
|
| 294 |
+
x0=0,
|
| 295 |
+
y0=1,
|
| 296 |
+
x1=1,
|
| 297 |
+
y1=0,
|
| 298 |
line=dict(color="rgba(0,0,0,0.35)", width=1),
|
| 299 |
+
layer="above",
|
| 300 |
)
|
|
|
|
| 301 |
fig.update_layout(
|
| 302 |
title=dict(text="Matriz de Correlação", x=0.5),
|
| 303 |
height=600,
|
| 304 |
+
template="plotly_white",
|
| 305 |
xaxis=dict(tickangle=45, showgrid=False),
|
| 306 |
+
yaxis=dict(autorange="reversed", showgrid=False),
|
| 307 |
)
|
|
|
|
| 308 |
return fig
|
| 309 |
+
except Exception as exc:
|
| 310 |
+
print(f"Erro na geração do gráfico: {exc}")
|
|
|
|
| 311 |
traceback.print_exc()
|
| 312 |
return None
|
| 313 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
|
| 315 |
def gerar_todos_graficos(pacote):
|
| 316 |
+
graficos = {"obs_calc": None, "residuos": None, "hist": None, "cook": None, "corr": None}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
|
| 318 |
obs_calc = pacote["modelo"]["obs_calc"]
|
| 319 |
modelos_sm = pacote["modelo"]["sm"]
|
|
|
|
| 324 |
if primeira_linha:
|
| 325 |
nome_y = primeira_linha.split(": ", 1)[0].strip() or None
|
| 326 |
|
|
|
|
| 327 |
y_obs = None
|
| 328 |
y_calc = None
|
| 329 |
residuos = None
|
| 330 |
indices = None
|
| 331 |
|
|
|
|
| 332 |
if obs_calc is not None and not obs_calc.empty:
|
| 333 |
cols_lower = {str(c).lower(): c for c in obs_calc.columns}
|
|
|
|
|
|
|
| 334 |
indices = obs_calc.index.values if obs_calc.index is not None else None
|
| 335 |
|
| 336 |
+
for nome in ["observado", "obs", "y_obs", "y", "valor_observado"]:
|
|
|
|
| 337 |
if nome in cols_lower:
|
| 338 |
y_obs = obs_calc[cols_lower[nome]].values
|
| 339 |
break
|
| 340 |
+
for nome in ["calculado", "calc", "y_calc", "y_hat", "previsto"]:
|
|
|
|
|
|
|
| 341 |
if nome in cols_lower:
|
| 342 |
y_calc = obs_calc[cols_lower[nome]].values
|
| 343 |
break
|
| 344 |
+
for nome in ["residuo", "residuos", "resid"]:
|
|
|
|
|
|
|
| 345 |
if nome in cols_lower:
|
| 346 |
residuos = obs_calc[cols_lower[nome]].values
|
| 347 |
break
|
| 348 |
|
|
|
|
| 349 |
if modelos_sm is not None:
|
| 350 |
try:
|
| 351 |
+
if y_obs is None and hasattr(modelos_sm.model, "endog"):
|
| 352 |
y_obs = modelos_sm.model.endog
|
| 353 |
+
if y_calc is None and hasattr(modelos_sm, "fittedvalues"):
|
| 354 |
y_calc = modelos_sm.fittedvalues
|
| 355 |
+
if residuos is None and hasattr(modelos_sm, "resid"):
|
| 356 |
residuos = modelos_sm.resid
|
| 357 |
except Exception:
|
| 358 |
pass
|
| 359 |
|
|
|
|
| 360 |
if residuos is None and y_obs is not None and y_calc is not None:
|
| 361 |
residuos = np.array(y_obs) - np.array(y_calc)
|
| 362 |
|
|
|
|
| 363 |
y_obs = np.array(y_obs) if y_obs is not None else None
|
| 364 |
y_calc = np.array(y_calc) if y_calc is not None else None
|
| 365 |
residuos = np.array(residuos) if residuos is not None else None
|
| 366 |
|
|
|
|
| 367 |
if y_obs is not None and y_calc is not None:
|
| 368 |
graficos["obs_calc"] = _criar_grafico_obs_calc(y_obs, y_calc, indices)
|
|
|
|
| 369 |
if residuos is not None and y_calc is not None:
|
| 370 |
graficos["residuos"] = _criar_grafico_residuos(y_calc, residuos, indices)
|
|
|
|
| 371 |
if residuos is not None:
|
| 372 |
graficos["hist"] = _criar_histograma_residuos(residuos)
|
|
|
|
| 373 |
if modelos_sm is not None:
|
| 374 |
graficos["cook"] = _criar_grafico_cook(modelos_sm)
|
| 375 |
graficos["corr"] = _criar_grafico_correlacao(modelos_sm, nome_y=nome_y)
|
| 376 |
|
| 377 |
return graficos
|
| 378 |
|
| 379 |
+
|
|
|
|
|
|
|
| 380 |
def reorganizar_modelos_resumos(diagnosticos):
|
|
|
|
|
|
|
|
|
|
| 381 |
gerais = diagnosticos.get("gerais", {})
|
| 382 |
return {
|
| 383 |
"estatisticas_gerais": {
|
|
|
|
| 387 |
"mse": {"nome": "MSE", "valor": gerais.get("mse")},
|
| 388 |
"r2": {"nome": "R²", "valor": gerais.get("r2")},
|
| 389 |
"r2_ajustado": {"nome": "R² ajustado", "valor": gerais.get("r2_ajustado")},
|
| 390 |
+
"r_pearson": {"nome": "Correlação Pearson", "valor": gerais.get("r_pearson")},
|
| 391 |
},
|
| 392 |
"teste_f": {
|
| 393 |
"nome": "Teste F",
|
| 394 |
"estatistica": diagnosticos.get("teste_f", {}).get("estatistica"),
|
| 395 |
"pvalor": diagnosticos.get("teste_f", {}).get("p_valor"),
|
| 396 |
+
"interpretacao": diagnosticos.get("teste_f", {}).get("interpretacao"),
|
| 397 |
},
|
| 398 |
"teste_ks": {
|
| 399 |
"nome": "Teste de Normalidade (Kolmogorov-Smirnov)",
|
| 400 |
"estatistica": diagnosticos.get("teste_ks", {}).get("estatistica"),
|
| 401 |
"pvalor": diagnosticos.get("teste_ks", {}).get("p_valor"),
|
| 402 |
+
"interpretacao": diagnosticos.get("teste_ks", {}).get("interpretacao"),
|
| 403 |
},
|
| 404 |
"perc_resid": {
|
| 405 |
"nome": "Teste de Normalidade (Comparação com a Curva Normal)",
|
|
|
|
| 407 |
"interpretacao": [
|
| 408 |
"Ideal 68% → aceitável entre 64% e 75%",
|
| 409 |
"Ideal 90% → aceitável entre 88% e 95%",
|
| 410 |
+
"Ideal 95% → aceitável entre 95% e 100%",
|
| 411 |
+
],
|
| 412 |
},
|
| 413 |
"teste_dw": {
|
| 414 |
"nome": "Teste de Autocorrelação (Durbin-Watson)",
|
| 415 |
"estatistica": diagnosticos.get("teste_dw", {}).get("estatistica"),
|
| 416 |
+
"interpretacao": diagnosticos.get("teste_dw", {}).get("interpretacao"),
|
| 417 |
},
|
| 418 |
"teste_bp": {
|
| 419 |
"nome": "Teste de Homocedasticidade (Breusch-Pagan)",
|
| 420 |
"estatistica": diagnosticos.get("teste_bp", {}).get("estatistica"),
|
| 421 |
"pvalor": diagnosticos.get("teste_bp", {}).get("p_valor"),
|
| 422 |
+
"interpretacao": diagnosticos.get("teste_bp", {}).get("interpretacao"),
|
| 423 |
},
|
| 424 |
+
"equacao": diagnosticos.get("equacao"),
|
| 425 |
}
|
| 426 |
|
| 427 |
+
|
|
|
|
|
|
|
| 428 |
def formatar_monetario(valor):
|
| 429 |
+
if pd.isna(valor):
|
| 430 |
+
return "N/A"
|
| 431 |
return f"R$ {valor:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
| 432 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
|
| 434 |
+
def formatar_resumo_html(resumo_reorganizado):
|
| 435 |
def criar_titulo_secao(titulo):
|
| 436 |
return f'<div class="section-title-orange-solid">{titulo}</div>'
|
| 437 |
|
| 438 |
def criar_linha_campo(campo, valor):
|
| 439 |
+
return f'<div class="field-row"><span class="field-row-label">{campo}</span><span class="field-row-value">{valor}</span></div>'
|
| 440 |
|
| 441 |
def criar_linha_interpretacao(interpretacao):
|
| 442 |
+
return f'<div class="field-row"><span class="field-row-label">Interpretação</span><span class="field-row-value-italic">{interpretacao}</span></div>'
|
| 443 |
|
| 444 |
def formatar_numero(valor, casas_decimais=4):
|
| 445 |
+
if valor is None:
|
| 446 |
+
return "N/A"
|
| 447 |
+
if isinstance(valor, (int, float, np.floating)):
|
| 448 |
+
return f"{valor:.{casas_decimais}f}"
|
| 449 |
return str(valor)
|
| 450 |
|
| 451 |
linhas_html = []
|
|
|
|
|
|
|
| 452 |
estat_gerais = resumo_reorganizado.get("estatisticas_gerais", {})
|
| 453 |
if estat_gerais:
|
| 454 |
linhas_html.append(criar_titulo_secao("Estatísticas Gerais"))
|
| 455 |
for chave, dados in estat_gerais.items():
|
| 456 |
linhas_html.append(criar_linha_campo(dados.get("nome", chave), formatar_numero(dados.get("valor"))))
|
| 457 |
|
|
|
|
| 458 |
for chave_teste, label in [("teste_f", "Estatística F"), ("teste_ks", "Estatística KS")]:
|
| 459 |
teste = resumo_reorganizado.get(chave_teste, {})
|
| 460 |
if teste.get("estatistica") is not None:
|
|
|
|
| 465 |
if teste.get("interpretacao"):
|
| 466 |
linhas_html.append(criar_linha_interpretacao(teste["interpretacao"]))
|
| 467 |
|
|
|
|
| 468 |
perc_resid = resumo_reorganizado.get("perc_resid", {})
|
| 469 |
if perc_resid.get("valor") is not None:
|
| 470 |
linhas_html.append(criar_titulo_secao(perc_resid.get("nome")))
|
|
|
|
| 484 |
if teste.get("interpretacao"):
|
| 485 |
linhas_html.append(criar_linha_interpretacao(teste["interpretacao"]))
|
| 486 |
|
| 487 |
+
return f'<div class="dai-card scrollable-container">{"".join(linhas_html)}</div>'
|
| 488 |
+
|
| 489 |
|
|
|
|
|
|
|
|
|
|
| 490 |
def criar_titulo_secao_html(titulo):
|
| 491 |
return f'<div class="section-title-orange">{titulo}</div>'
|
| 492 |
|
| 493 |
+
|
|
|
|
|
|
|
| 494 |
def formatar_escalas_html(escalas_raw):
|
| 495 |
+
if isinstance(escalas_raw, pd.DataFrame):
|
| 496 |
+
itens = escalas_raw.iloc[:, 0].tolist()
|
| 497 |
+
elif isinstance(escalas_raw, list):
|
| 498 |
+
itens = escalas_raw
|
| 499 |
+
else:
|
| 500 |
+
itens = [str(escalas_raw)]
|
| 501 |
|
| 502 |
itens = [str(item) for item in itens if item and str(item).strip()]
|
| 503 |
+
if not itens:
|
| 504 |
+
return "<p style='color: #6c757d; font-style: italic;'>Nenhuma transformação disponível.</p>"
|
| 505 |
|
| 506 |
max_chars = max(len(item) for item in itens)
|
| 507 |
largura_min = max(150, max_chars * 7 + 28)
|
|
|
|
| 509 |
for item in itens:
|
| 510 |
if ":" in item:
|
| 511 |
partes = item.split(":", 1)
|
| 512 |
+
conteudo = (
|
| 513 |
+
f'<span style="font-weight: 600; color: #495057;">{partes[0].strip()}:</span>'
|
| 514 |
+
f'<span style="font-weight: 400; color: #6c757d; margin-left: 4px;">{partes[1].strip()}</span>'
|
| 515 |
+
)
|
| 516 |
else:
|
| 517 |
+
conteudo = f'<span style="font-weight: 600; color: #495057;">{item}</span>'
|
| 518 |
+
cards_html += f'<div class="dai-card-light" style="min-width: {largura_min}px;">{conteudo}</div>'
|
| 519 |
|
| 520 |
+
return (
|
| 521 |
+
f'<div class="dai-card">{criar_titulo_secao_html("Escalas / Transformações")}'
|
| 522 |
+
f'<div class="dai-cards-grid" style="grid-template-columns: repeat(auto-fill, minmax({largura_min}px, 1fr));">'
|
| 523 |
+
f"{cards_html}</div></div>"
|
| 524 |
+
)
|
| 525 |
|
| 526 |
|
| 527 |
def _aplicar_jitter_sobrepostos(df_mapa, lat_col, lon_col, lat_plot_col, lon_plot_col):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
df_plot = df_mapa.copy()
|
| 529 |
df_plot[lat_plot_col] = pd.to_numeric(df_plot[lat_col], errors="coerce")
|
| 530 |
df_plot[lon_plot_col] = pd.to_numeric(df_plot[lon_col], errors="coerce")
|
|
|
|
| 539 |
passo_metros = 4.0
|
| 540 |
max_raio_metros = 22.0
|
| 541 |
metros_por_grau_lat = 111_320.0
|
|
|
|
| 542 |
lat_plot_pos = int(df_plot.columns.get_loc(lat_plot_col))
|
| 543 |
lon_plot_pos = int(df_plot.columns.get_loc(lon_plot_col))
|
| 544 |
|
|
|
|
| 559 |
for pos, pos_idx in enumerate(posicoes):
|
| 560 |
if pos == 0:
|
| 561 |
continue
|
|
|
|
| 562 |
pos_ring = pos - 1
|
| 563 |
ring = 1
|
| 564 |
while pos_ring >= (6 * ring):
|
|
|
|
| 568 |
slots_ring = max(6 * ring, 1)
|
| 569 |
angulo = angulo_base + (2.0 * math.pi * (pos_ring / slots_ring))
|
| 570 |
raio_m = min(ring * passo_metros, max_raio_metros)
|
|
|
|
| 571 |
delta_lat = (raio_m * math.sin(angulo)) / metros_por_grau_lat
|
| 572 |
delta_lon = (raio_m * math.cos(angulo)) / metros_por_grau_lon
|
| 573 |
|
|
|
|
| 605 |
itens_por_pagina = max_itens_pagina * max_colunas_por_pagina
|
| 606 |
paginas = [itens[i:i + itens_por_pagina] for i in range(0, len(itens), itens_por_pagina)]
|
| 607 |
itens_primeira_pagina = len(paginas[0]) if paginas else 0
|
| 608 |
+
colunas_visiveis = max(
|
| 609 |
+
1,
|
| 610 |
+
min(
|
| 611 |
+
max_colunas_por_pagina,
|
| 612 |
+
int(math.ceil(itens_primeira_pagina / max_itens_pagina)) if itens_primeira_pagina else 1,
|
| 613 |
+
),
|
| 614 |
+
)
|
| 615 |
+
popup_largura_px = (
|
| 616 |
+
popup_padding_horizontal_px
|
| 617 |
+
+ (coluna_largura_px * colunas_visiveis)
|
| 618 |
+
+ (gap_cols_px * (colunas_visiveis - 1))
|
| 619 |
+
)
|
| 620 |
|
| 621 |
pages_html = []
|
| 622 |
for page_idx, page_items in enumerate(paginas):
|
|
|
|
| 658 |
if len(paginas) > 1:
|
| 659 |
controls_html = (
|
| 660 |
"<div class='mesa-popup-controls' style='display:flex; gap:5px; flex-wrap:nowrap; margin-top:8px; align-items:center; justify-content:center; white-space:nowrap; width:100%;'>"
|
| 661 |
+
"<button type='button' data-page-nav='first' data-a='first' style=\"border:1px solid #ced8e2; background:#fff; border-radius:6px; padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;\">«</button>"
|
| 662 |
+
"<button type='button' data-page-nav='prev' data-a='prev' style=\"border:1px solid #ced8e2; background:#fff; border-radius:6px; padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;\">‹</button>"
|
| 663 |
"<div data-page-number-wrap='1' style='display:flex; gap:5px; align-items:center; justify-content:center; flex-wrap:nowrap;'></div>"
|
| 664 |
+
"<button type='button' data-page-nav='next' data-a='next' style=\"border:1px solid #ced8e2; background:#fff; border-radius:6px; padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;\">›</button>"
|
| 665 |
+
"<button type='button' data-page-nav='last' data-a='last' style=\"border:1px solid #ced8e2; background:#fff; border-radius:6px; padding:2px 7px; font-size:11px; cursor:pointer; color:#4e6479;\">»</button>"
|
| 666 |
"</div>"
|
| 667 |
)
|
| 668 |
|
|
|
|
| 725 |
)
|
| 726 |
return html, 380
|
| 727 |
|
| 728 |
+
|
|
|
|
|
|
|
| 729 |
def criar_mapa(
|
| 730 |
df,
|
| 731 |
lat_col="lat",
|
|
|
|
| 738 |
popup_auth_token=None,
|
| 739 |
avaliandos_tecnicos=None,
|
| 740 |
):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 741 |
lat_real = None
|
| 742 |
lon_real = None
|
| 743 |
for col in df.columns:
|
|
|
|
| 758 |
return None
|
| 759 |
return dataframe.iloc[:, matches[0]]
|
| 760 |
|
|
|
|
| 761 |
df_mapa = df.copy()
|
| 762 |
lat_key = "__mesa_lat__"
|
| 763 |
lon_key = "__mesa_lon__"
|
|
|
|
| 770 |
df_mapa[lon_key] = pd.to_numeric(lon_serie, errors="coerce")
|
| 771 |
df_mapa = df_mapa.dropna(subset=[lat_key, lon_key])
|
| 772 |
df_mapa = df_mapa[
|
| 773 |
+
(df_mapa[lat_key] >= -90.0)
|
| 774 |
+
& (df_mapa[lat_key] <= 90.0)
|
| 775 |
+
& (df_mapa[lon_key] >= -180.0)
|
| 776 |
+
& (df_mapa[lon_key] <= 180.0)
|
| 777 |
].copy()
|
| 778 |
if df_mapa.empty:
|
| 779 |
return "<p>Sem coordenadas válidas para exibir.</p>"
|
| 780 |
|
|
|
|
| 781 |
centro_lat = float(df_mapa[lat_key].median())
|
| 782 |
centro_lon = float(df_mapa[lon_key].median())
|
| 783 |
+
mapa = folium.Map(
|
|
|
|
| 784 |
location=[centro_lat, centro_lon],
|
| 785 |
zoom_start=12,
|
| 786 |
tiles=None,
|
| 787 |
prefer_canvas=True,
|
| 788 |
control_scale=True,
|
| 789 |
)
|
| 790 |
+
folium.TileLayer(tiles="OpenStreetMap", name="OpenStreetMap", control=True, show=True).add_to(mapa)
|
| 791 |
+
folium.TileLayer(tiles="CartoDB positron", name="Positron", control=True, show=False).add_to(mapa)
|
| 792 |
+
add_bairros_layer(mapa, show=True)
|
| 793 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 794 |
if tamanho_col and tamanho_col != "Visualização Padrão" and not cor_col:
|
| 795 |
cor_col = tamanho_col
|
| 796 |
|
|
|
|
| 797 |
colormap = None
|
| 798 |
cor_key = None
|
| 799 |
if cor_col and cor_col in df_mapa.columns:
|
|
|
|
| 808 |
colors=["#2ecc71", "#a8e06c", "#f1c40f", "#e67e22", "#e74c3c"],
|
| 809 |
vmin=vmin,
|
| 810 |
vmax=vmax,
|
| 811 |
+
caption=cor_col,
|
| 812 |
)
|
| 813 |
+
colormap.add_to(mapa)
|
| 814 |
|
|
|
|
| 815 |
raio_min, raio_max = 3, 18
|
| 816 |
tamanho_func = None
|
| 817 |
tamanho_key = None
|
|
|
|
| 857 |
contorno_padrao = 0.8 if total_pontos_plot <= 2500 else 0.55
|
| 858 |
opacidade_preenchimento = 0.68 if total_pontos_plot <= 2500 else 0.6
|
| 859 |
|
|
|
|
| 860 |
for marker_ordem, (idx, row) in enumerate(df_plot_pontos.iterrows()):
|
| 861 |
+
cor = colormap(row[cor_key]) if colormap and cor_key and pd.notna(row[cor_key]) else COR_PRINCIPAL
|
|
|
|
|
|
|
|
|
|
|
|
|
| 862 |
|
|
|
|
| 863 |
if tamanho_func and tamanho_key and pd.notna(row[tamanho_key]):
|
| 864 |
raio = tamanho_func(row[tamanho_key])
|
| 865 |
peso_contorno = 1
|
|
|
|
| 867 |
raio = raio_padrao
|
| 868 |
peso_contorno = contorno_padrao
|
| 869 |
|
|
|
|
|
|
|
| 870 |
idx_display = int(row["index"]) if "index" in row.index else idx
|
| 871 |
popup_uid = f"mesa-pop-{marker_ordem}"
|
| 872 |
popup_html = None
|
|
|
|
| 886 |
popup_width = None
|
| 887 |
if popup_html is None or popup_width is None:
|
| 888 |
popup_html, popup_width = montar_popup_registro_html(row, popup_uid, max_itens_pagina=8)
|
| 889 |
+
|
| 890 |
tooltip_html = (
|
| 891 |
+
"<div style='font-family:\"Segoe UI\",Arial,sans-serif; font-size:14px; line-height:1.7; padding:2px 4px;'>"
|
|
|
|
| 892 |
f"<b>Índice {idx_display}</b>"
|
| 893 |
)
|
| 894 |
if tooltip_col and tooltip_key and tooltip_key in row.index:
|
|
|
|
| 901 |
val_str = f"{val_t:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
|
| 902 |
else:
|
| 903 |
val_str = str(val_t)
|
| 904 |
+
tooltip_html += f"<br><span style='color:#555;'>{tooltip_col}:</span> <b>{val_str}</b>"
|
|
|
|
|
|
|
|
|
|
| 905 |
tooltip_html += "</div>"
|
| 906 |
|
| 907 |
marcador = folium.CircleMarker(
|
|
|
|
| 909 |
radius=raio,
|
| 910 |
popup=folium.Popup(popup_html, max_width=popup_width),
|
| 911 |
tooltip=folium.Tooltip(tooltip_html, sticky=True),
|
| 912 |
+
color="black",
|
| 913 |
weight=peso_contorno,
|
| 914 |
fill=True,
|
| 915 |
fillColor=cor,
|
| 916 |
+
fillOpacity=opacidade_preenchimento,
|
| 917 |
+
).add_to(mapa)
|
| 918 |
marcador.options["mesaBaseRadius"] = float(max(1.0, raio))
|
| 919 |
|
| 920 |
if mostrar_indices and camada_indices is not None:
|
|
|
|
| 926 |
)
|
| 927 |
|
| 928 |
if mostrar_indices and camada_indices is not None:
|
| 929 |
+
camada_indices.add_to(mapa)
|
| 930 |
|
| 931 |
if avaliandos_tecnicos:
|
| 932 |
camada_trabalhos_tecnicos = folium.FeatureGroup(name="Avaliandos que usaram o modelo", show=True)
|
| 933 |
add_trabalhos_tecnicos_markers(camada_trabalhos_tecnicos, avaliandos_tecnicos)
|
| 934 |
+
camada_trabalhos_tecnicos.add_to(mapa)
|
| 935 |
|
| 936 |
+
folium.LayerControl().add_to(mapa)
|
| 937 |
+
plugins.Fullscreen().add_to(mapa)
|
|
|
|
| 938 |
plugins.MeasureControl(
|
| 939 |
+
primary_length_unit="meters",
|
| 940 |
+
secondary_length_unit="kilometers",
|
| 941 |
+
primary_area_unit="sqmeters",
|
| 942 |
+
secondary_area_unit="hectares",
|
| 943 |
+
).add_to(mapa)
|
| 944 |
+
add_zoom_responsive_circle_markers(mapa)
|
| 945 |
+
add_popup_pagination_handlers(mapa)
|
| 946 |
+
|
|
|
|
| 947 |
df_bounds = df_mapa
|
| 948 |
if len(df_mapa) >= 8:
|
| 949 |
lat_vals = df_mapa[lat_key]
|
|
|
|
| 952 |
lon_med = float(lon_vals.median())
|
| 953 |
lat_mad = float((lat_vals - lat_med).abs().median())
|
| 954 |
lon_mad = float((lon_vals - lon_med).abs().median())
|
|
|
|
| 955 |
lat_span = float(lat_vals.max() - lat_vals.min())
|
| 956 |
lon_span = float(lon_vals.max() - lon_vals.min())
|
| 957 |
lat_scale = max(lat_mad, lat_span / 30.0, 1e-6)
|
| 958 |
lon_scale = max(lon_mad, lon_span / 30.0, 1e-6)
|
|
|
|
| 959 |
score = ((lat_vals - lat_med) / lat_scale) ** 2 + ((lon_vals - lon_med) / lon_scale) ** 2
|
| 960 |
lim = float(score.quantile(0.75))
|
| 961 |
df_core = df_mapa[score <= lim]
|
|
|
|
| 983 |
lon_min = float(lon_min) - lon_delta
|
| 984 |
lon_max = float(lon_max) + lon_delta
|
| 985 |
|
| 986 |
+
mapa.fit_bounds([[float(lat_min), float(lon_min)], [float(lat_max), float(lon_max)]], padding=(48, 48), max_zoom=18)
|
| 987 |
+
return mapa.get_root().render()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 988 |
|
| 989 |
|
| 990 |
def _formatar_badge_completo(pacote, nome_modelo=""):
|
|
|
|
| 991 |
if not pacote:
|
| 992 |
return ""
|
| 993 |
|
| 994 |
def _esc(value):
|
| 995 |
+
return str(value or "").replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 996 |
|
| 997 |
def _data_br(value):
|
| 998 |
texto = str(value or "").strip()
|
|
|
|
| 1002 |
match_iso = re.match(r"^(\d{4})-(\d{2})-(\d{2})(?:[T\s].*)?$", texto)
|
| 1003 |
if match_iso:
|
| 1004 |
try:
|
| 1005 |
+
return datetime(int(match_iso.group(1)), int(match_iso.group(2)), int(match_iso.group(3))).strftime("%d/%m/%Y")
|
|
|
|
| 1006 |
except Exception:
|
| 1007 |
return texto
|
| 1008 |
|
| 1009 |
match_iso_slash = re.match(r"^(\d{4})/(\d{2})/(\d{2})(?:[T\s].*)?$", texto)
|
| 1010 |
if match_iso_slash:
|
| 1011 |
try:
|
| 1012 |
+
return datetime(int(match_iso_slash.group(1)), int(match_iso_slash.group(2)), int(match_iso_slash.group(3))).strftime("%d/%m/%Y")
|
|
|
|
| 1013 |
except Exception:
|
| 1014 |
return texto
|
| 1015 |
|
| 1016 |
match_br = re.match(r"^(\d{2})/(\d{2})/(\d{4})(?:[T\s].*)?$", texto)
|
| 1017 |
if match_br:
|
| 1018 |
try:
|
| 1019 |
+
return datetime(int(match_br.group(3)), int(match_br.group(2)), int(match_br.group(1))).strftime("%d/%m/%Y")
|
|
|
|
| 1020 |
except Exception:
|
| 1021 |
return texto
|
| 1022 |
|
|
|
|
| 1024 |
|
| 1025 |
model_name = str(nome_modelo or "").strip() or "-"
|
| 1026 |
observacao_modelo = str(pacote.get("observacao_modelo") or "").strip()
|
|
|
|
| 1027 |
periodo = pacote.get("periodo_dados_mercado") or {}
|
| 1028 |
data_inicial = _data_br(periodo.get("data_inicial"))
|
| 1029 |
data_final = _data_br(periodo.get("data_final"))
|
|
|
|
| 1132 |
"<div class='modelo-info-card'>"
|
| 1133 |
"<div class='modelo-info-split'>"
|
| 1134 |
"<div class='modelo-info-col'>"
|
| 1135 |
+
+ nome_modelo_html
|
| 1136 |
+
+ "<div class='modelo-info-stack-block'>"
|
| 1137 |
"<div class='elaborador-badge-title'>ELABORADO POR:</div>"
|
| 1138 |
+
+ elaborador_html
|
| 1139 |
+
+ "</div>"
|
| 1140 |
"</div>"
|
| 1141 |
"<div class='modelo-info-col modelo-info-col-vars'>"
|
| 1142 |
"<div class='elaborador-badge-title'>Variáveis selecionadas:</div>"
|
| 1143 |
+
+ y_html
|
| 1144 |
+
+ x_html
|
| 1145 |
+
+ periodo_html
|
| 1146 |
+
+ "</div>"
|
| 1147 |
"</div>"
|
| 1148 |
"</div>"
|
| 1149 |
"</div>"
|
| 1150 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/app/runtime_config.py
CHANGED
|
@@ -140,17 +140,27 @@ def apply_runtime_config(config_path: str | Path | None = None) -> RuntimeSettin
|
|
| 140 |
|
| 141 |
os.environ.setdefault("MESA_RUNTIME_DIR", str(runtime_dir))
|
| 142 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
models_dir = _expand_path(paths_cfg.get("models_dir"), base_dir=config_base_dir)
|
|
|
|
|
|
|
| 144 |
if models_dir is not None:
|
| 145 |
os.environ["MODELOS_REPOSITORIO_PROVIDER"] = "local"
|
| 146 |
os.environ["MODELOS_REPOSITORIO_LOCAL_DIR"] = str(models_dir)
|
| 147 |
|
| 148 |
trabalhos_db = _expand_path(paths_cfg.get("trabalhos_tecnicos_db"), base_dir=config_base_dir)
|
|
|
|
|
|
|
| 149 |
if trabalhos_db is not None:
|
| 150 |
os.environ["TRABALHOS_TECNICOS_PROVIDER"] = "local"
|
| 151 |
os.environ["TRABALHOS_TECNICOS_DB_LOCAL_PATH"] = str(trabalhos_db)
|
| 152 |
|
| 153 |
users_file = _expand_path(paths_cfg.get("users_file"), base_dir=config_base_dir)
|
|
|
|
|
|
|
| 154 |
if users_file is not None:
|
| 155 |
os.environ["APP_USERS_FILE"] = str(users_file)
|
| 156 |
|
|
|
|
| 140 |
|
| 141 |
os.environ.setdefault("MESA_RUNTIME_DIR", str(runtime_dir))
|
| 142 |
|
| 143 |
+
data_base_dir = _expand_path(paths_cfg.get("data_base_dir"), base_dir=config_base_dir)
|
| 144 |
+
if data_base_dir is not None:
|
| 145 |
+
os.environ["MESA_DATA_BASE_DIR"] = str(data_base_dir)
|
| 146 |
+
|
| 147 |
models_dir = _expand_path(paths_cfg.get("models_dir"), base_dir=config_base_dir)
|
| 148 |
+
if models_dir is None and data_base_dir is not None:
|
| 149 |
+
models_dir = (data_base_dir / "modelos_dai").resolve()
|
| 150 |
if models_dir is not None:
|
| 151 |
os.environ["MODELOS_REPOSITORIO_PROVIDER"] = "local"
|
| 152 |
os.environ["MODELOS_REPOSITORIO_LOCAL_DIR"] = str(models_dir)
|
| 153 |
|
| 154 |
trabalhos_db = _expand_path(paths_cfg.get("trabalhos_tecnicos_db"), base_dir=config_base_dir)
|
| 155 |
+
if trabalhos_db is None and data_base_dir is not None:
|
| 156 |
+
trabalhos_db = (data_base_dir / "trabalhos_tecnicos" / "trabalhos_tecnicos.sqlite3").resolve()
|
| 157 |
if trabalhos_db is not None:
|
| 158 |
os.environ["TRABALHOS_TECNICOS_PROVIDER"] = "local"
|
| 159 |
os.environ["TRABALHOS_TECNICOS_DB_LOCAL_PATH"] = str(trabalhos_db)
|
| 160 |
|
| 161 |
users_file = _expand_path(paths_cfg.get("users_file"), base_dir=config_base_dir)
|
| 162 |
+
if users_file is None and data_base_dir is not None:
|
| 163 |
+
users_file = (data_base_dir / "usuarios" / "usuarios.json").resolve()
|
| 164 |
if users_file is not None:
|
| 165 |
os.environ["APP_USERS_FILE"] = str(users_file)
|
| 166 |
|
backend/requirements.txt
CHANGED
|
@@ -15,5 +15,4 @@ openpyxl
|
|
| 15 |
pyshp
|
| 16 |
geopandas
|
| 17 |
fiona
|
| 18 |
-
gradio>=4.0
|
| 19 |
huggingface_hub>=0.30.0
|
|
|
|
| 15 |
pyshp
|
| 16 |
geopandas
|
| 17 |
fiona
|
|
|
|
| 18 |
huggingface_hub>=0.30.0
|
build/windows/appsettings.example.json
CHANGED
|
@@ -5,9 +5,7 @@
|
|
| 5 |
"open_browser": true
|
| 6 |
},
|
| 7 |
"paths": {
|
| 8 |
-
"
|
| 9 |
-
"trabalhos_tecnicos_db": "..\\dados\\trabalhos_tecnicos\\trabalhos_tecnicos.sqlite3",
|
| 10 |
-
"users_file": "..\\dados\\usuarios\\usuarios.json"
|
| 11 |
},
|
| 12 |
"env": {
|
| 13 |
"APP_LOGS_MODE": "disabled"
|
|
|
|
| 5 |
"open_browser": true
|
| 6 |
},
|
| 7 |
"paths": {
|
| 8 |
+
"data_base_dir": "../dados"
|
|
|
|
|
|
|
| 9 |
},
|
| 10 |
"env": {
|
| 11 |
"APP_LOGS_MODE": "disabled"
|
build/windows/build_portable.ps1
CHANGED
|
@@ -20,6 +20,7 @@ python -m PyInstaller build/windows/mesa_frame_portable.spec --noconfirm --clean
|
|
| 20 |
Write-Host "[mesa] provision external config folder"
|
| 21 |
New-Item -ItemType Directory -Force -Path "dist/MesaFrame/config" | Out-Null
|
| 22 |
Copy-Item "build/windows/appsettings.example.json" "dist/MesaFrame/config/appsettings.example.json" -Force
|
|
|
|
| 23 |
|
| 24 |
if (Test-Path "dist/MesaFrame-portable.zip") {
|
| 25 |
Remove-Item "dist/MesaFrame-portable.zip" -Force
|
|
|
|
| 20 |
Write-Host "[mesa] provision external config folder"
|
| 21 |
New-Item -ItemType Directory -Force -Path "dist/MesaFrame/config" | Out-Null
|
| 22 |
Copy-Item "build/windows/appsettings.example.json" "dist/MesaFrame/config/appsettings.example.json" -Force
|
| 23 |
+
Copy-Item "build/windows/appsettings.example.json" "dist/MesaFrame/config/appsettings.json" -Force
|
| 24 |
|
| 25 |
if (Test-Path "dist/MesaFrame-portable.zip") {
|
| 26 |
Remove-Item "dist/MesaFrame-portable.zip" -Force
|
build/windows/mesa_frame_portable.spec
CHANGED
|
@@ -16,8 +16,6 @@ datas = [
|
|
| 16 |
if LOCAL_DATA_DIR.exists():
|
| 17 |
datas.append((str(LOCAL_DATA_DIR), "local_data"))
|
| 18 |
datas += collect_data_files("safehttpx")
|
| 19 |
-
datas += collect_data_files("gradio")
|
| 20 |
-
datas += collect_data_files("gradio_client")
|
| 21 |
datas += collect_data_files("fiona")
|
| 22 |
datas += collect_data_files("geopandas")
|
| 23 |
datas += collect_data_files("pyproj")
|
|
|
|
| 16 |
if LOCAL_DATA_DIR.exists():
|
| 17 |
datas.append((str(LOCAL_DATA_DIR), "local_data"))
|
| 18 |
datas += collect_data_files("safehttpx")
|
|
|
|
|
|
|
| 19 |
datas += collect_data_files("fiona")
|
| 20 |
datas += collect_data_files("geopandas")
|
| 21 |
datas += collect_data_files("pyproj")
|
build/windows/package_portable_release.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
import shutil
|
| 6 |
+
import tempfile
|
| 7 |
+
import zipfile
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
REPO_ROOT = Path(__file__).resolve().parents[2]
|
| 12 |
+
DEFAULT_USERS_FILE = REPO_ROOT / "backend" / "app" / "core" / "auth" / "usuarios.json"
|
| 13 |
+
DEFAULT_MODELS_DIR = REPO_ROOT / "backend" / "app" / "core" / "pesquisa" / "modelos_dai"
|
| 14 |
+
DEFAULT_TRABALHOS_DB = REPO_ROOT / "backend" / "local_data" / "trabalhos_tecnicos.sqlite3"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def parse_args() -> argparse.Namespace:
|
| 18 |
+
parser = argparse.ArgumentParser(
|
| 19 |
+
description="Monta o zip final portatil do MesaFrame com dados externos e modelos."
|
| 20 |
+
)
|
| 21 |
+
parser.add_argument(
|
| 22 |
+
"--artifact-zip",
|
| 23 |
+
type=Path,
|
| 24 |
+
required=True,
|
| 25 |
+
help="Zip baixado do artifact do GitHub Actions.",
|
| 26 |
+
)
|
| 27 |
+
parser.add_argument(
|
| 28 |
+
"--output-zip",
|
| 29 |
+
type=Path,
|
| 30 |
+
required=True,
|
| 31 |
+
help="Caminho do zip final a ser gerado.",
|
| 32 |
+
)
|
| 33 |
+
parser.add_argument(
|
| 34 |
+
"--users-file",
|
| 35 |
+
type=Path,
|
| 36 |
+
default=DEFAULT_USERS_FILE,
|
| 37 |
+
help="Arquivo usuarios.json a incluir em dados/usuarios.",
|
| 38 |
+
)
|
| 39 |
+
parser.add_argument(
|
| 40 |
+
"--models-dir",
|
| 41 |
+
type=Path,
|
| 42 |
+
default=DEFAULT_MODELS_DIR,
|
| 43 |
+
help="Pasta local com os arquivos .dai.",
|
| 44 |
+
)
|
| 45 |
+
parser.add_argument(
|
| 46 |
+
"--trabalhos-db",
|
| 47 |
+
type=Path,
|
| 48 |
+
default=DEFAULT_TRABALHOS_DB,
|
| 49 |
+
help="SQLite de trabalhos tecnicos a incluir em dados/trabalhos_tecnicos.",
|
| 50 |
+
)
|
| 51 |
+
return parser.parse_args()
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def extract_zip(zip_path: Path, target_dir: Path) -> None:
|
| 55 |
+
with zipfile.ZipFile(zip_path, "r") as archive:
|
| 56 |
+
archive.extractall(target_dir)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def find_inner_portable_zip(extracted_artifact_dir: Path) -> Path:
|
| 60 |
+
matches = sorted(extracted_artifact_dir.rglob("MesaFrame-portable.zip"))
|
| 61 |
+
if not matches:
|
| 62 |
+
raise FileNotFoundError(
|
| 63 |
+
f"Nao encontrei MesaFrame-portable.zip dentro de {extracted_artifact_dir}"
|
| 64 |
+
)
|
| 65 |
+
return matches[0]
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def ensure_file(path: Path, label: str) -> Path:
|
| 69 |
+
if not path.exists():
|
| 70 |
+
raise FileNotFoundError(f"{label} nao encontrado: {path}")
|
| 71 |
+
return path
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def write_runtime_config(config_path: Path) -> None:
|
| 75 |
+
payload = {
|
| 76 |
+
"server": {
|
| 77 |
+
"host": "127.0.0.1",
|
| 78 |
+
"port": 8000,
|
| 79 |
+
"open_browser": True,
|
| 80 |
+
},
|
| 81 |
+
"paths": {
|
| 82 |
+
"data_base_dir": "../dados",
|
| 83 |
+
},
|
| 84 |
+
"env": {
|
| 85 |
+
"APP_LOGS_MODE": "disabled",
|
| 86 |
+
},
|
| 87 |
+
}
|
| 88 |
+
config_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def copy_tree_contents(source_dir: Path, target_dir: Path) -> None:
|
| 92 |
+
for item in source_dir.iterdir():
|
| 93 |
+
destination = target_dir / item.name
|
| 94 |
+
if item.is_dir():
|
| 95 |
+
shutil.copytree(item, destination)
|
| 96 |
+
else:
|
| 97 |
+
shutil.copy2(item, destination)
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def copy_models(models_dir: Path, target_models_dir: Path) -> int:
|
| 101 |
+
target_models_dir.mkdir(parents=True, exist_ok=True)
|
| 102 |
+
copied = 0
|
| 103 |
+
for model_file in sorted(models_dir.glob("*.dai")):
|
| 104 |
+
shutil.copy2(model_file, target_models_dir / model_file.name)
|
| 105 |
+
copied += 1
|
| 106 |
+
if copied == 0:
|
| 107 |
+
raise RuntimeError(f"Nenhum arquivo .dai encontrado em {models_dir}")
|
| 108 |
+
return copied
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def create_zip(source_dir: Path, output_zip: Path) -> None:
|
| 112 |
+
output_zip.parent.mkdir(parents=True, exist_ok=True)
|
| 113 |
+
if output_zip.exists():
|
| 114 |
+
output_zip.unlink()
|
| 115 |
+
with zipfile.ZipFile(output_zip, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
|
| 116 |
+
for file_path in sorted(source_dir.rglob("*")):
|
| 117 |
+
archive.write(file_path, file_path.relative_to(source_dir))
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def main() -> None:
|
| 121 |
+
args = parse_args()
|
| 122 |
+
|
| 123 |
+
artifact_zip = ensure_file(args.artifact_zip.resolve(), "Artifact zip")
|
| 124 |
+
users_file = ensure_file(args.users_file.resolve(), "usuarios.json")
|
| 125 |
+
trabalhos_db = ensure_file(args.trabalhos_db.resolve(), "SQLite de trabalhos tecnicos")
|
| 126 |
+
models_dir = ensure_file(args.models_dir.resolve(), "Pasta de modelos")
|
| 127 |
+
output_zip = args.output_zip.resolve()
|
| 128 |
+
|
| 129 |
+
with tempfile.TemporaryDirectory(prefix="mesa-frame-release-") as temp_dir_str:
|
| 130 |
+
temp_dir = Path(temp_dir_str)
|
| 131 |
+
artifact_dir = temp_dir / "artifact"
|
| 132 |
+
portable_dir = temp_dir / "portable"
|
| 133 |
+
stage_dir = temp_dir / "stage"
|
| 134 |
+
|
| 135 |
+
extract_zip(artifact_zip, artifact_dir)
|
| 136 |
+
inner_zip = find_inner_portable_zip(artifact_dir)
|
| 137 |
+
extract_zip(inner_zip, portable_dir)
|
| 138 |
+
|
| 139 |
+
source_app_dir = portable_dir / "MesaFrame"
|
| 140 |
+
if not source_app_dir.exists():
|
| 141 |
+
raise FileNotFoundError(f"Pasta MesaFrame nao encontrada dentro de {inner_zip}")
|
| 142 |
+
|
| 143 |
+
final_app_dir = stage_dir / "MesaFrame"
|
| 144 |
+
final_app_dir.mkdir(parents=True, exist_ok=True)
|
| 145 |
+
copy_tree_contents(source_app_dir, final_app_dir)
|
| 146 |
+
|
| 147 |
+
config_dir = final_app_dir / "config"
|
| 148 |
+
config_dir.mkdir(parents=True, exist_ok=True)
|
| 149 |
+
write_runtime_config(config_dir / "appsettings.json")
|
| 150 |
+
shutil.copy2(REPO_ROOT / "build" / "windows" / "appsettings.example.json", config_dir / "appsettings.example.json")
|
| 151 |
+
|
| 152 |
+
data_base_dir = final_app_dir / "dados"
|
| 153 |
+
models_target_dir = data_base_dir / "modelos_dai"
|
| 154 |
+
users_target_dir = data_base_dir / "usuarios"
|
| 155 |
+
trabalhos_target_dir = data_base_dir / "trabalhos_tecnicos"
|
| 156 |
+
|
| 157 |
+
users_target_dir.mkdir(parents=True, exist_ok=True)
|
| 158 |
+
trabalhos_target_dir.mkdir(parents=True, exist_ok=True)
|
| 159 |
+
|
| 160 |
+
models_count = copy_models(models_dir, models_target_dir)
|
| 161 |
+
shutil.copy2(users_file, users_target_dir / "usuarios.json")
|
| 162 |
+
shutil.copy2(trabalhos_db, trabalhos_target_dir / "trabalhos_tecnicos.sqlite3")
|
| 163 |
+
|
| 164 |
+
create_zip(stage_dir, output_zip)
|
| 165 |
+
print(
|
| 166 |
+
json.dumps(
|
| 167 |
+
{
|
| 168 |
+
"ok": True,
|
| 169 |
+
"output_zip": str(output_zip),
|
| 170 |
+
"models_count": models_count,
|
| 171 |
+
"users_file": str(users_file),
|
| 172 |
+
"trabalhos_db": str(trabalhos_db),
|
| 173 |
+
},
|
| 174 |
+
ensure_ascii=False,
|
| 175 |
+
indent=2,
|
| 176 |
+
)
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
if __name__ == "__main__":
|
| 181 |
+
main()
|