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 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&nbsp;≤&nbsp;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
- from app.runtime_config import resolve_core_path
18
-
19
- try:
20
- import gradio as gr
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
- # LÓGICA DE GERAÇÃO DE GRÁFICOS (ADAPTADA DE GRAFICOS.PY)
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='markers',
92
- marker=dict(
93
- color=COR_PRINCIPAL,
94
- size=10,
95
- line=dict(color='black', width=1)
96
- ),
97
- name='Dados',
98
  )
99
  if indices is not None:
100
- scatter_args['customdata'] = indices
101
- scatter_args['hovertemplate'] = "<b>Índice:</b> %{customdata}<br><b>Calculado:</b> %{x:.2f}<br><b>Observado:</b> %{y:.2f}<extra></extra>"
 
 
 
102
  else:
103
- scatter_args['hovertemplate'] = "<b>Calculado:</b> %{x:.2f}<br><b>Observado:</b> %{y:.2f}<extra></extra>"
 
 
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(go.Scatter(
114
- x=[min_val - margin, max_val + margin],
115
- y=[min_val - margin, max_val + margin],
116
- mode='lines',
117
- line=dict(color=COR_LINHA, dash='dash', width=2),
118
- name='Linha de identidade'
119
- ))
 
 
120
 
121
  fig.update_layout(
122
- title=dict(text='Valores Observados vs Calculados', x=0.5),
123
- xaxis_title='Valores Calculados',
124
- yaxis_title='Valores Observados',
125
  showlegend=True,
126
- plot_bgcolor='white',
127
- margin=dict(l=60, r=40, t=60, b=60)
128
  )
129
-
130
- fig.update_xaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
131
- fig.update_yaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
132
-
133
  return fig
134
- except Exception as e:
135
- print(f"Erro ao criar gráfico obs vs calc: {e}")
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='markers',
147
- marker=dict(
148
- color=COR_PRINCIPAL,
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['customdata'] = indices
156
- scatter_args['hovertemplate'] = "<b>Índice:</b> %{customdata}<br><b>Ajustado:</b> %{x:.2f}<br><b>Resíduo:</b> %{y:.4f}<extra></extra>"
 
 
 
157
  else:
158
- scatter_args['hovertemplate'] = "<b>Ajustado:</b> %{x:.2f}<br><b>Resíduo:</b> %{y:.4f}<extra></extra>"
 
 
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='Resíduos vs Valores Ajustados', x=0.5),
166
- xaxis_title='Valores Ajustados',
167
- yaxis_title='Resíduos',
168
  showlegend=False,
169
- plot_bgcolor='white',
170
- margin=dict(l=60, r=40, t=60, b=60)
171
  )
172
-
173
- fig.update_xaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
174
- fig.update_yaxes(showgrid=True, gridcolor='lightgray', showline=True, linecolor='black')
175
-
176
  return fig
177
- except Exception as e:
178
- print(f"Erro ao criar gráfico de resíduos: {e}")
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
- fig.add_trace(go.Histogram(
187
- x=residuos,
188
- histnorm='probability density',
189
- marker=dict(color=COR_PRINCIPAL, line=dict(color='black', width=1)),
190
- opacity=0.7,
191
- name='Resíduos'
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
- fig.add_trace(go.Scatter(
199
- x=x_norm,
200
- y=y_norm,
201
- mode='lines',
202
- line=dict(color=COR_LINHA, width=3),
203
- name='Curva Normal'
204
- ))
 
205
 
206
  fig.update_layout(
207
- title=dict(text='Distribuição dos Resíduos', x=0.5),
208
- xaxis_title='Resíduos',
209
- yaxis_title='Densidade',
210
  showlegend=True,
211
- plot_bgcolor='white',
212
- barmode='overlay',
213
- margin=dict(l=60, r=40, t=60, b=60)
214
  )
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
-
219
  return fig
220
- except Exception as e:
221
- print(f"Erro ao criar histograma: {e}")
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: return 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(x=[idx, idx],
244
- y=[0, valor],
245
- mode='lines',
246
- line=dict(color=cor, width=1.5),
247
- showlegend=False,
248
- hoverinfo='skip'))
249
-
250
- # Pontos
251
- cores_pontos = [
252
- COR_LINHA if v > limite else COR_PRINCIPAL for v in cooks_d
253
- ]
254
  fig.add_trace(
255
  go.Scatter(
256
  x=indices,
257
  y=cooks_d,
258
- mode='markers',
259
- marker=dict(color=cores_pontos,
260
- size=8,
261
- line=dict(color='black', width=1)),
262
- name='Distância de Cook',
263
- hovertemplate='Obs: %{x}<br>Cook: %{y:.4f}<extra></extra>'))
264
-
265
- fig.add_hline(y=limite,
266
- line_dash="dash",
267
- line_color='gray',
268
- annotation_text=f"4/n = {limite:.4f}",
269
- annotation_position="top right")
270
-
271
- fig.update_layout(title=dict(text='Distância de Cook', x=0.5),
272
- xaxis_title='Observação',
273
- yaxis_title='Distância de Cook',
274
- plot_bgcolor='white',
275
- margin=dict(l=60, r=40, t=60, b=60))
276
-
277
- fig.update_xaxes(showgrid=True,
278
- gridcolor='lightgray',
279
- showline=True,
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 e:
288
- print(f"Erro ao criar gráfico de Cook: {e}")
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, 'model'):
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 = str(nome_y or "").strip() or str(getattr(model, 'endog_names', 'Variável Dependente') or '').strip() or 'Variável Dependente'
 
 
 
 
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 ('const', 'intercept')],
307
- errors='ignore'
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
- # texto
334
- text = np.where(
335
- np.isnan(corr.values),
336
- "",
337
- np.round(corr.values, 2).astype(str)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, y0=1,
377
- x1=1, y1=0,
 
 
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='plotly_white',
386
  xaxis=dict(tickangle=45, showgrid=False),
387
- yaxis=dict(autorange='reversed', showgrid=False)
388
  )
389
-
390
  return fig
391
-
392
- except Exception as e:
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
- """Orquestra a geração de todos os gráficos a partir do pacote."""
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
- # Encontrar coluna observada
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, 'endog'):
521
  y_obs = modelos_sm.model.endog
522
- if y_calc is None and hasattr(modelos_sm, 'fittedvalues'):
523
  y_calc = modelos_sm.fittedvalues
524
- if residuos is None and hasattr(modelos_sm, 'resid'):
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): return "N/A"
 
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"""<div class="field-row"><span class="field-row-label">{campo}</span><span class="field-row-value">{valor}</span></div>"""
624
 
625
  def criar_linha_interpretacao(interpretacao):
626
- return f"""<div class="field-row"><span class="field-row-label">Interpretação</span><span class="field-row-value-italic">{interpretacao}</span></div>"""
627
 
628
  def formatar_numero(valor, casas_decimais=4):
629
- if valor is None: return "N/A"
630
- if isinstance(valor, (int, float, np.floating)): return f"{valor:.{casas_decimais}f}"
 
 
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"""<div class="dai-card scrollable-container">{"".join(linhas_html)}</div>"""
 
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): itens = escalas_raw.iloc[:, 0].tolist()
686
- elif isinstance(escalas_raw, list): itens = escalas_raw
687
- else: itens = [str(escalas_raw)]
 
 
 
688
 
689
  itens = [str(item) for item in itens if item and str(item).strip()]
690
- if not itens: return "<p style='color: #6c757d; font-style: italic;'>Nenhuma transformação disponível.</p>"
 
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 = f"""<span style="font-weight: 600; color: #495057;">{partes[0].strip()}:</span><span style="font-weight: 400; color: #6c757d; margin-left: 4px;">{partes[1].strip()}</span>"""
 
 
 
699
  else:
700
- conteudo = f"""<span style="font-weight: 600; color: #495057;">{item}</span>"""
701
- cards_html += f"""<div class="dai-card-light" style="min-width: {largura_min}px;">{conteudo}</div>"""
702
 
703
- return f"""<div class="dai-card">{criar_titulo_secao_html("Escalas / Transformações")}<div class="dai-cards-grid" style="grid-template-columns: repeat(auto-fill, minmax({largura_min}px, 1fr));">{cards_html}</div></div>"""
 
 
 
 
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(1, min(max_colunas_por_pagina, int(math.ceil(itens_primeira_pagina / max_itens_pagina)) if itens_primeira_pagina else 1))
795
- popup_largura_px = popup_padding_horizontal_px + (coluna_largura_px * colunas_visiveis) + (gap_cols_px * (colunas_visiveis - 1))
 
 
 
 
 
 
 
 
 
 
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
- f"<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;\">&laquo;</button>"
838
- f"<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;\">&lsaquo;</button>"
839
  "<div data-page-number-wrap='1' style='display:flex; gap:5px; align-items:center; justify-content:center; flex-wrap:nowrap;'></div>"
840
- f"<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;\">&rsaquo;</button>"
841
- f"<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;\">&raquo;</button>"
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) & (df_mapa[lat_key] <= 90.0) &
967
- (df_mapa[lon_key] >= -180.0) & (df_mapa[lon_key] <= 180.0)
 
 
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(m)
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
- # Cor do ponto
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='black',
1122
  weight=peso_contorno,
1123
  fill=True,
1124
  fillColor=cor,
1125
- fillOpacity=opacidade_preenchimento
1126
- ).add_to(m)
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(m)
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(m)
1144
 
1145
- # Controles
1146
- folium.LayerControl().add_to(m)
1147
- plugins.Fullscreen().add_to(m)
1148
  plugins.MeasureControl(
1149
- primary_length_unit='meters',
1150
- secondary_length_unit='kilometers',
1151
- primary_area_unit='sqmeters',
1152
- secondary_area_unit='hectares'
1153
- ).add_to(m)
1154
- add_zoom_responsive_circle_markers(m)
1155
- add_popup_pagination_handlers(m)
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
- bounds = [[float(lat_min), float(lon_min)], [float(lat_max), float(lon_max)]]
1200
- m.fit_bounds(bounds, padding=(48, 48), max_zoom=18)
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("&", "&amp;")
1515
- .replace("<", "&lt;")
1516
- .replace(">", "&gt;")
1517
- .replace('"', "&quot;")
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
- dt = datetime(int(match_iso.group(1)), int(match_iso.group(2)), int(match_iso.group(3)))
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
- dt = datetime(int(match_iso_slash.group(1)), int(match_iso_slash.group(2)), int(match_iso_slash.group(3)))
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
- dt = datetime(int(match_br.group(3)), int(match_br.group(2)), int(match_br.group(1)))
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
- x_html +
1672
- periodo_html +
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;\">&laquo;</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;\">&lsaquo;</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;\">&rsaquo;</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;\">&raquo;</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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;")
 
 
 
 
 
 
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
- "models_dir": "..\\dados\\modelos_dai",
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()