Guilherme Silberfarb Costa commited on
Commit
da3ac65
·
1 Parent(s): 303655d

melhorias gerais

Browse files
README.md CHANGED
@@ -71,6 +71,11 @@ Variáveis de ambiente do backend:
71
  - `MODELOS_REPOSITORIO_HF_SUBDIR` (ex.: `modelos_dai`)
72
  - `HF_TOKEN` (opcional para dataset privado)
73
 
 
 
 
 
 
74
  No modo `hf_dataset`, o backend consulta a revisão atual do dataset e só
75
  sincroniza novamente quando detectar mudança de revisão.
76
 
 
71
  - `MODELOS_REPOSITORIO_HF_SUBDIR` (ex.: `modelos_dai`)
72
  - `HF_TOKEN` (opcional para dataset privado)
73
 
74
+ Regra automática de provider:
75
+
76
+ - Em runtime HF Spaces (`SPACE_ID`/`SPACE_AUTHOR_NAME`/`HF_SPACE_ID`), o backend força `hf_dataset`.
77
+ - Fora do HF Spaces, o fallback continua `local` quando o provider não é informado.
78
+
79
  No modo `hf_dataset`, o backend consulta a revisão atual do dataset e só
80
  sincroniza novamente quando detectar mudança de revisão.
81
 
backend/app/core/elaboracao/charts.py CHANGED
@@ -460,7 +460,7 @@ def criar_grafico_cook(cook_distances, indices=None, limite=None):
460
  return fig
461
 
462
 
463
- def criar_matriz_correlacao(df, colunas=None):
464
  """
465
  Heatmap de correlação com estilo premium:
466
  Diagonal limpa, linha divisória e cores customizadas.
@@ -487,6 +487,15 @@ def criar_matriz_correlacao(df, colunas=None):
487
  # Cálculo da correlação
488
  corr = df_corr.corr()
489
 
 
 
 
 
 
 
 
 
 
490
  # --- LÓGICA DE ESTILO DA PRIMEIRA FUNÇÃO ---
491
 
492
  # 1. Remover diagonal (substitui por NaN para não colorir o Heatmap)
 
460
  return fig
461
 
462
 
463
+ def criar_matriz_correlacao(df, colunas=None, coluna_y: str | None = None):
464
  """
465
  Heatmap de correlação com estilo premium:
466
  Diagonal limpa, linha divisória e cores customizadas.
 
487
  # Cálculo da correlação
488
  corr = df_corr.corr()
489
 
490
+ if coluna_y:
491
+ coluna_y_str = str(coluna_y)
492
+ rotulos = [
493
+ f"{nome} (Y)" if str(nome) == coluna_y_str and not str(nome).endswith(" (Y)") else str(nome)
494
+ for nome in corr.columns
495
+ ]
496
+ corr.columns = rotulos
497
+ corr.index = rotulos
498
+
499
  # --- LÓGICA DE ESTILO DA PRIMEIRA FUNÇÃO ---
500
 
501
  # 1. Remover diagonal (substitui por NaN para não colorir o Heatmap)
backend/app/core/elaboracao/core.py CHANGED
@@ -2312,10 +2312,22 @@ def avaliar_imovel(modelo_sm, valores_x, colunas_x, transformacoes_x, transforma
2312
 
2313
  if percentual > 100:
2314
  qtd_extrapolacoes_acima_limites += 1
2315
- extrapolacoes[col] = {"status": "grave", "percentual": percentual, "direcao": "acima"}
 
 
 
 
 
 
2316
  else:
2317
  qtd_extrapolacoes_dentro_limites += 1
2318
- extrapolacoes[col] = {"status": "warning", "percentual": percentual, "direcao": "acima"}
 
 
 
 
 
 
2319
 
2320
  # Abaixo do mínimo
2321
  elif val < min_val:
@@ -2326,10 +2338,22 @@ def avaliar_imovel(modelo_sm, valores_x, colunas_x, transformacoes_x, transforma
2326
 
2327
  if percentual > 50:
2328
  qtd_extrapolacoes_acima_limites += 1
2329
- extrapolacoes[col] = {"status": "grave", "percentual": percentual, "direcao": "abaixo"}
 
 
 
 
 
 
2330
  else:
2331
  qtd_extrapolacoes_dentro_limites += 1
2332
- extrapolacoes[col] = {"status": "warning", "percentual": percentual, "direcao": "abaixo"}
 
 
 
 
 
 
2333
 
2334
  # --------------------------------------------------------
2335
  # 3. APLICAR TRANSFORMAÇÕES
 
2312
 
2313
  if percentual > 100:
2314
  qtd_extrapolacoes_acima_limites += 1
2315
+ extrapolacoes[col] = {
2316
+ "status": "grave",
2317
+ "percentual": percentual,
2318
+ "direcao": "acima",
2319
+ "limite_utilizado": float(max_val),
2320
+ "valor_informado": float(val),
2321
+ }
2322
  else:
2323
  qtd_extrapolacoes_dentro_limites += 1
2324
+ extrapolacoes[col] = {
2325
+ "status": "warning",
2326
+ "percentual": percentual,
2327
+ "direcao": "acima",
2328
+ "limite_utilizado": float(max_val),
2329
+ "valor_informado": float(val),
2330
+ }
2331
 
2332
  # Abaixo do mínimo
2333
  elif val < min_val:
 
2338
 
2339
  if percentual > 50:
2340
  qtd_extrapolacoes_acima_limites += 1
2341
+ extrapolacoes[col] = {
2342
+ "status": "grave",
2343
+ "percentual": percentual,
2344
+ "direcao": "abaixo",
2345
+ "limite_utilizado": float(min_val),
2346
+ "valor_informado": float(val),
2347
+ }
2348
  else:
2349
  qtd_extrapolacoes_dentro_limites += 1
2350
+ extrapolacoes[col] = {
2351
+ "status": "warning",
2352
+ "percentual": percentual,
2353
+ "direcao": "abaixo",
2354
+ "limite_utilizado": float(min_val),
2355
+ "valor_informado": float(val),
2356
+ }
2357
 
2358
  # --------------------------------------------------------
2359
  # 3. APLICAR TRANSFORMAÇÕES
backend/app/core/elaboracao/formatadores.py CHANGED
@@ -520,6 +520,70 @@ def _popup_fundamentacao_html(aval):
520
  return html
521
 
522
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
523
  def formatar_avaliacao_html(avaliacoes_lista, indice_base=0, elem_id_excluir="excluir-aval-elab"):
524
  """Formata resultados de avaliação como tabela HTML acumulada.
525
 
@@ -589,11 +653,13 @@ def formatar_avaliacao_html(avaliacoes_lista, indice_base=0, elem_id_excluir="ex
589
  elif status in ("dicotomica", "codigo_alocado", "percentual"):
590
  celula = f'{val_fmt} \u2014'
591
  elif status == "warning":
592
- celula = f'{val_fmt} \u26a0\ufe0f {perc:.1f}%'
 
593
  elif ext.get("valor_invalido"):
594
  celula = f'{val_fmt} \u274c'
595
  else: # grave
596
- celula = f'{val_fmt} \u274c {perc:.1f}%'
 
597
 
598
  html += f'<td {_td_r}>{celula}</td>'
599
  html += '</tr>'
@@ -605,6 +671,17 @@ def formatar_avaliacao_html(avaliacoes_lista, indice_base=0, elem_id_excluir="ex
605
  html += f'<td {_td_bold_r} style="text-align: right; padding: 8px 12px; border-bottom: 1px solid #dee2e6; font-weight: 600;">{_formatar_brl(aval["estimado"])}</td>'
606
  html += '</tr>'
607
 
 
 
 
 
 
 
 
 
 
 
 
608
  # Estimado / Base (só se houver mais de 1 avaliação e base selecionada)
609
  if n > 1 and indice_base_normalizado is not None:
610
  estimado_base = avaliacoes_lista[indice_base_normalizado]["estimado"]
 
520
  return html
521
 
522
 
523
+ def _popup_fronteira_html(aval):
524
+ """Gera conteúdo HTML do popup para a linha 'Extrapoladas na fronteira'."""
525
+
526
+ def _fmt_num(valor):
527
+ if valor is None:
528
+ return "—"
529
+ try:
530
+ bruto = float(valor)
531
+ except Exception:
532
+ return "—"
533
+ s = f"{bruto:,.2f}"
534
+ return s.replace(",", "X").replace(".", ",").replace("X", ".")
535
+
536
+ if not aval.get("houve_extrapolacao"):
537
+ return (
538
+ '<div style="font-weight: 600; margin-bottom: 4px; color: #495057;">'
539
+ 'Extrapoladas na fronteira</div>'
540
+ '<div style="font-size: 11px; color: #6c757d; line-height: 1.45;">'
541
+ 'Nenhuma variável extrapolou os limites amostrais nesta avaliação.'
542
+ '</div>'
543
+ )
544
+
545
+ fronteira = _formatar_brl(aval.get("fronteira"))
546
+ linhas = []
547
+ for col, info in (aval.get("extrapolacoes") or {}).items():
548
+ status = str(info.get("status") or "")
549
+ if status not in ("warning", "grave"):
550
+ continue
551
+ informado = _fmt_num(info.get("valor_informado"))
552
+ limite = _fmt_num(info.get("limite_utilizado"))
553
+ direcao = str(info.get("direcao") or "")
554
+ if direcao == "acima":
555
+ direcao_txt = "acima do máx."
556
+ elif direcao == "abaixo":
557
+ direcao_txt = "abaixo do mín."
558
+ else:
559
+ direcao_txt = "fora"
560
+ linhas.append(
561
+ '<tr style="border-bottom: 1px solid #f0f0f0;">'
562
+ f'<td style="padding: 2px 6px;"><b>{col}</b></td>'
563
+ f'<td style="padding: 2px 6px; text-align: right;">{informado}</td>'
564
+ f'<td style="padding: 2px 6px; text-align: right;">{limite}</td>'
565
+ f'<td style="padding: 2px 6px;">{direcao_txt}</td>'
566
+ '</tr>'
567
+ )
568
+
569
+ tabela = ''.join(linhas) if linhas else '<tr><td colspan="4" style="padding: 4px 6px;">Sem detalhes.</td></tr>'
570
+ return (
571
+ '<div style="font-weight: 600; margin-bottom: 4px; color: #495057;">'
572
+ 'Extrapoladas na fronteira</div>'
573
+ f'<div style="font-size: 11px; color: #6c757d; margin-bottom: 6px; line-height: 1.45;">'
574
+ f'Estimado na fronteira: <b>{fronteira}</b></div>'
575
+ '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">'
576
+ '<tr style="border-bottom: 1px solid #e9ecef;">'
577
+ '<th style="text-align: left; padding: 2px 6px; color: #6c757d;">Variável</th>'
578
+ '<th style="text-align: right; padding: 2px 6px; color: #6c757d;">Informado</th>'
579
+ '<th style="text-align: right; padding: 2px 6px; color: #6c757d;">Limite usado</th>'
580
+ '<th style="text-align: left; padding: 2px 6px; color: #6c757d;">Direção</th>'
581
+ '</tr>'
582
+ f'{tabela}'
583
+ '</table>'
584
+ )
585
+
586
+
587
  def formatar_avaliacao_html(avaliacoes_lista, indice_base=0, elem_id_excluir="excluir-aval-elab"):
588
  """Formata resultados de avaliação como tabela HTML acumulada.
589
 
 
653
  elif status in ("dicotomica", "codigo_alocado", "percentual"):
654
  celula = f'{val_fmt} \u2014'
655
  elif status == "warning":
656
+ seta = '\u2191' if ext.get("direcao") == "acima" else ('\u2193' if ext.get("direcao") == "abaixo" else '')
657
+ celula = f'{val_fmt} \u26a0\ufe0f{seta} {perc:.1f}%'
658
  elif ext.get("valor_invalido"):
659
  celula = f'{val_fmt} \u274c'
660
  else: # grave
661
+ seta = '\u2191' if ext.get("direcao") == "acima" else ('\u2193' if ext.get("direcao") == "abaixo" else '')
662
+ celula = f'{val_fmt} \u274c{seta} {perc:.1f}%'
663
 
664
  html += f'<td {_td_r}>{celula}</td>'
665
  html += '</tr>'
 
671
  html += f'<td {_td_bold_r} style="text-align: right; padding: 8px 12px; border-bottom: 1px solid #dee2e6; font-weight: 600;">{_formatar_brl(aval["estimado"])}</td>'
672
  html += '</tr>'
673
 
674
+ # Extrapoladas na fronteira (logo abaixo do estimado)
675
+ html += f'<tr><td {_td}>Extrapoladas na fronteira</td>'
676
+ for aval in avaliacoes_lista:
677
+ qtd_extrapolacoes = int(aval.get("qtd_extrapolacoes") or 0)
678
+ houve_extrapolacao = bool(aval.get("houve_extrapolacao")) or qtd_extrapolacoes > 0
679
+ valor_fronteira = aval.get("fronteira")
680
+ celula = _formatar_brl(valor_fronteira) if houve_extrapolacao else '\u2014'
681
+ popup = _popup_grau(_popup_fronteira_html(aval))
682
+ html += f'<td {_td_r}>{celula} {popup}</td>'
683
+ html += '</tr>'
684
+
685
  # Estimado / Base (só se houver mais de 1 avaliação e base selecionada)
686
  if n > 1 and indice_base_normalizado is not None:
687
  estimado_base = avaliacoes_lista[indice_base_normalizado]["estimado"]
backend/app/core/elaboracao/modelo.py CHANGED
@@ -363,7 +363,7 @@ def ajustar_modelo_callback(df, coluna_y, colunas_x, transformacao_y, outliers_a
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)
367
  except Exception as e:
368
  print(f"Erro ao gerar matriz de correlação transformada: {e}")
369
  fig_corr = None
 
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
backend/app/core/{dados/Bairros_LC12112_16.shp → pesquisa/modelos_dai/MOD_V_TER_GENERICO_2016_2026_001.dai} RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:1446d8cc7fdff70154b792fd1cf78b251d064ce38a58c8a81128702d2352967a
3
- size 5562340
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:33d53230f6ba56a80fb89c4ae4f092e08dc2476bd4ce6a06013c762ec73ca58a
3
+ size 19219547
backend/app/core/visualizacao/app.py CHANGED
@@ -269,7 +269,7 @@ def _criar_grafico_cook(modelos_sm):
269
  print(f"Erro ao criar gráfico de Cook: {e}")
270
  return None
271
 
272
- def _criar_grafico_correlacao(modelos_sm):
273
  """Gera heatmap de correlação com cores customizadas e diagonal limpa."""
274
  try:
275
  if modelos_sm is None or not hasattr(modelos_sm, 'model'):
@@ -279,7 +279,8 @@ def _criar_grafico_correlacao(modelos_sm):
279
  X = model.exog
280
  X_names = model.exog_names
281
  y = model.endog
282
- y_name = getattr(model, 'endog_names', 'Variável Dependente')
 
283
 
284
  df_X = pd.DataFrame(X, columns=X_names)
285
  df_X = df_X.drop(
@@ -456,6 +457,12 @@ def gerar_todos_graficos(pacote):
456
 
457
  obs_calc = pacote["modelo"]["obs_calc"]
458
  modelos_sm = pacote["modelo"]["sm"]
 
 
 
 
 
 
459
 
460
  # Identificar vetores
461
  y_obs = None
@@ -521,7 +528,7 @@ def gerar_todos_graficos(pacote):
521
 
522
  if modelos_sm is not None:
523
  graficos["cook"] = _criar_grafico_cook(modelos_sm)
524
- graficos["corr"] = _criar_grafico_correlacao(modelos_sm)
525
 
526
  return graficos
527
 
 
269
  print(f"Erro ao criar gráfico de Cook: {e}")
270
  return None
271
 
272
+ def _criar_grafico_correlacao(modelos_sm, nome_y: str | None = None):
273
  """Gera heatmap de correlação com cores customizadas e diagonal limpa."""
274
  try:
275
  if modelos_sm is None or not hasattr(modelos_sm, 'model'):
 
279
  X = model.exog
280
  X_names = model.exog_names
281
  y = model.endog
282
+ y_name_base = str(nome_y or "").strip() or str(getattr(model, 'endog_names', 'Variável Dependente') or '').strip() or 'Variável Dependente'
283
+ y_name = y_name_base if re.search(r"\(Y\)$", y_name_base, flags=re.IGNORECASE) else f"{y_name_base} (Y)"
284
 
285
  df_X = pd.DataFrame(X, columns=X_names)
286
  df_X = df_X.drop(
 
457
 
458
  obs_calc = pacote["modelo"]["obs_calc"]
459
  modelos_sm = pacote["modelo"]["sm"]
460
+ info_transf = pacote.get("transformacoes", {}).get("info") or []
461
+ nome_y = None
462
+ if isinstance(info_transf, list) and info_transf:
463
+ primeira_linha = str(info_transf[0] or "").strip()
464
+ if primeira_linha:
465
+ nome_y = primeira_linha.split(": ", 1)[0].strip() or None
466
 
467
  # Identificar vetores
468
  y_obs = None
 
528
 
529
  if modelos_sm is not None:
530
  graficos["cook"] = _criar_grafico_cook(modelos_sm)
531
+ graficos["corr"] = _criar_grafico_correlacao(modelos_sm, nome_y=nome_y)
532
 
533
  return graficos
534
 
backend/app/services/audit_log_service.py CHANGED
@@ -28,12 +28,23 @@ _HF_LOCK = Lock()
28
  _HF_ROOT_READY: dict[str, bool] = {}
29
 
30
 
 
 
 
 
 
 
 
 
31
  def _provider_hint() -> str:
32
- raw = (
33
- os.getenv("MODELOS_REPOSITORIO_PROVIDER")
34
- or os.getenv("PESQUISA_MODELOS_PROVIDER")
35
- or "local"
36
- )
 
 
 
37
  value = str(raw).strip().lower()
38
  if value in {"hf", "hf_dataset", "dataset", "huggingface"}:
39
  return "hf_dataset"
 
28
  _HF_ROOT_READY: dict[str, bool] = {}
29
 
30
 
31
+ def _is_hf_runtime() -> bool:
32
+ for key in ("SPACE_ID", "SPACE_AUTHOR_NAME", "HF_SPACE_ID"):
33
+ value = str(os.getenv(key) or "").strip()
34
+ if value:
35
+ return True
36
+ return False
37
+
38
+
39
  def _provider_hint() -> str:
40
+ if _is_hf_runtime():
41
+ return "hf_dataset"
42
+
43
+ raw = os.getenv("MODELOS_REPOSITORIO_PROVIDER")
44
+ if raw is None:
45
+ raw = os.getenv("PESQUISA_MODELOS_PROVIDER")
46
+ if raw is None:
47
+ raw = "local"
48
  value = str(raw).strip().lower()
49
  if value in {"hf", "hf_dataset", "dataset", "huggingface"}:
50
  return "hf_dataset"
backend/app/services/elaboracao_service.py CHANGED
@@ -693,6 +693,30 @@ def _calcular_periodo_dados_mercado(df: pd.DataFrame, coluna_data: str) -> dict[
693
  }
694
 
695
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
696
  def _resumo_faltantes_variaveis(df: pd.DataFrame, colunas: list[str]) -> dict[str, Any]:
697
  if df is None or not colunas:
698
  return {"has_missing": False, "rows_missing": 0, "total_rows": 0, "columns": [], "rows_preview": []}
@@ -1061,6 +1085,7 @@ def apply_selection(
1061
  df_filtrado = df_filtrado.drop(index=outliers, errors="ignore")
1062
 
1063
  session.df_filtrado = df_filtrado
 
1064
  session.coluna_y = coluna_y
1065
  session.colunas_x = colunas_x_validas
1066
 
@@ -1340,7 +1365,11 @@ def fit_model(
1340
  try:
1341
  df_corr = resultado["X_transformado"].copy()
1342
  df_corr[session.coluna_y] = resultado["y_transformado"]
1343
- fig_corr = charts.criar_matriz_correlacao(df_corr, [session.coluna_y] + list(session.colunas_x))
 
 
 
 
1344
  except Exception:
1345
  fig_corr = None
1346
 
@@ -2525,6 +2554,7 @@ def limpar_historico_outliers(session: SessionState) -> dict[str, Any]:
2525
  session.outliers_anteriores = []
2526
  session.iteracao = 1
2527
  session.df_filtrado = session.df_original.copy()
 
2528
  session.reset_modelo()
2529
 
2530
  return {
 
693
  }
694
 
695
 
696
+ def _atualizar_periodo_dados_mercado_filtrado(session: SessionState) -> None:
697
+ coluna_data = str(session.coluna_data_mercado or "").strip()
698
+ if not coluna_data:
699
+ session.periodo_dados_mercado_inicio = None
700
+ session.periodo_dados_mercado_fim = None
701
+ return
702
+
703
+ df = session.df_filtrado if session.df_filtrado is not None else session.df_original
704
+ if df is None or df.empty or coluna_data not in df.columns:
705
+ session.periodo_dados_mercado_inicio = None
706
+ session.periodo_dados_mercado_fim = None
707
+ return
708
+
709
+ try:
710
+ periodo = _calcular_periodo_dados_mercado(df, coluna_data)
711
+ except HTTPException:
712
+ session.periodo_dados_mercado_inicio = None
713
+ session.periodo_dados_mercado_fim = None
714
+ return
715
+
716
+ session.periodo_dados_mercado_inicio = periodo["data_inicial"]
717
+ session.periodo_dados_mercado_fim = periodo["data_final"]
718
+
719
+
720
  def _resumo_faltantes_variaveis(df: pd.DataFrame, colunas: list[str]) -> dict[str, Any]:
721
  if df is None or not colunas:
722
  return {"has_missing": False, "rows_missing": 0, "total_rows": 0, "columns": [], "rows_preview": []}
 
1085
  df_filtrado = df_filtrado.drop(index=outliers, errors="ignore")
1086
 
1087
  session.df_filtrado = df_filtrado
1088
+ _atualizar_periodo_dados_mercado_filtrado(session)
1089
  session.coluna_y = coluna_y
1090
  session.colunas_x = colunas_x_validas
1091
 
 
1365
  try:
1366
  df_corr = resultado["X_transformado"].copy()
1367
  df_corr[session.coluna_y] = resultado["y_transformado"]
1368
+ fig_corr = charts.criar_matriz_correlacao(
1369
+ df_corr,
1370
+ [session.coluna_y] + list(session.colunas_x),
1371
+ coluna_y=session.coluna_y,
1372
+ )
1373
  except Exception:
1374
  fig_corr = None
1375
 
 
2554
  session.outliers_anteriores = []
2555
  session.iteracao = 1
2556
  session.df_filtrado = session.df_original.copy()
2557
+ _atualizar_periodo_dados_mercado_filtrado(session)
2558
  session.reset_modelo()
2559
 
2560
  return {
backend/app/services/model_repository.py CHANGED
@@ -37,6 +37,15 @@ _STATE: dict[str, Any] = {
37
  }
38
 
39
 
 
 
 
 
 
 
 
 
 
40
  @dataclass(frozen=True)
41
  class ModelRepositoryResolution:
42
  provider: str
@@ -60,11 +69,14 @@ class ModelRepositoryResolution:
60
 
61
 
62
  def _provider() -> str:
63
- raw = (
64
- os.getenv("MODELOS_REPOSITORIO_PROVIDER")
65
- or os.getenv("PESQUISA_MODELOS_PROVIDER")
66
- or "local"
67
- )
 
 
 
68
  value = str(raw).strip().lower()
69
  if value in {"hf", "hf_dataset", "dataset", "huggingface"}:
70
  return "hf_dataset"
 
37
  }
38
 
39
 
40
+ def _is_hf_runtime() -> bool:
41
+ # HF Spaces define essas variaveis; em runtime HF queremos evitar fallback local.
42
+ for key in ("SPACE_ID", "SPACE_AUTHOR_NAME", "HF_SPACE_ID"):
43
+ value = str(os.getenv(key) or "").strip()
44
+ if value:
45
+ return True
46
+ return False
47
+
48
+
49
  @dataclass(frozen=True)
50
  class ModelRepositoryResolution:
51
  provider: str
 
69
 
70
 
71
  def _provider() -> str:
72
+ if _is_hf_runtime():
73
+ return "hf_dataset"
74
+
75
+ raw = os.getenv("MODELOS_REPOSITORIO_PROVIDER")
76
+ if raw is None:
77
+ raw = os.getenv("PESQUISA_MODELOS_PROVIDER")
78
+ if raw is None:
79
+ raw = "local"
80
  value = str(raw).strip().lower()
81
  if value in {"hf", "hf_dataset", "dataset", "huggingface"}:
82
  return "hf_dataset"
backend/app/services/pesquisa_service.py CHANGED
@@ -385,7 +385,7 @@ def listar_modelos(filtros: PesquisaFiltros, limite: int | None = None, somente_
385
  )
386
 
387
 
388
- def gerar_mapa_modelos(modelos_ids: list[str], limite_pontos_por_modelo: int = 4500) -> dict[str, Any]:
389
  ids = [str(item).strip() for item in (modelos_ids or []) if str(item).strip()]
390
  if not ids:
391
  raise HTTPException(status_code=400, detail="Selecione ao menos um modelo para gerar o mapa")
@@ -884,18 +884,20 @@ def _identificar_coluna_por_alias(colunas: Any, aliases: list[str]) -> str | Non
884
 
885
  def _extrair_bairros(df: pd.DataFrame) -> list[str]:
886
  candidatos = [col for col in df.columns if _has_alias(str(col), BAIRRO_ALIASES)]
887
- bairros: set[str] = set()
 
888
 
889
  for col in candidatos:
890
  serie = df[col]
891
- for valor in serie.dropna().head(5000):
892
  texto = str(valor).strip()
893
- if texto:
894
- bairros.add(texto)
895
- if len(bairros) >= 30:
896
- break
 
897
 
898
- return sorted(bairros)[:30]
899
 
900
 
901
  def _extrair_finalidades(df: pd.DataFrame) -> list[str]:
 
385
  )
386
 
387
 
388
+ def gerar_mapa_modelos(modelos_ids: list[str], limite_pontos_por_modelo: int = 0) -> dict[str, Any]:
389
  ids = [str(item).strip() for item in (modelos_ids or []) if str(item).strip()]
390
  if not ids:
391
  raise HTTPException(status_code=400, detail="Selecione ao menos um modelo para gerar o mapa")
 
884
 
885
  def _extrair_bairros(df: pd.DataFrame) -> list[str]:
886
  candidatos = [col for col in df.columns if _has_alias(str(col), BAIRRO_ALIASES)]
887
+ bairros: list[str] = []
888
+ vistos = set()
889
 
890
  for col in candidatos:
891
  serie = df[col]
892
+ for valor in serie.dropna():
893
  texto = str(valor).strip()
894
+ chave = _normalize(texto)
895
+ if not texto or not chave or chave in vistos:
896
+ continue
897
+ vistos.add(chave)
898
+ bairros.append(texto)
899
 
900
+ return sorted(bairros, key=lambda item: item.lower())
901
 
902
 
903
  def _extrair_finalidades(df: pd.DataFrame) -> list[str]:
frontend/src/components/AvaliacaoTab.jsx CHANGED
@@ -33,23 +33,256 @@ function formatarDataHoraIso(iso) {
33
  return data.toLocaleString('pt-BR')
34
  }
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  function classificarExtrapolacao(info) {
37
  const status = String(info?.status || 'ok')
38
  const percentual = Number(info?.percentual || 0)
 
 
39
  if (status === 'ok') return 'ok'
40
- if (status === 'warning') return `⚠ ${formatarNumero(percentual, 1)}%`
41
- if (status === 'grave') return ` ${formatarNumero(percentual, 1)}%`
42
  if (status === 'dicotomica' || status === 'codigo_alocado' || status === 'percentual') return '—'
43
  return status
44
  }
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  function corGrau(valor) {
47
- const texto = String(valor || '').toLowerCase()
48
- if (texto.includes('grau iii')) return '#1f7a40'
49
- if (texto.includes('grau ii')) return '#2d8dbf'
50
- if (texto.includes('grau i')) return '#c97400'
51
- if (texto.includes('sem enquadramento')) return '#b22f40'
52
- return '#48627a'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  }
54
 
55
  function escaparCsv(valor) {
@@ -96,6 +329,7 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
96
  const [avaliacoesCards, setAvaliacoesCards] = useState([])
97
  const [baseCardId, setBaseCardId] = useState(BASE_COMPARACAO_SEM_BASE)
98
  const [confirmarLimpezaAvaliacoes, setConfirmarLimpezaAvaliacoes] = useState(false)
 
99
 
100
  const uploadInputRef = useRef(null)
101
  const quickLoadHandledRef = useRef('')
@@ -458,6 +692,14 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
458
  setConfirmarLimpezaAvaliacoes(false)
459
  }
460
 
 
 
 
 
 
 
 
 
461
  function calcularComparacaoBase(avaliacao) {
462
  if (!baseCard || !baseCard.avaliacao) return '—'
463
  const baseEstimado = Number(baseCard.avaliacao.estimado)
@@ -492,6 +734,7 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
492
  'Precisao',
493
  'Fundamentacao',
494
  'QtdExtrapolacoes',
 
495
  ...variaveis.map((item) => `X_${item}`),
496
  ]
497
  const linhas = [cabecalho.join(';')]
@@ -514,6 +757,7 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
514
  String(aval.precisao || ''),
515
  String(aval.fundamentacao || ''),
516
  String(aval.qtd_extrapolacoes ?? ''),
 
517
  ]
518
  const camposVars = variaveis.map((variavel) => {
519
  const valor = aval?.valores_x?.[variavel]
@@ -763,6 +1007,20 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
763
 
764
  <div className="avaliacao-modelos-metrics">
765
  <div><strong>Estimado:</strong> {formatarMoeda(aval.estimado)}</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
766
  <div><strong>CA -15%:</strong> {formatarMoeda(aval.ca_inf)}</div>
767
  <div><strong>CA +15%:</strong> {formatarMoeda(aval.ca_sup)}</div>
768
  <div><strong>IC 80% Inf.:</strong> {formatarMoeda(aval.ic_inf)} ({`-${formatarNumero(aval.perc_inf, 1)}%`})</div>
@@ -772,11 +1030,33 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
772
  </div>
773
 
774
  <div className="avaliacao-modelos-graus">
775
- <span style={{ color: corGrau(aval.precisao) }}>
776
  <strong>Precisão:</strong> {String(aval.precisao || '-')}
 
 
 
 
 
 
 
 
 
 
 
777
  </span>
778
- <span style={{ color: corGrau(aval.fundamentacao) }}>
779
  <strong>Fundamentação:</strong> {String(aval.fundamentacao || '-')}
 
 
 
 
 
 
 
 
 
 
 
780
  </span>
781
  </div>
782
  </article>
@@ -791,6 +1071,9 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
791
  </div>
792
 
793
  <LoadingOverlay show={loading} label="Processando dados..." />
 
 
 
794
  {error ? <div className="error-line">{error}</div> : null}
795
  </div>
796
  )
 
33
  return data.toLocaleString('pt-BR')
34
  }
35
 
36
+ function formatarExtrapoladasNaFronteira(aval) {
37
+ const qtdExtrapolacoes = Number(aval?.qtd_extrapolacoes ?? 0)
38
+ const houveExtrapolacao = Boolean(aval?.houve_extrapolacao) || qtdExtrapolacoes > 0
39
+ if (!houveExtrapolacao) return '-'
40
+
41
+ const fronteira = Number(aval?.fronteira)
42
+ if (!Number.isFinite(fronteira)) return '-'
43
+ return formatarMoeda(fronteira)
44
+ }
45
+
46
+ function formatarExtrapoladasNaFronteiraCsv(aval) {
47
+ const qtdExtrapolacoes = Number(aval?.qtd_extrapolacoes ?? 0)
48
+ const houveExtrapolacao = Boolean(aval?.houve_extrapolacao) || qtdExtrapolacoes > 0
49
+ if (!houveExtrapolacao) return '-'
50
+
51
+ const fronteira = Number(aval?.fronteira)
52
+ if (!Number.isFinite(fronteira)) return '-'
53
+ return formatarNumero(fronteira, 2)
54
+ }
55
+
56
  function classificarExtrapolacao(info) {
57
  const status = String(info?.status || 'ok')
58
  const percentual = Number(info?.percentual || 0)
59
+ const direcao = String(info?.direcao || '')
60
+ const seta = direcao === 'acima' ? '↑' : (direcao === 'abaixo' ? '↓' : '')
61
  if (status === 'ok') return 'ok'
62
+ if (status === 'warning') return `⚠️${seta} ${formatarNumero(percentual, 1)}%`
63
+ if (status === 'grave') return `❌${seta} ${formatarNumero(percentual, 1)}%`
64
  if (status === 'dicotomica' || status === 'codigo_alocado' || status === 'percentual') return '—'
65
  return status
66
  }
67
 
68
+ function escaparHtml(value) {
69
+ return String(value ?? '')
70
+ .replaceAll('&', '&amp;')
71
+ .replaceAll('<', '&lt;')
72
+ .replaceAll('>', '&gt;')
73
+ .replaceAll('"', '&quot;')
74
+ .replaceAll("'", '&#39;')
75
+ }
76
+
77
+ const CORES_GRAU = {
78
+ 'Grau III': '#28a745',
79
+ 'Grau II': '#17a2b8',
80
+ 'Grau I': '#e67e00',
81
+ 'Sem enquadramento': '#dc3545',
82
+ }
83
+
84
+ function normalizarGrau(value) {
85
+ const texto = String(value || '').toLowerCase()
86
+ if (texto.includes('grau iii')) return 'Grau III'
87
+ if (texto.includes('grau ii')) return 'Grau II'
88
+ if (texto.includes('grau i')) return 'Grau I'
89
+ if (texto.includes('sem enquadramento') || texto.includes('sem enq')) return 'Sem enquadramento'
90
+ return String(value || '-')
91
+ }
92
+
93
  function corGrau(valor) {
94
+ const grau = normalizarGrau(valor)
95
+ return CORES_GRAU[grau] || '#495057'
96
+ }
97
+
98
+ function popupPrecisaoHtml(aval) {
99
+ const amplitude = Number(aval?.amplitude)
100
+ const amplitudeTexto = Number.isFinite(amplitude) ? `${formatarNumero(amplitude, 1)}%` : '—'
101
+ const grau = normalizarGrau(aval?.precisao)
102
+ const regras = [
103
+ ['≤30%', 'Grau III', '#28a745'],
104
+ ['≤40%', 'Grau II', '#17a2b8'],
105
+ ['≤50%', 'Grau I', '#e67e00'],
106
+ ['>50%', 'Sem enquadramento', '#dc3545'],
107
+ ]
108
+
109
+ const linhas = regras.map(([faixa, nome, cor]) => {
110
+ const ativo = nome === grau
111
+ const bg = ativo ? `background: ${cor}11;` : ''
112
+ const fw = ativo ? 'font-weight: 600;' : ''
113
+ const marca = ativo ? ' ◀' : ''
114
+ return (
115
+ `<tr style="border-bottom: 1px solid #f0f0f0; ${bg}">`
116
+ + `<td style="padding: 2px 6px; ${fw}">${faixa}</td>`
117
+ + `<td style="padding: 2px 6px; color: ${cor}; ${fw}">${nome}${marca}</td>`
118
+ + '</tr>'
119
+ )
120
+ }).join('')
121
+
122
+ return (
123
+ '<div style="font-weight: 600; margin-bottom: 4px; color: #495057;">'
124
+ + 'Precisão — NBR 14.653-2, Tabela 2'
125
+ + '<span style="font-weight: 400; font-size: 11px; color: #6c757d; margin-left: 8px;">'
126
+ + 'Amplitude do IC 80% em relação ao estimado.</span></div>'
127
+ + '<div style="display: flex; align-items: baseline; gap: 12px; margin-bottom: 6px;">'
128
+ + `<span>Amplitude do IC 80%: <b>${amplitudeTexto}</b></span>`
129
+ + '</div>'
130
+ + '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">'
131
+ + '<tr style="border-bottom: 1px solid #e9ecef;">'
132
+ + '<th style="text-align: left; padding: 2px 6px; color: #6c757d;">Amplitude</th>'
133
+ + '<th style="text-align: left; padding: 2px 6px; color: #6c757d;">Grau</th>'
134
+ + '</tr>'
135
+ + linhas
136
+ + '</table>'
137
+ )
138
+ }
139
+
140
+ function popupFundamentacaoHtml(aval) {
141
+ const grau = normalizarGrau(aval?.fundamentacao)
142
+ const corGrauAtual = CORES_GRAU[grau] || '#495057'
143
+ const qtdExtrapolacoes = Number(aval?.qtd_extrapolacoes ?? 0)
144
+ const houveExtrapolacao = Boolean(aval?.houve_extrapolacao) || qtdExtrapolacoes > 0
145
+ const percExt = Number(aval?.perc_ext)
146
+ const percExtNum = Number.isFinite(percExt) ? percExt : 0
147
+ const header = (
148
+ '<div style="font-weight: 600; margin-bottom: 4px; color: #495057;">'
149
+ + 'Fundamentação — NBR 14.653-2, Tabela 1</div>'
150
+ + '<div style="font-size: 11px; color: #6c757d; margin-bottom: 6px; line-height: 1.45;">'
151
+ + 'Depende de quantas variáveis extrapolam os limites amostrais e do '
152
+ + '<b style="color:#333;">impacto no valor estimado</b>. '
153
+ + 'O impacto é calculado aplicando o modelo duas vezes: uma com os valores informados '
154
+ + '(incluindo os extrapolados) e outra simulando as variáveis extrapoladas no seu limite '
155
+ + 'amostral mais próximo (mínimo ou máximo, conforme o caso). A diferença percentual entre '
156
+ + 'os dois valores unitários resultantes é o impacto no estimado. '
157
+ + 'É esse impacto — e não a % de extrapolação individual de cada variável — que define o grau, '
158
+ + 'exceto no caso de extrapolação grave'
159
+ + ' (variável >100% acima do máximo amostral, >50% abaixo do mínimo amostral, ou valor '
160
+ + 'inválido em dicotômica), que resulta automaticamente em Sem enquadramento.'
161
+ + '</div>'
162
+ )
163
+
164
+ if (!houveExtrapolacao) {
165
+ return (
166
+ header
167
+ + `<div>Nenhuma variável extrapolou os limites amostrais. <span style="color: #28a745; font-weight: 600;">→ ${grau}</span></div>`
168
+ )
169
+ }
170
+
171
+ const extrapolacoes = aval?.extrapolacoes || {}
172
+ const linhasVars = Object.entries(extrapolacoes)
173
+ .filter(([, info]) => {
174
+ const status = String(info?.status || '')
175
+ return status === 'warning' || status === 'grave'
176
+ })
177
+ .map(([variavel, info]) => {
178
+ const status = String(info?.status || '')
179
+ const percentual = Number(info?.percentual)
180
+ const percentualTexto = Number.isFinite(percentual) ? `${formatarNumero(percentual, 1)}%` : '—'
181
+ const direcao = String(info?.direcao || '')
182
+ const icone = status === 'grave' ? '❌' : '⚠️'
183
+ if (direcao === 'acima') return `${icone} <b>${escaparHtml(variavel)}</b>: ${percentualTexto} acima do máx.`
184
+ if (direcao === 'abaixo') return `${icone} <b>${escaparHtml(variavel)}</b>: ${percentualTexto} abaixo do mín.`
185
+ if (info?.valor_invalido) return `${icone} <b>${escaparHtml(variavel)}</b>: valor inválido`
186
+ return `${icone} <b>${escaparHtml(variavel)}</b>: ${percentualTexto} fora`
187
+ })
188
+
189
+ const temGrave = Object.values(extrapolacoes).some((info) => String(info?.status || '') === 'grave')
190
+ let motivo = ''
191
+ if (temGrave) {
192
+ motivo = 'Extrapolação grave (>100% do máx., >50% abaixo do mín., ou dicotômica inválida).'
193
+ } else if (percExtNum > 20) {
194
+ motivo = `Impacto de ${formatarNumero(percExtNum, 1)}% no estimado, acima do limite de 20%.`
195
+ } else if (qtdExtrapolacoes >= 2) {
196
+ motivo = `${qtdExtrapolacoes} variáveis extrapoladas, impacto de ${formatarNumero(percExtNum, 1)}% no estimado.`
197
+ } else if (qtdExtrapolacoes === 1 && percExtNum > 15) {
198
+ motivo = `1 variável extrapolada, impacto de ${formatarNumero(percExtNum, 1)}% no estimado (>15%).`
199
+ } else {
200
+ motivo = `1 variável extrapolada, impacto de ${formatarNumero(percExtNum, 1)}% no estimado (≤15%).`
201
+ }
202
+
203
+ const regras = [
204
+ ['Nenhuma extrapolação', 'Grau III'],
205
+ ['1 variável extrapolada, impacto ≤15% no estimado', 'Grau II'],
206
+ ['1 variável extrapolada c/ impacto >15% e ≤20%, ou >1 variável extrapolada c/ impacto ≤20%', 'Grau I'],
207
+ ['Impacto >20% no estimado, ou extrapolação grave', 'Sem enq.'],
208
+ ]
209
+ const tabelaRegras = regras.map(([condicao, nome]) => {
210
+ const ativo = nome === grau || (nome === 'Sem enq.' && grau === 'Sem enquadramento')
211
+ const bg = ativo ? `background: ${corGrauAtual}11;` : ''
212
+ const fw = ativo ? 'font-weight: 600;' : ''
213
+ const marca = ativo ? ' ◀' : ''
214
+ return (
215
+ `<tr style="border-bottom: 1px solid #f0f0f0; ${bg}">`
216
+ + `<td style="padding: 2px 6px; ${fw}">${condicao}</td>`
217
+ + `<td style="padding: 2px 6px; ${fw}">${nome}${marca}</td>`
218
+ + '</tr>'
219
+ )
220
+ }).join('')
221
+
222
+ const resumo = `${qtdExtrapolacoes} variável(is) extrapolada(s), impacto de <b>${formatarNumero(percExtNum, 1)}%</b> no estimado.`
223
+ const varsStr = linhasVars.length ? linhasVars.join(' &nbsp;│&nbsp; ') : '-'
224
+
225
+ return (
226
+ header
227
+ + `<div style="margin-bottom: 3px;">${resumo}</div>`
228
+ + `<div style="background: #f8f9fa; border-radius: 4px; padding: 4px 8px; margin-bottom: 4px; font-size: 11px;">${varsStr}</div>`
229
+ + `<div style="margin-bottom: 4px; font-size: 11px; color: #495057;">${motivo} <span style="color: ${corGrauAtual}; font-weight: 600;">→ ${grau}</span></div>`
230
+ + '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">'
231
+ + '<tr style="border-bottom: 1px solid #e9ecef;">'
232
+ + '<th style="text-align: left; padding: 2px 6px; color: #6c757d;">Condição</th>'
233
+ + '<th style="text-align: left; padding: 2px 6px; color: #6c757d;">Grau</th>'
234
+ + '</tr>'
235
+ + tabelaRegras
236
+ + '</table>'
237
+ )
238
+ }
239
+
240
+ function popupFronteiraHtml(aval) {
241
+ const qtdExtrapolacoes = Number(aval?.qtd_extrapolacoes ?? 0)
242
+ const houveExtrapolacao = Boolean(aval?.houve_extrapolacao) || qtdExtrapolacoes > 0
243
+ if (!houveExtrapolacao) {
244
+ return (
245
+ '<div style="font-weight: 600; margin-bottom: 4px; color: #495057;">Extrapoladas na fronteira</div>'
246
+ + '<div style="font-size: 11px; color: #6c757d; line-height: 1.45;">Nenhuma variável extrapolou os limites amostrais nesta avaliação.</div>'
247
+ )
248
+ }
249
+
250
+ const itens = Object.entries(aval?.extrapolacoes || {})
251
+ .filter(([, info]) => {
252
+ const status = String(info?.status || '')
253
+ return status === 'warning' || status === 'grave'
254
+ })
255
+ .map(([variavel, info]) => {
256
+ const valorInformado = Number(info?.valor_informado)
257
+ const limiteUtilizado = Number(info?.limite_utilizado)
258
+ const valorInformadoTxt = Number.isFinite(valorInformado) ? formatarNumero(valorInformado, 2) : '—'
259
+ const limiteTxt = Number.isFinite(limiteUtilizado) ? formatarNumero(limiteUtilizado, 2) : '—'
260
+ const direcao = String(info?.direcao || '')
261
+ const direcaoTxt = direcao === 'acima' ? 'acima do máx.' : (direcao === 'abaixo' ? 'abaixo do mín.' : 'fora')
262
+ return (
263
+ '<tr style="border-bottom: 1px solid #f0f0f0;">'
264
+ + `<td style="padding: 2px 6px;"><b>${escaparHtml(variavel)}</b></td>`
265
+ + `<td style="padding: 2px 6px; text-align: right;">${valorInformadoTxt}</td>`
266
+ + `<td style="padding: 2px 6px; text-align: right;">${limiteTxt}</td>`
267
+ + `<td style="padding: 2px 6px;">${direcaoTxt}</td>`
268
+ + '</tr>'
269
+ )
270
+ }).join('')
271
+
272
+ const fronteira = formatarExtrapoladasNaFronteira(aval)
273
+ return (
274
+ '<div style="font-weight: 600; margin-bottom: 4px; color: #495057;">Extrapoladas na fronteira</div>'
275
+ + `<div style="font-size: 11px; color: #6c757d; margin-bottom: 6px; line-height: 1.45;">Estimado na fronteira: <b>${fronteira}</b></div>`
276
+ + '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">'
277
+ + '<tr style="border-bottom: 1px solid #e9ecef;">'
278
+ + '<th style="text-align: left; padding: 2px 6px; color: #6c757d;">Variável</th>'
279
+ + '<th style="text-align: right; padding: 2px 6px; color: #6c757d;">Informado</th>'
280
+ + '<th style="text-align: right; padding: 2px 6px; color: #6c757d;">Limite usado</th>'
281
+ + '<th style="text-align: left; padding: 2px 6px; color: #6c757d;">Direção</th>'
282
+ + '</tr>'
283
+ + (itens || '<tr><td colspan="4" style="padding: 4px 6px;">Sem detalhes.</td></tr>')
284
+ + '</table>'
285
+ )
286
  }
287
 
288
  function escaparCsv(valor) {
 
329
  const [avaliacoesCards, setAvaliacoesCards] = useState([])
330
  const [baseCardId, setBaseCardId] = useState(BASE_COMPARACAO_SEM_BASE)
331
  const [confirmarLimpezaAvaliacoes, setConfirmarLimpezaAvaliacoes] = useState(false)
332
+ const [avaliacaoPopupHtml, setAvaliacaoPopupHtml] = useState('')
333
 
334
  const uploadInputRef = useRef(null)
335
  const quickLoadHandledRef = useRef('')
 
692
  setConfirmarLimpezaAvaliacoes(false)
693
  }
694
 
695
+ function onPopupEnter(html) {
696
+ setAvaliacaoPopupHtml(String(html || ''))
697
+ }
698
+
699
+ function onPopupLeave() {
700
+ setAvaliacaoPopupHtml('')
701
+ }
702
+
703
  function calcularComparacaoBase(avaliacao) {
704
  if (!baseCard || !baseCard.avaliacao) return '—'
705
  const baseEstimado = Number(baseCard.avaliacao.estimado)
 
734
  'Precisao',
735
  'Fundamentacao',
736
  'QtdExtrapolacoes',
737
+ 'ExtrapoladasNaFronteira',
738
  ...variaveis.map((item) => `X_${item}`),
739
  ]
740
  const linhas = [cabecalho.join(';')]
 
757
  String(aval.precisao || ''),
758
  String(aval.fundamentacao || ''),
759
  String(aval.qtd_extrapolacoes ?? ''),
760
+ formatarExtrapoladasNaFronteiraCsv(aval),
761
  ]
762
  const camposVars = variaveis.map((variavel) => {
763
  const valor = aval?.valores_x?.[variavel]
 
1007
 
1008
  <div className="avaliacao-modelos-metrics">
1009
  <div><strong>Estimado:</strong> {formatarMoeda(aval.estimado)}</div>
1010
+ <div>
1011
+ <strong>Extrapoladas na fronteira:</strong> {formatarExtrapoladasNaFronteira(aval)}{' '}
1012
+ <button
1013
+ type="button"
1014
+ className="avaliacao-popup-trigger"
1015
+ aria-label="Detalhes de extrapoladas na fronteira"
1016
+ onMouseEnter={() => onPopupEnter(popupFronteiraHtml(aval))}
1017
+ onMouseLeave={onPopupLeave}
1018
+ onFocus={() => onPopupEnter(popupFronteiraHtml(aval))}
1019
+ onBlur={onPopupLeave}
1020
+ >
1021
+
1022
+ </button>
1023
+ </div>
1024
  <div><strong>CA -15%:</strong> {formatarMoeda(aval.ca_inf)}</div>
1025
  <div><strong>CA +15%:</strong> {formatarMoeda(aval.ca_sup)}</div>
1026
  <div><strong>IC 80% Inf.:</strong> {formatarMoeda(aval.ic_inf)} ({`-${formatarNumero(aval.perc_inf, 1)}%`})</div>
 
1030
  </div>
1031
 
1032
  <div className="avaliacao-modelos-graus">
1033
+ <span className="avaliacao-grau-item" style={{ color: corGrau(aval.precisao) }}>
1034
  <strong>Precisão:</strong> {String(aval.precisao || '-')}
1035
+ <button
1036
+ type="button"
1037
+ className="avaliacao-popup-trigger"
1038
+ aria-label="Detalhes do enquadramento de precisão"
1039
+ onMouseEnter={() => onPopupEnter(popupPrecisaoHtml(aval))}
1040
+ onMouseLeave={onPopupLeave}
1041
+ onFocus={() => onPopupEnter(popupPrecisaoHtml(aval))}
1042
+ onBlur={onPopupLeave}
1043
+ >
1044
+
1045
+ </button>
1046
  </span>
1047
+ <span className="avaliacao-grau-item" style={{ color: corGrau(aval.fundamentacao) }}>
1048
  <strong>Fundamentação:</strong> {String(aval.fundamentacao || '-')}
1049
+ <button
1050
+ type="button"
1051
+ className="avaliacao-popup-trigger"
1052
+ aria-label="Detalhes do enquadramento de fundamentação"
1053
+ onMouseEnter={() => onPopupEnter(popupFundamentacaoHtml(aval))}
1054
+ onMouseLeave={onPopupLeave}
1055
+ onFocus={() => onPopupEnter(popupFundamentacaoHtml(aval))}
1056
+ onBlur={onPopupLeave}
1057
+ >
1058
+
1059
+ </button>
1060
  </span>
1061
  </div>
1062
  </article>
 
1071
  </div>
1072
 
1073
  <LoadingOverlay show={loading} label="Processando dados..." />
1074
+ {avaliacaoPopupHtml ? (
1075
+ <div className="avaliacao-popup-overlay" dangerouslySetInnerHTML={{ __html: avaliacaoPopupHtml }} />
1076
+ ) : null}
1077
  {error ? <div className="error-line">{error}</div> : null}
1078
  </div>
1079
  )
frontend/src/components/ElaboracaoTab.jsx CHANGED
@@ -1843,6 +1843,29 @@ export default function ElaboracaoTab({ sessionId }) {
1843
  }
1844
  }
1845
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1846
  function applyBaseResponse(resp, options = {}) {
1847
  const resetXSelection = Boolean(options.resetXSelection)
1848
  const colunaYPadrao = String(resp.coluna_y_padrao || '')
@@ -1892,6 +1915,7 @@ export default function ElaboracaoTab({ sessionId }) {
1892
  }))
1893
  setOutliersAnteriores(resp.contexto.outliers_anteriores || [])
1894
  setIteracao(resp.contexto.iteracao || 1)
 
1895
  } else if (resetXSelection) {
1896
  setColunaY('')
1897
  setColunaYDraft(colunaYPadrao)
@@ -2003,6 +2027,9 @@ export default function ElaboracaoTab({ sessionId }) {
2003
  if (typeof resp.outliers_html !== 'undefined') {
2004
  setOutliersHtml(resp.outliers_html || '')
2005
  }
 
 
 
2006
 
2007
  if (resp.mapa_html) {
2008
  setMapaHtml(resp.mapa_html)
@@ -2866,6 +2893,7 @@ export default function ElaboracaoTab({ sessionId }) {
2866
  setOutliersHtml(resp.outliers_html || '')
2867
  setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
2868
  setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
 
2869
  })
2870
  }
2871
 
 
1843
  }
1844
  }
1845
 
1846
+ function syncPeriodoDataMercadoFromContext(contexto) {
1847
+ if (!contexto || typeof contexto !== 'object') return
1848
+
1849
+ if (Object.prototype.hasOwnProperty.call(contexto, 'coluna_data_mercado')) {
1850
+ const coluna = String(contexto.coluna_data_mercado || '')
1851
+ setColunaDataMercado(coluna)
1852
+ setColunaDataMercadoAplicada(coluna)
1853
+ }
1854
+
1855
+ if (Object.prototype.hasOwnProperty.call(contexto, 'periodo_dados_mercado')) {
1856
+ const periodo = normalizePeriodoDadosMercado(contexto.periodo_dados_mercado)
1857
+ setPeriodoDadosMercado(periodo)
1858
+ setPeriodoDadosMercadoPreview(periodo)
1859
+ setModeloCarregadoInfo((prev) => {
1860
+ if (!prev) return prev
1861
+ return {
1862
+ ...prev,
1863
+ periodo_dados_mercado: periodo,
1864
+ }
1865
+ })
1866
+ }
1867
+ }
1868
+
1869
  function applyBaseResponse(resp, options = {}) {
1870
  const resetXSelection = Boolean(options.resetXSelection)
1871
  const colunaYPadrao = String(resp.coluna_y_padrao || '')
 
1915
  }))
1916
  setOutliersAnteriores(resp.contexto.outliers_anteriores || [])
1917
  setIteracao(resp.contexto.iteracao || 1)
1918
+ syncPeriodoDataMercadoFromContext(resp.contexto)
1919
  } else if (resetXSelection) {
1920
  setColunaY('')
1921
  setColunaYDraft(colunaYPadrao)
 
2027
  if (typeof resp.outliers_html !== 'undefined') {
2028
  setOutliersHtml(resp.outliers_html || '')
2029
  }
2030
+ if (resp.contexto) {
2031
+ syncPeriodoDataMercadoFromContext(resp.contexto)
2032
+ }
2033
 
2034
  if (resp.mapa_html) {
2035
  setMapaHtml(resp.mapa_html)
 
2893
  setOutliersHtml(resp.outliers_html || '')
2894
  setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
2895
  setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
2896
+ syncPeriodoDataMercadoFromContext(resp.contexto)
2897
  })
2898
  }
2899
 
frontend/src/styles.css CHANGED
@@ -2402,6 +2402,7 @@ button.pesquisa-coluna-remove:hover {
2402
  padding: 10px 11px;
2403
  display: grid;
2404
  gap: 8px;
 
2405
  }
2406
 
2407
  .avaliacao-modelos-card-head {
@@ -2409,11 +2410,14 @@ button.pesquisa-coluna-remove:hover {
2409
  align-items: flex-start;
2410
  justify-content: space-between;
2411
  gap: 8px;
 
2412
  }
2413
 
2414
  .avaliacao-modelos-card-title {
2415
  display: grid;
2416
  gap: 1px;
 
 
2417
  }
2418
 
2419
  .avaliacao-modelos-card-title strong {
@@ -2427,6 +2431,9 @@ button.pesquisa-coluna-remove:hover {
2427
  font-size: 0.79rem;
2428
  font-weight: 600;
2429
  line-height: 1.2;
 
 
 
2430
  }
2431
 
2432
  .avaliacao-modelos-card-subtitle {
@@ -2437,6 +2444,7 @@ button.pesquisa-coluna-remove:hover {
2437
  .avaliacao-modelos-card-actions {
2438
  display: inline-flex;
2439
  align-items: center;
 
2440
  }
2441
 
2442
  .avaliacao-modelos-delete-btn {
@@ -2491,11 +2499,17 @@ button.pesquisa-coluna-remove:hover {
2491
  .avaliacao-modelos-vars-item span:first-child {
2492
  color: #526a80;
2493
  font-weight: 700;
 
 
 
2494
  }
2495
 
2496
  .avaliacao-modelos-vars-item span:last-child {
2497
  color: #2e475d;
2498
  text-align: right;
 
 
 
2499
  }
2500
 
2501
  .avaliacao-modelos-metrics {
@@ -2513,6 +2527,48 @@ button.pesquisa-coluna-remove:hover {
2513
  font-size: 0.79rem;
2514
  }
2515
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2516
  label {
2517
  font-weight: 700;
2518
  color: #394a5e;
 
2402
  padding: 10px 11px;
2403
  display: grid;
2404
  gap: 8px;
2405
+ min-width: 0;
2406
  }
2407
 
2408
  .avaliacao-modelos-card-head {
 
2410
  align-items: flex-start;
2411
  justify-content: space-between;
2412
  gap: 8px;
2413
+ min-width: 0;
2414
  }
2415
 
2416
  .avaliacao-modelos-card-title {
2417
  display: grid;
2418
  gap: 1px;
2419
+ min-width: 0;
2420
+ flex: 1 1 auto;
2421
  }
2422
 
2423
  .avaliacao-modelos-card-title strong {
 
2431
  font-size: 0.79rem;
2432
  font-weight: 600;
2433
  line-height: 1.2;
2434
+ min-width: 0;
2435
+ overflow-wrap: anywhere;
2436
+ word-break: break-word;
2437
  }
2438
 
2439
  .avaliacao-modelos-card-subtitle {
 
2444
  .avaliacao-modelos-card-actions {
2445
  display: inline-flex;
2446
  align-items: center;
2447
+ flex: 0 0 auto;
2448
  }
2449
 
2450
  .avaliacao-modelos-delete-btn {
 
2499
  .avaliacao-modelos-vars-item span:first-child {
2500
  color: #526a80;
2501
  font-weight: 700;
2502
+ min-width: 0;
2503
+ overflow-wrap: anywhere;
2504
+ word-break: break-word;
2505
  }
2506
 
2507
  .avaliacao-modelos-vars-item span:last-child {
2508
  color: #2e475d;
2509
  text-align: right;
2510
+ min-width: 0;
2511
+ overflow-wrap: anywhere;
2512
+ word-break: break-word;
2513
  }
2514
 
2515
  .avaliacao-modelos-metrics {
 
2527
  font-size: 0.79rem;
2528
  }
2529
 
2530
+ .avaliacao-grau-item {
2531
+ display: inline-flex;
2532
+ align-items: center;
2533
+ gap: 6px;
2534
+ min-width: 0;
2535
+ flex-wrap: wrap;
2536
+ }
2537
+
2538
+ .avaliacao-popup-trigger {
2539
+ all: unset;
2540
+ display: inline-flex;
2541
+ align-items: center;
2542
+ justify-content: center;
2543
+ cursor: help;
2544
+ font-size: 0.85em;
2545
+ line-height: 1;
2546
+ opacity: 0.7;
2547
+ }
2548
+
2549
+ .avaliacao-popup-overlay {
2550
+ position: fixed;
2551
+ left: 50%;
2552
+ top: 50%;
2553
+ transform: translate(-50%, -50%);
2554
+ z-index: 3600;
2555
+ width: min(520px, calc(100vw - 24px));
2556
+ background: #fff;
2557
+ border: 1px solid #dee2e6;
2558
+ border-radius: 8px;
2559
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
2560
+ padding: 10px 14px;
2561
+ font-size: 12px;
2562
+ font-weight: 400;
2563
+ color: #333;
2564
+ text-align: left;
2565
+ line-height: 1.4;
2566
+ white-space: normal;
2567
+ max-height: min(78vh, 680px);
2568
+ overflow: auto;
2569
+ pointer-events: none;
2570
+ }
2571
+
2572
  label {
2573
  font-weight: 700;
2574
  color: #394a5e;
resposta_avaliacao.html ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="pt-BR">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Versao consolidada - Avaliacao</title>
7
+ <style>
8
+ :root {
9
+ color-scheme: light;
10
+ }
11
+
12
+ body {
13
+ margin: 0;
14
+ background: #f5f6f8;
15
+ color: #1f2933;
16
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
17
+ line-height: 1.6;
18
+ }
19
+
20
+ main {
21
+ max-width: 980px;
22
+ margin: 32px auto;
23
+ background: #ffffff;
24
+ border: 1px solid #d9e2ec;
25
+ border-radius: 10px;
26
+ padding: 28px 34px;
27
+ box-shadow: 0 4px 14px rgba(15, 23, 42, 0.06);
28
+ }
29
+
30
+ h2 {
31
+ font-size: 1.28rem;
32
+ margin: 28px 0 12px;
33
+ line-height: 1.35;
34
+ }
35
+
36
+ p {
37
+ margin: 0 0 12px;
38
+ }
39
+
40
+ ol {
41
+ margin: 8px 0 16px;
42
+ padding-left: 24px;
43
+ }
44
+
45
+ li {
46
+ margin: 6px 0;
47
+ }
48
+ </style>
49
+ </head>
50
+ <body>
51
+ <main>
52
+ <p><strong>Versão consolidada (técnica + didática para leigos)</strong></p>
53
+
54
+ <p>A ferramenta foi desenhada para apoiar o avaliador em três decisões centrais:<br />
55
+ 1. reaproveitar modelo existente quando possível,<br />
56
+ 2. avaliar com segurança e comparabilidade,<br />
57
+ 3. criar novo modelo só quando realmente necessário.</p>
58
+
59
+ <h2>1. As 4 abas e a função de cada uma</h2>
60
+ <ol>
61
+ <li><strong>Pesquisa/Visualização</strong><br />
62
+ Ponto de partida do trabalho. O avaliador procura modelos já prontos, verifica se cobrem o caso e testa rapidamente antes de decidir reutilizar ou elaborar novo.</li>
63
+ <li><strong>Elaboração/Edição</strong><br />
64
+ Ambiente de construção/refino de modelo. Vai da base bruta até o modelo final validado.</li>
65
+ <li><strong>Avaliação de Imóveis</strong><br />
66
+ Ambiente operacional para simular cenários, comparar resultados e exportar avaliação.</li>
67
+ <li><strong>Repositório de Modelos</strong><br />
68
+ Acervo central dos modelos oficiais, com governança de inclusão/substituição/exclusão.</li>
69
+ </ol>
70
+
71
+ <h2>2. Fluxo real do avaliador (como acontece na prática)</h2>
72
+ <ol>
73
+ <li>Começa em <strong>Pesquisa/Visualização</strong> para ver se já existe modelo aderente.</li>
74
+ <li>Filtra, abre os melhores, verifica cobertura e qualidade.</li>
75
+ <li>Testa no avaliador para ver comportamento do resultado.</li>
76
+ <li>Se não ficar satisfatório, vai para <strong>Elaboração/Edição</strong>.</li>
77
+ <li>Depois de validado, publica no <strong>Repositório</strong> para reuso da equipe.</li>
78
+ </ol>
79
+
80
+ <h2>3. Pesquisa: o que os campos significam e como o app ajuda</h2>
81
+ <p>Os filtros são cumulativos: o avaliador informa o perfil do imóvel e o sistema cruza com os modelos.</p>
82
+
83
+ <ol>
84
+ <li><strong>Nome do modelo</strong>: busca textual por identificação.</li>
85
+ <li><strong>Tipo do modelo (aluguel/venda)</strong>: contexto de mercado do modelo.</li>
86
+ <li><strong>Finalidade genérica</strong>: tipologia macro do modelo.</li>
87
+ <li><strong>Finalidades cadastrais que devem estar contidas no modelo</strong>: aderência fina ao tipo de imóvel.</li>
88
+ <li><strong>Área do imóvel</strong>: compatibilidade com faixa de área do modelo.</li>
89
+ <li><strong>RH do imóvel</strong>: compatibilidade com faixa de RH.</li>
90
+ <li><strong>Zonas de avaliação</strong>: aderência por zona.</li>
91
+ <li><strong>Bairros</strong>: aderência territorial detalhada.</li>
92
+ <li><strong>Período mínimo desejado</strong>: compatibilidade temporal da base do modelo.</li>
93
+ </ol>
94
+
95
+ <p>O que o app faz automaticamente aqui (e seria manual):</p>
96
+ <ol>
97
+ <li>sugere preenchimentos,</li>
98
+ <li>cruza o imóvel com regras/faixas dos modelos,</li>
99
+ <li>retorna “modelos aceitos”,</li>
100
+ <li>resume metadados relevantes em cada card,</li>
101
+ <li>permite comparar cobertura espacial no mapa com poucos cliques.</li>
102
+ </ol>
103
+
104
+ <h2>4. Elaboração/Edição: seção por seção (com explicação integrada)</h2>
105
+ <ol>
106
+ <li><strong>Importar Dados</strong><br />
107
+ Recebe Excel/CSV ou <code>.dai</code>.<br />
108
+ O app detecta tipo de arquivo, abas e recupera contexto quando vem de modelo existente.</li>
109
+
110
+ <li><strong>Resolver Coordenadas</strong><br />
111
+ “Coordenada” é o ponto exato no mapa (latitude e longitude). “Geocodificar” significa transformar endereço (rua + número) em esse ponto geográfico automaticamente. O app faz isso sozinho para toda a base, mostra quais linhas falharam, permite corrigir e rodar de novo sem perder o trabalho.</li>
112
+
113
+ <li><strong>Visualizar Mapa</strong><br />
114
+ Mostra no mapa, de forma visual, onde estão os imóveis da base. Em vez de ler só tabela, o avaliador enxerga concentração, vazios e diferenças por região. O app permite trocar a forma de visualização: pontos (cada imóvel), mapa de calor (onde há mais concentração) e superfície contínua (tendência espacial).</li>
115
+
116
+ <li><strong>Visualizar Dados de Mercado</strong><br />
117
+ Exibe os dados carregados em tabela para conferência. “Exclusões anteriores” são registros que já foram retirados em etapas anteriores por estarem muito fora do padrão (outliers), e o app deixa isso visível para manter rastreabilidade do que foi removido.</li>
118
+
119
+ <li><strong>Definir Data dos Dados de Mercado</strong><br />
120
+ O app identifica nas colunas do Excel quais parecem ser data, sugere a melhor opção e calcula automaticamente o período da base (data inicial até data final). Isso ajuda o avaliador a julgar se os dados são atuais o suficiente para o caso e, portanto, se o modelo ainda está válido.</li>
121
+
122
+ <li><strong>Selecionar Variável Dependente (Y)</strong><br />
123
+ Define o que o modelo vai estimar (o alvo principal).</li>
124
+
125
+ <li><strong>Selecionar Variáveis Independentes (X)</strong><br />
126
+ Variáveis “explicativas” são as características que ajudam a explicar o preço do imóvel (ex.: área, localização, padrão, etc.). O app ajuda classificando o tipo de variável (binária 0/1, percentual, código), reduzindo trabalho manual e erro de configuração.</li>
127
+
128
+ <li><strong>Estatísticas das Variáveis Selecionadas</strong><br />
129
+ Gera resumo automático (mínimo, máximo, médias, dispersão) das variáveis escolhidas. Na prática, isso ajuda a verificar se essas variáveis realmente têm comportamento consistente para explicar o valor dos imóveis, em vez de incluir informação fraca ou distorcida.</li>
130
+
131
+ <li><strong>Teste de Micronumerosidade</strong><br />
132
+ Verifica se existe quantidade suficiente de dados para sustentar o modelo com segurança, inclusive dentro de combinações relevantes das variáveis. Em linguagem simples: evita tirar conclusão forte com amostra pequena demais.</li>
133
+
134
+ <li><strong>Gráficos de Dispersão das Variáveis Independentes</strong><br />
135
+ Mostra visualmente como cada variável se relaciona com o valor-alvo, antes do ajuste final.</li>
136
+
137
+ <li><strong>Transformações Sugeridas</strong><br />
138
+ Transformação é mudança de escala matemática (por exemplo log) para melhorar ajuste quando a relação não é linear. O app testa combinações automaticamente e sugere as mais promissoras.</li>
139
+
140
+ <li><strong>Aplicação das Transformações</strong><br />
141
+ O avaliador confirma/ajusta e o app recalcula o modelo já com a configuração escolhida.</li>
142
+
143
+ <li><strong>Dispersão com Transformações e Resíduos</strong><br />
144
+ “Observado” é o valor real que veio nos dados (o que o mercado mostrou na base). “Calculado” é o valor que o modelo estimou. “Resíduo” é a diferença entre os dois. O app mostra isso visualmente para identificar onde o modelo está acertando e onde está errando de forma sistemática.</li>
145
+
146
+ <li><strong>Diagnóstico de Modelo</strong><br />
147
+ Consolida equação, coeficientes, tabelas principais e indicadores num só lugar para revisão técnica.</li>
148
+
149
+ <li><strong>Gráficos de Diagnóstico</strong><br />
150
+ Os painéis ajudam a validar se o modelo está saudável no uso real: Obs x Calc mostra se o estimado acompanha o real; Resíduos mostra se os erros estão equilibrados; Histograma mostra distribuição dos erros; Cook aponta pontos com influência exagerada; Correlação mostra variáveis muito parecidas entre si (risco de redundância). Na prática, o avaliador ganha um “check-up” completo do modelo sem montar análise manual.</li>
151
+
152
+ <li><strong>Analisar Resíduos</strong><br />
153
+ Aprofunda onde o modelo erra mais, em que direção (superestima ou subestima) e em quais regiões/perfis. Com isso, o avaliador entende se o erro é pontual ou estrutural e decide se precisa recalibrar o modelo.</li>
154
+
155
+ <li><strong>Exclusão ou Reinclusão de Outliers</strong><br />
156
+ Grande ganho do app: ao definir regras/limites, ele identifica automaticamente quem está fora do padrão e monta a lista de exclusão para o avaliador decidir. E a recursividade é essencial: quando alguns pontos saem, o modelo é recalculado, o “sarrafo” muda, e registros que antes pareciam normais podem virar discrepantes. O app repete esse ciclo automaticamente até estabilizar, reduzindo muito trabalho manual e aumentando consistência.</li>
157
+
158
+ <li><strong>Avaliação de Imóvel (dentro da elaboração)</strong><br />
159
+ Permite testar o modelo recém-ajustado em cenário real, comparar resultados e checar se ele está pronto para uso operacional.</li>
160
+
161
+ <li><strong>Exportar Modelo</strong><br />
162
+ Gera o <code>.dai</code> final e exporta base tratada, fechando o ciclo com material pronto para governança e reuso.</li>
163
+ </ol>
164
+
165
+ <h2>5. Como a avaliação é feita (em elaboração e em avaliação)</h2>
166
+ <ol>
167
+ <li><strong>Na Elaboração</strong><br />
168
+ Serve para validar o modelo que está sendo construído.</li>
169
+ <li><strong>Na aba Avaliação</strong><br />
170
+ Serve para operação diária, inclusive comparação entre cenários/modelos.</li>
171
+ </ol>
172
+
173
+ <p>Em ambos, o app automatiza etapas críticas:</p>
174
+ <ol>
175
+ <li>valida entrada por tipo de variável,</li>
176
+ <li>detecta extrapolação por variável,</li>
177
+ <li>calcula estimado, faixa de arbítrio, intervalo de confiança, amplitude e graus de qualidade,</li>
178
+ <li>compara cenários com base definida,</li>
179
+ <li>exporta resultados.</li>
180
+ </ol>
181
+
182
+ <h2>6. Repositório: como os modelos são geridos</h2>
183
+ <ol>
184
+ <li>Centraliza modelos oficiais da operação.</li>
185
+ <li>Todos consultam e abrem modelos.</li>
186
+ <li>Admin inclui/exclui modelos.</li>
187
+ <li>Substituição exige confirmação quando há conflito.</li>
188
+ <li>Exclusão exige confirmação forte para evitar erro.</li>
189
+ <li>Metadados do catálogo aceleram decisão e governança.</li>
190
+ </ol>
191
+
192
+ <p>Resultado executivo: o app transforma análise que seria dispersa e manual em um fluxo guiado, comparável e auditável, com mais velocidade para o avaliador e mais controle para a gestão.</p>
193
+ </main>
194
+ </body>
195
+ </html>