Spaces:
Running
Running
Guilherme Silberfarb Costa commited on
Commit ·
da3ac65
1
Parent(s): 303655d
melhorias gerais
Browse files- README.md +5 -0
- backend/app/core/elaboracao/charts.py +10 -1
- backend/app/core/elaboracao/core.py +28 -4
- backend/app/core/elaboracao/formatadores.py +79 -2
- backend/app/core/elaboracao/modelo.py +1 -1
- backend/app/core/{dados/Bairros_LC12112_16.shp → pesquisa/modelos_dai/MOD_V_TER_GENERICO_2016_2026_001.dai} +2 -2
- backend/app/core/visualizacao/app.py +10 -3
- backend/app/services/audit_log_service.py +16 -5
- backend/app/services/elaboracao_service.py +31 -1
- backend/app/services/model_repository.py +17 -5
- backend/app/services/pesquisa_service.py +10 -8
- frontend/src/components/AvaliacaoTab.jsx +293 -10
- frontend/src/components/ElaboracaoTab.jsx +28 -0
- frontend/src/styles.css +56 -0
- resposta_avaliacao.html +195 -0
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] = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2316 |
else:
|
| 2317 |
qtd_extrapolacoes_dentro_limites += 1
|
| 2318 |
-
extrapolacoes[col] = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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] = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2330 |
else:
|
| 2331 |
qtd_extrapolacoes_dentro_limites += 1
|
| 2332 |
-
extrapolacoes[col] = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 593 |
elif ext.get("valor_invalido"):
|
| 594 |
celula = f'{val_fmt} \u274c'
|
| 595 |
else: # grave
|
| 596 |
-
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 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 =
|
| 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:
|
|
|
|
| 888 |
|
| 889 |
for col in candidatos:
|
| 890 |
serie = df[col]
|
| 891 |
-
for valor in serie.dropna()
|
| 892 |
texto = str(valor).strip()
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
|
|
|
| 897 |
|
| 898 |
-
return sorted(bairros
|
| 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 `
|
| 42 |
if (status === 'dicotomica' || status === 'codigo_alocado' || status === 'percentual') return '—'
|
| 43 |
return status
|
| 44 |
}
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
function corGrau(valor) {
|
| 47 |
-
const
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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('&', '&')
|
| 71 |
+
.replaceAll('<', '<')
|
| 72 |
+
.replaceAll('>', '>')
|
| 73 |
+
.replaceAll('"', '"')
|
| 74 |
+
.replaceAll("'", ''')
|
| 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(' │ ') : '-'
|
| 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>
|